C++進階_Effective_C++第三版(一) 讓自己習慣C++ Accustoming Yourself to C++

1、視C++爲一個語言聯邦

View C++ as a federatiom of languages.
將C++視爲一個有相關語言組成的聯邦而非單一語言,在其某個次語言(sublanguage)中,各種守則與通例都傾向簡單、直觀易懂、並且容易記住。然而當你從一個次語言移往另一個次語言,守則可能改變。次語言共有4個:

  • C:C++仍是以C爲基礎的。區塊(blocks)、語句(statements)、預處理器(preprocessor)、內置數據類型(built-in
    data types)、數組(arrays)、指針(pointers)等都來自C。許多時候C++對問題的解法其實不過是較高級的C解法。
  • Object-Oriented C++:這部分爲C with
    Classes,classes(包括構造函數和析構函數),封裝(encapsulation)、繼承(inheritance)、多態(polymorphism)、virtual函數(動態綁定)等等。這部分是面向對象設計的古典守則在C++上的最直接實施。
  • Template C++:爲++的泛型編程(generic
    programming)部分,Template的相關考慮與設計已經瀰漫整個C++,它爲C++帶來了嶄新的編程範型(programming
    paradigm)。TMP相關規則很少與C++主流編程互相影響。
  • STL:是個template程序庫,對容器(containers)、迭代器(iterators)、算法(algorithms)以及函數對象(function
    objects)的規則有極佳的緊密配合與協調。

當從某個次語言切換到另一個,導致高效編程守則要求也需要改變,如對於內置類型而言,pass-by-value通常比pass-by-reference高效,但是Object-Oriented C++中由於用戶自定義構造函數和析構函數的存在pass-by-reference-to-const更高效,使用Template C++也是如此。但是STL中對於迭代器和函數對象而言,pass-by-value更高效,這是因爲迭代器和函數對象都是在C指針上塑造出來的。

  • C++高效編程守則視狀況而變化,取決於你使用C++的哪一部分。

2、儘量以const,enum,inline替換#define

Prefer consts,enums,and inlines to #define
讓編譯器替換預處理器,如下面
#define ASPECT_RaTIO 1.653
記號名稱ASPECT_RaTIO在編譯器處理源碼之前就會被預處理器移走,此記號名稱ASPECT_RaTIO無法加入到記號表(symbol table)內,對於使用此常量的地方都會替換爲1.653,如果出現編譯錯誤,錯誤信息會提到1.653而不是ASPECT_RaTIO,而且常量一般定義在頭文件中,如此很難地位問題的位置。
解決辦法爲用一個常量替換上述的宏(#define):
const double AspectRatio = 1.653;
作爲一個常量,編譯器肯定可以看到,也會進入到記號表中。而且使用常量可比使用#define生成較小量的碼,因爲預處理器會盲目的將宏名稱ASPECT_RaTIO替換成1.653,導致目標碼出現多份1.653。
對於已常量替換#define時,有兩種特殊情況。
第一是定義常量指針(constant pointers)由於常量定義式通常被放在頭文件內,因此需要將指針聲明爲const,如果char*- based是字符串,則應該寫成如下:
const char * const AuthorName = “Socott Meyers”;
更好的寫法爲:const std::string AuthorName(“Socott Meyers”);
第二是class專屬常量。爲了將常量的作用域(scope)限制於class內,必須讓它成爲class的一個成員(member);而爲確保此常量至多隻有一份實體,必須爲一個static成員:

class GamePlayer{
private:
	static const int NumTurns = 5;
	int scores[NumTurns];
};

但是目前看到的是NumTurns的聲明式而非定義式。通常C++要求對所有使用的任何東西提供一個定義式,但如果它是個class專屬常量又是static且爲整數類型(integral type,例如ints,chars,bools),則需要特殊處理。只要不取它們的地址,可以聲明並使用它們而無須提供定義式。但如果你取某個class專屬常量的地址,或者編譯器需要提供一個定義式,則必須另外提供定義式如下:
const int GamePlayer:: NumTurns;
此語句應放在實現文件中,而且由於在定義時以對其設置了初值,此處不可以再設初值。
比較舊的編譯器有可能不支持此語法,應該改爲如下語句:

class GamePlayer{
private:
	static const int NumTurns;
};

const int GamePlayer:: NumTurns = 5;
但是如果在class編譯期間需要一個class常量值,此時可以使用枚舉類型來實現,如下:

class GamePlayer{
private:
	enum {}NumTurns = 5 };
	int scores[NumTurns];
};

