?摘要:
本文實現了一個簡易的string類,主要包含以下功能:
1. 默認成員函數:構造函數(默認/參數化)、拷貝構造、賦值重載和析構函數,采用深拷貝避免內存問題;
2. 迭代器支持:通過char*實現begin()/end()迭代器;
3. 容量操作:size()/capacity()獲取大小容量,reserve()/resize()調整空間;
4. 字符串修改:push_back()、append()、insert()、erase()等操作;
5. 訪問操作:重載operator[]和c_str()方法;
6. 實用功能:find()查找、substr()子串;
7. 運算符重載:關系運算符、流操作符<<和>>。類內部使用動態分配的char數組存儲字符串,通過_size和_capacity管理空間。實現時特別注意了深拷貝、邊界檢查和內存管理,基本模擬了標準string類的核心功能。
目錄
?摘要:
實現框架
一、默認成員函數
1.默認構造函數
2.構造函數
3.拷貝構造函數(重點)
4.賦值運算符重載函數
5.析構函數
二、迭代器相關函數
begin和end
三、容量和大小相關函數
size和capacity
reserve和resize
四、與修改字符串相關的函數
push_back
append
operator+=
insert?
erase
clear
五、訪問字符串相關函數
operator[ ]
c_str
小知識點- npos
find函數
substr函數
六、關系運算符重載函數
>、>=、<、<=、==、!=
>>和<<運算符的重載
實現框架
#include<iostream>
#include<assert.h>
using namespace std;namespace lzg
{class string{public://typedef char* iterator;using iterator = char*;using const_iterator = const char*;//一、默認成員函數string(const char* str = ""); //默認構造string(const string& s); //拷貝構造string& operator=(const string& s); //賦值重載~string(); //析構函數//二、迭代器相關函數iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}//三、容量和大小相關函數void reserve(size_t n);size_t size() const{return _size;}size_t capacity() const{return _capacity;}void resize(size_t n, char ch = '\0');//四、與修改字符串相關的函數void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len = npos);void clear(){_str[0] = '\0';_size = 0;}//五、訪問字符串相關函數char& operator[](size_t i){assert(i < _size);return _str[i];}const char& operator[](size_t i) const{assert(i < _size);return _str[i];}const char* c_str() const{return _str;}size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);string substr(size_t pos, size_t len = npos);private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;public:static const size_t npos;};//六、關系運算符重載函數bool operator== (const string& lhs, const string& rhs);bool operator!= (const string& lhs, const string& rhs);bool operator> (const string& lhs, const string& rhs);bool operator< (const string& lhs, const string& rhs);bool operator>= (const string& lhs, const string& rhs);bool operator<= (const string& lhs, const string& rhs);ostream& operator<<(ostream& os, const string& str);istream& operator>>(istream& is, string& str);
}
一、默認成員函數
1.默認構造函數
string():_str(new char[1] {'\0'}),_size(0),_capacity(0)
{}
為_str開辟1字節空間來存放'\0'_capacity不記錄\0的大小,這樣調用c_str()就不會報錯
2.構造函數
string(const char* str):_size(strlen(str))
{_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);
}
注意在private中的聲明順序,這里初始化列表只走_size(大小為str的長度),讓_str和_capacity在函數體內初始化(這樣保證了不會出錯,比如三者都在初始化列表的話,會先初始化_str這樣有風險),同樣的,_str開辟空間時,為'\0'多開辟1字節
構造函數有一個默認構造就行了,寫1個就行,把1注釋掉,并把2改為全缺省
string(const char* str="")//不要寫成" "這是非空:_size(strlen(str))
{_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);
}
3.拷貝構造函數(重點)
在模擬實現拷貝構造函數前,我們應該首先了解深淺拷貝:
我們不寫拷貝構造,編譯器默認生成的拷貝構造是值拷貝也叫淺拷貝
淺拷貝:拷貝出來的目標對象的指針和源對象的指針指向的內存空間是同一塊空間。其中一個對象的改動會對另一個對象造成影響。
深拷貝:深拷貝是指源對象與拷貝對象互相獨立。其中任何一個對象的改動不會對另外一個對象造成影響。
很明顯,我們并不希望拷貝出來的兩個對象之間存在相互影響,因此,我們這里需要用到深拷貝。
//s2(s1) s1string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_capacity = s._capacity;_size = s._size;}
這里還有一個現代(偷懶)寫法
string(const string& s){string tmp(s._str);swap(tmp);}
現代寫法與傳統寫法的思想不同:先構造一個tmp對象,然后再將tmp對象與拷貝(s)對象的數據交換即可。
4.賦值運算符重載函數
與拷貝構造函數類似,賦值運算符重載函數的模擬實現也涉及深淺拷貝問題,我們同樣需要采用深拷貝
再強調一下賦值和拷貝的區別,賦值是兩個已創建的對象之間完成賦值操作,拷貝是指一個已創建的對象調用拷貝構造生成一個新的對象
// s1 = s3 s3
string& operator=(const string& s)
{if (this != &s)//避免自己給自己賦值{delete[] _str; //釋放掉s1的舊空間,開辟一個和s3容量的新空間_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;
}
同樣的賦值也有現代寫法
// 現代寫法// s1 = s3string& operator=(string s){swap(s);return *this;}
賦值運算符重載函數的現代寫法是通過采用“值傳遞”接收右值的方法,讓編譯器自動調用拷貝構造函數,然后我們再將拷貝出來的對象與左值進行交換即可
5.析構函數
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
二、迭代器相關函數
string類中的迭代器實際上就是字符指針,只是給字符指針起了一個別名叫iterator而已。
typedef char* iterator;
typedef const char* const_iterator;
begin和end
iterator begin()
{return _str; //返回字符串中第一個字符的地址
}const_iterator begin()const
{return _str; //返回字符串中第一個字符的const地址
}iterator end()
{return _str + _size; //返回字符串中最后一個字符的后一個字符的地址
}const_iterator end()const
{return _str + _size; //返回字符串中最后一個字符的后一個字符的const地址
}
其實范圍for的底層就是調用了begin()和end()迭代器,當你模擬實現了迭代器,范圍for自然就能使用
string s("hello world!!!");
//編譯器將其替換為迭代器形式
for (auto e : s)
{cout << e << " ";
}
cout << endl;
注:自己寫的begin()和end()函數名必須是這樣的,不能有任何大小寫否則范圍for就報錯
三、容量和大小相關函數
size和capacity
因為string類的成員變量是私有的,我們并不能直接對其進行訪問,所以string類設置了size和capacity這兩個成員函數,用于獲取string對象的大小和容量。
size函數用于獲取字符串當前的有效長度(不包括’\0’)。
size_t size() const
{return _size;
}
capacity函數用于獲取字符串當前的容量。(不包括’\0’)。
size_t capacity() const
{return _capacity;
}
reserve和resize
reserve和resize這兩個函數的執行規則一定要區分清楚。
reserve規則:
?1、當n大于對象當前的capacity時,將capacity擴大到n或大于n。
?2、當n小于對象當前的capacity時,什么也不做。
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//多開一個空間用于存放'\0'strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}
resize規則:
1、當n大于當前的size時,將size擴大到n,擴大的字符為ch,若ch未給出,則默認為’\0’。
2、當n小于當前的size時,將size縮小到n。
//改變大小
void resize(size_t n, char ch = '\0')
{if (n <= _size) {_size = n; _str[_size] = '\0'; }else {if (n > _capacity) {reserve(n); }for (size_t i = _size; i < n; i++) {_str[i] = ch;}_size = n; _str[_size] = '\0'; }
}
四、與修改字符串相關的函數
push_back
void push_back(char c)
{if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = c; // 在結尾位置寫入新字符(覆蓋原'\0')_size++; // 增加長度_str[_size] = '\0'; // 在新結尾補終止符
}
實現push_back還可以直接復用下面即將實現的insert函數。
//尾插字符
void push_back(char ch)
{insert(_size, ch); //在字符串末尾插入字符ch
}
append
//尾插字符串
void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){size_t newCapacity = 2 * _capacity;// 擴2倍不夠,則需要多少擴多少if (newCapacity < _size + len)newCapacity = _size + len;reserve(newCapacity);}//避免尾插字符串時如果字符串大于二倍_capacity時的多次擴容strcpy(_str + _size, str);_size += len;
}
operator+=
+=運算符的重載是為了實現字符串與字符、字符串與字符串之間能夠直接使用+=運算符進行尾插。
+=運算符實現字符串與字符之間的尾插直接調用push_back函數即可。
//+=運算符重載
string& operator+=(char ch)
{push_back(ch); //尾插字符串return *this; //返回左值(支持連續+=)
}
+=運算符實現字符串與字符串之間的尾插直接調用append函數即可。
//+=運算符重載
string& operator+=(const char* str)
{append(str); //尾插字符串return *this; //返回左值(支持連續+=)
}
insert?
insert函數的作用是在字符串的任意位置插入字符或是字符串。
void insert(size_t pos, char ch);
void insert(size_t pos,const char* str);
void insert(size_t pos, char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t end = _size;while (end >=(int)pos){_str[end + 1] = _str[end];end--;}_str[pos] = ch;_size++;
}
insert函數插入字符串時也是類似思路
void insert(size_t pos, const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){size_t newCapacity = 2 * _capacity;// 擴2倍不夠,則需要多少擴多少if (newCapacity < _size + len)newCapacity = _size + len;reserve(newCapacity);}size_t end = _size + len;while (end > pos + len - 1){_str[end] = _str[end - len];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}
erase
erase函數的作用是刪除字符串任意位置開始的n個字符。
void erase(size_t pos, size_t len)
{assert(pos < _size);if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{ //從后往前挪size_t end = pos + len;while (end <= _size){_str[end-len] = _str[end];//覆蓋pos及后len長度的值,完成刪除end++;}_size -= len;}
}
clear
clear函數用于將對象中存儲的字符串置空,實現時直接將對象的_size置空,然后在字符串后面放上’\0’即可
//清空字符串
void clear()
{_size = 0; //size置空_str[_size] = '\0'; //字符串后面放上'\0'
}
swap
有三個swap,有兩個是string里的swap函數,另外一個是算法庫的swap函數
std::swap template <class T> void swap (T& a, T& b);std::string::swap
void swap (string& str);void swap (string& x,sring& y);
算法庫中的swap也能交換自定義類型,但代價非常大
我們自己實現swap完全不用這么復雜,直接把s1、s2指針及數據交換一下就行了
//交換兩個對象的數據
void swap(string& s)
{//調用庫里的swap::swap(_str, s._str); //交換兩個對象的C字符串::swap(_size, s._size); //交換兩個對象的大小::swap(_capacity, s._capacity); //交換兩個對象的容量
}
還有一個swap存在的意義是什么呢?
void swap(string& s1,string& s2)
{s1.swap(s2);
}
這樣在類外面調用swap函數時就不會調到算法庫的swap函數了(模板實例化會走上面的swap函數)
五、訪問字符串相關函數
operator[ ]
[ ]運算符的重載是為了讓string對象能像C字符串一樣,通過[ ] +下標的方式獲取字符串對應位置的字符。
//[]運算符重載(可讀可寫)
char& operator[](size_t i)
{assert(i < _size); //檢測下標的合法性return _str[i]; //返回對應字符
}
在某些場景下,我們可能只能用[ ] +下標的方式讀取字符而不能對其進行修改。例如,對一個const的string類對象進行[ ] +下標的操作,我們只能讀取所得到的字符,而不能對其進行修改。所以我們需要再重載一個[ ] 運算符,用于只讀操作。
//[]運算符重載(可讀可寫)
const char& operator[](size_t i)const
{assert(i < _size); //檢測下標的合法性return _str[i]; //返回對應字符
}
c_str
c_str函數用于獲取對象C類型的字符串
//返回C類型的字符串
const char* c_str()const
{return _str;
}
小知識點- npos
在 C++ 標準庫中,?npos
? 是一個特殊的靜態常量成員,主要用于表示“無效位置”或“未找到”的狀態
注:只有整形才能在聲明中定義
static const size_t npos=-1;
??核心用途
(1) ?查找函數失敗時的返回值
std::string str = "Hello";
size_t pos = str.find('x'); // 查找不存在字符
if (pos == std::string::npos) { // 必須用 npos 檢查std::cout << "Not found!";
}
(2) ?表示“直到字符串末尾”?
std::string sub = str.substr(2, std::string::npos); // 從索引2到末尾
特性
-
?無符號值?:由于是?
size_t
類型,避免直接與?-1
比較,而應使用?npos
。? -
?足夠大?:其值(如 18446744073709551615)保證不會與任何有效索引沖突。
find函數
find函數是用于在字符串中查找一個字符或是字符串
find函數:正向查找即從字符串開頭開始向后查找
1、正向查找第一個匹配的字符。
size_t find(char ch,size_t pos=0)
{//默認從首字符開始查找assert(pos<_size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}//找不到返回nposreturn npos;
}
2、正向查找第一個匹配的字符串。
這里用到了一個strstr函數
char * strstr (char * str1, const char * str2 );返回指向 str1 中第一次出現的 str2 的指針,如果 str2 不是 str1 的一部分,則返回空指針。
size_t find(const char* str, size_t pos = 0)
{assert(pos < _size);const char* ptr = strstr(_str, str);if (ptr)//如果找到了{return ptr - _str; //下標}else //為空{return npos;}
}
substr函數
substr函數用來取字符串中的子字符串,位置和長度由自己決定
默認從首字符取,默認取的長度是取到尾
string substr (size_t pos = 0, size_t len = npos) const;調用拷貝構造,返回pos位后len長度的字符串
string substr(size_t pos, size_t len)
{assert(pos < _size);//len長度足夠大就直接取到尾if (len > _size - pos){len = _size - pos;}string sub;sub.reserve(len);for (size_t i = pos;i < len; i++){sub += _str[pos + i];}return sub;
}
六、關系運算符重載函數
>、>=、<、<=、==、!=
關系運算符有 >、>=、<、<=、==、!= 這六個,但是對于C++中任意一個類的關系運算符重載,我們均只需重載其中的兩個,剩下的四個關系運算符可以通過復用已經重載好了的兩個關系運算符來實現。
例如,對于string類,我們可以選擇只重載 <?和 == 這兩個關系運算符。
注:重載在類外面,命名域例:lzg里。這樣操作數就不用限定死是string類的
?
lhs
?:代表 ?Left-Hand ?Side (左側操作數/左側值)?
rhs
?:代表 ?Right-Hand ?Side (右側操作數/右側值)
//==運算符重載
bool operator== (const string& lhs, const string& rhs)
{return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}//<運算符重載
bool operator< (const string& lhs, const string& rhs)
{return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}
剩下的四個關系運算符的重載,就可以通過復用這兩個已經重載好了的關系運算符來實現了。
//<=運算符重載
bool operator<= (const string& lhs, const string& rhs)
{return lhs < rhs || lhs == rhs;
}//!=運算符重載
bool operator!= (const string& lhs, const string& rhs)
{return !(lhs == rhs);
}//>運算符重載
bool operator> (const string& lhs, const string& rhs)
{return !(lhs <= rhs);
}//>=運算符重載
bool operator>= (const string& lhs, const string& rhs)
{return !(lhs < rhs);
}
>>和<<運算符的重載
>>運算符的重載
ostream& operator<<(ostream& os, const string& str)
{for (size_t i = 0; i < str.size(); i++){os << str[i];}return os;
}
<<運算符的重載
istream& operator>>(istream& is, string& str)
{str.clear();char ch;//is >> ch;ch = is.get();while (ch != ' ' && ch != '\n'){str += ch;ch = is.get();}return is;
}
參考鏈接
C++string接口