c++之STL容器的学习(上)

发布于:2025-06-04 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、泛型编程(函数模板和类模板)

这部分围绕泛型编程技术展开,C++中的泛型编程主要是通过函数模板和类模板实现的,主要会介绍标准模板库STL的知识点。

1.关于模板的理解

	模板就是建立一种通用的模式,从而提高复用性。

 在生活中有很多模板的应用,比如说PPT模板,比如说工业领域生成的模具。

代码中,也可以建立一些通用性的代码,成为模板代码。这种建立通用模板代码的编程方式成为泛型编程。泛指的是泛泛的意思。

2.函数模板

2.1 函数模板的作用

    建立一个通用函数,其返回值和形参的类型不具体指定,而用一个通用的虚拟的类型来代替,这个虚拟类型称为泛型。函数模板可以提高复用性,因为它将类型参数化了。

    语法:在函数前加一行模板声明:template<typename T>或者template<class T>

    使用函数模板有两种方式:

        1)自动类型推导

        2)显式指定类型

定义模板函数,来适配所有情况

定义模板函数,来适配所有情况
template<typename T>
void mySwap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}
void test01()
{
    //1.交换两个int
    int n1 = 10;
    int n2 = 20;
    mySwap(n1, n2);
    cout << "交换后,n1=" << n1 << ",n2=" << n2 << endl;
    //2.交换两个double
    double d1 = 20.45;
    double d2 = 95.52;
    mySwap(d1, d2);
    cout << "交换后,d1=" << d1 << ",d2=" << d2 << endl;
    //3.交换两个char
    char c1 = 'a';
    char c2 = 'b';
    mySwap(c1, c2);
    cout << "交换后,c1=" << c1 << ",c2=" << c2 << endl;
    //以上使用函数模板的方法属于 1)自动类型推导,它会根据实际传入的实参推导出泛型T的类型
    //接下来使用 2)显式指定类型
    int n3 = 5;
    int n4 = 8;
    mySwap<int>(n3, n4);//这里用<>模板参数列表,传入具体的类型int,指定了函数模板所使用的类型,然后按照对应的类型传参即可
    //mySwap<double>(n3, n4);//如果指定了T的类型,就要传入跟它匹配的实参
    cout << "交换后,n3=" << n3 << ",n4=" << n4 << endl;
}

2.2 函数模板注意事项

1)如果使用自动类型推导,必须推导出一致的数据类型T,才可以使用

2)模板必须要确定出T的类型,才可以使用,如果无法自动推导,那就需要手动显式指定

template<typename T>
void func() { cout << "func被调用,此时T的类型:" << typeid(T).name() << endl; }
void test02()
{
    //1)如果使用自动类型推导,必须推导出一致的数据类型T,才可以使用
    int n = 10;
    char c = 'a';
    //mySwap(n, c);//报错,因为推导的类型不一致

    //2)模板必须要确定出T的类型,才可以使用,如果无法自动推导,那就需要手动显式指定
    //func();//报错,因为T的类型无法确定
    func<int>();//只能显式指定T的类型
    func<double>();
}

2.3 函数模板和普通的区别:

1)普通函数调用时可以发生自动类型转换(隐式类型转换)

2)函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。如果使用显式指定类型方式,也可以发生隐式类型转换。

int add(int a, int b) { return a + b; }
template<class T>
T add_tpl(T a, T b) { return a + b; }
void test05()
{
    int a = 10;
    char c = 'a';
    cout << add(a, c) << endl;
    //发生隐式类型转换,a转换成int的ASCII码97
    //cout << add_tpl(a, c) << endl;
    //函数模板自动推导是无法隐式类型转换
    cout << add_tpl<int>(a, c) << endl;
    //如果使用显式指定类型方式,也可以发生隐式类型转换。
}

2.4 普通函数和函数模板的调用规则

1)如果函数模板和普通函数实现了重载,都可以实现,优先调用普通函数(具体的高于通用的)

2)可以通过空的模板参数列表<>来强制调用函数模板

3)函数模板之间也可以重载(有多个同名的函数模板,参数不同)

4)如果函数模板比普通函数能产生更好的参数匹配,那么就会调用函数模板

void myPrint(int a, int b) { cout << "a=" << a << " b=" << b << " 调用普通函数" << endl; }
template<class T>
void myPrint(T a,T b){ cout << "a=" << a << " b=" << b << " 调用函数模板:两个参数" << endl; }
template<class T>
void myPrint(T a, T b,T c) { cout << "a=" << a << " b=" << b <<" c="<<c << " 调用函数模板:三个参数" << endl; }
void test06()
{
    int a = 10;
    int b = 20;
    myPrint(a, b);//都能实现,优先调用普通函数

    myPrint<>(a, b);//可以通过空的模板参数列表<>来强制调用函数模板

    int c = 30;
    myPrint(a, b, c);//函数模板之间也可以重载(有多个同名的函数模板,参数不同)

    char c1 = 'a';
    char c2 = 'b';
    myPrint(c1, c2);//如果函数模板比普通函数能产生更好的参数匹配,那么就会调用函数模板
}

2.5 特定的模板(解决特定类型的问题)

模板的通用并不是万能的,有些情况下没办法通用,比如自定义类型的数据。

这时候就要通过特定模板来解决特定问题。

语法:在函数名的同一行前面加上template<>

class Student
{
public:
    string m_Name;
    int m_Age;
    Student(string name, int age) :m_Name(name), m_Age(age) {}
};
//对于自定义类型来说,就需要写一个特定模板来解决这个特定问题
template<> bool myCompare(Student& s1, Student& s2)
{
    cout << "调用特定模板myCompare(Student& s1, Student& s2)" << endl;
    if (s1.m_Name.compare(s2.m_Name)==0 and s1.m_Age==s2.m_Age)
    {
        return true;
    }
    else
    {
        return false;
    }
}

 Student s1("Tom", 10);
    Student s2("Lucy", 12);
    Student s3("Tom", 10);
    cout << myCompare(s1, s2) << endl;//这里调用了特定模板

3 类模板

3.1 类模板的作用

建立一个通用的类,类中的成员(属性和方法)用到的数据类型可以不具体指定,用一个虚拟的泛型来代替。

语法:跟函数模板类似,在类前面加一行模板声明:template<typename 泛型变量名称>或者template<class 泛型变量名称>

类模板的使用:只能显式指定类型,没有自动推导的使用方式。这是和函数模板最大的区别。

类模版

//一个Person类,包含姓名和年龄两个属性,还有show方法,setValue方法(函数模板)
template<class NameType,class AgeType=int>//第二个泛型有默认值
class Person
{
	NameType m_Name;
	AgeType m_Age;
public:
	Person(NameType name, AgeType age)
	{
		m_Name = name;
		m_Age = age;
	}
	void show() { cout << "姓名:" << m_Name << ",年龄:" << m_Age << endl; }
	//函数模板
	void setValue(NameType name, AgeType age) { m_Name = name; m_Age = age; }
};
void test01()
{
	Person<string, int> p1("孙悟空", 1000);//类模板只能显式指定类型,不能自动推导
	Person<string, string> p2("白骨精", "五百岁");
	Person<string> p3("唐僧", 30);//使用了默认值
	p1.show();
	p2.show();
	p3.show();
	//使用类中的函数模板
	p1.setValue("斗战胜佛", 1100);
	p2.setValue("白骨灰", "五百五十岁");
	p1.show();
	p2.show();
}

3.2 类模板结合函数模板来使用

将类模板对象当作函数参数使用,一共有三种传入方式:

1)指定类模板对象的泛型类型

2)不指定类模板对象的泛型类型,而是通过函数模板的模板参数来指定。

3)将整个类当成一个泛型,使用通用函数的模板参数来指定。

1)指定类模板对象的泛型类型
void show_01(Person<string, int>& p) { p.show(); }
2)不指定类模板对象的泛型类型,而是通用函数模板的模板参数来指定。
template<typename T1,typename T2>
void show_02(Person<T1, T2>& p)
{
	p.show();
	cout << "T1的类型:" << typeid(T1).name() << endl;
	cout << "T2的类型:" << typeid(T2).name() << endl;
}
3)将整个类当成一个泛型,使用通用函数的模板参数来指定。
template<typename T>
void show_03(T& p)
{
	p.show();
	cout << "T的类型:" << typeid(T).name() << endl;
}
//以上三种情况,灵活度逐步提高
class Student
{
public:
	string m_Name;
	int m_Age;
	Student(string name,int age):m_Name(name),m_Age(age){}
	void show(){ cout << "姓名:" << m_Name << ",年龄:" << m_Age << endl; }
};
void test02()
{
	Person<string, int> p1("张三", 10);
	Person<string, string> p2("李四", "十八岁");
	//1)指定类模板对象的泛型类型
	show_01(p1);//必须传入Person<string,int>的对象
	//show_01(p2);//这个对象类型不匹配

	//2)不指定类模板对象的泛型类型,而是通用函数模板的模板参数来指定。
	show_02(p1);
	show_02<string, string>(p2);

	//3)将整个类当成一个泛型,使用通用函数的模板参数来指定。
	show_03(p1);
	show_03(p2);
	Student s1("Tom", 15);
	show_03(s1);
}

3.3 类模板遇到继承

如果父类是类模板,子类继承父类,有几种情况:

1)子类可以是普通类,也可以是类模板

2)当父类子类都是类模板时,子类和父类的泛型可以不同,各自指定自己的泛型类型。

3)当父类子类都是类模板时,让它们使用同一个泛型类型。

//父类是类模板
template<class T>
class Fruit
{
public:
	Fruit() { cout << "Fruit在构造,泛型类型是:" << typeid(T).name() << endl; }
};
//1)子类可以是普通类,也可以是类模板
class Apple :public Fruit<int>//此时父类的泛型类型要确定下来
{
public:
	Apple() { cout << "Apple在构造,它是普通类" << endl; }
};
//2)当父类子类都是类模板时,子类和父类的泛型可以不同,各自指定自己的泛型类型。
template<class T>
class Banana :public Fruit<int>
{
public:
	Banana() { cout << "Banana在构造,它是类模板,它的泛型是:" << typeid(T).name() << ",它的父类泛型是int" << endl; }
};
//3)当父类子类都是类模板时,让它们使用同一个泛型类型。
template<class T>
class Orange :public Fruit<T>
{
public:
	Orange() { cout << "Orange在构造,它是类模板,它和父类使用同一个泛型:" << typeid(T).name()<< endl; }
};
void test03()
{
	Apple a;
	Banana<double> b;
	Orange<char> o;
}

二、STL与string容器

1.STL概念

为了建立数据结构和算法的一套标准,提高复用性,诞生了STL(standard template library)标准模板库

STL都采用了泛型编程实现,里面几乎都是类模板和函数模板。

STL分为三大部分:容器(container)、算法(algorithm)、迭代器(iterator)。

容器:放数据的地方,STL容器实现了常见的数据结构,用来组织和存储数据。

算法:用于对数据进行运算的,用于解决问题的方法。比如排序、查找等等。

迭代器:用于指向容器中的每个元素,用来访问元素,本质上就是一个封装了指针的类。

2.STL中的常用容器

1 string容器

1.1基本概念

string是STL里定义的字符串,本质是一个类,是一个特殊的容器,元素类型只能是char类型。

string和char*的区别:

char是一个指针,而string是一个类,类内部封装了char

string管理char*所分配的内存,而且不用担心越界的问题,由类来负责管理,string封装了很多成员方法,使用起来非常方便

1.2string构造

string();//创建一个空的字符串

string(const char* s);//使用字符串s来初始化string对象

string(const string& str);//使用string对象str来初始化string对象

string(int n,char c);//使用n个字符c来初始化string对象

string s1;//构造一个空字符串
cout << "s1=" << s1 << endl;

const char* str = "hello world";
string s2(str);//把常量字符串转换成string字符串
cout << "s2=" << s2 << endl;

string s3(s2);//拷贝构造
cout << "s3=" << s3 << endl;

string s4(10, 'a');//10个a字符构成的字符串
cout << "s4=" << s4 << endl;

1.3string赋值操作

重载赋值运算符:

string& operator=(const char* s);//字符串常量赋值

string& operator=(const string& s);//string对象的赋值

string& operator=(char c);//单个字符赋值

赋值函数assign():

string& assign(const char* s);//字符串常量赋值

string& assign(const char* s,int n);//将字符串s的前n个字符赋值给string

string& assign(const string& s);//使用string对象完成赋值

string& assign(int n,char c);//使用n个字符c来赋值

//重载赋值运算符:
	string str1;
	str1 = "hello world";
	cout << "str1=" << str1 << endl;
	string str2;
	str2 = str1;
	cout << "str2=" << str2 << endl;
	string str3;
	str3 = 'A';
	cout << "str3=" << str3 << endl;

	//赋值函数assign():
	string str4;
	str4.assign("hello C++");
	cout << "str4=" << str4 << endl;
	string str5;
	str5.assign("hello C++", 5);
	cout << "str5=" << str5 << endl;
	string str6;
	str6.assign(str5);
	cout << "str6=" << str6<< endl;
	string str7;
	str7.assign(5, 'x');
	cout << "str7=" << str7 << endl;

1.4string字符串拼接

在字符串末尾拼接字符串
重载运算符+=:

	string& operator+=(const char* s);
	string& operator+=(const string& str);
	string& operator+=(char c);

append函数:
	string& append(const char* s);
	string& append(const char* s,int n);
	//把字符串s的前n个字符拼接到string字符串
	string& append(const string& s);
	string& append(const string& s,int pos,int n);
	//pos代表position位置,从0开始的下标。从pos位置开始,将字符串s后面的n个字符拼接到当前字符串

//重载运算符+=:
	string str1 = "我";
	str1 += "爱玩游戏";
	cout << "str1=" << str1 << endl;
	str1 += ':';
	cout << "str1=" << str1 << endl;
	string str2 = "LOL DNF";
	str1 += str2;
	cout << "str1=" << str1 << endl;

	//append函数:
	string str3 = "I";
	str3.append(" like ");
	str3.append("play game such as ", 10);
	cout << "str3=" << str3 << endl;
	str3.append(str2, 4, 3);
	cout << "str3=" << str3 << endl;

1.5string查找和替换

查找:查找指定的字符串是否存在,找到会返回对应的位置,找不到返回-1(-1即string::npos),查找是常函数

从左往右查找:find

	int find(const string& str,int pos=0) const;//查找字符串str,返回它第一次出现的位置,从pos位置开始找,pos默认值是0,即开头位置
	int find(const char* s,int pos=0) const;
	int find(const char* s,int pos=0,int n) const;//从pos位置开始查找字符串s的前n个字符第一次出现的位置
	int find(char c,int pos=0) const;//从pos位置查找字符c第一次出现的位置

从右往左查找:rfind

int rfind(const string& str,int pos=npos) const;

	//从右往左查找str最后一次出现的位置,从pos位置开始查找,pos默认值是npos,npos是string类的静态常量,表示字符串结束
	int rfind(const char* s,int pos=npos) const;
	int rfind(const string& s,int pos=npos,int n) const;
	//从pos开始从右往左查找s字符串的前n个字符最后一次出现的位置
	int rfind(char c,int pos=npos) const;
	替换:在指定的位置替换字符串
	string& replace(int pos,int n,const string& str);
	//从pos位置开始的n个字符被替换为字符串str
	string& replace(int pos,int n,const char* s);
	//从pos位置开始的n个字符被替换为字符串s

//从左往右查找:find
	string str1 = "abcdefgde";
	int pos=str1.find("de");
	cout << "第一个de字符串的位置:" << pos << endl;//3
	pos = str1.find("de", 5);
	cout << "第二个de字符串的位置:" << pos << endl;//7
	pos = str1.find("fgde", 0, 2);
	cout << "第一个fg字符串的位置:" << pos << endl;//5
	pos = str1.find('f');
	cout << "f字符的位置:" << pos << endl;//5

	//从右往左查找:rfind
	cout << "从右往左查找:" << endl;
	pos = str1.rfind("de");
	cout << "第一个de字符串的位置:" << pos << endl;//7
	pos = str1.rfind("de", 5);
	cout << "第二个de字符串的位置:" << pos << endl;//3
	pos = str1.rfind("fgde", -1, 2);
	cout << "第一个fg字符串的位置:" << pos << endl;//5
	pos = str1.rfind('d');
	cout << "d字符的位置:" << pos << endl;//7

	//替换:在指定的位置替换字符串
	str1.replace(1, 4, "8888");
	cout << "被替换后,str1=" << str1 << endl;

1.6string字符串比较

比较方式:是按照字符串中每个字符的ASCII码逐个比较,如果比较出大小就返回结果,如果直到结束都没有比较出大小,证明两个字符串相等。

所以,比较方法主要的作用就是判断两个字符串是否相等,至于大小没实际意义。

返回值:相等的时候返回0,大于的时候返回1,小于返回-1

