const修饰符, 顾名思义是修饰某个东西为“常量”,不允许修改. 在C++中, 有很多地方需要用到const修饰符. 它不仅是一个良好的书写习惯, 在某些情况下, 它甚至是必要的. 这篇文章试图涵盖C++中所有const的出现情况.
目录
- 1 const与变量定义
- 1.1 普通变量
- 1.2 指针
- 1.3 引用
- 2 const与函数
- 2.1 形参
- 普通
- 指针
- 引用
- 2.2 返回值
- 2.3 案例: 一些特殊函数
- 比较运算符重载
- iostream的输入输出
- 2.1 形参
- 3 cosnt与类
- 3.1 属性
- 3.2 方法
- 3.2.1 方法的const
1. const 与 变量定义
1.1 const 与 普通变量
这是最初级、最直观的用法: 普通变量的值可以变, 而被const修饰过的普通变量的值不能变. 由此引申出一个特性, 被const修饰的变量必须在声明时立刻初始化.
下文基本上都用int
作为普通变量的代表.
它原则上可以直接替换为任何基本变量、结构体、类.
1 | /* 1.1.1 无修饰的普通变量 */ |
1.2 const 与 指针变量
我想我不用详细描述指针的作用. 指针相当于两层, 指针本身存储地址, 这是一层; 存储的地址对应一个变量, 这是第二层.
由于指针的概念有两层, const的修饰也有两层: 是第一层, 指针存储的地址不能变呢? 还是第二层, 指针指向的变量的值不能变呢?
从这两个维度出发, 可以绘制这样的表格:
指针的地址 通过指针访问的变量的值 | 能变 | 不能变 |
---|---|---|
能变 | int* |
const int* |
不能变 | int* const |
const int* const |
我觉得上表已经很简明的描述了这四种指针的定义以及主要特点. 读者可以对照下面的使用例来判断自己理解是否正确.
1 | /* 1.2 const与指针 */ |
1.3 const 与 引用
引用是C++有别于C的一个特性. 由于引用本身也很有话题, 这里不做过多介绍, 只是简单的说明一下引用与const的关系.
可以将引用粗略的理解为指针. 和指针不同在于: 1. 首先,
它不需要*
运算符就能直接取得其所引用的变量的值,
因此引用相当于其所引用的变量的“别名” 2. 其次, 它必须在声明时初始化,
之后不能再修改其所引用的是哪个变量.
所以类似指针, 也能画出一个二维表格, 不过, “引用的是哪个变量”这件事, 不论对何种引用而言, 都是不能修改的. 因此这个表格事实上只有一行.
引用的是哪个变量 通过引用访问变量的值 | 能变 | 不能变 |
---|---|---|
不能变 | int& |
const int& |
1 | /* 1.3.1 普通引用无法引用const修饰过的变量 */ |
需要格外注意的是: const int&
有一个非常特殊的用法: 它可以直接引用一个字面量.
这是指针和无const修饰的引用都没有的特性. 在接下来的内容中,
读者将看到这种类型的重要性.
1 | const int& k = 3; // 正确 |
这里不得不说明一下: 以下每一行内的几种写法是完全等价的:
1 | int *p;int* p;int * p; |
简而言之, *
, &
这样的修饰符,
紧挨着谁都是一样的. 而笔者主要使用第一种写法.
2. const 与 函数
2.1 const 与 函数形参
这部分内容可以直接由上面关于变量的介绍平移而来. 下文推测读者已经基本了解C++函数参数传递的机制.
2.1.1 普通类型形参
1 | void fun1(int a){ |
由于参数传递机制,这两个函数都可以接收普通和const修饰的int变量
2.1.2 指针形参
1 | void fun1(int* a){ |
这四个函数都可以传入普通变量的地址. 普通变量的地址被拷贝如函数后, 会被自动加上const修饰.
而只有fun2和fun4可以传入const修饰的变量的地址.
为什么不能传入fun1和fun3?可以这样理解:
编译器: 这个变量本身是const保护的, 传给函数之后, 这个保护就没有了! 在fun1和fun3中, 是允许通过指针a修改变量的值的, 而这个值本身却是const保护的! 这怎么办, 算了给个error吧.
即: 在传递时, 允许加强const条件, 不允许丢掉const条件(这很直观).
其中, fun2是格外有用的. 如果你 不想在参数传递时拷贝原有数据的值 , 又希望函数 以只读的方式访问某个变量 ,fun2是一个好选择.
2.1.3 引用形参
1 | void fun1(int& a){ |
类似的, 两个函数都能传入普通变量. 而fun1不能传入带const修饰的变量.
此外,十分重要的: fun2可以传入字面量! 这很特殊
其中, fun2是格外有用的. 如果你
不想在参数传递时拷贝原有数据的值 , 又希望函数
以只读的方式访问某个变量 , 还希望
函数内部不要出现指针的*
和->
操作,
调用函数不要出现取地址&
操作 , 还希望它
能传入字面量 , fun2是一个好选择.
2.2 const 与 函数返回值
这在某些场合是有点莫名其妙的命题. 比如这样
1 | const int fun(){ |
对函数返回值传递机制稍有了解便能知道, 这个const是完全无用的.
但在某些情况它会不同, 比如:
1 | int* fun(){ |
就有不同了. fun()返回的指针可以修改所指位置的值, 而fun2()返回的指针不能.
类似的, 对于引用, 有:
1 | int& fun(){ |
它们也是不同的. 不过这两个函数单看其实有些奇怪. 我们之后会在 类 那一部分更深入的探讨它们的区别和各自运用.
2.3 案例: const 在几种特殊函数中的作用
在2.1.3, 我提到了:
如果你 不想在参数传递时拷贝原有数据的值 , 又希望函数 以只读的方式访问某个变量 , 还希望 函数内部不要出现指针的
*
和->
操作, 调用函数不要出现取地址&
操作 , 还希望它 能传入字面量 , fun2是一个好选择.
这种情况会在什么时候出现呢? 没错, 那就是——
(函数体外的) 运算符重载
运算符重载有两种, 一种是写在函数体(准确的说是结构体或类定义)外,没有打括号包裹的, 另一种是定义在类内部的.
这里着重举两个例子.
以这样一个结构体为例
1 | struct Int{ |
2.3.1 类(或结构体)的比较运算符重载
1 | bool operator<(const Int& a,const Int& b){ |
这是比较规范的运算符重载写法. 注意这里的const Int&
,
需要const修饰.
如果不加const,只保留&,那么这个运算符将不能处理右值(可以理解为常量和字面量);
如果不加const,也不保留&,那么调用运算符会进行值的拷贝, 这在Int是一个复杂的数据类型时, 会凭空增加内存消耗, 也很耗时.
2.3.2 输入输出重载
1 | std::ostream& operator<<(std::ostream& out,const Int& a){ |
这是比较规范的输入输出流重载写法.
注意在输出时需要加const修饰(否则无法输出字面量),
而输入时不能加const修饰(否则无法修改).
这里也能看出引用&
符的巨大作用.
3. const 与 类
3.1 const 与 类的属性
基本和变量完全一致.
3.2 const 与 类的方法
比如这样一个类:
1 | class string{ |
方法和函数在很多方面是类似的. 它也有参数的const, 和返回值的const. 这里着重介绍另一种: 方法的const.
3.2.1 方法的const
为什么需要方法const? 因为对象可能被const修饰了.
比如, 假设string有一个方法size:
1 | class string{ |
对于一个普通string对象, 我们可以执行size().
然而, 对于一个 加了const修饰的string对象 , 还能执行size()吗? 对于一个const修饰的对象, 其中的所有属性都自动增加了const修饰. 然而, 系统不知道这个size()方法会不会修改这些被加了const修饰属性. 因此, 对于const string对象, 系统不会允许执行这个size()方法, 会报错.
1 | string s; |
然而, 事实上这个size()方法并不会修改对象的属性. 我们的确想让const string对象也能执行size()方法. 因此, 我们要给size()对象增加const修饰, 就像这样:
1 | class string{ |
这样, 对于const string对象, 也可以正常执行size()方法了.
这里, const修饰就是告诉编译器: 这个方法内的所有代码都不会修改当前类的属性值.
const修饰的方法有以下几个特性:
- const修饰的对象只能执行const修饰的方法
- const修饰的方法内部只能执行别的同样被const修饰的方法
- 没有const修饰的对象可以执行const修饰的方法
- 允许存在一个无const修饰一个有const修饰的两个同名方法. 程序会自动根据对象有没有const选择执行哪一个.
Remark: 关于特性4, 有以下的例子:
1 | class array{ |
Remark2: const保护并不代表万无一失. 对上面的例子稍作修改:
1 | class array{ |
程序会完全正确的运行. 因为,
const只保护了data指针, 没有保护data指向的那片空间. 因此,
它是允许你修改诸如 data[0]
的内容的. 但是,
这并不符合我们的预期. 我们给一个array增加const修饰,
当然是想让里面的值不被修改.
因此, 如果你想确保const修饰过的类按你的预期不被修改, 你需要 自觉做到 在const修饰过的方法中不去修改相关的值. 没人能帮你检查.
这个const修饰还有一个常见的应用:
类内部的运算符重载(尤其是==
的重载). 请看:
1 | class test{ |
这里会报一个warning:
ISO C++20 considers use of overloaded operator ‘==’ (with operand types test’ and ‘test’) to be ambiguous despite there being a unique best viable function.
这是为什么呢? 我的理解是,
相等运算==
左右两边应当是对等的.
而这个重载的==
运算, 由于没有加const修饰,
左右两边变得不对等了. 由于参数加了const, 而方法没有加const,
这意味着进行运算时, 有一方的属性可能被修改,
而另一方的属性标记了不会被修改. 这是不对等的.
这只是我的粗浅理解. 总之, 给这个方法增加const修饰, 这个warning就消失了.
以上是我目前能想到的全部内容.