引用与指针 =============== C++ 中的引用(reference)与指针(pointer)可能是最令人头痛的内容。 .. _reference: 引用 ---------- 在定义引用时,使用 ``&`` 符作为标识符前缀: .. code-block:: c++ int n = 7; int &r = n; // 定义一个 n 的引用,其名为 r 引用在定义时不会拷贝值,而是与被引用对象进行绑定。 * 引用本身不是对象 * 引用在定义时必须初始化,且其初始绑定必须是一个 **同类型对象**\ 。除了两种例外: * 定义对常量的引用时,也允许使用任意 *能转换为该类型* 的表达式,甚至字面值。参考 :ref:`const-reference` 一节。 * 在基类中定义引用时,也允许绑定到派生类对象 * 引用在定义后,不能更改绑定到新的对象 .. code-block:: c++ 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 不是左值(不能改变绑定到新的对象) .. attention:: 再次重申:引用不是对象,它只是被引对象的另一个名字。 通过引用,我们可以修改原对象的值(除了对 const 常量的引用情形,参考 :ref:`const-reference` 一节): .. code-block:: c++ int val = 100; int &r1 = val; r1 = 3; std::cout << val << std::endl; // val 的值变为 3 .. _const-reference: 常量引用 ^^^^^^^^^^^^^^ .. important:: 常量引用与普通的引用有诸多不同之处,这有助于我们更深入地理解引用。 有时我们称呼“对 const 常量的引用”为“常量引用”,因为这样更简单。由于引用不是对象,常量是修饰被引用对象的(而不是修饰引用名),因此这两种表述实质等同,不涉及歧义。与之不同的是,“指针常量”与“指向常量的指针”则表示的是不同的东西。 常量引用不允许对绑定对象进行修改: .. code-block:: c++ const int cval = 20; int &r2 = cval; r2 = 7; // 错误:不允许修改对常量的引用 此外,常量引用的特殊之处在于,允许使用任意同类型(或能转换为该类型的)表达式作为初始值。而普通引用不允许转换类型,也不允许使用字面值。下例给出了一些正确的常量引用定义: .. code-block:: c++ double val = 1.23; const int &r1 = val; const int &r2 = 127; .. note:: 为什么上述定义对于常量引用是允许的,对于普通的引用就不允许?这是因为上述语句在定义时实际上借助了临时量: .. code-block:: c++ const int temp = val; // 先用临时量对 val 进行类型转换 const int &r1 = temp; // 再将 r1 绑定到临时量 temp 上 同理,常量引用 ``&r2`` 在定义时,也需要对字面值创建临时量。毕竟引用的定义实质上就是对某个对象的绑定。 在常量引用中,由于绑定值不可修改,因此上述定义是允许的。如果非常量引用可以这样定义,那么会导致修改引用实质上修改了临时量的值,而不是原始对象的值;这种逻辑是不合理的,因此不允许这样定义非常量引用。 .. _pointer: 基础指针 ---------- .. warning:: 下面你将认识的是,C++ 世界中最强大的苦痛之源、声名远播到甚至其他编程语言世界的折磨王,指针。请打起精神! .. important:: 为了更安全地管理动态内存,现代 C++ 提倡使用智能指针,参考 :ref:`smart_pointer`\ 。在阅读智能指针相关的内容前,我仍推荐阅读本节了解基础的指针概念。 指针像引用一样提供了对其他对象的访问。但不同的是: * 指针本身是一个对象。指针可以被复制,也可以被更改指向的对象。 * 指针实质上存储了所指向对象的地址。 * 在语法上,指针在定义时可以不显式地指定其初始值。 * 建议总是显式初始化指针,至少指向空指针字面值 ``nullptr``\ |cpp11|\ 。在过去,空指针使用 ``NULL`` 或者 0,现在应当使用 nullptr 代替。 .. danger:: 警惕对无效指针进行操作,这会导致错误。更进一步地,应该在代码中尽可能地消除无效指针的出现。 在定义指针时,使用 ``*`` 标识符前缀: .. code-block:: c++ // 定义整数 m, n 以及一个指向整数的指针 p1 int *p1, m, n; // 定义一个空指针 p2 double *p2 = nullptr; // 定义一个指针 p3, 初始化为指针 p1 指向的对象(p1 中存放的地址) int *p3 = p1; // 注意,此时不需要用 & 对 p1 取地址 .. admonition:: 建议使用 int \*p 而不是 int\* p 有时候我们会看见用 ``int* p;`` 这种书写方式表示定义 int 指针的写法。它把 int 与 \* 写在一起,强调我们正在定义的是一个指针,而不是一个简单的 int 类型变量。 我并不推荐这种写法。在定义时, ``*p`` 应当视为一个整体(变量标识符),表示要定义一个指针,其名为 p。写为 ``int *p`` 可以较少地引起误解。这也可以通过下例说明: .. code-block:: c++ // 等价于:int *p1, p2, p3; int* p1, p2, p3; // 定义了一个指针 p1 与两个整数 p2, p3 即使写成 ``int*``\ ,它们也不能视为一个整体,故也不能认为 p2, p3 被定义为指针。因此,我认为 ``int*`` 这种迷惑性的写法是应当避免的。 下面是一个简单的例子,展示了指针的基本操作: * 指针需要指向一个同类型对象,并利用 **取地址符(**\ ``&``\ **)** 取得对象的地址。 * 在使用指针时,利用 **解引用符(**\ ``*``\ **)** 访问指向的对象。 .. literalinclude:: codes/pointer.cpp :linenos: :language: c++ 在使用指针时,一定不要混淆这两种用法: * 对解引用的指针 ``*p`` 进行赋值,等于对指针 ``p`` 指向的对象赋值,指针仍然指向那个对象。 * 对指针 ``p`` 进行赋值,等于更改指针 ``p`` 中存放的地址,即指针指向了一个新的对象。 .. code-block:: c++ double x = 1.0, y = 9.80665; double *p = nullptr; // 更改 x 为 2.0,指针 p 仍指向 x *p = 2.0; // 让指针 p 指向新对象 y p = &y; 指针的布尔操作 ^^^^^^^^^^^^^^^^ * 当一个指针是空指针时,其逻辑值为 false;否则为 true。 * 如果两个指针均为空指针,或者它们存储了相同地址,那么这两个指针相等;否则它们不相等。 .. literalinclude:: codes/pointer_bool.cpp :linenos: :language: c++ 指针嵌套 ^^^^^^^^^^^^^ 由于指针是对象,因此指针与引用也都可以指向一个指针对象。 .. code-block:: c++ 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 类型的。 .. _pointer-and-const: 指针与 const ^^^^^^^^^^^^^^^^ 指针与 const 有两种组合方式:一是指向常量的指针,二是常量指针。 **指向常量的指针**\ ,比如 ``const int *p``\ ,就像常量引用一样,不能通过它改变指向对象的值。 * 如果要让指针指向一个常量,只能使用指向常量的指针,不能使用常量指针或普通指针。 * 指向常量的指针只是不允许通过该指针更改其指向对象,但指向对象可以是非常量对象。 .. code-block:: c++ const double pi = 3.14; const double *p = π *p = 3.14159; // 错误:不能更改 p 指向的对象 **常量指针**\ ,比如 ``int *const p``\ ,是指指针本身是常量(而不是说指针指向的对象是常量)。 * 常量指针必须显式初始化。 * 常量指针的值(即其存放的地址)一经初始化,就不能改变。 * 与前述的指向常量的指针组合,可以得到指向常量的常量指针(\ ``const int *const p``\ ) .. code-block:: c++ // 常量指针 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``\ ),也就是说它同时是一个指向常量的指针。 .. admonition:: 顶层常量与底层常量 :class: note 像上例中展示的一样,指针的特殊性使得 const 的修饰有时让人迷惑。如果指针本身是一个常量,我们称之为 **顶层常量**\ ,比如常量指针;如果指针的基础类型(指向对象的类型)是一个常量,我们称之为 **底层常量**\ ,比如指向常量的指针。在一个多重 const 修饰的定义中: .. code-block:: c++ const int *const q2 = &n; 第一个 const 表示指向的是一个 int 型常量,因此它对应底层常量;第二个 const 表示要定义的是一个 const 类型的指针,因此它对应顶层常量。 * 顶层常量可以复制到同类型的非常量。 * 底层常量在复制时,要求两个对象具有相同的底层常量类型,或者可以转换。一般非常量可以转换为常量,反之不允许。 .. _void-pointer: void 指针* ^^^^^^^^^^^^^^^^ void 指针是一种特殊指针,它可以存放任意对象的地址。它可以用于不清楚所指对象类型的场合,但是局限性很大。它只能: * 指向任意对象的地址 * 进行布尔操作 * 作为函数输入或输出 * 赋给另一个 void 指针 .. code-block:: c++ double x = 1.2; int n = 10; void *p = &x; *p = &n; 由于不能通过 void 指针更改其指向的对象,我们较少使用这种指针。 .. _smart_pointer: 智能指针 ---------- 智能指针 |cpp11| 模板由标准库 ```` 提供,旨在结束指针的所有使用后自动销毁指向的对象,从而释放内存。该库提供了两种智能指针: * ``std::shared_ptr`` 声明一个共享智能指针。允许与其他指针共享其所指向的、类型为 T 的对象。 * ``std::unique_ptr`` 声明一个独占智能指针。不允许其他指针指向其所指的、类型为 T 的对象。 以及一种比较特殊的弱指针 ``std::weak_ptr``\ 。 共享指针:shared_ptr ^^^^^^^^^^^^^^^^^^^^^^^^ 为 ``shared_ptr`` 分配动态内存时,推荐使用 ``std::make_shared`` 函数,它将在动态内存中分配一个类型为 T 的对象(并用函数接受的参数初始化 T 对象),然后返回一个指向该对象的 ``shared_ptr`` 指针。给该函数传递参数时,参数的形式必须与 T 类型的某一构造函数匹配。 一个使用 ``shared_ptr`` 智能指针的用例: * ``std::make_shared``\ :分配动态内存,常用于初始化智能指针。 * ``sp1.swap(sp2)``\ :交换两个智能指针 sp1 与 sp2 的值。 * ``sp.reset()``\ :让 sp 不再指向原对象。如果无其他共享指针指向该对象,则释放该对象。 * ``sp.unique()``\ :检查 sp 是否是所指对象的唯一用户。 .. literalinclude:: codes/pointer_smart_shared.cpp :linenos: :language: c++ * ``shared_ptr`` 通过引用计数器来控制销毁。当初始化或拷贝智能指针时,计数器会加1;当智能指针被赋予一个新值(令其指向一个新对象),或者离开作用域时,计数器会减1。当计数器归零时,其指向的对象会被释放。 * 如果将在临时容器内存储 ``shared_ptr``\ (比如用于排序),那么在使用后应当用 erase 或者 clear 释放容器以便销毁。 * 类对象有时需要在内部使用其自身的共享指针。这时,我们需要让类继承 ``std::enable_shared_from_this`` 并使用 ``shared_from_this()``\ : .. code:: c++ class Node: public std::enable_shared_from_this { // ... child->parent = shared_from_this(); // ... } 具体的使用,请参考 :ref:`智能指针示例:树节点 ` 的代码。 独占指针:unique_ptr ^^^^^^^^^^^^^^^^^^^^^^^^ ``unique_ptr`` 并不能像 ``shared_ptr`` 一样使用 ``make_shared`` 分配动态内存,而是使用 ``make_unique``\ |cpp14| 代替。 .. literalinclude:: codes/pointer_smart_unique.cpp :linenos: :language: c++ 独占指针也可以使用 ``up1.swap(up2)`` 来交换两个指针的值。 弱指针:weak_ptr ^^^^^^^^^^^^^^^^^^^^^^^^ 弱指针 ``std::weak_ptr`` 是一种有趣而实用的指针,它可以指向一个共享指针 ``std::shared_ptr`` 指向的对象,但不影响其引用计数器。当最后一个指向该对象的共享指针销毁,即使仍然有弱指针指向它,该对象也会被释放。 * 弱指针需要使用共享指针来初始化。 * 弱指针可能指向无效,因此必须在使用时用其 ``.lock()/.expired()`` 成员函数来检查。 * ``expired()``\ :判断弱指针是否有效(指向的对象是否被释放),返回 true 或者 false。 * ``lock()``\ :当弱指针有效时,返回一个指向对象的共享指针;否则,返回一个空的共享指针。 .. admonition:: 将弱指针临时强化为共享指针,再访问其指向的对象 :class: note 对于弱指针 wp,一般我们先用 ``sp = wp.lock()`` 返回共享指针对象 ``sp``,并用 if 语句判断其是否为空。如果非空,那么我们可以操作这个临时的共享指针,例如 ``sp->member()``\ 。 下例展示了弱指针的一些基本用法: .. literalinclude:: codes/pointer_smart_weak.cpp :linenos: :language: c++ 弱指针的一个重要意义是在 **设计从属关系的对象时避免循环引用**\(因为这会阻止智能指针释放对象)。例如,在树的节点定义中,父节点需要一系列指针指向其每个子节点,而子节点也需要一个指针指向其父节点。如果在这两处都使用共享指针,那么父节点与子节点将会相互指向,导致节点在被删除时引用计数总不为零,从而并不能被释放。因此,我们在节点中使用共享指针定义 ``children``\ ,使用弱指针定义 ``parent``\ 。 下例是一个使用弱指针与共享指针,对树节点进行定义的例子: .. _pointer-smart-weak-treenode: .. literalinclude:: codes/pointer_smart_weak_treenode.cpp :linenos: :language: c++ :emphasize-lines: 37-38