C++--模板&STL(1)

本文详细介绍了C++中的模板函数模板和类模板,包括它们的调用原理与匹配规则,重点剖析了STL中的容器如string、Vector和list的使用、方法和实现。同时讨论了迭代器及其在STL中的应用,以及适配器的作用。
摘要由CSDN通过智能技术生成

目录

一.模板

1.1函数模板

1.1.1模板调用过程

1.1.2模板的调用原理

1.1.3模板参数的匹配原则

1.2类模板

1.2.1类模板的定义

 1.2.2类模板的实例化

二.STL

2.1STL的概念

2.2STL的六大组件

2.3常用容器1--string

2.3.1string的基本概念

2.3.2string的迭代器

2.3.3string方法

2.3.4深拷贝实现

2.3.5string类的实现

2.4常用容器2--Vector

2.4.1Vector基本概念

2.4.2Vector方法

2.4.3Vector模拟实现

2.4.4Vecotr迭代器失效

2.5常用容器3--list

2.5.1list容器的概念

2.5.2list常用接口方法

2.5.3list迭代器失效

2.5.4list容器的模拟实现

2.6,2.7迭代器和适配器 (在另一篇我的博客里)


一.模板

1.1函数模板

函数模板是通用类型的代码,是通用的函数描述,也就是说他们用泛型(泛型可具体指类型 )来定义函数。
举个栗子:

//并不是一个真正的函数,他只是一个模板
template<typename T>  //模板参数列表--告诉编译器T是一种类型
T Add(T left,T right){//参数列表
    cout<<typeid(T).name()<<endl;  //查看类型 
    reutrn left+right;
}

建立一个模板并将模板命名成T。关键字template和typename是必须的(在c++98添加temp那么之前,一直使用class里创建模板。)模板并不创建任何函数,只是告诉编译器如何定义函数。
在编写模板的过程中,编写的模板可能无法处理某些类型,因此就要为特定类型提供具体化的模板定义。

1.1.1模板调用过程

模板被调用的过程就是实例化的过程,分为隐式实例化和显示实例化。

  1. 隐式实例化:代码在运行之前,现需要编译,在编译阶段,对模板如果是隐式实例化编译器会对传递的实参的类型进行推演,然后确定模板参数列表中T的实际类型,确定出来后,然后再根据确定的类型生成实际类型的代码。
  2. 显示实例化:在函数名后的<>中指定模板参数的实际类型称之为显示转换。会直接指定模板参数列表中T的类型,编译器在编译代码期间,可以直接根据用户所提供的T类型生成处理具体类型,并且且生成代码后,可能回对用户传递的实参进行隐式转化。
//隐式转换与显示转转换
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}

Add(1,2.0);//隐式转换,编译器会报错
//因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
 //通过第一个将T推演为int,通过第二个实参将T推演为double类型,但模板参数列表中只有一个T,
 //编译器无法确定此处到底该将T确定为int 或者 double类型而报错
//改变的方式:
//1.强转
//2.可以在模板参数列表再加上一种类型,(在使用typename和class其实两个都可以)
template<typename T,typename S> 
//3.对函数模板进行显示实例化,对其直接指明是什么类型
Add<int>(1,2.0);//显示转换

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功,编译器会报错。 

1.1.2模板的调用原理

函数模板是一个蓝图,它本身并不是函数,是编译器使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。

1.1.3模板参数的匹配原则

  • 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

  • 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板
  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

1.2类模板

我们在之前写代码的时候,会使用typedef处理各种需求,然而之中方法有两个缺点:首先是每次修改类型时都需要编辑头文件,其次实在每个程序中只能使用这种技术生成一种栈,既不能让typedef同时代表两种不同的类型。因此C++类模板为生成通用类声明提供了好的方法。他不是具体的类,是编译器根据实例化结果生成具体类的模板。

1.2.1类模板的定义

//定义格式
template<class T1, class T2, ...., class Tn>
class 类模板名
{
    //类内成员定义
};

举个栗子:动态顺序表,在这里我们要注意,vector不是具体的类,而是编译器根据被实例化的类型生成具体类的模具。

template<class T>
class Vector
{
 public: 
        Vector(size_t capaticy = 10)
              :_pData(new T[capacity])
              ,_size(0)
              ,_capacity(capacity)
            {}
        //使用析构函数演示:在类中声明,在类外定义。
        ~Vector();
        void PushBack(const T& data);
        void PopBack();
        //...
        size_t Size() {
            return _size;
        }
        T& operator[](size_t pos){
            assert(pos < _size);
            return _Pdata[pos];
        }
 private:
        T* _pData;
        size_t _size;       
        size_t _capacity;
};
//注意:类模板中函数放在类外进行定义的时候,需要加上模板参数列表
template <class T>
Vector<T>::~Vector(){
    if(_pDate)
        delate[] _pData;
        _size = _capacity = 0;
}

 1.2.2类模板的实例化

模板的具体实现被称之为实例化或者具体化。不能将模板成员函数放在对立的实现文件当中。模板不是函数,不能单独编译,必须将模板与其特定的模板实例化请求一起使用。类模板实例化与函数模板实例化不同,类模板实例化需要在类模板明知后面跟上<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

仅在程序包含模板是不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。

//Vector列明,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

二.STL

2.1STL的概念

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且 是一个包罗数据结构与算法的软件框架。通俗的说,就是将常见的数据结构以模板的方式进行封装,还增加了一些通用的灵活的算法。(通用的体现:模板,与类型无关;find,任意类型数据结构组织数据都可以查找,与具体类型无关)(灵活地体现:让用户选择定制算法的功能)STL的一个重要特点是数据结构和算法分离。另一个重要特点是他不是面向对象的。STL的核心思想在于将容器和算法分开,彼此独立设计,最后再重新组合在一起。

2.2STL的六大组件

 STL中有六大组件,我们可以理解为有六个模块,这六个模块不是相互独立的。

  1. 容器:容器是一种数据结构,是用来管理数据的,就是对常见数据结构进行封装。常用的数据结构:数组,链表,树,栈,队列,集合,映射表。(a.序列式容器:每个元素都有固定的位置,取决于插入的时机和地点,和元素无关;b.关联式容器元素位置取决于特定的排序准则,和插入顺序无关)
    容器种类
    序列式容器Vector将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),设计组的头部和尾部添加或者移除元素都十分快速,但是在中部或者头部插入元素十分费时。
    string是动态类型顺序表,字符串是表示字符序列的类,标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作 单字节字符字符串的设计特性。string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型。
    Deque是“double-ended queue”的缩写,可以随机存取元素(用索引直接存取),数组头部和尾部添加或移除元素都非常快速。但是在中部或头部安插元素比较费时。
    List双向链表,不提供随机存取,在任何位置上执行插入和删除动作都十分迅速,内部只需调整一下指针。
    关联式容器Set/Multiset内部的元素依据其值自动排序,Set内的相同数值的元素只能出现一次,Multisets内可包含多个数值相同的元素,内部由二叉树实现,便于查找。
    Map/MultimapMap的元素是成对的键值/实值,内部的元素依据其值自动排序,Map内的相同数值的元素只能出现一次,Multimaps内可包含多个数值相同的元素,内部由二叉树实现,便于查找。
  2. 迭代器:它是一种抽象的概念,是一种设计模式。提供了访问容器种对象的方法,是只能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
    迭代器的种类
    迭代器功能描述
    输入迭代器提供对数据的只读访问只读,支持++,++,!=
    输出迭代器提供对数据的只写访问只写,支持++
    前向迭代器提供读写操作,并能向前推进迭代器读写,支持++,++,!=
    双向迭代器提供读写操作,并能向前和向后操作读写,支持++,-
    随机访问迭代器提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器读写,支持++,-,[n],-n,<,<=,>,>=
  3. 算法:是用来操作容器中的数据的模板函数。函数本身与他们操作的数据的结构和类型无关,因此他们可以再从简单数组到高度复杂容器的任何数据结构上使用。
  4. 适配器:标准库提供了三种顺序容器适配器:queue(FIFO队列)、priority_queue(优先级队列)、stack(栈)
  5. 仿函数(函数对象),用来设计算法的功能,让算法变得更加灵活。
  6. 空间配置器:在STL底层用来管理空间的申请,释放。 

2.3常用容器1--string

2.3.1string的基本概念

  1. string是表示字符串的字符串类。,char*是一个指针,string是一个类,string封装了char*,管理这个字符串面试一个char*型的容器。
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string string。
  4. 不能操作多字节或者变长字符的序列。

2.3.2string的迭代器

迭代器主要是用来遍历容器的,一般情况下遍历string是直接使用for下或者范围for,现在我们可以使用以下的函数进行操作:

begin+endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭 代器
rbegin+rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭 代器

具体操作如下:

void test(){
    string s("hello");
    
    //使用正向迭代器来进行遍历
    string::iterator it = s.begin();
    while(it != s.end()){
        cout<< *it;
        ++it;
    }
    cout<<endl;
    //使用反向迭代器
    string::reverse_iterator rit = s.rbegin();
    while(rit !=rend()){
        cout<<*rit;
        ++rit;//此时指针向前走
    }
    cout<<endl;
}

2.3.3string方法

1.string构造方式与遍历方式,如下:

//构造
string s1("hello world");
string s2(10,'a');
string s3(s2);//拷贝构造
char* p = "ellow world";
string s5(p,p+5);//区间的方式进行构造

2.string类对象的容量操作:

函数名称功能说明
size返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty检测字符串释放为空串,是返回true,否则返回false
clear清空有效字符
reserve为字符串预留空间
resize将有效字符的个数该成n个,多出的空间用字符c填充
  • 检测size(),capacity(),clear(),empty(),length函数 
    void test(){
    	string s("hello");
    	//查看有效元素的个数
    	cout << s.size() << endl;
    	//底层空间最大存放对少
    	cout << s.capacity() << endl;
        //输出字符串的有效长度长度
        cout << s.length() << endl;
    
    	//判断是否为空
    	if(s.empty()){
    		cout << "true:空!" << endl;
    	}else{
    		cout << "false:不空!" << endl;
    	}
    	//清空
    	s.clear();
    }
    注意:size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本使用size()。clear()只是将string中有效字符清空,不改变底层空间大小。
  • rsize()函数   
    void test(){
    	string s("hello");
    	//扩容 or 减少
    	resize(size_t n, char ch);//将有效元素增加到n个,对出的空间,用ch补充
    	resize(size_t n);//将有效空间增加到n,多出的空间用默认值0补充
    }
    在这里我们要注意,元素增多时可能会扩容,将元素个数减少时,容量是不改变的。当n>old_capacity,此时进行扩容,开辟新空间,拷贝元素,释放就空间;n<old_capacity,此时进行容量减少,当n>15时,不会将容量缩小,n<=15时,将空间释放掉,直接使用内部固定大小(在VS2013下进行的检测)
  • reverse()函数   
    void test(){
    	string s("hello");
    	s.reserve(10);//将空间进行扩容到10 
    }
    注意,当参数大于底层空间的时候会发生扩容,在扩容的时候有有效元素的个数不会发生改变。缩小的时候以15为分界线,在大于15的时候,reverse在将容量缩小的时候,底层的容量不会发生变化,当小于等于15的时候,reverse才会真正将空间总的容量进行修改。(是15的原因:因为在VS2013环境下,内部还会有固定的长度为char类型的数组:char xxx[16];当其修改时小于15的时候,就会将原有的空间释放掉,然后利用他本身固有的数组)
  • at()函数 :at函数检测是否有越界  
    void test(){
    	string s("hello");
    	s.reserve(10);//将空间进行扩容到10 
    	cout << s.at(100) << endl;
    	//此时会报出异常
    }

