c++之容器详解

5. 类模板编程

有时候继承、包含并不能满足重用代码的需要,这一般在容器类里面体现的尤为突出。例如: 我们定义了一个容器类,Container, 这个Container类可以实现类似verctor一样的工作,能保存数据,能修改数据,并且数据的类型不限制,但是针对数据的操作都是一样的。那么类模板编程就成了不二之选了。

1. 定义模板类

这里以栈作为参照对象,定义一个模板类,实现栈一样的功能。

  • 原始代码
class Stack{
private :
    enum{MAX = 10}; //表示这个Stack容器最多只能装10个。
    int top =0 ; //表示最顶上的索引位置
    string items[MAX]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(string val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    string pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    string operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};
  • 类模板

上面的Stack容器仅仅只能针对string这种数据类型,如果想存自定义的类型或者其他类型,那么Stack就无法满足了。 要定义类模板,需要在类的前面使用template <typename T> , 然后替换里面的所有string 即可,这样Stack就能为所有的类型工作了。 如果是自定义类型,那么需要自定义类型提供无参构造函数,因为数组的定义会执行对象的构造。若想避免构造的工作发生,可以使用allocator来操作。

template <typename T> class Stack{
private :
    enum{MAX = 10}; //表示这个Stack容器最多只能装10个。
    int top =0 ; //表示最顶上的索引位置
    T items[MAX]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(const T& val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    T pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    T operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};

6. 练习

模拟vector实现自定义容器

  1. 底层使用数组实现
  2. 数组开辟空间,使用allocator ,否则会一块执行对象构造工作
  3. 能装任意类型
  4. 提供push_back函数,添加元素到容器末端
  5. 提供pop函数,返回容器的最前端元素
  6. 提供[]操作符,根据下标可以获取元素
  7. 提供size函数
  8. 提供*操作符 ,以便修改指定元素。如:

Stu stu = myvector[0];

*stu.name =“张三”;

二、容器

在C++ 里面,针对容器的划分有: 顺序容器关联容器

1. 顺序容器

所谓的顺序容器指的是,在容器的内部,元素的摆放是有顺序的。通常 vector已经足以满足大部分开发了。

容器描述
string与vector相似,尾部插入|删除速度快
array固定大小数组,支持快速随机访问,不能添加和删除
vector可变大小数组,支持快速随机访问,在尾部之外的位置插入和删除 速度慢
deque双端队列,支持快速随机访问,两段插入和删除速度快
forward_list单向链表、只支持单向顺序访问,插入和删除快,查询慢
list与单向链表不同,它是双向链表,其他一样。

2. 迭代器

早前访问数组、vector、字符串时,都可以使用索引下标的方式访问,实际上还有一种更为通用的机制 : 迭代器 。所有标准库中的容器都可以使用迭代器,但是只有少数几个支持下标索引的方式。与指针相似,迭代器也能对对象进行间接访问,但是不能简单认为,指针就是对象,也不能直接认为迭代器就是对象。只是可以通过迭代获取到对象。

  • 使用迭代器

迭代器通常都是靠容器的 begin()end() 函数获取。 其中begin 返回的是指向第一个元素的迭代器, 而end函数返回的值指向最后一个元素的下一个位置,也就是指向容器里面不存在的尾后元素。所以一般end()返回的迭代器仅是一个标记而已,并不用来获取数。

在这里插入图片描述

  • 迭代器运算

由于begin() 的返回值只会指向第一个元素,若想获取到后面的元素,可以对迭代器进行运算,它的用法和指针的运算是一样的。通过 + - 的操作,来让返回前后元素对应的迭代器。 在迭代器的内部,提供两种获取迭代器对应的数据,一是base()返回的是 指向数据的指针, 二是使用*操作符函数,在迭代器里面重载了*运算符函数。通过它可以直接获取到数据

在这里插入图片描述

  • 使用迭代器
vector<int> s{88,85,90,95,77};
cout <<*s.begin() << endl; //88
cout <<*(s.begin()+1) << endl; //85
//...
cout <<*(s.end()-1) << endl;//77

//遍历容器
for(auto i = s.begin() ; i != s.end() ; i++){
    cout << *i << endl;
}

3. 关联容器

关联容器和顺序容器有很大的不同,顺序容器的元素在容器中是有顺序的(按照添加先后计算) , 而关联容器并不计较顺序的概念,因为他们是按照关键字来访问元素的。C++中常用的关联容器有两个:mapset , map 有点类似 python 里面的字典,使用键值对的形式来存储

1. pair介绍

pair定义在头文件#include <utility> 中,一个pair保存两个数据成员,它是类模板,所以在创建对象的时候,需要添加泛型参数,以用来表示所保存的数据类型。

  • 构建pair
pair<string ,int> p("张三",17) ;
cout << p.first  << p.second <<endl;
2. map操作

map 只允许产生一对一的关系,也就是一个关键字对应一个值,如生活中大多数人,一人一套房差不多。但是也有人是名下多套房,这时候可以使用multimap, 它允许一个关键字对应多个值。 它们都定义在头文件 #include<map> 中。

  • 添加
map<string , string> address_map ;

//匹配可变参数列表
address_map.insert({"张三" , "星湖花园1号"});
address_map.insert(make_pair("李四" , "星湖花园1号"));
address_map.insert(pair<string ,string>("王五" , "星湖花园1号"));

//有疑问
address_map.insert({"张三" , "星湖花园2号"}); //与第一个同关键字,会覆盖原有数据
  • 访问

map可以使用[]的方式来获取指定的元素,要求传递进来的是key关键字

//访问指定元素
string address = address["张三"] ;
cout << address << endl;

//使用at函数访问
string address2 = map.at("张三2")
cout << address2 << endl;    
  • 删除

除了能使用迭代器的方式删除之外,关联容器由于使用了关键了记录数据,所以删除的时候也可以根据关键字来删除数据。

//迭代器方式删除。
for(auto i = address_map.begin() ; i != address_map.end() ; i++){
    cout <<i->first << " =  "<< i->second << endl;
    if(i->first == "李四"){
        address_map.erase(i);
    }
}

//使用关键字删除
address_map.erase("王五");

//清空整个map
address_map.clear();
  • 修改

修改其实就是替换,但是如果还是用insert 是无法起作用的,因为它会执行唯一检查。使用 at函数,对某一个特定关键字的位置修改值。

map<string , int> map;
map.insert( {"张三1" ,18});
map.insert( {"张三2" ,28});
map.insert( {"张三3" ,38});

cout <<map["张三1"] << endl; //18

map.at("张三1") = 99;

cout <<map["张三1"] << endl; //99
  • 容量查询

判断是否为空、获取大小、判断是否存在key

map<string , int> map;
map.insert( {"张三1" ,18});

//判断是否为空
bool empty = map.empty();
cout <<"empty = " << empty << endl;

//获取大小
int size = map.size();
cout <<"size = " << size << endl;

//查询该key在map中的个数,可用来判断是否存在该key
int count = map.count("张三1");
cout <<"count = " << count << endl;
3. set操作

set就是关键字的简单集合,并且容器内部元素不可重复且有顺序。当只是想知道一个值是否存在时,set是最有用的。 set 为不可重复容器,而multiset为可重复容器。需要导入头文件#include

在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。set中元素的值不能直接被改变。set内部采用的是一种非常高效的平衡检索二叉树:红黑树,也称为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树。

  • 创建对象
//创建空白set容器
set<int>s ;

//创建容器并初始化,可变参数往里面赋值,但是这里添加了重复的3. 后面的3不会被添加进去。
set<int> s1({3,4,5,6,3});
  • 添加元素
set<int> s;
s.insert(6);
s.insert({7,8,9,10}); //也可以接收可变参数
  • 遍历set

遍历的逻辑和其他含有迭代器的容器一样。

set<int> s1({3,4,5,6,3});
for(auto p = s1.begin(); p != s1.end() ; p++){
    cout <<*p << endl;
}
  • 删除指定元素

还是使用erase 函数删除 。

set<int> s1({3,4,5,6,3});
for(auto p = s1.begin(); p != s1.end() ; p++){
    cout <<*p << endl;
    if(*p == 4){
        s1.erase(p);
    }
}

//清空这个容器
s1.clear();
  • 容量查询

判断是否为空、获取大小、判断是否存在key

set<int> s1({3,4,5,6});

//判断是否为空
bool empty = s1.empty();
cout <<"empty = " << empty << endl;

//获取大小
int size = s1.size();
cout <<"size = " << size << endl;

//查询该key在map中的个数,可用来判断是否存在该key
int count = s1.count("张三1");
cout <<"count = " << count << endl;

三、常用容器函数

到目前为止,容器的操作基本上也就包含了增删改查这些基本的功能,但是有时候可能会面临一些特殊的操作,比如:查找特定元素、替换或删除特定值、对容器进行重新排序等等。标准库并未给每个容器实现这些操作的函数,而是定义一组泛型算法 。 算法大部分包含在 #include <algorithm> | #include <numeric>

1. 只读函数

这一类算法只会读取元素,并不会改变元素。常见的有 : find | cout | accumulate

  • accumulate

它定义在头文件numeric 中,所以不要忘记了导入头文件,它的功能是对一个容器的所有元素取和。

//参数一:求和的起始范围 迭代器
//参数二:求和的结束范围 迭代器
//参数三:最初的求和值,
int a = accumulate(s1.begin(), s1.end() , 0 );
cout <<a << endl;

//cbegin() 和 cend() 的 c 其实是const的意思。
int a2 = accumulate(s1.begin(), s1.end() , 0 );
cout << a2 << endl;
  • equal

比较两个不同类型的容器是否保存了相同的值,比较的是里面保存的值,容器里面的元素类型要一样。

set<int> s1{1,2,3,4,5};
vector<string> v1{"1","2","3","4","5"};

bool flag = equal(s1.begin(), s1.end(),v1.begin());
cout <<flag << endl;

find

查找容器里是否存在某个值,

//参数一:求和的起始范围 迭代器
//参数二:求和的结束范围 迭代器
//参数三:要查找的值,
#include <algorithm>              //需要导入头文件
vector<int> a = {1, 2, 3, 4};
    auto f = find(a.begin(), a.end(), 1);  //f为迭代器.
    cout << *f << endl;

2. 写入函数

  • fill

给定一个范围,把这个范围的元素全部替换成指定的值。实际上也是填充的意思。

vector<int> v {10,20,30,40,50,60,70};

fill(v.begin() ,v.end() , 0 ); //从起始到结束,全部用0替换。
fill(v.begin()+1 ,v.end() , 0 ); //从第一个元素后面开始替换成0 
  • 拷贝

由于数组是不允许拷贝的,若要对数组进行一次拷贝,通常使用copy 函数来实现

int scores[] {66,77,88,99,100};
int scores2[sizeof(scores) / sizeof(int)];
//参数一,表示起始位置,参数二: 结束为止,参数三:拷贝的目标为止
copy(begin(scores), end(scores) , scores2);
  • 替换

对一个容器中,某一段范围的某一个指定元素,替换成指定的值。

vector<int> v1 {30,20,30,40,30,60};

//把容器中,从开始到结束为止,所有的30,替换成100.
replace(v1.begin(),v1.end(),30 , 100);
  • 容器元素重新排序

有时候有些容器在存储数据时,并没有排序的概念,如果要排序使之输出有一定的格式,可以选择使用sort对容器进行排序

vector<string> v2{"ab","bc","aa","abc","ac"};

//默认使用字母表顺序排序
sort(v2.begin(),v2.end());
  • 删除重复元素

除了set不接受重复元素之外,其他容器都可以接收重复元素,当然map稍微特殊一点,因为它是键值对形式存储,要求的是key不允许重复,值实际上可以重复。

vector<string> v3{"ab","bc","aa","abc","ac","bc","aa","bc"};
//排序
sort(v3.begin(),v3.end());

//确定唯一值,只要是重复元素的,放到后面去 ,前面是不重复的数据,
//也就是重复的元素,把其中一个放到后面去排列
//返回值 end  指向的最后一个不重复的元素位置
auto end =  unique(v3.begin(),v3.end());

//看似是删除了从重复位置到v3的结束位置,实际上并不会删除,只是藏起来了。也不占用size的长度
v3.erase(end , v3.size);

cout <<v3.size() << endl;
 v3.erase(end , v3.end());

  • 自定义排序

默认情况下容器中的排序算法是按照字母表顺序排序,但有时候,我们希望按照自己定义的规则排序,比如我们希望字数多的字符串排在后面,而不是看字母表顺序。mysort 的参数位置还要一个好听的名字,叫做“谓词” ,一元谓词表示这个函数接收一个参数,二元谓词表示接收两个参数,这个函数还要求有一个返回值,以方便底层排序获取我们的定义的排序规则。

//回调函数,由sort内部调用,并且要求返回结果。
bool mysort(const string& a , const string& b){
    return a.size() < b.size();
}

vector<string> v2{"ab","bc","aa","abc","ac"};

//默认使用字母表顺序排序 按照我们规定的排序算法排序
sort(v2.begin(),v2.end() , mysort);

//如果字数一样,长度一样,那么再按字母表顺序排序,可以使用stable_sort
stable_sort(v2.begin(),v2.end() , mysort);

二、函数对象

C++语言中有几种可调用对象:函数函数指针lambda表达式bind创建的对象 以及重载了函数调用运算符的类

1. function 介绍

在前面讲解函数对象的时候,提过,函数对象也是有类型的,函数对象的类型,由函数的返回值类型和参数共同决定。而function的出现让这个定义更为简化。function是一个模板类,可以用来表示函数的类型,当然需要在定义的时候,表示函数的返回值类型以及参数的类型。 除了能使用函数指针来调用函数之外,其实也可以声明一个function的对象,接收某一个函数,直接调用function的对象,也等同于调用函数。

//函数
string getMaxString(string str1 ,string str2 ,int num){}

//函数指针
string (*pfs)(string ,string , int  ) =getMaxString;
pfs();

//使用function对象接收函数 <string (string , string , int)>
//前面的string 表示返回值  <>表示接收的参数类型,个数要对应上。
 function<string  (string , string , int)> f= getMaxString;

2. function 使用

  • 接收全局函数
void print(int a , string b){}

function<void (int ,string)> f = print;
  • 接收lambda表达式
function<int (int ,int)> f = [](int a ,int b){return a + b ;};
f(1 ,2 );
  • 接收成员函数

接收成员函数时,需要额外提供,调用函数的对象实体,可以是对象,可以是引用,可以是指针。也就是参数的第一个位置写调用这个函数的对象实体

class stu{
public:
    int run(string name ,int age){
        cout <<"run" << endl;
    }
};

//实际上我门想的应该是这样: 因为run 函数的返回值是int, 接收两个参数(string ,int)
//但是不能这么做,因为这个函数是非静态函数,也不是全局函数,它是成员函数,不能直接调用,
//如果编译通过,那么直接使用f("aa" , 18) ; 显然是不允许的。
function <int ( string , int )> f1 =  &stu::run;   //错误写法


function <int (stu , string , int )> f1 =  &stu::run; //正确写法 对象
function <int (stu& , string , int )> f2 =  &stu::run; //正确写法 对象引用
function <int (stu* , string , int )> f3 =  &stu::run; //正确写法 指针

//真正调用run();
stu s1;
f1(s1 , "张三" , 18 ); //会发生拷贝

stu s2;
f2(s2 , "张三" , 18 ); //避免发生拷贝,引用指向实体

stu s3;
f3(&s3 , "张三" , 18 );

2. bind函数

c++ 11推出的一套机制bind ,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制常常出现在回调函数中。说的简单点就是,有一个可以直接调用的实体,但是可以对这个实体进行一下包装,包装出来的实体,也可以调用,那么调用这个包装的实体,等同于调用原来的实体。

bind 到底绑的是什么呢?

可以理解为把函数 、函数的实参以及调用函数的对象打包绑定到一起。然后用一个变量来接收它,这个变量也就是这些个打包好的整体。

  • 绑定全局函数
int add3(int a ,int b){
    cout <<"执行了add函数" << endl;
    return a + b;
}
//可以看成是把p 绑定到add3上, 以后使用p() 等同于 add3()
//但是bind的一个好处是,参数不用在后续调用p()的时候传递了,因为在绑定的时候已经传递进来了。
auto p = bind(add3 , 3 ,4 );
int b = p();
cout <<"b = "<< b << endl;

//也可以是用function来接收,但是由于参数的值已经在bind的时候传递进去了,所以接收的时候
//function的参数可以使用() 表示无参。否则在调用p1的时候,还必须传递参数进来
function<int ()> p1 = bind(add3 , 5 ,4 );
int a =p1();
cout <<"a = "<< a << endl;
  • 绑定成员函数

bind 也可以 bind 类中的某个成员函数

class stu{
public :
    int run(string name ,int age){
        cout <<age << " 的 " << name << "在跑步" << endl;
        return 10;
    }
};

//创建对象,也就是一会要调用run函数的对象。
stu s ;
//绑定 stu里面的函数run , s : 表示一会要调用这个函数的对象s
// 张三  和 18  ,分别是调用这个函数的时候传递的参数。
auto b = bind(&stu::run,s,"张三",18 );
b();

//****************************************************
//可以使用function来接收
function<int ()> f= bind(&stud::run , s , "张三" ,18);
f();
  • 使用 placeholders 占位

在绑定的时候,如果一时半会还不想传递实参,那么可以使用 placeholders来占位,第一个占位的索引位置是1,第二个占位的索引是2.

class stu{
public :
    int run(string name ,int age){
        cout <<age << " 的 " << name << "在跑步" << endl;
        return 10;
    }
};


stu s ;
//placeholders::_1 , 表示run的第一个参数,现在先不给,后面调用的时候再给。
//占位的个数不限制,可以全部占位,后面在传递真正的实参。
auto b = bind(&stu::run,s,placeholders::_1,18 );
b("王五");

//当然使用function来接收的话,就显得复杂些,所以平常建议使用auto
//因为在绑定的时候,并没有传递run函数的第一个实参,那么表示这个实参在调用的时候在传
//function在接收的时候,必须与函数完全比配,所以 f的参数必须带上 string
//如果所有的数据都在后面给了,那么<>里面的()就可以空着了
stu s ;
function <int (string)> f = bind(&stu::run,s, placeholders::_1,18 );
f("王五");
  • 0
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值