重溫C++——類,以CRC破解為例
文章目錄
定義抽象資料型別
類的基本思想是資料抽象
封裝實現了類的介面和實現的分離。封裝後的類隱藏了它的實現細節,也就是說,類的使用者只能使用介面而無法訪問實現的部分。
為了實現類的介面和實現的分離,需要首先定義一個抽象資料型別(abstract data type)。在抽象資料型別中,由類的設計者負責考慮類的實現過程;使用該類的程式設計師則只需要抽象地思考型別做了什麼,而無需瞭解型別的工作細節。
根據CRC的計算過程設計BINPOLY類
迴圈冗餘校驗(Cyclic Redundancy Check, CRC)是一種根據網路資料包或電腦檔案等資料產生簡短固定位數校驗碼的一種雜湊函式,主要用來檢測或校驗資料傳輸或者儲存後可能出現的錯誤,常見於乙太網資料幀和rar型別的壓縮檔案。
計算時,將二進位制資料和給定的模多項式進行二元域上的除法運算。在進行加減法的過程中,就是進行異或操作。下面通過一個例子來說明CRC,二進位制資料為10110011,模多項式為11001:
綠色為原始資料,橙色部分為填充位元,和綠色部分一起作為被除數,紫色為除數,紅色為商,藍色為餘數。餘數即為所得校驗碼。在進行除法的過程中,首先要在末尾補0,個數為模多項式的位數減1。
根據以上過程,可以將使用C++標準庫中的string
#include <string>
using namespace std;
class BINPOLY
{
friend BINPOLY operator+(const BINPOLY&, const BINPOLY&);
friend BINPOLY operator-(const BINPOLY&, const BINPOLY&);
friend BINPOLY operator*(const BINPOLY&, const BINPOLY&);
friend pair<BINPOLY, BINPOLY> operator/(const BINPOLY&, const BINPOLY&);
private:
string strBin;
void Set(size_t);
void Reset(size_t);
void DelZero();
void check();
static char xor(char, char);
public:
BINPOLY() : strBin(1, '0'){};
BINPOLY(const string& strBin) : strBin(strBin){ DelZero(); };
BINPOLY(const BINPOLY&);
BINPOLY(BINPOLY&&);
~BINPOLY(){};
BINPOLY& operator=(const BINPOLY&);
BINPOLY& operator=(BINPOLY&&);
BINPOLY& operator+=(const BINPOLY&);
BINPOLY& operator-=(const BINPOLY&);
BINPOLY& operator*=(const BINPOLY&);
static string toBinData(const string&);
string toHexData();
string::size_type size() const;
const string& get() const;
const char& getBit(size_t) const;
BINPOLY& set(const string&);
BINPOLY& setBit(size_t, char);
BINPOLY& append(size_t, char);
BINPOLY& push_back(char);
BINPOLY& pop_back();
};
建構函式
每個類都分別定義了它的物件被初始化的方式,類通過一個或者幾個特殊的成員函式來控制其物件的初始化過程,這些函式叫做建構函式(constructor)。建構函式的任務是初始化類物件的資料成員,無論何時只要累的物件被建立,就會執行建構函式。
建構函式的名字和類名相同。和其他函式不一樣的是,建構函式沒有返回型別,除此之外都類似於其它的函式,比如空引數列表和空函式體。類可以包含多個建構函式,即過載(overloaded)。過載函式名稱可以相同,所以要使用引數型別和引數數量進行區分。需要注意的是,不可以只用返回型別來區分過載函式。
預設建構函式
預設建構函式是沒有引數的建構函式,它指出了類物件的預設構造方法,即在不提供任何引數的情況下該如何構造物件。上面的程式碼中,BINPOLY() : strBin(1, '0'){}
就是預設建構函式。
建構函式初始值列表
上面的建構函式中出現了新的部分,即冒號以及冒號和花括號之間的程式碼,這一部分稱為建構函式初始值列表(constructor initialize list),它的作用是為新建立的物件的一個或幾個資料成員賦初值。建構函式初始值是成員名字的一個列表,每個名字後面緊跟括號括起來的(或者在花括號內的)成員初始值。不同成員的初始值通過逗號分割開來。在上面的程式碼中,strBin
物件被初始化為字串"0"
。
拷貝建構函式
如果一個建構函式的第一個引數是自身類型別的引用,且任何額外引數(我還沒見過有其它引數的。。。)都有預設值,則此建構函式是拷貝建構函式(copy constructor),例如上面程式碼中的BINPOLY(const BINPOLY&)
。如果我們沒有為一個類定義拷貝建構函式,編譯器會為我們定義一個合成拷貝建構函式(synthesize copy constructor)。雖然大多數情況下編譯器都會生成一個正確的拷貝建構函式,但是最好還是自己定義一個。
因為任何非引用型別的引數初始化都需要拷貝操作,所以拷貝建構函式的引數必須是引用型別,否則為了拷貝引數,又要呼叫拷貝建構函式,這就陷入了無限迴圈中。
移動建構函式
C++11新標準的一個最主要的特性就是可以移動物件而非拷貝物件。大多數情況下,物件拷貝之後原始物件就被銷燬了,這時如果移動物件將會大幅提升效能。右值引用(rvalue reference)就是必須繫結到右值的引用,通過使用&&
來獲得右值引用,所以原來的引用更準確的應為左值引用(lvalue-reference)。右值引用有著完全相反的繫結特性:我們可以將一個右值引用繫結到要求轉換的表示式、字面值常量或是返回右值的表示式:
int i = 42;
int& r = i; //正確,左值引用
int&& rr = i; //錯誤,右值引用繫結到了左值上
int& r2 = i * 42 //錯誤,i*42是一個右值
const int& r3 = i *42; //正確,可以將常量引用繫結到一個右值
int&& rr2 = i * 42; //正確,右值引用
移動建構函式(move constructor)的第一個引數是該類型別的一個右值引用,任何的額外引數都應該有預設實參。除了完成資源移動,移動建構函式還必須確保銷燬源物件是無害的。一旦完成資源移動後,源物件必須不再指向被移動的資源——這些資源的所有權已經歸屬新建立的物件。上面的例子中BINPOLY(BINPOLY&&)
就是一個移動建構函式。
解構函式
解構函式(destructor)執行和建構函式相反的操作:建構函式初始化物件的非static
資料成員,而解構函式釋放物件所使用的資源,銷燬非static
資料成員。
解構函式是一個成員函式,名字用波浪線接類名組成,它沒有返回值,也不接受引數,上面的例子中~BINPOLY(){}
就是解構函式。當未定義解構函式是,編譯器會生成一個合成解構函式(synthesized destructor)。雖然大多數情況下編譯器都會生成一個正確的拷貝建構函式,但是最好還是自己定義一個。
this指標
成員函式通過一個名為this
的額外隱式引數來訪問呼叫成員函式的那個物件。當我們呼叫一個成員函式時,用呼叫該函式的那個物件的地址初始化this
。在成員函式的內部,可以直接使用呼叫該函式的物件的成員,而無須通過成員訪問符來做到這一點,因為this
所指向的就是這個物件。任何對成員的直接訪問都被看做是對this
的隱式引用。
const成員函式
上面的例子中,string::size_type size() const
最後的const修飾的是隱式指標this
的型別。預設情況下,this
的型別是BINPOLY* const
,即this
是一個常量指標,它只能指向本物件。根據之前的介紹,this
是一個頂層const,所以可以通過它來修改它所指向的物件。但是如果該物件是一個常量物件(const BINPOLY
),那麼this
是不能指向這個物件的,除非this
的型別為const BINPOLY* const
。但是由於this
是隱式引數,便只能將const
寫在函式宣告的最後,來表明呼叫此成員函式的物件是一個指向常量的指標,類似這樣的成員函式稱作常量成員函式(const member function)。
在上面的例子中,const string& get() const
返回該二元域上的多項式,無論是常量物件還是非常量物件,都應該可以呼叫此函式,所以需要尾部的const
。由於有了這個const
,那麼該物件就變成了常量物件,其成員變數strBin
就變為了const string
型別,故而返回型別為const string&
。const char& getBit(size_t) const
也是同樣的道理。
運算子過載
過載的運算子是具有特殊名字的函式,它們的名字由關鍵字operator
和其後要定義的運算子共同組成。和其它函式一樣,過載的運算子也包含返回型別、引數列表以及函式體。
過載運算子函式的引數數量與該運算子作用的運算物件數量一樣多。一元運算子有一個引數,二元運算子有兩個。對於二元運算子來說,左側運算物件傳遞給第一個引數,而右側運算物件傳遞給第二個引數。除了過載的函式呼叫運算子operator()
之外,其它過載運算子不能含有預設實參。下表中給出了常見的可以被過載的運算子。
可以被過載的運算子 | 含義 |
---|---|
+,-,*,/,% | 加法、減法、乘法、除法和取模運算子 |
^,|,~ | 異或、或和取反運算子 |
& | 按位與運算子(不要過載為取地址運算子) |
! | 條件非運算子 |
, | 逗號運算子(不建議過載) |
<,>,<=,>= | 小於、大於、小於等於和大於等於運算子 |
==,!= | 相等和不等運算子 |
++,– | 自增和自減運算子 |
<< | 移位運算子和輸出運算子 |
>> | 移位運算子和輸入運算子 |
&&,|| | 邏輯與、或運算子(不建議過載,會丟失短路屬性) |
= | 賦值運算子 |
+=,-=,*=,/=,%= | 加法、減法、乘法、除法和取模賦值運算子 |
&=,/ | =,^= |
<<=,>>= | 移位賦值運算子 |
[] | 下表運算子 |
() | 呼叫運算子 |
-> | 成員訪問運算子 |
new,new[],delete,delete[] | 記憶體分配運算子 |
如果一個運算子函式是成員函式,則它的第一個(左側)運算物件預設繫結到this
指標上,因此成員運算子函式的引數數量比運算子的運算物件少1個。下面是判斷是否為成員函式的一些標準:
- 賦值運算子(=)、下標運算子([])、呼叫運算子(())和成員訪問運算子(->)必須為成員函式;
- 複合賦值運算子一般來說應該是成員函式,但是這不是必須的;
- 改變物件狀態的運算子或者與給定型別密切相關的運算子,如遞增(++)、遞減(–)和解引用(*)運算子通常應該是成員函式;
- 具有對稱性的運算子,例如算術、相等性判斷、關係判斷和位運算子等通常應該是非成員函式。
拷貝賦值運算子
拷貝賦值運算子(copy-assignment operator)是過載的賦值(=
)運算子,它接受一個和自身類相同的引數,此引數可以理解為賦值運算子右邊的運算元。上面程式碼中BINPOLY& operator=(const BINPOLY&)
就是一個拷貝賦值運算子。引數型別可以是型別本身,但是此時傳參過程將會進行拷貝。通常拷貝物件的開銷都是比較大的,所以引數型別一般是該型別的常量引用。
為了和普通的賦值操作運算子保持一致,拷貝賦值運算子通常返回左側運算物件的引用。舉個例子,類似a=b=c
的賦值是正確的,在計算時先進行b=c
的賦值,為了能讓a=b
繼續執行,b=c
要返回b
的引用(其實這裡不一定是引用,返回左側運算物件的一份拷貝也可以,但是會增大開銷)。
和拷貝建構函式一樣,如果沒有定義拷貝建構函式,編譯器會生成一個合成拷貝賦值運算子(synthesized copy-assignment operator)。雖然大多數情況下編譯器都會生成一個正確的拷貝建構函式,但是最好還是自己定義一個。
移動賦值運算子
移動賦值運算子(move-assignment operator)執行與解構函式和移動建構函式相同的工作。上面的例子中,BINPOLY& operator=(BINPOLY&&)
就是移動建構函式。
複合賦值運算子
複合賦值運算子不一定是要是類的成員函式,但是我比較傾向於定義為成員函式,例如上面的BINPOLY& operator+=(const BINPOLY&)
,因為它們不改變右側運算物件的狀態,所以引數型別為const BINPOLY&
,返回型別的為該物件的引用。
算術運算子
算術運算子通常會計算它的兩個運算物件並得到一個新值,這個值有別於任意一個運算物件,常常位於一個區域性變數之內,操作完成後返回該區域性變數的副本作為其結果。BINPOLY
中BINPOLY operator+(const BINPOLY&, const BINPOLY&)
,類似的減法、乘法和除法運算子也都被定義為非成員函式。
三五法則
如上所述,由3個基本操作可以控制類的拷貝操作:拷貝建構函式、拷貝賦值運算子和解構函式。在C++11新標準下,一個類還可以定義一個移動建構函式和一個移動賦值運算子。一般來說,要麼只定義3個拷貝操作,要麼在此基礎上再定義2個移動操作,這就是三五法則。
訪問控制與封裝
到目前為止,已經為BINPOLY
定義了介面,但是沒有任何機制強制使用者使用這些介面,也就是類還沒有封裝,依然可以通過.
運算子來直接訪問成員變數。接下來使用訪問說明符(access specifiers)對類進行封裝。
- 定義在
public
說明符之後的成員在成個程式內可以被訪問,public成員定義類的介面; - 定義在
private
說明符之後的成員可以被類的成員函式訪問,但是不能通過.
運算子訪問,private
部分封裝了類的實現細節。
BINPOLY
類中,將string strBin
設定為private的就是為了禁止直接訪問strBin
。Set(size_t)
、Reset(size_t)
、DelZero()
、check()
和xor(char, char)
這四個成員函式不想被類物件直接使用,只供成員函式使用,因此也定義為了私有的。
友元
上文提到,加法運算子被過載為非成員函式,它雖然也是類介面的一部分,但是由於strBin
是私有的,所以加法運算子無法訪問strBin
(實際上它也沒有訪問strBin
)。如果某些非成員函式想要訪問類中的私有成員,那麼該函式要作為類的友元(friend),即在類中宣告該函式,並且在宣告的最開始處加上friend
關鍵字。在BINPOLY
中,算術運算子都是它的友元。
類的靜態成員
有時候類需要它的一些成員和類本身直接相關,而不是和各個物件保持關聯。BINPOLY
所定義的物件是二元域上多項式的係數,所以它必須都是由0和1組成。但是在計算CRC時,通常都是給出字串作為原始明文,這就需要將其轉換為二進位制的形式,所以需要定義一個函式來實現這個轉換。我們當然可以把轉換函式作為成員函式,但是仔細想想又有問題。我們就是為了構造一個BINPOLY
的物件,但是當沒有物件時,該使用哪個物件來呼叫這個轉換函式呢?為了解決這個問題,就需要把轉換函式定義為靜態的(static)。
在BINPOLY
類中,toBinData(const string&)
函式被定義為公有靜態成員函式。為了將普通字串進行轉換,toHexData()
必須可以在類外被訪問,所以必須是公有靜態成員函式。
xor(char, char)
是BINPOLY
類進行算術運算的基本方式,和類物件無關,也不希望被外部訪問,所以被定義為私有靜態成員函式。另外,由於在過載的除法運算子中需要使用xor()
函式,而除法運算子作為友元存在,在呼叫時沒有傳遞this
指標,直接呼叫xor()
運算子將使編譯器無法解析,所以也必須通過類來呼叫。
訪問類的靜態成員通過::
作用域運算子,例如BINPOLY::toBinData
和BINPOLY::xor()
。