细讲C++ [4]:手写unique_ptr智能指针、仿函数、模板特化与内存泄漏底层详解

📅 2026/7/31 15:55:01
细讲C++ [4]:手写unique_ptr智能指针、仿函数、模板特化与内存泄漏底层详解
一、内存泄漏与裸指针底层缺陷基础铺垫1.1 系统资源与堆资源释放规则#includeiostream using namespace std; #includecstdlib #includeassert.h #includecstdio int main() { FILE* p fopen(test.txt, w);//该裸指针指向的地方就是内核系统资源但你但看这个p你不知道是不是堆资源还是系统资源。 if (nullptr p) { exit(EXIT_FAILURE); } fclose(p);//FILE* p就是一个系统资源那么就要通过fclose这个函数来释放资源不调用这个fclose那么这个资源就会一直占用直到程序结束。 p nullptr; //delete p; }核心知识点程序结束时操作系统会自动回收堆区资源但系统内核资源不会自动回收必须手动调用对应API释放禁止偷懒。FILE* 文件指针属于内核系统资源必须通过fclose释放不能使用delete否则会引发程序异常。资源释放后置空指针是开发规范避免形成野指针、悬空指针。1.2 内存泄漏分类与裸指针致命缺陷int main() { for(int i0; ;i) p new int(20);//千万不要去试这个代码这个代码是占用所有堆区资源直到系统崩溃为止这是内存泄露的一种 //内存泄漏分为两种第一种就是堆内存的泄露就是上面那一种情况 // 另外一种情况就是系统资源创建的泄露例如socket文件描述符你创建了不归还久而久之也会内存泄漏 int* p new int(19); //这种外表裸露*号的指针我们叫做裸指针 //裸指针存在一个问题就是因为数组的特性我们无法区分这个裸指针是指向一个对象还是一组对象 //另外裸指针无法去区分所指向的资源是系统内核资源还是堆区资源以及无法判断该指针是否为空悬指针。 //当你free或delete一个指针之后系统会回收你的堆区资源但是这并不代表你的裸指针已经被置为空了事实上裸指针它仍然指向原来的位置并没有置为空此时这个指针也叫做空悬指针 //这就是为什么一定要delete或free一个指针之后一定要将这个指针置为空。 }核心知识点内存泄漏两类① 堆内存泄漏new不delete、循环new不释放② 系统内核资源泄漏文件、socket、信号量等不主动关闭释放。裸指针四大缺陷无法区分指向单个对象/数组、无法区分堆资源/内核资源、释放后不会自动置空、极易产生悬空指针。开发铁律delete/free释放内存后必须手动将指针置空规避悬空指针风险。1.3 Linux内核常见可泄漏资源汇总Linux系统下常见内核资源创建后必须手动释放否则永久泄漏进程管理资源进程描述符、PID、线程、进程间通信信号量、消息队列、共享内存内存管理资源物理内存页、虚拟地址空间、页表、内存映射文件、虚拟内存区域文件系统资源文件描述符、inode、目录项、文件锁、IO缓冲区设备管理资源字符/块设备、中断控制器、DMA通道、设备驱动资源网络资源套接字、网络缓冲区、路由表、ARP缓存、网络命名空间内核同步资源互斥锁、读写锁、自旋锁、信号量、原子操作、完成量其他资源内核线程、内存池、slab分配器、内核日志资源等二、仿函数函数对象底层原理struct Add {//拥有两个参数所以Add又叫做二元仿函数 int operator()(int a, int b)const { return a b; } }; int main() { Add add; //仿函数的调用方式有两种第一种就是仿函数的调用方式第二种就是仿函数的operator()的调用方式 int x add(12, 23); cout x endl; x add.operator()(12, 24);//这2种形式也叫做二元仿函数 cout x endl; }核心知识点仿函数定义重载了operator()的自定义结构体/类也叫函数对象本质是自定义类型并非普通函数。分类带1个参数为一元仿函数带2个参数为二元仿函数。两种调用方式简洁调用add(a,b)、原生调用add.operator()(a,b)两种效果完全一致。三、模板特化泛型、部分特化、完全特化templateclass T1,class T2//泛型泛型就相当于是个人我就娶过来当老婆 class C { public:C() { cout T1,t2 endl; } }; templateclass T1, class T2 class C T1*,T2*{//部分特化专门处理某一类情况这里专门处理指针部分特化就是只要是一个女人我可以娶过来当老婆 public:C() { cout T1*,t2* endl; } }; template class C const char*,const char * {//完全特化专门出来某一类的某一种情况这里专门处理特定类型的指针指名道姓想什么我就要小海作为我老婆就要她一个人。 public:C() { cout const char*,const char* endl; } }; //需要注意的是使用完全特化以及使用部分特化的时候前提是一定要先写泛型模板然后再写部分特化最后写完全特化顺序不能颠倒否则编译器会报错。 int main() { Cint, int c1; Cint*, int* c2; Cconst int, int c3; Cint[10], int[20] c4; }核心知识点泛型模板通用模板适配所有未知类型是所有特化的基础。部分特化针对某一类类型特化本例针对所有指针类型范围宽泛。完全特化针对具体指定类型精准特化本例仅针对const char*优先级最高。特化优先级完全特化 部分特化 通用泛型模板。编写顺序强制要求必须先通用泛型 → 再部分特化 → 最后完全特化顺序颠倒编译报错。四、自定义Int测试类重载运算符全套实现class Int { private: int value; public: Int(int x 0) :value(x) { cout create Int Object this value endl; } ~Int() { cout delete Int Object this value endl; value -1; } Int(const Int it) :value(it.value) { cout it copy Int Object this value endl; } Int(Int it) :value(it.value) { it.value -1; cout move Int Object this value endl; } Int operator(Int it) { if (this ! it) { this-value it.value; it.value -1; } } Int operator(const Int a) { if (this ! a) this-value a.value; cout this operator (const Int it) a endl; return *this; } void Print()const { cout value endl; } ostream operator (ostream os) const { os value; return os; } istream operator (istream in) { in this-value; return in; } void Set(int x) { value x; } int GetValue()const { return value; } int Value() { return value; } const int Value()const { return value; } Int operator() { this-value 1; return *this;// } Int operator(int) { Int tmp(*this); *this; return tmp; } operator int() const { return this-value; } Int* operator () { return this; } const Int* operator () const { return this; } };核心知识点完整实现类的六大默认函数构造、析构、拷贝构造、移动构造、拷贝赋值、移动赋值。重载输入输出运算符、前置/后置运算符、取地址运算符、int类型隐式转换运算符。提供普通/常量取值接口兼顾读写权限与常量安全性。移动语义实现转移数值后将原对象数值置-1标记资源转移完成。五、自定义删除器仿函数单对象/数组适配//------------------------------------------------------------------------------------------------- templateclass _Ty struct default_deleter {//这种重载括号的函数我们叫做仿函数或者叫函数对象。本质上仍然是一个用户自定义类型 void operator() (_Ty* _Ptr) const {//只有一个参数所以又叫做一元仿函数 delete _Ptr; } }; //------------------------------------------------------------------------------------------------- templateclass _Ty struct default_deleter_Ty[] {//这种重载括号的函数我们叫做仿函数或者叫函数对象。本质上仍然是一个用户自定义类型 void operator() (_Ty* _Ptr) const {//只有一个参数所以又叫做一元仿函数 delete []_Ptr; } };核心知识点删除器本质是一元仿函数重载operator()实现资源释放。模板特化区分释放规则普通类型走delete单对象释放数组类型走delete[]批量释放杜绝内存释放出错。为后续unique_ptr提供通用、可适配的内存释放策略解耦指针与释放逻辑。六、手写my_unique_ptr智能指针单对象版本templateclass _Ty, class _Dx default_deleter_Ty//模板里面可以给上默认值第二个参数就是默认值为default_deleter类型但它本身就是一个模板所以这里是模板套模板。 //我们写的是唯一性智能指针必须是一对一操作一个指针只能指向一个对象。 class my_unique_ptr { public: using pointer _Ty*; using element_type _Ty; using deleter_type _Dx; private: pointer mPtr; deleter_type mDeleter; //mDeleter(mptr); public: deleter_type get_deleter() const { return mDeleter; } operator bool() const {return mPtr ! nullptr; } void reset(pointer ptr nullptr) { mDeleter(mPtr);//因为设计的逻辑就是一个智能指针只能指向一个内存空间所以当你要去指向其他内存空间的时候原来指向的那一块内存空间就没有人去指向了所以要直接释放掉它。 mPtr ptr; } pointer release() { pointer oldptr mPtr; mPtr nullptr; return oldptr;//释放资源。 } void swap(my_unique_ptr other) { if (other ! this) { std::swap( this-mPtr,other.mPtr); } } pointer get()const { return mPtr; } public: my_unique_ptr(pointer p nullptr):mPtr(p),mDeleter(){ cout构造函数 endl; }//构造函数; ~my_unique_ptr() { reset(mPtr); cout 析构函数 endl; } //my_unique_ptr本质上是一个类对象当我们要把它当作指针使用的时候我们要去进行重载*和-运算符 _Ty operator*() { return *get(); }//这里返回的是对象解引用返回的是对象层面对于自定义类型他不会去调用自定义类型里面的值或函数停留在整体层面。 _Ty* operator-() { return get(); }//这里可以访问对象的值或者是函数所以我们我们要返回的是地址。 _Ty operator[](size_t index) { return get()[index]; }//相当于返回的是reutrn mPtr[indx]; my_unique_ptr(const my_unique_ptr other) delete;//不允许拷贝构造(),将拷贝构造函数删除因为不可以一个共享指针指向两个相同区域。 my_unique_ptr(my_unique_ptr other) { if (other ! this) { mPtr other.mPtr; other.mPtr nullptr; } } my_unique_ptr operator(const my_unique_ptr other) delete;//当然也不允许被赋值 my_unique_ptr operator(my_unique_ptr other) { if (other ! this) { this-reset(other.release()); } return *this; } };核心知识点唯一性核心删除拷贝构造、拷贝赋值禁止多指针共享同一块内存仅支持移动语义保证一对一独占内存。核心接口get()获取原生裸指针release()释放内存管理权返回裸指针自身置空reset()释放原有内存绑定新内存swap()交换两个智能指针的内存资源。指针模拟重载重载*、-、[]让类对象可以像原生指针一样使用。隐式bool转换可直接通过if(智能指针对象)判断指针是否非空。模板嵌套模板参数嵌套删除器模板实现通用内存释放适配。6.1 单对象unique_ptr测试用例底层调用逻辑int main() { my_unique_ptrInt pa(new Int(10)); Int* p new Int(10); p-Print(); if (pa) { //pa.访问的是智能指针里面的成员 pa-Print();//他访问的是他存储指针所指向的对象里面的成员不是智能指针里面的成员 //为什么不是pa--Print()这样子的写法呢 //1. 首先调用 pa.operator-() //2. 因为 operator-() 返回的是 Int* 指针 //3. 然后在这个指针上应用 - 运算符 //4. 相当于 (pa.operator-())-Print() //operator-的特殊设计允许智能指针像普通指针一样使用不需要额外的-。这是C语言的一个特性使得智能指针的使用更加自然和直观。 (*pa).Print();//同上面一样的写法。 } return 0; }核心知识点operator- 链式调用特性C语法特殊优化自动链式解析无需重复写-简化智能指针调用。智能指针pa.xxx访问自身成员pa-xxx访问指向的堆对象成员。if(pa)依靠隐式bool转换快速判断智能指针是否持有有效内存。七、手写my_unique_ptr数组特化版本数组专属templateclass _Ty, class _Dx class my_unique_ptr _Ty[], _Dx { public: using pointer _Ty*; using element_type _Ty; using deleter_type _Dx; private: pointer mPtr; deleter_type mDeleter; // mDeleter(mPtr); public: deleter_type get_deleter() const { return mDeleter; } operator bool() const { return mPtr ! nullptr; } pointer release() { pointer oldptr mPtr; mPtr nullptr; return oldptr; } void reset(pointer ptr nullptr) { mDeleter(mPtr); // delete ; free; nullptr; mPtr ptr; } void swap(my_unique_ptr other) { std::swap(this-mPtr, other.mPtr); } pointer get() const { return mPtr; } public: my_unique_ptr(pointer p nullptr) :mPtr(p) {} ~my_unique_ptr() { reset(); } _Ty operator*()const { return *get(); } //return mPtr; _Ty* operator-() const { return get(); } // 有没有错误 _Ty operator[](const int index) const { assert(index 0); int size *((int*)mPtr - 1);//特殊写法。 //sizeof堆区指针返回的是指针的大小而不是指针指向的堆区内存块的大小。 //为什么可以这么操作获取内存块的大小这是因为 /* 在许多内存分配实现中分配器会在实际分配给用户的内存块之前存储一些额外的控制信息包括内存块的大小、分配状态等。这种设计是为了让分配器能够正确地管理内存并在释放时知道需要释放多少内存。 具体来说 当调用 new 或 malloc 分配内存时分配器会在实际返回给用户的内存块之前分配一些额外的空间通常是一个或多个机器字的大小用于存储控制信息。 在这个控制信息区域中通常会存储内存块的大小这样在调用 delete 或 free 时分配器就知道需要释放多少内存。 分配器返回给用户的指针即 mPtr指向的是实际可用的内存块的起始地址而不是整个分配区域包括控制信息的起始地址。 因此要获取内存块的大小我们需要回退一个 int 大小的空间即 mPtr - 1然后解引用这个位置获取存储的大小值。*/ assert(index size); return get()[index]; // return mPtr[index]; } my_unique_ptr(const my_unique_ptr) delete; //c11; my_unique_ptr operator(const my_unique_ptr) delete; my_unique_ptr(my_unique_ptr other) :mPtr(other.mPtr) { other.mPtr nullptr; } my_unique_ptr operator(my_unique_ptr other) { if (this ! other) { this-reset(other.release()); } return *this; } };核心知识点基于模板部分特化专门适配数组类型智能指针自动匹配delete[]释放规则。堆内存大小获取原理堆内存分配器会在用户可用内存前预留控制信息区存储内存大小通过*(int*)(mPtr-1)可读取数组开辟长度。重载[]运算符增加下标越界断言校验比原生裸指针更安全。延续unique_ptr唯一性特性禁止拷贝仅支持移动语义独占数组内存资源。sizeof(堆指针)只能获取指针本身大小无法获取堆内存块大小必须依赖分配器预留的控制信息。7.1 数组unique_ptr完整测试用例原生运行日志int main() { my_unique_ptrInt[] pa(new Int[10]); /*create Int Object008CC4B4 0 create Int Object008CC4B8 0 create Int Object008CC4BC 0 create Int Object008CC4C0 0 create Int Object008CC4C4 0 create Int Object008CC4C8 0 create Int Object008CC4CC 0 create Int Object008CC4D0 0 create Int Object008CC4D4 0 create Int Object008CC4D8 0*///刚开始都初始化为0. for (int i 0; i 10; i) { pa[i].Value() i 100; } int index; while (cin index, index ! -1) { pa[index].Print(); } return 0; }核心知识点数组对象默认初始化new 创建自定义类型数组时会默认调用无参构造所有对象初始值为0。通过[]下标遍历修改数组内每个Int对象的数值支持动态读写。循环输入下标查询数据输入-1终止程序封装完整的数组智能指针使用流程。程序结束自动调用unique_ptr析构通过delete[]批量释放数组内存杜绝内存泄漏。八、后缀补充C11类型别名新特性typedef unsigned int u_int;//类型转换 using u_int32_t u_int;//是c加加11l里面新增的类型转换,需要注意的是新的名字和原来名字的相对顺序变反了。核心知识点typedef传统类型别名语法为typedef 原类型 新类型。usingC11新增类型别名语法为新类型 原类型顺序与typedef相反可读性更强支持模板别名。九、全文核心总结裸指针存在诸多安全缺陷智能指针通过RAII机制自动管理内存生命周期彻底解决内存泄漏、悬空指针问题。unique_ptr核心是独占语义禁止拷贝、仅支持移动适配单对象/数组两种内存场景。仿函数是C通用策略核心通过重载()实现通用逻辑封装适配删除器、算法回调等场景。模板特化实现类型精准适配通用模板兜底、特化精准优化是泛型编程的核心思想。堆内存与系统资源区分管理堆内存程序结束系统自动回收内核资源必须手动释放。