C/C++知识点之C++语言学习(十四)——C++类成员函数调用分析
小标 2019-04-22 来源 : 阅读 1042 评论 0

摘要:本文主要向大家介绍了C/C++知识点之C++语言学习(十四)——C++类成员函数调用分析,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。

本文主要向大家介绍了C/C++知识点之C++语言学习(十四)——C++类成员函数调用分析,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助。


一、C++成员函数


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


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


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<<endl;
    cout <<&Parent::print<<endl;
    cout <<&Parent::sayHello<<endl;
    cout <<&Parent::func<<endl;

    return 0;
}


上述代码中,打印出的所有的成员函数的地址为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 <<ret << endl;
        return ret;
    }
    void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    int m_i;
    int m_j;
};

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent parent(100,200);
    ChildA childA(1,2,3.14);
    Parent* pTestA = &childA;
    typedef void (Parent::*pPrintFunc)();
    pPrintFunc pPrint = &Parent::print;
    typedef double (Parent::*pSumFunc)();
    pSumFunc pSum = &Parent::sum;
    typedef void (Parent::*pDisplayFunc)();
    pDisplayFunc pDisplay = &Parent::display;

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);
    //不能将派生类的成员函数指针赋值给基类的函数指针
    //pPrint = &ChildA::print;//error
    //可以将基类的成员函数指针赋值给派生类
    void (ChildA::*pChildPrintFunc)() = pPrint;
    (childA.*pChildPrintFunc)();//Parent::print
    void (*p)() = reinterpret_cast(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 <<ret << endl;
        return ret;
    }
    void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    int m_i;
    int m_j;
};

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent parent(100,200);
    ChildA childA(1,2,3.14);
    Parent* pTestA = &childA;
    typedef void (Parent::*pPrintFunc)();
    pPrintFunc pPrint = &Parent::print;
    typedef double (Parent::*pSumFunc)();
    pSumFunc pSum = &Parent::sum;
    typedef void (Parent::*pDisplayFunc)();
    pDisplayFunc pDisplay = &Parent::display;

    printf("0x%X\n",pPrint);
    printf("0x%X\n",pSum);
    printf("0x%X\n",pDisplay);
    //绑定类对象进行调用
    (pTestA->*pPrint)();
    (pTestA->*pSum)();
    (pTestA->*pDisplay)();
   <sp    

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标编程语言C/C+频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程