施工提示
本博文尚在施工中,内容并未完成且可能变更。
引用与指针
C++ 中的引用(reference)与指针(pointer)可能是最令人头痛的内容。
引用
在定义引用时,使用 &
符作为标识符前缀:
int n = 7;
int &r = n; // 定义一个 n 的引用,其名为 r
引用在定义时不会拷贝值,而是与被引用对象进行绑定。
引用本身不是对象
引用在定义时必须初始化,且其初始绑定必须是一个 同类型对象。除了两种例外:
定义对常量的引用时,也允许使用任意 能转换为该类型 的表达式,甚至字面值。参考 常量引用 一节。
在基类中定义引用时,也允许绑定到派生类对象
引用在定义后,不能更改绑定到新的对象
double pi = 3.14159; int m = 3, n = 7; int &r = n; // 以下均错误 int &r1 = 255; // 错误:不能引用字面值 int &r2 = r; // 错误:不能引用一个引用 int &r3 = pi; // 错误:对象非 int 类型 &r = m; // 错误:&r 不是左值(不能改变绑定到新的对象)
注意
再次重申:引用不是对象,它只是被引对象的另一个名字。
通过引用,我们可以修改原对象的值(除了对 const 常量的引用情形,参考 常量引用 一节):
int val = 100;
int &r1 = val;
r1 = 3;
std::cout << val << std::endl; // val 的值变为 3
常量引用
重要
常量引用与普通的引用有诸多不同之处,这有助于我们更深入地理解引用。
有时我们称呼“对 const 常量的引用”为“常量引用”,因为这样更简单。由于引用不是对象,常量是修饰被引用对象的(而不是修饰引用名),因此这两种表述实质等同,不涉及歧义。与之不同的是,“指针常量”与“指向常量的指针”则表示的是不同的东西。
常量引用不允许对绑定对象进行修改:
const int cval = 20;
int &r2 = cval;
r2 = 7; // 错误:不允许修改对常量的引用
此外,常量引用的特殊之处在于,允许使用任意同类型(或能转换为该类型的)表达式作为初始值。而普通引用不允许转换类型,也不允许使用字面值。下例给出了一些正确的常量引用定义:
double val = 1.23;
const int &r1 = val;
const int &r2 = 127;
备注
为什么上述定义对于常量引用是允许的,对于普通的引用就不允许?这是因为上述语句在定义时实际上借助了临时量:
const int temp = val; // 先用临时量对 val 进行类型转换
const int &r1 = temp; // 再将 r1 绑定到临时量 temp 上
同理,常量引用 &r2
在定义时,也需要对字面值创建临时量。毕竟引用的定义实质上就是对某个对象的绑定。
在常量引用中,由于绑定值不可修改,因此上述定义是允许的。如果非常量引用可以这样定义,那么会导致修改引用实质上修改了临时量的值,而不是原始对象的值;这种逻辑是不合理的,因此不允许这样定义非常量引用。
基础指针
警告
下面你将认识的是,C++ 世界中最强大的苦痛之源、声名远播到甚至其他编程语言世界的折磨王,指针。请打起精神!
重要
为了更安全地管理动态内存,现代 C++ 提倡使用智能指针,参考 智能指针。在阅读智能指针相关的内容前,我仍推荐阅读本节了解基础的指针概念。
指针像引用一样提供了对其他对象的访问。但不同的是:
指针本身是一个对象。指针可以被复制,也可以被更改指向的对象。
指针实质上存储了所指向对象的地址。
在语法上,指针在定义时可以不显式地指定其初始值。
建议总是显式初始化指针,至少指向空指针字面值
nullptr
C++ 11。在过去,空指针使用NULL
或者 0,现在应当使用 nullptr 代替。
危险
警惕对无效指针进行操作,这会导致错误。更进一步地,应该在代码中尽可能地消除无效指针的出现。
在定义指针时,使用 *
标识符前缀:
// 定义整数 m, n 以及一个指向整数的指针 p1
int *p1, m, n;
// 定义一个空指针 p2
double *p2 = nullptr;
// 定义一个指针 p3, 初始化为指针 p1 指向的对象(p1 中存放的地址)
int *p3 = p1; // 注意,此时不需要用 & 对 p1 取地址
建议使用 int *p 而不是 int* p
有时候我们会看见用 int* p;
这种书写方式表示定义 int 指针的写法。它把 int 与 * 写在一起,强调我们正在定义的是一个指针,而不是一个简单的 int 类型变量。
我并不推荐这种写法。在定义时, *p
应当视为一个整体(变量标识符),表示要定义一个指针,其名为 p。写为 int *p
可以较少地引起误解。这也可以通过下例说明:
// 等价于:int *p1, p2, p3;
int* p1, p2, p3; // 定义了一个指针 p1 与两个整数 p2, p3
即使写成 int*
,它们也不能视为一个整体,故也不能认为 p2, p3 被定义为指针。因此,我认为 int*
这种迷惑性的写法是应当避免的。
下面是一个简单的例子,展示了指针的基本操作:
指针需要指向一个同类型对象,并利用 取地址符(
&
) 取得对象的地址。在使用指针时,利用 解引用符(
*
) 访问指向的对象。
1#include <iostream>
2#include <string>
3using std::cout;
4using std::endl;
5
6int main() {
7 double x = 1.23;
8 double *p = &x;
9 cout << "*p = " << *p
10 << "\np = " << p << endl;
11
12 *p = 0.3;
13 cout << "\n*p = " << *p
14 << "\nx = " << x << endl;
15 return 0;
16}
17/*
18 输出(p 的地址每次运行都会不同):
19 *p = 1.23
20 p = 0xac89bffc70
21
22 *p = 0.3
23 x = 0.3
24*/
在使用指针时,一定不要混淆这两种用法:
对解引用的指针
*p
进行赋值,等于对指针p
指向的对象赋值,指针仍然指向那个对象。对指针
p
进行赋值,等于更改指针p
中存放的地址,即指针指向了一个新的对象。
double x = 1.0, y = 9.80665;
double *p = nullptr;
// 更改 x 为 2.0,指针 p 仍指向 x
*p = 2.0;
// 让指针 p 指向新对象 y
p = &y;
指针的布尔操作
当一个指针是空指针时,其逻辑值为 false;否则为 true。
如果两个指针均为空指针,或者它们存储了相同地址,那么这两个指针相等;否则它们不相等。
1#include <iostream>
2#include <string>
3using std::cout;
4using std::endl;
5
6int main() {
7 int n = 10;
8
9 int *p = nullptr;
10 if (!p) { cout << "p: false" << endl; }
11
12 int *p1 = &n, *p2 = p1;
13 if (p1 == p2) { cout << "p1 == p2" << endl; }
14 int *q = nullptr;
15 if (p == q) { cout << "p == q" << endl; }
16 return 0;
17}
18/*
19 输出:
20 p: false
21 p1 == p2
22 p == q
23*/
指针嵌套
由于指针是对象,因此指针与引用也都可以指向一个指针对象。
int n = 10;
// p 是一个指向 int 变量 n 的指针
int *p = &n;
// q 是一个指向 int 型指针 p 的指针
// 要解引用到 n,使用 **q
int **q = &p;
// r 是对指针 p 的引用
// 要解引用,使用 *r
int *&r = p;
对指针的引用 *&r
可以从右往左理解:我们要定义的是 r
,它是一个引用(&
),这个引用将引用一个指针对象(*
),而这个指针应是 int 类型的。
指针与 const
指针与 const 有两种组合方式:一是指向常量的指针,二是常量指针。
指向常量的指针,比如 const int *p
,就像常量引用一样,不能通过它改变指向对象的值。
如果要让指针指向一个常量,只能使用指向常量的指针,不能使用常量指针或普通指针。
指向常量的指针只是不允许通过该指针更改其指向对象,但指向对象可以是非常量对象。
const double pi = 3.14; const double *p = π *p = 3.14159; // 错误:不能更改 p 指向的对象
常量指针,比如 int *const p
,是指指针本身是常量(而不是说指针指向的对象是常量)。
常量指针必须显式初始化。
常量指针的值(即其存放的地址)一经初始化,就不能改变。
与前述的指向常量的指针组合,可以得到指向常量的常量指针(
const int *const p
)// 常量指针 q1,指向整数 m int m = 14; int *const q1 = &m; // 指向常量的常量指针 q2,指向 int 型常量 n const int n = 23; const int *const q2 = &n;
同样从右往左理解 q2 的定义:我们要定义的是 q2
;它是一个常量(&
),还是一个指针(*
),也就是说它是一个常量指针;这个常量指针将指向一个 int 型常量(const int
),也就是说它同时是一个指向常量的指针。
顶层常量与底层常量
像上例中展示的一样,指针的特殊性使得 const 的修饰有时让人迷惑。如果指针本身是一个常量,我们称之为 顶层常量,比如常量指针;如果指针的基础类型(指向对象的类型)是一个常量,我们称之为 底层常量,比如指向常量的指针。在一个多重 const 修饰的定义中:
const int *const q2 = &n;
第一个 const 表示指向的是一个 int 型常量,因此它对应底层常量;第二个 const 表示要定义的是一个 const 类型的指针,因此它对应顶层常量。
顶层常量可以复制到同类型的非常量。
底层常量在复制时,要求两个对象具有相同的底层常量类型,或者可以转换。一般非常量可以转换为常量,反之不允许。
void 指针*
void 指针是一种特殊指针,它可以存放任意对象的地址。它可以用于不清楚所指对象类型的场合,但是局限性很大。它只能:
指向任意对象的地址
进行布尔操作
作为函数输入或输出
赋给另一个 void 指针
double x = 1.2;
int n = 10;
void *p = &x;
*p = &n;
由于不能通过 void 指针更改其指向的对象,我们较少使用这种指针。
智能指针
智能指针 C++ 11 模板由标准库 <memory>
提供,旨在结束指针的所有使用后自动销毁指向的对象,从而释放内存。该库提供了两种智能指针:
std::shared_ptr<T>
声明一个共享智能指针。允许与其他指针共享其所指向的、类型为 T 的对象。std::unique_ptr<T>
声明一个独占智能指针。不允许其他指针指向其所指的、类型为 T 的对象。
以及一种比较特殊的弱指针 std::weak_ptr<T>
。
独占指针:unique_ptr
unique_ptr
并不能像 shared_ptr
一样使用 make_shared
分配动态内存,而是使用 make_unique
C++ 14 代替。
1#include <iostream>
2#include <memory>
3
4class DataClass {
5private:
6 int n;
7 double x;
8 double result;
9public:
10 DataClass(int n, double x): n(n), x(x) { result = x * n; };
11 double getResult() { return result; };
12};
13
14int main() {
15 std::unique_ptr<int> up1 = std::make_unique<int>(12);
16 // 使用 auto 简化书写
17 auto up2 = std::make_unique<int>(79);
18 // 交换两个指针的值
19 up2.swap(up1);
20 std::cout << "up1: " << *up1 << ", up2: " << *up2 << std::endl;
21 // 传入符合 DataClass 构造函数的参数
22 auto up3 = std::make_unique<DataClass>(2, 3.14);
23 std::cout << "Result: " << up3->getResult() << std::endl;
24}
25/*
26 输出:
27 up1: 79, up2: 12
28 Result: 6.28
29*/
独占指针也可以使用 up1.swap(up2)
来交换两个指针的值。
弱指针:weak_ptr
弱指针 std::weak_ptr
是一种有趣而实用的指针,它可以指向一个共享指针 std::shared_ptr
指向的对象,但不影响其引用计数器。当最后一个指向该对象的共享指针销毁,即使仍然有弱指针指向它,该对象也会被释放。
弱指针需要使用共享指针来初始化。
弱指针可能指向无效,因此必须在使用时用其
.lock()/.expired()
成员函数来检查。expired()
:判断弱指针是否有效(指向的对象是否被释放),返回 true 或者 false。lock()
:当弱指针有效时,返回一个指向对象的共享指针;否则,返回一个空的共享指针。
将弱指针临时强化为共享指针,再访问其指向的对象
对于弱指针 wp,一般我们先用 sp = wp.lock()
返回共享指针对象 sp
,并用 if 语句判断其是否为空。如果非空,那么我们可以操作这个临时的共享指针,例如 sp->member()
。
下例展示了弱指针的一些基本用法:
1#include <iostream>
2#include <memory>
3
4void wp_check(std::weak_ptr<int> p_weak) {
5 if (std::shared_ptr<int> p = p_weak.lock()) {
6 // p_weak 指向的对象有效
7 std::cout << "Valid: " << *p << std::endl;
8 } else {
9 std::cout << "Invalid weak pointer.";
10 }
11}
12
13int main() {
14 // 用共享指针 sp 初始化弱指针 wp
15 auto sp = std::make_shared<int>(7);
16 auto wp = std::weak_ptr(sp);
17 wp_check(wp);
18
19 // 将 sp 置空,其指向对象被释放
20 sp.reset();
21 wp_check(wp);
22
23 // 将 wp 置空
24 wp.reset();
25 return 0;
26}
27/*
28 输出:
29 Valid: 7
30 Invalid weak pointer.
31*/
弱指针的一个重要意义是在 设计从属关系的对象时避免循环引用(因为这会阻止智能指针释放对象)。例如,在树的节点定义中,父节点需要一系列指针指向其每个子节点,而子节点也需要一个指针指向其父节点。如果在这两处都使用共享指针,那么父节点与子节点将会相互指向,导致节点在被删除时引用计数总不为零,从而并不能被释放。因此,我们在节点中使用共享指针定义 children
,使用弱指针定义 parent
。
下例是一个使用弱指针与共享指针,对树节点进行定义的例子:
1#include <algorithm>
2#include <iostream>
3#include <memory>
4#include <vector>
5
6class Node: public std::enable_shared_from_this<Node> {
7public:
8 Node(int v) : value(v) {};
9 void addChild(std::shared_ptr<Node> child) {
10 // 使用 shared_from_this 从对象内部创建其自身的共享指针
11 child->parent = shared_from_this();
12 children.push_back(child);
13 }
14 void removeChild(std::shared_ptr<Node>& child) { // 传引用,以更改外部 child
15 auto it = std::find_if(children.begin(), children.end(),
16 [&child](const auto& v) { return v == child; });
17 // 将 child 从当前节点的子节点列表中移除,然后置空 child
18 if (it != children.end()) {
19 children.erase(it);
20 child.reset();
21 }
22 }
23 void printValues() const {
24 std::cout << "Current: " << getValue() << " (parent value: ";
25 // 先将弱指针强化为shared_ptr,再判断是否为空,非空则使用
26 if (auto parent_ = getParent().lock()) {
27 std::cout << parent_->getValue() << "; ";
28 } else {
29 std::cout << "None; ";
30 }
31 std::cout << "has " << getChildren().size() << " children)\n";
32 for (auto sp : getChildren()) {
33 std::cout << "* Child value: " << sp->getValue() << "\n";
34 }
35 }
36 int getValue() const { return value; }
37 std::weak_ptr<Node> getParent() const { return parent; }
38 std::vector<std::shared_ptr<Node>> getChildren() const { return children; }
39
40private:
41 int value;
42 std::weak_ptr<Node> parent;
43 std::vector<std::shared_ptr<Node>> children;
44};
45
46int main() {
47 auto root = std::make_shared<Node>(1);
48 auto child1 = std::make_shared<Node>(101);
49 auto child1_wptr = std::weak_ptr<Node>(child1); // 此处仅用于追踪引用计数
50 auto child2 = std::make_shared<Node>(102);
51 root->addChild(child1);
52 root->addChild(child2);
53 auto grandchild1 = std::make_shared<Node>(10101);
54 child1->addChild(grandchild1);
55
56 std::cout << "--- Root ---" << "\n";
57 root->printValues();
58 std::cout << "--- Child 1 ---" << "\n";
59 child1->printValues();
60 // 引用计数来自 child1 自身,以及 root 中的 children
61 std::cout << "Use count: " << child1.use_count() << "\n";
62 std::cout << "--- Grandchild 1 ---" << "\n";
63 grandchild1->printValues();
64
65 root->removeChild(child1);
66 std::cout << "\n--- Root (After removal) ---" << "\n";
67 root->printValues();
68 std::cout << "--- Child 1 (After removal) ---" << "\n";
69 std::cout << "Use count: " << child1_wptr.use_count() << "\n";
70 std::cout << "--- Grandchild 1 (After removal) ---" << "\n";
71 grandchild1->printValues();
72 std::cout << "Use count: " << grandchild1.use_count();
73
74 return 0;
75}
76/*
77 输出:
78 --- Root ---
79 Current: 1 (parent value: None; has 2 children)
80 * Child value: 101
81 * Child value: 102
82 --- Child 1 ---
83 Current: 101 (parent value: 1; has 1 children)
84 * Child value: 10101
85 Use count (Node): 2
86 Use count (Weak ptr): 2
87 --- Grandchild 1 ---
88 Current: 10101 (parent value: 101; has 0 children)
89
90 --- Root (After removal) ---
91 Current: 1 (parent value: None; has 1 children)
92 * Child value: 102
93 --- Child 1 (After removal) ---
94 Use count: 0
95 --- Grandchild 1 (After removal) ---
96 Current: 10101 (parent value: None; has 0 children)
97 Use count: 1
98*/