1. 程式人生 > 程式設計 >詳解C++之C++11的牛逼特性

詳解C++之C++11的牛逼特性

一、列表初始化

1.1 C++98中,標準允許使用花括號{}對陣列元素進行統一的列表初始值設定。

int array1[] = {1,2,3,4,5};
int array2[] = {0};

對對於一些自定義型別,卻不行.

vector<int> v{1,5};

在C++98中這樣無法通過編譯,因此需要定義vector之後,在使用迴圈進行初始賦值。
C++11擴大了用初始化列表的使用範圍,讓其適用於所有的內建型別和自定義型別,而且使用時,=可以不寫

// 內建型別
int x1 = {10};
int x2{10}
// 陣列
int arr1[5] {1,5}
int arr2[]{1,5};
// 標準容器
vector<int> v{1,3}
map<int,int> m{{1,1},{2,2}}
// 自定義型別
class Point
{
int x;
int y;
}
Power p{1,2};

1.2 多個物件的列表初始化
給類(模板類)新增一個帶有initializer_list型別引數的建構函式即可支援多個物件的,列表初始化.

#include<initializer_list>
template<class T>
class Vector{
public:
	Vecto(initializer_list<T> l)
		:_capacity(l.size()),_size(0){
			_array = new T[_capacity];
			for(auto e : l)
				_array[_size++] = 3;
		}
private;
	T* _array;
	size_t _capacity;
	size_t _size;
};

二、變數型別推導

2.1 auto
在C++中,可以使用auto來根據變數初始化表示式型別推導變數的實際型別,簡化程式的書寫

// 不使用auto需要寫很長的迭代器的型別
map<string,string> m;
map<string,string>::iterator it1 = m.begin();
// 使用auto就很簡單
auto it2 = m.begin();

2.1 decltype 型別推導
auto使用時,必須對auto宣告的型別進行初始化,否則編譯器不能推匯出auto的實際型別。
但是有些場景可能需要根據表示式執行後的結果進行型別推導。因為編譯時,程式碼不會執行,auto也就…

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

如果用加完後的結果作為函式的返回型別,可能會出錯,這就需要程式執行完後才能知道結果的實際型別,即RTTI(執行時型別識別)
decltype可以根據表示式的實際型別推演出定義變數時所用的型別

// 推演表示式作為變數的定義型別
int a = 1,b=2;
decltype(a+b) c;
cout<<typeid(c).name()<<endl;

// 推演函式的返回值型別
void GetMemory(size_t size){
	return malloc(size);
}
cout<<typeid(decltype(GetMemory)).name()<<endl;

三、基於範圍for的迴圈

vector<int> v{1,5};
for(const auto& e : v)
	cout<<e<<' ';
cout<<endl;

四、final和override
在我的多型的文章中有介紹:https://www.jb51.net/article/162078.htm

五、委派建構函式
委派建構函式可以通過委派其它建構函式,使多建構函式的類編寫更加容易

class Info
{
public;
	Info()
		:_type(0),_name('s')
		{}
	Info(int type)
		:_type(type),_name('a')
		{}
	Info(char a)
		:_type(0),_name(a)
		{}
pirvate;
	int _type;
	char _name;
};

上面的建構函式除了初始化列表不同之外,其它部分都是類似的,程式碼重複,可以使用委派建構函式
委派建構函式就是把構造的任務委派給目標建構函式來完成類的構造

class Info
{
// 目標建構函式
public:
	Info()
		:_type(0),_a('a')
	{}
// 委派建構函式
	Info(int type)
		:Info()
		{
			_type = type;
		}
private;
	int _type = 0;
	char _a = 'a';
};

在初始化列表中呼叫“基準版本”的建構函式稱為委派建構函式,而被呼叫的“基準版本”則稱為目標建構函式

六、預設函式控制
在C++中對於空類,編譯器會生成一些預設的成員函式,如果在類中顯式定義了,編譯器就不會重新生成預設版本。但是如果在一個類中聲明瞭帶參的建構函式,如果又需要定義不帶參的例項化無參的物件。這時候編譯器是有時生成,有時不生成,就會造成混亂,C++11可以讓程式設計師自己控制是否需要編譯器生成。

6.1 顯式預設函式
在C++11中,可以在預設函式定義或宣告時加上=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;

6.2 刪除預設函式
要想限制一些預設函式的生成,在C++98中,可以把該函式設為私有,不定義,這樣,如果有人呼叫就會報錯。在C++11中,可以給該函式宣告加上=delete就可以。

