nullptr想必大家都知道,它在C++中用于表示空指针,使用空指针是未定义行为。但是大家请看下面一道面试题:以下代码会发生什么?
struct Logger {
void log(const string& msg) {
cout << msg << endl;
}
};
int main() {
Logger *p = nullptr;
p->log("hello"); // 会发生空指针异常吗?输出什么?
}
第一印象,大家很容易觉得当尝试通过空指针p调用成员函数log时,是未定义行为,可能会导致程序崩溃,就算没有崩溃,程序输出什么也是取决于编译器。
大家可以用不同的编译器进行测试,如gcc、clang、msvc。这里笔者也已经用不同版本的编译器进行了测试,程序都会正常输出"hello",大家也可以自行测试。
肯定还是会有人觉得那也是取决于编译器呀,只是这里运气好才使得程序没有崩溃,并不能说明什么。不过笔者接下来会说明,其实这段代码不会发生空指针异常,反而会正常输出 "hello"。
首先,C++ 中调用成员函数的语法 p->log("hello"); 实际上会被编译器转换为类似于 Logger::log(p, "hello"); 的函数调用形式。这里的 p(即指向 Logger 对象的指针)被传递给函数作为隐含的 this 指针。即使 p 是空指针,编译器仍然会将它作为参数传递给 log 函数。而在 log 函数内部,它只是使用了 msg 这个传入的参数,并没有使用 this 指针指向的对象的任何成员变量,也就是说没有真正访问到this指针。所以,即便 p 是 nullptr,也不会出错。
因此这里不会发生空指针异常,会正常输出 "hello"。
但如果 log 函数中有访问 this 指针的操作,比如访问类的成员变量,则会发生空指针异常。当你尝试访问类的成员变量时,this 指针就会起作用,因为成员变量是通过 this 来访问的。如果 p 是空指针,那么 this 也为空,任何对成员变量的访问都会导致空指针解引用,这会导致运行时错误,通常表现为程序崩溃。
例如,假设 Logger 类有一个成员变量 int count;,并且 log 函数尝试使用这个成员变量:
struct Logger {
int count;
void log(const string& msg) {
cout << msg << " count: " << count << endl;
}
};
在这种情况下,log 函数需要访问 this->count 来输出 count。如果 p 是空指针,this 也是 nullptr,所以访问 this->count 时会发生空指针解引用,导致崩溃。
让我们结合汇编来分析:
第一段是Logger类的部分代码,第二段是main函数的部分代码。这里第50行,即mian函数中通过call Logger::log调用log函数,而C++的编译器会隐式地传递一个额外的参数,即 this 指针。第一个片段中,log函数内通过count输出了4次,因此会调用4次std::basic_ostream。重点看17-19行代码。首先将 this 指针的地址从栈上取出,并存入 rax 寄存器,然后从 this 指针所指向的内存中取出 count 成员的值,存入 eax,最后将 eax 的值(即 count)传递给后续的函数调用,但是在17行之前,并没有任何访问this指针的行为,虽然rdi存储了this指针的值,后续传递给了rsi,但是函数内并没有访问this指针中的任何成员变量,因此不会引起崩溃。(注:rdi 通常用来存储第一个参数。在成员函数调用时,第一个参数是 this 指针;rbp寄存器用于存储当前栈帧的基指针,而rbp-8和rbp-16通常用于访问函数的局部变量或参数)
如果 this 指针为 nullptr,那么 rbp-8 处存储的值就是 0。当执行 mov eax, DWORD PTR [rax] 时,编译器尝试从 nullptr 指针中读取 count 成员变量的值。由于 nullptr 没有合法的内存地址,访问这个地址会导致空指针解引用错误,从而引发崩溃。
在之前的例子中,即使 p 是一个空指针,当调用 p->log("hello"); 时,由于 log 函数本身没有使用 this 指针来访问任何类成员变量,因此并不会触发空指针异常。编译器将会把 p->log("hello"); 视作一个普通的函数调用,即 Logger::log(nullptr, "hello");。这就是为什么即使 p 是空指针,程序仍然能够正常输出 "hello",因为没有解引用 this 指针访问任何成员变量。
这里也能看出编译器将会把
p->log("hello"); 视作一个普通的函数调用。即成员函数有它自己的地址,this指针为空,但是成员函数地址并不为空。
因此,问题的关键在于 函数内部是否显式使用了 this 指针访问成员变量。如果函数没有访问成员变量,即使 this 是空指针,程序也不会崩溃。