《深入探索C++对象模型》笔记 Chapter4 成员函数

《深入探索C++对象模型》笔记 Chapter2 构造函数
《深入探索C++对象模型》笔记 Chapter3 成员变量
《深入探索C++对象模型》笔记 Chapter4 成员函数

第4章 成员函数

4.1 总述

非静态成员函数

C++的设计准则之一就是,非静态成员函数至少必须要和普通函数有相同的效率。

编译器会将成员函数转换为了对等的普通函数。转换步骤如下:

  • 改写函数原型(signature),传入参数添加一个this指针
  • 将对成员变量(非静态)的操作改为通过this指针来存取
  • 对成员函数的函数名称做命名重整(mangling)

上述所说的 mangling 过程,通常函数名会被加上class名称的前缀,以及参数类型的后缀,从而确保每个函数名都是独一无二的。

虚函数

//虚函数调用
ptr->func();
//转为为
(*ptr->vptr[1])(ptr);

以上的转换,vptr表示指向虚函数表的指针;1是虚函数表中该函数的索引值,和func()函数关联;函数参数中的ptr表示this指针。

静态成员函数

首先来看看为什么需要静态成员函数。

在引入静态成员函数之前,C++规定所有成员函数都必须由对象来调用,然而有些成员函数不对成员变量操作,也就没必要传入this指针,而C++并不能辨识这种情况。

于是,如果将静态成员变量声明为private,就必须提供成员函数来调用它,也就必须要实例化一个对象才能调用,以至出现了以下奇葩的写法:
((ClassName*) 0 ) -> get_staticmember_func();
将0强转成class指针,然后调用成员函数返回一个静态成员变量。

而有了静态成员函数,才解决了上述所说的问题。静态成员函数的主要特性是没有this指针,以此延伸出了其他特性:

  • 不能存取非静态成员
  • 不能被声明为 const volatile virtual
  • 不需要经由对象才能被调用

由于静态成员函数没有this指针,所以它在内存上的存储类似于普通函数。

4.2 虚函数

在C++中,多态表示以一个 public base class 的指针(或 reference),寻址出一个 derived class object 的意思。

单继承

《深入探索C++对象模型》笔记 Chapter4 成员函数_第1张图片
单继承内存模型

如上图, Point3d 继承 Point2d , Point2d 继承 Point 。

virtual table 在编译期就已经确定,virtual table 的每一个函数地址称之为一个 slot ,派生类定义虚函数会 overriding 基类相应函数的 slot 。这样 ptr->z() ,不管 ptr 指向的对象是基类还是派生类,它调用 z() 时函数地址一定是放在第四个 slot ,于是编译器转换代码为 (*ptr->vptr[4])(ptr)

对于纯虚函数,slot 里放置的是 pure_virtual_called() 函数,这个函数如果被意外调用,通常会结束掉这个程序。可以利用这个特性做运行期异常处理。

多继承

《深入探索C++对象模型》笔记 Chapter4 成员函数_第2张图片
多继承内存模型

虚继承

《深入探索C++对象模型》笔记 Chapter4 成员函数_第3张图片
虚继承内存模型

4.3 效率

经过测试,普通函数、非静态成员函数、静态成员函数的效率很相近。inline成员函数效率惊人。

4.4 指向成员函数的指针

A::* 意为指向类A成员的指针,我们可以用 void (A::*pmf)() = &A::func 表示指向A中 func() 成员函数的指针。对于非虚函数,这个指针指向一个函数地址,对于虚函数,由于地址在编译时期是未知的,所以存放的是该函数在虚函数表中的索引值。(个人以为,此时再说 pmf 是个指针就比较牵强了,它只是个记录函数相对位置的int值)
于是,(ptr->*pmf)() 就会被编译器翻译为 (*ptr->vptr[(int)pmf])(ptr) 。可以写个demo做个验证:

#include 
#include 
using namespace std;

class A {
    public:
    void common(){}
    virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
};
class B :public A{
    public:
    virtual void foo() { printf("B::foo(): this = 0x%p\n", this); }
    virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
    virtual void foo(int i) { printf("B::bar(): this = 0x%p\n", this); }
};
void (A::*pafoo)() = &A::foo;  
void (B::*pbfoo)() = &B::foo;   
void (B::*pbbar)() = &B::bar;
void (A::*pcommon)() = &A::common;
int main(){
    A* a = new A;
    B* b = new B;
    printf("A::commont: %x\n",pcommon);
    printf("A::foo: %x\n",pafoo);
    printf("B::foo: %x\n",pbfoo);
    printf("B::bar: %x\n",pbbar);
}

输出如下:

A::commont: 31425c9e
A::foo: 1
B::foo: 1
B::bar: 9

至于为什么索引值从1开始,书上没有说,不过想来和 3.6指向成员函数的指针 所说的类似,为了区分指向成员函数的空指针。

而索引值按8递增,那是因为我的云主机是64位,一个指针8字节。

单继承的情况,一个索引值就足够调用不同虚函数了,但是考虑到多继承呢?一个派生类继承自A和B,调用它的虚函数时,this指针作为传入参数可能是A,也可能是B,所以多继承还需要考虑this指针的偏移。我本来想仿照此篇博客 C/C++杂记:深入理解数据成员指针、函数成员指针 打印出this指针的偏移,但是事与愿违,偏移值始终为0,也只能归因于不同编译器的实现方式不同了。

4.5 内联函数

inline关键字并不能强迫将任何函数都变成 inline ,而是给编译器一个建议。至于编译器是否采纳这个建议,需要一系列复杂的测试,包括计算赋值、调用函数、调用虚函数的次数,每种操作都会有一个权值,这些操作与权值的乘积之和就是函数的复杂度。

对于inline函数,如果传入的参数是变量或者常量,那么直接对函数体内代码执行参数替换就可以了,但是如果传入的参数是个表达式呢?难道函数体内的代码每次碰到这个表达式都要算一遍?显示编译器不会这么傻。针对这种情况,编译器会产生一个临时对象,以避免重复求值。

再考虑一种情况,如果inline函数中有局部变量会怎么样?如果直接把代码扩展到调用inline函数的函数,万一后者有同名的变量呢?所以需要把局部变量放在封闭区段中,让它们有自己的作用域。这个过程依旧会产生局部变量。

可以看到,局部变量和表达式参数都会使inline函数产生临时对象,所以使用inline不当会产生大量临时对象反而降低效率。

参考文章

C/C++杂记:深入理解数据成员指针、函数成员指针

你可能感兴趣的:(《深入探索C++对象模型》笔记 Chapter4 成员函数)