avatar

目录
319 - Class Const

Class Const

🧑‍💻 User

In this lecture, we’re going to learn about static constants.And this is going to be a byproduct of what we have learned about in the last few lectures.Remember that we have designed a cylinder class in the last chapter, I think, and the way we did things,we were maintaining a variable, a constant variable for modeling the Pi variable that we use to computethe volume of a cylinder.Now we have tools to make this design a little better because remember, the Pi variable isn’t reallygoing to be used in many other locations.It should belong to the cylinder class.We can’t really make it a regular member variable because each object then is going to have its owncopy of Pi and we will be basically wasting memory.But now that we know about static variables, we can bind the Pi variable to the class itself by markingit as static.That’s what we do here.But we can also make it inline and allow for people to initialize this inside the class itself and avoidhaving to do this in the CPP file like we have seen in the previous lecture.So this is how static inline constants can really help you do a better design.You don’t have to maintain constants on the outside.They can be maintained inside the class itself and each class object is going to have access to thisvariable if it needs it.Now that you know this, we’re going to head over to Visual Studio code and play with this a littlemore.Okay.Here we are in our working folder.The current project is Static constants.We’re going to grab our template files like we always do, and we’re going to put them in place andwe are going to open this in Visual Studio code by dragging and dropping here, this is going to openour project or folder, I should say, and we’re going to clean up the main CPP file.Okay.We need the cylinder class to play with, so we’re going to create files for that.We’re going to create a header file and a dot CPP file.We’re going to hit the new file button here and we’re going to create a cylinder dot h file and we’regoing to do that again and create a cylinder dot cpp file.I am going to put in the code I have prepared before because I don’t want you to see me typing all this.This is nothing you haven’t seen before.We have our include guard here.We have the class declaration inside, we have a public section and a private section.The private section is going to model the base radius and the height.And in the public section, we’re going to have a bunch of functions that are going to make this classwork.We have a few constructors, we have the gators, we have the setters, and we have a method to computethe volume of our cylinder here.The volume function is really not going to modify the class here or the member variables of the class,so I am going to mark it as const.I am also going to mark our Gators as costs.Let’s do that because they are not modifying our thing.And now that we have this, we can go in our CPP file and put in our implementation.We are just going to include the header file and we’re going to be modifying the signatures of our gatorsbecause we have just marked them as const.We should be consistent for that here.And the volume function or member function is not going to modify the member variables.So it’s also going to be const.Other than this, I think we really are done here.So let’s go back to the header and focus on our constants.The constant is marked as static because we want it to be attached to the class itself.It’s not going to be attached to any object and it is in line because we want this facility to initializethat in the class declaration.We don’t want to go in any CPP file and do the initialization there because that’s just going to makemaintaining this member variable a little harder.We will have to go to different places to really maintain it and that’s by design.So we are marking that as inline.Note that this syntax is only going to work if you are using C plus plus 17 or a newer version of Cplus plus.So we are using C plus plus 20 in this course.So this is going to work without a problem.And another thing I want you to notice is that we have another static variable on top here.It’s going to be modeling the default color for our cylinder here.So this is something you can do.And I am doing this just to show you that you can declare all kinds of crazy constants in your classif you happen to need that.Okay.Now that we have this in place, we can head over to.Our main file and include our class.Let’s do that.We’re going to include the cylinder dot h, and we’re going to go down and create a cylinder objectand do things with it.For example, we can print the volume of a cylinder and we can print the default color using the syntaxhere.This is going to work because default color is living in the public scope of our class if it’s not public.The syntax here is not going to work as we have seen before.But if it’s not public, you can go through a member function to get it and print that out if you want.Down here you see that we can even use an object and print the default color.The syntax here is also going to work without a problem.We can build this program.Let’s use GCC to do that and we’re going to bring up a terminal to be able to play with us.We can clear and run Rooster and we’re going to see the volume printed out and we’re going to see thecolor for our cylinder printed out here.This is really all I had to share in this lecture, showing you that you can model constants in yourclass.Remember before we had to maintain the Pi variable in a separate file?It was on the outside and that was really bad design because all we wanted was to use that to computethe volume of our cylinder.So in a way, the Pi variable here is tied to the cylinder class and now we have a design that reallyconforms to that because this variable is attached to the class and it’s not attached to any objectas we have here in the main function with cylinder one, it is attached to the class as a blueprintas a whole.It’s not attached to any object here and this is really cool.It is good design.Okay, this is working fine and we are happy.But what do you do if you don’t have access to C plus plus 17 or a newer version?If that’s the case, you’re going to resort to declaring your variable in the class definition likethis.And then you’re going to have to initialize the variable in a CPP file somewhere.And we’re going to see how you can do that in the next lecture.So go ahead and finish up here and meet me there.


