make_shared的使用:
shared_ptr<string> p1 = make_shared<string>(10, '9'); shared_ptr<string> p2 = make_shared<string>("hello"); shared_ptr<string> p3 = make_shared<string>();
盡量使用make_shared初始化
C++11 中引入了智能指針, 同時還有一個模板函數 std::make_shared 可以返回一個指定類型的 std::shared_ptr, 那與 std::shared_ptr 的構造函數相比它能給我們帶來什么好處呢 ?
make_shared初始化的優點
1、提高性能
shared_ptr 需要維護引用計數的信息:
強引用, 用來記錄當前有多少個存活的 shared_ptrs 正持有該對象. 共享的對象會在最后一個強引用離開的時候銷毀( 也可能釋放).
弱引用, 用來記錄當前有多少個正在觀察該對象的 weak_ptrs. 當最后一個弱引用離開的時候, 共享的內部信息控制塊會被銷毀和釋放 (共享的對象也會被釋放, 如果還沒有釋放的話).
如果你通過使用原始的 new 表達式分配對象, 然后傳遞給 shared_ptr (也就是使用 shared_ptr 的構造函數) 的話, shared_ptr 的實現沒有辦法選擇, 而只能單獨的分配控制塊:

如果選擇使用 make_shared 的話, 情況就會變成下面這樣:

std::make_shared(比起直接使用new)的一個特性是能提升效率。使用std::make_shared允許編譯器產生更小,更快的代碼,產生的代碼使用更簡潔的數據結構。考慮下面直接使用new的代碼:
std::shared_ptr<Widget> spw(new Widget);
很明顯這段代碼需要分配內存,但是它實際上要分配兩次。每個std::shared_ptr都指向一個控制塊,控制塊包含被指向對象的引用計數以及其他東西。這個控制塊的內存是在std::shared_ptr的構造函數中分配的。因此直接使用new,需要一塊內存分配給Widget,還要一塊內存分配給控制塊。
如果使用std::make_shared來替換
auto spw = std::make_shared<Widget>();
一次分配就足夠了。這是因為std::make_shared申請一個單獨的內存塊來同時存放Widget對象和控制塊。這個優化減少了程序的靜態大小,因為代碼只包含一次內存分配的調用,並且這會加快代碼的執行速度,因為內存只分配了一次。另外,使用std::make_shared消除了一些控制塊需要記錄的信息,這樣潛在地減少了程序的總內存占用。
對std::make_shared的效率分析可以同樣地應用在std::allocate_shared上,所以std::make_shared的性能優點也可以擴展到這個函數上。
2、 異常安全
我們在調用processWidget的時候使用computePriority(),並且用new而不是std::make_shared:
processWidget(std::shared_ptr<Widget>(new Widget), //潛在的資源泄露 computePriority());
就像注釋指示的那樣,上面的代碼會導致new創造出來的Widget發生泄露。那么到底是怎么泄露的呢?調用代碼和被調用函數都用到了std::shared_ptr,並且std::shared_ptr就是被設計來阻止資源泄露的。當最后一個指向這兒的std::shared_ptr消失時,它們會自動銷毀它們指向的資源。如果每個人在每個地方都使用std::shared_ptr,那么這段代碼是怎么導致資源泄露的呢?
答案和編譯器的翻譯有關,編譯器把源代碼翻譯到目標代碼,在運行期,函數的參數必須在函數被調用前被估值,所以在調用processWidget時,下面的事情肯定發生在processWidget能開始執行之前:
表達式“new Widget”必須被估值,也就是,一個Widget必須被創建在堆上。
std::shared_ptr(負責管理由new創建的指針)的構造函數必須被執行。
computePriority必須跑完。
編譯器不需要必須產生這樣順序的代碼。但“new Widget”必須在std::shared_ptr的構造函數被調用前執行,因為new的結構被用為構造函數的參數,但是computePriority可能在這兩個調用前(后,或很奇怪地,中間)被執行。也就是,編譯器可能產生出這樣順序的代碼:
執行“new Widget”。 執行computePriority。 執行std::shared_ptr的構造函數。
如果這樣的代碼被產生出來,並且在運行期,computePriority產生了一個異常,則在第一步動態分配的Widget就會泄露了,因為它永遠不會被存放到在第三步才開始管理它的std::shared_ptr中。
使用std::make_shared可以避免這樣的問題。調用代碼將看起來像這樣:
processWidget(std::make_shared<Widget>(), //沒有資源泄露 computePriority());
在運行期,不管std::make_shared或computePriority哪一個先被調用。如果std::make_shared先被調用,則在computePriority調用前,指向動態分配出來的Widget的原始指針能安全地被存放到被返回的std::shared_ptr中。如果computePriority之后產生一個異常,std::shared_ptr的析構函數將發現它持有的Widget需要被銷毀。並且如果computePriority先被調用並產生一個異常,std::make_shared就不會被調用,因此這里就不需要考慮動態分配的Widget了。
如果使用std::unique_ptr和std::make_unique來替換std::shared_ptr和std::make_shared,事實上,會用到同樣的理由。因此,使用std::make_unique代替new就和“使用std::make_shared來寫出異常安全的代碼”一樣重要。
缺點
構造函數是保護或私有時,無法使用 make_shared
make_shared
雖好, 但也存在一些問題, 比如, 當我想要創建的對象沒有公有的構造函數時, make_shared
就無法使用了, 當然我們可以使用一些小技巧來解決這個問題, 比如這里 How do I call ::std::make_shared on a class with only protected or private constructors?
對象的內存可能無法及時回收
make_shared
只分配一次內存, 這看起來很好. 減少了內存分配的開銷. 問題來了, weak_ptr
會保持控制塊(強引用, 以及弱引用的信息)的生命周期, 而因此連帶着保持了對象分配的內存, 只有最后一個 weak_ptr
離開作用域時, 內存才會被釋放. 原本強引用減為 0 時就可以釋放的內存, 現在變為了強引用, 若引用都減為 0 時才能釋放, 意外的延遲了內存釋放的時間. 這對於內存要求高的場景來說, 是一個需要注意的問題.
作者:宋大壯
鏈接:https://www.jianshu.com/p/03eea8262c11
來源:簡書