C++ const、volatile和mutable关键字详解

对于cvconstvolatile)类型限定符和关键字mutable在《cppreference》中的定义为:

cv可出现于任何类型说明符中,以指定被声明对象或被命名类型的常量性(constness)或易变性(volatility)。

  • const----------定义类型为常量类型。

  • volatile--------定义类型为易变类型。

mutable用于指定不影响类的外部可观察状态的成员(通常用于互斥体、记忆缓存、惰性求值和访问指令等)。

  • mutable------容许常量类类型对象修改相应类成员。

const

const实际上是一个类型说明,告诉编译器const修饰的目标是不变的,允许编译器对其进行额外的优化,如果后面代码不小心修改它了,就编译失败,告诉用户该目标被意外修改了,提高程序的安全性和可控性。

const修饰普通变量

const修饰过的变量,编译器往往将其作为一个常量进行处理,同时,const修饰的变量在编译阶段会被编译器替换为相应的值,来提高程序的执行效率。

#include 

using namespace std;

int main() {
  const int i = 50;          // 普通常量
  const static int si = 50;  // 静态常量

  int* p_int = (int*)&i;  // 强制类型转换为int*
  int* p_sint = (int*)&si;
  *p_int = 100;  // 通过非常常量指针修改常量i的值,该行为是C++为未定义行为
  //*p_sint = 100;//编译不会报错,程序运行时崩溃,且该行为也是C++为未定义行为

  cout << "i:" << i << ", i的地址: " << &i << endl;//编译器阶段会将常量i替换为50
  cout << "*p_int:" << *p_int << ", *p_int的地址: " << p_int << endl;

  return 0;
}

:类型是 const修饰的对象,或常量对象的非可变子对象。这种对象不能被修改:直接尝试这么做是编译时错误,而间接尝试这么做(例如通过到非常量类型的引用或指针修改常量对象)的行为未定义。

输出结果:

i:50, i的地址: 0x7fffffffd9d4
*p_int:100, *p_int的地址: 0x7fffffffd9d4

i*p_int打印出的地址都是0x7fffffffd9d4可以看出,我们偷偷修改i的值成功了(但该行为是C++未定义的行为),但是为何i*p_int的结果却是不同的,这就从侧面证实了const常量具有宏替换的特性,即程序在编译阶段就会其进行部分的替换,例如上述例子中对语句

cout << "i:" << i << ", i的地址: " << &i << endl;

在编译阶段替换为

cout << "i:" << 50 << ", i的地址: " << &i << endl;

因此导致我们输出的i的值为50

同时,当我们想要通过使用非常量指针修改静态常量si时候,编译通过,但是在运行过程中导致程序崩溃(这就是不按规矩办事的后果,使用了C++未定义的行为,编译器也帮不了我们,最终导致程序挂掉)。

const的内部链接性

通常情况下,在未声明为 extern 的变量声明上使用 const 限定符,会给予该变量内部连接(即名称在用于声明变量的文件的外部是不可见的)的特性。即与static类似(详情可参考《C++避坑---关键字static的使用及注意事项》),但是与其不同的是其可以通过extern来改变其内部连接性。

const修饰指针和引用

/********指针********/
//指向const对象的指针
const int* p1; //const修饰的是int,表示p1指向的内容不能被修改
int const* p2; //const修饰的是int,表示p2指向的内容不能被修改

//指向对象的const指针
int* const p3; //const修饰的是*,表示p3的指向不能被修改

//指向const对象的const指针
const int* const p4; //第一个修饰的是int,第二个修饰的是*,表示p4指向的内容和p4的指向都不能被修改
const int const* p5; //同上,表示p5指向的内容和p5的指向都不能被修改


/*注:从上面我们可以总结出来的一个规律:
const优先修饰其左边最近的类型,
如果左边没有,就修饰右边离他最近的那个*/


/********引用********/
const int a = 0;
//由于a1引用a之后,不能引用其他实体,所以对于const int&可以看作是const int* const
const int& a1 = a; 

int b = 0;
const int& b1 = b;//C++允许无限定类型的引用/指针能被转换成到 const 的引用/指针

const在类中的应用

非静态数据成员可以被cv限定符修饰,这些限定符写在函数声明中的形参列表之后。其中,带有不同cv限定符(或者没有)的函数具有不同的类型,它们可以相互重载。在具有cv限定符的成员函数内,*this拥有同向的cv限定。例子如下:

#include 

using namespace std;

class A {
    public:
    A(int a) : a(a){};

