More Effective C++35個改善編程與設計的有效方法筆記

Scott Meyers大師Effective三部曲:Effective C++、More Effective C++、Effective STL,這三本書出版已很多年,後來又出版了Effective Modern C++。

Effective C++的筆記見:https://blog.csdn.net/fengbingchun/article/details/102761542

Effective STL的筆記見:https://blog.csdn.net/fengbingchun/article/details/103223914

Effective Modern C++的筆記見:https://blog.csdn.net/fengbingchun/article/details/104136592

這裏是More Effective C++的筆記:                 

1. 指針與引用的區別

void printDouble(const double& rd)
{
	std::cout<<rd; // 不需要測試rd,它肯定指向一個double值
}

void printDouble(const double* pd)
{
	if (pd) { // 檢查是否爲NULL
		std::cout<<*pd;
	}
}

int test_item_1()
{
	char* pc = 0; // 設置指針爲空值
	char& rc = *pc; // 讓指針指向空值,這是非常有害的,結果將是不確定的

	//std::string& rs; // 錯誤,引用必須被初始化
	std::string s("xyzzy");
	std::string& rs = s; // 正確,rs指向s
	std::string* ps; // 未初始化的指針,合法但危險

{
	std::string s1("Nancy");
	std::string s2("Clancy");
	std::string& rs = s1; // rs引用s1
	std::string* ps = &s1; // ps指向s1
	rs = s2; // rs仍舊引用s1,但是s1的值現在是"Clancy"
	ps = &s2; // ps現在指向s2,s1沒有改變
}

	std::vector<int> v(10);
	v[5] = 10; // 這個被賦值的目標對象就是操作符[]返回的值,如果操作符[]
		   // 返回一個指針,那麼後一個語句就得這樣寫: *v[5] = 10;

	return 0;
}

指針與引用看上去完全不同(指針用操作符”*”和”->”,引用使用操作符”.”),但是它們似乎有相同的功能。指針和引用都是讓你間接引用其它對象。

在任何情況下都不能使用指向空值的引用。一個引用必須總是指向某些對象。C++裏,引用應被初始化

不存在指向空值的引用這個事實意味着使用引用的代碼效率比使用指針的要高。因爲在使用引用之前不需要測試它的合法性。

指針與引用的另一個重要的不同是指針可以被重新賦值以指向另一個不同的對象。但是引用則總是指向在初始化時被指定的對象,以後不能改變。

總的來說,在以下情況下你應該使用指針,一是你考慮到存在不指向任何對象的可能(在這種情況下,你能夠設置指針爲空),二是你需要能夠在不同的時刻指向不同的對象(在這種情況下,你能改變指針的指向)。如果總是指向一個對象並且一旦指向一個對象後就不會改變指向,那麼你應該使用引用。

當你知道你必須指向一個對象並且不想改變其指向時,或者在重載操作符併爲防止不必要的語義誤解時(最普通的例子是操作符[]),你不應該使用指針。而在除此之外的其它情況下,則應使用指針。

關於引用的更多介紹參考:https://blog.csdn.net/fengbingchun/article/details/69820184

2. 儘量使用C++風格的類型轉換

class Widget {
public:
	virtual void func() {}
};

class SpecialWidget : public Widget {
public:
	virtual void func() {}
};

void update(SpecialWidget* psw) {}
void updateViaRef(SpecialWidget& rsw) {}

typedef void (*FuncPtr)(); // FuncPtr是一個指向函數的指針
int doSomething() { return 1; };

int test_item_2()
{
	int firstNumber = 1, secondNumber = 1;
	double result1 = ((double)firstNumber) / secondNumber; // C風格
	double result2 = static_cast<double>(firstNumber) / secondNumber; // C++風格類型轉換

	SpecialWidget sw; // sw是一個非const對象
	const SpecialWidget& csw = sw; // csw是sw的一個引用,它是一個const對象
	//update(&csw); // 錯誤,不能傳遞一個const SpecialWidget*變量給一個處理SpecialWidget*類型變量的函數
	update(const_cast<SpecialWidget*>(&csw)); // 正確,csw的const顯示地轉換掉(csw和sw兩個變量值在update函數中能被更新)
	update((SpecialWidget*)&csw); // 同上,但用了一個更難識別的C風格的類型轉換

	Widget* pw = new SpecialWidget;
	//update(pw); // 錯誤,pw的類型是Widget*,但是update函數處理的是SpecialWidget*類型
	//update(const_cast<SpecialWidget*>(pw)); // 錯誤,const_cast僅能被用在影響constness or volatileness的地方,不能用在向繼承子類進行類型轉換

	Widget* pw2 = nullptr;
	update(dynamic_cast<SpecialWidget*>(pw2)); // 正確,傳遞給update函數一個指針是指向變量類型爲SpecialWidget的pw2的指針, 如果pw2確實指向一個對象,否則傳遞過去的將是空指針

	Widget* pw3 = new SpecialWidget;
	updateViaRef(dynamic_cast<SpecialWidget&>(*pw3)); // 正確,傳遞給updateViaRef函數SpecailWidget pw3指針,如果pw3確實指向了某個對象,否則將拋出異常

	//double result3 = dynamic_cast<double>(firstNumber) / secondNumber; // 錯誤,沒有繼承關係
	const SpecialWidget sw4;
	//update(dynamic_cast<SpecialWidget*>(&sw4)); // 錯誤,dynamic_cast不能轉換掉const

	FuncPtr funcPtrArray[10]; // funcPtrArray是一個能容納10個FuncPtr指針的數組
	//funcPtrArray[0] = &doSomething; // 錯誤,類型不匹配
	funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); // 轉換函數指針的代碼是不可移植的(C++不保證所有的函數指針都被用一樣的方法表示),在一些情況下這樣的轉換會產生不正確的結果,所以應該避免轉換函數指針類型

	return 0;
}

C++通過引進四個新的類型轉換(cast)操作符克服了C風格類型轉換的缺點(過於粗魯,能允許你在任何類型之間進行轉換;C風格的類型轉換在程序語句中難以識別),這四個操作符是:static_cast、const_cast、dynamic_cast、reinterpret_cast。

static_cast在功能上基本上與C風格的類型轉換一樣強大,含義也一樣。它也有功能上限制。例如,不能用static_cast像用C 風格的類型轉換一樣把struct轉換成int類型或者把double類型轉換成指針類型,另外,static_cast不能從表達式中去除const屬性,因爲另一個新的類型轉換操作符const_cast有這樣的功能。

const_cast用於類型轉換掉表達式的const或volatileness屬性。如果你試圖使用const_cast來完成修改constness或者volatileness屬性之外的事情,你的類型轉換將被拒絕。

