C++【string类,模拟实现string类】
🌟个人主页:落叶
🌟当前专栏: C++专栏
目录
为什么学习string类
C语言中的字符串
标准库中的string类
auto和范围for
auto关键字
迭代器
范围for
string类的常用接口说明和使用
1. string类对象的常见构造
2.string类对象的容量操作
3.string类对象的访问及遍历操作
4. string类对象的修改操作
【push_back】在字符串后尾插字符c
【append】在字符串后追加一个字符串
【find + npos(重 点)】从字符串pos位置开始往后找字符c,返回该字符在字符串中的 位置
【rfind】从字符串pos位置开始往前找字符c,返回该字符在字符串中的 位置。
【erase】从字符串中删除字符
5.string类非成员函数
【operator+】尽量少用,因为传值返回,导致深拷贝效率低
【operator>> (重点)】输入运算符重载
【operator<< (重点)】输出运算符重载
【getline (重点)】获取一行字符串
【relational operators (重点)】大小比较
string类的模拟实现
经典的string类问题
浅拷贝
深拷贝
浅拷贝(Shallow Copy)
深拷贝(Deep Copy)
写时拷贝(了解)
为什么学习string类
C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户 自己管理,稍不留神可能还会越界访问。
标准库中的string类
在使用string类时,必须包含#include头文件以及using namespace std;
auto和范围for
auto关键字
在这里补充2个C++11的小语法,方便我们后面的学习。
- 在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个 不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型 指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期 推导而得。
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
- auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
- auto不能直接用来声明数组
auto声明的变量编译器会推导而得类型。
这个typeid是获取变量的真实的类型,
我们可以看到类型是对得上的。
不能没有值
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项auto e;
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能用来声明数组
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
迭代器
https://legacy.cplusplus.com/reference/string/string/begin/
迭代器类似于指针,它指向容器中的一个元素,并且可以通过递增或递减来移动到下一个或前一个元素。迭代器提供了一组操作,如解引用(*),递增(++),递减(--),比较(==, !=)等。
add.begin()是指向第一个元素,
add.end()是指向最后一个元素,
通过i遍历每个字符
在这个例子中,add.begin
()
返回一个指向vec
第一个元素的迭代器,add.end()
返回一个指向add最后一个元素之后位置的迭代器。循环中,i
被用来遍历add中的元素,*i
用来访问当前元素的值。
迭代器使得算法可以与容器解耦,提高了代码的复用性和灵活性。
范围for
- 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历
- 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
范围for底层就是迭代器,范围for可以作用到数组和容器对象上进行遍历。
为什么要在auto后面加引用呢?
通过引用遍历可以避免不必要的拷贝构造函数的调用,从而提高性能。
这样做有几个优点:
性能优化:如果容器中的元素很大,或者元素是复杂类型(如类对象),通过引用遍历可以避免不必要的拷贝构造函数的调用,从而提高性能。
修改元素:如果需要在循环中修改容器中的元素,必须通过引用来遍历它们。如果不使用引用,循环变量将是容器中元素的一个副本,对
副本的修改不会影响到原始容器中的元素。
举个例子,auto&
告诉编译器,num
是一个引用 add容器中的实际元素,而不是一个副本。因此,如果对 num
的修改会直接 add中的元素。
string类的常用接口说明和使用
在C++中,std::string
类是标准库的一部分,它提供了一个方便的接口来处理字符串。std::string
位于 <string>
头文件中,并且是 std
命名空间的一部分。
#include<string>
1. string类对象的常见构造
https://legacy.cplusplus.com/reference/string/string/string/
(constructor)函数名称 | 功能说明 |
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重点) | 拷贝构造函数 |
#include<iostream>
using namespace std;
#include<string>int main()
{string s1; //构造空的string对象string s2("hello");//用字符串构造string对象string s3(10, 'w');//用10个w构造string对象string s4(s2); //拷贝构造函数return 0;
}
2.string类对象的容量操作
函数名称 | 功能说明 |
size(重点) | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间** |
resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
size我们可以看到字符串有效字符长度是5
https://legacy.cplusplus.com/reference/string/string/size/
capacity返回空间总大小
https://legacy.cplusplus.com/reference/string/string/capacity/
明明只有4个字符,空间大小怎么会是15呢?
因为底层有个16大小的数组存放数据,
当我们给满16个字符,会自动扩容到32大小的空间。
empty(重点)检测字符串释放为空串,是返回true,否则返回false
https://legacy.cplusplus.com/reference/string/string/empty/
是空字符串返回true就是1,不是空字符串返回false就是0
clear (重点)清空有效字符
https://legacy.cplusplus.com/reference/string/string/clear/
我们可以看到清空了有效字符,但是空间大小不变。
reserve (重点)为字符串预留空间**
https://legacy.cplusplus.com/reference/string/string/reserve/
reserve 也就是扩容空间的意思。
数据很多的话,我们可以提前扩容空间避免多次2倍扩容。
resize (重点)将有效字符的个数改成n个,多出的空间用\0填充
https://legacy.cplusplus.com/reference/string/string/resize/
字符不变,多出来的空间都用\0填充。
这里是3,abc保留,cdef被清除。
注意: 1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接 口保持一致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不 同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数 增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参 数小于string的底层空间总大小时,reserver不会改变容量大小。
3.string类对象的访问及遍历操作
函数名称 | 功能说明 |
operator[] (重 点) | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位 置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位 置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
operator[] (重 点)返回pos位置的字符,const string类对象调用】
https://legacy.cplusplus.com/reference/string/string/operator[]/
rbegin + rend begin获取一个字符的迭代器 + end获取最后一个字符下一个位 置的迭代器。
https://legacy.cplusplus.com/reference/string/string/rbegin/
正向迭代器是iterator,反向迭代器是reverse_iterator。
reverse_iterator
是标准库提供的一种迭代器适配器,它允许我们以逆序的方式遍历容器。reverse_iterator
的设计是为了简化反向遍历容器的过程,使得我们可以像使用普通迭代器一样使用它,但方向相反。
以下是使用 reverse_iterator
的几个主要原因:
1.简化代码:使用
reverse_iterator
可以避免手动计算容器的结束位置和开始位置之间的偏移量,从而简化代码。2.一致性:它提供了一种与普通迭代器使用方式一致的方法来反向遍历容器,使得代码更加直观和易于理解。
3.与算法兼容:许多标准库算法都设计为与迭代器一起工作。通过使用
reverse_iterator
,我们可以将这些算法应用于容器的反向遍历,而无需修改算法本身。4.安全性:使用
reverse_iterator
可以减少由于手动计算迭代器位置而可能引入的错误。
4. string类对象的修改操作
函数名称 | |
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重 点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find + npos(重 点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的 位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的 位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
erase | 从字符串中删除字符 |
【push_back】在字符串后尾插字符c
https://legacy.cplusplus.com/reference/string/string/push_back/
【append】在字符串后追加一个字符串
https://legacy.cplusplus.com/reference/string/string/append/
abcdefg插入了1111的后面
【operator+= (重 点)】 在字符串后追加字符串str
https://legacy.cplusplus.com/reference/string/string/operator+=/
可以+=字符串和字符
也可以+=一个对象
【c_str(重点)】返回C格式字符串
https://legacy.cplusplus.com/reference/string/string/c_str/
可以返回字符串
【find + npos(重 点)】从字符串pos位置开始往后找字符c,返回该字符在字符串中的 位置
https://legacy.cplusplus.com/reference/string/string/find/
在字符串中查找内容
下面我们可以看到,从0下标位置开始查找9这个字符,找到了返回9的下标,没有找到返回-1
没有找到交换返回-1
【rfind】从字符串pos位置开始往前找字符c,返回该字符在字符串中的 位置。
https://legacy.cplusplus.com/reference/string/string/rfind/
我们不写下标,默认从最后一个下标开始往前查找。
找到了返回下标,没有找到返回-1。
【substr 】 在str中从pos位置开始,截取n个字符,然后将其返回
https://legacy.cplusplus.com/reference/string/string/substr/
从3下标开始截取4个字符,给s2。
我们也可以用find查找a这个下标, 然后通过i下标这个位置截取3个字符。
【erase】从字符串中删除字符
https://legacy.cplusplus.com/reference/string/string/erase/
下面我们可以看到,从下标1删除到下标5,就只剩下a和g了。
5.string类非成员函数
函数 | 功能说明 |
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
【operator+】尽量少用,因为传值返回,导致深拷贝效率低
https://legacy.cplusplus.com/reference/string/string/operator+/
我们可以看到s1是您好,s2是落叶,加起来就是您好落叶。
我们也可以加字符串
【operator>> (重点)】输入运算符重载
https://legacy.cplusplus.com/reference/string/string/operator%3E%3E/
可以往s1对象,输入字符串,
但是遇到空格就会停止
【operator<< (重点)】输出运算符重载
https://legacy.cplusplus.com/reference/string/string/operator%3C%3C/
【getline (重点)】获取一行字符串
https://legacy.cplusplus.com/reference/string/string/getline/
这个它遇到空格不会停止,会继续往后读取字符串,也会把空格给读取了。
【relational operators (重点)】大小比较
https://legacy.cplusplus.com/reference/string/string/operators/
计算比较大小,如果大于为真,返回1,为假返回0。
string类的模拟实现
经典的string类问题
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让 学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析 构函数。大家看下以下string类的实现是否有问题?
// 为了和标准库区分,此处使用String
class String
{
public:/*String():_str(new char[1]){*_str = '\0';}*///String(const char* str = "\0") 错误示范//String(const char* str = nullptr) 错误示范String(const char* str = ""){// 构造String类对象时,如果传递nullptr指针,可以认为程序非if (nullptr == str){assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);}~String(){if (_str){delete[] _str;_str = nullptr;}}
private:char* _str;
};
// 测试
void TestString()
{String s1("hello bit!!!");String s2(s1);
}
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认 的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内 存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致 多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该 资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一 不想分享就你争我夺,玩具损坏。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父 母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给 出。一般情况都是按照深拷贝方式提供。
浅拷贝(Shallow Copy)
浅拷贝通常指的是对象之间的简单位拷贝(bitwise copy),这意味着新对象和原对象将共享相同的资源。在C++中,如果你没有显式地定义拷贝构造函数和赋值运算符,编译器会为你生成默认的版本,这些默认版本执行的是浅拷贝。
对于
std::string
来说,浅拷贝是不存在的,因为std::string
内部管理着自己的动态内存。当你进行拷贝构造或赋值操作时,std::string
会执行深拷贝。这意味着新创建的字符串对象拥有自己独立的内存空间,与原字符串对象不共享任何资源。深拷贝(Deep Copy)
深拷贝指的是创建一个新对象,并且递归地复制原对象中的所有元素,使得新对象和原对象完全独立。在
std::string
的情况下,每次你进行拷贝构造或赋值操作时,都会执行深拷贝
stringl类模拟实现【代码】
string.h
#define _CRT_SECURE_NO_WARNINGS 1#pragma once
#include<iostream>
#include<assert.h>
using namespace std;namespace bit
{class string{public://迭代器typedef char* iterator;static const int npos;string(const char* str = "");string(const string& s);~string();///扩容void reserve(size_t n);//尾插字符void push_back(char ch);//尾插字符串void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);//传统string& operator=(const string& s);//现代string& operator=(string s);//清除字符void clear(){_str[0] = '\0';_size = 0;}//首字符iterator begin(){return _str;}//最后的iterator end(){return _str + _size;}//返回_strchar* c_str() const{return _str;}//size_t size()const{return _size;}size_t capacity()const{return _capacity;}//指定位置修改字符char& operator[](size_t index){assert(index < _size);return _str[index];}//指定位置修改字符const char& operator[](size_t index)const{assert(index < _size);return _str[index];}////查询字符size_t find(char ch, size_t pos = 0);//查询字符串size_t find(const char* str, size_t pos = 0);//指定位置插入字符string& insert(size_t pos, char c);//指定位置插入字符串string& insert(size_t pos, const char* str);// 删除pos位置上的元素,并返回该元素的下一个位置string& erase(size_t pos, size_t len);//调整字符串大小void resize(size_t n, char c = '\0');//判断字符串是不是空bool empty()const;//字符串交换void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}void swap(string& s, string& s1){s.swap(s1);}//流插入friend ostream& operator<<(ostream& _cout, const bit::string& s);//流提取friend istream& operator>>(istream& _cin, bit::string& s);private:char* _str;size_t _size;size_t _capacity;};//判断大于-小于-大于等于 - 小于等于 - 等于 - 不等于bool operator<(const string& lhs, const string& rhs);bool operator>(const string& lhs, const string& rhs);bool operator<=(const string& lhs, const string& rhs);bool operator>=(const string& lhs, const string& rhs);bool operator==(const string& lhs, const string& rhs);bool operator!=(const string& lhs, const string& rhs);
}
string.cpp
#include"string.h"namespace bit
{const int string::npos = -1;//构造string::string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}深拷贝-(传统写法)//string::string(const string& s)//{// _str = new char[s._capacity + 1];// strcpy(_str, s._str);// _size = s._size;// _capacity = s._capacity;//}//深拷贝-(现代写法)string::string(const string& s){string tmp(s._str);swap(tmp);}//析构string::~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}/////扩容void string::reserve(size_t n){cout << "reserve:"<< n << endl;if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void string::push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;}//尾插字符串void string::append(const char* str){size_t len = strlen(str);if ((_size + len) > _capacity){size_t tmp = _capacity * 2;if (tmp < (_size + len)){tmp = (_size + len);}reserve(tmp);}strcpy(_str + _size, str);_size += len;}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}赋值-(传统写法)//string& string::operator=(const 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;//}//赋值-(现代写法) string& string::operator=(string s){swap(s);return *this;}//查询字符size_t string::find(char ch, size_t pos){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (ch == _str[i]){return i;}}return npos;}//查询字符串size_t string::find(const char* str, size_t pos){assert(pos < _size);const char* tmp = strstr(_str + pos, str);if (tmp == __nullptr){return npos;}return tmp - _str;}//在字符串里插入字符string& string::insert(size_t pos, char ch){assert(pos < _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//循环移动字符int i = _size;while ((int)pos <= i){_str[i + 1] = _str[i];i--;}//在pos位置插入字符_str[pos] = ch;_size++;return *this;}//指定位置插入字符串string& string::insert(size_t pos, const char* str){size_t len = strlen(str);if ((_size + len) > _capacity){size_t tmp = _capacity * 2;if (tmp < (_size + len)){tmp = (_size + len);}reserve(tmp);}//循环往后移动字符size_t i = _size + len;while (pos < i){_str[i] = _str[i - len];i--;}//循环把str的字符一个一个,赋值给_str[pos+i]的位置for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size + len;return *this;}//指定位置删除字符串string& string::erase(size_t pos, size_t len){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 = _size - len;}return *this;}//判断大于-小于-大于等于 - 小于等于 - 等于 - 不等于bool operator<(const string& lhs, const string& rhs){return strcmp(lhs.c_str(), rhs.c_str()) < 0;}bool operator>(const string& lhs, const string& rhs){return !(lhs <= rhs);}bool operator<=(const string& lhs, const string& rhs){return (lhs < rhs) || (lhs == rhs);}bool operator>=(const string& lhs, const string& rhs){return !(lhs < rhs);}bool operator==(const string& lhs, const string& rhs){return strcmp(lhs.c_str(), rhs.c_str()) == 0;}bool operator!=(const string& lhs, const string& rhs){return !(lhs == rhs);}//调整字符串大小void string::resize(size_t n, char c){_str[n] = c;}//删除pos位置上的元素,并返回该元素的下一个位置bool string::empty()const{if (_size != 0){return false;}else{return true;}}//流插入ostream& operator<<(ostream& _cout, const bit::string& s){for (size_t i = 0; i < s._size; i++){_cout << s._str[i];}return _cout;}//流提取istream& operator>>(istream& _cin, bit::string& str){str.clear();int i = 0;char buff[256];char ch;ch = _cin.get();while (ch != ' ' && ch != '\n'){buff[i++] += ch;if (i == 255){buff[i] = '\0';str += buff;i = 0;}ch = _cin.get();}if (i > 0){buff[i] = '\0';str += buff;}return _cin;}}
test.cpp【测试】
#include"string.h"//int main()
//{
// bit::string n("asdasdf");
// n += 'w';
// cout << n.c_str() << endl;
//}//int main()
//{
// bit::string n("asfsddfgdf");
// //迭代器
// bit::string::iterator i = n.begin();
// while (i != n.end())
// {
// cout << *i << endl;
// i++;
// }
//}//int main()
//{
// bit::string n("qweeqqqw");
// //n.clear();
// //cout << n.c_str() << endl;
// n.insert(0, "11111111");
// cout << n.c_str() << endl;
//}//int main()
//{
// bit::string n("11223344556677");
// /*n.erase(4, 99);*/
// n[2] = 'x';
// cout << n.c_str() << endl;
//}//int main()
//{
// bit::string n("11223344556677");
// bit::string s1(n);
// cout << (n < s1) << endl;
// cout << (n > s1) << endl;
// cout << (n <= s1) << endl;
// cout << (n >= s1) << endl;
// cout << (n == s1) << endl;
// cout << (n != s1) << endl;
//
//}//int main()
//{
// bit::string n("11223344556677");
// /*n.resize(8,'y');
// n.resize(8);
// cout << n.c_str() << endl;*/
//
// cout << n.empty() << endl;
//}//int main()
//{
// string n("1122");
// string s1("qqwee");
// n.swap(s1);
// cout << n << endl;
// cout << s1 << endl;
//}int main()
{bit::string n("112233");bit::string n1 = n;//bit::string s1("qweqwer");//n.swap(s1);//cout << n << endl;//cout << s1 << endl;/*cin >> n;cout << n << endl;*///getline(cin, n);//n = n1;//swap(n, n1);//swap(n,n1);//cout << n << endl;cout << n1 << endl;
}
写时拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该 资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源, 如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有 其他对象在使用该资源。
写时拷贝
写时拷贝在读取是的缺陷