目录
一、继承
- 面向对象三大特性:(严格来说不止三种)最重要三种封装、继承、多态, 其他:抽象、反射(Java)。
- 封装本质是一种更好的管理,相比c语言面向过程,数据和方法都放到类中进行管理,在通过访问限定符进行限制。
- 相对C语言面向过程,数据和方法都放到类中进行管理,再通过访问限定符进行限制。
(一)概念
继承是为了从类设计的角度避免重复定义数据和方法,进行类角度的复用。
(二)父类在子类中的访问方式
- 在子类的访问方式=权限min(基类的访问方式,继承方式)public>protected>private
- 基类的private成员,在子类中都不可见。本质意义是说,我想在子类中用父类变量就不能用,但是通过父类函数调用父类私有参数就可以用。常考考点!!!
- 语法层面不能对变量改,可以取地址,强转int*就可以改。
- 继承的protected变量可以在类里用,不能在类外用。
- 想让子类用就定义公有或者保护,不想让子类用就定义保护,保护和公有区别就是保护不能在类外用,公有可以在类外用。
- 想复用就定义公有,想在类内随便用就定义保护。
- 最常用的是两种,公有继承父类的公有和保护成员。保护继承和私有继承最不常用。
- class默认继承方式是私有,struct默认继承方式是公有。
(三)基类和派生类对象赋值转换
int main()
{
Person=p;
Student=s;
p=s; //子类对象赋值给父类对象,切割切片。
return 0;
}
- p基类 s子类 p=s 子对象赋值给父类对象 ,存在切割、切片。
- 引用和指针都是一样 切割切片。
父类的指针或者引用是可以转回子类类型指针或者引用,但是子类无法转回父类,不安全存在风险。
指针的意义就是看能否看到几个字节空间,比如int*只能看到4个字节空间,char*只能看到一个字节空间。基类指针指向父类相当于看到的空间多了,但是能正常访问,子类指针转回父类,相当于多看到空间了,存在越界。
所有讲的赋值继承都只存在与公有继承,比如父类原本是公有,通过保护继承子类变成保护继承,如果在赋值后,父类成员是公有的,子类成员是私有的,就矛盾了。私有和保护继承是不存在的。所以实际中很少使用私有和保护继承。
(四)继承中的作用域(也是一个C++缺陷)
- 在继承体系中基类和派生类都有独立的作用域。
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
- 重载(函数重载,函数名必须在同一作用域) 重写(覆盖) 重定义(隐藏,函数名相同是在不同作用域将会构成隐藏)。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
(五)派生类的默认成员函数
- 默认写构造用全缺省
- const成员必须在初始化列表初始化。
- 父类部分的成员要使用父类的构造函数去初始化,不能自己显示的去初始化。
析构函数,基类调用复用父类的析构函数,加上父类名,就可以使用。但是最后析构调用了两次。
友元关系不能继承,要访问,定义成子类的友元。
成员变量的构造跟声明的顺序有关系。相当于入栈析构相当于出栈。语法上有两个迷惑点:
一是子类的析构函数和父类的析构函数构成隐藏->由于多态重写的需要。所有类的析构函数名字会被编译器统一处理成destructor。
二是如果自己显示调用容易存在父类先析构,不符合规则。规则应该是子类后入栈,最先析构,最后父类析构。
(六)继承与静态成员
多个继承计算派生出多个子类,定义一个全局变量放到构造函数里让++,但是一般体现封装性,应该定义为静态变量,这样保证子类中看到的是同一个成员,让其++可计算出派生了多少个子类。前提是公有继承,如果是保护继承,定义一个成员函数访问。
(七)菱形继承
1.概念
- 菱形继承是多继承的一种特殊情况。
只有一个直接父类的叫单继承,有两个或以上直接父类的继承关系为多继承。
菱形继承有数据冗余和数据二义性。
虚继承和虚函数用的同一个关键字,但是二者没有关系。
- 虚继承是为了解决数据冗余和二义性的。
- 在父亲和爷爷之间的父亲这一层加上关键字virtual。
2.菱形虚拟继承内存中体现
- 对象模型:对象在内存中是如何存储的模型。----------《深度探索c++对象模型》不推荐 《effctive C++》 推荐 《STL源码剖析》推荐
- 菱形虚拟继承:只有一份公共虚基类成员,解决了数据冗余和二义性。多了两份内存是两个虚基表指针,指向两个虚基表。
- 虚基表:存储的是当前位置距离虚基表类对象的偏移量。当要发生切片时,可以通过虚基表内的偏移量在后面的位置找到爷爷,把虚基类放到了对象的最后位置,爷爷既属于父亲也属于孙子。
- 公有继承是一个is a和组合是一个has a
- 都是类设计角度的复用,公有继承是一种白箱复用(公开透明),组合是一种黑箱复用(隐藏的,不透明),要修改可以用友元,友元能少用就少用,破坏封装。
- 白盒要求更高,黑盒测试高级。
- 什么时候用继承?什么时候用组合?他们谁更好?
软件工程里面认为组合好,能用组合就用组合,组合更符合高内聚(一个功能写一个类不要多个功能写一个类),低耦合。实际中更符合is a(狗是一个动物)关系用继承,更符合has a(车有一个轮胎)关系用组合,都符合,用组合。
二、多态
(一)概念
- 完成某个行为不同的人干同一件事,不同的人(对象)会产生不同的状态。
- 举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
(二)多态的条件及虚函数的重写(指向谁调用谁的虚函数)
- 1.继承类中,虚函数的重写。派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
- 2.A->B->C想在A B间实现多态,用A的指针,想在B C间实现多态,用B的指针。
- 3.基类指针或者引用去调用这个虚函数。
- 4.重写的条件:
- 父子类中的函数都是虚函数。
- 函数名、参数、返回值都相同。
class Person
{
public:
virtual void BuyTicket()
{
cout << "正常排队-全价格买票" << endl;
}
};
class Student : public Person
{
public:
// 重写
virtual void BuyTicket()
{
cout << "正常排队-半价格买票" << endl;
}
};
- 5.重写的两个例外:
协变要求返回值之间必须是有父子关系的指针。就叫做重写的协变。
子类中重写虚函数可以不加virtual,但是基类析构函数必须加virtual。
- 6.各种开后门特例,本质都是为了析构函数构成重写,因为如果不构成重写,特殊情况下,可能会存在内存泄漏。面试题:析构函数最好定义成虚函数,这样就可以构成重写,
- 7.不满足多态跟类型有关,也就是说,P是什么类型,就调的是这个类型的函数。满足多态,跟对象有关,也就是指向哪个对象,调用的就是哪个的函数。
- 8.不满足重写就是隐藏。
(三)C++11 override 和 final
- final修饰一个虚函数,表示该函数无法被继承,final修饰一个类,叫最终类,无法被人继承。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
(四)重载、覆盖(重写)、隐藏(重定义)的对比
(五)抽象类
1.概念
- 纯虚函数,只需要声明,不需要实现,包含纯虚函数的叫做抽象类,不能实例化对象。植物可以定义为抽象类。
- 隐藏技能:强制子类去重写
class Car
{
public:
virtual void Drive() = 0;
};
2.接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
- 接口实际上是可以给你用的函数,普通函数的继承是一种实现继承。
(六)多态的原理
1.虚函数表
// sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
- 通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
- 虚函数存在哪里的?从运行进程来说,无论是虚函数还是普通函数都是存在代码段的。未运行的函数是存在硬盘的。
2.虚函数表指针(简称虚表指针)
- 虚函数表指针本质是一个指针数组(虚函数表指针)定义两个同类不同的对象时,,两个对象的虚表是同一个,虚表是存在(代码段)常量区,虚表是在编译时生成的。
- 继承的目的是复用,两个类是独立的。不是共用。
- 编译时是静态绑定,运行时是动态绑定。
3.什么是多态?
- 静态多态->函数重载 int i=1; double d=2.22; cout<<i;cout<<d;调用的cout不是同一个函数。
- 运行时动态的多态->虚函数重写以后,父类指针或引用调用重写虚函数,指向谁调用的就是谁的虚函数,
- 使代码用起来更灵活更好用。