在继承中,需要特别注意对动态内存的处理,当操作基类和派生类对象时,如果管理不当,就会造成如内存的二次释放、派生类内存未释放等问题。申请内存时需要在构造函数(尤其是拷贝构造函数)、赋值运算符中使用new
来申请,在释放内存时则需要在析构函数中使用delete
释放。
下面就来看看继承中和如何合理地通过拷贝构造、赋值运算符、析构函数来管理动态内存。
1.基类中未使用动态内存分配,派生类中未使用动态内存分配
-
拷贝构造函数:编译器提供的默认拷贝构造进行成员复制,对于基类的成员,会自动使用基类拷贝构造函数进行复制,由于不存在new分配内存,因此默认拷贝构造函数可以满足成员的复制。
-
赋值运算符:编译器提供的默认赋值运算符进行成员复制,对于基类的成员将自动使用基类的赋值运算符进行成员复制,由于不存在new分配内存,因此默认赋值运算符可以满足成员的复制。
-
析构函数: 若派生类中不提供,编译器提供默认析构函数,由于不存在new分配内存,因此可以使用默认析构函数。
示例:
//dma_in_extends.h
namespace DMA_V1{
class Person
{
private:
int age;
char name[40];
public:
Person(const char * ch = "null", int age = 0);
virtual void show() const;
};
class Student : public Person
{
private:
char school[40];
public:
Student(const char * name = "null", const char * school = "null", int age = 0);
virtual void show() const;
};
}
// dma_in_extends.cpp
#include <iostream>
#include <cstring>
#include "dma_in_extends.h"
namespace DMA_V1 {
Person::Person(const char * ch, int age) {
std::strcpy(name,ch);
this->age = age;
}
void Person::show() const {
std::cout << "Name: " << name <<std::endl;
std::cout << "age: " << age << std::endl;
}
Student::Student(const char * name, const char * school, int age) : Person(name,age) {
std::strcpy(this->school,school);
}
void Student::show() const {
Person::show();
std::cout << "School: " << school << std::endl;
}
}//end DMA_V1
2.基类中未使用动态内存分配,派生类中使用动态内存分配
-
拷贝构造函数:由于派生类中存在new分配的内存,而默认拷贝构造函数只进行成员复制,因此必须在派生类中重写拷贝构造,基类中无要求(默认拷贝构造可以满足)。
-
赋值运算符:由于派生类中存在new分配的内存,默认赋值运算符也只进行成员的复制,因此需要重载赋值运算符。基类中无要求(默认赋值运算符可以满足)。
-
析构函数: 由于派生类中存在new分配内存,因此基类中应声明虚析构函数,以确保正确释放内存,派生类中必须提供析构函数,并在析构函数中使用delete释放内存。
示例:
//dma_in_extends.h
namespace DMA_V2{
class Person
{
private:
int age;
char name[40];
public:
Person(const char * ch = "null", int age = 0);
virtual void show() const;
//由于派生类中存在new分配内存,因此需要声明为虚析构
virtual ~Person();
};
class Student : public Person
{
private:
char * school;
public:
Student(const char * name = "null", const char * school = "null", int age = 0);
virtual void show() const;
//由于char * school的存在,需要定义拷贝构造、赋值运算符和析构函数
Student(const Student & stu);
Student & operator=(const Student & stu);
virtual ~Student();
};
}//enf DMA_V2
//dma_in_extends.cpp
namespace DMA_V2 {
Person::Person(const char * ch, int age) {
std::strcpy(name,ch);
this->age = age;
}
void Person::show() const {
std::cout << "Name: " << name <<std::endl;
std::cout << "age: " << age << std::endl;
}
Person::~Person() {
std::cout << "~Person()" << ",delete " << name << std::endl;
}
Student::Student(const char * name, const char * school, int age) : Person(name,age) {
int len = std::strlen(school);
this->school = new char[len + 1];
std::strcpy(this->school,school);
}
Student::Student(const Student & stu):Person(stu) {
int len = std::strlen(stu.school);
this->school = new char[len + 1];
std::strcpy(this->school,stu.school);
}
Student & Student::operator=(const Student & stu) {
if (this == & stu)
return *this;
delete [] school;
Person::operator=(stu);//复制基类中的成员
int len = std::strlen(stu.school);
this->school = new char[len + 1];
std::strcpy(this->school,stu.school);
return *this;
}
Student::~Student() {
std::cout << "~Student()" << " ,delete:" << school << std::endl;
delete [] school;
}
void Student::show() const {
Person::show();
std::cout << "School: " << school << std::endl;
}
}//end DMA_V2
3.基类中使用动态内存分配,派生类中未使用动态内存分配。
-
拷贝构造函数:由于存在new分配的内存,默认拷贝构造仅仅对成员进行复制,因此需要实现拷贝构造。而派生类中不存在new内存,因此可以使用默认拷贝构造函数。
-
赋值运算符:由于存在new分配的内存,默认赋值运算符仅仅对成员进行复制,因此需要重载赋值运算符。而派生类中不存在new分配内存,因此可以使用默认赋值运算符,。
-
析构函数: 由于基类中存在new内存,因此必须提供析构函数,并在析构函数中使用delete释放内存。而派生类中不存在new内存,因此可以使用默认析构函数。
示例:
//dma_in_extends.h
namespace DMA_V3{
class Person
{
private:
int age;
char * name;
public:
Person(const char * ch = "null", int age = 0);
virtual void show() const;
//由于需要new 分配内存,因此需要重写拷贝构造,赋值运算符和虚析构
Person(const Person & p);
Person & operator=(const Person & p);
virtual ~Person();
};
class Student : public Person
{
private:
char school[40];
public:
Student(const char * name = "null", const char * school = "null", int age = 0);
virtual void show() const;
};
}//end DMA_V3
//dma_in_extends.cpp
namespace DMA_V3 {
Person::Person(const char * ch, int age) {
int len = std::strlen(ch);
name = new char[len + 1];
std::strcpy(name,ch);
this->age = age;
}
Person::Person(const Person & p) {
int len = std::strlen(p.name);
name = new char[len + 1];
std::strcpy(name,p.name);
this->age = p.age;
}
Person & Person::operator=(const Person & p) {
int len = std::strlen(p.name);
name = new char[len + 1];
std::strcpy(name,p.name);
this->age = p.age;
return *this;
}
Person::~Person() {
std::cout << "~Person(), delete " << name << " object" << std::endl;
delete [] name;
}
void Person::show() const {
std::cout << "Name: " << name <<std::endl;
std::cout << "age: " << age << std::endl;
}
Student::Student(const char * name, const char * school, int age) : Person(name,age) {
std::strcpy(this->school,school);
}
void Student::show() const {
Person::show();
std::cout << "School: " << school << std::endl;
}
}//end DMA_V3
4.基类中使用动态内存分配,派生类中使用动态内存分配。
-
拷贝构造函数:由于存在new分配的内存,默认拷贝构造仅仅对成员进行复制,因此需要在基类和派生类中都要重写拷贝构造。
-
赋值运算符:由于存在new分配的内存,默认赋值运算符仅仅对成员进行复制,因此需要在基类和派生类中都要重写赋值运算符。
-
析构函数: 由于存在new分配的内存,因此基类和派生类都必须提供析构函数,析构函数中使用delete释放内存,并在将基类析构声明为虚析构。
示例:
//dma_in_extends.h
namespace DMA_V4 {
class Person
{
private:
int age;
char * name;
public:
Person(const char * ch = "null", int age = 0);
Person(const Person & p);
Person & operator=(const Person & p);
~Person();
virtual void show() const;
};
class Student : public Person
{
private:
char * school;
public:
Student(const char * name = "null", const char * school = "null", int age = 0);
Student(const Student & stu);
Student & operator=(const Student & stu);
virtual ~Student();
virtual void show() const;
};
}//end DMA_V4
//dma_in_extends.cpp
namespace DMA_V4 {
Person::Person(const char * ch, int age) {
int len = std::strlen(ch);
name = new char[len + 1];
std::strcpy(name,ch);
this->age = age;
}
Person::Person(const Person & p) {
int len = std::strlen(p.name);
name = new char[len + 1];
std::strcpy(name,p.name);
this->age = p.age;
}
Person & Person::operator=(const Person & p) {
int len = std::strlen(p.name);
name = new char[len + 1];
std::strcpy(name,p.name);
this->age = p.age;
return *this;
}
Person::~Person() {
delete [] name;
}
void Person::show() const {
std::cout << "Name: " << name <<std::endl;
std::cout << "age: " << age << std::endl;
}
Student::Student(const char * name, const char * school, int age) : Person(name,age) {
int len = std::strlen(school);
this->school = new char[len + 1];
std::strcpy(this->school,school);
}
Student::Student(const Student & stu):Person(stu) {
int len = std::strlen(stu.school);
this->school = new char[len + 1];
std::strcpy(this->school,stu.school);
}
Student & Student::operator=(const Student & stu) {
int len = std::strlen(stu.school);
this->school = new char[len + 1];
std::strcpy(this->school,stu.school);
}
Student & Student::operator=(const Student & stu) {
if (this == & stu)
return *this;
delete [] school;
Person::operator=(stu);
int len = std::strlen(stu.school);
this->school = new char[len + 1];
std::strcpy(this->school,stu.school);
return *this;
}
Student::~Student() {
delete [] school;
}
void Student::show() const {
Person::show();
std::cout << "School: " << school << std::endl;
}
}//end DMA_V4
总结
总而言之,若基类和派生类中都存在动态内存分配时,必须重写它们的拷贝构造、析构函数和赋值运算符,且派生类中必须使用相应的基类方法来处理继承自基类元素,但对于这三种函数,它们的处理方式有所不同,通过上述示例我们也看到:
- 1.对于拷贝构造函数,在派生类中是通过成员初始化列表调用积累的拷贝构造完成的。
- 2.对于赋值运算符,在派生类中是通过使用作用域解析符显式地调用基类的赋值运算符来完成的。
- 3.对于析构函数,编译器将自动调用。