avatar

目录
287 - const 对象作为函数参数的注意事项

Const 对象作为函数参数的注意事项

🧑‍💻 User

In this lecture.We’re going to see what’s going to happen if you try and use a const object as a function argument.We’re going to be using the same class we’ve used in the last lecture.No change at all.We’re going to have a few constructors and we’re going to have the setters and getters and we’re goingto have a principal function and we’re going to have our member variables which are going to be modeledas a string view variables, and the age is going to be a row int variable.So it’s not going to be quite like this, but it’s going to be exactly like what we did in the lastlecture.The main thing we want to see is what happens if you use a const object as a function argument and we’regoing to have a couple of options.We could either pass by value, we could either pass our parameters by Non-const reference, we couldpass by const reference, we could pass by pointer to non const, or we could even pass by pointer tocost.So I want you to pause the video and try to think about what could happen in each of these cases.If we pass a const dog object as a function of parameter, try to think about it and then I am goingto tell you the first case is going to be passing by value.And here we have a simple example of that.We have a function called function taking dog.It’s not going to return anything and it’s going to take a dog object by value inside.We’re going to try and set the name and print the information.And this is what the function here does.In the main function.You see, we have a const dog object and we’re going to pass it by value.The thing is, this is going to work because what we really have inside the function is not the originalvariable, it’s a copy.And we want to have any problem trying to modify a copy because the customers of the original objectis going to be preserved.We’re not going to be modifying it.We’re going to be modifying the copy.So passing by value is going to work fine.So what if you pass by Non-const reference?Here we have another simple example.We have a function taking dog ref.It’s not going to return anything, but it’s going to take a dog object by Non-const reference.Notice that we don’t have a const in front of the dog parameter here, so this is a non-const reference.The compiler is not going to allow this because this reference could be used to modify the originalobject.So this code here won’t even compile.And the reason is because our object is const, we’re not allowed to do any modification on this objectand we are trying to pass that to a function by reference which could allow us to potentially modifythis object.The compiler is going to prevent us from making a call like this.Okay, so this is not going to compile.If we pass by const reference, things are going to be improved a little bit.The function is going to be set up like this.It’s going to be called function taking a const dog ref.It’s not going to return anything, but it’s going to take the parameter by const reference.Here we have a const.This is going to protect from modification from this reference here.So if we didn’t have anything in the body of the function, the code here could compile.But the problem is going to come if we try and do any modification here.For example, if we call set name here, we’re going to be prevented from doing this from this constreference because it is a const reference.We can’t go through it to modify our object that we are referencing.But the call to Printinfo here is also not going to work.And this is the exact same thing we saw in the last lecture.The compiler doesn’t really know that the Printinfo function isn’t modifying our object, but we knowthat.But the compiler doesn’t know that that’s the problem here and the compiler is going to throw a compilererror If we try to call the printinfo function from our reference here.So we are back to the same problem in the last lecture.Our dog object that we passed by reference here is really going to be useless in this function bodyhere.We can’t read from it, we can’t write to it.We can’t basically do anything with it.So passing by const reference isn’t really going to work for you either.Okay.Passing by reference isn’t really working for us.What about passing by Pointer?Well, I hate to disappoint you, but this is also not going to work.Here we are passing by Non-const pointer and this pointer could be used to modify the original thingwe are pointing to.So the compiler is going to prevent the call like this to even compile because we are passing an addressto a const object and the pointer parameter here could be used to modify that object.So we’re going to get a compiler error.If we try to compile this function call, this is not going to work either.Okay.So you might.Be saying, What if we pass our parameter by pointer to const and here we have an example doing that.The function is not returning anything and it’s taking its parameter by const dog pointer.So the compiler is going to allow the signature here to compile.But if we try to do any modification inside, we’re going to get a compiler error on our setter here.And this is expected because this is a pointer to const, so we can’t go through this pointer to modifythe object we are pointing to.But the call to Printinfo here is also not going to work because the compiler thanks plentiful herecould be used to modify the object we are pointing to, but that object happens to be cast, so we reallyare not allowed to modify that and the compiler is going to do everything it can to protect the consumersof this object here.So hopefully you can see that even in function calls, const objects can be problematic to work withbecause the compiler is not going to allow us to read or write through the object.And the object is basically going to be useless in our eyes.So now that you have an idea about this problems, we’re going to head over to Visual Studio code andplay with this a little more.Okay.Here we are in our working folder.The current project is Const Objects as the function parameters.We’re going to grab the code from the last lecture because we’re going to be using that as a startingpoint.We can grab the class header and CPP file and the main function.Let’s copy that and use that in our current project here.So we’re going to do that and we don’t want to forget the VS code folder, which is going to bring usthe settings we can use in our project.So let’s go back to our project here and make sure we have this dot vs code folder.And once we have it set up, we’re going to open it in Visual Studio code by dragging and dropping here.This is going to open our project.You see, we have the main function.We have our dog class which lives in the header and the CPP file here.Now that we have this, we can actually close the left pane because we don’t need it anymore and we’regoing to remove everything and leave our cast dog object here.The first thing we’re going to do is put in a function that takes the parameter by value.So function taking dog is taking the parameter by value here and inside you’re going to see that we’regoing to set the name to our dog object and we’re going to say print info in the function body here.Let’s use this function, let’s say function taking parameter by value.And notice that we are passing our const object as a parameter.We’re going to say function taking dog and passing dog one.So what do you think will happen?This is a const object and we are passing it by value.Notice that the parameter is not const, so because we are passing by value, what we will have in thebody is going to be a copy of this dog object that we passed here.So modifying it is not going to cause any problem because the changes we do in the body of the functionare not going to affect our original const object.So the constantness of the original thing is going to be preserved.If we try to compile this program in GCC, we’re going to see that it’s going to work.We’re not going to have any problem.And if we bring up a terminal to run this program and run Rooster, try to think about what we’re goingto get.We have a const object here and I actually think we should also print eval before we call the function,because this is going to be better.We’re going to say dog one and say principal and we’re going to be able to see that the memory addresses,oh, we can’t call the principal function because it could potentially be modifying our const object.This is really bad.So let’s not do that.We can run the program as is now, So we’re going to run Rooster and we’re going to see the memory addressinside to be ending in D50.We’re going to see that the name has been changed from the inside of the function and we’re going toprint the information after we apply the change to really prove that this is a copy.We could try and print the address ourselves from the outside of this function here so we could saystdcout address of object, and we’re going to put that out.We’re going to say address of dog one.And if we’re bold, we’re going to see that let’s world with GCC.Okay, so let’s clear and run Rooster.We’re going to see that the address on the outside ends in DE0 and the address on the inside ends inD50.So there are two different objects because they live at two different memory locations.And the main message here is if you pass a const dog object as a function parameter.To a function that takes its parameter by value.The code is going to compile and you are going to be allowed to do anything in the body of the functionbecause the function will be basically working on a copy.And the changes you do inside the function body are not going to affect the original object.So this is what I want you to see here.So this is going to work.Okay.So let’s comment this call out and we are going to go on top and put in a function that takes the parameterby reference.We’re going to put this in and let’s say reference properly.Reference.Okay.So this is a function we’re going to set up to do stuff with that and we will try to use it with ourconst object here.So let’s do that.It’s going to be called function taking the ref, let’s call it.We’re going to go down here and do that and we’re going to try and pass in our dog object here.The moment we do that, we’re going to get a squiggly line and this is going to say qualifiers droppedin binding reference of type dog reference to initializer of type const dog.So what this really means is if the compiler allowed this to compile, this is not a const reference,so this is a non const reference.So it could potentially be used to modify the parameter that we pass to it and the compiler is goingto prevent us from doing this because we don’t want to modify our const object here.That’s the reason.So this is not going to compile.We’re going to get a compiler error, so we’re going to build this and pass this through GCC to reallylet you see this.And we’re going to get the same error we have seen before.We’re going to try and bind the reference of dog reference type to cast dog.And this is not going to work.Again, from the context of this function, call our const dog object here is really not working.You can’t use it in this function here.Okay, so if we try and pass our parameter by const reference, so this function here is going to seemto work.Let’s go down and save that here.We’re going to copy the name of this function because I don’t want to type all this here.We’re going to put it in place and we’re going to pass in Dog one.And if we issue a call like this and we’re not doing anything in the body of the function, if we tryto compile, you’re going to see that code is going to compile fine because the parameter is const reference.So this const reference is really going to protect the constants of the object here because we can’tgo through it and modify the object.But if we uncomment what we have inside here, we’re going to get a compiler error which is expectedon the first line here.But because we can’t really go through this const reference to modify the object.But the problem is the second line here is also not going to compile because the compiler doesn’t reallyknow that the printinfo function here isn’t modifying our object.So it’s going to throw an error here.Let’s do that.We’re going to build this with GCC and show you these compiler errors.You’re going to see that we have a problem with our setname function, but we also have a problem withour Printf function because they all use the this pointer and the this pointer could be used to modifythe object.So the compiler is not going to allow us to use a function like this.And again, our const object is really useless in the body of this function.We can’t read from it, we can’t write data into it, we can’t really do anything with it.So it is useless in this context here.So this is the problem I want you to be aware of.Okay, so we have seen that references aren’t really going to work.What about pointers?Let’s pass a parameter which is a pointer to Non-const and we can set up a function like this, whichis going to take a dog pointer parameter.And if we go down and try to call this function, you probably know what is going to happen.The compiler is not going to allow us to even call with the address of our dog object here.The moment we do this, we’re going to get a squiggly line and Visual Studio code is going to say theerror here is not clear.Let’s pass this through the compiler and see the compiler error.And it is basically going to be the same thing we have seen before.It’s going to say invalid conversion from const dog pointer to dog pointer.So what we have in our function parameter here or as a function argument is a const dog pointer becauseit is pointing to a const object.But our function here is taking its parameter clearly as a non const and this pointer could potentiallymodify our object.So the compiler is not going to allow this call here to compile.We can’t use this.Okay, so this is not going to work.And if you try to pass your.By pointer to const.We’re also going to have problems.So let’s go down and put in our function here.It’s going to be called function taking pointer to const the dog and the parameter is going to be takenby pointer to const here.If we go down and call our function, let’s comment out what we have in the body.Show you that this is going to actually compile.So we’re going to call the function here and we don’t want the entire thing.We just want the name of the function.Sorry for this.So let’s take out the void thing here.We just want to call our function and we’re going to give it the address of our dog object.So we’re going to say dog one.If we try to compile this code and we’re not doing anything in the body, the code is going to compileand try to think about why.Why do we have a problem here?Well, finished with errors and which problem do we have here?We have a const dog reference and we are trying to go through it to modify stuff here.So this is going to give us a compiler error.This function here wouldn’t compile.This is the problem here and we’re going to build again.And now you see that we also have another compiler error.So what is the problem here?We have an expected unqualified ID for return and we’re going to see that we have the problem at lineLet’s go there.A curly brace here from the function that we had.That’s the problem here.So what we’re going to see is that this function call here is going to compile.Let’s do that.We’re going to pass this through.JK Now the bolt is good, but if we go into the function body and try to do anything, for example,we try to say to the name, This is not going to work because we can’t go through a pointer to constand modify the object we are pointing to.So this is not going to work.But our printf function is also not going to work because the compiler thinks we could go through thisfunction to modify our object through the this pointer because that’s going to be a pointer to the currentobject.And if we try to compile, we’re going to get a weird error, which is basically going to say that wecould go through the this pointer to modify our object.It’s going to say that this pointer could be used to modify our object in set name and it’s going tosay the same thing in the principal here.Plentiful.We have a problem because we’re not using pointer access notation.So let’s fix this a little bit, but we’re going to build again and we’re going to get the same error.Basically, we’re going to say that this pointer could be used to modify our object from the printingfunction.This is what the compiler is saying here.And you see we have the same problem in set name that this pointer could be used to modify our object.So what I really wanted you to see in this lecture is that you have to be careful If you want to useconst objects of your classes, the object is almost going to be useless because the compiler is goingto thank your getters have potential to modify the object and your setters have the potential to modifythe object and the compiler is going to really stop you from using these guys and your objects are reallygoing to be useless.This is really bad and we’re going to see how we can solve this problem starting in the next lecture.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,没问题!很高兴能帮助你学习 C++。我们会循序渐进,确保你理解每一个概念。

