string是STL中的类,相比于C语言的字符数组和字符指针,string对字符串的处理方便很多。
文章目录
- 一、字符的一些补充
- 二、string类
- 2.1.string类的常见构造函数和析构函数
- 2.2.三种遍历方式
- 2.3.string常用的接口
- 三、string模拟实现
- 3.1.深浅拷贝的问题
- 3.2.命名空间
- 3.3.默认成员函数
- 构造函数
- 析构函数
- 重载赋值运算符=
- 3.4.元素访问函数
- []重载
- find和rfind
- c_str
- 3.5.容积函数
- size()和capacity()
- reserve
- resize
- empty
- clear
- 3.6.增删查改
- 用来交换对象中成员变量的swap函数
- insert
- push_back和append
- 重载+=
- erase
- 3.7.迭代器
- begin()和end()迭代器
- 3.8.非成员函数重载
- 重载输出<<
- 重载输入>>
- getline
- 3.9.关系运算符重载
一、字符的一些补充
众所周知英文字符等符号在计算机中采用ASCII码的方式储存。
但是对于中文等其他字符,ASCII码则无法表示(因为一个字节存不下),对于这些字符,则采用Unicode编码么标准:它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。当然Unicode标准下字符是两个字节,这样就能存下大部分字符了。
当然也有新的字符型wchar_t
来存储两个字节的字符。
二、string类
相比于C语言,string类型更符合面向对象的思想,它将string以及成员函数放在了一个头文件<string>
中,而库< iostream >
已经隐式地包含了<string>
头文件,所以无需显式引用该头文件也能够使用string类。这样更方便一些。
相比于C语言的字符数组或字符指针,string更安全一些,字符数组或字符指针很容易就会越界访问。
了解string类最好的方式就是查看官方文档。 string类
2.1.string类的常见构造函数和析构函数
在C++98中有以下七种构造函数:
其中比较重要的是默认构造、字符串构造和拷贝构造。
#include<iostream>
using namespace std;
int main()
{
string s1; //默认构造
string s2("hello"); //字符串构造
string s3(s2); //拷贝构造
string s4("hello", 1); //复制字符串前 n 个字符
string s5("hello", 1, 2); //从字符串第一个位置往后拷贝2个字符
string s6("hello", 1, string::npos); //npos是一个静态的全局变量为-1补码无穷大 超出长度则全部拷贝
string s7("hello", 1, 20); //同上
string s8(5, 'a'); //用n个字符填充字符串
string s9 = "hello"; //等价于字符串构造,编译器优化了隐式类型转换的过程
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
cout << s7 << endl;
cout << s8 << endl;
cout << s9 << endl;
s1 = s8; //重载=
cout << s1 << endl;
return 0;
}
析构函数则是在对象销毁时自动调用,销毁开辟的空间:
2.2.三种遍历方式
- 采用下标的方式
size()是一个公共成员函数,返回string的长度。和length()相同。
- 迭代器遍历
迭代器iterator是string中的类,所以要加域作用限定符。
begin()返回开始的下标位置的迭代器。
迭代器返回的是一个左闭右开的下标区间,所以end()返回的不是最后一个数据位置的迭代器,返回是最后一个位置下一个位置。
C++中凡是给迭代器一般都是给的左闭右开的区间。迭代器是类似指针一样的东西,它的用法就是模拟指针。
迭代器的意义是遍历更复杂的容器,比如list、map等。
反向迭代器:
反向迭代器++的时候是倒着往前走的。
const对象只能使用const迭代器遍历,const迭代器是只读:
iterator遍历普通对象是可读可写的,const_iterator 遍历const对象是只读的。
- 范围for
依次取容器中的数据赋值给e,自动判断结束。
范围for实现的本质是迭代器,因此支持其他容器的遍历。
2.3.string常用的接口
几个常用的函数:
- push_back(ch)
尾插一个字符ch - append(str)
在字符串后追加一个字符串str - +=str
在字符串后追加一个字符串str。重载了+=.。相比于append,实际中更常用+= - insert(pos,str) insert(pos,n,ch)
在下标为pos位置插入一个字符串str。或者插入n个字符ch。尽量少用insert,因为插入需要挪动数据。 - erase(pos,len)
在下标为pos后删除长度为len的字符,如果不写pos和len则默认从开始的位置向后全删除。 - resize(n,ch)
改变size的大小到n,capacity也会改变。多出来的size空间则用字符ch补全,如果不写ch则默认为\0
。 - reserve(n)
改变容量到大于n的大小,size并不会改变。 - c_str()
获取对象中字符串的首地址。 - find(ch/str,pos)和rfind(ch/str,pos)
从pos位置开始找一个字符或者字符串,pos不写默认给0。返回字符或字符串在源字符串的下标位置。
rfind从后往前找。 - substr(pos,len)
返回字符串从pos位置开始,长度为len的部分。
通过find和substr两个函数可以查找网址的域名和协议:
#include<iostream>
using namespace std;
string GetDomain(const string& url)
{
size_t pos = url.find("://");
if (pos != string::npos)
{
size_t start = pos + 3;
size_t end = url.find('/', start);
if (end != string::npos)
{
return url.substr(start, end - start);
}
else
{
return string();
}
}
else
{
return string();
}
}
string GetProtocol(const string& url)
{
size_t pos = url.find("://");
if (pos != string::npos)
{
return url.substr(0, pos - 0);
}
else
{
return string();
}
}
int main()
{
string url = "https://blog.csdn.net/qq_44918090/article/details/118532221";
cout << GetDomain(url) << endl;
cout << GetProtocol(url) << endl;
return 0;
}
- getline(cin,s)
往string对象s中输入一行(包括空格),遇到换行终止。 - reverse(s.begin(),s.end())
逆置s,传入开始和结束的迭代器。
三、string模拟实现
string本质上是一个管理字符串的顺序表(数组),字符串的结尾有\0
,所以采用顺序表的写法即可。
3.1.深浅拷贝的问题
string类模拟实现需要注意的问题就是深浅拷贝,所以拷贝构造函数应该采用深拷贝的方式,因为新的对象也需要开辟空间,如果采用浅拷贝就会导致它们指向同一块空间,在析构时会释放两次该空间。
如果是深拷贝:
另外,赋值运算符=
默认也是浅拷贝。
3.2.命名空间
如果不加命名空间和库实现隔离,则会和库里面的string重名导致无法调用,当然也可以给模拟实现的string改类名,这里采用命名空间的解决办法,命名空间起名为A,当然也可以起别的名字:
namespace A
{
class string
{
private:
char* _str;//字符指针,指向字符串
size_t _size;//记录字符串的长度
size_t _capacity;//记录字符串空间的大小
static const size_t npos;//表示字符串的最大长度,设置成静态常量
};
const size_t string::npos = -1;//在类外给静态常量赋值
}
3.3.默认成员函数
构造函数
//构造函数
string(const char* str = "")
::_str(new char[strlen(str) + 1])//要有\0,所以开辟的空间要比长度大
, _size(strlen(str))
, _capacity(_size + 1)//要有\0,所以空间要比长度大
{
strcpy(_str, str);//将str中的字符串拷到this中
}
//拷贝构造
string(const string& s)
:_str(new char[strlen(s._str) + 1])//因为有\0,所以开辟长度+1的空间
,_size(strlen(s._str))
,_capacity(_size+1)
{
strcpy(_str, s._str);//将s中的字符串拷到this中
}
//也可以采用这种写法
//string(const string& s)
// :_str(nullptr)
// ,_size(0)
// ,_capacity(0)//将this的字符指针置为空,长度、空间大小置为0
//{
// string tmp(s._str);//用默认构造创建一个临时对象,不能传s,否则会死循环
// swap(tmp);//将临时对象中的数据和this交换
//}
析构函数
~string()
{
//_size和_capacity不需要置零,因为对象生命周期结束这俩成员变量就没了
delete[]_str;
_str = nullptr;
}
重载赋值运算符=
//因为是值传递,所以会出现自己给自己赋值的情况,字符指针指向的字符串地址会发生改变
string& operator=(string s)//因为是值传递,在传参时会拷贝 构造一个临时对象s
{
swap(s);//交换s和this
return *this;
}
//也可以采用这种写法
/*string& operator=(const string& s)
{
if (this != &s) //排除自己给自己赋值的情况
{
delete[]_str;//销毁原来的字符指针_str指向的空间
_str = new char[strlen(s._str) + 1];//重新给_str开辟一块空间
strcpy(_str, s._str);//将s中字符串拷贝到_str指向的空间中
}
return *this;
}*/
3.4.元素访问函数
[]重载
//[]重载
//重载两个,一个普通对象,一个const对象
char& operator[](size_t i)//返回引用可以对成员变量进行修改
{
assert(i < _size);//防止越界
return _str[i];
}
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
find和rfind
//查找一个字符和字符串
size_t find(char ch, size_t pos = 0)const
{
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)const
{
assert(pos < _size);
const char* ret = strstr(_str+pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
//反向查找字符或字符串
//将原字符串逆置然后复用find函数
size_t rfind(char ch, size_t pos = npos)
{
string tmp(*this);
reverse(tmp.begin(), tmp.end()); //逆置tmp
//如果pos大于字符串长度,则将pos改为字符串最后一个位置的下标
if (pos >= _size)
{
pos = _size - 1;
}
//逆置以后,pos的位置要发生改变
pos = _size - 1 - pos;
size_t ret = tmp.find(ch, pos); //复用find函数
if (ret != npos)
{
return _size - 1 - ret; //返回ret镜像对称后的位置
}
else
{
return npos;
}
}
size_t rfind(const char* str, size_t pos = npos)
{
string tmp(*this);
size_t len = strlen(str); //待查找的字符串的长度
//拷贝并逆置str
char* arr = new char[len + 1];
strcpy(arr, str);
int left = 0, right = len - 1;
while (left < right)
{
::swap(arr[left], arr[right]);
left++;
right--;
}
reverse(tmp.begin(), tmp.end()); //逆置tmp
//如果pos大于字符串长度,则将pos改为字符串最后一个位置的下标
if (pos >= _size)
{
pos = _size - 1;
}
//逆置以后,pos的位置要发生改变
pos = _size - 1 - pos;
size_t ret = tmp.find(arr, pos); //复用find函数
delete[] arr; //销毁arr
if (ret != npos)
{
return _size - ret - len; //返回ret镜像对称后再调整的位置
}
else
{
return npos;
}
}
c_str
char* c_str()const
{
return _str;
}
3.5.容积函数
size()和capacity()
size_t size()const//加const的好处是普通对象和const对象都可以调用
{
return strlen(_str);
}
size_t capacity()
{
return _capacity;
}
reserve
void reserve(size_t n)
{
if (n > _capacity)//如果需要开的空间比实际的小就不用开了
{
char* tmp = new char[n+1];
strncpy(tmp, _str,_size+1);//将\0也要拷进去
delete[]_str;//销毁原来的空间
_str = tmp;//指向新开的空间
_capacity = n+1;
}
}
resize
//开空间+初始化,扩展capacity 并且初始化空间,size也要改变
void resize(size_t n, char val = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';//n比实际的size小的时候直接将下标为n的地方改为\0即可
}
else
{
// 如果newSize大于底层空间大小,则需要重新开辟空间
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, val, n - _size);
_size = n;
_str[n] = '\0';
}
}
empty
bool empty()
{
return strcmp(_str, "") == 0;
}
clear
void clear()
{
_size = 0;
_str[0] = '\0';
}
3.6.增删查改
用来交换对象中成员变量的swap函数
void swap(string& s)
{
//加域作用限定符::是为了调用std中的swap函数,如果不加则是自己调用自己死循环
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
这个函数的目的是为了交换对象中的成员变量,在接下来其他函数的实现中被调用。
insert
//下标为pos位置插入字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size +len > _capacity)
{
reserve(_size + len);
}
//挪字符串
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + len) = *end;
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
//下标为pos位置插入n个字符
string& insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if ((_size+n) >= _capacity)
{
reserve(_capacity == 0 ? (_size + n) : (_size + n) * 2);
}
char* end = _str+_size;
while (end >= _str + pos)
{
*(end + n) = *end;
end--;
}
memset(_str + pos, ch, n);
_size+=n;
return *this;
}
push_back和append
void push_back(char ch)
{
//方法一
/*if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);//空间不够扩到两倍
}
_str[_size] = ch;
_str[_size + 1] = '\0';
_size++;*/
//方法二、直接调用insert
insert(_size,1, ch);
}
void append(const char* str)
{
//方法一
/*size_t len = _size + strlen(str);//计算插入后的长度
if (len >= _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);//从_str结束位置开始拷贝
_size = len; */
//方法二、直接调用insert
insert(_size, str);
}
重载+=
//复用尾插函数即可
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
erase
//删除
string& erase(size_t pos,size_t len=npos)
{
assert(pos < _size);
size_t leftlen = _size - pos;
if (len >= leftlen)//pos后的长度比要删的长度小
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);//删除后剩余的字符移到前面
_size -= len;
}
return *this;
}
3.7.迭代器
begin()和end()迭代器
iterator begin()
{
return _str;
}
const_iterator begin()const
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator end()const
{
return _str + _size;
}
3.8.非成员函数重载
重载输出<<
ostream& operator<<(ostream& out,const string& s)
{
//也可以使用下标遍历
/*for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}*/
//使用范围for遍历
for (auto ch : s)
{
out << ch;
}
return out;
}
重载输入>>
istream& operator>>(istream& in, string& s)
{
s.clear();//清空s里的数据
char ch;
ch=in.get();//连续输入
while (ch != ' ' && ch != '\n')//遇到空格或换行结束
{
s += ch;
ch = in.get();
}
return in;
}
getline
istream& getline(istream& in, string& s)
{
s.clear();//清空s里的数据
char ch;
ch = in.get();//连续输入
while (ch != '\n')//遇到换行结束
{
s += ch;
ch = in.get();
}
return in;
}
3.9.关系运算符重载
参考日期类的关系运算符重载,>、>=、<、<=、==、!=
只需要实现出两个,其他直接复用这两个即可。
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 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);
}