当前位置: 首页 > article >正文

C++笔记21•C++11的新特性•

      相比于 C++98/03C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

1.统一的列表初始化

1.1 {}初始化

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 int array1[]{ 1, 2, 3, 4, 5 };
 int array2[5]{ 0 };
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
 return 0;
}
int main()
{
 //Date是一个日期类
 Date d1(2022, 1, 1); // 旧格式
 //C++11支持的列表初始化,这里会调用构造函数初始化
 Date d2{ 2022, 1, 2 };//新格式
 Date d3 = { 2022, 1, 3 };
 return 0;
}

2.关键字声明

2.1 auto:

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将 其用于实现自动类型推断。

int main()
{
int i = 10;
auto p = &i;// auto可以自动推断出p是一个指针

map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
map<string, string>::iterator it = dict.begin();
auto it = dict.begin();// auto可以自动推断出it是一个迭代器

return 0;
}

2.2decltype

关键字decltype将变量的类型声明为表达式指定的类型。

//关键字decltype 将变量的类型 声明为 表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret;   // decltype可以推断出ret的类型是double
decltype(&x) p;      // decltype可以推断 p的类型是int*
cout << typeid(ret).name() << endl;  //typeid(ret).name()可以打印出ret的类型名字
cout << typeid(p).name() << endl;  //typeid(p).name()可以打印出p的类型名字

return 0;
}

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

 3.右值引用和左值引用

       C++11中新增了的右值引用语法特性,所以从现在开始之前学习的引用就叫做左值引用无论左值引用还是右值引用,都是给对象取别名

3.1左值和左值引用概念

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(1);
int b = 1;
const int c = 2;

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;  

//特别注意:左值可以出现在赋值符号的右边  但是右值不能出现出现在赋值符号的左边

return 0;
}

3.2右值和右值引用概念

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{
double x = 1.1, y = 2.2;

// 以下几个都是常见的右值
10;  //字面常量
x + y;//表达式的返回值
fmin(x, y);//函数的返回值

// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

return 0;
}


//左值引用:单 &
//右值引用:双 &&

3.3左值引用和右值引用的比较

左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值

    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}




右值引用总结:
1. 右值引用只能引用右值,不能引用左值。
2. 但是右值引用可以move以后的左值。

int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;//编译不通过

 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);//编译通过
 return 0;
}

3.4上面看似右值引用没有太大用处,但其实也有它的用武之地,右值引用的使用场景

如果s被传入的是右值,如果既有拷贝构造又有移动构造,优先使用移动构造这样可以减少拷贝构造的次数,增加效率。

3.5 左值引用的使用场景

做参数和做返回值都可以提高效率

void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
 bit::string s1("hello world");
 // func1和func2的调用我们可以看到"左值引用做参数减少了拷贝",提高效率的使用场景和价值
 func1(s1);
 func2(s1);

 //"左值引用做返回值减少了拷贝"
 // string operator+=(char ch) 传值返回存在深拷贝,降低效率
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率

 s1 += '!';
 return 0;
}

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

右值引用和移动语义解决上述问题:

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

总结:

右值引用做参数和做返回值减少拷贝的本质是利用了移动构造和移动赋值
左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方,他们两个是相辅相成的

左值引用:解决的是传参过程中和返回值过程中的拷贝
做参数: void push(T x) -> void push(const T& x)解决的是传参过程中减少拷贝
做返回值:T f() -> T& f()解决的返回值过程中的拷贝
Ps :但是要注意这里有限制,如果返回对象出了作用域不在了就不能用传引用,这个左值引用无法解决,等待C++11右值引用解决

