目录
一,前言
为什么要学习string类
C语言中的字符串
C++中的字符串
STL(Standard Template Library) 里面的 string 类
二,string类的基本使用
文档的阅读
常见接口的基本使用
1,构造函数(constructor)
2,拷贝构造(copy constructor)
3,运算符 operator= 重载
4,析构函数 (destructor)
5,push_back 插入一个字符
6, append 插入一个字符串
7,operator+= 插入一个字符或字符串
8,operator[ ] 的重载 与 size()
operator[] size() 的应用, 将字符串转成整型
9,iterator迭代器的使用
遍历字符串数组
正向、反向迭代器,const、非const 迭代器
10,size(), capacity(), length()的使用
11,resize(), reserve() 的使用
12,insert(),erase() 的使用
13,c_str() 的使用
14,find() 的使用
15, operator+ 和 operator+=(一般使用operator+=)
16,关系运算符(relational operators)
17,getline的使用
三,string类的模拟实现
简单的string类的实现
构造函数的实现
string.h
test.cpp
深浅拷贝问题
析构函数的实现
拷贝构造的实现
运算符重载operator= 的实现
以上整体代码:
string.h
test.cpp
深拷贝的现代写法
实现具有增删改查的string类
1.构造函数
2.析构函数
3.拷贝构造
4.赋值运算符
5.迭代器 iterator的实现
6. push_bcak
7. append
8. reserve
9.重载运算符(operator+=)
10.insert
11. erase
12. find
13. resize
14. 运算符重载
15. 重载 operator<< 与 operator>>
整体代码
string.h
test.cpp
四,小结
一,前言
为什么要学习string类
C语言中的字符串
C语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合面向对象的思想,而且底层空间需要用户自己管理,稍不留神可 能还会越界访问。
C++中的字符串
C++兼容C语言的语法,所以字符串也是和C语言一样以 ' \0 ’作为结束标志,而我们的实际中处理字符串是非常常见的操作。C++就引入了string 类,string
类提供了方便、高效和安全的方式来操作字符串,满足各种字符串处理的需求。
STL(Standard Template Library) 里面的 string 类
模板(Template)是 C++ 中的一种编程机制,指泛型编程,而标准模板库(Standard Template Library,STL)是 C++ 标准库的一部分,其中大量使用了模板这种机制。 可以说 STL 是基于模板构建起来的一系列通用的、可复用的数据结构和算法的集合。
例如,STL 中的容器(如 vector、list 、map 等)和算法(如 sort、find 等)都是通过模板来实现的,从而可以处理不同类型的数据。
需要注意的是,string 类是 C++ 标准库(包括 STL)的一部分。它与 STL 中的其他组件一样,遵循相同的设计理念和规范,为编程提供了方便、高效和可复用的字符串处理功能。
通常所说的 STL 主要侧重于一些常见的数据结构和算法组件,如 vector、list 等,而较少单独提及 string 类
STL 常被强调的是用于数据存储和操作的数据结构(如 vector
用于动态数组,list
用于链表)以及相关的算法(如排序、查找等)。string
类主要专注于字符串处理,相对来说功能较为特定。
二,string类的基本使用
文档的阅读
这里主要演示最为常见的接口,其它接口需要靠自己阅读文档学习,C++STL的学习需要到官网阅读文档,以下是文档的网址:
https://cplusplus.com/
必须学会看文档,如果英语水平不是很好,可以下一个翻译软件,但是尽量不要使用翻译软件,自己不会的单词就去学。
以下是我使用的翻译软件网址:
https://fanyi.youdao.com/download-Windows
进入手册, 找到 string 类的接口
常见接口的基本使用
在 C++ 中使用 string
类之前,需要引入 <string>
头文件。
1,构造函数(constructor)
先看文档,序号1-7都是构造函数的重载,这些序号与下面的说明一一对应。
构造函数的应用
#include <iostream>
#include <string>
int main()
{
std::string s1(10, 'a'); //里面需要10个字符a
std::cout << s1 << std::endl;
std::string s2(3, 'b'); //里面需要10个字符b
std::cout << s2 << std::endl;
return 0;
}
以上就是根据文档使用构造函数完成初始化字符串数组,接下来我就不逐次看文档了,因为这些都是以前学习过的基础,只不过现在用起来了。
构造函数的其它重载函数的使用
default (1) | string(); |
---|---|
copy (2) | string (const string& str); |
#include <iostream>
#include <string>
int main()
{
std::string s1; //构造空字符串
std::string s2("hello"); //构造hello字符串
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
return 0;
}
pos是起始位置,len 缺省参数是 npos,不填就是一个很大的值,意思就是拷贝到最后。
substring (3) | string (const string& str, size_t pos, size_t len = npos); |
---|
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("12345");
string s1(s, 1, 2); //打印 23
string s2(s, 1, string::npos); //打印 2345 // npos是string类中的一个静态成员变量,size_t npos = -1;表示很大的一个值
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
2,拷贝构造(copy constructor)
文档中的第二点,拷贝构造也属于构造的一种,用于初始化对象。
拷贝构造的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello"); // 构造
string s2(s1); //拷贝构造
string s3 = s1; //s1先构造给临时对象tmp, tmp(s1), tmp拷贝构造, s3(tmp)
//直接优化成拷贝构造s3(s1);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
return 0;
}
3,运算符 operator= 重载
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello"); // 构造
string s2("dear"); //构造
s2 = s1; // 运算符=的重载
cout << s2 << endl;
return 0;
}
4,析构函数 (destructor)
出了作用域自动调用
5,push_back 插入一个字符
void push_back(char c);
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello");
s.push_back('A'); //插入一个字符
cout << s << endl; //打印结果 helloA
return 0;
}
6, append 插入一个字符串
c-string (3) | string& append (const char* s); |
---|
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello");
s.append(" world"); //插入一个字符串
cout << s << endl; //打印结果 hello world
return 0;
}
7,operator+= 插入一个字符或字符串
c-string (2) | string& operator+= (const char* s); |
---|---|
character (3) | string& operator+= (char c); |
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1;
s1 += '1'; //插入一个字符
s1 += "2222"; //插入一个字符串
cout << s1 << endl; // 打印结果 12222
string s2;
s2 += "hello";
s2 += ".";
s2 += "world";
cout << s2 << endl; // 打印结果 hello.world
return 0;
}
8,operator[ ] 的重载 与 size()
char& operator[] (size_t pos); 可以修改其返回值
const char& operator[] (size_t pos) const; 不可以修改其返回值
size_t size() const; 返回字符串的大小,不包含 \0
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("12345");
//遍历字符串
for (size_t i = 0; i < s.size(); ++i) //size(),计算字符串数组中的个数
{
cout << s[i] << " "; // s[i] 相对于 s.operator[](&s,i)这样调用函数; operator返回的是一个字符
}
return 0;
}
operator[] size() 的应用, 将字符串转成整型
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("12345");
int val = 0;
for (size_t i = 0; i < s.size(); ++i)
{
val *= 10;
val += s[i] - '0';
}
cout << val << endl;
return 0;
}
9,iterator迭代器的使用
遍历字符串数组
- 1.利用operator[] 和 size() 遍历
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello");
s1 += ' ';
s1 += "world";
cout << s1 << endl;
//除了可以读还可以写
for (size_t i = 0; i < s1.size(); ++i)
{
s1[i] += 1;
}
//读
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
return 0;
}
- 2. 迭代器遍历:迭代器 iterator类型 属于string类,迭代器具有很强的迁移性,vector、list 也是这样使用的
在string类中(字符串数组中),迭代器有点类似于指针遍历数组,但是迭代器不一定是指针, 而是一个像指针一样的东西
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
string::iterator it = s1.begin();
//写
while (it != s1.end())
{
*it -= 1;
++it;
}
//读
it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
- 3. 范围for原理: 会被替换成迭代器
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
正向、反向迭代器,const、非const 迭代器
目前来看operator[] 和 size 很方便,迭代器看都不想看,但是operator[] 和 size 只能用于string、vetctor,后面的list等等就可以感受到迭代器带来的便利
#include <iostream>
#include <string>
using namespace std;
int string2int(const string& str) // const 接收
{
int val = 0;
//const迭代器,只能读,不能写
string::const_iterator it = str.begin();
while (it != str.end())
{
//*it -= 1; 不能写
val *= 10;
val += (*it - '0');
++it;
}
//反向的const迭代器
val = 0;
string::const_reverse_iterator rit = str.rbegin();
while (rit != str.rend())
{
val *= 10;
val += (*rit - '0');
++rit;
}
return val;
}
//其它迭代器
int main()
{
string s1("hello world");
//1,反向迭代器,倒着遍历
string::reverse_iterator rit = s1.rbegin(); // 指向 d
while (rit != s1.rend()) // rend() 指向h 的前一个位置
{
cout << *rit << " ";
++rit;
}
cout << endl;
// 2,const迭代器
string nums("12345");
cout << string2int(nums) << endl;
return 0;
}
10,size(), capacity(), length()的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello world");
string s2("hello");
cout << s1.size() << endl; //求有多少个有效字符
cout << s2.size() << endl;
cout << s1.length() << endl;
cout << s2.length() << endl;//string比STL早一点点、字符串length长度,后面出现了树等等,就用size了有效个数的意思,现在一般使用size
cout << s1.max_size() << endl; // max_size返回字符串可以达到的最大长度 没什么用了解一下
cout << s2.max_size() << endl;
cout << s1.capacity() << endl; // 求容量
cout << s2.capacity() << endl;
s1 += "1111111";
cout << s1.capacity() << endl; //增容到的目前容量
s1.clear(); //只是把size清空了,也就是把有效字符清空了
cout << s1 << endl;
cout << s1.capacity() << endl; //容量没有变化
}
11,resize(), reserve() 的使用
resize 除了增容还会改变size,因为有插入字符
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s("hello world");
s.resize(5);//默认不给填\0,给了什么就填什么
cout << s << endl;
s.resize(20, 'x'); //除了会扩容还会填充 x ,如果不给值默认补\0
cout << s << endl;
}
- reserve就是开空间,如果给100不是空间开100而是1.5倍/2倍的开(容量大于100)
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
s.reserve(100);
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capcaity changed:" << sz << endl;
}
}
cout << s.capacity(); //capacity计算的是存储有效字符的空间容量,不包括\0, 打印111,本质是112个最后一个给 \0
}
12,insert(),erase() 的使用
insert的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
s += '1';
s += "3456";
cout << s << endl;
s.insert(s.begin(), '0'); // iterator insert(iterator p, char c);
cout << s << endl;
s.insert(2, "2"); //string& insert (size_t pos, const char* s); 字符'2'不能插入,只能插入字符串"2"
cout << s << endl;
return 0;
}
erase的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
s += "123456";
// 删除字符
s.erase(2, 10); //如果才读比字符串里面的长,直接删到结束的位置
cout << s << endl; //打印 12
return 0;
}
13,c_str() 的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello");
//获取字符数组的首地址,用C字符串的形式遍历
const char* str = s1.c_str();
while (*str)
{
cout << *str << " ";
++str;
}
cout << endl;
cout << s1 << endl; //调用的string重载的operator<< 输出
cout << s1.c_str() << endl;//直接输出 const char*
return 0;
}
c_str遇到 \0就会停止,而对象会把字符数组中的所有字符输出
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello");
s1 += '\0';
s1 += "world";
cout << s1 << endl; //调用的string重载的operator<< 输出,将对象数组中的所有字符输出
cout << s1.c_str() << endl;//直接输出 const char*,遇到\0就结束
return 0;
}
14,find() 的使用
character (4) | size_t find (char c, size_t pos = 0) const; |
---|
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("string.cpp");
string s2("string.c");
string s3("string.txt");
size_t pos1 = s1.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
if (pos1 != string::npos)
{
cout << s1.substr(pos1) << endl; //substr 函数用于从一个字符串中提取子字符串。
//它的作用是根据指定的起始位置和长度,返回原字符串的一部分。
}
size_t pos2 = s2.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
if (pos2 != string::npos)
{
cout << s2.substr(pos1) << endl;
}
size_t pos3 = s3.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
if (pos3 != string::npos)
{
cout << s3.substr(pos1) << endl;
}
return 0;
}
但是呢还有一个问题、string.cpp.zip 取最后一个后缀,如何取到呢?
rfind,从后往前找,和 find一样使用。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("string.cpp.zip");
size_t pos = s1.rfind('.');
if (pos != string::npos)
{
cout << s1.substr(pos) << endl; //打印.zip
}
return 0;
}
find 的使用场景:网址的分离,这种情况下我们可以把以下的代码写成分离函数,直接调用
#include <iostream>
#include <string>
using namespace std;
int main()
{
//协议 域名 资源名称
string url("http://www.cp.uplus.com/reference/string/find/");
//string url2("https://v.baidu.vip.logn");
//分理出url的协议、域名、资源名称
size_t i1 = url.find(':'); //返回 5
if (i1 != string::npos)
{
cout << url.substr(0, i1) << endl;
}
size_t i2 = url.find('/', i1 + 3);
if (i2 != string::npos)
{
cout << url.substr(i1 + 3, i2 - (i1 + 3)) << endl;
}
cout << url.substr(i2 + 1) << endl;
return 0;
}
15, operator+ 和 operator+=(一般使用operator+=)
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("hello");
string ret1 = s1 + "world"; // s1不改变
string ret2 = s1 += "world"; // s1改变
cout << ret1 << endl;
cout << ret2 << endl;
return 0;
}
16,关系运算符(relational operators)
string是可以直接比较大小的
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("abcd");
string s2("bbcd");
cout << (s1 < s2) << endl; // 1
cout << (s1 < "bbcd") << endl; // 1
cout << ("abcd" < s2) << endl; // 1
return 0;
}
17,getline的使用
(1) | istream& getline (istream& is, string& str, char delim); |
---|---|
(2) | istream& getline (istream& is, string& str); |
cin 或者 scanf 遇到空格或者换行就结束了,如果多个值只能用空格换行间隔
geline就派上用场了,遇到空格不会结束
geline(cin,s); //遇到换行才结束
三,string类的模拟实现
模拟实现是指实现最核心的功能,不是实现一个一模一样的, 也不是吧每一个接口都实现,这样没有意义。
简单的string类的实现
实现一个简单的string类,限于时间,不可能要求具备 std::string的功能,但是至少要求能正确管理资源。-> 构造 + 析构 + 拷贝构造 + 赋值operator=,这些都是默认的成员函数。
将自定义的
string
类放在特定的命名空间中,如my_string
,能够有效地避免与标准库中的std::string
产生混淆。当未展开std
时,通过明确的命名空间限定来区分使用的是哪一个string
类。如果不将自定义的
string
类封装在命名空间中,又展开了std
,那么当同时存在标准库的string
和自定义的string
时,编译器将无法明确您要使用的是哪一个,从而导致混淆和潜在的错误。例如,如果在同一个作用域内同时有
string str1;
和my_string::string str2;
,通过命名空间就能清晰地知道str1
是标准库的,str2
是自定义命名空间中的。这样的命名空间管理有助于提高代码的可读性、可维护性和可扩展性。
我们就写两个文件,因为代码比较少,实现也是头文件中实现了,如果你想分离也可以,把声明和定义分离
在写第一步构造函数的时候就出现了问题,以下代码就存在深浅拷贝的问题。
构造函数的实现
string.h
#pragma once
namespace my_string
{
class string
{
public:
//构造函数
string(char* str)
:_str(str)
{}
//求大小
size_t size()
{
return strlen(_str);
}
char& operator[](size_t i)
{
return _str[i];
}
private:
char* _str;
};
}
test.cpp
#include <iostream>
#include <string>
using namespace std;
#include "string.h"
int main()
{
my_string::string s1("hello");
for (int i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
return 0;
}
上面代码是实现就是一个浅拷贝,直接把 str 赋值给 _str, 两者指向的还是同一块空间,析构的时候也会析构两次,这是类和对象(中),提到的深浅拷贝问题,而且该数据在常量区,不能进行修改。
深浅拷贝问题
类和对象(中) ---- 拷贝构造函数,遗留的一个深浅拷贝问题,现在我们可以来解决了
我们要解决的是两次析构的问题,和不能修改的问题,怎么办呢???
只有一个办法就是进行深拷贝,如何操作?
就是堆区上开一个和 "hello"一样大小的空间,可以动态增长,str 指向自己的空间, _str 是另外开辟的一份空间,所以析构的时候析构的是自己的空间,堆上的数据可以进行修改。
针对上面的代码进行修改后的构造函数
栈上的空间是自动分配的,而堆上可以自由管理,空间不够了可以增容
String对象中存储指针,指针指向的数组中存储字符,字符最后必须保留\0
带参的需要这样给,在堆上开一份一样大的空间,把值赋值过去
//构造函数
string(const char* str)
:_str(new char[strlen(str) + 1]) // hello \0,要多开一个存储\0.
{
strcpy(_str, str); //把str的值拷贝给 _str,strcpy 会把 \0 也拷贝过去
}
Std::string 当为空串的时候,不打印任何的东西
当我们自己实现string的时候,不传参数也存在问题,当我们遍历无参的string的时候,程序直接崩溃了。
所以我们可以再实现一个构造函数(重载)。以下是无参的构造函数
//接受无参的构造函数,
string()
:_str(new char[1])
{
_str[0] = '\0';
}
进而优化代码:无参和带参的可以合并,写一个全缺省的。
写到这里,我们终于把构造函数搞定了
string(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
析构函数的实现
~string()
{
delete[] _str;
_str = nullptr;
}
拷贝构造的实现
拷贝构造也是构造函数的一种,如果直接赋值也会是浅拷贝,所以我们也是给 _str 开一份一样大的空间
//拷贝构造
// string s2(s1)
string(const string& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
运算符重载operator= 的实现
//赋值运算符 =
string& operator=(const string& s)
{
if (this != &s) //防止自己给自己赋值
{
char* tmp = new char[strlen(s._str) + 1];
if (tmp == nullptr)
{
return *this;
}
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
}
以上整体代码:
string.h
#pragma once
namespace my_string
{
class string
{
public:
/*string()
:_str(new char[1])
{
_str[0] = '\0';
}
string(const char* str)
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}*/
//构造函数
string(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
}
//计算大小
size_t size()
{
return strlen(_str);
}
//operator[]重载
char& operator[](size_t i)
{
return _str[i];
}
//拷贝构造
// string s2(s1)
string(const string& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
//赋值 operator=
//支持返回值,是因为有连等
string& operator=(const string& s)
{
if (this != &s) //防止自己给自己赋值
{
char* tmp = new char[strlen(s._str) + 1];
if (tmp == nullptr)
{
return *this;
}
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
}
//c_str
const char* c_str()
{
return _str;
}
private:
char* _str;
};
//下面都是测试,可以写 test.c,使用命名空间的东西都是需要指定这个命名空间 my_string::
void test_string1()
{
string s1("hello");
string s2;
for (int i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
for (int i = 0; i < s2.size(); ++i)
{
cout << s2[i] << " ";
}
cout << endl;
}
void test_string2()
{
string s1("hello");
string s2(s1); //拷贝构造
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
string s3("world");
s1 = s3; //赋值
cout << s1.c_str() << endl;
cout << s3.c_str() << endl;
}
}
test.cpp
#include <iostream>
#include <string>
using namespace std;
#include "string.h"
int main()
{
my_string::test_string1();
my_string::test_string2();
return 0;
}
深拷贝的现代写法
首先,在拷贝构造函数
string(const string& s)
中,将_str
初始化为nullptr
,这是为了确保在后续的操作中,出作用域析构的时候析构随机值就会出错,或出现异常或者其他情况,不会因为使用未初始化的_str
而导致错误。然后,创建一个临时对象
tmp
,并通过调用普通的构造函数为其分配内存并进行数据的拷贝(string tmp(s._str)
)。接下来,使用
swap
函数交换_str
和tmp._str
。这样,新创建的对象(即正在进行拷贝构造的对象)就拥有了与原始对象独立的内存空间和数据副本。通过这种方式,实现了对原始对象的深拷贝,使得新对象和原始对象在内存上相互独立,修改其中一个对象的数据不会影响到另一个对象。
赋值运算的现代写法也是同理
构造函数和拷贝构造现代写法
//深拷贝--现代写法
string(const string& s)
:_str(nullptr)
{
string tmp(s._str); //tmp去调构造函数,开空间拷贝数据
swap(_str, tmp._str);
}
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);//调构造,如果写s就是调用拷贝构造
swap(_str, tmp._str);
}
return *this;
}
operator= 还可以进一步简写
string & operator=(string s) // 传参给s取调拷贝构造 s就是外面传过来的副本,在有拷贝构造的基础是也是深拷贝
{
swap(_str, s._str);
return *this;
}
以上简单string的实现其实是一个面试题目 ->深浅拷贝的问题。
搞定了这么是一个面试题,接下来我们要实现一个支持增删查改的 string
实现具有增删改查的string类
实现一个具有增删改查的string类,就需要实现可动态增长,需要多增加两个变量来记录字符的有效个数和容量的大小,如果空间不够就需要增容,这和顺序表的实现类似。
在实现之前,我们还是先把默认的成员函数先实现(构造+析构+拷贝构造+赋值)
第一步先把string类架子搭建起来
namespace my_string
{
class string
{
public:
//迭代器的实现
//构造函数
//析构函数
//拷贝构造
//......
private:
char* _str;
size_t _size; //已经有多少个有效字符
size_t _capacity;
};
}
1.构造函数
string(const char* str = "")
{
_size = strlen(str); // 因为每次使用strlen本质都是在变量字符串数组,所以我们就使用一次strlen,减少复用
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
2.析构函数
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
3.拷贝构造
传统写法和现代写法,选择一种实现即可
//拷贝构造---传统写法
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
char* tmp = new char[_capacity + 1];
strcpy(tmp, s._str);
_str = tmp;
}
//拷贝构造---现代写法
void swap(string& s) //因为要交换三个值,赋值运算符的重载也会用到交换三个值这个函数,直接调用
{
::swap(_str, s._str); // ::表示调用的是全局的swap,标准库已经实现了的,我们不需要写
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//拷贝构造
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
this->swap(tmp); //this通常省略,这是隐藏的this指针去调用swap函数
//swap(tmp);
}
4.赋值运算符
传统写法和现代写法二选一
//传统写法
string& operator=(const string& s)
{
if (this != &s) //防止自己给自己赋值
{
_size = s._size;
_capacity = s._capacity;
char* tmp = new char[_capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
}
//现代写法
string& operator=(string s) //调用拷贝构造
{
if (this != &s) //防止自己给自己赋值
{
this->swap(s); //这是写拷贝构造实现的swap函数,可以直接调用完成三个变量的交换
//swap(s);
return *this;
}
}
5.迭代器 iterator的实现
我们可以把size,capacity,operator[] ,c_str 也顺便一起实现了
#include <assert.h>
namespace my_string
{
class string
{
public:
//迭代器的实现
typedef char* iterator;
iterator begin()
{
return _str; // 像指针一样指向字符串数组的下标0
}
iterator end()
{
return _str + _size; //指向字符串数组的最后一个元素的下一个位置
}
//构造函数
//析构函数
//拷贝构造
//赋值
size_t size() const //当外面是const的时候可以调用const,但是不能调用非const所以加上const
{
return _size;
}
size_t capcaity() const
{
return _capacity;
}
// operator[] 和const operator[]
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
const char* c_str()
{
return _str;
}
private:
char* _str;
size_t _size; //已经有多少个有效字符
size_t _capacity;
};
}
6. push_bcak
push_back和顺序表中的尾插一样,尾插之前第一步都是先判断容量是否充足,如果不够就需要增容,同样的逻辑
void push_back(char ch)
{
//如果空间不够,增2倍
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
char* newstr = new char[newcapacity + 1]; //\0不是有效数据,但是\0需要占用一个空间
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = newcapacity;
}
_str[_size] = ch;
++_size;
_str[_size] = '\0'; //这里必须在末尾加 \0
}
7. append
append 和 push_back逻辑也是类似的
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = _size + len;
char* newstr = new char[newcapacity + 1];
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = newcapacity;
}
strcpy(_str + _size, str);
_size += len;
}
8. reserve
push_back 和 append 的实现有逻辑一样的代码,就是在增容的时候都是同样的代码,我们就可以写成一个 reserve 改变容量,直接调用reserve
//reserve 把容量增到多少,消灭重复的代码,假设n == 100,容量不够的时候就增加
void reserve(size_t n)
{
if (n > _capacity) //检查是因为这是一个公有函数,不仅仅是插入删除会调用这个函数,还有其它会调用就做了一步检查
{
char* newstr = new char[n + 1];
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
9.重载运算符(operator+=)
+=一个字符可以直接调用上面的push_back
+=一个字符串可以直接调用 append
单独实现也是同样的逻辑,推荐使用 +=。
// s1+= 'a';
string& operator+=(char ch)
{
this->push_back(ch);
return *this;
}
// s1+= "aaaaa";
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
10.insert
insert 插入一个字符
① 插入字符之前,先判断是否需要增容
② 把 pos 到 _size-1 的有效字符全部往后挪动一个字符单位
我们发现最后还有补一个 \0 作为结束标志,所以我们直接把
pos 到 \0 这些字符一起往后挪动一位
③ 我就是要让 pos 为 size_t 类型,当 pos = 0 的时候,end 不断减小,直到变成 -1, 如果不把 pos 强转成 int
pos <= -1, int 和 size_t 比较,会把 -1 整型转成size_t类型,end会变成一个很大的数字,
这样就会死循环了
//插入一个字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while ((int)pos <= end)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
insert 插入一个字符串
同样和 append 一样的逻辑,只不过多了个 pos,需要把位置空出来,插入字符串
和 insert 插入一个字符一样
①判断容量
②挪走空出位置
③把字符串拷贝过去
//插入一个字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size); // = 指尾插
size_t len = strlen(str);
if(len + _size > _capacity )
{
size_t newcapacity = len + _size;
reserve(newcapacity);
}
int end = _size;
while (end >= (int)pos) //当end是-1的时候会转成很大的数
{
//每次移动 len,空出 len 个位置插入
_str[end + len] = _str[end];
--end;
}
//不拷贝\0
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
11. erase
定义npos
class string
{
public:
//.....
private:
char* _str;
size_t _size; //已经有多少个有效字符
size_t _capacity;
static size_t npos; //静态成员 npos声明
};
size_t string::npos = -1; // npos 的定义
erase的实现
以下是图解:
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t i = pos + len;
while (i <= _size)
{
_str[i - len] = _str[i];
++i;
}
_size -= len;
}
return *this;
}
12. find
查找一个字符或者一个字符串
//查找一个字符
size_t find(char ch, size_t pos = 0)
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//查找一个字符串
size_t find(const char* str, size_t pos = 0)
{
char* p = strstr(_str, str); // 找字串 strstr的模拟实现必须要会,这是C语言基础
if (p == nullptr)
{
return npos;
}
else
{
return p - _str; //返回个数
}
}
13. resize
调整字符串的大小,如果调整成比原来的字符串小直接补 \0
如果调整的范围超出了字符串的范围,补插入的字符,如果不给默认就是补 \0 填充
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (int i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
14. 运算符重载
// 两个字符的比较大小 s1 < s2
bool operator<(const string& s)
{
int ret = strcmp(_str, s._str); //strcmp比较字符串模拟实现需要会
return ret < 0;
}
bool operator==(const string& s)
{
int ret = strcmp(_str, s._str);
return ret == 0;
}
bool operator<=(const string& s)
{
return *this == s || *this < s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
15. 重载 operator<< 与 operator>>
operator<< 输出,可以使用友元函数,也可以直接遍历
//可以使用友元函数,重载输出 operator<<
//或者直接输出每一个字符
ostream& operator<<(ostream& out, const string& s) // const 接受
{
for (size_t i = 0; i < s.size(); ++i) // s.size() s是const去调用size,所以我们 size 也加了const
{
cout << s[i]; // operator[]函数 也加了const
}
cout << endl;
return out;
}
//下面是友元函数
使用友元函数需要再类内部声明,这样才能访问私有成员
/*ostream& operator<<(ostream& out, const string& s)
{
out << s._str << endl;
return out;
}*/
operator>>输入
// operator>>输入
//getline,空格不结束
istream& operator>>(istream& in, string& s)
{
while (1)
{
char ch;
//in >> ch;
ch = in.get(); //每次读取一个字符
if (ch == ' ' || ch == '\n')
{
break;
}
else
{
s += ch; //调用 += 把字符拼接起来
}
}
return in;
}
两者对比
整体代码
string.h
//实现一个支持增删查改的string
#include <assert.h>
namespace my_string
{
class string
{
public:
//迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//friend ostream& operator<<(ostream& out, const string& s);
//构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造---传统写法
/* string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
char* tmp = new char[_capacity + 1];
strcpy(tmp, s._str);
_str = tmp;
}*/
//拷贝构造---现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
this->swap(tmp);
//swap(tmp);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 赋值
//s1 = s3
//传统写法
string& operator=(const string& s)
{
if (this != &s) //防止自己给自己赋值
{
_size = s._size;
_capacity = s._capacity;
char* tmp = new char[_capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
return *this;
}
}
//现代写法
string& operator=(string s) //调用拷贝构造
{
if (this != &s) //防止自己给自己赋值
{
this->swap(s);
//swap(s);
return *this;
}
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
size_t size() const
{
return _size;
}
size_t capcaity() const
{
return _capacity;
}
// operator[] 和const operator[]
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
const char* c_str()
{
return _str;
}
//reserve 把容量增到多少,消灭重复的代码,假设n == 100,容量不够的时候就增加
void reserve(size_t n)
{
if (n > _capacity) //检查是因为这是一个公有函数,不仅仅是插入删除会调用这个函数,还有其它会调用就做了一步检查
{
char* newstr = new char[n + 1];
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (int i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
//增
void push_back(char ch)
{
如果空间不够,增2倍
//if (_size == _capacity)
//{
// size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
// //char* newstr = new char[newcapacity + 1]; //\0不是有效数据,但是\0需要占用一个空间
// //strcpy(newstr, _str);
// //delete[] _str;
// //_str = newstr;
// //_capacity = newcapacity;
// reserve(newcapacity);
//}
//_str[_size] = ch;
//++_size;
//_str[_size] = '\0';
//转成直接调用insert
insert(_size, ch);
}
void append(const char* str)
{
//size_t len = strlen(str);
//if (_size + len > _capacity)
//{
// size_t newcapacity = _size + len;
// /*char* newstr = new char[newcapacity + 1];
// strcpy(newstr, _str);
// delete[] _str;
// _str = newstr;
// _capacity = newcapacity;*/
// reserve(newcapacity);
//}
//strcpy(_str + _size, str);
//_size += len;
//转成直接调用insert
insert(_size, str);
}
// 运算符重载 operator+=
// s1+= 'a';
string& operator+=(char ch)
{
this->push_back(ch);
return *this;
}
// s1+= "aaaaa";
string& operator+=(const char* str)
{
this->append(str);
return *this;
}
//插入一个字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while ((int)pos <= end)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
//插入一个字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size); // = 指尾插
size_t len = strlen(str);
if(len + _size > _capacity )
{
size_t newcapacity = len + _size;
reserve(newcapacity);
}
int end = _size;
while (end >= (int)pos) //当end是-1的时候会转成很大的数
{
//每次移动 len,空出 len 个位置插入
_str[end + len] = _str[end];
--end;
}
/* int start = 0;
while (start < len)
{
_str[pos++] = str[start++];
}*/
//不拷贝\0
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t i = pos + len;
while (i <= _size)
{
_str[i - len] = _str[i];
++i;
}
_size -= len;
}
return *this;
}
size_t find(char ch, size_t pos = 0)
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
char* p = strstr(_str, str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
// 两个字符的比较大小 s1 < s2
bool operator<(const string& s)
{
int ret = strcmp(_str, s._str);
return ret < 0;
}
bool operator==(const string& s)
{
int ret = strcmp(_str, s._str);
return ret == 0;
}
bool operator<=(const string& s)
{
return *this == s || *this < s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
private:
char* _str;
size_t _size; //已经有多少个有效字符
size_t _capacity; // 能存多少个有效字符,\0不是有效字符
static size_t npos;
};
size_t string::npos = -1;
//可以使用友元函数,重载输出 operator<<
//或者直接输出每一个字符
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i];
}
cout << endl;
return out;
}
//下面是友元函数
/*ostream& operator<<(ostream& out, const string& s)
{
out << s._str << endl;
return out;
}*/
// operator>>输入
//getline,空格不结束
istream& operator>>(istream& in, string& s)
{
while (1)
{
char ch;
//in >> ch;
ch = in.get();
if (ch == ' ' || ch == '\n')
{
break;
}
else
{
s += ch;
}
}
return in;
}
//以下全是测试代码
void test_string1()
{
string s1;
string s2("hello");
cout << s1 << endl;
cout << s2 << endl;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
//三种遍历方式
for (size_t i = 0; i < s2.size(); ++i)
{
s2[i] += 1; //没有调用+=,字符 += 1
cout << s2[i] << " ";
}
cout << endl;
//迭代器
string::iterator it2 = s2.begin();
while (it2 != s2.end())
{
*it2 -= 1;
cout << *it2 << " ";
++it2;
}
cout << endl;
//范围for是由迭代器支持的,也就是说这段代码最终会被编译器替换成迭代器
//iterator begin end
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
}
void test_string2()
{
string s1("hello");
s1.push_back('a');
s1.push_back('b');
s1.push_back('c');
s1.push_back('e');
s1.push_back('f');
s1.push_back('g'); //增加到第六次就出现问题
//s1.append(" dyyx1231");
cout << s1 << endl;
s1 += 'a';
s1 += "dyyy";
cout << s1 << endl;
}
void test_strign3()
{
string s1("hello");
s1.insert(1,'x');
s1.insert(1,"xyz");
cout << s1 << endl;
string s2("hello");
s2.reserve(11);
cout << s2.size() << endl;
cout << s2.capcaity() << endl;
s2.resize(8, 'x');
cout << s2.size() << endl;
cout << s2.capcaity() << endl;
s2.resize(2);
cout << s2.size() << endl;
cout << s2.capcaity() << endl;
}
void test_strign4()
{
string s1("helloworld");
s1.erase(5, 2);
cout << s1 << endl;
s1.erase(5, 4);
cout << s1 << endl;
string s2("abcdabcef");
cout << s2.find("bce") << endl;
cout << s2.find("sfa") << endl;
}
void test_strign5()
{
/*string s;
cin >> s;
cout << s;*/
string s("hello");
s.insert(2, "xxx");
cout << s << endl;
s.insert(0, "xxx");
cout << s << endl;
}
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "string.h"
int main()
{
//模拟实现string
my_string::test_string1();
//my_string::test_string2();
//my_string::test_strign3();
//my_string::test_strign4();
//my_string::test_strign5();
return 0;
}
四,小结
我们能够实现出个一个具有增删改查的 string 类,实现过程中我们把前面所学的知识都运用起来了,顺便把深浅拷贝问题也解决了,静态成员变量的使用、友元函数的使用等等,认识了迭代器的使用。
此外,我们还对代码进行了优化,提高了程序的性能和可读性。例如,在内存分配和释放时,采用了更高效的算法,减少了不必要的开销以及深浅拷贝的现代写法。
通过这次实现,我们不仅巩固和拓展了所学的 C++ 知识,还提升了问题解决能力和编程思维,为我们日后打下了基础。加油~