0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C++面向对象编程中的深拷贝和浅拷贝

jf_78858299 来源:小余的自习室 作者:小余的自习室 2023-03-30 12:53 次阅读

前言

最近在写代码的过程中,发现一个大家容易忽略的知识点: 深拷贝和浅拷贝

可能对于Java程序员来说,很少遇到深浅拷贝问题,但是对于C++程序员来说可谓是又爱又恨。。

浅拷贝:

  • 1.将原对象或者原对象的引用直接赋值给新对象,新对象,新数组只是原对象的一个引用。

  • 2.C++默认的拷贝构造函数与赋值运算符重载都是浅拷贝,可以节省一定空间,但是可能会引发同一块内存重复释放问题,

    二次释放内存可能导致严重的异常崩溃等情况。

  • 浅拷贝模型:

    图片

深拷贝:

  • 1.创建一个新的对象或者数组,将对象或者数组的属性值拷贝过来,注意此时新对象指向的不是原对象的引用而是原对象的值,新对象在堆中有自己的地址空间。

  • 2.浪费空间,但是不会引发浅拷贝中的资源重复释放问题。

  • 深拷贝模型

    图片

案例分析

下面使用一个案例来看下一个因为浅拷贝带来的bug。

#include "DeepCopy.h"
#include 
#include 
using namespace std;
class Human {
public:
    Human(int age):_age(age) {


    }
    int _age;;

};
class String {
public:
    String(Human* pHuman){
        this->pHuman = pHuman;
    }
    ~String() {
        delete pHuman;
    }
    Human* pHuman;
};

void DeepCopy::main() 
{    
    Human* p = new Human(100);
    String s1(p);
    String s2(s1);

}

这个程序从表面看是不会有啥问题的,运行后,出现如下错误:

图片

先说下原因

这个错误就是由于代码 String s2(s1) 会调用String的默认拷贝构造函数,而 默认的拷贝构造函数使用的是浅拷贝,即仅仅只是对新的指针对象pHuman指向原指针对象pHuman指向的地址

在退出main函数作用域后,会回调s1和s2的析构函数,当回调s2析构函数后,s2中的pHuman内存资源被释放,此时再回调s1,也会回调s1中的pHuman析构函数,可是此时的pHuman指向的地址

已经在s2中被释放了,造成了二次释放内存,出现了崩溃的情况

所以为了防止出现二次释放内存的情况,需要使用深拷贝

深拷贝需要重写拷贝构造函数以及赋值运算符重载,且在拷贝构造内部重新去new一个对象资源.

代码如下:

#include "DeepCopy.h"
#include 
#include 
using namespace std;
class Human {
public:
    Human(int age):_age(age) {

}

int _age;;

};
class String {
public:
    String(Human* pHuman){
        this->pHuman = pHuman;
    }
    //重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃
    String(const String& str) {
        pHuman = new Human(str.pHuman->_age);
    }
    ~String() {
        delete pHuman;
    }
    Human* pHuman;
};

void DeepCopy::main() 
{    
    Human* p = new Human(100);
    String s1(p);
    String s2(s1);

}

默认情况下使用:

String s2(s1)或者String s2 = s1 这两种方式去赋值,就会调用String的拷贝构造方法,如果没有实现,则会执行默认的拷贝构造,即浅拷贝。

可以在拷贝构造函数中使用new重新对指针进行资源分配,达到深拷贝的要求、

说了这么多只要记住一点: 如果类中有成员变量是指针的情况下,就需要自己去实现深拷贝

虽然深拷贝可以帮助我们防止出现二次内存是否的问题,但是其会浪费一定空间,如果对象中资源较大,拿每个对象都包含一个大对象,这不是一个很好的设计,而浅拷贝就没这个问题。

那么有什么方法可以兼容他们的优点么? 即不浪费空间也不会引起二次内存释放

兼容优化方案:

  • 1.引用计数方式
  • 2.使用move语义转移

引用计数

我们对资源增加一个引用计数,在构造函数以及拷贝构造函数中让计数+1,在析构中让计数-1.当计数为0时,才会去释放资源,这是一个不错的注意。

如图所示:

图片

对应代码如下:

#include "DeepCopy.h"
#include 
#include 
using namespace std;
class Human {
public:
    Human(int age):_age(age) {

    }
    int _age;;
};
class String {
public:
    String() {
        addRefCount();
    }
    String(Human* pHuman){
        this->pHuman = pHuman;
        addRefCount();
    }
    //重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃
    String(const String& str) {
        ////深拷贝
        //pHuman = new Human(str.pHuman->_age);
        //浅拷贝
        pHuman = str.pHuman;
        addRefCount();
    }
    ~String() {
        subRefCount();
        if (getRefCount() <= 0) {
            delete pHuman;
        }   
    }
    Human* pHuman;
private:
    void addRefCount() {
        refCount++;
    }
    void subRefCount() {
        refCount--;
    }
    int getRefCount() {
        return refCount;
    }
    static int refCount;
};
int String::refCount = 0;
void DeepCopy::main() 
{    
    Human* p = new Human(100);
    String s1(p);
    String s2 = s1;

}

此时的拷贝构造函数使用了浅拷贝对成员对象进行赋值,且 只有在引用计数为0的情况下才会进行资源释放

但是引用计数的方式会出现循环引用的情况,导致内存无法释放,发生 内存泄露

循环引用模型如下:

图片

我们知道在C++的 智能指针shared_ptr中就使用了引用计数

类似java中对象垃圾的定位方法,如果有一个指针引用某块内存,则引用计数+1,释放计数-1.如果引用计数为0,则说明这块内存可以释放了。

下面我们写个shared_ptr循环引用的情况:

class A {
  public:
    shared_ptr pa;

**
~A() {

cout << "~A" << endl;

}

};
class B {
public:
    shared_ptr pb;


~B() {
cout << "~B" << endl;
}

};
void sharedPtr() {
    shared_ptr a(new A());
    shared_ptr b(new B());
    cout << "第一次引用:" << endl;
    cout <<"计数a:" << a.use_count() << endl;
    cout << "计数b:" << b.use_count() << endl;
    a->pa = b;
    b->pb = a;
    cout << "第二次引用:" << endl;
    cout << "计数a:" << a.use_count() << endl;
    cout << "计数b:" << b.use_count() << endl;
}
运行结果:
第一次引用:
计数a:1
计数b:1
第二次引用:
计数a:2
计数b:2

[**
可以看到运行结果并没有打印出对应的析构函数,也就是没被释放。

指针a和指针b是栈上的,当退出他们的作用域后,引用计数会-1,但是其计数器数是2,所以还不为0,也就是不能被释放。你不释放我,我也不释放你,咱两耗着呗。

这就是标志性的由于循环引用计数导致的内存泄露.。所以 我们在设计深浅拷贝代码的时候千万别写出循环引用的情况

move语义转移

在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。

而现在在某些情况下,我们没有必要复制对象,只需要移动它们。

C++11引入移动语义

源对象资源的控制权全部交给目标对象。注意这里说的是控制权,即使用一个新的指针对象去指向这个对象,然后将原对象的指针置为nullptr

模型如下:

图片

要实现move语义,需要实现移动构造函数

代码如下:

//移动语义move
class Human {
public:
    Human(int age) :_age(age) {

}

int _age;;

};
class String {
public:

String(Human* pHuman) {

this->pHuman = pHuman;

}

//重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃

String(const String& str) {

////深拷贝

//pHuman = new Human(str.pHuman->_age);

//浅拷贝

pHuman = str.pHuman;

}

//移动构造函数

String(String&& str) {

pHuman = str.pHuman;

str.pHuman = NULL;

}

~String() {

if (pHuman != NULL) {

delete pHuman;

}

}

Human* pHuman;

};
void DeepCopy::main()
{
    Human* p = new Human(100);
    String s1(p);

String s2(std::move(s1));

String s3(std::move(s2));

}

该案例中, 指针p的权限会由s1让渡给s2,s2再让渡给s3 .

使用move语义转移在C++中使用还是比较频繁的,因为其可以大大缩小因为对象对象的创建导致内存吃紧的情况。比较推荐应用中使用这种方式来优化内存方面问题.

总结

本篇文章主要讲解了C++面向对象编程中的深拷贝和浅拷贝的问题,以及使用引用计数和move语义转移的方式来优化深浅拷贝的问题。

C++不像Java那样,JVM都给我们处理好了资源释放的问题,没有二次释放导致的崩溃情况, C++要懂的东西远非Java可比,这也是为什么C++程序员那么少的原因之一吧

]()

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2954

    浏览量

    104511
  • C++
    C++
    +关注

    关注

    21

    文章

    2104

    浏览量

    73469
  • 面向对象编程

    关注

    0

    文章

    22

    浏览量

    1804
