Different Type Members
🧑💻 User
In this lecture, we’re going to explore the situation when we are setting up a class, but it has membervariables of other types.So it’s basically going to be different from what we had in the last lecture where we had member variablesof the same class, why we are trying to declare the same class.So here is a class we’re going to use.It’s called Integer.It’s basically going to be a wrapper on an int type and it’s going to have a bunch of the constructors.We have the destructor here and we have a getter and setter that we can use to manipulate the valuethat we are wrapping around here.Nothing complicated here, but we’re going to be setting up member variables of integer type in ourpoint class, and here we’re going to basically try and simulate what we had in the last lecture.Here we have a member variable of integer type.Our integer class, we have a cast member variable.You’re going to see that this is going to work.We have an inline static variable.We can set this up.It’s going to work.We can initialize that here because it is inline.You already know this.We can have a const static member variable of integer type and we can initialize that in here.This is also going to work.You see that the limitations we had in the last lecture don’t apply because the member variable is basicallydifferent from the class that we are trying to set up here.I want you to be aware of this.Another point which is going to be very different is if we try to use a pointer member variable likewe are doing here, we initialize this to null pointer, but if we hit the implementation, you’re goingto see that now we don’t have to go through a function to set up new memory and allocate that on theheap.We can do this directly in the initializer list and this is going to work because we’re not trying tocall the constructor multiple times.It is going to be a pointer constructor calling an integer constructor.So we’re not going to have that chain of the constructor calling itself like we had in the last lecture.And this is going to work just fine.We’re not going to crash our program.If we go down, we see that we can create a static integer member variable.This is going to work.We can even make that const, this is going to work.But because this static member variables are not in line, we’re going to have to initialize them inthe CPP file.You have to remember that.So if we go in our CPP file, we’re going to see that we are initializing IE6 and IE7 and putting valuesinside.And this is really all if the member variable is not of the same class that you are declaring, youare free to use many things as you want 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.Here we are in our working folder.The current project is member variables of other types.We are going to grab our template files and use them here, so we’re going to put them in place.Okay, here we are in our working folder.The current project is member variables of other types.We’re going to grab our template files and we’re going to put them in place.Let’s do that and we’re going to open this in Visual Studio code by dragging and dropping here, thisis going to open our project and we’re going to create the classes that we want to use.We want to use the point class and the integer class.So let’s create the integer class first.We’re going to say integer, dot h for the header and we’re going to say integer dot cpp for the implementationfile.I am going to grab the code for integer, dot h and put that in.Here.We have our include guard, we have the class declaration inside, we have our member variable whichis inner and we are basically wrapping around this and through this integer class, which is our owncustom class here.We are also going to put in the implementation for the class and in the implementation file we’re goingto have our constructor and destructor.This is nothing you don’t know yet.We’re going to just close this class here because it is already defined and we’re going to create ourpoint class.We’re going to create a point dot h class header file, I should say, and we’re going to do the implementationfile, which is going to be point dot CPP and we are going to put in the header file.Let’s do that.And again, it is nothing you haven’t seen before.We have our include guard inside.We have the declaration for the class point.We have our good old member variables X and y.We have the point count static variable.But what we really want to see are these member variables of integer type.We can use all this.Taxes here.And this is going to work.Unlike what we had in the last lecture where the member variables were of point type.Basically creating incomplete type compiler errors if we use syntaxes like this here.But in this case it is working because the member variable type is different from the class type here.Okay.This is then we’re going to put in the implementation part and here it is basically what you alreadyknow.The main difference is that now, because integer six and seven are static and they are not inline,we will have to initialize them in the CPP file and that’s what we do here.Another main difference for you to see is that if we use a pointer, we can initialize this directlyas null pointer.This is going to work, but if we go in the CPP file now we have the option to initialize this directlywithout going through some member function like we did in the last lecture.The reason is, again, we don’t have a situation in which the constructor for Point is going to callitself repeatedly.The types are different.So here Point is going to call the constructor for Integer and this is going to work.We’re not going to have any problem.Okay.Now that we have this setup, we can head over in the main CPP file.We can clean it up a little bit and remove what we don’t need.We’re going to include our point class.Let’s do that.We’re going to include that and we can go in the main function and do all kinds of crazy things.For example, we’re going to put in a piece of code which is going to try the point class a little bit.We’re going to set up a point object.We can print the distance.We can get the point that we have in here.For example, we can access I1 i2 i3 I4 If we try to set the value for I1 and I2, we’re going to geta compiler error.If we try to set up I1.This is going to work because I1 is not const.If we go in we’re going to see that it is not const here, but if we go in main and try to change Ito let’s go back I2 is cost.If we try to change that, we’re going to get a compiler error.Let’s try to use this.We’re going to see that we’re going to get a squiggly line.This is going to give us a compiler error.Let’s build and you’re going to see that, okay, we have a compiler error and we are trying to calland we’re going to get our compiler error build finished with errors.And the problem is that we are trying to modify something that is cost.So we get to this error here that we are trying to use this pointer to modify something that is costand this is not going to be allowed.Okay, So this is the compiler error.We can go down and access the point five.And you remember that this was a pointer, but we’re going to get the value inside and use that thisis going to work.We can access point six and seven and we’re going to get all these values here.This is going to work just fine if we try to set the value for I six.Let’s go back and see what six is.It is a non const integer.So this is going to work.But if we try to do the same for I seven, we’re going to get a compiler error because I seven is constand we can see that here.Okay.So once we do all these things, we’re going to end our test and things are going to work exactly likewe expect.Let’s build again.We shouldn’t get any compiler error now.The build is good.We can clear and run Rooster and we’re going to get our data in our output terminal.This is really all I wanted you to see in this lecture that you can set up things like this.Please try to go through this and really make sense of what you see here.You can even try and set up your own variables and really try and break things.That’s how we learn.We are going to stop here in this lecture and the next one we’re going to try and learn about staticmember functions.Go ahead and finish up here and meet me there.
🤖 Assistant
好的,同学们,欢迎来到我们的C++进阶课程!
标题:探索C++类中“其他类型”成员变量的奥秘
概要
本节课我们将深入学习C++类中如何设置非自身类型的成员变量(member variables)以及它们与“自身类型”成员变量的不同之处。我们将通过一个Integer
封装类和一个Point
类来具体演示各种成员变量的声明、定义和初始化方法,重点分析为什么这些场景下的限制比上一讲中“自身类型”成员变量的限制要少,并提供详细的代码示例、常见误区和练习。
2. 详细讲解:C++类中的非自身类型成员变量
同学们好!在上一讲中,我们探讨了C++类如何拥有其自身类型的成员变量,并发现了一些限制,比如不能直接声明一个非指针的自身类型成员变量,以及在构造函数中初始化时的递归问题。今天,我们将把目光投向一个更常见、也更灵活的场景:当一个类拥有其他类型(other types)的成员变量时。
为了更好地理解这个概念,我们将创建一个简单的Integer
类,它将作为对C++内置int
类型的一个封装(wrapper)。这意味着Integer
类的对象将包含一个int
类型的值,并提供一些方法来操作它。然后,我们将在Point
类中使用这个Integer
类作为其成员变量。
2.1 Integer
类:一个简单的数值封装器
首先,我们来定义我们的Integer
类。这个类非常简单,它包含一个私有的int
成员变量,以及一些构造函数(constructors)、析构函数(destructor)、获取器(getter)和设置器(setter)。
Integer.h
文件:
1 |
|
Integer.cpp
文件:
1 | // 实际上,Integer类的方法如果都很短,也可以直接在头文件中实现 |
注意:为了简洁和符合常见的短函数内联实践,我们在
Integer.h
中直接实现了这些方法。因此Integer.cpp
文件在这个例子中可以保持为空。
2.2 Point
类:包含Integer
成员变量
现在,我们来创建Point
类,并让它包含Integer
类型的成员变量。我们将演示各种不同类型的成员变量:普通成员变量、常量成员变量、指针成员变量、静态成员变量和静态常量成员变量。
Point.h
文件:
1 |
|
Point.cpp
文件:
1 |
|
2.3 main.cpp
文件:测试和演示
1 |
|
2.4 核心要点及对比
现在,让我们来逐一分析各种成员变量的声明和初始化方式,以及与上一讲的“自身类型”成员变量的区别。
普通成员变量 (
Integer i1;
):- 声明:直接在类定义内部声明,就像内置类型一样。
- 初始化:如果类(
Point
)没有提供显式的构造函数来初始化它,或者在Point
的构造函数初始化列表中没有显式初始化它,它会调用Integer
的默认构造函数(default constructor)进行初始化。如果Integer
类没有默认构造函数,则会引发编译错误。 - 区别:与上一讲不同,这里没有“不完整类型”(incomplete type)的问题。因为
Integer
类在Point.h
中被#include
了,编译器在处理Point
类时,Integer
的完整定义是可见的。
常量成员变量 (
const Integer i2;
):- 声明:在类型前加上
const
关键字。 - 初始化:必须在
Point
类的构造函数的成员初始化列表中进行初始化。这是因为const
成员变量在对象构造时必须立即获得其最终值,不能在构造函数体内部赋值(那样是先默认构造再赋值,但const
不允许)。 - 区别:同样,没有“不完整类型”问题。其初始化规则与内置类型的
const
成员变量类似。
- 声明:在类型前加上
内联静态成员变量 (
inline static Integer i3 = 100;
):- 声明:使用
inline static
关键字。 - 初始化:可以在类定义内部直接进行初始化。
inline
关键字允许在头文件中定义和初始化静态成员变量,避免了多重定义错误(multiple definition error)。对于用户自定义类型(user-defined type)的静态成员变量,inline
是实现类内初始化的关键。 - 区别:在C++17之前,只有静态常量整型(static const integral type)成员变量(如
int
,char
,bool
等)才允许在类内初始化。对于用户自定义类型如Integer
,需要使用inline
才能在类内初始化。
- 声明:使用
内联静态常量成员变量 (
inline static const Integer i4 = 200;
):- 声明:使用
inline static const
关键字。 - 初始化:与
i3
类似,inline
关键字使其可以在类定义内部直接初始化。 - 区别:和
i3
一样,关键在于inline
。
- 声明:使用
指针成员变量 (
Integer* i5;
):- 声明:声明为一个指针类型。
- 初始化:
- 可以在类内初始化为
nullptr
(推荐,避免未初始化指针)。 - 可以在
Point
的构造函数成员初始化列表中,使用new
关键字来动态分配(dynamically allocate)内存并构造一个Integer
对象。 - 内存管理:由于
i5
指向的内存是在堆(heap)上动态分配的,因此必须在Point
类的析构函数中delete
该内存,以防止内存泄漏(memory leak)。
- 可以在类内初始化为
- 区别:
- 没有递归调用问题:这是最显著的区别。当
Point
的构造函数初始化i5
时,它调用的是Integer
类的构造函数(通过new Integer(50)
),而不是Point
自身的构造函数。因此,不会像上一讲中自身类型指针那样可能导致无限递归(如果设计不当)。 - 直接在初始化列表分配内存:因为没有递归问题,我们不需要像上一讲那样通过一个单独的成员函数来间接完成内存分配,可以直接在初始化列表中
new
。
- 没有递归调用问题:这是最显著的区别。当
非内联静态成员变量 (
static Integer i6;
):- 声明:使用
static
关键字。 - 初始化:必须在类定义外部(通常在
.cpp
文件中)进行定义和初始化。 - 特点:
static
成员变量是所有对象共享的,它不属于任何一个具体的Point
对象,而是属于Point
类本身。它的生命周期与程序的生命周期相同。
- 声明:使用
非内联静态常量成员变量 (
static const Integer i7;
):- 声明:使用
static const
关键字。 - 初始化:与
i6
类似,必须在类定义外部(通常在.cpp
文件中)进行定义和初始化。 - 特点:既是
static
(共享),又是const
(常量)。
- 声明:使用
总结区别:
当成员变量是“其他类型”时,最核心的不同在于:
- 完整类型可见性:因为被包含(
#include
),编译器在定义包含类时已经知道了成员变量的完整定义,不会有“不完整类型”导致的限制。 - 构造函数无递归问题:包含类的构造函数调用的是成员变量类型的构造函数,而非自身类的构造函数,因此不会陷入无限递归的陷阱。
2.5 const
正确性(Const Correctness)
在我们的代码中,i2
、i4
和i7
都被声明为const
。这意味着一旦它们被初始化,它们的值就不能再被修改。
- 尝试调用
p1.setI2Value(222)
、p1.setI4Value(444)
或p1.setI7Value(777)
都将导致编译错误。 - 这是因为
const
成员变量的setValue
方法(如果存在)不能被调用,因为它会尝试修改const
数据。即便setValue
方法不是const
成员函数,只要它尝试修改一个const
对象,就会出错。 - 我们在
Integer
类中getValue()
方法后面加上了const
,表示这个方法不会修改对象的状态,因此它可以被const Integer
对象调用。这是一个很好的const
正确性实践。
4. QA 闪存卡 (QA Flash Cards)
Q1: 在C++中,当一个类拥有“其他类型”的成员变量时,与拥有“自身类型”的成员变量相比,有哪些主要的不同点?
- A1: 主要的不同点是:
- 完整类型可见性:因为“其他类型”通常在类定义之前已被
#include
,所以不会有“不完整类型”的编译问题。 - 构造函数无递归调用问题:包含类的构造函数调用的是成员变量类型的构造函数,而不是自身类型的构造函数,因此不会出现无限递归的风险,可以直接在初始化列表中
new
分配内存。
- 完整类型可见性:因为“其他类型”通常在类定义之前已被
- A1: 主要的不同点是:
Q2: 为什么
const Integer i2;
必须在Point
类的构造函数的成员初始化列表中进行初始化,而不能在构造函数体内部赋值?- A2: 因为
i2
是一个const
(常量)成员变量。const
对象在创建时必须立即获得其最终值,不能在构造完成后再赋值。构造函数体内部的语句是在成员变量已经完成初始化之后才执行的,如果在这里赋值,就相当于对一个const
对象进行第二次赋值,这是不允许的。成员初始化列表则是在对象构造时,为成员变量分配内存并调用其构造函数(或直接初始化)的阶段。
- A2: 因为
Q3:
inline static Integer i3;
和static Integer i6;
在初始化方式上有什么不同?- A3:
inline static Integer i3;
:是一个内联静态成员变量。它可以(并且推荐)在类定义内部直接初始化,这得益于C++17引入的inline
变量特性,它解决了多重定义问题。static Integer i6;
:是一个普通的静态成员变量(非内联)。它必须在类定义外部(通常在.cpp
文件中)进行定义和初始化。
- A3:
Q4: 当一个类拥有
Integer* i5;
这样的指针成员变量时,在管理内存方面有什么需要特别注意的?- A4:
Integer* i5;
这种指针成员变量通常指向在堆(heap)上通过new
动态分配的内存。因此,为了防止内存泄漏(memory leak),你必须在类的析构函数中,使用delete i5;
来释放这块内存。并且为了防止悬空指针(dangling pointer),通常在delete
之后将i5
赋值为nullptr
。
- A4:
5. 常见误解或错误
- 忘记在析构函数中释放动态分配的内存:这是最常见的错误之一。如果像
Integer* i5;
这样的指针成员变量在构造函数中通过new
分配了内存,却忘记在析构函数中delete
,就会导致内存泄漏。程序运行时会不断占用内存,最终可能导致系统资源耗尽。 - 尝试修改
const
成员变量:初学者常常会忘记const
成员变量一旦初始化后就不能修改的特性,导致编译错误。记住:const
就是“常量”,其值固定不变。 - 混淆
static
和inline static
的初始化规则:在C++17之前,用户自定义类型的static
成员变量无论如何都不能在类内初始化。C++17引入inline
变量后,才允许inline static
成员变量在类内初始化。如果忘记使用inline
而尝试在类内初始化用户自定义类型的static
成员变量,会导致编译错误(除非它是静态常量整型)。 - 对
static
成员变量的误解:认为static
成员变量是每个对象都有自己的副本。实际上,static
成员变量是类级别的,所有对象共享同一个副本。 - 忘记对指针成员变量进行空检查:在访问指针成员变量指向的数据之前(例如
i5->getValue()
),应该始终检查指针是否为nullptr
,以避免空指针解引用(null pointer dereference)导致的程序崩溃。例如if (i5) i5->getValue();
。
6. 编程练习
现在,轮到你来动手实践了!请在下面的Point
类中,添加一个新的Integer
类型的成员变量i8
,并确保它是一个非内联的静态常量成员变量,其初始值为800
。你需要修改Point.h
和Point.cpp
文件。
你的任务:
- 在
Point.h
中声明i8
。 - 在
Point.cpp
中定义并初始化i8
。 - 在
Point.h
中添加一个getI8Value()
的const
成员函数来获取i8
的值。 - 在
main.cpp
中调用getI8Value()
并打印其值,验证你的修改。
Point.h
(你需要修改这里):
1 |
|
Point.cpp
(你需要修改这里):
1 |
|
main.cpp
(你需要修改这里):
1 |
|
请你完成上述代码修改,然后将你的解决方案提供给我。期待你的答案!