Effective C++ 學習筆記 條款07 為多態基類聲明virtual析構函數

有許多種做法可以記錄時間,因此,設計一個TimeKeeper base class和一些derived classes作為不同的計時方法很合理:

class TimeKeeper
{
public:TimeKeeper();~TimeKeeper();// ...
};class AtomicClock : public TimeKeeper { /* ... */ };    // 原子鐘
class WaterClock : public TimeKeeper { /* ... */ };    // 水鐘
class WristWatch : public TimeKeeper { /* ... */ };    // 腕表

很多客戶只想在程序中使用時間,不想操心時間如何計算等細節,這是我們可以設計factory(工廠)函數,返回指針指向一個計時對象,Factory函數會“返回一個base class指針,指向新生成的derived class對象”:

TimeKeeper *getTimeKeeper();    // 返回一個指針,指向一個TimeKeeper派生類的動態分配對象

為遵循factory函數的規矩,被getTimeKeeper()返回的對象必須位于heap。因此為了避免泄露內存和其他資源,將factory函數返回的每一個對象適當地delete掉很重要:

TimeKeeper *ptk = getTimeKeeper();    // 從TimeKeeper繼承體系獲得一個動態分配對象
// 運用它...
delete ptk;    // 釋放它,避免資源泄露

條款13說“倚賴客戶執行delete,基本上便帶有某種錯誤傾向”,條款18則談到factory函數接口該如何修改以便預防常見的客戶錯誤,但這些在此都是次要的,因為此條款內我們要對付的是上述代碼的一個更根本的弱點:縱使客戶把每一件事都做對了,仍然沒辦法知道程序如何行動。

問題出在getTimeKeeper返回的指針指向一個derived class對象(例如AtomicClock),而那個對象卻經由一個base class指針(例如一個TimeKeeper *指針)被刪除,而目前的base class(TimeKeeper)有個non-virtual析構函數。

這是有問題的,因為C++明白指出,當derived class對象經由一個base class的指針被刪除,而該base class帶著一個non-virtual析構函數時,其結果未定義——實際執行時通常發生的是對象的derived成分沒被銷毀。如果getTimeKeeper返回指針指向一個AtomicClock對象,其內的AtomicClock成分(也就是聲明于AtomicClock class內的成員變量)很可能沒被銷毀,而AtomicClock的析構函數也未能執行起來。然而其base class成分(也就是TimeKeeper這一部分)通常會被銷毀,于是造成一個詭異的“局部銷毀”對象。這會形成資源泄露、毀壞數據結構、浪費許多時間在調試器上。

消除這個問題的做法很簡單:給base class一個virtual析構函數。此后刪除derived class對象就會如你想要的那般,銷毀整個對象,包括所有derived class成分:

class TimeKeeper 
{
public:TimeKeeper();virtual ~TimeKeeper();
};TimeKeeper *ptk = getTimeKeeper();
// ...
delete ptk;    // 現在,行為正確

像TimeKeeper這樣的base classes除了析構函數之外通常還有其他virtual函數,因為virtual函數的目的是允許derived class的實現得以客制化(見條款34)。例如TimeKeeper就可能擁有一個virtual getCurrentTime,它在不同的derived class中有不同的實現。任何class只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數。

如果class不含virtual函數,通常表示它并不打算被用做一個base class。當class被用作base class,令其析構函數為virtual往往是個餿主意。考慮一個用來表示二維空間點坐標的class:

class Point 
{    // 一個二維空間點(2D point)
public:Point(int xCoord, int yCorrd);~Point();
private:int x, y;
};

如果int占用32bits,那么Point對象可塞入一個64-bit緩存器(用于臨時存儲數據的高速存儲設備。它通常位于處理器內部或者靠近處理器,并且比主存儲器更快)中,更有甚者,這樣一個Point對象可被當做一個“64-bit量”傳給以其他語言如C或FORTRAN撰寫的函數。然而當Point的析構函數是virtual,形勢起了變化。

欲實現出virtual函數,對象必須攜帶某些信息,主要用來在運行期決定哪一個virtual函數該被調用。這份信息通常是由一個所謂vptr(virtual table pointer)指針指出。vptr指向一個由函數指針構成的數組,稱為vtbl(virtual table);每一個帶有virtual函數的class都有一個相應的vtbl。當對象調用某一virtual函數,實際被調用的函數取決于該對象的vptr所指的那個vtbl——編譯器在其中尋找適當的函數指針。

