C++——关键字const

前景小故事

  最近重温《C++ Primer》时被一位朋友看见。也许因为这位朋友太闲了,他强行凑过来要与我讨论一些零零散散的C++内容,其中就有这篇文章的主角——C++中的const关键字。
  起初我内心是拒绝的:const是什么值得讨论的东西吗?不过最后我还是被他的热情打败,一番交流后得出了下面的内容。
(希望有缘点进来看到这里的读者可以花一点时间看正文内容并给笔者一些反馈,这是笔者第一次发布文章,需要各位的建议让以后的文章比现在的能看)

正题

  在C++中,const绝大多数地出现是为了作为限制符限制变量的行为。被const修饰的变量称为常量(总觉得这句话有语病),最集中的一点体现是:常量需要在定义时初始化,至于是否必须显式初始化这个问题,笔者本机g++测试的结果是:内置类型(如int)在函数体外定义(此时内置类型会被默认初始化)会被编译器报错,而自定义的数据结构(拥有默认构造函数)则可以被编译通过。这个问题笔者认为不必深究,在使用常量时都用显式初始化即可,因为实在是想像不到常量使用默认初始化的必要情况。
  最简单的常量定义(以内置类型int为例)如下:

const int ivar = 777;

  定义后,ivar将在其生命周期内永远地(如果没有什么意外的话)作为777的代言,编译器会检测在ivar生命周期内对其所代表的内存进行非法改动的行为。试图对它直接赋值的语句将会被报错:

ivar = 888; 
//error: cannot assign to variable 'ivar' with const-qualified type 'const int'

  对于const的用处,最常被摆上台面的例子之一是:定义一个名为buf_size的常量用于表示需要定义的缓冲区大小:

const size_t buf_size = 1024;
/* 其他代码 */
//缓冲区定义
char *buf[buf_size] = {0};

  之后对缓冲区的操作如果出现需要使用缓冲区大小这一信息的情况时可以直接使用buf_size常量,保证需要修改缓冲区大小时不需要修改多个地方。人们称以上技巧为非硬编码。

与预处理变量的区别

朋友的朋友指出,C++11中有用于定义常量表达式的关键字constexpr,不仅完全替代了预处理变量的功能,还比后者更安全,所以宏定义永远不再应该被用来定义预处理变量了,故以下内容仅作了解。

  在C语言中经常能看见大量的宏定义。宏定义经常会被用来定义一些预处理变量,如:

#define BUF_SIZE 1024

  源程序添加以上语句后,在编译前的预处理阶段该语句以下的BUF_SIZE都被替换为1024。比如:

//预处理前源程序
char *buf[BUF_SIZE] = {0};
//预处理后等价于
char *buf[1024] = {0};

  C++继承了C语言的预处理机制,也就是说C++也可以使用宏定义定义预处理变量。在C++11前,为了表示空指针经常会使用到定义在中的NULL正是预处理变量。
  如上面说的,宏定义参与的是编译前的预处理过程,看起来与文本替换有点相似。于是预处理变量与const修饰的变量最大差别出现了:聚焦上面的例子,预处理变量BUF_SIZE在编译前完成了替换的工作,到了编译阶段编译器根本不知道1024曾经是一个预处理变量而把它当作一个字面值处理;而const修饰的变量buf_size是会参与编译过程并分配到作为变量应有的内存的。抛开繁琐的细节简单地说:预处理变量参与预处理过程而const修饰的变量参与编译过程;预处理变量并不是真正意义上的变量而const修饰的变量是。
  根据上面的总结,预处理变量似乎比const修饰的变量好用:没有多余定义变量的指令。想下定论前笔者认为还有两点需要考虑到:首先是前者仅是一个替换的过程,比起有指定类型且参与编译过程的后者可能会有着更多的安全问题;如果常量不是简单的内置类型(如字符串)且在多处被使用,前者每次都需要申请临时空间而后者只需要申请一次即可。
  笔者的见解是,在所需常量为内置类型且有足够把握不会出现安全问题的情况下使用预处理变量,而其他情况使用const修饰的变量比较好;甚至如果闲麻烦可以全部都使用后者。

术语解释:顶层const与底层const

  按照《C++Primer》里的说法,顶层const(top-level const)指变量本身是不可改变的,前文例子中buf_size即是顶层const;而底层const(low-level const)与以指针为代表的复合类型所指向的值有关,如:

const int *pi = &ivar;

  此时可以说pi是底层const。

指针与const

  以下通过例子列举了const在指针中的使用:

const int cval = 7;
int val = 777, val2 = 7777;
const int *pi1 = &cval;       //pi1是一个指向int常量的指针,且确实指向了常量
const int *pi2 = &val;        //pi2是一个指向int常量的指针,但其实指向的并不是常量
*pi1 = 9;                      //错误,不允许更改指向了const的指针所指向的值
pi1 = &val;                    //正确,允许更改pi1的指向
*pi2 = 888;                  //错误,不允许更改指向了const的指针所指向的值
int *const pi3 = &cval;       //错误:pi3是一个指向int的常量指针,但却指向了常量
int *const pi4 = &val;    //pi4是一个指向int的常量指针,且确实指向了int变量
pi4 = &val2;                //错误,不允许更改常量指针的指向
*pi4 = 999;                //正确,允许更改指向非const的指针所指向的值
const int *const pi5 = &cval; //pi5是一个指向int常量的常量指针

  通过上面可以看出,本身是常量时,指针不允许修改自身的指向;而当所指向的值类型被const所修饰时,不能修改指针所指向的值。前者即为顶层const的含义,后者为底层const的含义。最后一个例子指出指针可以同时是顶层const以及底层const。

引用与const

  引用在普通情况是不可以与字面值、表达式等绑定的,但如果引用加上const后就可以达到这一目的了。下面是const引用的一些例子:

int i = 7;
const int ci = 7;
const int &ival1 = i;          //ival1绑定了一个int变量,但不能通过ival改变i的值
const int &ival2 = ci;        //ival2绑定了一个int常量
const int &ival3 = 777;    //ival3绑定了一个字面值
const int &ival4 = ci + 1; //ival4绑定了一个表达式
int &ival5 = 777;            //错误,引用不能绑定字面值
int &ival6 = 777;            //错误,引用不能绑定表达式

  上述例子ival1、ival2的情况都是可以预见的,至于ival3与ival4是因为编译器会将其做如下变换(ival3为例):

const int temp = 777;
const int &ival3 = temp;

  也就是说,最终ival3绑定的是一个存放了777的临时变量。
  由于顶层const是针对变量而言的,所以不能被称为变量的引用并不存在顶层const。
  上面的例子也表明了,无论是指针也好引用也好,身为底层const时它们会认为与之绑定或其指向的变量为顶层const,所以对变量的值进行修改是非法的。而实际上那个变量并没有一定被const修饰,只是它们自以为的罢了。

  以上就是我们讨论的const的内容了。事实上const还有其他用处和比较特殊的用法在这里并没有提到,主要还是对变量定义的const作用展开了一些讨论和记录。如果有机会其他的用法会穿插在其他的C++讨论中。

你可能感兴趣的:(C++——关键字const)