From 7481fedc36dac918eb99aa457b004159c7c0b522 Mon Sep 17 00:00:00 2001 From: dynilath Date: Sun, 28 Jul 2024 22:08:28 +0800 Subject: [PATCH] Update reference-type.md: restructure chapter --- src/zh/03-types/reference-type/README.md | 4 + .../03-types/reference-type/function-ref.md | 149 ++++++++++++++++++ .../object-ref.md} | 23 ++- 3 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 src/zh/03-types/reference-type/README.md create mode 100644 src/zh/03-types/reference-type/function-ref.md rename src/zh/03-types/{reference-type.md => reference-type/object-ref.md} (97%) diff --git a/src/zh/03-types/reference-type/README.md b/src/zh/03-types/reference-type/README.md new file mode 100644 index 00000000..c59b9d93 --- /dev/null +++ b/src/zh/03-types/reference-type/README.md @@ -0,0 +1,4 @@ +--- +title: 3.5 引用类型 +index: false +--- diff --git a/src/zh/03-types/reference-type/function-ref.md b/src/zh/03-types/reference-type/function-ref.md new file mode 100644 index 00000000..abcf3c02 --- /dev/null +++ b/src/zh/03-types/reference-type/function-ref.md @@ -0,0 +1,149 @@ +--- +title: 3.5.2 函数引用 +--- + +可以使用引用绑定到函数: + +```cpp +int add(int a, int b) { + return a + b; +} + +auto& add_ref = add; // add_ref 是 add 函数的引用 + +int result = add_ref(1, 2); // 调用 add 函数 +``` + +这里的 `add_ref` 是 `add` 函数的引用,可以像函数一样调用。这种引用称为**函数的引用**。 + +函数引用也可以作为函数的参数,例如: +```cpp +int apply(auto& func, int a, int b) { + return func(a, b); +} + +int result = apply(add, 1, 2); +``` + +在这里,`apply` 函数接受一个函数引用作为参数,然后调用这个函数。 + +## 函数类型与函数引用类型 + +读者可以发现,在上面的例子中,`apply` 函数中的参数 `auto& func` 也能绑定到对象。此时: +```cpp +int a = 1; +int result = apply(a, 2, 3); // ![!code error] // 错误,a 不是函数 +``` +这样就会产生错误,因为 `a` 不是函数。但是这个错误会出现在 `apply` 函数里面,在调用方并不会产生提示。 +此时,我们就会希望通过类型来限制 `func` 只能绑定到函数。这就需要使用函数类型,以及函数引用类型。 + +函数类型的形式是 +```cpp +return_type (parameter_type1, parameter_type2, ...) +``` +这里的 `return_type` 是函数的返回值类型,`parameter_type1, parameter_type2, ...` 是函数的参数类型。例如: + +```cpp +using binary_int_func = int(int, int); +``` +这样,`binary_int_func` 就是一个函数类型,它接受两个 `int` 类型的参数,返回一个 `int` 类型的值。 + +在我们声明了 `binary_int_func` 之后,就可以使用这个类型来限制 `func` 的类型: +```cpp +// 通过在 binary_int_func 后面加上 & 来组成一个函数引用类型 +int apply(binary_int_func& func, int a, int b) { + return func(a, b); +} + +int a = 1; +apply(a, 2, 3); // [!code error] // 错误,a 不是函数 + +float add(float a, float b) { + return a + b; +} +apply(add, 2, 3); // [!code error] // 错误,add 类型不匹配 +``` +这样,在调用 `apply` 函数时,就会提示无法用 `a` 初始化 `apply` 的第一个参数。这在 `apply` 函数的实现比较复杂的时候能够有效的提升分析错误的效率。 + +在声明函数类型时,并不能推导出函数调用类型为何,因此在声明函数类型时,必须显式指定参数类型,而不能使用 `auto`。例如: +```cpp +using binary_int_func = int(auto, auto); // [!code error] // 错误,auto 不能用在这里 +``` + +但是,我们可以通过函数类型来确定一个声明中有 `auto` 的函数的形式: +```cpp +auto add(auto a, auto b) { // 这个函数的参数和返回值类型尚不确定 + return a + b; +} + +using binary_int_func = int(int, int); +int apply(binary_int_func& func, int a, int b) { + return func(a, b); +} + +int result = apply(add, 2, 3); // 正确,用 add 初始化 func +``` +这里,使用 `add` 初始化 `func` 时,会被推导为 `int(int, int)`,从而确定了 `add` 的参数和返回值类型。 + +::: info 你的第一个模板函数 +考虑如下的代码: +```cpp +auto add(auto a, auto b) { + return a + b; +} +``` +技术性地说,这里的 `add` 函数是一个函数模板,而非函数。当函数存在参数的类型(返回值没有影响)声明为 `auto` 时,这个函数就是一个模板。 +这样设计使得这个 `add` 可以适应于各种各样的参数,并且以和计算结果相同的方式返回出来。 + +当 `add` 函数的两个参数类型被确定时,这个函数模板就会被自动推导成为一个对应的函数,例如: +```cpp +auto result = add(1, 2); // result 是 int 类型 +auto result2 = add(1.0, 2.0); // result2 是 double 类型 +auto result3 = add(1, 2.0); // result3 是 double 类型 +``` +你可能会觉得,这不就是把运算符 `+` 变成了个函数吗,但我们可以考虑一些更复杂的计算,例如[简单的控制台程序](../02-program-structure/cli-program.md)中的 `sqrt` 函数: +```cpp +auto sqrt(auto x) { + auto a = x; + while (a * a > x) { + a = (a + x / a) / 2; + } + return a; +} + +auto result = sqrt(314159ull); // result 是 unsigned long long 类型 +``` +这时候,无论 `x` 是什么整数类型,`sqrt` 函数都能选择对应类型的局部对象 `a` 的类型,并返回对应类型的结果。而不需要费劲写下 `sqrt` `sqrtus` `sqrtul` `sqrtull` 等一系列函数,不用在调用的时候推敲要用哪个函数,不用再担惊受怕会不会因为类型不匹配发生数据溢出错误。 +::: + +::: info 直接声明函数引用类型与旧式类型别名 +在前面,我们为了限制函数引用的类型,使用了一个类型别名来表达函数类型: +```cpp +using binary_int_func = int(int, int); +``` + +函数引用类型当然也可以使用类型别名: +```cpp +using binary_int_func_ref = binary_int_func&; +``` + +如果要直接声明一个函数引用类型,其形式就会变得复杂: +```cpp +using binary_int_func_ref = int(&)(int, int); +``` + +为什么在这里?因为 `&` 作为一个修饰符应当出现在那个标识符的前面,而函数的参数应当出现在标识符的后面,这种期待声明、类型、调用的形式统一的设计是从 C 语言中继承下来的。 + +使用[旧式类型别名](./type-intro.md#旧式类型别名) `typedef` 时,这种体现会更加明显: +```cpp +typedef int binary_int_func(int, int); // 声明了一个函数类型 +typedef int (&binary_int_func_ref)(int, int); // 声明了一个函数引用类型 + +int add(int,int); // 函数声明,为了简便起见,省略了函数体 +``` +::: + +## 在返回值中使用函数引用 + +::: important TODO: 补充内容 +::: diff --git a/src/zh/03-types/reference-type.md b/src/zh/03-types/reference-type/object-ref.md similarity index 97% rename from src/zh/03-types/reference-type.md rename to src/zh/03-types/reference-type/object-ref.md index de305ad3..96f7ac07 100644 --- a/src/zh/03-types/reference-type.md +++ b/src/zh/03-types/reference-type/object-ref.md @@ -1,5 +1,5 @@ --- -title: 3.5 引用类型 +title: 3.5.1 对象引用 --- **引用**是 C++ 中的一种特殊类型,它用来指代一个对象,或者一个函数。例如: @@ -176,7 +176,7 @@ ref = 2; // 为什么?这不是字面量吗? ``` 你可能会奇怪,为什么右值引用绑定到字面量之后,这个右值引用还能修改?右值引用不是右值吗? -首先,右值引用的名字本身,是一个标识符表达式,当然是左值。 +首先,右值引用的名字本身,是一个标识符组成的基础表达式,当然是左值表达式。 此外,`ref` 所绑定到的对象,实际上是一个从 `1` 构造而来的临时对象。为了将一个右值表达式绑定给右值引用,会将右值表达式转换为一个与引用类型匹配的[将亡值表达式](./value-category.md)。 将亡值表达式代指这个临时的对象,这个临时的对象的生命周期会延长到绑定到它的引用的生命期结束。举例而言: @@ -194,7 +194,7 @@ auto&& ref = a; // ref 是 int& 类型 auto&& ref2 = 1; // ref2 是 int&& 类型 ``` -这一原理及其应用我们会在后面的章节中详细介绍。 +这一原理及其应用会在后面的章节中详细介绍。 ### 函数参数中的右值引用 @@ -209,7 +209,7 @@ void set_a_to(int&& value) { 这个 `set_a_to` 函数接受一个右值引用,可以接受右值表达式作为参数,例如: ```cpp set_a_to(1); -set_a_to(a + 1); // a + 1 是右值表达式 +set_a_to(a + 1); // a + 1 是纯右值表达式 ``` 当然,这个函数不接受左值表达式作为参数,例如: @@ -239,6 +239,11 @@ const auto& ref = 1; // ref 是 const int& 类型 const auto& ref2 = a; // ref2 也是 const int& 类型 ``` +::: info 为什么允许常量左值引用绑定到右值? +当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。 +因此,就像在其他地方使用右值一样,我们也应当允许常量左值引用绑定到右值,使我们能够更方便地使用这个值。 +::: + 藉由常量左值引用,我们可以设计一个接受左值表达式也接受右值表达式的函数: ```cpp int a; @@ -254,10 +259,6 @@ set_a_to(a); // a 是左值,也可以传入 这个 `set_a_to` 函数当然也可以使用 `int` 类型作为参数,但当类型更为复杂,构造参数的成本也不得不被考虑时,常量左值引用的作用就会凸显出来。 -::: info 为什么允许常量左值引用绑定到右值? -当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。 -因此,就像在其他地方使用右值一样,我们也应当允许常量左值引用绑定到右值,使我们能够更方便地使用这个值。 -::: ## 悬垂引用 @@ -298,9 +299,3 @@ const int& ref2 = max(ref, 3); // ![!code error] // 错误,试图使用悬垂 因此,代码规范往往要求不要返回引用,或者返回引用时要确保引用绑定的对象的生命期足够长。 ::: - -## 函数的引用 - -::: important TODO: 补充内容 -::: -