const的最初动机是用于取代预处理器#defines来进行值替代,从此以后,它被用在指针、函数变量、返回类型、类对象以及成员函数上。
一、值替代
当我们使用C语言进行程序设计的时候,预处理器可以不受限制地建立宏来进行值的替代。因为预处理器只进行文本的替换,不会进行类型检查,所以可能会出现一些问题。这些问题在C++中可以得到避免。所以我们应该更多地使用const来替代#define,例如:
#define BUFSIZE 100
BUFSIZE是一个名字,只存在于预处理期间,不占用储存空间并且可以放在一个头文件中,目的是为了所以的编译单元提供一个值。但是在大多数情况下,BUFSIZE工作方式和普通的变量一样,但是没有类型信息,这就容易隐藏错误,应该尽量被完全替代为:
const int bufsize = 100;
使用const的时候应该注意:const只有在const定义的文件之中才是可见的,并且定义一个const的时候,必须立即赋值给它,除非使用extern作出清楚的解释。例如:
extern const int bufsize;
这是因为,在通常情况下,编译器并不为const创建存储空间,相反,编译器把const定义存储在符号表中,但是extern强制进行了存储空间的分配(除了使用extern关键字强制进行内存分配,还可以取一个const的地址来进行强制内存分配),extern意味着外部链接(如果不使用extern关键字,const默认是内部链接的,这一点在上文中提到,也就是只有在const被定义过的文件里const才是可见的),而外部链接必须分配存储空间,这样几个不同的编译单元才能够引用他。
需要注意的是const用于数组元素的时候,是一定会分配内存的。但是不能在编译期间使用const数组里面的值,因为编译器在编译期间并不知道存储的内容。例如:
const int i[]={1,2,3,4};
//float f[i[3]];//会报错
struct S{ int i,int j;};
const S s[]= {{1,2}.{3,4}};
//double d[s[1].j];//会报错
int main(){}
并且,const关键字会比#define更加安全。const关键字初始化一个变量以后,将会知道运行期间产生的值在变量的生命周期不会改变。如果试图改变,编译器将会给出错误信息。
例如,上图中没有进行const变量初始化,以及试图改变const变量的值,编译器都会进行报错。
二、指针
在有关指针的内容,我们需要着重区分const修饰指针正指向的对象(指向const的指针,Pointer to const),或者const修饰在指针里存储的地址(const指针,const pointer)。
首先是指向const的指针,指向const的指针的特征是,const在指针符号*前面,也就是说不论是const int * 还是int const * ,都表示的是指向const的指针。
const int *a;
int const* b;
例如,无法通过const指针改变值,也无法用普通指针指向const的对象(因为这会造成风险,const对象默认是不可以改变的)
其次是const指针,使指针本身是一个const指针,const表明的部分必须放在*的右边。
int d=1;
int *const w =&d;
编译器要求给const指针一个初始值,这个值在指针生命期间内不会改变,然而要改变它所指向的值是可以的。例如,使用const指针指向v1以后,就无非再把这个指针重新指向v2(这在普通指针是当然可以的),但是我们可以通过这个const指针改变指针指向的值,同时,由于这是一个const类型的指针,并不能指向const对象,所以尝试把const类型指针指向const对象也是非法的。
同时,还有指向const对象的const指针,同时兼具两种特性。
可以看出,我们既不能改变pv1指向的对象,又不能改变pv1指向的值。
又例如,我们不能把普通的指针p1指向const对象f1,也不能把const类型的指针p2指向const对象f1,只有指向const的指针p3可以指向f1,对于普通变量f2,我们可以使用const类型指针,指向const的指针,又或者是普通指针指向f2。
值得一提的是,可以通过强制类型转换来强制让普通类型的指针指向const,但是这违背了const的安全性,所以这里不表。
三、函数
用const修饰按值传递的对象,意味着传递的参数在函数里无法修改。如果按常量返回一个对象的值,它意味着返回值无法被修改,如果传递并返回地址,const保证这个地址的内容不会被改变。
void f1(const int i)
{
//i++;//报错,无法修改
}
对于按值传递的返回类型,const是没有意义的。
int f3(){return 1}
const int f4(){return 1;}
int main(){
const int j = f3();
int k = f4();
}
例如,上面传递const的函数u()内部,试图改变const类型的变量的值,是非法的;试图把普通类型的指针指向const类型的指针cip,也是非法的;
右侧t(cip)试图把指向const的指针赋给普通类型的指针,是非法的,因为这可能威胁指向const类型指针的安全性。
在接受返回值的时候,如果是const类型的返回值,需要用指向const的指针。
对于函数w(),返回一个指向const的const类型的指针,需要用const类型的指针来接受,同时,也不能改变函数的值。
值得一提的是,编译器自动把临时量作为const类型。
int f1(int &a){return a;}
int f2(int a){return a;}
f1(f2(0));//非法
例如,,编译器使用一个临时对象来保存f2(0)的值,来使他可以传递给f1(),如果f1()的参数是按值传递的,就能很不会报错。但是f1()的参数是按照引用传递的,这意味着它取临时变量的地址,又因为f1()的值不是按照const来传递的,意味着可以对传递的值进行修改,但是对于临时量,一旦离开这个表达式,临时量将会被销毁,所以更改将会没有意义,对于const类型的临时量,如果不按照const来传递,就会报错。
因此,临时变量按照引用传递给一个函数,这个函数的参数必须是const引用。
在传递一个参数的时候,我们应该首先选择引用传递,而且是const引用。按照const引用传递意味着函数不会改变地址所指向的内容,效果上和按值传递一样。而且,把一个临时对象传递给接受const引用的函数是可能的。但是不能把一个临时对象传递给一个指针引用的函数,对于指针,必须明确地接受地址。
也就是说:一个总是const类型的临时变量,他的地址可以传递给一个参数是const引用的函数。
class X{};
X f() {return X();}
void g1(X&);
void g2(const X&);
int main()
{
g1(f());//报错
g2(f());//OK
}
四、类
首先介绍static关键字,static关键字意味着“不管类的对象被创建多少次,都只有一个实例”,必须被初始化在类外,不使用static关键字,加上类名。
例如:
作为区别,const常量的成员变量不能修改,必须在构造函数的初始化列表中初始化。
另外,const还可以用来修饰成员函数和对象。const类型的对象保证对象的数据成员在声明周期内不会改变,如果声明一个成员函数为const,等于告诉编译器这个成员函数可以被const对象调用,一个没有声明为const的成员函数将会被看做将要修改对象中的数据成员的函数。所以编译器不允许被const对象调用。
对于mutable关键字,代表数据可以允许更新。尽管被一个const成员函数改变。