avatar

目录
297 - 构造函数初始化列表

构造函数初始化列表

🧑‍💻 User

In this lecture, we’re going to learn about initializer lists with constructors.We already know that we can set up a class like Cylinder and use a constructor like this.And when we set up this constructor, what we have been using is member wise copy initialization.And what do I mean by that?Well, we have been implementing our constructor like this.We pass the parameters in here and inside the body of the constructor we do member wise assignment.For example, we take the radius parameter and assign that to our member variable for best radius andwe take height param and we assign this to our height member variable.This is what we have been doing and it works just fine.But in this lecture we’re going to see that we have another syntax which might be better to do the samething, and it is using initializer lists like we see here.The syntax is you’re going to specify your parameter list like you see here.This is the same thing we have been using before.But what you do after your parameter list, you put a colon like you see here and then you say whichone of your member variable you want to initialize with this parameters.So we want to initialize base radius with radius param.We specify that with this parentheses here we put a comma and we say that we want to initialize ourheight member variable with height param.And when we do this we don’t need to do any member wise assignment in the body of the constructor.The object is going to be initialized when it is being created and the next lecture we’re going to reallysee the difference between these two ways of constructing your objects member wise, copy initializationand initializer list initialization.But in this lecture I just want you to be familiar with the syntax for initializer lists, initializationwith your constructors, and this is the way to do this.Again, we have our parameter list, we put a colon and then we specify our initializations.With the Syntaxes.Here we specify that we want the base radius to be initialized with radius param.We specify that we want height to be initialized with height param using this syntax here.And for the last one you don’t put anything, you just put the body of your constructor and the compileris going to take this and it is going to use this to initialize your member variables.Okay.Initializer lists have a few benefits.One of the benefits is that they avoid unnecessary copies.You have seen that if we do member wise copy initialization, we are using the assignment operator andif for example, you have member variables that are objects that’s going to cause copies.And suppose you have a member variable, which is a string and you are copying that many strings.This is really bad and in many cases it’s going to lead to memory waste because you are copying a lotof things and you’re going to waste a lot of CPU cycles with initializer lists.Your objects are initialized when they are created and you don’t really have to do any copy after theobject is created.But this is really not the topic of this lecture.We’re going to see more about this later.For now, I just want you to know that initializer lists have a few benefits, but in some cases theyare the only way you have to initialize an object.And we will have a chance to see this later in the course when we have more tools to really set thisup.For now, we’re going to head over to Visual Studio Code and see how we can play with initializer lists.Okay, here we are in our working folder.The current project is initializer lists for constructors.We’re going to grab our template files, we’re going to put them in place and we are going to open thisin Visual Studio code.But before we do, I think it is a good idea to grab our files and use them for the cylinder class.We’re going to grab them from the last lecture.We can do that.If you don’t have them.Please check the resources section of this lecture.You’re going to find the source code attached and then you can grab the cylinder files and use themin your project.I am just going to put them in here because I don’t want to type this.It’s going to waste time.So let’s open this in Visual Studio code.By dragging and dropping here, this is going to open our project.We’re going to have the main file and we’re going to have our cylinder files, we’re going to have theheader file and the CPP file, and we can close the left pane here and do whatever it is we want todo here.What we really want to see is how to use initializer lists and initialize our member variables in ourconstructors.For example, if we look at the second constructor.Here and at the implementation we have in the CPP file, we’re going to see that we are doing memberwise copy initialization.And what we mean by that is that we are taking the parameters and copying them manually into our membervariables.So this is what we have been doing all along.It is working, but now we want to see how we can use initializer lists to initialize our member variablesbecause initializer lists have a few benefits.We are going to comment out what we have so far and we’re going to see how we can use initializer list.The syntax is going to be almost the same.We’re going to say the name of the class.We’re going to say which constructor we want to implement here.We’re going to say double read param the same thing we had in the other constructor and we’re goingto say double height param.But the similarities end here after this point where we have our parameters and we’re going to go downand specify a column, you don’t really have to go down.You can do this on one line, but splitting this in different lines is better.Let’s do this on the same line just to show you that it is possible.So we’re going to say, let’s say double param here.So we’re going to say that we want to initialize our base radius with our read param and we’re goingto put a comma and we’re going to say that we want to initialize our height with height param.I think we can say that.And you see that this is good syntax, it is accepted by the compiler.Now that we have this and we can actually go to the next line and put in our body and the compiler isgoing to take this.And with this in place we can go in the main function, include our class, we’re going to include cylinderand we’re going to take out what we don’t need here.We’re going to go in the main function, create our cylinder object, we’re going to call this C one,and we’re going to only specify one parameter.I think we can do that because our constructor has a default parameter for the second parameter here.So we can do this.And if we print the volume or we can even print the member variables, let’s say base red and we’regoing to print the base radius down, I’m going to say get base radius and we’re going to call a functionhere.We’re going to put in our parentheses, we’re going to put out the height and we’re going to put outthe volume.If we do this, we’re going to see our information printed out on the console.We can bring up a terminal because we want to run this program and then we’re going to build with GCClike we have been doing all along.So you see that the world is actually good and we are using our implementation with initializer lists.Here again, the syntax is to say a colon after your parameter list.Then you say which one you want to initialize with what.For example, the base radius here is going to be initialized with red param and the height is goingto be initialized with height param and we specify our initializers in parentheses like this.This is the syntax and it is going to work.So this is really long.Some people like to split this into different lines and put our initializers on a second line.Even if you want.If you have many member variables here, you can split this again on different lines and this is goingto work and it is going to be much more readable.Again, if we build this, this is going to build just fine.We’re going to run the test to build with GCC, you’re going to see that it is going to build fine andwe’re going to have our information here.So let’s clear and run Rooster.We’re going to see that the base radius is five.It is what we specified here.If we go in our declaration for the constructor, we’re going to see that the default parameter is five.And we can prove this because the height is five here and we have our volume.If you plug this in, you’re going to see that you get this volume here and it is using the formulawe have in our volume function implementation here.This is really all I wanted you to see in this lecture.The syntax you can use to use initializer lists and it is really cool.The body is going to be empty because by the time your object is created, it’s already going to beinitialized.And this is the syntax I usually recommend to do your constructors if you can, because it has a fewbenefits as we’re going to see in the next few lectures.We are going to stop here in this lecture and the next one we’re going to try and compare initializerlists, initialization with member wise, copy initialization.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,C++新同学!很高兴能和你一起探索C++的奇妙世界。我是你的C++老师,我们将一起学习最地道的C++知识。


