C++之多态

多态

多态的概念

多态就是具有不同的形态;同一个接口,展现出不同的形态

举个例子:动物"叫"这个行为,猫叫就是"喵喵喵",狗叫就是"汪汪汪",他们都是使用"叫"这个行为

多态的分类

1、静态多态(编译时多态):静态多态一般就是指函数重载(或函数模版),函数重载就是说在同一个作用域中,函数名相同,但是函数参数个数或者类型不同,这样就称为函数重载,函数重载是在编译时就确定执行的是哪个函数,所以是静态的

2、动态多态(运行时多态):动态多态是具备继承关系的类,不同类都有相同的成员函数,并且这个成员函数使用virtual修饰,当这时,同一个成员函数被不同的类(这个类需要是引用或者指针)调用,就会产生不同的形态。

实现动态多态的必要条件

1、在继承的体系里

2、子类必须对父类中的虚函数进行重写

3、需要通过父类的指针或引用调用重写的虚函数

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

class Father{
public:
	virtual void Test(){
		cout << "我是Father的Test()" << endl;	
	}
};
class Son :public Father{
public:
	virtual void Test() override{
		cout << "我是Son的Test()" << endl;	
	}
};
int main()
{
	//通过引用的方式
	Father f;
	Son s;
	Father& pp = f;
	f.Test();
	Father& pp_ = s;
	s.Test();
	
	//通过指针的方式
	Father* t = new Father();
	t->Test();
	delete t;
	Father* t_ = new Son();
	t_->Test();
	delete t_;

   return 0;
}

接受对象为父类的指针或者引用,传递的是父类就调用父类的函数,传递的是子类就调用子类的函数

虚函数

前面说了动态多态有个限制,成员函数需要用virtual修饰,被virtual修饰的成员函数就称为虚函数,非成员函数不能加virtual修饰。

我们知道,一个类的大小是成员变量内存对齐后的大小,但是如果这个类有虚函数,那么此时需要加上虚表指针内存对齐后的大小,因为有虚函数实例化出来的对象就会有虚表指针

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

class MyClass{
public:
	virtual void Test(){}
private:
	char c;
};

int main()
{
	MyClass m;
	cout << "m的大小为:" << sizeof(m) << endl;
   return 0;
}

override(建立在虚函数上)

1、override:一般重写父类的虚函数,那么就需要使用override修饰,子类重写虚函数不使用override也可以,但是建议使用,这样可以知道这个成员函数是否是重写父类的虚函数,如果不是的话就会报错;

父类的虚函数使用override修饰会报错,还有子类不是重写父类的虚函数加override修饰也会报错,意思就是说第一次声明定义虚函数不能添加override修饰;

override是重写的意思,第一次声明定义就不存在重写啦,故第一次声明定义虚函数不能添加override修饰

代码如下:

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

class Father{
public:
	//这样会报错
	/*
	virtual void Test() override{
		cout << "我是Father的Test()" << endl;	
	}
	*/
	virtual void Test(){
		cout << "我是Father的Test()" << endl;	
	}
};

class Son :public Father{
public:
	//可以不使用override修饰
	/*
	virtual void Test(){
		cout << "我是Son的Test()" << endl;	
	}
	*/
	//这是使用override修饰,最好这样写
	virtual void Test() override{
		cout << "我是Son的Test()" << endl;	
	}
	
	//使用override修饰的话,此时就会编译失败,因为父类没有test()虚函数
	/*
	virtual void test() override{
		cout << "我是Son的Test()" << endl;	
	}
	*/
};

int main()
{
	Father* f = new Son();
	f->Test();
	delete f;
   return 0;
}

final

1、使用在虚函数上,那么表示这个虚函数不可以被子类重写

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

class Father{
public:
	virtual void Test() final{
		cout << "我是Father的Test()" << endl;	
	}
};
class Son :public Father{
public:
	//因为父类的Test()成员函数使用final,所以下面不管哪种方式都会编译失败
	virtual void Test(){
		cout << "我是Son的Test()" << endl;	
	}
	/*
	virtual void Test() override{
		cout << "我是Son的Test()" << endl;	
	}
	*/
};
int main()
{
	Father* f = new Son();
	f->Test();
	delete f;
   return 0;
}

2、使用在类上,就表明这个类不能被继承

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

class Father final{
};
class Son :public Father{
};
int main()
{
	Father* f = new Son();
	f->Test();
	delete f;
   return 0;
}

纯虚函数与抽象类

虚函数使用 =0 修饰,那么这个虚函数是一个纯虚函数,此时,这个类是一个抽象类;抽象类不能实例化对象,想要实例话对象就需要子类继承,并且把纯虚函数都实现了才能实例化对象,否则子类也还是抽象类,也不能实例化对象

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

class Father{
public:
	virtual void Test() = 0;	//纯虚函数
};
class Son :public Father{
public:
	virtual void Test() override{
		cout << "我是Son的Test()" << endl;	
	}
};
int main()
{
	//不能实例化Father对象,报错
	//Father* f = new Father();
   
	//因为子类实现了Test(),所以可以实例化子类对象
	Father* s = new Son();
	s->Test();
	delete s;
	
	return 0;
}

虚函数表和虚表指针

一个类,里面有虚函数的话,那么这个类在编译时候就会生成一个虚函数表,这时候实例化的每一个对象都会有一个虚表指针,这个虚表指针是一个函数指针数组,里面存的是每一个虚函数的地址

一个类(存在虚函数)一个虚函数表,该类实例化的对象的虚函数表指针都指向这个虚函数表;这个虚函数表存在代码段的常量区上

子类的虚函数表包括:基类的虚函数地址、派生类重写的虚函数地址、派生类自己的虚函数地址;

从以下的代码可以看出,一个具有虚函数的类只有一个虚函数表,并且虚函数表是存在代码段的常量区

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

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

    void bar() {
        cout << "Base bar()" << endl;
    }
};

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

int main() {
    Derived obj1, obj2, obj3;

    cout << "Vtable address for obj1: " << *(void**)&obj1 << endl;
    cout << "Vtable address for obj2: " << *(void**)&obj2 << endl;
    cout << "Vtable address for obj3: " << *(void**)&obj3 << endl;
	
	Base* b1 = new Derived();
	Base* b2 = new Derived();
	Base* b3 = new Derived();
	cout << *(void**)b1 << endl;
	cout << *(void**)b2 << endl;
	cout << *(void**)b3 << endl;
	
	Derived* bb1 = new Derived();
	Derived* bb2 = new Derived();
	Derived* bb3 = new Derived();
	cout << *(void**)bb1 << endl;
	cout << *(void**)bb2 << endl;
	cout << *(void**)bb3 << endl;
	
	Base* bbb1 = new Base();
	Base* bbb2 = new Base();
	Base* bbb3 = new Base();
	cout << *(void**)bbb1 << endl;
	cout << *(void**)bbb2 << endl;
	cout << *(void**)bbb3 << endl;
	
	int i = 0;
	static int j = 0;
	int* p1 = new int;
	const char* p2 = "hello";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	
	printf("虚函数地址:%p\n", &Derived::foo);
	printf("普通函数地址:%p\n", &Base::bar);
	
	//这样也能看出虚函数表的地址
	cout << hex << *(int*)b1 << endl;
	cout << hex << *(int*)b2 << endl;
	cout << hex << *(int*)b3 << endl;

    return 0;
}

动态多态的实现方式

所以动态多态的实现方式就是先找到虚表指针,通过虚表指针找到对应的虚函数表,从虚函数表找函数指针,然后执行该函数代码。

子类继承父类就是先把父类的虚函数表里面的每一个虚函数地址拷贝下来,然后如果发生重写,那么对应的虚函数地址就会发生更改,改为子类实现的虚函数那个函数地址,从这可以知道子类的虚函数表和父类的虚函数表不是同一个,因为他们虚函数表里面的虚函数地址都不一样
这里需要注意,重写只是把虚函数的实现部分重写,但是声明还是使用父类的声明

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

class FFather{
public:
	virtual void Test(int x = 9){
		cout << "我是FFather的Test(),x = " << x << endl;	
	}
};

class Father : public FFather{
public:
	virtual void Test(int x = 666){
		cout << "我是Father的Test(),x = " << x << endl;	
	}
};

class Son :public Father{
public:
	virtual void Test(int x = 888) override{
		cout << "我是Son的Test(),x = " << x << endl;	
	}
};
int main()
{
	FFather* f = new Son();
	f->Test();
	delete f;
	
	Father* f_ = new Son();
	f_->Test();
	delete f_;
	
	Son* s = new Son();
	s->Test();
	delete s;
		
	Son ss;		//这是普通对象,使用自己的虚函数声明
	ss.Test();

   return 0;
}

静态绑定与动态绑定

对不满足多态条件的函数调用是在编译时调用函数的地址,叫做静态绑定

满足多态条件的函数调用是在运行时绑定函数调用的地址,也就是运行时到指向对象的虚函数中找到调用函数的地址,叫做动态绑定

普通函数与虚函数继承的区别

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现

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

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

class Father{
public:
	void Test(int x = 147){
		cout << "我是Father的Test(),x = " << x << endl;	
	}
	virtual void VTest(int x = 666){
		cout << "我是Father的VirtualTest(),x = " << x << endl;	
	}
};

class Son :public Father{
public:
	void Test(int x = 369){
		cout << "我是Son的Test(),x = " << x << endl;	
	}
	virtual void VTest(int x = 888){
		cout << "我是Son的VirtualTest(),x = " << x << endl;	
	}
};
int main()
{
	cout << "使用Father* f = new Son();构造的对象" << endl;
	Father* f = new Son();
	f->Test();
	f->VTest();
	delete f;
	cout << endl;
	
	cout << "使用Son* s = new Son();构造的对象" << endl;
	Son* s = new Son();
	s->Test();
	s->VTest();
	delete s;
	cout << endl;
	
	cout << "使用Father f_;构造的对象" << endl;
	Father f_;
	f_.Test();
	f_.VTest();
	cout << endl;

	cout << "使用Son s_;构造的对象" << endl;
	Son s_;
	s_.Test();
	s_.VTest();
	cout << endl;
	
	return 0;
}

多继承

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

typedef void(*VFTPTR)();
 
void PrintVFPtr(VFTPTR a[])
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("a[%d]:%p->", i, a[i]);
		VFTPTR p = a[i];
		p();
	}
	cout << endl;
}
class Base1 {
public:
	virtual void func1() { 
		cout << "Base1::func1" << endl; 
	}
	virtual void func2() { 
		cout << "Base1::func2" << endl; 
	}
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { 
		cout << "Base2::func1" << endl; 
	}
	virtual void func2() { 
		cout << "Base2::func2" << endl; 
	}
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { 
		cout << "Derive::func1" << endl; 
	}
	virtual void func3() { 
		cout << "Derive::func3" << endl; 
	}
private:
	int d1;
};
int main()
{
	Base1 b1;
	PrintVFPtr((VFTPTR*)(*(int*)&b1));
	
	Base2 b2;
	PrintVFPtr((VFTPTR*)(*(int*)&b2));

	Derive d;
	PrintVFPtr((VFTPTR*)(*(int*)&d));
	return 0;
}

class Derive : public Base1, public Base2这个继承的意思先Base1中虚函数表的虚函数地址拷贝到Derive的虚函数表上,然后再拷贝Base2中虚函数表的虚函数地址贝到Derive的虚函数表上,但是在中间发现已经存在func2()函数,于是就跳过,所以最后子类执行func2()函数时和Base1的func2()的函数地址一样;

要是把Base1与Base2调换,那么就会先拷贝Base2的虚函数地址;读者可以自行验证一下,就是把代码从class Derive : public Base1, public Base2改为class Derive : public Base2, public Base1,,看看打印

继承除了这种继承方式,还有虚继承的继承方式,就是继承时候加上virtual修饰,但是一般很少用到,这里笔者就不写了

传参拷贝的问题

使用普通类接收,实参到形参会发生一次拷贝,此时只拷贝成员变量,虚表指针不发生拷贝

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

class Base {
public:
	Base(int x) : x_(x){}
	virtual void func() { 
		cout << "Base::func" << endl; 
	}
	void Print(){
		cout << "x_ = " << x_ << endl;	
	}
private:
	int x_;
};
class Derive : public Base{
public:
	Derive(int x) : Base(x){}
	virtual void func() override{ 
		cout << "Derive::func" << endl; 
	}
};

void Test(Base b)
{
	b.func();
	b.Print();
}

int main()
{
	Base b(666);
	b.func();
	b.Print();
	Test(b);

	Derive d(888);
	d.func();
	d.Print();
	Test(d);
	return 0;
}

协变

协变是指在继承体系中,方法返回类型的变更,可以保持一定的子类型关系,即子类可以重写基类的方法,并使返回类型变得更加具体。

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

class Animal{
public:
	virtual Animal* Clone(){
		return new Animal();	
	}
	virtual void Speak(){
		cout << "我是Animal的Speak()" << endl;	
	}
};

class Dog : public Animal{
public:
	virtual Dog* Clone(){		//和基类的返回类型不一致
		return new Dog();	
	}
	virtual void Speak() override{
		cout << "我是Dog的Speak()" << endl;	
	}
};


int main()
{
	Animal* animal = new Animal();
	Animal* dog = new Dog();
	animal->Speak();
	dog->Speak();
	Animal* t1 = animal->Clone();
	Animal* t2 = dog->Clone();
	t1->Speak();
	t2->Speak();
	
	delete animal;
	delete dog;
	delete t1;
	delete t2;
   
	return 0;
}

关于多态常遇到的问题

1、构造函数可以是虚函数吗?为什么?

不可以,因为虚函数的执行过程是在对象的虚函数指针里找到虚函数表,从虚函数表再找到虚函数地址;执行虚函数前提是有对象,而构造函数是构造对象的,但此时构造函数又是虚函数,那么就执行不下去,所以说构造函数不可以是虚函数

2、析构函数可以是虚函数吗?为什么?

有继承的话,析构函数最好是虚函数,如果不是虚函数,释放对象时就不会调用子类的析构函数,此时子类的析构函数有资源释放的话就没有得到正确的释放,造成资源泄露

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

class Father{
public:
	virtual ~Father(){
		cout << "我是~Father()" << endl;	
	}
};
class Son :public Father{
public:
	virtual ~Son(){
		cout << "我是~Son()" << endl;	
	}
};
int main()
{
	//通过指针的方式
	//Father* t = new Father();
	//delete t;		//只调用父类的析构函数

	Father* t_ = new Son();
	delete t_;		//先调用子类的析构函数,再调用父类的析构函数
	
	cout << "我是分割线" << endl;

	//通过引用的方式
	//Father f;			//只调用父类的析构函数
	Son s;				//先调用子类的析构函数,再调用父类的析构函数

   return 0;
}
3、虚函数表存放在哪里?

虚函数表存放在代码段的常量区上

4、对象访问普通函数快还是虚函数更快?

如果是普通对象,那么他们是一样的快;如果是通过基类指针或引用指向的对象,那么普通函数快,因为访问虚函数还需要通过虚表指针里面的函数地址调用。

关于多态就写到这里了,如果文章有错误的地方,可以指出来,笔者很欢迎读者能够只指出来的;最后,感谢您阅读这篇文章,笔者在此祝您每天生活愉快,希望我们下次还能相遇!

相关推荐
软件黑马王子1 小时前
C#初级教程(4)——流程控制:从基础到实践
开发语言·c#
闲猫1 小时前
go orm GORM
开发语言·后端·golang
黑不溜秋的2 小时前
C++ 设计模式 - 策略模式
c++·设计模式·策略模式
李白同学3 小时前
【C语言】结构体内存对齐问题
c语言·开发语言
黑子哥呢?4 小时前
安装Bash completion解决tab不能补全问题
开发语言·bash
青龙小码农4 小时前
yum报错:bash: /usr/bin/yum: /usr/bin/python: 坏的解释器:没有那个文件或目录
开发语言·python·bash·liunx
大数据追光猿4 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法
Dream it possible!4 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode
柠石榴5 小时前
【练习】【回溯No.1】力扣 77. 组合
c++·算法·leetcode·回溯
王老师青少年编程5 小时前
【GESP C++八级考试考点详细解读】
数据结构·c++·算法·gesp·csp·信奥赛