深拷貝與淺拷貝、值語義與引用語義對象語義 ——以C++和Python為例

深拷貝與淺拷貝、值語義與引用語義/對象語義 ——以C++和Python為例

值語義與引用語義(對象語義)

本小節參考自:https://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

概念

在任何編程語言中,區分深淺拷貝的關鍵都是要區分值語義和引用語義(對象語義)。

值語義(value sematics)指的是對象的拷貝與原對象是獨立的、無關的,就像拷貝 int 一樣。C++ 的內置類型(bool/int/double/char)都是值語義,標準庫里的 complex<> 、pair<>、vector<>、map<>、string 等等類型也都是值語意,拷貝之后就與原對象脫離關系。Java 語言的 primitive types 也是值語義。

與值語義對應的是“對象語義/object sematics”,或者叫做引用語義(reference sematics)。對象語義指的是面向對象意義下的對象,對象拷貝是禁止的。例如 muduo 里的 Thread 是對象語義,拷貝 Thread 是無意義的,也是被禁止的:因為 Thread 代表線程,拷貝一個 Thread 對象并不能讓系統增加一個一模一樣的線程。Java 里邊的 class 對象都是對象語義/引用語義。

生命期

值語義的一個巨大好處是生命期管理很簡單,就跟 int 一樣——你不需要操心 int 的生命期。值語義的對象要么是 stack object,或者直接作為其他 object 的成員,因此我們不用擔心它的生命期(一個函數使用自己stack上的對象,一個成員函數使用自己的數據成員對象)。

相反,對象語義的 object 由于不能拷貝,我們只能通過指針或引用來使用它。一旦使用指針和引用來操作對象,那么就要擔心所指的對象是否已被釋放,這一度是 C++ 程序 bug 的一大來源。此外,由于 C++ 只能通過指針或引用來獲得多態性,那么在C++里從事基于繼承和多態的面向對象編程有其本質的困難——資源管理。

C++與標準庫中的值語義

C++ 的 class 本質上是值語義的,這才會出現 object slicing 這種語言獨有的問題,也才會需要程序員注意 pass-by-value 和 pass-by-const-reference 的取舍。在其他面向對象編程語言中,這都不需要費腦筋。

值語義是C++語言的三大約束(與C兼容,零開銷,值語義)之一,C++ 的設計初衷是讓用戶定義的類型(class)能像內置類型(int)一樣工作,具有同等的地位。為此C++做了以下設計(妥協):

  • class 的 layout 與 C struct 一樣,沒有額外的開銷。定義一個“只包含一個 int 成員的 class ”的對象開銷和定義一個 int 一樣。
  • 甚至 class data member 都默認是 uninitialized,因為函數局部的 int 是 uninitialized。
  • class 可以在 stack 上創建,也可以在 heap 上創建。因為 int 可以是 stack variable。
  • class 的數組就是一個個 class 對象挨著,沒有額外的 indirection。因為 int 數組就是這樣。
  • 編譯器會為 class 默認生成 copy constructor 和 assignment operators。其他語言沒有 copy constructor 一說,也不允許重載 assignment operator。C++ 的對象默認是可以拷貝的,這是一個尷尬的特性。
  • 當 class type 傳入函數時,默認是 make a copy (除非參數聲明為 reference)。因為把 int 傳入函數時是 make a copy。
  • 當函數返回一個 class type 時,只能通過 make a copy(C++ 不得不定義 RVO 來解決性能問題)。因為函數返回 int 時是 make a copy。
  • 以 class type 為成員時,數據成員是嵌入的。例如 pair<complex<double>, size_t> 的 layout 就是 complex<double> 挨著 size_t。

C++ 要求凡是能放入標準容器的類型必須具有值語義。準確地說:type 必須是 SGIAssignable concept 的 model。但是,由 于C++ 編譯器會為 class 默認提供 copy constructor 和 assignment operator,因此除非明確禁止,否則 class 總是可以作為標準庫的元素類型——盡管程序可以編譯通過,但是隱藏了資源管理方面的 bug。

因此,在寫一個 class 的時候,先讓它繼承 boost::noncopyable,幾乎總是正確的。

在現代 C++ 中,一般不需要自己編寫 copy constructor 或 assignment operator,因為只要每個數據成員都具有值語義的話,編譯器自動生成的 member-wise copying&assigning 就能正常工作;如果以 smart ptr 為成員來持有其他對象,那么就能自動啟用或禁用 copying&assigning。例外:編寫 HashMap 這類底層庫時還是需要自己實現 copy control。