C++构造函数中的初始化列表 (Initializer Lists) 深度解析

简介: 本次课程将深入探讨C++构造函数中一种高效、推荐的成员变量初始化方式——初始化列表。我们将学习它的语法、与传统赋值方式的区别,以及使用它带来的诸多好处,并通过实际代码示例加深理解。


1. 详细讲解: 构造函数与初始化列表

我们都知道,当定义一个类(class)时,通常会为它提供一个或多个构造函数 (constructor) 来初始化类的成员变量 (member variable)。过去,我们可能习惯于在构造函数的函数体 (function body) 内部,使用赋值运算符 (assignment operator) 来进行成员变量的初始化。这种方式被称为“逐成员赋值 (member-wise assignment)”。

让我们以一个 Cylinder (圆柱体) 类为例来回顾一下:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
class Cylinder {
public:
double base_radius; // 基底半径
double height; // 高度

// 传统构造函数实现方式 (逐成员赋值)
Cylinder(double radius_param, double height_param) {
base_radius = radius_param; // 赋值
height = height_param; // 赋值
}
// ... 其他方法
};

这种方法虽然可行,但在某些情况下效率不高,并且有其局限性。C++为我们提供了另一种更优、更高效的成员变量初始化机制:初始化列表 (initializer lists)。

1.1 什么是初始化列表?

