C++ | 多态

目录

前言

一、多态的概念

二、多态的定义与使用

1、多态的构成条件

2、虚函数

3、虚函数的重写(覆盖)

4、虚函数重写的两个例外

(1)协变

(2)析构函数的重写

5、子类的指针或者引用调用

6、C++11的override与final关键字

7、重载、重定义(隐藏)、重写(覆盖)之间的对比 

三、抽象类

四、多态的原理

1、虚函数表

2、虚函数表的打印

3、多态的原理 

4、静态绑定与动态绑定

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

1、单继承中的虚函数表

2、多继承中的虚函数表


前言

        我们都知道类的三大特性分别为类的封装、继承与多态;前面我们介绍过了类的封装与继承,本章主要介绍类的多态这一性质,其实类的多态这一性质基于类的继承,可以说没有类的继承也就没有类的多态;

一、多态的概念

        所谓多态即多种形态,指不同的对象取调用一个函数(看起来像一个函数)会产生多种不同的效果;举个例子,在我们日常通勤中,我们可能会乘坐公共汽车、高铁等等交通工具;但是当不同人去完成买票这一动作时,会产生不同的效果;学生去买票会以学生票的价格卖给学生,成人去买票会以成人的价格卖给成人,我们今天学的多态也是如此;

二、多态的定义与使用

1、多态的构成条件

我们想构成一个多态,必须有以下条件(缺一不可);

1、虚函数的重写

2、必须通过基类的指针或者引用调用虚函数

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

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

void func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student stu;
	func(p);
	func(stu);
	return 0;
}

        上述代码实现了多态,该代码仅仅只是为了让大家看看多态,下面会介绍相关概念;

2、虚函数

        虚函数即为用关键字virtual修饰的成员函数;如上面的BuyTicket函数便是虚函数;

class A
{
public:
	virtual void func() {}; // 虚函数
};

3、虚函数的重写(覆盖)

        虚函数的重写,也叫覆盖,是构成多态的条件之一,子类中有一个与父类完全相同的虚函数,我们就称子类对该虚函数进行了重写(覆盖);这里的完全相同指的是返回值、函数参数、函数名相同,其中函数的参数只要参数类型相同即可,形参名可以不相同;

class A
{
public:
	// 虚函数
	virtual void func(int a1 = 1, double d1 = 2.0) 
	{
		cout << "A: " << endl;
		cout << a1 << ": " << d1 << endl;
	}; 
};

class B : public A
{
public:
	// 虚函数
	virtual void func(int a2 = 10, double d2 = 20.0)
	{
		cout << "B: " << endl;
		cout << a2 << ": " << d2 << endl;

	};
};

void Func(A* a)
{
	a->func();
}

int main()
{
	A a;
	B b;
	Func(&a);
	Func(&b);
	return 0;
}

        仔细观察上图与代码,我们发现虚函数的重写仅仅只是对实现进行了重写,我们父类的函数缺省值为1和2.0,而子类函数的缺省值为10和20.0,可打印出来的确实1和2,说明了虚函数的重写仅仅只是对实现部分进行了重写;

4、虚函数重写的两个例外

        上面我们说过,要实现虚函数的重写必须实现三同,可是,这其实也有特殊例外的语法;以下分别一一介绍;

(1)协变

        重写的虚函数可以返回值不同,但是他们的返回值必须为父子类关系的指针或引用;这种重写虚函数我们称为协变;如下所示;

class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
		return *this;
	}
};

class Student : public Person
{
public:
	virtual Student& BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
		return *this;
	}
};

void func(Person& p)
{
	p.BuyTicket();
	cout << endl;
}


int main()
{
	Person p;
	Student stu;

	func(p);

	func(stu);

	return 0;
}

其中返回父子类的指针和引用并非必须返回本类的父子类,还可以返回别的父子类;

class A
{
public:

};
class B : public A
{
public:

};
class Student;
class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
		A* p = new A;
		return p;
	}
};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
		B* p = new B;
		return p;
	}
};
void func(Person& p)
{
	p.BuyTicket();
	cout << endl;
}
int main()
{
	Person p;
	Student stu;

	func(p);

	func(stu);

	return 0;
}

        其中A、B类与Person、Student类并无关系,可是用他们作为返回值时,也可构成协变;其中父类写道父类虚函数的返回值中,子类写到子类虚函数的返回值中;

(2)析构函数的重写

        析构函数的重写可以不用不用相同的名字,实际上,底层还是会将其改成相同的名字---destructor,只不过我们看着好像不同的函数名实现了多态;

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

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

void Func(A* p)
{
	// 如果传入一个子类指针,如果没有多态根本不能完全释放
	delete p;
}

int main()
{
	A* ptra = new A;
	B* ptrb = new B;

	Func(ptra);
	Func(ptrb);

	return 0;
}

注意:有一个小细节,父类的虚函数必须写virtual关键字,子类可以不用写virtual,直接对其重写; 

5、子类的指针或者引用调用

        在上述所有代码中,我们实现多态的第二个条件就是子类的指针或者引用来调用虚函数,这是实现多态的必要条件;上述代码均有体现;

