More Effective C++
35個改善編程與設計的有效方法
- 只有深入了解C++編譯器如何解釋代碼, 才有可能用C++語言寫出健壯的軟件.
- C++的難學, 不僅在其廣博的語法, 語法背后的語義, 語義背后的深層思維, 深層思維背后的對象模型;
- C++4種不同的編程思維模型:
- 基於過程的程序設計(procedural-based);
- 基於對象的編程思想(object-based);
- 面向對象的編程思想(object-based);
- 范式模板的編程思想(generic paradigm).
- 要有效率, 又要有彈性, 又要有前瞻望遠, 又要回溯相容, 又要能治大國, 又要能烹小鮮, 學習起來不可能太簡單.
- C++4種不同的編程思維模型:
- 繼承(Inheritance)機制會引發指針或引用擁有兩個不同的類型: 靜態類型和動態類型.
- 指針或引用的靜態類型是指其聲明時的類型;
- 動態類型則由他們實際所指的對象來決定.
- 當分配了內存而沒有釋放它, 就存在內存泄漏問題(memory leak).
- 如果構造函數還申請了其他資源(文件描述符, 互斥量, 句柄, 數據鎖), 析構函數沒有釋放的話, 這些資源也會被泄漏掉(resource leaks).
基礎議題
- 指針(pointers), 引用(references), 類型轉換(casts), 數組(arrays), 構造函數(constructors).
仔細區別pointers和references
- 指針使用'*'和'->'操作符, 引用使用'.'操作符.
- 沒有所謂的空引用(null reference).
- 一個引用必須總代表某個對象.
- 未初始化的指針, 雖然有效, 但風險很高.
- 引用可能回避使用指針更具有效率.
- 指針可以被重新賦值, 指向另一個對象, 但引用卻總是指向它最初獲得的那個對象.
- 在不同時間指向不同對象的時候, 應該使用指針.
- operator[]一般需要使用引用.
最好使用C++轉型操作符
- 改變對象的常量性;
- 改變對象的類型;
- C++具有4個新的轉型操作符(cast operators):
- static_cast; --- 擁有與C舊式轉型相同的威力與意義.
- const_cast; --- 用來改變表達式中的常量性或變異性(volatileness).
- dynamic_cast; --- 用於繼承體系中, 將基類的引用或指針轉換為派生類的引用或指針.
- reinterpret_cast; --- 與編譯平台相關, 不具有移植性, 用於轉換函數指針類型. --- 函數指針轉換.
絕對不要以多態(polymorphically)方式處理數組
- 繼承的重要性質之一是: 用指向基類的指針或引用操作派生類對象.
- 多態是多重類型的意思.
- 通過基類指針刪除一個由派生類對象構成的數組, 其結構是未定義的.
- 多態和指針算術不能混用.
- 一個具體類最好不要繼承另一個具體類.
非必要不提供默認構造函數(default constructor)
- 默認構造函數的意義是在沒有任何外來信息的情況下將對象初始化.
- 一個類缺乏一個默認的構造函數, 當使用這個class時便會有些限制.
- 數組對象應該以相反順序析構掉.
- 對模板(template)而言, 被實例化的目標類型(instantiated)的目標類型必須得有一個默認構造函數.
- 添加無意義的默認構造函數(default constructors)會影響class的效率.
操作符
- 操作符(overloadable operators) 是可以被重載的.
對定制的"類型轉換函數"保持警覺
- C++允許編譯器在不同類型之間執行隱式轉換(implicit conversions).
- 默認把char 轉換為 int, 將short 轉換為 double.
- 單自變量構造函數和隱式類型轉換操作符.
- 隱式類型轉換是一個擁有奇怪名稱的成員函數, 關鍵詞operator之后加上一個類型名稱.
- 隱式類型轉換操作符的缺點: 它們的出現可能導致非預期的錯誤, 或非預期的函數調用, 卻很難發現.
- 以一個功能對等的另一個函數取代類型轉換操作符.
- string對象沒有到char* 隱式轉換的函數. 但提供一個c_str()顯示轉換函數.
- 單變量的構造函數很可能發生不容易發覺的隱式轉換.
- 使用關鍵字explicit, 編譯器就不能因隱式類型轉換的需要而調用它們.
- 編譯器不能轉換一個以上的用戶自定義類型隱式轉換行為.
- 代理類技術(proxy classes) --- 將基本類型重新簡單封裝為一個類.
- 允許編譯器執行隱式轉換, 害處多過好處.
區別自加(increment)/自減(decrement)操作符的前置和后置形式
-
后置有一個int的自變量, 默認為0.
-
后置函數一般都會產生一個臨時對象, 效率可能沒有前置函數效率高.
// 前置 UPint& UPint::operator++() { *this += 1; return *this; } // 后置 const UPint UPint::operator++(int) { UPint oldValue = *this; // 臨時變量 ++(*this); return oldValue; // 先取出在加. }
千萬不要重載&&, ||和逗號,操作符
- 一般表達式真假的確定以"驟死式"進行, 如果一旦該表達式的真假值確定, 即使表達式中還有部分尚未校驗, 整個評估工作仍然結束.
- 可以在全局或是每個類中重載&&和||操作符; 但函數調用語義將取代驟死式語義.
- C++語言規范並沒有明確定義函數調用動作中各參數的評估順序.
- 表達式如果含有逗號, 那么逗號左側先被評估, 逗號右側后背評估.
. .* :: ?: new delete sizeof typeid static_cast dynamic_cast const_cast reinterpret_cast // 以上操作符不能被重載
- 操作符重載的目的是要讓程序更容易被閱讀, 被撰寫和被理解.
了解各種不同意義的new和delete
- new操作符:
- 分配足夠的內存, 用來放置某類型的對象;
- 調用一個構造函數(constructor), 為分配的內存中的那個對象設定初值.
- placement new(放置new操作符):
- 針對一個已存在的對象調用其構造函數並無意義.
- placement new 允許在已經分配好的內存上進行對象的構造.
- 當程序運行在共享內存或(memory-mapped I/O), placement new函數將很有用.
new (buffer) Widget(widgetSize)
.
void * operator new(size_t, void *location) { return location; }
- placement new是C++標准程序庫的一部分, 如果使用placement new, 就必須用
#include <new>
頭文件. - 如果將對象產生於堆上(heap), 應使用new operator.
- 如果只分配內存就使用operator new.
- 如果在已分配(並擁有指針)的內存中構造對象, 應該使用placement new.
- 為了避免資源泄漏(resource leaks), 每個動態分配行為都必須匹配一個相應的釋放動作.
- operator new[]負責分配一個數組對象的空間, 通常稱為array new, 進行動態分配內存.
- 數組版的new operator必須為數組中的每個對象調用一個構造函數.
- new operator和delete operator都是內建操作符.
異常
- C語言中的setjmp和longjmp可能有異常捕獲的功能, 異常安全程序比較重要(exceptions-safe).
利用destructor(析構函數)避免資源泄漏
- 局部對象總是會在函數結束時被正確地析構. --- 在這種類的析構函數中調用delete操作符.
- 行為類似指針的對象被稱為智能指針(smart pointers).
- auto_ptr; --- 基本被棄用.
- shared_ptr; --- 共享指針, 引用計數為零就銷毀對象空間.
- weak_ptr; --- weak_ptr是用來解決shared_ptr相互引用時的死鎖問題. 弱引用不會增加引用計數.
- unique_ptr; --- unique_ptr 是一個獨享所有權的智能指針,它提供了嚴格意義上的所有權.
在構造函數(constructors)內阻止資源泄漏
- C++保證刪除null指針是安全的.
- 面對尚未完全構造好的對象, C++拒絕調用其對應的析構函數.
- C++不自動清理那些構造期間拋出異常(exceptions)的對象, 需要在構造函數中捕獲可能存在的異常.
- 最好把共享代碼抽出放進一個private的輔助函數內, 然后讓析構或構造函數都調用它.
- 智能指針shared_ptr可以幫助構造函數處理構造過程中出現的異常.
禁止異常(exceptions)流出destructors(析構)之外
- 兩種情況下析構函數會被調用:
- 對象正常狀態下被銷毀, 離開了其生存空間(scope)或是被明確刪除.
- 當對象被異常處理機制銷毀.
- 異常傳播過程中的棧展開機制(stack-unwinding).
- C++可能調用terminate函數, 結束掉程序.
- 全力阻止exceptions傳出析構函數之外:
- 它可以避免terminate函數在異常傳播過程的棧展開機制中被調用;
- 確保析構函數完成其應該完成的每一件事情.
了解拋出一個異常與傳遞一個參數或調用一個虛函數之間的差異
- 函數和異常的傳遞方式有三種:
- 值傳遞(by value);
- 引用傳遞(by conference);
- 指針傳遞(by pointers).
- 函數調用, 控制權最終會回到調用端, 當拋出一個異常, 控制權不會再回到拋出端.
- 捕獲異常不論其是以值或引用傳遞異常, 都會把異常進行一次拷貝, 交給catch子句手上的是一個異常副本.
- 一個對象被拋出作為異常, 總是會發生復制(copy). 即使是一個static對象, 也會發生拷貝.
- 所以異常傳遞是比較慢的.
- 復制行為由對象的賦值構造(copy constructor)函數完成, 而且賦值構造函數只針對靜態類型進行copy.
- 復制動作永遠是以對象的靜態類型為本.
- 函數調用將一個臨時對象傳遞給一個非const引用參數是不允許的, 但對異常卻是合法的.
- 不要拋出一個指向局部對象的指針.
- 隱式轉換一般不會發生在exceptions與catch子句中.
- 繼承體系中的類轉換
- 有型指針轉換為無型指針.
- 當調用一個虛函數, 虛函數采用best fit(最佳吻合)策略, 而異常處理機制采用first fit(最先吻合)策略.
- 絕不要將針對基類而設計的catch子句放在針對派生類設計的catch子句之前.
以by reference方式捕捉異常(exceptions)
- 如果異常對象被分配於heap(堆), 他們必須刪除, 否則便會泄漏資源.
- 4個標准的異常:
- bad_alloc --- 當無法滿足內存需求時會發出.
- bad_cast --- 當對一個引用施行dynamic_cast失敗時發出.
- bad_typeid --- 當dynamic_cast被實施於一個null指針時發出.
- bad_excepttion --- 適用於未預期的異常情況.
- 如果catch by reference(引用)可以避開對象刪除問題.
明智運用exception specifications(異常具現化)
- 編譯器只會對exception specifications(異常具現化)做局部性檢驗.
- 避免將exception specifications(異常具現化)放在需要類型自變量的template身上.
- 不應該將template和exception specifications(異常具現化)混合使用.
- 在函數傳遞之際檢驗exception specifications(異常具現化).
- 處理系統可能拋出的異常.
了解異常處理的成本
- 針對每個try語句塊, 都必須記錄對應的catch子句及能夠處理的異常類型.
- 異常使程序速度要慢3個數量級.
- 效率分析工具(profiler)可以分析程序效率.
效率
- 高性能算法和數據結構, 語言本身的效率.
謹記 80-20法則
- 一個程序80%的資源用在20%的代碼身上.
- 軟件中整體性能幾乎總是由其構成要素(代碼)的一小部分決定的.
- 遇到瓶頸, 用經驗, 用直覺猜往往是錯誤的做法. 程序的性能特質傾向高度的非直覺.
- 需要根據觀察或實驗識別造成痛心的那20%的代碼.
- 盡可能地以最多的數據來分析軟件.
考慮使用lazy evaluation(緩式評估)
- 拖延戰術的思想.
- 引用計數(reference count):
- 數據共享引起的唯一危機僅在某個字符串被修改時才發生.
- 在真正需要數據之前, 不必着急為某物拷貝一個副本, 在對某物進行寫操作時才進行拷貝副本.
- 區分讀和寫.
- lazy fetching(緩式取出):
- 只產生對象的一個外殼, 當對象內的某個字段被需要了, 程序才從數據庫中取回對應的數據.
- mutable關鍵字, 允許對const對象進行修改, 由編譯器支持.
- lazy expression evalution(表達式緩評估)
- 數值應用.
分期攤還預期的計算成本
- over-eager evaluation, 如果程序常常用到某個計算, 設計一份數據結構以便能夠及有效率地處理需求.(caching).
- 利用告訴緩存暫存使用頻率高的內容.
- caching是分期攤還預期計算成本的一種做法. 預先取出是另一種做法.
- 系統調用往往比進程內的函數調用慢.
- 較快的速度往往導致較大的內存, 空間交換時間.
- 較大對象比較不容易塞入虛內存分頁(virtual memory page)或緩存分頁(cache page).
- 對象變大可能會降低性能, 因為換頁活動會增加, 緩存命中率(cache hit rate)會降低.
了解臨時對象的來源
- C++中真正的所謂的臨時對象是不可見的, 並不會在源碼中出現.
- 當隱式類型轉換(implicit type conversions);
- 函數返回對象的時候.
- 如果對象傳遞給非const的引用參數, 隱式轉換是不會發生的.
- 因為臨時對象被修改不是程序所想看到的.
- 臨時對象可能很耗成本, 應盡可能地消除它們.
協助完成"返回值優化(RVO)"
- 以傳值(by value)方式返回對象, 背后隱藏的構造和析構將無法消除.
- C++允許編譯器將臨時對象優化, 使它們不存在.
- return value optimization, 返回值優化(返回的臨時對象將不會被馬上析構掉).
利用重載技術(overload)避免隱式類型轉換(implicit type conversions)
- 每個重載操作符必須獲得至少一個用戶定制類型的自變量.
- 重載int和自定義類型的函數, 避免int隱式轉換為自定義的類型.
考慮以操作符復合形式(op=)取代其獨身形式(op)
- 已復合形式實現單獨形式的操作符.
- 當面臨命名對象或臨時對象的抉擇時, 最好選擇臨時對象.
考慮使用其他程序庫
- 理想的程序庫應該小, 快速, 威力強大, 富有彈性, 有擴展性, 直觀, 可廣泛運用, 有良好支持, 使用時沒有束縛, 而且沒有bug.
- iostream程序庫比stdio程序庫具有類型安全特性(type-safe), 並且可擴充, 但效率沒有后者高.
了解虛函數, 多繼承, 虛基類, 運行時類型識別的成本
- 大部分編譯器使用虛表(virtual tables, vtbls)和虛表指針(virtual table pointers, vptrs)來處理對象動態類型的指針或引用.
- 虛表(vptl)通常是一個由函數指針構成的數組, 某些編譯器會以鏈表取代數組.
- 程序中的每個類聲明或繼承了虛函數, 都會存在一個虛表, 表中的每一項(條目)就是該類的各個虛函數實現的函數指針.
- 必須為每個擁有虛函數的類消耗一個虛表(vtbl)空間, 其大小視函數的個數而定, 每個類應該最多只有一個vbtl.
- 在每個需要vbtl的目標文件中產生䘝vbtl副本. --- 鏈接器剔除重復副本, 只留每個vbtl的單一實體.
- 更常見的做法是探勘式做法, vtbl被產於第一個非內聯, 非純虛函數定義式的目標文件中. 如果虛函數被聲明為inline, 這種方法行不通.
- 避免將虛函數聲明為inline.
- 虛表指針(vptr)指示每個對象相對於哪一個虛表(vtbl).
- 每個聲明有虛函數的對象內都隱藏着一個虛表指針, 被編譯器加入到編譯器才知道的位置.
- 每個擁有虛表指針的對象都會將付出一個額外的指針代價.
- 較大的對象意味着難以放入緩存分頁或虛內存分頁中, 可能會增加換頁活動.
- 較大的對象意味着難以放入緩存分頁或虛內存分頁中, 可能會增加換頁活動.
- 虛函數真正的運行時期成本發生在和inlining互動的時候. 虛函數不應該inline.
- 因為inline函數需要在編譯器將函數本體拷貝, 而virtual意味着等待, 直到運行期才知道運行誰.
- 多重繼承往往導致虛基類的需求(virtual base class), 會形成更復雜和特殊的虛表.
- 一個類只需一份RTTI信息(運行時類型識別), 當某種類型至少擁有一個虛函數, 才能保證檢驗該對象的動態類型.
- RTTI的設計理念根據類的虛表(vtbl)來實現的.
- RTTI的空間成本只需在每個類的虛表(vtbl)內增加一個條目, 即一個類型信息(type_info)對象空間.
技術
- 慣用手法(idioms)和模式(patterns).
將構造函數和非成員函數虛化
- 虛函數(virtual function)會造成因類型而異的行為.
- 虛構造函數(virtual constructors)很有用.
- 根據不同的輸入可能產生不同的對象.
- 虛copy構造函數(virtual copy constructor)會返回一個指針, 指向其調用者的一個新副本.
- 引用計數(reference count);
- copy-on-write(寫時才復制).
- 當派生類重新定義基類的一個虛函數時, 不再需要一定聲明與原本相同的返回類型.
- 將非成員函數的行為虛化
- 非成員函數(non-member functions)的行為視其參數的動態類型而不同.
限制某個類所能產生的對象數量
- 有時候資源有限, 必須限制能同時產生對象的數量.
- 允許零個或一個對象:
- 阻止對象創建的最簡單的方法是將其構造函數聲明為私有函數(private).
- 命名空間(namespace)可以阻止名稱沖突.
- 類中擁有的一個static對象的意思是: 即使從未被用到, 它也會被構造及析構.
- 函數擁有一個static對象時, 此對象在函數第一次調用的時候才產生, 函數沒有被調用的話, 就不會產生該對象.
- 函數每次調用時會檢查該對象是否已經被創建.
- 函數擁有一個static對象時, 此對象在函數第一次調用的時候才產生, 函數沒有被調用的話, 就不會產生該對象.
- C++中同一編譯單元中的static對象初始化順序有保證, 但不同編譯單元內的初始化順序沒有任何說明.
- 將一個對象聲明為static, 意味着只需要一份對象.
- 對於非成員函數聲明inlining內聯函數的話, 該函數會進行內部鏈接, 內部連接意味着目標代碼的copy.
- 所以千萬不要產生內含局部static對象的inline非成員函數.
- 不同對象的構造狀態
- 避免具體類繼承其他的具體類.
- 帶有私有構造函數的類不能被用來當作基類, 也不能被用來內嵌於其他對象內.
- 允許對象生生滅滅:
- 將引用計數與偽構造函數相結合起來.
- 這種技術可以用於任意指定個數對象的創建.
- 類的靜態成員必須進行義務性定義.
- 在類定義區內為靜態常量成員指定初值.
- 一個用來計算對象個數的基類(base class):
- 聲明一個基類作為對象計數所用 --- 引用計數技術(reference count).
- 模板類(class template).
- 私有繼承后, 基類成員都變成了私有成員.
- 為了恢復public訪問層, 可以使用using declaration(using聲明).
- 聲明一個基類作為對象計數所用 --- 引用計數技術(reference count).
要求(或禁止)對象產生於heap之中
- 要求對象產生與heap之中(Heap-Based Objects):
- 構造函數聲明為public, 析構函數聲明為private, 並且聲明偽析構函數.
- 一個類只有一個析構函數.
- 判斷某個對象是否位於Heap內:
new UPNumber(*new UPNumber)
會先new空間, 再調用構造函數.
- 程序的地址空間以線性序列組織而成, 其中stack(棧)高地址往低地址成長, heap(堆)由低地址往高地址成長.
- 利用局部變量在棧上的思想判定一個new的對象地址的關系.
- static對象, 全局變量, 命名空間內的對象, 即不放在stack中, 也不放在heap中, 他們被置於heap之下.
- 抽象混合式基類(abstract mixin base class), 是一個不能夠被實例化的基類.
- 多重或虛擬基類的對象可能擁有多個地址.
- 禁止對象產生於heap中:
- 對象被直接實例化;
- 對象被實例化為派生類內的基類成分.
- 對象被內嵌於其他對象之中.
- new operator總是調用operator new(可以自行聲明).
智能指針(smart pointer)
- 智能指針是一個看起來, 用起來, 感覺起來都像內建指針, 但是提供了更多機能的一種對象.
- 資源管理;
- 自動的重復寫碼工作.
- 以智能指針取代C++內建指針:
- 構造和析構: 何時被產生以及何時被銷毀.
- 賦值和復制(Assignment and Copying), 復制和賦值其所指對象, 執行所謂的深拷貝(deep copy).
- 解引用(Dereferencing): 智能指針有權決定所指之物發生了什么事情.
- 采用lazy fetching方法.
- 遠程過程調用(remote procedure calls, RPC).
- 只能指針的構造, 賦值和析構
- 只有當確定要將對象所有權傳遞給函數的某個參數時, 才應該以by value方式傳遞auto_ptrs.
- 實現解引操作符(Dereferencing Operators):
- 返回引用值.
- 測試智能指針是否為null:
- 提供一個隱式類型轉換操作符來進行測試.
- 將智能指針(smart pointers) 轉換為內建指針(Dumb Pointers).
- 不要提供對內建指針的隱式轉換操作符, 除非不得已.
- 智能指針(Smart Pointers)和繼承有關的類型轉換
- 每個只能指針有個隱式類型轉換操作符, 用來轉換至另一個只能指針類.
- 函數調用的自變量匹配規則;
- 隱式類型轉換函數;
- template函數的暗自實例化;
- 成員函數模板(member function templates)等技術.
- 智能指針與const:
- const用來修飾被指之物, 或是指針本身, 或是兩者都可以. 智能指針也具有同樣的彈性.
- 對於智能指針只有一個地方可以放置const: 只能放置與指針身上, 不能置於所指的對象.
- non-const轉換至const是安全的, 從const轉換至non-const則不安全.
- 自己實現的智能指針不容易實現, 了解和維護.
引用計數(Reference Counting)
- 引用計數(Reference counting)允許多個等值對象共享同一實值.
- 引用計數可以消除記錄對象擁有權的負荷, 因為當對象運用引用計數, 它便擁有自己, 一旦不在有任何人使用它, 它便自動銷毀自己.
- 許多對象有相同的值, 如果這些值重復出現肯定不夠高效. 讓所有等值對象共享一份實值.
- 引用計數(Reference counting)的實現:
- 保存一個引用計數值.
- 寫時才復制(Copy-on-Write):
- 各個進程之間往往允許共享某些內存分頁(memory pages), 直到它們打算修改自己的那一頁.
- 一個引用計數(reference counter)基類:
- 任何類如果不同的對象可能擁有相同的值, 都可以使用引用計數的技術.
- 讓一個嵌套類繼承另一個類, 而后者與外圍類完全無關.
- 將引用計數加到已有的類身上.
- 加上一層間接的封裝 --- 計算機科學領域大部分問題得以解決.
- 引用計數是一個優化技術, 其使用前提是: 對象常常共享實值.
替身類, 代理類(Proxy classes)
- 凡是用來代表(象征)其他對象的對象, 常被稱為proxy object(替身對象), 替身對象的類稱為代理類.
- 二維數組是觀念上並不存在的一維數組.
- 讀取動作是所謂的右值運用(rvalue usage); 寫動作是所謂的左值運用(lvalue usages).
- 返回字符串中字符的proxy, 而不返回字符本身.
- 對於一個proxy, 只有3間事情可做:
- 產生它;
- 以它作為賦值動作的目標(接收端).
- 以其他方式使用它.
- Proxy 類很適合用來區分operator[]的左值運用和右值運用.
- 對proxy取址所獲得的指針類型和對真是對象取址所獲得的指針類型不同.
- 用戶將proxy傳遞給接受引用到非const對象的函數.
- ploxies難以完全取代真正對象的最后一個原因在於隱式類型轉換.
- proxy 對象是一種臨時對象, 需要被產生和被銷毀.
- 類的身份從與真實對象合作轉移到與替身對象(proxies)合作, 往往會造成類語義的改變, 因為proxy 對象所展現的行為常常和真正的行為有些隱微差異.
讓函數根據一個以上的對象類型來決定如何變化
- 面向對象函數調用機制(mutil-method): 根據所希望的多個參數而虛化的函數; --- C++暫時不支持.
- 消息派分(message dispatch): 虛函數調用動作.
- 虛函數+RTTI(運行時期類型辨識):
- 虛函數可以實現single dispatch, 利用typeid操作符來獲取一個類的類型參數值.
- 虛函數被發明的主要原因:
- 把生產及維護"以類型為行事基准的函數"的負荷, 從程序員轉移給編譯器.
- 只用虛函數:
- 將double dispatching以兩個single dispatches(兩個分離的虛函數調用)實現出來:
- 一個用來決定第一對象的動態類型.
- 另一個用來決定第二對象的動態類型.
- 編譯器必須根據此函數所獲得的自變量的靜態類型(被聲明時的類型), 才能解析出哪一組函數被調用.
- 將double dispatching以兩個single dispatches(兩個分離的虛函數調用)實現出來:
自行仿真虛函數表格(virtual function tables):
-
編譯器通常通過函數指針數組(虛表, vbtl)來實現虛函數:
- 決定正確的vbtl索引;
- 調用vbtl中的索引位置內所指的函數.
-
定義一個函數指針來處理不同類型的對象.
typedef void (SpaceShape::*HitFunctionPtr)(GameObject&);
. -
一定要預先把自行仿真的虛函數表格(virtual function table)進行初始化.
-
多重繼承中的A-B-C-D菱形繼承體系應該盡量避免.
-
使用非成員(non-member)函數的碰撞處理函數
- 匿名命名空間(namespace)內的每樣東西對其所駐在的編譯單元(文件)而言都是私有的, 其效果就好像在文件里頭將函數聲明為static一樣.
-
繼承 + 自行仿真的虛函數表格
- 擴大繼承體系時, 必須要求每個人重新編譯.
-
產生一個注冊類, 處理不同的情況.
雜項討論
在未來時下發展程序
- 好的軟件對於變化有很好的適應能力.
- 為每個類處理assignment和copy constructor動作.
- 努力寫出可移植性的代碼.
- 只要有人刪除B*, 而它實際上指向D, 便表示需要定義一個虛析構函數(virtual destructor).
- 如果多繼承體系中有任何析構函數, 就應該為基類聲明一個虛析構函數.
- 未來思維模式:
- 提供完整的classes;
- 設計接口, 是有利於共同的操作行為, 阻止共同的錯誤;
- 盡量使代碼一般化(泛化), 除非有不良的巨大后果.
- 增加代碼的重用性, 可維護性, 健壯性.
將尾端類(non-leaf classes)設計為抽象類(abstract classes)
- 聲明函數為純虛函數, 並非暗示它沒有實現碼, 而是意味着:
- 目前這個類是抽象的;
- 任何繼承此類的具體類, 都必須將該純虛函數重新聲明為一個正常的虛函數.
- 面向對象設計的目標是辨識出一些有用的抽象性, 並強迫他們稱為抽象類.
同一程序中結合C++和C
- 結合C++和C程序需要考慮的問題:
- 名稱重整(name mangling):
- 名稱重整(name mangling)是C++中的一種程序, 為每個函數編出獨一無二的名稱.
- 絕不要重整其他語言編寫函數的名稱.
- 壓制名稱重整(name mangling), 必須在C++中使用
extern "C" { ... }
指令. --- 進行C連接. - 不同編譯器以不同的方法進行重整名稱.
- static的初始化:
- 在main之前執行的代碼: static class對象, 全局對象, namespace內的對象, 文件范圍(file scope)內的對象, 其構造函數都在main函數之前執行.
- 動態內存分配:
- C++中使用new和delete, C中使用malloc和free.
- 數據結構的兼容性:
- structs可以安全地在C++和C之間往返.
- 名稱重整(name mangling):
- 在同一程序中混用C++和C, 應該記住以下幾個簡單規則:
- 確定C++和C編譯器產出兼容的目標文件(object file).
- 將雙方都使用的函數聲明為extern "C".
- 如果可能, 盡量在C++中撰寫main.
- 總是以delete刪除new返回的內存, 總是以free釋放malloc返回的內存.
- 將兩個語言間的數據結構傳遞限制於C所能了解的形式; C++structs如果內涵非虛函數, 倒是不受此限制.
讓自己習慣於標准C++語言
- 新的語言特性:
- RTTI, 命名空間(namespace), bool, 關鍵字mutable, 關鍵字explicit, enums作為重載函數的自變量所引發的類型晉升轉換, 在類中為const static成員變量設定初值.
- STL(standard template library) --- C++標准程序庫中最大的組成部分.
- 迭代器(iterators)是一種行為類似指針的對象, 針對STL 容器而定義.