C语言多文件编程分析

一、C语言的可执行文件的构建机制

从人编写的.c文件(源文件)到CPU能看懂的可执行文件,需要经过很多步,不能直接生成。

  1. 预处理.c文件经过预处理,决定了哪些编译,哪些不编译。
  2. 编译:把预处理后的文件进行分析及优化,生成汇编代码文件。
  3. 链接:组合+确定地址。配合OS的一些初始化文件、第三方库以及其他的一些目标文件。
    • 组合:看整体是否缺失内容、是否有重名内容
    • 确定地址:把每个目标按照一定顺序排列,然后加上基地址,最终可执行文件确定了每个资源的真正的地址。
  4. 生成可执行文件
.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也可以用

解决方案

  1. 换名字,每个项目组都加个前缀。C++的解决办法是加了命名空间namespace的语法。
  2. 让该函数的使用权限(范围)只在本文件内有效——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. 头文件里的内容

只写声明,不写实现。同时,为了保证头文件在编译过程中只被读取一次,那么所有的头文件都要做防止重复定义的保护措施

  1. #pragma once这是C++的用法
  2. #ifndef...#define...#endifC语言原创,为了向下兼容,现代编译器通常都是这么写。
#ifndef A_H // 自己取的名字
#define A_H
文件内容
#endif

四、static关键字的作用

  1. static修饰局部变量,将局部变量从默认的栈空间中,转移到数据段
  2. static修饰全局变量,将这个全局变量访问限制在本文件内
  3. static修饰全局函数,将这个函数访问限制在本文件内

SUFE大二在读