avatar

目录
318 - inline static variable

318 - Inline Static Variable

🧑‍💻 User

In this lecture, we’re going to learn about inline static member variables, and these are static membervariables you can set up and initialize them in the class definition itself.If you remember in the last lecture, we weren’t able to do something like this, and if we did that,we did get a compiler error.If we marked the member variable as inline, we are telling the compiler to allow this syntax here tocompile.And what it is going to do is to constrain this member variable to the translation unit where this classis going to show up and that’s going to avoid problems and that’s going to be avoiding one definitionrule issues.And this is going to work.We can’t really say much about how this is going to work under the hood.We’re going to have more details to understand that starting the next chapter.But for now, note that if you want to initialize your static member variable inside the class, youcan mark it as inline and it is going to work.Now that you know this, we’re going to head over to Visual Studio code and play with this a littlemore.Okay.So here we are in our working folder.The current project is in line static member variables.We’re going to grab the files from the last project because we have a good starting point from there.So we’re going to go there and grab everything except for the rooster file because we’re going to bebuilding our own in this lecture and we’re going to paste them in place and we’re going to open thisup in Visual Studio code pretty quick.Okay, We’re going to open up our main CPP file and the point files, and I don’t think we need thesome other translation file anymore so we can ditch it and really take this out.And let’s see, let’s see what we can do.We can delete that.We don’t need this anymore.Let’s remove this and we are going to go in our class and mark our static member variable as inlineand we’re going to be allowed to initialize that like this.The syntax is going to work now because this guy is in line and the compiler knows that this is notgoing to be problematic.If we go in the main function, we can use our program like we have been using it in the last lectureand it is going to work.We’re going to see a one and a four printed out because that’s what we got in the last lecture.And the point count was basically how many points we have created in our program.At this point, we have one, which is this guy here.So we print a one, we’re going to add three points that we store in this array.And if we print here, we’re going to get four points.So let’s build this with GCC.We’re going to do that.The world is good.We can clear and run Rooster.We should see a one and a four.And this is working exactly as we expect.This is really all I wanted you to see in this lecture that you can use inline static member variablesif you want to initialize your thanks in the class declaration itself.And another thing I should point out is that this was only possible starting in C plus plus 17.So if you are on C plus plus 14 or 11 or any other C plus plus version, this is not going to work,but we are using C plus plus 20 here.So this is working.So this is really all I had to share in this lecture.I hope you found it interesting.We are going to stop here in this one.In the next one, we’re going to learn about static constants.Go ahead and finish up here and meet me there.


🤖 Assistant

你好!很高兴能帮助你学习C++。作为一个C++开发者和你的老师,我将用最清晰、最详细的方式来讲解“inline static 成员变量”这个概念。


C++ 进阶:深入理解 inline static 成员变量

课程摘要: 本课程将详细讲解C++17引入的 inline static 成员变量。你将学会如何在类定义中直接初始化静态成员变量,以及 inline 关键字在此处的特殊作用,从而避免传统静态成员初始化中的“一次定义规则” [One Definition Rule (ODR)] 问题。


1. 详细讲解 (Detailed Explanation)

在C++中,static 关键字有很多用途,其中之一就是修饰类的成员变量。我们首先来回顾一下 static 成员变量,然后引入 inline static 的概念。

1.1 static 成员变量回顾

  • 定义: 当一个类的成员变量被声明为 static 时,它就不再属于类的某个具体对象,而是属于整个类本身。这意味着:

    • 共享性: 所有的类对象共享同一个 static 成员变量的副本。无论你创建多少个类的对象,它们都访问和修改的是同一个 static 成员变量。
    • 生命周期: static 成员变量的生命周期与程序的生命周期相同。它在程序启动时被创建,在程序结束时被销毁,而不是在对象创建时创建,对象销毁时销毁。
    • 访问方式: 你可以通过类名直接访问 static 成员变量(例如 ClassName::static_variable),也可以通过对象访问,但通常推荐通过类名访问,因为它更清晰地表明了变量的类级别属性。
  • 传统初始化方式: 在C++17之前(以及对于非 const static 整型成员变量),static 成员变量通常不能在类定义内部直接初始化。你需要在类定义之外,在一个 源文件 [.cpp file] 中进行定义和初始化。

    • 为什么不能在类内部直接初始化? 这涉及到C++的“一次定义规则” [One Definition Rule (ODR)]。如果一个 static 成员变量在头文件中被声明,并在头文件中直接初始化,那么当这个头文件被多个 源文件 [source files] 包含时,这个 static 成员变量就会在每个 源文件 对应的 翻译单元 [translation unit] 中都被定义一次。链接器在尝试将这些 翻译单元 组合成一个可执行文件时,就会发现同一个变量被定义了多次,从而导致链接错误。

    举例说明传统方式:

    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Point.h
    class Point {
    public:
    Point();
    // 声明一个静态成员变量
    static int point_count;
    };

    // Point.cpp
    #include "Point.h"
    // 在源文件中定义并初始化静态成员变量
    int Point::point_count = 0; // 必须在这里初始化
    Point::Point() {
    point_count++;
    }

    这种方式确保了 point_count 在整个程序中只有一个定义。