🤖 Assistant

好的,没问题!作为您C++学习旅程的向导,我将以最清晰、最易懂的方式,带您深入理解C++中的静态常量(static constants)。


C++ 静态常量 (Static Constants) 深度解析:优化你的类设计

摘要: 本课程将深入探讨C++中如何在类内部定义和使用静态常量,特别是利用C++17引入的 static inline const 特性,来提升代码的设计质量和可维护性。我们将通过一个圆柱体(Cylinder)类的实例来具体演示。


1. 详细讲解:一步步揭秘静态常量

在C++编程中,有效地管理常量是良好设计的重要组成部分。特别是在面向对象编程中,我们经常会遇到一些数据,它们与某个类紧密相关,但又不属于任何一个具体的对象实例。这就是静态常量(static constants)大显身手的地方。

让我们从一个熟悉的例子开始:圆柱体(Cylinder)类。

1.1 问题的引入:PI 常量的困境

在之前的课程中,您可能已经接触过如何计算圆柱体的体积,这通常需要用到圆周率 PI。在C++中,我们有几种方式来定义这个 PI

  1. 作为全局常量(Global Constant)或外部常量:

    cpp
    1
    2
    3
    4
    5
    6
    // cylinder.h (或单独的 constants.h)
    const double PI = 3.14159265358979323846;

    // cylinder.cpp
    #include "cylinder.h" // 包含 PI
    // ... 在 Cylinder 类中使用 PI 计算体积
    • 问题: 这种做法使得 PI 成为一个全局可见的常量,它不属于任何特定的类。这可能导致:
      • 命名冲突(Name Clashes): 如果在大型项目中,其他部分也定义了同名的 PI,就可能发生冲突。
      • 设计不清晰: 从语义上讲,PI 是用来计算圆柱体体积的,它和 Cylinder 类有强关联性,但作为全局常量,这种关联性并没有明确体现出来。这使得代码的意图不那么直观。
  2. 作为普通成员变量(Member Variable):

    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Cylinder {
    private:
    double radius;
    double height;
    // const double PI_VALUE = 3.14159265358979323846; // 这种直接初始化非静态const成员是不允许的
    // 或者通过构造函数初始化
    // Cylinder() : PI_VALUE(3.14159...) {}
    public:
    // ...
    };
    • 问题: 如果将 PI 定义为 Cylinder 类的一个普通成员变量 (member variable),那么每创建一个 Cylinder 对象,这个对象都会拥有自己的一份 PI 副本。这会造成严重的内存浪费(Memory Waste)!因为 PI 是一个数学常数,对所有圆柱体对象来说都是完全相同的。我们只需要一份 PI 就足够了。

1.2 解决方案:静态成员(Static Members)

为了解决上述问题,C++引入了静态变量 (static variables)的概念。

  • 什么是静态成员? 静态成员变量属于类本身,而不是类的任何具体对象实例。这意味着:
    • 所有该类的对象共享同一个静态成员的副本。
    • 静态成员在程序启动时被初始化,并且只有一份存储空间。
  • 如何声明? 在成员变量前面加上 static 关键字。

PI 声明为 static 可以解决内存浪费的问题,因为无论创建多少个 Cylinder 对象,都只会有一个 PI 的副本。

1.3 结合常量:静态常量(Static Constants)

PI 不仅是所有 Cylinder 对象共享的,它还是一个不可改变(Immutable)的常量。因此,我们应该结合 staticconst 关键字,将其声明为 static const

  • 概念: static const 成员变量结合了 staticconst 的特性。它既属于类,又不可修改。
  • 重要性: 适用于那些与类逻辑紧密相关、但又不会随对象状态变化、且不应被修改的常量。

1.4 初始化问题与 C++17 的 inline 关键字

在 C++17 之前,static const 成员变量的初始化方式有些复杂:

  • 对于整型(Integer)或枚举(Enum)类型的 static const 成员: 如果它们是常量表达式(compile-time constants),可以在类内部直接声明并初始化。
    cpp
    1
    2
    3
    4
    class MyClass {
    public:
    static const int MAX_VALUE = 100; // 可以在类内部直接初始化
    };
  • 对于非整型(Non-integer)或复杂类型的 static const 成员(如 doublestd::string): 通常需要在类外部(通常是 .cpp 文件中)进行定义和初始化。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    // cylinder.h
    class Cylinder {
    public:
    static const double PI; // 声明 (Declaration)
    };

    // cylinder.cpp
    const double Cylinder::PI = 3.14159265358979323846; // 定义和初始化 (Definition and Initialization)
    • 弊端: 这种传统方式需要你在头文件 (header file)中声明常量,然后在对应的实现 (implementation)文件(.cpp 文件)中进行定义和初始化。这导致一个常量需要在两个不同的地方进行维护,增加了代码的分散性和维护成本。

