《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;
}