C++ 学习笔记四 —— 编译

一、编译过程

从代码到可执行文件的过程,在IDE中往往点击运行一步完成,但实际上在其背后包含四个步骤:预编译、编译、汇编、链接。

1.1 预编译

预编译过程主要处理的是源代码文件中以#开头的预编译指令,包括:

  • 将所有的#define删除,并展开宏定义;
  • 处理所有条件预编译指定,比如#if,#ifdef,#elif,#else,#endif等,将其删除,并直接返回条件满足的结果。
  • 处理#include预编译指令,将别包含的文件路径插入到预编译指令的位置;
  • 删除所有的注释//、/* */;
  • 添加行号和文件名标识;
  • 保留所有的#pragma。

对应指令:$ g++ -E hello.cpp -o hello.i

1.1.1 宏定义 #define

语法:#define 标识符 替换语句(可以是常数、表达式、字符串等)

有了这条指令以后,预处理器会将源程序中所有出现该标识符的地方在编译阶段进行替换。

缺点:直接替换可能产生语义改变(和别的表达式混合),需要给整个表达式、每个参数都加括号

1.1.2 移除定义 #ubdef

语法:#undef 标识符

用于移除之前使用#define创建的宏;后续出现的标识符被预处理器忽略。

1.1.3 文件包含命令 #include

用来引入对应的头文件(.h文件)或其他文件。其语法一般有两种形式:

#include<xxx.h>:编译器会到系统路径下查找头文件;

#include"xxx.h":编译器会首先在当前目录下查找头文件;如果没找到,再到系统路径下查找。

1.1.4 条件编译

包含指令#if、#endif,必须一一对应,两者中间允许有多个#elif,但是只能有一个#else。

#if OPTION == 1
cout << "Option: 1" << endl;
#elif OPTION == 2
cout << "Option: 2" << endl; //选择这句
#else
cout << "Option: Illegal" << endl;
#endif

/*例子2*/
/* TEST1 或 TEST2被定义,则选择执行printf1,否则执行printf2 */
#if defined TEST1 || defined TEST2
 printf1(".....");
#else
 printf2(".....");
#endif

1.1.5 特殊符号

  • __FILE__包含当前程序文件名的字符串;
  • __LINE__表示当前行号的整数;
  • __DATE__包含当前日期的字符串;
  • __STDC__如果编译器遵循ANSI C标准,它就是个非零值;
  • __TIME__包含当前时间的字符串。

1.2 编译

经过预编译阶段输出的文件.i中包含字符串如main、if、while等,这些具体代表的意思还需要编译器经过词法分析、语法分析和语义分析,最后生成汇编代码。

对应指令:$ g++ -S hello.i -o hello.s

1.2.1 词法分析

使用词法分析器读入.i文件的字符串内容,根据构词规则分解成一系列单词符号,具体包括:

  • 关键字:程序中具有固定意义的单词,如new、while、if等
  • 标识符:用来表示各种名字,比如变量名
  • 运算符:如+、-、>、<等
  • 分解符:如"[","("、";"等
  • 常量:如数字10等

1.2.2 语法分析

词法分析结果只是得到了一个个的具有独立意义的单词,连续的字符按照构词规则被构造为形如“if”、"int"、"x"、"=="、"1"等的单词,但这并不能表示出它们之间的关系,还需要进行语法分析将单词构成语句。

常用的方法包括上下文无关文法、抽象语法树(AST)等。

1.2.3 语义分析

负责检测语法分析中的抽象语法树的上下文有关的属性,比如:

  • 变量在使用前先进行声明
  • 每个表达式都有合适的类型
  • 函数调用和函数的定义一致

通常采用符号表来存储各个变量的相关信息,如类型、作用域等,具体数据结构可以是哈希表(查找快)、红黑树(空间小)。

1.2.4 汇编代码

基于上述步骤,最终形成汇编代码:

1.3 汇编

把汇编代码转化成机器能够读懂的机器指令,编译后会生成目标(object)文件。

对应指令:$ g++ -c hello.s -o hello.o

1.4 链接

分文件编写的文件是单独编译汇编的,为了将文件之间的调用关系关系给连接起来,最后一步称之为链接。一般分为两类:

