C++11 智能指针

如果在程序中使用 new 从堆 (自由存储区) 分配内存,等到不再需要时,应使用delete将其释放。C++引入了智能指针auto ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL时)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了 auto_pt 并新增了三种智能指针 unique_ptrshared_ptrweak_ptr,所有新增的智能指针都能与STL容器移动语义协同工作

1. 为何使用智能指针

在程序中使用 new 从堆 (自由存储区) 分配内存,等到不再需要时,应使用delete将其释放。但是即使在代码中使用delete将其释放内存,也可能造成问题,如以下代码,当出现异常时,delete将不被执行而导致内存泄漏。

void remodel(std::string &str) {
    std::string *ps = new std::string(str);
    ...
    if (weird_thing())
        throw exception();
    str = *ps;
    delete ps;
    return;
}

remodel()这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将从栈内存中删除——因此指针 ps 占据的内存将被释放。如果 ps 指向的内存也被释放,那该有多好啊。如果ps有一个析构函数,该析构函数将在 ps 过期时释放它指向的内存。因此,ps 的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是auto_ptrunique_ptrshared_ptr 背后的思想。模板auto_ptrC++98提供的解决方案,C++11已将其摒弃,并提供了另外两种解决方案。然而,虽然 auto_ptr 被摒弃,但它已使用了多年;同时,如果您的编译器不支持其他两种解决方案,auto_ptr将是唯一的选择。

2. 使用智能指针

这三个智能指针模板(auto_ptrunique_ptrshared_ptr)都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。图16.2说明了auto_ptr和常规指针在行为方面的差别;share_ptrunique_ptr 的行为与auto_ptr相同。

Smart Pointer Considerations

Begin by considering assignment:

auto_ptr<string> ps (new string("I reigned lonely as a cloud."));
auto_ptr<string> vocation;
vocation = ps;

What should the assignment statement accomplish? If ps and vocation were ordinary pointers, the result would be two pointers pointing to the same string object. That is not acceptable here because the program would wind up attempting to delete the same object twice—once when ps expires,and once when vocation expires. There are ways to avoid this problem:

  • Define the assignment operator so that it makes a deep copy. This results in two pointers pointing to two distinct objects, one of which is a copy of the other.

  • Institute the concept of ownership, with only one smart pointer allowed to own a particular object. Only if the smart pointer owns the object will its destructor delete the object. Then have assignment transfer ownership. This is the strategy used for auto_ptr and for unique_ptr, although unique_ptr is somewhat more restrictive.

  • **Create an even smarter pointer that keeps track of how many smart pointers refer to a particular object. **This is called reference counting. Assignment, for example, would increase the count by one,and the expiration of a pointer would decrease the count by one. Only when the final pointer expires would delete be invoked. This is the shared_ptr strategy.

The same strategies we’ve discussed for assignment, of course, would also apply to the copy constructors.

Each approach has its uses. Listing 16.6 shows an example for which auto_ptr is poorly suited.

// fowl.cpp -- auto_ptr a poor choice
#include <iostream>
#include <string>
#include <memory>

int main() {
    using namespace std;
    auto_ptr<string> films[5] = {
            auto_ptr<string>(new string("Fowl Balls")),
            auto_ptr<string>(new string("Duck Walks")),
            auto_ptr<string>(new string("Chicken Runs")),
            auto_ptr<string>(new string("Turkey Errors")),
            auto_ptr<string>(new string("Goose Eggs"))
    };
    auto_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership
    cout << "The nominees for best avian baseball film are\n";
    for (int i = 0; i < 5; i++)
        cout << *films[i] << endl;
    cout << "The winner is " << *pwin << "!\n";
    cin.get();
    return 0;
}

Here is some sample output:

The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Segmentation fault (core dumped)

The “core dumped” message should help fix in your memory that a misused auto_ptr can be a problem. (The behavior for this sort of code is undefined, so you might encounter different behavior, depending on your system.) Here the problem is that the following statement transfers ownership from films[2] to pwin:

pwin = films[2]; // films[2] loses ownership

That causes films[2] to no longer refer to the string. After an auto_ptr gives up ownership of an object, it no longer provides access to the object. When the program goes to print the string pointed to by films[2], it finds the null pointer, which apparently is an unpleasant surprise.

