【问题标题】:How do C++ class members get initialized if I don't do it explicitly?如果我不明确地初始化 C++ 类成员,如何初始化?
【发布时间】:2011-03-08 20:00:23
【问题描述】:

假设我的班级有私人成员 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?参考成员呢?还有 const 引用呢?

我想学习它,以便编写更好(无错误)的程序。任何反馈都会有所帮助!

【问题讨论】:

  • 迈克,哦,我的意思是某本书中解释它的章节。不是整本书! :)
  • 不过,阅读一整本关于您打算使用的编程语言的书可能是个好主意。如果你已经读过一本,但它没有解释这一点,那它就不是一本好书。
  • Scott Meyers(一位受欢迎的前 C++ 专家建议大师)在 Effective C++ 中指出,“在我看来,规则很复杂——太复杂以至于不值得记住。 ...确保所有构造函数都初始化对象中的所有内容。”因此,在他看来,(尝试)编写“无错误”代码的最简单方法不是试图记住规则(事实上他没有在书中列出它们),但是显式初始化所有内容。但是请注意,即使您在自己的代码中采用这种方法,您也可能会从事由不这样做的人编写的项目,因此这些规则可能仍然很有价值。
  • @TylerMcHenry 您认为哪些 C++ 书籍“好”?我读过几本关于 C++ 的书,但没有一本能完全解释这一点。正如我之前的评论中所指出的,Scott Meyers 明确拒绝提供Effective C++中的完整规则。我还阅读了 Meyers 的 Effective Modern C++、Dewhurst 的 C++ Common Knowledge 和 Stroustrup 的 A Tour of C++。在我的记忆中,没有一个解释了完整的规则。显然我可以阅读标准,但我几乎不会认为这是一本“好书”! :D 我希望 Stroustrup 可能会在 C++ 编程语言中解释它。

标签: c++ initialization member-initialization


【解决方案1】:

代替显式初始化,类中成员的初始化与函数中局部变量的初始化相同。

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

对于原始类型(指针、整数等),它们初始化——它们包含之前碰巧出现在该内存位置的任意垃圾。 p>

对于references(例如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

【讨论】:

  • +1。值得注意的是,根据严格的标准定义,原始类型的实例以及各种其他事物(任何存储区域)都被视为对象
  • “如果对象的类没有默认构造函数,如果不显式初始化就会出现编译错误”这是错误!如果一个类没有默认构造函数,则给它一个默认默认构造函数,它是空的。
  • @wiz 我认为他的字面意思是“如果对象没有默认构造函数”,因为也没有生成的构造函数,如果类显式定义除默认构造函数以外的任何构造函数(不会生成默认 ctor)。如果我们过于迂腐,我们可能会感到困惑,而不仅仅是帮助,而 Tyler 在他之前对我的回应中对此提出了很好的观点。
  • @wiz-loz 我会说foo 确实有一个构造函数,它只是隐含的。但这确实是语义的争论。
  • 我将“默认构造函数”解释为可以不带参数调用的构造函数。这可以是您自己定义的,也可以是编译器隐式生成的。所以没有它,既不是你自己定义的,也不是生成的。或者这就是我的看法。
【解决方案2】:

首先,让我解释一下 mem-initializer-list 是什么。 mem-initializer-list 是一个逗号分隔的 mem-initializer 列表,其中每个 mem-initializer 是一个成员名称,后跟(,后跟 表达式列表,然后是 )表达式列表 是成员的构造方式。例如,在

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成员被the std::string constructor that takes two input iterators初始化,rname成员被引用name被初始化,crname成员被初始化为一个对name 的常量引用,并且age 成员被初始化为-4

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

当您不指定成员的 mem-initializer 时,C++ 标准会说:

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

这里,因为name 是类类型的非静态数据成员,所以如果在mem-initializer-list 中没有指定name 的初始化程序,则默认初始化。 Example 的所有其他成员都没有类类型,因此它们没有被初始化。

当标准说它们没有被初始化时,这意味着它们可以有 any 值。因此,因为上面的代码没有初始化pname,所以它可以是任何东西。

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

【讨论】:

  • 当您想要严格区分 declaration (in .h) 和 definition (in .cpp) 时,这是初始化成员的最佳方式) 没有显示太多内部结构。
【解决方案3】:

您还可以在声明数据成员时对其进行初始化:

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;
};

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

【讨论】:

    【解决方案4】:

    如果您的示例类在堆栈上实例化,则未初始化的标量成员的内容是随机且未定义的。

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

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

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

    【讨论】:

    • 我进行了实验,在堆栈上初始化类后,string name 似乎为空。你确定你的答案吗?
    • string 将有一个默认提供空字符串的构造函数 - 我会澄清我的答案
    • @bodacydo :Paul 是正确的,但如果你关心这种行为,那么明确表示永远不会有坏处。把它扔到初始化列表中。
    • 感谢您的澄清和解释!
    • 这不是随机的!随机这个词太大了!如果标量成员是随机的,我们就不需要任何其他随机数生成器。想象一个分析“剩余”数据的程序——比如在内存中取消删除文件——数据远非随机。它甚至不是未定义的!通常很难定义,因为通常我们不知道我们的机器在做什么。如果您刚刚取消删除的“随机数据”是您父亲的唯一图像,那么如果您说它是随机的,您的母亲甚至可能会觉得冒犯......
    【解决方案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
    

    【讨论】:

      【解决方案6】:

      这取决于类的构造方式

      回答这个问题需要理解 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 进入“默认初始化的影响”部分并开始 case 语句:

      • "if T is a non-POD": no(POD的定义本身就是一个巨大的switch语句)
      • “如果 T 是数组类型”:否
      • “否则,什么都不做”:因此它留下了一个未定义的值

      然后,如果有人决定进行值初始化,我们就去https://en.cppreference.com/w/cpp/language/value_initialization“值初始化的效果是”并开始case语句:

      • “如果 T 是一个没有默认构造函数或具有用户提供或删除的默认构造函数的类类型”:不是这种情况。您现在将花费 20 分钟谷歌搜索这些术语:
        • 我们有一个隐式定义的默认构造函数(特别是因为没有定义其他构造函数)
        • 它不是用户提供的(隐式定义)
        • 没有被删除 (= delete)
      • “如果 T 是具有既不是用户提供也不是删除的默认构造函数的类类型”:是

      这就是为什么我强烈建议您永远不要依赖“隐式”零初始化。除非有强大的性能原因,否则显式初始化所有内容,无论是在构造函数上(如果您定义了一个),还是使用聚合初始化。否则,你会让未来的开发者面临很大风险。

      【讨论】:

        【解决方案7】:

        具有构造函数的成员将调用其默认构造函数进行初始化。

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

        【讨论】:

          【解决方案8】:

          如果它在堆栈上,没有自己的构造函数的未初始化成员的内容将是随机且未定义的。即使它是全球性的,依赖它们被归零也是一个坏主意。无论它是否在堆栈上,如果一个成员有自己的构造函数,都会调用它来初始化它。

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-05-11
            • 2015-03-22
            • 1970-01-01
            相关资源
            最近更新 更多