这小段文章要理清楚的是,在C语言中,const是如何保证变量不被修改的?

我们可以想到两种方式:

第一种,由编译器来阻止修改const变量的语句,让这种程序不能通过编译;

第二种,由操作系统来阻止,即把const 的内存地址访问权限标记为“只读”,一旦运行中的程序试图修改它,就会产生异常,终止进程。

上面想到的这两种方式,都能达到让某一变量的值不被修改的目的,那么究竟是哪一种呢?我们写两个例子来看一看。

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

#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:

$ 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;

忽略之,运行程序:

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

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

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

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

#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:

$ 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;

忽略之,运行程序:

./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规定的,而是编译器厂商出于实际应用的考虑作出的选择。

以上,是我根据编译器和程序运行时的行为所做的推测,这一思路并不妥当,只是我在编程时遇到了上述两个例子的困惑,又没找到说得很清楚的资料,所以就写出来了,若要进一步验证,应该查看编译后的可执行文件分段情况,我偷了个懒没看,暂时放在这里。

如果推测不正确,希望有前辈指出。

总结

到此这篇关于C语言中const如何保证变量不被修改的文章就介绍到这了,更多相关C语言const变量不修改内容请搜索程序员的世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持程序员的世界!

C语言中的const如何保证变量不被修改的更多相关文章

  1. OpenCV 之 图象几何变换

    二维平面中,图像的几何变换有等距、相似、仿射、投影等,如下所示: 1 图象几何变换1.1 等距变换 等距变换 (Isometric Transformation),是一种二维的刚体变换,可理解为旋转和平移的组合 $\quad \beg......

  2. 关于C++中构造函数的常见疑问

    基本概念我们已经知道在定义一个对象时,该对象会根据你传入的参数来调用类中对应的构造函数。同时,在释放这个对象时,会调用类中的析构函数。其中,构造函数有三种,分别是默认构造函数,有参构造函数和拷贝构造函数。在类中,如果我们没有自行定义任何的构造函数,编译器会为我们提供两种构造函数(默认构造函数和拷贝构......

  3. C/C++内存对齐详解

    1、什么是内存对齐 还是用一个例子带出这个问题,看下面的小程序,理论上,32位系统下,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte,这就是内存对齐所导致的。 //32位系统 #inclu......

  4. C语言位运算解决只出现一次的数字

    解题所需要的C语言基础知识hello!从现在开始就进入本题解的正式内容了。首先给大家用图解的方式介绍3个C语言位运算的基本操作符 & | ^这些知识对下面的解题都非常重要,一定要熟练掌握,不然等会会有一种“我在哪,我是谁我在干什么”的感觉。只出现一次的数字I题目描述只出现一次的数字给定一个非......

  5. C++ 入门篇

    C++基础入门 1 C++初识 1.1 第一个C++程序 编写一个C++程序总共分为4个步骤 创建项目 创建文件 编写代码 运行程序 1.1.1 创建项目 Visual Studio是我们用来编写C++程序的主要工具,我们先将它打开 1.1.2 创建文件 右键源文件,选......

  6. 关于 C++ 中的强制转换 - 基础篇

    引言假设有基类 A,包含了虚函数 func1,以及有派生类 B,继承于类 A,派生类 B 中实现了函数 func1。此时可以用 A 类型的指针指向 B 类型的对象,并用 A 类型的指针调用 B 类型对象中的函数 func1。这时,就形成了多态。包含虚函数的类 A,我们也称为多态类。由于派生类 B 完......

  7. 虚函数表-C++多态的实现原理解析

    参考:http://c.biancheng.net/view/267.html1、说明我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是什么呢?答案是虚函数表在 关于virtual 一文中,我们详细了解了C++多态的使用方式,我们知道没有 virtual 关键子就没法使用多态2、虚函......

  8. std::async的使用总结

    C++98标准中并没有线程库的存在,直到C++11中才终于提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。多线程库对应的头文件是#include ,类名为std::thread。然而线程毕竟是比较贴近系统的东西,使用起来仍然不是很方便,特别是线程同步及获取线程运行结......

  9. std::async的使用总结

    C++98标准中并没有线程库的存在,直到C++11中才终于提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。多线程库对应的头文件是#include ,类名为std::thread。然而线程毕竟是比较贴近系统的东西,使用起来仍然不是很方便,特别是线程同步及获取线程运行结......

  10. C语言之漫谈指针(下)

    C语言之漫谈指针(下)在上节我们讲到了一些关于指针的基础知识:详见:C语言之漫谈指针(上)本节大纲:零.小tips一.字符指针二.指针数组与数组指针三.数组传参与指针传参四.函数指针及函数指针数组五.回调函数六.例题讲解 零.小tips在正式开始下节之前,我们先来穿插两个小tips:1.打印函数......

随机推荐

  1. asp取整数mod 有小数的就自动加1

    有一位同学问我一个问题:asp程序,有一个不确定的数除以10,结果需要用asp程序处理取整数,如果有小数点就自动加1这个问题有两个解决思路,如果用在分页上,rs的属性pagecount就可以轻松实现,另外一种方法是数学判断方法。现在做分别介绍。除法分页方法rs.pagesize = 10这个代表每页......

  2. Linux中自定义shell脚本启动jar包的方法

    一键启动、停止、重启 java项目创建.sh文件vi XXX.sh编写shell脚本#!/bin/shport=8080 #定义变量等号左右不能有空格jar_name=/opt/oaclou/XXX.jar#运行脚本提示信息tips(){echo "--------------------......

  3. Python创建二叉树

    前言本文的内容是数据结构中二叉树部分最基础的,之所以写一下主要是为了方便刷题的时候,能够在自己电脑上很快的使用这种小的demo进行复杂的练习。二叉树节点定义二叉树的节点定义如下:class TreeNode():#二叉树节点def __init__(self,val,lchild=None,rchi......

  4. 解析Mybatis的insert方法返回数字-2147482646的解决

    前言:前几天在做项目demo的时候,发现有一个很奇怪的现象,就是MyBatis发现更新和插入返回值一直为"-2147482646".无论怎么改,这个值一直不变...是在摸不着头脑,百度和谷歌了一下,有这样的说法原来是由defaultExecutorType设置引起的,如果设置为B......

  5. MYSQL数据库操作语句

    1.创建数据库CREATE {DATABASE | SCHEMA} [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] create_specification: [DEFAULT] CHARACTE......

  6. Neo4j 导入 Nebula Graph 的实践总结

    主要介绍如何通过官方 ETL 工具 Exchange 将业务线上数据从 Neo4j 直接导入到 Nebula Graph 以及在导入过程中遇到的问题和优化方法。摘要: 主要介绍如何通过官方 ETL 工具 Exchange 将业务线上数据从 Neo4j 直接导入到 Nebula Graph 以及在导入......

  7. 细数JS中实用且强大的操作符&运算符

    1,前言博主收录了一些在实际开发过程中,很实用且方便的JS操作符,熟练掌握的话,不仅代码看上去高大上(实为装逼),而且简洁大方。2,代码+应用2.1,短路运算符 ||从左往右1,只要有一个条件为true时,结果就为true。2,当两个条件都为false时,结果才为false。3,当一个条件为true......

  8. 将不规则的Python多维数组拉平到一维的方法实现

    原始需求:例如有一个列表:l = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]希望把它转换成下面这种形式:[1, 2, 3, 4, 5, 6, 7, 8, 9]其实这个非常简单,我将分享三个一行式代码来解决这个问题。但如果是下面这种不规则的多维列表:l = [[1, 2], [......

  9. 使用JWT创建安全的ASP.NET Core Web API

    在本文中,你将学习如何在ASP.NET Core Web API中使用JWT身份验证。我将在编写代码时逐步简化。我们将构建两个终结点,一个用于客户登录,另一个用于获取客户订单。这些api将连接到在本地机器上运行的SQL Server Express数据库。JWT是什么?JWT或JSON Web To......

  10. php结合GD库实现中文验证码的简单方法

    前言上一次写了一个常见的验证码,现在玩一下中文的验证码,顺便升级一下写的代码流程基本差不多先看GD库开启了没生成中文5位验证码开始画图画干扰素生成图形完事生成中文验证码?1234567891011//小小心机$hanzi= "如果觉得写得还可以的话互相关注报团取暖交流经验来自合肥的小码农巴......