【C++】继承与多态

目录

  • 前言
  • 1. 继承
    • 1.1 继承的概念
    • 1.2 继承的定义
    • 1.3 切片赋值
    • 1.4 继承中的作用域
    • 1.5 派生类的默认成员函数
    • 1.6 继承与友元、静态成员
    • 1.7 多继承、菱形继承、菱形虚拟继承
      • 1.7.1 区分单继承与多继承
      • 1.7.2 菱形继承
      • 1.7.3 菱形虚拟继承
      • 1.7.4 菱形虚拟继承的原理
  • 2. 多态
    • 2.1 概念
    • 2.2 多态的定义和实现
    • 2.3 C++11中的两个关键字
    • 2.4 区分重载、重写、重定义
    • 2.5 抽象类
    • 2.6 多态的原理
      • 2.6.1 虚函数表
      • 2.6.2 多态如何实现
    • 2.7 静态绑定和动态绑定


前言

面向对象程序设计的三大特征是封装、继承、多态。封装在类和对象模块已经基本掌握了,而本文将着重介绍继承和多态。

1. 继承

1.1 继承的概念

继承(inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在执行期扩展。

选自《维基百科》

⭕继承是面向对象编程中实现代码复用的重要手段,它可以在保存原有类(基类、也称父类)特性的基础上,增加新特性,产生新的类,称为派生类(也称子类)。之前我们实现代码复用的手段都是在函数层面的,而继承是类层面的代码复用。

举个栗子

class A
{	
public:
	void func1()
	{}
protected:
	int _a;
}

class B: public A
{
public:
	void func2()
	{}
private:
	int _b;
}

如这段代码,我们称B类继承了A类,B是A的派生类(子类),A是B的基类(父类)下文均用基类、派生类的叫法

【C++】继承与多态_第1张图片

如图是B类的继承模型。可以看到,B类不仅有自己的方法fun2和数据_b,同时也继承了A类的fun1和_a。


1.2 继承的定义

【C++】继承与多态_第2张图片

继承方式和类内的访问限定符用的关键字相同,都是publicprotectedprivate,但意义不同。不同继承方式和基类不同访问限定符结合,派生类会继承不同的访问限定的成员。

如下表

基类成员\继承方式 public protected private
基类中的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类中的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类中的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

记忆方法:

基类中的private成员,无论派生类以哪种方式继承都不可见。其他的,只需记住各个关键字的权限大小关系:public > protected > private,假设派生类以X(继承方式)方式继承基类,那么基类中的Y(访问限定符)成员就是派生类中的min(X,Y)成员。

总结与一些注意事项:

  • 区分类中privateprotected成员的区别,private成员在类外无论任何场景都无法访问,protected在类外也无法访问,但是可在派生类中访问。保护限定符protected为继承而生。

  • 没有显式写出访问限定符和继承方式的情况下:
    class默认访问限定符和默认继承方式private
    struct默认访问限定符和默认继承方式public
    最好显式写出访问限定符和继承方式

  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强


1.3 切片赋值

不同类型的普通对象之间的赋值通常会发生类型转换,中间还会生成一个具有常性的临时变量,如下:

	int a = 3.14;
	double f = a;
	const int& ref = f;

派生类与基类之间的赋值,并不会发生类型转化,即便它们是两个不同的类,而是一种特殊的机制——切片赋值。派生类对象赋值给基类对象/基类引用,派生类对象的地址可以赋值给基类指针,切片赋值就是将派生类中的基类部分切割下来赋值给基类。

注意:基类不能赋值给派生类,只能向上转换。

实例

class Person
{
public:
	string name = "张三";
	int age = 18;
};

class Student :public Person
{
public:
	string stuID;
	int score = 100; 
};

void test()
{
	Student s;
	Person p = s; // 1
	Person& rp = s; // 2
	Person* pp = &s; // 3
}

1.对象s中的基类部分赋值给对象p

【C++】继承与多态_第3张图片

  1. rp是对象s中基类部分的别名

【C++】继承与多态_第4张图片

3.pp指向对象s中的父类部分

【C++】继承与多态_第5张图片

测试,改变rp和pp的指向内容,s对象的成员发生变化,证明以上特性

【C++】继承与多态_第6张图片

  • 基类指针可以通过强制类型转化赋值给派生类指针,但是要注意可能存在越界问题。
void test()
{
	Student s;
	Person p = s;
	
	Person* pp = &s;
	Student* ps = (Student*)pp;//可以,没问题
	ps = (Student*)&p;//可以,但可能会发生越界访问
}

1.4 继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}

	int _n = 10;
};

