Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

基础语法与编译模型 / Language & Compilation

1. C++ 语言基础完整学习手册 / Complete C++ Language Basics Guide

2. C++ 语言基础与程序运行 / C++ Language Basics and Program Execution

3. C++ 程序是怎样做出来的:从源码到可执行文件 / From Source to Executable

4. 值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects

5. 表达式、运算符与控制流 / Expressions, Operators, and Control Flow

6. 函数、作用域与重载 / Functions, Scope, and Overloading

7. 数组、指针与字符串:从地址到借用范围 / Arrays, Pointers, and Strings

8. 引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction

9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases

10. 一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces

11. 基础 I/O、错误与调试:让程序会观察自己的失败 / Basic I/O, Errors, and Debugging

本页目录

一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces ​

1. 先看一个让人困惑的现象 ​

我们先创建三个文件。

第一个文件叫 math.hpp:

cpp
#pragma once

int add(int left, int right);
1
2
3

第二个文件叫 first.cpp:

cpp
#include "math.hpp"

int first_result() {
    return add(1, 2);
}
1
2
3
4
5

第三个文件叫 main.cpp:

cpp
#include "math.hpp"
#include <iostream>

int first_result();

int main() {
    std::cout << first_result() << '\n';
}
1
2
3
4
5
6
7
8

这里 math.hpp 被两个 .cpp 文件包含。

很多初学者会问:

同一个头文件被包含了两次,为什么没有立刻报错?

把头文件改成直接放函数定义:

cpp
#pragma once

int add(int left, int right) {
    return left + right;
}
1
2
3
4
5

再让两个 .cpp 都包含它,可能会在链接阶段看到:

text
multiple definition of add(int, int)
1

于是出现了一个值得追问的问题:

text
同一个头文件
  |
  +-- 被 first.cpp 看见
  +-- 被 main.cpp 看见

为什么声明通常没问题?
为什么普通函数定义可能出问题?
1
2
3
4
5
6
7

答案要经过几个阶段:

text
头文件
  |
  | #include
  v
预处理后的记号
  |
  v
两个独立的翻译单元
  |
  v
两个目标文件
  |
  v
链接器检查定义和符号
1
2
3
4
5
6
7
8
9
10
11
12
13
14

本文会实际观察:

  • #include 到底做了什么;
  • .cpp 文件为什么分别编译;
  • include guard 解决什么、不能解决什么;
  • 宏为什么绕过类型系统;
  • 头文件应该放声明还是定义;
  • 命名空间怎样影响名字查找;
  • 命名空间为什么不是权限或二进制隔离;
  • 这些内容如何连接到 ODR 和链接。

2. 本文基础词小注释 ​

2.1 预处理器 ​

预处理器是正式 C++ 编译前运行的一步。

它处理以 # 开头的指令:

cpp
#include
#define
#if
#ifdef
#endif
1
2
3
4
5

它更像记号处理工具。

它不会完整理解:

  • 类型;
  • 对象生命周期;
  • 函数重载;
  • 类不变量;
  • 模板约束。

2.2 头文件 ​

头文件通常使用 .h 或 .hpp 扩展名。

它常用来提供多个源文件需要知道的接口:

text
函数声明
类型定义
类声明
模板定义
常量和 inline 定义
1
2
3
4
5

头文件不是运行时文件。

程序运行时不会打开头文件。

它在构建阶段被处理。

2.3 翻译单元 ​

翻译单元可以先理解为:

text
一个 .cpp 文件
  +
递归展开的头文件内容
  +
宏和条件编译处理后的结果
1
2
3
4
5

例如:

text
main.cpp
  +-- include math.hpp
  +-- include iostream
  +-- 自己的 main()
1
2
3
4

预处理结束后,这些内容组成一个供编译器处理的整体。

2.4 声明和定义 ​

声明告诉编译器“它是什么”:

cpp
int add(int, int);
1

定义提供“它怎样工作”:

cpp
int add(int left, int right) {
    return left + right;
}
1
2
3

声明可以让当前 .cpp 完成类型检查。

定义通常需要在链接阶段被找到。

2.5 宏 ​

宏是预处理器保存的替换规则:

cpp
#define LIMIT 10
1

它不是变量,也不是函数。

2.6 命名空间 ​

命名空间为名字提供组织层级:

cpp
namespace app {
int version;
}
1
2
3

使用时:

cpp
app::version
1

:: 是作用域解析运算符。

3. #include 到底做了什么 ​

3.1 一个最小头文件 ​

创建 message.hpp:

cpp
#pragma once

const char* message();
1
2
3

创建 main.cpp:

cpp
#include "message.hpp"
#include <iostream>

int main() {
    std::cout << message() << '\n';
}
1
2
3
4
5
6

先把:

cpp
#include "message.hpp"
1

近似理解为:

text
把 message.hpp 中的内容放到这里
1

于是编译器看到的概念结果接近:

cpp
const char* message();

// <iostream> 展开出的许多内容

int main() {
    std::cout << message() << '\n';
}
1
2
3
4
5
6
7

这不是严格的普通字符串替换。

预处理器处理的是预处理记号。

但这个近似模型足够解释大多数头文件问题。

3.2 角括号和双引号 ​

cpp
#include "project/config.hpp"
#include <iostream>
1
2

常见约定:

text
"..."
  +-- 优先寻找项目相关头文件

<...>
  +-- 优先寻找系统或工具链头文件
1
2
3
4
5

真实搜索顺序还受编译器和包含路径选项影响。

先记用途:

text
项目接口
  -> "..."

标准库或外部库接口
  -> <...>
1
2
3
4
5

3.3 递归包含 ​

text
main.cpp
  |
  +-- app.hpp
          |
          +-- config.hpp
                  |
                  +-- types.hpp
1
2
3
4
5
6
7

最终翻译单元会包含这条依赖链。

所以一个公共头文件改变,可能让许多 .cpp 重新编译:

text
types.hpp 改动
  |
  v
包含它的头文件内容变化
  |
  v
更多翻译单元需要重新编译
1
2
3
4
5
6
7

头文件影响正确性,也影响构建时间。

4. 第一个实验:观察预处理结果 ​

4.1 准备两个文件 ​

config.hpp:

cpp
#pragma once

#define PROJECT_LIMIT 8
1
2
3

main.cpp:

cpp
#include "config.hpp"

int main() {
    return PROJECT_LIMIT;
}
1
2
3
4
5

4.2 最简单的预处理命令 ​

bash
g++ -E main.cpp -o main.i
1

拆开看:

text
g++
  +-- 启动 C++ 编译器工具

-E
  +-- 只做预处理

main.cpp
  +-- 输入源文件

-o main.i
  +-- 指定输出文件名
1
2
3
4
5
6
7
8
9
10
11

打开 main.i,搜索 PROJECT_LIMIT。

通常它已经变成:

cpp
return 8;
1

这说明:

text
#define PROJECT_LIMIT 8
  |
  v
预处理器替换记号
  |
  v
C++ 编译器看到替换后的结果
1
2
3
4
5
6
7

4.3 预处理器不知道业务含义 ​

cpp
#define PROJECT_LIMIT 8
1

预处理器不知道它表示:

  • 重试次数;
  • 队列长度;
  • 线程数量;
  • 包大小。

如果不需要条件编译,可以优先写:

cpp
inline constexpr int project_limit = 8;
1

现在它有类型、有作用域,也能被调试器理解。

5. include guard:同一个翻译单元不要重复展开 ​

5.1 菱形包含 ​

types.hpp:

cpp
struct Point {
    int x;
    int y;
};
1
2
3
4

geometry.hpp:

cpp
#include "types.hpp"

int distance_squared(Point point);
1
2
3

drawing.hpp:

cpp
#include "types.hpp"

void draw(Point point);
1
2
3

main.cpp:

cpp
#include "geometry.hpp"
#include "drawing.hpp"

int main() {
    Point point{3, 4};
    draw(point);
}
1
2
3
4
5
6
7

依赖图:

text
             +-------------+
             | types.hpp   |
             +-------------+
                ^       ^
                |       |
       +--------+       +--------+
       |                         |
+--------------+       +--------------+
| geometry.hpp |       | drawing.hpp  |
+--------------+       +--------------+
       ^                         ^
       |                         |
       +------------+------------+
                    |
                main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

types.hpp 通过两条路径到达 main.cpp。

没有保护时,Point 可能被定义两次。

5.2 经典 include guard ​

cpp
#ifndef PROJECT_TYPES_HPP
#define PROJECT_TYPES_HPP

struct Point {
    int x;
    int y;
};

#endif
1
2
3
4
5
6
7
8
9

处理过程:

text
第一次到达
  |
  +-- PROJECT_TYPES_HPP 未定义
  +-- 定义它
  +-- 保留 Point 内容

