目錄
一、C++ string詳解
使用 string 類需要包含頭文件<string>
,下面的例子介紹了幾種定義 string 變量(對象)的方法:使用 string 類需要包含頭文件<string>
,下面的例子介紹了幾種定義 string 變量(對象)的方法:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1;
string s2 = "c plus plus";
string s3 = s2;
string s4 (5, 's');
return 0;
}
變量 s1 只是定義但沒有初始化,編譯器會將默認值賦給 s1,默認值是""
,也即空字符串。
變量 s2 在定義的同時被初始化爲"c plus plus"
。與C風格的字符串不同,string 的結尾沒有結束標誌'\0'
。
變量 s3 在定義的時候直接用 s2 進行初始化,因此 s3 的內容也是"c plus plus"
。
變量 s4 被初始化爲由 5 個's'
字符組成的字符串,也就是"sssss"
。
從上面的代碼可以看出,string 變量可以直接通過賦值操作符=
進行賦值。string 變量也可以用C風格的字符串進行賦值,例如,s2 是用一個字符串常量進行初始化的,而 s3 則是通過 s2 變量進行初始化的。
string函數:
1.string 類提供的 length() 函數:
string s = "http://c.biancheng.net";
int len = s.length();
cout<<len<<endl;
由於 string 的末尾沒有'\0'
字符,所以 length() 返回的是字符串的真實長度,而不是長度 +1。
2.轉換爲C風格的字符串c_str()
雖然 C++ 提供了 string 類來替代C語言中的字符串,但是在實際編程中,有時候必須要使用C風格的字符串(例如打開文件時的路徑),爲此,string 類爲我們提供了一個轉換函數 c_str(),該函數能夠將 string 字符串轉換爲C風格的字符串,並返回該字符串的 const 指針(const char*)。請看下面的代碼:
string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");
3.string 字符串的輸入輸出
string 類重載了輸入輸出運算符,可以像對待普通變量那樣對待 string 變量,也就是用>>
進行輸入,用<<
進行輸出。
4.訪問字符串中的字符
string 字符串也可以像C風格的字符串一樣按照下標來訪問其中的每一個字符。string 字符串的起始下標仍是從 0 開始。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s = "1234567890";
for(int i=0,len=s.length(); i<len; i++){
cout<<s[i]<<" ";
}
cout<<endl;
s[5] = '5';
cout<<s<<endl;
return 0;
}
5.字符串的拼接
有了 string 類,我們可以使用+
或+=
運算符來直接拼接字符串,非常方便,再也不需要使用C語言中的 strcat()、strcpy()、malloc() 等函數來拼接字符串了,再也不用擔心空間不夠會溢出了。
用+
來拼接字符串時,運算符的兩邊可以都是 string 字符串,也可以是一個 string 字符串和一個C風格的字符串,還可以是一個 string 字符串和一個字符數組,或者是一個 string 字符串和一個單獨的字符。請看下面的例子:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "first ";
string s2 = "second ";
char *s3 = "third ";
char s4[] = "fourth ";
char ch = '@';
string s5 = s1 + s2;
string s6 = s1 + s3;
string s7 = s1 + s4;
string s8 = s1 + ch;
cout<<s5<<endl<<s6<<endl<<s7<<endl<<s8<<endl;
return 0;
}
6.插入字符串
insert() 函數可以在 string 字符串中指定的位置插入另一個字符串,它的一種原型爲:
string& insert (size_t pos, const string& str);
pos 表示要插入的位置,也就是下標;str 表示要插入的字符串,它可以是 string 字符串,也可以是C風格的字符串。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1, s2, s3;
s1 = s2 = "1234567890";
s3 = "aaa";
s1.insert(5, s3);
cout<< s1 <<endl;
s2.insert(5, "bbb");
cout<< s2 <<endl;
return 0;
}
運行結果:
12345aaa67890
12345bbb67890
insert() 函數的第一個參數有越界的可能。如果越界,則會產生運行時異常,我們將會在《C++異常(Exception)》一章中詳細講解如何捕獲這個異常。
7.刪除字符串
erase() 函數可以刪除 string 中的一個子字符串。它的一種原型爲:
string& erase (size_t pos = 0, size_t len = npos);
pos 表示要刪除的子字符串的起始下標,len 表示要刪除子字符串的長度。如果不指明 len 的話,那麼直接刪除從 pos 到字符串結束處的所有字符(此時 len = str.length - pos)。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1, s2, s3;
s1 = s2 = s3 = "1234567890";
s2.erase(5);
s3.erase(5, 3);
cout<< s1 <<endl;
cout<< s2 <<endl;
cout<< s3 <<endl;
return 0;
}
運行結果:
1234567890
12345
1234590
有讀者擔心,在 pos 參數沒有越界的情況下, len 參數也可能會導致要刪除的子字符串越界。但實際上這種情況不會發生,erase() 函數會從以下兩個值中取出最小的一個作爲待刪除子字符串的長度:
- len 的值;
- 字符串長度減去 pos 的值。
說得簡單一些,待刪除字符串最多隻能刪除到字符串結尾。
8.字符串提取
substr() 函數用於從 string 字符串中提取子字符串,它的原型爲:
string substr (size_t pos = 0, size_t len = npos) const;
pos 爲要提取的子字符串的起始下標,len 爲要提取的子字符串的長度。
請看下面的代碼:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "first second third";
string s2;
s2 = s1.substr(6, 6);
cout<< s1 <<endl;
cout<< s2 <<endl;
return 0;
}
運行結果:
first second third
second
系統對 substr() 參數的處理和 erase() 類似:
- 如果 pos 越界,會拋出異常;
- 如果 len 越界,會提取從 pos 到字符串結尾處的所有字符。
9.字符串查找
string 類提供了幾個與字符串查找有關的函數,如下所示。
1) find() 函數
find() 函數用於在 string 字符串中查找子字符串出現的位置,它其中的兩種原型爲:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
第一個參數爲待查找的子字符串,它可以是 string 字符串,也可以是C風格的字符串。第二個參數爲開始查找的位置(下標);如果不指明,則從第0個字符開始查找。
請看下面的代碼:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "first second third";
string s2 = "second";
int index = s1.find(s2,5);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}
運行結果:
Found at index : 6
find() 函數最終返回的是子字符串第一次出現在字符串中的起始下標。本例最終是在下標6處找到了 s2 字符串。如果沒有查找到子字符串,那麼會返回一個無窮大值 4294967295。
2) rfind() 函數
rfind() 和 find() 很類似,同樣是在字符串中查找子字符串,不同的是 find() 函數從第二個參數開始往後查找,而 rfind() 函數則最多查找到第二個參數處,如果到了第二個參數所指定的下標還沒有找到子字符串,則返回一個無窮大值4294967295。
請看下面的例子:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "first second third";
string s2 = "second";
int index = s1.rfind(s2,6);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}
運行結果:
Found at index : 6
3) find_first_of() 函數
find_first_of() 函數用於查找子字符串和字符串共同具有的字符在字符串中首次出現的位置。請看下面的代碼:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "first second second third";
string s2 = "asecond";
int index = s1.find_first_of(s2);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}
運行結果:
Found at index : 3
本例中 s1 和 s2 共同具有的字符是 ’s’,該字符在 s1 中首次出現的下標是3,故查找結果返回3。
二、引用
我們知道,參數的傳遞本質上是一次賦值的過程,賦值就是對內存進行拷貝。所謂內存拷貝,是指將一塊內存上的數據複製到另一塊內存上。
對於像 char、bool、int、float 等基本類型的數據,它們佔用的內存往往只有幾個字節,對它們進行內存拷貝非常快速。而數組、結構體、對象是一系列數據的集合,數據的數量沒有限制,可能很少,也可能成千上萬,對它們進行頻繁的內存拷貝可能會消耗很多時間,拖慢程序的執行效率。
C/C++ 禁止在函數調用時直接傳遞數組的內容,而是強制傳遞數組指針,這點已在《C語言指針變量作爲函數參數》中進行了講解。而對於結構體和對象沒有這種限制,調用函數時既可以傳遞指針,也可以直接傳遞內容;爲了提高效率,我曾建議傳遞指針,這樣做在大部分情況下並沒有什麼不妥,讀者可以點擊《C語言結構體指針》進行回顧。
但是在 C++ 中,我們有了一種比指針更加便捷的傳遞聚合類型數據的方式,那就是引用(Reference)。
在 C/C++ 中,我們將 char、int、float 等由語言本身支持的類型稱爲基本類型,將數組、結構體、類(對象)等由基本類型組合而成的類型稱爲聚合類型(在講解結構體時也曾使用複雜類型、構造類型這兩種說法)。
引用(Reference)是 C++ 相對於C語言的又一個擴充。引用可以看做是數據的一個別名,通過這個別名和原來的名字都能夠找到這份數據。引用類似於 Windows 中的快捷方式,一個可執行程序可以有多個快捷方式,通過這些快捷方式和可執行程序本身都能夠運行程序;引用還類似於人的綽號(筆名),使用綽號(筆名)和本名都能表示一個人。
引用的定義方式類似於指針,只是用&
取代了*
,語法格式爲:
type &name = data;
type 是被引用的數據的類型,name 是引用的名稱,data 是被引用的數據。引用必須在定義的同時初始化,並且以後也要從一而終,不能再引用其它數據,這有點類似於常量(const 變量)。
#include <iostream>
using namespace std;
int main() {
int a = 99;
int &r = a;
cout << a << ", " << r << endl;
cout << &a << ", " << &r << endl;
return 0;
}
運行結果:
99, 99
0x28ff44, 0x28ff44
本例中,變量 r 就是變量 a 的引用,它們用來指代同一份數據;也可以說變量 r 是變量 a 的另一個名字。從輸出結果可以看出,a 和 r 的地址一樣,都是0x28ff44
;或者說地址爲0x28ff44
的內存有兩個名字,a 和 r,想要訪問該內存上的數據時,使用哪個名字都行。
注意,引用在定義時需要添加&
,在使用時不能添加&
,使用時添加&
表示取地址。如上面代碼所示,第 6 行中的&
表示引用,第 8 行中的&
表示取地址。除了這兩種用法,&
還可以表示位運算中的與運算。
由於引用 r 和原始變量 a 都是指向同一地址,所以通過引用也可以修改原始變量中所存儲的數據,請看下面的例子:
#include <iostream>
using namespace std;
int main() {
int a = 99;
int &r = a;
r = 47;
cout << a << ", " << r << endl;
return 0;
}
運行結果:
47, 47
最終程序輸出兩個 47,可見原始變量 a 的值已經被引用變量 r 所修改。
如果讀者不希望通過引用來修改原始的數據,那麼可以在定義時添加 const 限制,形式爲:
const type &name = value;
也可以是:
type const &name = value;
這種引用方式爲常引用。
#include <iostream>
using namespace std;
void swap1(int a, int b);
void swap2(int *p1, int *p2);
void swap3(int &r1, int &r2);
int main() {
int num1, num2;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap1(num1, num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap2(&num1, &num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap3(num1, num2);
cout << num1 << " " << num2 << endl;
return 0;
}
//直接傳遞參數內容
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//傳遞指針
void swap2(int *p1, int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//按引用傳參
void swap3(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;
}
引用和指針的其他區別
1) 引用必須在定義時初始化,並且以後也要從一而終,不能再指向其他數據;而指針沒有這個限制,指針在定義時不必賦值,以後也能指向任意數據。
2) 可以有 const 指針,但是沒有 const 引用。也就是說,引用變量不能定義爲下面的形式:
int a = 20;
int & const r = a;
但是,常引用即不允許改變a值的引用是允許的,即:
int const &r=a;
因爲 r 本來就不能改變指向,加上 const 是多此一舉。
3) 指針可以有多級,但是引用只能有一級,例如,int **p
是合法的,而int &&r
是不合法的。如果希望定義一個引用變量來指代另外一個引用變量,那麼也只需要加一個&
,如下所示:
int a = 10;
int &r = a;
int &rr = r;
4) 指針和引用的自增(++)自減(--)運算意義不一樣。對指針使用 ++ 表示指向下一份數據,對引用使用 ++ 表示它所指代的數據本身加 1;自減(--)也是類似的道理。
5)引用佔用內存,引用佔用的內存的存儲內容是被引用變量的存儲地址,引用佔用的內存地址不被C++允許獲取。
什麼樣的臨時數據會放到寄存器中
寄存器離 CPU 近,並且速度比內存快,將臨時數據放到寄存器是爲了加快程序運行。但是寄存器的數量是非常有限的,容納不下較大的數據,所以只能將較小的臨時數據放在寄存器中。int、double、bool、char 等基本類型的數據往往不超過 8 個字節,用一兩個寄存器就能存儲,所以這些類型的臨時數據通常會放到寄存器中;而對象、結構體變量是自定義類型的數據,大小不可預測,所以這些類型的臨時數據通常會放到內存中。
下面的代碼是正確的,它證明了結構體類型的臨時數據會被放到內存中:
#include <iostream>
using namespace std;
typedef struct{
int a;
int b;
} S;
//這裏用到了一點新知識,叫做運算符重載,我們會在《運算符重載》一章中詳細講解
S operator+(const S &A, const S &B){
S C;
C.a = A.a + B.a;
C.b = A.b + B.b;
return C;
}
S func(){
S a;
a.a = 100;
a.b = 200;
return a;
}
int main(){
S s1 = {23, 45};
S s2 = {90, 75};
S *p1 = &(s1 + s2);
S *p2 = &(func());
cout<<p1<<", "<<p2<<endl;
return 0;
}
下面的代碼演示了表達式所產生的臨時結果:
int n = 100, m = 200;
int *p1 = &(m + n); //m + n 的結果爲 300
int *p2 = &(n + 100); //n + 100 的結果爲 200
bool *p4 = &(m < n); //m < n 的結果爲 false
這些表達式的結果都會被放到寄存器中,嘗試用&
獲取它們的地址都是錯誤的。
下面的代碼演示了函數返回值所產生的臨時結果:
int func(){
int n = 100;
return n;
}
int *p = &(func());
func() 的返回值 100 也會被放到寄存器中,也沒法用&
獲取它的地址。
總之,引用不能玩太多花樣,規範點來不好嗎?
但是常引用可能會方便很多問題,尤其在引用作爲參數,實參是常量表達式的情況下,不加const就是錯的。
bool isOdd(const int &n){ //改爲常引用
if(n/2 == 0){
return false;
}else{
return true;
}
}
由於在函數體中不會修改 n 的值,所以可以用 const 限制 n,這樣一來,下面的函數調用就都是正確的了:
純文本複製
int a = 100;
isOdd(a); //正確
isOdd(a + 9); //正確
isOdd(27); //正確
isOdd(23 + 55); //正確
也就是說,編譯器只有在必要時纔會創建臨時變量。
但其實,我覺得這個可能是多此一舉,爲什麼要加引用哦?
。。。但是。。。
當引用作爲函數參數時,如果在函數體內部不會修改引用所綁定的數據,那麼請儘量爲該引用添加 const 限制。
概括起來說,將引用類型的形參添加 const 限制的理由有三個:
- 使用 const 可以避免無意中修改數據的編程錯誤;
- 使用 const 能讓函數接收 const 和非 const 類型的實參,否則將只能接收非 const 類型的實參;
- 使用 const 引用能夠讓函數正確生成並使用臨時變量。