内置类string常用接口及模拟实现【C++】【STL】【附题】
文章目录
- 1. 标准库类型string
- 2. 练习
- 2.1 [仅仅反转字母](https://leetcode.cn/problems/reverse-only-letters/)
- 2.2 [字符串中的第一个唯一字符](https://leetcode.cn/problems/first-unique-character-in-a-string/)
- 2.3 [**字符串最后一个单词的长度**](https://www.nowcoder.com/practice/8c949ea5f36f422594b306a2300315da?tpId=37&&tqId=21224&rp=5&ru=/activity/oj&qru=/ta/huawei/question-ranking)
- 2.4 [验证回文串](https://leetcode.cn/problems/valid-palindrome/)
- 2.5 [字符串相加](https://leetcode.cn/problems/add-strings/)
- 2.6 [反转字符串 II](https://leetcode.cn/problems/reverse-string-ii/)
- 2.7 [反转字符串中的单词 III](https://leetcode.cn/problems/reverse-words-in-a-string-iii/)
- 2.8 [字符串相乘](https://leetcode.cn/problems/multiply-strings/)
- 2.9 [**找出字符串中第一个只出现一次的字符**](https://www.nowcoder.com/practice/e896d0f82f1246a3aa7b232ce38029d4?tpId=37&&tqId=21282&rp=1&ru=/activity/oj&qru=/ta/huawei/question-ranking)
- 3. 模拟实现内置类string
1. 标准库类型string
标准库类型string表示可变长字符序列,也就是我们所说的字符串。string包含在std命名空间中,且必须在使用前包含头文件。
1.2 常用接口
string作为一种新类型,相较于C中的字符数组,加之以C++中访问对象中的各种功能函数,在操作上有了许多方便。
1.2.1 *构造接口
下面使用几个例子了解string类构造对象的各种方法:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1 = "hello";
string s2 = s1;
string s3("world");
string s4(s3);
string s5(5,'a');
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
return 0;
}
结果:
hello
hello
world
world
aaaaa
- s1和s3是等价的,都是以一个常量字符串初始化对象,创建一个副本,可以认为赋值符号右边的字符串是一个匿名对象;
- s2和s4是等价的,都是拷贝构造;
- s5是被初始化为5个连续的
a
。
【注意】
C++为了兼容C,在字符串最后会添加一个\0
,即使是空串:
void test2()
{
string s = "";
}
int main()
{
test2();
return 0;
}
通过调试可以看到,s
中的内容是\0
。
1.2.2 功能接口
长度or容量
void test3()
{
string s = "hello";
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
}
int main()
{
test3();
return 0;
}
结果:
5
5
22
- size&&length:返回字符串有效字符长度。它们没有什么不同,只是名字上的差别,因为size的意思更有“个数”的意思,所以一般使用size;
- capacity:string类对象的总空间的大小,即容量。在这里,默认容量是22个字节(char);
其他
void test4()
{
string s1 = "hello";
string s2;
cout << "s1 isEmpty?" << s1.empty() << endl;
cout << "s2 isEmpty?" << s2.empty() << endl;
s1.clear();
cout << "s1 isEmpty?" << s1.empty() << endl;
cout << "s1 capacity:" << s1.capacity() << endl;
s1.reserve(25);
cout << "s1 capacity:" << s1.capacity() << endl;
string s3 = "world";
s3.resize(8,'a');
cout << s3 << endl;
}
int main()
{
test4();
return 0;
}
结果:
s1 isEmpty?0
s2 isEmpty?1
s1 isEmpty?1
s1 capacity:22
s1 capacity:31
worldaaa
- empty():是空串则返回true,否则返回false;
- clear():清空字符串(有效字符);
- reserve(size_t n):;为字符串预留空间(有效字符);
- resize(size_t n, char c):将有效字符改成n个,多余的空间用字符c填充。
【注意】
- clear()不会改变capacity;
- resize(size_t n)和resize(size_t n, char c),如果n比capacity大,会自动扩容,增大capacity;如果n比capacity小,则不会改变capacity;
- reserve()同上。
1.2.3 访问及遍历
operator[]
实际使用直接像数组一样使用[]
即可。这里仅强调是重载过的。
void test5()
{
string s1 = "hello";
for(int i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
}
int main()
{
test5();
return 0;
}
结果:
h e l l o
[i]
作用:返回i位置的字符,const string类对象的调用。
begin()&&end()
void test5()
{
string s1 = "hello";
std::basic_string<char>::iterator i = s1.begin();
for(;i < s1.end(); i++)
{
cout << *i << " ";
}
}
结果:
h e l l o
begin()和end()都是迭代器,暂时把它当做指针,在STL会学习他。
- begin():获取第一个字符的迭代器;
- end():获取最后一个字符的下一个位置的迭代器。
1.2.4 修改操作
push_back(char c)、append(const char* s)、operator+=
void test6()
{
string s1 = "hell";
s1.push_back('o');
cout << s1 << endl;
s1.append(" world");
cout << s1 << endl;
s1 += "!!!";
cout << s1 << endl;
}
int main()
{
test6();
return 0;
}
结果:
hello
hello world
hello world!!!
- push_back(char c):在字符串后尾插字符c;
- append(const char* s):在字符串后追加一个新的字符串s;
- operator+=:在字符串后追加字符串或字符。
【注意】
- 追加字符时,三者都可,值得注意的是append(),需要写成:append(1,c);为了方便,常使用+=。
- 为了合理利用空间,可以使用reserve提前为字符串预留空间。
c_str
void test7()
{
string s = "hello";
const char* c = s.c_str();
cout << c << endl;
}
int main()
{
test7();
return 0;
}
结果:
hello
c_str(),返回一个指向数组的指针,该数组包含以空字符结尾的字符序列(即 C 字符串),表示字符串对象的当前值。此数组包含构成字符串对象值的相同字符序列,以及末尾的附加终止空字符 ( \0
)。
【注意】
- 此函数只能获取对应的字符串;
- 返回的字符串是被const修饰的,原因是c_str()本身被const修饰。
npos、find、rfind
void test8()
{
string s1 = "hello";
cout << s1.find('o') << endl;
cout << s1.find('o', 5) << endl;
cout << s1.find('p') << endl;
cout << string::npos << endl;
}
int main()
{
test8();
return 0;
}
结果:
4
18446744073709551615
18446744073709551615
18446744073709551615
- find(char c, size_t pos):从字符串下标pos位置往后开始,往后查找c字符,找到则返回下标,否则返回npos;rfind则从下标pos位置往前开始;
- npos:这是一个定义在string类内部的公有静态成员变量,它的值是-1,类型是size_t,也就是正数;按无符号整型打印-1,这是一个很大的数。
size_t:即unsigned int或unsigned long。在底层实现中,所有下标都是size_t类型,因为下标不可能为负数。为了统一,接收下标使用的临时变量尽量也使用size_t定义。
substr
void test9()
{
string s1 = "hello";
cout << s1.substr(1,7) << endl;
}
int main()
{
test9();
return 0;
}
结果:
ello
- substr(size_t pos, size_t n):在字符串中从下标pos位置开始,往后截取含有n个字符的子串并返回。
1.2.5 string类非成员函数
operator+
void test10()
{
string s1 = "hello";
string s2 = s1 + " world";
cout << s2 << endl;
}
int main()
{
test10();
return 0;
}
- operator+能实现简单的字符串拼接,包括字符、字符串、string对象。
【注意】
operator+的底层实现是传值返回,效率较低,在模拟实现string类时会了解。
operator>>、operator<<
void test11()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
int main()
{
test11();
return 0;
}
输入:
hello
输出:
hello
- 其实前面一直在使用重载的输入和输出运算符,作用就和它本身一样,将字符串读取或写入内存中。
getline()
void test12()
{
string s1 = "hello";
getline(cin, s1);
cout << s1 << endl;
}
int main()
{
test12();
return 0;
}
输入:
world
输出:
world
- getline(istream& is, string& str):获取一行字符串。从输入流istream is中提取字符并储存在字符串str中,然后返回istream类型的引用,以判断功能是否正常。
比较大小
void test13()
{
string s1 = "hello";
string s2 = "world";
if(s1 == s2)
cout << "s1 == s2" << endl;
if(s1 < s2)
cout << "s1 < s2" << endl;
if(s1 > s2)
cout << "s1 > s2" << endl;
if(s1 >= s2)
cout << "s1 >= s2" << endl;
if(s1 <= s2)
cout << "s1 <= s2" << endl;
if(s1 != s2)
cout << "s1 != s2" << endl;
}
int main()
{
test13();
return 0;
}
结果:
s1 < s2
s1 <= s2
s1 != s2
- 这些比较大小的操作符都是比较字符串字符的ascii值,直到不同的就返回结果。类似C语言中的strcmp函数。
2. 练习
2.1 仅仅反转字母
【思路】
和字符串反转思路类似,在限制下标的同时,在字符串的头和尾分别移动,如果遇到字母则停止,然后交换。
【注意】
- 接收下标的临时变量最好使用size_t类型;
- 在循环内进行查找操作时,用if只能移动一次。用循环必须在循环内再限制下标,否则可能会出现死循环;
- 使用了标准库内置的swap()函数和isalpha()函数。
【代码】
class Solution {
public:
string reverseOnlyLetters(string s) {
if(s.empty())
return s;
size_t begin = 0, end = s.size() - 1;
while(begin < end)
{
while(!isalpha(s[begin]) && begin < end)
begin++;
while(!isalpha(s[end]) && begin < end)
end--;
swap(s[begin], s[end]);
begin++;
end--;
}
return s;
}
};
2.2 字符串中的第一个唯一字符
【思路】
类似计数排序的思路:时间换空间。新开一个数组,遍历字符串,为每个小写字母计数;然后再遍历一次字符串,如果字母出现的次数等于1,则返回下标,否则返回-1;
【注意】
- 保持一致,依然使用size_t处理下标;
- 以0-25为下标给26个小写“挖坑”的简便方法:用字母减去字母表中第一个字母a,即得到当前字母在字母表中的序号-1。
class Solution {
public:
int firstUniqChar(string s) {
int a[26] = { 0 };
for(size_t i = 0; i < s.size(); i++)
{
a[s[i] - 'a']++;
}
for(size_t i = 0; i < s.size(); i++)
{
if(a[s[i] - 'a'] == 1)
return i;
}
return -1;
}
};
2.3 字符串最后一个单词的长度
【思路】
非常简单,得到字符串长度,从后计数,直到遍历完或者遇到空格停止。
【注意】
C++的标准输入流对象cin
不支持输入字符串,因为空格或回车会被认为是分隔符。我们使用getline()函数代替cin>>s
。
【代码】
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
getline(cin, s);
size_t end = s.size() - 1;
int count = 1;
while (end--)
{
if (s[end] != ' ')
{
count++;
}
else
break;
}
cout << count << endl;
}
2.4 验证回文串
【思路】
根据题意,大写转小写,移除所有数字字符,言外之意这就是稍后要比较的前提。
思路简单,头尾同时遍历,分别遇到非字母字符就跳过,然后比较。相同则继续走,不同则返回false。
【注意】
- 大写转小写:由于ASCII表中小写字母都比对应的大写字母多32,所以只需遍历+=32即可。
- 同第一题,非字母字符迭代同样要用while,且要限制begin<end,避免出现死循环。如果最后错误有溢出,很有可能是出现了死循环。
【代码】
class Solution {
public:
void toSmallAlpha(string& s)
{
for (int i = 0; i < s.size(); i++)
{
if (s[i] >= 'A' && s[i] <= 'Z')
s[i] += 32;
}
}
bool isNumberOrAlpha(char ch)
{
return isalpha(ch) || isdigit(ch);
}
bool isPalindrome(string s) {
if (s.empty())
return false;
toSmallAlpha(s);
int begin = 0, end = s.size() - 1;
while (begin < end)
{
while (begin < end && !isNumberOrAlpha(s[begin]))
begin++;
while (begin < end && !isNumberOrAlpha(s[end]))
end--;
if (s[begin] == s[end])
{
begin++;
end--;
}
else
return false;
}
return true;
}
};
2.5 字符串相加
【思路】
逆序遍历两个字符串,然后分别相加当前字符对应的数字,增加一个保存进位的临时变量tmp,用作下一次相加的对象之一。经过进位处理,得到当前数字对于的字符,并将它尾插到新字符串尾部,直到两个数字字符串都被遍历完毕。最后得到的字符串是倒序的,需要反转后再返回。
【注意】
- 迭代的条件:至少还有一个没有遍历完,也就是所有数字字符都要遍历到;
- 数字和它对应的字符相互转换的方法同字母;
- 关于对进位的处理:搭配%和/使用,逻辑较简洁,但是这样效率可能不是很高;因为两个个位数相加最大值为18,所以最多进1,tmp为0和1两种状态。但是这样做需要考虑极端情况:999+1,最后一个进位无法尾插,所以在最后一次进位时尾插tmp。
【代码】
class Solution {
public:
string addStrings(string num1, string num2) {
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
string str;
int tmp = 0, Num = 0, i = 0, Num1 = 0, Num2 = 0;
while (end1 >= 0 || end2 >= 0)
{
if(end1 >= 0)
Num1 = num1[end1--] - '0';
if(end2 >= 0)
Num2 = num2[end2--] - '0';
tmp = tmp + Num1 + Num2;
if (tmp < 10)
{
str.push_back(tmp + '0');
tmp = 0;
}
else
{
str.push_back((tmp - 10) + '0');
tmp = 1;
if (end1 == -1 && end2 == -1)
str.push_back(tmp + '0');
}
Num1 = Num2 = 0;
}
reverse(str.begin(), str.end());
return str;
}
};
2.6 反转字符串 II
【思路】
从题目翻译过来的意思:每隔k个反转一次,如果剩余不够k个,就全部反转。
其实就是一个定长的区间内反转,然后移动固定距离。控制步长和步数,也就控制了控制了反转的区间。
【注意】
- 如何让一个定长区间移动固定距离:让左右端点同时移动(步数*步长)个单位长度;
- 除例子中的情况是反转长度为k的区间外,剩下的都是直接全部反转;由于第二种情况是最后一次反转,步长的累加放在第一种情况就好。
【代码】
class Solution {
public:
//反转函数
void reverseSubstr(string& s, int begin, int end)
{
while (begin < end)
{
swap(s[begin++], s[end--]);
}
}
string reverseStr(string s, int k) {
int num = s.size(), count = 0;
int begin = 0, end = k - 1;//begin和end限制了步长
while (num >= 0)
{
if (num < k)
{
reverseSubstr(s, begin + (2 * k) * count, s.size() - 1);
}
else
{
reverseSubstr(s, begin + (2 * k) * count, end + (2 * k) * count);
count++;//统计步数
}
//num是剩余长度,每次用掉2k个
num -= (2 * k);
}
return s;
}
};
2.7 反转字符串中的单词 III
【思路】
正向反向遍历都可以,主要是确定反转的单词区间和控制下标不要越界。
反向遍历:用begin和end下标限制反转区间,首先将end置为最后一个元素下标,然后用循环找空格(假设有空格)的下标,空格后一个位置就是begin,以此类推。
【注意】
- 需要考虑只有一个单词的字符串,即没有空格。在找空格的下标的循环中,要限制下标不越界,既避免了死循环,也能兼顾找不到空格的情况。
【代码】
class Solution {
public:
void reverseSubstr(string& s, int begin, int end)
{
while (begin < end)
{
swap(s[begin++], s[end--]);
}
}
string reverseWords(string s) {
int cur = s.size() - 1;
int begin = 0, end = 0;
int pos = 0;
while(cur >= 0)
{
end = cur;
while(cur >= 0 && s[cur] != ' ')
{
cur--;
}
begin = cur + 1;
reverseSubstr(s, begin, end);
cur--;
}
return s;
}
};
2.8 字符串相乘
【思路】
根据题意,是要返回一个新字符串,
【代码】
class Solution {
public:
string multiply(string num1, string num2) {
int len1 = num1.size(), len2 = num2.size();
int len = len1 + len2;
//一定要初始化新串的值,否则后面会出现随机值
string ret(len, '0');
for(int i = len1 - 1; i >= 0; i--)
{
for(int j = len2 - 1; j >= 0; j--)
{
//tmp是当前位之和
int tmp = (num1[i] - '0') * (num2[j] - '0') + (ret[i + j + 1] - '0');
//当前的最后一位,得
ret[i + j + 1] = tmp % 10 + '0';
ret[i + j] += tmp / 10;
}
}
for(int i = 0; i < len; i++)
{
if(ret[i] != '0')
return ret.substr(i);
}
return "0";
}
};
2.9 找出字符串中第一个只出现一次的字符
【思路】
遍历字符串,使用内置类string的接口:find_first_of(char ch)和find_last_of(char ch),前者是找到ch第一次出现的位置,后者是找到ch最后一次出现的位置,找不到则返回npos(-1)。如果只出现一次,那么这两个接口的返回值都是相同的。另一种情况就是不存在只出现一次的字符,则在遍历完毕前最后一次打印-1。
【代码】
#include<iostream>
#include<string>
using namespace std;
int main(){
string s;
getline(cin, s);
for(int i = 0; i < s.size(); i++)
{
if(s.find_first_of(s[i]) == s.find_last_of(s[i]))
{
cout << s[i] << endl;
break;
}
if(i == s.size() - 1)
cout << "-1" << endl;
}
return 0;
}
3. 模拟实现内置类string
了解接口的底层实现,能熟练且深刻地掌握string类各种常用接口的用法。而模拟实现某个类或接口,也是面试时常见的问题。
当然,内置类string有一百多个接口,在这里只实现最常用的,大部分接口都是被重载的。
为了区分内置类,取名为Mystring
,除此之外,也可以在自定义命名空间内使用string作为自定义字符串类,以区分内置string类。
3.1 成员变量
string底层是一个能自动扩容的字符数组,所以需要有一个变量记录容量和字符的个数。除此之外,还有最重要的字符数组,它是被new出来的。
- 容量:size_t _capacity;
- 数量:size_t _size;
- 字符数组:char* str;
- npos:static const size_t npos = -1;
许多容器都包含npos,它表示“找不到”的情况的返回值。既然如此,npos是公有静态成员变量。
private:
size_t _size;
size_t _capacity;
char* _str;
public:
const static size_t npos = -1;
3.2 *构造函数&&析构函数
- 构造函数:由于底层是一个动态数组所以使用new动态分配内存,之所以不使用malloc,是为了方便;
- 析构函数:相对地,使用delete释放new分配的内存,并将容量和数量计数器置零,指针置空。
浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。
//构造函数
Mystring(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new(char[_capacity + 1]);
strcpy(_str, str);
}
//析构函数
~Mystring()
{
delete[] _str;
_size = 0;
_str = nullptr;
_capacity = 0;
}
【注意】
这里的构造函数给str以缺省值""
,是为了符合内置string默认空串为\0
的做法。除此之外,申请内存的大小比_capacity
多1,就是为了保存这个\0
。
值得注意的是,_size即个数,不包含末尾的\0
。
构造函数中的初始化列表不是必要的,而且它可能会出现初始化顺序的问题。
【另辟蹊径】
向系统申请内存,然后将字符拷贝到内存中,这样做是符合常理且传统的,有的人提出了新的方法:直接使用传入的字符串,交换它们成员变量的值,以达到构造Mystring对象的目的。
由于稍后还要使用这里的思路,所以将交换功能封装为一个函数。
void swap(Mystring& tmp)
{
std::swap(tmp._capacity, _capacity);
std::swap(tmp._size, _size);
std::swap(tmp._str, _str);
}
Mystring(const Mystring& str)
{
string tmp(str._str);
swap(tmp);
}
3.3 operator[]
- operator[]:重载[]操作符,是为了让使用Mystring能像使用数组一样自然。
【注意】
- 接收下标的类型最好为size_t,下面将不再赘述;
- 返回值为引用类型的原因:a. 为了像数组一样读和写;b. 减少传值拷贝在性能上的消耗;
- 需要判断下标是否越界,由于pos是size_t类型,只需要判断其小于_size即可;
- 运算符重载,还需要有被const修饰的版本,目的是配合用户需要,返回值只能读不能写,下面将不再赘述。
//返回引用是为了读写
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];
}
3.4 迭代器
迭代器在初学STL会学习到,在这里只需要把它当做指针即可。
【注意】
区间是左闭右开。
//迭代器(begin、end)
//可读可写
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//只读
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
3.5 size()、capacity()
- 获取元素个数和容量。
【注意】
个数和容量均不包括\0
。
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
3.6 =、+=、+
- +=、+:相当于尾插;
- =:赋值,相当于拷贝构造。
【注意】
- 返回值都是引用类型;
- 深浅拷贝的解释在下面。
//默认=,浅拷贝
// Mystring& operator=(const Mystring& s)
// {
// if (this != &s)
// {
// char* pStr = new char[strlen(s._str) + 1];
// strcpy(pStr, s._str);
// delete[] _str;
// _str = pStr;
// }
// return *this;
// }
//新=,深拷贝
Mystring& operator=(Mystring& s)
{
swap(s);
return *this;
}
Mystring& operator+=(const char* s)
{
append(s);
return *this;
}
Mystring& operator+=(char* s)
{
append(s);
return *this;
}
Mystring& operator+(const char* s)
{
append(s);
return *this;
}
Mystring& operator+(char* s)
{
append(s);
return *this;
}
3.7 *拷贝构造【深浅拷贝】
- 对于内置类型:深浅拷贝都是直接拷贝;
- 对于自定义类型:浅拷贝只拷贝它们的地址,深拷贝则完全复制一份,地址不同。
浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。
使用默认的=,实现赋值也是粗暴的浅拷贝,首先会出现内存泄漏,原来的地址被覆盖了,其次还是刚才的问题。如果想直接拷贝值,这样做看起来简单,实际上反而麻烦,因为两个空间的大小会影响太小或太大。
- 传统写法:先释放原空间,然后让它再指向新空间,记得
\0
,然后拷贝,最后更新容量。要允许自身赋值,通过判断地址。 - 新写法:要是不想改变原有数据,创建新的临时对象,然后使用swap构造新对象。
3.8 比较运算符重载
- 各种比较运算符重载,只需要实现其中的两个,剩下的复用即可。但在这里我是直接使用strcmp()。
- 其中比较的是第一个不相同的字符的ASCII值。
bool operator>=(const Mystring& s) const
{
return !(strcmp((*this)._str, s._str) < 0);
}
bool operator<=(const Mystring& s) const
{
return !(strcmp((*this)._str, s._str) > 0);
}
bool operator==(const Mystring& s) const
{
return strcmp((*this)._str, s._str) == 0;
}
bool operator>(const Mystring& s) const
{
return strcmp((*this)._str, s._str) > 0;
}
bool operator<(const Mystring& s) const
{
return strcmp((*this)._str, s._str) < 0;
}
3.9 reserve()
这个接口在上面已经提到过。它的功能就是管理容量,传入的参数就是要保存的容量。
- 增容:如果要保存的容量比本身的容量还大,那就是扩容,思路同构造函数;
- 减容:由于内置string类判断字符串终止的标志是
\0
,只需将它放在最后一个位置,然后更新_size。
//更改容量
void reserve(size_t n)
{
if (n > _capacity)//扩容
{
char* tmp = new(char[n + 1]);
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
//元素个数未变
}
else//减容,相当于删除
{
_str[n] = '\0';
_size = n;
}
}
3.10 insert()、push_back()、append()
它们分别是:插入符号或字符串,尾插符号,尾接字符串。(实际上通过查阅官方文档可得知:它们的功能远不如此,为了实现方便,所以只实现简单的几个)。
insert():
- 尾插字符:
- 首先要判断容量,不够要扩容,这里使用了常见的三目运算符;
- 其次就是挪动数据,由于size_t的特殊性,-1是一个很大的正数,所以在循环中不能让下标变成-1,否则会越界;
- 然后再插入,最后返回。
- 尾插字符串:
- 思路同上。
**push_back():**复用insert()尾插。
**append():**同上。
【注意】
- 插入后需要更新_size。
- 为什么不使用strcat(),因为效率低。
Mystring& insert(size_t pos, const char ch)
{
assert(pos <= _size);
//扩容
if (_capacity == _size)
reserve(_capacity == 0 ? 4 : _capacity * 2);
//要挪动的数据个数,再加上\0
/*int num = _size - pos + 1;
int tail = _size;
while (num--)
{
_str[tail + 1] = _str[tail];
tail--;
}*/
size_t tail = _size + 1;
while (tail > pos)
{
_str[tail] = _str[tail - 1];
tail--;
}
_str[pos] = ch;
_size++;
return *this;
}
Mystring& insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 挪动数据
size_t end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, s, len);
_size += len;
return *this;
}
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* s)
{
insert(_size, s);
}
3.11 clear()、erase()
- clear():清空Mystring对象,思路同reserve();
- erase():清空pos位置往后的内容,思路同上。
void clear()
{
_str[0] = '\0';
_size = 0;
_capacity = 0;
}
void erase(size_t pos)
{
assert(pos >= 0);
_str[pos] = '\0';
_size = pos;
}
3.12 operator<<、operator>>
在类和对象中,我们学习了流输入和流输出运算符,简单地说,<<就是将流中的数据提取到显示器(可以是其他设备);>>就是将输入的数据插入到流中,在这里可以认为“流”是内存的一部分,它由各种数据组成。
人们经常使用cin和cout搭配流插入和流提取运算符输入,之所以能支持众多类型,是因为它被重载了。
- operator<<:流提取。遍历字符串,将每个元素重载到ostream对象中,然后一个一个地返回;
- operator>>:流插入。不同于流提取,需要将一定量的数据存入“缓冲区”(buffer),然后将缓冲区的数据插入到流中。通常情况下,在重载中定义一个自定义大小的数组作为缓冲区。注意回车和
\0
是作为字符串结束的标志。
缓冲区:CPU的速度非常快,寄存器更甚,而磁盘的速度相去甚远,数据流是“穿过”它们的,但是由于速度不匹配,这样会造成输入和输出中的一个很快,一个很慢,这样就会出现不必要的等待时间,缓冲区因此而生。
【注意】
- 一般情况重载运算符都在类的内部,或作为友元函数(友元的目的是访问类的成员变量)。但作为友元函数,使用它们就没那么自然,需要
对象名.<<
这样的格式,为了符合日常的习惯,将其写在类的外部,只要能访问类的成员变量即可。 - 可能出现的问题:链接失败。类的声明的代码都写在.h头文件中,可以尝试将>>和<<的重载的实现写在.cpp文件中。原因是它被编译了两次,使用
#program once
或上面的方法也许可以解决。
ostream& operator<<(ostream& os, Mystring& s)
{
for (size_t i = 0; i < s.size(); i++)
{
os << s[i];
}
return os;
}
istream& operator>>(istream& is, Mystring& s)
{
s.clear();
const int N = 32;
char ch = is.get();
//自定义缓冲区
char buffer[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
//达到缓冲区容量,最后加上\0
if (i == N - 1)
{
buffer[i] = '\0';
//连接
s += buffer;
i = 0;
}
//迭代
buffer[i++] = ch;
ch = is.get();
}
//连接
buffer[i] = '\0';
s += buffer;
return is;
}
string还有许多其他内置类型转换,例如to_string......
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程