dynamic_cast被用於安全地沿着類的繼承關係向下進行類型轉換。這就是說,你能用dynamic_cast把指向基類的指針或引用轉換成指向其派生類或其兄弟類的指針或引用,而且你能知道轉換是否成功。失敗的轉換將返回空指針(當對指針進行類型轉換時)或者拋出異常(當對引用進行類型轉換時)。dynamic_cast在幫助你瀏覽繼承層次上是有限制的,它不能被用來缺乏虛函數的類型上,也不能用它來轉換掉constness。如你想在沒有繼承關係的類型中進行轉換,你可能想到static_cast。如果是爲了去除const,你總得用const_cast。

reinterpret_cast使用這個操作符的類型轉換,其轉換結果幾乎都是執行期定義(implementation-defined)。因此,使用reinterpret_cast的代碼很難移植。此操作符最普通的用途就是在函數指針之間進行轉換。

關於類型轉換更多介紹參考:https://blog.csdn.net/fengbingchun/article/details/51235498

3. 不要對數組使用多態

class BST {
public:
	virtual ~BST() { fprintf(stdout, "BST::~BST\n"); }
private:
	int score;
};

class BalancedBST : public BST {
public:
	virtual ~BalancedBST() { fprintf(stdout, "BalancedBST::~BalancedBST\n"); }
private:
	int length;
	int size; // 如果增加此一個int成員,執行test_item_3會segmentation fault,註釋掉此變量,運行正常
};

int test_item_3()
{
	fprintf(stdout, "BST size: %d\n", sizeof(BST)); // 16
	fprintf(stdout, "BalancedBST size: %d\n", sizeof(BalancedBST)); // 24

	BST* p = new BalancedBST[10];
	delete [] p; // 如果sizeof(BST) != sizeof(BalancedBST),則會segmentation fault

	return 0;
}

C++允許你通過基類指針和引用來操作派生類數組。不過這根本就不是一個特性,因爲這樣的代碼幾乎從不如你所願地那樣運行。數組與多態不能用在一起。值得注意的是如果你不從一個具體類(concrete classes)(例如BST)派生出另一個具體類(例如BalancedBST),那麼你就不太可能犯這種使用多態性數組的錯誤。

4. 避免無用的缺省構造函數

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber) {}
};

int test_item_4()
{
	//EquipmentPiece bestPieces[10]; // 錯誤,沒有正確調用EquipmentPiece構造函數
	//EquipmentPiece* bestPieces2 = new EquipmentPiece[10]; // 錯誤,與上面的問題一樣

	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正確,提供了構造函數的參數

	// 利用指針數組來代替一個對象數組
	typedef EquipmentPiece* PEP; // PEP指針指向一個EquipmentPiece對象
	PEP bestPieces4[10]; // 正確,沒有調用構造函數
	PEP* bestPieces5 = new PEP[10]; // 也正確
	// 在指針數組裏的每一個指針被重新賦值,以指向一個不同的EquipmentPiece對象
	for (int i = 0; i < 10; ++i)
		bestPieces5[i] = new EquipmentPiece(ID1);

	// 爲數組分配raw memory,可以避免浪費內存,使用placement new方法在內存中構造EquipmentPiece對象
	void* rawMemory = operator new[](10*sizeof(EquipmentPiece));
	// make bestPieces6 point to it so it can be treated as an EquipmentPiece array
	EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);
	// construct the EquipmentPiece objects in the memory使用"placement new"
	for (int i = 0; i < 10; ++i)
		new(&bestPieces6[i]) EquipmentPiece(ID1);
	// ...
	// 以與構造bestPieces6對象相反的順序解構它
	for (int i = 9; i >= 0; --i)
		bestPieces6[i].~EquipmentPiece(); // 如果使用普通的數組刪除方法,程序的運行將是不可預測的
	// deallocate the raw memory
	delete [] rawMemory;

	return 0;
}

構造函數能初始化對象,而缺省構造函數則可以不利用任何在建立對象時的外部數據就能初始化對象。有時這樣的方法是不錯的。例如一些行爲特性與數字相仿的對象被初始化爲空值或不確定的值也是合理的,還有比如鏈表、哈希表、圖等等數據結構也可以被初始化爲空容器。但不是所有的對象都屬於上述類型,對於很多對象來說,不利用外部數據進行完全的初始化是不合理的。比如一個沒有輸入姓名的地址薄對象,就沒有任何意義。

利用指針數組代替一個對象數組這種方法有兩個缺點:第一你必須刪除數組裏每個指針所指向的對象。如果忘了,就會發生內存泄漏。第二增加了內存分配量,因爲正如你需要空間來容納EquipmentPiece對象一樣,你也需要空間來容納指針。

對於類裏沒有定義缺省構造函數還會造成它們無法在許多基於模板(template-based)的容器類裏使用。因爲實例化一個模板時,模板的類型參數應該提供一個缺省構造函數。在多數情況下,通過仔細設計模板可以杜絕對缺省構造函數的需求。

5. 謹慎定義類型轉換函數

class Name {
public:
	Name(const std::string& s); // 轉換string到Name
};

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1) // 轉換int到有理數類
	{
		n = numerator;
		d = denominator;
	}

	operator double() const // 轉換Rational類成double類型
	{
		return static_cast<double>(n) / d;
	}

	double asDouble() const
	{
		return static_cast<double>(n) / d;
	}

private:
	int n, d;
};

template<class T>
class Array {
public:
	Array(int lowBound, int highBound) {}
	explicit Array(int size) {}
	T& operator[](int index) { return data[index]; }

private:
	T* data;
};

bool operator== (const Array<int>& lhs, const Array<int>& rhs)
{ return false; }

int test_item_5()
{
	Rational r(1, 2); // r的值是1/2
	double d = 0.5 * r; // 轉換r到double,然後做乘法
	fprintf(stdout, "value: %f\n", d);

	std::cout<<r<<std::endl; // 應該打印出"1/2",但事與願違,是一個浮點數,而不是一個有理數,隱式類型轉換的缺點
				 // 解決方法是不使用語法關鍵字的等同的函數來替代轉換運算符,如增加asDouble函數,去掉operator double

	Array<int> a(10);
	Array<int> b(10);
	for (int i = 0; i < 10; ++i) {
		//if (a == b[i]) {} // 如果構造函數Array(int size)沒有explicit關鍵字,編譯器將能通過調用Array<int>構造函數能轉換int類型到Array<int>類型,這個構造函數只有一個int類型的參數,加上explicit關鍵字則可避免隱式轉換

		if (a == Array<int>(b[i])) {} // 正確,顯示從int到Array<int>轉換(但是代碼的邏輯不合理)
		if (a == static_cast<Array<int>>(b[i]))	 {} // 同樣正確,同樣不合理
		if (a == (Array<int>)b[i]) {} // C風格的轉換也正確,但是邏輯依舊不合理
	}
	return 0;
}

