8.1.3 析构函数与动态内存分配
我们以后会发现,程序中经常需要为类的数据成员动态分配内存。我们可以在构造函数中使用new运算符来为对象成员分配空间。在这种情况下,我们必须提供适当的析构函数,在不再需要该对象时释放空间。下面首先定义一个简单的类,以进行这样的练习。
假设我们希望定义一个类,其中每个对象都是描述性的消息(如文本串)。这个类应该尽可能高效地利用内存,因此不能将数据成员定义成足以容纳所需最大长度字符串的char数组。我们应该在创建对象时在自由存储器中为消息分配内存。类定义如下所示:
- //Listing 08_01
- class CMessage
- {
- private:
- char* pmessage; // Pointer to object text string
-
- public:
-
- // Function to display a message
- void ShowIt() const
- {
- cout << endl << pmessage;
- }
-
- // Constructor definition
- CMessage(const char* text = "Default message")
- {
- pmessage = new char[strlen(text) + 1];
// Allocate space for text - strcpy(pmessage, text); // Copy text to new memory
- }
-
- ~CMessage(); // Destructor prototype
- };
该类仅仅定义了一个数据成员pmessage。该成员是一个指向文本串的指针,是在类的private部分定义的,因此不能从类外部访问。
在public部分,ShowIt()函数将CMessage对象输出到屏幕上。我们还定义了构造函数,并添加了类析构函数的原型~CMessage()-- 我们很快就会讨论它。
类的构造函数要求实参是字符串,但如果不传递任何实参,它将使用为形参指定的默认字符串。构造函数通过使用库函数strlen(),获得字符串实参的长度(不包括终止空字符)。为了使构造函数能够使用strlen()库函数,程序中必须有嵌入<cstring>头文件的#include语句。通过将trlen()函数返回的数值加1,构造函数即可求出在自由存储器中存储该字符串所需的内存字节数。
注意:
当然,如果内存分配失败,则将抛出异常,程序终止。如果我们希望管理此类故障,以便程序顺利运行,那么应该在构造函数代码中捕获此类异常(见第6章关于处理内存不足状况的信息)。
在使用new运算符获得供字符串使用的内存之后,我们使用也是在<cstring>头文件中声明的strcpy()库函数,将给构造函数提供的字符串实参复制到为字符串分配的内存中。strcpy()函数将第二个指针实参指定的字符串,复制到第一个指针实参包含的地址中。
我们现在需要编写类的析构函数,以释放为消息分配的内存。如果不给该类提供析构函数,程序将无法释放为类对象分配的内存。如果按照现状在创建大量CMessage对象的程序中使用这个类,那么自由存储器将逐渐被耗尽,直至程序失败为止。在不容易发现此类问题的环境中,却很容易出现上述现象。例如,如果我们要在一个被程序调用许多次的函数中创建临时的CMessage对象,则可能认为该对象将在从函数返回时被销毁。当然,这种看法是正确的,只是自由存储器中的内存没有被释放。因此,每调用一次该函数,就有更多的自由存储器内存被抛弃的CMessage对象占用。
CMessage类析构函数的代码如下所示:
- // Listing 08_02
- // Destructor to free memory allocated by new
- CMessage::~CMessage()
- {
- cout << "Destructor called." // Just to track what happens
- << endl;
- delete[] pmessage; // Free memory assigned to pointer
- }
因为是在类定义外部定义析构函数,所以我们必须以类名CMessage限定析构函数名。析构函数的作用只是显示一条消息,告诉我们所发生的事情,然后使用delete运算符释放pmessage成员指向的内存。注意,delete后面的方括号是必需的,因为我们是在删除数组(char类型)。
试一试:使用消息类
通过下面这个小小的示例,我们可以练习CMessage类的用法。
- // Ex8_02.cpp
- // Using a destructor to free memory
- #include <iostream> // For stream I/O
- #include <cstring> // For strlen() and strcpy()
- using std::cout;
- using std::endl;
-
- // Put the CMessage class definition here (Listing 08_01)
-
- // Put the destructor definition here (Listing 08_02)
-
- int main()
- {
- // Declare object
- CMessage motto("A miss is as good as a mile.");
-
- // Dynamic object
- CMessage* pM = new CMessage("A cat can look at a queen.");
-
- motto.ShowIt(); // Display 1st message
- pM->ShowIt(); // Display 2nd message
- cout << endl;
-
- // delete pM; // Manually delete object created with new
- return 0;
- }
记着用CMessage类和析构函数定义代替此处代码中的注释,如果没有它们,该程序将不能编译(下载的源代码中含有本示例的所有代码)。
示例说明
在main()的开始部分,我们以通常的方式声明并定义了一个已初始化的CMessage对象motto。在第二条声明语句中,定义了一个指向CMessage对象的指针pM,并使用new运算符为该指针指向的CMessage对象分配内存。对new运算符的调用将调用CMessage类的构造函数,结果是再次调用new运算符为数据成员pmessage指向的消息文本分配空间。如果我们编译并执行该示例,那么将得到下面的输出:
- A miss is as good as a mile.
- A cat can look at a queen.
- Destructor called.
虽然我们创建了两个CMessage对象,但输出中只记录了一次析构函数调用。前面说过,编译器不负责删除在自由存储器中创建的对象。编译器之所以为对象motto调用析构函数,是因为虽然该对象的数据成员占用的内存是由构造函数在自由存储器中分配的,但它只是一个普通的自动对象。pM指向的对象就不同了。我们在自由存储器中为该对象分配内存,因此必须使用delete将其删除。我们需要使下面这条出现在main()中return语句之前的语句不再是注释形式:
- // delete pM; // Manually delete object created with new
如果现在运行该程序,则得到下面的输出:
- A miss is as good as a mile.
- A cat can look at a queen.
- Destructor called.
- Destructor called.
现在,析构函数被调用了两次。显然,delete只处理函数中new运算符分配的内存,即只释放指针pM指向的内存。因为pM指向一个CMessage对象(该类的析构函数已经定义过),所以delete还要调用析构函数来释放该对象的成员所占用的内存。因此,当我们使用delete删除new运算符动态创建的对象时,delete将在释放该对象占用的内存之前,首先调用该对象的析构函数。