gcc/g++
GCC程序语言编译器自由软件,现在可以编译很多语言,gcc 是GCC中的c编译器,而g++ 是GCC中的c++编译器。
指令格式 :gcc 选项 文件名
公共选项
-o 文件名:指定输出文件名
预处理
作用
- 处理所有#开头的预处理指令
- 头文件包含(
#include
):将目标头文件的内容复制到当前文件中; - 宏定义(
#define
)及宏替换; - 条件编译(
#ifdef
、#ifndef
、#if 0
等);
- 删除注释
- 预编成一个
.i文件
选项
-E
只进行预处理-I路径
:添加头文件搜索路径,可多次使用-D宏名\[=值]
,常用于控制条件编译
gcc -DDEBUG -DVERSION=1.0 -E main.c -o main.i -I./include
编译
此步最耗时,因为要检查语法语义错误,对于这些控制选项也是最多的
作用
- 进行语法、语义检查
- 生成平台相关的汇编代码
.s文件
选项
-S
只进行到编译-O0/O1/O2/O3
优化级别递增,默认O0不优化-Wall
启用大多数警告,默认显示优先级最高的警告-w
禁止所有警告-g
生成调试信息,用于gdb调试-std=标准
指定语言标准,如c11
汇编
作用
- 将汇编代码转换为机器指令获得目标文件.o
选项
- -c 只进行到汇编
链接(静态)
作用
本质就是对主文件中的调用函数填上对应地址,查找函数地址顺序:主文件-->显示指定二进制文件-->显示指定库文件-->标准库中文件
- 把多个二进制的目标文件链接成一个单独的可执行文件
- 找到依赖的库文件(静态与动态)
选项
-L
解决"库文件在哪里"的问题(系统标准库不需要指定),-l
解决"需要哪些库符号"的问题(涉及到库的都需要指定),一般第三方库需要两者配合使用
-L目录
添加库文件搜索路径-l库名
链接指定库,如-lpthread
,注意这里是小写的l
预处理器
对于#include,就是粘贴复制头文件中代码
obj单定义规则: 全局变量和函数只能有一个定义,类、模板以及内联函数可以在多个翻译单元中有完全相同的定义,所以不要将函数和变量定义放在头文件中。
代码高知h和.cpp应该成对出现
使用双引号时,预处理器知道这是编写的头文件。预处理器首先在当前目录中搜索头文件。如果找不到匹配的头文件,将搜索系统目录。
没有.h扩展名头文件,声明了std命名空间中的所有标识符,使用标准库头文件时,优先使用不带.h扩展名的版本,即std命名空间中的标识符,这也是为什么引入string之后,却使用std::string的原因
当引入其他目录下的头文件时,尽量别使用相对路径,而是更改单个编辑器的设置,g++ -o main -I/source/includes main.cpp,-I后面没有空格
对于ifdef、ifndef、endif,只对单文件使用
#if 0 可用于注释
对于#define,进行简单的文本替换
链接器
链接器将程序中的变量、函数等符号(Symbols) 转换为可执行文件中的内存地址
- 符号地址的分配方式
- 静态链接(非PIC) : 链接器为符号分配绝对虚拟地址 (如
0x400520
),这些地址在程序加载时固定不变。 示例 :main
函数可能位于0x401000
,其他函数按编译顺序依次排列。 - 动态链接(PIC) : 使用位置无关代码 时,符号地址为相对于加载基址的偏移 ,通过全局偏移表GOT动态计算实际地址。
- 动态库函数的处理(PLT/GOT) 动态库函数(如
printf
)的调用通过以下机制实现:
-
PLT(过程链接表) : 首次调用
printf@plt
时,PLT跳转到动态链接器(ld-linux.so
)解析函数真实地址,并更新到GOT中。 -
GOT(全局偏移表) : 存储动态库函数的实际内存地址,后续调用直接跳转到GOT中的地址,避免重复解析。
C// 首次调用触发地址解析 printf("Hello"); // 编译为 call printf@plt
- 未解析符号的报错规则
- 静态链接阶段 : 若符号未在目标文件(
.o
)或静态库(.a
)中定义,直接报错undefined reference
。 - 动态链接阶段 : 若动态库缺失或符号未找到(如运行时
libfoo.so
未安装),程序加载时报错cannot open shared object file
。
初始化列表
定义
定义变量时同时提供初始值,提供初始值的方式有:
cpp
int a;//默认初始化,未赋值不要直接打印
int b = 2;//拷贝初始化
int c(3);//直接初始化
//列表初始化(统一初始化)
int d{};
int e = {3};
int f{ 2 };
推荐使用列表初始化,这样对于数组也是统一的,而且不允许大类型转小类型,比如使用int a = {2.5};
是会报错的,而使用int a = 2.5;
就会默认丢失,此时a为2。
库
库本质是 目标文件(
.o
)的集合,即一堆函数的集合,但是去掉了编译(预处理编译汇编)过程(最耗时的部分)
静态库
有两个文件:头文件和库文件.a/.lib,库文件(二进制文件)提供函数的具体实现,链接时会查找库文件,将未描述的符号写入目标文件
制作lib库名.a
- 将.c生成.o文件
gcc -c add.c -o add.o
- 使用ar工具制作静态库
ar rcs lib库名.a add.o sub.o div.o
使用
- 源代码中引入库的头文件
- 编译静态库到可执行文件中
gcc test.c lib库名.a -o a.out
链接器按 从左到右的顺序 处理输入文件,且只在处理到该库时 检查当前未解析符号,并提取需要的
.o
文件,所以如果将库名和源文件顺序交换,则会报错,流程:
- 先处理库文件,此时没有任何未解析符号,因为没有编译test.c
- 连接器认为不需要该库中任何内容,跳过整个库
- 再编译test.c生成test.o,并发现存在未解析的函数(即找不到定义)
- 查找标准库,并将未解析的函数填上地址,如printf()
- 发现最终依然存在未解析的函数,报错
动态库
动态库(
.so
或.dll
)在编译时不会被完全链接到可执行文件中,而是在程序运行时发现少了该库,如果该库没有转载到内存,则会转载到内容并获得所需符号的地址。动态库的代码(函数入口地址)可以被多个程序共享,减少内存占用,且更新库文件无需重新编译主程序。
制作 lib库名.so
(Linux)或 库名.dll
(Windows)
-
生成位置无关的目标文件 :
Bashgcc -c -fPIC add.c sub.c div.c # -fPIC 生成位置无关代码(Position Independent Code)
- 输出:
add.o
,sub.o
,div.o
-
将目标文件打包为动态库 :
Bash# Linux gcc -shared -o lib库名.so add.o sub.o div.o # Windows(MinGW) gcc -shared -o 库名.dll add.o sub.o div.o
- 输出:
lib库名.so
(Linux)或库名.dll
(Windows)
使用动态库
-
源代码中引入头文件
-
编译时链接动态库
Bashgcc test.c -o a.out -L. -l库名 -I头文件目录
-
运行时加载动态库
-
Linux :设置
LD_LIBRARY_PATH
环境变量Bashexport LD_LIBRARY_PATH=动态库路径//只会对当前bash生效,且如果关闭后会丢失
-
Windows :将
.dll
文件放在以下任一目录:- 可执行文件所在目录
- 系统目录(如
C:\Windows\System32
) - PATH 环境变量包含的目录
-
编译后生成的可执行文件,如果系统中找不到动态库,会报错