现在的位置: 首页 > 综合 > 正文

Inside C++ Object Model读书笔记:Chapter 4

2018年03月31日 ⁄ 综合 ⁄ 共 2873字 ⁄ 字号 评论关闭
第四章 函数语义学
C++支持三种函数:静态,非静态和虚函数(附注:0x还支持lambda函数)。静态函数无法访问类中的非静态数据,也不能被定义为const。

4.1 函数调用方式之间的区别
非静态函数:非静态函数被设计为和C函数具有相近效率的调用模式。首先,它必须在参数列表中自动增加一个* const this或者const * const this作为第一个参数(附注:const int* p和int * const p之间的区别:前者可以指向任何一个int但是不能修改其值;后者只能指向一个特定的int但是却可以修改具体数值)。之后将函数内部所有默认引用对象中的变量改写为通过this引用的变量。最后通过一个变换规则,得到一个C++ized的函数名,将它改写为extern格式,使之在整个程序中有效。
改写时可能有NRV优化存在。
函数名称的变换:通过类的名称(附注:为了维护不同类中的相同函数)和参数名称(附注:为了维护重载)(附注:以及返回值,为了维护const int operator[]和int& operator[]之间的划分)
虚成员函数:将会被变换为虚表中的相应位置:
ptr->etwas();
将变为:
(*(ptr->vptr[??])(ptr)
其中vptr代表虚表指针,vptr可能名字会有所更改,因为一个复杂的派生类中可能有多个虚表。ptr是this指针。
注意对于(*ptr).etwas(),不会经过虚表,因为多态只对指针有效。这时inline函数可以被展开。
静态函数:静态函数不需要this指针,所以一个((classname*)0)->etwas()就足够访问了。因而静态函数不能直接调用类中的非静态函数,不能被声明为const,volatile,virtual,不需要一个对象就可以访问(虽然也可以通过对象访问)。静态函数被最终拖出对象,指向它的指针不包含对象成分。静态函数可以用来作为回调函数。(附注:记住STL中的mem_fun_ptr可以将一个成员函数“非静态化”)

4.2 虚成员函数
C++中,只有使用指针或者引用的对象才具有多态性。基类的指针能够指向派生类,这称作被动多态性。使用这样的指针时,称作主动多态性。使用RTTI技术,运行时基类指针所指向的具体对象也可以被识别出来。
保证多态性行为需要储存下面的数据:
1、基类指针所指向的实际对象类型。
2、实际对象所在的内存位置。
这样,保存的数据有:
1、表示对象类型的标识符。
2、一个指向虚函数表的指针。
3、对于每个对象,需要有一个内建指针指向虚函数表。
4、为了定位虚函数的地址,表中每个虚函数需要有一个固定的内存偏移。
每个类都有一个虚表。每个虚表都保存了虚函数的数据:
1、类内定义的函数对象,包括从基类重载的对象。
2、从基类中继承的函数对象,没有重载。
3、如果函数对象是纯虚的,则放置一个指向异常处理代码的函数。
每个虚函数都有一个相应的虚表中的位置。
对于派生类中的虚表:
1、可以直接继承基类的虚函数,这时派生类中的虚表的相应位置保存的是基类虚表中相应位置的函数指针。
2、重载虚函数,放入自己的指针。
3、自己导入新的虚函数,使得虚表增长。
编译时期不知道调用哪个虚函数,只能知道指向对象指针中vptr的位置,以及需要调用函数的在虚表中的索引。所以调用时是通过上述两个数据来处理的。
多重继承下,需要在运行时调整this指针的信息。
第一个问题是虚析构函数的调用问题。假定第二个基类指针指向派生类对象:
Base2* pbase2=new Derived;
则需要调整pbase2指向Derived对象的相应位置:
Base2 *pbase2=(new Derived) ? ptr_of_new_Derived+sizeof(Base1) : 0;
析构时:
delete pbase2;
则指针需要被调整,使得它指向整个Derived对象。这个在编译时期是不能确认的,因为pbase2指向的对象未知。
这时在运行时,需要修改this指针指向位置(因为要调用相应的虚析构函数),以便让类正确析构。C++采用的方法是,在相应的虚析构函数中加入一小块代码,称作thunk,使得this指针能正确指向相应的位置,然后再跳到析构操作代码。相应的虚表中的函数指针也要进行修改。
在这里,是对Derive对象中继承的虚~Base2前加上thunk。
thunk的缺点是,对于不同的基类在虚表中都要产生相应的析构函数thunk位置。
同样的,对于其他函数也要产生相应的thunk,以保证基类中的函数能够被正确调用。
第二个问题是在多重继承中,派生类对象需要访问基类的虚函数时,调用基类函数需要调整this指针指向派生类对象中的基类部分。
第三个问题是如果将指向派生类对象的指针赋给基类指针,则需要一个对指针的调整。有一些编译器将这个调整放在函数内部,将返回派生类对象指针和基类指针看作两个不同的函数。
对于虚继承,作者认为最好保证虚基类中没有任何非静态的数据成员。

4.3 函数效率
对于类的函数,非virtual的函数的效率和一般函数的效率几乎相同。(附注:这是由于非virtual函数只是比一般函数多一个*this指针的传递造成的,传递*this只消耗一个入栈命令)但是使用了virtual则需要受到抽象惩罚,包括查虚表和this指针的变换。(附注:所以说那些ACM的家伙叫唤说C++慢主要是他们脑子里面有着太多的抽象)

4.4 指向成员的函数
第三章中,可以看出来指向成员数据的指针是类中相应成员的所在地址偏移(+1)。从一个非静态类函数中得到的指针,如果不是虚函数,那么是这个函数在内存中的具体位置。这个值往往是不完整的,需要指出类的类型以传递正确的*this。对于静态成员,不需要传递指针,所以静态成员函数和一般的函数没有太大的区别。

指向虚成员的函数指针将被编译器换算成虚表的相应位置。为了正确支持多重继承和虚继承,对于成员指针有如下附加结构:
struct __mptr {
    int delta;
    int index;
    union {
        ptrotofunc faddr;
        int v_offset;
    };
};
index代指虚表中的索引,对于非虚成员函数index=-1。而另外一个实现是,使用thunk来解决问题:对于调用非虚函数,直接取地址;虚函数则被指向一个thunk,thunk中的代码检索虚表并且调用相应的函数。
delta代表this指针的正确位置,当使用虚继承的时候。
许多编译器做出了优化,M$版本:
1、单继承中只保留一个指针,指向thunk或者函数位置。
2、多继承时保存一个附加的delta。
3、虚继承的时候保存四个变量。

指向成员指针的效率
(附注:使我感到困惑的是,为什么不用汇编来说话?)
抽象惩罚并不太严重。

内联函数
编译器首先分析函数是否具有“内在的内联特性”。之后函数在调用处展开,需要考虑临时变量的问题。
每个函数在展开时,形参被替换成实参,如果有本地变量的话,可能还要引进临时变量。也有可能直接根据return从句来将数据代换。

抱歉!评论已关闭.