0%

C++11新特性 - 智能指针

C++11 新增的智能指针

不当释放指针带来的问题

指针是 C++ 非常强大的功能,但是在实践中,不正确地使用指针会造成一系列的问题

例如,我们定义了一个类 Dog

1
2
3
4
5
6
7
8
9
class Dog {
private:
string m_name;
public:
void bark() { cout << "Dog " << m_name << " rules!" << endl; }
Dog(string name) {cout << "Dog is created: " << name << endl; m_name = name; }
Dog() { cout << "Nameless dog created." << endl; m_name = "nameless"; }
~Dog() { cout << "dog is destroyed: " << m_name << endl; }
};

在使用指针的过程中,常见的不当释放指针会造成两种后果: 悬空指针内存泄漏

1
2
3
4
5
6
7
void foo() {
Dog *p= new Dog('Gunner');
/*Do something*/
delete p;
/*Do something else*/
p->bark(); // 悬空指针 - 未定义行为
}
1
2
3
4
void foo() {
Dog *p= new Dog('Gunner');
/*Do something*/
} // 忘记 delete p, 内存泄漏

正确地释放指针通常是一件麻烦且乏味的工作,C++11 引入了智能指针来帮助程序员正确地释放指针

shared_ptr

shared_ptr 的原理是,在内部维护了一个对象的引用计数,但引用计数归零,即最后一个指向对象的 shared_ptr 被销毁时,将指向的对象也释放掉

基本使用方法

shared_ptr 使用非常简单

1
2
3
4
5
6
7
8
9
void foo() {
shared_ptr<Dog> p(new Dog("Gunner")); // 引用计数 = 1
{
shared_ptr<Dog> p2 = p; // 引用计数 = 2
p2->bark();
cout << p.use_count() << endl; // 输出 2
} // 引用计数 = 1
p->bark();
} // 引用计数 = 0,释放 Dog

不当使用方法

1
2
3
4
5
void foo() {
Dog *d = new Dog();
shared_ptr<Dog> p(d); // p 引用计数 = 1
shared_ptr<Dog> p2(d); // p2 引用计数 = 1
} // p 和 p2 分别释放 Dog, 未定义行为

因此,我们应该牢记,使用智能指针时,一个对象被创建的时候就应该被赋值给智能指针,而不应该使用原生指针

使用 make_shared 生成 shared_ptr

在使用 shared_ptr 时,C++ 推荐使用 make_shared 函数来生成 shared_ptr,更快更安全

1
shared_ptr<Dog> p = make_shared<Dog>("Tank");

推荐这样做的原因是,shared_ptr<Dog> p(new Dog("Gunner")) 包含两个步骤:1. 生成 Dog; 2. 生成 p;而 make_shared 把这两部合并成了一步

释放对象

shared_ptr 在这些场景下会释放对象:

1
2
3
4
5
6
7
8
9
void foo() {
shared_ptr<Dog> p1 = make_shared<Dog>("Gunner");
shared_ptr<Dog> p2 = make_shared<Dog>("Tank");

// 以下三种情况均会释放 Gunner
p1 = p2;
p1 = nullptr;
p1.reset();
}

自定义 deleter

默认情况下,shared_ptr 的会使用 delete 运算符作为 deleter,但是我们也可以自定义 deleter,例如使用 lambda 函数:

1
2
3
4
shared_ptr<Dog> pD4(
new Dog("Victor"),
[](Dog* p) {cout << "deleting a dog.\n"; delete p;}
);

当我们使用 shared_ptr 指向数组时,自定义的 deleter 就非常有用了,否则,如果使用默认的 delete 而非 delete[] 除第一个意外的 Dog 就内存泄漏了

1
shared_ptr<Dog> pDD(new Dog[3], [](Dog* p) {delete[] p;} );

shared_ptr 获取原生指针

我们可以使用 shared_ptr.get() 方法获取原生指针

1
2
shared_ptr<Dog> p1 = make_shared<Dog>("Gunner");
Dog *d = p1.get();

如果一定要使用这种方法,要非常小心,否则会带来问题,例如我们引入一个新的类 DogHouse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DogHouse {
Dog *m_pD;
public:
void saveDog(Dog *p) { m_pD = p; cout << "Dog entered house." << endl;}
};
{
DogHouse doghouse;
{
shared_ptr<Dog> p = make_shared<Dog>("Gunner");
Dog *d = p.get();
doghouse.saveDog(d);
}// p 释放 Gunner
// 这里 doghouse.m_pD 会成为一个空悬指针
}

weak_ptr

weak_ptr 的使用场景

