文章目录
介绍
在现代 C++(自 C++11 起)中, 有许多新特性可以简化我们的工作,
使代码更直观. 本文介绍有关类成员初始化方面的知识.
构造函数
构造函数是一种特殊的成员函数, 用于初始化类的对象.
构造函数的名称与类名称相同, 没有返回类型, 也没有返回值.
构造函数可以有参数, 也可以没有参数.
#include <iostream>
struct Position {
int x;
int y;
Position() : x(0), y(0) { std::cout << "Default constructor called\n"; }
Position(int _x, int _y) : x(_x), y(_y) {
std::cout << "Constructor with arguments called, {x: " << x << ", y: " << y
<< "}\n";
}
};
int main() {
Position a; // calls the default constructor
Position b(1, 2); // calls the constructor with two arguments
Position c{3, 4}; // another call using brace initialization
}
可以看到Position
类有两个构造函数, 一个是默认构造函数, 另一个是带有两个参数的构造函数.
需要注意的是编译器是按照类成员声明的顺序初始化的, 而不是按照初始化列表的顺序. 如果出现两者的顺序不一致, 则会出现警告.
拷贝构造函数
拷贝构造函数是一种特殊的成员函数, 用于创建一个新对象, 该对象是另一个对象的副本.
#include <iostream>
struct Position {
int x;
int y;
Position() : x(0), y(0) { std::cout << "Default constructor called\n"; }
Position(int _x, int _y) : x(_x), y(_y) {
std::cout << "Constructor with arguments called, {x: " << x << ", y: " << y
<< "}\n";
}
Position(const Position& rhs) : x(rhs.x), y(rhs.y) {
std::cout << "Copy constructor called, {x: " << x << ", y: " << y << "}\n";
}
};
int main() {
Position a; // calls the default constructor
Position b(1, 2); // calls the constructor with two arguments
Position c{3, 4}; // another call using brace initialization
Position d(b); // calls the copy constructor
Position e{c}; // calls the copy constructor
Position f = a; // calls the copy constructor
}
移动构造函数(From C++11)
移动构造函数有一个右值引用作为参数, 用于将临时对象的资源转移到新对象.
#include <iostream>
struct Position {
int x;
int y;
Position() : x(0), y(0) { std::cout << "Default constructor called\n"; }
Position(int _x, int _y) : x(_x), y(_y) {
std::cout << "Constructor with arguments called, {x: " << x << ", y: " << y
<< "}\n";
}
Position(const Position& rhs) : x(rhs.x), y(rhs.y) {
std::cout << "Copy constructor called, {x: " << x << ", y: " << y << "}\n";
}
Position(Position&& rhs) noexcept : x(rhs.x), y(rhs.y) {
std::cout << "Move constructor called, {x: " << x << ", y: " << y << "}\n";
}
// overload + operator
friend Position&& operator+(const Position& lhs, const Position& rhs) {
return std::move(Position(lhs.x + rhs.x, lhs.y + rhs.y));
}
};
int main() {
Position a; // calls the default constructor
Position b(1, 2); // calls the constructor with two arguments
Position c{3, 4}; // another call using brace initialization
Position d(b); // calls the copy constructor
Position e{c}; // calls the copy constructor
Position f = a; // calls the copy constructor
Position g(std::move(a)); // calls the move constructor
Position h(b + c); // calls the move constructor for temporary object
}
与赋值操作符的区别
#include <iostream>
struct Position {
int x;
int y;
Position() : x(0), y(0) { std::cout << "Default constructor called\n"; }
Position(int _x, int _y) : x(_x), y(_y) {
std::cout << "Constructor with arguments called, {x: " << x << ", y: " << y
<< "}\n";
}
Position(const Position& rhs) : x(rhs.x), y(rhs.y) {
std::cout << "Copy constructor called, {x: " << x << ", y: " << y << "}\n";
}
Position(Position&& rhs) noexcept : x(rhs.x), y(rhs.y) {
std::cout << "Move constructor called, {x: " << x << ", y: " << y << "}\n";
}
// overload + operator
friend Position&& operator+(const Position& lhs, const Position& rhs) {
return std::move(Position(lhs.x + rhs.x, lhs.y + rhs.y));
}
// copy assignment operator
Position& operator=(const Position& rhs) {
x = rhs.x;
y = rhs.y;
std::cout << "Copy assignment operator called, {x: " << x << ", y: " << y
<< "}\n";
return *this;
}
Position& operator=(Position&& rhs) noexcept {
x = rhs.x;
y = rhs.y;
std::cout << "Move assignment operator called, {x: " << x << ", y: " << y
<< "}\n";
return *this;
}
};
int main() {
Position a;
Position b{a}; // copy constructor called!
Position c = b; // copy ctor called!
c = a; // assignment operator called!
std::cout << "====================\n";
Position e{1, 2}, f;
Position g{e + f}; // move constructor called for the temporary!
g = e + f; // move assignment called
}
委托构造函数(From C++11)
委托构造函数是一个构造函数, 它调用同一个类中的另一个构造函数.
下面的例子中, 我们定义了一个委托构造函数,
它调用了带有两个参数的构造函数.
struct Position {
int x;
int y;
Position() : Position(0, 0) {}
Position(int _x, int _y) : x(_x), y(_y) {}
};
int main() {
Position a;
Position b(1, 2);
}
委托构造函数的限制
过多的委托构造函数可能会导致一些错误和递归调用. 参考下面例子:
class Car {
public:
Car() : Car(0, 0, 0) {}
Car(int x, int y) : Car(x, y, 0) {}
Car(int x, int y, int z) : Car(x, y) {}
private:
int x;
int y;
int z;
};
int main() {
Car x;
Car b(1, 2, 3);
}
另外一个限制是不能同时使用构造函数列表和委托构造函数.
非静态数据成员初始化
数据成员默认值(From C++11)
从C++11开始, 我们可以在类定义中为非静态数据成员提供默认值.
#include <cassert>
struct Position {
int x = 0;
int y = 0;
};
int main() {
Position a; // calls the default constructor
assert(a.x == 0);
assert(a.y == 0);
}
如何工作
#include <cassert>
#include <iostream>
int getX() {
std::cout << "getX called\n";
return 1;
}
int getY() {
std::cout << "getY called\n";
return 2;
}
struct Position {
int x = getX();
int y = getY();
Position() = default;
// 此处没有初始化y, y会使用默认初始化
Position(int _x) : x(_x) {}
};
int main() {
Position a; // calls the default constructor
std::cout << "====================\n";
Position b(10); // calls the constructor with one argument
}
上面函数的输出结果是:
getX called
getY called
====================
getY called
拷贝构造函数
#include <cassert>
#include <iostream>
int getX() {
std::cout << "getX called\n";
return 1;
}
int getY() {
std::cout << "getY called\n";
return 2;
}
struct Position {
int x = getX();
int y = getY();
Position() = default;
// 此处没有初始化y, y会使用默认初始化
Position(int _x) : x(_x) {}
Position(const Position& rhs) {
std::cout << "copy ctor\n";
x = rhs.x;
y = rhs.y;
};
};
int main() {
Position a; // calls the default constructor
std::cout << "====================\n";
Position b = a; // calls the copy constructor
}
上面函数的输出结果是:
getX called
getY called
====================
getX called
getY called
copy ctor
可以看到成员变量此时先被赋值,接着被覆盖.
改进方法:
Position(const Position& rhs) = default;
// or
Position(const Position& rhs) : x(rhs.x), y(rhs.y) { }
移动构造函数
#include <cassert>
#include <iostream>
int getX() {
std::cout << "getX called\n";
return 1;
}
int getY() {
std::cout << "getY called\n";
return 2;
}
struct Position {
int x = getX();
int y = getY();
Position() = default;
// 此处没有初始化y, y会使用默认初始化
Position(int _x) : x(_x) {}
Position(const Position& rhs) {
std::cout << "copy ctor called\n";
x = rhs.x;
y = rhs.y;
};
Position(const Position&& rhs) {
std::cout << "move ctor called\n";
x = rhs.x;
y = rhs.y;
};
};
int main() {
Position a; // calls the default constructor
std::cout << "====================\n";
Position b = std::move(a); // calls the copy constructor
}
上面函数的输出结果是:
getX called
getY called
====================
getX called
getY called
move ctor called
我们还是看到成员变量此时先被赋值,接着被覆盖. 可以采用类似的方法避免
Position(Position&&) = default;
// or
Position(Position&& rhs) : x(std::move(rhs.x)), y(std::move(rhs.y)) { }
C++14的更新
起初在C++11中, 如果使用默认成员初始化, 你的类不能是一个聚合类型:
struct Point {
int x = 1.0;
int y = 2.0;
};
// won't compile in C++11
Point a{1, 2};
在C++14中, 这个限制被取消了, 你可以在聚合类型中使用默认成员初始化:
#include <iostream>
struct Point {
int x = 1.0;
int y = 2.0;
};
int main() {
Point a{1, 2};
std::cout << a.x << ", " << a.y << '\n';
}
聚合类型是下面的类型之一: An aggregate is one of the following types:
-
数组类型
-
包含如下条件的类:
-
没有私有或保护的非静态数据成员
-
没有用户声明或继承的构造函数
-
没有虚拟, 私有或保护的基类
-
没有虚拟成员函数
-
C++20的更新
增加了对位域的支持
#include <iostream>
struct CompatStruct {
int type : 4 {1};
int flag : 4 {2};
};
int main() {
CompatStruct t;
std::cout << t.type << '\n';
std::cout << t.flag << '\n';
}
优缺点
优点
-
容易编写
-
可以确保每个成员都被正确初始化
-
声明和默认值在同一个地方, 更容易维护
-
特别适用于有多个构造函数的情况
-
-
当你有很多个构造函数的时候非常有用
缺点
-
性能方面, 如果你有性能关键的数据结构,
你可能想要一个"空"的初始化代码. 你可能会有未初始化的数据成员,
但是你可能会节省几个CPU指令. -
(仅限于C++14)
NSDMI使类在C++11中不是聚合的. 参见关于C++14更改的部分. -
由于默认值在头文件中,
任何更改可能需要重新编译依赖的编译单元. 如果只在实现文件中设置值,
则不会出现这种情况. 如果使用了C++ Module,可能这方面就不用考虑.
C++17静态数据成员初始化
在C++18之前, 静态成员变量的初始化必须在类的定义外部进行, 例如: 头文件:
struct Position {
static int unit;
};
实现文件:
int Position::unit = 0;
唯一的例外是一个静态常量整数变量, 你可以在一个地方声明和初始化:
class Position {
static const int ImportantValue = 42;
};
在C++17中, 你可以使用内联变量来定义并初始化静态数据成员:
// a header file, C++17:
struct Position {
static inline int unit = 0;
// ...
};
编译器保证这个静态变量的定义只有一个,
无论哪个翻译单元包含了类声明. inline变量仍然是静态类变量,
因此它们将在调用main()函数之前初始化.
这个功能让开发只包含头文件的库变得更容易,
因为不需要为静态变量创建CPP文件, 或者使用一些技巧来保持它们在头文件中.
C++20 指定初始化(Designated Initializers)
基本用法
struct Embed {
int id;
int vendor;
};
struct Date {
int year;
int month;
int day;
Embed embed;
static int mode;
};
int main() {
Date a{.year = 2024, .month = 3, .day = 26};
Date c{2024, 3, 26}; // 可读性较差
Date e{
.year = 2024,
.month = 3,
.day = 26,
.embed{
.id = 1,
.vendor = 8086,
},
}; // ok
}
使用规则
使用规则如下:
-
只适用于聚合初始化, 因此只支持聚合类型
-
只能初始化非静态数据成员
-
初始化顺序必须与类声明中的顺序相同
-
并非所有数据成员都必须在表达式中指定
-
不能混合常规初始化和指定初始化
-
每个数据成员只能指定初始化一次
-
不能嵌套.
一些错误示例:
struct Embed {
int id;
int vendor;
};
struct Date {
int year;
int month;
int day;
Embed embed;
static int mode;
};
Date a{.mode = 10}; // error, mode is static!
Date b{.day = 1, .year = 2010}; // error, order not same!
Date c{2050, .month = 12}; // error, mix!
Date d{.embed.id = 1}; // error, nested!
使用指定初始化的优点:
-
可读性
- 设计器指向特定的数据成员, 因此在这里不可能出错. 灵活性
- 你可以跳过一些数据成员, 并依赖其他数据成员的默认值. 与C兼容
-
在C99中, 使用类似的初始化形式很流行(尽管更加放松). 有了C++20的功能,
可以有非常相似的代码并共享它.
标准化
- 一些编译器, 如GCC或Clang, 已经对此功能有一些扩展, 因此将其在所有编译器中启用是一个自然的步骤.
可以使用 auto
推导的情况
对静态成员可以使用auto
:
class Position {
static inline auto answer = 42; // 推导出int
};
但是不能用于非静态变量:
class Position {
auto x { 0 }; // error
auto y { 10.5f }; // error
auto z = int { 10 }; // error
};
类模板参数推到CTAD(class template argument deduction)
自C++17起, 可用CTAD定义一个类模板对象, 而不指定模板参数. 例如:
#include <string>
#include <utility>
#include <vector>
std::pair p2(10.5, 42);
std::pair<double, int> p1(10.5, 42);
std::vector v1{1.1f, 2.2f, 3.3f};
std::vector<float> v2{1.1f, 2.2f, 3.3f};
using namespace std::string_literals;
std::vector s1{"hello"s, "world"s};
std::vector<std::string> s2{"hello", "world"};
这个新功能对于静态数据成员初始化是有效的,
但是对于非静态数据成员初始化是无效的.
class Type {
static inline std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // deduced vector<int>
};
class Type {
std::vector ints { 1, 2, 3, 4, 5, 6, 7}; // error!
};
完整示例
#include <iostream>
#include <string>
struct Flags {
unsigned int mode : 4 {0};
unsigned int visible : 2 {1};
unsigned int ext : 2 {0};
};
struct Position {
inline static unsigned default_width = 100;
inline static unsigned default_height = 100;
unsigned width_{default_width};
unsigned height_{default_height};
Flags flags_{.mode = 2};
std::string title_{"Default Position"};
Position() = default;
explicit Position(std::string title) : title_(std::move(title)) {}
friend std::ostream& operator<<(std::ostream& os, const Position& w) {
os << w.title_ << ": " << w.width_ << "x" << w.height_;
return os;
}
};
int main() {
Position w("Super Position");
std::cout << w << '\n';
Position::default_width = 1920;
Position w2("Position");
std::cout << w2 << '\n';
}