    void show() { cout << "void show(): " << a << endl; }
    void show() const { cout << "void show() const: " << a << endl; }  // 重载
    /*这里
  void show() 等价于 void show(A* this)
  void show() const 等价于 void show(const A* this)
  因此该成员函数show()可以被重载
  */

    void set_a(int n) { a = n; }
    /*
  void set_a(int n) const
  {
      a = n;//此时*this拥有const限制,导致a不能够被修改
  }//程序报错
  */

    // void print_a() const 等价于 void print_a(const A* this)
    void print_a() const { cout << "print_a() const: " << a << endl; }

    private:
    int a;
};

int main() {
    A a1(1);
    const A a2(2);

    a1.show();
    a2.show();


    a1.print_a();  // 非const对象可以调用const成员函数
    // 根本原因是A* this可以隐式转换const A* this
    // 最终调用void print_a(const A* this)
    // 即void print_a() const
    a2.print_a();


    a1.set_a(2);
    //   a2.set_a(1);  // 编译报错,const对象不能调用非const成员函数,根本原因是根本原因是const A* this可以隐式转换A* this

    return 0;
}

输出结果:

void show(): 1
void show() const: 2
print_a() const: 1
print_a() const: 2

对于上述例子我们可以得出:

  • const对象不能调用非const成员函数。 => const成员函数内部不能调用其他非cosnt成员函数。
  • const对象可以调用const成员函数。=> 非cosnt成员函数可以调用其他cosnt成员函数。

volatile

volatile 主要作用是告诉编译器其修饰的目标是易变的,在编译的时候不要去优化它(例如读取的时候,都从目标内存地址读取),防止编译器误认为某段代码中目标是不会被改变,而造成过度优化。

:编译器大部分情况是从内存读取变量的值的,但有时候编译器认为在某段代码中,某个变量的值是没有变化的,所以认为寄存器里的值跟内存里是一样的,为了提高读取速度,编译器可能会从寄存器中读取变量,但是在某些情况下变量的值被其他元素(如另外一个线程或者中断服务)修改,这样导致程序读取变量的值不是最新的,产生异常。

因此,volatile 关键字对于声明共享内存中可由多个进程访问的对象或用于与中断服务例程通信的全局数据区域很有用。如果某个目标被声明为 volatile,则每当程序访问它的时候,编译器都会重新加载内存中的值。 这虽然降低了目标的读取速度,但是保证了目标读取的正确性,这也是保证我们程序可预见性的唯一方法。

下面我们通过一个读取系统时间的例子来看一下volatile在实际开发过程中的应用:

#include 
#include 
#include 
// #include  //win下为该头文件

using namespace std;
int main()
{
  const time_t time_val = 0; 
  time_t *p_time_t = const_cast(&time_val);

  time(p_time_t);
  cout << time_val << endl;

  // 休眠1s
  sleep(1); // linux下sleep函数,单位为秒
  // Sleep(1000); // win下的sleep函数,单位为毫秒

  time(p_time_t);
  cout << time_val << endl;

  return 0;
}

time函数在ctime头文件中定义,其主要作用是获取系统当前时间,其原型如下:

std::time_t time( std::time_t* arg );

返回编码为std::time_t对象的当前日历时间,并将它存储于 arg 所指向的对象,除非 arg 是空指针。

输出结果:

0
0

很明显结果不符合我们的预期,具体原因就是我们上面介绍的const常量具有宏替换的特性,编译器认为这段可以更好的优化,在编译阶段就对其进行了替换。那我们如何修改才能达到我们的实现呢?对,就是添加volatile关键字修饰,具体实现如下:

#include 
#include 
#include 
// #include  //win下为该头文件

using namespace std;
int main()
{
  volatile const time_t time_val = 0;
  time_t *p_time_t = const_cast(&time_val);

  time(p_time_t);
  cout << time_val << endl;

  // 休眠1s
  sleep(1); // linux下sleep函数,单位为秒
  // Sleep(1000); // win下的sleep函数,单位为毫秒

  time(p_time_t);
  cout << time_val << endl;

  return 0;
}

输出结果:

1680339937
1680339938

从输出结果看出,结果符合我们的预期。

这时候你可能会有疑问:volatile const是什么鬼?const表示time_val是常量,volatile表示time_val是可变的,难道是易变的常量?这不是矛盾吗?

