指针声明为常量和易失性变量

63

在阅读过程中,我遇到了这种声明类型和以下的一行代码 -

const volatile char *p=(const volatile char *) 0x30;

p的值仅受外部条件影响

我不明白所谓的“外部条件”是什么。这种声明有什么实际用途?


2
所有的 const 只是意味着你不能改变它。考虑 int a=5;int& x=a;,你仍然可以执行 a=6;(这会影响到 x!) - Alec Teal
2
@AlecTeal 在你的例子中,a 没有被声明为 const。你是不是想说 const int a=5; - ApproachingDarknessFish
3
我指的是 const int& x - 你不能通过 x 改变 a,但是 a 仍然可以改变! - Alec Teal
非常好的问题。我喜欢它。 - Destructor
6个回答

60

const表示程序的流程不会修改p所指向的内容。在解引用指针后尝试修改该值的任何操作都将导致编译时错误:

*p = 'A'; // will not compile

请注意,这并不是一个特别强的合约;除了 p 以外,通过别名非 const 指针仍然可以更改位于 0x30 处的值:
volatile char *q = 0x30;
*q = 'A'; // will compile

另一种打破这个合同的方法是从 p 中弃掉 const:

*(volatile char *) p = 'A'; // will compile

然而,volatile并不排除其他线程、内核、异步信号处理程序或具有访问同一内存空间的外部设备可能引起的任何修改。这样,编译器就不会错误地假设由p指向的值不变,并在每次引用时都从内存中加载它。
/*
 The character at 0x30 will be read on every iteration,
 even if the compiler has proven that the program itself
 doesn't modify the value at that address.
*/
while (*p) {
    ...
}

如果编译器错误地优化这样的结构,它可能会发出从内存中仅加载一次值并将其保存在寄存器中的指令。 寄存器实际上是一个独立的副本,对原始位置的任何更改都不会反映在那里,这显然会导致一些非常严重的错误。

1
内存中的值和寄存器中的值可能不同,这种声明能防止这种情况发生吗? - ameyCU
8
如果编译器决定将值存储在寄存器中,并且外部设备修改了指定位置的内存,由于寄存器是独立的存储位置,所以更改不会反映在寄存器中。 volatile 的作用就是避免这种情况发生。 - Blagovest Buyukliev
请注意,这仅在特殊情况下才是必需的——主要是硬件驱动程序和信号处理程序。即使对于多线程编程,它的用途也有限或没有。在“常规”编程中,您不需要volatile——在这里,寄存器由编译器与内存同步。 - DevSolar
@DevSolar 我现在明白了,除非我们不进行系统级编程,否则我们不需要这种类型。 - ameyCU
1
@Blagovest Buyukliev:嗯,第一句话是错误的;const并不像你说的那样。 (你可以添加“...通过lvalue p”来使你的第一句话正确。)此外,在程序中修改*p并不需要强制转换const,可以使用不相关的lvalue;可以编写(char *)0x30 ='a'。请参见AlecTeal在问题上的评论。 - philipxy
显示剩余4条评论

23

考虑一个只读的硬件寄存器,例如您的网络卡。

它可能会在程序控制之外发生改变,因此编译器不允许将其值缓存在寄存器中或优化掉。因此使用 volatile

而且它是只读的,所以你不应该对它进行写操作。因此使用 const


22

首先,让我引用来自标准、第6.7.3章节,类型限定符的例子

声明了一个对象

extern const volatile int real_time_clock;

该对象可能由硬件修改,但不能被赋值、增加或减少。

另外,相关的脚注(134),

volatile声明可以用于描述与内存映射输入/输出端口相对应的对象或由异步中断函数访问的对象。对这样声明的对象的操作不得被实现优化掉或重新排序,除非允许按照计算表达式的规则进行。

也就是说,变量的值可以通过硬件(通过内存映射)进行修改,但不能通过编程方式进行修改。

因此,在这里有两个优点:

  • 每次使用时,将从内存中读取值(不允许缓存),给您最新更新的值(如果有更新)。
  • 该值无法故意或无意地被程序更改 (被覆盖)。

