智能指針
為了使管理動(dòng)態(tài)內(nèi)存更容易、更安全,新標(biāo)準(zhǔn)庫提供了兩種管理動(dòng)態(tài)對(duì)象的智能指針類型少梁。智能指針的作用類似于常規(guī)指針,但格外重要的是它會(huì)自動(dòng)刪除它指向的對(duì)象矫付。新標(biāo)準(zhǔn)庫定義了兩種智能指針凯沪,它們?cè)诠芾淼讓又羔樂矫嬗兴煌?code>shared_ptr(允許多個(gè)指針引用同一個(gè)對(duì)象)和unique_ptr
(“擁有”它指向的對(duì)象)。該庫還定義了一個(gè)名為weak_ptr
的伴隨類买优,它是對(duì)shared_ptr
管理的對(duì)象的弱引用妨马。這三個(gè)類都在頭文件<memory>
中定義。
關(guān)于頭文件<memory>
的更多信息請(qǐng)參考header <memory>杀赢。
shared_ptr類
與vector
一樣烘跺,智能指針也是模板。因此脂崔,當(dāng)我們創(chuàng)建智能指針時(shí)滤淳,我們必須提供附加信息,即在這種情況下砌左,指針可以指向的類型脖咐。與vector
一樣,我們?cè)诩饫ㄌ?hào)內(nèi)提供該類型汇歹,它遵循我們定義的智能指針類型的名稱:
Code:
shared_ptr<string> p1; // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints
默認(rèn)初始化智能指針為空指針文搂。我們也可以使用其他初始化摸板類的方法對(duì)其進(jìn)行初始化。
我們使用智能指針的方式類似于使用指針秤朗。解引用智能指針會(huì)返回指針指向的對(duì)象煤蹭。當(dāng)我們?cè)跅l件中使用智能指針時(shí),效果是測(cè)試指針是否為空:
Code:
// if p1 is not null, check whether it's the empty string
if (p1 && p1->empty())
*p1 = "hi"; // if so, dereference p1 to assign a new value to that string
下表中列出了shared_ptr
和unique_ptr
共有的操作:
操作 | 功能 |
---|---|
shared_ptr<T> sp unique_ptr<T> up
|
生成一個(gè)可以指向T類型對(duì)象的空指針取视。 |
p |
用p作為條件硝皂,如果p指向一個(gè)對(duì)象則返回true。 |
*p |
解引用p來獲得p指向的對(duì)象 |
p->mem |
等效于(*p).mem 作谭。 |
p.get() |
返回p中的指針稽物。請(qǐng)謹(jǐn)慎使用;當(dāng)智能指針要?jiǎng)h除它時(shí)折欠,返回的指針指向的對(duì)象將消失贝或,這樣返回的指針就是一個(gè)空指針吼过。 |
swap(p, q) p.swp(q)
|
交換p和q中的指針。 |
下表中列出了shared_ptr
獨(dú)有的操作:
操作 | 功能 |
---|---|
make_shared<T> (args) |
返回指向類型為T的動(dòng)態(tài)分配對(duì)象的shared_ptr 咪奖。使用args 初始化該對(duì)象盗忱。 |
shared_ptr<T> p(q) |
p 是shared_ptr 對(duì)象q 的副本; 增加q 中的計(jì)數(shù)。q 中的指針必須可轉(zhuǎn)換為T * 羊赵。 |
p = q |
p 和q 是shared_ptr 趟佃,它們包含可以相互轉(zhuǎn)換的指針。減少p 的引用計(jì)數(shù)并增加q 的計(jì)數(shù); 如果p 的計(jì)數(shù)變?yōu)?昧捷,則刪除p 的現(xiàn)有內(nèi)存闲昭。 |
p.unique() |
如果p.use_count() 是1則返回true,否則返回false靡挥。 |
p.use_count() |
返回與p 共享的對(duì)象數(shù)序矩;這可能是一個(gè)慢速的操作,主要用于調(diào)試目的跋破。 |
關(guān)于shared_ptr
更多信息請(qǐng)參考header <shared_ptr>贮泞。
動(dòng)態(tài)分配對(duì)象的列表初始化
我們可以使用直接初始化的方式來初始化動(dòng)態(tài)分配的對(duì)象。我們也可以使用傳統(tǒng)構(gòu)造的構(gòu)造方式(使用括號(hào))進(jìn)行初始化幔烛。在新標(biāo)準(zhǔn)下,我們也可以使用列表初始化的方式(帶花括號(hào))進(jìn)行初始化:
Code:
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9'); // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
我們還可以通過使用類型名稱后跟一對(duì)空括號(hào)來初始化動(dòng)態(tài)分配的對(duì)象:
Code:
string *ps1 = new string; // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int; // default initialized; *pi1 is undefined
int *pi2 = new int(); // value initialized to 0; *pi2 is 0
對(duì)于定義了自己的構(gòu)造函數(shù)的類類型(如string
)囊蓝,類型名稱后面是否后跟一對(duì)空括號(hào)無關(guān)緊要饿悬;無論形式如何,對(duì)象都由默認(rèn)構(gòu)造函數(shù)進(jìn)行初始化聚霜。在內(nèi)置類型的情況下妖谴,類型名稱后面是否后跟一對(duì)空括號(hào)的差異是顯著的驮瞧;內(nèi)置類型的值初始化對(duì)象具有明確定義的值,但默認(rèn)初始化對(duì)象不具有。同樣实牡,對(duì)于那些依賴于編譯器默認(rèn)生成的構(gòu)造函數(shù)的類,類中的內(nèi)置類型的成員沒有在類體中初始化据悔,那么這些成員也將未初始化岳守。
與我們通常初始化變量的原因相同,初始化動(dòng)態(tài)分配的對(duì)象也是一個(gè)好主意凉唐。
auto和動(dòng)態(tài)分配
當(dāng)我們?cè)诶ㄌ?hào)內(nèi)提供初始化器時(shí)庸追,我們可以使用auto
推導(dǎo)出我們想要從該初始化器中生成的對(duì)象的類型。但是台囱,因?yàn)榫幾g器使用初始化器的類型來推斷要分配的類型淡溯,所以我們只能在括號(hào)內(nèi)使用auto
和一個(gè)初始化器:
Code:
auto p1 = new auto(obj); // p points to an object of the type of obj
// that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer
p1
是一個(gè)指向由obj
通過auto
推導(dǎo)出的類型的指針。如果obj
是int
簿训,則p1
是int *
咱娶;如果obj
是string
米间,則p1
是string *
;依此類推膘侮。新分配的對(duì)象由obj的值進(jìn)行初始化屈糊。
unique_ptr類
unique_ptr
“擁有”它指向的對(duì)象。與shared_ptr
不同喻喳,一次只能有一個(gè)unique_ptr
指向給定對(duì)象另玖。當(dāng)unique_ptr
被銷毀時(shí),unique_ptr
指向的對(duì)象將被銷毀表伦。下表列出了unique_ptr
特有的操作谦去。
操作 | 功能 |
---|---|
unique_ptr<T> u1 unique_ptr<T, D> u2
|
生成一個(gè)指向類型T 的空unique_ptr 。.u1 將使用delete 來釋放它的指針; u2 將使用類型為D 的可調(diào)用對(duì)象來釋放其指針蹦哼。 |
unique_ptr<T, D> u(d) |
生成一個(gè)指向類型T 且使用d 的空unique_ptr 鳄哭。d 必須是類型D 的對(duì)象而不是delete 。 |
u = nullptr u.release()
|
刪除u 指向的對(duì)象纲熏,并將u 置位空妆丘。 |
u.reset() u.reset(q) u.reset(nullptr)
|
刪除u 指向的對(duì)象。如果提供了內(nèi)置指針q 局劲,則指向該對(duì)象勺拣。 否則使u 為空。 |
與shared_ptr
不同鱼填,沒有與make_shared
相類似的庫函數(shù)返回unique_ptr
药有。相反,當(dāng)我們定義unique_ptr
時(shí)苹丸,我們將它綁定到new
返回的指針愤惰,與shared_ptr
一樣,我們必須使用直接初始化形式對(duì)其進(jìn)行初始化:
Code:
unique_ptr<double> p1; // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42
因?yàn)?code>unique_ptr擁有它指向的對(duì)象赘理,所以unique_ptr
不支持普通拷貝或賦值:
Code:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2; // error: no assign for unique_ptr
雖然我們無法拷貝或?qū)?code>unique_ptr進(jìn)行賦值宦言,但我們可以通過調(diào)用release
或reset
將所有權(quán)從一個(gè)(nonconst
)unique_ptr
轉(zhuǎn)移到另一個(gè)(nonconst
)unique_ptr
:
Code:
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed
release
成員函數(shù)返回當(dāng)前存儲(chǔ)在unique_ptr
中的指針,并使unique_ptr
為null
商模。因此奠旺,p2
由存儲(chǔ)在p1
中的指針值初始化,p1
變?yōu)榭铡?br>
reset
成員接收一個(gè)可選指針施流,并重新定位unique_ptr
以指向給定指針凉倚。如果unique_ptr
不為null
,則刪除unique_ptr
指向的對(duì)象嫂沉。因此稽寒,對(duì)p2
進(jìn)行的reset
調(diào)用釋放了從“Stegosaurus”初始化的字符串所使用的內(nèi)存,將p3
的指針傳遞給p2
趟章,并使p3
為空杏糙。
調(diào)用release
會(huì)破壞unique_ptr
與其管理的對(duì)象之間的連接慎王。通常由release
返回的指針用于初始化或分配另一個(gè)智能指針。在這種情況下宏侍,管理內(nèi)存的責(zé)任只是從一個(gè)智能指針轉(zhuǎn)移到另一個(gè)智能指針赖淤。但是,如果我們不使用另一個(gè)智能指針來保存從release
返回的指針谅河,我們的程序?qū)⒇?fù)責(zé)釋放該資源:
Code:
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)
關(guān)于unique_str
更多信息請(qǐng)參考header <unique_str>咱旱。
weak_ptr類
weak_ptr
是一個(gè)智能指針,它不控制它指向的對(duì)象的生命周期绷耍。相反吐限,weak_ptr
指向由shared_ptr
管理的對(duì)象。將weak_ptr
綁定到shared_ptr
不會(huì)更改該shared_ptr
的引用計(jì)數(shù)褂始。一旦指向該對(duì)象的最后一個(gè)shared_ptr
消失诸典,該對(duì)象本身將被刪除。即使有指向它的weak_ptr
崎苗,該對(duì)象也將被刪除狐粱。因此名稱為weak_ptr
,它強(qiáng)調(diào)了weak_ptr
“弱”共享其對(duì)象的想法胆数。
下表列出了weak_ptr
的相關(guān)操作:
操作 | 功能 |
---|---|
weak_ptr<T> w |
生成一個(gè)指向類型T對(duì)象的空weak_ptr 肌蜻。 |
weak_ptr<T> w(sp) |
weak_ptr 指向與shared_ptr 對(duì)象sp 相同的對(duì)象。類型T 必須可以轉(zhuǎn)換為sp 指向的類型必尼。 |
w = p |
p 可以是shared_ptr 或weak_ptr 蒋搜。賦值后w 與p 共享所有權(quán)。 |
w.reset() |
使w 為空胰伍。 |
w.use_count() |
返回與w 共享所有權(quán)的shared_ptr 數(shù)。 |
w.expired() |
如果w.use_count() 為0酸休,則返回true骂租;否則返回false。 |
w.lock() |
如果w.expired 為true斑司,則返回一個(gè)空shared_ptr 渗饮;否則將返回一個(gè)指向w 指向的對(duì)象的shared_ptr 。 |
當(dāng)我們創(chuàng)建一個(gè)weak_ptr
時(shí)宿刮,我們用shared_ptr
初始化它:
Code:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
這里wp
和p
都指向同一個(gè)對(duì)象互站。由于共享較弱,創(chuàng)建wp
不會(huì)改變p
的引用計(jì)數(shù)僵缺;wp
點(diǎn)指向的對(duì)象可能會(huì)被刪除胡桃。
因?yàn)閷?duì)象可能不再存在,所以我們不能使用weak_ptr
直接訪問其對(duì)象磕潮。要訪問該對(duì)象翠胰,我們必須調(diào)用lock
容贝。lock
函數(shù)檢查weak_ptr
指向的仍然存在的對(duì)象。如果存在之景,lock
會(huì)將shared_ptr
返回給共享對(duì)象斤富。與任何其他shared_ptr
一樣,我們保證shared_ptr
指向的底層對(duì)象至少在shared_ptr
存在的情況下仍然存在锻狗。例如:
Code:
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
// inside the if, np shares its object with p
}
這里满力,我們只在調(diào)用lock
成功時(shí)才進(jìn)入if
的主體。在if
中轻纪,使用np
訪問該對(duì)象是安全的油额。
關(guān)于weak_str
更多信息請(qǐng)參考header <weak_str>。
for范圍聲明不適用動(dòng)態(tài)分配的數(shù)組
(array type)桐磁!所以不能對(duì)其使用for
范圍聲明悔耘!
動(dòng)態(tài)數(shù)組的列表初始化
新標(biāo)準(zhǔn)下,我們可以使用列表初始化器來初始化一個(gè)動(dòng)態(tài)數(shù)組:
Code:
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a", "an", "the", string(3,'x')};
當(dāng)我們列出初始化內(nèi)置數(shù)組類型的對(duì)象時(shí)我擂,初始化器用于初始化數(shù)組中的第一個(gè)元素衬以。如果初始值設(shè)定項(xiàng)的個(gè)數(shù)少于元素個(gè)數(shù),則其余元素是值初始化的校摩。如果初始值設(shè)定項(xiàng)多于給定的大小看峻,那么new
表達(dá)式將失敗,并且不會(huì)分配任何內(nèi)存衙吩。在這種情況下互妓,new
會(huì)拋出bad_array_new_length
類型的異常。與bad_alloc
一樣坤塞,此類型在new
頭中定義冯勉。關(guān)于頭文件new
更多的信息可參考header <new>。
雖然我們可以使用空括號(hào)來值初始化數(shù)組的元素摹芙,但是我們不能在括號(hào)內(nèi)提供元素初始值設(shè)定項(xiàng)灼狰。事實(shí)上,我們不能在括號(hào)內(nèi)提供初始值浮禾,這意味著我們不能使用auto
來申請(qǐng)分配數(shù)組交胚。
動(dòng)態(tài)分配空數(shù)組是合法的
我們可以使用任意表達(dá)式來確定要分配的對(duì)象數(shù)量:
Code:
size_t n = get_size(); // get_size returns the number of elements needed
int* p = new int[n]; // allocate an array to hold the elements
for (int* q = p; q != p + n; ++q)
/* process the array*/ ;
一個(gè)有趣的問題出現(xiàn)了:如果get_size
返回0會(huì)發(fā)生什么?答案是我們的代碼正常工作盈电。即使我們不能創(chuàng)建大小為0的數(shù)組變量蝴簇,使用n
等于0調(diào)用new [n]
仍然是合法的:
Code:
char arr[0]; // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
當(dāng)我們使用new
來分配一個(gè)大小為零的數(shù)組時(shí),new
返回一個(gè)有效的非零指針匆帚。該指針與new
返回的任何其他指針不同熬词。此指針用作零元素?cái)?shù)組的非結(jié)束指針(off-the-end pointer)。我們可以通過一個(gè)非結(jié)束迭代器(off-the-end iterator)來使用這個(gè)指針吸重〉磁欤可以像在上述代碼中的for
循環(huán)那樣比較指針均践。我們可以在這樣的指針上加零(或從中減去零),也可以減去其自身摩幔,得到零彤委。 這種指針不能被解引用。畢竟或衡,它指向的數(shù)組中沒有元素焦影。
在上述代碼中,如果get_size
返回0封断,則n
也為0斯辰。對(duì)new
的調(diào)用將分配一個(gè)大小為零的數(shù)組。for
中的判定條件將失斊绿邸(p
等于q + n
彬呻,因?yàn)?code>n為0)。因此柄瑰,不執(zhí)行for
循環(huán)體闸氮。
allocator::construct可以使用任何構(gòu)造函數(shù)
allocator
分配的內(nèi)存是未構(gòu)造的。我們通過在該內(nèi)存中構(gòu)造對(duì)象來使用此內(nèi)存教沾。在新標(biāo)準(zhǔn)庫中蒲跨,construct
成員接收一個(gè)指針和零個(gè)或多個(gè)附加參數(shù),它在給定位置構(gòu)造一個(gè)元素授翻,附加參數(shù)用于初始化正在構(gòu)造的對(duì)象或悲。與make_shared
的參數(shù)一樣,這些附加參數(shù)必須是正在構(gòu)造的類型的對(duì)象的有效初始值設(shè)定項(xiàng)堪唐。特別是巡语,如果對(duì)象是類類型,則這些參數(shù)必須與該類的構(gòu)造函數(shù)匹配:
Code:
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
在早期版本的標(biāo)準(zhǔn)庫中淮菠,construct
只接收兩個(gè)參數(shù):構(gòu)造對(duì)象的指針和元素類型的值男公。因此,我們只能將一個(gè)元素復(fù)制到未構(gòu)造的空間中兜材,我們不能對(duì)元素類型使用任何其他構(gòu)造函數(shù)理澎。
使用尚未構(gòu)造對(duì)象的原始內(nèi)存是錯(cuò)誤的:
Code:
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!
注:我們必須construct
對(duì)象以便使用allocate
返回的內(nèi)存逞力。以其他方式使用未構(gòu)造的內(nèi)存是未定義的曙寡。
當(dāng)我們完成對(duì)象的使用時(shí),我們必須銷毀我們構(gòu)造的元素寇荧,我們通過在每個(gè)構(gòu)造的元素上調(diào)用destroy
來實(shí)現(xiàn)举庶。destroy
函數(shù)接受一個(gè)指針并在指向的對(duì)象上運(yùn)行析構(gòu)函數(shù):
Code:
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated
在循環(huán)開始時(shí),q
指向最后一個(gè)構(gòu)造元素的后一位置揩抡。我們?cè)谡{(diào)用destroy
之前遞減q
户侥。因此镀琉,在第一次調(diào)用調(diào)用destroy
時(shí),q
指向最后構(gòu)造的元素蕊唐。我們?cè)谧詈笠淮蔚衐estroy第一個(gè)元素屋摔,之后q
將等于p
并且循環(huán)結(jié)束。
一旦元素被destroy替梨,我們可以重用內(nèi)存來保存其他字符串或?qū)?nèi)存返回給系統(tǒng)钓试。我們通過調(diào)用deallocate
來釋放內(nèi)存:
alloc.deallocate(p, n);
我們傳遞給deallocate
的指針不能為null
;它必須指向由allocate
分配的內(nèi)存副瀑。而且弓熏,傳遞給deallocate
的size
參數(shù)必須與調(diào)用allocate
中使用的大小相同才能獲得指針?biāo)赶虻膬?nèi)存。
關(guān)于allocator::construct
的更多信息可參考std::allocator::construct糠睡。
參考文獻(xiàn)
[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.