C++三大特性---多态

Posted 可乐不解渴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++三大特性---多态相关的知识,希望对你有一定的参考价值。

对花对酒,落梅成愁,十里长亭水悠悠。

多态的概念

多态是在不同对象 ,去调用同一个名字的函数,产生了不同的行为。

例如:在现实生活中我们去游乐园玩都需要买票才能进入。而我们共同的属性都是人,并且由于我是学生,我买学生票,但如果我是成人,所以我买成人票。针对不同身份的人去买票,所产生的行为是不同的,这就是所谓的多态。

多态的定义及实现

多态分为静态多态和动态多态两种。
静态多态有两种:
1、函数重载:包括普通函数的重载和成员函数的重载。
2、函数模板的使用。
而动态多态需要满足一些条件才能实现。

动态多态的构成条件

前提是一定是在继承的体系中。
在继承中要构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数 。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(不重写则调用的还是基类的虚函数)。

虚函数

什么是虚函数?
答:简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。

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

在上面的代码中func1和func2两个函数就是虚函数。

注意:

1、构造函数不能加virtual。因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
2、静态成员不可以是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3、这里的虚函数和纯虚函数与前面的虚继承弄混淆,它们之间没有任何关系,它们只是使用了同一个关键字virtual。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

虚函数的重写

虚函数的重写(也被称为覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。但也有例外:如协变与析构函数。

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

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

在这里我们就可以利用父类(Base)指针或者引用来调用虚函数,此时根据父类的指针或者引用接收的类型不一样,调用的就是不同对象的虚函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

void func(Base&ref)
{
	//通过父类的引用来接收子类或者父类的对象来调用虚函数
	ref.func1();	
}

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

虚函数重写的两个例外

1、析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,但我们在上篇继承篇中我们说过,编译器会将所有的析构函数名称变destructor。那么为什么要统一名称呢?
由于当一个父类指针new的是一个子类的对象,然后销毁这个new出来的对象必须要调用到父类的析构函数和子类的析构函数,如果不实现多态,就会导致只会调用父类的析构函数来析构父类的部分数据,而子类的数据并没有被释放,当这个类中有指针变量时,会导致内存泄漏发生。所以为了能让数据正确释放,必须给析构函数变为虚析构函数来构造多态,而构成多态的前提是在继承体系中函数名、参数、返回值要相同,所以这是为什么要将所有的析构函数名称统一改为destructor的原因。
不是虚析构函数

虚析构函数

2、协变(基类与派生类虚函数返回值类型不同
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用(这里返回的指针或者引用可以是其他的继承体系中,也可以是自己的继承体系中),派生类虚函数返回派生类对象的指针或者引用时,称为协变。

//返回本继承体系中,本类的指针或引用
class Base1 
{
public:
	virtual Base1* func1() 
	{ 
		cout << "Base1::func1" << endl; 
		return this;
	}
private:
	int a;
};

class Derive1 :public Base1 
{
public:
	virtual Derive1* func1() 
	{ 
		cout << "Derive::func1" << endl; 
		return this;
	}
private:
	int b;
};

//返回其他继承体系中的指针或者引用
class Base2 
{
public:
	virtual Base1* func1() 
	{ 
		cout << "Base1::func1" << endl; 
		return this;
	}
private:
	int a;
};

class Derive2 :public Base2 
{
public:
	virtual Derive1* func1() 
	{ 
		cout << "Derive::func1" << endl; 
		return this;
	}
private:
	int b;
};

重载、覆盖(重写)、隐藏(重定义)概念的对比

抽象类

什么是抽象类?
在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象

class Person 
{
public:
    virtual void func()=0;
};

int main()
{
	//由于该类有纯虚函数,无法实例化对象。
	//Person p; error
	return 0;
}

并且派生类继承后也不能实例化出对象,只有当派生类重写该纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Person 
{
public:
    virtual void func()=0;
};
class Student : public Person 
{
public:
};

class Teacher : public Person
{
public:
    virtual void func()
    {
        cout << "I am Teacher" << endl;
    }
};
int main()
{
    //Student s;	 //error 由于没有重写继承父类的纯虚函数无法实例化对象
    Teacher t;
    return 0;
}

抽象类的作用:抽象类可以更好的用来表示现实世界中没有具体实例对应的抽象类型。

多态的原理

虚函数表

在讲解多态的原理之前我们先来看一下下面这道面试题。

class Person 
{
public:
    virtual void func()
    {}
    int m_a;
};

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

通过观察测试我们发现在32位系统下Person的大小是8个字节(64位系统下是16字节)具体怎么算可以看往期结构体内存对齐博客

除了m_a成员,还多一个vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function,ptr代表pointer)。一个含有虚函数的类中都至少都有一个虚函数表指针,虚函数表也简称虚表。

那我们知道了有虚函数的类创建的对象至少有一个虚函数指针,然后该指针会去指向本类的虚函数表,那么我虚函数表中存储的是什么呢?
答:虚函数表中存储的是虚函数的地址。

针对上面的代码我们进行一些改造。
我们用Person派生出两个类,分别是学生类、老师类,该学生类不去重写父类的虚函数,其内部增加一个普通函数func1。在老师类内部重写了父类的虚函数。

class Person {
public:
    virtual void func(){
        cout << "I am Person::func()" << endl;
    }
};
class Student : public Person {
public:
    void func1() {cout << "I am Student ::func1()" << endl;};
};
class Teacher : public Person{
public:
    virtual void func(){
        cout << "I am Teacher::func()" << endl;
    }
};
int main()
{
    Person p;
    Student s;
    Teacher t;
    return 0;
}

在父类的对象中,我们通过调试会发现在其对象内部有一个虚函数指针,指向的就是Person::func()

而在Student中,由于Student中并没有去重写父类的函数,所以Student内部的虚函数指针指向的内容其实是从父类中继承下来的。
在Teacher中重写了父类的虚函数,所以子类重写的虚函数的地址会覆盖掉父类的虚函数地址的位置。此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

派生类的虚表生成步骤如下:
1、首先将基类中的虚表内容拷贝一份到派生类的虚表。
2、如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

多态的原理

在上面我们简单的说明了拥有虚函数的类中至少会有一个指针变量,该指针变量会指向一个本类对应的虚函数表,该表中存储的是虚函数的地址。那么多态的原理到底是什么呢?
例如下面这段代码

class Person {
public:
    virtual void func(){
        cout << "I am Person::func()" << endl;
    }
    int p;
};
class Student : public Person {
public:
    virtual void func() {
        cout << "I am Student::func()" << endl;
    }
    int s;
};

void test(Person& obj)
{
    obj.func();
}
int main()
{
    Person P;
    test(P);
    Student S;
    test(S);
    return 0;
}

通过调试可以发现,对象p中包含一个虚表指针和一个成员变量,对象s中包含一个虚表指针和两个成员变量,这两个对象当中的虚表指针分别指向自己的虚表。


1、根据上图一我们便可以分析出obj是指向P对象时,obj.func在P的虚表中找到虚函数是Person::func()。
2. 观察上图二我们看到,obj是指向S对象时,obj.func在S的虚表中找到虚函数是Student::func()。

那么虚表是在什么阶段初始化呢,并且存储在那呢?虚函数存储在那呢?
答:虚表和虚函数指针是在构造函数初始化列表阶段才初始化的。并且虚表和虚函数都是存储在常量区(代码段)。

如果小伙伴还没看懂可以在评论区留言,我会在评论区给你解答!
如有错误之处还请各位指出!!!
那本篇文章就到这里啦,下次再见啦!

以上是关于C++三大特性---多态的主要内容,如果未能解决你的问题,请参考以下文章

C++三大特性---多态

C++三大特性---多态

C++ 面向对象程序三大特性之 多态

[C++]面向对象语言三大特性--多态

[C++]面向对象语言三大特性--多态

C++三大特性