目录
每日鸡汤:
🚀世上没有绝望的处境,只有对处境绝望的人。
🏆一、constructor
string的构造是比较多的,其中重点是掌握四个基本的构造函数。
npos是定义在string中的一个无符号类型常量,npos=-1。
🏆 二、string类对象的访问及遍历操作
👓Ⅰoperator [] && at
[]就是简单的下标访问。
at功能和operator[]类似,发生越界会抛异常。
👓 Ⅱ范围for
范围for需要注意的是auto推演类型,是传引用。范围for是套用了迭代器,只要支持迭代器,就支持范围for。
👓Ⅲ迭代器
迭代器是行为上像指针一样的东西。
string的迭代器有很多:
①正向迭代器
这里begin()和end()是正向迭代器。其中需要注意的是,end()指向的是最后一个数据的下一个位置。
可能有老铁认为迭代器好像没有下标访问那么便利,迭代器的最大意义是它的通用访问形式。string类的本质是顺序表,采用下标访问当然可以,但是如果对于链表和树,[]访问是不方便的,一句话讲,迭代器是STL通用的访问数据的形式。
②反向迭代器
如果倒着访问数据,就可以使用反向迭代器。
这里之所以报错是因为尽管正向迭代器(begin())和反向迭代器(rbegin())都是迭代器,但是他么的类型是不相同的。如果要使用反向迭代器也只能使用反向迭代器类型!!
👓Ⅲconst 迭代器
const限制的对象必须使用const迭代器,因为非const迭代器是具有对数据进行修改的权力,而const对象是不能被修改的,所以const限制的对象只能使用const迭代器。
const迭代器只能读不能写。
注意const迭代器之所以写法不是const string::iterator,而是string:: const_iterator,是因为const迭代器保护的是it指向的内容,而不是it,it可以++遍历,但是*it不能修改。
const反向迭代器。
总结一下:
简单来提一下库中const类型和非const类型在库中实现的准则:根据需求来实现
🏆三、string类对象的容量操作
函数名称 功能说明
size(重点) 返回字符串有效字符长度length 返回字符串有效字符长度capacity 返回空间总大小empty (重点) 检测字符串释放为空串,是返回true,否则返回falseclear (重点) 清空有效字符
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size() 。
-
clear()只是将string中有效字符清空,不改变底层空间大小。
-
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大 小,如果是将元素个数减少,底层空间总大小不变。
-
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小。
验证:
这些接口都比较简单,比较有意义的是reserve这个接口:
频繁扩容是比较影响效率的一件事情,如果我们预先大概知道要开辟多大的空间,从而预先开辟好空间避免频繁扩容,是提高效率的一种方式。
我们可以先来观察一下vs和Linux下是如何扩容的。
相同的代码在不同的平台上扩容机制是不同的.
🏆四.string类对象的修改操作
函数名称 功能说明push_back 在字符串后尾插字符cappend 在字符串后追加一个字符串operator+= (重点) 在字符串后追加字符串strc_str(重点) 返回C格式字符串find 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
- 不要轻易使用insert和erase,因为它们涉及到数据搬运,复杂度比较大。
erase的使用:从pos位置开始删除len个字符,如果字符串过短或者len大于从pos到末尾的长度,那么就从pos位置开始剩余字符串全部删除。
👓Ⅰassign和replace
assign和replace很容易搞混,但是他们的含义是完全不同的。
assign--替代,全部替代,赋值--趋向于赋值
replace则是替换一部分。
assign会先清空原本字符串内的数据,而replace则是在原本字符串的基础上进行修改。它们都很好用,但是不建议使用,涉及到搬运数据的复杂度比较大。
👓Ⅱfind&&rfind
find通常用于查找某个字符。比如我们想判断一个文件是什么类型的,往往要先找到'.'后缀以方便判断。
我们这里就取到了test.cpp的后缀。这里我们取字符串用了substr的接口。
如果我们的文件格式是test.cpp.zip.tar这样有多个用.隔开的,使用find查找就比较麻烦。库里面提供了rfind接口方便我们倒着查找。
🏆五、string的模拟实现
#include<string>
#include<iostream>
#include<assert.h>
using namespace std;
//string模拟实现
namespace Gyh
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")//给""
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
//swap(_str, tmp._str);//指针交换 --_str指向tmp指向
//swap(_size, tmp._size);
//swap(_capacity, tmp._capacity);
swap(tmp);//究极简化
//析构tmp会有风险--tmp指向野指针--给tmpnullptr
}
string& operator=(string s)
{
//不要tmp ,现成打工人
if(this!=&s)
swap(s);
return *this;
//写法简洁
}
~string()
{
delete[] _str;
_capacity = _size = 0;
}
const char* c_str()
{
return _str;
}
inline size_t size()const
{
return _size;
}
inline size_t capacity()const
{
return _capacity;
}
//普通对象,可读可写
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const对象,只读
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
_capacity = newCapacity;
reserve(_capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';//补上\0
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
string & operator +=(char ch)
{
push_back(ch);
return *this;
}
string& operator +=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
//挪动数据
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)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size+1;
_size = _size + len;
while (end > pos)
{
_str[end+len-1] = _str[end-1];
--end;
}
strncpy(_str + pos, str, len);
return *this;
}
string& erase(size_t pos,size_t len=npos)
{
assert(pos < _size);
if (len == npos || len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size = _size - len;
}
return *this;
}
size_t find(const char ch, size_t pos = 0)const
{
//从某个位置开始找
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
// kmp
size_t find(const char* str, size_t pos = 0)const
{
//从某个位置开始找
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos = -1;//只针对整型
};
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];//不一定要友元
}
//要const版本
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();//先清除
char buffer[128] = { '\0' };
char ch = in.get();
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
s += buffer;
i = 0;
}
buffer[i++] = ch;
ch = in.get();
}
if (i > 0)
{
buffer[i] = '\0';
s += buffer;
}
return in;
}
}
这只是string的简单模拟实现。其中比较有诸多细节需要注意。
👓Ⅰ流插入和流提取的细节
对于一个string类对象,如果我们流插入这个类,和流插入这个类的c_str,两者有什么区别呢?
我们看到如果我们在字符中间插入一个'\0',如果是打印C形式字符串,默认会到'\0'终止,而如果我们是流插入类对象就会把它的全部内容打印出来,这就是区别。所以内核代码是这样撰写的。
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];//不一定要友元
}
//要const版本
return out;
}
它的结束标识是size的大小。
👓Ⅱ赋值和拷贝构造的现代写法
传统写法:
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
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;
}
现代写法:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);//究极简化
//析构tmp会有风险--tmp指向野指针--给tmpnullptr
}
//赋值拷贝写法1
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
//赋值拷贝写法2
string& operator=(string s)
{
//不要tmp ,现成打工人
if(this!=&s)
swap(s);
return *this;
//写法简洁
}
传统和现代写法并无优劣,只是我们为了方便,写出了现代写法。可能现代写法比较奇怪,其中swap函数要特别说明,它和库中通用的swap函数是不同的。
库中的swap函数是通用的,它套用了模板,如果是string类使用库中通用的swap函数,涉及三次深拷贝--代价比较大。所以我们自己设计。
设计的理念在于:
对于拷贝构造,我们可以取巧采用构造函数,构造出一个tmp中间变量。这个tmp对象的内容和数据和我们要拷贝的对象是相同的,只需要把它的内容和我们目的对象交换就可以实现拷贝构造的目的。
需要注意的是,因为交换过后在拷贝构造结束后会自动调用析构函数析构tmp,所以this指向的对象要初始化为nullptr,以免出现野指针析构。
对于赋值拷贝,我们本身就要构造出一个中间变量,所以最简写法就是传值传参,而非传引用传参。
🏆六、string类的一些oj
思想:计数排序。
class Solution {
public:
int firstUniqChar(string s)
{
int ans[26]={0};
for(auto m:s)
{
ans[m-'a']++;
}
int i=0;
for(i=0;i<s.size();++i)
{
if(ans[s[i]-'a']==1)
return i;
}
return -1;
}
};
思想:反向迭代器
#include <iostream>
using namespace std;
int main()
{
string str;
getline(cin,str);//重载了流提取和流插入
size_t pos=str.rfind(' ');
cout<<str.size()-pos-1<<endl;
return 0;
}
// 64 位输出请用 printf("%lld")
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1,end2=num2.size()-1;
int carry=0;
string retStr;
retStr.reserve(end1+end2+1);
while(end1>=0 ||end2>=0)
{
int val1=end1>=0?num1[end1]-'0':0;
int val2=end2>=0?num2[end2]-'0':0;
int ret=val1+val2+carry;
carry=ret/10;
ret%=10;
retStr.push_back(ret+'0');
--end1;
--end2;
}
if(carry)
retStr.push_back(carry+'0');
reverse(retStr.begin(),retStr.end());
return retStr;
}
};
find的使用
class Solution {
public:
string reverseWords(string s)
{
size_t st=0,pos=0;
while(pos<s.size())
{
pos=s.find(' ',pos);
if(pos==string::npos)
{
pos=s.size();
}
reverse(s.begin()+st,s.begin()+pos);
++pos;
st=pos;
}
return s;
}
};
这道题目比较难,涉及到数学的一些知识和字符串相加的思想。
class Solution {
public:
string multiply(string num1, string num2)
{
if(num1=="0"||num2=="0")
{
return "0";
}
int n1=num1.size(),n2=num2.size();
//tmp数组临时存储
auto tmp= vector<int>(n1+n2);
for(int i=n1-1;i>=0;--i)
{
int x=num1.at(i)-'0';
for(int j=n2-1;j>=0;--j)
{
int y=num2.at(j)-'0';
tmp[i+j+1]+=x*y;
}
}
string ans;
ans.reserve(n1+n2);
for(int i=n1+n2-1;i>0;--i)
{
tmp[i-1]+=tmp[i]/10;
tmp[i]%=10;
}
int index=tmp[0]==0?1:0;
for(int i=index;i<n1+n2;++i)
{
ans.push_back(tmp[i]+'0');
}
return ans;
}
};