一、前言
在我们对STL中的string类有了一个基本的认识后,今天我们手动的从0到1去模拟实现一个STL库中的string类中的一些常用接口。
二、string类的模拟实现
为了不和库里的string类产生冲突,我们可以建立一个名为其他的命名空间,此时因为作用域不同,就不会产生冲突。即namespace xxx.
下面是对string类的声明:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
namespace zihan//命名空间
{
class string//定义类
{
public:
//构造
string(const char* str = " ")//全缺省
{
_size = strlen(str);
_capacity = _size;
//_capacity中不包含\0,为了存放字符 \0 需要长度加一
_str = new char[_capacity + 1];
strcpy(_str, str);//拷贝字符串
}
//显示调用拷贝构造
//S2(S1)
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//赋值 S1=S2
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()
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
//返回pos位置的字符
const char& operator[](size_t pos)const
{
return _str[pos];
}
//清掉所有的数据但不动空间
void clear()
{
_str[0] = '\0';
_size = 0;
}
//预留空间
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, size_t len = npos);
//查找字符、字符串
size_t find(char ch, size_t pos = 0);
size_t fing(const char* str, size_t pos = 0);
//截取字符串
string substr(size_t pos, size_t len);
//比较
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;
private:
char* _str = nullptr;
size_t _capacity = 0;
size_t _size = 0;
static const size_t npos;
};
//流插入 流提取
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& out, string& s);
}
2.1成员函数
2.1.1构造函数
string(const char* str = " ")
string(const char* str = " ")//全缺省
{
_size = strlen(str);
_capacity = _size;
//_capacity中不包含\0,为了存放字符 \0 需要长度加一
_str = new char[_capacity + 1];
strcpy(_str, str);//拷贝字符串
}
2.1.2拷贝构造函数
string(const string& s)
我们知道在拷贝构造中,若是一个类在没有显示定义拷贝构造,对于内置类型不做处理,对于自定义类型会去调用类中默认提供的拷贝构造函数,此时就会造成浅拷贝问题。
浅拷贝:编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一对象还不知该资源已被释放,以为还有效,若继续对资源进行操作时,就会发生访问违规。
深拷贝:源对象和拷贝对象相互独立,其中一个对象的改动不会对另一个对象造成影响
若想进行深拷贝,必须显示定义拷贝构造
1.传统写法
//显示调用拷贝构造
//S2(S1)
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
传统写法的主要思想是开辟一块足够的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也拷贝过去即可。
2.现代写法
void swap(string& s)
{
std::swap(_str , s._str);//调用标准库中的swap函数将对象的成员变量进行交换成员变量
std::swap(_size , s._size);
std::swap(_capacity , s._capacity);
}
//现代写法
string(const string& s)
{
string tmp(s._str);//带参构造
swap(tmp);//通过交换去掠夺
}
现代写法的主要思想是先根据原字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象进行数据交换。
2.1.3赋值运算符重载函数
string& operator=(const string& s)
1.传统写法
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;
}
2.现代写法
//S1=S2
//现代写法
string& operator=(const string& s)
{
//不能自己赋值自己
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
这里通过用源对象字符串构造一个tmp对象, tmp对象与s1交换之后,出了作用域也会销毁,可以达到将s2赋值给s1的效果。
2.1.4析构函数
~string()
~string()
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
2.2元素访问
2.2.1迭代器
本质上是封装思想,可以类比指针,可以指向容器中的某个元素,并可以通过操作迭代器来访问和修改容器中的元素 。
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
//普通迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//常量迭代器
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
2.2.2 返回pos位置的字符
const char& operator[](size_t pos)const
//返回pos位置的字符
const char& operator[](size_t pos)const
{
return _str[pos];
}
2.3字符串修改
2.3.1push_back 尾插字符
//尾插字符
void string::push_back(char ch)
{
//可能要扩容
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
//不要忘记\0
_str[_size] = '\0';
}
2.3.2append尾插字符串
//尾插字符串
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容 如果长度大于2倍 则该多大就开多大 否则就开2倍的空间
reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
2.3.3operator+=(char ch)
复用push_back即可
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
2.3.4operator+=(const char* str)
复用append即可
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
2.3.5在pos位置插入字符insert
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
//万一要扩容
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
//进行挪动 end 挪向end+1
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
注意这里挪动数据时的while循环的判断条件end>=pos,因为end和pos都是size_t无符号类型,所以当_size=0时,end无论怎么— —都不会小于_size,也就会陷入死循环 。
这里有解决方法:1.把无符号类型强转为有符号类型,进行整型提升
2.令end=_size+1,循环条件变成>
2.3.6在pos位置插入字符串insert
void string::insert(size_t pos, const char* str)
{
assert(pos <=_size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容 如果长度大于2倍 则该多大就开多大 否则就开2倍的空间
reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);
}
size_t end = pos + len;
while (end >= pos+len)
{
_str[end] = _str[end - len];
end--;
}
//再依次把字符串插进去
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
2.3.7删除pos之后的n个字符
void string::erase(size_t pos, size_t len )
void string::erase(size_t pos, size_t len )
{
if (len > _size - pos)
{
_str[pos] = '\0';
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
}
_size -= len;
}
这里分两种情况:
1.如果len>_size-pos,说明pos之后的全删除,只需把pos位置置为'\0'
2.否则需要将不删除的字符一个个往前挪动
2.4比较运算符
2.4.1大于>
bool string::operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
2.4.2小于<
bool string::operator<(const string& s)const
{
return strcmp(_str, s._str) < 0;
}
2.4.3等于==
bool string::operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
2.4.4大于等于>=
这里可以复用>和=,也可以对<取反
bool string::operator>=(const string& s)const
{
return !(*this < s);
}
2.4.5小于等于<=
同理,这里可以复用<和=,也可以对>取反
bool string::operator<=(const string& s)const
{
return !(*this > s);
}
2.4.6不等于!=
bool string::operator!=(const string& s)const
{
return !(*this == s);
}
2.5查找和截取
2.5.1find,寻找一个字符
size_t find(char ch, size_t pos = 0);
size_t string::find(char ch, size_t pos)
{
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
return npos;
}
}
2.5.2find,寻找一个字符串
size_t fing(const char* str, size_t pos = 0);
这里直接使用C语言中的库函数strstr函数
size_t string::fing(const char* str, size_t pos)
{
const char* ptr = strstr(_str + pos, str);//str在_str+pos中的位置
if (ptr)
{
return ptr - _str;//找到了,返回下标,所以指针相减
}
return npos;//没找到
}
2.5.3substr,截取字符串
string string::substr(size_t pos, size_t len)
//截取字符串
string string::substr(size_t pos, size_t len)
{
if (len > _size - pos)//大于有效字符个数
{ //更新len
len = _size - pos;
}
//最后需要返回字符串
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
//把要截取的字符尾插到sub
sub += _str[pos + i];
}
return sub;
}
在更新好有效字符个数后,从pos位置取n个字符追加到临时的string对象中去, 最后将其返回即可,这里我们返回了一个出作用域就销毁的临时变量,只能使用传值返回,不能使用传引用返回。
2.6空间操作
2.6.1size
获取当前字符串的有效长度,不包含'\0'
size_t size()const
{
return _size;
}
2.6.2capacity
获取字符串的当前的容量
size_t capacity()const
{
return _capacity;
}
2.6.3clear清空数据
清掉所有的数据但不动空间
//清掉所有的数据但不动空间
void clear()
{
_str[0] = '\0';
_size = 0;
}
2.6.4reserve预留空间,扩容
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//开出一块新的空间,永远要多开一个空间 存\0
strcpy(tmp, _str);//将原本的数据拷贝过来
delete[]_str;//释放旧空间
_str = tmp;//指向新空间
_capacity = n;
}
}
2.7流插入,流提取
2.7.1operator<< 流插入
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch: s)//范围for遍历
{
out << ch;
}
return out;
}
注意这里不用加友元,因为并没有访问私有成员变量
2.7.2operator>> 流提取
istream& operator>>(istream& in, string& s)
{
s.clear();//清理字符串
char ch = in.get();//一个一个字符获取
while (ch != ' ' && ch != '\n')
{
s += ch;//将读取到的字符插到字符串后面
ch = in.get();//继续读取字符串
}
return in;
}
可以进行优化,遇到长度比较大的字符串时可以减少扩容次数
istream& operator>>(istream& in, string& s)
{
s.clear();
const int n = 256;
char buff[n];
char ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i = n - 1)//如果字符存满了
{
buff[n - 1] = '\0';
s += buff;
i = 0;//继续循环 直到读完
}
if (i > 0)//如果没有读满数组 还有剩余空间
{
buff[i] = '\0';
s += buff;
}
return in;
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
样例测试:
以上就是string类基本接口的模拟实现,有不足的地方还请大家指正,共同交流!