深度探索C++对象模型-第四章

Function语意学

一 成员的各种调用方式

1 非静态成员函数

非静态成员函数采用隐式传入this指针的方式进行成员的调用。this指针指向对象中的成员。

class A{
public:
    A(const A &lhs){
        a = lhs.a;
        b = lhs.b;
        c = lhs.c;
    }
private:
    int a;
    int b;
    int c;
};

内部是通过this取得的内部成员。

A(const A &lhs){
    this.a = lhs.a;
    this.b = lhs.b;
    this.c = lhs.c;
}

二 虚拟成员函数

虚函数内部通过虚函数指针指向虚函数表调用内部的函数指针(虚函数是通过函数指针实现的)指向成员函数。

class A{
public:
    virtual int func(){ }
private:
    int a;
};

虚函数的调用将被转换为:

A *ptr = new A;
ptr->func();

(*ptr->vptr[1])(ptr);

vptr是虚表指针,指向虚函数表中索引为1的函数指针,并隐式传入this指针。

如果显式通过类名调用虚函数将会压制虚函数的多态效果

A::func();  //只会调用A的虚函数

1 静态成员函数

类的静态函数是不传入this指针的,所以很显然,类静态函数不能直接存取类中的非静态成员;静态函数不能被声明为const,volatile或者virtual;不需要经由类对象进行调用(但是可以通过对象进行调用)。

class A
{
public:
    A(){ }
    A(const A &a){ b = a.b; }
    static int Afunc(){ return a; }
private:
    static int a;
    int b;
};
int A::a = 100;
int main()
{
    cout << A::Afunc() << endl; //100
    cout << sizeof(A) << endl;  //4
    A a;
    cout << a.Afunc() << endl;  //100

    return 0;
}

如果对一个静态成员函数去地址,获得的是其在内存中的地址,由于静态成员函数没有this指针,所以其地址的类型并不是一个指向类成员函数的指针,而是一个非成员函数指针。

&A::Afunc();

会得到一个数值,类型是unsigned int(*)(), 而不是unsigned int (A::*)()

所以类静态成员函数可以当作回调函数来使用

2 虚拟成员函数

每一个类都有一个虚函数表,指向该类中有作用的虚函数的地址,内含该类中有作用的虚函数的地址,然后每一个对象有一个虚函数表指针,指向虚函数表。

虚函数在执行期如何找到正确的虚函数实例?

首先需要两个条件:

  1. 虚函数指针所指对象的真实类型,这可以让我们选择正确的虚函数实例
  2. 虚函数实例的位置,这样我们可以正确调用虚函数

在实现上我们是通过虚表指针指向虚函数表的方式,来找到虚函数,这些工作都是在编译期完成,在执行期只要在特定的虚表中激活虚函数即可。

一个类中有一个虚函数表,每一个表格内涵其对应的类对象中所有激活的虚函数的函数实例的地址,这些激活的虚函数包括:

  • 这个类定义的函数实例
  • 继承自基类的函数实例
  • 纯虚函数的函数实例

如果某个类中有从基类继承而来的虚函数并进行了重写的话,在基类和派生类的虚函数表中,这个虚函数通常是放在虚函数表的同一个位置,这样在编译期我们不需要知道指针指向对象的真正类型,经由ptr指向固定的地方即可,运行期找到真正的虚函数实例就行:

ptr->func();    //假设放在虚函数的第4个槽中
//转换为以下代码
(*ptr->vptr[4])(ptr);   //只需要知道指向第4个位置就行,具体第4个位置是基类还是派生类的虚函数,在执行器判定

2.1 多重继承下的虚函数

多重继承下,不同的基类在派生类中的位置不相同

class A
{
public:
    virtual A *clone() const;
protected:
    int a;
};
class B
{
public:
    virtual B *clone() const;
protected:
    int b;
};
class C : public A, public B
{
public:
    virtual C *clone() const;
protected:
    int c;
};

C先继承的A,后继承的B,所以A的子对象在C对象的最前面,B的子对象在C对象中A子对象的后面,C本身的数据在最后面。当用A类型指针,指向C对象的时候,this指针不需要做偏移,但是当B类型指针指向C对象的时候,this指针需要做偏移,偏移sizeof(A)个字节。

2.2 虚拟继承下的虚函数

虚拟继承下,子类中有一个虚基类指针,指向自己的虚基类,同样子类也会继承基类的数据成员,但是当菱形继承发生的时候,最底层的子类中只会保留一份基类的数据成员。

