Skip to content

Latest commit

 

History

History
1338 lines (747 loc) · 59.5 KB

内存管理机制.md

File metadata and controls

1338 lines (747 loc) · 59.5 KB

内存管理机制

1.内存管理

1.1C++内存管理详解

1.1.1内存分配方式

1.1.1.1分配方式简介

在C++中,内存分为五个区:

  • **堆:**由new分配的内存块,它们的释放编译器不去管,由我们的应用程序区控制,一般一个new就要对应一

​ delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

  • **栈:**在执行函数时,函数局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释

    ​ 放。栈内存分配运算内置于处理器的指令集中,效率很高但是分配的内存容量有限。

  • **自由存储区:**由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

  • **全局/静态存储区:**全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化

​ 和未初始化的,在C++里面没有这个区分,它们共同占用一块内存区。

  • **常量存储区:**这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改。
1.1.1.2明确区分堆与栈
void f(){int* p=new int[5];}
短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?它分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,它在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax

这里我们为了简单并没有释放内存,那么该怎么去释放呢?应该用delete[]p。
1.1.1.3堆和栈究竟有什么区别?

主要的区别有以下几点:

  • 管理方式:

    栈:编译器自动管理,无需手动控制。

    堆:释放工作由程序员控制,容器产生memory leak。

  • 空间大小:

    堆:在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。

    栈:有一定的空间大小。

  • 碎片问题:

    堆:频繁的new/delete势必会造成内存空间不连续,从而造成大量的碎片,是程序效率降低。

    栈:栈是先进后出的队列,是一一对应的,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出。

  • 生长方向:

    堆:生长方向是向上的,也就是向着地址增加的方向增长

    栈:生长方向是向下的,也就是向着地址减小的方向增长

  • 分配方式:

    堆:动态分配。

    栈:栈有两种分配方式。静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配有allocate函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手动实现。

  • 分配效率:

    堆:堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低很多。

    栈:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执。

虽然栈有如此多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生意想不到的结果。

1.1.2

1.1.2.1重载全局的new和delete操作符
可以很容易地重载newdelete操作符
void* operator(size_t size)
{
void*p=malloc(size);
return p;
}
void operator delete(void*p)
{
free(p);
}

也可以对单个类的new和delete操作符重载。因此可以灵活地控制对象的内存分配

class TestClass {

public:

void * operator new(size_t size);

void operator delete(void *p);

// .. other members here ...
};