class A
{
A(int a)
	:_a(a)
{}
A(constA&) = delete; // 禁止編譯器生成預設的拷貝建構函式
private:
	int _a;
};

七、右值引用
7.1 移動語義

class String
{
public:
	String(char* str = '")
	{
		if(str == nullptr)
			_str = "";
		_str = new char[strlen(str)+1];
		strcpy(_str,str);
	}
	String(const String& s)
		:_str(new char[strlen(c._str)+1])
	{
		strcpy(_str,s._str);
	}
	~String()
	{
		if(_str)
			delete[] _str;
	}
private:
	char* _str;
};

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

在上面的程式碼中,GetString函式返回的臨時物件,將s2拷貝成功之後,立馬銷燬了(臨時物件
的空間被釋放);而s2拷貝構造的時,又需要分配空間,一個剛釋放,一個又申請,有點多此一舉,那能否把GetString返回的臨時物件的空間直接交給s2呢?這樣s2也不需要重新開闢空間了。

在這裡插入圖片描述

移動語義:將一個物件資源移動到另一個物件中的方式,在C++中要實現移動語義,必須使用右值引用.

7.2 C++中的右值
右值引用,顧名思義就是對右值的引用。在C++中右值由純右值和將亡值構成。

  • 純右值:用於識別變數和一些不跟物件關聯的值。比如:常量、運算子表示式等、
  • 將亡值:宣告週期將要結束的物件。比如:在值返回時的臨時物件

7.3 右值引用
格式:型別&& 應用變數名字 = 實體;
使用場景:
1、與移動語義相結合,減少必須要的資源的開闢,提高執行效率

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

2、給一個匿名物件取別名,延長匿名物件的生命週期

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

注意

  • 右值引用在定義時必須初始化
  • 右值引用不能引用左值
int a = 10;
int&& a1; // 未初始化,編譯失敗
int&& a2 = a; // 編譯失敗,a是一個左值
// 左值是可以改變的值

7.4 std::move()
C++11中,std::move()函式位於標頭檔案中,它可以把一個左值強制轉化為右值引用,通過右值引用使用該值,實現移動語義。該轉化不會對左值產生影響.
注意:其更多用在生命週期即將結束的物件上。

7.5 移動語義中要注意的問題
1、在C++11中,無參建構函式/拷貝建構函式/移動建構函式實際上有三個版本

Object()
Object(const T&)
Object(T&&)

2、如果將移動建構函式宣告為常右值引用或者返回右值的函式宣告為常量,都會導致移動語義無法實現

String(const String&&);
const String GetString();

3、C++11預設成員函式,預設情況下,編譯器會隱士生成一個移動建構函式,是按照位拷貝來進行。因此在涉及到資源管理時,最好自己定義移動建構函式。

class String
{
public:
	String(char* str = "")
	{
		if(str == nullptr)
			str = "";
		_str = new char[strlen(str)+1];
		strcpy(_str,str);
	}
	// 拷貝構造
	// String s(左值物件)
	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
		{
			strcpy(_str,s_str);
		}
	// 移動構造
	// String s(將亡值物件)
	String(String&& s)
		:_str(nullptr)
		{
			swap(_str,s._str);
		}
	// 賦值
	String& operator=(const String& s)
	{
		if(this != &s)
		{
			char* tmp = new char[strlen(s._str)+1];
			stcpy(tmp,s._str);
			delete[] _str;
			_str = tmp;
		}
		return *this;
	}
	// 移動賦值
	String& operator=(String&& s)
	{
		swap(_str,s._str);
		return *this;
	}
	~String()
	{
		if(_str)
			delete[] _str;
	}
	// s1 += s2 體現左值引用,傳參和傳值的位置減少拷貝
	String& operator+=(const String& s)
	{
		// this->Append(s.c_str());
		return *thisl
	}
	// s1 + s2
	String operator+(const String& s)
	{
		String tmp(*this);
		// tmp.Append(s.c_str());
		return tmp;
	}
	const char* c_str()
	{
		return _str;
	}
private:
	char* _str;
};
int main()
{
	String s1("hello"); // 例項化s1時會呼叫移動構造
	String s2("world");
	String ret 
	ret = s1 + s2 // +返回的是臨時物件,這裡會呼叫移動構造和移動賦值,減少拷貝
	
	vector<String> v;
	String str("world");
	v.push_back(str); // 這裡呼叫拷貝建構函式
	v.push_back(move(str)); // 這裡呼叫移動構造,減少一次拷貝
	return 0;
}

在這裡插入圖片描述

總結:
左值:可以改變的值;
右值: 不可以改變的值(常量,表示式返回值,臨時物件)
左值引用: int& aa = a; 在傳參和傳值的位置使用,減少拷貝,提高效率
右值引用: int&& bb = 10; 在傳值返回和將亡值傳參時,通過呼叫移動構造和移動賦值,減少拷貝,提高效率。
const 左值引用可以引用右值
右值引用可以應用move後的左值

7.6 完美轉發
完美轉發是指在函式模板中,完全按照模板的引數的型別,將引數傳遞給函式模板中呼叫的另外一個函式

void Func(int x)
{
	// ......
}
template<typename T>
void PerfectForward(T t)
{
	Fun(t);
}

PerfectForward為完美轉發的模板函式,Func為實際目標函式,但上面的轉發還不夠完美,完美轉發是目標函式希望將引數按照傳遞給轉發函式的實際型別轉給目標函式,而不產生額外開銷,就好像沒有轉發者一樣.
所謂完美:函式模板在向其他函式傳遞自身形參時,如果相應實參是左值,就轉發左值;如果是右值,就轉發右值。(這樣是為了保留在其他函式針對轉發而來的引數的左右值屬性進行不同處理,比如引數為左值時實施拷貝語義、引數為右值時實施移動語義)
在C++11中,通過forward函式來實現完美轉發。

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; }

