C语言多文件编程分析
一、C语言的可执行文件的构建机制
从人编写的.c
文件(源文件)到CPU能看懂的可执行文件,需要经过很多步,不能直接生成。
- 预处理:
.c
文件经过预处理,决定了哪些编译,哪些不编译。 - 编译:把预处理后的文件进行分析及优化,生成汇编代码文件。
- 链接:组合+确定地址。配合OS的一些初始化文件、第三方库以及其他的一些目标文件。
- 组合:看整体是否缺失内容、是否有重名内容
- 确定地址:把每个目标按照一定顺序排列,然后加上基地址,最终可执行文件确定了每个资源的真正的地址。
- 生成可执行文件
.c ==预处理==> 决定了哪些编译,哪些不编译 ==编译==> .o(不可直接执行,是相对地址) .obj + 组合、确定地址 ===>可执行文件
C语言的入口是main
,而程序的入口是系统依赖库_start
,它做了诸如初始化系统、加载main函数等事情。
nm
命令:查看二进制文件中的符号表。例如:nm a.o
。
gcc -c -o a.o a.c
: 由.c
文件生成.o
文件。
二、多文件分模块下常见链接错误
1. 少东西
少东西一定会看到类似下面的东西:
undefined reference to `funB' // 找不到funB的标签
collect2.exe: error: ld returned 1 exit status // 看到ld就要知道是链接出错
解决方案:检查目标文件是否都参与链接
2. 多东西
multiple definition of `show' // 链接器发现了多个show标签
原理:
// 函数默认都是extern,因为写得太多,所以省略了。只要是在函数外面写的,不管是变量还是函数,都是全局的。
void show(); // extern void show();等价。在C语言中,加extern与不加是等价的。
extern void show(); // 这一句的意思是:我的外部有一个全局标签叫show,这个标签是一个函数的地址,其他.c也可以用
解决方案:
- 换名字,每个项目组都加个前缀。C++的解决办法是加了命名空间
namespace
的语法。 - 让该函数的使用权限(范围)只在本文件内有效——加
static
关键字。
在多团队协作开发的时候,如果程序员设计了一个函数,而且这个函数不会被其他项目组调用,那么最好都加上static
进行约束,避免最后各团队进行链接时发生名字冲突的情况。
三、头文件的含义
1. 引例
C语言函数的调用是直接翻译成调用一个函数名的标签,如果这个函数需要接收参数,但此时C编译器并不知道这个函数声明形式,那么编译器会采用根据调用行为来执行这个函数的调用。
// 在a.c文件中,没有定义fun。那么此时调用行为是fun(),编译器就会当成没有参数传入的函数;如果是fun(10),那么编译器会认为是传入int这种的。
int main(){
printf("%d",fun());
}
C语言的编译是只针对一个.c
文件的,而.c编译时,会调用很多函数,每个函数如果不声明,那么编译器在编译时就不清楚函数的具体参数行为,只能按照默认行为操作。所以要在调用函数前对这个函数进行声明。
但是,如果其他函数是其他团队开发的,会出现许多问题。于是,C语言提出了解决办法:让每个团队把能够对外公布的接口、数据类型等,都放在一个文件里,并引入了预处理器#include
语法。
#include
语法:把include
后面写的文件打卡,读取里面的内容,展开在当前的位置。
2. 头文件里的内容
只写声明,不写实现。同时,为了保证头文件在编译过程中只被读取一次,那么所有的头文件都要做防止重复定义的保护措施。
#pragma once
这是C++的用法#ifndef...#define...#endif
C语言原创,为了向下兼容,现代编译器通常都是这么写。
#ifndef A_H // 自己取的名字
#define A_H
文件内容
#endif
四、static关键字的作用
- static修饰局部变量,将局部变量从默认的栈空间中,转移到数据段
- static修饰全局变量,将这个全局变量访问限制在本文件内
- static修饰全局函数,将这个函数访问限制在本文件内