为了解决这个问题,C++17 引入了一个非常方便的特性:inline 关键字与 static 成员变量的结合。

  • static inline const 的作用:
    在 C++17 及更高版本中,你可以在类声明(class declaration)内部,使用 static inline const 直接定义和初始化非整型的 static const 成员变量。
    cpp
    1
    2
    3
    4
    5
    // cylinder.h
    class Cylinder {
    public:
    static inline const double PI = 3.14159265358979323846; // 声明和初始化都在头文件内
    };
  • 优势:

    1. 更简洁的设计: 常量的声明和定义都在一个地方,提高了代码的内聚性,使得类和其相关常量的定义更加集中。
    2. 避免 ODR (One Definition Rule) 冲突: inline 关键字在这里的作用与 inline 函数类似,它允许在多个编译单元中出现相同的定义,编译器会确保最终只有一个定义被链接进来。这意味着你可以将带有 static inline const 成员的类完全放在头文件中,而不用担心链接错误。
    3. 头文件友好: 使得类及其相关常量可以完全在头文件中定义,这对于开发仅包含头文件的库(header-only library)非常有用。
  • 注意: 此语法特性仅适用于 C++17 或更高版本。如果您的编译器不支持 C++17,您将不得不使用传统的在 .cpp 文件中初始化 static const 成员的方式。

1.5 另一个静态常量示例:DEFAULT_COLOR

除了像 PI 这样的数值常量,static inline const 还可以用于其他类型。例如,您可能希望为圆柱体设置一个默认颜色。

cpp
1
2
3
4
5
6
class Cylinder {
public:
static inline const double PI = 3.14159265358979323846;
static inline const std::string DEFAULT_COLOR = "Blue"; // 默认颜色常量
// ...
};

这个 DEFAULT_COLOR 也是与 Cylinder 类相关联,对所有 Cylinder 对象都是相同的,且不应被修改,因此非常适合作为 static inline const 成员。

1.6 访问静态成员

访问类的 static 成员有两种主要方式:

  1. 通过类名访问(推荐):
    这是最清晰、最推荐的方式,因为它明确表示该成员属于类,而不是某个特定的对象。
    cpp
    1
    2
    double volume = Cylinder::PI * radius * radius * height;
    std::cout << "Default Color: " << Cylinder::DEFAULT_COLOR << std::endl;
  2. 通过对象访问(可行但不推荐):
    虽然语法上允许通过对象实例来访问 static 成员,但这可能会误导读者,让他们误以为该成员是对象的一部分。
    cpp
    1
    2
    Cylinder myCylinder(5, 10);
    std::cout << "Default Color from object: " << myCylinder.DEFAULT_COLOR << std::endl; // 不推荐

1.7 const 成员函数 (const Correctness)

Cylinder 类的设计中,您还会看到一些成员函数后面带 const 关键字,例如 getVolume()getRadius() 等。

  • 什么是 const 成员函数? 在成员函数签名后面加上 const 关键字,表示这个函数不会修改对象的状态(即不会修改该对象的非 static 成员变量)。
  • 为什么重要?
    • 提高代码健壮性: 编译器会检查 const 成员函数是否真的没有修改对象状态,这有助于避免潜在的错误。
    • 提高可读性: 明确告诉其他开发者这个函数是“只读”的,不会有副作用。
    • 允许对 const 对象调用: 如果你有一个 const Cylinder 对象,你只能调用它的 const 成员函数。如果你的 get 方法不是 const 的,你就无法通过 const 对象来获取其属性。

例如:

cpp
1
2
3
4
5
6
7
8
9
10
11
class Cylinder {
public:
double getVolume() const; // 标记为 const
double getRadius() const; // 标记为 const
// ...
};

// 在 .cpp 文件中实现时,也要保持 const
double Cylinder::getVolume() const {
return PI * radius * radius * height;
}

2. 代码示例

为了帮助您更好地理解,以下是一个完整的 Cylinder 类及其使用 static inline const 成员的示例。

我们将代码分为三个文件:cylinder.h (头文件)、cylinder.cpp (实现文件) 和 main.cpp (主程序文件)。

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
#ifndef CYLINDER_H
#define CYLINDER_H