初始化列表是C++构造函数特有的一种语法,它允许你在构造函数的参数列表 (parameter list) 之后,冒号 (:) 之前,直接指定成员变量的初始值。

来看一下使用初始化列表的 Cylinder 构造函数:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cylinder {
public:
double base_radius; // 基底半径
double height; // 高度

// 使用初始化列表的构造函数
Cylinder(double radius_param, double height_param)
: base_radius(radius_param), // 初始化列表:将radius_param赋值给base_radius
height(height_param) // 初始化列表:将height_param赋值给height
{
// 构造函数体可以为空,因为成员变量已经初始化
}
// ... 其他方法
};

语法解析:

  1. 在构造函数的参数列表(例如 (double radius_param, double height_param))的右括号 ) 之后,紧接着添加一个冒号 (: )。
  2. 冒号后面跟着一个或多个初始化表达式,每个表达式用逗号 (,) 分隔。
  3. 每个初始化表达式的格式是:成员变量名(参数名 或 初始值)。例如,base_radius(radius_param) 表示用 radius_param 来初始化 base_radius
  4. 最后一个初始化表达式后不需要逗号,直接跟着构造函数的函数体 {}
  5. 为了代码的可读性,当成员变量较多时,通常会将初始化列表中的每个成员放在新的一行。

1.2 为什么推荐使用初始化列表?

使用初始化列表有以下几个显著的优点:

  1. 避免不必要的拷贝 (avoid unnecessary copies):
    当我们在构造函数体内使用赋值的方式初始化成员变量时,实际上是发生了两次操作:

    • 首先,成员变量会先进行一次默认构造 (default construction)。
    • 然后,再通过赋值操作 (assignment operation) 将参数值赋给成员变量。
      特别是当成员变量是自定义类对象(如 std::string)时,默认构造可能创建一个空对象,然后赋值操作又会销毁这个空对象并重新创建一个新的对象。这会导致不必要的临时对象创建和销毁,浪费CPU周期和内存。
      而使用初始化列表时,成员变量在对象创建时 (object creation) 就直接通过提供的参数进行构造 (construction) 和初始化,避免了先默认构造再赋值的二次操作。这就像是你建造一栋房子时,直接把家具放进去,而不是先建好空房子再搬家具。
  2. 效率更高: 由于避免了额外的拷贝和赋值操作,初始化列表通常比在构造函数体内赋值的方式更高效。

  3. 某些成员变量必须通过初始化列表初始化:
    这是初始化列表最重要的一个原因,甚至可以说在某些情况下是“唯一”的初始化方式。

    • 常量成员变量 (const member variables): const 成员变量在定义后就不能被修改,因此它们必须在对象构造时就被赋予一个初始值。构造函数体内的赋值操作属于修改,而不是初始化。
    • 引用成员变量 (reference member variables): 引用在定义时就必须被初始化,并且一旦绑定就不能更改绑定的对象。
    • 没有默认构造函数的类类型成员变量: 如果一个类类型的成员变量没有提供默认构造函数 (default constructor) (即没有无参数的构造函数),那么它就无法在构造函数体内被“默认构造”,因此你必须通过初始化列表显式地调用其带参数的构造函数来初始化它。
    • 基类构造函数调用: 当派生类构造时,它需要负责调用其基类的构造函数来初始化基类部分。基类的构造函数也必须通过初始化列表来调用。

    这些情况下,如果你不使用初始化列表,编译器会报错。

  4. 强制初始化所有成员: 有些人认为,使用初始化列表可以清晰地表明所有成员变量都已在构造时被考虑和初始化,提高了代码的清晰度。

因此,强烈建议你在编写C++构造函数时,尽可能地使用初始化列表来初始化你的成员变量。


2. 代码示例

下面是一个完整的 Cylinder 类示例,展示了如何使用初始化列表。

cylinder.h 文件:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#ifndef CYLINDER_H
#define CYLINDER_H

