# 【CCG】源文件 作者:wallace-lai
发布:2024-02-25
更新:2024-03-05
## 一、接口与实现 ### 规则1:如果项目没有其他约定,那么源文件使用cpp后缀,头文件使用h后缀 除非你的项目已经有了其它的后缀命名策略,否则一律按照规则1来处理。 ### 规则2:头文件中不可包含对象定义或者非内联函数定义 如果你的头文件里含有**对象**或者**非内联函数**的定义,则可能导致链接步骤报错。实际上,在C++中,有所谓的单一定义规则ODR。ODR在函数定义方面有以下几点说法。 (1)一个函数在任何翻译单元中不能有一个以上的定义; (2)一个函数在程序中不能有一个以上的定义; (3)有外部链接属性的内联函数可以在一个以上的翻译单元中被定义,这些定义必须满足一个要求:它们全相同; 注意:将**类**定义在头文件中,并不会引起链接器的不满,比如下面这个案例。 ```cpp // header.h class Base { Base() {} ~Base() {} void FormatBase(void) { cout << "FormatBase" << endl; } }; ``` ```cpp // t1.cpp #include "header.h" void fun1(void) { Base b; b.FormatBase(); return; } ``` ```cpp // t2.cpp #include "header.h" void func2(void) { Base b; b.FormatBase(); return; } int main() { Base b; b.FormatBase(); return 0; } ``` 同理,内联函数和类一样,也是可以被定义在头文件中的。 ### 规则5:.cpp源文件必须包含其接口定义的头文件.h 如果不这么做,则会在链接过程中出问题,比如下面的案例: ```cpp // impl.h int func(int); ``` ```cpp // impl.cpp std::string func(int x) { return to_string(x + 32); } ``` 此时源文件`impl.cpp`并没有包含对应的头文件`impl.h`,并且函数`func`的声明和定义中的返回值类型不一致。 ```cpp // main.cpp int main() { int result = func(32); cout << result << endl; return 0; } ``` 在主函数中调用`func`,则会得到一个`undefined reference`的错误。 ``` main.cpp:(.text+0x12): undefined reference to `func(int)' ``` 如果遵循了规则5,那么类似这种浅显的错误就可以在编译过程中暴露出来,不用等到链接过程。 ### 规则8:为所有的.h头文件使用宏防止出现多重包含 给所有的头文件都添加宏,用于防止出现多重包含的情况。 ```cpp // footer.h #ifndef FOOTER_H_ #define FOOTER_H_ // ... #endif /* FOOTER_H_ */ ``` 有两点需要注意的: (1)你应该保证不同的头文件的防护宏的名字应该是唯一的; (2)使用`#pragma once`预处理指令来解决多重包含是不可移植的,因为`#pragma`指令并未标准化; ### 规则9:避免源文件之间循环依赖 比如下面这个案例就是典型的头文件之间循环依赖,无法通过编译。 ```cpp // a.h #pragma once #include "b.h" class A { B b; }; ``` ```cpp // b.h #pragma once #include "a.h" class B { A a; }; ``` ```cpp // main.cpp #include "a.h" #include "b.h" int main() { A a; B b; return 0; } ``` 编译报错为: ``` b.h:6:5: error: ‘A’ does not name a type ``` 解决办法是**前置所依赖类的声明并使用指针**,如下所示: ```cpp // a.h #pragma once class B; class A { B *b; }; ``` ### 规则10:避免对隐含#include进来的名字的依赖 比如下面这个案例在GCC 5.4和微软编译器19.00.23506版本中进行编译,前者没问题;后者无法通过编译。 ```cpp #include int main() { std::string s = "hello world"; std::cout << s << std::endl; return 0; } ``` 原因在于GCC 5.4编译器的`iostream`头文件中包含了`string`;而微软编译器中的`iostream`没有。我们需要手动包含`string`以摆脱代码对隐含名字的依赖。 ### 规则11:头文件应该是自包含的 应该说是在头文件能够做到自包含的情况下,尽量达成头文件的自包含。 ## 二、命名空间 ### 规则16:(仅)对代码迁移、基础程序库(比如std)或者在局部作用域中使用using namespace指令 下面的案例将无法通过编译,原因在于局部变量`sqrt`和标准库中的`sqrt`函数重名了。 ```cpp #include using namespace std; int g(int x) { int sqrt = 7; // ... return sqrt(x); } ``` 不仅如此,using namespace指令隐藏了名称的来源,且破坏了代码的可读性。只允许在以下三种场景下使用using namespace指令: (1)代码迁移(这是什么?); (2)基础库std; (3)局部作用域中; ### 规则17:不要在头文件的全局作用域中使用using namespace 头文件中全局作用域的using namespace会将名字注入到包括该头文件的每个源文件中。这种注入有一些不好的后果: (1)当你使用这个头文件时,你无法再撤销其中的using指令; (2)名字冲突的可能性急剧增加; (3)对被包含的namespace的改变可能会破坏你的编译,比如因为其中引入了一个新的名字(导致冲突); ### 规则20:使用namespace表示逻辑结构 ### 规则21:不要在头文件中使用匿名命名空间 ### 规则22:为所有的内部(不导出的实体)使用匿名命名空间 匿名namespace使用的是内部链接。而内部链接意味着匿名namespace内的名称只能在当前翻译单元内引用,而不能导出。不能导出这一点同样适用于在匿名namespace中声明的名称。 而当你在头文件中使用匿名namespace时(规则21),每个翻译单元都定义了这个无名namespace的唯一实例,这会导致: (1)产生的可执行文件大小会膨胀 (2)匿名namespace中的任何声明都将是不同翻译单元中的不同实体,这可能不是程序员期望的行为 匿名namespace的用法类似C语言里使用的static关键字。