Last Updated on 2022年9月27日
编译系统基础
工具链
-
一个典型的工具链是:链接器-编译器(汇编器一般不接触)-预处理器-文本编辑器-构建系统
- 调试工具及profile工具通常作为独立的部分出现.
-
构建系统,或者称为工程管理器,对于开发意义重大,是工具链中重要的一环,其意义在于组织文件,管理源代码,明确目标输出,组织编译/链接的顺序和关系.
- 常见的构建系统有,VS的
nmake
,QT的qmake
,跨平台的CMAKE(使用makelist.txt
),unix平台的make(使用makefile
).其中CMAKE是目前事实上的标准构建系统,bazel也随着google项目的扩张越来越流行 - VS和QT这样的IDE对自己的构建系统更加友好,往往可以按GUI的形式设置构建参数,CMAKE等则都需要开发者手动写构建文件.
- 常见的构建系统有,VS的
-
使用
#ifndef
或者#pragma once
可以有效的避免重复include,一个正常的头文件都应该包含include guard. -
一个典型的编译模型是: 每个源文件都是一个Module,编译完成后将输出对应的obj文件,链接器负责将这些obj合并成一个最终的obj.
- 注意,各个Module的编译是彼此独立的,例如
#ifndef
就是各个Module单独判定的,它只能保证Module内只包含一份代码,不同的Module完全可以包含相同的定义.
- 注意,各个Module的编译是彼此独立的,例如
-
重定义是一个同时涉及编译和链接的问题,它相当复杂,在编译阶段的重定义会直接报错,在链接阶段,多个Module中的重复定义则可以按照一定的规则进行处理.
-
在不够熟悉链接器的规则以前,为了避免
#include
引起的重定义,在头文件内的编程习惯应当如下:- 只进行函数声明
- 只进行全局变量的
extern
声明 - 只进行类声明
- 成员函数可以进行
inline
形式的定义
- 成员函数可以进行
- 类模板的偏特化.(函数模板的特化一定是全特化,只应该定义在cpp文件中)
- 模板的默认实现.
- 各种被内联的函数(inline,constexpr等).
- 枚举.
- 常量相关的
#define
和const
定义可以出现在头文件中(const
默认是文件私有的,所以不会引起重定义)- 在一些特殊的场景,可能必须要用
extern const int a;
这样的语法
- 在一些特殊的场景,可能必须要用
-
extern "C"仅仅指明了链接规范,使用时一定要加
{}
,避免一些隐晦的错误.extern "C"{int a};
是定义,而extern "C" int a;
是声明,这一点区别很奇怪extern "C"
定义的函数还可能影响函数指针的类型,例如extern "C" void (*p1)();
与void (*p2)();
,这两个指针可能指向的是两种不同的函数std::function
和lambda
等C++专有特性所返回的指针一定是C++
型链接的,不可能是extern "C"
风格的指针.
使用动态库和静态库
- 从初学者的角度看: 大部分的使用场景中,静态库和动态库是没有区别的.
文件 | 功能 |
---|---|
.h |
包含库的接口声明 |
lib_xx.a/.lib |
静态库,包含了编译后的目标代码(.obj ). |
.dll + .lib |
Windows的动态库体系,.lib 称为导入库,用于提供动态库的符号信息,仅用于链接阶段..dll 则仅用于运行阶段.如果没有导入库,也可以手动载入dll,手动链接和linux下用法类似. |
.so/.dylib |
Unix的动态库,编译&运行使用同一个文件. |
预处理器
预处理器是一个独立于编译器的程序,它是一个纯文本的处理器,负责在编译前对所有的待编译文件做文本批处理.源代码中可以使用宏来指导预处理器进行操作.
宏
- 最常用的
#include "xx.h"
宏,指示预处理器查找"xx.h"文件,并将该文件的内容插入到当前位置 #define
宏可以用于定义宏函数和常量- 宏函数实质就是将"实参文本"替换到后面的定义语句中.
- 宏函数不支持重载,不支持递归.
##
可以将实参文本拼接,例如#define FUN(x,y) x##y
展开后就是xy
#
可以为实参文本加"",例如#define CODE_RAW(str) #str
展开后就是"str"
- 大部分编译器都是针对特定平台的,编译器会通过预定义宏给出很多平台信息.
- 指示目标编译平台,是否支持多线程编译,自然字节长度,等等
- 指示文件名,函数名等信息.