class B:public A
{
public:
	int func(int val = 0)
	{
		cout << "B::func()" << endl;
		return val;
	}

	int _n = 20;
};

void test2()
{
	B b;
	// b对象中派生类部分和基类部分存在同名成员,默认调用派生类成员
	cout << b._n << endl;
	b.func();

	// 若想调用基类部分成员,应指定作用域
	cout << b.A::_n << endl;
	b.A::func();
}

⭕测试结果

【C++】继承与多态_第7张图片

1.5 派生类的默认成员函数

默认成员函数有:构造函数,析构函数,拷贝构造,赋值重载和取地址重载。普通类具有这些成员函数,派生类也不例外。但是派生类的默认成员函数的“职责”会比普通类的多一项,那就是还要对基类部分进行处理。

  1. 派生类的构造函数必须先调用基类的构造函数来初始化基类部分的成员。派生类会在构造函数的初始化列表调用基类的构造函数,如果不显式写则是调用基类的默认构造。
class Person
{
public:
	Person(string name = "张三",int age = 18)
		:_name(name)
		,_age(age)
	{}
public:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	// 构造函数
	Student(string name, int age, int score)
		:Person(name, age) // 此处调用了基类Person的构造函数
		, _score(score)
	{}
public:
	int _score;
};

⭕构造完毕,符合预期
【C++】继承与多态_第8张图片

  1. 派生类的拷贝构造函数必须调用基类的拷贝构造,完成基类部分的拷贝初始化。
class Person
{
public:
	Person(string name = "张三",int age = 18)
		:_name(name)
		,_age(age)
	{}
	Person(const Person& pn)
		:_name(pn._name)
		,_age(pn._age)
	{}
public:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	// 拷贝构造
	Student(const Student& st)
		:Person(st) // 调用基类的拷贝构造,参数发生切片赋值
		, _score(st._score)
	{}

public:
	int _score;
};

⭕ 拷贝构造成功
【C++】继承与多态_第9张图片

❓小问题:当派生类的拷贝构造函数的初始化列表不显示地调用基类拷贝构造,会发生什么?

class Person
{
public:
	Person(string name = "张三",int age = 18)
		:_name(name)
		,_age(age)
	{}
	Person(const Person& pn)
		:_name(pn._name)
		,_age(pn._age)
	{}
public:
	string _name;
	int _age;
};
class Student :public Person
{
public:
	Student(string name, int age, int score)
		:Person(name, age)
		, _score(score)
	{}
	Student(const Student& st)
		:/*Person(st) // 不显示地调用基类拷贝构造函数
		, */_score(st._score)
	{}
public:
	int _score;
};

⭕调试发现s1的基类Person部分调用了基类的默认构造函数去初始化

【C++】继承与多态_第10张图片
得出结论,当派生类的拷贝构造函数的初始化列表不显示地调用基类拷贝构造,会调用基类的默认构造函数来初始化基类部分,而非调用基类的默认拷贝构造函数。但这并不符合“拷贝构造”的需求,因此我们自己写派生类的拷贝构造函数时,必须在初始化列表显式地调用基类拷贝构造函数。

  1. 派生类的operator=函数(赋值重载)同样需要调用基类的operator=来处理基类部分的成员。
class Person
{
public:
	Person& operator=(const Person& pn)
	{
		if (this != &pn)
		{
			_name = pn._name;
			_age = pn._age;
		}
		return *this;
	}
public:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	Student& operator=(const Student& st)
	{
		if (this != &st)
		{
			// 先调用基类的operator=
			// 三种调用方法
			//Person::operator=(st);

			//*((Person*)this) = st;

			Person& tmp = *this;
			tmp = st;
			
			// 再处理派生类成员
			_score = st._score;
		}
		return *this;
	}
public:
	int _score;
};
  1. 派生类析构函数会在调用结束后自动调用基类的析构函数以清理基类部分的成员。保证了派生类对象先清理派生类成员再清理基类成员的顺序。

验证:

class Person
{
public:
	Person(string name = "张三",int age = 18)
		:_name(name)
		,_age(age)
	{}
	~Person() // 基类的析构函数
	{
		cout << "~Person()" << endl;
	}
public:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	Student(string name, int age, int score)
		:Person(name, age)
		, _score(score)
	{}
	~Student() // 派生类的析构函数
	{
		cout << "~Student()" << endl;
	}
public:
	int _score;
};

void test()
{
	Student s("李四", 22, 90);
}

⭕ 验证了先调用派生类的析构函数,再自动调用基类的析构函数
在这里插入图片描述


最终的继承模型中默认成员函数代码一览

class Person
{
public:
	Person(string name = "张三",int age = 18)
		:_name(name)
		,_age(age)
	{}
	Person(const Person& pn)
		:_name(pn._name)
		,_age(pn._age)
	{}
	~Person()
	{
		cout << "~Person()" << endl;
	}
	Person& operator=(const Person& pn)
	{
		if (this != &pn)
		{
			_name = pn._name;
			_age = pn._age;
		}
		return *this;
	}
public:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	Student()
	{}

	Student(string name, int age, int score)
		:Person(name, age)
		, _score(score)
	{}

	Student(const Student& st)
		:Person(st)
		, _score(st._score)
	{}

	~Student()
	{
		cout << "~Student()" << endl;
	}

	Student& operator=(const Student& st)
	{
		if (this != &st)
		{
			Person::operator=(st);
			_score = st._score;
		}
		return *this;
	}

public:
	int _score;
};

1.6 继承与友元、静态成员

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。可以理解为“父亲的朋友不一定是儿子的朋友”。

class A
{
	friend void func();
protected:
	int _a;
	string _str;

	void funcA()
	{}
};

class B :public A
{
protected:
	void funcB()
	{}
private:
	int _b;
};

void func()
{
	A a;
	a._a = 0;
	a._str = "hello world";
	a.funcA();

	B b;
	b._b = 1; // err,无法访问
	b.funcB(); // err,无法访问
}

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class A
{
public:
	A()
	{
		_a++;
	}
public:
	static int _a;
};

int A::_a = 0;

class B :public A
{
public:
	B()
	{
		_a++;
	}
};

class C :public A
{
public:
	C()
	{
		_a++;
	}
};

void test_static()
{
	A a;
	B b;
	C c;
	//注意b、c的实例化会调用A的构造函数
	cout << A::_a << endl;
}

⭕ 测试结果,说明A、B、C类的不同对象,共用一个static成员_a;

在这里插入图片描述

1.7 多继承、菱形继承、菱形虚拟继承

1.7.1 区分单继承与多继承

  1. 单继承:一个派生类只有一个直接基类

【C++】继承与多态_第11张图片

  1. 多继承:一个派生类有两个或两个以上直接基类

【C++】继承与多态_第12张图片

1.7.2 菱形继承

菱形继承是由多继承和单继承混用引发的问题,是多继承的一种特殊模式,其模型如下:

【C++】继承与多态_第13张图片

代码实现

class Person
{
public:
	int _age;
};

class Teacher :public Person
{
public:
	int _salary;
};

class Student :public Person
{
public:
	int _score;
};

class Assistant :public Teacher, public Student
{
public:
	int _majorNum;
};

此时class Assistant的类模型:

【C++】继承与多态_第14张图片

可以发现,菱形继承引发的问题:

  1. 数据冗余,一个Assistant类有两份Person类的数据,浪费空间。
  2. 二义性,当你访问Assistant::_age时,编译器无法判断是哪个_name。
void test2()
{
	Assistant x;
	// 存在二义性
	//cout << x._age << endl;//err

	// 指定类域可解决二义性问题
	cout << x.Student::_age << endl;
	cout << x.Teacher::_age << endl;

	// 但是数据冗余问题不好解决
	cout << sizeof(x) << endl;
}

通过调试发现,对象x确实有两份Person类成员

【C++】继承与多态_第15张图片

因此,C++提供了菱形虚拟继承的方法,用以解决数据冗余和二义性问题。

