C\C++编译相关
前言
本文为《CUDA与TensorRT部署学习笔记》系列中c\c++篇。本文主要记录编译器、c的编译流程、g++及gcc、cmake的理解和一般cmake标准工程一个参考示例的记录。
1 什么是编译器?
编译器是将高级语言代码转换为低级语言代码的程序。
-
高级语言代码,也称为源代码,是人类可以阅读和理解的代码。
-
低级语言代码,也称为机器代码,是计算机可以直接执行的代码。
编译器的工作原理是将源代码逐行分析,然后将其转换为机器代码。编译器会检查源代码是否有错误,如果有错误,会报告错误信息。
编译器可以分为两种类型:
- 解释型编译器:解释型编译器会在运行时逐行解释源代码,并将其转换为机器代码。
- 编译型编译器:编译型编译器会将源代码一次性转换为机器代码,并生成可执行文件。
解释型编译器的优点是执行速度快,缺点是需要占用更多的内存。
编译型编译器的优点是执行速度慢,但生成的可执行文件体积小,而且只需要编译一次,就可以多次执行。
编译器是软件开发中必不可少的工具。它可以将人类可读的源代码转换为计算机可执行的机器代码,从而使软件开发变得更加简单和高效。
大白话理解
编译器就是一个翻译器。它可以将我们写的程序代码翻译成计算机可以理解的语言。这样,计算机就可以按照我们的指示,执行我们的程序。
2 c/c++编译流程
C/C++
的编译流程一般分为以下四个阶段:
- 预处理
- 编译
- 汇编
- 链接
2.1 预处理
预处理:将源代码中的预处理指令(#include
、#define
等)展开,并将宏定义替换为实际的值。
例如,我们有一个名为 hello.c
的 C 程序源代码文件,内容如下:
C
#include <stdio.h>
#define NAME "AAA"
int main() {
printf("Hello, %s!\n", NAME);
return 0;
}
在预处理阶段,编译器会将#include
指令展开,并将 #define
指令替换为实际的值。
使用以下命令可以将 hello.c
文件转换为预处理后的源代码文件 hello.i
:
bash
gcc -E hello.c -o hello.i
经过预处理后,源代码文件的内容如下:
C
#include <stdio.h>
int main() {
printf("Hello, AAA!\n");
return 0;
}
2.2 编译
编译:将预处理后的源代码转换为汇编代码。
在编译阶段,编译器会将预处理后的源代码中的每个语句转换为一个或多个汇编指令。
使用gcc -c
命令可以将源代码文件转换为汇编文件。
例如,我们可以使用以下命令将 hello.i
文件转换为汇编文件 hello.s
:
bash
gcc -c hello.i -o hello.s
例如,上述源代码的编译结果如下:
bash
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello, AAA!\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
ret
2.3 汇编
汇编:将汇编代码转换为机器代码。
在汇编阶段,汇编器会将汇编代码转换为机器代码,即计算机可以直接执行的代码。
使用 as
命令可以将汇编文件转换为机器代码文件。
例如,我们可以使用以下命令将 hello.s
文件转换为机器代码文件 hello.o
:
bash
as hello.s -o hello.o
例如,上述汇编代码的汇编结果如下:
bash
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 04 sub $0x4,%esp
6: 8d 45 fc lea -0x4(%ebp),%eax
9: 89 04 24 mov %eax,(%esp)
c: e8 00 00 00 00 call 0 <printf>
11: b8 00 00 00 00 mov $0x0,%eax
16: 5d pop %ebp
17: c3 ret
2.4 链接
链接:将多个目标文件链接成可执行文件。
在链接阶段,链接器会将多个目标文件链接成一个可执行文件。目标文件是经过编译和汇编后的文件。
例如,我们有一个名为 hello.c
的 C 程序源代码文件,以及一个名为 math.c
的 C 程序源代码文件。 math.c
文件包含一些数学函数。
我们可以使用以下命令来编译 hello.c
和 math.c
文件:
bash
gcc hello.c math.c -o hello
这条命令会将 hello.c
和 math.c
文件编译成一个名为 hello
的可执行文件。
hello
可执行文件可以直接运行,其功能是打印一条包含用户名的欢迎信息。
2.5 总结
C/C++的编译流程就是将我们写的程序代码翻译成计算机可以理解的语言的过程。
具体来说,编译流程包括以下四个阶段:
- 预处理:将程序代码中的宏定义替换为实际的值,并将头文件中的函数原型和宏定义导入到程序代码中。
- 编译:将预处理后的程序代码转换为汇编代码。
- 汇编:将汇编代码转换为机器代码。
- 链接
注意
在实际开发中,通常使用 gcc 命令来完成整个编译流程。例如,我们可以使用以下命令将 hello.c 文件编译成一个名为 hello 的可执行文件:
bash
gcc hello.c -o hello
这条命令会将 hello.c
文件先转换为汇编文件 hello.s
,然后将 hello.s
文件汇编成机器代码文件 hello.o
,最后将 hello.o
文件链接成可执行文件 hello
。
3 理解g++/gcc
3.1 g++与gcc说明
g++
和 gcc
是两个编译器,它们可以将 C
和 C++
语言的源代码编译成可执行文件。
g++
是GNU C++
编译器,它可以编译C++
语言的源代码。gcc
是GNU C
编译器,它可以编译C
语言的源代码。
g++ 和 gcc 都属于 GNU 编译器套件(GCC)的一部分。GCC 是一个免费和开源的编译器套件,它支持多种编程语言,包括 C、C++、Objective-C、Fortran、Ada、Go 等。
大白话理解
g++ 和 gcc 就是两个可以将我们写的程序代码翻译成计算机可以理解的语言的工具。
具体来说,g++ 可以将我们写的 C++ 程序代码翻译成计算机可以理解的 C++ 机器代码,而 gcc 可以将我们写的 C 程序代码翻译成计算机可以理解的 C 机器代码。
g++ 和 gcc 的使用方法比较简单。我们只需要将程序源代码保存在一个文件中,然后使用 g++ 或 gcc 命令来编译该文件,就可以生成可执行文件。
例如,我们有一个名为 hello.cpp 的 C++ 程序源代码文件,内容如下:
c++
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}
我们可以使用以下命令来编译该文件:
bash
g++ hello.cpp -o hello
这条命令会将 hello.cpp
文件编译成一个名为 hello
的可执行文件。
我们可以使用以下命令来运行 hello
可执行文件:
bash
./hello
这条命令会在终端中显示以下输出:
bash
Hello, world!
因此,g++
和 gcc
是软件开发中必不可少的工具。它们可以帮助我们将程序代码编译成计算机可以理解的语言,从而使软件开发变得更加简单和高效。
3.2 g++与gcc区别联系
主要回答以下问题:
gcc和g++都一样么?两个都能编译c或者c++么?系统是怎么进行判断的,然后如果都支持,那在实现上有什么区别?
gcc
和 g++
都是GNU Compiler Collection
(GNU编译器集合)的一部分,但它们在实际用途和默认行为上有一些区别。
3.2.1 区别和用途
-
gcc:
- gcc 是 GNU Compiler Collection 的主要前端命令,用于编译C语言源代码。
- 默认情况下,gcc 将源代码看作是C代码,生成对应的可执行文件。
-
g++:
- g++ 也是 GNU Compiler Collection 的前端命令,专门用于编译C++语言源代码。
- 默认情况下,g++ 将源代码看作是C++代码,生成对应的可执行文件。
3.2.2 编译过程
-
系统判断:
- 系统通常通过文件扩展名来判断源代码的类型。如果文件扩展名为 .c,则系统默认使用 gcc;如果文件扩展名为 .cpp、.cc 或 .cxx 等,则系统默认使用 g++。
-
通用性:
- 事实上,gcc 和 g++ 都可以编译C和C++代码,而且它们在编译时会自动根据源代码的扩展名选择合适的语言模式。因此,如果你使用 g++ 编译C代码,它会以C++的方式进行编译;反之亦然。
3.2.3 区别
-
默认链接库:
- g++ 默认链接 C++ 标准库,而 gcc 默认链接 C 标准库。
-
编译选项:
- g++ 在链接时会自动添加 C++ 标准库的链接选项 -lstdc++,而 gcc 不会。
大白话理解
- gcc 和 g++ 都是编译器,一个主要用于C语言,一个主要用于C++语言。
- 如果你有一个以 .c 结尾的源代码文件,使用 gcc 编译器就好;如果是以 .cpp 结尾的文件,用 g++ 编译器更合适。
- 实际上,g++ 能够处理C代码,gcc 也能处理C++代码,系统会根据文件扩展名来自动选择使用哪个编译器。
- 在链接时,g++ 默认链接 C++ 标准库,而 gcc 默认链接 C 标准库,这是它们的主要区别。
- 对于大多数用途,选择 g++ 或 gcc 都可以,它们的区别在于默认行为,但你也可以通过编译选项手动调整它们的行为。
3.3 示例说明
3.3.1 gcc编译cpp文件、g++编译c文件
编译C++源文件时使用gcc,或者编译C源文件时使用g++可能会导致一些问题,因为默认的标准库和链接方式会有不同。
下面通过一个简单的例子来说明可能的问题和解决方法。
一个简单的C++
程序 example.cpp
:
c++
#include <iostream>
int main() {
std::cout << "Hello, C++!" << std::endl;
return 0;
}
然后,有一个简单的C
程序 example.c
:
c
#include <stdio.h>
int main() {
printf("Hello, C!\n");
return 0;
}
(1)使用 gcc 编译 C++ 文件
bash
gcc example.cpp -o example_cpp
问题:由于默认链接 C 标准库,可能会导致找不到 C++ 标准库的错误。
解决方法:手动添加 C++
标准库链接选项 -lstdc++
:
bash
gcc example.cpp -o example_cpp -lstdc++
(2)使用 g++ 编译 C 文件
bash
g++ example.c -o example_c
问题:由于默认链接 C++ 标准库,可能会导致找不到 C 标准库的错误。
解决方法:手动添加 C 标准库链接选项 -lc
:
bash
g++ example.c -o example_c -lc
(3)结论
- 尽管
gcc
和g++
都可以编译C和C++代码,但最好还是使用它们的默认行为,即 gcc 编译C代码,g++
编译C++
代码,以确保链接到正确的标准库。 - 如果不得不使用
gcc
编译C++
代码或g++
编译C代码,确保手动添加正确的链接选项,以匹配源文件类型对应的标准库。在上述例子中,-lstdc++
链接C++
标准库,-lc
链接C
标准库。
4 理解CMake
4.1 什么是CMake
CMake(Cross-platform Make)是一个开源的跨平台构建系统生成工具,用于管理和自动化项目的构建过程。它使用一种称为CMakeLists.txt的简单配置文件来描述项目的构建、编译、链接和安装规则,然后生成适用于不同构建系统(如Makefile、Visual Studio、Xcode等)的相应构建文件。CMake的设计使得软件的构建在不同平台和编译器之间更加灵活和可移植。
大白话理解
CMake是一段跨平台的构建脚本,可以根据具体平台上生成对应的makefile,所以CMake的本质还是生成makefile,然后还是通过makefile来构建项目,CMake本身不构建项目。CMake 就是将多个 cpp、hpp 文件组合构建为一个大工程的语言。
4.2 CMake的优缺点?
(1)优点:
- 开源,使用类BSD许可发布。
- 跨平台,并可以生成 native 编译配置文件,在 Linux/Unix 平台,生成 makefile;在苹果平台可以生成 Xcode;在Windows 平台,可以生成 MSVC 的工程文件。
- 能够管理大型项目。
- 简化编译构建过程和编译过程。cmake 的工具链非常简单:cmake + make。
- 高效率,因为 cmake 在工具链中没有 libtool。
- 可扩展,可以为 cmake 编写特定功能的模块,扩展 cmake 功能。
(2)缺点:
- cmake 只是看起来比较简单,而使用并不简单。
- cmake 编写的过程实际上是编程的过程,每个项目使用一个 CMakeLists.txt(每个目录一个),使用的是 cmake 语法。
- cmake 跟已有体系配合不是特别的理想,比如 pkgconfig。
引用:https://www.coonote.com/linux-note/cmake-quick-start.html
4.3 cmake编译流程
CMake的编译流程包括配置和生成两个主要阶段。
4.3.1 配置阶段
(1)CMakeLists.txt 文件: 在项目根目录下编写 CMakeLists.txt
文件,该文件包含了项目的配置信息、源文件列表、目标等。
(2)创建构建目录: 在项目根目录外创建一个用于构建的目录,通常称为build目录。在这个目录下运行 cmake 命令。
bash
mkdir build
cd build
cmake ..
这将告诉CMake
在当前目录上一级(即项目的根目录)查找 CMakeLists.txt
文件并进行配置。
(3)生成配置文件: CMake
根据CMakeLists.txt
文件中的内容生成相应的配置文件(如Makefile
、Visual Studio
项目文件等),这些文件用于描述项目的构建规则。
(4)变量和选项设置: 在配置阶段,可以通过-D
选项传递变量,设置项目的一些选项。
bash
cmake -DCMAKE_BUILD_TYPE=Release ..
4.3.2 生成阶段
(1)生成构建系统文件: 在配置阶段完成后,运行 make(或其他构建系统的命令,如ninja等)生成可执行文件或库。
bash
make
如果使用了其他构建系统,命令可能不同。
(2)实际构建: 构建系统根据配置文件中的规则,编译源代码、链接目标文件,最终生成可执行文件或库。
bash
./my_test
(3)可选: 可以使用构建系统的其他命令,如make install
将生成的文件安装到系统中。
4.3.3 补充说明
- 多配置选项: 在配置阶段,可以通过
CMAKE_BUILD_TYPE
等选项指定构建的类型,如Debug
、Release
等。 - 生成器选择:
CMake
支持多种生成器,可通过-G
选项指定,例如-G "Unix Makefiles"
、-G "Visual Studio 16"
等。
整个流程可以根据项目的需求和开发者的偏好进行调整。CMake
的灵活性使得它适用于各种项目,并能够轻松地配置和生成不同构建系统的文件。
5 CMake工程化标准参考
一个build目录专门用于存放CMake产生的文件,工程源文件和库源文件分开存放在src和lib目录,工程根目录有个CMakeLists.txt用于管理全局的CMakeLists文件。
-
根目录的CMakeLists.txt,将src和lib目录加入构建就好:
cmakeCMAKE_MINIMUM_REQUIRED(VERSION 3.16) PROJECT(Demo) #引入2个子目录 ADD_SUBDIRECTORY(lib) ADD_SUBDIRECTORY(src)
-
src的CMakeLists.txt:
cmake#指定可执行文件输出路径为执行CMake的目录下的/scr/bin(path/Demo/build/src/bin) SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) # 添加 lib 子目录 include_directories(../lib) #添加头文件目录 include_directories(../lib) # 查找目录下的所有源文件 # 并将名称保存到 DIR_SRCS 变量 aux_source_directory(. DIR_SRCS) Message("DIR_SRCS = ${DIR_SRCS}") Message("PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}") Message("PROJECT_BINARY_DIR = ${PROJECT_BINARY_DIR}") #指定生成可执行文件的源文件 add_executable(Demo ${DIR_SRCS}) # 添加链接库 target_link_libraries(Demo Animal)
-
lib的CMakeLists.txt:
cmake# 查找当前目录下的所有源文件 # 并将名称保存到 DIR_LIB_SRCS 变量 aux_source_directory(. DIR_LIB_SRCS) #指定库的输出路径为lib(path/Demo/build/lib) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) # 指定生成 Animal链接库 add_library (Animal SHARED ${DIR_LIB_SRCS})
-
由于需要在build下生成构建文件,所以需要在build中执行CMake:
cmakecd build cmake ..
此时build文件已经生成构建文件Makefile和对应的构建结果文件lib和src,并且lib和src已经分别生成了各自对应的Makefile文件。
-
在build下执行make:
make
-
此时可执行文件和动态链接库已经生成,执行可执行文件:./Demo