1.2 “一次定义规则” (One Definition Rule, ODR)

“一次定义规则” [One Definition Rule (ODR)] 是C++编程中一个非常重要的概念。它规定了在整个程序中,每个非内联 [non-inline] 的函数、每个变量、每个类型等都只能有唯一的定义。如果同一个实体被定义了多次,就会导致链接错误。对于 static 成员变量而言,传统的做法就是将其定义和初始化放在一个 .cpp 文件中,以确保它只被定义一次。

1.3 inline static 成员变量的引入 (C++17)

从C++17标准开始,C++引入了一个非常方便的特性:允许在类定义内部直接初始化 static 成员变量,只需要给它加上 inline 关键字。

  • 语法:

    cpp
    1
    2
    3
    4
    5
    6
    7
    // ClassName.h
    class ClassName {
    public:
    // ...
    inline static int static_member_variable = initial_value;
    // ...
    };
  • 它解决了什么问题? 它的主要目的是为了方便,特别是对于那些需要在头文件中定义和初始化的 static 成员变量,比如在 header-only 库中。通过 inline static,你不再需要额外的 .cpp 文件来定义和初始化这些静态成员。

1.4 inline 关键字的作用

你可能已经知道,inline 关键字可以用于函数,作为对编译器的“建议”,希望编译器将函数体直接展开到调用点,以减少函数调用的开销。但当 inline 用于 static 成员变量时,它的作用完全不同,它主要与“一次定义规则” [One Definition Rule (ODR)] 相关:

  • 允许在多个翻译单元中定义: 当一个 static 成员变量被标记为 inline 时,编译器就知道,即使这个变量在多个 翻译单元 [translation units](即多个 .cpp 文件编译后的目标文件)中都被定义和初始化了,它也是合法的。
  • 链接器处理: 链接器在最终链接时会确保所有这些定义都指向同一个实际的变量实例,从而避免了链接错误。它就像编译器和链接器之间的一个特殊约定:inlinestatic 成员变量允许在多个地方出现定义,但最终只会被“挑选”一个出来使用。

总结一下 inline static 的核心:
它允许你在 头文件 [header file] 中的类定义内部声明和初始化 static 成员变量,同时避免了传统 static 成员变量可能导致的 ODR 问题。这使得代码更简洁,特别是对于小型项目或 header-only 库非常有用。

1.5 C++ 版本兼容性

非常重要的一点: inline static 成员变量是在 C++17 标准中引入的。这意味着如果你使用C++11、C++14或更早的标准,这个特性将不可用,编译器会报错。在学习或使用时,请确保你的编译器支持C++17或更高版本(例如C++20)。本课程中,我们假设你使用的是C++17或C++20。

2. 代码示例 (Code Example)

让我们根据课程内容,通过一个具体的代码示例来理解 inline static 成员变量的使用。

目标: 我们将创建一个 Point 类,并使用 inline static 成员变量 point_count 来追踪创建了多少个 Point 对象。

首先,设置你的项目。假设你有一个名为 inline_static_member_variables 的工作文件夹。

文件结构:

Code
1
2
3
inline_static_member_variables/
├── Point.h
└── main.cpp

Point.h (类的定义)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once // 防止头文件被多次包含

