後臺開發:核心技術與應用實踐 -- C++

zhhfan發表於2021-03-06

本書介紹的“後臺開發”指的是“服務端的網路程式開發”,從功能上可以具體描述為:伺服器收到客戶端發來的請求資料,解析請求資料後處理,最後返回結果。

C++程式設計常用技術

include 一個 .h 檔案,就是等於把整個 .h 檔案給複製到程式中,include 一個 cpp 檔案也是如此。使用include的方式有兩種:1. #include<> 2. #include""
#include<>#include""的區別是:#include<>常用來包含系統提供的標頭檔案,編譯器會到儲存系統標準標頭檔案的位置查詢標頭檔案;而#include""常用於包括程式設計師自己編號的標頭檔案,用這種格式時,編譯器先查詢當前目錄是否有指定名稱的標頭檔案,然後從標準頭目錄中
進行查詢。

包含C語言的標頭檔案是,常引用的是.h檔案,而C+++標準為了語言區別開,也為了正確使用名稱空間,規定標頭檔案不再使用字尾 .h

C++允許用同函式名定義多個函式,但這些函式必須引數個數不同或型別不同,這就是函式過載。

函式模板,實際上是建立一個通用函式,其函式型別和形參不具體指定,而用一個虛擬的型別來代表,這個通用函式就是函式模板。凡是函式體相同的函式都可以用這個模板來代替,而不用定義多個函式,實際使用時只需在模板中定義一次就可以了。在呼叫函式時,系統會根據實參的型別來取代模板中的虛擬型別,從而實現不同函式的功能。
定義函式模板的一般格式是:

template<typename T>
T min(T a,T b,T c){ 
    if(a>b)a=b ; 
    if(a>c)a=c ; 
    return a;
}

通常用 strlen() 函式來計算一個字串的長度,strlen() 函式比較容易混淆的是 sizeof() 函式。
strlen和sizeof的區別如下所述:

  1. strlen()是函式,在執行時才能計算,引數必須是字元型指標(char *),且必須是以\0結尾的。當陣列名作為引數傳入時,實際上陣列已經退化為指標了,它的功能是返回字串的長度。
  2. sizeof()是運算子,而不是一個函式,在編譯時就計算好了,用於計算資料空間的位元組數。因此,sizeof 不能用來返回動態分配的記憶體空間的大小 sizeof 常用於返回型別和靜態分配的物件、結構或陣列所佔的空間,返回值跟物件、結構、陣列所儲存的內容沒有關係。

當引數分別如下時 sizeof 返回的值表示的含義如下所述:

  1. 陣列一一編譯時分配的陣列空間大小
  2. 指標一一儲存該指標所用的空間大小(int型別大小,32位機器為4 Byte)
  3. 型別一一該型別所佔的空間大小
  4. 物件一一物件實際佔用空間大小
  5. 函式一一函式的返回型別所佔的空間大小,且函式的返回型別不能是 void

C++編譯系統在 32 位機器上為整型變數分配4Byte,為單精度浮點型變數分配 4Byte ,為字元型變數分配 1Byte。

陣列指標與指標陣列
陣列指標也稱為行指標:假設有定義 int (*p)[n];且()優先順序高,首先說明p是一個指標,且指向一個整型的一維陣列。這個一維陣列的長度是n,也可以說是p的步長,也就是說執行 p+l 時,p要跨過n個整型資料的長度。

int a[3][4]; 
int (*p)[4]; 
p=a ; 
p++ ;

指標陣列不同於陣列指標,假設有定義 int *p[n];且[]優先順序高,可以理解為先與p結合成為一個陣列,再由 int*說明這是一個整型指標陣列。它有n個指標型別的陣列元素。

int *p[3];

優先順序 () > [] > *

函式指標是指向函式的指標變數 所以,函式指標首先是個指標變數,而且這個變數指向一個函式。
函式指標的宣告方法:

// 返回值型別 (*指標變數名) ([形參列表]);
int func(int a); 
int (*f) (int a); 
f=&func;
int b;
(*f) (b); // 函式呼叫

在宣告一個引用變數時,必須同時使之初始化,即宣告它代表哪個變數,函式執行期間,不可以將其再作為其他變數的引用。

使用引用傳遞函式的引數時,在記憶體中並沒有產生實參的副本,而是對實參直接操作。當使用一般變盤傳遞函式的引數時,當函式發生呼叫,需要給形參分配儲存單元,形參變數是實參變數的副本;如果傳遞的是物件,還將呼叫拷貝建構函式。因此,當引數傳遞的資料較大時,用引用比用 一般變數傳遞引數的效率更高,所佔空間更少。

結構體的宣告方法如下所示:

struct 結構名{
    資料型別 成員名;
    資料型別 成員名;
    ...
}

共用體,用關鍵字 union 來定義,它是一種特殊的類,一個共用體裡可以定義多種不同的資料型別,這些資料共享一段記憶體,在不同的時間裡儲存不同的資料型別和長度的變數,以達到節省空間的目的,但同一時間只能儲存其中一個成員變數的值。

共用體的宣告方式為:

union 共用體型別名{
    資料型別 成員名;
    資料型別 成員名;
    ...
}變數名;

可以使用 union 判斷系統是 big endian (大端)還 little endian 小端。

#include<iostream> 
using namespace std; 
union TEST{ 
    short a ; 
    char b[sizeof(short)] ;
}
int main(){ 
    TEST test;
    test.a=Ox0102; // 不能引用共用體變數 只能引用共用體變數中的成員
    if(test.b[0] == 0x01 && test.b[1] == 0x02) 
        cout << " big endian. " << endl;
    else if(test.b[0] == 0x02 && test.b[1] == 0x01)
        cout << " small endian." << endl;
    else cout << "unkonw" << endl;
}

其中, big endian 是指低地址存放最高有效位元組, little endian 是低地址存放最低有效位元組。

列舉型別是一種基本資料型別,而不是構造型別,因為它不能再分解為任何其他基本型別。
列舉的宣告方式為:

enum 列舉型別名{列舉常量表列};

如同結構和共用體一樣,列舉變數也可用不同的方式說明,即先定義後說明,同時定義說明或直接說明
設有變 a,b,c 是列舉型別 weekday,可採用下述任一種方式:

// 1.
enum weekday{ sun , mou , tue , wed , thu , fri , sat }; 
enum weekday a,b,c; 
// end
// 2.
enum weekday{ sun ,mou , tue , wed , thu , fri , sat }a,b,c;
// end
// 3.
enum { sun, mou , tue , wed , thu, fri, sat}a,b,c;
// end

列舉值是常量,不是變數。 不能在程式中用賦值語句再對它賦值。

只能把列舉值賦予列舉變數,不能把元素的數值直接賦予列舉變數

a = sum; // correct 
b = mon; // correct
a = 0; // error
b = 1; // error

如果一定要把數值賦予列舉變數,則必須用強制型別轉換

a=(enum weekday)2;
a=tue; // 以上二者等價

一般64位機器上各個資料型別所佔的儲存空間(byte):

Type char short int long float double long long
Size 1 2 4 8 4 8 8

其中,long 型別在 32 位機器上只佔 4Byte ,其他型別在 32 位機器和 64 位機器都是佔同樣的大小。

union的位元組數計算
union 變數共用記憶體應以最長的為準,同時共用體內變數的預設記憶體對齊方式以最長的型別對齊。

union A{
    int a[5];
    double b;
    char c;
}

該結構體佔用記憶體為24Byte,因為要以double對齊,double佔8byte,4*5=20,對齊之後變為24byte。同樣a[5] 改為a[6]依舊佔用24byte,但是改為a[7]將佔用32byte。

struct的位元組數計算

struct B{ 
    char a; 
    double b; 
    int c; 
};

這是因為 char 的偏移量為0,佔用 lByte; double 指的是下一個可用的地址的偏移量為1,不是 sizeof(double )=8的倍數,需要補足 7Byte 才能使偏移量變為8; int 指的是下一個可用的地址的偏移量為 16,是 sizeof(int)=4 的倍數,滿足 int 的對齊方式。
故所有成員變數都分配了空間,空間總的大小為 1+7+8+4=20 ,不是結構的節邊界數(即結構中佔用最大空間的基本型別所佔用的位元組數 sizeof (double )=8 )的倍數,所以需要填充 4Byte ,以滿足結構的大小為 s.izeof( double )=8 的倍數,即 24。

