标准库类型string表示可变长字符序列,也就是我们所说的字符串。string包含在std命名空间中,且必须在使用前包含头文件。
string作为一种新类型,相较于C中的字符数组,加之以C++中访问对象中的各种功能函数,在操作上有了许多方便。
下面使用几个例子了解string类构造对象的各种方法:
#include
#include
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
a
。【注意】
C++为了兼容C,在字符串最后会添加一个\0
,即使是空串:
void test2()
{
string s = "";
}
int main()
{
test2();
return 0;
}
通过调试可以看到,s
中的内容是\0
。
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
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
【注意】
实际使用直接像数组一样使用[]
即可。这里仅强调是重载过的。
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类对象的调用。
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会学习他。
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!!!
【注意】
void test7()
{
string s = "hello";
const char* c = s.c_str();
cout << c << endl;
}
int main()
{
test7();
return 0;
}
结果:
hello
c_str(),返回一个指向数组的指针,该数组包含以空字符结尾的字符序列(即 C 字符串),表示字符串对象的当前值。此数组包含构成字符串对象值的相同字符序列,以及末尾的附加终止空字符 ( \0
)。
【注意】
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
size_t:即unsigned int或unsigned long。在底层实现中,所有下标都是size_t类型,因为下标不可能为负数。为了统一,接收下标使用的临时变量尽量也使用size_t定义。
void test9()
{
string s1 = "hello";
cout << s1.substr(1,7) << endl;
}
int main()
{
test9();
return 0;
}
结果:
ello
void test10()
{
string s1 = "hello";
string s2 = s1 + " world";
cout << s2 << endl;
}
int main()
{
test10();
return 0;
}
【注意】
operator+的底层实现是传值返回,效率较低,在模拟实现string类时会了解。
void test11()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
int main()
{
test11();
return 0;
}
输入:
hello
输出:
hello
void test12()
{
string s1 = "hello";
getline(cin, s1);
cout << s1 << endl;
}
int main()
{
test12();
return 0;
}
输入:
world
输出:
world
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
【思路】
和字符串反转思路类似,在限制下标的同时,在字符串的头和尾分别移动,如果遇到字母则停止,然后交换。
【注意】
【代码】
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;
}
};
【思路】
类似计数排序的思路:时间换空间。新开一个数组,遍历字符串,为每个小写字母计数;然后再遍历一次字符串,如果字母出现的次数等于1,则返回下标,否则返回-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;
}
};
【思路】
非常简单,得到字符串长度,从后计数,直到遍历完或者遇到空格停止。
【注意】
C++的标准输入流对象cin
不支持输入字符串,因为空格或回车会被认为是分隔符。我们使用getline()函数代替cin>>s
。
【代码】
#include
#include
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;
}
【思路】
根据题意,大写转小写,移除所有数字字符,言外之意这就是稍后要比较的前提。
思路简单,头尾同时遍历,分别遇到非字母字符就跳过,然后比较。相同则继续走,不同则返回false。
【注意】
【代码】
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;
}
};
【思路】
逆序遍历两个字符串,然后分别相加当前字符对应的数字,增加一个保存进位的临时变量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;
}
};
【思路】
从题目翻译过来的意思:每隔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;
}
};
【思路】
正向反向遍历都可以,主要是确定反转的单词区间和控制下标不要越界。
反向遍历:用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;
}
};
【思路】
根据题意,是要返回一个新字符串,
【代码】
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";
}
};
【思路】
遍历字符串,使用内置类string的接口:find_first_of(char ch)和find_last_of(char ch),前者是找到ch第一次出现的位置,后者是找到ch最后一次出现的位置,找不到则返回npos(-1)。如果只出现一次,那么这两个接口的返回值都是相同的。另一种情况就是不存在只出现一次的字符,则在遍历完毕前最后一次打印-1。
【代码】
#include
#include
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;
}
了解接口的底层实现,能熟练且深刻地掌握string类各种常用接口的用法。而模拟实现某个类或接口,也是面试时常见的问题。
当然,内置类string有一百多个接口,在这里只实现最常用的,大部分接口都是被重载的。
为了区分内置类,取名为Mystring
,除此之外,也可以在自定义命名空间内使用string作为自定义字符串类,以区分内置string类。
string底层是一个能自动扩容的字符数组,所以需要有一个变量记录容量和字符的个数。除此之外,还有最重要的字符数组,它是被new出来的。
许多容器都包含npos,它表示“找不到”的情况的返回值。既然如此,npos是公有静态成员变量。
private:
size_t _size;
size_t _capacity;
char* _str;
public:
const static size_t npos = -1;
浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。
//构造函数
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);
}
【注意】
//返回引用是为了读写
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];
}
迭代器在初学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;
}
【注意】
个数和容量均不包括\0
。
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
【注意】
//默认=,浅拷贝
// 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;
}
浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。
使用默认的=,实现赋值也是粗暴的浅拷贝,首先会出现内存泄漏,原来的地址被覆盖了,其次还是刚才的问题。如果想直接拷贝值,这样做看起来简单,实际上反而麻烦,因为两个空间的大小会影响太小或太大。
\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;
}
bool operator<(const Mystring& s) const
{
return strcmp((*this)._str, s._str) < 0;
}
这个接口在上面已经提到过。它的功能就是管理容量,传入的参数就是要保存的容量。
\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;
}
}
它们分别是:插入符号或字符串,尾插符号,尾接字符串。(实际上通过查阅官方文档可得知:它们的功能远不如此,为了实现方便,所以只实现简单的几个)。
insert():
**push_back():**复用insert()尾插。
**append():**同上。
【注意】
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);
}
void clear()
{
_str[0] = '\0';
_size = 0;
_capacity = 0;
}
void erase(size_t pos)
{
assert(pos >= 0);
_str[pos] = '\0';
_size = pos;
}
在类和对象中,我们学习了流输入和流输出运算符,简单地说,<<就是将流中的数据提取到显示器(可以是其他设备);>>就是将输入的数据插入到流中,在这里可以认为“流”是内存的一部分,它由各种数据组成。
人们经常使用cin和cout搭配流插入和流提取运算符输入,之所以能支持众多类型,是因为它被重载了。
\0
是作为字符串结束的标志。缓冲区:CPU的速度非常快,寄存器更甚,而磁盘的速度相去甚远,数据流是“穿过”它们的,但是由于速度不匹配,这样会造成输入和输出中的一个很快,一个很慢,这样就会出现不必要的等待时间,缓冲区因此而生。
【注意】
对象名.<<
这样的格式,为了符合日常的习惯,将其写在类的外部,只要能访问类的成员变量即可。#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......