C++:String
为什么学习string类?
C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问
标准库中的string类
string类(了解)
https://cplusplus.com/reference/string/string/?kw=string
在使用string类时,必须包含#include头文件以及using namespace std;
string类的常用接口说明
string类对象的常见构造
| 函数名称 | 功能说明 |
| string()(重要) | 构造空的string类对象,即空字符串 |
| string(const char* s)(重要) | 用c-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string&s)(重要) | 拷贝构造函数 |
范例:
void String()
{
string str; //构造空的对象
string str2("Hello World"); //用c格式的字符串构造string类对象
string str3(str2); //拷贝构造
string str4(str2, 1, 6);
cout << str << endl;
cout << str2 << endl;
cout << str3 << endl;
cout << str4 << endl;
}
int main()
{
String();
return 0;
}

string类对象的容量操作
| 函数名称 | 功能说明 |
| size(重要) | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty(重要) | 检测字符串释放为空串,是返回true,否则返回false |
| clear(重要) | 清空有效字符 |
| reverve(重要) | 为字符串预留空间** |
| resize(重要) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
size()和length()的底层逻辑几乎一样,引入size()的原因是要和其他的接口保持一致,正常情况下都是用size()
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,resverse不会改变容量大小
string类对象的访问及遍历操作
| 函数名称 | 功能说明 |
| operator[](重要) | 返回pos位置的字符,const string类对象调用 |
| begin+end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| rbegin+rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| 范围for | C++11支持更简洁的范围for的新遍历方式 |
补充:
iterator和const_iterator
这俩都是迭代器,一个可以修改指向内容的数据一个无法对指向的内容进行修改,通常搭配begin和end使用
代码示例:
void String()
{
string s1("123456");
const string s2("Hello World");
string::iterator str = s1.begin();
while (str != s1.end())
{
(*str)--;
++str;
}
cout << s1 << endl;
string::const_iterator str2 = s2.begin();
while (str2 != s2.end())
{
//(*str2)--; //不能修改
++str2;
}
cout << s2 << endl;
}
如果对const_iterator修改则会报错


运行结果:

除了上述两个外还有reverse_iterator和const_reverse_iterator,这俩和iterator和const_iterator相似,不过这俩是倒过来读取的
代码范例:
void String()
{
string s1("123456");
const string s2("Hello World");
string::reverse_iterator str3 = s1.rbegin();
while (str3 != s1.rend())
{
cout << *str3 << " ";
++str3;
}
cout << endl;
string::const_reverse_iterator str4 = s2.rbegin();
while (str4 != s2.rend())
{
cout << *str4 << " ";
++str4;
}
cout << endl;
}
运行结果:

string类对象的修改操作
| 函数名称 | 功能说明 |
| push_back | 在字符串后尾插字符c |
| appned | 在字符串后追加一个字符串 |
| operator+=(重要) | 在字符串后追加字符串str |
| c_str(重要) | 返回C格式字符串 |
| find+npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
| rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
| substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
注意:
在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串
对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好
auto和范围for
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
在C++中可以使用auto来进行类型推导
void String2()
{
auto x = 20;
auto z = 23.4;
cout << x << endl;
cout << z << endl;
}
int main()
{
String2();
return 0;
}

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加
当auto要声明引用类型时要加上引用操作符(“&”)
void String2()
{
int& y = x;
auto& i = y;
i++;
}
我们来进行调试:
此时x为20,我们接着往下走

y也变成了20,继续往下

i也变成了20,接着往下走

再往下走后就变成了21
那么如果auto后不加引用会发生什么事?

此时的i是y的拷贝,并非引用
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组

范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
范围for可以作用到数组和容器对象上进行遍历
int array[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
cout << array[i] << endl;
}
for (auto a : array)
{
cout << a << " ";
}
cout << endl;
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到
string模拟实现
初始化
这里我们写上const的和非const版本
namespace MyString
{
String::String()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity(0)
{}
String::String(const char* str)
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
memcpy(_str, str);
}
}
取得字符串
namespace MyString
{
const char* String::c_str() const
{
return _str;
}
}
我们来运行看看
运行结果:
取得字符串中的长度
namespace MyString
{
size_t String:: size()
{
return _size;
}
}
这里我们要将字符串中的每个字符都往后+1
运行结果:

下标操作符重载
这里也写上const和非const的版本,这里需要使用迭代器
namespace MyString
{
char& String::operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& String::operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
}
这里补充一些东西,这里遍历时无法使用迭代器,因为自定义类型并没有调用库中的东西
所以这我们begin和end要自己实现
这要先将char*进行重命名
typedef char* iterator;
我们知道begin是要找到字符串中的第一个字符,end则是找到最后一个字符
namespace MyString
{
String::iterator String::begin()
{
return _str;
}
String::iterator String::end()
{
return _str + _size;
}
}
运行结果:
尾插
在写尾插之前我们先模拟实现一个函数“reserve”,“resever”是用于分配内存空间的一个函数,它可以避免内存多次进行分配,有效提升元素插入时的运行效率
现在开始写代码:
void String::reserve(size_t n)
{
if (n > _capacity)
{
char* str = new char[n + 1];
memcpy(str, _str, _size + 1);
delete[] _str;
_str = str;
_capacity = n;
}
}
接下来继续写尾插代码
void String::push_back(char str)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = str;
++str;
_str[_size] = '\0';
}
这里写的和之前的不同,这里多加了一个‘\0’
运行结果:
追加字符串
append是用于追加字符串的函数,可以追加任何形式的字符而不要进行重新分配,接下来进行模拟实现
void String::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newcapacity);
}
//strcpy(_str + _size, str);
memcpy(_str, str, len + 1); //这里可以使用strcpy,但是使用memcpy会更好
_size += len;
}
运行结果:

重载函数”+=“:
这里写const和非const版本
String& String::operator+=(char ch)
{
push_back(ch);
return *this;
}
String& String::operator+=(const char* str)
{
append(str);
return *this;
}
这里和尾插和追加字符串相似,当我们想要追加字符和字符串可以使用这个符号来代替
运行结果:
流输出符号重载
这个需要在类外面进行实现,需要使用友元才能使用私有成员,不过这并不需要访问类的私有成员
ostream& operator<<(ostream& out, const string& s)
{
//out << s.c_str(); //如果使用这个会有”坑“
for (size_t i = 0; i < s.size();i++)
{
out << s[i];
}
return out;
}
我们来进行调试:
首先走到断点处

往下走,此时s1的值变为“hello world”

我们看到终端画面

这样就是输出成功,调试结束
指定位置插入字符
string& string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
/*int end = _size;
while(end>= (int) pos) //size_t是无符号变量,end会从int变成unsigned int。这称为整型提升,因此这里要强转
{
_str[end + 1] = _str[end];
--end;
}*/
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
指定位置插入字符串版本
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
/*int end = _size;
while(end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}*/
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
return *this;
}
运行结果:

删除指定字符
在写这个代码前需要写npos的定义
const size_t string::npos = -1;//定义
继续写erase的代码
string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
//要删除的数据,要大于pos后面的字符个数
if (len == npos || len >= (_size - pos))
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t i = pos + len;
memmove(_str + pos, _str + i, _size + 1 - i);
_size -= len;
}
return *this;
}
运行结果:
寻找指定字符
size_t string::find(char ch, size_t pos)const //寻找指定字符
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)const //寻找指定字符串和下一个字符的起始位置
{
const char* p1 = strstr(_str + pos, str);
if (p1 == nullptr)
{
return npos;
}
else
{
return p1 - _str;
}
}
这里还需要写上substr
substr是用于提取字符串中提取子字符串的函数
代码:
string string::substr(size_t pos, size_t len)const
{
if (len == npos || len >= _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; i++)
{
ret += _str[pos + i];
}
return ret;
}
这里find和substr要搭配使用
代码:
void spliturl(const string& url)
{
size_t i1 = url.find(':');
if (i1 != string::npos)
{
cout << url.substr(0, i1) << endl;
}
size_t i2 = i1 + 3;
size_t i3 = url.find('/', i2);
if (i3 != string::npos)
{
cout << url.substr(i2, i3 - i2) << endl;
cout << url.substr(i3 + 1) << endl;
}
cout << endl;
}
运行结果:

大于、大于等于、小于、小于等于、等于和不等于符号重载
bool string::operator<(const string& s)const //大于
{
size_t s1 = 0, s2 = 0;
while (s1 < _size && s2 < s._size)
{
if (_str[s1] < s[s2])
{
return true;
}
else if(_str[s1] > s[s2])
{
return false;
}
else
{
s1++;
s2++;
}
}
return s2 < s._size;
}
bool string::operator<=(const string& s)const//大于等于
{
return *this < s || *this == s;
}
bool string::operator>(const string& s)const//小于
{
return !(*this <= s);
}
bool string::operator>=(const string& s)const//小于等于
{
return !(*this < s);
}
bool string::operator==(const string& s)const//等于
{
size_t s1 = 0, s2 = 0;
while (s1 < _size && s2 < s._size)
{
if (_str[s1] != s[s2])
{
return false;
}
else
{
s1++;
s2++;
}
}
return s1 == _size && s2 ==s._size;
}
bool string::operator!=(const string& s)const//不等于
{
return !(*this == s);
}
这里我们以大于来做测试,这里如果是大于返回0否则返回1,最后的输出结果为:0、0、1
运行结果:

流输入符号重载
在写这个之前我们需要写一个clear的模拟实现,如果没写会出现bug
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
接下来继续写写上流输入符号重载的代码
istream& operator>>(istream& in, string& s)
{
s.clear();
char buff[128];
int i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += i;
i = 0;
}
s += ch;
ch = in.get();
}
if (i < 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
我们来进行调试:
先走到断点处

我们往下走并输入“Hello”和“world”

之后看到监视窗口,此时s1和s2分别为“hello”和“world”

调试结束
从流中提取字符串
istream& getline(istream& is, string& str, char delim)
{
str.clear();
char ch = is.get();
while (ch != delim)
{
str += ch;
ch = is.get();
}
return is;
}
运行结果:

拷贝构造函数
拷贝构造函数分为传统写法和现代写法,这里先写上传统写法
传统写法:
string::string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str,s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
现代写法:
在写现代写法前要先写swap代码
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
接下来写上现代写法的拷贝构造函数
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
赋值符号重载
赋值符号重载也分为两种写法,一个是传统写法一个是现代写法,先来写传统写法
传统写法:
string& string::operator=(const string& s)//赋值操作符重载(传统)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, _size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法:
string& string::operator=(const string& s)//赋值操作符重载(现代)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
现代写法还有更简洁的写法
string& string::operator=(string& tmp) //赋值操作符重载(现代)
{
swap(tmp);
return *this;
}更多推荐

所有评论(0)