Skip to content

Commit d29db2c

Browse files
committed
improve wording, update content for overload.md
1 parent 2089bc4 commit d29db2c

File tree

4 files changed

+128
-29
lines changed

4 files changed

+128
-29
lines changed

src/zh/03-types/arithmetic-types.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,13 @@ double d = 42; // d 的值是 42.0
439439
- 否则,如果S的转换等级高于U,那么将操作数都转换为S
440440
- 否则,将操作数都转换为S的无符号版本
441441

442+
上面这样一条一条的规则显然只适合于考试和编译器开发者,如果需要日常开发中天天想着这个规则,显然是不现实的。为了方便理解,这里对这个规则做出一个简略的总结:
443+
1. 将整数变成浮点
444+
2. 将小于`int`的类型变成`int`
445+
3. 将扩展类型变成标准类型
446+
4. 将较小的类型变成较大的类型
447+
5. 将一样大的有符号类型变成无符号类型。
448+
442449
举例而言:
443450
```cpp
444451
int a = 1;
@@ -454,13 +461,7 @@ auto x1 = a + b;
454461
// 将 `int` 转换为 `float`,结果是 `float` 类型
455462
auto x2 = a + c;
456463
```
457-
458-
上面这样一条一条的规则显然只适合于考试和编译器开发者,如果需要日常开发中天天想着这个规则,显然是不现实的。为了方便理解,这里对这个规则做出一个简略的总结:
459-
1. 将整数变成浮点
460-
2. 将小于`int`的类型变成`int`
461-
3. 将扩展类型变成标准类型
462-
4. 将较小的类型变成较大的类型
463-
5. 将一样大的有符号类型变成无符号类型。
464+
这里的转换规则仍然是一定程度的简化,涉及值类别转换和枚举类型的部分,会在后续章节中介绍。
464465

465466
::: info 为什么?
466467
一般算术转换的规则是对现实的概括和妥协。
@@ -473,5 +474,3 @@ auto x2 = a + c;
473474

474475
事实上,这种先抽象成基础类型,再抽象成扩展类型的二次抽象是一种过于复杂的历史遗留。程序员仍然需要浪费心智查看硬件手册,以理解 `int``char` 究竟是什么东西。已经进行了尝试的读者也会发现,几乎所有情况下,`std::int32_t` 就是 `int``std::int64_t` 就是 `long long int`,这层抽象在实际中几乎没有产生任何价值。于是,更现代的设计直接将扩展类型作为基础类型,具体如何选择表示和指令让编译器来分析。笔者也推荐读者直接使用扩展类型,而不要费神纠结基础类型的性质。
475476
:::
476-
477-
这里的转换规则仍然是一定程度的简化,涉及值类别转换和枚举类型的部分,会在后续章节中介绍。

src/zh/03-types/overload.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,98 @@
22
title: 3.6 初识重载
33
---
44

