天天看點

Effective C++ 讀書筆記(1)

  寫在前面:看書的過程總是看了忘,忘了看,着實讓人苦惱啊,現在決定用部落格來記錄自己的看書學習過程,一方面總結知識、記錄下來的方法比單純的看書對知識的印象會更深刻一些,另一方面也算是對學習過程的記錄和自己成長的見證。

第一章 讓自己習慣 C++

本章共有如下4個條款:

  • 條款1 視 C++為一個聯邦
  • 條款2 盡量以 const、enum、inline替換 #define
  • 條款3 盡可能使用const
  • 條款4 确定對象被使用前已被初始化
條款1 視 C++為一個聯邦

C++ 發展至今,已經是一個多重泛型程式設計語言,它支援:過程形式、面向對象形式、函數形式、泛型形式、元程式設計形式。可以把 C++ 看成由下面4個部分組成

  • C :C是C++的基礎,是以C++是完全相容C語言的,當然,有部分的關鍵字和結構還是有一些差別的,如static關鍵字C++有了更為複雜的含義。如struct,c++拓展了結構體類型的面向對象特性,等等還有一些其他的,但是主要是拓展性的差別。簡而言之,C++就是提供了更為高可用的高效程式設計,如模闆(templates)、異常(exception)、重載(overload)等等,這些在c裡面都是沒有的。
  • Object-Oriented C++:這部分就是C++的語言特性相關,包括面向對象的内容:classes(構造函數、析構函數)、封裝(encapsulation)、繼承(inheritance)、多态(polymorphism)、virtual函數(dynamic binding)、等等…這些就是OO的在C++上的具體實作。
  • Template C++:這是C++的泛型程式設計(generic programming)部分,就是C++的函數模闆(function template)、類模闆(classes template),它們使得泛型程式設計成為可能。
  • STL:STL是C++的标準模闆庫(Standard Template Library),它實作了一系列通用的容器(containers)、疊代器(itrators)、算法(algorithms)、以及函數對象(function objects),并且以巧妙并且不失高效的設計模式将它們連貫在一起,程式員可以通過使用STL來更為快速的開發高效的程式。
條款2 盡量以 const、enum、inline替換 #define

#define不被視為語言的一部分,在編譯器處理代碼之前由預處理器來處理,其特點如下:

  • 不對資料配置設定記憶體空間,不進行類型檢查
  • 編譯時隻做簡單的文本插入替換
  • 不是語句,不在最後加分号

使用建議

  • 對于單純常量,最好以 const 對象或 enums 替換 #define
  • 對于形似函數的宏,最好改用 inline 函數替換 #define

關于第二點,看如下例子:

//以 a 和 b 的較大值調用 f
 #define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
           

寫這種宏要注意加括号,且用起來容易出問題,可以改成下面的形式:

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

另外提到的比較重要的一點是:enum、static實作編譯期間就要知道數組的大小功能

//利用 static 實作
class GamePlayer
{
private:
	static const int NumTurns = 5;//常量聲明式,還未定義!static要在類外定義。
	int scores[NumTurns];//使用該常量
};
const int GamePlayer::NumTurns;//聲明時已經有了初值,定義時不能有了,會報錯。
 
//利用 enum 實作
class GamePlayer
{
	//enum了解為記号
	enum {NumTurns = 5};//令NumTurns成為5的記号
	int scores[NumTurns];
};

           
條款3 盡可能使用const

const 作用總結如下

  • 修飾普通變量:如

    const int a=1;

  • 修飾引用:不能通過别名修改原變量的值
int a=1;
	const int &b=a;
	a=2;//正确
	b=3;//錯誤
	const int c=5;
	const int &d=c;//正确
	int &e=c;//錯誤
           
  • 修飾指針:離什麼近就表示什麼不可變
    • 1)、const char* p表示被指物不可改變——常量(内容)指針
    char s1[100],s2[100];
    	const char* p=s1;
    	p=s2;//正确
    	p[0]='a';//錯誤,不能通過指針修改所指記憶體中的内容
    	*p='a';//錯誤  
               
    • 2)、char* const p表示指針不可以改變——指針常量
    char s1[100],s2[100];
    	char* const p=s1;
    	*p='a';//正确 
    	p=s2;//錯誤  
               
    • 3)、const char* const p表示指針和被指物均不可改變——常指針常量
  • 修飾函數
    • 出現在傳回值前面

      const int fun1();

      const int *fun2(int &a);

      int * const fun3(int &a);

    • 常引用作形參,表明是輸入參數

      fun(const int& a);

  • 修飾類資料成員:值不能變,隻能在構造函數初始化清單中初始化
  • 修飾類函數成員:隻能引用本類中的資料成員,不能修改它們,也不能調用類中任何非const 成員函數

    形式:void fun() const;

  • 修飾類對象:隻能調用對象的 const 成員函數,不能修改成員。