class Point {
public:
// 构造函数:每创建一个Point对象,就增加point_count
Point();

// 析构函数:虽然这里我们不减少point_count,但好的实践会考虑资源释放
// ~Point();

// 获取当前Point对象的ID(假设我们给每个Point一个独特的ID)
int get_id() const;

// C++17特性:inline static 成员变量
// 我们现在可以在类定义内部直接声明并初始化静态成员变量
// 它是一个int类型,名为 point_count,初始值为0
inline static int point_count = 0;

private:
int m_id; // 每个Point对象的唯一ID
};

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream> // 用于输入输出
#include <vector> // 用于存储Point对象
#include "Point.h" // 包含Point类的定义

// Point类的实现,放在main.cpp中或单独的Point.cpp中
// 为了简化,我们直接放在main.cpp中(实际项目中Point.cpp更规范)
Point::Point() {
// 每次创建Point对象时,m_id 设置为当前的 point_count 值,然后 point_count 递增
// 这样每个Point对象都会有一个从0开始递增的唯一ID
m_id = point_count;
point_count++; // 每次构造函数被调用时,静态变量 point_count 就会增加
}

int Point::get_id() const {
return m_id;
}


int main() {
std::cout << "------------------------------------------" << std::endl;
std::cout << "欢迎来到 inline static 成员变量学习!" << std::endl;
std::cout << "------------------------------------------" << std::endl;

// 1. 在创建任何Point对象之前,打印 point_count
// 此时 point_count 应该为 0 (或者如果Point::Point() 已经初始化了第一个对象,则为1)
// 根据Point::Point()的实现,m_id = point_count; point_count++;
// 如果 main 函数中第一个 Point 是 `Point p1;` 那么第一个 m_id 为 0,point_count 变为 1
// 如果是 Point::point_count = 0; 那么 m_id 为 0。
// 在这里我们是创建了一个 Point 对象 p1。
Point p1; // 创建第一个Point对象

std::cout << "创建第一个Point对象后,当前 Point 数量: " << Point::point_count << std::endl; // 预计输出 1

// 2. 创建一个Point对象的数组,并添加一些对象
// 这将创建 3 个额外的Point对象
std::vector<Point> points_array;
for (int i = 0; i < 3; ++i) {
points_array.emplace_back(); // 使用emplace_back效率更高
}

std::cout << "再创建3个Point对象后,当前 Point 数量: " << Point::point_count << std::endl; // 预计输出 1 + 3 = 4

std::cout << "------------------------------------------" << std::endl;
std::cout << "示例完成!" << std::endl;
std::cout << "------------------------------------------" << std::endl;

return 0;
}

编译和运行:

  1. 保存文件: 将上述代码分别保存为 Point.hmain.cpp
  2. 打开终端/命令提示符: 导航到你保存文件的目录。
  3. 编译 (使用 g++ 或 clang++):
    为了确保使用C++17或更高标准,请添加 -std=c++17-std=c++20 标志。
    bash
    1
    g++ main.cpp -o my_program -std=c++17 -Wall -Wextra
    • g++: C++ 编译器(如果你在Windows上,可能需要安装 MinGW 或 MSYS2;在Linux/macOS上通常预装)。
    • main.cpp: 要编译的源文件。
    • -o my_program: 指定输出可执行文件的名称为 my_program (Windows上是 my_program.exe)。
    • -std=c++17: 启用C++17标准。
    • -Wall -Wextra: 启用所有常用警告和额外警告,帮助你写出更健壮的代码。
  4. 运行:
    bash
    1
    ./my_program
    预期输出:
    Code
    1
    2
    3
    4
    5
    6
    7
    8
    ------------------------------------------
    欢迎来到 inline static 成员变量学习!
    ------------------------------------------
    创建第一个Point对象后,当前 Point 数量: 1
    再创建3个Point对象后,当前 Point 数量: 4
    ------------------------------------------
    示例完成!
    ------------------------------------------
    这个输出与课程中提到的预期输出 14 完全一致。

3. 问答闪卡 (QA Flash Cards)

Q1: inline static 成员变量和普通的 static 成员变量在声明和初始化上有什么不同?
A1: 普通的 static 成员变量通常在类定义内部声明,但在类定义外部的一个源文件中定义并初始化(因为要遵守ODR)。而 inline static 成员变量允许在类定义内部直接声明并初始化,并且可以在多个翻译单元中出现,链接器会处理好 ODR 问题。

