从派生类调用基类构造函数的 C++ 规则是什么?
例如,我知道在 Java 中,您必须将其作为子类构造函数的第一行(如果您不这样做,则假定隐式调用无参数超级构造函数 - 如果缺少,则会出现编译错误) .
super class
也称为 base class
并且在 qt 工具包 parent class
中也称为 fe - 按此顺序 sub class
也称为 child class
也许这有助于消除术语上的一些潜在混淆
如果它们没有参数,则会自动为您调用基类构造函数。如果要调用带参数的超类构造函数,则必须使用子类的构造函数初始化列表。与 Java 不同,C++ 支持多重继承(无论好坏),因此必须通过名称引用基类,而不是“super()”。
class SuperClass
{
public:
SuperClass(int foo)
{
// do something with foo
}
};
class SubClass : public SuperClass
{
public:
SubClass(int foo, int bar)
: SuperClass(foo) // Call the superclass constructor in the subclass' initialization list.
{
// do something with bar
}
};
有关构造函数的初始化列表 here 和 here 的更多信息。
在 C++ 中,所有超类和成员变量的无参数构造函数都会在进入构造函数之前为您调用。如果你想向它们传递参数,有一个单独的语法称为“构造函数链接”,它看起来像这样:
class Sub : public Base
{
Sub(int x, int y)
: Base(x), member(y)
{
}
Type member;
};
如果此时运行任何东西抛出,先前已完成构造的基/成员将调用其析构函数,并将异常重新抛出给调用者。如果要在链接期间捕获异常,则必须使用函数 try 块:
class Sub : public Base
{
Sub(int x, int y)
try : Base(x), member(y)
{
// function body goes here
} catch(const ExceptionType &e) {
throw kaboom();
}
Type member;
};
在这种形式中,请注意 try 块是函数体,而不是在函数体内部;这允许它捕获由隐式或显式成员和基类初始化以及函数体期间引发的异常。但是,如果函数 catch 块没有抛出不同的异常,则运行时将重新抛出原始错误;初始化期间的异常不容忽视。
在 C++ 中有一个构造函数初始化列表的概念,您可以并且应该在其中调用基类的构造函数,并且还应该在其中初始化数据成员。初始化列表在冒号之后的构造函数签名之后,构造函数主体之前。假设我们有一个 A 类:
class A : public B
{
public:
A(int a, int b, int c);
private:
int b_, c_;
};
然后,假设 B 有一个采用 int 的构造函数,A 的构造函数可能如下所示:
A::A(int a, int b, int c)
: B(a), b_(b), c_(c) // initialization list
{
// do something
}
可以看到,在初始化列表中调用了基类的构造函数。顺便说一下,初始化初始化列表中的数据成员比在构造函数的主体内分配 b_ 和 c_ 的值更可取,因为您节省了额外的赋值成本。
请记住,数据成员始终按照它们在类定义中声明的顺序进行初始化,而不管它们在初始化列表中的顺序如何。为避免在数据成员相互依赖时可能出现的奇怪错误,您应始终确保初始化列表和类定义中成员的顺序相同。出于同样的原因,基类构造函数必须是初始化列表中的第一项。如果您完全省略它,则将自动调用基类的默认构造函数。在这种情况下,如果基类没有默认构造函数,则会出现编译器错误。
每个人都提到了通过初始化列表调用构造函数,但没有人说可以从派生成员的构造函数体中显式调用父类的构造函数。例如,请参阅问题 Calling a constructor of the base class from a subclass' constructor body。关键是,如果您在派生类的主体中使用对父类或超类构造函数的显式调用,这实际上只是创建父类的实例,而不是在派生对象上调用父类构造函数.在派生类的对象上调用父类或超类构造函数的唯一方法是通过初始化列表,而不是在派生类构造函数体中。所以也许它不应该被称为“超类构造函数调用”。我把这个答案放在这里是因为有人可能会感到困惑(就像我一样)。
MyClass::MyClass() { new (this) BaseClass; /* UB, totally wrong */ }
- 这是用于显式调用构造函数的 C++ 语法。这就是“构造函数调用”的样子。所以这个荒谬的错误答案被赞成的事实对我来说完全是个谜。
MyClass()
不是任何类型的“调用”!它与例如 int()
具有相同的含义,并且它创建一个值!
如果您有一个没有参数的构造函数,它将在派生类构造函数执行之前被调用。
如果要使用参数调用基构造函数,则必须在派生构造函数中显式编写,如下所示:
class base
{
public:
base (int arg)
{
}
};
class derived : public base
{
public:
derived () : base (number)
{
}
};
如果不调用 C++ 中的父构造函数,就无法构造派生类。如果它是非 arg C'tor,则会自动发生这种情况,如果您如上所示直接调用派生构造函数,或者您的代码将无法编译,则会发生这种情况。
将值传递给父构造函数的唯一方法是通过初始化列表。初始化列表是用一个 : 实现的,然后是一个类列表和要传递给该类构造函数的值。
Class2::Class2(string id) : Class1(id) {
....
}
还要记住,如果您有一个不带父类参数的构造函数,它将在子构造函数执行之前自动调用。
如果您的基构造函数中有默认参数,则将自动调用基类。
using namespace std;
class Base
{
public:
Base(int a=1) : _a(a) {}
protected:
int _a;
};
class Derived : public Base
{
public:
Derived() {}
void printit() { cout << _a << endl; }
};
int main()
{
Derived d;
d.printit();
return 0;
}
输出为:1
Base()
,它与 Base(int)
具有相同的主体,但加上 : _aBase()
的隐式初始化程序。如果在 init-list 中没有链接特定的基本构造函数,则始终调用 Base()
。而且,正如其他地方所提到的,C++11 的委托构造函数和大括号或等号初始化使得默认参数变得不太必要(当它们在很多示例中已经是代码味道时)。
CDerived::CDerived()
: CBase(...), iCount(0) //this is the initialisation list. You can initialise member variables here too. (e.g. iCount := 0)
{
//construct body
}
当一个类派生自多个类时,没有人提到构造函数调用的顺序。在派生类时,序列如前所述。
如果您只是想将所有构造函数参数传递给基类(=parent),这是一个最小的示例。
这使用模板将每个带有 1、2 或 3 个参数的构造函数调用转发到父类 std::string
。
代码
#include <iostream>
#include <string>
class ChildString: public std::string
{
public:
template<typename... Args>
ChildString(Args... args): std::string(args...)
{
std::cout
<< "\tConstructor call ChildString(nArgs="
<< sizeof...(Args) << "): " << *this
<< std::endl;
}
};
int main()
{
std::cout << "Check out:" << std::endl;
std::cout << "\thttp://www.cplusplus.com/reference/string/string/string/" << std::endl;
std::cout << "for available string constructors" << std::endl;
std::cout << std::endl;
std::cout << "Initialization:" << std::endl;
ChildString cs1 ("copy (2)");
char char_arr[] = "from c-string (4)";
ChildString cs2 (char_arr);
std::string str = "substring (3)";
ChildString cs3 (str, 0, str.length());
std::cout << std::endl;
std::cout << "Usage:" << std::endl;
std::cout << "\tcs1: " << cs1 << std::endl;
std::cout << "\tcs2: " << cs2 << std::endl;
std::cout << "\tcs3: " << cs3 << std::endl;
return 0;
}
输出
Check out:
http://www.cplusplus.com/reference/string/string/string/
for available string constructors
Initialization:
Constructor call ChildString(nArgs=1): copy (2)
Constructor call ChildString(nArgs=1): from c-string (4)
Constructor call ChildString(nArgs=3): substring (3)
Usage:
cs1: copy (2)
cs2: from c-string (4)
cs3: substring (3)
更新:使用可变参数模板
推广到 n 个参数并简化
template <class C>
ChildString(C arg): std::string(arg)
{
std::cout << "\tConstructor call ChildString(C arg): " << *this << std::endl;
}
template <class C1, class C2>
ChildString(C1 arg1, C2 arg2): std::string(arg1, arg2)
{
std::cout << "\tConstructor call ChildString(C1 arg1, C2 arg2, C3 arg3): " << *this << std::endl;
}
template <class C1, class C2, class C3>
ChildString(C1 arg1, C2 arg2, C3 arg3): std::string(arg1, arg2, arg3)
{
std::cout << "\tConstructor call ChildString(C1 arg1, C2 arg2, C3 arg3): " << *this << std::endl;
}
至
template<typename... Args>
ChildString(Args... args): std::string(args...)
{
std::cout
<< "\tConstructor call ChildString(nArgs="
<< sizeof...(Args) << "): " << *this
<< std::endl;
}
std::endl
。人们看到这一点并将其放入循环中,并想知道为什么“在 C++ 中”将一堆行写入文本文件比使用 fprintf
慢 5 到 20 倍。 TL;DR:使用 "\n"
(添加到现有字符串文字,如果有的话),并且仅在需要将缓冲区刷新到文件时使用 std::endl
(例如,如果代码崩溃并且您想查看它的调试最后的话)。我认为 std::endl
是一个方便的设计错误:一个很酷的“小工具”,它的作用远比名字所暗示的要多。
SubClass anObject(1,2)
实例化一个子类对象时,1
是否会传递给SuperClass(foo)
(成为参数foo
的参数)?我一直在搜索高低文档,但没有一个明确说明 SubClass 构造函数的参数可以作为参数传递给 SuperClass 构造函数。: SuperClass(foo)
部分。foo
被显式传递给超类的构造函数。