当前位置: 首页 > news >正文

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 的循环引用

相关文章:

  • 奈雪小程序任务脚本
  • Python与C++中浮点数的精度与计算误差(易忽略易错)
  • C++11(2):
  • 历年华东师范大学保研上机真题
  • 计算机病毒的发展历程及其分类
  • 审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验
  • JavaScript 中的 structuredClone() 如何彻底改变你的对象复制方式
  • 制造业主要管理哪些主数据范围
  • 智能办公系统 — 审批管理模块 · 开发日志
  • 理解HTTP基本认证与表单登录认证
  • [创业之路-381]:企业战略管理案例分析-战略制定/设计-市场洞察“五看”:看宏观-经济-如何获得国家经济政策与愿景规划,以及技术发展趋势、技术成熟度
  • Windows 开始菜单快捷方式路径说明
  • Cygwin:在Windows上搭建类Linux环境的桥梁
  • 《红警2000》游戏信息
  • 工业级FPGA正在推进太空AI的基础设施建设
  • 前端面试热门知识点总结
  • Windows端的C函数setlocale、printf与wprintf打印中文字符谜局小解
  • 算法打卡第六天
  • C++:auto自动类型推导
  • 【算法】枚举右,维护左与滑动窗口对比理解(知识点详解提升思维)5.25
  • 让其他公司做网站应注意什么/百度信息流推广技巧
  • wordpress企业站教程/百度推广账户登录
  • 做gif动图的网站犯法吗/2021谷歌搜索入口
  • 美国少年 建设网站/一键生成app制作器
  • 金华网站建设方案开发/优化seo教程
  • 做网站都有那些步骤/站长查询