const关键字是ANSI标准新增加的关键字。const是个类型限定符,可以和任何类型说明符一起使用,以指定被声明对象的特殊属性。C语言的类型说明符包括:
- void
- char
- short
- int
- long
- float
- double
- signed
- unsigned
- 结构体说明符
- 联合体说明符
- 枚举说明符
- 类型定义名
const用于声明可以存放在只读存储器中的对象,并可能提高优化的可能性。
概念是抽象的,下面我们从几个方面来说一下const的用法:
1. 修饰局部变量
const int n=5;
int const n=5;
这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了。
关键字const并不能把变量变成常量!在一个符号前加上const限定符只是表示这个符号不能被赋值,但是不能阻止通过其它的方法来修改这个值。下面的例子就demo了这点。
int main(void)
{
const int limit = 10;
int *p = &limit;
/* error: assignment of read-only variable ‘limit’ */
/* limit = 1; */
*p = 11;
printf("%d\n", limit); /* OK, limit=11 now */
return 0;
}
2. const与指针
2.1 常量指针
const int * n;
int const * n;
这段代码是等价的,但是一般建议用第一个。表示n是一个指向整形常量的指针,简称常量指针。
需要注意的是一下两点:
- 常量指针说的是不能通过这个指针改变它所指向的变量的值,但是还是可以通过其他的引用来改变变量的值。
int main(void)
{
int a = 1;
const int *limitp = &a;
int i = 27;
/* error: assignment of read-only location ‘*limitp’ */
/* *limitp = 1; */
a = 2; /* OK */
}
- 常量指针指向的值不能改变,但是常量指针可以指向其他的地址。
printf("limitp %p, %d\n", limitp, *limitp);
limitp = &i;
printf("limitp %p, %d\n", limitp, *limitp);
2.2 指针常量
指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:
int *const n;
需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。
int b = 100;
int *const p2 = &b;
/* error: assignment of read-only variable ‘p2’ */
/* p2 = &i; */
*p2 = 101; /* OK */
区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。int const * n;是常量指针,int *const n;是指针常量。
2.3 指向常量的常指针
const int* const p;
是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
int c = 1000;
const int *const p3 = &c;
int i = 1;
p3 = &i; /* error: assignment of read-only variable ‘p3’ */
*p3 = 1001; /* error: assignment of read-only location ‘*p3’ */
3. 修饰函数的参数
const最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指向的数据,但是其他的函数却可能会修改它。
根据常量指针与指针常量,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、以上两种的结合。
来看一个具体的例子,编译器对foo函数发出了一条警告,foo2正常
/* note: expected ‘const char **’ but argument is of type ‘char **’ */
void foo(const char **p)
{
p = p;
}
void foo2(const char *p2)
{
p = p;
}
int main(int argc, char **argv)
{
char *str = "test";
foo(argv);
foo2(str);
return 0;
}
也就是说实参char* str
与形参const char *p2
是相容的,标准库中所有的字符串处理函数都是这样的。那么,为什么实参char **argv
和形参const char **p
实际上不能相容呢?
答案是肯定的,它们并不相容。原因是…
在ANSI C标准第6.3.2.2节中讲述约束条件的小节中有这么一句话:
每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)
也就是说参数传递过程类似于赋值。所以除非一个类型为char **
的值可以赋值给一个const char **
类型的对象,否则肯定会产生一条诊断信息。关于赋值的部分,位于第6.3.16.1节,描述下列约束条件:
两个操作数都是指向有限定符或者无限定符的相容类型的指针,左边指针所指向的类型必须要具有右边指针所指向类型的全部限定符。
之所以实参char* str
与形参const char *p2
是相容的,是因为下面的代码中:
char *cp;
const char *ccp;
cpp = cp;
左操作数是一个指向有const限定符的char的指针。
右操作数是一个指向没有限定符的char的指针。
char类型与char类型是相容的,左操作数所指向的类型具有右操作数所指向类型的限定符(无),再上自身的限定符(const).
注意,反过来就不能进行赋值。下面的代码将产生警告:
cp = cpp;
标准第6.3.16.1节有没有说实参char **
和形参const char **
是相容的呢?没有。
标准第6.1.2.5节中讲述实例的部分声称:
const float*
类型并不是一个有限定符的类型—它的类型是“指向一个具有const限定符的float类型的指针”,也就是说const限定符是修饰指针所指向的类型,而不是指针本身。
类似地,const char **
也是一个没有限定符的指针类型。它的类型是“指向一个有const限定符的char类型的指针的指针”。由于char **
和const char **
都是没有限定符的指针类型,但它们所指向的类型不一样,前者指向的是char *
,后者指向的是const char *
,因此它们是不相容的。因此,类型为char **
的实参与类型为const char **
的形参是不相容的,因为违法了标准第6.3.2.2节中讲述约束条件,编译器必然会产生一条诊断信息。
4. 修饰函数的返回值
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
例如函数
const char * GetString(void);
如下语句将出现编译警告,但是没有编译错误:
/* warning: assignment discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers] */
char *str = GetString();
正确的用法是
const char *str = GetString();
5. 修饰全局变量
全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致出了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰。
参考:
《The C Programming Language中文版(第2版.新版)》
《C专家编程》