第十九章 特殊工具與技術
19.1 控制內存分配
19.1.1 重載new和delete
string *sp = new string("a value");
string *arr = new string[10];
-
new表達式調用了一個名爲operator new(或operator new[])的標準庫函數。該函數分配一塊足夠大的、原始的、未命名的內存空間以便存儲特定類型的對象
-
編譯器運行相應的構造函數構造這些對象,併爲其傳入初始值。
-
對象被分配了空間並構造完成,返回一個指向該對象的指針。
delete sp;
delete [] arr;
-
對sp所指的對象或者arr所指的數組中的元素執行對應的析構函數
-
第二步,編譯器調用operator delete(或operator delete[])的標準庫函數釋放內存空間。
operator new接口和operator delete接口
標準庫定義了operator new函數和operator delete函數的8個重載版本。其中前四個版本可能拋出bad_alloc異常,後4個不會。
void * operator new(size_t);
void * operator new[] (size_t);
void * operator delete(void *) noexcept;
void * operator delete[](void *) noexcept;
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;
與析構函數類似,operator delete也不允許拋出異常。當重載這些運算符時,必須使用noexcept異常說明符
應用程序可以自定義上面函數版本中的任意一個,前提是自定義的版本必須位於全局作用域或者類作用域中。當我們將上述運算符函數定義成類的成員時,他們是隱式靜態的。我們無須聲明static,當然這麼做也不會引發錯誤。
因爲operator new用在對象構造之前而operator delete用在對象銷燬之後,所以這兩個成員必須是靜態的,而且他們不能操作類的任何數據成員
自定的operator new函數,可以爲它提供額外的形參。此時,用到這些自定義函數的new表達式必須使用new的定位形式將實參傳遞給新增的形參。
儘管在一般情況下我們可以自定義具有任何形參的operator new,但是下面這個函數卻無論如何也不能被用戶重載:
void * operator new (size_t,void *);
19.1.2 定位new表達式
格式如下:
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {braced initializer list}
當僅通過一個地址值調用時,定位new使用operator new(size_t,void *)"分配"它的內存。這是一個我們無法自定義的operator new版本。該函數不分配任何內存,它只是簡單地返回指針實參;然後由new表達式負責在指定的地址初始化對象以完成整個工作。事實上,定位new允許我們再一個特定的,預先分配的內存地址上構造對象
顯式的析構函數調用
string *sp = new string("a value");
sp->~string();
注意:調用析構函數會銷燬對象,但是不會釋放內存
19.2 運行時類型識別
運行時類型識別(RTTI)由兩個運算符實現:
- typeid運算符,用於返回表達式的類型
- dynamic_cast 運算符,用於將基類的指針或引用,安全地轉換成派生類的指針或引用
19.2.1 dynamic_cast運算符
形式如下:
dynamic_cast<type*> (e)
dynamic_cast<type &>(e)
dynamic_cast<type&&>(e)
在上面的所有形式中,e的類型必須符合以下三個條件中的任意一個:e的類型是目標type的公有派生類、e的類型是目標type的公有基類或者e的類型就是目標type的類型。
如果一條dynamic_cast語句的轉換目標是指針類型並且失敗了,則結果爲0.如果轉換目標是引用類型並且失敗了,則dynamic_cast運算符將拋出一個bad_cast異常
指針類型的dynamic_cast
例子如下:
if(Derived *dp = dynamic_cast<Derived*>(bp)){
//使用dp指向的Derived對象
}else{
//使用bp指向的對象
}
上面需要注意一下,我們再條件部分定義了dp,這樣做的好處可以在一個操作中同時完成類型轉換和條件檢查兩項任務。而且,指針dp在if語句外部是不可訪問的。
引用類型的dynamic_cast
例如:
void f(const Base &b){
tye{
const Derived &d = dynamic_cast<const Derived&>(b);
//使用b引用的Derived對象
}catch(bad_cast){
//處理類型轉換失敗的情況
}
}
19.2.2 typeid運算符
形式如下:
typeid(e)
其中e可以是任意表達式或類型名字。結果是一個常量對象的引用,該對象的類型是標準庫類型type_info或者type_info的公有派生類型。
當typeid作用於數組或函數時,並不會向指針的標準類型轉換。因此,對於數組a來說,typeid(a),得到的結果是數組類型而非指針類型
當運算對象不屬於類類型或者是一個不包含任何虛函數的類時,typeid運算符指示的是運算對象的靜態類型。
而當運算對象定義了至少一個虛函數的類的左值時,typeid的結果知道直到時纔會求得
使用typeid運算符
例如:
Derived *dp = new Derived;
Base *bp = dp;//兩個指針都指向Derived對象
if(typeid(*bp)==typeid(*dp)){
//bp和dp指向同一類型的對象
}
if(typeid(*bp)==typeid(Derived)){
//bp實際指向Derived對象
}
注意,typeid應該作用於對象,因此使用*bp而不是bp
if(typeid(bp)==typeid(Derived)){
//此處代碼永遠不會執行
}
這個條件比較的是Base*和Derived。儘管指針所指的對象類型是一個含有虛函數的類,但是指針本身並不是一個類類型的對象。類型Base*將在編譯時求值,顯然它與Derived不同,因此不論bp所指的對象到底是什麼類型,上面條件都不會滿足。
19.2.4 type_info類
type_info類的精確定義隨着編譯器的不同而略有差異。不過c++標準規定type_info類必須定義在typeinfo頭文件中,並且至少提供了表19.1所列的操作
type_info類沒有默認構造函數,而且他的拷貝和移動構造函數以及賦值運算符都被定義成刪除的。因此無法定義或拷貝type_info類型的對象,也不能爲type_info類型的對象賦值。創建type_info對象的唯一途徑是使用typeid運算符
type_info 類的name成員函數返回一個c風格字符串,表示對象的類型名字。對於某種給定的類型來說,name的返回值因編譯器而異並且不一定與在程序中使用的名字一致。對於name返回值的唯一要求是,類型不同則返回的字符串必須有所區別。
19.3 枚舉類型
c++包含兩種枚舉:限定作用域和不限定作用域
限定作用域類型:關鍵字enum class(或者enum struct),隨後是枚舉類型名字以及用花括號括起來的以逗號分隔的枚舉成員,最後是分號:
enum class open_modes(input,output,append);
不限定作用域的枚舉類型:就是忽略關鍵字class或(struct)
enum color {red,yellow,green};
枚舉成員
在限定作用域的枚舉類型中,枚舉成員的名字遵循常規的作用域準則,並且在枚舉型的作用域外是不可訪問的。
與之相反,在不限定作用域的枚舉類型中,枚舉成員的作用域與枚舉類型本身的作用域相同
默認情況下,枚舉值從0開始,依次加1,也可以如下:
enum class intTypes{
charType = 8,shorType = 16, intType = 16,
longType = 32,long_longType = 64
};
枚舉成員是const的。
和類一樣,枚舉也定義新的類型
只要enum有名字,我們就能定義並初始化該類型的成員。要想初始化enum對象或者位enum對象賦值,必須使用該類型的一個枚舉成員或者該類型的另一個對象:
open_modes om = 2;//錯誤;2不屬於open_modes
om = open_modes::input;//正確:input是open_modes的一個枚舉成員
一個不限定作用域的枚舉類型的對象或枚舉成員自動地轉換成整形。因此,我們可以在任何需要整形值得地方使用他們:
int i = color::red;//正確:不限定作用域的枚舉類型成員隱式地轉換成int
指定enum大小
儘管每個enum都定義了唯一的類型,但實際上enum是由某種整數類型表示的。在c++11新標準中,我們可以在enum的名字後面加上冒號以及我們想在該enum中使用的類型:
enum intValues:unsigned long long{
charTyp = 255,shortTyp = 65535,
intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 184467440737095516115ULL
};
如果沒有指定enum的潛在類型,則默認情況下限定作用域的enum成員類型是int。
對於不限定作用域的枚舉類型來說,其枚舉成員不存在默認類型,我們只知道成員的潛在類型足夠大,肯定能容納枚舉值。
如果我們指定了枚舉成員的潛在類型,則一旦某個枚舉成員的值超出了該類型所能容納的範圍,將引發程序錯誤
枚舉類型的前置聲明
可以提前聲明enum。enum的前置聲明必須指定其成員的大小:
enum intValues:unsigned long long;//不限定作用域,必須指定成員類型
enum class open_modes;//限定作用域的枚舉類型可以使用默認的成員類型int
//錯誤:所有的聲明和定義必須對enum是限定作用域還是不限定作用域保持一致
enum class intValues;
//錯誤:intValues已經被聲明成限定作用域的enum
enum intValues;
//錯誤:intValues已經被聲明稱int了
enum intValues:long;
形參匹配與枚舉類型
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;
}
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL);//調用newf(int)
newf(uc);//調用newf(unsigned char)
19.4 類成員指針
成員指針:是指可以指向類的非靜態成員的指針。
指向靜態成員的指針,與普通指針沒什麼區別。
成員指針的類型囊括了類的類型和成員的類型。當初始化這樣一個指針時,我們令其指向類的某個成員,但是不指定該成員所屬的對象;知道使用成員指針時,才提供成員所屬的對象
19.4.1 數據成員指針
const string Screen::*pdata;
在*之前添加所屬類。上例表示:pdata可以指向一個Screen對象的const string成員
pdata = &Screen::contents;
上面對pdata進行賦值,令其指向Screen對象的contents成員
使用數據成員指針
Screen myScreen,*pScreen = &myScreen;
//.*解引用pdata以獲得myScreen對象的contents成員
auto s = myScreen.*pdata;
//->*解引用pdata以獲得pScreen所指對象的contents成員
s = pScreen->*pdata
19.4.2 成員函數指針
//pmf是一個指針,它可以指向Screen的某個常量成員函數
//前提是該函數不接受任何實參,並且返回一個char
auto pmf = &Screen::get_cursor;
char (Screen::*pmf2)(Screen::pos,Screen::pos) const;
pmf2 = &Screen::get;
和普通函數不同的是,在成員函數和指向該成員函數的指針之間不存在自動轉換規則:
//pmf是指向一個Screen成員,該成員不接受任何實參且返回類型爲char
pmf = &Screen::get;
pmf = Screen::get;//錯誤:在成員函數和指針之間不存在自動轉換規則
使用成員函數指針
Screen myScreen,*pScreen = &myScreen;
//通過pScreen所指的對象調用pmf所指的函數
char c1 = (pScreen->*pmf)();
//通過myScreen對象將實參0,0傳遞給鏈各個形參的get函數
char c2 = (myScreen.*pmf2)(0,0);
注意:因爲函數調用運算符的優先級高,所以在聲明指向成員函數的指針並使用這樣的指針進行函數調用時,括號必不可少
使用成員指針的類型別名
using Action = char (Screen::*)(Screen::pos,Screen::pos) const;
Action get = &Screen::get;
使用function生成可調用對象
從指向成員函數的指針獲取可調用對象的一種方法是使用標準模板function
function<bool(const string &)> fcn = &string::empty;
find_if(svec.begin(),svec.end(),fcn);
上述的function被定義爲:接受string參數並返回bool值。
而實際上,string的empty函數並不需要參數。但是在調用對象的成員函數時,隱含有一個this實參。因此傳遞給fcn的參數,並當做了this。所以上述的例子才能成立.
所以,可以總結如下:
當我們定義一個function對象時,必須指定該對象所能表示的函數類型,即可調用對象的形式。如果可調用對象是一個成員函數,則第一個形參必須是表示該成員是在哪個對象上面執行的。同時,我們提供給function的形式中還必須指明對象是否是以指針或引用的形式傳入的。
例如:
vector<string *> pvec;
function<bool (const string *)> fp = &string::empty;
find_if(pvec.begin(),pvec.end(),fp);
使用mem_fn
mem_fn來讓編譯器負責推斷成員的類型,並且可以從一個成員指針生成一個可調用對象。
find_if(svec.begin(),svec.end(),mem_fn(&string::empty));
mem_fn生成的可調用對象可以通過對象調用,也可以通過指針調用:
auto f = mem_fn(&string::empty);
f(*svec.begin());
f(&svec[0]);
使用bind生成一個可調用對象
auto it = find_if(svec.begin(),svec.end(),
bind(&string::empty,_1));
19.5 嵌套類
嵌套類在其外層類中定義了一個類型成員。和其他成員類似,該類型的訪問權限由外層類決定。位於外層類public部分的嵌套類實際上定義了一種可以隨處訪問的類型;位於外層類protected部分的嵌套類定義的類型只能被外層類及其友元和派生類訪問;位於外層類private部分的嵌套類定義的類型只能被外層類的成員和友元訪問。
聲明一個嵌套類
class TextQuery{
public:
class QueryResult;
};
//下面的類負責定義QueryResult
class TextQuery::QueryResult{
friend std::ostream& print(std::ostream&,const QueryResult&);
public:
QueryResult(std::string,
std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>);
};
//下面的代碼實現QueryResult的成員
TextQuery::QueryResult::QueryResult(std::string,
std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>):
sought(s),lines(p),file(f){}
嵌套類的作用域查找和嵌套命名空間的作用域查找類似
19.6 union:一種節省空間的類
union(聯合):可以有多個數據成員,但是在任意時刻只有一個數據成員可以有值。因此,分配給一個union對象的存儲空間至少要能容納它的最大的數據成員。
union具有下面的特殊性:
-
不能含有引用類型的成員
-
在c++11新標準中,含有構造函數或析構函數的類類型也可以作爲union的成員類型。
-
union可以指定其成員爲public,protected,private。默認情況下爲public
-
union可以定義包括構造函數和析構函數在內的成員函數。但是由於union既不能繼承自其它類,也不能作爲基類使用,所以union中不能含有虛函數
定義unio
union Token{
char cval;
int ival;
double dval;
};
使用union
Token first_token = {'a'};//初始化cval成員
Token last_token;//未初始化Token對象
Token *pt = new Token;//指向一個爲初始化的Token對象的指針
last_token.cval = 'z';
pt->ival = 42;
匿名union
匿名union是一個未命名的union,並且在右花括號和分號之間沒有任何聲明。一旦定義了匿名union,編譯器就自動爲該union創建一個未命名的對象
union{
char cval;
int ival;
double dval;
};
cval = 'c';
ival = 42;
注意:匿名union不能包含受保護的成員或私有成員,也不能定義成員函數
含有類類型成員的union
當union包含的是內置類型的成員時,編譯器將按照成員的次序合成默認構造函數或拷貝控制成員。
但是如果union含有類類型成員,並且該類型自定義了默認構造函數或拷貝控制成員,則編譯器將爲union合成對應的版本並將其聲明爲刪除的。
對於union來說,要想構造或銷燬類類型的成員必須執行非常複雜的操作,因此我們通常把含有類類型成員的union內嵌在另外一個類當中。這個類可以管理並控制與union的類類型成員有關的狀態轉換。
19.7 局部類
定義在某個函數內部的類,稱爲局部類。
局部類的所有成員都必須完整定義在類的內部。因此,局部類的作用於嵌套類相比相差很遠
同樣,局部類中也不允許聲明靜態數據成員。
局部類不能使用函數作用域中的變量
局部類只能訪問外層作用域定義的類型名、靜態變量以及枚舉成員。如果局部類定義在某個函數內部,則該函數的普通局部變量不能被該局部類使用:
int a,val;
void foo(int val){
static int si;
enum Loc{a = 1024,b};
struct Bar{
Loc locVal;//正確:使用局部類型名
int barVal;
void fooBar(Loc l = a){//正確默認實參時Loc::a
barVal = val;//錯誤:val是foo的局部變量
barVal = ::val;//正確:使用一個全局對象
barVal = si;//正確:使用一個靜態局部對象
locVal = b;//正確:使用一個枚舉成員
}
};
//...
}
19.8 固有的不可移植的特性
19.8.1 位域
類可以將其數據成員定義爲位域。
注意:位域在內存中的佈局是與機器相關的
位域的類型必須是整形或枚舉類型。因爲帶符號位域的行爲是由具體實現確定的。所以在通常情況下我們使用無符號類型保存一個位域。形式如下:
typedef unsigned int Bit;
class File{
Bit mode:2;//mode佔2位
Bit modified:1;//modified佔1位
Bit prot_owner:3;//prot_owner佔3位
Bit prot_gourp:3;//prot_group佔3位
Bit prot_world:3;//prot_world佔3位
public:
enum modes{READ = 01,WRITE = 02,EXECUTE = 03};
File &open(modes);
void close();
void write();
bool isRead() const;
void setWrite();
};
mode位域佔2個二進制位,modified佔1個,其他成員各佔3個。如果可能的話,在類的內部連續定義的位域壓縮成一個整數的相鄰位,從而提供存儲壓縮。
例如,這五個位域可能會存儲在一個unsigned int中。至於二進制位是否能壓縮到一個整數中以及如何壓縮是與機器相關的。
取地址運算符不能作用於位域,因此任何指針都無法指向類的位域。
訪問位域的方式與訪問類的其他數據成員的方式類似
19.8.2 volatile限定符
volatile的確切含義與機器有關,只能通過閱讀編譯器文檔來理解。要想讓使用了volatile的程序在移植到新機器或新編譯器後仍然有效,通常需要對該程序進行某些改變。
直接處理硬件的程序常常包含這樣的數據元素:他們的值由程序直接控制之外的過程控制。
關鍵字volatile告訴編譯器不應對這樣的對象進行優化
成員函數也可以定義爲volatile。只有volatile的成員函數才能被volatile的對象調用。
注意:合成的拷貝對volatile對象無效
如果一個類希望拷貝、移動或賦值它的volatile對象,則該類必須自定義拷貝或移動操作。
class Foo{
public:
//從一個volatile對象進行拷貝
Foo(const volatile Foo&);
//將一個volatile對象賦值給一個非volatile對象
Foo& operator=(volatile const Foo&);
//將一個volatile對象賦值給一個volatile對象
Foo& operator=(volatile const Foo&) volatile;
};
19.8.3 鏈接指示:extern “C”
聲明一個非c++的函數
extern "C" size_t strlen(const char *);
extern "C" {
int strcmp(const char *,const char *);
char *strcat(char *,const char *);
}
指向extern “C”函數的指針
//pf指向一個c函數,該函數接受一個int返回void
extern "C" void (*pf)(int);
注意:指向c函數的指針與指向c++函數的指針是不一樣的類型。一個指向c函數的指針不能用在執行初始化或賦值操作後指向c++函數,反之亦然。
void (*pf1)(int);//指向c++函數
extern "C" void (*pf2)(int);//指向c函數
pf1 = pf2;//錯誤:pf1和pf2的類型不同
鏈接指示對整個聲明有效
//f1是一個c函數,它的形參是一個指向c函數的指針
extern "C" void f1(void(*)(int));
導出c++函數到其他語言
//calc函數可以被c程序調用
extern "C" double calc(double dparm){}
本章完
c++終於完結,下一個系列,armlink