這樣實現由於取一個enum的地址是不合法的,而取#define的地址通常也不合法,如此則更加完美的替代了#define。
對於使用#define另一個誤用情況爲以它實現宏(macros)。宏看起來像函數,但不會產生函數調用(function call)帶來的額外開銷。如下比較兩個數大小的例子:
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
此寫法會很難讓人看懂,而且必須給每個實參加上小括號,否則會出現一些奇怪的錯誤,但是即使加上小括號也會出現一些奇怪的錯誤如下:

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);      //a被累加2次
CALL_WITH_MAX(++a, b+10);   //a被累加1次

對於此種情況可以使用template inline函數改寫:

template<typename T>
inline void callWithMax(const T& a, const T& b)
{
	f(a > b ? a : b);
}

這個template產出一整羣函數,每個函數都接受兩個同類型對象,並以其中較大者調用f,這裏不需要給參數加上括號,也不需要操心參數被覈算多次等等問題。此外由於callWithMax是個真正的函數,它遵守作用域(scope)和訪問規則。例如可以寫一個class內的private inline函數,一般宏無法實現。
雖然有了consts、enums和inlines,但是對預處理器(特別是#define)的需求降低了,但是並未完全消除。#include仍然是必需品,而#ifdef、#ifndef還會繼續扮演控制編譯的重要角色。

  • 對於單純常量,最好以const對象或enums替換#defines。
  • 對於形如函數的宏(macros),最好改用inline函數替換#defines。

3、儘可能使用const

Use const whenever possible
const允許你指定一個語義約束(就是指定一個“不該被改動”的對象),而編譯器會強制實施這項約束。它允許你告訴編譯器或者其他程序員某個值應該保持不變。
可以用在classer外部修飾global或者namespace作用域中的常量,或者修飾文件、函數、區塊作用域(block scope)中被聲明爲static的對象。你也可以用它修飾classes內部的static和non-static成員變量。面對指針,可以指出指針自身,指針所指物,或者兩者都是const。如下:

char greeting[] = “Hello”;
char* p = greeting;
const char* p = greeting;     //指針更改,但指向的內存區域不可更改。
char const * p = greeting;     //指針更改,但指向的內存區域不可更改。
char* const p = greeting;     //指針不可更改,但指向的內存區域可更改。
const char* const p = greeting; //指針不可更改,指向的內存區域也不可更改。

STL的迭代器系以指針爲根據塑模出來,所以迭代器的作用就像個T* 指針。聲明迭代器爲const和聲明指針爲const一樣(即聲明一個T* const指針),表示這個迭代器不得指向不同的東西,但是它指的東西的值可以改動。如果希望迭代器所值的東西不可被改動(即模擬一個const T* 指針),則需要使用const_iterator:

std::vector<int> vec;const std::vector<int>:: iterator iter = vec.begin(); //iter的作用像個T* const
*iter = 10;        //改變iter所指物
++iter;           //錯誤,iter是const的不能改變。
std::vector<int>:: const_iterator citer = vec.begin(); //iter的作用像個const T*
*citer = 10;        //錯誤,不能改變所指物
++citer;           //沒問題

const最具威力的用法是在面對函數聲明時。在一個函數聲明式內,const可以和函數返回值、各參數、函數自身產生關聯。
讓函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性。如下:

class Rational {};
const Rational operator* {const Rational& lhs, const Rational& rhs};

如果不如此實現,就可以如此使用:

Rational a, b, c;(a * b) = c; //在a*b的結果上調用operator=很多時候次錯誤爲編碼打字錯誤如只是想做個比較,
if(a * b = c)//只是做個比較動作,但是少寫了個=。

如果將operator*返回值定義爲const,就可以讓用戶自定義類型和內置類型不兼容,此情況下,編譯是編譯器會提早報告錯誤,更快的處理程序錯誤。
const用於成員函數,可以確認該成員函數可作用於const對象身上,如此做有兩個好處,第一,使class接口比較容易被理解。因爲得知那個函數可以改動對象,那個函數不可以是很重要的。第二,使操作const對象成爲可能,對編寫高效代碼是個關鍵,因爲改善C++代碼程序效率的一個根本辦法就是以pass by reference-to-const方式傳遞對象,而此技術可行的前提是,我們有const成員函數可用來處理取得的const對象。
兩個成員函數如果只是常量性(constness)不同,可以被重載。如下:

class TextBlock {
public:
	const char& operator[] {std::size_t position} const {return text[position];} //const對象
	char& operator[] {std::size_t position} {return text[position];} //非const對象
	private:
		std::string text;
};

對於TextBlock的operator[]s可被如下使用:

TextBlock tb(“Hello”);
std::cout<<tb[0];  //調用非const TextBlock::operator[].
const TextBlock ctb(“World”);
std::cout<<ctb[0];  //調用const TextBlock::operator[].
tb[0] = ‘x’;   //可正常修改值,
ctb[0] = ‘x’;  //錯誤,因爲ctb對象爲const,無法修改

由於一般const對象大多用passed by pointer-to-const或passed by reference-to-const傳值。上述ctb應該改爲:

void print(const TextBlock& ctb){ std::cout<<ctb[0]};  //調用const TextBlock::operator[].

但是如果上述的TextBlock類中如果只是重載了char& operator[],未重載const char& operator[],成員爲char*此時,執行下面代碼:

const TextBlock ctb(“World”);//聲明一個常量對象
char * pc= &ctb[0];        //調用const operator[]取得一個指針。
*pc = ‘J’;                 //cctb內容變成了Jorld。

本意爲不可更改的值,但是由於代碼實現的原因,值又可以被更改。
對於如果想在const函數中改變某些成員變量的值,可使用mutable(可變的)。mutable釋放掉non-static成員變量的bitwise constness約束。如下代碼:

class CtextBlock {
public:
	std::size_t length() const;
private:
	char * pText;
	mutable std::size_t textLength;  //變量的值總會被改變,即使在const成員函數內。
};
std::size_t CtextBlock::length() const
{
	textLength = std::strlen(pText);  //即使在const函數中,也可以改變值。
}

在const和non-const成員函數中避免重複。可以使用轉型進行實現如下:

class TextBlock {
public:
const char& operator[] {std::size_t position} const 
{return text[position];
} 
char& operator[] {std::size_t position} 
{	
	return const_cast<char&>(                /*將op[]返回值的const移*除,*/
	static_cast<const TextBlock&>(*this)[position]); //爲*this加上const調用const op[]。
} 
	private:
		std::string text;
};

此處轉型使用了兩次,第一次爲*this添加const使可以調用const版本的op,第二次是將返回值中的const移除。如果反向使用的話,是不可以的,因爲const成員函數內不改變其對象的邏輯狀態,而non-const成員函數可對其對象做任何動作;const成員函數調用non-const成員函數會破壞const的規則,所以不被允許如此做。

  • 將某些東西聲明爲const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
  • 編譯器強制實施bitwise constness,但編寫程序時應該使用“概念上的常量性”(conceptual constness)。
  • 當const和non-const成員函數有着實質性等價的實現時,令non-const版本調用const版本可避免代碼重複。

4、確定對象被使用前已先被初始化

Make sure that object are initialized before they’re used.
對於將對象初始化,C++在特定的語境下會有不同的結果。通常如果使用C part of C++而且初始化會導致運行期成本,那麼就不能保證會默認初始化。但是對於non-C parts of C++,情況就會有些變化,如數值不保證其內容被初始化,但是vector卻可以保證內容被默認初始化。
最好的解決辦法就是在使用對象之前先將它初始化,對於無任何成員的內置類型,必須手動完成初始化。如:

int x = 0;					//對int進行手動初始化。
const char* text = “A C”;		//對指針進行手動初始化。
double d; std::cin>>d;		//以讀取input stream的方式完成初始化。

對於內置類型外的任何其他東西,初始化應該在構造函數中完成,確保每個構造函數都對對象的每一個成員初始化。
但是在執行時,不要混淆了賦值(assignment)和初始化(initialization)。例如:

class PhoneNumber {};
class ABEntry {
public:
	ABEntry(const std::string& name, const std::list<PhoneNumber>& phones);
private:
	std::string theName;
	std::list<PhoneNumber> thePhones;
	int numTimesConsulted;
};
ABEntry:: ABEntry(const std::string& name, const std::list<PhoneNumber>& phones)
{
	theName = name;     //這些都是賦值而不是初始化。
	thePhones = phones;
	numTimesConsulted = 0;
}

因爲對象的成員變量的初始化動作發生在進入構造函數本體之前。在ABEntry構造函數內,其成員都是在被賦值。初始化發生的時間更早,較佳的寫法是使用member initialization list(成員初始化列表)替換賦值動作:

ABEntry:: ABEntry(const std::string& name, const std::list<PhoneNumber>& phones)
:theName(name), thePhones(phones),numTimesConsulted(0){ }

這個構造函數和上一個的最終結果相同,但是通常效率較高。因爲上一個首先調用default構造函數爲成員變量設初值,然後再對它們賦予新值。Default構造函數的作爲因此浪費了。使用成員初值列的做法避免了這一個問題,因爲初值列中針對各個成員變量而設的實參,被拿去作爲各成員變量的構造函數的實參。
有些情況下即使面對成員變量屬於內置類型,也一定得使用初始列,比如成員變量是const或references,它們就一定需要初值,不能被賦值。
如果classes擁有多個構造函數,每個構造函數有自己的成員初值列,如果這種classes存在許多成員變量或base classes,這種情況下,可以將使用初值列和賦值表現一樣好的成員變量,移往某個函數(通常爲private),供所有構造函數調用。
C++對於成員初始化次序是固定的,base classes早於其drived classes被初始化,而class的成員變量總是以其聲明次序被初始化(與在成員初值列中的次序無關)。
多個編譯單元內的non-local static對象經由“模板隱式具現化(implicit template instantiations)”形成,不但不可能決定正確的初始化次序,甚至往往不值得尋找“可決定正確次序”的特殊情況。將每個non-local static對象搬到自己的專屬函數內(該對象再次函數內被聲明爲static)。這些函數返回一個reference指向它所含的對象。然後用戶調用這些函數,而不直接訪問這些對象。即non-local static對象被local static對象替換了。Singleton模式就是使用此方式實現的。如下:

class FileSystem {};
	FileSystem& tfs()   //函數用來替換tfs對象,它在FileSystem class中可能是個static。
{
	static FileSystem fs;  //定義並初始化一個local static對象,
	return fs;          //返回一個reference指向上述對象。
}
Class Directory {};
Directory::Directory(params)
{.
	std::size_t disk = tfs().numDisks();  //使用tfs()調用FileSystem的成員函數。}
Directory& tempDir()
{
	static Directory td;
	return td;
}

如此實現,系統程序使用函數返回的“指向static對象”的references,而不再使用static對象自身。

  • 爲內置型對象進行手工初始化,因爲C++不保證初始化它們。
  • 構造函數最好使用成員初值列(member initialization list),而不要在構造函數本體內使用賦值操作(assignment)。初值列列出的成員變量,其排列次序應該和它們在class中的聲明次序相同。
  • 爲免除“跨編譯單元的初始化次序”問題,以local static對象替換non-local static對象。

下一篇:C++進階_Effective_C++第三版(二) 構造/析構/賦值運算 Constructors,Destructors,and Assignment Operators

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章