(如果之前有人询问过,我很抱歉;搜索功能似乎被打破了:结果区域完全是空白的,即使它说有几页结果......在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) {}
};
所以......呃......那到底是怎么回事?语法rv signature(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) {} };
答案 0 :(得分:5)
构造函数定义的语法是:
Type( parameter-list ) : initialization-list
{
constructor-body
};
其中'initialization-list'是以逗号分隔的对base和/或成员属性的构造函数的调用列表。需要初始化没有默认构造函数,常量子对象和引用属性的任何子对象(基础或成员),并且在所有其他情况下应优先于构造函数块中的赋值。
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()
之前,该类将按以下顺序初始化其基类和属性:
base(int)
构造函数
base2(int)
构造函数
member1
x*2
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 *
};
答案 1 :(得分:3)
这个习语称为initialization list。
基本上每个项目都调用一个构造函数:
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*)
构造函数。
答案 2 :(得分:1)
编译器可以确定您正在调用基类的构造函数或正在进行初始化的天气的天气。
示例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
的成员变量作为你的类的一部分,因此它将使用提供的值初始化它。
您可以在同一个初始化列表中初始化成员并调用基类构造函数(这就是构造函数中的函数声明语法 - 初始化列表)。
答案 3 :(得分:1)
在构造函数中,您可以明确地为成员变量调用构造函数。
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!
};
无论如何,像这样明确地调用构造函数称为初始化程序。
编辑:确保按照您在标题中声明的顺序初始化成员,否则您的代码将在编译时发出警告。
答案 4 :(得分:0)
在构造函数初始值设定项列表中,您编写data_member(val)
以使用data_member
初始化val
。请注意,val
可能是一个表达式,即使是只能在运行时进行计算的表达式。如果数据成员是一个对象,那么将使用该值调用它的构造函数。此外,如果它有一个期望多个争论的构造函数,你可以将它们全部传递,就像在函数调用中一样,例如data_member(i, j, k)
。现在,出于这种初始化的目的,您应该将对象的基类部分视为数据成员,其名称只是基类的名称。因此MyBase(val)
或MyBase(i, ,j ,k)
。将调用基类的构造函数。多重继承以相同的方式工作。只需初始化您想要的任何基类作为列表中的单独项目:MyBase1(x), MyBase2(y)
。未明确调用其构造函数的基类将由其默认构造函数初始化(如果存在)。如果他们不这样做,除非您明确初始化,否则代码将无法编译。
答案 5 :(得分:0)
本书试图解释C ++ 初始化列表。通常,初始化列表由构造函数调用组成,包括父类构造函数和类属性构造函数。
初始化列表应包含(按顺序):
首先应该调用所有基类构造函数。基类构造函数调用的顺序由编译器定义。正如C++ FAQ所述:
[基类构造函数] ......是 按照它们出现的顺序执行 深度优先从左到右遍历 左边的基类图 右边是指顺序 基类名称的出现。
因此,初始化列表中的基类构造函数的顺序无关紧要。如果初始化列表中未明确列出基类构造函数,则将调用默认构造函数。
在基类构造函数调用之后是类属性构造函数调用。这些看起来像函数调用,但实质上是初始化称为构造函数初始化的变量的有效方法。例如,以下C ++代码完全有效:
int i(0);
请注意,初始化列表中类属性的顺序应与类标题中的定义顺序相匹配。
最后值得一提的是,使用初始化列表是一种很好的做法。它比在构造函数体中使用赋值更有效,因为它消除了使用默认值或未定义值的类属性的初始构造,并确保了严格的初始化顺序。
答案 6 :(得分:0)
C ++具有非常模糊语法,因此可以使用不同的语法来满足许多类似的语法结构。
例如:
A a(b);
这可能意味着通过调用带有参数值'b'的构造函数'A :: A'来创建类'A'的对象'a'。 但这也可以是函数'a'的声明,它具有类型'b'的形式参数并返回类型'A'的值。
对于“简单”语法,编译器可以实现为包含几乎独立模块的管道:Lexer,Parser,Semantic Analyzer等。 通常,这些语言不仅易于由编译器决定,而且易于人类(程序员)理解。
C ++有非常复杂的语法(和语义)。 因此,没有语义信息,C ++解析器无法决定应用哪种语法规则。 这导致设计和实现C ++编译器的困难。 另外,C ++使程序员理解程序成为问题。
因此,理解语法的问题的根源不在你的脑海中,而在于C ++的语法。
上述原因导致建议不要使用C ++(以及其他过于复杂的语言)来为初学者教授编程。 首先,使用一种简单(但功能强大的)语言来开发编程技能,然后转向主流语言。