为什么C++调用空指针对象的成员函数可以运行通过

先看一段代码:

#include <iostream>
using namespace std; 

class B {
public:
   void foo() { cout << "B foo " << endl; }
   void pp() { cout << "B pp" << endl; }
   void FunctionB() { cout << "funB" << endl; }
};
int main()
{
   B *somenull = NULL;
   somenull->foo();
   somenull->pp();
   somenull->FunctionB();
   return 0;
}

为什么 somenull 为空指针,还能运行通过呢?

foo(), pp(), FunctionB()不是virtual,还有这些函数内没有对this解引用。

       原因是:因为对于非虚成员函数,C++这门语言是静态绑定的。这也是C++语言和其它语言Java, Python的一个显著区别。以此下面的语句为例:

somenull->foo();

这语句的意图是:调用对象somenull的foo成员函数。

       如果这句话在Java或Python等动态绑定的语言之中,编译器生成的代码大概是:找到somenull的foo成员函数,调用它。(注意,这里的找到是程序运行的时候才找的,这也是所谓动态绑定的含义:运行时才绑定这个函数名与其对应的实际代码。有些地方也称这种机制为迟绑定,晚绑定。)

       但是对于C++,为了保证程序的运行时效率,C++的设计者认为凡是编译时能确定的事情,就不要拖到运行时再查找了。所以C++的编译器看到这句话会这么干:

1:查找somenull的类型,发现它有一个非虚的成员函数叫foo。(编译器干的)

2:找到了,在这里生成一个函数调用,直接调B::foo(somenull)。

      所以到了运行时,由于foo()函数里面并没有任何需要解引用somenull指针的代码,所以真实情况下也不会引发segment fault。这里对成员函数的解析,和查找其对应的代码的工作都是在编译阶段完成而非运行时完成的,这就是所谓的静态绑定,也叫早绑定。

     正确理解C++的静态绑定可以理解一些特殊情况下C++的行为。

     C++只关心你的指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性。况且在有些系统上,地址0也是有效的,理论上完全可以构造一个在地址0的C++对象。

class B {
public:
   void foo() {cout << "B foo " << endl; }
   void pp() { cout<< "B pp" << endl; }
   void FunctionB(){ cout << "funB" << endl; }
};

      实际上,上面这段代码编译以后是下面这个样子的,你自己觉得会不会异常呢?如果有兴趣的话可以去查查编译后生成的符号表验证一下。

class B;
void foo(B *this) { cout << "Bfoo " << endl; }
void pp(B *this) { cout << "Bpp" << endl; }
void FunctionB(B *this) { cout <<"funB" << endl; }

例如类 A 有一个子类 B,B 有一个虚函数 foo;假设有下面的代码:

某个函数(){
B b;
b.foo();
}

或者

{
  A *p = new B();
  p->foo();
}

      由于构造过程是该局部可见的(所以对象类型在该局部就完全明确了),所以在编译这段代码时,编译器能够确定这个 foo 函数就是 B::foo() (假设B有定义foo的话)。所以这个时候,也可能有静态绑定。

 即:虚函数不一定 都是运行时确定其地址的。

      和c++内存布局有关,为了节约内存和提高调用效率,一般类成员的存储分成两块,一块是单个instance所有,比如非静态成员变量,另一块是所有instances共享的,比如函数代码。这样的布局是对于性能有好处的,代码只要load一次,减少了cache占用和miss。如果你的函数不引用任何instance独有的内存部分,nullptr并无问题,因为不会使用this,只会使用类instance共享的部分,这部分始终存在,即使你没有任何类实例。反之就会出问题,因成员函数

class B{ void foo(){} };

在编译的时候会被预先翻译为类似

void foo(struct B b){}

这样的C语言形式

而你的代码中没有任何一行引用到空指针b(也就是this),因此不会崩溃。

       从某种意义上,this 指针可以看做成员函数的第一个参数。实际上,C语言模拟成员函数的做法就是定义一个 struct,然后定义一些自由函数,把 struct 的指针作为第一个参数传递进去。

       看看Python class的成员函数的写法,然后把cpp的写法转化成python的写法你就会理解,参数传入一个NULL但是没有访问是没有太大问题的,这里的访问包括了函数内部逻辑访问也包括了语言级别的访问。 

     你可以认为非virtual成员函数有个static修饰符,每个class的成员函数只有一份在内存里面,调用的时候直接取地址调用。

somenull->foo()会被翻译成foo(somenull),如果foo没事用this指针的成员,那样执行没有问题啊。 

     简单地说就是,你给函数传递了错误的参数,但在该函数内部并没有使用该参数,所以其不影响函数的运行。


猜你喜欢

转载自blog.csdn.net/li_wei_quan/article/details/79807892