【C++】继承与多态

news2024/10/1 19:26:00

目录

  • 前言
  • 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的基类(父类)下文均用基类、派生类的叫法

在这里插入图片描述

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


1.2 继承的定义

在这里插入图片描述

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

📝如下表

基类成员\继承方式publicprotectedprivate
基类中的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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

  • 基类指针可以通过强制类型转化赋值给派生类指针,但是要注意可能存在越界问题。
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();
}

⭕测试结果

在这里插入图片描述

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

⭕构造完毕,符合预期
在这里插入图片描述

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

⭕ 拷贝构造成功
在这里插入图片描述

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

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部分调用了基类的默认构造函数去初始化

在这里插入图片描述
💡得出结论,当派生类的拷贝构造函数的初始化列表不显示地调用基类拷贝构造,会调用基类的默认构造函数来初始化基类部分,而非调用基类的默认拷贝构造函数。但这并不符合“拷贝构造”的需求,因此我们自己写派生类的拷贝构造函数时,必须在初始化列表显式地调用基类拷贝构造函数。

  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. 单继承:一个派生类只有一个直接基类

在这里插入图片描述

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

在这里插入图片描述

1.7.2 菱形继承

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

在这里插入图片描述

💬代码实现

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的类模型:

在这里插入图片描述

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

  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++提供了菱形虚拟继承的方法,用以解决数据冗余和二义性问题。

1.7.3 菱形虚拟继承

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

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

在这里插入图片描述

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

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 菱形虚拟继承的原理

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

在这里插入图片描述

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

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

  1. 菱形继承
    在这里插入图片描述

  2. 菱形虚拟继承
    在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

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();
}

在这里插入图片描述

🔎 下面介绍几个概念

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

在这里插入图片描述

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

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

在这里插入图片描述

而如下派生类函数不加 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类的派生类成员没有被释放,造成内存泄漏。

在这里插入图片描述

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

💭 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加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;
}

在这里插入图片描述


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 区分重载、重写、重定义

在这里插入图片描述


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个字节

在这里插入图片描述

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

在这里插入图片描述

💬看下面代码

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

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

在这里插入图片描述


2.6.2 多态如何实现

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

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

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

如图:

在这里插入图片描述

💬通过代码观察

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

在这里插入图片描述

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

在这里插入图片描述

可以看到虚函数表中,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类对象的虚表中

在这里插入图片描述

  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

在这里插入图片描述

2.7 静态绑定和动态绑定

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

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/380566.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch实战之(商品搜索API实现)

Elasticsearch实战之&#xff08;商品搜索API实现&#xff09; 1、案例介绍 某医药电商H5商城基于Elasticsearch实现商品搜索 2、案例分析 2.1、数据来源 商品库 - 平台运营维护商品库 - 供应商维护 2.2、数据同步 2.2.1、同步双写 写入 MySQL&#xff0c;直接也同步往…

如何使用C2concealer生成随机化的C2 Malleable配置文件

关于C2concealer C2concealer是一款功能强大的命令行工具&#xff0c;在该工具的帮助下&#xff0c;广大研究人员可以轻松生成随机化的C2 Malleable配置文件&#xff0c;以便在Cobalt Strike中使用。 工具运行机制 开发人员对Cobalt Strike文档进行了详细的研究&#xff0c;…

【转载】2020融云:基于WebRTC的低延迟视频直播

原文直接访问本文是读书笔记。基于WebRTC的低延迟视频直播 需要学习rtp包的缓存设计,于是找到了这一篇文章rtp包缓存 如何适应直播需求?直播与实时通信的区别 流量更少: RTMP或者HLS主要基于TCP传输,WebRTC是基于UDP的传输, **UDP协议的头小。**TCP为了保证传输质量,因…

Zotero设置毕业论文/中文期刊参考文献格式

大家在使用zotero时很容易遇到的问题&#xff1a; 英文参考文献中有多个作者时出现“等”&#xff0c;而不是用"et al"引文最后面有不需要的DOI号&#xff0c;或者论文链接对于一些期刊分类上会出现OL字样&#xff0c;即[J/OL]作者名为全大写 本文主要解决以上几个…

string函数以及string常用接口

本文介绍的是C关键字string中一些重要用法&#xff0c;以及各种字符串序列的处理操作 ——飘飘何所似&#xff0c;天地一沙鸥 文章目录前言一、string&#xff08;字符串类&#xff09;二、string类对象的容量操作2.1 size/length2.2 capacity2.3 empty/clear2.4 resize/reser…

教你如何搭建设备-保养管理系统,demo可分享

1、简介1.1、案例简介本文将介绍&#xff0c;如何搭建设备-保养管理。1.2、应用场景设备管理员进行制定设备保养计划、记录设备保养信息、可以查看设备保养日历。2、设置方法2.1、表单搭建1&#xff09;新建表单【设备档案-履历表】&#xff0c;字段设置如下&#xff1a;名称类…

SSM SpringBoot vue 在线教学质量评价系统

