1、一些C++基礎知識 模板類string的設計屬於底層,其中運用到了很多C++的編程技巧,比如模板、迭代器、友元、函數和運算符重載、內聯等等,為了便於後續理解string類,這裡先對涉及到的概念做個簡單的介紹。C++基礎比較扎實的童鞋可以直接跳到第三節。 1.1 typedef 1.1.1 四種常 ...
1、一些C++基礎知識
模板類string的設計屬於底層,其中運用到了很多C++的編程技巧,比如模板、迭代器、友元、函數和運算符重載、內聯等等,為了便於後續理解string類,這裡先對涉及到的概念做個簡單的介紹。C++基礎比較扎實的童鞋可以直接跳到第三節。
1.1 typedef
1.1.1 四種常見用法
- 定義一種類型的別名,不只是簡單的巨集替換。可用作同時聲明指針型的多個對象
typedef char* PCHAR; PCHAR pa, pb; // 同時聲明兩個char類型的指針pa和pb char* pa, pb; // 聲明一個指針(pa)和一個char變數(pb) // 下邊的聲明也是創建兩個char類型的指針。但相對沒有typedef的形式直觀,尤其在需要大量指針的地方 char *pa, *pb;
順便說下,*運算符兩邊的空格是可選的,在哪裡添加空格,對於編譯器來說沒有任何區別。
char *pa; // 強調*pa是一個char類型的值,C中多用這種格式。 char* pa; // 強調char*是一種類型——指向char的指針。C++中多用此種格式。另外在C++中char*是一種複合類型。
- 定義struct結構體別名
在舊的C代碼中,聲明struct新對象時,必須要帶上struct,形式為:struct 結構名 對象名。
1 // 定義 2 struct StudentStruct 3 { 4 int ID; 5 string name; 6 }; 7 // C聲明StudentStruct類型對象 8 struct StudentStruct s1;
使用typedef來定義結構體StrudentStruct的別名為Student,聲明的時候就可以少寫一個struct,尤其在聲明多個struct對象時更加簡潔直觀。如下:
1 // 定義 2 typedef struct StudentStruct 3 { 4 int ID; 5 string name; 6 }Student; 7 // C聲明StudentStruct類型對象s1和s2 8 Student s1, s2;
而在C++中,聲明struct對象時本來就不需要寫struct,其形式為:結構名 對象名。
// C++聲明StudentStruct類型對象s1和s2 StudentStruct s1,s2;
所以,在C++中,typedef的作用並不大。瞭解他便於我們閱讀舊代碼。
- 定義與平臺無關的類型
比如定義一個REAL的浮點類型,在目標平臺一上,讓它表示最高精度的類型為:
typedef long double REAL;
在不支持long double的平臺二上,改為:
typedef double REAL;
在連double都不支持的平臺三上,改為:
typedef float REAL;
也就是說,在跨平臺時,只要改下typedef本身就行,不要對其他源碼做任何修改。
標準庫中廣泛使用了這個技巧,比如size_t、intptr_t等
1 // Definitions of common types 2 #ifdef _WIN64 3 typedef unsigned __int64 size_t; 4 typedef __int64 ptrdiff_t; 5 typedef __int64 intptr_t; 6 #else 7 typedef unsigned int size_t; 8 typedef int ptrdiff_t; 9 typedef int intptr_t; 10 #endif
-
為複雜的聲明定義一個新的簡單的別名
在閱讀代碼的過程中,我們經常會遇到一些複雜的聲明和定義,例如:
1 // 理解下邊這種複雜聲明可用“右左法則”: 2 // 從變數名看起,先往右,再往左,碰到一個圓括弧就調轉閱讀的方向;括弧內分析完就跳出括弧,還是按先右後左的順序,如此迴圈,直到整個聲明分析完。 3 4 // 例1 5 void* (*(*a)(int))[10]; 6 // 1、找到變數名a,往右看是圓括弧,調轉方嚮往左看到*號,說明a是一個指針; 7 // 2、跳出內層圓括弧,往右看是參數列表,說明a是一個函數指針,接著往左看是*號,說明指向的函數返回值是指針; 8 // 3、再跳出外層圓括弧,往右看是[]運算符,說明函數返回的是一個數組指針,往左看是void*,說明數組包含的類型是void*。 9 // 簡言之,a是一個指向函數的指針,該函數接受一個整型參數並返回一個指向含有10個void指針數組的指針。 10 11 // 例2 12 float(*(*b)(int, int, float))(int);// 1、找到變數名b,往右看是圓括弧,調轉方嚮往左看到*號,說明b是一個指針; 13 // 2、跳出內層圓括弧,往右看是參數列表,說明b是一個函數指針,接著往左看是*號,說明指向的函數返回值是指針; 14 // 3、再跳出外層圓括弧,往右看還是參數列表,說明返回的指針是一個函數指針,該函數有一個int類型的參數,返回值類型是float。 15 // 簡言之,b是一個指向函數的指針,該函數接受三個參數(int, int和float),且返回一個指向函數的指針,該函數接受一個整型參數並返回一個float。 16 17 // 例3 18 double(*(*(*c)())[10])(); 19 // 1、先找到變數名c(這裡c其實是新類型名),往右看是圓括弧,調轉方嚮往左是*,說明c是一個指針; 20 // 2、跳出圓括弧,往右看是空參數列表,說明c是一個函數指針,接著往左是*號,說明該函數的返回值是一個指針; 21 // 3、跳出第二層圓括弧,往右是[]運算符,說明函數的返回值是一個數組指針,接著往左是*號,說明數組中包含的是指針; 22 // 4、跳出第三層圓括弧,往右是參數列表,說明數組中包含的是函數指針,這些函數沒有參數,返回值類型是double。 23 // 簡言之,c是一個指向函數的指針,該函數無參數,且返回一個含有10個指向函數指針的數組的指針,這些函數不接受參數且返回double值。 24 25 // 例4 26 int(*(*d())[10])(); // 這是一個函數聲明,不是變數定義 27 // 1、找到變數名d,往右是一個無參參數列表,說明d是一個函數,接著往左是*號,說明函數返回值是一個指針; 28 // 2、跳出裡層圓括弧,往右是[]運算符,說明d的函數返回值是一個指向數組的指針,往左是*號,說明數組中包含的元素是指針; 29 // 3、跳出外層圓括弧,往右是一個無參參數列表,說明數組中包含的元素是函數指針,這些函數沒有參數,返回值的類型是int。 30 // 簡言之,d是一個返回指針的函數,該指針指向含有10個函數指針的數組,這些函數不接受參數且返回整型值。
如果想要定義和a同類型的變數a2,那麼得重覆書寫:
void* (*(*a)(int))[10]; void* (*(*a2)(int))[10];
那怎麼避免這種沒有價值的重覆呢?答案就是用typedef來簡化複雜的聲明和定義。
// 在之前的定義前邊加typedef,然後將變數名a替換為類型名A typedef void* (*(*A)(int))[10]; // 定義相同類型的變數a和a2 A a, a2;
typedef在這裡的用法,總結一下就是:任何聲明變數的語句前面加上typedef之後,原來是變數的都變成一種類型。不管這個聲明中的標識符號出現在中間還是最後。
1.1.2 使用typedef容易碰到的陷進
- 陷進一
typedef定義了一種類型的新別名,不同於巨集,它不是簡單的字元串替換。比如:
typedef char* PSTR; int mustrcmp(const PSTR, const PSTR);
上邊的const PSTR並不是const char*,而是相當於.char* const。原因在於const給予了整個指針本身以常量性,也就是形成常量指針char* const。
- 陷進二
typedef在語法上是一個存儲類的關鍵字(和auto、extern、mutable、static、register等一樣),雖然它並不真正影響對象的存儲特性。如:
typedef static int INT2;
編譯會報錯:“error C2159:指定了一個以上的存儲類”。
1.2 #define
#define是巨集定義指令,巨集定義就是將一個標識符定義為一個字元串,在預編譯階段執行,將源程式中的標誌符全部替換為指定的字元串。#define有以下幾種常見用法:
- 無參巨集定義
格式:#define 標識符 字元串
其中的“#”表示這是一條預處理命令。凡是以“#”開頭的均為預處理命令。“define”為巨集定義命令。“標識符”為所定義的巨集名。“字元串”可以是常數、表達式、格式串等。
- 有參巨集定義
格式:#define 巨集名(形參表) 字元串
1 #define add(x, y) (x + y) //此處要打括弧,不然執行2*add(x,y)會變成 2*x + y 2 int main() 3 { 4 std::cout << add(9, 12) << std::endl; // 輸出21 5 return 0; 6 }
- 巨集定義中的條件編譯
在大規模開發過程中,頭文件很容易發生嵌套包含,而#ifdef配合#define,#endif可以避免這個問題。作用類似於#pragma once。
#ifndef DATATYPE_H #define DATATYPE_H ... #endif
- 跨平臺
在跨平臺開發中,也常用到#define,可以在編譯的時候通過#define來設置編譯環境。
1 #ifdef WINDOWS 2 ... 3 (#else) 4 ... 5 #endif 6 #ifdef LINUX 7 ... 8 (#else) 9 ... 10 #endif
- 巨集定義中的特殊操作符
#:對應變數字元串化
##:把巨集定義名與巨集定義代碼序列中的標識符連接在一起,形成一個新的標識符
1 #include <stdio.h> 2 #define trace(x, format) printf(#x " = %" #format "\n", x) 3 #define trace2(i) trace(x##i, d) 4 5 int main(int argc, _TCHAR* argv[]) 6 { 7 int i = 1; 8 char *s = "three"; 9 float x = 2.0; 10 11 trace(i, d); // 相當於 printf("x = %d\n", x) 12 trace(x, f); // 相當於 printf("x = %f\n", x) 13 trace(s, s); // 相當於 printf("x = %s\n", x) 14 15 int x1 = 1, x2 = 2, x3 = 3; 16 trace2(1); // 相當於 trace(x1, d) 17 trace2(2); // 相當於 trace(x2, d) 18 trace2(3); // 相當於 trace(x3, d) 19 20 return 0; 21 } 22 23 // 輸出: 24 // i = 1 25 // x = 2.000000 26 // s = three 27 // x1 = 1 28 // x2 = 2 29 // x3 =3
__VA_ARGS__:是一個可變參數的巨集,這個可變參數的巨集是新的C99規範中新增的,目前似乎只有gcc支持。實現思想就是巨集定義中參數列表的最後一個參數為省略號(也就是三個點)。這樣預定義巨集__VA_ARGS__就可以被用在替換部分中,替換省略號所代表的字元串,如:
1 #define PR(...) printf(__VA_ARGS__) 2 int main() 3 { 4 int wt=1,sp=2; 5 PR("hello\n"); // 輸出:hello 6 PR("weight = %d, shipping = %d",wt,sp); // 輸出:weight = 1, shipping = 2 7 return 0; 8 }
附:C++中其他常用預處理指令:
#include // 包含一個源代碼文件 #define // 定義巨集 #undef // 取消已定義的巨集 #if // 如果給定條件為真,則編譯下麵代碼 #ifdef // 如果巨集已定義,則編譯下麵代碼 #ifndef // 如果巨集沒有定義,則編譯下麵代碼 #elif // 如果前面#if給定條件不為真,當前條件為真,則編譯下麵代碼 #endif // 結束一個#if...#else條件編譯塊 #error // 停止編譯並顯示錯誤信息 __FILE__ // 在預編譯時會替換成當前的源文件cpp名 __LINE__ // 在預編譯時會替換成當前的行號 __FUNCTION__ // 在預編譯時會替換成當前的函數名稱 __DATE__ // 進行預處理的日期(“Mmm dd yyyy”形式的字元串文字) __TIME__ // 源文件編譯時間,格式為“hh:mm:ss”
1.3 typedef VS #define
C++為類型建立別名的方式有兩種,一種是使用預處理器#define,一種是使用關鍵字typedef,格式如下:
#define BYTE char // 將Byte作為char的別名 typedef char byte;
但是在聲明一系列變數是,請使用typedef而不是#define。比如要讓byte_pointer作為char指針的別名,可將byte_pointer聲明為char指針,然後再前面加上typedef:
typedef float* float_pointer;
也可以使用#define,但是在聲明多個變數時,預處理器會將下邊聲明“FLOAT_POINTER pa, pb;”置換為:“float * pa, pb;”,這顯然不是我們想要的結果。但是用typedef就不會有這樣的問題。
#define FLOAT_POINTER float* FLOAT_POINTER pa, pb;
1.4 using
using關鍵字常見用法有三:
- 引入命名空間
using namespace std; // 也可在代碼中直接使用std::
- 在子類中使用using引入基類成員名稱
子類繼承父類之後,在public、protected、private下使用“using 可訪問的父類成員”,相當於子類在該修飾符下聲明瞭該成員。
- 類型別名(C++11引入)
一般情況下,using與typedef作用等同:
// 使用using(C++11) using counter = long; // 使用typedef(C++03) // typedef long counter;
別名也適用於函數指針,但比等效的typedef更具可讀性:
1 // 使用using(C++11) 2 using func = void(*)(int); 3 4 // 使用typedef(C++03) 5 // typedef void (*func)(int); 6 7 // func can be assigned to a function pointer value 8 void actual_function(int arg) { /* ... */ } 9 func fptr = &actual_function;
typedef的局限是它不適用於模板,但是using支持創建類型別名,例如:
template<typename T> using ptr = T*; // the name 'ptr<T>' is now an alias for pointer to T ptr<int> ptr_int;
1.5 typename
- 在模板參數列表中,用於指定類型參數。(作用同class)
template <class T1, class T2>... template <typename T1, typename T2>...
- 用在模板定義中,用於標識“嵌套依賴類型名(nested dependent type name)”,即告訴編譯器未知標識符是一種類型。
這之前先解釋幾個概念:
> 依賴名稱(dependent name):模板中依賴於模板參數的名稱。
> 嵌套依賴名稱(nested dependent name):從屬名稱嵌套在一個類裡邊。嵌套從屬名稱是需要用typename聲明的。
template<class T> class X { typename T::Y m_y; // m_y依賴於模板參數T,所以m_y是依賴名稱;m_y同時又嵌套在X類中,所以m_y又是嵌套依賴名稱 };
上例中,m_y是嵌套依賴名稱,需要typename來告訴編譯器Y是一個類型名,而非變數或其他。否則在T成為已知之前,是沒有辦法知道T::Y到底是不是一個類型。
- typename可在模板聲明或定義中的任何位置使用任何類型。不允許在基類列表中使用該關鍵字,除非將它用作模板基類的模板自變數。
1 template <class T> 2 class C1 : typename T::InnerType // Error - typename not allowed. 3 {}; 4 template <class T> 5 class C2 : A<typename T::InnerType> // typename OK. 6 {};
1.6 template
C++提供了模板(template)編程的概念。所謂模板,實際上是建立一個通用函數或類,其類內部的類型和函數的形參類型不具體指定,用一個虛擬的類型來代表。這種通用的方式稱為模板。模板是泛型編程的基礎,泛型編程即以一種獨立於任何特定類型的方式編寫代碼。
1.6.1 函數模板
函數模板是通用的函數描述,也就是說,它們使用泛型來定義函數,其中的泛型可用具體的類型(如int或double)替換。通過將類型作為參數傳遞給模板,可使編譯器生成該類型的函數。由於模板允許以泛型方式編程,因此又被稱為通用編程。由於類型用參數表示,因此模板特性也被稱為參數化類型(parameterized types)。
請註意,模板並不創建任何函數,而只是告訴編譯器如何定義函數。一般如果需要多個將同一種演算法用於不同類型的函數,可使用模板。
(1)模板定義
template <typename T> // or template <class T> void f(T a, T b) {...}
在C++98添加關鍵字typename之前,用class來創建模板,二者在此作用相同。註意這裡class只是表明T是一個通用的類型說明符,在使用模板時,將使用實際的類型替換它。
(2)顯式具體化(explicit specialization)
- 對於給定的函數名,可以有非模板函數、模板函數和顯示具體化模板函數以及他們的重載版本。
- 他們的優先順序為:非模板 > 具體化 > 常規模板。
- 顯示具體化的原型與定義應以template<>打頭,並通過名稱來指出類型。
舉例如下:
1 #include <iostream> 2 3 // 常規模板 4 template <typename T> 5 void Swap(T &a, T &b); 6 7 struct job 8 { 9 char name[40]; 10 double salary; 11 int floor; 12 }; 13 14 // 顯示具體化 15 template <> void Swap<job>(job &j1, job &j2); 16 17 int main() 18 { 19 using namespace std; 20 cout.precision(2); // 保留兩位小數精度 21 cout.setf(ios::fixed, ios::floatfield); // fixed設置cout為定點輸出格式;floatfield設置輸出時按浮點格式,小數點後有6為數字 22 23 int i = 10, j = 20; 24 Swap(i, j); // 生成Swap的一個實例:void Swap(int &, int&) 25 cout << "i, j = " << i << ", " << j << ".\n"; 26 27 job sxx = { "sxx", 200, 4 }; 28 job xt = { "xt", 100, 3 }; 29 Swap(sxx, xt); // void Swap(job &, job &) 30 cout << sxx.name << ": " << sxx.salary << " on floor " << sxx.floor << endl; 31 cout << xt.name << ": " << xt.salary << " on floor " << xt.floor << endl; 32 33 return 0; 34 } 35 36 // 通用版本,交換兩個類型的內容,該類型可以是結構體 37 template <typename T> 38 void Swap(T &a, T &b) 39 { 40 T temp; 41 temp = a; 42 a = b; 43 b = temp; 44 } 45 46 // 顯示具體化,僅僅交換job結構的salary和floor成員,而不交換name成員 47 template <> void Swap<job>(job &j1, job &j2) 48 { 49 double t1; 50 int t2; 51 t1 = j1.salary; 52 j1.salary = j2.salary; 53 j2.salary = t1; 54 t2 = j1.floor; 55 j1.floor = j2.floor; 56 j2.floor = t2; 57 }
(3)實例化和具體化
- 隱式實例化(implicit instantiation):編譯器使用模板為特定類型生成函數定義時,得到的是模板實例。例如,上邊例子第23行,函數調用Swap(i, j)導致編譯器生成Swap()的一個實例,該實例使用int類型。模板並給函數定義,但使用int的模板實例就是函數定義。這種該實例化fangshi被稱為隱式實例化。
- 顯示實例化(explicit instantiation):可以直接命令編譯器創建特定的實例。語法規則是,聲明所需的種類(用<>符號指示類型),併在聲明前加上關鍵字template:
template void Swap<int>(int, int); // 該聲明的意思是“使用Swap()模板生成int類型的函數定義”
- 顯示具體化(explicit specialization):前邊以介紹,顯示具體化使用下麵兩個等價的聲明之一:
// 該聲明意思是:“不要使用Swap()模板來生成函數定義,而應使用專門為int類型顯示定義的函數定義” template <> void Swap<int>(int &, int &); template <> void Swap(int &, int &);
註意:顯示具體化的原型必須有自己的函數定義。
以上三種統稱為具體化(specialization)。下邊的代碼總結了上邊這些概念:
1 template <class T> 2 void Swap(T &, T &); // 模板原型 3 4 template <> void Swap<job>(job &, job &); // 顯示具體化 5 template void Swap<char>(char &, char &); // 顯式實例化 6 7 int main(void) 8 { 9 short a, b; 10 Swap(a, b); // 隱式實例化 11 12 job n, m; 13 Swap(n, m); // 使用顯示具體化 14 15 char g, h; 16 Swap(g, h); // 使用顯式模板實例化 17 }
編譯器會根據Swap()調用中實際使用的E參數,生成相應的版本。
當編譯器看到函數調用Swap(a, b)後,將生成Swap()的short版本,因為兩個參數都是short。當編譯器看到Swap(n, m)後,將使用為job類型提供的獨立定義(顯示具體化)。當編譯器看到Swap(g, h)後,將使用處理顯式實例化時生成的模板具體化。
(4)關鍵字decltype(C++11)
- 在編寫模板函數時,並非總能知道應在聲明中使用哪種類型,這種情況下可以使用decltype關鍵字:
template <class T1, Class T2> void ft(T1 x, T2 y) { decltype(x + y) xpy = x + y; // decltype使得xpy和x+y具有相同的類型 }
- 有的時候我們也不知道模板函數的返回類型,這種情況下顯然是不能使用decltype(x+y)來獲取返回類型,因為此時參數x和y還未聲明。為此,C++新增了一種聲明和定義函數的語法:
// 原型 double h(int x, float y); // 新增的語法 auto h(int x, float y) -> double;
該語法將返回參數移到了參數聲明的後面。->double被稱為後置返回類型(trailing return type)。其中auto是一個占位符,表示後置返回類型提供的類型。
所以在不知道模板函數的返回類型時,可使用這種語法:
template <class T1, Class T2> auto ft(T1 x, T2 y) -> decltype(x + y) { return x + y; }
1.6.2 類模板
(1)類模板定義和使用
1 // 類模板定義 2 template <typename T> // or template <class T> 3 class A 4 {...} 5 6 // 實例化 7 A<t> st; // 用具體類型t替換泛型標識符(或者稱為類型參數)T
程式中僅包含模板並不能生成模板類,必須要請求實例化。為此,需要聲明一個類型為模板類對象,方法是使用所需的具體類型替換泛型名。比如用來處理string對象的棧類,就是basic_string類模板的具體實現。
應註意:類模板必須顯示地提供所需的類型;而常規函數模板則不需要,因為編譯器可以根據函數的參數類型來確定要生成哪種函數。
(2)模板的具體化
類模板與函數模板很相似,也有隱式實例化、顯示實例化和顯示具體化,統稱為具體化(specialization)。模板以泛型的方式描述類,而具體化是使用具體的類型生成類聲明。
- 隱式實例化:聲明一個或多個對象,指出所需的類型,而編譯器使用通用模板提供的處方生成具體的類定義。需要註意的是,編譯器在需要對象之前,不會生成類的隱式實例化。
- 顯示實例化:使用關鍵字template並指出所需類型來聲明類時,編譯器將生成類聲明的顯示實例化。
1 // 類模板定義 2 template <class T, int n> 3 class ArrayTP 4 {...}; 5 6 // 隱式實例化(生成具體的類定義) 7 ArrayTP<int, 100> stuff 8 9 // 顯示實例化(將ArrayTP<string, 100>聲明為一個類) 10 template class ArrayTP<string, 100>;
- 顯示具體化:是特定類型(用於替換模板中的泛型)的定義。
- 部分具體化(partial specializaiton):即部分限制模板的通用性。
第一:部分具體化可以給類型參數之一指定具體的類型:
1 // 通用模板 2 template <typename T1, typename T2> class Pair {...}; 3 // 部分具體化模板(T1不變,T2具體化為int) 4 template <typename T1> class Pair<T1, int> {...}; 5 // 顯示具體化(T1和T2都具體化為int) 6 template <> calss Pair<int, int> {...};
如果有多種模板可供選擇,編譯器會使用具體化程度最高的模板(顯示 > 部分 > 通用),比如對上邊三種模板進行實例化:
Pair<double, double> p1; // 使用通用模板進行實例化 Pair<double, int> p2; // 使用Pair<T1, int>部分具體化模板進行實例化 Pair<int, int> p3; // 使用Pair<T1, T2>顯式具體化模板進行實例化
第二:也可以通過為指針提供特殊版本來部分具體化現有的模板:
// 通用模板 template <typename T> class Feeb {...}; // 指針部分具體化模板 template <typename T*> class Feeb {...};
編譯器會根據提供的類型是不是指針來選擇使用通用模板或者指針具體化模板:
Feeb<char> fb1; // 使用通用模板,T為char類型 Feeb<char *> fb2; //