当基类、派生类用,或者不用动态内存时,共有四种情况:
(注意,前提是基类的动态内存分配相关函数符合常规使用动态内存的要求)
情况一:基类 使用 动态内存分配、派生类新增数据成员 不使用 动态内存分配
假如基类使用动态内存分配(new),其必然设定①构造函数、②复制构造函数、③赋值运算符、④析构函数。
又知,派生类的构造函数(包括默认构造函数),需要调用基类的构造函数。
那么基类的数据成员若使new,则已经没问题。
派生类新增的数据成员,由于不使用new,因此可以按值传递。
①对于派生类构造函数:直接将参数赋值对应的数据成员——ok;
②对于派生类复制构造函数,使用默认的复制构造函数(因此对于派生类新增数据对象使用按值传递,对于基类的数据对象使用基类的复制构造函数)——ok;
③对于赋值运算符来说,使用默认的赋值运算符,是按值传递(因此对派生类新增的数据成员,按值传递,对基类数据对象,使用基类的赋值运算符函数),如果有特殊需求,则显式调用基类的赋值运算符(A::operator=(b); //显式调用基类赋值运算符函数),然后对派生类的数据成员进行处理(③可以参考情况三的赋值运算符处理)——ok;
④对于析构函数来说,会先调用派生类的析构函数,然后调用基类的析构函数——ok。
因此:假如派生类新增数据成员不使用动态内存的话,如无特别需求,可以无需特别设置。
使用默认的构造函数、复制构造函数、赋值运算符、析构函数即可。
如代码:
class A //基类 { char*name; public: A(const char*q) //构造函数 { name = new char[strlen(q) + 1];strcpy_s(name, strlen(q) + 1, q); } A(const A&a) //复制构造函数 { name = new char[strlen(a.name) + 1];strcpy_s(name, strlen(a.name) + 1, a.name); } virtual ~A() { delete[]name; } //析构函数 A&operator=(const A&a) //赋值运算符 { if (this == &a)return *this; //为防止自己赋值自己 delete[]name;name = new char[strlen(a.name) + 1]; strcpy_s(name, strlen(a.name) + 1, a.name); return *this; } friend std::ostream& operator<<(std::ostream&os, const A&b) { os << b.name; return os; } }; class B :public A //基类的派生类 { int id; public: B(const char*a, int q) :A(a) //构造函数 { id = q; } B(const B&b) :A(b) //复制构造函数,可省略,使用默认复制构造函数 { id = b.id; } B&operator=(const B&b) //赋值运算符,可省略,使用默认的赋值运算符 { if (this == &b)return *this; //为防止自己赋值自己 A::operator=(b); //显式调用基类赋值运算符函数 id = b.id + 5; //为了区分而修改 return *this; } friend std::ostream& operator<<(std::ostream&os, const B&b) { os << A(b) << "," << b.id; return os; } };
情况二:基类 不使用 动态内存分配、派生类新增数据成员 不使用 动态内存分配
和情况一并没有什么区别。如无特殊需求,使用默认的复制构造函数、赋值运算符即可。
情况三:基类 使用 动态内存分配、派生类新增数据成员 使用 动态内存分配
由于派生类新增数据成员使用动态内存分配,那么显然,不能使用默认构造函数、默认的复制构造函数、默认的赋值运算符、默认的析构函数了(否则无法形成new和delete的对应)。
①构造函数:
首先,构造函数需要调用基类的构造函数,是毫无疑问的。因此,基类的动态内存分配ok;
其次,对于派生类新增的数据成员,对于需要使用动态内存的,使用new运算符来分配内存。不使用动态内存的,常规处理,因此ok;
②复制构造函数:
首先,复制构造函数需要调用基类的复制构造函数。将派生类对象作为参数传递给基类初始化列表,由于基类引用可以指向派生类对象,因此可以成功初始化派生类的基类部分。
然后,对派生类新增的部分,对于需要使用动态内存的,使用new运算符来分配内存。不使用动态内存的,常规处理,因此ok;
③赋值运算符:
首先,需要显式的调用基类的赋值运算符(A::operator=(b); //显式调用基类赋值运算符函数),以使得基类的部分被成功赋值;
其次,对于派生类部分,按照正常方式处理使用动态内存的数据成员(delete后再new),和不使用动态内存的数据成员。
最后,注意在函数的开始部分,添加防止自己赋值给自己的代码。
④析构函数:
由于派生类的析构函数,会自动调用基类的析构函数,因此,只对派生类新增的数据成员进行delete释放内存处理。
如代码(只修改了派生类B):
class B :public A //基类的派生类 { char* id; public: B(const char*a, char*q) :A(a) //构造函数 { id=new char[strlen(q)+1]; strcpy_s(id, strlen(q) + 1, q); } B(const B&b) :A(b) //复制构造函数 { id = new char[strlen(b.id) + 1]; strcpy_s(id, strlen(b.id) + 1, b.id); } ~B() //析构函数,处理派生类新增数据成员 { delete[]id; } B&operator=(const B&b) //赋值运算符 { if (this == &b)return *this; //为防止自己赋值自己 A::operator=(b); //显式调用基类赋值运算符函数 delete[]id; //需要先delete id = new char[strlen(b.id) + 1]; strcpy_s(id, strlen(b.id) + 1, b.id); return *this; } friend std::ostream& operator<<(std::ostream&os, const B&b) { os << A(b) << "," << b.id; return os; } };
情况四:基类 不使用 动态内存分配、派生类新增数据成员 使用 动态内存分配
和情况三的办法一样(因为情况三使用基类的各种方法来处理基类部分的数据)。
总结:
①我忘了给基类加析构函数;
②我加了析构函数,忘了给析构函数加关键字virtual变成虚函数。
对于友元函数:
如果想在派生类的友元函数中调用基类的友元函数,那么应该使用强制类型转换,即 基类名(派生类对象) ,即可使用,如A(b)
也可以这样:(const 基类名&)派生类对象 表示强制转换派生类对象为const基类类型。