C++ const详解

来源:互联网 发布:少儿趣味编程 编辑:程序博客网 时间:2024/06/17 21:13
1.    const的用法:
看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。
constconstant的缩写,“恒定不变”的意思。const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
1.       const修饰函数的参数
如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。const只能修饰输入参数:
参数作输出用是指在函数内传入参数的值会改变,所以不应该用const修饰,例如要得到整数x的一半,函数应为int half(int x),x=x/2,而不能是int half(const int x)
如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。
例如StringCopy函数:
void StringCopy(char *strDestination, const char *strSource);
其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。
修改hello为HELLO,可行代码:
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. void Modify(char *strSource)  
  4. {  
  5.     while (*strSource!=0)  
  6.     {  
  7.         *strSource=*strSource-32;  
  8.         strSource++;  
  9.     }  
  10. }  
  11. int main()  
  12. {  
  13.     char a[6]="hello";  
  14.     Modify(a);  
  15.     cout<<a<<endl;  
  16.     cin.get();  
  17.     return 0;  
  18. }  
有错代码:
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. void Modify(const char *strSource)  
  4. {  
  5.     while (*strSource!=0)  
  6.     {  
  7.         *strSource=*strSource-32;  
  8.         strSource++;  
  9.     }  
  10. }  
  11. int main()  
  12. {  
  13.     char a[6]="hello";  
  14.     Modify(a);  
  15.     cout<<a<<endl;  
  16.     cin.get();  
  17.     return 0;  
  18. }  
如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。
例如不要将函数void Func1(int x)写成void Func1(const int x)
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. int Modify(int num)  
  4. {  
  5.     num=num/2;  
  6.     return num;  
  7. }  
  8. int main()  
  9. {  
  10.     int a=10;  
  11.     int b=Modify(a);  
  12.     cout<<a<<endl;  
  13.     cin.get();  
  14.     return 0;  
  15. }  
输出a的值是没有改变的,所以不用加const来保护输入参数
同理不要将函数void Func2(A a)写成void Func2(const A a)。其中A为用户自定义的数据类型。
对于非内部数据类型的参数而言,象void Func(A a)这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A & a)存在一个缺点:
“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,const修饰即可,因此函数最终成为void Func(const A &a)
以此类推,是否应将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)
2 .const修饰函数的返回值
如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();
正确代码:
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. const char* GetString(char* strSource)  
  4. {  
  5.     char *str=strSource;  
  6.     return str;  
  7. }  
  8. int main()  
  9. {  
  10.     char a[6]="hello";  
  11.     const char *p=GetString(a);  
  12.     cout<<p<<endl;  
  13.     cin.get();  
  14.     return 0;  
  15. }  
[cpp] view plaincopy
  1.   
错误代码:
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. const char* GetString(char* strSource)  
  4. {  
  5.     char *str=strSource;  
  6.     return str;  
  7. }  
  8. int main()  
  9. {  
  10.     char a[6]="hello";  
  11.     char *p=GetString(a);  
  12.     cout<<p<<endl;  
  13.     cin.get();  
  14.     return 0;  
  15. }  
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。
例如不要把函数int GetInt(void)写成const int GetInt(void)
同理不要把函数A GetA(void) 写成const A GetA(void),其中A为用户自定义的数据类型。
正确代码:
[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3. const int add(int a)  
  4. {  
  5.     int b=a+5;  
  6.     return b;  
  7. }  
  8. int main()  
  9. {  
  10.     int num=10;  
  11.     int num1=add(num);  
  12.     cout<<num1<<endl;  
  13.     cin.get();  
  14.     return 0;  
  15. }  
我的理解:为什么不需要const int num1=add(num),估计是add(num)的值是函数返回值的一份复制,所以可以int num1=add(num),这可能是值传递的特点吧。
如果返回值不是内部数据类型,将函数A GetA(void)改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
例如:
class A
{
A & operate = (const A & other); //赋值函数
} ;
A a, b, c; // a, b, cA的对象
a = b = c; //正常的链式赋值
(a = b) = c; //不正常的链式赋值,但合法
如果将赋值函数的返回值加const修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c仍然正确,但是语句(a = b) = c 则是非法的。
3. const成员函数
任何不会修改数据成员(即函数中的变量)的函数都应该声明为const类型如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack的成员函数GetCount仅用于计数,从逻辑上讲GetCount应当为const函数。编译器将指出GetCount函数中的错误。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const成员函数
private:
int m_num;
int m_data[100];
} ;
int Stack::GetCount(void) const
{
++ m_num; //编译错误,企图修改数据成员m_num
Pop(); //编译错误,企图调用非const函数
return m_num;
}
const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
关于Const函数的几点规则:
a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
e. 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的

为什么使用const?
采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替)

用法1:常量
取 代了C中的宏定义,声明时必须进行初始化。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
C标准中,const定义的常量是全局的,C++中视声明位置而定。

用法2:指针和常量
使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
所以出现在 * 之前的const是作为基础类型的一部分:
char *const cp; //到char的const指针
char const *pc1; //到const char的指针
const char *pc2; //到const char的指针(后两个声明是等同的)
从右向左读的记忆方式:
cp is a const pointer to char.
pc2 is a pointer to const char.

用法3:const修饰函数传入参数
将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数

用法4:修饰函数返回值

可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。

用法5:const修饰成员函数
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。


Const深度解析
我们也许学习过const的使用,但是对于const的细致的技术细节却不一定掌握。const的用法在许多的教材上只是简单的介绍,在这里我们对 const进行细致的概念以及用法剖析。const 是由c++采用,并加进标准c中,但是他们的意义完全不同,在旧版本(标准前)的c中,如果想建立一个常量,必须使用预处理器:
#define PI 3.14159
此后无论在何处使用PI,都会被预处理器以3.14159替代。编译器不对PI进行类型检查,也就是说可以不受限制的建立宏并用它来替代值,如果使用不慎,很可能由预处理引入错误,这些错误往往很难发现。
我们也不能得到PI的地址(即不能向PI传递指针和引用)。
c++引入了命名常量的概念,命名常量就像变量一样,只是它的值不能改变,如果试图改变一个const 对象,编译器将会产生错误。 const 和正常变量一样有作用域,所以函数内部的const也不会影响程序的其余部分。在c++中const可以取代预处理器#define来进行值替代, const有安全的类型检查,所以不用担心会像预处理器一样引入错误。
在通常的情况下const同预处理器#define一样只是将所赋值保存入编译器的符号表中(符号表仅仅在编译时存在,在编译过程中编译器将程序中的名字与之在符号表中定义的数值作简单的替换),在使用的时候进行值替换,并不为const创建存储空间。我们将const的定义放进头文件里,这样通过包含头文件,可以把const定义单独放在一个地方并把它分配给一个编译单元,const默认为内部连接(内部连接意味着只对正在编译的文件创建存储空间,别的文件可以使用相同的标示符和全局变量,编译器不会发现冲突,外部连接意味着为所有被编译过的文件创建一片单独的存储空间,一般全局变量和函数名的外部连接通过extern声明,可以通过其他的文件访问)也就是说const仅能被它所定义过的文件访问,在定义一个const时,必须赋一个值给它,除非用extern做出说明:
extern const int a;
这表示const的定义在其他的什么地方,这里仅仅是一个声明,但是这样的做法使const使用了外部连接,也就是说上面的extern强制进行了对const的存储空间分配,这样我们就无法再用const作为常量折叠(在可能的情况下,符号常量的值会代替改名字的出现,这个替代过程叫做常量折叠)使用了,即使我们在其他地方定义了const的值,如:
extern const int a=3;
因为const的值被放入了存储单元,在编译的过程中,编译器不会去读存储单元的内容。如果我们这样做:
int b[a];
编译器就会给我们一个错误信息。
想不为const分配存储空间是不可能的,因为对于复杂的结构,例如集合,编译器不会复杂到将集合保存到它的符号表中,所以必须分配内存空间,这就意味着“这是一块不能改变的存储空间”,当然也就不能在编译期间使用它的值,因为编译器不知道存储的内容:
const int i[]={1,2,3,4};
//float f[i[2]]; 
//将得到错误信息,编译器提示不能在数组定义里找到一个常数表达式。
因为编译器靠移动栈指针来存储和读取数据。(编译时必须为数组分配内存,但这时数组无法读取const常量的值,所以编译器不知道为数组f分配多大的空间。)
也因此,由于无法避免为const分配内存,所以const的定义必须默认为内部连接,否则由于众多的const在多个文件中分配内存,就会引起错误。下面我们看一段简单有效的代码来说明const的常量折叠:
#include <iostream.h>
const int a=3;
const int b=a+1;
float *f=(float*)&b;
char c[b+3];
void main()
{
const char gc=cin.get();
const char c2=gc+3;
}
我们可以看到,a是一个编译器期间的const,b是从a中计算出来的,由于a是一个const,b的计算值来自一个常数表达式,而它自身也是一个编译期间的const,接着下面指针f取得了b的地址,所以迫使编译器给b分配了存储空间,不过即使分配了存储空间,由于编译器已经知道了b的值,所以仍然不妨碍在决定数组c的大小时使用b。
在主函数main()里,标识符gc的值在编译期间是不知道的,这也意味着需要存储空间,但是初始化要在定义点进行,而且一旦初始化,其值就不能改变,我们发现c2是由gc计算出来的,它的作用域与其他类型const的作用域是一样的,这是对#define用法的一种改进。
在c++引进常量的时候,标准c也引入了const,但是在c中const的意思和在c++中有很大不同,在c中const的意思是“一个不能改变的普通变量”,const常量总是被分配存储空间而且它的名字是全局符即const使用外部连接。于是在c中:
const int size=100;
char c[size];
得出一个错误。但是在c中可以这样写:
const int size;
因为c中的const被默认为外部连接,所以这样做是合理的。
在c语言中使用限定符const不是很有用,如果希望在常数表达式里(必须在编译期间被求值)使用一个已命名的值,必须使用预处理器#define。
在c++中可以使指针成为const,这很有用,如果以后想在程序代码中改变const这种指针的使用,编译器将给出通知,这样大大提高了安全性。在用带有const的指针时,我们有两种选择:const修饰指针指向的对象,或者const修饰指针自己指向的存储空间。
如果要使指向的对象不发生改变,则需要这样写:
const int *p;
这里p是一个指向const int 的指针,它不需要初始化,因为p可以指向任何标识符,它自己并不是一个const,但是它所指的值是不能改变的,同样的,我们可以这样写:
int const *p;
这两种方法是等同的,依据个人习惯以及编码风格不同,程序员自己决定使用哪一种形式。
如果希望使指针成为一个const必须将const标明的部分放在*右边。
int a=3;
int *const j=&a
编译器要求给它一个初始值,这个值在指针的生命期间内不变,也就是说指针始终指向a的地址,不过要改变它地址中的值是可以的:
*j+=4;
也可以是一个const指针指向一个const对象:
const int *j1=&a;
int const *j2=&a;
这样指针和对象都不能改变,这两种形式同样是等同的。在赋值的的时候需要注意,我们可以将一个非const的对象地址赋给一个const指针,但是不能将一个const对象地址赋给一个非const指针,因为这样可能通过被赋值的指针改变对象的值,当然也可以用类型的强制转换来进行const对象的赋值,但是这样做打破了const提供的安全性。
const也被用于限定函数参数和函数的返回值,如果函数参数是按值传递时,即表示变量的初值不会被函数改变,如果函数的返回值为const那么对于内部类型来说按值返回的是否是一个cosnt是无关紧要的,编译器不让它成为一个左值,因为它是一个值而不是一个变量,所以使用const是多余的,例如:
const int f(){return 1;}
void main(){int a=f();}
但是当处理用户定义类型的时候,按值返回常量就很有意义了,这时候函数的返回值不能被直接赋值也不能被修改。仅仅是非const返回值能作为一个左值使用,但是这往往失去意义,因为函数返回值在使用时通常保存为一个临时量,临时量被作为左值使用并修改后,编译器将临时量清除。结果丢失了所有的修改。
可以用const限定传递或返回一个地址(即一个指针或一个引用):
const int * const func(const int *p)
{ static int a=*p;
return &a;
}
参数内的const限定指针p指向的数据不能被改变,此后p的值被赋给静态变量a,然后将a的地址返回,这里a是一个静态变量,在函数运行结束后,它的生命期并没有结束,所以可以将它的地址返回。(如果是局部变量,则不可将其值或其地址作为返回值)因为函数返回一个const int* 型,所以函数func的返回值不可以赋给一个非指向const的指针 (因为在赋值的的时候需要注意,我们可以将一个非const的对象地址赋给一个const指针,但是不能将一个const对象地址赋给一个非const指针,因为这样可能通过被赋值的指针改变对象的值),但它同时接受一个const int * const和一个const int *指针,这是因为在函数返回时产生一个const临时指针用以存放a的地址,所以自动产生了这种原始变量不能被改变的约定,于是*右边的const只有当作左值使用时才有意义。
const同样运用于类中,但是它的意义又有所不同,我们可以创建const的数据成员,const的成员函数,甚至是const的对象,但是保持类的对象为const比较复杂,所以const对象只能调用const成员函数。
const的数据成员在类的每一个对象中分配存储,并且一旦初始化这个值在对象的生命期内是一个常量,因此在类中建立一个const数据成员时,初始化工作必须在构造函数初始化列表中。如果我们希望创建一个有编译期间的常量成员,这就需要在该常量成员的前面使用static限定符,这样所有的对象都仅有一个实例或者利枚举变量:
class X
{
static const int size=50;
int a[size];
public:
X();
};
const对象只能调用const成员函数,一个普通对象同样可以调用const成员函数,因此,const成员函数更具有一般性,但是成员函数不会默认为const。声明一个const成员函数,需要将const限定符放在函数名的后面:
void f (void ) const;
当我们运用const成员函数时,遇到需要改变数据成员,可以用mutable进行特别的指定:
class X
{
mutable int i;
public:
X();
void nochange() const;
};
void X::nochange const(){i++;}
mutable
  mutable 可以用来指出,即使结构或者类变量为const,其某个成员也可以被修改
  在c++的类中, 如果一个函数被const 修饰,那么它将无法修改其成员变量的,但是如果这个成员变量是被mutable修饰的话,则可以修改。
  例如
  struct data
  {
  char name[30];
  mutable int accesses;
  ....
  };
  const data veep = {"david";,0,}
  strcpy(veep.name,"Jimmy");// not allowed
  veep.accesses++; // allowed
  veep 的const限定符禁止程序修改veep的成员,但access成员的mutable说明符表示access不受这种限制
const消除了预处理器的值替代的不良影响,并且提供了良好的类型检查形式和安全性,在可能的地方尽可能的使用const对我们的编程有很大的帮助。
const类型定义:指明变量或对象的值是不能被更新,引入目的是为了取代预编译指令

**************常量必须被初始化
*************************

cons的作用

   (1)可以定义const常量         例如:
             const int Max=100;
             int Array[Max];        
   
(2)便于进行类型检查            例如:
             void f(const int i) { .........}
        
编译器就会知道i是一个常量,不允许修改;
   (3)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
        还是上面的例子,如果在函数体内修改了i,编译器就会报错;
        例如:
             void f(const int i) { i=10;//error! }
    (5) 为函数重载提供了一个参考。

         class A
         {
           ......
           void f(int i)       {......} file://
一个函数
           void f(int i) const {......} file://上一个函数的重载
            ......
          };
    
(6) 可以节省空间,避免不必要的内存分配。
         例如:
              #define PI 3.14159         file://常量宏
              const doulbe  Pi=3.14159;  file://此时并未将Pi放入ROM中,而是记录在常量表中。 在通常的情况下const同预处理器#define一样只是将所赋值保存入编译器的符号表中(符号表仅仅在编译时存在,在编译过程中编译器将程序中的名字与之在符号表中定义的数值作简单的替换),在使用的时候进行值替换,并不为const创建存储空间。
              ......
              double i=Pi;               file://
此时为Pi分配内存,以后不再分配!
              double I=PI;               file://编译期间进行宏替换,分配内存
              double j=Pi;               file://没有内存分配
              double J=PI;               file://再进行宏替换,又一次分配内存!
    const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
     (7) 提高了效率。
           编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

使用const
   (1)修饰一般常量,常数组,常对象

   修饰符const可以用在类型说明符前,也可以用在类型说明符后。      例如:   
           int const x=2;  或  
const int x=2;

       int const a[5]={1, 2, 3, 4, 5};    或
  const int a[5]={1, 2, 3, 4, 5};

           class A;      const A a;  或
     A const a;
  
   
   (2)修饰指针

        const int *A;   或  int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
        int *const A;              //const修饰指针A,     A不可变,A指向的对象可变
        const int *const A;      //指针A和A指向的对象都不可变

   (3)修饰引用
       const double & v;      该引用所引用的对象不能被更新
 (4)修饰函数的返回值:
        const修饰符也可以修饰函数的返回值,是返回值不可被改变,格式如下:
            const int Fun1(); 
            const MyClass Fun2();
   
(5)修饰类的成员函数:
        const修饰符也可以修饰类的成员函数,格式如下:
            class ClassName 
     {
             public:
            
      int Fun() const;
             
       .....
             };

        这样,在调用函数Fun时就不能修改类里面的数据
    (6)在另一连接文件中引用const常量

         extern const int i;     //正确的引用
         extern const int j=10;  //错误!常量不可以被再次赋值
   


*******************
放在类内部的常量有什么限制?
  
        class A
        {
         private:
           const int c3 = 7;           // err  const
成员只能在定义对象的时候初始化
           static int c4 = 7;          // err,静态数员成员只能在外部初始化
           static const float c5 = 7;  // err
          ......
  };
初始化类内部的常量
        1 初始化列表:
         class A
         {
          public:
                A(int i=0):test(i) {}
          private:
                const int i;
          }

         2 外部初始化,例如:
         class A
         {
          public:
                A() {}
          private:
                static const int i;  
          }

          const int A::i=3;
C++ const 用法总结
#include <iostream>
void func(const int* p, int n);
const char * getStr();
class CA
{
private:
int _nA;
public:
CA(int a):_nA(a)
{
}
inline int getA() const
{
   //_nA++; //error C2166: l 
值指定常数对象
   return _nA;
}
inline void setA(int a)
{
   _nA = a;
}
};
int main(void)
{
using namespace std;
//===========================================================
// 1.const
修饰变量使其成为常量
//===========================================================
{
   cout<<"----const
修饰变量使其成为常量:"<<endl;
   const int a = 5;
   cout<<"a = "<<a<<endl;
   //a = 3; //error C2166: l 值指定常数对象

   //int * pa = &a; //error C2440: “初始化” : 无法从“const int *__w64 ”转换为“int *”
   //int * const pa = &a; //error C2440: “初始化” : 无法从“const int *__w64 ”转换为
“int *const ”
   const int * pa1 = &a;
   int const * pa2 = &a;
   cout<<"*pa1 = "<<*pa1<<endl;
   cout<<"*pa2 = "<<*pa2<<endl;
   //*pa1 = 3; //error C2166: l 值指定常数对象

}
//===========================================================
// 2.const
修饰指针的几种形式
//===========================================================
cout<<"----const
修饰指针的几种形式:"<<endl;
//第一种,常量指针,指针是常量,不能修改指向

{
   int a1 = 3;
   int a2 = 4;
   int * const pa = &a1;
   //pa = &a2; //error C2166: l 
值指定常数对象
   *pa = 5;
   cout<<"a1 = "<<a1<<endl;
}
//第二种,指向常量的指针,不能通过此指针修改指向的变量的值
{
   int a = 3;
   const int * pa1 = &a;
   int const * pa2 = &a;
   //*pa1 = 5; //error C2166: l 
值指定常数对象
   a = 5;
   cout<<"a = "<<a<<endl;
   const int A = 3;
   const int *pA = &A;
   //*pA = 5; //error C2166: l 
值指定常数对象
   //A = 5; //error C2166: l 值指定常数对象
}
//第三种,指向常量的常量指针,既不能修改指向,也不能修改指向变量的值
{
   int a1 = 3;
   int a2 = 4;
   const int * const pa = &a1;
   //pa = &a2; //error C2166: l 
值指定常数对象
   //*pa = 5; //error C2166: l 值指定常数对象
}
//===========================================================
// 3.const
修饰函数参数
//===========================================================
cout<<"----const
修饰函数参数:"<<endl;
//const一般用于修饰指针或引用类型的函数参数,以避免在函数内部修改其指向的值,

//不要用于修饰值传递类型的参数,这样做没有意义。
{
   int a = 3;
   func(&a, 5);
}
//===========================================================
// 4.const
修饰函数返回值
//===========================================================
cout<<"----const
修饰函数返回值:"<<endl;
//返回值为const的指针只能赋值给同类型的const指针

{
   //char * p = getStr(); //error C2440: “初始化” : 无法从“const char *”转换为“char *” 转换丢失限定符
   const char * p = getStr();
   cout<<p<<endl;
}
//===========================================================
// 5.const
修饰类成员函数
//===========================================================
cout<<"----const
修饰类成员函数:"<<endl;
//在const修饰的类成员函数内部,不能修改类成员变量的值

CA a(1);
cout<<a.getA()<<endl;
cin.get();
return 0;
}
void func(const int * p, int n)
{
using namespace std;
cout<<"*p = "<<*p<<endl;
cout<<"n = "<<n<<endl;
//*p = n;   //error C2166: l 
值指定常数对象
}
const char* getStr()
{
return "hello";
}
面向对象是C++的重要特性. 但是c++在c的基础上新增加的几点优化也是很耀眼的就const直接可以取代c中的#define,以下几点很重要,学不好后果也也很严重
const
1. 限定符声明变量只能被读
   const int i=5;
   int j=0;
   ...
   i=j;   //非法,导致编译错误
   j=i;   //合法
2. 必须初始化
   const int i=5;    //合法
   const int j;      //非法,导致编译错误
3. 在另一连接文件中引用const常量
   extern const int i;     //合法
   extern const int j=10;  //非法,常量不可以被再次赋值
4. 便于进行类型检查
   const方法可以使编译器对处理内容有更多了解。
   #define I=10
   const long &i=10;   /*dapingguo提醒:由于编译器的优化,使
      得在const long i=10; i不被分配内存,而是已10直接代入
      以后的引用中,以致在以后的代码中没有错误,为达到说教效
      果,特别地用&i明确地给出了i
的内存分配。不过一旦你关闭所
      有优化措施,即使const long i=10;也会引起后面的编译错误。*/
   char h=I;      //没有错
   char h=i;      //编译警告,可能由于数的截短带来错误赋值。
5. 可以避免不必要的内存分配
   #define STRING "abcdefghijklmn\n"
   const char string[]="abcdefghijklm\n";
   ...
   printf(STRING);   //STRING分配了第一次内存
   printf(string);   //string一次分配了内存,以后不再分配

   ...
  printf(STRING);   //STRING分配了第二次内存
   printf(string);
   ... 
   由于const定义常量从汇编的角度来看,只是给出了对应的内存地址,
   而不是象#define一样给出的是立即数,所以,const定义的常量在
   程序运行过程中只有一份拷贝,而#define定义的常量在内存中有
   若干个拷贝。

6. 可以通过函数对常量进行初始化
   int value(); 
   const int i=value();
   dapingguo说:假定对ROM编写程序时,由于目标代码的不可改写,
   本语句将会无效,不过可以变通一下:
   const int &i=value();
   只要令i的地址处于ROM之外,即可实现:i通过函数初始化,而其
   值有不会被修改。
7. 是不是const的常量值一定不可以被修改呢?
   观察以下一段代码:
   const int i=0;
   int *p=(int*)&i;
   p=100;
   通过强制类型转换,将地址赋给变量,再作修改即可以改变const常量值。

8. 请分清数值常量和指针常量,以下声明颇为玩味:
   int ii=0;
   const int i=0;            //i是常量,i的值不会被修改
   const int *p1i=&i;        //指针p1i所指内容是常量,可以不初始化
   int  * const p2i=&ii;     //指针p2i是常量,所指内容可修改
   const int * const p3i=&i; //指针p3i是常量,所指内容也是常量
   p1i=&ii;                  //合法
   *p2i=100;                 //合法
关于C++中的const关键字的用法非常灵活,而使用const将大大改善程序的健壮性,参考了康建东兄的const使用详解一文,对其中进行了一些补充,写下了本文。

1.       const常量,如const int max = 100;  
优点:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)