1.4.1 静态链接

将目标文件直接拷贝到可执行文件中的链接方式,该方式会将所有相关的目标文件都放入可执行文件,使用静态链接方式可以生成静态库文件(.lib/Windows或.a/Linux文件,本质上是若干目标文件的集合),其有如下特点:

  • 静态库对函数库的链接是在编译时期完成的
  • 程序在运行时,不需要静态库文件就可以直接运行,移植方便
  • 浪费空间和资源,因为和所需要函数相关的所有的库都会被打包进可执行文件

1.4.2 动态链接

在程序运行时动态加载目标文件的链接方式,该方式会将需要用的目标文件在运行才放入可执行文件,使用动态链接方式可以生成动态链接库(.dll/Windows或.so/Linux文件,本质上是若干个目标文件集合),其特点如下:

  • 可以实现进程之间的资源共享(因此也称为共享库),不同编程语言可以调用同一动态库
  • 对库函数的链接推迟到程序运行时
  • 开发过程独立、耦合度小,便于不同开发者之间的开发测试,程序升级变得简单
  • 运行程序时,必须保证动态库是存在的,否则会出错

1.4.3 静态链接 VS 动态链接

静态库相比动态库有两个方面的问题:

  • 浪费空间,所有用到的目标文件都会拷贝进来
  • 库更新以后需要重新编译:库是被复制到可执行文件中去了,如果某个库更新了,则与它相关的所有可执行文件都需要重新编译。而DLL文件内容发生变化时,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件就好,可执行文件文件(exe/eft)不用再次编译也可以在执行时更新调用DLL函数的内容。极大地提高了可维护性和可扩展性。

因为静态链接库会使得可执行文件的size变大,并且相对于动态链接库运行速度更快。故静态链接库适用于小型应用的开发。

因为静态链接库不能链接其它链接库,无论静态接库或者动态链接库。所以,当该当链接库还需要链接其它的链接库,包括静态或者动态链接库的场合,应该使用动态链接库。此外,动态链接库可以链接到不同编程语言的工程,所以对于多语言的软件开发的场合也是适用的。

参考资料:

c++编译过程 - 知乎

静态链接库和动态链接库的使用场景_玛丽奥的船的博客-CSDN博客

二、管理和编译C++的工具

2.1 Makefile

过去使用g++命令直接编译、运行:

g++ main.cpp factorial.cpp printhello.cpp -o main

对于大型项目、文件较多时,会导致编译时间非常长;如果只是某个文件进行了修改,可以只对单个文件进行重新编译后再链接,但逐个输入变量名,编译效率低。可以使用make工具将编译指令都写在一个脚本文件中进行操作。

2.1.1 版本1

写在一个脚本文件中,然后在目录下运行make

hello: main.cpp printhello.cpp factorial.cpp
	g++ -o hello main.cpp printhello.cpp factorial.cpp

生成可执行程序hello,依赖于main.cpp printhello.cpp factorial.cpp;Tab符后为实现功能的具体指令。

make会查找目录下是否已经生成过可执行文件,如果有会比较生成时间与依赖文件的更新时间,如果源文件在可执行文件后经过修改,则会重新生成。

2.1.2 版本2

该版本的好处是只会对修改后的新文件进行编译。

CXX = g++
TARGET = hello 
OBJ = main.o printhello.o factorial.o

$(TARGET): $(OBJ)
	$(CXX) -o $(TARGET) $(OBJ)

main.o: main.cpp
	$(CXX) -c main.cpp

printhello.o: printhello.cpp
	$(CXX) -c printhello.cpp

factorial.o: factorial.cpp
	$(CXX) -c factorial.cpp

2.1.3 版本3

进一步将编译和链接过程半自动化。

CXX = g++
TARGET = hello 
OBJ = main.o printhello.o factorial.o

CXXFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(CXX) -o $@ $^

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $< -o $@

.PHONY: clean
clean:
	rm -f *.o $(TARGET)

.PHONY是为了避免目录下存在clean,系统将clean识别为文件而非指令。

2.1.4 版本4

进一步自动化,无需再输入文件名。

CXX = g++
TARGET = hello 
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))

CXXFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(CXX) -o $@ $^

%.o: %.cpp
	$(CXX) $(CXXFLAGS) $< -o $@

.PHONY: clean
clean:
	rm -f *.o $(TARGET)

参考资料:Makefile 20分钟入门,简简单单,展示如何使用Makefile管理和编译C++代码_哔哩哔哩_bilibili

make的基础用法

2.2 CMake

Cmake的优势在于可以支持跨平台使用,例如在大型项目的开发中,多个人用不同的语言或者编译器情况下,可以通过Cmake来输出可执行文件或者共享库(dll,so等等)。

所有操作都是通过编译CMakeLists.txt来完成的。

2.2.1 CmakeList的基本使用

一个最简单的CmakeList.txt文件内容如下:

PROJECT (HELLO)

SET(SRC_LIST main.cpp)

MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})

ADD_EXECUTABLE(hello ${SRC_LIST})

在命令行的同级目录下输入cmake .就生成了CMakeFiles, CMakeCache.txt, cmake_install.cmake 等文件,并且生成了Makefile.文件

再在命令行的同级目录下输入make进行编译,即生成了名为hello的可执行程序。

2.2.2 CmakeList的基本语法

2.2.2.1  PROJECT关键字

作用:指定工程的名字和支持的语言,默认支持所有语言

PROJECT (HELLO)   指定了工程的名字,并且支持所有语言—建议

PROJECT (HELLO CXX)      指定了工程的名字,并且支持语言是C++

PROJECT (HELLO C CXX)      指定了工程的名字,并且支持语言是C和C++

隐式定义了两个CMAKE的变量:<projectname>_BINARY_DIR,<projectname>_SOURCE_DIR

2.2.2.2 SET关键字

作用:设置指定变量

SET(SRC_LIST main.cpp)    SRC_LIST变量就包含了main.cpp

也可以包含多个文件,比如 SET(SRC_LIST main.cpp t1.cpp t2.cpp)

2.2.2.3 MESSAGE关键字

作用:向终端输出用户自定义的信息

主要包含三种信息:

- SEND_ERROR,产生错误,生成过程被跳过。

- SATUS,输出前缀为—的信息。

- FATAL_ERROR,立即终止所有 cmake 过程.

2.2.2.4 ADD_EXECUTABLE关键字

作用:生成可执行文件

ADD_EXECUTABLE(hello ${SRC_LIST})     生成的可执行文件名是hello,源文件读取变量SRC_LIST中的内容

2.2.2.5 语法的基本原则和注意事项

- 变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名

- 指令(参数 1 参数 2...) 参数使用括弧括起,参数之间使用空格或分号分开。

- 指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令

- SET(SRC_LIST main.cpp) 可以写成 SET(SRC_LIST “main.cpp”),如果源文件名中含有空格,就必须要加双引号

2.2.3 在工程中应用

内部构建:会产生较多临时文件,不方便清理

外部构建:将生成的临时文件放在build目录下,推荐使用外部构建方式

2.2.3.1 基本流程

- 为工程添加一个子目录 src,用来放置工程源代码

- 添加一个子目录 doc,用来放置这个工程的文档 hello.txt

- 在工程目录添加文本文件 COPYRIGHT, README

- 在工程目录添加一个 [runhello.sh](http://runhello.sh/) 脚本,用来调用 hello 二进制

- 将构建后的目标文件放入构建目录的 bin 子目录

- 将 doc 目录 的内容以及 COPYRIGHT/README 安装到/usr/share/doc/cmake/

2.2.3.2 将构建后的目标文件放入构建目录(build)的bin子目录

每个目录下都要有一个CMakeLists.txt说明

——————有如下目录结构(基于Cmake新建bin,并将目标文件放入)

├── build

     └── (bin)

├── CMakeLists.txt

└── src

    ├── CMakeLists.txt

    └── main.cpp

其中外层CMakeLists.txt:

PROJECT(HELLO)
//用向当前工程添加存放源文件的子目录(src),并指定中间二进制和目标二进制存放的位置(bin)
//如果不进行 bin 目录的指定,那么编译结果(包括中间结果)都将存放在build/src 目录
ADD_SUBDIRECTORY(src bin) 

src下的CMakeLists.txt

//重新定义 EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH 变量 来指定最终的目标二进制的位置
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

ADD_EXECUTABLE(hello main.cpp)
2.2.3.3 安装文档/版权/脚本文件

——————有如下目录结构(已经有各个文件,使用Cmake进行安装)

├── build

├── CMakeLists.txt

├── COPYRIGHT

├── doc

     └── hello.txt

├── README

├── runhello.sh

└── src

    ├── CMakeLists.txt

    └── main.cpp

 1. 安装文件COPYRIGHT和README

语句:INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/)

FILES:文件

DESTINATION:1、写绝对路径;2、写相对路径,相对路径实际路径是:${CMAKE_INSTALL_PREFIX}/<DESTINATION 定义的路径>

CMAKE_INSTALL_PREFIX  默认是在 /usr/local/

2. 安装脚本runhello.sh

语句:INSTALL(PROGRAMS runhello.sh DESTINATION bin)

PROGRAMS:非目标文件的可执行程序安装(比如脚本之类)

说明:实际安装到的是 /usr/bin

3. 安装 doc 中的 hello.txt

语句: INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake)

 DIRECTORY 后面连接的是所在 Source 目录的相对路径

4. 安装

cmake  -  make  - make install

2.2.3.4 构建共享文件so

——————有如下目录结构(在build/bin下生成.so文件)

├── build

├── CMakeLists.txt

└── lib

    ├── CMakeLists.txt

    ├── hello.cpp

    └── hello.h

项目中的cmake内容:

PROJECT(HELLO)
ADD_SUBDIRECTORY(lib bin)

lib中CMakeLists.txt中的内容

SET(LIBHELLO_SRC hello.cpp)
// ADD_LIBRARY生成库文件
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
//- hello:就是正常的库名,生成的名字前面会加上lib,最终产生的文件是libhello.so
//- SHARED,动态库    STATIC,静态库
//- ${LIBHELLO_SRC} :源文件

1.同时生成静态库和动态库

如果不改变名字,直接用相同的命令,只将SHARED改为STATIC是无法生成的,会清理掉上一个库

语句:SET_TARGET_PROPERTIES

作用:设置输出的名称,对于动态库,还可以用来指定动态库版本和 API 版本;因此可用于同时构建静态和动态库

SET(LIBHELLO_SRC hello.cpp)
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

//对hello_static的重名为hello
SET_TARGET_PROPERTIES(hello_static PROPERTIES  OUTPUT_NAME "hello")
//cmake 在构建一个新的target 时,会尝试清理掉其他使用这个名字的库,
//也就是说,在构建 libhello.so 时, 就会清理掉 libhello.a,所以需要添加下面这句
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

SET_TARGET_PROPERTIES(hello PROPERTIES  OUTPUT_NAME "hello")
SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)

2. 安装生成的库文件

//文件放到该目录下
INSTALL(FILES hello.h DESTINATION include/hello)

//二进制,静态库,动态库安装都用TARGETS
//ARCHIVE 特指静态库,LIBRARY 特指动态库,RUNTIME 特指可执行目标二进制。
INSTALL(TARGETS hello hello_static LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)

注意:安装的时候,指定一下路径,放到系统下

cmake -D CMAKE_INSTALL_PREFIX=/usr ..

3. 使用外部共享库和头文件

准备工作,新建一个目录来使用外部共享库和头文件

——————有如下目录结构(使用外部共享库)

├── build

├── CMakeLists.txt

└── src

    ├── CMakeLists.txt

    └── main.cpp

### 解决:make后头文件找不到的问题

方法1:在cpp文件中修改路径include <hello/hello.h>

方法2::关键字:INCLUDE_DIRECTORIES    

作用:向工程添加多个特定的头文件搜索路径,路径之间用空格分割

语法:INCLUDE_DIRECTORIES(/usr/include/hello) 在CMakeLists.txt中加入头文件搜索路径

 参考资料:

CMakeLists语法介绍_哔哩哔哩_bilibili

5分钟理解make/makefile/cmake/nmake - 知乎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值