如果我不明确地进行初始化,那么类成员是如何被初始化的呢?

204
假设我有一个类,其中包含私有成员变量ptrnamepnamernamecrnameage。如果我不自己初始化它们,会发生什么?以下是一个示例:
class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

然后我做:
int main() {
    Example ex;
}

成员在ex中如何初始化?指针会发生什么情况?stringint会使用默认构造函数string()int()进行0初始化吗?那引用成员呢?常量引用又如何处理?
我想学习这些知识,以便能够编写更好(无bug)的程序。任何反馈都将对我有所帮助!

3
有关书籍推荐,请参见https://dev59.com/_3RC5IYBdhLWcg3wK9yV。 - Mike Seymour
Mike,哦,我的意思是某本书的章节,它解释了这个问题。不是整本书! :) - bodacydo
阅读一本你打算编程的语言的完整书籍可能是一个好主意。如果你已经阅读了一本书,但它没有解释清楚这个问题,那么这本书可能并不是很好。 - Tyler McHenry
4
流行的前C++顾问Scott Meyers在《Effective C++》中指出,“规则非常复杂--在我看来,记住这些规则并不值得......确保所有构造函数都初始化对象中的所有内容。”因此,在他看来,尝试编写“无错误”的代码最简单的方法不是试图记忆规则(事实上他在书中并没有列出规则),而是明确地初始化每一个东西。然而请注意,即使您在自己的代码中采用了这种方法,您可能会处理那些未经初始化的代码编写者编写的项目,因此这些规则仍然很有价值。 - Kyle Strand
3
@TylerMcHenry 你认为哪些C++书籍“好”?我读过几本C++书,但没有一本书完全解释清楚。正如我先前的评论所指出的,Scott Meyers明确地拒绝Effective C ++中提供完整的规则。我也读过Meyers的Effective Modern C ++,Dewhurst的C++ Common Knowledge和Stroustrup的A Tour of C ++。据我记忆,没有一本书解释了完整的规则。显然,我可以阅读标准,但我不会认为那是一本“好书”! :D 我希望Stroustrup可能在 The C++ Programming Language中解释它。 - Kyle Strand
8个回答

266

在没有显式初始化的情况下,类成员的初始化方式与函数中局部变量的初始化完全相同。

对于对象,将调用它们的默认构造函数。例如,对于std::string,默认构造函数会将其设置为空字符串。如果对象的类没有默认构造函数,则如果您没有显式初始化它,将导致编译错误。

对于基本类型(指针、整数等),它们不会被初始化-- 它们包含任意先前存储在该内存位置的垃圾值。

对于引用(例如std::string&),不初始化它们是违法的,您的编译器会抱怨并拒绝编译这种代码。 引用必须始终进行初始化。

因此,在您的特定情况下,如果它们没有被显式初始化:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk

6
值得注意的是,按照严格的标准定义,原始类型的实例以及各种其他东西(存储区域)都被认为是“对象”。 - stinky472
7
“如果对象的类没有默认构造函数,在您不明确初始化它的情况下,会出现编译错误”这是错误的!如果一个类没有默认构造函数,它将被赋予一个默认的默认构造函数,该构造函数为空。 - Wizard
27
我认为他的意思是,如果对象没有默认构造函数,也就是说,如果类显式定义除默认构造函数以外的任何构造函数(不会生成默认构造函数),那么这将是情况所在。如果我们过于追求严格准确,可能会引起更多困惑,正如Tyler在我之前回复我的时候所指出的。请注意,这里涉及到的术语比较专业,需要根据上下文来理解。 - stinky472
10
我会说 foo 是有构造函数的,只是它是隐式的。但这其实是一个语义上的争论。 - Tyler McHenry
6
我将“默认构造函数”解释为可以在不传递参数的情况下调用的构造函数,这可以是您自己定义的构造函数,也可以是编译器隐式生成的构造函数。因此,如果没有默认构造函数,则意味着既未由您自己定义,也未被编译器生成。这就是我的理解。 - 5ound
显示剩余5条评论

34

首先,让我解释一下什么是 mem-initializer-list mem-initializer-list 是由逗号分隔的 mem-initializer 列表组成的,其中每个 mem-initializer 是一个成员名称,后面跟着,然后是一个 expression-list ,最后是一个 expression-list 是成员如何构建的。例如,在

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

用户提供的无参构造函数的mem-initializer-listname(s_str, s_str + 8), rname(name), crname(name), age(-4)。这个mem-initializer-list意味着name成员由采用两个输入迭代器的std::string构造函数进行初始化,rname成员使用name的引用进行初始化,crname成员使用name的const引用进行初始化,age成员的值为-4

每个构造函数都有自己的mem-initializer-list,并且成员只能按照指定顺序(基本上是类中声明成员的顺序)进行初始化。因此,Example的成员只能按照以下顺序进行初始化:ptrnamepnamernamecrnameage

当您未指定成员的mem-initializer时,C++标准规定:

  

如果实体是类类型的非静态数据成员...,则该实体将进行默认初始化(8.5)。... 否则,该实体未初始化。

在此处,因为name是类类型的非静态数据成员,如果namemem-initializer-list中没有指定初始化程序,则会进行默认初始化。 Example的所有其他成员均不具有类类型,因此它们未进行初始化。

