RAII:

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术

RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。主要应用智能指针!!!

这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。

RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源一定会被释放。
说白了,就是拥有了对象,就拥有了资源,对象在,资源则在。所以,RAII机制是进行资源管理的有力武器,C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全。在以后的编程实际中,可以使用RAII机制,让自己的代码更漂亮。

裸指针:

1
2
3
4
5
6
7
8
9
#include <memory>

struct X { int a,b,c; };

int main()
{
X* np = new X;
delete np;
}

np是指向X类型对象的指针-如果您已动态分配(新的/ malloc)该对象,则必须删除/释放该对象…像np这样的简单指针称为“裸指针”。实际工作中可能会因为各种原因导致指针指向的资源未释放,导致内存泄漏。

所以,我们需要智能指针来帮助我们管理动态分配的内存。其来源于一个事实:栈比堆要安全的多,因为栈上的变量离开作用域后,会自动销毁并清理。智能指针结合了栈上变量的安全性和堆上变量的灵活性。

智能指针的类对象是栈上的,所以当函数(或程序)结束时会自动被释放

std::unique_ptr

std::unique_ptr是std::auto_ptr的替代品,其用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权:

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
class Fraction
{
private:
int m_numerator = 0;
int m_denominator = 1;

public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator(numerator), m_denominator(denominator)
{
}

friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << "/" << f1.m_denominator;
return out;
}
};

int main()
{

std::unique_ptr<Fraction> f1{ new Fraction{ 3, 5 } };
cout << *f1 << endl; // output: 3/5

std::unique_ptr<Fraction> f2; // 初始化为nullptr

// f2 = f1 // 非法,不允许左值赋值
f2 = std::move(f1); // 此时f1转移到f2,f1变为nullptr

// C++14 可以使用 make_unique函数
auto f3 = std::make_unique<Fraction>(2, 7);
cout << *f3 << endl; // output: 2/7

// 处理数组,但是尽量不用这样做,因为你可以用std::array或者std::vector
auto f4 = std::make_unique<Fraction[]>(4);
std::cout << f4[0] << endl; // output: 0/1

cin.ignore(10);
return 0;
}

std::shared_ptr

std::shared_ptr与std::unique_ptr类似。std::shared_ptr与std::unique_ptr的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
auto ptr1 = std::make_shared<Resource>();
cout << ptr1.use_count() << endl; // output: 1
{
//通过复制构造函数或者赋值来共享内存
auto ptr2 = ptr1; // 通过复制构造函数使两个对象管理同一块内存
std::shared_ptr<Resource> ptr3; // 初始化为空
ptr3 = ptr1; // 通过赋值,共享内存
cout << ptr1.use_count() << endl; // output: 3
cout << ptr2.use_count() << endl; // output: 3
cout << ptr3.use_count() << endl; // output: 3
}
// 此时ptr2与ptr3对象析构了
cout << ptr1.use_count() << endl; // output: 1

cin.ignore(10);
return 0;
}

std::weak_ptr

std::shared_ptr可以实现多个对象共享同一块内存,当最后一个对象离开其作用域时,这块内存被释放。但是仍然有可能出现内存无法被释放的情况,联想一下“死锁”现象,对于std::shared_ptr会出现类似的“循环引用”现象:

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
class Person
{
public:
Person(const string& name):
m_name{name}
{
cout << m_name << " created" << endl;
}

virtual ~Person()
{
cout << m_name << " destoryed" << endl;
}

friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
{
if (!p1 || !p2)
{
return false;
}

p1->m_partner = p2;
p2->m_partner = p1;

cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
return true;
}

private:
string m_name;
std::shared_ptr<Person> m_partner;
};

int main()
{
{
auto p1 = std::make_shared<Person>("Lucy");
auto p2 = std::make_shared<Person>("Ricky");
partnerUp(p1, p2); // 互相设为伙伴
}

cin.ignore(10);
return 0;
}

整个程序很简单,创建两个Person动态对象,交由智能指针管理,并且通过partnerUp()函数互相引用为自己的伙伴。但是执行的结果却却有问题:

1
2
3
Lucy created
Ricky created
Lucy is now partnered with Ricky

对象没有被析构!出现内存泄露!仔细想想std::shared_ptr对象是什么时候才能被析构,就是引用计数变为0时,但是当你想析构p1时,p2内部却引用了p1,无法析构;反过来也无法析构。互相引用造成了“死锁”,最终内存泄露!这样的情形也会出现在“自锁”中:

1
2
3
4
5
6
7
8
9
10
int main()
{
{
auto p1 = std::make_shared<Person>("Lucy");
partnerUp(p1, p1); // 自己作为自己的伙伴
}

cin.ignore(10);
return 0;
}

这时候std::weak_ptr应运而生。std::weak_ptr可以包含由std::shared_ptr所管理的内存的引用。但是它仅仅是旁观者,并不是所有者。那就是std::weak_ptr不拥有这块内存,当然不会计数,也不会阻止std::shared_ptr释放其内存。但是它可以通过lock()方法返回一个std::shared_ptr对象,从而访问这块内存。这样我们可以用std::weak_ptr来解决上面的“循环引用”问题:

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
class Person
{
public:
Person(const string& name):
m_name{name}
{
cout << m_name << " created" << endl;
}

virtual ~Person()
{
cout << m_name << " destoryed" << endl;
}

friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
{
if (!p1 || !p2)
{
return false;
}

p1->m_partner = p2; // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
p2->m_partner = p1;

cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
return true;
}

private:
string m_name;
std::weak_ptr<Person> m_partner;
};

int main()
{
{
auto p1 = std::make_shared<Person>("Lucy");
auto p2 = std::make_shared<Person>("Ricky");
partnerUp(p1, p2); // 互相设为伙伴
}

cin.ignore(10);
return 0;
}

程序正常输出(注意创建与析构的顺序是反的?在栈上!):

1
2
3
4
5
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed

常见问题:

Q:shared_ptr的原理
A:shared_ptr维护了一个指向control block的指针对象,来记录引用个数。

Q:weak_ptr的原理
A:weak_ptr用于避免shared_ptr相互指向产生的环形结构,造成的内存泄漏。weak_ptr count是弱引用个数;弱引用个数不影响shared count和对象本身,shared count为0时则直接销毁。

Q:如何判断weak_ptr的对象是否失效?
A:1、expired():检查被引用的对象是否已删除。
2、lock()会返回shared指针,判断该指针是否为空。
3、use_count()也可以得到shared引用的个数,但速度较慢。

Q:shared 和 unique区别
A:unique具有唯一性,对指向的对象值存在唯一的unique_ptr。unique_ptr不可复制,赋值,但是move()可以转换对象的所有权,局部变量的返回值除外。与shared_ptr相比,若自定义删除器,需要在声明处指定删除器类型,而shared不需要,shared自定义删除器只需要指定删除器对象即可,在赋值时,可以随意赋值,删除器对象也会被赋值给新的对象。

参考链接:

https://zhuanlan.zhihu.com/p/54078587?from_voters_page=true