virtual函數的實現細節不重要,重要的是如果Point class內含virtual函數,其對象的體積會增加:在32-bit計算機體系結構中將占用64 bits(為了存放兩個ints)至96 bits(兩個ints加上vptr);在64-bit計算機體系結構中占用128bits(兩個ints加上vptr,在64位系統中,int還是占32位),因為指針在這樣的計算機結構中占64 bits。因此,為Point添加一個vptr會增加其對象大小達50%~100%!Point對象不能再塞入一個64-bit緩存器,而C++的Point對象也不再和其他語言(如C)內的相同聲明有著一樣的結構(因為其他語言的對應物并沒有vptr),因此也就不再可能把它傳遞至(或接受自)其他語言所寫的函數,除非你明確補償vptr——那屬于實現細節,也因此不再具有移植性。

因此,無端地將所有classes的析構函數聲明為virtual,就像從未聲明它們為virtual一樣,都是錯誤的。許多人的心得是:只有當class內含至少一個virtual函數,才為它聲明virtual析構函數。

即使class完全不帶virtual函數,被“non-virtual析構函數問題”給咬傷還是有可能的。舉個例子,標準string不含任何virtual函數,但有時候程序員會錯誤地把它當做base class:

class SpecialString : public std::string    // 餿主意!std::string有個non-virtual析構函數
{// ...
};

乍看似乎無害,但如果你在程序任意某處無意間將一個pointer-to-SpecialString轉換為一個pointer-to-string,然后將轉換所得的那個string指針delete掉,行為就不正確了:

SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
// ...
ps = pss;    // SpecialString * => std::string *
// ...
delete ps;    // 未定義行為,現實中*ps的SpecialString資源會泄漏,因為SpecialString析構函數沒被調用

