c++11简介
C++11是C++编程语言的一个版本,它是ISO/IEC 14882:2011标准的简称,由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)于2011年8月12日公布,并于2011年9月出版。C++11是C++98发布后13年来的第一次重大修正,也是C++编程语言的第三个官方标准。
C++11引入了很多新特性,例如类型推导(auto关键字)、Lambda表达式、线程库、列表初始化、智能指针、右值引用、包装器等。此外,C++11还对C++03标准中的约600个缺陷进行了修正,使C++11更像是从C++98/03中孕育出的一种新语言。
C++11的新特性使得C++更加现代化、易用和强大。例如,类型推导(auto关键字)可以自动推断变量的类型,减少了程序员的工作量;Lambda表达式可以方便地定义匿名函数,简化了代码;线程库提供了对多线程编程的支持,使得C++能够更好地用于并行计算等场景。
统一的初始化列表
在C++98中,标准允许使用大括号{}对数组或者结构体元素进行统一的列表初始值设定
struct Stu
{
Stu() //支持隐式类型转换
{}
//explicit Stu() //只能被显式地调用,而不能被用于隐式类型转换
string name;
int id;
};
int main()
{
Stu stu = {"小明", 12};
int arr[2] = {1, 2};
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号,也可不添加。
struct Stu
{
string name;
int id;
};
int main()
{
Stu stu {"小明", 12};
int arr[2]{1, 2};
Stu* p = new Stu[2] {{"小东", 1}, {"小西", 2}};
//容器list、vecor、map...都支持这样初始化
//因为c++11新加了initializer_list作为参数的构造函数
vector<int> v{1,2,3,4};
map<string, string> dict{{"apple", "苹果"}};
}
initializer_list
This type is used to access the values in a C++ initialization list, which is a list of elements of type const T.
Objects of this type are automatically constructed by the compiler from initialization list declarations, which is a list of comma-separated elements enclosed in braces
上面的代码中vector用{}初始化和赋值是应为在底层写了用initializer_list作为参数的构造和赋值
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
//for (auto e : l)
// *vit++ = e;
}
vector<T>& operator=(initializer_list<T> l)
{
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
右值引用&移动语义
左值和右值
左值:左值是一个表示数据的表达式(比如变量名或解引用的指针),它有一个明确的内存地址,并且可以通过这个地址进行读取和修改操作。左值可以出现在赋值语句的左边,可以取地址,并且可以在多个位置引用和访问。
int x = 10; // x 是一个左值,因为它有一个明确的内存地址,并且可以被赋值
int& ref = x; // ref 是 x 的引用,也是一个左值
int arr[5];
arr[2] = 42; // arr[2] 是一个左值,因为它是数组的一个元素,具有明确的位置
// 可以对左值取地址
int* ptr = &x;
// 可以对左值进行赋值
x = 20;
ref = 30;
右值:右值通常指的是那些没有持久存储位置的对象,它们通常是在表达式中临时生成的值,比如字面量、临时对象或者返回非引用类型值的函数返回的结果。右值通常用于初始化对象或赋值给左值,但不能被取地址或赋值。
右值在C++11中进一步被细分为纯右值(prvalue)和将亡值(xvalue)。纯右值包括运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的函数返回临时变量、lambda表达式等。将亡值则是指即将被销毁的值,如std::move后的对象。
int y = 10 + 20; // 10 + 20 的结果是一个右值,用于初始化 y
const int& cref = 10 + 20; // 错误!字面量或临时值不能绑定到非const引用上
// 临时对象也是右值
std::string s = std::string("hello") + std::string(" world"); // "hello" 和 " world" 连接产生的临时 std::string 对象是一个右值
// 函数的返回值(如果返回的是非引用类型)也可能是右值
int func() { return 42; }
int z = func(); // func() 的返回值是一个右值,用于初始化 z
// 注意:右值不能取地址(下面代码会编译错误)
// int* ptr2 = &(10 + 20); // 错误!不能对右值取地址
// 右值也不能被赋值(没有意义,因为它们没有存储位置)
// (10 + 20) = 50; // 错误!不能对右值进行赋值
右值引用
右值引用是对右值的引用。在C++中,右值通常指的是临时对象(如字面量、表达式返回值等),它们没有持久的状态,并且其生命周期通常很短。右值引用使用&&
符号表示。
int main()
{
2;
int&& rr = 2;
}
注意:
//1.右值是不能取地址的,但是给右值取别名之后,会导致右值存储到特定的位置,可以取到该位置的地址
void test1()
{
int&& rr1 = 10;
rr1 = 20; //可以
}
//2.左值引用与右值引用比较
//2.1左值引用只能引用左值,不能引用右值
//2.2const左值引用既可引用左值,也可引用右值
void test2_1()
{
int x = 2;
int& r = 2; //error
const int & r1 = 2; //可以
}
//2.3右值引用只能右值,不能引用左值。
//2.4右值引用可以move以后的左值
//move告诉编译器希望像右值一样处理,但是此时x已经不是2了处于一个未定义的状态,
//不能再依赖x原来的值,也就是不能使用移动后源对象的值。
//std::move本身并不会移动任何数据。它只是将源对象的资源所有权转移给目标对象,并将源对象置于一个有效但未定义的状态。实际的资源移动(如内存拷贝或指针交换)是在移动构造函数或移动赋值运算符中完成的。
void test2_3()
{
int x = 2;
int&& rr = x; //error
int&& rr1 = std::move(x); //可以
}
右值引用主要有两个用途:
- 移动语义(Move Semantics):允许我们避免不必要的拷贝操作,特别是在处理大量数据时。
- 完美转发(Perfect Forwarding):允许函数模板将参数原样地(包括其值类别)转发给另一个函数。
移动语义
为了让自定义类型支持移动操作,需要定义移动构造函数和移动赋值运算符。在c++11更新后,容器的插入接口函数增加了右值引用。
string
namespace jt
{
// 拷贝构造
//const左值引用是可以引用右值的
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造 --把资源原封不动的给别人
string(string&& s)
:_str(nullptr)
,_size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
this->swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动资源" << endl;
swap(s);
return *this;
}
}
完美转发
完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其他函数中。这里的“完美”指的是不仅能准确地转发参数的值,还能保证被转发的参数的左、右值属性不变。
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
//模板中叫万能引用,既能接收左值又能接收右值
//但是在后序的时候中都会退化为左值
template<typename T>
void f(T&& t)
{
Fun(t);
}
//std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void Perfectf(T&& t)
{
Fun(std::forward<T>(t))
}
int main()
{
f(10); // 右值
int a;
f(a); // 左值
f(std::move(a)); // 右值
const int b = 8;
f(b); // const 左值
f(std::move(b)); // const 右值
}
完美转发其他应用
//只要是右值,相向下传递必须用完美转发
void PushBack(T&& x)
{
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
为什么有了左值还需要右值?
- 右值引用和移动语义允许在不需要对象持久存在的情况下,将其资源(如内存、文件句柄等)从一个对象“移动”到另一个对象,而不是进行复制。这可以显著提高程序的性能,特别是在处理大型对象或容器时。
- 在函数参数传递时,区分左值和右值允许我们更精细地控制参数的传递方式。对于左值参数,我们通常使用左值引用或值传递;而对于右值参数,我们可以使用右值引用,并利用移动语义来避免不必要的拷贝。
- 返回值优化:避免不必要的拷贝和内存分配,从而提高程序的性能。
lambda表达式
语法格式
[capture-list](parameter-list) -> return-type { function-body }
capture-list
:捕获列表,指定lambda内部可以访问哪些外部变量,以及如何访问它们(按值捕获或按引用捕获)。parameter-list
:参数列表,与普通函数的参数列表类似。如果不需要参数传递,则可以连同()一起省略return-type
:返回类型,如果省略,编译器会根据函数体中的返回语句进行类型推断。function-body
:函数体,包含lambda函数的实现。
捕获列表说明
-
[var]:表示值传递方式捕捉变量var
-
[=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)(默认捕获方式)
-
[&var]:表示引用传递捕捉变量var
-
[&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
-
[=, &a, &b] //可以 [=, a, ] //不可以,捕捉列表不允许变量重复传递,否则就会导致编译错误。 const auto l1 = []() { return 1; }; // 没有捕获任何内容 const auto l2 = [=]() { return x; }; // 按值捕获所有变量 const auto l3 = [&]() { return y; }; // 按引用捕获所有变量 const auto l4 = [x]() { return x; }; // 仅对x进行按值捕获 const auto l5 = [&y]() { return y; }; // 仅对y进行按引用捕获 const auto l6 = [x, &y]() { return x * y; }; // 对x按值捕获,对y按引用捕获 const auto l9 = [this]() { } // 捕获this指针 const auto la = [*this]() { } // 按值捕获*this对象
注意:
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
- 在块作用域以外的lambda函数捕捉列表必须为空,作用域是指定义lambda表达式的代码块。lambda可以从这个作用域中捕获变量。这些变量可以是该代码块中的局部变量、函数参数或静态变量等。
- lambda表达式本身是一个匿名类型,每个lambda都有自己唯一的类型,不能直接将一个lambda赋值给另一个lambda变量
lambda的使用
struct Stu
{
string _name;
int _id;
};
struct Compateid
{
bool Compateid(const Stu& s1, const Stu& s2)
{
return s1._id < s2._id;
}
}
int main()
{
Stu s1 = {{"小米", 1}, {"小红", 2}};
//仿函数写法
sort(s1, sizeof(s1)/ sizeof(s1[0]), Compateid);
//lambad表达式
sort(s1, sizeof(s1)/ sizeof(s1[0]), [](const Stu& s1, const Stu& s2
{return s1._id < s2._id ; });
}
lambda底层
对上面代码进行反汇编可以看出,对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
包装器
在C++中,“函数包装器”(Function Wrapper)通常指的是一种技术,它允许你包装(或封装)一个函数或可调用对象,以便在调用原始函数之前或之后执行额外的操作,或者改变函数的某些行为。这种技术可以通过多种方式实现,例如使用函数对象(functor)、lambda表达式、std::function和std::bind等。
函数对象(Functor)
函数对象是一个重载了operator()的类实例,因此它可以像函数一样被调用。函数对象可以用来包装函数,并在调用时执行额外的操作。
class FunctionWrapper {
public:
void operator()(int x) {
// 在这里执行一些操作
std::cout << "hello world " << x << std::endl;
// 调用函数OtherFunction(x)
// OtherFunction(x);
}
};
// 使用
FunctionWrapper wrapper;
wrapper(42); // 输出 "hello world 1 "
function
std::function 是 C++ 标准库中的一个通用、多态的函数封装器。它允许你存储、复制和调用任何可调用的目标(function objects, lambda 表达式, bind 表达式,或其他函数指针和成员函数指针)
基本使用
#include<functional>
#include<iostream>
void print(int x)
{
cout << x << endl;
}
int main()
{
// 创建一个可以调用接受 int 参数、返回 void 的函数的 std::function 对象
std::function<void(int)> func = print;
func(1); //调用func等于调用print
}
function放lambad表达式
//逆波兰表达式求值
bool isnumber(string& token)
{
if(token=="+"||token=="-"||token=="*"||token=="/")
return false;
else
return true;
}
int evalRPN(vector<string>& tokens)
{
stack<int>st;
map<string,function<int(int,int)>> FuncMap;
//没找到符号就在map中插入
FuncMap["+"]=[](int a,int b)->int{return a+b;};
FuncMap["-"]=[](int a,int b)->int{return a-b;};
FuncMap["*"]=[](int a,int b)->int{return a*b;};
FuncMap["/"]=[](int a,int b)->int{return a/b;};
for(int i=0;i<tokens.size();i++){
//遇到操作数入栈
string& num=tokens[i];
if(FuncMap.find(num)==FuncMap.end())
{
st.push(stoi(num));
}
//遇到操作符开始运算
else
{
int right=st.top();
st.pop();
int left=st.top();
st.pop();
st.push(FuncMap[num](left,right));
}
}
return st.top();
}
注意:
- 使用 std::function 时会有一定的性能开销,因为它需要进行类型擦除和动态内存分配。在性能关键的代码中,应该仔细考虑是否真的需要 std::function的灵活性。
- std::function只能存储可以复制和移动的可调用目标。如果你有一个不能复制或移动的可调用目标(例如,一个 lambda 捕获了一个不能复制或移动的对象),那么你不能将它存储在 std::function中。
- 当使用std::function时,你应该始终指定完整的调用签名,包括返回类型和参数类型。这有助于确保你的代码是类型安全的。
bind
std::bind是 C++ 标准库 头文件中提供的一个工具,它用于将可调用对象(如函数、成员函数、函数对象或 lambda 表达式)与一些参数进行绑定,并生成一个新的可调用对象。这个新的可调用对象在调用时会将已经绑定的参数传递给原始的可调用对象,并可以接收额外的参数。
#include<functional>
#include<iostream>
void add(int x, int y)
{
cout << x + y << endl;
}
class A
{
public:
void sub(int x, int y)
{
cout << x - y << endl;
}
};
int main()
{
//1.绑定函数
// 使用 std::bind 绑定 add 的第一个参数为 2
auto bind_fun = std::bind(add, 2, std::placeholders::_1);
bind_fun(1); // 结果3
//2.bind类内成员函数
A a;
//第一个参数是成员函数,第二个参数类的实例,后续参数是要绑定的参数
auto bind_fun1 = std::bind(&A::sub, &a, std::placeholders::_2, std::placeholders::_1);
bind_fun1(3, 1); // -2
}
线程库
c++11提供了,以下是它的成员函数。
- get_id(): 获取线程的标识符。返回一个类型为 std::thread::id 的对象。
- joinable(): 检查线程是否可以被 join。如果线程已经执行完毕、被 join过或被 detach 过,那么它就不再是可 join的。
- join(): 阻塞当前线程,直到被 join的线程执行完毕。如果线程已经被join或detach过,或者不是由当前线程创建的,那么调用join()会抛出 std::system_error 异常。
- detach(): 将线程与 std::thread 对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。分离后的线程不能被join。
- native_handle(): 返回与 std::thread 具体实现相关的线程句柄。这通常用于与底层操作系统线程 API 进行交互。
基本使用
#include <iostream>
#include <thread>
#include <chrono> // 用于等待一段时间
// 这是线程将执行的函数
void thread_function() {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread function is running. Count: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 暂停一秒
}
}
int main() {
// 创建一个线程对象,并传递函数作为参数
std::thread t(thread_function);
// 主线程继续执行
std::cout << "Main thread is running.\n";
// 等待线程完成
t.join();
std::cout << "Thread has finished execution.\n";
return 0;
}
注意:
-
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。当创建一个线程对象后,如果没有提供线程函数,则该对象实际没有对应任何线程。
void fun() { std::thread t; // 默认构造的 thread 对象,不表示任何线程执行 }
-
当创建一个线程对象并且给定与线程关联的线程函数后,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:函数指针、lambda 表达式、函数对象。
void fun() { std::thread t([]{ cout << "hello world"} << endl;); }
-
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值。
-
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。如果是类成员函数作为线程参数时,必须将this作为线程函数参数
//1. void add(int& x) { x += 2; } int main() { int x = 5; thread t(add, x); t.join(); cout << x << endl; // x = 5 thread t2(add, std::ref(x)); t2.join(); cout << x << endl; // x = 7; } //2. #include <iostream> #include <thread> class MyClass { public: MyClass(int value) : value_(value) {} // 这是一个类的成员函数,我们想要在一个线程中运行它 void DoSomething() { // 访问类的成员变量 std::cout << "DoSomething called with value: " << value_ << std::endl; } // 这是一个静态成员函数,它可以作为线程函数,因为它不直接依赖于类的非静态成员 // 但是,它可以接受一个MyClass*参数来访问类的成员 static void ThreadFunction(MyClass* obj) { // 通过传入的obj指针访问类的成员 obj->DoSomething(); } private: int value_; }; int main() { MyClass obj(42); // 创建线程,将obj的this指针作为参数传递给ThreadFunction std::thread t(&MyClass::ThreadFunction, &obj); // 等待线程完成 t.join(); return 0; } /*在这个例子中,MyClass::ThreadFunction 是一个静态成员函数,它可以作为线程函数。当我们创建线程时,我们将&obj(即obj的this指针)作为参数传递给ThreadFunction。然后,在ThreadFunction内部,我们使用该指针来调用obj的DoSomething成员函数。 注意,虽然这个例子中的ThreadFunction是静态的,但它仍然可以访问类的非静态成员,只要它有一个指向类的实例的指针。这是因为在C++中,静态成员函数和非静态成员函数的主要区别在于它们是否隐式地接收一个指向类的实例的指针(即this指针)。静态成员函数不接收这个指针,因此它们不能直接访问类的非静态成员。但是,如果它们接收一个指向类的实例的指针作为参数,那么它们就可以像非静态成员函数一样访问类的成员了。*/
-
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
(1).采用无参构造函数构造的线程对象
(2).线程对象的状态已经转移给其他线程对象
(3).线程已经调用jion或者detach结束
线程安全
上面的代码会存在线程安全的问题,需要借助原子性的操作/互斥锁来保证线程的安全。
原子性操作库atomic
原子性操作库提供了一系列原子操作函数,用于安全地读取、写入、比较和交换数据。这些操作包括:
- load():读取原子变量的值。
- store():将值存储到原子变量中。
- exchange():将原子变量的当前值替换为新值,并返回旧值。
- compare_exchange_weak() 和 compare_exchange_strong():比较原子变量的当前值和期望值,如果相等则将原子变量更新为新值,并返回是否成功更新。
- fetch_add()
、
fetch_sub()、
fetch_and()、
fetch_or()、
fetch_xor()`:对原子变量执行相应的算术或位运算,并返回更新前的值。
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 原子整数变量,初始值为0
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 原子增加操作
}
}
int main() {
const int num_threads = 5; // 线程数量
std::vector<std::thread> threads(num_threads);
// 创建并启动线程
for (int i = 0; i < num_threads; ++i) {
threads[i] = std::thread(increment);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
// 输出最终结果
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
mutex
用于保护共享资源免受并发访问的破坏。当多个线程需要访问和修改共享数据时,使用std::mutex可以确保一次只有一个线程可以访问该数据,从而避免数据竞争和不一致。
在c++11中,一共有4个互斥量的种类。
-
std::mutex
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
lock() 上锁 unlock() 解锁 try_lock() 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 注意:线程函数调用lock()时,可能会发生以下三种情况:
(1).如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到(2).调用 unlock之前,该线程一直拥有该锁如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
(3).死锁(deadlock)问题:线程A持有互斥量M1,并尝试获取互斥量M2。同时,线程B持有互斥量M2,并尝试获取互斥量M1。此时,线程A在等待线程B释放M2,而线程B在等待线程A释放M1。这导致了一个循环等待,从而产生了死锁。 -
std::recursive_mutex
允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
-
std::timed_mutex
增加了一个能够等待一定时间尝试获取锁的机制。比std::mutex多了两个成员函数try_lock_for(),try_lock_until() 。
(1).try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。(2)try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
-
std::recursive_timed_mutex
(1).递归锁:与recursive_mutex 类似,recursive_timed_mutex 允许同一线程多次获取同一个互斥量,而不会产生死锁。每次获取锁时,都会增加锁的“递归计数”。只有当递归计数归零时,其他线程才能获取到这个互斥量。
(2).尝试锁与超时:与timed_mutex 类似,recursive_timed_mutex提供了两个成员函数来尝试获取锁,并带有超时功能
lock_guard
自动管理互斥量(mutex)的锁定和解锁,从而简化了对互斥量的使用,减少了由于忘记解锁互斥量而导致的问题。当 std::lock_guard对象被创建时,它会尝试锁定给定的互斥量;当 std::lock_guard 对象离开其作用域(例如函数返回或到达作用域末尾)时,它会自动解锁互斥量。
template<class _Mutex>
class lock_guard
{
public
explicit lock_guard(_Mutex& _mtx)
:_myMutex(_mtx)
{
_myMutex.lock();
}
//互斥量已经被当前线程锁定,不会再次锁定
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{
// 不调用 _MyMutex.lock();
}
~lock_guard()
{
_myMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _myMutex;
}
int main() {
Mutex mtx;
// 使用默认的 lock_guard 构造函数,自动锁定互斥量
{
lock_guard<Mutex> lg(mtx);
// 在这里,mtx 是锁定的
} // 离开作用域时,lg 析构,mtx 自动解锁
mtx.lock(); // 手动锁定互斥量
// 使用 adopt_lock 构造函数,不再自动锁定互斥量
{
lock_guard<Mutex> lg(mtx, adopt_lock_t());
// 在这里,mtx 仍然是锁定的,因为我们之前已经手动锁定了它
} // 离开作用域时,lg 析构,mtx 自动解锁
return 0;
}
unique_lock
lock_guard 最大是简单,没有给程序员提供足够的灵活度,因此,C++11 标准中定义了另外一个与 Mutex RAII 相关类 unique_lock,该类与 lock_guard 类相似,也很方便线程对互斥量上锁,但它提供了更好的上锁和解锁控制。
- 灵活性:与 std::lock_guard 相比,std::unique_lock 提供了更多的控制选项,如手动锁定和解锁、延迟锁定、尝试锁定等。
- 条件变量:std::unique_lock 常与 std::condition_variable 一起使用,以在特定条件满足时阻塞和唤醒线程。
- 异常安全性:与 std::lock_guard 一样,std::unique_lock 也提供了异常安全性,即在异常发生时会自动释放互斥量。
锁定和解锁操作
- lock():
尝试锁定互斥量。如果互斥量已经被另一个线程锁定,则当前线程将阻塞,直到互斥量可用。- try_lock():
尝试锁定互斥量,但不会阻塞。如果互斥量已经被锁定,则立即返回,不会等待。返回一个布尔值,指示是否成功获得了锁。- try_lock_for(chrono::duration<Rep, Period> rel_time):
尝试锁定互斥量,最多等待指定的相对时间。如果互斥量在指定时间内变得可用,则锁定它并返回true
;否则返回false
。- try_lock_until(chrono::time_point<Clock, Duration> abs_time):
尝试锁定互斥量,直到指定的绝对时间为止。如果互斥量在该时间点之前变得可用,则锁定它并返回true
;否则返回false
。- unlock():
解锁互斥量。如果当前线程没有锁定互斥量,则调用此函数是未定义的行为。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lk(mtx);
while (!ready) { // 如果条件不满足,则等待
cv.wait(lk); // 当前线程被阻塞,当条件变量被通知时,线程会醒来并重新检查条件
}
// ... 这里是临界区,互斥量 mtx 被锁定 ...
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lk(mtx);
ready = true; // 修改共享数据
lk.unlock(); // 手动解锁,以便等待的线程可以获取锁
cv.notify_all(); // 通知所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_id, i);
}
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto &th : threads) {
th.join();
}
return 0;
}
关于锁放到函数外面还是里面
- 代码范围:
- 内部:如果你只想保护函数内部的一部分代码,你应该在函数内部使用锁。
- 外部:如果你想要保护整个函数的执行,或者想要确保在函数执行期间某个资源不会被其他线程访问,你可能需要在函数外部(即调用函数之前)加锁,并在函数返回后解锁。但请注意,这种方法会限制函数的并发性,并且可能导致死锁,如果其他线程也在等待相同的锁。
- 异常安全:
- 如果在加锁后的代码块中抛出异常,并且你没有正确地解锁,那么锁可能会被意外地保持,导致死锁。为了避免这种情况,你可以使用
std::lock_guard
或std::unique_lock
这样的RAII(Resource Acquisition Is Initialization)封装器来自动管理锁的生命周期。 - 如果你在函数外部加锁,你需要确保在所有可能的退出路径(包括异常路径)上都正确地解锁。这可能会使代码更复杂。
- 如果在加锁后的代码块中抛出异常,并且你没有正确地解锁,那么锁可能会被意外地保持,导致死锁。为了避免这种情况,你可以使用
- 性能:
- 在函数外部加锁可能会减少并发性,因为锁在整个函数执行期间都会被保持。这可能会降低性能,特别是如果函数执行时间较长或经常被调用。
- 在函数内部加锁可以允许更细粒度的并发控制,从而提高性能。但是,这也增加了编写正确并发代码的难度。
- 可维护性:
- 将锁放在函数内部可以使代码更易于理解和维护,因为它清晰地表明了哪些代码是线程安全的,哪些不是。
- 如果锁在函数外部,其他开发人员可能不会立即意识到需要同步访问,这可能导致并发问题。
condition_variable
基本用法
- 创建:创建一个 std::condition_variable 对象。
- 等待:使用 wait
、
wait_for 或 wait_until 方法等待条件成立。这些方法都需要一个互斥量作为参数,并在等待期间锁定该互斥量。 - 通知:使用 notify_one 或 notify_all 方法通知一个或多个等待的线程条件已经成立。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cv;
// 生产者线程
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时操作
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // 通知一个消费者线程
}
}
// 消费者线程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
//pred返回false时,该函数才会阻塞,并且只有当pred变为true时,通知才能解除线程的阻塞
cv.wait(lock, [] { return !data_queue.empty(); }); // 等待队列非空
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumed: " << data << std::endl;
// 假设我们只消费一定数量的数据后退出
if (data == 9) break;
}
}
int main() {
std::thread producer_thread(producer);
std::thread consumer_thread(consumer);
producer_thread.join();
consumer_thread.join();
return 0;
}
支持两个线程交替打印,一个打印奇数,一个打印偶数
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;
//1.像之前一样加锁的话会出现什么问题?
// 会出现thread1获取锁后打印完值,thread2的还没运行到锁时间片用完了进入休眠排队等待状态
//thread1时间片充足继续获得了锁接着打印,出现了一个进程同时打印多次的问题
int main()
{
int N = 100;
int i = 0;
mutex mtx;
condition_variable cv; //block
bool flag = false;
//偶数先打印
thread thread1([N, &i, &mtx,&flag,&cv]{
while (i < N)
{
unique_lock<mutex> lock(mtx);
//void wait (unique_lock<mutex>& lck, Predicate pred);
//If pred is specified (2), the function only blocks if pred returns false
//and notifications can only unblock the thread when it becomes true
cv.wait(lock, [&flag](){return !flag;});
cout << this_thread::get_id() << "->" << i << endl;
++i;
//保证不会多次让thread1打印
flag = true;
cv.notify_one();//Unblocks
}
});
//奇数
thread thread2([N, &i, &mtx, &flag, &cv]{
while (i < N)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag]{return flag;});
cout << this_thread::get_id() << "->" << i << endl;
++i;
//保证不会多次让thread2打印
flag = false;
cv.notify_one();//Unblocks
}
});
thread1.join();
thread2.join();
return 0;
}
可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template<class ...Args> //Args模板参数包
void ShowList(Args... args) //args 函数形参参数包
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。语法不支持使用args[i]这样方式获取可变参数。
递归方式展开参数包
//递归终止函数
template<class T>
void ShowList(const T& t)
{
cout << t << endl;
}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, string("hello world"));
}
初始化列表和逗号运算符的结合来模拟展开
template<typename T>
void printargs(T t) //注意这不是递归终止函数!而是一个用来输出参数内容的函数
{
cout << t << endl;
};
template<class... Args>
void expand(Args... args)
{
cout << sizeof ... (args) << endl; // expand(1, "hello world", 12345); 结果是3
//方法1:数组的初始化列表
//int arr[] = {(printargs(args),0)...};//逗号表达式。包扩展为(printargs(args1),0)
//(printargs(args1),0),...,(printargs(argsN),0)
//计算每个逗号表达式,调用printargs()(在这里获得各个参数)
//同时,每个逗号表达式结果得0,然后用N个0初始化arr。
//方法2:利用std::initializer_list
std::initializer_list<int>{(printargs(args),0)...}; //比方法1简洁,且无需定义一个辅助的arr。
//方法3:利用lambda表达式
//[&args...]{std::initializer_list<int>{(cout << args << endl,0)...};}();
//[&]{std::initializer_list<int>{(cout << args << endl, 0)...}; }();
}
int main()
{
expand(1, "hello world", 12345);
expand(2, 3, 4, 5, 60, 1);
}
emplace_black vs push_back
push_back函数用于向 vector 的末尾添加一个元素。在调用 push_back时,你需要提供一个已经构造好的对象。如果这个对象是通过拷贝构造函数(对于非移动语义类型)或移动构造函数(对于支持移动语义的类型)来传递的,那么可能会产生额外的开销。
emplace_back函数则不同,它直接在emplace_back的末尾构造元素,通过接收构造元素所需的参数列表来做到这一点。这意味着它避免了先构造一个临时对象再将其移动到容器中的过程,从而可能提高性能。
template <class... Args>
void emplace_back (Args&&... args);
class MyClass {
public:
MyClass(const std::string& str) : data(str) {
std::cout << "MyClass constructor called with " << str << std::endl;
}
std::string data;
};
int main()
{
vector<MyClass> v;
MyClass mc("hello world"); //临时对象
v.push_back(mc);
v.emplace_back("hello world"); //直接在vector中构造对象
}
小语法
decltype:用于在编译时推断表达式的类型。
void fun()
{
int x = 10;
decltype(x) y = 10; // y的类型是int
int ref = &x;
decltype(ref) r = x; // r 是 x 的引用
vector<int> v {1, 2, 3};
decltype(v.begin()) it = v.begin();
}
新的类成员函数
C++11 新增了两个:移动构造函数和移动赋值运算符重载。针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
default:强制生成默认函数
class Student
{
Student() = default; //强制生成Student()默认构造函数
};
delete:禁止生成默认函数
class Student
{
Student(const Student& stu) = delete;
Student& operator=(const Student& stu) = delete;
};