【问题标题】:Understanding constructor concept理解构造函数概念
【发布时间】:2014-10-07 15:06:22
【问题描述】:

我不明白构造函数在 C++ 中的正式含义。我正在阅读 3.8 条款(对象生命周期,N3797)并遇到以下内容:

如果一个对象属于 类或聚合类型,并且它或其成员之一由以下方式初始化 一个普通的默认构造函数以外的构造函数。

我想了解一般的初始化。我已阅读第 8.5 节,N3797。如果某个对象被初始化,是否会调用构造函数(可能是微不足道的默认值)?我的意思是每个初始化过程(甚至是零初始化)都意味着构造函数调用。最好能提供相应的标准参考。

【问题讨论】:

  • 阅读 C++ 标准可能不是学习基础知识的最佳方式...请从教程或介绍性文本开始。如果您搜索,您可以找到此类列表。
  • @TonyD 对于不同类型初始化的细节以及它们何时被触发,规范可能是最好的方法。这个问题似乎不是关于一般的构造函数,而是更多关于规范承诺的细节。
  • @templatetypedef 我同意。这个问题不是“什么是构造函数”,而是关于初始化要求和通过其中的标准采取的路径。显然那些回答这个问题的人也很难得到这个。
  • 尝试通过阅读标准来学习 C++ 是值得称赞的,但不切实际。有很多信息,作为初学者,你并不真正需要。在这种情况下,您可能不需要知道[when] an object is said to have non-trivial initialization 或聚合类型是什么(我自己几乎不了解聚合,而且我不是初学者)。我什至敢说,标准中的大部分信息对于普通程序员来说是没有必要知道的。
  • @DmitryFucintv 构造函数是“特殊成员函数”(C++11 §12.1 [特殊])。因此(成员函数),它们需要一个类类型。原始类型没有成员函数,因此没有构造函数,无论是微不足道的还是其他的。这并不是说它们在适当使用时不能通过=、{} 或() 进行初始化。 那是你想知道的吗?

标签: c++ constructor language-lawyer


【解决方案1】:

我不明白构造函数在 C++ 中的正式含义。

据我所知,该标准并未明确包含“构造函数”一词的定义。但是,§12.1/1 说

构造函数没有名称。特殊的声明符语法用于声明或定义构造函数。 语法使用:

  • 一个可选的decl-specifier-seq,其中每个decl-specifier是一个function-specifier或constexpr,李>
  • 构造函数的类名,以及
  • 参数列表

按这个顺序。在这样的声明中,构造函数类名周围的可选括号将被忽略。

因此,如果您根据此语法声明成员函数,则您声明的函数是构造函数。此外,

默认构造函数(12.1)、拷贝构造函数和拷贝赋值运算符(12.8)、移动构造函数 和移动赋值运算符 (12.8) 和析构函数 (12.4) 是特殊的成员函数。 [ 注意: 当程序执行时,实现将为某些类类型隐式声明这些成员函数 没有明确声明它们。如果它们被 odr-used (3.2),实现将隐式定义它们。 见 12.1、12.4 和 12.8。 — 尾注 ] 程序不应定义隐式声明的特殊成员函数。

(§12/1)

所以你去——每个类至少声明了三个构造函数,无论是隐式的还是显式的;您还可以使用 §12.1/1 中的语法声明其他构造函数。如此声明的整个函数集构成了构造函数集。

如果某个对象被初始化,是否会调用构造函数(可能是微不足道的默认值)?我的意思是每个初始化过程(甚至是零初始化)都意味着构造函数调用。

不,这是错误的。例如,int 没有构造函数。尽管您可以使用与类类型对象的初始化类似的语法来初始化 int。

struct Foo {};
Foo f {}; // calls default constructor
int i {}; // sets the value of i to 0

此外,零初始化从不调用构造函数,但零初始化也绝不是对象初始化中的唯一步骤。

如果“对象”是指“类类型的对象”,则始终调用构造函数仍然不正确,尽管如上所述始终声明了三个构造函数。有关值初始化,请参见 §8.5/7:

对T 类型的对象进行值初始化意味着:

  • 如果T 是具有用户提供的构造函数 (12.1) 的(可能是 cv 限定的)类类型(第 9 条),则 调用T 的默认构造函数(如果T 没有可访问的默认值,则初始化格式错误 构造函数);
  • 如果 T 是一个(可能是 cv 限定的)非联合类类型,没有用户提供的构造函数,那么对象 是零初始化的,如果T 的隐式声明的默认构造函数是非平凡的,则该构造函数是 调用。
  • 如果T是数组类型,那么每个元素都是值初始化的;
  • 否则,对象被零初始化。

因此,当非联合类类型的默认构造函数很简单并且您正在对该类型的对象进行值初始化时,构造函数实际上不会被调用。

【讨论】:

    【解决方案2】:

    类是类型:

    类是一种类型。

    §9 [类]

    但是,并非所有类型都是类。该标准在 §3.9 中引用了非类类型(例如标量类型)的类型。

    然而,只有类类型可以有成员函数:

    在类定义中声明的函数(不包括使用friend 说明符声明的函数)称为该类的成员函数。

    §9.3 [class.mfct]

    构造函数是成员函数。因此可以存在没有构造函数的类型(即不是类类型的类型)。因此初始化不一定涉及调用构造函数,因为可能会初始化非类类型(例如int)。

    请注意,某些东西不一定是类类型才能成为“对象”:

    一个对象是一个存储区域。

    §1.8 [intro.object]

    因此,int 虽然不是类类型,但将是一个“对象”。

    【讨论】:

      【解决方案3】:

      我认为你的答案会在 N3797 的 3.6.2 和 8.5 中找到。

      在对象创建时总是调用构造函数。但是对象的初始化是 多步骤过程。

      我知道零初始化是独立的,并作为对象初始化期间的第一步执行,构造函数(可能是微不足道的默认值)稍后调用

      【讨论】:

        【解决方案4】:

        构造函数是特殊函数。它看起来像一个正常的功能。它没有返回类型(尽管有人说返回类型是类的对象),如 void、int、char、double 等。它与类的名称相同。它在创建对象时自动运行。 在 C++ 中,您不需要 new 运算符来初始化对象。只需声明它并设置它的属性。即ClassA object1, object2;

        例如

        Class Player{
        int jerseyNo;
        
        //constructor
        Player(){
        cout<<"HELLO";
        }
        };
        

        在 main 中,您可以执行以下操作:

        Player nabin;
        

        你也可以声明析构函数。析构函数是类似于构造函数的特殊函数,但在对象被销毁时运行,即当对象超出范围时运行。

        示例:

        Class Player{
        ..
        ~Player(){
        cout<<"Destructor ran";
        }
        ..
        };
        

        P.S 构造函数和析构函数的执行顺序是相反的。 示例:

        Player p1,p2,p3;
        

        它们的执行顺序

        1. p1's constructor runs
        2. p2's constructor runs
        3. p3's constructor runs
        4. p3's destructor runs
        5. p2's destructor runs
        6. p1's destructor runs
        

        【讨论】:

          【解决方案5】:

          你问:

          如果某个对象被初始化,是否会调用构造函数(可能是微不足道的默认值)?

          简短的回答:不。

          更长的答案:仅对类类型的对象调用构造函数。对于其他类型的对象,没有构造函数,因此无法调用构造函数。

          你说:

          我的意思是每个初始化过程(甚至是零初始化)都意味着构造函数调用。

          类类型的对象可以通过调用构造函数来初始化,可以是显式的也可以是隐式的。它们也可以通过其他初始化方法进行初始化。其他类型的对象通过直接设置其占用的内存初始值来初始化。

          您已经看过关于初始化的标准草案的第 8.5 节。

          关于类构造函数的详细信息可以在12.1 构造函数部分找到。

          关于类初始化的细节可以在12.6 初始化部分找到。

          【讨论】:

          • 类类型的对象并不总是调用构造函数。
          • @NirFriedman,你是对的。我需要完善我的答案。感谢您指出这一点。
          【解决方案6】:

          基本上,每当您在 c++ 中构造数据类型时,它都可能以两种方式之一发生。您可以调用构造函数,也可以基本上复制内存块。所以构造函数并不总是被调用。

          在 C++ 中,有一个原语的概念。整数、双精度、指针、字符、指针(指向任何东西)都是原语。基元是数据类型,但它们不是类。所有原语都可以安全地按位复制。当您创建或分配原语时,不会调用构造函数。所发生的只是生成了一些汇编代码,这些代码复制了一些位。

          使用类会稍微复杂一些;答案是通常会调用构造函数,但并非总是如此。特别是,C++11 有普通类的概念。平凡类是满足几个条件的类:

          1. 它们使用所有“特殊”函数的默认值:构造函数、析构函数、移动、复制、赋值。
          2. 它们没有虚函数。
          3. 类的所有非静态成员都是普通类或原语。

          就 C++11 而言,任何满足此要求的类的对象都可以被视为数据块。当你创建这样一个对象时,它的构造函数不会被调用。如果您在堆栈上创建这样一个对象(没有 new),那么它的析构函数也不会像通常那样在程序终止时被调用。

          不过,不要相信我的话。查看assembly generated for the code I wrote。你可以看到有两个类,一个是琐碎的,一个不是。非平凡的类在程序集中调用了它的构造函数,而平凡的则没有。

          【讨论】:

          • "当你创建这样一个对象时,它的构造函数不会被调用。" 如果你想真的学究,你可以这样说不是全部真相。对于默认初始化,始终调用构造函数(在 as-if 下),对于值初始化,检查它是否可以被调用。删除默认构造函数的类型可以说是微不足道的,请参阅open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1496
          • 我不认为我完全遵循。您能否使用 Godbolt 页面为我的答案创建一个反例? (原始答案中的链接)。我认为这对线程有指导意义。
          • 例如,struct foo { foo() = delete; }; int main() { foo f{}; } 将无法编译,因为默认构造函数已被删除——即使它不会被调用。
          • 好的,我明白你在说什么,但我不太同意。正如您所说,整个反例取决于这样一个想法,即删除了默认构造函数的类型“可以说”是微不足道的。我认为它们绝对不是微不足道的,因为它们删除了默认构造函数。规则 1 是该类需要使用默认构造函数,所以这让我觉得直接违反了规则 1,因此非常重要,因此调用了不存在的构造函数,因此编译器错误。一种完全一致(并且更合乎逻辑)的看待它的方式,然后试图声称这个类是微不足道的。
          • 我想说的是:构造函数必须是callable。在默认初始化期间调用了一个简单的构造函数。当然,根据 as-if 规则,编译器可能会选择不为该调用发出代码。对于值初始化和零初始化,一个普通的默认 ctor不 被调用,但它必须是可调用的。当然,这在实践上没有区别,而只是一种迂腐的说法。稍微简化一下,规则是:总是调用默认的ctor,它可能只是一个空操作。琐碎只对 value-init 很重要,它将为具有琐碎 ctor 的类执行零初始化。
          猜你喜欢
          • 1970-01-01
          • 2015-02-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-09-19
          相关资源
          最近更新 更多