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