为什么指针不默认初始化为NULL?

129

能否有人解释一下为什么指针不会初始化为NULL
例如:

  void test(){
     char *buf;
     if (!buf)
        // whatever
  }

由于buf不为null,程序不会进入if语句块。

我想知道,在什么情况下我们需要一个具有垃圾值的变量,特别是指向内存垃圾的指针?


13
因为基本类型会被保留未初始化,所以我猜你实际想问的是:为什么基本类型不被初始化? - GManNickG
11
程序不会进入if语句,因为buf不为null。这是不正确的。由于您不知道buf是什么,所以不能确定它不是*什么。 - Drew Dormann
1
与Java等语言相比,C++更多地将责任交给开发人员。 - Rishi
整数,指针,在使用()构造函数时默认为0。 - Erik Aronesty
1
由于假设使用C++的人知道他们在做什么,而且使用原始指针而不是智能指针的人更加知道自己在做什么! - Lofty Lion
15个回答

167
我们都知道指针(和其他POD类型)应该被初始化。
那么问题就变成了“谁来初始化它们”。
基本上有两种方法:
1.编译器会对它们进行初始化。 2.开发人员对它们进行初始化。
让我们假设编译器会初始化任何未被开发人员显式初始化的变量。然后我们遇到了这样的情况:初始化变量是不平凡的,而开发人员没有在声明点处执行初始化的原因是他/她需要执行某些操作,然后再赋值。
现在我们面临的情况是,编译器添加了一个额外的指令来将变量初始化为NULL,然后稍后添加了开发人员代码以进行正确的初始化。或者在其他条件下,变量可能永远不会被使用。在这两种情况下,很多C++开发者都会因为额外指令的代价而大声抗议。
这不仅仅是时间问题,还涉及到空间问题。有很多环境,两种资源都很珍贵,开发人员不想放弃任何一个。
但是:您可以模拟强制初始化的效果。大多数编译器都会警告您有未初始化的变量。所以我总是把我的警告级别调成最高级。然后告诉编译器把所有警告都当作错误处理。在这些条件下,大多数编译器将为使用未初始化但被使用的变量生成一个错误,从而防止代码被生成。

5
Bob Tabor说:“太多人没有认真思考初始化的问题!自动初始化所有变量虽然'友好',但需要时间,慢速程序是不'友好'的。如果表格或编辑器显示了malloc找到的随机垃圾,则是不可接受的。对于受过训练的用户来说,C语言是一种锋利的工具(如果误用则很危险),应该不会花费太多时间初始化自动变量。一个初始化变量的训练轮廓宏可能有帮助,但许多人认为最好的方法是站起来,保持警觉,并流一点血。在紧急情况下,你必须按平时练习的方式工作。所以要按照实际需求进行练习。” - Bill IV
2
仅仅通过修复所有初始化问题,你会惊讶地发现可以避免多少 Bug。如果没有编译器警告,这将是一个繁琐的工作。 - Jonathan Henson
4
@Loki,我很难理解你的观点。我只是想称赞你的回答有帮助,我希望你能理解。如果没有,我很抱歉。 - Jonathan Henson
3
如果指针首先被设为NULL,然后再设置为任何值,编译器应该能够检测到这一点,并优化第一个NULL的初始化,对吗? - Korchkidu
1
@Korchkidu:有时候。但是其中一个主要问题是,它无法警告您忘记进行初始化,因为它无法知道默认值是否完美适合您的使用。 - Deduplicator
显示剩余4条评论

43

引用Bjarne Stroustrup在《C++程序设计语言》中的话(Special Edition p.22):

一个特性的实现不应该对不需要它的程序造成重大的负担。


8
@ Jonathan,没有任何阻止你将指针初始化为 null 或者像 C++ 中标准的那样初始化为 0。 - stefanB
16
是的,但Stroustrup本可以通过将指针进行零初始化来使默认语法偏向于程序正确性而不是性能,并让程序员必须明确要求未初始化指针。毕竟,大多数人更喜欢正确但慢的结果,因为通常更容易优化少量代码,而不是修复整个程序中的错误,特别是当一个好的编译器可以完成很多工作时。 - Robert Tuck
1
@Neil:以什么样的方式会破坏C兼容性呢?任何符合C标准的代码,如果同时符合C++标准,并且C++突然开始保证指针的零初始化,那么它仍然是符合标准的。或者你指的是如果基于这种假设编写的C++代码在其他情况下不符合C标准? - Pavel Minaev
2
它不会破坏兼容性。这个想法是与“int* x = __uninitialized”一起考虑的 - 默认安全,意图加速。 - MSalters
4
我喜欢 D 所做的事情。如果您不想进行初始化,请使用以下语法:float f = void;int* ptr = void;。现在默认情况下已经初始化,但如果您真的需要,可以阻止编译器执行初始化。 - deft_code
显示剩余7条评论

