【C++学习】日积月累——多态详解

一、多态的概念

1.1 概念

  多态通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。例如下图所示,移动支付(微信、支付宝)的抢红包活动,每个人所抢到的红包金额都不尽相同,有运气王也有手气一般的。

【C++学习】日积月累——多态详解_第1张图片

图1 抢红包

二、多态的定义及实现

2.1 多态的构成条件

  多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。例如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件

  1. 必须通过基类(父类)的指针或引用调用虚函数;
  2. 被调用的函数必须是虚函数,且派生类(子类)必须对基类的虚函数进行重写。
class Person {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
};

class Student :public Person {
public:
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
};

void Func(Person& people) {
	people.BuyTicket();
}

void test_polymorphism01() {
	Person p;
	Student s;
	Func(p);
	Func(s);
}

【C++学习】日积月累——多态详解_第2张图片

图2 2.1节案例代码的实现图

根据图3的调试结果看以:不同的对象(普通人p、学生s)去买票的结果是不一样的,其中普通人票价是全价,学生的票价是半价。

【C++学习】日积月累——多态详解_第3张图片

图3 调试结果

2.2 虚函数

  虚函数即被virtual修饰的类成员函数称为虚函数。例如下面案例的代码所示的虚函数:

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

2.3 虚函数的重写

  虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
    //重写 体现的是接口的继承,重写继承基类这个函数的实现
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
    
    virtual ~Person() {
        cout << "~Person()" << endl;
	}
};

class Student :public Person {
public:
    //这里的virtual可以不写,因为它继承父类的接口,重写实现
	void BuyTicket() {
		cout << "买票-半价" << endl;
	}
    
    ~Student() {
		cout << "~Student()" << endl;
	}
};

void Func(Person* p) {
	p->BuyTicket();
	delete p;
}

void test_polymorphism02() {
	Func(new Person);
	Func(new Student);
}

【C++学习】日积月累——多态详解_第4张图片

图4 虚函数重写调试结果

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议使用。

虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)

  派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时称为协变。

class Person {
public:
	virtual Person* BuyTicket() {
		cout << "买票-全价" << endl;
		return this;
	}
};

class Student :public Person {
public:
	virtual Student* BuyTicket() {
		cout << "买票-半价" << endl;
		return this;
	}
};

void Func(Person& people) {
	people.BuyTicket();
}

void test_polymorphism03() {
	Person p;
	Student s;
	Func(p);
	Func(s);
}

具体的调试结果如图5所示:

【C++学习】日积月累——多态详解_第5张图片

图5 虚函数的返回值不同时调试结果
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

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

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

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

