关于 C++ 中构造函数(和多重继承)的一些基本问题?

发布于 2024-08-22 19:45:25 字数 1938 浏览 1 评论 0原文

(如果之前有人问过这个问题,我很抱歉;搜索功能似乎已损坏:结果区域完全是空白的,即使它说有几页结果......在 Chrome、FireFox 和 Safari 中)

所以,我刚刚学习 C++……而我正在阅读的这本书在以我可以掌握的方式解释构造函数方面做得非常糟糕。到目前为止,我几乎已经掌握了所有其他内容,但我无法弄清楚构造函数的语法实际上是如何工作的。

例如,我被告知以下内容将导致构造函数调用指定超类的构造函数:

class something : something_else {
  something(int foo, double bar) : something_else(int foo) {}
};

另一方面,在本书后面描述如何初始化 const 成员时,使用了相同的语法:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

那么……呃……到底发生了什么?语法 rvsignature(param) : Something_else(what); 实际上意味着什么?我无法弄清楚 something_else(what) 是什么以及它周围的代码。它似乎具有多种含义;我确信它所对应的语言一定有一些底层元素,我只是不知道是什么

编辑: 另外,我应该提到,上一个示例中的 what 有时是一个参数列表,这非常令人困惑(所以 something_else(what)看起来像一个函数签名)……有时是一个常量值表达式(所以 something_else(what) 看起来像一个函数调用)。

现在,继续:多重继承和构造函数怎么样?如何指定调用哪些父类的构造函数……以及默认调用哪些构造函数?我知道,默认情况下,以下两个是相同的......但我不确定当涉及多重继承时等效项是什么:

class something : something_else {
//something(int foo, double bar) : something_else() {}
  something(int foo, double bar) {}
};

任何帮助理解这些主题的帮助都将非常感激;我不喜欢这种感觉,因为我无法理解一些基本的东西。我一点也不喜欢它。

编辑2:好的,到目前为止,下面的答案都非常有帮助。不过,他们提出了这个问题的另一部分:“初始化列表”中基类构造函数调用的参数如何与您定义的构造函数相关?它们是否必须匹配……是否必须有默认值?他们必须匹配多少?换句话说,以下哪些行为非法

class something_else {
  something_else(int foo, double bar = 0.0) {}
  something_else(double gaz) {}
};


class something : something_else {
  something(int foo, double bar)  : something_else(int foo, double bar) {}   };
class something : something_else {
  something(int foo)              : something_else(int foo, double bar) {}   };
class something : something_else {
  something(double bar, int foo)  : something_else(double gaz) {}   };

(I’m sorry if this has been asked before; the search feature seems to be broken: the results area is completely blank, even though it says there are a few pages of results… in Chrome, FireFox, and Safari)

So, I’m just learning C++… and the book I’m moving through is doing a really bad job of explaining constructors in a way that I can grasp them. I’ve pretty much grokked everything else so far, but I can’t figure out how the syntax for constructors actually works.

For instance, I am told that the following will cause the constructor to call the designated superclass’s constructor:

class something : something_else {
  something(int foo, double bar) : something_else(int foo) {}
};

On the other hand, that same syntax was utilized later on in the book, when describing how to initialize const members:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

So… uh… what the hell is going on there? What does the syntax rv signature(param) : something_else(what); actually mean? I can’t figure out what that something_else(what) is, with relation to the code around it. It seems to take on multiple meanings; I’m sure there must be some underlying element of the language that it corresponds to, I just can’t figure out what.

Edit: Also, I should mention, it’s very confusing that the what in the previous example is sometimes a parameter list (so something_else(what) looks like a function signature)… and sometimes a constant-value expression (so something_else(what) looks like a function call).

Now, moving on: What about multiple-inheritance and constructors? How can I specify what constructors from which parent classes are called… and which ones are called by default? I’m aware that, by default, the following two are the same… but I’m not sure what the equivalent is when multiple-inheritance is involved:

class something : something_else {
//something(int foo, double bar) : something_else() {}
  something(int foo, double bar) {}
};

Any help in grokking these topics would be very appreciated; I don’t like this feeling that I’m failing to understand something basic. I don’t like it at all.

Edit 2: Okay, the answers below as of now are all really helpful. They raise one more portion of this question though: How do the arguments of base-class-constructor-calls in ‘initialization lists’ relate to the constructor you’re defining? Do they have to match… do there have to be defaults? How much do they have to match? In other words, which of the following are illegal:

class something_else {
  something_else(int foo, double bar = 0.0) {}
  something_else(double gaz) {}
};


class something : something_else {
  something(int foo, double bar)  : something_else(int foo, double bar) {}   };
class something : something_else {
  something(int foo)              : something_else(int foo, double bar) {}   };
class something : something_else {
  something(double bar, int foo)  : something_else(double gaz) {}   };

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

洒一地阳光 2024-08-29 19:45:25

构造函数定义的语法是:

Type( parameter-list ) : initialization-list 
{
   constructor-body
};

其中“初始化列表”是对基类和/或成员属性的构造函数的调用的逗号分隔列表。需要初始化任何没有默认构造函数、常量子对象和引用属性的子对象(基类或成员),并且在所有其他情况下应优先于构造函数块中的赋值。

struct base {
   base( int ) {};
};
struct base2 {
   base2( int ) {};
};
struct type : base, base2
{
   type( int x ) 
      : member2(x), 
        base2(5), 
        base(1), 
        member1(x*2) 
   { f(); }
   int member1;
   int member2;
};

初始化列表的执行顺序在类声明中定义:基类按照声明的顺序,成员属性按照声明的顺序。在上面的示例中,在构造函数主体中执行 f() 之前,该类将按以下顺序初始化其基类和属性:

  1. 使用参数 1 调用 base(int) 构造函数
  2. 使用参数 5 调用 base2(int) 构造函数 用
  3. x*2 初始化 member1 用值 x*2
  4. 初始化 member2 用值 >x

当您引入虚拟继承时,虚拟基类在虚拟继承层次结构的最派生类中进行初始化,因此它可以(或者如果没有默认构造函数,则必须)出现在该初始化列表中。在这种情况下,虚拟基类将在从该基类虚拟继承的第一个子对象之前初始化。

class unrelated {};
class base {};
class vd1 : virtual base {};
class vd2 : virtual base {};
struct derived : unrelated, vd1, vd2 {
   derived() : unrelated(), base(), vd1(), vd2() {} // in actual order
};

关于编辑 2

我认为您没有阅读答案中的详细信息。初始化列表中的元素是构造函数调用,而不是声明。如果合适,编译器将为调用应用通常的转换规则。

struct base {
   base( int x, double y );
   explicit base( char x );
};
struct derived : base {
   derived() : base( 5, 1.3 ) {}
   derived( int x ) : base( x, x ) {} 
      // will convert x into a double and call base(int,double)
   derived( double d ) : base( 5 ) {} 
      // will convert 5 to char and call base(char)
// derived( base b ) {} // error, base has no default constructor
// derived( base b, int x ) : base( "Hi" ) {} 
      // error, no constructor of base takes a const char *
};

The syntax for a constructor definition is:

Type( parameter-list ) : initialization-list 
{
   constructor-body
};

Where the 'initialization-list' is a comma separated list of calls to constructors for the bases and/or member attributes. It is required to initialize any subobject (base or member) for which there is no default constructor, constant subobjects and reference attributes, and should be preferred over assignment in the constructor block in all other cases.

struct base {
   base( int ) {};
};
struct base2 {
   base2( int ) {};
};
struct type : base, base2
{
   type( int x ) 
      : member2(x), 
        base2(5), 
        base(1), 
        member1(x*2) 
   { f(); }
   int member1;
   int member2;
};

