一、volatile 用法
可以将成员函数声明为volatile, 如果一个类对象的值可能被修改的方式是编译器无法控制或检测的,例如:如果它是表示 I/O端口的数据结构,则把它声明为 volatile 与 const 类对象类似,对于一个 volatile类对象,只有 volatile成员函数、构造函数和析构函数可以被调用:
class Screen {
public:
char poll() volatile;
// ...
};
char Screen::poll() volatile { ... }
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
volatile 相当于告诉编译器, 由它声明的东西的易变的, 不确定的, 可能由外部程序 (如中断程序) 改变的, 禁止编译器对其读写操作进行优化, 如果定义:
int i;
则编译器可能会将其优化, 而放到 CPU 寄存器中, 这在多数情况下是好的, 然而在有些情况下, 我们会要求一些变量必须在内存中 (如驱动程序, 中断处理程序等等), 这时编译器这个优化就是引起问题, 为了避免这种情况, 应该这样定义:
volatile int i;
PS: volatile 通常也用来阻止编译器具优化操作, 如你有一个非精确延时函数:
void delay(unsigned int timeout)
{
unsigned int i;
for (i = 0; i < timeout; i++);
}
有些编译会足够聪明地注意到这个函数本质上是什么也没干, 会将针对这个函数的调用优化掉, 但这样是不对的, 所以你应该这么声明:
volatile void delay(...)
{
// 同上
}
二、mutable数据成员
当我们把一个 Screen类对象声明为 const 时出现了一些问题,我们期望的行为是 一旦 const Screen对象被初始化,它的内容就不能被修改,但是我们应该能够监视到 Screen对象的内容。例如,给出下面的 Screen对象 cs :
const Screen cs( 5, 5 );
我们想监视在位置(3, 4)的内容,我们这样做 :
// 读位置(3, 4)的内容
// 喔! 不能工作
cs.move( 3, 4 );
char ch = cs.get();
但是,这不能工作,你知道为什么吗?move()不是 const 成员函数,而且不能很容易地变成const 成员函数,move()的定义如下:
inline void Screen::move( int r, int c )
{
if ( checkRange( r, c ) )
{
int row = (r-1) * _width;
_cursor = row + c - 1; // 修改 _cursor
}
}
我们注意到 move()修改了数据成员_cursor,因此若不加改动,它就不能被声明为const,但是,对一个 Screen类的 const 对象不能修改_cursor 这看起来可能很奇怪,因为_curso只是一个索引,修改_cursor 不会修改 Screen本身的内容,我们只是想记住要被监视的 Screen位置,即使 Screen对象是 const 也应该允许修改_cursor,因为这么做对于监视 Screen对象内容是必需的,而且又不会修改 Screen本身的内容 。
为了允许修改一个类的数据成员,即使它是一个const 对象的数据成员,我们也可以把该数据成员声明为mutable(易变的), mutable 数据成员永远不会是 const 成员,即使它是 一个const 对象的数据成员,mutable 成员总可以被更新,即使是在一个 const 成员函数中 。为把一个成员声明为mutable 数据成员,我们必须把关键字 mutable 放在类成员表中的数据成员声明之前:
class Screen {
public:
// 成员函数
private:
string _screen;
mutable string::size_type _cursor; // mutable 成员
short _height;
short _width;
};
现在任何 const 成员函数都可以修改_cursor,我们可以把成员函数 move()声明为 const ,即使 move()修改了数据成员_cursor,也不会有编译错误产生:
// move() 是一个 const 成员函数
inline void Screen::move( int r, int c ) const
{
// ...
// ok: const 成员函数可以修改 mutable 成员
_cursor = row + c - 1;
// ...
}
现在我们可以执行开始时给出的操作来监视 Screen对象 cs 而不会有错误发生。我们注意到只有_cursor 被声明为 mutable 数据成员,而_screen _height 和_width都没有,因为这些数据成员的值在const 的Screen类对象中是不应该被改变的。