跳转到内容
新建笔记

C++ 指针与引用:类型、寿命、数组和智能指针

本篇以 C++17 为例,分别讨论“如何访问对象”和“由谁负责对象寿命”。裸指针、引用首先表达访问关系,智能指针进一步表达所有权;指向某个地址并不保证该对象仍然存在。

指针、引用与对象寿命

跳转到“指针、引用与对象寿命”

int* p = &value; 声明一个指向 int 的指针对象:&value 取得对象地址,*p 在指针有效时访问所指对象。指针可以为空,也可以改为指向别处;未初始化的局部指针不能直接使用。引用 int& r = value; 则绑定到已有对象,之后 r = other; 是给被引用对象赋值,并不会让 r 改绑到 other。

指针变量保存对象地址,解引用得到该对象

原图中 pointer_1 指向值为 100 的整数。图中的位置只帮助理解引用关系,不表示语言规定了具体地址。

特性指针 T*左值引用 T&
初始化推荐初始化为有效地址或 nullptr定义时通常必须绑定对象
改变目标可重新赋值不能重新绑定
表达“没有对象”用空指针没有合法的空引用
访问成员p->memberr.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 + 1int (*)[4]第二行地址,步长 4 * sizeof(int)
a[1]数组类型为 int[4],通常转换为 int*第二行首元素地址
a[1] + 2int*第二行第三个元素地址
&a[1][2]int*同一元素地址
a[1][2] / *(*(a + 1) + 2)int 左值值为 6 的元素
&aint (*)[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,只有它具有短路求值;把布尔条件中的 && 换成 & 会让两侧都求值。

通过基类引用调用虚函数的规则见 继承与多态。引用绑定不复制对象,因此它也不会造成按值复制产生的对象切片。