C++提供的預處理功能主要有以下四種:巨集定義、檔案包含、條件編譯和佈局控制。

  • 巨集定義

    #define 命令是一個巨集定義命令,它用來將一個識別符號定義為一個字串,該識別符號被稱為巨集名,被定義的字串稱為替換文字。該命令有兩種格式:一種是簡單的巨集定義,另一種是帶引數的巨集定義。
    簡單的巨集定義的宣告格式如下所示:

    #define 巨集名 字串
    eg: #define pi 3.14
    

    帶引數的巨集定義的宣告格式如下所示:

    #define 巨集(參數列列)巨集
    eg: #define A(x) x*x
    #define area(x) x*x 
    int main (){ 
        int y = area(2+2) ; 
        cout << y << endl ;
        return 0;
    }
    // output: 8
    
  • 條件編譯

    一般情況下,源程式中所有行的語句都參加編譯,但是有時程式設計師希望其中一部分內容只在滿足一定條件時才進行編譯,也就是對 部分內容指定編譯的條件,這就用到了“條件編譯”。
    條件編譯命令最常見的形式為:

    #ifdef 識別符號
        程式段
    #else 
        程式段
    #endif
    // 另一種形式
    #if 表示式
        程式段
    #else 
        程式段
    #endif
    

物件導向的C++

物件是類型別的一個變數,類則是物件的模板,類是抽象的,不佔用儲存空間的;而物件是具體的,佔用儲存空間。

struct和class相似,但是還有一些不同。struct 中的成員訪問許可權預設是 public,而 class 中則是 private。在C語言中, struct 中不能定義成員函式,而在 C++ 中,增加 class 型別後 ,擴充套件了 struct 的功能,struct 中也能定義成員函式了。

類中的成員和成員函式具有三種訪問許可權:private,protected, public,預設為private。private成員只限於類成員訪問,protected成員:允許類成員和派生類成員訪問,不允許類外的任何成員訪問,public成員:允許類成員和類外的任何成員訪問。

成員函式可以在類體中定義,也可以在類外定義。
在類外定義樣例:

返回型別 類名::函式名(引數列表){
    函式體
}

類的靜態資料成員來擁有一塊單獨的儲存區,而不管建立了多少個該類的物件,所有這些物件的靜態資料成員都共享一塊靜態儲存空間,這就為這些物件提供了一種互相通訊的方法。靜態資料成員是屬於類的,它只在類的範圍內有效。因為不管產生了多少物件,類的靜態資料成員都有著單一的儲存空間,所以儲存空間必須定義在一個單一的地方。如果一個靜態資料成員被宣告而沒有被定義,連結器會報告一個錯誤:“定義必須出現在類的外部而且只能定義一次”。

與資料成員類似,成員函式也可以定義為靜態的,在類中宣告函式的前面加 static 關鍵字就成了靜態成員函式,如:

class Box{
public:
    static int volume();
}

如果要在類外呼叫公用的靜態成員函式,要用類名和域運算子“: ”,如:

Box::volume();

實際上也允許通過物件名呼叫靜態成員函式,如:

a.volume( );

但這並不意味著此函式是屬於物件a的,而只是用a的型別而巳。
與靜態資料成員不同,靜態成員函式的作用不是為了物件之間的溝通,而是為了能處理靜態資料成員。
而靜態成員函式並不屬於某一物件,它與任何物件都無關,因此靜態成員函式沒有 this 指標。

靜態成員函式與非靜態成員函式的根本區別是:非靜態成員函式有 this 指標,而靜態成員函式沒有 this 指標,由此決定了靜態成員函式不能訪問本類中的非靜態成員,在 C++ 程式中,靜態成員函式主要用來訪問靜態資料成員,而不訪問非靜態成員。

物件的儲存空間

對於一個空類,裡面既沒有資料成員,也沒有成員函式,該類物件的大小為1Byte。
類的靜態資料成員不佔物件的記憶體空間,同時,成員函式包括建構函式和解構函式也是不佔空間的。而對於有虛擬函式的類來說,每個物件都會儲存一個指向虛擬函式表的指標,該指標在64位的機器上佔8Byte。

在每一個成員函式中都包含一個特殊的指標,這個指標的名字是固定的,稱為 this指標,它是指向本類物件的指標,它的值是當前被呼叫的成員函式所在的物件的起始地址。

在一般情況下,呼叫解構函式的次序正好與呼叫建構函式的次序相反:最先被呼叫的建構函式,其對應的(同一物件中的)解構函式最後被呼叫;而最後被呼叫的建構函式,其對應的解構函式最先被呼叫。

繼承與派生

宣告派生類的一般形式為:

class 派生類名 [繼承方式] 基類名{
    派生類新增加的成員
};

其中的繼承方式包括 public (公用的)、 private (私有的)和 protected (受保護的),此項是可選的,如果不寫此項,則預設為 private (私有的)。

基類成員在派生類中的訪問屬性:

  1. 公用繼承(public inheritance):基類的公用成員和保護成員在派生類中保持原有訪問屬性,其私有成員仍為基類私有
  2. 私有繼承(private inheritance):基類的公用成員和保護成員在派生類中成了私有成員,其私有成員仍為基類私有
  3. 受保護的繼承(protected inheritance):基類的公用成員和保護成員在派生類中成了保護成員,其私有成員仍為基類私有。受保護成員的意思是,不能被外界引用但可以被派生類的成員引用。

綜上,可以視為基類訪問許可權與派生類繼承方式的疊加最小訪問許可權。同時,無論哪一種繼承方式,在派生類中是不能訪問基類的私有
成員的,私有成員只能被本類的成員函式所訪問,畢竟派生類與基類不是同一個類

構造派生類的物件時,必須對基類資料成員、新增資料成員和成員物件的資料成員進行初始化。派生類的建構函式必須要以合適的初值作為引數,隱含呼叫基類和新增物件成員的建構函式,來初始化它們各自的資料成員,然後再加入新的語句對新增普通資料成員進行初始化。

派生類建構函式必須對這3類成員進行初始化,其執行順序是這樣的:

  1. 先呼叫基類建構函式;
  2. 再呼叫子物件的建構函式;
  3. 最後呼叫派生類的建構函式體

當派生類有多個基類時,處於同一層次的各個基類的建構函式的呼叫順序取決於定義派生類時宣告的順序(自左向右),而與在派生類建構函式的成員初始化列表中給出的順序無關。

在派生時,派生類是不能繼承基類的解構函式的,也需要通過派生類的解構函式去呼叫基類的解構函式。在派生類中可以根據需要定義自己的解構函式,用來對派生類中所增加的成員進行清理工作;基類的清理工作仍然由基類的解構函式負責。在執行派生類的解構函式時,系統會自動呼叫基類的解構函式和子物件的解構函式,對基類和子物件進行清理。

類的多型

在 C++ 程式設計中,多型性是指具有不同功能的函式可以用同一個函式名,這樣就可以用一個函式名呼叫不同內容的函式。在物件導向方法中,一般是這樣表述多型性的:向不同的物件傳送同一個訊息,不同的物件在接收時會產生不同的行為(即方法);也就是說,每個物件可以用自己的方式去響應共同的訊息所謂訊息,就是呼叫函式,不同的行為就是指不同的實現,即執行不同的函式。

兩個同名函式不在同一個類中,而是分別在:基類和派生類中,屬於同名覆蓋。若是過載函式,二者的引數個數和引數型別必須至少有一者不同,否則系統無法確定呼叫哪一個函式。而 虛擬函式 的作用是允許在派生類中重新定義與基類同名的函式,並且可以通過基類指標或引用來訪問基類和派生類中的同名函式。

虛擬函式的宣告方式:

virtual 返回型別 函式名();

當把基類某個成員函式宣告為虛擬函式後,就允許在其派生類中對該函式重新定義,賦予它新的功能,且可以通過指向基類的指標指向同一類族中不同類的物件,從而呼叫其中的同名函式。虛擬函式實現了同一類族中不同類的物件可以對同一函式呼叫作出不同的響應的動態多型性。

C++中規定,當某個成員函式被宣告為虛擬函式後,其派生類中的同名函式都自動成為虛擬函式。

純虛擬函式是在基類中宣告的虛擬函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛擬函式的方法是在函式原型後加=,如下所示:

virtual void funtion()=0;