C++編譯器能夠在兩種數據類型之間進行隱式轉換(implicit conversions),它繼承了C語言的轉換方法,例如允許把char隱式轉換爲int和從short隱式轉換爲double。你對這些類型轉換是無能爲力的,因爲它們是語言本身的特性。不過當你增加自己的類型時,你就可以有更多的控制力,因爲你能選擇是否提供函數讓編譯器進行隱式類型轉換。

有兩種函數允許編譯器進行這些的轉換:單參數構造函數(single-argument constructors)和隱式類型轉換運算符。單參數構造函數是指只用一個參數即可調用的構造函數。該函數可以是隻定義了一個參數,也可以是雖定義了多個參數但第一個參數以後的所有參數都有缺省值。

隱式類型轉換運算符只是一個樣子奇怪的成員函數:operator關鍵字,其後跟一個類型符號。你不用定義函數的返回類型,因爲返回類型就是這個函數的名字。

explicit關鍵字是爲了解決隱式類型轉換而特別引入的這個特性。如果構造函數用explicit聲明,編譯器會拒絕爲了隱式類型轉換而調用構造函數。顯式類型轉換依然合法。

6. 自增(increment)、自減(decrement)操作符前綴形式與後綴形式的區別

class UPInt { // unlimited precision int
public:
	// 注意:前綴與後綴形式返回值類型是不同的,前綴形式返回一個引用,後綴形式返回一個const類型
	UPInt& operator++() // ++前綴
	{
		//*this += 1; // 增加
		i += 1;
		return *this; // 取回值
	}

	const UPInt operator++(int) // ++後綴
	{
		// 注意:建立了一個顯示的臨時對象,這個臨時對象必須被構造並在最後被析構,前綴沒有這樣的臨時對象
		UPInt oldValue = *this; // 取回值
		// 後綴應該根據它們的前綴形式來實現
		++(*this); // 增加
		return oldValue; // 返回被取回的值
	}

	UPInt& operator--() // --前綴
	{
		i -= 1;
		return *this;
	}

	const UPInt operator--(int) // --後綴
	{
		UPInt oldValue = *this;
		--(*this);
		return oldValue;
	}

	UPInt& operator+=(int a) // +=操作符,UPInt與int相運算
	{
		i += a;
		return *this;
	}

	UPInt& operator-=(int a)
	{
		i -= a;
		return *this;
	}

private:
	int i;
}; 

int test_item_6()
{
	UPInt i;
	++i; // 調用i.operator++();
	i++; // 調用i.operator++(0);
	--i; // 調用i.operator--();
	i--; // 調用i.operator--(0);

	//i++++; // 注意:++後綴返回的是const UPInt

	return 0;
}

無論是increment或decrement的前綴還是後綴都只有一個參數,爲了解決這個語言問題,C++規定後綴形式有一個int類型參數,當函數被調用時,編譯器傳遞一個0作爲int參數的值給該函數。

前綴形式有時叫做”增加然後取回”,後綴形式叫做”取回然後增加”。

當處理用戶定義的類型時,儘可能地使用前綴increment,因爲它的效率較高

7. 不要重載”&&”, “||”,或”,”

int test_item_7()
{
	// if (expression1 && expression2)
	// 如果重載了操作符&&,對於編譯器來說,等同於下面代碼之一
	// if (expression1.operator&&(expression2)) // when operator&& is a member function
	// if (operator&&(expression1, expression2)) // when operator&& is a global function

	return 0;
}

與C一樣,C++使用布爾表達式短路求值法(short-circuit evaluation)。這表示一旦確定了布爾表達式的真假值,即使還有部分表達式沒有被測試,布爾表達式也停止運算。

C++允許根據用戶定義的類型,來定製&&和||操作符。方法是重載函數operator&&和operator||,你能在全局重載或每個類裏重載。風險:你以函數調用法替代了短路求值法。函數調用法與短路求值法是絕對不同的。首先當函數被調用時,需要運算其所有參數。第二是C++語言規範沒有定義函數參數的計算順序,所以沒有辦法知道表達式1與表達式2哪一個先計算。完全可能與具有從左參數到右參數計算順序的短路計算法相反。因此如果你重載&&或||,就沒有辦法提供給程序員他們所期望和使用的行爲特性,所以不要重載&&和||。

同樣的理由也適用於逗號操作符。逗號操作符用於組成表達式。一個包含逗號的表達式首先計算逗號左邊的表達式,然後計算逗號右邊的表達式;整個表達式的結果是逗號右邊表達式的值。如果你寫一個非成員函數operator,你不能保證左邊的表達式先於右邊的表達式計算,因爲函數(operator)調用時兩個表達式作爲參數被傳遞出去。但是你不能控制函數參數的計算順序。所以非成員函數的方法絕對不行。成員函數operator,你也不能依靠於逗號左邊表達式先被計算的行爲特性,因爲編譯器不一定必須按此方法去計算。因此你不能重載逗號操作符,保證它的行爲特性與其被料想的一樣。重載它是完全輕率的行爲。

8. 理解各種不同含義的new和delete

class Widget8 {
public:
	Widget8(int widget8Size) {}
};

void* mallocShared(size_t size)
{
	return operator new(size);
}

void freeShared(void* memory)
{
	operator delete(memory);
}

Widget8* constructWidget8InBuffer(void* buffer, int widget8Size)
{
	return new(buffer) Widget8(widget8Size); // new操作符的一個用法,需要使用一個額外的變量(buffer),當new操作符隱含調用operator new函數時,把這個變量傳遞給它
	// 被調用的operator new函數除了待有強制的參數size_t外,還必須接受void*指針參數,指向構造對象佔用的內存空間。這個operator new就是placement new,它看上去像這樣:
	// void * operator new(size_t, void* location) { return location; }
}

int test_item_8()
{
	std::string* ps = new std::string("Memory Management"); // 使用的new是new操作符(new operator)
	//void * operator new(size_t size); // 函數operator new通常聲明
	void* rawMemory = operator new(sizeof(std::string)); // 操作符operator new將返回一個指針,指向一塊足夠容納一個string類型對象的內存
	operator delete(rawMemory);

	delete ps; // ps->~std::string(); operator delete(ps);

	void* buffer = operator new(50*sizeof(char)); // 分配足夠的內存以容納50個char,沒有調用構造函數
	operator delete(buffer); // 釋放內存,沒有調用析構函數. 這與在C中調用malloc和free等同OA

	void* sharedMemory = mallocShared(sizeof(Widget8));
	Widget8* pw = constructWidget8InBuffer(sharedMemory, 10); // placement new
	//delete pw; // 結果不確定,共享內存來自mallocShared,而不是operator new
	pw->~Widget8(); // 正確,析構pw指向的Widget8,但是沒有釋放包含Widget8的內存
	freeShared(pw); // 正確,釋放pw指向的共享內存,但是沒有調用析構函數

	return 0;
}

