C++11新特性總結 (二)

薰衣草的旋律發表於2016-07-18

1. 範圍for語句

C++11 引入了一種更為簡單的for語句,這種for語句可以很方便的遍歷容器或其他序列的所有元素

vector<int> vec = {1,2,3,4,5,6};
for(int x: vec)
{
    cout<<x<<endl;
}

2. 尾置返回型別

要想引入尾置型別,我們還得從複雜的型別宣告說起。如果我們需要定義一個含有10個int元素的陣列,一般是這樣的:

int arr[10] = {0};

如果要定義指向這個陣列的指標呢:

int (*p_arr)[10] = &arr; //注意:int *p_arr[10] 表示一個陣列,有10個元素,元素型別是int*

如果要定義一個函式,這個函式接受一個char型別的引數,並返回一個指向10個int型別陣列的指標呢:

int (*func(char x))[10];

這樣的宣告是不是看的頭都大了,其實我們可以簡化一點,一般情況下我們可以使用別名進行簡化,比如:

typedef int ARR[10] ; // 定義一個型別 ARR這是一個陣列型別,含有10個int型別的元素
using ARR = int[10] ; // 同上

再定義如上的函式:

ARR * func(char x) ; // 函式返回的型別是 ARR* 也就是指向具有10個int元素陣列的指標

當然在C++11中我們可以用之前講到過的另外一個關鍵字decltype:

decltype(arr) * func(char x) ; // decltype(arr)表示式會獲得arr的型別

最後就輪到我們本節要說的C++11的另外一個特性,尾置返回型別,任何函式都可以使用尾置返回型別,這種形式對於返回型別比較複雜的函式最有效,比如上面的函式可以使用如下方式:

auto func(char x) -> int(*) [10]; 

這種形式將函式的返回型別寫在函式宣告的最後面,並且在函式形參列表後面加上 -> 符號,然後緊接著是函式需要返回的型別,由於函式的返回型別被放在了形參列表之後,所以在函式名前面使用一個 auto替代。

3. =default 生成預設建構函式

在C++的類中,如果我們沒有定義建構函式,編譯器會為我們合成預設的無參建構函式,如果我們定義了建構函式,則編譯器就不生成預設建構函式了,但是如果我們定義建構函式同時也希望編譯器生成預設建構函式呢? C++11中可以通過在建構函式的宣告中直接  =default的方式要求編譯器生成建構函式。

class ClassName{
	public:
		ClassName(int x);
		ClassName()=default; // 顯示要求編譯器生成建構函式
};

4. 類物件成員的類內初始化

class ClassName
{
        public:
                int x = 10; //C++11 之前是不允許的
};

5. lambda表示式與bind函式

lambda表示式是一個可以被呼叫的程式碼單元,相當於一個行內函數,有引數和返回值以及函式體。但是跟函式不同的是,lambda表示式可以定義在函式的內部,一個完整的lambda表示式具有如下形式:

[捕獲列表](引數列表) mutable -> 返回型別 {函式體}

int x = 10;
int y = 20;
auto f = [x,&y](int a ,int b){++y;return a+b+x+y;};
cout<<f(1,2)<<endl; //34
cout<<y<<endl;      //21

lambda可以省略引數列表(如果沒有引數的話),可以省略返回型別,但是不能省略捕獲部分與函式體部分,即使捕獲列表為空,也要有一個空的[],lambda有兩種捕獲,一種是值捕獲,一種是引用捕獲。如果是值捕獲那麼lambda中獲得的是捕獲的變數的副本,如果是引用捕獲則獲得的是引用,可以在lambda內部修改引用的變數的值,如上x是值捕獲,y是引用捕獲,lambda中預設是值捕獲,如果變數前面新增&則是引用捕獲,另外lambda中還有兩種形式的引用捕獲,例如[=]表示值捕獲所有可見的變數,而[&]則表示引用捕獲所有可見變數。如果希望值捕獲所有可見變數,但是又有個別變數採用引用捕獲呢,[=,&x]表示值捕獲所有可見變數,同時引用捕獲x。而[&,x]則表示引用捕獲所有可見變數,x採用值捕獲的方式。

有關bind函式,在很多地方我們可以使用函式替換lambda表示式,畢竟如果很多地方需要用到同一個lambda表示式,而且這個lambda表示式比較長的話,將其定義成函式應該是最好的。對於沒有捕獲列表的lambda表示式我們可以直接使用函式替代,例如:

void main()
{
	auto f=[](int x,int y){return x+y};
	f();
}

我們可以用下面的方式替代:

int f(int x,int y)
{
   return x+y;
}

void main()
{
	f();
}

與上面的lambda是等價的,但是對於有捕獲列表的lambda表示式應該怎麼處理呢,例如:

void main()
{
	int x = 10;
	int y = 20;
	auto f = [x,&y](int a ,int b){return a+b+x+y;}; //一個值捕獲,一個引用捕獲
	f(33,44);
}

如果轉換成函式的形式:

int x = 10;
int y = 20;
int f(int a,int b)
{
  return a+bx+y;
}

void main()
{
	f(33,44);
}

這是一種可行的方法,但是總不能把所有的捕獲變數定義成全域性變數吧。現在的關鍵問題是lambda的捕獲表示式中的內容轉換成函式不可行,C++11提供了bind函式來完成這樣的操作。

#include <functional> //bind()
#include <iostream>
using namespace std;
using namespace std::placeholders; // _1,_2所在的名稱空間
int f(int x,int y,int a,int b)
{
  return a+b+x+y;
}

