浅谈C++普通指针和智能指针管理动态内存的陷阱
来源:互联网 发布:安卓版看图软件 编辑:程序博客网 时间:2024/06/03 15:47
浅谈C++普通指针和智能指针管理动态内存的陷阱
前言:
C++中动态内存的管理主要是使用new/delete表达式和std::allcator类。为了管理动态内存更加安全,C++11新标准库推出了智能指针。这里只讨论使用他们在使用过程常见的错误以及解决方法,不过多讨论语法。
一、使用new和delete管理动态内存三个常见的问题。
1、忘记释放(delete)内存。忘记释放动态内存会导致人们常说的 “内存泄漏(memory leak)” 问题 ,因为这种内存永远不可能归还系统,除非程序退出。比如在某个作用域的代码如下:向系统申请了一块内存,离开作用域之前没有接管用户这块内存,也没有释放这块内存。
{ //.... int *p = new int(0); //.... }有两个方法可以避免以上问题:
(1) 在p离开它new所在作用域之前,释放这块内存。如:delete p
{ //.... int *p = new int(0); //.... delete p; //释放p的向系统申请的内存 p = nullptr; //尽管在这个地方没必要,这是一个好习惯,也是动态管理内存常见的出错的地方。等下会说到。 }
(2) 接管p的向系统申请的内存。 比如通过赋值,函数返回值等。
int *pAnother; { //.... int *p = new int(0); //.... pAnother = p; //pAnother接管p所指向的内存。 } //pAnother do something delete pAnother; //通关pAnother,将p所申请的内存归还系统。2、使用已经释放内存的对象。这种行为是未定义的,通过在释放内存后将指针设置位空指针(nullptr),有时可以避免这个问题(这是基于一个前提条件,使用动态分配内存对象前,需要检查该对象是否指向空(nullptr))。假如不对已经释放内存的对象赋值空指针,他的值是未定义的,就好比其他变量,使用未初始化的对象,其行为大都是未定义。
note: nullptr(C++11刚引入)是一种特殊类型的字面值,它可以被转换成任何其他指针类型。过去程序使用NULL的预处理变量来给指针赋值。 他们的值都是0。
使用已经释放内存的对象,如下代码:
{ //.... int *p = new int(0); // p do something delete p; //do other thing... std::cout<<*p<<std::endl; //*p的值是未定义 //.... }避免以上问题:(对已经释放内存对象赋于一个空指针,使用前进行判断是否为空指针)
{ //.... int *p = new int(0); // p do something delete p; //下面三条语句等价 p = nullptr; //p = NULL; //p = 0; //do other thing... if(p!=nullptr) //等价if(p) std::cout<<*p<<std::endl; //.... }note: 同样当我们定义一个指针时,如果没有立即为它分配内存,也需要将指针设置为空指针,防止不恰当使用。这里也涉及一个问题,new出来的内存也应该初始化,稍后再讲。
3、同一块内存释放两次。 当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个对象进行了delete操作,对象的内存就归还给系统,如果我们随后有delete第二个指针,堆空间可能被破坏。
产生问题代码:
int *pAnother; { //.... int *p = new int(0); pAnother =p; //p do something.... delete p; } delete pAnother; //未定义行为
避免这个问题:在delete p 之后, 将p置为一个空指针。
其次明白一个道理:delete p, p 必须指向一个空指针或者动态分配的内存,否则其行为未定义。
note: 这也很好就解释了为什么delete一个对象之后需要将该对象置为空指针,一是为了避免再次访问它出现未定义行为,二是为了避免再次delete它出现未定义行为。
小结:
1、定义一个指针需要初始化为空指针,(除非在定义的时候给它申请一块内存)
2、访问一个指针需要先判断该指针是否为空指针。
3、 释放一个指针之后,应该将它置为空指针。
二、使用std::allocator类管理动态内存
在继续了解标准库std::allocator类管理动态内存之前,有必要先了解new和delete具体工作(机制)。
new完成的操作:
(1): 它分配足够存储一个特定类型对象的内存
(2):为它刚才分配的内存中的那个对象设定初始值。(对于内置类型对象,就是默认初始化该它,对应类类型,调用constructor初始化)
delete完成的操作:
(1):销毁给定指针指向的对象
(2):释放该对象的对应内存
这儿有详细的讲叙,new, delete背后在做什么:http://blog.csdn.net/hazir/article/details/21413833
标准库std::allocator类帮助我们将内存分配和对象初始化分离开来,也允许我们将对象的销毁跟对象内存释放分开来。std::allocator分配的内存是原始的、未构造的。这里提供一个实例感受一下这个流程。然后注意事项跟new/delete类似。std::allocator在memory头文件中。
{ std::allocator<std::string> allocate_str; //定义一个可以分配内存的string的allocator对象allocate_str std::string *p = allocate_str.allocate(1); //分配一个未初始化的string,p指向一块大小为string的原始内存 //std::cout<<*p<<std::endl; eg:这种行为是未定义的 allocate_str.construct(p,"hello world"); //初始化p,*p="hello world"; std::cout<<*p<<std::endl; //打印出hello world allocate_str.destroy(p);// 销毁p构造的对象。对应的是调用p的析构函数, //这时候指向一块原始内存,其值是未定义的。 allocate_str.deallocate(p,1); //将指向的原始内存归还给系统,也就是释放p的内存}
三、智能指针(smart pointer)
为了更加安全的管理动态内存,C++11新标准库推出了智能指针。主要是std::shared_ptr 、 std::unique_ptr 、std::weak_ptr(作为一个伴随类)。他们都位于memory后文件中。
智能指针的行为类似普通指针,一个重要区别是他负责自动释放所指向对象的内存。智能指针可以提供对动态分配的内存安全而又方便的管理,但这是建立在正确使用的前提下,为了正确使用智能指针,我们必须坚持一些基本规范。
在管理new分配出来的资源,shared_ptr类大概可以这样理解:(省略很多,最明显没有一个计数器,但有助加深对智能指针理解,我是这么认为。)
template<class T>class shared_ptr{public: shared_ptr(T* p=0):ptr(p) {} //存储对象 ~shared_ptr(){ delete ptr; } //删除对象 T* get() { return ptr;}private: T *ptr;};
1、不使用相同的普通指针初始化多个智能指针。因为当某个智能指针对象释放其内存时,这个普通指针相应会被delete,此时其他智能指针管理的资源已经被释放了,再对资源进行操作其行为是未定义。请看下面代码。
{ int *p = new int(10); std::cout<<*p<<std::endl; std::shared_ptr<int> ptr1(p); //... { //.... std::shared_ptr<int> ptr2(p); //... } //当ptr2离开其作用域,释放ptr2对象,p所指向的资源也被delete,可以参考上面的hare_ptr类定义。 //.. //此时ptr1对象所管理的资源已经被释放了。 std::cout<<*ptr1<<std::endl; //这种行为是未定义的}
2、不delete get()返回的指针。get()即返回智能指针对象中保存的指针,这个应该很容易理解,delete了get()返回的指针,那么相当于释放了智能指针的资源。代码如下:
{ std::shared_ptr<int> ptr(new int(10)); //... int *p =ptr.get(); //.. std::cout<<*p<<std::endl; //可以访问 //... delete p; //此时ptr对象所管理的资源是被释放了。 std::cout<<*ptr<<std::endl; //这个值是未定义的}
3、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。这个道理跟第2条类似,这两条都是普通指针跟智能指针公用资源,那么无论谁释放了内存,另外一个都不能再使用该资源,其行为是未定义的。
int *p=nullptr;{ std::shared_ptr<int> ptr(new int(0)); //ptr do something.... p = ptr.get(); //....} //当ptr离开作用域,其引用次数减为0,因此释放其所管理资源std::cout<<*p<<std::endl; //此时p的值是未定义的
4、不使用get()初始化或reset()另一个智能指针。这个道理也是跟上面类似,reset()作用大概是释放调用者所管理的资源,如果有参数,那么该调用者转去管理新的资源(参数)。
std::shared_ptr<int> ptr(new int(0));{ //使用get()去初始化另一个智能指针。那么当ptrAnother离开其作用域, //他将会释放ptr管理的资源(引用计数为0), std::shared_ptr<int> ptrAnother(ptr.get()); std::cout<<*ptr<<std::endl; //其分析跟上面一样, std::shared_ptr<int> ptrThird; ptrThird.reset(ptr.get());}
5、如果你使用的智能指针管理的资源不是new管理的内存,记住传递它一个删除器。
C++类动应以了析构函数,但是一些为了C和C++两种语言而设计的类。通常都没有定义析构函数。很容易发生内存泄漏。
struct destination; // 表示我们正在连接什么struct connection; // 打开连接所需的信息connection connect(destination*); // 打开连接void disconnect(connection); // 关闭给定的连接void f(destination &d /* other parameters */) { // 获得一个连接,使用完记得关闭它。 connection c = connect(&d); //.....使用连接 //如果再离开f前忘记调用disconnect,就无法关闭c了。}为了避免这种问题,可以使用std::shared_ptr,但是需要传递一个删除器给他。
#include <iostream>#include <string>#include <memory>struct connection { std::string ip; int port; connection(std::string ip_, int port_) : ip(ip_), port(port_) {}};struct destination { std::string ip; int port; destination(std::string ip_, int port_) : ip(ip_), port(port_) {}};connection connect(destination* pDest){ std::shared_ptr<connection> pConn(new connection(pDest->ip, pDest->port)); std::cout << "creating connection(" << pConn.use_count() << ")" << std::endl; return *pConn;}void disconnect(connection pConn){ std::cout << "connection close(" << pConn.ip << ":" << pConn.port << ")" << std::endl;}void end_connection(connection* pConn){ disconnect(*pConn);}void f(destination& d){ connection conn = connect(&d); std::shared_ptr<connection> p(&conn, end_connection); //p管理&conn的资源,当其引用计数为0,调用end_connection。 在这里就相当于离开函数f,释放conn的资源。 std::cout << "connecting now(" << p.use_count() << ")" << std::endl;}int main(){ destination dest("202.118.176.67", 3316); f(dest);}
小结:智能指针跟普通指针混合使用应当特别注意,防止引用不存在的资源。另外不具备析构函数的类,使用智能指针的时候应该提供一个删除器。
原文:http://blog.csdn.net/qq_33850438/article/details/52994314
参考:
C++ Primer 5th
Effective C++
- 浅谈C++普通指针和智能指针管理动态内存的陷阱
- 【C++】动态内存管理和智能指针
- 动态内存和智能指针
- 【C++】动态内存管理(四)智能指针(std)
- c指针 c的动态内存管理
- 智能指针的陷阱
- 动态内存管理和智能指针 2.0 -- shared_ptr
- 动态内存管理与智能指针
- 动态内存,智能指针
- 内存管理-智能指针
- 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱
- 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱
- C++动态内存和智能指针
- C++动态内存和智能指针
- c++内存管理和智能指针
- 内存管理之常用智能指针的用法和注意事项
- 普通指针到智能指针的转换
- 动态内存与智能指针
- 如何严格设置php中session过期时间
- 配置文件lighttpd.conf参数详细说明的链接和选译
- C#与C/C++的交互
- Count Palindrome in String
- 【Python】Python Assert 为何不尽如人意
- 浅谈C++普通指针和智能指针管理动态内存的陷阱
- Codeforces Round #378 (Div. 2)
- Nginx配置过程中问题记录
- work沉淀
- 安卓APK软件权限一览表
- c#创建带参数的线程
- dirent--文件以及文件夹相关操作(跨平台)
- Unity自动寻路(官方自带组件)
- java synchronized详解