收藏 人收藏

    评论

    相关推荐

    基于C/C++面向对象的方式封装socket通信类

    在掌握了基于 TCP 的套接字通信流程之后,为了方便使用,提高编码效率,可以对通信操作进行封装,本着有的原则,先基于 C 语言进行面向过程的函数封装,然后再基于
    的头像 发表于 12-26 09:57 1273次阅读

    C++零基础教程之C++拷贝拷贝,轻松上手C++拷贝拷贝

    编程语言C++语言
    电子学习
    发布于 :2023年01月14日 11:37:32

    拷贝拷贝的实现方法概述

    拷贝拷贝的实现
    发表于 07-19 13:35

    python深浅拷贝是什么?

    python的直接赋值python的拷贝python的拷贝
    发表于 11-04 08:33

    请问哪位大神可以详细介绍JavaScript拷贝拷贝

    JavaScript数据类型JavaScript拷贝拷贝
    发表于 11-05 07:16

    C++ 面向对象多线程编程下载

    C++ 面向对象多线程编程下载
    发表于 04-08 02:14 70次下载

    面向对象的程序设计(C++

    面向对象的程序设计(C++).面向对象的基本思想 C++
    发表于 03-22 14:40 0次下载

    C#拷贝拷贝区别解析

     所谓拷贝就是将对象的所有字段复制到新的副本对象
    发表于 11-29 08:32 2.6w次阅读
    <b class='flag-5'>C</b>#<b class='flag-5'>浅</b><b class='flag-5'>拷贝</b>与<b class='flag-5'>深</b><b class='flag-5'>拷贝</b>区别解析

    Python如何防止数据被修改Python拷贝拷贝的问题说明

    在平时工作,经常涉及到数据的传递。在数据传递使用过程,可能会发生数据被修改的问题。为了防止数据被修改,就需要再传递一个副本,即使副本被修改,也不会影响原数据的使用。为了生成这个副本,就产生了拷贝——今天就说一下Python
    的头像 发表于 03-30 09:54 3029次阅读
    Python如何防止数据被修改Python<b class='flag-5'>中</b>的<b class='flag-5'>深</b><b class='flag-5'>拷贝</b>与<b class='flag-5'>浅</b><b class='flag-5'>拷贝</b>的问题说明

    C++:详谈拷贝构造函数

    只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类
    的头像 发表于 06-29 11:45 2105次阅读
    <b class='flag-5'>C++</b>:详谈<b class='flag-5'>拷贝</b>构造函数

    C++拷贝构造函数的copy及copy

    C++编译器会默认提供构造函数;无参构造函数用于定义对象的默认初始化状态;拷贝构造函数在创建对象拷贝
    的头像 发表于 12-24 15:31 704次阅读

    C语言是怎么面向对象编程

    在嵌入式开发C/C++语言是使用最普及的,在C++11版本之前,它们的语法是比较相似的,只不过C++提供了
    的头像 发表于 02-14 13:57 1621次阅读
    <b class='flag-5'>C</b>语言是怎么<b class='flag-5'>面向</b><b class='flag-5'>对象</b><b class='flag-5'>编程</b>

    C/C++面向对象编程思想3

    C++作为一门在C和Java之间的语言,其既可以使用C语言中的高效指针,又继承了Java面向对象
    的头像 发表于 03-30 15:16 537次阅读
    <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>之<b class='flag-5'>面向</b><b class='flag-5'>对象</b><b class='flag-5'>编程</b>思想3

    C++拷贝拷贝详解

    当类的函数成员存在指针成员时会产生拷贝拷贝和问题。
    发表于 08-21 15:05 319次阅读
    <b class='flag-5'>C++</b><b class='flag-5'>深</b><b class='flag-5'>拷贝</b>和<b class='flag-5'>浅</b><b class='flag-5'>拷贝</b>详解

    Python拷贝拷贝的操作

    【例子】拷贝拷贝 list1 = [ 123 , 456 , 789 , 213 ]list2 = list1list3 = lis
    的头像 发表于 11-02 10:58 375次阅读