天天看點

Effective C++ (一) 讓自己習慣C++

條款01 視C++為一個語言聯邦

  • C++是多元的,使用時以友善為主。

C++最早隻是簡單的C與對象的結合(C with classes),随着語言的發展,C++已經是一個多重範型語言,一個支援:

  • 過程(procedural)
  • 面向對象(object-oriented)
  • 函數式(functional)
  • 泛型(generic)
  • 元程式設計(metaprogramming)

需要掌握其中主要的次語言(代表性的語言):

  • C
  • Object-oriented C++
  • Template C++
  • STL

根據實際切換已達到合适的程式設計節奏,如在往函數傳遞一個對象,若該對象是一個内置類型,使用C風格傳值即可;若參數是對象,那麼Object-oriented C++傳遞引用的特性就比較合适;有時候我們甚至不知道處理對象的類型,那麼則使用Template模闆抽象化參數類型;STL在使用過程中,疊代器、函數對象也應用到了C傳值風格。總而言之,C++寫程式往往不會是單一的範式,一切以友善為主。

條款02 盡量以const,enum,inline替換#define

  • 對于單純的常量最好以const或enums代替#define
  • 形似函數的宏,最好改用inline函數
  1. 為什麼要用const代替?
  • 目标碼可能更小
  • 更容易發現錯誤(進入記号表)
  • 有作用域範圍
  1. 為什麼要用enums?

    因為const具有作用域,自然可能需要一個類專屬的常量,隻保有一個實體,應聲明為 static const:

class A
{
public:
	static const int  size= 3;//舊式編譯器不支援
	A() { std::cout << "A()" << std::endl; }
	int a[size];
};
           

條款提到enum隻是為了給出編譯器不支援static const 類内初始化的解決方案。

  1. 為什麼用inline函數代替#define定義的函數?

為了防止降低#define容易忘記小括号導緻的錯誤,除此inline是個函數,遵循通路規則和作用域限制。

條款03 盡可能使用const

const這個東西在C++中很多地方都有用到,作用非常廣泛:

  • classes外部修飾global或者namespace作用域的常量
  • 檔案、函數和塊作用聲明static的對象
  • 指針常量、常量指針和引用常量

3.1 const和指向性類型讨論

const的文法與指向類型結合含義較為複雜,簡單來說,const出現在左邊表示所指之物為常量,出現在右邊則表示指向不變,前者是對象具有low-level屬性,後者表示對象具有top-level屬性,當然也可以同時具備兩種。PS,因為引用從綁定之刻就不會發生改變,是以隻具有low-level屬性。

在聲明中,const可以和函數傳回值、參數和成員函數本身起作用,避免一些無意義的情況。const作為傳回值目的是防止類似以下的情況:

class Ratioanl{...}
const Rational operator*(const Rational &lhs,const Rational &rhs);
int main()
{
	Rational a,b,c;
	(a*b)=c;//a*b傳回值是一個局部變量,進行指派,整個式子沒有意義,用const可以避免,
	if(a*b=c){...}//a*b傳回值指派運算始終為真,可以通過const避免
}
           

3.2 const成員函數讨論

const成員函數是必要的,接口更加清晰,是提升C++效率的重要手段;const成員函數是保證不更改資料成員的聲明;const成員函數分為兩大陣營bitwise和logic,後者通過關鍵字mutable實作bitwise檢查豁免,最後作者介紹了常量性移除用于優化、簡化代碼。

const成員函數很重要,基于兩個理由:

  • 接口更加清晰
  • 使操作const對象成為可能

void A::fun(int i)const;

盡管

const A a

執行個體是一個常量,但是我們仍能對其進行操作,在保證不修改常量執行個體下進行操作,作者說,改善C++程式效率的根本辦法是pass by reference-to-const,如果不能操作const對象,改善無從談起。作者還提到,const函數和非const函數是一對重載聲明:

void fun(int i)const ;
void fun(int i);
           

const 成員函數的兩個陣營:bitwise constness(physical constness)和logic constness,這兩個陣營其實争論點在于如何去定義一個const對象,是一個資料成員改變就是nont-const(bitwise constness),還是根據用于具體邏輯,改變一個兩個都可以視作const(logic constness)。

