1. C++11簡介
相比於C++98/03,C++11則帶來了數量可觀的變化,其中包含了約140個新特性,以及對C++03標準中約600個缺陷的修正,這使得C++11更像是從C++98/03中孕育出的一種新語言。相比較而言,C++11能更好地用於系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率。
2. 列表初始化
2.1 C++98中{}的初始化問題
在C++98中,標準允許使用花括號{}對數組元素進行統一的列表初始值設定。
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
對於一些自定義的類型,卻無法使用這樣的初始化。
vector<int> v{1,2,3,4,5};//C++98無法編譯
就無法通過編譯,導致每次定義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()
{
Point p{ 1, 2 };
return 0;
}
2. 多個對象的列表初始化
多個對象想要支持列表初始化,需給該類(模板類)添加一個帶有initializer_list類型參數的構造函數即可。
注意:initializer_list是系統自定義的類模板,該類模板中主要有三個方法:begin()、end()迭代器以及獲取區間中元素個數的方法size()。
#include <initializer_list>
template<class T>
class Vector {
public:
// ...
Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for(auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
};
3. 變量類型推導
3.1 爲什麼需要類型推導
在定義變量時,必須先給出變量的實際類型,編譯器才允許定義,但有些情況下可能不知道需要實際類型怎麼給,或者類型寫起來特別複雜,比如:
#include <map>
#include <string>
int main()
{
short a = 32670;
short b = 32670;
// c如果給成short,會造成數據丟失,如果能夠讓編譯器根據a+b的結果推導c的實際類型,就不會存在問題
short c = a + b;
auto d = a + b;
std::map<std::string, std::string> m{ { "apple", "蘋果" }, { "banana", "香蕉" } };
// 使用迭代器遍歷容器, 迭代器類型太繁瑣
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
auto its = m.begin();
while (its != m.end())
{
cout << its->first << " " << its->second << endl;
++its;
}
return 0;
}
C++11中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,可以給程序的書寫提供許多方便。將程序中c與it的類型換成auto,程序可以通過編譯,而且更加簡潔。
3.2 decltype類型推導
3.2.1 爲什麼需要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 運行時類型識別)。
3.2.2 decltype
decltype是根據表達式的實際類型推演出定義變量時所用的類型,比如:
- 推演表達式類型作爲變量的定義類型
int main()
{
double a = 10.9887;
int b = 20;
// 用decltype推演a+b的實際類型,作爲定義c的類型
decltype(a + b) c;
c = a + b;
cout << typeid(c).name() << endl;
cout << c << endl;
return 0;
}
- 推演函數返回值的類型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果沒有帶參數,推導函數的類型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果帶參數列表,推導的是函數返回值的類型,注意:此處只是推演,不會執行函數
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
4 範圍for循環
4.1 基於範圍的for循環
for(元素類型 元素對象:容器對象)
{
循環體
}
- 如果循環體由單條語句或者單個結構塊組成,可以省略花括號
- 用元素對象依次結合容器對象中的每一個元素,每結合一個元素,執行依次循環體,直至容器內的所有元素都被結合完爲止.
- 不依賴於下標元素,通用
- 不需要訪問迭代器,透明
- 不需要定義處理函數,簡潔
5 final與override
5.1 override
作用:指定一個虛函數覆蓋另一個虛函數
class A
{
virtual void foo();
void bar();
};
class B : A
{
void foo() const override; // 錯誤:B::foo 不覆蓋 A::foo
// (簽名不匹配)
void foo() override; // OK:B::foo 覆蓋 A::foo
void bar() override; // 錯誤:A::bar 非虛
};
override作用是幫助檢查是否繼承了想要繼承的虛函數。可以避免出現 “在繼承的時候寫錯了函數(參數類型、參數個數不符),編譯沒問題但是程序運行時和預想的不一樣” 的情況。
建議重寫虛函數的時候加上 override。
5.2final
作用:指定某個虛函數不能在子類中被覆蓋,或者某個類不能被子類繼承。
class Base
{
virtual void foo();
};
class A : Base
{
void foo() final; // Base::foo 被覆蓋而 A::foo 是最終覆蓋函數
void bar() final; // 錯誤:非虛函數不能被覆蓋或是 final
};
class B final : A // class B 爲 final
{
void foo() override; // 錯誤:foo 不能被覆蓋,因爲它在 A 中是 final
};
class C : B // 錯誤:B 爲 final
{
};
6 智能指針
7. 新增加容器—靜態數組array、forward_list以及unordered系列
/*
array 是一個類似vector的容器,但是是保存在棧區的,因此性能更好,不能夠隱式轉換爲指針
編譯時創建固定大小數組,只需要指定類型和大小即可
*/
void Tarray()
{
array<int, 5> arr={ 1, 2, 3, 4, 5 };//長度必須是常量或者常量表達式
int * parr = &arr[0];
parr = arr.data();
parr = nullptr;//轉換爲指針的操作
//forward_list<int> flist;
//區別於list雙向鏈表的單向鏈表,空間利用率和速率都更高
/*
新增兩組無序容器:
unordered_map
unordered_multimap
unordered_set
unordered_multiset
不同於set和map內部通過紅黑樹實現,而是hash表實現
*/
}
void Ttuple()
{
//就像一個可以容納不同類型的結構體
tuple<int, double, string> tps(12, 17.58, "pixel");
auto tp = make_tuple(5, 12.125, "hello");//構造元組,類型推斷爲tuple<int,double,string>
cout << get<2>(tp) << endl;//獲取元素,無法使用變量下標
int id;
double bim;
string item;
tie(id, bim, item) = tp;//元組拆包
cout << id << " " << bim << " " << item << endl;
auto ntp = tuple_cat(tps, tp);//元組的連接
/*
pair,一個兩個成員的結構體
*/
auto p1 = make_pair(12, 'c');
pair<double, int> p2(12.125, 17);
cout << p2.first << " " << p2.second << endl;
p2 = make_pair(1, 1.2);
p2 = p1;//一個含有成員函數的結構體
//pair可以使用typedef進行簡化聲明
}
int main()
{
Tarray();
Ttuple();
system("pause");
return 0;
}
8. 默認成員函數控制
在C++中對於空類編譯器會生成一些默認的成員函數,比如:構造函數、拷貝構造函數、運算符重載、析構函數和&和const&的重載、移動構造、移動拷貝構造等函數。如果在類中顯式定義了,編譯器將不會重新生成默認版本。有時候這樣的規則可能被忘記,最常見的是聲明瞭帶參數的構造函數,必要時則需要定義不帶參數的版本以實例化無參的對象。而且有時編譯器會生成,有時又不生成,容易造成混亂,於是C++11讓程序員可以控制是否需要編譯器生成。
8.1 顯式缺省函數
在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;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
8.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;
}
9 右值引用
9.1 右值引用概念
C++98中提出了引用的概念,引用即別名,引用變量與其引用實體公共同一塊內存空間,而引用的底層是通過指針來實現的,因此使用引用,可以提高程序的可讀性。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
}
爲了提高程序運行效率,C++11中引入了右值引用,右值引用也是別名,但其只能對右值引用
int Add(int a, int b)
{
return a + b;
}
int main()
{
const int&& ra = 10;
// 引用函數返回值,返回值是一個臨時變量,爲右值
int&& rRet = Add(10, 20);
return 0;
}
9.2 左值和右值
左值和右值的區別:
- 普通類型的變量,因爲有名字,可以取地址,都認爲是左值。
- const修飾的常量,不可修改,只讀類型的,理論應該按照右值對待,但因爲其可以取地址(如果只是const類型常量的定義,編譯器不給其開闢空間,如果對該常量取地址時,編譯器才爲其開闢空間),C++11認爲其是左值。
- 如果表達式的運行結果是一個臨時變量或者對象,認爲是右值。
- 如果表達式運行結果或單個變量是一個引用則認爲是左值。
總結:
- 不能簡單地通過能否放在=左側右側或者取地址來判斷左值或者右值,要根據表達式結果或變量的性質判斷,比如上述:c常量
- 能得到引用的表達式一定能夠作爲引用,否則就用常引用。
C++11對右值進行了嚴格的區分:
- C語言中的純右值,比如:a+b, 100
- 將亡值。比如:表達式的中間結果、函數按照值的方式進行返回。
9.3 引用與右值引用比較
int main()
{
// 普通類型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra爲a的別名
//int& ra2 = 10; // 編譯失敗,因爲10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情況不能直接引用左值。
int main()
{
// 10純右值,本來只是一個符號,沒有具體的空間,
// 右值引用變量r1在定義過程中,編譯器產生了一個臨時變量,r1實際引用的是臨時變量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 編譯失敗:右值引用不能引用左值
return 0;
}
值的形式返回對象的缺陷
如果一個類中涉及到資源管理,用戶必須顯式提供拷貝構造、賦值運算符重載以及析構函數,否則編譯器將會自動生成一個默認的,如果遇到拷貝對象或者對象之間相互賦值,就會出錯,比如:
class String
{
public:
String(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* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
~String()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1 + s2);
return 0;
}
在operator+中:strRet在按照值返回時,必須創建一個臨時對象,臨時對象創建好之後,strRet就被銷燬了,最後使用返回的臨時對象構造s3,s3構造好之後,臨時對象就被銷燬了。仔細觀察會發現:strRet、臨時對象、s3每個對象創建後,都有自己獨立的空間,而空間中存放內容也都相同,相當於創建了三個內容完全相同的對象,對於空間是一種浪費,程序的效率也會降低,而且臨時對象確實作用不是很大
9.5 移動語義
C++11提出了移動語義概念,即:將一個對象中資源移動到另一個對象中的方式,可以有效緩解9.4內存浪費問題。
在C++11中如果需要實現移動語義,必須使用右值引用。上述String類增加移動構造:
String(String&& s)
: _str(s._str)
{
s._str = nullptr;
}
因爲strRet對象的生命週期在創建好臨時對象後就結束了,即將亡值,C++11認爲其爲右值,在用strRet構造臨時對象時,就會採用移動構造,即將strRet中資源轉移到臨時對象中。而臨時對象也是右值,因此在用臨時對象構造s3時,也採用移動構造,將臨時對象中資源轉移到s3中,整個過程,只需要創建一塊堆內存即可,既省了空間,又大大提高程序運行的效率。
注意:
- 移動構造函數的參數千萬不能設置成const類型的右值引用,因爲資源無法轉移而導致移動語義失效。
- 在C++11中,編譯器會爲類默認生成一個移動構造,該移動構造爲淺拷貝,因此當類中涉及到資源管理時,用戶必須顯式定義自己的移動構造。
9.6 右值引用引用左值
當需要用右值引用引用一個左值時,可以通過move函數將左值轉化爲右值。C++11中,std::move()函數位於 頭文件中,該函數名字具有迷惑性,它並不搬移任何東西,唯一的功能就是將一個左值強制轉化爲右值引用,然後實現移動語義。
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
注意:
- 被轉化的左值,其生命週期並沒有隨着左值的轉化而改變,即std::move轉化的左值變量value不會被銷燬。
- STL中也有另一個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;
}
9.7 完美轉發
完美轉發是指在函數模板中,完全依照模板的參數的類型,將參數傳遞給函數模板中調用的另外一個函數。
函數模板在向其他函數傳遞自身形參時,如果相應實參是左值,它就應該被轉發爲左值;如果相應實參是右值,它就應該被轉發爲右值。
這樣做是爲了保留在其他函數針對轉發而來的參數的左右值屬性進行不同處理(比如參數爲左值時實施拷貝語義;參數爲右值時實施移動語義)
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(const int &&x){ cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t){ Fun(std::forward<T>(t)); }
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
9.8 右值引用作用
C++98中引用作用:因爲引用是一個別名,需要用指針操作的地方,可以使用指針來代替,可以提高代碼的可讀性以及安全性。
C++11中右值引用主要有以下作用:
- 實現移動語義(移動構造與移動賦值)
- 給中間臨時變量取別名:
int main()
{
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之後的結果拷貝構造的新對象
stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之後結果的別名
return 0;
}
- 實現完美轉發
10 lambda表達式
10.1 C++98中的一個例子
在C++98中,如果想要對一個數據集合中的元素進行排序,可以使用std::sort方法。
#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默認按照小於比較,排出來結果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改變元素的比較規則
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素爲自定義類型,需要用戶定義排序時的比較規則:
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;
}
10.2 lambda表達式
int main()
{
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;});
return 0;
}
lamb表達式實際是一個匿名函數。
10.3 lambda表達式語法
lambda表達式書寫格式:
[capture-list] (parameters)
mutable -> return-type { statement }
- 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使用,以及使用的方式傳值還是傳引用。- [var]:表示值傳遞方式捕捉變量var
- [=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this)
- [&var]:表示引用傳遞捕捉變量var
- [&]:表示引用傳遞捕捉所有父作用域中的變量(包括this)
- [this]:表示值傳遞方式捕捉當前的this指針
注意:
- 父作用域指包含lambda函數的語句塊
- 語法上捕捉列表可由多個捕捉項組成,並以逗號分割。
比如:[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量
[&,a, this]:值傳遞方式捕捉變量a和this,引用方式捕捉其他變量 - 捕捉列表不允許變量重複傳遞,否則就會導致編譯錯誤。
比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重複 - 在塊作用域以外的lambda函數捕捉列表必須爲空。
- 在塊作用域中的lambda函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯報錯。
- 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;
}
10.4 函數對象與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()。
11 線程庫
11.1 thread類的簡單介紹
windows和linux下各有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進行支持了,使得C++在並行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。要使用標準庫中的線程,必須包含< thread >頭文件。
函數名 | 功能 |
---|---|
thread() | 構造一個線程對象,沒有關聯任何線程函數,即沒有啓動任何線程 |
thread(fn, args1, args2, …) | 構造一個線程對象,並關聯線程函數fn,args1,args2,…爲線程函數的參數 |
get_id() | 獲取線程id |
jionable() | 線程是否還在執行,joinable代表的是一個正在執行中的線程。 |
jion() | 該函數調用後會阻塞住線程,當該線程結束後,主線程繼續執行 |
detach() | 在創建線程對象後馬上調用,用於把被創建線程與線程對象分離開,分離的線程變爲後臺線程,創建的線程的"死活"就與主線程無關 |
注意:
- 線程是操作系統中的一個概念,線程對象可以關聯一個線程,用來控制線程以及獲取線程的狀態。
- 當創建一個線程對象後,沒有提供線程函數,該對象實際沒有對應任何線程。
get_id()的返回值類型爲id類型,id類型實際爲std::thread命名空間下封裝的一個類,該類中包含了一個結構體:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
- 當創建一個線程對象後,並且給線程關聯線程函數,該線程就被啓動,與主線程一起運行。線程函數一般情況下可按照以下三種方式提供:
- 函數指針
- lambda表達式
- 函數對象
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結束
併發與並行的區別?
(1)並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生。
(2)並行是在不同實體上的多個事件,併發是在同一實體上的多個事件。
(3)在一臺處理器上“同時”(這個同時實際上市交替“”)處理多個任務,在多臺處理器上同時處理多個任務
11.2 線程函數參數
線程函數的參數是以值拷貝的方式拷貝到線程棧空間中的,因此:即使線程參數爲引用類型,在線程中修改後也不能修改外部實參,因爲其實際引用的是線程棧中的拷貝,而不是外部實參。
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;
}
注意:如果是類成員函數作爲線程參數時,必須將this作爲線程函數參數。
11.3 join與detach
啓動了一個線程後,當這個線程結束的時候,如何去回收線程所使用的資源呢?thread庫給我們兩種選擇:
- join()方式
- join():主線程被阻塞,當新線程終止時,join()會清理相關的線程資源,然後返回,主線程再繼續向下執行,然後銷燬線程對象。由於join()清理了線程的相關資源,thread對象與已銷燬的線程就沒有關係
了,因此一個線程對象只能使用一次join(),否則程序會崩潰。
- join():主線程被阻塞,當新線程終止時,join()會清理相關的線程資源,然後返回,主線程再繼續向下執行,然後銷燬線程對象。由於join()清理了線程的相關資源,thread對象與已銷燬的線程就沒有關係
// jion()的誤用一
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; }
bool DoSomething() { return false; }
int main()
{
std::thread t(ThreadFunc);
if(!DoSomething())
return -1;
t.join();
return 0;
}
/*
說明:如果DoSomething()函數返回false,主線程將會結束,jion()沒有調用,線程資源沒有回收,
造成資源泄漏。
*/
// 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的方式對線程對象進行封裝,比如
- detach()方式
- detach():該函數被調用後,新線程與線程對象分離,不再被線程對象所表達,就不能通過線程對象控制線程了,新線程會在後臺運行,其所有權和控制權將會交給c++運行庫。同時,C++運行庫保證,當線程退出時,其相關資源的能夠正確的回收。
detach()函數一般在線程對象創建好之後就調用,因爲如果不是jion()等待方式結束,那麼線程對象可能會在新線程結束之前被銷燬掉而導致程序崩潰。因爲std::thread的析構函數中,如果線程的狀態是jionable,std::terminate將會被調用,而terminate()函數直接會終止程序。
因此:線程對象銷燬前,要麼以jion()的方式等待線程結束,要麼以detach()的方式將線程與線程對象分離。
11.4 原子性操作庫(atomic)
多線程最主要的問題是共享數據帶來的問題(即線程安全)。如果共享數據都是隻讀的,那麼沒問題,因爲只讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據。但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩。比如
#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<iostream>
#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類模板,定義出需要的任意原子類型
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;
}
11.5 lock_guard與unique_lock
在多線程環境下,如果想要保證某個變量的安全性,只要將其設置成對應的原子類型即可,即高效又不容易出現死鎖問題。但是有些情況下,我們可能需要保證一段代碼的安全性,那麼就只能通過鎖的方式來進行控制。
11.5.1 Mutex的種類
在C++11中,Mutex總共包了四個互斥量的種類:
1. std::mutex
C++11提供的最基本的互斥量,該類的對象之間不能拷貝,也不能進行移動。mutex最常用的三個函數:
函數名 | 函數功能 |
---|---|
lock() | 上鎖:鎖住互斥量 |
unlock() | 解鎖:釋放對互斥量的所有權 |
try_lock() | 嘗試鎖住互斥量,如果互斥量被其他線程佔有,則當前線程也不會被阻塞 |
注意,線程函數調用lock()時,可能會發生以下三種情況:
- 如果該互斥量當前沒有被鎖住,則調用線程將該互斥量鎖住,直到調用 unlock之前,該線程一直擁有該鎖
- 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住
- 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)
線程函數調用try_lock()時,可能會發生以下三種情況:
- 如果當前互斥量沒有被其他線程佔有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量
- 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而並不會被阻塞掉
- 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)
2. std::recursive_mutex
其允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock(),除此之外,std::recursive_mutex 的特性和std::mutex 大致相同。
3. 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。
4. std::recursive_timed_mutex
11.5.2 lock_guard
std::lock_gurad 是 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對象要被銷燬,調用析構函數自動解鎖,可以有效避免死鎖問題。
lock_guard的缺陷:太單一,用戶沒有辦法對該鎖進行控制,因此C++11又提供了unique_lock。
11.5.3 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所管理的互斥量的指針)。