前言:
在之前的学习中,我们已经对string类进行了简单的介绍,大家只要能够正常使用即可。但是在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。因此,接下来我将带领大家手动模拟实现一下。
目录
(一)成员函数
1、构造函数
2、拷贝构造
3、赋值重载
4、析构函数
(二)容量
1、size()
2、capacity()
3、reserve()
4、resize()
5、clear()
(三)元素访问
1、 operator[]
(四)修改
1、 operator+=
2、append()
3、push_back()
4、insert()
5、erase()
6、swap()
(五)字符串操作
1、c_str()
2、find()
(六)非成员函数重载
1、relational operators()
2、operator<<
3、operator>>
(七)代码汇总
(八)总结
(一)成员函数
1、构造函数
刚开始时,如果我们要实现构造函数,可能就需要分别实现带参的构造函数和无参的构造函数,但是有没有简单方法可以做到一步到位呢?
💨 因此,为了更加的灵活方便,我们直接把带参的构造函数和无参构造函数集合,形成全缺省的构造函数,这样就省得再去写两个构造函数。
代码如下:
//全缺省的构造函数
//string(const char* str = nullptr) //不可以,对其解引用如果遇到空指针就报错
//string(const char* str = '\0') //类型不匹配,char 不能匹配为指针
//string(const char* str = "\0") //可以
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 5 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
2、拷贝构造
编译器默认的实现的是浅拷贝,但是浅拷贝存在问题:
- 如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
因此为了解决上述的问题,可以采用深拷贝解决浅拷贝问题:
- 每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
代码如下:
//深拷贝
// str3(str2)
string(const string& STR)
:_size(STR._size)
, _capacity(STR._capacity)
{
_str = new char[STR._capacity + 1];
strcpy(_str, STR._str);
}
3、赋值重载
注意:
- 当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;
- 当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。
代码如下:
string& operator=(const string& STR)
{
if (this != &STR)
{
char* tmp = new char[STR._capacity + 1];
strcpy(tmp, STR._str);
delete[] _str;
_str = tmp;
_size = STR._size;
_capacity = STR._capacity;
}
return *this;
}
4、析构函数
析构函数的实现就比较简单,只需将指针所指的空间进行释放并把置空即可(防止野指针) ,最后把剩余的两个成员置为0即可。
代码如下:
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
(二)容量
1、size()
顾名思义返回字符串的长度(以字符数为单位)
代码如下:
size_t size() const
{
return _size;
}
2、capacity()
返回当前为basic_string分配的存储空间的大小,以字符表示。
代码如下:
size_t capacity() const
{
return _capacity;
}
3、reserve()
表示请求更改容量,使字符串容量适应计划的大小更改为最多 n 个字符。
注意是有效字符,不包含标识字符,而在具体实现的时候,我们在底层多开一个空间给\0。
代码如下:
//扩容操作
void reserve(size_t N)
{
if (N > _capacity)
{
char* tmp = new char[N + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = N;
}
}
4、resize()
其实它的情况大体上可以分为插入数据和删除数据两种情况。
- 1.对于插入数据来说直接调用【reserve】提前预留好空间,然后搞一个for循环将字符ch尾插到数组里面去,最后再在数组末尾插入一个\0标识字符;
- 2.对于删除数据就比较简单了,如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符以外的字符,然后重置一下_size的大小为n即可。
代码如下:
//扩容+初始化
void resize(size_t n, char STR = '\0')
{
if (n < _size)
{
// 删除数据--保留前n个
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
size_t end = _size;
while (end < n)
{
_str[end] = STR;
end++;
}
_size = n;
_str[_size] = '\0';
}
}
5、clear()
顾名思义就是清除字符串,擦除basic_string的内容,该内容变为空字符串(长度为 0 个字符)。
代码如下:
void clear()
{
_str[0] = '\0';
_size = 0;
}
(三)元素访问
1、 operator[]
元素访问操作相对来说用的最多的就是operator[] ;
- 对它进行调用时可能进行的是写操作,也可能进行读操作,所以为了适应const和非const对象,operator[]应该实现两个版本的函数;
- 并且这个函数处理越界访问的态度就是assert直接断言,而at对于越界访问的态度是抛异常。
代码如下:
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
文章来源地址https://www.toymoban.com/news/detail-448111.html
(四)修改
1、 operator+=
追加到字符串,通过在当前值的末尾附加其他字符来扩展
在这里我们只实现添加字符和字符串的操作;
我们可以直接复用【push_back】的操作来实现。
代码如下:
//+=
string& operator+=(char STR_1)
{
push_back(STR_1);
return *this;
}
string& operator+=(const char* STR_2)
{
append(STR_2);
return *this;
}
2、append()
追加到字符串, 通过在当前值的末尾附加其他字符来扩展。
- 我们可以直接调用strcpy接口来进行字符串的尾插,但是需要注意一点,那就是【string】类的字符串函数是不会进行自动扩容的,所以我们需要判断一下是否需要进行扩容,在空间预留好的情况下进行字符串的尾插即可实现;
- 其次,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。
代码如下:
//追加字符串
void append(const char* STR)
{
size_t len = strlen(STR);
if (len + _size > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, STR);
_size += len;
//insert(_size, STR);
}
3、push_back()
注意:
- 首先对于【push_back】有一个特别需要注意的地方就是当容量不够时的扩容操作。如果是一个空对象进行push_back的话,这时如果我们采取的二倍扩容就有问题,因为0*2还是0,所以对于空对象的情况我们应该给他一个初始的capacity值,所以上述构造函数的时候我给成了【5】,其他情况下进行二倍扩容即可;
- 其次,就是在尾插字符之后,要记得进行补【\0】操作,,否则在打印的时候就会有麻烦了。
- 最后跟【append】一样,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。
代码如下:
//尾插操作
void push_back(char STR)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = STR;
++_size;
_str[_size] = '\0';
//insert(_size, STR);
}
4、insert()
插入到字符串中,在 pos(或 p)指示的字符之前将其他字符插入
注意:
- 对于【insert】函数,有经常会引出错误的地方,那就是对于while循环里面的操作;
可能很多的小伙伴在while循环里面都是这样写的:_str[end + 1] = _str[end] ,那么这样写有没有问题呢?答案是会出问题的;
- 我们的end是size_t定义的,因为size_t是无符号数,那么-1会被认为是无符号整数,进行隐式类型转换,由于-1的补码是全1,此时就是恒大于0,程序会陷入死循环。所以我们可以不用size_t来定义end,防止发生隐式类型转换;
- 那么是不是只要把【size_t end = _size + len;】中的【end】用 int 定义就可以解决了呢?答案当然不是的 (是不是觉得很坑了呀!!!);
- 因为-1在和size_t定义的pos进行比较时,又会发生隐式类型转换。这是因为比较运算符也是运算符,只要进行运算就有可能出现隐式类型转换,因此此时又可能出现上述那样的情况,-1就又会被转为无符号整型,程序就又陷入死循环;
- 那么有没有解决方法呢?当然是有的,我们只需在比较时将【size_t】的pos强转为【int】类型,此时再去比较就没得问题了;
- 但当我们就想使用size_t类型,通过把【end-1】位置的元素挪到【end】位置上去,在while循环条件的判断位置,我们用end来和pos位置进行比较,end应该大于pos的位置,一旦end=pos我们就跳出循环,这样就可以了。
代码如下:
//插入字符操作
string& insert(size_t pos, char STR_1)
{
assert(pos < _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = STR_1;
++_size;
return *this;
}
//插入字符串
string& insert(size_t pos, const char* STR_2)
{
assert(pos < _size);
size_t len = strlen(STR_2);
if (_size + len > _capacity){
reserve(_size + len);
}
// 挪动数据
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 拷贝插入
strncpy(_str + pos, STR_2, len);
_size += len;
return *this;
}
5、erase()
意思很简单,就是从字符串中删除字符
对于删除,思路很简单,分为两种情况下的删除:
- 1.如果当前位置加上要删除的长度大于字符串的长度,即【 pos + len >= _size】,此时的意思即为删除pos之后的所有元素;
- 2.除了上述情况,就是在字符串内正常删除操作。我们只需利用strcpy来进行,将pos+len之后的字符串直接覆盖到pos位置,这样实际上就完成了删除的工作。
注意:
- 对于【npos】这个参数,首先我们知道对于静态成员变量,它的规则是在类外定义,类里面声明,定义时不加static关键字;
- 但如果静态成员变量有const修饰,这时它可以在类内直接进行定义,这样的特性只针对于整型,对于其他类型则是不适用的;
- npos就是const static修饰的成员变量,可以直接在类内进行定义。
代码如下:
//删除操作
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
6、swap()
至于交换,这个就没有必要再多说什么了很简单,我相信大家肯定也会这个。
代码如下:
//交换
void swap(string& STR)
{
std::swap(_str, STR._str);
std::swap(_capacity, STR._capacity);
std::swap(_size, STR._size);
}
(五)字符串操作
1、c_str()
获取等效的 C 字符串,返回指向一个数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示basic_string对象的当前值
const char* c_str()
{
return _str;
}
2、find()
查找字符串中的第一个匹配项, 在basic_string中搜索由其参数指定的序列的第一个匹配项。
代码展示:
//查找
size_t find(char STR, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == STR)
{
return i;
}
}
return npos;
}
size_t find(const char* STR, size_t pos = 0)
{
assert(pos < _size);
char* p = strstr(_str + pos, STR);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
(六)非成员函数重载
1、relational operators()
basic_string的关系运算符,以ascll码的方式比较大小
这个实现的过程,跟之前日期类的时间如出一辙,基本上都是一样的。
代码如下:
//比较大小
bool operator >(const string& STR) const
{
return strcmp(_str, STR._str) > 0;
}
bool operator == (const string & STR)const
{
return strcmp(_str, STR._str) == 0;
}
bool operator >= (const string & STR)const
{
return *this > STR || *this == STR;
}
bool operator < (const string & STR)const
{
return !(*this >= STR);
}
bool operator <= (const string& STR)const
{
return !(*this > STR);
}
bool operator!=(const string& STR) const
{
return !(*this == STR);
}
2、operator<<
将字符串插入流, 将符合 str 值的字符序列插入到 os 中。
代码如下:
//operator<<
ostream& operator<<(ostream& out, const string& STR)
{
for (auto e : STR)
{
out << e;
}
return out;
}
3、operator>>
从流中提取字符串, 从输入流中提取字符串,将序列存储在 str 中,该序列被覆盖(替换 str 的先前值)
注意:
- 流提取是以空格和\n作为间隔标志的 ,而【getline】则是以【\0】就停止。
代码如下:
//operator>>
istream& operator>>(istream& in, string& STR)
{
STR.clear();
char ch = in.get();
//如果输入到缓冲区里的字符串非常非常的长,那么+=就需要频繁的扩容,则效率就会降低
//因此,在这里可以使用开辟一个数组,先将有效数据放入数组中,在进行操作,可有效提高效率
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\0')
{
buff[i++] = ch;
if (i == 127) //最后得留一个位置给\0
{
buff[127] = '\0';
STR += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
STR += buff;
}
return in;
}
(七)代码汇总
代码汇总如下:
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;
}
//全缺省的构造函数
//string(const char* str = nullptr) //不可以,对其解引用如果遇到空指针就报错
//string(const char* str = '\0') //类型不匹配,char 不能匹配为指针
//string(const char* str = "\0") //可以
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 5 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//深拷贝
// str3(str2)
string(const string& STR)
:_size(STR._size)
, _capacity(STR._capacity)
{
_str = new char[STR._capacity + 1];
strcpy(_str, STR._str);
}
//赋值操作
string& operator=(const string& STR)
{
if (this != &STR)
{
// str1 = str1 的情况不满足
/*delete[] _str;
_str = new char[s._capaicty + 1];
strcpy(_str, s._str);
_size = s._size;
_capaicty = s._capaicty;*/
char* tmp = new char[STR._capacity + 1];
strcpy(tmp, STR._str);
delete[] _str;
_str = tmp;
_size = STR._size;
_capacity = STR._capacity;
}
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str()
{
return _str;
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
//比较大小
bool operator >(const string& STR) const
{
return strcmp(_str, STR._str) > 0;
}
bool operator == (const string & STR)const
{
return strcmp(_str, STR._str) == 0;
}
bool operator >= (const string & STR)const
{
return *this > STR || *this == STR;
}
bool operator < (const string & STR)const
{
return !(*this >= STR);
}
bool operator <= (const string& STR)const
{
return !(*this > STR);
}
bool operator!=(const string& STR) const
{
return !(*this == STR);
}
//扩容+初始化
void resize(size_t n, char STR = '\0')
{
if (n < _size)
{
// 删除数据--保留前n个
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
size_t end = _size;
while (end < n)
{
_str[end] = STR;
end++;
}
_size = n;
_str[_size] = '\0';
}
}
//扩容操作
void reserve(size_t N)
{
if (N > _capacity)
{
char* tmp = new char[N + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = N;
}
}
//尾插操作
void push_back(char STR)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = STR;
++_size;
_str[_size] = '\0';
//insert(_size, STR);
}
//追加字符串
void append(const char* STR)
{
size_t len = strlen(STR);
if (len + _size > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, STR);
_size += len;
//insert(_size, STR);
}
//+=
string& operator+=(char STR_1)
{
push_back(STR_1);
return *this;
}
string& operator+=(const char* STR_2)
{
append(STR_2);
return *this;
}
//插入字符操作
string& insert(size_t pos, char STR_1)
{
assert(pos < _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = STR_1;
++_size;
return *this;
}
//插入字符串
string& insert(size_t pos, const char* STR_2)
{
assert(pos < _size);
size_t len = strlen(STR_2);
if (_size + len > _capacity){
reserve(_size + len);
}
// 挪动数据
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 拷贝插入
strncpy(_str + pos, STR_2, len);
_size += len;
return *this;
}
//删除操作
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
//交换
void swap(string& STR)
{
std::swap(_str, STR._str);
std::swap(_capacity, STR._capacity);
std::swap(_size, STR._size);
}
//查找
size_t find(char STR, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == STR)
{
return i;
}
}
return npos;
}
size_t find(const char* STR, size_t pos = 0)
{
assert(pos < _size);
char* p = strstr(_str + pos, STR);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
//operator<<
ostream& operator<<(ostream& out, const string& STR)
{
for (auto e : STR)
{
out << e;
}
return out;
}
//operator>>
istream& operator>>(istream& in, string& STR)
{
STR.clear();
char ch = in.get();
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\0')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
STR += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
STR += buff;
}
return in;
}
(八)总结
到此,关于string的模拟实现,在这里我们主要实现的是经常用得到的,对于其他的,我们并没有一一列举。如果后面有机会再给大家展示。
接下来,我们简单总结一下本文:
- 我们从文档的先后顺序入手,依次对各个板块的常用接口进行了模拟实现;
- 大家在上手操作的时候,一定要想明白为什么,做到真正的掌握string类它是非常重要的。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
到此,便于string类的模拟实现便讲解完毕了。希望本文对大家有所帮助,感谢各位的观看!!!
文章来源:https://www.toymoban.com/news/detail-448111.html
到了这里,关于【C++】——string的模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!