void test_polymorphism04() {
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

具体的调试结果如图6所示:

【C++学习】日积月累——多态详解_第6张图片

图6 析构函数的重写

2.4 C++11 override和final

  从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car {
public:
	virtual void Drive() final {

	}
};

class Benz :public Car {
public:
	virtual void Drive() {
		cout << "Benz-舒适" << endl;
	}
};

具体的调试结果如图7所示:

image-20230404212509068

图7 虚函数包含关键字final的调试结果
  1. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car {
public:
	void Drive() {

	}
};

class Benz :public Car {
public:
	virtual void Drive() override {
		cout << "Benz-舒适" << endl;
	}
};

具体的调试结果如图8所示:

image-20230404212731050

图8 override检查虚函数

依据报错修改后的代码如下:

class Car {
public:
	virtual void Drive() {

	}
};

class Benz :public Car {
public:
	virtual void Drive() override {
		cout << "Benz-舒适" << endl;
	}
};

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

【C++学习】日积月累——多态详解_第7张图片

图9 三个概念对比

三、抽象类

3.1 概念

  在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

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

class Benz :public Car {
public:
	virtual void Drive() {
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car {
public:
	virtual void Drive() {
		cout << "BMW-操控" << endl;
	}
};

void test_polymorphism07() {
	Car c;
}

具体的调试结果详见图10,可知抽象类的对象无法实例化:

【C++学习】日积月累——多态详解_第8张图片

图10 抽象类实例化Bug
//派生类虚函数重写后可以实例化出对象
void test_polymorphism08() {
	Benz bz;
	BMW bw;
}

注意:一个类型在现实中没有对应的实体,可以将该类定义为抽象类。

3.2 接口继承和实现继承

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

四、多态的原理

4.1 虚函数表

class Base {
public:
	virtual void Func1() {
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
	char _ch;
};

void test_polymorphism08() {
	cout << sizeof(Base) << endl;
	Base bb;
}

通过调试可知对象bb占用12byte,除了成员变量_b_ch外,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个和平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表简称虚表。

【C++学习】日积月累——多态详解_第9张图片

图11 虚表(1)

4.2 多态原理

  依然是买票问题,经过分析可知:指针或引用指向基类对象调用基类的虚函数,指向派生类对象则调用派生类的虚函数。基类对象的虚表存放基类的虚函数,派生类对象的虚表存放派生类的虚函数。

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

class Student :public Person {
public:
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
};

void Func(Person& people) {
	people.BuyTicket();
}

void test_polymorphism09() {
	Person mike;
	Student johnson;
	Func(mike);
	Func(johnson);
}

具体调试解果如图12所示:

【C++学习】日积月累——多态详解_第10张图片

图12 虚表(2)
  • 图12中,红色箭头people指向mike对象时,people->BuyTicketmike的虚表中找到虚函数是Person::BuyTicket
  • 图12中,绿色箭头people指向johnson对象时,people->BuyTicketjohnson的虚表中找到虚函数是Student::BuyTicket
  • 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
class Base {
public:
	virtual void Func1() {
		cout << "Base::Func1()" << endl;
	}

	virtual void Func2() {
		cout << "Base::Func2()" << endl;
	}

	void Func3() {
		cout << "Base::Func3()" << endl;
	}

private:
	int _b = 1;
};

class Derive :public Base {
public:
	virtual void Func1() {
		cout << "Derive::Func1()" << endl;
	}

private:
	int _d = 2;
};

void test_polymorphism10() {
	Base b;
	Derive d;
}

【C++学习】日积月累——多态详解_第11张图片

图13 虚表(3)

  派生类Derive对象的虚表里拷贝了基类Base的虚表,并将虚函数Func1()地址进行重写(覆盖),对于虚表中Func2()虚函数的地址直接继承了基类Base的虚函数Func2()的地址。

  在Student类中添加虚函数Func4代码,结果在监视窗口查看时发现虚表中未发现Func4的指针。

virtual void Func4() {
	cout << "Derive::Func4()" << endl;
}

【C++学习】日积月累——多态详解_第12张图片

图14 虚表(4)

  若不想通过调式时使用监控窗口或内存来查看虚表,同时也为了应对图14的bug,亦可以使用下列代码打印虚表:

//用程序打印虚表
//声明一个函数指针 VF_PTR代表void(*)()
typedef void(*VF_PTR)();

//虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(VS系列编译器G++没有)
void PrintVFTable(VF_PTR table[]) {
	for (int i = 0; table[i] != nullptr; ++i) {
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

void test_polymorphism10() {
	Base b;
	Derive d;
	//能适应32位和64位平台
	cout << "b的虚表地址:" << endl;
	PrintVFTable(*(VF_PTR**)&b);//&b==Base* -> (int*)&b == (int*)(Base*)
	cout << "d的虚表地址:" << endl;
	PrintVFTable(*(VF_PTR**)&d);
}

图15可以发现打印结果与调式时内存结果完全相同:

【C++学习】日积月累——多态详解_第13张图片

图15 虚表(5)

小结

  • 虚表在编译阶段生成;
  • 对象中的虚表指针在构造函数的初始列表初始化阶段完成初始化;
  • 虚表是存在常量区(代码段);对象里存的是虚表指针,不是虚表。
  • 虚函数表指针(虚表指针);虚函数表,本质是一个虚函数指针数组;
  • 对象只拷贝成员,不拷贝虚函数表

4.3 多态绑定与静态绑定

  静态绑定又称前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,例如:函数重载;

  动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

五、单继承和多继承关系的虚函数表

  需要注意的是在单继承和多继承关系中,需要关注派生类对象的虚表模型。

5.1 单继承中的虚函数表

  详见4.2节多态原理案例Derive对象的虚表无法现实func4()指针。

5.2 多继承中的虚函数表

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

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

private:
	int b1;
};

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

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

private:
	int b2;
};

class Derive :public Base1, public Base2 {
public:
	virtual void func1() {
		cout << "Derive::func1()" << endl;
	}

	virtual void func3() {
		cout << "Derive::func3()" << endl;
	}

private:
	int d1;
};

typedef void(*VF_PTR)();
void PrintVTable(VF_PTR vTable[]) {
	cout << " 虚表地址" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i) {
		printf("[%d]:%p->", i, vTable[i]);
		VF_PTR f = vTable[i];
		f();
	}
	cout << endl;
}

void test_polymorphism11() {
	Derive d;
	PrintVTable((VF_PTR*)(*(int*)&d));

	//PrintVTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));
	Base2* ptr2 = &d;
	PrintVTable((VF_PTR*)(*(int*)(ptr2)));
}

调试结果具体如图所示:

【C++学习】日积月累——多态详解_第14张图片

图16 监视窗口、内存窗口及输出台的虚表对比

  此外,上图可知:多继承后,虚表中重写的func1()的地址不一样。

5.3 菱形继承、菱形虚拟继承

  在实际工程中,不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定性能损耗。所以菱形继承、菱形虚拟继承的虚表我们一般看不到。

class A {
public:
	virtual void func1() {

	}

public:
	int _a;
};

class B :virtual public A {
public:
	virtual void func1() {

	}

	virtual void func2() {

	}

public:
	int _b;
};

class C :virtual public A {
public:
	virtual void func1() {

	}

	virtual void func3() {

	}

public:
	int _c;
};

class D :public B, public C {
public:
	virtual void func1() {

	}

public:
	int _d;
};

void test_polymorphism12() {
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

菱形虚继承的虚表、虚基表,如图17所示:

【C++学习】日积月累——多态详解_第15张图片

图17 虚表、虚基表

你可能感兴趣的:(c++,学习,开发语言)