预处理
-
编译之前(预处理阶段)执行
-
预处理功能举例
- #include
- 头文件保护符
- 避免同一个文件被include多次
- 通过判断预处理变量是否已定义:#ifndef、#define、#endif
- 预处理变量名全局唯一(通常通过头文件中的类名来定义保护符的名字,个人感觉 namespace name + class name 来定义较好)
- 缺点
- 如果项目的不同模块中有同名的两个头文件(内容可能不同),其头文件保护符很可能也是相同的,这时可能会造成其中一个头文件被意外exclude
- #ifndef/#endif需要成对出现
- 避免同一个文件被include多次
- #pragma once
- 同样也是避免同一个文件被include多次
- 此处“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件
- 无法对一个头文件中的一段代码作pragma once声明,而只能针对文件
- 缺点
- non-standard
- 如果项目中某个头文件被拷贝多次,编译时会认为这是多个不同头文件(同时也是优点,如项目中不同模块的两个同名但不同内容头文件,不使用头文件保护符,而是使用这种方式就不会意外exclude其中一个)
- 同样也是避免同一个文件被include多次
-
预处理变量及注意事项
- #define 指令把一个名字设定为预处理变量
- #ifdef、#ifndef 检测预处理变量已定义、未定义
- 预处理变量无视C++语言中关于作用域的规则(即不能提供任何封装性)
- 整个程序中的 预处理变量(包括头文件保护符)必须唯一
概念及规则
- 编译单元:即一个源文件+其include的文件,编译后可生成一个目标文件
- 可声明多次,但只能定义一次
- 可重声明函数,但不能重声明结构/类
- 分离式编译过程中,预处理器在每个编译单元中查找头文件保护符是否被定义
内联函数
模板函数
通常模板函数声明和定义都位于头文件,原因为在同一编译单元内,编译器要根据模板函数定义来实例化模板函数,而实例化模板函数发生的的位置在源文件,因此模板函数的定义应位于头文件
另外当多个源文件中都有相同的模板函数实例化时,每个编译单元内确实都生成了对应的实例化函数,但它是一个弱符号,最终在链接阶段只会选择其中一个,从而不会出现多重定义问题,具体可参考:链接器如何解析多重定义的全局符号
重复定义相关测试
类成员
- 成员函数定义在头文件中类外部时,可能会有多重定义的错误(编译或者链接阶段都有可能)
- 成员函数定义在头文件中类内部时为内联函数,不会出现多重定义错误
- 类静态成员的定义在头文件中类外部时,可能会有多重定义的错误
- 当不对类静态成员进行定义时,代码中如果有对其访问则会报未定义编译错误
全局变量
- 静态全局变量、常量全局变量为内部链接 ,每个编译单元中可保存一份副本,在头文件中的定义不会引起重复定义错误
- 普通全局变量如果定义在头文件,可能引起重复定义错误
参考
《C++ Primer 第五版》
《Effective C++ 第三版》
Is #pragma once a safe include guard?
#ifndef 与 #program once 的区别
头文件与cpp文件为什么要分开写
头文件重复包含和变量重复定义
How is compilation unit defined in c++?
What is a “translation unit” in C++
How does the linker handle identical template instantiations across translation units?
separate-template-class-defn-from-decl