# C内存管理深度剖析从new/delete到智能指针## 1. 引言C是一门以性能和灵活性著称的语言它赋予了程序员对内存的精细控制权。然而这种控制权是一把双刃剑正确使用可以写出高效的程序但稍有不慎就会引入内存泄漏、悬垂指针、重复释放等难以调试的问题。传统的手动内存管理方式——new和delete——要求程序员显式地分配和释放内存这在复杂软件中极易出错。为了应对这些挑战C社区发展了RAIIResource Acquisition Is Initialization惯用法并最终在标准库中引入了智能指针。智能指针封装了原始指针利用C的对象生命周期自动管理内存极大地减少了内存错误的发生。本文将从最基础的new/delete开始深入剖析C内存管理的方方面面逐步过渡到现代C中的各种智能指针探讨它们的原理、用法、性能考量以及最佳实践。本文的目标是帮助读者建立起完整的内存管理知识体系从原理层面理解为什么现代C鼓励使用智能指针以及如何在项目中正确、高效地应用它们。## 2. 原始内存管理new 和 delete在C中我们可以使用new表达式在自由存储区通常是堆上动态分配内存并使用delete表达式释放内存。这是最原始、最直接的内存管理方式。### 2.1 基本用法cpp// 分配单个对象int* p new int(42); // 分配内存并初始化为42// 使用p...delete p; // 释放内存// 分配数组int* arr new int[10]; // 分配10个int的内存未初始化// 使用arr...delete[] arr; // 释放数组内存必须使用delete[]注意事项- new和delete必须成对使用且形式要匹配单个对象用delete数组用delete[]。否则会导致未定义行为通常是内存泄漏或程序崩溃。- new在分配内存失败时会抛出std::bad_alloc异常除非使用nothrow版本new (std::nothrow) int此时失败返回nullptr。### 2.2 operator new 和 operator delete 的重载new表达式实际上执行了两个步骤1) 调用operator new分配原始内存2) 在该内存上构造对象调用构造函数。delete表达式则先调用析构函数再调用operator delete释放内存。我们可以重载全局或类特定的operator new和operator delete来控制内存分配行为。这在实现自定义内存池或跟踪内存分配时非常有用。cpp#include iostream#include cstdlibclass MyClass {public:MyClass() { std::cout MyClass constructor\n; }~MyClass() { std::cout MyClass destructor\n; }// 重载类的 operator newstatic void* operator new(size_t size) {std::cout Custom new for MyClass, size size \n;void* ptr std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 重载类的 operator deletestatic void operator delete(void* ptr) {std::cout Custom delete for MyClass\n;std::free(ptr);}};int main() {MyClass* obj new MyClass(); // 调用自定义newdelete obj; // 调用自定义deletereturn 0;}输出Custom new for MyClass, size 1MyClass constructorMyClass destructorCustom delete for MyClass注意空类的大小通常为1字节所以size为1。我们还可以重载operator new[]和operator delete[]来支持数组分配。### 2.3 Placement newplacement new允许在已经分配的原始内存上构造对象。它不分配内存只调用构造函数。这在内存池、容器实现等场景中很有用。cpp#include iostream#include new // 需要包含头文件int main() {// 分配原始内存块char buffer[sizeof(int) * 10];// 在buffer上构造int对象int* p new (buffer) int(42);std::cout *p \n; // 输出42// 必须显式调用析构函数对于非POD类型// 对于int这样的POD析构函数是平凡的可以不调用p-~int(); // 显式析构仅当需要时才调用// 内存由buffer管理不需要delete preturn 0;}注意使用placement new构造的对象其生命周期结束时应显式调用析构函数但释放原始内存的方式取决于内存来源这里是栈上的数组自动释放。如果内存来自堆则需要用delete[]或free释放原始内存。### 2.4 new/delete 的局限性手动内存管理存在以下主要问题- **内存泄漏**忘记delete或delete[]导致内存无法回收。- **悬垂指针**指针指向的内存已被释放但指针未置空后续使用导致未定义行为。- **重复释放**对同一指针多次调用delete导致程序崩溃。- **异常安全问题**在new和delete之间抛出异常可能导致delete无法执行。- **所有权不明确**谁负责释放指针指向的内存难以在代码中表达。这些问题促使了RAII和智能指针的出现。## 3. 内存泄漏与野指针### 3.1 内存泄漏内存泄漏是指动态分配的内存无法被回收因为指向它的指针丢失了。例如cppvoid leak() {int* p new int(100);// 忘记 delete p;} // p 离开作用域指向的内存无法访问造成泄漏内存泄漏的后果是程序占用内存不断增加最终可能导致系统资源耗尽。### 3.2 野指针悬垂指针野指针是指针指向的内存已被释放但指针仍然持有原地址。对野指针的解引用或delete会导致未定义行为。cppint* p new int(10);delete p;// p 现在是野指针*p 20; // 未定义行为可能崩溃或破坏数据### 3.3 如何避免- 使用智能指针自动管理生命周期。- 遵循RAII原则将资源绑定到对象生命周期。- 释放后将指针置为nullptr但只能避免部分问题多个指针指向同一内存时无效。- 使用静态分析工具、Valgrind、AddressSanitizer等检测内存问题。## 4. RAII 思想RAIIResource Acquisition Is Initialization是C中管理资源的核心思想。它将资源的获取如内存、文件句柄、互斥锁与对象的初始化绑定将资源的释放与对象的析构绑定。当对象生命周期结束时离开作用域、异常抛出等其析构函数自动执行从而确保资源被正确释放。cppclass Buffer {public:Buffer(size_t size) : data(new int[size]), sz(size) {}~Buffer() { delete[] data; }// 禁止拷贝或实现深拷贝/移动语义Buffer(const Buffer) delete;Buffer operator(const Buffer) delete;private:int* data;size_t sz;};void useBuffer() {Buffer buf(100); // 分配内存// 使用buf...// 函数结束或异常时buf的析构函数自动释放内存}RAII是智能指针的基础也是C资源管理的基石。## 5. 智能指针概述智能指针是封装了原始指针的类模板它重载了operator*和operator-使得可以像使用原始指针一样使用它同时利用RAII自动管理内存。C98中引入了std::auto_ptr但由于其设计缺陷拷贝时转移所有权导致源指针变为空在C11中被弃用并被新的智能指针取代。C11提供了三种智能指针- std::unique_ptr独占所有权不可拷贝只能移动。- std::shared_ptr共享所有权通过引用计数管理。- std::weak_ptr配合shared_ptr使用弱引用不增加引用计数用于解决循环引用。此外C17对shared_ptr的数组支持做了改进C20引入了std::atomic_shared_ptr原子操作版本。## 6. std::unique_ptrstd::unique_ptr是一种独占所有权的智能指针。它不能被拷贝只能通过移动语义转移所有权。它的设计轻量、高效通常与裸指针大小相同且无额外开销如果使用默认删除器。### 6.1 基本用法cpp#include memory#include iostreamstruct Widget {Widget(int v) : val(v) { std::cout Widget( val )\n; }~Widget() { std::cout ~Widget( val )\n; }int val;};int main() {// 使用 make_unique (C14)auto p1 std::make_uniqueWidget(10);// 不能拷贝 unique_ptr// auto p2 p1; // 错误// 可以移动auto p2 std::move(p1);if (!p1) {std::cout p1 is null\n;}p2-val 20;// p2 离开作用域时自动删除 Widgetreturn 0;}输出Widget(10)p1 is null~Widget(20)### 6.2 自定义删除器std::unique_ptr可以接受自定义删除器用于管理非new分配的资源如文件句柄、套接字等。cpp#include memory#include cstdioauto fileCloser [](FILE* f) {if (f) fclose(f);std::cout File closed\n;};int main() {std::unique_ptrFILE, decltype(fileCloser)fp(fopen(test.txt, w), fileCloser);if (fp) {fprintf(fp.get(), Hello, world!\n);}// 离开作用域时自动调用 fclosereturn 0;}注意自定义删除器的类型是unique_ptr类型的一部分这会影响函数签名和大小。无状态的lambda捕获列表为空可以优化为与裸指针相同的大小使用空基类优化。### 6.3 数组支持std::unique_ptr特化了数组版本std::unique_ptrT[]它使用delete[]释放内存并重载了operator[]以便访问元素。cppstd::unique_ptrint[] arr std::make_uniqueint[](10);for (int i 0; i 10; i) {arr[i] i * i;}// 自动 delete[]### 6.4 性能特点std::unique_ptr几乎没有运行时开销除了可能的删除器存储。它的大小通常与裸指针相同操作也是内联的。因此它是替代裸指针的首选。## 7. std::shared_ptrstd::shared_ptr通过引用计数实现共享所有权。多个shared_ptr可以指向同一对象当最后一个指向对象的shared_ptr被销毁时对象被删除。引用计数是线程安全的增减操作是原子的但对象本身的访问需要外部同步。### 7.1 基本用法cpp#include memory#include iostreamstruct Widget {Widget(int v) : val(v) { std::cout Widget( val )\n; }~Widget() { std::cout ~Widget( val )\n; }int val;};int main() {std::shared_ptrWidget p1 std::make_sharedWidget(1);{std::shared_ptrWidget p2 p1; // 拷贝引用计数1std::cout use_count: p2.use_count() \n; // 2} // p2 析构引用计数-1std::cout use_count: p1.use_count() \n; // 1// p1 析构引用计数为0删除对象return 0;}### 7.2 std::make_shared推荐使用std::make_shared创建shared_ptr因为它更高效且异常安全。- **效率**make_shared一次分配内存同时容纳对象和控制块引用计数等而直接new后传给shared_ptr构造函数会分配两次内存一次对象一次控制块。- **异常安全**如果在分配对象后、构造shared_ptr前抛出异常使用new表达式可能导致内存泄漏make_shared避免了此问题。cpp// 推荐auto p std::make_sharedWidget(42);// 不推荐但有特殊需求时可用std::shared_ptrWidget p2(new Widget(42));### 7.3 自定义删除器与unique_ptr类似shared_ptr也支持自定义删除器但删除器的类型不影响shared_ptr的类型因为删除器被存储在控制块中类型擦除。这使得多个shared_ptr即使有不同的删除器也可以指向同一对象。cppauto del [](Widget* w) {std::cout Custom delete\n;delete w;};std::shared_ptrWidget p(new Widget(1), del);### 7.4 数组支持在C17之前shared_ptr不直接支持数组需要提供自定义删除器来调用delete[]。C17引入了部分支持shared_ptrT[]允许使用operator[]访问元素但仍需提供默认删除器会调用delete[]实际上标准规定如果T是数组类型默认删除器会调用delete[]。但注意shared_ptrint[]的默认删除器确实使用delete[]这与unique_ptr一致。cpp// C17std::shared_ptrint[] sp(new int[10]{1,2,3,4,5,6,7,8,9,0});for (int i 0; i 10; i) {std::cout sp[i] ;}然而std::make_shared不支持数组直到C20C20引入了std::make_sharedT[]。在C17中需要手动new数组。### 7.5 性能与开销shared_ptr比unique_ptr重因为它需要维护引用计数原子操作、控制块包含删除器、分配器等。每次拷贝、赋值、销毁都会涉及原子操作可能成为性能瓶颈。此外make_shared分配的内存块中对象和控制块在一起但对象的析构可能早于控制块当最后一个指向对象的shared_ptr销毁时对象析构但若还有weak_ptr存在控制块仍存活。这种布局可以提高缓存局部性但也意味着对象内存的释放可能延迟到所有weak_ptr销毁后。## 8. std::weak_ptrstd::weak_ptr是一种弱引用它指向由shared_ptr管理的对象但不增加引用计数。它主要用于解决shared_ptr相互引用导致的循环引用问题。weak_ptr不能直接解引用必须通过lock()方法获取一个shared_ptr如果对象还存在或者通过expired()检查对象是否已销毁。### 8.1 基本用法cpp#include memory#include iostreamint main() {auto sp std::make_sharedint(100);std::weak_ptrint wp sp; // 引用计数仍为1if (auto locked wp.lock()) { // 获取 shared_ptrstd::cout *locked \n;} else {std::cout Object expired\n;}sp.reset(); // 释放 shared_ptr对象被销毁if (wp.expired()) {std::cout Now expired\n;}return 0;}### 8.2 解决循环引用考虑一个双向链表或父子结构如果两个对象互相持有shared_ptr则引用计数永远无法降为0导致内存泄漏。cppstruct Node {std::shared_ptrNode next;std::shared_ptrNode prev;~Node() { std::cout Node destroyed\n; }};int main() {auto n1 std::make_sharedNode();auto n2 std::make_sharedNode();n1-next n2;n2-prev n1;// 循环引用两个 Node 都不会销毁return 0;} // 无输出解决方法是使用weak_ptr打破循环将其中一个方向的指针改为weak_ptr。cppstruct Node {std::shared_ptrNode next;std::weak_ptrNode prev; // 使用 weak_ptr~Node() { std::cout Node destroyed\n; }};int main() {auto n1 std::make_sharedNode();auto n2 std::make_sharedNode();n1-next n2;n2-prev n1;// 现在 n1 的引用计数为1n2-prev 弱引用不计数// n2 的引用计数为1n1-next// 退出作用域时n1和n2析构正常销毁return 0;}输出Node destroyedNode destroyed### 8.3 观察者模式等场景weak_ptr也可用于缓存或观察者模式其中需要访问对象但不希望影响其生命周期。## 9. 智能指针与裸指针混用的陷阱虽然智能指针自动管理内存但有时仍需与裸指针交互例如- 传递到期望裸指针的遗留函数。- 实现自己的数据结构。- 避免循环引用但通常用weak_ptr解决。需要注意的陷阱### 9.1 不要从原始指针创建多个独立的 shared_ptr如果从同一个裸指针构造两个shared_ptr它们会各自创建一个控制块导致重复释放。cppint* p new int(42);std::shared_ptrint sp1(p);std::shared_ptrint sp2(p); // 错误第二个 shared_ptr 也会管理 p导致 double free正确做法是直接从一个shared_ptr拷贝构造另一个或者使用make_shared。### 9.2 get() 返回的裸指针不能 delete也不能创建新的智能指针shared_ptr和unique_ptr都提供了get()方法返回裸指针但此指针仅用于解引用绝不能手动delete也不能用它构造另一个智能指针除非确保原始智能指针已失效但通常不安全。cppauto sp std::make_sharedint(10);int* raw sp.get();// delete raw; // 错误sp 析构时会再次 delete// std::shared_ptrint sp2(raw); // 错误会导致 double free### 9.3 小心 this 指针生成 shared_ptr如果一个类本身被shared_ptr管理而类内部需要将自己的this指针传递给其他函数或创建指向自己的shared_ptr直接shared_ptrMyClass(this)会导致重复管理。应该从std::enable_shared_from_this继承并使用shared_from_this()。cpp#include memoryclass Bad : public std::enable_shared_from_thisBad {public:std::shared_ptrBad getptr() {return shared_from_this();}};int main() {auto b1 std::make_sharedBad();auto b2 b1-getptr(); // 正确b2 与 b1 共享所有权// Bad* b new Bad();// auto b3 b-getptr(); // 错误b 没有被 shared_ptr 管理会抛出 std::bad_weak_ptrreturn 0;}注意必须在shared_ptr管理对象上调用shared_from_this()否则行为未定义通常抛出异常。## 10. 自定义删除器深入自定义删除器使得智能指针能够管理各种资源不仅仅是堆内存。例如文件句柄、数据库连接、POSIX锁等。### 10.1 unique_ptr 中的删除器unique_ptr的删除器是类型的一部分因此不同的删除器导致不同的类型。这带来了零开销但限制了灵活性。可以使用std::function包装删除器但会引入额外开销。cpp// 函数指针删除器void closeFile(FILE* f) { if (f) fclose(f); }std::unique_ptrFILE, decltype(closeFile) up1(fopen(a.txt, r), closeFile);// lambda 删除器推荐auto deleter [](FILE* f) { if (f) fclose(f); };std::unique_ptrFILE, decltype(deleter) up2(fopen(b.txt, r), deleter);### 10.2 shared_ptr 中的删除器shared_ptr的删除器不参与类型通过类型擦除存储在控制块中。因此可以创建两个shared_ptr指向同一对象但使用不同的删除器只要它们是从同一shared_ptr拷贝而来或者原始指针相同但分别构造会导致独立控制块必须小心。一般不会混用不同的删除器管理同一对象但可以做到。cppauto del1 [](int* p) { std::cout delete1\n; delete p; };auto del2 [](int* p) { std::cout delete2\n; delete p; };std::shared_ptrint sp1(new int(1), del1);std::shared_ptrint sp2(sp1); // 共享控制块删除器仍是 del1// std::shared_ptrint sp3(new int(2), del2); // 独立## 11. 性能考量智能指针虽然方便但并非零开销。理解其开销有助于在性能关键代码中做出明智选择。### 11.1 unique_ptr 的开销unique_ptr在默认删除器下大小与裸指针相同操作也是内联的没有运行时开销。使用无状态自定义删除器时通过空基类优化也能保持大小不变。只有带状态的删除器例如捕获了变量的lambda会增加unique_ptr的大小。### 11.2 shared_ptr 的开销- **内存开销**每个shared_ptr对象本身通常包含两个指针一个指向对象一个指向控制块。控制块包含引用计数强引用计数、弱引用计数、删除器、分配器等。- **时间开销**拷贝、赋值、销毁shared_ptr时涉及原子操作修改引用计数这比非原子操作慢但在多线程环境下是必要的。原子操作可能导致缓存一致性开销。- **make_shared 的优势**一次分配对象和控制块在一起减少了内存分配次数也提高了局部性。但缺点是对象内存可能延迟释放因为控制块可能存活更久如果还有weak_ptr对于大对象可能不是最佳。### 11.3 移动语义优化移动unique_ptr或shared_ptr只是转移指针所有权不修改引用计数因此非常快相当于拷贝指针。在可能的情况下使用移动而不是拷贝可以提升性能。cpp// 通过值传递 unique_ptr 表示转移所有权void sink(std::unique_ptrWidget p) {// 使用 p}auto w std::make_uniqueWidget();sink(std::move(w)); // 移动无开销## 12. 内存池与自定义分配器除了智能指针C还允许通过自定义分配器控制容器的内存分配策略。std::allocator是默认分配器我们可以实现自己的分配器来使用内存池、共享内存等。### 12.1 标准分配器接口分配器必须提供allocate和deallocate成员函数以及一些类型别名。例如cpptemplate typename Tclass MyAllocator {public:using value_type T;MyAllocator() default;T* allocate(std::size_t n) {std::cout Allocating n objects\n;return static_castT*(::operator new(n * sizeof(T)));}void deallocate(T* p, std::size_t n) {std::cout Deallocating n objects\n;::operator delete(p);}// 需要提供相等比较bool operator(const MyAllocator) const { return true; }bool operator!(const MyAllocator) const { return false; }};int main() {std::vectorint, MyAllocatorint vec;vec.push_back(1);vec.push_back(2);return 0;}### 12.2 内存池内存池预先分配一大块内存然后从中切分出小块给对象减少频繁的堆分配开销并减少内存碎片。通常用于大量小对象的场景。实现一个简单的内存池cppclass FixedBlockPool {public:FixedBlockPool(size_t blockSize, size_t blockCount): blockSize_(blockSize), blockCount_(blockCount) {pool_ static_castchar*(::operator new(blockSize * blockCount));// 初始化自由链表for (size_t i 0; i blockCount; i) {char* block pool_ i * blockSize;*reinterpret_castchar**(block) nextFree_;nextFree_ block;}}~FixedBlockPool() {::operator delete(pool_);}void* allocate() {if (!nextFree_) throw std::bad_alloc();void* block nextFree_;nextFree_ *reinterpret_castchar**(nextFree_);return block;}void deallocate(void* p) {if (!p) return;*reinterpret_castchar**(p) nextFree_;nextFree_ static_castchar*(p);}private:size_t blockSize_;size_t blockCount_;char* pool_;char* nextFree_ nullptr;};然后将这个内存池与自定义分配器结合用于容器或智能指针。但智能指针本身不直接支持分配器参数make_shared允许传入分配器C11后如std::allocate_shared。cppFixedBlockPool pool(sizeof(int), 100);std::shared_ptrint sp std::allocate_sharedint(MyAllocatorint(), 42);// 但我们的MyAllocator并未使用pool需要编写与pool关联的分配器。## 13. 智能指针与动态数组### 13.1 unique_ptr 处理数组unique_ptr有数组特化使用std::unique_ptrT[]自动调用delete[]支持下标操作。推荐使用std::make_uniqueT[](size)C14。cppauto arr std::make_uniqueint[](5);for (int i 0; i 5; i) {arr[i] i;}### 13.2 shared_ptr 处理数组在C17之前没有shared_ptrT[]特化需要自定义删除器来调用delete[]。cppstd::shared_ptrint sp(new int[10], std::default_deleteint[]());// 但不能使用下标操作需要 get() 索引C17引入了shared_ptrT[]允许使用下标但make_shared仍不支持数组直到C20。cpp// C17std::shared_ptrint[] sp(new int[10]{0});sp[0] 42; // OK// C20auto sp2 std::make_sharedint[](10); // 默认初始化auto sp3 std::make_sharedint[5](1,2,3,4,5); // 直接初始化### 13.3 使用 std::vector 替代动态数组对于动态数组通常优先考虑std::vector它提供了更丰富的接口且内存管理自动。只有在需要避免vector的开销或与C API交互时才使用原始数组或智能指针数组。## 14. 循环引用及其它陷阱### 14.1 循环引用我们已经在weak_ptr部分看到了循环引用的例子。除了双向链表任何相互持有shared_ptr的图结构都可能导致循环。使用weak_ptr打断循环是关键。### 14.2 使用 shared_ptr 管理 this之前提到的enable_shared_from_this是正确做法。但如果对象被shared_ptr管理但又在外部通过this创建了新的shared_ptr会导致多个控制块。### 14.3 将 shared_ptr 传递给 lambda 时的生命周期如果lambda捕获shared_ptr则lambda会持有该shared_ptr的拷贝延长对象的生命周期直到lambda被销毁。这在异步编程中常见。cppauto sp std::make_sharedint(10);std::thread t([sp]() {// sp 被拷贝到线程中确保对象在线程执行期间存活std::this_thread::sleep_for(std::chrono::seconds(1));std::cout *sp \n;});t.detach(); // 注意detach后如果主线程结束可能对象仍被sp持有直到线程执行完// 但主线程可能先结束导致程序终止所有资源释放### 14.4 智能指针的线程安全shared_ptr的引用计数操作是线程安全的但指向的对象本身不是。多个线程同时修改对象需要同步。此外shared_ptr对象本身如sp变量的修改如赋值、reset也不是线程安全的需要外部同步。## 15. 现代C内存管理最佳实践基于以上讨论总结以下最佳实践1. **避免使用裸 new/delete**除非在实现底层内存分配器或与C库交互否则不要直接使用new和delete。优先使用智能指针或容器。2. **使用 std::make_unique 和 std::make_shared**它们更安全、更高效且避免了直接使用new的异常安全问题。3. **明确所有权**选择正确的智能指针- 独占所有权std::unique_ptr。- 共享所有权std::shared_ptr。- 观察者或打破循环std::weak_ptr。4. **在接口中传递裸指针仅用于非所有权访问**如果函数只是要使用对象而不涉及生命周期管理可以传递裸指针或引用。不要传递从智能指针get()获得的裸指针并期望函数管理生命周期。5. **使用 std::unique_ptr 作为工厂函数的返回类型**这清晰地表明调用者获得对象的所有权。6. **在类内部如果必须持有指针成员考虑使用智能指针**明确成员的所有权语义。对于非拥有指针可以使用原始指针但需确保生命周期。7. **避免循环引用**在设计数据结构时分析哪些指针应该是弱引用。8. **使用 std::enable_shared_from_this 安全地获取 this 的 shared_ptr**。9. **性能敏感处注意开销**shared_ptr的原子操作可能成为瓶颈可考虑使用unique_ptr或侵入式引用计数boost::intrusive_ptr。10. **与C接口交互时小心管理生命周期**可能需要使用自定义删除器调用C释放函数或使用智能指针包装C资源。## 16. 案例研究实现一个简单的对象池结合所学知识实现一个简单的对象池使用unique_ptr自定义删除器将对象归还到池中。cpp#include memory#include vector#include iostreamtemplate typename Tclass ObjectPool {public:using DeleterType std::functionvoid(T*);ObjectPool(std::size_t initSize 10) {for (std::size_t i 0; i initSize; i) {pool_.push_back(new T());}}~ObjectPool() {while (!pool_.empty()) {delete pool_.back();pool_.pop_back();}}// 获取一个对象使用智能指针自动归还std::unique_ptrT, DeleterType acquire() {if (pool_.empty()) {// 扩展池分配新对象T* obj new T();return std::unique_ptrT, DeleterType(obj, [this](T* p) { release(p); });} else {T* obj pool_.back();pool_.pop_back();return std::unique_ptrT, DeleterType(obj, [this](T* p) { release(p); });}}std::size_t size() const { return pool_.size(); }private:void release(T* obj) {// 可以重置对象状态*obj T(); // 假设 T 可赋值pool_.push_back(obj);}std::vectorT* pool_;};struct Widget {int a, b;Widget() : a(0), b(0) { std::cout Widget default ctor\n; }Widget(int x, int y) : a(x), b(y) { std::cout Widget ctor( x , y )\n; }~Widget() { std::cout Widget dtor\n; }};int main() {ObjectPoolWidget pool(2);std::cout Pool size: pool.size() \n;{auto w1 pool.acquire();w1-a 10;w1-b 20;std::cout Acquired w1: w1-a , w1-b \n;auto w2 pool.acquire();std::cout Acquired w2\n;// w1 和 w2 离开作用域时会调用自定义删除器将对象归还池中std::cout Pool size before release: pool.size() \n;}std::cout Pool size after release: pool.size() \n;auto w3 pool.acquire();std::cout Acquired w3, its values: w3-a , w3-b \n; // 应该是重置后的0,0return 0;}输出示例Widget default ctorWidget default ctorPool size: 2Acquired w1: 10,20Acquired w2Pool size before release: 0Widget dtorWidget dtorPool size after release: 2Acquired w3, its values: 0,0Widget dtorWidget dtor注意这里的DeleterType使用了std::function会有额外开销。实际实现可以用无状态的lambda模板参数但为了简化这里使用了类型擦除。## 17. 结论C内存管理经历了从手动new/delete到智能指针的演进极大地减轻了程序员的负担提高了代码的安全性和可维护性。理解底层原理如operator new、引用计数、RAII有助于更好地运用这些工具。现代C推荐尽可能使用智能指针和标准容器将资源管理交给对象生命周期。然而没有银弹智能指针也有其开销和适用场景需要根据实际需求权衡。通过掌握本文剖析的概念和技巧你将能够在C项目中游刃有余地处理内存管理问题写出更健壮、更高效的代码。本文从原始内存管理讲起逐步深入到智能指针的方方面面涵盖了基本用法、原理、性能、陷阱和最佳实践。希望读者能够将这些知识应用到实际开发中充分享受C11/14/17/20带来的便利。
C++内存管理深度剖析:从new/delete到智能指针
# C内存管理深度剖析从new/delete到智能指针## 1. 引言C是一门以性能和灵活性著称的语言它赋予了程序员对内存的精细控制权。然而这种控制权是一把双刃剑正确使用可以写出高效的程序但稍有不慎就会引入内存泄漏、悬垂指针、重复释放等难以调试的问题。传统的手动内存管理方式——new和delete——要求程序员显式地分配和释放内存这在复杂软件中极易出错。为了应对这些挑战C社区发展了RAIIResource Acquisition Is Initialization惯用法并最终在标准库中引入了智能指针。智能指针封装了原始指针利用C的对象生命周期自动管理内存极大地减少了内存错误的发生。本文将从最基础的new/delete开始深入剖析C内存管理的方方面面逐步过渡到现代C中的各种智能指针探讨它们的原理、用法、性能考量以及最佳实践。本文的目标是帮助读者建立起完整的内存管理知识体系从原理层面理解为什么现代C鼓励使用智能指针以及如何在项目中正确、高效地应用它们。## 2. 原始内存管理new 和 delete在C中我们可以使用new表达式在自由存储区通常是堆上动态分配内存并使用delete表达式释放内存。这是最原始、最直接的内存管理方式。### 2.1 基本用法cpp// 分配单个对象int* p new int(42); // 分配内存并初始化为42// 使用p...delete p; // 释放内存// 分配数组int* arr new int[10]; // 分配10个int的内存未初始化// 使用arr...delete[] arr; // 释放数组内存必须使用delete[]注意事项- new和delete必须成对使用且形式要匹配单个对象用delete数组用delete[]。否则会导致未定义行为通常是内存泄漏或程序崩溃。- new在分配内存失败时会抛出std::bad_alloc异常除非使用nothrow版本new (std::nothrow) int此时失败返回nullptr。### 2.2 operator new 和 operator delete 的重载new表达式实际上执行了两个步骤1) 调用operator new分配原始内存2) 在该内存上构造对象调用构造函数。delete表达式则先调用析构函数再调用operator delete释放内存。我们可以重载全局或类特定的operator new和operator delete来控制内存分配行为。这在实现自定义内存池或跟踪内存分配时非常有用。cpp#include iostream#include cstdlibclass MyClass {public:MyClass() { std::cout MyClass constructor\n; }~MyClass() { std::cout MyClass destructor\n; }// 重载类的 operator newstatic void* operator new(size_t size) {std::cout Custom new for MyClass, size size \n;void* ptr std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 重载类的 operator deletestatic void operator delete(void* ptr) {std::cout Custom delete for MyClass\n;std::free(ptr);}};int main() {MyClass* obj new MyClass(); // 调用自定义newdelete obj; // 调用自定义deletereturn 0;}输出Custom new for MyClass, size 1MyClass constructorMyClass destructorCustom delete for MyClass注意空类的大小通常为1字节所以size为1。我们还可以重载operator new[]和operator delete[]来支持数组分配。### 2.3 Placement newplacement new允许在已经分配的原始内存上构造对象。它不分配内存只调用构造函数。这在内存池、容器实现等场景中很有用。cpp#include iostream#include new // 需要包含头文件int main() {// 分配原始内存块char buffer[sizeof(int) * 10];// 在buffer上构造int对象int* p new (buffer) int(42);std::cout *p \n; // 输出42// 必须显式调用析构函数对于非POD类型// 对于int这样的POD析构函数是平凡的可以不调用p-~int(); // 显式析构仅当需要时才调用// 内存由buffer管理不需要delete preturn 0;}注意使用placement new构造的对象其生命周期结束时应显式调用析构函数但释放原始内存的方式取决于内存来源这里是栈上的数组自动释放。如果内存来自堆则需要用delete[]或free释放原始内存。### 2.4 new/delete 的局限性手动内存管理存在以下主要问题- **内存泄漏**忘记delete或delete[]导致内存无法回收。- **悬垂指针**指针指向的内存已被释放但指针未置空后续使用导致未定义行为。- **重复释放**对同一指针多次调用delete导致程序崩溃。- **异常安全问题**在new和delete之间抛出异常可能导致delete无法执行。- **所有权不明确**谁负责释放指针指向的内存难以在代码中表达。这些问题促使了RAII和智能指针的出现。## 3. 内存泄漏与野指针### 3.1 内存泄漏内存泄漏是指动态分配的内存无法被回收因为指向它的指针丢失了。例如cppvoid leak() {int* p new int(100);// 忘记 delete p;} // p 离开作用域指向的内存无法访问造成泄漏内存泄漏的后果是程序占用内存不断增加最终可能导致系统资源耗尽。### 3.2 野指针悬垂指针野指针是指针指向的内存已被释放但指针仍然持有原地址。对野指针的解引用或delete会导致未定义行为。cppint* p new int(10);delete p;// p 现在是野指针*p 20; // 未定义行为可能崩溃或破坏数据### 3.3 如何避免- 使用智能指针自动管理生命周期。- 遵循RAII原则将资源绑定到对象生命周期。- 释放后将指针置为nullptr但只能避免部分问题多个指针指向同一内存时无效。- 使用静态分析工具、Valgrind、AddressSanitizer等检测内存问题。## 4. RAII 思想RAIIResource Acquisition Is Initialization是C中管理资源的核心思想。它将资源的获取如内存、文件句柄、互斥锁与对象的初始化绑定将资源的释放与对象的析构绑定。当对象生命周期结束时离开作用域、异常抛出等其析构函数自动执行从而确保资源被正确释放。cppclass Buffer {public:Buffer(size_t size) : data(new int[size]), sz(size) {}~Buffer() { delete[] data; }// 禁止拷贝或实现深拷贝/移动语义Buffer(const Buffer) delete;Buffer operator(const Buffer) delete;private:int* data;size_t sz;};void useBuffer() {Buffer buf(100); // 分配内存// 使用buf...// 函数结束或异常时buf的析构函数自动释放内存}RAII是智能指针的基础也是C资源管理的基石。## 5. 智能指针概述智能指针是封装了原始指针的类模板它重载了operator*和operator-使得可以像使用原始指针一样使用它同时利用RAII自动管理内存。C98中引入了std::auto_ptr但由于其设计缺陷拷贝时转移所有权导致源指针变为空在C11中被弃用并被新的智能指针取代。C11提供了三种智能指针- std::unique_ptr独占所有权不可拷贝只能移动。- std::shared_ptr共享所有权通过引用计数管理。- std::weak_ptr配合shared_ptr使用弱引用不增加引用计数用于解决循环引用。此外C17对shared_ptr的数组支持做了改进C20引入了std::atomic_shared_ptr原子操作版本。## 6. std::unique_ptrstd::unique_ptr是一种独占所有权的智能指针。它不能被拷贝只能通过移动语义转移所有权。它的设计轻量、高效通常与裸指针大小相同且无额外开销如果使用默认删除器。### 6.1 基本用法cpp#include memory#include iostreamstruct Widget {Widget(int v) : val(v) { std::cout Widget( val )\n; }~Widget() { std::cout ~Widget( val )\n; }int val;};int main() {// 使用 make_unique (C14)auto p1 std::make_uniqueWidget(10);// 不能拷贝 unique_ptr// auto p2 p1; // 错误// 可以移动auto p2 std::move(p1);if (!p1) {std::cout p1 is null\n;}p2-val 20;// p2 离开作用域时自动删除 Widgetreturn 0;}输出Widget(10)p1 is null~Widget(20)### 6.2 自定义删除器std::unique_ptr可以接受自定义删除器用于管理非new分配的资源如文件句柄、套接字等。cpp#include memory#include cstdioauto fileCloser [](FILE* f) {if (f) fclose(f);std::cout File closed\n;};int main() {std::unique_ptrFILE, decltype(fileCloser)fp(fopen(test.txt, w), fileCloser);if (fp) {fprintf(fp.get(), Hello, world!\n);}// 离开作用域时自动调用 fclosereturn 0;}注意自定义删除器的类型是unique_ptr类型的一部分这会影响函数签名和大小。无状态的lambda捕获列表为空可以优化为与裸指针相同的大小使用空基类优化。### 6.3 数组支持std::unique_ptr特化了数组版本std::unique_ptrT[]它使用delete[]释放内存并重载了operator[]以便访问元素。cppstd::unique_ptrint[] arr std::make_uniqueint[](10);for (int i 0; i 10; i) {arr[i] i * i;}// 自动 delete[]### 6.4 性能特点std::unique_ptr几乎没有运行时开销除了可能的删除器存储。它的大小通常与裸指针相同操作也是内联的。因此它是替代裸指针的首选。## 7. std::shared_ptrstd::shared_ptr通过引用计数实现共享所有权。多个shared_ptr可以指向同一对象当最后一个指向对象的shared_ptr被销毁时对象被删除。引用计数是线程安全的增减操作是原子的但对象本身的访问需要外部同步。### 7.1 基本用法cpp#include memory#include iostreamstruct Widget {Widget(int v) : val(v) { std::cout Widget( val )\n; }~Widget() { std::cout ~Widget( val )\n; }int val;};int main() {std::shared_ptrWidget p1 std::make_sharedWidget(1);{std::shared_ptrWidget p2 p1; // 拷贝引用计数1std::cout use_count: p2.use_count() \n; // 2} // p2 析构引用计数-1std::cout use_count: p1.use_count() \n; // 1// p1 析构引用计数为0删除对象return 0;}### 7.2 std::make_shared推荐使用std::make_shared创建shared_ptr因为它更高效且异常安全。- **效率**make_shared一次分配内存同时容纳对象和控制块引用计数等而直接new后传给shared_ptr构造函数会分配两次内存一次对象一次控制块。- **异常安全**如果在分配对象后、构造shared_ptr前抛出异常使用new表达式可能导致内存泄漏make_shared避免了此问题。cpp// 推荐auto p std::make_sharedWidget(42);// 不推荐但有特殊需求时可用std::shared_ptrWidget p2(new Widget(42));### 7.3 自定义删除器与unique_ptr类似shared_ptr也支持自定义删除器但删除器的类型不影响shared_ptr的类型因为删除器被存储在控制块中类型擦除。这使得多个shared_ptr即使有不同的删除器也可以指向同一对象。cppauto del [](Widget* w) {std::cout Custom delete\n;delete w;};std::shared_ptrWidget p(new Widget(1), del);### 7.4 数组支持在C17之前shared_ptr不直接支持数组需要提供自定义删除器来调用delete[]。C17引入了部分支持shared_ptrT[]允许使用operator[]访问元素但仍需提供默认删除器会调用delete[]实际上标准规定如果T是数组类型默认删除器会调用delete[]。但注意shared_ptrint[]的默认删除器确实使用delete[]这与unique_ptr一致。cpp// C17std::shared_ptrint[] sp(new int[10]{1,2,3,4,5,6,7,8,9,0});for (int i 0; i 10; i) {std::cout sp[i] ;}然而std::make_shared不支持数组直到C20C20引入了std::make_sharedT[]。在C17中需要手动new数组。### 7.5 性能与开销shared_ptr比unique_ptr重因为它需要维护引用计数原子操作、控制块包含删除器、分配器等。每次拷贝、赋值、销毁都会涉及原子操作可能成为性能瓶颈。此外make_shared分配的内存块中对象和控制块在一起但对象的析构可能早于控制块当最后一个指向对象的shared_ptr销毁时对象析构但若还有weak_ptr存在控制块仍存活。这种布局可以提高缓存局部性但也意味着对象内存的释放可能延迟到所有weak_ptr销毁后。## 8. std::weak_ptrstd::weak_ptr是一种弱引用它指向由shared_ptr管理的对象但不增加引用计数。它主要用于解决shared_ptr相互引用导致的循环引用问题。weak_ptr不能直接解引用必须通过lock()方法获取一个shared_ptr如果对象还存在或者通过expired()检查对象是否已销毁。### 8.1 基本用法cpp#include memory#include iostreamint main() {auto sp std::make_sharedint(100);std::weak_ptrint wp sp; // 引用计数仍为1if (auto locked wp.lock()) { // 获取 shared_ptrstd::cout *locked \n;} else {std::cout Object expired\n;}sp.reset(); // 释放 shared_ptr对象被销毁if (wp.expired()) {std::cout Now expired\n;}return 0;}### 8.2 解决循环引用考虑一个双向链表或父子结构如果两个对象互相持有shared_ptr则引用计数永远无法降为0导致内存泄漏。cppstruct Node {std::shared_ptrNode next;std::shared_ptrNode prev;~Node() { std::cout Node destroyed\n; }};int main() {auto n1 std::make_sharedNode();auto n2 std::make_sharedNode();n1-next n2;n2-prev n1;// 循环引用两个 Node 都不会销毁return 0;} // 无输出解决方法是使用weak_ptr打破循环将其中一个方向的指针改为weak_ptr。cppstruct Node {std::shared_ptrNode next;std::weak_ptrNode prev; // 使用 weak_ptr~Node() { std::cout Node destroyed\n; }};int main() {auto n1 std::make_sharedNode();auto n2 std::make_sharedNode();n1-next n2;n2-prev n1;// 现在 n1 的引用计数为1n2-prev 弱引用不计数// n2 的引用计数为1n1-next// 退出作用域时n1和n2析构正常销毁return 0;}输出Node destroyedNode destroyed### 8.3 观察者模式等场景weak_ptr也可用于缓存或观察者模式其中需要访问对象但不希望影响其生命周期。## 9. 智能指针与裸指针混用的陷阱虽然智能指针自动管理内存但有时仍需与裸指针交互例如- 传递到期望裸指针的遗留函数。- 实现自己的数据结构。- 避免循环引用但通常用weak_ptr解决。需要注意的陷阱### 9.1 不要从原始指针创建多个独立的 shared_ptr如果从同一个裸指针构造两个shared_ptr它们会各自创建一个控制块导致重复释放。cppint* p new int(42);std::shared_ptrint sp1(p);std::shared_ptrint sp2(p); // 错误第二个 shared_ptr 也会管理 p导致 double free正确做法是直接从一个shared_ptr拷贝构造另一个或者使用make_shared。### 9.2 get() 返回的裸指针不能 delete也不能创建新的智能指针shared_ptr和unique_ptr都提供了get()方法返回裸指针但此指针仅用于解引用绝不能手动delete也不能用它构造另一个智能指针除非确保原始智能指针已失效但通常不安全。cppauto sp std::make_sharedint(10);int* raw sp.get();// delete raw; // 错误sp 析构时会再次 delete// std::shared_ptrint sp2(raw); // 错误会导致 double free### 9.3 小心 this 指针生成 shared_ptr如果一个类本身被shared_ptr管理而类内部需要将自己的this指针传递给其他函数或创建指向自己的shared_ptr直接shared_ptrMyClass(this)会导致重复管理。应该从std::enable_shared_from_this继承并使用shared_from_this()。cpp#include memoryclass Bad : public std::enable_shared_from_thisBad {public:std::shared_ptrBad getptr() {return shared_from_this();}};int main() {auto b1 std::make_sharedBad();auto b2 b1-getptr(); // 正确b2 与 b1 共享所有权// Bad* b new Bad();// auto b3 b-getptr(); // 错误b 没有被 shared_ptr 管理会抛出 std::bad_weak_ptrreturn 0;}注意必须在shared_ptr管理对象上调用shared_from_this()否则行为未定义通常抛出异常。## 10. 自定义删除器深入自定义删除器使得智能指针能够管理各种资源不仅仅是堆内存。例如文件句柄、数据库连接、POSIX锁等。### 10.1 unique_ptr 中的删除器unique_ptr的删除器是类型的一部分因此不同的删除器导致不同的类型。这带来了零开销但限制了灵活性。可以使用std::function包装删除器但会引入额外开销。cpp// 函数指针删除器void closeFile(FILE* f) { if (f) fclose(f); }std::unique_ptrFILE, decltype(closeFile) up1(fopen(a.txt, r), closeFile);// lambda 删除器推荐auto deleter [](FILE* f) { if (f) fclose(f); };std::unique_ptrFILE, decltype(deleter) up2(fopen(b.txt, r), deleter);### 10.2 shared_ptr 中的删除器shared_ptr的删除器不参与类型通过类型擦除存储在控制块中。因此可以创建两个shared_ptr指向同一对象但使用不同的删除器只要它们是从同一shared_ptr拷贝而来或者原始指针相同但分别构造会导致独立控制块必须小心。一般不会混用不同的删除器管理同一对象但可以做到。cppauto del1 [](int* p) { std::cout delete1\n; delete p; };auto del2 [](int* p) { std::cout delete2\n; delete p; };std::shared_ptrint sp1(new int(1), del1);std::shared_ptrint sp2(sp1); // 共享控制块删除器仍是 del1// std::shared_ptrint sp3(new int(2), del2); // 独立## 11. 性能考量智能指针虽然方便但并非零开销。理解其开销有助于在性能关键代码中做出明智选择。### 11.1 unique_ptr 的开销unique_ptr在默认删除器下大小与裸指针相同操作也是内联的没有运行时开销。使用无状态自定义删除器时通过空基类优化也能保持大小不变。只有带状态的删除器例如捕获了变量的lambda会增加unique_ptr的大小。### 11.2 shared_ptr 的开销- **内存开销**每个shared_ptr对象本身通常包含两个指针一个指向对象一个指向控制块。控制块包含引用计数强引用计数、弱引用计数、删除器、分配器等。- **时间开销**拷贝、赋值、销毁shared_ptr时涉及原子操作修改引用计数这比非原子操作慢但在多线程环境下是必要的。原子操作可能导致缓存一致性开销。- **make_shared 的优势**一次分配对象和控制块在一起减少了内存分配次数也提高了局部性。但缺点是对象内存可能延迟释放因为控制块可能存活更久如果还有weak_ptr对于大对象可能不是最佳。### 11.3 移动语义优化移动unique_ptr或shared_ptr只是转移指针所有权不修改引用计数因此非常快相当于拷贝指针。在可能的情况下使用移动而不是拷贝可以提升性能。cpp// 通过值传递 unique_ptr 表示转移所有权void sink(std::unique_ptrWidget p) {// 使用 p}auto w std::make_uniqueWidget();sink(std::move(w)); // 移动无开销## 12. 内存池与自定义分配器除了智能指针C还允许通过自定义分配器控制容器的内存分配策略。std::allocator是默认分配器我们可以实现自己的分配器来使用内存池、共享内存等。### 12.1 标准分配器接口分配器必须提供allocate和deallocate成员函数以及一些类型别名。例如cpptemplate typename Tclass MyAllocator {public:using value_type T;MyAllocator() default;T* allocate(std::size_t n) {std::cout Allocating n objects\n;return static_castT*(::operator new(n * sizeof(T)));}void deallocate(T* p, std::size_t n) {std::cout Deallocating n objects\n;::operator delete(p);}// 需要提供相等比较bool operator(const MyAllocator) const { return true; }bool operator!(const MyAllocator) const { return false; }};int main() {std::vectorint, MyAllocatorint vec;vec.push_back(1);vec.push_back(2);return 0;}### 12.2 内存池内存池预先分配一大块内存然后从中切分出小块给对象减少频繁的堆分配开销并减少内存碎片。通常用于大量小对象的场景。实现一个简单的内存池cppclass FixedBlockPool {public:FixedBlockPool(size_t blockSize, size_t blockCount): blockSize_(blockSize), blockCount_(blockCount) {pool_ static_castchar*(::operator new(blockSize * blockCount));// 初始化自由链表for (size_t i 0; i blockCount; i) {char* block pool_ i * blockSize;*reinterpret_castchar**(block) nextFree_;nextFree_ block;}}~FixedBlockPool() {::operator delete(pool_);}void* allocate() {if (!nextFree_) throw std::bad_alloc();void* block nextFree_;nextFree_ *reinterpret_castchar**(nextFree_);return block;}void deallocate(void* p) {if (!p) return;*reinterpret_castchar**(p) nextFree_;nextFree_ static_castchar*(p);}private:size_t blockSize_;size_t blockCount_;char* pool_;char* nextFree_ nullptr;};然后将这个内存池与自定义分配器结合用于容器或智能指针。但智能指针本身不直接支持分配器参数make_shared允许传入分配器C11后如std::allocate_shared。cppFixedBlockPool pool(sizeof(int), 100);std::shared_ptrint sp std::allocate_sharedint(MyAllocatorint(), 42);// 但我们的MyAllocator并未使用pool需要编写与pool关联的分配器。## 13. 智能指针与动态数组### 13.1 unique_ptr 处理数组unique_ptr有数组特化使用std::unique_ptrT[]自动调用delete[]支持下标操作。推荐使用std::make_uniqueT[](size)C14。cppauto arr std::make_uniqueint[](5);for (int i 0; i 5; i) {arr[i] i;}### 13.2 shared_ptr 处理数组在C17之前没有shared_ptrT[]特化需要自定义删除器来调用delete[]。cppstd::shared_ptrint sp(new int[10], std::default_deleteint[]());// 但不能使用下标操作需要 get() 索引C17引入了shared_ptrT[]允许使用下标但make_shared仍不支持数组直到C20。cpp// C17std::shared_ptrint[] sp(new int[10]{0});sp[0] 42; // OK// C20auto sp2 std::make_sharedint[](10); // 默认初始化auto sp3 std::make_sharedint[5](1,2,3,4,5); // 直接初始化### 13.3 使用 std::vector 替代动态数组对于动态数组通常优先考虑std::vector它提供了更丰富的接口且内存管理自动。只有在需要避免vector的开销或与C API交互时才使用原始数组或智能指针数组。## 14. 循环引用及其它陷阱### 14.1 循环引用我们已经在weak_ptr部分看到了循环引用的例子。除了双向链表任何相互持有shared_ptr的图结构都可能导致循环。使用weak_ptr打断循环是关键。### 14.2 使用 shared_ptr 管理 this之前提到的enable_shared_from_this是正确做法。但如果对象被shared_ptr管理但又在外部通过this创建了新的shared_ptr会导致多个控制块。### 14.3 将 shared_ptr 传递给 lambda 时的生命周期如果lambda捕获shared_ptr则lambda会持有该shared_ptr的拷贝延长对象的生命周期直到lambda被销毁。这在异步编程中常见。cppauto sp std::make_sharedint(10);std::thread t([sp]() {// sp 被拷贝到线程中确保对象在线程执行期间存活std::this_thread::sleep_for(std::chrono::seconds(1));std::cout *sp \n;});t.detach(); // 注意detach后如果主线程结束可能对象仍被sp持有直到线程执行完// 但主线程可能先结束导致程序终止所有资源释放### 14.4 智能指针的线程安全shared_ptr的引用计数操作是线程安全的但指向的对象本身不是。多个线程同时修改对象需要同步。此外shared_ptr对象本身如sp变量的修改如赋值、reset也不是线程安全的需要外部同步。## 15. 现代C内存管理最佳实践基于以上讨论总结以下最佳实践1. **避免使用裸 new/delete**除非在实现底层内存分配器或与C库交互否则不要直接使用new和delete。优先使用智能指针或容器。2. **使用 std::make_unique 和 std::make_shared**它们更安全、更高效且避免了直接使用new的异常安全问题。3. **明确所有权**选择正确的智能指针- 独占所有权std::unique_ptr。- 共享所有权std::shared_ptr。- 观察者或打破循环std::weak_ptr。4. **在接口中传递裸指针仅用于非所有权访问**如果函数只是要使用对象而不涉及生命周期管理可以传递裸指针或引用。不要传递从智能指针get()获得的裸指针并期望函数管理生命周期。5. **使用 std::unique_ptr 作为工厂函数的返回类型**这清晰地表明调用者获得对象的所有权。6. **在类内部如果必须持有指针成员考虑使用智能指针**明确成员的所有权语义。对于非拥有指针可以使用原始指针但需确保生命周期。7. **避免循环引用**在设计数据结构时分析哪些指针应该是弱引用。8. **使用 std::enable_shared_from_this 安全地获取 this 的 shared_ptr**。9. **性能敏感处注意开销**shared_ptr的原子操作可能成为瓶颈可考虑使用unique_ptr或侵入式引用计数boost::intrusive_ptr。10. **与C接口交互时小心管理生命周期**可能需要使用自定义删除器调用C释放函数或使用智能指针包装C资源。## 16. 案例研究实现一个简单的对象池结合所学知识实现一个简单的对象池使用unique_ptr自定义删除器将对象归还到池中。cpp#include memory#include vector#include iostreamtemplate typename Tclass ObjectPool {public:using DeleterType std::functionvoid(T*);ObjectPool(std::size_t initSize 10) {for (std::size_t i 0; i initSize; i) {pool_.push_back(new T());}}~ObjectPool() {while (!pool_.empty()) {delete pool_.back();pool_.pop_back();}}// 获取一个对象使用智能指针自动归还std::unique_ptrT, DeleterType acquire() {if (pool_.empty()) {// 扩展池分配新对象T* obj new T();return std::unique_ptrT, DeleterType(obj, [this](T* p) { release(p); });} else {T* obj pool_.back();pool_.pop_back();return std::unique_ptrT, DeleterType(obj, [this](T* p) { release(p); });}}std::size_t size() const { return pool_.size(); }private:void release(T* obj) {// 可以重置对象状态*obj T(); // 假设 T 可赋值pool_.push_back(obj);}std::vectorT* pool_;};struct Widget {int a, b;Widget() : a(0), b(0) { std::cout Widget default ctor\n; }Widget(int x, int y) : a(x), b(y) { std::cout Widget ctor( x , y )\n; }~Widget() { std::cout Widget dtor\n; }};int main() {ObjectPoolWidget pool(2);std::cout Pool size: pool.size() \n;{auto w1 pool.acquire();w1-a 10;w1-b 20;std::cout Acquired w1: w1-a , w1-b \n;auto w2 pool.acquire();std::cout Acquired w2\n;// w1 和 w2 离开作用域时会调用自定义删除器将对象归还池中std::cout Pool size before release: pool.size() \n;}std::cout Pool size after release: pool.size() \n;auto w3 pool.acquire();std::cout Acquired w3, its values: w3-a , w3-b \n; // 应该是重置后的0,0return 0;}输出示例Widget default ctorWidget default ctorPool size: 2Acquired w1: 10,20Acquired w2Pool size before release: 0Widget dtorWidget dtorPool size after release: 2Acquired w3, its values: 0,0Widget dtorWidget dtor注意这里的DeleterType使用了std::function会有额外开销。实际实现可以用无状态的lambda模板参数但为了简化这里使用了类型擦除。## 17. 结论C内存管理经历了从手动new/delete到智能指针的演进极大地减轻了程序员的负担提高了代码的安全性和可维护性。理解底层原理如operator new、引用计数、RAII有助于更好地运用这些工具。现代C推荐尽可能使用智能指针和标准容器将资源管理交给对象生命周期。然而没有银弹智能指针也有其开销和适用场景需要根据实际需求权衡。通过掌握本文剖析的概念和技巧你将能够在C项目中游刃有余地处理内存管理问题写出更健壮、更高效的代码。本文从原始内存管理讲起逐步深入到智能指针的方方面面涵盖了基本用法、原理、性能、陷阱和最佳实践。希望读者能够将这些知识应用到实际开发中充分享受C11/14/17/20带来的便利。