C++【string的模拟实现】
在前文我们讲解了string类接口使用(C++【string类的使用】(上),C++【string类的使用】(下)),本片文章就来模拟实现string类。
注:本文实现的是string的部分重点内容,目的是为了更好的了解string,而不是实现出一个功能一模一样的string
文章目录
- 1. 三个文件的链接
- 1.1 string.h
- 1.2 string.cpp
- 1.3 test.cpp
- 2. 正式实现
- 2.1 string的内部成员变量
- 2.2 构造函数
- 2.3 析构函数
- 2.4 拷贝构造
- 1.深浅拷贝问题
- 2. 传统写法
- 3. 现代写法
- 2.5 operator=
- 传统写法
- 现代写法
- 2.6 常量成员对象npos
- 2.7 提供C接口 c_str
- 2.8 容量接口
- 2.9 operator[]
- 2.10 迭代器
- 2.11 修改容量 reserve
- 2.12 字符串的增加与删除
- push_back
- append
- operator+=
- insert
- erase
- 2.13 查找 find
- 2.14 字串 substr
- 2.15 string之间的比较
- 2.16 重载输出流 operator<<
- 2.17 重载输入流 operator>>
- 3. 完整代码
- string.h
- string.cpp
- test.cpp
- 4. 结语
1. 三个文件的链接
1.1 string.h
负责string类函数的声明(一些短小且频繁调用的函数会在类里实现)
1.2 string.cpp
负责string类函数的实现
1.3 test.cpp
负责测试函数是否成功
2. 正式实现
2.1 string的内部成员变量
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
};
- _str:是string类的关键,用来存放被开辟空间的地址
- _size:非空字符的个数(string类的长度)
- _capacity:string类当前的容量
- _npos:静态成员函数,用来当缺省值
2.2 构造函数
我们就实现两个构造:无参构造与字符串构造。
注意:无参构造不能给_str一个nullptr
,因为string本质上还是读取字符串,而字符串只有遇到\0
才会停止读取,如果你给了一个空指针,那么在查找_str的时候就查找不到\0
,就会一直找下去,这时的程序就已经无法使用了。
不管是无参构造,还是带参构造,都要有\0
,但是我们的_capacity
并不包含\0
,所以我们在开空间的时候是要多开一个空间的。
string()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity(0)
{}
string(const char* str)
{
_size = strlen(str);
_capacity = _size; //capacity不包含 '/0'
_str = new char[_size + 1];
strcpy(_str, str);
}
我们也可以将这两个函数合二为一,给const char* str
一个缺省值就好了。
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size; //capacity不包含 '/0'
_str = new char[_size + 1];
strcpy(_str, str);
}
注意:缺省值不需要给成const char* str ="\0"
,因为一个空字符串里面就默认包含了\0
,如果我们在多给一个,里面就会存储两个\0
,那就有点画蛇添足了。
2.3 析构函数
我们看到_str是申请了空间,那么编译器默认生成的析构就无法满足我们的需求了,我们就需要自己手搓一个析构函数。
其实析构就很简单,就是将申请的空间释放掉,将_str设置为空指针,你的_size和_capacity要不要置0都无所谓。
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
既然有了析构,那么拷贝构造与operator=
也需要我们自己手搓(他们是配套出现的)
2.4 拷贝构造
1.深浅拷贝问题
我们先看看没有手动实现拷贝构造,使用编译器默认的拷贝构造会发生什么事情。
可以看到发生了报错,那这么是为什么呢,我们结合图来一探究竟。
我们可以看到,这两个对象中_str的地址是一样的。
这是因为编译器默认生成的拷贝构造是每个字节每个字节的拷贝,这也就导致了s1,s2的_str都指向了同一块空间,当test函数结束,要需要销毁s1和s2,就会先销毁掉s2的_str,当s2的_str被销毁后,这个_str所指向的空间就已经被释放了,这时在销毁s1,就会使一个空间被释放两次,就会发生崩溃。
这种拷贝方式被称为浅拷贝。
浅拷贝就像一个家里有两个孩子,但只有一个玩具,当他们都想玩玩具,且只想自己玩的时候,就会你争我抢,导致玩具损坏。
可以用深拷贝来解决这个问题,,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
2. 传统写法
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
和构造函数思路一样,都是开辟一块新空间,然后拷贝数据。
3. 现代写法
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
这种写法非常巧妙,我们先用 s 的 _str 初始化一个临时变量tmp,然后将 tmp 的 _str 与 this 的 _str 交换,这样就完成了拷贝构造,同时也是深拷贝。
大致过程如下图:
2.5 operator=
和拷贝构造类似,也是有传统写法与现代写法。
传统写法
string string::operator=(const string& s)
{
if (this != &s)//判断是不是自己
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法
string string::operator=(string s)
{
swap(_str,s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
注意:这里函数参数string s
,是传值传参,发生了拷贝构造,这是为了不影响 = 右边的对象。
传统写法中,我们还需要手动的销毁原空间,创建新空间,拷贝数据;但是在现代写法,这些都不需要了,新空间新数据已经被 s 开好了,我们只需要交换就好了。
接下来就开始实现常量成员对象npos与一些常用接口。
2.6 常量成员对象npos
//常量成员对象
const size_t string::npos = -1;
npos的作用就是用来当缺省值,用来表示“到字符串结束”。
2.7 提供C接口 c_str
//只读字符串
const char* c_str() const
{
return _str;
}
提供只读字符串,返回 _str 即可, 这个_str 被const修饰,无法改变。
2.8 容量接口
查询容量,实现size()和capacity()即可。
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
2.9 operator[]
我们在使用那部分也讲过,返回对应位置的引用即可。
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.10 迭代器
之前已经提到过,现阶段我们可以将迭代器理解成一种指针,通过解引用来访问数据元素。我们定义string类时,使用指针来模拟迭代器。迭代器定义如下:
typedef char* iterator;
typedef const char* const_iterator;
我们只实现普通迭代器与const迭代器,反向迭代器后面再讲解。
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator cbgin() const
{
return _str;
}
const_iterator cend() const
{
return _str + _size;
}
要想支持范围for,我们的名字必须和库里的名字一样,因为范围for是傻瓜式替换。
我们现在试验下我们自己实现的访问
void test_string1()
{
string s1;
string s2("zhuo zhuo zhuo");
//cout << s1.c_str() << endl;
//cout << s2.c_str() << endl;
cout << "下标访问遍历:";
for (int i = 0; i < s2.size(); i++)
{
cout << s2[i] << ' ';
}
cout << endl;
string::iterator sit = s2.begin();
cout << " 迭代器遍历:";
while (sit != s2.end())
{
cout << *sit << ' ';
sit++;
}
cout << endl;
cout << " 范围for遍历:";
for (auto e : s2)
{
cout << e << ' ';
}
}
2.11 修改容量 reserve
这个虽然不属于字符串的增加与删除,但是每个增加元素的函数都会用到他,所以我就把它提取到这个模块了
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
else;
}
设计思路和vs是一样的,比原来的_capacity大我就扩容,比_capacity小都不扩容。
注意:到了C++,我们就要舍弃realloc()这个函数来扩容,全部都自己写,因为realloc无法自动调用构造函数,而new可以。
2.12 字符串的增加与删除
push_back
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
append
append有很多接口,我们实现一个尾插字符串就可以了
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t NewCapacity = 2 * _capacity;
reserve(_size + len > NewCapacity ? _size + len : NewCapacity);
}
strcpy(_str + _size, str);
_size += len;
}
operator+=
这个直接复用push_back()
和append()
就好了。
string += 字符
string& string::operator+= (char ch)
{
push_back(ch);
return *this;
}
string += 字符串
string& string::operator+= (const char* str)
{
append(str);
return *this;
}
operator+=,push_back,append
这三个都是在string尾部插入元素,要想直线在任意位置插入元素,那就需要insert
这个函数。
insert
我们同样只实现两个版本,插入字符与插入字符串,这两个版本都是在指定位置之前插入元素
string类内部的声明
void insert(size_t pos, char ch);//插入字符
void insert(size_t pos, const char* str);//插入字符串
- insert插入字符
既然要在之前插入字符,那挪动数据的时候,也需要挪动原pos位置的元素。
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (pos == _size)
{
push_back(ch);
}
else
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//挪动数据
size_t end = _size;
while (end != pos-1)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_str[++_size] = '\0';
流程如下图
- insert插入字符串
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
if (pos == _size)
{
append(str);
}
else
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t NewCapacity = 2 * _capacity;
reserve(_size + len > NewCapacity ? _size + len : NewCapacity);
}
//挪动数据
size_t end = _size+len;
while (end != pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//覆盖
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
}
注意:我们在挪动数据的时候,我们的结束条件一定不能写
e
n
d
≥
p
o
s
end \ge pos
end≥pos,因为在头插环境会无线循环。
所以就会无限循环
把 end 换成 int 类型行不行呢?
答案是不行的,这是C语言留下来的坑,当级别低的与级别高作为操作符的操作数时,级别低的变量会发生整形提升。
也就是说当end与pos位置比较的时候,end会自动提升为 size_t (unsigned_int) 类型
如果你把 pos 也改成 int 类型就能成功运行,但是我们要结合使用场景,如果我们要插入的字符位置是比int
正整数最大值大一点的数字,那么我们就会插入到负输上,但字符串是没有负数这个概念的,所以将 end 和 pos 都换成 int 是不行的。
正确方法就是上文代码的方法while (end != pos-1)
和while (end != pos + len - 1)
,!= 这个操作符不需要比较位置之间的大小,只需要看是否相等,如果相等,就停下来跳出循环。
而 -1 是为了插入字符串,因为我们要在指定位置前插入字符串,如果不 -1,那么就会少挪动一位,导致原本的数据丢失一位。
erase
void erase(size_t pos, size_t len = npos);
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
当要删除的字符个数大于_size后字符个数,直接在将pos位置置为\0
,然后将_szie更新。
如果小于,那就把后面的值往前覆盖,长度-=要删减的字符个数。
2.13 查找 find
这个就很简单了,直接遍历string就好了,如果是查找字符串,那就直接调用C语言string库的strstr
//查找字符
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//查找字符串
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
2.14 字串 substr
直接在函数里创建一个临时变量,让他来拷贝子串,然后出函数的时候再进行构造函数来进行深拷贝
string string::substr(size_t pos, size_t len) const
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (int i = 0; i < len; i++)
{
sub += _str[i + pos];
}
return sub;
}
2.15 string之间的比较
我们直接调用strcmp就可以了,重载==
加任意一个判断运算符(除!=
),其他就可以直接复用。
bool operator>(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
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);
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
2.16 重载输出流 operator<<
ostream& operator<<(ostream& out, string& s)
{
//out << s.c_str();
for (auto e : s)
{
out << e;
}
return out;
}
2.17 重载输入流 operator>>
istream& operator>>(istream& in, string& s)
{
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
in.get()
是输入流的一个函数,功能是在缓冲区中拿到空格和换行(一般的插入是不会读取的)。
3. 完整代码
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
#include<algorithm>
namespace zhuo
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
//string()
// :_str(new char[1]{'\0'})
// ,_size(0)
// ,_capacity(0)
//{}
//string(const char* str)
//{
// _size = strlen(str);
// _capacity = _size; //capacity不包含 '/0'
// _str = new char[_size + 1];
// strcpy(_str, str);
//}
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size; //capacity不包含 '/0'
_str = new char[_size + 1];
strcpy(_str, str);
}
//string(const string& s)
//{
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
//}
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator cbgin() const
{
return _str;
}
const_iterator cend() const
{
return _str + _size;
}
const char* c_str() const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos <= _size);
return _str[pos];
}
char& operator[](size_t pos) const
{
assert(pos <= _size);
return _str[pos];
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
void reserve(size_t);
//增删
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, size_t len = npos);
//查找
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos) const;
//string operator=(const string& s);
string operator=(string s);
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
};
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& out, string& s);
istream& operator>>(istream& in, string& s);
}
string.cpp
#include"string.h"
namespace zhuo
{
const size_t string::npos = -1;
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
else;
}
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t NewCapacity = 2 * _capacity;
reserve(_size + len > NewCapacity ? _size + len : NewCapacity);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+= (char ch)
{
push_back(ch);
return *this;
}
string& string::operator+= (const char* str)
{
append(str);
return *this;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (pos == _size)
{
push_back(ch);
}
else
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//挪动数据
size_t end = _size;
while (end != pos-1)
{
if (end == 0)
;
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_str[++_size] = '\0';
}
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
if (pos == _size)
{
append(str);
}
else
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t NewCapacity = 2 * _capacity;
reserve(_size + len > NewCapacity ? _size + len : NewCapacity);
}
//挪动数据
size_t end = _size+len;
while (end >= pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//覆盖
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len) const
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (int i = 0; i < len; i++)
{
sub += _str[i + pos];
}
return sub;
}
//string string::operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
string string::operator=(string s)
{
swap(_str,s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
bool operator>(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
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);
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, string& s)
{
//out << s.c_str();
for (auto e : s)
{
out << e;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
}
test.cpp
#include"string.h"
#include<string>
namespace zhuo
{
void test_string1()
{
string s1;
string s2("zhuo zhuo zhuo");
//cout << s1.c_str() << endl;
//cout << s2.c_str() << endl;
cout << "下标访问遍历:";
for (int i = 0; i < s2.size(); i++)
{
cout << s2[i] << ' ';
}
cout << endl;
string::iterator sit = s2.begin();
cout << " 迭代器遍历:";
while (sit != s2.end())
{
cout << *sit << ' ';
sit++;
}
cout << endl;
cout << " 范围for遍历:";
for (auto e : s2)
{
cout << e << ' ';
}
}
void test_string2()
{
string s1;
s1.push_back('a');
s1 += 'b';
cout << s1.c_str() << endl;
s1.append(" hello");
cout << s1.c_str() << endl;
s1 += " world";
cout << s1.c_str() << endl;
s1.insert(0, 'c');
cout << s1.c_str() << endl;
s1.insert(0, "cccc");
cout << s1.c_str() << endl;
s1.erase(1, 100);
cout << s1.c_str() << endl;
string s2("hello world");
s2.erase(1);
cout << s1.c_str() << endl;
string s3("0123456789");
s3.erase(2,5);
cout << s3.c_str() << endl;
}
void test_string3()
{
string s1("0123456789");
size_t pos = s1.find('3');
string s2 = s1.substr(pos);
cout << s2.c_str() << endl;
string s3("test.cpp.zip");
pos = s3.find("cpp");
string s4 = s3.substr(pos);
cout << s4.c_str() << endl;
}
void test_string4()
{
string s1("hello world");
cout << s1 << endl;
cin >> s1;
cout << s1 << endl;
}
void test_string5()
{
string s1("hello");
string s2(s1);
int s;
}
}
int main()
{
zhuo::test_string2();
return 0;
}
4. 结语
那么这次的分享就到这里结束了~
string类还是比较简单的~
最后感谢您能阅读完此片文章~
如果您认为这篇文章对你有帮助的话,可以用你们的手点一个免费的赞并收藏起来哟~
如果有任何建议或纠正欢迎在评论区留言~
也可以前往我的主页看更多好文哦(点击此处跳转到主页)。