继承和动态内存分配
今天这篇文章来聊聊继承与动态内存分配。
这里面有一个问题,当我们的基类使用动态内存分配,并且重新定义赋值和复制构造函数,这会对派生类的实现有什么影响呢?
我们来看两种情况。
派生类不用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);
- };
在这个声明里包含了构造函数、析构函数、复制构造函数和重载赋值运算符。
现在假设我们从baseDMA派生出了类lackDMA,但是后者不使用new:
- class lackDMA: public baseMDA {
- private:
- char color[40];
- public:
- ...
- };
问题来了,我们要不要给lackDMA这个类定义析构函数、复制构造函数和赋值运算符呢?
答案是不需要。
首先是析构函数,这个很好想明白,如果我们没有定义析构函数,那么编译器会自动定义一个不执行任何操作的默认析构函数。实际上派生类的析构函数往往会在执行一些逻辑之后调用基类的构造函数,因为lackDMA类中的成员不是通过new创建的,因此不需要额外的操作,所以默认析构函数是合适的。
同样的默认复制构造函数也会执行非new创建成员的复制,所以对于color变量来说是没问题的。并且在派生类当中,默认复制构造函数除了会复制非new创建的成员之外,还会调用基类的复制构造函数来复制父类成员的部分。所以,对于派生类lackDMA来说,我们使用默认的复制构造函数一样没有问题。
赋值也是一样的,默认的赋值运算符也会自动使用基类的赋值运算符来对基类的成员进行赋值。
派生类使用new
我们再来看看派生类当中使用了new的情况。
- class hasDMA: public baseMDA {
- private:
- char *style;
- public:
- ...
- };
在hasDMA这个类当中,我们添加了一个需要使用new创建的char*成员。在这种情况下,我们就没办法使用默认的函数了,就必须定义显式析构函数、复制构造函数和赋值运算符了,我们一个一个来看。
首先是析构函数,派生类的析构函数会自动调用基类的析构函数,所以我们只需要在析构函数当中释放派生类中独有的成员变量即可。
- hasDMA::~hasDMA() {
- delete []style;
- }
然后我们再来看看拷贝构造函数,由于派生类不能访问基类private成员,所以我们需要调用基类的拷贝构造函数。
- hasDMA::hasDMA(const hasDMA& hs): baseDMA(hs) {
- style = new char[std::strlen(hs.style) + 1];
- std::strcpy(style, hs.style);
- }
最后是赋值运算符,同样,由于派生类不能访问基类中私有成员,我们也需要借助基类的赋值运算符:
- 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;
- }
这当中有一个语句看起来有些奇怪:
- baseDMA::operator=(hs);
这是我们手动显式调用了基类的赋值运算符,我们直接用等于号赋值也有同样的效果:
- *this = hs;
为什么不这么干呢?这是因为编译器在执行的时候会默认调用子类的赋值运算符hasDMA::operator=,从而导致一直递归导致死循环。
所以我们需要手动写明作用域解析符,表明这是调用的父类赋值运算符,而非派生类的运算符,这一点比较隐晦,要千万注意。