#include <iostream>
#include <cmath> // For M_PI or pi constant if needed

// 定义一个常量PI,也可以使用M_PI(需要定义_USE_MATH_DEFINES)
const double PI = 3.14159265358979323846;

class Cylinder {
private:
double base_radius; // 圆柱体基底半径
double height; // 圆柱体高度

public:
// 构造函数1: 默认构造函数,使用初始化列表给默认值
Cylinder();

// 构造函数2: 带参数的构造函数,演示初始化列表
// 为height_param设置了默认参数,这样可以用一个参数或两个参数构造
Cylinder(double radius_param, double height_param = 5.0);

// 拷贝构造函数 (如果自定义了,也常使用初始化列表)
// Cylinder(const Cylinder& source); // 暂时不深入,但要知道也是用初始化列表

// 获取基底半径
double get_base_radius() const;

// 获取高度
double get_height() const;

// 计算圆柱体体积
double volume() const;

// 设置基底半径
void set_base_radius(double radius_param);

// 设置高度
void set_height(double height_param);

// 析构函数 (这里很简单,但对于资源管理很重要)
~Cylinder();
};

#endif // CYLINDER_H

cylinder.cpp 文件:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include "cylinder.h"

// 构造函数1: 默认构造函数实现,使用初始化列表
Cylinder::Cylinder()
: base_radius(2.0), // 默认半径2.0
height(3.0) // 默认高度3.0
{
std::cout << "默认构造函数被调用,通过初始化列表初始化。" << std::endl;
}

// 构造函数2: 带参数的构造函数实现,使用初始化列表
Cylinder::Cylinder(double radius_param, double height_param)
: base_radius(radius_param), // 使用参数初始化base_radius
height(height_param) // 使用参数初始化height
{
// 构造函数体可以为空,因为成员变量已经通过初始化列表完全初始化
std::cout << "带参数构造函数被调用,通过初始化列表初始化。" << std::endl;
}

// 获取基底半径
double Cylinder::get_base_radius() const {
return base_radius;
}

// 获取高度
double Cylinder::get_height() const {
return height;
}

// 计算圆柱体体积
double Cylinder::volume() const {
return PI * base_radius * base_radius * height;
}

// 设置基底半径
void Cylinder::set_base_radius(double radius_param) {
base_radius = radius_param;
}

// 设置高度
void Cylinder::set_height(double height_param) {
height = height_param;
}

// 析构函数
Cylinder::~Cylinder() {
std::cout << "Cylinder 对象被销毁。" << std::endl;
}

main.cpp 文件:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "cylinder.h" // 包含Cylinder类的头文件

int main() {
// 1. 使用默认构造函数创建对象
std::cout << "--- 创建 C1 (默认构造函数) ---" << std::endl;
Cylinder c1; // 调用 Cylinder() 默认构造函数
std::cout << "C1 - 半径: " << c1.get_base_radius()
<< ", 高度: " << c1.get_height()
<< ", 体积: " << c1.volume() << std::endl;
std::cout << std::endl;

// 2. 使用带一个参数的构造函数创建对象 (因为height_param有默认值)
std::cout << "--- 创建 C2 (带一个参数的构造函数) ---" << std::endl;
Cylinder c2(10.0); // 调用 Cylinder(10.0, 5.0) 构造函数
std::cout << "C2 - 半径: " << c2.get_base_radius()
<< ", 高度: " << c2.get_height()
<< ", 体积: " << c2.volume() << std::endl;
std::cout << std::endl;

// 3. 使用带两个参数的构造函数创建对象
std::cout << "--- 创建 C3 (带两个参数的构造函数) ---" << std::endl;
Cylinder c3(7.0, 15.0); // 调用 Cylinder(7.0, 15.0) 构造函数
std::cout << "C3 - 半径: " << c3.get_base_radius()
<< ", 高度: " << c3.get_height()
<< ", 体积: " << c3.volume() << std::endl;
std::cout << std::endl;

// 程序结束时,c1, c2, c3 对象会被销毁,析构函数会被调用
return 0;
}

如何编译和运行 (在终端):

bash
1
2
g++ main.cpp cylinder.cpp -o cylinder_app -std=c++11 # 或者更高的标准如-std=c++17
./cylinder_app

运行结果会显示每个对象创建时构造函数被调用的消息,以及它们的成员值和计算出的体积。


3. QA 闪卡

  1. Q: 什么是构造函数中的初始化列表?
    A: 初始化列表是C++构造函数特有的语法,用于在构造函数体执行之前,直接初始化类的成员变量。

  2. Q: 初始化列表的语法是怎样的?
    A: 在构造函数的参数列表后,紧跟一个冒号 :,然后列出要初始化的成员变量及其对应的初始值,用逗号 , 分隔。例如:MyClass(int x) : member_var(x) {}

  3. Q: 相较于在构造函数体内赋值,使用初始化列表有哪些主要优势?
    A: 主要优势包括:避免不必要的拷贝(尤其是对于类类型成员变量),更高效的初始化过程,以及在某些情况下是初始化 const 成员、引用成员或没有默认构造函数的类类型成员的唯一方式。

  4. Q: 什么时候必须使用初始化列表?
    A: 必须使用初始化列表的情况包括:初始化 const 成员变量、初始化引用成员变量、初始化没有默认构造函数的类类型成员变量,以及调用基类的构造函数。


4. 常见误解或错误

  1. 忘记冒号或语法错误: 初学者常会忘记在参数列表后添加冒号,或者将初始化列表的格式搞错,例如使用等号 = 而不是括号 ()

    • 错误示例: MyClass(int x) { member_var = x; } (这是赋值,不是初始化列表)
    • 错误示例: MyClass(int x) : member_var = x {} (初始化列表不能用等号,除非是C++11的统一初始化语法,但通常用括号表示)
    • 正确示例: MyClass(int x) : member_var(x) {}
  2. 混淆初始化和赋值: 认为在构造函数体内赋值和使用初始化列表是一回事。实际上,初始化是在对象创建时进行的,而赋值是在对象已经存在后进行的。对于基本数据类型(如 int, double),区别可能不那么明显,但对于复杂对象,区别巨大。

  3. 在初始化列表中使用未声明的变量: 确保在初始化列表中使用的变量名是构造函数的参数名或类中定义的成员变量名。

    • 错误示例: Cylinder(double r) : base_radius(radius) {} (如果 radius 未声明为参数或全局变量)
  4. 初始化顺序的误解: 初始化列表中的成员变量初始化顺序取决于它们在初始化列表中出现的顺序,而是取决于它们在类中声明的顺序。这是一个非常常见的C++陷阱。如果初始化列表中的某个成员变量依赖于另一个成员变量的值,而后者在类中声明的位置靠后,可能会导致未定义的行为。

    • 示例:
      cpp
      1
      2
      3
      4
      5
      6
      7
      8
      class MyClass {
      int b;
      int a; // a 在 b 之后声明

      public:
      // 尽管这里先初始化a,再初始化b,但实际的初始化顺序是 b (先声明), 然后 a (后声明)
      MyClass() : a(b + 1), b(10) {} // 错误!b 尚未初始化时就被a使用
      };
      正确的做法是确保依赖项先被声明:
      cpp
      1
      2
      3
      4
      5
      6
      7
      8
      class MyClass {
      int b;
      int a; // a 在 b 之后声明

      public:
      // 尽管这里先初始化a,再初始化b,但实际的初始化顺序是 b (先声明), 然后 a (后声明)
      MyClass() : b(10), a(b + 1) {} // b先初始化为10,然后a可以安全地使用b+1
      };
      所以,始终按照成员变量在类中声明的顺序来排列初始化列表,以避免这种潜在的问题,并提高代码的可读性。

5. 编程练习

