赞
踩
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
可以看出:
1 string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string类是basic_string类模板的一个实例化,使用char作为其字符类型。
4. 不能操作多字节或者变长字符的序列
使用string类的说明
1包含#include 头文件
2 使用using namespace std;
① 构造空的string类对象,即空字符串
②拷贝构造函数
③用C-string来构造string类对象
示例
void test1()
{
//构造空的string类
string str;
cout << str << endl;
//用字符串构造string类
string s("hello world");
cout << s << endl;
//拷贝构造
string ss(s);
cout << ss << endl;
}
析构函数会自动调用
可以使用string对象,字符串,以及字符进行赋值
void test10()
{
string s1 ("hello world");
string s2;
//string对象进行赋值
s2 = s1;
cout << s2 << endl;
//字符串进行赋值
s2 = "cppp";
cout << s2 << endl;
//字符进行赋值
s2 = 'c';
cout << s2 << endl;
}
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
reserve | 为字符串预留空间 |
resize | 为字符串预留空间并初始化 |
void test2() { string s1("hello world"); cout << s1.size() << endl; cout << s1.length() << endl; //size 和length都是返回字符串的长度 cout << s1.capacity() << endl; //返回空间总大小 string s2; s2.reserve(100); //开好100个字符空间的大小 string s3; s3.resize(10, 'c'); //开10个字符空间,并初始化为'c' }
注:当reserve的参数小于string的底层空间总大小(15)时,reserve不会改变容量大小。
函数名称 | 功能说明 |
---|---|
operator[ ] | 返回pos位置的字符 |
begin + end | begin返回起始位置,end返回结尾字符的下一个位置 |
rbegin + rend | rbegin返回反向起始位置,end返回反向结尾字符的下一个位置 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
注意:用operator[ ]访问string对象是用下标进行访问,类似于用下标访问数组的方式;而用begin,end,rbegin,rend对string对象进行访问,是通过迭代器进行。迭代器是为STL容器专门打造的一种机制,访问容器中的元素,需要通过迭代器进行,迭代器是像指针一样的类型,所以用法也和指针类似。通过迭代器就可以对它所指向的元素进行相关操作。
operator[ ]
因为operator[ ] 返回的是pos位置的引用,所以支持修改string对象
void test4() { string s1 = "hello world"; cout << s1[2] << endl; //将s1[2]位置的字符修改为'w’ s1[2] = 'w'; cout << s1[2] << endl; cout << s1 << endl; int i; for (i = 0; i < s1.size(); i++) { //对s1[i]位置的字符++ s1[i]++; } cout << s1 << endl; }
迭代器
正向迭代器
void test5()
{
string s1 = "hello world";
string::iterator it = s1.begin();
//begin()返回字符串的起始下标
while (it != s1.end())
{
//end()返回字符串最后一个元素的下一个位置,即'\0'
cout << *it ;
++it;
}
}
注意:iterator是迭代器的类型名,在使用时必须指明类域
反向迭代器
顾名思义:倒着遍历字符串
void test6()
{
//反向迭代器
string s1 = "hello world";
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit;
rit++;
}
cout << endl;
}
常量迭代器
常量迭代器不支持修改对象,一般用于常对象或者常对象做形参防止函数内部对对象进行修改
void PrintString(const string& s)//s为常对象 { //正向迭代 string::const_iterator cit = s.begin(); /*auto cit = s.begin();*///如果cit的类型太长,也可以使用auto关键字自动推导类型 while (cit != s.end()) { cout << *cit; cit++; } cout << endl; //反向迭代 string::const_reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit; rit++; } cout << endl; }
范围for
void test7()
{
string s1("hello world");
for (auto &ch : s1)
{
cout << ch;
ch++;
}
}
范围for在使用起来非常方便,它可以自动判断数组元素的类型和数组的大小,但是只能正向遍历
范围for的底层实际就是迭代器。
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符 |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
insert | 在pos位置插入字符或字符串 |
erase | 删除字符串中从pos位置开始并跨越len个字符的部分 |
push_back和 append
void test8()
{
string s1("hello,world");
//s1后面插入'x'
s1.push_back('x');
cout << s1 << endl;
//s1后面插入字符串"hi"
s1.append("hi");
cout << s1 << endl;
}
其中append还支持使用迭代器追加一段区间的字符串
void test8()
{
string s1("hello,world");
string s("this is a demo ");
//在字符串s的后面追加字符,范围是从[s1.begin() s1.end()]
s.append(s1.begin(), s1.end());
cout << s << endl;
//在字符串s的后面追加字符,范围是从[s1.begin()+3 s1.end()-3]
s.append(s1.begin() + 3, s1.end() - 3);
cout << s << endl;
}
operator+=
可以看出,operator+=支持追加字符,也支持追加字符串,还支持追加string类型的对象,所以平常使用的过程中,我们更倾向于使用operator+=来进行追加,其代码可读性也更强
void test9()
{
string s1("hello,world");
string s("!!!");
//追加字符'x'
s1 += 'x';
//追加字符串"你好"
s1 += "你好";
//追加string类型的对象s
s1 += s;
cout << s1 << endl;
}
insert
假设在字符串的每个空格位置前插入###,该如何实现呢?
void test_string1() { string s1("I am back"); for (int i = 0; i < s1.size();) { //遇到空格就进行插入 if (s1[i] == ' ') { s1.insert(i, "###"); //在插入完成后,要改变i,使i指向当前空格的下一个位置,才能继续插入 i += 4; } else { //没有遇到空格就往后++找空格 i++; } } cout << s1 << endl; }
补充:如果要将字符串的空格位置替换为###,又该如何实现呢?
构造一个新的字符串news,遍历s,没有遇到空格就把s的字符+=到news后面,遇到空格,+= ###到news的后面
oid test_string4() { string s("I am back"); string news; for (int i = 0; i < s.size();i++) { if (s[i] == ' ') { news+="###"; } else { news += s[i]; } } cout << news << endl; }
erase
void test_string2()
{
string s("I am back");
for (int i = 0; i < s.size(); i++)
{
if (s[i] == ' ')
{
//将空格删除
s.erase(i,1);
}
}
cout << s << endl;
}
函数名称 | 功能说明 |
---|---|
c_str | 返回c格式的字符串 |
find+npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
find ,rfind,substr
void test_string5() { string filename1("test.cpp"); //找文件名的后缀 //方法,find()函数找到"."的位置,substr()函数返回字符"."以后的字符串,即为后缀 size_t pos1= filename1.find('.'); if (pos1 != string::npos) { string str = filename1.substr(pos1); cout << str << endl; } string filename2("test.cpp.zip.tar"); //找文件名的真实后缀,即最后一个字符'.'对应的后缀 //这时就需要倒着找,使用rfind(); size_t pos2 = filename2.rfind('.'); if (pos2 != string::npos) { string ss = filename2.substr(pos2); cout << ss << endl; } }
注: string::npos参数 —— npos 是一个常数,表示无符号整型的最大值,用来表示不存在的位置
使用find+substr分割网址
void test_string6() { string url("https://cplusplus.com/reference/string/basic_string/basic_string/"); size_t pos1 = url.find("://"); if (pos1 == string::npos) { cout << "非法的url" << endl; return; } string protocol = url.substr(0,pos1); size_t pos2 = url.find('/', pos1 + 3); if (pos1 == string::npos) { cout << "非法的url" << endl; return; } string domain = url.substr(pos1 + 3, pos2 - pos1 - 3); string uri = url.substr(pos2+1); cout << protocol << endl; cout << domain << endl; cout << uri << endl; }
这些函数都是全局函数并没有实现在string类里
其它接口 | 功能说明 |
---|---|
字符串与其它类型进行转换 | stoi,stol,stof,stod等分别是字符串转整形,长整形,单精度浮点型,双精度浮点型 ;to_string是其它类型转为字符串 |
getline | 读取一行字符串,不会因为空格而结束 |
void test_string7() { int vali = 999; double vald = 999.88; //int转为字符串 string stri = to_string(vali); //double转为字符串 string strd = to_string(vald); cout << stri << endl; cout << strd << endl; stri = "888"; strd = "888.99"; //字符串转为int vali = stoi(stri); //字符串转为double vald = stod(strd); cout << vali << endl; cout << vald << endl; }
cin在输入字符串的时候,会以空格作为单词的分割,如果输入的字符串里面带有空格,则需要用getline
假设输入"hello world"
如果使用cin输入,则只会输出hello
namespace zbt { class string { public: string()//构造函数 { } ~string()//析构函数 { } private: char* _str; size_t _size;//指向有效字符的下一个 size_t _capacity;//区间容量 }; void test1() { ... } }
为了和C++库里的string区分开,我们自己定义一个命名空间,将自己实现的string类以及测试函数都放在里面
string(const char* str = "")//给一个缺省值,如果没有传参,默认为空值""
{
_size = strlen(str);//strlen算出有效字符的个数,赋值给_size
_capacity = _size;
_str = new char[_size + 1];//开空间时要多开一个,存储'\0'
strcpy(_str, str);//将str的内容拷贝到_str中
}
需注意,在初始化_str时,需动态开辟一个和str一样大的空间,再将str的值拷贝到_str中
~string()
{
delete[]_str;//delete[]和new[]对应
_str = nullptr;
_size = _capacity = 0;
}
char& operator[](size_t pos)
{
assert(pos < _size);//首先判断给的下标要小于_size
return _str[pos];//返回pos位置的值,传引用返回,支持修改string对象
}
const char& operator[](size_t pos)const//const类型,只读,不可修改
{
assert(pos < _size);
return _str[pos];
}
这里我们先实现正向迭代器,string的迭代器底层实际就是指针
typedef char* iterator;//将char*重名为iterator typedef const char* const_iterator;// const char*重名为const_iterator iterator begin()//返回字符串的起始下标,即为_str { return _str; } iterator end()//返回有效字符的下一个位置,即'\0' { return _str + _size;//因为是指针,所以起始位置+ _size即为'\0’的位置 } const_iterator begin()const//常量正向迭代器,加上const即可 { return _str; } const_iterator end()const { return _str + _size; }
这里就会涉及深浅拷贝问题,string的成员变量都为内置类型,如果不自己实现拷贝构造函数,那么编译器会自己默认生成一个浅拷贝的构造函数,假设用s1去拷贝构造s2,s1和s2中的成员变量_str便会指向同一块空间,在执行析构函数的时候,便会对同一块空间释放两次,程序会崩溃掉。深浅拷贝详细讲解请点击这里
那么拷贝构造函数应该实现的是深拷贝,即s1和s2中的成员变量_str指向两块不同的空间,两块空间上内容一样。即给每个对象独立分配资源,保证多个对象之间不会因为共享资源而导致多次释放
既然知道了string的拷贝构造函数是深拷贝,那么具体该如何实现呢?首先是要给s2开辟一块新空间,大小和s1一样,其中s2成员变量中的_size,_capacity应该和s1中的一样,最后再将s1中的字符串内容拷贝给s2。
//s2(s1) s为s1的别名
//传统版本
string(const string& s)
:_str(new char[s._capacity + 1])
, _size(s._size)
, _capacity(s._capacity)
{
strcpy(_str, s._str);
}
对于成员变量的初始化,采用的是初始化列表的方法,函数体内实现拷贝字符串内容。
这里是传统版本的写法,其代码的可读性高。还有一种版本是现代版本,其代码简洁,一起来看看吧
现代版本的核心思想是老板思维,要实现拷贝构造函数,不自己实现,而是去构造出一个临时对象tmp(可以理解为打工人),tmp中的内容和s1一样,最后将s2和tmp交换即可
void swap(string& tmp) { //函数里面调用的是库里面的swap函数实现成员变量的交换,并不是我们自己写的swap函数 ::swap(_str, tmp._str); ::swap(_size, tmp._size); ::swap(_capacity, tmp._capacity); } //s2(s1),s是s1的别名 string(const string& s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str);//调用构造函数,构造出临时对象tmp swap(tmp);//将s2和tmp的内容交换 }
需要对s2的成员变量_str初始化为空,因为当s2和tmp交换后,tmp即为原来的s2,出函数作用域的时候,tmp是局部变量,会调用tmp的析构函数释放tmp,如果_str没有初始化,那就是随机值,会出错。
(1) 原始写法
实现 s1=s2;函数里面s便是s2的别名
string& operator=(const string& s)
{
if (this != &s)//防止自己给自己赋值
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
首先需要开辟一块数组空间tmp,里面存储s2的内容,既然要将s1的内容改为s2,那么s1原有的内容就应该先释放掉,然后s1里面的内容为之前tmp保存的内容。size和capacity也应该和s2一致。
(2) 现代写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);//调用构造函数
// string tmp(s);//调用拷贝构造
swap(tmp);
}
return *this;
}
要实现运算符重载,不自己实现,构造一个tmp对象,里面的内容和s2一样,将s1和tmp进行交换即可。因为上文已经实现了拷贝构造函数,所以这里在构造tmp对象的时候也可以调用拷贝构造函数。
(3)更为简洁的现代写法
//s1=s2,s是s2的一份拷贝,这里让s做tmp完成交换
string& operator=(string s)
{
swap(s);
return *this;
}
这种写法需要注意,形参不能用引用,如果用引用的话,s是s2的别名,s和s1进行交换也就是s2和s1进行交换,会改变s2的值。
void push_back(char ch)//尾插一个字符
{
if (_size == _capacity)//满了就需要扩容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
因为_size指向的是有效字符的下一个位置,所以在下标为_size的位置直接插入ch即可,因为是字符串,所以插入完成后要在下一个位置要添上\0
在空间容量满的时候,需要扩容,再来看看扩容的实现吧
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
自己实现扩容,只能实现异地扩。即自己再新开一块空间,将原有的内容拷贝过去,_str指向新开辟的空间,_capacity更新为现有的容量,别忘了原来开辟的空间也要释放掉。
实现了reserve,再来看看和它很像的resize函数吧
resize是开空间并初始化
void resize(size_t n, char ch = '\0') { if (n > _size)//初始化字符的个数比原有的有效字符个数多 { reserve(n);//先调用reserve开好空间 int i; for ( i = _size; i < n; i++) { _str[i] = ch;//将_size之后的字符初始化为ch } _str[i] = '\0'; _size = n; } else//初始化字符的个数比原有的字符个数少 {//删除数据,不初始化 _str[n] = '\0'; _size = n; } }
void append(const char* s)//尾插一串字符串
{
size_t len = strlen(s);
if (_size + len > _capacity)//先计算要插入字符的个数,空间不够就继续扩容
{
reserve(_size + len);
}
strcpy(_str + _size, s);//将要插入的字符串s拷贝到原有字符串的后面
_size += len;//更新有效字符的个数
}
注意这里扩容不能像以前一样直接扩二倍,有可能插入的字符串很长,扩二倍容量还是不够,所以需要多少空间就扩多少。
void append(const string &s)//插入string类型的对象
{
append(s._str);//调用尾插字符串的函数
}
直接复用之前写好的尾插函数
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& operator+=(const string& str)
{
append(str);
return*this;
}
任意位置插入一个字符
string& insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity)//空间不够就扩容 { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while (end > pos)//把pos位置到结尾的字符统一向后移动一个单位长度 { _str[end] = _str[end - 1]; end--; } _str[pos] = ch;//ch放入pos位置 _size++; return *this; }
任意位置插入一串字符串
string& 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 + len; while (end>=pos+len)//pos位置至结尾的字符统一向后移动len个单位长度 { _str[end] = _str[end - len]; end--; } strncpy(_str + pos, str, len);//插入的字符串拷贝过来 _size = _size + len; return *this; }
扩容的时候同样也要注意,不能扩二倍,而是需要多少空间就扩多少
string& erase(size_t pos, size_t len = npos)
{
//从pos位置开始删除len个字符
if (len == npos || pos + len >= _size)//直接从pos位置删到结尾
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);//pos+len位置之后的字符拷贝到pos位置处
_size = _size - len;
}
return *this;
}
第一种情况,如果从pos位置之后要删除完,那么pos位置直接置为’\0’就好,第二种情况,没有删除完,那么pos+len位置之后的字符串要拷贝到pos位置处
size_t find(char ch, size_t pos = 0)const
{
//从pos位置开始查找一个字符
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if(_str[i]==ch)//如果找到了,返回下标
return i;
}
return npos;//没找到,返回npos
}
size_t find(const char* sub, size_t pos = 0)const
{
assert(sub);
assert(pos < _size);
char* ptr = strstr(_str + pos, sub);//调用strstr函数,看有没有匹配的子串
if (ptr == nullptr)
{
//ptr为空,说明匹配失败,返回npos
return npos;
}
else
return ptr - _str;//找到了,返回子字符串的起始下标
}
查找字符串的时候,调用的是c里面的strstr函数,看从pos位置往后的字符串里面有没有与sub匹配的字符串,有的话ptr就会指向找到的字符串,没有的话指向空。
string substr(size_t pos = 0, size_t len = npos)const { //从pos位置开始,返回len个字符 assert(pos < _size); size_t reallen = len;//记录返回字符的真实个数 if (len == npos || pos + len > _size) { reallen = _size - pos;//真实的个数为总的字符个数减去pos位置之前的个数 } string sub; for (int i = 0; i < reallen; i++) { sub += _str[pos + i]; } return sub; }
在返回子字符串的时候,我们是把从pos位置往后的len个字符+=到sub中,最后返回sub。在访问字符的时候,使用的是operator[ ],那么这就要求下标不能越界,但是有可能返回字符的个数len大于_size,或者从pos位置往后加上len个字符会大于_size,此时就要修改返回字符的真实个数。
bool operator>(const string s)const { return strcmp(_str, s._str) > 0; } bool operator==(const string s)const { return strcmp(_str, s._str) == 0; } 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); } bool operator!=(const string s)const { return !(*this == s); }
字符串之间的比较,直接调用strcmp函数即可,实现>和==,其他的复用。
ostream& operator<<(ostream& out, const string&s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
通过访问下标的方式,输出每一个字符
实现在string类的外面,但是并没有访问类里面的私有成员,所以也不用写为友元函数
void clear() { _str[0] = '\0'; _size = 0; } istream& operator>>(istream& in, string& s) { s.clear();//s本身有数据,还要输入,要把之前的数据清空再输入 char ch; ch = in.get(); const size_t N = 32; char buff[N]; int i = 0; //先把从键盘获取到的字符存储在buff数组里面 while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == N - 1)//buff数组满了 { buff[i] = '\0'; s += buff;//将获取到的一串字符串+=到s上 i = 0;//下标置为0,重复利用buff数组,继续存储接下来的字符 } ch = in.get(); } //遇到空格或者换行,一串字符获取结束,将buff数组剩下的字符+=到s上 buff[i] = '\0'; s += buff; return in; }
当输入的字符串很长的时候,便会频繁+=,不断扩容,效率变低。所以这里采用先把字符存储在buff数组里面,buff数组满了以后再+=,并且buff数组的空间可以重复使用,会提高效率。
使用get()函数读取字符是因为cin在输入的时候以空格或者换行作为字符串的分割,如果使用cin读字符,便不会获取到空格或者换行,便无法得知字符输入何时结束,而get函数是cin的一个成员函数,它可以读取任意一个字符 。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。