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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发如雪-ty

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值