C++函数在未初始化对象的情况下被调用。

5
为什么下面的代码可以运行?
#include <iostream>
class A {
    int num;
    public:
        void foo(){ num=5; std::cout<< "num="; std::cout<<num;}
};

int main() {
    A* a;
    a->foo();
    return 0;
}

输出结果为:
num=5

我使用gcc编译这段代码,但只得到以下警告信息(位于第10行):

(警告:'a'在此函数中未初始化使用)

但据我理解,这段代码应该根本无法运行吧?而且当还没有创建类型A的任何对象时,它是如何将值5赋给num的呢?


一个额外的问题:如果你没有成员变量 num ,你是否可以期望这段代码工作?例如,如果它只包含 std::cout << "num=";,所以是本地状态无关的(我是真正地在询问,而不仅仅是提供思考食物) - Merlyn Morgan-Graham
@Merlyn Morgan-Graham:不行。这里的代码取消引用了一个未初始化的指针。这是未定义的行为(即程序可能会做任何事情(包括似乎工作))。 - Martin York
@Martin:我猜我可以理解规范没有定义这个,所以你不应该依赖它,但是如果没有继承和数据成员,编译器可能会做什么导致这不起作用呢? - Merlyn Morgan-Graham
@Merlyn Morgan-Graham:那是不可知的。每个编译器都可以并且确实使用不同的优化技术。 - Martin York
显示剩余4条评论
7个回答

4
代码产生了未定义的行为,因为它试图引用一个未初始化的指针。未定义的行为是不可预测的,没有任何逻辑可言。因此,任何关于你的代码为什么会做某些事情或不做某些事情的问题都没有意义。
你问为什么它运行?它并没有运行。它产生了未定义的行为
你问它如何将5分配给不存在的成员?它没有将任何东西分配给任何东西。它产生了未定义的行为
你说输出是5?错误的。没有有意义的输出。这段代码产生了未定义的行为。仅仅因为在你的实验中它偶然打印出5,这并没有任何有意义的解释。

2

您还没有初始化*a

尝试这样做:

#include <iostream>

class A
{
    int num;
    public:
        void foo(){ std::cout<< "num="; num=5; std::cout<<num;}
};

int main()
{
    A* a = new A();
    a->foo();
    return 0;
}

如果你不小心初始化指针,可能会导致未定义的行为。如果你很幸运,你的指针指向堆中一个需要初始化的位置。(假设在这样做时没有抛出异常。)如果你不幸,你将覆盖正在用于其他目的的内存的一部分。如果你非常不幸,这可能不会被注意到。
这不是安全的代码;"黑客"可能会利用它。
当然,即使你访问那个位置,也不能保证它不会在以后被"初始化"。
"幸运"(实际上,"幸运"会使调试程序更加困难):
// uninitialized memory 0x00000042 to 0x0000004B
A* a;
// a = 0x00000042;
*a = "lalalalala";
// "Nothing" happens

"不幸"(这使得调试程序更加容易,所以我并不认为它是“不幸”的):

void* a;
// a = &main;
*a = "lalalalala";
// Not good. *Might* cause a crash.
// Perhaps someone can tell me exactly what'll happen?

1
我故意没有初始化a。重点是我可以在foo()内设置'num'的值而不初始化a! - Apoorva Iyer

2

A* a;是一个未初始化的指针。

你看到的值是垃圾值,并且你很幸运没有遇到崩溃。

这里没有进行初始化。

这里没有进行赋值。

你的类恰好足够简单,没有展示更严重的问题。

A* a(0);会导致崩溃。未初始化的指针在某些情况下会导致崩溃,并且在更复杂的类型中更容易重现。

这是处理未初始化的指针和对象的后果,并且它指出了编译器警告的重要性。


我怀疑这个值是垃圾值。如果我将foo中num的值从num = 5更改为num = _任何数字_,我会在输出中得到那个数字。 - Apoorva Iyer
这绝对是垃圾代码 - 写入num的值时,你正在覆盖内存中附近其他东西的地址。 - justin
Apoorva Iyer,你显然不理解“未定义行为”的概念。 - titaniumdecoy
哦,原来是这样。所以我覆盖了其他垃圾地址!我以为你的意思是num的值是垃圾。现在更有意义了。谢谢! - Apoorva Iyer
此外,如果一个变量没有被赋值,它所获得的值就是垃圾值(具体而言)。除此之外,你要么访问一个有效的内存地址,要么访问一个无效的内存地址(== 崩溃或非常神秘的行为)。 - justin
确实!所以当它不崩溃时,你看到的是一个有效的地址。这就是你能够在没有错误访问的情况下读/写它的方式。但这样做只会读/写一个你没有意图读/写的内存块。 - justin

1
A* a;
a->foo();

这会引起未定义行为。最常见的情况是使程序崩溃。

C++03标准中的§4.1/1节说:

类型为T的非函数、非数组的左值(3.10)可以转换为右值。如果T是一个不完全的类型,则需要进行此转换的程序是非法的。如果左值所指向的对象既不是类型为T的对象,也不是派生自T的对象,或者如果该对象未初始化,则需要进行此转换的程序具有未定义行为。如果T是一种非类类型,则rvalue的类型是cv-unqualified版本的T。否则,rvalue的类型是T。

请参阅类似主题: C++标准确切地在哪里说解除未初始化指针的引用是未定义行为?


“为什么在还没有创建A类型的对象时,它会将值5分配给num,而num并不存在呢?”
这就是所谓的幸运。但这种情况并不总是发生。

1

这是我认为会发生的事情。

a->foo(); 之所以能够工作,是因为你只是调用了A::foo(a)

a是一个指针类型变量,位于主调用栈中。当访问位置a时,foo()函数可能会抛出分段错误,但如果没有,那么foo()就会从a跳转到一些位置,并用值5覆盖4个字节的内存。然后它再读取相同的值。

我是对还是错?请告诉我,我正在学习调用栈,非常感谢您对我的回答提供任何反馈。

另请查看以下代码

#include<iostream>
class A {
    int num;
    public:
        void foo(){ num=5; std::cout<< "num="; std::cout<<num;}
};

int main() {

    A* a;
    std::cout<<"sizeof A is "<<sizeof(A*)<<std::endl;
    std::cout<<"sizeof int is "<<sizeof(int)<<std::endl;
    int buffer=44;
    std::cout<<"buffer is "<<buffer<<std::endl;
    a=(A*)&buffer;

    a->foo();
    std::cout<<"\nbuffer is "<<buffer<<std::endl;
    return 0;
}

0
在对象创建时,即使您没有使用关键字new,该类成员也会为该特定对象分配,因为该对象是指向类的指针。因此,您的代码运行良好并给出了num的值,但GCC发出警告,因为您没有显式实例化对象。

0

我会指向(嘿嘿)我之前回答过的一个非常相似的问题的答案链接:Tiny crashing program

基本上,你正在用指针覆盖envs堆栈变量,因为你没有将envs添加到main声明中。

由于envs是一个数组的数组(字符串),它实际上已经被分配了很多空间,你正在用5覆盖该列表中的第一个指针,然后再次读取它以使用cout打印。

现在这是一个关于为什么会发生这种情况的答案。显然,你不应该依赖这个。


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