C++的多态学习(二)

C++的多态学习(二),第1张

目录
  • C++11 关键字final 和override
  • 重载、重写和重定义三者区别
  • 抽象类
  • 多态的原理
  • 单继承和多继承时的虚函数表
  • 常见问答题
C++11 关键字final 和override

final关键字有两个用处。


1.用来修饰类,使该类成为最终类不能被继承下去。


2.用来修饰虚函数,使派生类不能重写该虚函数。






override关键字是用来修饰派生类虚函数的,检查其是否重写了基类的虚函数,没重写就报错。



重载、重写和重定义三者区别

重载是定义在同一作用域下,函数名相同函数参数列表中参数类型、个数和顺序不同。

调用函数时根据所给参数确定调用的函数。

重写是分别在基类作用域下和派生类作用域下的虚函数,函数名、返回参数和参数都相同,派生类中重新定义基类中对应虚函数的行为。

重定义是分别在基类作用域下和派生类作用域下的成员函数,函数名相同,在派生类中会隐藏从基类继承下来的同名函数,派生类调用该同名函数时调用的是本类的,还要注意基类和派生类的同名函数不构成多态时就构成重定义。

抽象类 纯虚函数的概念

在声明虚函数时在后面加上“=0” 则该函数被声明为纯虚函数,有纯虚函数的类就称为抽象类,抽象类是不能实例化出对象的。




纯虚函数是可以定义其行为的,但没有这个必要,应为本类是抽象类不能生成对象,用对象去调用这个函数,虚函数又不能是静态函数也不能通过类型::成员函数的方式去调用,构成多态时没有本类的对象可以传给本类指针或引用来调用,所以定义纯虚函数的行为是无意义的,只要声明就行了。

抽象类的派生类中必须重写这个纯虚函数,否则派生类由于继承了抽象类的纯虚函数属性,派生类就也是抽象类,不能实例化对象。

抽象类就是接口类专门用来定义各种接口,让派生类去实现各种接口,定义各自想要的行为,体现了接口继承。

class A
{
public:
	virtual void f() = 0;
};
class B:public A
{
public:
	void f()
	{
		cout << "B::f()" << endl;
	}
};
class C:public A
{
public:
	void f()
	{
		cout << "C::f()" << endl;
	}
};
void test(A& aa)
{
	aa.f();
}
int main()
{
	B b;
	C c;
	test(b);
	test(c);
	return 0;
}

多态的原理

先回顾一下构成多态的条件
父类的指针或引用指向对象来调用虚函数
子类必须重写父类的虚函数。


当一个类中用虚函数时,我们可以测试该类类型的大小

class A
{
public:
	virtual void f()
	{
		cout << "A::f()" << endl;
	}
private:
	int _a;
};
int main()
{
     A a;
	cout << sizeof(A) << endl;
	return 0;
}


在32为平台下为什么会是8字节呢?
我们知道成员函数实在代码区,一般类的大小都是计算该类的成员数据的,打开监视窗口就能知道原因了。


类中多了一个指针,所以是8字节。

经过测试可以得知,如果类中定义了虚函数
该类对象中就会多出一个指针_vfptr,这是虚函数表指针,指向一张虚函数表,而虚函数表本质上就是函数指针数组,里面存放的就是本类的所有虚函数地址,最后一般会放入nullptr,虚函数和普通成员函数的地址都是存放在代码区的,只是虚函数的地址又存了一份在该类的虚函数表中,派生类继承基类后先把基类虚表也继承下来,在派生类中重写了虚函数后,就把重写的虚函数的地址覆盖掉原来基类虚函数的地址,产生多态是用基类指针或引用指向不同对象调用同一个虚函数,根据对象的不同调用行为不同的函数,我们传入对象让指针或引用接受这里不是赋值,只是站在基类的角度看待传入的对象,调用虚函数时,就会去虚函数表中找到对应函数的地址,去对应的地址上去执行代码,传入基类对象,我就按固定的步骤去虚函数表中找到的就是基类虚函数的地址执行基类的虚函数,传入派生类对象,我在虚表中找到的是派生类的虚函数地址执行派生类虚函数,因为我是用基类指针或引入来调用的,站在统一的视角上,改变传入的对象,我实际都是在不同的对象虚表中找到它对应的虚函数地址的。

多态不能用对象的方式是因为用对象来接受对象就是赋值了,不同对象赋值给基类对象,是不会把虚表指针赋值过去的,所以只能用指针或引用的方式。

下面来验证一下所说多态的机制。

class A
{
public:
	virtual void f()
	{
		cout << "A::f()" << endl;
	}
private:
	int _a;
};
class B:public A
{
public:
	void f()
	{
		cout << "B::f()" << endl;
	}
};
class C:public A
{
public:
	void f()
	{
		cout << "C::f()" << endl;
	}
};
void test(A& aa)
{
	aa.f();
}
int main()
{
	A a;
	B b;
	C c;
	test(a);
	test(b);
	test(c);
	return 0;
}


可以看出A B C三种类型不是同一张虚表了,虚表地址不同,B C 都重写了A的虚函数,存在虚表中的函数地址也都不相同了。


test(a)test(b)test(c)代码执行到test中aa.f()的汇编都是


这时步骤都还是一样的。

call eax 跳一层到

这里test(a)用基类A引用接受传入的A 类型对象a,在虚表中就能找到基类虚函数的地址,跳到该地址执行相应的行为

传入B类型对象b时

在它的虚表中找到了B重写的虚函数的地址

传入C类型对象c时步骤一样


对照监视窗口的虚函数表,发现里面存到地址虽然不是虚函数的直接地址,但是就是多了一次跳转指令的地址,也就相当于存了函数地址。

多态的原理就是这样的。

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

我们探讨一下但基类和派生类中有不同数量的虚函数,且派生类中重写了一部分基类的虚函数的情况

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

上面是基类有两个虚函数,派生类重写了其中一个,自己又定义了两个虚函数,
可以看看监视窗口

可以看成只显示了基类的两个虚函数,并且派生类重写了一个虚函数后,虚表中就覆盖了重写的那个虚函数的函数地址,没重写的就都一样,派生类中定义的自己的两个虚函数在不在虚表中,可以打印虚表测试一下。

typedef void(*vfuc)();       //现在虚函数函数指针类型重命名一下
void print(vfuc* vfptr)              //打印虚表中存到函数地址
{
	for (int i = 0; vfptr[i]; i++)
	{
		printf("vfptr[%d]  %p\n", i, vfptr[i]);
		vfptr[i](); //直接拿到函数地址后调用相应的函数
	}
}
int main()
{
	Base1 b;
	Darive d;
	print((vfuc*)*(void**)&d);    //虚表指针在对象内存的前4个字节,64位下是前8个字节,这里巧妙的用类型强转取到了不管是32位还是64位下对应的虚表指针
	return 0;
}


可以看出派生类自己定义的两个虚函数是紧跟着前两个虚函数放在虚表中的。

多继承中的虚表的情况

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 Student :public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Student::func1" << endl;
	}
	virtual void func3()
	{
		cout << "Student::func3" << endl;
	}
};
typedef void(*vfuc)();       //现在虚函数函数指针类型重命名一下
void print(vfuc* vfptr)              //打印虚表中存到函数地址
{
	for (int i = 0; vfptr[i]; i++)
	{
		printf("vfptr[%d]  %p\n", i, vfptr[i]);
		vfptr[i]();
	}
}
int main()
{
	Base1 b1;
	Base2 b2;
	Student s;
	print((vfuc*)*(void**)&s);    //打印第一张虚表
	cout << endl;
	print((vfuc*)*((void**)&s + 1));// 打印第二张虚表
	return 0;
}

这里学生类继承了两个基类,自己重写了两个基类中都有的一个虚函数,自己又定义了一个虚函数。



看监视窗口能看成,派生类就有两个虚表指针,指向两张表,两张表分别是从它的两个父类继承下来的,它重写的虚函数,在自己的两张表中就覆盖了基类的虚函数地址,自己这两张表中重写的虚函数的地址虽然不同,但可以通过测试证实它们最终都跳到去指向本类重写的虚函数上去,没重写的就跟基类的地址一样。


自己定义的虚函数的地址存在哪里呢?下面测试看看它在哪里。



可以看出派生类自己定义的虚函数放在第一张虚表中。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/674372.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存