C++ 類的行為 | 行為像值的類、行為像指針的類、swap函數處理自賦值

文章目錄

  • 概念
  • 行為像值的類
  • 行為像指針的類
    • 概念
    • 引用計數
      • 動態內存實現計數器
  • 類的swap
    • 概念
    • swap實現自賦值


概念

行為像值的類和行為像指針的類這兩種說法其實蠻拗口的,這也算是 《C++Primer》 翻譯的缺點之一吧。。。

其實兩者的意思分別是:

  • 行為像值的類: 每個類的對象都有自己的實現
  • 行為像指針的類: 所有類的對象共享類的資源(類似于 shared_ptr 智能指針,每有一個對象持有該資源則引用計數+1,每有一個對象釋放該資源則引用計數-1,引用計數為0時釋放內存)

本篇博客的內容跟 類 和 智能指針 兩篇博客有關。不了解的同學可以先看看這兩篇博客。


行為像值的類

對于類管理的資源,每個對象都應該有一份自己的拷貝(實現)。如下面的 string類型的指針 ,使用拷貝構造函數 or 賦值運算符時,每個對象拷貝的都是 指針成員ps 指向的 string 而非 ps本身 。換言之,每個對象 都有一個ps 而不是 給ps加引用計數

class A
{int i = 0;string* ps;
public:A(const string &s = string()): ps(new string(s)), i(0) {}A(const A &a): ps(new string(*a.ps)), i(a.i) {}A& operator=(const A&);~A() { delete ps; }
};A& A::operator=(const A& a)
{string* newps = new string(*a.ps); // 將a.ps指向的值拷貝到局部臨時對象newps中delete ps;  // 銷毀ps指向的內存,避免舊內存泄漏ps = newps; i = a.i;return *this; // 返回此對象的引用
}

為什么不能像下面這樣實現賦值運算符呢?

A& A::operator=(const A& a)
{delete ps;  // 銷毀ps指向的內存,避免內存泄漏ps = new string(*(a.ps)); i = a.i;return *this; // 返回此對象的引用
}

這是因為如果 a*this同一個對象delete ps 會釋放 *thisa 指向的 string。接下來,當我們在 new表達式 中試圖拷貝*(a.ps)時,就會訪問一個指向無效內存的指針(即空懸指針),其行為和結果是未定義的。

因此,第一種實現方法可以確保銷毀 *this 的現有成員操作是絕對安全的,不會產生空懸指針


行為像指針的類

概念

對于行為類似指針的類,使用拷貝構造函數 or 賦值運算符時,每個對象拷貝的都是 ps本身 而非 指針成員ps 指向的 string。換言之,每有一個對象都是 給指向string的ps加引用計數

因此,析構函數不能粗暴地釋放 ps 指向的 string ,只有當最后一個指向 stringA類對象 銷毀時,才可以釋放 string 。我們會發現這個特性很符合 shared_ptr 的功能,因此我們可以使用 shared_ptr 來管理 像指針的類 中的資源

但是,有時我們需要程序員直接管理資源,因此就要用到 引用計數(reference count) 了。


引用計數

工作方式:

  • 每個構造函數(拷貝構造函數除外)都要創建一個引用計數,用來記錄有多少對象與正在創建的對象共享狀態。當我們創建一個對象時,只有一個對象共享狀態,因此將計數器初始化為1。
  • 拷貝構造函數不分配新的計數器,而是拷貝給定對象的數據成員,包括計數器。拷貝構造函數遞增共享的計數器,指出給定對象的狀態又被一個新用戶所共享。
  • 析構函數遞減計數器,指出共享狀態的用戶少了一個。如果計數器變為0,則析構函數釋放狀態。
  • 拷貝賦值運算符遞增右側運算對象的計數器,遞減左側運算對象的計數器。如果左側運算對象的計數器變為0,意味著它的共享狀態沒有用戶了,拷貝賦值運算符就必須銷毀狀態。

唯一的難題是確定在哪里存放引用計數。計數器不能直接作為 A對象 的成員。舉個例子:

A a1("cmy");
A a2(a1); // a2和a1指向相同的string
A a3(a2); // a1、a2、a3都指向相同的string

如果計數器保存在每個對象中,創建 a2 時可以遞增 a1 的計數器并拷貝到 a2 中。可創建 a3 時,誠然可以更新 a1 的計數器,但怎么找到 a2 并將它的計數器更新呢?

那么怎么處理計數器呢?


動態內存實現計數器

class A
{int i = 0;string *ps;size_t *use; // 記錄有多少個對象共享*ps的成員
public:A(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) {}A(const A &a): ps(new string(*a.ps)), i(a.i), use(a.use) { ++*use; }A& operator=(const A&);~A() {}
};
A::~A(){if(--*use == 0){ // 引用計數變為0delete ps; // 釋放string內存delete use; // 釋放計數器內存}
}
A& A::operator=(const A& a)
{++*(a.use); // 之所以將計數器自增操作放這么前// 是為了防止自賦值時計數器自減導致ps、use直接被釋放if(--(*use) == 0){delete ps;delete use;}ps = a.ps;i = a.i;use = a.use;return *this; // 返回此對象的引用
}

類的swap

概念

我們在設計類的 swap 時,雖然邏輯上是這樣:

A tmp = a1;
a1 = a2;
a2 = tmp;

但如果真的這樣實現的話,還需要創建一個新的對象 tmp,效率是很低的,造成了內存空間的浪費。因此我們實際上希望的是這樣的邏輯實現:

string *tmp = a1.ps;
a1.ps = a2.ps;
a2.ps = tmp;

創建一個 string類型 總比創建一個 A類對象 要省內存。具體實現:

class A
{friend void swap(A&, A&);
};
inline void swap(A& a1, A& a2){using std::swap;swap(a1.ps, a2.ps);swap(a1.i, a2.i);
}

swap實現自賦值

使用拷貝和交換的賦值運算符:

A& A::operator=(A a){ // 傳值,使用拷貝構造函數通過實參(右側運算對象)拷貝生成臨時量aswap(*this, a); // a現在指向*this曾使用的內存return *this; // a的作用域結束,被銷毀,delete了a中的ps
}

上面重載的賦值運算符參數并不是一個引用,也就是說 a 是右側運算對象的一個副本。

在函數體中,swap 交換了 a 和 *this 中的數據成員。*thisps 指向右側運算對象中 string 的一個副本;*this 原來的 ps 存入 a 中。但函數體執行完,a 作為局部變量被銷毀,deletea 中的 ps,即 釋放掉了左側運算對象(*this)中原來的內存。

這個技術的有趣之處是它自動處理了自賦值情況且天然就是異常安全的。

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

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

相關文章

C++ 右值引用 | 左值、右值、move、移動語義、引用限定符

文章目錄C11為什么引入右值?區分左值引用、右值引用move移動語義移動構造函數移動賦值運算符合成的移動操作小結引用限定符規定this是左值or右值引用限定符與重載C11為什么引入右值? C11引入了一個擴展內存的方法——移動而非拷貝,移動較之拷…

且談關于最近軟件測試的面試

前段時間有新的產品需要招人,安排和參加了好幾次面試,下面就談談具體的面試問題,在面試他人的同時也面試自己。 面試問題是參與面試同事各自設計的,我也不清楚其他同事的題目,就談談自己設計的其中2道題。 過去面試總是…

C++ 多態 | 虛函數、抽象類、虛函數表

文章目錄多態虛函數重寫重定義(參數不同)協變(返回值不同)析構函數重寫(函數名不同)final和override重載、重寫、重定義抽象類多態的原理虛函數常見問題解析虛函數表多態 一種事物,多種形態。換…

C++ 運算符重載(一) | 輸入/輸出,相等/不等,復合賦值,下標,自增/自減,成員訪問運算符

文章目錄輸出運算符<<輸入運算符>>相等/不等運算符復合賦值運算符下標運算符自增/自減運算符成員訪問運算符輸出運算符<< 通常情況下&#xff0c;輸出運算符的第一個形參是一個 非常量ostream對象的引用 。之所以 ostream 是非常量是因為向流寫入內容會改變…

C++ 重載函數調用運算符 | 再探lambda,函數對象,可調用對象

文章目錄重載函數調用運算符lambdalambda等價于函數對象lambda等價于類標準庫函數對象可調用對象與function可調用對象function函數重載與function重載函數調用運算符 函數調用運算符必須是成員函數。 一個類可以定義多個不同版本的調用運算符&#xff0c;互相之間應該在參數數…

C++ 運算符重載(二) | 類型轉換運算符,二義性問題

文章目錄類型轉換運算符概念避免過度使用類型轉換函數解決上述問題的方法轉換為 bool顯式的類型轉換運算符類型轉換二義性重載函數與類型轉換結合導致的二義性重載運算符與類型轉換結合導致的二義性類型轉換運算符 概念 類型轉換運算符&#xff08;conversion operator&#…

Tomcat中JVM內存溢出及合理配置

Tomcat本身不能直接在計算機上運行&#xff0c;需要依賴于硬件基礎之上的操作系統和一個Java虛擬機。Tomcat的內存溢出本質就是JVM內存溢出&#xff0c;所以在本文開始時&#xff0c;應該先對Java JVM有關內存方面的知識進行詳細介紹。 一、Java JVM內存介紹 JVM管理兩種類型的…

俄羅斯農民乘法 | 快速乘

文章目錄概念概念 俄羅斯農民乘法經常被用于兩數相乘取模的場景&#xff0c;如果兩數相乘已經超過數據范圍&#xff0c;但取模后不會超過&#xff0c;我們就可以利用這個方法來拆位取模計算貢獻&#xff0c;保證每次運算都在數據范圍內。 A 和 B 兩數相乘的時候我們如何利用加…

Linux網絡編程 | socket選項設定 及 網絡信息API

文章目錄讀取和設置 socket 選項SO_REUSEADDRSO_RCVBUF 和 SO_SNDBUFSO_RCVLOWAT 和 SO_SNDLOWATSO_LINGER 選項網絡信息APIgethostbyname 和 gethostbyaddrgetservbyname 和 getservbyportgetaddrinfogetnameinfo讀取和設置 socket 選項 正如 fcntl 系統調用是控制文件描述符…

Linux | 高級I/O函數

文章目錄創建文件描述符的函數pipe函數dup函數、dup2函數讀取或寫入數據readv函數、writev函數零拷貝sendfile函數splice函數tee函數進程間通信——共享內存mmap函數 和 munmap函數控制文件描述符fcntl函數創建文件描述符的函數 pipe函數 不再贅述&#xff0c;詳情見我的另一…

分布式理論:CAP、BASE | 分布式存儲與一致性哈希

文章目錄分布式理論CAP定理BASE理論分布式存儲與一致性哈希簡單哈希一致性哈希虛擬節點分布式理論 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系統中的所有數據副本&#xff0c;在同一時刻是否一致&#xff08;所有節點訪問同一份最新的數據副…

Tomcat服務器性能優化

一、概述 本文檔主要介紹了Tomcat的性能調優的原理和方法。可作為公司技術人員為客戶Tomcat系統調優的技術指南&#xff0c;也可以提供給客戶的技術人員作為他們性能調優的指導手冊。 二、調優分類 由于Tomcat的運行依賴于JVM&#xff0c;從虛擬機的角度我們把Tomcat的調整分為…

分布式系統概念 | 分布式事務:2PC、3PC、本地消息表

文章目錄分布式事務2PC&#xff08;二階段提交協議&#xff09;執行流程優缺點3PC&#xff08;三階段提交協議&#xff09;執行流程優缺點本地消息表&#xff08;異步確保&#xff09;分布式事務 分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分…

數據結構算法 | 單調棧

文章目錄算法概述題目下一個更大的元素 I思路代碼下一個更大元素 II思路代碼132 模式思路代碼接雨水思路算法概述 當題目出現 「找到最近一個比其大的元素」 的字眼時&#xff0c;自然會想到 「單調棧」 。——三葉姐 單調棧以嚴格遞增or遞減的規則將無序的數列進行選擇性排序…

最長下降子序列

文章目錄題目解法DP暴搜思路代碼實現貪心二分思路代碼實現題目 給出一組數據 nums&#xff0c;求出其最長下降子序列&#xff08;子序列允許不連續&#xff09;的長度。&#xff08;類似于lc的最長遞增子序列&#xff09; 示例&#xff1a; 輸入&#xff1a; 6 // 數組元素個…

Linux 服務器程序規范、服務器日志、用戶、進程間的關系

文章目錄服務器程序規范日志rsyslogd 守護進程syslog函數openlog函數setlogmask函數closelog函數用戶進程間的關系進程組會話系統資源限制改變工作目錄和根目錄服務器程序后臺化服務器程序規范 Linux 服務器程序一般以后臺進程&#xff08;守護進程[daemon]&#xff09;形式運…

IO模型 :阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO

文章目錄IO模型阻塞IO非阻塞IO信號驅動IO多路復用IO異步IOIO模型 根據各自的特性不同&#xff0c;IO模型被分為阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO五類。 最主要的兩個區別就是阻塞與非阻塞&#xff0c;同步與異步。 阻塞與非阻塞 阻塞與非阻塞最主要的區別就…

Tomcat服務器集群與負載均衡實現

一、前言 在單一的服務器上執行WEB應用程序有一些重大的問題&#xff0c;當網站成功建成并開始接受大量請求時&#xff0c;單一服務器終究無法滿足需要處理的負荷量&#xff0c;所以就有點顯得有點力不從心了。另外一個常見的問題是會產生單點故障&#xff0c;如果該服務器壞掉…

Linux服務器 | 事件處理模式:Reactor模式、Proactor模式

文章目錄Reactor模式Proactor模式同步I/O模型模擬Proactor模式兩者的優缺點ReactorProactor同步I/O模型通常用于實現 Reactor 模式&#xff0c;異步I/O模型通常用于實現 Proactor 模式。&#xff08;不是絕對的&#xff0c;同步I/O也可模擬出 Proactor 模式&#xff09; React…

Linux服務器 | 服務器模型與三個模塊、兩種并發模式:半同步/半異步、領導者/追隨者

文章目錄兩種服務器模型及三個模塊C/S模型P2P模型I/O處理單元、邏輯單元、存儲單元并發同步與異步半同步/半異步模式變體&#xff1a;半同步/半反應堆模式改進&#xff1a;高效的半同步/半異步模式領導者/追隨者模式組件 &#xff1a;句柄集、線程集、事件處理器工作流程兩種服…