3.string的访问及遍历方法: 

函数名称功能说明
operator返回pos位置的字符,const string类对象调用
begin+endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭 代器
rbegin+rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭 代器
范围forC++11支持更简洁的范围for的新遍历方式

 代码展示:

void Test()
{
 string s("hello");
 // 3种遍历方式:
 // 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
 // 另外以下三种方式对于string而言,第一种使用最多
 // 1. for+operator[]
 for(size_t i = 0; i < s.size(); ++i)
 cout<<s[i]<<endl;

 // 2.迭代器
 string::iterator it = s.begin();
 while(it != s.end())
 {
 cout<<*it<<endl;
 ++it;
 }

 string::reverse_iterator rit = s.rbegin();
 while(rit != s.rend())
 cout<<*rit<<endl;

 // 3.范围for
 for(auto ch : s)
    cout<<ch<<endl;
}

 4.string类对象的修改操作:

函数名称

功能

push_back

在字符串后尾插字符c

append

在字符串后追加一个字符串

operator+=

在字符串后追加字符串str

insert在任意位置进行插入字符串

c_str

返回C格式字符串

erase任意位置字符串的删除操作

find+npos

从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

rfind

从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置

substr

在str中从pos位置开始,截取n个字符,然后将其返回

代码展示: 

//追加操作,插入操作,删除操作
void test(){
	string s;
    s += "hello";
    s += " ";
    string s1 = ("lllll");
    s = s.append(3,'!');//可以在s后面追加n个字符!
    s = s.append(s1, 1, 2);//可以给s字符串之后追加s1的1下标位置之后的2个字符串
  

  //在起始位置插入字符串
    s.insert(0, "abc ");
    //在某个位置插入字符
    s.insert(2,4,'a');//在2下标后的位置插入4个a

    //在任意位置进行和删除
    s.erase(0, 5);//在起始位开始向后置删除5个字符
    //也可以通过迭代器的方式给出位置
    s.erase(s.begin());//删除起始位置的字符
    s.erase(s.begin(),s.end());//从开始删除到末尾
}

//特殊操作
void test(){
    //将string类型的字符串转成char*类型
    string s("12345");
    atoi(s.c_str());//先将s转化成char*类型;在利用atoi()将其转化成整形
 

    //获取文件的后缀
    string file("string.cpp");
    string postFix = file.substr(file.find('.')+1);//先获取字符.后面的字符位置,在获取这个位置之后的字符串
    //获取文件的名字
    string file2("F:\\work\\string.cpp");
    string name = file2.substr(file2.rfind('\\')+1,file2.rfind('.')-(file2.rfind('\\')+1));
    //从file2的末尾位置开始查找,找到'\\'后面的位置,
    //再找到所找位置到'.'的字符数,进行输出
}

 string在自动插入的时候会有自己的扩容机制,再VS下,扩容是按照1.5倍进行扩容,再linux下是按照2倍进行扩容。

2.3.4深拷贝实现

 在这里我们发现了一个问题:如果一个类中没有显示提供拷贝构造函数,则编译器会生成一份默认的拷贝函数,拷贝方式是将一个对象中的内容原封不动的拷贝到另一个对象中,这是浅拷贝。但是因为String中涉及到了资源管理,如果使用编译器生成默认拷贝构造函数,最后就会导致多个对象在底层使用同一份资源,导致的结果就是多个对象在销毁时,会将同一份资源释放多次而导致代码崩溃。

1.普通写法:

class String{
public:
	String(const char* str = ""){//构造函数
		if(nullptr == str){//先检测用户给出的字符串是否为空
			str = "";
			//这样无论如何str都有一个合法的指针--str一定是一个合法的字符串
		}
		//开辟空间
		_str = new char[strlen(str)+1];
		//将str里的字符串往当前对象里面进行拷贝
		//strcpy(_str, str);浅拷贝相当于拷贝的是对象本身的内容
	}
    //解决浅拷贝的方式是使用深拷贝,深拷贝拷贝的是对象中管理资源中缩放的数据
    //如果一个类中涉及到资源管理时,需要用户显示提供拷贝构造
   
