✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
小伞的主页:xiaosan_blog
1. 什么是STL
1.1 STL的版本
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
P. J. 版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。
SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本
1.2 STL的六大组件
2.使用第一个容器string
string - C++ Reference (cplusplus.com)
查看C++参考对string的解释,我们会发现string与字符串有关,
在使用string类时,必须包含#include头文件以及using namespace std;
2.1 auto和范围for
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组
int func1()
{
return 10;
}
// 不能做参数
//void func2(auto a)
//{}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
return 3;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
//auto e;
//typeid().name可以打印变量类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
//auto cc = 3, dd = 4.0;
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
//auto array[] = { 4, 5, 6 };
return 0;
}
#include<iostream>
#include <string>
using namespace std;
#include <map>
int main()
{
map<string, string> dict = { { "apple", "ping guo" },{ "orange","cheng zi" }};
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
auto适用大部分的容器,如:“字符串”“顺序表”......
2.2 范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include<iostream>
#include<string>
using namespace std;
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9 };
// C++98的遍历
//采用计算长度,利用for循环遍历
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
arr[i] *= 2;
}
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
cout << arr[i] << endl;
}
// C++11的遍历
//auto会自己遍历
for (auto& e : arr)
e *= 2;
for (auto e : arr)
cout << e << " " << endl;
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
容器遍历实际就是替换为迭代器,这个从汇编层也可以看到,采用范围for会减少访问指针时的报错。
2.3 string类的常用接口说明(注意下面我只讲解最常用的接口)
int main() {
string s1;//构造空的string类对象s1
string s2("hello world");//构造的string类对象s1
string s3(s2);//拷贝构造
return 0;
}
2.3 string类对象的容量操作
函数名称 功能说明 size(重点) 返回字符串有效字符长度 length 返回字符串有效字符长度 capacity 返回空间总大小 empty(重点) 检测字符串释放为空串,是返回true,否则返回false clear(重点) 清空有效字符 reserve(重点)reserve 为字符串预留空间 resize(重点) 将有效字符的个数调整成n个字符,多出的空间用字符c填充
#include<iostream>
#include<string>
using namespace std;
void test1(string& s) {
//返回字符串有效字符(不包括'\0')
cout << s.size()<< endl;
cout << s.length()<< endl;
//返回空间总大小
cout << s.capacity() << endl;
//检测字符串释放为空串,是返回true,否则返回false
cout << s.empty() << endl;
//清空有效字符
s.clear();
cout << s.empty() << endl;
//为字符串预留空间
s.reserve(40);
//将有效字符的个数调整成n个字符,多出的空间用字符c填充
s.resize(60);
}
void test2(string& s) {
//string s2("hello world");
//返回空间总大小:15
cout << s.capacity() << endl;
//预留空间<空间大小
s.reserve(10);
//返回空间大小:15
cout << s.capacity() << endl;
//预留空间>空间大小
s.reserve(500);
//返回空间大小:
cout << s.capacity() << endl;
s.reserve(20);
//返回空间大小:15
cout << s.capacity() << endl;
}
void test3() {
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
//从 15 31 47 70 105 157 235,依次增加空间,基本符合1.5倍空间开辟,后面会2倍开辟空间
//而在gcc的环境下,空间为2倍开辟
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
void test4() {
string s;
// 测试reserve是否会改变string中有效元素个数
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
s.reserve(50);
cout << s.size() << endl;
cout << s.capacity() << endl;
//0 111 0 111
}
int main() {
string s1;//构造空的string类对象s1
string s2("hello world");//构造的string类对象s1
string s3(s2);//拷贝构造
//test1(s2);
test2(s2);
return 0;
}
reserve(为字符串预留空间):注意其小于有效字符大小时,不改变其空间大小
resize(将有效字符的个数调整成n个字符,多出的空间用字符c填充):注意其小于有效字符大小时,会删除其字符
2.4 string类对象的访问及遍历操作
函数名称 | 功能说明 |
operator[](重点) | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位 置的迭代器 |
rbegin + rend | rbegin:返回一个反向迭代器,该迭代器指向字符串的最后一个字符(即其反向开头) rend:返回一个反向迭代器,该迭代器指向字符串第一个字符之前的理论元素(被视为其反向端) |
范围for | C++11支持更简洁的范围for的新遍历方式 |
operate[] :返回pos位置的字符,const string类对象调用
void test5() {
string s1("hello world");
const string s2("Hello world");
cout << s1 << " " << s2 << endl;
//支持下标的访问及更改(注意const对象不能更改)
cout << s1[0] << " " << s2[0] << endl;
s1[0] = 'H';
cout << s1 << endl;
// s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改
}
begin+ end:begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
void test6() {
string s("hello world");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i];
cout << endl;
// 2.迭代器:我们还可以自己实现begin和end迭代器以适配其他容器
string::iterator it = s.begin();//it:获取s的第一个字符的迭代器
while (it != s.end())//end获取最后一个字符迭代器
{
//相当于地址,此时需要解引用
cout << *it ;
++it;
}
cout << endl;
// string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
//反向迭代器
auto rit = s.rbegin();
while (rit != s.rend()) {
cout << *rit ;
rit++;
}
cout << endl;
// 3.范围for,在编译层可以看到,其实是使用迭代器,但如果该改变begin与end的名,则不能使用,而iterator可以
for (auto ch : s)
cout << ch ;
}
hello world
hello world
dlrow olleh(因为rbegin为反向迭代器):
auto rit = s.rbegin();当rit++时,rit是往s第一个字符的方向;
hello world
2.5 string类对象的修改操作
函数名称 | 功能说明 |
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中第一次出现的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中最后一次出现的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
void test7() {
string str;
str.push_back(' '); // 在str后插入空格
str.append("hello"); // 在str后追加一个字符"hello"
str += 'w'; // 在str后追加一个字符'w'
str += "orld"; // 在str后追加一个字符串"orld"
cout << str << endl;
cout << str.c_str() << endl; // 以C语言的方式打印字符串
// 获取file的后缀
string file("str.ing.cpp");
size_t pos = file.rfind('.');//返回该字符出现的最后一次的位置
string suffix(file.substr(pos, file.size() - pos));//在str中从pos位置开始,截取n个字符,然后将其返回
cout << suffix << endl;
// npos是string里面的一个静态成员变量
// static const size_t npos = -1;
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
{
cout << "invalid url" << endl;
return;
}
start += 3;//找到www.,删除前部分
size_t finish = url.find('/', start);//查找第一次出现的位置
string address = url.substr(start, finish - start);
cout << address << endl;
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);//删除0到pos+3的位置
cout << url << endl;
}
2.6 string类非成员函数
函数 | 功能说明 |
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
getline(获取一行字符串):
之前在C语言中我们通常scanf("%[^\n]",&s):需要先定义字符串数组。fget()函数
scanf("%[^\n]",&s);(这里是可以存放空格的);
char c[20];
fgets(c, sizeof(c), stdin);(可以存放空格)
puts(c);
而在C++中准备好了getline()函数,我们不需要提前准备好s的大小
string s;
getline(cin,s);
cout << s;
3.string容器的实现
3.1 string.h(实现接口)
对于调用频繁的函数,包在头文件中实现,效率更高,程序结构也更规范
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace sui
{
class string
{
public:
//迭代器 普通类型与const类型
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//构造函数,这里为全缺省函数""相当于初始化'\0';
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
//字符串中'\0'算一个字节
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//析构函数
~string()
{
if (_str) {
free(_str);
_str = nullptr;
_size = _capacity = 0;
}
}
const char* c_str() const
{
return _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
//实现接口
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
private:
//初始化成员
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
static const size_t npos;
};
}
3.2 string.cpp
void reserve(size_t n);
void string::reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char [n + 1];//多开辟一个字节存放'\0';//字符串拷贝
strcpy(tmp, _str);
delete[] _str;//注意[],因为new[],所以要delete[]与之匹配
_str = tmp;
_capacity = n;//_capacity不包含'\0';
}
}
void push_back(char ch);(插入字符)
void string::push_back(char ch) {
//插入之前,得判断是否剩余空间存放字符
if (_size == _capacity) {//三目操作符
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;//因为_str[_size] = ch;覆盖了原存在的'\0';
_str[_size] = '\0';
}
void append(const char* str);
void string::append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity)
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);//在_str + _size的位置后插入str;
_size += len;
}
string& operator+=(char ch);
string& string::operator+=(char ch) {
push_back(ch);
return *this;
}
string& operator+=(const char* str);
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void insert(size_t pos, char ch);
void string::insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//挪动数据(最后一个字符先移动,否则会覆盖数据)
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[end] = ch;
_size++;
}
void insert(size_t pos, const char* str);
void string::insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}size_t end = _size + len;
//找到目标位置的首个字符位置,移动,从前向后覆盖
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}//覆盖目标位置
for (size_t i = 0; i < len; i++) {
_str[i + pos] = str[i];
}
_size += len;
}
void erase(size_t pos, size_t len = npos)默认设定为-1;
(注意:不要省略npos)
void string::erase(size_t pos, size_t len ) {
assert(pos <= _size);
size_t end = pos;
while (end < pos + len - 1) {
_str[end] = _str[end + len];
--end;
}
_size -= len;
}