使用空指针调用成员函数会如何?
举个例子: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;
}
在分析结果之前,有几个概念需要分清楚:
- 静态类型:声明时所采用的类型或者表达式生成的类型,在编译期就确定了。
- 动态类型:实际内存中的对象类型,在运行期确定。
- 静态绑定:绑定静态类型,发生在编译期。
- 动态类型:绑定动态类型,发生在运行期,通常通过指针和引用来调用虚函数是动态绑定,其他一般为静态绑定。
有了基本的了解,再来看上面的例子:
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++对象模型》
**