class Base{ int a; };
class Base2{ int a2; };
class Derived : public virtual Base, public virtual Base2{ int b; };

int main()
{
    cout << sizeof(Base) << endl;   //8:4字节int+4字节vptr
    cout << sizeof(Derived) << endl;    //24:三个4字节int+4字节vptr+两个4字节虚基类指针

    return 0;
}

PS:菱形继承的内存:

class A{ int a; };
class B : virtual A{ int b; };
class C : virtual A{ int c; };
class D : public B, public C{ int d; };
int main()
{
    cout << sizeof(A) << endl;  //4(4字节int)
    cout << sizeof(B) << endl;  //12(两个4字节int,一个4字节vptr)
    cout << sizeof(C) << endl;  //12(两个4字节int,一个4字节vptr)
    cout << sizeof(D) << endl;  //24(四个4字节int,两个(B、C)4字节vptr)

    return 0;
}

三 函数的效能

类中的静态成员函数,非静态成员函数,和非成员函数在C++的实现中都可以被转化为相同的形式。所以效率是一致的。

在拥有虚函数后,类的构造函数,将会获得一个参数专门用于设定虚函数表指针,所以每多一层继承,就会多增加一个额外的虚函数表指针的设定

四 指向成员函数的指针

1 首先一个指向成员函数的指针是很好定义的,比普通的函数指针多了一个classname::,但是在指定函数指针的值的时候,需要加取地址符,不能直接用classname::funcname来指定成员函数指针的值。

#include <iostream>
using namespace std;

class A
{
public:
    void func1(){ cout << "func1" << endl; }
private:
    int a;
};
int main()
{
    void (A::*ptr)();   //设置一个类成员函数的函数指针
    ptr = &A::func1;        //指定值(一定要加取地址符)
    A a;
    (a.*ptr)();     //通过对象调用(也可以通过指针或引用)

    return 0;
}

使用一个函数指针,但是并不用于虚函数,多重继承,虚基类等情况的话,并不会比使用一个非成员函数指针的成本更高。

五 内联函数

内联函数效率较高(没有调用过程,编译期直接以函数内容取代调用的代码),当在类的内部使用内联函数进行private成员变量的存取的时候,可以如同public直接存取成员变量一般,拥有很高的效率,而且也兼顾了封装性。

处理内联函数一般有两个过程:

  1. 分析函数定义,一般情况下函数因为其复杂度或者构建问题,被判断为不可成为内联函数,它会被转为一个static函数,并在“被编译模块”内产生对应的函数定义。
  2. 真正的内联函数扩展操作是在调用的那一点上,这会带来参数的求值操作临时性对象的管理

5.1 形式参数

inline扩展期间,每一个形参都会被对应的实参所代替。如果实参是一个常量表达式,可以在替换之前完成求值操作,然后直接在替换的时候把常量绑上去。如果实参有求值操作(类似于a+1,a++),那么在扩展的时候不能像define(define就存在多次求值的问题,所以define定义的宏不能传入有求值操作的参数)一样简单的传入参数。

inline int min(int a, int b)
{
    return a < b ? a : b;
}

inline int func()
{
    int minval;
    int val1 = 1024;
    int val2 = 2048;

    /*(1)*/minval = min(val1, val2);
    /*(2)*/minval = min(1024, 2048);
    /*(3)*/minval = min(func1(), func1() + 1);
}

(1)会被扩展为:

minval = val1 < val2 ? val1 : val2;

(2)会直接为常量:

minval = 1024;

(3)为了防止多次求值,会引入临时对象:

minval = (t1 = func1()), (t2 = func1() + 1), t1 < t2 ? t1 : t2;

如果内联函数中有局部变量的话,局部变量会被提取出来。

inline int min(int a, int b)    //内联函数
{
    int ret;
    ret = a < b ? a : b;
    return ret;
}

int fimc()  //调用
{
    int minval;
    minval = min(val1, val2);
    ...
}

转化为:

int fimc()  //调用
{
    int minval;
    int __min__lv_minval;   //一个专有名称
    minval = (__min_lv_minval = 
              val1 < val2 ? val1 : val2),__min__lv_minval;
    ...
}

内联函数中的局部变量再加上有副作用的函数,可能会导致大连临时性对象的产生。这就导致如果内联函数多次调用可能会导致大量的扩展码被产生,使程序大小暴涨。

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80500324