文章目錄
- 三種對象的分類
- 三種內存的區別
- 動態內存
- 概念
- 智能指針允許的操作
- 智能指針的使用規范
- new
- 概念
- 內存耗盡/定位new
- 初始化
- 默認初始化
- 直接初始化
- 值初始化
- delete
- 概念
- 手動釋放動態對象
- 空懸指針
- shared_ptr類
- 格式
- 獨有的操作
- make_shared函數
- shared_ptr的計數器
- 通過new用普通指針初始化shared_ptr
- unique_ptr
- 概念、初始化、特性
- 支持的操作
- weak_ptr
- 概念
- 關于普通指針和智能指針
- 不能使用內置指針來訪問shared_ptr所指向的內存
- get()函數
- reset函數
- 處理異常
- 動態數組
- 概念
- new分配對象數組
- 兩種方法
- new的返回值
- 初始化
- 動態分配一個空數組是合法的
- 釋放動態數組
- 動態數組和unique_ptr
- 動態數組和shared_ptr
- allocator類
- new的局限性(使用allocator的原因)
- 概念即創建銷毀操作
- construct
- destroy
- deallocate
- 兩個伴隨算法
- 使用了動態生存期的資源的類
- 實例
三種對象的分類
三種對象:
- 全局對象在程序啟動時分配,在程序結束時銷毀。
- 局部自動對象,當我們進入其定義所在的程序塊時被創建,在離開塊時銷毀。
- 局部static對象在第一次使用前分配,在程序結束時銷毀。
三種內存的區別
- 靜態存儲區:主要存放static靜態變量、全局變量、常量。這些數據內存在編譯的時候就已經為他們分配好了內存,生命周期是整個程序從運行到結束。
- 棧區:存放局部變量。在執行函數的時候(包括main這樣的函數),函數內的局部變量的存儲單元會在棧上創建,函數執行完自動釋放,生命周期是從該函數的開始執行到結束。線性結構。
- 堆區:程序員自己申請的任意大小的內存。一直存在直到被釋放。鏈表結構。
前兩種內存中的將對象由編譯器自動創建和銷毀。堆也被稱作自由空間,被用來存儲動態分配的對象(程序運行時分配的對象),動態對象的生存期由程序來控制——當動態對象不被使用時,必須顯式地消滅他們。
動態內存
概念
為什么要使用動態內存:
- 程序不知道自己需要使用多少對象
- 程序不知道所需對象的準確類型
- 程序需要在多個對象間共享數據
動態內存的分配與釋放通過一對運算符來完成:
- new:在動態內存中為對象分配空間并返回一個指向該對象的指針,可以選擇對對象進行初始化;
- delete:接受一個動態對象的指針,銷毀該對象,并釋放與之關聯的內存。
使用動態內存時容易出現的問題:
- 忘記釋放內存,產生的內存泄漏。這種內存永遠不可能被歸還給自由空間了。查找本錯誤是非常困難的,通常應用程序運行很長時間之后,真正耗盡內存時,才能檢測到這種錯誤。
- 在尚有指針引用內存的情況下釋放內存,產生引用非法內存的指針
- 釋放一個已經被delete的內存,產生double free的問題。出現此操作時,自由空間就可能被破壞。
為了避免上述問題,c++11提供了兩種智能指針(smart pointer)類型來管理動態對象,兩種指針的區別在于管理底層指針的方式:
- shared_ptr:允許多個指針指向同一個對象
- unique_ptr:獨占所指向的對象
除此之外,標準庫還定義了一個名為weak_ptr的伴隨類,他是一種弱引用,指向shared_ptr所管理的對象。這三種類型都定義在memory頭文件中。
智能指針允許的操作
智能指針的使用規范
- 不使用相同的內置指針值初始化(或reset)多個智能指針。
- 不delete get()返回的指針。
- 不使用get()初始化或reset另一個智能指針。
- 如果你使用get()返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了。
- 如果你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器。
new
概念
在自由空間分配的內存是無名的,因此new無法為其分配的對象命名,而是返回一個指向該對象的指針:
int *pi = new int; // pi指向一個動態分配的、未初始化的無名對象
當然用new分配const的對象也是合法的,但是一個動態分配的const對象必須進行初始化:
- 定義了默認構造函數的類類型,const動態對象可以隱式初始化
- 其他類型必須顯式初始化
- 由于分配的對象是const的,因此new返回的指針是一個指向const的指針
內存耗盡/定位new
值得一提的是,如果一個程序用光了它所有可用的內存,new表達式就會失敗。 默認情況下。如果new不能分配所要求的內存空間,就會拋出一個bad_alloc的異常。可以改變使用new的方式來阻止它拋出異常:
我們稱上面形式的new未定位new(placement new),定位new表達式允許我們向new傳遞額外的參數。上例中,我們傳遞給它一個由標準庫定義的名為nothrow的對象,將nothrow傳遞給new,意圖是告訴它不能拋出異常。
bad_alloc和northrow都定義在頭文件new中。
初始化
默認初始化
默認情況下,動態分配的對象是默認初始化的,這意味著內置類型或組合類型的對象的值將是未定義的,而類類型對象將用默認構造函數進行初始化:
直接初始化
為了避免未定義行為,最好使用直接初始化的方式來初始化一個動態分配的對象:
- 可以用圓括號
- 可以用列表初始化
值初始化
也可以使用值初始化,只需要直接在類型名之后跟一對空括號即可:
值得一提的是:
- 對于定義了自己的構造函數的類類型來說,要求值初始化是沒有意義的——不管采用什么形式,對象都會通過默認構造函數來初始化。
- 對于內置類型,兩種形式的差別就很大了;值初始化的內置類型對象有著良好定義的值,而默認初始化的對象的值則是未定義的。類似的,對于類中那些依賴于編譯器合成的默認構造函數的內置類型成員,如果它們未在類內被初始化,那么它們的值也是未定義的。
delete
概念
為了防止內存耗盡,在動態內存使用完畢后,必須通過delete表達式將動態內存歸還給系統。
默認情況下shared_ptr和unique_ptr都使用delete釋放指向的對象,但也都允許重載默認的刪除器(delete)。
delete執行兩個動作:
- 銷毀給定的指針指向的對象
- 釋放對應的內存
delete表達式接受一個指針,指向我們想要釋放的對象,該指針必須指向動態分配的內存,或者是一個空指針。
通常情況下,
- 編譯器不能分辨一個指針指向靜態還是動態分配的對象
- 編譯器不能分辨一個指針所指向的內存是否已經被釋放
對于上述兩種情況,大多數編譯器會編譯通過,盡管他們是錯誤的。
因此,釋放一塊非new分配的內存或者將相同的指針值多次釋放,其行為是未定義的:
另外,const對象的值雖然不能夠被改變,但是其本身可以被銷毀:
const int *pci = new const int(1024);
delete pci; // 正確:釋放一個const對象
手動釋放動態對象
智能指針可以在計數值為0時自動釋放動態對象,而delete是一種手動釋放動態對象的方式,這就要求程序員不能忘記delete這一步驟。
與類類型不同,內置類型的對象被銷毀時什么也不會發生。 特別是,當一個指針離開其作用域時,它所指向的對象什么也不會發生。如果這個指針指向的是動態內存,那么內存將不會被自動釋放。
舉個例子:
foo *factory(T arg){return new Foo(arg); // 調用factory的對象負責釋放動態內存
}void use_factory(T arg){Foo *p = factory(arg);
} // p離開了它的作用域,但實際所指向的內存沒有被釋放
本例中,一旦use_factory返回,程序就沒有辦法釋放這塊內存了。修正這個錯誤的唯一方法是在use_factory中記得釋放內存:
void use_factory(T arg){Foo *p = factory(arg);delete p;
}
空懸指針
執行delete p;
后,p并不指向空指針,相反的,p的值(指向的地址)不變,但不能再使用p處理該地址的內容(指針失效),也不能重復delete p。 此時p
就變成了空懸指針(dangling pointer),即指向一塊曾經保存數據對象但現在已經無效的內存的指針。
不能重復delete p
:
但是可以重復 delete
空指針:
避免空懸指針有兩種方法:
- 在指針即將要離開其作用域之前釋放掉它所關聯的內存。這樣,在指針關聯的內存被釋放掉后,就沒有機會繼續使用指針了。
- 也可以在delete之后將nullptr賦予指針,這樣就清楚地指出指針不指向任何對象。
但重置指針地方法仍然不是完美的,動態內存的一個基本問題是可能有多個指針指向相同的內存。在delete內存之后重置指針的方法只對這個指針有效,對其他任何仍指向(已釋放的)內存的指針是沒有作用的,然而在實際中,查找只想相同內存地所有指針也是異常困難的:
shared_ptr類
格式
shared_ptr<類型>
默認初始化的智能指針中保存著一個空指針。
獨有的操作
關于上面兩表的具體操作將在下面的普通指針和智能指針中指出。
make_shared函數
shared_ptr可以協調對象的析構,但這僅限于其自身的拷貝(也是shared_ptr)之間。因此最安全的分配和使用動態內存的方法是調用一個名為make_shared的標準庫函數,而不是new。這樣,我們就能在分配對象的同時就將shared_ptr與之綁定,從而避免了無意中將同一塊內存綁定到多個獨立創建的shared_ptr上。
make_shared函數定義在頭文件memory中。
功能:在動態內存中分配一個對象并初始化它,返回此對象的shared_ptr。
實例:
調用make_shared<T>
時傳遞的參數必須與T的某個構造函數相匹配,換言之,調用make_shared的行為的底層操作其實是調用對應類型的構造函數。
當然,用auto定義一個對象來保存make_shared的結果也是可以的:
auto p = make_shared<vector<string>>();
shared_ptr的計數器
因為shared_ptr允許多個指針指向同一個對象。因此每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數(reference count),用來記錄有多少個其他shared_ptr指向相同的對象。
當
- 用一個shared_ptr初始化另一個shared_ptr
- shared_ptr作為參數傳遞給一個函數
- shared_ptr作為函數的返回值
時,shared_ptr所關聯的計數器就會遞增。
當
- 給shared_ptr賦予一個新值(舊值計數器遞減,新值計數器遞增)
- shared_ptr被銷毀(例如一個局部的shared_ptr離開其作用域)
時,shared_ptr所關聯的計數器就會遞減。
一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的對象。
由于在最后一個shared_ptr銷毀前內存都不會釋放, 保證shared_ptr在無用之后不再保留就非常重要了。如果你忘記了銷毀程序不再需要的shared_ptr,程序仍會正確執行,但會浪費內存。
share_ptr在無用之后仍然保留的一種可能情況是,你將shared_ptr存放在一個容器中,隨后重排了容器,從而不再需要某些元素。在這種情況下,你應該確保用erase刪除那些不再需要的shared_ptr元素。
通過new用普通指針初始化shared_ptr
可以使用new返回的指針來初始化智能指針。接受指針參數的智能指針構造函數是explicit的。因此,我們不能進行內置指針到智能指針間的隱式轉換,必須使用直接初始化形式來初始化一個智能指針:
p1的初始化隱式地要求編譯器將一個new返回的int*隱式轉換成一個shared_ptr,這是不被允許的。
同樣的,一個返回shared_ptr的函數不能在其返回語句中隱式轉換一個普通指針:
必須將shared_ptr顯式綁定到一個想要返回的指針上:
unique_ptr
概念、初始化、特性
某個時刻只能有一個unique_ptr指向一個給定對象。unique_ptr被銷毀時,它所指向的對象也被銷毀。
unique_ptr沒有類似make_shared的標準庫函數。定義一個unique_ptr時,需要將其綁定到一個new返回的指針上,且必須采用直接初始化形式:
unique_ptr<double> pb;
unique_ptr<int> pi(new int(2));
根據“獨占”的特性,unique_ptr不支持普通的拷貝或賦值操作:
不能拷貝unique_ptr的規則有個例外:可以拷貝或賦值一個將要被銷毀的unique_ptr。
常見的例子是從函數返回一個unique_ptr:
或者返回一個局部對象的拷貝:
支持的操作
可以通過release或reset起到類似拷貝或賦值的作用:
release會切斷unique_ptr和它原來管理的對象間的聯系,返回的指針常被用來初始化另一個智能指針或給另一個智能指針賦值。但是,如果不用另一個智能指針來保存release返回的指針,就要記得手動釋放資源:
weak_ptr
概念
weak_ptr是一種不控制所指向對象生存期的智能指針,它指向一個由shared_ptr管理的對象。
具有以下特點:
- 將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數
- 引用計數歸零時,即使仍有weak_ptr指向對象,對象還是會被釋放
由于對象可能不存在,我們不能使用weak_ptr直接訪問對象,必須調用lock。因此可以這樣使用:
關于普通指針和智能指針
不能使用內置指針來訪問shared_ptr所指向的內存
當將一個shared_ptr綁定到一個普通指針時,我們就將內存的管理責任交給了這個shared_ptr,不應該再使用內置指針來訪問shared_ptr所指向的內存了。
使用一個內置指針來訪問一個智能指針所負責的對象是很危險的,因為我們無法知道對象何時會被銷毀。
舉例:
對于上面的函數,以智能指針作為參數以傳值方式傳遞是安全的,當process結束時,ptr的引用計數為1,因此雖然局部ptr被銷毀,但是ptr指向的內存不會被釋放:
但同時也可以傳遞給process一個用內置指針顯式構造的臨時shared_ptr。但是這樣做的風險是很大的:
process(x);
結束時,臨時對象被銷毀,其引用計數為0,指向的內存會被釋放,此時x變成了空懸指針。
get()函數
get函數返回一個內置指針,指向智能指針管理的對象。
函數是為了這種情況設計的:我們需要向不能使用智能指針的代碼傳遞一個內置指針。
使用get返回的指針的代碼不能delete此指針。
雖然編譯器不會給出錯誤信息,但是將另一個智能指針也綁定到get返回的指針上是錯誤的:
上述代碼中,shared_ptr<int>(q);
將另一個指針綁定到get返回的指針上,會導致程序塊結束時p指向的內存被釋放,p變成空懸指針。
reset函數
reset將一個新的指針賦予shared_ptr:
shared_ptr<int> p = new int(1024); // error:不能將一個普通指針賦予shared_ptr
p.reset(new int(1024)); // 正確:p指向一個新對象
與賦值類似,reset會更新引用計數,在需要的時候,可以釋放p指向的對象。
reset成員經常與unique一起使用,來控制多個shared_ptr共享的對象。在改變底層對象之前,我們檢查自己是否是當前對象僅有的用戶。如果不是,在改變之前要制作一份新的拷貝:
if(!p.unique()){p.reset(new string(*p)); // 不是舊對象僅有的指針,分配新拷貝}*p += newVal; // 是舊對象僅有的指針,直接改變對象的值,因為不會再有別的指針訪問舊對象
處理異常
當處理異常時,經常會使程序塊過早結束,也就是如果使用普通指針管理內存,可能在遇到detele之前推出程序塊:
void f()
{int *ip = new int(2);// throw一個異常且在f中未被捕獲delete ip; //沒能正常退出因此無法調用本句釋放內存
}
如果ip是shared_ptr類型則不會出現內存泄漏的情況,在程序塊結束時,自動釋放內存。
動態數組
概念
動態數組并不是數組類型。
new和delete運算符一次分配/釋放一個對象,但某些應用需要一次為很多對象分配內存的功能。
- 使用容器的類可以使用默認版本的拷貝、賦值和析構操作。
- 分配動態數組的類必須定義自己版本的操作,在拷貝、復制以及銷毀對象時管理所關聯的內存。
new分配對象數組
兩種方法
方法一:在類型名之后跟一對方括號,在其中指明要分配的對象的數目,方括號中的大小必須是整形,但不必是常量。
例如:
// 調用get_size確定分配多少個int
int *pia = new int[get_size()]; // pia指向第一個int
方法二:也可以用一個表示數組類型的類型別名來分配一個數組,這樣,new表達式中就不需要方括號了:
typedef int arrT[10]; // arrT表示10個int的數組類型
int *p = new arrT; // 分配一個10個int的數組;p指向第一個int
但編譯器在執行這個表達式時還是會用new[]:
int *p = new int[42];
new的返回值
當用new分配一個數組時,我們并未得到一個數組類型對象,而是得到一個數組元素類型的指針。因此:
- 不能對動態數組調用begin或end。這些函數使用數組維度來返回指向首元素和尾后元素的指針。
- 不能用范圍for語句來處理動態數組中的元素
初始化
默認情況下,new分配的對象,不管是單個的還是數組中的,都是默認初始化的。也可以通過一對空括號進行值初始化:
以及提供一個元素初始化器的花括號列表:
- 初始化器數目小于元素數目,剩余元素進行值初始化。
- 初始化器數目大于元素數目,new表達式失敗,不會分配任何內存。
new表達式失敗時會拋出一個類型為bad_array_new_length的異常。類似bad_alloc,定義在頭文件new中。
值得一提的是: 雖然我們用空括號對數組中元素進行值初始化,但不能在括號中給出初始化器,這意味著不能用auto分配數組。 因為auto是編譯器根據初始化值來判斷類型的,使用auto就必須有初始值,沒有初始值(這里是初始化器)auto自然也就不可以用了。
動態分配一個空數組是合法的
雖然我們不能創建一個大小為0的靜態數組對象,但當n等于0時,調用new[n]是合法的:
當我們用new分配一個大小為0的數組時,new返回一個合法的非空指針。此指針保證與new返回的其他任何指針都不相同。對于零長度的數組來說,此指針就像尾后指針一樣,我們可以像使用尾后迭代器一樣使用這個指針。可以用此指針進行比較操作,但此指針不能解引用——畢竟它不指向任何元素。
釋放動態數組
為了釋放動態數組,可以使用一種特殊的delete——在職陣前加上一個方括號對。
第二條語句銷毀pa指向的數組中的元素,并釋放對應的內存。數組中的元素按逆序銷毀,即,最后一個元素首先被銷毀,然后是倒數第二個,依此類推。
當我們釋放一個指向數組的指針時,空方括號對是必需的:它指示編譯器此指針指向一個對象數組的第一個元素。如果我們在delete一個指向數組的指針時忽略了方括號(或者在delete一個指向單一對象的指針時使用了方括號),其行為是未定義的。
動態數組和unique_ptr
- 當一個unique_ptr指向一個數組時,我們不能使用點和箭頭成員運算符。畢竟unique_ptr指向的是一個數組而不是單個對象,因此這些運算符是無意義的。
- 我們可以使用下標運算符來訪問數組中的元素
實例:
類型說明符中的方括號(<int[]>)指出up指向一個int數組而不是一個int。由于up指向一個數組,當up銷毀它管理的指針時,會自動使用delete[]。
動態數組和shared_ptr
shared_ptr不直接支持管理動態數組。如果希望用shared_ptr管理,必須提供自己的刪除器:
shared_ptr不直接支持動態數組管理這一特性會影響我們如何訪問數組中的元素:
shared_ptr未定義下標運算符,而且智能指針類型不支持指針算術運算。 因此,為了訪問數組中的元素,必須用get獲取一個內置指針,然后用它來訪問數組元素。
allocator類
new的局限性(使用allocator的原因)
- new將內存分配和對象構造組合在了一起。
- delete將對象析構和內存釋放組合在了一起
上述特性在靈活性上是有一定局限性的。這樣在分配單個對象時當然是好的,可以明確知道對象應該有什么值。但是分配大塊內存時,我們希望將內存分配和對象構造分離。這意味著我們可以先分配大塊內存,只有在真正需要時才執行對性的創建操作。
實例:
有如下問題:
- 我們可能不需要n個string,可能只用到了少量的string。因此我們可能創建了一些永遠也用不到的對象。
- 對于確實需要使用的對象,每個都被賦值了兩次:第一次是在默認初始化時,第二次是在賦值時。
- 沒有默認構造函數的類就不能動態分配數組了。
概念即創建銷毀操作
- allocator定義在頭文件memory中。
- allocator分配的內存是未構造的。還未構造對象的情況下就是用原始內存是錯誤的。
construct
構造對象是通過construct完成的:
- construct成員函數接受一個指針和零個或多個額外參數, 在給定位置構造一個元素。
- 額外參數用來初始化構造的對象。類似make_shared的參數,這些額外參數必須是與構造的對象的類型相匹配的合法的初始化器:
allocator<string> alloc;
auto const p = alloc.allocate(20);
auto q = p;
// q指向最后構造的元素之后的位置
// p指向分配的內存的首地址
alloc.construct(q++, 5, 'x'); // *p為xxxxx
cout << *p << endl; // 正確:使用string的輸出運算符
cout << *q << endl; // 災難:q指向未構造的內存
為了理解上面的代碼,用下面的代碼查看一下構造對象前后p和q分別指向的地址:
可以看到,p一直指向分配的內存的首地址,q指向最后構造的元素之后的地址,因此 *p 可以訪問已經構造的對象;而 *p 訪問的是未構造對象的原始內存,這種行為是錯誤的。
destroy
用完對象后,必須對每個構造的元素調用destory來銷毀它們。
destory接受一個指針,對指向的對象執行析構函數:
while (q != p) {alloc.destroy(--q); // 釋放我們真正構造的string
}
不妨來查看一下執行上述代碼之后的地址指向情況即內存分配的對象的值:
可以看到,執行完while之后,q指向的地址已經和p一樣了,而再訪問p中的對象——執行*p也無法輸出”xxxxx“了。
但是在atom里面嘗試運行的時候發現和預期的不一樣。。。。destroy之后解引用p仍然能得到”xxxxx“:
(吐槽:同一段代碼在不同的編譯器上得到的結果不同,猜測可能是底層的編譯環境不同導致的。 emmmmm……還是更傾向于相信vs的運行結果,如果有大佬看到這個問題知道原因的話,請不吝賜教,孩子實在不知道為什么會這樣。)
- 我們只能對真正構造了的元素進行destory操作。
- 一旦元素被銷毀后,可以重新使用這部分內存來保存其他的string,也可將內存歸還給系統。
deallocate
釋放內存通過deallocate來完成:
alloc.deallocate(p, 20);
- 傳遞給deallocate的指針不能為空,必須指向由allocate分配的內存。
- 傳遞給deallocate的大小參數必須與調用allocate分配內存時提供的大小參數具有一樣的值。
兩個伴隨算法
- 用來初始化內存中創建的對象
實例:
- uninitialized_copy返回遞增后的目的位置迭代器,指向最后一個構造元素之后的位置。
使用了動態生存期的資源的類
大多數類中,分配的資源都與對應對象生存期一致。 例如:每個vector(對象)“擁有”其自己的元素(分配的資源)。當我們拷貝一個vector時,原vector和副本vector中的元素是相互分離的。
某些類分配的資源具有與原對象相獨立的生存期(可能一個資源被兩個對象共同引用)。 換言之,如果兩個對象共享底層的數據,當某個對象被銷毀時,我們不能單方面地銷毀底層數據。
實例
構建一個類A,用share_ptr
管理vector<string>
:
#pragma once
#include <vector>
#include <string>
#include <memory>using namespace std;class A{
public:A(): vs(make_shared<vector<string>>()){} // 分配一個空的vectorA(initializer_list<string> il): vs(make_shared<vector<string>>(il)){}// 接受一個初始化器的花括號列表,將il當作make_shared的參數初始化vs// 通過調用底層vector的成員函數來完成size、empty、push_back、pop_backvector<string>::size_type size() const{return vs->size();}bool empty() const{return vs->empty();}void push_back(const string& s){vs->push_back(s);}// pop_back、front、back操作需要先檢查操作對象是否為空void pop_back(){check(0, "pop_back on empty A");vs->pop_back();}string& front(){check(0, "front on empty A");return vs->front();}string& back(){check(0, "back on empty A");return vs->back();}// 針對const的A對象的front和back函數重載const string& front() const;const string& back() const;
private:shared_ptr<vector<string>> vs;// check函數提供判空功能,如果操作對象為空拋出一個異常void check(vector<string>::size_type si, const string &s) const{if(si >= vs->size()){throw out_of_range(s);}}
};const string& A::front() const{check(0, "front on empty A");return vs->front();
}
const string& A::back() const{check(0, "back on empty A");return vs->back();
}
用一個簡單的A的使用程序。測試類的正確性:
#include <iostream>using namespace std;#include "my_A.h"int main(int argc, char const *argv[]) {A a1;{A a2 = {"a", "an", "the"};a1 = a2;a2.push_back("about");cout << a2.size() << endl;}cout << a1.size() << endl;cout << a1.front() << " " << a1.back() << endl;const A a3 = a1;cout << a3.front() << " " << a3.back() << endl;return 0;
}
輸出結果: