后台開發:核心技術與應用實踐 -- C++


本書介紹的“后台開發”指的是“服務端的網絡程序開發”,從功能上可以具體描述為:服務器收到客戶端發來的請求數據,解析請求數據后處理,最后返回結果。

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 選擇作為了關聯容器的內部結構。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM