我们经常将 const 变量称为常量(Constant)。创建常量的格式通常为:

1
const type name = value;

const 和 type 都是用来修饰变量的,它们的位置可以互换,也就是将 type 放在 const 前面:

1
type const name = value;

但我们通常采用第一种方式,不采用第二种方式。另外建议将常量名的首字母大写,以提醒程序员这是个常量。如下:

1
const int AGE = 22;

C语言const是如何保证变量不被修改的?

保证变量不被修改的方式有两种:
由编译器来阻止修改const变量的语句,让这种程序不能通过编译;
由操作系统来阻止,即把const 的内存地址访问权限标记为“只读”,一旦运行中的程序试图修改它,就会产生异常,终止进程。

上述两种方式,都可以达到让某一变量的值不被修改的目的,那么,实际运行时,就是是哪一种呢?

先来看一个简单的例子,源文件const.c:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
const int a=10;
int main()
{
int *p=&a;
printf("initial: %d\n",a);
*p=1;
printf("modified: %d\n",a);
return 0;
}

编译,会收到一个 warning:

1
2
3
4
$ gcc -o const1 const1.c
const.c: In function ‘main’:
const.c:7:12: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
int *p=&a;

忽略之,运行程序:

1
2
3
$ ./const1
initial: 10
Segmentation fault (core dumped)

运行出错了,报错是“segmentation fault”,即“段错误”,它是在提醒我们,程序中用错误的权限访问了内存某区域。这说明,操作系统把变量a加载到了一段只读内存区域之中,因此对该区域地址的写操作将引发异常,这是由操作系统的内存保护机制决定的

也就是说,在这段程序里,const的只读属性是由操作系统来实现的,而不是由编译器来实现的(编译器只抛出了warning,并没有阻止编译通过)。

这对吗?不完全对,我们来看另一个例子,源文件const2.c:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
const int a=10;
int *p=&a;
printf("initial: %d\n",a);
*p=1;
printf("modified: %d\n",a);
return 0;
}

编译,还是收到同样的warning:

1
2
3
4
$ gcc -o const2 const2.c
const.c: In function ‘main’:
const.c:6:12: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
int *p=&a;

忽略之,运行程序:

1
2
3
$ ./const2
initial: 10
modified: 1

咦?怎么成功运行了,而且a的值还被顺利修改了?

结合以上两个例子,我们可以得出以下推测:

const只是C语言中的一种对变量的修饰符,例子中的a,与其说是“常量”,不如说是“不打算修改的变量”。它只是语法上的一种声明,它的作用就是告诉编译器“我不想修改它”,因此编译器会从语法上检查程序中是否有修改它的语句(例如“a=1;”),一旦发现这种“违背初衷”的语句,就会报错阻止你。

然而,编译器所阻止的仅仅是对a这个符号对应值的修改而已,却并不阻止对这个地址的值的修改,源文件“const2.c”之所以能顺利通过编译且正常运行,就是因为它利用一个名字不叫a的指针指向它,从而绕过了编译器的语法检查。

打个比方,周树人的笔名叫鲁迅,警察只知道要抓鲁迅,这时候他就可以用一句“你们抓鲁迅跟我周树人有什么关系?”来骗过他们。

从这个角度来说,const的作用是靠编译器仅仅从语法检查来实现的,因此存在运行时的漏洞。

那么为什么“const1.c”就不能正常运行呢?

仔细看这两个源程序,区别仅仅在于,在“const1.c”中,a被声明为全局变量,而在“const2.c”中,它被声明为main函数中的一个局部变量。全局变量与局部变量的区别在于,前者会在程序开始运行之前就被加载,加载后会一直留在内存中,且加载的位置在数据区,直到程序退出;后者只有在运行到它时才会被加载,且加载的位置是运行时的栈帧,一旦超出作用于就会被回收。

因此,编译器会对被声明为全局变量的const int a进行优化,把它放到只读内存区内,这一内存区的权限是“read only”,权限信息由操作系统所维护的段表来保存,程序每访问某地址时,操作系统都会检测其访问权限是否合法。“const2.c”中企图用“写”的方式来访问“只读”的段,自然会报出“segment fault”的错了。

从这个角度来说,当a是全局变量时,编译器把原本只是“不打算修改的变量”优化成了“真正的常量”,然后交给操作系统去维持其不变属性。

综上所述,C的初衷只是让编译器去保证const的不变属性,这一属性有漏洞(可以用指针去骗过编译器修改它),所以当const修饰的对象是全局变量时(全局变量很重要,因为很多源文件都要访问它,牵一发而动全身,所以不应轻易更改),编译器知道自己的能力有限,只能管得了编译,管不了运行时如何,所以优化了语句把它编程真正的常量,让操作系统的内存保护功能来履行这一职责。

这一优化,并不是C规定的,而是编译器厂商出于实际应用的考虑作出的选择。

事实上 const 关键字只是给编译器参考的。const的行为都由编译器决定,上面的实验结果其实也是在特定的运行环境特定的编译器下的运行实验,并不严谨,但也是不错的参考。

来源:https://zhuanlan.zhihu.com/p/362747502

const 和指针

开始之前我们需要弄清指针变量指针常量

指针变量:通常我们说的指针都是指针变量,只是因为变量常常被省略不说,比如int* i,它是一个指向int类型的指针变量i。

指针常量:比如int* i,通过const关键字修饰变量ii就是常量了,我们称i为指针常量。

const 也可以和指针变量一起使用,这样可以限制指针变量本身,也可以限制指针指向的数据。const 和指针一起使用会有几种不同的顺序,如下所示:

1
2
3
const int *p1; //指向常量的指针(变量)
int const *p2; //和上面相同意思
int * const p3; //指针常量

在最后一种情况下,指针是只读的,也就是 p3 本身的值不能被修改;在前面两种情况下,指针所指向的数据是只读的,也就是 p1、p2 本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。

当然,指针本身和它指向的数据都有可能是只读的,下面的两种写法能够做到这一点:

1
2
const int * const p4; //指向常量int的指针常量
int const * const p5; //同上

const 和指针结合的写法多少有点让初学者摸不着头脑,大家可以这样来记忆:const 离变量名近就是用来修饰指针变量的,离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

C primer plus六版有介绍到,const关键字的作用以*为界,在左是指针指向的值不能被更改,在右是指针不能指向。

好,再来。

1
const int ** p6;//指向‘指向常量的指针变量’的指针变量

这是一个二级指针(指针指针),我们先确定符号(变量)p6是一个变量,然后是指针变量,该指针变量又指向了一个指针变量,然后那个指针又指向一个常量。

以下相同解法,不再赘述:

1
2
int const * const * p7 //指向‘指向常量的指针常量’的指针变量
int const * const * const//指向‘指向常量的指针常量’的指针常量

const 和函数形参

在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。
在C语言标准库中,有很多函数的形参都被 const 限制了,下面是部分函数的原型:
纯文本复制

1
2
3
4
5
6
7
size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, ... );

const 和非 const 类型转换

当一个指针变量 str1 被 const 限制时,并且类似const char *str1这种形式,说明指针指向的数据不能被修改;如果将 str1 赋值给另外一个未被 const 修饰的指针变量 str2,就有可能发生危险。因为通过 str1 不能修改数据,而赋值后通过 str2 能够修改数据了,意义发生了转变,所以编译器不提倡这种行为,会给出错误或警告。

也就是说,const char _和char _是不同的类型,不能将const char _类型的数据赋值给char _类型的变量。但反过来是可以的,编译器允许将char _类型的数据赋值给const char _类型的变量。

这种限制很容易理解,char _指向的数据有读取和写入权限,而const char _指向的数据只有读取权限,降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险。

C语言标准库中很多函数的参数都被 const 限制了,但我们在以前的编码过程中并没有注意这个问题,经常将非 const 类型的数据传递给 const 类型的形参,这样做从未引发任何副作用,原因就是上面讲到的,将非 const 类型转换为 const 类型是允许的。

下面是一个将 const 类型赋值给非 const 类型的例子:

1
2
3
4
5
6
7
8
#include <stdio.h>
void func(char *str){ }
int main(){
const char *str1 = "lautung.com";
char *str2 = str1;
func(str1);
return 0;
}

第7、8行代码分别通过赋值、传参(传参的本质也是赋值)将 const 类型的数据交给了非 const 类型的变量,编译器不会容忍这种行为,会给出警告,甚至直接报错。

参考

  1. https://zhuanlan.zhihu.com/p/362747502
  2. https://www.zhihu.com/question/443195492/answer/1723886545
  3. https://blog.csdn.net/Chen_rr/article/details/104855028