C++進階:C++11

列表初始化

在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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章