• C++ STL之string类模拟实现


    目录

    1.C语言必备函数

    2.构造模块

      (1)构造函数

        [1]构造函数一

        [2]构造函数二

      (2)析构函数 

      (3)拷贝构造函数

        [1]传统版本

        [2]简洁版本

      (4)赋值运算符重载 

        [1]传统版本

        [2]简洁版本

    3.容量模块

      (1)有效字符函数 

      (2)有效字符函数

      (3)扩容函数 

      (4)设置有效字符函数 

      (5)判空函数 

      (6)清空函数 

    4.迭代器模块

      (1)正向迭代器

      (2)反向迭代器

    5.访问模块

      (1)重载[]

      (2)重载cout<< 

    6.修改模块

      (1)尾插单个字符

      (2)重载+=

    7.特殊操作模块

      (1)转为C语言字符串类型

      (2)复制函数

      (3)正向查找函数

      (4)反向查找函数

    8.总体实现


             在前面的文章中已经验证了,在涉及资源管理的类中,拷贝构造函数,析构函数,赋值运算符重载函数都需要用户显示实现,如果使用编译器默认生成的,就会有浅拷贝的风险。如果有读者不理解浅拷贝,可以看下面这幅图。

            浅拷贝通俗来讲就是多个对象指向了同一块空间,例如s1与s2同时指向此空间,当s1对象中的内容修改后,s2中的内容也随之被修改了。但这不是最严重的,最严重的是如果将s1对象销毁,这块空间将被释放,可是s2还在,如果它要对自己的成员变量做修改或是销毁s2,毫无疑问程序会崩溃,因为它此时已经是个野指针了,指向了一块被释放的空间。

            在string类的模拟实现中,主要有六大模块组成:构造模块、容量模块、迭代器模块、访问模块、修改模块、特殊操作模块。因为string类的操作太多,我们只实现其中最常用的功能即可。(本文代码均在win10系统下的vs2019下运行成功)

            在六个模块中的函数均为一部分代码,不能单独运行,总体中的代码才可以运行。   

            在准备模拟string类之前,需要先来回顾一下C语言中用来操作字符串的函数:strlen、strcpy、strncpy、strcat、memset、memcpy。这些函数在模拟string类的时候都要用到。     

    1.C语言必备函数

    函数名称函数功能
    size_t strlen(const char *str)
    计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
    char *strcpy(char *dest, const char *src)

    src 所指向的字符串复制到 dest

    注意dest指向的空间要足够大。

    char *strncpy(char *dest, const char *src, size_t n)
    src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
    void *memset(void *str, int c, size_t n)
    复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
    void *memcpy(void *str1, const void *str2, size_t n)
    从存储区 str2 复制 n 个字节到存储区 str1
    strcat(char* s1,char* s2)src 所指向的字符串追加到 dest 所指向的字符串的结尾。

            在回顾函数之前,需要说明的是,在vs2019中这样定义一个字符串是会报错的:char* str = "abc";解决办法如下代码一:

            代码一:

    1. //代码一
    2. #include "iostream"
    3. using namespace std;
    4. #include
    5. int main() {
    6. //方法1
    7. const char* s1 = "abc";
    8. cout << s1 << endl;
    9. //方法2
    10. char s2[] = { "1234" };
    11. char* p2 = s2;
    12. cout << p2 << endl;
    13. //方法3
    14. char* s3 = new char[100]{ "asd" };
    15. cout << s3 << endl;
    16. }

            解决上述问题后,开始回顾函数,六个函数的使用都在代码二中了: 使用这些函数时,请加上#define _CRT_SECURE_NO_WARNINGS,否则编译器会因为这些函数不安全而报错。

            代码二:

    1. //代码二
    2. #define _CRT_SECURE_NO_WARNINGS
    3. #include "iostream"
    4. using namespace std;
    5. #include
    6. int main() {
    7. char* s1 = new char[100]{ "12345" };
    8. char* s2 = new char[100]{ "abcde" };
    9. char* s3 = new char[100];
    10. //strlen 计算s1有效元素个数
    11. int len = strlen(s1);
    12. cout << len << endl;//5
    13. //strcpy 把s2拷贝到s1中
    14. strcpy(s1, s2);
    15. cout << s1 << endl;//abcde
    16. //strcpy 把s1的前2个字符拷贝到s3中
    17. strncpy(s3, s1, 2);
    18. s3[2] = '\0';
    19. cout << s3 << endl;//ab
    20. //memset 把1个 '9'字符复制到s1的前1个位置
    21. memset(s1, '9', 1);
    22. cout << s1 << endl;//9bcde
    23. //memcpy 从s1中复制4个字节到s3中
    24. memcpy(s3, s1, 4);
    25. s3[4] = '\0';
    26. cout << s3 << endl;//9bcd
    27. //strcat 把s3指向的字符串追加到s1指向的字符串结尾
    28. strcat(s1, s3);
    29. cout << s1 << endl;//9bcde9bcd
    30. }

    2.构造模块

      (1)构造函数

            这里实现两个构造函数。

            String类中的成员应该有一个字符串指针,一个有效元素个数变量,一个容量大小变量,一个npos变量(因为这是原本的string类中就有的)。为了方便起见,在设计阶段先将所有成员的访问权限均设置为public。同时将构造函数定义成全缺省函数。

        [1]构造函数一

            代码三:使用字符串构造对象

    1. //代码三
    2. #define _CRT_SECURE_NO_WARNINGS
    3. #include "iostream"
    4. using namespace std;
    5. class String {
    6. public:
    7. char* Str;//字符串指针
    8. size_t Size;//有效元素个数
    9. size_t Capacity;//容量
    10. static size_t npos;
    11. public:
    12. String(const char* s = "") {
    13. //将指向空的指针指向空字符串
    14. if (s == nullptr)
    15. s = "";
    16. //注意要比申请比字符串长度多1的空间来存储 '\0'
    17. Str = new char[strlen(s) + 1];
    18. //将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
    19. strcpy(Str, s);
    20. //更新有效元素个数和容量
    21. Size = strlen(s);
    22. Capacity = strlen(s);
    23. }
    24. };
    25. size_t String::npos = -1;

        [2]构造函数二

            代码四:使用n个字符构造对象

    1. //代码四
    2. String(size_t n, char c) {
    3. //申请比有效元素多一个的空间
    4. Str = new char[n + 1];
    5. //把n个c存入Str指向的空间中
    6. memset(Str, c, n);
    7. //不要忘记设置'\0'
    8. Str[n] = '\0';
    9. //更新有效元素和容量
    10. //容量要保持比有效元素个数多
    11. //用来存储'\0'
    12. Size = n;
    13. Capacity = n+1;
    14. }

      (2)析构函数 

            代码五:析构函数一定要检查字符串指针是否指向空,如果是空就不可以再释放了。

    1. //代码五
    2. ~String() {
    3. //如果指向空值,释放程序会崩溃的
    4. if (Str)
    5. delete[] Str;
    6. Str = nullptr;
    7. Size = 0;
    8. Capacity = 0;
    9. }

      (3)拷贝构造函数

            这里提供两种版本的拷贝构造函数以供参考。

        [1]传统版本

            代码六:传统版本是先释放指向的旧空间,构造新空间,然后使用strcpy函数拷贝数据。

            注意这样有个缺点,虽然概率很小。万一申请空间失败呢?那样就连原有的数据都没有了。

    1. //代码六
    2. String(const String& s) {
    3. //需要释放原本的空间
    4. if (Str) {
    5. delete[] Str;
    6. }
    7. Str = new char[strlen(s.Str) + 1];//+1是为了存储'\0'
    8. strcpy(Str, s.Str);
    9. Size = strlen(Str);
    10. Capacity = Size+1;//+1是因为申请空间时就申请了这么多
    11. }

        [2]简洁版本

            代码七: 简洁版本是先使用目标字符串构造一个临时对象,然后交换两个对象的字符串指针。这样做不但不用担心空间申请失败,也不用自己手动释放空间。当临时对象生命周期结束,系统会自动调用析构函数。

    1. //代码七
    2. String(const String& s) {
    3. String temp(s.Str);
    4. //交换temp.Str和this->Str指向的空间
    5. std::swap(Str, temp.Str);
    6. Size = strlen(Str);
    7. Capacity = Size+1;
    8. }

      (4)赋值运算符重载 

            赋值运算符重载也提供了两个版本以供参考。

        [1]传统版本

            代码八:传统版本需要考虑是不是对象自己给自己赋值,若是就直接返回。一定要记得开辟空间时,要比字符串长度大,这是为了存储'\0'

    1. //代码八
    2. String& operator=(const String& s) {
    3. //判断是否是自己给自己赋值
    4. if (this != &s) {
    5. String temp(Str);
    6. //记得释放原本指向的空间
    7. delete[] Str;
    8. Str = new char[strlen(s.Str) + 1];
    9. strcpy(Str, s.Str);
    10. Size = strlen(s.Str);
    11. Capacity = Size+1;
    12. }
    13. return *this;
    14. }

        [2]简洁版本

            代码九:简介版本不需要考虑自己给自己赋值,反正都是交换空间。

    1. //代码九
    2. String& operator=(const String& s) {
    3. String temp(s.Str);
    4. //交换空间
    5. std::swap(Str, temp.Str);
    6. Size = strlen(s.Str);
    7. Capacity = Size +1;
    8. return *this;
    9. }

    3.容量模块

      (1)有效字符函数 

            代码十:

    1. //代码十
    2. size_t SIZE() {
    3. return Size;
    4. }

      (2)有效字符函数

            代码十一: 

    1. //代码十一
    2. size_t CAPAcity() {
    3. return Capacity;
    4. }

      (3)扩容函数 

            代码十二:这里直接按照参数的两倍来扩容,记得需要释放原本的字符串空间。

    1. //代码十二
    2. void Reverse(size_t newCapacity) {
    3. //新容量大于旧容量
    4. if (newCapacity >= Capacity) {
    5. char* temp = new char[newCapacity *2];
    6. strcpy(temp, Str);
    7. delete[] Str;
    8. Str = temp;
    9. Capacity = 2 * newCapacity;
    10. }
    11. }

      (4)设置有效字符函数 

            代码十三:此函数机制为:新有效元素大于旧有效元素时可能需要扩容,继续判断,当新有效元素比容量还要大时需要扩容,扩容后使用memset函数添加元素。最后不要忘了手动加'\0'。

    1. //代码十三
    2. void Resize(size_t newSize, char c) {
    3. size_t oldSize = Size;
    4. size_t oldCapa = Capacity;
    5. //新的有效元素比旧的多
    6. if (newSize > oldSize) {
    7. //新元素比容量多 扩容
    8. if (newSize > oldCapa) {
    9. Reverse(newSize);
    10. }
    11. //往Str尾部插入合适数量的c
    12. memset(Str + Size, c, newSize - oldSize);
    13. }
    14. Size = newSize;
    15. Str[Size] = '\0';
    16. }

      (5)判空函数 

            代码十四:

    1. //代码十四
    2. bool Empty() {
    3. //容量等于0返回true
    4. return Size == 0;
    5. }

      (6)清空函数 

            代码十五:注意清空的含义是清空有效元素,不对容量进行操作。

    1. //代码十五
    2. void clear() {
    3. //只改变有效元素个数,不改变容量
    4. Size = 0;
    5. Str[0] = '\0';
    6. }

    4.迭代器模块

            其实迭代器就是当作指针使用的,为了和string迭代器形式相似,我们要用这条语句给指针起个小名:typedef char* Iterator;

      (1)正向迭代器

            代码十六:

    1. //代码十六
    2. typedef char* Iterator;
    3. Iterator Begin() {
    4. return Str;
    5. }
    6. Iterator End() {
    7. return Str + Size;
    8. }

      (2)反向迭代器

            代码十七:反向迭代器可以直接复用正向迭代器。 

    1. //代码十七
    2. typedef char* Iterator;
    3. Iterator Rbegin() {
    4. return End();
    5. }
    6. Iterator Rend() {
    7. return Begin();
    8. }

    5.访问模块

      (1)重载[]

            代码十八:注意一定要判断是否越界访问。同时也要多重载一份为const对象使用。使用assert来控制时要加头文件。#include

    1. //代码十八
    2. #include
    3. char& operator[](size_t pos) {
    4. assert(pos < Size);
    5. return Str[pos];
    6. }
    7. const char& operator[](size_t pos)const {
    8. assert(pos < Size);
    9. return Str[pos];
    10. }

      (2)重载cout<< 

            代码十九:注意<<不可以重载为成员函数,但我们还要使用私有变量,就把它设置为友元函数。至于为什么不可以重载为成员函数,我在《C++实现日期类》中已经总结了,感兴趣的可以去看看。

    1. //代码十九
    2. friend ostream& operator<<(ostream& _cout, const String& s) {
    3. _cout << s.Str;
    4. return _cout;
    5. }

    6.修改模块

      (1)尾插单个字符

            代码二十:尾插时需要注意扩容条件。

    1. //代码二十
    2. void Push_back(char c) {
    3. //满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
    4. if (Size + 1== Capacity) {
    5. Reverse(Capacity);
    6. }
    7. Str[Size] = c;
    8. Str[Size+1] = '\0';
    9. Size++;
    10. }

      (2)重载+=

            代码二十一:+=有两种,+=字符串、+=对象。后者可以复用前者。这里要特别注意如何判断是否要扩容,当两个字符串有效元素加起来再加上'\0'的大小不比容量小时就应该扩容。

    1. //代码二十一
    2. void operator+=(const char* s) {
    3. size_t oldSize = Size;
    4. //两个空间加起来元素个数
    5. size_t size = Size + strlen(s);
    6. //元素个数接近容量
    7. if (size+1 >= Capacity) {
    8. Reverse(size);
    9. }
    10. strcat(Str + oldSize, s);
    11. Size = size;
    12. }
    13. //直接复用上面的
    14. String& operator+=(const String& s) {
    15. *this += s.Str;
    16. return *this;
    17. }

    7.特殊操作模块

      (1)转为C语言字符串类型

            代码二十二:

    1. //代码二十二
    2. const char* c_str()const
    3. {
    4. return Str;
    5. }

      (2)复制函数

            代码二十三:注意参数的调整问题。

    1. //代码二十三
    2. String Substr(size_t pos = 0, size_t n = npos) {
    3. //没有传参
    4. if (n == npos)
    5. n = Size;
    6. //传递的参数个数大于有效元素个数就需要进行调整
    7. if (pos + n >= Size) {
    8. n =Size - pos;
    9. }
    10. char* temp = new char[n + 1];
    11. strncpy(temp, Str + pos, n);
    12. temp[Size] = '\0';
    13. String Stemp(temp);
    14. delete[] temp;
    15. return Stemp;
    16. }

      (3)正向查找函数

            代码二十四:注意没找到就返回npos。npos是-1。

    1. //代码二十四
    2. size_t Find(char c, size_t pos = 0) {
    3. for (int i = pos; i < Size; i++) {
    4. if (Str[i] == c)
    5. return i;
    6. }
    7. return npos;
    8. }

      (4)反向查找函数

            代码二十五:

    1. //代码二十五
    2. size_t Rfind(char c, size_t pos = npos) {
    3. //查找的开始位置越界就将位置定在结尾
    4. pos = pos < Size ? pos : Size;
    5. for (int i = pos; i >= 0; i--) {
    6. if (Str[i] == c) {
    7. return i;
    8. }
    9. }
    10. return npos;
    11. }

    8.总体实现

            以下是String类的模拟实现,在运行程序发现不足之处的,可以在评论区留言。

    1. #define _CRT_SECURE_NO_WARNINGS //解决C语言函数报错
    2. #include "iostream"
    3. #include
    4. using namespace std;
    5. class String {
    6. private:
    7. char* Str;//字符串指针
    8. size_t Size;//有效元素个数
    9. size_t Capacity;//容量
    10. static size_t npos;
    11. public:
    12. //构造模块
    13. /
    14. //构造函数1
    15. String(const char* s = "") {
    16. //将指向空的指针指向空字符串
    17. if (s == nullptr)
    18. s = "";
    19. //注意要比申请比字符串长度多1的空间来存储 '\0'
    20. Str = new char[strlen(s) + 1];
    21. //将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
    22. strcpy(Str, s);
    23. //更新有效元素个数和容量
    24. Size = strlen(s);
    25. Capacity = strlen(s)+1;
    26. }
    27. //构造函数2
    28. String(size_t n, char c) {
    29. //申请比有效元素多一个的空间
    30. Str = new char[n + 1];
    31. //把n个c存入Str指向的空间中
    32. memset(Str, c, n);
    33. //不要忘记设置'\0'
    34. Str[n] = '\0';
    35. //更新有效元素和容量
    36. //容量要保持比有效元素个数多
    37. //用来存储'\0'
    38. Size = n;
    39. Capacity = n+1;
    40. }
    41. //析构函数
    42. ~String() {
    43. //如果指向空值,释放程序会崩溃的
    44. if (Str)
    45. delete[] Str;
    46. Str = nullptr;
    47. Size = 0;
    48. Capacity = 0;
    49. }
    50. //传统版拷贝构造
    51. /*
    52. String(const String& s) {
    53. //需要释放原本的空间
    54. if (Str) {
    55. delete[] Str;
    56. }
    57. Str = new char[strlen(s.Str) + 1];
    58. strcpy(Str, s.Str);
    59. Size = strlen(Str);
    60. Capacity = Size+1;
    61. }
    62. */
    63. //简洁版拷贝构造
    64. String(const String& s) {
    65. String temp(s.Str);
    66. //交换temp.Str和this->Str指向的空间
    67. std::swap(Str, temp.Str);
    68. Size = strlen(Str);
    69. Capacity = Size+1;
    70. }
    71. //传统版赋值运算符重载
    72. /*
    73. String& operator=(const String& s) {
    74. //判断是否是自己给自己赋值
    75. if (this != &s) {
    76. String temp(Str);
    77. //记得释放原本指向的空间
    78. delete[] Str;
    79. Str = new char[strlen(s.Str) + 1];
    80. strcpy(Str, s.Str);
    81. Size = strlen(s.Str);
    82. Capacity = Size + 1;
    83. }
    84. return *this;
    85. }
    86. */
    87. //简洁版赋值运算符重载
    88. String& operator=(const String& s) {
    89. String temp(s.Str);
    90. //交换空间
    91. std::swap(Str, temp.Str);
    92. Size = strlen(s.Str);
    93. Capacity = Size +1;
    94. return *this;
    95. }
    96. //容量模块
    97. /
    98. //有效元素
    99. size_t SIZE() {
    100. return Size;
    101. }
    102. //容量
    103. size_t CAPAcity() {
    104. return Capacity;
    105. }
    106. //扩容函数
    107. void Reverse(size_t newCapacity) {
    108. //新容量大于旧容量
    109. if (newCapacity >= Capacity) {
    110. char* temp = new char[newCapacity *2];
    111. strcpy(temp, Str);
    112. delete[] Str;
    113. Str = temp;
    114. Capacity = 2 * newCapacity;
    115. }
    116. }
    117. //设置有效元素
    118. void Resize(size_t newSize, char c) {
    119. size_t oldSize = Size;
    120. size_t oldCapa = Capacity;
    121. //新的有效元素比旧的多
    122. if (newSize > oldSize) {
    123. //新元素比容量多 扩容
    124. if (newSize > oldCapa) {
    125. Reverse(newSize);
    126. }
    127. //往Str尾部插入合适数量的c
    128. memset(Str + Size, c, newSize - oldSize);
    129. }
    130. Size = newSize;
    131. Str[Size] = '\0';
    132. }
    133. //判空函数
    134. bool Empty() {
    135. //容量等于0返回true
    136. return Size == 0;
    137. }
    138. //清空函数
    139. void clear() {
    140. //只改变有效元素个数,不改变容量
    141. Size = 0;
    142. Str[0] = '\0';
    143. }
    144. //迭代器模块
    145. /
    146. typedef char* Iterator;
    147. //获取正向首指针
    148. Iterator Begin() {
    149. return Str;
    150. }
    151. //获取正向尾指针
    152. Iterator End() {
    153. return Str + Size;
    154. }
    155. typedef char* Iterator;
    156. //获取反向首指针
    157. Iterator Rbegin() {
    158. return End();
    159. }
    160. //获取反向尾指针
    161. Iterator Rend() {
    162. return Begin();
    163. }
    164. //访问模块
    165. /
    166. //普通[]重载
    167. char& operator[](size_t pos) {
    168. assert(pos < Size);
    169. return Str[pos];
    170. }
    171. //const[]重载
    172. const char& operator[](size_t pos)const {
    173. assert(pos < Size);
    174. return Str[pos];
    175. }
    176. // <<重载
    177. friend ostream& operator<<(ostream& _cout, const String& s) {
    178. _cout << s.Str;
    179. return _cout;
    180. }
    181. //访问模块
    182. /
    183. //尾插
    184. void Push_back(char c) {
    185. //满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
    186. if (Size + 1== Capacity) {
    187. Reverse(Capacity);
    188. }
    189. Str[Size] = c;
    190. Str[Size+1] = '\0';
    191. Size++;
    192. }
    193. //+=重载
    194. void operator+=(const char* s) {
    195. size_t oldSize = Size;
    196. //两个空间加起来元素个数
    197. size_t size = Size + strlen(s);
    198. //元素个数接近容量
    199. if (size+1 >= Capacity) {
    200. Reverse(size);
    201. }
    202. strcat(Str + oldSize, s);
    203. Size = size;
    204. }
    205. //+=重载
    206. String& operator+=(const String& s) {
    207. *this += s.Str;
    208. return *this;
    209. }
    210. //特殊操作模块
    211. /
    212. //转换C语言字符串
    213. const char* c_str()const
    214. {
    215. return Str;
    216. }
    217. //复制
    218. String Substr(size_t pos = 0, size_t n = npos) {
    219. if (n == npos)
    220. n = Size;
    221. if (pos + n >= Size) {
    222. n =Size - pos;
    223. }
    224. char* temp = new char[n + 1];
    225. strncpy(temp, Str + pos, n);
    226. temp[Size] = '\0';
    227. String Stemp(temp);
    228. delete[] temp;
    229. return Stemp;
    230. }
    231. //正向查找
    232. size_t Find(char c, size_t pos = 0) {
    233. for (int i = pos; i < Size; i++) {
    234. if (Str[i] == c)
    235. return i;
    236. }
    237. return npos;
    238. }
    239. //反向查找
    240. size_t Rfind(char c, size_t pos = npos) {
    241. pos = pos < Size ? pos : Size;
    242. for (int i = pos; i >= 0; i--) {
    243. if (Str[i] == c) {
    244. return i;
    245. }
    246. }
    247. return npos;
    248. }
    249. };
    250. size_t String::npos = -1;
    251. int main() {
    252. const char* sp = "abc12345566743212343455655";
    253. String s1(sp);
    254. String s2 = s1;
    255. auto it = s2.Begin();
    256. while (it != s2.End()) {
    257. cout << *it << " ";
    258. it++;
    259. }
    260. }

  • 相关阅读:
    MIPI CSI-2笔记(8) -- Low Level Protocol(同步短包的数据类型编码)
    MySQL8--Windows下使用压缩包安装的方法
    .Net 6.0全局异常捕获
    STM8的C语言编程(6)--8位定时器应用之二
    Uniapp 实现全民分销功能
    拷贝控制操作
    indexDB & localForage
    主存储器与CPU的连接
    遨博机械臂URDF功能包ROS仿真
    openstack nova 源码分析
  • 原文地址:https://blog.csdn.net/weixin_57761086/article/details/126560762