Const 对象问题与指针引用访问
🧑💻 User
In this lecture, we’re going to learn about cost objects.Suppose you have a class like this.The class is called Dog, and we have two sections in the class.We have a public section and we have a private section inside the class.We also have a few constructors and we have a destructor.But the important thing we want to look at in this lecture is the setters, which is this block here.And the getters that we have in this class now, we have this class declared.We don’t have the definitions in here because we could move them in a separate file to keep the headerclean.But suppose we created an object of this class and made it const.So we are declaring a dog object and it is made const here and when we make it const I want you to thinkabout what this means.It means that you can’t change any member variable in this object.Essentially, you can’t change the dog name, you can’t change the dog breed, you can’t change thedog age.If you try to do that, you’re going to get a compiler error.In this lecture, we’re going to see some problems that come with declaring your object cost.Then we’re going to see a solution to this in a few lectures ahead.So if we have our object declared here and we tried to set the name to another name, for example,through the call here, you would expect this to fail because this object is cast and we shouldn’t beable to modify any member variable of this object and say to name is going to try and modify the nameof the object.So this is going to give us a compiler error and we expect a compiler error.The problem is going to come if we try to call a function like print information here.And the print function is really trying to read from the dog object.The main purpose of this function is, for example, to read the dog name and print that out, to readthe breed name for the dog and print that out.It’s also going to read the age and print that out.It’s really not going to modify the object.But if we try to call this and compile our program, we’re going to get a compiler error.This is even going to be the case if we try to read using any of the getters we have in the function.This is going to be even the case if we try to read the dog name through any of the getters we havein our class.So, for example, if we say Dog one get name here, we’re going to get a compiler error.So why is this the case?The problem is we have no special way to differentiate between the getters and setters and tell thecompiler that the getters aren’t going to modify the data in the class and that the setters are onlygoing to be able to modify.We don’t really have a way to tell the compiler which methods are going to be modifying the objectsbecause we want our setters to modify the object and tell the compiler that our getters are innocent.They’re not going to try and modify the object in the eyes of the compiler.Our getters could also modify the object.So that’s the problem here, and that’s why we are getting a compiler error.If we try to call any of these methods on a const object.Okay, so we have a problem.And the problem again is that we can’t read data from our const objects, but we’re going to get theprotection from the compiler if we try to call the setter methods.But if we can’t read from our objects, our objects are really going to be useless.We are going to see a solution to this in a few lectures ahead.But first let’s address some thoughts some of you might have about this problem here.Some of you might be saying, What if we try and go through pointers or references and try to modifythe object or try to call our getters and see if things work?Well, I had to break it to you.That’s not going to work either.But let’s analyze this things case by case.Okay, so suppose we are trying to go through a pointer to Non-const and try to see if we can call oursetter or getter functions.So we have our object declared here and we have a pointer to dog.Notice that this is a pointer to Non-const.So this pointer could potentially be used to modify the object it is pointing to.And the moment we assign the address of our cast object to this pointer, we’re going to get a compilererror.The compiler is going to see that this pointer could be used to modify our const dog object.So it’s going to stop us from doing the assignment here.So this is going to fail.This is not going to work.Okay, so our first attempt failed, but what if we try to go through a non-const reference?Let’s see what happens.Again, we have our dog object.We are trying to set up a non-const reference to dog and we try to assign our dog object to this reference.Here again, the compiler is going to see that this reference could be used to modify our cast dog objectand it is going to store.Stop us from doing this assignment.So if we set up this assignment in our code, we’re going to get a compiler error.And the compiler error is going to say we can’t convert from a const.Doug Reference to Doug reference because that’s what we are trying to do here.Okay, so what if we try to go through pointer to cost?Let’s see what happens.We’re going to have our Doug object, which is const and we are going to set up a pointer to const.Doug That’s what we have here and we’re going to assign the address of our object to this pointer.So this is going to work, this is going to compile, but the moment we try to call a setter to ourDoug object, we’re going to get a compiler error.And this is expected because we have no business modifying a const object.But if we try to call an innocent getter function, which is not going to really modify our object,we’re also going to get a compiler error.And this is the same problem we saw before.But I want you to see this in the context of pointers and references here.So this is also not going to work.Okay, so what if we try to go through const references, We’re going to set up our Doug object andwe’re going to set up a const reference here and we’re going to assign our object to this reference.And the moment we try to go through our reference to modify the object, we’re going to get a compilererror, which is what we expect.But you see that if we even call our printing function here, we’re going to get a compiler error thatwe don’t really expect.So this is a problem with const objects and we need to find a solution to this.So we have seen that const objects are really problematic and if you set them up, you’re going to beprotected from changing your objects from the setters.But even your getters, which aren’t really modifying your object, the compiler is going to preventyou from calling them.And this is the problem I really want you to see and keep in your mind.Now that we have really described the problem, it is time we headed over to Visual Studio Code andshow you how this works in practice.Okay, here we are in our working folder.The current project is Const Objects.We’re going to grab our template files and put them in place.Let’s copy this and we’re going to put that in our folder and we are going to open this in Visual Studiocode like we always do.Let’s drag and drop here we have our folder open, let’s open the main CPP file and clean it up a littlebit.The first thing we’re going to do is to create our class that we’re going to be using to learn aboutthe problems you might come across if you use const objects in your C plus plus program.So we’re going to come to the left sidebar here and click on the plus icon, which says New file here.We’re going to create our header file, which is going to be called Doc Dot H, and we’re going to createanother file, which is going to be our CPP file.Let’s close the Open editors menu here because it’s confusing me so we’re going to also create a CPPfile.We’re going to say dot dot CPP and I am going to put in the code for the class because I have it prepared.So let’s paste that in here.I have the code in the header.I am going to go to the doc CPP file here, which is open and I’m going to put in the code that I haveprepared.Let’s go through this class a little bit because I don’t really want to waste your time typing this.We’re going to look at it and we’re going to see how it’s made.Then we’re going to try and use it.We have a class.It’s called Dog.It has a public section and a private section.Notice that our member variables now have been changed to string view because we want to avoid copieswhen we pass parameters to the constructors here.So that’s the reason why we used Stdstring view here.We also have a third member variable which is of unsigned and because the age can’t really be negative,so it makes sense to make this unsigned.And if you go on top here, we also have a few other functions.We have our getters get name, get breed, get age.They are going to be returning our member variables.We have a couple of setters.We have set name, set, breed, set age.They are going to be setting data to our member variables.We also have a utility function which is called principal and it’s going to print information aboutour class.So this is the definition of our class.But in the definition of the class, what we have are the prototypes for the functions.The definitions for the member functions are going to live in dog CPP, and if you go in dog CPP, you’regoing to have an include statement.Here, we’re going to have our constructor, which is going to use the parameters to initialize ourmember variables.We have our getters and all they will do is really return the member variables.One thing you should notice is that we have.The scope resolution operator in front of each function name here.So you see, Doug has a scope resolution operator in front.So this is going to tell the compiler that this function here belongs to the Doug class.That’s what the scope resolution operator here means.We have the same on our getters, so we have our scope resolution.Operator You can see that on get name, get breed and get age.You see that we also have that on our setters.The setters are going to be setting data in our member variables.So we’re going to set name, breed and age and our utility function is just going to print informationabout our class object.It’s just going to print.Doug It’s going to say the memory address where the object lives.It’s going to print the name, the breed and the age, and we’re going to see that on the console.So this is our class.If you don’t want to type it, you can get it from the resource section of this lecture and you canstart using it right away because this is going to take some time.But I do recommend spending some time to type this code.And because this is really how you learn.Another thing I haven’t explained I think is the include guard here.This is going to prevent people from including this twice.So the preprocessor is basically going to come in this file and say, if this thing here is not defined,I am going to define it and then I’m going to include whatever I have in this file here.If this thing is already defined, it’s not going to define this and it’s not going to include whatwe have here.So we have learnt about this before and we are reusing this here.So now that we have this class, let’s try to use it in main.We’re going to go to the main CPP file and we’re going to include our dog class.And we say that by saying quotes and inside the put in the class header file.And now that we have the class included, we can try and use it in the main function.So we’re going to say dog and say dog one and we’re going to pass in the information to use our constructor.We’re going to pass in the name, which is going to be fluffy like always.We’re going to pass in the breed, which is going to be Shefford, and we’re going to pass in the ageof the dog.Now that we have this, we can try and print information about this dog.So we’re going to say dog one and say print info.This is going to call our print function that we have in the class.So if we go back to the class definition we have in Dog dot H, we’re going to see that print info isgoing to print information.But another thing you can do in Visual Studio code is right click on the class and say, Go to definition.When you do that, it’s not going to do that.So this doesn’t work in Visual Studio code, but we can go and find the definition for the functionourselves.So print your form is going to print dog.It’s going to say the address of the object and it’s going to print information about our object.So we’re going to have name, breed and age printed out here, and we’re going to be respectively printingour member variables here.So let’s go back to Main and we’re going to try and build this program with GCC.The build is good.We can bring up a terminal and say Clear and run a rooster.Now you’re going to see that we have the information about our dog object here printed out.We say dog.We say the memory location for this object.And we’re going to say information about this dog, the name, the breed and the age here.All this information is available.We can use our setters.So, for example, we can go in here and say, dog one, set name and change this to me, for example.Why not?And it’s going to be Wire Fox.We’re going to set the name, nothing else.So we’re going to say Milu here and we’re going to print the information again.We’re going to say dog one printable.And if we do that, you see that the program is going to work.Visual Studio Code is not complaining.If we try to compile this program with GCC, you’re going to see that the world is good.And if we run, we’re going to see that our setting has applied to our object.The name is Fluffy, then the name is male.So the set name function here is doing its job.Now, what we really want to see in this lecture is how to work with const objects.So we’re going to change this dog.One object here to be const, let’s do that.And the moment we do that, you’re going to see problems pop up.We change this guy to const here by appending or prepending the const keyword in front.And if we try to build this program, we’re going to pass this through GCC, we’re going to get compilererrors and you see that we have a few squiggly lines here show up.The first problem that we really expect is.When we try to set the name.Because now, because this object is cast, we can’t really change any of its member variables.So if we try to change the name, the breed or the age, we’re going to get a compiler error.And here we have that compiler error.We are trying to change the name of a const object.So we can’t do that.We’re going to get a compiler error.So we’re going to see the message here which says that we have a problem at line nine in our code.If we go to line nine, you see that we are calling Printinfo, but we’re not going to look at thisjust yet and we’re going to see that we have a problem again on line 11, which is what we are interestedin looking at now.But it’s also going to say that we have a problem with our setname function call.Here we have on line 11 and it’s going to say that const Doug as this argument discards the const qualifier.So what’s the error here means is that we are trying to modify our const Doug object through the displayor because the setname function here is going to be using the this pointer to set the member variable.So what I haven’t really explained here is that, for example, if we use the setname function, let’slook at the definition.We say M name equals name param.What the compiler is really going to generate in the background is use the this keyword or the thispointer and it’s going to say this m name, then it’s going to say equals name param.This is what the compiler is going to do.And going through the this pointer, you can really modify this object.So this is what the compiler is saying here.It’s saying that this pointer that you have in your function call is going to discard the const qualifierwe have in the main function.In other words, you can go through the this pointer and modify a const object and we can’t allow youto do that.We’re going to give you a compiler error.We actually have the same problems with our calls to Printinfo because that’s going to do the same thingthrough our function calls.So let’s go back to the definition.You see that we are accessing.M name.M Breed.M age.We are actually not modifying anything in this function, but the compiler doesn’t know that.We know that we are not modifying our function.But what the compiler is going to look at is the signature of the function here.And if you look, there is really nothing telling the compiler that this function couldn’t modify ourobject here.That’s why the compiler is stopping us from doing this altogether.So the compiler is going to look at what we do here.It’s going to change our references to the member variables to this using the this pointer.So it’s going to say this M name, it’s going to say this M breed and it’s going to change this to thisM age.And it’s going to see that we could go through the this pointer and modify stuff about our object,and it’s going to stop us from using this function altogether.On const objects, you have seen that this function was working when the object wasn’t const, but nowit is cost and we could go through the printf function to modify our object.The compiler is going to give us a compiler error here.Hopefully you can understand this and I want you to know that this is the case.So let’s say that this is a compiler error.So this is the problem I want you to see with const objects.If you flag your object as const, you want to be able to modify the object through the setters, butno other function is going to work anymore because the compiler is going to think that you could gothrough those functions to modify your object.So some of you might be saying, What if we try and go through pointers and references which are eitherconst or not cost?Let’s try and see that we’re going to comment this out and we’re going to say direct access here, meaningthat we are going directly through the object name to try and do stuff.We go through the object name and call Printinfo.We’re going to get a compiler error.We go through the direct object, we try to set the name, we’re going to get a compiler error.This is what we mean here by direct access.Okay, so let’s try and go through a pointer to Non-const and see if we can access this function hereand do things with them.We’re going to go down here and set up a pointer.We’re going to say Doug Pointer.It’s a pointer to Non-const, so it’s just going to be a Doug Pointer.We’re going to say Doug TR and we’re going to initialize this with the address of our Doug object.Let’s say that Doug one and at the moment we do that we should get a problem because this is a non-constpointer.We could go through it to modify this object here.And if we even try to compile the code by now, we’re going to get a.Hi there.Okay, we have our compiler error.And what the compiler error is going to say is we are passing a const dog as this argument and thisis going to discard our qualifier.Why is that?We’re not doing that here?This was from before, but if we go down here, we’re going to see a compiler error that says invalidconversion from const dog pointer to dog pointer.So what we have to the right of the assignment here is really a const dog pointer.It is a pointer to cast because we are using a const object here.But what we have on the left of the assignment here is a non const pointer and the compiler is goingto notice that we could go through this pointer and modify this object here.So it’s going to stop us from setting up this pointer altogether.So this is not going to work either.And it is what I wanted you to see.Okay, so a non const pointer is not going to work, but what about a non const reference?Let’s do that and we’re going to set up a non-const reference to our object.We’re going to say dog reference and say dog ref and we’re going to assign dog one and we’re going toget the same behavior.This is a non const reference and we are trying to assign a const reference to a non const reference.If we try to compile the program, let’s do that.We’re going to get the same compiler error and you see binding reference of type dog reference to constdog reference.So this is going to not work.We’re going to get the same compiler error because we could go through this reference and modify thiscast object.The compiler is going to stop us from doing this altogether.Okay, this is not going to work.Let’s comment this out.Okay.Some of you might be saying, what if we change our pointer to cast so we can go down here and say thatlet’s say pointer to const and we’re going to set up a const dog pointer, which is going to be constDog Peter.We can call it like this and we’re going to assign the address of Dog one.And if we do this and compile, the compiler is going to trust this pointer.Now because it is a const pointer.We can’t go through this pointer to modify the object we are pointing to now.So the compiler is going to trust us with this pointer.If we build, you’re going to see that the code is going to work.So this is a good step forward.But if we try to do anything with this pointer, let’s do that.We’re going to say const dog pointer and we’re going to say set name.And we try to set the name to Milu, for example.This is not going to go well because we are trying to go through a const pointer to modify stuff.So this is going to give us a compiler error because we’re not allowed to do that.We can’t go through this pointer and modify our object.We’re going to see that the compiler error is saying const dog as this argument discards the qualifierof const.So we can’t say set name through this pointer because we are pointing to a const object and this isa pointer to cast.So this is expected because this is a pointer to const.We really expect not to be able to go through the pointer to modify the object, but even our gettersare not going to work.So if we say const dog pointer and say get name, for example, this is a getter.It’s really not modifying the object, but the compiler doesn’t know that.It doesn’t know that this function isn’t modifying our object because what really the compiler looksat is the signature of the function.It’s going to look at the gate name, let’s look at that.It’s going to look at this function and it’s going to say, There is nothing telling me that this functioncouldn’t modify our object.So I’m going to stop you from trying to even call that function in the main function here.If you try to call it, I’m going to throw a compiler error.That’s what the compiler is going to say.And if we try to world, let’s do that, we’re going to get a compiler error and it’s going to say thesame thing that this argument that is used in this getName function.And if we go in get name again, the compiler is going to see what we do here and it’s going to changethis to return this name.And it’s going to say you could go through this pointer here and modify this object.So I am going to stop you from doing that.That’s why we have this compiler error pointer to const isn’t also going to work.So what if we use the const reference?We’re going to go down and try to use this.We’re going to say const dog reference and I say const dog ref and we’re going to assign dog one whatwe always do.Now the compiler is going to trust this reference because it is cast.Let’s try and compile and really prove this to see the world is good.But again, if we try and go through this and do anything, we’re going to say.Cast a dark wrath.And I tried to set the name, for example, and we’re going to change the name to Milu.That’s what we want to do.And if we compile, we’re going to get a compiler error.Let’s do that.We’re going to use GCC.You see, we have a compiler error and the compiler error says this argument could be used to modifythe objects.So I want to let you use set name here.That’s the compiler error.And we expect that because we are going through a Const reference and we can’t really go through a constreference to modify the object we are referencing.So this is expected.But what is not expected is not to be able to even read stuff from our const object.So for example, if we say const dog reference and say get name, let’s use this here.We expect this call to work.But if we build, we’re going to really be surprised that it is not working again with the compilererror is going to say const dog.This argument discards the const qualifier because because we could go through the this pointer insidethis function definition and modify our const object here.So I am going to stop you from doing that and I am going to throw a compiler error.This is really what I wanted you to see in this lecture, that when your object is const, you’re goingto be locked away from using your object and it is practically useless because you can’t either readfrom it and you can’t write to it.We’re going to stop here in this lecture and in the next one I am going to show you that you’re goingto have the same problems if you try to use this object as an argument to functions.Then in the following lecture I am going to show you how we can solve this problem.So go ahead and finish up here and meet me in the next lecture.
🤖 Assistant
好的,C++ 新同学你好!非常欢迎来到 C++ 的世界。今天我们来聊一聊一个非常重要的概念:const
对象 (const objects)。这节课我们会发现,有时候想让一个对象保持不变,反而会遇到一些意想不到的麻烦。别担心,我会一步一步带你弄清楚!
C++ 中的 const
对象:美好的初衷与恼人的现实
简要总结:
当我们把一个对象声明为 const
(常量),意味着我们不希望它的任何成员变量被修改。这在很多情况下是非常有用的,比如确保数据在传递过程中不被意外更改。然而,C++ 编译器在处理 const
对象时,为了保证这种“不变性”,会严格限制我们能对这个对象做什么。令人困惑的是,有时候即使我们只是想读取对象的信息(比如调用一个“获取”方法),编译器也可能会报错。这节课我们就来深入探讨这个问题,并为后续学习解决方案打下基础。
详细解释:
我们先来看一个你可能会在编程中用到的例子。想象我们有一个 Dog
(狗) 类:
1. Dog
类的设计
这个 Dog
类有它自己的属性(比如名字、品种、年龄)和行为(比如设置名字、获取名字、打印信息)。
头文件 (
dog.h
): 通常我们会把类的声明放在头文件中。- 包含守卫 (Include Guards):这几行代码叫做“包含守卫”。它们的作用是防止同一个头文件被意外地多次包含到同一个源文件中,那样会导致编译错误。cpp
1
2
3
4
// ... 类的声明 ...#ifndef DOG_H
检查DOG_H
这个宏是否还没被定义。如果没定义,#define DOG_H
就会定义它,然后处理中间的代码。如果下次再遇到#include "dog.h"
,由于DOG_H
已经被定义了,#ifndef
到#endif
之间的代码就会被跳过。 - 成员变量 (Member Variables):
m_name
(名字): 我们使用了std::string_view
类型。std::string_view
(字符串视图) 是一个轻量级的对象,它“指向”一个已存在的字符串数据,但本身不拥有这个数据。这样做的好处是,在传递字符串时可以避免不必要的拷贝,提高效率。m_breed
(品种): 同样是std::string_view
。m_age
(年龄): 我们使用了unsigned int
(无符号整型),因为年龄通常不会是负数。
这些成员变量通常放在private
(私有)区域,意味着它们只能被类自己的成员函数访问,外部代码不能直接修改它们,这是一种封装 (encapsulation) 的体现。
- 成员函数 (Member Functions):
- 构造函数 (Constructors): 用于创建和初始化
Dog
对象。 - 析构函数 (Destructor): 在对象生命周期结束时执行清理工作(在这个例子中比较简单,可能为空)。
- Setters (设置器): 例如
set_name()
,set_breed()
,set_age()
。这些函数用于修改对象的成员变量。 - Getters (获取器): 例如
get_name()
,get_breed()
,get_age()
。这些函数用于读取对象的成员变量的值。 - 工具函数 (Utility Functions): 例如
print_info()
,用于打印狗狗的全部信息。
这些成员函数通常放在public
(公共)区域,意味着外部代码可以调用它们。
- 构造函数 (Constructors): 用于创建和初始化
- 包含守卫 (Include Guards):
源文件 (
dog.cpp
): 通常我们会把类成员函数的具体实现(定义)放在源文件中。- 在源文件中,你需要
#include "dog.h"
来引入类的声明。 - 作用域解析运算符 (Scope Resolution Operator)
::
: 当你在.cpp
文件中定义成员函数时,你需要告诉编译器这个函数属于哪个类。例如:这里的cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 在 dog.cpp 中
Dog::Dog(std::string_view name_param, std::string_view breed_param, unsigned int age_param)
: m_name(name_param), m_breed(breed_param), m_age(age_param) {
std::cout << "Dog 对象被创建了: " << m_name << std::endl;
}
std::string_view Dog::get_name() {
return m_name;
}
void Dog::set_name(std::string_view new_name) {
m_name = new_name;
}
// ... 其他函数的定义 ...Dog::
就是作用域解析运算符,它表明Dog(...)
构造函数、get_name()
函数等是Dog
类的一部分。
- 在源文件中,你需要
2. 什么是 const
对象?
现在,假设我们创建了一个 Dog
对象,并且我们希望这个对象一旦创建后就不能被修改。我们可以使用 const
关键字 (keyword) 来声明它:
1 | const Dog dog1("小黄", "中华田园犬", 3); |
这里的 const
告诉编译器:dog1
是一个常量对象。这意味着 dog1
的任何成员变量(m_name
, m_breed
, m_age
)都不应该被改变。
3. const
对象带来的问题
调用 Setter (设置器):
如果你试图修改dog1
的属性,比如:cpp1
// dog1.set_name("小白"); // 尝试修改 const 对象的名字
编译器会报错!这是符合我们预期的,因为
dog1
是const
的,我们不希望它被修改。set_name
函数的目的是修改m_name
,这显然违背了const
对象的初衷。调用 Getter (获取器) 或其他看似只读的函数:
现在麻烦来了。如果我们只是想打印狗狗的信息或者获取它的名字呢?cpp1
2// dog1.print_info(); // 尝试打印 const 对象的信息
// std::cout << dog1.get_name(); // 尝试获取 const 对象的名字你可能会惊讶地发现,这两行代码同样会导致编译错误!
为什么会这样呢?
问题在于,编译器如何判断一个成员函数是否会修改对象?默认情况下,编译器会假设任何普通的成员函数都有可能修改对象的成员变量。它不会去分析函数体内部的具体代码(比如print_info
只是std::cout
,get_name
只是return m_name;
)。对于一个非
const
成员函数(比如我们目前定义的print_info
和get_name
),编译器认为它们内部的this
指针 (this pointer) 是一个指向非const
对象的指针 (例如Dog* this
)。当你用一个const Dog
对象去调用这样一个函数时,相当于要把一个const Dog*
(指向const
对象的指针) 传递给一个期望Dog*
(指向非const
对象的指针) 的this
指针。这种转换会“丢弃const
限定符”,是不安全的,因为函数内部可能会通过这个非const
的this
指针修改对象。因此,编译器为了安全起见,直接禁止了这种调用。简单来说,编译器看到
print_info()
和get_name()
没有被特殊标记为“保证不会修改对象”,所以它就认为这些函数可能会修改对象,因此不允许在const
对象上调用它们。
4. 尝试通过指针或引用访问 const
对象
有些同学可能会想,能不能通过指针或引用来绕过这个问题呢?我们来试试:
指向非
const
对象的指针 (Pointer to Non-const):cpp1
2const Dog dog1("小黄", "中华田园犬", 3);
// Dog* ptr_dog = &dog1; // 编译错误!这行代码会产生编译错误。为什么?因为
dog1
是const Dog
类型,所以&dog1
(取地址) 得到的是一个const Dog*
(指向const Dog
的指针)。你不能把一个const Dog*
赋值给一个Dog*
(指向非const Dog
的指针),因为这样做会丢失const
限定,使得你可以通过ptr_dog
来修改dog1
的内容,这就破坏了dog1
的常量性。编译器会阻止这种危险行为,错误信息通常是 “invalid conversion from ‘const Dog’ to ‘Dog’”。非
const
对象的引用 (Reference to Non-const):cpp1
2const Dog dog1("小黄", "中华田园犬", 3);
// Dog& ref_dog = dog1; // 编译错误!同样,这也会编译错误。
dog1
是const Dog
,你不能把它绑定到一个非const
的引用Dog&
上。原因和指针类似,如果允许这样做,你就可以通过ref_dog
修改dog1
。错误信息通常是 “binding reference of type ‘Dog&’ to ‘const Dog’ discards qualifiers”。指向
const
对象的指针 (Pointer to const):cpp1
2const Dog dog1("小黄", "中华田园犬", 3);
const Dog* ptr_const_dog = &dog1; // OK!这个是允许的。
ptr_const_dog
是一个指向const Dog
的指针,这意味着你不能通过ptr_const_dog
来修改dog1
所指向的对象。
但是,问题依旧:cpp1
2
3// ptr_const_dog->set_name("小白"); // 编译错误! (符合预期,不能通过 const 指针修改)
// ptr_const_dog->print_info(); // 编译错误! (和直接调用 dog1.print_info() 一样的问题)
// std::cout << ptr_const_dog->get_name(); // 编译错误! (和直接调用 dog1.get_name() 一样的问题)即使我们用了
const
指针,调用那些普通的 getter 或print_info
仍然失败,因为这些函数本身没有被声明为const
成员函数。const
对象的引用 (Reference to const):cpp1
2const Dog dog1("小黄", "中华田园犬", 3);
const Dog& ref_const_dog = dog1; // OK!这也是允许的。
ref_const_dog
是一个对const Dog
的引用。
同样的问题:cpp1
2
3// ref_const_dog.set_name("小白"); // 编译错误! (符合预期)
// ref_const_dog.print_info(); // 编译错误!
// std::cout << ref_const_dog.get_name(); // 编译错误!
总结一下当前的困境:
一旦对象被声明为 const
,我们就无法调用那些没有被特殊标记为“安全”(即不会修改对象)的成员函数,即使这些函数实际上只是读取数据。这使得 const
对象在当前情况下几乎“毫无用处”,因为我们既不能写也不能(安全地)读。
别灰心!这个问题是有解决方案的(就是将成员函数声明为 const
成员函数),我们会在接下来的课程中学习。现在,重要的是理解为什么会产生这些编译错误。
代码示例:
我们会创建三个文件:dog.h
(类声明), dog.cpp
(类定义), 和 main.cpp
(主程序,使用 Dog
类)。
dog.h
1 |
|
dog.cpp
1 |
|
main.cpp
1 |
|
编译和运行:
你需要一个 C++ 编译器(如 G++)。
1 | g++ main.cpp dog.cpp -o main_program -std=c++17 |
当你取消
main.cpp
中那些会导致错误的行的注释时,你会看到编译器报错。仔细阅读编译器的错误信息,它们通常会提示类似 “passing ‘const Dog’ as ‘this’ argument discards qualifiers” 这样的信息。
QA 闪卡 (QA Flash Cards):
Q: 什么是
const
对象 (const object)?
A: 一个在初始化之后,其成员变量的值不能被修改的对象。它通过const
关键字声明。Q: 为什么在一个
const
对象上调用一个看起来只是读取数据(如get_name()
)的普通成员函数也会导致编译错误?
A: 因为编译器默认情况下无法保证这个普通成员函数不会修改对象内部的数据。它看到的是函数签名 (function signature),而不是函数体。除非该函数被明确标记为const
成员函数(我们将在后续课程学习),否则编译器会认为它可能会修改对象,因此禁止在const
对象上调用。Q: 尝试将一个
const
对象的地址赋值给一个非const
指针 (pointer to non-const) 会发生什么?例如Dog* ptr = &const_dog_object;
A: 会导致编译错误。因为这样做会丢失const
限定,使得程序可能通过这个非const
指针修改一个本应是const
的对象,这是不安全的。Q:
this
指针 (this pointer) 在const
对象调用成员函数时扮演了什么角色?
A: 在一个普通的(非const
)成员函数内部,this
指针的类型是ClassName*
(例如Dog*
)。当你在一个const ClassName
(例如const Dog
) 对象上尝试调用这个普通成员函数时,编译器需要将对象的地址(类型为const ClassName*
)传递给this
指针。从const ClassName*
到ClassName*
的转换会“丢弃const
限定符”,这是一种危险操作,所以编译器会报错。
常见误解或新手易犯错误:
误解:“我的
getter
函数明明只是返回一个成员变量,它肯定不会修改对象,所以它应该能在const
对象上调用。”- 纠正: 编译器并不去分析你函数体内部的逻辑。它只看函数的声明。如果一个成员函数没有在其声明的末尾标记
const
(例如std::string_view get_name() const;
),编译器就认为它是一个可能会修改对象的函数,因此不允许在const
对象上调用它。
- 纠正: 编译器并不去分析你函数体内部的逻辑。它只看函数的声明。如果一个成员函数没有在其声明的末尾标记
错误:认为只要是指针或引用被声明为
const
(例如const Dog* ptr
),那么它指向的对象的所有方法都能被安全调用。- 纠正:
const Dog* ptr
意味着你不能通过ptr
这个指针来修改它所指向的对象。但是,能否调用对象的某个方法,取决于该方法本身是不是const
成员函数。如果方法不是const
成员函数,那么即使通过const Dog*
指针调用,也会失败。
- 纠正:
混淆“指向常量的指针/引用”和“常量指针/引用本身”:
const Dog* p;
// p 是一个指针,它指向一个Dog
对象,这个Dog
对象通过p来看是常量(不能通过p修改)。p本身可以指向其他Dog
对象。Dog* const p;
// p 是一个常量指针,p一旦初始化后不能再指向其他对象,但它指向的Dog
对象可以通过p来修改(如果对象本身不是const)。- 本节课主要讨论的是对象本身是
const
(const Dog my_dog;
),或者通过指向常量的指针/引用 (const Dog* ptr;
或const Dog& ref;
) 来访问对象时遇到的问题。
试图用
const_cast
去掉const
性质来调用非const
成员函数:- 虽然
const_cast
可以用来移除变量的const
限定,但如果原始对象确实是以const
方式定义的,然后你通过const_cast
修改了它,这是未定义行为 (undefined behavior),非常危险,应该避免。我们这里遇到的问题,正确的解决方式是修改类的设计(使用const
成员函数),而不是试图“欺骗”编译器。
- 虽然
编码练习:
下面是 dog.h
和 dog.cpp
的代码(和上面示例一样,你可以直接复制使用)。你的任务是补全 main.cpp
中的 // TODO:
部分,并观察编译器的行为。
dog.h
1 |
|
dog.cpp
1 |
|
main.cpp
(练习部分)
1 |
|
请你尝试完成
main.cpp
中的 // TODO:
部分,编译并观察结果。然后把你的 main.cpp
代码发给我,我会帮你看看并给出反馈!
我们这节课就先到这里。理解了 const
对象带来的这些问题,下一节课我们就能更好地学习如何解决它们了!加油!
1 | main.cpp: In function ‘int main()’: |