本节我们将介绍 C++ STL 中智能指针的使用。
智能指针(英语:Smart pointer)是一种抽象的数据类型。在程序设计中,它通常是经由类模板 (C++) 来实现,借由模板来达成泛型,借由类模板的析构函数来达成自动释放指针所指向的存储器或对象。
内容来自维基百科。
在构造时,它分配内存,当离开作用域时,它会自动释放已分配的内存。这样,我们就不需要手动管理动态内存了,这无疑使繁杂的内存管理变得简单起来了。
注:本节内容主要参考了 C 语言中文网 的内容。
在绝大多数时候,容器中保存指针类型会比保存对象更好,在智能指针比原生指针更加优越的情况,使用容器保存智能指针具有的优势是十分明显的。
原因如下:
- 在容器中保存指针只需要复制指针而不是它指向的对象。因为复制指针通常比复制对象快。
- 对指针容器进行排序的速度要比对象排序快得多;因为只需要移动指针,而不需要移动对象。
- 在容器中保存指针有多态性。存放元素基类指针的容器也可以保存其派生类型的指针。当要处理有共同基类的任意对象序列时,这种功能是非常有用的。
- 保存智能指针要比保存原生指针更安全,因为在对象不再被引用时,自由存储区的对象会被自动删除。因而不会产生内存泄漏。不指向任何对象的指针默认为 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> 类型的指针可以通过移动的方式保存到容器中。
以 unique_ptr<string> 为例:
vector<unique_ptr<string>> data;
data.push_back(make_unique<string>("zeros"));
data.push_back(make_unique<string>("one"));
vector 内保存了 unique_ptr<string> 类型的智能指针。
make_unique<T>() 函数可以生成对象和智能指针,并且返回智能指针。因为返回结果是一个临时 unique_ptr<string> 对象,这里调用的 push_back() 函数不需要拷贝对象。
另一种添加 unique_ptr 对象的方法是,先创建一个局部变量 unique_ptr ,然后使用 move() 将它移到容器中。然而,之和任何关于拷贝容器元素的操作都会失败,因为只能有一个 unique_ptr 对象。如果想能够复制元素,需要使用 shared_ptr 对象;否则就使用 unique_ptr 对象。
在序列容器中保存指针
下面先解释一些在容器中使用原生指针会碰到的问题,之和再使用智能指针。
下面是读取 n 个字符串,将指向字符串的指针保存至 vector 中。
vector<string*> data;
int n;
cout<<"Please input n :"<<endl;
cin>>n;
while(n--){
string d;
cin>>d;
data.push_back(new string{d});
}
push_back() 的括号内生成了一个字符串对象,因此它接受的参数是一个对象的地址。
我们使用以下方式输出 data 的内容。
for(auto d : data){
cout<<*d<<" ";
}
cout<<endl;
也可以使用迭代器。
for(auto iter = begin(data);iter != end(data);iter++){
cout<<**iter<<" ";
}
cout<<endl;
因为 iter 是一个迭代器,所以必须通过解引用来访问它所指向的元素。
这里,容器的元素也是指针,要得到 string 对象,表达式为:**iter 。
注意,在删除元素时,需要先释放它所指向的内存;否则,在删除指针后,会无法释放它所指向的内存,除非你保存了指针的副本。
这是原生指针在容器中常见的内存泄漏来源。下面演示它如何在 data 中发生:
for(auto iter = begin(data);iter != end(data);){
if(**iter == "one"){
data.erase(iter);
}else{
++iter;
}
}
cout<<endl;
尽管我们删除了一个指针,但它所指向的内存仍然存在。
无论什么时候删除一个是原生指针的元素,都需要首先释放它所指向的内存:
for(auto iter = begin(data);iter != end(data);){
if(**iter == "one"){
delete *iter;
data.erase(iter);
}else{
++iter;
}
}
cout<<endl;
在离开 vector 的使用范围之前,记住要删除自由存储区的 string 对象。可以按如下方式来实现:
for(auto& d : data){
delete d;
}
data.clear();
当使用索引来访问指针,就可以使用 delete 运算符删除 string 对象。当循环结束时,vector 中的所有指针元素都会失效,因此不要让 vector 处于这种状态。调用 dear() 移除所有元素,这样 size() 会返回 0。
当然,也可以像下面这样使用迭代器:
for(auto iter = begin(data);iter != end(data);iter++){
delete *iter;
}
如果保存了智能指针,就不用担心要去释放自由存储区的内存。智能指针会做这些事情。下面是一个读入字符串,然后把 shared_ptr<string> 保存到 vector 中的代码片段:
vector<shared_ptr<string>> data;
int n;
cout<<"Please input n "<<endl;
cin>>n;
while(n--){
string d;
cin>>d;
data.push_back(make_shared<string>(d));
}
for(auto d : data){
cout<<*d<<" ";
}
cout<<endl;
输入字符串并打印。
Please input n
3
one two three
one two three
这和使用原生指针别无二致。
vector 模板现在的类型参数是 shared_ptr<string>,push_back() 的参数会调用 make_shared(),在自由存储区生成 string 对象和一个指向它的智能指针。因为智能指针由参数表达式生成,这里会调用 push_back() 将指针移到容器中。
模板类型参数可能有些冗长,我们可以使用 using 来简化代码。例如:
using PString = shared_ptr<string>;
使用 using 后,可以这样定义:
vector<PString> data;
我们可以通过智能指针元素来访问字符串,这和使用原生指针相同。之前输出 words 内容的代码片段都可以使用智能指针。当然,不需要删除自由存储区的 string 对象;因为智能指针会自动做这些事情。执行 words.clear() 会移除全部的元素,因此会调用智能指针的析构函数;这也会导致智能指针释放它们所指向对象的内存。
为了避免 vector 频繁的内存分配,可以先创建 vector,然后使用 reserve() 来分配一定数量的初始内存。例如:
vector<shared_ptr<string>> data;
data.reserve(10);
因为每一个元素都是通过调用 shared_ptr<string> 构造函数生成的,这样生成 vector 会比指定元素个数来生成要好。尽管会有一定的开销,但通常情况下每个智能指针所需要的内存空间远小于它们所指向对象需要的空间。
我们可以在外面使用保存的 shared_ptr<T> 对象的副本。如果不需要这种功能,应该使用 unique_ptr<T> 对象。下面展示如何在 words 中这样使用:
vector<shared_ptr<string>> data;
int n;
cout<<"Please input n :"<<endl;
cin>>n;
while(n--){
string d;
cin>>d;
data.push_back(make_unique<string>(d));
}
在上面的代码中,用 unique 代替 shared 是没有差别的。
优先级队列存储智能指针
智能指针和原生指针的使用基本上是相同的,除非想要自己负责删除它们所指向的对象。
当生成优先级队列或堆时,需要一个顺序关系来确定元素的顺序。当它们保存的是原生指针或智能指针时,总是需要为它们提供一个比较函数;如果不提供,就会对它们所保存的指针而不是指针所指向的对象进行比较.
让我们考虑一下,如何定义一个保存指针的 priority_queue,指针则指向自由存储区的 string 对象。
我们需要定义一个用来比较对象的函数对象,被比较的对象由 shared_ptr<string> 类型的指针指向。按如下所示定义比较函数:
auto comp = [] (const shared_ptr<string>& str1, const shared_ptr<string>& str2){
return *str1 < *str2;
};
这里定义了一个 lambda 表达式 comp,可以用来比较两个智能指针所指向的对象。对 lambda 表达式命名的原因是需要将它作为 priority_queue 模板的类型参数。下面是优先级队列的定义:
priority_queue<shared_ptr<string>,vector<shared_ptr<string>>, decltype(comp)>wordsl {comp};
第一个模板类型参数是所保存元素的类型,第二个用来保存元素的容器的类型,第三个是用来比较元素的函数对象的类型。
我们必须指定第三个模板类型参数,因为 lambda 表达式的类型和默认比较类型 less<T> 不同。
我们仍然可以指定一个外部容器来初始化存放指针的 priority_queue:
vector<shared_ptr<string>>init { make_shared<string> ("one"),make_shared<string>("two"), make_shared<string>("three"),make_shared<string> ("four") };
priority_queue<shared_ptr<string>, vector<shared_ptr<string>>, decltype(comp)>words(comp, init);
vector 容器 init 是用 make_shared<string>() 生成的初始值。优先级队列构造函数的参数是一个定义了元素比较方式的对象以及一个提供初始化元素的容器。先复制 vector 中的智能指针,然后用它们来初始化优先级队列 words。当然,也可以用另一个容器中的元素来初始化优先级队列,这意味不能使用 unique_ptr<string> 元素,必须是shared_ptr<string>。
如果初始元素不需要保留,可以用 priority_queue 对象的成员函数 emplace() 直接在容器中生成它们:
priority_queue<shared_ptr<string>,std::vector<shared_ptr<string>>, decltype(comp)>words1{comp};
words1.emplace(new string {"one"});
words1.emplace(new string {"two"});
words1.emplace(new string {"three"});
words1.emplace(new string {"five"});
words1 的成员函数 emplace() 会调用它们所保存对象的类型的构造函数,也就是 shared_ ptr<string> 的构造函数。这个构造函数的参数是自由存储区的一个 string 对象的地址, string 对象是由 emplace() 的参数表达式生成的。这段代码会在优先级队列中保存 4 个 string 对象的指针,它们分别指向"two","three","one","five"这 4 个对象。元素在优先级队列中的顺序取决于之前定义的 comp。
当然,如果不需要保留初始元素,可以用优先级队列保存 unique_ptr<string> 元素。例如:
auto ucomp = [](const unique_ptr<string>& str1, const unique_ptr<string>& str2){
return *str1 < *str2;
};
priority_queue<unique_ptr<string>, vector<unique_ptr<string>>,decltype(ucomp)> words2 {ucomp};
这个定义比较运算的 lambda 表达式可以接受 unique_ptr<string> 对象的引用。我们需要为这个优先级队列指定 3 个模板类型参数,因为我们需要指定比较函数的类型。第二个模板类型参数可以是 deque<String>,它是默认使用的容器类型。也可以用 emplace() 向优先级队列中添加元素:
words2.emplace(new string{"one"});
words2.emplace(new string {"two"});
words2.emplace (new string {"three"});
words2.emplace(new string {"five"});
或者,也可以使用 push():
words2.push(make_unique<string>("one"));
words2.push(make_unique<string>("two"));
words2.push(make_unique<string>("three"));
words2.push(make_unique<string>("five"));
make_imique<string>() 返回的对象会被移到容器中,这是由右值引用参数版的 push() 自动选择的。
所有的代码汇总如下:
#include<bits/stdc++.h>
using namespace std;
int main(){
auto comp = [] (const shared_ptr<string>& str1, const shared_ptr<string>& str2){
return *str1 < *str2;
};
priority_queue<shared_ptr<string>,vector<shared_ptr<string>>, decltype(comp)>wordsl {comp};
vector<shared_ptr<string>>init { make_shared<string> ("one"),make_shared<string>("two"), make_shared<string>("three"),make_shared<string> ("four") };
priority_queue<shared_ptr<string>, vector<shared_ptr<string>>, decltype(comp)>words(comp, init);
priority_queue<shared_ptr<string>,vector<shared_ptr<string>>, decltype(comp)>words1{comp};
words1.emplace(new string {"one"});
words1.emplace(new string {"two"});
words1.emplace(new string {"three"});
words1.emplace(new string {"five"});
auto ucomp = [](const unique_ptr<string>& str1, const unique_ptr<string>& str2){
return *str1 < *str2;
};
priority_queue<unique_ptr<string>, vector<unique_ptr<string>>,decltype(ucomp)> words2 {ucomp};
words2.emplace(new string{"one"});
words2.emplace(new string {"two"});
words2.emplace (new string {"three"});
words2.emplace(new string {"five"});
words2.push(make_unique<string>("one"));
words2.push(make_unique<string>("two"));
words2.push(make_unique<string>("three"));
words2.push(make_unique<string>("five"));
return 0;
}
至此,C++ STL 之智能指针就暂告一段落了。