C++ 練氣期之一文看懂字元串

C++ 練氣期之細聊字元串

1. 概念

程式不僅僅用於數字計算,現代企業級項目中更多流轉著充滿了煙火氣的人間話語。這些話語,在電腦語言稱為字元串

從字面上理解字元串,類似於用一根竹籤串起了很多字元,讓人很容易想起冰糖葫蘆

字元串的基本組成元素是字元,可以認為字元串就是字元類型的數組。

量變總會引起質變,字元串是由字元的量變演化出的新類型2 者在數據含義存儲結構都有著本質上區別。

1.1 數據含義

C++字元類型當成整型數據類型看待。如下程式碼,當把A賦值給myChar時, 編譯器先獲取A的底層 ASCII 編碼,然後再把編碼值賦值給myChar

int myChar='A';
cout<<myChar; 
//輸出:65

如下程式碼,編譯器先找到97對應的字元,然後再賦值給myChar字元類型整型類型語法層面有差異,在底層,C++一視同仁。

char myChar=97;
cout<<myChar; 
//輸出:a

所以,用於整型數據類型的運算符都可以用於char類型。

char myChar='B';
char myChar_='A';
int res=myChar+myChar_;
cout<<"加操作:"<<res<<endl; 
res=myChar-myChar_;
cout<<"減操作:"<<res<<endl;
res=myChar*myChar_;
cout<<"乘操作:"<<res<<endl;  
res=myChar/myChar_;
cout<<"除操作:"<<res<<endl; 
bool is=myChar>myChar_;
cout<<"關係操作:"<<is<<endl;

輸出結果:

加操作:131
減操作:1
乘操作:4290
除操作:1
關係操作:1

雖然,字元串可看成是字元組成的數組,但是,應該把字元串當成一個獨立的整體,其數據含義更貼近現實意義:

  • 字元是單一詞,所能表達的語義非常有限。
  • 字元串則是由許多字元組成的語句,可用來表達豐富的語義。如:可以是姓名、可以是問候、可以情感表達、可以是提示……根據使用的上下文環境,字元串有其自己特定的現實意義。

1.2 存儲結構

字元常量必須用單引號包起來,字元直接存儲在變數中。

char myChar='A';

字元串的存儲方案比字元複雜很多,C++支援兩種字元串的存儲方案:

  • C語言風格的存儲。
  • C++語言的對象存儲。

下面深入了解這 2 種存儲方案的區別。

2. C 風格的字元串

C++可以直接延用C語言中的2種字元串存儲方案:

2.1 數組

數組存儲能較好地詮釋字元串是由字元所組成的概念。

使用數組存儲時,並不能簡單如下程式碼所示。對於開發者而言,可能想表達的是輸出一句HTLLO問候語。但在實際執行時,輸出時可能不僅只是HELLO

char myStr[5]= {'H','E','L','L','O'};
cout<<myStr<<endl;

為什麼會輸出更多資訊?

因為cout底層邏輯在輸出字元數組時,會以一個特定標識符\0作為結束標誌。cout在輸出 myStr字元數組的數據時,如果沒有遇到開發者提供的\0結束符號,則會在數組的存儲範圍之外尋找\0符號。

上述程式碼雖然能得到HELLO,那是因為在未使用的存儲空間中,\0符號很常見。

顯然,不能總是去碰運氣。所以,在使用字元數組時描述字元串時,則需要在適當位置添加字元串結束標識符\0

因結束符佔用了一個存儲位,HELLO需要5個存儲位,在聲明數組時,需要注意數組的實際長度為 6

char myStr[6]= {'H','E','L','L','O','\0'};
cout<<myStr<<endl;
//輸出結果:HELLO

執行下面的程式碼,查看輸出結果,想想為什麼輸出結果是HEL

char myStr[6]= {'H','E','L','\0','O','\0'};
cout<<myStr<<endl;
//輸出結果:HEL 

原因很簡單,cout在遇到第一個 \0時,就認定字元串到此結束了。

這裡有一個問題,如果實際的字元個數大於數組聲明的長度,會出現什麼情況?

char myStr[3]= {'H','E','L','L','O','\0'};
cout<<myStr<<endl;

