当基类、派生类用,或者不用动态内存时,共有四种情况:
(注意,前提是基类的动态内存分配相关函数符合常规使用动态内存的要求)
情况一:基类 使用 动态内存分配、派生类新增数据成员 不使用 动态内存分配
假如基类使用动态内存分配(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基类类型。