C++对象模型 第二章 虚函数

编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。

多态还可分为:

  • 动态多态(dynamic polymorphism):通过类继承机制和虚函数机制生效于运行期。可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口。也被称为子类型多态(Subtype polymorphism)或包含多态(inclusion polymorphism)。在面向对象程序设计中,这被直接称为多态

  • 静态多态(static polymorphism):模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。可以用来实现类型安全、运行高效的同质对象集合操作。C++STL不采用动态多态来实现就是个例子。

    • 函数重载(Function Overloading)
    • 运算符重载(Operator Overloading)
    • 带变量的宏多态(macro polymorphism)
    • 非参数化多态或译作特设多态(Ad-hoc polymorphism):
    • 参数化多态(Parametric polymorphism):把类型作为参数的多态。在面向对象程序设计中,这被称作**泛型编程**。

对于C++语言,带变量的宏和函数重载(function overload)机制也允许将不同的特殊行为和单个泛化记号相关联。然而,习惯上并不将这种函数多态(function polymorphism)、宏多态(macro polymorphism)展现出来的行为称为多态(或静态多态),否则就连C语言也具有宏多态了。谈及多态时,默认就是指动态多态,而静态多态则是指基于模板的多态。

最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。

C++多态意义探究

C++多态成立的三个条件:

  1. 存在继承
  2. 虚函数重写
  3. 父类指针指向子类对象

第二章 虚函数

继承关系作用下虚函数的手工调用 和 分析

  • (1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。
  • (2)父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为你是继承父类的。
    也有人认为,如果子类中把父类的虚函数的virtual去掉,是不是这些函数就不再是虚函数了?
    只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。
    但不管是父类还是子类,都只会有一个虚函数表,不能认为子类中有一个虚函数表+父类中有一个虚函数表,
    得到一个结论:子类中有两个虚函数表。
    子类中是否可能会有多个虚函数表呢?后续我们讲解这个事;
  • (3)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。
    但,仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。
    虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),
    则该表项所执行的该函数的地址应该相同。
  • (4)超出虚函数表部分内容不可知;
  • 虚函数表跟着类走,虚函数指针跟着对象中
class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base 
{
	virtual void g() { cout << "Derive::g()" << endl; }
	/* void f() { cout << "Derive::f()" << endl; }
	 void g() { cout << "Derive::g()" << endl; }
	 void h() { cout << "Derive::h()" << endl; }*/

};

int main() {
	// 继承关系作用下虚函数的手工调用			
	cout << sizeof(Base) << endl;	
	cout << sizeof(Derive) << endl;

	Derive *d = new Derive(); //派生类指针。
	Derive *d2 = new Derive(); //派生类指针。

	long *pvptr = (long *)d;  //指向对象的指针d转成了long *类型。
	long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。

	long *pvptr2 = (long *)d2;
	long *vptr2 = (long *)(*pvptr2);


	for (int i = 0; i <= 4; i++) //循环5次;
	{
		printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
	}

	typedef void(*Func)(void); //定义一个函数指针类型
	Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
	Func g = (Func)vptr[1];
	Func h = (Func)vptr[2];
	//Func i = (Func)vptr[3];
	//Func j = (Func)vptr[4];*/

	f();
	g();
	h();
	i();
	return 0;
}
  • 直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
    所以Base base = derive;实际干了两个事情:
    第一个事情:生成一个base对象
    第二个事情:用derive来初始化base对象的值。
    这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
    derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器没做这部分工作;
	Base base = derive; // 直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
	                    // 所以Base base = derive;实际干了两个事情:
	                            // 第一个事情:生成一个base对象
	                            // 第二个事情:用derive来初始化base对象的值。
	                              // 这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
	                                // derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器帮我们做到了这点;
	long *pvptrbase = (long *)(&base);
	long *vptrbase = (long *)(*pvptrbase); //0x00b09b34 {project100.exe!void(* Base::`vftable'[4])()} {11538847}
	Func fb1 = (Func)vptrbase[0];   //0x00b0119f {project100.exe!Base::f(void)}
	Func fb2 = (Func)vptrbase[1];   //0x00b01177 {project100.exe!Base::g(void)}
	Func fb3 = (Func)vptrbase[2];   //0x00b01325 {project100.exe!Base::h(void)}
	Func fb4 = (Func)vptrbase[3];    //0x00000000
	Func fb5 = (Func)vptrbase[4];    //0x65736142
  • OO(面向对象) 和OB(基于对象)概念:
    c++通过类的指针和引用来支持多态,这是一种程序设计风格,这就是我们常说的面向对象。object-oriented model;
    OB(object-based),也叫ADT抽象数据模型【abstract datatype model】,不支持多态,执行速度更快,因为
    因为 函数调用的解析不需要运行时决定(没有多态),而是在编译期间就解析完成,内存空间紧凑程度上更紧凑,因为没有虚函数指针和虚函数表这些概念了;
    Base *pbase = new Derive();
    Base &base2 = derive2;
    但显然,OB的设计灵活性就差;
    C++既支持面向对象程序设计(继承,多态)(OO),也支持基于对象(OB)程序设计。

