前言
各位读者朋友们大家好!上期我们讲完了C++的模板初阶,这一期我们开启STL的学习。STL是C++的数据结构和算法库,是我们学习C++的很重要的一部分内容,在以后的工作中也很重要。现在我们开始讲解。
目录
一. 为什么学习string类
1. C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但这些库函数与字符串是分开的,不太符合OOP(面向对象编程,Object-Oriented Programming是一种广泛使用的编程范式,它基于“对象”的概念来设计和实现软件。)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
二. 标准库中的string类
2.1 string类
string类的文档
在使用string类时,必须包含#include< string >头文件以及指明命名空间std;
2.2 auto和范围for
auto关键字
在这里补充两个C++11的语法,方便后续学习:
-
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推到而得。
-
用auto声明指针类型时,用auto和auto * 没有区别,但是auto声明引用类型的时候必须加&
-
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
-
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
-
auto不能用来直接声明数组
范围for -
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会犯错,因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“:”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判结束。
-
范围for可以作用到数组和容器对象上进行遍历
-
范围for的底层很简单,容器遍历实际就是替换迭代器,这个在汇编层面可以看到
2.3 string类的常用接口说明
1. string类对象的常见构造
- 1.string()
这是string的无参构造,构造一个长度为0的空字符串。
int main()
{
string s1;
cout << s1;
return 0;
}
- 2. string (const string& str);
这是string类的拷贝构造
int main()
{
string s1;
string s2(s1);
cout << s2;
return 0;
}
- 3. string (const string& str, size_t pos, size_t len = npos);
从字符串的指定位置(我们给的是下标)拷贝len个字符,如果字符串太短或着len是npos就拷贝到字符串结束。
int main()
{
string s("hello world");
string s1(s, 0, 5);
string s2(s, 0, 15);
string s3(s, 0);// 第三个参数使用缺省值npos
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
return 0;
}
- 4.string (const char * s);
这是string类的带参构造
int main()
{
string s("hello world");
cout << s << endl;
return 0;
}
- 5. string (const char * s, size_t n);
从s指向的字符数组中,复制前n个字符
int main()
{
string s("hello world", 5);
cout << s;
return 0;
}
- 6. string (size_t n, char c);
用n个字符c来构造字符串
int main()
{
string s(5, 'h');
cout << s;
return 0;
}
2. string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串的有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty(重点) | 检测字符串是否为空串,是返回True,否则返回False |
clear(重点) | 清空有效字符 |
reserve(重点) | 为字符串预留空间 |
resize(重点) | 将有效字符的个数改成n个,多出的空间用字符c填充 |
2.1 size和lenth
size和lenth都是用来求字符串长度的
2.2 capacity
capacity是返回字符串的容量的,跟顺序表中的capacity一样,但是string中的capacity不包含\0
在实现顺序表的时候,我们对顺序表的扩容是二倍扩容,在Vs环境中,编译器对string类的扩容是怎样扩的呢?我们通过下面的一段代码看一下:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
Vs中,string类的实现加了一个buff数组,当字符长度小于16时,就存在buff数组中,如果大于16就去堆上开空间,看一下string的大小:
在32位环境下,按照上面string类的底层内存对齐计算得string类的大小也是28字节,不管使用数组还是在堆上开空间。
在Linux环境中使用g++编译器,是完全的2倍扩容
2.3 empty
2.4 clear
将字符串清空,空间清不清空看编译器,Vs和g++都不清
int main()
{
string s("hello world");
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
2.5 reserve
reserve用来预留空间,预留的空间在Vs环境下会大于等于我们给定的空间,这与Vs的内存对齐有关,在g++环境下会严格等于要求的大小。
那如果预留的空间过大,编译器会不会缩容呢?
C++标准并没有强制要求缩容,看编译器,但是有一点是确定的:这个函数不会改变字符串的长度和内容。
int main()
{
string s("********************");
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(15);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(28);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(48);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
return 0;
}
上面可以看到,在Vs环境下,是不会缩的,但是在g++环境下编译器是会进行缩容的。
2.6 resize
int main()
{
string s("************************");//24
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(18);
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(28, 'x');
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(36, '#');
cout << s << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
3. string类的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[](重点) | 返回pos位置的字符,const string类对象调用 |
begin+end | begin获取开始位置字符的迭代器+end获取最后一个字符下一个位置的迭代器 |
rbegin+rend | rbegin获取最后一个字符的迭代器+rend获取第一个字符前一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
1. 迭代器(iterator)
在C++中,迭代器(Iterator)是一种用于访问容器(如数组、向量、列表、集合等)中元素的对象。迭代器提供了一种统一的方式来遍历容器中的元素,以下是迭代器的主要作用:
1.遍历容器 |
---|
2.访问元素 |
3. 范围操作 |
4. 支持泛型编程:迭代器使得算法和容器可以分离,从而实现高层次的抽象和泛型编程。通过定义迭代器类型,容器可以与各种算法无缝集成。 |
5. 插入和删除操作 |
6. 统一接口:迭代器提供了一个统一的接口来访问和操作容器中的元素,使得用户可以用相同的方式处理不同类型的容器。 |
迭代器共有四种:
2.operator[]访问
string类重载了[]这一操作符,返回的是字符串的任意位置的字符的引用,这样我们就可以修改字符串某一位置的值了,并且越界访问还会报错。
int main()
{
string s("hello world");
cout << s[4] << endl;
//cout << s[20];
s[4] = 'x';
cout << s << endl;
return 0;
}
我们可以认为string类是如下结构:
class string
{
public:
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
operator[]封装在类里面作为成员函数默认为内联,效率也会很高。
3. [] + 下标遍历
int main()
{
string s("hello world");
for (int i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
return 0;
}
这种方式像数组访问一样
4. begin + end遍历
int main()
{
string s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
return 0;
}
这种遍历方式所有的容器都可以使用这种方式
当字符串被const修饰时,使用string::const_interator迭代器遍历:
5. rbegin + rend遍历
rebegin和rend是反向迭代器(string::reverse_iterator)的函数,rbegin获取最后一个字符的迭代器+rend获取第一个字符前一个位置的迭代器。这样方式的遍历是反向遍历
int main()
{
string s("hello world");
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
++rit;
}
return 0;
}
6. 范围for遍历
int main()
{
string s("hello world");
for (auto ch : s)
{
cout << ch << " ";
}
return 0;
}
范围for是自动推导出类型,自动遍历的,也就是说编译器推导出s的类型,然后将其拷贝赋值给ch。
在汇编层面看,范围for的底层就是迭代器。
我们发现,对ch进行修改并没有影响s,这是因为ch是字符串的拷贝,而对迭代器进行操作就会影响到s,我们可以将迭代器理解为指针,对指针解引用修改,想要通过范围for修改s的内容,可以给auto后面加引用,这样ch就是s里面每个变量的别名。
4. string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+=(重点) | 在字符串后面追加字符串 |
insert | 在pos位置插入字符 |
erase | 在pos位置删除len长度的字符 |
4.1 push back
在字符串的后面尾插一个字符
int main()
{
string s("hello wodld");
s.push_back('x');
cout << s << endl;
return 0;
}
4.2 append
在字符串的末尾追加一个新的字符串
int main()
{
string s("hello world");
s.append(" hello Yuey");
cout << s << endl;
return 0;
}
4.3 operator+=
重载了+=操作符,可以在字符串的末尾追加string类、字符串以及字符
int main()
{
string s1("hello world");
string s2("hello world");
string s3("hello world");
// 追加string类
string s4(" hello Yuey");
s1 += s4;
cout << s1 << endl;
// 追加字符串
s2 += " hello Yuey";
cout << s2 << endl;
// 追加字符
s3 += "*";
cout << s3 << endl;
}
这个+=在实践中使用最多
4.4 insert
在pos位置插入字符或字符串,也可以使用迭代器
int main()
{
string s("hello world");
string s1(" hello Yuey");
//s.insert(5, "#");// 插入一个字符
s.insert(5,1,'#');
s.insert(12, s1);
cout << s << endl;
// 迭代器
s.insert(s.begin(), '*');
cout << s << endl;
return 0;
}
4.5 erase
erase支持在指定位置删除len长度的字符,如果len不传的话默认删除npos也就是删除pos位置及以后的所有字符,erase也支持迭代器删除,但是用的最多的还是第一种
int main()
{
string s1("hello world");
// 头删
s1.erase(0, 1);
cout << s1 << endl;
s1.erase(s1.begin());
cout << s1 << endl;
// 尾删
s1.erase(--s1.end());
cout << s1 << endl;
s1.erase(s1.size() - 1, 1);
cout << s1 << endl;
// 指定位置删除
string s2("hello Yuey");
s2.erase(6, 4);
cout << s2 << endl;
// 指定位置不传长度
s2.erase(1);
cout << s2 << endl;
return 0;
}
5. string类的其他相关操作
函数名 | 功能说明 |
---|---|
c_str(重点) | 返回c格式字符 |
find + npos(重点) | 从字符串pos位置开始往后找到字符c,返回该字符在字符串中的位置 |
rfind | 从字符串的pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 从str中从pos位置开始,截取n个字符,然后将其返回 |
5.1 c_str
这个函数的目的主要是兼容C语言,返回的是指向数组的指针,主要在文件读取时使用
5.2 find + npos
find支持在pos位置开始正向查找string类、字符串以及字符,返回值是第一次找到的指定字符的下标,没找到返回npos,如果pos不传,默认从第一个字符开始
将字符串中的空格替换为%:
int main()
{
string s("hello world hello Yuey");
cout << s << endl;
size_t pos = s.find(' ', 0);
while (pos != string::npos)
{
s.replace(pos, 1, "%");
pos = s.find(' ', pos + 1);
}
cout << s << endl;
return 0;
}
这里用到了replace函数:
replace函数就是将pos及以后位置的len长度的字符替换为给定的字符
int main()
{
string s1("hello world");
s1.replace(0, 2, "***");
cout << s1 << endl;
string s2("hello Yuey");
s2.replace(5, 3, "***");
cout << s2;
return 0;
}
但是上面的代码在空格极多的时候效率极低,我们可以用空间换时间的方法:
int main()
{
string s("hello world hello Yuey");
cout << s << endl;
string tmp;
for (auto ch : s)
{
if (ch == ' ')
{
tmp += "%";
}
else
{
tmp += ch;
}
}
s = tmp;
cout << s << endl;
return 0;
}
5.3 substr
substr是返回从pos位置开始的后面len长度的子字符串,如果没传len函数使用缺省值npos,返回pos位置及以后的所有字符。
int main()
{
string s("hello world");
string S1;
S1 = s.substr(5);
cout << S1;
return 0;
}
5.4 rfind
rfind是在pos位置反向查找string类、字符串以及字符,并返回第一次出现该字符的下标,没找到返回npos
int main()
{
string s("Test.cpp.zip");
// 看文件类型
size_t pos = s.rfind('.');
string s1 = s.substr(pos);
cout << s1;
return 0;
}
5.5 find_first_of 和 find_last_of
find_first_of:
这个函数是在字符串中找给定的字符或字符串中的所有字符,并返回第一次找到的地址
int main()
{
string s("abcdefggfedcba");
size_t pos = s.find_first_of("abc");
while (pos != string::npos)
{
s[pos] = '*';
pos = s.find_first_of("abc", pos + 1);
}
cout << s;
return 0;
}
find_last_of:
这个可以理解为反向找
void SplitFilename(const std::string& str)
{
std::cout << "Splitting: " << str << '\n';
std::size_t found = str.find_last_of("/\\");
std::cout << " path: " << str.substr(0, found) << '\n';
std::cout << " file: " << str.substr(found + 1) << '\n';
}
int main()
{
std::string str1("/usr/bin/man");
std::string str2("c:\\windows\\winhelp.exe");
SplitFilename(str1);
SplitFilename(str2);
return 0;
}
这样就能实现文件的路径和文件名分离
5.6 find_first_not_of 和 find_last_not_of
这两个函数是从字符串中找除了给定字符串里的的字符,并返回第一次找到的下标
int main()
{
string s("abcdefggfedcba");
size_t pos = s.find_first_not_of("abc");
while (pos != string::npos)
{
s[pos] = '*';
pos = s.find_first_not_of("abc", pos + 1);
}
cout << s;
return 0;
}
6.getline
在讲getline之前我们先看一道题目: 字符串最后一个单词的长度
这种情况下就需要借助getline函数了:getline
getline默认读取到换行的时候读取结束,读取结束的条件也可以自己设定
自定义结束条件
结语
以上就讲完了string类的基本用法,对于string类的模拟实现我们下期再讲,感谢大家的阅读,欢迎大家批评指正!