C++string類的實現
C++中提供了一種新的資料型別——字串型別(string)。實際上string並不是C++的基本型別,它是在C++標準庫中宣告的一個字串類,用這種資料型別可以定義物件,每一個字串變數都是string類的一個物件。標準庫型別string表示可變長的字元序列,使用string型別必須首先包含它的標頭檔案。
作為標準庫的一部分,string定義在名稱空間std中。
【例】
#include<string>//注意這裡沒有.h
using namespace std;
string類的意義有兩個:第一個是為了處理char型別的陣列,並封裝了標準C中的一些字串處理的函式。而當string類進入了C++標準後,它的第二個意義就是一個容器。
string類有106個成員介面函式。C++ 的一個常見面試題是讓你實現一個 String 類,限於時間,不可能要求具備 std::string 的功能,但至少要求能正確管理資源。具體來說:
1)能像 int 型別那樣定義變數,並且支援賦值、複製。
2)能用作函式的引數型別及返回型別。
3)能用作標準庫容器的元素型別,即 vector/list/deque 的 value_type。(用作 std::map 的 key_type 是更進一步的要求,本文從略)。
下面是模擬實現string類的幾個重要函式:
1、建構函式
【例】
#define _CRT_SECURE_NO_WARNINGS
#include<string>
using namespace std;
class String
{
public:
String(char *str = "")
{
if (str == NULL)
{
_str = new char[1];//為了與delete[]配合使用
*_str = '\0';
size = 0;
}
else
{
int length = strlen(str);
_str = new char[length + 1];
strcpy(_str, str);
size = length;
}
}
~String()
{
if (NULL != _str)
{
delete[] _str;//delete[]釋放的空間必須是動態分配的
}
}
private:
char *_str;//指向字串的指標
size_t size;//儲存當前字串長度
};
void Test2()
{
String s1("hello");
String s2(new char[3]);
}
int main()
{
Test2();
system("pause");
return 0;
}
釋:在建構函式中,_str被初始化為空字串(只有‘\0’)而不是NULL。因為C++中的任何字串的長度至少為1(即至少包含一個結束符‘\0’)。孔字串也是有效的字串,它的長度為1,因此他代表一塊合法的記憶體單元而不是NULL。
2、拷貝建構函式
1)淺拷貝
【例】
String::String(const String& s)//淺拷貝
:_str(s._str)
{}
void Test2()
{
String s1("hello");
String s2(s1);
}
以上程式碼會使系統崩潰。因為s1只是將_str的地址傳給s2中的_str,即淺拷貝(位拷貝),如下圖所示:
由圖可知:s1和s2指向同一塊記憶體,析構時先對s2釋放“hello”這塊空間,當要再析構s1時,系統將崩潰。
釋:淺拷貝又稱為位拷貝,編譯器只是將指標的內容拷貝過來,導致多個物件共用一塊記憶體空間,當其中任意物件將這塊空間釋放之後,當再次訪問時將出錯。
解決方法:(深拷貝)給要拷貝構造的物件重新分配空間。
【例】
String::String(const String& s)//深拷貝
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
size = strlen(s._str);
}
void Test2()
{
String s1("hello");
String s2(s1);
}
其執行的狀態如圖所示:
由圖可知:拷貝的物件s2中_str的值(字串的地址)和s1物件中的_str的值不同,“hello”儲存在地址不同的兩個空間裡,說明了系統為物件重新開闢了空間——深拷貝。
其工作原理如圖所示:
3、拷貝賦值函式
【例】
//方法一:
String& String::operator=(const String& s)
{
if (this != &s)//檢查自賦值
{
delete[]_str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
size = strlen(s._str);
}
return *this;//為了支援鏈式訪問
}
//方法二:
String& String:: operator=(const String& s)
{
if (this != &s)//1)檢查自賦值
{//2)建立臨時變數獲得分配的記憶體空間,並複製原來的內容
char *tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[]_str;//3)釋放原有的記憶體
_str = s._str;
size = strlen(s._str);
}
return *this;//4)返回本物件引用
}
其執行狀態如圖所示:
分析:一般情況下,上面的兩種寫法都可以,但是相對而言,第二種更優一點。
方法一,先釋放原有的空間,但是如果下面用new開闢新空間時失敗了,而這時將s2賦值給s3,不僅不能成功賦值(空間開闢失敗),還破壞了原有的s3物件。
方法二,先開闢新空間,將新空間的地址賦給一個臨時變數,就算這時空間開闢失敗,也不會影響原本s3物件。
綜上所述:第二種方法更優一點。
注意:最後的返回值是為了支援鏈式訪問。
例如:s3 = s2 = s1;
4、拷貝建構函式的現代寫法:
【例】
String::String(const String& s)
:_str(NULL)//一定要對_str初始化
{
String tmp(s._str);
std::swap(tmp._str, _str);
}
釋:如果沒有初始化,_str的值很可能是一個隨機值,其指向的記憶體空間是不合法的。當tmp._str和_str交換後析構tmp就會出錯。
5、賦值運算子過載函式的兩種現代寫法
【例】
//第一種:
String& String::operator=(String s)
{
std::swap(_str, s._str);
return *this;
}
//第二種:
String& String::operator=(const String& s)
{
if (this->_str != s._str)
{
String tmp(s);
std::swap(tmp._str, _str);
}
return *this;
}
在面試時,一般寫出上面四個string類的成員函式即可,除非面試官特別要求。