电商型网站建设,莱州网站设计,临安市住房和建设局网站,做网站的感觉前言
C当中的内存管理机制需要我们自己来进行控制#xff0c;比如 在堆上 new 了一块空间#xff0c;那么当这块空间不需要再使用的时候。我们需要手动 delete 掉这块空间#xff0c;我们不可能每一次都会记得#xff0c;而且在很大的项目程序当中#xff0c;造成内存泄漏…前言
C当中的内存管理机制需要我们自己来进行控制比如 在堆上 new 了一块空间那么当这块空间不需要再使用的时候。我们需要手动 delete 掉这块空间我们不可能每一次都会记得而且在很大的项目程序当中造成内存泄漏也是不少了。
C 不像 Java一样有 gc也就是垃圾回收站器因为 Java 在操作系统之上还有一层虚拟机这层虚拟机可以理解为运行的一个进程所有的Java 程序都是在这个 虚拟机 之上运行的。在虚拟机当中就有 这个 gc 在运作。
gc 简单来说就是把所有动态开辟都记录起来当不需要使用的时候就自动释放了。
但是 Java 在实现这个 虚拟机 是有消耗的所以在特别需要效率的项目上很多都用的是 C比如在游戏开发服务器当中。
而且在C 当中 我们自己控制 delete 空间的话就算是我们想起来 要控制程序当中设计得复杂我们都不能完全的控制比如下述
pairint, int* pa new pairint,int;func();delete pa;
如果没有 func函数的话那么最后肯定是能释放掉的但是现在有一个不确定因素在 func函数当中我们不清楚在func函数当中到底做了什么我们就不能 100% 没问题。假设在func当中抛异常被主函数当中捕获了那么就有可能会直接跳过这个 delete。
虽然抛异常的跳过的问题我们可以 先 delete 需要释放的空间然后再抛出异常解决很多场景。但是有一个场景是不能解决的
new 也是可能会抛出异常的虽然概率很低但是也是有可能的那么下面的场景 如果 p1 的new 抛出异常那么还好没有什么问题如果是 p2 抛出异常的话那么 p1 开的空间应该怎么解决呢
如果此时我们再把 func函数加上
pairint, int* p1 new pairint,int;
pairint, int* p2 new pairint,int;try{func();
}catch(...){delete p1;delete p2;throw ....................
}delete pa; 如果 func函数抛出异常那么就要 delete p1 和 p2。
那如果还不嫌麻烦如果在多来几个呢 p3 p4 p5 p6 ····························
基于上述问题就搞出了 智能指针。
智能指针 智能指针虽然听上去很高端但实际很简单。
就更之前在 各种库当中实现的迭代器是一样把指针包装了一下在其中实现各个函数比如指针需要的 operator*() , operator-() 函数等等最重要的是实现 ~析构函数他是我们自动 释放空间的核心因为在创建类的对象之后编译器会自动的调用这个对象的析构函数用于析构这个对象和对象空间。
templateclass T
class SmartPtr
{
public:// RAII// 资源交给对象管理对象生命周期内资源有效对象生命周期到了释放资源// 1、RAII管控资源释放// 2、像指针一样SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout delete: _ptr endl;delete _ptr;}
private:T* _ptr;
}; 这样的话只需要用这个指针构造 一个指针指针对象那么就会自动维护这个指针
pairint, int* p1 new pairint,int;
SmartPtr sp1(p1);
此时p1 指针维护的空间我就不需要手动释放了自己就会释放。
当然上述的实现是分开的我们可以不用 p1 传进去直接把 new pairint,int; 当参数传进去就行因为 new 本身返回的就是 这个 空间的指针。 SmartPtrpairstring, string sp2(new pairstring, string);
SmartPtrpairstring, string sp3(new pairstring, string); 所以说虽然 C 没有 gc 但是有智能指针我们可以自己实现自己实现的智能指针没有什么问题的话这个智能指针是可以帮助我们解决绝大部分的内存泄漏问题的异常也不用怕了。 有人把这种机制叫做 RAII。
RAII RAIIResource Acquisition Is Initialization是一种利用对象生命周期来控制程序资源如内存、文件句柄、网络连接、互斥量等等的简单技术。 在对象构造时获取资源接着控制对资源的访问使之在对象的生命周期内始终保持有效最后在对象析构的时候释放资源。
我们实际上把管理一份资源的责任托管给了一个对象。对象是给编译器进行管理。也就是利用了对象无论以什么方式离开了作用域都会调用其析构函数的特点。
也就是获取到资源的时候马上初始化。
这样做的好处是
不需要显式地释放资源。采用这种方式对象所需的资源在其生命期内始终保持有效。 智能指针像指针一样使用 其实上述也说过了智能指针的构造函数是利用指针来构造这个智能指针的对象析构函数是释放这个指针指向的空间。
那么指针的使用还有 operator*() operator-() operator[]() 这样子的函数要支持。
实现就更迭代器的实现是一样的可读可写。
templateclass T
class SmartPtr
{
public:// RAII// 资源交给对象管理对象生命周期内资源有效对象生命周期到了释放资源// 1、RAII管控资源释放// 2、像指针一样SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout delete: _ptr endl;delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}
private:T* _ptr;
};
智能指针之间的赋值问题拷贝问题 如下面两个智能指针 SmartPtrstring sp1(new string(xxxxx));SmartPtrstring sp2(new string(yyyyy));
我们把两个指针 delete 的地址打印一下输出 两个空间都释放了。
但是如果上述的两个智能指针对象进行相互赋值操作的话就会出现问题 SmartPtrstring sp1(new string(xxxxx));SmartPtrstring sp2(new string(yyyyy));sp1 sp2;
此时输出 发现两次释放的是一个空间这就出问题了啊。
首先是对一块空间析构了两次我们说对同一块空间析构两次是可能会出现问题的。
其次是原本是两个智能指针维护的是两块空间但是现在我们只是放了一块另一块空间没有释放就造成了内存泄漏。
其实是因为我们没有写 拷贝构造函数如果我们没有写拷贝构造函数的话编译器就会自己实现一个 默认的浅拷贝的 拷贝构造函数。按照之前我们应该自己用现代写法来实现了一下 拷贝构造函数。
但是在智能指针当中我们就不能 用上述深拷贝的方式来解决因为我们写的智能指针本质上就是要模拟实现指针来实现的。而原生指针在赋值这一块本来就是浅拷贝。 auto_ptr 为了解决上述所说的 内存管理问题在C98 当中就提出了 auto_ptr 。它可以解决上述的问题但是这个 auto_ptr 有一个非常大的坑具体请看下述 为了演示 到底是没有释放空间我们需要创建一个类来帮助我们因为 如果是我们自己实现的智能指针那么我们可以在 析构的时候打印一下来提示我们此时发生了空间的释放但是在库当中的实现的 auto_prt 我们没办法打印但是 delete 一个对象就调用这个对象的析构函数所以我们实现一个对象的析构函数在其中打印这个 以下提示我们就行
class A
{
public:A(int a 1):_a(a){cout A(int a 1) endl;}~A(){cout ~A() endl;}private:int _a;
};
如何使用 auto_ptr 呢如下所示
std::auto_ptrA sp1(new A(1));
auto_ptr 是一个模版类模版参数只需要给出 需要管理的类型即可不需要给出指针如上述的 int 。auto_ptr 的构造函数可以直接传入这个 需要管理的空间的 地址像上述我们就直接使用 new 开空间后返回这个空间的地址来实现。 输出 发现没有显示释放这个空间我们都释放了这个空间。 如果我们进行像上述在拷贝问题当中实现的赋值一样的如下代码所示 auto_ptrA aptr1(new A(1));auto_ptrA aptr2(new A(1));aptr1 aptr2;
输出
A(int a 1)
A(int a 1)
~A()
~A()
发现它也正常释放了。 我们要发现他的问题就得知道他做了什么如下例子阐述 如上述所示我们用 ap1 构造了 ap3也是相当于是 赋值了那么此时发生了什么呢
其实 auto_ptr 指针在赋值这一块做了一件事情 ----- 管理权转移。 如上述例子他把 原本属于 ap1 的空间转给了 赋值之后的 ap3 管理了所以我们看到上述的 ap1 指向的是 empty。 在 auto_ptr 当中的 拷贝构造函数就相当于是实现了 移动构造的玩法直接交换 ap1 和 ap3 当中的指针变量把 ap1 指向 ap3 当中的空把 ap3 当中的指针指向 ap1 原本维护的指针。 但是只是相当于这里并不是移动构造的玩法移动构造是把 右值 当中的 将亡值即将释放的值把其中的资源直接转移到 新的 值 来维护。如果不是 将亡值 比如是 左值我们是不敢直接转移的因为 此时左值在后序程序当中很有可能会用到。 但是 auto_ptr 当中也就相当于是 强制进行 移动构造不管是左值还是右值这么左的风险在上述也说过了ap1 是左值在后序是很有可能会用到了这就是它最大的问题。 举例
如果是不懂的人对 赋值之后的 ap1 进行操作的话编译器甚至都不会报错但是一运行程序就会出事 auto_ptrA ap1(new A(1));auto_ptrA ap2(new A(1));auto_ptrA ap3(ap1);// 此时把 _a 成员变量修改为了 publicap1-_a;ap2-_a; 成功生成解决方案 出事儿 auto_ptr 其实是一段失败的代码但是已经有人使用 autp_ptr 写了项目委员会不敢删所以延续至今但是在一般实践公司当中明确规定不能使用 auto_ptr 。 auto_ptr 模拟实现
// C98 管理权转移 auto_ptr
namespace bit
{templateclass Tclass auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptrT sp):_ptr(sp._ptr){// 管理权转移sp._ptr nullptr;// 一定要置空// 不然两个对象管理同一块空间// 在析构函数释放的时候就会释放两次// 就会报错}auto_ptrT operator(auto_ptrT ap){// 检测是否为自己给自己赋值if (this ! ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr ap._ptr;ap._ptr NULL;}return *this;}~auto_ptr(){if (_ptr){cout delete: _ptr endl;delete _ptr;}}// 像指针一样使用T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;};
} unique_ptr 在C11 当中提供更加稳定的 unique_ptr 智能指针。
cplusplus.com/reference/memory/unique_ptr/ unique_ptr 的解决方案的话非常的简单粗暴就是直接不允许你进行 unique_ptr 智能指针之间的赋值拷贝。 unique_ptrA up1(new A(1));unique_ptrA up2(new A(1));up1 up2; 编译报错 error C2280: “std::unique_ptrA,std::default_deleteA std::unique_ptrA,std::default_deleteA::operator (const std::unique_ptrA,std::default_deleteA )”: 尝试引用已删除的函数 unique_ptr的模拟实现 对于 unique_ptr 的模拟实现很简单我们在类当中说过要想某一个函数不给外部使用右里那个三种方式第一种是只声明不实现第二种是 把这个函数用 private 修饰第三种是在函数之后写上 delete 意思就是把这个函数删除掉。
但是第一种值声明不实现别人可以在外部给你实现这个函数所以建议用后面的两种方法。
templateclass T
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout delete: _ptr endl;delete _ptr;}}// 像指针一样使用T operator*(){return *_ptr;}T* operator-(){return _ptr;}unique_ptr(const unique_ptrTsp) delete;unique_ptrT operator(const unique_ptrTsp) delete;private:T* _ptr;
};
std::shared_ptr
当然上述的 unique_ptr 版本的智能指针只是给我们手撕智能指针的模版我们手撕一个 unique_ptr 非常简单。
但是 unique_ptr 比较不能解决 指针 赋值的问题。
所以就有了 shared_ptr 智能指针的出现。 shared_ptrA sp1(new A(1));shared_ptrA sp2(new A(1));sp1 sp2;sp1-_a;sp2-_a;cout sp1-_a endl;cout sp2-_a endl; 输出
A(int a 1)
A(int a 1)
~A()
3
3
~A()
shared_ptr 模拟实现 解决指针拷贝问题 上述几种不同的 智能指针不同就不同在 拷贝构造函数 和 operator() 两个函数的实现不同
所以我们主要实现 shared_ptr 的拷贝构造函数operator当中的 可以服用拷贝构造函数。
用一个shared_ptr 指针给另一个 shared_ptr 赋值和 拷贝构造其中肯定是要 两个 智能指针管理两块空间的主要是要解决 两个对象析构两次会报错的问题。
我们选择使用 引用计数的方式来解决:
也就是记录一下有多少 引用 引用了当前空间。在 某一个 智能指针 对象析构的时候不先释放空间先判断 当前引用计数是不是 0 如果是 大于 0的说明当前 不止当前智能指针 指向这块空间那么就 --引用计数如果当前引用计数 是 0 那么说明当前就本 智能指针 指向这块空间就可以 释放这块空间。 但是在类当中 应该使用什么 成员变量来 记录这个 引用计数呢普通的变量可能是不能满足的如果是 非静态的变量每一个是存储在各个类当中的我们要实现统一计数的话非常的麻烦。
所以我们使用静态的成员变量来存储这个 引用计数因为 静态的成员变量不单单属于某一个对象而是属于这个整个类你可以理解为静态的成员变量是属于每一个对象的每一个对象共用一个 静态成员变量。 类似这样 代码实现
public// 构造函数shared_ptr(T* ptr nullptr):_ptr(ptr),_pcount(new int(1)){}// 析构函数~shared_ptr(){// -- 引用计数 之后 如果 0 就可以释放空间了if (--(*_pcount) 0){cout delete: _ptr endl;delete _ptr;delete _pcount;}}// 拷贝构造函数shared_ptr(const shared_ptrT sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount);}// operator() 函数实现shared_ptrT operator(const shared_ptrT sp){// 先判断 赋值和被赋值的两个指针是否是重复的// 是就直接返回if (_ptr sp._ptr)return *this;// 判断当前 赋值指针在赋值出去之前// 是否是所维护空间的唯一指针if (--(*_pcount) 0){delete _ptr;delete _pcount;}// 开始赋值_ptr sp._ptr;_pcount sp._pcount;// 引用计数(*_pcount);return *this;}private:T* _ptr;int* _pcount;};
如上所示operator函数才是最难实现的我们要判断当前对象也就是被赋值 的对象 在当前赋值之前是否 右引用其他对象如果引用了其他的对象那么要先 -- 引用计数看-- 之后的引用计数是否 0如果是 0 就要把原有的 空间给释放掉然后才能进行赋值操作赋值就简单了直接把 赋值对象 当中的 两个成员赋值过来然后在 引用计数即可。 而且还需要注意自己给自己赋值的情况比如 sp1 和 sp2 都指向了 同一块空间那么对于 sp1 sp1 和 sp2 sp1 两者实际上都是自己给自己赋值在上述的代码当中不进行判断的话是没有什么问题的但是在上述判断之前空间和赋值新的空间的操作都是多余做的了所以我们可以 在函数的开口就判断是不是自己给自己赋值的情况。
如果 有一个 sp3 委会一块空间这块空间没有被其他指针来维护的话 假设现在有 sp3 sp3 这样的赋值的话如果没有特殊判断来终止赋值的话sp3 维护的资源 就会在 operator 当中 被释放掉然后在把 sp3 赋值给 sp3 的话就是一个已经被释放了的空间地址当我们再次使用这个 sp3 的时候就会出问题。
可以利用 资源来判断是不是 同一块空间也可以用 计数来判定也是可以的
// 判断当前对象当中的原生指针指向的空间是否和
// 当前要赋值的指针指向的空间是相同的
if(_ptr sp._ptr_return *this; shared_ptr 完整代码实现
templateclass Tclass shared_ptr{public:// RAII// 像指针一样shared_ptr(T* ptr nullptr):_ptr(ptr),_pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) 0){cout delete: _ptr endl;delete _ptr;delete _pcount;}}T operator*(){return *_ptr;}T* operator-(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptrT sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount);}// sp1 sp5// sp6 sp6// sp4 sp5shared_ptrT operator(const shared_ptrT sp){// 先判断 赋值和被赋值的两个指针是否是重复的// 是就直接返回if (_ptr sp._ptr)return *this;// 判断当前 赋值指针在赋值出去之前// 是否是所维护空间的唯一指针if (--(*_pcount) 0){delete _ptr;delete _pcount;}// 开始赋值_ptr sp._ptr;_pcount sp._pcount;// 引用计数(*_pcount);return *this;}// 返回引用计数int use_count() const{return *_pcount;}// 拿到原生指针T* get() const{return _ptr;}private:T* _ptr;int* _pcount;}; 循环引用问题用 weak_ptr 指针 shared_ptr 指针几乎没缺点但是也不是意味着 完全没有缺点的如下所示
struct Node
{A _val;Node* _next;Node* _prev;
};int main()
{shared_ptrNode sp1(new Node);shared_ptrNode sp2(new Node);sp1-_next sp2;sp2-_prev sp1;return 0;
}
向上述的结点的指针链接 是非常 常规的操作但是上述的操作就有问题虽然上述能够自动释放空间但是 以为 _next 和 _prev 两个指针的类型但是 Node*不是 shared_ptr 智能指针类型的所以这里是经典的类型不匹配。
有人就想到把 _next 和 _prev 的指针类型改为 shared_ptrNode 的不就行了吗确实可以
struct Node
{A _val;shared_ptrNode _next;shared_ptrNode _prev;
};
但是当我们把调用的函数都输出一遍输出 只调用了 构造函数但是 析构函数是没有 调用的也就是说此时发生了 内存泄漏而且我们惊讶的发现当只调用 sp1-_next sp2; 的时候就没有问题正常释放两个空间当中的内容
A(int a 1)
A(int a 1)
~A()
~A() 所以问题就出在 sp1-_next sp2; sp2-_prev sp1; 这两句当中当中和我们先把两个结点 链接关系画一下 如上述所示因为在 sp1._next 和 sp2._prev 各自都链接上了 对方的结点空间所以 两个结点空间的 引用计数 静态变量 就会 现在两个空间的 静态空间变量都是 2 了。
当主函数当中执行完毕之后因为 sp1 比 sp2 先声明所以 sp2 要先析构sp2 析构sp2 空间上的 引用计数就 -- 到1然后 sp1 析构也是一样的过程sp2 和 sp1 析构之后如下所示 此时就出现了 循环引用 的场景 _prev 是在 sp2 这个空间当中的 _next 是在 sp1 这个空间当中的那么此时 不管谁先释放了谁都不愿先调用自己的析构函数因为
如果想要析构 _prev 那么就要析构 第二个结点空间 的时候才会析构到 _prev 但是要想析构到 第二个结点空间就得先析构 _next 所在的第一个结点空间而第一个 结点所在空间要析构又要 _prev 先析构这不就死锁了吗 两个都不能先析构那么就不能析构了。 循环引用的场景介绍
在外部有两个 智能指针维护这两个不同的空间在这两个空间当中又各自有一个指针你的指针管理着我的空间我的指针管理着你的空间。 所以为了解决循环引用问题专门写了一个指针叫做 weak_ptr ,
weak_ptr 不是智能指针而是 为了专门解决 循环引用问题而专门写出来的一个 指针。 所以我们发现在官方库当中 weak_ptr 都没有 指针类型传参的构造函数 weak_ptr 实现就是不使用 引用计数不参与资源释放的管理但是可以访问其中的资源。 如果上述的 sp1 和 sp2 当中的空间都没有使用引用计数来管理的话当 第一步 sp2 和 sp1 释放的时候就会直接把这两个空间给释放了。就没有有后续 _next 和 _prev 两个指针的事情了。 所以weak_ptr 只是拿到智能指针当中的 指针可以访问这个原生指针但是对于这个指针维护的空间和引用计数 我 weak_ptr 是完全不管的我值管访问对于空间的维护是其他 智能指针管理的。
而且虽然 weak_ptr 不支持用原生指针来构造对象但是支持用 shared_ptr 智能指针来构造对象所以我们可像下面一样写
struct Node
{A _val;weak_ptrNode _next;weak_ptrNode _prev;
};int main()
{shared_ptrNode sp1(new Node);shared_ptrNode sp2(new Node);sp1-_next sp2;sp2-_prev sp1;return 0;
}
输出
A(int a 1)
A(int a 1)
~A()
~A() weak_ptr 完整代码
templateclass Tclass weak_ptr{public:weak_ptr():_ptr(nullptr){}// 下述的拷贝构造函数 和 operator函数// 都不管其中的 空间释放 引用计数等等weak_ptr(const shared_ptrT sp):_ptr(sp.get()){}weak_ptrT operator(const shared_ptrT sp){_ptr sp.get();return *this;}// 下述是指针操作T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;// 不使用引用计数};
} boost库简介 Boost库_百度百科 (baidu.com)
他其实是在 C11 出来之前由 C委员会当中的一些成员创建了 Boost 库社区在这个社区当中探索出了很多 有用的语法比如右值引用等等。
在 boost 库当中也诞生了 更好的 智能指针 C11 当中相当于是沿用了 boost 库当中的一些智能指针进行了一些细节上的修改基本上属于是 cv了。