avatar

目录
286 - const 对象问题与指针引用访问

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
      #define DOG_H
      // ... 类的声明 ...
      #endif // DOG_H
      这几行代码叫做“包含守卫”。它们的作用是防止同一个头文件被意外地多次包含到同一个源文件中,那样会导致编译错误。#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 (公共)区域,意味着外部代码可以调用它们。
  • 源文件 (dog.cpp): 通常我们会把类成员函数的具体实现(定义)放在源文件中。

    • 在源文件中,你需要 #include "dog.h" 来引入类的声明。
    • 作用域解析运算符 (Scope Resolution Operator) ::: 当你在 .cpp 文件中定义成员函数时,你需要告诉编译器这个函数属于哪个类。例如:
      cpp
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 在 dog.cpp 中
      #include "dog.h"
      #include <iostream> // 为了 std::cout

      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) 来声明它:

cpp
1
const Dog dog1("小黄", "中华田园犬", 3);

这里的 const 告诉编译器:dog1 是一个常量对象。这意味着 dog1 的任何成员变量(m_name, m_breed, m_age)都不应该被改变。

3. const 对象带来的问题

  • 调用 Setter (设置器):
    如果你试图修改 dog1 的属性,比如:

    cpp
    1
    // dog1.set_name("小白"); // 尝试修改 const 对象的名字

    编译器会报错!这是符合我们预期的,因为 dog1const 的,我们不希望它被修改。set_name 函数的目的是修改 m_name,这显然违背了 const 对象的初衷。

  • 调用 Getter (获取器) 或其他看似只读的函数:
    现在麻烦来了。如果我们只是想打印狗狗的信息或者获取它的名字呢?

    cpp
    1
    2
    // dog1.print_info();      // 尝试打印 const 对象的信息
    // std::cout << dog1.get_name(); // 尝试获取 const 对象的名字

    你可能会惊讶地发现,这两行代码同样会导致编译错误

    为什么会这样呢?
    问题在于,编译器如何判断一个成员函数是否会修改对象?默认情况下,编译器会假设任何普通的成员函数都有可能修改对象的成员变量。它不会去分析函数体内部的具体代码(比如 print_info 只是 std::coutget_name 只是 return m_name;)。

    对于一个非 const 成员函数(比如我们目前定义的 print_infoget_name),编译器认为它们内部的 this 指针 (this pointer) 是一个指向非 const 对象的指针 (例如 Dog* this)。当你用一个 const Dog 对象去调用这样一个函数时,相当于要把一个 const Dog* (指向 const 对象的指针) 传递给一个期望 Dog* (指向非 const 对象的指针) 的 this 指针。这种转换会“丢弃 const 限定符”,是不安全的,因为函数内部可能会通过这个非 constthis 指针修改对象。因此,编译器为了安全起见,直接禁止了这种调用。

    简单来说,编译器看到 print_info()get_name() 没有被特殊标记为“保证不会修改对象”,所以它就认为这些函数可能会修改对象,因此不允许在 const 对象上调用它们。

4. 尝试通过指针或引用访问 const 对象

有些同学可能会想,能不能通过指针或引用来绕过这个问题呢?我们来试试:

  • 指向非 const 对象的指针 (Pointer to Non-const):

    cpp
    1
    2
    const Dog dog1("小黄", "中华田园犬", 3);
    // Dog* ptr_dog = &dog1; // 编译错误!

    这行代码会产生编译错误。为什么?因为 dog1const 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):

    cpp
    1
    2
    const Dog dog1("小黄", "中华田园犬", 3);
    // Dog& ref_dog = dog1; // 编译错误!

    同样,这也会编译错误。dog1const Dog,你不能把它绑定到一个非 const 的引用 Dog& 上。原因和指针类似,如果允许这样做,你就可以通过 ref_dog 修改 dog1。错误信息通常是 “binding reference of type ‘Dog&’ to ‘const Dog’ discards qualifiers”。

  • 指向 const 对象的指针 (Pointer to const):

    cpp
    1
    2
    const Dog dog1("小黄", "中华田园犬", 3);
    const Dog* ptr_const_dog = &dog1; // OK!

    这个是允许的。ptr_const_dog 是一个指向 const Dog 的指针,这意味着你不能通过 ptr_const_dog 来修改 dog1 所指向的对象。
    但是,问题依旧:

    cpp
    1
    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):

    cpp
    1
    2
    const Dog dog1("小黄", "中华田园犬", 3);
    const Dog& ref_const_dog = dog1; // OK!

    这也是允许的。ref_const_dog 是一个对 const Dog 的引用。
    同样的问题:

    cpp
    1
    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

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

