• C++的内存回收【转】

    2010-10-14 18:17:00
    【转贴,以备需要】Java的爱好者们经常批评C++中没有提供与Java类似的废料收集(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好),导致C++中对动态存储的...

    【转贴,以备需要】

    [ 转载文档 本文适合中级读者 已阅读3260次 ]


    C++中的废料收集
    作者:死猫

    提交者:eastvc 发布日期:2003-9-20 10:01:25
    原文出处:http://www.cpphelp.net/issue/gc.html

     

    Java的爱好者们经常批评C++中没有提供与Java类似的废料收集(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好),导致C++中对动态存储的官吏称为程序员的噩梦,不是吗?你经常听到的是内存遗失(memory leak)和非法指针存取,这一定令你很头疼,而且你又不能抛弃指针带来的灵活性。

    在本文中,我并不想揭露Java提供的废料收集机制的天生缺陷,而是指出了C++中引入废料收集的可行性。请读者注意,这里介绍的方法更多的是基于当前标准和库设计的角度,而不是要求修改语言定义或者扩展编译器。

    1 什么是废料收集?

    作为支持指针的编程语言,C++将动态管理存储器资源的便利性交给了程序员。在使用指针形式的对象时(请注意,由于引用在初始化后不能更改引用目标的语言机制的限制,多态性应用大多数情况下依赖于指针进行),程序员必须自己完成存储器的分配、使用和释放,语言本身在此过程中不能提供任何帮助,也许除了按照你的要求正确的和操作系统亲密合作,完成实际的存储器管理。标准文本中,多次提到了“未定义(undefined)”,而这大多数情况下和指针相关。

    某些语言提供了废料收集机制,也就是说程序员仅负责分配存储器和使用,而由语言本身负责释放不再使用的存储器,这样程序员就从讨厌的存储器管理的工作中脱身了。然而C++并没有提供类似的机制,C++的设计者Bjarne Stroustrup在我所知的唯一一本介绍语言设计的思想和哲学的著作《The Design and Evolution of C++》(中译本:C++语言的设计和演化)中花了一个小节讨论这个特性。简而言之,Bjarne本人认为,

    “我有意这样设计C++,使它不依赖于自动废料收集(通常就直接说废料收集)。这是基于自己对废料收集系统的经验,我很害怕那种严重的空间和时间开销,也害怕由于实现和移植废料收集系统而带来的复杂性。还有,废料收集将使C++不适合做许多底层的工作,而这却正是它的一个设计目标。但我喜欢废料收集的思想,它是一种机制,能够简化设计、排除掉许多产生错误的根源。

    需要废料收集的基本理由是很容易理解的:用户的使用方便以及比用户提供的存储管理模式更可靠。而反对废料收集的理由也有很多,但都不是最根本的,而是关于实现和效率方面的。

    已经有充分多的论据可以反驳:每个应用在有了废料收集之后会做的更好些。类似的,也有充分的论据可以反对:没有应用可能因为有了废料收集而做得更好。

    并不是每个程序都需要永远无休止的运行下去;并不是所有的代码都是基础性的库代码;对于许多应用而言,出现一点存储流失是可以接受的;许多应用可以管理自己的存储,而不需要废料收集或者其他与之相关的技术,如引用计数等。

    我的结论是,从原则上和可行性上说,废料收集都是需要的。但是对今天的用户以及普遍的使用和硬件而言,我们还无法承受将C++的语义和它的基本库定义在废料收集系统之上的负担。”

    以我之见,统一的自动废料收集系统无法适用于各种不同的应用环境,而又不至于导致实现上的负担。稍后我将设计一个针对特定类型的可选的废料收集器,可以很明显地看到,或多或少总是存在一些效率上的开销,如果强迫C++用户必须接受这一点,也许是不可取的。

    关于为什么C++没有废料收集以及可能的在C++中为此做出的努力,上面提到的著作是我所看过的对这个问题叙述的最全面的,尽管只有短短的一个小节的内容,但是已经涵盖了很多内容,这正是Bjarne著作的一贯特点,言简意赅而内韵十足。

    下面一步一步地向大家介绍我自己土制佳酿的废料收集系统,可以按照需要自由选用,而不影响其他代码。

    2 构造函数和析构函数

    C++中提供的构造函数和析构函数很好的解决了自动释放资源的需求。Bjarne有一句名言,“资源需求就是初始化(Resource Inquirment Is Initialization)”。

    因此,我们可以将需要分配的资源在构造函数中申请完成,而在析构函数中释放已经分配的资源,只要对象的生存期结束,对象请求分配的资源即被自动释放。

    那么就仅剩下一个问题了,如果对象本身是在自由存储区(Free Store,也就是所谓的“堆”)中动态创建的,并由指针管理(相信你已经知道为什么了),则还是必须通过编码显式的调用析构函数,当然是借助指针的delete表达式。

    3 智能指针

    幸运的是,出于某些原因,C++的标准库中至少引入了一种类型的智能指针,虽然在使用上有局限性,但是它刚好可以解决我们的这个难题,这就是标准库中唯一的一个智能指针::std::auto_ptr<>。

    它将指针包装成了类,并且重载了反引用(dereference)运算符operator *和成员选择运算符operator ->,以模仿指针的行为。关于auto_ptr<>的具体细节,参阅《The C++ Standard Library》(中译本:C++标准库)。

    例如以下代码,

    #include < cstring >
    #include < memory >
    #include < iostream >
    
    
    class string
    {
    public:
        string(const char* cstr) { _data=new char [ strlen(cstr)+1 ]; strcpy(_data, cstr); }
        ~string() { delete [] _data; }
        const char* c_str() const { return _data; }
    private:
        char* _data;
    };
    
    
    void foo()
    {
        ::std::auto_ptr < string > str ( new string( " hello " ) );
        ::std::cout << str->c_str() << ::std::endl;
    }

    由于str是函数的局部对象,因此在函数退出点生存期结束,此时auto_ptr<string>的析构函数调用,自动销毁内部指针维护的string对象(先前在构造函数中通过new表达式分配而来的),并进而执行string的析构函数,释放为实际的字符串动态申请的内存。在string中也可能管理其他类型的资源,如用于多线程环境下的同步资源。下图说明了上面的过程。

               进入函数foo                                    退出函数
                    |                                            A
                    V                                            |
    auto_ptr<string>::auto<string>()            auto_ptr<string>::~auto_ptr<string>()
                    |                                            A
                    V                                            |
             string::string()                             string::~string()
                    |                                            A
                    V                                            |
             _data=new char[]                             delete [] _data
                    |                                            A
                    V                                            |
                使用资源 -----------------------------------> 释放资源

    现在我们拥有了最简单的废料收集机制(我隐瞒了一点,在string中,你仍然需要自己编码控制对象的动态创建和销毁,但是这种情况下的准则极其简单,就是在构造函数中分配资源,在析构函数中释放资源,就好像飞机驾驶员必须在起飞后和降落前检查起落架一样。),即使在foo函数中发生了异常,str的生存期也会结束,C++保证自然退出时发生的一切在异常发生时一样会有效。

    auto_ptr<>只是智能指针的一种,它的复制行为提供了所有权转移的语义,即智能指针在复制时将对内部维护的实际指针的所有权进行了转移,例如

    auto_ptr < string > str1( new string( < str1 > ) );
    cout << str1->c_str();
    auto_ptr < string > str2(str1); // str1内部指针不再指向原来的对象
    cout << str2->c_str(); 
    cout << str1->c_str(); // 未定义,str1内部指针不再有效
    

    某些时候,需要共享同一个对象,此时auto_ptr就不敷使用,由于某些历史的原因,C++的标准库中并没有提供其他形式的智能指针,走投无路了吗?

    4 另一种智能指针

    但是我们可以自己制作另一种形式的智能指针,也就是具有值复制语义的,并且共享值的智能指针。

    需要同一个类的多个对象同时拥有一个对象的拷贝时,我们可以使用引用计数(Reference Counting/Using Counting)来实现,曾经这是一个C++中为了提高效率与COW(copy on write,改写时复制)技术一起被广泛使用的技术,后来证明在多线程应用中,COW为了保证行为的正确反而导致了效率降低(Herb Shutter的在C++ Report杂志中的Guru专栏以及整理后出版的《More Exceptional C++》中专门讨论了这个问题)。

    然而对于我们目前的问题,引用计数本身并不会有太大的问题,因为没有牵涉到复制问题,为了保证多线程环境下的正确,并不需要过多的效率牺牲,但是为了简化问题,这里忽略了对于多线程安全的考虑。

    首先我们仿造auto_ptr设计了一个类模板(出自Herb Shutter的《More Execptional C++》),

    template < typename T >
    class shared_ptr
    {
    private:
      class implement  // 实现类,引用计数
      {
      public:
        implement(T* pp):p(pp),refs(1){}
        
        ~implement(){delete p;}
        
        T* p; // 实际指针
        size_t refs; // 引用计数
      };
      implement* _impl;
    
    
    public:
      explicit shared_ptr(T* p)
        :  _impl(new implement(p)){}
    
    
      ~shared_ptr()
      {
        decrease();  // 计数递减
      }
    
    
      shared_ptr(const shared_ptr& rhs)
        :  _impl(rhs._impl)
      {
        increase();  // 计数递增
      }
      
      shared_ptr& operator=(const shared_ptr& rhs)
      {
        if (_impl != rhs._impl)  // 避免自赋值
        {
          decrease();  // 计数递减,不再共享原对象
          _impl=rhs._impl;  // 共享新的对象
          increase();  // 计数递增,维护正确的引用计数值
        }
        return *this;
      }
    
    
      T* operator->() const
      {
        return _impl->p;
      }
        
      T& operator*() const
      {
        return *(_impl->p);
      }
      
    private:
      void decrease()
      {
        if (--(_impl->refs)==0)
        {  // 不再被共享,销毁对象
          delete _impl;
        }
      }
      
      void increase()
      {
        ++(_impl->refs);
      }
    };
    

    这个类模板是如此的简单,所以都不需要对代码进行太多地说明。这里仅仅给出一个简单的使用实例,足以说明shared_ptr<>作为简单的废料收集器的替代品。

    void foo1(shared_ptr < int >& val)
    {
      shared_ptr < int > temp(val);
      *temp=300;
    }
    
    
    void foo2(shared_ptr < int >& val)
    {
      val=shared_ptr < int > ( new int(200) );
    }
    
    
    int main()
    {
      shared_ptr < int > val(new int(100));
      cout<<"val="<<*val;
      foo1(val); 
      cout<<"val="<<*val;
      foo2(val);
      cout<<"val="<<*val;
    }
    

    在main()函数中,先调用foo1(val),函数中使用了一个局部对象temp,它和val共享同一份数据,并修改了实际值,函数返回后,val拥有的值同样也发生了变化,而实际上val本身并没有修改过。

    然后调用了foo2(val),函数中使用了一个无名的临时对象创建了一个新值,使用赋值表达式修改了val,同时val和临时对象拥有同一个值,函数返回时,val仍然拥有这正确的值。

    最后,在整个过程中,除了在使用shared_ptr < int >的构造函数时使用了new表达式创建新之外,并没有任何删除指针的动作,但是所有的内存管理均正确无误,这就是得益于shared_ptr<>的精巧的设计。

    拥有了auto_ptr<>和shared_ptr<>两大利器以后,应该足以应付大多数情况下的废料收集了,如果你需要更复杂语义(主要是指复制时的语义)的智能指针,可以参考boost的源代码,其中设计了多种类型的智能指针。

    5 标准容器

    对于需要在程序中拥有相同类型的多个对象,善用标准库提供的各种容器类,可以最大限度的杜绝显式的内存管理,然而标准容器并不适用于储存指针,这样对于多态性的支持仍然面临困境。

    使用智能指针作为容器的元素类型,然而标准容器和算法大多数需要值复制语义的元素,前面介绍的转移所有权的auto_ptr和自制的共享对象的shared_ptr都不能提供正确的值复制语义,Herb Sutter在《More Execptional C++》中设计了一个具有完全复制语义的智能指针ValuePtr,解决了指针用于标准容器的问题。

    然而,多态性仍然没有解决,我将在另一篇文章专门介绍使用容器管理多态对象的问题。

    6 语言支持

    为什么不在C++语言中增加对废料收集的支持?

    根据前面的讨论,我们可以看见,不同的应用环境,也许需要不同的废料收集器,不管三七二十一使用废料收集,需要将这些不同类型的废料收集器整合在一起,即使可以成功(对此我感到怀疑),也会导致效率成本的增加。

    这违反了C++的设计哲学,“不为不必要的功能支付代价”,强迫用户接受废料收集的代价并不可取。

    相反,按需选择你自己需要的废料收集器,需要掌握的规则与显式的管理内存相比,简单的多,也不容易出错。

    最关键的一点, C++并不是“傻瓜型”的编程语言,他青睐喜欢和善于思考的编程者,设计一个合适自己需要的废料收集器,正是对喜爱C++的程序员的一种挑战。

     

    展开全文
  • C++垃圾回收机制

    2010-06-16 14:56:00
    标准C++没有垃圾回收机制的原因:1) 没有共同基类C++是从C发展而成,允许直接操作指针,允许将一个类型转换为另一个类型,对于一个指针无法知道它真正指向的类型;而Java或C#都有一个共同基类 2) 系统开销垃圾...

    标准C++没有垃圾回收机制的原因:

    1)      没有共同基类

    C++是从C发展而成,允许直接操作指针,允许将一个类型转换为另一个类型,对于一个指针无法知道它真正指向的类型;而JavaC#都有一个共同基类

     

    2)      系统开销

    垃圾回收所带来的系统开销,不符合C++高效的特性,使得不适合做底层工作

     

    3)      耗内存

    C++产生的年代内存很少,垃圾回收机制需要占用更多的内存

     

    4)      替代方法

    C++C++有析构函数、智能指针、引用计数去管理资源的释放,对GC的需求不迫切

     

    展开全文
  •  在C++中, 用 "类" 来描述 "对象", 所谓的"对象"是指现实世界中的一切事物。那么类就可以看做是对相似事物的抽象, 找到这些不同事物间的共同点, 如自行车和摩托车, 首先他们都属于&...

    一、"类" 的介绍
        在C++中, 用 "" 来描述 "对象", 所谓的"对象"是指现实世界中的一切事物。那么类就可以看做是对相似事物的抽象, 找到这些不同事物间的共同点, 如自行车和摩托车, 首先他们都属于"对象", 并且具有一定得相同点, 和一些不同点, 相同点如他们都有质量、都有两个轮子, 都是属于交通工具等。"都有质量"、"两个轮子"属于这个对象的属性, 而"都能够当做交通工具"属于该对象具有的行为, 也称方法。
        
        类是属于用户自定义的数据类型, 并且该类型的数据具有一定的行为能力, 也就是类中说描述的方法。通常来说, 一个类的定义包含两部分的内容, 一是该类的属性, 另一部分是它所拥有的方法。以 "人类" 这个类来说, 每个人都有自己的姓名、年龄、出生日期、体重等, 为 人类 的属性部分, 此外, 人能够吃饭、睡觉、行走、说话等属于人类所具有的行为。
        
        上面举例中所描述的 "人" 类仅仅是具有人这种对象的最基础的一些属性和行为, 可以称之为人的"基类"。 再说说一些具有一些职业的人, 例如学生, 一个学生还具有"基类"中所没有的属性, 如学校、班级、学号; 也可以具有基类所不具有的行为, 如每天需要去上课, 需要考试等。
        
        学生类可以看做是基类的一个扩展, 因为他具有基类的所有属性和行为, 并且在此基础上增加了一些基类所没有的属性和行为, 像"学生"这样的类称为"人类"这个基类的"派生类"或者"子类"。在学生的基础上海可以进一步的扩展出其他更高级的类, 如"研究生"类。
        
        到此, 我们不再更深的去介绍类的其他相关知识。
        
    二、C++类的定义

        C++中使用关键字 class 来定义类, 其基本形式如下:

    class 类名
    {
        public:
        //公共的行为或属性
    
        private:
        //公共的行为或属性
    };
    

    说明:
            ①. 类名 需要遵循一般的命名规则;
            
            ②. public 与 private 为属性/方法限制的关键字, private 表示该部分内容是私密的, 不能被外部所访问或调用, 只能被本类内部访问; 而 public 表示公开的属性和方法, 外界可以直接访问或者调用。
                一般来说类的属性成员都应设置为private, public只留给那些被外界用来调用的函数接口, 但这并非是强制规定, 可以根据需要进行调整;
                
            ③. 结束部分的分号不能省略。

    三、C++类的实现
        在上面的定义示例中我们只是定义了这个类的一些属性和方法声明, 并没有去实现它, 类的实现就是完成其方法的过程。类的实现有两种方式, 一种是在类定义时完成对成员函数的定义, 另一种是在类定义的外部进行完成。
        
        1>. 在类定义时定义成员函数
            成员函数的实现可以在类定义时同时完成, 如代码:

    #include <iostream>
    
    using namespace std;
    
    class Point
    
    {
    
        public:
    
        void setPoint(int x, int y) //实现setPoint函数
        
        {
    
            xPos = x;
    
            yPos = y;
    
        }
    
    
        void printPoint() //实现printPoint函数
    
        {
    
            cout<< "x = " << xPos << endl;
    
            cout<< "y = " << yPos << endl;
    
        }
    
    
        private:
        
           int xPos;
    
           int yPos;
    
    };
    
    
    int main()
    
    {
    
        Point M; //用定义好的类创建一个对象 点M
        
        M.setPoint(10, 20); //设置 M点 的x,y值
    
        M.printPoint(); //输出 M点 的信息
    
    
        return 0;
    
    }

    运行输出:

    x = 10 
    y = 20

    与类的定义相比, 在类内实现成员函数不再是在类内进行声明, 而是直接将函数进行定义, 在类中定义成员函数时, 编译器默认会争取将其定义为 inline 型函数 2>. 在类外定义成员函数
            在类外定义成员函数通过在类内进行声明, 然后在类外通过作用域操作符 :: 进行实现, 形式如下:

    返回类型 类名::成员函数名(参数列表)
    {
         //函数体
    }
    
    
    
    #include <iostream>
    
    using namespace std;
    
    class Point
    
    {
    
        public:
    
        void setPoint(int x, int y); //在类内对成员函数进行声明
    
        void printPoint();
    
    
        private:
    
        int xPos;
    
        int yPos;
    
    };
    
    void Point::setPoint(int x, int y) //通过作用域操作符 '::' 实现setPoint函数
    
    {
    
        xPos = x;
    
        yPos = y;
    
    }
    
    void Point::printPoint() //实现printPoint函数
    
    {
    
        cout<< "x = " << xPos << endl;
    
        cout<< "y = " << yPos << endl;
    
    }
    
    int main()
    
    {
    
        Point M; //用定义好的类创建一个对象 点M
    
        M.setPoint(10, 20); //设置 M点 的x,y值
    
        M.printPoint(); //输出 M点 的信息
    
    
        return 0;
    
    }

    依 setPoint 成员函数来说, 在类内声明的形式为 void setPoint(int x, int y); 那么在类外对其定义时函数头就应该是 void Point::setPoint(int x, int y) 这种形式, 其返回类型、成员函数名、参数列表都要与类内声明的形式一致。

    四、对象的作用域、可见域与生存周期
        类对象的作用域、可见域以及生存周期与普通变量的保持相同, 当对象生存周期结束时对象被自动撤销, 所占用的内存被回收, 需要注意的是, 如果对象的成员函数中有使用 new 或者 malloc 申请的动态内存程序不会对其进行释放, 需要我们手动进行清理, 否则会造成内存泄露。

    c++ 类的定义和使用

    #include "stdafx.h"
    #include <iostream>
    #include <string>
    using namespace std;
    class A
    {
    private:
        int n2;
        int n1;
    
    public:
    
        A() :n2(34), n1(n2+1) {}
    
        void Print() {
            cout << "n1:" << n1 << ", n2: " << n2 << endl;
        }
    };
    
    int main()
    {
        A a;
        a.Print();
        getchar();
        return 1;
    }

    这里写图片描述




    对上面代码稍微做调整: 将n1和n2两个变量定义的顺序互换一下


    #include "stdafx.h"
    #include <iostream>
    #include <string>
    using namespace std;
    class A
    {
    private:
        int n1;
        int n2;
    
    public:
    
        A() :n2(34), n1(n2+1) {}
    
        void Print() {
            cout << "n1:" << n1 << ", n2: " << n2 << endl;
        }
    };
    
    int main()
    {
    
        A a;
        a.Print();
    
        getchar();
        return 1;
    }

    打印结果:


    这里写图片描述




    由此可以总结: 构造函数中,变量初始化的顺序,是以变量定义的顺序来定的,而不是简单的以构造函数中变量出现的顺序来定的


    还可以使用有参的构造函数:


    #include "stdafx.h"
    #include <iostream>
    #include <string>
    using namespace std;
    class A
    {
    private:
        int n1;
        int n2;
    
    public:
    
        A(int k1,int k2) :n2(k1), n1(k2) {}
    
        void Print() {
            cout << "n1:" << n1 << ", n2: " << n2 << endl;
        }
    };
    
    int main()
    {
    
        A a(78,97);
        a.Print();
    
        getchar();
        return 1;
    }
    •  

    打印结果:


    这里写图片描述



    下面对上面进行改造一下:

    #include "stdafx.h"
    #include <iostream>
    #include <string>
    using namespace std;
    class A
    {
    private:
        int n1;
        int n2;
    
    public:
    
        A(int k1) :n2(k1), n1(n2) {}
    
        void Print() {
            cout << "n1:" << n1 << ", n2: " << n2 << endl;
        }
    };
    
    int main()
    {
    
        A a(115);
        a.Print();
    
        getchar();
        return 1;
    }

    打印结果: 
    这里写图片描述


    通过最后一个例子,再次证明,变量初始化的顺序是严格按照各个变量定义的先后顺序来的,而不是简单的依据各变量在构造函数中出现的先后顺序来定的



    下面两种写法是一个意思: 
    写法一:

    public:
        A(int k1,int k2) {
            n1 = k1;
            n2 = k2;
        }

    写法二:

    public:
    
        A(int k1,int k2) :n1(k1),n2(k2){}

     

    展开全文
  • 探讨C++内存回收

    2013-03-10 22:23:39
    3.1 C++内存对象大会战  如果一个人自称为程序高手,却对内存一无所知,那么我可以告诉你,他一定在吹牛。用C或C++写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,...

    转自:http://club.topsage.com/thread-541343-1-1.html

    3.1 C++内存对象大会战


      如果一个人自称为程序高手,却对内存一无所知,那么我可以告诉你,他一定在吹牛。用C或C++写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当我们操作内存的时候一不小心就会出现问题,而且很多时候,这些问题都是不易发觉的,比如内存泄漏,比如悬挂指针。笔者今天在这里并不是要讨论如何避免这些问题,而是想从另外一个角度来认识C++内存对象。

      我们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。那么这些不同的内存对象有什么区别了?堆对象和栈对象各有什么优劣了?如何禁止创建堆对象或栈对象了?这些便是今天的主题。

    3.1.1 基本概念

      先来看看栈。栈,一般用于存放局部变量或对象,如我们在函数定义中用类似下面语句声明的对象:
    1. Type stack_object;
    复制代码
    stack_object便是一个栈对象,它的生命期是从定义点开始,当所在函数返回时,生命结束。

      另外,几乎所有的临时对象都是栈对象。比如,下面的函数定义:
    1. Type fun(Type object);
    复制代码
    这个函数至少产生两个临时对象,首先,参数是按值传递的,所以会调用拷贝构造函数生成一个临时对象object_copy1 ,在函数内部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,如果我们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2,这个临时对象会在函数返回后一段时间内被释放。比如某个函数中有如下代码:
    1. Type tt ,result ; //生成两个栈对象
    2. tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
    复制代码
    上面的第二个语句的执行情况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,然后再调用赋值运算符执行
    1. tt = object_copy2 ; //调用赋值运算符
    复制代码
    看到了吗?编译器在我们毫无知觉的情况下,为我们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销可能是很大的,所以,你也许明白了,为什么对于“大”对象最好用const引用传递代替按值进行函数参数传递了。

      接下来,看看堆。堆,又叫自由存储区,它是在程序执行的过程中动态分配的,所以它最大的特性就是动态性。在C++中,所有堆对象的创建和销毁都要由程序员负责,所以,如果处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如果已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。

      那么,C++中是怎样分配堆对象的?唯一的方法就是用new(当然,用类malloc指令也可获得C式堆内存),只要使用new,就会在堆中分配一块内存,并且返回指向该堆对象的指针。

      再来看看静态存储区。所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在 main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之前,会调用由编译器生成的exit函数,来释放所有的全局对象。比如下面的代码:
    1. void main(void)
    2. {
    3.  … …// 显式代码
    4. }
    复制代码
    实际上,被转化成这样:
    1. void main(void)
    2. {
    3.  _main(); //隐式代码,由编译器产生,用以构造所有全局对象
    4.  … … // 显式代码
    5.  exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
    6. }
    复制代码
    所以,知道了这个之后,便可以由此引出一些技巧,如,假设我们要在main()函数执行之前做某些准备工作,那么我们可以将这些准备工作写到一个自定义的全局对象的构造函数中,这样,在main()函数的显式代码执行之前,这个全局对象的构造函数会被调用,执行预期的动作,这样就达到了我们的目的。刚才讲的是静态存储区中的全局对象,那么,局部静态对象了?局部静态对象通常也是在函数中定义的,就像栈对象一样,只不过,其前面多了个static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。

      还有一种静态对象,那就是它作为class的静态成员。考虑这种情况时,就牵涉了一些较复杂的问题。

      第一个问题是class的静态成员对象的生命期,class的静态成员对象随着第一个class object的产生而产生,在整个程序结束时消亡。也就是有这样的情况存在,在程序中我们定义了一个class,该类中有一个静态对象作为成员,但是在程序执行过程中,如果我们没有创建任何一个该class object,那么也就不会产生该class所包含的那个静态对象。还有,如果创建了多个class object,那么所有这些object都共享那个静态对象成员。

      第二个问题是,当出现下列情况时:
    1. class Base
    2. {
    3.  public:
    4.   static Type s_object ;
    5. }

    6. class Derived1 : public Base / / 公共继承
    7. {
    8.  … …// other data
    9. }

    10. class Derived2 : public Base / / 公共继承
    11. {
    12.  … …// other data
    13. }

    14. Base example ;
    15. Derivde1 example1 ;
    16. Derivde2 example2 ;
    17. example.s_object = …… ;
    18. example1.s_object = …… ;
    19. example2.s_object = …… ;
    复制代码
    请注意上面标为黑体的三条语句,它们所访问的s_object是同一个对象吗?答案是肯定的,它们的确是指向同一个对象,这听起来不像是真的,是吗?但这是事实,你可以自己写段简单的代码验证一下。我要做的是来解释为什么会这样?我们知道,当一个类比如Derived1,从另一个类比如Base继承时,那么,可以看作一个Derived1对象中含有一个Base型的对象,这就是一个subobject。一个Derived1对象的大致内存布局如下:
      
      让我们想想,当我们将一个Derived1型的对象传给一个接受非引用Base型参数的函数时会发生切割,那么是怎么切割的呢?相信现在你已经知道了,那就是仅仅取出了Derived1型的对象中的subobject,而忽略了所有Derived1自定义的其它数据成员,然后将这个 subobject传递给函数(实际上,函数中使用的是这个subobject的拷贝)。

      所有继承Base类的派生类的对象都含有一个Base型的subobject(这是能用Base型指针指向一个Derived1对象的关键所在,自然也是多态的关键了),而所有的subobject和所有Base型的对象都共用同一个s_object对象,自然,从Base类派生的整个继承体系中的类的实例都会共用同一个s_object对象了。上面提到的example、example1、example2的对象布局如下图所示:

    3.1.2 三种内存对象的比较

      栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。

      堆对象,其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

    接下来看看static对象。

      首先是全局对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如C#,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。C++也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容C。

      其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些class之间或这些class objects之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。

      接着是静态局部对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个nonstatic局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。

      在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

    3.1.3 使用栈对象的意外收获

      前面已经介绍到,栈对象是在适当的时候创建,然后在适当的时候自动释放的,也就是栈对象有自动管理功能。那么栈对象会在什么会自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。你也许说,这些都很正常啊,没什么大不了的。是的,没什么大不了的。但是只要我们再深入一点点,也许就有意外的收获了。

      栈对象,自动释放时,会调用它自己的析构函数。如果我们在栈对象中封装资源,而且在栈对象的析构函数中执行释放资源的动作,那么就会使资源泄漏的概率大大降低,因为栈对象可以自动的释放资源,即使在所在函数发生异常的时候。实际的过程是这样的:函数抛出异常时,会发生所谓的 stack_unwinding(堆栈回滚),即堆栈会展开,由于是栈对象,自然存在于栈中,所以在堆栈回滚的过程中,栈对象的析构函数会被执行,从而释放其所封装的资源。除非,除非在析构函数执行的过程中再次抛出异常――而这种可能性是很小的,所以用栈对象封装资源是比较安全的。基于此认识,我们就可以创建一个自己的句柄或代理来封装资源了。智能指针(auto_ptr)中就使用了这种技术。在有这种需要的时候,我们就希望我们的资源封装类只能在栈中创建,也就是要限制在堆中创建该资源封装类的实例。

    3.1.4 禁止产生堆对象

      上面已经提到,你决定禁止产生某种类型的堆对象,这时你可以自己创建一个资源封装类,该类对象只能在栈中产生,这样就能在异常的情况下自动释放封装的资源。

      那么怎样禁止产生堆对象了?我们已经知道,产生堆对象的唯一方法是使用new操作,如果我们禁止使用new不就行了么。再进一步,new操作执行时会调用operator new,而operator new是可以重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。现在,你也许又有疑问了,难道创建栈对象不需要调用new吗?是的,不需要,因为创建栈对象不需要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让我们看看下面的示例代码:
    1. #include <stdlib.h> //需要用到C式内存分配函数

    2. class Resource ; //代表需要被封装的资源类
    3. class NoHashObject
    4. {
    5.  private:
    6.   Resource* ptr ;//指向被封装的资源
    7.   ... ... //其它数据成员
    8.   void* operator new(size_t size) //非严格实现,仅作示意之用
    9.   {
    10.    return malloc(size) ;
    11.   }

    12.   void operator delete(void* pp) //非严格实现,仅作示意之用
    13.   {
    14.    free(pp) ;
    15.   }

    16.  public:
    17.   NoHashObject()
    18.   {
    19.    //此处可以获得需要封装的资源,并让ptr指针指向该资源
    20.    ptr = new Resource() ;
    21.   }

    22.   ~NoHashObject()
    23.   {
    24.    delete ptr ; //释放封装的资源
    25.   }
    26. };
    复制代码
    NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码:
    1. NoHashObject* fp = new NoHashObject() ; //编译期错误!
    2. delete fp ;
    复制代码
    上面代码会产生编译期错误。好了,现在你已经知道了如何设计一个禁止堆对象的类了,你也许和我一样有这样的疑问,难道在类NoHashObject 的定义不能改变的情况下,就一定不能产生该类型的堆对象了吗?不,还是有办法的,我称之为“暴力破解法”。C++是如此地强大,强大到你可以用它做你想做的任何事情。这里主要用到的是技巧是指针类型的强制转换。
    1. void main(void)
    2. {
    3.  char* temp = new char[sizeof(NoHashObject)] ;
    4.  //强制类型转换,现在ptr是一个指向NoHashObject对象的指针
    5.  NoHashObject* obj_ptr = (NoHashObject*)temp ;
    6.  temp = NULL ; //防止通过temp指针修改NoHashObject对象
    7.  //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
    8.  Resource* rp = (Resource*)obj_ptr ;
    9.  //初始化obj_ptr指向的NoHashObject对象的ptr成员
    10.  rp = new Resource() ;
    11.  //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
    12.  ... ...
    13.  delete rp ;//释放资源
    14.  temp = (char*)obj_ptr ;
    15.  obj_ptr = NULL ;//防止悬挂指针产生
    16.  delete [] temp ;//释放NoHashObject对象所占的堆空间。
    17. }
    复制代码
    上面的实现是麻烦的,而且这种实现方式几乎不会在实践中使用,但是我还是写出来路,因为理解它,对于我们理解C++内存对象是有好处的。对于上面的这么多强制类型转换,其最根本的是什么了?我们可以这样理解:

      某块内存中的数据是不变的,而类型就是我们戴上的眼镜,当我们戴上一种眼镜后,我们就会用对应的类型来解释内存中的数据,这样不同的解释就得到了不同的信息。

      所谓强制类型转换实际上就是换上另一副眼镜后再来看同样的那块内存数据。

      另外要提醒的是,不同的编译器对对象的成员数据的布局安排可能是不一样的,比如,大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动作像我们预期的那样执行:
    1. Resource* rp = (Resource*)obj_ptr;
    复制代码
    但是,并不一定所有的编译器都是如此。

      既然我们可以禁止产生某种类型的堆对象,那么可以设计一个类,使之不能产生栈对象吗?当然可以。

    3.1.5 禁止产生栈对象

      前面已经提到了,创建栈对象时会移动栈顶指针以“挪出”适当大小的空间,然后在这个空间上直接调用对应的构造函数以形成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete操作的,所以将operator new/delete设置为private不能达到目的。当然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。

      这样的确可以,而且我也打算采用这种方案。但是在此之前,有一点需要考虑清楚,那就是,如果我们将构造函数设置为私有,那么我们也就不能用 new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数啊。所以,我打算只将析构函数设置为private。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。

      如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。

      为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:
    1. class NoStackObject
    2. {
    3.  protected:
    4.   ~NoStackObject() { }

    5.  public:
    6.   void destroy()
    7.   {
    8.    delete this ;//调用保护析构函数
    9.   }
    10. };
    复制代码
    接着,可以像这样使用NoStackObject类:
    1. NoStackObject* hash_ptr = new NoStackObject() ;
    2. ... ... //对hash_ptr指向的对象进行操作
    3. hash_ptr->destroy() ;
    复制代码
    呵呵,是不是觉得有点怪怪的,我们用new创建一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。所以,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?我们可以用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton 模式就可以用这种方式实现。)让我们来看看:
    1. class NoStackObject
    2. {
    3.  protected:
    4.   NoStackObject() { }
    5.   ~NoStackObject() { }

    6.  public:
    7.   static NoStackObject* creatInstance()
    8.   {
    9.    return new NoStackObject() ;//调用保护的构造函数
    10.   }

    11.   void destroy()
    12.   {
    13.    delete this ;//调用保护的析构函数
    14.   }
    15. };
    复制代码
    现在可以这样使用NoStackObject类了:
    1. NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
    2. ... ... //对hash_ptr指向的对象进行操作
    3. hash_ptr->destroy() ;
    4. hash_ptr = NULL ; //防止使用悬挂指针
    复制代码
    现在感觉是不是好多了,生成对象和释放对象的操作一致了。

    3.2 浅议C++ 中的垃圾回收方法

      许多 C 或者 C++ 程序员对垃圾回收嗤之以鼻,认为垃圾回收肯定比自己来管理动态内存要低效,而且在回收的时候一定会让程序停顿在那里,而如果自己控制内存管理的话,分配和释放时间都是稳定的,不会导致程序停顿。最后,很多 C/C++ 程序员坚信在C/C++ 中无法实现垃圾回收机制。这些错误的观点都是由于不了解垃圾回收的算法而臆想出来的。

      其实垃圾回收机制并不慢,甚至比动态内存分配更高效。因为我们可以只分配不释放,那么分配内存的时候只需要从堆上一直的获得新的内存,移动堆顶的指针就够了;而释放的过程被省略了,自然也加快了速度。现代的垃圾回收算法已经发展了很多,增量收集算法已经可以让垃圾回收过程分段进行,避免打断程序的运行了。而传统的动态内存管理的算法同样有在适当的时间收集内存碎片的工作要做,并不比垃圾回收更有优势。

      而垃圾回收的算法的基础通常基于扫描并标记当前可能被使用的所有内存块,从已经被分配的所有内存中把未标记的内存回收来做的。C/C++ 中无法实现垃圾回收的观点通常基于无法正确扫描出所有可能还会被使用的内存块,但是,看似不可能的事情实际上实现起来却并不复杂。首先,通过扫描内存的数据,指向堆上动态分配出来内存的指针是很容易被识别出来的,如果有识别错误,也只能是把一些不是指针的数据当成指针,而不会把指针当成非指针数据。这样,回收垃圾的过程只会漏回收掉而不会错误的把不应该回收的内存清理。其次,如果回溯所有内存块被引用的根,只可能存在于全局变量和当前的栈内,而全局变量 (包括函数内的静态变量)都是集中存在于 bss 段或 data段中。

      垃圾回收的时候,只需要扫描 bss 段, data 段以及当前被使用着的栈空间,找到可能是动态内存指针的量,把引用到的内存递归扫描就可以得到当前正在使用的所有动态内存了。

      如果肯为你的工程实现一个不错的垃圾回收器,提高内存管理的速度,甚至减少总的内存消耗都是可能的。如果有兴趣的话,可以搜索一下网上已有的关于垃圾回收的论文和实现了的库,开拓视野对一个程序员尤为重要。
    展开全文
  • C++实现垃圾回收机制

    2013-01-13 17:49:09
    C++实现垃圾回收机制  我想讨论的问题是,如何让下面的代码正确无误: #include classHuman { public:  Human()  {  std::cout "Human"  }  ~Human()  {  std::cout "~Human()"  } }; ...

    C++实现垃圾回收机制

           我想讨论的问题是,如何让下面的代码正确无误:

    #include <iostream>

    classHuman

    {

    public:

        Human()

        {

           std::cout << "Human" << std::endl;

        }

        ~Human()

        {

           std::cout << "~Human()" << std::endl;

        }

    };

    int main()

    {

           Human*pHuman = new Human;

           system(“pause”);

           return0;

    }

           有没有发现其中的错误,而且这个错误很严重,嗯……我想你发现了,没有delete pHuman回收指针所指的内存空间,怎么知道?起码Human的析构函数没有调用,这会造成内存泄漏,非常严重的问题。就想你借了人家钱而不还一样,当然,如果你借的钱少,那问题不至于严重到不可收拾的地步,但是你借了很多钱而不还,那可麻烦了。

           JavaC#AS这些语言中,上面的代码是没有任何问题的,因为你借了这些内存空间,在程序结束后,虚拟机会帮你还,也就是说,它们自带有垃圾回收机制,而C++?必须得手动delete,如果只是new出来一两个,手动delete还不觉得这么麻烦,但是如果你new出来很多东西,那怎么办,你得一个个记住,然后一个个delete?就像你借了很多人的钱,你必须记住谁谁借了钱给我,你这时会感叹,如果能有个人帮我还钱,那该多好啊,那样的话,你就只管借而不用担心钱没还而惹来的麻烦了。那在C++里,怎么实现呢?

           你可能会想到用智能指针来管理,嗯……很好的想法,很自然的,你会修改上面的代码:

           intmain()

           {

                  std::auto_ptr<Human> pHuman(new Human);

                  system(“pause”);

                  return;

    }

    如果编译器说std里没有auto_ptr的话,#include<memory>就可以了,这就是智能指针的好处,只要你这个智能指针不是new创建出来的,它会答应帮你delete掉,如果你是new出来的,那么也得delete,否则它无法完成它的承诺。

    也就是说要像下面那样:

    int main()

    {

                  std::auto_ptr<Human>*pHuman  = new std::auto_ptr<Human>(new Human);

                  delete pHuman;

                  system(“pause”);

                  return;

    }

    如果你new创建智能指针的话,问题又继续循环了,所以智能指针不是用new出来的,否则你也必须delete。也就是说你借了那个帮你还钱的人的钱,所以你得先把钱还给他,他才帮你还清其他的钱。

    OK,你保证你使用智能指针不new创建出来,那事情是不是就完了呢?当然还没,使用std::auto_ptr还有其他要注意的问题,想知道详细的话,研究Effective C++More Effective C++便可以了解。但在这里不是我讨论的重点,我想讨论的是,智能指针的原理是什么?看下面代码便清楚了:

     

    classHumanPtr

    {

    private:

        Human* m_ptr;

    public:

        HumanPtr(Human* ptr = 0):m_ptr(ptr)

        {

        }

        ~HumanPtr()

        {

           if(m_ptr)

           {

               deletem_ptr;

               m_ptr = 0;

           }

        }

    };

    那么像下面那样子使用:

    int main()

    {

           HumanPtrpHuman(new Human);

           /.../

    }

    也就是只要把new创建出来的那个指针放到一个对象里保存,而这个对象不是使用new创建的,只要符合这个规则即可实现只需new创建,而不用delete

           那为什么会自动帮我delete呢?

           我们知道,每个对象都有一个生命周期,当生命结束后,系统会调用它的析构函数,然后回收它的内存空间。像上面那个HumanPtr pHuman,它的生命周期是整个程序运行时间,如果程序结束,它也结束,因为它是在栈里开辟空间的,在栈里开辟的空间,系统会自动回收掉,而在堆里开辟的,系统无法自动做到,如果能做到,也就没必要在这里讨论这么多了,标准库也就没必要弄出一个什么智能指针的东西了。

          因此,智能指针都应该是在栈里开辟空间。

    当然std::auto_ptr比这个HumanPtr做的事情还要多,但是它便是基于这个思想的。但是,这样还是没有解决我的问题。

    我想要的是下面的代码正确无误:

    int main()

    {

           Human* pHuman = new Human;

           system(“pause”);

           return0;

    }

           现在的问题是,这段代码造成了内存泄漏,如何解决这个问题?

           嗯……要解决这个问题,只需调用delete pHuman就可以了,那能不能让系统帮我们调用呢?答案是可以的,看下面的代码:

    //.h头文件

    //对象

    classObject

    {

    public:

        //自定义的operator new

        static void* operator new(std::size_t);

        //自定义的operator delete

        static void operator delete(void* pVoid);

     

        virtual~Object() = 0{};

    };

    //单体

    classJSingleton

    {

    protected:

        JSingleton(){};

        virtual~JSingleton() = 0{};

    private:

        JSingleton(constJSingleton&);

        JSingleton& operator=(const JSingleton&);

    };

    //工厂

    classFactory:public JSingleton

    {

    protected:

        Factory(){};

        virtual~Factory();

    private:

        std::vector<Object*> m_pObjects;

        friend class auto_ptr<Factory>;

        staticauto_ptr<Factory> s_factory;

    public:

        staticFactory* Instance();

        //增加对象

        voidAddObject(Object* pObject)

        {

           m_pObjects.push_back(pObject);

        };

        //移除对象

        voidEraseObject(Object *pObject);

    };

    classHuman:public Object

    {

    public:

        Human()

        {

           std::cout << "Human" << endl;

        }

        ~Human()

        {

           std::cout << "~Human()" << endl;

        }

    };

    //.cpp文件

    #include<memory>

    #include<iostream>

    #include<vector>

    auto_ptr<Factory>Factory::s_factory;

    Factory*Factory::Instance()

    {

        //如果工厂没有创建

        if(s_factory.get() == 0)

        {

           s_factory.reset(newFactory);

        }

        returns_factory.get();

    }

    Factory::~Factory()

    {

        //在工厂里保存的所有指针删除,回收空间

        for (DWORD i= 0; i < m_pObjects.size(); ++i)

        {

           if(m_pObjects[i])

           {

               ::deletem_pObjects[i];//调用全局的delete

           }

        }

    }

    //移除对象

    voidFactory::EraseObject(Object *pObject)

    {

        vector<Object*>::iterator iter =m_pObjects.begin();

        //遍历寻找对应的指针

        while (iter!= m_pObjects.end())

        {

           if (*iter== pObject)

           {

               iter = m_pObjects.erase(iter);//移除操作返回的是移除后的下一个

               ::operatordelete(pObject);//调用全局的delete

               return;

           }

           else

    {

    ++iter;

    }

        }

    }

    void*Object::operatornew(std::size_tst)

    {

        Human *pHuman = (Human*)::operatornew(st);//调用全局的new

        Factory *pf = Factory::Instance();//获得唯一实例化的引用

        pf->AddObject(pHuman);//将创建后的指针放到工厂里保存

        returnpHuman;

    }

    void Object::operatordelete(void* pObject)

    {

        Factory *pFactory = Factory::Instance();//获得唯一实例化的引用

        pFactory->EraseObject((Object*)pObject);//移除工厂中保存对象的指针

    }

    //main主函数

    intmain()

    {

    Human *pHuman = new Human;

             system("pause");

        return 0;

    }

             运行上面的代码,你会发现当程序结束后,Human的析构函数调用了,那Human的空间有没有回收掉呢?答案是肯定的,我测试过,是没问题的,你可以自己做一下测试,可以创建一万个Human出来,然后看一下内存,程序结束后,再看一下内存,需要注意的是程序结束后是看不到内存了,而内存的释放又是在程序结束后的,因此,你可以将工厂设置为可以delete掉的,可以提前结束它的生命,又或者是提供一个Clear函数接口来测试,那样就可以看到内存的变化了。这里的工厂用的是单体模式,保证只存在一份实例,如果对单体模式不了解的话,可以看下我的另一篇关于单体模式的文章。

           当然,这里只是提供一个思路和一份实现而已,你可以有你自己的一份实现,但是原理都差不多,就是把new创建出来的指针放到一个对象管理,这里采用的是工厂,这个对象要保证如果用户不自行delete,那么在程序结束后,这个delete工作将由这个对象执行。如果Human是一个抽象类,上面那个Factory也能正常工作。

           这里需要注意的是,在使用newdelete时的一些事情,当使用new实例化时,编译器做了两件工作:

    1)首先在堆内存开辟了该类大小的空间,也就是说会先调用operator new(std::size_t)函数开辟空间,至于调用全局的还是这个类的,就要看这个类是否重载了operator new(std::size_t),如果这个类重载了,便调用该类提供的,编译器会调用sizeof(Human)计算出这个类的内存大小,然后作为实参传入到这个函数中,这个函数分配好空间了,返回这个内存空间的首地址。

    2)分配好内存空间后,调用这个类的构造函数,上面调用的是默认构造函数。

           那么当delete的时候,编译器也做了两件事,是new相反顺序的,

    1)首先调用这个对象的析构函数

    2)再调用void operatordelete(void*)函数进行回收空间,至于调用全局

    还是这个类提供的,就看这个类是否重载了这个函数。

    关于operator new operator delete的信息,可以研究一下C++ Primer第四版,那里会有更详细的说明。

    还得注意的是,这里采用的是vector顺序表来存储指针,如果你担心内存,可以采用list链表才存储,因为vectorerase移除操作并非是真正的释放空间,虽然你delete掉了pHuman的指针所指的内存,但是vector内部并没有收回掉那4个字节的内存,因为它要预留着下次来使用,那样就不需要再分配内存了。当然,你可以调用vector的析构函数来释放vector保存指针的那4个字节内存。

    还有个问题是,如果自定义一个新类型时,必须得继承Object,不然得自行提供operator newoperator delete,否则无法自动回收内存。

    可能还有其他的情况我没考虑到,如果发现,强烈希望指出。希望这个技术能对你有用。

    这是最后源代码的链接http://download.csdn.net/detail/zx504287/4988003

    展开全文
  • 比如C++的虚函数,垃圾回收,异常,在底层开发中使用,反而会造成很多不必要的麻烦。比如C++编译器为了重载函数,其编译出来的函数名会被改成包括参数的形式(换名),而且每个编译器都有自己的内部前缀和后缀,这...
  • C++内存回收

    2018-02-07 17:42:32
    3.1 C++内存对象大会战  如果一个人自称为程序高手,却对内存一无所知,那么我可以告诉你,他一定在吹牛。用C或C++写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,...
  • Java的爱好者们经常批评C++中没有提供与Java类似的垃圾回收(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好)。 垃圾回收导致C++中对动态存储的管理称为...
  • Java的爱好者们经常批评C++中没有提供与Java类似的垃圾回收(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好),导致C++中对动态存储的官吏称为程序员的...
  • C++垃圾回收器的实现

    2016-06-19 19:07:56
    转载内容的gc原理是: 把所有已new的对象的信息放到一个set里,把所有smartptr对象放到一个链表里。注意:smartptr的个数可能大于已new对象的个数。如果两个循环引用就是一个例子:有两个对象,4个smartptr。...
  • Java的爱好者们经常批评C++中没有提供与Java类似的垃圾回收(Gabage Collector)机制(这很正常,正如C++的爱好者有时也攻击Java没有这个没有那个,或者这个不行那个不够好),导致C++中对动态存储的官吏称为程序员的...
  • 使用C语言来模拟C++中的类以及继承
  • template class CAutoPtr { public: T* m_p; CAutoPtr(T* p) : m_p(p) { } ~CAutoPtr() { delete m_p; } operator T*() const { return (T*)m_p; } T& operator*() co
  • “new”是C++的一个关键字,同时也是操作符。关于new的话题非常多,因为它确实比较复杂,也非常神秘,下面我将把我了解到的与new有关的内容做一个总结。 new的过程 当我们使用关键字new在堆上动态创建一个对象...
  • Java与C#中的单例模式可以不用考虑内存的回收,但C++的不考虑是不行... 代码如下: class CSingleton { // 其它成员 public: static CSingleton * GetInstance() { if (m_pInstance == NULL) m_pInstance =...
  • 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对 C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...
1 2 3 4 5 ... 20
收藏数 68,520
精华内容 27,408