文章目录
- C++string类
-
- 为什么要学习string类
- 标准库中的string类
-
- string类
- string类的常用接口说
- [VS 和 G++ 下 string 的结构](#VS 和 G++ 下 string 的结构)
- 小试牛刀
C++string类
为什么要学习string类
C语言字符串的问题
C 语言中字符串是以'\0' 结尾的字符数组,为了操作方便C语言提供了一些 str 系列函数,例如 strlen、strcpy、strcat。但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
标准库中的string类
string类
string是表示字符串的字符串类- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作
string的常规操作。 string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;- 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using std::string;
比较不舒服的地方就是string类并不支持char类型的构造,原因大致有一下几种:
- C语言的char类型既是字符也是字符串,这就导致string准确没办法处理,所以就干脆不处理,就算处理也是将char类型直接当成字符串来处理;
- 因为C++标准库强烈倾向于 显式优于隐式 。这是因为迭代器的原因,后续我们会了解我为什么说是因为迭代器的原因,这里就简单说一下迭代器可以让这些类(容器)拥有一套统一的访问接口,从而完美地与各种泛型算法;
- 因为标准库有 string(size_t n, char c)为了避免产生逻辑冲突,那么可以认为2和3才是为什么C++不提供char类型构造的核心原因。
其实还有一些原因就不展开的说了,因为在后续我们对C++的不断的学习就会不断的了解这些原因。
string类的常用接口说
- 构造函数
| 函数 | 功能 |
|---|---|
string() |
构造空 string |
string(const char* s) |
用 C 字符串构造 |
string(size_t n, char c) |
包含 n 个字符 c |
string(const string& s) |
拷贝构造 |
cpp
#include <iostream>
#include <string>
using std::string;
void test1()
{
string s1; // 构造空的string类对象s1
string s2("hello world"); // 用C-string来构造string类对象s2
string s3(s2); // 拷贝构造s3
string s4(5,'x'); // 构造包含 n 个字符 c类对象s4
}
- string类对象的容量操作
| 函数 | 功能 |
|---|---|
size() |
返回有效字符长度 |
length() |
同上,历史原因保留 |
capacity() |
返回空间总大小 |
empty() |
是否为空 |
clear() |
清空有效字符 |
reserve() |
预留空间 |
resize() |
改变有效字符个数 |
cpp
void test2()
{
string s1 = "hello world";
int size = s1.size();
cout << "size: " << size << endl;
size = s1.capacity();
cout << "capacity: " << size << endl;
if(s1.empty())
{
cout << "s1为空!" << endl;
}
else {
cout << "s1不为空!" << endl;
}
s1.clear();
cout << s1 << endl;
string s2("hello c++");
s2.reserve(13);
cout << s2.size() << endl;
s2.resize(12,'a');
cout << s2 << endl;
}
注意:
- size() 和 length() 底层一致,推荐 size(),为了和其他容器统一。
- clear() 只清空有效字符,不改变底层空间大小。
- resize(n) 增大时用 '\0' 填充;resize(n, c) 用 c 填充。
- resize 增大可能扩容,减小时底层容量通常不变。
- reserve(n) 只预留空间,不改变有效元素个数;如果 n 小于当前容量,通常不改变容量。
- string类对象的访问及遍历操作
| 函数 | 功能 |
|---|---|
operator[] |
返回 pos 位置字符 |
begin()/end() |
正向迭代器 |
rbegin()/rend() |
反向迭代器 |
| 范围 for | C++11 简洁遍历 |
cpp
void test3()
{
string s1 = "abcdefg";
// 常规
for(size_t i = 0; i < s1.size();++i)
{
cout << s1[i] << " ";
}
cout << endl;
// 范围for
for(auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
注:范围for的底层是迭代器来实现的,范围for使用起来更方便并且不易越界。
- tring类对象的修改操作
| 函数名称 | 功能说明 |
|---|---|
push_back |
在字符串后尾插字符c |
append |
在字符串后追加一个字符串 |
operator+= |
在字符串后追加字符串str |
c_str |
返回C格式字符串 |
find + npos |
从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind |
从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr |
在str中从pos位置开始,截取n个字符,然后将其返回 |
cpp
void test4()
{
string s1;
s1 += "hello";
s1 += " world";
cout << s1 << endl;
// char ch = s1;
const char* ch = s1.c_str();
size_t size = s1.find("hello");
if(size != string::npos)
{
cout << "找到了" << endl;
}
else {
cout << "没有找到了" << endl;
}
cout << s1.substr(0,5) << endl;
}
注意:
- push_back( c)、append(1, c)、+= 'c' 效果类似,但 += 最常用。
- += 不仅能连接字符,还能连接字符串。
- 如果能预估字符数量,先 reserve。
- string类非成员函数
| 函数 | 功能说明 |
|---|---|
operator+ |
尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> |
输入运算符重载 |
operator<< |
输出运算符重载 |
getline |
获取一行字符串 |
relational operators |
大小比较 |
cpp
void test5()
{
string s1;
string s2 = "hello ";
string s3 = "world";
s1 = s2 + s3;
cout << s1 << endl;
s1.clear();
std::getline(cin,s1);
cout << s1 << endl;
if (s1 == s2) std::cout << "s1 == s2(内容相同)" << std::endl;
if (s1 != s2) std::cout << "s1 != s2" << std::endl;
if (s1 < s2) std::cout << "s1 < s2 ('a'的ASCII小于'b')" << std::endl;
if (s1 > s2) std::cout << "s1 > s2" << std::endl;
}
注 :如果连续输入string类型,不要使用cin >> s1,因为它遇到空格就会退出建议使用std::getline(cin,s1);
VS 和 G++ 下 string 的结构
VS 下:小字符串优化 SSO
32 位平台下,VS 的 string 大约占 28 字节:
- vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
cpp
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。还有 size、capacity、指针等字段。
故总共占16+4+4+4=28个字节。

g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
cpp
struct _Rep_base
{
size_type M_length;
size_type M_capacity;
_Atomic_word M_refcount;
};
这里的写时拷贝在后续章节我们会了解。写时拷贝就是一种按需修改,当你需要拷贝时它才会真的干活,如果你不需要它就会在哪里等着你需要,就像吃完饭有的是吃完就去洗碗,有的是等下次吃的时候再去洗。写时拷贝就是第二种吃的时候在洗碗的表现。
小试牛刀
我们经过上述对C++的string类进行了充分的了解现在我们通过下面的一些编程题巩固一下对string各种接口的使用。
cpp
bool isLetter(char ch)
{
if (ch >= 'A' && ch <= 'Z')
{
return true;
}
if (ch >= 'a' && ch <= 'z')
{
return true;
}
return false;
}
string reverseOnlyLetters(string s)
{
if (s.empty())
{
return s;
}
size_t begin = 0, end = s.size() - 1;
while (begin < end)
{
while (begin < end && !isLetter(s[begin]))
++begin;
while (begin < end && !isLetter(s[end]))
--end;
std::swap(s[begin],s[end]);
++begin;
--end;
}
return s;
}
核心思路:定义两个双指针头和尾,跳过非字母,交换字母。
cpp
int firstUniqChar(string s)
{
int buffer[256] = {0};
for(auto ch : s)
{
buffer[ch]++;
}
for(int i = 0; i < s.size();++i)
{
if(buffer[s[i]] == 1)
{
return i;
}
}
return -1;
}
思路:统计每个字符出现次数,再按原顺序找第一个次数为 1 的字符。为什么用 256 数组?
字符范围通常是 0~255,直接映射计数,O(n) 时间,实际上不一定就要用256要根据题目给出的范围来定,比如保证了题目只有小写字母就是只需要给数组26的大小即可。
cpp
int main() {
string s1;
while(getline(cin,s1))
{
size_t pos = s1.rfind(' ');
cout << s1.size() - pos - 1 << endl;
}
return 0;
}
我们在上面了解过为什么这里使用getline,使用rfind是因为可以直接从后往前找最后一个空格,空格后面的部分就是最后一个单词。同样这里的判断条件需要根据题目描述决定。
本章节我们了解了C++的string类,下一章节我们就开始自己去实现属于直接的string类了,为什么要实现string类,因为我要通过实现string类来了解底层到底是怎么回事从而进一步加强我们对该类的了解。
完