C++多态详解

多态的概念

当不同的对象去完成某个行为时会产生出不同的状态。

比如:
买火车票,普通人是全价,学生是半价,儿童是免票。

多态的定义和实现

在了解多态的构成条件之前,我们首先要了解虚函数重写

虚函数

virtual修饰的类成员函数称为虚函数。

class person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——全价" << endl;
	}
};

重写

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

class person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——全价" << endl;
	}
};

class student:public person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——半价" << endl;
	}
};

协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B :public A{};
class person
{
public:
	virtual A* buyTicket()
	{
		return new A;
	}
};

class student:public person
{
public:
	virtual B* buyTicket()
	{
		return new B;
	}
};

析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。

class person
{
public:
	virtual ~person()
	{
		cout << "~person()" << endl;
	}
};
class student:public person
{
public:
	//只有派生类Student的析构函数重写了Person的析构函数
    //delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数
	virtual ~student()
	{
		cout << "~student()" << endl;
	}
};

多态构成的条件

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  3. 前提:继承
class person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——全价" << endl;
	}
};

class student:public person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——半价" << endl;
	}
};

void fun1(person& people)
{
	people.buyTicket();
}

void fun2(person* people)
{
	people->buyTicket();
}

int main()
{
	person krystal;
	fun1(krystal);

	student crystal;
	fun2(&crystal);
	return 0;
}

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

overridefinal关键字

override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class car
{
public:
	virtual void drive(){}
};

class porsche:public car
{
public:
	virtual void drive() override
	{
		cout << "porsche——fast" << endl;
	}
};

final

修饰虚函数,表示该虚函数不能再被继承。

class car
{
public:
	virtual void drive() final {}
};

class porsche:public car
{
public:
	virtual void drive() 
	{
		cout << "porsche——fast" << endl;
	}
};

在这里插入图片描述

抽象类

概念

在虚函数的后面写上=0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类抽象类不能实例化出对象

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

class car
{
public:
	virtual void drive() = 0;
};
	
class porsche:public car
{
public:
	virtual void drive() 
	{
		cout << "porsche——fast" << endl;
	}
};

int main()
{
	car c1;
	porsche p1;

	return 0;
}

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

接口继承和实现继承

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

多态的原理

虚函数表

class Base
{
private:
	int _b=1;
public:
	virtual void fun1()
	{
		cout << "fun1()" << endl;
	}
};
int main()
{
	Base b;
	return 0;
}

运行代码,使用监视窗口查看对象内容。
C++多态详解_第3张图片
发现b对象中除了_b成员还有一个_vfptr放在对象前面,我们把对象中的这个指针叫做虚函数表指针简称虚表指针,指向一个虚表。

一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

派生类的虚表

派生类的虚表中存放了什么呢?

我们修改代码,进行观察。

  1. 加一个派生类Derive去继承Base
  2. 派生类中重写fun1()
  3. Base增加虚函数fun2()和普通函数fun3()
class Base
{
private:
	int _b=1;
public:
	virtual void fun1()
	{
		cout << "BASE::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "BASE::fun2()" << endl;
	}
	void fun3()
	{
		cout << "BASE::fun3()" << endl;
	}
};

class Derive :public Base
{
private:
	int _d = 2;
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

C++多态详解_第4张图片通过监视窗口,我们发现:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,父类虚表指针中的一部分,另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现fun1()完成了重写,所以d的虚表中存的是重写的Derive::fun1(),所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. fun2()继承下来后是虚函数,所以放进了虚表,fun3()也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

总结:
派生类的虚表生成

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚表存放的位置

VS下存放在代码段

class Base
{
private:
	int _b=1;
public:
	virtual void fun1()
	{
		cout << "BASE::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "BASE::fun2()" << endl;
	}
	void fun3()
	{
		cout << "BASE::fun3()" << endl;
	}
};

class Derive :public Base
{
private:
	int _d = 2;
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
};
void fun()
{
}

int main()
{
	Base b;
	Derive d;
	static int si = 0;//数据段
	int i = 1;//栈
	int* pi = new int;//堆
	void(*fptr)();
	fptr = fun;//代码段
	cout << "数据段:" << &si<<endl;
	cout << "栈:" << &i<<endl;
	cout << "堆:" << &pi<< endl;
	cout << "代码段:" <<fptr<< endl;
	typedef void(*vfptr)();
	vfptr* ptr = (vfptr*)(*((int*)&d));
	cout << "虚表:" << ptr << endl;

	return 0;
}

运行结果
C++多态详解_第5张图片
可以观察到:
在vs编译器中,虚表存放在代码段。

多态原理

class Person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——全价" << endl;
	}
};

