C++中const关键字的功能总结
C++语言是C语言的升级版,它支持更多的语法形式,用起来更加方便,功能也更加强大。本文尝试分析C++中针对const关键字进行的改进。
在C语言中,const关键字仅用于修饰指针类型的变量,最常见的例子就是strcpy函数了: char *strcpy(char *strDestination, const char *strSource );
。这里const的作用是防止指针所指的内容(源字符串)在函数内被改变。
在C++中,const可以用来修饰所有类型的变量了。图为C99的所有变量类型:
const修饰非指针型变量
之所以C++中支持让const修饰非指针型变量,是因为C语言中的宏的弊端。在C语言中,无参宏被大量使用的同时,留下了许多隐患。例如:
#define NUMBER 0xFFFFFFFF
这个宏定义,无法说明NUMBER的类型,所以不同的情况可以有不同的解释(例如整形、浮点型、有无符号等)。由此产生的隐患不会在编译过程发出警告中,而是往往在软件的使用过程中造成异常。
不仅如此,C语言中也没有方便的方法来限定宏的作用域(只能用#undefine
和#define
的组合,来使一个宏在指定区域内无效),宏一旦被定义,它的作用域就是整个文件剩下的部分。
C++的设计者希望程序员用const修饰变量的语法,来代替之前大量使用的无参宏。这样做的好处:
- 有类型的说明了,编译器现在知道如何去解释它
- 有作用域的限制了,作用域规则和普通的变量一样
现在我们在定义一些常量的时候,就可以避免歧义了。同时为了和宏的用法统一,我们习惯于把const修饰的变量名称全部大写,例如:
#include "stdafx.h"
#include <string.h>
#include <iostream>
using namespace std;
//传统C语言宏定义
#define NUMBER 0xFFFFFFFF
//C++的const修饰变量定义方法
const int NUMBER_1 = 0xFFFFFFFF;
const unsigned int NUMBER_2 = 0xFFFFFFFF;
int main(int argv, char* argc[])
{
//有歧义的写法
cout << NUMBER <<endl;
//无歧义的写法
cout << NUMBER_1 << endl;
cout << NUMBER_2 << endl;
return 0;
}
程序的运行结果如图所示:
既然NUMBER_1是一个变量,那么可不可以通过指针的强大功能来突破const的限制,修改NUMBER_1的值呢?答案是不能的,通过下面的代码可以验证:
#include <string.h>
#include <iostream>
using namespace std;
int main(int argv, char* argc[])
{
const int NUMBER_1 = 2;
* (int *) &NUMBER_1 = 3;
cout << NUMBER_1 << endl;
return 0;
}
程序输出如图所示,NUMBER_1的值依然是2,并不是3,没能骗过编译器:
通过单步调试,可以发现:NUMBER_1的确在栈中有4字节的空间,而且* (int *) &NUMBER_1 = 3
这行代码的确改变了此空间内的值,如图所示:
那为什么程序依然显示2呢?原因是编译器在编译的过程中,自动把NUMBER_1提前都换成数字2了,所以程序的cout << NUMBER_1 << endl;
一句,已经变成了cout << 2 << endl;
,尽管程序运行过程中NUMBER_1的值发生了改变(突破了const的语法限制),但这个改变发生在代码cout << NUMBER_1 << endl;
的改变之后,并不会对程序结果造成影响。这一做法和C语言中的宏相似,区别是宏的作用时间是预处理阶段,而const变量的作用时间是编译过程中(预处理阶段之后)。
PS:此实验只能在debug版本中实现,在release版本中const修饰的变量在栈中是没有地址的(这个说法可能不对,待确定)。
综上所述,在C++中,我们完全可以用const修饰变量来替代无参宏。
附:尝试通过传递引用的方式修改const变量NUMBER_1的值。其实和修改指针是一个原理,都是表面上看上去成功了,但是本质上无法突破const的限制,因为代码替换工作(类似于宏)发生在NUMBER_1被改变之前。
#include "stdafx.h"
#include <string.h>
#include <iostream>
using namespace std;
void fun_fail(int n)
{
cout << "fun_fail " << n << endl;
}
void fun_success(int& n)
{
cout << "fun_success " << n << endl;
n = 4; //尝试修改NUMBER_1的值,函数内成功,函数外无效。
cout << "fun_success step 2 " << n << endl; //输出是4。
}
int main(int argv, char* argc[])
{
const int NUMBER_1 = 2;
* (int *) &NUMBER_1 = 3; //此处成功把NUMBER_1的内存从2修改为3。
int n = NUMBER_1; //n的值是2。
cout << NUMBER_1 << endl; //输出是2。
//尝试输出3,失败。
fun_fail(NUMBER_1);
//尝试输出3,成功。本质上是传送了已被修改的一段内存地址给函数。
fun_success((int&)NUMBER_1);
/*虽然在fun_success里对NUMBER_1的引用做出了修改(修改为了4),但是函数之外,
NUMBER_1的值依然没有改变,因为在编译过程中,下一行代码已经被换成
"cout << 2 << endl;"了。代码的替换发生在NUMBER_1的内存改变之前。*/
cout << NUMBER_1 << endl; //输出依然是2。
return 0;
}
const修饰指针型变量
C语言中const修饰指针型变量的唯一语法是 :const char *pString
. 代表了指针指向的内容不可更改,而指针的内容可以更改。
C++中,const修饰指针型变量的用法变得更加强大,具体用法和解释如示例代码所示:
#include "stdafx.h"
int main(int argv, char* argc[])
{
char *pTmp = "hello";
char *pAnother = "world";
//1.
//指针pString1指向的字符串不可修改。
//指针pString1的内容可以修改。
const char *pString1 = pTmp;
pString1[1] = 'd'; //报错
pString1 = pAnother; //合法
//2. 等同于1
//如同 `unsigned int n = 1` 等价与 `int unsigned n = 1`. const和char位置可互换
//指针pString2指向的字符串不可修改。
//指针pString2的内容可以修改。
char const *pString2 = pTmp;
pString2[1] = 'd'; //报错
pString2 = pAnother; //合法
//3.
//指针pString3指向的字符串可修改。
//指针pString3的内容不可修改。指针pString3必须初始化。
char * const pString3 = pTmp;
pString3[1] = 'd'; //合法
pString3 = pAnother; //报错
//4.
//指针pString4指向的字符串不可修改。
//指针pString4的内容不可修改。指针pString4必须初始化。
const char * const pString4 = pTmp;
pString4[1] = 'd'; //报错
pString4 = pAnother; //报错
return 0;
}
看上去有些杂乱,读的技巧是关注const和*的相对位置。const在左边就说明指针指向的内容不可改,const在右边就说明指针的内容不可改。之所以有限定指针的内容不可修改的需求,是为了保护一些关键指针,比如main函数的agrv指针,如果在程序的某处把它改为NULL,那么就永远不能再读取程序的参数列表了。
在编程的过程中给函数参数加上const是个好习惯,这样能避免很多能编译通过,但是运行时会出错的bug。例如,程序运行时经常发生的C05错误大多是因为对全局常量区的字符串进行写入造成的,可以在编程时在这些指向全局常量区的指针前加入const修饰符,来规避这种错误。