C && C++
一、基础
1.1 编译
c/c++ 源代码一般会通过以下步骤,被编译、链接成可执行文件:
- 预处理(Preprocess) :
gcc -E test.cpp -o test.i - 编译为汇编(Compilation):
gcc -S test.i -o test.s - 汇编(Assembly):
gcc -c test.s -o test.o - 连接(Linking):
gcc test.o -o test
当然,上述步骤可以直接通过 gcc define.cpp test.cpp -o test 一步到位.
1.2 标准库
c && c++ 标准定义了语法、语言特性 以及 C++ 内置库(也就是C++标准库)的实现规范, 但不同的编译器对标准库的实现方法各不一致
| Subject | Content | Status |
|---|---|---|
| 语言特性 | C++ 11/14/17 标准、多态、模版 | ☑ |
| 异常 | 标准异常、VEH、SEH、TopLevelEH... | ☐ |
| 调优 | 内存、性能 | ☐ |
| 编译运行 | 编译、运行期的 action 和 mechanism | ☐ |
| Design | is-a(Inheritance) 和 has-a(Composition) | ☑ |
| 平台特性 | window、linux | ☐ |
| 技巧 | Tricks and Traps | ☐ |
1.3 Feature
| Version | Name | Sample | Note |
|---|---|---|---|
| C++11 | rvalue reference | int &&ref_a_left = 6; | 右值引用 和 移动语义 |
| C++11 | move semantics | std::move | move and move contructor |
| C++11 | perfect forwarding | std::forward | 移动语义 和 完美转发 |
| C++11 | decltype | int n = 3; decltype(n) t = n + 4; | 类型推导, 以普通表达式作为参数返回该表达式的类型, 注意,decltype 不会对表达式进行求值 |
| C++11 | auto | auto a = 4 + 6; | 类型推导, 从变量声明的初始化表达式获得变量的类型 |
| C++14 | "auto" return type deduction | template <typename A, typename B>auto do_something(const A& a, const B& b){ return a.do_something(b); } | |
| C++11 | trailing-return type | auto foo(int p) -> int { return p * 42; } | auto, decltype, trailing-return type |
| C++11 | lambda | auto aLambda = [](auto a, auto b) { return a < b; }; | |
| C++11 | constexpr | constexpr int x = 5; | |
| C++11 | strongly typed enums | enum class : char { }; | |
| C++11 | static assert | static_assert(sizeof(int) == 4); | 编译期的断言 |
| C++11 | range based for | for (auto x : vec) { /* TODO */ } | |
| C++11 | type alias | using func = void (*) (int, int); | using、typedef、typename using 可读性更高,且可以用于模板别名The "typename" keyword |
| C++11 | unicode strings | const char16_t* utf16 = u"foo";const char32_t* utf32 = U"foo"; | |
| C++11 | override | struct A { virtual void foo() {}; };struct B : public A { void foo() override {}; }; | |
| C++17 | constexpr if | if constexpr (sizeof(int) == 1) return 0; | |
| C++17 | template auto | template <auto Value>struct constant { static constexpr auto value = Value;}; | type and non-type template parameters |
| C++17 | nested namespace definition | namespace A::B::C { class X {}; } | |
| C++17 | inline variable | class X { static inline int field = 2; }; | |
| C++17 | structured binding | int arr[3] = {3,4,5};auto [a, b, c] = arr;auto [x, y, z] = std::make_tuple("str", 0.4, 1); | |
| C++20 | designated initializer | struct A { int x; int y; int z; };A a { .x = 1, .z = 2 }; | |
| C++20 | modules | export module A; import A; import std.core; | |
| C++20 | consteval | static consteval int square(int n) { return n * n; } | |
| C++20 | constinit | constinit auto sqrOf5 = square(5); | |
| C++20 | concept | template<typename T>concept Integral = std::is_integral<T>::value; | |
| C++11 | attribute specifier sequence | [[gnu::const]] [[nodiscard]]inline int f(); | |
| C++ | enumerators with attributes | enum class myEnum{ value1 [[anAttribute]], }; | ?? |
| C++11 | std::integral_constant | 包装特定类型的静态常量。它是 C++ 类型特征的基类。 | |
| C++11 | constexpr length for const string | template<class T>constexpr size_t const_str_len(const T* str) { return (*str == 0) ? 0 : const_str_len(str + 1) + 1; } | |
| C++17 | constexpr length for const string | constexpr auto l = std::char_traits<char>::length("123"); |
Thriving in a Crowded and Changing World: C++ 2006–2020
这是 C++ 之父 Bjarne Stroustrup 的 HOPL4 论文的标题。
HOPL(History of Programming Languages,编程语言历史)ACM(Association of Computing Machines,国际计算机协会)旗下的一个会议,约每十五年举办一次。
Bjarne 的这篇论文是他为 2021 年 HOPL IV 会议准备的论文,它涵盖了 C++98 之后的所有 C++ 版本,从 C++11 直到 C++20。
鉴于这篇论文可以帮助 C++ 从业者对 C++ 的设计原则和历史有一个系统的了解,全球 C++ 及系统软件技术大会的主办方 Boolan 组织了一群译者,把这篇重要论文翻译成了中文: 在拥挤和变化的世界中茁壮成长:C++ 2006–2020
二、多态
2.1 虚函数表内存模型
虚函数表是 C++ 等语言实现动态分派的核心机制(Java 所有的实例方法也默认使用虚函数表实现)。
- 基本原理:编译器会为每个包含虚函数的类创建单独的虚函数表,并在对象的内存模型中生成包含虚函数表指针(
vptr)的代码。 - 复杂继承场景:
- 多继承和指针修正 (thunks)
- 菱形继承与虚基类
2.2 方法派发 (Dispatch) 机制
派发机制是指选择正确方法调用的过程。按照选择正确方法的时期(编译期或运行期),主要分为以下两种:
静态派发 (Static Dispatch)
在编译期就完全确定了调用方法的分派方式。也就是说,编译期直接决定了函数地址(方法指针),运行时可以直接通过函数地址调用方法。
延伸:内联 (Inline) 静态派发的一种进一步优化方式。编译器将方法的实现直接平铺在调用方的可执行文件内,从而节省了函数调用的消耗。
- 与宏展开的区别:内联发生在编译期,且不会改变源文件;宏展开在预编译期完成,直接替换源码本身后再进行编译。
- 建议性而非强制性:
inline关键字只是一个 desire 声明而非 require。它只能告诉编译器倾向使用内联方式,最终由编译器定夺。- 适用场景:如果一个方法被内联 10 次,会出现 10 份方法的副本。因此内联适用于频繁调用的短小方法;如果方法特别大,即使加了
inline修饰,编译器也可能拒绝内联。
动态派发 (Dynamic Dispatch)
在运行期根据对象的实际类型,选择调用方法实现的流程(通常就是通过上述的虚函数表来完成)。
对比:迟绑定 (Late Binding) Late Binding(也叫 Dynamic Binding 或 Dynamic Linkage)是指在运行时通过对象调用方法或函数名的一套机制(简单的说,就是编译期并不解读足够的信息去确定方法是否存在)。
- 优势:在组件对象模型(COM)编程中,最大优势在于不要求编译器在编译期间去引用包含对象的库,这能有效避免类的虚函数表突然更改带来的冲突。
- 适用场景:大部分的动态类型语言(如 Python、Ruby 等)都可以在运行时去修改对象的方法列表,因此它们深度依赖 Late Binding;而 C++ 更多使用基于虚函数表的动态派发。
三、模版
模板分为函数模板(function template)和类模板(class template), 一般在头文件中定义, 可能被包含多次,编译和链接时会消除等价模板实例;
C++ 模板有以下特点:
- 图灵完备
- 函数式编程
所谓元编程,就是编写直接生成或操纵程序的程序,而 C++ 模板赋予了 C++ 语言提元编程的能力。
图灵完备
C++ 模板最初是为实现泛型编程设计的,但人们发现模板的能力远远不止于最初的设计初衷。一个重要理论结论就是:C++ 模板是图灵完备的(Turing-complete)。也就是说,C++ 模板元编程是"意外"功能,而不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。
C++ 模板是图灵完备的,主要体现在具备以下能力:
- 编译期数值计算
- 类型计算
- 代码计算(如循环展开)
从编程形式来看,模板的“<>”中的模板参数相当于函数调用的输入参数,模板中的 typedef 或 static const 或 enum 定义函数返回值(类型或数值,数值仅支持整型,如果需要可以通过编码计算浮点数)
依赖以下两个能力,C++ 模板具有了和普通语言一样通用的能力(图灵完备性):
- 依赖 C++ 模板的特例化, 模板拥有了条件判断能力
- 依赖 C++ 模板的递归嵌套, 模板拥有了循环的能力
所以,可以确定,能够用模板实现类似普通程序中的 if 和 while 语句。
理论上说 C++ 模板可以执行任何计算任务,但实际上因为模板是编译期计算,其能力受到具体编译器实现的限制(如递归嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。
数值计算的实际意义不太大,但可以很好证明 C++ 模板的能力,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)
代码计算是通过类型计算进而选择类型的函数实现的(C++ 属于静态类型语言,编译器对类型的操控能力很强)。
类型计算, 比如类型萃取, 依赖模板的特化来实现

