0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

C++面向?qū)ο缶幊讨械纳羁截惡蜏\拷貝

jf_78858299 ? 來源:小余的自習(xí)室 ? 作者:小余的自習(xí)室 ? 2023-03-30 12:53 ? 次閱讀

前言

最近在寫代碼的過程中,發(fā)現(xiàn)一個(gè)大家容易忽略的知識(shí)點(diǎn): 深拷貝和淺拷貝

可能對(duì)于Java程序員來說,很少遇到深淺拷貝問題,但是對(duì)于C++程序員來說可謂是又愛又恨。。

淺拷貝:

  • 1.將原對(duì)象或者原對(duì)象的引用直接賦值給新對(duì)象,新對(duì)象,新數(shù)組只是原對(duì)象的一個(gè)引用。

  • 2.C++默認(rèn)的拷貝構(gòu)造函數(shù)與賦值運(yùn)算符重載都是淺拷貝,可以節(jié)省一定空間,但是可能會(huì)引發(fā)同一塊內(nèi)存重復(fù)釋放問題,

    二次釋放內(nèi)存可能導(dǎo)致嚴(yán)重的異常崩潰等情況。

  • 淺拷貝模型:

    圖片

深拷貝:

  • 1.創(chuàng)建一個(gè)新的對(duì)象或者數(shù)組,將對(duì)象或者數(shù)組的屬性值拷貝過來,注意此時(shí)新對(duì)象指向的不是原對(duì)象的引用而是原對(duì)象的值,新對(duì)象在堆中有自己的地址空間。

  • 2.浪費(fèi)空間,但是不會(huì)引發(fā)淺拷貝中的資源重復(fù)釋放問題。

  • 深拷貝模型

    圖片

案例分析

下面使用一個(gè)案例來看下一個(gè)因?yàn)闇\拷貝帶來的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);

}

這個(gè)程序從表面看是不會(huì)有啥問題的,運(yùn)行后,出現(xiàn)如下錯(cuò)誤:

圖片

先說下原因

這個(gè)錯(cuò)誤就是由于代碼 String s2(s1) 會(huì)調(diào)用String的默認(rèn)拷貝構(gòu)造函數(shù),而 默認(rèn)的拷貝構(gòu)造函數(shù)使用的是淺拷貝,即僅僅只是對(duì)新的指針對(duì)象pHuman指向原指針對(duì)象pHuman指向的地址 。

在退出main函數(shù)作用域后,會(huì)回調(diào)s1和s2的析構(gòu)函數(shù),當(dāng)回調(diào)s2析構(gòu)函數(shù)后,s2中的pHuman內(nèi)存資源被釋放,此時(shí)再回調(diào)s1,也會(huì)回調(diào)s1中的pHuman析構(gòu)函數(shù),可是此時(shí)的pHuman指向的地址

已經(jīng)在s2中被釋放了,造成了二次釋放內(nèi)存,出現(xiàn)了崩潰的情況

所以為了防止出現(xiàn)二次釋放內(nèi)存的情況,需要使用深拷貝 。

深拷貝需要重寫拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載,且在拷貝構(gòu)造內(nèi)部重新去new一個(gè)對(duì)象資源.

代碼如下:

#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;
    }
    //重寫拷貝構(gòu)造,實(shí)現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
    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);

}

默認(rèn)情況下使用:

String s2(s1)或者String s2 = s1 這兩種方式去賦值,就會(huì)調(diào)用String的拷貝構(gòu)造方法,如果沒有實(shí)現(xiàn),則會(huì)執(zhí)行默認(rèn)的拷貝構(gòu)造,即淺拷貝。

可以在拷貝構(gòu)造函數(shù)中使用new重新對(duì)指針進(jìn)行資源分配,達(dá)到深拷貝的要求、

說了這么多只要記住一點(diǎn): 如果類中有成員變量是指針的情況下,就需要自己去實(shí)現(xiàn)深拷貝 。

雖然深拷貝可以幫助我們防止出現(xiàn)二次內(nèi)存是否的問題,但是其會(huì)浪費(fèi)一定空間,如果對(duì)象中資源較大,拿每個(gè)對(duì)象都包含一個(gè)大對(duì)象,這不是一個(gè)很好的設(shè)計(jì),而淺拷貝就沒這個(gè)問題。

那么有什么方法可以兼容他們的優(yōu)點(diǎn)么? 即不浪費(fèi)空間也不會(huì)引起二次內(nèi)存釋放 ?

兼容優(yōu)化方案:

  • 1.引用計(jì)數(shù)方式
  • 2.使用move語義轉(zhuǎn)移

引用計(jì)數(shù)