int compare(const string& str) const;
	//当前字符串跟字符串str比较
	int compare(const char* s) const;
	//当前字符串跟字符串s比较

string s1 = "hello";
	string s2 = "hello";
	string s3 = "Hello";
	cout << s1.compare(s2) << endl;//0
	cout << s1.compare(s3) << endl;//1

1.7 string插入和删除

   插入:
	string& insert(int pos,const char* s);
	//在pos位置插入字符串s
	string& insert(int pos,const string& str);
	//在pos位置插入字符串str
	string& insert(int pos,int n,char c);
	//在pos位置插入n个字符c
   删除:
	string& erase(int pos,int n);
	//删除从pos开始的n个字符

    string str = "hello world";
	str.insert(1, "888");
	cout << "str=" << str << endl;
	str.insert(1, 3, 'a');
	cout << "str=" << str << endl;
	str.erase(1, 6);
	cout << "str=" << str << endl;

1.8string截取子字符串

从字符串中截取想要的子字符串
string& substr(int pos,int n=npos) const;
//返回从pos开始的n个字符组成的子字符串,n的默认值是npos,代表最后

1.9获取长度

size():获取字符串的长度,也就是字符数

string str = "abcdefg";
	string subStr1 = str.substr(1, 3);
	cout << "subStr1=" << subStr1 << endl;
	string subStr2 = str.substr(3);//第二个参数取默认值
	cout << "subStr2=" << subStr2 << endl;
	cout << "str的长度:" << str.size() << endl;

1.10string元素的存取

string中单个字符存取可以通过下面两个方式:
	char& operator[](int n);//运算符重载,通过[index]取值,index代表下标
	char& at(int n);//通过at成员函数取值,n也是下标

//取,即查询
	string str = "hello world";
	cout << str[4] << endl;//o
	cout << str.at(6) << endl;//w

	//存,即修改保存
	str[0] = 'H';
	str[6] = 'W';
	cout << str << endl;

	//越界的处理
	//cout << str[12] << endl;//使用[]访问的时候,越界会报错导致程序中断
	//通过循环控制下标的范围
	for (int i = 0; i < str.size(); i++)
	{
		cout << str[i];
	}
	cout << endl;
	//使用at代替[]来访问,这个函数在越界的时候会抛异常
	try
	{
		cout << str.at(12) << endl;//越界了,会抛异常
	}
	catch (out_of_range& e)
	{
		cout << "越界了" << endl;
		cout << e.what() << endl;
	}
	catch (const std::exception&)
	{
		cout << "发生异常" << endl;
	}

	//使用迭代器遍历字符串,避免越界
	for (string::iterator it = str.begin(); it != str.end(); it++)
	{
		cout << *it << " ";
	}
}

//课堂练习:1.完成string字符串和char*字符串之间的相互转换
void test09()
{
	//char*转string,使用string的构造函数
	const char* s1 = "welcome to china";
	string s2(s1);
	cout << s2 << endl;

	//string转char*
	//string是个类或者对象,它是对传统的字符串char*内存空间的封装管理,方便我们去操作
	//所以,string中的第一个字符也就是字符数组中第第一个字符,而字符数组的首地址即字符串,所以只要拿到string中第一个字符的地址即可
	string s3 = "hello world";
	char* s = &s3[0];
	cout << s << endl;

	//除此之外,还可以利用标准库的一个函数来将string字符串转换成标准字符串:c_str()
	string s4 = "let it be";
	const char* c=s4.c_str();
	cout << c << endl;
}
//课堂练习:2.写一个函数,可以将任意一个邮箱地址字符串(string类型的)的邮箱用户名取出来返回。如hello@sina.com
string get_email_username(string& s)
{
	//思路:用户名即首字符到@符之间的字符串
	int pos=s.find('@');//找到@的位置,返回它的小标
	string username=s.substr(0, pos);//截取用户名
	return username;
}
void test10()
{
	string email_1 = "hello@sina.com";
	string email_2 = "29858399@qq.com";
	cout << get_email_username(email_1) << "," << get_email_username(email_2) << endl;
}

写一个函数,可以将一个英文字符串中的大小写字母相互转换。

