const in C++(1)




前言


  有时候我们需要定义这样的一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处在于,当我们觉得缓冲区的大小不再合适的时候,可以通过修改变量的值来调整缓冲区的大小。而我们又不希望程序的其他部分不一小心修改了这个变量的值。为了实现这一要求,我们可以使用关键词const来对变量类型进行限定。



一、CONST限定符


  const是一个C语言(ANSI C)的关键字,具有着举足轻重的地位。它限定一个变量不允许被改变,产生静态作用。使用const在一定程度上可以提高程序的安全性和可靠性。
                                  ------------------------------摘自百度百科“C中的const”
  一个变量在定义的时候加上const关键字以后,就说明这个变量不能被赋值,不能被修改。
const int x = 7;
//加上const限定符之后,以下操作都是非法的
x = 9;			//illegal
x++;			//illegal

  需要注意的是,const类型变量对于C++来说仍然是变量,而不是常数。 既然是变量,编译器就会在内存中给它分配相应的空间,常数则是在编译过程中由编译器记录在内存表中的一个实体。



二、CONST在C++中的使用



1、初始化与CONST


  由于const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
const int i = get_size();			//正确,运行时初始化
const int j = 42;					//正确,编译时初始化
const int k;						//错误,k是一个未经初始化的常量

  众所众知,对象的类型决定了其所能进行的操作。const类型的对象能实现大部分非const类型的对象所能实现的操作,主要的限制在于只能在const类型的对象上执行不改变其内容的操作。 例如,const int类型的对象和int类型的对象一样能参与算术运算,也都能转换成一个布尔值。

  值得一提的是在初始化操作中,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要

下面看一段代码:
int i = 72;
const int j = i;
int k = j;

  在本例中,尽量j是整型常量,但j变量保存的内容还是一个整数。更具体的说,变量j的常量特性仅仅在执行改变j的操作时才有效。而当用j来初始化k时,仅仅是将j保存的整数值拷贝给k,没有进行改变常量值的操作,此时的j完全可以当做一个整型变量来使用。

2、CONST与编译


  默认条件下,const对象仅在文件内有效。我们知道,我们所写下的代码,一个.c/.cpp源文件要成为可执行文件.exe(这是对于Windows而言,在Linux/Unix下则生成.out文件),要经过编译和链接两大步,C++支持分离式编译(separare compilation),编译以一个.c/.cpp文件为基本单元,每个文件独立编译。

   假设我们以这样一段代码定义缓冲区的大小:
const int BufSize = 1024;

  当以编译时初始化的方式定义一个const对象时(形如const int j = 42即在编译时初始化,而类似于const int i = get_size()这样的初始化语句则在运行时进行初始化),就如对BufSize的定义一样。编译器在编译过程中将用到该变量的地方都替换成相应的值,更具体地说,编译器会找到代码中所有用到BufSize的地方,将其替换成整型字面值常量(literal)1024。

  为了执行上述替换,编译器必须知道变量的初始值,这也可以作为const类型变量必须初始化的原因去理解。默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

  如果需要一个const变量在文件间共享,而不希望编译器为每个文件分别生成独立的变量,解决的办法是,对于const变量不管是定义还是声明都添加extern关键字。   
//file_1.cpp
extern const int bufSize = fcn();
//flie_1.h
extern const int bufSize;

在file_1.cpp中定义并初始化了bufSize,在file_1的头文件中的声明中由extern进行了限定,指明bufSize并非本文件独有,它的定义将在别处出现。


3、CONST与#define


  在上述的说明中,我们提到过,当以编译时初始化的方式定义一个const对象时,编译器在编译过程中将用到该变量的地方都替换成相应的值,这很容易让我们联想到编译预处理中的宏定义#define

下面以百度百科中的一个例子解释二者的区别:
#define PI 3.14159	
const double pi = 3.14159;//此时并未将Pi放入ROM中
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝。


4、CONST与引用


  可以把引用绑定到const对象上,称为对常量的引用(reference to const),与普通引用不同,对常量的引用不能用作修改它所绑定的对象。

const int cv = 1024;
const int &cr = cv			//OK
cr = 2048;					//Error:不允许通过对常量的引用来修改它所绑定的对象
int &r = cv;				//Error:不允许让非常量引用绑定一个常量引用


  我们也可以这样理解:假设让非常量引用绑定一个常量的操作是合法的,那么就可以通过该非常量引用来修改该常量的值,这显然是错误的。

  我们知道,引用的类型必须与它所绑定的对象类型相同,但是也有例外。我们允许在初始化常量引用(对const的引用,严格来说,不存在常量引用,因为引用不是一个对象,C++中不允许修改引用所绑定的对象,所以从这层意思上说,引用本身就是常量)时用任意表达式作为初始值,只要这个表达式的结果能转换成引用的类型即可。例如,允许一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。