C++ 教程:const (常量) 对象作为函数参数

摘要: 本讲座将探讨将 const (常量) object (对象) 作为函数参数传递时的不同情况,包括pass by value (按值传递)、按 reference (引用) 传递(const 和非 const)以及按 pointer (指针) 传递(const 和非 const),并解释 compiler (编译器) 的行为和原因。我们将使用一个简单的 Dog class (类) 来进行演示。


2. 详细解释

当我们有一个被 const 修饰的对象时,意味着我们不希望这个对象的状态被修改。C++ 编译器会尽力保证这一点。现在,让我们看看当我们将这样一个 const 对象作为参数传递给函数时,会发生什么。

我们将基于以下简单的 Dog 类(假设它在之前的课程中已经定义):

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
// 假设 Dog 类的定义如下 (为了本讲座的目的简化)
#include <iostream>
#include <string>
#include <string_view> // 字符串视图

class Dog {
private:
std::string m_name;
int m_age;

public:
Dog(std::string_view name, int age) : m_name(name.data(), name.length()), m_age(age) { // 使用 string_view 初始化 string
std::cout << "狗狗 \"" << m_name << "\" 被创建了。\n";
}

// 拷贝构造函数,方便观察是否发生拷贝
Dog(const Dog& other) : m_name(other.m_name), m_age(other.m_age) {
std::cout << "狗狗 \"" << m_name << "\" 被拷贝创建了。\n";
}

void set_name(std::string_view name) {
m_name = name.data(); // 修改成员变量
std::cout << "[" << (void*)this << "] 狗狗的名字被设置为: " << m_name << std::endl;
}

// 注意:这个成员函数目前不是 const 成员函数
void print_info() {
std::cout << "狗狗信息:名字=" << m_name << ", 年龄=" << m_age
<< " (对象地址: " << (void*)this << ")" << std::endl;
}
};

