目錄
lambda表達式
定義
捕捉的方式
可變模板參數
遞歸函數方式展開參數包
數組展開參數包
移動構造和移動賦值
包裝器
綁定bind
智能指針
RAII
auto_ptr
unique_ptr
循環引用
weak_ptr
補充
總結
特殊類的設計
不能被拷貝的類
只能在堆上創建類
將構造函數私有
將析構函數私有
只能在棧上創建對象
?只能創建一個對象(單例模式)
餓漢模式
懶漢模式
單例對象的釋放
本文是對上一篇文章《C++11新特性》的補充,將會介紹過很多的C++11特性。
C++11新特性-CSDN博客文章瀏覽閱讀987次,點贊30次,收藏30次。本文對C++11重要特性進行介紹,重點解析了C++中的左值引用和右值引用的區別,幫助讀者快速了解C++11新特性以及這些特性的使用方法。https://blog.csdn.net/2401_87944878/article/details/147116766
lambda表達式
定義
C++中可調用對象有三種:函數,仿函數,以及lambda表達式。
函數的類型寫起來太繁瑣了,所以一般都會使用仿函數來替代函數,但是對于實現簡單的函數實現,使用仿函數代碼長,比較笨重;因此引出了lambda表達式來解決這一問題。
lambda表達式包含三個部分:1)捕捉列表;2)函數參數;3)函數返回值類型;4)函數體。
struct Add1
{int operator()(int x, int y){return x + y;}
};
auto Add2 = [](int x, int y)->int {return x + y ;};
Add1()(1, 1);
Add2(1, 1);
以上分別是仿函數和lambda表達式實現加法函數,可以看出lambda表達式的使用更簡約。
對于lambda表達式,如果沒有參數,參數部分可以省略;返回值類型也可以不寫,讓編譯器自動推導。
auto Add3 = [](int x, int y) {return x + y; };
函數指針---在C++中人不用就不用,其類型寫起來不太方便;
仿函數---是一個類,重載了operator();?
lambda---表達本質上是一個局部匿名函數對象。
lambda表達式的函數體內可以直接調用全局變量,但是不能直接調用局部變量或局部函數。如果需要使用局部對象就要將其添加至捕捉列表中。
void test_02()
{double rate = 0.5;auto Add3 = [rate](int x, int y) {return (x + y)*rate; };}
如上圖,rate就實現了捕捉,在lambda表達式中可以只用rate。
捕捉的方式
捕捉的方式有四種:
1)[var],對var進行值捕捉;
2)[&var],對var進行引用捕捉,lambda表達式中var的改變會影響其外部的var;
3)[ = ],對局部變量和函數進行全部值捕捉;
4)[ & ],對局部變量和函數進行全部引用捕捉。
當然捕捉列表也可以混合起來使用。
int a = 1, b = 2;
int c = 10;
auto Add4 = [&, c] { a++, b++;cout << c; };
以上就是混合捕捉:對所有局部變量進行引用捕捉,除了c使用值捕捉。
lambda表達式的底層實際上還是仿函數。
lambda表達式在進行值捕捉的時候,默認捕捉后的類型是const修飾的,也就是說進行值捕捉后的參數是不能進行修改的,如果想要修改需要添加mutable關鍵字。
int a = 1, b = 2;
auto Add4 = [a, b]()mutable { a++, b++;cout << a << b; };
?使用mutable后,值捕捉的變量就就可以實現修改了。
可變模板參數
在C語言中就已經有可變參數的概念了,比如printf和scnaf的參數都屬于可變參數;C++引入了模板概念,自然也有了可變模板參數概念。?
在C++11新增了可變模板參數,使得模板中參數可以不是固定的了,但是可變模板參數使用起來不太方便,此處我們進行簡略介紹。
模板的參數是類型,函數的參數是對象,要將這兩點分開。
template<class ... Args>
void Show(Args ... args)
{sizeof...(args);//....
}
模板中的Args指的是模板參數包,函數中的args指的是函數參數包;其中...表示其是可變參數,此處需要注意省略號的位置;通過sizeof...(args)可以打印出參數的個數。
參數包的語法不支持直接使用args[i],所以參數包是不能直接使用的,需要先展開再使用;
遞歸函數方式展開參數包
通過遞歸依次減少參數,直到參數為零,其與參數遞歸類似,只不過遞歸停止的條件是參數。
template<class T>
void Show(T val)
{cout << val << endl;
}
template<class T ,class ... Args>
void Show(T val ,Args ... args)
{cout << val << " ";Show(args...);
}
void test_03()
{Show(1);Show(1,"x");Show(1, "x", 1.3);
}
如圖,上面通過兩個重載函數來實現參數包的展開,參數大于1會優先匹配void Show(T val ,Args ... args);當參數等于1的時候就去匹配void Show(T val),此時遞歸結束。
數組展開參數包
template<class T>
int Show(T val)
{cout << val << " ";return 0;
}
template<class ... Args>
void Show(Args ... args)
{int arr[] = { Show(args)... };cout << endl;
}
void test_03()
{Show(1);Show(1,"x");Show(1, "x", 1.3);
}
int arr[] = { Show(args)... };數組元素個數是沒有給定的所以需要通過對數組元素進行計數,此時就需要將參數包展開;
移動構造和移動賦值
C++11新增了兩個默認成員函數:移動構造和移動賦值。
移動構造和移動賦值都屬于移動語義,其是為了解決對于返回值的參數無法直接引用問題。
在無析構函數+無拷貝構造+無賦值重載+無移動構造的情況下,編譯器會生成默認移動構造:對于內置類型進行淺拷貝,對于內置類型會去調用其自己的拷貝構造。
移動賦值也同理。
一般要寫析構函數的內是深拷貝的類,需要寫拷貝構造和賦值重載;
包裝器
C++11引入了包裝器function,通過包裝器可以實現將函數,仿函數以及lambda表達式類型統一。
void Print1()
{cout << "hello world" << endl;
}
struct Print2
{void operator()(){cout << "hello world" << endl;}
};
void test_04()
{auto Print3 = [] {cout << "hello world" << endl; };cout << typeid(Print1).name() << endl;cout << typeid(Print2()).name() << endl;cout << typeid(Print3).name() << endl;
}
以上三個可調用對象實現的功能都是一樣的,此處可以使用typeid().name打印出其三個可調用對象的類型。
可以看出三者類型是完全不同的,C++11引入了包裝器function使得可以通過函數返回值,參數將不同的可調用對象類型統一。
function<void()> arr[] = { Print1,Print2(),Print3 };
以上將三個可調用對象都放入到了數組中,其類型都是function<void()>,關于function的使用方法就是:function<函數返回值(函數參數)>,function使用時的頭文件是<functional>。
包裝器function的本質是一個適配器,可以對函數指針,仿函數以及lambda表達式進行包裝。
綁定bind
對于庫中的有些接口,參數傳遞很多并且有些參數是固定的;此時就可以通過bind綁定將接口參數進行綁定。
double RAdd(int x, int y, double rate)
{return (x + y) * rate;
}void test_05()
{//如果rate始終是固定的,不需要進行修改次數就可以使用綁定bindauto RAdd2 = bind(RAdd, placeholders::_1, placeholders::_2, 0.54);cout << RAdd2(10, 4) << endl;
}
bind的參數:可調用對象;參數匹配的位置:10就和_1位置匹配,4就和_2位置匹配,_1和_2有分別和函數參數匹配即x和y;
通過對_1和_2位置的調換,可以出現不同的結果。
如下圖所示:通過對_1和_2位置的調換可能會導致函數的調用發生變化。
?補充:對于類成員函數的綁定是不同的:對于非靜態成員函數要加&,并且要給出對象或對象地址;對于靜態成員函數可以不加&,但是建議加上。
class A
{
public:static int Add1(int x, int y){return x + y;}int Add2(int x, int y){return x + y;}private:int _a;
};void test_05()
{//非靜態成員函數的綁定auto CAdd1 = bind(&A::Add2, A(), placeholders::_1, placeholders::_2);A aa;auto CAdd2 = bind(&A::Add2, &aa, placeholders::_1, placeholders::_2);//靜態成員函數的綁定auto CAdd3 = bind(&A::Add1, placeholders::_1, placeholders::_2);}
智能指針
C++中對于錯誤的處理是使用拋異常的方式解決的,拋異常就會導致進程流被修改,當進程流改變后,原本需要釋放的內存沒有走到delete就會導致內存泄漏。
為了解決這一問題可以采用多次拋異常,在拋異常前進行空間的釋放,如果一個函數中有多個位置進行了空間開辟,當拋異常后還需要考慮哪一個需要釋放,這就導致了情況多,代碼繁瑣的問題。?
能不能一個空間開辟后,出作用域就銷毀呢???此時就可以使用智能指針來實現。
?智能指針通過將指針交給對象,讓對象進行資源的管理,在對象出作用域時調析構函數進行資源的釋放。
template<class T>
class Ptr
{
public:Ptr(T* ptr):_ptr(ptr){}Ptr(const Ptr& p):_ptr(p._ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~Ptr(){delete _ptr;}
private:T* _ptr;
};
以上是對智能指針的簡單模擬實現,但是在使用的時候,對指針進行拷貝構造時就會出現對同一塊區域的多次釋放。在C++中有不同的智能指針,不同的指針處理方式也是不同的。?
RAII
資源獲取即初始化,利用對象生命周期來控制資源,在構造函數時獲取資源,在對象析構時銷毀資源。
優勢:1)不需要顯式釋放空間,出作用域自動銷毀;
2)對象所需的資源在其生命周期類持續有效;
auto_ptr
auto_ptr是C++98時推出的一個智能指針;auto_ptr對于指針拷貝的方法是:將空間的管理權轉移,在完成拷貝構造之后,將被拷貝對象置空。其底層實現如下:
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(const auto_ptr& p):_ptr(p._ptr){p._ptr = nullptr;}auto_ptr operator=(const auto_ptr& p){_ptr = p._ptr;p._ptr = nullptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~auto_ptr(){delete _ptr;}
private:T* _ptr;
};
可以看到,在拷貝完成后,被拷貝對象懸空導致其無法使用,這使得auto_ptr不被廣泛使用。
unique_ptr
unique_ptr對于拷貝和賦值的處理方法更加暴力,unique_ptr不支持賦值和拷貝構造。
所以在實現的時候,需要將拷貝和賦值只聲明不定義,并且置為私有防止其在外面被定義,為防止編譯器自動生成在函數后面加上delete。
template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~unique_ptr(){delete _ptr;}
private:unique_ptr(const unique_ptr& p) = delete;unique_ptr operator=(const unique_ptr& p) = delete;T* _ptr;
};
shared_ptr
shared_ptr通過對指向每個空間的指針進行計數來實現對空間的釋放,當一個指向一個空間的指針數量為0是對空間進行釋放;
所以shared_ptr需要添加一個成員變量來實現計數。
template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(const shared_ptr& p):_ptr(p._ptr),_pcount(p._pcount){++(*_pcount);}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}shared_ptr operator=(const shared_ptr& p){_ptr = p._ptr;_pcount = p._pcount;++(*_pcount);return *this; }~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;}}
private:T* _ptr;int* _pcount;
};
shared_ptr確實解決了何時釋放的問題,但是又出現了循環引用的問題。
循環引用
當一個對象有前后指針的時候就會出現問題,比如一下代碼。
template<class T>
struct Node
{Node():_a(T()),_next(nullptr),_prev(nullptr){ }T _a;shared_ptr<Node> _next;shared_ptr<Node> _prev;
};void test_06()
{shared_ptr<Node<int>> aa(new Node<int>);shared_ptr<Node<int>> bb(new Node<int>);aa->_next = bb;bb->_prev = aa;
}
當aa的后繼指針指向bb的時候,bb的引用計數就變成了2,同理aa的引用計數也編程了2。當對aa進行銷毀的時候,其引用計數是2無法調用Node的析構,也就不能進行_next和_prev的析構了,此時就出現了空間泄露。
為了解決這一問題添加了weak_ptr。
weak_ptr
weak_ptr與shared_ptr的使用是一樣的,只是在對shared_ptr拷貝的時候不會對引用計數進行改變。weak_ptr不遵循RAII,其是專門為解決循環引用而出現的。
template<class T>
class weak_ptr
{
public:weak_ptr(T* ptr):_ptr(ptr){}weak_ptr(const shared_ptr<T>& p):_ptr(p._ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}weak_ptr operator=(const shared_ptr<T>& p){_ptr = p._ptr;return *this;}
private:T* _ptr;
};
上面Node類定義變為:
template<class T>
struct Node
{Node():_a(T()),_next(nullptr),_prev(nullptr){ }T _a;weak_ptr<Node> _next;weak_ptr<Node> _prev;
};
補充
shared_ptr<int> aa(new int[10]);
當我們開辟一個數組的空間時,釋放時再使用delete就不能進行空間釋放了,所以對于shared_ptr和unique_ptr的參數增加了一個:釋放對象。
結合lambda表達式使用起來更加簡單。Del的類型可以使用function表示。
template<class T, class Del=function<void(T*)>>
class shared_ptr
{
public:shared_ptr(T* ptr, Del del = [](T* ptr) {delete ptr; }):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(const shared_ptr<T>& p):_ptr(p._ptr),_pcount(p._pcount){++(*_pcount);}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}shared_ptr operator=(const shared_ptr<T>& p){_ptr = p._ptr;_pcount = p._pcount;++(*_pcount);return *this;}~shared_ptr(){if (--(*_pcount) == 0){_del(_ptr);}}
private:T* _ptr;int* _pcount;Del _del;
};
shared_ptr<int> a(new int[10], [](int* ptr) {delete[] ptr; });
shared_ptr<FILE> b(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
總結
auto_ptr:管理權轉移,被拷貝的對象會懸空;
unique_ptr:不支持賦值和拷貝構造;
shared_ptr:通過引用計數控制內存的釋放;
weak_ptr:解決shared_ptr的循環引用問題,拷貝和賦值的時候不增加引用計數。
特殊類的設計
不能被拷貝的類
C++11增加了delete的功能,不僅能對new的空間進行釋放,放在默認成員函數后面還能表示:不讓編譯器生成該函數。
實現:將拷貝和賦值置為私有,且使用delete不讓編譯器生成即可。
template<class T>
class NoCopy
{
public://...
private:NoCopy(const NoCopy&) = delete;NoCopy operator=(const NoCopy&) = delete;T _val;
};
只能在堆上創建類
也就是不能再棧上創建對象,棧上創建對象必須能夠調用構造函數和析構函數,所以對于類的實現有兩種方法:1)將構造函數私有;2)將析構函數私有;還需要將拷貝構造和賦值刪除。
將構造函數私有
將構造函數私有之后,new也不能調用構造函數了,所以需要增加接口專門返回在對上創建的對象。
template<class T>
class HeapOnly
{static HeapOnly* Get(){return new HeapOnly;}private:HeapOnly(const HeapOnly&) = delete;HeapOnly operator=(const HeapOnly&) = delete;HeapOnly(){//...}T _val;
};
將析構函數私有
將析構函數私有之后,還需要提供接口進行空間的釋放。
template<class T>
class HeapOnly
{HeapOnly(){//...}private:~HeapOnly(){//...}HeapOnly(const HeapOnly&) = delete;HeapOnly operator=(const HeapOnly&) = delete;T _val;
};
只能在棧上創建對象
對于只能在棧上創建對象,就需要不能讓new正常使用,new分為兩步:1)調用operator new;2)調用構造函數;
實現:重載operator new,將其刪除并將構造函數私有;向外提供構造接口。
template<class T>
class StackOnly
{static StackOnly Creat(){StackOnly st;return st;}~StackOnly(){//...}
private:void* operator new(size_t size) = delete;StackOnly(){//...}T _val;
};
?只能創建一個對象(單例模式)
創建一個類只能創建一個對象,這中類稱為單例模式。
實現方法:1)只創建一個對象,將這個對象設為類的及靜態成員,在每次獲取的時候都將這一靜態成員返回;2)將構造函數設為私有,防構造;3)防拷貝,將拷貝和賦值刪除。
該類分為兩種:餓漢模式和懶漢模式。
餓漢模式
餓漢模式指的是在main函數之前就創建出對象。
class Hungry_class
{
public:Hungry_class& Get(){return _date;}private:Hungry_class(const Hungry_class&) = delete;Hungry_class operator=(Hungry_class) = delete;Hungry_class(){//...}static Hungry_class _date;
};
Hungry_class Hungry_class::_date;
函數自始至終都只有一個對象_date。
餓漢模式的劣勢:
1)在main函數之前就進行對象的創建,當有多個單例模式需要被創建時就會影響程序的啟動效率。
2)如果兩個單例類A和B之間相互聯系,B的創建需要A,如果還是使用餓漢模式就會導致無法控制哪一個單例對象先創建。
懶漢模式
懶漢模式與餓漢不同的是:懶漢模式是在需要單例對象時才去創建。
實現方法:與餓漢模式一樣,但是懶漢的成員變量是指針而不是對象。
class Lazy_class
{
public:Lazy_class& Get(){if (_date == nullptr){Lazy_class* _date = new Lazy_class;}return *_date;}private:Lazy_class(const Lazy_class&) = delete;Lazy_class operator=(Lazy_class) = delete;Lazy_class(){//...}T _val;static Lazy_class* _date;
};
Lazy_class* Lazy_class::_date;
單例對象的釋放
一般單例對象是不需要進行釋放的,但是如果中途需要進行顯示釋放或程序需要進行一些特殊動作(如持久化)等;
單例對象通常是在多個進程中使用的,所以單例對象的釋放不能簡單的根據作用域讓其自己釋放,需要提供專門的接口釋放。
單例模式析構可以手動調用,也可以像智能指針一樣自動調用。
可以通過設計一個內部類實現自動調用析構函數。
class Lazy_class
{
public:static Lazy_class& Get(){if (_date == nullptr){Lazy_class* _date = new Lazy_class;}return *_date;}static void Destory(){//...}struct GC{~GC(){Lazy_class::Destory();}};private:Lazy_class(const Lazy_class&) = delete;Lazy_class operator=(Lazy_class) = delete;Lazy_class(){//...}static Lazy_class* _date;static GC _gc;
};
Lazy_class* Lazy_class::_date;
Lazy_class::GC Lazy_class::_gc;