如果我们按照这种方法使用 shared_ptr, 那么 pDpD2 都不会被释放,造成内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog {
shared_ptr<Dog> m_pFriend;
public:
string m_name;
void bark() { cout << "Dog " << m_name << " rules!" << endl; }
Dog(string name) { cout << "Dog is created: " << name << endl; m_name = name; }
~Dog() { cout << "dog is destroyed: " << m_name << endl; }
void makeFriend(shared_ptr<Dog> f) { m_pFriend = f; }
};

int main ()
{
shared_ptr<Dog> pD(new Dog("Gunner"));
shared_ptr<Dog> pD2(new Dog("Smokey"));
pD->makeFriend(pD2);
pD2->makeFriend(pD);
}

原因是 pD->m_pFriend = pD2pD2->m_pFriend = pD,这里构成了循环引用,即使出了 main 函数的作用域后 Gunner 和 Smokey 这两个对象仍然保持着一个对方的引用,于是他们两个都不会被释放

为了解决这个问题,我们需要使用 weak_ptrweak_ptr 并不会增加 shared_ptr 的引用计数

1
2
3
4
class Dog {
weak_ptr<Dog> m_pFriend; // 将 shared_ptr 替换为 weak_ptr
//....
};

访问 weak_ptr 指向的对象

shared_ptr 代表了一种对象的共享所有权,而 weak_ptr 这不拥有对象的所有权,只是保留了一种访问对象的能力,即 weak_ptr 并不关心对象何时以何种方法被释放

因此,weak_ptr 可能会指向一个已经被删除的对象,所以我们想访问 weak_ptr 指向的对象时,必须使用过 weak_ptr.lock() 方法,例如我们要给 Dog 类添加一个 showFriend 方法

1
2
3
4
5
6
7
8
class Dog {
//...
void showFriend() {
if (!m_pFriend.expired()) {
cout << "My friend is: " << m_pFriend.lock()->m_name << endl;
}
}
}

在这个例子中,我们首先使用 weak_ptr.expired() 方法,检查指向的对象是否已被释放了,如果没被释放,则通过 weak_ptr.lock() 方法,生成一个 shared_ptr 方法对象,这样,保证了我们在访问 m_name 属性时,该对象不会被释放

我们同样也可以对 weak_ptr 调用 use_count 方法

1
cout << m_pFriend.use_count() << endl;

unique_ptr

unique_ptr 的使用场景

unique_ptr 代表了对象的独有所有权,是一种轻量级的智能指针

在老式的 C++ 中,我们经常使用这样的写法,处理不当,可能内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void test(){
Dog *pD = new Dog("Gunner");
pD->bark();

/*Do something with pD*/
//内存泄漏 Case 1, 中途 return
if (condition) return;

//内存泄漏 Case 2,异常
throw runtime_error("error");

delete pD;
}

int main() {
test();
}

在 C++11 中,我们只需把原生指针替换为 unique_ptr 即可高枕无忧

1
2
3
4
5
void test() {
unique_ptr<Dog> pD(new Dog("Gunner"));
pD->bark();
/*Do something else with pD*/
}

当然,这里我们也可以用 shared_ptr,但是 unique_ptr 更加轻量级

shared_ptr 获取原生指针

类似 shared_ptr.get(),我们可以调用 unique_ptr.release() 来获取原生指针,要注意的是,调用之后该 unique_ptr 会变成空指针

1
2
3
4
5
void test() {
unique_ptr<Dog> pD(new Dog("Gunner"));
Dog *d = pD.release();
if (!pD) cout << "empty\n"; // 输出 empty
}

类似的,如果我们对 weak_ptr 调用 reset() 函数或赋值为 nullptr, 也会导致指向的对象被删除

unique_ptr 之间转移所有权

由于 unique_ptr 代表了对象的独有所有权,因此只能被移动不能被拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test() {
unique_ptr<Dog> pD(new Dog("Gunner"));
unique_ptr<Dog> pD2(new Dog("Smokey"));
pD->bark();

pD2 = pD; // 编译器报错

pD2 = move(pD);
// 1. Smokey 被释放
// 2. pD 变成空指针
// 3. pD2 指向 Gunner.

pD2->bark();
}

unique_ptr 作为参数和返回值

unique_ptr 作为参数时,出了函数作用域会释放对象,作为返回值时,会制动使用移动语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void f(unique_ptr<Dog> p) {
p->bark();
} // 释放 p 指向的 Dog 对象

unique_ptr<Dog> getDog() {
unique_ptr<Dog> p(new Dog("Smokey"));
return p; // 自动使用移动语义
}

void test() {
unique_ptr<Dog> pD(new Dog("Gunner"));
f(move(pD)); // Gunner 已经被释放了
if (!pD) cout << "empty\n"; // 输出 empty

unique_ptr<Dog> pD2 = getDog();
pD2->bark();
}