22

因为初始化需要时间。在C++中,你应该对任何变量的第一件事就是显式地进行初始化:

int * p = & some_int;

或者:

int * p = 0;

或者:

class A {
   public:
     A() : p( 0 ) {}  // initialise via constructor
   private:
     int * p;
};

1
如果初始化需要时间,而我仍然想要它,有没有办法使我的指针自动变为空指针,而不需要手动设置呢?你看,这并不是因为我不想写,而是因为我似乎永远不会使用带有垃圾地址的未初始化的指针。 - Jonathan
1
你可以在类的构造函数中初始化类成员 - 这是C++的工作方式。 - anon
3
@Jonathan:但是 null 也是垃圾。你不能使用空指针做任何有用的事情。解引用一个空指针也同样会导致错误。请使用正确的值创建指针,而不是空值。 - DrPizza
2
将指针初始化为Nnull可能是一个明智的选择。而且,你可以对空指针执行多种操作 - 你可以测试它们,也可以调用delete函数。 - anon
4
如果你从不在没有显式初始化指针的情况下使用它,那么在给它赋值之前它所包含的内容并不重要,而按照C和C++的原则,只付出必要的代价,这不是自动完成的。如果有一个可接受的默认值(通常是空指针),你应该对其进行初始化。你可以选择初始化它或者让它保持未初始化状态。 - David Thornley
显示剩余11条评论

22

因为C++的座右铭之一是:


你不必支付你不使用的东西


正因为这个原因,vector类的operator[]方法不会检查索引是否越界。


11

出于历史原因,主要是因为这是在C语言中的做法。为什么在C语言中会这样做,另一个问题,但我认为“零开销原则”在这个设计决策中以某种方式起了作用。


我猜是因为C语言被认为是一种低级语言,具有易于访问的内存(即指针),因此它给你自由去做你想做的事情,并且不会通过初始化所有内容来施加额外负担。顺便说一句,我认为这取决于平台,因为我曾在基于Linux的移动平台上工作过,该平台在使用之前将其所有内存初始化为0,因此所有变量都将设置为0。 - stefanB
自动将其初始化为零不会破坏与C的向后兼容性。如果未初始化的变量可以是任何值,那么它们也可以是零。 - Donald Duck

7
此外,当你犯错误时我们有一个警告:"在赋值之前可能已经被使用" 或其他类似的措辞,具体取决于您使用的编译器。请问,您是否启用了编译警告?

这只是可能作为承认编译器跟踪可能存在错误。 - Deduplicator

5

极少数情况下,变量未初始化是有意义的,而默认初始化的成本很小,那么为什么要这样做呢?

C++不是C89。甚至C语言也不是C89。你可以混合声明和代码,所以应该推迟声明,直到有适当的值可以进行初始化。


2
然后,每个值都需要写两次——一次是由编译器的设置例程写入,另一次是由用户程序写入。通常不是一个大问题,但它会累积起来(例如,如果你正在创建一个包含100万项的数组)。如果您想要自动初始化,您可以随时创建自己的类型来实现;但这样做就不会被强制接受不必要的开销。 - Jeremy Friesner

3

指针只是另一种类型。如果您创建一个intchar或任何其他POD类型,它不会被初始化为零,那么为什么指针要被初始化为零呢?对于像这样编写程序的人来说,这可能被认为是不必要的开销。

char* pBuf;
if (condition)
{
    pBuf = new char[50];
}
else
{
    pBuf = m_myMember->buf();
}

如果您知道要初始化它,为什么程序在方法顶部首次创建pBuf时应该产生成本?这就是零开销原则。


1
另一方面,您可以执行char *pBuf = condition ? new char[50] : m_myMember->buf();。这更像是语法问题而不是效率问题,但我仍然同意您的观点。 - the_drow
1
@the_drow:嗯,可以让它变得更加复杂,这样就不可能进行重写了。 - Deduplicator

