C++序列容器儲存智慧指標詳解

2020-07-16 10:04:31
通常用容器儲存指標比儲存物件更好,而且大多數時候,儲存智慧指標比原生指標好。下面是一些原因:
  • 在容器中儲存指標需要複製指標而不是它所指向的物件。複製指標通常比複製物件快。
  • 在容器中儲存指標可以得到多型性。存放元素基礎類別指標的容器也可以儲存其派生型別的指標。當要處理有共同基礎類別的任意物件序列時,這種功能是非常有用的。應用這一特性的一個常見範例是展示一個含有直線、曲線和幾何形狀的物件序列。
  • 對指標容器的內容進行排序的速度要比對物件排序快;因為只需要移動指標,不需要移動物件。
  • 儲存智慧指標要比儲存原生指標安全,因為在物件不再被參照時,自由儲存區的物件會被自動刪除。這樣就不會產生記憶體漏失。不指向任何物件的指標預設為 nullptr。

如你所知,主要有兩種型別的智慧指標:unique_ptr<T>shared_ptr<T>,其中 unique_ptr<T> 獨佔它所指向物件的所有權,而 shared_ptr<T> 允許多個指標指向同一個物件。還有weak_ptr<T> 型別,它是一類從 shared_ptr<T> 生成的智慧指標,可以避免使用 shared_ptrs<T> 帶來的迴圈參照問題。unique_ptr<T> 型別的指標可以通過移動的方式儲存到容器中。例如,下面的程式碼可以通過編譯:
std::vector<std::unique_ptr<std::string>> words;
words.push_back(std::make_unique<std::string>("one"));
words.push_back(std::make_unique<std::string>("two"));
vector 儲存了 unique_ptr<string> 型別的智慧指標。make_unique<T>() 函數可以生成物件和智慧指標,並且返回後者。因為返回結果是一個臨時 unique_ptr<string> 物件,這裡呼叫一個有右值參照引數的 push_back() 函數,因此不需要拷貝物件。另一種新增 unique_ptr 物件的方法是,先建立一個區域性變數 unique_ptr ,然後使用 std::move() 將它移到容器中。然而,後面任何關於拷貝容器元素的操作都會失敗,因為只能有一個 unique_ptr 物件。如果想能夠複製元素,需要使用 shared_ptr 物件;否則就使用 unique_ptr 物件。

在序列容器中儲存指標

下面首先解釋一些在容器中使用原生指標會碰到的問題,然後再使用智慧指標(這是推薦的使用方式)。下面是一段程式碼,用來從標準輸入流讀取單詞,然後將指向自由儲存區的字串物件的指標儲存到 vector 容器中:
std::vector<std::string*> words;
std::string word;
std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end: n";
while (true)
{
    if ((std::cin >> word).eof())
    {
        std::cin. clear();
        break;
    }

    words.push_back(new std::string {word});// Create object and store its address
}
push_back() 的參數列達式在自由儲存區生成了一個字串物件,因此 push_back() 的引數是一個物件的地址。可以按如下方式輸出 words 中的內容:
for (auto& w : words)
    std: : cout << w <<" ";
std::cout << std::endl;
如果想使用疊代器來存取容器中的元素,輸出字串的程式碼可以這樣寫:
for (auto iter = std::begin(words);iter != std::end(words); ++iter)
    std::cout << **iter <<" ";
std::cout << std::endl;
iter 是一個疊代器,必須通過解除參照來存取它所指向的元素。這裡,容器的元素也是指標,因此必須解除參照來獲取 string 物件。因此表示式為:**iter。注意,在刪除元素時,需要先釋放它所指向的記憶體。如果不這樣做,在刪除指標後,就無法釋放它所指向的記憶體,除非儲存了指標的副本。這是容器中的原生指標常見的記憶體漏失來源。下面演示它如何在 words 中發生:
for (auto iter = std::begin(words);iter != std::end(words);)
{
    if (**iter == "one")
        words.erase (iter); // Memory leak!
    else
        ++iter;
}
這裡刪除了一個指標,但它所指向的記憶體仍然存在。無論什麼時候刪除一個是原生指標的元素,都需要首先釋放它所指向的記憶體:
for (auto iter = std::begin(words); iter != std::end(words);)
{
    if (**iter == "one")
    {
        delete *iter;//Release the memory...
        words.erase (iter);    //... then delete the pointer
    }
    else
        ++iter;
}
在離開 vector 的使用範圍之前,記住要刪除自由儲存區的 string 物件。可以按如下方式來實現:
for (auto& w : words)
    delete w; // Delete the string pointed to
words.clear(); // Delete all the elements from the vector
用索引來存取指標,這樣就可以使用 delete 運算子刪除 string 物件。當迴圈結束時,vector 中的所有指標元素都會失效,因此不要讓 vector 處於這種狀態。呼叫 dear() 移除所有元素,這樣 size() 會返回 0。當然,也可以像下面這樣使用疊代器:
for (auto iter = std::begin(words);iter != std::end(words); ++iter)
    delete *iter;
