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++
規定在使用{}
進行字面值初始化數組時,{}
內的實際數據個數不能大於數組聲明的長度。
當不確定字元串的長度時,可以採用省略[]
中數字的方案。
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 字元串常量
上述字元串的描述方式,略顯繁瑣,因需要時時注意添加\0
。C
當然也會想到這一點,可以使用字元串常量簡化字元串數組的創建過程。
char myStr[8]="HELLO";
cout<<myStr<<endl;
//輸出結果:HELLO
字元串常量需要使用雙引號
括起來。
當執行如下程式碼時,會出現錯誤。
錯誤提示,數組長度不夠存儲給定的數據。可能要問!
數組長度是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
庫提供了大量處理字元串的函數,如大小寫轉換函數tolower
和toupper
等。本文僅介紹幾個常用函數,需要時,可查閱文檔,其使用並不是很複雜。
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_t
、char16_t
、char32_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_back
、append
追加字元和字元串。
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
,因字元串有空白字元
。則會出現獲取到錯誤數據的問題。
原因解析:
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;
執行效果:
第二次接受用戶輸入的過程根本沒出現。
原因是第一次接受用戶輸入後,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;
如果要使用cin
輸入一行字元串,並賦值給字元串對象,則需要使用全局 getline
函數。
//字元串對象
string str;
//第一個參數:cin對象 第二個參數:字元串對象
getline(cin,str);
cout<<str<<endl;
5. 總結
本文主要講解了C++
字元串的2
種存儲方案,一個是C
語言風格的數組存儲方案,一個是C++
對象存儲方案。
因存儲方案不同,其操作函數的提供方式也不相同。