一、内存管理的一些基础
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>();
- }
- }
复制代码 • 在对象中生存多态子对象(数据成员)
六、高级用法
- // 函数指针形式
- 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]);
复制代码- 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); // 向上转型
复制代码- // 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 的循环引用
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|