当标准规定它们未初始化时,这意味着它们可以具有任何值。 因此,由于上面的代码未对pname进行初始化,所以它可能是任何值。

请注意,您仍然必须遵循其他规则,例如引用必须始终初始化。 未初始化引用是编译器错误。


这是在你想要严格分离声明(在.h中)和定义(在.cpp中)而不显示太多内部细节时初始化成员变量的最佳方式。 - Matthieu

12
你也可以在声明数据成员的地方初始化它们:
class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

我几乎完全使用这种形式,尽管我读到有些人认为它是“不好的形式”,也许是因为它只是最近引入的 - 我想在C++11中。对我来说,这更合乎逻辑。

新规则的另一个有用方面是如何初始化本身就是类的数据成员。例如,假设CDynamicString是一个封装字符串处理的类。它有一个构造函数,允许您指定其初始值CDynamicString(wchat_t* pstrInitialString)。您可能会将此类用作另一个类内部的数据成员 - 比如封装Windows注册表值的类,在这种情况下存储邮政地址。要将注册表键名“硬编码”到其中,可以使用大括号:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

请注意第二个字符串类,它保存实际的邮政地址并没有初始化器,因此在创建时将调用其默认构造函数 - 可能会自动将其设置为空字符串。

11

如果实例化您的类时使用栈(stack),未初始化标量成员的内容是随机且未定义的。

对于全局实例,未初始化的标量成员将被清零。

对于那些自身是类实例的成员,它们的默认构造函数将被调用,因此您的字符串对象将得到初始化。

  • int *ptr; //未初始化的指针(如果是全局变量则为零)
  • string name; //构造函数被调用,初始化为空字符串
  • string *pname; //未初始化的指针(如果是全局变量则为零)
  • string &rname; //如果未初始化,将导致编译错误
  • const string &crname; //如果未初始化,将导致编译错误
  • int age; //标量值,未初始化且随机(如果是全局变量则为零)

我进行了实验,似乎在堆栈上初始化类之后,string name为空。你对你的答案非常确定吗? - bodacydo
1
字符串将具有默认提供空字符串的构造函数 - 我会澄清我的答案。 - Paul Dixon
@bodacydo:Paul是正确的,但如果您关心这种行为,明确说明永远不会有坏处。将其放入初始化程序列表中。 - Stephen
@bod:string 不是标量类型。 - fredoverflow
2
这不是随机的!“随机”这个词太大了!如果标量成员是随机的,我们就不需要任何其他随机数生成器了。 想象一下一个分析“剩余数据”的程序——比如内存中的未删除文件——这些数据远非随机。 它甚至不是未定义的!通常很难定义,因为通常我们不知道我们的机器在做什么。 如果你刚刚恢复的“随机数据”是你父亲的唯一形象,如果你说它是随机的,你的母亲甚至可能会觉得冒犯。 - slyy2048

8

这取决于类的构造方式

回答这个问题需要理解C++语言标准中一个巨大的switch case语句,而这对于普通人来说很难直观理解。

举个简单的例子来说明难度:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

在默认初始化中,您将从https://en.cppreference.com/w/cpp/language/default_initialization开始,然后转到部分“默认初始化的效果”,并开始执行以下情况:

  • “如果T是非POD”:否(POD的定义本身就是一个巨大的开关语句)
  • “如果T是数组类型”:否
  • “否则不做任何操作”:因此它保留着未定义的值

然后,如果有人决定进行值初始化,我们将转到https://en.cppreference.com/w/cpp/language/value_initialization,“值初始化的效果”并开始执行以下情况:

  • 如果T是一个没有默认构造函数或者有用户提供或删除的默认构造函数的类类型:不是这种情况。您现在需要花20分钟Google这些术语:
    • 我们有一个隐式定义的默认构造函数(特别是因为没有定义其他构造函数)
    • 它不是用户提供的(隐式定义)
    • 它没有被删除(= delete
  • 如果T是一个具有既不是用户提供的也不是删除的默认构造函数的类类型:是的
    • “对象被零初始化,然后如果它有一个非平凡的默认构造函数,则进行默认初始化”:没有非平凡的构造函数,只需进行零初始化。至少“零初始化”的定义很简单,并且执行您期望的操作:https://en.cppreference.com/w/cpp/language/zero_initialization

这就是为什么我强烈建议您永远不要依赖“隐式”的零初始化。除非有强烈的性能原因,否则请显式地初始化所有内容,无论是在您定义的构造函数中还是使用聚合初始化。否则,您将为未来的开发人员带来非常大的风险。


5

未初始化的非静态成员将包含随机数据。实际上,它们只有被分配的内存位置的值。

当然,对于对象参数(如string),对象的构造函数可以进行默认初始化。

在您的示例中:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value

2

有构造函数的成员将在初始化时调用它们的默认构造函数。

您不能依赖其他类型的内容。


0

如果它在堆栈上,未初始化成员的内容将是随机和未定义的,如果没有自己的构造函数。即使它是全局的,依赖于它们被清零是一个坏主意。无论它是否在堆栈上,如果成员有自己的构造函数,那么将调用该函数来初始化它。

因此,如果您有string* pname,指针将包含随机垃圾。但对于string name,默认的string构造函数将被调用,给您一个空字符串。对于您的引用类型变量,我不确定,但它可能是对一些随机内存块的引用。


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