new操作符(new operator)和new操作(operator new)的區別:

new操作符就像sizeof一樣是語言內置的,你不能改變它的含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是分配足夠的內存以便容納所需類型的對象。第二部分是它調用構造函數初始化內存中的對象。new操作符總是做這兩件事情,你不能以任何方式改變它的行爲。你所能改變的是如何爲對象分配內存。new操作符調用一個函數來完成必須的內存分配,你能夠重寫或重載這個函數來改變它的行爲。new操作符爲分配內存所調用函數的名字是operator new。

函數operator new通常聲明:返回值類型是void*,因爲這個函數返回一個未經處理(raw)的指針,未初始化的內存。參數size_t確定分配多少內存。你能增加額外的參數重載函數operator new,但是第一個參數類型必須是size_t。就像malloc一樣,operator new的職責只是分配內存。它對構造函數一無所知。把operator new返回的未經處理的指針傳遞給一個對象是new操作符的工作。

placement new:特殊的operator new,接受的參數除了size_t外還有其它。

new操作符(new operator)與operator new關係:你想在堆上建立一個對象,應該用new操作符。它既分配內存又爲對象調用構造函數。如果你僅僅想分配內存,就應該調用operator new函數,它不會調用構造函數。如果你想定製自己的在堆對象被建立時的內存分配過程,你應該寫你自己的operator new函數,然後使用new操作符,new操作符會調用你定製的operator new。如果你想在一塊已經獲得指針的內存裏建立一個對象,應該用placement new。

Deletion and Memory Deallocation:爲了避免內存泄漏,每個動態內存分配必須與一個等同相反的deallocation對應。函數operator delete與delete操作符的關係與operator new與new操作符的關係一樣。

如果你用placement new在內存中建立對象,你應該避免在該內存中用delete操作符。因爲delete操作符調用operator delete來釋放內存,但是包含對象的內存最初不是被operator nen分配的,placement new只是返回轉到給它的指針。

Arrays:operator new[]、operator delete[]

9. 使用析構函數防止資源泄漏

用一個對象存儲需要被自動釋放的資源,然後依靠對象的析構函數來釋放資源,這種思想不只是可以運用在指針上,還能用在其它資源的分配和釋放上。

資源應該被封裝在一個對象裏,遵循這個規則,你通常就能夠避免在存在異常環境裏發生資源泄漏,通過智能指針的方式。

C++確保刪除空指針是安全的,所以析構函數在刪除指針前不需要檢測這些指針是否指向了某些對象。

10. 在構造函數中防止資源泄漏

C++僅僅能刪除被完全構造的對象(fully constructed objects),只有一個對象的構造函數完全運行完畢,這個對象才被完全地構造。C++拒絕爲沒有完成構造操作的對象調用析構函數。

在構造函數中可以使用try catch throw捕獲所有的異常。更好的解決方法是通過智能指針的方式。

如果你用對應的std::unique_ptr對象替代指針成員變量,就可以防止構造函數在存在異常時發生資源泄漏,你也不用手工在析構函數中釋放資源,並且你還能像以前使用非const指針一樣使用const指針,給其賦值。

std::unique_ptr的使用參考:https://blog.csdn.net/fengbingchun/article/details/52203664

11. 禁止異常信息(exceptions)傳遞到析構函數外

禁止異常傳遞到析構函數外有兩個原因:第一能夠在異常傳遞的堆棧輾轉開解(stack-unwinding)的過程中,防止terminate被調用。第二它能幫助確保析構函數總能完成我們希望它做的所有事情。

12. 理解”拋出一個異常”與”傳遞一個參數”或”調用一個虛函數”間的差異

你調用函數時,程序的控制權最終還會返回到函數的調用處,但是當你拋出一個異常時,控制權永遠不會回到拋出異常的地方。

C++規範要求被作爲異常拋出的對象必須被複制。即使被拋出的對象不會被釋放,也會進行拷貝操作。拋出異常運行速度比參數傳遞要慢。

當異常對象被拷貝時,拷貝操作是由對象的拷貝構造函數完成的。該拷貝構造函數是對象的靜態類型(static type)所對應類的拷貝構造函數,而不是對象的動態類型(dynamic type)對應類的拷貝構造函數。

catch子句中進行異常匹配時可以進行兩種類型轉換:第一種是繼承類與基類間的轉換。一個用來捕獲基類的catch子句也可以處理派生類類型的異常。這種派生類與基類(inheritance_based)間的異常類型轉換可以作用於數值、引用以及指針上。第二種是允許從一個類型化指針(typed pointer)轉變成無類型指針(untyped pointer),所以帶有const void*指針的catch子句能捕獲任何類型的指針類型異常。

catch子句匹配順序總是取決於它們在程序中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有能直接處理該派生類異常的catch子句,與相同的try塊相對應。不要把處理基類異常的catch子句放在處理派生類異常的catch子句的前面。

把一個對象傳遞給函數或一個對象調用虛擬函數與把一個對象作爲異常拋出,這之間有三個主要區別:第一,異常對象在傳遞時總被進行拷貝;當通過傳值方式捕獲時,異常對象被拷貝了兩次。對象作爲參數傳遞給函數時不一定需要被拷貝。第二,對象作爲異常被拋出與作爲參數傳遞給函數相比,前者類型轉換比後者要少(前者只有兩種轉換形式)。最後一點,catch子句進行異常類型匹配的順序是它們在源代碼中出現的順序,第一個類型匹配成功的catch將被用來執行。當一個對象調用一個虛擬函數時,被選擇的函數位於與對象類型匹配最佳的類裏,即使該類不是在源代碼的最前頭。

try catch介紹參考:https://blog.csdn.net/fengbingchun/article/details/65939258

13. 通過引用(reference)捕獲異常

通過指針捕獲異常不符合C++語言本身的規範。四個標準的異常----bad_alloc(當operator new不能分配足夠的內存時被拋出);bad_cast(當dynamic_cast針對一個引用(reference)操作失敗時被拋出);bad_typeid(當dynamic_cast對空指針進行操作時被拋出);bad_exception(用於unexpected異常)----都不是指向對象的指針,所以你必須通過值或引用來捕獲它們。