這些設計帶來了性能上的好處,原因是 memory locality

個人覺得是這樣(在學習,有錯誤請指出):

基本數據類型自定義class
C++值語義值語義
Java值語義引用語義
Python引用語義引用語義

當然這都是默認情況下,具體情況具體需求可以用深/淺拷貝來處理。

另外,類的某個成員變量是值語義/引用語義與這個類本身是值語義/引用語義無關。

C++的另一個麻煩之處在于不支持自動垃圾回收,所以要程序員自己小心地處理生命周期問題。

C++中的深淺拷貝

C++中類的拷貝控制

首先我們簡單地提一下C++中的拷貝控制這件事情。當我們定義一個類的時候,為了讓我們定義的類類型像內置類型(char,int,double等)一樣好用,我們通常需要考下面幾件事:

Q1:用這個類的對象去初始化另一個同類型的對象。

Q2:將這個類的對象賦值給另一個同類型的對象。

Q3:讓這個類的對象有生命周期,比如局部對象在代碼部結束的時候,需要銷毀這個對象。

因此C++就定義了5種拷貝控制操作,其中2個移動操作是C++11標準新加入的特性:

拷貝構造函數(copy constructor)

移動構造函數(move constructor)(C++11)

拷貝賦值運算符(copy-assignment operator)

移動賦值運算符(move-assignment operator)(C++11)

析構函數 (destructor)

前兩個構造函數發生在Q1時,中間兩個賦值運算符發生在Q2時,而析構函數則負責類對象的銷毀。

但是對初學者來說,既是福音也是災難的是,如果我們沒有在定義的類里面定義這些控制操作符,編譯器會自動的為我們提供一個默認的版本。這有時候看起來是好事,但是編譯器不是萬能的,它的行為在很多時候并不是我們想要的。

所以,在實現拷貝控制操作中,最困難的地方是認識到什么時候需要定義這些操作

拷貝控制又是一個大的話題,為了弄明白深淺拷貝,這里我們只需要認識到拷貝構造函數和拷貝賦值運算符:它們是一個在類的對象發生將某個對象賦值給另一個同類的對象時會被用到的拷貝控制。編譯器提供了它們的默認實現,但是在某些情況下,默認的實現并不能很好地工作。

無指針的類

上面已經介紹過,在C++中主要是值語義。首先考慮這樣一個類:

class Foo {
private:int m_a;int m_b;
public:Foo(): m_a(0), m_b(0){ }Foo(int a, int b): m_a(a), m_b(b){ }
};

在這個類中,只有值語義的成員 m_am_b

如果我們要拷貝這個類的一個對象如:

int main(){Foo obj1(3, 5);Foo obj2 = obj1;std::cout << &obj1 << std::endl;std::cout << &obj2 << std::endl;return 0;
}

此時會調用編譯器默認的拷貝構造函數,即淺拷貝,就是簡單地將對象 obj1 內的成員直接照模照樣復制一份,放到新的對象 obj2 中。注意,由于默認提供了拷貝構造函數和賦值運算符,C++中的對象都是值語義的。從而,這樣的賦值操作是會新建一個對象,而非增加一個指向原對象 obj1 的引用(這與Java和Python中不同)。這可以通過查看兩個對象的地址得到驗證,輸出:

0x7ffffaa2ffa0
0x7ffffaa2ff98

二者地址不同。

OK,so far, so good. 這時深淺拷貝其實是一樣的,因為類內沒有指針類型的成員。淺拷貝(編譯器提供的默認拷貝構造函數)就可以工作的很好,不需要我們做什么調整。但是,如果類內包含指針類型的成員,問題就來了。

含有指針的類

當類成員中含有指針類型時,情況就大不相同了,考慮下面的類:

#include <iostream>class Bar {
private:int m_a;int* m_p;
public:Bar(): m_a(0), m_p(nullptr){ }Bar(int a, int* p): m_a(a), m_p(p){ }// Bar(const Bar &bar) {				// 自己重寫拷貝構造函數,深拷貝//    this->m_a = bar.m_a;//    this->m_p = new int(*(bar.m_p));// }void print_member() {std::cout << m_a << ',' << *m_p << std::endl;}void change_p(int num) {*m_p = num;}
};

假設我們現在還沒有寫上面的重寫的拷貝構造函數,也就是說還是執行編譯器為我們默認提供的淺拷貝的拷貝構造函數,執行以下測試:

int main(){int a = 3;int b = 5;Bar obj1(a, &b);Bar obj2 = obj1;std::cout << &obj1 << std::endl;std::cout << &obj2 << std::endl;obj1.print_member();obj2.print_member();obj1.change_p(6);obj1.print_member();obj2.print_member();return 0;
}

得到輸出:

0x7ffe92762a40
0x7ffe92762a50
3,5
3,5
3,6
3,6

兩個對象是在內存地址,是獨立的,這仍然沒有問題。但是問題來了,當我們改變 obj1m_p 指針所指向的值時。obj2 的值也跟著改變了。這時因為默認的拷貝構造函數(淺拷貝)只會將類內的所有成員都復制一份給到新的對象,至于是指針還是值,他一概不管的。這就導致了指針類型的成員變量 m_p 也被原封不動的給到了新的對象 obj2 ,這樣兩個對象 obj1obj2m_p 指針的指向的是相同的。從而導致了上面 obj2 的值跟著 obj1變化的情況。這種情況,相當于是值語義的對象中有引用語義的成員。

這種情況下,默認的淺拷貝顯然就不能滿足我們的需求了,這時我們就要自己重寫實現一個拷貝構造函數來完成深拷貝,將指針類型所指向的值重新找一塊地址來存放,從而避免與原對象指向了相同的地址。

實現也一并在上面的代碼塊中了。當我們打開拷貝構造函數的注釋,再執行測試,得到結果如下:

0x7fff232ea1e0
0x7fff232ea1f0
3,5
3,5
3,6
3,5

可以看到,現在兩個對象的改變是完全獨立的了,obj1 的變化并不會影響的 obj2 。程序的行為符合我們的預期。

總結與思考

總結一下,在 C++ 中:

  • 由于編譯器提供了默認的拷貝構造函數和賦值運算符,所以自定義的類一般都是值語義的。

  • 如果類內沒有指針類型的成員變量,完全可以使用編譯器默認提供的淺拷貝的拷貝構造函數。然而,當類內存在指針類型的成員變量,我們必須重寫拷貝構造函數實現深拷貝,從而避免bug的出現。

那么為什么編譯器不能智能地在合適的時候執行深拷貝呢?在知乎的一個問題中,有人指出了一些原因:

編譯器等…默認的行為都是淺拷貝的原因之一,是深拷貝不一定能夠實現。例如指向的對象可能是多態的(C++沒有標準的虛構造函數),也可能是數組,也可能有循環引用(如 struct N(N *p;};)。所以只能留待成員變量的類來決定怎樣復制。

值得一提的是,除了復制操作,還可以考慮移動和交換操作。它們的性能通常比復制操作更優。自C++11開始也提供了標準的移動操作的實現方法。

Python中的深淺拷貝

在 Python 和 Java 中,變量保存的是對象(值)的引用,也就是說 Python 都是上面提到的 引用語義。我們剛才在C++中提到過,當值語義的對象中有引用語義的成員時,我們需要自己實現深拷貝來保證兩個對象所引用內容的獨立、分離。那在Python中,全都是引用語義,該怎么處理呢,深淺拷貝的區別更需要仔細辨別,有這三種情況:賦值、淺拷貝和深拷貝。

以下是簡要的圖文介紹。詳細可參考:Python中的深拷貝與淺拷貝。

賦值

b = a

賦值引用,a 和 b都指向同一個對象,a 與 b 的變化完全同步。

在這里插入圖片描述

賦值引用

淺拷貝

b = a.copy(),也可以 b = copy.copy(a),其中后者可以處理所有類型,前者不能處理內置數據類型如 int 。

淺拷貝, a 和 b 是一個獨立的對象,但他們的子對象還是指向統一對象(是引用),所以它們的子對象變化同步,其他不同步。

實際上,淺拷貝指的是重新分配一塊內存,創建一個新的對象,但里面的元素是原對象中各個子對象的引用。

在這里插入圖片描述

淺拷貝

深拷貝

b = copy.deepcopy(a)

深拷貝, a 和 b 完全拷貝了父對象及其子對象,兩者是完全獨立的,兩者的變化也完全無關。

實際上,淺拷貝是指重新分配一塊內存,創建一個新的對象,并且將原對象中的元素,以遞歸的方式,通過創建新的子對象拷貝到新對象中。

在這里插入圖片描述

深拷貝

Ref:

https://www.zhihu.com/question/36370072

https://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

https://www.cnblogs.com/ronny/p/3734110.html

https://blog.csdn.net/weixin_44966641/article/details/122118289?spm=1001.2014.3001.5501

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/532578.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/532578.shtml
英文地址,請注明出處:http://en.pswp.cn/news/532578.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一次打卡軟件的實戰滲透測試

直接打卡抓包, 發現有疑似企業網站,查ip直接顯示以下頁面 直接顯示了后臺安裝界面…就很有意思 探針和phpinfo存在 嘗試連接mysql失敗 fofa掃描為阿里云服務器 找到公司官網使用nmap掃描,存在端口使用onethink 查詢onethink OneThink是一個開源的內容管理框架&#xff0c;…

C++中類的拷貝控制

C中類的拷貝控制 轉自&#xff1a;https://www.cnblogs.com/ronny/p/3734110.html 1&#xff0c;什么是類的拷貝控制 當我們定義一個類的時候&#xff0c;為了讓我們定義的類類型像內置類型&#xff08;char,int,double等&#xff09;一樣好用&#xff0c;我們通常需要考下面…

centos7ubuntu搭建Vulhub靶場(推薦Ubuntu)

這里寫目錄標題一.前言總結二.成功操作&#xff1a;三.出現報錯&#xff1a;四.vulhub使用正文&#xff1a;一.前言總結二.成功操作&#xff1a;三.出現報錯&#xff1a;四.vulhub使用看完點贊關注不迷路!!!! 后續繼續更新優質安全內容!!!!!一.前言總結 二.成功操作&#xff1…

使用 PyTorch 數據讀取,JAX 框架來訓練一個簡單的神經網絡

使用 PyTorch 數據讀取&#xff0c;JAX 框架來訓練一個簡單的神經網絡 本文例程部分主要參考官方文檔。 JAX簡介 JAX 的前身是 Autograd &#xff0c;也就是說 JAX 是 Autograd 升級版本&#xff0c;JAX 可以對 Python 和 NumPy 程序進行自動微分。可以通過 Python的大量特征…

Yapi Mock 遠程代碼執行漏洞

跟風一波復現Yapi 漏洞描述&#xff1a; YApi接口管理平臺遠程代碼執行0day漏洞&#xff0c;攻擊者可通過平臺注冊用戶添加接口&#xff0c;設置mock腳本從而執行任意代碼。鑒于該漏洞目前處于0day漏洞利用狀態&#xff0c;強烈建議客戶盡快采取緩解措施以避免受此漏洞影響 …

C++ ACM模式輸入輸出

C ACM模式輸入輸出 以下我們都以求和作為題目要求&#xff0c;來看一下各種輸入輸出應該怎么寫。 1 只有一個或幾個輸入 輸入樣例&#xff1a; 3 5 7輸入輸出模板&#xff1a; int main() {int a, b, c;// 接收有限個輸入cin >> a >> b >> c;// 輸出結果…

CVE-2017-10271 WebLogic XMLDecoder反序列化漏洞

漏洞產生原因&#xff1a; CVE-2017-10271漏洞產生的原因大致是Weblogic的WLS Security組件對外提供webservice服務&#xff0c;其中使用了XMLDecoder來解析用戶傳入的XML數據&#xff0c;在解析的過程中出現反序列化漏洞&#xff0c;導致可執行任意命令。攻擊者發送精心構造的…

樹莓派攝像頭 C++ OpenCV YoloV3 實現實時目標檢測

樹莓派攝像頭 C OpenCV YoloV3 實現實時目標檢測 本文將實現樹莓派攝像頭 C OpenCV YoloV3 實現實時目標檢測&#xff0c;我們會先實現樹莓派對視頻文件的逐幀檢測來驗證算法流程&#xff0c;成功后&#xff0c;再接入攝像頭進行實時目標檢測。 先聲明一下筆者的主要軟硬件配…

【實戰】記錄一次服務器挖礦病毒處理

信息收集及kill&#xff1a; 查看監控顯示長期CPU利用率超高&#xff0c;懷疑中了病毒 top 命令查看進程資源占用&#xff1a; netstat -lntupa 命令查看有無ip進行發包 netstat -antp 然而并沒有找到對應的進程名 查看java進程和solr進程 ps aux &#xff1a;查看所有進程…

ag 搜索工具參數詳解

ag 搜索工具參數詳解 Ag 是類似ack&#xff0c; grep的工具&#xff0c;它來在文件中搜索相應關鍵字。 官方列出了幾點選擇它的理由&#xff1a; 它比ack還要快 &#xff08;和grep不在一個數量級上&#xff09;它會忽略.gitignore和.hgignore中的匹配文件如果有你想忽略的文…

CVE-2013-4547 文件名邏輯漏洞

搭建環境&#xff0c;訪問 8080 端口 漏洞說明&#xff1a; Nginx&#xff1a; Nginx是一款輕量級的Web 服務器/反向代理服務器及電子郵件&#xff08;IMAP/POP3&#xff09;代理服務器&#xff0c;在BSD-like 協議下發行。其特點是占有內存少&#xff0c;并發能力強&#xf…

CMake指令入門 ——以構建OpenCV項目為例

CMake指令入門 ——以構建OpenCV項目為例 轉自&#xff1a;https://blog.csdn.net/sandalphon4869/article/details/100589747 一、安裝 sudo apt-get install cmake安裝好后&#xff0c;輸入 cmake -version如果出現了cmake的版本顯示&#xff0c;那么說明安裝成功 二、c…

CVE-2017-7529Nginx越界讀取緩存漏洞POC

漏洞影響 低危&#xff0c;造成信息泄露&#xff0c;暴露真實ip等 實驗內容 漏洞原理 通過查看patch確定問題是由于對http header中range域處理不當造成&#xff0c;焦點在ngx_http_range_parse 函數中的循環&#xff1a; HTTP頭部range域的內容大約為Range: bytes4096-81…

Linux命令行性能監控工具大全

Linux命令行性能監控工具大全 作者&#xff1a;Arnold Lu 原文&#xff1a;https://www.cnblogs.com/arnoldlu/p/9462221.html 關鍵詞&#xff1a;top、perf、sar、ksar、mpstat、uptime、vmstat、pidstat、time、cpustat、munin、htop、glances、atop、nmon、pcp-gui、collect…

Weblogic12c T3 協議安全漏洞分析【CVE-2020-14645 CVE-2020-2883 CVE-2020-14645】

給個關注&#xff1f;寶兒&#xff01; 給個關注&#xff1f;寶兒&#xff01; 給個關注&#xff1f;寶兒&#xff01; 關注公眾號&#xff1a;b1gpig信息安全&#xff0c;文章推送不錯過 ## 前言 WebLogic是美國Oracle公司出品的一個application server,確切的說是一個基于JAV…

Getshell總結

按方式分類&#xff1a; 0x01注入getshell&#xff1a; 0x02 上傳 getwebshell 0x03 RCE getshell 0x04 包含getwebshell 0x05 漏洞組合拳getshell 0x06 系統層getcmdshell 0x07 釣魚 getcmdshell 0x08 cms后臺getshell 0x09 紅隊shell競爭分析 0x01注入getshell&#xff1a;…

編寫可靠bash腳本的一些技巧

編寫可靠bash腳本的一些技巧 原作者&#xff1a;騰訊技術工程 原文鏈接&#xff1a;https://zhuanlan.zhihu.com/p/123989641 寫過很多 bash 腳本的人都知道&#xff0c;bash 的坑不是一般的多。 其實 bash 本身并不是一個很嚴謹的語言&#xff0c;但是很多時候也不得不用。以下…

python 到 poc

0x01 特殊函數 0x02 模塊 0x03 小工具開發記錄 特殊函數 # -*- coding:utf-8 -*- #內容見POC.demo; POC.demo2 ;def add(x,y):axyprint(a)add(3,5) print(------------引入lambad版本&#xff1a;) add lambda x,y : xy print(add(3,5)) #lambda函數,在lambda函數后面直接…

protobuf版本常見問題

protobuf版本常見問題 許多軟件都依賴 google 的 protobuf&#xff0c;我們很有可能在安裝多個軟件時重復安裝了多個版本的 protobuf&#xff0c;它們之間很可能出現沖突并導致在后續的工作中出現版本不匹配之類的錯誤。本文將討論筆者在使用 protobuf 中遇到的一些問題&#…

CMake常用命令整理

CMake常用命令整理 轉自&#xff1a;https://zhuanlan.zhihu.com/p/315768216 CMake 是什么我就不用再多說什么了&#xff0c;相信大家都有接觸才會看一篇文章。對于不太熟悉的開發人員可以把這篇文章當個查找手冊。 1.CMake語法 1.1 指定cmake的最小版本 cmake_minimum_r…