理解常量const
常量const通俗理解
首先先讲一讲常量,为什么要使用常量,因为不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量。
C++ 语言可以用const
来定义常量,也可以用 #define
来定义常量。但是前者比后者有更多的优点:
- const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
- 有些集成化的调试工具可以对
const
常量进行调试,但是不能对宏常量进行调试。
所以本文就针对const
修饰的常量以及其他的类型进行探讨。
在C++的学习过程中,不管是初学者,还是略有了解的人,对const
关键字的理解总是或多或少遇到一些问题。本文不妨采用一种通俗的理解方式,另辟蹊径来和大家分享一下理解思路。
先说说技巧,怎么区分const
修饰的是什么: 理解的时候,在定义或声明中跳过或者忽略掉数据类型(int,char,double等),const后面的内容即为修饰的内容。
比如:
普通类型:
|
|
指针类型:
|
|
当然还有其他的类型等,放到后面再说。
const修饰的普通变量
变量名的本质:一段连续内存空间的别名。
比如:
定义了 :int p = 10;
则系统会分配4个字节的内存,这块内存我们命名为p,其地址是不确定的,这里我们假定是0x2222,于是在p生存的周期内,这块从0x2222开始的地址的内存空间就被命名为p,p = 10 即向这块内存内写入10。
对于普通变量,const
修饰时,仅仅改变的是变量的属性,即将原来变量的可读写性变成了只读性。
可以这么理解:普通变量加const
修饰是为了防止变量被修改。
但有个例外:如果该变量为全局变量,则不能通过指针修改,但如果是局部变量,则还是可以通过指针修改的。
原因是:全局变量在全局静态区,内容不能被修改,但局部变量本身仍在栈区,可以使用指针修改。
|
|
默认情况下,const
对象被设定为仅在文件内有效。当多个文件中出现了同名的const
变量时,其实等同于在不同的文件中分别定义了独立的变量。所以如果想要在多个文件中共享const
对象,则对于const
变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了。
const 与指针
利用最前面分析的方法,可以知道const
修饰指针变量有三种情况:
①const
修饰 *
号
②const
修饰变量
③const
既修饰 *
号又修饰变量
只要掌握了前两种情况,最后第三种自然明了。
先说说const修饰 * 号,该情况为(局部变量):
|
|
当const
修饰*号时,表示不能通过指针p_a修改p_a指向的内容。这句话有3层意思(对应上述代码三种情况):
①当p_a指向a时,不能通过指针p_a取*号修改a中的内容
②a中的内容仍可以由a自身修改
③p_a指向可以改变,即p_a可以指向b
可以这么记忆:* 号是指针用来对指向的内存操作(读写)的,当对 * 加了`修饰,意味着 * 号的操作只剩下只读的功能,也就是只能使用 *号来读取指针指向的内容,而失去了写的特性,这点与普通变量的情况类似。
再说说const修饰变量的情况:
|
|
当const
修饰变量p_a时,表示不能改变p_a的内容,即改不了指针的指向。这句话有3层意思(对应上述代码三种情况):
①当p_a指向a时,能通过指针p_a取*号修改a中的内容
②a中的内容仍可以由a自身修改
③p_a指向不可以改变,即p_a不可以修改,指向b或者其他变量
最好的理解便是:指针变量也是变量,变量名的本质:一段连续内存空间的别名。理解好了这句,也就明白了,const
修饰了这个变量,也就是修饰了这块内存空间,使得这块内存空间的可读写性改为了可读性,也就是指针指向固定,无法更改。
看到这里,如果前面的都了解了,那么你就会明白了用const修饰,实际上就是将对应的可读写改成了只读性质。
那么,最后一种情况就是,const
既修饰 * 号,又修饰变量的情况了:
|
|
如果明白了前两种情况,那么最后这种情况就好理解了(对应上述代码三种情况)
①const
修饰*号限定了指针对内存的操作只能为只读
②const
修饰变量p限定了指针的指向,p不能指向其他变量
③a或b本身的变量可以自身修改。
至于为什么必须初始化,如果理解了上述内容,那也就明白了(因为只读)。并且在初始化的过程中,利用一个对象去初始化另一个对象,则它们是不是const
都无关紧要,因为拷贝一个对象的值并不会改变它。说下不初始化的情况,编译器(我用的VS2017)仍能通过,但有警告,p没有明确初始化,并不知道其指向哪里,由于该变量既不能修改指向,又不能修改指向的内容,所以此时该指针也就没有作用了,这种做法是不好的。
const 与引用类型
可以把引用绑定到const
对象上,就像绑定到其他对象身上一样,我们称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
|
|
因为不允许直接为ci 赋值,当然也不可以通过引用去改变ci,因此对r2的初始化是错误的,假设该初始化合法, 就可以通过r2来改变其引用的值,这显然不正确。
“常量引用”是“对const
的引用“,严格来说,并不存在常量引用,因为引用不是一个对象,我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上来看所有的引用又都算常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
引用的类型必须与其所引用的对象保持一致,但是有两个例外:第一个是初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转化成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式。
对于以下代码:
|
|
此处ri
引用了一个Int
型的数,对ri
的操作应该是整数运算,但dval
是一个双精度浮点数而非整数。因此为了确保让ri
绑定一个整数,编译器将代码变成了以下形式:
|
|
这种情况下,ri绑定了一个临时量对象,所谓的临时量对象就是编译器需要一个空间来暂存表达式的求值结果临时创建的一个未命名的对象。
对const
的引用可能引用一个并非const
的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。
指针、常量和类型别名
针对上述的情况,特别举一例子作为讲解
先看下面的代码
|
|
根据编译器报错的提示信息,你可能猜到了在(2)式中的指针变为了常量指针,而(1)没有改变。那为什么会造成这种情况呢,这得从编译器的角度来说明变量的申明顺序问题。
首先,先针对声明语句进行剖析一下。
(a) c和c++中声明包含零个或者多个声明说明符,和一个声明符 例如:
|
|
|
|
一个声明符就是被声明的名称,可能还伴有操作符,如*,(),[],&等,以本例为例,*表示x是一个指针,[]表示序列, *x[N]表明x是含有N个指针的序列。 可能基础比较弱的人一直分不清楚指针数组和数组指针,以及返回指针的函数和函数指针怎么写。其实与运算符一样,操作符也是有优先级的,在本例中[]的优先级要比*的优先级高,所以首先其是一个序列,然后才声明序列中的元素是指针。而(*x)[N]中()的优先级比[]的优先级高,所以首先确定了其是一个指针。
(b) 声明说明符包含类型说明符和与类型无关的说明符,分清楚这个对于理解例子中的两个例子为什么会出现不同的情况至关重要。注意在(1)式中char是类型说明符,而const是与类型无关的说明符,所以1)式中const并不会修饰变量data,应该理解为指向const char的指针序列,而不是指向char的const指针序列。。
(c) 声明说明符在一个声明中出现的顺序并不重要,如:
|
|
这两者实际上是等价的,但是要注意对于指针的声明是从右到左来看的,所以对于在对指针的声明说明符中,正确的写法应该将const放在类型说明符的右边。如:
|
|
上面的式子中表示指向const T的指针,而下面的式子修饰了操作符*则表示指向T类型的const指针,说以对于最开始的问题中的(2)按照正确的写法应该是:
|
|
这样对照刚刚的讲解,现在是不是就能理解为什么data1序列中的指针为const类型了。
constexpr
constexpr
是 C++11 引入的,一方面是为了引入更多的编译时计算能力,另一方面也是解决 C++98 的 const
的双重语义问题。
在 C 里面,const
很明确只有 「只读」 一个语义,不会混淆。C++ 在此基础上增加了 「常量」 语义,也由 const
关键字来承担,引出来一些奇怪的问题。C++11 把 「常量」 语义拆出来,交给新引入的 constexpr
关键字。
看如下代码:
|
|
可以看到,f 和 g 都有一个 const int x
,但它们的行为却不同。原因在于:f 的 const int x
只是「一个只读的变量」;而 g 的 const int x
既是「一个只读的变量」,又是「一个值为5的常量」,变得飘忽不定。
在 C++11 以后,建议凡是 「常量」 语义的场景都使用 constexpr
,只对 「只读」 语义使用 const
。
可能有人就会糊涂了,只读变量 难道不就是 常量吗?然则非也。
同样一个内存地址,用常量指针关联时,通过这一路径就无法修改;换用非常量指针 关联时,在这条路径上就是可以修改的。
换句话说,用 const
限定变量时,只是剥夺了通过该变量修改相应内存中内容的可能性,但是有可能其他程序或其他指向该内存地址的变量会改变这块内存中的内容,也就是说这块内存地址空间的内容并不会保证一直不变。所以,从现在开始,就把const
理解成只读的限定符,把constexpr
理解成常量的限定符
参考文献
https://blog.csdn.net/rlyhaha/article/details/80397227
https://blog.csdn.net/JayFan_Ma/article/details/82942903
https://blog.csdn.net/qq_40416052/article/details/82655736
https://blog.csdn.net/qq_40399012/article/details/84069983
https://blog.csdn.net/love_gaohz/article/details/7567856
(43条消息) C++ const 和 constexpr 的区别?_Sunny_Jie的博客-CSDN博客_const与constexpr的区别