SSM SpringBoot vue 在线教学质量评价系统 SSM 在线教学质量评价系统 功能介绍 首页 图片轮播展示 登录 学生注册 教师注册 督导注册 教师展示 教师详情 学生评价 课程信息 课程详情 提交选修该课 学生选课 学生留言 个人中心 后台管理 管理员或学生或教师或督导登录 个人中…

项目经理处理团队冲突 5大注意事项

1、在时间、场景、体验矩阵中的5种处理方式 第一种方式&#xff1a;强迫命令&#xff0c;即职位高的一方在不考虑对方感受的情况下&#xff0c;强迫职位低的一方接受自己的意见。这种处理方式的适用场景为重要且紧急&#xff0c;这种方式团队成员的体验感低。 第二种方式&#…

Linux 学习笔记(一):终端 和 Shell 的区别和联系

一、Linux 介绍 1、什么是 Linux Linux 就是一个操作系统&#xff0c;全称 GNU/Linux&#xff0c;是一种类 Unix 操作系统Linux 一开始是没有图形界面的&#xff0c;所有操作都靠 命令 完成。如 磁盘操作、文件存取、目录操作、进程管理、文件权限 等等&#xff0c;可以说 Li…

Android Handler机制(二) Handler 实现原理

一. 前言 接上一篇文章为什么设计Handler , 我们来继续讲解一下Handler的实现原理, 俗话说一个好汉三个帮, 接下来一步一步引入各个主角,并说明它们在Handler机制中扮演的角色和作用. 二. Handler实现原理 首先我们先确定一个结论: 使用 Handler 是希望它被实例化在哪个线程&a…

不同序列模型的输入和输出总结

不同序列模型的输入和输出总结 文章目录不同序列模型的输入和输出总结RNNLSTMGRURNN RNN 是迭代输出&#xff1a; 输入第一个 -> 输出第二个&#xff0c; 输入第二个 -> 输出第三个&#xff0c; 输出倒数第二个 -> 输出最后一个。 LSTM LSTM 也是迭代输出&#xff…

Ep_操作系统面试题-什么是协程

协程 是一种 比线程更加轻量级的存 在&#xff0c;一个线程可以拥有多个协程。是一个特殊的 函数 &#xff0c;这个函数可以在某个地方挂起&#xff0c;并且可以重新在挂起处外继续运行。协程 不是被操作系统内核所管理 &#xff0c; 而完全是由程序所控制&#xff08;也就是在…

冯诺依曼体系结构与操作系统的理解

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;操作系统 &#x1f4ac;<3>前言&#xff1a;今天来介绍一下冯诺依曼体系结构&#xff0c;和操作系统的理解。 目录 1.冯诺依曼体系结构 冯诺依曼体系的工作原理&#xff1a; 为…

注意力机制详解系列(四):混合注意力机制

👨‍💻作者简介: 大数据专业硕士在读,CSDN人工智能领域博客专家,阿里云专家博主,专注大数据与人工智能知识分享。 🎉专栏推荐: 目前在写CV方向专栏,更新不限于目标检测、OCR、图像分类、图像分割等方向,目前活动仅19.9,虽然付费但会长期更新,感兴趣的小伙伴可以…

超详细Eclipse配置JDK

在此附上Eclipse安装教程 超详细Eclipse安装教程 在此附上JDK1.8安装配置教程 超详细JDK1.8安装与配置 ①打开Eclipse–>点击Window–>点击Preferences ②找到Java–>找到Installed JREs–>点击Add… ③选中Standard VM–>之后点击Next ④点击Directory找…

Substrate 基础教程(Tutorials) -- 模拟网络 添加可信节点

三、模拟网络 本教程基本介绍了如何使用一个私有验证器&#xff08;validators&#xff09;的授权集合来启动私有区块链网络。 Substrate节点模板使用授权共识模型(authority consensus model)&#xff0c;该模型将块生产限制为授权帐户的旋转列表(rotating list)。授权帐户(…

活动目录(Active Directory)批量用户管理

在大型复杂的 Windows 网络中&#xff0c;批量管理 AD 用户可能是一项挑战。此外&#xff0c;使用本机工具和 PowerShell 脚本需要深入了解 AD 和脚本&#xff0c;才能在 AD 中完成批量用户管理。 ADManager Plus是一款AD管理和报告软件&#xff0c;允许您使用CSV创建和管理多…

visual studio 2022 社区版 c# 环境搭建及安装使用【图文解析-小白版】

visual studio 2022 社区版 c# 环境搭建及安装使用【图文解析-小白版】visual studio 安装 C# 环境安装流程创建c#窗体应用程序visual studio 安装 C# 环境 首先&#xff0c;进入其官网下载对应的visual studio社区版本&#xff0c;官网链接: https://visualstudio.microsoft…

【操作系统】内存管理

虚拟内存 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存&#xff0c;从而让程序获得更多的可用内存。 为了更好的管理内存&#xff0c;操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间&#xff0c;这个地址空间被分割成多个块&#xff0c;每一块称为一页。…

网络安全入门教程(非常详细)从零基础入门到精通,看完这一篇就够了。

学前感言: 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了 2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发. 3.有时多google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答. 4.遇到实在搞不懂的,可以先放放,以后再来解决…