第十九章 特殊工具與技術(shù)
控制內(nèi)存分配
1. 重載new和delete
重載這兩個(gè)運(yùn)算符與重載其他運(yùn)算符的過程大不相同私股。想要真正重載new和delete的方法,首先要對new表達(dá)式和delete表達(dá)式的工作機(jī)制足夠了解:
// new表達(dá)式
string *sp = new string("a value"); // 分配并初始化一個(gè)string對象
string *arr = new string[10]; // 分配10個(gè)默認(rèn)初始化的string對象
當(dāng)我們使用一條new表達(dá)式時(shí)酬屉,實(shí)際上執(zhí)行了三步操作:
- 第一步:new表達(dá)式調(diào)用一個(gè)名為operator new或者operator new[]的標(biāo)準(zhǔn)庫函數(shù),該函數(shù)分配一塊足夠大的靶衍、原始的司浪、未命名的空間以便存儲(chǔ)特定類型的對象(或者對象的數(shù)組)
- 第二步:編譯器運(yùn)行相應(yīng)的構(gòu)造函數(shù)以構(gòu)造這些對象,并為其傳入初始值
- 第三步:對象被分配了空間并構(gòu)造完成挠蛉,返回一個(gè)指向該對象的指針
delete sp; // 銷毀*sp, 然后釋放sp指向的內(nèi)存空間
delete [] arr; // 銷毀數(shù)組中的元素, 然后釋放對應(yīng)的內(nèi)存空間
當(dāng)我們使用一條delete表達(dá)式刪除一個(gè)動(dòng)態(tài)分配的對象時(shí)祭示,實(shí)際上執(zhí)行了兩步操作:
- 第一步:對sp所指的對象或者arr所指的數(shù)組中的元素執(zhí)行對應(yīng)的析構(gòu)函數(shù)
- 第二步:編譯器調(diào)用名為operator delete或者operator delete[]的標(biāo)準(zhǔn)庫函數(shù)釋放內(nèi)存空間
應(yīng)用程序可以在全局作用域中定義operator new函數(shù)和operator delete函數(shù),也可以把它們定義為成員函數(shù)谴古。當(dāng)編譯器發(fā)現(xiàn)一條new表達(dá)式或者delete表達(dá)式后质涛,將在程序中查找可供調(diào)用的operator函數(shù):
- 如果被分配(釋放)的對象是類類型,則編譯器首先在類及其基類的作用域中查找
- 否則在全局作用域中查找掰担,如果找到了用戶自定義的版本汇陆,則使用該版本執(zhí)行new或者delete表達(dá)式
- 沒找到的話,則使用標(biāo)準(zhǔn)庫定義的版本
我們可以使用作用域運(yùn)算符使得new表達(dá)式或delete表達(dá)式忽略定義在類中的函數(shù)带饱,直接執(zhí)行全局作用域的版本毡代。比如::new
和::delete
。
2. operator new接口和operator delete接口
標(biāo)準(zhǔn)庫定義了operator new函數(shù)和operator delete函數(shù)的8個(gè)重載版本勺疼。其中前4個(gè)可能拋出bad_alloc異常教寂,后4個(gè)版本不會(huì)拋出異常:
// 這些版本可能拋出異常
void *operator new(size_t); // 分配一個(gè)對象
void *operator new[](size_t); // 分配一個(gè)數(shù)組
void *operator delete(void*) noexcept; // 釋放一個(gè)對象
void *operator delete[](void*) noexcept; // 釋放一個(gè)數(shù)組
// 這些版本承諾不會(huì)拋出異常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
標(biāo)準(zhǔn)庫函數(shù)operator new和operator delete的名字讓人容易誤解。和其他operator函數(shù)不同执庐,這兩個(gè)函數(shù)并沒有重載new表達(dá)式或者delete表達(dá)式酪耕。實(shí)際上我們根本無法自定義new表達(dá)式或者delete表達(dá)式的行為。一條new表達(dá)式的執(zhí)行過程總是先調(diào)用operator new函數(shù)以獲取內(nèi)存空間轨淌,然后在得到的內(nèi)存空間中構(gòu)造對象迂烁。與之相反,一條delete表達(dá)式的執(zhí)行過程總是先銷毀對象递鹉,然后調(diào)用operator delete函數(shù)釋放對象所占空間盟步。
我們提供新的operator new和operator delete函數(shù)的目的在于改變內(nèi)存分配的方式。
3. malloc函數(shù)和free函數(shù)
malloc函數(shù)接受一個(gè)表示待分配字節(jié)數(shù)的size_t躏结,返回指向分配空間的指針或者返回0以表示分配失敗却盘。free函數(shù)接受一個(gè)void*,它是malloc返回的指針的副本媳拴,free將相關(guān)內(nèi)存返回給系統(tǒng)黄橘。調(diào)用free(0)沒有任何意義。
下面給出了operator new和operator delete的簡單方式:
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); }
4. 定位new表達(dá)式
C++早期版本中禀挫,allocator類還不是標(biāo)準(zhǔn)庫一部分旬陡。應(yīng)用程序如果想把內(nèi)存分配和初始化分離開的話,需要調(diào)用operator new和operator delete语婴。這兩個(gè)函數(shù)的行為與allocator的allocate成員和deallocate成員非常類似描孟,它們負(fù)責(zé)分配或釋放內(nèi)存空間,但是不會(huì)構(gòu)造或銷毀對象砰左。
與allocator不同的是匿醒,對于operator new分配的內(nèi)存空間,我們不能使用construct函數(shù)構(gòu)造對象缠导。相反我們應(yīng)該用new的定位new形式構(gòu)造對象廉羔。
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
其中place_address必須是一個(gè)指針,同時(shí)在initializers中提供一個(gè)(可能為空)的以逗號值分割的初始值列表僻造,該初始值列表用于構(gòu)造新分配的對象憋他。當(dāng)僅通過一個(gè)地址值調(diào)用時(shí)孩饼,定位new使用operator new(size_t, void*),這是以一個(gè)我們無法自定義的operator new版本竹挡,它只是簡單地返回指針實(shí)參镀娶,然后由new表達(dá)式負(fù)責(zé)在指定的地址初始化對象以完成整個(gè)工作。
當(dāng)只傳入一個(gè)指針類型的實(shí)參時(shí)揪罕,定位new表達(dá)式構(gòu)造對象但是不分配內(nèi)存梯码,它允許我們在一個(gè)特定的、預(yù)先分配的內(nèi)存地址上構(gòu)造對象好啰。
盡管定位new與allocator的construct非常相似轩娶,但是有一個(gè)重要的區(qū)別:我們傳給construct的指針必須指向同一個(gè)allocator對象分配的空間,但是傳給定位new的指針無須指向operator new分配的內(nèi)存框往,甚至不需要指向動(dòng)態(tài)內(nèi)存鳄抒。
5. 顯式的析構(gòu)函數(shù)調(diào)用
就像定位new與使用allocate類似一樣,對析構(gòu)函數(shù)的顯式調(diào)用也與使用destroy很類似搅窿。
string *sp = new string("a value"); // 分配并初始化一個(gè)string對象
sp->~string();
和調(diào)用destroy類似嘁酿,調(diào)用析構(gòu)函數(shù)可以清除給定的對象但是不會(huì)釋放該對象所在的空間。如果需要的話男应,我們可以重新使用該空間闹司。
調(diào)用析構(gòu)函數(shù)會(huì)銷毀對象,但是不會(huì)釋放內(nèi)存沐飘。
運(yùn)行時(shí)類型識別
運(yùn)行時(shí)類型識別run-time type identification, RRTTI
的功能由兩個(gè)運(yùn)算符實(shí)現(xiàn):
- typeid運(yùn)算符游桩,用于返回表達(dá)式的類型
- dynamic_cast運(yùn)算符,用于將基類的指針或引用安全地轉(zhuǎn)換成派生類的指針或引用
當(dāng)我們將兩個(gè)運(yùn)算符用于某種類型的指針或者引用時(shí)耐朴,并且該類型含有虛函數(shù)時(shí)借卧,運(yùn)算符將使用指針或者引用所綁定對象的動(dòng)態(tài)類型。
這兩個(gè)運(yùn)算符特別適用于如下情況:當(dāng)我們想使用幾類對象的指針或者引用執(zhí)行某個(gè)派生類操作并且該操作不是虛函數(shù)筛峭。一般來說铐刘,只要有可能我們應(yīng)該盡量使用虛函數(shù),當(dāng)操作被定義成虛函數(shù)時(shí)影晓,編譯器將根據(jù)對象的動(dòng)態(tài)類型自動(dòng)地選擇正確的函數(shù)版本镰吵。
然而并非任何時(shí)候都能定義一個(gè)虛函數(shù)。假設(shè)我們無法使用虛函數(shù)挂签,那么可以使用一個(gè)RTTI運(yùn)算符疤祭。另一方面,與虛成員函數(shù)相比饵婆,使用RTTI運(yùn)算符蘊(yùn)涵著更多潛在的風(fēng)險(xiǎn):程序員必須清楚地知道轉(zhuǎn)換的目標(biāo)類型并且必須檢查類型轉(zhuǎn)換是否被成功執(zhí)行勺馆。
使用RTTI必須加倍小心,在可能的情況下,最好定義虛函數(shù)而非直接接管類型管理的重任草穆。
1. dynamic_cast運(yùn)算符
dynamic_cast運(yùn)算符的使用形式如下所示:
dynamic_cast<type*>(e) // e必須是一個(gè)有效的指針
dynamic_cast<type&>(e) // e必須是一個(gè)左值
dynamic_cast<type&&>(e) // e不能是左值
在上面的所有形式中灌灾,e的類型必須符合以下三個(gè)條件的任意一個(gè):
- e的類型是目標(biāo)type的公有派生類
- e的類型是目標(biāo)type的公有基類
- e的類型是目標(biāo)type本身
如果符合則轉(zhuǎn)換可以成功,否則轉(zhuǎn)換失敗续挟。如果一條dynamic_cast的轉(zhuǎn)換目標(biāo)是指針類型并且失敗了紧卒,則結(jié)果為0侥衬;如果轉(zhuǎn)換目標(biāo)是引用類型并且失敗了诗祸,則拋出一個(gè)bad_cast異常。
1.1 指針類型的dynamic_cast
假定Base類至少含有一個(gè)虛函數(shù)轴总,Derived是Base的公有派生類直颅。如果有一個(gè)指向Base的指針bp,則我們在運(yùn)行時(shí)將它轉(zhuǎn)換成指向Derived的指針:
if (Derived *dp = dynamic_cast<Derived*>(bp))
{
// 使用dp指向的Derived對象
} else { // bp指向一個(gè)Base對象
// 使用bp指向的Base對象
}
1.2 引用類型的dynamic_cast
void f(const Base &b)
{
try {
const Derived &d = dynamic_cast<const Derived&>(b);
// 使用b引用的Derived對象
} catch (bad_cast) {
// 處理類型轉(zhuǎn)換失敗的情況
}
}
2. typeid運(yùn)算符
typeid可以作用于任意類型的表達(dá)式怀樟。和往常一樣功偿,頂層const被忽略,如果表達(dá)式是一個(gè)引用往堡,則typeid返回該引用所引對象的類型械荷。不過當(dāng)typeid作用于數(shù)組或者函數(shù)時(shí),并不會(huì)執(zhí)行向指針的標(biāo)準(zhǔn)類型轉(zhuǎn)換虑灰。比如我們對數(shù)組a執(zhí)行typeid(a)吨瞎。所得的結(jié)果是數(shù)組類型而非指針類型。
當(dāng)運(yùn)算對象不屬于類類型或者是一個(gè)不包含任何虛函數(shù)的類時(shí)穆咐,typeid運(yùn)算符指示的是運(yùn)算對象的靜態(tài)類型颤诀。而當(dāng)運(yùn)算對象是定義了至少一個(gè)虛函數(shù)的類的左值時(shí),typeid的結(jié)果直到運(yùn)行時(shí)才會(huì)求得对湃。
通常情況下我們使用typeid比較兩條表達(dá)式的類型是否相同崖叫,或者比較一條表達(dá)式的類型是否與指定類型相同:
Derived *dp = new Derived;
Base *bp = dp; // 兩個(gè)指針都指向Derived對象
// 在運(yùn)行時(shí)比較兩個(gè)對象的類型
if (typeid(*bp) == type(*dp)) {
// bp和dp指向通醫(yī)藥類型對象
}
// 檢查類型是否是某種指定類型
if (typeid(*bp) == typeid(Derived)) {
// bp實(shí)際指向Derived類型
}
注意typeid應(yīng)該作用于對象,因此我們使用*bp而不是bp:
// 下面檢查永遠(yuǎn)失敗: bp類型是指向Base的指針
if (typeid(bp) == typeid(Derived)) {
// 此處代碼永遠(yuǎn)不會(huì)執(zhí)行
}
當(dāng)typeid作用于指針時(shí)(而非指針指向的對象)拍柒,返回的結(jié)果是該指針的靜態(tài)編譯時(shí)類型心傀。
3. 使用RTTI
在某些情況下RTTI非常有用,比如我們想為具有繼承關(guān)系的類實(shí)現(xiàn)相等運(yùn)算符時(shí)拆讯。對于兩個(gè)對象來說脂男,如果他們的類型相同并且對應(yīng)的數(shù)據(jù)成員取值相同,則我們說這兩個(gè)類是相等的往果。
我們定義兩個(gè)示例類:
class Base {
friend bool operator==(const Base&, const Base&);
public:
// Base的接口成員
protected:
virtual bool equal(const Base&) const;
// Base的數(shù)據(jù)成員和其他用于實(shí)現(xiàn)的成員
};
class Derived: public Base {
public:
// Derived的其他接口成員
protected:
bool equal(const Base&) const;
// Derived的數(shù)據(jù)成員和其他用于實(shí)現(xiàn)的成員
};
類型敏感的相等運(yùn)算符:
bool operator==(const Base &lhs, const Base &rhs)
{
// 如果typeid不相同,則返回fallse; 否則虛調(diào)用equal
// 當(dāng)運(yùn)算對象是Base的對象時(shí)調(diào)用Base::equal, 否則調(diào)用Derived::equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
虛equal函數(shù):繼承體系中的每個(gè)類都必須定義自己的equal函數(shù)疆液,派生類的所有函數(shù)要做的第一件事情就是將實(shí)參的類型轉(zhuǎn)換為派生類類型:
bool Derived::equal(const Base &rhs) const
{
// 我們清楚兩個(gè)類型是相等的, 所以轉(zhuǎn)換不會(huì)拋出異常
auto r = dynamic_cast<const Derived&>(rhs);
// 執(zhí)行比較兩個(gè)Derived對象的操作并返回結(jié)果
}
基類equal函數(shù):
bool Base::equal(const Base &rhs) const
{
// 執(zhí)行比較Base對象的操作
}
4. type_info類
type_info的操作包括:
- t1 == t2:如果type_info對象t1和t2表示同一種類型,則返回true
- t1 != t2:如果type_info對象t1和t2表示不同的類型陕贮,則返回true
- t.name():返回一個(gè)C風(fēng)格字符串堕油,表示類型名字的可打印形式
- t1.before(t2):返回一個(gè)bool值,表示t1是否位于t2之前,順序關(guān)系依賴于編譯器
type_info類沒有默認(rèn)構(gòu)造函數(shù)掉缺,而且它的拷貝和移動(dòng)構(gòu)造函數(shù)以及賦值運(yùn)算符都被定義為刪除的卜录。因此,我們無法定義或者拷貝type_info類型的對象眶明,也不能為type_info對象賦值艰毒。創(chuàng)建type_info對象的唯一途徑就是使用typeid運(yùn)算符。
枚舉類型
C++包含兩種枚舉:限定作用域和不限定作用域的搜囱。C++新標(biāo)準(zhǔn)引入了限定作用域的枚舉類型丑瞧。
定義限定作用域的枚舉類型:
enum class open_modes {input, output, append};
// 等價(jià)
enum struct open_modes {input, output, append};
定義不限定作用域的枚舉類型:
- 省略掉關(guān)鍵字class
- 枚舉名字是可選的
enum color {red, yellow, green};
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
1. 枚舉也可以定義新的類型
enum color {red, yellow, green}; // 不限定作用域的枚舉類型
enum stoplight {red, yellow, green}; // 錯(cuò)誤: 重復(fù)定義了枚舉成員
enum class peppers {red, yellow, green}; // 正確: 枚舉成員被隱藏了
int i = color::red; // 正確: 不限定作用域的枚舉類型的枚舉成員隱式地轉(zhuǎn)換成int
int j = peppers::red; // 錯(cuò)誤: 限定作用域的枚舉類型不會(huì)進(jìn)行隱式轉(zhuǎn)換
2. 指定enum的大小
盡管每個(gè)enum都定義了唯一的類型,但是實(shí)際上enum是由某種整數(shù)類型表示的蜀肘。在C++11新標(biāo)準(zhǔn)中绊汹,我們可以在enum的名字后加上冒號以及我們想在該enum使用的類型:
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4394967295UL,
long_longTyp = 18446744073709551615ULLL
};
3. 形參匹配與枚舉類型
// 不限定作用域的枚舉類型,潛在類型因機(jī)器而異
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;
}
類成員指針
成員指針是指可以指向類的非靜態(tài)成員的指針扮宠。一般情況下西乖,指針指向一個(gè)對象,但是成員指針指示的是類的成員坛增,而非類的對象获雕。類的靜態(tài)成員不屬于任何對象,因此無須特殊的指向靜態(tài)成員的指針收捣,指向靜態(tài)成員的指針和普通指針沒有任何區(qū)別届案。
成員指針的類型囊括了類的類型以及成員的類型。當(dāng)初始化一個(gè)這樣的指針時(shí)坏晦,我們令其指向類的某個(gè)成員萝玷,但是不指定該成員所屬的對象;直到使用成員指針時(shí)昆婿,才給提供成員所屬的對象球碉。
為了解釋成員指針的原理,我們使用該Screen類:
class Screen {
public:
typedef std::string::size_type pos;
char get_cursor() const { return contents[cursor]; }
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
};
1. 數(shù)據(jù)成員指針
與普通指針不同的是仓蛆,成員指針還必須包含成員所屬的類睁冬。因此,我們必須在*
之前添加classname::
以表示當(dāng)前定義的指針可以指向classname的成員看疙,例如:
// pdata可以指向一個(gè)常量(非常量)Screen對象的string成員
// 將pdata聲明為"一個(gè)指向Screen類的const string成員的指針"
const string Screen::*pdata;
當(dāng)我們初始化一個(gè)成員指針(或者向它賦值)時(shí)豆拨,需要指定它所指的成員。例如我們可以令pdata指向某個(gè)非特定Screen對象的contents成員:
pdata = &Screen::contents;
在C++11新標(biāo)準(zhǔn)中聲明成員指針最簡單的方法是使用auto或者decltype:
auto pdata = &Screen::contents;
1.1 使用數(shù)據(jù)成員指針
當(dāng)我們初始化一個(gè)成員指針或者為成員指針賦值時(shí)能庆,該指針并沒有指向任何數(shù)據(jù)施禾。成員指針指定了成員而非該成員所屬的對象,只有當(dāng)解引用成員指針時(shí)我們才提供對象的信息搁胆。
我們可以通過.*
和->*
兩個(gè)運(yùn)算符解引用指針以獲得該對象的成員:
Screen myScreen, *pScreen = &myScreen;
// .*解引用pdata以獲得myScreen對象的contents成員
auto s = myScreen.*pdata;
// ->*解引用pdata以獲得pScreen所指對象的contents成員
s = pScreen->*pdata;
1.2 返回?cái)?shù)據(jù)成員指針的函數(shù)
Screen的contents成員是私有的弥搞,因此之前對于pdata的使用必須位于Screen類的成員或友元內(nèi)部邮绿,否則程序?qū)l(fā)生錯(cuò)誤。如果像Screen這樣的類希望我們可以訪問它的contents成員攀例,最好定義一個(gè)函數(shù):
class Screen {
public:
// data是一個(gè)靜態(tài)成員, 返回一個(gè)成員指針
static const std::string Screen::*data()
{ return &Screen::contents; }
}
// 我們調(diào)用data函數(shù)時(shí), 將得到一個(gè)成員指針
// data()返回一個(gè)指向Screen類的contents成員的指針
const string Screen::*pdata = Screen::data();
// pdata指向Screen類的成員而非實(shí)際數(shù)據(jù), 要想使用pdata必須把它綁定到Screen類型的對象上
auto s = myScreen.*pdata;
2. 成員函數(shù)指針
我們也可以定義指向類的成員函數(shù)的指針:
// pmf是一個(gè)指針, 它可以指向Screenn的某個(gè)常量成員函數(shù)
// 前提是該函數(shù)不接受任何實(shí)參, 并且返回一個(gè)char
auto pmf = &Screen::get_cursor;
- 指向成員函數(shù)的指針也需要指定目標(biāo)函數(shù)的返回類型和形參列表
- 如果成員函數(shù)是const成員或引用成員船逮,我們必須將const限定符或者引用限定符包含進(jìn)來
- 如果成員存在重載的問題,那么我們必須顯式地聲明函數(shù)類型以明確指出來我們想要使用的是哪個(gè)函數(shù)
// 例如我們可以聲明一個(gè)指針, 令其指向含有兩個(gè)形參的get:
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get; // 必須加取地址符&, 在成員函數(shù)和指針之間不存在自動(dòng)轉(zhuǎn)換規(guī)則
2.1 使用成員函數(shù)指針
Screen myScreen, *pScreen = &myScreen;
// 通過myScreen所指的對象調(diào)用pmf所指的函數(shù)
char c1 = (pScreen->*pmf)();
// 通過myScreen對象將實(shí)參0, 0 傳給含有兩個(gè)形參的get函數(shù)
char c2 = (myScreen.*pmf2)(0, 0);
2.2 使用成員指針的類型別名
使用類型別名或者typedef可以讓成員指針更容易理解粤铭,例如下面的類型別名將Action定義為兩個(gè)參數(shù)get函數(shù)的同義詞:
// Action是一種可以指向Screen成員函數(shù)的指針, 它接收兩個(gè)pos實(shí)參, 返回一個(gè)char
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
通過使用Action挖胃,我們可以簡化指向get的指針定義:
Action get = &Screen::get; // get指向Screen的get成員
我們可以將指向成員函數(shù)的指針作為某個(gè)函數(shù)的返回類型或者形參類型:
// action接受一個(gè)Screen的引用和一個(gè)指向Screen成員函數(shù)的指針
Screen& action(Screen&, Action = &Screen::get);
Screen myScreen;
// 等價(jià)調(diào)用
action(myScreen); // 使用默認(rèn)實(shí)參
action(myScreen, get); // 使用我們之前定義的變量get
action(myScreen, &Screen::get); // 顯式地傳入地址
2.3 成員指針函數(shù)表
對于普通函數(shù)指針和指向成員函數(shù)的指針來說,一種常見的用法是將其存入一個(gè)函數(shù)表當(dāng)中梆惯。如果一個(gè)類含有幾個(gè)相同類型的成員酱鸭,則這樣一張表可以幫助我們從這些成員中選擇一個(gè)。假定Screen類中含有幾個(gè)成員加袋,每個(gè)函數(shù)負(fù)責(zé)將光標(biāo)向指定的方向移動(dòng):
class Screen {
public:
// 其他接口和實(shí)現(xiàn)成員與之前一致
// 這幾個(gè)函數(shù)共同點(diǎn): 不接受任何參數(shù), 并且返回值是發(fā)生光標(biāo)移動(dòng)的Screen的引用
Screen& home(); // 光標(biāo)移動(dòng)函數(shù)
Screen& froward();
Screen& back();
Screen& up();
Screen& down();
}
我們希望定義一個(gè)move函數(shù)凛辣,使其可以調(diào)用上面任意一個(gè)函數(shù)并執(zhí)行對應(yīng)的操作。為了支持這個(gè)新函數(shù)职烧,我們將在Screen中添加一個(gè)靜態(tài)成員,該成員是指向光標(biāo)移動(dòng)函數(shù)的指針的數(shù)組:
class Screen {
public:
// Action是一個(gè)指針, 可以用任意一個(gè)光標(biāo)移動(dòng)函數(shù)對其賦值
using Action = Screen& (Screen::*)();
// 指定具體要移動(dòng)的放共享
enum Directions { HOME, FORWARD, BACK, UP, DOWN };
Screen& move(Directions);
private:
static Action Menu[]; // 函數(shù)表
};
Screen& Screen::move(Directions cm)
{
// 運(yùn)行this對象中索引值為cm的元素
return (this->*Menu[cm])(); // Menu[cm]指向一個(gè)成員函數(shù)
}
Screen::Action Screen::Menu[] = {
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down,
};
當(dāng)我們調(diào)用move函數(shù)式防泵,給它傳入一個(gè)表示光標(biāo)移動(dòng)方向的枚舉成員:
Screen myScreen;
myScreen.move(Screen::HOME); // 調(diào)用myScreen.home
myScreen.move(Screen::DOWN); // 調(diào)用myScreen.down
3. 將成員函數(shù)用作可調(diào)用對象
要想通過有一個(gè)指向成員函數(shù)的指針進(jìn)行函數(shù)調(diào)用蚀之,必須首先利用.*
或者->*
運(yùn)算符將該指針綁定到特定的對象上。因此與普通的函數(shù)指針不同捷泞,成員指針不是一個(gè)可調(diào)用對象足删,這樣的指針不支持函數(shù)調(diào)用運(yùn)算符。
因?yàn)槌蓡T指針不是可調(diào)用對象锁右,因此我們不能直接將一個(gè)指向成員函數(shù)的指針傳遞給算法失受。比如我們想在一個(gè)string的vector中找到第一個(gè)空string,顯然不能這么寫:
auto fp = &string::empty; // fp指向string的empty函數(shù)
// 錯(cuò)誤, 必須使用.*或者->*調(diào)用成員指針
find_if(svec.begin(), svec.end(), fp);
// 在find_if內(nèi)部試圖執(zhí)行如下代碼, 但是要想通過成員指針調(diào)用函數(shù), 必須使用該->*運(yùn)算符, 所以失敗
if (fp(*it))
3.1 使用fuction生成一個(gè)可調(diào)用對象
vector<string> svec;
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
find_if(pvec.begin(), pvec.end(), fp);
3.2 使用mem_fn生成一個(gè)可調(diào)用對象
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
// mem_fn生成的對象可以通過對象調(diào)用, 也可以通過指針調(diào)用
auto f = mem_fn(&string::empty); // f接收一個(gè)string或者一個(gè)string*
f(*svec.begin()); // 正確: 傳入一個(gè)string對象, f使用.*調(diào)用empty
f(&svec[0]); // 正確: 傳入一個(gè)string指針, f使用->*調(diào)用empty
3.3 使用bind生成一個(gè)可調(diào)用對象
auto it = find_if(svec.begin(), svec.end(), bing(&string::empty, _1));
// bind生成的可調(diào)用對象第一個(gè)實(shí)參既可以是string的指針, 也可以是string的引用
auto f = bind(&string::empty, _1);
f(*svec.begin());
f(&svec[0]);
嵌套類
一個(gè)類可以定義在另一個(gè)類的內(nèi)部咏瑟,前者被定義為嵌套類拂到。嵌套類的名字在外層類作用域中是可見的,在外層作用域之外不可見码泞。
1. 聲明一個(gè)嵌套類
我們?yōu)門extQuery類定義了一個(gè)名為QueryResult的配套類兄旬。QueryResult類的主要作用是表示TextQuery對象上query操作的結(jié)果,顯然將QueryResult用作其他目的沒有任何意義余寥。
class TextQuery {
public:
class QueryResult; // 嵌套類稍后定義
}
2. 在外層類之外定義一個(gè)嵌套類
// QueryResult是TextQuery的成員
class TextQuery::QueryResult {
// 位于類的作用域內(nèi), 因此我們不必對QueryResult形參進(jìn)行限定
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
// 嵌套類可以直接使用外層類的成員, 無須對該名字進(jìn)行限定
QueryResult(std::string, std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>);
};
3. 定義嵌套類的成員
TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p,
std::shared_ptr<std::vector<std::string>> f) :
sought(s), lines(p), file(f) { }
union: 一種節(jié)省空間的類
聯(lián)合union是一種特殊的類领铐,一個(gè)union可以有多個(gè)數(shù)據(jù)成員,但是在任意時(shí)刻只有一個(gè)數(shù)據(jù)成員有值宋舷。當(dāng)我們給union的某個(gè)成員賦值之后绪撵,該union的其他成員就變成未定義的狀態(tài)了。
1. 定義union
union提供了一種有效的途徑使得我們可以方便地表示一組類型不同的互斥值祝蝠。舉個(gè)例子音诈,假設(shè)我們需要處理一些不同種類的數(shù)字?jǐn)?shù)據(jù)和字符數(shù)據(jù)汹来,則可以定義一個(gè)union來保存這些值:
// Token類型的對象只有一個(gè)成員, 該成員的類型可能是下列類型中的任意一個(gè)
union Token {
// 默認(rèn)情況下成員是公有的
char cval;
int ival;
double dval;
};
2. 使用union類型
和其他內(nèi)置類型一樣,默認(rèn)情況下union是未初始化的改艇,我們可以像顯式地初始化聚合類一樣用一對花括號內(nèi)的初始值顯式地初始化一個(gè)union:
Token first_token = {'a'}; // 初始化cval成員, 如果提供初始值則用于初始化第一個(gè)成員
Token last_token; // 未初始化的Token對象
Token *pt = new Token; // 指向一個(gè)未初始化的Token對象的指針
3. 匿名union
union {
char cval;
int ival;
double dval;
}; // 未命名對象, 我們可以直接訪問它的成員
cval = 'a'; // 為匿名union賦一個(gè)新值
ival = 42; // 該對象當(dāng)前保存的值是42
4. 其他
由于現(xiàn)在電腦普遍內(nèi)存較大收班,使用union的地方比較少,故這一塊后續(xù)碰上了再學(xué)習(xí)
局部類
類可以定義在某個(gè)函數(shù)的內(nèi)部谒兄,我們稱這樣的類為局部類local class
摔桦。
- 局部類的成員必須完整定義在類的內(nèi)部,所以成員函數(shù)的復(fù)雜性不能太高承疲,一般只有幾行代碼
- 在局部類中不允許聲明靜態(tài)數(shù)據(jù)成員
1. 局部類不能使用函數(shù)作用域中的變量
局部類只能訪問外層作用于定義的類型名邻耕、靜態(tài)變量以及枚舉成員。
int a, val;
void foo(int val)
{
static int si;
enum Loc { a = 1024, b };
// Bar是foo的局部類
struct Bar {
Loc locVal;
int barVal;
void fooBar(Loc l = a)
{
barVal = val; // 錯(cuò)誤, val是foo的局部變量
barVal = ::val; // 正確: 使用一個(gè)全局變量
barVal = si; // 正確: 使用一個(gè)靜態(tài)局部對象
locVal = b; // 正確: 使用一個(gè)美劇成員
}
};
// ...
}
2. 常規(guī)的訪問保護(hù)規(guī)則對局部類同樣適用
外層函數(shù)對局部類的私有成員沒有任何訪問特權(quán)燕鸽。當(dāng)然兄世,局部類可以將外層函數(shù)聲明為友元;或者更常見的是局部類將其成員聲明成公有的啊研。在程序中有權(quán)訪問局部類的代碼非常有限御滩,局部類已經(jīng)封裝在函數(shù)作用域中,通過信息隱藏進(jìn)一步封裝就顯得沒有必要党远。
固有的不可移植的特性
為了支持低層編程削解,C++定義了一些固有的不可移植的特性。所謂不可移植的特性是指因機(jī)器而異的特性沟娱,當(dāng)我們將不可移植特性的程序從一臺(tái)機(jī)器轉(zhuǎn)移到另一臺(tái)機(jī)器上時(shí)氛驮,通常需要重新編寫該程序。
1. 位域
類可以將其(非靜態(tài))數(shù)據(jù)成員定義成位域bit-field
济似,在一個(gè)位域中含有一定數(shù)量的二進(jìn)制位矫废。當(dāng)一個(gè)程序需要向其他程序或者硬件設(shè)備傳遞二進(jìn)制數(shù)據(jù)時(shí),通常會(huì)用到位域砰蠢。
typedef unsigned int Bit;
class File {
Bit mode: 2; // mode占兩位
Bit modified: 1; // modified占1位
Bit prot_owner: 3; // 占3位
Bit prot_group: 3; // 占3位
Bit prot_world: 3; // 占3位
public:
// 文件以八進(jìn)制的形式表示
enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };
File &opne(modes);
void close();
void write();
bool isRead() const;
void setWrite();
};
- 如果可能的話蓖扑,在類的內(nèi)部連續(xù)定義的位域液壓鎖在同一整數(shù)的相鄰位,這意味著前面五個(gè)位域可能會(huì)存儲(chǔ)在一個(gè)unsigned int中娩脾,這些二進(jìn)制位能否壓縮到一個(gè)整數(shù)中以及如何壓縮是與機(jī)器相關(guān)的
- 取地址運(yùn)算符
&
不能作用域位域赵誓,因此任何指針都無法指向類的位域- 最好將位域設(shè)為無符號類型,存儲(chǔ)在帶符號類型中的位域的行為將因具體實(shí)現(xiàn)而定
2. volatile限定符
直接處理硬件的程序通常包含這樣的數(shù)據(jù)元素柿赊,例如程序可能包含一個(gè)由系統(tǒng)時(shí)鐘定時(shí)更新的變量俩功。當(dāng)對象的值可能在程序的控制或檢測之外被改變時(shí),應(yīng)該將該對象聲明為volatile碰声,告訴編譯器不應(yīng)對這樣的對象進(jìn)行優(yōu)化诡蜓。
volatile int display_register; // 該int值可能發(fā)生改變
3. 鏈接指示: extern "C"
C++程序有時(shí)候需要調(diào)用其他語言編寫的函數(shù)(比如C語言)。其他語言中的函數(shù)名字也必須在C++中進(jìn)行聲明胰挑,并且該聲明必須指定返回類型和形參類別蔓罚。
3.1 聲明一個(gè)非C++函數(shù)
// cstring頭文件中C函數(shù)的聲明
// 單語句鏈接指示
extern "C" size_t strlen(const char *);
// 復(fù)合語句鏈接指示
extern "C" {
int strcmp(const char*, const char*);
char *strcat(char*, const char*);
}
3.2 鏈接指示與頭文件
// 復(fù)合語句鏈接指示
extern "C" {
#include <string.h> // 操作C風(fēng)格字符串的C函數(shù)
}
上面的寫法意味著頭文件中所有普通函數(shù)聲明都被認(rèn)為是由鏈接指示的語言編寫的椿肩。
3.3 指向extern "C"函數(shù)的指針
// pf指向一個(gè)C函數(shù), 該函數(shù)接受一個(gè)int返回void
extern "C" void (*pf)(int);
指向C函數(shù)的指針和指向C++函數(shù)的指針是不一樣的類型
3.4 鏈接指示對整個(gè)聲明都有效
// f1是一個(gè)C函數(shù), 它的形參是一個(gè)指向C函數(shù)的指針
extern "C" void f1(void(*)(int));
3.5 導(dǎo)出C++函數(shù)到其他語言
通過鏈接指針對函數(shù)進(jìn)行定義,我們可以令一個(gè)C++函數(shù)在其他語言編寫的程序中可用:
// calc函數(shù)可以被C程序調(diào)用
extern "C" double calc(double dparm) { /*...*/) }
3.6 重載函數(shù)與鏈接指示
C語言不支持函數(shù)重載豺谈,因?yàn)橐簿筒浑y理解一個(gè)C鏈接指示只能用于說明一組重載函數(shù)中的某一個(gè)了:
// 錯(cuò)誤: 兩個(gè)extern "C"函數(shù)的名字相同
extern "C" void print(const char*);
extern "C" void print(int)