2.       const 修饰类的数据成员。如:
class A

{

    const int size;

    …

}

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const数据成员的值是什么。

class A

{

const int size = 100;    //错误

int array[size];         //错误,未知的size

}

const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如

class A

{…

enum {size1=100, size2 = 200 };

int array1[size1];

int array2[size2];

}

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

3.   const修饰指针的情况,见下式:

int b = 500; 
const int* a = &           [1] 
int const *a = &           [2] 
int* const a = &           [3] 
const int* const a = &     [4]
如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effective c++Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,[1][2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3 [3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。

4.    const的初始化
先看一下const变量初始化的情况

1) 非指针const常量初始化的情况:A b;     // A为前面自定义的类类型
const A a = b;
2) 指针const常量初始化的情况:

A* d = new A(); 
const A* c = d; 
或者:const A* c = new A();

3)引用const常量初始化的情况:
A f; 
const A& e = f;      // 这样作e只能访问声明为const的函数,而不能访问一            
般的成员函数;
    [思考1]以下的这种赋值方法正确吗?
    const A* c=new A(); 
    A* e = c; 
    [思考2]以下的这种赋值方法正确吗?
    A* const c = new A(); 
    A* b = c;

5.     另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:A& operator=(const A& a);
void fun0(const A* a ); 
void fun1( ) const; // fun1( ) 为类成员函数
const A fun2( );

