本篇以 C++17 为例,分别讨论“如何访问对象”和“由谁负责对象寿命”。裸指针、引用首先表达访问关系,智能指针进一步表达所有权;指向某个地址并不保证该对象仍然存在。
指针、引用与对象寿命
跳转到“指针、引用与对象寿命”int* p = &value; 声明一个指向 int 的指针对象:&value 取得对象地址,*p 在指针有效时访问所指对象。指针可以为空,也可以改为指向别处;未初始化的局部指针不能直接使用。引用 int& r = value; 则绑定到已有对象,之后 r = other; 是给被引用对象赋值,并不会让 r 改绑到 other。

原图中 pointer_1 指向值为 100 的整数。图中的位置只帮助理解引用关系,不表示语言规定了具体地址。
| 特性 | 指针 T* | 左值引用 T& |
|---|---|---|
| 初始化 | 推荐初始化为有效地址或 nullptr | 定义时通常必须绑定对象 |
| 改变目标 | 可重新赋值 | 不能重新绑定 |
| 表达“没有对象” | 用空指针 | 没有合法的空引用 |
| 访问成员 | p->member | r.member |
| 对象寿命 | 不自动延长被指对象寿命 | 普通对象引用不自动延长被引用对象寿命 |
引用在语言层面是别名;它是否需要额外存储由实现和使用场景决定,不能据此保证“引用永远不占内存”。局部名字离开作用域,也不等于其指向的对象必然销毁;反过来,一个仍在作用域内的指针也可能已经悬空。引用规则、对象寿命
#include <cassert>#include <memory>
int createValue(int value) { return value; }std::unique_ptr<int> createOwnedValue(int value) { return std::make_unique<int>(value);}
int main() { int a = 100; int b = 7; int* p = &a; int& r = a; r = b; // 修改 a,并未重新绑定 r assert(a == 7 && &r == &a); p = &b; // 指针可以改指向 *p = 9; assert(b == 9 && a == 7); assert(createValue(12) == 12); auto owned = createOwnedValue(23); assert(*owned == 23);}不要返回局部变量或按值形参的地址。 例如下面的错误例子返回时 value 的寿命已经结束,后续解引用是未定义行为,不是“返回一个新整数”:
// 错误示范,不要使用;编译器可能以警告指出问题。int* dangling(int value) { return &value; }返回小对象优先按值;需要动态所有权时返回 std::unique_ptr;返回借用指针或引用时必须说明被借用对象由谁持有、何时失效。const T* 禁止经此指针修改对象,T* const 禁止改变指针自身,const T* const 同时具有两种限制。
函数指针与返回指针的函数
跳转到“函数指针与返回指针的函数”括号决定声明的结合方式:int (*f)(int, int) 是指向函数的指针,int* f(int) 是返回 int* 的函数。函数指针既可以写 f(2, 3),也可以写 (*f)(2, 3);“函数指针不能解引用”是错误的。
#include <cassert>
int add(int x, int y) { return x + y; }int* select(int& x) { return &x; } // 借用:x 必须比返回指针的使用期长
struct Calculator { int scale(int x) const { return 2 * x; }};
int main() { using Binary = int (*)(int, int); Binary f = &add; assert(f(2, 3) == 5 && (*f)(2, 3) == 5); int value = 8; assert(select(value) == &value); Calculator calculator; int (Calculator::*method)(int) const = &Calculator::scale; assert((calculator.*method)(4) == 8);}非静态成员函数指针需要一个对象才能调用,不能直接当成普通函数指针。静态成员函数没有隐含的 this,可以在签名匹配时转换为普通函数指针。
原笔记中 CAMERAThread 调用 YOLOWINDOW 方法的问题还涉及线程:通过函数指针、成员函数指针或普通方法调用,都在发起调用的线程中执行,不会因接收对象属于 GUI 线程而自动切换。工作线程请求修改界面,应使用带接收对象上下文的队列连接等方式,参见 Qt 信号连接的线程规则。
指针数组、数组指针与二维数组
跳转到“指针数组、数组指针与二维数组”int* list[3] 是含三个指针的数组;int (*row)[4] 是指向“含四个整数的数组”的指针。二维数组 int a[3][4] 的元素是四元素数组,所以它在通常表达式中转换为 int (*)[4],不是 int**。

原图用整数 0 到 11 展示行优先布局,并假定该平台 sizeof(int) == 4。应通过 sizeof 推导偏移,不要把图中地址或四字节整数当成跨平台保证。
| 表达式 | 类型或通常转换后的类型 | 含义 |
|---|---|---|
a | 数组类型为 int[3][4],通常转换为 int (*)[4] | 第一行地址 |
a + 1 | int (*)[4] | 第二行地址,步长 4 * sizeof(int) |
a[1] | 数组类型为 int[4],通常转换为 int* | 第二行首元素地址 |
a[1] + 2 | int* | 第二行第三个元素地址 |
&a[1][2] | int* | 同一元素地址 |
a[1][2] / *(*(a + 1) + 2) | int 左值 | 值为 6 的元素 |
&a | int (*)[3][4] | 整个二维数组的地址,步长 sizeof(a) |
#include <cassert>#include <type_traits>
int main() { int a[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}; int (*row)[4] = a; int* list[3] = {a[0], a[1], a[2]}; static_assert(std::is_same_v<decltype(a), int[3][4]>); static_assert(std::is_same_v<decltype(&a), int (*)[3][4]>); static_assert(std::is_same_v<decltype(row), int (*)[4]>); assert(row + 1 == &a[1]); assert(*(row + 1) + 2 == &a[1][2]); assert(*(*(row + 1) + 2) == 6); assert(list[2][3] == 11); assert(sizeof(a) == 12 * sizeof(int));}指针运算必须受所属数组边界约束;可形成尾后指针,但不能解引用它。不要用越界指针算术“跨过一行”,应通过合法的行索引访问下一行。指针加减规则
智能指针:所有权与借用
跳转到“智能指针:所有权与借用”| 操作/性质 | std::unique_ptr<T> | std::shared_ptr<T> | std::weak_ptr<T> |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 不延长对象寿命的观察关系 |
| 复制 | 禁止 | 允许,共享同一控制块 | 允许 |
移动、reset、swap | 支持 | 支持 | 支持 |
| 访问对象 | *、->、get() | *、->、get() | 先 lock() 得到可能为空的 shared_ptr |
release() | 支持,交出裸指针且不删除对象 | 没有 | 没有 |
use_count() | 没有 | 支持,仅观察当时的共享所有者数量 | 支持 |
expired() | 没有 | 没有 | 支持;访问时仍应直接检查 lock() 的结果 |
data() | 没有 | 没有 | 没有 |
下面的示例验证移动、共享和弱观察的寿命变化:
#include <cassert>#include <memory>#include <utility>
int main() { auto first = std::make_unique<int>(10); int* borrowed = first.get(); auto second = std::move(first); assert(!first && second.get() == borrowed && *borrowed == 10);
// 接受 release 的指针后立即交给另一个 owner,避免丢失所有权。 std::unique_ptr<int> third(second.release()); assert(!second && *third == 10);
std::weak_ptr<int> observer; { auto owner = std::make_shared<int>(20); auto shared = owner; observer = owner; assert(owner.use_count() == 2); if (auto locked = observer.lock()) { assert(*locked == 20); } shared.reset(); assert(owner.use_count() == 1); } assert(observer.expired() && !observer.lock()); auto copy = observer; auto moved = std::move(copy); observer.swap(moved); observer.reset();}get() 只返回借用指针,不转移所有权。不要用同一个裸指针分别构造两个 unique_ptr,也不要从 shared_ptr::get() 再构造一个独立 shared_ptr;这样会建立相互不知情的所有权,可能重复删除。共享所有权应复制已有 shared_ptr。
shared_ptr 允许不同指针对象并发操作同一控制块,但不会自动保护所指对象的数据,也不意味着同一个 shared_ptr 变量可以无同步地读写。use_count() == 1 也不能用作可靠的跨线程互斥条件。互相强持有的对象可能形成循环,适合借用的一侧可用 weak_ptr 打破循环。shared_ptr 规范、weak_ptr 规范
字符串指针与地址输出
跳转到“字符串指针与地址输出”C++ 中普通字符串字面量的类型是 const char[N],const char* p = "hello"; 合法,char* p = "hello"; 不是合法的标准 C++17 初始化。要修改文本,使用自己的字符数组或 std::string。C 语言字面量类型规则不同,但修改字符串字面量同样是未定义行为。字符串字面量
#include <cassert>#include <iostream>
int main() { const char* p = "hello"; const char* before = p; std::cout << p << '\n'; // 字符序列 std::cout << static_cast<const void*>(p) << '\n'; // 所指字符的地址 std::cout << &p << '\n'; // 指针变量自身的地址 assert(p == before); // 输出不会推进调用者的 p char text[] = "hello"; text[0] = 'H'; assert(text[0] == 'H');}std::cout 对字符指针有输出字符串的重载;printf("%s", p) 也读取以空字符结尾的序列,而不是修改调用者的指针变量。指针必须指向有效且有终止符的序列,否则读取可能越界。
最后区分三个常见 &:声明中的 T& 表示引用,一元 &x 取地址,二元 a & b 是按位与。逻辑与是 a && b,只有它具有短路求值;把布尔条件中的 && 换成 & 会让两侧都求值。
通过基类引用调用虚函数的规则见 继承与多态。引用绑定不复制对象,因此它也不会造成按值复制产生的对象切片。