The order in which the initialization list is executed is defined in the class declaration: bases in the order in which they are declared, member attributes in the order in which they are declared. In the example above before executing f() in the constructor body the class will initialize its base classes and attributes in the following sequence:

  1. call base(int) constructor with parameter 1
  2. call base2(int) constructor with parameter 5
  3. initialize member1 with value x*2
  4. initialize member2 with value x

When you throw in virtual inheritance, the virtual base is initialized in the most derived class of the virtual inheritance hierachy, and as such it can (or must if there is no default constructor) appear in that initialization list. In that case, the virtual base will be initialized right before the first subobject that inherits virtually from that base.

class unrelated {};
class base {};
class vd1 : virtual base {};
class vd2 : virtual base {};
struct derived : unrelated, vd1, vd2 {
   derived() : unrelated(), base(), vd1(), vd2() {} // in actual order
};

On Edit 2

I think you are not reading the details in the answers. The elements in the initialization list are constructor calls, not declarations. The compiler will apply the usual conversion rules for the call if appropriate.

struct base {
   base( int x, double y );
   explicit base( char x );
};
struct derived : base {
   derived() : base( 5, 1.3 ) {}
   derived( int x ) : base( x, x ) {} 
      // will convert x into a double and call base(int,double)
   derived( double d ) : base( 5 ) {} 
      // will convert 5 to char and call base(char)
// derived( base b ) {} // error, base has no default constructor
// derived( base b, int x ) : base( "Hi" ) {} 
      // error, no constructor of base takes a const char *
};
第七度阳光i 2024-08-29 19:45:25

这个习惯用法称为初始化列表

基本上,对于每个项目,您都调用一个构造函数:

class C: public A, public B {
    int a;
    std::string str;

public:
    C(): 
        A(5),            // 1
        B('c'),          // 2
        a(5),            // 3
        str("string")    // 4
    {};
};

在 (1) 中,您调用基类构造函数,该基类构造函数将 int 作为参数,或者可以执行适当的转换。

在 (2) 中,您调用以 char 作为参数的基类构造函数

在 (3) 中,您调用“构造函数”来初始化 int,在本例中是简单的

赋值(4) 调用 std::string(const char*) 构造函数。

This idiom is called initialization list.

Basically with each item you call a constructor:

class C: public A, public B {
    int a;
    std::string str;

public:
    C(): 
        A(5),            // 1
        B('c'),          // 2
        a(5),            // 3
        str("string")    // 4
    {};
};

At (1) you call base class constructor that takes int as a parameter or can perform appropriate conversion.

At (2) you call base class constructor that takes char as a parameter

At (3) you call "constructor" to initialize an int which in this case is simple assignment

At (4) you call std::string(const char*) constructor.

生活了然无味 2024-08-29 19:45:25

编译器可以确定您是否正在调用基类的构造函数或是否正在进行初始化。

示例 1:

class something : something_else {
  void something(int foo, double bar) : something_else(int foo) {}
};

编译器可以看到您提供的名称属于基类。因此它将调用基类中相应的构造函数。

示例 2:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

编译器可以看到您的类中有一个名为 constant_member 的成员变量,因此它将使用提供的值对其进行初始化。

您可以在同一初始化列表中初始化成员并调用基类构造函数(这就是构造函数中的函数声明语法 - 初始化列表)。

The compiler can determine weather you are calling the constructor of a base class or weather you are making an initialization.

Example 1:

class something : something_else {
  void something(int foo, double bar) : something_else(int foo) {}
};

The compiler can see that the name you are supplying belongs to a base class. It will therefore call the corresponding constructor in the base class.

Example 2:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

The compiler can see you have a member variable called constant_member as part of your class, therefore it will initialize it with the value supplied.

You can initialize members and call base class constructors in the same initialization list (thats what the function-declaration syntax in the constructor is -- a initialization list).

晨敛清荷 2024-08-29 19:45:25

在构造函数中,您可以显式调用成员变量的构造函数。