现在,让我们来分析不同的参数传递方式:

2.1 按值传递 (Pass by Value)

  • 机制:当按值传递参数时,函数会接收到原始对象的一个 copy (拷贝)。函数内部对这个拷贝的任何修改都不会影响到原始对象。
  • 对于 const 对象

    • 你可以将一个 const 对象按值传递给一个接受非 const 参数的函数。
    • 因为函数操作的是对象的副本,所以原始 const 对象的常量性得到了保证,它本身不会被修改。
    • 函数内部可以自由修改这个副本。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 函数定义
    void function_pass_by_value(Dog dog_param) {
    std::cout << "进入 function_pass_by_value...\n";
    dog_param.set_name("旺财 (副本)"); // 修改副本是允许的
    dog_param.print_info();
    std::cout << "离开 function_pass_by_value...\n";
    }

    // 调用
    // const Dog dog1("小白", 2);
    // function_pass_by_value(dog1); // 这是允许的

    在这个例子中,dog1const 的。当它传递给 function_pass_by_value 时,会调用 Dog 类的拷贝构造函数创建一个新的 Dog 对象 dog_param。函数内部对 dog_param 的修改(例如调用 set_name)只影响这个副本,不影响 dog1

2.2 按非 const 引用传递 (Pass by Non-const Reference)

  • 机制:引用是对象的别名。传递非 const 引用意味着函数可以通过这个引用修改原始对象。
  • 对于 const 对象

    • 编译器会禁止这种行为。你不能将一个 const 对象传递给一个接受非 const 引用的函数。
    • 原因很直接:如果允许这样做,函数内部就可能通过这个非 const 引用修改 const 对象,这就违背了 const 的承诺。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 函数定义
    void function_pass_by_non_const_ref(Dog& dog_ref_param) { //注意是 Dog&
    std::cout << "进入 function_pass_by_non_const_ref...\n";
    // dog_ref_param.set_name("小黑 (通过引用)"); // 如果能调用,会修改原始对象
    // dog_ref_param.print_info();
    std::cout << "离开 function_pass_by_non_const_ref...\n";
    }

    // 调用
    // const Dog dog1("小白", 2);
    // function_pass_by_non_const_ref(dog1); // 编译器错误!

    编译器会报错,类似于:”binding ‘const Dog’ to reference of type ‘Dog&’ discards qualifiers” (将 const Dog 类型的对象绑定到 Dog& 类型的引用会丢弃 const 限定符)。

2.3 按 const 引用传递 (Pass by Const Reference)

  • 机制const 引用也是对象的别名,但它承诺不会通过这个引用来修改对象。这使得它可以安全地引用 const 对象和非 const 对象。
  • 对于 const 对象

    • 这是允许的。将 const 对象传递给接受 const 引用的函数是安全的,因为函数保证不会修改它。
    • 但有一个陷阱:即使你通过 const 引用传递了对象,如果尝试调用该对象上的非 const member function (成员函数),编译器仍然会报错。
    • 这是因为非 const 成员函数理论上可以修改对象的状态(即使它实际上没有修改,比如我们的 print_info)。编译器只看函数的声明,如果一个成员函数没有被声明为 const,编译器就假设它会修改对象。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 函数定义
    void function_pass_by_const_ref(const Dog& dog_const_ref_param) { // 注意是 const Dog&
    std::cout << "进入 function_pass_by_const_ref...\n";
    // dog_const_ref_param.set_name("小黄 (通过const引用)"); // 错误!不能通过 const 引用调用修改函数
    // dog_const_ref_param.print_info(); // 错误!因为 print_info() 不是 const 成员函数
    std::cout << "离开 function_pass_by_const_ref...\n";
    }

    // 调用
    // const Dog dog1("小白", 2);
    // function_pass_by_const_ref(dog1); // 调用本身是允许的

    调用 set_name 会失败,因为 dog_const_ref_paramconst 引用,不能调用修改器。
    调用 print_info 也会失败(在当前的 Dog 类定义下),因为 print_info 没有被声明为 const 成员函数。编译器不知道 print_info 是否安全,所以它会阻止这个调用。这是上一讲中提到的问题:const 对象变得“几乎无用”。

2.4 按非 const 指针传递 (Pass by Non-const Pointer)

  • 机制:指针存储对象的内存地址。通过非 const 指针,函数可以修改指针指向的对象。
  • 对于 const 对象

    • 编译器会禁止这种行为。你不能将一个 const 对象的地址传递给一个接受非 const 指针参数的函数(例如 Dog*)。
    • 原因与非 const 引用类似:如果允许,函数内部就可以通过这个指针修改 const 对象。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 函数定义
    void function_pass_by_non_const_ptr(Dog* dog_ptr_param) { // 注意是 Dog*
    std::cout << "进入 function_pass_by_non_const_ptr...\n";
    // if (dog_ptr_param) {
    // dog_ptr_param->set_name("小灰 (通过指针)");
    // dog_ptr_param->print_info();
    // }
    std::cout << "离开 function_pass_by_non_const_ptr...\n";
    }

    // 调用
    // const Dog dog1("小白", 2);
    // function_pass_by_non_const_ptr(&dog1); // 编译器错误!

    编译器会报错,类似于:”invalid conversion from ‘const Dog‘ to ‘Dog‘“ (从 const Dog*Dog* 的转换无效)。

2.5 按 const 指针传递 (Pass by Const Pointer / Pointer to Const)

  • 机制:这里指的是指向常量的指针 (const Dog*Dog const*)。这意味着你不能通过这个指针来修改它所指向的对象。指针本身可以被修改以指向另一个对象(除非指针本身也是 const,如 const Dog* const)。
  • 对于 const 对象

    • 这是允许的。你可以将 const 对象的地址传递给一个接受指向常量的指针的函数。
    • 同样的陷阱:与 const 引用类似,即使你通过指向常量的指针传递了对象,如果尝试调用该对象上的非 const 成员函数,编译器仍然会报错。
    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 函数定义
    void function_pass_by_const_ptr(const Dog* dog_const_ptr_param) { // 注意是 const Dog*
    std::cout << "进入 function_pass_by_const_ptr...\n";
    // if (dog_const_ptr_param) {
    // dog_const_ptr_param->set_name("小紫 (通过const指针)"); // 错误!不能通过 const 指针调用修改函数
    // dog_const_ptr_param->print_info(); // 错误!因为 print_info() 不是 const 成员函数
    // }
    std::cout << "离开 function_pass_by_const_ptr...\n";
    }

    // 调用
    // const Dog dog1("小白", 2);
    // function_pass_by_const_ptr(&dog1); // 调用本身是允许的

    这里的行为和 const 引用非常相似。调用 set_name 会失败。调用 print_info 也会失败,因为它不是 const 成员函数。

总结一下关键点:

  1. 按值传递:总是安全的,因为操作的是副本。const 对象的原始值不会改变。
  2. const 引用/指针:不能用于传递 const 对象,因为它们暗示了修改原始对象的可能性,这会破坏 const 对象的常量性。编译器会阻止你。
  3. const 引用/指针:可以用于传递 const 对象。它们承诺不通过引用或指针修改对象。但是,这引出了一个重要问题:即使你有一个 const 引用或指向 const 对象的指针,你也只能调用该对象上被声明为 const 的成员函数。如果一个成员函数(比如我们的 print_info)没有被标记为 const,编译器就认为它可能会修改对象,因此不允许在 const 上下文中调用它。这使得 const 对象在很多情况下看起来“功能受限”或“无用”,除非类设计者正确地使用了 const 关键字来标记那些真正不会修改对象状态的成员函数。

这个问题(如何让 print_info 这样的函数能在 const 对象上被调用)的解决方案,通常是在成员函数声明的末尾加上 const 关键字,我们会在后续的课程中详细探讨。


3. 代码示例

下面是一个完整的代码示例,演示了上述所有情况。为了能编译通过并观察行为,我会注释掉那些会导致编译错误的代码行,并建议你取消注释来亲自看看编译器的错误信息。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <iostream>
#include <string>
#include <string_view> // C++17, for std::string_view