void main()
{
  int x = 10;
  int y = 20;

  auto f_wrap = bind(f,x,y,_1,_2);
  cout<<f_wrap(33,44)<<endl; // _1,_2是佔位符,表示呼叫f_wrap的時候_1是第一個引數,_2是第二個引數。最終會被替換成呼叫  f(10,20,33,44)
}

如果引用型別的捕獲怎麼做呢,看下面的例子,用lambda是這樣的:

#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
void main()
{
  int x = 10;
  ostream &o = cout;
  auto f =[&o](int a){o<<a<<endl;}; // 注意這裡的輸出物件是用的引用捕獲
  f(x);
}

使用bind是這樣的:

#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
void f(ostream &o,int x)
{
  o<<x<<endl;
}
int main()
{
  int x = 10;
  auto f_wrap = bind(f,ref(cout),_1); //將變數的引用傳遞到bind中是個問題,為此C++11提供了ref()函式用於獲得引用
  f_wrap(x);
  return 0 ;
}

6. 智慧指標share_ptr,unique_ptr

C++11中引入了幾種智慧指標,智慧指標能夠自動釋放所指向的物件,其中shared_ptr允許多個指標指向同一個物件,unique_ptr則獨佔所指向的物件,我們主要說明shared_ptr的使用。通過使用make_shared<type>()函式產生智慧指標物件。

shared_ptr<int> p = make_shared<int>(40); // p指向一個值為40的int物件
shared_ptr<string> p2 = make_shared<string>(10,'c'); //指向值為'cccccccccc'的string物件

make_shared<type>()函式中傳遞的值要與對應的type的建構函式相匹配,實際上應該就是直接呼叫的對應type的建構函式。

我們可以使用new初始化的指標來初始化智慧指標:

share_ptr<int> p (new int(40));
p.get(); // 使用share_ptr<type>的get()函式來獲得其關聯的原始指標。

shared_ptr物件在離開其作用域(例如一個函式體),會自動釋放其關聯的指標指向的動態記憶體,就像區域性變數那樣。另外多個shared_ptr可以指向一個物件,當最後一個shared_ptr物件銷燬的時候才會銷燬其關聯的那個物件的動態記憶體。這裡使用了引用記數。

7. 右值引用與move呼叫,移動建構函式

為了支援移動操作,C++11中使用了一種稱為右值引用的型別。移動操作是什麼呢,一般情況下我們將一個物件賦值給另一個物件的時候會呼叫物件到拷貝建構函式或者拷貝賦值運算子。而移動建構函式和移動賦值運算子則用來將資料從一個物件移動到另一個物件。在很多情況下物件拷貝之後需要被銷燬,此時使用移動操作會大幅提高效能。右值引用被使用之後,其關聯的物件會被銷燬。右值引用使用兩個&&表示,例如 int && 表示右值引用,而int &則是左值。通過C++11標準庫提供的函式 std::move()可以得到某一物件的右值引用。

TestClass::TestClass(TestClass &&t) noexcept //移動建構函式不應該丟擲任何異常
:x(t.x),y(t.y),z(t.z),p(t.p)
{
  t.p=nullptr; // 實現移動操作之後需要保證被移動的物件的析構是安全的,所以源物件的指標成員置為空,隨後t的解構函式將會被自動呼叫,t被銷燬。
}

8. function

C++11中提供了名為function的標準庫型別,定義在標頭檔案<functional>中,該型別用來儲存一個可呼叫的物件,統一了所有可以像函式一樣呼叫的呼叫形式,例如:

#include <functional>
#include <iostream>
using namespace std;

int add(int x,int y)
{
	return x+y;
}

class Add
{
	public:
		int operator()(int x,int y)
		{
			return x+y;
		}
};

void main()
{
  //function模版的引數是函式呼叫形式,包括返回型別,以及引數列表個數和型別
	function<int(int,int)> f1 = add;  //函式指標
	function<int(int,int)> f2 = Add(); // 可呼叫類物件
	function<int(int,int)> f3 = [](int x,int y){return x+y;}; //lambda表示式
	
	cout<<f1(10,20)<<" "<<f2(30,40)<<" "<<f3(50,60)<<endl;
}

9. 其他新增型別(array,forward_list,bitset,regex)

實際上C++11中除了一些語法特性新增之外,還增加了一些新的庫。例如array相當於我們傳統使用的定長陣列,支援隨機訪問,不能新增刪除元素,不可以像vector一樣增長,使用array比傳統陣列更加安全。

forward_list是C++11中增加的單向連結串列

regex則是C++11中新增的正規表示式庫

10. 總結

C++11中增加了一些庫,庫本身的使用不做過多介紹,可以參考C++標準庫/STL原始碼剖析,這都是用單獨的大部頭書籍講解的,有些特性和庫是我感覺比較驚豔的,例如:

auto 定義:可以讓編譯器自動推算定義的變數型別,而不需要寫長長的一串型別,特別是在含有迭代器的型別上。
decltype :可以根據已知的變數來定義一個跟該變數一樣的型別。
lambda:個人認為這是C++11中增加的最驚豔的特性之一,對應的還有bind()函式,其實這些內容是從Boost中來的。
智慧指標:shared_ptr 雖然在以前的C++中有類似auto_ptr的智慧指標,但是auto_ptr有一些缺陷並不算很好用。
function型別:標準庫的function型別定義了一種可呼叫物件,該型別統一了所有可以當作函式一樣呼叫的呼叫形式,例如lambda,函式指標,過載了函式呼叫運算子()的類物件等,該特性也是參考了Boost庫。
regex庫:C++11中總算有了方便的regex可以使用了。

 

C++11新特性總結 (一)

 

如果您覺得這篇文章對您有幫助,需要您的【贊】,讓更多的人也能看見哦

 

相關文章