C++引入智能指針的目的
-
使用智能指針來管理動態分配的對象,當一個對象應該被釋放的時候,指向他的智能指針確保自動釋放它
內存分配
- 靜態內存:局部static對象、類static數據成員、定義在任何函數之外的變量
- 棧內存:定義在函數內的非static對象
- 分配在靜態或者棧內存中的對象由編譯器自動創建和銷毀
- 棧對象,僅在其定義的程序塊運行時候才會存在
- static對象在使用之前就會被分配,程序結束的時候會被銷毀
- 除了靜態內存和棧內存,還有一個內存池,被稱作自由空間或者叫堆,用來存儲動態分配的對象,這些動態對象的生存周期由程序來控制,當動態獨享不在使用的時候,代碼必須顯示的銷毀他們
動態內存和智能指針
- C++動態內存通過一對運算符號來完成,new創建 和 delete刪除
- new:在動態內存中為對象分配空間并且返回一個指向該對象的指針,我們可以選擇對象并且初始化
- delete:接受一個動態對象的指針,銷毀該對象,并且釋放與之相關的內存空間
- 問題:內存釋放的時間;忘記釋放->內存泄露;提前釋放->引用非法內存指針
- C++引入兩種智能指針來管理動態對象;
- shared_ptr 允許多個指針指向同一個對象
- unique_ptr 獨占所指定的對象,還為此定義了一個weak_ptr的伴隨類,這是一個弱的引用,指向unique_ptr管理的對象
- 三者都定義在<memory>頭文件中
shared_ptr類
- 需要提供額外的信息-指針指向的類型,類似vector,之后是定義的這種智能指針的名字
- shared_ptr<string> p1; //shared_ptr 指向string
- shared_ptr<list<int>> p2; //shared_ptr 指向int的list
- 智能指針和普通指針類似,解引用一個智能指針返回他所指向的對象,例如在條件判斷中使用指正指針,效果就是檢測他是否為空
if(p1 && p1->empty){// 如果p1不為空,檢測他是否指向一個空的string*p1 = "Hello world";//如果p1指向一個空的string,將Hello world賦值給指針
}
make_shared函數
- 最安全的分配和使用動態內存的方式是調用一個名為make_shared的標準庫函數
- 函數的目的是在動態內存中分配一個對象并且初始化他,返回的是指向這個對象的shared_ptr指針
- 需要在<>括號內指定創建對象的類型
shared_ptr<int>p3 = make_shared<int>(42);//指向一個數值為42的int的shared_ptrshared_ptr<string> p4 = make_shared<string>(10,'9');//指向一個數值為"9999999999"的string
- 類似順序容器的emplace成員,make_shared用其參數來構造指定類型的對象;比如上面的定義的指向string或者int的shared_ptr,需要滿足string或者int的構造函數
- 如果不傳遞任何參數,對象就會進行數值初始化
- 通常使用auto來定義一個對象來保存make_shared的結果
- auto p6 = make_shared<vector<string>>();
shared_ptr的拷貝和賦值
- 當進行拷貝和賦值的時候,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象
auto p = make_shared<int>(42);auto q(p);//p和q指向相同的對象,這個對象有兩個引用者
- 每一個shared_ptr都有一個關聯的計數器,稱作引用計數。無論何時拷貝一個shared_ptr都會使得計數器遞增,比如使用shared_ptr初始化、將其作為函數返回值返回、將其作為一個參數傳遞給一個函數,都會使關聯計數器遞增
- 當給shared_ptr賦一個新的數值或者銷毀一個局部的shared_ptr(局部定義離開作用域),都會遞減計數器
- 當計數器的數值變成0的時候,就會自動釋放所管理的對象?
auto r = make_shared<int>(42);//r指向的int只有一個引用者r = q;//給r賦值,使得他指向另外一個地址//遞增q指向的對象的引用計數//遞減r指向的對象的引用次數//r先前的指向的對象,因為已經沒有了引用者,因此會自動釋放
Shard_ptr自動銷毀所管理的對象
- 使用析構函數實現銷毀工作
- 析構函數一般用來釋放對象所分配的資源,例如string、vector的若干操作都會分配內存來保存元素,也會使用析構函數銷毀元素,并且釋放內存?
- shared_ptr無用之后仍然保留的一種可能情況是將shared_ptr存放在了一個容器中,然后重排了容器,從而不再需要某些元素,這個時候需要注意,確保使用erase刪除那些不再需要的shared_ptr元素
使用動態生存期的資源的類
程序使用動態內存出于以下三種原因之一
- 程序不知道自己需要使用多少對象
- 程序不知道對象的準確類型
- 程序需要在多個對象之間共享數據
注意事項
- 容器類是出于第一個原因而使用動態內存的典型例子
- 第15章 出于第二個原因而使用共享內存的例子
- 例子:使用共享內存使多個對象之間共享相同的底層的數據
使用動態內存的一個常見的原因是,允許多個對象之間共享相同的狀態,vector v2 = v1;那么v2會具有和v1相同的元素,但是v1因為不在使用而被刪除,與之相反,假設我們定義一個名為Blob的類,希望對Blob的不同拷貝之間共享相同的元素,這個底層是相當于,拷貝元素,不是將元素重新復制一遍然后開辟新的內存空間存儲拷貝出來的數據,而是使用引用的方式,每一份拷貝的數據都會映射到同一個數據資源。
#include <memory>
class StrBlob{
public:typedef std::vector<std::string>::size_type size_type;StrBlob();StrBlob(std::initializer_list<std::string>i1);//size empty 和 push_back成員通過指向底層的vector成員來完成對應的工作size_type size() const { return data->size();}bool empty() const { return data->empty();};//添加和刪除元素void push_back(const std::string &t){data->push_back(t);}void pop_back(){check(0,"pop_back on empty StrBlob");data->pop_back();};//元素訪問std::string& front(){check(0,"front on empty StrBlob");return data->front();};std::string& back(){check(0,"back on empty STrBlob");return data->back();};public:std::shared_ptr<std::vector<std::string>>data;//如果data[i]不合理,就會拋出一個異常void check(size_type i,const std::string &msg) const{if (i > data->size()){throw std::out_of_range(msg);}}};
//兩個函數都使用初始化列表來初始化其data成員,使其指向一個動態分配的vector
//默認構造函數分配一個空vector
StrBlob::StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
//接受一個initializer_list的構造函數將其參數傳遞給對應的vector構造函數
//這個函數通過拷貝列表中的數值來初始化vector的元素
StrBlob::StrBlob(std::initializer_list<std::string> i1) : data (std::make_shared<std::vector<std::string>>(i1)) {};
//使用new和delete直接進行管理內存不能依賴類對象的拷貝、賦值和銷毀操作的任何默認定義,因此使用智能指針更易于編寫和調試
//使用new只能返回一個指向該對象的指針,而無法為其分配的對象命名
int *p1 = new int;//p1指向一個動態分配的、未初始化的無名對象
//這個new表達式在自由空間內構造了一個int型對象,并且返回指向這個對象的指針,
// 默認情況下,動態分配的對象是默認初始化的,內置類型或者組合類型的對象的數值將會是未定義的,類類型對象將使用默認構造函數進行初始化
std::string *ps = new std::string;//初始化為空的string
int *p2 = new int;//p1指向一個動態分配的、未初始化的無名對象
//可以使用直接初始化方式初始化一個動態分配的對象,可以使用傳統的構造方式(使用圓括號),也可以使用列表初始化(使用花括號)
int *p3 = new int (1024);//p3指向的對象的數值為1024
std::string *ps1 = new std::string(10,'9');//*ps = "9999999999"
std::vector<int> *pv = new std::vector<int>{0,1,2,3,4,5,6,7,8,9};//vector里面有10個元素,數值依次從0到9
//對動態分配的對象進行值初始化 需要在類型名之后加上一對空括號即可
std::string *ps2 = new std::string();//值初始化為空string
int *p4 = new int;//默認初始化,*p4的數值沒有定義
int *p5 = new int();//默認初始化,*p5的數值為0
//如果使用括號提供一個括號包圍的初始化器,就可以使用auto從初始化器里面推斷出想要分配給對象的類型
//但是括號里面僅有單一的初始化器才可以使用auto
//auto p6 = new auto{1,2,3}; //錯誤
auto p6 = new auto{1};//p6的類型是一個指針,指向從obj里面推斷出來的類型
// 如果obj是一個int,p1就是int*;如果obj是一個string,那么p1是一個string*//使用new分配const對象是合法的
const int *pci = new const int(1024);//分配并且初始化一個const int
const std::string *pcs = new const std::string;//分配并且初始化一個const的空string
//和其他任何const對象一樣,動態分配的const對象必須進行初始化
//對于定義了默認構造函數的類類型可以進行隱式初始化,其他類型必須進行顯示初始化
//由于分配的對象是const的,new返回的指針是一個指向const的指針//內存空間耗盡
//如果new不能分配所要求的內存空間,就會拋出一個類型為bad_alloc的異常,可以通過改變使用new的方式來阻止它拋出異常
int *p7 = new (std::nothrow)int;//如果分配失敗,new返回一個空的指針
//這種方式叫做定位new,其允許我們向new傳遞額外的參數,上例將nothrow傳遞給new,即告訴他不要拋出異常//釋放動態內存
//為了防止內存耗盡
// delete接受一個指針,指向要釋放的內存
指針值和delete
- 傳遞給delete的指針必須指向動態分配的內存,或者是一個空指針(參見2.3.2節,第48頁)。釋放一塊并非new分配的內存,或者將相同的指針值釋放多次,其行為是未定義的:
- int i,*pil=&i,*pi2=nullptr;double*pd=new double(33),*pd2=pd;
- delete i;//錯誤:i不是一個指針
- delete pil;//未定義:pil指向一個局部變量
- delete pd;//正確
- delete pd2;//未定義:pd2指向的內存已經被釋放了
- delete pi2;//正確:釋放一個空指針
- 對于deletei的請求,編譯器會生成一個錯誤信息,因為它知道i不是一個指針。執行delete pil和pd2所產生的錯誤則更具潛在危害:通常情況下,編譯器不能分辨一個指針指向的是靜態還是動態分配的對象。類似的,編譯器也不能分辨一個指針所指向的內存是否已經被釋放了。對于這些delete表達式,大多數編譯器會編譯通過,盡管它們是錯誤的。
- 雖然一個const對象的值不能被改變,但它本身是可以被銷毀的。如同任何其他動態對象一樣,想要釋放一個const動態對象,只要delete指向它的指針即可:
- const int *pci=new const int(1024);delete pci;//正確:釋放一個const對象
動態對象的生存期直到被釋放時為止
- 如12.1.1節(第402頁)所述,由shared_ptr管理的內存在最后一個shared_ptr銷毀時會被自動釋放。但對于通過內置指針類型來管理的內存,就不是這樣了。對于一個由內置指針管理的動態對象,直到被顯式釋放之前它都是存在的。
- 返回指向動態內存的指針(而不是智能指針)的函數給其調用者增加了一個額外負擔--調用者必須記得釋放內存:
// factory 返回一個指針,指向一個動態分配的對象
Foo* factory(T arg)//視情況處理arg
return new Foo (arg) ; // 調用者負責釋放此內存
)void use_factory(T arg)
{
Foo *p = factory(arg);
// 使用p但不delete它
} //p離開了它的作用域,但它所指向的內存沒有被釋放!
- 類似之前定義的factory函 數 (參 見 12.1.1節,第 403頁),這個版本的factory分配一個對象,但并不delete它。factory的調用者負責在不需要此對象時釋放它。此函數只負責申請創建空間,并不負責釋放內存。不幸的是,調用者經常忘記釋放對象:
- 此處,use_factory函 數調用factory,后者分配一個類型 為 Foo 的新對象。當use_factory返問時,局部變量p 被銷毀。此變量是一個內置指針,而不是一個智能指針。 與類類型不同,內置類型的對象被銷毀時什么也不會發生。特別是,當一個指針離開其作用域時,它所指向的對象什么也不會發生。如果這個指針指向的是動態內存,那么內存將不會被自動釋放。
- 由內置指針(而不是智能指針)管理的動態內存在被顯式釋放前一直都會存在。
在本例中,p 是指向factory分配的內存的唯一指針。一旦use_factory返回, 程序就沒有辦法釋放這塊內存了。根據整個程序的邏輯,修正這個錯&的正確方法是在
use_factory中記得釋放內存: void use_factory(T arg)
(
Foo *p = factory (arg);
// 使用p
delete p; / / 現在記得釋放內存,我們已經不需要它了
)
還有一種可能,我們的系統中的其他代碼要使用use_factory所分配的對象,我們就應該修改此函數,讓它返回一個指針,指向它分配的內存:
Foo* use_factory(T arg)
(
、
Foo *p = factory(arg);
// 使 用 p
return p; // 調用者必須釋放內存
}
使用new和delete管理動態內存存在三個常見問題:
- 1,忘記delete內存。忘記釋放動態內存會導致人們常說的“內存泄漏”問題,因為這種內存永遠不可能被歸還給自由空間了。查找內存泄露錯誤是非常困難的,因為通常應用程序運行很長時間后,真正耗盡內存時,才能檢測到這種錯誤。
- 2.使用已經釋放掉的對象。通過在釋放內存后將指針置為空,有時可以檢測出這種錯誤。
- 3.同一塊內存釋放兩次。當有兩個指針指向相同的動態分配對象時,可能發生這種錯誤。如果對其中一個指針進行了delete操作,對象的內存就被歸還給自由空間了。如果我們隨后又delete第二個指針,自由空間就可能被破壞。相對于查找和修正這些錯誤來說,制造出這些錯誤要簡單得多"
- 堅持只使用智能指針,就可以避免所有這些.問題。對于一塊內存,只有在沒有任何智能指針指向它的情況下,智能指針才會自動釋放它。
delete之后重置指針值
- 當delete一個指針后,指針值就變為無效了。雖然指針已經無效,但在很多機器上指針仍然保存著(已經釋放了的)動態內存的地址。指針雖然存在,但是和內存空間脫離了關系。在delete之后,指針就變成了空懸指針(danglingpointer),即,指向一塊曾經保存數據對象但現在己經無效的內存的指針。
- 未初始化指針(參見2.3.2節,第49頁)的所有缺點空懸指針也都有。有一種方法可以避免空懸指針的問題:在指針即將要離開其作用域之前釋放掉它所關聯的內存。這樣,在指針關聯的內存被釋放掉之后,就沒有機會繼續使用指針了。如果需要保留指針,可以在delete之后將nullptr賦予指針,這樣就清楚地指出指針不指向任何對象。
這只是提供了有限的保護
- 動態內存的一個基本問題是可能有多個指針指向相同的內存。在delete內存之后重置指針的方法只對這個指針有效,對其他任何仍指向(已釋放的)內存的指針是沒有作用。
- 多個指針指向同一個內存空間,僅僅對一個指針進行賦值為空的操作,其他指針仍然會出錯
- int *p(new int(42));//p指向動態內存
- auto q = p;//p和q指向相同的內存
- deletep;//p和q均變為無效
- p=nullptr;//指出p不再綁定到任何對象
- 本例中P和q指向相同的動態分配的對象。我們delete此內存,然后將p置為nullptr,指出它不再指向任何對象。但是,重置p對q沒有任何作用,在我們釋放p所指向的(同時也是q所指向的!)內存時,q也變為無效了。在實際系統中,查找指向相同內存的所有指針是異常困難的。
12.1.3shared_ptr和new結合使用
- 如前所述,如果不初始化一個智能指針,它就會被初始化為一個空指針。如表12.3所示,還可以用new返回的指針來初始化智能指針:
- shared_ptr<double>pl;//shared_pt:r可以指向一個double
- shared_ptr<int>p2(new int(42));//p2指向一個值為42的int
- 接受指針參數的智能指針構造函數是explicit的(參見7.5.4節,第265頁)(必須使用直接初始化的形式,不可以使用拷貝初始化的方式)。因此,不能將一個內置指針隱式轉換為一個智能指針,必須使用直接初始化形式(參見3.2.1節,第76頁)來初始化一個智能指針:
- shared_ptr<int>pl=new int(1024);//錯誤:必須使用直接初始化形式 不可以使用拷貝初始化的方式
- shared_ptr<int>p2(new int(1024));//正確:使用了直接初始化形式
- pl的初始化隱式地要求編譯器用一個new返回的int*來創建一個shared_ptr,由于不能進行內置指針到智能指針間的隱式轉換,因此這條初始化語句是錯的。出于相同的原因,一個返回shared_ptr的函數不能在其返回語句中隱式轉換一個普通指針:
shared_ptr<int>clone(int p)(
return new int(p);//錯誤:隱式轉換為sha:red_ptrvint>
}
- 我們必須將shared_ptr顯式綁定到一個想要返回的指針上:
shared_ptr<int>clone(intp)(
//正確:顯式地用int*創建shared_ptr<int>
return shared_ptr<int>(new int(p));
}
- 默認情況下,一個用來初始化智能指針的普通指針必須指向動態內存,因為智能指針默認使用delete釋放它所關聯的對象。可以將智能指針綁定到一個指向其他類型的資源的指針上,但是為了這樣做,必須提供自己的操作來替代delete
不要混合使用普通指針和智能指針……
- shared_ptr可以協調對象的析構,但這僅限于其自身的拷貝(也是shared_ptr)之間。這也贏什么推薦使用make_shared而不是new的原因。這樣,在分配對象的同時就將shared_ptr與之綁定,從而避免了無意中將同一塊內存綁定到多個獨立創建的shared_ptr上。
- 考慮下面對shared_ptr進行操作的函數:
//在函教被調用時ptr被創建并初始化
void process(shared_ptr<int>ptr)
(
//使用ptr
}//ptr離開作用域,被銷毀
- process的參數是傳值方式傳遞的,因此實參會被拷貝到ptr中。拷貝一個shared_ptr會遞增其引用計數,因此,在process運行過程中,引用計數值至少為2。當process結束時,ptr的引用計數會遞減,但不會變為0。因此,當局部變量ptr被銷毀時,ptr指向的內存不會被釋放。
- ?使用此函數的正確方法是傳遞給它一個shared_ptr:
- shared_ptr<int>p(new int(42));//引用計數為1
- process(p);//拷貝p會遞增它的引用計數;在process中引用計數值為2
- inti=*p;//正確:引用計數值為1
雖然不能傳遞給process-個內置指針,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內置指針顯式構造的。但是,這樣做很可能會導致錯誤:
- int *x(new int(1024));//危險:x是一個普通指針,不是一個智能指針
- process(x);//錯誤:不能將int*轉換為一個shared_ptr<int>
- process(shared_ptr<int>(x));//合法的,但內存會被釋放!
- intj=*x;//未定義的:x是一個空懸指針!
- 在上面的調用中,將一個臨時shared_ptr傳遞給process。當這個調用所在的表達式結束時,這個臨時對象就被銷毀了。銷毀這個臨時變量會遞減引用計數,此時引用計數就變為0了。因此,當臨時對象被銷毀時,它所指向的內存會被釋放。
- 但x繼續指向(已經釋放的)內存,從而變成一個空懸指針。如果試圖使用x的值,其行為是未定義的。
- 當將一個shared_ptr綁定到一個普通指針時,我們就將內存的管理責任交給了這個shared_ptr,一旦這樣做了,我們就不應該再使用內置指針來訪問shared_ptr所指向的內存了
- 使用內置指針來訪問一個智能指針所負責的對象是很危險的,因為不知道對象何時會被銷毀
也不要使用get初始化另一個智能指針或為智能指針賦值
- 智能指針類型定義了一個名為get的函數(參見表12.1),它返回一個內置指針, 指向智能指針管理的對象。此函數是為了這樣一種情況而設計的:我們需要向不能使用智能指針的代碼傳遞一個內置指針。使用get返回的指針的代碼不能delete此指針。
- 雖然編譯器不會給出錯誤信息,但將另一個智能指針也綁定到get返回的指針上是錯誤的:
shared_ptr<int> p (new int (42) ) ; // 引用計數為 1
int *q = p.get () ; / / 正確:但使用q 時要注意,不要讓它管理的指針被釋放 {
// 新程序塊
// 未定義:兩個才蟲立的shared_ptr指向相同的內存
shared_ptr<int>(q);
) // 疽序塊結束,q 被銷毀,它指向的內存被釋放
int foo = *p; // 未定義:p 指向的內存已經被釋放了
- 在本例中,p 和 q 指向相同的內存。由于它們是相互獨立創建的,因此各自的引用計數都 是 1。當q所在的程序塊結束時,q 被銷毀,這會導致q 指向的內存被釋放。從而p 變成 一個空懸指針,意味著當我們試圖使用p 時,將發生未定義的行為。而且,當p 被銷毀時, 這塊內存會被第二次delete。
- get用來將指針的訪問權限傳遞給代碼,你只有在確定代碼不會delete指針的情況下,才能使用get。特別是,永遠不要用get初始化另一個智能指針或者為另一個智能指針賦值。
其他shared_ptr操作
- shared_ptr還定義了其他一些操作,參見表12.2和表12.3所示。我們可以用reset來將一個新的指針賦予一個shared_ptr:
- p=new int(1024);//錯誤:不能將一個指針賦予shared_ptr
- p.reset(newint(1024));//正確:p指向一個新對象
- 與賦值類似,reset會更新引用計數,如果需要的話,會釋放p指向的對象。reset成員經常與unique一起使用,來控制多個shared_ptr共享的對象。在改變底層對象之前,我們檢查自己是否是當前對象僅有的用戶。“果不是,在改變之前要制作一份新的拷貝:
if(!p.unique())
p.reset(newstring(*p));//我們不是唯一用戶;分配新的拷貝
*p+=newVal;//現在我們知道自己是唯一的用戶,可以改變對象的值
智能指針和異常
- 程序退出有兩種可能:1,正常處理結束;2,發生了異常。局部對象都會被銷毀
- 使用new創建內存 到 delete刪除異常這一段空間發生異常,內存不會被釋放
- 使用智能指針就可以
智能指針和啞類
- 包括所有標準庫類在內的很多C++類都定義了析構函數(參見12.1.1節,第402頁),負責清理對象使用的資源。但是,不是所有的類都是這樣良好定義的。特別是那些為C和C++兩種語言設計的類,通常都要求用戶顯式地釋放所使用的任何資源。
- 那些分配了資源,而又沒有定義析構函數來釋放這些資源的類,可能會遇到與使用動態內存相同的錯誤--程序員非常容易忘記釋放資源。類似的,如果在資源分配和釋放之間發生了異常,程序也會發生資源泄漏。
- 與管理動態內存類似,我們通常可以使用類似的技術來管理不具有良好定義的析構函數的類。例如,假定我們正在使用一個C和C++都使用的網絡庫,使用這個庫的代碼可能是這樣的:
struct destination; 表示我們正在連接什么
struct connection; 使用連接所需的信息
connection connect(destination*); 打開連接
void disconnect(connection); 關閉給定的連接
void f (destination &d /* 其他參數 */)
獲得一個連接;記住使用完后要關閉它
connection c = connect(&d);
使用連接
如果我們在f退出前忘記調用disconnect,就無法關閉c 了
)
- 如果connection有一個析構函數,就可以在f結束時由析構函數自動關閉連接。但是,connection沒有析構函數。這個問題與我們上一個程序中使用shared_ptr避免內存泄漏幾乎是等價的。使用shared_ptr來保證connection被正確關閉,已被證明是一種有效的方法。
使用我們自己的釋放操作
- 默認情況下,shared_ptr假定它們指向的是動態內存。因此,當一個shared_ptr 被銷毀時,它默認地對它管理的指針進行delete操作。為了用shared_ptr來處理一個 connection,我們必須首先定義一個函數來代替delete。這個刪除器(deleter)函數必須能夠完成對shared_ptr中保存的指針進行釋放的操作。在本例中,我們的刪除器必須接受單個類型為connection*的參數:void end_connection(connection *p) ( disconnect(*p); )
- 當我們創建一個shared_ptr時,可以傳遞一個(可選的)指向刪除器函數的參數(參 見 6.7節,第 221頁):
void f (destination &d /* 其他參數 */)
{
connection c = connect(&d);
shared_ptr<connection> p (&c, end_connection);
// 使用連接
// 當f退出時(即使是由于異常而退出), connection會被正確關閉
}
- 當p被銷毀時,它不會對自己保存的指針執行delete,而是調用end_connectiono接下來,end_connection會調用disconnect?從而確保連接被關閉。如果f正常退出,那么p的銷毀會作為結束處理的一部分。如果發生了異常,P同樣會被銷毀,從而連接被關閉。
智能指針使用規范
- 智能指針可以提供對動態分配的內存安全而又方便的管理,但這建立在正確使用的前提下。為了正確使用智能指針,我們必須堅持一些基本規范:
- 不使用相同的內置指針值初始化(或reset)多個智能指針
- 不 delete get()返回的指針
- 不使用get()初始化或reset 另一個智能指針
- 如果你使用get()返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了。
- 如果你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器 (參見 12.1.4節,第415頁 和 12.1.5節,第419頁
12.1.5unique_ptr
- 一個unique_ptr“擁有”它所指向的對象。與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定對象。當unique_ptr被銷毀時,它所指向的對象也被銷毀。表12.4列出了unique_ptr特有的操作。與shared_ptr相同的操作列在表12.1(第401頁)中。
- 與shared_ptr不同,沒有類似make_shared的標準庫函數返回一個unique_ptr.當我們定義一個unique_ptr時,需要將其綁定到一個new返回的指針上。類似shared_ptr,初始化unique_ptr必須采用直接初始化形式:
- unique_ptr<double>pl;//可以指向一個double的unique_ptr
- unique_ptr<int>p2(new int(42));//p2指向一個值為42的int
- 由于一個unique_ptr擁有它指向的對象,因此unique_ptr不支持普通的拷貝或賦值操作:
- unique_ptr<string>pl(new string("Stegosaurus"));
- unique_ptr<string>p2(pl);//錯誤:unique_ptr不支持拷貝
- unique_ptr<string>p3;
- p3=p2;//錯誤:unique_ptr不支持賦值
- 雖然我們不能拷貝或賦值unique_ptr,但可以通過調用release或reset將指針的所有權從一個(非const)unique_ptr轉移給另一個unique:
- //將所有權從pl(指向stringStegosaurus)轉移給p2
- unique_ptr<string>p2(pl.release());//release將pl置為空
- unique_ptr<string>p3(new string("Trex"));
- //將所有權從p3轉移給p2
- p2.reset(p3.release());//reset釋放了p2原來指向的內存
- release成員返回unique_ptr當前保存的指針并將其置為空。因此,p2被初始化為pl原來保存的指針,而pl置為空。
- reset成員接受一個可選的指針參數,令unique_ptr重新指向給定的指針。如果unique_ptr不為空,它原來指向的對象被釋放。因此,對p2調用reset釋放了用"Stegosaurus"初始化的string所使用的內存,將p3對指針的所有權轉移給p2,并將p3置為空。
- 調用release會切斷unique_ptr和它原來管理的對象間的聯系。release返回的指針通常被用來初始化另一個智能指針或給另一個智能指針賦值。在本例中,管理內存的責任簡單地從一個智能指針轉移給另一個。但是,如果我們不用另一個智能指針來保存release返回的指針,我們的程序就要負責資源的釋放:
- p2.release();//錯誤:p2不會釋放內存,而且我們丟失了指針
- auto p=p2.release();//正確,但我們必須記得delete(p)
傳遞unique_ptr 參數和返回unique_ptr
- 我們可以拷貝或賦值一個將要被銷毀的unique_ptr?最常見的例子是從函數返回一個unique_ptr:
unique_ptr<int> clone (int p) ( // 正確:從 int*創建一個 unique_ptr<int> return unique_ptr<int>(new int(p));
}
還可以返回一個局部對象的拷貝:
unique_ptr<int> clone (int p) ( unique_ptr<int> ret(new int (p));
// ...
return ret;
}
- 于兩段代碼,編譯器都知道要返回的對象將要被銷毀。在此情況下,編譯器執行一種特殊 的 “拷貝”,我們將在13.6.2節 (第473頁)中介紹它。
向 unique_ptr傳遞刪除器
// p 指向一個類型為objT的對象,并使用一個類型為delT的對象釋放objT對象
// 它會調用一個名為fen的 delT類型對象
unique_ptr<objT, delT> p (new objT, fen);
作為一個更具體的例子,我們將重寫連接程序,用 unique_ptr來代替shared_ptr, 如下所示:
void f (destination &d /* 其他需要的參數 */)
{
connection c = connect (&d) ; // 打開連接
// 當p 被銷毀時,連接將會關閉
unique_ptr<connection, decltype(end_connection)*>
p (&c, end_connection);
// 使用連接
// 當f退出時(即使是由于異常而退出),connection 會被正確關閉
}
- 在本例中我們使用了 decltype (參見2.5.3節,第 62頁)來指明函數指針類型。由于decltype (end_connection)返回一個函數類型,所以我們必須添加一個*來指出我們正在使用該類形的一個指針(參見6.7節,第 223頁)。
12.1.6weak_ptr
- weak_ptr(見表12.5)是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。一旦最后一個指向對象的shared_ptr被銷毀,對象就會被釋放。即使有weak_ptr指向對象,對象也還是會被釋放,因此,weak_ptr的名字抓住了這種智能指針“弱”共享對象的特點。
- 當我們創建一個weak_ptr時,要用一個shared_ptr來初始化它:
- auto p = make_shared<int>(42);
- weak_ptr<int> wp (p) ; // wp弱共享p; p 的引用計數未改變
- 本例中wp和 p 指向相同的對象。由于是弱共享,創建wp不會改變p 的引用計數;wp指 向的對象可能被釋放掉。由于對象可能不存在,我們不能使用weak_ptr直接訪問對象,而必須調用lock來訪問對象。 lock函數檢測?weak_ptr指向的對象是否仍存在。如果存在,lock返回一個指向共享對象的 shared_ptro,與任何其他shared_ptr類似,只要此shared_ptr存在,它所指向
的底層對象也就會一直存在。 - 例如:
if (shared_ptr<int> np = wp. lock() ) ( // 如果 np 不為空則條件成立
/ / 在 if中,np與 p 共享對象
}在這段代碼中,只有當lock調用返回true時我們才會進入if語句體。在 if中,使用 np訪問共享對象是安全的。使用lock訪問對象?
核查指針類
- 作為weak_ptr用途的一個展示,我們將為StrBlob類定義一個伴隨指針類。我們的指針類將命名為StrBlobPtr,會保存一個weak_ptr,指向StrBlob的 data成員,這是初始化時提供給它的。通過使用weak_ptr,不會影響一個給定的StrBlob所指向 的vector的生存期。但是,可以阻止用戶訪問一個不再存在的vector的企圖。
12.2動態數組
- 兩種一次性分配一個對象數組的方法
- new表達式語法
- allocator的類,允許將分配和初始化分離
- 當需要可變的數量的對象的時候,使用vector 或者其他標準庫容器是快速并且安全的。大多數情況下,應該使用標準庫容器而不是動態分配的數組,因為容器更為簡單、不容易出現內存管理的錯誤,并且有很強的性能
- 如前所述,使用容器的類可以使用默認版本的拷貝、賦值和析構操作(參見7.1.5節,第 239頁)。分配動態數組的類則必須定義自己版本的操作,在拷貝、復制以及銷毀對象時管理所關聯的內存。
12.2.1 new和數組
- 為了讓new分配一個對象數組,我們要在類型名之后跟一對方括號,在其中指明要分配的對象的數目。在下例中,new分配要求數量的對象并(假定分配成功后)返回指向第一個對象的指針:
- 調 用 get_size確定分配多少個int
- int *pia = new int [get_size()] ; // pia 指向第一個 int
- 方括號中的大小必須是整型,但不必是常量。
- 也可以用一個表示數組類型的類型別名(參見2.5.1節,第60頁)來分配一個數組,這樣,new表達式中就不需要方括號了:
- typedef int arrT [42] ; // arrT 表示 42 個 int 的數組類型
- int *p = new arrT; / / 分配一個42個 int的數組;p 指向第一個int
- 在本例中,new分配一個int數組,并返回指向第一個int的指針。即使這段代碼中沒有方括號,編譯器執行這個表達式時還是會用new[]。即,編譯器執行如下形式:
int *p = new int[42];
分配一個數組會得到一個元素類型的指針
- 雖然我們通常稱new T[]分配的內存為“動態數組”,但這種叫法某種程度上有些誤導。當 用 new 分配一個數組時,我們并未得到一個數組類型的對象,而是得到一個數組元素類型的指針(指針指向申請的內存空間,指針類型和數組類型一致)。即使我們使用類型別名定義了一個數組類型,new也不會分配一個數組類型的對象。在上例中,我們正在分配一個數組的事實甚至都是不可見的一一連[num]都 沒有。new返回的是一個元素類型的指針。
- 由于分配的內存并不是一個數組類型,因此不能對動態數組調用begin或 end (參 見 3.5.3節,第 106頁)。這函數使用數組維度(回憶一下,維度是數組類型的一部分)來返回指向首元素和尾后元素的指針。出于相同的原因,也不能用范圍f o r 語句來處理 (所謂的)動態數組中的元素。
- 動態數組并不是數組類型
初始化動態分配對象的數組
- 默認情況下,new分配的對象,不管是單個分配的還是數組中的,都是默認初始化的。 可以對數組中的元素進行值初始化(參見3.3.1節,第 88頁),方法是在大小之后跟一對空括號。
- int *pia = new int[10];? ? ? ? ? ? ? ? ? ? ??// 10個未初始化的int
- int *pia2 = new int[10]();? ? ? ? ? ? ? ? ?// 10個值初始化為0 的 int
- string *psa = new string[10];? ? ? ? ? // 10 個空 string
- string *psa2 = new string[10]();? ? ?// 10 個空 string
- 在新標準中,我們還可以提供一個元素初始化器的花括號列表:使用圓括號是采用默認初始化,使用花括號是使用自己提供的數值進行初始化
- // 10個 int分別用列表中對應的初始化器初始化
- ?int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
- // 10個 string,前 4個用給定的初始化器初始化,剩余的進行值初始化 '\0'
- string *psa3 = new string [10] ( "a', "an”, "the”, string (3,'x'?)};
- 與內置數組對象的列表初始化(參見3.5.1節,第 102頁)一樣,初始化器會用來初始化動態數組中開始部分的元素。如果初始化器數目小于元素數目,剩余元素將進行值初始化。
- 如果初始化器數目大于元素數目,則 new表達式失敗,不會分配任何內存。在本例中,new會拋出一個類型為bad_array_new _length的異常。類似bad_alloc ,此類型定義在頭文件new中。
- 雖然我們用空括號對數組中元素進行值初始化,但不能在括號中給出初始化器,這意味著不能用auto 分配數組(參見12.1.2節,第 407頁)。
動態分配一個空數組是合法的
- 可以用任意表達式來確定要分配的對象的數目:
- size_t n = get_size () ; // get_size返回需要的元素的數目
- int* p = new int [n] ; / / 分配數組保存元素
- for (int* q = p; q != p + n; ++q)
- / * 處 理 數 組 */ ;
- 這產生了一個有意思的問題:如果get_size返回0,會發生什么?答案是代碼仍能正常工作。雖然我們不能創建一個大小為0曲靜態數組對象,但當n等于0時,調用new[n]是合法的:
- char arr[0];//錯誤:不能定義長度為0的數組
- char*cp=new char[0];//正確:但cp不能解引用
- 當我們用new分配一個大小為0的數組時,new返回一個合法的非空指針。此指針保證與new返回的其他任何指針都不相同。對于零長度的數組來說,此指針就像尾后指針一樣(參見3.5.3節,第106頁),我們可以像使用尾后迭代器一樣使用這個指針。可以用此指針進行比較操作,就像上面循環代碼中那樣。可以向此指針加上(或從此指針減去)0,也可以從此指針減去自身從而得到0。但此指針不能解引用一一畢竟它不指向任何元素。在我們假想的循環中,若get_size返回0,則n也是0,new會分配0個對象。for循環中的條件會失敗(p等于q+n「因為n為0)。因此,循環體不會被執行
釋放動態數組
- 為了釋放動態數組,我們使用一種特殊形式的delete— 在指針前加上一個空方括號對:
- delete p; // p 必須指向一個動態分配的對象或為空
- delete [] pa; // pa必須指向一個動態分配的數組或為空
- 第二條語句銷毀pa指向的數組中的元素,并釋放對應的內存。數組中的元素按逆序銷毀, 即,最后一個元素首先被銷毀,然后是倒數第二個,依此類推。
- 當我們釋放一個指向數組的指針時,空方括號對是必需的:它指示編譯器此指針指向一個對象數組的第一個元素。如果我們在delete 一個指向數組的指針時忽略了方括號 (或者在delete 一個指向單一對象的指針時使用了方括號),其行為是未定義的。 回憶一下,當我們使用一個類型別名來定義一個數組類型時,在 new表達式中不使用【】。即使是這樣,在釋放一個數組指針時也必須使用方括號:
- typedef int arrT [42] ; // arrT是 42個 int的數組的類型別名
- int *p = new arrT; / / 分配一個 42個 int的數組;p 指向第—個元素
- delete [] p; / / 方括號是必需的,因為我們當初分配的是一個數組
- 不管外表如何,p 指向一個對象數組的首元素,而不是一個類型為arrT的單一對象。因此,在釋放p 時我們必須使用【】
智能指針和動態數組
- 標準庫提供了一個可以管理new分配的數組的unique_ptr 版本。為了用一個 unique_ptr管理動態數組,我們必須在對象類型后面跟一對空方括號:
- // up指向一個包含10個未初始化int的數組
- unique_ptr<int[]> up (new int[10]);
- up. release () ; / / 自動用delete []銷毀其指針
- 類型說明符中的方括號(<int[]>)指出up指向一個int數組而不是一個int
- 由于up指向一個數組,當up銷毀它管理的指針時,會自動使用delete【】
- 指向數組的unique_ptr提供的操作與我們在12.1.5節(第417頁)中使用的那些操作有一些不同,我們在表12.6中描述了這些操作。當一個unique_ptr指向一個數組時,我們不能使用點和箭頭成員運算符。畢竟unique_ptr指向的是一個數組而不是單個對象,因此這些運算符是無意義的。另一方面,當一個unique_ptr指向一個數組時,我們可以使用下標運算符來訪問數組中的元素:
- for(size_t i=0;i!=10;++i)
- up[i]=i;//為每個元素賦予一個新值
- 與unique_ptr不同,shared_ptr不直接支持管理動態數組。如果希望使用shared_ptr管理一個動態數組,必須提供自己定義的刪除器:
- //為了使用shared_ptr,必須提供一個刪除器
- shared_ptr<int>sp(new int[10],[](int*p){delete []p;});
- sp.reset();//使用我們提供的lambda釋放數組,它使用delete[]
- 本例中我們傳遞給shared_ptr一個lambda(參見10.3.2節,第346頁)作為刪除器,它使用delete[]釋放數組。如果未提供刪除器,這段代碼將是未定義的。默認情況下,shared_ptr使用delete銷毀它指向的對象。如果此對象是一個動態數組,對其使用delete產生的問題與釋放一個動態數組指針時忘記【】產生的問題一樣(參見12.2.1節,第425頁)。
- shared_ptr不直接支持動態數組管理這一特性會影響我們如何訪問數組中的元素:
- //shared_ptr未定義下標運算符,并且不支持指針的算術運算
- for(size_ti=0;i!=10;++i)*(sp.get()+i)=i;//使用get獲取一個內置指針
- shared_ptr未定義下標運算符,而且智能指針類型不支持指針算術運算。因此,為了訪問數組面的元素,必須用get獲取一個內置指針,然后用它來訪問數組元素。
12.2.2allocator類
- new有一些靈活性上的局限,其中一方面表現在它將內存分配(申請空間)和對象構造(創建對象)組合在了一起。類似的,delete將對象析構和內存釋放組合在了一起。我們分配單個對象時,通常希望將內存分配和對象初始化組合在一起。因為在這種情況下,我們幾乎肯定知道對象應有什么值。
- 當分配一大塊內存時,我們通常計劃在這塊內存上按需構造對象。在此情況下,我們希望將內存分配和對象構造分離。這意味著我們可以分配大塊內存,但只在真正需要時才真正執行對象創建操作(同時付出一定開銷)。一般情況下,將內存分配和對象構造組合在一起可能會導致不必要的浪費。例如:
- string *const p=new string[n];//構造n個空string
- string s;
- string*q=p;//q指向第一個string
- while(cin >> s&&q!=p+n)
- *q++=s;//賦予*q一個新值
- const size_t size=q-p;//記住我們讀取了多少個string
- //使用數組
- delete[]? p;//p指向一個數組;記得用delete[]來釋放
- new表達式分配并初始化了n個string。但是,我們可能不需要n個string,少量string可能就足夠了。這樣,我們就可能創建了一些永遠也用不到的對象。而且,對于那些確實要使用的對象,我們也在初始化之后立即賦予了它們新值。每個使用到的元素都被賦值了兩次:第一次是在默認初始化時,隨后是在賦值時。
- 更重要的是,那些沒有默認構造函數的類就不能動態分配數組了。
allocator類
- 標準庫allocator類定義在頭文件memory中,它幫助我們將內存分配和對象構造分離開來。它提供一種類型感知的內存分配方法,它分配的內存是原始的、未構造的。表12.7概述了allocator支持的操作。在本節中,我們將介紹這些allocator操作。在13.5節(第464頁),我們將看到如何使用這個類的典型例子。
- 類似vector,allocator是一個模板(參見3.3節,第86頁)。為了定義一個allocator對象,我們必須指明這個allocator可以分配的對象類型。當一個allocator對象分配內存時,它會根據給定的對象類型來確定恰當的內存大小和對齊位置:
- allocator<string>alloc;//可以分配string的allocator對象
- auto const p=alloc.allocate(n);//分配n個未初始化的string??這個allocate調用為n個string分配了內存。
allocator分配未構造的內存
- allocator分配的內存是未構造的(unconstructed)。我們按需要在此內存中構造對象。在新標準庫中,construct成員函數接受一個指針和零個或多個額外參數,在給定位置構造一個元素。額外參數用來初始化構造的對象。類似make_shared的參數(參見12.1.1節,第401頁),這些額外參數必須是與構造的對象的類型相無配的合法的初始化器:
- auto q=p;//q指向最后構造的元素之后的位置
- alloc.construct(q++);//*q為空字符串
- alloc.construct(q++,10,zcr);//*q為cccccccccc
- alloc.construct(q++,"hi”);//*q為hi!
- 在早期版本的標準庫中,construct只接受兩個參數:指向創建對象位置的指針和一個元素類型的值。因此,我們只能將一個元素拷貝到未構造空間中,而不能用元素類型的任
- 何其他構造函數來構造一個元素。
- 還未構造對象的情況下就使用原始內存是錯誤的:
- cout<<*p<<endl;//正確:使用string的輸出運算符
- cout<<*q<<endl;//災難:q指向未構造的內存
- 當我們用完對象后,必須對每個構造的元素調用destroy來銷毀它們。函數destroy接受一個指針,對指向的對象執行析構函數(參見12.1.1節,第402頁):
- while(q!=p)?alloc.destroy(--q);//釋放我們真正構造的string
- 在循環開始處,q指向最后構造的元素之后的位置。我們在調用destroy之前對q進行了遞減操作。因此,第一次調用destroy時,q指向最后一個構造的元素。最后一步循環中我們destroy了第一個構造的元素,隨后q將與p相等,循環結束
- 一旦元素被銷毀后,就可以重新使用這部分內存來保存其他s tr in g ,也可以將其歸還給系統。釋放內存通過調用deallocate來完成:alloc.deallocate(p, n);
- 我們傳遞給deallocate的指針不能為空,它必須指向由allocate分配的內存。而且, 傳遞給deallocate的大小參數必須與調用allocated分配內存時提供的大小參數具有一樣的值。
拷貝和填充未初始化內存的算法
- 標準庫還為allocator類定義了兩個伴隨算法,可以在未初始化內存中創建對象。 表 12.8描述了這些函數,它們都定義在頭文件memory中。
- 作為一個例子,假定有一個int的vector,希望將其內容拷貝到動態內存中。我們將分配一塊比vector中元素所占用空間大一倍的動態內存,然后將原vector中的元素拷貝到前一半空間,對后一半空間用一個給定值進行填充:
- //分配比vi中元素所占用空間大一倍的動態內存
- auto p=alloc.allocate(vi.size()*2);
- //通過拷貝vi中的元素來構造從p開始的元素
- auto q=uninitialized_copy(vi.begin(),vi.end(),p);
- //將剩余元素初始化為42'
- uninitialized_fill_n(q,vi.size(),42);
- 類似拷貝算法(參見10.2.2節,第341頁),uninitialized_copy接受三個迭代器參數。前兩個表示輸入序列,第三個表示這些元素將要拷貝到目的空間。傳遞給uninitialized_copy的目的位置迭代器必須指向未構造的內存。與copy不同,uninitialized_copy在給定目的位置構造元素。類似copy,uninitialized_copy返回(遞增后的)目的位置迭代器。因此,一次uninitialized_copy調用會返回一個指針,指向最后一個構造的元素之后的位置。
- 在本例中'我們將此指針保存在q中,然后將q傳遞給uninitialized_fill_no此函數類似fill_n(參見1022節,第340頁),接受一個指向目的位置的旨針、一個計數和一個值。它會在目的位置指針指向的內存中創建給定數目個對象,用給定值對它們進行初始化。
使用標準庫:文本查詢程序
- 我們將實現一個簡單的文本查詢程序,作為標準庫相關內容學習的總結。我們的程序允許用戶在一個給定文件中查詢單詞。查詢結果是單詞在文件中出現的次數及其所在行的列表。如果一個單詞在一行中出現多次,此行只列出一次。行會按照升序輸出—— 即,第7行會在第9行之前顯示,依此類推。例如,我們可能讀入一個包含本章內容(指英文版中的文本)的文件,在其中尋找單詞 element。輸出結果的前幾行應該是這樣的:
12.3.1文本查詢程序設計
- 開始一個程序的設計的一種好方法是列出程序的操作。了解需要哪些操作會幫助我們分析出需要什么樣的數據結構。從需求入手,我們的文本查詢程序需要完成如下任務:
- 當程序讀取輸入文件時,它必須記住單詞出現的每一行。因此,程序需要逐行讀取輸入文件,并將每一行分解為獨立的單詞
- 當程序生成輸出時,
- 1,它必須能提取每個單詞所關聯的行號
- 2,行號必須按升序出現且無重復
- 3,它必須能打印給定行號中的文本。
- 利用多種標準庫設施,我們可以很漂亮地實現這些要求:
- 我們將使用一個vector<string>來保存整個輸入文件的一份拷貝。輸入文件中的每行保存為vector中的一個元素。當需要打印一行時,可以用行號作為下標來提取行文本。
- 我們使用一個istringstream(參見8.3節,第287頁)來將每行分解為單詞。我們使用一個set來保存每個單詞在輸入文本中出現的行號。這保證了每行只出現一次且行號按升序保存。
- 我們使用一個map來將每個單詞與它出現的行號set關聯起來。這樣我們就可以方便地提取任意單詞的set。我們的解決方案還使用了shared_ptr,原因稍后進行解釋。
數據結構
- 雖然我們可以用vector,set和map來直接編寫文本杳詢程序,但如果定一個更為抽象的解決方案,會更為有效。我們將從定義一個保存輸入文件的類開始,這會令文件查詢更為容易。我們將這個類命名為TextQuery,它包含一個vector和一個map。vector用來保存輸入文件的文本,map用來關聯每個單詞和它出現的行號的set。這個類將會有一個用來讀取給定輸入文件的構造函數和一個執行查詢的操作。
- 查詢操作要完成的任務非常簡單:查找map成員,檢查給定單詞是否出現。設計這個函數的難點是確定應該返同什么內容。一旦找到了一個單詞,我們需要知道它出現了多
少次、它出現的行號以及每行的文本。 - 返回所有這些內容的最簡單的方法是定義另~個類,可以命名為QueryResult,來保存查詢結果。這個類會有一個print函數,完成結果打印工作。
在類之間共享數據
- 我們的QueryResult類要表達查詢的結果。這些結果包括與給定單詞關聯的行號的set和這些行對應的文本。這些數據都保存在TextQuery類型的對象中。由于QueryResult所需要的數據都保存在-TextQuery對象中,我們就必須確定如何訪問它們。我們可以拷貝行號的set,但這樣做可能很耗時。而且,我們當然不希望拷貝vector,因為這可能會引起整個文件的拷貝,而目標只不過是為了打印文件的小部分而已(通常會是這樣)。
- 通過返回指向TextQuery對象內部的迭代器(或指針),我們可以避免拷貝操作。但是,這種方法開啟了-個陷阱:如果TextQuery對象在對應的QueryResult對象之前被銷毀,會發生什么?在此情況下,QueryResult就將引用一個不再存在的對象中的數據。
- 對于QueryResult對象和對應的TextQuery對象的生存期應該同步這一觀察結果,其實已經暗示了問題的解決方案。考慮到這兩個類概念上“共享”了數據,可以使用shared_ptr(參見12.1.1節,第400頁)來反映數據結構中的這種共享關系。
使用TextQuery類
- 當我們設計一個類時,在真正實現成員之前先編寫程序使用這個類,是一種非常有用的方法。通過這種方法,可以看到類是否具有我們所需要的操作。例如,下面的程序使用TTextQuery和QueryResult類。這個函數接受一個指向要處理的文件的ifstream,并與用戶交互,打印給定單詞的查詢結果
#include <vector>
#include <memory>
#include <map>
#include <set>class QueryResult;//為了定義函數query返回的類型,這個定義是必須的
class TextQuery{
public:using line_no = std::vector<std::string>::size_type;TextQuery(std::ifstream&);QueryResult query(const std::string&) const;private:std::shared_ptr<std::vector<std::string>>file;//輸入文件//每個單詞到它所在的行號的集合的映射std::map<std::string,std::shared_ptr<std::set<line_no>>>wm;
};
TextQuery構造函數
- TextQuery的構造函數接受一個ifstream,逐行讀取輸入文件:
#include "TextQuery.h"//讀取文件并且建立單詞到行號的映射
TextQuery::TextQuery(std::ifstream &is) : file(new std::vector<std::string>) {std::string text;while (std::getline(is,text)){ //對文件的每一行file->push_back(text); //保存此行文本int n = file->size() - 1; //當前行號std::istringstream line(text); //將行文本分解為單詞std::string word;while (line >> word){ //對行中的每個單詞//如果單詞不在wm中,以之為下標在vm中添加一項auto &lines = wm[word]; //lines是一個shared_ptrif (!line){ //在第一次遇到這個單詞的時候,指針為空lines.reset(new std::set<line_no>);//分配一個新的set}lines->insert(n); //將此行號插入到set中}}
}
- 構造函數的初始化器分配一個新的vector來保存輸入文件中的文本。我們用getline逐行讀取輸入文件,并存入vector中。由于file是一個shared_ptr,我們用-〉運算符解引用file來提取file指向的vector對象的push_back成員。
- 接下來我們用一個istringstream (參見8.3節,第 287頁 )來處理剛剛讀入的一行中的每個單詞。內層while循環用istringstream的輸入運算符來從當前行讀取每個單詞,存入word中。在 while循環內,我們用map下標運算符提取與word相關聯的shared_ptr<set>,并將lines綁定到此指針。注意,lines是一個引用,因此改變 lines也會改變wm 中的元素。
- 若 word不在map中,下標運算符會將word添加到w m 中 (參 見 11.3.4節,第 387頁),與 word關聯的值進行值初始化。這意味著,如果下標運算符將word添加到w m 中,lines將是一個空指針。如果lines為空,我們分配一個新的set,并調用reset更新 lines引用的shared_ptr,使其指向這個新分配的set。
- 不管是否創建了一個新的set,我們都調用insert將當前行號添加到set中。由于 lines是一個引用,對 insert的調用會將新元素添加到w m 中的set中。如 果 一 個 給定單詞在同一行中出現多次,對 insert的調用什么都不會做。
QueryResult 類
- QueryResult類有三個數據成員:一個string,保存查詢單詞; -個shared_ptr, 指向保存輸入文件的vector; 一個shared_ptr,指向保存單詞出現行號的set。它唯一的一個成員函數是一個構造函數,初始化這三個數據成員
query函數
- query函數接受一個string參數,即查詢單詞,query用它來在map中定位對應的行號set。如果找到了這個string, query函數構造一個 QueryResult.保存給定 string, TextQuery的 file成員以及從wm中提取的set。
- 唯一的問題是:如果給定string未找到,我們應該返回什么?在這種情況下,沒有 可返回的set。為了解決此問題,我們定義了一個局部static對象,它是一個指向空的 行號set的 shared_ptro當未找到給定單詞時,我們返回此對象的一個拷貝:
參考鏈接
- cppreference.com shared_ptr