// --- Dog Class Definition ---
class Dog {
private:
std::string m_name;
int m_age;

public:
Dog(std::string_view name, int age) : m_name(name.data(), name.length()), m_age(age) {
std::cout << "Dog \"" << m_name << "\" constructed. Address: " << (void*)this << "\n";
}

// Copy constructor
Dog(const Dog& other) : m_name(other.m_name), m_age(other.m_age) {
std::cout << "Dog \"" << m_name << "\" COPIED. New address: " << (void*)this
<< " from old address: " << (void*)&other << "\n";
}

~Dog() {
std::cout << "Dog \"" << m_name << "\" destructed. Address: " << (void*)this << "\n";
}

void set_name(std::string_view name) {
std::cout << "Attempting to set name for dog at " << (void*)this << "\n";
m_name = name.data();
std::cout << "Dog at " << (void*)this << " name set to: " << m_name << std::endl;
}

// IMPORTANT: print_info is NOT a const member function yet
void print_info() /* no const here */ {
std::cout << "Dog Info (object @ " << (void*)this << "): Name=" << m_name << ", Age=" << m_age << std::endl;
}

// Let's add a const version of a getter for demonstration later if needed
// const std::string& get_name() const { return m_name; }
};

// --- Function Definitions ---

// 1. Pass by value
void function_pass_by_value(Dog dog_param) { // A copy is made
std::cout << "\n--- Inside function_pass_by_value ---\n";
std::cout << "Address of dog_param (copy): " << (void*)&dog_param << std::endl;
dog_param.set_name("ValueDog"); // Modifies the copy
dog_param.print_info(); // Calls print_info on the copy
std::cout << "--- Exiting function_pass_by_value ---\n";
}

// 2. Pass by non-const reference
void function_pass_by_non_const_ref(Dog& dog_ref_param) {
std::cout << "\n--- Inside function_pass_by_non_const_ref ---\n";
std::cout << "Address of object via dog_ref_param: " << (void*)&dog_ref_param << std::endl;
dog_ref_param.set_name("NonConstRefDog");
dog_ref_param.print_info();
std::cout << "--- Exiting function_pass_by_non_const_ref ---\n";
}

// 3. Pass by const reference
void function_pass_by_const_ref(const Dog& dog_const_ref_param) {
std::cout << "\n--- Inside function_pass_by_const_ref ---\n";
std::cout << "Address of object via dog_const_ref_param: " << (void*)&dog_const_ref_param << std::endl;
// dog_const_ref_param.set_name("ConstRefDog"); // COMPILE ERROR: set_name is not const and modifies
// dog_const_ref_param.print_info(); // COMPILE ERROR: print_info is not a const member function
if (true) { // Placeholder to avoid unused parameter warning if lines above are commented
std::cout << "Cannot call non-const member functions like set_name or print_info (as it is currently defined) on a const reference.\n";
}
std::cout << "--- Exiting function_pass_by_const_ref ---\n";
}

// 4. Pass by non-const pointer
void function_pass_by_non_const_ptr(Dog* dog_ptr_param) {
std::cout << "\n--- Inside function_pass_by_non_const_ptr ---\n";
if (dog_ptr_param) {
std::cout << "Address of object via dog_ptr_param: " << (void*)dog_ptr_param << std::endl;
dog_ptr_param->set_name("NonConstPtrDog");
dog_ptr_param->print_info();
}
std::cout << "--- Exiting function_pass_by_non_const_ptr ---\n";
}

// 5. Pass by const pointer (pointer to const)
void function_pass_by_const_ptr(const Dog* dog_const_ptr_param) {
std::cout << "\n--- Inside function_pass_by_const_ptr ---\n";
if (dog_const_ptr_param) {
std::cout << "Address of object via dog_const_ptr_param: " << (void*)dog_const_ptr_param << std::endl;
// dog_const_ptr_param->set_name("ConstPtrDog"); // COMPILE ERROR: set_name is not const and modifies
// dog_const_ptr_param->print_info(); // COMPILE ERROR: print_info is not a const member function
if (true) { // Placeholder
std::cout << "Cannot call non-const member functions like set_name or print_info (as it is currently defined) using a pointer-to-const.\n";
}
}
std::cout << "--- Exiting function_pass_by_const_ptr ---\n";
}


int main() {
const Dog const_dog("Buddy (const)", 3);
std::cout << "Address of const_dog in main: " << (void*)&const_dog << std::endl;
// const_dog.set_name("Tom"); // COMPILE ERROR: cannot call set_name on a const object
// const_dog.print_info(); // COMPILE ERROR: print_info is not a const member function, cannot be called on const_dog

std::cout << "\n======= Testing Pass by Value =======\n";
function_pass_by_value(const_dog); // OK: A copy is made. const_dog is not affected.
std::cout << "After function_pass_by_value, const_dog is: ";
// const_dog.print_info(); // Still can't call directly for the same reason

Dog non_const_dog("Lucy (non-const)", 1);
std::cout << "Address of non_const_dog in main: " << (void*)&non_const_dog << std::endl;


std::cout << "\n======= Testing Pass by Non-const Reference =======\n";
// function_pass_by_non_const_ref(const_dog); // COMPILE ERROR: cannot bind const Dog to Dog&
function_pass_by_non_const_ref(non_const_dog); // OK for non-const object
std::cout << "After function_pass_by_non_const_ref, non_const_dog is: ";
non_const_dog.print_info();

std::cout << "\n======= Testing Pass by Const Reference =======\n";
function_pass_by_const_ref(const_dog); // OK: const Dog can be passed to const Dog&
// (but inside the function, non-const methods cannot be called)
function_pass_by_const_ref(non_const_dog); // OK: non-const Dog can also be passed to const Dog&

std::cout << "\n======= Testing Pass by Non-const Pointer =======\n";
// function_pass_by_non_const_ptr(&const_dog); // COMPILE ERROR: cannot convert const Dog* to Dog*
function_pass_by_non_const_ptr(&non_const_dog); // OK for non-const object
std::cout << "After function_pass_by_non_const_ptr, non_const_dog is: ";
non_const_dog.print_info();

std::cout << "\n======= Testing Pass by Const Pointer =======\n";
function_pass_by_const_ptr(&const_dog); // OK: const Dog* can be passed to const Dog*
// (but inside the function, non-const methods cannot be called)
function_pass_by_const_ptr(&non_const_dog); // OK: Dog* can be converted to const Dog*

std::cout << "\nEnd of main.\n";
return 0;
}

