const可能是C++最受欢迎的描述字了。他提供了更好的安全语义。很多企业在面试的时候经常抛出诸如“请谈谈const的用法”之类的题目。
const描述字允许我们提供一个语义约束:声称其被描述的对象具有“不该被改动”的性质,令人感到兴奋的是,编译器会强制实施这项约束。同时,他也允许告诉编译器和其他程序员,某些值应该保持不变。const多才多艺,在许多语境下可以用到他:变量、指针、对象、参数、返回值甚至函数本身。现在把const在C/C++中的用法作一个总结。
1.C中的const
在C中的const主要用于描述变量、指针以及函数声明,我们一步步来。
1.1 变量
以下是const修饰变量时的常用用法:
- const int SIZE = 1024;
- const double PI = 3.1415926;
<o:p></o:p>
他为程序引入一个常量,以防止出现太多的“Magic Number”,同时也是为了能够方便的维护。在C中,我们也可以用#define来达到同样的目的:
- #define SIZE 1024
- #define PI 3.1415926
不过其中存在一些微妙的区别,以至于我们强烈推荐使用const而不是#define来引入一个常量。原因如下:
#define语句是一个预处理命令,他在编译器对源代码进行编译之前,就把所有的“名称”用“值”所替换了。所以,我们的程序中,其实并没有包括SIZE或PI这样的字,而是1024和3.1415926,因此,也更加不会有SIZE或PI的类型信息。所以,概括来说,用#define来引入常量有两点缺陷:
1. 我们所使用的名称没有被引入符号表(symbol table);
2. 他抛弃了名称的类型信息;
第一点会在调试、维护阶段添不少麻烦。可以说,用#define仅仅屏蔽了“开发期”的Magic Number。而第二点,部分的放弃了C++蕴含的严格的类型检查机制所带来的种种益处。综合考虑,我们强烈推荐在C/C++中用const替代#define。
此外,还有一个传说中的“the enum hack”,用一个无名的enum来引入常量。其用法是:
<o:p> </o:p>- enum { SIZE = 1024, MAX = 4096 };
从某方面来讲,enum与#define有些相似,有时候这是必须的。取enum或者#define常量的地址是不合法行为,但取const常量的地址就是合法的。如果你不想别人取得你常量的指针或者引用,同时你也不希望与#define带来的缺点进行某种“妥协”,那么enum会是一个很好的选择。我们应该认识enum hack的动力还来自于,很多现存的C++代码都大量的使用了这个技巧,我们应该认识他们。
1.2 指针
指针从某方面来说也是一种变量,所以上一小节所述同样也适用。不过还是有两点要注意:
首先指针蕴含两方面内容:指针本身以及所指对象,可以针对这两部分分别使用const描述字:
- char greeting[] = “hello”;
- char * p = greeting; //non-const ptr, non-const data;
- const char * p = greeting; //non-const ptr, const data;
- char * const p = greeting; //const ptr, non-const data;
- const char * const p = greeting; //const ptr, const data;
同时,还应该注意到,const char *和char const *并没有什么不同,个人习惯罢了。虽然变化多端,却并不高深莫测:星号右边的[1]const描述指针本身;星号左边的const描述所指对象。
第二点,由于常量指针经常用于引入常量字符串,所以我们在头文件中经常看见上述用法。不过,这并不是一个好想法。如果我们的想法确实是引入字符串常量,请尝试使用:
<o:p> </o:p>- const std::string GREETING(“hello”);
const最具威力的用法就是在函数声明时的应用。
关于const参数,没有什么特别新颖的观念。唯一需要注意的就是,尽量在不会被修改的参数前面加上const。只不过多打6个字母,却可以在以后很长时间内,省下那些恼人的错误。这也是C++中为数不多的,很“便宜”的好处,尽量的使用吧!如果还想深入一点,就需要注意以下问题:由于种种原因[2],利用const来区分“需要改动”和“不需要改动”的参数,并不是一种“万能公式”。幸好,经过我们精心修改以后的“公式”,在大多数场合都是适用的:
1. 如果参数是内置类型,请使用Type *和Type来加以区分;
2. 如果参数是用户自定类型,并且具有较复杂的构造-析构函数,请使用Type &和 const Type &来加以区分;
关于返回值,也差不多这样:返回一个内置类型的时候,按值返回就好了;当返回一个用户自定义类型时,最好采用const Type。原因是:良好的自定义类型应该避免无端的与内置类型不兼容。而改动内置类型返回值是不合法的,所以,我们也最好为我们自己的类型提供这个“安全锁”。这样,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
2.面向对象中的const
这一章我们从前面一章的内容过渡到成员函数上,然后最后讨论以下有关bit const和logical const的相关概念。
2.1 过渡
第一章的内容大部分都可以适用于这一章。但有两点需要注意:
首先,关于常量初始化的问题。因为const变量一旦定义后即不可更改,所以在global域或者namespace域[3]中使用const常量,通常(也必须)把声明和定义放在一起。在面向对象部分则不能这么干。因为类定义不可以对数据成员进行初始化(虽然有些编译器允许),所以就要另外找个地方对const数据成员进行初始化。构造函数?不。按照构造函数的语义,他是在已经“创造”了一个对象后,才进行的一些初始化操作。而按照用户自定义类中的常量语义,他应该在此之前就存在。所以应该(事实上也必须)在成员初始化列表中对数据常量进行定义。
- class A {
- const int MAX_SIZE;
- int* arr;
- public:
- A(void)
- : MAX_SIZE(1024),
- arr(new int [MAX_SIZE] ) {
- for(int i=0; i
- arr[i] = i;
- }
- ~A() {}
- void print(void) {
- for(int i=0; i
- std::cout << arr[i] << std::endl;
- }
- };
这样做有着清晰却复杂的语法。所以另外一种妥协方式就是“the enum hack”:
cpp 代码
- class A {
- enum { MAX_SIZE = 1024 };
- int* arr;
- public:
- A(void)
- : arr(new int [MAX_SIZE] ) {
- for(int i=0; i
- arr[i] = i;
- }
- …
- };
简洁明了,这也是为什么现有代码大量使用这种技术的原因。不过他也有个显见的缺点:不能引入除整型以外其他类型的常量。
2.2 const成员函数
const还可以修饰类的成员函数。
像函数参数一样,类的成员函数也可以划分两类:修改数据成员和不修改数据成员。当我们声明一个成员函数的时候,编译器会默认其为修改数据成员的函数,我们称这种为non-const成员函数(non-const member function),而与之对应的称为const成员函数(const member function)。同时,C++规定,对于用const声明的类对象如:
<o:p> </o:p>- const A test_A;
不能调用其non-const成员函数,而只能调用const成员函数。也就是说如果我们不采取一些措施来通知编译器,那么按默认规则,test_A将不能调用任何成员函数(构造和析构函数除外)。
- const A test_A;
- //Error!
- //const object can not call non-const member function;
- test_A.print();
改善C++程序效率的一个根本办法是以const Type &来传递对象,而这技术可行的前提是可以调用const对象的const成员函数。所以在设计类的成员函数的时候,我们就一定要明确区分那些是const成员函数,哪些是non-const成员函数,并且在const成员函数的参数列表和分号之间,使用const描述字,例如class A中:
cpp 代码
- class A {
- …
- void print(void) const {
- for(int i=0; i
- std::cout << arr[i] << std::endl;
- }
- };
- int main(void) {
- const A test_A;
- test_A.print(); //OK!
- return 0;
- }
同时注意,如果成员函数的常量性不同可以被重载。例如:
cpp 代码
- class A {
- …
- void print(void){
- for(int i=0; i
- std::cout << arr[i] << std::endl;
- }
- void print(void) const {
- for(int i=0; i
- std::cout << arr[i] << std::endl;
- }
- };
- int main(void) {
- const A const_A;
- A non_const_A;
- const_A.print(); //const version print();
- non_const_A.print(); //non-const version print();
- return 0;
- }