3.Hello转换成hELLO
void upper_or_lower(string& str)
{
	//从ASCII码入手,小写字母比对应的大写的ASCII码大32
	for (int i = 0; i < str.size(); i++)
	{
		//分情况转换
		if (str[i]>=65 and str[i]<=90)//此范围就是大写
		{
			str[i] = str[i] + 32;
		}
		else
		{
			str[i] = str[i] - 32;
		}
	}
}

2 vector动态数组

2.1vector的基本概念

!8475e48a65bab2ddda7779324b0276d](E:\note\图片文件\8475e48a65bab2ddda7779324b0276d.jpg)

	vector的数据结构跟数组非常相似,也叫做单端数组。
	vector与数组的区别:数组是静态的,一旦声明了数组的长度,是不可变的。而vector是动态的,它的长度可以动态拓展。
	拓展原理:当空间不足的时候,再添加新元素,vector会重新申请一块更大的内存空间,并且将旧空间的数据拷贝到新空间,然后释放旧空间。
	涉及到两个概念:容量capacity 和大小size。
	容量指的是空间的总大小;
	size指的是实际存放的元素数。

2.2vector构造函数

vector<T> v;
//采用类模板实现,无参构造,构造了一个空的vector容器,就是v
vector<T> vec(v.begin(),v.end());
//参数是两个迭代器,两个位置,v是一个vector对象,将v[begin,end)区间中的元素拷贝给当前vec对象,完成构造。
vector<T> v(const vector& vec);
//拷贝构造
vector<T> v(n,ele);
//ele是element,即元素。使用n个ele元素完成当前vector对象的构造。

vector容器中使用迭代器遍历数据

void printVector(vector<int>& v)
{
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

vector<int> v1;//无参构造
	for (int i = 0; i < 10; i++)
	{
		v1.push_back(i);
	}
	printVector(v1);

	vector<int> v2(v1.begin(), v1.end());
	printVector(v2);

	vector<int> v3(v2);
	printVector(v3);

	vector<int> v4(10, 100);
	printVector(v4);

2.3vector赋值

重载赋值运算符:
	vector& operator=(const vector& vec);
成员函数assign:
	assign(beg,end);//将vector[beg,end)区间的元素赋值给当前的vector对象,beg和end是两个迭代器位置
	assign(n,ele);//将n个ele元素赋值给当前vector


vector<int> v1;
	for (int i = 0; i < 10; i++)
	{
		v1.push_back(i);
	}
	printVector(v1);
	//运算符=
	vector<int> v2 = v1;
	printVector(v2);
	//assign函数
	vector<int> v3;
	v3.assign(v1.begin() + 2, v1.end() - 2);
	printVector(v3);
	vector<int> v4;
	v4.assign(10, 100);
	printVector(v4);

2.4vector容量和大小

	empty();
	//判断容器是否为空
	capacity();
	//返回容器的容量
	size();//返回容器中元素的个数
	resize(int num);
	//重新指定容器的size为num,如果容器的size变多了,会以默认值填充多余的位置,如果size变少了,会删除多余的元素。
	resize(int num,ele);
	//重新指定容器的size为num,如果容器的size变多了,会以ele元素填充多余的位置,如果size变少了,会删除多余的元素。


	if (v1.empty())
	{
		cout << "v1为空" << endl;
	}
	else
	{
		cout << "v1不为空" << endl;
		cout << "v1的容量:" << v1.capacity() << endl;
		cout << "v1的大小:" << v1.size() << endl;
	}
	v1.resize(5);
	v1.resize(10);
	v1.resize(15, 100);
	v1.resize(5);
	printVector(v1);
	cout << "v1的容量:" << v1.capacity() << endl;
	//可见,resize之后,容量没有变小,存在浪费情况
	//可以通过缩容函数实现容量的收缩,收缩到跟size一致
	v1.shrink_to_fit();
	cout << "缩容之后,v1的容量:" << v1.capacity() << endl;

2.5vector插入和删除

尾部操作:
	push_back(ele);
	//尾部推入一个元素ele
	pop_back();
	//尾部最后一个元素ele被弹出
	
指定位置插入:insert
	insert(iterator pos,ele);
	//迭代器指向位置pos插入元素ele,并且返回新插入元素的位置迭代器。
	insert(iterator pos,int n,ele);
	//迭代器指向位置pos插入n个元素ele,并且返回新插入多个元素的第一个元素的位置迭代器。
	insert(iterator pos,beg,end);
	//在pos位置插入另一个容器的[beg,end)区间中的值
	
删除:erase
	erase(iterator pos);
	//删除迭代器所指向的元素,返回被删除元素的下一个迭代器位置。
	erase(iterator beg,iterator end);
	//删除beg到end之间的元素,不包括end
	
	clear();
	//清除容器中的所有元素
注意:删除或者插入操作都会导致删除和插入位置以及以后位置的迭代器改变,所以如果要继续使用迭代器来操作后面的元素,就必须更新迭代器。
	这个原则适用于连续空间的数据结构,包括我们马上要学习的另外一个容器deque。但是对于后面再学习的基于链表或者是树的容器,就不适用了。比如list、map、set等。


//尾部操作
	vector<int> v1;
	v1.push_back(50);
	v1.pop_back();
	
//指定位置插入:insert
	v1.insert(v1.begin(), 100);
	v1.insert(v1.begin(), 2, 1000);
	vector<int> v2 = { 6,7,8,9,0 };
	//大括号的方式初始化
	v1.insert(v1.begin(), v2.begin(), v2.begin() + 2);
	//将v2前两个值插入v1开头
	

	//删除:erase
	v1.erase(v1.begin());
	v1.erase(v1.begin(), v1.begin() + 2);

	//关于返回值
	vector<int>::iterator it = v1.insert(v1.begin() + 1, 500);
	cout << *it << endl;//500
	vector<int>::iterator it_erase = v1.erase(v1.begin() + 1);
	cout << *it_erase << endl;
	it_erase = v1.erase(v1.begin(), v1.begin() + 2);
	cout << *it_erase << endl;

迭代器失效问题

对于vector容器进行for循环中使用erase和insert时,要注意指针的移动,防止指针没有进一步操作二进入循环导致输出异常和错误。

void test05()
{
	vector<int> v;
	v = { 10,20,30,30,40,50 };
	printVector(v);
	//删除容器中的30
	for (vector<int>::iterator it = v.begin(); it !=v.end(); it++)
	{
		if (*it==30)
		{
			v.erase(it);
		}
	}
	
	//改进:在发生删除操作导致迭代器失效的时候,更新当前的迭代器变量,更新成一个新的有效的迭代器
	for (vector<int>::iterator it = v.begin(); it != v.end();)
	{
		if (*it == 30)
		{
			it=v.erase(it);
	//这里就是对迭代器变量的更新操作,接受了erase的返回值
	//由于erase会返回下一个元素的迭代器,所以这行代码会指向下一个值,相当于it+1
		}
		else
		{
			it++;//所以应该把循环条件中的it++放到这里,否则就会跳过第二个30
		}
	}
	printVector(v);

	//在30的位置插入100
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
	{
		if (*it==30)
		{
			it=v.insert(it, 100);
			it++;//上一行代码接受了insert的返回值,即100的位置,下次循环再it++,又碰到30了,这样会进入死循环,所以这里再++
		}
	}
	printVector(v);
}

2.6vector数据存取

operator[];//重载了[]运算符,可以通过下标取值
at(int index);//返回索引index位置的元素
front();//返回容器中第一个元素
back();//返回容器中最后一个元素


vector<int> v1;
	//通过下标取值来打印
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}

	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1.at(i) << " ";
	}

	cout << "首值:" << v1.front() << endl;
	cout << "尾值:" << v1.back() << endl;

	//越界的处理
	//如果发生越界,会抛出异常,要做异常处理,否则程序会停止
	try
	{
		cout << v1.at(12);
	}
	catch (out_of_range& e)
	{
		cout << "越界了" << endl;
		cout << e.what() << endl;
	}
	catch (const std::exception&)
	{
		cout << "发生未知错误" << endl;
	}

2.7vector互换

可以实现两个vector容器元素的互换
v1.swap(v2);//v1和v2互换元素


v1.swap(v2);

2.8vector预留空间

当vector存储的数据量很大时,由于vector的动态数组特性,会导致多次的动态扩展发生,这样会降低效率。

为了避免多次拓展的情况发生,可以在一开始就指定一些预留的空间,其实就是指定了capacity的值。

reserve(int len);//给容器预留len个元素的位置,预留的位置不可访问,因为没有被初始化。

//只需要观察某个值的地址是否变化,就能判断是否发生动态拓展

