C++三大特征之多态(虚函数、抽象类)

  • 面向对象语言三大特征:封装、继承和多态。

    文章目录

    • 一、多态的概念
      • 1.多态的定义
      • 2.多态的构成条件
    • 二、虚函数
      • 1.虚函数的重写
      • 2.多态(虚函数重写)的例外
      • 3.抽象类(接口类)
    • 三、多态的原理
      • 1.多态类的对象模型
      • 2.多态的原理
    • 四、多态的动态绑定与静态绑定
      • 1.多态的静态绑定
      • 2.多态的动态绑定
    • 五、单继承和多继承与虚函数表
      • 1.单继承的虚表
      • 2.多继承的虚表
    • 六、重载、重定义(隐藏)和重写(覆盖)的对比

一、多态的概念

1.多态的定义

  • 多态就是多种形态。是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
  • 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如动物类Animal,,Dog继承了Animal中的eat函数是吃肉,而Sheep继承了Animal中的eat函数是吃草。

2.多态的构成条件

(1)必须通过基类的指针或者引用调用虚函数。
(2)被调用的函数,必须是虚函数,且派生类必须对基类的虚函数进行重写。

二、虚函数

  • 虚函数即被virtual修饰的类成员函数。
class Animal
{
public:
	virtual void eat()
	{
		cout << "吃草或者肉" << endl;
	}
};

1.虚函数的重写

  • 派生类中有一个跟基类完全相同的函数(派生类的虚函数与基类的虚函数的返回值类型、函数名字、参数列表完全相同),称为派生类的虚函数重写了基类的虚函数。
  • 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写,因为继承后基类的虚函数被继承下来了在派生类中依旧保持虚函数的属性。但不建议这样使用。
#include 

using namespace std;

class Animal
{
public:
	virtual void eat()
	{
		cout << "吃草或者吃肉" << endl;
	}
};
class Dog : public Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉" << endl;
	}
};

int main()
{
	cout << "当不是基类的指针或引用时:" << endl;
	Animal a;
	a.eat();
	Dog d;
	d.eat();
	a = d;
	cout << endl;

	cout << "当通过的是基类的指针或引用时:" << endl;
	Animal a2;
	a2.eat();
	Dog d2;
	Animal* ap = &a2;
	ap->eat();//调用的是父类的
	ap = &d2;//通过基类的引用或指针
	ap->eat();//构成重写。调用的是子类的。
}

C++三大特征之多态(虚函数、抽象类)_第1张图片

2.多态(虚函数重写)的例外

(1)重写的协变(基类和派生类虚函数的返回值类型不同)

#include 

using namespace std;
class A
{};
class B : public A
{};
class Animal
{
public:
	virtual A* eat()
	{
		cout << "new A: 吃草或者吃肉" << endl;
		return new A;
	}
};
class Dog : public Animal
{
public:
	virtual B* eat()
	{
		cout << "new B: 吃肉" << endl;
		return new B;
	}
};

int main()
{
	cout << "当不是基类的指针或引用时:" << endl;
	Animal a;
	a.eat();
	Dog d;
	d.eat();
	a = d;
	cout << endl;

	cout << "当通过的是基类的指针或引用时:" << endl;
	Animal a2;
	a2.eat();
	Dog d2;
	Animal* ap = &a2;
	ap->eat();
	ap = &d2;//通过基类的引用或指针
	ap->eat();//构成重写
}

C++三大特征之多态(虚函数、抽象类)_第2张图片

  • 基类虚函数返回值类型为基类对象的指针(A*)或者引用(A&),派生类虚函数返回值类型为派生类对象的指针(B*)或者引用(B&),即是协变。

(2)析构函数的重写

  • 析构函数可以定义成虚函数。
  • 析构函数不是必须写成虚函数,但是一种情况下必须写成虚函数。
#include 

using namespace std;


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

