C++多态 虚函数表 函数指针

1、多态

 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
那么在继承中要构成多态还有两个条件:
 1、 必须通过基类的指针或者引用调用虚函数
 2、 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
例如以下代码:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	//virtual void BuyTicket() { cout << "买票-半价" << endl; }
	//注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继
   //承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);  // 输出"买票-全价
	Func(st); // 输出"买票-半价
	return 0;

 我们知道,派生类的对象,指针,引用都能赋值给相应的基类,在我们调用Func(st),st赋值给p,如果不关键字virtual,调用的都是person中的BuyTicket()方法,而如果使用的虚方法,则调用的是Student 中的BuyTicket()方法。这是因为在有虚方法的类中实例的对象中存在虚函数表,派生类的虚函数表会对子类中的虚函数表进行重写(覆盖),所以在经过“切片”将派生类的对象赋值给基类的对象后,实际上基类的对象的虚函数表已经发生了改变。虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

2、虚函数表

 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。虚函数重写的两个例外:1、 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。2. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
 Base类的对象b和Derive 类的对象d都有一个虚函数表(这里显示的是虚函数表指针,是一个数组指针),由于d对虚函数进行了重写,所以d中的虚函数表fun1的地址和fun2的地址是不同的。

2.1 单继承中的虚函数表

 如果子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。


class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
};
class Derive : public Base
{
public:
	virtual void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
};
class people : public Derive
{
public:
	virtual void Func5()
	{
		cout << "Derive::Func3()" << endl;
	}
};
int main()
{
	Base b;
	Derive d;
	people p;
	return 0;
}

在这里插入图片描述
 但是,我们发现,不论是子类Derive的对象d,还是子类people的对象p,显示的虚函表中只有fun1和fun2,这个其实可以理解为编译器的bug,其实在内部是存在其他的虚函数,我们可以将其打印出来。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
};
class Derive : public Base
{
public:
	virtual void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
};
class people : public Derive
{
public:
	virtual void Func5()
	{
		cout << "people::Func5()" << endl;
	}
};
typedef void(*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()
{
	Base b;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	Derive d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb2);
	people p;
	VFPTR* vTableb3 = (VFPTR*)(*(int*)&p);
	PrintVTable(vTableb3);
	return 0;
}

在这里插入图片描述
 可见,父类的虚函数在子类的虚函数前面。
 当子类的虚函数重写后,1)覆盖的fun1函数被放到了虚表中原来父类虚函数的位置。2)没有被覆盖的函数依旧。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
};
typedef void(*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()
{
	Base b;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	Derive d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb2);
	return 0;
}

在这里插入图片描述

2.2 函数指针和虚函数表

 对上述求虚函数表地址的方法,使用了函数指针,这里再总结一下。
void (*fun)(char* str),很明显,这就是一个函数指针的表示方法,表示一个指针指向一个参数是char*,返回值是void的函数。
函数指针可以直接等于一个函数。

void Print(const char* str)
{
	cout << str << endl;
}
void main()
{
	void(*fun)(const char* str);
	const char* str = "1234";	
	fun = Print;
	//fun = Print(str);不允许
	fun(str);//输出1234
}

 如果我们需要多次使用这个函数,可以typedef,这样再需要使用是只需定义一个函数指针,然后对其进行初始化,最后运行。

void Print(const char* str)
{
	cout << str << endl;
}
typedef void(*Fun)(const char* str) ;
void main()
{
	const char* str = "1234";	
	Fun fun1 = NULL;//定义一个函数指针
	fun1 = Print;//进行初始化
	fun1(str);//运行
}

当我们需要打印一个数组是,可以这样传参。

void Print(int arr[])
{
	for (int i = 0; i < 3; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}

void main()
{
	int arr[3] = { 1,2,3 };
	Print(arr);
}

 这时,在理解我们之前的求虚函数地址的代码就好理解了。定义一个函数指针类型VFPTR,由于对象的虚函数表指针都是存在对象的前四个字节,所以需要取出来。 ( V F P T R ∗ ) ( ∗ ( i n t ∗ ) & b ) (VFPTR*)(*(int*)\&b) (VFPTR)((int)&b)
1、对b取地址,然后强转为int类型
2、再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3、再强转成VFPTR
,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4、虚表指针传递给PrintVTable进行打印虚表,因为虚表指针是一个函数指针数组,所以接受也需要是相同类型VFPTR vTable[]。
5、打印的时候根据我们之前演示的,定义一个函数指针,初始化,运行即可。
6、注意,在vs中,虚表数组的最后一个元素是NULL,而在Linux中,如果这个虚表结束,最后一个元素是0,如果这个虚表之后还有虚表,最后一个元素是1(多继承)

typedef void(*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()
{
	Base b;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	Derive d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb2);
	return 0;
}

2.3 多继承中的虚函数表

 有虚函数覆盖时,1) 每个父类都有自己的虚表。2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。原理如下:
在这里插入图片描述
代码演示:

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
};
typedef void(*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()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

在这里插入图片描述
在这里插入图片描述

2.4虚函数表的不安全性

 1、在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,所以在运行时,可以使用父类指针调用子类中的未覆盖父类的成员函数,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
 2、如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
 以上两点参考酷壳

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值