std::exception的介紹參考:https://blog.csdn.net/fengbingchun/article/details/78303734

14. 審慎使用異常規格(exception specifications)

如果一個函數拋出一個不在異常規格範圍裏的異常,系統在運行時能夠檢測出這個錯誤,然後一個特殊函數std::unexpected將被自動地調用(This function is automatically called when a function throws an exception that is not listed in its dynamic-exception-specifier.)。std::unexpected缺省的行爲是調用函數std::terminate,而std::terminate缺省的行爲是調用函數abort。應避免調用std::unexpected。

避免在帶有類型參數的模板內使用異常規格。

C++允許你用其它不同的異常類型替換std::unexpected異常,通過std::set_unexpected。

15. 瞭解異常處理的系統開銷

採用不支持異常的方法編譯的程序一般比支持異常的程序運行速度更快所佔空間也更小。

爲了減少開銷,你應該避免使用無用的try塊。如果使用try塊,代碼的尺寸將增加並且運行速度也會減慢。

16. 牢記80-20準則(80-20 rule)

80-20準則說的是大約20%的代碼使用了80%的程序資源;大約20%的代碼耗用了大約80%的運行時間;大約20%的代碼使用了80%的內存;大約20%的代碼執行80%的磁盤訪問;80%的維護投入於大約20%的代碼上。基本的觀點:軟件整體的性能取決於代碼組成中的一小部分。

17. 考慮使用lazy evaluation(懶惰計算法)

在某些情況下要求軟件進行原來可以避免的計算,這時lazy evaluation纔是有用的。

18. 分期攤還期望的計算

over-eager evaluation(過度熱情計算法):在要求你做某些事情以前就完成它們。隱藏在over-eager evaluation後面的思想是如果你認爲一個計算需要頻繁進行,你就可以設計一個數據結構高效地處理這些計算需求,這樣可以降低每次計算需求時的開銷。

當你必須支持某些操作而不總需要其結果時,lazy evaluation是在這種時候使用的用以提高程序效率的技術。當你必須支持某些操作而其結果幾乎總是被需要或不止一次地需要時,over-eager是在這種時候使用的用以提高程序效率的一種技術。

19. 理解臨時對象的來源

size_t countChar(const std::string& str, char ch)
{
	// 建立一個string類型的臨時對象,通過以buffer做爲參數調用string的構造函數來初始化這個臨時對象,
	// countChar的參數str被綁定在這個臨時的string對象上,當countChar返回時,臨時對象自動釋放

	// 將countChar(const std::string& str, char ch)修改爲countChar(std::string& str, char ch)則會error
	return 1;
}

#define MAX_STRING_LEN 64

int test_item_19()
{
	char buffer[MAX_STRING_LEN];
	char c;

	std::cin >> c >> std::setw(MAX_STRING_LEN) >> buffer;
	std::cout<<"There are "<<countChar(buffer, c)<<" occurrences of the character "<<c<<" in "<<buffer<<std::endl;

	return 0;
}

在C++中真正的臨時對象是看不見的,它們不出現在你的源代碼中。建立一個沒有命名的非堆(non-heap)對象會產生臨時對象。這種未命名的對象通常在兩種條件下產生:爲了使函數成功調用而進行隱式類型轉換和函數返回對象時

僅當通過傳值(by value)方式傳遞對象或傳遞常量引用(reference-to-const)參數時,纔會發生這些類型轉換。當傳遞一個非常量引用(reference-to-non-const)參數對象,就不會發生。

C++語言禁止爲非常量引用(reference-to-non-const)產生臨時對象。

臨時對象是有開銷的,所以你應該儘可能地去除它們。在任何時候只要見到常量引用(reference-to-const)參數,就存在建立臨時對象而綁定在參數上的可能性。在任何時候只要見到函數返回對象,就會有一個臨時對象被建立(以後被釋放)。

20. 協助完成返回值優化

class Rational20 {
public:
	Rational20(int numerator = 0, int denominator = 1) {}

	int numerator() const { return 1; }
	int denominator() const { return 2; }
};

