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

一、内存泄漏与裸指针底层缺陷(基础铺垫)

1.1 系统资源与堆资源释放规则

#include<iostream> using namespace std; #include<cstdlib> #include<assert.h> #include<cstdio> 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 i=0; ;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),两种效果完全一致。

三、模板特化:泛型、部分特化、完全特化

template<class T1,class T2>//泛型,泛型就相当于是个人我就娶过来当老婆 class C { public:C() { cout << "T1,t2" <<endl; } }; template<class 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() { C<int, int> c1; C<int*, int*> c2; C<const int, int> c3; C<int[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,标记资源转移完成。

五、自定义删除器仿函数(单对象/数组适配)

//------------------------------------------------------------------------------------------------- template<class _Ty> struct default_deleter {//这种重载括号的函数我们叫做仿函数,或者叫函数对象。本质上仍然是一个用户自定义类型 void operator() (_Ty* _Ptr) const {//只有一个参数,所以又叫做一元仿函数 delete _Ptr; } }; //------------------------------------------------------------------------------------------------- template<class _Ty> struct default_deleter<_Ty[]> {//这种重载括号的函数我们叫做仿函数,或者叫函数对象。本质上仍然是一个用户自定义类型 void operator() (_Ty* _Ptr) const {//只有一个参数,所以又叫做一元仿函数 delete []_Ptr; } };

核心知识点

  • 删除器本质是一元仿函数,重载operator()实现资源释放。

  • 模板特化区分释放规则:普通类型走delete单对象释放,数组类型走delete[]批量释放,杜绝内存释放出错。

  • 为后续unique_ptr提供通用、可适配的内存释放策略,解耦指针与释放逻辑。

六、手写my_unique_ptr智能指针(单对象版本)

template<class _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_ptr<Int> 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数组特化版本(数组专属)

template<class _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; //c++11; 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_ptr<Int[]> 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[]批量释放数组内存,杜绝内存泄漏。

八、后缀补充:C++11类型别名新特性

typedef unsigned int u_int;//类型转换 using u_int32_t = u_int;//是c加加11l里面新增的类型转换,需要注意的是新的名字和原来名字的相对顺序变反了。

核心知识点

  • typedef:传统类型别名,语法为typedef 原类型 新类型

  • using:C++11新增类型别名,语法为新类型 = 原类型,顺序与typedef相反,可读性更强,支持模板别名。

九、全文核心总结

  • 裸指针存在诸多安全缺陷,智能指针通过RAII机制,自动管理内存生命周期,彻底解决内存泄漏、悬空指针问题。

  • unique_ptr核心是独占语义,禁止拷贝、仅支持移动,适配单对象/数组两种内存场景。

  • 仿函数是C++通用策略核心,通过重载()实现通用逻辑封装,适配删除器、算法回调等场景。

  • 模板特化实现类型精准适配,通用模板兜底、特化精准优化,是泛型编程的核心思想。

  • 堆内存与系统资源区分管理,堆内存程序结束系统自动回收,内核资源必须手动释放。