C++语言学习(十四)——C++类成员函数调用分析

C++语言学习(十四)——C++类成员函数调用分析

一、C++成员函数

1、C++成员函数的编译

C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。函数重命名的过程通过一个特殊的Name Mangling(名字编码)算法来实现。Name Mangling算法是一种可逆的算法,既可以通过现有函数名计算出新函数名,也可以通过新函数名逆向推导出原有函数名。
Name Mangling算法可以确保新函数名的唯一性,只要命名空间、所属的类、参数签名等有一个不同,那么产生的新函数名也不同。
不同的编译器有不同的 Name Mangling 算法,产生的函数名也不一样。

成都创新互联主要从事网站设计、成都做网站、网页设计、企业做网站、公司建网站等业务。立足成都服务辰溪,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:13518219792

2、this指针

this指针属性如下:
A、名称属性:标识符this表示。
B、类型属性:classname const
C、值属性:表示当前调用该函数对象的首地址。
D、作用域:this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。
E、链接属性:在类作用域中,不同类的非静态成员函数中,this指针变量的链接属性是内部的,但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个。
F、存储类型:this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。
this指针并不是对象的一部分,this指针所占的内存大小是不会反映在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。
类的成员函数默认第一个参数为T
const register this。
this在成员函数的开始执行前构造,在成员函数执行结束后清除。

二、C++成员函数指针

1、C++成员函数指针简介

C++语言规定,成员函数指针具有contravariance特性,即基类的成员函数指针可以赋值给派生类的成员函数指针,C++语言提供了默认的转换方式,但反过来不行。
C++编译器在代码编译阶段会对类对象调用的成员函数进行静态绑定(虚函数进行动态绑定),类成员函数的地址在代码编译时就确定,类成员函数地址可以使用成员函数指针进行保存。
成员函数指针定义语法如下:

ReturnType (ClassName::* pointerName) (ArgumentLList);
ReturnType:成员函数返回类型
ClassName: 成员函数所属类的名称
Argument_List: 成员函数参数列表
pointerName:指针名称
class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

成员函数指针语法极其严格:
A、不能使用括号:例如&(Test::print)不对。
B、 必须有限定符:例如&print不对,即使在类ClassName作用域内也不行。
C、必须使用取地址符号:直接写Test::print不行,必须写:&Test::print。
Test类的成员函数print的函数指针声明如下:
void (Test::*pFun)();
初始化如下:
pFunc = &Test::print;
Test类的成员函数print的函数指针声明及初始化如下:
void (Test::* pFunc)() = &Test::print;
通常,为了简化代码,使用typedef关键字。

typedef void (Test::*pFunc)();
pFunc p = &Test::print;

可以通过函数指针调用成员函数,示例代码如下:

#include 

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

int main(int argc, char *argv[])
{
    void (Test::* pFunc)() = &Test::print;
    Test test;
    //通过对象调用成员函数
    (test.*pFunc)();//Test::print
    Test* pTest = &test;
    //通过指针调用成员函数
    (pTest->*pFunc)();//Test::print
    //pFunc();//error
    //error: must use '.*' or '->*' to call pointer-to-member
    //function in 'pFunc (...)', e.g. '(... ->* pFunc) (...)'

    return 0;
}

上述代码中,.*pFunc将pFunc绑定到对象test,->*pFunc绑定pFunc到pTest指针所指向的对象。
成员函数指针不是常规指针(保存的是某个确切地址),成员函数指针保存的是成员函数在类布局中的相对地址。

2、C++成员函数地址

C++成员函数使用thiscall函数调用约定。C++静态成员函数、普通成员函数的函数地址在代码区,虚成员函数地址是一个相对地址。

#include 

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual void sayHello()
    {
        cout << "Parent::sayHello()" << endl;
    }
    virtual void func()
    {
        cout << "Parent::func()" << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
    static void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int v)
    {
        return m_i + m_j + v;
    }
protected:
    int m_i;
    int m_j;
};

