Cpp学习16 第 12 章 复合类型:引用与指针

铁名_IronName Lv5

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>

// T& is an lvalue reference, so this overload will be preferred for lvalues
template <typename T>
constexpr bool is_lvalue(T&)
{
return true;
}

// T&& is an rvalue reference, so this overload will be preferred for rvalues
template <typename T>
constexpr bool is_lvalue(T&&)
{
return false;
}

// A helper macro (#expr prints whatever is passed in for expr as text)
#define PRINTVCAT(expr) { std::cout << #expr << " is an " << (is_lvalue(expr) ? "lvalue\n" : "rvalue\n"); }

int getint() { return 5; }

int main()
{
PRINTVCAT(5); // rvalue
PRINTVCAT(getint()); // rvalue
int x { 5 };
PRINTVCAT(x); // lvalue
PRINTVCAT(std::string {"Hello"}); // rvalue
PRINTVCAT("Hello"); // lvalue
PRINTVCAT(++x); // lvalue
PRINTVCAT(x++); // rvalue
}

12.3 — L 值引用

在 C++ 中, 引用是现有对象的别名。一旦定义了引用,对该引用执行的任何操作都会应用于被引用的对象。这意味着我们可以使用引用来读取或修改被引用的对象。
现代 C++ 包含两种类型的引用:左值引用和右值引用。本章我们将讨论左值引用。

左值引用 (通常简称为“引用”,因为在 C++11 之前只有一种类型的引用)充当现有左值(例如变量)的别名。

指定引用的类型(例如 int& )称为引​​用类型 。可以被引用的类型(例如 int )称为被引用类型 。

1
2
3
4
5
// regular types
int // a normal int type (not an reference)
int& // an lvalue reference to an int object
double& // an lvalue reference to a double object
const int& // an lvalue reference to a const int object

左值引用变量是一个指向左值(通常是另一个变量)的引用变量。
要创建一个左值引用变量,我们只需定义一个具有左值引用类型的变量即可:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
int x { 5 }; // x is a normal integer variable
int& ref { x }; // ref is an lvalue reference variable that can now be used as an alias for variable x

std::cout << x << '\n'; // print the value of x (5)
std::cout << ref << '\n'; // print the value of x via ref (5)

return 0;
}

定义引用时,将 & 符号放在类型旁边(而不是引用变量的名称旁边)。

可以使用非常量引用来修改被引用对象的值

与常量类似,所有引用都必须初始化。引用不能重新定位(不能更改为指向另一个对象)

如果非常量左值引用可以绑定到不可修改的(常量)左值或右值,那么就可以通过引用来改变这些值,这将违反它们的常量性。

引用变量遵循与普通变量相同的作用域和持续时间规则

引用 和 引用对象 具有独立的生命周期。

当引用在被引用对象之前被销毁时,被引用对象不会受到影响。
当被引用的对象在指向它的引用被销毁之前就被销毁时,该引用就指向了一个已经不存在的对象。这样的引用被称为悬空引用 。访问悬空引用会导致未定义行为。

12.4 — const 左值引用

在声明左值引用时使用 const 关键字,我们告诉左值引用将它引用的对象视为常量。这样的引用称为指向常量值的左值引用 (有时也称为常量引用常量引用 )。

指向 const 的左值引用可以绑定到不可修改的左值:

1
2
3
4
5
6
7
int main()
{
const int x { 5 }; // x is a non-modifiable lvalue
const int& ref { x }; // okay: ref is a an lvalue reference to a const value

return 0;
}

指向 const 的左值引用甚至可以绑定到不同类型的值,只要这些值可以隐式转换为引用类型即可。

当 const 左值引用 直接 绑定到临时对象时,临时对象的生命周期会延长至与引用的生命周期相同。

Constexpr 左值引用,略。

12.5 — 按左值引用传递

既然可以直接使用变量本身,为什么还要创建变量的别名呢?
当我们调用该函数时,我们复制了参数的值,但只是短暂地使用它,然后就将其销毁了!
标准库提供的大多数类型(例如 std::string )都是Class类型。Class类型的复制通常开销很大。我们应该尽可能避免对开销很大的对象进行不必要的复制,尤其是在几乎立即销毁这些副本的情况下。