3
如果您想要一个始终初始化为NULL的指针,您可以使用C++模板来模拟该功能:
template<typename T> class InitializedPointer
{
public:
    typedef T       TObj;
    typedef TObj    *PObj;
protected:
    PObj        m_pPointer;

public:
    // Constructors / Destructor
    inline InitializedPointer() { m_pPointer=0; }
    inline InitializedPointer(PObj InPointer) { m_pPointer = InPointer; }
    inline InitializedPointer(const InitializedPointer& oCopy)
    { m_pPointer = oCopy.m_pPointer; }
    inline ~InitializedPointer() { m_pPointer=0; }

    inline PObj GetPointer() const  { return (m_pPointer); }
    inline void SetPointer(PObj InPtr)  { m_pPointer = InPtr; }

    // Operator Overloads
    inline InitializedPointer& operator = (PObj InPtr)
    { SetPointer(InPtr); return(*this); }
    inline InitializedPointer& operator = (const InitializedPointer& InPtr)
    { SetPointer(InPtr.m_pPointer); return(*this); }
    inline PObj operator ->() const { return (m_pPointer); }
    inline TObj &operator *() const { return (*m_pPointer); }

    inline bool operator!=(PObj pOther) const
    { return(m_pPointer!=pOther); }
    inline bool operator==(PObj pOther) const
    { return(m_pPointer==pOther); }
    inline bool operator!=(const InitializedPointer& InPtr) const
    { return(m_pPointer!=InPtr.m_pPointer); }
    inline bool operator==(const InitializedPointer& InPtr) const
    { return(m_pPointer==InPtr.m_pPointer); }

    inline bool operator<=(PObj pOther) const
    { return(m_pPointer<=pOther); }
    inline bool operator>=(PObj pOther) const
    { return(m_pPointer>=pOther); }
    inline bool operator<=(const InitializedPointer& InPtr) const
    { return(m_pPointer<=InPtr.m_pPointer); }
    inline bool operator>=(const InitializedPointer& InPtr) const
    { return(m_pPointer>=InPtr.m_pPointer); }

    inline bool operator<(PObj pOther) const
    { return(m_pPointer<pOther); }
    inline bool operator>(PObj pOther) const
    { return(m_pPointer>pOther); }
    inline bool operator<(const InitializedPointer& InPtr) const
    { return(m_pPointer<InPtr.m_pPointer); }
    inline bool operator>(const InitializedPointer& InPtr) const
    { return(m_pPointer>InPtr.m_pPointer); }
};

1
如果我在实现这个功能,我不会费心去写复制构造函数或赋值运算符 - 默认的已经足够好了。而且你的析构函数是无意义的。当然,在某些情况下,你也可以使用小于运算符等来测试指针,所以你应该提供它们。 - anon
好的,实现小于操作是微不足道的。我已经编写了析构函数,以便如果对象超出范围(即在函数的子范围内定义的本地对象),但仍占用堆栈空间,则不会将内存留作指向垃圾的悬挂指针。但是,老兄,说真的,我在不到5分钟的时间内就写完了这个。它并不是要完美无缺。 - Adisak
已添加所有比较运算符。默认覆盖可能是冗余的,但它们在此明确说明,因为这是一个示例。 - Adisak
1
我不明白这个怎么会让所有指针都变成null,而不需要手动设置它们,你能解释一下你在这里做了什么吗? - Jonathan
1
@Jonathan:这基本上是一个“智能指针”,除了将指针设置为null之外,什么也不做。例如,您可以使用InitializedPointer<Foo> a代替Foo *a——这只是一种纯学术性的练习,因为Foo *a=0更简洁。然而,从教育角度来看,上面的代码非常有用。通过对“占位”构造函数/析构函数和赋值操作进行一些修改,它可以轻松扩展到各种类型的智能指针,包括作用域指针(在析构函数中释放)和引用计数指针(通过在设置或清除m_pPointer时添加inc/dec操作)。 - Adisak

3

请注意,静态数据将被初始化为0(除非您另有说明)。

是的,您应该尽可能晚地声明变量,并赋予一个初始值。像下面这样的代码

int j;
char *foo;

当您阅读到这段内容时,应该会引起警觉。我不知道是否有任何代码检查工具会对此发出警告,因为这是百分之百合法的。


这是保证了的吗,还是当今编译器常用的一种做法? - danielschemmel
1
静态变量被初始化为0,这对指针也做了正确的事情(例如,将它们设置为NULL,而不是所有位都为0)。这个行为是由标准保证的。 - Alok Singhal
1
C和C++标准保证静态数据的初始化为零,这不仅仅是常见的做法。 - groovingandi
1
也许是因为有些人想要确保他们的堆栈对齐得很好,所以他们在函数顶部预先声明所有变量?也许他们正在使用需要这样做的 C 方言? - KitsuneYMG

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