《C++Primer》讀書筆記(三)字符串、向量、數組

命名空間using聲明

(1)本節學習最安全的方法使用using聲明

using namespace::name
  • 注意:使用一個名字就要有其對應的uesing聲明,如:using std::cin;

(2)頭文件不應包含using聲明
- 頭文件將會被直接拷貝進入文件中,有using可能會引起不必要的衝突

標準庫類型string

string類型表示可變長的字符序列

定義和初始化string對象

  • 常用初始化方式
string s1; //空字符串
string s2=s1; //s2是s1的副本
string s3="hiya"; //s3是該字符串的副本
string s4(10,'c'); //s4的內容是cccccccccc

(1)直接初始化和拷貝初始值
- 使用“=”是拷貝初始化,不使用等號則爲直接初始化

string s5="hiya"; //拷貝初始化
string s6("hiya"); //直接初始化
  • 另外,沒有必要將==直接初始化進行拷貝初始化==,如:string s8=string(10,’c’);

string對象上的操作

  • string的操作
操作 結果
os << s 將s寫到輸出流os當中,返回 os
is >> s 從is中讀取字符串賦值給 s,字符串以空白分隔,返回 is
getline(is,s) 從is中讀取一行賦值給s,返回 is
…… ……

(1)讀寫string對象

string s;
cin>>s;
cout<<s<<endl;
  • 在執行讀取操作時,string對象會自動忽略開頭的空白(空格、換行符、製表符等),直到遇見下一處空白爲止

(2)讀取未知數量的string

string word;
while (cin>>word) //反覆讀取,直到到達文件末尾
cout<<word<<endl; //逐個輸出單詞,每個單詞後面緊跟一個換行符

(3)使用getline讀取一整行
- 當我們希望最終得到的字符串中保留輸入時的空白符時,可以使用getline來代替原來的>>

string line;
while (getline(cin,line))
  cout<<line<<endl;
  • getline直到讀取到換行符爲止,抱愧換行符也被讀入,但是存入string對象時,又不存換行符。所以,得到的對象==不包含換行符==

(4)string 的empty和size操作
- 它們都是string的成員函數
- 調用方法:s.empty() 和 s.size()

(5)string::size_type 類型
- 對於size函數來說,返回的是int或者unsigned 似乎都是合情合理的,但其實它返回的是string::size_type類型的值
- 切記s.size()返回的是一個無符號整型,在表達式中混用帶符號和無符號數將會引起麻煩

建議:如果一個表達式中,有了size就不要有int了,避免混用無符號數

(6)比較string對象

(7)爲string對象賦值

(8)兩個string對象相加
- 兩個string相加將實現拼接的效果

(9)字面值和string對象相加
- 字符串字面值與string是不同的類型
- ==字面值不能與字面值==相加

處理string對象中的字符

  • 想要知道某個字符的特性,使用cctype頭文件中定義的標準庫函數
函數 執行
isalnum(c) 當c是字母或數字時爲真
isalpha 當c是字母時爲真
…… ……

(1)處理每個字符?使用基於範圍的for語句
- 範圍for語句

for (declaration:expression)
  statement

其中,expression部分是一個對象,用於表示一個序列。declaration部分負責定義一個變量,該變量將被用於訪問序列中的基礎元素。每次迭代,declaration部分的變量會被初始化爲expression部分的下一個元素值

string str("some string");
for(auto c:str)  //對於str中的每個字符
   cout<< c <<endl;  //輸出當前字符,後面緊跟一個換行符

(2)使用範圍for語句改變字符串中的字符

  • 要想改變string對象中字符的值,必須把循環變量定義成引用類型。因爲引用只是給定對象一個別名,因此當使用引用作爲循環控制變量時,這個變量實際上被依次綁定到了序列的每個元素上,使用這個引用,就能改變它綁定的字符
string s("Hello World");
for (auto &c:s) //對s中的每個字符
  c=toupper(c); //c是一個引用,因此賦值語句將改變s中字符的值
