这里写目录标题
- 衔接继承总结
- 继承和组合
- 白箱复用
- 黑箱复用
- 多态的概念
- 多态的定义以及实现
- 虚函数重写的两个例外
- 协变
- 面试题
- 析构函数的重写
- final
- voerride
- 重载
- 隐藏(重定义)
- 重写(覆盖)
- 抽象类
- 什么是抽象类?
- 实现继承和接口继承
- 多态的原理
- 虚函数表
- 那多态的调用是怎么实现的呢?
- 动态绑定和静态绑定
- 切片的区别。
- 多态的两种形态?
- 单继承中的虚函数表
- 虚表存在哪个区域?
- 多继承中的虚函数表
衔接继承总结
继承的概念和定义,基类和派生类对象赋值转换,这点可以去学学。
一般子类继承级就用公有继承。
继承中的作用域就了解一下就OK。
虚拟继承半了解就行。
我们自己设计尽量不要用菱形继承。但是可以用多继承。
继承和组合
继承是有复用。组合是相互独立。
优先使用对象组合,
白箱复用
继承这种通过生成派生类的复用称为白箱复用。因为基类的内部细节对子类可见,这样这一定程度破坏了封装。基类的改变,对派生类影响很大。派生类和基类的依赖关系很强,耦合度高。
黑箱复用
黑箱复用对象的内部细节不可见。没有很强的依赖关系,耦合度低。
实际中多去使用组合,耦合度低,代码维护性好。
学软件工程一定要画UML图。
要学会画类图。
C只要不修改公有.就不会影响D。
对象组合是类继承之外的另一种复用的选择。
C对象公有成员D不能直接用。
C对象保护成员D不能直接用。
多态的概念
多态就是:同一个对象去调用同一个函数时会产生不同的效果。
多态的关键字是:virtual
在函数面前加上virtual就实现了多态。
构成条件:
1.是虚函数+virtual
2.函数名/参数/返回值相同,构成重写,重写也叫作覆盖。
注意:要和继承的隐藏区分开,隐藏是没有+virtual。
多态有两个要求;
1.子类虚函数重写父类的虚函数。(重写就是三同+virtual)
(三同就是参数,返回值,函数名相同)
2.必须用父类指针或者引用去调用虚函数。
为什么要用父类的?不能用子类的?因为只有父类的话,传参的话,符合对象赋值兼容的规则。
不能用普通对象调用。
假如用普通对象调用,调用的都是普通对象的,也就是普通人的票。
这点为什么不同呢?可以看下面的多态的实现原理。
多态的定义以及实现
怎么让不同的对象调用不同的函数,这里涉及到对象赋值转换,切割。
#include <iostream>
using namespace std;
//普通票
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票——全价" << endl;
}
};
//学生票 继承普通人
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票——半价" << endl;
}
};
//教师票 继承普通人
class Teacher : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票——免费" << endl;
}
};
void Pay(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person p;
Student s;
Teacher t;
Pay(&t);
return 0;
}
虚函数重写的两个例外
协变
这个是C++的缺陷。
首先满足协变,需要是对象
Student继承了Person,假如返回值不同,三同中的返回值不同。
子函数需要加virtual,这是好习惯。
class A
{
};
class B :public A
{
};
class Person
{
public:
virtual A* f()
{
cout << "virtual A* Person::f()" << endl;
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
cout << "virtual B* Student::f()" << endl;
return new B;
}
};
面试题
接口继承。
子类 继承 重写父类虚函数
把virtual继承了下来,缺省参数也继承了下来。
1.接口继承(所以B中func不写virtual也是虚函数,复合多态条件,缺省参数也是用的A::func的1)
2.重写的函数实现。
析构函数的重写
析构函数+virtual构成重写,析构函数的名字统一会被改成destructer
基类的析构函数一般要定义为虚函数,定义为虚函数可以完成析构函数的重写。这样可以不容易导致析构的时候发生错误。
final
final中文叫做最后,可以理解为:最后的类。
1.在函数后面添加final关键字,修饰虚函数,表示该函数不能被重写
2.在基类的后面加final,表示该类不能被继承。
voerride
override是写在子类中的,用于检查子类是否完成了重写,如果没有完成重写,就会报错。
比如:
1.忘记加了virtual
2.函数仓鼠不同。
重载
1.两个函数在同一作用域
2.函数名/参数不同
隐藏(重定义)
隐藏针对的是两个普通函数
1.两个函数分别在基类和派生类的作用域
2.函数名相同
3.两个基类和派生类的同名函数不构成重写就是重定义(隐藏)。
重写(覆盖)
1.两个函数分别在基类和派生类的作用域
2.函数名/参数/返回值必须相同(协变除外)
3.两个函数必须是虚函数。
抽象类
概念:C++中的包含有纯虚函数的类就叫做抽象类,抽象类的不能实例化处对象。
子类继承抽象类,必须重写虚函数,才能实例化对象。
什么是抽象类?
抽象的意思就是:在现实中一般没有对应的实体。
举个例子:
不能实例化出来对象。比如在学校中,人不是具体的实体,而老师和学生是具体的实体。
所以人就是抽象类。
抽象类间接要求子类必须重写,才能实例化对象。
实现继承和接口继承
实现继承:普通函数的继承是实现继承。继承的是函数的实现,也就是函数体。
接口继承:虚函数的继承是接口继承,继承的是虚函数的参数/函数名/返回值,继承的是接口,目的是为了重写,达成多态。
所以不重写,不实现多态,就不要写虚函数。
多态的原理
多态就是实现指针指向哪儿,调用哪的。
怎么实现的呢?
虚函数表
虚函数表里面存放的是虚函数的指针。
父类的对象模型是:
1.指向虚函数表的指针。
2.成员变量
子类的对象模型:
1.从父类继承下来的指针和变量
2.自己的变量
不同的是子类的虚函数表,在子类的虚函数表中,继承下来的重写的函数不一样。
子类中重写的函数覆盖了父类的函数。
这就说明重写:是语法层的概念,对函数实现进行了重写。
原理层的概念:就是子类的虚表,拷贝父类的虚表,然后覆盖重写的那个虚函数。
那多态的调用是怎么实现的呢?
多态调用的实现,依靠运行时决议,去指向对象的虚表中查看调用函数的地址。
普通函数的调用:编译时决议,编译时确定调用函数的地址。
、
指向谁我在谁的虚表里找到调用的。
父类赋值给子类对象,也可以切片,为什么实现不了多态?
为什么对象实现不了多态。只有指针和引用可以实现。
原理:编译器很死板,在编译的时候就确定了,编译器在检查时发现不构成多态,
怎么才能让对象可以支持多态?对象是不可以的。
动态绑定和静态绑定
1.在程序编译期间确定了程序的行为,
2.构成多态就叫做动态绑定,也叫晚绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数
切片的区别。
对象的切片是把子类中父类的值,拷贝过去,自定义类型是深拷贝。
但是不拷贝虚表指针,因为都指向了同一个虚函数表。导致混乱,不知道调用谁。
对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针,假如拷贝就混乱了,父类对象中到底是父类的虚表指针,还是子类的虚表指针?到底该调用谁就混乱了。
但是多态是指向谁调用谁,这不就混乱了,父类指向子类调用的还是子类。所以对象不可能实现多态。
但是引用和指针切片的话就是直接指向父类或引用父类的那一部分,不存在拷贝的苦恼。
多态的两种形态?
静态的多态:函数重载。
为什么有静态的多态,重载实际是在编译期间,根据函数名修饰规则找到不同的函数。
动态的多态:就是上面所讲的。是在运行时候去指向的虚函数表找,而实现的两种形态。
单继承中的虚函数表
虚函数理论而言要进虚表,但如果只有子类有的虚函数,监视窗口只有父类的虚表,难道子类只有的虚函数不进虚表吗?编译器规定,虚函数都要进虚表的,但是子类一般是拷贝父类的的虚表,然后重写需要重写的虚函数进行覆盖。因为监视窗口一般都是骗人的。但在内存中可以看到。
为了验证这个现象,我们可以写一段程序来打印虚表。
虚函数表是一个数组,数组里的每一个元素是函数指针。
这里说明vs的监视窗口看到的虚函数表不一定是真实的,可能会被处理过。
虚表存在哪个区域?
同一个类型的对象,共用一个虚表。
所以虚表存在的区域只有静态区(数据段)或常量区(代码段)。
那么到底在哪个区域呢。
常量区
常量区更加合理,因为静态区一般是全局变量.
我们也可以打印来验证,会发现虚标的地址和常量区的地址最为接近。
多继承中的虚函数表
多继承中虚函数表中虚函数的位置
派生类的未重写的虚函数会放在继承的第一个基类部分的虚函数表。