当前位置 : 主页 > 编程语言 > c语言 >

C++中const、volatile、mutable使用方法小结

来源:互联网 收集:自由互联 发布时间:2021-05-12
相信const大家对他并不陌生,可能大家在日常的编写代码当中就会时常用到const,但是剩下的两个关键字不知道我们有 没有使用过volatile和mutable两个关键字其实不算特别常用,但是我们

相信const大家对他并不陌生,可能大家在日常的编写代码当中就会时常用到const,但是剩下的两个关键字不知道我们有

没有使用过volatile和mutable两个关键字其实不算特别常用,但是我们一定要知道这个关键字有什么用,应该怎么用.首

先const的基本操作我曾经写过一篇博客:const的基本使用

现在我要说一个const操作里面比较骚的一些做法,

举个例子我们以前写过的一个类,我们会使用operator[]来返回一个reference的指向,这个一般情况我们都会写一个const的也会写一个非const的opeartor[].这是我们最常见的一个代码:

相信const大家对他并不陌生,可能大家在日常的编写代码当中就会时常用到const,但是剩下的两个关键字不知道我们有没有使用过volatile和mutable两个关键字其实不算特别常用,但是我们一定要知道这个关键字有什么用,应该怎么用.

const的基本使用

const的用法我觉得对于一个以后想着做一个优秀的程序员来说,这是必须熟练掌握的技能。因为网上有好多的文章介绍它的写的非常好,有的我就直接拿过来了~,现在我们来看看他的用法。

const 要求他所修饰的对象为常量,不可被改变,不可被赋值,不可作为左值.

1、函数体内修饰局部变量

例:

void func(){
const int a=0;
}

const作为一个类型限定词,和int有相同的地位。

const int a; int const a;

是等价的。于是此处我们一定要清晰的明白,const修饰的对象是谁,是a还是int
const要求他所修饰的对象为常量,不可被改变,不可被赋值,不可作为左值(l-value)。所以很明显它修饰的是a。这是一个很常见的使用方式:

const double pi=3.14;

在程序的后面如果企图对pi再次赋值或者修改就会出错。然后看一个稍微复杂的例子。

const int* p;

因为int* p;和 int *p;是等价的。
所以const int (*p)和int const (*p)是等价的。现在一目了然const 修饰的是谁? 是*p.所以p+=1;是合法的

*p+=1;是非法的因为const修饰了你。

int* const p;那这个什么意思?

看const修饰的是什么? 它修饰的p。但是p是一个int型的指针,所以这个指针的地址没有办法修改。

p+=1; //这就是非法的
*p+=1; //这个是合法的

再看一个更复杂的例子,它是上面二者的综合

const int* const p;说明p自己是常量,且p指向的变量也是常量。
于是

p+=1; //非法
*p+=1; //非法

const 还有一个作用就是用于修饰常量静态字符串。例如:

const char* name=David;

如果没有const,我们可能会在后面有意无意的写name[4]='x'这样的语句,这样会导致对只读内存区域的赋值,然后程序会立刻异常终止。有了 const,这个错误就能在程序被编译的时候就立即检查出来,这就是const的好处。让逻辑错误在编译期被发现。

2、在函数声明时修饰参数
举个例子void * myMemMove(void *dst,constvoid *src,intcount )这是我写的memmove函数的声明,这个函数的意思就是(任意类型)把*src的内容复制给*dst,我们现在很明显的看到*src它只让你复制,你不能修改它的值,所以怕你在以后的函数的定义里出现问题现在在声明里限制你。

3、全局变量
我们的原则依然是,尽可能少的使用全局变量。我们的第二条规则 则是,尽可能多的使用const。如果一个全局变量只在本文件中使用,那么用法和前面所说的函数局部变量没有什么区别。如果它要在多个文件间共享,那么就牵扯到一个存储类型的问题。

有两种方式。

1.使用extern
例如
/* test.h */
extern const double pi;
/* test.c */
const double pi=3.14;

然后其他需要使用pi这个变量的,包含test.h

#include test.h

或者,自己把那句声明复制一遍就好。

这样做的结果是,整个程序链接完后,所有需要使用pi这个变量的共享一个存储区域。

