Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions documents/compilation/01-compilation-and-linking-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ extern_var |0000000000000000| D | OBJECT|0000000000000004

​ 您可以看到,demo_extern解决的是extern_var的定义,但是`extern_func`的定义没找到,咱们又只给了这两个文件,自然连接器不知道上哪找到你的`extern_func`,自然也就会爆这个错误。

​ 我们现在知晓了链接器的重要功能——解决最小可执行文件(为什么是最小的呢?我们之后继续讨论)的符号未定义问题。任何那些**你没提供对应信息告知定义的具体内容(那些用了的函数的源代码漏写)**的链接都会失败!最后当链接器搜寻一圈后,只要存在未定义符号(也就是nm或者dumpbin中Class是U的符号),链接器就会拉起报错:告诉你所有那些没有定义的符号。**这个时候你的解决方案非常简单——找到这些符号的可重定位文件(一般构建系统的源代码文件名和可重定位文件名相同,只有后缀不同),然后链接的时候提供!**这是所有无动态库的编译场景下解决`undefined reference`的**唯一办法**。
​ 我们现在知晓了链接器的重要功能——解决最小可执行文件(为什么是最小的呢?我们之后继续讨论)的符号未定义问题。任何那些**你没提供对应信息告知定义的具体内容**(那些用了的函数的源代码漏写)的链接都会失败!最后当链接器搜寻一圈后,只要存在未定义符号(也就是nm或者dumpbin中Class是U的符号),链接器就会拉起报错:告诉你所有那些没有定义的符号。**这个时候你的解决方案非常简单——找到这些符号的可重定位文件(一般构建系统的源代码文件名和可重定位文件名相同,只有后缀不同),然后链接的时候提供**!这是所有无动态库的编译场景下解决`undefined reference`的**唯一办法**。

​ 现在我们看了nm的输出,就可以回答整个问题了:

Expand Down Expand Up @@ -409,15 +409,15 @@ collect2: error: ld returned 1 exit status

