本文使用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;
  1. 从标识符p开始往左看,声明中有*号,则p为指针,p指向的类型为const intint const(等价);
  2. 标识符p旁边没有const关键字,则指针的指向可以被改变(可以对p赋值);
  3. const关键字贴近数据类型int,则指针指向的数据不可变(不能对*p赋值);

2.3 int * const p

*(++指针++先)->const(++常量++后),按顺序就是“指针常量”,指针常量强调常量,即p本身是常量,因此p不能赋值,p指针(认为自己)指向的是一个整形数的地址,但自己不能被改变。(中文叫法可忽略)

int a = 1;
int * const p = &a;
  1. 从标识符p开始往左看,声明中有*号,则p为指针;
  2. const关键字贴近标识符p,则指针的指向不可被改变(不能对p赋值);
  3. 数据类型int旁边没有const关键字,则指针指向的数据可以被改变(可以对*p赋值);
  4. 由于指针的指向不可被改变,p在定义时必须完成初始化,后续再对p赋值将编译失败。

2.4 const int * const p

int a = 1;
const int * const p = &a;
  1. 从标识符p开始往左看,声明中有*号,则p为指针;
  2. const关键字贴近标识符p,则指针的指向不可被改变(不能对p赋值);
  3. const关键字贴近数据类型int,则指针指向的数据不可被改变(不能对*p赋值);
  4. 由于指针的指向不可被改变,p在定义时必须完成初始化,后续再对p赋值将编译失败。

2.5 const int * const *pp

int a = 1;
const int * const p = &a;
const int * const *pp = &p;
  1. 从标识符pp开始往左看,声明中有*号,则pp为指针;
  2. 声明中有两个*号,则pp为指针的指针;
  3. const int * const *pp分为两部分:const int * const*pp,可以发现ppconst int * const类型的指针,而const int * const 类型本身就是个指针类型,这个类型指针指向的数据不可被改变指针的指向不可被改变
  4. const int * const为“指向常量的常量指针”类型 (中文叫法可忽略),则const int * const *为“指向常量的常量指针的指针”类型 **(中文叫法可忽略)**;
  5. 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);
  1. src标识符往左看,声明中有*号,因此src为指针;
  2. src标识符旁没有const关键字,则指针的指向可以被改变(可以对src赋值);
  3. 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本身也不能变了,不过,正常人会写这个???