Q2: 为什么C++17之前不能在类定义内部直接初始化 static 成员变量(除了 const static 整型)?
A2: 主要是因为“一次定义规则” [One Definition Rule (ODR)]。如果头文件中的 static 成员变量在类定义内部被初始化,当该头文件被多个源文件包含时,这个变量就会被定义多次,导致链接错误。

Q3: inline 关键字用于 static 成员变量时,其主要作用是什么?
A3: inline 关键字在这里并不意味着强制内联代码,而是告诉编译器和链接器,允许这个 static 成员变量在多个 翻译单元 [translation units] 中被定义。链接器在最终链接时会确保所有这些定义都指向同一个实际的变量实例,从而避免了 ODR 问题。

Q4: 从哪个C++标准版本开始,inline static 成员变量才被允许使用?
A4:C++17 标准开始。

4. 常见误解与错误 (Common Misunderstandings and Mistakes)

  1. 误解 inline 的含义: 最常见的误解是认为 inline static 成员变量的 inline 关键字意味着像内联函数一样,会提升性能或将变量值直接“内联”到使用它的地方。实际上,这里的 inline 与性能无关,它是一个与“一次定义规则” [One Definition Rule (ODR)] 相关的语言规则,允许变量在多个翻译单元中被定义。
  2. C++版本不兼容: 尝试在C++14或更早版本的项目中编译使用 inline static 成员变量的代码,会导致编译错误。新手可能不清楚这个特性是C++17才引入的。
  3. 忘记 static 关键字: 错误地将 inline 关键字用于非 static 成员变量。inline 只能用于 static 成员变量(或函数),对于普通的成员变量,它没有意义。
  4. 误以为所有 static 成员变量都可以这样初始化: 只有 inline static 成员变量才可以在类内部直接初始化。传统的非 inline static 成员变量(特别是如果你的C++版本低于C++17或者需要旧的行为)仍然需要在类外定义和初始化。
  5. 过度使用或滥用: 尽管 inline static 很方便,但并不是所有 static 成员变量都适合在头文件中初始化。例如,如果静态成员变量的初始化涉及复杂逻辑、依赖其他库或在多个 .cpp 文件中有不同的用途(虽然这种情况不常见且通常是设计问题),则传统的在 .cpp 文件中初始化可能更合适。但在大多数简单计数器或常量场景下,inline static 是非常棒的。

5. 编程练习 (Coding Exercise)

现在,请你尝试完成下面的编程练习。你需要修改现有的代码,使其能够追踪并显示程序中所有 Dog 对象的数量。

任务:

Dog 类中添加一个 inline static 成员变量 dog_count,用于在每次创建 Dog 对象时增加计数。在 main 函数中,分别在创建第一个 Dog 对象后和创建更多 Dog 对象后,打印当前的 dog_count 值。

Dog.h:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once

#include <string>
#include <iostream>

class Dog {
public:
Dog(const std::string& name);
void print_info() const;

// TODO: 在这里添加 inline static 成员变量 dog_count 并初始化为 0
// 例如:inline static int some_variable = 0;

private:
std::string m_name;
};

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
31
32
33
#include <iostream>
#include <vector>
#include "Dog.h"

// Dog 类的实现 (为了简化,这里直接提供)
Dog::Dog(const std::string& name) : m_name(name) {
std::cout << "Dog " << m_name << " created." << std::endl;
// TODO: 在这里增加 dog_count 的值
}

void Dog::print_info() const {
std::cout << "This dog's name is: " << m_name << std::endl;
}

int main() {
std::cout << "--- Dog Counter Exercise ---" << std::endl;

// TODO: 创建第一个 Dog 对象,并打印当前的 Dog 数量
// 例如:Dog d1("Buddy");
// std::cout << "Current dog count: " << Dog::some_variable << std::endl;


// TODO: 创建一个包含 2 个 Dog 对象的 vector,并打印当前的 Dog 数量
// 例如:std::vector<Dog> dogs;
// dogs.emplace_back("Max");
// dogs.emplace_back("Bella");
// std::cout << "Current dog count after adding more: " << Dog::some_variable << std::endl;


std::cout << "--- Exercise End ---" << std::endl;

return 0;
}

请你将代码填写完整,然后告诉我你的解决方案。


评论