在前一章中,您学习了模板编程和泛型编程的好处。在本章中,您将了解以下智能指针主题:
- 内存管理
- 原始指针的问题
- 循环依赖
- 智能指针:
auto_ptr
unique_ptr
shared_ptr
weak_ptr
让我们探索 C++ 提供的内存管理工具。
在 C++ 中,内存管理通常是软件开发人员的责任。这是因为 C++ 标准没有在 C++ 编译器中强制垃圾收集支持;因此,这是留给编译器供应商的选择。例外的是,Sun C++ 编译器附带了一个名为libgc
的垃圾收集库。
C++ 语言有许多强大的特性。不用说,在这些特性中,指针是最强大、最有用的特性之一。说了这么多,指针非常有用,它们确实有自己奇怪的问题,因此必须负责任地使用它们。当内存管理不被重视或做得不太正确时,会导致许多问题,包括应用崩溃、核心转储、分段故障、调试问题的间歇性困难、性能问题等。悬空指针或流氓指针有时会干扰其他不相关的应用,而罪魁祸首应用会静默执行;事实上,受害者应用可能会受到多次指责。关于内存泄漏最糟糕的部分是,在某些时候,它变得非常棘手,甚至有经验的开发人员最终会调试受害者的代码无数个小时,而罪魁祸首的代码却没有被触及。有效的内存管理有助于避免内存泄漏,并允许您开发高性能的内存高效应用。
由于每个操作系统的内存模型不同,对于相同的内存泄漏问题,每个操作系统在不同的时间点可能会有不同的行为。内存管理是一个很大的话题,C++ 提供了很多方法来做好它。我们将在接下来的章节中讨论一些有用的技术。
大多数 C++ 开发人员都有一个共同点:我们都喜欢编写复杂的代码。你问一个开发人员,“嘿,伙计,你是想重用已经存在并且有效的代码,还是想自己开发一个?”虽然在外交上,大多数开发人员会说尽可能重用已经存在的东西,但他们的内心会说,“我希望我能自己设计和开发它。”复杂的数据结构和算法往往需要指针。在遇到麻烦之前,使用原始指针真的很酷。
原始指针在使用前必须分配内存,一旦完成就需要释放;就这么简单。然而,在一个产品中事情变得复杂,指针分配可能发生在一个地方,而释放可能发生在另一个地方。如果内存管理决策做得不正确,人们可能会认为释放内存是调用者或被调用者的责任,有时,这两个地方的内存都可能释放不出来。还有一种可能是,同一个指针在不同的地方被多次删除,这可能导致应用崩溃。如果这发生在 Windows 设备驱动程序中,它很可能会以死亡的蓝屏告终。
试想一下,如果有一个应用异常,并且抛出异常的函数有一堆指针,这些指针在异常发生之前就已经被分配了内存,那该怎么办?谁都说不准:会有内存泄漏。
让我们举一个利用原始指针的简单例子:
#include <iostream>
using namespace std;
class MyClass {
public:
void someMethod() {
int *ptr = new int();
*ptr = 100;
int result = *ptr / 0; //division by zero error expected
delete ptr;
}
};
int main ( ) {
MyClass objMyClass;
objMyClass.someMethod();
return 0;
}
现在,运行以下命令:
g++ main.cpp -g -std=c++ 17
检查这个程序的输出:
main.cpp: In member function ‘void MyClass::someMethod()’:
main.cpp:12:21: warning: division by zero [-Wdiv-by-zero]
int result = *ptr / 0;
现在,运行以下命令:
./a.out
[1] 31674 floating point exception (core dumped) ./a.out
C++ 编译器真的很酷。看这条警告信息,它在指出问题方面敲了几下。我喜欢 Linux 操作系统。Linux 在发现行为不端的流氓应用方面相当聪明,它会在它们对其余应用或操作系统造成任何损害之前及时将其关闭。核心转储实际上是好的,尽管它被诅咒了,而不是庆祝 Linux 方法。你猜怎么着,微软的 Windows 操作系统也同样聪明。当他们发现一些应用进行可疑的内存访问时,他们会进行错误检查,并且视窗操作系统也支持迷你转储和完全转储,这相当于 Linux 操作系统中的核心转储。
让我们看一下 Valgrind 工具的输出,以检查内存泄漏问题:
valgrind --leak-check=full --show-leak-kinds=all ./a.out
==32857== Memcheck, a memory error detector
==32857== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==32857== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==32857== Command: ./a.out
==32857==
==32857==
==32857== Process terminating with default action of signal 8 (SIGFPE)
==32857== Integer divide by zero at address 0x802D82B86
==32857== at 0x10896A: MyClass::someMethod() (main.cpp:12)
==32857== by 0x1088C2: main (main.cpp:24)
==32857==
==32857== HEAP SUMMARY:
==32857== in use at exit: 4 bytes in 1 blocks
==32857== total heap usage: 2 allocs, 1 frees, 72,708 bytes allocated
==32857==
==32857== 4 bytes in 1 blocks are still reachable in loss record 1 of 1
==32857== at 0x4C2E19F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32857== by 0x108951: MyClass::someMethod() (main.cpp:8)
==32857== by 0x1088C2: main (main.cpp:24)
==32857==
==32857== LEAK SUMMARY:
==32857== definitely lost: 0 bytes in 0 blocks
==32857== indirectly lost: 0 bytes in 0 blocks
==32857== possibly lost: 0 bytes in 0 blocks
==32857== still reachable: 4 bytes in 1 blocks
==32857== suppressed: 0 bytes in 0 blocks
==32857==
==32857== For counts of detected and suppressed errors, rerun with: -v
==32857== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[1] 32857 floating point exception (core dumped) valgrind --leak-check=full --show-leak-kinds=all ./a.out
在这个输出中,如果你注意到文本的粗体部分,你会注意到 Valgrind 工具确实指出了导致这个核心转储的源代码行号。main.cpp
文件第 12 行如下:
int result = *ptr / 0; //division by zero error expected
当异常出现在main.cpp
文件的第 12 行时,出现在异常下面的代码将永远不会被执行。在main.cpp
文件的第 13 行,出现一个delete
语句,由于异常,该语句将永远不会被执行:
delete ptr;
分配给前面的原始指针的内存不会被释放,因为指针所指向的内存在堆栈展开过程中不会被释放。每当一个函数抛出异常,而该异常又不是由同一个函数处理时,栈展开就得到保证。然而,在堆栈展开过程中,只有自动局部变量会被清理,而不是指针所指向的内存。这会导致内存泄漏。
这是使用原始指针带来的奇怪问题之一;还有许多其他类似的场景。希望你现在确信使用原始指针的刺激是有代价的。但是付出的代价并不值得,因为在 C++ 中有很好的替代方法来处理这个问题。你说得对,使用智能指针是一种解决方案,它提供了使用指针的好处,而无需支付原始指针的附加成本。
因此,智能指针是在 C++ 中安全使用指针的方法。
在 C++ 中,智能指针可以让您专注于手头的问题,将您从处理自定义垃圾收集技术的烦恼中解放出来。智能指针允许您安全地使用原始指针。他们负责清理原始指针使用的内存。
C++ 支持多种类型的智能指针,可用于不同的场景:
auto_ptr
unique_ptr
shared_ptr
weak_ptr
在 C++ 11 中引入了auto_ptr
智能指针。auto_ptr
智能指针有助于在堆内存超出范围时自动释放堆内存。然而,由于auto_ptr
将所有权从一个auto_ptr
实例转移到另一个实例的方式,它被否决了,而unique_ptr
被引入作为它的替代。shared_ptr
智能指针帮助多个共享智能指针引用同一个对象,并承担内存管理负担。当应用设计中存在循环依赖问题时,weak_ptr
智能指针有助于解决因使用shared_ptr
而出现的内存泄漏问题。
还有其他类型的智能指针和相关的东西不常用,它们列在下面的项目符号列表中。但是,我强烈建议您自己探索它们,因为您永远不知道什么时候会发现它们有用:
owner_less
enable_shared_from_this
bad_weak_ptr
default_delete
如果两个或多个智能指针共享相同的原始指向对象,则owner_less
智能指针有助于比较它们。enable_shared_from_this
智能指针有助于获得this
指针的智能指针。bad_weak_ptr
智能指针是一个异常类,意味着shared_ptr
是使用无效的智能指针创建的。default_delete
智能指针引用unique_ptr
使用的默认销毁策略,调用delete
语句,同时也支持使用delete[]
的数组类型部分特殊化。
在本章中,我们将逐一探讨auto_ptr
、shared_ptr
、weak_ptr
和unique-ptr
。
auto_ptr
智能指针获取一个原始指针,将其包装,并确保每当auto_ptr
对象超出范围时,原始指针指向的内存都会释放回来。任何时候,只有一个auto_ptr
智能指针可以指向一个对象。因此,每当一个auto_ptr
指针被分配给另一个auto_ptr
指针时,所有权被转移到已经接收到该分配的auto_ptr
实例;复制auto_ptr
智能指针时也会发生同样的情况。
用一个简单的例子来观察正在发生的事情会很有趣,如下所示:
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
using namespace std;
class MyClass {
private:
static int count;
string name;
public:
MyClass() {
ostringstream stringStream(ostringstream::ate);
stringStream << "Object";
stringStream << ++ count;
name = stringStream.str();
cout << "\nMyClass Default constructor - " << name << endl;
}
~MyClass() {
cout << "\nMyClass destructor - " << name << endl;
}
MyClass ( const MyClass &objectBeingCopied ) {
cout << "\nMyClass copy constructor" << endl;
}
MyClass& operator = ( const MyClass &objectBeingAssigned ) {
cout << "\nMyClass assignment operator" << endl;
}
void sayHello( ) {
cout << "Hello from MyClass " << name << endl;
}
};
int MyClass::count = 0;
int main ( ) {
auto_ptr<MyClass> ptr1( new MyClass() );
auto_ptr<MyClass> ptr2( new MyClass() );
return 0;
}
前面程序的编译输出如下:
g++ main.cpp -std=c++ 17
main.cpp: In function ‘int main()’:
main.cpp:40:2: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]
auto_ptr<MyClass> ptr1( new MyClass() );
In file included from /usr/include/c++/6/memory:81:0,
from main.cpp:3:
/usr/include/c++/6/bits/unique_ptr.h:49:28: note: declared here
template<typename> class auto_ptr;
main.cpp:41:2: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]
auto_ptr<MyClass> ptr2( new MyClass() );
In file included from /usr/include/c++/6/memory:81:0,
from main.cpp:3:
/usr/include/c++/6/bits/unique_ptr.h:49:28: note: declared here
template<typename> class auto_ptr;
如您所见,C++ 编译器警告我们不推荐使用auto_ptr
。因此,我不再推荐使用auto_ptr
智能指针;它被unique_ptr
取代。
现在,我们可以忽略警告,继续前进,如下所示:
g++ main.cpp -Wno-deprecated
./a.out
MyClass Default constructor - Object1
MyClass Default constructor - Object2
MyClass destructor - Object2
MyClass destructor - Object1
正如您在前面的程序输出中看到的,堆中分配的Object1
和Object2
都被自动删除了。这要归功于auto_ptr
智能指针。
您可能已经从MyClass
定义中了解到,它定义了默认的constructor
、copy
构造函数和析构函数、assignment
运算符和sayHello()
方法,如下所示:
//Definitions removed here to keep it simple
class MyClass {
public:
MyClass() { } //Default constructor
~MyClass() { } //Destructor
MyClass ( const MyClass &objectBeingCopied ) {} //Copy Constructor
MyClass& operator = ( const MyClass &objectBeingAssigned ) { } //Assignment operator
void sayHello();
};
MyClass
的方法只不过是一个打印语句,表示方法被调用了;它们纯粹是为了演示的目的。
main()
函数创建两个auto_ptr
智能指针,指向两个不同的MyClass
对象,如下所示:
int main ( ) {
auto_ptr<MyClass> ptr1( new MyClass() );
auto_ptr<MyClass> ptr2( new MyClass() );
return 0;
}
如您所知,auto_ptr
是包装原始指针的本地对象,而不是指针。当控件命中return
语句时,堆栈展开过程开始,作为此过程的一部分,堆栈对象,即ptr1
和ptr2
,被销毁。这又会调用auto_ptr
的析构函数,最终删除堆栈对象ptr1
和ptr2
所指向的MyClass
对象。
我们还没有完全完成。让我们探索一下auto_ptr
更多有用的功能,如下图main
功能所示:
int main ( ) {
auto_ptr<MyClass> ptr1( new MyClass() );
auto_ptr<MyClass> ptr2( new MyClass() );
ptr1->sayHello();
ptr2->sayHello();
//At this point the below stuffs happen
//1\. ptr2 smart pointer has given up ownership of MyClass Object 2
//2\. MyClass Object 2 will be destructed as ptr2 has given up its
// ownership on Object 2
//3\. Ownership of Object 1 will be transferred to ptr2
ptr2 = ptr1;
//The line below if uncommented will result in core dump as ptr1
//has given up its ownership on Object 1 and the ownership of
//Object 1 is transferred to ptr2.
// ptr1->sayHello();
ptr2->sayHello();
return 0;
}
我们刚刚看到的main()
函数代码展示了auto_ptr
智能指针的许多有用的技术和一些有争议的行为。下面的代码创建了两个auto_ptr
实例,即ptr1
和ptr2
,它们将两个创建的MyClass
对象包装在一个堆中:
auto_ptr<MyClass> ptr1( new MyClass() );
auto_ptr<MyClass> ptr2( new MyClass() );
接下来,下面的代码演示了如何使用auto_ptr
调用MyClass
支持的方法:
ptr1->sayHello();
ptr2->sayHello();
希望你遵守ptr1->sayHello()
声明。它会让你相信auto_ptr
ptr1
对象是指针,但实际上,ptr1
和ptr2
只是作为局部变量在栈中创建的auto_ptr
对象。由于auto_ptr
类重载了->
指针操作符和*
解引用操作符,它看起来像一个指针。事实上,MyClass
暴露的所有方法只能使用->
指针操作符访问,而所有auto_ptr
方法都可以像您经常访问堆栈对象一样访问。
下面的代码演示了auto_ptr
智能指针的内部行为,请密切关注;这将会非常有趣:
ptr2 = ptr1;
看起来前面的代码是一个简单的assignment
语句,但是它触发了auto_ptr
中的许多活动。由于前述assignment
声明,发生了以下活动:
ptr2
智能指针将放弃MyClass
对象 2 的所有权。MyClass
对象 2 将被销毁,因为ptr2
已经放弃了对object 2
的所有权。object 1
的所有权将转移给ptr2
。- 此时,
ptr1
既不指向object 1
,也不负责管理object 1
使用的内存。
以下评论行有一些事实要告诉你:
// ptr1->sayHello();
由于ptr1
智能指针已经释放了对object 1
的所有权,试图访问sayHello()
方法是非法的。这是因为ptr1
在现实中不再指向object 1
,而object 1
归ptr2
所有。当ptr2
超出范围时,ptr2
智能指针负责释放object 1
使用的内存。如果前面的代码没有注释,将导致核心转储。
最后,下面的代码让我们使用ptr2
智能指针调用object 1
上的sayHello()
方法:
ptr2->sayHello();
return 0;
我们刚才看到的return
语句将在main()
功能中启动堆栈展开过程。这将最终调用ptr2
的析构函数,反过来将释放object 1
使用的内存。美在于这一切都是自动发生的。当我们专注于手头的问题时,auto_ptr
智能指针在幕后为我们努力工作。
但是,由于以下原因,C++ 11
以后不推荐使用auto_ptr
:
auto_ptr
对象不能存储在 STL 容器中auto_ptr
复制构造函数将从原始源中移除所有权,即auto_ptr
auto_ptr
副本assignment
操作者将从原始来源,即auto_ptr
中移除所有权auto_ptr
违背了复制构造函数和assignment
运算符的初衷,因为auto_ptr
复制构造函数和assignment
运算符将从右侧对象中移除源对象的所有权,并将所有权分配给左侧对象
unique_ptr
智能指针的工作方式与auto_ptr
完全相同,只是unique_ptr
解决了auto_ptr
引入的问题。因此,unique_ptr
是auto_ptr
的替代品,从C++ 11
开始。unique_ptr
智能指针只允许一个智能指针独占一个堆分配的对象。从一个unique_ptr
实例到另一个实例的所有权转移只能通过std::move()
功能完成。
因此,让我们重构前面的例子,用unique_ptr
代替auto_ptr
。
重构的代码示例如下:
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
using namespace std;
class MyClass {
private:
static int count;
string name;
public:
MyClass() {
ostringstream stringStream(ostringstream::ate);
stringStream << "Object";
stringStream << ++ count;
name = stringStream.str();
cout << "\nMyClass Default constructor - " << name << endl;
}
~MyClass() {
cout << "\nMyClass destructor - " << name << endl;
}
MyClass ( const MyClass &objectBeingCopied ) {
cout << "\nMyClass copy constructor" << endl;
}
MyClass& operator = ( const MyClass &objectBeingAssigned ) {
cout << "\nMyClass assignment operator" << endl;
}
void sayHello( ) {
cout << "\nHello from MyClass" << endl;
}
};
int MyClass::count = 0;
int main ( ) {
unique_ptr<MyClass> ptr1( new MyClass() );
unique_ptr<MyClass> ptr2( new MyClass() );
ptr1->sayHello();
ptr2->sayHello();
//At this point the below stuffs happen
//1\. ptr2 smart pointer has given up ownership of MyClass Object 2
//2\. MyClass Object 2 will be destructed as ptr2 has given up its
// ownership on Object 2
//3\. Ownership of Object 1 will be transferred to ptr2
ptr2 = move( ptr1 );
//The line below if uncommented will result in core dump as ptr1
//has given up its ownership on Object 1 and the ownership of
//Object 1 is transferred to ptr2.
// ptr1->sayHello();
ptr2->sayHello();
return 0;
}
前面程序的输出如下:
g++ main.cpp -std=c++ 17
./a.out
MyClass Default constructor - Object1
MyClass Default constructor - Object2
MyClass destructor - Object2
MyClass destructor - Object1
在前面的输出中,您可以注意到编译器没有报告任何警告,程序的输出与auto_ptr
相同。
注意auto_ptr
和unique_ptr
之间main()
功能的差异很重要。让我们来看看main()
功能,如下面的代码所示。这段代码创建了两个unique_ptr
实例,即ptr1
和ptr2
,它们包装了堆中创建的两个MyClass
对象:
unique_ptr<MyClass> ptr1( new MyClass() );
unique_ptr<MyClass> ptr2( new MyClass() );
接下来,下面的代码演示了如何使用unique_ptr
调用MyClass
支持的方法:
ptr1->sayHello();
ptr2->sayHello();
就像auto_ptr
一样,unique_ptr
智能指针ptr1
对象重载了->
指针操作符和*
解引用操作符;因此,它看起来像一个指针。
下面的代码演示了unique_ptr
不支持将一个unique_ptr
实例分配给另一个实例,所有权转移只能通过std::move()
功能实现:
ptr2 = std::move(ptr1);
move
功能触发以下活动:
ptr2
智能指针放弃MyClass
对象 2 的所有权MyClass
对象 2 被破坏,因为ptr2
放弃了对object 2
的所有权object 1
的所有权转移至ptr2
- 此时,
ptr1
既不指向object 1
,也不负责管理object 1
使用的内存
以下代码如果未注释,将导致核心转储:
// ptr1->sayHello();
最后,下面的代码让我们使用ptr2
智能指针调用object 1
上的sayHello()
方法:
ptr2->sayHello();
return 0;
我们刚才看到的return
语句将在main()
功能中启动堆栈展开过程。这将最终调用ptr2
的析构函数,反过来将释放object 1
使用的内存。注意unique_ptr
对象可以存储在 STL 容器中,不像auto_ptr
对象。
当一组shared_ptr
对象共享堆分配对象的所有权时,使用shared_ptr
智能指针。当使用共享对象完成所有shared_ptr
实例时,shared_ptr
指针释放共享对象。shared_ptr
指针使用引用计数机制来检查对共享对象的总引用;每当引用计数变为零时,最后一个shared_ptr
实例将删除共享对象。
我们通过一个例子来看看shared_ptr
的用法,如下:
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
using namespace std;
class MyClass {
private:
static int count;
string name;
public:
MyClass() {
ostringstream stringStream(ostringstream::ate);
stringStream << "Object";
stringStream << ++ count;
name = stringStream.str();
cout << "\nMyClass Default constructor - " << name << endl;
}
~MyClass() {
cout << "\nMyClass destructor - " << name << endl;
}
MyClass ( const MyClass &objectBeingCopied ) {
cout << "\nMyClass copy constructor" << endl;
}
MyClass& operator = ( const MyClass &objectBeingAssigned ) {
cout << "\nMyClass assignment operator" << endl;
}
void sayHello() {
cout << "Hello from MyClass " << name << endl;
}
};
int MyClass::count = 0;
int main ( ) {
shared_ptr<MyClass> ptr1( new MyClass() );
ptr1->sayHello();
cout << "\nUse count is " << ptr1.use_count() << endl;
{
shared_ptr<MyClass> ptr2( ptr1 );
ptr2->sayHello();
cout << "\nUse count is " << ptr2.use_count() << endl;
}
shared_ptr<MyClass> ptr3 = ptr1;
ptr3->sayHello();
cout << "\nUse count is " << ptr3.use_count() << endl;
return 0;
}
前面程序的输出如下:
MyClass Default constructor - Object1
Hello from MyClass Object1
Use count is 1
Hello from MyClass Object1
Use count is 2
Number of smart pointers referring to MyClass object after ptr2 is destroyed is 1
Hello from MyClass Object1
Use count is 2
MyClass destructor - Object1
下面的代码创建了一个指向MyClass
堆分配对象的shared_ptr
对象的实例。就像其他智能指针一样,shared_ptr
也有过载的->
和*
操作符。因此,所有MyClass
对象方法都可以像使用原始指针一样被调用。use_count()
方法告诉引用共享对象的智能指针的数量:
shared_ptr<MyClass> ptr1( new MyClass() );
ptr1->sayHello();
cout << "\nNumber of smart pointers referring to MyClass object is "
<< ptr1->use_count() << endl;
在下面的代码中,智能指针ptr2
的范围被包装在花括号包围的块中。因此,ptr2
将在下面的代码块末尾被销毁。代码块中预期的use_count
函数是 2:
{
shared_ptr<MyClass> ptr2( ptr1 );
ptr2->sayHello();
cout << "\nNumber of smart pointers referring to MyClass object is "
<< ptr2->use_count() << endl;
}
在下面的代码中,预期的use_count
值为 1,因为ptr2
将被删除,这将使参考计数减少 1:
cout << "\nNumber of smart pointers referring to MyClass object after ptr2 is destroyed is "
<< ptr1->use_count() << endl;
下面的代码将打印一条 Hello 消息,后面跟着use_count
作为 2。这是因为ptr1
和ptr3
现在引用堆中的MyClass
共享对象:
shared_ptr<MyClass> ptr3 = ptr2;
ptr3->sayHello();
cout << "\nNumber of smart pointers referring to MyClass object is "
<< ptr2->use_count() << endl;
main
功能结束时的return 0;
语句将破坏ptr1
和ptr3
,将参考计数减少到零。因此,我们可以观察到MyClass
析构函数在输出的末尾打印语句。
到目前为止,我们已经用例子讨论了shared_ptr
的积极一面。然而,当应用设计中存在循环依赖时,shared_ptr
无法清理内存。要么必须重构应用设计以避免循环依赖,要么我们可以利用weak_ptr
来解决循环依赖问题。
You can check out my YouTube channel to understand the shared_ptr
issue and how it can be resolved with weak_ptr
: https://www.youtube.com/watch?v=SVTLTK5gbDc.
假设有三个类:A、B 和 C,类 A 和 B 有一个 C 的实例,而 C 有一个 A 和 B 的实例,这里有一个设计问题。甲依赖丙,丙也依赖甲。同样,乙依赖丙,丙也依赖乙。
考虑以下代码:
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
using namespace std;
class C;
class A {
private:
shared_ptr<C> ptr;
public:
A() {
cout << "\nA constructor" << endl;
}
~A() {
cout << "\nA destructor" << endl;
}
void setObject ( shared_ptr<C> ptr ) {
this->ptr = ptr;
}
};
class B {
private:
shared_ptr<C> ptr;
public:
B() {
cout << "\nB constructor" << endl;
}
~B() {
cout << "\nB destructor" << endl;
}
void setObject ( shared_ptr<C> ptr ) {
this->ptr = ptr;
}
};
class C {
private:
shared_ptr<A> ptr1;
shared_ptr<B> ptr2;
public:
C(shared_ptr<A> ptr1, shared_ptr<B> ptr2) {
cout << "\nC constructor" << endl;
this->ptr1 = ptr1;
this->ptr2 = ptr2;
}
~C() {
cout << "\nC destructor" << endl;
}
};
int main ( ) {
shared_ptr<A> a( new A() );
shared_ptr<B> b( new B() );
shared_ptr<C> c( new C( a, b ) );
a->setObject ( shared_ptr<C>( c ) );
b->setObject ( shared_ptr<C>( c ) );
return 0;
}
前面程序的输出如下:
g++ problem.cpp -std=c++ 17
./a.out
A constructor
B constructor
C constructor
在前面的输出中,您可以观察到,即使我们使用了shared_ptr
,对象 A、B 和 C 使用的内存也从未被释放。这是因为我们没有看到各个类的析构函数被调用。其原因是shared_ptr
内部利用引用计数算法来决定共享对象是否需要析构。但是,它在这里失败了,因为除非删除对象 C,否则不能删除对象 A。除非删除对象 A,否则不能删除对象 C。另外,除非删除对象 A 和对象 B,否则不能删除对象 C。同样,除非删除对象 C,否则不能删除对象 A,除非删除对象 C,否则不能删除对象 B。
底线是这是一个循环依赖设计问题。为了解决这个问题,从 C++ 11 开始,C++ 引入了weak_ptr
。weak_ptr
智能指针不是强引用。因此,与shared_ptr
不同的是,所引用的对象可以在任何时候被删除。
循环依赖是对象 A 依赖于 B,对象 B 依赖于 A 时出现的问题,现在让我们看看如何通过shared_ptr
和weak_ptr
的组合来解决这个问题,最终打破循环依赖,如下所示:
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
using namespace std;
class C;
class A {
private:
weak_ptr<C> ptr;
public:
A() {
cout << "\nA constructor" << endl;
}
~A() {
cout << "\nA destructor" << endl;
}
void setObject ( weak_ptr<C> ptr ) {
this->ptr = ptr;
}
};
class B {
private:
weak_ptr<C> ptr;
public:
B() {
cout << "\nB constructor" << endl;
}
~B() {
cout << "\nB destructor" << endl;
}
void setObject ( weak_ptr<C> ptr ) {
this->ptr = ptr;
}
};
class C {
private:
shared_ptr<A> ptr1;
shared_ptr<B> ptr2;
public:
C(shared_ptr<A> ptr1, shared_ptr<B> ptr2) {
cout << "\nC constructor" << endl;
this->ptr1 = ptr1;
this->ptr2 = ptr2;
}
~C() {
cout << "\nC destructor" << endl;
}
};
int main ( ) {
shared_ptr<A> a( new A() );
shared_ptr<B> b( new B() );
shared_ptr<C> c( new C( a, b ) );
a->setObject ( weak_ptr<C>( c ) );
b->setObject ( weak_ptr<C>( c ) );
return 0;
}
前面重构代码的输出如下:
g++ solution.cpp -std=c++ 17
./a.out
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor
在本章中,您了解了
- 由于原始指针引起的内存泄漏问题
auto_ptr
关于赋值和复制构造函数的问题unique_ptr
它的优势shared_ptr
在内存管理中的作用及其与循环依赖相关的局限性。- 您还可以通过
weak_ptr
解决循环依赖问题
在下一章中,您将学习如何用 C++ 开发图形用户界面应用。