void *TestClass::operator new(size_t size)
{
void *p = malloc(size); // Replace this with alternative allocator
{
return (p);
}

void TestClass::operator delete(void *p)
{
free(p); // Replace this with alternative de-allocator
}

所有TestClass对象的内存分配都采用这段代码。更进一步,任何从TestClass继承的类也都采用这一方式,除非它自己也重载了newdelete操作符。通过重载newdelete操作符的方式,可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象。
1.1.2.2为单个的类重载new[]和delete[]

必须小心对象数组的分配。你可能希望调用到被你重载过的new和delete操作符,但并不如此。内存的请求被定向到全局的new[]和delete[]操作符,而这些内存来自于系统堆。

C++将对象数组的内存分配作为一个单独操作,而不同于单个对象的内存分配。为了改变这种方式,你同样需要重载new[]和delete[]操作符

class TestClass {

public:

void * operator new[ ](size_t size);

void operator delete[ ](void *p);

// .. other members here ..

};

void *TestClass::operator new[ ](size_t size)

{

void *p = malloc(size);

return (p);

}

void TestClass::operator delete[ ](void *p)

{

free(p);

}

int main(void)

{

TestClass *p = new TestClass[10];

// ... etc ...

delete[ ] p;

} 

但是注意,对于大多数C++的实现,new[]操作符的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。应该尽量避免分配对象数组,从而使你的内存分配策略简单。

1.1.3常见的内存错误及其对策

  • 内存分配未成功,你却使用了它:

    在使用内存前检查指针是否为NULL,如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。

  • 内存分配虽然成功,但是尚未初始化就引用它:

    两个原因:一是没有初始化观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略。

  • 内存分配成功并且已经初始化,但操作越过了内存的边界:

    例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

  • 忘了释放内存,造成内存泄漏:

    含有这种错误的函数每被调用一次就丢失一块内存。刚开始系统的内存太足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

    动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

  • 释放了内存还继续使用:

    有三种情况:

    • 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
    • 函数的return语句写错了,注意不要返回指向“栈内存”或者“引用”,因为该内存在函数体结束时被自动销毁。
    • 使用free或delete释放了内存之后,没有将指针设置为NULL,导致产生了“野指针”。

    **规则1:**用malloc或new申请内存之后,没有将指针设置为NULL。防止使用指针值为NULL的内存。

    **规则2:**不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

    **规则3:**避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”的操作。

    **规则4:**动态内存的申请与释放必须配对,防止内存泄漏。

    **规则5:**用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

1.1.4指针与数组的对比

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命周期内保持不变,只有数组的内容可以改变。

指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

1.1.4.1修改内容
下面示例中,字符数组a的容量时6个字符,其内容为hello。a的内容可以改变,如a[0]='X'。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。
    
char a[]="hello";
a[0]='X';
cout<<a<<endl;
const char*p="world";//注意p指向常量字符串
p[0]='X';//error
cout<<p<<endl;
1.1.4.2内容复制与比较
不能对数组名进行复制与比较。若想把数组a的内容复制给数组b,不能用语句b=a,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用b==a来判断,应该用标准库函数strcmp进行比较。

语句p=a并不能把a的内容复制给指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串赋值。同理,语句p==a比较的不是内容而是地址,应该用库函数strcmp来比较。
    
char a[]="hello";
char b[10];
strcpy(b,a);
if(strcmp(b,a)==0);

int len=strlen(a);
char*p=(char*)malloc(sizeof(char)*(len+1));
strcpy(p,a);
if(strcmp(b,a)==0);
1.1.4.3计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。如下示例中sizeof(a)的值时12(注意别忘' ''\0')。指针p指向a,但是sizeof(p)的值却时4.这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
    
char a[]="hello world";
char*p=a;
cout<<sizeof(a)<<endl;//12字节
cout<<sizeof(p)<<endl;//4字节
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量时多少,sizeof(a)始终等于sizeof(char*)。
    
void Func(char a[100])
{
    cout<<sizeof(a)<<endl;//4字节而不是100字节
}

1.1.5指针参数时如何传递内存的?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str,100)并没有使str获得期望的内存,str依旧是NULL,为什么?

void GetMemory(char*p,int num)
{
    p=(char*)malloc(sizeof(char)*num);
}

void Test(void)
{
    char*str=NULL;
    GetMemory(str,100);//str仍为NULL
    strcpy(str,"hello");//运行错误
}

毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本使_p,编译器使_p=p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相对应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
    
如果非得要用指针参数去申请内存,那么应该改用”指向指针的指针“

void GetMemory2(char**p,int num)
{
    *p=(char*)malloc(sizeof(char)*num);
}
void Test2(void)
{
    char* str=NULL;
    GetMemory(&str,100);//注意参数是&str,而不是str
    strcpy(str,"hello");
    cout<str<<endl;
    free(str);
}
由于”指向指针的指针“这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单。
 
char* GerMemory3(int num)
{
    char*p=(char*)malloc(sizeof(char)*num);
    return p;
}
void Test3(void)
{
    char*str=NULL;
    str=GetMemory3(100);//调用完GetMemory3函数后,指针p会被回收,但此时str已经指向p所指的那块地址。
    strcpy(str,"hello");
    cout<<str<<endl;
    free(str);
}
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动死亡。
    
char*GetString(void)
{
    char p[]="hello world";
    return p;
}
void Test4(void)
{
    char*str=NULL;
    str=GetString;//str的内容是垃圾
    cout<<str<<endl;
}
用调试器逐步跟踪Test4,发现执行str=GetString语句后str不再是NULL指针,但是str的内容不是“hello world”,而是垃圾。
    
如果把上述示例改写成如下示例,会怎么样?

char*GetString2(void)
{
    char*p="hello world";
    return p;
}
void Test5(void)
{
    char*str=NULL;
    str=GetString2();
    cout<<str<<endl;
}
函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错的。因为GetStirng2内的“hello world”是常量字符串,位于静态存储区,它在生命期内恒定不变,无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。

1.1.6杜绝野指针

”野指针“不是NULL指针,是指向”垃圾“内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是”野指针“是很危险的,if语句对它不起作用。”野指针“的成因主要有三种:

  • 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 指针被free后者delete之后,没有置为NULL,让人误以为p是个合法的指针。

  • 指针操作超越了变量的作用域范围。例如:

    class A
    {
    public:
        void Func(void){cout<<"Func of class A"<<endl;}
    };
    void Test(void)
    {
        A*p;
        {
            A a;
            p=&a;//注意a的生命周期
        }
        p->Func();//p是”野指针“
    }
    函数Test在指行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了野指针。

