Skip to content

Latest commit

 

History

History
846 lines (604 loc) · 30 KB

File metadata and controls

846 lines (604 loc) · 30 KB

四、智能指针

在前一章中,您学习了模板编程和泛型编程的好处。在本章中,您将了解以下智能指针主题:

  • 内存管理
  • 原始指针的问题
  • 循环依赖
  • 智能指针:
    • 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_ptrshared_ptrweak_ptrunique-ptr

auto_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 

正如您在前面的程序输出中看到的,堆中分配的Object1Object2都被自动删除了。这要归功于auto_ptr智能指针。

代码演练-第 1 部分

您可能已经从MyClass定义中了解到,它定义了默认的constructorcopy构造函数和析构函数、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语句时,堆栈展开过程开始,作为此过程的一部分,堆栈对象,即ptr1ptr2,被销毁。这又会调用auto_ptr的析构函数,最终删除堆栈对象ptr1ptr2所指向的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;

}

代码演练-第 2 部分

我们刚刚看到的main()函数代码展示了auto_ptr智能指针的许多有用的技术和一些有争议的行为。下面的代码创建了两个auto_ptr实例,即ptr1ptr2,它们将两个创建的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对象是指针,但实际上,ptr1ptr2只是作为局部变量在栈中创建的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 1ptr2所有。当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运算符将从右侧对象中移除源对象的所有权,并将所有权分配给左侧对象

S7-1200 可编程控制器

unique_ptr智能指针的工作方式与auto_ptr完全相同,只是unique_ptr解决了auto_ptr引入的问题。因此,unique_ptrauto_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_ptrunique_ptr之间main()功能的差异很重要。让我们来看看main()功能,如下面的代码所示。这段代码创建了两个unique_ptr实例,即ptr1ptr2,它们包装了堆中创建的两个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对象。

共享 _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。这是因为ptr1ptr3现在引用堆中的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;语句将破坏ptr1ptr3,将参考计数减少到零。因此,我们可以观察到MyClass析构函数在输出的末尾打印语句。

弱 _ptr

到目前为止,我们已经用例子讨论了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_ptrweak_ptr智能指针不是强引用。因此,与shared_ptr不同的是,所引用的对象可以在任何时候被删除。

循环依赖

循环依赖是对象 A 依赖于 B,对象 B 依赖于 A 时出现的问题,现在让我们看看如何通过shared_ptrweak_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++ 开发图形用户界面应用。