#include <string> // 引入 std::string
#include <iostream> // 用于输出,虽然这里主要是类声明

class Cylinder {
public:
// 静态内联常量:PI,属于类本身,不可修改,且在头文件中直接定义初始化 (C++17+)
static inline const double PI = 3.14159265358979323846;

// 静态内联常量:DEFAULT_COLOR,属于类本身,不可修改,且在头文件中直接定义初始化 (C++17+)
static inline const std::string DEFAULT_COLOR = "Blue";

private:
double radius;
double height;

public:
// 构造函数 (Constructors)
Cylinder() = default; // 默认构造函数
Cylinder(double radius_param, double height_param);

// Getters (访问器) - 标记为 const,因为它们不修改对象状态
double getRadius() const;
double getHeight() const;

// Setters (修改器)
void setRadius(double radius_param);
void setHeight(double height_param);

// 计算体积的方法 - 标记为 const,因为不修改对象状态
double getVolume() const;
};

#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
#include "cylinder.h" // 包含 Cylinder 类的声明

// 构造函数的实现
Cylinder::Cylinder(double radius_param, double height_param) {
radius = radius_param;
height = height_param;
}

// Getters 的实现 - 注意这里的 const 关键字也要与声明一致
double Cylinder::getRadius() const {
return radius;
}

double Cylinder::getHeight() const {
return height;
}

// Setters 的实现
void Cylinder::setRadius(double radius_param) {
radius = radius_param;
}

void Cylinder::setHeight(double height_param) {
height = height_param;
}

// getVolume 方法的实现 - 注意这里的 const 关键字也要与声明一致
double Cylinder::getVolume() const {
// 内部直接使用静态常量 PI,无需通过对象访问
return PI * radius * radius * height;
}

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
#include <iostream>
#include "cylinder.h" // 包含 Cylinder 类的声明

int main() {
// 创建 Cylinder 对象
Cylinder cylinder1(5.0, 10.0);

// 打印圆柱体体积
std::cout << "Cylinder 1 Volume: " << cylinder1.getVolume() << std::endl;

// 访问并打印静态常量 DEFAULT_COLOR
// 推荐方式:通过类名访问
std::cout << "Default Cylinder Color (via class name): " << Cylinder::DEFAULT_COLOR << std::endl;

// 也可以通过对象访问静态常量,但不推荐,因为可能引起误解
std::cout << "Default Cylinder Color (via object name - not recommended): " << cylinder1.DEFAULT_COLOR << std::endl;

// 访问并打印静态常量 PI (虽然一般直接在计算中使用,但也可以打印)
std::cout << "Value of PI: " << Cylinder::PI << std::endl;

std::cout << "\n--- Testing another cylinder ---\n";
Cylinder cylinder2(3.0, 7.0);
std::cout << "Cylinder 2 Volume: " << cylinder2.getVolume() << std::endl;
// 再次证明 PI 和 DEFAULT_COLOR 是共享的,而不是每个对象一份
std::cout << "Cylinder 2's PI value is the same: " << cylinder2.PI << std::endl;

return 0;
}

如何编译和运行:
如果你使用 g++ (GCC) 编译器,可以在终端中执行以下命令:

bash
1
2
g++ main.cpp cylinder.cpp -o my_cylinder_app -std=c++17 # 或 -std=c++20 或更高
./my_cylinder_app

3. QA 闪卡 (QA Flash Cards)

用这些问题来检验你对静态常量的理解:

  1. 问: 什么是 static 成员变量?它和普通成员变量有什么核心区别?
    答: static 成员变量属于类本身,所有对象共享同一个副本。普通成员变量则属于类的每个独立对象,每个对象都有自己的副本。
  2. 问: 为什么我们需要 static const 成员变量?它解决了什么问题?
    答: static const 成员变量用于表示与类逻辑紧密相关、对所有对象都相同、且不可修改的常量。它解决了全局常量命名冲突和内存浪费(每个对象都有副本)的问题,并提升了设计的清晰度。
  3. 问: 在 C++17 中,static inline const 声明中的 inline 关键字有什么作用?
    答: inline 允许 static const 成员变量在类声明(通常是头文件)内部直接定义和初始化,即使是非整型。它避免了在 .cpp 文件中单独定义和初始化的麻烦,并确保在多个编译单元中包含时不会违反 ODR(One Definition Rule)。
  4. 问: 如何访问类的 static 成员变量?哪种方式更推荐?
    答: 可以通过类名(ClassName::member_name)或对象(object.member_name)访问。推荐通过类名访问,因为它更清晰地表明该成员属于类而不是特定对象。
  5. 问: 什么是 const 成员函数?为什么要在 getVolume()getRadius() 这样的 get 方法后面加上 const
    答: const 成员函数承诺不修改对象的状态(非 static 成员变量)。在 get 方法后加上 const,表示这些方法是“只读”的,不会改变对象的数据,这提高了代码的健壮性和可读性,并且允许通过 const 对象调用这些方法。