class FileOpener
{
public:
  // Note: no FileOpener() constructor
  FileOpener( string path ){ //Opens a file }
};
class A
{
public:
  A():b("../Path/To/File.txt"){}
  FileOpener b;
};

当您的成员变量没有默认构造函数时,这一点至关重要。

同样,当父类的默认构造函数不起作用或不存在时,您可以显式调用父类的构造函数。

class F
{
public:
  // Note: No default constructor again.
  F( int arg ){ var = arg;}
private:
  int var;
};
class D : public F
{
  D(){} //Compiler error! Constructors try to use the parent's default C 
        // constructor by default.
  D( int arg ):C(arg){} //This works!
};

无论如何,像这样显式调用构造函数称为初始化程序。

编辑:请务必按照您在标头中声明成员的顺序初始化成员,否则您的代码将在编译时出现警告。

In your constructor, you can explicity call constructors for your member variables.

class FileOpener
{
public:
  // Note: no FileOpener() constructor
  FileOpener( string path ){ //Opens a file }
};
class A
{
public:
  A():b("../Path/To/File.txt"){}
  FileOpener b;
};

This is essential when your member variables don't have default constructors.

Similarly, you can explicitly call the constructor for your parent class when its default constructor won't do or doesn't exist.

class F
{
public:
  // Note: No default constructor again.
  F( int arg ){ var = arg;}
private:
  int var;
};
class D : public F
{
  D(){} //Compiler error! Constructors try to use the parent's default C 
        // constructor by default.
  D( int arg ):C(arg){} //This works!
};

Anyway, calling a constructor explicitly like this is called an initializer.

Edit: Be sure to initialize members in the order you declared them in your header or your code will have warnings at compile time.

百思不得你姐 2024-08-29 19:45:25

在构造函数初始值设定项列表中,您可以编写 data_member(val) 以便使用 val 初始化 data_member。请注意,val 可能是一个表达式,甚至是只能在运行时计算的表达式。如果数据成员是一个对象,则将使用该值调用它的构造函数。此外,如果它有一个需要多个参数的构造函数,您可以将它们全部传递,就像在函数调用中一样,例如 data_member(i, j, k)。现在,为了进行此类初始化,您应该将对象的基类部分视为一个数据成员,其名称只是基类的名称。因此 MyBase(val)MyBase(i, ,j ,k)。将调用基类的构造函数。多重继承以同样的方式工作。只需将您想要的基类初始化为列表中的单独项目即可:MyBase1(x)、MyBase2(y)。您未显式调用其构造函数的基类将由其默认构造函数(如果存在)进行初始化。如果不这样做,除非您显式初始化,否则代码将无法编译。

In constructor initializer lists you write data_member(val) in order to initialize data_member with val. Note that val may be an expression, even one that can only be evaluated at runtime. If the data member is an object, it's constructor will be called with that value. Furthermore, if it has a constructor expecting several argumets, you can pass them all, just like in a function call, e.g., data_member(i, j, k). Now, for the purpose of such initializations you should think of the base class part of the object as a data member whose name is simply the base class' names. Hence MyBase(val) or MyBase(i, ,j ,k). The base class's constructor will be called. Multiple inheritence works in the same way. Just initialize whichever base classes you want as separate items on the list: MyBase1(x), MyBase2(y). Base classes whose constructors you don't call explicitly will be initialized by their default constructors, if these exist. If they don't, the code won't compile unless you initialize explicitly.

避讳 2024-08-29 19:45:25

本书试图解释 C++ 初始化列表。一般来说,初始化列表由构造函数调用组成,包括对父类构造函数和类属性构造函数的调用。

初始化列表应包含(按顺序):

  1. 基类构造函数
  2. 类属性构造函数

首先应调用所有基类构造函数。基类构造函数的调用顺序由编译器定义。正如 C++ 常见问题解答所述:

[基类构造函数]...是
按照它们出现的顺序执行
深度优先从左到右遍历
基类图,左边
向右参考顺序
基类名称的外观。

因此,初始化列表中基类构造函数的顺序并不重要。如果初始化列表中未显式列出基类构造函数,则将调用默认构造函数。

基类构造函数调用之后是类属性构造函数调用。这些看起来像函数调用,但本质上是初始化变量的有效方法,称为构造函数初始化。例如,下面的 C++ 代码完全有效:

int i(0);

请注意,初始化列表中类属性的顺序应与类头中定义的顺序相匹配。

最后值得一提的是,使用初始化列表是一种很好的做法。它比在构造函数主体中使用赋值更有效,因为它消除了使用默认或未定义值的类属性的初始构造,并且确保了严格的初始化顺序。

The book is trying to explain C++ initialization lists. In general, an initialization list consists of constructor calls, both to parent class constructors and class property constructors.

An initialization list should consist of (in order):

  1. Base class constructors
  2. Class property constructors

First all base class constructors should be called. The order of base class constructor calls is defined by the compiler. As stated by the C++ FAQ:

[Base class constructors]...are
executed in the order they appear in a
depth-first left-to-right traversal of
the graph of base classes, where left
to right refer to the order of
appearance of base class names.

Thus, the order of the base class constructors in the initialization list does not matter. In case a base class constructor is not explicitly listed in the intialization list, the default constructor will be called.

Following the base class constructor calls are the class property constructor calls. These look like function calls, but are in essence a valid way of initializing variables called constructor initialization. For example, the following piece of C++ code is perfectly valid:

int i(0);

Note that the order of the class properties in the initialization list should match the order of definition in the class header.

Finally it is worth mentioning that using initialization lists is good practice. It is more efficient than using assignments in the constructor body since it eliminates the initial construction of the class property with a default or undefined value, and it ensures a strict order of initialization.

我还不会笑 2024-08-29 19:45:25

C++ 的语法非常模糊,因此要准备好迎接许多具有不同语义的类似语法结构。

例如:

A a(b);

这可能意味着通过使用参数值“b”调用其构造函数“A::A”来创建类“A”的对象“a”。
但这也可以是具有类型“b”的形式参数并返回类型“A”的值的函数“a”的声明。

对于“简单”语法,编译器可以实现为包含几乎独立模块的管道:词法分析器、解析器、语义分析器等。
通常,这样的语言不仅易于编译器决定,而且易于人类(程序员)理解。

C++ 具有非常复杂的语法(和语义)。
因此,如果没有语义信息,C++ 解析器无法决定应用哪种语法规则。
这给设计和实现 C++ 编译器带来了困难。
此外,C++ 给程序员理解程序带来了问题。

所以你在理解语法方面的问题根源不在于你的头脑,而在于 C++ 的语法。

上述原因导致建议不要使用 C++(和其他过于复杂的语言)来向初学者教授编程。
首先,使用简单(但足够强大)的语言来培养编程技能,然后转向主流语言。

C++ has very ambiguous syntax, so be ready to meet many similar syntax constructions with different symantics.

For example:

A a(b);

This could mean a creation of an object 'a' of class 'A' by calling its contructor 'A::A' with a parameter value 'b'.
But also this can be a declaration of function 'a' having a formal parameter of type 'b' and returning a value of type 'A'.

For a "simple" grammar the compiler can be implemented as a pipeline containing almost independent modules: Lexer, Parser, Semantic Analyzer, etc.
Usually, such languages are not only simple for deciding by compilers but also are easy for understanding by humans (programmers).

C++ has very complicated grammar (and semantics).
Therefore, without semantic information, C++ parser cannot decide which grammar rule to apply.
This leads to difficulties in designing and implementing a C++ compiler.
In addition, C++ makes it problematic to understand the programs by programmers.

So the root of your problems in understanding the syntax is not in your head but in the grammar of C++.

The reasons above leads to recommendations not to use C++ (and other overcomplicated languages) for teaching programming to beginners.
First, use a simple (but powerful enought) language for developyng programming skills, then move to mainstream languages.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文