八、lambda表示式
在C++98中,如果想對一個數據集合中的元素進行排序,可以使用std::sort()方法,但其預設按照小於比較,如果想排降序,則要傳入第三個引數,可以使用std::greater()的比較方法,

vector<int> v{1,7,6,5};
// 預設按照小於比較,結果是升序
sort(v.begin(),v.end());
// 傳入第三個模板引數std::greater<T>(),按照大於比較,預設是降序
sort(v.begin(),v.end(),greater<int>());

但是該方法只支援內建型別,對於用於自定義的型別就無能為力了,這是就需要用於自定義排序時的規則。目前我們可以通過函式指標,仿函式,lambda來解決。

1、lambda 表示式語法

[捕捉列表](引數列表)mutable->返回值型別{函式體}
捕捉列表:該列表出現在lambda函式的開始位置,編譯器根據[]來判斷接下來的程式碼是否為lambda函式,捕捉列表可以捕捉上下文中的變數供lambda函式使用
引數列表:與普通函式的引數列表一致。則可以連同()一起省略
mutable:預設情況下,lambda函式總是一個const函式,mutable可以取消其常量性。使用該修飾符,引數列表不可以省略(即使引數列表為空)
->返回值型別。用於追蹤返回值型別。沒有返回值時可以省略。返回值型別明確的情況下,也可以省略
{函式體}:在該函式體,除了可以使用引數外,也可以使用捕捉到的所有變數

!!!在lambda函式定義中,引數列表和返回值型別都是可選部分,而捕捉列表和函式體可以為空。

int main()
{
	// 最簡單的lambda表示式
	[]{};
	// 省略引數列表和返回值型別,返回值型別有編譯器推演為int
	int a=3,b=4;
	[=]{return a+3;};
	// 省略返回值型別
	auto fun1 = [&](int c){b = a + c;};
	// 各部分完整的lambda函式
	auto fun2 = [=,&b](int c)->int(return += a + c;);
	// 複製捕捉x
	int x = 10;
	auto add_x = [x](int a)mutable{x *= 2; return a + x;};
	return 0;
}

2、捕獲列表說明
捕獲列表描述了上下文中那些資料可以被lambda使用,以及使用的方式傳值還是引用

a [var]:表示值傳遞方式捕獲變數var
b [=]:表示值傳遞方式捕獲所有父作用域中的變數(包括this)
c [&var]:表示引用傳遞變數var
d [&]:表示引用傳遞捕獲所有父作用域中的變數(this)
e [this]:表示值傳遞方式捕獲當前的this指標

!!!:

a 父作用域包含lambda函式的語句塊
b 語法上捕獲列表可由多個捕獲項組成,並以逗號分隔
	比如:[=,&a,&b]:以引用傳遞的方式捕獲變數a 和 b,值傳遞的方式捕獲其它所有變數.
	[&,a,this];值傳遞的方式捕獲變數a和this,引用方式捕獲其它變數。
	捕捉列表不允許變數重複傳遞,否則會導致編譯錯誤。比如:[=,a]以傳值的方式捕獲了所有變數,又重複捕捉a