1.7.3 菱形虚拟继承

1️⃣ 菱形虚拟继承的使用

在菱形继承模型的腰部作修改。两个类的继承方法前加上关键字virtual,称为虚拟继承。

【C++】继承与多态_第16张图片

代码实现(此处给各个类的成员变量一个缺省值,方便后续观察)

class Person
{
public:
	int _age = 20;
};

class Teacher :virtual public Person
{
public:
	int _salary = 1;
};

class Student :virtual public Person
{
public:
	int _score = 2;
};

class Assistant :public Teacher, public Student
{
public:
	int _majorNum = 10;
};

⭕ 解决了刚才的问题

在这里插入图片描述

1.7.4 菱形虚拟继承的原理

通过调试,观察菱形虚拟继承的派生类模型。

【C++】继承与多态_第17张图片

很奇怪,这里的派生类在普通基础的基础上,甚至多了一个Person类,说好的解决数据冗余问题呢?怎么不减反增了?其实这里的vs监视窗口是不准确的,我们可以通过内存窗口来观察。

下面对比菱形继承和菱形虚拟继承的派生类的内存布局,以探究菱形虚拟继承的原理:

  1. 菱形继承
    【C++】继承与多态_第18张图片

  2. 菱形虚拟继承
    【C++】继承与多态_第19张图片

原理:菱形虚拟继承实际是将基类中冗余的部分独立出来,成为各个直接基类的共享数据,然后每个基类中多出一个指针,称为虚基表指针。虚基表指针指向一张虚基表,表中存储着一个偏移量,能够让直接基类找到共享部分的数据。

【C++】继承与多态_第20张图片

上图验证了Tercher和Student部分的Person数据是共享的

如图,Teacher类和Student类的Person类被抽离出来,放到Assistant内存的最下面,此时的Person是Teacher和Student共享的。Teacher和Student都有一个虚基表指针,指向的虚基表中存有一个偏移量,可以让其找到最下面的Person。

【C++】继承与多态_第21张图片

Q: 为什么要搞一个指针供Tercher部分、Student部分找到属于自己的Person呢?
A: 在切片赋值时,需要通过虚基表中的偏移量,找到基类的数据,完成赋值。如以下情形

void test2()
{
	Assistant a;

	Teacher t = a;
	Student s = a;
}

⭕除了菱形虚拟继承的场景,不要在其它地方运用虚拟继承,很容易出错。



2. 多态

2.1 概念

多态,即多种形态,通俗理解就是不同对象完成同一个行为时,会产生多种不同的结果。

在现实生活中,存在着许多形式的“多态”,例如:车站买票,普通人买全价票,学生买半价学生票,军人可以优先购票;医院排队挂号,普通人要排队,有特殊情况的可走绿色通道,军人优先…

在C++中,多态的具体表现形式主要有两种:函数重载类继承中的多态调用

  • 先试着以函数重载理解多态的概念
class Person
{};

class Student :public Person
{};

void Buyticket(Person p)
{
	cout << "全价票" << endl;
}

void Buyticket(Student s)
{
	cout << "学生票" << endl;
}

void test2()
{
	Buyticket(Person());
	Buyticket(Student());
}

代码中函数Buyticket构成重载,传入不同的类对象(这里是Person类和Student)会有不同的行为,产生不同的结果。

在这里插入图片描述

而下文将着重介绍类继承中的多态。


2.2 多态的定义和实现

类继承中的多态,是指在一个继承模型中,调用不同类对象的同一个成员函数,会产生不同的结果。比如Student和Soldier继承了Person,并且它们都有成员函数Buyticket,那么当它们分别调用Buyticket时会用不同的结果。

实现多态的条件:

  1. 必须通过基类的引用或指针调用虚函数。
  2. 派生类的虚函数必须对基类的虚函数完成重写(覆盖)

示例如下:

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "全价票" << endl;
	}
};

class Student :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "学生票" << endl;
	}
};

class Soldier :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "军人优先购票" << endl;
	}
};

void test1()
{
	Person* p1 = new Person();
	Person* p2 = new Student();
	Person* p3 = new Soldier();
	
	p1->Buyticket();
	p2->Buyticket();
	p3->Buyticket();
}

【C++】继承与多态_第22张图片

