【1++的C++进阶】之多态

作者主页:进击的1++
专栏链接:【1++的C++进阶】

文章目录

  • 一,什么是多态?
  • 二,剖析多态的调用原理
  • 三,抽象类
  • 四,多继承中的虚函数表

一,什么是多态?

多态的定义:不同继承关系的类对象,去调用同一个函数,产生不同的行为。再说通俗点就是:一个行为,不同的对象去做会产生不同的结果。

构成多态的条件:

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写的条件:

  1. 是虚函数 (被virtual修饰的成员函数)
  2. 三同(函数名,参数,返回值)

特例:

  1. 子类虚函数不加virtual依旧构成重写。
  2. 重写的协变:返回值可以不同,但必须是父子关系的指针或引用
  3. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
    都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

而且不符合重写就是隐藏关系,这也是我们判断隐藏关系的条件之一。
2. 必须是基类的指针或引用去调用虚函数。

实例如下:

class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
protected:
	int _a;
};

class B:public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}
protected:
	int _b;
};
void Test1()
{
	B t1;
	A& a1 = t1;
	a1.Func1();
	A* ptra = &t1;
	ptra->Func1();
}

【1++的C++进阶】之多态_第1张图片

上面还遗留一个问题,为什么要把析构函数重写。

【1++的C++进阶】之多态_第2张图片
来看以下代码:

class A
{
public:

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

	A& operator=(const A& a)
	{
		cout << "A::operator=()" << endl;
		return *this;
	}
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
protected:
	int _a;
};

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

	/*B& operator=(const B& a)
	{
		A::operator=(*this);
		cout << "B::operator=()" << endl;
		return *this;
	}*/
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}
protected:
	int _b;
};
void Test1()
{
	A* ptra1 = new A;
	A* ptra2 = new B;
	delete ptra1;
	delete ptra2;	
}

若析构函数没有重写:
【1++的C++进阶】之多态_第3张图片
我们可以看到new的B类类型的对象没有调用自己的析构而是其指针类的析构函数。这就会存在空间没有释放的问题,造成内存泄漏。

若构成重写后:
【1++的C++进阶】之多态_第4张图片
我们可以看到new出的B对象调用了自己的析构函数。并且上一篇文章我们讲过,子类对象的析构会先调用自己的构造函数,然后调用父类的构造函数。

这里我们做一个小的总结:通过上述的两段代码,我们会发现,多态调用重写函数,指向哪个对象就去调用哪个对象的重写函数。

二,剖析多态的调用原理

首先我们先来回答这么一个问题:

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

	virtual int Func2()
	{
		cout << "A::Func2()" << endl;
		return 0;
	}
protected:
	int _a;
};

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

protected:
	int _b;
};

void Test2()
{
	A t2;
	B t1;
	cout << sizeof(t1) << endl;

}

上述代码中的sizeof的值为多少呢?
【1++的C++进阶】之多态_第5张图片
答案为12!!可能会有人疑惑,为什么不是8呢?
我们通过监视窗口来观察。
【1++的C++进阶】之多态_第6张图片
我们发现,除了子类的成员变量和继承的A的成员变量外还多了一个_vfptr。这是虚函数表指针。
那么这个表中放的是什么呢?
我们继续来看下面这段代码:

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

	virtual int Func2()
	{
		cout << "A::Func2()" << endl;
		return 0;
	}

	virtual int Func4()
	{
		cout << "A::Func4()" << endl;
		return 0;
	}
protected:
	int _a;
};

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

	virtual int Func3()
	{
		cout << "B::Func3()" << endl;
		return 0;

	}

	virtual int Func4()
	{
		cout << "B::Func4()" << endl;
		return 0;

	}
protected:
	int _b;
};



typedef void(*Vfptr)();

void PrintVFptr(Vfptr* arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		printf("vfptr[%d]->%p  ", i, arr[i]);
		Vfptr pf = arr[i];
		pf();
	}
}

void Test2()
{
	A t2;
	B t1;
	printf("B::vfptr\n");
	PrintVFptr((Vfptr*)*(int*)(&t1));
	printf("A::vfptr\n");
	PrintVFptr((Vfptr*)*(int*)(&t2));


}

【1++的C++进阶】之多态_第7张图片
【1++的C++进阶】之多态_第8张图片
再解读前,我们先来说明一个东西,只要是该类的虚函数,就会被存入该类的虚函数表中,并且对于单继承来说,每个类只有一份虚函数表,子类继承了父类的虚函数表,并且将重写的虚函数覆盖为自己的。也就是说子类的虚函数表是:继承父类的并进行重写覆盖后+自己的虚函数。

