类中指针数据成员的处理
一般而言,类中只含有内置类型时,只使用编译器提供的默认constructor函数、默认destructor和默认overloaded assignment operator(重载操作符)即可,但是一旦有了指针数据成员,具体的说是指向堆中的值的指针数据成员,就得另当别论了。
由于编译器添加的默认函数都比较简单,对于比较简单的类而言,通常没有什么问题,但是当类中有数据成员指向堆中的值时,什么都需要程序员自己做了。
“实践出真知”,现在拿出一个比较好的代码分析:
这是一个简单的animal的类,只有两个数据成员,但其中又一个是string*类型的,它是我们讨论的对象。
//Heap Data Member
//Demonstrates an object with a dynamically allocated data member
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal(const string& name = "", int age = 0);
~Animal(); //destructor prototype
Animal(const Animal& c); //copy constructor prototype
Animal& Animal::operator=(const Animal& c); //assignment operator
void Greet() const;
private:
string* m_pName; //★要注意的关键
int m_Age;
};
Animal::Animal(const string& name, int age)
{
cout << "(构造函数被调用)\n";
m_pName = new string(name); //另外分配内存空间
m_Age = age;
}
Animal::~Animal() //手工定义析构函数的必要性
{
cout << "(析构函数被调用)\n";
delete m_pName; //释放内存空间
}
Animal::Animal(const Animal& c) //手工定义拷贝构造函数的必要性
{
cout << "(拷贝构造函数被调用)\n";
m_pName = new string(*(c.m_pName)); //实现“深拷贝”的关键
m_Age = c.m_Age;
}
Animal& Animal::operator=(const Animal& c) //手工定义操作符重载的必要性
{
cout << "(重载操作符被调用)\n";
if (this != &c)
{
delete m_pName;//别忘了释放掉原先的内存
m_pName = new string(*(c.m_pName));
m_Age = c.m_Age;
}
return *this;
}
void Animal::Greet() const
{
cout << "你好,我叫" << *m_pName << " ,我今年" << m_Age << "岁了。 ";
cout << "&m_pName: " << cout << &m_pName << endl;
}
//声明3个测试函数
void testDestructor();
void testCopyConstructor(Animal aCopy);
void testAssignmentOp();
int main()
{
testDestructor();
cout << endl;
Animal crit("Poochie", 5);
crit.Greet();
testCopyConstructor(crit);
crit.Greet();
cout << endl;
testAssignmentOp();
return 0;
}
void testDestructor()
{
Animal toDestroy("Rover", 3);
toDestroy.Greet();
} //运行结束时,toDestroy对象随即被销毁
void testCopyConstructor(Animal aCopy) //注:不是引用类型
{
aCopy.Greet();
}
void testAssignmentOp()
{
Animal ani1("ani1", 7);
Animal ani2("ani2", 9);
ani1 = ani2;
ani1.Greet();
ani2.Greet();
cout << endl;
Animal ani3("ani", 11);
ani3 = ani3;
ani3.Greet();
}
运行结果:
可以发现:Animal对象被销毁、复制以及相互赋值是,对这些数据成员做出了不同的处理,具体分析如下。
析构函数
当对象的数据成员指向堆中的值时,可能产生的问题就是内存泄露。如果不编写自己的析构函数,则编译器会替程序员创建一个默认析构函数,但她并不尝试释放掉任何数据成员指向的堆中的内存。当类中有数据成员指向堆中值时,则应当编写自己的析构函数,以便能在对象消失以前释放掉与对象相关的堆中内存,避免内存泄露。所以在这里的析构函数必须对申请的堆中内存做相应处理:
delete m_pName; //释放内存空间
main函数中调用testDestructor()时测试了析构函数。它创建了一个toDestroy的对象,并且打印出m_pName中的堆中字符的地址。当 testDestructor()调用完毕要返回main时,自动调用了析构函数,释放掉toDestroy对象占用的内存,包括“Rover”字符串占用的堆中内存。析构函数对m_Age没有做任何处理,这完全没有问题,因为m_Age不在堆中,而是toDestroy的一部分,并且会随Animal对象的其余部分被妥善的处理。
总之:如果在堆中分配内存,则应当编写析构函数来清理与释放堆中的内存。
拷贝构造函数
同构造函数和析构函数一样简单,编译前生成的默认拷贝构造函数只是简单地将每个数据成员的值复制给新对象同名数据成员,即按成员逐项进行复制。但在我们这个函数中却不能在使用默认拷贝构造函数,原因还是m_pName的存在。如果只是用默认拷贝构造函数,对象的自动复制将会导致新的对象指向堆中的同一个字符串,因为新对象的指针仅仅获得存储在原始对象的指针中地址的一个副本。这就造成了数据的浅拷贝。真正需要的拷贝构造函数是能让新生成的对象在堆中拥有自己的内存块,对象中的每个数据成员都指向一个队中的对象,这就是深拷贝。所以在Animal类的默认拷贝构造函数需要对m_pName成员分配新的堆内存。
m_pName = new string(*(c.m_pName));
观察程序,发现当testCopyConstructor()时使用的m_pName堆中地址与main中crit使用的 m_pName地址并不相同,这就实现了堆内存数据的复制。当testCopyConstructor()执行结束时,析构函数被调用,释放了对象内存。
总之:当类的数据成员指向堆中内存时,应当考虑编写手动拷贝构造函数来为新对象分配内存,实现深拷贝。
赋值运算符的重载
同上述几个函数一样,如果程序员没有编写自己的赋值运算符成员函数,编译器就会为程序员提供一个默认的成员函数,但这也是相当简单的。
如果只是用默认的赋值运算符成员函数,也只是实现的浅拷贝。所以Animal的赋值运算符成员函数应当写成:
Animal& Animal::operator=(const Animal& c) //手工定义操作符重载的必要性
{
cout << "(重载操作符被调用)\n";
if (this != &c)
{
delete m_pName;//别忘了释放掉原先的内存
m_pName = new string(*(c.m_pName));
m_Age = c.m_Age;
}
return *this;
}
main中的testAssignmentOp()测试了
赋值运算符的重载。
总之:当类中有数据成员指向堆中内存时,应当考虑为该类重载赋值运算符。
如需转载,请注明出处:http://write.blog.youkuaiyun.com/postedit/8046379