8

智能指针避坑指南——几种常见的错误用法

 3 years ago
source link: https://bianchengnan.gitee.io/articles/misuse-cases-of-shared-ptr/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

智能指针避坑指南——几种常见的错误用法

2020-11-29

|

2020-12-13

| 开发

| 热度: 20℃

智能指针的出现大大减轻了 C++ 程序员的心理负担(最少对于我是这样的),不用再时时刻刻担心一个 new 出来的指针是否被 delete 的问题了。虽然智能指针很强大,但是如果用不好,还是会导致各种各样的问题。最近,在项目里看到了几种智能指针的典型错误用法。有的严重,有的轻,有的问题在研发阶段并没有立刻暴露出来,埋下了一颗定时炸弹。趁着这个机会,总结一下几种常见的错误用法。希望对各位小伙伴儿有帮助。

说明:智能指针有很多种,标准库提供了几种,比如最常用的 shared_ptrunique_ptr, 比较少用的 weak_ptr 以及被废弃的 auto_ptr。项目代码里还有一种基于引用计数的智能指针。本文只列举几种常见的错误用法,不会深入到各种智能指针的实现。如果想深刻理解智能指针的用法,一定要看源码。

在列举各种错误用法之前,想说明一点:这些错误都是实际遇到过的,并不是凭空想出来的。所以,即使有些示例代码错的是那么明显,也请不要轻视。

1. 同一指针交给两个智能指针管理导致二次释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace case1
{
/*
* pRawData is already managed by a shared_ptr (pData in function #Entry()),
* in function #Use(), another shared_ptr (pNewData) manages the raw pointer too.
* now, we have two shared_ptr manage the same raw pointer. Oops, double free!
*/
static void Use(Common::CDerived* pRawData)
{
std::shared_ptr<Common::CDerived> pNewData(pRawData);
pNewData->DoSomething();
}

static void Entry()
{
auto pData = std::make_shared<Common::CDerived>();
Use(pData.get());
}
}

2. 错误的动态转换导致的二次释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#pragma once
#include <memory>

#include "common.h"

namespace case2
{
static void Use(std::shared_ptr<Common::CDerived> pData)
{
pData->DoSomething();
}

static std::shared_ptr<Common::CBase> GetData()
{
return std::make_shared<Common::CDerived>();
}
static void Entry()
{
auto pTest = std::make_shared<Common::CDerived>();

// Oops, double free
std::shared_ptr<Common::CBase> pData = GetData();
auto pRawData = pData.get();
Common::CDerived* pDerived = dynamic_cast<Common::CDerived*>(pRawData);
if (pDerived)
{
Use(std::shared_ptr<Common::CDerived>(pDerived));
}

// code below is good
//Use(std::dynamic_pointer_cast<Common::CDerived>(pData));
}
}

3. 返回智能指针管理的原生指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace case3
{
/*
* after #ReturnRawPointer() and #GetRawPointer() returned, shared_ptr's destructor is called,
* then the returned raw pointer points to a deleted address. bang!
*/
static Common::CDerived* ReturnRawPointer()
{
auto pData = std::make_shared<Common::CDerived>();
return pData.get();
}

static bool GetRawPointer(Common::CDerived* & pReturnedData)
{
auto pData = std::make_shared<Common::CDerived>();
pReturnedData = pData.get();
return (pReturnedData != nullptr);
}

static void Entry()
{
auto pData = ReturnRawPointer();
pData->DoSomething();

pData = nullptr;
GetRawPointer(pData);
pData->DoSomething();
}
}

4. 类中的成员变量指针交给外部智能指针管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace case4
{
/*
* CTest::pData is managed by CTest, but when call #Use(), it is used to construct a shared_pt,
* then it is also managed by a shared_ptr. double free!
*/
class CTest
{
public:
CTest() { pData = new Common::CDerived(); }

~CTest() { delete pData; }

Common::CDerived* pData;
};

/* this function need a shared_ptr, this is ok.*/
static void Use(std::shared_ptr<Common::CDerived> pData)
{
pData->DoSomething();
}

static void Entry()
{
auto pTest = std::make_shared<CTest>();
Use(std::shared_ptr<Common::CDerived>(pTest->pData));
}
}

5. 栈变量的地址交给智能指针管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace case5
{
/*
* data is on stack, after managed by a shared_ptr, it will be deleted.
* we CAN NOT delete an address on stack!
*/
static void Use(std::shared_ptr<Common::CDerived> pData)
{
pData->DoSomething();
}

static void Entry()
{
// well, I have simplify this error quite much.
Common::CDerived data;
auto pData = std::shared_ptr<Common::CDerived>(&data);

Use(pData);
}
}

6. 循环引用导致的内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#pragma once
#include <memory>
#include <vector>
#include "common.h"

namespace case6
{
class CPerson
{
public:
CPerson() { printf(__FUNCTION__ "\n"); }
virtual ~CPerson() { printf(__FUNCTION__ "\n"); }
};

class CParent : public CPerson
{
public:
CParent() { printf(__FUNCTION__ "\n"); }
virtual ~CParent() { printf(__FUNCTION__ "\n"); }
std::vector<std::shared_ptr<CPerson>> children;
};

class CChild : public CPerson
{
public:
CChild() { printf(__FUNCTION__ "\n"); }
virtual ~CChild() { printf(__FUNCTION__ "\n"); }
std::shared_ptr<CPerson> parent;
};

static void Entry()
{
// Oops, no one will be died then.
std::shared_ptr<CParent> parent = std::make_shared<CParent>();
std::shared_ptr<CChild> child1 = std::make_shared<CChild>();
std::shared_ptr<CChild> child2 = std::make_shared<CChild>();
std::shared_ptr<CChild> child3 = std::make_shared<CChild>();

parent->children.push_back(child1);
parent->children.push_back(child2);
parent->children.push_back(child3);
child1->parent = parent;
child2->parent = parent;
child3->parent = parent;
}
}

完整的示例代码下载地址

CSDN:https://download.csdn.net/download/xiaoyanilw/13203123

百度云:https://pan.baidu.com/s/1dFUKevDXJZfja3HyO-jazg 提取码: 8ra8

以上示例代码虽然有的看起来非常不可思议,这是我简化后的结果,在实际代码中经常以另外一种形式出现,一不小心就容易中招。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK