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)可以保护被修改的东西,防止意外的修改,增强程序的健壮性。例如:
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 nopconst定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#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’
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 56: 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; //再进行宏替换,又一次分配内存!
- C语言再学习 -- 关键字const
- C语言中const关键字学习
- C语言关键字----Const
- C语言关键字----Const
- C语言----------const关键字
- C语言关键字const
- c语言const关键字
- c语言关键字const
- C语言关键字const
- C语言:const关键字
- C语言关键字const再理解
- C语言const关键字作用
- C语言关键字const用法
- c语言const关键字用法
- C语言关键字 - 铁布衫:const
- C语言中const关键字
- C语言的const关键字
- C语言关键字 - 铁布衫:const
- 一个可能对你们有帮助的网站
- poj 3040 贪心
- linux作业(2)
- MySQL修复表的简单分析
- svn安装服务端、客户端
- C语言再学习 -- 关键字const
- 【BZOJ 2588】Spoj 10628. Count on a tree 主席树+树上差分
- Fiddler抓包工具以后调接口不用再用log啦
- 设计模式-面向对象的六大设计原则
- windows和linux 共享文件夹注意事项
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- 使用docker制作hexo镜像
- 如何快速转载CSDN中的博客
- 信号量