C++面试基础系列-volatile

系列文章目录

总目录链接



C++面试基础系列-volatile

1.volatile核心规则

  • volatile修饰变量或指针功能
    • 告诉编译器,被volatile修饰的变量或指针(寄存器或硬件寄存器),不要进行优化
    • 变量或指针在中断服务子程序,用户函数中同时调用,不要进行优化
    • 多线程,多任务同时调用变量或指针,使用volatile修饰,表示每次操作该变量或指针,都要从内存中取最新的值进行操作。
  • 简单来说
    • 编译器,不要优化
    • 从内存取值

2.C与C++中volatile区别

  • 在C和C++中,volatile关键字都用于告诉编译器一个变量可能会在程序的控制之外被改变,通常是由于硬件的作用。尽管C和C++都支持volatile,但它们在某些方面存在一些差异:

2.1.C语言中的volatile

  • 在C语言中,volatile主要用于多线程环境或中断服务例程(ISR)中,指示编译器该变量可能会在任何时候被改变,因此每次使用时都应该从内存中重新读取,而不是从寄存器中。
  • C标准并没有详细说明volatile的所有行为,尤其是在多线程同步方面。

2.2.C++中的volatile

  • C++中的volatile用法与C类似,但C++标准对volatile的行为有更明确的定义,特别是在多线程环境中。
  • 在C++11及以后的版本中,标准定义了内存模型,volatile变量的访问将遵循这些规则,以确保在多线程环境中对它们的访问是原子的(如果需要)。

2.3.原子性和顺序

  • 在C++11中,volatile关键字不保证操作的原子性,即使在多线程环境中也是如此。如果需要原子性,C++提供了std::atomic模板和相关的原子操作函数。
  • C++中的volatile也不保证编译器不会重排相关操作的顺序,这与C类似。

2.4.易失性

  • 在C和C++中,volatile都可以用来指示编译器一个变量是易失性的,即它可能会因为外部硬件事件而改变。

2.5.优化

  • 由于volatile告诉编译器变量可能在任何时候改变,编译器将不会对这个变量进行优化。

2.6.使用场景

  • 在嵌入式编程中,volatile经常用于访问内存映射的硬件寄存器,因为这些寄存器可能会被外部硬件改变。

2.7.C++特有的特性

  • C++中volatile的使用可能会受到C++特有的特性影响,如类和对象模型。例如,C++中的volatile成员变量需要特别注意,因为它们的行为可能不如预期。

2.8.C++20引入的变化(如果有)

  • C++20标准可能会对volatile有进一步的说明或改进,但截至知识截止日期,这些变化尚未明确。

总结来说,C和C++中的volatile在基本用途上相似,但C++标准提供了更明确的定义,尤其是在多线程环境中。在C++中,如果需要原子性或线程安全的同步,应该使用std::atomic或其他同步机制,而不是仅仅依赖volatile。

3.volatile常见面试问题

在面试中,volatile 关键字是一个常见的话题,以下是一些可能会问到的问题以及相关的答案:

  1. volatile 关键字是什么?
    volatile 是一种类型修饰符,用于告诉编译器该变量可能会在程序的控制之外被改变,例如由中断服务程序、多线程或硬件修改。使用 volatile 可以防止编译器对这些变量进行优化,确保每次访问都直接从内存中读取或写入,从而保证数据的一致性。
  2. volatile 变量的可见性如何保证?
    volatile 变量保证了对所有线程的可见性。当一个线程修改了一个 volatile 变量时,其他线程能够立即看到这个修改,这是因为 volatile 变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新值。
  3. volatile 能否保证原子性?
    volatile 关键字不能保证操作的原子性。它只能确保单次的读/写操作具有原子性,但对于复合操作,如自增(i++),volatile 无法保证整个操作的原子性。因此,对于需要原子性保证的操作,应该使用其他同步机制,如 synchronizedAtomicInteger
  4. volatile 变量在多线程中的使用场景有哪些?
    volatile 变量适用于多线程环境下的某些特定场景,例如:
    • 作为状态标志,用于控制线程间的协调,如退出循环的条件。
    • 在没有其他同步机制的情况下,用于保护简单的状态变量,以确保变量的可见性和有序性。
    • 与锁结合使用,实现读写锁模式,提高性能。
  5. volatileconst 的区别是什么?
    const 关键字用于定义常量,即值不能被修改的变量。而 volatile 用于定义可能会在程序的控制之外被改变的变量。const 保证的是值的不变性,而 volatile 保证的是变量的可见性和禁止指令重排序。
  6. volatile 指针是什么?
    volatile 指针是指指向的变量可能会在程序的控制之外被改变的指针。它可以是指向 volatile 变量的指针,也可以是指针本身的值是 volatile 的。在使用 volatile 指针时,编译器不会对通过该指针进行的操作进行优化。
  7. 在嵌入式系统中,volatile 的作用是什么?
    在嵌入式系统中,volatile 通常用于声明与硬件寄存器相关的变量,以确保编译器不会对与硬件交互的代码进行优化。这是因为硬件寄存器的值可能会由硬件本身的操作所改变,需要每次访问时都从内存中读取最新值。
  8. volatile 能否替代 synchronized
    volatile 在某些情况下可以作为 synchronized 的轻量级替代,特别是在读操作远多于写操作的场景下。但是,由于 volatile 不能保证复合操作的原子性,它不能完全替代 synchronized。在需要原子性保证的情况下,仍然需要使用 synchronized 或其他同步机制。