如果儲存了智慧指標,就不用擔心要去釋放自由儲存區的記憶體。智慧指標會做這些事情。下面是一個讀入字串,然後把 shared_ptr<string> 儲存到 vector 中的程式碼片段:
std::vector<std::shared_ptr<std::string>> words; std::string word;
std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:n";
while (true)
{
    if ((std::cin >> word).eof())
    {
        std::cin. clear ();
        break;
    }
    words.push_back(std::make_shared<string>(word)); // Create smart pointer to string & store it
}
這和使用原生指標的版本沒有什麼不同。vector 模板現在的型別引數是 std::shared_ptr<std::string>,push_back() 的引數會呼叫 make_shared(),在自由儲存區生成 string 物件和一個指向它的智慧指標。因為智慧指標由參數列達式生成,這裡會呼叫一個右值參照引數版的 push_back() 來將指標移到容器中。

模板型別引數可能有些冗長,但是可以使用 using 來簡化程式碼。例如:
using PString = std::shared_ptr<std::string>;
使用 using 後,可以這樣定義:
std::vector<PString> words;
可以通過智慧指標元素來存取字串,這和使用原生指標相同。前面那些輸出 words 內容的程式碼片段都可以使用智慧指標。當然,不需要刪除自由儲存區的 string 物件;因為智慧指標會做這些事情。執行 words.clear() 會移除全部的元素,因此會呼叫智慧指標的解構函式;這也會導致智慧指標釋放它們所指向物件的記憶體。

為了阻止 vector 太頻繁地分配額外記憶體,可以先建立 vector,然後呼叫 reserve() 來分配一定數量的初始記憶體。例如:
std::vector<std::shared_ptr<std::>>words;
words.reserve(100); // Space for 100 smart pointers
這樣生成 vector 比指定元素個數來生成要好,因為每一個元素都是通過呼叫 shared_ptr<string> 建構函式生成的。不這樣做也不是什麼大問題,但會產生一些不必要的額外開銷,即使開銷很小。通常,每個智慧指標所需要的空間遠小於它們所指向物件需要的空間,因此可以大方地使用 reserve() 來分配空間。

可以在外面使用儲存的 shared_ptr<T> 物件的副本。如果不需要這種功能,應該使用 unique_ptr<T> 物件。下面展示如何在 words 中這樣使用:
std::vector<std::unique_ptr<std::string>>words;
std::string word;
std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:n";
while (true)
{
    if ((std::cin >> word).eof())
    {
        std::cin.clear();
        break;
    }
    words.push_back(std::make_unique<string>(word));
    //Create smart pointer to string & store it
}
在上面的程式碼中,用 unique 代替 shared 是沒有差別的。

我們看一下,如何使用智慧指標來實現前面章節中的超市結賬模擬程式。 Customer 類的定義和之前的版本相同,但是 Checkout 類的定義中使用了智慧指標,因而產生了一些變化,我們也可以在 main() 中使用智慧指標。在整個程式中,我們都不需要使用智慧指標的副本,因此我們選擇使用 unique_ptr<T>。下面是 Checkout.h 標頭檔案中的新內容:
// Supermarket checkout - using smart pointers to customers in a queue
#ifndef CHECKOUT_H
#define CHECKOUT_H
#include <queue> // For queue container
#include <memory> // For smart pointers
#include "Customer.h"
using PCustomer = std::unique_ptr<Customer>;

class Checkout
{
private:
    std::queue<PCustomer> customers;                // The queue waiting to checkout

public:
    void add(PCustomer&& customer) { customers.push(std::move(customer)); }
    size_t qlength() const { return customers.size(); }

    // Increment the time by one minute
    void time_increment()
    {
        if (customers.front()->time_decrement().done())  // If the customer is done...
        customers.pop();                               // ...remove from the queue
    };

    bool operator<(const Checkout& other) const { return qlength() < other.qlength(); }
    bool operator>(const Checkout& other) const { return qlength() < other.qlength(); }
};
#endif
我們需要直接包含 memory 標頭檔案,這樣就可以使用智慧指標型別的模板。queue 容器儲存 PCustomer 元素,用來記錄排隊結賬的顧客。使用 using 為 std::unique_ptr<Customer> 定義了一個別名 PCustomer,這可以節省大量的輸入。PCustomer 物件不能被複製,因而當呼叫 add() 函數時,它的引數是右值參照,引數會被移到容器中。以 unique 指標作為元素時,也會以同樣的方式被移到容器中;當然,引數不能是 const。做了這些修改後,就可以使用 unique_ptr 了,不再需要修改其他的內容。
// Using smart pointer to simulate supermarket checkouts
#include <iostream>                              // For standard streams
#include <iomanip>                               // For stream manipulators
#include <vector>                                // For vector container
#include <string>                                // For string class
#include <numeric>                               // For accumulate()
#include <algorithm>                             // For min_element & max_element
#include <random>                                // For random number generation
#include <memory>                                // For smart pointers
#include "Checkout.h"
#include "Customer.h"

using std::string;
using distribution = std::uniform_int_distribution<>;
using PCheckout = std::unique_ptr<Checkout>;

// Output histogram of service times
void histogram(const std::vector<int>& v, int min)
{
    string bar (60, '*');                          // Row of asterisks for bar
    for (size_t i {}; i < v.size(); ++i)
    {
        std::cout << std::setw(3) << i+min << " "    // Service time is index + min
        << std::setw(4) << v[i] << " "             // Output no. of occurrences
        << bar.substr(0, v[i])                     // ...and that no. of asterisks
        << (v[i] > static_cast<int>(bar.size()) ? "...": "")
        << std::endl;
    }
}

int main()
{
    std::random_device random_n;

    // Setup minimum & maximum checkout periods - times in minutes
    int service_t_min {2}, service_t_max {15};
    std::uniform_int_distribution<> service_t_d {service_t_min, service_t_max};

    // Setup minimum & maximum number of customers at store opening
    int min_customers {15}, max_customers {20};
    distribution n_1st_customers_d {min_customers, max_customers};

    // Setup minimum & maximum intervals between customer arrivals
    int min_arr_interval {1}, max_arr_interval {5};
    distribution arrival_interval_d {min_arr_interval, max_arr_interval};

    size_t n_checkouts {};
    std::cout << "Enter the number of checkouts in the supermarket: ";
    std::cin >> n_checkouts;
    if(!n_checkouts)
    {
        std::cout << "Number of checkouts must be greater than 0. Setting to 1." << std::endl;
        n_checkouts = 1;
    }

    std::vector<PCheckout> checkouts;
    checkouts.reserve(n_checkouts);                // Reserve memory for pointers

    // Create the checkouts
    for (size_t i {}; i < n_checkouts; ++i)
        checkouts.push_back(std::make_unique<Checkout>());
    std::vector<int> service_times(service_t_max-service_t_min+1);

    // Add customers waiting when store opens
    int count {n_1st_customers_d(random_n)};
    std::cout << "Customers waiting at store opening: " << count << std::endl;
    int added {};
    int service_t {};

    // Define comparison lambda for pointers to checkouts
    auto comp = [](const PCheckout& pc1, const PCheckout& pc2){ return *pc1 < *pc2; };
    while (added++ < count)
    {
        service_t = service_t_d(random_n);
        auto iter = std::min_element(std::begin(checkouts), std::end(checkouts), comp);
        (*iter)->add(std::make_unique<Customer>(service_t));
        ++service_times[service_t - service_t_min];
    }

    size_t time {};                                // Stores time elapsed
    const size_t total_time {600};                 // Duration of simulation - minutes
    size_t longest_q {};                           // Stores longest checkout queue length

    // Period until next customer arrives
    int new_cust_interval {arrival_interval_d(random_n)};

    // Run store simulation for period of total_time minutes
    while (time < total_time)                      // Simulation loops over time
    {
        ++time;                                      // Increment by 1 minute

        // New customer arrives when arrival interval is zero
        if (--new_cust_interval == 0)
        {
            service_t = service_t_d(random_n);         // Random customer service time
            (*std::min_element(std::begin(checkouts), std::end(checkouts), comp))->add(std::make_unique<Customer>(service_t));
            ++service_times[service_t - service_t_min];  // Record service time
   
            // Update record of the longest queue length
            for (auto& pcheckout : checkouts)
                longest_q = std::max(longest_q, pcheckout->qlength());

            new_cust_interval = arrival_interval_d(random_n);
        }

        // Update the time in the checkouts - serving the 1st customer in each queue
        for (auto& pcheckout : checkouts)
            pcheckout->time_increment();
    }

    std::cout << "Maximum queue length = " << longest_q << std::endl;
    std::cout << "nHistogram of service times:n";
    histogram(service_times, service_t_min);

    std::cout << "nTotal number of customers today: "
                << std::accumulate(std::begin(service_times), std::end(service_times), 0)
                << std::endl;
}
vector 容器現在儲存的是指向 Checkout 物件的 unique 指標。vector 的疊代器指向 Checkout 物件,即 unique_ptr<Checkout> 物件的指標,因而可以通過疊代器來呼叫 Checkout 的成員函數。首先必須解除參照疊代器,然後用間接成員選擇運算子來呼叫函數。可以看到,我們已經修改了 main() 中的相關程式碼。min_element() 預設使用 < 運算子來從疊代器指向的元素中獲取結果。預設會比較智慧指標,但是並不能得到正確的結果。我們需要為 min_element() 提供第 3 個引數作為它所使用的比較函數。這個函數是由名為 comp 的 lambda 表示式定義的。因為我們想在後面繼續使用這個表示式,所以對它做命名。

為了存取 Checkout 物件,這個 lambda 表示式解除參照了智慧指標引數,然後使用 Checkout 類的成員函數 operator<() 來比較它們。所有的 Checkout 和 Customer 物件都是在自由儲存區生成的。智慧指標會維護它們所使用的記憶體。這個版本的模擬程式的輸出和之前版本的相同。在這個範例中也可以使用 shared_ptr<T>,但是它們會執行得慢一些。就執行時間和記憶體使用而言, unique_ptr<T> 物件相對於原生指標的開銷最小。