2.9几个常用的算法函数(需要引入头文件#include )

这是算法,适用于大部分容器的,不是vector的成员函数

排序:

sort(iterator beg,iterator end);

//对区间内的元素进行排序,默认升序,从小到大

sort(iterator beg,iterator end,func);

//对区间内的元素进行排序,使用func指定排序方式

反转:

reverse(iterator beg,iterator end);

//对区间内元素反转倒置

复制:

copy(源容器起始位置,源容器结束位置,目标容器的起始位置);

//将源容器的区间元素复制到目标容器指定位置,注意是替换,新复制进去的值替换了原来位置的值

查找:

find(iterator beg,iterator end,ele);

//在区间内查找元素ele,找到返回所在位置的迭代器,找不到返回end迭代器,注意这个end指的是区间中指定的end位置,不一定是整个容器的end()。

遍历:

for_each(iterator beg,iterator end,func);//遍历容器区间内的元素,func是个函数或者仿函数,指明遍历时对元素的操作。

2.10总结

vector是单端数组,所以它在尾部的插入和删除操作效率很高,但是在其他位置做insert插入操作的时候,效率很低,原因是会引起其他元素的移动。
同时,由于它是动态数组,是连续存储的物理空间,所以可以通过下标直接访问,所以查找效率很高。

3 deque双端队列

3.1概念

deque全称是double-ended-queue,也叫双端数组,可以对两端进行操作

vector和deque的区别:

vector是单向开口的连续空间,deque是双向开口的连续空间,可以高效地在头尾两端做元素的插入和删除操作。当然如果在中间insert操作,效率仍然是低的。

deque没有容量capacity的概念,它是以分段的连续空间组合而成的,随时可以增加一段新的空间连接起来,它不会像vector那样因为空间不足,重新申请一块空间的操作。

deque通过内部的中控器来维护这些分段的连续空间。中控器维护每个连续空间的地址,使它们连接起来。

deque的成员函数跟vector几乎一样,不再赘述。

3.2 deque构造函数

函数原型:

deque<T> deq; //默认构造形式

deque(beg, end); //将参数deque[beg, end)区间中的元素拷贝。

deque(n, ele); //将n个ele拷贝。

deque(const deque &deq); //拷贝构造函数

//打印函数
void printDeque(deque<int>& d)
{
	for (deque<int>::iterator it = d.begin(); it != d.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

3.3 deque赋值操作

函数原型:
deque& operator=(const deque &deq); 
//重载赋值运算符
assign(beg, end); 
//将参数deque[beg, end)区间中的数据赋值
assign(n, elem); 
//将n个elem赋值


deque<int> d2 = d1;
d3.assign(d1.begin(), d1.begin() + 5);
d4.assign(8, 8);

3.4 deque大小操作

函数原型:
empty(); 
//判断容器是否为空
size(); 
//返回容器中元素的个数
resize(num); 
//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超
出容器长度的元素被删除。
resize(num, elem); 
//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则
末尾超出容器长度的元素被删除。

3.5 deque 插入和删除

函数原型:
两端插入操作:
push_back(elem); //在容器尾部添加一个数据
push_front(elem); //在容器头部插入一个数据
pop_back(); //删除容器最后一个数据
pop_front(); //删除容器第一个数据
指定位置操作:
insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem); //在pos位置插入n个elem数据,返回新数据的位置。
insert(pos,beg,end); //在pos位置插入另一个deque[beg,end)区间的数据,返回新数据的位置。
erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos); //删除pos位置的数据,返回下一个数据的位置。
clear(); //清空容器的所有数据

3.6 deque 数据存取

函数原型:
at(int idx); //返回索引idx所指的数据
operator[]; //返回下标索引所指的数据
front(); //返回容器中第一个数据元素
back(); //

3.7 deque 元素互换swap

迭代器失效问题

void test07()
{
deque<int> d = { 5,2,2,8,2 };
//删除2
for (deque<int>::iterator it = d.begin(); it != d.end(); )
{
	if (*it==2)
	{
		it = d.erase(it);
	}
	else
	{
		it++;
	}
}
printDeque(d);

//在2的位置插入100
for (deque<int>::iterator it = d.begin(); it != d.end(); it++)
{
	if (*it==2)
	{
		it = d.insert(it, 100);
		it++;
	}
}
printDeque(d);

shangjialu


网站公告

今日签到

点亮在社区的每一天
去签到