模板特例化(template specialization)
模板的特例化分完全特例化(full specialization)和部分特例化(partial specialization)。实例化将会匹配参数集合最小的特例。
模板特化时,使用类型等价判断规则确定使用(模板的多个实例中的)哪一个模板:
- 同一个模板(模板名及其参数类型列表构成的模板签名 template signature 相同)且指定的模板实参等价(类型参数是等价类型,非类型参数值相同)
- 函数模板可以重载,类模板不存在重载
可变模版参数(variadic templates),C++11新增的最强大的特性之一。可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归;
函数式编程
从编程范型(programming paradigm)上来说,C++ 模板是函数式编程(functional programming),它的主要特点是:函数调用不产生任何副作用(没有可变的存储,都是编译期常量),用递归形式实现循环结构的功能。
函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。
关键字:template、typename、this
template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果;
C++标准规定,如果解析器在一个模板中遇到一个嵌套依赖名字,它假定那个名字不是一个类型,除非显式用 typename 关键字前置修饰该名字;
和上一条 typename 用法类似,template 用于指明嵌套类型或函数为模板;
this 用于指定查找基类中的成员(当基类是依赖模板参数的类模板实例时,由于实例化总是推迟,这时不依赖模板参数的名字不在基类中查找)。
特性 (Traits)、策略 (Policy)、标签 (Tag)
特性、策略、标签是模板编程的常用技巧,它们可以让模板变得更加通用。
特性 (Traits):一般是对类型的信息(如
value_type、reference)进行包装,使得上层代码可以以统一的接口访问这些信息。cpp// Traits 示例:提取类型的 value_type template <typename IterT> struct iterator_traits { typedef typename IterT::value_type value_type; };说明:如果这些类型的信息的访问方式不一致(如迭代器和指针),通过加一层 Traits 可以统一接口,避免大量重复代码。
策略 (Policy):一般是一个类模板,将模板中经常变化的那一部分子功能块集中起来作为模板参数。
cpp// Policy 示例:STL 容器的分配器 template<class T, class Alloc = allocator<T>> class vector;说明:
allocator<T>就是一种内存分配策略,带有默认参数。标签 (Tag):一般是一个空类,作为独一无二的类型名字用于标记某些属性,方便在编译期进行函数重载决议。
cpp// Tag 示例:STL 迭代器类型标签 struct input_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {};
缺点
C++ 模板编程主要面临以下两个挑战:
- 难以调试:如果出错,会产生冗长且难以阅读的编译错误信息。
- 缓解方案:可以使用
static_assert或 C++20 的concept增加前置检查代码,让编译器尽早且清晰地报错。
- 缓解方案:可以使用
- 代码膨胀:包括源代码膨胀和二进制对象文件膨胀。
- 缓解方案:使用特性、策略等让模板更通用,或者将非依赖模板参数的逻辑抽离到基类中,以合并模板实例。
Reference
四、导入 (Import) 导出 (Export)
4.1 对象类型
支持导出的实体包括:函数、接口、类、成员函数、成员变量。
4.2 导出配置
在构建动态链接库时,需要明确声明哪些符号是对外可见的:
- Windows / MSVC 体系:
- 使用模块定义文件 (
.def) - 使用声明修饰符:
__declspec(dllexport)和__declspec(dllimport)
- 使用模块定义文件 (
- Linux / GCC 体系:
- 使用可见性修饰符:
__attribute__((visibility("default")))和__attribute__((visibility("hidden"))) - 编译器链接选项:
-fvisibility=default(GCC 默认全部可见)或-fvisibility=hidden
- 使用可见性修饰符:
4.3 动态库加载方式
显式加载 (Explicit Linking)
在运行时通过系统 API(如 Windows 的 LoadLibrary/GetProcAddress,Linux 的 dlopen/dlsym)从导出表获取函数地址,然后通过函数指针进行调用。
隐式加载 (Implicit Linking)
编译期间依赖于 DLL 生成的导入库(.lib 文件),并在代码中直接引用头文件。.lib 文件包含了导出的符号信息,在模块被加载时会主动寻找 .dll/.so 文件并关联对应的符号。
关于
__declspec(dllimport)的深度解析典型的宏定义模式(同一个头文件兼顾导出与导入):
cpp#ifdef _EXPORTING #define API_DECLSPEC __declspec(dllexport) #else #define API_DECLSPEC __declspec(dllimport) #endif根据 MSDN:
- 对于函数而言,使用
dllimport是可选的(没有它也能链接成功),但使用了它会让编译器生成更高效的代码。- 对于公共数据符号和对象(全局变量/类的静态成员等),你必须使用
dllimport才能正确访问。底层效率差异示例(假设调用 DLL 函数
func()):
- 没有
dllimport:编译器产生常规的call func,链接器会生成一个中间跳转桩(Thunk):0x40000001: jmp DWORD PTR __imp_func。程序先跳到 Thunk,再通过导入表跳转。- 有
dllimport:链接器不会产生 Thunk,而是直接生成间接调用:call DWORD PTR __imp_func。有效减少了目标代码大小并提升了执行效率。
注意事项:在导出类、成员函数、成员变量时,需要注意它们在不同模块间的内存归属及释放问题(避免跨模块的堆内存分配与释放)。
五、名称修饰 (Name Mangling)
名称修饰是编译器为了支持函数重载、命名空间等特性,在编译阶段将函数签名重新编码为唯一符号串的过程。不同的编译器有不同的修饰规则。
5.1 查看工具
- Linux:使用 GCC 编译后,用
nm命令查看.o或.so中的符号。 - Windows:使用 cl 编译(参数
-c)后,用dumpbin /SYMBOLS命令查看符号。
5.2 注意事项
- 使用模块定义文件 (
.def) 导出的 C 函数,不会使用名称修饰,保持原名。 - Microsoft 提供了
UnDecorateSymbolName()API,可将修饰后的名称还原(Demangle)为函数签名。 - VC 可使用
#pragma修改导出名称:#pragma comment(linker,"/EXPORT:MyExportFunction=_MyExportFunction@4")。
5.3 编译器名称修饰规则对比表
| 语言 | 编译器 | 架构 | 调用约定 | 修饰前示例 | 修饰后格式 / 示例 |
|---|---|---|---|---|---|
| C | MSVC | x86 | __stdcall | func(int, int) | _func@8 (下划线前缀 + @ + 参数字节数) |
| C | MSVC | x86 | __fastcall | func(int, int) | @func@8 (@前缀 + @ + 参数字节数) |
| C | MSVC | x64 | 任何约定 | func | func (保持原名不变) |
| C | GCC | x86 | __cdecl | func | _func (仅加下划线前缀) |
| C | GCC | x64 | __cdecl | func | func (保持原名不变) |
| C++ | MSVC | Any | Any | int C2::func(int) | ?func@C2@C@@AAEHH@Z (以?开头,包含类名、访问控制、参数返回类型等,以Z结尾) |
| C++ | GCC | Any | Any | N::C::func(int) | _ZN1N1C4funcEi (以_Z开头,N表示嵌套,长度+名称拼合,结尾接参数类型如i代表int) |
六、特性
6.1 const 关键字
- 修饰指针:通过判断
const在*的左右侧来确定语义:const int* p(或int const* p):底层 const,指针指向的值不能通过p修改。int* const p:顶层 const,指针本身的地址不能被修改。
- 修饰成员变量:必须在构造函数的初始化列表中进行初始化。
- 修饰成员函数:
void func() const;表示该函数承诺不修改其所在对象的任何非静态成员变量。const成员函数内部只能调用其他const成员函数。
- 修饰对象:
const ClassName obj;- 该对象的任何非 const 成员函数都不能被调用,因为非 const 成员函数有修改成员变量的企图。
- 类型转换:使用
const_cast<type_id>(expression)可以移除变量的 const 属性(通常用于适配非 const 签名的遗留 API)。
6.2 内存对齐 (Attribute & Pack)
在处理跨平台网络协议或内存映射时,常需要精细控制结构体的内存对齐:
#if defined(__GNUC__)
/* __attribute__ 是 GCC 特有机制 */
typedef struct {
char a;
int b;
} __attribute__((packed)) position_t; // 取消优化对齐,紧凑排列
#endif
/* 微软 MSVC 及跨平台的 #pragma pack 方式 */
#if (_MSC_VER >= 800 && !defined(_M_I86)) || defined(_PUSHPOP_SUPPORTED)
#pragma pack(push, 1) // 保存当前对齐状态,并设为 1 字节对齐
struct test {
char a;
int b;
};
#pragma pack(pop) // 恢复之前的对齐状态
#else
#pragma pack(1)
#endif6.3 继承体系下的对象模型
单一继承
- 形式:
class D : public B {} - 特点:派生类对象内存布局中,基类子对象位于最上方。若包含虚函数并发生了 Override,派生类会拥有自己的虚函数表,重写的方法会覆盖虚表中的基类槽位。
多重继承
- 形式:
class D : public B1, public B2, public B3 {} - 特点:
- 派生类会和第一个基类公用
vptr(虚表指针)和虚表。 - 内存调整:派生类转换为第二、第三个基类指针时,编译器会自动进行偏移调整。
- 新实体引入:为了支持虚函数调用和指针调整,虚表中会引入
offset_to_top(记录当前子对象到对象首地址的偏移)以及non-virtual thunk(一段简短的汇编代码,用于在调用方法前调整this指针的偏移量)。
- 派生类会和第一个基类公用
菱形继承 (虚拟继承)
- 形式:
class D : public B1, public B2 {}; class B1 : virtual public BB {}; class B2 : virtual public BB {}; - 特点:用于解决菱形继承导致的最顶层基类(
BB)数据冗余和二义性问题。对象模型中会引入虚基类表(vbtable)指针,所有继承路径共享同一个虚基类实例。
注意事项与辅助工具
- 派生类子对象与基类子对象之间也会遵循内存对齐 (Alignment) 规则。
- 对象模型查看利器:
- Compiler Explorer (godbolt.org):可直接看各编译器的汇编输出。
- C++ Insights (cppinsights.io):可查看编译器为你生成的幕后 C++ 源码(如转型、虚表设定)。
七、Traps
1. Enum
1.1 疑问
class Test {
public:
enum Result {
Result_OK = 0x1000,
};
Test(enum Result emRst) {
m_nRst = emRst;
}
static int m_nRst;
};
int Test::m_nRst = 0; /* OK */
// Test::m_nRst = 0; /* ERROR, 此声明没有存储类或类型说明符 */
int main() {
Test(Test::Result(Test::Result_OK)); /* OK */
Test((Test::Result)Test::Result_OK); /* OK */
// Test(Test::Result_OK); /* ERROR, 当前范围内无法定义 constant "Test::Result_OK" */
}为什么上述代码在去掉注释"//"后,vs2019编译器会提示错误?
1.2 背景
我们需要明白,enum 关键字会定义一个类型,同时,其内定义的项目会被展开为int型常量到定义 enum 的作用域。
这会造成,在全局位置定义的 enum 类型中的项目名需要是全局唯一的, 如下述定义会编译出错(error C2365: “a”: 重定义):
enum X { a };
enum Y { a };由此,为了解决作用域 以及 强类型限定问题,C++11 中引入了 enum class 或 enum struct。
1.3 解答
- 全局位置的 ERROR, 此声明没有存储类或类型说明符:
- c/c++ 没有全局代码块,在全局位置的是 声明或定义
- 传递枚举值到枚举类型参数时 ERROR, 当前范围内无法定义 constant:
- ?
- vs2019 会报错,但,g++ 在 linux 下编译正常
- 可能是:强类型限定造成现在拒绝枚举量与int之间的隐式转换
2. Array & Pointer
2.1 疑问
// define.c
int arr[] = { 1,2,3 };
int* arm = &arr[0];
// test.c
int art[] = { 5,6,7,8,9 };
extern int* arr;
void test() {
int* x = arr + 1;
int* m = art + 1;
}请问 test 函数中的 x 和 m 分别是多少?(int)x = 5, *m = 6
同我们的认知:数组可以当成指针使用 不一样,对吧...
2.2 解答
将上述代码编译成汇编后的代码如下:
- define.cpp
.file "define.cpp"
.text
.globl arr
.data
.align 8
.type arr, @object
.size arr, 12
arr:
.long 1
.long 2
.long 3
.globl arm
.section .data.rel.local,"aw"
.align 8
.type arm, @object
.size arm, 8
arm:
.quad arr
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5- test.cpp
.file "test.cpp"
.text
.globl art
.data
.align 16
.type art, @object
.size art, 20
art:
.long 5
.long 6
.long 7
.long 8
.long 9
.text
.globl test
.type test, @function
test:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq arr(%rip), %rax
addq $4, %rax
movq %rax, -16(%rbp)
leaq 4+art(%rip), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
movl %eax, -20(%rbp)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size test, .-test
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5观察:
define.cpp中 arr 和 arm 的定义test.cpp中 arr(%rip) 和 art(%rip) 的操作
可以发现:
- 数组可以当成指针使用,的原因是:编译器帮忙做了取地址操作
- 因为
extern int* arr在使用时缺少了编译器帮忙做取地址操作这一动作,直接被把 arr 当成了 指针使用:具体的方式可以参考 arm 的定义:直接把 arm 的值当成了地址使用
3. Single Parameter Constructor
C++中单参数构造函数是可以被隐式调用的,主要有两种情形会隐式调用单参数构造函数:
- 同类型对象的拷贝构造
- 即用相同类型的其它对象来初始化当前对象
- 不同类型对象的隐式转换
- 即其它类型对象隐式调用单参数拷贝构造函数初始化当前对象。比如 A a=1; 就是隐式转换,而不是显示调用构造函数,即A a(1); 像 A(1) 这种涉及类型转换的单参数构造函数,又被称为转换构造函数(Converting Constructor)
可以通过 explict 关键字避免这种隐式调用。
4. HeapOnly & StackOnly
#include <iostream>
class HeapOnly {
public:
HeapOnly() { }
void destroy() const { delete this; }
private:
~HeapOnly() { }
};
class StackOnly {
public:
StackOnly() { }
~StackOnly() { }
private:
void* operator new(size_t);
};
int main() {
StackOnly s; // ok
StackOnly *p = new StackOnly; // wrong
HeapOnly *p = new HeapOnly; // ok
p->destroy();
HeapOnly h; // wrong
return 0;
}5. 在头文件中定义全局变量
// test.h
// 如果此头文件被多次包含,会报重定义错误
int global_value = 100;
// 如果此头文件被多次包含,不会报错,,但问题更隐蔽、严重
// 因为:包含了该头文件的所有源文件中都定义了此变量,即该头文件被包含了多少次,此变量就定义了多少次
// 验证:在多个源文件中包含该头文件,然后在每个源文件中改变变量的值并输出值、以及变量地址。对比输出,如果都不同,则推测得证;否则推测错误
static int global_value = 100;
// 常见的在头文件中定义全局变量的方式(在某个源文件中定义并赋值)。
extern int global_value;
// __declspec(selectany) 是 Microsoft Visual C++ 提供的一个特定关键字,
// 它的主要作用是告诉编译器,如果某个全局变量或静态变量在多个编译单元(通常是多个源文件)中被定义多次,编译器只选用其中一个定义,而忽略其他重复的定义
__declspec(selectany) int global_value = 100;
// inline 关键字:C++17及以后的标准,作用与 __declspec(selectany) 类似,不同的是 inline 是标准 C++ 的一部分,因此具有更好的可移植性
inline int global_value = 100;Reference
- 《深度探索C++对象模型》
- 《Effective C++》
- 《More Effecitve C++》
- 《C专家编程》
- 《C陷阱与缺陷》
- 《C++ Templates》
- gcc.gnu.org/onlinedocs
