C++:多态(面向对象的主要手段之一)

Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。

我的博客: <但愿.

我的专栏: C语言题目精讲算法与数据结构C++

欢迎点赞,关注

目录

前言

一多态

1.1多态概念

1.2多态分类

1.2.1编译时多态(静态多态)

1.2.2运⾏时多态(动态多态)

1.3运⾏时多态(动态多态)的实现

二虚函数

2.1虚函数概念

2.2虚函数的重写/覆盖

2.2.1虚函数的重写/覆盖的概念

2.2.2虚函数的重写/覆盖的实现

2.2.3虚函数在多态中的应用

2.3虚函数重写的⼀些其他问题

2.3.1协变(了解)

2.3.1.1协变概念

2.3.1.2协变分类(两种)

2.3.1.3协变的实现

2.3.2析构函数的重写

2.3.2.1析构函数的重写的概念

2.3.2.2什么时候要重写析构函数

2.3.2.3析构函数的重写的作用

2.3.2.4析构函数的重写的实现

2.3.3C++11: override 和 final关键字

2.3.3.1override关键字

2.3.3.2final关键字

2.3.4重载/重写/隐藏的对⽐(重要)

三纯虚函数和抽象类

3.1纯虚函数和抽象类的概念

3.1.2抽象类(无法实例化出对象,但是可以定义成指针/引用)

四多态的原理

4.1验证虚函数表指针的存在

4.2 多态的原理

4.2.1多态是如何实现的(怎么达到指向谁调用谁)

4.2.2动态绑定与静态绑定

4.2.3 虚函数表【存放虚函数地址的一个指针数组】

前言

**多态分为编译时多态(静态多态)和运⾏时多态(动态多态),**由于编译时多态(静态多态)我们前面的函数重载、函数模板就是,所以这⾥我们重点讲运⾏时多态。

一 多态

1.1多态概念

函数调用所展现出的多种形态。

1.2多态分类

1.2.1编译时多态(静态多态)

编译时多态(静态多态) 主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的 函数,通过参数不同达到多种形态 ,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的我们把编译时⼀般归为静态,运⾏时归为动态

1.2.2运⾏时多态(动态多态)

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同类型的对象就会完成不同的⾏为,就达到多种形态。

1.3运⾏时多态(动态多态)的实现

在继承的条件下
必须是基类的指针或者引⽤调⽤虚函数(因为只有基类的指针或引⽤才能既指向基类
对象⼜指向派⽣类对象)
•被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖(只有重写或者覆盖了,基类和派
⽣类之间才能有不同的函数,多态的不同形态效果才能达到)

【示例】

cpp 复制代码
//两种动态-静态动态(编译时动态)/动态动态(运行时动态)
//1 静态动态(编译时动态)-函数重载,函数模板【通过函数匹配等条件在编译时匹配函数】
//int main()
//{
//	int i = 1;
//	double d = 2.1;
//	cout << i << d << endl;//静态动态,这里对于输出两个不同类型的变量,看起来是调用同一个函数,
//	//实际上是通过函数重载分别调用两个不同的函数
//}

//2动态动态(运行时动态)
//必须满足条件:2.1必须是基类的指针或者引⽤调⽤虚函数
//2.2被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
class Person {
public:
	virtual void BuyTicket()//虚函数
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person {
public:
	virtual void BuyTicket()
	{
		cout << "买票-打折" << endl;//虚函数的重写
	}
};

void func(Person& ptr)//使用基类的引⽤调⽤虚函数
{
	ptr.BuyTicket();//调⽤的函数是虚函数
}

void func(Person* ptr)//使用基类的指针调⽤虚函数
{
	ptr->BuyTicket();//调⽤的函数是虚函数
}

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

二 虚函数

2.1虚函数概念

类成员函数 前⾯ 加virtual修饰 ,那么这个成员函数被称为虚函数。注意 ⾮成员函数不能加virtual修
饰。
【示例】

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

2.2虚函数的重写/覆盖

2.2.1虚函数的重写/覆盖的概念

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,三同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

2.2.2虚函数的重写/覆盖的实现

一个动物类中有动物发出的叫声将其作为基类,实现猫和狗的类。

【示例】

cpp 复制代码
class Animal
{
public:
	virtual void talk() const//发出叫声的函数
	{
	}
};

class Dog :public Animal
{
public:
	virtual void talk() const//跟基类完全相同的虚函数
	//(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,三同)
	{
		cout << "汪汪"<<endl;
	}
};

class Cat :public Animal 
{
public:
	virtual void talk() const////跟基类完全相同的虚函数
	//(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,三同)
	{
		cout << "(>^ω^<)喵"<< endl;
	}
};

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

int main()
{
	Dog dog;
	Cat cat;
	letsHear(dog);
	letsHear(cat);
	return 0;
}
2.2.3虚函数在多态中的应用

这里只有注意函数重写是实现(定义) ,所以函数重写时虚函数对应的声明是基类中对应虚函数的声明(所以派生类可以不加关键字virtual),只有多态调用才这样 。所以在多态的调用中一个虚函数的重写是由基类/父类的声明+派生类/子类的定义组成。

【示例】
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

cpp 复制代码
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();//对于多态的调用,重写只是实现(定义),而函数的声明还是基类中对应函数的声明,
	//所以一个重写的函数是基类中的声明+派生类中的定义,只有多态的调用是这样。