class Student:public Person
{
public:
	virtual void buyTicket()
	{
		cout << "买票——半价" << endl;
	}
};

void fun(Person* p)
{
	p->buyTicket();
}

int main()
{
	Person zhangsan;
	fun(&zhangsan);

	Student lisi;
	fun(&lisi);

	return 0;
}

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

  1. 观察红色箭头我们看到,p是指向zhangsan对象时,p->buyTicket在zhangsan的虚表中找到虚函数是Person::buyTicket

  2. 观察蓝色箭头我们看到,p是指向lisi对象时,p->buyTicket在lisi的虚表中找到虚函数是Student::buyTicket

  3. 这样就实现了,不同对象完成同一行为时,状态不同。

下面我们来看一下汇编代码:
C++多态详解_第7张图片

  1. 获取对象首地址,放入eax寄存器中
  2. 从对象首地址开始读取四个字节内容(虚表首地址),存入edx中
  3. 从虚表首地址开始,获取虚表中的一个函数地址,存入eax
  4. 调用函数

可以看到,在多态场景下
使用的是动态绑定:
在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数。

单继承和多继承的虚函数表

单继承中的虚表

class Base
{
private:
	int _b;
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}

};

class Derive :public Base
{
private:
	int _d;
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Derive::fun4()" << endl;
	}
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

查看监视窗口:
C++多态详解_第8张图片

我们发现看不见fun3fun4。这里是编译器的监视窗口故意隐藏了这两个函数。

单继承通过代码来查看虚表

  1. 定义一个虚函数指针类型vfptr
  2. 定义一个vfptr*类型的fptr,指向虚表
  3. 依次取虚表中的虚函数指针打印并调用
class Base
{
private:
	int _b;
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}

};

class Derive :public Base
{
private:
	int _d;
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Derive::fun4()" << endl;
	}
};
typedef void(*vfptr)();
void PrintVtable(vfptr Vtable[])
{
	cout << "虚表地址:" << Vtable << endl;
	vfptr* fptr = Vtable;
	while (*fptr != nullptr)
	{
		printf("虚函数地址:0X%x", fptr);
		(*fptr)();
		++fptr;
	}
}
int main()
{
	Base b;
	Derive d;
	 //思路:取出b、d对象的头4bytes,就是虚表的指针 
     // 1.先取b的地址,强转成一个int*的指针   
    // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针    
 	// 3.再强转成vfpte*,因为虚表就是一个存vfptr类型(虚函数指针类型)的数组。   
    // 4.虚表指针传递给PrintVtable进行打印虚表
	vfptr* vtablebbb = (vfptr*)(*(int*)&b);
	PrintVtable(vtablebbb);
	cout << endl;
	vfptr* vtableddd = (vfptr*)(*(int*)&d);
	PrintVtable(vtableddd);
	return 0;
}

C++多态详解_第9张图片

多继承中的虚表

派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

多继承通过代码来查看虚表

class Base1
{
private:
	int _b1;
public:
	virtual void fun1()
	{
		cout << "Base1::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base1::fun2()" << endl;
	}

};
class Base2
{
private:
	int _b2;
public:
	virtual void fun1()
	{
		cout << "Base2::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base2::fun2()" << endl;
	}

};
class Derive:public Base1, public Base2
{
private:
	int _d;
public:
	virtual void fun1()
	{
		cout << "Derive::fun2()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun2()" << endl;
	}

};
typedef void(*vfptr)();
void PrintVtable(vfptr Vtable[])
{
	cout << "虚表地址:" << Vtable << endl;
	vfptr* fptr = Vtable;
	while (*fptr != nullptr)
	{
		printf("虚函数地址:0X%x", fptr);
		(*fptr)();
		++fptr;
	}

}
int main()
{
	Derive d;
	vfptr* vtable1 = (vfptr*)(*(int*)&d);
	PrintVtable(vtable1);
	cout << endl;
	vfptr* vtable2 = (vfptr*)(*(int*)((char*)&d+sizeof(Base1)));
	PrintVtable(vtable2);
	return 0;

}

C++多态详解_第10张图片

可以看出:
派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

你可能感兴趣的:(CPP,多态,抽象类,接口)