C++笔记归纳11:多态

多态

目录

多态

一、多态的概念

1.1.多态的概念

1.2.多态的类型

1.3.多态的定义及实现

二、虚函数

2.1.虚函数的定义

2.2.虚函数的重写(覆盖)

2.3.虚函数的试题

2.4.虚函数重写问题

2.4.1.协变

2.4.2.析构函数的重写

2.4.3.override和final关键字

2.5.重载vs重写vs隐藏

2.6.纯虚函数和抽象类

三、多态的原理

3.1.虚函数表指针(虚表指针)

3.2.动态绑定与静态绑定

3.3.虚函数表


一、多态的概念

1.1.多态的概念

**多态(polymorphism):**多种形态

1.2.多态的类型

**编译时多态(静态多态):**函数重载和函数模板

传不同类型的参数就可以调用不同的函数,通过参数的不同达到多种形态

示例:

cpp 复制代码
//函数重载
int i1 = 1;
int i2 = 2;
double d1 = 1.0;
double d2 = 2.0;

cout << i;
cout << d;

swap(i1,i2);
swap(d1,d2);

**编译时:**实参传给形参的参数匹配是在编译时完成的

**静态:**编译时一般归为静态,运行时一般归为动态

**运行时多态(动态多态):**完成某个行为(函数)时,传不同的对象可以完成不同的行为

示例:

买票(函数):

  • 普通人买票:全价买票
  • 学生买票:优惠买票
  • 军人买票:优先买票

动物的叫声(函数):

  • 猫对象:喵喵喵(>^w^<)
  • 狗对象:汪汪汪

1.3.多态的定义及实现

**多态的定义:**一个继承关系下的类对象,去调用同一函数,产生不同行为

示例:

Student继承了Person,Person对象买票全价,Student对象买票优惠

多态的构成条件:

  • 必须是基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数

条件1:

必须是基类的指针或引用,因为只有基类的指针或引用才能指向派生对象

条件2:

派生类必须对基类的虚函数重写(覆盖),重写或覆盖了,派生类才能有不同的函数

二、虚函数

2.1.虚函数的定义

**虚函数的定义:**类的成员函数前面加virtual修饰

(注:非成员函数不能加virtual修饰)

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

2.2.虚函数的重写(覆盖)

**派生类的虚函数重写了基类的虚函数:**派生类中有一个与基类完全相同的虚函数

(注:派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表类型完全相同)

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

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

int main()
{
	return 0;
}

必须是基类的指针或引用来调用虚函数

指针调用:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

void Func(Person * ptr)
{
	//这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	//但是跟ptr没关系,而是由ptr指向的对象决定的。
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	
	Func(&ps);
	Func(&st);
	
	return 0;
}

引用调用:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

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

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);
	
	return 0;
}

如果传参为对象,则不满足多态,只能调用父类的成员函数

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

void Func(Person ptr)
{
	ptr.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);
	
	return 0;
}

补充:

在重新基类虚函数时,派生类的虚函数在不加virtual关键字时

虽然也能构成重写,但是不规范,不建议这样使用

(因为继承后基类的虚函数被继承下来了,在派生类依然保持虚函数特性)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class Student : public Person 
{
public:
	void BuyTicket() { cout << "买票-打折" << endl; }
};

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

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);
	
	return 0;
}

**示例:**动物类的实现

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Animal
{
public:
	virtual void talk() const
	{}
};

class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};

void letsHear(const Animal& animal)
{
	animal.talk();
}

int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
} 

2.3.虚函数的试题

以下程序输出的结果是什么(B)

A.A->0

B.B->1

C.A->1

D.B->0

E.编译出错

F.以上都不正确

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class A
{
public:
	virtual void func(int val = 1) 
	{ 
		std::cout << "A->" << val << std::endl; 
	}

	virtual void test() 
	{ 
		func(); 
	}
};

