C项目的文件组织和编译
C项目的代码, 由头文件(.h后缀)和C文件(.c后缀)组成
- C语言的函数和变量, 分
声明
和定义
两个阶段 - 头文件和C文件是等价的, 相当于C文件的一部分, 其功能由人为划分, 用于变量和函数的
声明
, 头文件也可以用于变量和函数的定义
, 但是这属于非标准用法, 一般不这么用 - 同一个编译中, 函数在一处定义, 处处可用(除非使用
static
关键字)- 在A.c中定义后, 在B.c中用extern声明这个函数, 就可以调用
- 将A.c中的函数声明提取到A.h, 在B.c中include A.h, 或者通过B.c include B.h, B.h include A.h, 都可以实现函数引用
- C的编译, 是按文件编译的, 每个C文件会编译为一个目标文件
- 头文件不单独编译, 与include这个头文件的C文件, 在预编译阶段展开, 之后在C文件中编译
- 编译需要知道C文件的列表和头文件的目录列表
- 编译会依次编译C文件列表中的每个文件, 不管最终是否用到
C项目结构示例
定义一个头文件 inc.h,声明两个函数func1和func2, 将定义写在func1.c和func2.c. 在main.c中通过main.h引用inc.h, 调用这些函数, 程序目录结构如下
├── inc
│ ├── func1.c
│ ├── func2.c
│ └── inc.h
├── main.c
├── main.h
└── obj
main.c
#include <stdio.h>
#include "main.h"
int main()
{
uint8_t a = 0x08;
uint8_t b = func1(a);
printf("%X", b);
return 0;
}
main.h
#ifndef MAIN_H
#define MAIN_H
#include "inc.h"
#endif
inc.h
#ifndef INC_H
#define INC_H
typedef unsigned char uint8_t;
uint8_t func1(uint8_t a);
uint8_t func2(uint8_t a);
#endif
func1.c
#include "inc.h"
uint8_t func1(uint8_t a)
{
a = a << 1;
return a;
}
func2.c
#include "inc.h"
uint8_t func2(uint8_t a)
{
a = a >> 1;
return a;
}
gcc的编译过程
gcc命令其实依次执行了四步操作
- 预处理(Preprocessing),
- 编译(Compilation),
- 汇编(Assemble),
- 链接(Linking)
1.预处理(Preprocessing)
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,你可以通过如下命令对 main.c进行预处理:
gcc -E -I./inc main.c -o obj/main.i
# or
$ cpp main.c -I./inc -o obj/main.i
-E
是让编译器在预处理之后就退出,不进行后续编译过程; -I
指定头文件目录, -o
指定输出文件名.
经过预处理之后代码体积会大很多, main.c只有10行, 但是main.i有749行, 预处理之后的文件可以用文本编辑器查看
2.编译(Compilation)
这一步的编译将经过预处理之后的程序转换成特定汇编代码的过程, 编译的命令如下:
$ gcc -S -I./inc main.c -o obj/main.s
-S
让编译器在编译之后停止. 这一步会生成程序的汇编代码, 内容如下:
.file "main.c"
.text
.section .rodata
.LC0:
.string "%X"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movb $8, -2(%rbp)
movzbl -2(%rbp), %eax
movl %eax, %edi
call func1@PLT
movb %al, -1(%rbp)
movzbl -1(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3.汇编(Assemble)
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生了二进制的目标文件, gcc汇编过程通过as命令完成
as obj/main.s -o obj/main.o
# por
gcc -c obj/main.s -o obj/main.o
这一步需要给每一个源文件产生一个目标文件, 以便后面link
gcc -c -I./inc inc/func1.c -o obj/func1.o
gcc -c -I./inc inc/func2.c -o obj/func2.o
4.链接(Linking)
通过上面的步骤, 在obj目录下已经有main.o, func1.o和func2.o这三个目标文件, 现在需要通过linker将这些目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)
命令如下
gcc -o obj/main obj/main.o obj/func1.o obj/func2.o
这时候在obj目录下就会生成可执行文件main
链接并不会忽略未使用的目标文件
上面的编译产生的main文件大小为16824字节, 不管在main中是否调用了func1或者func2.
如果在link中去掉func2.o (因为main中未调用func2, 所以不会产生错误), 这样产生的main文件为16760字节
gcc -o obj/main obj/main.o obj/func1.o
如果需要减小尺寸, 可以使用 -fdata-sections
-ffunction-sections
-Wl
--gc-sections
-Os
等参数优化. 例如
gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections
头文件, 静态库(.lib, .a) 和动态库(.dll, .so)
静态库 vs 动态库
库文件就是已经预编译好的目标文件, 只需要link到你的程序里就可以用了, 例如常见的方法 printf() and sqrt(). 库文件有两种类型: 静态库和动态库(也叫共享库).
静态库 在Linux下使用扩展名.a
, 在Windows下使用扩展名.lib
, 当link静态库时, 这些对象文件的机器码会被复制到你的可执行文件中.
动态库 在Linux下使用扩展每.so
, 在Windows下使用扩展名.dll
, 当你的程序link静态库时, 只会在你的程序可执行文件中添加一个表, 在运行你的程序之前, 操作系统会将这些外部方法的机器码载入进来. 这种方式可以节约磁盘资源, 让程序更小, 另外大多数操作系统也运行内存中的一份动态库在多个运行的程序中共享. 动态库升级时无需重新编译执行程序.
GCC默认情况下以动态库方式link. 要查看库内容, 可以用命令nm filename
编译中定位包含头文件和库文件 (-I, -L and -l)
当编译项目时, 编译器需要头文件的信息, linker需要库文件解决外部依赖.
对于项目中include的头文件, 编译器会去搜索相应的路径, 这些路径通过 -Idir
参数 ( 或者环境变量 CPATH
) 指定, 因为头文件的文件名是已知的, 所以编译器只需要知道路径.
对于linker, 会去搜索库路径, 这个通过 -Ldir
参数 (大写 'L' 后面是路径) (或者环境变量 LIBRARY_PATH
). 另外你需要指定库名称. 在Unix系统中, 库文件 libxxx.a 通过参数 -lxxx
指定 (小写字符 'l' 不带lib前缀, 不带.a扩展名). 在Windows下, 需要提供文件全名, 例如 -lxxx.lib. 路径和文件名都需要指定.
默认的 Include-paths, Library-paths 和 Libraries
可以通过cpp -v
命令列出:
> cpp -v
......
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-pc-cygwin/6.4.0/include
/usr/include
/usr/lib/gcc/x86_64-pc-cygwin/6.4.0/../../../../lib/../include/w32api
在编译时, 加入-v
参数开启verbose mode, 可以了解系统中使用到的库路径(-L)以及库明细(-l)
> gcc -v -o hello.exe hello.c
......
-L/usr/lib/gcc/x86_64-pc-cygwin/6.4.0
-L/usr/x86_64-pc-cygwin/lib
-L/usr/lib
-L/lib
-lgcc_s // libgcc_s.a
-lgcc // libgcc.a
-lcygwin // libcygwin.a
-ladvapi32 // libadvapi32.a
-lshell32 // libshell32.a
-luser32 // libuser32.a
-lkernel32 // libkernel32.a
Eclipse CDT 在 Eclipse CDT 中, 可以在项目上右键, 点击project ⇒ Properties ⇒ C/C++ General ⇒ Paths and Symbols
, 在标签页"Includes", "Library Paths" and "Libraries"下, 设置 include path, library paths 和 libraries.
GCC环境变量
GCC 使用下列环境变量:
- PATH: 用于搜索可执行文件和运行时的动态链接库(.dll, .so).
- CPATH: 用于搜索头文件包含路径. 优先级低于直接用
-I<dir>
指定的路径.C_INCLUDE_PATH
andCPLUS_INCLUDE_PATH
可分别用于指定C和C++的头文件路径. - LIBRARY_PATH: 用于搜索库文件的路径, 优先级低于用
-L<dir>
指定的路径.