簡體   English   中英

RAII和C ++中的智能指針

[英]RAII and smart pointers in C++

在使用C ++的實踐中,什么是RAII ,什么是智能指針 ,如何在程序中實現這些以及將RAII與智能指針一起使用有什么好處?

RAII的一個簡單(也許是過度使用)的例子是File類。 沒有RAII,代碼可能看起來像這樣:

File file("/path/to/file");
// Do stuff with file
file.close();

換句話說,我們必須確保在完成文件后關閉文件。 這有兩個缺點 - 首先,無論我們在哪里使用File,我們都必須調用File :: close() - 如果我們忘記這樣做,我們將保留文件的時間比我們需要的長。 第二個問題是如果在關閉文件之前拋出異常會怎樣?

Java使用finally子句解決了第二個問題:

try {
    File file = new File("/path/to/file");
    // Do stuff with file
} finally {
    file.close();
}

或者從Java 7開始,嘗試使用資源語句:

try (File file = new File("/path/to/file")) {
   // Do stuff with file
}

C ++使用RAII解決了這兩個問題 - 也就是說,在File的析構函數中關閉文件。 只要File對象在正確的時間被銷毀(無論如何都應該被銷毀),關閉文件將由我們處理。 所以,我們的代碼現在看起來像:

File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us

這不能在Java中完成,因為無法保證何時銷毀對象,因此我們無法保證何時釋放諸如文件之類的資源。

在智能指針上 - 很多時候,我們只是在堆棧上創建對象。 例如(並從另一個答案竊取一個例子):

void foo() {
    std::string str;
    // Do cool things to or using str
}

這很好 - 但是如果我們想要返回str呢? 我們可以這樣寫:

std::string foo() {
    std::string str;
    // Do cool things to or using str
    return str;
}

那么,那有什么不對? 好吧,返回類型是std :: string - 所以這意味着我們按值返回。 這意味着我們復制str並實際返回副本。 這可能很昂貴,我們可能希望避免復制它的成本。 因此,我們可能想出通過引用或指針返回的想法。

std::string* foo() {
    std::string str;
    // Do cool things to or using str
    return &str;
}

不幸的是,這段代碼不起作用。 我們正在返回一個指向str的指針 - 但str是在堆棧上創建的,所以一旦我們退出foo()就會被刪除。 換句話說,當調用者獲得指針時,它是無用的(並且可能比無用更糟,因為使用它可能會導致各種各樣的時髦錯誤)

那么,解決方案是什么? 我們可以使用new在堆上創建str - 這樣,當foo()完成時,str不會被銷毀。

std::string* foo() {
    std::string* str = new std::string();
    // Do cool things to or using str
    return str;
}

當然,這種解決方案也不完美。 原因是我們創建了str,但我們從不刪除它。 這可能不是一個非常小的程序中的問題,但一般來說,我們希望確保刪除它。 我們可以說調用者必須在完成后刪除該對象。 缺點是調用者必須管理內存,這會增加額外的復雜性,並且可能會出錯,導致內存泄漏,即不再刪除對象,即使不再需要它。

這是智能指針的用武之地。以下示例使用shared_ptr - 我建議您查看不同類型的智能指針,以了解您實際想要使用的內容。

shared_ptr<std::string> foo() {
    shared_ptr<std::string> str = new std::string();
    // Do cool things to or using str
    return str;
}

現在,shared_ptr將計算str的引用數。 例如

shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;

現在有兩個對同一個字符串的引用。 一旦沒有對str的剩余引用,它將被刪除。 因此,您不必再擔心自己刪除它。

快速編輯:正如一些評論所指出的那樣,這個例子並不完美(至少!)兩個原因。 首先,由於字符串的實現,復制字符串往往是便宜的。 其次,由於所謂的命名返回值優化,按值返回可能並不昂貴,因為編譯器可以做一些聰明來加快速度。

所以,讓我們嘗試使用File類的不同示例。

假設我們想要將文件用作日志。 這意味着我們想要以僅附加模式打開我們的文件:

File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log

現在,讓我們將文件設置為其他幾個對象的日志:

void setLog(const Foo & foo, const Bar & bar) {
    File file("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

不幸的是,這個例子結尾可怕 - 文件將在此方法結束時立即關閉,這意味着foo和bar現在具有無效的日志文件。 我們可以在堆上構造文件,並將指向文件的指針傳遞給foo和bar:

void setLog(const Foo & foo, const Bar & bar) {
    File* file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

但是誰負責刪除文件? 如果既不刪除文件,那么我們既有內存又有資源泄漏。 我們不知道foo或bar是否會先完成文件,所以我們不能指望自己刪除文件。 例如,如果foo在bar完成之前刪除了該文件,則bar現在具有無效指針。

所以,正如您可能已經猜到的那樣,我們可以使用智能指針來幫助我們。

void setLog(const Foo & foo, const Bar & bar) {
    shared_ptr<File> file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

現在,沒有人需要擔心刪除文件 - 一旦foo和bar都完成並且不再有任何文件引用(可能是由於foo和bar被銷毀),文件將自動被刪除。

RAII這是一個簡單但令人敬畏的概念的奇怪名稱。 更好的是Scope Bound Resource Management (SBRM)。 我們的想法是,您經常在塊的開頭分配資源,並且需要在塊的出口處釋放它。 退出塊可以通過正常的流量控制,跳出它,甚至是異常來實現。 為了涵蓋所有這些情況,代碼變得更加復雜和冗余。

只是一個沒有SBRM的例子:

void o_really() {
     resource * r = allocate_resource();
     try {
         // something, which could throw. ...
     } catch(...) {
         deallocate_resource(r);
         throw;
     }
     if(...) { return; } // oops, forgot to deallocate
     deallocate_resource(r);
}

如你所見,我們可以通過很多方式進行實踐。 我們的想法是將資源管理封裝到一個類中。 其對象的初始化獲取資源(“資源獲取是初始化”)。 在我們退出塊(塊范圍)時,資源再次被釋放。

struct resource_holder {
    resource_holder() {
        r = allocate_resource();
    }
    ~resource_holder() {
        deallocate_resource(r);
    }
    resource * r;
};

void o_really() {
     resource_holder r;
     // something, which could throw. ...
     if(...) { return; }
}

如果你有自己的類,這不僅僅是為了分配/解除分配資源的目的,這是很好的。 分配只是他們完成工作的另一個問題。 但是,只要您想分配/解除分配資源,上述內容就變得不合適了。 您必須為您獲得的每種資源編寫一個包裝類。 為了簡化這一點,智能指針允許您自動執行該過程:

shared_ptr<Entry> create_entry(Parameters p) {
    shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
    return e;
}

通常,智能指針是new / delete周圍的瘦包裝器,當它們擁有的資源超出范圍時恰好會調用delete 一些智能指針,比如shared_ptr,允許你告訴他們一個所謂的刪除器,它被用來代替delete 例如,只要告訴shared_ptr關於正確的刪除器,就可以管理窗口句柄,正則表達式資源和其他任意內容。

有不同的智能指針用於不同的目的:

的unique_ptr

是一個智能指針,專門擁有一個對象。 它不是在提升,但它可能會出現在下一個C ++標准中。 它是不可復制的,但支持所有權轉讓 一些示例代碼(下一個C ++):

碼:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

與auto_ptr不同,unique_ptr可以放入容器中,因為容器將能夠保存不可復制(但可移動)的類型,例如streams和unique_ptr。

使用scoped_ptr

是一個提升智能指針,既不可復制也不可移動。 當你想要確保指針在超出范圍時被刪除時,這是一個完美的選擇。

碼:

 void do_something() { scoped_ptr<pipe> sp(new pipe); // do something here... } // when going out of scope, sp will delete the pointer automatically. 

shared_ptr的

是為了共享所有權。 因此,它既可復制又可移動。 多個智能指針實例可以擁有相同的資源。 一旦擁有資源的最后一個智能指針超出范圍,資源就會被釋放。 我的一個項目的一些現實世界的例子:

碼:

 shared_ptr<plot_src> p(new plot_src(&fx)); plot1->add(p)->setColor("#00FF00"); plot2->add(p)->setColor("#FF0000"); // if p now goes out of scope, the src won't be freed, as both plot1 and // plot2 both still have references. 

如您所見,plot-source(函數fx)是共享的,但每個都有一個單獨的條目,我們在其上設置顏色。 有一個weak_ptr類,當代碼需要引用智能指針所擁有的資源時使用,但不需要擁有該資源。 您應該創建一個weak_ptr,而不是傳遞原始指針。 當它注意到你試圖通過weak_ptr訪問路徑訪問資源時會拋出異常,即使沒有shared_ptr擁有該資源。

在概念上,前提和原因很簡單。

RAII是一種設計范例,用於確保變量在其構造函數中處理所有需要的初始化,並在其析構函數中處理所有需要的清理。 這將所有初始化和清理減少到一個步驟。

C ++不需要RAII,但越來越多的人認為使用RAII方法會產生更強大的代碼。

RAII在C ++中有用的原因是C ++在進入和離開作用域時,無論是通過正常的代碼流還是通過異常觸發的堆棧展開,本質上都會管理變量的創建和銷毀。 這是C ++中的免費贈品。

通過將所有初始化和清理與這些機制聯系起來,您可以確保C ++也將為您處理這項工作。

在C ++中談論RAII通常會導致對智能指針的討論,因為指針在清理時特別脆弱。 當管理從malloc或new獲取的堆分配的內存時,程序員通常負責在銷毀指針之前釋放或刪除該內存。 智能指針將使用RAII原理確保在銷毀指針變量時銷毀堆分配的對象。

智能指針是RAII的變體。 RAII意味着資源獲取是初始化。 智能指針在使用之前獲取資源(內存),然后在析構函數中自動將其拋出。 發生了兩件事:

  1. 我們在使用它之前總是分配內存 ,即使我們不喜歡它 - 用智能指針做另一種方式很難。 如果沒有發生這種情況,您將嘗試訪問NULL內存,從而導致崩潰(非常痛苦)。
  2. 即使出現錯誤,我們也會釋放內存 沒有記憶懸空。

例如,另一個例子是網絡套接字RAII。 在這種情況下:

  1. 我們在使用之前打開網絡套接字 ,總是,即使我們不喜歡這樣 - 用RAII做另一種方式很難。 如果您嘗試在沒有RAII的情況下執行此操作,則可能會打開空插槽,例如MSN連接。 那么像今晚“讓我們這樣做”的消息可能不會被轉移,用戶也不會被放下,你可能會被解雇。
  2. 即使出現錯誤,我們也會關閉網絡套接字 沒有任何套接字被掛起,因為這可能會阻止響應消息“肯定會在底部”發送回來。

現在,正如您所看到的,RAII在大多數情況下是一個非常有用的工具,因為它可以幫助人們鋪設。

智能指針的C ++來源在網絡上有數百萬,包括我之上的響應。

Boost有許多這些,包括Boost.Interprocess中的共享內存。 它極大地簡化了內存管理,特別是在令人頭疼的情況下,例如當你有5個進程共享相同的數據結構時:當每個人都完成一大塊內存時,你希望它自動獲得釋放而不必坐在那里試圖找出誰應該負責在一塊內存上調用delete ,以免最終導致內存泄漏,或者指針被錯誤地釋放兩次並可能破壞整個堆。

void foo()
{
   std::string bar;
   //
   // more code here
   //
}

無論發生什么,一旦foo()函數的范圍被遺忘,bar將被正確刪除。

內部std :: string實現經常使用引用計數指針。 因此,只有在其中一個字符串副本發生更改時才需要復制內部字符串。 因此,引用計數智能指針使得可以在必要時僅復制某些內容。

此外,內部引用計數使得在不再需要內部字符串的副本時可以正確刪除內存。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM