我的關於Effective C++的筆記。javascript
未徹底根據 C++1114 進行修正,待更新html
能夠參考怎麼樣纔算是精通 C++? - vzch的回答 - 知乎java
#define
常量:用 const
代替c++
函數:用inline
代替git
類常量: 用const
, enum
代替github
const
const
的對象: 通常變量, 引用參數,指針,返回值、成員函數shell
養成能寫const
就添加const
關鍵字的習慣,嚴謹定義對象的性質。數據庫
在const
和 非const
函數有相同實現時,在非const
函數實現中複用const
函數實現。後端
而在Java中,僅當成員方法、方法參數、lambda方法中引用的變量中須要特別強調不可變的性質時才須要聲明
final
關鍵字,通常狀況下不須要使用final
,對一個方法中的對象參數聲明爲final
幾乎沒有意義。數組在Javascript 中,也儘可能使用
const
和let
代替var
每個類型的構造函數須要確保已對全部成員變量初始化,特別是,子類須要經過調用父類的對應函數確保父類部分的完整初始化行爲。
不一樣於Java, 應使用成員初始化列表而不是在構造函數中使用賦值操做。
靜態(全局)對象使用函數包裹,以函數方式(接口)提供靜態對象的訪問。
複製行爲包括copy ctor
, operator=
若肯定不容許/不須要複製行爲時,應禁用copy ctor
, operator=
,(經過private
或 delete
)
若肯定容許/須要複製行爲時,仔細定義完整的複製行爲,參見 12
對基類的析構函數聲明爲virtual ~Clazz() = 0
,能夠防止基類被實例化。
若是不是用做基類,則不該聲明析構函數爲virtual。
不要繼承沒有聲明爲虛析構函數的類,特別是,不要繼承任何標準庫類,請使用組合而非繼承。
(在ES6中也不要繼承任何內置對象,包括Error)
儘量不使用異常。
不在析構函數上拋出異常,若是可能出現異常,必須在析構函數中進行捕獲和處理。不然,析構數組時可能出現內存泄露和不一致數據。
不使用異常規格(exception specification)
一些關於異常的使用觀點 https://www.zhihu.com/questio...
ctor
、dtor
調用virtual
方法因爲在父類構造析構函數調用在子類構造析構以前,子類未完成初始化,virtual方法會調用父類行爲,C++不會私自調用未定義的行爲,所以不會調用子類virtual方法。
子類初始化邏輯應在子類構造中明肯定義。
Java旨在維持繼承概念的完整性,在父類構造函數能夠調用到子類的virtual方法,但仍不推薦在構造函數中調用子類virtual方法,由於子類部分仍未初始化。
// An example that the base class invoke the devired class in Java class Base { public Base() { System.out.println("Base::Base()"); virt(); } void virt() { System.out.println("Base::virt()"); } } class Derived extends Base { public Derived() { System.out.println("Derived::Derived()"); virt(); } void virt() { System.out.println("Derived::virt()"); } }
// Output Base::Base() Derived::virt() Derived::Derived() Derived::virt()
=operator
實現的正確姿式標準的函數聲明,rhs
means 'right hand side':
Clazz& operator=(const Clazz& rhs);
=operator
函數聲明應返回賦值對象的引用,保持連續賦值的語義。返回語句:
return *this;
考慮參數rhs
和自身對象是同一個引用的case
=operator
正確行爲應該是,先複製參數對象的數據,後刪除本身對象的數據。
這屬於一種新舊對象的處置過程思想,尤爲當舊的對象須要作出必定處理時,(若是不須要處理就隨便了)如:
在緩存池中,緩存空間滿時,新的待緩存對象須要選擇剔除一個已緩存對象以騰出空間,待剔除的緩存對象由於可能在緩存期間進行了更新,須要寫入這些更新到後端(如數據庫)中以保持一致性。則這些對象的寫入規則應該是:(1) 將待刪除緩存對象,(2)持久化到後端,(3)等到持久化完成時才寫入新的緩存對象,也即在持久化期間須要對這個緩存空間加鎖。而不是先刪除舊緩存,緩存新對象,再持久化,不然,將會致使舊的後端數據又在緩存中,致使數據不一致。
適當調用父類的=operator
函數,見12
複製行爲包括copy ctor
, operator=
完整行爲包括:(1)經過調用父類的對應函數確保父類的完整複製行爲。(2)完整處理當前每個成員變量
C++ 和Java一個明顯不一樣點是須要明確對象的資源全部權,資源通常指所佔內存,也包括文件、流、鎖等,全部權決定了當對象結束使用時銷燬其資源的義務。
std::vector<Fruit> fruits;
vector
中的fruit對象的全部權屬於fruits,fruits負責對fruit的資源管理義務,即fruits被銷燬時,fruits銷燬全部數組中的元素
std::vector<*Fruit> fruits
fruits僅對*Fruit
變量(指針)所佔資源負責,fruits不負責對fruit的資源管理義務,即fruits被銷燬時,fruits不會銷燬指向的元素。對應的,應是調用的fruits.push_back(&fruit)
的對象(或函數)擁有對fruit
的資源管理義務。
解決對象全部權處理(內存管理)的基本思路是,設計一個在棧上的對象,並將該對象和動態內存的對象關聯起來,因爲棧上的對象總會在函數或做用域結束時被銷燬(調用析構函數),所以只要在此對象中的析構函數實行對動態內存對象的管理操做,便可完成全部權的管理。這些處理全部權的對象即shared_ptr
, unique_ptr
。
RAII的核心是,當對象被建立時,其生命週期也被準肯定義,一定存在一個肯定條件,使得對象資源在知足條件時必定會被回收處理, 且肯定條件一定可達。
使用單獨的語句聲明建立管理資源對象(make_shared()等)。 不要與其餘函數調用等語句複合。(如 getCat(make_shared<int>(42), init())
, 若init
發生異常拋出時將可能致使內存泄露)
在管理資源對象中良好封裝被管理資源的delete
操做,不要暴露到外部,不然可能會出現兼容性問題。
一個典型的RAII特性使用的例子是:不須要對流(istream
,ostream
)顯式調用close
, stackoverflow
通常狀況下應禁用operator=
和copy ctor
,C++11以前使用private
限定訪問符,C++11以後使用delete
關鍵字
若能夠複製,則明確複製的行爲:(1)僅複製引用 (2)深度複製 (3)轉移全部權
經過operator->
訪問對應被管理對象的公開屬性和方法。
經過get()
得到原始對象的指針。
經過隱式轉換 operator T()
(不建議任何隱式行爲)
delete []
對動態分配的數組進行析構給定一個指針p,系統沒法知道p指向一個對象仍是指向一個對象數組,只能經過調用不一樣的delete operator
(delete
和delete[]
)間接告訴系統須要釋放的行爲。
只要向指針p調用delete[]
, 系統即知道須要釋放數組空間,並且也知道分配的內存大小。主流編譯器一般有兩種方法記錄數組的元數據。
over-allocation:大部分編譯器採用此方法實現,使用即另外再分配一段空間專門存儲數組的元信息。一般放置在對象分配空間的前面,注意此時傳入operator delete[]
的指針會指向元數據開始處(比第一個對象的分配地址更低的地址),由於元數據自己的空間也須要回收。
associative array:專門設置一個內置對象(如arrayLengthAssociation
)存儲全部動態分配數組的元信息。
不要對數組對象使用typedef
聲明類型別名,這會在使用別名時掩蓋了數組對象的實質。
對於數組對象的動態分配,建議使用vector<T>
代替。
儘可能使用自定義的類型封裝數據,限制合法輸入,並提供可讀的接口聲明。
接口的語義應該符合人的慣性思惟,特別地,要和語言內置的接口、類型聲明風格保持一致。
話雖這樣說,但我的認爲準官方日期庫date並無設計出易用的日期API,反而造出一堆須要理解的晦澀的學術概念,如
time_point
,duration
,system_clock
等(說明文檔在此),並暴露在API層上,使用前必須得先了解這些概念才能上手。歷來沒用過一個簡單需求的API能用得如此痛苦。嚴謹是一回事,易用是另外一回事。例如假設須要將一個int整形看成Unix時間戳並轉化爲一個日期對象,獲取年月日等的信息時,JavaScript 只須要:
let myDate = new Date(1487489218000); myDate.getYear();惟一注意的是時間戳的單位是毫秒,算是一個不足,可使用更好的第三方庫moment:
let day = moment.unix(1487489218); day.year();而C++中須要:
將
uint64_t
轉成duration<microseconds>
,告訴這個是以毫秒爲單位的。microseconds{ timestamp }從
duration
構造出時間點time_point
,duration
只是一個時間段的值,要設定基準點爲Unix系統時鐘system_clock
。const time_point<system_clock> datetime_point(microseconds{ timestamp });這時候還不能拿出年月日的數據,須要轉化爲年月日,還要告訴如何處理時分秒的數據,這裏把時分秒數據截斷,只要拿到年月日就行。
floor<days>(datetime_point)須要轉爲一個日期對象,是
Date
類型嗎?不是,是year_month_day
這麼一個名字:auto ymd = year_month_day(floor<days>(datetime_point));這時候終於能夠獲取年月日了,文檔還說明最好轉換爲
unsigned
類型,須要這麼寫:unsigned(ymd.month());這是若是須要獲取時分秒信息怎麼辦,很差意思,
year_month_day
就是年月日,沒有其餘信息,本身查文檔吧。這真的使人失去耐心。可能有提供更簡潔的方法,但至少根據文檔說明應該是這樣寫的。
另外,日期的構造聲明方法也破壞了概念的一致性,爲了實現聲明日期的簡潔化,擅自使用除號重載
/
做爲日期屬性分割符(date.h),又因爲重載符只能在自定義類型中使用,不能寫成2015/4/13
,只能寫成這樣的半成品:constexpr auto x1 = 2015_y/mar/22;還增長使用者的記憶負擔,年月日必須至少有一個須要以顯式類型聲明,而且分割符是
/
, 而不是.
或者\
。還不如好好地遵照C++構造函數的語法傳遞參數。