C++中修饰数据可变的关键字有三个:const、volatile和mutable。const比较好理解,表示其修饰的内容不可改变(至少编译期不可改变),而volatile和mutable恰好相反,指示数据总是可变的。mutable和volatile均可以和const搭配使用,但两者在使用上有比较大差别。
一、mutable关键字
mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能够修饰非静态数据成员。
我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。下面是一个小例子:
1 class ClxTest 2 { 3 public: 4 void Output() const; 5 }; 6 7 void ClxTest::Output() const 8 { 9 cout << "Output for test!" << endl; 10 } 11 12 void OutputTest(const ClxTest& lx) 13 { 14 lx.Output(); 15 }
类ClxTest的成员函数Output是用来输出的,不会修改类的状态,所以被声明为const的。函数OutputTest也是用来输出的,里面调用了对象lx的Output输出方法,为了防止在函数中调用其他成员函数修改任何成员变量,所以参数也被const修饰。
如果现在,我们要增添一个功能:计算每个对象的输出次数。如果用来计数的变量是普通的变量的话,那么在const成员函数Output里面是不能修改该变量的值的;而该变量跟对象的状态无关,所以应该为了修改该变量而去掉Output的const属性。这个时候,就该我们的mutable出场了——只要用mutalbe来修饰这个变量,所有问题就迎刃而解了。下面是修改过的代码:
1 class ClxTest 2 { 3 public: 4 ClxTest(); 5 ~ClxTest(); 6 7 void Output() const; 8 int GetOutputTimes() const; 9 10 private: 11 mutable int m_iTimes; 12 }; 13 14 ClxTest::ClxTest() 15 { 16 m_iTimes = 0; 17 } 18 19 ClxTest::~ClxTest() 20 {} 21 22 void ClxTest::Output() const 23 { 24 cout << "Output for test!" << endl; 25 m_iTimes++; 26 } 27 28 int ClxTest::GetOutputTimes() const 29 { 30 return m_iTimes; 31 } 32 33 void OutputTest(const ClxTest& lx) 34 { 35 cout << lx.GetOutputTimes() << endl; 36 lx.Output(); 37 cout << lx.GetOutputTimes() << endl; 38 }
计数器m_iTimes被mutable修饰,那么它就可以突破const的限制,在被const修饰的函数里面也能被修改。mutable只能作用在类成员上,指示其数据总是可变的。不能和const 同时修饰一个成员,但能配合使用:const修饰的方法中,mutable修饰的成员数据可以发生改变,除此之外不应该对类/对象带来副作用。考虑一个mutable的使用场景:呼叫系统中存有司机(Driver)的信息,为了保护司机的隐私,司机对外展现的联系号码每隔五分钟从空闲号码池更新一次。
const方法中不允许对常规成员进行变动,但mutable成员不受此限制。对Driver类来说,其固有属性(姓名、年龄、真实手机号等)未发生改变,符合const修饰。mutable让一些随时可变的展示属性能发生改变,达到了灵活编程的目的。
二、volatile关键字
volatile原意是“易变的”,但这种解释简直有点误导人,应该解释为“直接存取原始内存地址”比较合适。“易变”是相对与普通变量而言其值存在编译器(优化功能)未知的改变情况(即不是通过执行代码赋值改变其值的情况),而是因外在因素引起的,如多线程,中断等。编译器进行优化时,它有时会取一些值的时候,直接从寄存器里进行存取,而不是从内存中获取,这种优化在单线程的程序中没有问题,但到了多线程程序中,由于多个线程是并发运行的,就有可能一个线程把某个公共的变量已经改变了,这时其余线程中寄存器的值已经过时,但这个线程本身还不知道,以为没有改变,仍从寄存器里获取,就导致程序运行会出现未定义的行为。并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化。而加了volatile修饰的变量,编译器将不对其相关代码执行优化,而是生成对应代码直接存取原始内存地址。
volatile用于修饰成员或变量,指示其修饰对象可能随时变化,编译器不要对所修饰变量进行优化(缓存),每次取值应该直接读取内存。由于volatile的变化来自运行期,其可以与const一起使用。两者一起使用可能让人费解,如果考虑场景就容易许多:CPU和GPU通过映射公用内存中的同一块,GPU可能随时往共享内存中写数据。对CPU上的程序来说,const修饰变量一直是右值,所以编译通过。但其变量内存中的值在运行期间可能随时在改变,volatile修饰是正确做法。
在多线程环境下,volatile可用作内存同步手段。例如多线程爆破密码:
1 volatile bool found = false; 2 void run(string target) { 3 while (!found) { 4 // 计算字典口令的哈希 5 if (target == hash) { 6 found = true; break; 7 } 8 } 9 }
在volatile的修饰下,每次循环都会检查内存中的值,达到同步的效果。需要注意的是,volatile的值可能随时会变,期间会导致非预期的结果。例如下面的例子求平方和:
1 double square(volatile double a, volatile double b) { 2 return (a + b) * (a + b); 3 }
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;
总结
mutable只能用与类变量,不能与const同时使用;在const修饰的方法中,mutable变量数值可以发生改变;
volatile只是运行期变量的值随时可能改变,这种改变即可能来自其他线程,也可能来自外部系统。
参考
1. https://en.cppreference.com/w/cpp/language/cv
2. https://blog.csdn.net/aaa123524457/article/details/80967330