1
2
3
4
void printValue(std::string& y) // type changed to std::string&
{
std::cout << y << '\n';
} // y is destroyed here

引用参数被视为与实参是同一个对象。

按引用传递允许我们更改参数的值。

按引用传递只能接受可修改的左值参数,这意味着我们不能传递常量变量或字面量。

12.6 — 按常量左值引用传递

最佳实践

除非有特殊原因(例如函数需要更改参数的值),否则应优先使用 const 引用传递,而不是使用非常量引用传递。

一般来说,基本类型按值传递,Class 类型按常量引用传递。

按价值传递与按引用传递的成本,略。

最佳实践

除非你的函数调用其他需要 C 风格字符串或 std::string 参数的函数,否则最好使用 std::string_view (按值传递)而不是 const std::string& 来传递字符串。

原因,略。

12.7 — 指针简介

地址运算符 (&)

取地址 address-of 运算符 (&) 会返回其操作数的内存地址。

解引用运算符 (*)

地址最有用的用途是访问存储在该地址处的值。 解引用 dereference 运算符 (*)(有时也称为间接运算符 )将给定内存地址处的值作为左值返回:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
int x{ 5 };
std::cout << x << '\n'; // print the value of variable x
std::cout << &x << '\n'; // print the memory address of variable x

std::cout << *(&x) << '\n'; // print the value at the memory address of variable x (parentheses not required, but make it easier to read)

return 0;
}

Pointers  指针

指针是一个对象,它的值是一个 内存地址  (通常是另一个变量的地址)。
指定指针的类型(例如 int* )称为指针类型 。与引用类型使用 & 符号声明类似,指针类型使用星号 (*) 声明
虽然通常不应该在一行中声明多个指针变量,但如果一定要这样做,则每个变量都必须包含星号。
与普通变量一样,指针默认情况下_不会_被初始化。未初始化的指针有时被称为“ 野指针” 。野指针指向的是一个无意义的地址,解引用野指针会导致未定义行为。

最佳实践

务必初始化指针。

指针的类型也必须与被指向对象的类型相匹配:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
int i{ 5 };
double d{ 7.0 };

int* iPtr{ &i }; // ok: a pointer to an int can point to an int object
int* iPtr2 { &d }; // not okay: a pointer to an int can't point to a double object
double* dPtr{ &d }; // ok: a pointer to a double can point to a double object
double* dPtr2{ &i }; // not okay: a pointer to a double can't point to an int object

return 0;
}

用字面值初始化指针是不允许的。

Pointers and assignment  指针和赋值

我们可以用两种不同的方式对指针进行赋值:

  1. 要改变指针指向的内容(通过给指针分配一个新地址)
  2. 要更改所指向的值(通过给 解引用的指针 赋一个新值)

指针和左值引用的行为类似。

指针的大小

指针的大小取决于可执行文件编译时所用的架构——32 位可执行文件使用 32 位内存地址——因此,在 32 位机器上,指针为 32 位(4 字节)。在 64 位可执行文件中,指针为 64 位(8 字节)。

Dangling pointers  悬空指针

与悬空引用类似, 悬空指针是指指向不再有效的对象(例如,已被销毁的对象)地址的指针。

指针比引用更灵活,但也更危险。

12.8 — 空指针

Null pointers
除了内存地址之外,指针还可以保存一个额外的值:空值。 空值 (通常缩写为 null )是一个特殊值,表示某个东西没有实际意义。这样的指针被称为空指针 。

创建空指针最简单的方法是使用值初始化:

1
2
3
4
5
6
int main()
{
int* ptr {}; // ptr is now a null pointer, and is not holding an address

return 0;
}

就像关键字 true 和 false 表示布尔值一样,关键字 nullptr 表示空指针字面量。我们可以使用 nullptr 显式地初始化指针或将其赋值为空值。

无论何时使用指针,都需要格外小心,确保代码不会解引用空指针或悬空指针,因为这会导致未定义行为(很可能是应用程序崩溃)。

检查空指针

