C++多态

多态

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

一、多态定义及实现

1.多态定义条件

1.多态是在不同继承关系的类对象,去调用同一函数
2.必须通过基类的指针或者引用调用虚函数
3.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class Person 
{
public:
	virtual void BuyTicket() 
	{
		cout << "Person全价票" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student半价学生票" << endl;
	}
};

void Func(Person& people) 
{
	people.BuyTicket();
}
int main() 
{
	Person father;
	Func(father);
	Student child;
	Func(child);
	return 0;
}

 2.虚函数

被virtual修饰的函数叫做虚函数

3.虚函数重写

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

 

 必须满足virtual修饰,返回值相同,函数名相同,参数列表也相同,才能完成重写

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

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { 
		cout << "A";
		return new A; }
};
class Student : public Person {
public:
	virtual B* f() { 
		cout << "B";
		return new B; }
};

void Func(Person& people) 
{
	people.f();
}
int main() 
{
	Student s;
	Person p;
	Func(s);
	Func(p);
	return 0;
}

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

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

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

class Student : public Person
{
public:
	virtual ~Student() 
	{
		cout << "~Student()"<<endl;
	}
};
int main() 
{
	Student* s = new Student();
	Person* p = new Person();

	delete s;
	delete p;

	return 0;
}

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能
构成多态,才能保证sp指向的对象正确的调用析构函数。

5.C++11中的override和final——帮助我们查看是否重写

(1)final—— 修饰虚函数,表示该虚函数不能再被继承

 直接就查出来我重写了函数

(2)override
重写了并没有报错

 破坏重写中的其中一项,则会报错,节省了时间,在写代码的时间就查出来了

 二、抽象类

1.什么是抽象类

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

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生半价票" << endl;
	}
};

class Teacher :public Person 
{
public:
	virtual void BuyTicket() 
	{
		cout << "老师八折票" << endl;
	}
};

 

 Person是一个虚基类,并不能建立对象,相当于一个接口类,其他的类只要对其函数进行重写即可构成多态

2.接口继承和实现继承的区别

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

3.多态的原理

1.虚函数表

以下代码段的sizeof是多少?x86平台

class Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "Person()";
	}
private:
	int _num;
};

答案是8byte,64位的是16byte,明明我们只有一个int类型啊,怎么会是8个字节呢?

 通过监视窗口,我们可以看到,p对象中不止一个_num,还有一个_vfptr,很明显_vfptr是一个指针,它指向的是一个数组,数组的第0个是一个Person::BuyTicket,说明数组是一个函数指针数组,_vfptr指向的这个数组我们称为虚基表

那么继承该类的类也有虚基表吗?

我们稍微改一下代码,再增加一个虚函数,一个正常的函数

class Person 
{
public:
	virtual void Func1()
	{
		cout << "Func1"<<endl;
	}
	virtual void Func2() 
	{
		cout << "Func2" << endl;
	}
	void Func3() 
	{
		cout << "Func3" << endl;
	}
private:
	int _num;
};

class Student : public Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}
private:
	int _s;
};

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

 如上图所示,派生类是会继承基类的虚基表的,但是如果我们对虚函数重写后会发现虚基表的指针和更改的虚函数的函数指针与基类不同,说明如果重写虚函数,继承下来的虚基表指针会发生改变,并且其中存的虚函数数组,被重写的虚函数的指针也会改变

总结:

1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类Person 对象和派生类Student 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数的覆盖。重写是语法的 叫法,覆盖是原理层的叫法。
3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个 nullptr
5. 总结一下派生类的虚表生成: a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个很容易混淆的问题: 虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的 。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指 针,不是虚函数 ,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外 对象中存的不是虚表,存的是虚表指针。

2.多态的原理

通过上面那么一大堆的分析,我们可以再次看最上面第一段代码分析

1. 观察下图的红色箭头我们看到, p 是指向 mike 对象时, p->BuyTicket mike 的虚表中找到虚函数是Person::BuyTicket
2. 观察下图的蓝色箭头我们看到, p 是指向 johnson 对象时, p->BuyTicket johson 的虚表中找到虚函数是 Student::BuyTicket
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
5.看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到 对象的中取找的。不满足多态的函数调用时编译时确认好的

 3.静态绑定和动态绑定

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

 4.单继承虚函数表

 从上图看,派生类继承了基类,并且对Func1重写了,所以Fun1两个地址是不同的,那么派生类的Func3和Func4呢?其实是被优化隐藏了,我们用一点点特殊的手法让他出来吧。

class Person 
{
public:
	virtual void Func1()
	{
		cout << "Person::Func1"<<endl;
	}
	virtual void Func2() 
	{
		cout << "Person::Func2" << endl;
	}
private:
	int _num;
};

class Student : public Person
{
public:
	virtual void Func1()
	{
		cout << "Student::Func1" << endl;
	}
	virtual void Func3()
	{
		cout << "Student::Func3" << endl;
	}
	virtual void Func4() 
	{
		cout << "Student::Func4" << endl;
	}
private:
	int _s;
};
typedef void(*VFPTR)();//定义一个函数指针 类型叫做 VFPTR
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main() 
{
	Person p;
	Student s;
	VFPTR* vTable1 = (VFPTR*)(*(int*)&p);
	VFPTR* vTable2 = (VFPTR*)(*(int*)&s);
	PrintVTable(vTable1);
	PrintVTable(vTable2);
	return 0;
}

 

 派生类也有自己的虚函数表,继承基类的表如果没有重写则不会变,如果重写则会发生覆盖

5.多继承:菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值