目录
1. 为什么学习string类?
C 语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便, C 标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
2. 标准库中的string类
1 . string是表示字符串的字符串类2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;4. 不能操作多字节或者变长字符的序列。在使用string类时,必须包含#include头文件以及using namespace std;
string类的常用接口说明
1. string类对象的常见构造
这里只讲解常用的string类的使用,这里结合了默认构造函数的知识,因为这个string类是库里面实现的,所以可以直接使用。
2. string类对象的容量操作
这里展示一下size的用法,以及范围for的遍历,size的用法更加的广泛,范围for在写循环的时候很方便。
注意要点:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。
我们可以看到在vs下的默认的capacity是15我们就可以通过下面的试验可以得出reserve以及resize的不同情况
resize的结果:
3. string类对象的访问及遍历操作
第一个和范围for在前面已经演示过了,这里只讲迭代器:
首先迭代器是什么?
这里不做全面的解释,我们做一个感性的认识:迭代器就像指针一样,但其不一定是个指针,这和它底层原理以及编译器有关。
怎么使用?
可能你回想,这个远没有for循环以及范围for好用,这个东西存在的意义是什么?
对于string类型确实可以使用范围for来遍历,但是对于后面更复杂的数据结构,像树还怎么使用这种方式遍历呢?
下面是rbegin的使用方法:
4. string类对象的修改操作
对于push_back和append的使用这里不过多的介绍,因为他们远没有+=好用,这里只挑重点来介绍
所以使用+=其实更加方便。
这里返回的是C字符串的地址,这个接口是为了有时候一些场景可能使用到。
find函数使用频率也很高。使用十分方便。这里的npos其实就是如果你没有规定边界,那就是找最后。因为npos的返回值是-1,又因为这是size_t类型的。所以npos其实是32个1,就是一个很大的数,对于字符串来说就一定能找到最后了。
5. string类非成员函数
这里就体现了类和对象有没有学明白了。这里的getline和cin有什么区别呢?
如果是这样的场景,cin还能做到吗?
但是getline就可以做到,因为cin是以空格或者换行结束的,但是getline就可以一次输入一行。
关于string类的一些经典题:
这题主要是双指针的思路,在之前学习排序的时候,就有一个partSort是使用双指针的,这题就是一个从begin前面找字符,end在后面找字符,如果遇到非字符就跳过,之后把begin和end的值交换即可。
class Solution {
public:
bool isletter(char s)
{
if (('a' <= s && s <= 'z') || ('A' <= s && s <= 'Z'))
{
return true;
}
return false;
}
string reverseOnlyLetters(string s) {
//双指针
int begin = 0;int end = s.size()-1;
while(begin<end)
{
while(begin<end && !isletter(s[begin]))
{
++begin;
}
while(begin<end && !isletter(s[end]))
{
--end;
}
//交换
swap(s[begin],s[end]);
++begin;
--end;
}
return s;
}
};
2。力扣
这题可以采用计数的思想,因为我们要找最开始重复出现的字符,因为字符的个数是有限的,我们可以开一个数组,记录字符出现的个数,然后从数组中找第一个出现重复的字符。
class Solution {
public:
int firstUniqChar(string s) {
//利用统计字符的方式,将字符串中的字符全部统计一次,如果最后的结果等于1那么就是要的
int arr[256] = {0};
for(auto& ch :s)
{
arr[ch]++;
}
//遍历数组找1
for(int i = 0;i<s.size();i++)
{
if(arr[s[i]] == 1)
{
return i;
}
}
return -1;
}
};
3.力扣
思路:我们可以采用平时我们加的方法去做,但是要注意一点就是时间复杂度,如果我们开辟的新的数组,然后不断头插结果,那么这个时间复杂度就会达到O(N2),但是可以通过得到的结果尾插,最后再逆置就可以把时间复杂度降到O(N)
class Solution {
public:
string addStrings(string num1, string num2) {
//将两个字符串相加,然后以尾插的方式存放在新的字符串中,然后再将字符串逆序
int end1 = num1.size()-1; int end2 = num2.size()-1;
//找出较大的那个的容量
string s;
s.reserve(max(num1.size(),num2.size())+1);
int carry = 0;
while(end1>=0 || end2 >=0)
{
int val1 = end1>=0? num1[end1]-'0':0;
int val2 = end2>=0? num2[end2]-'0':0;
int tmp = val1+val2 + carry;
carry = tmp/10;
tmp = tmp%10;
s += (tmp + '0');
end1--;
--end2;
}
cout<<s<<endl;
//判断最后一次的carry有没有加
if(carry == 1)
{
s += carry + '0';
}
reverse(s.begin(),s.end());
return s;
}
};
4.力扣
思路:我们可以用平时我们算乘法的思想去解决,我们都知道两个数相乘的长度不可能超过两个数相加的长度,然后我们在做乘法的时候就可以把结果存到一个空间中,然后对那个空间做/10的操作将结果放到前一个空间中,然后%10的结果放到当前空间.
这里画了第一步帮助理解:
class Solution {
public:
string multiply(string num1, string num2) {
//利用每个位相乘,然后结果相加即可得到结果,然后再存进string中
int sz1 = num1.size();
int sz2 = num2.size();
string s(sz1 + sz2, '0');
for (int i = sz1 - 1; i >= 0; --i)
{
for (int j = sz2 - 1; j >= 0; --j)
{
int tmp = (s[i + j + 1] - '0') + (num1[i] - '0') * (num2[j] - '0');
//把tmp的值放入字符串
s[i + j + 1] = tmp % 10 + '0';
//把剩下的值放入前一个字符中
s[i + j] += tmp / 10;
}
}
//在字符串中找到子串,其中第一位肯定是非0的
for (int i = 0; i < sz1 + sz2; ++i)
{
if (s[i] != '0')
{
return s.substr(i);
}
}
//到这里就是字符串都是0,结果自然就是0
return "0";
}
};
string类的拷贝构造以及赋值的现代写法
按照以前的拷贝构造以及赋值运算符重载的方式去写显得很麻烦,下面看看这两个方式的差别:
首先登场的是传统的方式:
//老版本的拷贝构造 string(const string& s) { const char* str = s.c_str(); _size = s.size(); _capacity = s.capacity(); _str = new char[_capacity + 1]; strcpy(_str, str); }
实现的方式比较简单,首先就是我们要把空间都开好,然后再慢慢赋值,这样看着挺麻烦的,我们可以通过一个中间的打工人去完成这个工作:
看看这个代码会比传统的方式简单很多:
//现代版本的拷贝构造 string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { //找一个临时对象当打工人 string tmp(s._str); swap(tmp);//这里把我们想要的对象和打工人交换一下,拿到我们想要的深拷贝数据 }
下面我们看看传统的赋值:
//老版本赋值 string& operator=(const string& s) { if (*this != s) { //要记得释放原来的空间 delete[]_str; _size = s.size(); _capacity = s.capacity(); char* tmp = new char[_capacity + 1]; _str = tmp; strcpy(_str, s._str); } return *this; }
我们要先释放掉原来空间的数据,然后开辟空间来进行构造操作。这样显得就很繁琐,我们还是可以使用刚刚的方式来进行操作,先找一个打工人构造好,然后再把它和打工人交换一下,这样我们都可以不需要自己释放,它会自动调用析构函数进行释放:
我们看看代码,这将非常简洁:
// 现代写法 string& operator=(string s)//这里相当于天然有一个对象,可以直接利用 { swap(s); return *this; }
3. string类的模拟实现
这里不过多的讲解,只谈谈深浅拷贝的问题,我们在C++类和对象的章节的时候就谈到了栈空间赋值重载的时候使用的深拷贝,要深拷贝的根本原因应该是指针过维护的空间,因为在拷贝的时候往往会把原指针的地址拷贝过去,从而导致在析构的时候析构两次,并且自己开辟的空间没有释放,导致内存泄露。所以在赋值重载的时候一定要注意深浅拷贝的问题。
如果是这样写的构造函数:
string(const char* str = "") { int len = strlen(str); _size = len; _capacity = len; _str = new char[_capacity + 1]; //拷贝 strcpy(_str, str); }
所以我们的赋值重载函数应该这样写:
string& operator=(const string& s) { if (*this != s) { //要记得释放原来的空间 delete[]_str; _size = s.size(); _capacity = s.capacity(); char* tmp = new char[_capacity + 1]; _str = tmp; strcpy(_str, s._str); } return *this; }
下面是全部的string类的模拟实现,难度不是很大,细节很多,一定要小心边界:
class string
{
public:
typedef char* iterator;
public:
//构造
string(const char* str = "")
{
int len = strlen(str);
_size = len;
_capacity = len;
_str = new char[_capacity + 1];
//拷贝
strcpy(_str, str);
}
//老版本的拷贝构造
/*string(const string& s)
{
const char* str = s.c_str();
_size = s.size();
_capacity = s.capacity();
_str = new char[_capacity + 1];
strcpy(_str, str);
}*/
//现代版本的拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
//找一个临时对象当打工人
string tmp(s._str);
swap(tmp);//这里把我们想要的对象和打工人交换一下,拿到我们想要的深拷贝数据
}
老版本赋值
//string& operator=(const string& s)
//{
// if (*this != s)
// {
// //要记得释放原来的空间
// delete[]_str;
// _size = s.size();
// _capacity = s.capacity();
// char* tmp = new char[_capacity + 1];
// _str = tmp;
// strcpy(_str, s._str);
// }
// return *this;
//}
// 现代写法
string& operator=(string s)//这里相当于天然有一个对象,可以直接利用
{
swap(s);
return *this;
}
//析构
~string()
{
if (_str != nullptr)
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
}
//
// iterator
//迭代器
iterator begin()
{
return _str;
}
iterator end()
{
_str + _size;
}
// modify
//尾插和加等的效果是一样的
void push_back(char c)
{
*this += c;
}
string& operator+=(char c)
{
//考虑扩容
if (_size == _capacity)
{
//这里有可能原来没有空间
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size++] = c;
//放上'\0'
_str[_size] = '\0';
return *this;
}
void append(const char* str)
{
*this += str;
}
string& operator+=(const char* str)
{
int len = strlen(str);
if (len + _size > _capacity)
{
//扩容
reserve(len + _size);
}
strcpy(_str + _size, str);
_size += len;
return *this;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
const char* c_str()const
{
return _str;
}
// capacity
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
bool empty()const
{
return _size == 0;
}
//调整空间大小
void resize(size_t n, char c = '\0')
{
//不缩容
if (n > _size)
{
reserve(n);
for (int i = _size; i < n; ++i)
{
_str[i] = c;
}
_str[n] = '\0';
_size = n;
}
else
{
_str[n] = '\0';
_size = n;
}
}
//对容量的调整
void reserve(size_t n)
{
//不支持缩容
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
// access
//可写
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
//只读
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
//relational operators
//字符串之间的比较
bool operator<(const string & s)
{
for (int i = 0; i < _size; ++i)
{
//s可能比*this更短或者相等
if (i == s._size - 1)
{
return false;
}
if (_str[i] > s[i])
{
return false;
}
}
if (_size > s._size)
{
return false;
}
return true;
}
bool operator<=(const string& s)
{
return !((*this) > s);
}
bool operator>(const string& s)
{
for (int i = 0; i < _size; ++i)
{
//s比*this短
if (i == s._size)
{
return true;
}
if (_str[i] < s[i])
{
return false;
}
}
if (s._size > _size)
{
return false;
}
return true;
}
bool operator>=(const string& s)
{
return !((*this) < s);
}
bool operator==(const string& s)
{
int len = strlen(s.c_str());
if (len != _size)
{
return false;
}
for (int i = 0; i < s.size(); ++i)
{
if (_str[i] != s[i])
{
return false;
}
}
return true;
}
bool operator!=(const string& s)
{
return !((*this) == s);
}
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (int i = pos; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
const char* str = strstr(_str + pos, s);
if (str == nullptr)
{
return npos;
}
else
{
return str - _str;
}
}
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{
assert(pos <= _size);
size_t end = _size + 1;
if (_size == _capacity)
{
//可能刚开始的容量为0
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
//将剩下的字符移动
//strcpy(_str + pos + len, _str + pos);
size_t end = pos + len;
//画图控制边界不容易出错
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//把插入字符串插入进去
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
//pos位置开始全部删完
if (pos + len > _size || len == npos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
//把剩下没有删完的拷贝到pos位置
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
private:
char* _str;
size_t _capacity;
size_t _size;
const static size_t npos = -1;
};
//流插入
ostream& operator<<(ostream& _cout, const liang::string& s)
{
for (int i = 0; i < s.size(); ++i)
{
_cout << s[i];
}
cout << endl;
return _cout;
}
//流提取
istream& operator>>(istream& _cin, liang::string& s)
{
s.clear();
char ch = _cin.get();
int i = 0;
char ret[128] = { '\0' };
while (ch != '\0' && ch != '\n')
{
//防止多次扩容
if (i == 127)
{
s += ret;
i = 0;
}
ret[i++] = ch;
ch = cin.get();
}
//这里可能没有到127
if (i > 0)
{
ret[i] = '\0';
s += ret;
}
return _cin;
}