C++ 基础:(2) 引用与指针、右值引用、通用引用与智能指针(unique_ptr、shared_ptr 与 weak_ptr)
本文系统梳理了C++中引用和指针的核心概念与应用。引用部分详细解析了左值、纯右值与将亡值的分类特点,阐述了引用在函数参数传递、返回值以及复制构造函数中的使用规范与底层机制,重点介绍了右值引用、std::move和移动语义的实现原理及其性能优化价值。指针部分剖析了裸指针的安全风险,对比了智能指针(unique_ptr、shared_ptr、weak_ptr)的RAII实现机制,包括独占所有权、引用计
1. 引用
1.1 左值、纯右值与将亡值
|
类别 |
全称 |
中文 |
特点 |
底层存储位置(典型情况) |
|---|---|---|---|---|
|
lvalue |
left-value |
左值 |
有身份(identity),不可移动(通常) |
栈(局部变量)、堆(动态分配)、全局/静态区(全局或 static 变量) —— 有确定、持久的内存地址 |
|
rvalue |
right-value |
右值 |
可移动(movable) |
(右值是 prvalue 和 xvalue 的统称) |
|
└─prvalue |
pure rvalue |
纯右值 |
无身份(如字面量、临时对象) |
寄存器(小类型如 int/double) 或 调用者栈帧中的临时对象区域(经 RVO/NRVO 优化后);生命周期极短,可能根本不在内存中 |
|
└─xvalue |
expiring value |
将亡值 |
有身份 + 可移动 |
与原始对象相同的位置(如栈、堆、全局区)—— 它不是新对象,只是对已有对象的“可移动视图” |
1.2 & 用作函数参数
普通按值传参使得函数调用值的拷贝,而按引用传参可直接访问该变量。
所谓“按引用传参”,底层本质是传递地址的值,然后通过该地址间接访问原始内存。
例:
void foo(int &x) {
x = 100;
}
int main() {
int a = 5;
foo(a);
// a == 100
}
- 编译器实际上把 int &x 编译成一个常量指针(int * const x)。
- 调用时自动传 &a,函数内使用 x 时自动解引用。
- 所以,引用在底层仍然是通过地址访问,但由编译器自动处理,避免显式 * 和 &。
1.3 & 用作函数返回值
返回引用不可以返回临时变量(作用域在函数内的),可以将一个形参作为返回值,或使用new开辟一段内存空间。
函数的返回值一般为右值(临时变量,位于临时内存单元,执行下一条语句可能就会消失,不可取地址),而返回引用为左值(除了右值引用&&),如果函数声明为:
int& Function (int a, int b);
使得在编写下面语句时不会报错:
Function(a,b)= 2;
故需要使函数返回值为 const &(为不可更改的左值,不可被赋值),避免上述错误,正确编写:
const int& Function(int& a, int& b);
返回的引用必须是函数中作为参数的引用,否则便是返回局部变量。
即:
int& identity(int& x) {
return x; // ✅ 安全!x 是调用者传进来的对象的引用
}
int main() {
int a = 10;
int& r = identity(a); // r 就是 a 的别名
r = 20; // 修改 a
}
例:
int global = 100;
int& getRef() {
return global; // 返回一个已有变量的引用
}
int main() {
getRef() = 200; // 合法!因为返回的是左值
int* p = &getRef(); // 合法!可以取地址
}
1.4 & 用作复制构造函数
复制构造函数的形参必须是引用,否则会无限递归,如:
Myclass(const Myclass);
这里形参不是引用,在传实参 myClass1 时,按值传递会进行复制,myClass1 被复制时又会调用拷贝构造函数,再次传参从而复制从而调用拷贝构造函数,造成没有终止条件的无穷递归。
1.5 右值引用
1.5.1 std::move
int a = 10;
int& ra = a; // 左值引用 引用 左值
int&& rr1 = 10; // 右值引用 引用 右值,延长了其生命周期(直到 rr1 作用域结束)
int&& rr2 = std::move(rr1); // 资源所有权转移,rr1是左值,std::move将其转化为将亡值
- 右值引用是个类型,右值引用类型的变量才分左右值,这里rr1是个右值引用类型的变量,为左值。
- std::move 的本质是将一个对象转换为右值引用,允许资源被转移而不是拷贝,提高性能。
- std::move 不移动任何东西,它只是“类型转换”;真正的移动发生在移动构造函数或移动赋值运算符中。
- 无论是左值引用还是右值引用,本质上都是在给这一块内存起别名,当右值引用引用右值时,则是对该内存的所有权进行转移,原来右值的资源(如vector和类所持有的内存块的指针,动态分配的内存)等处于一个未定义的状态(通常是nullptr,但也可以是其他值,具体取决于实现)。如:
std::string a = "hello world"; // a 拥有堆上的字符数据
std::string b = std::move(a);
// 即:
std::string b(std::string&& other); // 移动构造函数被调用
// 移动构造函数的实现:
std::string::string(std::string&& other) {
ptr = other.ptr; // 1. 把 other 的指针“拿过来”
size = other.size;
capacity = other.capacity;
other.ptr = nullptr; // 2. 把 other 置为空,不再拥有资源
other.size = 0;
other.capacity = 0;
}
原来的对象仍然在内存中存在, 只是其所拥有的指针和值等被置空.
对const对象无效,移动会变为拷贝:
const std::string str = "Hello";
std::string newStr = std::move(str); // 实际上调用了拷贝构造函数
常用于容器中的元素移动:
#include <iostream>
#include <vector>
#include <string>
#include <utility>
int main() {
std::vector<std::string> vec;
std::string str = "Hello, World!";
vec.push_back(std::move(str)); // 将 str 的内容移动到 vec 中
std::cout << "vec[0]: " << vec[0] << "\n"; // 输出 "Hello, World!"
std::cout << "str: " << str << "\n"; // 输出空字符串(资源已被移动)
return 0;
}
在使用std::move前,应确原对象不会再次被使用。
1.5.2 移动构造函数
#include <iostream>
#include <utility> // For std::move
class Resource {
private:
int* data; // 指向动态分配的资源
public:
// 构造函数
Resource(int size) : data(new int[size]) { };
// 移动构造函数
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 析构函数
~Resource() {
delete[] data; // 释放资源
}
// 禁止复制构造函数和复制赋值运算符
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
// 移动赋值运算符
Resource& operator=(Resource&& other) noexcept {
delete[] data; // 释放当前资源
data = other.data;
other.data = nullptr; // 将资源指针设置为nullptr
return *this;
}
};
int main() {
Resource r1(10); // 创建Resource对象r1
Resource r2 = std::move(r1); // 将r1移动到r2
// 此时,r1处于未定义状态,r2接管了r1的资源
// 下面的代码是危险的,因为它试图访问r1的资源,而r1的资源已经被移动
// std::cout << *r1.data << std::endl; // 未定义行为
// 正确做法是不再使用r1,因为它已经没有资源了
// r2将继续存在,直到其生命周期结束,此时将释放资源
return 0;
}
这里的未定义状态是在移动构造函数或移动赋值运算符中实现的。
1.5.3 &&用作函数返回值
语义:返回一个 将亡值(xvalue)
std::string global = "hello";
std::string&& getRvalueRef() {
return std::move(global); // 返回 global 的右值引用
}
std::string s = getRvalueRef(); // 调用移动构造函数(高效)
1.6 通用引用
如果 T&& 出现在模板中,并且 T 是通过类型推导确定的,则 T&& 是通用引用 。
template <typename T>
void foo(T&& param); // 通用引用
foo(42); // T 推导为 int,为通用引用
foo<int>(42); // 显式指定 T,此时 T&& 是普通右值引用
用于实现完美转发:
- 如果传入的是右值,T&& 表示右值引用。
- 如果传入的是左值,T&& 表示左值引用。
- 常与std::forward一起使用,如std::forward<int>(x)。
2. 指针
2.1 指针的风险
2.1.1 声明而不开辟
int* a;
*a = 1;
- 执行 int* a; 创建指针时,内存中只开辟了a的内存块,而a的值未知即 a 这个内存块的值未知,未开辟 a 指向的内存块,执行 *a = 1 时,如果 a 碰巧=100,则地址为100的内存块中就会存储为1,即使该内存块正在被其他地方使用,这可能导致bug。
- 故在声明指针而不赋值时,应使用new开辟指针所指向的内存块,即又有 a 的内存块,又有 a 指向的地址的内存块,使用完后使用delete释放内存,但忘记使用delete或出现异常未执行delete会导致内存泄露。
- 但如果只对a操作,而不操作内存块,则无风险。
2.1.2 返回局部变量的地址
局部变量存储在栈上,函数返回时,局部变量的生命周期结束,这个地址指向的局部变量(栈上的内存)可能已经不再属于原来的变量。
2.2 数组名
数组名作为形参传递给函数时,会退化为指针,此时使用sizeof(数组名)(sizeof 是编译期运算符,不是函数)会得到指针大小而不是数组大小。
2.3 成员函数名
静态成员函数名即为地址,如:
静态成员函数名即为地址,如:
MyClass::StaticMemberFunction
非静态成员函数名包含隐式的this指针,若获取指针,使用&,如:
&MyClass::MemberFunction
2.4 智能指针
2.4.1 unique_ptr
std::unique_ptr 是基于 RAII(资源获取即初始化) 和 移动语义 实现的独占式智能指针。它内部封装了一个原始指针和一个可配置的删除器(Deleter),默认使用 delete 或 delete[](数组特化)。它通过删除拷贝构造函数和拷贝赋值运算符,禁止了共享行为,确保同一时刻只有一个 unique_ptr 拥有资源。资源的释放发生在其析构函数中,自动调用删除器,实现异常安全的资源管理。所有操作在编译期确定,无运行时开销,性能几乎与裸指针一致。支持自定义删除器(如 fclose、CloseHandle),可用于管理非内存资源。
不允许被赋值,除非源unique_ptr是个临时的右值:
std::unique_ptr<int> Function(int);
std::unique_ptr<int> a;
a = Function(1);
可以移动,ptr1 的所有权被移动到了 ptr2,ptr1 变成了一个空的智能指针,不会释放任何资源:
std::unique_ptr<int> ptr1(new int);
std::unique_ptr<int> ptr2 = std::move(ptr1);
2.2.2 shared_ptr
std::shared_ptr 实现共享所有权核心机制是引用计数。它内部包含两个指针:一个指向管理的对象,另一个指向控制块。
控制块中包含:
- 强引用计数(shared_ptr 的数量)
- 弱引用计数(weak_ptr 的数量)
- 自定义删除器
- 分配器
- 可能的对象指针(make_shared 优化时)
每次 shared_ptr 被拷贝,强引用计数原子递增;析构时递减。当强引用计数归零时,调用删除器释放对象。控制块本身在最后一个 shared_ptr 和 weak_ptr 都释放后才被销毁。引用计数的操作是 原子的,因此多个线程同时拷贝或析构不同的 shared_ptr 实例是线程安全的(但操作同一个实例仍需加锁)。
make_shared 合并对象与控制块的内存分配,减少开销、提升性能和异常安全性。
多个 std::shared_ptr 可以共享同一个对象的所有权,当所有 std::shared_ptr 都不再引用该对象时,对象会被自动释放。shared_ptr 的循环引用会导致内存泄漏,两个对象互相持有对方的 shared_ptr,且main函数中又有两个对象的shared_ptr,它们的引用计数都为 2(一个来自 main,一个来自对方),退出作用域后仍为 1,无法归零。
2.4.3 weak_ptr
std::weak_ptr 是 shared_ptr 的观察者,用于解决循环引用问题。它不增加强引用计数,只增加控制块中的弱引用计数。weak_ptr 不能直接访问所指对象,必须通过 lock() 方法尝试提升为 shared_ptr。lock() 会检查强引用计数:
- 如果 > 0,返回一个有效的 shared_ptr(强引用 +1)
- 如果为 0(对象已销毁),返回空 shared_ptr
这种机制允许安全地检查对象是否还存在,而不会延长其生命周期。
控制块的销毁时机:当强引用计数为 0 且弱引用计数也为 0 时,控制块本身被释放。这意味着即使对象已销毁,只要还有 weak_ptr 存在,控制块仍保留,以便 lock() 能正确判断状态。
例:两个对象互相持有 shared_ptr,导致内存泄漏(循环引用)
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // 循环引用
~B() { std:: std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B
b->a_ptr = a; // B 持有 A → 循环!
std::cout << "a.use_count(): " << a.use_count() << "\n"; // 输出 2
std::cout << "b.use_count(): " << b.use_count() << "\n"; // 输出 2
return 0;
// 程序结束,但 A 和 B 都不会被销毁!因为彼此的 shared_ptr 使引用计数 ≥1
}
输出:
a.use_count(): 2
b.use_count(): 2
// 没有 "A destroyed" 或 "B destroyed"!内存泄漏!
解决方案:将其中一个指针改为 weak_ptr
通常在“观察者”或“从属”关系中使用 weak_ptr。比如 B 是 A 的附属,那么 B 不应拥有 A。
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为 weak_ptr!不再增加强引用计数
~B() { std::cout << "B destroyed\n"; }
void doSomething() {
// 安全地访问 A(如果还存在)
if (auto locked = a_ptr.lock()) {
std::cout << "A is still alive!\n";
} else {
std::cout << "A has been destroyed.\n";
}
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr 赋值,不增加 a 的强引用计数
std::cout << "a.use_count(): " << a.use_count() << "\n"; // 1(只有 main 中的 a)
std::cout << "b.use_count(): " << b.use_count() << "\n"; // 2(main + a->b_ptr)
b->doSomething(); // 输出: A is still alive!
return 0;
// 函数结束:
// - a 被销毁 → A 对象析构
// - b 被销毁 → B 对象析构
}
- shared_ptr 和 weak_ptr 共享同一个控制块(包含强引用计数、弱引用计数、删除器等)。
- 对象销毁时机:强引用计数 == 0 → 调用析构函数,释放对象内存。
- 控制块销毁时机:强引用计数 == 0 且 弱引用计数 == 0 → 释放控制块本身。
- 因此,只要还有 weak_ptr 存在,控制块就保留,使得 lock() 能正确返回空 shared_ptr。
更多推荐



所有评论(0)