c 塊作用域以外的lambda函式捕捉列表必須為空
e 在塊作用域中的lambda函式僅能捕捉父作用域中區域性變數,捕捉任何非此作用域或者非區域性變數都會導致編譯報錯
f lambda表示式之間不能相互賦值,即使看起來型別相同.
void (*PF)();
int main()
{
	auto f1 = []{cout<<"hello world"<<endl;};
	auto f2 = []{cout<<"hello world"<<endl;};
	f1= f2; // 這裡會編譯失敗,提示找不到operator=()
	auto f3(f2); // 允許使用一個lambda表示式拷貝一個新的福分
	PF = f2; // 可以將lambda表示式賦值給相同型別的指標
	return 0;
}

3、lambda表示式與函式指標、仿函式

typedef bool (*GTF) (int,int);
bool greater_func1(int l,int r)
{
	return l > r;
}

struct greater_func2
{
	bool operator()(int l,int r)
	{
		return l > r;
	}
};

int main()
{
	// 函式指標
	GTF f1 = greater_func1; // typedef 定義
 // bool (*f1) (int,int) = greater_func1; // 不typedef,直接原生寫法,可讀性差
 cout<< f1(1,2)<<endl;
 // 仿函式
 greater_func2 f2;
 cout<< f2(1,2)<<endl;
 // lamba表示式
 auto f3 = [] (int l,int r) ->bool{return l > r;};
 cout<< f3(1,2)<<endl;
 
	int a[] = {1,5,9,8};
	sort(a,a+sizeof(a)/sizeof(a[0]),f1);
	sort(a,f2);
	sort(a,f3);
	// sort函式第三個模板引數能接受函式指標,仿函式、lambda表示式,是因為其第三個引數是一個模板custom (2)	
	template <class RandomAccessIterator,class Compare>
 void sort (RandomAccessIterator first,RandomAccessIterator last,Compare comp);
 
	return 0;
}

函式指標,仿函式,lambda用法上是一樣的,但函式指標型別定義很難理解,仿函式需要實現運算子的過載,必須先定義一個類,而且一個類只能實現一個()operator的過載。(ep:對商品的不同屬性實現比較就需要實現不同的類),要先定義好才能使用。而lambda可以定義好直接使用.

struct Goods
{
	string _name;
	double _price;
	double _appraise;
};

int main()
{
	Goods gds[] = { { "蘋果",2.1,10 },{ "相交",8 },{ "橙子",2.2,7 },{ "菠蘿",1.5,10 } };

	sort(gds,gds + sizeof(gds) / sizeof(gds[0]),[](const Goods& g1,const Goods& g2)->bool
	{return g1._price > g2._price; });

	sort(gds,const Goods& g2)->bool
	{return g1._appraise > g2._appraise; });

	return 0;
}

上面的例子就體現了其現做現用的特性。

4、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);
// 仿函式
auto r2 = [=](double monty,int year)->double{return monty*rate*year; };
r2(10000,2);
return 0; }

函式物件將rate作為其成員變數,在定義物件時候給出初始值即可,lambda表示式通過捕獲列表直接捕獲該變數.

在這裡插入圖片描述
在這裡插入圖片描述

通過上面的圖可以看出,實際在底層編譯器對於處理lambda表示式的處理方式,完全就是按照函式物件的方式處理的,即:如果定義了一個lambda表示式,編譯器會自動生成一個類,在該類中過載了operator();
並且編譯器是通過lambda_+uuid來唯一辨識一個lambda表示式的
九、執行緒庫
C++11中引入了執行緒庫,使得在C++在並行程式設計時可以不需要依賴第三方庫,而且在原子操作中引入了原子類的概念。
要使用標準庫中的執行緒,必須包含標頭檔案

#include<iostream>
#include<thread>

void fun()
{
	std::cout << "A new thread!" << std::endl;
}
int main()
{
	std::thread t(fun);
	t.join();
	std::cout << "Main thread!" << std::endl;
	system("pause");
	return 0;
}

在這裡插入圖片描述
9.1 執行緒的啟動
C++執行緒庫通過構造一個執行緒物件來啟動一個執行緒,該執行緒物件中包含了執行緒執行時的上下文環境,如:執行緒函式、執行緒棧、執行緒其實狀態、以及執行緒ID等,把所有操作全部封裝在一起,在同一傳遞給_beginthreadex()建立執行緒函式來實現(_beginthreadex是windows中建立執行緒的底層c函式)
std::thread()建立一個新的執行緒可以接受任意的可呼叫物件型別,包括lambda表示式,函式,函式物件,函式指標

