我们以实际的例子来辅助理解。并且,我们确保这里的每一个给出的例子都是可以完整运行的代码。
#include <string>
using namespace std;
struct S1 {
string s;
};
S1 x; // OK: x.s is initialized to "" 1️⃣
struct X { X(int); };
struct S2 {
X x;
};
// clangd: Call to implicitly-deleted default constructor of 'S2'
// S2 x1; // error 2️⃣
S2 x2{1}; // OK: x2.x is initialized with 1 3️⃣
int main(int argc, char *argv[]) {
return 0;
}
上面的代码是本杰明给出的,也就是那个 C++ 之父,在他的那本经典的 C++ Programming Language 中。
按照顺序一个一个来。
1️⃣ S1 是有编译器合成的一个构造函数的。这里要搞清楚一个问题,什么是 built-in types,这是 p265 提到的,关于这个问题,我们可以看微软的这个文档:https://learn.microsoft.com/en-us/cpp/cpp/fundamental-types-cpp?view=msvc-170,简单来讲,其实也就是一些诸如 char、int、float 之类的内置类型。书中有这样一句话:
The default constructor works for Sales_data only because we provide initializers for the data memebers with built-in type.
使用 deepl 翻译一下,得到:
默认构造函数只适用于 Sales_data,因为我们为内置类型的数据成员提供了初始化器。
也就是说,它是仅仅针对书里提供的这里的特定的代码的,因为编译器是已经给内置的类型提供了初始化器这样一种东西的,所以,即使下面的代码中,我们如果把 units_sold 的初始化值给去掉,代码也是没问题的。对于 std::string,虽然它不是内置类型,但是它属于标准库中的经典类型,所以它也是提供了默认的初始化器的,我们不用关注它。
#include <iostream>
#include <string>
struct Sales_data {
// constructors added
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p * n) { }
// other members as before
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data s;
int main(int argc, char *argv[]) {
std::cout << s.isbn() << " " << s.units_sold << '\\n';
return 0;
}
结合上面的例子,我们来举一个反例,这个反例来自(改编自) cppref,
struct A {
int& ref;
};
struct B {
B() = default;
A a;
};
int main(int argc, char *argv[]) {
A a;
B b; // compile error
return 0;
}
可以看到,上面的这个代码是无法编译成功的。因为 B 的成员 A 没有得到初始化(因为 A 中的引用类型导致其自身的默认构造函数失效/deleted,具体可以看 cppref: https://en.cppreference.com/w/cpp/language/default_constructor),所以,这里的 B 的默认构造器当然也是不能生效的了。
回到上面的2️⃣,再来解释一下为什么上面是一个 error。问题的原因是,X 没有默认构造器,因为 X 中已经有一个声明好的非默认构造器了,而且,这个构造器的参数没有默认值,所以,不能作为默认的构造器,因此,这个结构体/类就没有默认构造器了。
最后,再看一下3️⃣,这里的道理是聚合初始化,实际上,聚合初始化是列表初始化的类别之一,而列表初始化的表现形式就是我们经常看见的花括号初始化。正是由于聚合初始化的特性,所以我们可以把花括号中的值传递给 X 进行初始化,而 X 正好有一个构造器,那么,这里就可以顺利运行这个程序了。这里再次明确一点,X 是没有默认构造器的,因为已经是 delete 了,进而,受影响的 S2 也是没有默认构造器的。