这里我们确实可以将time_val成为易变的常量,只不过常量(不可修改)意味着time_val在其作用域中(这里指的是main函数中)是不可以被改变的,但是在其作用域外面(这里指的是time()函数内)是可以被改变的。volatile const其实是在告诉编译器,在main()函数内,time_valconst的,你帮我看点,我不能随意的修改,但这个值在作用域外可能会被其他东西修改,这玩意也是volatile的,你编译的时候也别优化它了,在每次读取的时候,也老老实实从它的存储位置重新读取吧。

注:volatile constconst volatile是一样的,都代表易变的常量。

volatile修饰常量、指针和引用

volatile修饰常量指针和引用的使用方法域const类似,这里不做过多的解释,但需要注意的是volatile没有像const的内部链接属性。

volatile修饰函数的参数

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

上述例子是为了计算一个易变类型的int的平方,但是函数内部实现存在问题,因为

return *ptr * *ptr;

其处理逻辑类似下面的情况:

int a = *ptr; 
int b = *ptr;
return a * b;

由于*ptr是易变的,因此ab获取的值可能是不一样的,因此最好采用如下的方式:

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

mutable

mutable主要是为了突破const的某些限制而设定的,即允许常量类型对象相应的类成员可以被修改,其常在非引用非常量类型的非静态数据成员中出现。

在上面的介绍中,我们知道在在获取类某些状态的成员函数中,如果不涉及状态的变更,我们一般会将成员函数声明成const,这将意味着在该函数中,所有的成员函数都不可以被修改,但有些时候我们需要在该const函数中修改一些跟类状态无关的数据乘员,那么这时候就需要mutable发挥作用了,即将该需要修改的成员使用mutable修饰。

#include 
using namespace std;

class A
{
public:
  A(int data = 0) : int_data(data), times(0) {}
  void show() const
  {
    times++; //因为times被mutable修饰,突破了const的限制
    cout << "data : " << int_data << endl;
  }

  int getNumOfCalls() { return times; }

private:
  int int_data;
  mutable int times;
};

int main()
{
  A a(1);
  cout << "a的show()被调用了:" << a.getNumOfCalls() << "次。" << endl;
  a.show();
  cout << "a的show()被调用了:" << a.getNumOfCalls() << "次。" << endl;
  return 0;
}

输出结果:

a的show()被调用了:0次。
data : 1
a的show()被调用了:1次。

上例void show()const修饰后,导致在该函数内类成员不能被修改,但由于timesmutable修饰后,突破了const的限制,使得times在该函数内部可以被修改。

mutable的另一个应用场景就是用来移除lambda函数中按复制捕获的形参的const限制。通常情况下(不提供说明符),复制捕获的对象在lambda体内是 const的,并且在其内部无法修改被捕获的对象,具体的例子如下:

#include 
using namespace std;

int main() {
  int a = 0;
  const int b = 0;

  auto f1 = [=]() {
  /*
  a++;  // 错误,不提供说明符时复制捕获的对象在 lambda 体内是 const 的。
  b++;  // 错误,同上,且按值传递const也会传递进来
  */
    return a;
  };

  auto f2 = [=]() mutable {  // 提供mutable说明符
    a++;                     // 正确,mutable解除const限制。
    /*
    b++;  // 错误,mutable无法突破b本身的const限制
    */
    return a;
  };

  cout << a << ", " << b << endl;        // 输出0, 0
  cout << f1() << ", " << f2() << endl;  // 输出0, 1

  return 0;
}

总 结

const主要用来告诉编译器,被修饰的变量是不变类型的,在有些情况下可以对其进行优化,同时如果后面代码不小心修改了,编译器在编译阶段报错。在类的应用中,const对象不能调用非const成员函数,非const对象可以调用const成员函数。

volatile主要用来告诉编译器,被修饰变量是易变,在编译的时候不要对其进行优化,读取它的时候直接从其内存地址读取。

同时,constvolatile限定的引用和指针支持下列的隐式转换:

  • 无限定类型的引用/指针能被转换成const的引用/指针
  • 无限定类型的引用/指针能被转换成volatile的引用/指针
  • 无限定类型的引用/指针能被转换成const volatile的引用/指针
  • const类型的引用/指针能被转换成const volatile的引用/指针
  • volatile 类型的引用/指针能被转换成const volatile的引用/指针

对于const修饰的成员函数内类成员const的属性,可以通过使用对mutable来解除const限制。同样的,mutable也可以用来移除lambda函数中按复制捕获的形参的const限制。

文章首发公众号:iDoitnow 如果喜欢话,可以关注一下

你可能感兴趣的:(C++ const、volatile和mutable关键字详解)