【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 更大的魅力是它可以修饰函数的参数、返回值,甚至函数体。