C++ const 关键字详解

前言

const 就字面意思而言,最重要的一点就是 不可变,这也是它的主要用途。本文将用通俗的语言讲解 const 的用法以及一些注意事项:


1. const 修饰变量

1.1 引入

假设在一个程序中,我们用到了 12 这个数字,它表示一年有 12 个月。你可以直接使用 12,但是当 “别人阅读你的程序” 又或者 “一段时间后你再阅读此程序”,可能会纳闷:12 表示什么?时钟的一圈为 12 时?还是 12 生肖?又或者是一年有 12 个月?显然这样的代码让人费解。
此外,如果有一天一年有 13 个月,那么所有用到 12 的地方(并且表示的是 一年有 12 个月的 “12”)都要被修改,不仅工作量较大,而且可能导致你修改了错误的地方。

那么我们可以将这个特殊的数字用一个变量 per_year_months 来保存。

int PER_YEAR_MONTHS = 12;

这时当别人看到 per_year_months 时,马上就反应过来:这是每年的月份数,是不是可读性更强一些。
但仍然还有瑕疵:如果你在程序的某处不小心修改了它的值,这会导致未定义行为,使得程序可能出现逻辑错误。
对此我们 期望它是一个常量,并且编译器能阻止任何试图改变常量的行为。下面有两套方案:

  • 宏定义 #define
    • 语法:
      #define var_name var_value
    • 作用:
      在预处理阶段,编译器将 var_name 替换为 var_value。

需要注意的是 #define 仅仅是 简单的文本替换

#define PER_YEAR_MONTHS 12

在预处理阶段,'PER_YEAR_MONTHS' 被文本替换为 '12'
并不是将 '12' 赋值给 'PER_YEAR_MONTHS''PER_YEAR_MONTHS' 并不是变量 (左值)

虽然 var_name 不占用内存,但是由于仅仅是文本替换,编译器并不会对其进行类型检查。
c++ 是强类型的语言,编译器通过类型检查,能帮助我们规避代码中潜在的一些问题,因此 此方案不建议使用

  • const 限定符
    • 语法:const type var_name;
    • 作用:定义一个 type 类型的常量 var_name,var_name 值不可变(编译时,运行时都不可变)。
const int PER_YEAR_MONTHS = 12;

// 以下均错误:试图修改常量的值
PER_YEAR_MONTHS = 100;	
PER_YEAR_MONTHS += 2;

虽然 const 变量占用内存,但是编译器会进行类型检查。
此外,使用此方案还有一个好处:倘若有一天 PER_YEAR_MONTHS = 13,那么我们只需要修改

const int PER_YEAR_MONTHS = 13;

即可。

1.2 必须初始化

const 修饰的变量必须初始化:编译时初始化运行时初始化

c++11 引入的 constexpr 关键字为 编译时初始化。

const int a;	// 错误:a 未初始化

int size() { //... }
const int b = size();	// 正确:运行时初始化

const int c = 1; 	// 正确:编译时初始化

int d = 1;
const int e = d;	// 正确:能用 非const 初始化 const
int f = e;   //正确:能用 const 初始化 非const

1.3 默认限定文件内有效

在一个源文件中定义的 const 常量,该常量只能在该文件中使用,其他文件无法访问(也就相当于它的作用域为当前文件)。
如果在不同文件中定义了同名的 const 常量,等同于在不同文件中定义了独立的常量。这就好比:

int fic1()
{
	const int a;
}

int fic2()
{
	const int a;
}

尽管 fic1、fic2 中都定义了 a,但是两个 a 由于作用域不同,因此它们相互独立,互不相干。

1.4 extern 关键字

有时我们需要 const 常量能够在不同文件之间使用,那么 extern 就派上用场了。
假如我们需要多个文件共享常量 A,可以在头文件中声明

// const.h 
extern const int A;	  // 声明

在对应的源文件中定义

// const.cpp
#include "const.h"
const int A = 1;

// 或者
extern const int A = 1;
// 合法,但是 extern 多余

2. 对常量的引用

可以将引用绑定在 const 对象上,该引用需要加上 const 修饰,表示 不能通过此引用修改所绑定对象的值,也就是所指的对象不可变

引用本身就不能改变,不能将 const int& p = a; 理解为 因为 const ,所以 p 不可变。
同时由于引用不是对象,所以“常量引用” 这一概念严格意义上说并不存在,但是有的地方仍然使用这一概念,实际上代指 “对常量的引用”。

const int a = 1;
const int& a1 = a;	// 正确:不能通过 a1 修改 a 的值
int& a2 = a;   // 错误:试图将非const引用指向const对象

int b = 1;
const int& b1 = b;	//正确:但是不能通过 b1 修改 b 的值
int& b2 = b;   // 正确:可以通过 b2 修改 b 的值

对于 int& a2 = a,我们也可以从语义上来理解为什么它是错误的:

首先 a 为 const 对象,也就表示 a 不可修改
然而 a2 却是非 const 引用,也就表示我们可以通过 a2 修改 a 的值
出现了矛盾,故错误。


3. 指针 与 const

3.1 指向常量的指针

const 指针类型 var_name;
表示 指针指向的对象不可变,但是指针可以改变

const int a = 1;
int b = 1;
const int* ptr = &a;  // 正确
*ptr = 2;  // 错误:试图改变所指对象的值
ptr = &b;	// 正确

在看下面的常量指针,可能读者会容易混淆二者,这里说说作者的一种方法:
类型解析从右到左

类型解析:

const int* ptr = &a;

1. '*':所以 ptr 为指针
2. const int*ptr = const int,
   即对指针解引用为 const int
3. 综上:ptr是指向 const int 的指针

3.2 常量指针

指针类型 const var_name;
表示 指针不可变,但是可通过指针修改所指的对象

int a = 1;
int* const ptr1 = &a;	// 正确
*ptr1 = 2;	// 正确:a = 2

const int b = 1;
int* const ptr2 = &b;	// 错误

类型解析:

int* const ptr2 = &a;

1. const:说明 ptr2 不可变 (常量)
2. *:ptr2 是指针 (常量指针)
3. int*ptr2 = int
4. 综上:ptr2 为指向 int 的常量指针

对于 int* const ptr2 = &b; 可以从语义上理解:

b 是 const int,然而 ptr2 仅仅是常量指针,也就是说能够通过 ptr2 修改 b 的值
这与 b 不可修改相矛盾,故错误

那么怎么可以使得其正确呢?这就是下面介绍的:

3.3 指向常量的常量指针

前两中的混合,所以表示 指针不可变,指向的对象也不可变

const int a = 1;
const int b = 1;
const int* const ptr2 = &b;	 // 正确
*ptr2 = 2;  // 错误
ptr2 = &a;  // 错误

看完上面的部分代码,你可能会注意到:对于 const 修饰的对象,当发生拷贝(赋值)时,有时候会报错,这是为什么呢:


4. 顶层 const 与 底层 const

这两个概念,更多的是出现在指针上:

  • 顶层 const:表示指针本身是个常量
  • 底层 const:表示指针指向的对象是个常量

更一般的:

  • 顶层 const 也适用于其他数据类型,表示 该对象本身是个常量
  • 由于引用也类似指针,因此引用存在 底层 const
int a = 0;
int *const p1 = &a;		// 常量指针:顶层 const
const int* p2 = &a;		// 指向常量的指针:底层 const
const int b = a;		// 自身不可变:顶层 const
const int& a1 = a;		// 所指的对象不可变:底层 const

对常量的引用都是底层 const,因为它表示该引用所指的对象不可变

对于执行拷贝操作,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般的可以理解为:非const 能转为 const,反之不能),顶层 const无影响。