当你编译并运行(或尝试编译)以上代码时,请特别注意:

  1. const_dog.print_info()main 函数中直接调用也会失败,因为 const_dogconst 对象,而 Dog::print_info() 不是 const 成员函数。
  2. function_pass_by_const_reffunction_pass_by_const_ptr 内部,对 print_info() 的调用(如果取消注释)也会失败,原因相同。

4. QA 闪卡 (QA Flash Cards)

  1. Q:const 对象按值传递给函数会发生什么?
    A: 会创建对象的副本。函数对副本的修改不影响原始 const 对象。编译通过。

  2. Q: 为什么不能将 const 对象传递给接受非 const 引用的函数?
    A: 因为非 const 引用意味着函数可能修改对象,这违反了原始对象的 const 属性。编译器会报错。

  3. Q:const 对象传递给接受 const 引用的函数,为什么调用非 const 成员函数(如 print_info,如果它未标记为 const)仍可能失败?
    A: 因为编译器只看函数签名。如果一个成员函数没有被声明为 const,编译器就假设它可能会修改对象的状态,因此不允许在 const 上下文(如通过 const 引用)中调用它。

  4. Q: const Dog* pDog* const p 有什么区别?
    A:

    • const Dog* p (或 Dog const* p):p 是一个指向 const Dog 对象的指针。你不能通过 p 来修改 Dog 对象(例如 p->set_name(...) 会报错)。但 p 本身可以指向另一个 Dog 对象(p = &another_dog; 是允许的,只要 another_dog 的类型兼容)。
    • Dog* const pp 是一个 const 指针,它指向一个 Dog 对象。这意味着 p 必须在初始化时指向一个地址,之后不能再指向其他地址(p = &another_dog; 会报错)。但是,如果它指向的对象不是 const 的,你可以通过 p 修改该对象(例如 p->set_name(...) 是允许的)。
  5. Q: const 对象在函数参数传递中显得“无用”的主要原因是什么?
    A: 如果类的成员函数(尤其是那些本意为只读的函数,如打印信息)没有被正确地声明为 const 成员函数,那么这些函数就不能在 const 对象上被调用,即使是通过 const 引用或 const 指针传递。


5. 常见误解或易犯错误

  1. 混淆 const T*T* const

    • const T* ptr:指向常量的指针 (pointer to const)。不能通过 ptr 修改 T 对象。ptr 本身可以改变,指向另一个 T
    • T* const ptr:常量指针 (const pointer)。ptr 初始化后不能改变,即不能指向另一个地址。但可以通过 ptr 修改它所指向的 T 对象(如果 T 本身不是 const)。
    • const T* const ptr:指向常量的常量指针。ptr 不能改变,也不能通过 ptr 修改 T 对象。
  2. 认为非 const 成员函数只要实际上不修改数据,就可以在 const 对象上调用:这是错误的。编译器只检查成员函数的声明。如果函数没有 const 关键字修饰,编译器就认为它会修改对象,从而禁止在 const 对象或通过 const 引用/指针调用。

  3. 忘记 this 指针的类型:在非 const 成员函数内部,this 指针的类型是 T* const (对于类 T)。在 const 成员函数内部,this 指针的类型是 const T* const。这就是为什么在 const 成员函数中不能修改成员变量(除非它们被声明为 mutable),也不能调用非 const 成员函数。

  4. 对按值传递的误解:新手有时可能会担心按值传递 const 对象时,const 性质会丢失或导致问题。实际上,按值传递会创建一个副本,原始 const 对象完全不受影响,所以这是安全的。函数内部的副本是不是 const 取决于函数参数的声明。

  5. 过度使用 const_cast:虽然 const_cast 可以移除对象的 const 限定,但这通常是危险的,应避免使用。如果原始对象确实是 const 的,通过 const_cast 去掉 const 后再尝试修改它,会导致未定义行为。


6. 编码练习

现在,这里有一个练习。下面的代码中,main 函数创建了一个 const Dog 对象,并尝试通过一个接受 const Dog& 的函数来调用它的 print_info() 方法。目前,这会导致编译错误。

你的任务是: 只修改 Dog 类的定义(特别是 print_info 函数的声明和定义),使得 main 函数中的 use_const_dog_ref 函数调用能够成功编译并正确打印狗狗的信息,不要修改 main 函数或 use_const_dog_ref 函数的任何代码,也不要移除 dog1const 限定符。

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
#include <iostream>
#include <string>
#include <string_view>

// --- Dog Class Definition ---
// TODO: 修改 Dog 类的 print_info 方法,使其可以在 const 对象上被调用
class Dog {
private:
std::string m_name;
int m_age;

public:
Dog(std::string_view name, int age) : m_name(name.data(), name.length()), m_age(age) {
std::cout << "Dog \"" << m_name << "\" constructed.\n";
}

// 这个函数需要被修改
void print_info() { // <- 思考这里需要加什么
std::cout << "Dog Info: Name=" << m_name << ", Age=" << m_age << std::endl;
}

void set_name(std::string_view name) {
m_name = name.data();
}
};

// --- 不要修改这个函数 ---
void use_const_dog_ref(const Dog& a_dog) {
std::cout << "Inside use_const_dog_ref:\n";
a_dog.print_info(); // <- 这里应该能够成功调用
}

// --- 不要修改 main 函数 ---
int main() {
const Dog dog1("Max (const)", 5);

// 我们希望下面的调用能够工作
use_const_dog_ref(dog1);

// 为了对比,一个非 const 对象
Dog dog2("Bella (non-const)", 2);
// dog2.print_info(); // 这个本来就能工作,但不是练习的重点

return 0;
}

