提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构像顺序表,链表,栈和队列,以及堆,等甚至包括算法都不需要自己重新造轮子,就好像站在巨人的肩膀上,健步如飞的进行开发。
一、【STL简介]
1.1【 STL是什么】
STL(standard template libaray)又名“ 标准模板库 ”:是C++标准库的重要组成部分,它不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。它主要包含六大组成部分,又叫六大组件:
本博客对于这六大组件都会有所涉及,还请耐心看完。
1.2【 STL的发行版本】
【原始版本】
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)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。本篇博客主要参考的就是这个版本。
1.3【STL的缺陷】
1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。
二、【string】
在C语言中,我们知道字符串是以 '\0' 结尾的一些字符的集合,而为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象编程)的思想,而且底层空间需要用户自己进行管理,稍不留神可能还会导致越界访问。
而STL为了体现C++中的OOP的思想,设计了string这一容器,它将相关算法封装到string的内部,当用户定义string对象时可以直接调用相关算法,而在正式开始介绍string之前,
【先附一份优质博客以供参考】:
C++面试中string类的一种正确写法 | 酷 壳 - CoolShell
2.1 【string的介绍】
【首先我们要知道以下内容】:
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char即作为它的字符类型。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。6.在使用string类时,必须包含#include头文件以及using namespace std;
【总结】:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。3. string在底层实际是:basic_string模板类的别名即使用了typedef:
typedef basic_string<char, char_traits, allocator> string ;
4. 不能操作多字节或者变长字符的序列,在使用时要包含对应的头文件。2.2 【string类的常用接口说明】
1.【 常见构造方式】
【参考资料】:
http://www.cplusplus.com/reference/string/string/string/
using namespace std; void TestString1() { string s1; string s2("hello world"); string s3(s2); //string s3("hello string", 3); //复制"hello string"的前3个字符 string s4(10, 's'); //生成10个's'字符的字符串 string s5(s2); //生成s2的复制品 string s6(s2, 0, 4); //复制s2中从字符位置0开始并跨越4个字符的部分 cout << s1 << endl; cout << s2 << endl; cout << s3 << endl; cout << s4 << endl; cout << s5 << endl; cout << s6 << endl; } int main() { TestString1(); return 0; }
2. 【容量操作方式】
【参考资料】:
size:http://www.cplusplus.com/reference/string/string/size/
length:http://www.cplusplus.com/reference/string/string/length/
capacity:http://www.cplusplus.com/reference/string/string/capacity/
empty:http://www.cplusplus.com/reference/string/string/empty/
clear:http://www.cplusplus.com/reference/string/string/clear/
reserve:http://www.cplusplus.com/reference/string/string/reserve/
resize:http://www.cplusplus.com/reference/string/string/resize/
string ::size和string::length都是同义词并返回相同的值
【使用示例】:
void TestString2() { string s1("hello world"); cout << s1.max_size() << endl; cout << s1.capacity() << endl; cout << s1.size() << endl; cout << s1.length() << endl; cout << s1 << endl; s1.reserve(100); cout << s1.capacity() << endl; cout << s1.size() << endl; cout << s1.length() << endl; s1.resize(20,'!'); cout << s1.capacity() << endl; cout << s1.size() << endl; cout << s1.length() << endl; cout << s1 << endl; cout << s1.empty() << endl; s1.clear(); cout << s1 << endl; cout << s1.empty() << endl; }
这里展示了reserve,capacity,resize,length,clear,empty,等接口的用法。
// 利用reserve提高插入数据的效率,避免增容带来的开销 //==================================================================================== 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'; } } }
这里我们可以看到初始容量是15,那么达到它的容量了以后会怎么办呢?会进行扩容吗?
答案是会的,会自动扩容,初始容量为15,第一次扩容为31,之后按接近1.5倍的速率增长。
所以我们可以通过利用reverse来提前对容量进行规划,以提高效率减少开销。
clear和resize的用法注意事项:
void Teststring2() { // 注意:string类对象支持直接用cin和cout进行输入和输出 string s("hello, bit!!!"); cout << s.size() << endl; cout << s.length() << endl; cout << s.capacity() << endl; cout << s << endl; // 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小 s.clear(); cout << s.size() << endl; cout << s.capacity() << endl; // 将s中有效字符个数增加到10个,多出位置用'a'进行填充 // “aaaaaaaaaa” s.resize(20, 'a'); cout << s << endl; cout << s.size() << endl; cout << s.capacity() << endl; // 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充 // "aaaaaaaaaa\0\0\0\0\0" // 注意此时s中有效字符个数已经增加到15个 s.resize(15); cout << s.size() << endl; cout << s.capacity() << endl; cout << s << endl; // 将s中有效字符个数缩小到5个 s.resize(5); cout << s.size() << endl; cout << s.capacity() << endl; cout << s << endl; }
注意事项:
1、当n大于对象当前的size时,将size扩大到n,扩大的字符为c,若c未给出,则默认为’\0’。
2、当n小于对象当前的size时,将size缩小到n。3、clear只会改变容器的大小不会改变容量,哪怕发生过扩容。
【注意】:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。需要注意的是resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变string中有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。也就是说reserve只能增加容量而不能减小容量。3.【 访问及遍历方式】
【参考资料】:
operator [ ] :http://www.cplusplus.com/reference/string/string/operator%5B%5D/
string迭代器iteraor:
begin: http://www.cplusplus.com/reference/string/string/begin/
end:http://www.cplusplus.com/reference/string/string/end/
rbegin:http://www.cplusplus.com/reference/string/string/rbegin/
rend:http://www.cplusplus.com/reference/string/string/rend/
这里可以看一下具体使用方法:
使用 [ ] 和正向迭代器以及反向迭代器来实现遍历:void TestString3() { string s3("hello world"); const string s4("aaaaa"); cout << s3[0] << endl;//可以直接使用[]打印 cout << s4[0] << endl;//可以直接使用[]打印 s3[0] = 'H';//可直接修改 //s4[0] = 'A';//此时s4类似常量字符串,不能修改。 // //1.For循环+operator[] ; for (int i = 0; i < s3.size(); i++) { cout << s3[i]; } cout << endl; //2.迭代器进行遍历 string::iterator it = s3.begin(); /// <summary> /// 这里也可以替换为auto it=s3.begin /// </summary> while (it != s3.end()) { cout << *it; it++; } cout << endl; string::reverse_iterator rit = s3.rbegin(); while (rit != s3.rend()) { cout << *rit; rit++; } cout << endl; //3.范围for遍历 for (auto ch : s3) { cout << ch ; } } int main() { //TestString1(); //TestString2(); //TestPushBack(); TestString3(); return 0; }
4. 【增删查改操作】
【参考资料】:
插入和拼接:
push_back:http://www.cplusplus.com/reference/string/string/push_back/
append:http://www.cplusplus.com/reference/string/string/append/
operator+=:http://www.cplusplus.com/reference/string/string/operator+=/c_str:http://www.cplusplus.com/reference/string/string/c_str/
查找(find+npos):
http://www.cplusplus.com/reference/string/string/find/
http://www.cplusplus.com/reference/string/string/npos/
rfind:http://www.cplusplus.com/reference/string/string/rfind/
subster:http://www.cplusplus.com/reference/string/string/substr/
使用示例:
void TestString1() { string s1("hello world"); cout << s1 << endl; s1.push_back('a'); cout << s1 << endl; s1.append("b"); cout << s1 << endl; s1 += 'c'; s1 += "abc"; cout << s1 << endl; cout << s1.c_str() << endl; }
rfind默认从后向前寻找,rfind找的是字符串中某个字符最后出现的位置,而find找的是某个字符首次出现的位置,而当字符串中不包含某个字符时,就会返回npos
先面来看一个示例:
//size_t find (const string& str, size_t pos = 0) const; //size_t find (const char* s, size_t pos = 0) const; //size_t find (char c, size_t pos = 0) const; void TestString2() { string file("Test.cpp.pp"); size_t pos = file.rfind('.'); cout << pos << endl; string suffix(file.substr(pos, file.size() - pos)); cout << suffix << endl; string url("http://www.cplusplus.com/reference/string/string/find/"); size_t start = url.find("/"); if (start == string::npos) { cout << "invalid url" << endl; return; } cout << start << endl; } //size_t rfind (const string& str, size_t pos = npos) const; //size_t rfind (const char* s, size_t pos = npos) const; //size_t rfind (char c, size_t pos = npos) const;
下面是整体的使用用例:
void TestString3() { // 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; size_t finish = url.find('/', start); string address = url.substr(start, finish - start);//利用substr将“//”和“第一个‘/’ ”之间 //的数据截取出来 cout << address << endl; // 删除url的协议前缀 size_t pos = url.find("://"); url.erase(0, pos + 3); cout << url << endl; }
删除操作使用示例:
void Test_String_Erase() { //1.使用pop_back进行尾删(void pop_back();) string s1("C++"); s1.pop_back(); s1.pop_back(); cout << s1 << endl; //C string s2("I like C++!!!"); //2.使用erase删除 // string& erase (size_t pos = 0, size_t len = npos); //iterator erase(iterator p); //iterator erase(iterator first, iterator last); //erase(pos, n)删除pos位置开始的n个字符 s2.erase(8, 5); //I like C //erase(pos)删除pos位置的字符 s2.erase(s2.end() - 1); //I like //erase(pos1, pos2)删除[pos1pos2)上所有字符 s2.erase(s2.begin() + 1, s2.end()); //I cout << s2 << endl; //I }
提取子串:
void Test_String() { //string substr(size_t pos = 0, size_t len = npos) const; string s1("abcdef"); string s2; //substr(pos, n)提取pos位置开始的n个字符序列作为返回值 s2 = s1.substr(2, 4); cout << s2 << endl; //cdef }
【注意】:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。5.【非成员函数】
【参考资料】:
operator++:http://www.cplusplus.com/reference/string/string/operator+/
operator>>:http://www.cplusplus.com/reference/string/string/operator%3E%3E/
operator<<:http://www.cplusplus.com/reference/string/string/operator%3C%3C/
getline:http://www.cplusplus.com/reference/string/string/getline/
relational operators:http://www.cplusplus.com/reference/string/string/operators/
使用示例:
void Test_String() { /*string类中对 + 运算符进行了重载,重载后的 + 运算符支持以下几种类型的操作: string类 + string类 string类 + 字符串 字符串 + string类 string类 + 字符 字符 + string类 它们相加后均返回一个string类对象。*/ string s1("hello"); cout << s1 << endl; string s2("CSDN"); string s3; s3 = s1+s2; cout << s3 << endl; s3 = s1+"world"; cout << s3 << endl; s3 = s1+"x"; cout << s3 << endl; s1 += s2; cout << s1 << endl; string s4("abcd"); string s5("abde"); cout << (s4> s5) << endl; //0 cout << (s4 < s5) << endl; //1 cout << (s4 == s5) << endl; //0 }
getline函数的使用:
我们知道,使用>>进行输入操作时,当>>读取到空格便会停止读取,基于此,我们将不能用>>将一串含有空格的字符串读入到string对象中。
这里就需要用到getline函数:
用法一、:
void Test_String() { //istream& getline(istream & is, string & str); //getline函数将从is中提取到的字符存储到str中,直到读取到换行符’\n’为止。 string s; getline(cin, s); //输入:hello CSDN cout << s << endl; //输出:hello CSDN }
用法二、:
void Test_String() { //istream& getline (istream& is, string& str, char delim); //getline函数将从is中提取到的字符存储到str中,直到读取到分隔符delim或不传参数就遇到换行符’\n’为止 string s; getline(cin, s, 'D'); //输入:hello CSDN cout << s << endl; //输出:hello CS }
6.【比较,拷贝,替换以及交换】:
1.【比较——compare】
int compare (const string& str) const; int compare (size_t pos, size_t len, const string& str) const; int compare (size_t pos, size_t len, const string& str, size_t subpos, size_t sublen)const;
比较规则:
1、比较字符串中第一个不匹配的字符对应ASCII,较小的字符串小,或者所有比较字符都匹配,但比较字符串较短,字符串小,小就返回小于0的值。
2、比较字符串中第一个不匹配的字符值,较大的字符串大,或者所有比较字符都匹配,但比较字符串较长,字符串大,大则返回大于0的值。
3、比较的两个字符串相等,则返回0。使用示例:
void Test_String() { string s1("hello world"); string s2("hello CSDN"); //"hello world"和"hello CSDN"比较 cout << s1.compare(s2) << endl; //1 //"ell"和"hello CSDN"比较 cout << s1.compare(1, 3, s2) << endl; //-1 //"hello"和"hello"比较 cout << s1.compare(0, 4, s2, 0, 4) << endl; //0 }
注意:除了支持string类之间进行比较,compare函数还支持string类和字符串进行比较。
2.【拷贝——copy】
size_t copy (char* s, size_t len, size_t pos = 0) const;
使用示例:
void Test_String() { string s("abcdef"); char str[20]; //copy(str, n, pos)复制pos位置开始的n个字符到str字符串 size_t length = s.copy(str, 4, 2);//返回的是成功拷贝的字符数 cout << length << endl; cout << sizeof(str) << endl; //copy函数不会在复制内容的末尾附加'\0',需要手动加 str[length] = '\0'; cout << str << endl; //dcef }
3.【替换——replace】
使用replace函数完成string的替换:
string& replace (size_t pos, size_t len, const char* s); string& replace (size_t pos, size_t len, size_t n, char c);
使用示例:
void Test_String() { string s("hello world"); //replace(pos, len, str)将pos位置开始的len个字符替换为字符串str s.replace(6, 4, "CSDN"); //hello CSDNd cout << s << endl; //replace(pos, len, n, char)将pos位置开始的len个字符替换为n个字符char s.replace(10, 1, 3, '!'); //hello CSDN!!! cout << s << endl; }
3.【交换——swap】
使用swap函数完成两个string类的交换:
void swap (string& x, string& y); void swap (string& str);
使用示例:
void Test_String() { string s1("hello"); string s2("CSDN"); //使用string类的成员函数swap交换s1和s2 s1.swap(s2); cout << s1 << endl; //CSDN cout << s2 << endl; //hello //使用非成员函数swap交换s1和s2 swap(s1, s2); cout << s1 << endl; //hello cout << s2 << endl; //CSDN }
string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
7.【vs和g++下string结构的说明】
下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
1.【vs下string的结构】
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
首先,当字符串长度小于16时,使用内部固定的字符数组来存放当字符串长度大于等于16时,从堆上开辟空间这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。2.【g++下string的结构】
g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小字符串有效长度
引用计数
指向堆空间的指针,用来存储字符串。
struct _Rep_base { size_type _M_length; size_type _M_capacity; _Atomic_word _M_refcount; };
但是string类中仍然有一些缺陷这里推荐博客:
STL 的string类怎么啦?_stl里为什么没讲string-CSDN博客
2.3【string的模拟实现】
附上一篇优质博客:C++STL详解(二)—— string类的模拟实现_string(const string& s): _str(), _size(), _capacit-CSDN博客
我们为了跟更加深入了解string除了了解其使用方法以外,最好能够模拟实现string:
下面是代码部分:【String.h部分】:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <string> #include <stdbool.h> #include <iostream> using namespace std; namespace str { class String { public: void swap(str::String& s); const static size_t npos = -1; typedef char* iterator; typedef const char* const_iterator; String(const char* str = ""); ~String(); String(const String& s); String& operator=(const str::String& s); size_t capacity() const; size_t size() const; const char* c_str() const; iterator begin(); iterator end(); const_iterator begin() const; const_iterator end() const; const char& operator[](size_t pos)const; void push_back(char ch); void append(const char* str); void reserve(size_t n); String& operator+=(char ch); String& operator+=(const char* str); void Insert(size_t pos, const char* str); void Insert(size_t pos, char ch); void erase(size_t pos, size_t len = npos); bool operator<(const String& s)const; bool operator==(const String& s)const; bool operator>(const String& s)const; bool operator<=(const String& s)const; bool operator!=(const String& s)const; bool operator>=(const String& s)const; void clear(); void resize(size_t n, char ch = '\0'); size_t find(char ch, size_t pos = 0); size_t find(const char* sub, size_t pos = 0); String substr(size_t pos, size_t len = npos); private: char* _str; size_t _size; size_t _capacity; }; } ostream& operator<<(ostream& out, const str::String& s); istream& operator>>(istream& in, str::String& s);
【String.cpp部分】:
#define _CRT_SECURE_NO_WARNINGS #include "String.h" str::String::String(const char* str) :_size(strlen(str)) ,_capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, str); } str::String::String(const str::String& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } str::String& str::String::operator=(const str::String& s) { if (this != &s) { char* tmp = new char[s._capacity + 1]; strcpy(tmp, s._str); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; } str::String::~String() { delete[] _str; _str = nullptr; _size = _capacity = 0; } size_t str::String::capacity() const { return _capacity; } size_t str::String::size() const { return _size; } const char* str::String:: c_str() const { return _str; } str::String::iterator str::String:: begin() { return _str; } str::String::iterator str::String::end() { return _str + _size; } str::String::const_iterator str::String::begin() const { return _str; } str::String::const_iterator str::String::end() const { return _str + _size; } const char& str::String::operator[](size_t pos)const { assert(pos < _size); return _str[pos]; } void str::String::reserve(size_t n)//按需调整空间 { if (_capacity < n)//当前容量小于指定容量就进行扩容,将数组的内容扩大。 { char* tmp = new char[n + 1];//多开的1个空间是为了存储‘\0’ strcpy(tmp, _str);//之后将原字符串中的内容拷贝到新空间 delete[] _str;//并释放原空间 _str = tmp;//将原数组指向新开辟的数组空间 _capacity = n;//并更新容量 } } void str::String::push_back(char ch) { if (_size == _capacity)在进行尾插之前,要先判断是否需要扩容 { reserve((_capacity == 0) ? 4 : _capacity * 2); } _str[_size] = ch; _size++; _str[_size] = '\0'; } void str::String::append(const char* str) { size_t len = strlen(str); if (_size + len >= _capacity) { reserve(_size + len); } strcpy(_str + _size, str); _size += len; } str::String& str::String::operator+=(char ch) { push_back(ch); return *this; } str::String& str::String::operator+=(const char* str) { append(str); return *this; } void str::String::Insert(size_t pos, char ch) { if (_size == _capacity)//判断是否需要扩容 { reserve((_capacity == 0) ? 4 : 2 * _capacity); } size_t end = _size + 1;//这里定义end位于‘\0’之后的一个位置 while (end > pos)//将pos之后的数据全都向后移 { _str[end] = _str[end-1]; end--; } _str[pos] = ch;//将pos位置处放上需要插入的字符 _size++;//最后将_size++即可 /*int end = _size; while (end >=(int) pos)//这里转换成int的原因是,pos为无符号整数,直接进行比较大小会导致end {//进行类型提升将end也当作无符号整数,当end--为-1时,由于end变为无符整形,-1=4294967295 _str[end+1] = _str[end];//会大于0 end--; } _str[pos] = ch; _size++;*/ } bool str::String:: operator<(const String& s)const { return strcmp(_str, s._str) < 0; } bool str::String:: operator==(const String& s)const { return strcmp(_str, s._str)== 0; } bool str::String:: operator>(const String& s)const { return strcmp(_str, s._str) > 0; } bool str::String:: operator<=(const String& s) const { return *this < s || *this == s; } bool str::String::operator>=(const String& s) const { return !(*this < s); } bool str::String::operator!=(const String& s) const { return !(*this == s); } void str::String::clear() { _str[0] = '\0'; _size = 0; } ostream& operator<<(ostream& out, const str::String& s) { /*for (size_t i = 0; i < s.size(); i++) { out << s[i]; } return out;*/ for (auto ch : s)//范围for遍历字符串 { out << ch; } return out; /*str::String::const_iterator it = s.begin(); while (it != s.end()) { out << *it; it++; } return out;*/ } istream& operator>>(istream& in, str::String& s) { s.clear();//在流提取之前要先将字符串里之前的数据清空,不然提取到的新字符串就会尾插在旧数据 char ch;//之后 ch = in.get();//由于cin类似于scanf遇到空格就不会再进行读取了因此这里需要用到istream里的get char buffer[129];//这里定义一个数组,用来存放读取到的字符,由于是在函数中创建的,不会占用 size_t i = 0;//字符串的空间,跟随函数的生命周期 while (ch != ' ' && ch != '\n') { buffer[i++] = ch; if (i == 128)//如果读取的字符太长数组都存满了,还没有读取结束 {//就将获取到的字符先放入到字符串中,之后重新进行读取 buffer[i] = '\0'; s += buffer; i = 0; } ch = in.get(); } if (i != 0)//如果字符不长,则可以直接将其放到字符串中 { buffer[i] = '\0'; s += buffer; } return in; } void str::String::Insert(size_t pos, const char* str) { assert(pos < _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(len + _size); } //int end = _size + len; //while (end > pos) //{ // _str[end] = _str[end - len]; // end--; //} int end = _size; while (end >= (int)pos) { _str[end + len] = _str[end]; end --; } strncpy(_str + pos, str, len); _size += len; } void str::String::erase(size_t pos, size_t len ) { assert(pos < _size); if (len == npos || len + pos > _size) { _str[pos] = '\0'; _size = pos; } else { size_t begin = pos + len; while (begin <= _size) { _str[begin - len] = _str[begin]; begin++; } _size -= len; } } void str::String::resize(size_t n, char ch) { if (n <= _size) { _str[n] = '\0'; _size = n; } else { reserve(n); while (_size < n) { _str[_size] = ch; ++_size; } _str[_size] = '\0'; } } size_t str::String::find(char ch, size_t pos) { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } size_t str::String::find(const char* sub, size_t pos) { assert(pos < _size); const char* p = strstr(_str + pos, sub); if (p) { return p - _str; } else { return npos; } } str::String str::String::substr(size_t pos, size_t len ) { assert(pos < _size); str::String s; size_t end = pos + len; if (len == npos || len + pos > _size) { len = _size - pos; end = _size; } s.reserve(len); for (size_t i = pos; i < end; i++) { s += _str[i]; } return s; }
【Test.cpp部分】:
#define _CRT_SECURE_NO_WARNINGS #include "String.h" void test_string2() { str::String s1("hello world"); cout << s1.c_str() << endl; s1.push_back(' '); s1.append("hello bit hello bit"); cout << s1.c_str() << endl; s1 += '#'; s1 += "*********************"; cout << s1.c_str() << endl; str::String s2; s2 += '#'; s2 += "*********************"; cout << s2.c_str() << endl; } void test_string3() { str::String s1("hello world"); cout << s1.c_str() << endl; s1.Insert(5, '%'); cout << s1.c_str() << endl; s1.Insert(s1.size(), '%'); cout << s1.c_str() << endl; s1.Insert(0, '%'); cout << s1.c_str() << endl; } void test_string4() { str::String s1("hello world"); str::String s2("hello world"); cout << (s1 >= s2) << endl; //s1[0] = 'z'; cout << (s1 >= s2) << endl; cout << s1 << endl; //cin >> s1; cout << s1 << endl; } void Test_string1() { str::String s1("hello world"); str::String s2; //cout << s1 << endl; //cout << s2 << endl; for (size_t i = 0; i < s1.size(); i++) { cout << s1[i] << " "; } cout << endl; str::String::iterator it = s1.begin(); while (it != s1.end()) { (*it)++; cout << *it << " "; ++it; } cout << endl; for (auto& ch : s1) { ch++; cout << ch << " "; } cout << endl; cout << s1.c_str() << endl; } void test_string5() { str::String s1("hello world"); s1.Insert(5, "abc"); cout << s1 << endl; s1.Insert(0, "xxx"); cout << s1 << endl; s1.erase(0, 3); cout << s1 << endl; s1.erase(5, 100); cout << s1 << endl; s1.erase(2); cout << s1 << endl; } void test_string6() { str::String s1("hello world"); cout << s1 << endl; s1.resize(5); cout << s1 << endl; s1.resize(25, 'x'); cout << s1 << endl; } void test_string7() { str::String s1("test.cpp.tar.zip"); //size_t i = s1.find('.'); //size_t i = s1.rfind('.'); //string s2 = s1.substr(i); //cout << s2 << endl; str::String s3("https://legacy.cplusplus.com/reference/string/string/rfind/"); //string s3("ftp://www.baidu.com/?tn=65081411_1_oem_dg"); // 协议 // 域名 // 资源名 str::String sub1, sub2, sub3; size_t i1 = s3.find(':'); if (i1 != str::String::npos) sub1 = s3.substr(0, i1); else cout << "没有找到i1" << endl; size_t i2 = s3.find('/', i1 + 3); if (i2 != str::String::npos) sub2 = s3.substr(i1 + 3, i2 - (i1 + 3)); else cout << "没有找到i2" << endl; sub3 = s3.substr(i2 + 1); cout << sub1 << endl; cout << sub2 << endl; cout << sub3 << endl; } void test_string8() { str::String s1("hello world"); str::String s2 = s1; cout << s1 << endl; cout << s2 << endl; str::String s3("xxxxxxxxxxxxxxxxxxx"); s2 = s3; cout << s2 << endl; cout << s3 << endl; } void test_string9() { str::String s1("hello world"); cin >> s1; cout << s1 << endl; cout << s1.size() << endl; cout << s1.capacity() << endl; } int main() { //Test_string1(); //test_string1(); //test_string2(); //test_string3(); //test_string4(); //test_string6(); //test_string7(); //test_string8(); //test_string9(); return 0; }
2.4【模拟实现中的问题】
1.【拷贝构造】
下面是老生常谈的问题了,也就是在调用拷贝构造时会出现的两个指针指向一块空间的情况,这样再调用析构函数时,就会导致同一块空间连续析构两次。
说明:
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
这里可以通过增加一个引用计数来解决,也就是说增加一个变量用来统计某块空间被多少个变量所引用。
2.【写时拷贝】
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。【其他参考资料】:
三、【vector】
3.1 【vector的介绍】
【参考资料】:
http://www.cplusplus.com/reference/vector/vector/
1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像静态数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质上讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小需要增加相应的存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,所以存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。
3.2【vector的使用】1.【vector的构造】
代码:
void TestConstructure() { vector<int> a;//无参构造 vector<int> arr(4, 100);//创建一个大小为4,数组元素为100的数组。 vector<int> brr(arr.begin(), arr.end());//以arr的始迭代器和arr的末迭代器构造 vector<int> crr(brr);//用brr拷贝构造crr }
2.【vector中的迭代器】
begin和end
通过begin函数可以得到容器中第一个元素的正向迭代器,通过end函数可以得到容器中最后一个元素的后一个位置的正向迭代器。
rbegin和rend
通过rbegin函数可以得到容器中最后一个元素的反向迭代器,通过rend函数可以得到容器中第一个元素的前一个位置的反向迭代器。
void TestIterator() { vector<int> arr(10, 0); vector<int>::iterator it = arr.begin(); int i = 1; while (it != arr.end()) { *it = i; cout << *it << " "; i++; it++; } cout << endl; int myints[] = { 16,2,77,29 }; vector<int> fifth(myints, myints + sizeof(myints) / sizeof(int));//迭代器构造 cout << "The reverse_contents of fifth are:" << endl; for (vector<int>::reverse_iterator it = fifth.rbegin(); it != fifth.rend(); ++it) cout << ' ' << *it; cout << '\n'; }
3.【容量操作】
【reserve和resize】:
1、通过reserse函数改变容器的最大容量,resize函数改变容器中的有效元素个数。
2、reserve规则:
当所给值大于容器当前的capacity时,将capacity扩大到该值。
当所给值小于容器当前的capacity时,什么也不做。3、resize规则:
当所给值大于容器当前的size时,将size扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为0。
当所给值小于容器当前的size时,将size缩小到该值。void TestCapacity() { int a[] = { 1,2,3,4,4,55,6,7,8,9,100 }; int len = sizeof(a) / sizeof(int); vector<int> arr(a, a+len); cout << " arr的大小为:" << arr.size()<<endl; for (int i=0;i<arr.size();i++) { cout << arr[i] << " "; } cout << endl; // 将有效元素个数设置为n个,如果时增多时,增多的元素使用data进行填充 // 注意:resize在增多元素个数时可能会扩容 arr.resize(2 * len, 0); cout << " arr的大小为:" << arr.size()<<endl; for (auto ch : arr) { cout << ch << " "; } cout << endl; arr.resize(100); cout << " arr的大小为:" << arr.size()<<endl; // 往vecotr中插入元素时,如果大概已经知道要存放多少个元素 // 可以通过reserve方法提前将容量设置好,避免边插入边扩容效率低 vector<int> v; size_t sz = v.capacity(); cout <<"容量为:" << v.capacity() << endl; v.reserve(100);// 提前将容量设置好,可以避免一遍插入一遍扩容 cout << "容量为:" << v.capacity() << endl; cout << "making bar grow:\n"; for (int i = 0; i < 100; ++i) { v.push_back(i); if (sz != v.capacity())//相等就代表扩容,打印新的容量 { sz = v.capacity(); cout << "capacity changed: " << sz << '\n'; } } } //v.reserve(100); // size = 0 capacity 100 //v.resize(100); // size = 100 capacity 100
需要注意的是size和capacity的变化:
void TestChange() { vector<int> v; cout << "v的大小为:" << v.size() << endl; // set some initial content: for (int i = 1; i < 10; i++) v.push_back(i); v.resize(5); cout << "v的大小为:" << v.size() << endl; v.resize(8, 100); cout << "v的大小为:" << v.size() << endl; v.resize(12); cout << "v的大小为:" << v.size() << endl; cout << "v contains:"; for (size_t i = 0; i < v.size(); i++) cout << ' ' << v[i]; cout << '\n'; size_t sz; vector<int> V; sz = V.capacity(); cout << "making V grow:\n"; for (int i = 0; i < 100; ++i) { V.push_back(i); if (sz != V.capacity()) { sz = V.capacity(); cout << "capacity changed: " << sz << '\n'; } } }
【注意点】:
1、capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
2、reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
3、resize在开空间的同时还会进行初始化,影响size。4、reserve和resize:通过reserse函数改变容器的最大容量,resize函数改变容器中的有效元素个数。
5、reserve规则:
当所给值大于容器当前的capacity时,将capacity扩大到该值。
当所给值小于容器当前的capacity时,什么也不做。6、resize规则:
当所给值大于容器当前的size时,将size扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为0。
当所给值小于容器当前的size时,将size缩小到该值。4.【增删查改】
void Add_Delete_Find_Modify() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(5); v.push_back(6); v.push_back(7); v.push_back(8); v.push_back(9); v.push_back(10); cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; auto it = v.begin(); cout << "正向打印:"; while (it != v.end()) { cout << *it << " "; ++it; } cout << endl; // vector<int>::reverse_iterator rit = v.rbegin(); auto rit = v.rbegin(); cout << "反向打印:"; while (rit != v.rend()) { cout << *rit << " "; ++rit; } cout << endl; // 在指定位置前插入值为val的元素,比如:8之前插入1000,如果没有则不插入 // 1. 先使用find查找8所在位置 // 注意:vector没有提供find方法,如果要查找只能使用STL提供的全局find it = find(v.begin(), v.end(), 8); v.insert(it, 1000); for (auto ch : v) { cout << ch<<" "; } cout << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; it = find(v.begin(), v.end(), 3); // 删除pos位置的数据 v.erase(it); for (auto ch : v) { cout << ch<<" "; } cout << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; // operator[]+index 和 C++11中vector的新式for+auto的遍历 // vector使用这两种遍历方式是比较便捷的。 v[0] = 10; cout << v[0] << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; // 1. 使用for+[]小标方式遍历 vector<int> swapv; swapv.swap(v); cout << "交换后:" << endl; cout << "v data:"; for (size_t i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; cout << "swapv data:"; for (size_t i = 0; i < swapv.size(); ++i) cout << swapv[i] << " "; cout << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; cout << "再次交换后:" << endl; v.swap(swapv); cout << "v data:"; for (size_t i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; cout << "swapv data:"; for (size_t i = 0; i < swapv.size(); ++i) cout << swapv[i] << " "; cout << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; v.clear(); cout << "clear后的数据:"<<endl; for (auto ch : v) { cout << ch<<" "; } cout << endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; v.shrink_to_fit(); cout << "shrink_to_fit后:"<<endl; cout << "v的大小为:" << v.size() << endl; cout << "v的容量为:" << v.capacity() << endl; }
find函数:
1、find函数共三个参数,前两个参数确定一个迭代器区间(左闭右开),第三个参数确定所要寻找的值。
2、find函数在所给迭代器区间寻找第一个匹配的元素,并返回它的迭代器,若未找到,则返回所给的第二个参数。3、find函数是在算法模块(algorithm)当中实现的,不是vector的成员函数。
3.3【vector的实现】
这里放一篇参考博客:
C++STL详解(四)—— vector的模拟实现_在不使用编译器的情况下,分析以下代码:将一系列值存入vector<int> ,然后剔除能被2-CSDN博客
实现之前注意vector的存储结构:
vector.h
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; #include <assert.h>; #include <string> namespace xzc { template<class T> class vector { public: typedef T* iterator;//定义vector的迭代器 typedef const T* const_iterator;//定义vector的const迭代器 size_t capacity()const { return _endofstorage - _start; } size_t size()const { return _finish - _start; } void reserve(size_t n) { size_t cp = capacity();//这里在使用capacity和size之前最好先用一个变量进行记录,因 size_t sz = size();//为一旦发生扩容,_start会发生改变,这样就会导致size算的是一个 if (n > cp)//随机值 { T* tmp = new T[n]; if (_start) { for (size_t i = 0; i < sz; i++) { tmp[i] = _start[i]; } delete[] _start; } _start = tmp; _finish = _start + sz; _endofstorage = _start + n; } } void resize(size_t n, const T& val = T()) { size_t cp = capacity(); size_t sz = size(); if (n <= sz) { _finish = _start + n; } else { reserve(n); while (_finish < _start + n) { *_finish = val; ++_finish; } } } vector() {} template <class InputIterator> vector(InputIterator first, InputIterator last) { while (first != last) { push_back(*first); ++first; } } vector(size_t n,const T& val=T()) { reserve(n); for (size_t i = 0; i < n; i++) { push_back(val); } } //现代写法的拷贝构造,创建要构造的对象,遍历要拷贝对象的值,追加到要构造对象里 vector(const vector<T>& v) { reserve(v.capacity()); for (auto& e : v) { push_back(e); } } void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endofstorage, v._endofstorage); } // v1 = v3 //现代写法vector,首先用v3拷贝构造tmp,交换v1和tmp vector<T>& operator=(vector<T> tmp) { swap(tmp); return *this; } ~vector() { delete[] _start; _start = _finish = _endofstorage = nullptr; } iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } size_t capacity() { return _endofstorage - _start; } size_t size() { return _finish - _start; } T& operator[](size_t pos) { assert(pos < size()); return _start[pos]; } const T& operator[](size_t pos) const { assert(pos < size()); return _start[pos]; } void insert(iterator pos, const T& x) { assert(pos >= _start && pos < _finish); size_t cp = capacity(); size_t sz = size(); if (_finish == _endofstorage) { size_t len = pos - _start; reserve((cp == 0) ? 4 : 2 * cp); pos = _start + len; }//插入之前检查是否需要扩容 iterator end = _finish - 1; while (end >= pos)//将pos之后的数据向后挪动,并在pos处放x { *(end + 1) = *end; --end; } *pos = x; _finish++; } iterator erase(iterator pos) { assert(pos >= _start && pos < _finish); iterator it = pos + 1; while (it < _finish)//将pos之后的数据移动覆盖pos位置的数据 { *(it - 1) = *it; ++it; } --_finish; return pos;//为了防止迭代器失效,删除后返回pos之后的数据 } void push_back(const T& x) { if (_finish == _endofstorage) { reserve((capacity() == 0) ? 4 : 2 * capacity()); }//容量满了就扩容,并在尾部插入x *_finish = x; _finish++; } private: iterator _start = nullptr; iterator _finish = nullptr; iterator _endofstorage = nullptr; }; }
test.cpp
#define _CRT_SECURE_NO_WARNINGS #include "vector.h" void test_vector1() { xzc::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); for (size_t i = 0; i < v.size(); i++) { cout << v[i] << " "; } cout << endl; xzc::vector<int>::iterator it = v.begin(); while (it != v.end()) { *it *= 10; cout << *it << " "; ++it; } cout << endl; for (auto e : v) { cout << e << " "; } cout << endl; } void test_vector2() { int i = 0; int j(1); int k = int(2); xzc::vector<int*> v1; v1.resize(10); xzc::vector<string> v2; //v2.resize(10, string("xxx")); v2.resize(10, "xxx"); for (auto e : v1) { cout << e << " "; } cout << endl; for (auto e : v2) { cout << e << " "; } cout << endl; } void test_vector3() { xzc::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); v.push_back(6); v.push_back(7); for (auto e : v) { cout << e << " "; } cout << endl; xzc::vector<int>::iterator it = v.begin() + 2; v.insert(it, 30); for (auto e : v) { cout << e << " "; } cout << endl; //v.insert(v.begin(), 30); v.insert(v.begin() + 3, 30); for (auto e : v) { cout << e << " "; } cout << endl; } void test_vector4() { xzc::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); v.push_back(6); v.push_back(7); for (auto e : v) { cout << e << " "; } cout << endl; auto pos = v.begin(); v.erase(pos); for (auto e : v) { cout << e << " "; } cout << endl; v.erase(v.begin() + 3); for (auto e : v) { cout << e << " "; } cout << endl; } void test_vector5() { //std::vector<int> v; xzc::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(2); v.push_back(4); v.push_back(4); v.push_back(5); v.push_back(6); for (auto e : v) { cout << e << " "; } cout << endl; auto it = v.begin(); while (it != v.end()) { if (*it % 2 == 0) { it = v.erase(it); } else { ++it; } } for (auto e : v) { cout << e << " "; } cout << endl; } void test_vector6() { xzc::vector<string> v; v.push_back("111111111111111111111"); v.push_back("111111111111111111111"); v.push_back("111111111111111111111"); v.push_back("111111111111111111111"); v.push_back("111111111111111111111"); for (auto e : v) { cout << e << " "; } cout << endl; } void test_vector7() { xzc::vector<int> v1; v1.push_back(1); v1.push_back(1); v1.push_back(1); v1.push_back(1); v1.push_back(1); xzc::vector<int> v2(v1); for (auto e : v1) { cout << e << " "; } cout << endl; for (auto e : v2) { cout << e << " "; } cout << endl; xzc::vector<int> v3; v3.push_back(10); v3.push_back(20); v3.push_back(30); v3.push_back(40); v1 = v3; for (auto e : v1) { cout << e << " "; } cout << endl; } void test_vector8() { //vector<int> v0(10, 0); xzc::vector<string> v1(10, "xxxx"); for (auto e : v1) { cout << e << " "; } cout << endl; xzc::vector<int> v2; v2.push_back(10); v2.push_back(20); v2.push_back(30); v2.push_back(40); xzc::vector<int> v3(v2.begin(), v2.end()); string str("hello world"); xzc::vector<char> v4(str.begin(), str.end()); for (auto e : v3) { cout << e << " "; } cout << endl; for (auto e : v4) { cout << e << " "; } cout << endl; } int main() { //test_vector8(); //test_vector7(); //test_vector6(); //test_vector5(); //test_vector4(); //test_vector3(); //test_vector2(); //test_vector1(); return 0; }
3.4【实现中遇到的问题】:
1、【vector迭代器失效】
迭代器的主要作用就是让我们在使用各个容器时不用关心其底层的数据结构,并让我们能够遍历到每个数据而vector的迭代器在底层实际上就是一个指针。迭代器失效就是指迭代器底层对应指针所指向的空间被销毁了,而指向的是一块已经被释放的空间,如果继续使用已经失效的迭代器,程序可能会崩溃。
【示例1】:
void Test_Vector() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5); //v: 1 2 3 4 5 vector<int>::iterator pos = find(v.begin(), v.end(), 2); //获取值为2的元素的迭代器 v.insert(pos, 10); //在值为2的元素的位置插入10 //v: 1 10 2 3 4 5 v.erase(pos); //删除元素2 ???error(迭代器失效) //v: 1 2 3 4 5 } /*/在该代码中,我们本意是使用元素2的迭代器在原序列中2的位置插入一个10,然后将2删除,但我们实际上获取的是指向2的指针,当我们在2的位置插入10后,该指针就指向了10,所以我们之后删除的实际上是10,而不是2。*/
【示例2删除偶数】:
void Test_Vector() { vector<int> v; for (size_t i = 1; i <= 6; i++) { v.push_back(i); } vector<int>::iterator it = v.begin(); while (it != v.end()) { if (*it % 2 == 0) //删除容器当中的全部偶数 { v.erase(it); } it++; } }
该代码看上去实际上并没有什么错误,但如果你画图仔细分析,你就会发现该代码的问题所在:
1.当it遍历到偶数时,调用erase,但是erase是将pos之后的数据向前挪动覆盖,就会导致pos位置后一个位置的数据没有被判断。
2.迭代器访问到了不属于容器的内存空间,导致程序崩溃。
因此在设计erase时直接返回pos位置的后一个数据的迭代器,从而避免迭代器失效的问题
那么对应就要改成:
void Test_Vector() { vector<int> v; for (size_t i = 1; i <= 6; i++) { v.push_back(i); } vector<int>::iterator it = v.begin(); while (it != v.end()) { if (*it % 2 == 0) //删除容器当中的全部偶数 { it = v.erase(it); //删除后获取下一个元素的迭代器 } else { it++; //是奇数则it++ } } }
当元素被删除后继续判断该位置的元素(因为该位置的元素已经更新,需要再次判断)。
2、【不使用memcpy的原因】
在关于容量的接口中:reserve中当需要进行扩容时必然会涉及数据的拷贝,那么为什么我们不使用memcpy呢?
1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。比如对于以下代码如果采用memcpy进行拷贝:
int main() { bite::vector<bite::string> v; v.push_back("1111"); v.push_back("2222"); v.push_back("3333"); return 0; }
结论: 如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。
3.【动态二维数组理解】
先放一道题目:. - 力扣(LeetCode)
解答:
class Solution { public: vector<vector<int>> generate(int numRows) { vector<vector<int>> vv;//定义二维vector,vv,外部vector中的数据是vector该vector中存放数 //据为int vv.resize(numRows);//先确定行数 for(int i=0;i<vv.size();i++)//在确定列数注意每行列数不同,并且每行的第一列和最后一列存放 //1其余都放0 { vv[i].resize(i+1,0); vv[i][0]=vv[i][vv[i].size()-1]=1; } for(int i=0;i<vv.size();i++) { for(int j=0;j<vv[i].size();j++) { if(vv[i][j]==0) { vv[i][j]=vv[i-1][j]+vv[i-1][j-1];//将为0的元素,改为上层两数之和 } } } return vv; } };
在vector中构造动态二维数组:
构造一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类
型的,每行没有包含任何元素,如果n为5时如下所示:vv中元素填充完成之后,如下图所示:
四、【list】
4.1【list的介绍】
【参考资料】:http://www.cplusplus.com/reference/list/list/?kw=list
1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间
开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)4.2 【list的使用】
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。1.【 list的构造】
例子:
void Init_list() { // list迭代器的使用 // 注意:遍历链表只能用迭代器和范围for int arr[] = { 1,2,3,4,5 }; list<int> l1; // 构造空的l1 list<int> l2(5, 10);// l2中放5个值为10的元素 list<int> l3(l2);//l2拷贝构造l3 list<int> l4(arr, arr + sizeof(arr)/sizeof(int));//以数组为迭代器区间构造l4 list<int> l5(l2.begin(), l2.end());// 用l2的[begin(), end())左闭右开的区间构造l3 list<int> l6{ 1,2,3,4,5 };// 列表格式初始化C++11 list<int> l7 = { 1,2,3,4 };//隐式类型转换+赋值重载 cout << "l1:" << endl; for (auto lt : l1) { cout << lt; } cout << endl; cout << "l2:" << endl; for (auto lt : l2) { cout << lt<<" "; } cout << endl; cout << "l3:" << endl; for (auto lt : l3) { cout << lt<<" "; } cout << endl; cout << "l4:" << endl; for (auto lt : l4) { cout << lt<<" "; } cout << endl; cout << "l5:" << endl; for (auto lt : l5) { cout << "l5:" << lt<<" "; } cout << endl; cout << "l6:" << endl; for (auto lt : l6) { cout << lt<<" "; } cout << endl; cout << "l7:" << endl; for (auto lt : l7) { cout << lt << " "; } cout << endl; }
2.【list 迭代器的使用】
此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。例子:
void PrintList(const list<int>& lt) { // 注意这里调用的是list的 begin() const,返回list的const_iterator对象 //同const 指针,const迭代器本质上是不想让迭代器指向的元素被修改,而迭代器本身可以修改 for (list<int>::const_iterator it = lt.begin(); it != lt.end(); ++it) { cout << *it << " "; //*it = 10;// 编译不通过 it++; } cout << endl; } void list_iterator() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; list<int> lt(arr, arr + sizeof(arr) / sizeof(int)); list<int>::iterator it = lt.begin(); cout << "正向" << endl; while (it != lt.end()) { cout << *it << " "; it++;//这里由于链表并不能直接支持++,所以这里的++本质上是运算符重载 } cout << endl; auto rit = lt.rbegin(); cout << "反向" << endl; while (rit != lt.rend()) { cout << *rit << " "; ++rit; } cout << endl; }
【注意】
1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动3.【容量操作】
resize
void Test_List() { list<int> lt(5, 3); for (auto e : lt) { cout << e << " "; } cout << endl; //3 3 3 3 3 lt.resize(7, 6); //将size扩大为7,扩大的值为6 for (auto e : lt) { cout << e << " "; } cout << endl; //3 3 3 3 3 6 6 lt.resize(2); //将size缩小为2 for (auto e : lt) { cout << e << " "; } cout << endl; //3 3 }
resize的两种情况:
- 当所给值大于当前的size时,将size扩大到该值,扩大的数据为第二个所给值,若未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
- 当所给值小于当前的size时,将size缩小到该值。
splice
void Test_List() { list<int> lt1(4, 2); list<int> lt2(4, 6); lt1.splice(lt1.begin(), lt2); //将容器lt2拼接到容器lt1的开头 for (auto e : lt1) { cout << e << " "; } cout << endl; //6 6 6 6 2 2 2 2 list<int> lt3(4, 2); list<int> lt4(4, 6); lt3.splice(lt3.begin(), lt4, lt4.begin()); //将容器lt4的第一个数据拼接到容器lt3的开头 for (auto e : lt3) { cout << e << " "; } cout << endl; //6 2 2 2 2 list<int> lt5(4, 2); list<int> lt6(4, 6); lt5.splice(lt5.begin(), lt6, lt6.begin(), lt6.end()); //将容器lt6的指定迭代器区间内的数据拼接到容器lt5的开头 for (auto e : lt5) { cout << e << " "; } cout << endl; //6 6 6 6 2 2 2 2 }
splice函数用于两个list容器之间的拼接,其有三种拼接方式:
- 将整个容器拼接到另一个容器的指定迭代器位置。
- 将容器当中的某一个数据拼接到另一个容器的指定迭代器位置。
- 将容器指定迭代器区间的数据拼接到另一个容器的指定迭代器位置。
remove和remove if
bool single_digit(const int& val) { return val < 10; } void Test_List() { list<int> lt; lt.push_back(10); lt.push_back(4); lt.push_back(7); lt.push_back(18); lt.push_back(2); lt.push_back(5); lt.push_back(9); lt.remove(18); for (auto e : lt) { cout << e << " "; } cout << endl; //10 4 7 18 2 5 9 lt.remove_if(single_digit); //删除容器当中值小于10的元素 for (auto e : lt) { cout << e << " "; } cout << endl; //10 18 }
empty和size
void list_capacity() { list<int> l1; list<int> l2{1,2,3,4,5}; if (l1.empty()) { cout << "l1为空" << endl; } else { cout << "l1不为空" << endl; } if (l2.empty()) { cout << "l2为空" << endl; } else { cout << "l2不为空" << endl; } cout << "l1的size:" << l1.size() << endl; cout << "l2的size:" << l2.size() << endl; std::list<int> mylist1, mylist2; std::list<int>::iterator it; // set some initial values: for (int i = 1; i <= 4; ++i) mylist1.push_back(i); // mylist1: 1 2 3 4 for (int i = 1; i <= 3; ++i) mylist2.push_back(i * 10); // mylist2: 10 20 30 it = mylist1.begin(); ++it;// mylist1中的it points to 2 cout << "mylist1" << endl; for (auto e : mylist1) { cout << e << " "; } cout << endl; cout << "mylist2" << endl; for (auto e : mylist2) { cout << e << " "; } cout << endl; //mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4 // mylist2 (empty) // "it" still points to 2 (the 5th element) //mylist1.splice(it, mylist2, ++mylist2.begin()); // mylist1: 1 20 2 3 4 // 10:20 mylist1.splice(it, mylist2, ++mylist2.begin(), mylist2.end()); // mylist1: 1 20 30 2 3 4 cout << "splice后的mylist1和mylist2" << endl; for (auto e : mylist1) { cout << e << " "; } cout << endl; for (auto e : mylist2) { cout << e << " "; } cout << endl; }
unique
unique函数用于删除容器当中连续的重复元素。
void Test_List() { list<int> lt; lt.push_back(1); lt.push_back(4); lt.push_back(3); lt.push_back(3); lt.push_back(2); lt.push_back(2); lt.push_back(3); for (auto e : lt) { cout << e << " "; } cout << endl; //1 4 3 3 2 2 3 lt.sort(); //将容器当中的元素排为升序 lt.unique(); //删除容器当中连续的重复元素 for (auto e : lt) { cout << e << " "; } cout << endl; //1 2 3 4 }
注意: 若想使用unique函数做到真正的去重,还需在去重前对容器内元素进行排序。
assign
void Test_List() { list<char> lt(3, 'a'); for (auto e : lt) { cout << e << " "; } cout << endl; lt.assign(3, 'b'); //将新内容分配给容器,替换其当前内容 for (auto e : lt) { cout << e << " "; } cout << endl; //b b b string s("hello world"); lt.assign(s.begin(), s.end()); //将新内容分配给容器,替换其当前内容 for (auto e : lt) { cout << e << " "; } cout << endl; //h e l l o w o r l d }
assign函数用于将新内容分配给容器,替换其当前内容,新内容的赋予方式有两种:
- 将n个值为val的数据分配给容器。
- 将所给迭代器区间当中的内容分配给容器。
4 .【list 的头和尾】
void list_front_back() { list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4); lt.push_back(3); lt.push_back(3); lt.push_back(3); lt.push_back(5); lt.push_back(5); lt.push_back(3); for (auto e : lt) { cout << e << " "; } cout << endl; cout << "lt的头和尾分别为:" << endl; cout << lt.front() << endl; cout << lt.back() << endl; lt.sort(); cout << "lt排序后:" << endl; for (auto e : lt) { cout << e << " "; } cout << endl; lt.unique(); cout << " lt去重后:" << endl; for (auto e : lt) { cout << e << " "; } cout << endl; lt.remove(3); cout << "lt去除3后:" << endl; for (auto e : lt) { cout << e << " "; } cout << endl; //注意在使用去重之前要先进行排序 }
5. 【增删查改】
void list_add_delete_find_modify() { int arr[] = { 1, 2, 3 }; list<int> L(arr, arr + sizeof(arr) / sizeof(arr[0])); cout << "L中的值:" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; // 在list的尾部插入4,头部插入0 L.push_back(4); L.push_front(0); cout << "插入4,0后:" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; // 删除list尾部节点和头部节点 L.pop_back(); L.pop_front(); cout << "头删和尾删后:" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; //获取链表中第二个节点 auto pos = ++L.begin(); cout << "打印当前list的第二个节点的值:"<<endl; cout << *pos << endl; // 在pos前插入值为4的元素 L.insert(pos, 4); cout << "pos之前插入4以后:" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; // 在pos前插入5个值为10的元素 cout << "pos前插入5个10:" << endl; L.insert(pos, 5, 10); for (auto lt : L) { cout << lt << " "; } cout << endl; // 在pos前插入[v.begin(), v.end)区间中的元素 vector<int> v{ 7, 8, 9 }; cout << "pos前插入[v.begin(), v.end()]区间中的元素:" << endl; L.insert(pos, v.begin(), v.end()); for (auto lt : L) { cout << lt << " "; } cout << endl; // 删除pos位置上的元素 cout << "打印pos节点的值:" << endl; cout << *pos << endl; L.erase(pos); cout << "删除pos以后的位置" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; // 删除list中[begin, end)区间中的元素,即删除list中的所有元素 L.erase(L.begin(), L.end()); cout << "删除list中的值后:" << endl; for (auto lt : L) { cout << lt << " "; } cout << endl; int array1[] = { 1, 2, 3 }; list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0])); cout << "打印l1:" << endl; for (auto lt : l1) { cout << lt << " "; } cout << endl; // 交换l1和l2中的元素 list<int> l2; cout << "交换前l1和l2:" << endl; for (auto lt : l1) { cout << lt << " "; } cout << endl; for (auto lt : l2) { cout << lt << " "; } cout << endl; l1.swap(l2); cout << "交换后的l1和l2:" << endl; for (auto lt : l1) { cout << lt << " "; } cout << endl; for (auto lt : l2) { cout << lt << " "; } cout << endl; // 将l2中的元素清空 l2.clear(); cout << "l2中的元素清空后:" << endl; for (auto lt : l2) { cout << lt << " "; } cout << endl; cout << "l2的size:" << endl; cout << l2.size() << endl; }
4.3【list的实现】
要模拟实现list,必须要熟悉list的底层结构以及其接口的含义,通过上面的学习,这些内容已基本掌握,现在我们来模拟实现list。1.【list的组成】
list主要分为3个部分:
1.节点:各节点的数据包括,数据,下一个节点的坐标,上一个节点的坐标
2.迭代器:用来遍历各个节点实现遍历简单化主要分为正向迭代器和反向迭代器,每种迭代器又分为两种const和非const
3.list主体:主要包括一个头结点和list的节点长度。
2.【list的迭代器】
【实现目的】
这里需要实现一个迭代器类,来遍历链表。具体形式如下
之前模拟实现string和vector时都没有说要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?
因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针。
但是对于list来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作。
而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
既然list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。例如,当你使用list当中的迭代器进行自增操作时,实际上执行了p = p->next语句,只是你不知道而已。
总结: list迭代器类,实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样。(例如,对结点指针自增就能指向下一个结点)
【迭代器分类】
而在性质上分为:
不难发现,随即迭代器是一种特殊的双向迭代器,双向迭代器是一种特殊的单向迭代器,不同种类的迭代器具有的功能不相同。
【反向迭代器】
反向迭代器就是正向迭代器的反向,它的++就是正向迭代器的--,反向迭代器的--就是正向迭代器的++,因此反向迭代器的实现可以借助正向迭代器,即:反向迭代器内部可以包含一个正向迭代器,对正向迭代器的接口进行包装即可。
以下是反向迭代器的实现:
template<class Iterator> class ReverseListIterator { // 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的类型,而不是静态成员变量 // 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量 // 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的 public: typedef typename Iterator::Ref Ref; typedef typename Iterator::Ptr Ptr; typedef ReverseListIterator<Iterator> Self; public: // // 构造 ReverseListIterator(Iterator it) : _it(it) {} // // 具有指针类似行为 Ref operator*() { Iterator temp(_it); --temp; return *temp; } Ptr operator->() { return &(operator*()); } // // 迭代器支持移动 Self& operator++() { --_it; return *this; } Self operator++(int) { Self temp(*this); --_it; return temp; } Self& operator--() { ++_it; return *this; } Self operator--(int) { Self temp(*this); ++_it; return temp; } // // 迭代器支持比较 bool operator!=(const Self& l)const { return _it != l._it; } bool operator==(const Self& l)const { return _it != l._it; } Iterator _it; };
3.【list的实现代码部分】
List.h:
#define _CRT_SECURE_NO_WARNINGS #pragma once #include <iostream> using namespace std; namespace xzc { template <class T> struct List_Node { List_Node(const T& x = T()) :_data(x) , _next(nullptr) , _prev(nullptr) {} T _data; List_Node<T>* _prev; List_Node<T>* _next; };//List中的各个节点 //迭代器实现方式1 //下面是List的迭代器,它通过设计3个模板参数,能够实现普通迭代器和const迭代器两种不同的迭代器类 //由于const_iterator和iterator是两种不同的类,需要实现两种不同的模板类,但是通过下面的3个参数 //T(迭代器指向的数据类型),Ref(迭代器指向的数据引用),Ptr(迭代器指向的数据地址),能同时实现这两个 //不同的迭代器类。 //template<class T, class Ref, class Ptr> //struct List_Iterator //{ // typedef List_Node<T> Node; // typedef List_Iterator<T, Ref, Ptr> self; // List_Iterator(Node* node) // :_nodeptr(node) // {} // self& operator++() // { // _nodeptr = _nodeptr->_next; // return *this; // } // self& operator--() // { // _nodeptr = _nodeptr->_prev; // return *this; // } // self operator++(int) // { // self tmp(*this); // _nodeptr = _nodeptr->_next; // return tmp; // } // self operator--(int) // { // self tmp(*this); // _nodeptr = _nodeptr->_prev; // return tmp; // } // Ref operator*() // { // return _nodeptr->_data; // } // Ptr operator->() // { // return &_nodeptr->_data; // } // bool operator !=(const self& it ) // { // return(this->_nodeptr != it._nodeptr); // } // bool operator ==(const self& it) // { // return (_nodeptr == it._nodeptr); // } // Node* _nodeptr; //}; //迭代器实现方式2 //这里是分别写了两个类,来实现List的迭代器下面是iterator template<class T> struct List_Iterator { typedef List_Node<T> Node; typedef List_Iterator<T> self; List_Iterator(Node* node) :_nodeptr(node) {} self& operator++() { _nodeptr = _nodeptr->_next; return *this; } self& operator--() { _nodeptr = _nodeptr->_prev; return *this; } self operator++(int) { self tmp(*this); _nodeptr = _nodeptr->_next; return tmp; } self operator--(int) { self tmp(*this); _nodeptr = _nodeptr->_prev; return tmp; } T& operator*() { return _nodeptr->_data; } T* operator->() { return &_nodeptr->_data; } bool operator!=(const self& s) { return _nodeptr != s._nodeptr; } bool operator==(const self& s) { return _nodeptr == s._nodeptr; } Node* _nodeptr; }; //这里是const_iterator template<class T> struct List_Const_Iterator { typedef List_Node<T> Node; typedef List_Const_Iterator<T> self; Node* _nodeptr; List_Const_Iterator(Node* node) :_nodeptr(node) {} self& operator++() { _nodeptr = _nodeptr->_next; return *this; } self& operator--() { _nodeptr = _nodeptr->_prev; return *this; } self operator++(int) { self tmp(*this); _nodeptr = _nodeptr->_next; return tmp; } self operator--(int) { self tmp(*this); _nodeptr = _nodeptr->_prev; return tmp; } // *it = 10 const T& operator*() { return _nodeptr->_data; } // it->a1 = 10 const T* operator->() { return &_nodeptr->_data; } bool operator!=(const self& s) { return _nodeptr != s._nodeptr; } bool operator==(const self& s) { return _nodeptr == s._nodeptr; } }; //List类的实现 template<class T> class List { typedef List_Node<T> Node; public: typedef List_Iterator<T> iterator;//对不同的迭代器进行重定义这里是实现方式2 typedef List_Const_Iterator<T> const_iterator; //typedef List_Iterator<T, T&, T*> iterator;这里是实现方式1 //typedef List_Iterator<T, const T&, const T*> const_iterator; void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; _size = 0; }//当List为空时,由于结构为带头双向循环,可以使用空初始化对其进行空初始化。 List()//构造函数调用空初始化 { empty_init(); } List(const List<T>& lt)//拷贝构造现代写法 { empty_init(); for (auto e : lt) { push_back(e); } } void swap(List<T>& lt) { std::swap(_head, lt._head); std::swap(_size, lt._size); } // lt3 = lt1 List<int>& operator=(List<int> lt)//赋值重载现代写法 { swap(lt); return *this; } size_t size() { return _size; } iterator begin() { //return iterator(_head->_next);//这里需要返回一个迭代器类型 return _head->_next;//不写iterator的原因是,能够进行隐式类型转换。 } iterator end() { //return iterator(_head->_next); return _head; } const_iterator begin()const { return _head->_next; } const_iterator end() const { return _head; } iterator insert(iterator pos, const T& x) {//链表的插入逻辑定义新节点,找到插入的位置,进行节点之间的连接 Node* cur = pos._nodeptr; Node* prev = cur->_prev; Node* newnode = new Node(x); prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; ++_size; return iterator(newnode); } iterator erase(iterator pos) {//链表的删除逻辑,找到要删除的位置,释放对应节点,将其前后节点进行连接 Node* cur = pos._nodeptr; Node* prev = cur->_prev; Node* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; cur = nullptr; --_size; return iterator(next);//为了方便遍历返回删除结点的后一个节点 } void clear() {//释放全部节点 iterator it = begin(); while (it != end()) { it=erase(it); } } ~ List() { clear(); delete _head; _head = nullptr; _size = 0; } void push_back(const T& x) { insert(end(), x); } private: Node* _head; int _size; }; //const迭代器遍历不能直接定义const迭代器进行遍历,由于类型匹配的原则,普通List对象调用begin调的 //是普通的begin,要么const的begin不和普通的begin重名,要么采用下面的方式遍历 /*template<typename Container> void print_container(const Container& con) { typename Container::const_iterator it = con.begin(); // 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的一个类型,而不是静态成员变 // 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量 // 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的 while (it != con.end()) { cout << *it << " "; ++it; } cout << endl; }*/ }
Test.cpp:
#define _CRT_SECURE_NO_WARNINGS #include "list.h" #include<iostream> #include<list> #include<vector> #include<string> #include<algorithm> using namespace std; //void test_list4() //{ // list<int> lt; // lt.push_back(1); // lt.push_back(2); // lt.push_back(3); // lt.push_back(4); // lt.push_back(5); // //print_list(lt); // xzc::print_container(lt); // // list<string> lt1; // lt1.push_back("1111111111111"); // lt1.push_back("1111111111111"); // lt1.push_back("1111111111111"); // lt1.push_back("1111111111111"); // lt1.push_back("1111111111111"); // //print_list(lt1); // xzc::print_container(lt1); // // vector<string> v; // v.push_back("222222222222222222222"); // v.push_back("222222222222222222222"); // v.push_back("222222222222222222222"); // v.push_back("222222222222222222222"); // //print_list(v); // xzc::print_container(v); //} struct AA { AA(int a1 = 0, int a2 = 0) :_a1(a1) , _a2(a2) {} int _a1; int _a2; }; void test_list3() { xzc::List<AA> lt; lt.push_back(AA(1, 1)); lt.push_back(AA(2, 2)); lt.push_back(AA(3, 3)); xzc:: List<AA>::iterator it = lt.begin(); while (it != lt.end()) { //cout << (*it)._a1<<" "<<(*it)._a2 << endl; cout << it->_a1 << " " << it->_a2 << endl; cout << it.operator->()->_a1 << " " << it.operator->()->_a1 << endl; ++it; } cout << endl; int* p = new int; *p = 1; AA* ptr = new AA; ptr->_a1 = 1; } void test_list2() { xzc::List<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4); lt.push_back(5); xzc::List<int> lt1(lt); for (auto e : lt) { cout << e << " "; } cout << endl; for (auto e : lt1) { cout << e << " "; } cout << endl; xzc::List<int> lt2; lt2.push_back(10); lt2.push_back(200); lt2.push_back(30); lt2.push_back(40); lt2.push_back(50); lt1 = lt2; for (auto e : lt1) { cout << e << " "; } cout << endl; for (auto e : lt2) { cout << e << " "; } cout << endl; } void test1() { xzc::List<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4); lt.push_back(5); // 封装屏蔽底层差异和实现细节 // 提供统一的访问修改遍历方式 xzc::List<int>::iterator it = lt.begin(); while (it != lt.end()) { *it += 20; cout << *it << " "; it++; } cout << endl; } int main() { test_list3(); //test_list2(); //test1(); //test_list4(); return 0; }
4.4.【实现中遇到的问题】
1.【list的迭代器失效】
前面说过,大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
void TestListIterator1() { int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; list<int> l(array, array + sizeof(array) / sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给 //其赋值 l.erase(it); ++it; } } // 改正 void TestListIterator() { int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; list<int> l(array, array + sizeof(array) / sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { l.erase(it++); // it = l.erase(it); } }
其他还有迭代器相关问题,在后面统一说明。
4.5【list与vector的对比】
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
五、【stack和queue】
5.1【stack】
5.1.1【stack的介绍】
与数据结构中的栈和队列相同。
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。5.1.2【stack的使用】
void Test_stack() { stack<int> s; s.push(1); s.push(2); s.push(3); s.push(4); s.push(5); cout << "未弹出之前栈的大小为:" << s.size()<<endl; while (!s.empty()) { cout << s.top() << " "; s.pop(); } cout << endl; cout << "弹出后栈的大小为:" << s.size()<<endl; }
5.1.3【stack的模拟实现】
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; #include <deque> namespace xzc { template<class T, class Container = deque<T>> class Stack { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_back(); } const T& top() { return _con.back(); } bool empty() { return _con.empty(); } size_t size() { return _con.size(); } private: Container _con; }; }
5.2【queue】
5.2.1【queue的介绍】
1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列
4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。5.2.2【queue的使用】
void Test_queue() { queue<int> q; q.push(1); q.push(2); q.push(3); q.push(4); q.push(5); q.push(6); cout << "未弹出之前队列的大小为:" << q.size() << endl; while (!q.empty()) { cout << q.back() << endl; cout << q.front() << " "; q.pop(); } cout << endl; cout << "弹出后队列的大小为:" << q.size() << endl; }
5.2.3【queue的模拟实现】
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; #include <deque> namespace xzc { template<class T, class Container = deque<T>> class Queue { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_front(); } const T& front() { return _con.front(); } const T& back() { return _con.back(); } bool empty() { return _con.empty(); } size_t size() { return _con.size(); } private: Container _con; }; }
5.3【deque的简单说明】
我们可以发现在stack和queue实现的过程中,模板参数容器部分有一个缺省参数deque。
那么什么是deque呢?下面让我们简单的了解一下。
5.3.1【deque的介绍】
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和 删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂。
如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
5.3.2 【deque的缺陷】
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。5.3.3【选择deque作为底层默认容器的原因】
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器。
主要是因为:
1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。5.4【优先级队列】
5.4.1【priority_queue的介绍】
本质上优先级队列体现的是堆的思想
1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素pop_back():删除容器尾部元素
5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。5.4.2【priority_queue的使用】
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用优先级队列(priority_queue)。
注意:
默认情况下priority_queue是大堆。void TestPriorityQueue() { // 默认情况下,创建的是大堆,其底层按照小于号比较 vector<int> v{ 3,2,7,6,0,4,1,9,8,5 }; priority_queue<int> q1; for (auto& e : v) q1.push(e); while (!q1.empty()) { cout << q1.top() << " "; q1.pop(); } cout << endl; // 如果要创建小堆,将第三个模板参数换成greater比较方式 priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end()); while (!q2.empty()) { cout << q2.top() << " "; q2.pop(); } } void Test_priority_queue() { priority_queue<int> pq; pq.push(1); pq.push(2); pq.push(3); pq.push(4); pq.push(5); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } }
priority_queue的本质是堆,而默认情况下为大堆。可加入greater<int>类型参数进行修改使其变为小堆。
5.4.3【priority_queue的模拟实现】
参考资料:
STL详解(九)—— priority_queue的使用与模拟实现_priority_queue vector-CSDN博客
1.【priority_queue.h】
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; #include <vector> #include <stdbool.h> namespace xzc { template<class T> class Less { public: bool operator()(const T& x, const T& y) { return x < y; } }; template<class T,class Compare=Less<T>,class Container = vector<T> > class priority_queue { public: priority_queue() { } template<class InputIterator> priority_queue(InputIterator first, InputIterator end) :_con(first,end) { for (size_t i=(_con.size() - 2) / 2; i >= 0; --i) { AdjustDown(i); } } void AdjustUp(size_t child) { Compare compare; int parent = (child - 1) / 2; while (child > 0) { if(compare(_con[parent],_con[child])) { swap(_con[child], _con[parent]); child = parent; parent = (child - 1) / 2; } /*if (_con[child] > _con[parent]) { swap(_con[child], _con[parent]); child = parent; parent = (child-1) / 2 ; }*/ else { break; } } } void AdjustDown(size_t parent) { Compare compare; int child = parent * 2 + 1; while (child < _con.size()) { if (child + 1 < _con.size() && _con[child + 1] > _con[child]) { child++; } if (compare(_con[parent], _con[child])) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; } /*if (_con[child] > _con[parent]) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; }*/ else { break; } } } void push(const T& x) { _con.push_back(x); AdjustUp(_con.size()-1); } void pop() { swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); AdjustDown(0); } const T& top() { return _con[0]; } bool empty() { return _con.empty(); } size_t size() { return _con.size(); } private: Container _con; }; }
2.【Test.c】
#include "priority_queue.h" class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} bool operator<(const Date& d)const { return (_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day); } bool operator>(const Date& d)const { return (_year > d._year) || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day); } friend ostream& operator<<(ostream& _cout, const Date& d); private: int _year; int _month; int _day; }; ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << "-" << d._month << "-" << d._day; return _cout; } template<class T> class Less { public: bool operator()(const T& x, const T& y) { return x < y; } }; template<class T> class Less<T*>//模板特化,当参与比较的是指针指向的对象时,将指针转换为对应对象进行比较 { public: bool operator()(T* x, T* y) { return *x < *y; } }; void test_priority_queue() { //内置类型 xzc::priority_queue<int, vector<int>, Less<int>> pq; // 大堆 //xzc::priority_queue<int,vector<int>,Greater<int>> pq; // 小堆 pq.push(3); pq.push(1); pq.push(5); pq.push(4); pq.push(2); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; //自定义类型 // 大堆,需要用户在自定义类型中提供<的重载 xzc::priority_queue<Date> q1; q1.push(Date(2018, 10, 29)); q1.push(Date(2018, 10, 28)); q1.push(Date(2018, 10, 30)); cout << q1.top() << endl; // 如果要创建小堆,需要用户提供>的重载 xzc::priority_queue<Date, vector<Date>, greater<Date>> q2; q2.push(Date(2018, 10, 29)); q2.push(Date(2018, 10, 28)); q2.push(Date(2018, 10, 30)); cout << q2.top() << endl; //当为指针指向的自定义类型时,由于指针为内置类型,所以要使用模板特化来解决 xzc::priority_queue<Date*, vector<Date*>,Less<Date*>> q3; q3.push(new Date(2018, 10, 29)); q3.push(new Date(2018, 10, 28)); q3.push(new Date(2018, 10, 30)); cout << *(q3.top()) << endl; } int main() { test_priority_queue(); return 0; }
5.5【容器适配器】
5.5.1【什么是适配器】
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。5.5.2 【STL标准库中stack和queue的底层结构】
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:
【STL总结】
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。