含有純虛擬函式的類稱為抽象類,它不能生成物件。

在C++中,,建構函式不能宣告為虛擬函式,這是因為編譯器在構造物件時,必須知道確切型別,才能正確地生成物件;其次,在建構函式執行之前,對像並不存在,無法使用指向此對像的指標來呼叫建構函式。然而,解構函式可以宣告為虛擬函式。C++明確指出,當derived class 物件經由 base class 指標被刪除 而該 base class 帶著一個non-virtual 解構函式, 導致物件的 derived 成分沒被銷燬掉,解構函式不是虛擬函式容易引發記憶體洩漏。

單例模式 通過類本身來管理其唯一例項,唯一的例項是類的一個普通物件,但設計這個類時,讓它只能建立一個例項並提供對此例項的全域性訪問。使用類的私有靜態指標變數指向類的唯一例項,並用一個公有的靜態方法來獲取該例項。單例模式的作用就是保證在整個應用程式的生命週期中的任何時刻,單例類的例項都只存在一個(當然也可以不存在)。

常用 STL 的使用

對於vector容器來說,可以使用reserve(*)來對容器進行擴容,避免多次自動擴容帶來的效能損失,可以使用技巧vector<int>(ivec).swap(ivec)來將容器容量緊縮到合適的大小。其中vector<int> (ivec)表示使用ivec來建立一個臨時vector,然後將現有的容器與臨時容器進行交換,之後臨時容器將會被銷燬,因為臨時容器的容量是自動設定的合適大小,因此,容量緊縮成功。需要注意的是vector 是按照容器現在容量的一倍進行增長

map 內部自建一棵紅黑樹(一種非嚴格意義上的平衡二叉樹),這棵樹具有對資料自動排序的功能,所以在 map 內部所有的資料都是有序的。

讓 map 中的元素按照 key 從大到小排序

map<string, int, greater<string>> mapStudent;

紅黑樹,一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是 Red或Black。通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡。

二叉查詢樹,也稱有序二叉樹 (ordered binary tree),或已排序二叉樹 (sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:

  1. 若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值
  2. 若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值
  3. 任意節點的左、右子樹也分別為二叉查詢樹
  4. 沒有鍵值相等的節點

紅黑樹雖然本質上是一棵二叉查詢樹,但它在二叉查詢樹的基礎上增加了著色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查詢 插入、刪除的時間複雜度最壞為 \(O(log n)\)

紅黑樹的5個性質:

  1. 每個結點要麼是紅的要麼是黑的
  2. 根結點是黑的
  3. 每個葉結點都是黑的(葉子是NIL結點)
  4. 如果一個結點是紅的,那麼它的兩個兒子都是黑的;
  5. 對於任意結點而言,其到葉結點樹尾端 NIL 指標的每條路徑都包含相同數目的黑結點

紅黑樹示例:

當在對紅黑樹進行插入和刪除等操作時,對樹做了修改可能會破壞紅黑樹的性質,為了繼續保持紅黑樹的性質,可以通過對結點進行重新著色,以及對樹進行相關的旋轉操作,即通過修改樹中某些結點的顏色及指標結構,來達到對紅黑樹進行插入或刪除結點等操作後繼續保持它的性質或平衡的目的。

樹的旋轉分為左旋和右旋,一下給出示例
左旋: (隻影響旋轉結點和其右子樹的結構,把右子樹的結點往左子樹挪了)

右旋:(隻影響旋轉結點和其左子樹的結構,把左子樹的結點往右子樹挪了)

樹在經過左旋右旋之後,樹的搜尋性質保持不變,但樹的紅黑性質被破壞了,所以紅黑樹插入和刪除資料後,需要利用旋轉與顏色重塗來重新恢復樹的紅黑性質。

紅黑樹參考文獻

set 作為一個關聯式容器,是用來儲存同一資料型別的資料型別。在 set 中每個元素的值都唯一的,而且系統能根據元素的值自動進行排序。應該注意的是 set 中元素的值不能直接被改變。C++ STL 中標準關聯容器 set、mutiset、map、multimap 內部採用的都是紅黑樹。紅黑樹的統計效能要好於一般平衡二叉樹,所以被 STL 選擇作為了關聯容器的內部結構。

相關文章