	//// 不是多态调用
	p->func();
	return 0;
}

运行结果:

由于**在多态的调用中一个虚函数的重写是由基类/父类的声明+派生类/子类的定义组成。**所以选B

2.3 虚函数重写的⼀些其他问题

2.3.1协变(了解)
2.3.1.1协变概念

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。

2.3.1.2协变分类(两种)

自身基类/父类和自身派生类/子类的指针或引用作为返回值;将 一对满足基类和派生类关系的指针或引用作为返回值。

2.3.1.3协变的实现
cpp 复制代码
//协变   
//1与自己基类和自己的派生类作的指针或引用为返回值
class Person {
public:
	virtual Person* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual Student* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

//2作为一对满足基类和派生类关系的指针或引用返回值

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

	return 0;
}
2.3.2析构函数的重写
2.3.2.1析构函数的重写的概念

基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写(即 基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写)。 虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,但是 由于编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor()函数, 所以基类的析构函数加了 vialtual修饰,基类和派⽣类的析构函数就构成重写

2.3.2.2什么时候要重写析构函数

如果是在一个继承中就一定要在基类的析构函数中加关键字virtual,因为析构函数是通过this指针(一个基类指针)指向一个基类和派生类,如果此时调用析构函数,尤其是派生类调用对应的析构函数是如果不加关键字(形成基类和派生类的析构函数重写)就无法正确调用对应的析构函数 。令一种说法是析构函数被编译器统一处理成一个函数,就是为了形成重写,基类析构函数加上关键字virtual,从而满足多态,只有满足多态才会正确调用对应的析构函数。

2.3.2.3 析构函数的重写的作用

防止内存泄漏,例如delete分为两步 一:调用对应的析构函数。二:调用重载的delete函数。如果此时不能正确的调用析构函数就会导致内存泄漏。

2.3.2.4析构函数的重写的实现
cpp 复制代码
class A
{
public:
	 virtual~A()//基类的析构函数一定要加virtual关键字(虚函数)
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()//派生类可加可不加virtual关键字(因为继承)
	{
		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;

	return 0;
}
2.3.3C++11: override 和 final关键字
2.3.3.1 final关键字

final修饰一个类,类不能被继承

final修饰一个虚函数,函数不能被重写,要注意只能修饰类的成员函数,其他的全局,静态函数等等都不能被修饰

2.3.3.2override关键字

override,可以帮助⽤⼾检测是否重写
【示例】

cpp 复制代码
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}


// error C3248: "Car::Drive": 声明为"final"的函数⽆法被"Benz::Drive"重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}

2.3.4重载/重写/隐藏的对⽐(重要)

三 纯虚函数和抽象类

3.1纯虚函数和抽象类的概念

3.1.1纯虚函数

虚函数的后⾯写上 =0 ,则这个函数为纯虚函数纯虚函数 不需要定义实现 (实现没啥意义因为要被派⽣类重写,但是语法上可以实现), 只要声明即可

3.1.2抽象类(无法实例化出对象,但是可以定义成指针/引用)

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象 ,如果 派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

【示例】

cpp 复制代码
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()
{
// 编译报错:error C2259: "Car": ⽆法实例化抽象类
Car car;//car包含纯虚函数所以是抽象类
Car* pBenz = new Benz;//抽象类可以定义指针/引用
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}

四 多态的原理

4.1验证虚函数表指针的存在

【示例】
下⾯编译为32位程序的运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 8 D. 12

cpp 复制代码
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则类的大小是8个字节,但是运行结果是12字节,由于类中有一个虚函数,所以会有一个虚函数列表(一个指针数组)由于储存虚函数的地址。

我们来看看类中包含的成员变量:

4.2 多态的原理

4.2.1多态是如何实现的(怎么达到指向谁调用谁)

