第 12 章

引用:不能为空的别名,以及它与指针的分工

这一章把引用与指针逐条对照:引用声明时必须初始化、不能改绑、使用时无需解引用;再讲 const 引用只读的边界,以及用「指向 const 的 const 指针」模拟引用的写法。

References: Aliases That Cannot Be Null, and How They Differ from Pointers

This chapter compares references and pointers point by point: a reference must be initialised, cannot be rebound, and needs no dereference; then it covers what const references restrict, and how a pointer-to-const that is itself const reproduces reference behaviour.

首发于 2022.02.17中 / EN
系统课程cppreferencespointersconst
语言
字号
目录
篇内目录

这一章在讲什么

这是 31 小时长视频的第 12 章,对应 12:22:23–12:46:10。

上一章末尾已经出现过引用:int& ref = data; 之后,通过 ref 修改就是修改 data。这一章回答「既然指针已经能做到,为什么还要引用」,并给出两者的分工。

答案可以压缩成三点:引用不需要解引用符号、不能改绑、声明时必须初始化。这三点让引用更接近「原变量的另一个名字」,也让它天然避开上一章一半的悬空指针问题。后半章讲 const 引用——只读别名,是后面函数参数最常用的形式。

时间轴

时间内容
12:22:23为什么要同时存在指针与引用
12:23:11三点区别:无需解引用、不能改绑、必须初始化
12:24:11读写的两种语法对照
12:25:44对引用赋值 ≠ 改绑:实验与地址验证
12:26:57引用相当于「const 指针」
12:27:35VS Code 实操:两种语法与改绑实验
12:32:22未初始化的引用直接编译失败
12:37:30const 引用:只读别名
12:38:56用 const T* const 模拟引用行为
12:39:20const 作用在引用名上,与原变量是否为 const 无关
12:40:05VS Code 实操:const 引用与写操作报错

三点区别

一、引用不需要解引用

double double_value {12.34};
double& ref_double_value {double_value};   // 引用
double* p_double_value {&double_value};    // 指针

// 读
std::cout << double_value << std::endl;      // 12.34
std::cout << ref_double_value << std::endl;  // 12.34
std::cout << *p_double_value << std::endl;   // 12.34(必须解引用)

// 写
ref_double_value = 20.0;      // 直接赋值
*p_double_value = 30.0;       // 必须解引用

引用在使用上就像原变量本身:读它、写它、取地址,都和原变量等价。指针则始终需要区分「地址」与「地址上的值」。

std::cout << p_double_value << std::endl;   // 打印地址
std::cout << ref_double_value << std::endl; // 打印值

二、引用不能改绑

double other_value {78.45};
ref_double_value = other_value;   // 这是赋值,不是改绑!

这一行不会让 ref_double_value 改为引用 other_value。它把 other_value 的值赋给 ref_double_value 所引用的 double_value。结果:

  • double_value 变成 78.45;
  • ref_double_value 仍然绑定 double_value;
  • 两者地址都没有变。

C++ 没有语法可以在初始化之后让引用改绑。唯一的机会就是声明的那一刻,这也解释了为什么引用必须初始化:如果允许声明时不初始化,这个引用将永远无法绑定到任何对象,毫无用处。编译器直接拒绝:

error: reference variable 'ref' requires an initializer

指针没有这个限制:

double* p {&double_value};
p = &other_value;          // 合法:指针改为指向另一个变量

三、引用像「const 指针」

把两者放在一起看:

行为引用 T&指针 T*
声明时是否必须初始化必须不必(但不初始化很危险)
能否改绑到其他对象不能能
使用是否需要 *不需要需要
能否为空不能能(nullptr)

「不能改绑」正是 T* const 的性质,所以引用常被描述为「自动解引用的 const 指针」。

const 引用

int age {27};
int& ref_age {age};              // 可写引用
const int& cref_age {age};       // 只读引用

ref_age++;                        // 合法,age 变成 28
cref_age++;                       // 编译错误

const 引用承诺:通过这个名字不能修改原对象。注意它并不要求原对象本身是 const:

const int& cref_age {age};   // age 是非 const 的
age = 30;                    // 合法,直接修改原变量
// cref_age 读出来就是 30

也就是说,const 限制的是「这个别名能不能写」,而不是「原对象能不能变」。这一点在函数参数里非常重要:const T& 表示「我要读你,但不会改你」,调用方不必担心副作用,也不必复制。

用指针模拟引用

引用的行为等价于「指向 const 的 const 指针」:

const int* const p_age {&age};

两个 const 分别限制两件事:

  • 左边的 const:不能通过 *p_age 修改 age;
  • 右边的 const:不能让 p_age 指向别处。

于是 *p_age = 30; 和 p_age = &other; 都会编译失败,行为与 const int& 一致。这也说明引用并非新机制,只是把这两种限制打包成了一个更简洁的语法。

顺带一提:const int& const 是非法的——引用本身没有「顶层 const」可言,因为它从来就不能改绑,再加一个 const 没有意义。

什么时候用哪个

  • 函数参数:只读传参用 const T&,需要修改用 T&。避免拷贝,语义清晰。
  • 必须「没有对象」时:用指针,nullptr 表达「没有」。引用无法表达空。
  • 需要改指向时:用指针。
  • 数组/动态内存:天然用指针。

一句话:优先用引用,只有在需要「可空」或「可改绑」时才退回指针。

一句话总结: 引用是必须初始化、不能改绑、无需解引用的别名;对引用赋值是改值不是改绑;const T& 只是禁止通过别名写入,与对象本身是否可变无关。

本章结论

  • 引用是已有对象的别名,声明时必须初始化,之后不能改绑。
  • 读写引用不需要 *,写法与原变量一致。
  • 对引用赋值只修改被引用对象的值,不会改变绑定关系。
  • 指针可以改绑、可以为空,但使用时要区分地址与值。
  • const T& 禁止通过引用修改对象,但不要求对象本身是 const。
  • 引用等价于 const T* const;不存在 const T& const。

理解检验

1. 引用与指针的三点主要区别是什么?

参考答案:第一,使用引用不需要解引用符号,读写方式与原变量一致;指针必须用 * 取值、& 取地址。第二,引用在初始化之后不能改绑到另一个对象,指针可以重新赋值指向别处。第三,声明引用时必须初始化,否则编译失败;指针可以先声明后赋值(但未初始化的指针是危险的)。

2. 执行 ref = other; 会发生什么?为什么这不是「改绑」?

参考答案:这是把 other 的值赋给 ref 所引用的对象,等价于 original = other;;ref 仍然绑定 original,两者的地址都不变。C++ 的引用在初始化时就固定了绑定,之后没有语法可以改变它——唯一的机会是声明那一刻。想验证这一点,可以打印 &original 与 &ref,赋值前后地址完全相同。

3. 为什么引用必须在声明时初始化?

参考答案:因为初始化是引用绑定对象的唯一机会。若允许声明时不初始化,这个引用就永远无法指向任何对象,之后任何使用都无意义,编译器因此直接拒绝。指针不同:它可以先为空、之后再赋值,所以不强制初始化(尽管实践中也应初始化)。

4. const int& cref = age; 中 age 不是 const,这合法吗?此时能否通过 age 修改它?

参考答案:合法。const 加在引用上,限制的是「通过 cref 这个名字不能写」,并不要求被引用的对象本身是 const。age 仍然可以被直接赋值,例如 age = 30;;之后通过 cref 读出来就是 30。所以 const T& 的准确含义是「只读别名」,而不是「只读对象」。

5. const int* const p = &age; 中的两个 const 分别限制什么?它为什么能模拟引用?

参考答案:左边的 const 修饰 int,表示不能通过 *p 修改 age;右边的 const 修饰指针本身,表示不能让 p 指向别处。两者合起来正好对应引用的两个性质:不能通过别名写原对象、不能改绑。再加上引用自动解引用,所以 const T& 在行为上等价于 const T* const。

6. 一个函数参数应该用 T、T& 还是 const T&?