第二次到达
  |
  +-- PROJECT_TYPES_HPP 已定义
  +-- 跳过 Point 内容
1
2
3
4
5
6
7
8
9
10

5.3 #pragma once ​

cpp
#pragma once

struct Point {
    int x;
    int y;
};
1
2
3
4
5
6

它要求主流编译器在同一个翻译单元中只处理一次该文件。

写法简短,主流工具链普遍支持。

但它不是 ISO C++ 标准中的普通预处理指令。

项目应统一选择一种风格。

5.4 guard 的边界 ​

它解决:

text
同一个翻译单元
  +-- 通过多条 include 路径重复到达同一头文件
1
2

它不解决:

text
不同 .cpp
  +-- 各自有独立宏环境
  +-- 各自处理一次头文件
1
2
3

也不解决:

text
多个 .cpp 都看到同一个普通函数定义
  +-- 跨翻译单元重复定义
  +-- 需要 ODR 和链接规则处理
1
2
3

6. 头文件应该放声明还是定义 ​

6.1 推荐的拆分 ​

math.hpp:

cpp
#pragma once

int add(int left, int right);
1
2
3

math.cpp:

cpp
#include "math.hpp"

int add(int left, int right) {
    return left + right;
}
1
2
3
4
5

main.cpp:

cpp
#include "math.hpp"
#include <iostream>

int main() {
    std::cout << add(20, 22) << '\n';
}
1
2
3
4
5
6

构建关系:

text
math.cpp + math.hpp
  |
  v
math.o

main.cpp + math.hpp
  |
  v
main.o

math.o + main.o
  |
  v
app
1
2
3
4
5
6
7
8
9
10
11
12
13
14

头文件让 main.cpp 知道接口。

math.cpp 提供唯一的普通函数定义。

6.2 为什么声明足够编译 ​

编译 main.cpp 时,编译器需要知道:

text
函数名
参数类型
返回类型
1
2
3

它可以先完成调用的类型检查。

函数机器码稍后由链接器寻找。

text
编译 main.cpp
  +-- 检查 add 调用是否匹配
  +-- 产生对 add 的未完成引用

链接
  +-- 从 math.o 找到 add 定义
  +-- 补齐引用
1
2
3
4
5
6
7

6.3 普通定义放头文件的风险 ​

cpp
#pragma once

int add(int left, int right) {
    return left + right;
}
1
2
3
4
5

如果两个 .cpp 都包含它:

text
first.cpp
  +-- add 定义 A

main.cpp
  +-- add 定义 B
1
2
3
4
5

链接器可能拒绝把两个同名外部定义放进同一程序。

6.4 inline 不等于一定内联 ​

cpp
inline int add(int left, int right) {
    return left + right;
}
1
2
3

这里 inline 的主要语言意义是允许满足一致性要求的多翻译单元定义。

它不保证:

text
编译器一定把调用展开到调用点
1

是否真正内联,是优化器的决定。

6.5 头文件中常见的合法定义 ​

通常可以放:

  • 类型定义;
  • 模板定义;
  • inline 函数;
  • constexpr 函数;
  • inline constexpr 变量;
  • 枚举定义;
  • 必要声明。

但“可以放”不等于“应该无限放”。

头文件越重,包含它的源文件编译成本越高。

7. 头文件自包含 ​

7.1 隐式依赖 ​

错误的 user.hpp:

cpp
#pragma once

struct User {
    std::string name;
};
1
2
3
4
5

它使用 std::string 却没有包含 <string>。

如果调用者碰巧先写:

cpp
#include <string>
#include "user.hpp"
1
2

可能暂时能编译。

换顺序:

cpp
#include "user.hpp"
#include <string>
1
2

可能失败。

7.2 正确写法 ​

cpp
#pragma once

#include <string>

struct User {
    std::string name;
};
1
2
3
4
5
6
7

头文件应直接包含它使用的类型定义。

这叫 self-contained,意思是可以独立包含。

7.3 自包含实验 ​

创建 check_user_header.cpp:

cpp
#include "user.hpp"

int main() {}
1
2
3

只编译这个文件:

bash
g++ -std=c++17 -c check_user_header.cpp -o check_user_header.o
1

逐项解释:

text
-c
  +-- 只生成目标文件

check_user_header.cpp
  +-- 用来检查头文件是否能独立使用

-o check_user_header.o
  +-- 指定目标文件名
1
2
3
4
5
6
7
8

