赞
踩
目录
在前面的文章中已经验证了,在涉及资源管理的类中,拷贝构造函数,析构函数,赋值运算符重载函数都需要用户显示实现,如果使用编译器默认生成的,就会有浅拷贝的风险。如果有读者不理解浅拷贝,可以看下面这幅图。
浅拷贝通俗来讲就是多个对象指向了同一块空间,例如s1与s2同时指向此空间,当s1对象中的内容修改后,s2中的内容也随之被修改了。但这不是最严重的,最严重的是如果将s1对象销毁,这块空间将被释放,可是s2还在,如果它要对自己的成员变量做修改或是销毁s2,毫无疑问程序会崩溃,因为它此时已经是个野指针了,指向了一块被释放的空间。
在string类的模拟实现中,主要有六大模块组成:构造模块、容量模块、迭代器模块、访问模块、修改模块、特殊操作模块。因为string类的操作太多,我们只实现其中最常用的功能即可。(本文代码均在win10系统下的vs2019下运行成功)
在六个模块中的函数均为一部分代码,不能单独运行,总体中的代码才可以运行。
在准备模拟string类之前,需要先来回顾一下C语言中用来操作字符串的函数:strlen、strcpy、strncpy、strcat、memset、memcpy。这些函数在模拟string类的时候都要用到。
函数名称 | 函数功能 |
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";解决办法如下代码一:
代码一:
- //代码一
- #include "iostream"
- using namespace std;
- #include <string.h>
-
- int main() {
- //方法1
- const char* s1 = "abc";
- cout << s1 << endl;
-
- //方法2
- char s2[] = { "1234" };
- char* p2 = s2;
- cout << p2 << endl;
-
- //方法3
- char* s3 = new char[100]{ "asd" };
- cout << s3 << endl;
- }
解决上述问题后,开始回顾函数,六个函数的使用都在代码二中了: 使用这些函数时,请加上#define _CRT_SECURE_NO_WARNINGS,否则编译器会因为这些函数不安全而报错。
代码二:
- //代码二
- #define _CRT_SECURE_NO_WARNINGS
- #include "iostream"
- using namespace std;
- #include <string.h>
-
- int main() {
- char* s1 = new char[100]{ "12345" };
- char* s2 = new char[100]{ "abcde" };
- char* s3 = new char[100];
-
- //strlen 计算s1有效元素个数
- int len = strlen(s1);
- cout << len << endl;//5
-
- //strcpy 把s2拷贝到s1中
- strcpy(s1, s2);
- cout << s1 << endl;//abcde
-
- //strcpy 把s1的前2个字符拷贝到s3中
- strncpy(s3, s1, 2);
- s3[2] = '\0';
- cout << s3 << endl;//ab
-
- //memset 把1个 '9'字符复制到s1的前1个位置
- memset(s1, '9', 1);
- cout << s1 << endl;//9bcde
-
- //memcpy 从s1中复制4个字节到s3中
- memcpy(s3, s1, 4);
- s3[4] = '\0';
- cout << s3 << endl;//9bcd
-
- //strcat 把s3指向的字符串追加到s1指向的字符串结尾
- strcat(s1, s3);
- cout << s1 << endl;//9bcde9bcd
- }
这里实现两个构造函数。
String类中的成员应该有一个字符串指针,一个有效元素个数变量,一个容量大小变量,一个npos变量(因为这是原本的string类中就有的)。为了方便起见,在设计阶段先将所有成员的访问权限均设置为public。同时将构造函数定义成全缺省函数。
代码三:使用字符串构造对象
- //代码三
- #define _CRT_SECURE_NO_WARNINGS
- #include "iostream"
- using namespace std;
-
- class String {
- public:
- char* Str;//字符串指针
- size_t Size;//有效元素个数
- size_t Capacity;//容量
- static size_t npos;
- public:
- String(const char* s = "") {
- //将指向空的指针指向空字符串
- if (s == nullptr)
- s = "";
- //注意要比申请比字符串长度多1的空间来存储 '\0'
- Str = new char[strlen(s) + 1];
- //将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
- strcpy(Str, s);
- //更新有效元素个数和容量
- Size = strlen(s);
- Capacity = strlen(s);
- }
- };
-
- size_t String::npos = -1;
代码四:使用n个字符构造对象
- //代码四
- String(size_t n, char c) {
- //申请比有效元素多一个的空间
- Str = new char[n + 1];
-
- //把n个c存入Str指向的空间中
- memset(Str, c, n);
-
- //不要忘记设置'\0'
- Str[n] = '\0';
-
- //更新有效元素和容量
- //容量要保持比有效元素个数多
- //用来存储'\0'
- Size = n;
- Capacity = n+1;
- }
-
代码五:析构函数一定要检查字符串指针是否指向空,如果是空就不可以再释放了。
- //代码五
- ~String() {
- //如果指向空值,释放程序会崩溃的
- if (Str)
- delete[] Str;
- Str = nullptr;
- Size = 0;
- Capacity = 0;
- }
这里提供两种版本的拷贝构造函数以供参考。
代码六:传统版本是先释放指向的旧空间,构造新空间,然后使用strcpy函数拷贝数据。
注意这样有个缺点,虽然概率很小。万一申请空间失败呢?那样就连原有的数据都没有了。
- //代码六
- String(const String& s) {
- //需要释放原本的空间
- if (Str) {
- delete[] Str;
- }
- Str = new char[strlen(s.Str) + 1];//+1是为了存储'\0'
- strcpy(Str, s.Str);
- Size = strlen(Str);
- Capacity = Size+1;//+1是因为申请空间时就申请了这么多
- }
代码七: 简洁版本是先使用目标字符串构造一个临时对象,然后交换两个对象的字符串指针。这样做不但不用担心空间申请失败,也不用自己手动释放空间。当临时对象生命周期结束,系统会自动调用析构函数。
- //代码七
- String(const String& s) {
- String temp(s.Str);
- //交换temp.Str和this->Str指向的空间
- std::swap(Str, temp.Str);
- Size = strlen(Str);
- Capacity = Size+1;
- }
赋值运算符重载也提供了两个版本以供参考。
代码八:传统版本需要考虑是不是对象自己给自己赋值,若是就直接返回。一定要记得开辟空间时,要比字符串长度大,这是为了存储'\0'
- //代码八
- String& operator=(const String& s) {
- //判断是否是自己给自己赋值
- if (this != &s) {
- String temp(Str);
- //记得释放原本指向的空间
- delete[] Str;
- Str = new char[strlen(s.Str) + 1];
- strcpy(Str, s.Str);
- Size = strlen(s.Str);
- Capacity = Size+1;
- }
- return *this;
- }
代码九:简介版本不需要考虑自己给自己赋值,反正都是交换空间。
- //代码九
- String& operator=(const String& s) {
- String temp(s.Str);
- //交换空间
- std::swap(Str, temp.Str);
- Size = strlen(s.Str);
- Capacity = Size +1;
- return *this;
- }
代码十:
- //代码十
- size_t SIZE() {
- return Size;
- }
代码十一:
- //代码十一
- size_t CAPAcity() {
- return Capacity;
- }
代码十二:这里直接按照参数的两倍来扩容,记得需要释放原本的字符串空间。
- //代码十二
- void Reverse(size_t newCapacity) {
- //新容量大于旧容量
- if (newCapacity >= Capacity) {
- char* temp = new char[newCapacity *2];
- strcpy(temp, Str);
- delete[] Str;
- Str = temp;
- Capacity = 2 * newCapacity;
- }
- }
代码十三:此函数机制为:新有效元素大于旧有效元素时可能需要扩容,继续判断,当新有效元素比容量还要大时需要扩容,扩容后使用memset函数添加元素。最后不要忘了手动加'\0'。
- //代码十三
- void Resize(size_t newSize, char c) {
- size_t oldSize = Size;
- size_t oldCapa = Capacity;
- //新的有效元素比旧的多
- if (newSize > oldSize) {
- //新元素比容量多 扩容
- if (newSize > oldCapa) {
- Reverse(newSize);
- }
- //往Str尾部插入合适数量的c
- memset(Str + Size, c, newSize - oldSize);
- }
- Size = newSize;
- Str[Size] = '\0';
- }
代码十四:
- //代码十四
- bool Empty() {
- //容量等于0返回true
- return Size == 0;
- }
代码十五:注意清空的含义是清空有效元素,不对容量进行操作。
- //代码十五
- void clear() {
- //只改变有效元素个数,不改变容量
- Size = 0;
- Str[0] = '\0';
- }
其实迭代器就是当作指针使用的,为了和string迭代器形式相似,我们要用这条语句给指针起个小名:typedef char* Iterator;
代码十六:
- //代码十六
- typedef char* Iterator;
-
- Iterator Begin() {
- return Str;
- }
-
- Iterator End() {
- return Str + Size;
- }
代码十七:反向迭代器可以直接复用正向迭代器。
- //代码十七
- typedef char* Iterator;
-
- Iterator Rbegin() {
- return End();
- }
-
- Iterator Rend() {
- return Begin();
- }
代码十八:注意一定要判断是否越界访问。同时也要多重载一份为const对象使用。使用assert来控制时要加头文件。#include<assert.h>
- //代码十八
- #include<assert.h>
-
- char& operator[](size_t pos) {
- assert(pos < Size);
- return Str[pos];
- }
-
- const char& operator[](size_t pos)const {
- assert(pos < Size);
- return Str[pos];
- }
代码十九:注意<<不可以重载为成员函数,但我们还要使用私有变量,就把它设置为友元函数。至于为什么不可以重载为成员函数,我在《C++实现日期类》中已经总结了,感兴趣的可以去看看。
- //代码十九
- friend ostream& operator<<(ostream& _cout, const String& s) {
- _cout << s.Str;
- return _cout;
- }
代码二十:尾插时需要注意扩容条件。
- //代码二十
- void Push_back(char c) {
- //满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
- if (Size + 1== Capacity) {
- Reverse(Capacity);
- }
- Str[Size] = c;
- Str[Size+1] = '\0';
- Size++;
- }
代码二十一:+=有两种,+=字符串、+=对象。后者可以复用前者。这里要特别注意如何判断是否要扩容,当两个字符串有效元素加起来再加上'\0'的大小不比容量小时就应该扩容。
- //代码二十一
-
- void operator+=(const char* s) {
- size_t oldSize = Size;
- //两个空间加起来元素个数
- size_t size = Size + strlen(s);
- //元素个数接近容量
- if (size+1 >= Capacity) {
- Reverse(size);
- }
- strcat(Str + oldSize, s);
- Size = size;
- }
-
- //直接复用上面的
- String& operator+=(const String& s) {
- *this += s.Str;
- return *this;
- }
代码二十二:
- //代码二十二
- const char* c_str()const
- {
- return Str;
- }
代码二十三:注意参数的调整问题。
- //代码二十三
- String Substr(size_t pos = 0, size_t n = npos) {
- //没有传参
- if (n == npos)
- n = Size;
-
- //传递的参数个数大于有效元素个数就需要进行调整
- if (pos + n >= Size) {
- n =Size - pos;
- }
-
- char* temp = new char[n + 1];
- strncpy(temp, Str + pos, n);
- temp[Size] = '\0';
-
- String Stemp(temp);
- delete[] temp;
- return Stemp;
- }
代码二十四:注意没找到就返回npos。npos是-1。
- //代码二十四
- size_t Find(char c, size_t pos = 0) {
- for (int i = pos; i < Size; i++) {
- if (Str[i] == c)
- return i;
- }
- return npos;
- }
代码二十五:
- //代码二十五
- size_t Rfind(char c, size_t pos = npos) {
-
- //查找的开始位置越界就将位置定在结尾
- pos = pos < Size ? pos : Size;
-
- for (int i = pos; i >= 0; i--) {
- if (Str[i] == c) {
- return i;
- }
- }
- return npos;
- }
以下是String类的模拟实现,在运行程序发现不足之处的,可以在评论区留言。
- #define _CRT_SECURE_NO_WARNINGS //解决C语言函数报错
- #include "iostream"
- #include<assert.h>
- using namespace std;
-
- class String {
- private:
- char* Str;//字符串指针
- size_t Size;//有效元素个数
- size_t Capacity;//容量
- static size_t npos;
- public:
- //构造模块
- /
- //构造函数1
- String(const char* s = "") {
- //将指向空的指针指向空字符串
- if (s == nullptr)
- s = "";
- //注意要比申请比字符串长度多1的空间来存储 '\0'
- Str = new char[strlen(s) + 1];
- //将s指向的空间中的内容拷贝到Str指向的空间中,这个操作会把'\0'一并拷贝
- strcpy(Str, s);
- //更新有效元素个数和容量
- Size = strlen(s);
- Capacity = strlen(s)+1;
- }
- //构造函数2
- String(size_t n, char c) {
- //申请比有效元素多一个的空间
- Str = new char[n + 1];
- //把n个c存入Str指向的空间中
- memset(Str, c, n);
- //不要忘记设置'\0'
- Str[n] = '\0';
- //更新有效元素和容量
- //容量要保持比有效元素个数多
- //用来存储'\0'
- Size = n;
- Capacity = n+1;
- }
- //析构函数
- ~String() {
- //如果指向空值,释放程序会崩溃的
- if (Str)
- delete[] Str;
- Str = nullptr;
- Size = 0;
- Capacity = 0;
- }
-
- //传统版拷贝构造
- /*
- String(const String& s) {
- //需要释放原本的空间
- if (Str) {
- delete[] Str;
- }
- Str = new char[strlen(s.Str) + 1];
- strcpy(Str, s.Str);
- Size = strlen(Str);
- Capacity = Size+1;
- }
- */
- //简洁版拷贝构造
- String(const String& s) {
- String temp(s.Str);
- //交换temp.Str和this->Str指向的空间
- std::swap(Str, temp.Str);
- Size = strlen(Str);
- Capacity = Size+1;
- }
-
- //传统版赋值运算符重载
- /*
- String& operator=(const String& s) {
- //判断是否是自己给自己赋值
- if (this != &s) {
- String temp(Str);
- //记得释放原本指向的空间
- delete[] Str;
- Str = new char[strlen(s.Str) + 1];
- strcpy(Str, s.Str);
- Size = strlen(s.Str);
- Capacity = Size + 1;
- }
- return *this;
- }
- */
-
- //简洁版赋值运算符重载
- String& operator=(const String& s) {
- String temp(s.Str);
- //交换空间
- std::swap(Str, temp.Str);
- Size = strlen(s.Str);
- Capacity = Size +1;
- return *this;
- }
-
- //容量模块
- /
- //有效元素
- size_t SIZE() {
- return Size;
- }
-
- //容量
- size_t CAPAcity() {
- return Capacity;
- }
-
- //扩容函数
- void Reverse(size_t newCapacity) {
- //新容量大于旧容量
- if (newCapacity >= Capacity) {
- char* temp = new char[newCapacity *2];
- strcpy(temp, Str);
- delete[] Str;
- Str = temp;
- Capacity = 2 * newCapacity;
- }
- }
-
- //设置有效元素
- void Resize(size_t newSize, char c) {
- size_t oldSize = Size;
- size_t oldCapa = Capacity;
- //新的有效元素比旧的多
- if (newSize > oldSize) {
- //新元素比容量多 扩容
- if (newSize > oldCapa) {
- Reverse(newSize);
- }
- //往Str尾部插入合适数量的c
- memset(Str + Size, c, newSize - oldSize);
- }
- Size = newSize;
- Str[Size] = '\0';
- }
-
- //判空函数
- bool Empty() {
- //容量等于0返回true
- return Size == 0;
- }
-
- //清空函数
- void clear() {
- //只改变有效元素个数,不改变容量
- Size = 0;
- Str[0] = '\0';
- }
- //迭代器模块
- /
- typedef char* Iterator;
- //获取正向首指针
- Iterator Begin() {
- return Str;
- }
-
- //获取正向尾指针
- Iterator End() {
- return Str + Size;
- }
- typedef char* Iterator;
- //获取反向首指针
- Iterator Rbegin() {
- return End();
- }
-
- //获取反向尾指针
- Iterator Rend() {
- return Begin();
- }
- //访问模块
- /
- //普通[]重载
- char& operator[](size_t pos) {
- assert(pos < Size);
- return Str[pos];
- }
- //const[]重载
- const char& operator[](size_t pos)const {
- assert(pos < Size);
- return Str[pos];
- }
- // <<重载
- friend ostream& operator<<(ostream& _cout, const String& s) {
- _cout << s.Str;
- return _cout;
- }
- //访问模块
- /
- //尾插
- void Push_back(char c) {
- //满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
- if (Size + 1== Capacity) {
- Reverse(Capacity);
- }
- Str[Size] = c;
- Str[Size+1] = '\0';
- Size++;
- }
- //+=重载
- void operator+=(const char* s) {
- size_t oldSize = Size;
- //两个空间加起来元素个数
- size_t size = Size + strlen(s);
- //元素个数接近容量
- if (size+1 >= Capacity) {
- Reverse(size);
- }
- strcat(Str + oldSize, s);
- Size = size;
- }
- //+=重载
- String& operator+=(const String& s) {
- *this += s.Str;
- return *this;
- }
- //特殊操作模块
- /
- //转换C语言字符串
- const char* c_str()const
- {
- return Str;
- }
-
- //复制
- String Substr(size_t pos = 0, size_t n = npos) {
- if (n == npos)
- n = Size;
- if (pos + n >= Size) {
- n =Size - pos;
- }
- char* temp = new char[n + 1];
- strncpy(temp, Str + pos, n);
- temp[Size] = '\0';
-
- String Stemp(temp);
- delete[] temp;
- return Stemp;
- }
- //正向查找
- size_t Find(char c, size_t pos = 0) {
- for (int i = pos; i < Size; i++) {
- if (Str[i] == c)
- return i;
- }
- return npos;
- }
- //反向查找
- size_t Rfind(char c, size_t pos = npos) {
- pos = pos < Size ? pos : Size;
- for (int i = pos; i >= 0; i--) {
- if (Str[i] == c) {
- return i;
- }
- }
- return npos;
- }
- };
-
- size_t String::npos = -1;
-
- int main() {
- const char* sp = "abc12345566743212343455655";
- String s1(sp);
- String s2 = s1;
- auto it = s2.Begin();
- while (it != s2.End()) {
- cout << *it << " ";
- it++;
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。