请你思考一下,print_info 函数应该如何修改才能在 const Dog& a_dog 上被调用。完成后,将你修改后的 Dog 类定义回复给我。


🧑‍💻 User

回答之前我问一下, 如果按值传递, 那么如果原来的const 对象里如果包含const 函数, 那么传递后的Local对象那个函数还是Const函数么?


🤖 Assistant

这是一个非常好的问题!它触及了 const 正确性的核心概念,以及类成员函数的特性。

在你尝试练习之前,我先来解答你的疑问:

问题: 如果按值传递,那么如果原来的 const 对象里如果包含 const 成员函数 (const member function),那么传递后的 local object (局部对象) 的那个成员函数还是 const 成员函数么?

回答:

是的,那个成员函数依然是一个 const 成员函数。

让我详细解释一下:

  1. 成员函数的 const 属性是类定义的一部分
    当你在类中声明一个成员函数时,比如 void print_info() const;,这个 const 关键字意味着该函数承诺不会修改调用它的对象的任何成员变量(除非这些成员变量被声明为 mutable)。这个 const 属性是函数签名 (function signature) 的一部分,是这个函数在 Dog 类中的固有特性。它不会因为对象是如何创建的(是原始对象还是副本)或对象本身是否为 const 而改变其“const 成员函数”的身份。

  2. 按值传递创建副本
    当你将一个对象(无论是 const 还是非 const)按值传递给一个函数时,函数内部会得到这个对象的一个副本。

  3. 副本的 const 状态取决于函数参数的声明

    • 如果函数参数是 void my_func(Dog local_dog),那么 local_dog 这个副本是const 的。
    • 如果函数参数是 void my_func(const Dog local_dog)(虽然按值传递 const 副本不常见,但语法上是允许的),那么 local_dog 这个副本是 const 的。
  4. 在副本上调用成员函数

    • 成员函数本身的定义不变Dog 类中定义的 print_info() const 始终是一个 const 成员函数。Dog 类中定义的 set_name() (假设它不是 const 成员函数) 始终是一个非 const 成员函数。
    • 可调用性取决于副本的 const 状态
      • 如果局部副本 local_dogconst (例如参数是 Dog local_dog):
        • 你可以调用 local_dog.print_info(),因为 const 成员函数可以被非 const 对象调用。
        • 你也可以调用 local_dog.set_name("NewName"),因为非 const 成员函数可以被非 const 对象调用。
      • 如果局部副本 local_dogconst (例如参数是 const Dog local_dog):
        • 你可以调用 local_dog.print_info(),因为 const 成员函数可以被 const 对象调用。
        • 不能调用 local_dog.set_name("NewName"),因为非 const 成员函数不能被 const 对象调用。

总结一下你的问题:
原始的 const 对象拥有一些成员函数,其中一些被定义为 const 成员函数。当你按值传递这个 const 对象时,会创建一个副本。
对于这个副本:

  • 那些在类定义中被声明为 const 的成员函数(例如 print_info() const)对于这个副本来说仍然是 const 成员函数。它们的“身份”没有变。
  • 关键在于这个副本对象本身是 const 还是非 const。如果函数参数是 Dog d,那么副本 d 是非 const 的,你可以对它调用 const 成员函数,也可以调用非 const 成员函数。

所以,一个函数是不是 const 成员函数,是由它在类中的声明决定的,而不是由它被调用的对象实例是否为 const 或是否为副本决定的。对象实例的 const 状态决定了你能够在该实例上调用哪些类型的成员函数。

希望这个解释清楚了你的疑问!现在你可以更有信心地尝试那个练习了。


现在,回到我们的练习。

7. 评价你的解答并给出正确答案和解释

请你提供你对 Dog 类的修改方案,特别是 print_info 函数。我会根据你的方案给出评价。

如果你还没有完成,这里是提示:为了让 print_info() 可以在一个 const Dog& (常量狗的引用) 上被调用,你需要向编译器保证 print_info() 函数不会修改 Dog 对象的任何成员变量。

等你准备好后,请分享你的代码。


🧑‍💻 User

include

include

include

// —- Dog Class Definition —-
// TODO: 修改 Dog 类的 print_info 方法,使其可以在 const 对象上被调用
class Dog {
private:
std::string m_name;
int m_age;

public:
Dog(std::string_view name, int age) : m_name(name.data(), name.length()), m_age(age) {
std::cout << “Dog \”” << m_name << “\” constructed.\n”;
}

// 这个函数需要被修改
void print_info() const{ // <- 思考这里需要加什么
    std::cout << "Dog Info: Name=" << m_name << ", Age=" << m_age << std::endl;
}

void set_name(std::string_view name) {
    m_name = name.data();
}

};

// —- 不要修改这个函数 —-
void use_const_dog_ref(const Dog& a_dog) {
std::cout << “Inside use_const_dog_ref:\n”;
a_dog.print_info(); // <- 这里应该能够成功调用
}

// —- 不要修改 main 函数 —-
int main() {
const Dog dog1(“Max (const)”, 5);

// 我们希望下面的调用能够工作
use_const_dog_ref(dog1);

// 为了对比,一个非 const 对象
Dog dog2("Bella (non-const)", 2);
// dog2.print_info(); // 这个本来就能工作,但不是练习的重点

return 0;

}


🤖 Assistant

太棒了!你的修改完全正确!

7. 评价你的解答并给出正确答案和解释

你的解答评价:

  • 得分:10/10
  • 评价: 你准确地找到了问题的关键,并在 print_info 函数的声明和定义末尾添加了 const 关键字。这正是解决这个问题的正确方法。做得非常好!

正确答案和解释:

你的代码修改如下:

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
#include <iostream>
#include <string>
#include <string_view>