1.1.7有了malloc/free为什么还要new/delete?

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数,由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理。

class Obj
{
public:
    Obj(void){cout<<"Initialization"<<endl;}
    ~Obj(void){cout<<"Destory"<<endl;}
    void Initialize(void){cout<<"Initialization"<<endl;}
    void Destory(void){cout<<"Destory"<<endl;}
};
void UseMallocFree(void)
{
    Obj*a=(Obj*)malloc(sizeof(Obj));//申请动态内存
    a->Initialize();
    a->Destory();
    free(a);
}
void UseNewDelete(void)
{
    Obj*a=new Obj;//申请动态内存并初始化
    delete a;
}

类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的”对象“没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

如果用free释放”new创建的动态对象“,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放”malloc申请的动态内存“,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

1.1.8内存耗怎么办?

如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常由三种方式处理”内存耗尽“问题。

  • 判断指针是否为NULL,如果是则马上用return语句终止本函数。
  • 判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。
  • 为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。

上述前两种方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么第一种方法就显得力不从心(释放内存很麻烦),应该用第二种方法处理。

很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”

不行。如果发生“内存耗尽”,一般来说应用程序已经无药可救。如果不用exit(1)把坏程序杀死,它可能会害死操作系统。

对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。

不加错误处理将导致程序的质量很差,千万不可因小失大。

1.1.9malloc/free的使用要点

函数malloc的原型:
void*malloc(size_t size);

用malloc申请一块长度为length的整数类型的内存
int*p=(int*)malloc(sizeof(int)*length);
重点:“类型转换”和“sizeof”
    
函数free的原型如下:
void free(void*memblock);

为什么free函数不像malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

1.1.10new/delete的使用要点

运算符new使用起来要比函数malloc简单得多
int*p2=new int[length];
这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可有多种形式。
    
delete释放对象数组时,不要丢了[]

1.2C++中的健壮指针的资源管理

C++11新特性.md(5)

1.2.1第一条规则(RAII)

1.2.2Smart Pointers

1.2.3Resource Transfer

1.2.4Strong Pointers

1.2.5Parser

1.2.6Transfer Semantics

C++程序中资源管理的宗旨是:将资源封装到一些轻量级的类中,并由类负责它们的释放。

所有用new操作符分配的资源都会被储存并传递进Strong Pointer(标准库中的auto_ptr)的内部

以传值方式被传递的对象有value semantics 或者称为 copy semantics。Strong Pointers是以值方式传递的--但是我们能说它们有copy semantics吗?不是这样的!它们所指向的对象肯定没有被拷贝过。事实上,传递过后,源auto_ptr不在访问原有的对象,并且目标auto_ptr成为了对象的唯一拥有者(但是往往auto_ptr的旧的实现即使在释放后仍然保持着对对象的所有权)。自然而然的我们可以将这种新的行为称作Transfer Semantics。

通过定义相应的拷贝构造函数和重载赋值操作符,你可以将Transfer Semantics加入到许多对象中。例如,许多Windows中的资源,比如动态建立的菜单或者位图,可以用有Transfer Semantics的类来封装。

1.2.7Strong Vectors

当需要对一些动态分配的对象排序的时候。你将它们的指针保存到一个strong vector中。然后你用一个标准的vector来保存从strong vector中获得的weak指针。你可以用标准的算法对这个vector进行排序。这种中介vector叫做permutation vector。相似的,你也可以用标准的maps, priority queues, heaps, hash tables等等。

1.2.8Code Inspection

1.2.9共享的所有权

共享的责任分配给被共享的对象和它的客户(client)。一个共享资源必须为它的所有者保持一个引用计数。另一方面,所有者再释放资源的时候必须通报共享对象。最后一个释放资源的需要在最后负责free的工作。

最简单的共享的实现是共享对象继承引用计数的类RefCounted:

class RefCounted

{

public:

RefCounted () : _count (1) {}

int GetRefCount () const { return _count; }

void IncRefCount () { _count++; }

int DecRefCount () { return --_count; }

private

int _count;

};

1.2.10所有权网络

(1) 首先,在你的工程中建立基本的Strong Pointer。然后通过查找代码中的new来开始封装裸指针。

