c++中使用空指针调用成员函数的理解

使用空指针调用成员函数会如何?

举个例子:base是基类,里面有两个函数:non-virtual func2 以及 virtual func1;
derived是派生类,使用public继承自base,里面有四个函数:virtual func1,non-virtual func3,non-virtual func4以及static non-virtual func5,特别注意:func4调用了基类的函数func2。具体如下代码:

class base
{
public:
    base(string s = "base") : _s(s) {}
    virtual string func1(string t) {
        return (_s + t);
    }
    string func2(string t)  { return (_s + t); }
private:
    string _s;
};

class derived : public base
{
public:
    virtual string func1(string t) {
        return t;
    }
    string func3(string t) {
        return t;
    }
    string func4(string t) {
        return func2(t);
    }
    static string func5(string t) {
        return t;
    }
};

在这中继承关系的基础上,我们来看看各种调用的结果:

#include <bits/stdc++.h>
using namespace std;
int main()
{
    derived* D = new derived();
    base* B1 = new base();
    base* B2 = D;
    derived* DNull = NULL;
    string t = "hello";
    cout << B1->func1(t) << endl;  //basehello
    cout << B2->func1(t) << endl;   //hello
    cout << D->func1(t) << endl;    //hello
    cout << D->func3(t) << endl;    //hello
    cout << D->func4(t) << endl;    //basehello
    //cout << DNull->func1(t) << endl; //错误
    cout << DNull->func3(t) << endl;    //hello
    //cout << DNull->func4(t) << endl; //错误
    cout << DNull->func5(t) << endl;    //hello
    delete B1;
    delete D;
    return 0;
}

在分析结果之前,有几个概念需要分清楚:

  1. 静态类型:声明时所采用的类型或者表达式生成的类型,在编译期就确定了。
  2. 动态类型:实际内存中的对象类型,在运行期确定。
  3. 静态绑定:绑定静态类型,发生在编译期。
  4. 动态类型:绑定动态类型,发生在运行期,通常通过指针和引用来调用虚函数是动态绑定,其他一般为静态绑定。
    有了基本的了解,再来看上面的例子:
    cout << B1->func1(t) << endl;  //basehello
    cout << B2->func1(t) << endl;   //hello

这两行的输出是比较显然的,需要注意的是B1的静态类型和动态类型都是base,采用的是静态绑定,而B2调用的是虚函数,B2的静态类型是base,但是动态类型却是derived。

    cout << D->func1(t) << endl;    //hello
    cout << D->func3(t) << endl;    //hello
    cout << D->func4(t) << endl;    //basehello
    cout << D->func5(t) << endl;    //hello

这几行也都比较好理解,都是通过derived的指针对象D调用函数,只不过函数不同,有的是虚函数,有的是非虚函数,还有的是静态函数。
接下来是重点,使用空指针调用函数

    //cout << DNull->func1(t) << endl; //错误
    cout << DNull->func3(t) << endl;    //hello
    //cout << DNull->func4(t) << endl; //错误
    cout << DNull->func5(t) << endl;    //hello

第一行使用DNull调用虚函数func1 为什么会出错呢?我来谈谈我的理解。一个指针ptr调用虚函数在内部会被转化为:

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

其中vptr是虚表指针指向virtual table; 1是virtual table slot的索引值,关联到调用的虚函数;第二个ptr表示this指针。关于函数语意,参考我这篇读书笔记。这里显式使用this指针,而ptr本身是一个空指针,这就是矛盾的,因此我觉得这个原因导致其出错。在知乎上也有这个问题的讨论,见链接为什么C++调用空指针对象的成员函数可以运行通过?

然后再看第二个DNull->func3(t) 的输出:可以正确输出hello。我们从Function语义学来看,一个非静态成员函数通常的内部转化有3点,分别是:
1.添加this指针;
2.Name Mangling(函数名称的特殊处理);
3.NRV(Named Return Value)优化;
func3经过内部转化可能变成下面的样子:

void func3_string(derived * const this, string t, string& result) {
    result.string::string(); //defalut ctor
    result = t;
}

转化之后的函数并没有对this指针有任何操作,因此使用空指针调用该函数可以有正确的结果。
再看第三个DNull->func4(t) ,而func4经过内部转化可能变成下面的样子:

void func3_string(derived * const this, string t, string& result){
    result.string::string(); //defalut ctor
    result = this->func4(t);
}

转化之后的函数显式调用了this指针,因此使用空指针调用该函数会出错。
最后一个 DNull->func5(t),使用空指针调用静态成员函数,静态成员函数没有this指针,故肯定能调用成功。

总结

我个人使用空指针调用成员函数的理解是:如果空指针调用成员函数没有调用this指针,就能成功调用,否则就会出错。

**

主要参考自侯捷老师的《深度探索C++对象模型》

**

猜你喜欢

转载自blog.csdn.net/qq_25467397/article/details/80177580
今日推荐