1. 程式人生 > >c++ primer第五版----學習筆記(十九)Ⅰ

c++ primer第五版----學習筆記(十九)Ⅰ

文章目錄

特殊工具與技術

1.控制記憶體分配

  • 應用程式需要過載new和delete運算子以控制記憶體分配

1.1 過載new和delete

  • 使用一個new表示式時,實際執行了三步操作
  1. new表示式呼叫一個名為delete new(或者operator new[])的標準庫函式
  2. 編譯器執行相應的建構函式初始化特定型別的物件
  3. 物件被分配了空間並構造完成,返回一個指向該物件的指標
  • 使用了delete表示式刪除一個動態分配的物件

1.對指標所指的物件或陣列中的元素執行對應的解構函式
2.編譯器呼叫名為operator delete(或者operator delete[])釋放記憶體空間

  • 當自定義了全域性的operator new函式和operator delete函式之後,我們就擔負起了控制動態記憶體分配的職責
  • 首先查詢是否有自定義版本,若有則呼叫自定義版本,沒有則呼叫標準庫版本
  • 可以使用作用域運算子令new、delete忽略類中的函式,直接執行全域性版本
::new;  //執行全域性作用域中的版本
  • 型別nothrow_t是定義在new標頭檔案的一個struct,使用者可以通過這個物件請求new或delete的非丟擲版本
void *operator new(size_t, nothrow_t&) noexcept; //noexcept指定不丟擲異常
coid *operator delete(void*, nothrow_t&) noexcept;
  • 當將上述運算子定義成類的成員時,它們隱式靜態的因為operator new用在物件建構函式之前而operator delete用在物件銷燬之後,所以new和delete必須是靜態的,而且不能操縱類的任何資料成員
  • 自定義operator new函式,則可以為它提供額外的形參;但下面的函式不能被使用者過載
void *operator new(size_t, void*);  //不允許重新定義這個版本呢
  • 一條new表示式執行過程總是先呼叫operator new函式以獲取記憶體空間,然後在得到的記憶體空間中構造物件;delete表示式總是先銷燬物件,然後呼叫operator delete函式釋放物件所佔的記憶體空間
  • 當自定義了全域性的operator new和operator delete函式後,可以使用名為malloc和free執行分配記憶體和釋放記憶體的操作
void *operator new(size_t size) {
	if (void *mem = malloc(size))
		return mem;
	else
		throw bad_alloc();
}
void operator delete(void *mem) noexcept { free(mem); }

1.2 定位new表示式

  • operator new和operator delete函式與allocator的allocate成員和deallocate成員類似,負責分配或釋放空間,但不會構造或銷燬物件
  • 對於operator new分配的記憶體空間無法使用construct函式建構函式,應該使用new的定位new形式構造物件:
new (place_address) type  //place_address必須是一個指標
  • 當只傳入一個指標型別的實參時,定位new表示式構造物件但不分配記憶體;傳給new的指標無須指向operator new分配的記憶體
  • 呼叫建構函式會銷燬物件,但是不會釋放記憶體

2. 執行時型別識別

  • 執行時型別識別(RTTI)的功能由兩個運算子實現:
    1.typeidd運算子,用於返回表示式的型別
    2.dynamic_cast運算子,用於將基類的指標或引用安全地轉換成派生類的指標或引用
  • 當我們將這兩個運算子用於某種型別的指標或引用,並且該型別含有虛擬函式時,運算子將使用指標或飲用所繫結物件的動態型別(動態繫結)
  • RTTI適用:想使用基類物件的指標或引用執行某個派生類操作並且該操作不是虛擬函式

2.1 dynamic_cast運算子

  • dynamic_cast運算子使用形式:
dynamic_cast<type*>(e)  //type必須是一個類型別,並且通常情況下該型別應該含有虛擬函式
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
  • e的型別必須符合以下三個條件中的任意一個:e的型別是目標type的共有派生類、e的型別是目標type的共有基類或者e的型別就是type的型別
  • 一條dynamic_cast語句,指標型別轉換失敗返回0,引用型別轉換失敗返回bad_cast異常,該異常定義在typeinfo標準庫標頭檔案中
  • 我們可以對一個空指標執行dynamic_cast,結果是所需型別的空指標
  • 在條件部分執行dynamic_cast操作可以確保型別轉換和結果檢查在同一條表示式中完成

2.2 typeid運算子

  • typeid表示式的形式:
typeid(e)  //e可以是任意表達式或型別的名字,得到結果為一個常量的引用
  • 如果表示式是一個引用,則typeid返回該引用所引物件的型別;如果對陣列a執行typeid(a),則所得結果是陣列型別而非指標型別
  • 當typeid作用於指標時(而非指標所指的物件),返回的結果是該指標的靜態編譯型別(即指標型別)
  • 如果表示式的動態型別可能與靜態型別不同,則必須在執行時對錶達式求值以確定返回的型別(即動態繫結)