参考答案:需要就地修改调用方对象时用 T&;只读且对象较大、不想复制时用 const T&;对象很小(如 int、double、指针)且只读时,直接按值 T 也很合理,复制成本低于间接访问的成本。需要表达「可能没有对象」时改用指针 T*,因为引用无法为空。

与前后章的关系

引用把上一章的指针危险面收窄了一大块:不能为空,就不会有空指针解引用;不能改绑,就不会有指向失效对象的指针。下一章(第 13 章)转向文本数据,先讲 C 字符串的内存布局与操作,再引出 std::string。而「const T& 传参」会在第 15 章的函数参数中成为默认写法,也是第 18 章类方法里避免拷贝的基本手段。

What this chapter is about

This is chapter 12 of a single 31-hour video, covering 12:22:23–12:46:10.

References already appeared at the end of the last chapter: after int& ref = data;, writing through ref writes through data. This chapter answers "if pointers can already do that, why do references exist", and sets out the division of labour.

The answer compresses to three points: a reference needs no dereference, cannot be rebound, and must be initialised. Those three make it feel like another name for the original variable, and they remove half of the dangling-pointer problems from the previous chapter. The second half covers const references — read-only aliases, the most common form for function parameters.

Timeline

TimeTopic
12:22:23Why both pointers and references exist
12:23:11Three differences: no dereference, no rebinding, required initialisation
12:24:11Side-by-side syntax for reading and writing
12:25:44Assigning to a reference is not rebinding: experiment and addresses
12:26:57A reference behaves like a const pointer
12:27:35Live coding: both syntaxes and the rebinding experiment
12:32:22An uninitialised reference fails to compile
12:37:30const references as read-only aliases
12:38:56Simulating a reference with const T* const
12:39:20const applies to the reference name, not the original object
12:40:05Live coding: const references and the write error

The three differences

One: no dereference

double double_value {12.34};
double& ref_double_value {double_value};   // reference
double* p_double_value {&double_value};    // pointer

// read
std::cout << double_value << std::endl;      // 12.34
std::cout << ref_double_value << std::endl;  // 12.34
std::cout << *p_double_value << std::endl;   // 12.34 (dereference required)

// write
ref_double_value = 20.0;      // direct
*p_double_value = 30.0;       // dereference required

A reference behaves like the original variable: reading, writing and taking its address all match. A pointer always forces you to distinguish "the address" from "the value at the address".

std::cout << p_double_value << std::endl;   // prints the address
std::cout << ref_double_value << std::endl; // prints the value

Two: a reference cannot be rebound

double other_value {78.45};
ref_double_value = other_value;   // assignment, not rebinding!

This line does not make ref_double_value refer to other_value. It assigns the value of other_value into double_value. The result:

  • double_value becomes 78.45;
  • ref_double_value is still bound to double_value;
  • neither address changes.

C++ has no syntax for rebinding a reference after initialisation. The only opportunity is the declaration itself, which is also why a reference must be initialised: a reference declared without one could never be bound to anything and would be useless. The compiler refuses outright:

error: reference variable 'ref' requires an initializer

A pointer has no such restriction:

double* p {&double_value};
p = &other_value;          // legal: the pointer now points elsewhere

Three: a reference is like a const pointer

Side by side:

BehaviourReference T&Pointer T*
Must be initialised at declarationyesno (but uninitialised is dangerous)
Can be reboundnoyes
Needs * to usenoyes
Can be nullnoyes (nullptr)

"Cannot be rebound" is exactly the property of T* const, so a reference is often described as a const pointer that dereferences itself.

const references

int age {27};
int& ref_age {age};              // writable reference
const int& cref_age {age};       // read-only reference

ref_age++;                        // legal, age becomes 28
cref_age++;                       // compile error

A const reference promises that this name cannot modify the object. It does not require the object itself to be const:

const int& cref_age {age};   // age is not const
age = 30;                    // legal, modify the original directly
// cref_age now reads 30

So const restricts whether the alias may write, not whether the object may change. This matters for function parameters: const T& means "I will read you but not modify you", so callers need not worry about side effects and nothing is copied.

