39.volatile、mutable和explicit关键字的用法

发布时间 2023-07-03 21:41:19作者: CodeMagicianT

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禁止编译器做过度优化

image-20230701114139700

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;
}

参考资料

参考资料来源于黑马程序员、阿秀等