2.使用static,静态外部存储类

/* constant.h */
static const pi=3.14;

需要使用这个变量的*.c文件中,必须包含这个头文件。

前面的static一定不能少。否则链接的时候会报告说该变量被多次定义。这样做的结果是,每个包含了constant.h的*.c文件,都有一份该变量自己的copy,该变量实际上还是被定义了多次,占用了多个存储空间,不过在加了static关键字后,解决了文件间重定义的冲突。坏处是浪费了存储空间,导致链接完后的可执行文件变大。但是通常,这个,小小几字节的变化,不是问题。好处是,你不用关心这个变量是在哪个文件中被初始化的。
其实const我觉得更多是程序员自己限制自己,自己告诉自己后面哪里不能出现错误

举个例子吧。

#include<stdio.h>
#include<Windows.h>
int main()
{
	int *p;
	const int a = 0;
	p = &a;
	*p = 3;
	printf("a= %d \n", a);
	system("pause");
	return 0;
}

现在看看运行结果

现在我要说一个const操作里面比较骚的一些做法,

举个例子我们以前写过的一个类,我们会使用operator[]来返回一个reference的指向,这个一般情况我们都会写一个

const的也会写一个非const的opeartor[].这是我们最常见的一个代码:

T& operator[](int position) 
{ 
 return xxx[position]; 
} 
const T& operator[](int position) const 
{ 
 return xxx[position]; 
}

这是我们平时写的初级的代码,但是现在当我们要写一个TextBlock内的opeartor[]不单只返回一个referencr了,也可能执行边界检查,日志访问信息,还有什么数据完善性检验等等一大堆繁琐的代码,这个时候当你实现operator[] const和operator[]() const,的时候两份代码大部分都一样,这里伴随的是代码重复,编译时间变长,维护代码膨胀等等头疼的问题. 当然啦,你可以让上述那些繁琐的函数全部封装的别的函数中,然后分别在operator[]()和operator[]()const当中调用但是你还说重复了一些代码比如两次return语句,函数调用.真正该做的是实现operator[]的机能一次并使用它两次。也就是你只需要写一个函数,令另外一个调用这个,这促使我们将常量性转移. 接下来 见证奇迹我们来看看下面这个代码是怎么实现的上述的操作的:

class TextBlock 
{ 
public: 
 ... 
 const char& operator[](std::size_t position) const 
 { 
  ... 
  ... 
  ... 
  return text[position]; 
 } 
 
 char& operator[](std::size_t position) 
 { 
  return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); 
 } 
};

来仔细看这个操作;return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
首先把*this强制转换为const TextBlock,再然后调用const的operator[],最后再把const的operator[]的返回值的const常量性取消,然后返回一个非const的值. 这里的调用实在是太妙了,我们可以思考一下,好好想想这里的深意.
但是会有人说,为什么不用const operator[]调用operator[]呢,这样强制两个都可以行的通啊.这样想是错的!

令const版本调用调用no-const版本以避免重复并不是你该做的事情. 记住const所修饰函数的承诺就是我绝对不会修改你,no-const函数可没有这种承诺,所以你让一个const函数去调用一个no-const函数是不现实的. over其实const有很多可以玩的属性,只要我们想到就可以去实现,这里就说这么一个就ok. 接下来我们来瞧瞧另外两个关键字.

C++中的mutable关键字

mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。

在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

我们知道,被const关键字修饰的函数的一个重要作用就是为了能够保护类中的成员变量。即:该函数可以使用类中的所有成员变量,但是不能修改他们的值。然而,在某些特殊情况下,我们还是需要在const函数中修改类的某些成员变量,因为要修改的成员变量与类本身并无多少关系,即使修改了也不会对类造成多少影响。当然,你可以说,你可以去掉该函数的const关键字呀!但问题是,我只想修改某个成员变量,其余成员变量仍然希望被const保护。

经典的应用场景比如说:我要测试一个方法的被调用次数。

class Person {
public:
 Person();
 ~Person();

 int getAge() const; /*调用方法*/
 int getCallingTimes() const; /*获取上面的getAge()方法被调用了多少次*/
private:
 int age;
 char *name;
 float score;
 int m_nums;   /*用于统计次数*/
};