就像我们可以使用条件语句来测试布尔值是 true 还是 false ,我们也可以使用条件语句来测试指针的值是否为 nullptr
指针也会隐式转换为布尔值:空指针转换为布尔值 false ,非空指针转换为布尔值 true 。这使得我们可以跳过显式检查 nullptr ,而直接使用隐式转换为布尔值来判断指针是否为空指针。

指针要么应该指向有效对象的地址,要么应该被设置为 nullptr。这样我们就只需要检查指针是否为空,并且可以假设任何非空指针都是有效的。

当一个对象被销毁时,指向该对象的所有指针都会变成悬空指针(不会自动设置为 nullptr )。您有责任检测这些情况,并确保这些指针随后被设置为 nullptr 。

除非需要指针提供的额外功能,否则应优先使用引用而不是指针。

12.9 — 指针和常量

指向常量值的指针 (有时简称 pointer to const )是指向常量值的(非常量)指针。
要声明指向常量值的指针,请在指针的数据类型前使用 const 关键字:

1
2
3
4
5
6
7
8
9
int main()
{
const int x{ 5 };
const int* ptr { &x }; // okay: ptr is pointing to a "const int"

*ptr = 6; // not allowed: we can't change a const value

return 0;
}

由于指向常量的指针本身并不是常量(它只是指向一个常量值),我们可以通过给指针分配一个新地址来改变指针指向的内容。

与引用 const 类似,指向 const 的指针也可以指向非常量变量。指向 const 的指针会将指向的值视为常量,无论该地址处的对象最初是否被定义为 const

1
2
3
4
5
6
7
8
9
10
int main()
{
int x{ 5 }; // non-const
const int* ptr { &x }; // ptr points to a "const int"

*ptr = 6; // not allowed: ptr points to a "const int" so we can't change the value through ptr
x = 6; // allowed: the value is still non-const when accessed through non-const identifier x

return 0;
}

常量指针

常量指针是指初始化后地址不能改变的指针。
要声明一个常量指针,请在指针声明中的星号后使用 const 关键字:

1
2
3
4
5
6
7
int main()
{
int x{ 5 };
int* const ptr { &x }; // const after the asterisk means this is a const pointer

return 0;
}

与普通的常量变量一样,常量指针必须在定义时进行初始化,并且不能通过赋值来改变其值。
由于被指向的 非常量,因此可以通过解引用 const 指针来改变被指向的值

指向常量值的常量指针:const int* const ptr { &value };

12.10 — 按地址传递

C++ 提供了第三种向函数传递值的方式(前两种:按值和按引用传递),称为按地址传递。使用按地址传递时,调用者不是提供对象本身作为参数,而是提供对象的 地址 (通过指针)。这个指针(保存着对象的地址)会被复制到被调用函数的指针参数中(此时该指针参数也保存着对象的地址)。然后,函数可以通过解引用该指针来访问被传递地址的对象。

以下是上述程序的一个版本,它增加了按地址传递的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

void printByValue(std::string val) // The function parameter is a copy of str
{
std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
std::cout << ref << '\n'; // print the value via the reference
}

void printByAddress(const std::string* ptr) // The function parameter is a pointer that holds the address of str
{
std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}

int main()
{
std::string str{ "Hello, world!" };

printByValue(str); // pass str by value, makes a copy of str
printByReference(str); // pass str by reference, does not make a copy of str
printByAddress(&str); // pass str by address, does not make a copy of str

return 0;
}
最佳实践

除非函数需要修改传入的对象,否则应优先使用指向 const 的函数参数,而不是指向非常量函数参数的指针。
除非有特殊原因,否则不要将函数参数设为常量指针。

通过地址传递参数时,在解引用该值之前,务必确保指针不是空指针。
如果不应该将空指针传递给函数,则可以使用 assert(断言 assert 旨在记录不应该发生的事情)

最佳实践

除非有特殊原因需要使用地址传递方式,否则请优先使用引用传递方式,而不是地址传递方式。

12.11 — 按地址传递-用途

按地址传递“可选”参数

按地址传递参数的一个常见用途是允许函数接受一个“可选”参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

void printIDNumber(const int *id=nullptr)
{
if (id)
std::cout << "Your ID number is " << *id << ".\n";
else
std::cout << "Your ID number is not known.\n";
}