面向对象的三大特性 —— 多态、继承、封装

面向对象主要有几个特性,封装、继承、多态。没有封装就不能继承,没有继承就没有运行时的多态。基于对象并不是单独的理论,而是面向对象的初级阶段,就是只有封装。只能是把属性、方法放进类中,实例化对象调用。学习面向对象要从基础知识入手,学会定义类、接口的定义、继承。然后要深入细致的研究现实事物,把现实事物或是需求文档中的名词抽象出来生成类或属性,如果是主语,多半还要根据整句的描述生成方法,定义类结构。

多继承虚函数表分析

  • 说明
    (1)一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是两个虚函数表指针,而不是两个虚函数表);
    //在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr);

    (2)老师画图:适合vs2017。
    (2.1)子类对象ins有里那个虚函数表指针,vptr1,vptr2
    (2.2)类Derived有两个虚函数表,因为它继承自两个基类;
    (2.3)子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl)(这里共用指的是 子类把自己先建立的虚函数 放在了第一个vptr指向的内存),
    因为我们注意到了类Derived的虚函数表1里边的5个函数,而g()正好是base1里边的函数。
    (2.4)子类中的虚函数覆盖了父类中的同名虚函数。比如derived::f(),derived::i();

image-20211116140000271

//子类
class Derived :public Base1, public Base2
{
public:
	virtual void f() //覆盖父类1的虚函数
	{
		cout << "derived::f()" << endl;
	}
	virtual void i() //覆盖父类2的虚函数
	{
		cout << "derived::i()" << endl;
	}

	//如下三个我们自己的虚函数
	virtual void mh() 
	{
		cout << "derived::mh()" << endl;
	}
	
	virtual void mi()
	{
		cout << "derived::mi()" << endl;
	}

	virtual void mj()
	{
		cout << "derived::mj()" << endl;
	}
};

辅助工具,vptr、 vtbl创建时机

第五节 辅助工具,vptr、vtbl创建时机

  • cl.exe:编译链接工具 —— 打印地址
    cl /d1 reportSingleClassLayoutDerived project100.cpp

    (linux下)g++ -fdump-class-hierarchy -fsyntax-only 3_4.cpp

  • vptr(虚函数表指针)什么时候创建出来的?
    vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候赋值的 —— 程序运行的时候(动态多态)
    实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。
    这属于编译器默默为我们做的事,我们并不清楚。
    当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有 给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了;

虚函数表是什么时候创建的?
实际上,虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。
然后也是在编译器期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到成成类对象的代码时,会调用类的构造函数,执行到类的构造函数中的 给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了;

image-20211116141428938

单纯的类不纯时引发的虚函数调用问题

单纯的类:比较简单的类,尤其不包含 虚函数和虚基类。

  • 如果类并不单纯,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形;
    那就是某些情况下,编译器会往类内部增加一些我们看不见 但真实存在的成员变量(隐藏成员变量),有了这种变量的类,就不单纯了;
    同时,这种隐藏的成员变量的 增加(使用) 或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行。
    那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了;

    比如你类中增加了虚函数,系统默认往类对象中增加 虚函数表指针,这个虚函数表指针就是隐藏的成员变量。

	virtual void virfunc()
	{
		cout << "虚函数virfunc()被执行" << endl;
	}
	void ptfunc()
	{
		cout << "普通函数ptfunc()被执行" << endl;
	}
  • 这个函数ptfunc()和virfunc()函数,是在编译的就确定好的;
    静态联编 和 动态联编。
    静态联编:我们编译的时候就能确定调用哪个函数。把调用语句和倍调用函数绑定到一起;
    动态联编:是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。

更明白:虚函数,多态,这种概念专门给指针或者引用用的; —— 父类指针 指向 子类(子类强转为父类)

多态:同样的语句再运行时有多种不同的表现形式(根据对象的实际类型来调用相应的函数,不会弱化成父类)

多态性:通过指向子类的父类指针或引用,可以访问子类中的同名覆盖的成员函数。

虚函数:根据指针指向的对象的类型,来执行不同类的同名覆盖函数,实现同一语句的不同行为。

虚函数关键字:virtual.

1. 被virtual声明的函数被重写后具有多态性。(通过指向子类的父类指针或引用,可以访问子类中的同名覆盖的成员函数)

2. 被virtual声明的函数叫虚函数。

3. 对可能要在继承时被重写的函数声明virtual 关键字。

多态的意义:

1. 多态是动态的意义,编译时无法预知实际调用,再运行时才展现具体的调用。

2. 重写函数必须用多态来实现。(避免无法访问子类的重写函数)

静态联编:在程序的编译器就决定具体的函数调用。函数重载

动态联编:在程序的执行期才决定具体的函数调用。虚拟函数的重写

函数覆盖:通过对象访问子类函数。通过作用域符,指针访问父类函数。

函数重写:通过对象访问子类函数。通过作用域符,指针访问父类函数。

函数多态:根据具体对象访问子类和父类函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值