(2) 最先封装的是在过程中定义的临时指针。简单的将它们替换为auto_ptr并且删除相应的delete。如果一个指针在过程中没有被删除而是被返回,用auto_ptr替换并在返回前调用release方法。在你做第二次传递的时候,你需要处理对release的调用。注意,即使是在这点,你的代码也可能更加"精力充沛"--你会移出代码中潜在的资源泄漏问题。

(3) 下面是指向资源的裸指针。确保它们被独立的封装到auto_ptr中,或者在构造函数中分配在析构函数中释放。如果你有传递所有权的行为的话,需要调用release方法。如果你有容器所有对象,用Strong Pointers重新实现它们。

(4) 接下来,找到所有对release的方法调用并且尽力清除所有,如果一个release调用返回一个指针,将它修改传值返回一个auto_ptr。

(5) 重复着一过程,直到最后所有new和release的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其他资源进行相似的操作。

(6) 你会发现资源管理清除了许多错误和异常处理带来的复杂性。不仅仅你的代码会变得精力充沛,它也会变得简单并容易维护。

2.内存泄漏

2.1C++动态内存分配引发问题的解决方案(深拷贝&浅拷贝问题)

/* String.h */

#ifndef STRING_H_

#define STRING_H_

class String

{

private:

char * str; //存储数据

int len; //字符串长度

public:

String(const char * s); //构造函数

String(); // 默认构造函数

~String(); // 析构函数

friend ostream & operator<<(ostream & os,const String& st);

};

#endif

/*String.cpp*/

#include <iostream>

#include <cstring>

#include "String.h"

using namespace std;

String::String(const char * s)

{

len = strlen(s);

str = new char[len + 1];

strcpy(str, s);

}//拷贝数据

String::String()

{

len =0;

str = new char[len+1];

str[0]='"0';

}

String::~String()

{

cout<<"这个字符串将被删除:"<<str<<'"n';//为了方便观察结果,特留此行代码。

delete [] str;

}

ostream & operator<<(ostream & os, const String & st)

{

os << st.str;

return os;

}

/*test_right.cpp*/

#include <iostream>

#include <stdlib.h>

#include "String.h"

using namespace std;

int main()

{

String temp("天极网");

cout<<temp<<'"n';

system("PAUSE");

return 0;

}

C++类有以下这些极为重要的函数:

一:复制构造函数。

二:赋值函数。

C++提供了默认的复制构造函数,问题也就出在这儿。

1):什么时候会调用复制构造函数呢?(以String类为例。)

  在我们提供这样的代码:String test1(test2)时,它会被调用;当函数的参数列表为按值传递,也就是没有用引用和指针作为类型时,如:void show_String(const String),它会被调用。其实,还有一些情况,但在这儿就不列举了。

(2):它是什么样的函数。

它的作用就是把两个类进行复制。拿String类为例,C++提供的默认复制构造函数是这样的:

String(const String& a)

{

str=a.str;

len=a.len;

}

在平时,这样并不会有任何的问题出现,但我们用了new操作符,涉及到了动态内存分配,我们就不得不谈谈浅复制和深复制了。以上的函数就是实行的浅复制,它只是复制了指针,而并没有复制指针指向的数据,可谓一点儿用也没有。打个比方吧!就像一个朋友让你把一个程序通过网络发给他,而你大大咧咧地把快捷方式发给了他,有什么用处呢?我们来具体谈谈:

假如,A对象中存储了这样的字符串:“C++”。它的地址为2000。现在,我们把A对象赋给B对象:String B=A。现在,A和B对象的str指针均指向2000地址。看似可以使用,但如果B对象的析构函数被调用时,则地址2000处的字符串“C++”已经被从内存中抹去,而A对象仍然指向地址2000。这时,如果我们写下这样的代码:cout<<A<<endl;或是等待程序结束,A对象的析构函数被调用时,A对象的数据能否显示出来呢?只会是乱码。而且,程序还会这样做:连续对地址2000处使用两次delete操作符,这样的后果是十分严重的!

本例中,有这样的代码:

String* String1=new String(test1);

cout<<*String1<<endl;

delete String1;

  假设test1中str指向的地址为2000,而String中str指针同样指向地址2000,我们删除了2000处的数据,而test1对象呢?已经被破坏了。大家从运行结果上可以看到,我们使用cout<<test1时,一点反应也没有。而在test1的析构函数被调用时,显示是这样:“这个字符串将被删除:”。

再看看这段代码:

cout<<"使用错误的函数:"<<endl;

show_String(test2);

cout<<test2<<endl;//这一段代码出现严重的错误!

show_String函数的参数列表void show_String(const String a)是按值传递的,所以,我们相当于执行了这样的代码:String a=test2;函数执行完毕,由于生存周期的缘故,对象a被析构函数删除,我们马上就可以看到错误的显示结果了:这个字符串将被删除:?=。当然,test2也被破坏了。解决的办法很简单,当然是手工定义一个复制构造函数喽!人力可以胜天!

String::String(const String& a)
{
len=a.len;
str=new char(len+1);
strcpy(str,a.str);
}

  我们执行的是深拷贝。这个函数的功能是这样的:假设对象A中的str指针指向地址2000,内容为“I am a C++ Boy!”。我们执行代码String B=A时,我们先开辟出一块内存,假设为3000。我们用strcpy函数将地址2000的内容拷贝到地址3000中,再将对象B的str指针指向地址3000。这样,就互不干扰了。

大家把这个函数加入程序中,问题就解决了大半,但还没有完全解决,问题在赋值函数上。我们的程序中有这样的段代码:

String String3;

String3=test4;

  经过我前面的讲解,大家应该也会对这段代码进行寻根摸底:凭什么可以这样做:String3=test4???原因是,C++为了用户的方便,提供的这样的一个操作符重载函数:operator=。所以,我们可以这样做。大家应该猜得到,它同样是执行了浅复制,出了同样的毛病。比如,执行了这段代码后,析构函数开始大展神威^_^。由于这些变量是后进先出的,所以最后的String3变量先被删除:这个字符串将被删除:第四个范例。很正常。最后,删除到test4的时候,问题来了:这个字符串将被删除:?=。原因我不用赘述了,只是这个赋值函数怎么写,还有一点儿学问呢!大家请看:

平时,我们可以写这样的代码:x=y=z。(均为整型变量。)而在类对象中,我们同样要这样,因为这很方便。而对象A=B=C就是A.operator=(B.operator=(c))。而这个operator=函数的参数列表应该是:const String& a,所以,大家不难推出,要实现这样的功能,返回值也要是String&,这样才能实现A=B=C。我们先来写写看:

String& String::operator=(const String& a)

{

delete [] str;//先删除自身的数据

len=a.len;

str=new char[len+1];

strcpy(str,a.str);//此三行为进行拷贝

return *this;//返回自身的引用

}

是不是这样就行了呢?我们假如写出了这种代码:A=A,那么大家看看,岂不是把A对象的数据给删除了吗?这样可谓引发一系列的错误。所以,我们还要检查是否为自身赋值。只比较两对象的数据是不行了,因为两个对象的数据很有可能相同。我们应该比较地址。以下是完好的赋值函数:

String& String::operator=(const String& a)

{

if(this==&a)

return *this;

delete [] str;

len=a.len;

str=new char[len+1];

strcpy(str,a.str);

return *this;

}

把这些代码加入程序,问题就完全解决,下面是运行结果:

  下面分别输入三个范例:

  第一个范例

  第二个范例

  第三个范例

  第一个范例

  这个字符串将被删除:第一个范例。

  第一个范例

   使用正确的函数:

  第二个范例。

  第二个范例。

   使用错误的函数:

  第二个范例。

  这个字符串将被删除:第二个范例。

  第二个范例。

  String2: 第三个范例。

  String3: 第四个范例。

  下面,程序结束,析构函数将被调用。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第二个范例。

  这个字符串将被删除:第一个范例。

2.2如何对付内存泄漏

写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new 操作、delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。
    
模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。

  如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了标准库中的auto_ptr,使需要为之负责的地方变得明确了。

#include<memory>

#include<iostream>

using namespace std;

struct S {