由于多态的调用都是基类的指针或者引用,那也就意味着其实看到的都是一个基类/父类对象(切片)。所以在完成了虚函数的重写了以后,不同类型基类/父类和派生类/子类当中虚表(虚函数表)当中放的就是不同的虚函数(基类/父类是基类/父类的,重写是重写后的),但是不是直接调用,而是运行时到指向对象的虚函数表中找到对应的虚函数进行调用(我们之前函数重载都是在编译时通过参数匹配就已经确定了)而运行时动态是运行时让你到指向对象的虚表中去找,这时候不就指向谁调用谁【总结指向谁调用谁:执行逻辑都是运行时到虚表中找对应的虚函数进行调用。即不管传基类函数派生类都是一致性的动作(通过虚表指针->虚表->取到虚函数地址->call函数地址)到同一个虚表中call(取地址),那为什么结果不同(因为传的对象不同,指向的虚表内容不同)。

为什么是实参是基类的指针或引用,虚函数的重写呢?我们看到的都是在基类/父类的虚表中找(因为切片即使传的是派生类得到的也是父类那部分),所以我们只管到基类/父类对象指向的虚表中找(只是这个基类/父类可能是本身自己是基类/父类,有可能是由派生类/子类切片得到的分类,做的但是一致性动作。【只有是基类的指针/引用,才可以指向派生类,也可以指向基类,实现指哪个调用哪个;只有虚函数的重写底层的虚表才能完成对应的覆盖,对应的位置就是重写的虚函数】

【示例】

cpp 复制代码
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;
}
4.2.2动态绑定与静态绑定

• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤ 函数的地址,叫做静态绑定。
• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数 的地址,也就做动态绑定。
【示例】

cpp 复制代码
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)
{
ptr->BuyTicket();
}

int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;

Func(&ps);
Func(&st);

return 0;
}

【我们来看一看满足动态时调用时的反汇编和去除动态调用时的反汇编】

4.2.3 虚函数表【存放虚函数地址的一个指针数组】

• 任何一个类对象,只有类有虚函数,就一个有虚函数表(因为虚函数表是用于储存虚函数地址的指针数组】
• 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表【即一个同类型的多个对象,它们的虚函数表是一样的(同一个)】,不同类型的对 象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表【哪怕没有重写也是一样】。
• 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表 指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基 类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴ 的。
• 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函 数地址。
• 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类 ⾃⼰的虚函数地址三个部分。
• 一个类虚函数表的个数:一般是一个,但是在多继承当中就不止一个(一个类继承了两个类,如果两个基类/父类都有虚函数是,就不止一个)
• 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)
• 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函 数的地址⼜存到了虚表中。
• 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以 对⽐验证⼀下。vs下是存在代码段(常量区)。

【vs下虚函数表储存位置,不同编译器可能不同自己可以去验证】

其实验证办法很简单:我们只需要起运行变量分别放在局部、全局、静态、栈中让这四个变量的地址与虚函数的地址进行比较观察地址离哪个更近即可。

【示例】

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

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

	virtual void func3() {}

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

	Student st;
	Student* ptr = &st;
	printf("虚函数表地址:%p\n", *((int*)ptr));

	return 0;
}

运行结果:

从运行结果来看,在vs下虚函数是储存在常量区,而网上一般都是说储存在静态区,其他编译器也可能不同,感兴趣的私下可以去验证!!!

本篇文章就到此结束,欢迎大家订阅我的专栏,欢迎大家指正,希望有所能帮到读者更好理解C++相关知识 ,觉得有帮助的还请三联支持一下~后续会不断更新C/C++相关知识,我们下期再见。

相关推荐
leo__5208 小时前
基于MATLAB实现的鲁棒性音频数字水印系统
开发语言·matlab·音视频
小年糕是糕手8 小时前
【C++】string类(一)
linux·开发语言·数据结构·c++·算法·leetcode·改行学it
sali-tec8 小时前
C# 基于halcon的视觉工作流-章70 深度学习-Deep OCR
开发语言·人工智能·深度学习·算法·计算机视觉·c#·ocr
晚霞的不甘8 小时前
C语言利用数组处理批量数据详解
android·c语言·开发语言
渡我白衣8 小时前
C++可变参数队列与压栈顺序:从模板语法到汇编调用约定的深度解析
c语言·汇编·c++·人工智能·windows·深度学习·硬件架构
_OP_CHEN8 小时前
【从零开始的Qt开发指南】(十二)Qt 布局管理器终极指南:5 大布局 + 实战案例,搞定所有界面排版需求
开发语言·qt·前端开发·qt控件·布局管理器·gui开发
ForteScarlet8 小时前
Kotlin 2.3.0 现已发布!又有什么好东西?
android·开发语言·后端·ios·kotlin
武藤一雄8 小时前
C#中常见集合都有哪些?
开发语言·微软·c#·.net·.netcore
艾莉丝努力练剑8 小时前
【Linux进程(二)】Linux进程的诞生、管理与消亡:一份基于内核视角的完整分析
大数据·linux·运维·服务器·c++·安全·centos