1. 程式人生 > 其它 >《C++ Primer》【Chapter 3】

《C++ Primer》【Chapter 3】

chapter3 字串、向量和陣列

using

using 有一個更細的用法就是直接指明名稱空間中的名字

//using namespace::name;
using std::cin;

一個要注意的點是:標頭檔案中不應包含using宣告
因為標頭檔案的內容會拷貝到所有引用它的檔案中去,如果標頭檔案裡有某個using宣告,那麼每個使用了該標頭檔案的檔案都會有這個宣告,這樣可能會在不經意間產生命名衝突。

string-可變長的字元序列

定義和初始化除了常用的以外,還可以通過指定數目生成連續的字串。

string s(10, 'c');

拷貝初始化和直接初始化

string s1;
string s2(s1);
string s2 = s1;
string s3("value");
string s3 = "value";
string s4(n, 'c');
string s5 = "hiya";             //使用等號一般都是拷貝
string s6("hello");             //直接
string s7(10, 'c');             //直接
string s8 = string(10, 'c');    //拷貝

操作

操作 作用
os<<s 將s寫到輸出流os中,返回os, eg:cout<<s
is>>s 從is中讀取字串賦給s,字串以空白分隔,返回is, eg:cin>>s
getline(is, s) 從is中讀取字串賦給s,字串以空白分隔,返回is, eg:getline(cin, s)
s.empty() 判空
s.size() 返回s中字元的個數
s[n] 取第n個字元的引用,n從0算起
s1+s2 字串拼接
s1=s2 用s2的副本拷貝給s1
s1!=s2 比較兩個串,逐字元比較,對大小寫敏感
<,<=,=>,> 根據字典序比較,對大小寫敏感

對於cin而言,讀取字串給string時,會忽略掉開頭的空白,字串的讀入結束也是空白,當需要讀入空白時,則需要使用getline, getline遇到換行結束

一些字元函式

操作 作用
isalnum(c) 當c是字母或陣列時為真
isalpha(c) 當c是字母時為真
iscntrl(c) 當c是控制字元時為真
isdigit(c) 當c是數字時為真
isgraph(c) 當c不是空格但可以列印時為真
islower(c) 當c是小寫時為真
isprint(c) 當c是可列印字元時為真(即c為空格,或具有可視形式)
ispunct(c) 當c是標點符號時為真(即c不是控制字元、數字、字母、可列印空白中的一種)
isspace(c) 當c是空白時為真(即c是空格、橫向製表符、縱向製表符、回車符、換行符、進紙符中的一種)
isupper(c) 當c是大寫字元時為真
isxdigit(c) 當c是十六進位制數字時為真,更直白的理解是不是(01,af,A~F)中的字元
tolower(c) 如果c是大寫字母,返回小寫;否則原樣返回
toupper(c) 如果c是小寫字母,返回大寫;否則原樣返回

string::size_type

配套型別size_type體現了標準庫型別與機器無關的特性,在具體使用的時候,通過作用域操作符來表明名字size_type是在類string中定義的。即string::size_type。它肯定是無符號的型別,所以儘量不要用int去定義s.size(),這樣可能會帶來問題

string s = "ddd"
unsigned len = s.size();
string s = "dwdadwwadaw";
for(string::size_type i = 0; i < s.size(); i++) {
    s[i] = toupper(s[i]);
}
cout << s << endl;

string的+操作必須保證字串字面值兩邊至少有一個string

原因:為了與C相容,C++中string和字串字面值是不同的型別!

vector

定義和初始化

用等號去賦值vector時,是拷貝,不是引用操作!

vector<int> vec = {0,1,2,3,4};  //列表初始化方法
vector<int> b = vec;            //拷貝
vec[4] = 111;       //b[4]並沒有改變

當使用花括號時,會有不同的情況,可能是列表初始值,也可能是元素數量

使用vector指定元素數目初始化時要滿足以下兩個條件:

  • 類必須要有明確的初始值或者有預設初始化函式
  • 只提供了元素的數量而沒有設定初始值,只能使用直接初始化

除非是所有元素值一樣,那麼定義一個空的vector然後逐個加入會比一開始指定vector的大小後新增可能更快

