C语言再学习 -- 关键字const

来源:互联网 发布:java商店管理系统 编辑:程序博客网 时间:2024/05/21 07:11

const 关键字其实我们并不陌生,之前有讲过const修饰数组和指针。现在来详细介绍这个关键字。

参看:【C/C++和指针】著名的《const的思考》


一、const 介绍

1、const 定义

const 修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的

2、const 目的

const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点(后面会讲到 const 与 宏的区别)。

3、const 作用

1)可以定义 const 常量,具有不可变性。例如:

const int Max = 100;  Max++会产生错误。

2)便于进行类型检查,使编译器对处理内容有更多了解,消除一些隐患。例如:

void f(const int i) {....} 编译器就会知道 i 是一个常量,不允许修改。

3)可以避免意义模糊的数字出现,同样可以很方便进行参数的调整和修改。同宏定义一样,可以做到不变则已,一变都变。

(这句话,没搞懂什么意思,希望有知道的大神告知!!)

4)可以保护被修改的东西,防止意外的修改,增强程序的健壮性。例如:

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #include <stdio.h>  
  2. void f (const int i)  
  3. {  
  4.     i = 10;  //在函数体内修改了 i ,编译器就会报错。  
  5. }     
  6. int main (void)  
  7. {  
  8.     f (1);  
  9.     return 0;  
  10. }  
  11.   
  12. 输出结果:  
  13. 错误: 向只读形参‘i’赋值  
5)可以节省空间,避免不必要的内存分配。例如:

#define PI 3.14159 //常量宏 
const double Pi=3.14159; //此时并未将Pi放入RAM中
double i=Pi; //此时为Pi分配内存,以后不再分配! 
double I=PI; //编译期间进行宏替换,分配内存 
double j=Pi; //没有内存分配 

double J=PI; //再进行宏替换,又一次分配内存! 

//test.c  #include <stdio.h>  int main (void)  {      const double Pi;      double i = Pi;      double j = Pi;      return 0;  }  
objdump -d test      080483b4 <main>:   80483b4:   55                      push   %ebp   80483b5:   89 e5                   mov    %esp,%ebp   80483b7:   83 e4 f8                and    $0xfffffff8,%esp   80483ba:   83 ec 20                sub    $0x20,%esp   80483bd:   dd 44 24 08             fldl   0x8(%esp)   80483c1:   dd 5c 24 10             fstpl  0x10(%esp)   80483c5:   dd 44 24 08             fldl   0x8(%esp)   80483c9:   dd 5c 24 18             fstpl  0x18(%esp)   80483cd:   b8 00 00 00 00          mov    $0x0,%eax   80483d2:   c9                      leave     80483d3:   c3                      ret     
//test1.c  #include <stdio.h>  #define PI 3.14159  int main (void)  {      double i = PI;      double j = PI;  }  
objdump -d test1    080483b4 <main>:   80483b4: 55                   push   %ebp   80483b5: 89 e5                mov    %esp,%ebp   80483b7: 83 e4 f8             and    $0xfffffff8,%esp   80483ba: 83 ec 10             sub    $0x10,%esp   80483bd: dd 05 b0 84 04 08    fldl   0x80484b0   80483c3: dd 1c 24             fstpl  (%esp)   80483c6: dd 05 b0 84 04 08    fldl   0x80484b0   80483cc: dd 5c 24 08          fstpl  0x8(%esp)   80483d0: c9                   leave     80483d1: c3                   ret       80483d2: 90                   nop   80483d3: 90                   nop  
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝。 

6)为函数重载提供了一个参考

class A{  void f(int i)       {......} //一个函数  void f(int i) const {......} //上一个函数的重载           ......};

7)提高效率

编译器通常不为普通 const 常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。


二、const 使用

1、const 修饰一般常量

一般常量是指简单类型的只读变量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如: 
int const x=2;  或  const int x=2;

const int a = 10;a = 20; // 错误,变量a为常量,只读,不能被修改;int const b = 10;b = 20; // 错误,变量b为常量,只读,不能被修改;

注意:

1)在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再改变它了

//C 下#include <stdio.h>int main (void){const int i;  //自动初始值为 随机数        //i = 10; //如果此时再向它赋值,会出现错误: 向只读变量‘i’赋值printf ("%d\n", i);}输出结果:-1217368076  //随机数
//C++下#include <iostream>int main (void) {   int const i;   //i = 10; //如果此时再向它赋值,会出现错误: 向只读变量‘i’赋值}/* gcc编译器不够严格,g++编译器下会报错 */错误: 未初始化的常量‘i’
2)const int const i = 10; 是否可行