cout<<s<<endl

(3)只處理一部分字符?
- 要想訪問string對象中的單個字符有兩種方式:1.使用下標 2.迭代器
- 使用下標執行迭代

for(decltype(s.size() index=0;
   index !=s.size() && !isspace(s[index]);
   ++index)
    s[index]=toupper(s[index]);

注意檢查下標的合法性

(4)使用下標執行隨機訪問

練習

1.編寫一段程序,使用範圍for語句將字符串中的所有字符換成X

string s="Hello World";
for(auto &c:s)
  c='X';
cout<<s<<endl;

2.下面的程序有何作用?合法?

string s;
cout<<s[0]<<endl;

顯示s的第一個字符,但是因爲s未初始化,所以s是一個空串

3.編程:讀入帶有標點符號的字符串,去除標點後輸出

string s="H,e,l.l!o";
for(auto &c:s)
    if(ispunct(c))
        c=0;
cout<<s<<endl;

另一種可供參考的程序,實現真正的去除,以上程序是將標點符號換成了空串

標準庫類型vector

定義和初始化vector對象

(1)初始化vector對象的方法

語句 結果
vector v1是一個空vector,它潛在的元素是T類型的,執行默認初始化
vector v2(v1) v2中包含有v1的所有元素的副本
vector v3(n,val) v3包含了n個重複的元素,每個元素的值都是val
vector v5{a,b,c…} v5包含了初始值個數的元素,每個元素被賦予相應值

- vector元素的拷貝

vector<int> ivec; //初始狀態爲空
// 在此處給ivec添加一些值
vector<int> ivec2(ivec); //把ivec的元素拷貝給ivec2
vector<int> ivec3=ivec; //把ivec的元素拷貝給ivec3
vector<string> svec(ivec2); //錯誤,svec的元素是string對象,不是int

(2)列表初始化vector對象(花括弧)

vector<string> v1{"a","an","the"};

(3)創建指定數量的元素

vector<int> ivec(10,-1); //10個int類型,每個都被初始化爲-1

(4)值初始化

vector<int> ivec(10); //10個元素,沒有都初始化爲0
vector<int> vi=10; //錯誤,必須使用直接初始化的形式指定向量大小

向vector對象中添加元素

(1)push_back函數

vector<int> v2;
for(int i=0;i!=100;++i)
   v2.push_back(i);
vector<string> text;
string word;
while(cin>>word){
    text.push_back(word);
}

(2)用vector對象添加元素蘊含的編程假定
- 範圍for語句體內不應改變其所遍歷序列的大小
- 於是,在循環體內向vector添加元素的語句,則不能使用範圍for循環

練習

1.編程:用cin讀入一組整數,存入vector對象中

vector<int> num;
int new_num;
while(cin>>new_num){
    push_back(new_num);
}

其他vector操作

操作 結果
v.empty() v不含有任何元素,返回真;否則爲假
v.size() 返回v中元素的個數
v.push_back(t) 向v的末尾添加一個值爲t的元素
v[n] 返回v中第n個位置上元素的引用
v1=v2 以v2的值拷貝替換v1

(1)訪問vector對象中元素的方法
- 與string顯示,可以用範圍for來處理所有元素

vector<int> v{1,2,3,4};
for(auto &i:v){
    i*=i; //求元素值的平方
}
for(auto i:v){
    cout<<i<<endl;
}

(2)要使用size()返回的size_type

vector<int>::size_type //正確
vector::size_tyoe //錯誤

(3)計算vector內對象的索引
- 統計分數段的程序

vector<int> scores(11,0);
unsigned grade;
while(cin>>grade){
    if(grade<=100)
       ++scores[grade/10];
}

(4)不能用下標形式添加元素

vector對象(以及string對象)的下標運算符可用於訪問已經存在的元素,而不能用於添加元素

==只能對確定已經存在的元素進行下標操作==

迭代器介紹

使用迭代器

  • 每一個有迭代器的類型都有begin和end的成員
  • begin負責返回第一個元素,end負責返回尾元素的下一位置
auto b=v.begin(),e=v.end();
  • 如果容器爲空,則begin和end返回的是同一個迭代器,都是尾後迭代器

(1)迭代器運算符

運算符 結果
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter並獲取該元素的名mem的成員,等價於(*iter).mem

- 小例子

string s("some string");
if(s.begin()!=s.end()){
    auto it=s.begin();
    *it=toupper(*it);
}

(2)將迭代器從一個元素移動到另一個元素
- 迭代器使用遞增(++)運算符來從一個元素移動到下一個元素。從邏輯上說,迭代器的遞增和整數的遞增類似,整數的遞增是在整數值上“+1”,而迭代器則是“向前移動一個位置”

因爲end返回的迭代器並不實際指示某個元素,所以不能對其進行遞增或解引用的操作

for(auto it=s.begin();it!=s.end()&&!isspace(*it);++it)
   *it=toupper(*it);

(3)迭代器類型
- 擁有迭代器的標準庫類型使用iterator和const_iterator

vector<int>::iterator it; //it能讀寫vector<int>的元素
string::iterator it2; //it2能讀寫string對象中的字符

vector<int>::const_iterator it3; //it3只能讀元素,不能寫元素
string::const_iterator it4; //it4只能讀字符,不能寫字符

(2)begin和end運算符
- 如果對象是常量,begin和end返回類型爲:const_iterator;如果對象不是常量,返回iterator
- 爲了可以直接得到const_iterator類型的返回值,C++中引入了cbegin和cend

(3)結合解引用和成員訪問操作
- 解引用迭代器可獲得迭代器所指的對象,如果該對象的類型恰好是類,就有可能希望進一步訪問它的成員

vector<string> st;
auto it=st.begin();

(*it).empty();

*it.empty(); //錯誤,試圖訪問it的名爲empty的成員,但it是迭代器,沒有此成員

即如果it是vector對象的迭代器,使用(*it).empty()

  • 爲了解決上述表達式,C++定義了==箭頭運算符(->)==,it->mem和(*it).mem表達的意思相同
// 依次輸出text的每一行直至遇到第一個空白行爲止
for (auto it=text.cbegin();
    it!=text.cend()&&!it->empty();++it)
    cout<<*it<<endl;

(4)某些對vector對象的操作會使迭代器失效

  1. 不能在範圍for循環中向vector對象添加元素
  2. 任何一種可能改變vector對象容量的操作都會使迭代器失效,比如push_back

迭代器運算

(1)迭代器的算術運算
- ==iter+n== 迭代器和一個整形數相加減,其返回值是移動若干個位置後的迭代器(結果指向一個元素或者指向尾元素的下一個位置)

auto mid-vi.begin()+vi.size()+2;
if(it<mid)
  // 處理vi前半部分的元素

(2)使用迭代器運算
- 使用迭代器運算的一個經典算法是二分搜索

auto beg=text.begin(),end=text.end();
auto mid=text.begin()+(end-beg)/2;

while(mid!=end && *mid!=sought){
    if(sought < *mid)
       end=mid;
    else
       beg=mid+1;
    mid=beg+(end-beg)/2;
}

數組

定義和初始化數組

  • 數組定義中,數組名字和數組的維度,都是數組的類型的一部分,編譯的時候應該是已知的(即維度也必須是常量表達式)
  • 和內置類型一樣,如果在函數內部定義了某種內置類型的數組,那麼默認初始化會令數組含有未定義的值

(1)顯式初始化數組元素
- 可以對數組進行列表初始化,此時允許忽略數組的維度
- 如果維度比提供的初始值數量大,則保存到前段,剩餘部分默認初始化

const unsigned sz=3;
int ia1[sz]={0,1,2}; //含有3個元素,值分爲爲0,1,2
int a2[]={0,1,2}; //維度是3的數組
int a4[5]={0,1,2}; //等價於a3[]={0,1,2,0,0}
int a5[2]={0,1,2}; //錯誤,初始值過多

(2)字符數組的特殊性
- 字符數組有一種額外的初始化形式,可以用字符串字面值,但一定要注意字符串字面值最後還有一個空字符,同樣也會被拷貝進入字符數組中

char a3[]="C++"; //結果會自動添加字符串結束的空字符

(3)不允許拷貝和賦值
- ==不能將數組的內容拷貝進其他數組作爲其初始值,也不能用數組爲其他數組賦值==

(4)理解複雜的數組聲明

int *ptrs[10]; //ptrs是含有10個整型指針的數組
int &refs[10]=/*?*/; //錯誤,不存在引用的數組

int (*Parray)[10]=&arr; //Parray指向一個含有10個整型的數組
int (&arrRef)[10]=arr; //arrRef引用一個含有10個整數的數組
  • 更復雜的情況
int *(&arry)[10]=ptrs; // arry是數組的引用,該數組含有10各指針
  • ==要理解數組聲明的含義,最好的辦法是從數組的名字開始按照由內向外的順序運輸==

訪問數組的元素

  • 在使用數組下表的時候,通常將其定義爲size_t類型——一種機器相關的無符號類型
unsigned scores[11]={};
unsigned grade;
while(cin>>grade){
    if(grade<=100)
    ++scores[grade/10];
}
  • 與vector和strin一樣,當要遍歷所有元素時,用範圍for很合適

(1)檢查下標的值

大多數常見的安全問題都源於緩衝區溢出錯誤。當數組或其他類似數據結構的下標越界並試圖訪問非法內存區域時,就會產生此類錯誤

指針和數組

  • 數組的元素取地址符,就能得到指向該元素的指針
string nums[]={"one","two","three"};
string *p=&nums[0]; //p指向nums的第一個元素
  • 數組還有一個特性:在用到數組名字的地方,編譯器都會自動地將其替換爲一個指向數組首元素的指針

數組的操作實際上是指針的操作

  • 所以,==當使用數組作爲一個auto變量的初始值時,其類型是指針而非數組==

  • 但是,使用decltype關鍵字時,不會發生上述轉換,decltype(ia)返回的類型是對應的數組類型

(1)指針也是迭代器
- 指向數組元素的指針擁有其他迭代器的運算

int arr[]={0,1,2,3};
int *p=arr;
++p;
  • 需要注意的是,==尾後指針不能執行解引用或者遞增操作==

(2)標準庫函數begin和end
- 數組的尾後指針及其容易出錯,所以C++11引入了begin和end函數,但是,數組不是類類型,所以這兩個函數不是成員函數,正確方法:

int ia[]={0,1,2,3,4,5,6,7,8,9};
int *beg=begin(ia);
int *last=end(ia);
  • 例子:假設arr是一個整型數組,下面的程序負責找到arr中的第一個負數
int *pbeg=begin(arr),*pend=end(arr);
while(pbeg!=pend&&*pbeg>=0)
     ++pbeg;

(3)指針運算
- 指向數組的指針可以自信所有迭代器的運算,包括解引用、遞增、比較、與整數相加、兩個指針相減等
- 兩個指針相減的結果等於它們之間的距離(必須指向同一個數組當中的元素),兩個指針相減的結果類型爲:ptrdiff_t類型,是與機器相關的帶符號類型
- 指向同一個數組的元素的兩個指針,可以比較大小,實際爲比較它們地址的先後

// 實現遍歷
int *b=arr,*e=arr+sz;
while(b<e){
    ++b;
}

(4)解引用和指針運算的交互
- 指針加上一個整型後依然是指針,所以同樣可以解引用(即獲得數組元素後面n位的元素)

(5)下標和指針

int *p=&ia[2]; // p指向索引爲2的元素
int j=p[1]; // j指向索引爲3的元素
int k=p[-2]; // p[-2]是ia[0]表示的元素

C風格字符串

(1)比較字符串
- 在C++標準庫string對象的方法,比較字符串的時候,用的是普通關係運算符,但是,如果用在C風格字符串上,實際將比較的是指針而非字符串本身:
- C風格字符串比較的方法

if(strcmp(ca1,ca2)<0) // 和兩個string對象的比較s1<s2效果一樣

(2)目標字符串的大小由調用者指定
- 使用C風格字符串連接string對象要使用strcat函數和strcpy函數,而且還需要準備存放結果的字符串

strcpy(largeStr,ca1);
strcat(largeStr," ");
strcar(largeStr,ca2);

但是,如此及其易發生錯誤!!!==最好使用標準庫string==

與舊代碼的接口

(1)混用string對象和C風格字符串
- 任何出現字符串字面值的地方都可以用以空字符結束的字符數組來替代
1. 允許使用空字符串結束的字符數組來初始化string對象或爲string對象賦值
2. 在string對象的加分運算中允許使用以空字符結束的字符數組作爲其中要給運算對象(不能兩個運算對象都是);在string對象的複合賦值運算中允許使用以空字符串結束的字符數組作爲右側的運算對象
- 上述性質反過來則不成立:如果程序某處需要一個C風格字符串,無法直接用string對象來替代它。例如,不能用string對象直接初始化指向字符的指針。爲了完成該功能,sting提供了c_str函數

char *str=s; // 錯誤:不能用string對象初始化char*
const char *str=s.c_str(); //正確

如果執行完C_str()函數後程序想一直都使用其返回的字符數組,最好將該數組重新拷貝一份

(2)使用數組初始化vector對象
- 數組不能用數組賦初值,也不允許用vector對象初始化。
- 但是,允許用數組來初始化vector對象(只需指明要拷貝的首元素地址和尾後元素地址)

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

多維數組

C++語言中實際上沒有多維數組,通常說的多維數組其實是數組的數組

int ia[3][4]; //大小爲3的數組,每個元素是含有4個整數的數組

int arr[10][20][30]; //大小爲10的數組,每個元素都是大小爲20的數組,20個數組元素是含有30個整型的數組

(1)多維數組的初始化

int ia[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}};