vector<int> v1(10);     //10個元素,都是0
vetcor<int> v2{10};     //1個元素,10
vector<int> v3(10, 1);  //10個元素,都是1
vectopr<int> v4{10, 1}; //2個元素,10、1

vector<string> v5{"hi"};        //列表初始化 1個元素
vector<string> v6("hi");        //錯誤! 不能用字串字面值構建vector物件
vector<string> v7{10};          //10個預設初始化的元素
vector<string> v8{10, "hi"};    //10個值為"hi"的元素

vector物件不能通過下標符新增元素,下標符只能訪問已存在的元素

由於編譯器並不會檢測出下標越界的情況,這可能會導致嚴重的緩衝區溢位(buffer overflow)錯誤。

迭代器

迭代器型別

  • iterator:可以修改
  • const_iterator:常量,且必須保證容器也是常量

為了便於專門的到const_iterator型別,C++11專門引入了兩個函式cbegin()和cend()

const vector<int> cv;
auto itr = cv.begin();  //vector<int>::const_iterator

解引用迭代器

解引用時必須加括號,因為不加括號相當於時訪問it的成員,而it只是迭代器型別

(*it).empty();   //正確
*it.empty();     //錯誤 

箭頭運算子->

箭頭運算子把解引用和成員訪問兩個操作結合在一起,也就是說it->mem和(*it).mem表達的意思相同

注意

當使用迭代器時,如果容器如vector動態增長了(push_back),會使迭代器失效。

迭代器運算

iter1 - iter2兩個迭代器的相減結果是他們之間的距離。

陣列

陣列的宣告中,維度必須是常量表達式。

unsigned cnt = 42;
int a[cnt];      //  錯誤
constexpr unsigned sz = 42;
int a[sz];      //正確     

字元陣列的特殊性

char a1[] = {'C', '+', '+'};            //列表初始化,沒有空字元
char a2[] = {'C', '+', '+', '\0'};      //列表初始化,含有顯式的空字元
char a3[] = "C++";                      //自動新增表示字串結束的空字元
const char a4[6] = "Daniel";            //錯誤,沒有空間存放結束符

陣列不允許拷貝和賦值

不能將陣列的內容拷貝給其他陣列作為初始值,也不能用陣列為其他陣列賦值。

int a[] = {0, 1, 2}
int a2[] = a;       //錯誤,不允許使用一個數組初始化另外一個數組
a2 = a;             //錯誤,不能把一個數組直接賦值給另一個數組

複雜的陣列宣告

就複雜陣列而言,由內向外閱讀,即先理解定義的名字是引用還是指標,然後在看外面是身累了型別的陣列。

int *ptrs[10];              //ptrs是一個包含10個整型指標的陣列
int &refs[10] = ?;          //錯誤,不存在引用型別的陣列
int (*Parray)[10] = &arr;   //Parray是10個整型陣列的指標
int (&arrRef)[10] = arr;    //arrRef是10個整型陣列的引用
int *(&arry)[10] = ptrs;    //10個整型指標的陣列的引用

訪問陣列元素

陣列的下標是size_t型別,在標頭檔案cstddef標頭檔案中

指標和陣列

指標也是迭代器

C++11中有新特性 可以使用begin和end直接獲取陣列的頭指標和尾後指標

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *beg = begin(ia);
int *last = end(ia);
ptrdiff_t len = last - beg;

兩個指標相減的結果的型別是一種名為ptrdiff_t的標準庫型別,和size_t一樣,ptrdiff_t也是一種定義在cstddef標頭檔案中的機器相關的型別。因為差值可能為負值,所以ptrdiff_t為有符號型別。

當兩個指標指向同一個陣列的元素,或指向該陣列的尾元素的下一位置,就能利用關係運算符對其進行比較。

int *b = arr, *e = arr + sz;
while(b < e) {
    b++;
}

下標和指標

int ia[] = {0, 2, 4, 5, 6, 7};
int *p = ia;
int i = *(p + 2)        //等價於 i = p[2]
int *p = ia[2];         //這是錯誤的,出了ia是指標,其他的都要用&
int k = p[-2];          //不會報錯

標準庫型別限定使用的下標必須是無符號型別,而內建的下標運算無此要求,即可為負值,但是並不像python中一樣有實際意義

C風格字串

C風格的字串不是一種型別,而是為了表達和使用字串而形成的一種約定俗成的寫法。

char ca[] = {'C', '+', '+'};
cout << strlen(ca);         //嚴重錯誤,ca沒有以'\0'空字元結束
為什麼要減少使用C風格字串,而推薦使用string

當使用C風格字串時,非常容易出現安全問題,例如strcat函式,如果連線到前一個字串大小不足以容納拼接後到字串,會導致嚴重的安全洩漏。

const char s1[] = "A string";
const char s2[] = "A different string";
int k = strcmp(s1, s2);         //比較字串函式, s1 = s2:k=0, s1 < s2:k<0, s1 > s2:k>0
char largeStr[100];
strcpy(largeStr, s1);       //s1拷貝給largeStr
strcat(largeStr, s2);       //將s2連線到largeStr後

與舊程式碼的介面

char陣列和string

若要混用string和C風格字串,需要使用c_str()函式。
需要注意的是,char指標可以初始化string,但是string不能初始化char指標

const char *str = s.c_str();

c_str函式返回的是const char*型別的,確保不會改變字元陣列的內容,但我們無法保證c_str函式一直有效,如果後續操作改變了s的值就可能讓之前返回的陣列失去效用。如果需要一直使用或者想改變,最好拷貝一份。

陣列初始化和vector物件

不允許使用一個數組為另一個內建型別的陣列賦初值,也不允許使用vector物件初始化陣列。相反的,允許使用陣列來初始化vector物件。只需要指出想要初始化陣列的初始位置和尾後位置指標就可以了。

int in_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(in_arr), end(in_arr));

多維陣列

C++中沒有多維陣列,只有陣列的陣列

多維陣列的初始化

int ia[3][4] = {0}; //陣列所有元素初始化為0
int ia[3][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};
//上下兩種初始化方式等價
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
//顯示地初始化每行的首元素, 其他元素執行預設值初始化
int ia[3][4] = {{0}, {4}, {8}};
//顯示地初始化首行, 其他元素執行預設值初始化
int ia[3][4] = {0, 3, 6, 9};
//將row定義成一個含有4個整數的陣列的引用,然後將其繫結到ia到第二行
int (&row)[4] = ia[1];

使用for語句處理多維陣列

外層迴圈使用引用型別的原因是:

  • 可以改變陣列元素的值
  • 為了避免陣列被自動轉化成指標(因為ia是陣列的陣列,即第一維陣列其實是指向其他維陣列的指標陣列),這樣加了引用後,就直接就變成了可以遍歷的陣列
//true
size_t cnt = 0;
for(auto &row : ia) {
    for(auto &col : row) {
        col = cnt++;
    }
}
//true
for(const auto &row : ia) {
    for(auto col : row) {
        cout << col << endl;
    }
}
//false
for(auto row : ia) {
    for(auto col : row) {
        cout << col << endl;
    }
}

上面程式碼最後一個錯誤的原因是:第一層迴圈其實是要取長度為n的陣列。因為row不是引用型別,所以編譯器初始化row時會自動將這些陣列形式的元素轉化成指向該陣列內首元素的指標,這樣row就是int*型別,那麼第二層迴圈就不合法了,因為不能用auto去遍歷int*型別。

要使用for去處理多維陣列,除了最內層的迴圈外,其他所有迴圈的控制變數都要加引用型別

指標和多維陣列

//指標宣告中,圓括號必不可少
int ia[3][4];
int (*p)[4] = ia;   //p指向含有4個整數的陣列
p = ia[2];
//auto遍歷
for(auto p = ia; p != ia + 3; ++p) {
    //這裡的p其實是指向ia[0/1/2]陣列的指標,*p才是陣列
    for(auto q = *p; q != *p + 4; ++q) {
        cout << *q << endl;
    } 
    cout << endl;
}
//使用begin, end函式
for(auto p = begin(ia); p != end(ia); ++p) {
    //這裡的p其實是指向ia[0/1/2]陣列的指標,*p才是陣列
    for(auto q = begin(*p); q != end(*p); ++q) {
        cout << *q << endl;
    } 
    cout << endl;
}