C++如何在内存中存储函数和对象?

10

假设我们有一个类

class A
{
    int x;
public:
    void sayHi()
    {
        cout<<"Hi";
    }
};

int main()
{
    A *a=NULL;
    a->sayHi();
}

上述代码在Turbo C中编译(我已测试)并输出Hi

我本来期望程序会崩溃,因为aNULL。此外,如果我将sayHi()函数设为虚函数,它会提示:

Abnormal temination(Segmentation fault in gcc) 

我知道很多都依赖于具体的实现,但如果有人能够介绍一些具体实现或者提供一个概述,那真是太好了。


2
通过空指针调用方法是未定义的行为。任何事情都可能发生 - 它不一定会崩溃,但标准允许它。 - Zyx 2000
不是C++专家,但我猜测:你的代码不需要访问任何A实例的内存。sayHi()没有使用字段x,也不是虚函数,因此不需要访问vtable来解析。实际上,C++编译器必须插入一个检查来查看a是否为有效指针才会导致错误。 - millimoose
顺便说一句,Turbo C 是一个古老的文物。你可以使用许多其他免费的开发环境。 - pm100
4个回答

7
显然,代码存在未定义的行为,即您得到的任何结果都是偶然的。也就是说,在调用非虚成员函数时,系统不需要知道对象:它可以根据签名直接调用。此外,如果成员函数不需要访问成员,则根本不需要对象,只需运行即可。这就是当代码打印一些输出时所观察到的情况。然而,系统是否实现了这种情况并没有定义,也就是说,并没有什么可以保证它能正常工作。
在调用虚函数类型时,类型系统首先查看与对象关联的类型信息记录。在空指针上调用虚函数时,不存在这样的信息,尝试访问它可能导致某种崩溃。尽管如此,它并不一定会发生崩溃,但大多数系统都会这样做。
顺便说一下,main()总是返回int。

6
在C++中,一个类的方法不会存储在该类的实例内部。它们只是一些“特殊”的函数,透明地接受程序员指定的参数以及this指针。
在你的情况下,sayHi()方法没有引用任何类字段,因此this指针(它是NULL)永远不会被使用。
但请不要误解,这仍然是未定义的行为。当你调用它时,你的程序可能会选择向你的联系人列表发送恶意电子邮件。在这种情况下,它做了最糟糕的事情,似乎可以正常工作。
虚方法的情况已经被添加到我回答问题的过程中,但我不会改变我的答案,因为其他人的答案已经包含了这一点。

1
+1 是为了指出未定义行为“能用”是不好的。 - Zyx 2000

5
一般来说,从类实例化的对象的布局如下所示:

* - v_ptr  ---> *  pTypeInfo
|               |- pVirtualFuncA
|               |- pVirtualFuncB
|- MemberVariableA
|- MemberVariableB

v_ptr 是指向虚函数表的指针,其中包含对象的虚函数和 RTTI 数据的地址。没有虚函数的类没有虚函数表,相应的对象也没有 v_ptr

在上面的示例中,class A 没有虚方法,因此没有虚函数表。这意味着调用 sayHi() 的实现可以在编译时确定并且是不变的。

编译器生成的代码将隐式的 this 指针设置为 a,然后跳转到 sayHi() 的开头。由于实现不需要对象的内容,所以当指针为 NULL 时它仍然可以正常工作,这是一个幸运的巧合。

如果你将 sayHi() 声明为虚函数,编译器无法在编译时确定要调用的实现,因此会生成查找函数地址并调用它的代码。在你的示例中,当 aNULL 时,编译器读取地址 0 的内容,导致程序异常终止。


类A没有虚方法这一事实并不重要。重要的是被调用的特定函数,比如sayHi,不是虚函数,并且不使用类对象的成员。因此,可以(并且已经)在不使用类对象指针的情况下调用此函数。 - Eric Postpischil

1
如果您调用一个类的非虚方法,对于编译器来说,只需要知道函数属于哪个类,并通过解引用指向该类的指针(即使是NULL指针)来调用该方法,编译器就能得到这些信息。 sayHi() 方法基本上只是一个以类实例指针作为隐藏参数的函数。这个指针是NULL,但如果您在方法中不引用任何属性,则无关紧要。
一旦将此方法设置为虚方法,情况就会发生变化。编译器不再知道与方法相关联的代码是什么,必须在运行时确定。它所做的是查看一个表,该表基本上包含所有虚方法的函数指针;这个表与类实例相关联,因此它查看相对于NULL指针的内存块,因此在这种情况下会崩溃。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接