目錄
- 前情提要
- Member functions —— 成員函數
- 構造函數
- 拷貝構造函數
- 賦值運算符重載
- 析構函數
- Element access —— 元素訪問
- Iterator —— 迭代器
- Capacity —— 容量
- size
- capacity
- clear
- empty
- reserve
- resize
- Modifiers —— 修改器
- push_back
- append
- operator+=(char ch)
- operator+=(const char* s)
- 在pos位置插入n個字符
- 刪除pos位置的n個元素
- swap
- String Operations —— 字符串操作
- 從pos位置開始找指定的字符
- 從pos位置開始找指定的字符串(找子串)
- 從pos位置開始取len個有效字符(取子串)
- 流插入和流提取
- 流插入
- 流提取
- swap函數解析
- 源碼
前情提要
因為我們接下來實現的類和和庫中std命名空間的string類的類名相同,所以我們為了防止沖突,用一個bit命名的命名空間解決這個問題
namespace bit
{class string {public://...private:size_t _size;size_t _capacity;char* _str;};
}
- 接下去呢,就在測試的test3.cpp中包含一下這個頭文件,此時我們才可以在自己實現的類中去調用一些庫函數
#include <iostream>
#include <assert.h>
using namespace std;#include "string.h"
Member functions —— 成員函數
構造函數
- 這里我們把構造函數的聲明放在string.h的頭文件中,然后用分文件編寫的設計在stirng.cpp中實現構造函數的源代碼
string::string():_str(new char[1] {'\0'}) , _size(0), _capacity(0)
{}
- 我們利用構造函數的初始化列表來初始化類的3個成員變量,_str表示存儲的string的內存空間,_size表示string對象的長度,_capacity表示這個string 對象的占用空間是多少個字節
或許有的兄弟會有疑問,為什么初始化_str的時候,開辟_str的空間要用 new char[1]而不是new char,這是因為我們string的數據存儲都是連續存儲在一起的,用\0標識結束位置,如果用new char那么單獨在各個不連續的空間并不能讓每個數據后面都有\0,所以我們new char[]連續的空間,存儲在一起,在這塊連續的空間后放上\0。
- 然后我們立即來測試一下,因為我們自己實現的 string類 是包含在了命名空間bit中的,那么我們在使用這個類的時候就要使用到 域作用限定符::
bit::string s1;
接下來有了無參的構造函數,我們再重載一個有參的字符串構造函數
string::string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}
- 先說一下為什么初始化列表我值初始化了_size
- 看到了嗎?不是,哥們,怎么_size都還沒初始化就去把_str初始化了,這個時候問題就不來了嗎,開的空間大小就是隨機的一個值。
- 那為什么會出現這個情況呢?
- 這個我在類和對象的文章里面講過,初始化列表要按照類中成員變量的聲明順序來初始化,很明顯_str這個變量比_size這個變量先聲明,所以這里就會出現先初始化_str變量需要用_size,但是_size還沒有初始化的問題
綜上,這就是為什么在初始化列表初始只初始化_size的原因,這樣保證一個變量的順序一定是正確的。其余的變量在函數體里面初始化, 當然第一個無參構造也可以這么寫,我這里提出來這個寫法就是想說明注意這個問題。
拷貝構造函數
- 在類和對象的文章中,我講過,一個對象拷貝給另一個對象,拷貝構造函數對于內置類型不做處理,對于自定義類型那么就要調用那個被拷貝對象的默認構造函數來拷貝給另一個對象。那么這個時候就會發生淺拷貝的問題。
- 所謂的淺拷貝,就是如上面一樣, s2是s1的拷貝,但是這種拷貝他不會讓s1指向原來的空間,s2指向新開辟的空間,淺拷貝只是把s1中3個成員變量的值拷貝過去,并不會執行開空間操作,這時候兩個對象的_str的成員變量都指向一個空間,就會導致其中一個對象已經析構了這塊空間,另一個對象銷毀的時候又對這塊已經被析構了的空間造成二次析構,這相當于就是內存泄露,一塊空間已經還給操作系統了,我們還通過一個指針來訪問這塊空間,這時候編譯器就會報錯了。
- 所以我們就要自定義來實現一個拷貝構造函數解決,兩個指針指向同一塊空間造成兩次析構的問題。
string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
- 這段代碼,我們就單獨給_str開了和被拷貝對象一樣大的空間,然后把被拷貝對象的數據拷貝到_str中,完成了深拷貝
賦值運算符重載
對于賦值運算符重載這一塊我們知道它也是屬于類的默認成員函數,如果我們自己不去寫的話類中也會默認地生成一個
- 但是呢默認生成的這個也會造成一個 淺拷貝 的問題。看到下面圖示,我們要執行s1 = s3,此時若不去開出一塊新空間的話,那么s1和s3就會指向一塊同一塊空間,此時便造成了下面這些問題
1.又會造成上面拷貝構造的問題,兩次析構
2.因為他們指向同一塊空間,修改一個對象就會影響另一個對象
3.原先s1所指向的那塊空間沒人維護了,就造成了內存泄漏的問題
string& string::operator = (string& s){if (this != &s){delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}}
- 和拷貝構造一樣,但是多了的是我們要把被賦值的對象的_str內存釋放了,不然那塊空間就沒人管了,就會導致內存泄露。
- 再補充一點這里為啥要判斷this != &s ,就是自己給自己賦值,也許大部分人不會這么做,但是不一定有人會用錯賦值,就像汽車的車窗防夾功能一樣,沒人會主動讓車窗夾,但是總有些特殊情況。
下面我們來寫一個現代的懶人寫法
string& string::operator = (string s)
{if (this != &s){swap(s);return *this;}
}
- 是不是非常的簡潔,這個就是用了一個swap函數來解決,來現在我們就來詳細說一下這個swap函數是怎么完成賦值拷貝的。
- 這就完美的完成了我們s1得到了s2的數據,又把原來s1的空間釋放了
- 附上swap的源代碼,就是和我們模擬的一樣,改變了_str的指向
string::string(const string& s)
{string tmp(s.c_str());swap(tmp);
}
- 那么拷貝構造也可以利用這個swap函數來完成拷貝給this對象的問題
析構函數
最后的話就是析構函數這一塊,前面在調試的過程中我們已經看到很多遍了,此處不再細述
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
Element access —— 元素訪問
- 嘿嘿,這個接口可是非常的好用,讓我們string可以像數組一樣使用,爽得很。
char& string::operator[](size_t pos)
{assert(pos >= 0 && pos < _size);return _str[pos];
}
- 因為我們string底層封裝的用來存儲數據的變量就是一個char*的_str指針,我們在C語言階段說過,指針是可以像數組那樣使用的,所以直接返回_str的pos位置元素就行了,另外注意pos一定是在有效的范圍內,所以斷言一下
// 可讀不可寫
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
- 這個版本就是提供給我們const的string對象使用的
那現在有人問,所以這玩意實現來有什么用?不急,請看VCR
- 是不是很輕松就拿到了string對象中的每個數據
- 看到沒,可以像數組一樣更改sting對象的數據
所以非常的方便,const對象一樣用法,就不展示了。
Iterator —— 迭代器
- 當然,除了[]可以訪問我們的string對象的數據,迭代器也可以
-而對于迭代器而言我們也是要去實現兩種,一個是非const的,一個則是const的
typedef char* iterator;
typedef const char* const_iterator;
- 這里的話我就實現一下最常用的【begin】和【end】,首位的話就是_str所指向的這個位置,而末位的話則是_str + _size所指向的這個位置
iterator begin()
iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}
- const迭代器就是照葫蘆畫瓢了,只需要重載就行了
const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}
Capacity —— 容量
size
- 首先是size, 這個接口非常的簡單,就是統計一下元素個數就行了. 因為不會改變this變量,所以加上一個 const
size_t size() const
{return _size;
}
capacity
- 對于 capacity() 也是同樣的道理
size_t capacity() const
{return _capacity;
}
clear
- 這個注意只清理元素個數,不會清理內存空間。我們直接在_str[0]這個位置放上一個\0即可,并且再去修改一下它的_size = 0即可
void clear()
{_str[0] = '\0';_size = 0;
}
empty
- 對于 empty() 來說呢就是對象中沒有數據,那么使用0 == _size即可
bool empty() const
{return 0 == _size;
}
reserve
- 當我們構造函數初始化開辟的空間內存不夠的時候,我們就要進行擴容。
// 擴容(修改_capacity)
void reserve(size_t newCapacity = 0)
{// 當新容量大于舊容量的時候,就開空間if (newCapacity > _capacity){// 1.以給定的容量開出一塊新空間char* tmp = new char[newCapacity + 1];// 2.將原本的數據先拷貝過來strcpy(tmp, _str);// 3.釋放舊空間的數據delete[] _str;// 4.讓_str指向新空間_str = tmp;// 5.更新容量大小_capacity = newCapacity;}
}
resize
1.如果n小于_size,那么就把元素個數調整到n就行
2.如果n大于_size小于_capacity,那么把元素個數調整到n,但是不擴容,把多的內存初始為\0
3.如果這個 n > _size 的話,我們便要選擇去進行擴容了
void string::resize(size_t n, char ch)
{if (n > _size){if (n > _capacity){reserve(n);}for (size_t i = _size; i < n; i++){_str[i] = ch;}}else{_size = n;_str[n] = '\0';}
}
Modifiers —— 修改器
push_back
- 首先明確一點,我們只要是要一直插入數據,就需要足夠大的空間,需要空間我們要動態的擴容,這里我們設置默認空間4, 每次擴容兩倍
- 最后擴容完,我們就可以安心插入數據了,但是要特別注意在尾插完加上一個\0
void string::push_back(char ch){//插入之前,檢查是否需要擴容if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;_str[_size] = '\0';}
append
- 接下來我們要追加的是一個字符串,首先我們要先計算出這個字符串的長度,然后在我們原有的元素個數上加上這個字符串的長度,如果總長度大于我們的內存再擴容到滿足裝下這個字符串的長度,否則兩倍擴容即可,有效利用空間。
void string::append(const char* str){size_t len = strlen(str);if (_size + len >= _capacity){int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;if (NewCapacity < _size + len){NewCapacity = _size + len;}reserve(NewCapacity);}int j = 0;for (size_t i = _size; i <= _size + len; i++){_str[i] = str[j++];}_size += len;}
operator+=(char ch)
- 首先的話是去【+=】一個字符,這里我們直接復用前面的push_back()接口即可,最后因為【+=】改變的是自身,所以我們return *this,那么返回一個出了作用域不會銷毀的對象,可以采取 引用返回 減少拷貝
string& operator+=(char ch)
{push_back(ch);return *this;
}
operator+=(const char* s)
- 而對于【+=】一個字符串,我們則是去復用前面的append()即可
string& operator+=(const char* s)
{append(s);return *this;
}
在pos位置插入n個字符
void string::insert(size_t pos, size_t n, char ch){assert(pos <= _size);if (_size + n >= _capacity){int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;if (NewCapacity < _size + n){NewCapacity = _size + n;}reserve(NewCapacity);}int j = _size;for (size_t i = _size + n; i >= pos + n ; i--){_str[i] = _str[j--];}j = 0;for (size_t i = pos; i <pos + n; i++){_str[i] = ch;}_size += n;}- 首先計算我們插入n個字符后的長度,如果大于內存,我們就需要擴容。然后
刪除pos位置的n個元素
void string::erase(size_t pos, size_t len){assert(pos <= _size);int j = pos;if (len != npos){for (size_t i = pos + len; i <= _size; i++){_str[j++] = _str[i];}_size -= len;}else{_str[pos + 1] = '\0';_size = pos + 1;}}
其實就是從pos + len 個位置的元素開始往前覆蓋,就行,然后減少長度,在遍歷的時候因為我們是遍歷到長度的位置就不會遍歷到長度之外的字符,然后后面插入的時候也能覆蓋
swap
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
String Operations —— 字符串操作
從pos位置開始找指定的字符
- 從0開始遍歷到長度的位置查找有沒有指定的字符,如果有返回下標,沒有返回npos
size_t find(char ch, size_t pos) const
{assert(pos <= _size);for (size_t i = 0; i < _size; i++){if (_str[i] == ch){return i;}}return npos;
}
- npos是一個在const靜態成員變量的無符號整數,用于標識沒查找到指定的內容,原則來說靜態成員變量應該在類外初始化,因為他要給類的所以對象使用,可是這里在缺省參數的位置初始化,這個位置原本是留給初始化列表中沒初始化成功的,但是這是一個例外.條件必須是const常量 并且整形的數據才能這樣寫
從pos位置開始找指定的字符串(找子串)
- 這可以使用strstr子串查找函數,如果返回指針為空就沒有,否則會返回子串的起始地址,如果我們想要得到子串在_str中的指針距離起始位置指針-指針即可
size_t find(const char* s, size_t pos) const
{assert(pos < _size);char* tmp = strstr(_str, s);if (tmp){// 指針相減即為距離return tmp - _str;}return npos;
}
從pos位置開始取len個有效字符(取子串)
string string::substr(size_t pos, size_t len)
{assert(pos < _size);if (len > (_size -= pos)){len = _size - pos; }string sub;sub.reserve(len);for (size_t i = 0; i <= len; i++){sub += _str[pos + i];}return sub;
}
- 如果截取的長度大于_size - = pos,從pos開始的總長度,最多只能截取到 _size - pos,后面就復用sub += 尾插了
流插入和流提取
流插入
- 如果不想寫s1 << cout這種就需要吧this這個在類中固定第一個位置的參數放在后面去,這樣我們就不能重載在類中,重載在全局中
ostream& operator<<(ostream& os, const string& s)
{for (size_t i = 0; i < s.size(); i++){os << s[i];}return os;
}
流提取
istream& operator>>(istream& is, string& s)
{s.clear(); // 對原來的字符串清理,重新輸入char ch;ch = is.get();char buffer[256];int i = 0;while (ch != ' ' && ch != '\n'){buffer[i++] = ch;if (i == 255){buffer[i] = '\0';s += buffer;i = 0;}ch = is.get();}if (i > 0){buffer[i] = '\0';s += buffer;}return is;
}
- 1.這里輸入ch一定要用流提取的get函數,不然cin 和scanf會直接忽略空格和\n, 不會寫入到ch里面導致不能結束,get就不會忽略
2.這里用buffer數組的原因是:盡量減少擴容的次數和空間浪費的情況
3.所以我們使用一個數組buffer暫存里面,如果buffer存儲滿了再擴容,這時候只需要擴容一次,而且buffer是在棧上開辟的,是可以這個函數結束就銷毀了
swap函數解析
- 為啥要自己寫一個成員函數swap?可以看一下算法庫swap的過程
源碼
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
namespace bit
{class string{public:typedef char* iterator;typedef const char* const_iterator;string();string(const char* str);string(const string& s);~string();void reserve(size_t n);iterator begin();iterator end();char& operator[](size_t pos);const char& operator[](size_t pos) const;void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void insert(size_t pos, size_t n, char ch);void erase(size_t pos, size_t len = npos);void clear();void swap(string& s);size_t size() const;const_iterator begin() const;const_iterator end() const;void push_back(char ch);void append(const char* str);void resize(size_t n, char ch = '\0');size_t find(char c, size_t pos = 0);size_t find(const char* str, size_t pos = 0);string& operator+=(char ch);string& operator+=(const char* str);string& operator = (string s);string substr(size_t pos, size_t len = npos);const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;public:static const size_t npos = -1;};ostream& operator<<(ostream& os, const string& s);istream& operator>>(istream& is, string& s);istream& getline(istream& is, string& s, char delim = '#');void swap(string& s1, string& s2);
}
string.cpp
#include"String.h"
#include<iostream>
using namespace std;
namespace bit
{typedef char* iterator;typedef const char* const_iterator;string::string():_str(new char[1] {'\0'}) //不能初始化為null實現c_str的時候防止cout對空指針解引用, _size(0), _capacity(0){}string::string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}//傳統寫法//string::string (const string& s)//{// _str = new char[s._capacity + 1];// strcpy(_str, s._str);// _size = s._size;// _capacity = s._capacity;//}string::string(const string& s){string tmp(s.c_str());swap(tmp);}string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}void string:: reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}iterator string::begin(){return _str;}char& string::operator[](size_t pos){assert(pos >= 0 && pos < _size);return _str[pos];}void string::resize(size_t n, char ch){if (n > _size){if (n > _capacity){reserve(n);}for (size_t i = _size; i < n; i++){_str[i] = ch;}}else{_size = n;_str[n] = '\0';}}const char& string::operator[](size_t pos) const{assert(pos >= 0 && pos <= _size);return _str[pos];}size_t string::size() const{return _size;}iterator string::end(){return _str + _size;}const_iterator string::begin() const{return _str;}const_iterator string::end() const{return _str + _size;}void string::push_back(char ch){//插入之前,檢查是否需要擴容if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;_str[_size] = '\0';}void string::append(const char* str){size_t len = strlen(str);if (_size + len >= _capacity){int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;if (NewCapacity < _size + len){NewCapacity = _size + len;}reserve(NewCapacity);}int j = 0;for (size_t i = _size; i <= _size + len; i++){_str[i] = str[j++];}_size += len;}void string::insert(size_t pos, char ch) {assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}for (int i = _size + 1; i > pos; i--){_str[i] = _str[i - 1];}_str[pos] = ch;_size++;_str[_size] = '\0';}void string::insert(size_t pos, size_t n, char ch){assert(pos <= _size);if (_size + n >= _capacity){int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;if (NewCapacity < _size + n){NewCapacity = _size + n;}reserve(NewCapacity);}int j = _size;for (size_t i = _size + n; i >= pos + n ; i--){_str[i] = _str[j--];}j = 0;for (size_t i = pos; i <pos + n; i++){_str[i] = ch;}_size += n;}void string::insert(size_t pos, const char* str){assert(pos <= _size);int len = strlen(str);if (_size + len >= _capacity){int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;if (NewCapacity < _size + len){NewCapacity = _size + len;}reserve(NewCapacity);}int j = _size;for (int i = _size + len ; i >= pos + len; i--){_str[i] = _str[j--];}j = 0;for (int i = pos; i < pos + len; i++){_str[i] = str[j++];}_size += len;}void string::erase(size_t pos, size_t len){assert(pos <= _size);int j = pos;if (len != npos){for (size_t i = pos + len; i <= _size; i++){_str[j++] = _str[i];}_size -= len;}else{_str[pos + 1] = '\0';_size = pos + 1;}}void string::clear(){_size = 0;_str[0] = '\0';}size_t string::find(char c, size_t pos){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}size_t string::find(const char* str, size_t pos){assert(pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}/*string& string::operator = (string& s){if (this != &s){delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}}*/string& string::operator = (string s){if (this != &s){swap(s);return *this;}}string string::substr(size_t pos, size_t len){assert(pos < _size);if (len > (_size -= pos)){len = _size - pos; }string sub;sub.reserve(len);for (size_t i = 0; i <= len; i++){sub += _str[pos + i];}return sub;}ostream& operator<<(ostream& os, const string& s){for (size_t i = 0; i < s.size(); i++){os << s[i];}return os;}istream& operator>>(istream& is, string& s){s.clear(); // 對原來的字符串清理,重新輸入char ch;ch = is.get();char buffer[256];int i = 0;while (ch != ' ' && ch != '\n'){buffer[i++] = ch;if (i == 255){buffer[i] = '\0';s += buffer;i = 0;}ch = is.get();}if (i > 0){buffer[i] = '\0';s += buffer;}return is;}istream& getline(istream& is, string& s, char delim){s.clear(); // 對原來的字符串清理,重新輸入char ch;ch = is.get();char buffer[256];int i = 0;while ( ch != delim){buffer[i++] = ch;if (i == 255){buffer[i] = '\0';s += buffer;i = 0;}ch = is.get();}if (i > 0){buffer[i] = '\0';s += buffer;}return is;}void string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}void swap(string& s1, string& s2){s1.swap(s2);}
}
這就是stirng的底層模擬實現,看完去實現一下吧