<C++>详解string类
文章目录
- 1. STL简介
- 2. 标准库里的string类
- 3. string类的常用接口说明
- 3.1 string类对象的常见构造
- 3.2 string类对象的容量操作
- 3.3 string类对象的访问及遍历操作
- 3.4 string类对象的修改操作
- 3.5 string类非成员函数
- 3. 深浅拷贝
- 3.1 浅拷贝
- 3.2 深拷贝
- 3.3 传统写法、现代写法的string类对比
- 3.4 拓展:写实拷贝
- 4. string类模拟实现
1. STL简介
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架
今天我们讲的是容器中的string
之后的博客会围绕这六大组件进行讲解
2. 标准库里的string类
标准库类型string表示可变长的字符序列
string类的文档介绍
总结:
-
string是表示字符串的字符串类
-
该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
-
string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
-
不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include <string>
以及using namespace std;
3. string类的常用接口说明
3.1 string类对象的常见构造
cplusplus网站上的string说明
void test1()
{
// 初始化
//std::string s;
// string();
string s1;// 构造空的string类对象s1
// string (const char* s);
string s2("hello world");// 用C格式字符串构造string类对象s2(C格式指传的字符串默认以\0结束)
// 拷贝构造
// string (const string& str);
string s3(s2);// 拷贝构造s3
string s4 = s2;// 拷贝构造s4
// string (const char* s, size_t n);
string s5("https://www.baidu.com", 6);// 输出https: // 从头开始到第6个字符
cout << s5 << endl;
// string (size_t n, char c);
string s6(10, 'x');// xxxxxxxxxx
cout << s6 << endl;
// string (const string& str, size_t pos, size_t len = npos);
string s7(s2, 6, 3);// wor
cout << s7 << endl;
// 缺省参数len默认的npos是size_t类型的最大值,这里的作用是默认输出pos位置后的所有字符
string s8(s2, 6);// world
cout << s8 << endl;
}
3.2 string类对象的容量操作
一般用于扩容
清理
void test6()
{
//string s("hello world");
//cout << s.length() << endl;// 返回字符串有效字符长度
//cout << s.size() << endl;// size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
//cout << s.max_size() << endl;// 整型最大值
//cout << s.capacity() << endl;// 容量大小
// reserve和resize
//string s;
s.reserve(1000);// reserve提前开够空间,提高插入数据的效率,避免增容带来的开销
s.resize(1000)// resize:括空间+初始化。capacity变,size也变
//s.resize(1000, 'x');// 先把前1000个空间存x,再执行以下程序
//size_t sz = s.capacity();
//cout << "making s grow:\n";
//for (int i = 0; i < 1000; ++i)
//{
// s.push_back('c');
// if (sz != s.capacity())
// {
// sz = s.capacity();
// cout << "capacity changed: " << sz << '\n';
// }
//}
string s1("hello");
//s1.reserve(100);// capacity变成100+
s1.resize(100);// capacity变成100+,size变成100(hello\0\0\0\0……共95个\0)
// vs下它们都不会缩容
s1.reserve(10);// capacity不变,size不变
s1.resize(10);// capacity不变,size会变成10,之后使用会覆盖数据
}
总结:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
3.3 string类对象的访问及遍历操作
遍历方法:
- 下标+[]
- 迭代器
- 范围for
void test1()
{
string s1("hello");
const string s2("hello");
// at和[]功能类似
// 区别在于越界时
//s1[6];// 断言报错
//s2[6];
//s1.at(6);// 抛异常
//s2.at(6);
s1[0] = 'x';
//s2[0] = 'x'; 代码编译失败,因为const类型对象不能修改
s1.at(0) = 'y';
}
void test2()
{
// 遍历string的每个字符
string s1("hello");
// 方法一:[下标]
// char& operator[] (size_t pos);// 这里的引用返回是为了支持修改返回对象
// const char& operator[] (size_t pos) const;
for (size_t i = 0; i < s1.size(); i++)
{
// s1.operator[](i)
cout << s1[i] << " ";
}
cout << endl;
// 方法二:迭代器(跟指针挺像)
string::iterator it = s1.begin();
while (it != s1.end())//end()指向最后一个数据的下一个位置(在这指向\0)
{
cout << *it << " ";
++it;
}
cout << endl;
// 方法三:范围for(本质上是迭代器)
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
迭代器分四种:正向迭代器、反向迭代器、const正向迭代器、const反向迭代器
void func(const string& s)
{
string::const_iterator it = s.begin();// 可读不可写
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//string::const_reverse_iterator rit = s.rbegin();// 可读不可写
auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
++rit;// 变了方向,还是++
}
cout << endl;
}
void test5()
{
// 迭代器
string s1("hello");
// 正向迭代器
string::iterator it = s1.begin();// 可读可写
while (it != s1.end())//end()指向最后一个数据的下一个位置(在这指向\0)
{
cout << *it << " ";
//(*it) += 1;// 可写
++it;
}
cout << endl;
// 反向迭代器
string::reverse_iterator rit = s1.rbegin();// 可读可写
while (rit != s1.rend())//end()指向最后一个数据的下一个位置(在这指向\0)
{
cout << *rit << " ";
++rit;// 变了方向,还是++
}
cout << endl;
func(s1);
}
3.4 string类对象的修改操作
尾插单个字符
尾插字符串
任意位置插入
删除
转换成c类型字符串
查找
倒着找
取字符串
// 增删
void test()
{
//string s;
尾插
//s.push_back('x');// 尾插字符
//s.append("hello");// 尾插字符串
//string str("world");
//s.append(str);
//s.push_back('x');
//cout << s << endl;
+=是尾插最方便的
//s += 'x';
//s += "hello";
//s += str;
//cout << s << endl;
头插
//string s1("hello");
//s1.insert(0, 1, 'x');
//s1.insert(s1.begin(), 'y');
//cout << s1 << endl;
//s1.insert(0, "sort ");
//cout << s1 << endl;
任意位置插入
//s1.insert(3, 1, 'x');
//s1.insert(s1.begin()+3, 'y');
//cout << s1 << endl;
// 删除
string s("hello world");
cout << s << endl;
s.erase(s.begin());// 头删
cout << s << endl;
s.erase(s.begin()+4);// 删第四个位置
cout << s << endl;
s.erase(3, 2);// 删第三个位置开始的两个,即第四第五个
cout << s << endl;
s.erase(3);// 删第三个位置后的所有字符
cout << s << endl;
}
// 找,取
void test7()
{
string s1("hello world");
string s2("string");
// C++98
s1.swap(s2);// 效率高。交换指针指向
swap(s1, s2);// 效率低。深拷贝交换
cout << s1 << endl;
cout << s1.c_str() << endl;
// 要求取出文件的后缀
string file("string.cpp.zip");
size_t pos = file.find('.');
//size_t pos = file.rfind('.');// 倒着找
if (pos != string::npos)
{
//string suffix = file.substr(pos, file.size() - pos);
string suffix = file.substr(pos);// 取pos位置后的字符串
cout << file << "后缀:" << suffix << endl;
}
else
{
cout << "没有后缀";
}
string url1("https://cplusplus.com/reference/string/string/rfind/");
string url2("https://www.baidu.com/");
string& url = url1;
// 协议 域名 uri
string protocol;
size_t pos1 = url.find("://");
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
cout << "protocol:" << protocol << endl;
}
else
{
cout << "非法url" << endl;
}
string domain;
size_t pos2 = url.find('/', pos1 + 3);// 从pos1+3的位置开始找
if (pos2 != string::npos)
{
domain = url.substr(pos1+3, pos2-(pos1+3));
cout << "domain:" << domain << endl;
}
else
{
cout << "非法url" << endl;
}
string uri = url.substr(pos2 + 1);
cout << "uri:" << uri << endl;
}
3.5 string类非成员函数
+是传值返回,要少用
输出
输入
>>
遇到空格或换行就停止
getline遇到换行才停止
3. 深浅拷贝
3.1 浅拷贝
默认生成的拷贝构造函数,就只能实现浅拷贝。
浅拷贝的危害:
-
指向同一块空间,修改数据会互相影响
-
析构时这块空间会被释放两次,程序崩溃
3.2 深拷贝
我们要自己实现深拷贝。
再开一块空间,安置s2
3.3 传统写法、现代写法的string类对比
class string
{
public:
string(const char* str = "")
{
if (nullptr == str)
str = "";
str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// s2(s1)
// 传统写法:自己干活
/*string(const string& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}*/
// 现代写法:让别人干活,然后窃取别人的劳动成果
string(const string& s)
: _str(nullptr)
{
string tmp(s._str);// tmp出函数之后会被析构,要先让_str置空,否则交换之后无法释放空间
swap(_str, tmp._str);
}
// 传统写法:自己干活
/*string& operator=(const string& s)
{
if (this != &s)
{
char* pStr = new char[strlen(s._str) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}*/
// 现代写法一:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
// 这里能直接析构,因为swap之后tmp是一定能直接释放的
/*
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(_str, tmp._str);
}
return *this;
}
*/
// 现代写法2:在现代写法1的基础上,加了剥削要从娃娃抓起的思想(doge)
// 传值传参,暗中调用拷贝构造,之后直接窃取成果
// 自己给自己赋值也没事,因为调用时已经深拷贝了,木已成舟
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
3.4 拓展:写实拷贝
为了减少深拷贝后又不使用的现象,造成的深拷贝资源浪费。
我们引入了写实拷贝:本质是浅拷贝+引用计数,解决析构问题;使用的时候再深拷贝,解决使用问题(其实还得深拷贝,只不过在赌你不用它)
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源 。有多少个使用者用就记多少,没人用再彻底析构,释放资源
4. string类模拟实现
库里面是用class string,为了区分,我们能把string写成大写String;或者增加一层命名空间,在命名空间里写string,不过在用的时候要记得调用命名空间里的string类
以下用的是第一种写法class String
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
// 先实现一个简单的string,只考虑资源管理深浅拷贝的问题
// 暂且不考虑增删查改
//class String
//{
//public:
// String(const char* str)
// : _str(new char[strlen(str)+1])// \0 // 在堆上开辟一段空间,把字符串拷贝过去,在堆上实现后续操作
// {
// strcpy(_str, str);
// }
//
// // s2(s1)
// String(const String& s)
// : _str(new char[strlen(s._str) + 1])
// {
// strcpy(_str, s._str);
// }
//
// // 不能直接拷贝的原因:空间不够大/空间浪费
// // s1 = s3;
// String& operator=(const String& s)
// {
// if (&s != this)// 防止自己给自己赋值:先把自己杀了再复活自己导致的随机值;或浪费资源
// {
// //delete[] _str;
// //_str = new char[strlen(s._str) + 1];
// //strcpy(_str, s._str);
//
// // 防止new失败,_str已经没了
// char* tmp = new char[strlen(s._str) + 1];
// strcpy(tmp, s._str);
// delete[] _str;
// _str = tmp;
// }
// return *this;
// }
//
// ~String()
// {
// if (_str)
// {
// delete[] _str;
// }
// }
//
// // const char* c_str(const string* const this)
// const char* c_str() const
// {
// return _str;
// }
//
// char& operator[](size_t pos)
// {
// assert(pos < strlen(_str) + 1);// 要加一吗?我觉得要加一
// return _str[pos];
// }
//
// size_t size()
// {
// return strlen(_str);
// }
//
//private:
// char* _str;
//};
// 考虑增删查改和使用
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;
}
void swap(String& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//String()
// : _size(0)
// , _capacity(0)
//{
// _str = new char[1];
// _str[0] = '\0';
//}
// 提供全缺省
// "\0"("\0\0") ""("\0") '\0'(字符,ascii值是0,把0给str,错)
String(const char* str = "")// 空字符串,默认常量字符串
: _size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];// 初始化列表的初始化顺序是按照声明顺序的
strcpy(_str, str);
}
// s2(s1)
// 传统写法:自己干活
/*String(const String& s)
: _size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}*/
// 现代写法:让别人干活,然后窃取别人的劳动成果
String(const String& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
String tmp(s._str);// tmp出函数之后会被析构,要先让_str置空,否则交换之后无法释放空间
swap(tmp);
}
不能直接拷贝目标内容的原因:原空间不够大/空间浪费
s1 = s3;
// 传统写法:自己干活
//String& operator=(const String& s)
//{
// if (&s != this)
// {
// //delete[] _str;
// //_str = new char[strlen(s._str) + 1];
// //strcpy(_str, s._str);
// // 防止new失败,_str已经没了
// char* tmp = new char[s._capacity + 1];
// strcpy(tmp, s._str);
// delete[] _str;
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
// 现代写法1:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
// 这里能直接析构,因为swap之后tmp是一定能直接释放的
s1 = s3;
/*String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s._str);
swap(tmp);
}
return *this;
}*/
// 现代写法2:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
// 传值传参,暗中调用拷贝构造,之后直接窃取成果
String& operator=(String s)
{
swap(s);
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
// const char* c_str(const string* const this)
const char* c_str() const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos < _size);// 要加一吗?如果是只允许操作有效字符,就不用+1
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);// 要加一吗?如果是只允许操作有效字符,就不用+1
return _str[pos];
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// 三种情况:大于_capacity;大于_size 小于_capacity;小于_size
void resize(size_t n, char ch = '\0')
{
// 删除部分数据,保留前n个
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
// 填充
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
void push_back(char ch)
{
/*if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';*/
insert(_size, ch);
}
String& operator+=(char ch)
{
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
append(str);
return *this;
}
void append(const char* str)
{
/*size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);
_size = len;*/
insert(_size, str);
}
String& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 移动
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_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 -1)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
String& earse(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || len + pos >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos + len;
while (begin <= _size)
{
_str[begin - len] = _str[begin];
++begin;
}
_size -= len;
}
return *this;
}
size_t find(char ch, size_t pos = 0)
{
for (;pos < _size; ++pos)
{
if (_str[pos] == ch)
{
return pos;
}
}
return npos;
}
size_t find(char* str, size_t pos = 0)
{
const char* p = strstr(_str + pos, str);
if (p ==nullptr)
{
return npos;
}
return p - _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
private:
char* _str;
size_t _size;// 有效字符的个数(不计\0)
size_t _capacity;// 存储有效字符的空间
//const static size_t npos = -1;// 这样也能通过,但这本质是声明
const static size_t npos;
};
const size_t String::npos = -1;
ostream& operator<< (ostream& out, const String& s)
{
//out << s.c_str();
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>> (istream& in, String& s)
{
//char ch;
in >> ch;// cin>> 拿不到空格或换行
//ch = in.get();
//while (ch != ' ' && ch != '\n')
//{
// s += ch;
// ch = in.get();
//}
//return in;
// 清理缓冲区
s.clear();
char ch;
//in >> ch;// cin>> 拿不到空格或换行
ch = in.get();
char buff[128] = { '\0'};
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)//要留一个位置装\0
{
s += buff;
memset(buff, '\0', 128);
i = 0;
}
ch = in.get();
}
s += buff;
return in;
}
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 strcmp(s1.c_str(), s2.c_str()) > 0;
}
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);
}