// 使用lambda表示式作為執行緒函式建立執行緒
int main()
{
	int n1 = 1;
	int n2 = 2;
	std::thread t([&](int addNum){n1 += addNum; n2 += addNum; },3);
	t.join();
	std::cout << n1 << " " << n2 << std:: endl;
	system("pause");
	return 0;
}

9.1 執行緒的結束
啟動一個執行緒後,當執行緒執行完畢時,如果護手執行緒使用的資源,thread庫提供了兩種選擇。
1、join()
join():會主動等待執行緒終止,在呼叫程序中join(),當新的執行緒終止時,join()會清理相關的資源,然後返回,呼叫執行緒在繼續向下執行。由於join()清理了執行緒的相關資源,thread物件與已銷燬的執行緒就沒有關係了,因此一個執行緒物件每次只能join()一次,如果多次呼叫join(),joinable()會返回false;

int main()
{
	int n1 = 1;
	int n2 = 2;
	std::thread t([&](int addNum){n1 += addNum; n2 += addNum; },3);
	std::cout << "join before,joinable=" << t.joinable() << std::endl;
	t.join();
	std::cout << "join after,joinable=" << t.joinable() << std::endl;
	system("pause");
	return 0;
}
// 執行結果:
join before,joinable=1
join after,joinable=0

2、detach()
detach:會從呼叫執行緒中分離出新的執行緒,之後不能再與新執行緒互動。這是呼叫joinable()會返回false。分離的執行緒會在後臺執行,其所有權和控制權會交給C++執行庫。C++執行庫會保證線上程退出時,其相關資源能正確回收。

int main()
{
	int n1 = 1;
	int n2 = 2;
	std::thread t([&](int addNum){n1 += addNum; n2 += addNum; },joinable=" << t.joinable() << std::endl;
	t.detach();
	std::cout << "join after,joinable=" << t.joinable() << std::endl;
	system("pause");
	return 0;
}

注意,必須在thread物件銷燬之前作出選擇,因為執行緒在join()或detach()之前,就可能已經結束,如果之後在分離,執行緒可能會在thread物件銷燬之後繼續執行。

9.3 原子性操作庫
多執行緒最主要的問題是共享資料帶來的問題(執行緒安全)。如果資料都是隻讀的,沒有問題,因為只讀不會影響資料,不會涉及資料的修改,所有執行緒都會獲得同樣的資料。但是,當多個執行緒要修改資料時,就會產生很多潛在的麻煩。

int sum = 0;

void fun(size_t num)
{
	for (size_t i = 0; i < num; i++)
		sum++;
}

int main()
{
	std::cout << "before,sum=" << sum << std::endl;
	std::thread t1(fun,100000000);
	std::thread t2(fun,100000000);
	t1.join();
	t2.join();
	std::cout << "After,sum=" << sum << std::endl;
	system("pause");
	return 0;
}

當fun的引數比較大時,就會產生和預期不相符的結果.
在C++98中可以通過加鎖來保護共享資料。

int sum = 0;
std::mutex m;

void fun(size_t num)
{
	for (size_t i = 0; i < num; i++)
	{
		m.lock();
		sum++;
		m.unlock();
	}
}

雖然加鎖結果了這個問題:但是它有一個缺陷:只要有一個執行緒在對sum++的時候,其它執行緒就會阻塞,會影響程式執行的效率,而且鎖如果控制不好,或導致思索的問題。
因此在C++11中引入了原子操作。對應於內建的資料型別,原子資料型別都有一份對應的型別。

在這裡插入圖片描述

要使用以上的原子操作,需要新增標頭檔案

#include<thread>
#include<mutex>
#include<atomic>

std::atomic_int sum{ 0 };

void fun(size_t num)
{
	for (size_t i = 0; i < num; i++)
	{
		sum ++; // 原子的
	}
}

int main()
{
	std::cout << "before,10000000);
	std::thread t2(fun,10000000);
	t1.join();
	t2.join();
	std::cout << "After,sum=" << sum << std::endl;
	system("pause");
	return 0;
}

到此這篇關於詳解C++之C++11的牛逼特性的文章就介紹到這了,更多相關C++11特性內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!