0. 项目结构
/home/pi/test/
├── src/
│ ├── add/
│ │ ├── add.cpp
│ │ ├── add.h
│ └── log/
│ ├── log.cpp
│ ├── log.h
│ ├── data.h
├── main.cpp
main.cpp代码
// main.cpp
#include "log.h"
#include "add.h"
int main() {
return 0;
}
add.cpp和add.h
// add.h
#pragma once
void add(int a, int b);
// add.cpp
#include "log.h"
#include "add.h"
void add(int a, int b) {
log_function(a+b);
}
log.cpp和log.h
// data.h
#pragma once
struct LogData{
int logData;
};
// log.h
#pragma once
void log_function(int res);
// log.cpp
#include <iostream>
#include "log.h"
#include "data.h"
void log_function(int res) {
std::cout << res << std::endl;
}
使用gcc将上面的项目编译成一个二进制文件。
1. 编译阶段
GCC支持分离式编译(Separate Compilation),这是C/C++语言开发中管理大型项目的核心机制。其本质在于将编译过程拆分为独立编译目标文件和统一链接两个阶段。使用 -c 选项时,GCC仅执行预处理、编译、汇编三个阶段,生成.o目标文件。例如:
gcc -c main.c -o main.o # 生成未链接的二进制目标文件
- 每个源文件独立编译,互不干扰
- 输出文件包含机器码和未解析符号表(函数/变量引用)
1.1 构建每个cpp的目标文件
由于是分离式编译的,我们直接对每个cpp文件执行gcc命令即可
gcc -c main.cpp -o main.o
gcc -c log.cpp -o log.o
gcc -c add.cpp -o add.o
编译的时候报错,说是找不到头文件。仔细想想,确实没有告诉gcc去哪里找这个头文件
gcc -c main.cpp -o main.o
main.cpp:1:10: fatal error: log.h: No such file or directory
1 | #include "log.h"
| ^~~~~~~
1.2 头文件搜索
在C/C++开发中,GCC编译器对头文件的搜索路径遵循特定规则,其顺序因包含方式(#include<>或#include"")和编译参数的不同而变化。
- 双引号包含(#include “header.h”)优先搜索当前源文件所在目录,如果是尖括号包含(#include <header.h>),则不会搜索当前源文件下的所在目录
- 通过-I参数显式指定的路径(按命令行中的书写顺序)
- 环境变量C_INCLUDE_PATH(C语言)或CPLUS_INCLUDE_PATH(C++)定义的路径
- 系统默认路径:/usr/local/include、/usr/include、GCC版本相关路径(如/usr/lib/gcc/x86_64-linux-gnu/9/include),平台架构特定路径(如/usr/include/x86_64-linux-gnu)
1.3 为每个cpp指定头文件搜索路径
由于每个目标文件是单独生成的,每个目标文件搜索的头文件也可以单独指定了
gcc -c main.cpp -o main.o -I src/add -I src/log
指定-I src/add
可以从src/add
目录下搜索到add.h
文件,指定-I src/log
可以从src/log
目录下搜索到log.h
文件,然后执行add.cpp
的构建
gcc -c add.cpp -o add.o -I src/log
cc1plus: fatal error: add.cpp: No such file or directory
发现找不到add.cpp文件,我们需要进入到src/add
目录下才能找到
cd src/add
gcc -c add.cpp -o add.o -I src/log
add.cpp:1:10: fatal error: log.h: No such file or directory
1 | #include "log.h"
| ^~~~~~~
但是发现找不到log.h
文件,明明已经指定了log的头文件搜索路径呀,为啥会不对呢?这是因为我们-I src/log
其实指定的是相对路径
1.4 相对路径搜索
所谓的相对路径,其实都是相对于工作目录而言的,啥是工作目录呢?就是gcc执行命令的目录。例如我们是进入到src/add
目录下执行的gcc命令,那么工作目录就是/home/pi/test/src/add
,-I src/log
目录其实就变成了/homg/pi/test/src/add/src/log
(工作目录/相对目录)下搜索log.h
文件,很明显不存在。
一种办法从工作目录定位到其他目录,此时-I ../../src/log
其实就是/homg/pi/test/src/add/../../src/log
,也就是/home/pi/test/src/log
。
cd src/add
gcc -c add.cpp -o add.o -I ../../src/log
另一种办法就是使用绝对路径,但是当你把项目挪到其他地方编译时,就需要把-I
所有的路径都改一下。
cd src/add
gcc -c add.cpp -o add.o -I /home/pi/test/src/log
最常用的办法就是直接指定add.cpp
的路径,还是在test
目录下编译
gcc -c src/add/add.cpp -o src/add/add.o -I src/log
gcc -c src/log/log.cpp -o src/log/log.o
还可以发现,编译log.cpp
的时候并没有指定log.h
和data.h
的路径,因为默认会从当前cpp的路径下搜索相应的头文件。
我猜测,当我们-c src/log/log.cpp的时候,其实相当于隐含的指定了src/log的搜索路径
1.5 统一指定搜索路径
gcc main.cpp src/log/log.cpp src/add/add.cpp -o main
当同时编译多个文件的时候,其实还是单个单个文件编译的,相当于
gcc -c main.cpp -o main.o
gcc -c src/log/log.cpp -o log.o
gcc -c src/add/add.cpp -o add.o
可以看到编译main.cpp的时候并没有指定log.h
和add.h
的搜索路径,依然会报错。通过添加-I src/log -I src/add
解决
gcc main.cpp src/log/log.cpp src/add/add.cpp -o main -I src/log -I src/add
但这其实就变成了每个cpp文件都使用了统一的头文件搜索路径,例如log.cpp我们并不需要指定-I src/add
的搜索路径。慎用!!!
gcc -c main.cpp -o main.o -I src/log -I src/add
gcc -c src/log/log.cpp -o log.o -I src/log -I src/add
gcc -c src/add/add.cpp -o add.o -I src/log -I src/add
1.6 编译阶段不验证符号定义
上面的命令其实会报错,我们生成二进制时会将所有的目标文件.o合并成最终的可执行文件。gcc是编译c语言的,但是我们使用了std::cout
这个函数,这个是c++语言的,在链接的时候gcc从c语言中找不到这个函数,所以报错。
pi@raspberrypi:~/test $ gcc main.cpp src/log/log.cpp src/add/add.cpp -o main -I src/log -I src/add
/usr/bin/ld: /tmp/cc53TCMJ.o: in function `log_function(int)':
log.cpp:(.text+0x14): undefined reference to `std::cout'
/usr/bin/ld: log.cpp:(.text+0x18): undefined reference to `std::cout'
/usr/bin/ld: log.cpp:(.text+0x1c): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
/usr/bin/ld: log.cpp:(.text+0x20): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)'
/usr/bin/ld: log.cpp:(.text+0x24): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)'
/usr/bin/ld: log.cpp:(.text+0x28): undefined reference to `std::ostream::operator<<(std::ostream& (*)(std::ostream&))'
/usr/bin/ld: /tmp/cc53TCMJ.o: in function `__static_initialization_and_destruction_0(int, int)':
log.cpp:(.text+0x6c): undefined reference to `std::ios_base::Init::Init()'
/usr/bin/ld: log.cpp:(.text+0x80): undefined reference to `std::ios_base::Init::~Init()'
/usr/bin/ld: log.cpp:(.text+0x84): undefined reference to `std::ios_base::Init::~Init()'
collect2: error: ld returned 1 exit status
问题是为啥编译的时候(gcc -c)不会报错呢?gcc -c 的作用是生成目标文件(.o),仅执行预处理、编译和汇编,不进行链接。在此阶段:
- 语法检查:GCC 检查代码的语法合法性(如括号匹配、分号缺失等)。
- 符号声明验证:确认函数调用是否符合已声明的原型(如参数类型、返回值类型)。
- 不验证符号定义:编译器不会检查函数是否在其他文件中定义,也不会解析外部符号的地址
例如以下代码:
// main.c
void func(); // 声明但未定义
int main() {
func(); // 调用未定义的函数
return 0;
}
执行 gcc -c main.c 时,编译器仅确认 func 的声明存在,但不会检查其是否实现,因此不会报错。
1.7 编译阶段的符号表作用
1.7.1 标识和区分程序中的实体
在程序代码中存在各种实体,如变量、函数等。符号表会为每个实体分配一个独一无二的标识符,比如变量int a;,其对应的标识符就是a。通过这个标识符,编译器能够区分不同的变量、函数,避免混淆。
符号表还能记录这些实体的作用域。例如,在函数f()中定义的局部变量b,其作用域仅限于f()函数内部,符号表可以帮助编译器明确这一点。
1.7.2 进行类型检查
代码中可能会存在类型错误,比如将整数赋值给指针变量。符号表存储了每个变量和函数的类型信息 。例如,如果变量x为double类型,函数func的返回类型是int。当代码中出现x = func();这样的语句时,编译器可以借助符号表中记录的类型来判断这个赋值操作是否合法,若类型不匹配,就发出警告或报错。
1.7.3 名字解析
当函数调用另一个函数,或者在某个作用域中引用其他作用域的变量(如全局变量被局部作用域引用)时,编译器需要通过符号表来寻找对应的实体。例如,在函数main()中调用函数g()。编译器首先会在符号表中查找g()函数的定义,找到后才能确定其参数列表、返回类型等信息,进而生成正确的调用指令。
1.8 链接阶段的符号表作用
1.8.1 帮助符号解析和地址分配
在链接过程中,多个对象文件(由源文件经编译生成)需要被整合到一个可执行文件中。不同对象文件内部可能引用了其他对象文件中定义的符号(如函数或全局变量) 。符号表提供了一个全局索引来定位这些被引用的符号。
例如,假设在对象文件file1.o中有对函数h()的引用,而函数h()是在对象文件file2.o中定义的。链接器通过查看两个对象文件的符号表来确定符号h在file2.o中的具体位置,并且在最终的可执行文件中为h分配正确的地址,以便在运行时正确地跳转到函数h()的代码处。
1.8.2 支持重定位操作
在生成可执行文件之前,链接器需要将对象文件中的数据和代码重定位到合适的内存地址位置。符号表中的信息(如符号的偏移地址等)对于计算重定位后的地址至关重要。比如,全局变量y在对象文件的符号表中记录了其在该对象文件中的相对偏移地址,链接器结合这个偏移地址和程序的加载基地址,就能计算出变量y在最终可执行文件中的准确地址。
1.9 调试和运行阶段的符号表作用
1.9.1 调试工具的基础
当开发者使用调试器(如 GDB)调试程序时,符号表提供了调试所需的关键信息。调试器可以通过符号表来反向定位代码中的变量和函数,从而允许开发者在运行时查看变量的值、对函数
1.9.2 进行单步执行等操作。
例如,开发者设置断点在函数k()的入口处,调试器依据符号表找到函数k()在可执行文件中的地址,当程序运行到该地址时就会暂停。开发者还可以通过符号表查询函数k()内部的局部变量名称和地址,方便查看这些变量的运行时数据。
1.9.3 动态符号表在运行时的作用
对于动态链接的程序,运行时加载的共享库(如.so文件)中的符号也需要通过符号表来解析。当程序调用共享库中的函数时,运行时环境(如动态链接器)利用共享库的符号表来找到函数的正确入口点,确保程序能够正确访问这些外部库的功能。