现在,轮到你来动手了!

请修改下面的 Book 类,将其构造函数从传统的逐成员赋值方式改为使用初始化列表的方式来初始化成员变量。

book.h 文件:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef BOOK_H
#define BOOK_H

#include <string>
#include <iostream>

class Book {
private:
std::string title;
std::string author;
int publication_year;
double price;

public:
// 构造函数声明 (你需要在这里修改,并修改.cpp文件中的实现)
Book(const std::string& title_param, const std::string& author_param, int year_param, double price_param);

// 获取书名
std::string get_title() const;

// 获取作者
std::string get_author() const;

// 获取出版年份
int get_publication_year() const;

// 获取价格
double get_price() const;

// 打印书籍信息
void print_info() const;
};

#endif // BOOK_H

book.cpp 文件 (你需要填充 Book 构造函数的实现):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "book.h"

// 请在这里修改 Book 构造函数的实现,使用初始化列表来初始化成员变量
Book::Book(const std::string& title_param, const std::string& author_param, int year_param, double price_param)
{
// 旧的实现方式 (你需要替换掉这些行,使用初始化列表)
// this->title = title_param;
// this->author = author_param;
// this->publication_year = year_param;
// this->price = price_param;

std::cout << "Book 构造函数被调用!" << std::endl;
}

std::string Book::get_title() const {
return title;
}

std::string Book::get_author() const {
return author;
}

int Book::get_publication_year() const {
return publication_year;
}

double Book::get_price() const {
return price;
}

void Book::print_info() const {
std::cout << "--------------------" << std::endl;
std::cout << "书名: " << title << std::endl;
std::cout << "作者: " << author << std::endl;
std::cout << "出版年份: " << publication_year << std::endl;
std::cout << "价格: " << price << std::endl;
std::cout << "--------------------" << std::endl;
}

main.cpp 文件 (无需修改,用于测试):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "book.h"
#include <vector>

int main() {
// 创建一个Book对象
Book my_book("C++ Primer", "Stanley B. Lippman", 2012, 59.99);
my_book.print_info();

std::cout << std::endl;

// 创建另一个Book对象
Book another_book("The Lord of the Rings", "J.R.R. Tolkien", 1954, 25.00);
another_book.print_info();

// 尝试创建多个Book对象到vector中
std::vector<Book> books;
books.push_back(Book("Effective C++", "Scott Meyers", 1991, 35.50));
books.push_back(Book("Clean Code", "Robert C. Martin", 2008, 45.00));

std::cout << "\n--- Vector 中的书籍信息 ---" << std::endl;
for (const auto& book : books) {
book.print_info();
}


return 0;
}

请你提交 book.cppBook 构造函数的修改方案。



🧑‍💻 User

回答之前我问个问题, 那个初始化列表的括号里, 是否可以是表达式?


🤖 Assistant

这是一个非常棒的问题!提得非常好!

是的,初始化列表的括号 ()可以是任意有效的C++表达式

这意味着,你不仅可以使用传递给构造函数的参数来初始化成员变量,还可以使用:

  • 字面量 (literals) (例如 53.14"hello")
  • 其他成员变量 (但要注意它们的声明顺序,因为初始化顺序是按照成员变量在类中的声明顺序,而不是初始化列表中的顺序)
  • 函数调用 (function calls)
  • 算术表达式 (arithmetic expressions)
  • 等等… 任何能产生一个值的表达式都可以。

例子:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <string>
#include <iostream>

class Product {
private:
std::string name;
double unit_price;
int quantity;
double total_cost; // 总价,可以通过其他成员计算

public:
// 构造函数,使用初始化列表中的表达式
Product(const std::string& n, double price, int qty)
: name(n), // 使用参数初始化
unit_price(price), // 使用参数初始化
quantity(qty), // 使用参数初始化
total_cost(unit_price * quantity) // 在这里使用表达式,计算总价
{
std::cout << "Product '" << name << "' created." << std::endl;
}

void print_info() const {
std::cout << "Name: " << name
<< ", Price: " << unit_price
<< ", Quantity: " << quantity
<< ", Total Cost: " << total_cost << std::endl;
}
};