2.3使用RTTI

  • 解決具有繼承關係的類實現相等運算子
  • 定義相等運算子,形參是基類的引用,然後使用typeid檢查兩個運算獨物件的型別是否一致。如果型別不一致,則==返回false;型別一致才呼叫equal函式
class Base {
	friend bool operator==(const  Base&, const Base&);
public:
	//Base的介面成員
protected:
	virtual bool equal(const Base&) const;
};
class Derived : public Base {
public:
	//Derived的其他介面成員
protected:
	bool equal(const Base&) const;
	//Derived的資料成員和其他用於實現的成員
};
bool operator==(const Base &lhs, const Base &rhs)
{
	return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
bool Derived::equal(const Base &rhs) const
{
	auto r = dynamic_cast<const Derived&>(rhs);
}
bool Base::equal(const Base &rhs) cosnt
{
	//執行比較Base物件的操作
}

2.4 type_info類

  • type_info類必須定義在typeinfo標頭檔案中,並至少提供以下的操作:
type_info的操作
t1 == t2 t1和t2表示同一種類型,返回true,否則返回false
t1 != t2 t1和t2表示不同型別,返回true,否則返回false
t.name() 返回一個C風格字串,表示型別名字
t1.before(t2) 返回一個bool值,表示t1是否位於t2之前

3. 列舉型別

  • 列舉型別使我們可以將一組整型常量組織在一起;列舉屬於字面值常量型別
  • 帶有關鍵字class或struct的列舉型別為限定作用域的列舉型別;省略掉關鍵字的則為不限定作用域的列舉型別
enum class open_modes {input, output, append};  //限定作用域
enum color {red, yellow, green};  //不限定作用域
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10}; //enum未命名,只能在定義enum時定義它的物件
  • 限定作用域的列舉型別,成員名字遵循常規的作用域準則,並且在列舉型別的作用域外是不可訪問的;在不限定作用域的列舉型別中,列舉成員的作用域與列舉本身的作用域相同
  • 預設情況下,列舉值從0開始,依次加1,也可以指定專門的值
  • 列舉成員是const,因此在初始化成員時提供的初始值必須是常量表達式
enum class intTypes {
	charType = 8, shortType = 16, intType = 16,
	longType = 32, long_longType = 64
};
constexpr intTypes charbits = intTypes::charType;  //必須是常量表達式

//必須使用該型別的一個列舉成員或者該型別的另一個物件初始化enum物件
open_modes om = 2;   //錯誤,2不屬於型別open_modes
om = open_modes::input;  //正確:input是open_modes的一個列舉成員
  • 一個不限定作用域的列舉型別的物件或列舉成員自動地轉換成整型
  • 可以先宣告不定義,enum的前置宣告必須指定其成員的大小;對於不限定作用域的enum來說,隱式定義為int:
enum intValues : unsigned long long;  //不限定作用域,必須指定成員型別
enum class open_modes;  //限定作用域,隱式定義成int
  • 對於形參匹配,即使某個整型值恰好與列舉成員的值相等,也不能作為函式的enum實參使用
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main() {
	Tokens curTok = INLINE;
	ff(128);   //精確匹配ff(int)
	ff(INLINE);  //精確匹配ff(Tokens)
	ff(curTok);  //精確匹配ff(Tokens)
	return 0;
}

4. 類成員指標

  • 成員指標:可以指向類的非靜態成員的指標
  • 類的靜態成員不屬於任何物件,因此無須特殊的指向靜態成員的指標,指向靜態的指標與普通指標並無區別
  • 成員指標的型別囊括了類的型別以及成員的型別

4.1 資料成員指標

  • 成員指標的宣告:

1.需指明當前宣告的是指標
2.必須在 * 之前新增類名

const string Screen::*pdata; //一個指向Screen類的從const string成員的指標
//宣告成指向const string成員的指標意味著pdata可以指向任何Screen物件的成員
  • 當我們初始化一個成員指標時,需指定它所指的成員
pdata = &Screen::contents;  //將取地址符作用於Screen類的成員而非記憶體中的一個該類物件
  • 當我們初始化一個成員指標或為成員指標賦值時,該指標並未指向任何資料,只有解引用成員指標時才提供物件的資訊(兩種成員指標訪問運算子)
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata;   //.*和->*運算子
s = pScreen->*pdata;
  • ==資料成員一般情況下是私有的,通常不能直接獲得資料成員的指標,最好定義一個函式,令其返回值是指向該成員的指標:
class Screen {
public:
	static const std::string Screen::*data()
		{ return &Screen::contents; }
};

4.2 成員函式指標

  • 定義指向類的成員函式的指標,使用auto自動判斷指向成員函式的指標型別:
//pmf是一個指標,可以指向Screen的某個常量成員函式
auto pmf = &Screen::get_cursor;
  • 如果成員存在過載的情況,則我們必須顯示宣告函式型別以明確指出我們想要使用的是哪個函式
  • 在成員函式和指向該成員的指標之間不存在自動轉換規則
pmf = &Screen::get;   //必須顯示地使用取地址運算子
  • 因為函式呼叫運算子的優先順序較高,所以在宣告指向成員函式的指標並使用這樣的指標進行函式呼叫時,括號必不可少:(C::*p)(parms)和(obj.*p)(args)

  • 使用成員指標的類型別名:

using Action = char (Screen*)(Screen::pos, Screen::pos) const;
  • 成員函式表,儲存自身函式的指標,為private,組成一個數組

4.3 將成員函式用作可呼叫物件

  • 因為成員指標不是可呼叫物件,所以我們不能直接將一個指向成員函式的指標傳遞給演算法,必須使用.*或->*呼叫成員指標
  • 從指向成員函式的指標獲取可呼叫物件的一種方法是使用標準庫模板function
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
  • 通過標準庫功能mem_fn來讓編譯器負責推斷成員型別;使用bind生成一個可呼叫物件

5. 巢狀類

  • 一個類可以定義在另一個類的內部,前者稱為巢狀類或者巢狀型別;巢狀類是一個獨立的類,與外層類基本沒有什麼關係,外層類的物件與巢狀類的物件也是相互獨立的
  • 巢狀類的名字在外層類作用域中是可見的,在外層類作用域之外不可見
  • 巢狀類必須宣告在類的內部,但是可以定義在類的內部或者外部
  • 巢狀類的所有成員定義都需要指明作用域

6. union

  • 聯合(union)是一種特殊的類,一個union可以有多個數據成員,但在任意時刻只有一個數據成員有值,分配給一個union物件的儲存空間至少要容納它的最大的資料成員
  • union不能含有引用型別的成員
  • 由於union既不能繼承自其他類,也不能作為基類使用,所以在union中不能含有虛擬函式
  • 使用一對花括號內的初始值顯示的初始化一個union:
union Token {
	char cval;
	int ival;
	double dval;
};
Token first_token = {'a'};  //初始化cval成員
  • 匿名union不能包含保護的成員或私有成員,也不能定義成員函式
  • 通常把含有類型別成員的union內嵌在另一個類當中,並將我們的union定義成匿名union,將自身類型別成員的控制權轉移給該類
  • 定義一個列舉型別(判別式)的成員來追蹤其union成員的狀態

7. 區域性類

  • 區域性類:類定義在某個函式的內部,所有成員都必須完整定義在類的內部,也不允許申請靜態資料成員
  • 如果類定義在某個函式內部,則該函式的普通區域性變數不能被該區域性類所使用:
  • 外層函式對區域性類的私有成員沒有任何訪問特權

8. 固有的不可移植的特性

  • c++從c語言繼承而來的另外兩種不可移植的特性:位域volatile限定符

8.1 位域

  • 當一個程式需要向其他程式或硬體裝置傳遞二進位制資料時,通常會用到位域
  • 位域的宣告形式是在成員名字之後緊跟一個冒號以及一個常量表達式,該表示式用於指定成員所佔的二進位制位數:
typedef unsigned int Bit;
class File {
	Bit mode : 2;    //mode佔2位
	Bit modified: 1;  //modified佔1位
	Bit prot_owner: 3;
	//...
};
  • 取地址運算子不能作用域位域,因此任何指標都無法指向類的位域

8.2 volatile限定符

  • 當物件的值可能在程式的控制或檢測之外被改變時,應該將該物件宣告為volatile,volatile告訴編譯器不該對這樣的物件進行優化
  • 只有volatile的成員函式才能被volatile的物件呼叫
volatile int v;  //v是一個colatile int
int *ip = &v;  //錯誤:必須使用指向volatile的指標
  • 可以將形參型別指定為const volatile引用,這樣就能利用任意型別的類物件進行拷貝或復值

8.3 連結指示:extern “C”

  • c++使用連結指示指出任意非C++函式所用的語言
  • 宣告一個非C++函式:
ertern "C" size_t strlen(const char*);
  • 花括號的作用是將適用於該連結指示的多個宣告聚合在一起:
ertern "C" {
#include <string.h>
}
  • 當我們使用連結指示時,它不僅對函式有效,而且對作為返回型別或形參型別的函式指標有效
ertern "C" void f1(void(*)(int));
  • 如果希望傳給c++函式傳入一個指向C函式的指標,必須使用類型別名
extern "C" typedef void FC(int);
void f2(FC *);
  • C語言不支援過載,因此一個C連結指示只能說明一組過載函式中的一個