1.類型推導的語法和規則
C++11提供了auto和decltype來靜態推導類型。
1.1 auto 類型推導
C++11 賦予 auto 關鍵字新的含義,使用它來做自動類型推導。也就是說,使用了 auto 關鍵字以后,編譯器會在編譯期間自動推導出變量的類型,這樣我們就不用手動指明變量的數據類型了。auto 是一個占位符,在編譯器期間它會被真正的類型所替代。
auto 關鍵字基本的使用語法如下:
auto name = value;
name 是變量的名字,value 是變量的初始值。
auto推導示例:
1.auto a = 5; // a為int,auto推導為int
auto b = 1.5; // b為double,auto推導為double
auto c = "Hello World"
// auto推導為const char*
2.int n = 1;
auto *p1 = &x; //p1為int*,auto推導為int
auto p2 = &x; //p2為int*,auto推導為int*
auto &r1 = x; //r1為int&,auto推導為int
auto r2 = r1; //r2為int,auto推導為int
3.int x = 0;
const auto c1 = x; //c1為const int,auto被推導為int
auto c2 = c1; //c2為const int,auto被推導為int(const 屬性被拋棄)
const auto &c3 = x; //c3為const int& 類型,auto被推導為int
auto &c4 = c3; //c4為const int& 類型,auto被推導為const int類型
//使用auto定義迭代器
4.for(vector<int>::iterator it = v.begin(); it != v.end(); ++it);
//等價於
for(auto it = v.begin(); it != v.end(); ++it);
1.2 decltype類型推導
decltype是C++ 11新增的一個關鍵字,它和auto的功能一樣,都用來在編譯時期進行自動類型推導,decltype主要用於獲取一個表達式的類型,decltype關鍵字基本的使用語法如下:
decltype(exp) varname;
varname 表示變量名,exp 表示一個表達式。
原則上講,exp 就是一個普通的表達式,它可以是任意復雜的形式,但是我們必須要保證 exp 的結果是有類型的,不能是 void;例如,當 exp 調用一個返回值類型為 void 的函數時,exp 的結果也是 void 類型,此時就會導致編譯錯誤。
decltype(exp)推導規則如下:
-
如果 exp 是一個不被括號
( )包圍的表達式,或者是一個類成員訪問表達式,或者是一個單獨的變量,那么 decltype(exp) 的類型就和 exp 一致,這是最普遍最常見的情況。 -
如果 exp 是函數調用,那么 decltype(exp) 的類型就和函數返回值的類型一致。
-
如果以上兩點都不是,且exp 是一個左值(lvalue),那么 decltype(exp) 的類型就是 T&。
decltype推導示例:
1.int a = 1, double b = 1.5;
decltype(a) c = 2; //c被推導成了int
decltype(b) d = 5.5; //d被推導成了double
decltype((a)) e = c; //e為int&,因為a是左值
2.基於范圍的for循環
C++11中,為for循環添加了一種全新的語法格式,如下所示:
for(declaration : expression{
//循環體
}
declaration為定義的變量,該變量的類型為要遍歷序列中變量的類型,可以用auto關鍵字表示;express為要遍歷的序列。
使用示例
vector<int> nums;
for(int num: nums);
for(auto n: nums);
3.右值引用和移動構造函數
3.1 右值引用
C++ 11 標准之前,不允許修改右值,這就產生一個問題,實際開發中我們可能需要對右值進行修改(實現移動語義時就需要),為此,C++ 11標准引入了一種新的引用方式,稱為右值引用,用&&表示。右值引用允許對右值進行修改,且只能用右值引用進行初始化,例如:
1.int a = 1;
int && b = 1;
int && c = a; //error,只能用右值初始化右值引用
2.int && x = 2;
x = 5;
cout << x << endl; //結果為5
3.2 移動構造函數
C++ 11 標准之前,如果想用其它對象初始化一個同類的新對象,只能借助類中的拷貝構造函數。C++ 11引入了右值引用的語法,借助它可以實現移動語義。
移動語義就是以移動而非深拷貝的方式初始化含有指針成員的類對象。對於程序執行過程中產生的臨時對象,往往只用於傳遞數據(沒有其它的用處),並且會很快會被銷毀。因此在使用臨時對象初始化新對象時,我們可以將其包含的指針成員指向的內存資源直接移給新對象所有,無需再新拷貝一份,這大大提高了初始化的執行效率。
注意:當類中同時包含拷貝構造函數和移動構造函數時,如果使用臨時對象初始化當前類的對象,編譯器會優先調用移動構造函數來完成此操作。只有當類中沒有合適的移動構造函數時,編譯器才會調用拷貝構造函數。
默認情況下,左值初始化同類對象只能通過拷貝構造函數完成,如果想調用移動構造函數,則必須使用右值進行初始化。但可以使用move函數,它可以將左值強制轉換成對應的右值,由此便可以使用移動構造函數。
移動構造函數示例:
#include <iostream>
using namespace std;
class Student
{
public:
//構造函數
Student():num(new int(0))
{
cout<<"構造函數"<<endl;
}
//拷貝構造函數
Student(const Student &stu):num(new int(*stu.num))
{
cout<<"拷貝構造函數"<<endl;
}
//移動構造函數
Student(Student &&stu):num(stu.num)
{
stu.num = nullptr;
cout<<"移動構造函數"<<endl;
}
~Student()
{
cout<<"析構函數"<<endl;
}
private:
int *num;
};
int main()
{
Student stu1 = Student();
Student stu2 = stu1;
Student stu3 = move(stu1);
//運行結果為:
/*
構造函數
拷貝構造函數
移動構造函數
析構函數
析構函數
析構函數
*/
return 0;
}
4.初始化列表
C++11統一了初始化列表的使用,可以直接在變量名后面跟上初始化列表,也可以用於任何類型對象的初始化,來進行對象的初始化。例如:
A a1 = A{1, 2};
A a2{5, 10};
5.constexpr關鍵字
在開發中我們經常會用到常量表達式,如下所示:
1.int a[10];
2.int N = 5;
int b[N]; //運行時error,編譯器認為N是變量
常量表達式和非常量表達式的計算時間是不一樣的,非常量表達式只能在程序運行階段計算出結果;而常量表達式的計算往往發生在程序的編譯階段,這可以極大提高程序的執行效率,因為表達式只需要在編譯階段計算一次,節省了每次程序運行時都需要計算一次的時間。所以希望在編譯階段即可判斷一個表達式是不是常量表達式。
C++ 11新引入了constexpr關鍵字,constexpr修飾的變量是一個編譯期常量,修飾的函數是一個編譯器常量函數表達式。但是constexpr不能修飾自定義類型。例如:
1.constexpr int N = 5;
int a[N]
2.constexpr int func(int x)
{
return x;
}
在C++ 11中,const關鍵字保留了“只讀”的屬性,而將“常量”的屬性分給了constexpr。所以一般建議凡是表達“只讀”語義的場景都使用 const,表達“常量”語義的場景都使用 constexpr。
6.nullptr空指針
實際開發中,避免產生“野指針”最有效的方法,就是在定義指針的同時完成初始化操作,即便該指針的指向尚未明確,也要將其初始化為空指針。C++ 11之前,一般初始化為0或者NULL。NULL是實現定義好的一個宏(#define NULL 0),使用NULL會引發如下問題:
void func(int n);
void func(char* c);
上述示例中,如果調用func(0),和func(NULL),實際都會調用到func(int n)。
C++ 11引入了nullptr關鍵字,nullptr 是 nullptr_t 類型的右值常量,專用於初始化空類型指針。nullptr 可以被隱式轉換成任意的指針類型,例如:
int* p1 = nullptr;
double* p2 = nullptr;
char* p3 = nullptr;
7.final和override
C++11新增了final和override關鍵字,功能如下:
final關鍵字
-
將類標記為final,表示該類不可被繼承
-
將方法標記為final,表示該方法不可被重寫
override關鍵字
-
將方法標記為override,表示該方法需要被重寫
使用示例:
class B1 final(){};
class D1 : B1{}; //error,B1不可被繼承
class B2
{
virtual void func1(int);
virtual void func2(int) final;
}
class D2 : B2
{
virtual void func1(int, char) override; //error,需要重寫父類方法,但是父類中無該方法
virtual void func2(int); //error,func2不可被重寫
}
8.tuple元組
C++11 標准新引入了一種類模板,命名為 tuple,tuple 最大的特點是:實例化的對象可以存儲任意數量、任意類型的數據。
STL中有pair類模板,該模板可以定義二元組,而tuple可以定義多元組,當需要存儲多個不同類型的元素時,可以使用 tuple;當函數需要返回多個數據時,可以將這些數據存儲在 tuple 中,函數只需返回一個 tuple 對象即可。
tuple的部分初始化方式如下所示:
tuple <string, int> tuple1;
tuple <string, double, int> tuple2("tuple2", 2.5, 5);
tuple <string, double, int> tuple3(tuple2);
// make_tuple()函數用於創建一個tuple右值對象
tuple <string, string, int> tuple4(make_tuple("tuple4", "test4", 1));
9.Lambda表達式
Lambda表達式主要提供了匿名函數的特性,利用Lamdba表達式可以編寫內嵌的匿名函數,用以替換獨立函數或者函數對象,Lambda表達式的語法格式如下:
[capture-list] (params) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }
[ ] 方括號用於向編譯器表明當前是一個 Lambda 表達式,每當定義一個Lambda表達式后,編譯器會自動生成一個匿名類,這個被稱為閉包類型(closure type)。那么在運行時,這個Lambda表達式就會返回一個匿名的閉包實例,其實是一個右值。
表達式中的參數
-
capture - list:捕捉列表,閉包的一個強大之處是其可以通過傳值或者引用的方式捕捉其封裝作用域內的變量,該參數為空時,表示沒有捕捉任何變量;
-
params:參數列表,可以省略(但是后面必須緊跟函數體);
-
mutable:可選,將lambda表達式標記為mutable后,函數體就可以修改傳值方式捕獲的變量;
-
constexpr:可選,C++17,可以指定lambda表達式是一個常量函數;
-
exception:可選,指定lambda表達式可以拋出的異常;
-
attribute:可選,指定lambda表達式的特性;
-
ret:可選,返回值類型;
-
body:函數執行體。
捕獲的方式可以是引用也可以是復制,但是具體說來會有以下幾種情況來捕獲其所在作用域中的變量:
-
[]:默認不捕獲任何變量;
-
[=]:默認以值捕獲所有變量;
-
[&]:默認以引用捕獲所有變量;
-
[x]:僅以值捕獲x,其它變量不捕獲;
-
[&x]:僅以引用捕獲x,其它變量不捕獲;
-
[=, &x]:默認以值捕獲所有變量,但是x是例外,通過引用捕獲;
-
[&, x]:默認以引用捕獲所有變量,但是x是例外,通過值捕獲;
-
[this]:通過引用捕獲當前對象(其實是復制指針);
-
[*this]:通過傳值方式捕獲當前對象;
Lambda表達式的使用示例如下:
int main()
{
int x = 10;
auto add_x = [x](int a) { return a + x; }; // 復制捕捉x
auto multiply_x = [&x](int a) { return a * x; }; // 引用捕捉x
cout << add_x(10) << " " << multiply_x(10) << endl;
// 輸出:20 100
return 0;
}
Lambda表達式與普通函數的使用區別示例如下:
/*Lambda表達式使用*/
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//對 a 數組中的元素進行排序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num)
{
cout << n << " ";
}
return 0;
}
/*普通函數使用*/
#include <iostream>
#include <algorithm>
using namespace std;
//自定義的升序排序規則
bool sort_up(int x,int y)
{
return x < y;
}
int main()
{
int num[4] = {4, 2, 3, 1};
//對 a 數組中的元素進行排序
sort(num, num+4, sort_up);
for(int n : num)
{
cout << n << " ";
}
return 0;
}
//輸出結果:1 2 3 4
10.智能指針
C++98中,支持使用 auto_ptr 智能指針來實現堆內存的自動回收,如下所示
auto_ptr<string> p1 (new string ("Hello World"));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不會報錯
此時不會報錯,p2剝奪了p1的所有權,但是當程序運行時訪問p1將會報錯。所以auto_ptr的缺點是:存在潛在的內存崩潰問題!
C++11 廢棄了auto_ptr,增添了 unique_ptr、shared_ptr 以及 weak_ptr 這 3 個智能指針來實現堆內存的自動回收。
10.1 unique_ptr
unique_ptr獨占式擁有或嚴格擁有概念,保證同一時間只有一個智能指針可以指向該對象。
unique_ptr<string> p3 (new string ("I reigned lonely as a cloud."));
unique_ptr<string> p4;
p4 = p3; // 會報錯!
編譯器認為p4=p3非法,避免了p3不再指向有效數據的問題,因此,unique_ptr比auto_ptr更安全。另外unique_ptr還有更聰明的地方:當程序試圖將一個unique_ptr賦值給另一個時,如果unique_ptr是個臨時右值,編譯器允許這么做;如果unique_ptr將存在一段時間,編譯器將禁止這么做。成員函數有:
-
operator=():重載了 = 賦值號,從而可以將 nullptr 或者一個右值 unique_ptr 指針直接賦值給當前同類型的 unique_ptr 指針。
-
operator*():獲取當前 unique_ptr 智能指針對象指向的數據。
-
operator->(): 重載 -> 號,當智能指針指向的數據類型為自定義的結構體時,通過 -> 運算符可以獲取其內部的指定成員。
-
operator :重載了 [] 運算符,當 unique_ptr 指針指向一個數組時,可以直接通過 [] 獲取指定下標位置處的數據。
-
operator bool():unique_ptr 指針可直接作為 if 語句的判斷條件,以判斷該指針是否為空,如果為空,則為 false;反之為 true。
-
get(): 獲取當前 unique_ptr 指針內部包含的普通指針。
-
get_deleter():獲取當前 unique_ptr 指針釋放堆內存空間所用的規則。
-
release(): 釋放當前 unique_ptr 指針對所指堆內存的所有權,但該存儲空間並不會被銷毀。
-
reset(p): 其中 p 表示一個普通指針,如果 p 為 nullptr,則當前 unique_ptr 也變成空指針;反之,則該函數會釋放當前 unique_ptr 指針指向的堆內存(如果有),然后獲取 p 所指堆內存的所有權(p 為 nullptr)。
-
swap(x):交換當前 unique_ptr 指針和同類型的 x 指針。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
std::unique_ptr<int> p5(new int);
*p5 = 10;
// p 接收 p5 釋放的堆內存
int * p = p5.release();
cout << *p << endl;
//判斷 p5 是否為空指針
if (p5)
{
cout << "p5 is not nullptr" << endl;
}
else
{
cout << "p5 is nullptr" << endl;
}
std::unique_ptr<int> p6;
//p6 獲取 p 的所有權
p6.reset(p);
cout << *p6 << endl;;
return 0;
}
/*輸出結果
10
p5 is nullptr
10
*/
10.2 shared_ptr
shared_ptr實現共享式有用概念。多個智能指針可以指向相同對象,shared_prt是強引用,該類型智能指針在實現上采用的是引用計數機制,即便有一個 shared_ptr 指針放棄了堆內存的“使用權”(引用計數減 1),也不會影響其他指向同一堆內存的 shared_ptr 指針(只有引用計數為 0 時,堆內存才會被自動釋放。
shared_ptr是為了解決auto_ptr在對象所有權上的局限性(auto_ptr是獨占的),在使用引用計數的機制上提供了可以共享所有權的智能指針。成員函數有:
-
operator=():重載賦值號,使得同一類型的 shared_ptr 智能指針可以相互賦值。
-
operator*():獲取當前 shared_ptr 智能指針對象指向的數據。
-
operator->(): 重載 -> 號,當智能指針指向的數據類型為自定義的結構體時,通過 -> 運算符可以獲取其內部的指定成員。
-
operator bool():判斷當前 shared_ptr 對象是否為空智能指針,如果是空指針,返回 false;反之,返回 true。
-
use_count():返回引用計數的個數。
-
unique():返回是否獨占所有權(use_count為1)。
-
swap():交換兩個shared_ptr對象(即交換所擁有的對象)。
-
reset():放棄內部對象的所有權或擁有對象的變更,會引起原有對象的引用計數的減少。
-
get():返回內部對象(指針),由於已經重載了()方法,因此和直接使用對象是一樣的。如shared_ptr<int> sp(new int(1));sp與sp.get()是等價的。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//構建 2 個智能指針
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//輸出 p2 指向的數據
cout << *p2 << endl;
p1.reset(); //引用計數減 1,p1為空指針
if (p1)
{
cout << "p1 不為空" << endl;
}
else
{
cout << "p1 為空" << endl;
}
//以上操作,並不會影響 p2
cout << *p2 << endl;
//判斷當前和 p2 同指向的智能指針有多少個
cout << p2.use_count() << endl;
return 0;
}
/*輸出結果
10
p1為空
10
1
*/
shared_ptr的線程安全性:
-
同一個shared_ptr對象可以被多線程同時讀取;
-
不同的shared_ptr對象可以被多線程同時修改;
-
同一個shared_ptr對象不能被多線程直接修改,需要加鎖
10.3 weak_ptr
weak_ptr是一種不控制對象生命周期的智能指針,它指向一個shared_ptr的管理對象,C++11標准雖然將 weak_ptr 定位為智能指針的一種,但該類型指針通常不單獨使用(沒有實際用處),只能和 shared_ptr 類型指針搭配使用。甚至於,我們可以將 weak_ptr 類型指針視為 shared_ptr 指針的一種輔助工具,進行該對象的內存管理的,是那個強引用的shared_ptr。weak_ptr只是提供了對管理對象的一個訪問手段,它的構造和析構不會引起引用計數的增加或減少。
weak_ptr是用來解決shared_ptr相互引用時的死鎖以及內存泄露問題,如果說兩個shared_ptr相互引用,那么這兩個指針的引用計數永遠不可能下降為0,資源永遠不會釋放。
weak_ptr成員函數:
-
operator=():重載 = 賦值運算符,是的 weak_ptr 指針可以直接被 weak_ptr 或者 shared_ptr 類型指針賦值。
-
swap(x):其中 x 表示一個同類型的 weak_ptr 類型指針,該函數可以互換 2 個同類型 weak_ptr 指針的內容。
-
reset():將當前 weak_ptr 指針置為空指針。
-
use_count(): 查看指向和當前 weak_ptr 指針相同的 shared_ptr 指針的數量。
-
expired():判斷當前 weak_ptr 指針為否過期(指針為空,或者指向的堆內存已經被釋放)。
-
lock():如果當前 weak_ptr 已經過期,則該函數會返回一個空的 shared_ptr 指針;反之,該函數返回一個和當前 weak_ptr 指向相同的 shared_ptr 指針。
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete" << endl;
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; // 輸出 2
cout << pa.use_count() << endl; // 輸出 2
}
int main()
{
fun();
return 0;
}
上述程序中,可以看到fun函數中pa,pb相互引用,兩個智能指針都是shared_ptr類型,兩個資源的引用計數為2,當要跳出函數時,智能指針pa、pb析構兩個資源引用計數會減1,但是兩者引用計數還是為1。導致跳出函數時資源沒有被釋放,所以A,B的析構函數沒有被調用。解決方法:把其中一個改為weak_ptr,如下所示:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
weak_ptr<B> pb_;
~A()
{
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete" << endl;
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; // 輸出 2
cout << pa.use_count() << endl; // 輸出 2
}
int main()
{
fun();
return 0;
}
把類A里面的shared_ptr pb改為weak_ptr pb,這樣的話,資源B的引用開始就只有1,當pb析構時,B的計數變為0,B得到釋放,B釋放的同時也會使A的計數減1,同時pa析構使A的計數減一,那么A的計數為 0,A得到釋放。
此外,C++ 11還在STL中增加了無序關聯式容器,這將在其他的文章中討論。
