string(c++)
前言
string 并不是STL库里的一个容器,而是游离于STL的一个容器,这是历史原因导致的。因为他的研发比STL早,但是后面大佬们研发出了STL。string为了兼容STL就做了一些调整,一贯的作风就是向上兼容。因此,他相比STL其他的容器会有点冗余。
提问:string就是字符串吗?他和字符串一摸一样吗?答案是:不是,大部分情况一摸一样。
string和字符串是有区别的,输出字符串是以\0为结束标志,但是string是一个容器,他以size为结束标志。所以,在模拟string库的时候拷贝使用要考虑到这种情况,建议统一使用memcpy。但是这种情况是很罕见的,没有人会在一个字符插一个\0,这很奇怪。
正文
string本质上就是一个顺序表,他的底层就是用一个堆上的内存存储字符,这个类用size记录字符个数,capacity记录开辟的空间,空间不够就扩容,在这基础上实现了一些功能而已。
我们学习string应该从他的本质上了解出发,不要认为string是什么高级的东西,不过就是一个顺序表吗?能有多复杂?
string类的函数介绍
构造函数
这是他的构造函数,很多个,很冗余,其实掌握几个常用的就行。
string()空参构造,string(const char* s),string(const string& s) 这几是比较常用的构造函数。
#include <iostream>
#include <string>
using namespace std;
int main()
{//构造函数 重点string s1();string s2("123");cout << s2 << endl;//string s3(s2);cout << s3 << endl;return 0;
}
string对象容量操作函数
函数名称 | 功能说明 |
size | 返回有效字符的长度 |
length | 返回有效字符的长度 |
capacity | 返回空间总大小 |
empty | 检测字符串是否为空串,返回值类型是bool |
clear | 清空有效字符 |
reserve | 扩容, |
resize | 将有效字符的个数设置成n个,多出的用字符c填充 |
这里size和length里两个函数底层实现是一摸一样,大家以后用size就行,这是为了兼容STL库,这也体现了的冗余。注意clear是清楚有效字符,不是释放空间,改变的是size。
reserve这个函数是扩容,改变的是capacaty,不是size。resize改变的才是size。
这里注意扩容的逻辑是,要增加空间,才会开辟空间,如果你的要求是减少空间,它实际是不会实施的,capacity还是原来的大小。
string对象访问操作函数
函数名称 | 功能说明 |
operator[] | 返回pos位置字符 |
begin + end | begin获得第一个字符的迭代器,end获得最后一个字符的下一个位置的迭代器 |
rbegin +rend | rbegin获得最后一个字符的下一个位置的迭代器,rend获得第一个字符的迭代器 |
范围for | 底层就是迭代器,c++11支持的遍历方式 |
这里的迭代器就是类里的一个公共变量,在string里他是一个指针,但是在其他容器里他的底层不一定是指针。
#include <iostream>
#include <string>
using namespace std;
int main()
{string s = "deepseek";//数组遍历for (size_t i = 0; i < s.size(); i++){cout << s[i] << " ";}cout << endl;//迭代器//从前往后string::iterator it1 = s.begin();while (it1 != s.end()){cout << *it1 << " ";it1++;}cout << endl;// 从后往前auto it2 = s.rbegin();while (it2 != s.rend()){cout << *it2 << " ";it2++;}cout << endl;//增强forfor (auto c : s){cout << c << " ";}return 0;
}
string类对象的修改操作
函数名称 | 功能说明 |
push_back | 尾插字符 |
append | 尾部追加字符串 |
operator+= | 尾部追加字符串str |
c_str | 返回对象的格式字符串 |
find | 从字符串pos位置开始找字符c,并返回下标,默认从前往后找 |
rfind | 从字符串pos位置开始找字符c,并返回下标,默认从后往前找 |
substr | 在str中pos位置开始,截取n个字符,然后将其返回 |
operator+ | 尽量少用,传值返回,效率低(深拷贝) |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
push_back 和 append本质上就是尾插,差别不大。operator+=包含了这连个函数的功能,底层是一样的,一般使用operator+=。
#include <iostream>
#include <string>
using namespace std;
int main()
{string s = "hello";size_t pos1 = s.find('l');size_t pos2 = s.rfind('l');cout << pos1 << endl << pos2 << endl;//尾插s.push_back(' ');s.append("world");cout << s.c_str() << endl;//截取字符串string sTmp = s.substr(1, 5);cout << sTmp << endl;//+=运算符重载s += " 字节跳动";cout << s << endl;
}
relational operators
这里大小比较是重点,必须掌握。重点是底层原理,用谁不会用,关键你要会实现。
我们接下介绍string函数,直接从他的底层出发,编写一个string类,虽然在实际中不会让你直接重新写一个string,但是你可以不写,不可以不会写。本质是让你加强理解。注意理解为重,完全没有必要把这些像背书一样背下来。
模拟string类实现
string的越界检查
string类的越界检查的是,size而不是capacity,这是他的一个设计理念。对于正常的数组我们认为越界检查的是capacity,但是我们这里string检查越界是针对size而言的。
可以这么理解,这就是一种设计习惯;
size表示有效元素的个数,capa表示当前开辟的空间;
当size == capa的时候,我们才会进行扩容;
这样设计的好处就是,可以减少扩容次数;提高效率;
如果咱们只有一个size,他既作为元素个数,又是空间;
那么每次插入的时候,都需要扩容;
扩容是很消耗资源的;
所以设计两个,可以减少系统消耗;提高效率
成员变量
private:size_t _size;size_t _capacity;char* _str;//特殊的静态变量声明static size_t npos;
//初始化
size_t string::npos = -1;
这里其他三个成员变量没啥好说的,正常的顺序表变量。
npos静态变量:用于表示一个“无效”或“不存在”的位置。它通常用于字符串操作中,特别是在查找字符串时。
但是npos静态变量,很特殊,由于静态变量必须初始化,但是我们在类里面只能声明。所以,他只可以在类外初始化,
必须指定类型(size_t)和类名(string::),这是他的语法要求,大家先记住就行。
这里你可能会感到很奇怪,不要怀疑自己。他这个语法就是很奇怪,这也是c++难学的一点,语法很多,而且很难。这里介绍的是c++98的语法规定。
swap交换函数
这里先单独介绍下这个函数,这是c++独有的,std库里实现了它的多种重载形式,非常方便,复用性极高。所以我们先实现这个函数,方便后序函数编写。对于string的swap实现只有一种形式,就是string对象的交换。相信我,swap函数绝对是一个YYDS。
void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
这里我们模拟实现string的swap也是复用std库里的swap函数
默认成员函数
这里对于取地址操作符重载(const修饰),没有实现,这两个无关紧要,主要是下面其他四个的实现。
构造函数
string(const char* str = ""):_size(strlen(str)),_capacity(strlen(str)),_str(new char[_capacity + 1]){memcpy(_str, str, _size + 1);}
拷贝构造函数
//传统写法string(const string& s){_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}//现代写法 要重新初始化你的空间 否则delete的时候会出错//有一个bug 无法解决/0在中间时的拷贝//对于对象的输出 它的本质是个顺序表 size有多少就输出多少个字符 //只有在构造函数的时候我们规定 按照字符串的结束标志读取
/* string(const string& s):_str(nullptr),_size(0),_capacity(0){string tmp(s.c_str());swap(tmp);}*/
这里拷贝构造函数有两种实现思路,一种是传统写法,直接memcpy。一种是现代写法,复用构造函数,再使用swap函数。但是这种思路无法解决\0在字符串中间的拷贝情况。相比现代写法,传统写法就没啥bug。这也算现代写法的一个小bug吧。虽然没有人会在一段字符串写一个\0,但是少不了喜欢找bug的人。这里其实传统写法有一个注意点就是,记得要用memcpy,不要用strcpy。这也是一个出错点。
析构函数
~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}
赋值重载(注意这里是深拷贝)
//传统写法/* string& operator=(const string& s){if (&s != this){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}*/// 现代写法 交换值 所以对象不能是const修饰string& operator=(string s){swap(s);return *this;}
这里也有两个版本,现代写法和传统写法,传统写法就是手搓拷贝。现代写法就是复用swap函数,很简洁,但不过这里时值传入,不是引用传入。
对象操作函数
容量操作
const char* c_str() const{return _str;}size_t size() const{return _size;}void clear(){_str[0] = '\0';_size = 0;}
这里c_str是获得string对象的字符串形式,clear是清空有效数据,不是把容量也清除了。
//扩容
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//斜杠0也拷贝过去memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}
}
//调整有效数据个数
void resize(size_t n, char ch = '\0')
{//if (n < _size){_size = n;_str[_size] = '\0';}else{//合并考虑 只要超过_size直接扩容 反正n小于capacity 是不会扩容的reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;//惯例 多加一个空间 设置斜杠0_str[_size] = '\0';}
}
这里着重讲一下这两个函数,reserve函数是扩容,只改变capacity,resize改变的才是size。
对象操作
//尾插
void push_back(char ch)
{if (_size == _capacity){//2倍扩容reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;_str[_size] = '\0';
}
//尾插字符串
void append(const char* str)
{size_t len = strlen(str);if (len + _size > _capacity){reserve(len + _size);}memcpy(_str + _size, str, len + 1);_size += len;
}
//string里没有单独实现头插 处于效率考虑 但是insrt可以满足这个要求
//pos位置插入
void insert(size_t pos, size_t n, char ch)
{//等于就尾插 判断越界assert(pos <= _size);//扩容if (_size + n > _capacity){reserve(_size + n);}//斜杆0也拷贝进去了int end = _size;while (pos <= end && end != npos){_str[end + n] = _str[end];end--;}for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}//修改数据_size += n;
}
// 字符串版本
void insert(size_t pos, const char* str)
{//等于就尾插 判断越界assert(pos <= _size);size_t len = strlen(str);//扩容if (_size + len > _capacity){reserve(_size + len);}//斜杆0也拷贝进去了int end = _size;while (pos <= end && end != npos){_str[end + len] = _str[end];end--;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}//修改数据_size += len;
}
//删除
void erase(size_t pos, size_t len = npos)
{assert(pos <= _size);//hello worldif (len == npos || pos + len >= _size){_size = pos;_str[_size] = '\0';}else{size_t end = pos + len;//hello world//hellorld size 位置把\0拷贝进去了while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}
}
//查找
size_t find(char ch, size_t pos = 0)
{//不能超过下标assert(pos < _size);for (size_t i = pos; i < _size; i++){if (ch == _str[i]){return i;}}return npos;
}
size_t find(const char* str, size_t pos = 0)
{assert(pos < _size);char* ret = strstr(_str + pos, str);if (ret){return ret - _str;}return npos;
}
//截取
string substr(size_t pos, size_t len = npos)
{assert(pos < _size);//n 是长度size_t n = len;//hello worldif (len == npos || pos + len > _size){n = _size - pos; //当前下标字符也算}string tmp;tmp.reserve(n);for (size_t i = pos; i < n + pos; i++){tmp += _str[i];}return tmp;
}
这里find函数有两个版本,一个是查找字符,一个是查找字符串。这里的append函数是有点冗余的,我们一般不会用这个函数。更喜欢用+=运算符重载(下面会实现,底层就是append和push_back),java比较喜欢用append。
运算符重载
访问和操作
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
string& operator+=(char ch)
{push_back(ch);return *this;
}
string& operator+=(const char* str)
{append(str);return *this;
}
这里你会发现都是复用上面的函数,所以,这也是我们为啥还要学上面那些(冗余)的函数。因为,底层还是那些逻辑,只不过我们把它包装,增加了复用性。
比较大小
//比较大小
bool operator<(const string& s) const
{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s) const
{return _size == s._size && memcmp(_str, s._str, _size) == 0;
}
bool operator!=(const string& s) const
{return !(s == *this);
}
bool operator<=(const string& s) const
{return *this < s || *this == s;
}
bool operator>(const string& s) const
{return !(*this <= s);
}
bool operator>=(const string& s) const
{return *this > s || *this == s;
}
这里比较大小复用的是c语言的memcmp,大家不要直接自己完全手搓。c++的发展起初就是为了完善c语言的不足,只不过后来经过多数人的研究发展,变成了一们成熟的语言。我们如果能用到c语言函数,就考虑用一下。c++的初衷是为了壮大c语言,其底层很多逻辑还是c语言。c++代码的风格就是,c语言和c++混搭,不要感觉奇怪,习惯这种感觉。
流操作
流插入
ostream& operator<<(ostream& out, const que::string& s)
{for (size_t i = 0; i < s.size(); i++){cout << s[i];}return out;
}
这里不要申请成友元,会破坏封装性,我们使用[]运算符重载,就能很好解决这个问题。
流提取
//写入数据不用加const 本来就要修改对象
istream& operator>>(istream& in, que::string& s)
{//处理空格和换行//使用get函数 一次读取一个字符char ch = in.get();while (ch == ' ' || ch == '\n'){ch = in.get();}//清楚之前数据s.clear();//int i = 0;char buf[128];while (ch != ' ' && ch != '\n'){buf[i++] = ch;if (i == 127){buf[i] = '\0';s += buf;i = 0;}ch = in.get();}if (i != 0){buf[i] = '\0';s += buf;}}
这里流提取没那么好些。因为cin会把空格和换行当做结束符,并且这里的对象是我们自己定义的。所以,我们不能直接复用std::cin。我们需要,一个字符一个的读取,这样才能判断下一个字符是否时空格或换行,并且记得清除前导的空格和换行。这里实现还有一个重要的思路,就是效率提升,从外部设备读取内存是很慢的,这里还会设计扩容。因此,我们可以创建一个中间数组,先把字符读取到数组中,最后把数组里的内容赋值到对象里,这样就大大提高了效率。这里提高效率的主要是,减少了扩容的次数。这种思路,采取一个类似的中间tmp数组的形似,读取字符。编程中很多都有体现,c语言的File流就是这样设计的,包括以后学的池也是这种思路。学过java的应该深有体会,java的文件流和template都时这种思路。
源码
#pragma once
#include <stdio.h>
#include <iostream>
#include <assert.h>
using namespace std;namespace que
{class string{public://迭代器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 = ""):_capacity(strlen(str)),_size(strlen(str)),_str(new char[_capacity + 1]) //应该是capacity + 1这个1放的是\0{memcpy(_str, str, _size + 1);}//拷贝构造//传统写法 /* string(const string& s){_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_capacity = s._capacity;_size = s._size;}*///现代写法 要重新初始化你的空间 否则delete的时候会出错//有一个bug 无法解决/0在中间时的拷贝//对于对象的输出 它的本质是个顺序表 size有多少就输出多少个字符 //只有在构造函数的时候我们规定 按照字符串的结束标志读取string(const string& s):_str(nullptr),_capacity(0),_size(0){string tmp(s.c_str());swap(tmp);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//返回string的字符串const char* c_str() const{return _str;}size_t size() const{return _size;}//[] 运算符重载 注意越界 都是size_t 不存在负值char& operator[] (size_t pos){assert(pos < _size);return _str[pos];}const char& operator[] (size_t pos) const{assert(pos < _size);return _str[pos];}//扩容void reserve(size_t n){//只有扩大空间才会 进去 //如果是缩小空间 不做处理 if(n > _capacity){char* tmp = new char[n + 1];//斜杠0也拷贝过去memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}//调整size大小 存储的有效字符数量会变少//这个函数 有两个参数 第二个是要填充的字符 默认时\0void resize(size_t n,char ch = '\0'){//三种情况 //小于直接赋值更改if (n < _size){_size = n;_str[_size] = '\0';}//大于或等于 直接扩容 反正缩小是不会处理的else{//合并考虑reserve(n);//从size位置开始赋值for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}}//尾插 一个字符一个字符插入void push_back(char ch){//扩容if (_size == _capacity){//2倍扩容思路 不唯一reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = ch;//记得\0_str[_size] = '\0';}//尾插字符串void append(const char* str){//计算字符串的大小(不算\0)size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}memcpy(_str + _size, str, len+1);//strcpy(_str + _size, str);_size += len;}//本质还是尾插string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}//pos位置插入void insert(size_t pos, size_t n, char ch){assert(pos <= _size);if (_size + n > _capacity){reserve(_size + n);}//挪动数据size_t end = _size;//npos 表示-1(size_t) 当pos=0 解决死循环 当pos等于0的时候while (end >= pos && end != npos){_str[end + n] = _str[end];end--;}for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}_size += n;}void insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//挪动数据size_t end = _size;//npos 表示-1(size_t) 当pos=0 解决死循环while (end >= pos && end != npos){_str[end + len] = _str[end];end--;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}void erase(size_t pos, size_t len = npos){assert(pos <= _size);if (len == npos || pos + len >= _size){//_str[pos] = '\0';_size = pos;_str[_size] = '\0';}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}}size_t find(char ch, size_t pos = 0){assert(pos < _size);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){assert(pos < _size);char* ptr = strstr(_str+pos, str);if (ptr){return ptr - _str;}return npos;}string substr(size_t pos = 0,size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < n + pos; i++){tmp += _str[i];}return tmp;}void clear(){_str[0] = '\0';_size = 0;}bool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);return ret == 0 ? _size < s._size : ret < 0;/*int i1 = 0, i2 = 0;while (i1 < _size && i2 < s._size){if (_str[i1] < s._str[i2]){return true;}if (_str[i1] > s._str[i2]){return false;}else{i1++;i2++;}}*///三种情况//hello hello //hello helloxx//helloxx hello//return i1 == _size && i2 != s._size;//return _size < s._size;}bool operator==(const string& s) const{return _size == s._size &&memcmp(_str, s._str, _size) == 0;}bool operator<=(const string& s) const{return *this < s || *this == s;}bool operator>(const string& s) const{//bool ret = _str <= s._str; return !(*this <= s);}bool operator>=(const string& s) const{return *this > s || *this == s;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//传统写法//string& operator=(const string& s)//{// if (&s != this)// {// char* tmp = new char[s._capacity + 1];// memcpy(tmp, s._str, s._size + 1);// delete[] _str;// _str = tmp;// //// _size = s._size;// _capacity = s._capacity;// }// // return *this;//}//// 现代写法 交换值 所以对象不能是const修饰string& operator=(string s){ swap(s);return *this;}private:size_t _size;size_t _capacity;char* _str;static size_t npos;};size_t string::npos = -1;
}ostream& operator<<(ostream& out, const que::string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
istream& operator>>(istream& in, que::string& s)
{char ch = in.get();//处理缓冲区空格和换行while (ch == ' ' || ch == '\n'){ch = in.get();}char buf[128];s.clear();int i = 0;while (ch != ' ' && ch != '\n'){buf[i++] = ch;if (i == 127){buf[i] = '\0';s += buf;i = 0;}ch = in.get();}if (i != 0){buf[i] = '\0';s += buf;}return in;
}
总结
这里我们介绍string对象,主要是从他的设计理念和设计思路和函数使用以及易错点着重的,至于函数时如何具体实现,并不会讲的很具体。相信学到这里的友友吗,对于这点函数的具体实现应该是没啥问题的。