Effective c++ 讀書總結 條款1
條款一:視C++為一個語言聯邦
目前C++已經是個多重泛型程式語言、它支援:面向過程、面向物件、函式形式、泛型形式、超程式設計形式。
正是因為c++的風格多變、也導致了c++這種語言的複雜程度越來越高。一個便於我們學習的方式就是將c++語言視為多種語言的集合、在使用某一種次語言的使用只需要遵循該次語言的程式設計風格和規則、這樣對於c++的學習和使用就會更加輕鬆。
c++一共包含四種次語言:
1、C語言部分:C語言是C++語言的基礎、區塊、語句、內建型別、指標等都是來自於C語言(沒有類、沒有模板、沒有異常,不能過載)
2、Object-Oriented C++:這部分屬於面向物件、封裝、繼承、多型。
3、Template C++:模板C++、這是C++泛型程式設計的部分、在C++中很多地方都使用template來設計介面和一些工具類、STL就是個基於template的程式庫、為程式設計提供便利。
4、STL:STL就是個模板程式庫、它包括一些容器、迭代器、演算法、使用它可使程式設計更加便捷。
條款二:儘量以const,enum,inline替換#define
1、全域性const變數替換#define定義
#define WITH_SIZE 1.6
這段程式碼屬於預處理期需要執行的、可能被前處理器移走了、但是在編譯期編譯器可能從來沒有見過WITH_SIZE、所以可能它沒有被加入到編譯符號表中、那麼就會產生編譯錯誤(但是很少出現這種情況)、由於前處理器盲目地把WITH_SIZE替換成1.6、這可能導致目標碼出現多份1.6 增加了碼量、而使用全域性常量則不會如此、所以儘量使用const double WithSize = 1.6;、這就是以編譯器替換掉前處理器。
使用const宣告指標時需要注意:
const char* p = &str; 表示p所指之物不可更改、也就是 (*p)[0] = '1' 錯誤
char* const p = &str; 表示p本身不可再指向其他地址、也就是 p = &str1 錯誤
const char* const p = &str; 都不可更改、星號左側所指之物、右側指標本身。
2、class專屬常量
#define WITH_SIZE 1.6 定義的替換在全域性有效、即使WITH_SIZE被定義在class中、不存在private 修飾的 #define形式(因為完全沒意義)、(可在某處使用#undef WITH_SIZE)這時可使用靜態常量成員解決:
//只有靜態整型常量才可以在類內直接初始化、當然char是可以的
class Test
{
private:
static const int index = 0;
static const double widthSize;
};
//static必須提供定義式、但是由於在宣告時已經初始化了、所以這裡不能再初始化
const int Test::index;
const double Test::widthSize = 1.6;
3、列舉型別可以類內設定其值
在有些情況一些static const 不能在類內初始化、例如上面的double widthSize;、如果使用static const作為陣列的長度、由於編譯器在編譯階段必須知道陣列長度、那麼static const的類外初始化則會在連結期完成、也就是會發生連結期錯誤、解決方案就是使用列舉型別:列舉常量是整型所以剛好滿足陣列的初始化要求:
class Test
{
private:
enum {Length = 5};
int array[Length];
};
其實列舉型別數值和#define 定義的整型數值是一樣的、它們都不能被取地址、不能被賦值、不佔用記憶體。
4、使用inline替換形如函式的#define替換
#define M(a,b) (a) > (b) ? (a) : (b)
int num = M(4,2); 則num = 4;
#define 的作用就是簡單的替換、在使用函式型的巨集定義時、對每一個引數必須使用小括號包圍、但是還是難以保證出現錯誤 使用template加inline就可達到與#define一樣的函式替換效果
template<class T>
inline T max(T a,T b)
{
return a > b ? a : b;
}
//行內函數適用於函式體小的、頻繁被呼叫的函式使用、在class中定義的函式都被隱式宣告為inline、但是至
//於編譯器是否遵從、還是依賴於函式體的大小、如果函式體過大、即使宣告為inline也不能像inline一樣替換
5、有了const、enum、inline我們對前處理器的需求變低了、但是#ifdef/#ifndef這種控制編譯的預處理語句、還扮演著重要的角色。
條款三:儘可能使用const
1、const的多種功能
可以用它在class外修飾global或namespace作用域中的常量、或修飾檔案、函式、區塊作用域中被宣告為static的物件、也可修飾class內的static/non-static物件、前面也提到了修飾指標的方式。
2、使用const修飾函式引數、返回值
//兩個函式都在函式體內修改引數
void fun(string str)
{
str[0] = '1';
}
void fun1(char* str)
{
str[0] = '1';
}
char fun2(const char* str)
{
return char();
}
int main()
{
fun("123"); //沒毛病、修改的是一個string物件、且string的長度超過0、若是str[4] = '4'則不可
fun1("123"); //相當於傳遞char* p = "123";字元常量區不能修改、發生錯誤、
//重點是錯誤可以通過編譯、所以當確認引數不可修改時應當將引數設定為const、這樣就算在函式中無意
//執行了修改語句、則在編譯期就會發現
fun2("123") = c;
//編譯通過、不過沒有意義對副本操作、在這行程式碼執行完、這個物件就被銷燬了、在以值作為返回值時
//都要加上const 修飾
}
//更改
void fun(const string& str)
{//使用引用消除引數拷貝的開銷、但是對於內建型別pass-by-value比pass-by-reference更好
.....
}
void fun1(const char* str)
{
str[1] = '1'; //編譯錯誤
}
const char fun2(const char* str)
{
return char();
}
3、const成員函式
const物件只能呼叫const成員函式、並且const函式內不可以對成員變數進行修改、當然const成員函式的返回值應該pass-by-value或者pass-by-const-reference傳遞、這樣可以保證不會改變成員變數。
兩個函式的常量性不同也被視為一種過載、常量物件只能呼叫常量函式、但是對於一些總是需要變化的變數來說、即使const物件也需要呼叫、可以使用mutable、告訴編譯器這個變數總是需要改變即使是在常量函式中、也就是允許常量函式改變mutable修飾的變數:
class Test
{
private:
char* str;
mutable size_t length;
mutable bool flag;
public:
size_t GetLength() const
{//常量物件第一次呼叫時、也會得到長度值
if(flag == false)
{
length = strlen(str);
flag = true;
}
return length;
}
};
4、避免const和non-const成員函式程式碼重複
例如使用操作符“[ ]”時:
class Test
{
public:
const char& operator[](size_t pos) const
{
if(pos > max_pos)
throw pos;
.... //其他操作
return str[pos];
}
char& operator[](size_t pos)
{//需要將*this轉換為const物件、是為了呼叫const版的[]運算子、避免程式碼重複
const char& c = (static_cast<const Test&>(*this))[pos];
return const_cast<char&>(c);
}
};
//在使用這種一個函式呼叫另一個函式避免重複時、一定要是non-const呼叫const不能相反
條款四:確定物件在使用前已先被初始化
1、內建型別與複合型別的初始化
對於複合型別的初始化就交由它的建構函式來完成、只需要記得在建構函式中將所有成員都初始化就可以了、內建型別在不同的作用域內會有不同的初始值、全域性的或在名稱空間中的整型變數預設初始化為0、而在其他位置它的初始值是不確定的。
2、認清初始化和賦值
對於內建型別來說在變數定義時就直接賦值的為初始化:int a = 0;(初始化)、int a;a = 0;(賦值)
在定義自定義型別時、需要將所有成員放入“類初始化列表”中、才可稱為初始化、
class Test
{
private:
string str;
int a;
double* p;
public:
Test();
};
Test::Test():str("123"),a(0),p(NULL) //初始化列表
{
//放在函式體中屬於賦值、在函式體中賦值則會經歷:預設構造->再賦值、而在初始化列表中只有:拷貝構造
//使用初始化列表效率更高、
}
如果變數是const修飾的或reference型、則必須使用初始化列表(因為不能對這些變數賦值、只能初始化)、另外、
c++有著固定的成員初始化次序、次序總是與成員宣告的順序相同、為了避免錯誤
({int a;char*b;} Test::Test():a(2),b(new char[a]));就是在初始化時不按順序誤認前面的變數會先被初始化而造成錯誤
3、不同編譯單元內定義的non-local static物件的初始化次序
所謂不同編譯單元是指產出單一目標檔案的那些原始碼、基本上它是單一原始碼檔案加上所包含的標頭檔案。
non-local static就是指那些在global或namespace中的static變數。
現在考慮兩個不在同一單元內的全域性static變數(為什麼一定要討論static物件? 因為non-static物件不存在這樣的問題、如果是一個non-static物件的初始化需要使用、另一個單元中的static物件、則一定是static物件先被初始化)、static物件在程式啟動時就存在了直到程式結束mian函式後static物件才被銷燬、:兩個static物件(不同單元)的初始化的先後順序是不能被確定的、所以在其中一個物件初始化時需要使用另一個物件時、可能發生不可預測的情況。
由於c++保證、函式內的local static物件會在函式第一次呼叫期間被初始化、所以如果以一個“函式呼叫”返回一個static物件的reference、替換直接訪問一個non-local static物件則可保證、物件一定被初始化了。
總之就是以函式內的local static替換全域性的non-local static物件、這也是設計模式中的單例模式的常用手法、另外、如果這個函式沒有被呼叫那麼就不需要支付構造和析構的開銷、但是在面對多執行緒呼叫時、還是會發生初始化“競速趨勢”、解決方案:可以在單執行緒階段、將各個類似這樣的函式都呼叫一遍就可以了。
條款五:瞭解C++默默編寫並呼叫哪些函式
1、構造、析構、拷貝構造、拷貝運算子
如果定義一個類、沒有手動宣告,構造、析構、拷貝構造、拷貝運算子。編譯器幫你建立預設的、
編譯器創造解構函式時它的virtual屬性是根據當前類的base的析構是不是virtual而確定的、
而有時編譯器預設的拷貝構造和拷貝運算子不能真正按照我們的意思定義:
class Test
{
private:
char str[100];
int a;
string c;
public:
Test(){}
Test(const Test& t):str(t.str),a(t.a),c(t.c)
{
//預設的拷貝構造使用這種方式、對成員變數進行初始化、而str(t.str);這會使this->str 和
//t.str會指向同一處空間、而其實我們要的是strcpy_s(str,100,t.str);、所以有時候重寫拷貝構造和拷貝運算子是必要的
}
};
然而面對class中定義了const或reference型別的物件的類、因為拷貝構造和拷貝運算子傳遞的都是const-reference型別、如果是reference變數完成初始化、那麼要是這個reference變數的值改變了引數的const reference物件也就改變了。
在使用編譯器預設拷貝運算子中需要向const物件進行復制。然而這是不合法的。
面對以上兩種情況、經過測試:第一種reference指向同一個物件的情況是存在的、預設生成的拷貝構造能夠完成、而如果成員中使用了const或reference那麼編譯器不會宣告拷貝運算子的、
class Test
{
public:
char& c;
const int num;
public:
Test(char& cc,int nNum):c(cc),num(nNum){};
~Test(){}
};
int main()
{
char c = '1';
Test t1(c,100);
Test t2(t1); //使用拷貝構造reference指向同一個物件
Test t3 = t1; //一樣的拷貝構造
t2 = t1; //拷貝運算子、編譯錯誤
}