类对象模型
友元函数和友元类?
- 友元关系不能被继承。
- 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
- 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
空指针能调用类成员函数吗?
一个对象的指针可以调用它的成员函数和虚函数,那么如果一个指向空 nullptr 的指针,能不能调用它的成员函数和虚函数 ?
空指针调用成员函数是没有问题的,但是调用它的虚函数就会出错,当然,这两种情况在编译时都能通过
空指针为什么能调用成员函数?
对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。当调用p->func1();
这句话时,其实就是调用A::func1(this);
而成员函数的地址在编译时就已经确定, 需要注意的是,你用空指针调用成员函数,只是让this
指针指向了空,所以空指针也是可以调用普通成员函数,只不过此时的this
指针指向空而已,但如果函数fun1
函数体内并没有用到this
指针,所以不会出现问题。
空指针为什么不能调用虚函数?
我们知道,如果一个类中包含虚函数,那么他所实例化处的对象的前四个字节是一个虚表指针,这个虚表指针指向的是虚函数表。当然,虚函数的地址也是在编译时就已经确定了,这些虚函数地址存放在虚函数表里面,而虚函数表就在程序地址空间的数据段(静态区),也就是说虚表的建立是在编译阶段就完成的;当调用构造函数的时候才会初始化虚函数表指针,即把虚表指针存放在对象前四个字节(32位下)。试想一下,假如用空指针调用虚函数,这个指针根本就找不到对应的对象的地址,因此他也不知道虚表的地址,没有虚表的地址,怎么能调用虚函数呢。
执行顺序 ?
首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;
执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
执行成员对象的构造函数,多个成员对象的构造函数按照声明的顺序构造;
执行派生类自己的构造函数;
析构与构造的相反的顺序执行
静态绑定和动态绑定的介绍
静态绑定也就是将该对象相关的属性或函数绑定为它的静态类型,也就是它在声明的类型,在编译的时候就确定。在调用的时候编译器会寻找它声明的类型进行访问。
动态绑定就是将该对象相关的属性或函数绑定为它的动态类型,具体的属性或函数在运行期确定,通常通过虚函数实现动态绑定。
纯虚函数 ?
virtual int A() = 0; 必须由派生类实现。
程序如何判断一个函数是虚函数 ?
- 派生类可以不显式地用
virtual
声明虚函数,这是系统就会用一下规则来判断派生类的一个函数成员是不是虚函数:- 该函数是否与基类的虚函数有相同的名称、参数个数以及对应参数类型
- 该函数是否与基类的虚函数有相同的返回值或者满则类型兼容规则的指针,引用型的返回值
- 如果从名称、参数以及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
- 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式
- 一般习惯与在派生类的函数中也是用 virtual 关键字,以增加程序的可读性。
虚函数可以是内联函数吗?
视具体情况而定。当使用非多态调用的时候,编译器可以选择内联。
许多时候,派生类的虚函数会调用基类的同名函数,这时候是多态调用,不可内联。
哪些函数不能作为虚函数?
常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
-
为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
-
为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
-
为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
-
为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他不归某个具体对象所有,所以他也没有要动态邦定的必要性。
-
为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
下面几个条款可以结合一起分析
C++ 进取之道
多继承的实现?可能出现什么问题?
派生类继承多个基类,和每个基类之间视作单继承。可能出现二义性问题,在函数调用时需要指明域。
C++ 对象模型之 RTTI 的实现原理 — typeinfo 提供了 typeid 方法,用于执行期进行反射操作
C++ 类内存布局
C++ 类在有无继承、有无虚函数、有无多重继承或者虚继承时,其内存布局是不一样的。本文将分别阐述各种 case。
-
无继承
-
无虚函数
示例代码如下:class A { private: short pri_short_a; public: int i_a; double d_a; static char ch_a; void funcA1() {} };
A 的大小及内存布局如下:
如上可以说明:- 静态数据成员虽然属于类,但不占用具体类对象的内存
- 成员函数不占用具体类对象内存空间,成员函数存在代码区
- 数据成员的访问级别并不影响其在内存的排布和大小,均是按照声明的顺序在内存中有序排布,并适当对齐
-
有虚函数
在 1.1 类 A 里增加一个虚函数:class A { private: short pri_short_a; public: int i_a; double d_a; static char ch_a; void funcA1() {} virtual void funcA2_v(); };
其内存大小及布局如下:
可以看到,A 的起始处存储的是虚指针vptr
,指针大小是 4 字节,这里是为了对齐 8 字节。为了方便观察,之后的讨论中,我们统一把数据成员都改为int
类型,占 4 字节。
现在我们再加一个虚函数funA_v2()
:class A { private: short pri_short_a; public: int i_a; double d_a; static char ch_a; void funcA1() {} virtual void funcA2_v1(); virtual void funcA2_v2(); };
布局如下:
所以,不论再多虚函数,都只会有一个虚指针vptr,不会改变类的大小。不同之处在于,虚指针所指向的虚表中会多一个项目,即指向另一个虚函数的地址。
-
-
单一继承
-
单一继承且无虚函数
如下,我们设计了类 A、类B 、和 类C,其中 B 继承自 A , C 继承自 B:class A { public: int i_a; static char ch_a; void funcA1() {} }; class B : public A { public: int i_b; void funcB1() {} }; class C :public B { public: int i_c; };
内存布局如下:
单一继承的内存布局很清晰,每个派生类中起始位置都是 Basic class subobject。现在我们在类中增加虚函数,观察在单一继承 + 有虚函数的情况下,类的内存布局。 -
单一继承且有虚函数
如下:- 类 A 增加了两个虚函数 funcA_v1() 和 funcA_v2()
- 类 B 继承自 A , 覆写 funcA_v1()
- 类 C 继承自 B , 重写 funcA_v1(), 且有自己定义的一个虚函数 funC_v1()
class A { public: int i_a; static char ch_a; void funcA1() {} virtual void funcA_v1(); virtual void funcA_v2(); }; class B : public A { public: int i_b; void funcB1() {} virtual void funcA_v1(); }; class C :public B { public: int i_c; virtual void funcA_v1(); virtual void funcC_v1(); };
class A
的内存布局,如同1.2 ,如下:
class B
的内存布局如下:
B中首先也是基类A subobject,同样含有一个虚指针vptr。由于B覆写了funcA_v1(),故虚表中第一个索引处的函数地址是&B::funcA_v1()。
理解了B的内存布局,接下来C的内存布局也就不必赘述:
必须要提及两点:虚析构函数和覆写。虚析构函数在B.3.中详述。怎么才算是覆写?——类的继承里,子类里含有与父类里同名的虚函数,函数名、函数返回值类型和参数列表必须相同,权限可以不同。如上面示例中,B和C都覆写了A的funcA_v1()。下面的例子说明了这一点:
-
虚析构函数
《Effective C++》第三版,Item 07:为多态基类声明virtual析构函数。 当一个派生类对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁。所以上述的类设计其实有错误,带多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。 在接下来的示例中,我们将加上虚析构函数。
-
-
多重继承
-
多重继承
如下是一个简单的继承关系,class C 同时继承自 A 和 B:class A { public: int i_a; void funcA1() {} virtual ~A() {} }; class B { public: int i_b; void funcB1() {} virtual ~B() {}; }; class C :public A, public B { public: int i_c; virtual ~C() {} };
类A 和 类B 的内存布局如同1.2 , 类 C 的内存布局如下:
可见,派生类C中依其继承的基类的顺序,存放了各个基类subobject及各自的vptr,然后才是Class C自己的数据成员。需要解释上图中的thunk:Thunk解释:所谓thunk是一小段assembly代码,用来(1)以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2指针调用Derived destructor,其相关的thunk可能看起来是下面这个样子:
//虚拟C++代码 pbase2_dtor_thunk: this += sizeof( base1 ); Derived::~Derived( this );
根据上面的解释,经由class A的指针调用C的析构函数,其offset等于0;而经由class B调用C的析构函数,其offset等于8,如同上图所示:this-=8。
同时也可以想到,随着base class的数量增多,派生类里也会首先顺序存放各个基类subobject。而派生类中也会记录其到各个base subobject的offset。如下图是类D同时继承类A、B、C:
-
菱形继承
如上图是一个菱形继承的示意图,类B和C均继承自类A,类D同时继承类B和C,代码如下:class A { public: int i_a; virtual ~A() {} }; class B :public A { public: int i_b; virtual ~B() {}; }; class C :public A { public: int i_c; virtual ~C() {} }; class D :public B, public C { public: int i_d; virtual ~D() {} };
类A 的内存布局很简单,如 1.2 。类B 和 C 的内存布局如 2.2 。类 D 的内存布局如下:
如上图,D中依次存放基类B subobject和基类C subobject。其中B和C中均存放一份class A subobject。-
虚拟继承
从菱形继承的most-derived class(即3.2.中的class D)的内存布局可以看出,subobject A有两份,所以A的data member也存了两份,但实际上对于D而言,只需要有一份subobject A即够了。菱形继承不仅浪费存储空间,而且造成了数据访问的二义性。虚拟继承可以很好地解决这个问题。同样以3.2.中的继承关系为例,不过这次我们B和C对A的继承都加上了关键字virtual。
class A { public: int i_a; virtual ~A() {} }; class B :virtual public A { public: int i_b; virtual ~B() {}; }; class C :virtual public A { public: int i_c; virtual ~C() {} }; class D :public B, public C { public: int i_d; virtual ~D() {} };
接下来看看各个类的内存布局。
A的内存布局同1.2。类B和C的内存布局如2.2?是吗?不是!如下图:
可以看到,class B中有两个虚指针:第一个指向B自己的虚表,第二个指向虚基类A的虚表。而且,从布局上看,class B的部分要放在前面,虚基类A的部分放在后面。在class B中虚基类A的成分相对内存起始处的偏移offset等于class B的大小(8字节)。C的内存布局和B类似。
这个布局与之前的不一样:为什么基类subobject反而放到后面了?Class如果内含一个或多个virtual base subobjects,将被分割成两部分:一个不变区域和一个共享区域。不变区域中的数据,不管后继如何衍化,总有固定的offset(从object的开头算起),所以这一部分可以直接存取。而共享区域所表现的就是virtual base class subobject。这部分数据的位置会因为每次的派生操作而发生变化,所以它们只可以被间接存取。
接下来看class D的内存布局:直接的基类B和C按照声明的继承顺序,在D的内存中顺序安放。紧接着是D的data member。然后是共享区域virtual base class A。
-
总结
可以看到,C++类在有无继承、有无虚函数、有无多重继承或者虚继承时,其内存布局大不一样,多重继承或者菱形继承下,内存布局甚至很复杂。大致理清之后,可以对C++类的内存布局有个清晰认识。
参考链接