int main()
{
	Animal* a = new Animal;
	delete a;
	cout << endl;
	Dog* d = new Dog;
	delete d;
	cout << endl;
	Animal* ap = new Dog;
	delete ap;

	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第3张图片

  • 上述例子中 Animal* ap = new Dog; 这种情况下,析构函数必须写成虚函数构成重写。
  • 只有派生类Dog的析构函数重写了Animal的析构函数,delete对象调用析构函数,才能构成多态,才能保证a和ap指向的对象正确的调用析构函数。
  • delete ap; ===》destructor + free(ap);其中destructor函数为普通调用,调用的是父类的不是子类的,会出错。故这种情况下要加virtual,此时的delete必须要构成多态(否则会造成资源泄露)。
  • 析构函数的重写,只要基类的析构函数加virtual变成虚函数,即完成了重写,从而构成了多态。

3.抽象类(接口类)

(1)概念:在虚函数后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数体现了接口继承。
(2)作用:① 强制派生类必须重写该虚函数。② 普通函数的继承是一种实现继承。虚函数的继承是一种接口继承(派生类继承的是基类虚函数的接口,目的是为了重写,只需要函数的接口,不需要函数的实现,达成多态)。如果不实现多态,不要把函数定义成虚函数。
(3)什么时候会使用抽象类?-------若要定义的基类非常抽象。

三、多态的原理

1.多态类的对象模型

#include 

using namespace std;


class Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉或吃草" << endl;
	}
	virtual void fun1()
	{
		cout << "Animal::fun1()" << endl;
	}
	void fun2()
	{
		cout << "Animal::fun2()" << endl;
 	}
private:
	double _d;
	char _c;
	int _i = 1;
};


int main()
{
	Animal a;

	cout << "sizeof(a) = " << sizeof(a) << endl;

	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第4张图片C++三大特征之多态(虚函数、抽象类)_第5张图片

  • 上述结果对象a的大小并不是double _d; char _c; int _i;的和16。而是24。此时我们在通过监视窗口可以看到对象a中不仅仅有_d,_c,_i三个成员变量,还有一个_vfptr,类型为void**的指针。
  • 其实_vfptr是虚表指针。指向的是函数指针数组。而这个函数指针数组就是虚函数表。其实虚函数是通过一张虚函数表来实现的,简称虚表(V-Table)。虚函数表中存的是每个虚函数的地址(第一条语句的地址),而虚函数是存在代码段里的。在类中先定义的虚函数的地址先存放在虚函数表中。
  • 虚基表里存的是偏移量,虚函数表里存的是函数的地址。虚函数和普通函数一样是存在代码段里的。对象里存的是虚表指针,在vs下虚表是存在代码段里的。(虚表可能存在代码段或者静态区里)
    C++三大特征之多态(虚函数、抽象类)_第6张图片

2.多态的原理

(1)

#include 

using namespace std;


class Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉或吃草" << endl;
	}
	virtual void fun1()
	{
		cout << "Animal::fun1()" << endl;
	}
	void fun2()
	{
		cout << "Animal::fun2()" << endl;
 	}
private:
	double _d;
	char _c;
	int _i = 1;
};
class Dog : public Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉" << endl;
	}
	virtual void fun1()
	{
		cout << "Dog::fun1()" << endl;
	}
};

int main()
{
	Animal a;
	Dog d;
	cout << "sizeof(a) = " << sizeof(a) << endl;
	cout << "sizeof(d) = " << sizeof(d) << endl;

	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第7张图片

  • 由上述结果可以得到,基类对象和派生类对象的大小相同。说明虚表指针是一样的。
  • 其实派生类只是将基类的虚表指针拷贝下来,虚表还是基类创建的。

(2)

#include 

using namespace std;


class Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉或吃草" << endl;
	}
	virtual void fun1()
	{
		cout << "Animal::fun1()" << endl;
	}
	void fun2()
	{
		cout << "Animal::fun2()" << endl;
 	}
private:
	double _d;
	char _c;
	int _i = 1;
};
class Dog : public Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉" << endl;
	}
	virtual void fun1()
	{
		cout << "Dog::fun1()" << endl;
	}
};

