学到string终于就不用像c语言一样造轮子了,接下来我们就模拟一下string方便我们更好理解string,首先我们都知道库里有个string,所以为了避免我们的string和库里的冲突,要用命名空间my_string将我们写的string包含在内。string的成员变量和我之前写的顺序表差不多,首先是一个指针指向一块存放字符串的空间,还有_size记录该空间内的有效字符个数,不包括\0,然后就是_capcity记录该空间能存放有效字符的个数,当然,实际上由于我们末尾要放一个\0,所以我们在实现的时候实际开辟空间比容量稍微大一点。库里的容量则更加充裕。
一:构造函数,析构函数和赋值运算符重载
1.构造函数,析构函数实现:
#pragma once
#include<iostream>
#include<string>
using namespace std;
namespace my_string
{
class string
{
public:
//构造函数
string(const char*str="")//默认用空串初始化
{
都是内置类型,在函数体内初始化也挺方便,免得初始化列表的
书写顺序和声明顺序不同导致出错
int len = strlen(str);计算要开辟的空间
_str = new char[len + 1];
_capcity = len;
_size = len;
strcpy(_str, str);
_str[_size] = '\0';末尾补\0
}
~string()
{
delete[]_str;要注意的是new[]对应delete[],new对应delete,原因不好解释。
_size = 0;
_capcity = 0;
_str = nullptr;
}
const char* c_str()const将string以c语言的字符串形式返回
{
return _str;
}
private:
size_t _capcity;
size_t _size;
char* _str;
};
};
2.拷贝构造函数
string(const string& Src)
{
_str = new char[Src._size+1];
_size = Src._size;
_capacity = _size;
memcpy(_str, Src._str, Src._size+1);//将\0也一同拷贝
}
3.测试构造函数
void TestString1()
{
my_string::string s1("hello world");
cout << s1.c_str() <<endl;
}
首先c._str()函数将string s1以字符串的形式返回,也就是返回指针,并且cout内部有内置类型的输出重载,所以可以将其打印出来。
const修饰对象只会限制成员变量不被修改,却并不限制修改其成员指向的空间,也就是说如果c_str()返回指针不加const修饰,我们可以在外部用该指针修改const对象,这是不合理的,又为了保证const对象和普通对象都可以调用这个函数,所以对c_str()这个函数用了两个const修饰
4.赋值运算符重载
string.h
string& operator=(string tmp)
{
swap(tmp);
return (*this);//支持连续赋值
}
main.c
string s1;
string s2;
s1=s2;
此时s1=s3被编译器转为s1.operator(s2), s2是传值传参,要调用拷贝构造,原因我在我的博客类的六大成员函数中曾提及,tmp是s2的深拷贝对象,我们交换了s1和tmp的成员,这样s1指向的空间就是s3深拷贝后的了,也就完成了赋值,最妙的是我们把s1要析构的空间给了局部对象tmp,让其在函数调用结束时销毁,不用我们手动delete[]。
void swap(string s)
{
//复用库的swap函数
std::swap(_str, s._str);//直接交换两个对象成员
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
要注意的是我们不是直接调用库里的swap函数交换两个string对象,而是自己写个swap,内部调用库里的swap函数来交换string对象的成员来达到交换对象的目的。原因是库里的swap函数是如下的:
class T
swap(T a,T b)
T c(a);
a=b;
b=c;
当我们调用赋值的时候,用了swap函数,swap内部又是赋值,赋值内部又用swap,无穷递归调用,死循环,千万注意。
二:string的遍历
1:实现迭代器和范围for
在第一次使用迭代器时是std::iterator it,现在我才意识到原来iterator实质上是char*的typedef,下面的begin()函数如果被const修饰,const对象可以调用,非const对象也可以调用,但是这样的话返回类型就只能是const char*了,那对于普通对象来说就无法修改了,所以要再实现一个const修饰的begin()函数,下面还重定义了const char*就是为了服务const对象。
public:
typedef char* iterator;
typedef const char* const_iterator;
char& operator[](int i)
{
return _str[i];
}
const char& operator[](int i)const
{
//_size,_str,_cap不可修改,因为this指针此时是const string*const
//若是const不修饰char&,_str指向的字符串可修改
return _str[i];
}
iterator begin() 对const对象不适用,所以要再写一个
{
return _str;
}
iterator end()
{
return _str + _size;
}
不会修改成员变量的成员函数都可以用const修饰,这样就方便const对象和普通对象的调用了。
size_t size()const//获取string中的有效字符个数
{
return _size;
}
char* c_str()const//将string以c语言的字符串形式返回
{
return _str;
}
如下为const对象的begin()和end()函数,返回类型是const_char*,为了和char*的重命名作区分,命名为const_iterator,注意:const对象的迭代器不可修改,所以返回的指针要用const修饰,否则该指针同样在外部会被用来修改const对象。
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
2.测试遍历功能
在准备好了成员函数后,就可以测试遍历功能了,下面提供三种遍历方式。
for (size_t i = 0; i < s1.size(); i++)//测试size()和[]重载
{
cout << s1[i];//测试[]重载
}
cout << endl;
//迭代器
//my_string::string::iterator it = s1.begin();
auto it = s1.begin();
是不是感觉it的类型名特别长,这个时候auto自动推导类型名的作用就很舒服了
while (it != s1.end())
{
cout << *it;
it++;
}
cout << endl;
范围for,范围for在底层被替换成上述的迭代器,而且是按照既定格式来替换的,
如果我们模拟的begin函数名改为Begin,范围for就无法运行,因为范围for被替换后是用begin函数名访问。
for (auto ch : s1)
{
cout << ch;
}
cout << endl;
三:string的添加字符
1.push.back尾插一个字符
void Reserve(size_t n=0)//扩容
{
char*tmp = new char[n + 1];//预留一个位置给\0
memcpy(tmp, _str, _size);
delete[]_str;
_str = tmp;
_capcity = n;
}
上面的扩容函数中我们用memcpy来把原先存在的字符串复制到tmp中,之所以不用strcpy,是因为我们考虑string中可能会存入"hello \0world'。
void push_back(char c)//尾插一个字符
{
if (_size == _capcity)//扩容
{
Reserve(_capcity==0?4:2*_capcity);
当string对象为空串时,给个初始容量
其余情况扩大二倍
}
_str[_size++] = c;
_str[_size] = '\0';//末尾补充\0
}
string& operator+=(char s)
{
push_back(s);
return (*this);
}
2.append尾插一个字符串
void append(const char* s)//尾插一个字符串
{
int len = strlen(s);
if (_size + len > _capcity)先判断size+len是否会超出容量
{
Reserve(_size+len); 扩容到_size+len而不是2*_capcity,
2*_capcity不一定大于_size+len
}
else
{
for (int i = 0; i < len; i++)
{
_str[_size++] = s[i];//插入字符
}
}
}
+=运算符重载,便于调用,且代码看起来更加规整
string& operator+=(const char* s)
{
append(s);
return (*this);
}
代码中使用+=运算符重载比直接append和push_back函数更加美观,如下。
s += 'w';
s += 'o';
s += "hello ";
s += "world";
对比
s.push_back('w');
s.push_back('o');
s.append("hello");
s.push_back("world");
3.在pos位置添加字符或者字符串
string& insert(size_t pos,char ch,size_t n)
{
assert(pos <= _size);
//判断容量
if (_size + n > _capacity)
{
Reserve(_size + n);
}
//挪动数据
size_t end = _size;//从\0开始移动
while (end>=pos && end!=npos) end为size_t,当pos=0时,end无法跳出循环
,所以添加一个判断条件
{
_str[end+n] = _str[end];
end--;
}
//插入数据
_size += n;
while (n)
{
_str[pos++] = ch;
n--;
}
return (*this);
}
string& insert(size_t pos,const char*src)
{
assert(pos <= _size);
int len = strlen(src);
//判断容量
if (_size + len> _capacity)
{
Reserve(_size + len);
}
//挪动数据
size_t end = _size;//从\0开始移动
while (end >= pos && end != npos)
{
str[end + len] = _str[end];
end--;
}
//插入数据
_size += len;
for (int i = 0; i < len; i++)
{
_str[pos++] = src[i];
}
return (*this);
}
四:删除string对象的字符
len的缺省值为npos,由于npos为-1,对于无符号数len来说是四十几亿,而实际上字符串都不会这么大,也就表示删除pos位置后全部的字符。
void erase(size_t pos=0,size_t len=npos) 删除pos位置以及之后len个字符
{
if (len==npos||pos + len>= _size)//删到末尾
{
_str[pos] = '\0';
_size = pos;
}
else
{
挪动覆盖数据
size_t begin = pos + len;
while (begin <= _size) 将后面未删除字符以pos为起点往后放
{ 包括了\0
_str[pos++] = _str[begin++];
}
_size -= len;
}
}
五:查找字符
1.从指定位置查找字符串,返回字符串第一次出现位置的首字符地址
size_t find(const char*goal,size_t pos=0)
{
if (pos >= _size)
return npos;//下标非法,返回-1
char*begin = strstr(_str+pos, goal);//复用库函数
if (begin == NULL)
return npos;//找不到,返回-1
return begin - _str;
}
2.从指定位置查找字符,返回字符下标
size_t find(char goal, size_t pos = 0)
{
if (pos >= _size)
return npos;//下标非法,返回-1
for (size_t i =pos; i < _size; i++)//遍历判断
{
if (_str[i] == goal)
return i;
}
return npos;//找不到,返回-1
}
六:截取字符串
string substr(size_t pos=0,size_t len=npos)
{
assert(pos <= _size);
string ret;
if (len==npos||pos + len > _size)//截取到末尾
{
此处有个细节是先判断len是否等于npos,可以免得当len=npos时,len+pos出现栈溢出
erase函数处同理
len = _size - pos;//处理len
}
for (size_t i = pos; i < len+pos; i++)
{
ret += _str[i];//ret无需处理\0,+=会处理
}
return ret;
}
七:流插入流提取
1.流插入
可以选择将流提取和流插入函数放在全局域或者命名空间内,最好直接放在命名空间内,免得冲突,但是放在命名空间的时候,若在命名空间内声明,外写定义,定义处要指定命名空间域,否则函数调用时编译器看到命名空间内有个声明,会认为有个定义,虽然没找到,而全局处的函数定义又没有指定命名空间,就不会认为是命名空间中那段声明对应的函数定义,这样函数调用时就会出现调用不明确。
ostream& my_string::operator<<(ostream& out, const my_string::string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i]; s[i]调用的是类的公有函数,
用公有函数访问string对象的字符不受访问限定符限制
}
out << endl;
return out;
}
2 流提取
istream& my_string::operator>>(istream& in, my_string::string& s)
{
s.clear();
char ch = 0;
ch = in.get();
while (ch == ' ' || ch == '\n')
{
ch = in.get(); 处理有效字符前的分隔符
}
int i = 0;
char arr[128] = { 0 }; 模拟缓冲区,防止s频繁扩容
while (ch != ' ' && ch != '\n')//当ch读到分隔符,一个string对象读取结束
{
arr[i++] = ch;//先存到数组中去
ch = in.get();
if (i == 127)//数组满了后,一次性填入s中
{
arr[i] = '\0';
i = 0;
s += arr;
}
} 可能i小于127,此时又读到了分隔符,要在外面判断是否要处理数组中剩下的字符
if (i != 0)
s += arr;
return in;
}
字数有点多,但是string是我们学习c++的关键,对于理解vector和list有着非常大的作用,个人的一些理解希望对大家有帮助。