本文使用gcc版本: 编译环境:gcc version 11.1.0
0. 引言
C/C++语言支持const
关键字,const
意为“常数,不变的”,C++中可用于定义真正的常量,但在C语言中使用const
修饰的标识符并不是真正意义上的常量,为只读变量,本文讨论C语言const
关键字的常见用法。
1. const定义的标识符不是常量
1.1 使用const修饰的标识符定义数组
const.c
#include <stdio.h>
int main()
{
int arr[2] = {0};
return 0;
}
以上代码段只在main
函数中定义了一个长度为2的int
数组,C规定数组在编译时必须确定长度,这里显式地用2
这个字面常量定义数组长度,编译运行是没有问题的。
$ gcc const.c
$ ./a.out
如果使用const
修饰的变量定义数组,编译是否通过?
#include <stdio.h>
int main()
{
const int i = 2;
int arr[i] = {0};
return 0;
}
编译:
$ gcc const.c
const.c: In function ‘main’:
const.c:7:5: error: variable-sized object may not be initialized
int arr[i] = {0};
^~~
const.c:7:19: warning: excess elements in array initializer
int arr[i] = {0};
^
const.c:7:19: note: (near initialization for ‘arr’)
显然,程序在编译阶段就停止了,const
修饰的标识符i
在编译阶段无法确定其值,它不是个常量。
1.2 使用指针变量修改const修饰的“常量”
const.c
#include <stdio.h>
int main()
{
const int i = 2;
printf("after, i = %d\n", i);
int *p = (int *)&i;
*p = 3;
printf("after, i = %d\n", i);
return 0;
}
编译运行:
$ gcc const.c
$ ./a.out
before, i = 2
after, i = 3
可以看到使用const
修饰的“常量”i
的值被改变了。在当前上下文中,如果此时想直接通过赋值修改i
的值,如:
i = 4;/* 编译报错,const修饰的变量量不能被赋值 */
编译:
$ gcc const.c
const.c: In function ‘main’:
const.c:15:7: error: assignment of read-only variable ‘i’
i = 4;
^
编译是不能通过的,编译器提示这是个“只读变量”。
因此,C语言中使用const
关键字修饰的标识符本质是只读变量,不能显式给const
修饰的变量赋值,const
关键字修饰的标识符并不是真正意义上的常量。
2. const关键字的常见声明
注:个人觉得关于“常量指针”或“指针常量”或…………这些中文叫法不重要,who cares,可能由于翻译的问题,每个人的理解不同,关于这些叫法可能我本人是错的,关键在于写代码目前用的是英文,以英文去理解最准确,理解const用于声明中表示的意义,会用才是王道。
2.1 const int a
const int a = 1;/* 等价于int const a = 1; */
常用的只读变量的定义方式,不希望a
被修改时添加const
修饰。
2.2 const int *p
const
(++常量++先)->*
(++指针++后),按顺序就是“常量指针”,常量指针即常量的指针,强调指针,p指针(认为自己)指向的量是一个常量(在C里是只读变量)。(中文叫法可忽略)
int a = 1;
const int *p = &a;
- 从标识符
p
开始往左看,声明中有*
号,则p
为指针,p指向的类型为const int
或int const
(等价); - 标识符
p
旁边没有const
关键字,则指针的指向可以被改变(可以对p
赋值); const
关键字贴近数据类型int
,则指针指向的数据不可变(不能对*p
赋值);
2.3 int * const p
*
(++指针++先)->const
(++常量++后),按顺序就是“指针常量”,指针常量强调常量,即p本身是常量,因此p不能赋值,p指针(认为自己)指向的是一个整形数的地址,但自己不能被改变。(中文叫法可忽略)
int a = 1;
int * const p = &a;
- 从标识符
p
开始往左看,声明中有*
号,则p
为指针; const
关键字贴近标识符p
,则指针的指向不可被改变(不能对p
赋值);- 数据类型
int
旁边没有const
关键字,则指针指向的数据可以被改变(可以对*p
赋值); - 由于指针的指向不可被改变,
p
在定义时必须完成初始化,后续再对p
赋值将编译失败。
2.4 const int * const p
int a = 1;
const int * const p = &a;
- 从标识符
p
开始往左看,声明中有*
号,则p
为指针; const
关键字贴近标识符p
,则指针的指向不可被改变(不能对p
赋值);const
关键字贴近数据类型int
,则指针指向的数据不可被改变(不能对*p
赋值);- 由于指针的指向不可被改变,
p
在定义时必须完成初始化,后续再对p
赋值将编译失败。
2.5 const int * const *pp
int a = 1;
const int * const p = &a;
const int * const *pp = &p;
- 从标识符
pp
开始往左看,声明中有*
号,则pp
为指针; - 声明中有两个
*
号,则pp
为指针的指针; - 将
const int * const *pp
分为两部分:const int * const
和*pp
,可以发现pp
是const int * const
类型的指针,而const int * const
类型本身就是个指针类型,这个类型指针指向的数据不可被改变、指针的指向不可被改变; const int * const
为“指向常量的常量指针”类型(中文叫法可忽略),则const int * const *
为“指向常量的常量指针的指针”类型 **(中文叫法可忽略)**;pp
本身只是用于容纳const int * const
类型的指针,因此pp
本身不必须在定义时完成初始化,后续可以对pp
赋值,pp
的值为const int * const
类型,因此不能对*pp
赋值,也不可对**pp
赋值。
2.6 const关键字修饰函数参数
void *memcpy(void *dest, const void *src, size_t n);
- 从
src
标识符往左看,声明中有*
号,因此src
为指针; src
标识符旁没有const
关键字,则指针的指向可以被改变(可以对src
赋值);const
关键字贴近数据类型void *
,则指针指向的数据不可被改变(不能对*src
赋值);
从以上对memcpy
函数形参src
的分析中可知,使用const
关键字修饰形参指针,可以保护源数据在函数运行过程中不被意外修改,从而增强程序的健壮性。
3. 总结
本文首先从两个方面论证了const
关键字在C语言中只能定义只读变量,接着分析了几种C语言中常用的const
声明,const和指针混在一起时,经常会混淆这些声明,基本上只要记住:**声明中靠近const的量不可变(不能赋值)**这一原则去分析,就能分析出所以然:
const int a; //const靠近int,a不能变
const int *p; //const靠近int,p指向的内存数据不能变,p本身可以变
int * const p; //const靠近p,p本身不能变,p指向的内存数据可以变
const int * const p; //const既靠近int也靠近p,则p指向的内存数据不能变,p本身也不能变
const int * const *pp; //是二级指针(简称套娃),指向const int * const类型,pp可以变,*pp和**pp都不能变
const int * const * const pp;//这时pp本身也不能变了,不过,正常人会写这个???