4. 常见误解或错误 (Common Misunderstandings or Mistakes)

在实际编码中,关于静态常量,初学者常犯以下错误或存在误解:

  1. 忘记 inline (C++17之前或误用):
    • 错误: 在 C++17 之前的版本,或者即使是 C++17 但忘记使用 inline,尝试在类内部直接初始化 static const double PI = 3.14; 会导致编译错误,因为它需要外部定义。
    • 正确做法: 检查你的 C++ 标准版本。如果是 C++17 或更高,请使用 static inline const。如果不是,你必须在 .cpp 文件中进行定义和初始化。
  2. 混淆 static 和非 static 成员:
    • 错误: 误以为 static 成员变量属于每个对象,或者尝试在 static 成员函数中(我们这里没有 static 函数,但这是常见混淆点)直接访问非 static 成员变量(因为 static 函数不与特定对象关联)。
    • 正确理解: static 成员是类的属性,与对象无关。非 static 成员是对象的属性。
  3. 不一致的 const 声明:
    • 错误: 在头文件(.h)中声明成员函数为 const(例如 double getVolume() const;),但在对应的 .cpp 文件中定义时忘记加上 const(写成 double Cylinder::getVolume() { ... })。
    • 结果: 这会导致编译错误,因为声明和定义不匹配。
    • 正确做法: 声明和定义中的 const 关键字必须保持一致。
  4. static 成员的访问权限:
    • 错误: 认为 static 成员不受 public/private/protected 访问修饰符的限制。
    • 结果: 如果 PI 被声明为 private static inline const double PI;,那么在 main 函数中尝试 std::cout << Cylinder::PI; 将会引发编译错误,因为它没有公开访问权限。
    • 正确理解: static 成员同样遵守 C++ 的访问控制规则。
  5. 滥用 static
    • 错误: 将所有常量都定义为 static 成员,即使它们只在一个特定的函数内部使用,或者与类本身没有强关联。
    • 正确理解: static 成员适用于那些真正与类本身相关联、且对所有对象都相同的常量。如果一个常量只在某个函数内部使用,考虑在函数内部定义为局部 const 变量。如果与类无关,考虑全局 const 或命名空间内的 const

5. 编程练习 (Coding Exercise)

现在轮到您来动手实践了!请完成下面的编码练习,以巩固您对 static inline const 的理解。

任务:
Cylinder 类中添加一个 static inline const 成员,表示圆柱体的默认高度 (DEFAULT_HEIGHT),其值为 10.0。然后,在 main 函数中打印出这个默认高度。

请您在下面的代码中填写空白处 // TODO: 在这里添加你的代码 的部分。

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
#ifndef CYLINDER_H
#define CYLINDER_H

#include <string>
#include <iostream>

class Cylinder {
public:
static inline const double PI = 3.14159265358979323846;
static inline const std::string DEFAULT_COLOR = "Blue";

// TODO: 在这里添加你的代码 (默认高度常量)

private:
double radius;
double height;

public:
Cylinder() = default;
Cylinder(double radius_param, double height_param);

double getRadius() const;
double getHeight() const;

void setRadius(double radius_param);
void setHeight(double height_param);

double getVolume() const;
};

#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
#include "cylinder.h"

Cylinder::Cylinder(double radius_param, double height_param) {
radius = radius_param;
height = height_param;
}

double Cylinder::getRadius() const {
return radius;
}

double Cylinder::getHeight() const {
return height;
}

void Cylinder::setRadius(double radius_param) {
radius = radius_param;
}

void Cylinder::setHeight(double height_param) {
height = height_param;
}

double Cylinder::getVolume() const {
return PI * radius * radius * height;
}

main.cpp (骨架代码)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "cylinder.h"

int main() {
Cylinder cylinder1(5.0, 10.0);
std::cout << "Cylinder 1 Volume: " << cylinder1.getVolume() << std::endl;
std::cout << "Default Cylinder Color: " << Cylinder::DEFAULT_COLOR << std::endl;
std::cout << "Value of PI: " << Cylinder::PI << std::endl;

// TODO: 在这里添加你的代码 (打印默认高度)

return 0;
}

请您将填充好的代码粘贴到您的回复中,我将为您检查并提供反馈!


评论