下面介绍几个概念

  • 虚函数:被virtual关键字修饰的函数称为虚函数。这种函数可以被派生类继承并重写。

【C++】继承与多态_第23张图片

  • 重写:又称覆盖,即派生类对基类的一些函数进行重新定义,使之成为派生类特有的方法。

派生类对基类虚函数的重写需满足三个相同:
函数名相同
参数列表相同
返回值类型相同

【C++】继承与多态_第24张图片

而如下派生类函数不加 virtual 也是可行的,因为派生类继承了基类的函数接口,其virtual特性也被继承了下来。但这种写法并不规范,一般不建议使用。

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "全价票" << endl;
	}
};

class Student :public Person
{
public:
	void Buyticket()
	{
		cout << "学生票" << endl;
	}
};

虚函数重写存在两个特例:

  1. 协变,即基类虚函数与派生类虚函数的返回值类型不同,也能完成虚函数重写。但也不能是任意的两种不同类型,只有当基类虚函数返回基类对象(可以是其他类)指针/引用,派生类虚函数返回派生类对象(可以是其他类)指针/引用时,才构成协变。
class A
{
public:
	virtual A* func()
	{
		cout << "I am A" << endl;
		return this;
	}
};

class B :public A
{
public:
	virtual B* func()
	{
		cout << "I am B" << endl;
		return this;
	}
};

void test3()
{
	A a;
	B b;

	A& ref1 = a;
	ref1.func();
	
	A& ref2 = b;
	ref2.func();
}

在这里插入图片描述

  1. 析构函数的重写,基类和派生类的析构函数的函数名不同,但能够完成重写。

⭕下面这段代码会出现内存泄漏的问题(析构函数没有重写)

A* p = new B();
delete p;

解释
new B()在堆上开辟了一个B类对象的空间,而p指针指向的是B类对象中的基类A部分。当执行delete p 时,调用的也是A类的析构函数,因此B类的派生类成员没有被释放,造成内存泄漏。

【C++】继承与多态_第25张图片

重写析构函数可以解决这个问题,使得在释放空间时,指针指向基类就释放基类,指向派生类就释放整个派生类。

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

class A
{
public:
	virtual ~A()
	{
		cout << "~A" << endl;
	}
};

class B :public A
{
public:
	~B()
	{
		cout << "~B" << endl;
	}
};

void test4()
{
	A* pa = new A();
	delete pa;
	
	cout << endl;
	
	A* pb = new B();
	delete pb;
}

【C++】继承与多态_第26张图片


2.3 C++11中的两个关键字

  1. final
  • 修饰类:该类不能再被继承,可理解为“最终类”
class C final
{};

class D :public C // err, 不能将final类型用作基类
{};
  • 修饰虚函数:该虚函数不能被派生类重写
class C
{
public:
	virtual void func() final
	{
		cout << "class C" << endl;
	}
};

class D :public C
{
public:
	virtual void func() // err, 无法重写final函数
	{
		cout << "class B" << endl;
	}
};
  1. override
    检查派生类虚函数是否重写了某个基类的虚函数,若否则编译时报错。
class C
{
public:
	virtual void func()
	{
		cout << "class C" << endl;
	}
};

class D :public C
{
public:
	virtual void func1() override // func1没有重写,编译报错
	{
		cout << "class B" << endl;
	}
};

2.4 区分重载、重写、重定义

【C++】继承与多态_第27张图片


2.5 抽象类

抽象类是一种不能实例化出对象的类,只能作为基类被继承。 抽象类中包含纯虚函数(在虚函数的声明后面加上 =0称为纯虚函数)。继承了抽象类的派生类也不能实例化出对象,只有重写了纯虚函数才可以实例化。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class AbstractClass
{
public:
	virtual void func() = 0 ; // 纯虚函数
};

class Next :public AbstractClass
{
public:
	virtual void func()
	{
		cout << "cover successfully" << endl;
	}
};

void test6()
{
	//AbstractClass x;//err, 抽象类不能实例化对象
	Next* pn = new Next();
	pn->func();
}

接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


2.6 多态的原理

2.6.1 虚函数表

提问,只有一个虚函数的类的大小为多少?

class A
{
public:
	virtual void func1()
	{
		cout << "func1() of A" << endl;
	}
};
cout << sizeof(A) << endl;

验证得到,其大小为4个字节

在这里插入图片描述

其实,每个包含虚函数的类都至少会有一个虚函数表(本质是一个函数指针数组,又称虚表),用于存放它的虚函数。因此,类对象中会存放一个虚函数表指针,指向其虚函数表。

【C++】继承与多态_第28张图片

看下面代码

class A
{
public:
	virtual void func1()
	{
		cout << "func1() of A" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
};

通过调试可以很清楚地观察虚函数表指针和虚函数表的存在

在这里插入图片描述


2.6.2 多态如何实现

  1. 派生类虚函数实现重写的过程

⭕派生类实例化时,会生成属于自己的虚表,总共分三步:

a. 先将基类中的虚表内容拷贝一份到派生类虚表中
b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

如图:

【C++】继承与多态_第29张图片

通过代码观察

class A
{
public:
	virtual void func1()
	{
		cout << "func1() of A" << endl;
	}

	virtual void func2()
	{
		cout << "func2()" << endl;
	}

	void func3()
	{
		cout << "func3()" << endl;
	}
};

class B :public A
{
public:
	virtual void func1()
	{
		cout << "func1() of B" << endl;
	}
	virtual void func4()
	{
		cout << "func4()" << endl;
	}

	void func5()
	{
		cout << "func5()" << endl;
	}
};

【C++】继承与多态_第30张图片

调试可以很清楚地发现,A::func1被重写为B::func1,func2无重写。但是却看不到派生类B自己的虚函数func4。这是因为vs监视窗口本身的问题,我们可以通过内存窗口更深入地观察虚函数表。

【C++】继承与多态_第31张图片

可以看到虚函数表中,func1和func2的指针后还有一个指针,那正是B类自己的虚函数func4。为了验证其准确性,我们也可以通过如下代码,取出虚函数表中每个虚函数并调用观察。

typedef void(*VFT)();

void CheckVFTable(VFT* ptr, int n)
{
	cout << "虚表地址:" << ptr << endl;
	for (int i = 0; i < n; ++i)
	{
		printf("[%d]:%p->", i, ptr[i]);
		ptr[i]();
	}

	cout << endl;
}

class A
{
public:
	virtual void func1()
	{
		cout << "func1() of A" << endl;
	}

	virtual void func2()
	{
		cout << "func2()" << endl;
	}

	void func3()
	{
		cout << "func3()" << endl;
	}
};

class B :public A
{
public:
	virtual void func1()
	{
		cout << "func1() of B" << endl;
	}
	virtual void func4()
	{
		cout << "func4()" << endl;
	}

	void func5()
	{
		cout << "func5()" << endl;
	}
};
void test5()
{
	B b;
	CheckVFTable((VFT*)(*(void**)&b), 3); //取出b对象空间中前4/8个字节,再强制成虚表指针类型
}

测试结果证明,func4的指针确实在B类对象的虚表中

【C++】继承与多态_第32张图片

  1. 多态调用的动态绑定

前面我们说过,实现多态,必须通过基类的引用或指针调用虚函数,这是有原因的。基类的引用/指针可以指向基类对象,也可以指向派生类对象(可见基类范围)。多态就是要达到指向基类时执行基类行为,指向派生类时执行派生类行为。经过前面的分析,我们知道基类和派生类都有虚表,在运行时,程序会根据当前指向的对象的虚表中来找到将要调用的虚函数。这种在运行时确认程序的具体行为,称为动态绑定

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "全价票" << endl;
	}
	// 其它成员...
};

class Student :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "学生票" << endl;
	}
	// 其它成员...
};

void test1()
{
	Person* p1= new Student();
	p1->Buyticket();
	
	Person* p2= new Person();
	p2->Buyticket();
}

如图,p1指向Student类对象时,可以找到Student::Buyticket;p2指向Person类对象时,可以找到Person::Buyticket

【C++】继承与多态_第33张图片

2.7 静态绑定和动态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态。

可以通过反汇编观察动态绑定的运动机理,及其与普通调用的区别。

【C++】继承与多态_第34张图片

你可能感兴趣的:(C++的修行之路,c++,开发语言)