0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

冬至子 来源:iDoitnow 作者:艰默 2023-07-18 16:49 次阅读

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

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

  • const ----------定义类型为常量类型。
  • volatile --------定义类型为易变类型。

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

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

const

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

const修饰普通变量

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

#include < iostream >

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类似,但是与其不同的是其可以通过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 < iostream >

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 < iostream >
#include < ctime >
#include < unistd.h >
// #include < windows.h > //win下为该头文件

using namespace std;
int main()
{
  const time_t time_val = 0; 
  time_t *p_time_t = const_cast< time_t * >(&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 < iostream >
#include < ctime >
#include < unistd.h >
// #include < windows.h > //win下为该头文件

using namespace std;
int main()
{
  volatile const time_t time_val = 0;
  time_t *p_time_t = const_cast< time_t * >(&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 < iostream >
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;
}

输出结果:

ashow()被调用了:0次。
data : 1
ashow()被调用了:1次。

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

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

#include < iostream >
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限制。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    30

    文章

    5037

    浏览量

    117765
  • 状态机
    +关注

    关注

    2

    文章

    486

    浏览量

    27187
  • C++语言
    +关注

    关注

    0

    文章

    146

    浏览量

    6878
  • gcc编译器
    +关注

    关注

    0

    文章

    78

    浏览量

    3234
收藏 人收藏

    评论

    相关推荐

    DSP编程技巧之17---非常“关键”的关键字

    的设计产生预期的结果。C28x的编译器支 持所有的标准C89的关键字,包括constvolatile和register,标准的
    发表于 08-20 11:38

    c语言中 volatile _Bool 关键字说明

    volatile 关键字呢?volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以消除一些代码
    发表于 01-06 10:46

    C语言关键字中static,volatile,const,extern,return含义及作用?

    C语言关键字中static,volatile,const,extern,return含义及作用?static 函数分为内部函数和外部函数当一个源程序由多个源文件组成时,
    发表于 09-15 20:18

    C++笔记010:C++C的扩展——register关键字增强

    的地址在C语言里面是会出错的。同样的代码我们放到C++编译环境下,发现编译是通过的!在C++中依然支持register关键字C++编译器有
    发表于 08-11 12:34

    【原创分享】单片机编程关键字volatile

    volatile其实和const一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素而改变,比如操作系统、硬件或者其他线程等等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化
    发表于 06-29 11:17

    C语言volatile关键字详解 精选资料分享

    1.volatile和什么有关百度翻译是这样子翻译volatile的:图1-1 百度翻译volatile截图volatile属于C语言的
    发表于 07-22 07:20

    volatile关键字应用场景及示例

    volatile关键字1.应用场景2.示例1.应用场景volatile关键字分析,往往应用在三种场合1)多线程编程共享全局变量的时候,该全局变量要加上
    发表于 08-24 07:21

    C语言volatile关键字之间有什么关系?

    C语言volatile关键字之间有什么关系?
    发表于 11-11 07:01

    嵌入式程序员常见的const、static、volatile关键字

    嵌入式程序员const、static、volatile三个关键字的朴素认识摘要:在C语言程序编写中,const、static
    发表于 12-21 06:08

    C++与C的const关键字有何差别?

    C++与C中的const关键字有何差别?
    的头像 发表于 02-03 14:51 1607次阅读

    C++mutable关键字详解与实战

    mutable关键字详解与实战 在C++mutable关键字是为了突破
    的头像 发表于 09-10 09:23 5254次阅读

    一文详解volatile关键字

    volatile 是易变的、不稳定的意思。和const一样是一种类型修饰符,volatile关键字修饰的变量,编译器对访问该变量的代码不再进行优化,从而可以提供对特殊地址的稳定访问。
    的头像 发表于 02-15 11:54 766次阅读
    一文<b class='flag-5'>详解</b><b class='flag-5'>volatile</b><b class='flag-5'>关键字</b>

    C++中的const关键字介绍

    前一篇文章我们主要介绍了C++中的复合类型引用和指针,这篇文章我们将会主要介绍C++const关键字。有时候我们想定义一个值不能被改变的变量,例如我们想使用一个变量存储buffer的
    的头像 发表于 03-17 14:01 421次阅读

    浅谈C++mutable关键字

    C++11中推出了一种特殊的关键字mutable用于修饰类变量。它的作用是标注该变量一定会被修改,因此也就不是const类型。目的是为了使这些成员变量在被const
    的头像 发表于 04-15 11:13 3389次阅读

    const关键字应用总结

    C++中的const关键字的用法非常灵活,而使用const将大大改善程序的健壮性
    的头像 发表于 05-26 09:06 368次阅读