GCC、G++
编译过程
c/c++编译会经历如下过程:
- 预编译:将
.c
和.h
文件预编译为.i文件,删除注释,将头文件引用内容汇总到一个文件,宏展开也在这一阶段。 - 编译:将临时文件
.i
文件编译为汇编代码.s
文件,如果有语法错误会输出。 - 汇编:将汇编代码
.s
文件翻译为机器指令.o
文件。 - 链接:将需要的库文件
.lib
和机器指令文件.o
链接起来得到可执行文件.out
或.exe
。
GCC和G++的区别
GCC(GNU Compiler Collection,GNU编译器集合)是一个集成了预编译,编译,汇编,链接的编译工具,它支持c/c++/fortran/java
等语言的编译。
G++(GUN C++ complier),与GCC类似,是一个c/c++的编译器,主要区别如下:
- G++在编译c和c++时会自动连接STL标准库,GCC则需要手动加-lstdc++这个flag。
- G++在编译.c和.cpp文件时,统一当做cpp文件处理,可以使用
extern "C"
来让G++按C的方式进行调用,GCC对于C和C++是分开处理的。 - G++在编译c和cpp时,都会加上一些宏,而GCC只会在编译cpp时加。
#define__GXX_WEAK__ 1
#define __cplusplus 1
#define __DEPRECATED 1
#define __GNUG__ 4
#define __EXCEPTIONS 1
#define __private_extern__ extern
GCC/G++工具使用
GCC包括三个主要部分
- gcc-core:即GCC编译器
- Binutils:一些二进制工具,位于/usr/bin文件下,一般有gcc程序调用,也可以手动调用。
- as汇编器:将汇编语言代码转为机器码.o文件。
- ld链接器:将编译生成的多个目标文件组织成最终的可执行程序。
- readelf:查看目标文件或可执行程序的文件信息。
- nm:查看目标文件的符号。
- size:查看目标文件代码段、数据段、静态区等的尺寸。
- objcopy:目标文件格式转换,如bin转elf等。
- objdump:查看目标文件信息,反汇编。
- glibc:使GUN实现的c标准库,提供诸如read、write、open、printf等这些函数的实现。
gcc/g++命令行参数
gcc和g++参数都是一样的,可以通过gcc/g++ --help
查看具体参数
参数 | 说明 | 实例 |
---|---|---|
-save-temps | 保持各个阶段产生的中间文件 | gcc -save-temps -o hello hello.c |
-o | 各个阶段输出文件 | gcc -o hello hello.c |
-E | 仅预编译 | gcc -E hello.c -o hello.i |
-S | 执行到编译阶段 | gcc -S -o hello.s hello.c |
-c | 执行到汇编阶段 | gcc -c -o hello.o hello.c |
-std=<standard> | 指定输入文件的standard | gcc -o hello -std=c++11 hello.cpp |
-Wp,<options> | 指定preprocesser阶段的选项 | |
-Wa,<options> | 指定(汇编)assembler阶段的选项 | |
-Wl,<options> | 指定链接阶段的选项 | |
-shared | 创建共享库 | |
–static | 使用静态链接(gcc默认是动态链接) | gcc hello.c -o hello_static --static |
-g | 生成带debug信息的可执行程序 | gcc -g hello.c -o hello_debug |
交叉编译
# 安装arm gcc交叉编译工具
sudo apt-get install gcc-arm-linux-gnueabihf
# 查看版本
arm-linux-gnueabihf-gcc -v
# 编译过程和本地编译没有区别
arm-linux-gnueabihf-gcc hello.c -o hello_arm
# 使用readelf查看目标信息
readelf -a hello_arm
make和makefile
make是一个批量构建工具,类似于shell脚本但是比shell智能得多,通过makefile描述的编译构建规则来构建需要的目标,构建过程包括但不限于目标编译、打包等。
- 它能根据makefile来自动建立依赖,并根据依赖决定构建顺序
- 能够根据修改的文件来决定哪些组件需要重新构建,而不是重新构建所有组件。
make命令参数
命令参数 | 长命令 | 说明 | 实例 |
---|---|---|---|
-f filename | –file --makefile | 指定makefile文件 | make -f makefile.txt |
-C dir | –directory=dir | 执行器先切到dir目录 | |
-I dir | –include-dir | 在dir中搜索被包含的makefile | |
-L | –check-symlink-times | 如果是软连接,使用软连接和连接目标中修改时间较晚的一个 ?? | |
-j n | –jobs | 指定同时跑几个任务,一般一个job用一个核心跑 | |
-i | –ignore-errors | 忽略命令返回的错误信息 | |
-k | –keep-going | 当某个目标构建出错是继续构建后续目标 | |
-b -m | - | 忽略兼容性 | |
-B | –always-make | 无条件make所有目标 | |
-d | -debug=[FLAGS] | 打印大量调试信息 | |
-e | –environment-overrides | 覆盖makefile中的环境变量 | make -e USE_GLIB=true |
- s | –silent | silent模式,不会输出相应的命令行信息 | |
- r | –no-builtin-rules | 禁止使用build-in规则 | |
-R | –no-builtin-variables | 禁用内置变量设置 | |
-n | –dry-run – just-print --recon | 非执行模式,打印所有执行命令但不执行 | |
- t | –touch | touch目标而不是重新构建他们 | |
-w | –print-diredotry | 打印当前目录 |
makefile语法
版本一
# makefile中以#开头为注释
# 指定构建目标和规则
# [目标]: 依赖1 依赖2 ...
# 目标下面用tab键(注意不能是空格)表示构建规则开始,后面写构建规则
# 这种方法和手写g++没有什么区别,一旦一个文件改变,就重新执行g++重新构建所有文件
hello: main.cpp bar.cpp foo.cpp
g++ -o hello bar.cpp foo.cpp main.cpp
- make是基于时间来检测是否需要重新构建目标的,当目标的修改时间比后面依赖的修改时间都晚时,就不需要重新构建,一旦有一个文件修改时间晚于目标(表示在目标构建之后修改过),就会重新构建。
- make命令后面如果不指定target,则会查找makefile中的第一个目标,依次往后生成。
- 例如下面的main.cpp修改时间比目标hello晚,此时如果执行make就会重新构建hello
### root@20010e383377:/tmp/workspace/makefile# ls -l
### -rw-r--r-- 1 root root 658 Apr 26 08:03 Makefile
### -rw-r--r-- 1 root root 131 Apr 26 07:51 bar.cpp
### -rw-r--r-- 1 root root 80 Apr 26 07:51 foo.cpp
### -rw-r--r-- 1 root root 74 Apr 26 07:54 header.h
### -rwxr-xr-x 1 root root 17536 Apr 26 08:01 hello
### -rw-r--r-- 1 root root 185 Apr 26 08:03 main.cpp
版本二
这个版本定义了一些变量,并将中间文件的构建分开,这样在改变一个文件时,只需要编译依赖这个文件的中间文件即可,最好再把中间文件链接起来,大大节省了构建时间。
# 可以使用”=“来定义变量
CXX = g++ # 定义编译器变量
TARGET = hello # 定义target变量
OBJ = main.o foo.o bar.o # 定义中间文件变量
# 定义目标构建规则
# 使用”$“和()来引用变量
$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)
# 定义中间文件构建规则
# gcc/g++ -c 表示只执行预编译、编译、汇编三步,不执行链接
main.o: main.cpp
$(CXX) -c main.cpp
foo.o: foo.cpp
$(CXX) -c foo.cpp
bar.o: bar.cpp
$(CXX) -c bar.cpp
执行make
之后
root@20010e383377:/tmp/workspace/makefile# make
g++ -c main.cpp
g++ -c foo.cpp
g++ -c bar.cpp
g++ -o hello main.o foo.o bar.o
这个版本的问题是,每个.cpp
文件的编译规则还是需要手动写,有没有办法能自动完成呢?看下面的版本。
版本三
CXX = g++
TARGET = hello
OBJ = main.o foo.o bar.o
CXXFLAGS = -c -Wall # 定义CXXFLAGS变量,统一管理FLAG
# $@ 表示构建规则中的目标,即冒号前面的, $^ 表示依赖集合,即冒号后面的
# 在下面这个构建规则中,$@即$(TARGET) $^即$(OBJ)
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
# %.o :%.cpp 表示,对于每一个依赖的.o文件,查询同名的cpp文件并添加一条构建规则
# $< 表示依赖集合中的第一个,由于此时%.cpp只有一个文件,所以等价于$^
# $@表示目标,和上面是一样的
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
# .PHONY也是一条构建规则,由于这个文件不存在,所以每次clean都会重新构建
# 这样做是为了防止make目录下有一个clean同名文件,阻止clean的运行
.PHONY: clean
clean:
rm -f *.o $(TARGET)
.PHONY
是一个声明,后面的成为伪目标(如上面的clean
),在这个声明中的标签会按照makefile
中的意图执行,没有声明的标签会寻找到makefile
同级的文件。
这个版本已经比较自动化了,当有新的.cpp
文件加入,只需要在OBJ
变量中加入xxx.o
即可,其他都不用改。有没有办法在加入新文件也不需要改makefile
呢?有的,看下一个版本。
版本四
CXX = g++
TARGET = hello
# 变量定义中的通配符会失效,使用wildcard函数来使其生效
# 表示查找当前目录下的所有.cpp文件
SRC = $(wildcard *.cpp)
# patsubst函数有三个参数,对于第三个参数中的所有成员,将.cpp替换成.o
OBJ = $(patsubst %.cpp,%.o,$(SRC))
CXXFLAGS = -c -Wall
$(TARGET):$(OBJ)
$(CXX) -o $@ $^
%.o:%.cpp
$(CXX) $(CXXFLAGS) $^ -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)
这个版本使用通配符自动查找当前目录下的所有.cpp
文件,并将.cpp
文件名的.cpp
替换成.o
,自动加入到OBJ
中,这样就不用每次手动加入了。
CMake
make可以很方便地构建程序,但是当跨平台或者环境(比如vs和qt creater),还是需要手动重新修改makefile,有没有办法能够用一套代码实现各个平台和环境的构建呢?有的,cmake是一个跨平台构建工具,它可以根据给定的CMakeFiles.txt文件来生成各个环境的构建文件,比如make的makefile,vs,xcode工程等,省去了手动编写构建规则的麻烦,(但CMakeLists的语法规则也挺复杂的)。同样的工具还有meson等。
最小构建文件
# 指定最小cmake版本
cmake_minimum_required(VERSION 3.15)
# 指定项目名称
project(hello)
# 添加可执行程序
add_executable(hello main.cpp foo.cpp bar.cpp)
使用set来创建变量
set(PROJECT_NAME hello)
set(SRC_FILE main.cpp foo.cpp bar.cpp)