int i = 42;
const int &r1 = i;			//OK
const int &r2 = 42;		 	//OK
const int &r3 = r1 * 2;		//OK
int &r4 = r1 * 2;			//ERROR,r4是一个普通的非常量引用

我们从底层来理解为什么可以进行这种操作
看下面一个例子:

double value = 5.33;
const int &r = value;		//OK,由于1)r是一个常量引用 2)value能转换成r引用对象的类型


  此处,value是一个双精度变量,r是一个对int类型对象的引用,为了能确保让r绑定一个整数类型的对象,编译器对上述代码进行了如下处理:

const int tmp = value;		//由双精度浮点数生成一个整型临时变量
const int &r1 = tmp;		//令r1绑定临时变量tmp


  在这种情况下,r1绑定了一个临时量对象(temporary)tmp。所谓临时量对象,是当编译器需要一块空间来暂时存放表达式运算结果时建立的未命名对象。由于r1是一个常量引用,无法通过常量引用来改变绑定对象的值,因此这种操作也视为合法。

  下面讨论为何只允许在初始化常量引用可以用任意表达式作为引用对象。当一个一般的引用绑定与它类型不相同的对象时,实际上也由编译器新建一个临时量,将引用绑定该临时量。由于这是个非常量引用,这就允许通过引用改变所引用对象的值,而实际上通过该引用改变的是绑定的临时量的值,也不是我们原意要让它绑定的对象的值,因此这是一个无效引用,C++也将其视为非法(illegal)


  常量引用只是对引用可参与的操作做了限制,对const的引用不一定是绑定常量对象,因此,在常量引用绑定非常量对象的时候,允许通过其他途径修改绑定的对象。

我们看下面一个例子:

int a = 1024;
int &r1 = a;
const int &r2 = a;

r2 = 2048;				//Error,不允许通过常量引用来修改绑定对象的值
r1 = 2048;				//OK,由于绑定对象a是非常量对象,所以允许通过其他方式修改a的值



5、CONST与指针


  与C语言相同,指针变量加上const限定符之后有两种情况,一种是该指针是常量,即该指针所保存的地址是常量,指向一块内存空间后不能再指向其他内存空间了。一种是指针所指的对象是常量,即指向常量的指针(pointer to const),我们不能通过指针来修改指针指向的对象的值。


  我们来看下面这一段代码:

double v = 3.14;
const double cv = 5.67;
double *p = &cv;			//Error
const double* cp = &cv;		//OK
cp = &v;					//OK

*cp = 5.28;					//Error
cp++;						//OK

  1)我们定义了一个double类型的变量v和一个const double类型的变量cv,由于*指针的类型必须与其所指对象的类型一致*,所以普通指针p指向cv的操作是错误的,而这里有一个特殊情况,允许一个指向常量的指针指向一个非常量对象,所以你可以看到将指向const char类型的指针cp指向变量v也是正确的。
  和常量引用一样,指向常量的指针也没有规定其指向的对象必须是一个常量。所谓的指向常量的指针,只是规定无法通过指针p来修改指向对象的值,而对于指针指向的对象没有要求,可以指向常量,也可以指向一般的变量,在指向一般变量的情况下,无法通过该指针来改变变量的值,但仍然可以通过其他方式来改变变量的值。
  指向常量的指针只是无法通过指针来修改指针指向的对象的值,其他功能与一般的指针相同。
double * const q = 6.12;
*q = 10.13;			//OK
q++;				//Error

  2)第二部分是一个常量指针(const poiner) 与引用不同,指针是对象,这就允许指针作为一个常量出现。常量指针必须初始化,一旦初始化之后,指针的值(指针指向的对象的地址)就不允许改变,所以对指针赋值,指针自增自减操作都是错误的。但是允许通过解引用符(*)来修改指针所指的对象的值,所以可以看到 *q = 10.13也是正确的。   
const double *a;
double const *b;
double * const c;
const double * const d;

  3)最后我们来区别一下三种写法的区别。这段代码中,a、b是指向const类型的指针,只是第二种写法会比较少见,c是指向double类型变量的常量指针。具体的区分方式是,(*)在const前面的是常量指针,(*)在const后面的是指向常量类型的指针。我们也可以自右往左地来分析,以对象d为例,首先d与const类型结合,说明d是一个常量,再与解引用符(*)结合,说明d是一个指针,声明符的剩余部分是const double,说明指向的对象是double类型的常量,即d是一个指向double的常量的常量指针。



总结

本文介绍了C++中const与引用、指针等操作,编译器是如何处理const,const变量的初始化以编译预处理及#define与const的区别,关于C++中顶层const与底层const以及constexpr将在 “const in C++(2)”中展开说明。

参考资料:

  《C++ Primer》(第五版)

  《面向对象程序设计-C++》ZJU-翁恺

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值