C++进阶 —— (C++11新特性)

news2024/11/28 14:36:35

三,范围for循环

四,final与override

五,智能指针

六,静态数组array、forward_list、unordered系列(新增容器)

七,默认成员函数的控制

        在C++中,对于空类编译器会生成一些默认成员函数(如构造函数、拷贝构造函数、运算符重载、析构函数,&和const&重载、移动构造函数、移动拷贝构造函数等);如在类中显示定义了,编译器将不会重新生成默认版本;有时这些规则可能被忘记,最常见的是声明了带参数的构造函数,必要是则需要定义不带参数的版本以实例化无参的对象;而且有时编译器会生成,有时又不会生成,容易产生混乱,于是C++11让程序员可以控制是否需要编译器生成;

显示缺省函数

        在C++11中可以在默认函数定义或声明时加上=default,从而显示的指示编译器生成该函数的默认版本,用=default修饰的函数称为显示缺省函数;

class A
{
public:
    A(int a)
        :_a(a)
    {}
    //显示缺省构造函数,由编译器生成
    A() = default;
    //在类中声明,在类外定义时让编译器默认生成
    A& operator=(const A& a);
private:
    int _a;
}
A& A::operator=(const A& a) = default;

删除默认函数

        如想限制某些默认函数的生成,在C++98中将函数设置成private并且不给定义,这样只要调用就会报错;在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数;

注意,避免删除函数和explicit一起使用;

//C++98
class A
{
public:
    A(int a)
        :_a(a)
    {}
private:
   A() = default;
   A& operator=(const A& a);
private:
    int _a;
};
class A
{
public:
    A(int a)
        :_a(a)
    {}
    //禁止编译器生成默认的拷贝构造和赋值运算符重载
    A() = delete;
    A& operator=(const A& a) = delete;
private:
    int _a;
};

八,右值引用

右值引用介绍

        C++98提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块空间,二引用底层是通过指针来实现的,使用引用可提高程序的可读性;

void swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

为了提高程序的运行效率,C++11引入了右值引用,右值引用也是别名,但其只能对右值引用;

左值与右值

        左值与右值是C语言中的概率,但并没有严格的区分方式;一般认为,可以放在=左边的、或能取地址的称为左值,只能放在=右边的、或不能取地址的称为右值,但不完全正确;

  • 普通类型的变量,因为有名字,可以取地址,都认为是左值;
  • const修饰的常量,不可修改,只读类型,理论应该是右值,但是可以取地址,C++11认为是左值;
  • 如表达式的运行结果是临时变量或对象,认为是右值;
  • 如表达式的运行结果或单个变量是引用则认为是左值;

总结

  • 不能简单通过是否放在=左右侧或取地址来判断左右值,要根据表达式结果或变量的性质判断;
  • 能得到引用的表达式一定能够作为引用,否则就是常引用;

C++11对右值进行了严格的区分

  • C语言中的纯右值,如a+b、100;
  • 将亡值,如表达式的中间结果,函数按照值的方式进行返回;

引用与右值引用比较

C++98中,普通引用只能引用左值不能引用右值,const引用即可引用左值也可引用右值;

C++11中,右值引用只能引用右值,一般情况下不能直接引用左值;

//普通类型只能引用左值,不能引用右值
int a = 10;
int& ra = a;
//int& rb = 10; 编译失败,10是右值

const int& rb = 10;
const int& rc = a;

//右值引用只能引用右值,一般不能引用左值
int&& rd = 10;
//int&& re = a; 编译失败,a是左值

值的形式返回对象的缺陷

        如一个类中涉及到资源管理,需显示提供拷贝构造、赋值运算符重载以及析构函数,否则拷贝对象或对象之间赋值就会出错(浅拷贝);