如果失败,说明头文件依赖了包含者的偶然顺序。

8. 前向声明:减少依赖 ​

8.1 指针不需要完整布局 ​

cpp
class Engine;

class Car {
public:
    void attach(Engine* engine);

private:
    Engine* engine_{};
};
1
2
3
4
5
6
7
8
9

class Engine; 是前向声明。

它只告诉编译器“有这样一种类型”。

指针的大小不依赖 Engine 对象的完整大小:

text
Engine;
  +-- 类型名字已知

Engine*
  +-- 只保存地址
1
2
3
4
5

8.2 什么时候需要完整类型 ​

cpp
class Engine;

class Car {
    Engine engine_;
};
1
2
3
4
5

按值成员需要知道 Engine 的完整布局。

通常还包括:

  • 继承;
  • sizeof(Engine);
  • 访问成员;
  • 某些析构函数位置;
  • 某些模板实例化。

前向声明不是越多越好。

目标是让依赖准确、编译边界清楚。

9. 宏:预处理器能做什么 ​

9.1 对象式宏 ​

cpp
#define MAX_RETRIES 3
1
cpp
int retries = MAX_RETRIES;
1

预处理器可能交给编译器:

cpp
int retries = 3;
1

它没有 C++ 类型。

更现代的写法:

cpp
inline constexpr int max_retries = 3;
1

这个名字有类型、有作用域和调试信息。

9.2 函数式宏 ​

cpp
#define SQUARE(x) ((x) * (x))
1

括号可以避免部分优先级错误:

cpp
SQUARE(1 + 2)
1

展开接近:

cpp
((1 + 2) * (1 + 2))
1

但:

cpp
int value = 3;
int result = SQUARE(value++);
1
2

展开接近:

cpp
int result = ((value++) * (value++));
1

参数出现两次,副作用也可能发生两次。

模板函数只会先取得一次参数值:

cpp
template <typename T>
constexpr T square(T value) {
    return value * value;
}
1
2
3
4

9.3 宏没有正常作用域 ​

cpp
#define VALUE 10

void f() {
    int VALUE = 20;
}
1
2
3
4
5

宏在正式编译前就处理。

它不是普通 C++ 名字。

因此宏名应有项目级前缀,并限制在小范围。

9.4 宏仍有合理用途 ​

条件编译:

cpp
#if defined(_WIN32)
    // Windows
#elif defined(__linux__)
    // Linux
#else
    #error "unsupported platform"
#endif
1
2
3
4
5
6
7

宏也能做:

  • 字符串化;
  • 记号拼接;
  • 编译器属性适配;
  • 平台差异选择。

业务逻辑优先交给 C++ 语言设施。

10. 条件编译会制造多个版本 ​

10.1 同一个源文件的两种结果 ​

cpp
#ifdef ENABLE_LOG
    log("start");
#endif
1
2
3

定义宏时,代码保留。

不定义时,代码被移除。

text
配置 A
  +-- ENABLE_LOG -> 翻译单元 A

配置 B
  +-- 没有 ENABLE_LOG -> 翻译单元 B
1
2
3
4
5

这意味着每种宏配置都需要被编译和测试。

10.2 宏影响公共布局 ​

cpp
struct Packet {
    int id;
#ifdef LARGE_PACKET
    int extra;
#endif
};
1
2
3
4
5
6

如果不同 .cpp 使用不同配置:

text
first.cpp
  +-- Packet 布局 A

second.cpp
  +-- Packet 布局 B
1
2
3
4
5

双方对同一个类型的大小理解不同。

这可能破坏 ODR、对象传递和 ABI。

10.3 把平台差异收敛到适配层 ​

不要在业务文件里堆几百行条件编译。

更清楚的结构:

text
公共接口
  |
  +-- Windows 实现
  |
  +-- Linux 实现
1
2
3
4
5

业务层只调用统一接口。

平台差异集中在少数 .cpp 文件中。

11. 命名空间:给名字增加上下文 ​

11.1 名字冲突 ​

cpp
namespace graphics {
struct Status {};
}

namespace network {
struct Status {};
}
1
2
3
4
5
6
7

使用:

cpp
graphics::Status graphics_status;
network::Status network_status;
1
2

命名空间图:

text
全局作用域
  |
  +-- graphics
  |     +-- Status
  |     +-- render()
  |
  +-- network
        +-- Status
        +-- connect()
1
2
3
4
5
6
7
8
9

11.2 嵌套命名空间 ​