5+
## 简单重载
6+
7+
考虑如下的两个函数(读者如果已经忘记了函数的形式参数的名字是可以省略的,可以回顾一下[前面的章节](../02-program-structure/function.md))。
8+
9+
```cpp
10+
int sqrt(int);
11+
long long sqrt(long long);
12+
double sqrt(double);
13+
long double sqrt(long double);
14+
```
15+
16+
这两个函数有完全相同的名称,当写下如下的函数调用的时候,会发生什么?
17+
```cpp
18+
sqrt(42);
19+
```
20+
21+
这里,编译器会根据 `42` 的类型,选择调用 `int sqrt(int)``long long sqrt(long long)`。由于 `42``int` 类型的字面量,因此会调用 `int sqrt(int)`
22+
23+
这种根据参数类型的不同而选择不同的函数的特性,称为**重载**
24+
25+
## 重载的作用
26+
27+
重载通常用于实现相似功能的函数,但是参数类型不同。例如这里提到的 `sqrt` 函数,对于不同的整数类型,都可以计算平方根。但人们往往不会不希望把所有的整数都转成`long long` 类型再计算,以及把所有的浮点数都转成 `long double` 类型再计算。人们一般只想使用那个效率最高且能满足需求的函数。如果不使用重载,这种需求就会变成如下的形式:
28+
29+
```cpp
30+
int sqrt_int(int);
31+
long long sqrt_long_long(long long);
32+
double sqrt_double(double);
33+
long double sqrt_long_double(long double);
34+
```
35+
36+
这样,调用 `sqrt_XXX` 函数的时候,就需要根据参数的类型选择不同的函数,用户每次调用 `sqrt_XXX` 函数都需要考虑参数的类型。例如:
37+
38+
```cpp
39+
void some_function(){
40+
/** 一些代码 */
41+
int value = make_some_value();
42+
/** 一些代码 */
43+
int temp_value = sqrt_int(value);
44+
/** 一些代码 */
45+
int temp_value2 = sqrt_int(value2);
46+
/** 一些代码 */
47+
}
48+
```
49+
50+
如果代码进行了一些改动,`make_some_value` 的返回类型发生了变化,那么所有的 `sqrt_XXX` 函数的调用都需要修改,产生与代码规模正相关的工作量。例如,如果 `make_some_value` 的返回类型从 `int` 变成了 `long long`,那么上面的代码就需要修改为:
51+
52+
```cpp
53+
void some_function(){
54+
/** 一些代码 */
55+
long long value = make_some_value();
56+
/** 一些代码 */
57+
long long temp_value = sqrt_long_long(value);
58+
/** 一些代码 */
59+
long long temp_value2 = sqrt_long_long(value2);
60+
/** 一些代码 */
61+
}
62+
```
63+
64+
这会导致代码越多,修改的工作量就越大。最终导致代码维护成本难以控制。
65+
66+
我们不妨看看 `sqrt` 使用重载来实现,并且用上 `auto` 类型推导会如何:
67+
68+
```cpp
69+
void some_function(){
70+
/** 一些代码 */
71+
auto value = make_some_value();
72+
/** 一些代码 */
73+
auto temp_value = sqrt(value);
74+
/** 一些代码 */
75+
auto temp_value2 = sqrt(value2);
76+
/** 一些代码 */
77+
}
78+
```
79+
80+
此时,即使 `make_some_value` 的返回类型发生了变化,`some_function` 中,(至少这里写出的部分)完全不需要进行任何修改。这样,代码的维护成本就大大降低了。
81+
82+
## 算数类型的重载
83+
84+
::: important TODO: 补充内容
85+
:::
86+
87+
## 涉及 `const``volatile` 的重载
88+
89+
::: important TODO: 补充内容
90+
:::
91+
92+
## 左值引用和右值引用的重载
93+
94+
::: important TODO: 补充内容
95+
:::
96+
597
## `auto` 参数的函数
698

799
::: important TODO: 润色与细化描述

src/zh/03-types/reference-type/object-ref.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ b = 43; // 修改了 a 的值
2020

2121
在这个例子中,通过对 `b` 赋值,修改了 `a` 的值。
2222

23-
除了一些特例外,引用总是当做指代的对象来使用(对于读取值或者写入值,一定是当做指代的对象来使用)。并且,在初始化之后,不能更改这种指代。用一个对象初始化引用,也称为将引用绑定到对象。例如:
23+
除了一些特例外,引用总是当做指代的对象来使用(对于读取值或者写入值,一定是当做指代的对象来使用)。并且,在初始化之后,不能更改这种指代。
24+
25+
用一个对象初始化引用,也称为将引用**绑定**到对象。
26+
27+
例如:
2428
```cpp
2529
int a = 1; // a 初始化为不确定值
2630
int& b = a; // 将 b 绑定到 a
@@ -41,13 +45,13 @@ int& b = a; // 以具有不确定值的 a 初始化引用
4145
b = 1; // 将 1 赋值给 a
4246
```
4347

44-
引用不是一个对象,具体来说,引用类型不需要有对齐和大小、不需要占用内存,也不能声明引用的引用。但根据需要,编译器可以生成使用存储来实现的引用。
48+
引用不是一个对象,具体来说,引用类型不需要有对齐和大小、不需要占用内存,也不能声明引用的引用。(但是,根据需要,编译器可以生成使用存储来实现的引用。)
4549

