C++ 类数据成员初始化: 从C++11到C++20

介绍

在现代 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';
}
  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值