class string
{
public:
	string(const char* str = "")
	{
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	string(const string& s)
		:_str(new char[strlen(s._str)+1])
	{
		strcpy(_str, s._str);
	}
	string& operator=(const string& s)
	{
		if (this != &s)
		{
			char* tmp = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);
			delete[] _str;
			_str = tmp;
		}
		return *this;
	}
	string operator+(const string& s)
	{
		char* tmp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(tmp, _str);
		strcpy(tmp + strlen(_str), s._str);
		string strRet(tmp);
		return strRet;
	}
	~string()
	{
		delete[] _str;
	}
private:
	char* _str;
};

int main()
{
 String s1("hello");
 String s2("world");
 String s3(s1+s2);
 return 0;
}

        上述代码s1+s2调用operator+中,返回strRet值时,需创建一个临时对象,然后销毁strRet,然后使用临时对象构造s3,最后在销毁此临时对象;在这个过程中,strRet、临时对象,S3,都有自己独立的空间,相当于创建了三个内容完全相同的对象,浪费空间,程序效率低下,且临时对象作用不大;

移动语义

  • 即将一个对象中资源移动到另一个对象中的方式;
  • C++11中如需实现移动语义,必须使用右值引用;

上述代码s1+s2调用operator+中,引入移动构造,将strRet中资源转移到临时对象中;

string(string&& s)
    :_str(s.str)
{
    s.str = nullptr;
}

        strRet在创建好临时对象后将结束,是将亡值(属右值);在用strRet构造临时对象时,就会调用移动构造,将strRet中资源转移到临时对象中,而临时对象也属于右值,因此在用临时对象构造s3时,也会调用移动构造,将临时对象中资源转移到s3中,整个过程只需创建一块堆内存即可,节省空间也提供运行效率;

注:

  • 移动构造函数的参数,不要设置成const类型的右值引用,因为资源无法转移而导致移动语义失效;
  • 在C++11中编译器会为类默认生成一个移动构造,为浅拷贝,因此当类中涉及到资源管理时需显示定义移动构造;

右值引用引用左值

        按照语法右值引用只能引用右值,但有些场景可能需要用右值去引用左值实现移动语义;当需要用右值引用引用左值时,可通过move函数将左值转化为右值;

std::move()函数位于头文件<utility>中,其唯一的功能就是将一个左值强制转化为右值引用,以实现移动语义;

template<class _Ty>
inline typename remove_reference<_ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
    return ((typename remove_reference<_Ty>::type&&)_Arg);
}

注:

  • 被转化的左值,其生命周期并没有随左值的转化而改变,即std::move转化的左值变量不会被销毁;
  • STL中也有另一个move函数,将一个范围的元素搬移到另一个位置;
string s1("hello world");
string s2(move(s1));
string s3(s2);

以上代码是move函数的经典误用,move将s1转化为右值后,在实现s2的拷贝时会使用移动构造,此时s1的资源就被转移到s2中了,s1就会成为无效字符串;

完美转发

  • 是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数;
void Func(int x)
{
    cout << x << endl;
}

template<typename T>
void PerfectForward(T t)
{
    Func(t);
}

        PerfectForword为转发的模板函数,Func为实际目标函数,但上述转发不算完美;完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转发给目标函数,而不产生额外开销,就好像转发者不存在一样;

        所谓完美,函数模板在向其他函数传递自身形参时,如相应实参是左值,就应该被转发为左值;如相应实参是右值,就应该被转发为右值;这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(如参数为左值时拷贝语义,参数为右值移动语义);

C++11通过forward函数来实现完美转发!