最普遍的作法就是在getAge()的方法体内对m_nums这个变量进行加+1,但是getAge()方法又是const方法,无法修改m_nums这个变量,我又不想去掉const关键字让别人能够修改age等成员变量,这个时候mutable关键字就派上用场了:

#include <iostream>

class Person {
public:
 Person();
 ~Person();

 int getAge() const; /*调用方法*/
 int getCallingTimes() const; /*获取上面的getAge()方法被调用了多少次*/
private:
 int age;
 char *name;
 float score;
 mutable int m_nums;   /*用于统计次数*/
};

Person::Person()
{
 m_nums = 0;
}

Person::~Person(){}

int Person::getAge() const
{
 std::cout << "Calling the method" << std::endl;
 m_nums++;
 // age = 4; 仍然无法修改该成员变量
 return age;
}

int Person::getCallingTimes()const
{
 return m_nums;
}

int main()
{
 Person *person = new Person();
 for (int i = 0; i < 10; i++) {
  person->getAge();
 }
 std::cout << "getAge()方法被调用了" << person->getCallingTimes() << "次" << std::endl;
 delete person;

 getchar();
 return 0;
}

运行结果:

Calling the method
Calling the method
Calling the method
Calling the method
Calling the method
Calling the method
Calling the method
Calling the method
Calling the method
Calling the method

getAge()方法被调用了10次

这样我们既保护了别的成员变量,又能够使计数器的值进行累加。

需要注意的是:mutable不能修饰const 和 static 类型的变量。

接着补充

实际运用起来也非常容易,就是你想改变的元素被const修饰了,你就往它前面加上mutable那么你就无敌了..

我就举一个最简单的例子,我定一个AA类,我在AA类中定义一个MT()函数,该函数属性为const属性,再然后我

想在MT()函数中添加该函数执行多少次时,程序编不过去了. 因为const修饰的函数里面的所有值都不能修改.

class AA
{
public:
	void MT() const
	{
		i++;
		cout << "hehe";
		cout << "执行了" << i << "次该程序";
	}
 
private:
	int i = 0;
};

但是这样编不过去啊,因为MT()函数为const函数,所以不能修改i的值,但是如果我在这里使用mutable关键字的

话,现在我们把i加上mutable关键字,这样它永远就是一个可变的了. 来我们加上去试一试,

class AA
{
public:
	void MT() const
	{
		i++;
		cout << "hehe" << " ";
		cout << "执行了" << i << "次该程序" << endl;;
	}
 
private:
	mutable int i = 0;
};
 
int main()
{
	AA a;
	a.MT();
	a.MT();
	a.MT();
	a.MT();
	return 0;
}

运行结果:

这就是mutable的最简单的一个应用,以后可以根据需求来使用~

volatile

为什么使用volatile ?
C/C++中的 volatile 关键字 和const对应,用来修饰变量,通常用于建立语言级别的memory barrier。这是BS在“The C++ Programming Language”对volatile修饰词的解释:
A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统,硬件或者其他线程等。

遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10; 
int a = i; 
... 
// 其他代码,并未明确告诉编译器,对 i 进行过操作 
int b = i; 
volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。注意,在 VC 6 中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无 volatile 关键字,对程序最终代码的影响:

输入下面的代码:

#include <stdio.h> 
  
void main() 
{ 
  int i = 10; 
  int a = i; 
   
  printf("i = %d", a); 
 
  // 下面汇编语句的作用就是改变内存中 i 的值 
  // 但是又不让编译器知道 
  __asm{ 
    mov dword ptr [ebp-4], 20h 
  } 
    
  int b = i; 
  printf("i = %d", b); 
} 
#include <stdio.h>
 
void main()
{
  int i = 10;
  int a = i;
  
  printf("i = %d", a);
 
  // 下面汇编语句的作用就是改变内存中 i 的值
  // 但是又不让编译器知道
  __asm{
    mov dword ptr [ebp-4], 20h
  }
   
  int b = i;
  printf("i = %d", b);
}

然后,在 Debug 版本模式运行程序,输出结果如下:

i = 10
i = 32
然后,在 Release 版本模式运行程序,输出结果如下:

i = 10
i = 10
输出的结果明显表明,Release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile 关键字,看看有什么变化:

#include <stdio.h> 
 void main() 
{ 
  volatile int i = 10; 
  int a = i; 
  printf("i = %d", a); 
  __asm { 
    mov dword ptr [ebp-4], 20h 
  } 
   
  int b = i; 
  printf("i = %d", b); 
} 
#include <stdio.h>
 
void main()
{
  volatile int i = 10;
  int a = i;
 
  printf("i = %d", a);
  __asm {
    mov dword ptr [ebp-4], 20h
  }
  
  int b = i;
  printf("i = %d", b);
}

分别在 Debug 和 Release 版本运行程序,输出都是:

i = 10
i = 32

这说明这个 volatile 关键字发挥了它的作用。其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

2.volatile 指针

和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:

修饰由指针指向的对象、数据是 const 或 volatile 的:

const char* cpch; 
volatile char* vpch; 
const char* cpch;
volatile char* vpch;

注意:对于 VC,这个特性实现在 VC 8 之后才是安全的。指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

char*const pchc; 
char*volatile pchv; 
char*const pchc;
char*volatile pchv;

注意:

(1) 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。

(2) 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
(3) C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

3. 多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:

volatile BOOL bStop = FALSE; 
// 在一个线程中:  
 while( !bStop ) { ... }  
bStop = FALSE;  
return;   
 
//在另外一个线程中,要终止上面的线程循环:  
bStop = TRUE;  
while( bStop ); //等待上面的线程终止, 
volatile BOOL bStop = FALSE; 
 
// 在一个线程中: 
 while( !bStop ) { ... } 
bStop = FALSE; 
return;  
 
//在另外一个线程中,要终止上面的线程循环: 
bStop = TRUE; 
while( bStop ); //等待上面的线程终止,

如果bStop不使用volatile申明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度,例如下段代码中:

在此段代码中,nMyCounter的拷贝可能存放到某个寄存器中(循环中,对nMyCounter的测试及操作总是对此寄存器中的值进行),但是另外又有段代码执行了这样的操作:nMyCounter -= 1;这个操作中,对nMyCounter的改变是对内存中的nMyCounter进行操作,于是出现了这样一个现象:nMyCounter的改变不同步。

下面是volatile变量的几个例子:

1.并行设备的硬件寄存器(如:状态寄存器

2.一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

3.多线程应用中被几个任务共享的变量

看下面例题:

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

这个程序有什么问题吗? 如果我们不去关心volatile关键字的话,那么这个程序你怎么看都会觉得没多大问题.但是这里

面问题大这ne, 首先参数声明为volatile就是表明*ptr可能会随时改变.上述代码运行时,编译器可能产生这样的代码:

int square(volatile int *ptr)
{
  int a,b;
  a = *ptr;
  b = *ptr;
  return a * b;
}

因为你的*ptr是随时都可以意想不到的变化,所以有可能a*b的时候,a b的值不相同. 这样你就得到一个错误的结果

改正后的程序:

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

第二个问题,看如下代码:

#include<iostream> 
#include<Windows.h> 
#include<assert.h> 
 
using namespace std; 
 
int main() 
{ 
  const int a = 2; 
  int *p = const_cast<int*>(&a); 
  *p = 3; 
  cout << a << endl; 
  system("pause"); 
  return 0; 
}

我们有理由的认为在内存当中a的值被修改为3,但是结果呢? 我们来看一看

这不科学啊?? 我们再打开监视窗口看一下a的值.

我们都知道监视窗口看到的都是从内存当中拿到的,但是为什么内存当中为3,打印出来就是2呢? 我来解释一下.

C++编译器具有优化功能,当你定一个const的常量的时候,系统觉得它不会被改变了,于是做一个优化把该常量存到寄

存器当中,下次访问的过程更快速一点. 所以当显示窗口读取数据的时候,他会直接去寄存器当中读取数据.而不是去

内存,所以导致我们明明该掉了a的值,却打印不出来.

这个时候该我们的volatile出马了,往i前面加一个volatile之后就会解决这个问题,来看结果:

到这这篇文章就结束了,自由互联小编结合了多篇文章,希望大家能看的明白。

网友评论