int ia{3}[4]={1,2,3,4,5,6,7,8,9,10,11};

int int[3][4]={{0},{4,},{8}}; //只聲明每行的首個元素

(3)多維數組的下標引用
- 使用下標運算符來訪問多維數組的元素

ia[2][3]=arr[0][0][0]; //用arr的首元素爲ia第3的第4個元素賦值
int (&row)[4]=ia[1]; ..把row綁定到ia的第二個4元素數組上

(4)使用範圍for語句處理多維數組

size_t cnt=0;
for(auto &row:ia) //對於外層數組的每一個元素
    for(auto &col:row){ //對於內層數組的每一個元素
        col=cnt;
        ++cnt;
    }

要使用範圍for語句處理多維數組,除了最內層的循環外,其他所有循環的控制變量都應該是引用類型

(5)指針和多維數組

int ia[3][4]; //大小爲3的數組,每個元素是含有4各整數的數組
int (*o)[4]=ia; //p指向含有4個整數的數組
p=&ia[2]; //p指向ia的尾元素
for(auto p=begin(ia);p!=end(ia);++p){ //p指向ia的第一個數組
    for(auto q=begin(*p);q!=end(*p);++q) //q指向內層數組的首元素
        cout<<*q<<" "<<end;
}

(6)類型別名簡化多維數組的指針
- 讀、寫和理解一個指向多維數組的指針是一個讓人不厭其煩的工作,使用類型別名能讓這個各做變得跟簡單

using int_array=int[4];
typedef int int_array[4];

for(int_array *p=ia;p!=ia+3;++p){
    for(int *q=*p;q!=*p+4;++q)
        cout<<*q<<""<<endl;
}
發佈了34 篇原創文章 · 獲贊 5 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章