int main() {
Product item1("Laptop", 1200.50, 2);
item1.print_info(); // Output: Total Cost: 2401

Product item2("Mouse", 25.00, 5);
item2.print_info(); // Output: Total Cost: 125

return 0;
}

在上面的例子中,total_cost(unit_price * quantity) 就是一个非常典型的在初始化列表内使用表达式的例子。unit_price * quantity 是一个算术表达式,它的计算结果会被用来初始化 total_cost

重要提示: 就像我之前强调的,尽管 total_cost 的初始化依赖于 unit_pricequantity,但如果 unit_pricequantity 在类成员变量声明的顺序中位于 total_cost 之前,那么这个表达式是安全的。C++会保证成员变量按照它们在类定义中的声明顺序进行初始化。

理解这一点非常关键,因为它大大增加了初始化列表的灵活性和实用性!

现在请你提交之前留下的 Book 类的编程练习解决方案吧!


🧑‍💻 User

include “book.h”

// 请在这里修改 Book 构造函数的实现,使用初始化列表来初始化成员变量
Book::Book(const std::string& title_param, const std::string& author_param, int year_param, double price_param)
: title(title_param),
author(author_param),
publication_year(year_param),
price(price_param)
{
// 旧的实现方式 (你需要替换掉这些行,使用初始化列表)
// this->title = title_param;
// this->author = author_param;
// this->publication_year = year_param;
// this->price = price_param;

std::cout << "Book 构造函数被调用!" << std::endl;

}

std::string Book::get_title() const {
return title;
}

std::string Book::get_author() const {
return author;
}

int Book::get_publication_year() const {
return publication_year;
}

double Book::get_price() const {
return price;
}

void Book::print_info() const {
std::cout << “——————————“ << std::endl;
std::cout << “书名: “ << title << std::endl;
std::cout << “作者: “ << author << std::endl;
std::cout << “出版年份: “ << publication_year << std::endl;
std::cout << “价格: “ << price << std::endl;
std::cout << “——————————“ << std::endl;
}


🤖 Assistant

太棒了!你的解决方案完全正确!👏

7. 评分与答案解析

7.1 解决方案评分

得分:5/5 (完美)

你的代码清晰、准确地展示了如何使用初始化列表来初始化 Book 类的所有成员变量。你完全理解了初始化列表的语法和目的,并将其应用于实际场景。非常棒!

7.2 实际答案与解释

你的代码就是正确答案:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "book.h"

// 请在这里修改 Book 构造函数的实现,使用初始化列表来初始化成员变量
Book::Book(const std::string& title_param, const std::string& author_param, int year_param, double price_param)
: title(title_param), // 使用title_param初始化title
author(author_param), // 使用author_param初始化author
publication_year(year_param), // 使用year_param初始化publication_year
price(price_param) // 使用price_param初始化price
{
// 构造函数体可以为空,因为所有成员都已在初始化列表阶段完成初始化
std::cout << "Book 构造函数被调用!" << std::endl;
}

// ... (其他方法保持不变)

解释:

  • 你正确地在构造函数的参数列表 ) 之后使用了冒号 :
  • 你为每个成员变量(title, author, publication_year, price)都提供了初始化表达式,并用逗号 , 将它们分隔开。
  • 你使用了构造函数传入的参数 (title_param, author_param, year_param, price_param) 来初始化对应的成员变量。
  • 这个构造函数的函数体现在是空的(除了那行输出语句),这正是使用初始化列表的典型表现:所有初始化工作都在进入函数体之前完成。

