【C++课程学习】:继承:默认成员函数
🎁个人主页:我们的五年
🔍系列专栏:C++课程学习
🎉欢迎大家点赞👍评论📝收藏⭐文章
目录
构造函数
🍩默认构造函数(这里指的是编译器生成的构造函数):
🍩显式写构造函数:
拷贝构造函数:
🍩编译器自己生成的拷贝构造:
🍩显式写拷贝构造函数:
赋值重载:
析构函数:
🍩析构函数名称变化
🍩父类和子类的析构顺序
继承和友元:
继承和静态成员:
菱形继承:
继承的总结和反思:
构造函数
子类的构造函数在初始化列表时,我们发现父类的声明在子类之前。所以不管在初始化列表怎么写,都是先初始化父类,再调子类的构造。
🍩默认构造函数(这里指的是编译器生成的构造函数):
首先来看看,如果我们在子类中不显式写构造函数,看看编译器生成的默认构造会干什么事情(环境:VS2022):
我们可以把子类的成员分成三类:
1.父类的成员。(看成整体)
2.子类的内置类型。
3.子类的自定义类型。
从VS2022中可以看出,如果不写字类的默认构造函数,那么编译器生成的默认构造函数做了:
1.调用父类的构造函数,对父类成员进行初始化。
2.内置类型不做处理。(有些环境可能会初始化为0)。
3.子类的自定义类型,调用它的构造函数。(有缺省值,进行有参的构造函数)。
🍩显式写构造函数:
我们在显示写构造函数时,一定要去调用父类的构造函数,构造函数调用的规则如下:
想调用哪个构造函数就传什么参数,根据不同的参数,可以调用不同的构造函数。
class person {
public:
person() //无参的构造函数
{
cout << "person()" << endl;
}
person(int age,string name) //传age,和name的构造函数
:_age(age)
,_name(name)
{
cout << "person(int age,string name)" << endl;
}
protected:
int _age;
string _name;
};
student(int num)
:person() //父类构造函数的调用
,_num(num)
{
}
当我们显式写子类的构造函数时,但是又没有调用父类的构造函数,编译器会帮我们怎么处理?
如果我们没有调用构造函数,那么编译器会去调用父类默认构造函数(无参的构造函数)。
但是会有一个问题?,如果父类没有无参的构造函数怎么办?
此时,由于子类写构造函数时,没有调用父类的构造函数。让编译器去调用父类的无参的构造函数。结果父类还没有无参的构造函数,编译器就会报错。
解决办法就是:在子类构造函数的初始化列表中调用父类带参的构造函数。
全缺省的可以不传参,所以也可以调到。
另外写person()=default;
可以让编译器强制生成默认构造函数。
拷贝构造函数:
🍩编译器自己生成的拷贝构造:
1.对于子定义类型,调用它的拷贝构造。
2.对于内置类型,进行值拷贝。
3.对于父类,调用父类的拷贝构造。
🍩显式写拷贝构造函数:
当我们显式写拷贝构造时,我们就需要注意要去显示调用父类的拷贝构造。
当我们显式写了拷贝构造,却又没有在初始化列表调用父类的拷贝构造,编译器不会帮我们调用父类的拷贝构造,因为父类的拷贝构造是带参的。
像下面一样,如果我们要显式调用person的拷贝构造,我们要怎么给person的拷贝构造传参呢?
传过来的是student类型的对象st,怎么变成person呢?
父类和子类有这样的特点:(赋值兼容转换)
子类对象可以赋值给父类的对象,父类的指针,父类的引用。
解决办法:直接传student类型的对象st就可以了。
//父类拷贝构造
person(const person& p)
:_age(p._age)
,_name(p._name)
{
cout << "person(person & p)" << endl;
}
//子类拷贝构造
student(const student& st)
:person(st)
,_num(st._num)
{
}
赋值重载:
先来看看下面这种写法的赋值运算符重载可行不?
答案是不行的,此时父类的operator=和子类operator=构成隐藏,它会去调用子类的operator=。
然后就会进入死循环。
解决办法:在operator=前面指定父类的作用域。
person::operator=(st);
//父类(基类)的赋值重载
person& operator=(person& p)
{
if (&p != this)
{
_age = p._age;
_name = p._name;
}
return *this;
}
//子类(派生类)的赋值重载
student& operator=(student& st)
{
if (this != &st)
{
operator=(st);
_num = st._num;
}
return *this;
}
析构函数:
🍩析构函数名称变化
由于析构函数的名称最后都会被处理成destruct,所以父类的析构和子类的析构是构成隐藏关系的。要想调用父类的析构函数,必须指定在父类的作用域。
🍩父类和子类的析构顺序
必须保证先析构子类,再析构父类。
由于在构造函数中,初始化列表是按照声明的顺序进行,父类的声明在子类的前面,所以可以保证父类先初始化,子类后初始化。所以构造函数可以只调用子类构造函数。
但是在析构中,编译器会在子类对象声明周期结束时,先调用子类析构,然后再调用父类析构。
~person()
{
cout << "~person()" << endl;
}
~student()
{
cout << "~student()" << endl;
}
继承和友元:
友元关系不能继承,父类的友元不能访问子类的私有成员。父亲的朋友不一定是我的朋友。
继承和静态成员:
如果父类定义了静态成员,那么在整个继承体系中,就只有这样一个成员,不管派生出多少个子类,都只有一个。
菱形继承:
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
继承的总结和反思:
1.在实际中,一定不要设计出菱形继承。
2.类复用的方式有继承和组合两冲方式。
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用组合,再是继承。
3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
4.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
5.实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。