int main(int argc, char *argv[])
{
    cout <<&Parent::display<

上述代码中,打印出的所有的成员函数的地址为1。原因在于输出操作符<<没有对C++成员函数指针类型进行重载,C++编译器将C++成员函数指针类型转换为bool类型进行了输出,所以所有的输出为1。因此,C++成员函数地址进行打印时不能使用cout,可以用printf输出,因为printf可以接收任意类型的参数,包括__thiscall类型。

#include 

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual void sayHello()
    {
        cout << "Parent::sayHello()" << endl;
    }
    virtual void func()
    {
        cout << "Parent::func()" << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
    static void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int v)
    {
        return m_i + m_j + v;
    }
protected:
    int m_i;
    int m_j;
};

int main(int argc, char *argv[])
{
    //静态成员函数
    cout << "static member function addree:" << endl;
    printf("0x%p\n", &Parent::display);
    printf("0x%p\n", Parent::display);
    //普通成员函数
    cout << "normal member function addree:" << endl;
    printf("0x%p\n", &Parent::add);
    cout << "virtual member function addree:" << endl;
    //虚成员函数
    printf("%d\n", &Parent::print);//1
    printf("%d\n", &Parent::sayHello);//5
    printf("%d\n", &Parent::func);//9

    return 0;
}

3、C++编译器成员函数指针的实现

C++编译器要实现成员函数指针,必须解决下列问题:
A、成员函数是不是虚函数。
B、成员 函数运行时,需不需要调整this指针,如何调整。
不需要调整this指针的情况如下:
A、继承树最顶层的类。
B、单继承,若所有类都不含有虚函数,那么继承树上所有类都不需要调整this指针。
C、单继承,若最顶层的类含有虚函数,那么继承树上所有类都不需要调整this指针。
可能需要进行this指针调整的情况如下:
A、多继承
B、单继承,最顶的base class不含virtual function,但继承类含虚函数,继承类可能需要进行this指针调整。
Microsoft VC对C++成员函数指针的实现采用的是Microsoft一贯使用的Thunk技术。Microsoft将成员函数指针分为两种:

struct pmf_type1{
    void* vcall_addr;
};

struct pmf_type2{
    void* vcall_addr;
    int  delta;  //调整this指针用
};

vcall_addr是Microsoft 的Thunk技术核心所在。vcall_addr是一个指针,隐藏了它所指的函数是虚拟函数还是普通函数的区别。如果所指的成员函数是一个普通成员函数,vcall_addr是成员函数的函数地址。如果所指的成员函数是虚成员函数,那么vcall_addr指向一小段代码,这段代码会根据this指针和虚函数索引值寻找出真正的函数地址,然后跳转到真实的函数地址处执行。
Microsoft根据情况选用函数指针结构表示成员函数指针,使用Thunk技术(vcall_addr)实现虚拟函数/非虚拟函数的自适应,在必要的时候进行this指针调整(使用delta)。
GCC对于成员函数指针统一使用下面的结构进行表示:

struct        
{    
    void* __pfn;  //函数地址,或者是虚拟函数的index    
    long __delta; // offset, 用来进行this指针调整   
};

不管是普通成员函数,还是虚成员函数,信息都记录在__pfn。一般来说因为对齐的关系,函数地址都至少是4字节对齐的。即函数地址的最低位两个bit总是0。 GCC充分利用了这两个bit。如果是普通的函数,__pfn记录函数的真实地址,最低位两个bit就是全0,如果是虚成员函数,最后两个bit不是0,剩下的30bit就是虚成员函数在函数表中的索引值。
GCC先取出函数地址最低位两个bit看看是不是0,若是0就使用地址直接进行函数调用。若不是0,就取出前面30位包含的虚函数索引,通过计算得到真正的函数地址,再进行函数调用。
GCC和Microsoft对成员函数指针实现最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚函数,开销自然要比Microsoft的实现要大一些。
在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta变量,如果不需要调整,__delta=0
GCC的实现比Microsoft简单,在所有场合其实现方式都是一样的。

4、C++成员函数指针的限制

C++语言的规定,基类的成员函数指针可以赋值给派生类的成员函数指针,不允许继承类的成员函数指针赋值给基类成员函数指针。
 C++规定编译器必须提供一个从基类成员函数指针到继承类成员函数指针的默认转换。C++编译器提供的默认转换最关键的就是this指针调整。
因此,一般情况下不要将继承类的成员函数指针赋值给基类成员函数指针。不同C++编译器可能有不同的表现。
解决方案:
A、不要使用static_cast将继承类的成员函数指针赋值给基类成员函数指针,如果一定要使用,首先确定没有问题。
B、如果一定要使用static_cast,注意不要使用多继承。
C、如果一定要使用多继承的话,不要把一个基类的成员函数指针赋值给另一个基类的函数指针。
D、单继承要么全部不使用虚函数,要么全部使用虚函数。不要使用非虚基类,却让子类包含虚函数。

#include 
#include 

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <(pPrint);
    p();

    return 0;
}

5、静态成员函数指针

对于静态成员函数,函数体内部没有this指针,与类的其它成员函数共享类的命名空间,但静态成员函数并不是类的一部分,静态成员函数与常规的全局函数一样,成员函数指针的语法针对静态成员函数并不成立。
静态成员函数的函数指针定义语法如下:

ReturnType (* pointerName) (ArgumentLList);
ReturnType:成员函数返回类型
Argument_List: 成员函数参数列表
pointerName:指针名称

静态成员函数的函数指针的使用与全局函数相同,但静态成员函数指针保存的仍旧是个相对地址。

#include 

using namespace std;

class Test
{
public:
    static void print()
    {
        cout << "Test::print" << endl;
    }
};

int main(int argc, char *argv[])
{
    void (* pFunc)() = &Test::print;
    cout << pFunc << endl;//1
    //直接调用
    pFunc();//Test::print
    (*pFunc)();//Test::print
    Test test;
    //(test.*pFunc)();//error
    Test* pTest = &test;
    //(pTest->*pFunc)();//error

    return 0;
}

6、普通成员函数指针

非静态、非虚的普通成员函数指针不能直接调用,必须绑定一个类对象。
普通函数指针的值指向代码区中的函数地址。如果强制转换为普通函数指针后调用,成员函数内部this指针访问的成员变量将是垃圾值。

#include 
#include 

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <*pPrint)();
    (pTestA->*pSum)();
    (pTestA->*pDisplay)();
    //强制转换为普通函数指针
    void (*p)() = reinterpret_cast(pPrint);
    p();//打印随机值

    return 0;
}

7、虚成员函数指针

C++通过虚函数提供了运行时多态特性,编译器通常使用虚函数表实现虚函数。

#include 
#include 

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <*pPrint)();//ChildA::print,多态
    (pTestB->*pPrint)();//ChildB::print,多态

    typedef double (Parent::*pVSumFunc)();
    pVSumFunc pSum = &Parent::sum;

    (parent.*pSum)();//Parent::sum
    (pTestA->*pSum)();//ChildA::sum,多态
    (pTestB->*pSum)();//ChildB::sum,多态

    typedef void (Parent::*pVDisplayFunc)();
    pVDisplayFunc pDisplay = &Parent::display;

    (parent.*pDisplay)();//Parent::display
    (pTestA->*pDisplay)();//ChildA::display,多态
    (pTestB->*pDisplay)();//ChildB::display,多态

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);

    return 0;
}

虚成员函数指针的值是一个相对地址,表示虚函数在虚函数表中,离表头的偏移量+1。
当一个对象调用虚函数时,首先通过获取指向虚函数表指针的值得到虚函数表的地址,然后将虚函数表的地址加上虚函数离表头的偏移量即为虚函数的地址。 

8、成员函数指针示例

成员函数指针的一个重要应用是根据输入来生成响应事件,使用不同的处理函数来处理不同的输入。

#include 
#include 
#include 

using namespace std;

//虚拟打印机
class Printer {
public:
    //复制文件
    void Copy(char *buff, const char *source)
    {
        strcpy(buff, source);
    }
    //追加文件
    void Append(char *buff, const char *source)
    {
        strcat(buff, source);
    }
};

//菜单中两个可供选择的命令
enum OPTIONS { COPY, APPEND };

//成员函数指针
typedef void(Printer::*PTR) (char*, const char*);

void working(OPTIONS option, Printer *machine,
             char *buff, const char *infostr)
{
    // 指针数组
    PTR pmf[2] = { &Printer::Copy, &Printer::Append };
    switch (option)
    {
    case COPY:
        (machine->*pmf[COPY])(buff, infostr);
        break;
    case APPEND:
        (machine->*pmf[APPEND])(buff, infostr);
        break;
    }
}

int main() {
    OPTIONS option;
    Printer machine;
    char buff[40];

    working(COPY, &machine, buff, "Strings ");
    working(APPEND, &machine, buff, "are concatenated!");

    std::cout << buff << std::endl;
}

// Output:
// Strings are concatenated!

三、C++类成员函数的调用分析

1、成员函数调用简介

类中的成员函数存在于代码段。调用成员函数时,类对象的地址作为参数隐式传递给成员函数,成员函数通过对象地址隐式访问成员变量,C++语法隐藏了对象地址的传递过程。由于类成员函数内部有一个this指针,类成员函数的this指针会被调用的类对象地址赋值。因此,如果类成员函数中没有使用this指针访问成员,则类指针为NULL时仍然可以成功对该成员函数进行调用。static成员函数作为一种特殊的成员函数,函数内部不存在this指针,因此类指针为NULL时同样可以成功对静态成员函数进行调用。

#include 
#include 

using namespace std;

namespace Core {

class Test
{
public:
    Test(int i)
    {
        this->i = i;
    }
    void print()
    {
        cout << "i = " << i << endl;
    }
    void sayHello()
    {
        cout << "Hello,Test." << endl;
    }
    static void printHello()
    {
        cout << "Hello,Test." << endl;
    }
private:
    int i;
};

}

int main()
{
    using namespace Core;
    Core::Test* ptest = NULL;
    ptest->sayHello();//Hello,Test.
    ptest->printHello();
    //定义函数指针类型
    typedef void (Test::*pFunc)();
    //获取类的成员函数地址
    pFunc p = &Test::print;
    Test test(100);
    //调用成员函数
    (test.*p)();//i = 100

    return 0;
}

2、普通成员函数调用机制分析

普通成员函数通过函数地址直接调用。

#include 

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

int main()
{
    Test test;
    Test* p = &test;
    p->print();
}

对于非虚、非静态成员函数的调用,如p->print(),C++编译器会生成如下代码:

Test* const this = p;
void Test::print(Test* const this)
{
    cout << "Test::print" << endl;
}

不管指针p是任何值,包括NULL,函数Test::print()都可以被调用,p被作为this指针并当作参数传递给print函数。因此,当传入print函数体内的p指针为NULL时,只要不对p指针进行解引用,函数就能正常调用而不发生异常退出。

#include 

using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
        sayHello();
    }
    void sayHello()
    {
        cout << "Test::sayHello" << endl;
    }
};

int main()
{
    Test* p = NULL;
    p->print();
}

// output:
// Test::print
// Test::sayHello

3、静态成员函数调用机制分析

静态成员函数通过函数地址进行调用,其调用方式同全局函数。

4、虚成员函数调用机制分析

虚成员函数的调用涉及运行时多态。
当一个对象调用虚函数时,首先通过运行时对象获取指向虚函数表指针的值得到虚函数表的地址,然后将虚函数表的地址加上虚函数离表头的偏移量即为虚函数的地址。 基类对象内部的虚函数表指针指向基类的虚函数表,派生类对象的虚函数表指针指向派生类的虚函数表,确保运行时对象调用正确的虚函数。


当前标题:C++语言学习(十四)——C++类成员函数调用分析
转载源于:http://scyanting.com/article/jccpse.html