6、C++11的override与final关键字

在C++11中,新加了这两个关键字;

1、final用于修饰该虚函数不能在被重写了;

class A
{
public:
	virtual void func()final { }
 };

class B : public A
{
public:
	virtual void func() {};
};

2、override关键字用于检查子类中的某个虚函数是否被重写;

class A
{
public:
	virtual void func(int x) { }
 };

class B : public A
{
public:
	virtual void func(double x)override  {};
};

7、重载、重定义(隐藏)、重写(覆盖)之间的对比 

这三个是我们之前学过的概念,很容易混淆,此处对其一一进行对比;

重载:

        两个函数必须在同一个作用域中,且函数名相同,参数不同,底层使用函数名修饰规则实现;

重定义:

        两个函数作用域必须分别在派生类与基类中,函数名相同即可;

重写:

        两个函数作用域必须分别在派生类与基类中,函数名、参数、返回值都必须相同(除了那两个特例除外),两个函数必须是虚函数(基类必须加virtual关键字);

三、抽象类

        语法上,我们规定一个只声明,不实现的虚函数,并且后加= 0,这个函数我们就称作纯虚函数,包含纯虚函数的类被称为抽象类;抽象类不能实例化出对象,继承后的派生类也不可实例化出对象,除非我们在派生类中对纯虚函数进行重写;

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

class Derive : public Base
{
	// 重写纯虚函数
	//virtual void func(){}
};

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

        但在测试中我们发现纯虚函数可以定义自己的函数体,但是没有意义;纯虚函数存在的目的就是让派生类进行重写;

四、多态的原理

1、虚函数表

首先,给大家一道面试题,以下代码结果是多少;

// 32位机器下
class Base
{
public:
	virtual void func()
	{}
	int _a;
};

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

        结果是否超出你的预料呢? 当我们将一个函数声明为虚函数时,该类内会多出一个指针,这个指针被称为虚函数表指针,简称虚表指针;因此结果为8字节;

         我们再通过内存窗口来观察这个类,如下所示;

        虚函数表实际上是一个函数指针数组,里面储存的是虚函数指针,在VS系列编译器中通常以 nullptr 结尾;后面我们打印虚表也是通过这一特性进行打印;我们接着加入继承,继续观察;

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

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

int main()
{
	Base b;

	Derive d;

	return 0;
}

        上述代码,我们新增了一个派生类,派生类对第一个虚函数进行了重写,然后在基类上增加了一个虚函数,一个普通成语函数;

        首先,通过监视窗口,我们发现派生类确实继承了父类的虚函数表,但是我们仔细观察发现虚函数表的地址不同,表中存放的虚函数地址也不同,其中,第一个虚函数我们对其进行了重写,因此第一个函数指针的地址不同,第二个虚函数我们并未对其进行重写,因此地址相同,而父类除了增加虚函数外,还增加了一个普通成员函数,普通成员函数并没存进虚函数表中;

总结一下:

        关于虚函数表(也称虚表),虚表也会被子类继承,只不过虚表被继承是子类拷贝父类的虚表,然后判断子类是否对其中某个虚函数进行重写,若重写,则用新的函数地址覆盖在原来虚表上的地址,若派生类新增了虚函数,也会继续依次填充在虚表后,虚表只会存放虚函数的地址;

        还有一些问题,我带着大家一起验证,关于虚表存在哪里、虚函数又存在哪里?有许多小伙伴对其充满疑惑;

        首先解答,虚表存在哪里?

// 该测试代码仅仅限于32位机器下
class Base
{
public:
	virtual void func1()
	{
		cout << "Base func1" << endl;
	}
	virtual void func2() 
	{
		cout << "Base func2" << endl;
	}
	void func3() { }
	int _a = 1;
};

class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive func1" << endl;
	}
	int _b = 2;
};

int main()
{
	Base ba;
	Derive d;

	int a = 10;
	int* pa = new int;
	static int b = 20;
	const char* c = "xxxxxxxxxxxxxxxxxx";

	printf("栈区: %p\n", &a);
	printf("堆区: %p\n", pa);
	printf("静态区: %p\n", &b);
	printf("常量区: %p\n", c);
	printf("父类对象虚表地址: %p\n", *(int*)&ba);
	printf("子类对象虚表虚表地址: %p\n", *(int*)&d);

	return 0;
}

        不难看出,虚表的地址更接近与常量区,因此不难推测虚表存放在常量区,当然,往上也有一部分人说虚表存在静态区;

 虚函数又存放在哪呢?

        与大部分函数一样,虚函数也是存在代码段中的,此处补充一个小知识,代码段中存的并不是我们写的代码,而是二进制机器代码,我们写的代码首先转换成汇编代码,然后再由汇编代码转换成二进制机器代码;

2、虚函数表的打印

        前面我们也说了,在VS系类的编译器下,虚函数表的最后一位会补上 nullptr ,因此我们可通过这一特性打印我们的虚函数表,还是上面那个类,此处就不重复了;

// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("table[%d]: %p ->", i, table[i]);
		Vf_ptr f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	Base* pa = &b;
	Derive* pd = &d;
	// 写法一
	PrintVf_ptr(*(Vf_ptr**)pa);
	PrintVf_ptr(*(Vf_ptr**)pd);
	// 写法二 只是用于32位机器
	//PrintVf_ptr((Vf_ptr*)*(int*)pa);
	//PrintVf_ptr((Vf_ptr*)*(int*)pd);
	return 0;
}

3、多态的原理 

        前面说了这么多,也只是为多态的原理进行铺垫;不止小伙伴们是否记得构成多态的基本条件,其中有一个是必须用父类的指针调用,那么为什么必须用父类的指针进行调用呢?我们假设不用父类的指针或引用,我们就使用值传递;拿以下类讲解;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person BuyTicket" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student BuyTicket" << endl;
	}
};
// 这里使用值传递
void func(Person p)
{
	p.BuyTicket();
	cout << endl;
}

int main()
{
	Person p;
	Student stu;
	func(p);
	func(stu);
	return 0;
}

        如果是值传递这里必然涉及拷贝的问题,那么虚表指针拷贝么,如果是拷贝,那么是拷贝父类虚表指针还是拷贝子类的虚表指针呢?这是不确定的,因为我们在调用这个函数里,不知道将来会被父类调用,还是子类调用,因此无法确定拷贝哪个虚表指针,因此必须要用父类的指针或者引用;而不能用切片直接传值;

        那么是如何实现的呢?当我们传指针或者引用时,加入原对象是父类对象,则直接传,若是子类对象,则会发生我们之前讲过的赋值兼容(切片);

        如果传过去的是父类,则类中存的是父类的虚表指针,而传过去的是子类,类中存的是子类的虚表指针;所以有了如下调用逻辑;

4、静态绑定与动态绑定

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

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

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

1、单继承中的虚函数表

        单继承中,父类的虚函数放在父类的虚函数表中,子类重写于父类的虚函数放在子类的虚函数表中,子类定义的虚函数也放在子类的虚函数表中;(放在理解为其指针存在虚函数表中);

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2" << endl;
	}
	int _a = 1;
};

class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
	virtual void func3()
	{
		cout << "A::func1" << endl;
	}
	int _b = 2;
};

int main()
{
	A a;

	B b;
	return 0;
}

        父类的虚函数表中存放了func1与func2地址,而子类虚函数表中,存放了重写的func1,因此地址不同;以及继承父类没有重写的func2,因此地址相同;还有在子类定义的虚函数func3;

2、多继承中的虚函数表

        在多继承的体系下,又是如何继承的呢?首先我们好奇的是,多继承体系下,会有几张虚表呢?即派生类会有几个虚表指针呢?我们做了如下测试;

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

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

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

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

        通过监视窗口不难看出,d类中有两个虚表指针,意味着有两种张虚表;那么问题又来了,我们在派生类定义的虚函数func3存在先继承的Base1中的虚表,还是存在Base2中的虚表呢?我们使用前面的虚表打印的代码,进行测试;结果如下;

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

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

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

// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("table[%d]: %p ->", i, table[i]);
		Vf_ptr f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	// 打印Base1虚表
	PrintVf_ptr(*(Vf_ptr**)&d);
	// 打印Base2虚表
	Base2* ptr = &d; // 切片
	PrintVf_ptr(*(Vf_ptr**)ptr);
	return 0;
}

        经过测试,我们发现我们在Derive新定义的虚函数,存在了先继承的Base1的虚表中,仔细观察的小伙伴们注意到了,Derive对func2没有进行重写,因此,我们继承的两张虚表的func2函数的地址不同可以理解,因为是两个不同的函数,可是为什么重写的func1的地址也不同呢?我们可以看到,他们明明是调用的同一个函数(—>后面是函数执行结果),那为什么地址不同呢??

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
    // 分别调用两个虚函数表中重写的func1
	ptr1->func1();
	ptr2->func1();

}

        关于调用同一个函数,这个函数却有两个地址的问题,我们还得观察汇编代码;接下来我带着大家一起观察我们程序的汇编代码;(call为汇编指令中的函数调用指令,后面接函数地址,而jmp为跳转指令,后接地址)

接着我们看Base2中的func1是在汇编代码中是如何调用的;

        重新捋一下思路,在多重继承下,子类继承了两个及以上的来自父类的虚表,我们重写来自父类的虚函数(且这个虚函数多个父类都有)时,我们会对其重写,重写的虚函数只有一个,两个虚表中,各保存一份,上述问题讨论的是,为什么同一个虚函数地址不同;

        观察汇编代码,我们发现,保存在Base2中的func1会多经过几次跳转;但最终还是会来到最终的函数入口地址;完成调用;其实与this指针有关;当我们分别用Base1*与Base2*类型的指针调用func1时,模型图如下所示;

        当我们用Base2*调用func1时,其中有一个动作时减8,其实那个动作正是调整this指针的指向; 由于Base1*的指向本来就是子类Derive的开始,因此不用调整,所以两个虚函数表中的func1地址不同的本质原因是,Base2*调用fucn1时还需要调用一个调整this指针的动作;

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值