C++成员常量
🧑💻 User
In this lecture, we’re going to see how to model static, constant member variables.If we don’t have access to a C plus plus 17 compiler or later, we’re also going to see how to workwith constant member variables that happen not to be static.And here is the rule before C plus plus 17, you couldn’t really do much in terms of initializing yourmember variables in the class declaration if they are static only integral types and by this I meanint and enum types where initialize able from the class declaration.Any other type would have to go in the CPP file and be initialized from there as we are about to see.So this is the rule.Let’s look at an example here.Here we have a cylinder class and inside we have a bunch of member variables.I put in many types so that you can really see this in action and have many examples to go by.But on top here, we’re going to have what we could initialize in the class declaration.And I mean wherever you declare your class within the curly braces, that includes your class.And before C plus plus 17, you could only do this for integral types and enum you see int constant.Here is an int and a color constant.Here is an enum.So if we initialize these guys in the class definition itself, this would work before C plus plus 17,but any other thing would give you a compiler error.So if we go down here, for example, we have a bunch of member variables.They are static.You see that they have the static keyword in front, but we have many types.So we have a string, we have a car pointer, we have an int, we have a float.All of these things wouldn’t be initialized in the header file.If you tried to do this, you’re going to get a compiler error or I should say in the class definition,you couldn’t really initialize this thing in the class declaration here.If you do that, you’re going to get a compiler error down here.You see that we have our double constant, which is pi here.We can’t also initialize this in the class declaration because it is not in line.So we will have to do that in the CPP file.So how do you exactly initialize these guys in the CPP file?Well, I have an example for you.Here is how you could do that.Our string would be initialized like this.You have to specify the type for what you want to initialize.You have to set the class in which it’s live and using the double colon syntax here.And then you say the variable and then you initialize that either using curly braces or parents.Any kind of initialization is going to work here.We do the same for Pi and our car pointer constant.We’re going to initialize this with a string literal.This is going to work.We have an array and we can initialize that using curly braces like this.We have a float.We’re going to initialize that like this with a float literal and we can initialize a string array justlike we do here.And this is going to work.Okay, so this is fine and dandy, but what about constant member variables that are not static?Let’s go down here and look at an example.We have weird float here.We have wild C string and we have weird int array constant.If we try to initialize these guys in the header file, we’re going to get a compiler error.This is not going to work.But if we do that in the CPP file, please remember that these guys are not static, so you will haveto initialize them like you initialize any other member variable, but because they are const they canonly be initialized through an initializer list.And this is what we have here.So we’re going to initialize weird float with a float literal or a double literal here, but we’re goingto have a narrowing conversion here.We’re going to initialize our weird C string member variable with a string literal and we’re going toinitialize our array of int using the syntax you see here.And this is going to work.We have to go through an initializer list.If our member variable happens to be a constant, if you don’t go through an initializer list and tryto do something like this in the body of the constructor, you’re going to get a compiler error.This is not allowed.So this is another case where we can only do this using initializer lists.And if you don’t use initializer lists, you basically won’t have a way to initialize your constantThis is the only way we have.Another thing I want you to know before we head over to Visual Studio code and play with this is thatif you use a.Static member variable to initialize another variable.You will be basically setting yourself up for disaster because static initialization order is not guaranteed.So if you run your program, some variable might be initialized before another one.And if you run the program again, the order of initialization might change.So sometimes if you are out of luck, you might try to use a variable which hasn’t been set up yet.Try to initialize another variable and your program is going to crash.My recommendation is to never use static member variables to initialize as static member variables becausethat’s really bad.It is recommended to only use static member variables once you have hit the main function because rememberthey are initialized before the main function runs.You should really know this.Now that we know this, we’re going to head over to Visual Studio code and play with this a little more.Okay, Here we are in our working folder.The current project is static constants before C plus plus 17.Here we are going to grab our template files and we’re going to put them in place pretty quick.Let’s do that.And we are going to open this in Visual Studio code by dragging and dropping here, this is going toopen our folder.We can open the main CPP file and clean this up a little bit and we are going to put in our cylinderclass because that’s what we’re going to play with.So let’s put in the files for that.We’re going to create a cylinder dot h file and we’re going to put together a cylinder dot CPP file.I am going to grab the file for the header and I put that in here.And if you don’t want to type all this, you can just grab the file from the resource section on thiscourse.You’re going to have access to this and you can use this however you want.I realize I have put this in the CPP file.It should be in the header file.So let’s correct this.And inside you see that we have basically a lot of member variables and we want to play with differenttypes so that you can really see how we do this in C plus plus versions lower than C plus plus 17,because sometimes you’re going to run into this.If you go down here, we’re going to see that int and enum can be initialized in the class declaration.This is what we should say here.Okay, So you see that we have our static const and we are initializing this right here.If we want we can even use the curly braces.Let’s do that.This is going to work.We’re going to go down and initialize our color, which is an enum that we just declared here.And again here we can also use curly braces.This is going to work.Let’s grab our thing here and put that in place.This is going to work.And this is really all we could do before a C plus plus 17 in terms of initial using our static membervariables inside the class declaration for other types, you will have to put the initialization ina CPP file.So here you see a bunch of types.We have a string, we have a character pointer, which is going to be a C string.We have an INT, we have a float, we have an array of constants, we have an array of strings.All these things will have to be initialized in the CPP file.If you go down, you’re going to see that we have our Pi member variable, which is static.So this is also going to be initialized in the CPP file.So I am going to just grab the code and paste that in here and we’re going to try and understand thatthis is including the header file and we’re going to initialize our member variables in the CPP file.You see, we are initializing the default color, we are initializing pi, the character pointer constant,that’s going to be a C string, the array, the float static variable and our string array.You see that we can really initialize anything in the CPP file and this is going to work.We’re going to have access to these things in the main function in a minute.The other thing I want you to see before we head over to the main function is what if you have constantmember variables that are not static because we haven’t really talked about them so far.If you have those kinds of member variables in your class, things are going to be a little different.You see, we have a float, we have a character pointer, we have a net.They are constants, but they are not static.So these are going to belong to the object.They’re not going to be attached to the blueprint or the class itself.Now, if you have to initialize these guys, the only option you have is to go and initialize the listlike we do here, and let’s be consistent here and use curly braces.Let’s take out.The parentheses here and we’re going to use curly braces here.Okay, So this is done.We will have to go through initializer lists.If you try to use the syntax like this and try to change the constant in the body, you won’t be allowedto do this because this is a constant.And at this point we are trying to change the constant.So C plus plus is going to complain.We’re not allowed to do this.The only way you can put a value in your constant is in the initializer list, because the initializerlist is really not going to be changing the value.It’s going to be initializing that and it is going to put the value in.At the moment, the member variable is created.That’s why this is possible.But you can’t do that in the body like we are doing here.If we do this, we’re going to get a compiler error.So now that we have this, we can head over in the main function and play with these things.We’re going to include our cylinder class.Let’s do that and I am going to put in a piece of code to really play with this.Thanks.I don’t want to type all this because you really know most of this.Thanks.The main message is that some things can be initialized in the class declaration, some things can’t.They have to be initialized in the CPP file.But now that we have this things properly initialized, we can really access them in the main functionand do things with them.For example, we have the cylinder, we can print the volume, we can print the default color.This is going to work.We can access our constants.We’re going to get the int constant, the color constant, the character pointer constant.We can get the array and for example, get the second element.We can get the float constant, We can get our string array constant, we can print all these things.If we go down here, you’re going to see that if we try and modify any of those constants, we’re goingto get a compiler error.So let’s wait.You’re going to see a squiggly line here and we’re going to get a compiler error.If we try to modify this.Let’s try and go through GCC and show you the problem.We’re going to get a compiler error.You see that the build finished with errors and we’re going to get an error saying assignment of readonly variable and constant.We can’t change this because it is a constant.So we’re going to comment this out.You have seen this error.The other error I want you to see is if you try to modify the constant member variable in the body ofyour constructor because some people are going to try and do this and they want to know what is beatingthem.So this is not allowed because this guy is really a constant.You have no business modifying it after it has been set up.If you do this, you’re going to get a compiler error.So let’s do this.We’re going to build with GCC so that you can really see these things we are learning.So don’t be afraid to break things a little bit and we’re going to get an error assignment of read onlymember variable weird float.We can’t do that even if we are in our own constructor.This is not allowed and we can go down and try to access some other things.So we have our weird constants here.Weird float is going to be printed out because I think it is in the public scope.You can get access to that.We’re going to print that in our main function and we’re going to print all these things here.So let’s build the program now.It should build because we have fixed every single problem that we had build finished successfully.We can clear and run Rooster.We’re going to see all the data here.And if you go and you’re going to see that we have what we put in here, for example, int array, let’sgo down and see what we had in there.Enter a constant at index three.Let’s go and see what we have.Enter a constant at index 30123.We should get a 40 and that’s what we have here.So this is working like we expect.One thing I want you to see before we wrap up this lecture is that the order static member variablesare initialized and is not guaranteed to be always the same, and that can get you into trouble.To make you understand this, we’re going to set up a new class.Let’s call that bird, for example.And we’re going to put in place two files.One is going to be a header file.Let’s do that.And we’re going to put in place a CPP file for that.We are going to put in place our bird class.It is really simple.All we have inside is a static constant.It is a float, so we can’t really initialize that in the class declaration.We will have to initialize this in the CPP file, but what we’re going to do is not initialize thiswith the literal.We’re going to initialize this with a static member variable coming from the cylinder class.So let’s do that.We’re going to include the cylinder and we’re going to include the bird before that.Let’s do that and then we’re going to say the type of our variable.So let’s grab the name and we’re going to say const float because that’s what it is and we’re goingto say Bird and we’re going to put our scope resolution.Operator.We can grab this from Visual Studio code and we’re going to initialize this.We’re not going to initialize this with a literal.For example, if we put in 45.3 and say, this is a float, this is going to work just fine, but we’renot going to do this.We’re going to initialize this with another static variable coming from the cylinder class.And let’s see if we can find a float.We’re going to find that here float constant.We have it.So we’re going to say cylinder float constant.Let’s use this.And the compiler is going to take this.But the problem is we don’t really know which one is going to be initialized first.It might be the bird wait constant.It might be the float constant here.And if we happen to initialize the weight constant before the float constant is put in place, we willbe trying to access something that doesn’t exist yet to try and initialize our weight constant.And you might guess what is going to happen.You’re going to get a crash.Let’s try and go in main and see if we can get this crash to show up, but we might not be that lucky.So we’re going to try our best here.So we’re going to go down and we’re going to put in the code to print the bird weight constant.For this to work, we will need to include our bird header.So let’s do that.And now our code should compile.At least let’s do that.We’re going to build this with GCC and you’re going to see that the world is good.But if we run the program, we might not get what we expect.So let’s clear and run Rooster.This is working at this point.This is working.This is working.This is working.This is working.So this is working for now.But if you are unlucky, the bird weight constant is going to be initialized first and it is going toeither grab whatever is at this float, constant member variable, the location of this thing in memory.It might grab something that doesn’t exist yet, it might give you junk value.And if you are trying to access critical memory locations, you’re going to get a crash.And this is really bad.So the message here is that the order for a static initialization is not guaranteed.Sometimes bird weight constant is going to be initialized first, sometimes float constant is goingto be initialized first.And if this is not working in your favor, you’re going to get some weird problems.Let’s actually prove that float constant is 6.98.Here it is.So this is working fine, but it might not work all the time.For example, if you run this program on another machine.Static initialization might not work the way it is working here and you might be in trouble.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.The next one we’re going to see how you can set up member variables of the same class that you are declaring.So go ahead and finish up here and meet me there.
🤖 Assistant
好的,欢迎来到C++的学习之旅!我是您的专属C++老师,将尽我所能,用最清晰、最易懂的方式带您探索C++的奥秘。
掌握C++类中的静态常量与非常量成员变量(C++17之前)
简介: 本节将深入讲解C++17标准之前,如何在类中声明和初始化static
(静态) 和const
(常量) member variables
(成员变量),包括静态常量成员变量和非常量成员变量,并探讨其特有规则和潜在问题。
详细讲解
C++中的类(class)允许我们封装数据和行为。这些数据通常通过成员变量来表示。当我们在类中定义成员变量时,有时会希望它们具有特定的性质,例如:
- 静态 (Static): 表示这个成员变量不属于类的某个具体对象,而是属于整个类。所有对象共享同一个静态成员变量的副本。
- 常量 (Const): 表示这个成员变量一旦被初始化,其值就不能再被修改。
将这两者结合起来,就产生了static const
成员变量,以及只使用const
的非常量成员变量。由于C++语言的演进,在C++17之前和之后,它们的初始化规则有所不同。本节我们将主要关注C++17之前的规则。
1. 静态常量成员变量 (Static Const Member Variables)
static const
成员变量是与类本身关联的常量,所有该类的对象共享同一个常量值。在C++17标准之前,对于这类变量的初始化有严格的限制:
1.1 仅允许在类声明中初始化的类型 (integral types
和 enum
)
在C++17之前,只有integral types
(整型,如int
, char
, short
, long long
等) 和 enum
(枚举) 类型的static const
成员变量可以在class declaration
(类声明) 内部(也就是类定义的{}
花括号内)直接进行initialization
(初始化)。
为什么?
这是因为这些类型在编译时其值就可以确定,并且它们通常占用固定且较小的内存空间。编译器可以直接将这些常量“烘焙”到程序代码中,而不需要在运行时分配内存。
示例:
1 | // Cylinder.h 文件 |
在上面的例子中,
s_int_constant
和s_default_color
可以直接在Cylinder
类的定义中初始化。您可以像上面一样使用等号=
进行初始化,也可以使用花括号 {}
进行列表初始化,例如 static const int s_int_constant {100};
。
1.2 必须在对应的.cpp
文件(定义文件)中初始化的类型
除了integral types
和enum
之外,其他所有类型的static const
成员变量,包括double
(双精度浮点型), float
(单精度浮点型), std::string
(字符串), char*
(字符指针), 以及各种数组类型等,都不能在class declaration
中初始化。它们必须在类的定义文件(通常是.cpp
文件,也称为translation unit
(翻译单元))中进行定义和初始化。
为什么?
对于非整型或非枚举类型的静态常量,它们的内存分配和初始化可能更复杂,或者其值在编译时无法完全确定(例如,std::string
的初始化可能涉及动态内存分配)。因此,C++标准要求它们在类的外部,也就是在.cpp
文件中分配存储空间并进行初始化。
初始化语法:
在.cpp
文件中初始化静态成员变量的语法是:类型 类名::静态成员变量名 = 值;
示例:
假设您的Cylinder
类声明(Cylinder.h
)如下:
1 | // Cylinder.h 文件 |
那么在
Cylinder.cpp
文件中,您需要这样初始化它们:1 | // Cylinder.cpp 文件 |
关键点:
- 在
.cpp
文件中初始化时,您必须再次指定变量的类型 (const double
,const std::string
等)。 - 必须使用
类名::
来指明这个静态成员变量是属于哪个类的。 - 可以使用各种形式的初始化(等号
=
, 花括号{}
, 圆括号()
)来赋值。
1.3 静态初始化顺序问题 (Static Initialization Order Fiasco
)
这是一个非常重要的概念,理解不当可能导致程序崩溃或产生undefined behavior
(未定义行为)。
问题描述:
当您的程序中有多个translation unit
(.cpp
文件) 并且每个文件中都有需要初始化的static
成员变量(无论是const
还是非const
)时,C++标准不保证这些static
变量的初始化顺序。
示例:
假设我们有两个类:Cylinder
和Bird
。Cylinder.h
中有一个static const float s_float_constant
。Bird.h
中有一个static const float s_bird_weight_constant
。
如果在Bird.cpp
中,我们尝试用Cylinder::s_float_constant
来初始化Bird::s_bird_weight_constant
:
1 | // Bird.cpp |
潜在风险:
由于静态初始化顺序不确定,可能发生以下两种情况:
Cylinder::s_float_constant
先被初始化,然后Bird::s_bird_weight_constant
使用它来初始化。这是我们期望的,程序正常运行。Bird::s_bird_weight_constant
先被初始化,此时Cylinder::s_float_constant
可能尚未被初始化。如果Bird::s_bird_weight_constant
试图访问一个未初始化的内存地址,它可能读取到垃圾值,甚至引发内存访问错误导致程序崩溃。
解决方案/最佳实践:
- 避免使用一个静态成员变量来初始化另一个静态成员变量。 除非它们位于同一个
translation unit
中,并且您能明确它们的初始化顺序(但这通常很难保证)。 - 推荐的做法是: 尽量用字面量(如
3.14f
)或简单的编译时常量来初始化静态成员变量。 - 延迟初始化 (Lazy Initialization): 如果必须使用一个静态成员变量来初始化另一个,可以考虑使用函数来延迟初始化,例如单例模式中的局部静态变量,它在第一次调用时才被初始化。但对于
const
成员,这种方法不直接适用。 - 确保在
main
函数开始执行后才使用静态成员变量。 因为所有静态存储期变量在main
函数执行前都会被初始化(如果它们在同一个translation unit
中)。
2. 非常量成员变量 (Non-Static Const Member Variables)
这类成员变量是const
的,但它们不是static
的。这意味着它们属于类的每个对象,并且一旦对象创建并初始化后,它们的值就不能改变。
核心规则:const
成员变量必须在构造函数中通过initializer list
(初始化列表) 进行初始化。它们不能在构造函数的函数体内部通过赋值操作进行初始化。
为什么?const
的含义是“创建后不可更改”。初始化列表是在对象构造阶段,成员变量被分配内存后立即进行值设定的唯一机会。在初始化列表完成后,进入构造函数体时,成员变量已经完成初始化,此时任何赋值操作都将被视为尝试修改一个const
变量,这是不允许的。
示例:
假设Cylinder
类声明(Cylinder.h
)如下:
1 | // Cylinder.h |
在
Cylinder.cpp
中,您必须使用初始化列表来初始化它们:1 | // Cylinder.cpp |
误区提示:
很多初学者会尝试在构造函数体内给
const
成员变量赋值,例如:1 | // 错误示例 |
这会导致编译错误,因为
m_weird_float
在进入构造函数体时已经默认初始化(如果类型允许),此时再赋值就成了“修改”一个const
变量。
3. C++17及之后的变化 (简要提及)
虽然本节主要关注C++17之前,但有必要知道C++17引入了inline static
关键字,允许static const
成员变量(以及非const
的static
成员变量)在类声明中进行定义和初始化,而无需在.cpp
文件中单独定义。这大大简化了代码,但其底层仍然需要编译器处理好静态初始化顺序问题。对于非常量成员变量,初始化列表的规则没有改变。
QA 闪卡 (QA Flashcards)
- Q1: 在C++17之前,哪种
static const
成员变量类型可以在类声明中直接初始化?
A1:integral types
(整型) 和enum
(枚举) 类型。 - Q2: 如果
static const
成员变量不是整型或枚举,在C++17之前应该在哪里进行初始化?
A2: 必须在对应的.cpp
文件(translation unit
)中进行定义和初始化。 - Q3: 为什么
const
(非常量)成员变量必须通过构造函数的initializer list
进行初始化,而不能在构造函数体内部赋值?
A3: 因为const
成员变量在对象创建后不能被修改。initializer list
是在成员变量创建时就赋予其初始值的机制,而构造函数体内的赋值操作则是在成员变量已经创建并可能被默认初始化后进行的“修改”行为,这与const
的语义冲突。 - Q4: 什么是
static initialization order fiasco
(静态初始化顺序问题)?它会带来什么风险?
A4: 它是指在程序中,不同translation unit
(.cpp
文件)中的static
变量的初始化顺序是不确定的。风险是如果一个static
变量在初始化时尝试访问另一个尚未初始化的static
变量,可能导致程序崩溃或产生undefined behavior
。
常见误解或错误 (Common Misunderstanding or Mistake)
- 误解1: 所有
static const
成员都可以在类内直接初始化。
纠正: 在C++17之前,只有整型和枚举类型可以。其他类型必须在.cpp
文件中初始化。尝试在类内初始化非整型/枚举的static const
会引发编译错误。 - 误解2:
const
成员变量可以在构造函数体内部赋值。
纠正: 这是最常见的错误之一。const
成员变量必须且只能通过构造函数的initializer list
进行初始化。在构造函数体内部的赋值操作是无效的,并会导致编译错误。 - 误解3: 静态成员变量的初始化顺序总是确定的。
纠正: 只有在同一个translation unit
(.cpp
文件)内部,它们的初始化顺序才基本遵循声明顺序。但跨translation unit
时,顺序是不确定的,这可能导致static initialization order fiasco
。 - 误解4:
static const
和非static const
的初始化方式相同。
纠正: 它们完全不同。static const
成员属于类本身,其初始化取决于类型和C++版本(C++17之前需在.cpp
文件)。非static const
成员属于对象,必须通过构造函数的initializer list
初始化。
编程练习 (Coding Exercise)
请根据我们今天所学的内容,完善下面的Product
类。
目标:
- 在
Product.h
中声明一个static const double PI_VALUE
,它是一个非整型。 - 在
Product.h
中声明一个static const int MAX_ID
,它是一个整型。 - 在
Product.h
中声明一个非静态的const std::string m_product_name
。 - 在
Product.cpp
中实现所有必要的初始化。
Product.h
:
1 |
|
Product.cpp
:
1 |
|
请您尝试完成 Product.h
和 Product.cpp
文件中的空白部分,然后我将为您提供解决方案、评分和进一步的建议。