class B : public A
{
public:
	void func(int val = 0) 
	{ 
		std::cout << "B->" << val << std::endl; 
	}
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

静态类型看声明:

变量/指针的声明是什么类型,它的静态类型就是什么(编译时决定,不可改变)

示例:A* p = new B(p的静态类型是A*)

动态类型看对象:

指针/引用的实际指向,在运行时才能确定(运行时决定,可以改变)

示例:A* p = new B (p的动态类型是B*)

函数调用看动态类型:

多态生效,执行子类的实现

默认参数/成员看静态:

看静态类型,取基类的数值

在本题中

test函数的this静态类型:A*(test写在A类中)

test函数的this动态类型:B*(p指向B的对象)

func()函数调用:看动态类型,调用B::func()

val默认参数:看静态类型,取A类的val = 1

最终输出:B->1

注:

这里的test中的this指针类型与p的指针类型无关

this指针的静态类型是因为test函数写在A类中

this指针的动态类型是因为p指向的是B类对象

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class A
{
public:
	virtual void func(int val = 1) 
	{ 
		std::cout << "A->" << val << std::endl; 
	}

	virtual void test() 
	{ 
		func(); 
	}
};
class B : public A
{
public:
	void func(int val = 0) 
	{ 
		std::cout << "B->" << val << std::endl; 
	}
};

int main(int argc, char* argv[])
{
	A* p1 = new A;
	p1->func();

	A* p2 = new B;
	p2->func();

	B* p3 = new B;
	p3->func();

	A* p4 = new A;
	p4->test();

	A* p5 = new B;
	p5->test();

	B* p6 = new B;
	p6->test();
	return 0;
}

**注:**在继承中,绝不重新定义继承而来的缺省值

2.4.虚函数重写问题

2.4.1.协变

**协变:**派生类重写基类虚函数时,与基类虚函数返回值类型可以不同

  • 基类虚函数返回基类对象的指针或引用
  • 派生类虚函数返回基类对象的指针或引用
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class A 
{
};

class B : public A 
{
};

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

class Student : public Person 
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

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

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
}
2.4.2.析构函数的重写

基类的析构函数为虚函数, 此时派生类析构函数只要定义

无论是否加virtual关键字,都与基类的析构函数构成重写

原因:

虽然基类与派生类析构函数名字不同(看起来不符合重写规则)

但是编译后的析构函数名称统一处理成destructor

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

//只有派生类Student的析构函数重写了Person的析构函数
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	//p1:析构函数(destructor)+operator delete()
	delete p1;
	delete p2;
}

如果不加virtual,不构成多态,那么无法调用派生类的析构函数

因为p2的类型为基类指针,指向的对象为派生类

当没有实现多态时,只能够调用基类的析构函数

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class A
{
public:
	//如果不加virtual,不构成多态
	//那么无法调用派生类的析构函数
	//因为p2的类型为基类指针
	//指向的对象为派生类
	//当没有实现多态时
	//只能够调用基类的析构函数
	~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;
}

直接用子类指针也没有问题

但多态的条件是使用基类的指针和引用调用虚函数

所以一般将子类的对象给父类的指针或引用

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	B* p2 = new B;

	delete p1;
	delete p2;
}
2.4.3.override和final关键字

C++对虚函数重写的要求比较严格,在有些情况下由于疏忽

(比如:函数名写错,参数写错)

造成无法构成重写,这种错误在编译期间不会报出

只有在程序运行时没有得到预期的结果才能显示

C++11提供了override,检测是否重写,重写就会显示报错

如果不想让派生类重写这个虚函数,可以用final修饰,重写就会显示报错

2.5.重载vs重写vs隐藏

重载:

  • 同一个作用域
  • 函数名相同
  • 参数个数、参数类型、参数顺序不同
  • 返回值可同、可不同

重写:

  • 分别在继承体系的父类和子类不同作用域
  • 函数名、参数、返回值相同(协变例外)
  • 必须为虚函数

隐藏

  • 分别在继承体系的父类和子类不同作用域
  • 函数名相同
  • 不构成重写就是隐藏
  • 父子类的成员变量相同也称为隐藏

2.6.纯虚函数和抽象类

**纯虚函数:**在虚函数后面加=0

纯虚函数不需要定义实现,只需要声明即可

(注:实现没有意义,因为会被派生类重写,但语法上可以实现)

**抽象类:**包含纯虚函数的类

(注:抽象类不能实例化出对象)

如果基类写了纯虚函数,而派生类不重写纯虚函数,那么派生类也是抽象类

纯虚函数某种程度强制了派生类重写虚函数,因为不重写实例化不出对象

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

