月下博客

[翻译]C++的const声明:原因和使用

转载请注明文章出处:https://tlanyan.me/cpp-const-declaration-why-and-how
原文链接:http://duramecho.com/ComputerInformation/WhyHowCppConst.html

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没有实际作用。