const Rational20 operator*(const Rational20& lhs, const Rational20& rhs)
{
	// 以某種方法返回對象,能讓編譯器消除臨時對象的開銷:這種技巧是返回constructor argument而不是直接返回對象
	return Rational20(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int test_item_20()
{
	Rational20 a = 10;
	Rational20 b(1, 2);
	Rational20 c = a * b; 

	return 0;
}

一些函數(operator*也在其中)必須要返回對象。這就是它們的運行方法。

C++規則允許編譯器優化不出現的臨時對象(temporary objects out of existence)。

21. 通過重載避免隱式類型轉換

class UPInt21 { // unlimited precision integers class
public:
	UPInt21() {}
	UPInt21(int value) {}
};

const UPInt21 operator+(const UPInt21& lhs, const UPInt21& rhs) // add UPInt21+UPInt21
{
	return UPInt21(1);
}

const UPInt21 operator+(const UPInt21& lhs, int rhs) // add UPInt21+int
{
	return UPInt21(1);
}

const UPInt21 operator+(int lhs, const UPInt21& rhs) // add int+UPInt21
{
	return UPInt21(1);
}

int test_item_21()
{
	UPInt21 upi1, upi2;
	UPInt21 upi3 = upi1 + upi2; // 正確,沒有由upi1或upi2生成臨時對象
	upi3 = upi1 + 10; // 正確,沒有由upi1或10生成臨時對象
	upi3 = 10 + upi2; // 正確,沒有由10或upi2生成臨時對象

	// 注意:註釋掉上面的operator+(UPInt21&, int)和operator+(int, UPInt21&)也正確,但是會通過臨時對象把10轉換爲UPInt21

	return 0;
}

在C++中有一條規則是每一個重載的operator必須帶有一個用戶定義類型(user-defined type)的參數。

利用重載避免臨時對象的方法不只是用在operator函數上。

沒有必要實現大量的重載函數,除非你有理由確信程序使用重載函數以後其整體效率會有顯著的提高。

22. 考慮用運算符的賦值形式(op=)取代其單獨形式(op)

class Rational22 {
public:
	Rational22(int numerator = 0, int denominator = 1) {}
	Rational22& operator+=(const Rational22& rhs) { return *this; }
	Rational22& operator-=(const Rational22& rhs) { return *this; }
};

// operator+根據operator+=實現
const Rational22 operator+(const Rational22& lhs, const Rational22& rhs)
{
	return Rational22(lhs) += rhs;
}

// operator-根據operator-=實現
const Rational22 operator-(const Rational22& lhs, const Rational22& rhs)
{
	return Rational22(lhs) -= rhs;
}

就C++來說,operator+、operator=和operator+=之間沒有任何關係,因此如果你想讓三個operator同時存在並具有你所期望的關係,就必須自己實現它們。同理,operator-, *, /, 等等也一樣。

確保operator的賦值形式(assignment version)(例如operator+=)與一個operator的單獨形式(stand-alone)(例如operator+)之間存在正常的關係,一種好方法是後者(指operator+)根據前者(指operator+=)來實現。

23. 考慮變更程序庫

不同的程序庫在效率、可擴展性、移植性、類型安全和其它一些領域上蘊含着不同的設計理念,通過變換使用給予性能更多考慮的程序庫,你有時可以大幅度地提供軟件的效率。

24. 理解虛擬函數、多繼承、虛基類和RTTI所需的代碼

當調用一個虛擬函數時,被執行的代碼必須與調用函數的對象的動態類型相一致;指向對象的指針或引用的類型是不重要的。大多數編譯器是使用virtual table和virtual table pointers,通常被分別地稱爲vtbl和vptr。

一個vtbl通常是一個函數指針數組。(一些編譯器使用鏈表來代替數組,但是基本方法是一樣的)在程序中的每個類只要聲明瞭虛函數或繼承了虛函數,它就有自己的vtbl,並且類中vtbl的項目是指向虛函數實現體的指針。

你必須爲每個包含虛函數的類的virtual table留出空間。類的vtbl的大小與類中聲明的虛函數的數量成正比(包括從基類繼承的虛函數)。每個類應該只有一個virtual table,所以virtual table所需的空間不會太大,但是如果你有大量的類或者在每個類中有大量的虛函數,你會發現vtbl會佔用大量的地址空間。

一些原因導致現在的編譯器一般總是忽略虛函數的inline指令。

Virtual table只實現了虛擬函數的一半機制,如果只有這些是沒有用的。只有用某種方法指出每個對象對應的vtbl時,它們才能使用。這是virtual table pointer的工作,它來建立這種聯繫。每個聲明瞭虛函數的對象都帶着它,它是一個看不見的數據成員,指向對應類的virtual table。這個看不見的數據成員也稱爲vptr,被編譯器加在對象裏,位置只有編譯器知道。

關於虛函數表的介紹參考:https://blog.csdn.net/fengbingchun/article/details/79592347

虛函數是不能內聯的。這是因爲”內聯”是指”在編譯期間用被調用的函數體本身來代替函數調用的指令”,但是虛函數的”虛”是指”直到運行時才能知道要調用的是哪一個函數”。

RTTI(運行時類型識別)能讓我們在運行時找到對象和類的有關信息,所以肯定有某個地方存儲了這些信息讓我們查詢。這些信息被存儲在類型爲type_info的對象裏,你能通過使用typeid操作符訪問一個類的type_info對象。

關於typeid的使用參考:https://blog.csdn.net/fengbingchun/article/details/51866559

RTTI被設計爲在類的vtbl基礎上實現。

25. 將構造函數和非成員函數虛擬化

虛擬構造函數是指能夠根據輸入給它的數據的不同而建立不同類型的對象。虛擬拷貝構造函數能返回一個指針,指向調用該函數的對象的新拷貝。類的虛擬拷貝構造函數只是調用它們真正的拷貝構造函數。被派生類重定義的虛擬函數不用必須與基類的虛擬函數具有一樣的返回類型。如果函數的返回類型是一個指向基類的指針(或一個引用),那麼派生類的函數可以返回一個指向基類的派生類的指針(或引用)。

26. 限制某個類所能產生的對象數量

阻止建立某個類的對象,最容易的方法就是把該類的構造函數聲明在類的private域。

27. 要求或禁止在堆中產生對象

// 判斷一個對象是否在堆中, HeapTracked不能用於內建類型,因爲內建類型沒有this指針
typedef const void* RawAddress;
class HeapTracked { // 混合類,跟蹤
public:
	class MissingAddress {}; // 從operator new返回的ptr異常類
	virtual ~HeapTracked() = 0;
	static void* operator new(size_t size);
	static void operator delete(void* ptr);
	bool isOnHeap() const;

private:
	static std::list<RawAddress> addresses;
};

std::list<RawAddress> HeapTracked::addresses;

HeapTracked::~HeapTracked() {}

void* HeapTracked::operator new(size_t size)
{
	void* memPtr = ::operator new(size);
	addresses.push_front(memPtr);
	return memPtr;
}

void HeapTracked::operator delete(void* ptr)
{
	std::list<RawAddress>::iterator it = std::find(addresses.begin(), addresses.end(), ptr);
	if (it != addresses.end()) {
		addresses.erase(it);
		::operator delete(ptr);
	} else {
		throw MissingAddress(); // ptr就不是用operator new分配的,所以拋出一個異常
	}
}

bool HeapTracked::isOnHeap() const
{
	// 生成的指針將指向"原指針指向對象內存"的開始處
	// 如果HeapTracked::operator new爲當前對象分配內存,這個指針就是HeapTracked::operator new返回的指針
	const void* rawAddress = dynamic_cast<const void*>(this);
	std::list<RawAddress>::iterator it = std::find(addresses.begin(), addresses.end(), rawAddress);
	return it != addresses.end();
}

class Asset : public HeapTracked {};

// 禁止堆對象
class UPNumber27 {
private:
	static void* operator new(size_t size);
	static void operator delete(void* ptr);
};

void* UPNumber27::operator new(size_t size)
{
	return ::operator new(size);
}

void UPNumber27::operator delete(void* ptr)
{
	::operator delete(ptr);
}

class Asset27 {
public:
	Asset27(int initValue) {}

private:
	UPNumber27 value;
};

int test_item_27()
{
	UPNumber27 n1; // okay
	static UPNumber27 n2; // also okay
	//UPNumber27* p = new UPNumber27; // error, attempt to call private operator new

	// UPNumber27的operator new是private這一點 不會對包含UPNumber27成員對象的對象的分配產生任何影響
	Asset27* pa = new Asset27(100); // 正確,調用Asset::operator new或::operator new,不是UPNumber27::operator new

	return 0;
}

禁止堆對象:禁止用於調用new,利用new操作符總是調用operator new函數這點來達到目的,可以自己聲明這個函數,而且你可以把它聲明爲private。

28. 靈巧(smart)指針

// 大多數靈巧指針模板
template<class T>
class SmartPtr {
public:
	SmartPtr(T* realPtr = 0); // 建立一個靈巧指針指向dumb pointer(內建指針)所指的對象,未初始化的指針,缺省值爲0(null)
	SmartPtr(const SmartPtr& rhs); // 拷貝一個靈巧指針
	~SmartPtr(); // 釋放靈巧指針
	// make an assignment to a smart ptr
	SmartPtr& operator=(const SmartPtr& rhs);
	T* operator->() const; // dereference一個靈巧指針以訪問所指對象的成員
	T& operator*() const; // dereference靈巧指針

private:
	T* pointee; // 靈巧指針所指的對象
};

靈巧指針是一種外觀和行爲都被設計成與內建指針相類似的對象,不過它能提供更多的功能。它們有許多應用的領域,包括資源管理和重複代碼任務的自動化。

在C++11中auto_ptr已經被廢棄,用unique_ptr替代。

std::unique_ptr的使用參考:https://blog.csdn.net/fengbingchun/article/details/52203664

29. 引用計數

class String {
public:
	String(const char* initValue = "");
	String(const String& rhs);
	String& operator=(const String& rhs);
	const char& operator[](int index) const; // for const String
	char& operator[](int index); // for non-const String
	~String();

private:
	// StringValue的主要目的是提供一個空間將一個特別的值和共享此值的對象的數目聯繫起來
	struct StringValue { // holds a reference count and a string value
		int refCount;
		char* data;
		bool shareable; // 標誌,以指出它是否爲可共享的
		StringValue(const char* initValue);
		~StringValue();
	};

	StringValue* value; // value of this String
};

String::String(const char* initValue) : value(new StringValue(initValue))
{}

String::String(const String& rhs)
{
	if (rhs.value->shareable) {
		value = rhs.value;
		++value->refCount;
	} else {
		value = new StringValue(rhs.value->data);
	}
}

String& String::operator=(const String& rhs)
{
	if (value == rhs.value) { // do nothing if the values are already the same
		return *this;
	}

	if (value->shareable && --value->refCount == 0) { // destroy *this's value if no one else is using it
		delete value;
	}

	if (rhs.value->shareable) {
		value = rhs.value; // have *this share rhs's value
		++value->refCount;
	} else {
		value = new StringValue(rhs.value->data);
	}

	return *this;
}

const char& String::operator[](int index) const
{
	return value->data[index];
}

char& String::operator[](int index)
{
	// if we're sharing a value with other String objects, break off a separate copy of the value fro ourselves
	if (value->refCount > 1) {
		--value->refCount; // decrement current value's refCount, becuase we won't be using that value any more
		value = new StringValue(value->data); // make a copy of the value for ourselves
	}

	value->shareable = false;
	// return a reference to a character inside our unshared StringValue object
	return value->data[index];
}

String::~String()
{
	if (--value->refCount == 0) {
		delete value;
	}
}

String::StringValue::StringValue(const char* initValue) : refCount(1), shareable(true)
{
	data = new char[strlen(initValue) + 1];
	strcpy(data, initValue);
}

String::StringValue::~StringValue()
{
	delete[] data;
}

// 基類,任何需要引用計數的類都必須從它繼承
class RCObject {
public:
	void addReference() { ++refCount; }
	void removeReference() { if (--refCount == 0) delete this; } // 必須確保RCObject只能被構建在堆中
	void markUnshareable() { shareable = false; }
	bool isShareable() const { return shareable; }
	bool isShared() const { return refCount > 1; }

protected:
	RCObject() : refCount(0), shareable(true) {}
	RCObject(const RCObject& rhs) : refCount(0), shareable(true) {}
	RCObject& operator=(const RCObject& rhs) { return *this; }
	virtual ~RCObject() = 0;

private:
	int refCount;
	bool shareable;

};

RCObject::~RCObject() {} // virtual dtors must always be implemented, even if they are pure virtual and do nothing

// template class for smart pointers-to-T objects. T must support the RCObject interface, typically by inheriting from RCObject
template<class T>
class RCPtr {
public:
	RCPtr(T* realPtr = 0) : pointee(realPtr) { init(); }
	RCPtr(const RCPtr& rhs) : pointee(rhs.pointee) { init(); }
	~RCPtr() { if (pointee) pointee->removeReference(); }

	RCPtr& operator=(const RCPtr& rhs)
	{
		if (pointee != rhs.pointee) { // skip assignments where the value doesn't change
			if (pointee)
				pointee->removeReference(); // remove reference to current value

			pointee = rhs.pointee; // point to new value
			init(); // if possible, share it else make own copy
		}

		return *this;
	}

	T* operator->() const { return pointee; }
	T& operator*() const { return *pointee; }

private:
	T* pointee; // dumb pointer this object is emulating

	void init() // common initialization
	{
		if (pointee == 0) // if the dumb pointer is null, so is the smart one
			return;

		if (pointee->isShareable() == false) // if the value isn't shareable copy it
			pointee = new T(*pointee);

		pointee->addReference(); // note that there is now a new reference to the value
	}
};

// 將StringValue修改爲是從RCObject繼承
// 將引用計數功能移入一個新類(RCObject),增加了靈巧指針(RCPtr)來自動處理引用計數
class String2 {
public:
	String2(const char* value = "") : value(new StringValue(value)) {}
	const char& operator[](int index) const { return value->data[index]; } // for const String2
	
	char& operator[](int index) // for non-const String2
	{
		if (value->isShared())
			value = new StringValue(value->data);
		value->markUnshareable();
		return value->data[index];
	}

private:
	// StringValue的主要目的是提供一個空間將一個特別的值和共享此值的對象的數目聯繫起來
	struct StringValue : public RCObject { // holds a reference count and a string value
		char* data;

		StringValue(const char* initValue) { init(initValue); }
		StringValue(const StringValue& rhs) { init(rhs.data); }

		void init(const char* initValue)
		{
			data = new char[strlen(initValue) + 1];
			strcpy(data, initValue);
		}

		~StringValue() { delete [] data; }
	};

	RCPtr<StringValue> value; // value of this String2

};

int test_item_29()
{
	String s1("More Effective C++");
	String s2 = s1;
	s1 = s2;
	fprintf(stdout, "char: %c\n", s1[2]);
	String s3 = s1;
	s3[5] = 'x';

	return 0;
}

引用計數是這樣一個技巧,它允許多個有相同值的對象共享這個值的實現。這個技巧有兩個常用動機。第一個是簡化跟蹤堆中的對象的過程。一旦一個對象通過調用new被分配出來,最要緊的就是記錄誰擁有這個對象,因爲其所有者----並且只有其所有者----負責對這個對象調用delete。但是,所有權可以被從一個對象傳遞到另外一個對象(例如通過傳遞指針型參數)。引用計數可以免除跟蹤對象所有權的擔子,因爲當使用引用計數後,對象自己擁有自己。當沒人再使用它時,它自己自動銷燬自己。因此,引用計數是個簡單的垃圾回收體系。第二個動機是由於一個簡單的常識。如果很多對象有相同的值,將這個值存儲多次是很無聊的。更好的辦法是讓所有的對象共享這個值的實現。這麼做不但節省內存,而且可以使得程序運行更快,因爲不需要構造和析構這個值的拷貝。

引用計數介紹參考:https://blog.csdn.net/fengbingchun/article/details/85861776

實現引用計數不是沒有代價的。每個被引用的值帶一個引用計數,其大部分操作都需要以某種形式檢查或操作引用計數。對象的值需要更多的內存,而我們在處理它們時需要執行更多的代碼。引用計數是基於對象通常共享相同的值的假設的優化技巧。如果假設不成立的話,引用計數將比通常的方法使用更多的內存和執行更多的代碼。另一方面,如果你的對象確實有具有相同值的趨勢,那麼引用計數將同時節省時間和空間。

30. 代理類

template<class T>
class Array2D { // 使用代理實現二維數組
public:
	Array2D(int i, int j) : i(i), j(j)
	{
		data.reset(new T[i*j]);
	}

	class Array1D { // Array1D是一個代理類,它的實例扮演的是一個在概念上不存在的一維數組
	public:
		Array1D(T* data) : data(data) {}
		T& operator[](int index) { return data[index]; }
		const T& operator[](int index) const { return data[index]; }

	private:
		T* data;
	};

	Array1D operator[](int index) { return Array1D(data.get()+j*index); }
	const Array1D operator[](int index) const { return Array1D(data.get()+j*index); }

private:
	std::unique_ptr<T[]> data;
	int i, j;
};

// 可以通過代理類幫助區分通過operator[]進行的是讀操作還是寫操作
class String30 {
public:
	String30(const char* value = "") : value(new StringValue(value)) {}
	
	class CharProxy { // proxies for string chars
	public:
		CharProxy(String30& str, int index) : theString(str), charIndex(index) {}

		CharProxy& operator=(const CharProxy& rhs)
		{
			// if the string is haring a value with other String objects,
			// break off a separate copy of the value for this string only
			if (theString.value->isShared())
				theString.value = new StringValue(theString.value->data);

			// now make the assignment: assign the value of the char
			// represented by rhs to the char represented by *this
			theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
			return *this;
		}
		
		CharProxy& operator=(char c)
		{
			if (theString.value->isShared())
				theString.value = new StringValue(theString.value->data);
			theString.value->data[charIndex] = c;
			return *this;
		}

		operator char() const { return theString.value->data[charIndex]; }

	private:
		String30& theString;
		int charIndex;
	};

	const CharProxy operator[](int index) const // for const String30
	{
		return CharProxy(const_cast<String30&>(*this), index);
	}

	CharProxy operator[](int index) // for non-const String30
	{
		return CharProxy(*this, index);
	}

	//friend class CharProxy;
private:
	// StringValue的主要目的是提供一個空間將一個特別的值和共享此值的對象的數目聯繫起來
	struct StringValue : public RCObject { // holds a reference count and a string value
		char* data;

		StringValue(const char* initValue) { init(initValue); }
		StringValue(const StringValue& rhs) { init(rhs.data); }

		void init(const char* initValue)
		{
			data = new char[strlen(initValue) + 1];
			strcpy(data, initValue);
		}

		~StringValue() { delete [] data; }
	};

	RCPtr<StringValue> value; // value of this String30

};

int test_item_30()
{
	Array2D<float> data(10, 20);
	fprintf(stdout, "%f\n", data[3][6]);

	String30 s1("Effective C++"), s2("More Effective C++"); // reference-counted strings using proxies
	fprintf(stdout, "%c\n", s1[5]); // still legal, still works
	s2[5] = 'x'; // also legal, also works
	s1[3] = s2[8]; // of course it's legal, of course it works

	//char* p = &s1[1]; // error, 通常,取proxy對象地址的操作與取實際對象地址的操作得到的指針,其類型是不同的,重載CharProxy類的取地址運算可消除這個不同

	return 0;
}

可以通過代理類實現二維數組。

可以通過代理類幫助區分通過operator[]進行的是讀操作還是寫操作。

Proxy類可以完成一些其它方法很難甚至可不能實現的行爲。多維數組是一個例子,左值/右值的區分是第二個,限制隱式類型轉換是第三個。

同時,proxy類也有缺點。作爲函數返回值,proxy對象是臨時對象,它們必須被構造和析構。Proxy對象的存在增加了軟件的複雜度。從一個處理實際對象的類改換到處理proxy對象的類經常改變了類的語義,因爲proxy對象通常表現出的行爲與實際對象有些微妙的區別。

31. 讓函數根據一個以上的對象來決定怎麼虛擬

32. 在未來時態下開發程序

未來時態的考慮增加了你的代碼的可重用性、可維護性、健壯性,以及在環境發生改變時易於修改。

33. 將非尾端類設計爲抽象類

34. 如何在同一程序中混合使用C++和C

名變換:就是C++編譯器給程序的每個函數換一個獨一無二的名字。在C中,這個過程是不需要的,因爲沒有函數重載,但幾乎所有C++程序都有函數重名。要禁止名變換,使用C++的extern “C”。不要將extern “C”看作是聲明這個函數是用C語言寫的,應該看作是聲明這個函數應該被當作好像C寫的一樣而進行調用。

靜態初始化:在main執行前和執行後都有大量代碼被執行。尤其是,靜態的類對象和定義在全局的、命名空間中的或文件體中的類對象的構造函數通常在main被執行前就被調用。這個過程稱爲靜態初始化。同樣,通過靜態初始化產生的對象也要在靜態析構過程中調用其析構函數,這個過程通常發生在main結束運行之後。

動態內存分配:C++部分使用new和delete,C部分使用malloc(或其變形)和free。

數據結構的兼容性:在C++和C之間這樣相互傳遞數據結構是安全的----在C++和C下提供同樣的定義來進行編譯。在C++版本中增加非虛成員函數或許不影響兼容性,但幾乎其它的改變都將影響兼容。

如果想在同一程序下混合C++與C編程,記住下面的指導原則:(1).確保C++和C編譯器產生兼容的obj文件;(2).將在兩種語言下都使用的函數聲明爲extern “C”;(3).只要可能,用C++寫main();(4).總用delete釋放new分配的內存;總用free釋放malloc分配的內存;(5).將在兩種語言間傳遞的東西限制在用C編譯的數據結構的範圍內;這些結構的C++版本可以包含非虛成員函數。

35. 讓自己習慣使用標準C++語言

GitHubhttps://github.com/fengbingchun/Messy_Test

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