C++多态

目录

一.多态的概念

构成多态的两个条件

二.虚函数

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

2.虚函数重写的两个例外:

三.C++11的override和final

四.重载、覆盖(重写)、隐藏(重定义)

五.抽象类和接口继承

纯虚函数

接口继承和实现继承

​编辑

六. 多态的原理

 虚表指针和虚表

 动态绑定与静态绑定

 七.常见面试题


 无论村子有多么黑暗,多少矛盾,我都是木叶的宇智波鼬,我会在暗处守护着木叶

C++多态_第1张图片

一.多态的概念

1.多态是在继承的基础之上实现的,我们说继承是类设计层次的代码复用的一种手段,而多态则是在此基础上实现的多种形态,完成某一件事,可以由于对象的不同产生不同的完成结果,我们称这种现象为多态。

2.多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
C++多态_第2张图片

构成多态的两个条件

 1.虚函数的重写,三同(函数名,参数,返回值)

2.父类指针或引用去调用

二.虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

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

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

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

派生类中继承基类的虚函数可以不加virtual关键字,虽然可以不加,但还是建议大家加上,代码风格更为规范一些。 

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};

//多态的第二个条件,父类指针或引用去调用
void Func(Person& p)
{ 
p.BuyTicket();
 }

//指针
//void Func(Person* p)
//{
//	p->BuyTicket();
//}

//普通调用,这个错误
//void Func(Person p)
//{
//	p.BuyTicket();//和对象类型有关,不符合多态调用,这里只会调用父类Person的BuyTicket()
//}

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

2.虚函数重写的两个例外:

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
class A{};
class B : public A {};

class Person {
public:
	virtual A* f() {return new A;}
};

class Student : public Person {
public:
	virtual B* f() {return new B;}
};

2.析构函数的重写(基类与派生类析构函数的名字不同)

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

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

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

int main()
{
	
	Person p1;
	Student p2;

	return 0;
}

C++多态_第3张图片

通过new来间接调用构造进行对象的创建,将对象的空间开辟在堆上,而不是开辟在栈上,并且两个对象在new之后都是用基类指针进行接收的,这也很合理, 因为基类指针既可以指向基类对象又可以指向派生类对象,但是delete的时候这里就会出问题,由于析构函数不是虚函数,则调用一定不是多态调用,那就是普通调用,普通调用只和调用对象类型有关,则new出来的Person和Student对象的析构都调用的是Person类的析构,如果派生类没有申请资源还好说,但只要申请资源,则Person的析构是无法完成Student对象资源的清理的,那在进程运行期间就会发生内存泄露,这就出大事情了。

C++多态_第4张图片

C++多态_第5张图片


三.C++11的override和final

1.final修饰类,类为最终类不能被继承。

如何实现一个不能被继承的类?
1.构造私有,C++98的方式。继承下来的成员不能自己初始化,所以如果父类构造函数私有,则不能被调用
2.类定义时加final,C++11的方式,表示类为最终类,不能被继承。
class A final
{
private:
	A()
	{}
};
class B :public A
{
};
int main()
{
	//B b;
	
	return 0;
}
2.final修饰虚函数,表示该虚函数不能再被重写(了解就行,虚函数不能被重写意义不大)
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

1.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car {
public:
	virtual void Drive(int x) {}
//这样的基类虚函数,派生类就没有重写,override会检查出来并报错。
//如果不加override,构成隐藏
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
	//assert是运行时做的断言检查,override是编译时做的虚函数是否重写检查。
};
int main()
{
	Benz b;
}

 C++多态_第6张图片

四.重载、覆盖(重写)、隐藏(重定义)

C++多态_第7张图片

隐藏可以看作重写的条件的子集。

五.抽象类和接口继承

纯虚函数

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

//抽象类
class Car//抽象类---不能实例化出对象
{
public:
	//纯虚函数所在类为抽象类
	virtual void Drive() = 0 //一般纯虚函数不写实现,写了也没啥用,因为其所在类无法实例化出对象
	{
		cout << "endl;" << endl; 
	} ;
};
class Benz :public Car
{
public:
 //如果不重写纯虚函数,则自然继承下来之后,派生类也会变为抽象类,自然也不能实例化出对象。
 //只有对纯虚函数进行重写之后,函数就不算纯虚函数了,派生类就不再是抽象类,就可以实例化出对象。抽象类强制子类重写纯虚函数
	virtual void Drive()//重写纯虚函数
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	BMW b;//如果BMW没有重写纯虚函数,则继承下来的纯虚函数就是原生的,那么BMW就是抽象类,抽象类是不能实例化对象的。
	//抽象类从某种程度上说,就是强迫子类重写纯虚函数。
	//override是检查虚函数是否重写,抽象类是强迫派生类重写纯虚函数,否则派生类无法实例化出对象。

	
}

接口继承和实现继承

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

 C++多态_第8张图片

首先,对于非静态类成员函数的调用,无论是对象去调用函数,还是函数之间进行调用,他们本质都是通过隐含的this指针来完成调用的,那么B继承A中的test()后,并未显示给出,则不满足重写定义,所以test()的this指针类型还是原来基类类型的也就是A *,那么当test()里面调用func时,就是通过A *的this指针进行调用,而func又是虚函数的重写,所以此时就满足多态调用,并且A *this指针指向的对象是B对象,那么调用的func函数就是B类的func函数,而多态调用下的虚函数继承又是接口继承,所以使用的val是1,那么最终的打印结果就是B→1。

