施工提示

本博文尚在施工中,内容并未完成且可能变更。

引用与指针

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++ 提倡使用智能指针,参考 智能指针。在阅读智能指针相关的内容前,我仍推荐阅读本节了解基础的指针概念。

指针像引用一样提供了对其他对象的访问。但不同的是:

  • 指针本身是一个对象。指针可以被复制,也可以被更改指向的对象。

  • 指针实质上存储了所指向对象的地址。

  • 在语法上,指针在定义时可以不显式地指定其初始值。

    • 建议总是显式初始化指针,至少指向空指针字面值 nullptrC++ 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 = &pi;
    *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>

共享指针:shared_ptr

shared_ptr 分配动态内存时,推荐使用 std::make_shared<T> 函数,它将在动态内存中分配一个类型为 T 的对象(并用函数接受的参数初始化 T 对象),然后返回一个指向该对象的 shared_ptr 指针。给该函数传递参数时,参数的形式必须与 T 类型的某一构造函数匹配。

一个使用 shared_ptr 智能指针的用例:

  • std::make_shared:分配动态内存,常用于初始化智能指针。

  • sp1.swap(sp2):交换两个智能指针 sp1 与 sp2 的值。

  • sp.reset():让 sp 不再指向原对象。如果无其他共享指针指向该对象,则释放该对象。

  • sp.unique():检查 sp 是否是所指对象的唯一用户。

 1#include <iostream>
 2#include <memory>
 3
 4int main() {
 5    std::shared_ptr<int> sp = std::make_shared<int>(12);
 6    // 使用 auto 简化书写
 7    auto sp2 = std::make_shared<int>(37);
 8
 9    // swap: 交换两个智能指针的值
10    sp2.swap(sp);
11    std::cout << "sp: " << *sp << ", sp2: " << *sp2 << std::endl;
12
13    // 令 sp 指向 sp2 指向的对象
14    // 当 sp 原指向的对象没有任何智能指针引用时,其会被销毁
15    sp = sp2;
16
17    // 拷贝智能指针,让 sp3 指向与 sp2 相同的对象
18    auto sp3(sp2);
19
20    // unique(): 检查 sp 是否是所指对象的唯一用户
21    sp = std::make_shared<int>(127);
22    std::cout << "sp unique? " << sp.unique() << std::endl;
23    
24    // reset: 让 sp 不再指向任何对象。如果无其他共享指针指向该对象,则释放其内存
25    sp.reset();
26}
27/*
28  输出:
29  sp: 37, sp2: 12
30  sp unique? 1
31*/
  • shared_ptr 通过引用计数器来控制销毁。当初始化或拷贝智能指针时,计数器会加1;当智能指针被赋予一个新值(令其指向一个新对象),或者离开作用域时,计数器会减1。当计数器归零时,其指向的对象会被释放。

    • 如果将在临时容器内存储 shared_ptr(比如用于排序),那么在使用后应当用 erase 或者 clear 释放容器以便销毁。

  • 类对象有时需要在内部使用其自身的共享指针。这时,我们需要让类继承 std::enable_shared_from_this<ClassName> 并使用 shared_from_this()

    class Node: public std::enable_shared_from_this<Node> {
       // ...
       child->parent = shared_from_this();
       // ...
    }
    

    具体的使用,请参考 智能指针示例:树节点 的代码。

独占指针:unique_ptr

unique_ptr 并不能像 shared_ptr 一样使用 make_shared 分配动态内存,而是使用 make_uniqueC++ 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*/