c++总结-04-智能指针
一、内存管理的一些基础
new/delete 表达式
new 表达式完成两件事情:
• 1. 分配对象所需要的内存
• 2. 调用构造器构造对象初始状态
delete 表达式完成两件事情:
• 1. 调用析构器析构对象状态
• 2. 释放对象所占的内存
new[] /delete[] 表达式
new[] 表达式完成两件事情:
• 1. 分配对象数组所需要的内存
• 2. 每一个元素调用构造器构造对象初始状态
delete[] 表达式完成两件事情
• 1. 每一个元素调用析构器析构对象状态
• 2. 释放对象数组所占的所有内存
new[]/delete[] 不能和new/delete 混搭,必须匹配
placement new
在指定内存位置构造对象
void* memory = std::malloc(sizeof(MyClass));
MyClass* myObject = ::new (memory) MyClass("Software");
• 只负责构造对象,不负责分配内存
• 没有placement delete,直接显式调用析构器即可。
myObject->~MyClass();
std::free(memory);
new/delete 操作符
• operator new负责分配内存(当new表达式被调用时)
• 可以定义全局也可以定义针对某一个类的“成对重载”
auto operator new(size_t size) -> void* {
void* p = std::malloc(size);
std::cout << "allocated " << size << " byte(s)\n";
return p;
}
auto operator delete(void* p) noexcept -> void {
std::cout << "deleted memory\n";
return std::free(p);
}
new[]/delete[] 操作符
• new/delete 操作符对应的数组形式, 可以成对重载
auto operator new[](size_t size) -> void* {
void* p = std::malloc(size);
std::cout << "allocated " << size << " byte(s) new[]\n"; return p;
}
auto operator delete[](void* p) noexcept -> void {
std::cout << "deleted memory delete[]\n";
return std::free(p);
}
为某一个类重载new/delete操作符
class MyClass {
public:
auto operator new(size_t size) -> void* {
return ::operator new(size);
}
auto operator delete(void* p) -> void {
::operator delete(p);
}
};
MyClass* p = ::new MyClass{};
::delete p;
小对象优化
• 堆分配可能有严重的碎片效应
• 不是所有的new都必然存储在堆上,可以自定义• 栈适合存储连续的少量对象
• 堆适合存储离散的大量对象
• 利用栈作为对象缓冲区
class MiniString {
private:// 内部存储:联合体区分堆模式/栈模式union {char* heap_ptr; // 堆模式:指向动态内存char stack_buf[16]; // 栈模式:存储最多15字符+1结束符};// 高1位标记模式,低15位记录长度(仅示例,实际需位操作)size_t metadata;// 辅助函数bool is_heap() const { return metadata & 0x8000; } // 最高位为1表示堆模式size_t length() const { return metadata & 0x7FFF; } // 低15位为长度public:explicit MiniString(const char* str) {size_t len = strlen(str);if (len < 16) {// 栈模式:直接复制到内部缓冲区strcpy(stack_buf, str);metadata = len; // 最高位0表示栈模式}else {// 堆模式:动态分配内存heap_ptr = new char[len + 1];strcpy(heap_ptr, str);metadata = len | 0x8000; // 最高位置1}}// 析构函数~MiniString() {if (is_heap()) {delete[] heap_ptr;}// 栈模式无需额外操作}// 打印字符串void print() const {if (is_heap()) {std::cout << heap_ptr;}else {std::cout << stack_buf;}}
};int main() {// 短字符串MiniString s1("hello\n");s1.print(); // 输出 "hello"(存储在栈缓冲区)// 长字符串MiniString s2("this_is_a_very_long_string_that_exceeds_15_characters\n");s2.print(); // 输出长字符串(存储在堆)return 0;
}
结果:
二、智能指针
• 智能指针封装了裸指针,内部还是裸指针的调用
• 智能指针使用RAII特点,将对象生命周期使用栈来管理。
• 智能指针区分了所有权,因此使用责任更为清晰。
• 智能指针大量使用操作符重载和函数内联特点,调用成本和裸指针无差别
一、unique_ptr解析
• 默认情况存储成本和裸指针相同,无添加
• 独占拥有权
• 不支持拷贝构造,只支持移动(所有权转移)
• 可以转换成shared_ptr
• 可自定义删除操作(policy设计),注意不同删除操作的存储成本:
• 函数对象(实例成员决定大小)
• lambda (注意捕获效应会导致lambda对象变大)
• 函数指针(增加一个指针长度)
二、unique指针内存模型
三、基本使用方法
1. 创建 unique_ptr
// 方式1:直接构造
std::unique_ptr<int> ptr1(new int(42));// 方式2:推荐使用 make_unique (C++14起)
std::unique_ptr<int> ptr2 = std::make_unique<int>(42);// 方式3:创建数组
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
2. 所有权转移
std::unique_ptr<int> ptrA = std::make_unique<int>(10);
std::unique_ptr<int> ptrB = std::move(ptrA); // 所有权转移// 此时 ptrA == nullptr,ptrB 拥有资源
3. 释放资源
ptrB.reset(); // 显式释放资源
ptrB.reset(new int(20)); // 释放旧资源,接管新资源
四、关键注意事项
禁止拷贝:unique_ptr 是独占所有权的智能指针,不能拷贝构造或拷贝赋值
std::unique_ptr<int> a = std::make_unique<int>(1);
std::unique_ptr<int> b = a; // 错误!编译失败
多态使用必须虚析构:
class Base {
public:virtual ~Base() = default; // 必须要有虚析构函数
};
数组与对象形式不同:
// 对象版本
std::unique_ptr<MyClass> obj;// 数组版本
std::unique_ptr<MyClass[]> arr;
避免裸指针转换:
int* raw = ptr.get(); // 可以获取但不建议长期保存
delete raw; // 严重错误!会导致双重释放
五、unique_ptr使用场景
• 为动态分配内存提供异常安全(RAII)
• 将动态分配内存的所有权传递给函数
• 从函数内返回动态分配的内存(工厂函数)
• 在容器中保存指针
std::unique_ptr<Shape> createShape(ShapeType type) {switch(type) {case Circle: return std::make_unique<Circle>();case Square: return std::make_unique<Square>();}
}
• 在对象中保存多态子对象(数据成员)
六、高级用法
- 1.自定义删除器
// 函数指针形式
void FileDeleter(FILE* fp) { fclose(fp); }
std::unique_ptr<FILE, decltype(&FileDeleter)> filePtr(fopen("a.txt", "r"), FileDeleter);
// 函数对象形式
struct ArrayDeleter {void operator()(int* p) { delete[] p; }
};
std::unique_ptr<int, ArrayDeleter> arrPtr(new int[10]);
- 2.多态转换
class Base { virtual ~Base() {} };
class Derived : public Base {};std::unique_ptr<Derived> derived = std::make_unique<Derived>();
std::unique_ptr<Base> base = std::move(derived); // 向上转型
- 3.作为函数参数
// 1. 值传递(转移所有权)
void takeOwnership(std::unique_ptr<Resource> res);// 2. 引用传递(不转移所有权)
void useResource(const std::unique_ptr<Resource>& res);// 3. 原始指针访问(不取得所有权)
void workWithResource(Resource* res);
unique_ptr可转为shared_ptr
std::unique_ptr<std::string> foo()
{return std::make_unique<std::string>("foo");
}int main()
{std::shared_ptr<std::string> sp1 = foo(); auto up = std::make_unique<std::string>("Hello World");std::shared_ptr<std::string> sp2 = std::move(up); //std::shared_ptr<std::string> sp3 = up; if(sp2.unique())cout<<"only 1 count"<<endl;}
七、shared_ptr解析
• 共享所有权
• 存储成本较裸指针多了引用计数指针(和相关控制块-共享)• 接口慎用(蔓延问题)
• 线程安全,引用计数增减会减慢多核性能
• 最适合共享的不变数据
• 支持拷贝构造,支持移动
八、共享指针内存模型
九、使用方法
1.基本使用
#include <memory>// 创建 shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>(42); // 推荐方式
std::shared_ptr<int> p2(new int(100)); // 直接构造(不推荐)// 拷贝和赋值(引用计数递增)
std::shared_ptr<int> p3 = p1; // p1、p3 共享所有权// 解引用访问对象
std::cout << *p1 << std::endl; // 输出 42
2.自定义删除器
// 管理文件句柄
std::shared_ptr<FILE> file_ptr(fopen("test.txt", "r"),[](FILE* f) { if (f) fclose(f); } // 自定义删除器
);// 管理数组(需自定义删除器)
std::shared_ptr<int[]> arr_ptr(new int[10], [](int* p) { delete[] p; }
);
3.结合 weak_ptr 打破循环引用
class B;
class A {
public:std::shared_ptr<B> b_ptr;
};class B {
public:std::weak_ptr<A> a_weak_ptr; // 使用 weak_ptr 避免循环引用
};auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_weak_ptr = a; // 不会增加引用计数
十、适用场景
1.共享资源所有权
多个对象需要共享同一块动态分配的内存。
2.复杂生命周期管理
不确定何时释放资源时(如异步操作、缓存系统)。
3.容器的对象管理
在容器中存储动态分配的对象,避免手动内存管理。
4.与第三方库交互
管理 C 风格 API 分配的资源(如文件句柄、网络连接)。
十一、实现原理
1.控制块(Control Block)
• 组成:引用计数、弱引用计数、删除器(Deleter)、分配器(Allocator)。
• 内存布局:
std::make_shared:对象和控制块连续分配(高效,减少内存碎片)。
直接构造:对象和控制块分开分配(两次内存申请)。
2.引用计数
• 线程安全:引用计数的增减是原子的(通过 std::atomic 实现),但对象本身的访问需要额外同步。
• 计数规则:
构造或拷贝 shared_ptr 时,引用计数 +1。
析构或赋值时,引用计数 -1,归零时调用删除器释放资源。
3.weak_ptr 的作用
• 不增加引用计数,但可检测资源是否有效(通过 lock() 获取临时 shared_ptr)。
• 弱引用计数归零时,释放控制块。
十二、注意点
1.避免常见错误
• 不要用裸指针初始化多个 shared_ptr:
int* raw = new int(10);std::shared_ptr<int> p1(raw);std::shared_ptr<int> p2(raw); // 错误!会导致双重释放
• 不要用栈对象地址初始化:
int x = 10;std::shared_ptr<int> p(&x); // 错误!栈对象会自动释放
2.性能与开销
• 原子操作开销:引用计数的原子操作有轻微性能损耗(高并发场景需评估)。
• 内存占用:每个 shared_ptr 携带指向控制块的指针(通常为 16 字节)。
3.循环引用
•问题:两个对象互相持有对方的 shared_ptr,引用计数永不归零。
•解决:用 weak_ptr 替代其中一个指针。
4. 优先使用 make_shared
•优点:
内存分配优化(对象和控制块连续)。
避免因异常导致的内存泄漏(如构造函数抛出异常时,new 分配的资源可能泄漏)。
•例外:需自定义删除器或需单独分配对象时
十三、智能指针最佳实践
• 智能指针仅用于管理内存,不要用于管理非内存资源。非内存资源使用RAII类封装
• 用 unique_ptr表达唯一所有权
• 用 shared_ptr表达共享所有权
• 优先采用 unique_ptr 而不是 shared_ptr,除非需要共享所有权
• 针对共享情况考虑使用引用计数。
• 使用 make_unique() 创建 unique_ptr
• 使用 make_shared() 创建 shared_ptr
• 使用 weak_ptr 防止 shared_ptr 的循环引用