Cpp学习16 第 12 章 复合类型:引用与指针
12.1 — 复合数据类型简介
每种数据类型要么是基本类型,要么是复合类型。C++ 语言标准明确定义了每种类型所属的类别。
C++ 支持以下复合类型:
- Functions 函数
- C 风格数组
- Pointer 指针类型:
- Pointer to object 指向对象的指针
- Pointer to function 函数指针
- Pointer to member 指向成员类型的指针:
- Pointer to data member 指向数据成员的指针
- Pointer to member function
指向成员函数的指针
- Reference 引用类型:
- L-value references L 值引用
- R-value references R 值引用
- Enumerated 枚举类型:
- Unscoped enumerations 无作用域枚举
- Scoped enumerations 作用域枚举
- Class 类型:
- Structs
- Classes
- Unions
12.2 — 值类别(左值和右值)
Value categories (lvalues and rvalues)
为了帮助确定表达式的求值方式以及它们可以在何处使用,C++ 中的所有表达式都具有两个属性:类型和值类别。
表达式的类型等同于表达式求值后得到的值、对象或函数的类型。
表达式(或子表达式)的值类别表明该表达式是解析为值、函数还是某种对象。
左值和右值表达式
左值 (读作“ell-value”,是“left value”或“locator value”的缩写,有时也写作“l-value”)是一个表达式,其值等于可识别的对象或函数(或位域)。
表达式 x 是一个左值表达式,因为它计算出的值为变量 x (具有标识符)。
自从语言中引入常量以来,左值分为两种子类型: 可修改左值是指其值可以修改的左值。 不可修改左值是指其值不能修改的左值(因为该左值是 const 或 constexpr)。
右值 (读作“arr-value”,是“right value”的缩写,有时也写作 r-value )是指非左值的表达式。右值表达式的求值结果为一个值。常见的右值包括字面量(C 风格的字符串字面量除外,它们是左值)以及按值返回的函数和运算符的返回值。右值不可识别(意味着它们必须立即使用),并且仅存在于它们所在的表达式的作用域内。
值类别和运算符
除非另有规定,运算符期望其操作数为右值。例如,二元 operator+ 期望其操作数为右值。
当需要右值但实际提供的是左值时,该左值会进行左值到右值的转换,以便在需要时使用。
以下代码演示了一种判断表达式是左值还是右值的方法:
1 |
|
12.3 — L 值引用
在 C++ 中, 引用是现有对象的别名。一旦定义了引用,对该引用执行的任何操作都会应用于被引用的对象。这意味着我们可以使用引用来读取或修改被引用的对象。
现代 C++ 包含两种类型的引用:左值引用和右值引用。本章我们将讨论左值引用。
左值引用 (通常简称为“引用”,因为在 C++11 之前只有一种类型的引用)充当现有左值(例如变量)的别名。
指定引用的类型(例如 int& )称为引用类型 。可以被引用的类型(例如 int )称为被引用类型 。
1 | // regular types |
左值引用变量是一个指向左值(通常是另一个变量)的引用变量。
要创建一个左值引用变量,我们只需定义一个具有左值引用类型的变量即可:
1 |
|
定义引用时,将 & 符号放在类型旁边(而不是引用变量的名称旁边)。
可以使用非常量引用来修改被引用对象的值
与常量类似,所有引用都必须初始化。引用不能重新定位(不能更改为指向另一个对象)
如果非常量左值引用可以绑定到不可修改的(常量)左值或右值,那么就可以通过引用来改变这些值,这将违反它们的常量性。
引用变量遵循与普通变量相同的作用域和持续时间规则
引用 和 引用对象 具有独立的生命周期。
当引用在被引用对象之前被销毁时,被引用对象不会受到影响。
当被引用的对象在指向它的引用被销毁之前就被销毁时,该引用就指向了一个已经不存在的对象。这样的引用被称为悬空引用 。访问悬空引用会导致未定义行为。
12.4 — const 左值引用
在声明左值引用时使用 const 关键字,我们告诉左值引用将它引用的对象视为常量。这样的引用称为指向常量值的左值引用 (有时也称为常量引用或常量引用 )。
指向 const 的左值引用可以绑定到不可修改的左值:
1 | int main() |
指向 const 的左值引用甚至可以绑定到不同类型的值,只要这些值可以隐式转换为引用类型即可。
当 const 左值引用 直接 绑定到临时对象时,临时对象的生命周期会延长至与引用的生命周期相同。
Constexpr 左值引用,略。
12.5 — 按左值引用传递
既然可以直接使用变量本身,为什么还要创建变量的别名呢?
当我们调用该函数时,我们复制了参数的值,但只是短暂地使用它,然后就将其销毁了!
标准库提供的大多数类型(例如 std::string )都是Class类型。Class类型的复制通常开销很大。我们应该尽可能避免对开销很大的对象进行不必要的复制,尤其是在几乎立即销毁这些副本的情况下。
1 | void printValue(std::string& y) // type changed to std::string& |
引用参数被视为与实参是同一个对象。
按引用传递允许我们更改参数的值。
按引用传递只能接受可修改的左值参数,这意味着我们不能传递常量变量或字面量。
12.6 — 按常量左值引用传递
除非有特殊原因(例如函数需要更改参数的值),否则应优先使用 const 引用传递,而不是使用非常量引用传递。
一般来说,基本类型按值传递,Class 类型按常量引用传递。
按价值传递与按引用传递的成本,略。
除非你的函数调用其他需要 C 风格字符串或 std::string 参数的函数,否则最好使用 std::string_view (按值传递)而不是 const std::string& 来传递字符串。
原因,略。
12.7 — 指针简介
地址运算符 (&)
取地址 address-of 运算符 (&) 会返回其操作数的内存地址。
解引用运算符 (*)
地址最有用的用途是访问存储在该地址处的值。 解引用 dereference 运算符 (*)(有时也称为间接运算符 )将给定内存地址处的值作为左值返回:
1 |
|
Pointers 指针
指针是一个对象,它的值是一个 内存地址 (通常是另一个变量的地址)。
指定指针的类型(例如 int* )称为指针类型 。与引用类型使用 & 符号声明类似,指针类型使用星号 (*) 声明
虽然通常不应该在一行中声明多个指针变量,但如果一定要这样做,则每个变量都必须包含星号。
与普通变量一样,指针默认情况下_不会_被初始化。未初始化的指针有时被称为“ 野指针” 。野指针指向的是一个无意义的地址,解引用野指针会导致未定义行为。
务必初始化指针。
指针的类型也必须与被指向对象的类型相匹配:
1 | int main() |
用字面值初始化指针是不允许的。
Pointers and assignment 指针和赋值
我们可以用两种不同的方式对指针进行赋值:
- 要改变指针指向的内容(通过给指针分配一个新地址)
- 要更改所指向的值(通过给 解引用的指针 赋一个新值)
指针和左值引用的行为类似。
指针的大小
指针的大小取决于可执行文件编译时所用的架构——32 位可执行文件使用 32 位内存地址——因此,在 32 位机器上,指针为 32 位(4 字节)。在 64 位可执行文件中,指针为 64 位(8 字节)。
Dangling pointers 悬空指针
与悬空引用类似, 悬空指针是指指向不再有效的对象(例如,已被销毁的对象)地址的指针。
指针比引用更灵活,但也更危险。
12.8 — 空指针
Null pointers
除了内存地址之外,指针还可以保存一个额外的值:空值。 空值 (通常缩写为 null )是一个特殊值,表示某个东西没有实际意义。这样的指针被称为空指针 。
创建空指针最简单的方法是使用值初始化:
1 | int main() |
就像关键字 true 和 false 表示布尔值一样,关键字 nullptr 表示空指针字面量。我们可以使用 nullptr 显式地初始化指针或将其赋值为空值。
无论何时使用指针,都需要格外小心,确保代码不会解引用空指针或悬空指针,因为这会导致未定义行为(很可能是应用程序崩溃)。
检查空指针
就像我们可以使用条件语句来测试布尔值是 true 还是 false ,我们也可以使用条件语句来测试指针的值是否为 nullptr
指针也会隐式转换为布尔值:空指针转换为布尔值 false ,非空指针转换为布尔值 true 。这使得我们可以跳过显式检查 nullptr ,而直接使用隐式转换为布尔值来判断指针是否为空指针。
指针要么应该指向有效对象的地址,要么应该被设置为 nullptr。这样我们就只需要检查指针是否为空,并且可以假设任何非空指针都是有效的。
当一个对象被销毁时,指向该对象的所有指针都会变成悬空指针(不会自动设置为 nullptr )。您有责任检测这些情况,并确保这些指针随后被设置为 nullptr 。
除非需要指针提供的额外功能,否则应优先使用引用而不是指针。
12.9 — 指针和常量
指向常量值的指针 (有时简称 pointer to const )是指向常量值的(非常量)指针。
要声明指向常量值的指针,请在指针的数据类型前使用 const 关键字:
1 | int main() |
由于指向常量的指针本身并不是常量(它只是指向一个常量值),我们可以通过给指针分配一个新地址来改变指针指向的内容。
与引用 const 类似,指向 const 的指针也可以指向非常量变量。指向 const 的指针会将指向的值视为常量,无论该地址处的对象最初是否被定义为 const
1 | int main() |
常量指针
常量指针是指初始化后地址不能改变的指针。
要声明一个常量指针,请在指针声明中的星号后使用 const 关键字:
1 | int main() |
与普通的常量变量一样,常量指针必须在定义时进行初始化,并且不能通过赋值来改变其值。
由于被指向的 值 非常量,因此可以通过解引用 const 指针来改变被指向的值
指向常量值的常量指针:const int* const ptr { &value };
12.10 — 按地址传递
C++ 提供了第三种向函数传递值的方式(前两种:按值和按引用传递),称为按地址传递。使用按地址传递时,调用者不是提供对象本身作为参数,而是提供对象的 地址 (通过指针)。这个指针(保存着对象的地址)会被复制到被调用函数的指针参数中(此时该指针参数也保存着对象的地址)。然后,函数可以通过解引用该指针来访问被传递地址的对象。
以下是上述程序的一个版本,它增加了按地址传递的功能:
1 |
|
除非函数需要修改传入的对象,否则应优先使用指向 const 的函数参数,而不是指向非常量函数参数的指针。
除非有特殊原因,否则不要将函数参数设为常量指针。
通过地址传递参数时,在解引用该值之前,务必确保指针不是空指针。
如果不应该将空指针传递给函数,则可以使用 assert(断言 assert 旨在记录不应该发生的事情)
除非有特殊原因需要使用地址传递方式,否则请优先使用引用传递方式,而不是地址传递方式。
12.11 — 按地址传递-用途
按地址传递“可选”参数
按地址传递参数的一个常见用途是允许函数接受一个“可选”参数。
1 |
|
按引用传递指针
不再推荐使用 0 或 NULL表示空指针。用 nullptr。
std::nullptr_t,略。
12.12 — 按引用返回和按地址返回
按引用返回
通过引用返回的对象在函数返回后必须存在。
通过引用返回的对象必须存在于返回该引用的函数的作用域之外,否则会导致悬空引用。切勿通过引用返回(非静态)局部变量或临时变量。
生命周期延长(临时变量的绑定到常量引用变量)无法跨 函数 生效。
避免返回对非常量局部静态变量的引用。
(到底是谁干这种事啊!)
使用返回的引用赋值/初始化普通变量会创建一个副本。
通过引用返回引用参数是可以的。
允许通过 const 引用传递的右值由 const 引用返回。
调用者可以通过引用修改值。
按地址返回
除非能够返回“无对象”(使用 nullptr )很重要,否则应优先使用引用返回而不是地址返回。
12.13 — 输入和输出参数
函数与其调用者之间通过两种机制进行通信:参数和返回值。当函数被调用时,调用者提供参数,函数通过其参数接收这些参数。这些参数可以按值、引用或地址传递。
通常情况下,我们会按值或常量引用传递参数。但有时我们可能需要采用其他方式。
在大多数情况下,函数参数仅用于接收来自调用者的输入。仅用于接收来自调用者的输入的参数有时被称为输入 in 参数 。
仅用于向调用者返回信息的函数参数称为输出 out 参数 。
1 |
|
避免使用 out 参数(除非在极少数情况下没有更好的选择)。
对于非可选的输出参数,建议按引用传递。
在极少数情况下,函数会在覆盖输出参数的值之前实际使用其值。这种参数称为输入/输出参数 。输入/输出参数的工作方式与输出参数完全相同,并面临同样的挑战。
12.14 — 使用指针、引用和 const 进行类型推导
默认情况下,类型推导会从类型中删除 const,还会丢弃引用。
如果您想要一个常量引用,即使并非绝对必要,也请重新应用 const 限定符,因为它能明确您的意图并有助于防止错误。
1 |
|
Constexpr 不是表达式类型的一部分,因此它不会被 auto 推导出来。
类型推导和指针
与引用不同,类型推导不会丢弃指针。可以使用星号结合指针类型推导( auto* )来更清楚地表明推导出的类型是指针:
1 |
|
auto 和 auto* 的区别,略。
类型推导和常量指针,略。
(用到再来看,ok?)
12.15 — std::optional
C++17 引入了 std::optional ,它是一个实现了可选值的类模板类型。也就是说,std::optional<T> 可以有一个类型为 T 的值,也可以不有一个值。我们可以用它来实现上面的第三种选择:
1 |
|
返回 std::optional 的优缺点
返回 std::optional 有很多好处:
- 使用
std::optional可以有效地表明一个函数可以返回一个值,也可以不返回一个值。 - 我们不必记住哪个值会被作为哨兵返回。
- 使用
std::optional语法既方便又直观。
返回 std::optional 确实存在一些缺点:
- 我们必须确保
std::optional包含值后才能获取其值。如果我们解引用一个不包含值的std::optional,则会导致未定义行为。 std::optional没有提供传递函数失败原因信息的方法。
它最大的用处恰恰是给 int、double、struct 这些原本没有“空”语义的值类型添加“无效状态” std::nullopt。
对于可能失败的函数,返回 std::optional (而不是哨兵值),除非你的函数需要返回有关其失败原因的附加信息。
使用 std::optional 作为可选函数参数
顾名思义, std::optional 是函数接受可选参数(仅用作传入参数)的另一种方式。它取代了以下做法:
1 |
|
你可以这样做:
1 |
|
对于可选返回类型,建议使用 std::optional 。
对于可选函数参数,应尽可能使用函数重载(情况1)。否则,当 T 通常按值传递时,应使用 std::optional<T> 来表示可选参数(情况2)。如果复制 T 成本很高,则应优先使用 const T* 。
情况1:
1 | // 重载 1:不传参数,默认行为(保存到默认路径) |
情况2:
1 | void setVolume(std::optional<float> volume = std::nullopt) { |
情况3:
1 | // 假设这是一个"巨大"的数据结构(复制成本极高) |
quiz passed.
- 标题: Cpp学习16 第 12 章 复合类型:引用与指针
- 作者: 铁名_IronName
- 创建于 : 2026-08-10 15:34:36
- 更新于 : 2026-08-10 20:20:36
- 链接: https://blog.ironname.top/2026/Cpp/Cpp学习16/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。