理解 volatile 的作用和限制对于编写正确的多线程程序至关重要。在面试中,展示对 volatile 的深入理解可以体现候选人的专业知识和经验。

4.volatile与const与指针

  • (1)一个参数既可以是const还可以是volatile吗?为什么?
    可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
  • (2)一个指针可以是volatile吗?为什么?
    可以。尽管这并不常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。

5.应用场景

在嵌入式系统开发中,正确地使用 volatile 关键字对于避免硬件中断对程序执行的影响至关重要。以下是一些关于如何在嵌入式系统中使用 volatile 的关键点:

  • 防止编译器优化:volatile 告诉编译器,即使在代码中看似没有改变,变量的值也可能在任何时候改变,因此编译器不应进行优化
  • 中断服务程序中的变量:在中断服务程序(ISR)中,经常需要访问或修改一些变量。如果这些变量在主程序中也被访问,那么它们应该被声明为 volatile,以确保每次访问时都能获取最新的值
  • 多线程共享变量:在多线程环境中,如果多个任务共享某些变量,并且这些变量的值可能被任何一个任务改变,那么这些变量也应该被声明为 volatile,以确保所有任务都能看到其他任务对共享变量的最新修改
  • 硬件寄存器访问:在嵌入式编程中,硬件寄存器的值可能会被硬件本身的操作所改变。使用 volatile 修饰硬件寄存器可以确保每次访问都是直接从硬件寄存器中读取,而不是从CPU缓存中
  • 保证内存顺序:volatile 还可以防止编译器和处理器对指令的重排序,确保指令按照代码中的顺序执行,这对于中断和主程序之间的同步尤为重要
  • 使用场景:volatile 适用于并行设备的硬件寄存器、中断服务子程序中访问的非自动变量、多线程应用中被多个任务共享的变量,以及需要防止编译器优化的情况,如for循环延时程序
  • 注意限制:虽然 volatile 可以确保变量的可见性,但它不保证操作的原子性。在多线程环境中,如果需要原子性,还需要使用其他同步机制,如互斥锁
  • 性能影响:使用 volatile 可能会降低程序的性能,因为它阻止了编译器进行某些优化。因此,只有在必要时才应该使用 volatile

总结来说,volatile 在嵌入式系统中是一个关键的关键字,用于确保变量的值能够反映出最新的状态,特别是在中断服务程序和多线程环境中。然而,开发者应当谨慎使用,避免滥用,同时注意它并不能替代其他同步机制来保证操作的原子性。

6.应用示例

(1)并行设备的硬件寄存器(如状态寄存器)。

  • 假设要对一个设备进行初始化,此设备的寄存器为0x0x80008004。
unsigned int  *output = (unsigned  int *)0x0x80008004; //定义一个IO端口;  
int main(void)  
{  
    int i;  
    for(i=0;i< 10;i++)
    {  
      *output = i;  
    }  
}
  • 如果开启的 -O3 优化,那么经过编译器优化后,编译器认为前面循环,对最后的结果毫无影响。
  • 最终只是将output这个指针赋值为 9,所以汇编后的程序相当于:
int  init(void)  
{  
    *output = 9;  
}
  • 如果你需要程序完全按照你所写程序运行,那就用volatile修饰变量。
  • 通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。

(2)一个中断服务子程序中访问到的变量;

static int i=0;

int main()
{
    while(1)
    {
    if(i) dosomething();
    }
}

/* Interrupt service routine */
void IRS()
{
 i=1;
}
  • 上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,
  • 但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。
  • 如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。

(3)多线程应用中被多个任务共享的变量。

  • 当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。
  • 作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。
  • volatile的作用是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:
volatile  bool bStop=false;  //bStop 为共享全局变量  
//第一个线程
void threadFunc1()
{
    ...
    while(!bStop){...}
}
//第二个线程终止上面的线程循环
void threadFunc2()
{
    ...
    bStop = true;
}
  • 要想通过第二个线程终止第一个线程循环,如果bStop不使用volatile定义,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,
  • 加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。

关于作者

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WeSiGJ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值