1 修饰参数的const,如 void fun0(const A* a ); 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引用传递,如果内部数据类型如int , char, double之类并且是值传递,则不要改成const引用传递。)

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

2  修饰返回值的const,如const A fun2( ); const A* fun3( );  此时返回值不能作为左值而被赋值
这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。const Rational operator*(const Rational& lhs, const Rational& rhs)                //对()运算符的重载

return Rational(lhs.numerator() * rhs.numerator(), 
lhs.denominator() * rhs.denominator()); 
}
返回值用const修饰可以防止允许这样的操作发生:Rational a,b;
Radional c;   
(a*b) = c;
一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。

[总结]

1.     一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回值为某个对象为constconst A test = A 实例)或某个对象的引用为constconst A& test = A实例)则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到

2.       如果给采用指针传递方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针,这样该指针和GetString返回的指针指向同一个对象,它们都不能修改返回的指针指向的对象。如:

const char * GetString(void);

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

char *str=GetString();

正确的用法是:

const char *str=GetString();
*strGetString返回的指针指向同一个对象,并且不能利用它们指向对象来改变对象的值。但是str指针本身是可以改变指向的。只是不能利它来改变当前指向对象的值

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

class A

{…

A &operate = (const A &other);  //赋值函数

}
A a,b,c;              //a,b,cA的对象


a=b=c;            //正常

(a=b)=c;          //不正常,但是合法

若赋值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
[思考3] 这样定义赋值操作符重载函数可以吗? 
const A& operator=(const A& a);

6.     类成员函数中const的使用
一般放在函数体后,形如:void fun() const; 
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const
成员函数,编译器将报错,这大大提高了程序的健壮性。如:

class Stack

{

public:

      void Push(int elem);

      int Pop(void);

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

private:

      int m_num;

      int m_data[100];

};

int Stack::GetCount(void) const

{

  ++m_num;              //编译错误,企图修改数据成员m_num

  Pop();                    //编译错误,企图调用非const
函数

  Return m_num;

}
7 const关键字可以用于参与重载函数的区分。例如:
    void Print(); 
    void Print() const 
    这两个函数可以用于重载。重载的原则是:常对象调用常成员函数,一般对象调用一般成员函数。
8.     使用const的一些建议
要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上
4 const在成员函数中的三种用法(参数、返回值、函数)要很好的使用; 
不要轻易的将函数的返回值类型定为const; 
6除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
[思考题答案
这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
这种方法正确,因为声明指针所指向的内容可变; 
这种做法不正确; 
const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
A a,b,c: 
(a=b)=c; 
因为a.operator=(b)的返回值是对aconst引用,不能再将c赋值给const常量。
2.static的用法:
静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值。
静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见。
用法1:函数内部声明的static变量,可作为对象间的一种通信机制
如果一局部变量被声明为static,那么将只有唯一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量。这个对象将只在执行线程第一次到达它的定义使初始化。
用法2:局部静态对象
对于局部静态对象,构造函数是在控制线程第一次通过该对象的定义时调用。在程序结束时,局部静态对象的析构函数将按照他们被构造的相反顺序逐一调用,没有规定确切时间。
用法3:静态成员和静态成员函数
如 果一个变量是类的一部分,但却不是该类的各个对象的一部分,它就被成为是一个static静态成员。一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本。同理,一个需要访问类成员,而不需要针对特定对象去调用的函数,也被称为一个static成员函数。
类的静态成员函数只能访问类的静态成员(变量或函数)。

3.extern的用法:

extern可以声明其他文件内定义的变量。在一个程序里,一个对象只能定义一次,它可以有多个声明,但类型必须完全一样。如果定义在全局作用域或者名字空间作用域里某一个变量没有初始化,它会被按照默认方式初始化。
将变量或函数声明成外部链接,即该变量或函数名在其它函数中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。
在C++中,还可以指定使用另一语言链接,需要与特定的转换符一起使用。
extern “C” 声明语句
extern “C” { 声明语句块 }

4.volatile的用法:
类型修正符(type-modifier),限定一个对象可被外部进程(操作系统、硬件或并发进程等)改变。volatile与变量连用,可以让变量被不同的线程访问和修改。声明时语法:
int volatile vInt;
常用于像中断处理程序之类的异步进程进行内存单元访问。
除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
注意:可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
1 0