旧写法:

cpp
namespace app {
namespace network {
struct Address {};
}
}
1
2
3
4
5

C++17:

cpp
namespace app::network {
struct Address {};
}
1
2
3

使用:

cpp
app::network::Address address;
1

11.3 命名空间不是权限系统 ​

命名空间不会自动:

  • 加密数据;
  • 隐藏动态库符号;
  • 阻止访问公开名字;
  • 提供进程隔离;
  • 保证 ABI 稳定。

它主要是源代码层面的名字组织机制。

12. using 声明和 using namespace ​

12.1 精确引入一个名字 ​

cpp
using std::cout;
1

之后可以写:

cpp
cout << "hello\n";
1

它只引入一个名字。

12.2 引入整个命名空间 ​

cpp
using namespace std;
1

它让整个 std 参与非限定查找。

在小型 .cpp 示例中可能方便。

不要在公共头文件全局作用域写它。

text
public.hpp
  |
  +-- using namespace std
  |
  v
所有包含者的名字查找范围被改变
1
2
3
4
5
6

公共头文件推荐:

cpp
#include <string>

namespace app {

struct User {
    std::string name;
};

}
1
2
3
4
5
6
7
8
9

13. 匿名命名空间和内部链接 ​

13.1 当前 .cpp 的私有辅助函数 ​

cpp
namespace {

int helper(int value) {
    return value * 2;
}

}
1
2
3
4
5
6
7

这个 helper 只供当前翻译单元使用。

可以理解为:

text
helper
  +-- 当前 .cpp 可以使用
  +-- 其他 .cpp 不通过同名外部符号使用
1
2
3

这叫内部链接效果。

13.2 不要随便放进头文件 ​

头文件中写:

cpp
namespace {
int helper = 1;
}
1
2
3

每个包含它的翻译单元可能得到自己的一份:

text
first.cpp  -> helper A
main.cpp   -> helper B
1
2

它们不是同一个共享对象。

14. 名字查找 ​

14.1 局部名字遮蔽 ​

cpp
int value = 10;

int main() {
    int value = 20;
    return value;
}
1
2
3
4
5
6

main 中的名字遮蔽了外层名字。

使用全局作用域:

cpp
int value = 10;

int main() {
    int value = 20;
    return ::value;
}
1
2
3
4
5
6

14.2 命名空间限定查找 ​

cpp
namespace app {
int value = 30;
}

int main() {
    return app::value;
}
1
2
3
4
5
6
7

限定名让查找路径明确。

14.3 ADL 的最小认识 ​

参数相关查找简称 ADL。

cpp
namespace geometry {

struct Point {};
void draw(Point);

}

int main() {
    geometry::Point point;
    draw(point);
}
1
2
3
4
5
6
7
8
9
10
11

编译器可能根据参数类型关联到 geometry 命名空间。

遇到歧义时先写:

cpp
geometry::draw(point);
1

先让调用目标明确,再研究 ADL。

15. 完整实验:从头文件到链接 ​

15.1 头文件 ​

calculator.hpp:

cpp
#pragma once

namespace learning {

int add(int left, int right);

}
1
2
3
4
5
6
7

15.2 实现文件 ​

calculator.cpp:

cpp
#include "calculator.hpp"

namespace learning {

int add(int left, int right) {
    return left + right;
}

}
1
2
3
4
5
6
7
8
9

15.3 调用文件 ​

main.cpp:

cpp
#include "calculator.hpp"
#include <iostream>

int main() {
    std::cout << learning::add(20, 22) << '\n';
}
1
2
3
4
5
6

15.4 分别编译 ​

bash
g++ -std=c++17 -c calculator.cpp -o calculator.o
g++ -std=c++17 -c main.cpp -o main.o
1
2

-c 表示只生成目标文件,不进行最终链接。

text
calculator.cpp -> calculator.o
main.cpp       -> main.o
1
2

15.5 链接 ​

bash
g++ calculator.o main.o -o calculator_app
1

这一步把目标文件组合成可执行文件。

15.6 运行 ​

bash
./calculator_app
1

输出:

text
42
1

15.7 故意漏掉实现 ​

bash
g++ main.o -o calculator_app
1

可能得到:

text
undefined reference to `learning::add(int, int)'
1

解释:

text
编译 main.cpp
  +-- 声明足够

链接 main.o
  +-- 没有 calculator.o 的定义
  +-- 找不到 add
1
2
3
4
5
6

头文件提供可见接口,不等于自动链接实现。

16. 常见错误与定位方法 ​

16.1 file not found ​

检查:

  • 文件名;
  • 当前目录;
  • include 路径;
  • 大小写;
  • -I 选项。

16.2 redefinition ​

可能是:

  • include guard 缺失;
  • 同一作用域重复定义;
  • 头文件中放了不该重复出现的外部定义。

先看包含关系,不要盲目加 inline。

16.3 undefined reference ​

通常是:

  • 漏掉 .o;
  • 声明和定义签名不一致;
  • 命名空间不一致;
  • 库没有参与链接。

16.4 宏产生怪异错误 ​

先查看预处理结果:

bash
g++ -E main.cpp -o main.i
1

在 main.i 中搜索宏名字。

观察:

text
源码写了什么
  |
  v
预处理后剩下什么
1
2
3
4

16.5 头文件依赖顺序错误 ​

把目标头文件放成第一个 include:

cpp
#include "project/widget.hpp"

int main() {}
1
2
3

不要先包含一大堆其他文件来掩盖缺失依赖。

17. 常见误解 ​

17.1 #include 是运行时导入 ​

不是。

它发生在构建阶段。

17.2 include guard 让所有 .cpp 只包含一次 ​

不是。

每个 .cpp 都有自己的翻译单元和宏环境。

17.3 头文件有声明就有实现 ​

不是。

声明让编译器知道接口。

实现还要参与链接。

17.4 inline 一定让程序更快 ​

不是。

它主要影响多翻译单元定义规则。

17.5 命名空间是私有区域 ​

不是。

它主要组织名字。

17.6 宏常量和 constexpr 完全一样 ​

不是。

宏没有 C++ 类型、作用域和正常调试语义。

17.7 包含头文件会自动链接库 ​

不是。

头文件让声明可见。

库和目标文件仍需要参与链接。

18. 练习:把“能编译”变成“知道为什么” ​

练习一:观察 include 展开 ​

准备一个很小的头文件。

运行:

bash
g++ -E main.cpp -o main.i
1

回答:

  • 头文件内容出现在什么位置?
  • 宏是否已替换?
  • <iostream> 为什么让结果变大?

练习二:制造重复定义 ​

把普通函数定义放入头文件。

让两个 .cpp 包含它。

分别编译,再链接。

记录错误发生在哪一步。

再把函数改成 inline,解释结果变化。

练习三:测试自包含 ​

每个公共头文件单独作为第一个 include。

失败时补上它直接需要的依赖。

练习四:比较宏和模板 ​

实现:

cpp
#define SQUARE(x) ((x) * (x))
1

以及:

cpp
template <typename T>
constexpr T square(T value) {
    return value * value;
}
1
2
3
4

分别传入 i++,解释为什么行为不同。

练习五:命名空间冲突 ​

创建两个命名空间,各自定义 Status。

用非限定名制造歧义。

再使用完整限定名解决。

19. 本篇总结 ​

text
头文件
  |
  | #include
  v
预处理记号
  |
  v
翻译单元
  |
  | 编译
  v
目标文件和符号
  |
  | 链接
  v
可执行程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

长期记住:

text
头文件
  +-- 让多个源文件看到共同接口

预处理器
  +-- 在 C++ 语义分析前处理 include、宏和条件编译

翻译单元
  +-- 一个 .cpp 加展开后的头文件内容

命名空间
  +-- 给名字增加组织层级

链接器
  +-- 把不同翻译单元中的定义和引用接起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14

写头文件时检查:

text
它需要哪些直接依赖?
  |
  v
它提供声明还是定义?
  |
  v
定义会不会被多个翻译单元看到?
  |
  v
名字是否在合适的命名空间?
  |
  v
宏是否真的不可替代?
1
2
3
4
5
6
7
8
9
10
11
12
13

20. 下一篇连接 ​

现在我们知道了项目如何由多个源文件组成。

下一篇会进入程序运行时:

text
输入字节
  |
  v
标准输入流
  |
  v
缓冲区和解析
  |
  v
成功、EOF 或格式错误
  |
  v
调试器和 Sanitizer
1
2
3
4
5
6
7
8
9
10
11
12
13

下一篇:基础 I/O、错误与调试

最后更新于:

Pager
上一篇9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases
下一篇11. 基础 I/O、错误与调试:让程序会观察自己的失败 / Basic I/O, Errors, and Debugging

持续记录,持续成长

Copyright © Tidenflow