int main()
{
	Car * pBenz = new Benz;
	pBenz->Drive();
	
	Car * pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

三、多态的原理

3.1.虚函数表指针(虚表指针)

下面编译为32为程序的运行结果是什么(D)

A.编译报错

B.运行报错

C.8

D.12

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

除了_b,_ch成员,还多了虚函数表指针__vfptr(virtual function)

虚函数表(虚表):本质是函数指针数组

一个含有虚函数的类中都至少有一个虚函数表指针

因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中

名称 类型
__vfptr 0x00007ff711c5bc38 {test_3_14.exe!void(* Base::`vftable'[4])()} {0x00007ff711c515a5 {test_3_14.exe!Base::Func1(void)}, ...} void * *
[0] 0x00007ff711c515a5 {test_3_14.exe!Base::Func1(void)} void *
[1] 0x00007ff711c515b4 {test_3_14.exe!Base::Func2(void)} void *
[2] 0x00007ff711c515b9 {test_3_14.exe!Base::Func3(void)} void *
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
	string _id;
};

class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
	string _codename;
};

void Func(Person * ptr)
{
	//这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	//但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}

int main()
{
	//其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
	//多态也会发生在多个派生类之间。
	Person ps;
	Student st;
	Soldier sr;
	Func(&ps);
	Func(&st);
	Func(&sr);
	return 0;
}
名称 类型
ps {_name="" } Person
__vfptr 0x00007ff6ac07df70 {test_3_14.exe!void(* Person::`vftable'[2])()} {0x00007ff6ac07105a {test_3_14.exe!Person::BuyTicket(void)}} void * *
_name "" std::string
st {_id="" } Student
◢ Person {_name="" } Person
__vfptr 0x00007ff6ac07df98 {test_3_14.exe!void(* Student::`vftable'[2])()} {0x00007ff6ac0715a5 {test_3_14.exe!Student::BuyTicket(void)}} void * *
_name "" std::string
_id "" std::string
sr {_codename="" } Soldier
◢ Person {_name="" } Person
__vfptr 0x00007ff6ac07dfc0 {test_3_14.exe!void(* Soldier::`vftable'[2])()} {0x00007ff6ac0711a9 {test_3_14.exe!Soldier::BuyTicket(void)}} void * *
_name "" std::string
_codename "" std::string

指向谁调用谁

指向哪个对象,在运行时,到指向对象的虚函数表中

找到对应虚函数的地址,然后进行调用

3.2.动态绑定与静态绑定

**静态绑定:**编译时确定调用的函数地址

对于满足多态条件的函数调用是在编译时绑定的

(指针或引用+调用虚函数)

**动态绑定:**运行时到指向对象的虚函数表中找到调用函数的地址

满足多态条件的函数是在运行是绑定的

cpp 复制代码
// ptr是指针+BuyTicket是虚函数满足多态条件。
// 这里就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调用函数地址
    ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax

// BuyTicket不是虚函数,不满足多态条件。
// 这里就是静态绑定,编译器直接确定调用函数地址
    ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)

3.3.虚函数表

基类对象的虚函数表中存放基类所有虚函数的地址

同类型对象虚表共用,不同类型对象虚表各自独立

**派生类有两部分组成:**继承下来的基类和自己的成员

一般情况下,继承下来的基类中有虚函数表指针,派生类自己就不会再生成一个虚函数表指针

但这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个

(就像基类对象的成员和派生类对象中基类对象成员也是独立的)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

	void func5() 
	{ 
		cout << "Base::func5" << endl;
	}
protected:
	int a = 1;
};

class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() 
	{ 
		cout << "Derive::func1" << endl;
	}

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

	void func4() 
	{ 
		cout << "Derive::func4" << endl; 
	}
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

派生类中重写的基类的虚函数,派生类的虚函数表中

对应的虚函数就会被派生类重写的虚函数地址覆盖

派生类的虚函数表包含基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址

虚函数表本质:存虚函数指针的指针数组

(一般情况下,vs编译器会在这个数组最后放个0x00000000标记,g++不会)

与普通函数一样,虚函数编译好后是一段指令,存放在代码段,虚函数的地址存放在虚表中

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

	void func5() 
	{ 
		cout << "Base::func5" << endl;
	}
protected:
	int a = 1;
};

class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() 
	{ 
		cout << "Derive::func1" << endl;
	}

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

	void func4() 
	{ 
		cout << "Derive::func4" << endl; 
	}
protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	
	Base b;
	Derive d;
	Base * p3 = &b;
	Derive * p4 = &d;
	
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

在VS下,虚表可以认为放在静态区(常量区)

相关推荐
小道士写程序2 小时前
Babylon.js WebGPU Ocean Demo — 完整踩坑记录
开发语言·javascript·ecmascript
Code知行合壹2 小时前
JDK10新特性
开发语言·jdk
qq_390760392 小时前
简单的线程安全日志记录器
开发语言·数据库·c#
T1an-12 小时前
C++11智能指针shared_ptr的控制块内都有什么?
开发语言·c++
迈巴赫车主2 小时前
天梯赛 L2-004 这是二叉搜索树吗?java
java·开发语言·数据结构·算法·天梯赛
小鸡吃米…2 小时前
基准测试与性能分析
开发语言·python
神仙别闹2 小时前
基于MATLAB实现(GUI)汽车出入库识别系统
开发语言·matlab·汽车
今儿敲了吗2 小时前
python基础学习笔记第一章
开发语言·python
badhope2 小时前
C语言二级考点全解析与真题精讲
c语言·开发语言·c++·人工智能·python·microsoft·职场和发展