//在C 下是可行的,但是还是不推荐使用#include <stdio.h>int main (void){const int const i = 10;printf ("%d\n", i);return 0;}输出结果:10
//在C++ 下是错误的#include <iostream>int main (void){const int const i = 10;std::cout << i << std::endl;}输出结果:错误: 重复的‘const’


扩展:常量与变量

参看:如何理解C语言常量与变量

说着说着,其实搞混了const到底修饰的是什么了。什么是常量,什么是变量?

常量,例如5, "abc",等,肯定是只读的,因为常量是被编译器放在内存中的只读区域,当然也就不能够去修改它。

enum类型和#define宏,这两个都可以用来定义常量。

采用宏定义#define指令创建一个指定数组大小的明显常量(SIZE),可以在定义数组和设置循环限制时使用这个常量,以后更改数组大小的时候方便处理,例如:

#define SIZE 5

int arr[SIZE];


变量 其值是可以改变的。一个变量应该有一个名字,在内存中占据一定的存储单元。变量定义必须放在变量使用之前。一般放在函数体的开头部分。要区分变量名和变量值是两个不同的概念。例如:int x = 3;


而“只读变量”则是在内存中开辟一个地方来存放它的值,只不过这个值由编译器限定不允许被修改。C语言关键字const就是用来 限定一个变量 不允许被改变的修饰符(Qualifier)。        

例如,const int a;

const只是一个修饰符,不管怎么样 a 仍然是一个int型的变量。

指定数组大小

直到C99标准出现之前,声明数组时在方括号内只能使用整数常量表达式。整数常量表达式是由整数常量组成的表达式。sizeof表达式被认为是一个整数常量,而(和C++不同)一个const值却不是整数常量。并且该表达式的值必须大于0。

#define SIZE 5  int n = 5;  float a1[5];            //可以  float a2[5*2 + 1];      //可以  float a3[sizeof (int) +1];  //可以  float a4[-4];           //不可以,数组大小必须大于0  float a5[0];            //不可以,数组大小必须大于0  float a6[2.5];          //不可以,数组大小必须大于0  float a7[(int)2.5];     //可以,把float类型指派为int类型  float a8[n];            //C99之前不允许  float a9[SIZE];         //可以  

//C99支持 这种形式,并不会报错#include <stdio.h>int main (void){    const int n = 5;    int a[n];    return 0;}


但是 const修饰的只读变量 不能放在 case  关键字后面、不能放在enum枚举名称后面,因为 case 关键字后面和枚举类型声明必须要 整数常量

#include <stdio.h>#define n 2  //常量int main (void){//int n = 2;   //变量,会出现错误: case 标号不能还原为一个整常量//const int n = 2;  //只读变量,会出现错误: case 标号不能还原为一个整常量switch (3){case 1:printf ("11111\n");break;case n:printf ("222222\n");break;case 3:printf ("333333\n");break;default:printf ("4444444\n");break;}return 0;}输出结果:333333

#include <stdio.h>#define n 3  //常量//int n = 3;  //变量,会出现错误: ‘QIU’的枚举值不是一个整数常量//const int n = 3;  //只读变量,会出现错误: ‘QIU’的枚举值不是一个整数常量typedef enum {CHUN =  1,XIA =  2,QIU = n,DONG = 4}Season;int main (void){printf ("%d\n", QIU);return 0;}输出结果:3


2、const修饰指针、数组

const定义的变量具有只读性const修饰的只读变量必须在定义的时候初始化

1)修饰数组

定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};或
const int a[5]={1, 2, 3, 4, 5};

const int numbers[] = {1, 2, 3, 4, 5};numbers[1] = 10; // 错误,数组被const修饰,因此,数组内容不可修改

2)修饰指针

这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。“近水楼先得月”,离谁近就修饰谁。

int arr[5];
const int *p = arr; //const 修饰*p,p 是指针,可变; *p 是指针指向的对象,不可变。
int const *p = arr; //const 修饰*p,p 是指针, 可变;*p 是指针指向的对象,不可变。
int *const p = arr; //const 修饰 p, p 是指针,不可变; p 指向的对象可变。
const int *const p= arr; //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变。

//示例一int a = 10;int b = 20;const int *p = &a;//等同 int const *p = &a;p  = &b; // 正确*p = 20; // 错误,指针变量p所指向的地址中的内容不能通过指针变量修改a  = 20; // 正确,变量a并没有被const关键字修饰;
//示例二int a = 10;int b = 20;int * const p = &a;p  = &b; // 错误,指针p只能指向同一个地址;*p = 20; // 正确
//示例三int a = 10;int b = 20;const int * const p = &a;p  = &b; // 错误*p = 20; // 错误

扩展:

指针数组和数组指针

