【C语言】模块划分、编译器工作原理

发布时间 2024-01-13 14:36:52作者: hzyuan

模块划分

在实际应用中,一个较大的 C 程序并不会把所有代码都放入 main 主函数中,而是进行模块化设计,每个程序模块作为一个源程序文件,再由若干源程序文件组成一个 C 程序。这样处理便于分别编写、分别编译、进而提高调试效率。

#include <stdio.h>

void func1(){ //函数声明并定义
    printf("hello\n");
}

int func2(int i); //函数声明

int main() {
    func1(); //函数调用
    int i = func2(3); //函数调用
    printf("%d\n",i);
    return 0;
}

int func2(int i) { //函数定义
    printf("%d\n",i);
    return i+2;
}

现在将上述代码拆分为两个源程序文件 func.c 和 main.c,同时添加一个头文件 func.h。
一般情况下在 .h 文件中进行变量、函数和宏的声明,在 .c 文件中进行变量和函数的具体实现。

func.h 文件如下:

#include <stdio.h>

int func2(int i); 
void func1();

func.c 文件如下:

#include "func.h"

void func1(){ 
    printf("hello\n");
}

int func2(int i) {
    printf("%d\n",i);
    return i+2;
}

main.c 文件如下:

#include "func.h"

int main() {
    func1(); //函数调用
    int i = func2(3); //函数调用
    printf("%d\n",i);
    return 0;
}
Tips:
#include <> 是从 C 语言标准库中查找头文件;
#include "" 是从源代码所在目录中查找头文件,一般自己编写的头文件用引号。

ps:为什么经常见 xx.c 中 include 对应的 xx.h?因为如果 .c 中的函数也需要调用同个 .c 中的其他函数,那么这个 .c 往往会 include 同名的 .h,这样就不需要为声明和调用顺序而发愁。这已经成为了一种代码规范。

编译器工作原理

  1. 预处理阶段:实际上是处理 define 和 include 等宏命令,进行宏替换。例如 #include "xx.h" 就是把这一行删掉,把 xx.h 中的内容原封不动地插入在当前行位置。
  2. 编译阶段:以 .c 文件为基本单位进行。这一阶段为所有 .c 文件中的变量、函数分配空间,并将各个全局变量、函数进行符号描述,编译、汇编成二进制码从而生成 .o 目标文件。(这一阶段并不关心变量的具体定义和函数的具体实现,只要存在变量和函数的相关声明即可通过编译)
  3. 链接阶段:以 .o 文件为基本单位进行,主要的工作是重定位各个目标文件的函数、变量,生成可执行文件。这个过程主要是找到变量和函数的具体定义和实现。

注意点

注意点一:C是否支持函数重载?

函数重载是函数的一种特殊情况:
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

C语言是不支持函数重载的。因为编译的时候,两个重载函数,函数名相同,一个名字对应多个地址,在 func.o 中的符号表中表示歧义,链接时也存在冲突,所以不支持。

如果有需求,可以用static关键字修饰其中一个函数,把该函数限制在其所在文件里。

拓展:C++引入了函数名修饰规则,用修饰过的函数名去符号表中匹配或者查找,保证不冲突。

注意点二:舍弃 .h 文件?

如果舍弃 .h 文件,在 .c 文件中包含变量、函数的声明以及实现,这时候其他 .c 文件用 #include 去包含这个 .c 文件就会出现问题。
func.c 文件:

#include <stdio.h>

void func1();

void func1(){
    printf("hello\n");
}

main.c 文件:

#include "func.c"

int main() {
    func1();
    return 0;
}


链接过程会出现 func1 函数重复定义的错误,因为编译生成的 main.o 和 func.o 中都有 func1 函数的具体实现。
可以对 main.c 做如下修改:

void func1();

int main() {
    func1(); 
    return 0;
}

如此编译后生成的 main.o 文件中就不再包含 func1 函数的实现。但是如果 func.c 中的函数很多,以及调用这些函数的 .c 文件也很多,这样在每个 .c 文件中手动挨个添加这些函数的声明就十分繁琐。.h 文件就是为了声明函数和变量方便而诞生的,将所有的变量和函数声明都写在 .h 文件中,其他 .c 文件只需 #include 相应的 .h 文件即可。

注意点三:.h 文件中 #ifndef、#define和#endif

.h 文件中 #ifndef、#define和#endif的作用是防止头文件在一个 .c 文件中被重复包含。
a.h 文件:

struct Node{
    int val1;
    int val2;
};

b.h 文件:

#include "a.h"

c.h 文件:

#include "a.h"

main.c 文件:

#include "b.h"
#include "c.h"

int main() {
    return 0;
}

a.h 中的内容被两次复制到 main.c 中,产生 struct Node 重复定义错误。
修改 a.h 内容:

#ifndef _A_H
#define _A_H

struct Node{
    int val1;
    int val2;
};

#endif

那么 a.h 在 main.c 中只会被替换一次,避免了头文件的重复包含。