结合我们的使用,我们可以发现string事实上就是一个字符串,但是里面添加了统计容量和字节大小的两个成员变量~
因此,私有成员我们可以像下面这样:
class my_string//与库里面的进行区分
{
private:
char* _str;//字符指针
size_t _size;//统计长度
size_t _capacity;//统计最大容量
};
我们可以看到在库里面的string类里面,有npos
被定义为一个成员常量,这个npos是有什么作用呢?我们先来了解一下~
std::string
类里的npos
是一个静态常量,用于表示一个无效的位置或未找到的位置。npos
的类型是std::string::size_type
,它通常是一个无符号整数类型,例如size_t
。npos
的值通常是该无符号整数类型所能表示的最大值。在查找操作(例如find
、rfind
、find_first_of
、find_last_of
、find_first_not_of
和find_last_not_of
)失败时,这些函数会返回npos
。
所以我们自己实现的时候也要注意加上npos这样一个静态常量,同时在类外进行初始化~
class my_string//与库里面的进行区分
{
private:
char* _str;//字符指针
size_t _size;//统计长度
size_t _capacity;//统计最大容量
const static size_t _npos;//静态常量,用于表示一个无效的位置或未找到的位置
public:
//成员函数
};
事实上,我们这里还可以像这样优化,为了与库里面的函数更好地区分,我们可以使用前面我们提到过的命名空间域~
namespace Xiaodu
{
class my_string//与库里面的进行区分
{
private:
char* _str;//字符指针
size_t _size;//统计长度
size_t _capacity;//统计最大容量
const static size_t _npos;//静态常量,用于表示一个无效的位置或未找到的位置
public:
//成员函数
};
}
知道了内部大致的结构,接下来我们就来试一试实现它的接口~首先因为总体代码量较大,所以我们把声明和定义分开在不同文件中,这里就需要再创建一个string.cpp源文件~同时包含我们自己的头文件string.h
我们知道string的构造有很多种调用方法,这里我们来模拟实现一下比较常见的接口~
注意:通过前面一篇博客我们知道成员变量里面的_size和_capacity都是不包含字符串结束标志'\0'的,但是在开空间的时候我们需要注意多开一个保证字符串正常结束~
1、使用常量字符串进行初始化~ 像以前,我们初始化会建议在初始化列表进行初始化,这一次为了代码更加简便,我们首先使用初始化列表初始化string的长度~剩下的就可以使用已经初始化的长度在函数体里面进行初始化,更加高效~
namespace Xiaodu
{
const size_t my_string::_npos = -1;
//构造
my_string::my_string(const char* str)//常量字符串
:_size(strlen(str))//使用strlen求有效长度来初始化
{
//使用已经初始化的_size来初始化剩下的
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
}
由于这里我们还没有对流插入流提取运算符进行重载,所以我们通过监视窗口进行查看:
我们给了需要参数的构造函数,那么系统就不会再提供默认构造函数了,所以我们还需要自己写一个默认构造函数~
//默认构造
my_string::my_string()
{
_str = new char[1];
_str[0] = '\0';//一个字符保存'\0'
_size = 0;
_capacity = 0;
}
2.使用n个字符进行初始化,那么这里就需要两个参数,一个是我们希望的字符长度,一个是初始化的字符~这里的初始化我们就可以完全使用初始化列表进行初始化,因为字符串的长度是已经知道的~
my_string::my_string(size_t n, char ch)
:_size(n),_str(new char[n+1]),_capacity(n)
{
for (int i = 0; i < n; i++)
{
_str[i] = ch;
}
_str[_size] = '\0';//结束标志\0
}
我们可以发现通过这些构造函数,成功进行了初始化~
有了构造,当然不能少了析构函数~通过前面的成员变量,我们知道字符指针是我们需要处理的~
以及将一些成员变量更改~
//析构
my_string::~my_string()
{
delete _str;//new,delete匹配使用
_str = nullptr;
_size = 0;
_capacity = 0;
}
为了后面更好的使用,我们这里先进行<<的运算符重载~
前面我们就知道这个运算符在类外进行定义才更加符合我们平时的使用习惯~这里我们可以让它输出我们想输出的内容~这里为了后面好观察,我们先让它把成员都进行输出,后面再进行修改~
ostream& operator<<(ostream& out, my_string& s)
{
out << "_str:" << s._str << endl;
out << "_size:" << s._size << endl;
out << "_capacity:" << s._capacity << endl;
return out;
}
拷贝构造也是一个十分重要的点,这里我们自己实现一下~
//拷贝构造
//s2(s1)
my_string::my_string(const my_string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
这是比较老老实实的写法,我们还有一种投机取巧的方法就是使用构造函数,让它给我构造一份,让别人实现~
//现代写法
my_string::my_string(const my_string& s)
{
my_string tmp(s._str);//让构造帮我们写
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
与拷贝构造相比,赋值运算符有下面的特点:
=
)将一个对象的值赋给另一个同类型的已存在对象时,会调用重载的赋值运算符。T&
),这允许进行链式赋值操作。例如,a = b = c;
这样的表达式在重载了赋值运算符后是可以正常工作的。//赋值运算符重载
//my_string::operator=——这样指定类域
//s4=s3
my_string& my_string::operator=(my_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;
}
在C++的
std::string
类中,reserve
成员函数的作用是请求改变字符串的容量(capacity)。这里的“容量”指的是字符串在不进行内存重新分配的情况下可以存储的最大字符数。需要注意的是,容量一般情况下并不等同于字符串当前的长度(size),而是指字符串内部为存储字符所预留的空间大小~
这里我们自己来实现一下,通过上一篇博客,我们知道reserve只有在更改容量比它原来的容量的时候会进行扩容~其他情况下,容量不会发生变化~
void my_string::reserve(size_t n)
{
if (n > _capacity)//修改容量比原来的大就进行扩容
{
char* tmp = new char[n + 1];
//创建一个中间指针变量修改容量
strcpy(tmp, _str);
delete _str;
_str = tmp;
_capacity = n;
}
}
std::string
类的push_back
成员函数用于在字符串末尾添加单个字符。它自动处理内存管理,若当前容量不足则增加容量。通过push_back
,可以逐个字符地构建或扩展字符串,无需手动管理内存分配和字符串大小调整。
void my_string::push_back(char ch)
{
//判断容量是否足够
if (_capacity == _size)//容量不够进行扩容
{
//这里就可以使用reserve进行代码复用
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++]=ch;//插入字符后长度增加
_str[_size] = '\0';
}
在C++中
std::string
类提供append
的成员函数,用于在字符串的末尾追加内容。我们知道这个函数有多个重载版本,允许我们以不同的方式追加数据。
这里我们同样实现一个常用的接口~增加一个常量字符串内容~
void my_string::append(const char* s)
{
//求出插入字符串长度
size_t l = strlen(s);
//容量不够进行扩容
if (_size + l > _capacity)
{
//默认两倍扩容,还是不够就按需扩容
size_t newcapacity = _capacity * 2;
if (_size + l > newcapacity)
{
newcapacity = _size + l;
}
//1.自己重新写
char* tmp = new char[newcapacity + 1];//容量记得+1,需要一个字符串结束标志
strcpy(tmp, _str);
delete _str;
_str = tmp;
_capacity = _size + l;
//2.代码复用
//reserve(newcapacity);
}
//拷贝剩下的
strcpy(_str + _size, s);
//_size增加
_size += l;
}
+=
运算符可以用于将一个字符串(或字符)追加到另一个字符串的末尾~
操作内容与我们前面写到的尾插和append有一些类似,这里我们就可以进行代码复用,更加方便快捷~
//+=一个字符
my_string& my_string::operator+=(char c)
{
push_back(c);
return *this;
}
//返回引用,减少拷贝
//+=一个字符串
my_string& my_string::operator+=(const char* c)
{
append(c);
return *this;
}
库里面实现的insert也有很多的接口,这里我们只实现常用的接口~该函数用于在字符串中的指定位置插入另一个字符串或字符
1.指定位置开始插入n个相同字符
//insert字符
void my_string::insert(size_t pos, size_t n, char c)
{
//判断插入位置和插入个数是否合法
assert(pos < _size);
assert(n > 0);
//判断容量是否足够
//不够进行扩容
if (_size + n > _capacity)
{
//默认两倍扩容,还是不够就按需扩容
size_t newcapacity = _capacity * 2;
if (_size + n > newcapacity)
{
newcapacity = _size + n;
}
//代码复用
reserve(newcapacity);
}
//移动后面的字符
size_t end = _size + n - 1;
//从后面开始移动,避免覆盖
while (end > pos + n - 1)
{
_str[end] = _str[end - n];
end--;
}
//插入字符
for (size_t i = pos; i < pos + n; i++)
{
_str[i] = c;
}
_size += n;
//字符串末尾置为'\0'
_str[_size] = '\0';
}
2.指定位置开始插入字符串
这里与前面插入一个字符逻辑是相似的,我们可以进行代码复用,也可以重新写~
方法一:重新写
void my_string::insert(size_t pos, const char* s)
{
//方法一:重新写
//判断插入位置和插入个数是否合法
size_t n = strlen(s);
assert(pos < _size);
assert(n);
//判断容量是否足够
//不够进行扩容
if (_size + n > _capacity)
{
//默认两倍扩容,还是不够就按需扩容
size_t newcapacity = _capacity * 2;
if (_size + n > newcapacity)
{
newcapacity = _size + n;
}
//代码复用
reserve(newcapacity);
}
//移动后面的字符
size_t end = _size + n - 1;
//从后面开始移动,避免覆盖
while (end > pos + n - 1)
{
_str[end] = _str[end - n];
end--;
}
//插入字符串里面的字符
for (size_t i = pos; i < pos + n; i++)
{
_str[i] = s[i - pos];
}
_size += n;
//字符串末尾置为'\0'
_str[_size] = '\0';
}
方法二:代码复用
void my_string::insert(size_t pos, const char* s)
{
//方法二:代码复用
size_t n = strlen(s);
//先占位置,再重新赋值
insert(pos, n, 'x');
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = s[i];
}
}
我们可以看见,它们达到了相同的效果,显然代码复用代码量大大减少,所以我们更加推荐使用代码复用~
插入我们都会了,相信删除更是不在话下~
//erase
void my_string::erase(size_t pos, size_t n)
{
//判断删除位置
if (pos + n >= _size)
//也就是后面的全部删除
{
_str[pos] = '\0';
_size -= n;
}
else
//删除直接进行覆盖就好了
{
//把后面的字符向前面移动
size_t begin = pos + n;
while (begin < _size)
{
_str[begin - n] = _str[begin];
begin++;
}
_size -= n;
_str[_size] = '\0';
}
}
在C++中
std::string
类提供了一个名为find
的成员函数,用于在字符串中查找子字符串或字符~1.查找字符 返回值:如果找到字符,则返回其位置索引;否则返回
std::string::npos
。
//find字符
size_t my_string::find(char c,size_t pos)
//默认从开头开始找
{
//遍历查找
size_t n = strlen(_str);
for (size_t i = pos; i < n; i++)
{
if (_str[i] == c)
{
//找到了返回下标
return i;
}
}
//找不到返回_npos
return _npos;
}
2.查找子字符串 返回值:如果找到子字符串,则返回其第一个字符在字符串中的位置索引;否则返回
std::string::npos
。
查找子字符串,我们可以使用库里面为我们提供的strstr标准库函数,用于在一个字符串中查找另一个字符串的第一次出现,这个函数是定义 <cstring>
(或旧式的 <string.h>
)头文件中~
str1
是要搜索的字符串(也称为源字符串)str2
是要在 str1
中查找的子字符串如果 str2
是 str1
的子串,则 strstr
返回一个指向 str1
中第一次出现 str2
的起始位置的指针。如果 str2
不是 str1
的子串,则返回 nullptr
~
有了这个库函数,我们实现就十分方便~
//find字符串
size_t my_string::find(const char* s, size_t pos)
{
const char* p = strstr(_str + pos, s);//从指定位置开始查找子字符串
if (p == nullptr)//没有找到,返回_npos
{
return _npos;
}
else//找到了,返回下标索引
{
return p - _str;
}
}
在C++中,
substr
用于获取字符串的一个子串,这个函数允许你指定开始的位置(索引)以及要复制的字符数,从而从原始字符串中提取出所需的部分~需要注意的是它不修改原始字符串:substr
函数不会修改调用它的原始字符串对象,而是返回一个新的字符串对象,该对象包含提取的子串~
//substr
my_string my_string::substr(size_t pos, size_t len)
{
size_t n = strlen(_str);
assert(pos >= 0);
assert(pos < n);//判断拷贝位置有效
my_string tmp;//调用默认构造
tmp._str = new char[len + 1];
//进行拷贝
for (size_t i = pos; i < pos + len; i++)
{
tmp._str[i - pos] = _str[i];
}
tmp._str[len] = '\0';
tmp._size = tmp._capacity = len;//修改容量和长度
return tmp;
}
前面C语言阶段,我们就知道有strcmp函数可以用来进行字符串比较,当然C++兼容C语言,我们简单回顾一下,实现代码复用~
一、函数原型 strcmp用于比较两个字符串的大小~
该函数接受两个参数,分别为要比较的两个字符串的指针(const char*类型),并返回一个整数来表示两个字符串的大小关系~ 二、返回值 strcmp函数的返回值是一个整数,用于表示两个字符串的大小关系,具体规则如下:
三、比较原理 strcmp函数比较两个字符串是按照字典序进行比较的,即逐个字符进行比较,直到遇到不同字符或字符串结束符'\0'。比较规则如下:
结合前面的经验,事实上,实现其中的几个,实现的通过代码复用就可以实现了,我们一起来看看代码~
//==
//s1==s2
bool my_string::operator==(const my_string& s)const
{
return strcmp(_str, s._str) == 0;//判断返回值是否等于0
}
//!=
bool my_string::operator!=(const my_string& s)const
{
return !(*this == s);//代码复用
}
//<
bool my_string::operator<(const my_string& s)const
{
return strcmp(_str, s._str) < 0;
}
//<=
bool my_string::operator<=(const my_string& s)const
{
return (*this == s) || (*this < s);
}
//>
bool my_string::operator>(const my_string& s)const
{
return !(*this <= s);
}
//>=
bool my_string::operator>=(const my_string& s)const
{
return !(*this < s);
}
当然,代码复用有很多种方式,选择自己喜欢的就好了~
测试:
前面我们重载的流插入运算符重载,是为了我们方便观察,而不使用监视进行观察,接下来我们来实现真正的模拟string类里面的流插入/流提取运算符~
我们先来看看库里面的<<实现连续输入的效果:
我们可以发现不同字符串之间是没有进行换行处理的,所以我们也类似这样处理~
ostream& operator<<(ostream& out, my_string& s)
{
cout << s._str;
return out;
}
接下来,我们来看看流提取运算符,首先看看库里面的效果~
我们不难发现,默认空格就是分隔符,所以我们输入两个字符串也就分别在s3、s4中~
我们也就可以写出下面的代码:
//>>运算符重载
istream& operator>>(istream& in, my_string& s)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
可是测试的时候,我们无论是输入空格还是换行符都不会发生变化,这是因为cin像scanf
函数一样根据提供的格式字符串来解析输入,会忽略起始的空白字符(如空格、制表符、换行符),并在遇到与格式不符的字符时停止读取,而cin
则更加自动化,它会忽略所有的空白字符,直到遇到非空白字符为止,或者达到文件结束符(EOF),cin
默认会忽略输入流中的前导空白字符,这包括空格、制表符以及换行符,所以我们前面输入的空格和换行符被它忽略了,这个时候我们就需要调用get来获取一个个字符~
事实上,上面的代码还有一个问题就是字符串在原来的基础上增加,而我们库里面是直接覆盖的,所以再输入内容前还需要对原来的内容进行清空~
//>>运算符重载
istream& operator>>(istream& in, my_string& s)
{
//清理原来的字符串
s.clear();
char ch = in.get();//调用get
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
这样也就达到了我们想要的效果~
我们还可以继续将上面的代码进行优化,使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据,这样如果输入一个大的字符串减少扩容次数,提高效率~
//>>运算符重载(优化版)
istream& operator>>(istream& in, my_string& s)
{
//清理原来的字符串
s.clear();
const size_t N = 1024;//可以自己指定大小
char arr[N];//使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据
int i = 0;
char ch = in.get();//调用get
while (ch != ' ' && ch != '\n')
{
arr[i++] = ch;
if (i == N - 1)
{
arr[i] = '\0';
s += arr;//字符串很长的情况下,调用+=可以按需扩容
i = 0;//i重新开始保存数据
}
ch = in.get();
}
if (i > 0)//arr里面还有字符数据
{
arr[i] = '\0';//末尾置为'\0'再进行插入
s += arr;
}
return in;
}
这比前面一个个字符写入就更加高效~
getline
是一个用于从输入流(如std::cin
、文件流等)读取一整行文本的函数~getline
本身不是std::string
类的一个成员函数
这个与>>运算符重载就十分类似了,只需要把循环条件修改一下就可以了~
//getline
istream& getline(istream& in, my_string& s, char end)
{
//清理原来的字符串
s.clear();
const size_t N = 1024;
char arr[N];//使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据
int i = 0;
char ch = in.get();//调用get
while (ch != end)
{
arr[i++] = ch;
if (i == N - 1)
{
arr[i] = '\0';
s += arr;//字符串很长的情况下,调用+=可以按需扩容
i = 0;//i重新开始保存数据
}
ch = in.get();
}
if (i > 0)//arr里面还有字符数据
{
arr[i] = '\0';//末尾置为'\0'再进行插入
s += arr;
}
return in;
}
是不是十分巧妙~string类的底层实现到此就结束啦~这篇博客只进行了部分接口的实现,小伙伴们有兴趣也可以自己实现更多的接口~