39.volatile、mutable和explicit关键字的用法
39.volatile、mutable和explicit关键字的用法
1.volatile
☀警告
volatile的确切含义与机器有关,只能通过阅读编译器文档来理解。要想让使用了volatile的程序在移植到新机器或新编译器后仍然有效,通常需要对该程序进行某些改变。
直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile。关键字volatile告诉编译器不应对这样的对象进行优化。(比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。)
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
1.1volatile指针
volatile限定符的用法和const很相似,它起到对类型额外修饰的作用:
volatile int display_register; // 该int值可能发生改变
volatile Task *curr_task; // curr task指向一个volatile对象
volatile int iax [max_size]; // iax的每个元素都是volatile
volatile Screen bitmapBuf; // bitmapBuf的每个成员都是volatile
const和volatile限定符互相没什么影响,某种类型可能既是const的也是volatile的,此时它同时具有二者的属性。
就像一个类可以定义const成员函数一样,它也可以将成员函数定义成volatile的。 只有volatile的成员函数才能被volatile的对象调用。
const限定符和指针的相互作用,在volatile限定符和指针之间也存在类似的关系。我们可以声明volatile指针、指向volatile对象的指针以及指向volatile对象的volatile指针:
volatile int v; // v是一个volatile int
int *volatile vip; // vip是一个volatile指针,它指向int
volatile int *ivp; // ivp是一个指针,它指向一个volatile int
volatile int *volatile vivp; // vivp是一个volatile指针,它指向一个volatile int
int intv;
int *ip = &v;//错误:必须使用指向volatile的指针
ivp = &v; //正确:ivp是一个指向volatile的指针
vivp = &v;//正确:vivp是一个指向volatile的volatile指针
v = intv;//正确:可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
intv = v;//错误
●和const一样,我们只能将一个 volatile对象的地址(或者拷贝一个指向volatile类型的指针)赋给一个指向volatile的指针。同时,只有当某个引用是volatile的时,我们才能使用一个volatile对象初始化该引用。
●C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
1.2合成的拷贝对volatile对象无效
const和volatile的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。合成的成员接受的形参类型是(非volatile)常量引用,显然我们不能把一个非volatile引用绑定到一个volatile对象上。
如果一个类希望拷贝、移动或赋值它的volatile对象,则该类必须自定义拷贝或移动操作。例如,我们可以将形参类型指定为const volatile引用,这样我们就能利用任意类型的Foo进行拷贝或赋值操作了:
class Foo
{
public:
Foo(const volatile Foo&);//从一个volatile对象进行拷贝
Foo& operator=(volatile const Foo&);//将一个volatile对象赋值给一个非volatile对象
Foo& operator=(volatile const Foo&) volatile;//将一个volatile对象赋值给一个volatile对象
//Foo类的剩余部分
};
尽管我们可以为volatile对象定义拷贝和赋值操作,但是一个更深层次的问题是拷贝volatile对象是否有意义呢?不同程序使用volatile的目的各不相同,对上述问题的同答与具体的使用目的密切相关。
1.3多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。
如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。
volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
1.4保证变量在内存中的可见性
有数据一致性的隐患
1.5禁止编译器做过度优化
1.6禁止指令重排
voltile具有内存屏障的功能
参考资料来源:
阿秀、C++Primer、在划水里划水
2.mutable
2.1const修饰成员函数
●用const修饰的成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量
● 当成员变量类型符前用mutable修饰时例外。
//const修饰成员函数
class Person{
public:
Person(){
this->mAge = 0;
this->mID = 0;
}
//在函数括号后面加上const,修饰成员变量不可修改,除了mutable变量
void sonmeOperate() const{
//this->mAge = 200; //mAge不可修改
this->mID = 10; //const Person* const this;
}
void ShowPerson(){
cout << "ID:" << mID << " mAge:" << mAge << endl;
}
private:
int mAge;
mutable int mID;
};
int main(){
Person person;
person.sonmeOperate();
person.ShowPerson();
system("pause");
return EXIT_SUCCESS;
}
2.2const修饰对象(常对象)
●常对象只能调用const的成员函数
● 常对象可访问 const 或非 const 数据成员,不能修改,除非成员用mutable修饰
程序1:
class Person
{
public:
Person()
{
this->mAge = 0;
this->mID = 0;
}
void ChangePerson() const
{
mAge = 100;
mID = 100;
}
void ShowPerson()
{
cout << "ID:" << this->mID << " Age:" << this->mAge << endl;
}
public:
int mAge;
mutable int mID;
};
void test()
{
const Person person;
//1. 可访问数据成员
cout << "Age:" << person.mAge << endl;
//person.mAge = 300; //不可修改
person.mID = 1001; //但是可以修改mutable修饰的成员变量
//2. 只能访问const修饰的函数
//person.ShowPerson();
person.ChangePerson();
}
程序2:
#pragma warning(disable:4996)
//2022年9月22日21:46:00
#include <iostream>
using namespace std;
class Maker
{
public:
int id;
int mAge;
mutable int score;//mutable修饰的成员变量
public:
Maker(int id, int age)
{
this->id = id;
this->mAge = age;
score = 100;
}
//常函数
void printMaker() const//1.函数后面加上const,该函数就是常函数
{
//id = 100;//err 2.常函数内不能修改普通成员变量
//3.const修饰的是this指针指向的空间中的变量,不能修改
//Maker *const this;
//变成
//const Maker *const this;//这是常函数修饰的
score = 200; //4.mutable修饰的变量在常函数中可以修改
cout << "score = " << score << endl;
}
};
void test01()
{
Maker m(1, 18);
m.printMaker();
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
输出结果:
score = 200
请按任意键继续. . .
程序3:
#pragma warning(disable:4996)
//2022年9月22日22:28:03
#include <iostream>
using namespace std;
class Maker
{
public:
int id;
int mAge;
mutable int score;//mutable修饰的成员变量
public:
Maker(int id, int age)
{
this->id = id;
this->mAge = age;
score = 100;
}
//常函数
void printMaker() const//1.函数后面加上const,该函数就是常函数
{
//id = 100;//err 2.常函数内不能修改普通成员变量
//3.const修饰的是this指针指向的空间中的变量,不能修改
//Maker *const this;
//变成
//const Maker *const this;//这是常函数修饰的
score = 200; //4.mutable修饰的变量在常函数中可以修改
cout << "score = " << score << endl;
}
};
void test01()
{
Maker m(1, 18);
m.printMaker();
}
void test()
{
//1.在数据类型前面加上const,让对象成为常对象
const Maker m(1, 18);
//m.id = 100;常对象不能改变普通成员变量的值
//m.func();//常对象不能调用普通成员函数
m.printMaker();//常对象可以调用常函数
m.score = 500;//常对象可以修改mutable修饰的成员变量
Maker m2(2, 20);
m2.printMaker();//普通对象可以调用常函数
}
int main()
{
test();
system("pause");
return EXIT_SUCCESS;
}
输出结果:
score = 200
score = 200
请按任意键继续. . .
参考资料来源于:
黑马程序员等
3.explicit
c++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。
[explicit注意]
● explicit用于修饰构造函数,防止隐式转化。
● 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
class MyString{
public:
explicit MyString(int n){
cout << "MyString(int n)!" << endl;
}
MyString(const char* str){
cout << "MyString(const char* str)" << endl;
}
};
int main(){
//给字符串赋值?还是初始化?
//MyString str1 = 1; //会进行隐式类型转换MyString(1)
MyString str2(10);
//寓意非常明确,给字符串赋值
MyString str3 = "abcd";
MyString str4("abcd");
return EXIT_SUCCESS;
}
错误:
//2022年9月20日20:23:11
#include <iostream>
using namespace std;
class Maker
{
public:
//explicit只能放在构造函数前面,构造函数只有一个参数或其他参数有默认值时
explicit Maker(int n)//防止编译器优化Maker m = 10;这种格式
{
}
};
int main()
{
Maker m = 10;
system("pause");
return EXIT_SUCCESS;
}
参考资料
参考资料来源于黑马程序员、阿秀等