C++中的const关键字

const与#define相比, 有何优点?

  1. const常量具有数据类型, 而宏常量没有数据类型. 编译器可以对前者进行类型安全检查, 而对后者只会进行字符替换, 没有类型安全检查, 容易发生意想不到的错误.
  2. 从编译的角度来看, const定义的常量在程序运行过程中只有一份拷贝(因为对应的是内存地址, 全局的只读变量, 在静态区), 而#define相当于字符替换, 每一次替换相当于打了一次拷贝.
  3. 有些集成化的调试工具可以对const常量进行调试, 但是不能对宏常量进行调试.

const常用形式及代表含义

含义 使用形式 及 作用
const常量 const int Max = 100; int const Max =100 const与int的位置可互换,二者等价
修饰后的变量在程序的任意位置将不能再被修改,就如同常数一样,任何修改该变量的尝试都会导致编译错误(由于常量在定义以后就不能再被修改,所以定义时必须初始化)。 对于类中的const成员变量必须通过初始化列表进行初始化
const引用 const int i = 1024; const int &ref_i = i; int const &ref2_i = i后两句等价
int &const ref3_i = i这种方式未定义,会报错:‘const’ qualifiers cannot be applied to ‘int&’
cosnt引用是指向const对象或者普通对象的引用,该引用ref_i可以读取对象i的值,但是,不能修改 i的值,任何对ref_i的赋值都是非法的。 并且 不能用普通引用指向const常量int &ref4_i = i; // 非法)。(注意,一旦引用被定义,它就不能再指向其它对象,这是引用本身的性质,这也是为什么引用必须进行初始化的原因)。 总结来说就是const引用只是表明:保证不会通过此引用间接的改变被引用的对象!
const指针 1)int age = 39; const int *ptr = &age;
2)const int age = 98; const int *ptr = &age;
3)const int age = 32; int *ptr = &age;
4)int age = 90; int * const ptr = &age;
对于1),const修饰的是int,表明ptr指向一个const int,但是ptrage本身都不是const,不能使用ptr来修改age的值,但是age自身可以修改自己的值,同时,ptr也可以转而指向其它变量;
对于2),const修饰的是int,表明ptr指向一个const int,同时age本身就是const,说明既不能通过ptr,也不能通过age来修改变量age的值,但是ptr本身仍然不是const,因此可以转而指向其它变量;
对于3),C++禁止将const变量的地址赋给非const指针(除非使用强制类型转换);
对于4),const修饰的是*,表明ptr本身是一个常量指针,这使得ptr自身的值不能改变,也就是只能指向age,不能指向其它变量
const函数参数 void fun(const int * i);
void fun(const int & i);
不能在函数体内修改指针或引用指向的值,但是这里指针可以转而指向其他值(其实没多大用,因为指针本身就是值传递,即使改变指向,也不会影响实参的指向,除非用二级指针)
const函数返回值 const int fun( int i);
阻止用户修改返回值,返回值必须要相应的赋给一个具有const属性的变量
const成员函数 <类型说明符> <函数名> ( <参数表> ) const;
不会修改类的成员数据,也不能调用其它非const成员函数。 可以利用const限定符实现函数重载。 const对象默认会调用const成员函数

const限定符和static的区别

  1. 静态变量的值虽然只能进行一次初始化,但是它的值是可变的。而const的值是不可变的。
  2. const定义的变量在超出其作用域后空间就会被释放,而static定义的静态变量不会释放空间,而是一直存在,知道程序结束
  3. static表示静态,类的静态成员函数、静态成员变量都是和类相关的,而不是和具体的对象相关,即使没有具体对象,也能调用静态成员函数和静态成员变量。而类则是和具体的对象相关的,不同的对象独占一个const变量
  4. static静态成员变量不能在类的内部进行初始化,智能在类的外部进行初始化,并且初始化时不能加static关键字。之后,无需创建对象也能通过类使用该静态变量,同时
  5. 由于const变量只是针对于某个类的对象而言的,类可以创建多个不同的对象,每个对象都有自己的const变量,又因为const变量值只能进行一次初始化而不仅再次赋值,因此, const变量也不能在类的内部初始化,而只能在类构造函数的初始化列表中进行
  6. 如果想要创建整个类的恒定变量,那么应用使用static const来声明(const枚举量也可以)

关于const的其它注意事项

将非const指针赋给const指针(两级间接关系)

p222

const引用与不可寻址值

const引用可以用不同类型的对象初始化(主要能从一种类型转换到另一种类型即可),也可以用字面常量初始化,如下所示:

1
2
3
4
5
double dval = 3.14159;
//下3行仅对const引用才是合法的
const int &ir = 1024;
const int &ir2 = dval; //隐式类型转换,生成临时变量
const double &dr = dval + 1.0;

首先要知道,上面的初始化对于非cosnt引用是不合法的,将导致编译错误!

原因:引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,将该对象的值置入临时对象中,引用实际上指向该对象(对该引用的操作就是对该临时对象的操作),但用户不能访问它。因此,C++为了不让用户通过引用修改该临时变量,强制要求必须使用const引用。

const引用与临时变量

由于上面提到的原因, C++引用类型在面对会产生临时变量的语句时,必须使用const引用来指向!!切记!!

1
2
3
4
5
6
7
8
9
10
11
int i = 100;
int *&p_i = &i;//错误
int *const &p_i = &i; //正确写法,const修饰int *
//&代表p_i是一个引用(别名),*代表这个引用(别名)是一个指向int类型的指针,
//而后面的&i代表取i的地址,注意,这里会生成一个存储地址的临时变量,因此,指向该临时变量的引用(别名)必须为const,
//也就是说,这个指向int的指针本身必须是const的,因为不能修改临时变量的值

const int i = 100;
int *&p_i = &i;// 错误,有临时变量生成,引用必须是const
int *const &p_i = &i; //错误,由于i本身就是cosnt,因此指针必须是一个指向cosnt int的指针
const int *const &p_i = i;//正确

const引用与非const引用在内存中的对比

内存地址都是一样的,网上有的写不一样,是错误的!

1
2
3
4
5
6
7
8
9
10
11
cosnt int t = 9;
const int &k = t;
cout<<&k<<endl; // 0012FF74
cout<<&t<<endl; // 0012FF74

int t = 9;
int &k = t;
const int &m = t;
cout<<&k<<endl; // 0012FF74
cout<<&m<<endl; // 0012FF74
cout<<&t<<endl; // 0012FF74

不允许非const引用指向需要临时对象的对象或值,即,编译器产生临时变量的时候引用必须为const!!!!切记!!

1
2
3
4
5
6
7
8
9
10
int iv = 100;
int * &pir = &iv;//错误,非const引用对需要临时对象的引用
int *const &pir = &iv;//ok
const int ival = 1024;
int *&pi_ref = &ival; //错误,非const引用是非法的
const int *&pi_ref = &ival; //错误,需要临时变量,且引用的是指针,而pi_ref是一个非常量指针
const int * const &pi_ref = &ival; //正确
//补充
const int *p = &ival;
const int *&pi_ref = p; //正确

const变量的生存期、作用域和链接性

临时变量一般会在语句块的末尾自动释放,但是有两个例外:

  • 将临时对象作为初始化因子,例如string s = string("hello world");
  • 将一个常量引用变量绑定到这个临时对象上。

作用域

在全局作用域中声明的const变量,只能在当前文件中访问,如果要在其他文件中使用,则必须用extern显式的声明const变量全局变量。(在C中,可以不用extern?)

用const修饰函数参数

是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
问题是如此的缠绵,我只好将“const &”修饰输入参数的用法总结一下。
对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。

对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)

用const修饰函数的返回值

  1. 修饰内置类型的返回值时, 加const没有意义
  2. 修饰自定义类型的返回值时, 此时的返回值不能作为左值使用, 既不能被赋值, 也不能被修改
  3. 修饰指针或引用的返回值时, 取决于我们想让用户干什么

用const修饰类的成员函数

表明该函数 不能修改类对象的数据成员, 并且不能调用非 const 函数 (因为非const函数有可能会修改数据成员)

1
int GetCount(void) const; // const 成员函数

在修饰成员函数时, const 关键字不能与 static 关键字同时使用, 因为 static 关键字修饰的是静态成员函数, 其不含有this指针, 因此不能实例化, 而 const 成员函数必须具体到某一实例.

类中的const常量

有时我们希望某些常量只在类中有效, 由于#define定义的宏常量是全局的, 因此不能达到目的, 但是直接用const修饰数据成员并不是我们想要的含义, const数据成员只在某个对象(注意是对象, 不是类)生存期内是常量, 而对于整个类而言却是可变的, 因为类可以创建多个对象, 不同的对象其const数据成员的值可以不同.

从上面的概念我们可以得出, 不能在类声明中初始化const数据成员, 而应该在类的构造函数的初始化表中进行.

要建立整个类中的常量, 则应该使用枚举常量, 枚举常量的缺点是, 它的隐含数据类型是整数, 其最大值有限, 且不能表示浮点数.