C++11

1.介绍

2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

2.初始化

2.1 C++98中{}的初始化问题

在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如

int arr1[] = {1,2,3,4,5};
int arr2[5] = {0};

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v{1,2,3,4,5};

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

2.2 内置类型的列表初始化
int main()
{
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}

注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别

2.3 自定义类型的列表初始化
  1. 标准库支持单个对象的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0): _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Pointer p{ 1, 2 };
return 0;
}
  1. 多个对象的列表初始化
    多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()

3.变量类型推导

3.1 auto关键字

C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
使用细则
1.auto与指针和引用结合起来使用,用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。

int main()
{
int x = 10;
auto a = &x;
auto* b = &x;//a和 b没有区别,*可加可不加
auto& c = x; // 引用必须加&
   return 0;
}

2.在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其它变量。

int main()
	{
		auto a = 1, b = 2;
		auto c = 3, d = 4.0;//编译失败,类型不同,默认推导成int型
		return 0;
	}

auto不能推导的场景
1.auto不能作为函数的参数。
2.auto不能直接用来声明数组。
3.为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
4.auto最常见的优势用法就是跟C++11中提供的新式for循环,还有lambda表达式.等进行配合使用。
5.auto不能定义类的非静态成员变量。
6.实例化模板时不能使用auto作为模板参数。

3.2 decltype类型推导

auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}

如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)
C++98中确实已经支持RTTI:
typeid只能查看类型不能用其结果类定义类型
dynamic_cast只能应用于含有虚函数的继承体系中
运行时类型识别的缺陷是降低程序运行的效率

int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
return 0;
}

4 基于范围for的循环

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易出错。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号" : "分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

#include<iostream>
using namespace std;
void Test1()
{
	//普通遍历
	int array[] = { 1,2,3,4,5,6 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
		array[i] *= 2;

	for (int *p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
		cout << *p << endl;
}

void Test2()
{
	//基于范围的for循环
	int array[] = { 1,2,3,4,5,6 };
	for (auto& e : array) // 带&,可以改变数组内容
		e * 2;
	for (auto e : array)
		cout << e << " ";
}
	int main()
	{
		Test1();
		Test1();
		return 0;
	}

5. final与override

final
在一些情况下,我们并不希望某个成员函数再被任何派生类所继承,在Java语言中,有final来进行限定,C++11也提供了 final (同 override一样不是关键字,只是特殊的标识符)。通过使用final对虚函数的限定,任何子类不能重写该函数

class Base
{
public:
	Base(){}
	~Base(){}
public:
	virtual void func(int param) final
	 { cout << "Base::func. param = "  << param <<endl; }
};

class Derive : public Base
{
public:
	Derive(){}
	~Derive(){}
public:
	//error C3248: 'Base::func': function declared as 'final' cannot be overridden by 'Derive::func'
	virtual void func(int param) override { cout << "Derive::func. param = "  << param <<endl; }

};

基类的func函数使用Final进行限定后,派生类无法再重写改函数。
override
override不是一个关键字,而是一个用于标记虚函数重写的标识符,使用override 标记的类成员函数表示我们希望其重写基类相对应的虚函数。如果没有重写,编译器会报错

class Base
{
public:
	Base(){}
	~Base(){}
public:
	virtual void func(int param) { cout << "Base::func. param = "  << param <<endl; }
	virtual void func2() const { cout<< "Base::func2."<<endl;}
};

class DeriveClass : public BaseClass
{
public:
	DeriveClass(){}
	~DeriveClass(){}
public:
	//error C3668: 'Derive::func' : method with override specifier 'override' did not override any base class methods
	virtual void func(float param) override { cout << "Derive::func. param = "  << param <<endl; }

	//error C3668: 'Derive::func2' : method with override specifier 'override' did not override any base class methods
	virtual void func2() override { cout<< "Derive::func2."<<endl;}
};

在派生类的成员函数中使用override时,如果基类中无此函数,或基类中的函数并不是虚函数,编译器会给出相关错误信息。

C++11新增添的 override 和 final 说明符可以使得虚函数的继承更加明确和安全。遵循新的规则,可以增进代码的可读性,使用final可以更好的对派生和重写虚函数进行限制

6.智能指针

参考智能指针

7. 新增加容器—静态数组array、forward_list以及unordered系列

array

array就是数组,为什么会出现这样一个容器呢,不是有vector和传统数组吗?那你有没有某些时候抱怨过vector速度太慢。array 保存在栈内存中,相比堆内存中的vector,我们就能够灵活的访问元素,获得更高的性能;同时真是由于其堆内存存储的特性,有些时候我们还需要自己负责释放这些资源。
array就是介于传统数组和vector两者之间的容器,封装了一些函数,比传统数组方便,但是又没必要使用vector;
array 会在编译时创建一个固定大小的数组array 不能够被隐式的转换成指针,定义时需要指定类型和大小。支持快速随机访问。不能添加或删除元素。

array<int, 3> a = {1,2,3};
array<int, 3> b(a);
sort(a.begin(),a.end());
lower_bound(a.begin(),a.end(),2);
forward_list

forward_list 是一个列表容器,使用方法和 list 基本类似。但forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问,也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 list 更高的空间利用率。

unordered_map,unordered_set,unordered_multimap,unordered_multiset

加了个unordered前缀,也是把Hash正式带入了STL中,内部没有红黑树,无法自动排序,只是用Hash建立了映射,其他用法相同,当只需要映射而不要排序时候,用这个会快很多。
区别:Map/Set:红黑树,迭代器遍历时有序,操作的平均时间复杂度:O(logN)
unordered_map/unordered_set:哈希表,迭代器遍历时无序,操作的平均时间复杂度:O(1)

8.委派构造函数

8.1 构造函数冗余造成重复

委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更加容易

class Info {
public:
Info(): _type(0), _name('a')
{ InitRSet();}
Info(int type): _type(type), _name('a')
{ InitRSet();}
Info(char a): _type(0), _name(a)
{ InitRSet();}
private:
void InitRSet() {//初始化其他变量}
private:
int _type;
char _name;
//...
};

上述构造函数除了初始化列表不同之外,其他部分都是类似的,代码重复。
初始化列表可以通过:类内部成员初始化进行优化,但是构造函数体的重复在C++98中无法解决。
能否将:构造函数体中重复的代码提出来,作为一个基础版本,在其他构造函数中调用呢?

8.2 委派构造函数

所谓委派构造函数:就是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式

class Info{
public:
// 目标构造函数
Info(): _type(0), _a('a')
{ InitRSet();}
// 委派构造函数
Info(int type): Info()
{ _type = type;}
// 委派构造函数
Info(char a): Info()
{ _a = a;}
private:
void InitRSet(){ //初始化其他变量 }
private:
int _type = 0;
char _a = 'a';
//...
};

在初始化列表中调用”基准版本”的构造函数称为委派构造函数,而被调用的”基准版本”则称为目标构造函
数。
注意:构造函数不能同时”委派”和使用初始化列表

9. 默认函数控制

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

9.1 显式缺省函数

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

class A
{
public:
A(int a): _a(a)c
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
9.2 删除默认函数

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

class A
{
public:
A(int a): _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
//A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0;
}

注意:避免删除函数和explicit一起使

10. 右值引用

10.1 移动语义

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将
会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错

//String 
class String
{
public:
   String(char* str="")
   {
   if(NULL==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 (const String&s)
               :_str(nullptr)
        {
           String strTmp(s._str);
           swap(_str,strTmp);
        }
        */
        String& operator=(const String& s)
        {
        if(this != &s)
        {
         char pTemp = new char[strlen(s._str)+1];
         strcpy(pTemp,s._str);
         delete[] _str;
         _str = pTemp;
         }
         return *this;
        }
        /*
        String& operator=(String s)
        {
        swap(_str,s._str);
        return *this;
        }
        */
        ~String()
        {
        if(_str) delete[]_str;
        }
       private:
       char *_str;
}

假设现在有一个函数,返回值为一个String类型的对象

String GetString(char* pStr)
{
String strTemp(pStr)
return strTemp;
} 
int main()
{
 String s1("hello");
 String s2(GetString("world"));
 return0
}

在这里插入图片描述
GetString函数返回的临时对象,将s2拷贝构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;而s2在拷贝构造时,又需要分配空间,一个刚释放一个又申请,有点多此一举。那能否将GetString返回的临时对象的空间直接交给s2呢?这样s2也不需要重新开辟空间了,代码的效率会明显提高
在这里插入图片描述
将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用

String(String&& s): _str(s._str)
{ s._str = nullptr; }
10.2 C++右值

右值:临时变量,常量,将亡值
使用:
类型&& 引用变量 = 实体
右值引用最常见的一个使用地方:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率

String &&GetString(char* pStr)
{
  String strTemp(pStr);
  return strTemp;
}
int  main()
{
String s1("hello");
String s2(GetString("world"));
  return 0;
}

右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的声明周期

String GetString(char* pStr)
{
return String(pStr);
}
int main()
{
String && s= GetString("hello");
return 0;
}

注意

  1. 与引用一样,右值引用在定义时必须初始化。
  2. 通常情况下,右值引用不能引用左值
int main()
{
int a = 10;
//int&& ra; //编译失败,没有进行初始化
//int&& ra = a;//编译失败,a是一个左值

//ra是匿名常量10的别名
const int&& ra = 10;
return 0;
}

10.3 std::move()

C++11中,std::move()函数位于 头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁

// 移动构造函数
class String
{
//....
String(String&& s): _str(s._str)
{ s._str = nullptr; }
// ....
};
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}

注意:上述代码是move()误用的一个非常典型的例子,move更多的是用在声明周期即将结束的对象上

class Person
{
public:
Person(char* name, char* sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
Person(const Person& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
#if 0
Person(Person&& p)
: _name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
#else
Person(Person&& p)
: _name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
#endif
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p;
}
int main()
{
Person p(GetTempPerson());
return 0;
}

注意:为了保证移动语义的传递,程序员在编写移动构造函数时,最好使用std::move转移拥有资源的 成员为右值

问题

  1. 如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现
String(const String&&);
const Person GetTempPerson();
  1. 在C++11中,无参构造函数/拷贝构造函数/移动构造函数实际上有3个版本
Object()
Object(const T&)
Object(T &&)

默认成员函数
默认情况下,编译器会为程序员隐式生成一个(如果没有用到则不会生成)移动构造函数。如果程序员声明了自定义的构造函数、移动构造、拷贝构造函数、赋值运算符重载、移动赋值、析构函数,编译器都不会再为程序员生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是按照位拷贝(即浅拷贝)来进行的。因此,在类中涉及到资源管理时,程序员最好自己定义移动构造函数。其他类有无移动构造都无关紧要。注意:在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义

10.4 完美转发

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

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

11 lambda表达式

11.1 书写规范

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函数不能做任何事情

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将其赋值给一个变量

11.2捕获列表

[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表达式之间不能相互赋值,即使看起来类型相同

void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
11.3 函数对象与lambda表达式

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

class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;c
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// 仿函数
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
在这里插入图片描述
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

12 线程库

12.1 线程库介绍

C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件,该头文件声明了std::thread 线程类

#include <iostream>
using namespace std;
#include <thread>
void fun()
{
cout << "A new thread!" << endl;
}
int main()
{
thread t(fun);
t.join();
cout << "Main thread!" << endl;
return 0;c
}
12.2 线程的启动

C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递_beginthreadex() 创建线程函数来实现 (注意:_beginthreadex是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针

// 使用lambda表达式作为线程函数创建线程
int main()
{
int n1 = 500;
int n2 = 600;
thread t([&](int addNum){
n1 += addNum;
n2 += addNum;
}, 500);
t.join();
std::cout << n1 << ' ' << n2 << std::endl;
return 0;
}
12.3 线程的结束

join():加入式。会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用的join()之后joinable()就
将返回false了

#include <iostream>
using namespace std;
#include <thread>
void foo()
{
this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
thread t(foo);
cout << "before join, joinable=" << t.joinable() << std::endl;
t.join();
cout << "after join, joinable=" << t.joinable()<< endl;
return 0;
}

detach:分离式。会从调用线程中分理出新的线程,之后不能再与新线程交互。就像是你和你女朋友分手,那之后你们就不会再有联系(交互)了,而她的之后消费的各种资源也就不需要你去埋单了(清理资源)。此时调joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收

12.4 原子操作

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

#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中传统的解决方式:可以对共享修改的数据可以加锁保护。

#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,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。

#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, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值