#include <string_view> // 为了 std::string_view
#include <iostream> // 为了 std::cout (虽然通常定义在.cpp,但print_info内联的话可能放这里)

class Dog {
private:
std::string_view m_name;
std::string_view m_breed;
unsigned int m_age;
// 一个静态成员,用于演示析构函数(可选)
// static inline size_t dog_count{};

public:
// 构造函数
Dog(std::string_view name_param, std::string_view breed_param, unsigned int age_param);
Dog(); // 默认构造函数
~Dog(); // 析构函数

// Getters
std::string_view get_name();
std::string_view get_breed();
unsigned int get_age();

// Setters
void set_name(std::string_view name_param);
void set_breed(std::string_view breed_param);
void set_age(unsigned int age_param);

// 工具函数
void print_info();
};

#endif // DOG_H

dog.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
49
50
51
52
53
54
55
56
57
58
#include "dog.h" // 包含类声明
#include <iostream>

// 构造函数定义
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) {
// dog_count++;
std::cout << "狗狗 '" << m_name << "' 被参数化构造了。" << std::endl;
}

Dog::Dog() : Dog("无名氏", "未知品种", 0) { // 委托构造
std::cout << "狗狗 '" << m_name << "' 被默认构造了。" << std::endl;
}

// 析构函数定义
Dog::~Dog() {
// dog_count--;
std::cout << "狗狗 '" << m_name << "' 被析构了。" << std::endl;
}

// Getters 定义
std::string_view Dog::get_name() {
return m_name;
}

std::string_view Dog::get_breed() {
return m_breed;
}

unsigned int Dog::get_age() {
return m_age;
}

// Setters 定义
void Dog::set_name(std::string_view name_param) {
m_name = name_param;
std::cout << "狗狗的名字被设置为: " << m_name << std::endl;
}

void Dog::set_breed(std::string_view breed_param) {
m_breed = breed_param;
std::cout << "狗狗的品种被设置为: " << m_breed << std::endl;
}

void Dog::set_age(unsigned int age_param) {
m_age = age_param;
std::cout << "狗狗的年龄被设置为: " << m_age << std::endl;
}

// 工具函数定义
void Dog::print_info() {
// 为了演示 this 指针,我们打印一下对象的地址
// 在成员函数内部,this 是一个指向调用该函数的对象的指针
std::cout << "狗狗信息 (对象地址: " << this << "):" << std::endl;
std::cout << " 名字: " << m_name << std::endl; // 编译器会理解为 this->m_name
std::cout << " 品种: " << m_breed << std::endl; // this->m_breed
std::cout << " 年龄: " << m_age << std::endl; // this->m_age
}

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
49
50
51
52
53
54
55
56
#include <iostream>
#include "dog.h" // 包含 Dog 类

int main() {
std::cout << "--- 非 const 对象测试 ---" << std::endl;
Dog dog_non_const("旺财", "拉布拉多", 2);
dog_non_const.print_info();
dog_non_const.set_name("大黄");
std::cout << "旺财的新名字: " << dog_non_const.get_name() << std::endl;
dog_non_const.print_info();

std::cout << "\n--- const 对象测试 ---" << std::endl;
const Dog dog_const("常量狗", "金毛", 5);
std::cout << "创建 const 对象 dog_const" << std::endl;

// 1. 尝试调用 setter - 应该编译失败 (符合预期)
// dog_const.set_name("试图修改"); // 取消注释这行会导致编译错误

// 2. 尝试调用 getter - 应该编译失败 (这就是问题所在!)
// std::cout << "常量狗的名字: " << dog_const.get_name() << std::endl; // 取消注释这行会导致编译错误

// 3. 尝试调用 print_info - 应该编译失败 (这也是问题所在!)
// dog_const.print_info(); // 取消注释这行会导致编译错误

std::cout << "\n--- const 对象与指针/引用测试 ---" << std::endl;
// 4. 尝试用非 const 指针指向 const 对象 - 应该编译失败
// Dog* ptr_non_const_to_const = &dog_const; // 取消注释这行会导致编译错误
// std::cout << "ptr_non_const_to_const 创建失败 (预期中)" << std::endl;


// 5. 尝试用非 const 引用绑定 const 对象 - 应该编译失败
// Dog& ref_non_const_to_const = dog_const; // 取消注释这行会导致编译错误
// std::cout << "ref_non_const_to_const 创建失败 (预期中)" << std::endl;

// 6. 使用 const 指针指向 const 对象 - OK
const Dog* ptr_const_to_const = &dog_const;
std::cout << "ptr_const_to_const 创建成功: " << ptr_const_to_const << std::endl;
// ptr_const_to_const->set_name("再次尝试修改"); // 编译错误 (预期中)
// std::cout << "名字: " << ptr_const_to_const->get_name() << std::endl; // 编译错误 (问题依旧)
// ptr_const_to_const->print_info(); // 编译错误 (问题依旧)


// 7. 使用 const 引用绑定 const 对象 - OK
const Dog& ref_const_to_const = dog_const;
std::cout << "ref_const_to_const 绑定成功" << std::endl;
// ref_const_to_const.set_name("又双叒叕尝试修改"); // 编译错误 (预期中)
// std::cout << "名字: " << ref_const_to_const.get_name() << std::endl; // 编译错误 (问题依旧)
// ref_const_to_const.print_info(); // 编译错误 (问题依旧)


if (true) { // 只是为了让上面的注释行不影响程序正常结束的输出
std::cout << "\n(请取消上面main.cpp中标注为会导致编译错误的行来观察编译器的报错信息)" << std::endl;
}
std::cout << "\n程序演示结束。" << std::endl;
return 0;
}

编译和运行:
你需要一个 C++ 编译器(如 G++)。
bash
1
2
g++ main.cpp dog.cpp -o main_program -std=c++17
./main_program

当你取消 main.cpp 中那些会导致错误的行的注释时,你会看到编译器报错。仔细阅读编译器的错误信息,它们通常会提示类似 “passing ‘const Dog’ as ‘this’ argument discards qualifiers” 这样的信息。


QA 闪卡 (QA Flash Cards):

  1. Q: 什么是 const 对象 (const object)?
    A: 一个在初始化之后,其成员变量的值不能被修改的对象。它通过 const 关键字声明。

  2. Q: 为什么在一个 const 对象上调用一个看起来只是读取数据(如 get_name())的普通成员函数也会导致编译错误?
    A: 因为编译器默认情况下无法保证这个普通成员函数不会修改对象内部的数据。它看到的是函数签名 (function signature),而不是函数体。除非该函数被明确标记为 const 成员函数(我们将在后续课程学习),否则编译器会认为它可能会修改对象,因此禁止在 const 对象上调用。

  3. Q: 尝试将一个 const 对象的地址赋值给一个非 const 指针 (pointer to non-const) 会发生什么?例如 Dog* ptr = &const_dog_object;
    A: 会导致编译错误。因为这样做会丢失 const 限定,使得程序可能通过这个非 const 指针修改一个本应是 const 的对象,这是不安全的。

  4. Q: this 指针 (this pointer) 在 const 对象调用成员函数时扮演了什么角色?
    A: 在一个普通的(非 const)成员函数内部,this 指针的类型是 ClassName* (例如 Dog*)。当你在一个 const ClassName (例如 const Dog) 对象上尝试调用这个普通成员函数时,编译器需要将对象的地址(类型为 const ClassName*)传递给 this 指针。从 const ClassName*ClassName* 的转换会“丢弃 const 限定符”,这是一种危险操作,所以编译器会报错。


常见误解或新手易犯错误:

  1. 误解:“我的 getter 函数明明只是返回一个成员变量,它肯定不会修改对象,所以它应该能在 const 对象上调用。”

    • 纠正: 编译器并不去分析你函数体内部的逻辑。它只看函数的声明。如果一个成员函数没有在其声明的末尾标记 const (例如 std::string_view get_name() const;),编译器就认为它是一个可能会修改对象的函数,因此不允许在 const 对象上调用它。
  2. 错误:认为只要是指针或引用被声明为 const(例如 const Dog* ptr),那么它指向的对象的所有方法都能被安全调用。

    • 纠正: const Dog* ptr 意味着你不能通过 ptr 这个指针来修改它所指向的对象。但是,能否调用对象的某个方法,取决于该方法本身是不是 const 成员函数。如果方法不是 const 成员函数,那么即使通过 const Dog* 指针调用,也会失败。
  3. 混淆“指向常量的指针/引用”和“常量指针/引用本身”:

    • 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;) 来访问对象时遇到的问题。
  4. 试图用 const_cast 去掉 const 性质来调用非 const 成员函数:

    • 虽然 const_cast 可以用来移除变量的 const 限定,但如果原始对象确实是以 const 方式定义的,然后你通过 const_cast 修改了它,这是未定义行为 (undefined behavior),非常危险,应该避免。我们这里遇到的问题,正确的解决方式是修改类的设计(使用 const 成员函数),而不是试图“欺骗”编译器。

编码练习:

下面是 dog.hdog.cpp 的代码(和上面示例一样,你可以直接复制使用)。你的任务是补全 main.cpp 中的 // TODO: 部分,并观察编译器的行为。

dog.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
#ifndef DOG_H
#define DOG_H

#include <string_view>
#include <iostream>

class Dog {
private:
std::string_view m_name;
std::string_view m_breed;
unsigned int m_age;

public:
Dog(std::string_view name_param, std::string_view breed_param, unsigned int age_param);
Dog();
~Dog();

std::string_view get_name();
std::string_view get_breed();
unsigned int get_age();

void set_name(std::string_view name_param);
void set_breed(std::string_view breed_param);
void set_age(unsigned int age_param);

void print_info();
};

#endif // DOG_H

dog.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
49
#include "dog.h"
#include <iostream>

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 << "狗狗 '" << m_name << "' 被参数化构造了。" << std::endl;
}

Dog::Dog() : Dog("无名氏", "未知品种", 0) {
std::cout << "狗狗 '" << m_name << "' 被默认构造了。" << std::endl;
}

Dog::~Dog() {
std::cout << "狗狗 '" << m_name << "' 被析构了。" << std::endl;
}

std::string_view Dog::get_name() {
return m_name;
}

std::string_view Dog::get_breed() {
return m_breed;
}

unsigned int Dog::get_age() {
return m_age;
}

void Dog::set_name(std::string_view name_param) {
m_name = name_param;
std::cout << "狗狗的名字被设置为: " << m_name << std::endl;
}

void Dog::set_breed(std::string_view breed_param) {
m_breed = breed_param;
std::cout << "狗狗的品种被设置为: " << m_breed << std::endl;
}

void Dog::set_age(unsigned int age_param) {
m_age = age_param;
std::cout << "狗狗的年龄被设置为: " << m_age << std::endl;
}

void Dog::print_info() {
std::cout << "狗狗信息 (对象地址: " << this << "):" << std::endl;
std::cout << " 名字: " << m_name << std::endl;
std::cout << " 品种: " << m_breed << std::endl;
std::cout << " 年龄: " << m_age << std::endl;
}

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

int main() {
const Dog my_const_dog("豆豆", "柯基", 1);
std::cout << "创建了一个 const Dog 对象 my_const_dog。" << std::endl;

// TODO 1: 尝试调用 my_const_dog 的 print_info() 方法。
// 编译一下,看看会发生什么。然后注释掉这行代码。
// Your code for TODO 1 here:


// TODO 2: 尝试调用 my_const_dog 的 get_name() 方法,并打印返回的名字。
// 编译一下,看看会发生什么。然后注释掉这行代码。
// Your code for TODO 2 here:


// TODO 3: 尝试声明一个普通的 Dog 指针 (Dog*), 让它指向 my_const_dog。
// 例如: Dog* ptr_to_const = &my_const_dog;
// 编译一下,看看会发生什么。然后注释掉这行代码,并解释为什么会失败。
// Your code for TODO 3 here:
// 解释为什么失败:


std::cout << "\n练习完成。如果你注释掉了所有导致错误的代码,程序应该能正常编译和运行。" << std::endl;
return 0;
}

请你尝试完成 main.cpp 中的 // TODO: 部分,编译并观察结果。然后把你的 main.cpp 代码发给我,我会帮你看看并给出反馈!

我们这节课就先到这里。理解了 const 对象带来的这些问题,下一节课我们就能更好地学习如何解决它们了!加油!

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
main.cpp: In function ‘int main()’:
main.cpp:11:28: error: passing ‘const Dog’ as ‘this’ argument discards qualifiers [-fpermissive]
11 | my_const_dog.print_info();
| ~~~~~~~~~~~~~~~~~~~~~~~^~
In file included from main.cpp:2:
dog.h:26:10: note: in call to ‘void Dog::print_info()’
26 | void print_info();
| ^~~~~~~~~~
main.cpp:17:26: error: passing ‘const Dog’ as ‘this’ argument discards qualifiers [-fpermissive]
17 | my_const_dog.get_name();
| ~~~~~~~~~~~~~~~~~~~~~^~
dog.h:18:22: note: in call to ‘std::string_view Dog::get_name()’
18 | std::string_view get_name();
| ^~~~~~~~
main.cpp:25:25: error: invalid conversion from ‘const Dog*’ to ‘Dog*’ [-fpermissive]
25 | Dog* ptr_to_const = &my_const_dog;
| ^~~~~~~~~~~~~
| |
| const Dog*

评论