// --- Dog Class Definition ---
class Dog {
private:
std::string m_name;
int m_age;

public:
Dog(std::string_view name, int age) : m_name(name.data(), name.length()), m_age(age) {
std::cout << "Dog \"" << m_name << "\" constructed.\n";
}

// 正确的修改:在函数声明末尾添加 const
void print_info() const { // <--- 在这里添加了 const
// 在 const 成员函数内部,成员变量 m_name 和 m_age 被视为 const
// 因此,你不能在这里做类似 m_age++; 的操作(除非 m_age 是 mutable)
std::cout << "Dog Info: Name=" << m_name << ", Age=" << m_age << std::endl;
}

void set_name(std::string_view name) {
m_name = name.data();
}
};

// --- 不要修改这个函数 ---
void use_const_dog_ref(const Dog& a_dog) {
std::cout << "Inside use_const_dog_ref:\n";
a_dog.print_info(); // <- 现在这里可以成功调用了!
}

// --- 不要修改 main 函数 ---
int main() {
const Dog dog1("Max (const)", 5);

// 我们希望下面的调用能够工作
use_const_dog_ref(dog1); // 编译通过,运行正常

Dog dog2("Bella (non-const)", 2);
dog2.print_info(); // 非 const 对象也可以调用 const 成员函数

return 0;
}

为什么这样修改有效?

  1. const 成员函数 (Const Member Function):
    当你在一个成员函数的声明和定义后面加上 const 关键字时,你是在告诉编译器这个函数是一个“const 成员函数”。

  2. this 指针的影响:

    • const 成员函数内部,this 指针的类型是 Dog* const (一个指向 Dog 类型的常量指针,指针本身不能被修改指向其他对象,但指向的对象可以被修改)。
    • const 成员函数内部,this 指针的类型变为 const Dog* const (一个指向 const Dog 类型的常量指针,指针本身不能被修改,并且它指向的对象也被视为 const,因此不能通过 this 修改对象的成员变量)。
  3. const 对象的兼容性:

    • 当你有一个 const Dog dog1; 或者 const Dog& a_dog; 时,你只能调用那些不会修改对象状态的成员函数。
    • 编译器通过检查成员函数是否被声明为 const 来判断这一点。如果一个成员函数被声明为 const,编译器就认为调用它是安全的,因为它承诺不会改变对象的状态。
    • 如果 print_info() 没有 const 修饰,编译器会认为它可能会修改对象(因为 thisDog* 类型),因此不允许在 const Dog 对象上调用它。
    • 通过将 print_info() 声明为 void print_info() const,你明确告诉编译器:“这个函数不会修改对象的任何成员(除非它们是 mutable 的)。”因此,编译器允许在 const Dog 对象(如 dog1)或通过 const Dog& (常量引用)(如 a_dog)调用 print_info() const
  4. const 对象也可以调用 const 成员函数:
    值得注意的是,非 const 对象(如 dog2)也可以调用 const 成员函数。这是完全安全的,因为 const 成员函数只是承诺不修改对象,这对非 const 对象来说也是一个有效的操作。

通过这个修改,Dog 类变得更加 const 正确 (const-correct),使得 const 对象也能发挥其应有的“只读”作用,而不是变得“无用”。


8. 扩展问题与调查建议

现在你已经掌握了 const 对象和 const 成员函数的基础,这里有一些可以进一步探索和思考的方向:

  1. mutable 关键字:

    • 了解 mutable (可变的) 关键字的用途。在某些情况下,即使在一个 const 成员函数中,你可能也需要修改某个特定的成员变量(例如,用于缓存计算结果或调试的计数器,这些修改不影响对象的外部可见状态)。mutable 关键字允许你在 const 成员函数中修改被它修饰的成员变量。
    • 思考: 什么时候使用 mutable 是合适的?什么时候应该避免?
  2. const 重载 (Const Overloading):

    • 一个类可以拥有两个同名成员函数,它们仅在 const 限定符上有所不同。例如:
      cpp
      1
      2
      3
      4
      5
      class MyClass {
      public:
      ResourceType& get_resource(); // 非 const 版本
      const ResourceType& get_resource() const; // const 版本
      };
    • 当对象是 const 时,会调用 const 版本;当对象是非 const 时,会调用非 const 版本。
    • 思考: 这种重载在什么场景下有用?它如何帮助我们编写更灵活和安全的代码?(提示:考虑返回引用的情况)
  3. 逻辑 const vs 位 const (Logical Constness vs. Bitwise Constness):

    • const (Bitwise constness):指成员函数不修改对象的任何一个字节(除了 mutable 成员)。这是编译器通常强制执行的。
    • 逻辑 const (Logical constness):指成员函数不修改对象的“抽象状态”或“外部可见状态”,即使它可能修改了某些内部的、非关键的字节(例如,通过 mutable 成员或通过指针修改了其他非对象自身拥有的数据)。
    • 思考: const 成员函数保证的是哪一种 constmutable 在这里扮演什么角色?
  4. const 在迭代器 (iterators) 中的应用:

    • STL (标准模板库) 容器提供了不同类型的迭代器,例如 iteratorconst_iterator
    • 调查: const_iterator 有什么特点?它与 const 成员函数和 const 对象有什么关系?为什么需要 const_iterator
  5. 在你的项目中贯彻 const 正确性:

    • 尝试回顾你之前写的或看到的 C++ 代码,思考哪些地方可以应用 const 来提高代码的健壮性和可读性。养成在合适的地方使用 const 的习惯,包括:
      • 将不会被修改的变量声明为 const
      • 将不会修改所引用或所指向数据的指针/引用参数声明为 const T*const T&
      • 将不会修改对象状态的成员函数声明为 const

通过探索这些问题,你将对 C++ 中的 const 有更深入和全面的理解。继续努力,你做得很好!


评论