C++ 继承与动态内存分配
一、问题导入
(1)继承是怎样与动态内存分配(即使用new与delete)进行互动的呢?
(2)假如基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?取决于派生类的属性。
根据上述两个问题,我们接下来看看如下两种情况下,继承与动态内存分配的关系:
二、第一种情况:派生类不使用new
2.1 基类使用动态内存分配
假如基类使用了动态内存分配,如下代码所示:
// 基类
// 声明中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造函数和重载赋值运算符
class BaseDMA
{
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
BaseDMA(const BaseDMA& rs);
virtual ~BaseDMA();
BaseDMA& operator=(const BaseDMA& rs);
};
// 派生类
// 派生类不使用new,也未包含其他一些不常用的、需要特殊处理的设计特性
class LacksDMA : public BaseDMA
{
private:
char color[40];
public:
// ...
};
问: 在上述代码示例情况下,是否需要为派生类定义显式析构函数、复制构造函数和赋值运算符?
答: 不需要。
2.2 析构函数
对于2.1小节代码示例:
(1)如果派生类(LacksDMA)没有定义析构函数,编译器将定义一个不执行任何操作的默认析构函数。
(2)实际上,派生类的默认构造函数总是要进行一些操作:执行自身的代码后调用基类析构函数。
(3)假设派生类(LacksDMA)成员不需执行任何特殊操作,所以默认析构函数是合适的。
2.3 复制构造函数
对于2.1小节代码示例:
(1)默认复制构造函数执行成员复制,这对于动态内存分配来说是不合适的,但对于派生类(LacksDMA)成员来说是合适的,因为派生类成员不涉及new与delete动态内存分配。
(2)此示例只需要考虑继承的BaseDMA对象。
(3)成员复制是根据数据类型采用相应的复制方式,因此将long复制到long中是通过使用常规赋值完成的,但复制类成员或继承的类组件时,则是使用该类的复制构造函数完成的。
(4)派生类(LacksDMA)的默认复制构造函数使用显式BaseDMA复制构造函数来复制LacksDMA对象的BaseDMA部分。
(5)默认复制构造函数对于派生类(LacksDMA)成员来说是合适的,同时对于继承的BaseDMA对象来说也是合适的。
2.4 赋值运算符
对于2.1小节代码示例:
(1)类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,对于派生类(LacksDMA)成员来说是合适的,因为派生类成员不涉及new与delete动态内存分配。
(2)派生类对象的这些属性也适用于本身是对象的类成员。
三、第二种情况:派生类使用new
3.1 派生类使用动态内存分配
假如派生类使用了动态内存分配,如下代码所示:
// 基类
// 声明中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造函数和重载赋值运算符
class BaseDMA
{
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
BaseDMA(const BaseDMA& rs);
virtual ~BaseDMA();
BaseDMA& operator=(const BaseDMA& rs);
};
// 派生类
// 派生类使用new进行动态内存分配,如char*的内存分配
class HasDMA : public BaseDMA
{
private:
char* style;
public:
// do somthing
};
问: 在本小节代码示例情况下,是否需要为派生类定义显式析构函数、复制构造函数和赋值运算符?
答: 必须的。
3.2 析构函数
对于3.1小节代码示例:
(1)派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的清理。
(2)HasDMA析构函数必须释放指针style管理的内存,并依赖于基类BaseDMA的析构函数来释放指针label管理的内存。
析构函数进行的工作如下代码所示:
// 基类析构函数
BaseDMA::~BaseDMA
{
delete[] label;
}
// 派生类析构函数
HasDMA::~HasDMA
{
delete[] style;
}
3.3 复制构造函数
对于3.1小节代码示例:
(1)基类(BaseDMA)的复制构造函数遵循用于char数组的常规模式,如下代码示例所示:
// 基类BaseDMA复制构造函数
BaseDMA::BaseDMA(const BaseDMA& bs)
{
label = new char[std::strlen(bs.label) + 1];
std::strcpy(label, bs.label);
rating = bs.rating;
}
(2)派生类(HasDMA)复制构造函数只能访问派生类(HasDMA)的数据,因此它必须调用基类(BaseDMA)复制构造函数来处理共享的BaseDMA数据,通过成员初始化列表调用,示例代码如下所示:
// 派生类复制构造函数调用基类复制构造函数
HasDMA::HasDMA(const HasDMA& hs) : BaseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
}
(3)需要注意的是,成员初始化列表将一个派生类(HasDMA)引用传递给基类(BaseDMA)构造函数。没有参数类型为派生类(HasDMA)引用的基类(BaseDMA)构造函数,其实也不需要这样的构造函数。
(4)基类(BaseDMA)复制构造函数有一个基类(BaseDMA)引用参数,而基类引用可以指向派生类对象。因此,基类(BaseDMA)复制构造函数将使用派生类(HasDMA)参数的基类(BaseDMA)部分来构造新对象的基类(BaseDMA)部分。
3.4 赋值运算符
对于3.1小节代码示例:
(1)基类(BaseDMA)赋值运算符遵循常规模式,示例代码如下所示:
BaseDMA & BaseDMA::operator=(const BaseDMA& bs)
{
if (this == &bs)
return *this;
delete[] label;
label = new char[std::strlen(bs.label) + 1];
std::strcpy(label, bs.label);
return *this;
}
(2)派生类(HasDMA)由于也是用动态内存分配,因此也需要一个显式赋值运算符。作为派生类(HasDMA)的方法,它只能直接访问派生类(HasDMA)的数据。然而派生类的显式赋值运算符必须负责所有继承的基类(BaseDMA)
对象的赋值,可以通过显式调用基类赋值运算符来完成这项工作。如下示例代码:
HasDMA& HasDMA::operator=(const HasDMA& hs)
{
if (this == &hs)
return *this;
// 通过函数表示法加作用域解析运算符,而不是运算符表示法显式调用基类赋值运算符
BaseDMA::operator=(hs);
delete[] style;
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
(3)当派生类显式调用基类赋值运算符时,如:
BaseDMA::operator(hs);
上述语句等同于如下语句:
*this = hs;
但是使用*this=hs语句时,编译器将使用BaseDMA::operator=(),从而会在派生类的赋值运算符中形成递归调用。所以使用函数表示法可以使得赋值运算符被正确调用。
3.5 总结
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求通过三种不同的方式来满足:
(1)对于析构函数,这是自动完成的
(2)对于复制构造函数,这是通过在成员初始化列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。
(3)对于赋值运算符,这是通过使用作用域解析运算符显式调用基类的赋值运算符来完成的。
四、使用动态内存分配与友元的继承示例
4.1 派生类访问基类友元函数示例代码
通过如下代码示例说明派生类如何访问基类的友元函数,如下代码:
// .h 头文件
// 基类
class BaseDMA
{
private:
char* label;
int rating;
public:
BaseDMA(const char* l = "null", int r = 0);
BaseDMA(const BaseDMA& rs);
virtual ~BaseDMA();
BaseDMA& operator=(const BaseDMA& rs);
friend std::ostream &operator<<(std::ostream & os, const BaseDMA & rs);
};
// LacksDMA派生类
class LacksDMA : public BaseDMA
{
private:
enum { COL_LEN = 40 };
char color[COL_LEN];
public:
LacksDMA(const char* c = "blank", const char* l = "null", int r = 0);
LacksDMA(const char* c, const BaseDMA& rs);
friend std::ostream &operator<<(std::ostream & os, const LacksDMA & rs);
};
// HasDMA派生类
class HasDMA : public BaseDMA
{
private:
char * style;
public:
HasDMA(const char* s = "none", const char* l = "null", int r = 0);
HasDMA(const char* s, const BaseDMA& rs);
HasDMA(const HasDMA& hs);
~HasDMA();
HasDMA& operator=(const HasDMA& hs);
friend std::ostream& operator<<(std::ostream& os, const HasDMA& hs);
};
// .cpp 源文件
// BaseDMA基类实现
BaseDMA::BaseDMA(const char *l, int r)
{
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}
BaseDMA::BaseDMA(const BaseDMA &rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}
BaseDMA::~BaseDMA()
{
delete[] label;
label = NULL;
}
BaseDMA & BaseDMA::operator=(const BaseDMA &rs)
{
if (this == &rs)
return *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
std::ostream & operator<<(std::ostream& os, const BaseDMA& rs)
{
os << "Label: " << rs.label << std::endl;
os << "Rating: " << rs.rating << std::endl;
return os;
}
// LacksDMA派生类实现
LacksDMA::LacksDMA(const char *c, const char *l, int r)
: BaseDMA(l, r)
{
std::strncpy(color, c, 39);
color[39] = '\0';
}
LacksDMA::LacksDMA(const char *c, const BaseDMA &rs)
: BaseDMA(rs)
{
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
}
std::ostream & operator<<(std::ostream& os, const LacksDMA& ls)
{
os << (const BaseDMA&)ls;
os << "Color: " << ls.color << std::endl;
return os;
}
// HasDMA派生类实现
HasDMA::HasDMA(const char *s, const char *l, int r)
: BaseDMA(l, r)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
HasDMA::HasDMA(const char *s, const BaseDMA &rs)
: BaseDMA(rs)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
HasDMA::HasDMA(const HasDMA &hs)
: BaseDMA(hs) // 调用基类的复制构造函数
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
HasDMA::~HasDMA()
{
delete[] style;
style = NULL;
}
HasDMA& HasDMA::operator=(const HasDMA &hs)
{
if (this == &hs)
return *this;
BaseDMA::operator=(hs);
delete [] style;
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
std::ostream & operator<<(std::ostream& os, const HasDMA& hs)
{
os << (const BaseDMA&)hs;
os << "Style: " << hs.style << std::endl;
return os;
}
// main()函数
int main()
{
BaseDMA shirt("Portabelly", 8);
LacksDMA balloon("red", "Blimpo", 4);
HasDMA map("Mercator", "Buffalo Keys", 5);
std::cout << "Displaying BaseDMA object:\n";
std::cout << shirt << std::endl;
std::cout << "Displaying LacksDMA object:\n";
std::cout << balloon << std::endl;
std::cout << "Displaying HasDMA object:\n";
std::cout << map << std::endl;
LacksDMA balloon2(balloon);
std::cout << "Result of LacksDMA copy:\n";
std::cout << balloon2 << std::endl;
HasDMA map2;
map2 = map;
std::cout << "Result of HasDMA assignment:\n";
std::cout << map2 << std::endl;
}
4.2 派生类访问基类友元函数说明
对于4.1小节代码示例,需要注意的是新加入的特性,派生类如何使用基类的友元函数,尚有如下问题待明确:
(1)派生类(HasDMA)的友元函数能访问派生类(HasDMA)的style成员,但是此友元函数不是基类(BaseDMA)的友元,那它如何访问基类(BaseDMA)的成员label和rating呢?
(2)由于友元函数不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数,这种情况该如何解决呢?
解决方案是:
(1)对于问题1,解决方式是使用基类(BaseDMA)的友元函数operator<<()。
(2)对于问题2,解决方式是使用强制类型转换,以便匹配原型时能够选择正确的函数。如4.1节示例代码中,将const HasDMA& 转换成类型为const BaseDMA&的参数,如下代码所示:
friend std::ostream& operator<<(std::ostream& os, const HasDMA& hs);
std::ostream& operator<<(std::ostream& os, const HasDMA& hs)
{
// 强制类型转换,转换后调用BaseDMA的友元函数operator<<(std::ostream&, const BaseDMA&)
os << (const BaseDMA&)hs;
os << "Style: " << hs.style << std::endl;
return os;
}