4650
在前面的[声明](../02-program-structure/declaration.md#类型与对象)一节中提到,变量不一定是对象。这里我们可以给出更准确的说法:对象,以及绑定到对象的引用,统称为**变量**
4751

4852
## 引用类型的声明与初始化
4953

50-
引用类型有两种,左值引用类型和右值引用类型
54+
引用类型有两种,**左值引用**类型和**右值引用**类型
5155

5256
左值引用类型的形式是
5357
```cpp
@@ -96,7 +100,7 @@ int a = 1;
96100
auto& ref = a; // ref 是 int& 类型
97101
```
98102

99-
### 在函数参数中使用左值引用
103+
### 函数中的左值引用
100104

101105
可以在函数参数中使用左值引用,来修改作为参数传入的对象:
102106
```cpp
@@ -109,7 +113,7 @@ set_to_42(b); // b 的值变为 42
109113
```
110114
111115
::: tip 传出参数
112-
这种通过引用修改参数的方式,也称为传出参数。例如考虑如下的代码:
116+
这种通过引用修改参数的方式,也称为**传出参数**。例如考虑如下的代码:
113117
114118
```cpp
115119
bool safe_divide(int a, int b, int& result) {
@@ -127,10 +131,9 @@ safe_divide(a, b, c); // c 的值变为 5
127131

128132
在这个 `safe_divide` 函数的设计中,我们需要从这个函数得到两个结果,一是函数是否执行成功,二是计算的结果。这种情况下,可以使用引用来传出结果。
129133

130-
但是,在工程中,我们一般不希望函数修改传入的参数,因此这种传出参数的方式在使用时应充分注释,或遵循一定的代码规范,以免造成误解。
134+
注意,在实际工程中,我们一般不希望函数修改传入的参数,传出参数的形式在使用时应充分注释,或遵循一定的代码规范,以免造成误解。
131135
:::
132136

133-
### 在函数返回值中使用左值引用
134137

135138
可以在返回值中使用左值引用,例如:
136139
```cpp
@@ -150,7 +153,7 @@ ref = 3; // 修改了 b 的值
150153
max(a, b) = 4; // 由于 max 返回的是引用,因此可以直接修改返回值,b 的值变为 4
151154
```
152155
153-
这里可以发现,当函数返回一个左值引用时,可以直接对返回值进行赋值操作结合在[值类别](./value-category.md)中的讲解,这种函数的调用是左值表达式。我们可以补充前面值类别的定义:
156+
这里可以发现,当函数返回一个左值引用时,可以直接对返回值进行赋值操作结合在[值类别](./value-category.md)中的讲解,这种函数的调用是左值表达式。我们可以补充前面值类别的定义:
154157
155158
> 下列表达式是左值表达式:
156159
> - 返回值类型是引用类型的函数调用
@@ -176,7 +179,7 @@ ref = 2; // 为什么?这不是字面量吗?
176179
```
177180
你可能会奇怪,为什么右值引用绑定到字面量之后,这个右值引用还能修改?右值引用不是右值吗?
178181

179-
首先,右值引用的名字本身,是一个标识符组成的基础表达式,当然是左值表达式。
182+
首先,右值引用的名字本身(也即这里的`ref`,是一个标识符组成的基础表达式,当然是左值表达式。
180183

181184
此外,`ref` 所绑定到的对象,实际上是一个从 `1` 构造而来的临时对象。为了将一个右值表达式绑定给右值引用,会将右值表达式转换为一个与引用类型匹配的[将亡值表达式](./value-category.md)
182185
将亡值表达式代指这个临时的对象,这个临时的对象的生命周期会延长到绑定到它的引用的生命期结束。举例而言:
@@ -185,7 +188,7 @@ ref = 2; // 为什么?这不是字面量吗?
185188
int&& ref = true; // 用 true 初始化一个 int 类型的临时对象,将这个临时对象绑定到 ref
186189
ref = 2; // 修改了这个临时对象的值,而非字面量 true 的值
187190
```
188-
191+
::: info 特殊的 auto&&
189192
前面介绍过 `auto &` 可以推导左值引用类型,但 `auto &&` 并不是推导右值引用,而是“通用引用”,它会根据初始化器的值类别推导出适合的引用类型,例如:
190193

191194
```cpp
@@ -195,8 +198,10 @@ auto&& ref2 = 1; // ref2 是 int&& 类型
195198
```
196199

197200
这一原理及其应用会在后面的章节中详细介绍。
201+
:::
202+
198203

199-
### 在函数参数中使用右值引用
204+
### 函数中的右值引用
200205

201206
类似于左值引用,右值引用也可以用在函数参数中,例如:
202207
```cpp
@@ -217,10 +222,7 @@ set_a_to(a + 1); // a + 1 是纯右值表达式
217222
set_a_to(a); // [!code error] // 错误,a 是左值表达式
218223
```
219224
220-
### 在函数返回值中使用右值引用
221-
222-
::: important TODO 补充内容
223-
:::
225+
函数返回值中当然也可以使用右值引用,其作用和左值引用相似,但是二者的差异涉及**重载**的知识,在本章后面的部分会介绍相关内容,这里暂且放下。
224226
225227
## 常量左值引用
226228
@@ -244,7 +246,10 @@ const auto& ref2 = a; // ref2 也是 const int& 类型
244246
```
245247

246248
::: info 为什么允许常量左值引用绑定到右值?
247-
当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。
249+
当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。
250+
251+
因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。
252+
248253
因此,就像在其他地方使用右值一样,我们也应当允许常量左值引用绑定到右值,使我们能够更方便地使用这个值。
249254
:::
250255

@@ -281,7 +286,9 @@ int&& max(int&& a, int&& b) {
281286
max(1, 2) = 3; // [!code error] // 错误,试图使用悬垂引用
282287
```
283288

284-
在这个例子中,`max` 函数的两个参数绑定到了从字面量 `1``2` 构造的临时对象。这两个临时对象的生命期在 `max` 函数返回后结束,但是 `ref` 仍然绑定到这两个临时对象中的较大者。这种情况下,`ref` 就是一个悬垂引用。在第二次调用 `max` 函数时,需要使用 `ref` 绑定的对象的值来比较大小,但是这个对象的生命期已经结束,因此这个操作是错误的。
289+
在这个例子中,`max` 函数的两个参数绑定到了从字面量 `1``2` 构造的临时对象。这两个临时对象的生命期在 `max` 函数返回后结束,但是 `ref` 仍然绑定到这两个临时对象中的较大者。
290+
291+
这种情况下,`ref` 就是一个悬垂引用。在第二次调用 `max` 函数时,需要使用 `ref` 绑定的对象的值来比较大小,但是这个对象的生命期已经结束,因此这个操作是错误的。
285292

286293
上面的例子用常量左值引用写时,会更难以被发现:
287294
```cpp
@@ -293,8 +300,7 @@ const int& max(const int& a, const int& b) {
293300
}
294301
}
295302

296-
// ref 是悬垂引用,由于不能写入,因此显得很隐晦
297-
const int& ref = max(1, 2);
303+
const int& ref = max(1, 2); // ref 是悬垂引用,由于不能写入,因此显得很隐晦
298304
const int& ref2 = max(ref, 3); // ![!code error] // 错误,试图使用悬垂引用
299305
```
300306

src/zh/03-types/value-category.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ a = 1;
8484

8585
除了上述的左值、纯右值、将亡值之外,C++ 还引入了泛左值和右值的概念。
8686

87-
有的时候,程序员需要通过一个左值的处理方式来处理将亡值,这时,将亡值和左值一起分类为**泛左值**。有的时候,程序员需要把将亡值当做右值来处理,这时,将亡值和纯右值一起分类为**右值**
87+
有的时候,程序员需要通过一个左值的处理方式来处理将亡值,这时,将亡值和左值一起分类为**泛左值**
88+
89+
有的时候,程序员需要把将亡值当做右值来处理,这时,将亡值和纯右值一起分类为**右值**
8890

8991
这里提到的处理方式目前还没有介绍,但在后面的章节中,读者会看到这两种处理方式。

0 commit comments

Comments
 (0)