右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题+传值返回接收返回值的拷贝
做参数:void push(T&& x)解决的push内部不再使用拷贝构造x到容器空间上,而是移动构造过去
做返回值:T f();解决的外面调用接收f()返回对象的拷贝,T ret = f(),这里就是右值引用的移动构造,减少了拷贝
Ps:右值引用实际上不建议写成T&& f() 和 T&& ret = f(),写成 T&& f() 和 T&& ret = f() 会带来潜在的风险,正确且安全的做法是返回一个局部对象(T f()),这样编译器能够自动处理移动语义或返回值优化,避免不必要的拷贝。
·使用 T f() 的方式返回对象,配合右值引用和移动构造函数,已经可以避免拷贝。编译器会自动选择移动构造 
 函数来高效地传递对象,因此没有必要显式返回右值引用。
·如果你写成 T&& ret = f();,实际上是将右值引用绑定到临时对象,这可能导致不安全的代码。

3.6移动赋值

不仅仅有移动构造,还有移动赋值:

// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
 xcn::string ret1;
 ret1 = xcn::to_string(1234);
 return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义(移动构造)
// string& operator=(string&& s) -- 移动语义(移动赋值)

        这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。xcn::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为xcn::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

STL中的容器都是增加了移动构造和移动赋值:例string

4.C++11增加新的类功能

 4.1.默认成员函数

   原来C++类中,有6个默认成员函数:

  (1). 构造函数

  (2). 析构函数

  (3). 拷贝构造函数

  (4). 拷贝赋值重载

  (5). 取地址重载

  (6). const 取地址重载

重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载

4.2.强制生成默认函数的关键字default

       C++11可以让你更好的控制要使用 的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 :_name(p._name)
 ,_age(p._age)
 {}

 Person(Person&& p) = default;

private:
 xcn::string _name;
 int _age;
};
     

4.3.禁止生成默认函数的关键字delete:

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

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
 xcn::string _name;
 int _age;
};
4.4.继承和多态中的 final override 关键字
C++11 override 和 final 检查重写的关键字
// final:修饰虚函数,表示该虚函数不能再被重写
// override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

详细见:C++笔记13•面向对象之多态•_c++ 面相对象 多态体现-CSDN博客

4.5.可变参数模板

       C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值

4.5.1递归函数方式展开参数包

//1.递归函数方式展开参数包
// 递归终止函数
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, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

4.5.2逗号表达式展开参数包

      这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行

printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组 int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

//2.逗号表达式展开参数包
template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	
	//int arr[] = {PrintArg(args)};//编译错误
	//PrintArg(args);//编译错误

    int arr[] = { (PrintArg(args), 0)... };//编译正确
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

注:STL容器中的empalce相关接口函数

//emplace_back可变参数模板
//template <class... Args>
//void emplace_back (Args&&... args);

int main()
{
 std::list< std::pair<int, bit::string> > mylist;

 mylist.emplace_back(10, "sort");
 mylist.emplace_back(make_pair(20, "sort"));

 mylist.push_back(make_pair(30, "sort"));
 mylist.push_back({ 40, "sort"});

//发现emplace_back是直接构造了,push_back是先构造,再移动构造,其实差别也不大。
 return 0;

}

5. lambda表达式

5.1 lambda表达式的由来

struct Goods
{
 string _name;  // 名字
 double _price; // 价格
 int _evaluate; // 评价
 Goods(const char* str, double price, int evaluate)
 :_name(str)
 , _price(price)
 , _evaluate(evaluate)
 {}
};
struct ComparePriceLess
{
 bool operator()(const Goods& gl, const Goods& gr)
 {
 return gl._price < gr._price;
 }
};
struct ComparePriceGreater
{
 bool operator()(const Goods& gl, const Goods& gr)
 {
 return gl._price > gr._price;
 }
};
int main()
{
 vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "桔子", 2.2, 3 }, { "脆梨",1.5, 4 } };
 sort(v.begin(), v.end(), ComparePriceLess());//升序
 sort(v.begin(), v.end(), ComparePriceGreater());//降序
}

     随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。将上述代码转化为以下Lambda表达式写法:

int main()
{
 vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "桔子", 2.2, 3 }, { "脆梨",1.5, 4 } };

 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._price < g2._price; });

 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._price > g2._price; });

 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._evaluate < g2._evaluate; });

 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._evaluate > g2._evaluate; });
}

5.2 lambda表达式的格式

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement

}

lambda表达式书写格式:[捕捉列表] (参数) mutable -> 返回类型 {函数体}

5.2.1. lambda表达式各部分说明

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来

判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda

函数使用。注意捕捉列表可为空 [ ].

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以

连同()一起省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量

性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回

值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推

导。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获

到的变量。

注意:

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为

。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

int main()
{
    // 最简单的lambda表达式, 该lambda表达式没有任何意义
   []{}; 
    
    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
   [=]{return a + 3; }; 
    
    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c){b = a + c; }; 
    fun1(10)
    cout<<a<<" "<<b<<endl;
    
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
    cout<<fun2(10)<<endl;
    
    // 复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; }; 
    cout << add_x(10) << endl; 
    return 0;
}

//通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,
//该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

       通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调

用,如果想要直接调用,可借助auto将其赋值给一个变量。

5.2.2. 捕获列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

特别注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
   比如: [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
          [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
   比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同

5.2.3 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, 2);
 // lamber表达式
 auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
 r2(10000, 2);
 return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

其实lambda表达式的底层就是(函数对象)仿函数实现的,定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

6.线程库

6.1thread类的简单介绍

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

 注意:

1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的 状态
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

3.当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。

线程函数一般情况下可按照以下三种方式提供:
                                      函数指针
                                      lambda表达式
                                      函数对象
#include <iostream>
using namespace std;
#include <thread>
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;
}
4. thread 类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不移向线程的执行。
5. 可以通过 joinable() 函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
       (1)采用无参构造函数构造的线程对象
       (2)线程对象的状态已经转移给其他线程对象
       (3)线程已经调用join或者detach结束
6. 线程函数参数
       线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的 ,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <memory>

#include <thread>
using namespace std;

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

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

int main()
{
    int a = 10;

    // 此时,传递的是 a 的拷贝,不会影响外部 a 的值
    //thread t1(ThreadFunc1, a);
    //t1.join();
    //cout << "After t1 (no ref): " << a << endl;  //目前编译器编译不过 因为要传递a的引用或地址

     //使用 std::ref() 传递引用,这样就可以修改外部 a 的值
    thread t2(ThreadFunc1, std::ref(a));
    //t2.join();//位置在这,正常编译,After t2 (with ref) a的值会发生改变
    cout << "After t2 (with ref): " << a << endl;

    // 通过指针传递,修改原始值
    thread t3(ThreadFunc2, &a);
    t3.join();
    cout << "After t3 (with pointer): " << a << endl;
   
    //t2.join();//位置在这,正常编译,After t2 (with ref) a的值不会发生改变
    //若删除t2.join()(不编写),程序运行崩溃,After t2 (with ref) a的值不会发生改变
    return 0;
}

//改变  t2.join();的位置After t1 (with ref): 10  没报错, 删除t2.join();After t1 (with ref): 10 但是程序崩溃
//解释说明:
// 在多线程编程中,join() 的位置非常重要,它确保主线程等待子线程执行完毕。如果你移动或删除 t2.join(),就会出现以下问题:

//(1)如果不调用 t2.join(): 删除 t2.join() 会导致主线程继续执行,而不等待线程 t2 完成。由于线程 t2 在后台修改变量 a,主线程可能在 t2 完成之前访问或输出 a 的值。这就可能导致打印出 a 的旧值(如输出 10 而不是 20)。更糟糕的是,主线程可能会在子线程 t2 还未完成时结束程序,这可能导致未定义行为或程序崩溃。
//(2)join() 调用过晚: 如果你把 t2.join() 移动到代码末尾,主线程在 t2 执行完毕前可能已经开始执行接下来的代码了。因此,在你调用 cout << "After t2 (with ref): " << a << endl; 时,线程 t2 可能尚未修改变量 a,导致打印 a 的旧值 10,而不是预期的 20。
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。
6.2线程安全( 原子性操作库 (atomic) )
多线程最主要的问题是共享数据带来的问题 ( 即线程安全 ) 。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦
#include <iostream>
using namespace std;
#include <thread>
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中传统的解决方式:可以对共享修改的数据可以加锁保护。
//虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
//线程安全
//#include <iostream>
//using namespace std;
//#include <thread>
//unsigned long sum = 0L;
0L是初始化,给 sum 赋初值 长整型 数值 0。 0L 是 长整型数值 0,与声明 int 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, 10000);
//	thread t2(fun, 10000);//此时看似没问题  当10000改为10000000 就有问题了  正常结果应该是20000000  但是结果不对
//	t1.join();
//	t2.join();
//	cout << "After joining,sum = " << sum << std::endl;
//	return 0;
//}


//两种解决方案:c++98 加锁解决   c++11引入原子操作解决

//c++98 加锁解决
//#include <iostream>
//using namespace std;
//#include <thread>
//#include <mutex>
//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 <atomic>
#include <iostream>
using namespace std;
#include <thread>
#include <atomic>

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, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();

	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}
C++11 中, 程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的 访问 。 更为普遍的,程序员可以使用 atomic 类模板,定义出需要的任意原子类。
atmoic<T> t;    // 声明一个类型为T的原子类型变量t


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

#include <atomic>
int main()
{
 atomic<int> a1(0);
 //atomic<int> a2(a1);   // 编译失败
 atomic<int> a2(0);
 //a2 = a1;               // 编译失败
 return 0;
}
6.3lock_guard unique_lock
       在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高 效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
//比如:一个线程对变量number进行加一100次,另外一个减一100次,每次操作加一或者减一之
后,输出number的结果,要求:number最后的值为1

#include <thread>
#include <mutex>
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

说明:

(1)lock_guard 类模板主要是通过 RAII 的方式,对其管理的互斥量进行了封
,在需要加锁的地方,只需要用上述介绍的 任意互斥体实例化一个 lock_guard ,调用构造函数 成功上锁,出作用域前, lock_guard 对象要被销毁,调用析构函数自动解锁,可以有效避免死锁 问题
lock_guard 的缺陷:太单一,用户没有办法对该锁进行控制,因此 C++11 又提供了unique_lock。
(2) 与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 所管理的互斥量的指针 )
 
补充:mutex的种类
(1)std::mutex :
lock() 上锁:锁住互斥量
unlock() 解锁:释放对互斥量的所有权
try_lock() :尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞
(2)std::recursive_mutex:
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
std::recursive_mutex 的特性和 std::mutex 大致相同。
(3)std::timed_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
(4)std::recursive_timed_mutex

http://www.kler.cn/a/311559.html

相关文章:

  • go T 泛型
  • 《硬件架构的艺术》笔记(一):亚稳态
  • 分布式和并发模型的比较和讨论记录
  • Python数据分析NumPy和pandas(二十九、其他Python可视化工具)
  • 重构代码之移动字段
  • 测试实项中的偶必现难测bug--<pre>标签问题
  • Springboot请求响应案例
  • Ruoyi Cloud K8s 部署
  • Golang | Leetcode Golang题解之第415题字符串相加
  • MySQL:索引02——使用索引
  • kafka 超详细的消息订阅与消息消费几种方式
  • 【运维】自定义exporter
  • Redis——笔记01
  • 【PyQt5】object属性
  • Java中的异步编程模式:CompletableFuture与Reactive Programming的实战
  • 性格类型识别系统源码分享
  • DTD 实体
  • 【HTTP】HTTP报文格式和抓包
  • C++初阶:STL详解(五)——vector的模拟实现
  • 【JOIN 详解】SQL连接全面解析:从基础到实战
  • PostgreSQL主从切换测试
  • 使用BGP及静态路由方式实现链路冗余和ByPass
  • C:字符串函数(完)-学习笔记
  • 北斗盒子TD20——水上作业的安全防线,落水报警守护生命
  • React 中的延迟加载
  • 音视频入门基础:AAC专题(10)——FFmpeg源码中计算AAC裸流每个packet的pts、dts、pts_time、dts_time的实现