Simulating a reference with a pointer

A reference behaves like a pointer to const that is itself const:

const int* const p_age {&age};

The two consts restrict two different things:

  • the left const forbids writing through *p_age;
  • the right const forbids making p_age point elsewhere.

Both *p_age = 30; and p_age = &other; fail to compile, matching const int&. This shows that a reference is not a new mechanism: it packages those two restrictions into a simpler syntax.

Incidentally, const int& const is illegal — a reference has no top-level const to add, because it can never be reseated in the first place.

Which one to use

  • Function parameters: const T& for read-only, T& when the callee must modify the argument. No copy, clear semantics.
  • When "no object" must be expressible: use a pointer and nullptr. A reference cannot be empty.
  • When the target must change: use a pointer.
  • Arrays and dynamic memory: pointers are the natural tool.

In one sentence: prefer references, and fall back to pointers only when you need nullability or rebinding.

One-line summary: a reference is an alias that must be initialised, cannot be rebound and needs no dereference; assigning to it changes the value, not the binding; const T& only forbids writing through the alias, regardless of whether the object itself is const.

Chapter takeaways

  • A reference is another name for an existing object; it must be initialised and cannot be rebound.
  • Reading and writing a reference needs no *; the syntax matches the original variable.
  • Assigning to a reference changes the referenced object's value, not the binding.
  • A pointer can be rebound and can be null, but you must separate the address from the value.
  • const T& forbids modification through the reference without requiring the object to be const.
  • A reference is equivalent to const T* const; const T& const does not exist.

Comprehension check

1. What are the three main differences between references and pointers?

Reference answer: First, using a reference needs no dereference operator — read and write look like the original variable — while a pointer needs * to reach the value and & to take an address. Second, once initialised a reference cannot be rebound to another object, whereas a pointer can be reassigned to point elsewhere. Third, a reference must be initialised at declaration or compilation fails, while a pointer may be declared and assigned later (though an uninitialised pointer is dangerous).

2. What happens when you execute ref = other;, and why is it not a rebinding?

Reference answer: It assigns the value of other into the object ref refers to, equivalent to original = other;. ref stays bound to original, and neither address changes. A C++ reference fixes its binding at initialisation and has no syntax to change it afterwards — the declaration is the only opportunity. Printing &original and &ref before and after the assignment shows identical addresses.

3. Why must a reference be initialised at declaration?

Reference answer: Initialisation is the only chance to bind a reference to an object. If a reference were allowed to exist uninitialised, it could never be bound later and every use would be meaningless, so the compiler rejects it. A pointer differs: it can start as null and be assigned afterwards, so initialisation is not mandatory (though it remains good practice).

4. Is const int& cref = age; legal when age is not const? Can age still be modified directly?

Reference answer: It is legal. The const applies to the reference, restricting writes through the name cref; it does not require the referenced object to be const. age can still be assigned directly, for example age = 30;, and cref then reads 30. The accurate meaning of const T& is "read-only alias", not "read-only object".

5. In const int* const p = &age;, what does each const restrict, and why does it simulate a reference?

Reference answer: The left const qualifies int, forbidding modification of age through *p; the right const qualifies the pointer, forbidding p from pointing elsewhere. Together they match the two properties of a reference: no writing through the alias and no rebinding. Add automatic dereferencing and const T& behaves exactly like const T* const.

6. Should a function parameter be T, T&, or const T&?

Reference answer: Use T& when the function must modify the caller's object; use const T& when it only reads and the object is large enough that copying is wasteful; use plain T for small read-only values such as int, double or a pointer, where copying costs less than indirection. When "there may be no object" must be expressible, use T*, because a reference cannot be null.

How this connects to the neighbouring chapters

References remove a large part of the danger from the previous chapter: no null means no null dereference, and no rebinding means no pointer left aimed at an object that has gone. Chapter 13 turns to text, covering the memory layout and operations of C strings before introducing std::string. Passing by const T& becomes the default in chapter 15's function parameters and a basic tool for avoiding copies in chapter 18's class methods.

来源