c++学习笔记-----继承与多态

目录

 

一、继承

(一)概念

(二)父类在子类中的访问方式

(三)基类和派生类对象赋值转换

(四)继承中的作用域(也是一个C++缺陷)

(五)派生类的默认成员函数

(六)继承与静态成员

(七)菱形继承

1.概念

2.菱形虚拟继承内存中体现

二、多态

(一)概念

(二)多态的条件及虚函数的重写(指向谁调用谁的虚函数)

(三)C++11 override 和 final

(四)重载、覆盖(重写)、隐藏(重定义)的对比

(五)抽象类

1.概念

2.接口继承和实现继承

(六)多态的原理

1.虚函数表

2.虚函数表指针(简称虚表指针)

3.什么是多态?


一、继承

  • 面向对象三大特性:(严格来说不止三种)最重要三种封装、继承、多态,        其他:抽象、反射(Java)。
  • 封装本质是一种更好的管理,相比c语言面向过程,数据和方法都放到类中进行管理,在通过访问限定符进行限制。
  • 相对C语言面向过程,数据和方法都放到类中进行管理,再通过访问限定符进行限制。

(一)概念

继承是为了从类设计的角度避免重复定义数据和方法,进行类角度的复用。

c++学习笔记-----继承与多态_第1张图片

(二)父类在子类中的访问方式

c++学习笔记-----继承与多态_第2张图片

c++学习笔记-----继承与多态_第3张图片

  1. 在子类的访问方式=权限min(基类的访问方式,继承方式)public>protected>private
  2. 基类的private成员,在子类中都不可见。本质意义是说,我想在子类中用父类变量就不能用,但是通过父类函数调用父类私有参数就可以用。常考考点!!!
  3. 语法层面不能对变量改,可以取地址,强转int*就可以改。
  4. 继承的protected变量可以在类里用,不能在类外用。
  5. 想让子类用就定义公有或者保护,不想让子类用就定义保护,保护和公有区别就是保护不能在类外用,公有可以在类外用。
  6. 想复用就定义公有,想在类内随便用就定义保护。
  7. 最常用的是两种,公有继承父类的公有和保护成员。保护继承和私有继承最不常用。
  8. class默认继承方式是私有,struct默认继承方式是公有。

(三)基类和派生类对象赋值转换

c++学习笔记-----继承与多态_第4张图片

int main()
{
  Person=p;
  Student=s;
  p=s;   //子类对象赋值给父类对象,切割切片。
  return 0;
}
  1. p基类   s子类   p=s   子对象赋值给父类对象 ,存在切割、切片。
  2. 引用和指针都是一样   切割切片。
  3. 父类的指针或者引用是可以转回子类类型指针或者引用,但是子类无法转回父类,不安全存在风险。

  4. 指针的意义就是看能否看到几个字节空间,比如int*只能看到4个字节空间,char*只能看到一个字节空间。基类指针指向父类相当于看到的空间多了,但是能正常访问,子类指针转回父类,相当于多看到空间了,存在越界。

  5. 所有讲的赋值继承都只存在与公有继承,比如父类原本是公有,通过保护继承子类变成保护继承,如果在赋值后,父类成员是公有的,子类成员是私有的,就矛盾了。私有和保护继承是不存在的。所以实际中很少使用私有和保护继承。

(四)继承中的作用域(也是一个C++缺陷)

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  3. 注意在实际中在继承体系里面最好不要定义同名的成员。
  4. 重载(函数重载,函数名必须在同一作用域)     重写(覆盖)     重定义(隐藏,函数名相同是在不同作用域将会构成隐藏)。
  5. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。

(五)派生类的默认成员函数

  1. 默认写构造用全缺省
  2. const成员必须在初始化列表初始化。
  3. 父类部分的成员要使用父类的构造函数去初始化,不能自己显示的去初始化。
  4. 析构函数,基类调用复用父类的析构函数,加上父类名,就可以使用。但是最后析构调用了两次。

  5. 友元关系不能继承,要访问,定义成子类的友元。

  6. 成员变量的构造跟声明的顺序有关系。相当于入栈析构相当于出栈。语法上有两个迷惑点:

  • 一是子类的析构函数和父类的析构函数构成隐藏->由于多态重写的需要。所有类的析构函数名字会被编译器统一处理成destructor。

  • 二是如果自己显示调用容易存在父类先析构,不符合规则。规则应该是子类后入栈,最先析构,最后父类析构。

(六)继承与静态成员

多个继承计算派生出多个子类,定义一个全局变量放到构造函数里让++,但是一般体现封装性,应该定义为静态变量,这样保证子类中看到的是同一个成员,让其++可计算出派生了多少个子类。前提是公有继承,如果是保护继承,定义一个成员函数访问。

(七)菱形继承

1.概念

  • 菱形继承是多继承的一种特殊情况。

c++学习笔记-----继承与多态_第5张图片

 

c++学习笔记-----继承与多态_第6张图片

  • 只有一个直接父类的叫单继承,有两个或以上直接父类的继承关系为多继承。

  • 菱形继承有数据冗余和数据二义性。

  • 虚继承和虚函数用的同一个关键字,但是二者没有关系。

  • 虚继承是为了解决数据冗余和二义性的。
  • 在父亲和爷爷之间的父亲这一层加上关键字virtual。

2.菱形虚拟继承内存中体现

c++学习笔记-----继承与多态_第7张图片

c++学习笔记-----继承与多态_第8张图片

  1. 对象模型:对象在内存中是如何存储的模型。----------《深度探索c++对象模型》不推荐     《effctive C++》 推荐 《STL源码剖析》推荐
  2. 菱形虚拟继承:只有一份公共虚基类成员,解决了数据冗余和二义性。多了两份内存是两个虚基表指针,指向两个虚基表。
  3. 虚基表:存储的是当前位置距离虚基表类对象的偏移量。当要发生切片时,可以通过虚基表内的偏移量在后面的位置找到爷爷,把虚基类放到了对象的最后位置,爷爷既属于父亲也属于孙子。
  4. 公有继承是一个is a和组合是一个has a   
  5. 都是类设计角度的复用,公有继承是一种白箱复用(公开透明),组合是一种黑箱复用(隐藏的,不透明),要修改可以用友元,友元能少用就少用,破坏封装。
  6. 白盒要求更高,黑盒测试高级。
  • 什么时候用继承?什么时候用组合?他们谁更好?

软件工程里面认为组合好,能用组合就用组合,组合更符合高内聚(一个功能写一个类不要多个功能写一个类),低耦合。实际中更符合is a(狗是一个动物)关系用继承,更符合has a(车有一个轮胎)关系用组合,都符合,用组合。

二、多态

(一)概念

  • 完成某个行为不同的人干同一件事,不同的人(对象)会产生不同的状态。

c++学习笔记-----继承与多态_第9张图片

  • 举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

(二)多态的条件及虚函数的重写(指向谁调用谁的虚函数)

  • 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;
	}
};

c++学习笔记-----继承与多态_第10张图片

 

  • 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;}
};

(四)重载、覆盖(重写)、隐藏(重定义)的对比

c++学习笔记-----继承与多态_第11张图片

(五)抽象类

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<
  • 运行时动态的多态->虚函数重写以后,父类指针或引用调用重写虚函数,指向谁调用的就是谁的虚函数,
  • 使代码用起来更灵活更好用。

 

你可能感兴趣的:(c++学习笔记,c++,多态)