預設情況C++将使用bitwise constness進行常量屬性檢查,但不是所有情況都工作良好,看下面這個例子:

class CTextBlock{
public:
	char &operator[](std::size_t position) const;
	{
		return pText[position];
	}
private:
	char * pText;
}
           

bitwise constness失效情況。編譯器根據bitwise constness預設規則檢查後發現下标運算符實作中沒有改變資料成員pText的值,編譯順利通過,但是其傳回了一個左值char &,如果使用者做如下操作:

const CTextBlock cctb("hello");
char *pc=&cctb[0];
*pc="j";
           

雖然使用者本意是建立一個常量的hello,但是暴露出的引用卻改變了他的常量屬性。

在有些應用,我們希望打破這種bitwise檢查:

class A
{
	public:
		void doSomething()const
		{
			m_i=555;//error,bitwise檢查失敗。
			m_j=564;//ok,logic檢查,特别通行證
		} 
	private:
		int m_i=4396; 
		mutable int m_j=567;
}
           

有時候我們既要const成員函數,也要non-const成員函數,但是其中内容相差不多,代碼較為備援。作者提供了一種解決方法:常量性移除(casting away constness)。

class TextBlock
{
public:
	const char& operator[](std::size_t position) const{
		return text[postion];
	}
	char &operator[](std::size_t postion)
	{
		return 
			const_cast<char &>						//強制轉換成const char&
			(static_cast<const TextBlock&>(*this)   //強制轉換成const,以便複用const char &operator
				[position];
			)
	}
}
           

這裡利用了兩次強制轉換:

  • 第一次,為*this添加const屬性以便調用const成員函數
  • 第二次,為const operator[ ]移除const,還原none-const屬性

注意,是在一般函數調用const函數,不是const函數調用一般函數!後者破壞了const的語義。

條款04 确定對象被使用前已先被初始化

4.1 區分初始化和指派

一個對象要麼執行預設初始化,要麼執行值初始化。内置類型有初值則執行值初始化;沒有初值執行預設初始化,預設初始化行為與作用域相關,要麼初始化為0,要麼不執行初始化。

預設初始化結果 作用域
不初始化 非static和全局作用域
初始化為0 static和全局作用域

推薦使用類内初值或初始化清單進行初始化,而不是先構造再進行指派;有些情況不得不采用前者,這是因為以下情況:

  • const成員
  • 引用對象
  • 資料成員未提供預設構造

清單初始化或者類内初值發生時間早于構造函數。另外,清單初始化在某個情況比類内初值更加靈活,例如一個類中定義了多個構造函數,每個構造函數的初值都有所不同,類内初值都是相同的初值,顯然不合适。

4.2 注意初始化順序

初始化清單隻說明資料成員的值,并不保證初始化順序。看看下面這個例子:

class X
{
	int i;
	int j;
public:
	X(int val):j(val),i(j){}
}
           

正如一開始說到的,清單初始化表示的是j的值是val,i的值是j,誰先誰後并不保證,是以可能出現j尚未被val初始化就被用于i指派,這樣的i值是沒有任何意義的。C++ primer建議,如果可能盡量避免使用某些資料成員來初始化其他成員。

C++對于定義于不同編譯單元内的non-local-static對象的初始化順序是不确定的。static對象其生存周期是從構造到程式結束,它可以分為local-static和non-local-static兩種,local-static隻有一種情況,那就是位于函數作用域中的;其他的,如global、namespace、class和file作用域都是屬于non-local-static,static對象隻在main()程式結束後調用。當一個non-static對象(以global為例),在一個檔案中被初始化,在另一個檔案中被使用,C++無法為我們保證static在使用之時被初始化,是以可能出現一些意想不到的現象。為了解決non-local-static對象這種不确定性,我們可以通過定義一個函數傳回一個local-static對象的指針或者引用,因為local-static會在第一次調用這個函數進行初始化,而不是在運作前就進行初始化,進而避免了因為調用順序而導緻的問題,這其實是單例模式的一種常見實作,但是在多線程中可能會出現競争問題,如何解決?通過在單線程啟動階段手動調用所有的reference-returning函數。

繼續閱讀