如果出現上述程式碼,說明,你的數組沒有學太好。C++規定在使用{}進行字面值初始化數組時,{}內的實際數據個數不能大於數組聲明的長度。

1.png

當不確定字元串的長度時,可以採用省略[]中數字的方案。

char myStr[]= {'H','E','L','L','O','\0'};
cout<<myStr<<endl;

數組存儲方案同樣具有數組所描述的操作能力,最典型的就是使用下標遍曆數組。

char myStr[6]= {'H','E','L','L','O','\0'};
for(int i=0;i<6;i++){
	cout<<myStr[i]<<endl;
}

輸出結果:

H
E
L
L
O

在使用上述程式碼時,有 2 個地方需要注意:

  • 當下標定位到\0數據位時,並不能識別\0是字元串結束符,它只是純粹當成一個一個字元輸出,不具有字元串語義。
char myStr[8]= {'H','E','L','L','O','\0','M','Y'};
for(int i=0;i<8;i++){
	cout<<myStr[i]<<endl;
}

輸出結果:

H
E
L
L
O

M
Y
  • 因是靜態數組聲明方案,可以動態計算數組的長度。
char myStr[8]= {'H','E','L','L','O','\0','M','Y'};
cout<<"數組的長度:"<<sizeof(myStr)<<endl;
for(int i=0;i<sizeof(myStr);i++){
	cout<<myStr[i]<<endl;
}

輸出結果:

數組的長度:8
H
E
L
L
O

M
Y

使用sizeof(myStr)計算出來的是創建數組時指定的物理存儲長度。

所以,這裡要注意:

  • 通過結束符描述字元串是編譯器層面上的約定。
  • 遍歷時,實質是底層指針移動,這時,編譯層面的字元串概念在這裡不復存在。也就是說不存在遇到\0,就認為輸出結束。

2.2 字元串常量

上述字元串的描述方式,略顯繁瑣,因需要時時注意添加\0C當然也會想到這一點,可以使用字元串常量簡化字元串數組的創建過程。

char myStr[8]="HELLO";
cout<<myStr<<endl;
//輸出結果:HELLO

字元串常量需要使用雙引號括起來。

當執行如下程式碼時,會出現錯誤。

2.png

錯誤提示,數組長度不夠存儲給定的數據。可能要問!

數組長度是5,實際數據HELLO的長度也是5,不是剛剛好嗎。

別忘記了,完整的字元串是包括結束符\0的。在使用字元常量賦值時,編譯器會在字元串常量的尾部添加\0,再存儲到數組中,所以數組的長度至少是:字元串常量的長度+1

如下的程式碼方能正確編譯運行:

char myStr[6]="HELLO";
cout<<myStr<<endl;
//輸出:HELLO

字元串常量只是上述{}賦值的語法簡法版,其它的操作都是相同的,如循環遍歷。

char myStr[6]="HELLO";
for(int i=0;i<sizeof(myStr);i++){
	cout<<myStr[i]<<endl;
}

注意,如下的程式碼是錯誤的。

char myStr[6]="HELLO";
myStr[0]="S";

"S"表示一個字元串,至少包括了'S''\0' 2 個字元,更重要的是 "S"返回的是記憶體地址。

2.3 字元串操作

C語言風格的字元串提供了cstring庫,此庫提供大量函數用來操作字元串,常見函數如下:

  • strcat:字元串拼接。
  • strcpy:字元串複製。
  • strcmp:字元串比較。
  • strstr:字元串查找。
  • ……

下面介紹幾個字元串的常見操作。

2.3.1 複製操作

C++中數組之間是不能直接賦值的,如下是錯誤的:

char myStr[6]="HELLO";
char myStr_[6];
//錯誤
myStr_=myStr;

可以使用cstring庫中的 strcpy 函數:

#include <iostream>
#include <cstring>
using namespace std;
int main(int argc, char** argv) {
	char myStr[6]="HELLO";
	char myStr_[6];
	strcpy(myStr_,myStr);
	cout<<myStr_<<endl;
	return 0;
}

strcpy需要 2 個參數:

  • 目標字元串指針。
  • 源字元串指針。

其作用是,把源字元串複製給目標字元串。

2.3.2 長度操作

使用 strlen函數計算字元串的長度。

char myStr[10]="HELLO";
cout<<strlen(myStr)<<endl;
//輸出結果:5

sizeof計算出來的長度區別:

  • sizeof創建數組時,分配到的實際物理空/間的長度。
char myStr[10]="HEL\0LO";
cout<<sizeof(myStr)<<endl;
cout<<strlen(myStr)<<endl;

輸出結果:

10 
3
  • strlen計算出的是字元數組中字元串的實際長度,即遇到\0結束符前所有字元的長度。如下程式碼:
char myStr[10]="HEL\0LO";
cout<<strlen(myStr)<<endl;

輸出結果是:3\0結束前的字元串是HEL

2.3.3 拼接操作

字元串常量之間可以使用空白(空格、換行符、製表符)字元自動完成拼接。

cout<<"this is a test" "hello world"; 
//輸出:this is a testhello world

需要注意的地方是,第一個字元串常量和第二個字元串常量的拼接處直接連接,中間不保留空白符。

使用strcat進行拼接。

#include <iostream>
#include <cstring>
using namespace std;
int main(int argc, char** argv) {
	char names[10]="Hello";
	char address[10]="changsha";
	strcat(names,address);
	cout<<names; 
	return 0;
}
//輸出:Hellochangsha

strcat是把第二字元串連接到第一個字元串後尾部。

2.3.4 字元串比較

字元能夠直接比較,字元串則不能。如果相互之間有比較的需求時,可以使用 strcmp 函數。

#include <iostream>
#include <cstring>

using namespace std;
int main(int argc, char** argv) {
	char names[10]="zs";
	char names_[10]="ls";
	cout<<strcmp(names,names_); 
	return 0;
}
//輸出結果:1

返回值的語義:

  • 如果返回值為小於 0,則names 小於 address
  • 如果返回值為 等於 0,則names 等於 address
  • 如果返回值大於 0,則names 大於 address

2.3.5 子字元串查找

在原子符串中查找給定的子字元串出現的位置,返回此位置的指針地址。

#include <iostream>
#include <cstring>
using namespace std;
int main(int argc, char** argv) {
	char srcStr[15]="Hello World";
	char subStr[5]="llo";
	cout<<strstr(srcStr,subStr); 
	return 0;
}
//輸出:llo World

如果沒有查找到,則返回null

cstring庫提供了大量處理字元串的函數,如大小寫轉換函數tolowertoupper等。本文僅介紹幾個常用函數,需要時,可查閱文檔,其使用並不是很複雜。

3. C++字元串對象

C++除了支援C風格的字元串,因其面向對象編程的特性,內置有string類,可以使用此類創建字元串對象。

string類定義在string頭文件中。

如下程式碼可以初始化字元串對象:

//空字元串
string str1;
//字元串常量直接賦值
string str2="Hello";
string str3 {"this"};
string str4("Hi");

string為了支援uncode字元編碼,底層為每一個字元提供了1~4個位元組的存儲空間。

所以,可以用來存儲中文:

string str="中國人";
cout<<str<<endl;
//輸出:中國人

除了支援 char、還支援wchar_tchar16_tchar32_t數據類型。

string類中封裝了很多處理字元串的相關函數(方法),在cstring庫中可以找到對應的函數。因得益於設計的優秀特性,string類中封裝的功能體相比較cstring庫,更豐富、更全面。

下面介紹幾個常用的功能,其它可以查閱文檔。

獲取字元串的常規資訊:如長度、是否為空……

string str="Hello World";
cout<<str.size()<<endl;
cout<<str.length()<<endl;
//是否為空
cout<<str.empty()<<endl;
//能存儲的最大長度 
cout<<str.max_size()<<endl;
//容量 
cout<<str.capacity()<<endl; 

輸出結果:

11
11
0
4611686018427387897
11

數據維護(增、刪除、改、查)方法:

  • clear:清除所有內容。
string str="Hello World";
str.clear();
cout<<str<<endl;
//沒有任何內容輸出
  • insert:插入字元。
string str="Hello World";
string str_="Hi";
//第一個參數指定插入位置,第二參數指定需要插入的字元串
str.insert(3,str_);
cout<<str<<endl;
//輸出結果:HelHilo World
  • erase:刪除指定範圍內的所有字元。
string str="Hello World";
//第一個參數:指定刪除的起始位置,第二個參數:指定刪除的結束位置
string str_= str.erase(1,3);
cout<<str_<<endl;
//輸出:Ho World
  • push_backappend追加字元和字元串。
string str="Hello World";
//只能追加字元串,不能追加字元
str.append("OK");
cout<<str<<endl;
//只能以字元為單位追加
str.push_back('O');
cout<<str<<endl;
//輸出結果:
//Hello WorldOK
//Hello WorldOKO
  • pop_back:刪除最後一個字元。
string str="Hello World";
str.pop_back();
cout<<str<<endl;
//輸出結果:Hello Worl
  • compare:比較兩個字元串。
string str="Hello World";
string str_="Hello";
int res= str.compare(str_);
//返回值的語義和 `strcmp`一樣。
  • copy:字元串的拷貝。
//源字元串
string foo("quuuux");
//目標字元串,數組形式
char bar[7];
//第一個參數,目標字元串,第二參數,向目標字元串複製多少
foo.copy(bar, sizeof bar);
bar[6] = '\0';
cout << bar << '\n';
//輸出:quuuux

總結下來,字元串的存儲方案有2 種:

  • 數組形式。
  • 字元串對象。

4. cin 輸入字元串

如果需要使用交互輸入方式獲取用戶輸入的數據,可以直接使用 cin

string str;
char bar[7];
cin>>str;
cin>>bar;
cout<<str<<endl;
cout<<bar<<endl;

如上程式碼,如果用戶輸入this is,因字元串有空白字元。則會出現獲取到錯誤數據的問題。

3.png

原因解析:

cin接受用戶輸入時,以用戶輸入的換行符作為結束標識。用戶輸入this is時,遇到字元串的中間空白字元(空格、製表符、換行符)時,就認定輸入結束,僅把this存儲到str中,並不是this is

cin內置有快取器,會把 is快取起來,也就是說 cin是以單詞為單位進行輸入的。

當再次使用cin接受用戶輸入時,cin會檢查到快取器中已經有數據,會直接把is賦值給 bar變數。

如果需要以行為單位進行輸入時,可以使用:

  • cin.get()方法。
  • cin.getline()方法。

上述 2 個方法主要用於字元串數組的賦值。

兩者在使用時,都可以接受 2 個參數:

  • 目標字元串。
  • 用來限制輸入的大小。
char str[20];
cin.get(str,10);
cout<<str<<endl;
//輸入: this is 輸出:this is

如下程式碼,能實現相同的效果。

char str[20];
cin.getline(str,10);
cout<<str<<endl;

兩者也有區別,cin.get()不會丟棄用戶輸入字元串時的結束符。在連續使用 cin.get有可能出現問題,如下程式碼:

char str[20];
char str_[20];
//第一次輸入
cin.get(str,10);
cout<<str<<endl;
//第二次輸入
cin.get(str_,10);
cout<<str_<<endl;

執行效果:

4.png

第二次接受用戶輸入的過程根本沒出現。

原因是第一次接受用戶輸入後,cin.get快取了用戶輸入的換行符。在第二次接受用戶輸入時,cin會首先檢查快取器中是否有數據,發現有換行符,直接結束輸入。

解決方案,手動清除快取器的數據。

char str[20];
char str_[20];
cin.get(str,10);
cout<<str<<endl;
//不帶參數的 get 方法可以清除數據
cin.get(); 
cin.get(str_,10);
cout<<str_<<endl;

cin.getline在接受用戶輸入後,不會保留換行符,所以可以用於連續輸入。如下程式碼:

char str[20];
char str_[20];
//第一次輸入
cin.getline(str,10);
cout<<"str:"<<str<<endl;
//第二次輸入
cin.getline(str_,10);
cout<<"str_:"<<str_<<endl;

5.png

如果要使用cin輸入一行字元串,並賦值給字元串對象,則需要使用全局 getline函數。

//字元串對象
string str;
//第一個參數:cin對象  第二個參數:字元串對象
getline(cin,str);
cout<<str<<endl;

5. 總結

本文主要講解了C++字元串的2種存儲方案,一個是C語言風格的數組存儲方案,一個是C++對象存儲方案。

因存儲方案不同,其操作函數的提供方式也不相同。