【C++】string的底層原理及實現

文章目錄

  • string類的存儲結構
  • 默認成員函數
    • 構造函數
    • 析構函數
    • 拷貝構造函數
    • 賦值重載
  • 容量操作
    • size()
    • capacity()
    • reserve()
    • resize()
    • clear()
  • 遍歷與訪問
    • operator[ ]
    • 迭代器范圍與for
  • 增刪查改
    • push_back()
    • pop_back()
    • append()
    • operator+=
    • insert()
    • erase()
    • c_str()
    • find()
    • substr()
  • 非成員函數
    • operator+
    • 關系運算符
    • 流插入<<和流提取>>
    • getline()

本篇參考C++string類參考手冊,實現一些string的常用接口,接口原型在該網站查閱。

string類的存儲結構

string類的底層實際上是char類型的順序表,所以結構上也比較相似。

namespace lw
{class string{public:static const size_t npos;private:size_t _size;//有效數據個數size_t _capacity;//可存儲的容量char* _str;//指向字符串起始位置的地址};const size_t string::npos = -1;
};

STL源碼中,許多整型變量類型都是size_t無符號整型(沒有負值);npos是string類常用到的一個值,有些函數的參數或者返回值是npos,并且這個值設為-1(表示232-1),參考標準庫中的定義。
在這里插入圖片描述

為什么string定義在一個命名空間中?
我們如果展開了標準庫using namespace std; 那string默認使用的就是標準庫中的。如果不展開標準庫,那么所有的cin和cout都需要加上std:: 比較繁瑣。

用一個命名空間對我們自己實現的string類進行封裝,可以方便我們隨時測試代碼,與庫里的string類進行測試對比,只需要改::前面的作用域即可。

#include <iostream>
using namespace std;
int main()
{string s("hello world");//默認標準庫std::string s("hello world");//標準庫lw::string s("hello world");//自己定義的類cout << s << endl;return 0;
}

string與C語言中的char類型字符串的一個較大的區別就是:char類型的字符串是以\0為結束符,遇到\0就停止;而string是以_size來判斷是否結束,遇到\0并不會停止。 這點很重要,一定要弄清楚!


默認成員函數

常用的四個默認成員函數,一般情況我們不需要顯示定義,使用編譯成生成的即可;但string類涉及到資源申請,所以我們必須自己顯示定義。

構造函數

官網的參考手冊中給的string標準庫有很多構造函數重載,我們只實現常用的兩個構造。這兩個我們可以合并成一個實現。

string();
string(const char* s);

缺省值不能給nullptr,一是因為strlen無法計算空指針,二是因為無參標準庫默認給的就是空串。
這里strcpy和memcpy都可以,因為是對char類型字符串進行拷貝,拷貝str在\0之前的內容。用memcpy只是為了和后面的寫法統一。

string(const char* str = ""): _size(strlen(str)), _capacity(strlen(str)), _str(new char[strlen(str) + 1])//多一個空間給'\0'
{//strcpy(_str, str);//strcpy也可以,只是為了與后面統一,換成memcpymemcpy(_str, str, _size + 1);//'\0'也要拷貝
}

我們在new新空間時,每次多new一個空間給\0留位置。 memcpy在拷貝時,多拷貝一個字節把末尾\0也拷貝過去。


析構函數

~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}

拷貝構造函數

深淺拷貝問題
編譯器默認生成的是淺拷貝,就是只將s._str的指針地址拷貝給了新對象的_str,兩個對象指向同一塊地址! 那么就會出問題:

1.一塊空間最后會析構兩次,程序必然崩潰;
2.一個對象修改,另一個對象也會修改;

所以我們需要進行深拷貝,重新申請一塊空間,把s._str指向地址的內容拷貝過來。

string(const string& s)
{_str = new char[s.capacity() + 1];//多一個空間給'\0'//strcpy(_str, s._str);//遇到'\0'終止,后面內容無法拷貝memcpy(_str, s._str, s._size + 1);//末尾'\0'也拷貝過來_size = s._size;_capacity = s._capacity;
}

注意:這里不能用strcpy來拷貝數據,因為strcpy的拷貝結束條件是遇到\0終止,而string對象是可以存儲\0的,strcpy不會將\0后面內容拷貝過來,所以要用memcpy或者memmove按照字節進行拷貝,多拷貝一個字節是將末尾的\0也拷貝過來。


賦值重載

與拷貝構造原理類似,但要先將申請的空間存放在臨時變量里,防止空間開辟失敗丟失原數據。原始空間別忘了釋放!

string& operator=(const string& s)
{if (this != &s){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;//釋放原始空間_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;//支持連續賦值
}

第二種寫法:我們可以利用庫里的swap函數將兩個對象_str指向的地址和數據個數、容量完全交換。順便將swap接口也實現了。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
//現代寫法
string& operator=(string s)//不能傳引用也不能加const
{swap(s);return *this;
}

注意:不能影響右操作數實參的值,所以右操作數傳參只能以傳值方式,且不能加const,否則不能交換改變。并且我們不需要進行自己給自己賦值的判斷,因為s是實參的拷貝,一定與原對象地址不同。


容量操作

size()

size_t size() const
{return _size;
}

capacity()

size_t capacity() const
{return _capacity;
}

reserve()

只擴容不縮容,指定n大于原始容量則進行擴容,否則不進行操作。
拷貝時還是同樣要注意\0問題,不能用strcpy。

void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//strcpy(tmp, _str);//錯誤,無法拷貝'\0'后面的內容memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}
}

resize()

resize()只改變有效數據個數,不會改變容量。
n > _size:縮小長度為n,多余內容刪掉。
n < _szie:增加長度到n,剩下空間用給定的字符參數c填充,無參默認補充\0

void resize (size_t n);
void resize (size_t n, char c);

庫里的resize()函數有兩個版本,同樣我們可以合成一個版本,第二個參數給缺省值\0即可。

void resize(size_t n, char ch = '\0')
{assert(n >= 0);if (n < _size)//縮小長度,后面刪掉{_size = n;_str[_size] = '\0';}else{reserve(n);//reserve會檢查容量for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}
}

clear()

void clear()
{_size = 0;_str[0] = '\0';
}

遍歷與訪問

operator[ ]

需要實現兩個版本:普通對象調用[ ]可讀可寫,const對象調用[ ]只可讀不可寫。

char& operator[](size_t pos)//可讀可寫
{assert(pos < _size);return _str[pos];
}
const& char operator[](size_t pos) const//可寫
{assert(pos < _size);return _str[pos];
}

迭代器范圍與for

迭代器不一定是指針,而string的迭代器我們可以用指針來模擬實現。將指針重命名為迭代器。

typedef char* iterator;//普通迭代器 可讀可寫
iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}typedef const char* const_iterator;//const迭代器 只可讀
const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}

左閉右開區間,所以begin()返回字符串起始位置,end()返回末尾字符的下一個位置\0

范圍for
實現迭代器后,我們就可以使用范圍for了。范圍for是C++11的新語法,它的底層是傻瓜式地替換成迭代器,支持迭代器就支持范圍for。

int main()
{lw::string s1("hello world");lw::string::iterator it = s1.begin();while (it != s1.end()){*it += 1;cout << *it << ' ';it++;}cout << endl;for (auto ch : s1){cout << ch << ' ';}cout << endl;
}

增刪查改

push_back()

末尾要放\0

void push_back(char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 10 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}

pop_back()

void pop_back()
{assert(_size > 0);_size--;_str[_size] = '\0';
}

append()

append()在末尾追加字符串,空間不夠則擴容,如果每次將_capacity擴2倍,一方面可能造成空間浪費,另一方面可能原始空間太小導致頻繁擴容,所以我們最好提前計算好擴容的空間。

append的接口也有很多,下面給出最常用的兩種。

string& append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//strcpy(_str + _size, str);//strcat效率O(n+m) strcpy效率O(m)memcpy(_str + _size, str, len + 1);//末尾的'\0'也要拷貝_size += len;return *this;
}
string& append(const string& s)
{if (_size + s._size> _capacity){reserve(_size + s._size);}memcpy(_str + _size, s._str, s._size + 1);//末尾的'\0'也要拷貝_size += s._size;return *this;
}

operator+=

+=是string經常用到的操作符,使用非常方便。
operator+=總共有三個重載版本,我們可以直接復用push_back()和append()

string& operator+=(char ch)
{push_back(ch);return *this;
}
string& operator+=(const char* str)
{append(str);return *this;
}
string& operator+=(const string& s)
{append(s);return *this;
}

insert()

任意位置插入字符ch

string& insert(size_t pos, size_t n, char ch)
{assert(pos <= _size);if (_size + n > _capacity){reserve(_size + n);}for (size_t end = _size; end >= pos; end--){//pos==0時end會減到-1//無符號整型 end=-1時表示42億多 不加判斷會無限循環if (end == npos){break;}_str[end + n] = _str[end];}for (size_t i = 0; i < n; i++){_str[i + pos] = ch;}_size += n;return *this;
}

任意位置插入字符串str

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}for (size_t end = _size; end >= pos; end--){if (end == npos){break;}_str[end + len] = _str[end];}memcpy(_str + pos, str, len);//不能多拷貝一個字節_size += len;return *this;
}

注意:這里的memcpy不能跟之前一樣多拷貝一個字節,否則會將原對象pos+1的字符替換成\0,會出錯。


erase()

string& erase(size_t pos, size_t len = npos)
{assert(pos < _size);//沒給參數 或者 要刪除的個數>=pos后面剩下的個數 則后面包括pos位置全部刪掉if (len == npos || pos + len >= _size){//pos及pos后面所有字符都刪掉_size = pos;_str[_size] = '\0';}else{memcpy(_str + pos, _str + pos + len, len);_size -= len;}return *this;
}

c_str()

返回字符串首地址,以\0結束。這個接口主要是用來兼容C語言的。

const char* c_str() const
{return _str;
}

find()

find查找失敗會返回npos,找到則返回下標

//查找字符
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;}}return npos;
}
//查找字符串
size_t find(const char* str, size_t pos = 0)
{assert(pos < _size);const char* p = strstr(_str + pos, str);if (p){return p - _str;}else{return npos;}
}

substr()

返回子串

string substr(size_t pos = 0, size_t len = npos) const
{assert(pos < _size);size_t n = len;//子串長度if (len == npos || pos + len > _size){n = _size - pos;}string tmp;for (size_t i = 0; i < n; i++){tmp += _str[i + pos];//復用+=}return tmp;
}

非成員函數

operator+

實際上+操作符很少用,直接用+=更方便省事;這里只實現了一個接口,重載為友元函數。

string operator+(const string& s1, const string& s2)
{string tmp(s1);tmp += s2;return tmp;
}

關系運算符

C++官方手冊中,每種運算符都有3種重載版本,這里每個只實現了一種,重要的是理解本質,加深對string的理解。

為什么用C語言的memcmp函數進行字節上的比較,不用strcmp?
因為strcmp遇到\0終止,而string不看\0,以_size為終止。
當然也可以不用memcmp,遍歷每個字符進行比較。

bool operator==(const string& s1, const string& s2)
{return s1._size == s2._size && memcmp(s1._str, s2._str, min(s1._size, s2._size)) == 0;
}
bool operator<(const string& s1, const string& s2)
{int ret = memcmp(s1._str, s2._str, min(s1._size, s2._size));return ret == 0 ? s1._size < s2._size : ret < 0;
}//直接復用
bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}
bool operator<=(const string& s1, const string& s2)
{return (s1 < s2) || (s1 == s2);
}
bool operator>(const string& s1, const string& s2)
{return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{return !(s1 < s2);
}

流插入<<和流提取>>

流插入的實現可以借助范圍for,本質是遍歷整個字符串打印每個字符。

ostream& operator<<(ostream& out, const string& s)
{//out << s._str;//'\0'后面的內容無法打印for (auto ch : s){out << ch;}return out;//帶返回值 支持連續輸出
}

流提取需要注意幾種情況:
1.每次讀取之前需要先清空string對象,否則會疊加之前的內容。
2.為什么用get()讀取而不用>>?
流提取>>默認是跳過空格和換行的,所以>>永遠無法讀取空格和換行,程序會一直運行一直可以輸入。
3.如果字符串前面有空格或者換行,標準庫的>>會默認清理空格和換行。所以要預先處理前面的空格和換行。

istream& operator>>(istream& in, string& s)
{s.clear();//清空之前的內容char ch = in.get();//可以讀取空格和換行//處理掉緩沖區前面的空格或者換行while (ch == ' ' || ch == '\n'){ch = in.get();}while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

getline()

getline可以自定義讀取的分隔符,遇到分隔符就不再讀取,字符參數默認為\n,換行截斷。

istream& getline(istream& in, string& s, char delim = '\n')
{s.clear();//清空之前的內容char ch = in.get();while (ch != delim){s += ch;ch = in.get();}return in;
}

整體代碼->:string模擬實現代碼

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/42309.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/42309.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/42309.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

c#的List<T>的SelectMany 和Select

在C#中&#xff0c;List<T>&#xff08;以及任何實現了IEnumerable<T>的集合&#xff09;的Select和SelectMany擴展方法都是LINQ&#xff08;Language Integrated Query&#xff09;的一部分&#xff0c;用于對集合中的元素進行查詢和轉換。 盡管它們的作用有些相…

Virtualbox和ubuntu之間的關系

1、什么是ubuntu Ubuntu 是一個類似于 Windows 的操作系統&#xff0c;但它是基于 Linux 內核開發的開源操作系統 2、什么是Virtualbox VirtualBox 是一款虛擬機軟件&#xff0c;使我們可以物理機上創建和運行虛擬機 也就是說,VirtualBox 提供了一個可以安裝和運行其他操作系…

力扣考研經典題 反轉鏈表

核心思想 頭插法&#xff1a; 不斷的將cur指針所指向的節點放到頭節點之前&#xff0c;然后頭節點指向cur節點&#xff0c;因為最后返回的是head.next 。 解題思路 1.如果頭節點是空的&#xff0c;或者是只有一個節點&#xff0c;只需要返回head節點即可。 if (head null …

算法訓練營day70

題目1&#xff1a;108. 冗余連接 (kamacoder.com) #include<iostream> #include<vector>using namespace std;int n; vector<int> father(10001, 0);void init() {for(int i 1;i < n;i) father[i] i; }int find(int u) {return u father[u] ? u : fa…

Echarts中的熱力圖和漏斗圖(在Vue中使用熱力圖和漏斗圖)

熱力圖 (Heatmap) Echarts的熱力圖用于展示兩個維度數據矩陣中的值分布情況。它通過在平面上劃分成多個矩形區域&#xff0c;并用不同的顏色填充這些區域來表示數據的大小或強度。顏色漸變從淺到深通常映射著數值從小到大&#xff0c;從而直觀展示數據的集中程度和分布模式。熱…

Mojolicious測試驅動開發:單元與集成測試的藝術

標題&#xff1a;Mojolicious測試驅動開發&#xff1a;單元與集成測試的藝術 Mojolicious是一個現代化的Perl Web開發框架&#xff0c;它不僅提供了強大的Web應用開發能力&#xff0c;還內置了豐富的測試工具來支持單元測試和集成測試。本文將深入探討如何在Mojolicious中進行…

人工智能期末復習簡答題

簡答: 子句集的化簡(1).消去連接詞”—>”和”<—>” (2)減少否定符號的轄域 (3)對變元標準化 (4)化為前束范式 (5)消去存在量詞 (6)化為Skolem標準形 (7)消去全稱量詞 (8)消去合取詞 (9)更換變元名稱 2.在狀態空間搜索中,Open表與Close…

半同步主從復制

半同步主從復制的概念 半同步主從復制&#xff08;Semisynchronous Replication, SBR&#xff09;是MySQL數據庫中的一種數據復制方式&#xff0c;它在異步復制的基礎上增加了一定程度的同步性&#xff0c;旨在提高數據安全性&#xff0c;減少數據丟失的風險。 半同步主從復制…

LeetCode 3101.交替子數組計數:等差數列求和(較詳題解)

【LetMeFly】3101.交替子數組計數&#xff1a;等差數列求和&#xff08;較詳題解&#xff09; 力扣題目鏈接&#xff1a;https://leetcode.cn/problems/count-alternating-subarrays/ 給你一個二進制數組 nums 。 如果一個子數組中 不存在 兩個 相鄰 元素的值 相同 的情況&a…

階段三:項目開發---大數據開發運行環境搭建:任務8:安裝配置Redis

任務描述 知識點&#xff1a;安裝配置Redis 重 點&#xff1a; 安裝配置Redis 難 點&#xff1a;無 內 容&#xff1a; Redis&#xff08;Remote Dictionary Server )&#xff0c;即遠程字典服務&#xff0c;是一個開源的使用ANSI C語言編寫、支持網絡、可基于內存亦可…

【C++:運算符重載】

運算符重載 特點&#xff1a; 函數名由operator運算符組成 注&#xff1a; 不能通過其他符號創建新的操作符&#xff0c;只能使用C/C語法存在的操作符重載操作符必須有一個類類型參數&#xff0c;原因&#xff1a;不能重載操作符改變內置類型的行為當類成員操作符重載時&#…

web自動化(五)上傳文件

我們需要準備一個上傳文件的html&#xff0c;創建a.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>文件上傳示例</title> </head> <body><form action"/upload"…

電路基礎知識匯總

1.0 串連&#xff0c;并聯&#xff0c;混連 串聯的定義 電路串聯是一種電路元件的連接方式&#xff0c;其中各個元件沿著單一路徑互相連接&#xff0c;形成一個連續的鏈。在串聯電路中&#xff0c;每個節點最多只連接兩個元件&#xff0c;這意味著電流只有一條路徑可以通過整個…

Apache Seata Mac下的Seata Demo環境搭建

本文來自 Apache Seata官方文檔&#xff0c;歡迎訪問官網&#xff0c;查看更多深度文章。 本文來自 Apache Seata官方文檔&#xff0c;歡迎訪問官網&#xff0c;查看更多深度文章。 Mac下的Seata Demo環境搭建&#xff08;AT模式&#xff09; 前言 最近因為工作需要&#xf…

Git教程

文章目錄 Git分布式版本控制工具版本控制器的方式常用命令遠程倉庫Tip Git分布式版本控制工具 ? Git是一個開源的分布式版本控制系統&#xff0c;可以有效、高速地處理從很小到非常大的項目版本管理。 ? Git是分布式的&#xff0c;Git不需要有中心服務器&#xff0c;我們每…

【感謝告知】本賬號內容調整,聚焦于Google賬號和產品的使用經驗和問題案例分析

親愛的各位朋友&#xff1a; 感謝您對本賬號的關注和支持&#xff01; 基于對朋友們需求的分析和個人興趣的轉變&#xff0c;該賬號從今天將對內容做一些調整&#xff0c;有原來的內容改為Google&#xff08;谷歌&#xff09;賬號和產品的使用經驗&#xff0c;以及相關問題的…

24西安電子科技大學經濟與管理學院—考研錄取情況

24西安電子科技大學—經理與管理學院—考研錄取統計 01、經理與管理學院各個方向 02、24經濟與管理近三年復試分數線對比 1、經管院24年院線相對于23年院線普遍下降2-15分&#xff0c;個別專業上漲4-10分。 2、經管院應用經濟學2024年院線350分&#xff1b;管理科學與工程院線…

java join與yield方法

join() join() 方法的主要作用是使當前線程&#xff08;調用 join() 方法的線程&#xff09;等待目標線程完成執行。當目標線程執行完畢后&#xff0c;當前線程才會繼續執行。 代碼示例&#xff1a; public class JoinExample {public static void main(String[] args) {Thr…

保研復習 | 數據結構

目錄 CH1?緒論☆ 數據項、數據元素、數據結構☆ 邏輯結構和存儲結構的區別☆ 順序存儲結構和鏈式存儲結構的比較☆ 算法的重要特性☆ 算法的復雜度 CH2?線性表☆ 單鏈表 CH3?棧、隊列和數組☆ 棧和堆是什么&#xff1f;☆ 棧在括號匹配中的應用☆ 棧在表達式求值中的應用☆ …

Linux|信號

Linux|信號 信號的概念信號處理的三種方式捕捉信號的System Call -- signal 1.產生信號的5種方式2.信號的保存2.1 core 標志位 2.信號的保存2.1 對pending 表 和 block 表操作2.2 阻塞SIGINT信號 并打印pending表例子 捕捉信號sigaction 函數驗證當前正在處理某信號&#xff0c…