C++学习之多态详解

目录

多态的实现

例题

重载 重写  重定义的区别

抽象类

多态实现原理


多态的实现

C++中的多态是指,当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。在C++中,通过将基类中的成员函数声明为虚函数,即可实现多态。

多态的发生是在继承的前提条件上,且要满足两个重要条件,否则都不能是多态:

1.虚函数的重写(要求三同,同函数名,同返回类型,同参数)-协变除外

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; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;//调父类析构
	//此时子类析构调用虚析构且这里的析构函数是一种重写
	delete p2;//先调子类析构释放子类自己的那一部分空间,再调父类析构,释放剩下的父类的空间
	return 0;
}

其次我们要知道,只有对于 new子类对象给父类指针时,才会需要调用虚析构,去释放除了子类本身的那一部分空间,还要释放继承父类的那一部分空间,否则会造成内存泄漏。

例题

下面用几道题检验我们的水平:

C++学习之多态详解_第1张图片

这里正确答案选择c项,对于p1它是父类指针B1指向子类对象,由于切片的原因,所以p1就是表示Base1的空间同理p2指向Base2的空间。但是p3只想自己,也就是它包含了继承的父类的空间,按照声明的顺序,p3指向Base1+Base2+Derive,首地址的话就是Base1,故选择c。

C++学习之多态详解_第2张图片

该题正确答案是B,相信大家可能都会选择D项,首先我们知道p指针是一个子类指针,但他继承了父类的成员函数,所以调用test是父类的函数,test再次调用func函数(这里的调用还是父类this调用func函数,继承父类的),由于指向的对象是子类对象,且满足函数重写,故这里会去调用子类的func,但是记住一点,子类的函数只会重写函数体,对于参数和函数名函数类型都是继承父类的,所以这里的缺省参数应该还是父类里的。

重载 重写  重定义的区别

C++学习之多态详解_第3张图片

总的来说对于继承不是重写就是重定义,函数重载参数不同(参数类型,参数个数,参数类型顺序)。

抽象类

当一个基类的成员函数不仅仅添加了virtual,并且函数体为空,如:Drive()这个函数

class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};

void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
}

这样的函数我们将它称为纯虚函数,这样的类我们叫做抽象类。抽象类不能直接实例化对象

其次关键字overried可以检查虚函数是否完成重写。

多态实现原理

那么虚函数这种是怎么实现的呢?

在此之前我们先了解一下虚函数表:

首先对于虚函数的实现,在类中会有一个虚函数表指针,我们也可以根据类的大小看到有一个指针。

C++学习之多态详解_第4张图片

那么这个虚函数表指针是干嘛用的呢?

实际上一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
虚函数表本质上是一个指针数组,存放虚函数的指针。
父类是这样,那对于继承它的子类呢?我们用如下代码观察:
class Person {
public:
	virtual void Bytiket()
	{ 
		cout << "成人买票全价" << endl; 
	}
	virtual void Job()
	{ 
		cout << "我是医生" << endl;
	}
	void habit()
	{
		cout << "打球" << endl;
	}

private:
	int _b;
};
class Student : public Person {
public:
	virtual void Bytiket() { cout << "学生买票半价" << endl; }
private:
	int _d;
};
void Func(Person &p)
{
	p.Bytiket();
 }
int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

首先对于父类,监视窗口并不能看到真正的情况,我们利用内存窗口再进行观察:

&p

C++学习之多态详解_第5张图片

 再详细的观察vfptr里面的内容

从内从中我们看到了确实和我们理解的一样,就是指针数组,里面存放着函数指针。

 此时我们再观察派生类里面的内容:

&s

vfptr

C++学习之多态详解_第6张图片

 可以看到子类中也有一个虚表指针,而且这与父类的虚函数表指针不一样,可以看到两者的虚函数指针地址都不一样,但是仔细观察里面存放的各个虚函数,可以看到第一个虚函数指针与父类的不一样,而第二个虚函数指针与父类的一样。

仔细一想我们大概就知道原因了,我们知道虚函数的重写其实是虚函数的覆盖,子类将虚函数表拷贝过来,在我们重写了某一个虚函数时,对应的虚函数指针就会被覆盖成新的,当我们不重写时,对应的虚函数地址没有发生改变,因此虚函数的重写本质上就是虚函数指针的覆盖。

完成覆盖后,当我们利用父类的指针或者引用指向子类对象,在调用时,就会调用完成覆盖后的虚函数的地址(新的虚表),此时调用的就是子类中重写的方法,这也就是我们会说调用的函数和指向的对象有关,指向子类调用子类的,指向父类调用父类的。本质就是指向某个对象的虚函数表。

那么又有一个问题我们也可以仔细想象了:为什么必须是父类对象的指针或者引用,对象就不行呢?

 了解到虚函数表的存在,我们再次思考,对象的指针或者引用那就是代表父类的这一部分的空间,指向子类对象时,中间不产生临时对象,可以当作切片剩下父类的那一部分,引用也就是直接引用那一部分。所以父类的引用与指针相当于就是子类中父类的那一部分空间,而我们用的是父类对象的话,父类对象指向子类对象,单单就是把子类中那父类的一部分给给父类但是不包括虚函数表,没有虚函数表多态就无法实现,故此必须是父类的指针或引用指向子类对象。

至于这里虚函数表不能拷贝,在设计之时就已经必须这样规定,如果虚函数指针也能被拷贝,那就全乱了,在调用时,该访问哪一个虚表?

你可能感兴趣的:(c++,开发语言)