本片段将介绍运行期而不是编译期的内存分配
1.变量的内存分配和方法的前期绑定
函数中声明的局部变量与其参数以及簿记数据一起被放置在一个活动记录中。活动记录存储在名为运行期栈(run-time stack)的应用程序内存中。
函数调用
当创建对象时,对象数据成员的存储也在当前执行函数或者方法的活动记录中。
大多数情况,程序只需要自动内存管理和前期绑定。以下两种情况前期绑定无法解决:
(1)需要利用多态
(2)需要访问的对象在创建它的函数或方法之外
2.需要解决的问题
程序会在编译器判断调用方法的版本,会与声明一个对象时的类型相匹配,而不是和之后定义的派生类相匹配,在此需要一种方法告诉编译器在程序运行的时候判断需要执行的代码,这就是所谓的后期绑定,是多态的特征。为解决这个问题,需要两个工具:指针变量和虚函数。
3.指针和程序的自由存储
(1)C++内存分配
为利用后期绑定,不想让对象成为运行期栈上的活动记录,于是操作系统为代码设置了内存(称为代码存储或者文本存储),为全局变量和静态变量设置了内存(称为静态存储),程序还被给予了额外的内存,称为自由存储(free store)或者应用程序堆(application heap),可以在这里存储数据。
new运算符在自由存储中分配内存
运行期栈中的变量所拥有的内存是自动分配与释放的,而自由存储中的变量即使在创建它们的函数或者方法终止之后都会存在。(容易内存泄漏)
用指针指向对象,e.g.
MagicBox
注意:如果声明了一个指针变量,但是没有立即创建一个对象供其引用,则应该将这个指针设置为nullptr。如下的赋值是必要的,因为C++不会初始化指针。
ToyBox<int>* myToyPtr=nullptr;
(2)释放内存
当指针变量所指的内存不再需要之后,需要使用delete运算符将其释放,然后将指针变量的值设置为nullptr,表示这个变量不再引用或者指向任何对象。
delete
如果在此示例中没有将somePtr设置为nullptr,那么somePtr就是应该悬挂指针(dangling pointer),因为这个指针仍然保存被释放对象的地址,悬挂指针是严重错误的来源。
(3)避免内存泄漏
①当在自由存储中创建了对象,但程序无法再访问这个对象时,就发生了内存泄漏。
MagicBox<string>* myBoxPtr=new MagicBox<string>();
MagicBox<string>* yourBoxPtr=new MagicBox<string>();
yourBoxPtr=myBoxPtr;//Results is inaccessible object
解决办法:应该将yourBoxPtr初始化为nullptr,或者是用myBoxPtr赋初值。
②当函数或者方法在自由存储中创建了对象,并且由于没有将指针你返回给调用者或者没有将其存储在类数据成员中而丢失了指向对象的指针时,就会发生更加微妙的内存泄漏。
//不合理的函数在自由存储中分配内存
void myLeakyFunction(const double& someItem)
{
ToyBox<double>* someBoxPtr=new ToyBox<double>();
someBoxPtr->setItem(someItem);
}//end myLeakyFunction
someBoxPtr存储在运行期栈中,函数结束即销毁,创建的对象存放在自由存储中,无法获取其地址而发生内存泄漏。
解决办法:在函数终止前删除对象;不要再自由存储中分配内存,而是使用局部变量;若函数内部创建的对象再外部会用到,则可以返回指向对象的指针,使用指针的代码段负责删除对象,注释中应该注明,但仍有错误使用的风险,如直接调用而不赋给其他指针。
ToyBox<double>* pluggedLeakyFunction(const double& someItem)
{
ToyBox<double>* someBoxPtr=new ToyBox<double>();
someBoxPtr->setItem(someItem);
return someBoxPtr;
}//end pluggedLeakyFunction
pluggedLeakyFunction(boxValue)//Misused;returned pointer is lost
为了防止内存泄漏,最好的方法不是使用函数返回新创建对象的指针,而是定义一个类,类中的方法完成这一任务。类负责删除自由存储中的对象,确保不会发生内存泄漏。这个类最少有三个部分:在自由存储中创建对象的方法、指向这个对象的数据字段,以及当类的实例不再需要的时候删除这个对象的方法,也就是析构函数。
通常,编译器生成的析构函数对类而言已经足够,但是如果类本身使用new运算符创建了对象,为了安全起见,实现析构函数时应该确保为对象分配的内存被释放。
(4)避免悬挂指针
可能导致悬挂指针的情况
①如果在使用delete之后不将指针变量设置为nullptr
②如果声明了一个指针变量但是不对其赋值
③两个指针指向同一个对象,删除了其中一个指针并置空,另一个指针成为悬挂指针
MagicBox<string>* myBoxPtr=new MagicBox<string>();
MagicBox<string>* yourBoxPtr=myBoxPtr;
delete myBoxPtr;
myBoxPtr=nullptr;//共同指向的对象已不存在
yourBoxPtr->getItem();//无法调用其方法
悬挂指针的解决办法:
- 初始化或者不需要的时候,指针变量设置为nullptr
- 减少别名的使用
- 删除对象时,将所有引用这个被删除对象的别名设置为nullptr
4.虚方法和多态
实现多态需要编译器执行后期绑定,为此必须将基类的方法声明为virtual。
为了实现后期绑定,必须在自由存储中创建变量并使用指针指向这些变量。
关于虚方法的要点:
- 虚方法是派生类可以重写的方法。
- 必须实现类的虚方法(纯虚方法不包含在内)。
- 派生类不需要重写被继承的虚方法的已有实现。
- 类的任何方法都可以是虚方法。当然,如果不想让派生类重写某些特定的方法,那么这些方法就不应该是虚方法。
- 析构函数不能是虚方法。
- 析构函数可以是也应该是虚方法。虚析构函数确保了对象的后代可以正确地释放自身。
- 虚方法的返回类型不能被重写。
5.数组的动态分配
int arraySize=50;
double* anArray=new double[arraySize];
//可以在程序运行期对arraySize赋值,改变数组的大小
delete []anArray;
数组大小用完后分配更多空间,并将原有数组复制过来
double* oldArray=anArray; //Copy pointer to array
anArray=new double(2*arraySize) //Double array size
for(int index=0;index<arraySize;index++) //Copy old array
anArray[index]=oldArray[index];
delete []oldArray; //Deallocate old array
本文参考《C++数据抽象和问题求解》第6版 清华大学出版社 [美]Frank M.Carrano Timothy Henry著 景丽译