C++——认识STL及使用及实现第一个容器string
✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏: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类对象s1string s2("hello world");//构造的string类对象s1string 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,否则返回falsecout << 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");//返回空间总大小:15cout << s.capacity() << endl;//预留空间<空间大小s.reserve(10);//返回空间大小:15cout << s.capacity() << endl;//预留空间>空间大小s.reserve(500);//返回空间大小:cout << s.capacity() << endl;s.reserve(20);//返回空间大小:15cout << 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类对象s1string s2("hello world");//构造的string类对象s1string 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;
}