继承
继承的定义: 继承是面向对象程序设计使代码可以复用的重要手段,允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生的类,称之为派生类.
继承体现了面向对象程序设计的层次结构,提高了代码的复用性,为多态提供了前提.
class Preson {
public:
void print() {
cout << _age << "\t" << _id << endl;
}
protected:
int _age;
int _id;
};
class Student : public Preson {
public:
void show() const{
cout << _id << "\t" << _age;
}
private:
int _num;
};
基类和派生类对象的赋值转换:
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用.形象的说法叫做切片或者切割,就是把派生类的那部分切来赋值过去
- 基类对象不能赋值给派生类对象
- 基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须是基类的指针是指向派生类对象时才是安全的. 基类如果是多态类型,可以使用dynamic_cast来进行识别后进行安全转换.
派生类函数的调用规则:
-
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
-
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
-
派生类的operator=必须要调用基类的operator=完成基类的复制。
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类 对象先清理派生类成员再清理基类成员的顺序。
-
派生类对象初始化先调用基类构造再调派生类构造。
-
派生类对象析构清理先调用派生类析构再调基类析构
扩展:
默认构造函数(只能存在一个):
- 编译器默认生成的(只有当类没有定义任何构造函数时,编译器才会自动生成一个无参构造)
- 显式定义的无参构造
- 全缺省构造函数
注意:在派生类的析构函数中不需要自己去调用基类的析构函数,编译器会在派生类析构函数结束后自动调用。
菱形继承的问题:数据冗余和二义性问题
class preson {
private:
int _age;
};
class teacher : public preson {
private:
int _teaNum;
};
class student : public preson {
private:
int _stuNum;
};
class A :public teacher, public student {
};
int main() {
A a;
return 0;
}
由上述代码和截图可以得知,这里的teacher和student都从preson中继承了相同的成员_age。但是A再从teacher和student继承时,就分别把这两个_age都给继承了过来,导致这里有了两个一样的成员.
倘若我们要给_age赋值:
因为a对象里面存在两个一样的_age,这时编译器就会报错通知我们指定的不够明确.因此需要指定作用域.
虚继承解决数据冗余以及二义性问题:
class preson {
public:
int _age;
};
class teacher : virtual public preson {
public:
int _teaNum;
};
class student : virtual public preson {
public:
int _stuNum;
};
class A :public teacher, public student {
};
由此可知,只继承了一次_age;
按道理来说,A类的大小应该是12字节,为什么这里显示20字节呢?
这里多出来的8个字节,其实是两个虚基表指针。
因为这里preson中的_age是teacher和student共有的,所以为了能够方便处理,在内存中分布的时候,就会把这个共有成员_age放到对象组成的最末尾的位置。然后在建立一个虚基表,这个表记录了各个虚继承的类在找到这个共有的元素时,在内存中偏移量的大小,而虚基表指针则指向了各自的偏移量。
小结:公有成员放在对象的最末尾,原本应该存放公有成员数值的地方现在存放的是虚基表指针.虚基表指针偏移4个字节的地方存放在公共部分的偏移量.
组合和继承的关系:
组合是比继承更好的代码复用的方式,组合就是将多个类组合在一起,实现代码复用.
1.继承
继承是一种is-a的关系,就是基类是一个大类,而派生类则是这个大类中细分出来的一个子类,但是它们本质上其实是一种东西.
class proson{
public:
int _age;
};
class student : public preson{
public:
int _stuNum;
};
学生也是人,所以它很好的继承人的所有属性,并在其中增加学生独有的属性.
2.组合
组合是一种has-a的关系,就是一种包含关系,比如对象a是对象b的成员,那么它们的关系就是对象b的组成中包含了对象a,对象a是对象b找那个的一部分,对象b包含对象a.
class study{
public:
void ToStudy(){
cout << "study" << endl;
}
};
class student : public preson{
public:
study _s;
int _stuNum;
};
这里的student类中包含了一个study类,学习是学生中非常重要的一部分,并且是不可缺少的一部分,每个学生都需要学习,学习是学生的本职。
如何选择组合和继承
如果综合考虑的话,其实应该多使用组合,因为在组合中,几个类的关联不大,你是我的一部分,所以我也只需要用到你那部分的某个功能,我并不需要了解你的实现和细节,只需要你开放对应的接口即可,并且如果我要修改,只修改那一部分功能即可。所以这就导致了组合的依赖关系弱,耦合度低,十分符合软件工程的低耦合,高类聚。这样保证了代码具有良好的封装性和可维护性。
继承的依赖关系就非常的强,耦合度非常高。因为你要想在子类中修改和增加某些功能,就必须要了解父类的某些细节,并且有时候甚至会修改到父类,父类的内部细节在子类中也一览无余,严重的破坏了封装性。并且一旦基类发生变化时,牵一发而动全身,所有的派生类都会有影响,这样的代码维护性会非常的差,因为很难在不了解具体细节的情况下能够不影响到子类的实现。但是继承也不是无用的,很多关系都很适合用继承,并且多态的实现也需要用到继承,这个还是得参考具体场景。
但是大部分场景下,如果继承和组合都可以选择,那就优先选择组合。