【C++】從設計原理來看string類

最菜程式設計師Sxx發表於2022-07-17

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;  // 使用Feeb T*具體化,T為char型別

  上述第二個宣告使用具體化模板,將T轉換為char型別;如果使用的是通用模板,則是將T轉換為char*型別。

(3)成員模板

  模板可作為結構、類或模板類的成員。下邊示例是將另一個模板類和模板函式作為該模板類的成員:

 1 #include <iostream>
 2 using std::cout;
 3 using std::endl;
 4 
 5 template <typename T>
 6 class beta
 7 {
 8 private:
 9     template <typename V>  // 巢狀的模板類成員(只能在beta類中訪問)
10     class hold
11     {
12     private:
13         V val;
14     public:
15         hold(V v = 0) :val(v) {}
16         void show() const { cout << val << endl; }
17         V value() const { return val; }
18     };
19     // beta類使用hold模板宣告兩個資料成員
20     hold<T> q;    // q是基於T型別(beta模板引數)的hold物件
21     hold<int> n;  // n是基於int的hold物件
22 public:
23     beta(T t, int i) :q(t), n(i) {}
24     template <typename U>  // 模板方法
25     U blab(U u, T t) { return  (n.value() + q.value()) * u / t; }
26     void Show() const { (q.show(); n.show(); }
27 };
28 
29 int main()
30 {
31     beta<double> guy(3.5, 3);
32     cout << "T was set to double\n";
33     guy.Show();
34     cout << "V was set to T, which id double, then V was set to int\n";
35     cout << guy.blab(10, 2.3) << endl;
36     cout << "U was set to int\n";
37     cout << guy.blab(10.0, 2.3) << endl;
38     cout << "U was set to double\n";
39     cout << "Done\n";
40     return 0;
41 }

(4)將模板用作引數

  模板可以包含型別引數(如typename T)和非型別引數(如int n),還可以包含本身就是模板的引數。格式如下:

template <template <typename T> class Type>
class B

  模板引數為template <typename T> classType,其中emplate <typename T> class是型別,Type是引數。

  示例如下:

 1 #include <iostream>
 2 #include "stacktp.h"
 3 template <template <typename T> class Thing>
 4 class Crab
 5 {
 6 private:
 7     Thing<int> s1;
 8     Thing<double> s2;
 9 public:
10     Crab() {};
11     // 假設pop()和push()是Thing類的成員函式
12     bool push(int a, double x) { return s1.push(a) && s2.push(x); }
13     bool pop(int& a, double& x) { return s1.pop(a) && s2.pop(x); }
14 };
15 
16 int main()
17 {
18     using std::cout;
19     using std::cin;
20     using std::endl;
21 
22     Crab<Stack> nebula;
23     int ni;
24     double nb;
25     while (cin >> ni >> nb && ni > 0 && nb > 0)
26     {
27         if (!nebula.push(ni, nb))
28             break;
29     }
30     while (nebula.pop(ni, nb))
31         cout << ni << ", " << nb << endl;
32     cout << "Done.\n";
33     return 0;
34 }

1.7 迭代器

  模板和迭代器都是STL通用方法的重要組成部分,模板使演算法獨立於儲存的資料型別,而迭代器使演算法獨立於使用的容器型別。

  迭代器是一個物件,可以迴圈C++標準庫容器中的元素,並提供對單個元素的訪問。C++標準庫容器全都提供有迭代器,因此演算法可以採用標準方式訪問元素,而不必考慮用於儲存元素的容器型別。簡言之,迭代器就是為訪問容器所提供的STL通用演算法的統一介面,每個容器類都定義了相應的迭代器型別,通過迭代器就能夠實現對容器中的元素進行訪問和操作。

  可以使用成員和全域性函式(例如begin()和end())顯式使用迭代器,例如 ++ 和 -- 分別用於向前和向後移動。 還可以將迭代器隱式用於範圍 for 迴圈或 (某些迭代器型別) 下標運算子 []

  在 C++ 標準庫中,序列或範圍的開頭是第一個元素。 序列或範圍的末尾始終定義為最後一個元素的下一個位置。 全域性函式begin和end將迭代器返回到指定的容器。 典型的顯式迭代器迴圈訪問容器中的所有元素,如下所示:

vector<int> vec{ 0,1,2,3,4 };
for (auto it = begin(vec); it != end(vec); it++)
{
    // 使用解除引用運算子訪問元素
    cout << *it << " ";
}

  也可使用C++11新增的基於範圍的for迴圈完成相同操作:

for (auto num : vec)
{
    // 不使用解除引用運算子
    cout << num << " ";
}

  STL中定義了5種型別的迭代器:輸入迭代器、輸出迭代器、正向迭代器、雙向迭代器和隨機訪問迭代器。

1.8 行內函數

  行內函數是C++為提高程式執行速度所做的一項改進。常規函式和行內函數之間的主要區別不在於編寫方式,而在於C++編譯器如何將它們組合到程式中。

  我們先來詳細看下常規函式的呼叫過程:

  • 執行到函式呼叫指令時,程式將在函式呼叫後立即儲存該指令的記憶體地址,並將函式引數複製到堆疊(為此保留的記憶體塊),跳到標記函式起點的記憶體單元,執行函式程式碼(也許還需將返回值放入到暫存器中),然後跳回到地址被儲存的指令處。

  行內函數的編譯程式碼與其他程式程式碼“內聯”起來了,即編譯器將使用相應的函式程式碼替換函式呼叫。對於內聯程式碼,程式無需跳到另一個位置處執行程式碼,再跳回來。因此,行內函數執行速度比常規函式稍快,但代價是需要佔用更多的記憶體。所以要權衡實際情況再選擇是否使用行內函數。

  下圖很直觀的給出了行內函數與常規函式的區別:

  行內函數使用方式很簡單,只需要在函式宣告和函式定義前加上inline關鍵字即可。使用行內函數需要注意以下幾點:

  • 行內函數會增大記憶體佔用,但是不需要承擔函式呼叫時的壓棧、跳轉、返回的時間和資源開銷,所以短小的函式程式碼片段(儘量不要超過10行)建議使用內聯。
  • 謹慎對待解構函式,解構函式往往比其表面看起來更長,因為有隱含的成員和基類解構函式被呼叫。
  • 避免將遞迴函式宣告為行內函數。遞迴呼叫堆疊的展開並不像迴圈那麼簡單,比如遞迴層數在編譯時可能是未知的。
  • 內聯inline關鍵字是對編譯器採用內聯編譯的請求,編譯器有可能拒絕或忽略(此時就會當成普通函式編譯)。
  • 有些情況下即便沒有inline關鍵字,但編譯器也會視情況採用內聯(優化)編譯。

內聯與巨集

  行內函數是將語句封裝成函式,而巨集定義替換隻是文字替換,沒有經歷系統完整的運算規則的規劃,有一定異變性。比如:

#define SQUARE(X) X*X
a = SQUARE(2 + 3)    // 相當於 a = 2 + 3 * 2 + 3

1.9 運算子過載

  運算子過載是一種形式的C++多型。我們可以定義多個函式名相同但特徵標(引數列表)不同的函式,稱之為函式過載(或函式多型)。運算子過載將過載的概念擴充套件到運算子上,允許賦予C++運算子更多的含義。

  過載運算子需要使用運算子函式,格式如下:

  operatorop(argument-list)

  其中:op代表運算子,且必須是有效的C++運算子。例如,operator+()表示過載+運算子,operator*()表示過載*運算子。 

  那具體應該怎麼在程式中使用呢?我們來結合下邊的例子進行說明:

  1 class Time
  2 {
  3 private:
  4     int hours;
  5     int minutes;
  6 public:
  7     Time();
  8     Time(int h, int m = 0);
  9     void AddMin(int m);
 10     void AddHr(int h);
 11     void Reset(int h = 0, int m = 0);
 12     Time Sum(const Time& t) const;          // Sum函式
 13     Time operator+ (const Time & t) const;  // 使用過載的加法運算子
 14     void Show() const;
 15 };
 16 
 17 Time::Time()
 18 {
 19     hours = minutes = 0;
 20 }
 21 
 22 Time::Time(int h, int m)
 23 {
 24     hours = h;
 25     minutes = m;
 26 }
 27 
 28 void Time::AddMin(int m)
 29 {
 30     minutes += m;
 31     hours += minutes / 60;
 32     minutes %= 60;
 33 }
 34 
 35 void Time::AddHr(int h)
 36 {
 37     hours += h;
 38 }
 39 
 40 void Time::Reset(int h, int m)
 41 {
 42     hours = h;
 43     minutes = m;
 44 }
 45 
 46 // 注意:不要返回指向區域性變數或臨時物件的引用。函式執行完畢後,區域性變數和臨時物件將消失,引用將指向不存在的資料。
 47 Time Time::Sum(const Time& t) const
 48 {
 49     Time sum;
 50     sum.minutes = minutes + t.minutes;
 51     sum.hours = hours + t.hours + t.minutes / 60;
 52     sum.minutes %= 60;
 53     return sum;
 54 }
 55 
 56 Time Time::operator+(const Time& t) const
 57 {
 58     Time sum;
 59     sum.minutes = minutes + t.minutes;
 60     sum.hours = hours + t.hours + t.minutes / 60;
 61     sum.minutes %= 60;
 62     return sum;
 63 }
 64 
 65 void Time::Show() const
 66 {
 67     std::cout << hours << " hours, " << minutes << " minutes";
 68 }
 69 
 70 int main()
 71 {
 72     using std::cout;
 73     using std::endl;
 74     Time planning;
 75     Time coding(2, 40);
 76     Time fixing(5, 55);
 77     Time total;
 78 
 79     cout << "planning time = ";
 80     planning.Show();
 81     cout << endl;
 82 
 83     cout << "coding time = ";
 84     coding.Show();
 85     cout << endl;
 86 
 87     cout << "fixing time = ";
 88     fixing.Show();
 89     cout << endl;
 90 
 91     // Sum函式
 92     total = coding.Sum(fixing);
 93     cout << "coding.sum(fixing) = ";
 94     total.Show();
 95     cout << endl;
 96 
 97     // 使用過載運算子
 98     total = coding + fixing;  // 與下一句程式碼等效
 99     total = coding.operator+(fixing); 
100     cout << "coding + fixing = ";
101     total.Show();
102     cout << endl;
103 
104     return 0;
105 }

  同Sum()一樣,opertor+()也是由Time物件呼叫的,它將第二個Time物件作為引數,並返回一個Time物件。呼叫operator+()可使用以下兩種等效的方式:

total = coding + fixing; 
total = coding.operator+(fixing); 

  operator+()函式的名稱使得可以使用函式表示法或運算子表示法來呼叫它。需要注意的是,在運算子表示法中,運算子左側的物件是呼叫物件,運算子右邊的物件是作為引數被傳遞的物件。

過載限制

(1)過載後的運算子必須至有一個運算元是使用者定義的型別,這將防止使用者為標準型別過載運算子。比如不能將減法運算子(-)過載為兩個double型別的和。

(2)使用運算子時不能違反運算子原來的句法規則。也不能修改運算子的優先順序。

(3)不能建立新的運算子,例如,不能定義ooperator**()函式來表示求冪。

(4)不能過載下面的運算子:

sizeof sizeof運算子
. 成員運算子
.* 成員指標運算子
:: 作用域解析運算子
?: 條件運算子
# 預處理命令:轉換為字串
## 預處理命令:拼接
typeid 一個RTTI運算子
const_cast 強制型別轉換運算子
dynamic_cast 強制型別轉換運算子
reinterpret_cast 強制型別轉換運算子
static_cast 強制型別轉換運算子

(5)下邊運算子只能通過成員函式進行過載

= 賦值運算子
() 函式呼叫運算子
[] 下標運算子
-> 函式指標訪問類成員的運算子

附:可過載的運算子: 

+ - * / % ^
& | ~= ! = <
> += -= *= /= %=
^= &= |= << >> >>=
<<= == != <= >= &&
|| ++ -- , ->* ->
() [] new delete new[] delete[]

->* 和 -> 用法示例:

1 void (Test::*pfunc)() = &Test::func;
2 Test t1;
3 Test* t2 = new Test();
4 
5 t1.func();        // 正常呼叫類成員方法
6 (t1.*pfunc)();    // 通過函式指標呼叫類成員方法
7 
8 t2->func();       // 正常呼叫類成員方法
9 (t2->*pfunc)();   // 通過函式指標呼叫類成員方法

1.10 友元

  通常我們只能通過公有類方法來訪問類物件的私有部分,這種限制有時顯得過於嚴格,以致不適合特定的程式設計問題。C++提供了另一種形式的訪問許可權:友元。友元有三種:友元函式、友元類和友元成員函式,通過讓他們成為類的友元,可以賦予他們與類的成員函式相同的訪問許可權。 

  哪些函式、成員函式或類為友元是由類定義的,所以儘管友元被授予從外部訪問類的私有部分的許可權,但它們並不與OOP的思想相悖。友元宣告可以位於公有、私有或保護部分,其所在的位置無關緊要。

1.10.1 友元函式

  友元函式是提供特殊訪問特權的常規外部函式,它不是類的成員,但是其訪問許可權與成員函式相同,有權訪問該類的私有成員和受保護成員。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 class Point
 5 {
 6     friend void ChangePrivate(Point&);
 7 public:
 8     Point(void) : m_i(0) {}  // 建構函式成員初始化列表
 9     void PrintPrivate(void) { cout << m_i << endl; }
10 
11 private:
12     int m_i;
13 };
14 
15 void ChangePrivate(Point& i) { i.m_i++; }
16 
17 int main()
18 {
19     Point sPoint;
20     sPoint.PrintPrivate();
21     ChangePrivate(sPoint);
22     sPoint.PrintPrivate();
23     // Output: 0
24     //         1
25 }

1.10.2 友元類

  將類作為友元,那麼友元類的所有方法都可以訪問原始類的私有成員和保護成員。用法和友元函式類似,不多做介紹。

1.10.3 友元成員函式

  很多時候,並不是友元類中所有的成員都需要訪問原始類的私有成員或受保護成員。這種情況下可以將類的成員宣告為原始類的友元。比如:

 1 Class Tv;  // 前向宣告,避免迴圈依賴
 2 
 3 class Remote
 4 {
 5 public:
 6     void set_chan(Tv & t, int c) {t.channel = c;}  // 行內函數
 7     ...
 8 };
 9 
10 Class Tv
11 {
12 private:
13    int channel;
14 public:
15     friend void Remote::set_chan(Tv & t, int c);
16     ...
17 };  

  注:上例中,Tv中包含了Remote的定義,這意味著Remot要定義在Tv之前;Remote的方法也提到了Tv物件,這意味著Tv要定義在Remote之前。為了避開這種迴圈依賴,可以在Remote定義之前加上 “class Tv;” 作為前向宣告(forward declaration)。

1.10.4 彼此互為友元

  有的時候,會要求A類和B類能夠進行互動,即A類的某些方法能夠影響B類的物件,B類的某些方法也能影響A類的物件。這可以讓類彼此稱為對方的友元來實現,示例如下:

 1 Class Tv
 2 {
 3     friend class Remote;
 4 public:
 5     void buzz(Remote & r);
 6     ...
 7 };
 8 
 9 class Remote
10 {
11     friend class Tv;
12 public:
13     void Bool volup(Tv & t) { t.volup(); }
14     ...
15 };
16 
17 inline void Tv::buzz(Remote & r)
18 {
19     ...
20 }

1.10.5 共同的友元

  存在另一種情況,函式需要訪問兩個(或多個)類的私有資料。邏輯上來講,該函式應該是兩個(或多個)類的成員函式,顯然這是不可能的。此時比較合理的方式就是將該函式作為兩個(或多個)類的友元。示例如下:

 1 class B;  // 前向宣告(forward declaration)
 2 class A
 3 {
 4     friend void sync(B& a, const A& p);  // sync a to p
 5     friend void sync(A& P, const B& a);  // sync p to a
 6 };
 7 class B
 8 {
 9     friend void sync(B& a, const A& p);  // sync a to p
10     friend void sync(A& P, const B& a);  // sync p to a
11 };
12 
13 // 友元函式定義
14 inline void sync(B& a, const A& p)
15 {
16     ...
17 }
18 inline void sync(A& P, const B& a)
19 {
20     ...
21 }

1.10.6 使用友元需注意的幾點

  • 友元類的關係是單向的。除非明確指定,否則友元不是相互的。
  • 在C++/CLI中,託管類不能有任何友元函式、友元類或者友元介面。
  • 友元不能是虛擬函式,因為友元不是類成員,而只有成員才能是虛擬函式。
  • 友元是不能繼承的,比如B是A的友元類,C又是B的友元類,意味著B可以訪問A的私有成員,且C可以訪問B的私有成員,但是C卻沒有許可權訪問A的私有成員。引用官方文件裡邊的一張圖:

  圖中有四種類宣告:Base,Derived,aFriend、anotherFriend。僅有aFriend類能夠直接訪問Base類(以及Base可能已繼承的所有成員)的私有成員。

2. char型別:字元和小整數

  char型別是整形的一種。用來處理字元和比short更小的整形。ASCII碼共128個字元,用char型別表示完全足夠,而像Unicode這種大型字符集,C++也支援用寬字元型別wchar_t來表示。

2.1 成員函式cout.put()

  cout是ostream類的特定物件,put()是ostream的成員函式。只能通過類的特定物件來使用成員函式,所以cout.put()表示通過類物件cout來使用函式oput(),其中句點“.”稱為成員運算子。

  cout.put() 提供了一種顯示字元的方法,可以替代<<運算子。char型別和cout.put()用法示例如下:

 1 #include <iostream>
 2 
 3 int main()
 4 {
 5     using namespace std;
 6     char ch = 'M';
 7     int i = ch;
 8     cout << "The ASCII code for " << ch << " is " << i << endl;
 9 
10     ch += 1;
11     i = ch;
12     cout << "The ASCII code for " << ch << " is " << i << endl;
13 
14     cout << "Displaying char ch using cout.put(ch): ";
15     cout.put(ch);
16 
17     cout.put('!');
18     return 0;
19 }

輸出:

2.2 char字面值

  將字元用單引號''括起來。比如'A'表示65,即字元A的ASCII碼。

  轉義字元:

字元名稱 ASCII符號 C++程式碼 十進位制ASCII碼 十六進位制ASCII碼
換行符 NL(LF) \n 10 0xA
水平製表符 HT \t 9 0x9
垂直製表符 VT \v 11 0xB
退格 BS \b 8 0c8
回車 CR \r 13 0xD
振鈴 BEL \a 7 0x7
反斜槓 \ \\ 92 0x5C
問號 ? \? 63 0x3F
單引號 ' \' 39 0x27
雙引號 " \" 34 0x22

  轉義字元作為字元常量時,應用單引號括起來;將它們放在字串中時,不要使用單引號。

  換行符可代替endl,下邊三行程式碼都起到換行作用:

cout << endl;
cout << '\n';
cout << "\n";   // 相當於'\n'在字串中,不需要加單引號

2.3 signed char和unsigned char

  char在預設情況下既不是有符號,也不是無符號,是否有符號由C++實現決定。signed char表示範圍為-128~127,unsigned char表示範圍為0~255。

2.4 wchar_t

  像漢字日文等,無法用一個8位的位元組l來表示,這種情況,C++一般有兩種處理方式:

  (1)將char定義為一個16位或者更長的位元組;

  (2)用8位的char來表示基本字符集,另外使用wchar_t(寬字元型別)表示擴充套件字符集。wchar_t流的輸入輸出工具為wcin和wcout。可通過加字首L來指示寬字元常量和寬字串。

wchar_t blb = L'P'
wcout << L"tall" << endl;

2.5 char16_t和char32_t(C++11新增)

  char16_T和char32_t都是無符號的,前者長16位,後者長32位。通常使用字首u表示char16_t字元常量和字串常量;用字首U表示char32_t字元常量和字串常量。

3. 模板類string

  至此,string類中涉及到的一些C++基礎知識也介紹的差不多了,接下來我們就來看看string類到底是如何設計的。

3.1 string類設計原理

  • 第一步:basic_string模板類的定義(該模板類中還定義了的一些型別、迭代器和靜態常量,便於後續定義方法,以及將STL的演算法用於字串)
 1 #define _STD ::std::
 2 
 3 template <class _Elem,                   // _Elem是儲存在字串中的型別
 4     class _Traits = char_traits<_Elem>,  // _Traits引數是一個類,定義了型別要被表示為字串時,所必須具備的特徵
 5     class _Alloc = allocator<_Elem>>     // _Alloc是用於處理字串記憶體分配的類
 6     class basic_string
 7 {                                        // null-terminated transparent array of elements
 8 private:
 9     friend _Tidy_deallocate_guard<basic_string>;
10     friend basic_stringbuf<_Elem, _Traits, _Alloc>;
11 
12     using _Alty = _Rebind_alloc_t<_Alloc, _Elem>;
13     using _Alty_traits = allocator_traits<_Alty>;
14 
15     ... 
16 
17 public:
18     // using源於C++11,等價於C++98的typedef,比如下邊第一行程式碼相當於:typedef _Traits traits_type;
19     using traits_type = _Traits;                                     // _Traits是對應於特定型別(如char_traits<char>)的模板引數
20     using allocator_type = _Alloc;
21 
22     using value_type = _Elem;
23     using size_type = typename _Alty_traits::size_type;              // 根據儲存的型別返回字串的長度(無符號型別)
24     using difference_type = typename _Alty_traits::difference_type;  // 用於度量字串中兩個元素之間的距離(size_type有符號版本)
25     using pointer = typename _Alty_traits::pointer;                  // 對於char具體化,pointer型別為char *,與基本指標有著相同的特徵
26     using const_pointer = typename _Alty_traits::const_pointer;
27     using reference = value_type&;                                   // 對於char具體化,reference型別為char &,與基本引用有著相同的特徵
28     using const_reference = const value_type&;
29 
30     using iterator = _String_iterator<_Scary_val>;                   // 迭代器型別
31     using const_iterator = _String_const_iterator<_Scary_val>;
32 
33     using reverse_iterator = _STD reverse_iterator<iterator>;
34     using const_reverse_iterator = _STD reverse_iterator<const_iterator>;
35 
36     ...
37 
38     // 靜態常量npos,size_type是無符號的,因此將-1賦給npos相當於將最大的無符號值賦給它,這個值比可能的最大陣列索引大1
39     static constexpr auto npos{ static_cast<size_type>(-1) };  
40 };

  上邊定義中用到了另外兩個模板類char_traits和allocator,前者又稱字元特性模板類,提供最基本的字元特性的統一的方法函式;後者是C++標準庫容器都具有一個預設的模板引數,通過使用自定義分配器構造容器可控制該容器的元素的分配和釋放。

  • 第二步:basic_string類别範本的具體化(string類的由來)

  即編譯器根據所需的型別,使用basic_string類别範本提供的處方生成具體的類定義,我們常用的string類便是這麼來的。可以看到,basic_string類别範本在具體化的過程中還使用了char_traits類别範本和allocator類别範本的具體化。而就具體化種類(隱式例項化、顯示例項化、顯示具體化)而言,這裡的具體化應屬於隱式例項化。string類就相當於basic_string類别範本關於char型別的隱式例項化。

using string = basic_string<char, char_traits<char>, allocator<char>>;
using wstring = basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t>>;
using u16string = basic_string<char16_t, char_traits<char16_t>, allocator<char16_t>>;
using u32string = basic_string<char32_t, char_traits<char32_t>, allocator<char32_t>>;
  • 第三步:string類的使用

  string本質上就是basic_string類的char版本,basic_string怎麼用string就怎麼用。

3.2 string類的建構函式(ctor)

  string實際上是模板具體化basic_string<char>的一個typedef,同時省略了與記憶體管理相關的引數。

建構函式 描    述
string(const char * s) 將string物件初始化為s指向的NBTS。NBTS(null-terminated string)表示以空字元結束的字串——傳統的C字串。
string(size_type n, char c) 建立一個包含n個元素的string物件,其中每個元素都被初始化為字元c。size_type是一個依賴於實現的整形,在string中定義。
string(const string & str) 將一個string物件初始化為string物件str(複製建構函式)。
string() 建立一個預設的string物件,長度為0(預設建構函式)。
string(const char * s, size_type n) 將string物件初始化為s指向的NBTS的前n個字元,即使超過了NBTS結尾。

template<class Iter>

string(Iter begin, Iter end)

將string物件初始化為區間[begin, end)內的字元,其中begin和end的行為就像指標,用於指定位置,範圍包括begin,但不包括end。

string(const string & str,

string size_type pos = 0, suze_type n = npos)

將一個string物件初始化為物件str中從位置pos開始到結尾的字元,或從位置pos開始的n個字元。string類將string::npos定義為字串的最大長度,通常為 unsigned int 的最大值。
string(string && str) noexcept C++11新增,將一個string物件初始化為string物件str,並可能修改str(移動建構函式)。
string(initializer_list<char> il) C++11新增,將一個string物件初始化為初始化列表il中的字元。

  建構函式用法示例如下:

 1 #include <string>
 2 #include <iostream>
 3 
 4 int main()
 5 {
 6     using namespace std;
 7     
 8     string one("SXX Winner!");    // ctor 1,將string物件one初始化為常規的C-風格字串
 9     cout << one << endl;
10 
11     string two(20, '$');    // ctor 2,將string物件two初始化為由20個$字元組成的字串
12     cout << two << endl;
13 
14     string three(one);    // ctor 3,複製建構函式將string物件three初始化為one
15     cout << three << endl;
16 
17     one += " Oops!";
18     cout << one << endl;
19 
20     two = "Sorry! That was ";
21     three[0] = 'Y';
22     string four;    // ctor 4,預設建構函式建立一個以後可以對其進行賦值的空字串
23     four = two + three;
24     cout << four << endl;
25     
26     char alls[] = "All's well that ends well";
27     string five(alls, 20);    // ctor 5, 將five初始化為alls的前20個字元
28     cout << five << "!\n";
29 
30     string six(alls + 6, alls + 10);
31     cout << six << ", ";    // ctor 6,將string物件six初始化為區間[6,10)內的字元
32     
33     string seven(&five[6], &five[10]);    // ctor 6, five[6]是一個char值,&five[6]是地址
34     cout << seven << "...\n";
35 
36     string eight(four, 7, 16);    // ctor 7, 將物件four的部分內容複製到構造的物件中
37     cout << eight << " in motion!" << endl;
38 
39     return 0;
40 }

  對於ctor 5,當n超過了C-風格字串的長度,仍將複製請求數目的字元。比如上邊的例子中,如果用40代替20,將導致15個無用字元被複制到five的結尾處(即建構函式將記憶體中位於字串“All's well that ends well”後面的內容作為字元)。

  第33行程式碼如果寫成 string seven(five + 6, five + 10) 是沒有意義的,物件名(不同於陣列名)不會被看作是物件的地址,因此five不是指標,five + 6也沒有意義;而five[6]是一個char值,所以&five[6]是地址。

3.3 string類常用成員函式

方    法 返   回   值
begin()  指向字串第一個字元的迭代器(如下圖示)
cbegin()  一個const_iterator,指向字串的第一個字元(C++11),作用和begin()類似,不同之處在於begin()可改元素值,cbegin()不可改
end()  超尾值的迭代器,注意它返回的不是指向字串最後一個字元的迭代器,而是指向字串最後一個字元的下一位置(稱為超尾值)的迭代器
cend()  為超尾值的const_iterator(C++11)
rbegin()  超尾值的反轉迭代器,即指向字串最後一個字元的迭代器
crbegin()  為超尾值的反轉const_iterator(C++11)
rend()  指向第一個字元的反轉迭代器,也就是指向第一個字元的前一位置的迭代器
crend()  指向第一個字元的反轉const_iterator(C++11)
size()  字串中的元素數,等於begin()到end()之間的距離
length()  同size()。length()成員來自較早版本的string類,而size()則是為提供STL相容性而新增的
capacity()  給字串分配的元素數。這可能大於實際的字元數,capacity() - size()的值表示在字串末尾附加多少字元後需要分配更多的記憶體
reverse() 為字串請求記憶體塊,分配空間。很多C++實現分配一個比實際字串大的記憶體塊,為字串提供增大空間。如果字串不斷增大,超過了分配給它的記憶體塊大小,程式將分配一個大小為原來兩倍的新記憶體塊,以提供足夠的增大空間,避免不斷地分配新的記憶體塊而導致的效率低下。
max_size()   字串的最大長度
data() 一個指向陣列第一個元素的const charT*指標,其第一個size()元素等於*this控制的字串中對應的元素,其下一個元素為charT型別的charT(0)字元(字串末尾標記)。當string物件本身被修改後,該指標可能無效
c_str()   一個指向陣列第一個元素的const charT*指標,其第一個size()元素等於*this控制的字串中對應的元素,其下一個元素為charT型別的charT(0)字元(字串尾標記)。當string物件本身被修改後,該指標可能無效
get_allocator()  用於為字串object分配記憶體的allocator物件的副本

  有些方法用來處理記憶體,如清除記憶體的內容,調整字串長度和容量。

方   法 作    用
void resize(size_type n) 如果n>npos,將引發out_of_range異常;否則,將字串的長度改為n,如果n<size(),則截短字串,如果n>size(),則使用charT(0)中的字元填充字串
void resize(size_type n, charT c) 如果n>npos,將引發out_of_range異常;否則,將字串的長度改為n,如果n<size(),則截短字串,如果n>size(),則使用字元c填充字串
void reverse(size_type res_arg = 0) 將capacity()設定為大於或等於res_arg。由於這將重新分配字串,因此以前的引用、迭代器和指標將無效。
void shrink_to_fit() 請求讓capacity()的值和size()相同(C++11新增)
void clear() noexcept 刪除字串中所有的字元
bool empty() const noexcept 如果size()==0,則返回true

3.4 字串存取

  有4種方法可以訪問各個字元,其中兩種使用[]運演算法,另外兩種使用at()方法:

1 // 能夠使用陣列表示法來訪問字串的元素,可用於檢索或更改值
2 reference operator[](size_type pos);
3 // 可用於const物件,但只能用來檢索值
4 const_reference operator[](size_type pos) const;
5 // 功能同第一句程式碼,索引通過函式引數提供
6 reference at(size_type n);
7 // 功能同第二句程式碼,索引通過函式引數提供
8 const_reference at(size_type n) const;

  at()方法執行邊界檢查,超界會引發out_of_range異常。operator[]()方法不進行邊界檢查。因此,可以根據安全性(使用at()檢測異常)和執行速度(使用陣列表示)選擇合適的方法。

3.5 字串搜尋

  string類提供6種搜尋函式,每個函式都有4個原型(過載)

(1)find():用於在字串中搜尋給定的子字串或字元。有4種過載的find()方法:

  • size_type find(const string & str, size_type pos = 0) const noexcept:從字串的pos位置開始,查詢子字串str。如果找到,則返回該子字串首次出現時其首字元的索引;否則,返回string:npos
  • size_type find(const char * s, size_type pos = 0) const :從字串的pos位置開始,查詢子字串s。如果找到,則返回該子字串首次出現時其首字元的索引;否則,返回string:npos
  • size_type find(const char * s, size_type pos = 0, size_type n) const :從字串的pos位置開始,查詢s的前n個字元組成的子字串。如果找到,則返回該子字串首次出現時其首字元的索引;否則,返回string:npos
  • size_type find(char ch, size_type pos = 0) const :從字串的pos位置開始,查詢字元ch。如果找到,則返回該子字串首次出現時其首字元的索引;否則,返回string:npos

(2)rfind():查詢子字串或字元最後一次出現的位置。

(3)find_first_of():在字串中查詢引數中任何一個字元首次出現的位置。

string snake("cobra");
int where = snake.find_first_of("hark")  // 返回r在“cobra”中的位置(即索引3)

(4)find_last_of():在字串中查詢引數中任何一個字元最後一次出現的位置。

string snake("cobra");
int where = snake.find_last_of("hark")  // 返回a在“cobra”中的位置(即索引4)

(5)find_first_not_of():在字串中查詢第一個不包含在引數中的字元。

string snake("cobra");
int where = snake.find_first_not_of("hark")  // 返回c在“cobra”中的位置(即索引0),因為“hark”中沒有c

(6)find_last_not_of():在字串中查詢最後一個不包含在引數中的字元。

string snake("cobra");
int where = snake.find_first_not_of("hark")  // 返回b在“cobra”中的位置(即索引2),因為“hark”中沒有b

3.6 字串修改方法

(1)過載的+=運算子或append():將一個字串追加到另一個字串的後面。

(2)assign():該方法使得能夠將整個字串、字串的一部分或由相同字元組成的字元序列賦給string物件。原型之一如下

basic_string& assign(const basic_string& str)

(3)insert():將string物件、字串陣列或幾個字元插入到string物件中。

(4)erase():從字串中刪除字元;pop_back():刪除字串中的最後一個字元

(5)replace():替換字串中指定的內容

(6)copy():將string物件或其中的一部分複製到指定的字串陣列中;swap():使用一個時間恆定的演算法來交換兩個string物件的內容

// copy()原型,s指向目標陣列,n是要複製的字串,pos指從string物件的什麼位置開始複製
size_type copy(charT* s, size_type n, size_type pos = 0) const;

// swap()原型
void swap(basic_string& str);

  注:copy()方法不追加空值字元,也不檢查目標陣列的長度是否足夠。

3.7 string類輸入

  對於C-風格字串,有3中輸入方式:

char info[100];
cin >> info;             // read a word                   
cin.getline(info, 100);  // read a line, discard \n     
cin.get(info, 100);      // read a line, leave \n in queue

  對於string物件,有兩種方式:

string stuff;
cin >> stuff;          // read a word                   
getline(cin, stuff);   // read a line, discard \n 

  兩個版本的getline()都有一個可選引數,用於指定使用哪個字元來確定輸入的邊界:

cin.getline(info, 100, ':');  // read up to :, discard :
getline(cin, stuff, ':');     // read up to :, discard : 

  兩者之間的主要區別在於,string版本的getline()將自動調整目標string物件的大小,使之剛好能夠儲存輸入的字元;

  另一個區別是,讀取C-風格字串的函式是istream類的方法,而string版本是獨立的函式。這就是C-風格字串輸入,cin是呼叫物件;而對於string物件輸入,cin是一個函式引數的原因。  

參考資料

1. C++ typedef用法詳解

2. C++ #define,typedef,using用法區別

3. C++ typedef的詳細用法

4. C++11:using 的各種作用

5. C++ | 指向類成員變數的指標 ( .* 運算子 與 ->* 運算子)

6. C++自定義迭代器(STL自定義迭代器)的實現詳解

7. 【C++】STL常用容器總結之十二:string類

8. 《C++ Primer Plus》相關

9. C++官方文件

相關文章