int main()
{
printIDNumber(); // we don't know the user's ID yet

int userid { 34 };
printIDNumber(&userid); // we know the user's ID now

return 0;
}

按引用传递指针
不再推荐使用 0 或 NULL表示空指针。用 nullptr。

std::nullptr_t,略。

12.12 — 按引用返回和按地址返回

按引用返回

通过引用返回的对象在函数返回后必须存在。
通过引用返回的对象必须存在于返回该引用的函数的作用域之外,否则会导致悬空引用。切勿通过引用返回(非静态)局部变量或临时变量。

生命周期延长(临时变量的绑定到常量引用变量)无法跨 函数 生效。

最佳实践

避免返回对非常量局部静态变量的引用。
(到底是谁干这种事啊!)

使用返回的引用赋值/初始化普通变量会创建一个副本。
通过引用返回引用参数是可以的。
允许通过 const 引用传递的右值由 const 引用返回。
调用者可以通过引用修改值。

按地址返回

除非能够返回“无对象”(使用 nullptr )很重要,否则应优先使用引用返回而不是地址返回。

12.13 — 输入和输出参数

函数与其调用者之间通过两种机制进行通信:参数和返回值。当函数被调用时,调用者提供参数,函数通过其参数接收这些参数。这些参数可以按值、引用或地址传递。
通常情况下,我们会按值或常量引用传递参数。但有时我们可能需要采用其他方式。

在大多数情况下,函数参数仅用于接收来自调用者的输入。仅用于接收来自调用者的输入的参数有时被称为输入 in 参数 。
仅用于向调用者返回信息的函数参数称为输出 out 参数 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cmath>    // for std::sin() and std::cos()
#include <iostream>

// sinOut and cosOut are out parameters
void getSinCos(double degrees, double& sinOut, double& cosOut)
{
// sin() and cos() take radians, not degrees, so we need to convert
constexpr double pi { 3.14159265358979323846 }; // the value of pi
double radians = degrees * pi / 180.0;
sinOut = std::sin(radians);
cosOut = std::cos(radians);
}

int main()
{
double sin { 0.0 };
double cos { 0.0 };

double degrees{};
std::cout << "Enter the number of degrees: ";
std::cin >> degrees;

// getSinCos will return the sin and cos in variables sin and cos
getSinCos(degrees, sin, cos);

std::cout << "The sin is " << sin << '\n';
std::cout << "The cos is " << cos << '\n';

return 0;
}
最佳实践

避免使用 out 参数(除非在极少数情况下没有更好的选择)。
对于非可选的输出参数,建议按引用传递。

在极少数情况下,函数会在覆盖输出参数的值之前实际使用其值。这种参数称为输入/输出参数 。输入/输出参数的工作方式与输出参数完全相同,并面临同样的挑战。

12.14 — 使用指针、引用和 const 进行类型推导

默认情况下,类型推导会从类型中删除 const,还会丢弃引用。

最佳实践

如果您想要一个常量引用,即使并非绝对必要,也请重新应用 const 限定符,因为它能明确您的意图并有助于防止错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <string>

const std::string& getConstRef(); // some function that returns a const reference

int main()
{
auto ref1{ getConstRef() }; // std::string (reference and top-level const dropped)
const auto ref2{ getConstRef() }; // const std::string (reference dropped, const dropped, const reapplied)

auto& ref3{ getConstRef() }; // const std::string& (reference dropped and reapplied, low-level const not dropped)
const auto& ref4{ getConstRef() }; // const std::string& (reference dropped and reapplied, low-level const not dropped)

return 0;
}

Constexpr 不是表达式类型的一部分,因此它不会被 auto 推导出来。

类型推导和指针

与引用不同,类型推导不会丢弃指针。可以使用星号结合指针类型推导( auto* )来更清楚地表明推导出的类型是指针:

1
2
3
4
5
6
7
8
9
10
11
#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
auto ptr1{ getPtr() }; // std::string*
auto* ptr2{ getPtr() }; // std::string*

return 0;
}

auto 和 auto* 的区别,略。
类型推导和常量指针,略。
(用到再来看,ok?)

12.15 — std::optional