```

​ 您注意到了,还是一样,因为编译器相信**链接器可以正确的处理任何符号的关系**(他只能一分一分的编译文件!他管不了全局其他的源文件!**整个结果单元(包含可执行文件,动态库和静态库)的符号裁决由链接器决定!**这是笔者要再强调一次的!)
​ 您注意到了,还是一样,因为编译器相信**链接器可以正确的处理任何符号的关系**(他只能一分一分的编译文件!他管不了全局其他的源文件!**整个结果单元(包含可执行文件,动态库和静态库)的符号裁决由链接器决定**!这是笔者要再强调一次的!)

​ 所以,链接的时候,链接器发现两个文件中居然存在一模一样的符号定义。自然,定义是不一样,就像您即说A是1,又说A是2,唯一性被打破,贸然决定只会让程序变得不可控。所以,链接器自然一巴掌闪回来,不予通过!至少在今天的GNU工具链的默认行为下,您这样做智慧得到一个`multiple definition`。

## 那链接器的作用就这样?

​ 我都这样问了,怎么可能就这样是不是?不知道您看到我反复强调这句话的时候,您有没有感受:

- 为什么是:**C/C++编译型语言允许你在编译的时候只出现声明而不用出现实现!**为什么不要求立马知道呢?好麻烦啊。
- 为什么是:**C/C++编译型语言允许你在编译的时候只出现声明而不用出现实现**!为什么不要求立马知道呢?好麻烦啊。

​ 您冷静想一下,举个例子。我让您去邮局送一个邮件,您显然不会打断我:"闭嘴伙计,您先把邮局扛过来我看到邮件了我在帮你送",比起来,您更加会在脑子里绘制出假象的邮局,"嗯很,我需要去一个叫邮局的地方帮忙送一个邮件"。您自然回去其他地方寻找邮件。这就是一样的道理。我们空余出来悬而未决的符号,我们自己管理和承诺他们都会出现在对应的地方——**这是您的责任而不是编译器的责任**。那好,我们现在就可以继续我们的疑问:

Expand Down Expand Up @@ -450,7 +450,7 @@ collect2: error: ld returned 1 exit status
> 快速的说说细节:
>
> - 在 **UNIX** 系统上,用于生成静态库的命令通常是 **`ar`**,生成出的库文件通常带有 **`.a`** 扩展名。这些库文件通常还以 **"lib"** 作为前缀,并在传递给链接器时使用 **`"-l"`** 选项,后接库的名称(不带前缀和扩展名)。例如,**`"-lfred"`** 就会选择 **`libfred.a`** 文件。(历史上,静态库还需要一个名为 **`ranlib`** 的程序来在库的开头构建一个符号索引。如今,**`ar`** 工具通常会自行完成这项工作。)
> - 在 **Windows** 系统上,静态库具有 **`.LIB`** 扩展名,并由 **`LIB`** 工具生成。但这可能会引起混淆,因为**"导入库"(import library)**也使用相同的扩展名,导入库仅包含一个 DLL 中可用内容的列表
> - 在 **Windows** 系统上,静态库具有 **`.LIB`** 扩展名,并由 **`LIB`** 工具生成。但这可能会引起混淆,因为"**导入库**"(import library)也使用相同的扩展名,导入库仅包含一个 DLL 中可用内容的列表

​ 对于链接阶段,当我们提供给链接器一个静态库,整个时候,我们的链接器会持有一个尚未裁决的符号表格,沉浸到静态库中,把这些符号一个一个找出来(举个例子,A符号丢失,他在Obj1.o中,这个时候我们就会把Obj1.o全部链接进来),直到我们解决了所有的符号未定义的问题。

Expand Down
2 changes: 1 addition & 1 deletion documents/compilation/04-dynamic-libraries-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ description: ''

#### 什么是`-fPIC`?

`-fPIC` 的含义是**`Position-Independent Code`**(生成位置无关代码),换而言之, 编译出来的机器指令 **不依赖固定的加载地址**,在运行时可以被加载到任意内存位置而无需修改代码本身。这很符合我们对动态库功能的感知。我们最后,总是要将一个动态库的符号导出出来给其他第三方的应用程序或者是其他的库进行使用,因此,显然我们不能安排一个绝对的映射地址给这些动态库符号,而是在复用的时候,动态的给予一个偏移地址映射到使用者的进程地址上,这样我们才能实现符号的复用。按照步骤的说:
`-fPIC` 的含义是`Position-Independent Code`(生成位置无关代码),换而言之, 编译出来的机器指令 **不依赖固定的加载地址**,在运行时可以被加载到任意内存位置而无需修改代码本身。这很符合我们对动态库功能的感知。我们最后,总是要将一个动态库的符号导出出来给其他第三方的应用程序或者是其他的库进行使用,因此,显然我们不能安排一个绝对的映射地址给这些动态库符号,而是在复用的时候,动态的给予一个偏移地址映射到使用者的进程地址上,这样我们才能实现符号的复用。按照步骤的说:

- -fPIC将会对符号采用 **相对地址** 而不是绝对地址进行映射
- 全局变量通过 **GOT(Global Offset Table)** 间接访问
Expand Down
2 changes: 1 addition & 1 deletion documents/compilation/05-dynamic-library-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ extern "C"{

#### 提供完整ABI声明的头文件

这里**"提供完整ABI声明的头文件"** 指的是一个头文件(`.h`),它包含了所有必要的声明,使得编译器能够**完全理解**一个库或模块的接口,从而能够:
这里"**提供完整ABI声明的头文件**" 指的是一个头文件(`.h`),它包含了所有必要的声明,使得编译器能够**完全理解**一个库或模块的接口,从而能够:

1. **正确编译** 调用该库的代码。
2. **正确生成** 与库中函数交互的机器码。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ description: ''

## 运行时动态加载是什么?

官方的说,运行时动态链接(dynamic loading)指程序**在运行时**按需加载一个共享库(shared object / dynamic library / DLL),并查找需要的符号(函数、变量)后调用。笔者认为,**这是插件系统的一个重要的实现机制。**因为现在:
官方的说,运行时动态链接(dynamic loading)指程序**在运行时**按需加载一个共享库(shared object / dynamic library / DLL),并查找需要的符号(函数、变量)后调用。笔者认为,**这是插件系统的一个重要的实现机制**。因为现在:

- 我们可以动态的加载进入插件,在运行时根据配置加载不同功能模块(国际化、渲染后端、驱动等)。
- 上述特性允许我们可以按照需求加载我们需要的依赖,节约一部分空间
Expand Down
4 changes: 2 additions & 2 deletions documents/compilation/08-library-search-logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ Windows 的可执行/装载器与 API(`LoadLibrary` / `LoadLibraryEx` / 自动

一般而言,Windows的方式有两种:隐式(导入表)与显式(运行时 API)

**隐式加载(implicit)**指的是可执行文件的导入表(Import Table)在进程启动或模块加载时由系统装载器解析,系统会为每个 `DLL` 尝试找到并映射到进程地址空间。开发者在链接阶段指定依赖(例如 `kernel32.dll`、`mydll.dll`),加载由系统在进程启动时期自动完成。
**隐式加载**(implicit)指的是可执行文件的导入表(Import Table)在进程启动或模块加载时由系统装载器解析,系统会为每个 `DLL` 尝试找到并映射到进程地址空间。开发者在链接阶段指定依赖(例如 `kernel32.dll`、`mydll.dll`),加载由系统在进程启动时期自动完成。

**显式加载(explicit)**指的是代码在运行时使用 `LoadLibrary` / `LoadLibraryEx` 等 API 手工加载 DLL,然后用 `GetProcAddress` 取得函数指针。显式加载能通过参数控制搜索行为(例如使用 `LOAD_LIBRARY_SEARCH_USER_DIRS` 等标志)。
**显式加载**(explicit)指的是代码在运行时使用 `LoadLibrary` / `LoadLibraryEx` 等 API 手工加载 DLL,然后用 `GetProcAddress` 取得函数指针。显式加载能通过参数控制搜索行为(例如使用 `LOAD_LIBRARY_SEARCH_USER_DIRS` 等标志)。

#### 默认搜索顺序(概念化顺序)

Expand Down
2 changes: 1 addition & 1 deletion documents/vol1-fundamentals/02-c-language-crash-course.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ uint32_t u32 = 4294967295U;// 精确32位无符号整数

```