class A
{
private:
	int x;
public:
	A(int a):x(a){}
	void fun1()
	{ 
		x=2;//正确
		cout<<"fun1"<<endl; 
	}
	void fun2() const
	{ 
		x=2;//錯誤
		cout<<x<<endl;//正确
	}	
}
int main()
{
	A p(5);
	p.fun1();p.fun2();//正确
	const A q(6);
	q.fun1();//錯誤
	q.fun2();//正确
	return 0;
}
           

文中有一句話值得注意:兩個成員函數如果隻是常量性不同,可以被重載。

如下的兩個函數算是重載的:

void func();

void func() const;

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

1、對象的初始化責任落在構造函數上,是以要確定構造函數将對象的每一個成員都初始化,注意兩點:

  • 構造函數對成員變量的初始化是在進入構造函數本體之前;
  • 如果在構造函數體内(會使用 pass by value)是指派,而前者效率更高。
//指派操作
A(int a,int b,int c)
{
	x=a;
	y=b;
	z=c;
}
//在清單中初始化
A(int a,int b,int c):x(a),y(b),z(c){}
           

2、必須在清單中初始化的成員

  • 初始化一個 const 成員
  • 初始化一個 reference 成員
  • 調用一個基類的構造函數,而該函數有一組參數
  • 調用一個資料成員對象的構造函數,而該函數有一組參數

3、初始化順序

  • 構造函數調用順序為:基類—>子對象—>目前類
  • 資料成員初始化順序為聲明時的順序,跟在初始化清單中的順序無關

還有非常重要的一點:跨編譯單元的靜态對象初始化問題

文中提到,C++ 對定義于不同編譯單元内的靜态對象的初始化次序無明确定義。C++ 标準中有規定,在同一個編譯單元内,靜态變量初始化的順序就是定義的順序,跨編譯單元的靜态變量的初始化順序未定義!!!具體的初始化順序取決于編譯器的實作。

看下面例子

/*第一個源碼檔案*/
class FileSystem
{
public:
    std::size_t numDisks() const;
};
extern FileSystem tfs;
/*第二個源碼檔案*/
class Directory
{
public:
    Directory();
};
Directory::Directory()
{
    std::size_t disks = tfs.numDisks();
}
/*建立一個Directory對象*/
Directory tempDir();           //(1)式
           

上述代碼中的(1)式,除非 tfs 在 tempDir 之前先被初始化,否則 tempDir 的構造函數會用到尚未初始化的 tfs,但實際上這是無法保證的。解決這個問題的辦法是:将每個non-local static對象 搬到自己的專屬函數内,這些函數傳回一個reference指向它所含的對象,然後使用者調用這些函數。

/*第一個源碼檔案*/
class FileSystem
{
public:
    std::size_t numDisks() const;
};
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;
}
           

進行上述修改以後,調用的方式直接使用 tfs() 和 tempDir() 代替 tfs 和 tempDir。

上述方法的基礎在于:C++ 保證,函數内的 local static 對象會在“該函數被調用期間”“首次遇上該對象之定義式”時被初始化。也就是說,如果一個靜态對象被定義在函數内,直到它所在的函數被第一次調用時才會被初始化。利用這樣的特性,我們可以確定一個靜态執行個體被讀取時已被初始化。這就消除了跨編譯單元的靜态對象的構造順序不确定問題。

更多的例子在 C++靜态變量的初始化 這篇文章中也有提到。

參考文章:

1、https://segmentfault.com/a/1190000014348286

2、https://blog.csdn.net/vict_wang/article/details/85763265

3、https://www.jianshu.com/p/35ddcc56b735

4、https://www.jianshu.com/p/dd34cee5242c

繼續閱讀