C++17 引入了 std::optional ,它是一个实现了可选值的类模板类型。也就是说,std::optional<T> 可以有一个类型为 T 的值,也可以不有一个值。我们可以用它来实现上面的第三种选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <optional> // for std::optional (C++17)

// Our function now optionally returns an int value
std::optional<int> doIntDivision(int x, int y)
{
if (y == 0)
return {}; // or return std::nullopt
return x / y;
}

int main()
{
std::optional<int> result1 { doIntDivision(20, 5) };
if (result1) // if the function returned a value
std::cout << "Result 1: " << *result1 << '\n'; // get the value
else
std::cout << "Result 1: failed\n";

std::optional<int> result2 { doIntDivision(5, 0) };

if (result2)
std::cout << "Result 2: " << *result2 << '\n';
else
std::cout << "Result 2: failed\n";

return 0;
}

返回 std::optional 的优缺点

返回 std::optional 有很多好处:

  • 使用 std::optional 可以有效地表明一个函数可以返回一个值,也可以不返回一个值。
  • 我们不必记住哪个值会被作为哨兵返回。
  • 使用 std::optional 语法既方便又直观。

返回 std::optional 确实存在一些缺点:

  • 我们必须确保 std::optional 包含值后才能获取其值。如果我们解引用一个不包含值的 std::optional ,则会导致未定义行为。
  • std::optional 没有提供传递函数失败原因信息的方法。

它最大的用处恰恰是给 intdoublestruct 这些原本没有“空”语义的值类型添加“无效状态” std::nullopt

最佳实践

对于可能失败的函数,返回 std::optional (而不是哨兵值),除非你的函数需要返回有关其失败原因的附加信息。

使用 std::optional 作为可选函数参数

顾名思义, std::optional 是函数接受可选参数(仅用作传入参数)的另一种方式。它取代了以下做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

void printIDNumber(const int *id=nullptr)
{
if (id)
std::cout << "Your ID number is " << *id << ".\n";
else
std::cout << "Your ID number is not known.\n";
}

int main()
{
printIDNumber(); // we don't know the user's ID yet

int userid { 34 };
printIDNumber(&userid); // we know the user's ID now

return 0;
}

你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <optional>

void printIDNumber(std::optional<const int> id = std::nullopt)
{
if (id)
std::cout << "Your ID number is " << *id << ".\n";
else
std::cout << "Your ID number is not known.\n";
}

int main()
{
printIDNumber(); // we don't know the user's ID yet

int userid { 34 };
printIDNumber(userid); // we know the user's ID now

printIDNumber(62); // we can also pass an rvalue

return 0;
}
最佳实践

对于可选返回类型,建议使用 std::optional 。

对于可选函数参数,应尽可能使用函数重载(情况1)。否则,当 T 通常按值传递时,应使用 std::optional<T> 来表示可选参数(情况2)。如果复制 T 成本很高,则应优先使用 const T* 。

情况1:

1
2
3
4
5
6
7
8
9
// 重载 1:不传参数,默认行为(保存到默认路径)
void saveGame() {
saveGame("SaveSlot_Default.sav"); // 委托给下面的函数
}
// 重载 2:传具体路径
void saveGame(const std::string& path) {
std::cout << "游戏已保存至: " << path << std::endl;
// ... 存档逻辑
}

情况2:

1
2
3
4
5
6
7
8
9
void setVolume(std::optional<float> volume = std::nullopt) {
static float currentVolume = 0.5f; // 默认音量
if (volume.has_value()) {
currentVolume = volume.value(); // 有值,修改它
std::cout << "音量调整为: " << currentVolume << std::endl;
} else {
std::cout << "未调整音量,当前为: " << currentVolume << std::endl;
}
}

情况3:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 假设这是一个"巨大"的数据结构(复制成本极高)
struct BigTextureConfig {
int data[10000]; // 假装很大
std::string shaderPath;
};
// 应用纹理配置,如果不传(nullptr)则使用默认纹理
void applyTexture(const BigTextureConfig* config = nullptr) {
if (config) {
std::cout << "应用自定义大纹理配置(地址: " << config << ")" << std::endl;
} else {
std::cout << "应用默认纹理配置" << std::endl;
}
}

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 进行许可。
评论