 S() { cout << "make an S"n"; }

 ~S() { cout << "destroy an S"n"; }

 S(const S&) { cout << "copy initialize an S"n"; }

 S& operator=(const S&) { cout << "copy assign an S"n"; }

};

S* f()

{

 return new S; // 谁该负责释放这个S?

};

auto_ptr<S> g()

{

 return auto_ptr<S>(new S); // 显式传递负责释放这个S

}

int main()

{

 cout << "start main"n";

 S* p = f();

 cout << "after f() before g()"n";

 // S* q = g(); // 将被编译器捕捉

 auto_ptr<S> q = g();

 cout << "exit main"n";

 // *p产生了内存泄漏

 // *q被自动释放

}

在更一般的意义上考虑资源,而不仅仅是内存。

如果在你的环境中不能系统地应用这些技巧,那么注意使用一个内存泄漏检测器作为开发过程的一部分,或者插入一个垃圾收集器(garbage collector)。

2.3浅谈C/C++内存泄漏及其检测工具

2.3.1内存泄漏的定义

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:
    
void MyFunction(int nSize)

{

 char* p= new char[nSize];

 if( !GetStringFrom( p, nSize ) ){

  MessageBox(“Error”);

  return;

 }

 …//using the string pointed by p;

 delete p;

}

当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。
    
广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。
    
    GDI Object的泄漏是一种常见的资源泄漏:

void CMyView::OnPaint( CDC* pDC )

{

 CBitmap bmp;

 CBitmap* pOldBmp;

 bmp.LoadBitmap(IDB_MYBMP);

 pOldBmp = pDC->SelectObject( &bmp );

 …

 if( Something() ){

  return;

 }

 pDC->SelectObject( pOldBmp );

 return;

}

  当函数Something()返回非零的时候,程序在退出前没有把pOldBmp选回pDC中,这会导致pOldBmp指向的HBITMAP对象发生泄漏。这个程序如果长时间的运行,可能会导致整个系统花屏。这种问题在Win9x下比较容易暴露出来,因为Win9x的GDI堆比Win2k或NT的要小很多。

2.3.2内存泄漏的发生方式

以发生的方式来分类,内存泄漏可以分为4类:

  • **常发性内存泄漏:**发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。

  • **偶发性内存泄漏:**发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  • **一次性内存泄漏:**发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。

  • **隐式内存泄漏:**程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

class Connection

{

 public:

  Connection( SOCKET s);

  ~Connection();

  …

 private:

  SOCKET _socket;

  …

};

class ConnectionManager

{

 public:

  ConnectionManager(){}

  ~ConnectionManager(){

   list::iterator it;

   for( it = _connlist.begin(); it != _connlist.end(); ++it ){

    delete (*it);

   }

   _connlist.clear();

  }

  void OnClientConnected( SOCKET s ){

   Connection* p = new Connection(s);

   _connlist.push_back(p);

  }

  void OnClientDisconnected( Connection* pconn ){

   _connlist.remove( pconn );

   delete pconn;

  }

 private:

  list _connlist;

};

假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

2.3.3检测内存泄漏

检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。这里只是简单的描述了检测内存泄漏的基本原理,详细的算法可以参见Steve Maguire的<>。

  如果要检测堆内存的泄漏,那么需要截获住malloc/realloc/free和new/delete就可以了(其实new/delete最终也是用malloc/free的,所以只要截获前面一组即可)。对于其他的泄漏,可以采用类似的方法,截获住相应的分配和释放函数。比如,要检测BSTR的泄漏,就需要截获SysAllocString/SysFreeString;要检测HMENU的泄漏,就需要截获CreateMenu/ DestroyMenu。(有的资源的分配函数有多个,释放函数只有一个,比如,SysAllocStringLen也可以用来分配BSTR,这时就需要截获多个分配函数)

  在Windows平台下,检测内存泄漏的工具常用的一般有三种,MS C-Runtime Library内建的检测功能;外挂式的检测工具,诸如,Purify,BoundsChecker等;利用Windows NT自带的Performance Monitor。这三种工具各有优缺点,MS C-Runtime Library虽然功能上较之外挂式的工具要弱,但是它是免费的;Performance Monitor虽然无法标示出发生问题的代码,但是它能检测出隐式的内存泄漏的存在,这是其他两类工具无能为力的地方。

3.探讨C++内存回收

3.1C++内存对象大会战

C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。

3.1.1基本概念

  • **栈:**一般用于存放局部变量或对象。

    Type fun(Type object);
    
      这个函数至少产生两个临时对象,首先,参数是按值传递的,所以会调用拷贝构造函数生成一个临时对象object_copy1 ,在函数内部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,如果我们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2,这个临时对象会在函数返回后一段时间内被释放。
          
    Type tt ,result ; //生成两个栈对象
    
    tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
    
      上面的第二个语句的执行情况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,然后再调用赋值运算符执行
    
    tt = object_copy2 ; //调用赋值运算符
    编译器在我们毫无知觉的情况下,为我们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销可能是很大的,所以,你也许明白了,为什么对于“大”对象最好用const引用传递代替按值进行函数参数传递了。
        
    • **堆:**自由存储区,它是在程序执行的过程中动态分配的,所以它最大的特性就是动态性。在C++中,所有堆对象的创建和销毁都要由程序员负责,所以,如果处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如果已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。

    • **静态对象:**所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之前,会调用由编译器生成的exit函数,来释放所有的全局对象。

      局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。

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

      子类共享父类的静态对象

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的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让我们看看下面的示例代码:

#include <stdlib.h> //需要用到C式内存分配函数

class Resource ; //代表需要被封装的资源类

class NoHashObject

{

 private:

  Resource* ptr ;//指向被封装的资源

  ... ... //其它数据成员

  void* operator new(size_t size) //非严格实现,仅作示意之用

  {

   return malloc(size) ;

  }

  void operator delete(void* pp) //非严格实现,仅作示意之用

  {

   free(pp) ;

  }

 public:

  NoHashObject()

  {

   //此处可以获得需要封装的资源,并让ptr指针指向该资源

   ptr = new Resource() ;

  }

  ~NoHashObject()

  {

   delete ptr ; //释放封装的资源

  }

}; 

NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码:

NoHashObject* fp = new NoHashObject() ; //编译期错误!

delete fp ; 

上面代码会产生编译期错误。好了,现在你已经知道了如何设计一个禁止堆对象的类了,你也许和我一样有这样的疑问,难道在类NoHashObject的定义不能改变的情况下,就一定不能产生该类型的堆对象了吗?不,还是有办法的,我称之为“暴力破解法”。C++是如此地强大,强大到你可以用它做你想做的任何事情。这里主要用到的技巧是指针类型的强制转换。
    
void main(void)

{

 char* temp = new char[sizeof(NoHashObject)] ;

 //强制类型转换,现在ptr是一个指向NoHashObject对象的指针

 NoHashObject* obj_ptr = (NoHashObject*)temp ;

 temp = NULL ; //防止通过temp指针修改NoHashObject对象

 //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员

 Resource* rp = (Resource*)obj_ptr ;

 //初始化obj_ptr指向的NoHashObject对象的ptr成员

 rp = new Resource() ;

 //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了

 ... ...

 delete rp ;//释放资源

 temp = (char*)obj_ptr ;

 obj_ptr = NULL ;//防止悬挂指针产生

 delete [] temp ;//释放NoHashObject对象所占的堆空间。

} 

  上面的实现是麻烦的,而且这种实现方式几乎不会在实践中使用,但是我还是写出来了,因为理解它,对于我们理解C++内存对象是有好处的。对于上面的这么多强制类型转换,其最根本的是什么了?我们可以这样理解:

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

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

  另外要提醒的是,不同的编译器对对象的成员数据的布局安排可能是不一样的,比如,大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动作像我们预期的那样执行:

Resource* rp = (Resource*)obj_ptr ; 

  但是,并不一定所有的编译器都是如此。

3.1.5禁止产生栈对象

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

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

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

  为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:

class NoStackObject

{

 protected:

  ~NoStackObject() { }

 public:

  void destroy()

  {

   delete this ;//调用保护析构函数

  }

}; 

  接着,可以像这样使用NoStackObject类:

NoStackObject* hash_ptr = new NoStackObject() ;

... ... //对hash_ptr指向的对象进行操作

hash_ptr->destroy() ; 

我们用new创建一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。所以,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?我们可以用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就可以用这种方式实现。)
    
class NoStackObject

{

 protected:

  NoStackObject() { }

  ~NoStackObject() { }

 public:

  static NoStackObject* creatInstance()

  {

   return new NoStackObject() ;//调用保护的构造函数

  }

  void destroy()

  {

   delete this ;//调用保护的析构函数

  }

};
现在可以这样使用NoStackObject类了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;

... ... //对hash_ptr指向的对象进行操作

hash_ptr->destroy() ;

hash_ptr = NULL ; //防止使用悬挂指针 

栈对象特点:自动生成和释放。只要将构造和析构函数不设为public,编译器就不会自动构造和析构了,这时候就需要我们手动去生成和释放对象了。如果想让该类成为基类,则构造和析构函数设为protected;如果不想,就设为private接即可。

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

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

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

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

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

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

4. union

只需要知道union中的变量共享一块内存即可