    //深拷贝方式:拷贝构造
    String(const String& s):_str(new char[strlen(s._str)+1]){
    //给新对象重新开辟空间
        strcpy(_str,s._str);
    }
    //如果没有显式实现赋值运算符重载,会造成内存泄漏而且会在销毁时因为资源释放多次而造成代码崩溃
    String& operator=(const String& s){
		if (this != &s){
			delete[] _str;//将当前的空间释放掉
			_str = new char[strlen(s._str) + 1];//重新开辟一个空间,空间大小为s字符串长度+1
			strcpy(_str, s._str);//然后将s中的字符串往当前空间进行拷贝

			/*另一种方法,两个效果一样
            char* temp = new char[strlen(s._str) + 1];
			strcpy(temp, s._str);
			delete[] _str;
			_str = temp;
            */
		}

		return *this;
	}
    ~String(){//因为对象里面管理了资源,所以一定要提供析构函数
		if (_str){
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

2.简洁写法:

class String{
public:
	String(const char* str = ""){
		if (nullptr == str)
		{
			str = "";
		}

		// 无论如何str都有一个合法的指针--str肯定是一个合法的字符串
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	// 深拷贝方式:拷贝构造
	String(const String& s)
		: _str(nullptr){//此时如果没有给其赋值=空,那么就是随机值,
        //那么在将临时对象中指针与当前对象中指针进行交换后,temp._str中存放的是随机值
        //在构造函数结束之前要进行销毁,就必须释放掉空间,当试图去释放时就会崩溃
		String temp(s._str);//创建1个临时对象,需要调用构造函数来进行构造
		swap(_str, temp._str);//将当前对象与temp的底层空间进行交换,此时相当于temp._str为空,他就不需要释放空间
	}

	String& operator=(String s){
		swap(_str, s._str);
		return *this;
	}

	~String(){
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

2.3.5string类的实现

class string{
public:
	//迭代器相关的
	typedef char* iterator;
public:
	//构造相关的
	string(const char* str=""){//构造空的对象
 		_size = strlen(str);
		 _capacity = _size;
	 	_str = new char[_capacity+1];
 		strcpy(_str, str);
	 }
	string(const string& s): _str(new char[s._capacity+1]){//拷贝构造
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	string(size_t n,char ch):_str(new char[n+1])
							,_capacity(n)
							,_size(n){//用n个字符ch的方法来进行构造
		memset(_str,ch,n);
		_str[_size] = '\0';
	}
	string& operator=(const string& s){//赋值运算符重载
		if(this != &s){
			char* temp = new char[strlen(s._str)+1];
			strcpy(temp, s_str);
			delete[] _str;
			_str = temp;
			_size = s._size;
			_capacity = s._capacity;
		}
	}
	~string(){
		if(_str){
			delete[] _str;
			_str = nullptr;
			_capaticy = 0;
			_size = 0;
		}
	}
public:
	//迭代器
	iterator begin(){
		return _str;
	}
	iterator end(){
		return _str + _size;
	}
public:
	//容量相关的
	size_t size()const{
		return _size;
	}
	size_t capacity()const{
		return _capacity;
	}
	bool empty()const{
		return 0 == _size;
	}
	void reverse(size_t newcapacity){
		size_t oldcapacity = capacity();
		if(_capacity() > oldcapacity){
			//说明底层空间增加了
			//开辟新空间
			char* str = new char[newcapacity+1];
			//拷贝元素
			strcpy(str,_str);
			//释放旧的空间
			delete[] _str;
			//使用新得空间
			_str = str;
			_capacity = newcapacity;
		}	
	}
	void resize(size_t newsize,char ch){//将对象中的元素个数进行修改,多出的个数用ch填充
		size_t oldsize = size();
		if(nesize() <= oldsize){
			//说明减少了
			_size = newsize;
		}else{
			size_t cap = _capacity();
			if(newsize > cap){//如果增加的size大于底层开辟的空间,那么就需要对底层空间增加
				reverse(newsize * 2);	
			}
			memset(_str + _size, ch, newsize-oldsize);	
		}
		_str[_size] = '\0';
		_size = newsize;
	}

	//元素访问
	char& operator[](size_t index){
		assert(index < _size);
		return _str[index];
	}
public:
	//modigy
	void push_back(char ch){
		*this += ch;
	}
	void append(char ch){
		push_back(ch);
	}
	void append(string s){
		*this += s;
	}
	string& operator+=(char ch){
		//先考虑空间的大小
		if(_size == _capacity){
			//此时,size=capacity的时候,在进行添加,需要扩容
			reverse(2 * _capacity);
		}
		//进行添加
		_str[_size] = ch;
		_str[_size+1] = '\0'
		size += 1;
		return *this;
	}
	string& operator+=(string s){
		size_t newsize = s._size + _size;
		if(newsize > _capacity){
			reverse(2 * newsize);
		}
		strcat(_str+_size, s._str);
		_size = newsize;
		_str[_size] = '\0';
		return *this;
	}

	void swap(string& s){
		std::swap(_str,s._str);
		std::swap(_size,s._size);
		std::swap(_capacity,s._capacity);
	}
	void clear(){
		_size = 0;
		_str[_size] = '\0';
	}


	//其他操作
	const char* c_str()const{
		return _str;
	}
	size_t find(char ch,size_t pos = 0){
		if(pos >=  _size){
			return npos;
		}
		for(size_t i = pos; i< _size; i++){
			if(_str[i] == ch){
				return i;
			}
		}	
		return npos;
	}
	size_t rfind(char ch,sizr_t pos = npos){//给成默认值
		if(pos >= _size){
			return npos;
		}
		for(size_t i = pos; i >=0; --i){
			if(_str[i] == ch){
				return i;
			}
		}
		return npos;
	}

	string substr(size_t pos = 0, size_t n = npos){
		assert(pos < _size);
		size_t len = strlen(_str + pos);
		n = min(len , n);//取最小的,因为若是超过了len会造成越界,因此取最小
		string temp;
		temp.rsize(n+1,0);
		strncpy(temp._str,_str+pos,n);
		return temp;
	}
	friend ostream& opsrator<<(ostream& _cout,const string& s){
		_cout << s._str;
		return _cout;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	statoc size_t npos;
};
size_t string::npos = -1;

2.4常用容器2--Vector

2.4.1Vector基本概念

vector是表示可变大小数组的序列容器。 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是 一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存 储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是 对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。

2.4.2Vector方法

1.Vecotr定义

构造函数说明接口说明
vector()无参构造
vector(size_type n, const value_type& val = value_type())构造并初始化n个val
vector (const vector& x);拷贝构造
vector (InputIterator first, InputIterator last);使用迭代器进行初始化构造

#include<vector>

void test(){
	vecotr<int> v1;
	vector<int> v2(10,5);
	int array[] = {1,2,3,4};
	//区间构造
	vector<int> v3(array,array + sizeof(array)/sizeof(array[0]));
	//拷贝构造
	vector<int> v4(v3);
	vector<int> v5{1,2,3,4};
    //使用迭代器
    int myints[] = {16,2,77,29};
    vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );
    for(vector<int>::iterator it = fifth.begin();it!=fifth.end();++it){
        cout<<' '<<*it;
        cout<<'\n';
    }
}

2.Vecotr迭代器的使用

iterator的使用

接口说明

begin+end

获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置 的iterator/const_iterator
rbegin+ rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator
//迭代器进行打印

void test(){
	vecotr<int>::iterator it = v.begin();
	//1.
	while(it != v.end()){
		cout<<*it<<" ";
		++it;
	}

	//2.
	auto it = v.begin();
	while(it != v.end()){
		cout<<*it<<" ";
		++it;
	}
}

3.Vecotr空间

容量空间接口说明
size获取数据个数
capacity获取容量大小
empty

判断是否为空

resize改变vector的size

reserve

改变vector放入capacity
//空间容量操作
void test(){
	vector<int> a(5,5);
	size_t cap = a.capacity();//获取容量大小
	size_t size = a.size();//获取数据个数

	//reserve
	a.reverse(20);//将空间容量增大到20
	a.resize(2);//改变a的size的值,如果小于,则将多出的直接销毁
	a.rseize(10,6);//如果大于原来的size,则将其对出的个数用6进行填补,如果没有给出val值,则将使用默认值填充
	if(a.empty() == 0){
		cout<<"此时是空的"<<endl;
	}
}

4.Vecotr增删改查操作

vector增删改查操作接口说明
push_back尾插
pop_back尾删
find查找
insert在position之前插入val
erase删除position位置的数据
swap交换两个vector的数据空间
operator[](size_t index)像数组一样访问(注意:如果index越界,assert触发)
 //insert在所给位置之前插入给定的值
iterator insert(iterator pos, const T& x);//在这个位置上插入x,返回值是一个迭代器
void insert(iterator pos,size_t n,const T& x);//给n个x插入到pos位置之前
void insert(iterator pos,iterator first,iterator last);//[first,last)将一个区间进行插入,所以必须使用迭代器
iterator erase(iterator pos) ;//返回的含义:将pos位置的元素删除后,让pos后一个元素位置返回来
iterator erase(iterator first,iterator last);//[first,last)
//operator[]+index 和vector中新式for+auto的遍历
void test(){
	int a[] = {1,2,3,4};
	vector<int> v(a,a+sizeof(a)/sizeof(a[0]));
	//通过[]遍历
	for(size_t i = 0;i < v.size(); ++i){
		cout<< v[i] <<endl;
	}
	//通过新式范围for遍历;注意,index越界,会抛出异常,out_of_range
	for(auto e : v)
		cout<< e <<" ";
	cout<<endl;


	//尾插和尾删    
    v.push_back(5);//在v后面插入5
    v.pop_back();//删除v的最后一个元素
    

    //在pos之前位置上进行插入
    veoctor<int>::inerator pos = fins(v.begin(),v.end(),3);//使用find查找3所在的位置的iterator
    v.insert(pos, 5);//在pos位置插入5
  

    //在pos位置进行删除
    v.erase(pos);
    //单个元素删除
    veoctor<int>::iterator it = v.begin();
    //auto it = v.begin();
    while(it !=v.end()){
        v.erase(it);
        ++it;
    }
}

 5.创建二维数组

void test(){
	vector<vector<int>> vv;
	vv.resize(5);    
    //方式1
	for(size_t i = 0;i < 5; ++i){
		vv[i].resize(6);
		for(size_t j = 0; j < 6; j++){
			vv[i][j] = j+1;
		}
	}
    //方式2
    vv.resize(5,vector<int>{1,2,3,4,5,6});
    //方式3
    vector<vector<int>> vv(5,vector<int>{1,2,3,4,5,6});
	//遍历
	for(size_t i = 0; i < vv.size();++i){
		for(sizt_t j = 0; j < vv[i].siez(); ++j){
			cout<< vv[i][j]<<" ";
		}
		cout << endl;
	}
}

2.4.3Vector模拟实现

vector就是动态类型的顺序表。

#include <iostream>
#include <assert.h>
using namespace std;
template<class T>
class vector{
public:
    
	// vector的迭代器实际就是:原生态的指针
	typedef T* iterator;

public:
	vector()
		: start(nullptr)
		, finish(nullptr)
		, endofstorage(nullptr){}

	vector(int n, const T& data = T())
		: start(new T[n])
		, finish(start + n)
		, endofstorage(finish){
		for (int i = 0; i < n; ++i){
			start[i] = data;
		}
	}

	// 区间构造
	// vector<int>  v2(10, 5);  // int  int
	template<class Iterator>
	vector(Iterator first, Iterator last){//区间构造
		size_t n = 0;
		auto it = first;
		while (it != last){
			n++;
			++it;
		}

		start = new T[n];
		finish = start + n;
		endofstorage = finish;
		for (size_t i = 0; i < n; ++i){
			start[i] = first[i];
		}
	}

	vector(const vector<T>& v) :start(new T[v.size()]){//拷贝构造
		for (int i = 0; i< v.size(); i++){
			start[i] = v[i];
		}
	}
    vector<T>& operator=(const vector<T>& v){
        if(this !=v){
            delete[] start;
            start = new T[v.size()];
            for (int i = 0; i< v.size(); i++){
			    start[i] = v[i];
		}
        }
    }

	~vector(){
		if (start)
{
			delete[] start;
			start = finish = endofstorage = nullptr;
		}
	}
public:
	//与迭代器相关的操作
	iterator begin()
	{
		return start;
	}

	iterator end()
	{
		return finish;
	}

	//与容量相关的操作
	size_t size()const{
		return finish - start;
	}

	size_t capacity()const{
		return endofstorage - start;
	}

	bool empty()const{
		return start == finish;
	}
    
    void resize(size_t newsize,const T& data = T()){
    size_t oldsize = size();
		// 将有效元素个数减少到newsize个
		if (newsize > oldsize){
			// 将有效元素个数增加到newsize个
			// 1. 考虑是否需要扩容
			if (newsize > capacity())
				reserve(newsize);

			for (size_t i = size(); i < newsize; ++i){
				start[i] = data;
			}
		}

		finish = start + newsize;
    }
    void reserve(size_t newcapcity){
		size_t oldcapacity = capacity();
		if (newcapcity > oldcapacity){//新空间增大,进行扩容
			size_t oldsize = size();
			// 1. 开辟新空间
			T* temp = new T[newcapcity];

			// 2. 拷贝元素--->有些情况下可以,有些情况下不行
			//memcpy(temp, start, oldsize*sizeof(T));
			for (size_t i = 0; i < oldsize; ++i){
				temp[i] = start[i];
			}

			// 3. 释放旧空间
			delete[] start;
			// 4. 使用新空间
			start = temp;
			// 如果此时我们使用的是finish = start + size();有问题
    //因为size()实现中是finish-start,连个指针相加,两个指针所指向的空间应该相同,但是此时不相同
			finish = start + oldsize;
			endofstorage = start + newcapcity;
		}
	}
    
    //元素访问
	T& operator[](size_t index){//访问任意位置元素
		assert(index < size());
		return start[index];
	}

	const T& operator[](size_t index)const{
		assert(index < size());
		return start[index];
	}

	T& front(){//访问首地址元素
		return start[0];
	}

	const T& front()const{
		return start[0];
	}

	T& back(){//范文末尾元素
		return *(finish - 1);
	}

	const T& back()const{
		return *(finish - 1);
	}
    //修改
	void push_back(const T& data){
		if (size() == capacity()){//检测是否需要扩容
			reserve(capacity() * 2 + 3);
		}

		*finish = data;
		finish++;
	}

	void pop_back(){//尾删
		if (empty())
			return;

		--finish;
	}

	iterator insert(iterator pos, const T& data){
		if (size() == capacity()){//任意位置插入
			reserve(capacity() * 2 + 3);
		}

		// 需要将pos位置及其后续所有的元素整体往后搬移
		iterator it = finish;  // it元素要搬移到的位置
		while (it != pos)
		{
			*it = *(it - 1);
			--it;
		}

		*pos = data;
		++finish;
		return pos;
	}
    iterator erase(iterator pos){//删除任意位置元素
		if (pos == end())
			return pos;

		iterator it = pos;   // 元素要搬移到的位置
		while (it != finish){
			*it = *(it + 1);
			++it;
		}

		--finish;
		return pos;
	}

	void clear(){
		// 有问题:存的是对象
		finish = start; 
		// erase(start, last);
	}

	void swap(vector<T>& v){
		std::swap(start, v.start);
		std::swap(finish, v.finish);
		std::swap(endofstorage, v.endofstorage);
	}

	
private:
	iterator start; //利用迭代器类型,相当于int*,指向数组的起始位置
	iterator finish;//执行数组元素的末尾位置
	iterator endofstorage;
};

2.4.4Vecotr迭代器失效

a.什么是迭代器失效

迭代器我们可以分成两种,一种他就是指针,另一种实际上就是将指针重新封装了一种新的类型iterator。迭代器的本质就是指针。迭代器失效实际上就是指针失效,指针失效指的是指针指向的空间不存在了,指针成了野指针,现在使用迭代器就相当于使用一个野指针,因此迭代器失效造成的后果就是程序崩溃。

b.vector在什么情况下失效

  1. 所有插入操作都有可能会引起迭代器失效
    //情况1:所有插入方式都有可能会引起迭代器失效
    void test(){
    	vector<int> v{1,2,3,4,5,6};
    	v.revere(8);
    	auto it = v.begin();  
    
    	v.push_back(7);
    	v.push_back(8);
    	v.push_back(9);//在插入9时需要扩容,此时迭代器会失效
    	//因为在进行扩容时会开辟新的空间,将就空间的数据复制到新空间
    	//并且将就空间进行释放,而it依旧指向就空间,因此就是指向已经释放的空间,因此会造成代码崩溃
        //push_back();insert();rserve();rsize();assgin();=等方法都会引起迭代器失效
    	while(it != v.end()){
    		cout<< *it <<" ";
    		++it;
    	}
    	cout<<endl;
    }
  2. erase(iterator pos):删除pos位置上的元素,删除成功后pos迭代器就失效了。
    //情况2指定位置元素的删除操作erase()
    void(){
    	vector<int> v{1,2,3,4,5,6};
    	//使用find查找3的位置
    	vector<int>::iterator pos = find(v.begin(),v.end(),3);
    	//删除pos位置上的数据会导致pos迭代器失效
    	v.erase(pos);
    	cout<< *pos <<end;//此时会导致非法访问
        //erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,
        //但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的
        //那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效
    了。
    
    }
  3. swap()也会产生得带起失效
void test(){
	vector<int> v1{1,2,3,4,5};
	auto it1 = v1.begin();
	vector<int> v2{6,7,8,9,0};
	auto it2 = v2.begin();
	v1.swap(v2);
	while(it1 != v1.end()){
		cout<< *it1 <<" ";
		++it1;
	}
	//因为v1和v2底层结构都是顺序表,进行交换就是将底层空间进行交换
	//然而此时it1是指向原v1的空间,但是此时v1已经与v2进行了交换,此时指向的是v2的空间
}

c. 解决迭代器失效

在b中1情况下,上述操作完成后,如果想要继续通过迭代器操作vector中的元素,只需要给it重新赋值就行。在b中2的情况下,也是直接用it给其重新赋值。

//情况1:所有插入方式都有可能会引起迭代器失效
void test(){
	vector<int> v{1,2,3,4,5,6};
	v.revere(8);
	auto it = v.begin();  

	v.push_back(7);
	v.push_back(8);
	v.push_back(9);
    it = v.begin();
	while(it != v.end()){
		cout<< *it <<" ";
		++it;
	}
	cout<<endl;
}
//情况2指定位置元素的删除操作erase()
void(){
	vector<int> v{1,2,3,4,5,6};
	vector<int>::iterator pos = find(v.begin(),v.end(),3);
    pos = v.erase(pos);
	cout<< *pos <<end;//此时会导致非法访问

}

2.5常用容器3--list

2.5.1list容器的概念

list是序列容器的一种,序列式是指我们所学的线性数据结构。list的底层是带头结点的双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。list容器的方法list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问。

2.5.2list常用接口方法

1.list的构造方法

构造函数接口说明
list()构造空的list
list (size_type n, const value_type& val = value_type())构造list中包含n个值为val的元素
list (const list<T>& x)构造拷贝函数
list (InputIterator first, InputIterator last)用区间进行构造,区间在(first,last)之间

//构造
void test(){
    list<int> L1;    // 头结点,空的
	list<int> L2(10, 5);

	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> L3(v.begin(), v.end());//区间构造
	list<int> L4(L3);//拷贝构造
	list<int> L5{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    L1 = L5;
}

2.list迭代器的使用

函数声明接口说明
end+begin返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rend+rbegin返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的 reverse_iterator,即begin位置
  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
void test(){
    list<int> L1;  
	list<int> L5{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	L1 = L5;
	auto it = L5.begin();//利用迭代器进行遍历
	while (it != L5.end()){
		cout << *it << " ";
		++it;
	}
	cout << endl;
    // e实际就是L1链表中每个节点值域的一份拷贝
	for (auto e : L1)
		cout << e << " ";
	cout << endl;
	auto rit = L5.rbegin();
	while (rit != L5.rend()){
		cout << *rit << " ";
		rit++;
	}
}

 3.list增删改查访问等操作

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
sizelist的有效节点
resize增加list中的值,并且值为val
front返回一个结点中的值得引用
back

返回最后一个结点的值得引用

push_front在list首元素之前加入val值
pop_front删除list首元素
push_back

在list末尾插入val

pop_back删除尾元素
insert在任意位置插入元素
erase在任意位置删除元素
swap交换两个list中的元素
clear清空list中的所有元素
unique删除重复值(这组数据必须是有序的)
remove将所有值为val的元素全部进行删除
void test(){
    list<int> L1;
    if (L1.empty()){//检测list中的元素是否为空
		cout << "list is empty!!!" << endl;
	}
	else{
		cout << "list is not empty!!!" << endl;
	}
    list<string> S1;
    cout << s.size() << endl;//查看list中的有效结点
    S.resize(5,"hello");//将list中的有效结点扩大到5个,并且用hello进行填充
    L1.push_front(2);//在L1的首元素位置插入2
    L1.push_front(1);//在L1的首元素位置插入1
    L1.push_front(0);//在L1的首元素位置插入0
    L1.push_back(3);//在L1的尾部位置插入3
    L1.push_back(4);//在L1的尾部位置插入4
    L1.pop_front();//删除首元素
    L1.pop_back();//尾删
    L1.front() = 0;//将首元素修改成0,一般情况下不这样进行使用
    //在任意位置进行插入
    auto it = find(L1.begin(), L1.end(), 2);
    if(it != L1.end()){//进行插入
        //插入单个元素,在元素2的位置插入元素0
        L1.insert(it,0);//在it位置进行插入0
        PrintList(L1);//再将结果进行打印
        //在任意位置插入一段区间
        int array[3] = {4,5,6};
        L1.insert(L.end(),array,array+sizeof(array)/sizeof(array[0]));
        PrintList(L1);//再将结果进行打印
    }
    //任意位置的删除,删除单个元素,再删除元素的时候,有可能会造成迭代器失效
    L1.erase(it);//it位置的删除
    //++it;这一步代码会崩溃
    //删除一段区间
    L1.erase(L1.begin(),L1.end());//相当于clear
    list<int> L2 = {1,3,6,9,2,4,6,8,1,3,6,9};
    L2.sort();//对L2进行排序
    L2.unique();//删除L2中的重复值
    L2.reverse();//对L2进行逆置
    list<int> L3 = {1,1,1,2,3,4,5,6,7};
    L3.remove(1);//将值为1的数据全部进行删除
    //他的是现实将L3拿到的值传递给fun这个方法,然后判断是否需要删除
    L3.remove_if(fun);//fun是传进的函数,函数实现了你所想删除的什么数据的方法
}

2.5.3list迭代器失效

失效的情况是指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

void test(){
	list<int> L{1,2,3,4,5};
	//情况1
	auto it = L.begin();
	L.erase(it);
	++it;
	//情况2
	L.assign(10,5);//将L中的10个有效元素用5填充
	it++;
	//情况3
	list<int> L1 = {9,8,7,6,5,4};
	L.swap(L1);//交换L与L1的有效元素
	while(it != L.emd()){
		cout<< *it << endl;
		++it;
	}
}

2.5.4list容器的模拟实现

//模拟实现
template<class T>
struct ListNode{//节点的定义
	ListNode(const T& x = T())
		: next(nullptr)
		, prev(nullptr)
		, data(x){}

	ListNode<T>* next;
	ListNode<T>* prev;
	T data;
};


template<class T>
struct ListIterator{
	typedef ListNode<T> Node;
	typedef ListIterator<T> Self;

public:
	// 0. 构造
	ListIterator(Node* n)
		: node(n){}
	// 1. 迭代器要能够移动 ++、--
	Self& operator++(){//前置++
		node = node->next;
		return *this;
	}
	Self operator++(int){//后置++
		Self temp(*this);
		node = node->next;
		return temp;
	}
	Self& operator--(){
		node = node->prev;
		return *this;
	}
	Self operator--(int){
		Self temp(*this);
		node = node->prev;
		return temp;
	}
	// 2. 能够解引用
	T& operator*(){
		return node->data;
	}
	T* operator->(){
		return &(node->data);
	}
	// 3. 迭代器要能够比较
	bool operator!=(const Self& s){
		return node != s.node;
	}
	bool operator==(const Self& s){
		return node == s.node;
	}
	Node* node;
};
template<class T>
class list{
	typedef ListNode<T> Node;
public:
	typedef ListIterator<T> iterator;
private:
	void CreadHead(){//创建头指针
		head = new Node;
		head->next = head;
		head->prev = head;
	}
public:
	list(){//构造空的list
		CreadHead();
	}
	list(int n, const T& data){//n个值为data,进行构造
		CreadHead();//先创建空链表
		for (int i = 0; i < n; ++i)
			push_back(data);//插入
	}
	template<class Iterator>//告诉编译器他是一个模板类型
	list(Iterator first, Iterator last){
		CreadHead();
		while (first != last){
			push_back(*first);
			++first;
		}
	}
	list(const list<T>& L){//拷贝构造
		CreadHead();
		auto it = L.begin();
		while (it != L.end()){
			push_back(*it);
			++it;
		}
	}
	~list(){//析构函数
		clear();
		delete head;
		head = nullptr;
	}
	//迭代器的操作
	iterator begin(){
		return iterator(head->next);
	}
	iterator end(){
		return iterator(head);
	}
	//容量相关的
	size_t size()const{
		size_t count = 0;
		Node* cur = head->next;
		while (cur != head){//在双向循环链表中没有空指针,因此当循环一次结束后再一次指向head是就结束
			++count;
			cur = cur->next;
		}
		return count;
	}
	bool empty()const{
		return  head->next == head;
	}
    void resize(size_t newsize, const T& data = T()){//T()是如果没有提供,给出默认值,写成T(),如果T是内置类型这样写直接数他的类型,如果T是自定义类型,那么就相当于创建了一个匿名对象,是调用类里面的无参构造函数,如果类里面没有午餐的构造函数就会进行报错
		size_t oldsize = size();
		if (newsize >= oldsize){// 增多
			for (size_t i = oldsize; i < newsize; ++i)
				push_back(data);
		}
		else{// 减少
			for (size_t i = newsize; i < oldsize; ++i)
				pop_back();
		}
	}
    
	T& front(){
		return head->next->data;
	}
	const T& front()const{//因为const对象不能访问普通的成员函数,因此还需要定义const类型的成员函数
		return head->next->data;
	}

	T& back(){
		return head->prev->data;
	}

	const T& back()const{
		return head->prev->data;
	}
	//增删改查操作
	void push_back(const T& data){//尾插
		insert(end(), data);
	}
	void pop_back(){//尾删
		auto it = head->prev;
		erase(it);
	}
	void push_front(const T& data){//头插
		insert(begin(), data);
	}
	void pop_front(){//头删
		erase(begin());
	}

	iterator insert(iterator pos, const T& data){//任意位置进行插入
		Node* newnode = new Node(data);
		Node* cur = pos.node;
		newnode->next = cur;
		newnode->prev = cur->prev;
		newnode->prev->next = newnode;
		cur->prev = newnode;
		return iterator(newnode);
	}

	iterator erase(iterator pos){//任意位置进行删除
		if (pos == end())
			return pos;
		Node* cur = pos.node;
		Node* retnode = cur->next;
		cur->prev->next = cur->next;
		cur->next->prev = cur->prev;
		delete cur;
		// 删除
		return iterator(retnode);
	}

	void swap(list<T>& L){//交换
		std::swap(head, L.head);//直接讲两个链表的头结点进行交换
	}

	void clear(){//将有效结点清空
		Node* cur = head->next;
		while (cur != head){
			head->next = cur->next;
			delete cur;
			cur = head->next;
		}
		head->next = head;
		head->prev = head;
	}


private:
	Node* head;
};

在模拟实现的部分我们要注意他的迭代器的实现 ,我们在进行vectoryustring迭代器的模拟时使用的都是原生指针,没有出问题的原因时他们的的底层是一段连续的空间。但是实现list时就不可以。再链表中,如果要获取下一个节点,是通过next指针域来获取取的,而不是对指针进行++。list的迭代器不能是原生态指针,如果是原生态指针就无法结束,因此我们将指针封装

结论:每个容器对应的迭代器的基本操作要统一,才能设计出与数据结构无关的通用算法,这就是泛型编程。

那么如何给list类封装迭代器?其实就是将节点类型的指针封装起来。

2.6,2.7迭代器和适配器 (在另一篇我的博客里)

  • 8
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值