const关键字

【1】const关键字的作用?

C++语言在C语言的基础上新增加了几点优化是很耀眼的。const算作其中之一。

const直接可以取代C语言中的宏 #define。 const 是 constant 的缩写,“恒定不变”的意思。

被const修饰的东西都受到强制保护,可以预防意外的修改,提高程序健壮性。

记住一点:const修饰的变量都是“只读的”。

【2】如何使用const?

(1)限定符  声明变量  必须初始化

作为一个限定符,const声明的变量,其在声明的同时必须初始化。

示例代码如下:

1     const int n = 5; // OK 
2     const int m;  // error! 编译错误!

(2)限定符  声明变量

作为一个限定符,const声明的变量,其命运随之改变,即就是:此变量具有常性,只能被读,任何试图对其的修改是非法的。

示例代码如下:

1     const int n = 5;
2     int m = 0;
3     n = m; // error! 编译错误!
4     m = n; // OK!

(3)在另一个文件中引用const变量

当在另一连接文件中引用const常量时,只需要声明,不可以再次赋值。

示例代码如下:

1     extern const int i;       // OK
2     extern const int j = 10;    // error! 常量不可以被再次赋值。

(4)便于进行类型检查

const常量有数据类型,而宏常量没有数据类型。

编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查。

所以,后者在字符替换时可能会产生意料不到的错误(边际效应)。

示例代码如下:

1     const int max = 100;    // const常量
2     
3     #define max 10;     // 宏定义 (注意分号)

(5)可以避免不必要的内存分配

示例代码如下:

 1     #define STRING "abcdefghijklmn\n"
 2 
 3     const char string[] = "abcdefghijklm\n";
 4 
 5     printf(STRING);   // 为STRING分配了第一次内存
 6 
 7     printf(string);   // 为string一次分配了内存,以后不再分配
 8 
 9     printf(STRING);   // 为STRING分配了第二次内存
10 
11     printf(string);

由于const定义常量从汇编的角度看,给出了对应的内存地址,而不是#define一样给出的是立即数。

所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

(6)const的常量值一定不可以被修改?

先看一段代码如下:

1     const int n = 0;
2     int* p = (int *)&n;
3     *p = 100;

通过强制类型转换,将地址赋给变量,再作修改即可以改变const常量值。

(7)分清 数值常量 和 指针常量

示例代码如下:

1     int n = 10;
2     const int cn = 20; // cn是常量,cn的值只读
3     const int * cip = &cn;  // 指针cip所指内容是常量
4     int * const icp = &n;    // 指针icp是常量,指针值不可改变,所指内容可改变
5     const int * const cipc = &cn;  // 指针cipc是常量,所指内容也是常量
6     cip = &n;   // OK
7     *icp = 100;   // OK

(8)const修饰类的数据成员

示例代码如下:

1 class A 
2 {
3     const int size; 
4 };

理解:const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。

因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。

提醒:所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const数据成员的值是多少。

注意:const数据成员的初始化只能在类的构造函数的初始化表中进行。

要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。

示例代码如下:

 1 // 试图用const在类中实现常量是不可取的
 2 class A 
 3 {
 4     const int size = 100;    // error!
 5 
 6     int array[size];         // error! 未知的size!
 7 };
 8 
 9 // 正确的方式是利用枚举
10 class A 
11 {
12     enum {size1 = 100, size2 = 200}; 
13 
14     int array1[size1];  // OK
15 
16     int array2[size2];  // OK
17 
18 };

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。

但是,枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

(9)const修饰指针

示例代码如下:

1 const int * a;   // [1]
2 
3 int const * a;   // [2]  
4 
5 int * const a;   // [3]
6  
7 const int * const a;   // [4]

如果:const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向内容为常量;

如果:const位于星号的右侧,const就是修饰指针本身,即指针变量是常量。

如果:星号左右两侧均有const,那么指针的“两个值”同时被限定。即指针变量的值不可修改,同时指针指向的内容值不可修改。

因此,可以分析为:

[1] 和 [2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 具体数值;

[3] 为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;

[4] 为指针本身和指向的内容均为常量。

(10)const的初始化

<1> 非指针const常量初始化的情况。代码如下:

1 // 假使A是自定义类型
2 A a;
3 const A b = a;

<2> 指针const常量初始化的情况。代码如下:

1 A* pa = new A();
2 const A * pb = pa;

<3> 引用const常量初始化的情况。代码如下:

1 A f;
2 const A & e = f;  // 作为常对象,e对象只能访问声明为const的常函数,而不能访问一般的成员函数。

(11)const一些强大的功能在于它对函数的修饰

在一个函数声明中,const 可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰整个函数。

有如下几种情况,以下会逐个说明用法:

1 A & operator = (const A & a);  // 修饰函数参数(典型的用法)
2  
3 void fun0 (const A * a );     // 修饰函数的参数
4 
5 void fun1() const;    // 修饰类成员函数 
6 
7 const A fun2( );       // 修饰函数返回值

<1> 修饰参数的const。示例代码:

1 void fun0(const A* a ); 
2 
3 void fun1(const A& a); 

当调用函数的时候,用相应的实参变量初始化const常量。则在函数体中,按照const所修饰的部分进行常量化。

如形参为const A * a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;

如形参为const A & a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。

注意:const修饰参数通常用于参数为指针 或 引用的情况,且只能修饰输入参数。

若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于赋值该参数,该参数本就不需要保护,所以不用const修饰。 

切记一点:

对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。

例如:将 void Func(A a) 改为 void Func(const A & a) 

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

例如:void Func (int x) 不应该改为 void Func(const int & x)

<2> const修饰返回值。示例代码如下:

1 const A fun2(); 
2 
3 const A* fun3(); 

这样声明返回值后,const按照“修饰原则”进行修饰,起到相应的保护作用。

示例代码如下:

const Rational operator*(const Rational& lhs, const Rational& rhs) 
{ 
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); 
} 

返回值用const修饰可以防止这样的操作发生:

1 Rational a, b; 
2 
3 Radional c; 
4  
5 (a * b) = c; 

一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。

【3】使用const注意事项

(1)一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。

通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。

原因如下:如果返回值为某个对象const(const test A = A 实例) 或 某个对象的引用为const(const A & test = A实例),

则返回值具有const属性,返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

(2)如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。

示例代码如下:

1 const char * GetString(void);  

如下语句将出现编译错误:

1 char *str = GetString();   //error!!!

正确的用法是:

1 const char *str = GetString();  //OK

(3)函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

示例代码如下:

 1 class A 
 2 {
 3 
 4 A &operate = (const A &other);  // 赋值函数 
 5 
 6 };
 7 
 8 A a, b, c;              // a, b, c为A的对象 
 9 
10 a = b = c;              // 正常  
11 
12 (a = b) = c;            // 不正常,但是合法

若赋值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a = b = c依然正确。(a = b) = c就不正确了。

【4】const修饰类成员函数作用

const修饰类成员函数一般放在函数体后。形如:void fun() const; 

任何不会修改数据成员的函数都应该声明为const类型。

如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。

示例代码如下:

 1 class Stack 
 2 {  
 3 public: 
 4     void Push(int elem);
 5     int Pop(void); 
 6     int GetCount(void)  const;   // const 成员函数 
 7 
 8 private: 
 9     int m_num; 
10     int m_data[100]; 
11 }; 
12 
13 int Stack::GetCount() const 
14 { 
15     ++m_num;  // error! 编译错误,企图修改数据成员m_num
16     Pop();    // error! 编译错误,企图调用非const函数
17     return m_num;
18 }

【5】const使用建议有哪些?

(1)要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委。

(2)要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题。

(3)在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上。

(4)const在成员函数中的三种用法(参数、返回值、函数)要很好的使用。

(5)不要轻易的将函数的返回值类型定为const。

(6)除了重载操作符外一般不要将返回值类型定为对某个对象的const引用。

思考1:以下的这种赋值方法正确吗?

1 const A * cp = new A(); 
2 
3 A* pe = cp; 

这种方法不正确。因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;

思考2:以下的这种赋值方法正确吗?

1 A * const pc = new A(); 
2  
3 A* pb = pc; 

这种方法正确,因为声明指针所指向的内容可变。

思考3:这样定义赋值操作符重载函数可以吗? 

const A & operator = (const A& a); 

这种做法不正确。

在const A::operator=(const A& a)中,参数列表中的const的用法正确。而当这样连续赋值的时侯,问题就出现了:

1 A a, b, c;
2 
3 (a = b) = c;

因为a.operator=(b) 的返回值是对a的const引用,不能再将c赋值给const常量。

看到const 关键字,C++程序员首先想到的可能是const 常量。这可不是良好的条件反射。如果只知道用const 定义常量,那么相当于把火药仅用于制作鞭炮。

const 更大的魅力是它可以修饰函数的参数、返回值,甚至函数体。

 

作者:kaizen
声明:本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此声明,且在文章明显位置给出本文链接,否则保留追究法律责任的权利。
签名:顺序 选择 循环
原文地址:https://www.cnblogs.com/Braveliu/p/2839817.html