C++ STL 之智能指针

本节我们将介绍 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 之智能指针就暂告一段落了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容