我们回来来解读上结果:Func1,Func4都进行了重写,所以我们发现其A与B打印出的函数地址不同。而Func2是通过继承下来的虚函数,但并没有进行重写,会存在虚表中,因此其在A和B的虚表中的函数指针相同。Func3则是子类中独有的虚函数,因此只在子类的需表中有。

所以多态的原理是:在编译阶段会形成虚函数表,在调用构造函数的初始化列表阶段会对虚函数表进行初始化。当程序运行后,在指向对象的虚函数表中去找对应的虚函数,这也是为什么我们前面说指向谁就调用谁,而对于普通函数来说,其在编译阶段就已经确定了调用谁。
【1++的C++进阶】之多态_第9张图片

动态绑定与静态绑定

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

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

三,抽象类

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

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

我们来看一道例题:

class A
 {
 public:
   virtual void func(int val = 1)
   { 
   		std::cout<<"A->"<< val <<std::endl;
   }

   virtual void test(){ func();}
 };
 
 class B : public A
 {
 public:
   void func(int val=0)
   { 
   		std::cout<<"B->"<< val <<std::endl; 
   }
 };
 
 int main(int argc ,char* argv[])
 {
   B*p = new B;
   p->test();
   return 0;
 }

问这道题的输出是什么?
【1++的C++进阶】之多态_第10张图片

答案是:B->1
这是为什么呢?
首先我们的我们用一个指向B对象的指针p去调用test函数,而test函数是父类中的虚函数,没有重写,会继承到子类B中,这时我们会忽略一个问题—this指针,test中的this指针是A类类型的指针,这符合构成多态的一个条件:父类指针或引用去调用虚函数。 我们将B*指针传过去后会发生切片,在test中调用func(),由于虚函数的继承是几口继承,因此其会继承父类的接口,用子类的实现。感觉像头和身子拼接起来的一样。因此,在这道题中会用到父类中func函数的缺省参数,而实现部分则用的是子类中的func。所以答案为:B->1。

四,多继承中的虚函数表

以以下代码为例:

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

protected:
	int _a;

};

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

	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}

protected:
	int _b;

};

class Boss:public Base1,public Base2
{
public:
	virtual void func1()
	{
		cout << "Boos::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Boos::func2" << endl;
	}

	virtual void func3()
	{
		cout << "Boos::func3" << endl;
	}

protected:
	int _c;

};

typedef void(*Vfptr)();

void PrintVFptr(Vfptr* arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		printf("vfptr[%d]->%p  ", i, arr[i]);
		Vfptr pf = arr[i];
		pf();
	}
}

int main()
{
	Base1 b1;
	Base2 b2;
	Boss d;
	cout << "Base1" << endl;
	PrintVFptr((Vfptr*)*(int*)(&b1));
	cout << "Base2" << endl;
	PrintVFptr((Vfptr*)*(int*)(&b2));
	cout << "Boos--1" << endl;
	PrintVFptr((Vfptr*)*(int*)(&d));
	cout << "Boos--2" << endl;
	PrintVFptr((Vfptr*)(*(int*)((char*)&d + sizeof(Base1))));

}

【1++的C++进阶】之多态_第11张图片
【1++的C++进阶】之多态_第12张图片
通过上述窗口和打印出的结果看,我们发现多继承其有n个虚函数表,n与继承的父类个数有关。并且,子类自身的虚函数放在第一个虚表中,如上述func3()。

还有一个有趣的现象:

Base1 b1;
	Base2 b2;
	Boss d;

	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();

【1++的C++进阶】之多态_第13张图片
若我们用子类的地址赋值给不同的父类指针去调用func1()按理来说,其两个指针都指向同一个子类对象,并且func1()都进行了重写,因此ptr1,ptr2 , Boos虚表中的func1()的地址应该是一样的,但是结果却不一样,这是为什么呢?
下面是d对象的模型:

【1++的C++进阶】之多态_第14张图片
因此当ptr2去调用func1()时,其传过去的this指针,并不是d的起始地址,因此为了使其this指针变为d的this 指针,这里编译器会进行一个操作,计算出Base1的大小,将ptr2减去Base1的大小,这时this指针就指向了d的起始位置,自然调用的就是d对象中的func1()。

补充两个关键字:

  1. final:修饰虚函数,表示该虚函数不能再被重写。
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

【1++的C++进阶】之多态_第15张图片

你可能感兴趣的:(1++的C++进阶,c++,多态)