c++_string模拟实现
目录
c_str:
函数定义
1. 函数的返回类型
2. 函数的作用
3. 为什么返回 const char*
构造:string(const char* str = ' ')
析构:~string()
string 遍历
字符串的大小:size_t size() const
容量大小:size_t capacity() const
char& operator[](size_t pos)是提供对字符串中特定位置字符的访问,并且允许对这个字符进行读写操作。
迭代器iterator _ 范围for
测试函数:
增删查改
拼接:string& operator+=(char ch)
插入字符 insert
插入字符串 insert
删除字符串
在库中
std::string 中的 erase()
删除单个字符
删除范围内的字符
删除从指定位置开始的多个字符
模拟实现:
缩减字符串的长度:resize
模拟实现
拷贝构造函数(传统写法):string(const string& s)
赋值:string& operator=(const string& s)
交换:swap()
找寻字符和字符串位置:find()
从字符串中提取子字符串:substr
参数说明:
返回值:
判断大小:operator== ,>,>=,<,<=,!=
流插入流提取:<< ,>>
在进入函数时,添加一个清除函数:clear()。
提取行getline()
照如图所示的思路:
拷贝构造函数更新(新写法、现代写法):
赋值运算符的现代写法
主要实现string类的构造、拷贝构造、赋值运算符重载以及析构函数
再开始之前我们需要提供对底层字符数组的直接访问的函数也就是:
c_str:
想要对底层字符数组的直接访问。需要提供一个c_str()函数:
以下是对 c_str()
函数的详细解释:
函数定义
cpp复制
const char* c_str() { return _str; }
1. 函数的返回类型
const char*
:返回一个指向const
字符的指针。这意味着返回的指针指向的字符串内容是只读的,不能通过这个指针修改字符串的内容。2. 函数的作用
返回底层字符数组的指针:
_str
是一个动态分配的字符数组,存储了字符串的内容,包括终止符\0
。
c_str()
返回_str
的指针,允许用户直接访问底层的字符数组。用途:
提供对字符串的直接访问,方便与其他 C 风格的函数或 API 交互。
例如,许多 C 标准库函数(如
printf
、strlen
等)需要一个const char*
类型的参数,c_str()
可以提供这种类型的指针。3. 为什么返回
const char*
保证只读访问:
返回
const char*
指针,确保用户不能通过这个指针修改字符串的内容。这与std::string
的行为一致,保证了字符串的不可变性。如果返回
char*
,用户可能会修改字符串的内容,这可能会导致不可预测的行为,尤其是在字符串对象的内部逻辑中(如动态内存管理)。兼容性:
返回
const char*
指针,使得bit::string
类与 C 标准库函数和其他需要只读字符串的 API 兼容。
构造:string(const char* str = ' ')
请阅读并观察下面的代码:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
namespace bit
{
class string
{
public:
//这里是有问题的
/* string()
:_str(nullptr)
, _size(0)
, _capacity(0)
{
}*/
//代参的构造函数初始化
//strlen是运行时去遍历字符串
// ,遇到'\0'就结束不宜多次调用,这里一共调用了三次
//string(const char* str)
// :_str(new char[strlen(str) + 1])
// ,_size(strlen(str))
// ,_capacity(strlen(str))//capacity不包含'\0'
//{
// //要满足能够修改
// strcpy(_str, str);
//}
//注意初始化列表走的顺序,是按照声明的顺序,所以不能这样
//string(const char* str)
// :_size(strlen(str))
// ,_str(new char[strlen(str) + 1])
// ,_capacity(strlen(str))//capacity不包含'\0'
//{
// //要满足能够修改
// strcpy(_str, str);
//}
//正确写法:
/*string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}*/
//最后:将不带参的构造函数和代参的构造函数合二为一
string(const char* str = nullptr)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
void test_string1()
{
string s1("hello world");
string s2;
}
}
以上代码还存在着错误:
因为空的string在这里存在着问题,会导致程序出现崩溃:
string(const char* str = nullptr)
:_size(strlen(str)) //在这里崩掉
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);}
string(const char* str = ‘\0’) //这里也存在着问题,由于语法会报错:这是一个字符自变量char,而我们定义的是char*
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);}
string(const char* str = "\0") //等于一个字符串正确了,但是相当于有两个\0
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);}
正确写法:
string(const char* str = "") //或者直接空串(包含\0)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
非要分开写时 :_str(nullptr) 这样的写法存在潜在的问题:
string()
:_str(nullptr)
, _size(0)
, _capacity(0)
{
}string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);}
const char* c_str()
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};void test_string1()
{
string s1("hello world");
string s2;
cout << s1.c_str() << endl;cout << s2.c_str() << endl;//这句话就会报错
}
因为在c_str()中返回的是一个空指针
需要将这段代码
string()
:_str(nullptr)
, _size(0)
, _capacity(0)
{
}
修改成:
string()
:_str(new char[1])
, _size(0)
, _capacity(0)
{
_str[1] = '\0';
}
析构:~string()
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = _size = 0;
}
string 遍历
字符串的大小:size_t size() const
size_t size() const//普通对象可以调用,const对象也可以调用
{
return _size;
}
容量大小:size_t capacity() const
size_t capacity() const
{
return _capacity;
}
char& operator[](size_t pos)是提供对字符串中特定位置字符的访问,并且允许对这个字符进行读写操作。
引用的好处:
可读可写:通过返回引用,可以直接修改
_str[pos]
的值。例如:cpp复制
s[0] = 'H'; // 修改字符串的第一个字符
高效:返回引用避免了不必要的拷贝,直接操作原始数据。
/*ReturnType& operator[](int index);
ReturnType:返回类型,通常是类中元素的引用,以便支持修改操作。
index:下标参数,表示要访问的元素位置。*/
char& operator[](size_t pos)
{
assert(pos < _size);/*检查是否越界!!!很有用!!!,一般越界读检查不出来,越界写是抽查*/
//为什么可以返回呢?
//因为用的引用,返回的是别名
return _str[pos];
/*_str:成员变量,_str[]:解引用所指向的空间在堆上,
出了作用域还在,返回的是pos位置上的字符,
从而用引用返回,而不是拷贝,而是别名,因此可读可写*/
通过所讲的构造、遍历、析构函数运行此函数:
void test_string1()
{
//可写
for (size_t i = 0; i < s1.size(); i++)
{
s1[i]++;
}
cout << endl;
//可读
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
}
通过这个测试实例,我们了解到operator[]()还需要完善,不能直接同size()函数一样在后面加const
const string s3("xxx");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s3[i] << " "; //报错原因,const对象不能调用非const成员函数
}
cout << endl;
通过阅读学习文档:则我们写一个常量访问的重载就可
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}//const对象是只读的
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
运行结果:
迭代器iterator _ 范围for
typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; }
string中大致类似图示:(注意不是所有容器的迭代器的实现都是指针,比如说链表的迭代器)
vs下的string迭代器(第一排代码),第二排是我的代码作用域中所使用的:
运行结果:
当前情况下的迭代器实现类似:
为了满足常量访问:
typedef const char* const_iterator; const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }
测试函数:
void test_string2()
{
string s3("hello world");
//迭代器
string::iterator it3 = s3.begin();
while (it3 != s3.end())
{
//*it3 -= 3;
cout << *it3 << " ";
++it3;
}
cout << endl;//自动取值迭代,编译底层类似于迭代器的代码,将*s3赋值给ch
for (auto ch : s3)
{
cout << ch << " ";
}
cout << endl;//const迭代器
string s4("ncjaskcja");
string::const_iterator it4 = s4.begin();
while (it4 != s4.end())
{
cout << *it4 << " ";
++it4;
}cout << endl;
for (auto ch : s4)
{
cout << ch << " ";
}
cout << endl;}
运行结果:
增删查改
void test_string3()
{
string s3("hello");
s3.push_back('1');
cout << s3.c_str() << endl;
s3.append("hello");
cout << s3.c_str() << endl;
}
实现这两个:
需要为append和push_back,提供一个reserve函数扩容
reserve函数:只扩容,不缩容,扩容的量比当前的capacity大的时候才会扩容
void push_back(char ch)
{
//扩容n倍
if (_size == _capacity)
{
//reserve(2 * _capacity);如果_capacity == 0?
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;//
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
//扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
对代码的解释:
append 要多少扩容多少,push_back扩容两倍
strcpy(_str + _size, str);
_str
是自定义字符串类bit::string
的内部存储空间,用于存储字符串数据。
_size
是当前字符串的长度(不包括终止符\0
)。
_str + _size
表示_str
中当前字符串的末尾位置(即第一个空闲位置)。
str
是要追加的字符串。
reserve函数:c++是没有malloc realloc的,扩容很复杂涉及到深浅拷贝等问题
void reserve(size_t n)//扩容n:库里的功能:当前空间大就不扩,小就扩
{
if (n > _capacity)//为什么要写,reserve单独使用的情况
{
char* tmp = new char[n+1];//+1为'\0'预留空间
strcpy(tmp, _str);//将值拷贝到新空间
delete[] _str;//释放旧空间
_str = tmp;//让指针指向新空间
_capacity = n;
}
}
拼接:string& operator+=(char ch)
string& operator+=(char ch) //拼接字符 { push_back(ch); return *this; } string& operator+=(const char* str) //拼接字符串 { append(str); return *this; }
测试:
string s3("hello"); s3 += 'x'; cout << s3.c_str() << endl; s3 += "wuwuwuuw"; cout << s3.c_str() << endl;
测试结果:
插入字符 insert
思路:
1.对插入的位置pos进行检查,小于等于字符串的大小_size
等于的时候是尾插:
assert(pos <= _size);
2.扩容:同push_back
3.赋值:
void insert(size_t pos, char ch)
{
//检查pos
assert(pos <= _size);
//扩容n倍
if (_size == _capacity)
{
//reserve(2 * _capacity);如果_capacity == 0?
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}size_t end = _size;//size_t等一下更改为int
while (end >= pos)//当end>=pos就可以将pos后的字符继续向后移动
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
问题:当pos = 0时,运行到--end时,end = -1 , 但 size_t 不存在 < 0,所以我们更改end的类型为int
而此时却再次进入循环:
赋值后:
陷入死循环:
为什么?
一个运算符如果当两边的的操作数类型不同时,会发生类型提升,范围小的类型会像范围大的提升,所以int 还是被提升为size_t,无符号数,最后发生越界。
方法:
1.强转
在库里:
(因为下标不可能是一个负数,所以用size_t)
图解:
插入字符串 insert
void insert(size_t pos,const char* str)
{
//检查pos
assert(pos <= _size);
//扩容
size_t len = strlen(str); //记录需要插入的字符串的长度
cout << len << endl;
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;//由插入后整个字符串的长度,得到end的位置
//
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
}//作业
删除字符串
在库中
std::string
中的erase()
std::string
也提供了erase
函数,用法与顺序容器类似:删除单个字符
iterator erase(iterator position);
参数:
position
是一个迭代器,指向要删除的字符。返回值:返回一个指向被删除字符之后字符的迭代器。
示例:
std::string str = "Hello"; auto it = str.begin() + 1; // 指向字符 'e' str.erase(it); // 删除字符 'e' // str 现在为 "Hllo"
删除范围内的字符
iterator erase(iterator first, iterator last);
参数:
first
和last
是迭代器,表示要删除的字符范围[first, last)
。返回值:返回一个指向被删除范围之后字符的迭代器。
示例:
std::string str = "Hello"; auto it1 = str.begin() + 1; // 指向字符 'e' auto it2 = str.begin() + 4; // 指向字符 'o' str.erase(it1, it2); // 删除字符 'e', 'l', 'l' // str 现在为 "Ho"
删除从指定位置开始的多个字符
basic_string& erase(size_type pos = 0, size_type count = npos);
参数:
pos
:要删除的起始位置。
count
:要删除的字符数。如果count
为npos
(默认值),则删除从pos
开始到字符串末尾的所有字符。返回值:返回修改后的字符串。
示例:
std::string str = "Hello, World!"; str.erase(7, 5); // 从位置 7 开始删除 5 个字符 // str 现在为 "Hello, !"
模拟实现:
void erase(size_t pos, size_t len = npos)
//从pos位置开始删,删除len个 ,npos默认值 = -1
{
assert(pos < _size);//_str[_size]的位置是'\0'
//1.当len = npos 或者pos + len 已经大于整个字符串大小的时候,就将后面的都删除
// if (len == npos ||pos + len >= _size)
//但这里存在问题 pos + len有可能溢出,npos = -1,实际上是整形最大值,所以这种方法还需要需改
if (len == npos ||len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
//2.在中间删除
//思路,用后边的字符来覆盖要删除的部分,再加'\0'
else
{
strcpy(_str + pos, _str + pos + len);/*用strcpy的好处在于,当拷贝到'\0'时,自动结束
也可以用strncpy,但是需要计算出拷贝的数量*/
_size -= len;
}}
缩减字符串的长度:resize
库:
模拟实现
void resize(size_t n,char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)//扩容,那么就从原始长度的下一个开始进行插入'\0'就是_size,到新长度n结束
{
_str[i] = ch;
}
_str[n] = '\0'; //记得结尾标志
_size = n;
}
}
拷贝构造函数(传统写法):string(const string& s)
在执行下面的代码中,我们的程序会出现报错。
是因为,这里使用的是程序自动生成的拷贝构造函数,只进行对s1的浅拷贝(值拷贝),在前面的文章有细致讲解过类似的简单浅拷贝深拷贝。而析构时析构两次统一空间,导致的程序报错。
所以我们需要做一个深拷贝:
string(const string& s)
{
//给需要拷贝值的对象开辟和被拷贝对象相同的空间
_str = new char[s._capacity + 1]; //注意又给'\0'预留了空间
//拷贝数据
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
监视观察:
s1和s2各自有各自的空间。
赋值:string& operator=(const string& s)
string& operator=(const string& s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
交换:swap()
void test_string6()
{
string s1("hello world");
cout << s1.c_str() << endl;
s1.insert(5, "123");
cout << s1.c_str() << endl;
string s2("xxxxxxx");
cout << s2.c_str() << endl;
swap(s1, s2);
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
}
这里的拷贝是深拷贝,这里一共就是三次拷贝加一次析构;
因此写一个swap成员函数:
void swap(string& s)
{
std::swap(_str, s._str); //注意不要直接写swap(),会在局部找,得指定区域
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
使用时:
那么有什么办法能避免直接使用swap:
我们再写一个这样的成员函数swap,通过这样,在有人直接使用swap(x,y)时,实际上调用的是我们自己写的更加高效的swap。
找寻字符和字符串位置:find()
怎么得到下标,得到两个位置的指针再相减就是下标。
size_t find(const char* sub, size_t pos = 0) const //找寻字符串位置
{
assert(pos < _size);
//思路:做子串的匹配
//1.strstr()暴力匹配
const char* p = strstr(_str, sub);
if (p)
{
return p - _str;
}
else
{
return npos;
}
//2.kmp算法(比特大博哥
}
从字符串中提取子字符串:substr
在C++中,
std::string
类提供了一个成员函数substr
,用于从字符串中提取子字符串。substr
函数的基本语法如下:std::string substr(size_t pos = 0, size_t len = npos) const;
参数说明:
pos:
表示子字符串的起始位置(从0开始计数)。
默认值为
0
,即从字符串的开头开始。len:
表示要提取的子字符串的长度。
默认值为
std::string::npos
,这是一个特殊值,表示从pos
开始到字符串的末尾。返回值:
返回一个新的
std::string
对象,包含从pos
开始的len
个字符。
模拟实现:
string substr(size_t pos = 0, size_t len = npos)
{
string sub;
if (len == npos || len >= _size - pos) //len == npos也是在len >= _size - pos范围中的
{
for (size_t i = pos; i < _size; i++)
{
sub += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
判断大小:operator== ,>,>=,<,<=,!=
这里我们需要用到strcmp,需要知道返回值:在这里,如果相等则返回 0
bool operator==(const string& s) { int ret = strcmp(_str, s._str); return ret == 0; }
当我们这样写之后:
第三个支持的原因是因为,单参数的构造函数支持隐式类型的转换。
这里的const char*("hello world") 可以转换为 string,因为在上面我们有写一个单参数的构造函数
支持用一个const char* 来构造一个string
而第二种就不行:
出现这样的错误提示:
因为我们的bool operator==(const string& s) 是一个成员函数,成员函数的左边必须是对象,对象才能调用成员函数。
所以在operator==这里不能写成成员函数,得重载成一个全局的
这里出现的原因是前面写模拟c_str()函数时,没有加const:现在加上
运行结果:
由此我们直接复用就可以得出接下来的大小比较的代码:
bool operator==(const string& s1,const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret == 0;
}
bool operator<(const string& s1, const string& s2)
{
int ret = strcmp(s1.c_str(), s2.c_str());
return ret < 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 !(s1 == s2);
}
流插入流提取:<< ,>>
流插入:
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
//之前类和对象模拟实现的时候用到友元是因为需要访问到私有成员,
// 但这里只需要得到下标,来打印字符,没必要用迭代器,没有直接访问到私有成员
return out;
}
流提取:
istream& operator<<(istream& in, string& s) //将提取的值放到string里
{
char ch;
//in << s[i];能直接这样吗 写个for循环,不可以,
// 因为在这里s的空间都没开好,无法写入数据
in >> ch;
while (ch != ' ' && ch != '\n') //结束标志,等于空格等于换行(getline、in.get(ch)可以解决这种问题)
{
s += ch;
in >> ch;
}return in;
}
我们现在测试:
测试时发现,程序陷入死循环:
调试时就会发现,明明代码中写了在有空格和换行时退出循环,但是这里却直接从o 跳到 z,忽略了空格,我们又输入了hh,调试信息中显示,h h -->又到h,忽略了换行,这是为什么?
因为c++的 cin 和 c 的 scanf 读字符的时候根本取不到 、忽略空格和换行,默认是多个字符之间的分割。
所以我们的流提取代码还有问题。
在c语言中可以用getchar(),和getc()来解决,但是c++我们用流提取的时候不行。因为c语言的io流,和c++的io流是不一样的,各自有各自的缓冲区(类似于流提取自己会把他的数据给提取进来到c++的内存里,而c语言读取到c语言的内存中)。
内置类型能直接支持是因为,库里面把内置类型直接重载了,能自动识别识别类型,匹配类型
所以我们修改流提取代码:
istream& operator<<(istream& in, string& s) //将提取的值放到string里
{
char ch;
//in << s[i];能直接这样吗 写个for循环,不可以,
// 因为在这里s的空间都没开好,无法写入数据
//in >> ch;ch = in.get(); //是一个字符就直接取,不会忽略分隔符
while (ch != ' ' && ch != '\n') //结束标志,等于空格等于换行(getline、in.get(ch)可以解决这种问题)
{
s += ch;
ch = in.get();
}return in;
}
而此时还存在问题,问题在于我们的s1 、s2不是空string,这里的+=相当于尾插,而流提取是覆盖;所以我们需要先清除string对象。
在进入函数时,添加一个清除函数:clear()。
主要是将字符串的内容清除为0,即size = 0,capacity可以不变。
void clear() { _size = 0; _str[_size] = '\0'; }
修改后正确的流提取函数:
istream& operator>>(istream& in, string& s) //将提取的值放到string里 { s.clear(); char ch; //in << s[i];能直接这样吗 写个for循环,不可以, // 因为在这里s的空间都没开好,无法写入数据 //in >> ch; ch = in.get(); //是一个字符就直接取,不会忽略分隔符 while (ch != ' ' && ch != '\n') //结束标志,不等于空格不等于换行(getline、in.get(ch);可以解决这种问题) { s += ch; //in >> ch; ch = in.get(); } return in; }
提取行getline()
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
char buff[128];
size_t i = 0;
while (ch != ' ' &&ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
照如图所示的思路:
记得要初始化s2
拷贝构造函数更新(新写法、现代写法):
按
库里面不一定会处理,但是我们这里有处理,为了安全所以最好还是要写上
因此我们在声明变量的时候先添加缺省值
赋值运算符的现代写法
string& operator=(string ss)
{
swap(ss);
return *this;
}
计算大小,为什么?
答案是 :28
12
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。 你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。