STL簡介+string模擬實現

STL簡介+string模擬實現

  • 1. 什么是STL
  • 2. STL的版本
  • 3. STL的六大組件
  • 4.STL的缺陷
  • 5. string
    • 5.1 C語言中的字符串
    • 5.2 1個OJ題
  • 6.標準庫中的string類
    • 6.1 string類(了解)
    • 6.2 string類的常用接口說明
      • 1.string類對象的常見構造函數
      • 2.析構函數(~string())
      • 3.賦值函數 (operator=)
    • 6.3 string類對象的容量操作
      • 1.size()、length()、capacity()、clear()
      • 2.string 在擴容方面是怎么樣擴容
      • 3.reserve() ->void reserve (size_t n = 0);
      • 4.resize()
      • 5.縮容
    • 6.4 string類對象的訪問及遍歷操作
      • 1. operator[]和迭代器(iterator) 遍歷string
      • 2.begin() 和 end()
      • 3.rbegin() 和 rend()
      • 4.at()
    • 6.5 string類對象的修改操作
      • 1.push_back()、append()、operator+=
      • 2.insert()
      • 3.erase()
      • 4.c_str()
      • 5.data()、copy() -- 不常用
      • 6.find() - 查找
      • 7.substr
    • 6.6 string類非成員函數
      • 1.operator>>
      • 2.operator<<
      • 3.getline
      • 4.to_string 將int 轉換string
  • 7. 牛刀小試
    • 917.僅僅反轉字母
    • 387.字符串中的第一個唯一字符
    • 125.驗證回文串
    • 415.字符串相加
  • 8.string 模擬實現
    • string.h
    • string.cpp
    • test.cpp
  • 9.淺拷貝
  • 10.深拷貝
  • 11.寫時拷貝(了解)

1. 什么是STL

STL(standard template libaray-標準模板庫):是C++標準庫的重要組成部分,不僅是一個可復用的組件庫,而且是一個包羅數據結構與算法的軟件框架。

2. STL的版本

  • 原始版本
    Alexander Stepanov、Meng Lee 在惠普實驗室完成的原始版本,本著開源精神,他們聲明允許任何人任意運用、拷貝、修改、傳播、商業使用這些代碼,無需付費。唯一的條件就是也需要向原始版本一樣做開源使用。 HP 版本–所有STL實現版本的始祖。

  • P. J. 版本
    由P. J. Plauger開發,繼承自HP版本,被Windows Visual C++采用,不能公開或修改,缺陷:可讀性比較低,符號命名比較怪異。

  • RW版本
    由Rouge Wage公司開發,繼承自HP版本,被C+ + Builder 采用,不能公開或修改,可讀性一般。

  • SGI版本
    由Silicon Graphics Computer Systems,Inc公司開發,繼承自HP版 本。被GCC(Linux)采用,可移植性好,可公開、修改甚至販賣,從命名風格和編程 風格上看,閱讀性非常高。我們后面學習STL要閱讀部分源代碼,主要參考的就是這個版本。

3. STL的六大組件

在這里插入圖片描述

4.STL的缺陷

  1. STL庫的更新太慢了。這個得嚴重吐槽,上一版靠譜是C++98,中間的C++03基本一些修訂。C++11出來已經相隔了13年,STL才進一步更新。
  2. STL現在都沒有支持線程安全。并發環境下需要我們自己加鎖。且鎖的粒度是比較大的。
  3. STL極度的追求效率,導致內部比較復雜。比如類型萃取,迭代器萃取。
  4. STL的使用會有代碼膨脹的問題,比如使用vector/vector/vector這樣會生成多份代碼,當然這是模板語
    法本身導致的。

5. string

string 本質就是串,它是一個字符數組。只是這個數組可以擴容,可以增刪查改。string 本質上是用的非常多的,大家想一想,數據類型本質上就是存各種各樣的數據,整形,浮點型是表示數據大小,還有一些更符合的信息都是用string存的,比如說身份證號,它是不能用整形存儲的,第一 這是表示大小范圍的問題。第二個 有些身份證號碼帶X,那只能用字符串存。名字、地址 都需要用字符串存。現實生活中有很多東西都用字符串存儲,所以string挺重要的。
string 其實是一個類模板,默認string是管理char數組的。
在這里插入圖片描述
也能管理其他的,比如說還有一個叫wchar_t,它是兩個字節。
在這里插入圖片描述
也有4個字節的,char32_t 就是4個字節的。
在這里插入圖片描述
當然我們平時這個階段也接觸不到。那C語言有沒有串呢?
C語言也有自己的串,它其實是面向過程的實現方式,數據和方法是分離的。數據是你自己管理,空間是你自己管理,方法庫里面給你提供了。如下:

5.1 C語言中的字符串

C語言中,字符串是以’\0’結尾的一些字符的集合,為了操作方便,C標準庫中提供了一些str系列的庫函數,但是這些庫函數與字符串是分離開的,不太符合OOP(面向對象程序設計)的思想,而且底層空間需要用戶自己管理,稍不留神可能還會越界訪問。
就給大家做個最簡單的比方,有一個C語言的函數叫strcpy ,strcat 。strcpy 是不是把一塊空間拷貝到另一塊空間,那這兩塊空間是不是 你都要自己提供,并且拷貝到那塊空間你得保證它倆是一樣大的,或者說至少比它大,如果目標空間比源空間小就會存在越界,strcpy 不管這些,是你在copy要注意的,它數據和方法是分離的,就會有很多的問題。用起來就挺煩,既要管空間,又要管方法。strcat是在當前串追加,它有兩個很挫的地方, 第一個是它會從前到尾找\0,這就效率很低了,如果前面這個串很長,那它找\0就有消耗,第二個,空間要自己準備好,要有足夠的空間。所以C語言這種方法是不好用的。 如果你用string串之后,你就再也不想用C語言這種方法了。

5.2 1個OJ題

字符串相加

在這里插入圖片描述
在OJ中,有關字符串的題目基本以string類的形式出現,而且在常規工作中,為了簡單、方便、快捷,基本都使用string類,很少有人去使用C庫中的字符串操作函數。

6.標準庫中的string類

6.1 string類(了解)

string類的文檔介紹

  1. 字符串是表示字符序列的類
  2. 標準的字符串類提供了對此類對象的支持,其接口類似于標準字符容器的接口,但添加了專門用于操作單字節字符字符串的設計特性。
  3. string類是使用char(即作為它的字符類型,使用它的默認char_traits和分配器類型(關于模板的更多信息,請參閱basic_string)。
  4. string類是basic_string模板類的一個實例,它使用char來實例化basic_string模板類,并用char_traits和allocator作為basic_string的默認參數(根于更多的模板信息請參考basic_string)。
  5. 注意,這個類獨立于所使用的編碼來處理字節:如果用來處理多字節或變長字符(如UTF-8)的序列,這個類的所有成員(如長度或大小)以及它的迭代器,將仍然按照字節(而不是實際編碼的字符)來操作。

總結:
1.string是表示字符串的字符串類
2.該類的接口與常規容器的接口基本相同,再添加了一些專門用來操作string的常規操作。
3. string在底層實際是:basic_string模板類的別名,typedef basic_string<char, char_traits, allocator> string;
4. 不能操作多字節或者變長字符的序列。
在使用string類時,必須包含#include< string >以及using namespace std;
在這里插入圖片描述

6.2 string類的常用接口說明

1.string類對象的常見構造函數

在這里插入圖片描述
string() ;無參構造 -> string s1
string(const string& s) ;拷貝構造 -> string s2(s1)
string(const string& s,size_t pos,size_t len = npos) ;拷貝s從pos位置len個字符初始化。
string (const char* s) ;c風格字符串構造 ->string(“hello world”);
string(const char* s,size_t n);取c風格字符串前n個字符初始化
string(size_t n,char c); 用n個c字符初始化。

在這里插入圖片描述
OK,話不多說 ,接下來看下面代碼:
string s1(“hello world”) 會調用C風格字符串構造。
string s2 = “hello world”; //單參數函數 -> 隱式類型轉換
“hello world”調用string(const char*s) 構造一個string類型的臨時對象,再用這個臨時對象拷貝構造s2. 構造+拷貝構造->直接構造
const string& s3 = “hello world”;
"hello world"構造一個string的臨時對象,又因為臨時對象具有常性,s3引用的是臨時對象,所以加const.

在這里插入圖片描述
看完了上述代碼 ,這個時候大家就可以理解一個東西了。
假設我們寫一個push_back,假設push_back一個string,是指其他數據結構,比如說 鏈表,順序表 push_back。我們調用push_back 以前要寫有名對象,定義一個有名對象,或者匿名對象,但是還有如下最方便的寫法。
在這里插入圖片描述

2.析構函數(~string())

在這里插入圖片描述
析構函數底層是把空間給釋放掉,要清理資源,它是自動調用的。

3.賦值函數 (operator=)

在這里插入圖片描述
在這里插入圖片描述

6.3 string類對象的容量操作

在這里插入圖片描述

1.size()、length()、capacity()、clear()

測試代碼1:
在這里插入圖片描述

2.string 在擴容方面是怎么樣擴容

下面有個問題是:string 在擴容方面是怎么樣擴容的。
若s1 小于 15,直接存s1對象_Buf數組中。
大于 15,讓s1對象下的指針開空間擴容存,存在堆上。
vs 擴容 1.5倍擴,2倍擴,這些都是不確定的。根據編譯器。
其次capacity 比實際空間少一個,有一個多的是預留給\0的(g++,vs)
如何擴容,C++標準并沒有規定,取決于編譯器實現

下面代碼能測試如何擴容
在這里插入圖片描述

在這地方有個原則是第一次是個2倍擴容,后面是1.5倍,為什么第一次是2倍,后面又是1.5倍,其實跟之前那個結構有關系,string里面小于16的存在它內部的_Buf數組里面,然后數組滿了,第一次要擴容至少要開32字節,上面capacity = 15, 嚴格來說不算擴容,只能算在堆上開空間。下面給大家演示看一下。
在這里插入圖片描述
s1小于16存在_Buf上,也就是它比較小,它其實沒存在堆上,存在對象本身上,對象本身放個數組,當你不斷插入數據或者你直接放大于16的string,這個時候它的數據就不存在_Buf上,存在指針_Ptr指向的空間,這個空間其實就是堆空間,所以說第一次嚴格,來說不算擴容,因為它不是對已有的空間擴容,它是從一個地方存在另一個地方,你可以認為VS的這個設計是一種一空間換時間的方法,效率會高一點點,但是處理復雜程度會麻煩一點點,小于一定程度,它不想去內存中開那么多的小空間,開很多小空間,效率上有一定的影響,其次,就是說,會有一些內存碎片這樣的問題。
下面是在Linux環境下的擴容,不同平臺下的擴容還是有差異的。
在這里插入圖片描述
從上圖同樣的函數插入,不同平臺的擴容差異還是很大的。
OK,下面我們來看

在這里插入圖片描述

3.reserve() ->void reserve (size_t n = 0);

reserve 通常功能是預分配內存空間,以避免頻繁的動態內存分配,從而提高性能
1.vector 和 string 在動態增長時,如果當前容量(capacity())不足,會重新分配更大的內存塊,并拷貝原有數據到新內存(O(n) 操作)。reserve(n) 提前分配至少能容納 n 個元素的內存,減少后續 push_back()、emplace_back() 或 insert() 時的擴容次數。
2.reserve(n) 不會改變容器中的元素數量(size() 不變),只是調整底層內存的容量(capacity())。如果 n ≤ 當前 capacity(),reserve() 可能什么都不做(取決于實現)。
3.reserve() 僅分配未初始化的內存,不會調用元素的構造函數
4.reserve() 不會減少內存,如需縮減內存,可用 shrink_to_fit()(C++11)。
5.reserve() 不適用于 std::list、std::map 等非連續內存容器。
6.reserve 開的空間只影響capacity(),不影響size,若
string s;
s.reserve(200); 此時 size = 0; capacity = 207;
s[100] = ‘x’; 會越界, [ ]會調用底層operator[ ] 檢查100是不是會小于size 。
resize 可以用[ ] 訪問,reserve 不可以

下面是在VS上演示的:
在這里插入圖片描述
reserve參數小于15,就是把堆上的空間釋放,就把它拷貝到_Buf上面,所以說,小于15,它會縮,其他堅決不縮。
linux 上的reserve 給什么就是什么。
在這里插入圖片描述
所以 reserve 會擴容,但不一定縮容,不同的平臺有可能縮也有可能不縮。
reserve有什么意義呢?reserve其實一般我們也不用它縮容,因為縮容也不一定好,其次擴容是有代價的,比如說這有一塊空間,我們需要在這個地方開另外一個空間,擴容,比如說,擴2倍,拷貝數據,在釋放空間舊的空間,這是標準的擴容動作,原地擴容是很少的,很少能實現原地擴容。那我們看到,我插入200個數據不斷不斷的擴容,其實是有很大的成本,所以這地方有什么解決方案嗎?有,就是我知道我要插入200個數據我可以用reserve提前開空間。
在這里插入圖片描述
上面reserve完了之后,最容易犯的錯誤是什么?很多人reserve完了之后,就開始訪問空間。
在這里插入圖片描述

上面操作不能實現,[ ]會調用底層operator[ ] 檢查100是不是會小于size,reserve只會改變我們的capacity,不會改變我們的size.空間改了,也就是說不會影響我們的數據。
在這里插入圖片描述

4.resize()

下面來看我們的resize.
在這里插入圖片描述
void resize(size_t n,char c) //開 n個位置,每個默認位置給c
假設我想訪問這些空間,比如說我加完的結果是76543,我想挨個挨個去放。這個時候就不能用reserve,reserve 只是開了空間,不影響size ,所以就不能用下標訪問,這個時候就可以用resize.
在這里插入圖片描述
如果resize不傳第二個默認參數
s1.resize(5) //此時填的是默認字符\0(空字符)
在這里插入圖片描述

resize其他功能:
插入數據
在這里插入圖片描述
刪除數據
在這里插入圖片描述

5.縮容

如果想要縮容的話,可以用
在這里插入圖片描述
這個接口是讓它的capacity 與size保持一致。比如說我空間開的很大很大,我插入很多數據,后面又刪刪,刪了半天,我覺的空間浪費的有點多,我想把我的空間給釋放下,那就可以調這個接口。但是不要經常調這個東西,這個東西不好。
縮容的本質是以時間換空間。開一塊比之前更小的空間,把數據拷貝過來,把原來的空間釋放掉。所以不要輕易的縮容,代價挺大的。
注意:

  1. size()與length()方法底層實現原理完全相同,引入size()的原因是為了與其他容器的接口保持一致,一般情況下基本都是用size()。
  2. clear()只是將string中有效字符清空,不改變底層空間大小。
  3. resize(size_t n) 與 resize(size_t n, char c)都是將字符串中有效字符個數改變到n個,不同的是當字符個數增多時:resize(n)用0來填充多出的元素空間,resize(size_t n, char c)用字符c來填充多出的元素空間。注意:resize在改變元素個數時,如果是將元素個數增多,可能會改變底層容量的大小,如果是將元素個數減少,底層空間總大小不變。
  4. reserve(size_t res_arg=0):為string預留空間,不改變有效元素個數,當reserve的參數小于string的底層空間總大小時,reserver不會改變容量大小。

6.4 string類對象的訪問及遍歷操作

在這里插入圖片描述

1. operator[]和迭代器(iterator) 遍歷string

在這里插入圖片描述

第一個階段的普通迭代器(iterator)
在這里插入圖片描述
注:在基于范圍的for循環中,auto 常用于聲明循環變量的類型,編譯器會根據迭代器的解引用類型來推斷循環變量的類型。

迭代器:
iterator 是在類里面typedef的一個類型,屬于類域。使用的時候要指定類域
在這里插入圖片描述
在這里插入圖片描述

總結: 我們學了三種遍歷方式,他們都是可讀可寫的,從語法層是三種方式,從底層是兩種方式,底層只有下標+[]和迭代器

2.begin() 和 end()

在這里插入圖片描述
在這里插入圖片描述

在這里插入圖片描述

第二個階段的迭代器(const_iterator)
typeid(變量名).name() //打印變量類型
const iterator 迭代器本身不能修改 類似 int* const
const_iterator 迭代器指向的數據不能修改 類似const int*

在這里插入圖片描述
圖示:
在這里插入圖片描述
在這里插入圖片描述

還有一種auto的方式(用于讓編譯器自動推斷變量或函數返回類型的類型)
在這里插入圖片描述

在C++中,const string s1("hello world"); 這行代碼聲明了一個名為 s1 的常量字符串,其值為 "hello world"。這里的 const 表示這個字符串是不可修改的,即不能對 s1 進行賦值操作。
接下來的 auto it1 = s1.begin(); 這行代碼聲明了一個迭代器 it1,其類型由編譯器自動推斷。s1.begin() 返回一個指向字符串 s1 開始位置的迭代器。迭代器是一個可以遍歷容器元素的抽象概念,對于字符串來說,迭代器可以遍歷字符串中的每個字符。
簡單來說,這段代碼創建了一個不可修改的字符串,然后獲取了一個可以遍歷這個字符串的迭代器。使用迭代器,你可以訪問字符串中的每個字符,但因為字符串是常量,你不能通過迭代器修改字符串的內容。

總結: 普通迭代器它是給普通的string用的,普通對象用的,const迭代器是給const對象用的,我可以遍歷string,但是不能修改數據。

3.rbegin() 和 rend()

反向迭代器 --倒著遍歷
在這里插入圖片描述

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
下一個問題是:我們說遍歷用下標+[ ]就可以了,那其他東西呢,迭代器有沒有下標+[ ]替代不了的,有,比如說 讓s1按字典序排序。
在這里插入圖片描述
在這里插入圖片描述

4.at()

在這里插入圖片描述
訪問pos位置的字符。跟[ ] 功能是一樣的。那他們的區別是什么呢?[ ]是暴力檢查,一越界就報錯。它是拋異常,簡單來說就是下面這樣的。
在這里插入圖片描述

6.5 string類對象的修改操作

在這里插入圖片描述

1.push_back()、append()、operator+=

在這里插入圖片描述

代碼測試:
在這里插入圖片描述

2.insert()

在這里插入圖片描述
代碼測試:
在這里插入圖片描述
下標必須是合法的,只有長度可以是非法。

3.erase()

在這里插入圖片描述
string& replace(size_t pos,size_t len,const char*s);
把pos位置開始的len個字符替換c-string

在這里插入圖片描述

4.c_str()

在這里插入圖片描述
與c更好地兼容,C++要用C語言接口就調用c_str()
c_str() 被調用來獲取 string 對象 str 的C字符串表示。結果被存儲在 const char* 類型的指針 cstr 中。也可以獲取底層的指針或者獲取首元素的地址。/返回C風格字符串

在這里插入圖片描述
c_str 可以跟C更好的兼容,有可能我們調的是C語言的接口,C++兼容C,C的庫C可以用,C++也可以用。

5.data()、copy() – 不常用

在這里插入圖片描述
data的功能與c_str 功能類似。但是我們平時用c_str.
在這里插入圖片描述
我可以把第pos位置len個字符copy到char* s里面.

6.find() - 查找

默認從pos位置查找str/c-string/字符在字符串中的位置。
在這里插入圖片描述
倒著找,從后往前查找
在這里插入圖片描述

7.substr

從字符串提取子字符串
string substr(size_t pos = 0,size_t len = npos)const;
從pos位置開始提取len個長度的字符串返回

string file("string.cpp.zip");
size_t pos = file.rfind('.');
//string suffix = file.substr(pos,file.size()-pos);
string suffix = file.substr(pos);
cout<<suffix<<endl;  //.zip

左閉右開下標一減就是個數

string url("https://gitee.com/abcdedg");
size_t pos1 = url.find(':');
string url1 = url.substr(0,pos1-0);
cout<<url1<<endl;  //httpssize_t pos2 = url.find('/',pos1+3);
string url2 = url.substr(pos1+3,pos2-(pos1+3));
cout<<url2<<endl;  //gitee.comstring url3 = url1.substr(pos2+1);
cout<<url1<<endl;   //abcdedg

注意:

  1. 在string尾部追加字符時,s.push_back( c ) / s.append(1, c) / s += 'c’三種的實現方式差不多,一般情況下string類的+=操作用的比較多,+=操作不僅可以連接單個字符,還可以連接字符串。
  2. 對string操作時,如果能夠大概預估到放多少字符,可以先通過reserve把空間預留好。

6.6 string類非成員函數

在這里插入圖片描述

1.operator>>

istream& operator>>(istream& is,string& str);

2.operator<<

在這里插入圖片描述

3.getline

輸入流對象,存取的字符串,可選的分隔符
在這里插入圖片描述
cin >> 流提取與scanf一樣遇到空格或者換行就結束
默認規定空格或者換行是多個值分割
若要獲取一行中包含空格的字符串,不能用>>,要用getline(cin,str);

string str
while(cin>>str)
{cout<<str<<endl;
} //持續的獲取流中字符串

用于從標準輸入流(cin)讀取一行字符串的函數,它會讀取輸入直到遇到換行符(\n),并將結果存儲到字符串str中

4.to_string 將int 轉換string

int x = 0,y =0;
cin>>x>>y;
string str = to_string(x+y);
cout<<str<<end;int z = stoi(str); //將str轉換整形

string 可以很好的兼容utf-8,gbk;

7. 牛刀小試

917.僅僅反轉字母

題目描述:
在這里插入圖片描述
代碼:
在這里插入圖片描述

387.字符串中的第一個唯一字符

題目描述:
在這里插入圖片描述
代碼:
在這里插入圖片描述
部分代碼解析:

在這里插入圖片描述

125.驗證回文串

題目描述:
在這里插入圖片描述
代碼:
在這里插入圖片描述

415.字符串相加

題目解析:
在這里插入圖片描述
代碼1:
在這里插入圖片描述
上面代碼時間復雜度是O(N^2),不推薦用上面這種寫法。
代碼2:
在這里插入圖片描述

8.string 模擬實現

string.h

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;namespace bit
{class string{public://任何平臺里的庫都會保證typedef一個iterator,但iterator 的原生類型是char* 還是什么不確定// STL規范了任何容器提供迭代器都typedef 成iterator,他也不會重名,因為每個類都是一個獨立的域//迭代器像指針一樣的東西//這是一種封裝,把迭代器的真實類型通過在類里面typedef以后進行了封裝,// 隱藏了底層實現細節(上面進行統一化),提供了一種簡單通用訪問容器的方式// 不關心底層是什么類型,只要給迭代器就能訪問容器,把算法和數據結構隔離開,迭代器是橋梁// 算法通過不同類型的迭代器來訪問不同類型的容器來修改數據// 傳string 就推演出 string的迭代器//string iterator 不一定是char* ,不同的平臺都不同typedef char* iterator;typedef const char* const_iterator;//const 迭代器是指向的內容不能修改,本身可以修改const_iterator begin() const;const_iterator end()const; iterator begin();iterator end();//string(); 無參//string(const char* str); //帶參//無參和帶參的可以寫一個全缺省的+初始化列表//聲明和定義分離,缺省參數寫在聲明//全缺省構造函數string(const char* str = "");//string(const char* str = '\0’);   但'\0'是字符,const char* 接收的是字符串//有同學還寫成上面那個,編譯能過,'\0'隱式轉換成整形,整形隱式轉換指針,相當于空指針了,string(const string& s);   //拷貝構造//string& operator=(const string& s);string& operator=(string tmp);~string();const char* c_str()const;size_t size() const;char& operator[](size_t pos);const char& operator[](size_t pos) const;void reserve(size_t n);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 = 0, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);void swap(string& s);string substr(size_t pos = 0, size_t len = npos);bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;void clear();private :char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;//特例,因為加了const//const static size_t npos = -1;// 不支持//const static double N = 2.2;const static size_t npos;//類里面的靜態成員變量就相當于全局變量//聲明和定義分離時,.h 放聲明,.cpp放定義//靜態成員變量不會走初始化列表};istream& operator>> (istream& is, string& str);ostream& operator<< (ostream& os, const string& str);
}

string.cpp

#define  _CRT_SECURE_NO_WARNINGS 1
#include "string.h"
namespace bit
{const size_t string::npos = -1;//迭代器是在類里面typedef 的一個類型 所以iterator 也要指定類域string::iterator string::begin(){return _str;}string::iterator string::end(){return _str + _size;}string::const_iterator string::begin() const{return _str;  //_str是char* 可以給const char* 權限縮小}string::const_iterator string::end()const{return _str + _size;}//無參構造/*string::string(){	//_str = nullptr; 不能給空指針,因為調用c_str(),這個函數返回值是const char* 會按字符串打印,會解引用,不能對空指針解引用_str = new char[1] {'\0'};_size = 0;_capacity = 0;}*///錯誤寫法//string(const char* str)//		:_str(str)  不能把str給_str,//因為 str 是常量字符串,回頭沒法給_str 擴容,修改//全缺省構造函數string::string(const char* str):_size(strlen(str)){_str = new char[_size + 1];  //_str 指向申請的動態內存空間_capacity = _size;strcpy(_str, str);           //給_str指向的空間拷貝數據}//拷貝構造 如果我們不寫,是淺拷貝。1.析構會析構兩次。一塊空間不能析構兩次。// 2.一塊空間的內容改變也會影響另一個// 所以要用深拷貝解決。他們應該有各自獨立的空間//s2(s1)  s1 就是s this 就是s2//傳統寫法(實在人)/*string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}*///現代寫法(讓別人干活,我們給他交換)//s2(s1)string::string(const string& s){string tmp(s._str);  //用s1的值拷貝構造tmp/*	std::swap(tmp._str, _str);std::swap(tmp._size, _size);std::swap(tmp._capacity, _capacity);*/swap(tmp);  //s2.swap(tmp)  s2 與 tmp 交換 此時 s2 不是隨機值,因為在成員變量聲明我們給缺省值了nullptr//有的編譯器s2 給 隨機值 ,tmp 與 s2 交換,tmp 出了作用域會調析構,有些編譯器會崩。}//方法1 傳統寫法//s1 = s3//s1 = s1//string& string::operator=(const string& s)//{	//	if (this != &s)  //判斷一下,不能自己給自己賦值//	{//		char* tmp = new char[s._capacity + 1];  //開一塊新空間//		strcpy(tmp, s._str);        //把s3對象的內容拷貝給新空間里//		delete[] _str;			//釋放s1指向的空間//		_str = tmp;			// s1 指向新空間//		_size = s._size;	//		_capacity = s._capacity;//	}//	return *this;//}//方法2//現代寫法//s1 = s3  //s1 和 s3 都是已存在的對象//string& string::operator=(const string& s)//{//	if (this != &s) //	{//		string tmp(s._str);  //s 就是 s3 ,tmp 開一塊跟s3一樣大的空間//		swap(tmp);  // s1.swap(tmp)//	}//	return *this;//}//方法3 .首先拷貝構造得寫好,盡可能的復用//s1 = s3  s1.operator=(s3)(賦值運算符重載)//當執行 s1 = s3 時,tmp 是通過 拷貝構造 從 s3 初始化的(即 string tmp(s3))。傳值傳參 s3會調用拷貝構造來構造 tmpstring& string::operator=(string tmp) //不能用引用,用引用就是s1與s3交換{swap(tmp);  // s1.swap(tmp) , 交換當前對象和 tmp 的資源//tmp 局部對象,函數結束時會調用析構,完成對象中資源的清理工作。return *this;  // 返回當前對象的引用}//析構函數string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//返回值為int* 會按指針打印//const char* 會按字符串打印,會解引用,需找到\0才終止//把 C++ 的 string 變成 C 能用的臨時字符串const char* string::c_str()const{return _str;}//返回字符串的有效長度(即 _size 成員變量的值)size_t string::size() const{return _size;}//遍歷字符串//返回 char&(引用),允許通過 [] 修改字符串內容(如 s1[0] = 'H')char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}//const operator[] 不能修改字符串內容const char& string::operator[](size_t pos) const{assert(pos < _size);return _str[pos];}//預留內存空間void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}//插入一個字符void string::push_back(char ch){/*if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;_str[_size + 1] = '\0';++_size;*/ insert(_size, ch);}//插入一個字符串void string::append(const char* str){/*size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;*/insert(_size, str);}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}//在pos位置插入一個字符void string::insert(size_t pos, char ch){//插入的位置要<= size  //_size 指向\0assert(pos <= _size);if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//size_t end = _size;// //while(end >= pos)  這種寫法如果在pos等于0位置插入就越界了。因為end是size_t end = -1//是 42億多 。end >= 0 就繼續 ,小于0就出來,但是這里end不會小于0。所以可以將size_t -> int//第一種寫法 (其中下面是兩個問題)/*int end = _size;//在一個運算符,兩邊的操作數,如果他們的類型不一樣,他們會發生隱式類型轉換//當有符號遇到無符號,有符號會隱式類型轉換成無符號,所以要把pos強轉成int在去比較while (end >= (int)pos){_str[end + 1] = _str[end];--end; }*///第二種寫法 // 如果 非要把 size_t 寫成 無符號整形 那end >= pos 終止條件就是 end<0,然 < 0 就越界//有沒有一種辦法把 等號去掉 。當然有size_t end = _size+1;while (end > pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;++_size;}//在pos位置插入一個字符串void string::insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}/*int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];--end; }memcpy(_str + pos, str, len);_size += len;*/size_t end = _size+len;//while(end >= pos+len)while (end > pos+len-1) //畫圖{_str[end] = _str[end-len];--end;}memcpy(_str + pos, str, len);_size += len;}//從pos位置刪除len個字符void string::erase(size_t pos, size_t len){assert(pos < _size); //pos不可以越界//當len 大于前面字符個數時,有多少就刪多少if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{//strcpy(_str + pos, _str + pos + len);//memcpy(_str+pos,_str+pos+len,len+1);memmove(_str + pos, _str + pos + len, _size - pos - len + 1);  // 確保 '\0' 被拷貝_size -= len;}}//從 pos 位置 找 字符 chsize_t string::find(char ch, size_t pos){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}//返回子字符串首次出現的位置索引//從字符串pos位置開始找子串sub// const char* strstr(主 const char* str1,const char* str)size_t string::find(const char* sub, size_t pos){char* p = strstr(_str + pos, sub);return p - _str; //指針相減計算的是兩個指針之間的元素(char)個數。//當前數據的下標就是前面的數據個數}//s1.swap(s3)void string::swap(string& s){//沒有交換string對象//交換char* ,size,capacity ,這是內置類型交換std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);}string string::substr(size_t pos, size_t len ){//len大于后面剩余字符,有多少取多少if (len > _size - pos){string sub(_str + pos); //用位置_str+pos后面的字符直到取到\0 拿去構造// 構造一個子串返回return sub;}else{string sub;sub.reserve(len);for (size_t i = 0 ; i < len; i++){sub += _str[pos + i];}return sub;}}bool string:: operator<(const string& s)const{return strcmp(_str, s._str) < 0;}bool string::operator<=(const string& s)const{return *this < s || *this == s;}bool string::operator>(const string& s)const{return !(*this <= s);}bool string::operator>=(const string& s)const{return !(*this < s);}bool string::operator==(const string& s)const{return strcmp(_str, s._str) == 0;}bool string::operator!=(const string& s)const{return !(*this == s);}void string::clear(){_str[0] = '\0';_size = 0;}//方法1://流提取 - 要針對之前的空間進行覆蓋//get() 是 istream 類的成員函數,用于從輸入流(如 cin)中逐個讀取字符(包括空格、換行符等空白字符)//istream& operator>> (istream& is, string& str)//{//	str.clear();  //清空目標字符串//	char ch = is.get();  //is.get() 用于逐個讀取字符(包括空白符)//	//while(ch != '\n')//	while (ch != ' ' && ch != '\n')//	{//		str += ch; //如果cin提取的內容比較多,str 就不斷的+= str就會不斷的擴容,大量的擴容也不好//					// 有人給了下面這種方法//		ch = is.get();//	}//	return is;//}//方法2:// 問題:直接 str += ch 會導致 string 頻繁擴容// 優化:使用 buff[128] 先緩存字符,攢夠 127 個字符后再一次性追加到 str,減少擴容次數。//從輸入流 istream 讀取數據到 string 的功能istream& operator>> (istream& is, string& str){str.clear();  //清空目標字符串:// 使用局部緩沖區 buff[128] 減少字符串的頻繁擴容,從而提高性能//  棧上分配的緩沖區,減少動態擴容char buff[128];  //局部數組,出了作用域就銷毀了int i = 0;  // 記錄當前緩沖區位置char ch = is.get(); // 讀取第一個字符(相當于初始化ch)while (ch != ' ' && ch != '\n'){buff[i++] = ch; // 存入緩沖區if (i == 127)  // 緩沖區即將滿(留 1 位給 '\0'){buff[i] = '\0'; // 手動添加字符串結束符// 追加到目標字符串str += buff; //str一次會把空間擴容好,不用頻繁的擴容i = 0;		// 重置緩沖區索引}//在每次循環結束時讀取下一個字符,更新 ch 的值,以便下一次循環條件判斷。//如果不寫第二次 is.get():ch 的值永遠不會更新,循環會無限執行(死循環)//例如,如果第一次讀取的是字母 'a',while 條件成立,但 ch 始終是 'a',導致無限循環。ch = is.get();}if (i != 0)   // 如果緩沖區還有未處理的數據{buff[i] = '\0';  // 添加結束符str += buff;}return is;   // 支持鏈式調用,如 `cin >> s1 >> s2`}//流插入ostream& operator<< (ostream& os, const string& str){//日期類時寫友元是因為要訪問私有成員變量//下面這里沒有寫成友元也可訪問私有//一個一個字符去輸出,調用公有成員函數 for (size_t i = 0; i < str.size(); i++){os << str[i];}return os;}
}

test.cpp

#define  _CRT_SECURE_NO_WARNINGS 1
#include "string.h"
namespace bit
{void test_string1(){bit::string s1("hello world");cout << s1.c_str() << endl;  //hello worldfor (size_t i = 0; i < s1.size(); i++){//s1.operator[](i)//s[i]++cout << s1[i] << " ";   //h e l l o   w o r l d}cout << endl;   bit::string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";  //h e l l o   w o r l d++it;}cout << endl;for (auto e : s1){cout << e << " ";  //h e l l o   w o r l d }cout << endl;bit::string s2;cout << s1.c_str() << endl;  //hello worldconst bit::string s3("gwwww");bit::string::const_iterator it3 = s3.begin();while (it3 != s3.end()){//*it3 += 3;  指向的內容不能修改cout << *it3 << " ";  //g w w w w++it3;}cout << endl;for (size_t i = 0; i < s3.size(); i++){//s3[i]++;  //s3是const對象 要調用const 類型的[],不能修改字符串內容cout << s3[i] << " ";  //g w w w w}cout << endl;}void test_string2(){bit::string s1("hello world");cout << s1.c_str() << endl;s1.push_back('x');s1.append("yyyyy");cout << s1.c_str() << endl;s1 += '1';s1 += "中國";cout << s1.c_str() << endl;}void test_string3(){bit::string s1("hello world");s1.insert(5, 'x');cout << s1.c_str() << endl;s1.insert(0, 'y');cout << s1.c_str() << endl;bit::string s2("hello world");s2.insert(3, "qqqq");cout << s2.c_str() << endl;s2.insert(0, "www");cout << s2.c_str() << endl;bit::string s3("hello world");cout << s3.c_str() << endl;s3.erase(7);cout << s3.c_str() << endl;}void test_string4(){bit::string s1("hello world");cout << s1.find('o') << endl;cout << s1.find("wor") << endl;}void test_string5(){bit::string s1("hello world");bit::string s2(s1);s1[0] = 'x';cout << s1.c_str() << endl;cout << s2.c_str() << endl;bit::string s3("yyyyy");s1 = s3;cout << s1.c_str() << endl;cout << s3.c_str() << endl;bit::string s4("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");s1 = s4;cout << s1.c_str() << endl;cout << s4.c_str() << endl;s1 = s1;cout << s1.c_str() << endl;cout << s3.c_str() << endl;std::swap(s1, s3); cout << s1.c_str() << endl;cout << s3.c_str() << endl;s1.swap(s3);cout << s1.c_str() << endl;cout << s3.c_str() << endl;}void test_string6(){bit::string url("https://chat.deepseek.com/a/chat/s/e0aed764-dd58-42a7-bae3-8ea7d1902beb");size_t pos1 = url.find(":");bit::string url1 = url.substr(0, pos1 + 0);cout << url1.c_str() << endl;size_t pos2 = url.find("/", pos1 + 3);bit::string url2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));cout << url2.c_str()<< endl;bit::string url3 = url.substr(pos2 + 1);cout << url3.c_str() << endl;}void test_string7(){/*bit::string s1("hello world");cout << s1 << endl;  */bit::string s1;cin >> s1;cout << s1 << endl;}void test_string8(){//現代寫法 拷貝構造 + 賦值運算符重載bit::string s1("hello world");bit::string s2(s1);cout << s1 << endl;cout << s2<< endl;bit::string s3("xxxxx");s1 = s3;cout << s1 << endl;cout << s3 << endl;}void test_string9(){bit::string s1("hello world");bit::string s2(s1);cout << (void*)s1.c_str() << endl; //地址不一樣沒有用寫時拷貝cout << (void*)s2.c_str()<< endl;}
}
int main()
{//bit::test_string1();//bit::test_string2();//bit::test_string3();//bit::test_string4();//bit::test_string5();//bit::test_string6();//bit::test_string7();//bit::test_string8();//bit::test_string8();bit::test_string9();return 0;
}

9.淺拷貝

淺拷貝:也稱位拷貝,編譯器只是將對象中的值拷貝過來。如果對象中管理資源,最后就會導致多個對象共享同一份資源,當一個對象銷毀時就會將該資源釋放掉,而此時另一些對象不知道該資源已經被釋放,以為還有效,所以當繼續對資源進項操作時,就會發生發生了訪問違規。
可以采用深拷貝解決淺拷貝問題,即:每個對象都有一份獨立的資源,不要和其他對象共享。
在這里插入圖片描述
上述String類沒有顯式定義其拷貝構造函數與賦值運算符重載,此時編譯器會合成默認的,當用s1構造s2時,編譯器會調用默認的拷貝構造。最終導致的問題是,s1、s2共用同一塊內存空間,在釋放時同一塊空間被釋放多次而引起程序崩潰,這種拷貝方式,稱為淺拷貝。

10.深拷貝

如果一個類中涉及到資源的管理,其拷貝構造函數、賦值運算符重載以及析構函數必須要顯式給出。一般情況都是按照深拷貝方式提供。
在這里插入圖片描述

11.寫時拷貝(了解)

寫時拷貝就是一種拖延癥,是在淺拷貝的基礎之上增加了引用計數的方式來實現的。
引用計數:用來記錄資源使用者的個數。在構造時,將資源的計數給成1,每增加一個對象使用該資源,就給計數增加1,當某個對象被銷毀時,先給該計數減1,然后再檢查是否需要釋放資源,如果計數為1,說明該對象時資源的最后一個使用者,將該資源釋放;否則就不能釋放,因為還有其他對象在使用該資源。

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

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

相關文章

golang實現一個mysql中隨機獲取cookies的API

之前用FASTAPI寫了一個隨機cookies請求的接口,現在嘗試用golang實現同樣的效果 1.編寫go代碼 package mainimport ("database/sql""encoding/json""fmt"_ "github.com/go-sql-driver/mysql""log""net/http"&quo…

[Vue2組件]三角形角標

[Vue2組件]三角形角標 <template><div class"ys-subscript" :style"svgStyle"><svg width"200" height"200" viewBox"0 0 200 200" xmlns"http://www.w3.org/2000/svg"><!-- 三角形背景 - 右…

洛谷刷題4

B4354 [GESP202506 一級] 假期閱讀 題目傳送門 B4354 難度&#xff1a;入門 很簡單的題&#xff0c;如果小A看的頁數≤這本書的頁數&#xff0c;輸出他看的頁數 否則&#xff0c;輸出這本書的頁數 AC代碼&#xff1a; #include <iostream> using namespace std; in…

【基于Echarts的地圖可視化】

<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>中國牛只分布可視化</title><script src"https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js"></script><script src"h…

系統架構設計師備考之架構設計基礎

1.計算機系統基礎知識 1.1.計算機系統概述 計算機系統的定義與組成 計算機系統是指用于數據管理的計算機硬件、軟件及網絡組成的系統。 計算機系統可劃分為硬件和軟件兩部分。硬件由機械、電子元器件、磁介質和光介質等物理實體構成&#xff1b; 軟件是一系列按照特定順序組織…

針對華為云服務器使用率過大

從這兩張監控圖可以看出&#xff0c;服務器在大約上午 10:30 前后經歷了一次明顯的負載變化&#xff1a; 1. 圖表解讀 CPU 使用率 從凌晨到上午約 10:00 前&#xff0c;CPU 基本處于 0–2% 的閑置狀態。10:00–14:00 之間&#xff0c;CPU 利用率逐步攀升&#xff0c;多次沖擊 3…

記dwz(JUI)前端框架使用之--服務端響應提示框

目錄 前言 一、DWZ服務器端響應種類 二、如何增加info級別的消息提示 1.打開項目的BaseController.java類 2.打開項目的dwz.min.js文件 3.最后在前端DWZ的主加載頁面或者js文件中添加如下代碼&#xff1a; 前言 本篇文章沒有講太多東西&#xff0c;主要是個人工作記錄保…

leetcode 295. 數據流的中位數

時間復雜度分析&#xff1a;為什么你的中位數查找方案會超時&#xff1f; 分析你提供的MedianFinder實現&#xff0c;其時間復雜度較高的原因主要在于findMedian函數的實現方式。讓我詳細解釋&#xff1a; 代碼時間復雜度分析 你的代碼中兩個主要函數的時間復雜度如下&#…

大語言模型介紹

隨著2022年底 ChatGPT 再一次刷新 NLP 的能力上限&#xff0c;大語言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;開始接替傳統的預訓練語言模型&#xff08;Pre-trained Language Model&#xff0c;PLM&#xff09; 成為 NLP 的主流方向&#xff0c;基于…

STM32 CCR寄存器

?1. CCR寄存器在輸入捕獲模式下的特性? ?只讀屬性?&#xff1a; 當定時器通道配置為輸入捕獲模式&#xff08;如捕獲上升沿/下降沿&#xff09;時&#xff0c;CCR寄存器硬件自動變為只讀?。軟件寫入操作無效&#xff0c;只能在捕獲事件發生時由硬件自動更新為當前CNT值。…

【JS-6-ES6中的let和const】深入理解ES6中的let和const:塊級作用域與變量聲明的新范式

在ES6(ECMAScript 2015)之前&#xff0c;JavaScript中只有var一種變量聲明方式&#xff0c;這導致了許多作用域相關的問題。ES6引入了let和const兩種新的變量聲明方式&#xff0c;徹底改變了JavaScript的作用域規則。本文將深入探討let和const的特性、優勢以及它們與var的區別。…

[C語言]數據類型關鍵字詳解

基本數據類型 關鍵字說明存儲大小(通常)取值范圍(通常)示例int聲明整型變量4字節(32位系統)-2,147,483,648 到 2,147,483,647int count 100;char聲明字符型變量1字節-128 到 127 或 0 到 255char grade ‘A’;float聲明單精度浮點數4字節1.2e-38 到 3.4e38 (約6-7位有效數字…

黑馬python(二十二)

目錄&#xff1a; 1.Python操作Mysql基礎使用 2.Python操作Mysql數據插入 3.綜合案例 1.Python操作Mysql基礎使用 2.Python操作Mysql數據插入 3.綜合案例 代碼復用 黑馬python&#xff08;二十一&#xff09;章節的的代碼&#xff0c;讀取文件內容

課堂筆記:吳恩達的AI課(AI FOR EVERYONE)-W1 深度學習的非技術性解釋

深度學習的非技術性解釋 &#xff08;1&#xff09;示例1&#xff1a;以商場為主買T恤為例&#xff0c;價格和需求的關系怎么樣&#xff1f; 一般來說&#xff0c;價格越高&#xff0c;需求越少 這里輸入A是 價格&#xff0c;輸出B是需求&#xff0c;其中的映射關系是神經元&a…

dlib檢測視頻中的人臉并裁剪為圖片保存

環境要求 找個帶有基本cv配置的虛擬環境安裝上dlib依賴的人臉檢測的基礎環境即可&#xff0c;主要是&#xff1a; pip install boost dlib opencv-python缺的按提示安裝。 demo 設置好視頻路徑和圖像保存路徑&#xff0c;裁剪尺寸&#xff08;默認256&#xff09;以及裁剪幀…

真的!ToDesk遠程控制已上線原生鴻蒙系統!

2025年5月&#xff0c;ToDesk遠程控制正式宣布完成對PC鴻蒙系統的適配&#xff0c;成為業界首批原生支持HarmonyOS OS的跨端遠控工具。 作為國內支持上億設備的遠程控制軟件&#xff0c;ToDesk以無縫互聯、快速響應、安全無界為核心&#xff0c;重新定義了跨設備遠程協作的界限…

Java-58 深入淺出 分布式服務 ACID 三階段提交3PC 對比2PC

點一下關注吧&#xff01;&#xff01;&#xff01;非常感謝&#xff01;&#xff01;持續更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持續更新中&#xff01;&#xff08;長期更新&#xff09; 目前2025年06月16日更新到&#xff1a; AI煉丹日志-29 - 字節…

matplotlib 繪制餅圖

1、功能介紹&#xff1a; 使用 python 的 matplotlib 庫來創建一個簡單的餅圖。 2、代碼部分&#xff1a; import matplotlib.pyplot as plt# 示例數據 labels [A, B, C, D, E] # 類別標簽 sizes [15, 30, 45, 5, 5] # 每個類別對應的數值&#xff08;百分比&#xff09…

用Rust寫平衡三進制除法器

1、除法的本質 除法的本質是減法&#xff0c;也就是一個大的數減去一個小的數&#xff0c;比如:10/2&#xff0c;也就是10-2-2-2-2-20&#xff0c;所以商5余0&#xff0c;10/3&#xff0c;也就是10-3-3-31&#xff0c;所以商3余1&#xff0c;這也是很常見的方法&#xff0c;但如…

深入探索WordPress Multisite:構建與管理多站點網絡

隨著互聯網的快速發展&#xff0c;越來越多的企業和個人開始使用內容管理系統來搭建和維護自己的網站。WordPress作為全球最受歡迎的CMS之一&#xff0c;因其強大的功能和靈活性&#xff0c;成為了許多網站管理員的首選平臺。而在一些特定需求的場景下&#xff0c;WordPress Mu…