菜鸟教程:修饰符、静态非静态、构造函数、类相关的杂七杂八的知识
本文从菜鸟教程的 修饰符类型
C++的修饰符类型:
即允许在 char、int 和 double 数据类型前放置修饰符。
修饰符是用于改变变量类型的行为的关键字,它更能满足各种情境的需求。
-
short:表示变量的范围比 int 更小。short int 可以缩写为 short。
-
long:表示变量的范围比 int 更大。long int 可以缩写为 long。
-
long long:表示变量的范围比 long 更大。C++11 中新增的数据类型修饰符
把他文章里“mutable 实例”的代码发给豆包,获取到一系列知识点(以下是追问豆包知道的)
静态成员变量:
对于一个类来说,静态成员变量是所有该类的对象共享的变量。它不属于某个特定的对象,而是属于整个类。例如,定义一个Car
类:
class Car { public: static int totalCars; }; int Car::totalCars = 0;
这里的totalCars
是一个静态成员变量。不管创建多少个Car
类的对象,如Car car1; Car car2;
,它们共享这一个totalCars
变量。当car1
对totalCars
进行修改时,car2
看到的totalCars
也是修改后的结果。通常用于统计类对象的数量、记录某种全局的状态等。
静态成员函数:
静态成员函数是属于类本身的函数,不能访问非静态成员变量(因为没有this
指针指向某个具体对象)。它主要用于操作静态成员变量或者执行一些与类整体相关的操作。例如:
class Car { public: static int totalCars; static void incrementTotalCars() { totalCars++; } }; int Car::totalCars = 0;
可以通过Car::incrementTotalCars();
来调用这个静态成员函数,它直接操作静态成员变量totalCars
,不需要创建Car
类的对象来调用,它关注的是类整体的情况,而不是某个具体对象的情况。
再说非静态的,在面向对象编程(以 C++ 为例)中,类的成员(包括成员变量和成员函数)可以分为静态成员和非静态成员。静态的已经说了,也是我追问才懂的,而非静态的就很简单了,非静态成员是与类的对象实例相关联的成员,也就是说,每个类的对象都有自己独立的一份非静态成员。如,有一个Person
类:
class Person { public: int age; // 其他成员函数等 };
这里的age
就是一个非静态成员变量。当创建多个Person
类的对象时,如Person p1; Person p2;
,p1
和p2
都有自己独立的age
变量。可以分别对它们进行赋值,如p1.age = 20; p2.age = 30;
,它们在内存中占据不同的存储空间,相互独立,这种与具体对象实例绑定的成员变量就是非静态成员变量。
对象实例就像之前刷题用到的,C语言里的结构体,比如有个Node结构体,然后我Node node
,用Node
定义的变量node
就是该结构体类型的一个变量。面向对象里面类的对象实例也是这意思
关于访问:
在类中,有两种主要的成员访问方式。
- 对于对象实例(非静态成员),使用 “.” 运算符访问。访问成员的方式和C语言类似,如果成员变量是公共的,就可以像C语言一样使用 “.” 运算符访问。
- 对于类的静态成员(用
static
关键字修饰的成员),使用 “::”(作用域解析运算符)访问。例如,如果Example
类有一个静态成员函数static void static_func()
,可以通过Example::static_func()
来调用这个静态成员函数。
关于struct,这玩意是定义结构体的
struct 结构体名称 { 数据成员类型 数据成员1; 数据成员类型 数据成员2; // 可以继续添加更多的数据成员 };
关于typedef,
typedef
是 C 和 C++ 语言中的一个关键字,用于为已有的数据类型定义一个新的别名,它主要有以下作用和特点:typedef 原数据类型 新别名;
。typedef int MyInt;
,这里就把 int
类型定义了一个新的别名 MyInt
,那么 MyInt num = 10;
就等价于 int num = 10;
。class Example { public: int get_value() const { return value_; // const 关键字表示该成员函数不会修改对象中的数据成员 } void set_value(int value) const { value_ = value; // mutable 关键字允许在 const 成员函数中修改成员变量 } private: mutable int value_; };
整体功能概述
这段代码定义了一个名为Example
的类,类中包含了一个私有成员变量value_
,并且提供了获取该变量值的get_value
函数 和 设置该变量值的set_value
函数。
get_value
函数
- 函数定义:
int get_value() const { return value_; }
- 功能说明:它是一个常成员函数,通过
const
关键字修饰,表示该函数承诺不会修改类对象的任何非mutable
数据成员。在这个函数中,它简单地返回了私有成员变量value_
的值,外界可以通过调用这个函数获取到类中所保存的值。
set_value
函数
- 函数定义:
void set_value(int value) const { value_ = value; }
- 功能说明:这也是一个常成员函数,按常规
const
成员函数不能修改对象的数据成员,但这里由于value_
被声明为mutable
(可变的),所以在这个函数里可以对value_
进行赋值操作,实现修改其值的功能,也就是可以通过调用这个函数来改变类对象中value_
所存储的值。
mutable
关键字的作用
- 在
private
部分声明的int value_
前面使用了mutable
关键字,其意义在于打破了const
成员函数不能修改数据成员的限制,使得在const
成员函数(像这里的set_value
函数)中能够合法地对这个被标记为mutable
的成员变量进行修改。这在一些特定场景下很有用,例如当需要在一个逻辑上不应该改变对象状态(用const
修饰来体现这种逻辑)的函数中,又要能够记录一些额外的、可变的信息(比如缓存值、计数器等情况)时,就可以使用mutable
关键字来修饰对应的成员变量。
mutable
关键字,因为如果滥用可能会使代码的逻辑变得不清晰,尤其是对于那些原本期望const
对象保持真正不可变的部分代码逻辑产生混淆,破坏了const
语义原本带来的可维护性和代码可读性上的优势。#include <iostream> int main() { Example ex; ex.set_value(10); std::cout << "Value after setting: " << ex.get_value() << std::endl; const Example& const_ex = ex; const_ex.set_value(20); std::cout << "Value after setting with const object: " << const_ex.get_value() << std::endl; return 0; }
main
函数中,先创建了一个普通的Example
类对象ex
并调用set_value
进行赋值,然后获取值输出。接着通过将ex
赋值给一个const
引用const_ex
,按常理const
对象不能调用修改其状态的函数,但由于set_value
函数能修改mutable
成员变量,所以依然可以通过const_ex
调用set_value
来修改值并再次获取输出,展示了mutable
关键字配合const
成员函数的特殊用法。以下是开始问豆包,学到的知识:
疑惑一Q:最后的这个例子,加个&就代表ex这个对象了吗?为啥const Example& const_ex = ex;,之前不都是const Example ex;吗?怎么加了个&
解释一A:当你使用const Example& const_ex = ex;
时,&
表示const_ex
是一个引用,它引用了ex
这个Example
对象。可以把引用理解为对象的一个别名。这就好比一个人有自己的本名,但是别人也可以用一个昵称来称呼他,这个昵称就相当于引用,和本名(原始对象)指向的是同一个人(同一块内存区域)。
Q:这个例子懂了,可是跟你说的不一样啊?你说“不过在实际使用中,要谨慎使用mutable关键字,因为如果滥用可能会使代码的逻辑变得不清晰,尤其是对于那些原本期望const对象保持真正不可变的部分代码逻辑产生混淆,破坏了const语义原本带来的可维护性和代码可读性上的优势。 以下是一个简单的使用示例:”你想举一个混淆不清晰的例子,结果却举了个正常的例子
mutable
会有问题的例子呀。
#include <iostream> class ConfusingExample { public: // 一个看起来只是获取值的const函数,但其实内部因为mutable可能改变状态 int getValue() const { counter++; return value; } // 设置值的函数 void setValue(int v) { value = v; } private: int value; mutable int counter; // 用来记录getValue被调用的次数 }; int main() { const ConfusingExample ex; // 这里从语义上看,只是想获取值,按常理const对象状态不应改变 std::cout << "Value: " << ex.getValue() << std::endl; std::cout << "Value again: " << ex.getValue() << std::endl; // 假设后面还有代码依赖于ex这个const对象状态没变化 // 比如有其他函数以const ConfusingExample& 接收ex并做一些判断等操作 // 但实际上因为mutable的counter被改变了,对象状态在“看不见”的地方变了 return 0; }
在这个ConfusingExample
类里呀:
- 违背直观预期:
getValue
函数被定义成了const
,从函数名和常规的const
函数理解角度,大家会觉得它就是单纯用来获取value
这个成员变量的值,不会去改变对象的任何状态。然而,因为使用了mutable
修饰了counter
,在getValue
函数内部每次调用都会去递增counter
的值,这就相当于在一个本应是 “只读” 的函数里悄悄改变了对象的一部分状态。 - 造成后续逻辑混淆:当在
main
函数里创建了const
的ConfusingExample
对象ex
后,按道理const
对象的状态应该保持稳定不变呀,我们只是去调用getValue
获取值而已。但实际上呢,每次调用getValue
,那个mutable
的counter
都会变化,这就可能导致后续依赖这个const
对象状态未改变的代码出现问题。比如要是后面还有其他函数接收这个const
的ex
对象做一些逻辑判断,它原本以为ex
一直是最初的状态,可实际上counter
的值已经变了,那很可能就得出错误的结论,整个代码的逻辑就变得很不清晰啦,破坏了原本const
对象应有的那种 “不可变” 的语义以及代码的可维护性和可读性呢。
mutable
,随意在一些不该改变状态的const
函数里通过mutable
去修改成员变量,就容易让代码逻辑变得混乱,让人不好理解啦。这下是不是更清楚些了呀?const ConfusingExample ex;
时,由于 ConfusingExample
类没有自定义的默认构造函数,而这里的 ex
又没有进行初始化,C++ 编译器就会报错啦,因为对于 const
对象来说,必须要初始化它,确保它从一开始就有确定的值,后续不能再被修改嘛。ConfusingExample
类添加一个默认构造函数来解决这个问题呀,修改后的代码如下:#include <iostream>
class ConfusingExample {
public:
// 添加默认构造函数,初始化value和counter
ConfusingExample() : value(0), counter(0) {}
// 一个看起来只是获取值的const函数,但其实内部因为mutable可能改变状态
int getValue() const {
counter++;
return value;
}
// 设置值的函数
void setValue(int v) {
value = v;
}
private:
int value;
mutable int counter; // 用来记录getValue被调用的次数
};
int main() {
const ConfusingExample ex;
// 这里从语义上看,只是想获取值,按常理const对象状态不应改变
std::cout << "Value: " << ex.getValue() << std::endl;
std::cout << "Value again: " << ex.getValue() << std::endl;
// 假设后面还有代码依赖于ex这个const对象状态没变化
// 比如有其他函数以const ConfusingExample& 接收ex并做一些判断等操作
// 但实际上因为mutable的counter被改变了,对象状态在“看不见”的地方变了
return 0;
}
ConfusingExample()
,在这个构造函数里使用初始化列表的方式把 value
初始化为 0
,counter
也初始化为 0
,这样声明 const
对象 ex
的时候就可以正常通过编译啦,也就能更好地体现出之前说的因为滥用 mutable
导致代码逻辑可能出现混淆的情况了哦。希望这次没问题啦,要是还有疑问可以再跟我说一说呀。-
定义和作用
- 构造函数是一种特殊的成员函数,它的主要作用是在创建对象时进行初始化操作。当你创建一个类的对象时,构造函数会自动被调用,用来为对象的成员变量赋初始值、分配资源或者进行其他必要的初始化步骤。
- 例如,在刚才的
ConfusingExample
类中,添加的构造函数ConfusingExample()
就用于初始化value
和counter
这两个成员变量。
-
语法特点
- 构造函数的名字与类名相同。它没有返回类型,甚至连
void
都不能有。这是因为构造函数的调用是由编译器自动完成的,不是像普通函数那样由用户显式调用并获取返回值。 - 构造函数可以有参数,用于接收初始化对象所需的值。例如,我们可以修改
ConfusingExample
类的构造函数,使其能够接收一个初始值来初始化value
:
class ConfusingExample { public: // 带有参数的构造函数,用于初始化value ConfusingExample(int initValue) : value(initValue), counter(0) {} // 其他成员函数... private: int value; mutable int counter; };
- 这样,在创建
ConfusingExample
对象时,就可以像下面这样提供初始值了:
ConfusingExample ex(5); // 创建一个对象ex,其value成员变量初始值为5,counter初始值为0
- 构造函数的名字与类名相同。它没有返回类型,甚至连
-
默认构造函数
- 如果一个类没有定义任何构造函数,编译器会自动生成一个默认构造函数。这个默认构造函数不接受任何参数,它会对对象的成员变量进行默认初始化(对于基本数据类型,通常是未定义的值;对于类类型,会调用其默认构造函数)。
- 但是,一旦你自己定义了一个构造函数(无论是带参数的还是不带参数的),编译器就不会再自动生成默认构造函数了。这就是为什么在之前的代码中,当我们自定义了一个带参数的构造函数后,还需要自己定义一个无参数的默认构造函数,否则创建无参数的对象时就会出错。
-
初始化列表
- 在构造函数的定义中,初始化列表是一种高效的初始化成员变量的方式。它位于构造函数的参数列表之后、函数体之前,以冒号开始。例如在
ConfusingExample(int initValue) : value(initValue), counter(0)
中,value(initValue)
和counter(0)
就是初始化列表的内容。 - 使用初始化列表的好处是可以直接初始化成员变量,而不是先默认初始化然后再在构造函数体中赋值。对于一些不能被默认初始化或者需要特定初始化方式的成员变量(如引用、const 成员变量等),初始化列表是必不可少的。
- 在构造函数的定义中,初始化列表是一种高效的初始化成员变量的方式。它位于构造函数的参数列表之后、函数体之前,以冒号开始。例如在
- 如果不是const声明的对象,是不是就不是必须用构造函数?
- 如果是const就必须用构造函数。
- 不是 const 声明的对象不是必须用构造函数。
- const 声明的对象必须用构造函数(或者带有初始化列表的构造函数来初始化)。
private
成员变量不能在类的外部直接访问呀。counter
这个成员变量被声明为private
,这意味着它只能在ConfusingExample
类的内部被访问,像在类的成员函数(比如getValue
函数内部可以访问并修改它)里面使用是没问题的。main
函数中,你写了cout << ex.counter << endl;
这样的语句,这是试图从类的外部去访问private
成员变量counter
了,这违反了类的访问控制规则,所以编译器就会报错提示你这个变量在当前的上下文中(也就是类的外部)是private
,不允许这样访问哦。counter
的值,可以通过在类里面添加一个public
的函数来获取它的值呀,比如在ConfusingExample
类中添加如下的函数:class ConfusingExample {
public:
// 构造函数等其他函数保持不变
int getCounter() const {
return counter;
}
private:
int value;
mutable int counter;
};
main
函数中这样调用就可以获取并输出counter
的值了:int main() {
const ConfusingExample ex;
std::cout << "Value: " << ex.getValue() << std::endl;
std::cout << "Counter: " << ex.getCounter() << endl;
std::cout << "Value again: " << ex.getValue() << std::endl;
std::cout << "Value again: " << ex.getValue() << std::endl;
std::cout << "Counter: " << ex.getCounter() << endl;
return 0;
}
public
的接口来获取private
成员变量的相关信息啦int main() { const ConfusingExample ex; std::cout << "Value: " << ex.getValue() << ", Counter: " << ex.counter << std::endl; std::cout << "Value again: " << ex.getValue() << ", Counter: " << ex.counter << std::endl; std::cout << "Value again: " << ex.getValue() << ", Counter: " << ex.counter << std::endl; return 0; }
也不对啊,private的也输出访问不了啊
private
成员不能直接从外部访问这个规则呢。public
的函数来获取counter
的值,这样才能在main
函数里合法地输出查看它呀。以下是完整修正后的代码示例:#include <iostream> class ConfusingExample { public: ConfusingExample() : value(0), counter(0) {} // 一个看起来只是获取值的const函数,但其实内部因为mutable可能改变状态 int getValue() const { counter++; return value; } // 设置值的函数 void setValue(int v) { value = v; } // 添加这个public函数用于获取counter的值 int getCounter() const { return counter; } private: int value; mutable int counter; // 用来记录getValue被调用的次数 }; int main() { const ConfusingExample ex; std::cout << "Value: " << ex.getValue() << ", Counter: " << ex.getCounter() << std::endl; std::cout << "Value again: " << ex.getValue() << ", Counter: " << ex.getCounter() << std::endl; std::cout << "Value again: " << ex.getValue() << ", Counter: " << ex.getCounter() << std::endl; return 0; }
ConfusingExample
类中添加getCounter
这个public
的const
函数,它可以返回counter
的值,这样在main
函数里就能通过ex.getCounter()
这种合法的方式来获取并输出counter
的值了,也就能够直观地看到每次调用getValue
函数时,mutable
修饰的counter
变量是如何递增变化的啦,这样就能更好地体现出之前说的因为滥用mutable
导致代码逻辑可能出现混淆的情况哦。再次为我之前的疏忽表示歉意呀,希望这次没问题啦const
对象有严格的初始化要求呀。
规则解释
const
对象,比如const ConfusingExample ex;
,编译器期望这个对象在创建的时候所有成员变量就已经有确定的初始值了,而且后续不能再改变(毕竟是const
嘛)。ConfusingExample() : value(0), counter(0) {}
这样的构造函数来初始化成员变量,那编译器就不知道该给value
和counter
赋什么初始值了。对于普通的非const
对象,成员变量可能会有默认的初始化行为(比如基本数据类型可能是未定义的值等),但const
对象必须明确初始化,不然就违反了它 “常量” 的这个语义设定呀。对比普通对象情况
const
对象,比如ConfusingExample ex;
(假设没有自定义构造函数),编译器会自动生成默认构造函数帮着进行一些默认初始化,虽然对于基本数据类型成员变量,这些默认初始化的值可能不太确定(像int
类型可能就是内存里原本那个不确定的值),但语法上是允许的。const
修饰,就必须要保证对象从一开始就处于一个完全确定且后续不能变的状态,所以就得通过构造函数的初始化列表等方式明确地把成员变量初始化成合适的值,这样才能符合const
对象的要求,避免编译器报错啦。const
对象就会触发 “uninitialized const
” 这样的错误提示哦,这下你应该更清楚些了吧。int a = 5;
就是对整型变量a
进行初始化。- 自动调用
- 在 C++ 中,构造函数是在创建对象时自动被调用的,不需要像 C 语言那样手动地一个一个地初始化成员变量。例如,当你写下
ConfusingExample ex;
,就会自动调用ConfusingExample
类对应的构造函数(如果没有定义构造函数,编译器会生成默认构造函数来做一些基本的初始化工作)。而在 C 语言中,没有这种自动调用机制来完成复杂的初始化任务。
- 在 C++ 中,构造函数是在创建对象时自动被调用的,不需要像 C 语言那样手动地一个一个地初始化成员变量。例如,当你写下
- 初始化多个成员和复杂操作
- 构造函数可以同时初始化类中的多个成员变量,而且可以进行更复杂的操作。以刚才的
ConfusingExample
类为例,构造函数ConfusingExample() : value(0), counter(0) {}
不仅初始化了value
和counter
这两个成员变量,还可以在构造函数体里面(如果有需要)进行资源分配(比如动态内存分配)、打开文件或者建立网络连接等操作。在 C 语言中,虽然也可以初始化多个变量,但对于更复杂的操作,就需要在初始化之后再单独写代码来完成,没有像构造函数这样集成在对象创建过程中的机制。
- 构造函数可以同时初始化类中的多个成员变量,而且可以进行更复杂的操作。以刚才的
- 对象相关的初始化
- 构造函数是和对象紧密相关的,它可以根据对象的不同创建方式(比如通过不同的参数)来进行不同的初始化。例如,有一个带有参数的构造函数
ConfusingExample(int initValue) : value(initValue), counter(0)
,可以根据传入的不同initValue
来初始化对象的value
成员变量。在 C 语言中,初始化主要是针对变量本身,没有这种和对象创建场景紧密结合的方式来灵活初始化。
- 构造函数是和对象紧密相关的,它可以根据对象的不同创建方式(比如通过不同的参数)来进行不同的初始化。例如,有一个带有参数的构造函数
Q:你说乱用mutable会导致问题,但如果去掉mutable代码报错,error: increment of member 'ConfusingExample::counter' in read-only object|
A:确实是这样呀,主人。在当前这个代码结构中,如果去掉 mutable
关键字的话,counter
就变成普通的成员变量了。而 getValue
函数被声明为 const
成员函数,按照 const
成员函数的规则,在其内部是不允许修改类的普通(非 mutable
)成员变量的呀。
const
的语义就是保证这个函数执行过程中,所操作的对象状态保持不变(除了那些被 mutable
修饰、特意允许改变的成员变量外)。在 getValue
函数里执行 counter++
这个操作,相当于要去修改 counter
的值了,对于普通成员变量来说,这就违背了 const
函数的约定,所以编译器就会报错提示 “increment of member 'ConfusingExample::counter' in read-only object
”,意思就是在一个本应是只读(const
)的对象里去递增这个成员变量是不被允许的啦。mutable
关键字存在的意义呀,它就是用来在一些特定场景下(像咱们这个例子里需要在 const
函数中记录调用次数这种情况)打破常规 const
限制,合理地去改变某些成员变量的值呢。要是滥用它,会像之前说的让代码逻辑变模糊;但要是该用的时候不用,又会导致代码编译不过去,出现像这样违反 const
语义的报错情况哦。继续看菜鸟教程里他的那个代码:
Q:这里set_value的设置是const,是不是没必要了啊,感觉跟mutable冲突呢?直接mutable不行吗
A:
- 不冲突且有必要的原因 - 从对象的使用场景角度
- 在 C++ 中,
const
对象的概念非常重要。当一个对象被声明为const
时,其语义是这个对象的状态在其生命周期内不应该被改变。例如:const Example ex; - 对于这样的
const
对象,如果set_value
函数不是const
成员函数,那么就不能在const
对象上调用set_value
函数,这是符合const
对象语义的。但是,通过将set_value
函数声明为const
,并且使用mutable
关键字来修饰成员变量value_
,就可以在const
对象上调用set_value
函数,这在某些情况下是非常有用的。 - 比如,
Example
类可能用于记录一些配置信息,其中value_
可能代表一个可以动态调整的阈值(即使对象本身在概念上是 “常量”)。在这种情况下,能够通过const
对象来修改这个阈值是很合理的设计,这就体现了const
函数和mutable
变量共同作用的优势。
- 在 C++ 中,
-
不冲突且有必要的原因 - 从代码可读性和维护性角度维护性角度
- 声明
set_value
为const
函数向其他阅读代码的人传达了一个重要信息:这个函数在逻辑上不会改变对象的 “主要状态”。这里的 “主要状态” 是指除了mutable
成员变量之外的其他成员变量(如果有的话)。 - 如果没有
const
修饰,代码的使用者可能会误解这个函数会随意改变对象的各种状态。而mutable
关键字则明确指出了有一个特殊的成员变量是可以在const
函数环境下被修改的。这种明确的分工有助于提高代码的可读性和可维护性。
- 声明
-
直接使用
mutable
而不使用const
函数的问题- 如果只使用
mutable
而不将set_value
声明为const
,那么在使用const
对象时,就无法调用set_value
函数来修改value_
。例如,在一个函数中接收一个const Example&
参数(这是一种常见的传递对象引用的方式,用于避免不必要的复制并且保证对象状态不被随意修改),如果set_value
不是const
函数,就不能在这个函数内部通过该const
引用修改value_
。 - 另外,从设计原则上讲,
const
函数的存在有助于将函数按照是否修改对象状态进行分类。如果抛弃const
修饰,会使得代码的设计原则变得模糊,难以区分哪些函数是真正改变对象主要状态的,哪些函数只是在const
语义下修改了一些辅助的(mutable
)成员变量
- 如果只使用
Q:没太懂啊,太罗嗦了,你给初学者讲解东西能尽量别牵扯其他知识吗?后面的知识也用不到,就最简单的解释来说
A:好呀,那我用最简单的方式给你解释一下哦。
set_value
函数,它被写成const
是有必要的呢,和mutable
并不冲突哦。const
在这里就像是给这个函数做了个标记,意思是这个函数一般情况下不会去改变这个对象里重要的、不能变的那些东西(也就是除了mutable
标记的成员之外的其他成员呀,不过这里暂时就只有value_
这一个成员啦)。
mutable
呢,它是专门给value_
这个成员变量用的,就是说哪怕这个函数是const
的,也就是承诺了尽量不改变东西,但因为value_
被标了mutable
,那在set_value
这个函数里就能去改变value_
的值啦。set_value
写成const
,那要是咱们有个const
的Example
类对象(比如const Example ex;
这样声明的对象),想调用set_value
去改value_
的值就不行啦,因为按照规则,const
对象只能调用const
的函数呀。
Q:既然这样去掉const不就行了吗
A:哎呀,不能去掉const
呀,我再给你讲讲为啥哈。
const
关键字声明的对象,像这样:const Example ex;
。set_value
函数没有const
修饰,那对于这个const
声明的ex
对象来说,就没办法调用set_value
函数了呀,因为按照规定,const
对象只能去调用那些同样带着const
标记的函数呢。const
对象的情况下,还能改value_
这个成员变量的值,多亏了mutable
关键字让它能改,同时又因为set_value
是const
函数,所以const
对象就能调用它去改value_
的值啦。const
去掉了,那在有const
对象的时候,这个改值的操作就做不了了,就没办法这么灵活地使用了,所以const
可不能去掉哦,它和mutable
一起配合着才能让代码按咱们想要的那样去工作呢。这下是不是更清楚一点啦?- 常量对象的安全性和语义准确性
- 在 C++ 编程中,使用
const
声明对象有很重要的意义。const
对象就像是一个被 “保护” 起来的对象,它向编译器和其他阅读代码的人传达了一个信息:这个对象的状态不应该被随意改变。这有助于防止意外的修改。 - 例如,如果你有一个函数,它接收一个
Example
对象作为参数,并且这个函数不应该修改这个对象的状态,你可以将这个参数声明为const Example&
(引用传递以避免复制成本)。这样一来,编译器会检查这个函数内部是否有对这个对象的非法修改。 - 假设没有
const
机制,那么任何函数都可以随意修改传递进来的对象,这可能会导致很难发现的错误。比如在一个复杂的函数调用链中,某个函数不应该修改对象状态,但由于没有约束,它可能会意外地修改了对象,从而影响到其他依赖这个对象原始状态的部分。
- 在 C++ 编程中,使用
- 代码的通用性和灵活性
- 当你设计一个类时,考虑到可能有用户希望以
const
的方式使用这个类的对象,那么提供const
成员函数就增加了类的通用性。 - 比如,你可能有一个函数模板,它可以处理不同类型的对象,包括
const
对象和非const
对象。如果Example
类的set_value
函数不是const
,那么这个函数模板在处理const Example
对象时就会受到限制,因为它不能调用非const
的set_value
函数来修改对象中的值(即使这个修改在逻辑上是合理的,比如只修改一个内部的、辅助性的成员变量)。 - 而有了
const
修饰的set_value
函数,就可以在const
对象和非const
对象上都能调用这个函数,这使得代码更加灵活,能够适应更多不同的使用场景。
- 当你设计一个类时,考虑到可能有用户希望以
- 设计的规范性和可扩展性
- 在编程中,遵循一定的规范和设计模式有助于代码的长期维护和扩展。虽然在这个具体的
Example
类中,看起来好像可以简单地去掉const
和mutable
来实现功能,但这样做可能会在更复杂的场景下引发问题。 - 例如,假设这个
Example
类未来会被其他开发者使用或者进一步扩展。如果没有const
和mutable
这种明确的标识,新的开发者可能会误解set_value
函数的行为。他们可能会认为这个函数会随意改变对象的所有成员,而不是像现在这样,通过const
和mutable
明确了只是改变一个特殊标记的成员变量,同时保持其他部分不变。
- 在编程中,遵循一定的规范和设计模式有助于代码的长期维护和扩展。虽然在这个具体的
- 与其他代码的兼容性和接口一致性
- 当你的代码与其他库或者模块交互时,
const
正确性是很重要的。很多库函数或者接口在设计时会考虑const
对象的处理。 - 比如,如果你想把
Example
类的对象存储在一个数据结构中,这个数据结构可能会有一些针对const
对象的操作或者优化。如果Example
类的成员函数不符合const
语义(没有合理使用const
和mutable
),可能会导致在这些数据结构中的使用出现问题。
- 当你的代码与其他库或者模块交互时,
- 逻辑分离和清晰性
- 使用
const
和mutable
可以将对象的不同状态变化逻辑清晰地分开。const
函数表示这个函数在逻辑上不会改变对象的主要状态,而mutable
成员变量则是一个例外情况,用于处理那些需要在const
语义下仍然可以改变的特殊情况。 - 这种分离使得代码的逻辑更加清晰。就像在这个
Example
类中,get_value
函数是纯粹的获取值,没有改变对象状态(除了mutable
变量外),而set_value
函数虽然改变了一个成员变量,但通过const
和mutable
的组合,明确地表示了它只是改变了一个特殊的、允许在const
函数中改变的变量,而不是随意改变对象的所有状态。
- 使用
-
关于
const
的广泛使用- 在良好的 C++ 编程实践中,确实应该尽量多使用
const
。当一个函数不打算修改对象的状态(除了可能的mutable
成员变量)时,将其声明为const
是一个很好的习惯。这样做可以让代码的语义更加清晰,让阅读代码的人(包括自己在以后回顾代码时)一眼就能知道这个函数不会对对象进行 “实质性” 的修改。 - 例如,对于像
get_value
这样的函数,它只是返回对象中成员变量的值,没有任何修改操作,使用const
修饰是非常合适的。这有助于在代码的使用过程中防止意外的修改,并且在处理const
对象时也能够正确地调用该函数。
- 在良好的 C++ 编程实践中,确实应该尽量多使用
-
mutable
的谨慎使用- 然而,并不是所有需要改变的成员变量都应该或者可以用
mutable
来处理。mutable
主要用于那些在逻辑上被认为是对象的 “附属” 或者 “辅助” 性质的变量,这些变量的改变不会影响对象的核心状态。 - 比如,
mutable
可能用于存储缓存数据、计数器或者一些用于内部记录目的的变量。如果一个成员变量的改变会导致对象的核心功能或者对外表现的状态发生改变,那么一般不适合用mutable
,而应该重新考虑对象的设计或者函数的const
属性。 - 另外,过度使用
mutable
会破坏const
对象所带来的语义上的保证,使得代码变得难以理解和维护。如果一个对象中有太多的mutable
变量,那么这个对象的const
属性就会变得很模糊,到底哪些部分是真正不变的,哪些部分是可以变的,就很难分辨清楚了。
- 然而,并不是所有需要改变的成员变量都应该或者可以用
const
应该被广泛合理地使用,但mutable
需要谨慎使用,只有在符合其设计意图(用于处理const
函数中的特定辅助变量修改)的情况下才使用。public
和private
的基本概念public
:- 当成员(函数或变量)被声明为
public
时,这意味着这些成员可以在类的外部被访问。在Example
类中,get_value
和set_value
函数是public
的,这允许其他代码(比如在main
函数或者其他类的函数中)通过Example
类的对象来调用这两个函数。 - 例如,如果有一个
Example
对象ex
,可以在类外部这样调用:ex.get_value()
和ex.set_value(5)
。这就像给外界提供了一个接口,让外界能够使用这个类的功能。
- 当成员(函数或变量)被声明为
private
:- 与之相反,
private
成员(在这里是value_
)只能在类的内部被访问。这意味着其他类或者函数不能直接访问value_
。这种限制有重要的安全和设计目的。 - 例如,通过将
value_
设为private
,可以确保它的值只能通过public
的get_value
和set_value
函数来获取和设置。这样可以防止外部代码随意地修改value_
的值,保证了类的封装性。封装性是面向对象编程的一个重要概念,它可以隐藏类内部的实现细节,使得类的使用者只需要关注public
接口提供的功能。
- 与之相反,
- 不写访问修饰符的情况(默认访问权限)
- 在 C++ 中,如果不写访问修饰符,对于类来说,默认的访问权限是
private
。这意味着如果省略了public
或private
关键字,成员将被视为private
。 - 不过,为了代码的清晰性,最好还是明确地写上访问修饰符。这样可以让代码的阅读者(包括自己在以后回顾代码时)一眼就能看出成员的访问权限,避免因为依赖默认规则而产生误解。
- 在 C++ 中,如果不写访问修饰符,对于类来说,默认的访问权限是
private
部分的位置先后顺序其实并没有严格要求它一定要在最开始或者某个固定位置啦,它只要在类的private
区域内声明就可以了。
get_value
函数里能return value_
这个情况呀,是因为value_
是这个类Example
的成员变量呀,虽然它的声明在后面,但在类的成员函数内部(像这里的get_value
函数),是可以直接访问同属于这个类的所有成员变量的,不管这个变量声明在前面还是后面呢。value_
在private
区域的后面声明,在前面的get_value
函数里同样可以正常地使用它来返回值呢。- 封装性和数据隐藏
- 将
value_
设为private
主要是为了实现封装性。封装是面向对象编程的一个核心原则,它的目的是隐藏对象内部的细节,只向外界暴露必要的接口。在这个例子中,value_
是类Example
的一个内部数据成员,如果它是public
,那么外部代码就可以直接访问和修改这个变量。 - 例如,假设
value_
是一个代表某种敏感配置信息的值,如数据库连接的密码或者系统关键参数。如果它是public
,任何一段外部代码都可以随意读取或者修改这个值,这可能会导致程序出现错误或者安全漏洞。而将value_
设为private
后,外部代码就不能直接访问它,只能通过类提供的public
成员函数(如get_value
和set_value
)来间接操作这个值,这样就可以在这些public
函数中添加必要的验证或者逻辑来确保数据的安全性和正确性。
- 将
- 可维护性和灵活性
- 另一个重要的原因是可维护性。当
value_
是private
时,类的内部实现细节(也就是value_
的具体类型、存储方式、取值范围等)可以自由地改变,而不会影响到使用这个类的外部代码。 - 比如说,最初
value_
可能是一个简单的int
类型,用来存储一个计数器的值。但随着程序的发展,可能需要对这个计数器进行更复杂的操作,比如限制它的取值范围,或者在每次修改它的值时记录日志。如果value_
是public
,那么所有直接访问value_
的外部代码都需要进行修改。但因为value_
是private
,只要public
接口(get_value
和set_value
)的功能和语义不变,外部代码就不需要做任何修改,只需要修改类内部关于value_
的实现就可以了,这大大提高了代码的可维护性和灵活性。
- 另一个重要的原因是可维护性。当
-
未来可能的扩展和修改
- 虽然目前看起来
value_
只是一个简单的int
变量,但在程序的生命周期中,情况可能会发生变化。比如,后续可能需要对这个变量进行一些额外的操作,如范围限制(确保value_
始终在某个特定区间内)、数据验证(检查传入value_
的值是否符合特定格式或规则)或者关联其他变量进行计算。 - 假设这个
Example
类是一个更大系统中的一部分,用于配置某些参数。最初,value_
可能只是简单地存储一个整数参数,但后来可能需要根据系统的其他状态或者用户权限来决定是否允许修改value_
,或者对修改后的value_
进行进一步的处理。如果value_
是public
,这些复杂的操作就很难实现,因为外部代码可能会绕过这些规则直接修改value_
。
- 虽然目前看起来
-
隔离外部影响
- 即使
value_
目前没有复杂的内部细节,将其设为private
也可以隔离外部代码对它的影响。外部代码可能会因为错误或者不恰当的使用而意外地修改value_
的值,导致程序出现难以预料的错误。 - 例如,有多个不同的模块或者函数都可以访问
value_
(如果它是public
),那么很难追踪是哪个部分的代码导致了value_
的值出现异常。而通过将value_
设为private
,并通过public
的get_value
和set_value
函数来访问它,就可以在这些函数中添加调试信息或者错误处理机制,方便定位和解决问题。
- 即使
-
遵循面向对象设计原则
- 从面向对象设计的角度来看,将成员变量设为
private
是一种良好的实践。这样可以使类的接口(public
部分)和实现(private
部分)分离,让类的使用者只需要关注接口提供的功能,而不需要了解内部的具体实现。这有助于构建更加模块化、可维护和可扩展的代码。 - 就像使用一个黑盒子设备一样,使用者只需要知道如何通过按钮(
public
接口)来操作这个设备,而不需要了解设备内部的电路(private
变量和实现细节)是如何工作的。这种抽象层次的分离可以提高代码的质量和可理解性。
- 从面向对象设计的角度来看,将成员变量设为
public
的set_value
函数是可以在外部修改value_
的值,但这和直接将value_
设为public
有着本质的区别呀。
- 逻辑控制方面
value_
是private
,并且通过set_value
函数来修改它时,我们可以在set_value
函数内部添加各种逻辑控制。比如,我们可以限定传入value_
的数值范围,像只允许传入大于等于 0 且小于 100 的整数,代码可以这样写:void set_value(int value) const {
if (value >= 0 && value < 100) {
value_ = value;
}
}
value_
直接是public
,那外部代码就可以随意传入任何值来修改它了,没办法去做这样的范围限定,很容易导致不符合预期的值被赋给value_
,进而可能引发程序后续出现错误。- 可维护性和灵活性方面
value_
的时候还需要记录下每次修改的时间戳,那如果value_
是private
,我们只需要在set_value
函数内部添加记录时间戳的代码就好啦,像这样:#include <ctime>
void set_value(int value) const {
std::time_t now = std::time(nullptr);
// 假设这里有个地方可以存储时间戳,比如另一个成员变量等
// 先忽略具体存储实现细节,重点是可以在这里添加额外逻辑
value_ = value;
}
value_
是public
,那所有直接修改value_
的外部代码都得跟着修改来实现记录时间戳这个新需求,这可就太麻烦啦,而且很容易遗漏一些地方没改对,导致程序出问题。- 数据一致性和封装性方面
set_value
函数来修改value_
,能保证整个类内部数据的一致性。因为这个函数是类自身提供的,它知道类内部其他成员变量的情况,在修改value_
时可以顺便检查或者更新与之相关的其他数据,维持整个类状态的协调。可要是value_
是public
,外部代码随意修改它时,就没法保证和类内部其他数据的关联性和一致性了。set_value
能在外部修改value_
,但把value_
设为private
有着诸多好处,是遵循良好编程规范和利于程序长远发展的做法呢。-
紧密关联与间接控制
- 当
value_
是private
时,set_value
函数就像是一个 “看门人”,对外部传入的数据进行检查和过滤后再修改value_
。这种紧密关联使得value_
的修改被间接控制在类所定义的规则范围内。 - 虽然
set_value
函数本身是public
,可以从外部调用,但它的存在是基于value_
为private
的前提。如果value_
是public
,那么外部代码就可以完全绕过set_value
函数,直接对value_
进行不符合规则的操作。例如,可能会出现这样的代码(假设value_
是public
):
Example ex; ex.value_ = -1; // 直接修改value_,可能会导致不符合预期的情况
- 而当
value_
是private
时,上面这种直接修改的代码是不被允许的,必须通过set_value
函数来修改,这样就强制外部代码遵循类所设定的修改规则。
- 当
-
维护数据的完整性和一致性
- 把
value_
设为private
并通过set_value
函数来修改它有助于维护数据的完整性和一致性。set_value
函数可以根据类内部的其他状态或者规则来调整value_
的值。 - 例如,假设
Example
类还有另一个成员变量min_value
,它规定了value_
的最小值。当value_
是private
时,可以在set_value
函数中添加代码来确保value_
不小于min_value
:
class Example { public: void set_value(int value) const { if (value < min_value) { value_ = min_value; } else { value_ = value; } } private: mutable int value_; int min_value; };
- 如果
value_
是public
,就很难保证在修改value_
的同时考虑到min_value
的限制,可能会破坏类内部数据的一致性。所以,value_
是private
和set_value
函数一起协同工作,能够更好地维护类内部数据的完整性和一致性
- 把
-
private
访问权限- 是的,
private
关键字用于设置访问权限。被声明为private
的成员变量和成员函数只能在类的内部访问。这是一种封装机制,目的是隐藏类的内部实现细节,使得外部代码不能直接操作这些成员。 - 例如,在
ConfusingExample
类中,value
和counter
是private
成员变量,所以不能在main
函数等类外部的代码中直接通过ex.value
或者ex.counter
这样的方式来访问它们。
- 是的,
-
const
与不可变性const
主要用于表明对象或者函数的不可变性。对于const
对象,其成员变量(除了mutable
修饰的)在该对象的生命周期内应该保持不变。- 对于成员函数,如果函数被声明为
const
,那么在这个函数内部,不应该修改类的非mutable
成员变量。例如,在ConfusingExample
类的const
成员函数getValue
中,按常规理解是不应该修改value
这个成员变量的(因为它没有mutable
修饰),但是由于counter
是mutable
的,所以在const
函数getValue
中可以修改counter
。
-
mutable
的作用mutable
关键字用于打破const
的限制,但仅针对它所修饰的成员变量。它允许在const
成员函数中修改被mutable
修饰的成员变量。- 例如,在
ConfusingExample
类的const
成员函数getValue
中,counter
是mutable
的,所以可以在这个函数中对counter
进行递增操作counter++
,即使这个函数是const
的,也不会违反const
的语义(因为const
语义对于mutable
成员变量是放开的)。
private
决定了成员的访问范围(类内部还是外部),而const
和mutable
主要是围绕成员变量在const
相关的上下文中(如const
对象和const
函数)是否可变来发挥作用的。private
控制访问范围,决定成员是否能在类内部访问;而const
和mutable
主要用于控制成员变量是否可以被改变,尤其是在const
对象和const
函数的场景下。private
声明的value
主要有以下这些作用呀:
- 数据封装与隐藏
ConfusingExample
类里,value
是类的一个内部状态数据,如果它不是private
,外部代码可能会随意给它赋不合理的值,导致类的行为出现异常。像这样:ConfusingExample ex;
ex.value = 100; // 若value不是private,外部能这样直接修改,可能破坏类的逻辑
value
设为private
后,外部代码就没办法直接这么做了,只能通过类提供的public
接口(比如setValue
函数)来间接修改它的值,这样就能在setValue
函数里添加一些验证逻辑之类的,保证数据的合理性和类整体行为的正确性。维护类的内部逻辑完整性
value
作为类内部的一个数据成员,它的变化往往和类的其他成员或者功能相关联。比如可能后续类的某个操作要依赖value
处于某个合理范围,或者在value
改变的时候要同步更新其他相关的数据成员等。将它设为private
,就能在类内部的成员函数中去控制它的变化过程以及与其他部分的协调,保证类内部逻辑完整、有序地运行,不受外部随意干扰。- 便于代码的可维护性和扩展性
value
的存储方式、取值范围等进行调整,只要public
接口(像getValue
和setValue
)的语义不变,外部使用这个类的代码基本不用修改,因为外部原本就不能直接接触到private
的value
,只需要在类内部对value
相关的实现进行修改就行啦,这使得代码在后续维护和扩展功能时更方便、灵活。const
关键字的位置不是固定要放在最后的。-
成员函数中的
const
位置- 对于成员函数,
const
通常放在函数参数列表之后、函数体之前。例如:
class MyClass { public: int getValue() const { // 函数体,不能修改非mutable成员变量 return value; } private: int value; };
- 这里
const
表示这个成员函数不会修改类对象的非mutable
成员变量,它的位置紧跟在函数参数列表(这个函数没有参数,所以直接是()
之后),这种语法是比较规范和常见的写法,让人一眼就能看出这个函数的const
属性。
- 对于成员函数,
-
const
对象和引用的声明位置- 当声明
const
对象或者const
引用时,const
一般在类型前面。比如:
const MyClass obj; const MyClass& ref = obj;
- 这里
const
用于限定obj
和ref
分别是const
对象和const
引用,它们的const
关键字在最前面,用于表明它们所代表的对象的常量性质。
- 当声明
- 是不是const定义的成员函数,里面的叫成员变量就不可以被更改。
- 如果setValue函数也是const的话,也没法value=v;了对吧?
- 唯一可以在后续改变他们是否可以被更改的,是不是就是定义他们的地方,也就是private那里,里面如果mutable了,就相当于有了特权,那哪怕上面函数里是const,里面只要有这个有特权的变量,也是可以更改的对吧?还有通过protect和private来确定是否可以从外部访问这些变量,跟他们有无特权是没关系的,是两码事对吧?
- 那protect啥意思
- 一般情况下,
const
成员函数里非mutable
成员变量不可以被更改。 - 对,如果
setValue
函数是const
,就不能修改非mutable
的value
,value = v;
会报错。 - 是的,
mutable
修饰的成员变量在const
函数中可以被更改。访问控制(private
、protected
)和是否能在const
函数中修改是两码事。 protected
表示所修饰的成员在类内可访问,在类的派生类中也可访问,在类外不能访问,用于继承体系中的访问控制。
protected
用于控制成员访问权限。它允许类自身及其派生类访问被修饰的成员,而类外部不能直接访问。这在继承关系中很有用,既可以把成员的访问范围扩展到派生类,又限制了外部的访问。-
基本代码示例
- 假设有一个基类
Base
和一个派生类Derived
。
class Base { protected: int protectedValue; public: Base() : protectedValue(0) {} }; class Derived : public Base { public: void modifyProtectedValue() { protectedValue = 1; // 可以访问和修改基类中的protected成员 } };
- 在这个例子中,
Base
类中有一个protected
成员变量protectedValue
。在Derived
类中,由于Derived
是Base
的派生类,所以Derived
类的成员函数modifyProtectedValue
可以访问和修改protectedValue
。
- 假设有一个基类
-
对比外部访问
- 但是,如果在
main
函数中这样写:
int main() { Base baseObj; // baseObj.protectedValue = 2; // 这是错误的,不能从外部访问protected成员 Derived derivedObj; derivedObj.modifyProtectedValue(); return 0; }
- 对于
Base
类的对象baseObj
,在类的外部是不能直接访问protectedValue
的。这体现了protected
的特性:在类自身和派生类内部可以访问,而对于外部(非派生类)是不可访问的
- 但是,如果在
-
class Derived : public Base
的含义- 这表示
Derived
类是从Base
类派生而来的,public
是继承方式。这种继承方式意味着Base
类中的public
成员在Derived
类中仍然是public
的,Base
类中的protected
成员在Derived
类中仍然是protected
的。 - 简单来说,
Derived
类会继承Base
类的成员(包括成员变量和成员函数),并且继承后这些成员的访问权限基本保持和在Base
类中一样(除了private
成员,派生类不能访问基类的private
成员)。
- 这表示
-
继承方式不必须是
public
- 除了
public
继承,还有private
继承和protected
继承。 private
继承:当使用class Derived : private Base
时,Base
类中的public
和protected
成员在Derived
类中都会变成private
成员。这意味着这些成员在Derived
类内部可以访问,但在Derived
类的派生类(如果Derived
类还被其他类继承)以及外部都不能访问。protected
继承:如果是class Derived : protected Base
,Base
类中的public
成员在Derived
类中会变成protected
成员,Base
类中的protected
成员在Derived
类中仍然是protected
成员。这样,这些成员在Derived
类内部和Derived
类的派生类中可以访问,但在外部不能访问。
- 除了
public
继承是最常用的,因为它符合 “是一个(is - a)” 的关系,能很好地体现类型之间的继承层次关系,例如 “派生类对象是一个基类对象” 这种语义。但具体使用哪种继承方式,要根据实际的设计需求和对访问权限的控制要求来决定。public
和protected
的区别 - 外部访问性public
成员:对于一个类中的public
成员,在类的外部可以直接访问。例如:
class MyClass { public: int publicValue; }; int main() { MyClass obj; obj.publicValue = 10; // 可以在类外部直接访问public成员 return 0; }
protected
成员:protected
成员在类的外部是不能直接访问的。如果尝试像访问public
成员那样在类外部访问protected
成员,编译器会报错。例如:
class MyBase { protected: int protectedValue; }; int main() { MyBase baseObj; // baseObj.protectedValue = 20; // 这是错误的,不能在类外部访问protected成员 return 0; }
public
和protected
的区别 - 继承中的行为public
继承下的public
成员:在public
继承中,基类的public
成员在派生类中仍然是public
的,外部可以通过派生类对象访问这些成员。例如:
class Base { public: int publicBaseValue; }; class Derived : public Base { }; int main() { Derived derivedObj; derivedObj.publicBaseValue = 30; // 可以通过派生类对象访问基类的public成员 return 0; } protected继承下的protected成员:在protected继承或者public继承(这是比较常见的情况)中,基类的protected成员在派生类中可以被访问和使用,但不能在派生类外部通过派生类对象访问。例如: class AnotherBase { protected: int protectedBaseValue; }; class AnotherDerived : public AnotherBase { void modifyProtectedValue() { protectedBaseValue = 40; // 在派生类内部可以访问基类的protected成员 } }; int main() { AnotherDerived anotherDerivedObj; // anotherDerivedObj.protectedBaseValue = 50; // 不能在派生类外部访问基类的protected成员 return 0; }
protected
的主要用途- 实现继承中的数据隐藏和访问控制:
protected
允许基类将一些成员的访问权限开放给派生类,同时限制外部的访问。这样在设计类层次结构时,可以在派生类中复用基类的代码和数据,同时保持一定的封装性。 - 为派生类提供特定的访问权限:比如,基类可能有一些内部状态(通过
protected
成员变量表示),它不希望外部直接修改,但允许派生类根据自身的需要进行适当的修改和扩展,这就体现了protected
的灵活性,它介于private
(完全隐藏)和public
(完全开放)之间。
- 实现继承中的数据隐藏和访问控制:
#include <iostream> class MyClass { private: int num; public: MyClass(int n) : num(n) {} // 非const成员函数 void changeValue(int newNum) { num = newNum; } // const成员函数 int getValue() const { return num; } }; // 函数模板,可处理不同对象 template<typename T> void printValue(const T& obj) { std::cout << obj.getValue() << std::endl; } int main() { MyClass normalObj(5); const MyClass constObj(10); // 对于非const对象,可以调用非const成员函数改变值 normalObj.changeValue(8); printValue(normalObj); // 对于const对象,只能调用const成员函数获取值,如果没有getValue这个const函数,这里就会报错 printValue(constObj); return 0; }
在这个例子中,MyClass
有改变值的非const
函数changeValue
和获取值的const
函数getValue
。printValue
函数模板能处理不同类型对象(这里是MyClass
的const
和非const
对象),要是没有const
的getValue
函数,处理const
对象时就没法正确获取值,会编译出错,而有了它,就能灵活应对不同场景(const
和非const
对象),体现了const
成员函数增加通用性这一点。
Q:这段代码里的 MyClass(int n) : num(n) {} 啥意思
A:在给定的代码中,MyClass(int n) : num(n) {} 这部分是类 MyClass 的构造函数的定义,以下是具体的解释:
构造函数基础概念
void
都不能写)。具体语法含义
MyClass(int n)
:- 这里表明这是
MyClass
类的一个接受一个int
类型参数的构造函数。参数n
就是用来传递初始化所需的值的,在创建MyClass
类对象时,外界可以传入一个int
值给这个构造函数,以帮助完成对象的初始化工作。
- 这里表明这是
: num(n)
:- 这是构造函数初始化列表的语法形式。初始化列表用于初始化类中的成员变量,在创建对象时,用传入构造函数的参数
n
来初始化类中的私有成员变量num
。它的好处是可以直接对成员变量进行初始化,相比于先默认初始化然后再在构造函数体里赋值,有些情况下(比如对于const
成员变量或者引用类型成员变量等)只能通过初始化列表来进行初始化,而且效率上也可能更有优势,因为它少了一次默认初始化然后再赋值的过程。
- 这是构造函数初始化列表的语法形式。初始化列表用于初始化类中的成员变量,在创建对象时,用传入构造函数的参数
{}
:- 大括号内是构造函数的函数体,在这个简单的构造函数中,函数体为空,因为成员变量
num
的初始化已经通过初始化列表完成了,这里不需要额外执行的语句。不过如果有其他需要在对象创建时执行的代码逻辑,就可以写在这个函数体里面。
- 大括号内是构造函数的函数体,在这个简单的构造函数中,函数体为空,因为成员变量
MyClass normalObj(5);
时,就会调用这个构造函数,把参数 5
传递给构造函数的 n
,然后通过初始化列表用 5
初始化 normalObj
对象的 num
成员变量。template<typename T>
是定义函数模板或者类模板时的一种语法结构,它的含义如下:
“template” 关键字
“typename T”
- “
typename
” 在这里是一个关键字,用于指定后面跟着的T
是一个类型参数。它表明T
代表的是一种类型,这个类型在模板被使用时才会确定具体是什么类型(比如可以是int
、double
、std::string
或者自定义的类类型等)。 - “
T
” 就是自定义的类型占位符,它就像一个变量一样,不过代表的是类型。在模板定义的函数体或者类的内部,可以像使用常规的类型(比如int
、char
等)那样去使用T
。例如,在函数模板里,如果T
是int
,那么T
类型的变量就可以进行整数相关的操作;如果T
是某个自定义的类类型,就可以调用这个类的成员函数等。
#include <iostream> // 定义函数模板,T是类型参数 template<typename T> T add(T a, T b) { return a + b; } int main() { // 实例化函数模板,此时T为int类型 int result1 = add(3, 5); std::cout << "整数相加结果:" << result1 << std::endl; // 实例化函数模板,此时T为double类型 double result2 = add(3.14, 2.71); std::cout << "浮点数相加结果:" << result2 << std::endl; return 0; }
template<typename T>
定义了一个函数模板 add
,它可以根据调用时传入的实际参数类型来确定 T
具体是什么类型(如传入 int
类型参数时,T
就是 int
;传入 double
类型参数时,T
就是 double
等),然后生成对应的函数版本进行相应的运算,这样就实现了代码对不同类型的通用性。const
成员函数增加通用性这一点。
#include <iostream> class MyClass { private: int num; public: MyClass(int n) : num(n) {} // 非const成员函数,用于改变对象的值 void changeValue(int newNum) { num = newNum; } // const成员函数,用于获取对象的值 int getValue() const { return num; } }; // 函数模板,既能处理const对象,也能处理非const对象 template<typename T> void processObject(T& obj) { // 先获取值并输出(调用const成员函数getValue,无论是const还是非const对象都能调用) std::cout << "当前值: " << obj.getValue() << " "; // 如果是非const对象,就调用非const成员函数changeValue来改变值 if constexpr (!std::is_const_v<T>) { obj.changeValue(100); std::cout << "改变后的值: " << obj.getValue() << std::endl; } else { std::cout << "(此为const对象,无法改变值)" << std::endl; } } int main() { MyClass nonConstObj(5); const MyClass constObj(10); // 处理非const对象 processObject(nonConstObj); // 处理const对象 processObject(constObj); return 0; }
1. MyClass
类部分
- 有非
const
成员函数changeValue
,用于改变类中成员变量num
的值,它只能在非const
对象上调用,因为它会修改对象的状态。 - 还有
const
成员函数getValue
,用于获取num
的值,const
对象和非const
对象都可以调用它,因为它不会修改对象的状态。
2. processObject
函数模板部分
- 这个函数模板的参数是
T&
,也就是可以接收各种类型(这里是MyClass
类型及其const
限定版本)的引用。 - 首先调用
obj.getValue()
,不管传入的obj
是const
还是非const
的MyClass
对象,都能够正确调用getValue
函数获取当前值并输出,这得益于getValue
是const
成员函数,这体现了const
成员函数对于不同对象(const
和非const
)通用性的一方面。 - 然后通过
if constexpr (!std::is_const_v<T>)
这个条件判断,利用 C++ 17 引入的if constexpr
特性,在编译期判断T
是否是const
类型。如果T
是非const
类型(也就是传入的是普通的、非const
限定的MyClass
对象),那就可以调用changeValue
函数来改变对象的值,之后再输出改变后的值;而如果T
是const
类型(传入的是const
限定的MyClass
对象),则不会执行改变值的操作,只会输出相应提示,因为const
对象不能调用非const
成员函数来修改自身。
const
成员函数的存在使得函数模板等代码能够更灵活地处理不同限定(const
和非const
)的对象,增加了类使用的通用性。- 成员函数调用机制
- 在 C++ 中,当你有一个类类型的对象(无论是
const
还是非const
),通过对象名和点运算符(.
)或者对象引用和箭头运算符(->
)就可以调用成员函数。对于processObject
函数中的obj
,它是T&
类型的引用,只要T
是MyClass
或者是从MyClass
派生的类(这里暂时不涉及派生情况),就可以调用MyClass
类中定义的成员函数。 getValue
函数是MyClass
类中的const
成员函数。const
成员函数可以被const
对象和非const
对象调用。这是因为const
成员函数承诺不会修改对象的内部状态,所以对于const
对象来说是安全的。
- 在 C++ 中,当你有一个类类型的对象(无论是
- 函数模板中的通用性
- 在函数模板
processObject
中,T
是一个模板参数,它可以在不同的调用场景下代表不同的类型。当我们用MyClass
类型(包括const MyClass
)的对象调用processObject
时,T
就被确定为MyClass
(或者const MyClass
)。 - 由于
getValue
是MyClass
类中的成员函数,并且obj
是T&
(这里T
是MyClass
相关类型),所以可以通过obj.getValue()
来调用这个成员函数,获取对象中的值。这就像你有一个普通的MyClass
对象myObj
,你可以调用myObj.getValue()
一样,只是在这里obj
是一个模板参数类型的引用,它的行为和普通对象引用调用成员函数是类似的,只要T
最终被确定为包含getValue
函数的类类型,就可以正常调用。
- 在函数模板
T
和 MyClass
之间的关系:
函数模板中的类型参数 T
template<typename T>
定义的函数模板中,T
是一个类型占位符,它在模板被使用之前,并没有确定具体是什么类型。它可以代表各种各样的类型,比如 int
、double
、std::string
等基本类型,也可以代表像 MyClass
这样自定义的类类型。processObject
函数模板与 MyClass
的关联
template<typename T> void processObject(T& obj)
这个函数模板时,它本身是具有通用性的,可以处理多种不同类型的对象(只要这些类型支持函数模板内部对其进行的操作)。main
函数里,我们有这样的调用:
MyClass nonConstObj(5); const MyClass constObj(10); processObject(nonConstObj); processObject(constObj);
当我们执行 processObject(nonConstObj)
时,编译器会根据传入的实参 nonConstObj
(它的类型是 MyClass
)来自动推导函数模板中 T
的类型,在这里就会把 T
确定为 MyClass
。也就是说,在这个函数调用的场景下,函数模板中的 T
就代表了 MyClass
,obj
的类型就是 MyClass&
(因为 T&
中的 T
被推导为 MyClass
),所以此时 obj
就可以调用 MyClass
类里定义的成员函数,比如 getValue
函数。
processObject(constObj)
时,编译器看到传入的实参 constObj
是 const MyClass
类型,就会把 T
推导为 const MyClass
,那 obj
的类型就是 const MyClass&
,由于 getValue
是 const
成员函数,它依然可以被 const MyClass&
类型的 obj
调用。MyClass
的不同限定情况)来自动推导 T
的类型,进而让函数模板里的代码可以按照对应类型的规则(比如调用对应类型的成员函数等)去操作这个传入的对象。T
就像是一个 “可变的类型标签”,在不同的使用场景下可以代表不同的具体类型,当我们传入 MyClass
相关对象时,它就代表 MyClass
这个具体的类类型,从而能对其按照 MyClass
的特性进行操作了。processObject
函数哈。processObject
函数里,关键就在于它接收的参数 T& obj
这个写法呀。T
是一个模板参数,它在函数被调用的时候,会根据你传入的实际对象的类型去确定自己具体是什么类型。main
函数里,我们有这两行调用:processObject(nonConstObj);
processObject(constObj);
processObject(nonConstObj)
时,因为 nonConstObj
的类型是 MyClass
(非 const
的),那此时函数模板里的 T
就被编译器推导为 MyClass
,那 obj
的类型就是 MyClass&
(也就是一个普通的、非 const
的对象引用),这样 obj
就能调用 MyClass
类里的非 const
成员函数(像 changeValue
),也能调用 const
成员函数(像 getValue
)啦。processObject(constObj)
时,因为 constObj
是 const MyClass
类型,这时候编译器就会把 T
推导为 const MyClass
,那 obj
的类型就变成了 const MyClass&
(也就是一个 const
对象的引用)。对于 const
对象的引用,按照规则,它可以调用类里的 const
成员函数,所以 obj
能调用 getValue
函数,但不能调用像 changeValue
这种非 const
成员函数了(因为非 const
成员函数会改变对象状态,而 const
对象是不允许被改变的)。processObject
函数本身虽然没显式声明 const
呀,但因为它接收的参数类型是通过模板参数 T
根据实际传入对象来推导的,所以就能处理不同限定情况(const
和非 const
)的 MyClass
类型对象了,也就是既能调用 const
成员函数,也能在合适的时候(传入非 const
对象时)调用非 const
成员函数。MyClass
例子里,processObject
函数模板就相当于那个可以处理不同类型对象的函数机制(类似之前说的函数模板能处理不同类型对象的情况)。MyClass
里有 const
成员函数 getValue
,这个函数就好比是那个被 const
修饰的 set_value
函数呀。processObject
函数模板里写 obj.getValue()
这行代码的时候:nonConstObj
(非 const
的 MyClass
对象),我们调用 processObject(nonConstObj)
,此时 obj
就是非 const
对象的引用(因为 T
被推导为 MyClass
),但它依然可以顺利调用 const
成员函数 getValue
,能获取到对象里的值。constObj
(const
的 MyClass
对象),当调用 processObject(constObj)
时,obj
就变成了 const
对象的引用(因为 T
被推导为 const MyClass
),同样也能调用 const
成员函数 getValue
来获取值呀。getValue
这个 const
成员函数,不管 obj
最终代表的是 const
还是非 const
的 MyClass
对象(取决于函数模板调用时传入的实际对象),都能够调用它来获取值,就像有了 const
修饰的 set_value
函数可以在 const
和非 const
对象上调用一样,让 processObject
这个函数模板在处理不同情况(const
或非 const
的 MyClass
对象)时更灵活了,能适应更多使用场景呀,不会因为对象是 const
的就没办法获取里面的值了呢。processObject
函数模板中,因MyClass
类有const
成员函数getValue
,所以不管传入的obj
是const
还是非const
的MyClass
对象(由模板参数T
根据实际传入对象推导类型),都能调用getValue
来获取值,就像const
修饰的函数可在不同限定对象上调用一样,让函数模板使用更灵活、适应更多场景。
- 不是必须。可以用非
const
引用或指针来指向const
对象,但不能通过它们修改const
对象。例如,const MyClass constObj; MyClass& ref = constObj;
是错误的,但const MyClass constObj; const MyClass& ref = constObj;
是正确的。 - 有。在定义对象或者引用、指针时可以用
const
修饰类名来表示这个对象是常量对象,如const MyClass obj;
,这里obj
是const
对象,其内部成员变量(除了mutable
修饰的)不能被修改。 - 是的。
const
成员函数承诺不修改对象的内部状态,所以const
和非const
对象都能调用类里的const
函数。
const
对象实例化时可以不用const
修饰引用它的变量(但有条件)- 当你有一个
const
对象时,最好用const
引用或const
指针来引用它,这样可以保证对象的常量性。但是,在某些情况下,也可以用非const
引用或指针来引用const
对象,不过这是比较危险的做法,并且需要进行类型转换(const_cast
)。 - 例如,假设你有一个
const
对象const MyClass constObj;
,你不应该直接这样做:MyClass& ref = constObj;
,因为这试图用一个非const
引用指向一个const
对象,编译器会报错。但是,如果你使用const_cast
,像这样:MyClass& ref = const_cast<MyClass&>(constObj);
,代码可以编译通过,不过这样就破坏了constObj
的常量性,如果你通过ref
修改constObj
的成员(除了mutable
成员),会导致未定义行为。 - 所以,虽然从语法上可以通过一些手段让非
const
的引用或指针指向const
对象,但这违背了const
对象的设计初衷,是不推荐的做法。正确的做法还是应该使用const
引用或const
指针来引用const
对象,如const MyClass& ref = constObj;
或const MyClass* ptr = &constObj;
。
- 当你有一个
const
对象实例化后,最好用const
来修饰引用它的变量(如const
引用或const
指针),这样符合const
对象不可修改的特性。但如果使用一些强制类型转换(如const_cast
),可以让非const
的变量引用const
对象,不过这是很危险的做法,容易导致错误,正常情况下应该避免。所以在良好的编程习惯中,const
对象最好都用const
修饰来引用。- 引用(就像是对象的别名)
- 想象你有一个人,他有自己的名字。现在你给他起了一个外号,这个外号就相当于引用。当你用外号叫这个人的时候,其实就是在叫这个人本身。
- 在代码里,如果你有一个变量,比如
int a = 5;
,然后你定义一个引用int& b = a;
,这时候b
就像是a
的外号。你对b
做的任何操作,就跟对a
做操作是一样的。比如b = 10;
,那么a
的值也会变成 10。
- 指针(就像是一个小纸条,上面写着对象住在哪里)
- 指针像是一个小纸条,纸条上写着变量在内存中的地址。假如变量是住在房子里的人,指针就是写着这个房子地址的小纸条。
- 例如
int* p;
就像是你有了一个小纸条,但是还没写地址上去呢。如果有int a = 5;
,然后p = &a;
,这时候p
这个小纸条上就写上了a
住的地方(也就是a
的内存地址)。你可以通过这个小纸条找到a
。如果想通过指针改变a
的值,就可以用*p = 10;
,这样a
的值就变成 10 了。不过指针有点复杂,因为你要小心别让小纸条上的地址写错了,不然可能会找不到正确的变量,或者找到不该找的地方,就会出问题。
- const和mutable是不是管能否修改的?这种东西叫什么?修饰符还是啥
- public和private和protect是不是管访问权限的
- 在 C++ 中,
const
和mutable
被称为类型修饰符(也可以称为限定符),const
主要用于声明常量,表明对象或变量的值不能被修改;mutable
用于突破const
的限制,允许被const
修饰的类的成员变量被修改。它们是类型修饰符,用于控制对象或成员的可修改性。 public
、private
和protected
是访问修饰符,用于控制类成员(变量和函数)在类的内部、派生类以及外部代码中的访问权限。public
允许任何代码访问,private
限制只有类内部可以访问,protected
允许类内部和派生类访问。
- 为什么经常看到用
const
创建对象const
声明的变量是常量,不能重新赋值。在创建对象时使用const
可以防止意外地重新赋值对象引用,这有助于维护代码的稳定性。
- 是否所有类创建实例对象时都可以用
const
或不用const
- 是的。用
const
声明类的实例对象,不能将该变量重新赋值为指向其他对象,但可以修改对象的属性。不用const
,变量可以重新赋值为指向其他对象。例如:javascript 代码
class MyClass { constructor() { this.prop = 1; } } // 使用const const myObj1 = new MyClass(); myObj1.prop = 2; // 可以修改属性 // 以下会报错,因为不能重新赋值myObj1 // myObj1 = new MyClass(); // 不使用const let myObj2 = new MyClass(); myObj2 = new MyClass(); // 可以重新赋值
- 是的。用
Q:这段代码C++也成立吗?
A:不成立,这段代码是 JavaScript 语法,在 C++ 中类的定义、对象创建及变量使用等语法规则与之不同。
class MyClass { public: int prop = 1; }; 创建对象可以: // 常量对象 const MyClass myObj1; // 普通对象 MyClass myObj2;
const
修饰的对象(myObj1
),不能通过它调用非const
成员函数去修改对象内部状态(除非成员函数有const
修饰表明不修改对象),而普通对象(myObj2
)可以正常调用成员函数修改内部状态等,并且不存在像 JavaScript 中const
声明变量不能重新赋值为其他对象这种完全一样的概念,它们底层机制和语法设计有较大差异。const
修饰或者不用const
修饰,以下是具体解释及示例:const
修饰实例对象
const
修饰,意味着可以通过该对象调用类的非const
成员函数来修改对象的成员变量等,对象的状态是可变的。#include <iostream> using namespace std; class MyClass { public: int num; void setNum(int n) { num = n; } }; int main() { MyClass obj; obj.setNum(10); cout << "obj的num值为: " << obj.num << endl; return 0; }
在上述代码中,obj
是MyClass
类的一个普通实例对象,通过调用setNum
函数修改了成员变量num
的值,对象的状态发生了改变。
用const
修饰实例对象
使用const
修饰类的实例对象时,这个对象就被视为常量对象,不能通过它调用非const
成员函数(因为非const
成员函数可能会修改对象的状态),只能调用const
成员函数,保证了对象的状态在其生命周期内不会被意外修改。
#include <iostream> using namespace std; class MyClass { public: int num; void setNum(int n) { num = n; } void printNum() const { cout << "num的值为: " << num << endl; } }; int main() { const MyClass obj; // 下面这行代码会报错,因为不能通过const对象调用非const成员函数 // obj.setNum(20); obj.printNum(); return 0; }
const MyClass obj
创建了一个常量对象obj
,尝试调用非const
的setNum
函数会引发编译错误,而调用const
修饰的printNum
函数是合法的,这体现了const
修饰对象在保证对象状态不可变方面的作用。const
来修饰,以此控制对象的可变性。- const_cast
const_cast
是 C++ 中的一种类型转换操作符。它主要用于去除变量的const
或volatile
属性。不过需要注意的是,使用const_cast
去除const
属性后,如果对原本被定义为const
的对象进行修改,可能会导致未定义行为,所以要谨慎使用。- 例如,假设有一个
const
对象,你通过const_cast
去除const
属性后修改它的值,这在语法上可以通过编译,但可能会引发程序运行时的错误。 - 示例代码如下:
class MyClass { public: int num; }; int main() { const MyClass obj; MyClass& nonConstObj = const_cast<MyClass&>(obj); // 下面这行代码虽然语法上可以通过编译,但会导致未定义行为 nonConstObj.num = 10; return 0; }
- const_ex
- 在 C++ 标准中没有
const_ex
这个关键词。它很可能是用户自定义的变量名、函数名或者是一个拼写错误。如果是作为变量名等自定义的标识符,它可以按照普通的变量命名规则来使用。
- 在 C++ 标准中没有
- 参数方面
MyClass(int n) : num(n) {}
- 这是一个带有一个参数
n
的构造函数。n
用于初始化类中的成员变量num
。例如:class MyClass { private: int num; public: MyClass(int n) : num(n) {} };
- 这是一个带有一个参数
- 当创建
MyClass
对象时,如MyClass obj(5);
,这里的5
作为参数n
传递进去,通过初始化列表将num
初始化为5
。 ConfusingExample() : value(0), counter(0) {}
- 这是一个无参数的构造函数。它在初始化列表中初始化了两个成员变量
value
和counter
,都初始化为0
。例如:class ConfusingExample { private: int value; int counter; public: ConfusingExample() : value(0), counter(0) {} };
- 这是一个无参数的构造函数。它在初始化列表中初始化了两个成员变量
- 当创建
ConfusingExample
对象时,如ConfusingExample obj;
,对象obj
的value
和counter
成员变量会被初始化为0
。
-
初始化列表中的变量个数和用途
MyClass(int n) : num(n) {}
- 只初始化了一个成员变量
num
,主要目的是根据传入的参数n
来初始化这个特定的成员变量。
- 只初始化了一个成员变量
ConfusingExample() : value(0), counter(0) {}
- 初始化了两个成员变量。这种方式在对象创建时同时将多个成员变量设置为特定的初始值,通常用于确保对象的初始状态是符合预期的,比如将计数变量初始化为
0
,将状态值初始化为某个默认值。
- 初始化了两个成员变量。这种方式在对象创建时同时将多个成员变量设置为特定的初始值,通常用于确保对象的初始状态是符合预期的,比如将计数变量初始化为
- 可能的用途场景
MyClass(int n) : num(n) {}
- 可以用于根据外部传入的数值来初始化一个核心的成员变量。例如,如果
MyClass
代表一个数学向量类,n
可能是向量的维度,num
就是存储这个维度值的成员变量。
- 可以用于根据外部传入的数值来初始化一个核心的成员变量。例如,如果
ConfusingExample() : value(0), counter(0) {}
- 适合用于初始化对象的多个状态相关的成员变量。比如
ConfusingExample
是一个游戏中的玩家类,value
可能是玩家的初始分数,counter
可能是玩家完成的关卡数,在对象创建时将它们初始化为0
是一种合理的默认初始化方式。
- 适合用于初始化对象的多个状态相关的成员变量。比如
class A { int x; public: A(int n) : x(n) {} };
中A(int n) : x(n) {}
的x(n)
就是初始化列表,用于初始化成员变量x
。value
和counter
初始化为 0。有点理解了感觉class也没多难,之前因为这玩意放弃学Java,感觉好JB难啊,现在发现C++也有,刨根问底的问AI也懂了一些
而且这个counter计数挺有趣的,之前刷题的时候,如果有这种计数,是需要手动写各种标志判断手动写++的
备注:
###:英语:
const:常数
volatile:不稳定的
restrict:限制
mutable:可变的
static:静态的
register:登记
context:背景
###:博客园的编译也太垃圾了,之前、之前,就提过,现在又发现,粘贴完豆包或者其他地方带格式的文本后,整个编辑框就有问题了,无论是光标上下行,还是做修改,滑动来查看编辑框里的内容的时候,编辑框总是乱蹦,真的垃圾。而且贴完带格式的文本后,都看不到一行的最末尾,得光标在那附近移动才能看概率的显示,出现跳来跳去的行,真的恶心。经常出现半拉可及的行,要刷新才行。复制完菜鸟教程里的带格式文本后,总跳行,都找不到自己刚复制完的东西在哪,~~~~(>_<)~~~~,好崩溃啊。有时候左边栏都看不到,右侧末尾的文字也看不到,除了保存刷新外,end貌似也行
###:BUPT导师会问到你说不出话为止,面试官也会一直追问,生活职场中我就想这样却举步稳健无法生存,还好有文心GPT
妈逼的为啥我学点东西咋这么费劲呢,好质疑自己啊
###:一千个人有一千个哈姆雷特,好老师也不一定讲的就完全适合自己,看视频的被动灌输知识,要远远没有自己思考主动去问AI学东西学得快、明白、透彻
AI的好处是:可以不用考虑别人的感受,不用考虑人情世故,专注的去问问题,纯粹的学知识,比如一个代码我有一个问题是:这代码是啥意思?
那么AI解释完,我又产生了新的两个问题,
Ai解释完这两个问题,我又产生了新的5个问题
AI再继续解答,当我最后翻来覆去问了好多问题的时候,比如AI已经解释了abc三个问题,但我对a不懂,就会先放弃bc的解答,去追着问a了,a的解答里又衍生出mn两个问题,问完懂了,现在又去问bc,AI还会继续解答,反复加深理解,而如果是真人的话,就会考虑bc的这两个问题他已经解释过了,问是不是不好,他会不会说“我不是说过了吗?你tm咋还问”,甚至其他一起学的会群起攻之,这对新人很不友好,其实往往想的透彻才会有这么多的问题。所以这就是AI的好处。
###:提前学东西就有种,GTA里作弊器的意思,提前学会开飞机等有任务的时候就直接会了
###:跟刷题最大的区别就是
刷题自己写完,过阵子自己看不懂,自己AC后别人的代码也看不懂,但再写就是可以AC,看难写容易,因为很多变量啥的命名啥的,纯逻辑数字(之前博客里说过)。而如果学新算法,是看懂容易,自己写又会有报错,所以学新算法的时候,在知道算法逻辑的前提下,看容易写难,在看容易前还有个难啃的看懂算法代码
而C++,自己写估计一堆报错,但看懂就容易写,我能理解AI给出的代码,但自己写估计一屁眼子报错,看容易写难
###:我直接把我的问题和豆包AI回答,全按照顺序放过来,最原汁原味。有些是重复的,都是我学的时候最真实的学习历程,贴出来方便自己回忆和加深印象。但还是有点写乱了,下次应该边问AI边写。每次重新看的时候觉得当时自己很蠢就是自己成长了
###:作息