主頁 > 後端開發 > 【C++】從設計原理來看string類

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

2022-07-18 06:28:21 後端開發

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++官方檔案

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/499506.html

標籤:C++

上一篇:第1章 預備知識

下一篇:JVM如何知道一個物件該回收了呢?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more