目录
在前面的文章中已经验证了,在涉及资源管理的类中,拷贝构造函数,析构函数,赋值运算符重载函数都需要用户显示实现,如果使用编译器默认生成的,就会有浅拷贝的风险。如果有读者不理解浅拷贝,可以看下面这幅图。
浅拷贝通俗来讲就是多个对象指向了同一块空间,例如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";解决办法如下代码一:
代码一:
//代码一
#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
}
2.构造模块
(1)构造函数
这里实现两个构造函数。
String类中的成员应该有一个字符串指针,一个有效元素个数变量,一个容量大小变量,一个npos变量(因为这是原本的string类中就有的)。为了方便起见,在设计阶段先将所有成员的访问权限均设置为public。同时将构造函数定义成全缺省函数。
[1]构造函数一
代码三:使用字符串构造对象
//代码三
#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;
[2]构造函数二
代码四:使用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;
}
(2)析构函数
代码五:析构函数一定要检查字符串指针是否指向空,如果是空就不可以再释放了。
//代码五
~String() {
//如果指向空值,释放程序会崩溃的
if (Str)
delete[] Str;
Str = nullptr;
Size = 0;
Capacity = 0;
}
(3)拷贝构造函数
这里提供两种版本的拷贝构造函数以供参考。
[1]传统版本
代码六:传统版本是先释放指向的旧空间,构造新空间,然后使用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是因为申请空间时就申请了这么多
}
[2]简洁版本
代码七: 简洁版本是先使用目标字符串构造一个临时对象,然后交换两个对象的字符串指针。这样做不但不用担心空间申请失败,也不用自己手动释放空间。当临时对象生命周期结束,系统会自动调用析构函数。
//代码七
String(const String& s) {
String temp(s.Str);
//交换temp.Str和this->Str指向的空间
std::swap(Str, temp.Str);
Size = strlen(Str);
Capacity = Size+1;
}
(4)赋值运算符重载
赋值运算符重载也提供了两个版本以供参考。
[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;
}
[2]简洁版本
代码九:简介版本不需要考虑自己给自己赋值,反正都是交换空间。
//代码九
String& operator=(const String& s) {
String temp(s.Str);
//交换空间
std::swap(Str, temp.Str);
Size = strlen(s.Str);
Capacity = Size +1;
return *this;
}
3.容量模块
(1)有效字符函数
代码十:
//代码十
size_t SIZE() {
return Size;
}
(2)有效字符函数
代码十一:
//代码十一
size_t CAPAcity() {
return Capacity;
}
(3)扩容函数
代码十二:这里直接按照参数的两倍来扩容,记得需要释放原本的字符串空间。
//代码十二
void Reverse(size_t newCapacity) {
//新容量大于旧容量
if (newCapacity >= Capacity) {
char* temp = new char[newCapacity *2];
strcpy(temp, Str);
delete[] Str;
Str = temp;
Capacity = 2 * newCapacity;
}
}
(4)设置有效字符函数
代码十三:此函数机制为:新有效元素大于旧有效元素时可能需要扩容,继续判断,当新有效元素比容量还要大时需要扩容,扩容后使用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';
}
(5)判空函数
代码十四:
//代码十四
bool Empty() {
//容量等于0返回true
return Size == 0;
}
(6)清空函数
代码十五:注意清空的含义是清空有效元素,不对容量进行操作。
//代码十五
void clear() {
//只改变有效元素个数,不改变容量
Size = 0;
Str[0] = '\0';
}
4.迭代器模块
其实迭代器就是当作指针使用的,为了和string迭代器形式相似,我们要用这条语句给指针起个小名:typedef char* Iterator;
(1)正向迭代器
代码十六:
//代码十六
typedef char* Iterator;
Iterator Begin() {
return Str;
}
Iterator End() {
return Str + Size;
}
(2)反向迭代器
代码十七:反向迭代器可以直接复用正向迭代器。
//代码十七
typedef char* Iterator;
Iterator Rbegin() {
return End();
}
Iterator Rend() {
return Begin();
}
5.访问模块
(1)重载[]
代码十八:注意一定要判断是否越界访问。同时也要多重载一份为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];
}
(2)重载cout<<
代码十九:注意<<不可以重载为成员函数,但我们还要使用私有变量,就把它设置为友元函数。至于为什么不可以重载为成员函数,我在《C++实现日期类》中已经总结了,感兴趣的可以去看看。
//代码十九
friend ostream& operator<<(ostream& _cout, const String& s) {
_cout << s.Str;
return _cout;
}
6.修改模块
(1)尾插单个字符
代码二十:尾插时需要注意扩容条件。
//代码二十
void Push_back(char c) {
//满足条件说明空间已经存满了,因为那个1是用来存储'\0'的
if (Size + 1== Capacity) {
Reverse(Capacity);
}
Str[Size] = c;
Str[Size+1] = '\0';
Size++;
}
(2)重载+=
代码二十一:+=有两种,+=字符串、+=对象。后者可以复用前者。这里要特别注意如何判断是否要扩容,当两个字符串有效元素加起来再加上'\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;
}
7.特殊操作模块
(1)转为C语言字符串类型
代码二十二:
//代码二十二
const char* c_str()const
{
return Str;
}
(2)复制函数
代码二十三:注意参数的调整问题。
//代码二十三
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;
}
(3)正向查找函数
代码二十四:注意没找到就返回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;
}
(4)反向查找函数
代码二十五:
//代码二十五
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;
}
8.总体实现
以下是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++;
}
}