const是c++当中很有特色的一个语言功能,它限制了对数据的操作,还限制了类成员函数的行为,而且是对c++的引用类型的函数参数和返回值这个功能的有益补充,c++程序员应该时时有意识地正确使用const关键字。const对于变量来说,是一个要求,而对于函数来说,是一个承诺,对它所操作的变量的承诺。由于const的使用场合和使用对象很多,要正确地使用它还是需要用一番心思的。
const在一下一些地方使用:
首先它可以修饰一个变量,此时这个const是这个变量的一个要求——它不可以被修改,所以这个变量必须在初始化的时候被赋值,初始化之后就不能赋值了,是一个只读的变量,见本文末尾的例子(2)。这意味着它不可以以任何方式被赋值,包括不能做赋值表达式的左值,同时不可以把它传递给那些没有承诺不更改它的值的函数。
那么函数怎么做这个承诺呢,看第五段。如果一个int类型变量i是const的,那么它的地址&i也是const的,从(3)中可以看出,&i只能赋值给const的指针变量p而不可以赋值给q,因为我们可以通过赋值给*q而更改它所指向的变量i. 从这里我们可以看到,上述声明表示我们不可以通过p修改它所指向的变量的值,也就是说*p也是一个常量了,所以 *p = 5 这样的表达式是错误的(4)。要注意的是, p作为一个变量,它自身是可以修改的(6),p的声明当中的const要求不可以通过p更改它所指向的值,是p对它所指向的变量的承诺。当它所指向的变量不需要这样的承诺时,p自然仍然可以指向那个变量(6),但是(7)仍然是错误的,因为p有承诺不可以改变它所指向的变量的值。另外,如果我们想要求p指针变量自身也不可以修改,那我们应该如(9)中所做,并且注意此时r必须在初始化时候赋值,否则它就不可能再被赋值了(10)。而从(11)中我们可以看出,指针变量k要求它自身是只读的,但是没有承诺不可以通过它来修改它所指向的变量,因而不可以把&i赋值给它。
当一个对象实例被const修饰后,它自身的状态在初始化之后就不可以修改了,这时候不仅上面的约束必须成立,而且我们不可以调用它的成员函数,因为它的成员函数可能会修改它的状态,除非那个成员函数承诺不会修改this对象的状态。而承诺的方式至少是在函数签名末尾加上"const",如(12)所示。而如果这个函数要返回一个非mutable的数据成员的引用的话,那么返回类型必须加上const表示那个返回的引用不可以被用于修改对象的状态,这样这个成员函数才完整地承诺了它不会被用于修改对象的状态(13),否则,就不可以说它是一个常量成员函数(14)。事实上,如果一定要返回数据成员的引用(比如拷贝构造的代价很大时),那么强烈建议返回const引用,以便禁止修改这个数据成员,否则将严重违反类的封装性,这种违反可能带来暂时的编码方便性,但是长期来看,一定会吃苦头的。
注意在(12)中,虽然返回了数据成员message_,但是我们返回的是它的一个copy而不是那个对象自身,所以get_message()函数不可能被用于修改message_数据成员,而(12.1)错就错在它返回了message_的引用。
那么什么是一个对象的状态呢,默认是这个对象的所有数据成员,但是在有一些情景下,一个对象的某些成员变量可能对于它的状态没有决定作用,这完全决定于类的设计者。如果类Foo中有这样的成员变量的话,我们需要把它声明为mutable,表示即使一个Foo的实例是常量,我们也可以修改这个成员,同理,类 Foo的那些承诺不修改对象状态的函数也可以修改这个成员变量(12)。类当中也可以有const的数据成员,它也像独立的const变量一样,完全不可以以任何方式修改它的值,所以它必须在类的构造函数的初始化列表中被初始化,在构造函数体中初始化是不可以的(15)。
那么一个独立的函数或者类的成员函数怎么承诺不会修改传入的变量自身呢?对于诸如(16)这样的函数,它们不会面临这个问题,因为所有的参数都是在传值,在(17)例子中,实际参数m, n传入函数add后,它们的值分别被赋值给其形参a和b,之后两个实际参数m, n就与函数add无关了,自然add不会修改到m和n的值。真正需要面对这个问题的是那些传递引用的函数,这里和下文的引用是广义的引用,包括& 修饰的引用类型变量,以及通过传递指针值来“引用”到实际参数本身的情形,如例子(18)。 当一个函数的参数中有某个参数是在传递引用,那么,如果这个函数确实不需要修改那个实际参数本身的值,那么一定要把那个参数声明为const的,这样,不仅可以传入可读可写的变量作为那个实参,而且可以传入一个const变量,因为我们已经承诺了不会修改那个实际参数本身的值(19)。在函数参数中传递引用常常是很有效的——如果对象拷贝构造的代价很大,或者要操纵实际参数自身——但是一定要在可能的时候,声明引用为const的。const在类中还有一个用处——用于定义静态常量数据成员,如例子(20)。
在设计类的时候,必须考虑清楚哪些数据成员应该是const的,初始化后就不可以再修改;哪些数据成员是mutable的,对对象的状态定义没有影响;哪些成员函数不会修改对象的状态,应该声明为常量函数,不会修改对象状态;那些函数不会修改引用参数的值,可以是const的。在应该和可以使用const 的地方一定使用它。
const的效果可能被一下一些语言功能所抵消:1. const_cast<> 2. C风格的强制类型转换。在设计良好的代码中,我们要非常小心地使用这种抵消const功能的语言功能,特别是第2种,应该被禁用。例如,类的使用者看到函数引用参数的const属性,会放心地把自己的常量数据传入函数,却想不到这个承诺是假的。这样的bug非常难调试。再比如,一个const函数返回了一个const的数据成员的引用,但是这个函数的调用者却使用1或者2方法抵消了const的作用,那么这个对象的状态改变很难追踪,这样的代码是非常危险的代码,应该坚决地避免使用。在使用某些旧的c语言代码时候,我们可能不得不做 第2类强制转换,这是程序员应该予以足够的特殊标注和注释。
在最新的C语言标准(c99)中,const也被引入了,可以修饰函数的参数和返回值,以及变量,使用方法与上面相同,新的C标准库也同步做了更新,所以在C语言中,也是要在可以使用const的地方一定使用它,原因也同上。
(1) const int i = 3; class Foo { string& get_message() const; × (12.1) const string& get_message_ref() const (13) string& get_message_ref() (14) Foo() : my_name("david"){} (15) }; // Foo int add(int a, int b); (16) int m = 6, n = 7; (17) void negate(int& a); (18) |