10类外的动态内存

1类外堆内存

1.1基本概念

当类对象拥有指针或引用,指向从malloc()、new等操作符所分配的额外堆内存时,这些内存称为类外堆内存。
类对象 ——>堆内存
类外堆内存的特点,除非手动调用free(),delete()等释放内存的手段,否则这些内存单元不会随着类对象的释放而自动释放。实际上、类外堆内存是导致程序内存泄露的一个大原因,并由此产生了一众所谓智能指针的机制,这在STL库中会有所探究。

1.2默认情况下的内存泄露

如上图所示,类对象本身在变量声明周期结束时会被系统自动释放,但是堆内存不会。
以下示例代码展示了未正确编写析构函数时产生的内存泄漏的典型情形:
class A{
char *p;
public://分配1000个字节
A(){p=new char[100];}
~A(){cout<<"析构"<<endl};
};
int main(int argc,char cosnt *argv[]){
A a;
return 0;
}
可以看出,虽然程序正确执行了析构函数,但由于析构函数并未正确释放指针p所指向的堆内存导致该程序泄露了1000个字节的系统内存。

1.3析构函数正确释放内存

从上述示例代码中容易得到结结论:只要类中分配有类外堆内存,就必须重写析构函数并在其中加以释放,否则会导致内存泄露。
以下是正确的使用析构函数释放类外堆内存的一个简单示例:
class A{
char *p;
public : //分配1000个字节
A(){p = new char[1000];}
~A(){cout <<"析构"<<endl;delete[] p;}
};
int main(int argc,char const *argv[]){
A a;
return 0;
}
注意:针对不同的分配的方式,析构函数也必须要用相应的方式去释放,比如:
构造函数中使用malloc()//calloc()f分配内存,析构函数必须使用free()释放内存
构造函数中使用new分配内存、析构函数必须使用delete释放内存
构造函数中使用new[]分配内存,析构函数必须使用deletep[]释放内存

2继承关系中的动态内存管理

假设基类Base,使用了动态内存:
class Base{char data ;指向类外动态内存
int size;
public : //构造函数
Base(const char
data = "null",int size = 0);
Base(const Base &r);
//赋值操作符函数
Base &operator = (const Base &r);
//虚析构函数
//此处可暂且不管其含义,只需要假设它负责妥善释放基类资源
virtual ~Base();
}
此处,由于基类Base使用了动态内存,因此基类的构造函数和析构函数都将会有显式定义来应对动态内存的管理。
这是,当基类派生出子类后,注意考虑两方面问题:
1.子类对象在拷贝构造,赋值操作时,如何正确处理基类相关的动态内存?
2.子类对象在析构时,如何正确处理基类相关的动态内存?
以上2个问题,又可以分别细分讨论两种情形:
子类没有使用动态内存
字母有使用动态内存

2.1子类没有使用动态内存

现在,从基类Base派生子类Derived如下:
class Derived:public Base{
//假设派生类Derived没有使用动态内存
}
此种情形是非常简单的,有如下图所示“
派生类(基类)——>(管理)堆内存
因此此时子类没有使用动态内存,所有子类就无需对构造函数、析构函数和赋值操作符函数等做显式定义,而派生类的无所作为的行为不会对基类的动态内存管理造成任何问题,理由是:
构造函数、拷贝构造函数、赋值运算符函数:
子类对象在默认的上述函数中都会自动调用基类的版本、因此无需做任何额外的处理。
析构函数:
派生类对象在执行完析构函数后,会自动调用基类的析构函数(包括在默认生成的空析构函数),因此也无需做任何额外的处理。

2.1.1最终的结论

只要基类Base本身设计妥当,那么对于不带动态内存的派生类而言,其设计无需考虑任何基类的动态内存的问题。
class Derived:public Base{
//无需对构造、拷贝构造、赋值操作符、构造等类方法
做额外的处理
};