void Fun(int& x) { cout << "int& -> lvalue ref" << endl; }
void Fun(int&& x) { cout << "int&& -> rvalue ref" << endl; }
void Fun(const int& x) { cout << "const int& -> lvalue ref" << endl; }
void Fun(const int&& x) { cout << "const int&& -> rvalue ref" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

int main()
{
	PerfectForward(10); //10是右值,int&& -> rvalue ref

	int a;
	PerfectForward(a); //a是左值,int& -> lvalue ref
	PerfectForward(std::move(a)); //将a转化为右值 int&& -> rvalue ref

	const int b = 8;
	PerfectForward(b); //b是const左值 const int& -> lvalue ref
	PerfectForward(std::move(b)); //将b转化为const右值 const int&& -> rvalue ref
	return 0;
}

右值引用作用

        C++98中引用作用,因为引用是别名,需要用指针操作的地方,可以使用引用来替代,可提高代码的可读性及安全性;

C++11中右值引用主要有以下作用

  • 实现移动语义(移动构造或移动赋值);
  • 给中间临时变量取别名;
  • 实现完美转发;

九,lambda表达式

        在C++98中,如对一个数据集合中的元素进行排序,可使用sort;如元素为自定义类型,需定义排序时的比较规则;随着C++的发展,人们开始觉的上面的写法太复杂了,为了实现一个algorithm算法,都要重新写一个类,如每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,都会非常不方便;

int arr[] = { 4,1,5,3,8,7 };
std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]));
std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>());
struct Goods
{
	string _name;
	double _price;
};
struct Compare
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price <= gr._price;
	}
};
int main()
{
	Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
	return 0;
}

lambda表达式

//lambda表达式实际是一个匿名函数
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), 
[](const Goods& l, const Goods& r)->bool{return l._price < r._price;});

lambda表达式语法

[capture-list] (parameters) mutable -> return-type { statement }

  • [capture-list],捕捉列表,该列表总是在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用;
  • (parameters),参数列表,与普通函数的参数列表一致,如不需要参数传递,可连同()一起省略;
  • mutable,默认情况下lambda总是一个const函数,mutable可取消其常量性,使用该修饰符时参数列表不可省略(即使参数为空);
  • -> return-type,返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略,返回值类型明确情况下也可省略,由编译器对返回类型进行推导;
  • { statement },函数体,该函数体内,除了可以使用参数外,还可以使用所有捕获到的变量;

注,在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空;因此C++11中最简单的lambda函数为:[]{};该函数不能做任何事;

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如想直接调用,可借助auto将其赋值给一个变量;

[] {};
int a = 3, b = 4, x = 10;
[=] {return a + 3; };
[&](int c) {b = a + c; };
auto fun1 = [=, &b](int c)->int {return b += a + c; };
fun1(10);
auto fun2 = [x](int a) mutable { x *= 2; return a + x; };
fun2(10);

[capture-list] 捕捉列表

捕捉列表描述了上下文中那些数据可被lambda使用,及使用的方式(传值、传引用);

  • [var],表示值传递方式捕捉变量var;
  • [=],表示值传递方式捕捉所有父作用域的变量(包括this);
  • [&var],表示引用传递捕捉变量var;
  • [&],表示引用传递捕捉所有父作用域中的变量(包括this);
  • [this],表示值传递方式捕捉当前的this指针;

注:

  • 父作用域指包含lambda函数的语句块;
  • 语法上捕捉列表可由多个捕捉项组成,以逗号隔开;
  • 捕捉列表不允许变量重复传递,否则编译报错;
  • 在块作用域意外的lambda函数捕捉列表必须为空;
  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或非局部变量都会编译报错;
  • lambda表达式之间不能相互赋值,即使看起来类型相同;

函数对象与lambda表达式

函数对象又称仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象;

实际上,底层编译器对于lambda表达式的处理方式,完全是按照函数对象的方式处理的,即如定义了一个lambda表达式,编译器会自动生成一个类,该类中重载了operator();

class Rate
{
public:
	Rate(double rate)
		:_rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};

int main()
{
	//函数对象
	double rate = 0.49;
	Rate r1 = (rate);
	r1(10000, 2000);
	//lambda表达式
	auto r2 = [=](double money, int year)->double {return money * rate * year; };
	r2(10000, 2000);
	return 0;
}

十,线程库

thread类的简单介绍

在C++11之前涉及多线程问题,都是和平台相关的,如windows和Linux下各有自己的接口,这使代码的可移植性较差;C++11中最重要的特性就是对线程进行支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引用了原子类的概念;

要使用标准库中的线程,必须包含<thread>头文件;

  • thread(),构造一个线程对象,没有关联任何线程函数,即没有启动任何线程;
  • thread(fn, args1, args2, ...),构造一个线程对象,并关联线程函数;
  • get_id(),获取线程id;
  • jionable(),线程是否还在执行,jionable代表一个正在执行中的线程;
  • jion(),该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行;
  • detach(),在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的“死活”与主线程无关;

 注:

  • 线程是操作系统的一个概念,线程对象可关联一个线程,用来控制线程以及获取线程的状态;
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程;
thread t;
cout << t.get_id() << endl;

get_id()返回类型为id,id类型实际为std::thread命名空间下封装的一个类;

  •  当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行;线程函数一般情况下可按照以下三种方式提供;
    • 函数指针
    • lambda表达式
    • 函数对象
#include <iostream>
#include <thread>
using namespace std;

void ThreadFunc(int a)
{
	cout << "Thread1:" << a << endl;
}

class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};

int main()
{
	//线程函数为函数指针
	thread t1(ThreadFunc, 10);
	//线程函数为lambda表达式
	thread t2([] {cout << "Thread2" << endl; });
	//线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();
	cout << "Main Thread!" << endl;
	return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但可移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行;
  • 可通过jionable()函数判断线程是否是有效的,如是以下任意情况则线程无效;
    • 采用无参构造函数构造的线程对象;
    • 线程对象的状态已经转移给其他线程对象了;
    • 线程已经调用jion或detach结束;

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间的,因此即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参;

注,如是类成员函数作为线程参数时,必须将this作为线程函数参数;

#include <iostream>
#include <thread>
using namespace std;

void ThreadFunc1(int& x)
{
	x += 10;
}
void ThreadFunc2(int* x)
{
	*x += 10;
}

int main()
{
	int a = 10;
	//在线程函数中对a修改,不会影响外部实参,因为线程函数参数虽然是引用方式,但实际引用的是线程栈中的拷贝;
	thread t1(ThreadFunc1, a);
	t1.join();
	cout << a << endl;
	//如像通过形参改变外部实参,需借助std::ref()函数
	thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	cout << a << endl;
	//地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;
}

jion与detach

启动了一个线程后,当这个线程结束的时候,thread库有两种方式回收线程使用的资源;

jion()

  • 主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象;由于jion()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程对象只能使用一次jion(),否则程序会崩溃;
//jion()的误用一
//如DoSomething()返回false主线程结束,jion()没有调用,线程资源没有回收,造成资源泄露
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; }
bool DoSomething() { return false; }
int main()
{
 std::thread t(ThreadFunc);
 if(!DoSomething())
 return -1;

 t.join();
 return 0;
}
// jion()的误用二
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; }
void Test1() { throw 1; }
void Test2()
{
 int* p = new int[10];
 std::thread t(ThreadFunc);
 try
 {
 Test1();
 }
 catch(...)
 {
 delete[] p;
 throw;
 }

 t.jion();
}
  • 因此采用jion()方式结束线程时,jion()的调用位置非常关键,为避免该问题,可采用RAII方式对线程对象进行封装;
#include <iostream>
#include <thread>
using namespace std;
class mythread
{
public:
    explicit mythread(std::thread& t)
        :m_t(t)
    {}
    ~mythread()
    {
        if (m_t.joinable())
            m_t.join();
    }
    mythread(mythread const&) = delete;
    mythread& operator=(const mythread&) = delete;
private:
    std::thread& m_t;
};

void ThreadFunc() { cout << "ThreadFunc()" << endl; }
bool DoSomething() { return false; }

int main()
{
    thread t(ThreadFunc);
    mythread q(t);
    if (DoSomething())
        return -1;
    return 0;
}

detach()方式

  • 该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会给C++运行库;同时C++运行库保证,当线程退出时,其相关资源能够正确回收;
  • detach()函数一般在线程对象创建好后就调用,因为如不是jion()等待方式结束,那么线程对象可能会在新线程结束之前被销毁而导致程序崩溃;因为std::thread的析构函数中,如线程的状态是jionable,std::terminate将会被调用,而terminate()函数直接会终止程序;
  • 因此线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式将线程对象分离;

原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全);如共享数据都是只读,没有问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据;但当一个或多个线程要修改共享数据时,就会产生潜在的麻烦;

#include <iostream>
#include <thread>
using namespace std;

unsigned long sum = 0L;
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;
}

int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

C++98传统的解决方式,对共享修改的数据可加锁保护;

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		m.lock();
		sum++;
		m.unlock();
	}
}

int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

虽然加锁可以解决,但加锁有一个缺陷,就是只要一个线程在对sum++是,其他线程就会被阻塞,会影响程序运行效率,而且锁如果控制不好,容易造成死锁;

因此C++11引入了原子操作,即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效;需要使用以上原子操作变量时,必须添加头文件;

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic_long sum{ 0 };
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++; // 原子操作
}
int main()
{
	cout << "Before joining, sum = " << sum << std::endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();

	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥访问;可以使用atomic类模板,定义出需要的任意原子类型;

// 声明一个类型为T的原子类型变量t
atmoic<T> t; 

注,原子类型通常属于“资源型”数据,多个数据只能访问单个原子类型的拷贝,因此C++11中原子类型只能从其他模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atomic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除了;

lock_guard与unique_lock

在多线程环境下,如想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题,但有些情况下,可能需要保证一段代码的安全性,那么就只能通过死锁的方式进行控制了;

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int number = 0;
mutex g_lock;
int ThreadProc1()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		++number;
		cout << "thread 1 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}
int ThreadProc2()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		--number;
		cout << "thread 2 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}
int main()
{
	thread t1(ThreadProc1);
	thread t2(ThreadProc2);
	t1.join();
	t2.join();
	cout << "number:" << number << endl;
	system("pause");
	return 0;
}

上述代码的缺陷是锁控制不好可能会造成死锁,最常见的比如在锁中间代码返回,或在锁的范围内抛异常;因此C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock;

mutex的种类

C++11中,mutex总共四个互斥量的种类;

std::mutex

  • C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动;
  • 最常见三个函数
    • lock(),上锁,锁住互斥量;
    • unlock(),解锁,释放对互斥量的所有权;
    • try_lock(),尝试锁住互斥量,如互斥量被其他线程占用,则当前线程也不会被阻塞;

线程函数调用lock()时,可能会发生以下三种情况

  • 如该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁;
  • 如当前互斥量被其他前程锁住,则当前的调用线程被阻塞;
  • 如当前互斥量被当前调用线程锁住,则会产生死锁;

线程函数调用try_lock()时,可能会发生以下三种情况

  • 如当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量;
  • 如当前互斥量被其他线程锁住,则当前调用线程返回false,并不会被阻塞掉;
  • 如当前互斥量被当前调用线程锁住,则会产生死锁;

std::recursive_mutex

  • 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的unlock(),除此之外,与std::mutex大致相同;

std::timed_mutex

  • 比std::mutex多了两个成员函数,try_lock_for(),try_lock_until();

try_lock_for()

  • 接受一个时间范围,表示在这一段时间范围内,线程如没有获得锁则被阻塞(与std::mutex的try_lock()不同,try_lock如被调用时没有获得锁则直接返回false),如在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如超时(即在指定时间内还是没有获得锁),则返回false;

try_lock_until()

  • 接受一个时间点作为参数,在指定时间点未到来之前线程如没有获得锁则被阻塞,如在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如超时(即在指定时间内还是没有获得锁),则返回false;

std::recursive_timed_mutex

lock_guard

std::lock_guard是C++11中定义的模板类;

template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。

在构造(或移动(move)赋值)时, unique_lock 对象需要传递一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入的Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁, unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放 (release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、 mutex(返回当前unique_lock所管理的互斥量的指针)。

lock_guard/unique_lock详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/597788.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式Linux驱动开发 03:平台(platform)总线驱动模型

文章目录 目的基础说明开发准备在驱动中获取资源单驱动使用多个资源总结 目的 前面文章 《嵌入式Linux驱动开发 01&#xff1a;基础开发与使用》 和 《嵌入式Linux驱动开发 02&#xff1a;将驱动程序添加到内核中》 介绍了驱动开发最基础的内容&#xff0c;这篇文章将在前面基…

Vue3 小兔鲜4:Layout-静态模版结构搭建

Vue3 小兔鲜4&#xff1a;Layout-静态模版结构搭建 Date: May 31, 2023 目标效果&#xff1a; 分成Nav、Heade、二级路由出口、Footer区域 组件结构快速搭建 Nav <script setup></script><template><nav class"app-topnav"><div clas…

如何用VS2019创建并调用动态库

如何用VS2019创建并调用动态库 创建动态库调用动态库 创建动态库 网上查了相关资料&#xff0c;创建动态库主要有两种方式&#xff0c;一种是通过空项目创建动态库&#xff0c;一种是直接创建动态链接库&#xff0c;本文所总结的就是第二种&#xff0c;直接创建动态链接库。 …

B树(C语言描述)

一.概念 B树是一种多路平衡查找树&#xff0c;不同于二叉平衡树&#xff0c;他不只是有两个分支&#xff0c;而是有多个分支&#xff0c;一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树&#xff0c;B树用于磁盘寻址&#xff0c;它是一种高效的查找算法。 二.性质…

【Kubernetes 入门实战课】Day03——容器的本质

系列文章目录 【Kubernetes 入门实战课】Day01——虚拟机创建及安装 【Kubernetes 入门实战课】Day02——初识容器 文章目录 系列文章目录前言一、容器到底是什么&#xff1f;二、为什么要隔离三、与虚拟机的区别是什么四、隔离是怎么实现的 前言 上一节中我们完成了在Linux虚…

Anaconda下载安装及使用方法汇总

软件说明: Anaconda是Red Hat Linux和Fedora的安装管理程式。它以Python及C语言写成&#xff0c;以图形的PyGTK和文字的python-newt介面写成。它可以用来自动安装配置&#xff0c;使用户能够以最小的监督运行。Anaconda安装管理程式应用在RHEL&#xff0c;Fedora和其他一些项目…

IMX6ULL裸机篇之I2C实验-

一. I2C 实验简介 I2C实验&#xff0c;我们就来学习如何使用 I.MX6U 的 I2C 接口来驱动 AP3216C&#xff0c;读取 AP3216C 的传感器数据。 AP3216C是一个三合一的环境光传感器&#xff0c;ALSPSIRLED&#xff0c;ALS是环境光&#xff0c;PS是接近传感器&#xff0c;IR是红外L…

MANTOO车联网RSU终端助您畅享智慧出行!

一、案例背景 随着社会经济的飞速发展&#xff0c;汽车逐渐走进了千家万户&#xff0c;目前我国家庭乘用汽车保有量在2.6亿辆&#xff0c;平均每6个人就拥有一辆汽车。汽车保有量的上涨同时也给道路交通安全带来了极大的挑战&#xff0c;为了降低交通事故发生&#xff0c;保障…

牛客网项目—开发社区首页

视频连接&#xff1a;开发社区首页_哔哩哔哩_bilibili 代码地址&#xff1a;Community: msf begin 仿牛客论坛项目 (gitee.com) 本文是对仿牛客论坛项目的学习&#xff0c;学习本文之前需要了解Java开发的常用框架&#xff0c;例如SpringBoot、Mybatis等等。如果你也在学习牛…

遗传算法讲解

遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09; 是模拟生物在自然环境中的遗传和进化的过程而形成的自适应全局优化搜索算法。它借用了生物遗传学的观点&#xff0c;通过自然选择、遗传和变异等作用机制&#xff0c;实现各个个体适应性的提高。 基因型 (G…

文件阅览功能的实现(适用于word、pdf、Excel、ppt、png...)

需求描述&#xff1a; 需要一个组件&#xff0c;同时能预览多种类型文件&#xff0c;一种类型文件可有多个的文件。 看过各种博主的方案&#xff0c;其中最简单的是利用第三方地址进行预览解析&#xff08;无需任何插件&#xff09;&#xff1b; 这里推荐三个地址&#xff1a…

EasyExcel实现excel区域三级联动(模版下载)

序号 前言需求不通过excel,实现省市区级联实战pom.xml配置controller配置service类业务处理类测试 前言 首先&#xff0c;我们先来了解一下java实现模板下载的几种方式 1、使用poi实现2、使用阿里的easyexcel实现 今天社长就给大家说一下easyexcel的实现模板下载的之旅。在这里…

phpword使用整理

目录 介绍 安装 创建文档 设置默认字体和字号 设置文本样式 编号标题 换行符 分页符 超链接 创建表格 添加图片 文件保护 加载word文件 内容转化为html 保存 模板替换 格式 加载模板 替换字符串 替换图片 替换表格 总结 参考 介绍 PHPWord 是一个用纯 …

Vue3 过渡动画效果

文章目录 Vue3 过渡动画效果概述<Transition>组件简单使用为过渡效果命名自定义过渡classJavaScript动画效果元素间过渡 <transition-group>组件列表动画状态动画 Vue3 过渡动画效果 概述 Vue 提供了两个内置组件&#xff0c;可以帮助你制作基于状态变化的过渡和…

中服云设备全生命周期管理系统4.0全新升级,震撼登场!

6月2日&#xff0c;中服云设备全生命周期管理系统4.0将在中服云官方视频号线上直播震撼发布。在此次线上直播发布会上&#xff0c;中服云将详细地介绍设备全生命周期管理系统4.0版本的全新特性和创新功能。同时邀请了业内权威售前顾问、设备管理工程师和合作伙伴&#xff0c;共…

降低FTP服务器速度的解决方案(Filezilla等)

我最近发现&#xff0c;尽管有70Mbps&#xff08;8.75MB / s&#xff09;的互联网连接和1Gbps&#xff08;125MB / s&#xff09;的专用服务器可以从中下载&#xff0c;但我似乎只能从FTP服务器上以大约16.8Mbps&#xff08;2.1MB / s&#xff09;的速度下载。在一个线程上。但…

深入篇【Linux】学习必备:理解文件权限

深入篇【Linux】学习必备&#xff1a;理解文件权限 Ⅰ.Linux权限的概念Ⅱ.Linux权限管理①.文件访问者的分类(访问者的身份)②.文件类型和访问权限(文件本身的事物属性)1.文件类型&#xff1a;2.基本权限: ③.文件权限值的表示方法1.字符表示方法2.八进制数值表示法 ④.文件访问…

【活动回顾】Databend 数据库表达式框架设计与实现 @GOTC

5月28日&#xff0c;“全球开源技术峰会 GOTC 2023 ”圆满落幕。在本次会上&#xff0c;Databend 数据库的 优化器 研发工程师 骆迪安作为嘉宾中的一员&#xff0c;在 rust 专题专区分会场进行了一次主题为《 Rust 实现的先进 SQL Parser 与高效表达式执行框架 — Databend 数…

多语言跨境商城源码,出海跨境商城软件开发模式平台

一、多语言跨境商城模式 多商家模式&#xff1a;容纳不同的商家 多用户模式&#xff1a;用户之社区&#xff0c;用户交互&#xff0c;分享和推广 支持扩展&#xff1a;使用现代化的技术架构和设计&#xff0c;包括支持并发访问、分布式数据存储等功能。 采用常用技术&#x…

合工大Java大作业1:货物进销管理系统

问题描述 编写一个Inventory.java完成以下功能&#xff08;没有学过Java文件处理之前&#xff0c;各位同学可以使用硬编码将数据放进两个Vector变量里。等学过Java文件处理之后&#xff0c;再补充数据文件读取部分&#xff09;&#xff1a; 1&#xff0e;程序首先打开并读取In…