那问题来了,什么时候用多大的呢?嗯,这个事情可以不必要如此的死板,不过有一个事情必须注意——**你的数据范围得够用**。那问题来了:**N 位的数据到底能存多大?**对于 **无符号整数**,N 位一共可以表示 **2ⁿ 个数**,取值范围是 **0 ~ 2ⁿ − 1**。那如果是 **有符号整数** 呢?最高位要拿来当符号位了,采用补码表示的话,范围就是 **−2ⁿ⁻¹ ~ 2ⁿ⁻¹ − 1**。大家都是嵌入式程序员,这点二进制应该都能算得过来。
那问题来了,什么时候用多大的呢?嗯,这个事情可以不必要如此的死板,不过有一个事情必须注意——**你的数据范围得够用**。那问题来了:**N 位的数据到底能存多大**?对于 **无符号整数**,N 位一共可以表示 **2ⁿ 个数**,取值范围是 **0 ~ 2ⁿ − 1**。那如果是 **有符号整数** 呢?最高位要拿来当符号位了,采用补码表示的话,范围就是 **−2ⁿ⁻¹ ~ 2ⁿ⁻¹ − 1**。大家都是嵌入式程序员,这点二进制应该都能算得过来。

### 1.2 浮点类型

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ void init_uart(int baudrate, int databits, int stopbits) {

表面上看,调用一个重载函数只是"写个名字、传个参数"这么简单的事。但实际上,编译器在背后执行了一套非常严格的决策流程——这套流程被称为**重载解析 (Overload Resolution)**。

每当你调用一个存在多个重载版本的函数时,编译器都会先收集所有名字匹配、参数数量一致的候选函数,然后逐一评估,试图回答一个问题:**哪一个是"最合适"的?**需要强调的是,编译器并不会理解你的业务语义,它只会机械地按照语言规则打分,最终选出匹配度最高的版本。
每当你调用一个存在多个重载版本的函数时,编译器都会先收集所有名字匹配、参数数量一致的候选函数,然后逐一评估,试图回答一个问题:**哪一个是"最合适"的**?需要强调的是,编译器并不会理解你的业务语义,它只会机械地按照语言规则打分,最终选出匹配度最高的版本。

在涉及模板和可变参数之前,编译器的判断标准可以理解为一条由强到弱的"匹配优先级链"。首先是**精确匹配**——实参与形参类型完全一致;如果不存在精确匹配,才会考虑**类型提升**,比如 `char` 提升为 `int`;再往后是**标准类型转换**,例如 `int` 转换为 `double`;最后才轮到用户自定义的类型转换。这个顺序非常关键,因为只要某一层级已经能找到可行的匹配,后面的规则就完全不会被考虑,哪怕它们在你看来更"合理"。

Expand Down Expand Up @@ -181,7 +181,7 @@ configure_uart(115200, 8, 2, 'E'); // 全部自定义

默认参数的语法看似简单,但它的规则其实非常严格,踩坑的人不在少数。

**规则一:默认参数必须从右向左连续出现。**编译器在处理函数调用时,只能通过"省略尾部参数"的方式来判断哪些值使用默认值。换句话说,你不能跳过中间的参数——如果要给第三个参数传值,前面的所有参数都必须显式给出。这也就意味着,如果你试图在某个有默认值的参数后面再放一个没有默认值的参数,编译器会直接拒绝。
**规则一:默认参数必须从右向左连续出现**。编译器在处理函数调用时,只能通过"省略尾部参数"的方式来判断哪些值使用默认值。换句话说,你不能跳过中间的参数——如果要给第三个参数传值,前面的所有参数都必须显式给出。这也就意味着,如果你试图在某个有默认值的参数后面再放一个没有默认值的参数,编译器会直接拒绝。

```cpp
// 正确:默认参数从右向左连续
Expand All @@ -193,7 +193,7 @@ void init_spi(int freq, int mode = 0, int bits = 8);

所以,在设计函数签名时,参数的排列顺序非常重要。一个实用的原则是:**把最常需要自定义的参数放在最左边,把几乎不会变的参数放在最右边**。

**规则二:默认参数只能被指定一次,而且应该放在声明处。**这一点在头文件与源文件分离的工程中尤为重要。默认值是接口的一部分,而不是实现细节——如果你在 `.cpp` 里又写了一遍默认参数,编译器会认为你在试图重新定义规则,直接报错。
**规则二:默认参数只能被指定一次,而且应该放在声明处**。这一点在头文件与源文件分离的工程中尤为重要。默认值是接口的一部分,而不是实现细节——如果你在 `.cpp` 里又写了一遍默认参数,编译器会认为你在试图重新定义规则,直接报错。

```cpp
// uart.h —— 声明时指定默认参数
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ private:

但 `mutable` 也容易被滥用。如果你发现自己在 `const` 函数里频繁修改 `mutable` 成员,而且这些修改会影响到对象的"可观测行为",那大概率是你的 `const` 设计有问题——要么这个函数不应该是 `const` 的,要么这些成员不应该是 `mutable` 的。

一个简单的判断标准是:**如果去掉 `mutable` 标记和相关的修改代码,函数对外部行为是否完全一样?**如果答案为"是",那 `mutable` 就是合理的;如果为"否",那就需要重新审视设计了。
一个简单的判断标准是:**如果去掉 `mutable` 标记和相关的修改代码,函数对外部行为是否完全一样**?如果答案为"是",那 `mutable` 就是合理的;如果为"否",那就需要重新审视设计了。

## 在线运行

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ title: C++98面向对象:继承与多态

继承的核心是表达一种非常明确的关系:**派生类 is-a 基类**。例如,一个温度传感器"是一种传感器",UART "是一种通信接口"。在这种语义成立的前提下,继承才是自然的。

我要强调一些事情:特别是在比较关键的设计场景下——**使用正确的语义总是比为了图省事强!使用正确的语义总是比为了图省事强!使用正确的语义总是比为了图省事强!**你也不想给未来的你和你的同事加班擦屁股吧。
我要强调一些事情:特别是在比较关键的设计场景下——**使用正确的语义总是比为了图省事强!使用正确的语义总是比为了图省事强!使用正确的语义总是比为了图省事强**!你也不想给未来的你和你的同事加班擦屁股吧。

我们来看一个完整的传感器层次结构示例:

Expand Down Expand Up @@ -431,7 +431,7 @@ send_command(spi, cmd, sizeof(cmd)); // 通过 SPI 发送

虚析构函数是多态中一个极其容易被忽视、却又极其致命的细节。

**只要你打算通过基类指针来管理派生类对象的生命周期,那么基类的析构函数就必须是虚的。**否则,在 `delete` 基类指针时,只会调用基类的析构函数,派生类中持有的资源将完全得不到释放。
**只要你打算通过基类指针来管理派生类对象的生命周期,那么基类的析构函数就必须是虚的**。否则,在 `delete` 基类指针时,只会调用基类的析构函数,派生类中持有的资源将完全得不到释放。

```cpp
class BadBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ title: C++98运算符重载

运算符重载是 C++ 最具争议但也最有魅力的特性之一。它允许**自定义类型像内置类型一样参与表达式计算**,从而显著提升代码的可读性与表达力。你是喜欢看两个向量塞到一个叫做特别别扭的 `VectorAdd` 方法里(这里内涵下 Java(逃)),还是直接使用 `a + b` 的方式更可读呢?相信各位自有答案。

不过,运算符重载是一个需要克制的特性。笔者就建议一个准则:**当你"自然地"会用某个运算符来读这段代码时,才值得重载它。**比如说自然的处理非内置的向量数学运算、物理量运算、时间日期、容器处理等等。如果你的运算符重载让人看完之后一头雾水——比如用 `+` 来表示"从容器中删除元素"——那不如老老实实写一个名为 `remove` 的函数。
不过,运算符重载是一个需要克制的特性。笔者就建议一个准则:**当你"自然地"会用某个运算符来读这段代码时,才值得重载它**。比如说自然的处理非内置的向量数学运算、物理量运算、时间日期、容器处理等等。如果你的运算符重载让人看完之后一头雾水——比如用 `+` 来表示"从容器中删除元素"——那不如老老实实写一个名为 `remove` 的函数。

## 1. 算术运算符重载

Expand Down
Loading
Loading