C++智能指针与C++核心特性详解
引言
C++作为一种系统级编程语言,手动管理内存是其核心特性之一。然而,手动管理内存也带来了两个经典问题:
内存泄漏:程序员忘记释放动态分配的内存,导致程序运行期间不断消耗系统资源。
共享所有权问题:当多个代码路径需要访问同一个对象时,如何确保对象在所有使用者都不再需要时才释放?
C++11之前,程序员只能依靠纪律和代码规范来避免这些问题。C++11引入的智能指针则从语言层面提供了自动、可靠的内存管理方案。
一、智能指针概述
1.1 四种智能指针对比
| 指针类型 | C++标准 | 所有权模式 | 能否复制 | 能否移动 | 典型场景 |
|---|---|---|---|---|---|
| auto_ptr | C++98 | 独占转移 | 不能(转移后原指针失效) | 可以 | 已废弃,不推荐使用 |
| unique_ptr | C++11 | 独占拥有 | 不能 | 可以 | 独占资源,一个对象只被一个拥有者管理 |
| shared_ptr | C++11 | 共享引用 | 可以 | 可以 | 共享所有权,多个对象共享同一资源 |
| weak_ptr | C++11 | 非拥有观察 | 可以(从shared_ptr构造) | 可以 | 打破循环引用,或临时观察shared_ptr资源 |
1.2 基本原理
智能指针的核心思想很简单:把指针封装成一个栈上对象,当栈对象析构时自动释放堆内存。
栈上对象生命周期结束时自动析构
|
v
调用delete释放堆内存
这利用了C++的基本规则:局部对象(栈对象)在离开作用域时自动析构。
二、shared_ptr详解
2.1 基本结构
shared_ptr的实现包含两部分:
- 指向堆对象的裸指针(raw pointer):指向用户实际分配的对象
- 共享控制块(control block):包含引用计数器和删除器
shared_ptr对象
+------------------+
| raw_ptr | -----> 堆上的T对象
| control_block | -----> +------------------+
+------------------+ | reference_count |
| deleter |
| weak_count |
+------------------+
2.2 创建方式
// 方式1:直接构造
std::shared_ptr<int> sp1(new int(100));
// 方式2:使用make_shared(推荐,更高效)
std::shared_ptr<int> sp2 = std::make_shared<int>(100);
// 方式3:指定删除器
void DeleteIntPtr(int* p) {
std::cout << "custom deleter called" << std::endl;
delete p;
}
std::shared_ptr<int> sp3(new int(200), DeleteIntPtr);
重要原则:永远不要把同一个裸指针同时赋给多个shared_ptr。
// 错误示例
int* raw = new int(100);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw); // 危险!sp1和sp2各自认为自己是唯一拥有者
// 当sp1析构时delete raw,sp2就成为了野指针
// 正确做法
std::shared_ptr<int> sp1(new int(100));
std::shared_ptr<int> sp2 = sp1; // 正确,引用计数变为2
2.3 线程安全问题
shared_ptr本身的引用计数是线程安全的,但指向的对象数据修改不是线程安全的。
reference_count:原子操作,多线程安全
T对象数据修改:需要额外加锁保护
std::shared_ptr<int> sp = std::make_shared<int>(100);
// 安全:多个线程同时读写sp本身
// 不安全:多个线程同时通过sp修改int值,需要加锁
2.4 常用操作
std::shared_ptr<int> sp = std::make_shared<int>(100);
// 获取原始指针(慎用!)
int* raw = sp.get(); // 可能导致二次释放
// 重置shared_ptr
sp.reset(); // 释放资源,sp变为空
sp.reset(new int(200)); // 重新绑定到新对象
// 获取引用计数
std::cout << sp.use_count() << std::endl; // 输出1
// 检查是否为空
if (sp) {
std::cout << "sp is valid" << std::endl;
}
2.5 enable_shared_from_this
当你需要在对象内部获取一个shared_ptr指向自身时,不能直接用this构造shared_ptr,而应该让类继承std::enable_shared_from_this。
class Node : public std::enable_shared_from_this<Node> {
public:
std::shared_ptr<Node> getShared() {
return shared_from_this(); // 返回指向自身的shared_ptr
}
};
// 正确用法
std::shared_ptr<Node> node = std::make_shared<Node>();
std::shared_ptr<Node> node2 = node->getShared(); // 引用计数变为2
// 错误用法(会导至双重析构)
Node* raw = new Node();
std::shared_ptr<Node> sp1(raw);
std::shared_ptr<Node> sp2(raw); // 两次析构同一对象
2.6 循环引用问题
循环引用会导致两个对象都无法释放。
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
};
class B {
public:
std::shared_ptr<A> a_ptr;
};
void testCycleRef() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // B引用计数变为1
b->a_ptr = a; // A引用计数变为1
// 离开作用域时:
// a引用计数从1减为0,析构A,但B的引用计数不能减为0(A还持有B的shared_ptr)
// b引用计数从1减为0,析构B,但A的引用计数不能减为0(B还持有A的shared_ptr)
// 结果:两个对象都无法释放,内存泄漏
}
三、weak_ptr详解
3.1 概念
weak_ptr是一种不控制对象生命周期的智能指针。它指向一个shared_ptr管理的对象,但不影响该对象的引用计数。
shared_ptr: 强引用,影响对象生命周期
weak_ptr: 弱引用,不影响对象生命周期
3.2 主要用途
用途一:打破循环引用
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // 换成weak_ptr即可解决循环引用
};
class B {
public:
std::shared_ptr<A> a_ptr;
};
用途二:临时观察对象
class Observer {
std::weak_ptr<Data> data_ptr; // 观察但不拥有
public:
void observe(const std::shared_ptr<Data>& d) {
data_ptr = d;
}
void process() {
// 检查资源是否仍存在
if (auto sp = data_ptr.lock()) { // lock()返回shared_ptr或空
// 数据仍然有效,可以安全使用
sp->doSomething();
} else {
// 数据已释放
std::cout << "data already released" << std::endl;
}
}
};
3.3 常用方法
std::shared_ptr<int> sp = std::make_shared<int>(100);
std::weak_ptr<int> wp = sp;
// 检查资源是否已释放
if (wp.expired()) { // 等价于 use_count() == 0
std::cout << "resource released" << std::endl;
}
// 获取shared_ptr(如果资源已释放,返回空的shared_ptr)
if (auto sp2 = wp.lock()) {
std::cout << "resource valid, value = " << *sp2 << std::endl;
} else {
std::cout << "resource already released" << std::endl;
}
// 获取引用计数(不常用)
std::cout << wp.use_count() << std::endl; // 1
四、unique_ptr详解
4.1 独占特性
unique_ptr拥有独占的所有权,不能复制,只能移动。
std::unique_ptr<int> up1(new int(100));
// 不能复制,编译失败
// std::unique_ptr<int> up2 = up1; // 错误
// 可以移动
std::unique_ptr<int> up2 = std::move(up1); // up1变为空,up2拥有资源
// 可以作为函数返回值
std::unique_ptr<int> create() {
return std::unique_ptr<int>(new int(200));
}
4.2 自定义删除器
void FileDeleter(FILE* f) {
if (f) fclose(f);
}
std::unique_ptr<FILE, decltype(&FileDeleter)>
file_ptr(fopen("test.txt", "r"), FileDeleter);
4.3 与shared_ptr的关系
std::unique_ptr<int> up(new int(100));
// 可以转换为shared_ptr
std::shared_ptr<int> sp = std::move(up); // up变为空
但反向转换不行:shared_ptr不能赋给unique_ptr。
五、右值引用与移动语义
5.1 左值与右值
左值(lvalue):可以取地址、位于等号左边的表达式。
int a = 5; // a是左值,可以取地址
int& ref_a = a; // 正确,左值引用指向左值
右值(rvalue):不能取地址、位于等号右边的表达式。
int& ref_wrong = 5; // 错误,不能用左值引用指向右值
const左值引用可以指向右值(这是C++98就有的特性):
const int& ref = 6; // 正确,const引用延长右值生命周期
5.2 右值引用
C++11引入右值引用,使用&&语法:
int a = 5;
int&& rref = 5; // 正确,右值引用指向右值
// 使用std::move将左值转为右值
int&& rref2 = std::move(a); // 正确,move后a的内容可以"移动"给其他对象
5.3 移动语义
移动语义的核心是避免不必要的内存拷贝。当你"移动"一个对象时,实际上是把资源的所有权转移,而不是复制。
class Buffer {
private:
char* data;
size_t size;
public:
// 拷贝构造函数(深拷贝)
Buffer(const Buffer& other) : size(other.size) {
data = new char[size];
memcpy(data, other.data, size);
}
// 移动构造函数
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // 原对象不再拥有资源
other.size = 0;
}
};
// 使用效果
Buffer b1(1000000); // 分配大内存
Buffer b2 = b1; // 调用拷贝构造函数,深拷贝1MB数据
Buffer b3 = std::move(b1); // 调用移动构造函数,只需移动指针,不拷贝数据
// 注意:移动后b1.data变为nullptr,使用b1会导致未定义行为
5.4 std::move的作用
std::move只是一个类型转换工具,它本身不移动任何东西:
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
std::move的作用是将左值"转换"为右值引用,从而触发移动语义。
5.5 完美转发
std::forward可以在函数模板中保持参数的原始值类别(左值或右值):
void process(int&) { std::cout << "lvalue" << std::endl; }
void process(int&&) { std::cout << "rvalue" << std::endl; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 保持原始值类别
}
int a = 5;
wrapper(a); // 调用lvalue版本
wrapper(5); // 调用rvalue版本
5.6 emplace系列函数
emplace操作可以直接在容器中构造对象,避免不必要的拷贝和移动:
std::vector<std::string> v;
// push_back需要先构造对象,再拷贝/移动
v.push_back(std::string("hello")); // 先构造临时string,再移动
// emplace直接在容器内构造
v.emplace_back("hello"); // 直接在vector内构造string,避免额外操作
// 结合move进一步优化
std::string s = getLargeString();
v.emplace_back(std::move(s)); // 移动而非拷贝
六、lambda表达式
6.1 基本语法
[捕获列表](参数列表) mutable(可选)异常属性 -> 返回类型 {
// 函数体
}
6.2 捕获方式
int x = 10;
int y = 20;
// 按值捕获
auto f1 = [x](int a) { return a + x; }; // 拷贝x的值
// 按引用捕获(可修改外部变量)
auto f2 = [&x, y](int a) mutable {
x = 100; // 可以修改x
return a + x + y;
};
// 隐式捕获
auto f3 = [=]() { return x + y; }; // 按值捕获所有外部变量
auto f4 = [&]() { return x + y; }; // 按引用捕获所有外部变量
auto f5 = [&, y]() { return x + y; }; // 除y按值外,其他按引用
auto f6 = [=, &x]() { return x + y; }; // 除x按引用外,其他按值
6.3 示例
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 使用lambda排序
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b; // 降序排列
});
// 使用lambda查找
auto it = std::find_if(v.begin(), v.end(), [](int n) {
return n > 5;
});
// 完整lambda语法示例
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(3, 4) << std::endl; // 输出7
return 0;
}
6.4 泛型lambda(C++14)
auto generic = [](auto x, auto y) {
return x + y;
};
int a = generic(1, 2); // int
std::string s = generic(std::string("hello"), std::string(" world")); // string
七、综合示例
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class DataBuffer {
private:
std::vector<int> data;
public:
DataBuffer(size_t size) : data(size, 0) {
std::cout << "constructor called, size=" << size << std::endl;
}
~DataBuffer() {
std::cout << "destructor called" << std::endl;
}
void fill(int value) {
std::fill(data.begin(), data.end(), value);
}
size_t size() const { return data.size(); }
};
int main() {
// shared_ptr示例
std::cout << "=== shared_ptr示例 ===" << std::endl;
{
std::shared_ptr<DataBuffer> sp1 = std::make_shared<DataBuffer>(100);
std::shared_ptr<DataBuffer> sp2 = sp1;
std::cout << "use_count: " << sp1.use_count() << std::endl; // 2
sp2->fill(42);
std::cout << "sp1->size(): " << sp1->size() << std::endl; // 100
sp1.reset(); // 引用计数变为1
std::cout << "after reset, use_count: " << sp2.use_count() << std::endl; // 1
} // sp2析构,DataBuffer最终释放
// unique_ptr示例
std::cout << "\n=== unique_ptr示例 ===" << std::endl;
{
std::unique_ptr<int> up1(new int(100));
std::unique_ptr<int> up2 = std::move(up1);
std::cout << "*up2: " << *up2 << std::endl; // 100
// std::cout << *up1 << std::endl; // 未定义行为,up1已为空
// 可以转换为shared_ptr
std::shared_ptr<int> sp = std::move(up2);
std::cout << "*sp: " << *sp << std::endl; // 100
}
// 移动语义示例
std::cout << "\n=== 移动语义示例 ===" << std::endl;
{
std::vector<std::string> v;
std::string s = "hello world"; // 长字符串
v.push_back(s); // 拷贝,s不变
v.push_back(std::move(s)); // 移动,s变为空
std::cout << "v[0]: " << v[0] << std::endl;
std::cout << "v[1]: " << v[1] << std::endl;
std::cout << "s after move: \"" << s << "\"" << std::endl; // 空字符串
}
return 0;
}
编译运行:
g++ -std=c++11 -o main main.cpp
./main
输出:
=== shared_ptr示例 ===
constructor called, size=100
use_count: 2
sp1->size(): 100
after reset, use_count: 1
destructor called
=== unique_ptr示例 ===
*up2: 100
*sp: 100
=== 移动语义示例 ===
v[0]: hello world
v[1]: hello world
s after move: ""
八、常见面试追问
| 问题 | 解答 |
|---|---|
| shared_ptr的引用计数是线程安全的吗? | 是的,引用计数的增减都是原子操作。但指向的对象本身修改不是线程安全的,需要额外加锁。 |
| 能否把同一个裸指针赋给两个shared_ptr? | 绝对不能。这会导致双重析构,应该使用shared_ptr的复制或make_shared创建。 |
| shared_ptr和unique_ptr的主要区别是什么? | shared_ptr共享所有权,引用计数决定释放时机;unique_ptr独占所有权,不能复制只能移动。 |
| weak_ptr什么时候使用? | 主要用于打破shared_ptr的循环引用,或者临时观察某个资源但不拥有。 |
| std::move和移动构造的区别是什么? | std::move是将左值转换为右值引用的工具函数;移动构造是类的一种构造函数,利用右值引用实现资源的转移。 |
| lambda的捕获列表和参数列表有什么区别? | 捕获列表用于捕获外部变量(类成员或全局作用域的变量),参数列表是lambda的输入参数。捕获发生在lambda创建时,参数传递发生在lambda调用时。 |
| emplace_back和push_back有什么区别? | push_back通常先创建临时对象,再拷贝或移动到容器中;emplace_back直接在容器内构造对象,避免额外的拷贝或移动。 |
九、总结
| 特性 | 核心要点 |
|---|---|
| 智能指针 | 利用栈对象自动析构特性,自动释放堆内存 |
| shared_ptr | 共享所有权,引用计数为0时析构,注意循环引用问题 |
| unique_ptr | 独占所有权,不能复制只能移动,性能优于shared_ptr |
| weak_ptr | 不控制生命周期,用于打破循环引用或临时观察 |
| 右值引用 | 使用&&语法,让右值可以被引用 |
| 移动语义 | 资源所有权转移,避免不必要的深拷贝 |
| std::move | 类型转换工具,将左值转为右值引用 |
| std::forward | 保持参数的原始值类别(左值/右值) |
| lambda | 匿名函数对象,捕获列表实现闭包 |
| emplace | 直接构造,避免额外拷贝移动 |
C++11的这些特性大幅改进了C++的性能表现。右值引用和移动语义减少了不必要的内存拷贝,智能指针自动管理内存避免了泄漏和重复释放,lambda表达式让代码更加简洁和表达力强。
根据零声教育教学写作https://github.com/0voice
更多推荐
所有评论(0)