列表初始化
在C++98中,我們可以使用花括號對數組元素進行統一的列表初始值設定,例如:
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 0 };
對於一些自定義類型,無法使用花括號進行初始化,例如:
vector<int> v{ 1, 2, 3, 4 };
無法通過編譯,導致每次定義vector時,都需要先把vector定義出來,然後使用循環對其賦初始值,非常不方便。C++11擴大了用花括號括起的列表(初始化列表)的使用範圍,使其可用於所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。
- 內置類型的列表初始化
//內置類型列表初始化
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<string, string> m{ { "one", "1" }, { "two", "2" } };
- 自定義類型列表初始化
1、標準庫支持單個對象的列表初始化
例如:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d2{ 2019, 11, 22 };
return 0;
}
2、多個對象的列表初始化
多個對象想要支持列表初始化,需給該類(模板類)添加一個帶有initializer_list類型參數的構造函數即可。注意:initializer_list是系統自定義的類模板,該類模板中主要有三個方法:begin()、end()迭代器以及獲取區間中元素個數的方法size();
例如:
#include <initializer_list>
namespace Daisy
{
template<class T>
class vector
{
public:
typedef T* iterator;
public:
vector()
{
_start = _finish = endofstorage;
}
vector(const initializer_list<T>& il)
:_start(new T[il.size()])//開闢空間
{
auto it = il.begin();
_finish = _start;
while (it != il.end())
{
*_finish++ = *it;//放元素
++it;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
iterator _start;
iterator _finish;
iterator endofstorage;
};
}
int main()
{
Daisy::vector<int> v1;
Daisy::vector<int> v2{ 1, 2, 3, 4, 5 };
//範圍for是爲了用戶使用方便
//但是編譯器最終會將範圍for轉換爲迭代器形式,加上迭代器的操作
for (auto e : v2)
cout << e << " ";
cout << endl;
return 0;
}
變量類型推導
- 爲什麼需要類型推導
例如:
#include <string>
int main()
{
short a = 32670;
short b = 32670;
short c = a + b;
map<string, string> m{ { "apple", "蘋果" }, { "banana", "香蕉" } };
map<string, string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second;
++it;
}
cout << endl;
return 0;
}
其中c如果給成short,會造成數據丟失,如果能夠讓編譯器根據a+b的結果推導c的實際類型,就不會存在問題;使用迭代器遍歷容器, 迭代器類型太繁瑣,可以使用auto關鍵字,C++11中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,可以給程序的書寫提供許多方便。將程序中c與it的類型換成auto,程序可以通過編譯,而且更加簡潔;但是在有些情況下,是不能使用auto的,例如:
template < class T1, class T2>
auto Add(const T1& left, const T2& right)//不知道該返回T1還是T2
{
return left + right;
}
//使用auto報錯,因爲編譯器在編譯期間會進行替換,但是此時不知道替換成什麼類型
int main()
{
return 0;
}
此時我們就不知道該返回什麼類型,因爲auto使用的前提是:必須要對auto聲明的類型進行初始化,否則編譯器無法推導出auto的實際類型。這時候需要根據表達式運行完成之後結果的類型進行推導,因爲編譯期間,代碼不會運行,此時auto也就無能爲力。
因此我們需要 decltype
- decltype
根據表達式的實際類型推演出定義變量時所用的類型,例如:
1、推演表達式類型作爲變量的定義類型
int main()
{
short a = 32760;
short b = 32760;
decltype(a + b)c;
cout << typeid(c).name()<< endl;
return 0;
}
2、推演函數返回值的類型
void TestFunc(int)
{}
void(*set_malloc_handler(void(*f)()))()
{
return nullptr;
}
typedef decltype(set_malloc_handler) SH;
int main()
{
//沒有帶參數,推演函數類型
cout << typeid(SH).name() << endl;
//帶參數,推演函數調用類型
cout << typeid(decltype(set_malloc_handler(nullptr))).name() << endl;
return 0;
}
結果是:
可以看出函數調用類型是一個函數指針
- 返回值類型追蹤
例如:
template <class T1,class T2>
auto Add(const T1& left, const T2& right)->decltype(left + right)
{
return left + right;
}
int main()
{
cout << typeid(Add(1, 2.0)).name() << endl;
return 0;
}
由於在編譯期間,left和right的類型不知道,因此將類型推演放在後面,使用->來進行指向,然後函數返回值是auto(語法規定);
默認函數控制
在C++中對於空類編譯器會生成一些默認的成員函數,比如:構造函數、拷貝構造函數、運算符重載、析構函數和&和const&的重載、移動構造、移動拷貝構造等函數。如果在類中顯式定義了,編譯器將不會重新生成默認版本。有時候這樣的規則可能被忘記,最常見的是聲明瞭帶參數的構造函數,必要時則需要定義不帶參數的版本以實例化無參的對象。而且有時編譯器會生成,有時又不生成,容易造成混亂,於是C++11讓程序員可以控制是否需要編譯器生成。
- 顯式缺省函數
在C++11中,可以在默認函數定義或者聲明時加上=default,從而顯式的指示編譯器生成該函數的默認版本,用=default修飾的函數稱爲顯式缺省函數。
例如:
class Date
{
public:
Date() = default;
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
return 0;
}
- 刪除默認函數
如果能想要限制某些默認函數的生成,在C++98中,是該函數設置成private,並且不給定義,這樣只要其他人想要調用就會報錯。在C++11中更簡單,只需在該函數聲明加上=delete即可,該語法指示編譯器不生成對應函數的默認版本,稱=delete修飾的函數爲刪除函數。
總結:什麼情況下編譯器一定會生成默認的構造函數
(1)如果類中定義了其他類類型對象,一定生成
(2)在繼承體系中,如果基類定義了無參的構造函數,派生類沒有定義任何構造函數,編譯器會給派生類生成一個無參的構造函數(作用:要在生成的無參構造函數初始化列表位置調用基類構造函數)
(3)如果類中定義了虛函數,如果沒有顯式定義任何構造函數,由於要在構造函數中放虛表指針,要生成構造函數;
(4)虛擬繼承中有虛基表指針,沒有顯式定義構造函數,編譯器也要生成默認構造函數;
源代碼(github):
https://github.com/wangbiy/C-3/tree/master/test_2019_11_22_1/test_2019_11_22_1
lambda表達式
我們在C++98中,如果要對內置類型進行排序,可以使用std::sort方法(它包含在#include < algorithm >頭文件中,它有3個參數,start/end/排序方法,第一個是排序數組的起始地址,第二個是要結束的地址,第三個是排序的方法,可以是升序也可以是降序),例如:
using namespace std;
#include <algorithm>
#include <functional>
int main()
{
int arr[] = { 8, 7, 9, 0, 5, 6, 3, 1, 2 };
sort(arr, arr + sizeof(arr) / sizeof(arr[0]));
sort(arr, arr + sizeof(arr) / sizeof(arr[0]),greater<int>());
return 0;
}
但是如果要排序類型是自定義類型,需要用戶自己定義排序時的排序規則,例如:
struct Goods
{
string _name;
double _price;
};
struct com
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price <= g2._price;
}
};
int main()
{
Goods gds[] = { { "蘋果", 2.2 }, { "香蕉", 1.5 }, { "橘子", 5 } };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), com());//按照價格的升序排列
return 0;
}
這個是使用仿函數的方法實現,也可以使用函數指針的方式;
struct Goods
{
string _name;
double _price;
};
bool Compare(const Goods& g1, const Goods& g2)
{
return g1._price <= g2._price;
}
int main()
{
Goods gds[] = { { "蘋果", 2.2 }, { "香蕉", 1.5 }, { "橘子", 5 } };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare);//按照價格的升序排列,函數指針
return 0;
}
當我們要實現多個類,如果每次比較的邏輯不一樣,還要實現多個類,特別是相同類的命名,這樣都會很麻煩,因此在C++11中我們出現了lamdba表達式;那麼上述就可以實現爲:
int main()
{
Goods gds[] = { { "蘋果", 2.2 }, { "香蕉", 1.5 }, { "橘子", 5 } };
sort(gds,gds+sizeof(gds)/sizeof(gds[0]),
[](const Goods& g1, const Goods& g2)->bool
{
return g1._price <= g2._price;
});
}
實際上lambda表達式是一個匿名函數;
- lambda表達式語法
lambda表達式書寫格式:[capture-list] (parameters) mutable -> return-type { statement }
[]表示捕獲列表,出現在lambda表達式的開始位置,編譯器根據[]來判斷接下來的代碼是否爲lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda函數使用。
()表示參數列表,若不需要參數傳遞,可以省略;
mutable表示默認情況下,lambda函數是const函數,mutable可以取消其常量性;
->return-type表示函數返回值類型;
{statement}表示函數體;
最簡單的lambda表達式是[]{},這個lambda表達式是沒有意義的,lambda表達式可以相當於一個無名函數,因此想要調用,藉助auto關鍵字將其賦值給一個變量; - 捕獲列表說明
例如:
int main()
{
int a = 10;
int b = 20;
int c = 0;
cout << &c << endl;
auto Add = [c](const int left, const int right)mutable
{
cout << &c << endl;
c=left + right;
};
Add(1, 2);
return 0;
}
可以發現捕獲列表按照值傳遞的方式捕獲變量c,此時可以在函數體中使用c變量,但是在調用了lambda表達式之後,c並沒有改變,如圖我們可以打印出捕獲前後c的值和地址:
發現c的值並沒有變成預想的3,地址也不一樣;
如果我們按照值傳遞的方式捕獲父作用域中所有的變量,例如:
int main()
{
int a = 10;
int b = 20;
int c = 0;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
auto Add = [=](const int left, const int right)mutable
{
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
c=left + right;
};
Add(1, 2);
return 0;
}
可以發現捕獲前後地址不同;
如果我們按照引用傳遞捕捉變量,例如:
int main()
{
int a = 10;
int b = 20;
int c = 0;
cout << &c << endl;
auto Add = [&c](const int left, const int right)mutable
{
cout << &c << endl;
c=left + right;
};
Add(1, 2);
return 0;
}
此時我們得到的結果就是:
發現c的值發生了變化,捕獲前後的地址相同;
如果按照引用傳遞捕捉父作用域中所有的變量,此時:
int main()
{
int a = 10;
int b = 20;
int c = 0;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
auto Add = [&](const int left, const int right)mutable
{
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
c=left + right;
};
Add(1, 2);
return 0;
}
它的結果就是:
這時我們發現捕獲前後所有變量的地址相同;捕捉列表不允許變量重複傳遞,否則就會導致編譯錯誤。 比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重複;
注意:父作用域指lambda函數的語句塊;語法上捕捉列表可由多個捕捉項組成,並以逗號分隔,例如:
int main()
{
int a = 10;
int b = 20;
int c = 0;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
auto Add = [=,&a,&b](const int left, const int right)mutable
{
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
c = left + right;
};
Add(1, 2);
return 0;
}
表示以引用傳遞的方式捕捉變量a和b,以值傳遞的方式捕捉其他所有變量,最後的結果是:
a和b的地址沒有發生變化,c的地址發生變化,這種叫做混合捕獲;
在塊作用域以外的lambda函數捕捉列表必須爲空;在塊作用域中的lambda函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯報錯;lambda表達式之間不能相互賦值,即使看起來類型相同,例如:
void(*PF)();//函數指針
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
f1 = f2;//編譯報錯,找不到operator=()
auto f3(f2);//允許一個lambda表達式拷貝一個副本
f3();
PF = f2;//可以將lambda表達式賦值給想通過類型的函數指針
PF();
return 0;
}
那麼爲什麼不能相互賦值,這時我們需要看lambda表達式的底層實現原理,介紹函數對象與lambda表達式,例如:
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);
//lambda表達式
auto r2 = [=](double money, int year)->double
{
return money*rate*year;
};
r2(10000, 2);
return 0;
}
此時我們使用兩種方法(仿函數與lambda表達式),轉到反彙編代碼,得到:
可以看出,實際上在底層,lambda表達式完全是按照函數對象的方式實現的,定義一個lambda表達式,會自動生成一個類,在類中重載了operator(),即lambda表達式中捕獲列表中的內容相當於類中的成員變量,參數列表、返回值、實現體相當於類的operator()重載;auto r2 = [=](double money, int year)->double{return money *rate *year;};相當於這個lambda表達式對應的類創建了r2對象;
這也恰恰證明了lambda表達式之間不能相互賦值,但是卻可以進行拷貝副本;
源代碼:
https://github.com/wangbiy/C-3/tree/master/test_2019_11_27_1/test_2019_11_27_1
- 右值引用
右值引用:是一個別名,只能引用右值,C標準中並沒有給出具體的區分左值和右值,但是有一個參考就是:左值是可以取地址的叫做左值,只能放在右邊或者不能取地址的叫做右值,右值引用的書寫格式是:類型&& 引用變量名字例如:
int a = 10;
int& ra = a;
a = 10;
cout << &a << endl;//左值
int&& rra = 10;//10是右值
但是這只是一個參考,具體的還要結合代碼來進行實現,例如:
int b1 = 1, b2 = 2;
//b1 + b2 = 10;//編譯失敗
//&(b1 + b2);//不行,可能是一個右值
int&& rrb = b1 + b2;
這個表達式b1+b2是右值,但是++b1表達式的結果是一個左值;
又例如:
int Test()
{
int a = 10;
return a;
}
int main()
{
//Test() = 10;//不行
//&(Test());//不行
int&& rra = Test();//Test()的返回值結果是一個右值
return 0;
}
函數的返回值結果是一個右值;
總結:右值:不能放在賦值運算符的左側,不能取&,有些表達式的結果可能是左值;有些這種以值返回形式的函數的返回結果是右值(以引用形式作爲返回值的函數的返回結果是左值);但是這些只能作爲判斷是否爲右值的參考,不是絕對的;
C++11對右值進行了嚴格的區分:
(1)純右值:是C++98中右值的概念,用於識別臨時變量和一些不跟對象相關聯的值,例如:常量,一些運算符表達式(1+3);
(2)將亡值:生命週期快要結束的對象,比如在值返回時的臨時對象,表達式的中間結果;
移動語義
例如:
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* str = new char[strlen(s._str) + 1];
strcpy(str, s._str);
delete[] _str;
_str = str;
}
return *this;
}
~String()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
String operator+(const String& s)
{
char* str = new char[strlen(_str) + strlen(s._str) + 1];//當前對象的大小和要加的對象的大小+1
strcpy(str, _str);//將當前對象的內容拷到新開闢的空間
strcat(str, s._str);//將要加的對象的內容拼接到新空間
String strRet(str);
return strRet;
}
private:
char* _str;
};
void TestString()
{
String s1("hello ");
String s2("world");
String s3;
s3 = s1 + s2;
}
int main()
{
TestString();
return 0;
}
例如我們模擬實現一個string類,涉及到資源管理,用戶顯式提供拷貝構造函數、賦值運算符重載、析構函數,但是,此時我們構造一個s3對象,它的內容是s1和s2的內容相加得到,我們進行了+號運算符重載,不能改變左右操作數,因此我們返回的不是當前對象,也不能返回參數s,返回一個結果,返回值是以值的形式返回,我們創建對象strRet,由於我們構造的strRet在棧上,它包含的指針==指向的內容“hello world”==在堆上,我們以值的形式返回,在返回時也創建了一個臨時對象,也包含了指針,指向的內容“hello world”在堆上,也就是s3=strRet;我們可以發現strRet與臨時對象以及s3的指針所指向的內容是一樣的,strRet出了函數作用域釋放,然後又創建了臨時對象,產生了一個資源釋放又申請的問題,效率不高並且浪費空間,strRet相對於臨時對象是一個將亡值,臨時對象相當於s3是一個將亡值,因此我們可以解決一個空間釋放完又申請一個的問題,採用資源轉移,也就是移動語義,有效緩解效率比較低浪費資源的問題,(即先將strRet中的指針轉移給臨時對象,再將臨時對象的指針轉移給s3,這個是資源轉移)
因此我們可以將代碼改爲:
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(String&& s)
:_str(s._str)//將s中的資源轉移給當前對象
{
s._str = nullptr;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* str = new char[strlen(s._str) + 1];
strcpy(str, s._str);
delete[] _str;
_str = str;
}
return *this;
}
//移動賦值
String& operator=(String&& s)
{
if (this != &s)
{
delete[]_str;
_str = s._str;
s._str = nullptr;
}
return *this;
}
~String()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
String operator+(const String& s)
{
char* str = new char[strlen(_str) + strlen(s._str) + 1];//當前對象的大小和要加的對象的大小+1
strcpy(str, _str);//將當前對象的內容拷到新開闢的空間
strcat(str, s._str);//將要加的對象的內容拼接到新空間
String strRet(str);
return strRet;
}
private:
char* _str;
};
void TestString()
{
String s1("hello ");
String s2("world");
String s3;
s3 = s1 + s2;
}
int main()
{
TestString();
return 0;
}
加上移動構造和移動賦值,我們存儲“hello world”的空間就只開闢了一次,也就是將strRet的資源轉移給臨時對象,將臨時對象的資源轉移給s3,提高了效率並且節省了空間;這裏要注意move的誤用,例如:
int main()
{
TestString();
String s1("hello");
String s2(move(s1));//move的誤用,調用了移動構造來構造s2,s1的空間就銷燬了,然後再修改s1的內容,就發生了錯誤
s1[0] = 'H';
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)
{}
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p;
//構造臨時對象,銷燬p,應該調用移動構造,p是將亡值,但是此時編譯器將p中的name sex age都不是將亡值,也就是左值,因此應該將其通過move改成右值
}
int main()
{
Person p(GetTempPerson());
return 0;
}
此時我們在函數GetTempPerson中返回p時會構造臨時對象,應該調用移動構造,p就是將亡值,但是p中的name sex age都不是將亡值,都是左值,因此應該通過move將其變爲右值,因此代碼應該改爲:
class Person
{
public:
Person(char* name, char* sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
Person(Person&& p)
: _name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
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轉移擁有資源的成員爲右值。
在使用move轉的期間,要保證他轉的是一個將亡值;
完美轉發
概念:完美轉發是指在函數模板中,完全依照模板參數的類型,將參數傳遞給函數模板中調用的另外一個函數
例如:
void Fun(int &x)//左值引用
{
cout << "lvalue ref" << endl;
}
void Fun(int &&x)//右值引用
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)//const類型的左值引用
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)//const類型的右值引用
{
cout << "const rvalue ref" << endl;
}
template<class T>
void PerfectForward(T &&t)
{
Fun(t);
}
int main()
{
PerfectForward(10);//應該調用右值引用
int a;
PerfectForward(a); //應該調用左值引用
PerfectForward(std::move(a)); //應該調用右值引用
const int b = 8;
PerfectForward(b);//應該調用const類型的左值引用
PerfectForward(std::move(b)); //應該調用const類型的右值引用
return 0;
}
按理說應該是上述的結果,即 PerfectForward(10)應該調用右值引用 PerfectForward(a)應該調用左值引用,PerfectForward(std::move(a))應該調用右值引用,PerfectForward(b)應該調用const類型的左值引用,PerfectForward(std::move(b))應該調用const類型的右值引用,但是將代碼進行調試,卻發現前三個完美轉發都只是調用了左值引用,後兩個調用了const類型的左值引用,並沒有實現完美轉發,在這裏我們需要知道C++11中使用forward進行完美轉發,此時將代碼改成:
void Fun(int &x)//左值引用
{
cout << "lvalue ref" << endl;
}
void Fun(int &&x)//右值引用
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)//const類型的左值引用
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)//const類型的右值引用
{
cout << "const rvalue ref" << endl;
}
template<class T>
void PerfectForward(T &&t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10);//應該調用右值引用
int a;
PerfectForward(a); //應該調用左值引用
PerfectForward(std::move(a)); //應該調用右值引用
const int b = 8;
PerfectForward(b);//應該調用const類型的左值引用
PerfectForward(std::move(b)); //應該調用const類型的右值引用
return 0;
}
此時就實現了完美轉發
- 線程庫
首先多線程的作用就是可以提高效率,C++11對線程進行支持,使得C++在並行編程時不需要依賴第三方庫,而是在原子操作中還引入了原子類的概念,即提供了thread線程類;
其中的函數有:thread()表示構造一個線程對象,但是沒有關聯任何線程函數,說明沒有啓動任何線程,thread(fun,args1, args2,…)表示構造一個線程對象,關聯線程函數fun,arg1、arg2是線程函數的參數,get_id()表示獲取線程id,joinable()表示判斷線程是否是有效的,join()表示該函數調用後會阻塞主線程,直到該線程結束後,主線程繼續執行,detach()表示在創建線程對象後馬上調用,用於把被創建線程與主線程分離,分離的線程變爲後臺線程,創建的線程的"死活"就與主線程無關;
注意:線程對象可以關聯一個線程,用來控制線程及獲取線程的狀態;
當創建一個線程對象後,沒有提供線程函數,該對象實際沒有對應任何線程;
當創建一個線程對象後,並且給線程關聯線程函數,該線程就被啓動,與主線程一起運行,創建線程函數的方法有三種:函數指針、函數對象(仿函數)、lambda表達式,例如:
#include <thread>
void threadFunc(int a)
{
cout << "thread1:" << a << endl;
}
class TF
{
void operator()()
{
cout << "thread2" << endl;
}
};
int main()
{
thread t1(threadFunc, 10);//線程函數爲函數指針
TF t;
thread t2(t);//線程函數爲函數對象,仿函數
thread t3([]{
cout << "thread3" << endl;
});//線程函數爲lambda表達式
t1.join();
t2.join();
t3.join();
cout << "Main thread end!" << endl;
return 0;
}
注意:1、thread類是防拷貝的,不允許進行拷貝構造和賦值,但是可以進行移動構造和賦值,即將一個線程對象關聯線程的狀態轉移給其他線程對象,轉移期間不影響線程的執行;2、join()已經清理了線程的資源,thread對象與這個資源就無關了,再調用join()程序就會崩潰;
併發和並行的區別:並行是指兩個或者多個事件在同一時刻發生,就好比多個線程同時進行,而併發是指兩個或者多個事件在同一時間間隔內發生,微觀上這些程序只能分時的交替進行,它是一種僞並行;
線程函數參數
例如:
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;
}
按照道理上來說運行結果應該是20,30,40,但是最終的運行結果是:
說明線程t1中的線程函數的形參沒有改變外部實參,a沒有變化,因爲每個線程都有一個線程棧,在構造t1時,生成一個線程棧,將a放進去,而線程函數參數引用的是線程棧中的a,並沒有改變外部實參的a,所以需要用形參改變外部的實參時,必須使用ref()函數;
join與detach
啓動一個線程,當這個線程結束時,需要回收線程所使用的資源,共有兩種方式:join和detach,
join我們前面已經介紹過,要注意它的誤用,例如:
void ThreadFunc()
{
cout << "ThreadFunc()" << endl;
}
bool DoSomething()
{
return false;
}
int main()
{
thread t(ThreadFunc);
if (!DoSomething())
return -1;
t.join();
return 0;
}
它在運行會發生崩潰,因爲當主線程走到if語句的判斷位置時,有可能直接退出,此時線程對象銷燬,join沒有調用,資源沒有回收,造成資源泄漏;
又例如:
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.join();
}
這個與上述原因相似,會造成資源泄漏;
我們可以使用RAII的方式對線程對象進行封裝,避免產生以上情況;
另外一種就是detach方法了,該函數被調用後,新線程與線程對象(主線程)分離,不能通過線程對象控制線程,新線程會在後臺運行,其所有權和控制權將會交給c++運行庫,同時,C++運行庫保證,當線程退出時,其相關資源的能夠正確的回收。
detach()函數一般在線程對象創建好之後就調用,因爲如果不是jion()等待方式結束,那麼線程對象可能會在新線程結束之前被銷燬掉而導致程序崩潰。
因爲std::thread的析構函數中,如果線程的狀態是jionable,std::terminate將會被調用,而terminate()函數直接會終止程序;
原子性操作庫
多線程會產生的問題是線程安全問題,如果共享數據都是隻讀的,那麼沒問題,因爲只
讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據。但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩 ,例如:
unsigned long sum = 0L;
void func(size_t num)
{
for (int i = 0; i < num; ++i)
{
sum++;
}
}
int main()
{
cout << "Before" << sum << endl;
thread t1(func, 10000000);
thread t2(func, 10000000);
t1.join();
t2.join();
cout << "After" << sum << endl;
return 0;
}
按理上來說我們最終得到的sum應該是20000000,但是它的運行結果是不確定的,每一次重新運行都得到不一樣的數字,因爲此時兩個線程很有可能在同時進行,在C++98中,我們可以通過加互斥鎖的方法來對共享資源操作,例如:
#include <mutex>
mutex m;
unsigned long sum = 0L;
void func(size_t num)
{
for (int i = 0; i < num; ++i)
{
m.lock();
sum++;
m.unlock();
}
}
int main()
{
cout << "Before" << sum << endl;
thread t1(func, 10000000);
thread t2(func, 10000000);
t1.join();
t2.join();
cout << "After" << sum << endl;
return 0;
}
但是這種加鎖的操作,只要一個線程在進行sum++操作時,其他的線程都會阻塞,大大的影響了效率,而且如果鎖沒有控制好,很有可能造成死鎖,因此,在C++11中引入了原子操作,即不可被中斷的一個或者一系列操作,例如:
#include <atomic>
atomic_long sum{ 0L };
void func(size_t num)
{
for (int i = 0; i < num; ++i)
{
sum++;
}
}
int main()
{
cout << "Before" << sum << endl;
thread t1(func, 10000000);
thread t2(func, 10000000);
t1.join();
t2.join();
cout << "After" << sum << endl;
return 0;
}
實現了不需要對原子類型變量進行加鎖解鎖操作,線程能夠對原子類型變量互斥的訪問 ,我們還可以使用atomic類模板,來對任意類型進行操作,例如:
#include <atomic>
struct Date
{};
int main()
{
atomic<Date> t;
}
注意:原子操作屬於“資源型”數據,多個線程只能訪問單個原子類型的拷貝,因此在C++11中,原子類型只能從其模板參數中進行構造,不允許原子類型進行拷貝構造、移動構造以及operator=等,標準庫已經將atmoic模板類中的拷貝構造、移動構造、賦值運算符重載默認刪除掉了;
lock_guard與unique_lock
在多線程環境下,如果想要保證某個變量的安全性,只要將其設置成對應的原子類型即可,既高效又不容易出現死鎖問題。但是有些情況下,我們可能需要保證一段代碼的安全性,例如:
#include <mutex>
mutex m;
int number = 0;
int func1()
{
for (int i = 0; i < 100; ++i)
{
m.lock();
number++;
cout << "number:" << number << endl;
m.unlock();
}
return 0;
}
int func2()
{
for (int i = 0; i < 100; ++i)
{
m.lock();
number--;
cout << "number:" << number << endl;
m.unlock();
}
return 0;
}
int main()
{
thread t1(func1);
thread t2(func2);
t1.join();
t2.join();
cout << number << endl;
return 0;
}
那麼就只能通過鎖的方式來進行控制,但是又因爲有時會存在死鎖問題,因此C++11中採用RAII的方式封裝了鎖,即lock_guard與unique_lock,lock_guard的定義如下:
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,調用構造函數成功上鎖,出作用域前,lock_guard對象要被銷燬,調用析構函數自動解鎖,可以有效避免死鎖問題,但是用戶沒有辦法對該鎖進行控制,因此C++11中出現了unique_lock,它也是採用了RAII的方式對鎖進行了封裝,對象之間不能發生拷貝,與lock_guard不同的是:
1、它提供了更多的成員函數,例如:
(1)上鎖/解鎖操作:lock、try_lock、try_lock_for、try_lock_until和unlock
(2)修改操作:移動賦值、交換(swap:與另一個unique_lock對象互換所管理的互斥量所有權)、釋放(release:返回它所管理的互斥量對象的指針,並釋放所有權)
(3)獲取屬性:owns_lock(返回當前對象是否上了鎖)、operator bool()(與owns_lock()的功能相同)、mutex(返回當前unique_lock所管理的互斥量的指針)。
相關源代碼(github):
https://github.com/wangbiy/C-3/tree/master/test_2019_12_2_1/test_2019_12_2_1