总结就是这个符合多态而且而多态调用下的虚函数继承又是接口继承,继承基类的接口,所以是B→1。

C++多态_第9张图片

六. 多态的原理

首先来看一道内存对齐的题目

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1; //0-3
	char _ch;   // 3-4   最后是0-4  5  对齐到4的整数倍  8
}; 

int main()
{
	cout<

C++多态_第10张图片 这里为什么是12而不是8呢?

C++多态_第11张图片

调试看一下里面有一个vfptr指针,这个指针是虚函数表指针 

当一个类里面出现虚函数时,这个类实例化出的对象模型就会发生改变,他的类成员除变量之外,还会多一个虚表指针,这个虚表指针指向一个数组,这个数组里面存放的是类里面虚函数的地址。我们将这个数组称为虚函数表,简称虚表,指向虚函数表的指针简称为虚表指针。

 虚表指针和虚表

虚表指针是指向函数指针数组的指针,虚表指针在对象里面,对象中虚表指针在什么时候初始化的?在构造函数初始化列表

虚函数表本质是函数指针数组,虚表在代码段,虚表在编译时候产生的

虚函数和普通函数一样的,都是存在代码段的

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;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

C++多态_第12张图片

1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是另一部分是自己的成员

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

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

 动态绑定与静态绑定

动态绑定Dynamic Binding)和静态绑定Static Binding)是与多态性相关的两个重要概念,用于描述在编译时和运行时如何决定调用哪个函数或方法

1.静态绑定(Static Binding): 静态绑定是在编译时(编译阶段)确定调用哪个函数或方法的过程。在静态绑定中,编译器会根据调用表达式中的信息,确定要调用的函数,这通常发生在编译器生成目标代码时。静态绑定适用于非虚函数,普通的函数重载,以及运算符重载等。

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

class Derived : public Base {
public:
    void print() {
        cout << "Derived class" << endl;
    }
};

int main() {
    Derived d;
    Base& b = d;

    b.print(); // 静态绑定,编译时确定调用 Base::print()
}

 在上述例子中,尽管 b 引用的是 Derived 类的对象,但因为 print 函数不是虚函数,所以在编译时就已经确定了调用 Base::print()

 2.动态绑定(Dynamic Binding): 动态绑定是在运行时(运行阶段)根据对象的实际类型来决定调用哪个函数或方法的过程。这通常涉及虚函数的使用,其中基类中声明的函数被标记为 virtual,并且派生类中进行了重写。运行时会根据对象的实际类型(而不仅仅是引用或指针的类型)调用适当的函数。

class Shape {
public:
    virtual void draw() {
        cout << "Drawing a shape." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing a circle." << endl;
    }
};

int main() {
    Circle c;
    Shape& s = c;

    s.draw(); // 动态绑定,运行时根据实际对象类型调用 Circle::draw()
}

在上述例子中,由于 draw 函数被声明为虚函数,调用 s.draw() 会根据实际对象类型 Circle 调用 Circle::draw()

 总结

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

 七.常见面试题

1. 什么是多态?
多态可以细分为静态多态和动态多态,静态多态例如函数重载,通过所传参数类型的不同确定调用的具体函数,动态多态就是利用虚函数的重写使得基类指针调用虚函数时,能够达到指针指向谁就调用谁的虚函数,从而实现动态多态。、
2.什么是重载、重写(覆盖)、重定义(隐藏)?

重载:
两个函数必须在同一作用域里面,满足函数名相同,参数类型,个数,顺序不同时即为函数重载。

隐藏:
两个函数分别处于不同的作用域,只要函数名相同就构成隐藏,在访问时如果不指定基类作用域限定符,则默认调用的同名函数为派生类类域。

重写:
两个函数分别处于不同的作用域,两个函数必须都为虚函数,且需要满足三同,这样才可以构成重写。协变和析构函数算是重写的特殊情况,另外子类中的虚函数可以不加virtual关键字

 

3. 多态的实现原理?
多态的实现主要通过虚函数的重写和虚函数表来实现,当基类指针指向不同类型时,发生多态调用后会去基类指针指向的类型里面的虚表去找对应的虚函数进行调用,基类和派生类都有自己的虚表,多态调用会去虚表找对应的虚函数。
4.inline函数可以是虚函数吗
可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
5.静态成员可以是虚函数吗
不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6.构造函数可以是虚函数吗
不能,虚函数的调用依赖于虚函数表,而指向虚函数表的指针 vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
7.析构函数可以是虚函数吗?
可以,必须是虚函数。如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据,会造成内存泄漏。所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而销毁派生类对象中的所有数据。

8.对象访问普通函数快还是虚函数更快? 

首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9.虚函数表是在什么阶段生成的,存在哪的?

虚函数表在程序编译期间生成,虚表指针在构造函数的初始化列表阶段进行初始化,vs上面虚表和虚函数都存在于代码段

10.什么是抽象类?抽象类的作用?

纯虚函数所在的类称之为抽象类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系

你可能感兴趣的:(c++)