一. const
1. 定义:
变量声明中带有关键词const,意味着不能通过赋值,增量或减量来修改该变量的值,这是显而易见的一点。
指针使用const则要稍微复杂点,因为不得不把 让指针本身成为const 和 指针指向的值成为cons t区别开来:
下面的声明表示pf指向的值必须是不变的,而pf则是可变的,它可以指向另外一个const或非const值
const float *pf;
相反,下面的声明说明pf是不能改变的,而pf所指向的值则是可以改变的:
float* const pf;
最后,当然可以有既不能改变指针的值也不能改变指针指向的值的值的声明方式:
const float * const pf;
需要注意的是,还有第三种放置const关键字的方法:
float const * pf; //等价于constfloat * pf;
总结就是:一个位于*左边任意位置的const使得数据成为常量,而一个位于*右边的const使得指针本身成为const
const 与 define的区别:
尽量用const和inline而不用#define 这个条款最好称为:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。
(1)define是预处理宏,在编译的预处理阶段起作用,进行替换,而const是在 编译、运行的时候起作用,const修饰的只读变量是在编译的时候确定其值
(2)#define宏没有类型,而const修饰的只读变量具有特定的类型,define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的
(3)存储方式不同 define仅仅是一个宏,那里使用,就在那里展开,不占用内存。 const 常量会在内存中分配内存
const 优点:
(1)const常量可以进行类型检查,避免一些低级的错误 (2)在我们调试的时候,const常量可以进行调试的,但是define是不能进行调试的,因为在预编译阶段就已经替换掉了
2. const在全局数据中的使用:
使用全局变量被认为是一个冒险的方法,它使得数据在程序的任何部分都可以被错误地修改,如果数据是const,那么这种担心就是多余的了不是嘛?因此对全局数据使用const是合理的。
然而,在文件之间共享const数据要格外小心,有两个策略可以使用:
1)一个是遵循外部变量的惯用规则,在一个文件进行定义声明,在其他文件进行引用声明(使用关键字extern)。
/*file1.c------定义一些全局常量*/
const double PI = 3.14159;
/*file2.c-----是用在其他文件中定义的全局变量*/
extern const dounle PI;
2)另外一个方法是把全局变量放在一个include文件里,这时候需要格外注意的是必须使用静态外部存储类
/*constant.h----定义一些全局常量*/
static const double PI = 3.14159;
/*file1.c-----使用其他文件定义的全局变量*/
#include”constant.h”。
/*file2.c-----使用其他文件定义的全局变量*/
#include”constant.h”
如果不使用关键字static,在文件file1.c和file2.c中包含constant.h将导致每个文件都有同一标识符的定义声明,ANSI标准不支持这样做(有些编译器确实支持)。通过使用static, 实际上给了每个文件一个独立的数据拷贝,如果文件想使用该数据与另外一个文件通话,这样做就不行了,因为每个文件只能看见他自己的拷贝,然而由于数据是不 可变的,这就不是问题了。使用头文件的好处是不必惦记在一个文件中进行定义声明,在另一个文件中进行引用声明,缺点在于复制了数据,如果常量很大的话,这 就是个问题了。
3. const的常规用法1)修饰局部变量
const int n=5;int const n=5;这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了。
接下来看看const用于修饰常量静态字符串,例如:
const char* str="fdsafdsa";如果没有const的修饰,我们可能会在后面有意无意的写str[4]='x'这样的语句,这样会导致对只读内存区域的赋值,然后程序会立刻异常终止。有了const,这个错误就能在程序被编译的时候就立即检查出来,这就是const的好处。让逻辑错误在编译期被发现。
2)常量指针与指针常量
常量指针是指针指向的内容是常量,可以有一下两种定义方式。
const int * n;int const * n;需要注意的是一下两点:
(1)常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。
int a=5;const int* n=&a;a=6;(2)常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。
int a=5;int b=6;const int* n=&a;n=&b;指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:
int *const n;需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。
int a=5;int *p=&a;int* const n=&a;*p=8;区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针',将const读作‘常量'的话,内容正好符合。int const * n;是常量指针,int *const n;是指针常量。
指向常量的常指针
是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
const int* const p;3)修饰函数的参数
根据常量指针与指针常量,const修饰函数的参数也是分为三种情况
(1)防止修改指针指向的内容
void StringCopy(char *strDestination, const char *strSource);其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
(2)防止修改指针指向的地址
void swap ( int * const p1 , int * const p2 )指针p1和指针p2指向的地址都不能修改。
(3)以上两种的结合。
4)修饰函数的返回值
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。 例如函数
const char * GetString(void);如下语句将出现编译错误:
char *str = GetString();正确的用法是
const char *str = GetString();4)修饰全局变量
全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,以为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样方式不必要的意外修改,使用的方法与局部变量是相同的。
以上就是const关键字的常见用法
二 volatile限定词volatile告诉编译器,该变量除了可被程序改变意外还可以被其他代理改变。典型的它用于硬件地址和其他并行运行的程序共享的数据。例如,一个地址中可能保存着当前的时钟信息。不管程序做些什么,该地址会随时间改变。另一种情况是一个地址用来接收来自其他计算机的信息;
语法同const:
volatile int a;//a是一个易变的位置
volatile int * pf;//pf指向一个易变的位置
把volatile作为一个关键字的原因是它可以方便编译器优化。
假如有如下代码:
va= x;
//一些不使用x的代码
vb= x;
一个聪明的编译器可能注意到你两次使用了x,但是没有改变它的值,它将把x临时存贮在一个寄存器中,接着,当vb需要x的时候,它从寄存器而非初始的内存位置得到x的值来节省时间。这个过程被称为缓存。通常缓存是一个好的优化方式,但是如果两个语句中间的其他代理改变了x的值的话就不是这样了。如果没有规定volatile关键字,编译器将无从得知这种改变是否可能发生,因此,为了安全起见,编译器不使用缓存。那是在ANSI以前的情形,现在,如果在声明中没有使用volatile关键字,编译器就可以假定一个值在使用过程中没有修改,它就可以试着优化代码。总而言之,volatile使得每次读取数据都是直接在内存读取而不是缓存。
你可能会觉得奇怪,const和volatile可以同时使用,但是确实可以。例如硬件时钟一般不能由程序改变,这使得他成为const,但他被程序以外的代理改变,这使得他成为volatile,所以你可以同时使用它们,顺序是不重要的:
const volatile time;
volatile表明某个变量的值可能在外部被改变,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。它可以适用于基础类 型如:int,char,long......也适用于C的结构和C++的类。当对结构或者类对象使用volatile修饰的时候,结构或者类的所有成员 都会被视为volatile.
该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。
简单示例:
复制代码代码如下:DWord __stdcall threadFunc(LPVOID signal){int* intSignal=reinterdivt_cast(signal);*intSignal=2;while(*intSignal!=1)sleep(1000);return 0;}该线程启动时将intSignal 置为2,然后循环等待直到intSignal 为1 时退出。显然intSignal的值必须在外部被改变,否则该线程不会退出。但是实际运行的时候该线程却不会退出,即使在外部将它的值改为1,看一下对应的伪汇编代码就明白了:mov ax,signallabel:if(ax!=1)goto label
对于C编译器来说,它并不知道这个值会被其他线程修改。自然就把它cache在寄存器里面。C 编译器是没有线程概念的,这时候就需要用到volatile。volatile 的本意是指:这个值可能会在当前线程外部被改变。也就是说,我们要在threadFunc中的intSignal前面加上volatile关键字,这时 候,编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取,所作的循环变为如下面伪码所示:label:mov ax,signal //重新从内存获取变量值,而不是从缓存if(ax!=1)goto label
注意:一个参数既可以是const同时是volatile,是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
三 restrict
关键字restrict通过允许编译器优化某几种代码增强了计算支持。记住,它只能用于指针,并且表明指针是访问一个数据对象的唯一且初始的方式。为了清楚为何这样做,我们需要看一些例子:
复制代码代码如下:intar[10];int* restrict restar = (int*)malloc(10*sizeof(int));
int* par = ar;
这里,指针restar是访问malloc分配的内存的唯一而且初始的方式,因此声明为restrict。然而,par指针既不是初始的,也不是访问数组ar中数据的唯一方式,所以不用restrict限定词。现在考虑下面这个更加复杂的例子,其中n是一个int复制代码代码如下:for(n= 0;n < 10;n++){
par[n]+= 5;
restar[n]+= 5;
ar[n]*= 2;
par[n]+= 3;
restar[n]+= 3;
}
知道了restar是访问它所指向的数据的唯一初始方式,编译器就可以用具有同样效果的一条语句来替代包含restar的两个语句restar[n]+= 8;/*可以替换*/
然而将两个计算par的语句精简为一个则会导致错误因为在par两次访问数据之间,ar改变了该数据的值。没有关键字restrict,编译器将不得不设想比较糟糕的那一种形式,而使用之后,编译器可以放心大胆的寻找计算的捷径。可以将关键字作为指针型函数参量的限定词使用,这意味着编译器可以假定在函数体内没有其他标志符修改指针指向的数据,因而可以试着优化代码,反之不然。来看一下C99标准下C库中的两个函数,他们从一个位置把字节复制到另一个位置
void*memcpy(void* restrict s1,const void* restrict s2,size_t n);
void*memmove(void* s1,const void * s2,size_t);
memcpy要求两个指针的位置不能重叠,但memmove没有这个要求。把s1,s2声明为restrict意味着每个指针都是相应数据的唯一访问方式,因此他们不能访问同一数据块。这满足了不能有重叠的要求。
关键字restrict有两个读者:编译器,它告诉编译器可以自由地做一些优化的假定。另一个读者是用户,他告诉用户仅使用满足restrict要求的参数。一般,编译器没法检查你是否遵循了这一限制,如果你蔑视它,也就是让自己冒险。
参考文档:
http://www.jb51.net/article/70831.htm
http://www.jb51.net/article/42348.htm
新闻热点
疑难解答