// 1. 变量
int i = 0;
const int ci = i;  // 正确:都无底层 const,顶层 const 没有影响
int a = ci;	// 正确:都无底层 const,没有影响
	
// 2. 引用
int& r1 = i;  // 正确:r1 与 i 都不是底层 const
int& r = ci;  // 错误:r 不是底层 const,但是 ci 是,并且 const int& (正确情况下 r 的类型) 不能隐式转为 int& (r 的实际类型)
const int& r = ci;	// 正确:r 与 ci 都是底层 const

// 3. 指针
int* p1 = &i;	// 正确:p1 与 i 都不是底层 const
int* p2 = &ci;	// 错误:p2 不是,ci 是, const int* 不能转为 int*
int *const p3 = &i;	  // 正确:都不是
const int* p4 = &i;	  // 正确:虽然 p4 是,i 不是,但是 int* 可以转为 const int* 

5. const 成员函数

类的成员函数可以用 const 修饰,表示 这个函数内部不能改变对象的所有数据,可变数据成员 (mutable data member) 除外。

class A {
public:
    void fic() const  // const 成员函数
    {
        var++; // 正确:可以修改可变数据成员
        sta--; // 错误
    }

private:
    mutable int var;  // 可变数据成员
    int sta;
};

6. const 形参与实参

6.1 顶层 const 的影响

在函数传递参数时,如果形参有 const 修饰,就需要注意 顶层const 带来的影响。

在形参中,顶层const会被忽略

都被忽略了,还能有什么影响?

void fic(const int x) { }
void fic(int x) { }

在 C++ 中,当函数同名不同参时,这样的函数是被允许存在的(重载函数),但是对于上面的两个函数会报错:重复定义 fic(int)。
这是因为 const int 为顶层 const,被忽略掉,因此 fic(const int) 等价与 fic(int)
此外对于 fic(const int),当实参去初始化形参时,顶层 const 被忽略,所以以下语句均正确

int i = 0;
const int ci = 0;
fic(i);
fic(ci);

6.2 底层 const 的影响

前面我们知道:一般来说,const 不能隐式转为 非const。

const_cast 可以显式地将 const 转为非const

因此对于函数

void fic(int& arg) { }
void fic(int* arg) { }

非 const 引用 称为 普通引用

下面的语句都是错误的

const int i = 0;
fic(i);  // 不能将普通引用绑定在常量i上
fic(1);  // 不能将引用绑定在字面值上

const int* p;
fic(p);  // 不能用指向 const int 的指针初始化 int* 

【建议】尽量不使用 对非 const 的引用


7. 其他

当然有的教程也会写到 const 可以修饰函数返回值,我个人认为这跟前面修饰变量,引用,指针等没什么区别,就只是表明返回值不可修改,拷贝时仍然会受到底层 const 的影响


最后

可以看出,const 的主要作用告诉编译器其值不能修改

本文参考 《C++ Primer》 以及 一些网上资料,同时添加自己的一些理解,如有错误,欢迎指正。

  • 30
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值