S19特殊工具與技術
一、控制內存分配
1、重載new
和delete
(1)new
/delete
的工作機制
調用new
時執行了:
new
表達式調用了名爲operator new
/operator new[]
的標準庫函數,分配足夠大、原始、未命名的內存空間- 編譯器運行相應的構造函數,來初始化對象並傳入初始值
- 分配空間並構造完成,返回一個指向該對象的指針
調用delete
時執行了:
- 對指針所指的對象執行相應的析構函數
- 編譯器調用名爲
operator delete
/operator delete[]
的標準庫函數,釋放內存空間
當遇到new
/delete
時,編譯器:
- (若對象是類類型)首先在類及其基類的作用域中查找是否有重載
new
/delete
- 若未找到,進一步在全局作用域查找匹配的
new
/delete
- 若未找到,調用標準庫的
new
/delete
(2)operator new
接口和operator delete
接口
重載標準庫中任意一個new
/delete
函數時,必須位於全局作用域或類作用域,若出現在類作用域則重載的new
/delete
是隱式static
的,自定義new
函數時可以爲其提供額外的參數(使用定位new
的方式)
注意:void *operator new(size_t, void *);
不允許被重新定義
注意:區分new
表達式與operator new
函數,重載的是operator new
函數的運行方式,而new
表達式永遠都是調用operator new
函數這一行爲,是不能改變的
(3)malloc
函數與free
函數
2、定位new
表達式
(1)對於operator new
分配的內存空間採用定位new
的形式構造對象
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
place_address
必須是一個指針,同時在initializers
中提供以,
分隔的初始值列表用於構造新分配的對象;當沒有initializers
僅有一個指針時定位new
調用operator new(size_t, void *)
,此時只簡單返回指針實參,然後由new
表達式負責初始化
注意:可以與allocator
的allocate
/deallocate
成員類比,但傳給定位new
的指針無須是operator new
分配返回的
(2)顯式的析構函數調用
調用析構函數會銷燬對象,但不會釋放內存,這塊內存可以重新使用
string *sp = new string("a value");
sp->~spring(); //sp所指的內存還可以使用,但是"a value"已被銷燬
二、運行時類型識別
運行時類型識別(run-time type identification, RTTI)的功能由兩個運算符實現
typeid
,返回表達式的類型dynamic_cast
,用於將基類的指針或引用安全地轉換成派生類的指針或引用
注意:當想要使用基類對象的指針或引用執行某個派生類操作且該操作不是虛函數時可以使用RTTI運算符,但是會帶來直接接管類型管理的風險
1、dynamic_cast
運算符
(1)dynamic_cast
的使用,通常type
類型應含有虛函數
dynamic_cast<type*>(e) //e必須是一個有效的指針
dynamic_cast<type&>(e) //e必須是一個左值
dynamic_cast<type&&>(e) //e不能是左值
在以上三種使用中,e
的類型也必須符合以下三個條件中的任意一個
e
的類型是目標type
的公有派生類e
的類型是目標type
的公有基類e
的類型就是目標type
的類型
不符合則轉換失敗,若轉換目標是指針類型且失敗則結果爲nullptr
,若轉換目標是引用類型且失敗則拋bad_cast
異常
(2)指針類型的dynamic_cast
if(Derived *dp = dynamic_cast<Derived*>(bp)) //在條件部分定義dp並轉換,若失敗則dp = 0完成條件判斷
{...}
else
{...}
注意:在條件部分執行dynamic_cast
可以確保類型轉換和結果檢查在同一條表達式中完成,確保程序安全
(3)引用類型的dynamic_cast
try
{
const Derived &d = dynamic_cast<const Derived&>(b); //由於引用的轉換與指針不同,因此用try方式
}
catch(bad_cast)
{...}
2、typeid
運算符
(1)typeid
運算符可以作用於任意類型的表達式,且會忽略頂層const
,返回一個常量對象的引用,這個常量對象是type_info
類或其公有派生類類型
注意:若運算對象不屬於類類型或是一個不包含任何虛函數的類時,typeid
獲得對象的靜態類型,若是定義了至少一個虛函數的類的左值時,typeid
直到運行時纔會返回結果
(2)使用typeid
運算符
通常使用typeid
比較兩條表達式的類型是否相同,或者比較一條表達式類型是否與指定類型相同
Derived *dp = new Derived;
Base *bp = dp;
if(typeid(*bp) == typeid(*dp)) {...} //注意,由於比較的是對象類型,因此要*bp而不能是bp,後者是指針
if(typeid(*bp) == typeid(Derived)) {...}
if(typeid(bp) == typeid(Derived)) {...} //if判斷永遠不成立
注意:當typeid作用於指針時(而非指針所指的對象),返回的結果是該指針的靜態類型
3、使用RTTI
//使用RTTI爲繼承關係的類實現相等運算符:
class Base {
friend bool operator==(const Base&, const Base&);
public:
//Base接口成員
protected:
virtual bool equal(const Base&) const; //虛函數
//Base的數據成員和其他成員
};
class Derived: public Base {
public:
//Derived的其他接口成員
protected:
bool equal(const Base&) const;
//Derived的數據成員和其他成員
};
bool operator==(const Base &lhs, const Base &rhs)
{
//如果lhs/rhs的typeid不同,則返回false,否則虛調用equal比較
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
bool Derived::equal(const Base &rhs) const
{
//虛調用equal後首先把傳入的基類引用rhs轉換爲對應的派生類的引用
//因爲在operator==中lhs.equal(rhs)到這裏說明進入的是Derived::equal,則rhs一定是Derived的引用
auto r = dynamic_cast<const Derived&>(rhs);
//執行轉換後才能利用派生類引用r進一步比較兩個派生類對象並返回結果,否則基類rhs不能訪問派生類成員
}
bool Base::equal(const Base &rhs) const
{
//執行比較Base對象
}
//typeid的練習19.10:
class A {public:virtual ~A() {}};
class B : public A {};
class C : public B {};
int main()
{
A *pa = new C;
cout << typeid(pa).name() << endl;
//class A * __ptr64//雖然pa指向C對象,但是pa是指針,返回靜態編譯類型
C cobj;
A &ra1 = cobj;
cout << typeid(&ra1).name() << endl;
//class A * __ptr64//雖然ra1是C對象的引用,但是&ra1取地址是指針,返回靜態編譯類型
B *px = new B;
A &ra2 = *px;
cout << typeid(ra2).name() << endl;
//class B//ra2是B對象的引用,且B有虛函數(虛析構),故運行時計算typeid,返回ra2實際應用的對象的類型
}
4、type_info
類
type_info
類定義在typeinfo頭文件中,並有如下操作
t1 == t2 //如果type_info對象t1/t2表示同一種類型,則返回true,否則返回false
t1 != t2
t.name() //返回C風格字符串,表示類型名字的可打印形式
t1.before(t2) //返回一個bool值,表示t1是否位於t2之前
注意:type_info
類沒有默認構造函數且拷貝/移動/賦值都定義爲刪除的,因此通過typeid
是獲得type_info
的唯一方式
三、枚舉類型
1、枚舉類型屬於字面值常量類型
C++包含兩種枚舉類型:限定作用域(enum class/struct ..
)和不限定作用域(enum ..
)的
2、枚舉成員
(1)限定作用域的枚舉類型中枚舉成員的名字也遵循常規作用域規則,並且在枚舉類型外不可訪問;不限定作用域的則與定義本身的有效域一樣
(2)默認第一個成員值是0,每個沒有初始值的成員默認是前一個成員值加1,枚舉成員是const
,初始化時要用常量表達式來初始化,因此枚舉成員本身也可以用在需要常量表達式的位置
enum color {red, yellow, green}; //不限定作用域的枚舉類型
enum stoplight {red, yellow, green}; //錯誤,重複定義枚舉成員
enum class peppers {red, yellow, green}; //正確,隱藏了全局color中的名字
color eyes = green; //正確,color是全局的,這一語句在有效的作用域中
peppers p = green; //錯誤,離開了peppers作用域,不可訪問其成員
color hair = color::red; //正確,顯式訪問
peppers p2 = peppers::red; //正確,顯式訪問
3、和類一樣,枚舉也定義新的類型
要初始化enum
對象或爲enum
對象賦值,必須使用該類型的一個枚舉成員或該類型的另一個對象
color clothes = 2; //錯誤,必須使用color的成員來初始化
int j = peppers::red; //錯誤,限定作用域的不會自動向int轉換
注意:不限定作用域的枚舉類型可以隱式自動轉換成int
,限定作用域的不會進行隱式轉換
4、指定enum
大小
在enum的名字後加上冒號來顯式要求我們在這個類裏使用的類型,限定作用域的枚舉默認是int
enum intValues : unsigned long long { longTyp = 4294967295UL };
5、枚舉類型的前置聲明
聲明需要顯式/隱式指出成員的數據類型
6、形參匹配和枚舉類型
初始化一個enum
對象,必須用其類型的另一個對象或是一個成員
enum Tokens { INLINE = 128, VIRTUAL = 129 };
void ff(Tokens);
void ff(int);
int main()
{
Token curTok = INLINE;
ff(128); //精確匹配ff(int)
ff(INLINE); //精確匹配ff(Tokens)
ff(curTok); //精確匹配ff(Tokens)
}
四、類成員指針
注意:成員指針是指可以指向類的非靜態成員的指針,靜態成員不屬於任何對象因此其指針和普通指針沒有區別
1、數據成員指針
(1)聲明定義數據成員指針
const string Screen::*pdata; //pdata可以指向Screen類對象的const string成員
pdata = &Screen::contents; //pdata指向某個非特定Screen對象的contents成員
(2)使用數據成員指針
成員指針指定了成員而非該成員所屬的對象,只有當解引用成員指針時我們才提供對象的信息(.*
/->*
)
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata; //pdata指向Screen的string,此時提供具體對象myScreen並通過.來獲取*pdata
s = pScreen->*pdata; //pScreen指向具體對象myScreen,->來獲取*pdata,進而得到pdata所指的string內容
(3)返回數據成員指針的函數
由於常規的訪問控制對成員指針依然有效,因此一般定義一個static
的函數返回成員指針(由於成員指針在解引用之前並不綁定任何具體對象,因此也是一個獨立於對象之外的靜態成員函數來返回成員指針)
class Screen {
public:
static const string Screen::*data() { return &Screen::contents; }
//其他成員
}
const string Screen::*pdata = Screen::data(); //調用Screen::data()返回指向Screen的contents的常量指針
auto s = myScreen.*pData; //pData是指向成員而非實際數據,使用時綁定具體Screen類對象
2、成員函數指針
(1)聲明定義成員函數指針
若成員存在重載,則必須顯式聲明函數類型避免二義性
auto pmf = &Screen::get_cursor;
char (Screen::*pmf2)(Screen::pos, Screen::pos) const; //顯式聲明具體函數版本,且(Scr..pmf2)要有括號
pmf2 = &Screen::get; //成員函數指針與所指函數不能自動轉換,必須取地址&
注意:成員函數指針也要在參數列表後指名是否是const的(這也是隱含的參數)
(2)使用成員函數指針
char c1 = (pScreen->*pmf)();
char c1 = (myScreen.*pmf2)(0, 0);
注意:由於函數調用運算符優先級高,因此必須有括號
(3)使用成員指針的類型別名
注意:通過類型別名,使得含有成員指針的代碼更容易讀寫
(4)成員指針函數表
class Screen
{
public:
using Action = Screen& (Screen::*)(); //Action是可以指向任意一個移動函數的成員函數指針類型
Screen &home();
Screen &forward();
Screen &back();
Screen &up();
Screen &down();
enum Directions { HOME, FORWARD, BACK, UP, DOWN };
Screen &move(Directions); //外界實際調用的接口
private:
static Action Menu[]; //保存成員函數指針的函數表
};
Screen::Action Screen::Menu[] = { &Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down,
};
Screen &Screen::move(Directions cm)
{
return (this->*Menu[cm])(); //Menu[cm]是某個成員函數指針,用this->*來調用它
}
Screen myScreen;
myScreen.move(Screen::HOME); //調用myScreen.home
myScreen.move(Screen::DOWN); //調用myScreen.down
3、將成員函數用作可調用對象
(1)使用function
生成一個可調用對象
由於成員函數指針需要用.*
/->*
才能將指針綁定到特定對象上,因此是不可調用對象,可以通過function
來生成一個可調用對象,此時必須顯式告訴function
執行成員函數的方法
auto fp = &string::empty;
find_if(svec.begin(), svec.end(), fp); //錯誤,fp不可調用,未綁定具體對象
//(const string &)顯式說明調用這個成員函數的是string對象,fcn接受一個const string &,然後使用.*調用empty
function<bool (const string &)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn); //正確,fcn可調用
//fp接受指向const string的指針,然後使用->*調用empty
//即如果可調用對象是一個成員函數,則第一個形參必須表示該成員在哪個具體對象上執行
function<bool (const string *)> fp = &string::empty;
(2)使用mem_fn
生成一個可調用對象
mem_fn
定義在頭文件functional中,可以根據成員指針的類型推斷可調用對象的類型而無須顯式指定,由mem_fn
生成的可調用對象可以通過對象調用,也可以通過指針調用
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
auto f = mem_fn(&string::empty); //f接受一個string或一個string*
f(*svec.begin()); //正確,傳入一個string,f使用.*調用empty
f(&svec[0]); //正確,傳入一個指向string的指針,f通過->*調用empty
(3)使用bind
生成一個可調用對象
不同於function
需要區分指針/引用,bind
是不用顯式區分傳入的是指針/引用的
auto f = bind(&string::empty, _1); //通過bind將傳入的對象綁定到empty的第一個參數
f(*svec.begin()); //正確,傳入一個string,f使用.*調用empty
f(&svec[0]); //正確,傳入一個指向string的指針,f通過->*調用empty
五、嵌套類
1、嵌套類的基本特點
定義在一個類內部的類稱爲嵌套類,嵌套類是個獨立的類,與外層類是相互獨立的,嵌套類對象中不包含外層類的成員並對外層類成員沒有特殊訪問權限,同時外層類的對象不包含嵌套類的成員並對嵌套類的成員也沒有特殊訪問權限
注意:定義在public/protected/private不同位置的嵌套類,嵌套類本身訪問權限是由外層類和訪問限定符決定的
2、聲明一個嵌套類
class TextQuery {
public:
class QueryResult; //嵌套類稍後定義
//other members
};
3、在外層類之外定義一個嵌套類
嵌套類必須在外層類內部聲明,但可以在內部或外部定義,在外部定義時需要加上外層類名和作用域運算符
class TextQuery::QueryResult {...};
注意:在嵌套類在其外層類之外完成真正的定義之前,它(嵌套類)都是一個不完全類型
4、定義嵌套類的成員
由於嵌套類定義在外層類內,嵌套類的成員定義時要有class_name::nested_class_name::member_name
這樣的前綴
TextQuery::QueryResult::QueryResult(string s..) : .. { .. } //嵌套類的構造函數在最外層定義
5、嵌套類的靜態成員定義
int TextQuery::QueryResult::static_mem = 1024;
6、嵌套類作用域中的名字查找
名字查找的一般規則在嵌套類中同樣適用,由於嵌套類本身也是嵌套作用域,因此也會查找嵌套類的外層作用域
7、嵌套類和外層類是相互獨立的
六、union:一種節省空間的類
1、基本特性
union
不能含有引用類型的成員,默認情況下union
的成員都是公有的,可以定義包括構造/析構函數在內的成員函數,但是union
不能繼承,因此不能有虛函數
2、定義union
union Token {char cval; int ival; double dval; };
3、使用union
類型
默認情況下union
是未初始化的,可以用花括號內的初始值顯式初始化一個union
,給union
對象的數據成員賦值會使其他數據成員變成未定義的狀態
Token first = {'a'};
Token *pt = new Token;
first.cval = 'z';
pt->ival = 42;
4、匿名union
匿名union
是一個未命名的union
,其內部成員在該union
定義所在的作用域內可以直接訪問,並且匿名union
不能有protected/private成員且不能定義成員函數
5、含有類類型成員的union
union
有類類型時,當將union
的值改爲類類型成員對應的值時必須運行該類型的構造函數,當改爲其他值時必須運行該類型的析構函數
6、使用類管理union
成員
由於含有類類型的union
管理非常複雜,因此一般將其內嵌在另一個類中,並定義成員函數來管理union
7、管理判別式並銷燬string
注意:需要根據原先類型來決定如何處理
8、管理需要拷貝控制的聯合成員
//union練習:
class Token
{
friend ostream &operator<<(std::ostream &os, const Token &t);
public:
Token() :tok(INT), ival(0) {} //默認初始化使用union的int,初始化爲0並設置tok爲INT
Token(const Token &t) :tok(t.tok) { copyUnion(t); } //拷貝構造,調用copyUnion成員函數完成union的拷貝
Token(Token &&t) :tok(std::move(t.tok)) { moveUnion(std::move(t)); } //移動構造
Token &operator=(const Token &);
Token &operator=(Token &&);
~Token() { if (tok == STR) sval.~string(); } //若union此時是string,則銷燬必須調用~string()
Token &operator=(const string &); //根據union內每個成員都定義一個設置成員的運算符
Token &operator=(char);
Token &operator=(int);
Token &operator=(double);
void copyUnion(const Token &);
void moveUnion(Token &&);
void free();
private:
enum {INT, CHR, DBL, STR} tok; //不限定作用域的enum類內直接使用,代表union狀態
union //匿名union,在類內可以直接使用union成員
{
char cval;
int ival;
double dval;
string sval;
};
};
Token &Token::operator=(int i) { //設定union表示int的賦值運算
if (tok == STR) sval.~string(); //若原來表示string,則必須先調用~string銷燬
ival = i;
tok = INT; //enum標記此時union表示int
return *this;
}
Token &Token::operator=(char i) {...}
Token &Token::operator=(double i) {...}
Token &Token::operator=(const string &i)
{
if (tok == STR) //檢驗當前類型,若已經是string,則直接=,會調用string的賦值運算符
sval = i;
else {
new(&sval) string(i); //若原先非string,此時需要在union中構建一個string對象
//因此利用placement new,向sval地址new一個string並由i初始化
tok = STR;
}
return *this;
}
void Token::copyUnion(const Token &t) {
switch (t.tok) {
case INT:ival = t.ival; break;
case CHR:cval = t.cval; break;
case DBL:dval = t.dval; break;
case STR:new(&sval) string(t.sval); break;//拷貝union中有類類型時,需要用placement new
}
}
void Token::moveUnion(Token &&t) {
switch (t.tok) {
case INT:ival = std::move(t.ival); break;
case CHR:cval = std::move(t.cval); break;
case DBL:dval = std::move(t.dval); break;
case STR:new(&sval) string(std::move(t.sval)); break;//移動union中有類類型時,需要用placement new
}
}
Token &Token::operator=(const Token &t)
{
if (tok == STR && t.tok != STR) //需要分別按原先/現在是否是類類型來做特別處理
sval.~string();
if (tok == STR && t.tok == STR)
sval = t.sval;
else
copyUnion(t);
tok = t.tok;
return *this;
}
Token &Token::operator=(Token &&t)
{
if (this != &t) { //移動賦值需要判斷是否是自身賦值
free(); //若自身union是string則需要調用free先析構string
moveUnion(std::move(t));
tok = std::move(t.tok);
}
return *this;
}
void Token::free() {
if (tok == STR)
sval.~string();
}
七、局部類
1、定義在某個函數內部的類稱爲局部類,局部類的使用受到嚴格限制,其所有成員都必須完整定義在類的內部,且不允許聲明靜態數據成員
2、局部類不能使用函數作用域中的變量,只能訪問外層作用域定義的類型名、靜態變量和枚舉成員
3、常規的訪問保護規則對局部類同樣適用,但一般沒有必要在局部類內再有private成員
4、局部類中的名字查找與其他類相同,一層一層向外查找
5、嵌套的局部類也是局部類,必須遵循局部類的各項規定,唯一差別在於嵌套類可以定義在外層局部類之外(依然要在函數體內)
八、固有的不可移植的特性
1、位域
(1)位域的定義
類可以將其非靜態的數據成員定義爲位域,在一個位域中有一定數量的二進制位,位域的類型必須是整型或枚舉類型,用:const expression
來說明這個成員佔幾位
unsigned int mode : 2; //mode佔據2位,這個位域是unsigned int類型的
unsigned int modified : 1;
注意:不能對位域取地址,且最好設位域爲無符號類型,因爲有符號類型的行爲具體實現可能不同
(2)使用位域
一般一位位域可以直接賦值0/1來操作,而超過1位的位域用內置的位運算符來操作
modified = 1;
mode |= READ;
mode = 4; //mode只有2位,而4是0100,故mode = 4之後mode實際變00,避免直接對位域對象賦值
2、volatile
限定符
(1)volatile
的確切含義與機器有關,volatile
告訴編譯器不要對這樣的對象進行優化,volatile
的值可能在程序控制/檢測之外被改變,而有時候優化會將某些對象放進寄存器,此時若程序用了寄存器的對象,而程序之外改變了內存中真實的對象,則會出現程序行爲的非原子性
(2)和const
相同,只能將一個volatile
對象的地址賦給指向volatile
對象的指針,當某個引用是volatile
時只能綁定一個volatile
對象
(3)合成的拷貝對volatile
對象無效
合成的拷貝/移動/賦值操作接受非volatile
的常量引用,顯然不能綁定到一個volatile
對象上,因此需要自定義拷貝/移動/賦值操作來處理volatile
對象
class Foo
{
public:
Foo(const volatile Foo&);
Foo &operator=(const volatile Foo&);
Foo &operator=(const volatile Foo&) volatile;
}
注意:使用volatile
需要參考具體的環境
3、鏈接指示:extern "C"
參考《C++ primer》p.759