指针数组:首先它是一个数组,数组的元素都是指针,例如:int *ptr1[10];

数组指针:首先它是一个指针,它指向一个数组,例如:int (*ptr2)[10];

这里需要明白一个符号之间优先级的问题,"[ ]"的优先级比"*"要高。p1 先与“ []”结合,构成一个数组的定义,数组名为 p1, int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个指向 int 类型数据的指针,即指针数组。

至于 p2 就更好理解了,在这里"( )"的优先级比"[ ]"高,"*"号和 p2 构成一个指针的定义,指针变量名为 p2, int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指
针,它指向一个包含 10 个 int 类型数据的数组,即数组指针。


为什么要讲指针数组和数组指针呢?是因为看到Dan Saks总结的const 用法很受启发。从另一个角度,来分析了const 的真实意义。

参看:const 的真实的意义(包含了Dan Saks以及一些网络人的理解)

文章从下面例子开始:

typedef void *VP;
const VP vectorTable[]
={..<data>..};           (1)
应该等同于:
const void* vectorTable[]
={..<data>..};           (2)
然而,在(1)中连接器把vectorTable放在了CONSTANT(只读)区,但是在(2)中却放在了DATA(数据)区。这是编译器的正常行为还是BUG?”


typedef关键字我们比较熟悉,参看:C语言再学习 -- 关键字typedef

如果 const 只是单纯的修饰指针,如,const void *P, void * const P,这也不过是简单考虑 指针常量,和常量指针问题。但是本例中修饰的 const void* vectorTable[] 是指针数组。这也是为什么要先区分指针数组和数组指针了。

再有就是需要清楚,存储类说明符和数据类型及类型修饰符。参看:C语言再学习--关键字

对应的就是文章里所说的声明说明符声明符

每一条C/C++声明语句都是有两个基本部分组成:零个或多个声明说明符序列;以及一个或多个 声明符序列,中间用逗号隔开。比如:

static unsigned int n = 3, m = 2;

extern int n; 等等

可以看出,存储类说明符,对于数据类型没有直接影响。而const 和 volatile 不是数据类型,它是限定符(specifier)。不会影响数据类型。

然后就明白了, 在(1)中,可看做 constVP vectorTable[] 修饰的是数组,所以vectorTable为只读;在(2)中,可以看做 constvoid *vectorTable[] 修饰的是指针数组,*vectorTable[]不可变,vectorTable[]是可变的,所以放在了DATA(数据)区。


3、const 修饰函数的形参和返回值

1)const 修饰符也可以修饰函数的传递参数,格式如下:

void Fun (const int Var);

告诉编译器Var在函数中是无法改变的,从而防止了使用者的一些无意或错误的修改。之前讲字符串,可以看到许多字符串函数就是如此定义的。

参看:C语言再学习 -- 字符串和字符串函数

2)const 修饰符也可以修饰函数的返回值,返回值不可被改变,格式如下:

const int Fun1 ( );

const MyClass Fun2 ( );

上述写法限定函数的返回值不可被更新,当函数返回内部的类型时,已经是一个数值,当然不可被赋值更新,所以,此时const无意义,最好去掉,以免困惑。当函数返回自定义的类型时,这个类型仍然包含可以被赋值的变量成员,所以,此时有意义。


在C++里,对 const 进行了进一步扩展 :

4、const 修饰常对象

常对象是指 对象常量,定义格式,如下:

class A;    const A a;A const a;

定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。


5、const 修饰常引用

使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。其定义格式,如下:
const double & v;


6、const 修饰类的成员变量

const修饰类的成员函数,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。

class A{     …    const int nValue;         //成员常量不能被修改    …    A(int x): nValue(x) { } ; //只能在初始化列表中赋值} 

规则:
1)const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
2)const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
3)const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
4) 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的。


7、const 修饰类的成员函数

const修饰符也可以修饰类的成员函数,格式如下:

class ClassName {public:   int Fun() const;  .....};

这样,在调用函数Fun时就不能修改类里面的数据 。

对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。


总结:

关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的
  如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
欲阻止一个变量被改变,可以使用 const 关键字。 
1)在定义该const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为 const,或二者同时指定为const;
3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
4)对于类的成员函数,若指定其为const 类型,则表明其是一个常函数,不能修改类的成员变量;
5)对于类的成员函数,有时候必须指定其返回值为const 类型,以使得其返回值不为“左值”。


三、const 与 extern 和 define的区别和联系

1、const 与 extern关系

参看:c与c++中的extern const的区别和联系

extern const int n;  //通过

extern const int i = 10;  //错误

示例一:

//file1.cconst int n = 10;
//file2.c#include <stdio.h>extern const int n;int main (void){printf ("%d\n", n);return 0;}
编译:gcc file1.c file2.c -o file输出结果:10
示例二:

#include <stdio.h>extern const int i = 10; //如果声明、定义int main (void){printf ("%d\n", i);return 0;}输出结果:警告: ‘i’已初始化,却又被声明为‘extern’


示例一中,gcc -c file1.c 生成 file1.o。然后使用 readelf -s file1.o 查看符号表: 

root@# readelf -s file1.o Symbol table '.symtab' contains 9 entries:   Num:    Value  Size Type    Bind   Vis      Ndx Name     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND      1: 00000000     0 FILE    LOCAL  DEFAULT  ABS file1.c     2: 00000000     0 SECTION LOCAL  DEFAULT    1      3: 00000000     0 SECTION LOCAL  DEFAULT    2      4: 00000000     0 SECTION LOCAL  DEFAULT    3      5: 00000000     0 SECTION LOCAL  DEFAULT    4      6: 00000000     0 SECTION LOCAL  DEFAULT    6      7: 00000000     0 SECTION LOCAL  DEFAULT    5      8: 00000000     4 OBJECT  GLOBAL DEFAULT    4 n
可以看到最后一行,n 在符号表中是 GLOBAL(全局)的。

接下来,我们不用改动代码,只是使用 g++ file1.c file2.c -o file 编译程序,可以看出错误:

 g++ file1.c file2.c -o file/tmp/cc3vh9lu.o: In function `main':file2.c:(.text+0xa): undefined reference to `n'collect2: ld 返回 1
链接错误原因是找不到 n 的定义。

使用 g++ -c file1.c 生成 file1.o,再使用 readelf -s file1.o 查看符号表:

readelf -s file1.o Symbol table '.symtab' contains 9 entries:   Num:    Value  Size Type    Bind   Vis      Ndx Name     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND      1: 00000000     0 FILE    LOCAL  DEFAULT  ABS file1.c     2: 00000000     0 SECTION LOCAL  DEFAULT    1      3: 00000000     0 SECTION LOCAL  DEFAULT    2      4: 00000000     0 SECTION LOCAL  DEFAULT    3      5: 00000000     0 SECTION LOCAL  DEFAULT    4      6: 00000000     4 OBJECT  LOCAL  DEFAULT    4 _ZL1n     7: 00000000     0 SECTION LOCAL  DEFAULT    6      8: 00000000     0 SECTION LOCAL  DEFAULT    5 
 6: 00000000     4 OBJECT  LOCAL  DEFAULT    4 _ZL1n

表明,n 变成了一个 LOCAL(本地)对象,只能在 file1.c 中可见,对file2.c 不可见。


解决方法:

将 file1.c中的

//file.cconst int n = 10;
改为:

//file.cextern const int n = 10;
这样g++编译器在第一次看到 n 的定义的时候,因为存在extern关键字,就把它当成GLOBAL对象写入符号表:

g++ -c file1.c 生成 file1.Oreadelf -s file1.o Symbol table '.symtab' contains 9 entries:   Num:    Value  Size Type    Bind   Vis      Ndx Name     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND      1: 00000000     0 FILE    LOCAL  DEFAULT  ABS file1.c     2: 00000000     0 SECTION LOCAL  DEFAULT    1      3: 00000000     0 SECTION LOCAL  DEFAULT    2      4: 00000000     0 SECTION LOCAL  DEFAULT    3      5: 00000000     0 SECTION LOCAL  DEFAULT    4      6: 00000000     0 SECTION LOCAL  DEFAULT    6      7: 00000000     0 SECTION LOCAL  DEFAULT    5      8: 00000000     4 OBJECT  GLOBAL DEFAULT    4 n


2、const 与 define 关系

参看:const的用法详解

上面有提到, 由于const定义常量从汇编的角度来看,只是给出了对应的内存地址, 而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

const 与define宏定义:
1)编译器处理方式不同:  
define宏是在预处理阶段展开;const常量是编译运行阶段使用。
2)类型和安全检查不同:define宏没有类型,不做任何类型检查,仅仅是展开;const常量有具体的类型,在编译阶段会执行类型检查;
3)存储方式不同:define宏仅仅是展开不会分配内存;const常量会在内存中分配;(只是说一般情况)
4)const 可以节省空间,避免不必要的内存分配。 例如:

#define PI 3.14159 //常量宏   const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......   double i=Pi; //此时为Pi分配内存,以后不再分配!   double I=PI; //编译期间进行宏替换,分配内存   double j=Pi; //没有内存分配   double J=PI; //再进行宏替换,又一次分配内存!  


0 0
原创粉丝点击