C++关键字const
const是为了使程序员在变和不变之间画出了一条界限,这条界限的两边分别是变量和常量。想要区别他们并非易事,所以,本文稍微的总结了一下const在什么时候、为什么、和怎么样使用关键字const。
- const限定符
- 值替代
- C/C++之辨析
- 常量表达式
- 引用与const
- 左值引用
- 右值引用
- 指针与const
- 函数参数和返回值
- 顶层const与底层const
- 类
- constexpr
- constexpr变量
- 指针与constexpr
值替代
const产生最初的动机是取代预处理器#define来进行值替代。令人感到吃惊的是,这样的一个关键字竟然被广泛的用于指针、函数变量、返回类型、类对象、类数据成员、类成员函数。当然掌握这些所有的用法,需要时间慢慢来消化、斟酌、反思。
当我们使用C语言进行程序设计的时候,预处理器可以不受限制的建立宏并用它来替代值。但是预处理器只能做文本的替换,它没有类型检查功能。于是,C/C++可以通过使用const值来避免一些问题。
预处理的用法:
#define BUFSIZE 100
BUFSIZE很奇怪,它仅仅在预处理的阶段存在,因此不占据存储空间同时能放入一个头文件中,目的是为使用它的编译单元提供一个值。进而,当程序进行决定更改一个值的时候,必须手工编辑。最重要的是不能跟踪以保证没有漏掉其中的一个。
C++中使用const来消除这些问题。
const int bufsize = 100;
这样就可以在编译时编译器需要知道这个值的任何的地方使用bufsize,同时编译器还可以进行“常量折叠”,可以简略的理解为通过必要的计算把一个复杂的“常量表达式(下有说明)”通过缩减简单化。例如在数组中:
char buf[bufsize];
因此,我们应该完全使用const 取代#define的值替代。
在头文件中,使用const也必须放入头文件中。这样,可以通过包含头文件,可以把const定义单独放在一个地方并把它分配在一个编译单元中。但是C ++中的const默认为内部链接,意思为:const仅仅在被定义过的文件里才是可见的,而在连接不能被其他编译单元看到。当定义一个const,必须赋值给它,除非使用extern做出了清楚的说明:
extern const int bufsize; //强制分配空间
通常C++编译器并不为const创建存储空间,相反把这个定义保存在他的符号表里,但是extern强制分配了空间。由于编译器不能完全的避免为const分配内存,所以const默认为内部连接!
const在运行器件产生的值初始化一个变量,而且知道在变量的生命期间是不变的,则用const限定该变量是程序设计中一个很好的做法,如果偶然试图改变它,编译器会给出一个错误的信息。
const int i = 100;
const int j = i+10;
long address = (long)&j;
char buf[j+10];
分析上面的代码:i是一个编译期间的const,但是j是从i中计算出来的,然而由于i是一个const,j的计算值来自一个常数表达式,而它自身也是一个编译期间的const,下面的一行需要j的地址,所以迫使编译器给j分配存储空间。
从上面我们可以看出const意味着“不能改变的一块存储空间”。然而不能在编译器件使用它的值,因为编译器在编译器件不需要知道存储器的内容。所以下面的代码是有错误的:
const int i[] = {1,2,3,4,};
float f[i[3]]; //error!
C/C++之辨析
常量的引进是在C++的早期标准中订制的,当时标准C规范正在制定,C委员会在C中使用const的意思是“一个不能被改变的普通变量”,const常量总是占用存储而且它的名字是全局符,因此,C编译器不能将const 看成一个编译期间的常量。
const int bufsize = 100;
char buf[bufsize]; //error!
因为bufsize占用内存,因此C编译器不知道它在编译时的值,在C语言中可以选择这样书写:
const int bufsize ;
这样写在C++中是不对的,而C编译器则把它当成是一个声明,指明在别的地方有存储分配,因为C默认const是外部连接的,所以这样做是合理的。C++默认const 是内部连接的。这样,想在C++完成与C中同样的事情,必须使用extern明确的把连接改成外部连接:
extern const int bufszie ; //Declaration only !
extern const int x = 1;
上面代码对x进行初始化,并指定为extern 。同时,强迫分配内存(编译器可以在这里仍然选择常量折叠)。初始化它使它成为一个定义而不是一个声明,在C++中的声明:
extern const int x;
意味着在别处进行了定义(在C中,不一定是这样),现在为什么明白了C++要求一个const 定义时必须初始化:初始化把定义和声明相区分开来。
在C语言中使用限定符const并不是很有用,如果希望在常数表达式中(必须在编译期间求值)使用一个已命名的值,C总是迫使程序员在预处理使用#define。但是在C++则不是那样。
常量表达式
何为常量表达式?
const int max_files = 10; //是常量表达式吗?
const int limit = max_files + 1; //是常量表达式吗?
int staff_size = 27; //是常量表达式吗?
const int sz = get_size(); //是常量表达式吗?
上面的代码很难说哪些是常量表达式,根据一些定义:理解为其值恒定不变并且在编译期间就能够得到计算机结果的表达式。显然,一般使用const来初始化的对象是常量表达式。
引用与const
引用(reference)是为对象另起一个名字,可以理解为——别名。同时,引用并非对象,而是为了一个已经存在的对象所起的一个名字。
当我们使用术语“引用”的时候,指的其实就是左值引用(lvalue reference)。后面我们会再次总结“右值引用”的问题。
int ival=1024;
int &refVal = ival; //refVal 是ival的别名
int &refVal2; // error:引用必须初始化
同时,定义了一个引用之后,对其进行的所有的操作都是与之绑定的对象上进行操作。因为引用本身不是一个对象,所以不能定义引用的引用。
int i = 1024,i2 = 2048;//定义i 、i2
int &r =i,r2 =&i2;//r 与r2都是一个引用
int i3 = 1024,&ri = i3;// ri是一个引用,与i3绑定在一起
int &r3 = i3,&r4 = i2;//r3 r4都是引用
注意:引用只能绑定在某个对象上,而不能与字面值或者某个表达式的结果绑定在一起。
int &refVal3 = 10;//error:引用的初始值必须是一个对象
double dval =3.14;
int &refVal5 = dval;//error:引用的初始值必须是int 类型
补充:引用的类型必须与其引用的对象的类型一致,但是有2种例外,第一是初始化常量引用的时候允许用任意的表达式作为初始值,只要该表达式能够转换成引用的类型即可,尤其允许一个常量的引用绑定非常量的对象、字面值,甚至是一个表达式:
int i =42;
const int &r1= i; //允许将const int & 绑定到一个普通的int 对象上。
const int &r2 = 42; //r2是一个常量引用
const int &r3 = r1*2; //r3是一个常量引用
int &r4 = r1*2; //error:r4是一个普通的非常量引用。
常量引用
将对“const 的引用”简称为“常量引用”.
如何理解?
C++语言不允许随意改变引用所绑定的对象,这里我们可以理解为所有的引用又都算是常量,但是引用不是一个对象。
如何理解上面的代码?我们需要明白当一个常量引用绑定到另外一种类型上发生了什么
double dval = 3.14;
const int &ri = dval;
编译器把上面代码变成了下面的形式:
const int temp = dval; //由双精度浮点数生成一个临时的整形常量
const int &ri =temp ; //让ri绑定这个临时的变量
指针与const
指针(pointer)是指向另外一种类型的的复合类型,实现了对其他对象的间接访问,然而指针与引用有很多的不同点。
- 指针本身就是一个对象,允许对指针的赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针不需要在定义的时候赋予初值,在一个代码块定义的指针如果没有初始化,将被也拥有一个不确定的值。
int *ip1, *ip2; //ip1 ip2都是指向int 类型对象的指针
double dp,*dp2; //dp2是指向double对象的指针,dp是指向double对象。
获取对象的地址需要使用 取地址符(操作符&)。
int ival = 42;
int *p = &ival; //p存放的是变量ival的地址
int **p2 = &p; //p2指向一个int 类型的指针
定义多个变量常常会产生误导,尤其以指针为例,产生的问题是究竟将star(解引用符)靠经数据类型还是变量?一般情况下有2种:第一种将修饰符和变量标识符写在一起,强调的是变量所具有的复合类型;第二种将修饰符和类型名写在一起,但是这样很容易产生错误。能够避免这种错误的方法是,每一条语句就定义一个变量。
指向指针的引用
- 引用本身并不是一个对象,因此不能定义指向引用的指针,但是指针是一个对象,所以存在指针的引用。
int i =42;
int *p;
int *&r = p; //r是对指针p的引用
r = &i; //本质就是将i的地址保存在p中,p指向i
*i = 0; //将i 的值改为0
顶层const
利用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示所指的对象是一个常量。
int i = 0;
int *const p1 =&i;//不能改变p1的值,顶层const
const int ci = 42;//不能改变ci 的值,这是一个顶层const
const int *p2 = &ci;//允许改变p2的值,这是一个底层const
const int *const p3 = p2;//左为顶层const,右为底层const
const int &r = ci ;//用于声明引用的const 都是底层const
当执行对象的拷贝操作时候,常量是顶层还是底层const区别明显。其中顶层const可以完成对象的拷贝。
i = ci ; //正确;拷贝ci 的值,ci 是一个顶层const ,对此操作没有影响
p2 = p3; //正确
但是,底层const的限制却很大,当执行对象的拷贝操作,拷入和烤出的对象必须具有相同的底层const资格。或者2个对象的数据类型必须能能够转换。例如:非常量可以转换为常量,但是常量不能转换为非常量。
int *p = p3; //error : p3 包含有底层const 的定义,而p没有
p2 = &i; //正确:int *能够转换为const int *类型
int &r = ci; //error:const int 不能转换为int 类型
const int &r2 = i; //非常量可以转为一个常量
赋值与类型检查
C ++关于类型检查是非常的精细的,体现在很多的方面,例如可以把一个非const 对象的地址赋值给一个const指针,因为有的时候不想改变某些可以改变的东西。但是却不能将一个const 对象的地址赋值给非const指针,因为这样做可能通过被赋值的指针改变这个对象的值。当然,总能用强制转换类型进行这样的赋值,但是,这是一个不好的习惯。
int d = 1;
const int e = 2;
int *u = &d; //OK
int *p = &e; //error!
int *w = (int *)&e; //OK,but not good practice
非常隐含的错误——字符数组
没有强调严格的const特性的地方,是字符数组的字面值,可能大部分人会写出下面的代码:
char *cp = "howdy";
编译器将接受它而不会报告错误,从技术层面上,这的确是一个错误,因为字符数组的字面值是被编译器作为一个常量字符数组建立的,所引用该字符数组所得到的结果是它在内存里的首地址,修改该字符数组的任何字符都会导致运行时错误。当然并不是所有的编译器都会做到这一点。
但是字符数组的字面值实际上是常量字符数组,当然,编译器把他们作为非常量看待,这是因为有许多现有的C代码是这样的完成的。如果想要修改字符串,就要把它放进一个数组中:
char cp[] = "howdy";
函数参数和返回值
用const限定函数参数及其返回值是常量概念引起混淆的另外的一个地方。
在函数中,const有这样的意义:参数不能改变,所以它其实是函数创建者的工具,而非函数调用者的工具。
对于返回值来讲,存在一个类似的道理,即如果一个函数的返回值是一个常量(const)。
const int g();
这就约定了函数框架里的原变量不会被修改。按值返回的,所以这个变量会制成副本,使得初值不会被返回值所修改。
int f3(){return 1;}
const int f4(){return 1;}
int main(){
const int j = f3(); //work fine
int k = f4(); //but this work fine too
}
临时量
有的时候,在求表达式的值的期间,编译器必须创建你临时对象,像其他任何对象一样,他们都需要存储空间,并且必须能够构造和销毁。
类
我们常常可能会在一个类中建立一个局部的const,将它用于常数表达式中,这个常数表达式在编译期间被求值,然而,const在类中是不同的,所以为了创建类的const数据成员,必须了解这一个选择。同时为了保证一个类对象为常量,引进了const成员函数,const成员函数只能由const对象调用。
在一个类中我们都可能合乎逻辑的选择将const放在一个类中,但是这不回产生一个预期的效果,在一个类中,const又回到了C语言中的含义。它在每一个类对象中分配存储空间并代表一个值,这个值一旦被初始化就不能被改变,在一个类中使用const意味着“在这个对象的生命周期中,它是一个常量”,对于这个常量,每一个对象可以含有一个不同的值
构造函数初始化列表用于提醒程序员表中的初始化发生在构造函数中的如何代码执行之前。这是初始化所有const的地方:
class Fred{
const int size;
public:
Fred(int sz);
void print();
};
Fred::Fred(int sz):size(sz){}
void Fred::print(){cout<<size<<endl;}
了解const成员函数我们必须先引入一个概念:this指针。事实上,当我们调用成员函数时,实际上是在替某个对象调用它,在C++中,成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。this形参是隐式定义的,实际上。任何自定义为this的参数或者变量都是非法的。this的目的总是指向的这个对象。this是一个常量指针,我们不允许改变this中保存的地址。
而const成员函数的作用就是修改隐式this指针的类型。
默认的情况下,this的类型是指向非常量的常量指针。因此,在默认的情况下,我们不能将this绑定到一个常量对象上。
常量 = 非常量; //do well
非常量 = 常量 ; //error!
所以这一情况使得我们不能在一个常量对象上调用普通的成员函数。我们需要的就是将this声明指向常量的指针。C++ 语言中允许将const关键字放在成员函数的参数列表之后。表明this是一个指向常量的指针。
原则上,对于不修改数据成员函数都应该成为const成员函数。
constexpr
constexpr变量
C++ 11标准中:允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr一定是一个常量,而且必须用常量表达式初始化
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf+1; //mf+1是常量表达式
constexpr int sz = size(); //只有当size()函数是一个constexpr函数才会编译通过
constexpr 与指针
在constexpr声明中如果定义了指针,限定符constexpr仅仅对指针有效,而非指针所指的对象无关。
const int *p = nullptr; //p是一个指向整形常量的指针
constexpr int *q =nullptr; //q是一个指向整数的常量指针
小结
关键字const能将对象、函数参数、返回值、成员函数定义为常量,并能够消除
预处理器中的值替代。我们在C++虽然可以继续使用C代码的习惯,但是你会在C++的世界里发现const是多么的关键。