我們對(duì)資源增加一個(gè)引用計(jì)數(shù),在構(gòu)造函數(shù)以及拷貝構(gòu)造函數(shù)中讓計(jì)數(shù)+1,在析構(gòu)中讓計(jì)數(shù)-1.當(dāng)計(jì)數(shù)為0時(shí),才會(huì)去釋放資源,這是一個(gè)不錯(cuò)的注意。

如圖所示:

圖片

對(duì)應(yīng)代碼如下:

#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();
    }
    //重寫拷貝構(gòu)造,實(shí)現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
    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;

}

此時(shí)的拷貝構(gòu)造函數(shù)使用了淺拷貝對(duì)成員對(duì)象進(jìn)行賦值,且 只有在引用計(jì)數(shù)為0的情況下才會(huì)進(jìn)行資源釋放 。

但是引用計(jì)數(shù)的方式會(huì)出現(xiàn)循環(huán)引用的情況,導(dǎo)致內(nèi)存無法釋放,發(fā)生 內(nèi)存泄露 。

循環(huán)引用模型如下:

圖片

我們知道在C++的 智能指針shared_ptr中就使用了引用計(jì)數(shù)

類似java中對(duì)象垃圾的定位方法,如果有一個(gè)指針引用某塊內(nèi)存,則引用計(jì)數(shù)+1,釋放計(jì)數(shù)-1.如果引用計(jì)數(shù)為0,則說明這塊內(nèi)存可以釋放了。

下面我們寫個(gè)shared_ptr循環(huán)引用的情況:

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 <<"計(jì)數(shù)a:" << a.use_count() << endl;
    cout << "計(jì)數(shù)b:" << b.use_count() << endl;
    a->pa = b;
    b->pb = a;
    cout << "第二次引用:" << endl;
    cout << "計(jì)數(shù)a:" << a.use_count() << endl;
    cout << "計(jì)數(shù)b:" << b.use_count() << endl;
}
運(yùn)行結(jié)果:
第一次引用:
計(jì)數(shù)a:1
計(jì)數(shù)b:1
第二次引用:
計(jì)數(shù)a:2
計(jì)數(shù)b:2

[**
可以看到運(yùn)行結(jié)果并沒有打印出對(duì)應(yīng)的析構(gòu)函數(shù),也就是沒被釋放。

指針a和指針b是棧上的,當(dāng)退出他們的作用域后,引用計(jì)數(shù)會(huì)-1,但是其計(jì)數(shù)器數(shù)是2,所以還不為0,也就是不能被釋放。你不釋放我,我也不釋放你,咱兩耗著唄。

這就是標(biāo)志性的由于循環(huán)引用計(jì)數(shù)導(dǎo)致的內(nèi)存泄露.。所以 我們?cè)谠O(shè)計(jì)深淺拷貝代碼的時(shí)候千萬別寫出循環(huán)引用的情況 。

move語義轉(zhuǎn)移

在C++11之前,如果要將源對(duì)象的狀態(tài)轉(zhuǎn)移到目標(biāo)對(duì)象只能通過復(fù)制。

而現(xiàn)在在某些情況下,我們沒有必要復(fù)制對(duì)象,只需要移動(dòng)它們。

C++11引入移動(dòng)語義

源對(duì)象資源的控制權(quán)全部交給目標(biāo)對(duì)象。注意這里說的是控制權(quán),即使用一個(gè)新的指針對(duì)象去指向這個(gè)對(duì)象,然后將原對(duì)象的指針置為nullptr

模型如下:

圖片

要實(shí)現(xiàn)move語義,需要實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)

代碼如下:

//移動(dòng)語義move
class Human {
public:
    Human(int age) :_age(age) {

}

int _age;;

};
class String {
public:

String(Human* pHuman) {

this->pHuman = pHuman;

}

//重寫拷貝構(gòu)造,實(shí)現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰

String(const String& str) {

////深拷貝

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

//淺拷貝

pHuman = str.pHuman;

}

//移動(dòng)構(gòu)造函數(shù)

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的權(quán)限會(huì)由s1讓渡給s2,s2再讓渡給s3 .

使用move語義轉(zhuǎn)移在C++中使用還是比較頻繁的,因?yàn)槠淇梢源蟠罂s小因?yàn)閷?duì)象對(duì)象的創(chuàng)建導(dǎo)致內(nèi)存吃緊的情況。比較推薦應(yīng)用中使用這種方式來優(yōu)化內(nèi)存方面問題.

總結(jié)

本篇文章主要講解了C++面向?qū)ο?a target="_blank">編程中的深拷貝和淺拷貝的問題,以及使用引用計(jì)數(shù)和move語義轉(zhuǎn)移的方式來優(yōu)化深淺拷貝的問題。

C++不像Java那樣,JVM都給我們處理好了資源釋放的問題,沒有二次釋放導(dǎo)致的崩潰情況, C++要懂的東西遠(yuǎn)非Java可比,這也是為什么C++程序員那么少的原因之一吧 。

]()

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    基于C/C++面向對(duì)象的方式封裝socket通信類

    在掌握了基于 TCP 的套接字通信流程之后,為了方便使用,提高編碼效率,可以對(duì)通信操作進(jìn)行封裝,本著有的原則,先基于 C 語言進(jìn)行面向過程的函數(shù)封裝,然后再基于
    的頭像 發(fā)表于 12-26 09:57 ?1140次閱讀

    C++零基礎(chǔ)教程之C++拷貝拷貝,輕松上手C++拷貝拷貝

    編程語言C++語言
    電子學(xué)習(xí)
    發(fā)布于 :2023年01月14日 11:37:32

    拷貝拷貝的實(shí)現(xiàn)方法概述

    拷貝拷貝的實(shí)現(xiàn)
    發(fā)表于 07-19 13:35

    python深淺拷貝是什么?

    python的直接賦值python的拷貝python的拷貝
    發(fā)表于 11-04 08:33

    請(qǐng)問哪位大神可以詳細(xì)介紹JavaScript拷貝拷貝?

    JavaScript數(shù)據(jù)類型JavaScript拷貝拷貝
    發(fā)表于 11-05 07:16

    C++ 面向對(duì)象多線程編程下載

    C++ 面向對(duì)象多線程編程下載
    發(fā)表于 04-08 02:14 ?70次下載

    面向對(duì)象的程序設(shè)計(jì)(C++

    面向對(duì)象的程序設(shè)計(jì)(C++).面向對(duì)象的基本思想 C++對(duì)
    發(fā)表于 03-22 14:40 ?0次下載

    C#拷貝拷貝區(qū)別解析

     所謂拷貝就是將對(duì)象的所有字段復(fù)制到新的副本對(duì)象;
    發(fā)表于 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>區(qū)別解析

    Python如何防止數(shù)據(jù)被修改Python拷貝拷貝的問題說明

    在平時(shí)工作,經(jīng)常涉及到數(shù)據(jù)的傳遞。在數(shù)據(jù)傳遞使用過程,可能會(huì)發(fā)生數(shù)據(jù)被修改的問題。為了防止數(shù)據(jù)被修改,就需要再傳遞一個(gè)副本,即使副本被修改,也不會(huì)影響原數(shù)據(jù)的使用。為了生成這個(gè)副本,就產(chǎn)生了拷貝——今天就說一下Python
    的頭像 發(fā)表于 03-30 09:54 ?2955次閱讀
    Python如何防止數(shù)據(jù)被修改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++:詳談拷貝構(gòu)造函數(shù)

    只有單個(gè)形參,而且該形參是對(duì)本類類型對(duì)象的引用(常用const修飾),這樣的構(gòu)造函數(shù)稱為拷貝構(gòu)造函數(shù)。拷貝構(gòu)造函數(shù)是特殊的構(gòu)造函數(shù),創(chuàng)建對(duì)象時(shí)使用已存在的同類
    的頭像 發(fā)表于 06-29 11:45 ?2043次閱讀
    <b class='flag-5'>C++</b>:詳談<b class='flag-5'>拷貝</b>構(gòu)造函數(shù)

    C++拷貝構(gòu)造函數(shù)的copy及copy

    C++編譯器會(huì)默認(rèn)提供構(gòu)造函數(shù);無參構(gòu)造函數(shù)用于定義對(duì)象的默認(rèn)初始化狀態(tài);拷貝構(gòu)造函數(shù)在創(chuàng)建對(duì)象時(shí)拷貝對(duì)
    的頭像 發(fā)表于 12-24 15:31 ?634次閱讀

    C語言是怎么面向對(duì)象編程

    在嵌入式開發(fā),C/C++語言是使用最普及的,在C++11版本之前,它們的語法是比較相似的,只不過C++提供了
    的頭像 發(fā)表于 02-14 13:57 ?1506次閱讀
    <b class='flag-5'>C</b>語言是怎么<b class='flag-5'>面向</b><b class='flag-5'>對(duì)象</b><b class='flag-5'>編程</b>

    C/C++面向對(duì)象編程思想3

    C++作為一門在C和Java之間的語言,其既可以使用C語言中的高效指針,又繼承了Java面向對(duì)象
    的頭像 發(fā)表于 03-30 15:16 ?475次閱讀
    <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>之<b class='flag-5'>面向</b><b class='flag-5'>對(duì)象</b><b class='flag-5'>編程</b>思想3

    C++拷貝拷貝詳解

    當(dāng)類的函數(shù)成員存在指針成員時(shí)會(huì)產(chǎn)生拷貝拷貝和問題。
    發(fā)表于 08-21 15:05 ?286次閱讀
    <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
    的頭像 發(fā)表于 11-02 10:58 ?315次閱讀