相同的分析適用于任何不帶virtual析構函數的class,包括所有STL容器如vector、list、set、tr1::unordered_map(見條款54)等等。因此不要繼承一個標準容器或任何其他“帶有non-virtual析構函數”的class(很不幸C++沒有提供類似Java的final classes或C#的sealed classes那樣的“禁止派生”機制)。

有時候令class帶一個pure virtual析構函數,可能頗為便利。pure virtual函數導致abstract(抽象) class——也就是不能被實體化(instantiated)的class。也就是說,你不能為那種類型創建對象。然而有時候你希望擁有抽象class,但手上沒有任何pure virtual函數,怎么辦?由于抽象class總是被當做一個base class來用,而又由于base class應該有個virtual析構函數,并且由于pure virtual函數會導致抽象class,因此很簡單:為你希望它成為抽象class的那個class聲明一個pure virtual析構函數。下面是一個例子:

class AWOV    // AWOV="Abstract w/o Virtuals"
{
public:virtual ~AWOV() = 0;    // 聲明為pure virtual析構函數
};

這個class有一個pure virtual函數,所以它是個抽象class,又由于它有個virtual析構函數,所以你不需要擔心析構函數的問題。但你必須要為這個pure virtual析構函數提供一份定義:

AWOV::~AWOV() { }    // pure virtual析構函數的定義

析構函數的運作方式是,最深層派生(most derived)的那個class其析構函數最先被調用,然后是其每一個base class的析構函數被調用。編譯器會在AWOV的derived classes的析構函數中創建一個對~AWOV的調用動作,所以你必須為這個函數提供一份定義。如果不這樣做,鏈接器會發出抱怨。

“給base classes一個virtual析構函數”,這個規則只適用于polymorphic(帶多態性質的)base classes身上。這種base classes的設計目的是為了用來“通過base class接口處理derived class對象”。TimeKeeper就是一個polymorphic base class,因為我們希望處理AtomicClock和WaterClock對象,縱使我們只有TimeKeeper指針指向它們。

并非所有base classes的設計目的都是為了多態用途。例如標準string和STL容器都不被設計作為base classes使用,更別提多態了。某些classes的設計目的是作為base classes使用,但不是為了多態用途。這樣的classes如條款6的Uncopyable和標準程序庫的input_iterator_tag(C++標準庫中定義的一個標簽類型,用于標識一個迭代器是輸入迭代器。它是一個空的結構體,通常用于迭代器的分類和特性判斷)(條款47),它們并非被設計用來“經由base class接口處置derived class對象”,因此它們不需要virtual析構函數。

請記住:
1.polymorphic(帶多態性質的)base class應該聲明一個virtual析構函數。如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數。

2.Classes的設計目的如果不是作為base classes使用,或不是為了具備多態性(polymorphically),就不該聲明virtual析構函數。

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

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

相關文章

DM數據庫學習之路(二十)DM8基于主備集群技術的兩地三中心集群部署及測試(全網最詳細)

DM兩地三中心介紹 摘要 金融行業對數據的可靠性和連續性有著極其嚴格的要求,任何數據丟失或服務中斷都可能導致嚴重的經濟損失。針對這一問題,基于達夢主備集群技術的兩地三中心解決方案能夠切實有效解決業務數據的可靠性和連續性需求。該方案通過構建兩個數據中心和一個災備…

MyBatis標簽獲取數組或者集合長度的方法

1、判斷列表長度&#xff1a; <if test"list ! null and list.size() > 0">... </if> 可結合in條件使用&#xff1a;SELECT * FROM users<where><if test"idList ! null and idList.size() > 0">id IN<foreach item"…

leetcode熱題100學習計劃-鏈表-相交鏈表

思路 兩條鏈表長短不一&#xff0c;找公共交點必須先對齊。記錄兩個鏈表各自長度&#xff0c;長的向短的看齊&#xff0c;長的先走多出來的那么一截&#xff0c;之后兩者一起走&#xff0c;直到相遇或抵達末尾 代碼 /*** Definition for singly-linked list.* public class …

解密Lawnchair:打造個性化極致的Android桌面體驗

解密Lawnchair&#xff1a;打造個性化極致的Android桌面體驗 1. 簡介 Lawnchair是一款知名的Android桌面定制工具&#xff0c;旨在為用戶提供個性化極致的桌面體驗。作為一個開源項目&#xff0c;Lawnchair融合了簡潔、靈活和強大的特點&#xff0c;讓用戶能夠自由定制其Andro…

Python | Conda安裝包報錯:PackagesNotFoundError

Conda在下載安裝包時報錯&#xff1a; PackagesNotFoundError: The following packages are not available from current channels:- XXXXXX&#xff08;包名&#xff09;有如下兩種解決方法&#xff1a; 方法一&#xff1a;將conda-forge添加到搜索路徑上 在命令行運行下方指令…

深入理解C語言:開發屬于你的三子棋小游戲

三子棋 1. 前言2. 準備工作3. 使用二維數組存儲下棋的數據4. 初始化棋盤為全空格5. 打印棋盤6. 玩家下棋7. 電腦下棋8. 判斷輸贏9. 效果展示10. 完整代碼 1. 前言 大家好&#xff0c;我是努力學習游泳的魚&#xff0c;今天我們會用C語言實現三子棋。所謂三子棋&#xff0c;就是…

Android 開發環境搭建的步驟

本文將為您詳細講解 Android 開發環境搭建的步驟。搭建 Android 開發環境需要準備一些軟件和工具&#xff0c;以下是一些基礎步驟&#xff1a; 1. 安裝 Java Development Kit (JDK) 首先&#xff0c;您需要安裝 Java Development Kit (JDK)。JDK 是 Android 開發的基礎&#xf…

TS總結10、ts的 class 類型(配置項strictPropertyInitialization、非空斷言)

一、簡介 1.類(class)是面向對象編程的基本構件,封裝了屬性和方法 1.1、屬性的類型:類的屬性可以在頂層聲明,也可以在構造方法內部聲明,如果不給出類型;TypeScript 會認為x和y的類型都是any;如果聲明時給出初值,可以不寫類型,TypeScript 會自行推斷屬性的類型; c…

【Android 內存優化】怎么理解Android PLT hook?

文章目錄 前言什么是hook?PLT hook作用基本原理PLT hook 總體步驟 代碼案例分析方案預研面臨的問題怎么做&#xff1f;ELFELF 文件頭SHT&#xff08;section header table&#xff09; 鏈接視圖&#xff08;Linking View&#xff09;和執行視圖&#xff08;Execution View&…

2核4G服務器咋收費的?阿里云貴不貴?

阿里云2核4G服務器多少錢一年&#xff1f;2核4G配置1個月多少錢&#xff1f;2核4G服務器30元3個月、輕量應用服務器2核4G4M帶寬165元一年、企業用戶2核4G5M帶寬199元一年。可以在阿里云CLUB中心查看 aliyun.club 當前最新2核4G服務器精準報價、優惠券和活動信息。 阿里云官方2…

YOLO-World 簡單無需標注無需訓練直接可以使用的檢測模型

參考: https://github.com/AILab-CVC/YOLO-World YOLO-World 常規的label基本不用訓練,直接傳入圖片,然后寫入文本label提示既可 案例demo: 1)官方提供 https://huggingface.co/spaces/stevengrove/YOLO-World https://huggingface.co/spaces/SkalskiP/YOLO-World 檢測…

基于信息間隙決策理論的碳捕集電廠優化調度程序代碼!

適用平臺&#xff1a;MatlabYalmipCplex 程序在建立電廠與碳捕集裝置協同調度模型的基礎上&#xff0c;引入信息間隙決策理論(information gap decision theory, IGDT)以同時滿足系統的魯棒性和經濟性要求&#xff0c;通過風險追求和風險規避&#xff12;種決策角度得到不同的…

移動端1px問題,使用vant配合rem后需要處理成1.5px或者2,3,等等,不然ios上顯示不出來1px的邊框

table{td {border: 1.5px solid #ccc;font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 24px;color: #4E5464;line-height: 28px;text-align: center;empty-cells: show;padding: 20px 10px;height: 80px;white-space: nowrap;} }table的td樣式&#xff0c…

93. 復原 IP 地址(力扣LeetCode)

文章目錄 93. 復原 IP 地址題目描述回溯算法回溯優化&#xff08;在原s字符串上操作&#xff09; 93. 復原 IP 地址 題目描述 有效 IP 地址 正好由四個整數&#xff08;每個整數位于 0 到 255 之間組成&#xff0c;且不能含有前導 0&#xff09;&#xff0c;整數之間用 ‘.’…

真不愧是華為出來的,真的太厲害了。。。

&#x1f345; 視頻學習&#xff1a;文末有免費的配套視頻可觀看 &#x1f345; 點擊文末小卡片&#xff0c;免費獲取軟件測試全套資料&#xff0c;資料在手&#xff0c;漲薪更快 實習去了博彥科技&#xff08;外包&#xff09;&#xff0c;做的就是螺絲釘的活&#xff0c;后面…

華為---MSTP(一)---MSTP生成樹協議

目錄 1. MSTP技術產生背景 2. STP/RSTP的缺陷 ?編輯 2.1 無法均衡流量負載 2.2 數據使用次優路徑 3. MSTP生成樹協議 3.1 MSTP相關概念 3.2 MSTP樹生成的形成過程 4. MSTP報文 1. MSTP技術產生背景 RSTP在STP基礎上進行了改進&#xff0c;實現了網絡拓撲快速收斂。但…

chisel入門初步2_2——-1/2次方生成器

由之前的GCN網絡的介紹可以得知&#xff0c;我們需要輸入兩個乘數&#xff08;兩個節點的節點度&#xff09;&#xff0c;并輸出他們乘積的-1/2次方&#xff0c;此處由于當時設計的booth編碼的乘法器為有符號數&#xff0c;而此處是無符號數&#xff0c;實在懶得再寫一份了&…

SpringBoot+Maven項目打包

項目的主POM文件里面添加maven打包插件 <build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.2</version><configuration><sour…

推薦一款新的自動化測試框架:DrissionPage

今天給大家推薦一款基于Python的網頁自動化工具&#xff1a;DrissionPage。這款工具既能控制瀏覽器&#xff0c;也能收發數據包&#xff0c;甚至能把兩者合而為一&#xff0c;簡單來說&#xff1a;集合了WEB瀏覽器自動化的便利性和 requests 的高效率優點。 一、DrissionPage框…

【C++庖丁解牛】默認成員函數

&#x1f4d9; 作者簡介 &#xff1a;RO-BERRY &#x1f4d7; 學習方向&#xff1a;致力于C、C、數據結構、TCP/IP、數據庫等等一系列知識 &#x1f4d2; 日后方向 : 偏向于CPP開發以及大數據方向&#xff0c;歡迎各位關注&#xff0c;謝謝各位的支持 目錄 前言1. 構造函數1.1 …