[翻译]C++的const声明:原因和使用
const
关键字是C++众多杂乱特性中的一个。
概念上它是直观的:const
修饰的变量变成常量,程序不能修改其值。然而它是C++缺失特性的一个简单粗暴的解决办法,并因此导致它变得非常复杂,在使用上有时还有让人不爽的限制。接下来的部分将尝试解释const
如何使用(How)及其存在的原因(Why)。
const
的简单使用
最简单的使用方式是声明一个命名常量。这在C++/C及衍生语言中都可用。
具体方式是像声明变量一样声明一个常量,只是在前面加上const
。你必须马上对它进行初始化,因为以后不能更改。例如:
const int Constant1=96;
这会创建一个整数常量Constant1
,并赋值96。
对编译后值不应该更改的参数,这样的常数是很有用的。相对于C语言的预处理宏#define
,其在源码送到编译器前只做简单的文本替换,const
的优势在于能被编译器理解和运用,如此以来错误信息将对开发人员更有帮助。
const
也能用在指针上,但必须谨慎选择const
的位置,因为这决定了指针还是其所指的值是常量。例如:
const int * Constant2
声明了Constant2
是一个指针变量,其指向一个整数常量。
int const * Constant2
是另一种等价的声明方式。而
int * const Constant3
声明Constant3
是一个指向整数的常量指针。此外
int const * const Constant4
声明Constant4
是指向一个整数常量的常量指针。原则上说,const
的修饰范围是紧邻的左侧(如果左侧无内容则修饰右侧)(译注:这请结合声明的“顺时针螺旋法则”理解这句话)。
const
用在函数返回值
指针和const
可能的组合中,在值可变但地址不变的情形下常量指针很有用。
更有用的是指向const
值的指针(指针可以是常量,也可以不是)。函数返回常量字符串和数组时这很有用,因为它们本来就是用指针实现的,如果没有const
,程序修改时将崩溃。有了const
,编译时会检测到修改不可变常量的行为并阻止,避免程序运行时崩溃及难以定位的情况。
例如,一个返回Some text
字符串的函数定义如下:
char *Function1() { return “Some text”;}
如果程序突然尝试修改其值,那么就会崩溃:
Function1()[1]=’a’;
函数改写成如下,那么编译器将抛出错误:
const char *Function1() { return "Some text";}
因为编译器知道函数返回值是不可变的。(当然,C++编译器理论上会推算出,但C编译器就没那么聪明了。)
混乱点 – 参数传递
当一个子例程或者函数被调用,传入数据的参数变量被读取,传出数据的参数变量被写入,或者既读取也写入。部分编程语言允许开发人员指定传入还是传出,例如使用in:
, out:
和 inout:
提示参数类型,然而在C中,开发人员更偏底层,从而要指定参数的传递方式以及数据流动方向。 例如,一个这样的子例程:
void Subroutine1(int Parameter1) { printf("%d",Parameter1);}
其以值传递方式接收参数,这是C和C++的默认方式。所以子例程能读取传递过来的参数值,但是无法修改,因为对形参的修改在子例程结束后就丢弃了。例如:
void Subroutine2(int Parameter1) { Parameter1=96;}
调用这个函数不会让实参修改成96。 C++中加一个&
(这个符号的选择很具有迷惑性,因为C中&
放在变量前会变成指针!)在参数前,这回让实参本身用在子例程里,所以它的值可以被更改并且能把数据从子例程中带回来。因此
void Subroutine3(int &Parameter1) { Parameter1=96;}
会把实参的值修改成96。这种将变量本身传递的方式在C++中叫做“引用传递”。
这种传递变量本身的方式是C++对C的补充。原本的C语言中,要传递一个可修改的变量,要用另一种函数调用方式。这种方式用“指针”作为参数,然后修改其所指向的值。例如:
void Subroutine4(int *Parameter1) { *Parameter1=96;}
能达到目的,但要求函数内的所有参数修改都这样做,并且调用函数时要传递指针。这是非常麻烦的。
但const
怎么就混入这里了?好吧,有一种不使用副本,而是按引用传递数据或者用指针的常见情形。那就是拷贝副本会浪费空间或者耗时太久。尤其是大的或者复杂的用户自定义数据类型(C中的结构体以及C++中的类)。一个声明如下的子例程
void Subroutine4(big_structure_type &Parameter1);
使用&
可能是因为函数会修改变量的值,也许只是为了节省拷贝时间。当函数编译在其他人的库时,根本没办法知道是哪种。确信子例程不会修改函数的值,这个假定回带来风险。 为了解决这个问题,const
可用在参数列表里。例如:
oid Subroutine4(big_structure_type const &Parameter1);
这让变量按引用传递避免了拷贝,同时防止它在函数内被修改。这个做法有点让人觉得混乱:就因为要让编译器做一些优化,把一个可双向修改的参数就变为只读的传入参数。
理想的情形,开发人员不应该控制参数传递的细节,而应该只指定数据方向,剩下的让编译器自动优化。但由于C被设计成一门可运行在低端计算机上的低级编程语言,开发人员不得不显式指定参数传递细节。
依然更混乱 – 用在OOP中
在 面向对象编程里,调用一个对象的“方法”(面向对象对函数的称呼)回带来额外的复杂度。和参数列表中的变量一样,类方法可以直接访问对象的成员变量。例如一个普通类Class1
定义为:
class Class1 { void Method1(); int MemberVariable1;}
Method1
方法没有显式参数,但是调用这个方法可能会修改MemberVariable1
,如果Method1
碰巧这么做的话,例如:
void Class1::Method1() { MemberVariable1=MemberVariable1+1;}
解决方案是在参数列表后放一个const
,像这样
class Class2 { void Method1() const; int MemberVariable1;}
这会禁止Class2
中的Method1
做任何尝试修改其对象的成员变量行为。
如果有时需要结合const
的不同用法,这可能会带来困惑:
const int*const Method3(const int*const&)const;
这里的5个const
用法分别表示:函数返回的指针内容不能被修改,返回的指针不能被修改,方法不会修改参数数据,也不会修改参数指针,以及这是一个不会修改对象内容的方法!
const
的不便处
除了困扰人的const
语法,还有阻止程序干活的问题。
我的程序常常要为运行速度优化,让我我特别恼火的一个点是,一个声明为const
的方法不能修改对象的隐藏属性,而修改这些属性在外界看来并没有导致对象发生改变。这包括为后续调用省时而存储耗时计算的临时结果。因为const
,要么把临时结果返回给调用方存储然后下次传回来(混乱),或者下次从头再算(低效)。后来版本的C++增加了mutable
关键字解决这个问题,但是它完全依赖于开发人员只用在这个目的。所以如果你的程序用了其他人包含mutable
的类,你不能保证mutable
真正让对象为常量,而这会导致const
没有实际作用。
然后你又不能简单的避免在类方法中使用const
,因为const
具有传染性。例如一个const
对象,以const &
方式作为参数传递,那么只能调用显式声明为const
的方法(因为C++的调用系统太简单,不能推算出不显式声明为const
但实际不改动数据的方法)。因此不改变对象的类方法最好声明为const
,以便它们在声明为const
的情形下可以调用。后来版本的C++,声明为const
的变量或对象可以使用const_cast
转成可变,这和mutable
的手法一样简单粗暴,也会让const
没有实际作用。
本站声明:网站内容来源于网络,如有侵权,请联系我们https://www.qiquanji.com,我们将及时处理。
微信扫码关注
更新实时通知