如题,为什么类用到的动态内存分配(DMA),其就必须定义显式析构函数、复制构造函数和赋值运算符呢?
一、派生类无DMA的情况(基类有DMA)
// lacks.h
class BaseDMA {// Base Class Using DMA
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
void showB() const;
virtual ~BaseDMA();
};
class LacksDMA :public BaseDMA {// derived class without DMA
private:
enum { COL_LEN = 40 };
char color[COL_LEN];
public:
LacksDMA(const char* c = "blank", const char* l = "null", int r = 0);
void showL() const;
};
// lacks.cpp
#include "lacks.h"
#include <iostream>
#include <cstring>
// BaseDMA methods
BaseDMA::BaseDMA(const char* l, int r) {
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}
void BaseDMA::showB() const {
std::cout << "Label: " << label << std::endl;
std::cout << "Rating: " << rating << std::endl;
}
BaseDMA::~BaseDMA() {
delete[] label;
}
// LacksDMA methods
LacksDMA::LacksDMA(const char* c, const char* l, int r) : BaseDMA(l, r) {
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
}
void LacksDMA::showC() const {
showB();
std::cout<< "Color: " << color << std::endl;
}
#include "lacks.h"
int main() {
LacksDMA ldma = LacksDMA("red", "Nike", 8);
ldma.showC();
return 0;
}
Label: Nike
Rating: 8
Color: red
分析:我们知道,基类永远是先于派生类构造创建的。上述代码,显式地调用了基类的构造函数(不显式调用就会默认调用基类的默认构造函数)为的是初始化基类的数据成员。这时,基类的成员lable动态分配了内存,这个字符指针指向的内容存储在堆内存中。在程序块运行结束时,自动对象ldma生命周期结束,系统自动销毁它,这时就调用了LacksDMA类的默认析构函数(无需处理任何逻辑),因为基类中的析构函数被声明是虚的,所以会自动调用基类的析构函数(否则不会自动调用),释放掉给成员lable分配的内存。这显然没任何问题,因为派生类不需要额外地做任何事。
二、派生类有DMA的情况(基类有DMA)
// dma.h
class BaseDMA {// Base Class Using DMA
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
void showB() const;
virtual ~BaseDMA();
};
class HasDMA :public BaseDMA {// derived class Using DMA
private:
enum { COL_LEN = 40 };
char color[COL_LEN];
char* style;
public:
HasDMA(const char* c = "blank", const char* s = "null", const char* l = "null", int r = 0);
void showC() const;
};
// dma.cpp
#include "dma.h"
#include <iostream>
#include <cstring>
// BaseDMA methods
BaseDMA::BaseDMA(const char* l, int r) {
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}
void BaseDMA::showB() const {
std::cout << "Label: " << label << std::endl;
std::cout << "Rating: " << rating << std::endl;
}
BaseDMA::~BaseDMA() {
delete[] label;
}
// HasDMA methods
HasDMA::HasDMA(const char* c, const char* s, const char* l, int r) : BaseDMA(l, r) {
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
void HasDMA::showC() const {
showB();
std::cout<< "Color: " << color << std::endl;
std::cout<< "Style: " << style << std::endl;
}
#include "dma.h"
int main() {
HasDMA hdma = HasDMA("red", "Running", "Nike", 8);
hdma.showC();
return 0;
}
Label: Nike
Rating: 8
Color: red
Style: Running
分析:程序虽然能正常运行,但程序结束时,显然出现了内存泄漏,即成员style分配的堆内存未主动释放。故:“类有DMA,其就必须定义显式析构函数”。
在dma.cpp中给派生类添加一个显式析构函数释放堆内存就OK了:
HasDMA::~HasDMA() {
delete[] style;
}
?如果涉及调用对象的复制构造函数呢?
// run_dam.cpp
#include "dma.h"
int main() {
HasDMA dma0 = HasDMA("red","Running","Nike",8);//#1 构造函数直接初始化dma0
HasDMA dma1 = dma0;//#2 调用默认【复制构造函数】初始化dma1
return 0;
}
先谈谈类的几种默认函数:
- A();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 默认构造函数(实例化一个默认成员数据值的对象)
- A(const A&);? ? ? ? ? ? ? ? ? ? ? ?//?默认复制构造函数(完成基本类型数据类型的拷贝)
- A& operator=(const A&)? ? ?//?默认赋值运算符(完成基本类型数据类型的拷贝)
- ~A();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 默认析构函数(什么也不做)
我们看run_dma.cpp中#2这行代码,它们调用了基类和派生类的默认复制构造函数,如下:
>>>>>>>>>>>>>>>> 默认复制构造函数 <<<<<<<<<<<<<<<<<<<<<<
// 基类
BaseDMA::BaseDMA(const BaseDMA& bd) {
label = bd.label;
rating = bd.rating;
}
// 派生类
HasDMA::HasDMA(const HasDMA& hs) : BaseDMA(hs) {
style = hs.style;
<!-- 成员color是数组,不是基本数据类型,默认不处理对它的浅复制 -->
}
注:默认生成的赋值运算符会自动调用基类的赋值运算符。显式定义派生类的赋值运算符, 编译器不会自动调用基类的赋值运算符。
在run_dma.cpp中,#2行:?HasDMA dma1 = dma0; 调用默认复制构造函数用dma0初始化dma1,这时char*类型的style被赋值为一个地址,这个地址正是dma0中成员变量style的地址,地址里存的内容没被复制,也就是所谓的浅拷贝。事实上,将以上代码加入到run_dma.cpp中,程序运行到#2行就自动终止了。
断点分析:经断点跟踪调试,发现派生类和基类的复制构造函数确实都被调用了,且调用顺序是先基类的后派生类,前提是派生类通过初始化列表显式调用了基类的复制构造函数。首先,程序创建有且两个对象dma0和dma1,问题出现在析构函数,对象创建顺序dma0、dma1,析构函数调用顺序dma1、[dma1基类]、dma0、[dma0基类]。析构dma1、[dam1基类]时:delete[] style;、delete[] label;?然后析构dma0时,delete[] style;?这个dma0的style和上次delete[]了的dma1的style地址相同,因为是浅复制过来的,所以是对同一地址进行第二次释放空间,所以程序运行错误!如果析构dma0和[dam0基类]时,对应的style和lable是新创建的地址空间,那程序才正常。这就是浅拷贝——只拷贝了指针地址,出的问题。应该进行深拷贝,给它们另行分配地址&空间。所以,用默认的复制构造函数不能解决问题,故:“类有DMA,其就必须定义显式复制构造函数(深度复制)”。
?如果涉及调用对象的赋值运算符呢?
// run_dma.cpp
#include "dma.h"
int main() {
HasDMA dmaM = HasDMA("red","Running","Nike",8);//#1 构造函数直接初始化dmaM
HasDMA dmaN;//#2 默认构造函数创建对象dmaN
dmaN = dmaM;//#3 调用默认【赋值运算符】将dmaM赋值给dmaN
return 0;
}
其实赋值运算符和复制构造函数的函数体代码是相似的,调试发现派生类和基类的赋值运算符都被调用了,调用了基类赋值运算符是因为在派生类中显式调用了基类的赋值运算符。
>>>>>>>>>>>>>>>> 默认赋值运算符 <<<<<<<<<<<<<<<<<<<<<<<<<
// 基类
BaseDMA& BaseDMA::operator=(const BaseDMA& bd) {
label = bd.label;
rating = bd.rating;
return *this;
}
// 派生类
HasDMA& HasDMA::operator=(const HasDMA& hs) {
BaseDMA::operator=(hs);// copy base portion
style = hs.style;
<!-- 成员color是数组,不是基本数据类型,默认不处理对它的浅复制 -->
return *this;
}
加上述代码添加到run_dma.cpp中,程序运行,问题同样出现在析构时。与上述的断点分析是一样的。故:“类有DMA,其就必须定义显式赋值运算符(深度复制)”。
总之,原因都是默认的复制构造函数、默认的赋值运算符,没法很好地处理动态内存分配。只有自己去显式地去定义而不是用默认的才能处理得完备,才能不让Bug产生!
现在给出完备的代码:自定义析构函数、自定义复制构造函数、自定义赋值运算符
// dma.h
class BaseDMA {// Base Class Using DMA
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
void showB() const;
virtual ~BaseDMA();
// Add
BaseDMA(const BaseDMA& bd);
BaseDMA& operator=(const BaseDMA& bd);
};
class HasDMA : public BaseDMA {// Derived class Using DMA
private:
enum { COL_LEN = 40 };
char color[COL_LEN];
char* style;
public:
HasDMA(const char* c = "blank", const char* s = "null", const char* l = "null", int r = 0);
void showC() const;
// Add
~HasDMA();
HasDMA(const HasDMA& hs);
HasDMA& operator=(const HasDMA& hs);
};
// dma.cpp
#include "dma.h"
#include <iostream>
#include <cstring>
BaseDMA::BaseDMA(const char* l, int r) {
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}
void BaseDMA::showB() const {
std::cout << "Label: " << label << std::endl;
std::cout << "Rating: " << rating << std::endl;
}
// Add destructor
BaseDMA::~BaseDMA() {
delete[] label;
}
// Add copy constructor
BaseDMA::BaseDMA(const BaseDMA& bd) {
label = new char[std::strlen(bd.label) + 1];
std::strcpy(label, bd.label);
rating = bd.rating;
}
// Add assignment operator
BaseDMA& BaseDMA::operator=(const BaseDMA& bd) {
if (this == &bd)
return *this;
delete[] label;// prepare for new style
label = new char[std::strlen(bd.label) + 1];
std::strcpy(label, bd.label);
rating = bd.rating;
return *this;
}
// —————————————————————————————————————————————————————————————————————————————————
HasDMA::HasDMA(const char* c, const char* s, const char* l, int r) : BaseDMA(l, r) {
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
void HasDMA::showC() const {
showB();
std::cout << "Color: " << color << std::endl;
std::cout << "Style: " << style << std::endl;
}
// Add destructor
HasDMA::~HasDMA() {
delete[] style;
}
// Add copy constructor
HasDMA::HasDMA(const HasDMA& hs) : BaseDMA(hs) {
std::strncpy(color, hs.color, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
// Add assignment operator
HasDMA& HasDMA::operator=(const HasDMA& hs) {
if (this == &hs)
return *this;
BaseDMA::operator=(hs);// copy base portion
std::strncpy(color, hs.color, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
delete[] style;// prepare for new style
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
测试代码: (程序完美运行)
#include "stock.h"
int main() {
HasDMA dma0 = HasDMA("red","Running","Nike",8);//#1 构造函数直接初始化dma0
HasDMA dma1 = dma0;// 调用【复制构造函数】将dma1初始化为dma0
HasDMA dma2;// 默认构造函数创建对象dma2
dma2 = dma0;// 调用【赋值运算符】将dma0赋值给dma1
dma2.showC();
return 0;
}
说明:
- 对于复制构造函数,派生类要通过初始化列表去调用基类的复制构造函数,否则的话,基类的数据成员没完成复制!
- 对于赋值运算符,派生类要显式调用基类的赋值运算符,否则的话,基类的数据成员没完成复制!
- 【复制构造函数】发生在用一个已存在的对象去初始化一个之前不存在的新对象或都生成一个临时对象,在这两种情况下,显然新对象也好临时对象也好,由于都是刚生成的,所以它们的lable/style成员是默认初始状态,从未给它们分配过内存,所以在复制构造函数里,不需要delete[] lable/style。【赋值运算符】发生在将一个已存在的对象赋值给另一个已存在的对象,这个赋值操作并不创建产生新对象,就是老对象的数据成员拷贝的操作,在这一情况下,被赋值的一方已经完成了lable/style成员的初始化,所以要先delete[]?然后再重新给它们分配空间。这就是为什么复制构造函数里没有delete[] lable/style;这样的语句,而赋值运算符里有的原因。
- 为何重载赋值运算符函数最前面有代码 if (this == &hs) { return *this; }?呢?因为this代码的是调用对象的地址,是operator(this, hs)第一个参数,&hs是第二个参数的地址,如果this == &hs,就说明是同一个对象,比如:HasDMA* hs =?new HasDMA(...); HasDMA* hx = hs;?这时就有&hs == &hx。所以,最开始的判断是为了避免就是同一对象,那样不必进行其他操作了,直接返回调用者自己(*this)就完事了。
|