1
如果一个变量被声明为volatile const char msg[] = "supercalifragilisticexpealidocious"; char const *const msg2 = "us";,并且使用实用程序在生成的二进制文件中找到了msg的地址并进行了修补,那么编译器是否需要在运行时读取该内存的内容(而不是假设它包含指定的字符串),并且避免将其存储用于其他任何事情[例如由msg2指向的“us”文字]? - supercat
1
@supercat:标准明确未指定两个字符串字面量是否为不同实体。据我所知,msgmsg2不能共享,因为您明确指定msg可能会受到外部影响而发生更改--如果它们被共享,那么非易失性的msg2也会随之更改。 - DevSolar
2
如果变量不是“volatile”,编译器可能会假定在编译代码和检查这些内容之间,msg的内容不会发生变化;在某些系统上,硬件可能保证当代码开始执行并检查它时,msg不会发生变化。另一方面,有时候让构建系统的一部分搜索二进制文件中的关键字(如“supercalifraglisticexpialodicious”[或者GUID])并替换为其他内容可能会很有用。问题在于是否需要GCC的代码“看到”这样的更改。 - supercat
天真的问题,但是如何修改某些东西,而不分配、增加或减少? - Akash
@Akash 通过硬件进行修改,无法通过代码进行赋值、递增或递减。 - Sourav Ghosh
显示剩余6条评论

11

我们可以使用文章 关于volatile关键字的介绍,该文章指出:

当一个变量的值可能会意外改变时,应将其声明为volatile。实际上,只有三种类型的变量可能会改变:

  • 内存映射外设寄存器
  • 被中断服务例程修改的全局变量
  • 多线程应用程序中的全局变量

并且:

嵌入式系统包含真实的硬件,通常带有复杂的外设。这些外设包含寄存器,其值可能异步地随着程序流而发生改变。作为一个非常简单的例子,考虑一个位于地址0x1234处的8位状态寄存器。需要轮询状态寄存器直到其变为非零。以下是一个天真和不正确的实现:

UINT1 * ptr = (UINT1 *) 0x1234;

// Wait for register to become non-zero.
while (*ptr == 0);
// Do something else.

一旦你打开优化器,这几乎肯定会失败,因为编译器将生成类似于以下内容的汇编语言:

mov    ptr, #0x1234     mov    a, @ptr loop     bz    loop
关键字表明您的程序不会更改变量,但如文章所述,外部资源可以更改。 防止被优化掉。

7

*const volatile char *p=(const volatile char ) 0x30;
什么是“p的值仅由外部条件改变”的意思。

概念上,您可以将这种类型的变量视为逻辑查看器。类似于门上的窥视孔的概念。 一个窥视孔允许您查看门的另一侧发生了什么,但不允许您更改另一侧发生的内容(const)。但是,门外的条件可能会自行更改(它们是volatile)。您可以看到发生了什么事情,但不能更改发生的事情。

例如,在嵌入式系统中,有硬件寄存器专门用于提供与外部世界中发生事件的状态信息。例如,用于感知RPM的光学编码器会在寄存器中设置一个值。每次旋转时,它会感知来自LED的光并修改硬件寄存器中的值。这就是所谓的外部条件。在另一方面,在您的代码中(例如PID控制循环),您可以读取此信息以用于提供对该循环的调整,但您不能更改此值,也不希望进行更改(const)。

从您的软件角度来看,这说明了:

enter image description here


+1. 相关链接:Wikibooks:嵌入式系统:const和volatile - David Cary
2
我真的很想象那个窥视孔的事情。 :) - Raghu Srikanth Reddy
1
哈哈,我喜欢你在“volition”和volatile之间制造的虚假联系 :D - Lightness Races in Orbit
顺便提一下,在“there”/“their”之前立即进行矫正。 - Lightness Races in Orbit
关于外部条件的解释非常好。 - Sumit Trehan

6

const并不会使变量本身成为常量,它只是告诉编译器拒绝一些写入操作。这意味着,仍然可以通过使用const强制类型转换指针来写入变量。

你可以把const看作是对编码错误的一种保护措施。

这个声明确保你不会意外地向p写入数据,同时告诉编译器不要优化访问(如缓存、乱序执行等),因为外部事件可能会对p进行写入操作。


1
我认为这句话 "* ...因为外部进程可能会写入 p。*" 存在歧义,因为它可能不是指任何正在运行的程序中的"进程"。请参考Sourav Ghosh的答案(https://dev59.com/J1wZ5IYBdhLWcg3wbv_r#31456549) 了解原因。 - alk
@alk,你说得完全正确,谢谢!我不知道如何概括它。我已经用“事件”替换了“进程”。但我仍然对表述感到不满意。 - johan d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接