int main()
{
	Animal a;
	Dog d;
	
	Animal* ap = &a;
	ap->eat();//调用的是Animal中的eat()
	ap->fun1();//调用的是Animal中的fun1()
	ap->fun2();
	cout << endl;

	ap = &d;
	ap->eat();//调用的是Dog中的eat()
	ap->fun1();//调用的是Dog中的fun1()

	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第8张图片

  • 由上述代码产生的结果可以知道ap = &a之后,ap->eat();调用的是Animal中的eat(),ap->fun1();调用的是Animal中的fun1();ap = &d之后ap->eat();调用的是Dog中的eat(),ap->fun1();调用的是Dog中的fun1()。
  • **其实派生类的指针和引用看到的虚表还是基类的虚表,不过该虚表是派生类覆盖之后的虚表,即将原本基类的虚函数地址覆盖成了现在派生类的虚函数地址。**这个就是构成多态的原理。指向父类,找到的是父类的虚函数表;指向子类,找到的是子类中父类的一部分(被切片之后的)。(虚函数表是在编译时候产生的)
  • 而普通函数就是按照普通方法寻找到的,所以如果不是构成多态,就不会用到虚函数表。而且对象是拥有自己的虚函数表,不会拷贝基类的虚函数表。
  • 满足多态以后的函数调用,不是在编译时候确定的,是在运行起来以后到对象中去找的。不满足多态的函数调用时在编译时确认好的,不需要用到虚函数表。

(3)

int main()
{
	Animal a;
	Dog d;
	
	a = d;//此时不构成多态,因为是通过基类的对象。
	a.eat();
	a.fun1();

	return 0;
}

在这里插入图片描述

  • 如果是通过基类的指针和引用时,虚函数是在虚表中寻找的(此时构成多态)。
  • 如果是用基类对象时,直接调用虚函数。(对象类型中的虚函数)上述对象a的类型是Animal类型,所以调用虚函数eat()和fun1()是Animal类中的虚函数。
  • 同一类型对象的虚表是一样的(虚函数表是在编译时创建的,固定的,不管申请多少个对象,他们的虚表都是同一个)。

四、多态的动态绑定与静态绑定

1.多态的静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如:函数重载。
// 函数重载(静态绑定)例子
#include 
using namespace std;

int main()
{
	int i = 10;
	double d = 99.9;
	cout << a << endl;
	cout << d << endl;

	return 0;
}
  • 上面两个cout << 构成了函数重载。

2.多态的动态绑定

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

五、单继承和多继承与虚函数表

1.单继承的虚表

#include 

using namespace std;


class Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉或吃草" << endl;
	}
	virtual void fun2()
	{
		cout << "Animal::fun2()" << endl;
	}
private:
	int _i;
};
class Dog : public Animal
{
public:
	virtual void eat()
	{
		cout << "吃肉" << endl;
	}
	virtual void dun()
	{

	}
	virtual void fun3()
	{

	}
private:
	int _a;
};
int main()
{
	Animal a;
	Dog d;


	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第9张图片

  • 只要一个类中有虚函数,就会产生虚函数表。
  • 由上面的监视窗口我们可以看出,即使子类有属于自己的虚函数,但是子类拥有的虚表指针也只有一个(从父类拷贝下来的)。说明子类和父类的虚表也只有一个, 子类就算有新的虚函数,也是覆盖写入父类的虚表中。
    C++三大特征之多态(虚函数、抽象类)_第10张图片

2.多继承的虚表

#include 

using namespace std;

class A
{
public:
	virtual void fun1()
	{}
	virtual void funA()
	{}
};

class B
{
public:
	virtual void fun2()
	{}
	virtual void funB()
	{}
};
class C : public A, public B
{
public:
	virtual void fun1()
	{}
	virtual void fun2()
	{}
	virtual void funC()
	{}
};



int main()
{
	C c;

	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第11张图片

  • 根据上面的监视窗口可以得出,对象c中有两个虚表指针。一个是拷贝基类A中的虚表指针,一个是拷贝基类B中的虚表指针。
  • 多继承中的子类中有多个虚表指针。
#include 

using namespace std;

typedef void(*VFPTR) ();
class A
{
public:
	virtual void fun1()
	{}

};

class B
{
public:
	virtual void fun2()
	{}
};
class C : public A, public B
{
public:
	virtual void fun1()
	{}
	virtual void fun2()
	{}
	virtual void fun3()
	{}
};

void Print(VFPTR vTable[])
{
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:0X%x, ->", i, vTable[i]);
		vTable[i]();
	}

	cout << endl;
}

int main()
{
	C c;
	VFPTR* vTableA = (VFPTR*)(*(int*)&c);
	Print(vTableA);
	cout << endl;

	VFPTR* vTableB = (VFPTR*)(*(int*)((char*)&c + sizeof(A)));
	Print(vTableB);


	return 0;
}

C++三大特征之多态(虚函数、抽象类)_第12张图片

  • 由上面代码运行结果可以看出,对象c中的funC()函数地址是存在基类A的虚表中的。
    C++三大特征之多态(虚函数、抽象类)_第13张图片
  • 多继承派生类中的、不是重写基类的而是自己的虚函数,是放在第一个继承的基类的虚函数表中的。

六、重载、重定义(隐藏)和重写(覆盖)的对比

  • 重载:两个函数必须在同一作用域。函数名必须相同,参数列表(参数的个数、参数的类型、参数的顺序)不相同。
  • 重定义(隐藏):两个函数分别在基类和派生类的作用域。函数名相同。两个基类和派生类的同名函数不是构成重写就是构成重定义。
  • 重写(覆盖):两个函数分别在基类和派生类的作用域。函数名、参数、返回值必须相同(协变例外)。两个函数必须是虚函数。

你可能感兴趣的:(C++)