C++浅拷贝与深拷贝及引用计数分析

所属分类: 软件编程 / C 语言 阅读数: 87
收藏 0 赞 0 分享

C++浅拷贝与深拷贝及引用计数分析

在C++开发中,经常遇到的一个问题就是与指针相关的内存管理问题,稍有不慎,就会造成内存泄露、内存破坏等严重的问题。不像Java一样,没有指针这个概念,所以也就不必担心与指针相关的一系列问题,但C++不同,从C语言沿袭下来的指针是其一大特点,我们常常要使用new/delete来动态管理内存,那么问题来了,特别是伴随着C++的继承机制,如野指针、无效指针使用、内存泄露、double free、堆碎片等等,这些问题就像地雷一样,一不小心就会踩那么几颗。

先来谈一下C++类中常见的浅拷贝问题,以及由此引发的double free。什么是浅拷贝?当类中的成员变量包括指针时,而又没有定义自己的拷贝构造函数,那么在拷贝一个对象的情况下,就会调用其默认拷贝构造函数,其实这个函数没做什么事,只是对其成员变量作了个简单的拷贝,也就是所谓的位拷贝,它们指向的还是同一个存储空间,当对象析构时,就会析构多次,也就是double free,下面举例说明。

class Common
{
public:
  Common()
  {
    std::cout << "Common::Common" << std::endl;
  }

  Common(const Common &r)
  {
    std::cout << "Common::Common copy-constructor" << std::endl;
  }

  ~Common()
  {
    std::cout << "Common::~Common" << std::endl;
  }
};

类Common是个一般的类,定义了构造、拷贝构造和析构函数,在函数里输出一些log,用以跟踪函数调用情况。

class BitCopy
{
public:
  BitCopy()
    : m_p(new Common)
  {
    std::cout << "BitCopy::BitCopy" << std::endl;
  }

  ~BitCopy()
  {
    std::cout << "BitCopy::~BitCopy" << std::endl;
    if (m_p) {
      delete m_p;
      m_p = NULL;
    }
  }

private:
  Common *m_p;
};

类BitCopy就是一个浅拷贝类,成员变量是我们刚定义的类指针,构造函数实例化成员变量,析构函数delete成员变量,没有定义拷贝构造函数。

int main()
{
  BitCopy a;
  BitCopy b(a);
  return 0;
}
log如下:
Common::Common
BitCopy::BitCopy
BitCopy::~BitCopy
Common::~Common
BitCopy::~BitCopy
Common::~Common
*** Error in `./a.out': double free or corruption (fasttop): 0x0000000001f4e010 ***
已放弃 (核心已转储)

从上面的log可以看出,对象a调用了构造函数,对象b调用的是默认拷贝构造函数,最后析构了两次,从而造成double free,核心已转储即core dump。

针对以上问题,该怎么解决呢?有两个办法,一个是深拷贝,一个是引用计数。先来看一下深拷贝,深拷贝要定义自己的拷贝构造函数,在函数中给成员变量重新分配存储空间,也就是所谓的值拷贝,这样它们所指向的就是不同的存储空间,析构时不会有问题,但这种方法只适用于较小的数据结构,如果数据结构过大,多次分配存储空间之后,剩余的存储空间将逐渐减小,

下面看个例子。

class ValueCopy
{
public:
  ValueCopy()
    : m_p(new Common)
  {
    std::cout << "ValueCopy::ValueCopy" << std::endl;
  }

  ValueCopy(const ValueCopy &r)
    : m_p(new Common(*r.m_p))
  {
    std::cout << "ValueCopy::ValueCopy copy-constructor" << std::endl;
  }

  ~ValueCopy()
  {
    std::cout << "ValueCopy::~ValueCopy" << std::endl;
    if (m_p) {
      delete m_p;
      m_p = NULL;
    }
  }

private:
  Common *m_p;
};

类ValueCopy是个深拷贝类,与上面例子的浅拷贝类不同的是定义了拷贝构造函数,在函数中给成员变量重新分配存储空间,下面是用法及log。

int main()
{
  ValueCopy c;
  ValueCopy d(c);
  return 0;
}
Common::Common
ValueCopy::ValueCopy
Common::Common copy-constructor
ValueCopy::ValueCopy copy-constructor
ValueCopy::~ValueCopy
Common::~Common
ValueCopy::~ValueCopy
Common::~Common

从上面的log可以看出,对象c调用了构造函数,对象d调用的是自定义拷贝构造函数,最后析构了两次而没有问题,可见深拷贝的用处所在。

引用计数与深拷贝不同,方法是共享同一块存储空间,这个对大的数据结构比较有利。使用引用计数,需要在类中定义一个成员变量专门用于计数,初始值为1,后面引用了这个对象就加1,对象销毁时引用减1,但并不真正的delete这个对象,只有当这个成员变量的值为0时才进行delete,例子如下。

class A
{
public:
  A()
    : m_refCount(1)
  {
    std::cout << "A::A" << std::endl;
  }

  A(const A &r)
    : m_refCount(1)
  {
    std::cout << "A::A copy-constructor" << std::endl;
  }

  ~A()
  {
    std::cout << "A::~A" << std::endl;
  }

  void attach()
  {
    std::cout << "A::attach" << std::endl;
    ++m_refCount;
  }

  void detach()
  {
    if (m_refCount != 0) {
      std::cout << "A::detach " << m_refCount << std::endl;
      if (--m_refCount == 0) {
        delete this;
      }
    }
  }

private:
  int m_refCount;
};

class B
{
public:
  B()
    : m_pA(new A)
  {
    std::cout << "B::B" << std::endl;
  }

  B(const B &r)
    : m_pA(r.m_pA)
  {
    std::cout << "B::B copy-constructor" << std::endl;
    m_pA->attach();
  }

  ~B()
  {
    std::cout << "B::~B" << std::endl;
    m_pA->detach();
  }

private:
  A* m_pA;
};

类A用到了引用计数,构造和拷贝构造函数都初始化为1,attach()函数为引用加1,detach()函数为引用减1,当引用计数值为0时delete对象。类B中的成员变量有个指针指向A,拷贝构造函数中调用了attach(),析构函数中调用了detach(),这样也是一种保护,不会有内存泄露,也不会有double free,log如下。

int main()
{
  B e;
  B f(e);
  return 0;
}
A::A
B::B
B::B copy-constructor
A::attach
B::~B
A::detach 2
B::~B
A::detach 1
A::~A

从log中可以看出,指针成员变量的引用计数为2,这是正确的,最后正确delete,没有问题。

在类中只要有指针成员变量,就要注意以上问题,另外,operator=这个赋值操作符也要在适当的时候进行重载。有时候,如果想规避以上问题,可以声明拷贝构造函数和operator=操作符为private而不去实现它们。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多