这样做的好处:

  1. 对于 std::string 这样的类类型成员(titleauthor),使用初始化列表可以避免一次默认构造和一次赋值操作。它直接调用 std::string 的带参数构造函数来初始化这些成员,效率更高。
  2. 对于 intdouble 这样的内置类型成员,虽然效率提升不那么明显,但语法保持了一致性,并且仍然是推荐的做法。
  3. 如果未来 Book 类中加入了 const 成员或引用成员,或者一个没有默认构造函数的自定义类对象成员,你现在已经掌握的这种初始化方式将是唯一的选择。

你已经很好地掌握了构造函数初始化列表的基本用法和重要性!

8. 进一步探索与思考

你已经对初始化列表有了很好的理解。为了让你成为更专业的C++开发者,这里有一些深入和扩展的问题,你可以思考和研究:

  1. 成员变量初始化顺序的真相:
    我们课堂上提到了一个常见误解:“初始化列表中的成员变量初始化顺序不取决于它们在初始化列表中出现的顺序,而是取决于它们在类中声明的顺序。”

    • 请你尝试编写一个示例类,包含两个 int 成员,让它们在类中的声明顺序和在初始化列表中的顺序不一致,并且让其中一个成员的初始化依赖于另一个。观察编译器是否会给出警告或错误,并分析运行结果。这能帮你更深刻地理解这个“陷阱”。
  2. 默认成员初始化器 (Default Member Initializers) (C++11及更高版本):
    在C++11标准之后,你可以在类的定义中直接为非静态成员变量提供默认值。

    cpp
    1
    2
    3
    4
    5
    6
    7
    class MyClass {
    int value = 10; // 默认成员初始化器
    std::string name{"Default"}; // 也可以使用花括号初始化
    public:
    MyClass() {} // 如果不提供初始化列表,这些默认值将生效
    MyClass(int v) : value(v) {} // 如果在初始化列表提供了,则会覆盖默认值
    };
    • 了解这种方式的语法和用法。它与初始化列表有何关系?它们的使用优先级是怎样的?什么时候使用默认成员初始化器,什么时候使用初始化列表?
  3. 委托构造函数 (Delegating Constructors) (C++11及更高版本):
    C++11允许一个构造函数调用(或“委托”)同一个类的另一个构造函数来完成初始化工作。

    cpp
    1
    2
    3
    4
    5
    6
    7
    class MyClass {
    int a;
    double b;
    public:
    MyClass(int val) : a(val), b(0.0) {} // 基础构造函数
    MyClass() : MyClass(0) {} // 委托构造函数,调用上面的构造函数
    };
    • 了解委托构造函数的语法和使用场景。它如何帮助你避免代码重复?
  4. 统一初始化 (Uniform Initialization) / 大括号初始化 (Brace Initialization) (C++11及更高版本):
    C++11引入了使用花括号 {} 进行初始化的方式,这被称为统一初始化,旨在提供一种统一的初始化语法。

    • Type var{value};
    • Type var = {value};
    • new Type{value};
    • 甚至在初始化列表中:member_var{param}
    • 研究这种初始化方式。它与圆括号 () 的初始化方式有何不同?特别是要了解它与 std::initializer_list 类型 (std::initializer_list 是一个特殊的类模板,与构造函数初始化列表中的花括号初始化有些关联,但概念上不同,但它们都使用花括号) 的关联以及它的“非收窄转换” (non-narrowing conversions) 特性。
  5. 拷贝构造函数 (Copy Constructor) 和移动构造函数 (Move Constructor) 中的初始化列表:
    我们这次主要讲了普通构造函数。在拷贝构造函数和C++11引入的移动构造函数中,初始化列表同样至关重要。

    • 尝试为 CylinderBook 类实现一个拷贝构造函数Book(const Book& other);。你会发现初始化列表在其中扮演了关键角色。
    • 如果你想进一步挑战,可以尝试了解移动构造函数Book(Book&& other);。它如何通过初始化列表实现资源的“移动”而不是“拷贝”,从而提高效率?

这些都是C++中关于构造和初始化非常重要的概念,掌握它们将大大提升你的C++编程能力和代码质量。加油!


评论