Suppose you go back to Listing 16.6 but use shared_ptr instead of auto_ptr. (You’ll need a compiler that supports the C++11 shared_ptr class.) Then the program runs fine and gives this output:

The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Chicken Runs
Turkey Errors
Goose Eggs
The winner is Chicken Runs!
2.1 Why unique_ptr Is Better than auto_ptr
auto_ptr<string> p1(new string("auto"); //#1
auto_ptr<string> p2; //#2
p2 = p1; //#3

在语句#3中,p2接管 string 对象的所有权后,p1的所有权将被剥夺。前面说过,这是件好事,可防止p1p2的析构函数试图删除同一个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。

unique_ptr<string> p3(new string("auto"); //#4
unique_ptr<string> p4; //#5
p4 = p3; //#6

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptrauto_ptr更安全(编译阶段错误比潜在的程序崩溃更安全)。
但有时候,将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr <string> demo(const char *s) {
    unique_ptr <string> temp(new string(s));
    return temp;
}

unique_ptr<string> ps;
ps = demo("Uniquely special");

demo()返回一个临时unique_ptr,然后 ps接管了原本归返回的unique_ptr所有的对象,而返回的unique_ptr被销毁。这没有问题,因为ps拥有了 string 对象的所有权。但这里的另一个好处是,demo()返回的临时unique_ptr很快被销毁,没有机会使用它来访问无效的数据。换句话说,没有理由禁止这种赋值。神奇的是,编译器确实允许这种赋值!

总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ ptr是个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器将禁止这样做:

using namespace std;
unique_ptr<string> pu1(new string "Hi ho!");
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string "Yo!"); // #2 allowed

相比于auto_ptrunique_ptr还有另一个优点。它有一个可用于数组的变体。别忘了,必须将deletenew配对,将delete []new []配对。模板auto_ptr使用delete而不是delete [],因此只能与new一起使用,而不能与new []一起使用。但unique_ptr有使用new[]delete [] 的版本:

std::unique_ptr< double[]>pda(new double(5)); // will use delete []

警告:使用new分配内存时,才能使用auto_ ptrshared_ptr,使用new []分配内存时,不能使用它们。不使用new分配内存时,不能使用auto_ptrshared_ptr;不使用newnew[]分配内存时,不能使用unique_ptr

2.2 选择智能指针

应使用哪种智能指针呢?如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL 容器包含指针。很多STL 算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique ptr(编译器发出警告)和 auto_ptr(行为不确定)。如果您的编译器没有提供shared_ptr,可使用 Boost库提供的shared_ptr
如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权将转让给接受返回值的unique_ptr, 而该智能指针将负责调用delete。可将unique_ptr存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋给另一个的方法或算法(如sort())。

3. shared_ptr 使用示例
// shared_ptr constructor example
#include <iostream>
#include <memory>

struct C {
    int *data;
};

int main() {
    std::shared_ptr<int> p1;
    std::shared_ptr<int> p2(nullptr);
    std::shared_ptr<int> p3(new int);
    std::shared_ptr<int> p4(new int, std::default_delete<int>());
    std::shared_ptr<int> p5(new int, [](int *p) { delete p; }, std::allocator<int>());
    std::shared_ptr<int> p6(p5);
    std::shared_ptr<int> p7(std::move(p6));
    std::shared_ptr<int> p8(std::unique_ptr<int>(new int));
    std::shared_ptr<C> obj(new C);
    std::shared_ptr<int> p9(obj, obj->data);

    std::cout << "use_count:\n";
    std::cout << "p1: " << p1.use_count() << '\n';
    std::cout << "p2: " << p2.use_count() << '\n';
    std::cout << "p3: " << p3.use_count() << '\n';
    std::cout << "p4: " << p4.use_count() << '\n';
    std::cout << "p5: " << p5.use_count() << '\n';
    std::cout << "p6: " << p6.use_count() << '\n';
    std::cout << "p7: " << p7.use_count() << '\n';
    std::cout << "p8: " << p8.use_count() << '\n';
    std::cout << "p9: " << p9.use_count() << '\n';
    return 0;
}
use_count:
p1: 0
p2: 0
p3: 1
p4: 1
p5: 2
p6: 0
p7: 2
p8: 1
p9: 2

参考资料:

[1] C++.Primer.Plus.6th.Edition.

[2] C++ Reference

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值