【Effective C++ 閱讀筆記】
前言
邁入 2021 年,今年給自己的目標就是讀兩本原文書,不然總有種原地踏步的感覺,其中一本是 Effective C++,另一本應該會選擇 STL 相關的書。
會選 Effective C++ 除了裡面很多平時會疏忽的知識外,就是它把整本書切割成 55 個小主題,在閱讀上也方便許多,大約 20~30 分鐘可以讀完一個,適合時間瑣碎時就拿來讀一下。
順帶一提,這是我的個人筆記,也就是內容並不完整,所謂筆記就是我主觀上覺得相對重要的部分,畢竟原文有 300 頁,不做筆記我甚麼都記不下來。
有時間的人也可以到天瓏帶一本原文,內文如果有誤的話也麻煩留言告知。
Accustoming Yourself to C++
1. View C++ as a federation of languages
請把 C++ 看做是許多程式語言的集合而非單一程式語言,主要由 C、Objected-Oriented C++、Template C++、STL 四種所構成,這四種子語言各有其特色。
也因 C++ 是四種語言的集合,不同狀況下會有不同的特性,比方說:C 裏頭的 Array 並不會自動初始化、但 STL 裡的 vector 卻會自動初始化成 0。
2. Prefer consts, enums, and inlines to #defines
盡量不要用 define,對於常數請用 const 或 enum 替換掉 define,函式請用 inline,原因有幾個:
- 無法使用 private 、 public 等被封裝
- 也沒有 scope 的概念
- 編譯器看不到導致後續吐出錯誤訊息時見不到識別字,而使除錯不便
另外提到下面這個有趣的例子:
#include <iostream>
#define CALL_WITH_MAX(a, b) f((a)>(b)?a:b)using namespace std;void f(int a){}int main()
{
int a = 5 , b = 0;
CALL_WITH_MAX(++a, b);
cout << "a = " << a << endl;
CALL_WITH_MAX(++a , b+10);
cout << "a = " << a << endl; return 0;
}
也就是在第一次 CALL_WITH_MAX 中,a 被加了 2 次,第二次 CALL_WITH_MAX 中,a 卻只被加了 1次,原因是置換後變成:
#include <iostream>
using namespace std;void f(int a){}int main()
{
int a = 5 , b = 0;
f((++a)>(b)?++a:b);
cout << "a = " << a << endl;
f((++a)>(b+10)?++a:b+10);
cout << "a = " << a << endl; return 0;
}
但如果寫成:
#include <iostream>
using namespace std;
void f(int a){}template<typename T>
inline void CALL_WITH_MAX(const T& a , const T& b){
f(a>b?a:b);
}int main()
{
int a = 5 , b = 0;
CALL_WITH_MAX(++a, b);
cout << "a = " << a << endl;
CALL_WITH_MAX(++a , b+10);
cout << "a = " << a << endl; return 0;
}
就不會有問題了。
對於常數請用 const 或 enum 替換掉 define,函式請用 inline。
3. Use const whenever possible
const 的是用就是針對 read-only 的修飾詞,那哪些狀況應該用 const?只要它是常數你就應該加。這樣可以降低使用錯誤造成的意外,
const 特別是用在函式的回傳值上,像是:
class Rational { ... };const Rational operator* (const Rational& lhs, const Rational& rhs);
這樣就可以避免以下錯誤:
Rational a , b, c;
(a*b) = c;
至於甚麼時候會發生這個錯誤呢?通常是在打 if 的時候少打了一個 =
if(a*b=c){...}
此外也區分 bitwise constness 跟 logical constness 這兩個概念,bitwise constness 代表不更動該變數的任何一個位元,而 logical constness 則允許在使用者不會發現的狀態下修改部分 bits,至於要怎麼讓某些特定變數可以修改呢? 利用 mutable 關鍵字。
class CTextBlock {
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const{
if (!lengthIsValid) {
textLength = strlen(pText);
lengthIsValid = true;
}
return textLength;
}
再來是如果類別底下有 const 與 non-const 函式有同樣實作的話,有可能造成無窮迴圈,因為 non-const 的 operator[] 如果再次呼叫 operator[],就會深陷其中無法自拔。
因此面這段程式碼會不斷呼叫產生 char& operator[](std::size_t position) 而產生無窮迴圈!
#include <iostream>class TextBlock{
private:
std::string text = "ABCDE";
public:
const char& operator[] (std::size_t position) const{
return text[position];
}
char& operator[](std::size_t position){
return (*this)[position]; //Error!!
}
};int main()
{
TextBlock book;
std::cout << book[0];
return 0;
}
解決方式就是讓裏頭的 non-const 函式有兩次轉型:const_cast 與 static_cast,先將 TextBlock& 轉型成 const TextBlock&,藉此呼叫 const 函式,接著再利用 const_cast 去掉回傳出的 const 的特性。
class TextBlock{
private:
std::string text;
public:
const char& operator[] (std::size_t position) const{
return text[position];
}
char& operator[](std::size_t position){
return
const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};
總結:
- 除非有改動的需求,不然請一律設定成 const。
- 編譯器遵守 bitwise constness,但可以利用 mutable 達到logical constness
- 如果類別底下有 const 與 non-const 函式有同樣實作的話,non-const 函式 應該進行轉型後呼叫 const 函式避免無窮迴圈。
4. Make sure that objects are initialized before they’re used
寫過 C/C++ 的人應該都知道如果宣告變數沒有初始化,之所以會這樣是因為減少運行時需要的資源:
int x;
也就是如果這樣宣告會發生不明確的狀況,在某些環境下讀取未初始化的變數甚至會造成程式直接終止。但如同開頭所說的, C++ 其實是四種語言的集合,由 C 語言貢獻的部分的確不會自動初始化,但 STL 的部分會,也就是下面這段 vector 是會被初始化的。
#include <iostream>
#include <vector>int main ()
{
std::vector<int> myVector;
myVector.resize(5); for (int i=0;i<5;i++){
std::cout << myVector[i] << " ";
}
return 0;
}
所以保持一個好習慣以避免不明確的定義,使用之前記得先將其初始化!
另外,類別 (class ) 底下建構式 (constructor) 嚴格來說並不是初始化 (initializations),而是賦值 (assignments)。原因是 C++ 的初始化會發生再呼叫建構式之前,也就是建構式裡執行的是賦值 (assignments)。
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address);
private:
std::string theName;
std::string theAddress;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address)
{
theName = name;
theAddress = address;
numTimesConsulted = 0;
}
要寫成初始化的話應該要這樣寫,效率會較高,原因是 theName 與 theAddress 在建構時會直接初始化,而不會像上面一樣先建構再賦值:
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address);
private:
std::string theName;
std::string theAddress;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address):
theName(name),
theAddress(address),
numTimesConsulted(0)
{}
但 int 型態因為是 built-in object ,所以 numTimesConsulted 無論是用哪種方式,花的資源都一樣。
即便不需要初始化,為了程式碼的統一,也應該要這樣寫:
class ABEntry{
public:
ABEntry();
private:
std::string theName;
std::string theAddress;
int numTimesConsulted;
};
ABEntry::ABEntry():
theName(),
theAddress(),
numTimesConsulted()
{}
再來是,如果有許多程式碼需要編譯時,這時候編譯的次序就相當重要,比方說我們有個標頭檔裏頭是這樣寫的:
filesystem.h
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
filesystem.cpp
std::size_t FileSystem::numDisks() const{
return 0;
}
很明顯的,如果你在 tfs 建構前就呼叫它就會出錯。因此若有另外一份程式碼長這樣:
directory.cpp
class Directory {
public:
Directory();
};
Directory::Directory(){
std::size_t disks = tfs.numDisks();
}
也就是 tfs 必須在 Directory 前被建構出來,否則就會報錯。但兩者在不同的 .cpp 檔內呀,要如何確定 tfs 在 Directory 前被初始化?
為了解決這件事情,把每個 non-local static 搬到自己的函式內。這裡補充一下 static 指的是生命週期跟著整個程式,當函式結束時仍然會存在直至程式結束,所以不存在於 stack 或 heap 中,而local static 與 non-local static 的差異:
- local static:第一次呼叫函式時才會出生,直至程式結束死亡
- non-local static:程式執行時就已經出生,直至程式結束死亡
這裡的 tfs 其實就是 non-local static,也就是改成下面這種寫法:
filesystem.h
class FileSystem {
public:
std::size_t numDisks() const;
};
filesystem.cpp
std::size_t FileSystem::numDisks() const{}
directory.cpp
FileSystem& tfs(){
static FileSystem fs;
return fs;
}class Directory {
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs().numDisks();
}
Directory& tempDir(){
static Directory td;
return td;
}
解決方式是把 non-local static 移到客製化的函式中,就像是上面把 tfs 宣告移到 FileSystem& tfs() 函式底下成為 local static ,如此一來就可以確保該函式被呼叫時,會一併初始化底下的 local static。也因為這樣通常這種 reference-returning 函式只有短短兩行。
總結:
- 使用 built-in-object 請記得初始化
- 在初始化類別時,請避免在建構式中賦值,而是使用 member initialization list 的方式實作
- 為避免不同程式碼在編譯時的次序問題,請用以 local static 代替 non-local static
工商
最後工商一下我下一期在台大資工訓練班的課 XD
↓C/C++資料結構↓
https://bit.ly/3251sIk
↓C/C++基礎↓
https://bit.ly/3jWmjDW
如果不方便來台北的話,目前也有跟Hiskio合作線上課程可以參考,不過目前只有基礎班的部分,進階班的話可能要今年中後才有辦法錄完了。
[C/C++基礎@Hiskio]
https://bit.ly/2IGvknO
有其他開課需求或家教的單位也可以找我,在這個大 Python 的時代教C/C++的我都快被時代淘汰掉惹 QQ。
這是我開的粉專,之後有系列文也會 Po 在下面
http://bit.ly/3evM7pk