实用经验 20 volatile和mutable用在何处?

volatile和mutable是C++中两个比较特殊的修饰符。我们首先看这两个关键字简单介绍。

  1. mutable关键字

字典意思:adj. 易变的,不定的;性情不定的。

语法意思:如果在const成员函数中修改一个成员变量的值。那么需要将这个成员变量修饰为mutable。即mutable修饰的成员变量不受const成员方法限制。实际上由于const_cast的存在这个关键字一般使用的较少。

  1. volatile关键字

字典意思:adj. 爆炸性的;不稳定的;挥发性的;反覆无常的 。n. 挥发物;有翅的动物。

语法意思:定义为volatile的变量,说明这个变量可以被意想不到地修改。编译器就会去假设这个变量的值。精确地说就是,优化器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份。

mutable

mutable是C++特有的关键字,其功能也比较简单:实现在const成员函数中修改类对象的成员变量。我们看看C++ Primer 4/E的例子:

//  屏幕实现类
class Screen
{
public:
    // 屏幕显示实现函数。
    void DoDisplay(std::ostream& os) const;
private:
    //  Screen添加一个成员变量m_access_ctr,
    //  使用m_access_ctr变量跟踪Screen的使用频繁程度。
    mutable size_t  m_access_ctr;
}
// 屏幕显示实现函数。
void Screen:: DoDisplay (std::ostream& os)
{
    // 尽管DoDisplay是const,它可以修改m_access_ctr,该成员是可变成员。
    m_access_ctr++;  
    os << contents;
}

volatile

volatile关键字是C和C++共存的一个关键字。用它声明的类型变量可被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

volatile关键字的功能:

(1)作为指令关键字,volatile具有确保本条指令不会因编译器的优化而省略的功能。为了说明这一功能,我们看下面这段代码:

unsigned char XByte[3] = {0};
XByte[2] = 0x34;
XByte[2] = 0x36;
XByte[2] = 0x3f;

对应外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作。而编译器不会像纯粹的程序那样编译,而是会对代码进行优化:只认为XByte [2]=0x3f(即忽略前两条语句,只产生一条机器代码)。如果添加volatile修饰符,则编译器会逐一的进行编译并产生相应的机器代码(三条)。

(2)使用volatile关键字修饰的类型变量可以被未知因素修改。当要求使用volatile 修饰的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。我们对比下面两段代码:

代码段一:

int nVint = 20;
int nValue = nVint;
printf("nVint = %d\r\n", nValue);
//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
__asm 
{ 
    mov dword ptr [ebp-4]20h 
}
Int nValue2 = nVint;
printf("nVint = %d\r\n", nValue2);

在debug模式运行程序,程序输出:

nVint = 20
nVint = 32;

在release模式运行程序,程序输出:

nVint = 20
nVint = 20;

从输出结果我们可以看出,release模式下,编译器对代码进行了优化,release下面没有输出正确的nVint值。

代码段二:

volatile int nVint = 20;
int nValue = nVint;
printf("nVint = %d\r\n", nValue);
//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
__asm 
{ 
    mov dword ptr [ebp-4]20h 
}
Int nValue2 = nVint;
printf("nVint = %d\r\n", nValue2);

在debug模式运行程序,程序输出:

nVint = 20
nVint = 32;

在release模式运行程序,程序输出:

nVint = 20
nVint = 32;

从输出结果我们可以看出,代码片段在debug和release两种模式下都输出了正确的结果。volatile发挥了它的关键作用。

小心陷阱

  • 变量可能在程序本身不知道的情况下发生变化。比如多线程程序,他们共同访问内存变量,而你的程序无法检测这些变量何时发生变化。
  • 外部设备的状态,当外部设备发生操作时,通过驱动程序和中断事件修改了变量的值,你的程序也不会知道,无法感知这一变化。

(3)对于volatile类型的变量,系统在用它的时候都是直接从对应的内存当中提取,而不会利用 cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化—显然也是因为它的数值随时都可能变化的情况。

典型示例1:

for (int i = 0; i < 100000; i++);

这个语句一般用来测试空循环的速度。但编译器肯定会把它优化掉,根本不执行。

for (volatile int i = 0; i < 100000; i++);

如果你写成这样,编译器就会执行了。

典型示例2:此代码是笔者研究生时代写的一段嵌入式程序。

static bool_t bIsRunning = false; 

int main(void) 
{ 
    ... 
    while{ 
        // 如果设置了运行标志,开始运行流程。
        if (bIsRunning)
        {
            doRunProc(); 
        }
    } 
} 

/********************************************
*函数名称:ISR()
*函数功能:定时器0溢出中断响应函数
*输入参数:无
*输出参数:是否成功
*********************************************/
ISR(TIMER0_OVF_vect)
{
        // 如果有按键设置运行
    if (Key_Press())
    {
            bIsRunning = true;
    } 
    else
    {
        bIsRunning = false;
    }
}

由于访问寄存器的速度要快过RAM,所以编译器一般都会减少存取外部RAM的优化。程序的本意是希望ISR(TIMER0_OVF_vect)中断产生时如果有键按下,在main中调用doRunProc函数。

但是由于编译器对bIsRunning的读写优化,导致编译器只一次从bIsRunning到某寄存器的读操作。然后每次if判断都只使用这个寄存器里面的“bIsRunning副本”,导致doRunProc永远也不会被调用。

如果将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中bIsRunning也应该如此说明。将bIsRunning变量加上volatile修饰。

总结,一般说来,volatile应用在如下地方:

  • 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  • 多任务环境下,各任务间共享的标志应该加volatile;
  • 并行设备,存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义。

最后,为了加深volatile的理解,我们再看下面几个问题:
(1)一个参数是否可以既是const又是volatile?
(2)一个指针可以是volatile 吗?解释为什么?
(3)下面程序是否存在问题?

int square(volatile int *ptr)
{
    return (*ptr) * (*ptr);
}

下面我们简短的回答一下上面的问题。首先看第一个问题,一个变量即是const又是volatile这是可以的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

然后第二个问题,这种假设也是成立的尽管这并不很常见。一个典型的例子是在中断服务程序中修改指向buffer的指针时。

第三个例子就更特别了。这段代码是一个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr) 
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果这段代码可能返回的不是你所期望的平方值!正确的代码如下:

int square(volatile int *ptr) 
{
    int a;
    a = *ptr;
    return a * a;
} 

小心陷阱
volatile关键字是区分C程序员和嵌入式系统程序员的最基本问题。嵌入式系统程序员经常同硬件、中断、RTOS等打交道,所用这些都要求volatile变量。不懂得volatile将会带来灾难。

请谨记

  • 请注意程序在编译过程中的优化问题,时刻警惕编译器偷偷为你所做的代码优化,明确mutable和volatile的使用场景。
  • 如果你是嵌入式开发人员,你同硬件、中断、RTOS等打交道时,这些都要求使用volatile变量。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值