C++11新特性

移动语义

概念理解

可以取地址的是左值,不能取地址的就是右值。右值短暂的存在于栈上

右值包括:临时对象、匿名对象、字面值常量

const左值引用可以绑定到左值与右值上面。正因如此,也就无法区分传进来的参数是左值还是右值。

右值引用只能绑定到右值,不能绑定到左值。所以可以区分出传进来的参数到底是左值还是右值,进而可以区分。

字面值常量:10 是个右值

字符串常量:"hello" 是个左值

String s3 = "hello";
String s4 = String("world");

移动语义的背景:

String("world") 是一个临时对象,临时对象开辟出一块空间,s4深拷贝后,临时对象开辟的空间立马销毁。但是如果临时对象开辟的空间直接转移给s4,s4也不用深拷贝了,这不就省下开销了吗,对吧。

那如何将临时对象开辟的空间转移给s4呢?那肯定先要识别临时对象是个右值还是右值,如果是右值就进行操作。

结论:非const的左值引用是不能绑定右值的;而const的左值引用既可以绑定左值,也可以绑定右值,但是无法区分左值、右值

C++98困境:不能完成通过代码区分左值与右值

C++11提出概念:右值引用

int &&rref = 10;
int &&rref = a ;//不可以,报错

右值引用可以绑定到右值上,但是不能绑定左值。

移动构造函数

将拷贝构造函数以及赋值运算符函数称为具有复制控制语义的函数。
将移动构造函数以及移动赋值运算符函数称为具有移动语义的函数。

1、移动语义的函数优先于具有拷贝语义的函数的执行
2、具有移动语义的函数如果不写的话,编译器是不会自动生成,必须手写

String(String &&rhs)
: _pstr(rhs._pstr)//将左值指向右值的空间
{
	cout << "String(String &&)" << endl;
	rhs._pstr = nullptr; //右值空间被转移后,使命完成,为避免共同指向销毁,右值指向为空
}

移动赋值运算符函数

String &operator=(String &&rhs)
{
	cout << "String &operator=(String &&)" << endl;
	if(this != &rhs)//1、自移动
	{
		delete [] _pstr;//2、释放左操作数
		_pstr = nullptr;
		_pstr = rhs._pstr;//3、浅拷贝,指向临时开辟的空间
		rhs._pstr = nullptr; //改变临时的指向,防止临时销毁后空间也跟着销毁
	}
	return *this;//4、返回*this
}

std::move 函数

将左值转换为右值,在内部其实上是做了一个强制转换,static_cast<T &&>(lvaule)

//s1是左值,现在将s1转换成右值,右值s1 = s1
s1 = std::move(s1);
cout << "s1 = " << s1 << endl;// 执行为空
//在移动赋值运算函数中,s1指向nullptr,那右边的s1也就为空了,再去给s1赋值,那其实大家都是空指针了嘛,所以打印为空

几个问题注意一下:

1、String("world") = String("world") 这里不存在自复制,因为这里虽然字符串一样,但是确实两个不一样的临时对象,且这样做无意义

2、自移动或者自复制这一步操作还是要有的,就是为了避免出现 s1 = std::move(s1) 这样的操作。

右值引用

右值引用的作用是识别出右值,但是其本身是一个左值。但是右值引用可不可以是一个右值呢?

左值:可以进行取地址的就是左值。
右值:不能进行取地址的就是右值。右值包括:临时变量、临时对象、匿名变量、匿名对象、字面值常量(10)

左值引用:int &ref;左值引用可以绑定到左值,不能绑定到右值。可以识别左值。
const左值引用:既可以绑定到左值,也可以绑定到右值,所以,const左值引用不能区分出左值与右值。
右值引用:int &&rref;右值引用可以绑定到右值,但是不能绑定到左值。可以识别右值。

右值引用的特点是:可以识别右值,可以绑定到右值。但是右值引用本身既可以是左值,也可以是右值。比如:当我们将右值引用作为函数参数的时候,在移动构造函数与移动赋值函数中就是左值,但是如果将函数的返回类型设置为右值引用的时候,那么右值引用本身就是一个右值了。

编译器优化技术—RVO

RVO(Return Value Optimization)

当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。

而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价

http://blog.csdn.net/zwvista/article/details/6845020

http://www.cnblogs.com/xkfz007/articles/2506022.html

资源管理

C的难点背景:

void UseFile(char const* fn){
	FILE* f = fopen(fn, “r”);  // 获取资源
    // 使用资源
    if (!g()) { fclose(f); return; }//关闭文件
    // ...   
    if (!h()) { fclose(f); return; }//关闭文件
    // ...  
    fclose(f);           // 释放资源
}

用于释放资源的代码需要在不同的位置重复书写多次。如果再加入异常处理,fclose(f)情况会变得更加复杂。

为了应对这种问题,c++11提出了 RAII技术(Resource Acquisition Is Initialization)。

RAII 是一种由 C++创造者 Bjarne Stroustrup 提出的, 利用栈对象生命周期管理程序资源(包括内存、文件句柄、锁等)的技术

RAII 思想

利用栈对象的生命周期管理资源(内存资源、文件描述符、文件、锁)。

四个基本特征

1、在构造函数中初始化资源,或者称为托管资源
2、在析构函数中释放资源
3、提供若干访问资源的方法(比如:读写文件)
4、一般不允许复制或者赋值。(对象语义)

对象语义:不允许复制或者赋值。

  • 1、将拷贝构造函数或者赋值运算符函数=delete
  • 2、将拷贝构造函数或者赋值运算符函数设置为私有的
  • 3、可以使用继承的思想,将基类拷贝构造函数与赋值运算符函数删除(设为私有),让派生类继承基类

值语义:可以进行复制或者赋值。

使用 RAII 时,一般在资源获得的同时构造对象, 在对象生存期间,资源一直保持有效;对象析构时,资源被释放。

关键:要保证资源的释放顺序与获取顺序严格相反。

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

class SafeFile{
public:
    //在构造函数中初始化资源(托管资源)
    SafeFile(FILE *fp)
    : _fp(fp){
        cout << "SafeFile(FILE *)" << endl;
    }

    //提供若干访问资源的方法(对文件进行写操作)
    //用C的方法向文件中写数据
    void write(const string &msg){
        fwrite(msg.c_str(), 1, msg.size(), _fp);
    }

    //在析构函数中释放资源
    ~SafeFile(){
        cout << "~SafeFile()" << endl;
        if(_fp){
            fclose(_fp);
            cout << "fclose(_fp)" << endl;
        }
    }

private:
    FILE *_fp;
};

void test(){
    string msg = "hello,world\n";
    SafeFile sf(fopen("wd.txt", "a+"));//sf是栈对象
    sf.write(msg);
    //栈对象随着声明周期的结束,会自动的调用析构函数,完成对文件的close操作
}

int main(int argc, char **argv){
    test();
    return 0;
}


RAII 实现

#include <iostream>

using std::cout;
using std::endl;

class Point
{
public:
    Point(int ix = 0, int iy = 0)
    : _ix(ix)
    , _iy(iy)
    {
        cout << "Point(int = 0, int = 0)"  << endl;
    }

    void print() const
    {
        cout << "(" << _ix
             << ", " << _iy
             << ")" << endl;
    }

    ~Point()
    {
        cout << "~Point()" << endl;
    }
private:
    int _ix;
    int _iy;
};

template <typename  T> //T加相当于任何数据类型有
class RAII
{
public:
    //1、在构造函数中初始化资源
    RAII(T *data)
    : _data(data)
    {
        cout <<"RAII(T *)" << endl;
    }

    //2、在析构函数中释放资源
    ~RAII()
    {
        cout << "~RAII()" << endl;
        if(_data)
        {
            delete _data;
            _data = nullptr;
        }
    }

    //3、提供若干访问资源的方法
    T *operator->()
    {
        return _data;
    }

    T &operator*()
    {
        return *_data;
    }

    T *get() const
    {
        return _data;
    }

    //重置
    void reset(T *data)
    {
        if(_data)
        {
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    //4、不允许复制或者赋值
    RAII(const RAII &rhs) = delete;
    RAII &operator=(const RAII &rhs) = delete;
private:
    T *_data;
    /* int *_data; */
    /* double *_data; */
    /* string *_data; */
    /* Point *_data; */
};

void test()
{
    //利用栈对象的生命周期结束的时候,会执行析构函数,完成资源的清理
    RAII<int> raii(new int(10));//栈对象raii,在尖括号中声明T是int类型,托管堆申请的空间

    cout << endl;
    //pt本身不是一个指针,但是pt的使用就与一个指针完全一样
    //可以托管资源(new int(10),new Point(1, 2)),但是可以
    //不用直接执行delete就可以将资源进行回收,资源的回收靠的
    //就是栈对象pt的销毁执行析构函数
    RAII<Point> pt(new Point(1, 2));//栈对象pt,在尖括号中声明T是Point类型,托管堆申请的空间
    pt->print();
    (*pt).print();
    /* pt.operator->()->print(); */
}

int main(int argc, char **argv)
{
    test();
    return 0;
}


智能指针

C++11提供了以下几种智能指针,位于头文件,它们都是类模板.

智能指针(Smart Pointer):

  • 是存储指向动态分配(堆)对象指针的类
  • 在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象
  • RAII类模拟智能指针,可以利用一个对象在离开一个域中会调用析构函数的特性,在构造函数中完成初始化,在析构函数中完成清理工作,将需要操作和保护的指针作为成员变量放入RAII中。

auto_ptr的使用

#include <iostream>
#include <memory>

using std::cout;
using std::endl;
using std::auto_ptr;

void test(){
    int *pInt = new int(10);
    auto_ptr<int> ap(pInt);
    cout << "*ap = " << *ap << endl;
    cout << "*pInt = " << *pInt << endl;

    cout << endl;
    auto_ptr<int> ap2(ap);
    cout << "*ap2 = " << *ap2 << endl;
    cout << "*ap = " << *ap << endl; //报错,Segmentation fault (core dumped)
}

int main(){
    test();
    return 0;
}

看一眼关于auto_ptr的源码

auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
template <class _Tp> class auto_ptr {
private:
  _Tp* _M_ptr;
public:
    //auto_ptr ap2(ap);
    //auto_ptr &__a = ap
    auto_ptr(auto_ptr& __a)
    : _M_ptr(__a.release())
    {
        
    }
    
    _Tp* release(){	//将ap托管的空间转移给了ap2
    	_Tp* __tmp = _M_ptr;
    	_M_ptr = 0; //ap自身指向为空
    	return __tmp;
  	}
    
};

auto_ptr的缺点:一般都不使用这种指针

表面上执行的是拷贝操作,但是底层己经将右操作数 ap 所托管的堆空间的控制权己经交给了左操作数ap2,并且ap底层的数据成员己经被置位空了,该拷贝操作存在隐患,所以该拷贝操作是存在缺陷的。在拷贝构造或赋值操作时,会发生所有权的转移。

unique_ptr 的使用

明确表明是独享所有权的智能指针,无法进行复制与赋值

保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象。

具有移动语义,可以作为容器的元素(容器中可以存unique_ptr 的类型)

void test()
{
	unique_ptr<int> up(new int(10));
	cout << "*up = " << *up << endl;
	cout << "up.get() = " << up.get() << endl;//获取托管的指针的值
	
    cout << endl ;
 /* unique_ptr<int> up2(up);//error,独享资源的所有权,不能进行拷贝复制 */
	
    cout << endl ;
	unique_ptr<int> up3(new int(10));
 /* up3 = up;//error,不能进行赋值操作 */
//独享所有权的智能指针,对托管的空间独立拥有。在语法层面就将auto_ptr的缺陷摒弃掉了,具有对象语义
/*------------------------------------------------------------------------------------------*/
	
    cout << endl ;
	unique_ptr<int> up4(std::move(up));//通过移动语义move转移up的所有权,初始化智能指针up4
	cout << "*up4 = " << *up4 << endl;
	cout << "up4.get() = " << up4.get() << endl;
	
    cout << endl ;
	unique_ptr<Point> up5(new Point(3, 4));
	vector<unique_ptr<Point>> vect;
    vect.push_back(std::move(up5));//不能传左值,但是可以传右值
    //或者直接构建右值,显式调用unique_ptr的构造函数
	vect.push_back(unique_ptr<Point>(new Point(1, 2)));
	
}

shared_ptr 的使用

是一个共享所有权的智能指针,允许对象之间进行复制或者赋值,展示出来的就是值语义。使用引用计数的观点。当对象之间进行复制或者赋值的时候,引用计数会加+1,当最后一个对象销毁的时候,引用计数减为0的时候,会负责回收托管的空间。

void test()
{
    shared_ptr<int> sp(new int(10));
    cout << "*sp = " << *sp << endl;
    cout << "sp.get() = " << sp.get() << endl;
    cout << "sp.use_count() = " << sp.use_count() << endl; //引用计数

    cout << endl;
    shared_ptr<int> sp2 = sp;//拷贝复制ok
    cout << "*sp = " << *sp << endl;
    cout << "*sp2 = " << *sp2 << endl;
    cout << "sp.get() = " << sp.get() << endl;
    cout << "sp2.get() = " << sp2.get() << endl;
    cout << "sp.use_count() = " << sp.use_count() << endl;
    cout << "sp2.use_count() = " << sp2.use_count() << endl;

    cout << endl;
    shared_ptr<int> sp3(new int(20));
    sp3 = sp;//赋值ok
    cout << "*sp = " << *sp << endl;
    cout << "*sp2 = " << *sp2 << endl;
    cout << "*sp3 = " << *sp3 << endl;
    cout << "sp.get() = " << sp.get() << endl;
    cout << "sp2.get() = " << sp2.get() << endl;
    cout << "sp3.get() = " << sp3.get() << endl;
    cout << "sp.use_count() = " << sp.use_count() << endl;
    cout << "sp2.use_count() = " << sp2.use_count() << endl;
    cout << "sp3.use_count() = " << sp3.use_count() << endl;

    cout << endl;
    vector<shared_ptr<Point>> vec; //作为容器的元素
    shared_ptr<Point> sp4(new Point(10, 20));
    vec.push_back(sp4); //可以传左值
    vec.push_back(std::move(sp4)); //也可以传右值
    vec.push_back(shared_ptr<Point>(new Point(1, 3)));//类名声明一个临时对象指针
}

image-20230412153509786

问题:存在循环引用?

该智能指针在使用的时候,会使得引用计数增加,从而会出现循环引用的问题?两个shared_ptr智能指针互指,导致引用计数增加,不能靠对象的销毁使得引用计数变为0,从而导致内存泄漏。。。

#include <iostream>
#include <memory>

using std::cout;
using std::endl;
using std::shared_ptr;

class Child;//类的前向声明

class Parent{
public:
    Parent(){
        cout << "Parent()" << endl;
    }

    ~Parent(){
        cout << "~Parent()" << endl;
    }
    shared_ptr<Child> parent;
};

class Child{
public:
    Child(){
        cout << "Child()" << endl;
    }

    ~Child(){
        cout << "~Child()" << endl;
    }
    shared_ptr<Parent> child;
};

void test()
{
    shared_ptr<Parent> pParent(new Parent());
    shared_ptr<Child> pChild(new Child());
    cout << "pParent.use_count() = " << pParent.use_count() << endl;
    cout << "pchild.use_count() = " << pChild.use_count() << endl;

    cout << endl;
    pParent->parent = pChild;
    pChild->child = pParent;
    cout << "pParent.use_count() = " << pParent.use_count() << endl;
    cout << "pchild.use_count() = " << pChild.use_count() << endl;
    //此代码存在内存泄漏
}

int main(int argc, char **argv){
    test();
    return 0;
}
image-20230412155106117

模板

1、简化程序,少写代码,维持结构的清晰,大大提高程序的效率。

2、解决强类型语言的严格性灵活性之间的冲突。。

2.1、带参数的宏定义(原样替换)

2.2、函数重载(函数名字相同,参数不同)

2.3、模板(将数据类型作为参数)

模板定义

template <typename T> //模板参数列表
template <class T>	//模板参数列表

注意:class与typename在此没有任何区别,完全一致。class出现的时间比较早,通用性更好一些,typename是后来才添加了,对于早期的编译器可能识别不了typename关键字。

模板类型

函数模板与类模板。通过参数实例化构造出具体的函数或者类,称为模板函数或者模板类。

函数模板和类模板的区别在于:

1,函数模板有自动类型推导,但是类模板没有自动类型推导,必须指定模板参数类型;

2,函数模板不可以有默认参数,但是类模板允许有默认参数,当类模板中有默认参数的时候,就可以把显示指定类型的参数去掉。

函数模板

template <模板参数表> //模板参数列表,此处模板参数列表不能为空

返回类型 函数名(参数列表)

{ //函数体 }

template <typename T>//模板参数列表
T add(T x, T y)
{
	cout << "T add(T, T)" << endl;
	return x + y;
}
				模板参数推导 (在实参传递的时候进行推导)

函数模板------------------------------------------------------------------------模板函数
实例化

https://gitee.com/demoCanon/cpp-gpt/blob/master/Project/week4/functionTemplate.cc

实例化:隐式实例化与显示实例化

cout << "add(ia, ib) = " << add(ia, ib) << endl;//隐式实例化,没有明确说明类型,T只能靠编译器推导

cout << "add(da, db) = " << add<double>(da, db) << endl;//显示实例化,显式写出参数类型T为double

函数模板、普通函数间的关系

1、函数模板与普通函数是可以进行重载的

2、普通函数优先于函数模板执行

3、函数模板与函数模板之间也是可以进行重载的

模板头文件与实现文件

模板不能写成头文件与实现文件形式(类型inline函数),或者说不能将声明与实现分开,这样会导致编译报错。

分开可以编译,但是在链接的时候是有问题的。

建议在头文件给出模板的声明后,立即include引入实现文件

image-20230410171120223

模板的特化:偏特化与全特化

template <> //全部特化出来就是全特化
//因为模板的所有参数都以特殊形式写出来了,所以模板列表写为空
const char *add(const char *pstr1, const char *pstr2)
{
	size_t len = strlen(pstr1) + strlen(pstr2) + 1;
	char *ptmp = new char [len]();
	strcpy(ptmp, pstr1);
	strcat(ptmp, pstr2);
	return ptmp;
}

注意:函数模板是支持全特化的,但是不同的编译器,不同的语法版本,可能不支持偏特化,不支持部分特化。

函数模板的参数类型

1、类型参数,class T 这种就是类型参数

2、非类型参数 常量表达式,整型:bool/char/short/int/long/size_t, 注意:float/double这些就不是整型

https://gitee.com/demoCanon/cpp-gpt/blob/master/Project/week4/templatePara.cc#

成员函数模板

#include <iostream>

using std::cout;
using std::endl;

class Point{
public:
    Point(double dx = 0.0, double dy = 0.0)
    : _dx(dx)
    , _dy(dy)
    {
        cout << "Point(double,double)" << endl;
    }

    //普通函数与成员函数都可以设置为模板形式
    template <typename T = double>
    T func(){
        return (T)_dx;
    }

    ~Point(){
        cout << "~Point()" << endl;
    }

private:
    double _dx;
    double _dy;
};

void test(){
    Point pt(1.1, 2.2);

    cout << "pt.func() = " << pt.func<int>() << endl;//使用传入参数int

    cout << "pt.func() = " << pt.func() << endl;//使用类型参数默认值double
}


int main()
{
    test();
    return 0;
}


可变模板参数

可以表示0个到任意个参数,参数的个数不确定,参数的类型也不确定。可以类比printf进行理解。

https://gitee.com/demoCanon/cpp-gpt/blob/master/Project/week4/variTemplate.cc

image-20230410195807447 image-20230410195821495 image-20230410195837134

类模板

template <typename T>
class Stack
{
private:
    T *_data;
    //int *_data;
    //double *_data;
    //Point *_data;
};

注意:成员模版不能声明为虚函数。写成模板相当于写了好多个函数,如果再将这些函数设计为虚函数,而虚函数会入虚表,那么都不知道模板可以推导出多少个函数,那么虚表就不知道分配多少空间。

模板是可以进行嵌套的。

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

//泛型编程
template <typename T = int, size_t sz = 10>
class Stack
{
public:
    Stack()
    : _top(-1)
    , _data(new T[sz]())
    {

    }
    ~Stack();
    bool empty() const;
    bool full() const;
    void push(const T &value);
    void pop();
    T top() const;
private:
    int _top;
    T *_data;
};

template <typename T, size_t sz> //头必须写出来,但是默认值前面已经给出,无需初始化
Stack<T, sz>::~Stack() //用类声明的时候也需要将模板参数显式亮出来
{
    if(_data)
    {
        delete [] _data;
        _data = nullptr;
    }
}

template <typename T, size_t sz>
bool Stack<T, sz>::empty() const
{
    return -1 == _top;
}

template <typename T, size_t sz>
bool Stack<T, sz>::full() const
{
    return _top == sz - 1;
}

template <typename T, size_t sz>
void Stack<T, sz>::push(const T &value)
{
    if(!full())
    {
        _data[++_top] = value;
    }
    else
    {
        cout << "The stack is full, cannot push any data" << endl;
        return;
    }
}

template <typename T, size_t sz>
void Stack<T, sz>::pop()
{
    if(!empty())
    {
        --_top;
    }
    else
    {
        cout << "The stack is empty, cannot pop any data" << endl;
        return;
    }
}

template <typename T, size_t sz>
T Stack<T, sz>::top() const
{
    return _data[_top];
}

void test()
{
    Stack<int> st;
    cout << "栈是不是空的 ?" << st.empty() << endl;
    st.push(1);
    cout << "栈是不是满的 ?" << st.full() << endl;

    for(size_t idx = 2; idx != 15; ++idx)
    {
        st.push(idx);
    }
    cout << "栈是不是满的 ?" << st.full() << endl;

    while(!st.empty())
    {
        cout << st.top() << "  ";
        st.pop();
    }
    cout << endl;
    cout << "栈是不是空的 ?" << st.empty() << endl;
}

void test2()
{
    Stack<string, 20> st;

    cout << "栈是不是空的 ?" << st.empty() << endl;
    st.push(string("aa"));
    cout << "栈是不是满的 ?" << st.full() << endl;

    for(size_t idx = 1; idx != 15; ++idx)
    {
        st.push(string(2, 'a' + idx));
    }
    cout << "栈是不是满的 ?" << st.full() << endl;

    while(!st.empty())
    {
        cout << st.top() << "  ";
        st.pop();
    }
    cout << endl;
    cout << "栈是不是空的 ?" << st.empty() << endl;
}

int main(int argc, char **argv)
{
    test2();
    return 0;
}

容器

Standard Template Library标准模板库,有6大组件

1、容器(数据结构):用来存数据的。

  • 序列式容器:vector 、 deque 、list
  • 关联式容器:set 、 map
  • 无序关联式容器:unordered_set 、 unordered_map

2、迭代器:看是一种指针,称为泛型指针。

3、算法:可以对容器中的数据进行相应的操作

4、适配器

  • 容器适配器 优先级队列
  • 算法适配器
  • 迭代器的适配器(源码)

5、函数对象(仿函数):做定制化操作

6、空间配置器 ( Allocator ) :空间的申请与释放。(研究基本使用+原理+源码)

程序 = 容器(数据结构)+ 算法


dynamic动态

deque双端队列

序列式容器

image-20230412091809472

初始化

三种序列式容器vector、deque、list完全支持五种初始化的方式

void test(){
    //-----------------------------初始化-------------------------------------
    //1、创建空对象 
    vector<int> number;

    //2、创建count个value
    vector<int> number2(10,3); //3是10个元素的初始化值

    //3、迭代器范围
    int arr[10] = {1,3,5,7,9,10,8,6,4,2};
    vector<int> number3(arr,arr + 10);//左闭右开的区间[,)这里的10指的是数组长度

    //4、拷贝构造函数与移动构造函数
    vector<int> number4_1 (number2); //拷贝构造函数
    vector<int> number4_2 (std::move(number2));//移动构造函数
    
    //5、大括号
    vector<int> number5 = {1,3,5,9,7,2,4,6,8};

//只需要将vector替换为对应的deque 与 list

遍历

4种遍历方式

三种序列式容器,其中:list是没有下标的,所以不支持下标访问运算符。其他的三种遍历的方式,三种容器都支持。

//只需要将vector替换为对应的deque 与 list
vector<int> number2(10,3);
void test(){
    //----------------------------遍历操作------------------------------------
#if 0 
    //1、使用下标进行遍历,只有list不支持下标访问
    for(size_t idx = 0; idx != number2.size();++idx){
        cout << number2[idx] << "  ";
    }
    cout << endl;
#endif
    
    //2、迭代器:泛型指针
    vector<int>::iterator it; //没有初始化迭代器it
    for(it = number.begin();it != number.end(); ++it){
        cout << *it << "  ";
    }
    cout << endl;
    
    //3、迭代器(对迭代器进行初始化)
    vector<int>::iterator it2 = number.begin();
    for(;it2 != number.end(); ++it2){
        cout << *it2 << "  ";
    }
    cout << endl;

    //4、for与auto,C++11
    for(auto &elem : number){ //&是引用的意思,减少拷贝操作
        cout << elem << "  ";
    }
    cout << endl;

}

注意:list不支持下标

image-20230412161507181
//display 打印函数
template <typename Container>
void display(const Container &con)
{
    for(auto &elem : con)
    {
        cout << elem << "  ";
    }
    cout << endl;
}

在尾部进行插入与删除

三种容器在尾部进行插入与删除的使用是完全一样的。

void test()
{
    cout << endl << "----------------初始化vector容器-----------------" << endl;
    vector<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number); //打印容器number的值                 
    cout << "number.size() = " << number.size() << endl //打印容器存储量
         << "number.capacity() = " << number.capacity() << endl ; //打印容器空间量

    cout << endl << "----------------在vector的尾部进行插入与删除------------------" << endl;
    cout << "在vector的尾部push_back插入2个元素,打印如下:" << endl;
    number.push_back(33);
    number.push_back(77);
    display(number);
    cout << "number.size() = " << number.size() << endl
         << "number.capacity() = " << number.capacity() << endl << endl;
    
    cout << "在vector的尾部pop_back删除元素,打印如下:" << endl;
    number.pop_back(); //删除尾部元素
    display(number);
    cout << "number.size() = " << number.size() << endl
         << "number.capacity() = " << number.capacity() << endl;
    
    cout << endl;
}

void test2(){
    cout << endl << "----------------初始化deque容器-----------------" << endl;
    deque<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number);

    cout << endl << "在deque的尾部push_back进行插入" << endl;
    number.push_back(33);
    number.push_back(77);
    display(number);
    cout << endl << "在deque的尾部pop_back进行删除" << endl;
    number.pop_back();
    display(number);
    
    cout << endl;
}
    
void test3(){
    cout << endl << "----------------初始化list容器-----------------" << endl;
    list<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number);

    cout << endl << "在list的尾部push_back进行插入" << endl;
    number.push_back(33);
    number.push_back(77);
    display(number);
    cout << endl << "在list的尾部pop_back进行删除" << endl;
    number.pop_back();
    display(number);
    
    cout << endl;
}

在头部进行插入与删除

deque与list支持在头部进行插入与删除,但是vector没有在头部进行插入与删除的操作。

void test2(){
    cout << endl << "----------------初始化deque容器-----------------" << endl;
    deque<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number);
	
    cout << endl << "在deque的头部push_front进行插入" << endl;
    number.push_front(100);
    number.push_front(200);
    display(number);
    cout << endl << "在deque的尾部pop_front进行删除" << endl;
    number.pop_front();
    display(number);
    
    cout << endl;
}
  
void test3(){
    cout << endl << "----------------初始化list容器-----------------" << endl;
    list<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number);
	
    cout << endl << "在list的头部push_front进行插入" << endl;
    number.push_front(100);
    number.push_front(200);
    display(number);
    cout << endl << "在list的尾部pop_front进行删除" << endl;
    number.pop_front();
    display(number);
    
    cout << endl;
}
  

Q:为什么vector不支持在头部进行插入与删除?

A:因为vector中的元素是连续的,如果在头部删除一个元素,那么后面的元素都会前移;如果在头部进行插入一个元素,那么后面的所有的元素都会后移,这样会导致一个元素的插入与删除,使得整个vector中的元素都要变动位置,时间复杂度O(N)

//Q:如何获取vector第一个元素的地址?
&number;  error,获取不到第一个元素的地址 */
&number[0];  ok,可以获取第一个元素的地址 */
&*number.begin();  ok,可以获取第一个元素的地址 */
int *pdate = number.data();  ok,可以获取第一个元素的地址 */
image-20230412205943097

vector的原理图

image-20230412210105184

_M_start:指向第一个元素的位置

_M_finish:指向最后一个元素的下一个位置

_M_end_of_storage:指向当前分配空间的最后一个位置的下一个位置

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector
{
public:
    //类型萃取(重要)
    typedef _Tp value_type;
    typedef value_type* pointer;
    typedef const value_type* const_pointer;
    typedef value_type* iterator;
    typedef const value_type* const_iterator;
    typedef value_type& reference;
    typedef const value_type& const_reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    
    //为了严格表明_Base::allocator_type是一种类型,所以加上typename关键字进行强调
    typedef typename _Base::allocator_type allocator_type;
    
    //vector的下标访问运算符的重载
    reference operator[](size_type __n)
    { 
        return *(begin() + __n); 
    }
    
    void _M_range_check(size_type __n) const 
    {
       if (__n >= this->size())
           __stl_throw_range_error("vector");
    }

    //总结:通过下标获取元素,可以使用at,也可以直接使用下标,但是at比下标访问更安全,因为有范围检查
    reference at(size_type __n)
    {
		_M_range_check(__n); 
		return (*this)[__n]; 
    }
};
image-20230412211434289

deque的原理图

image-20230412211554701 image-20230412211628202

注意:deque是由很多小片段组成的,片段内部是连续的,片段之间是不连续。所以可以这么说,逻辑上是连续的,但是物理上是不连续。可以通过中控器数组进行调控。

在任意位置进行插入

三种序列式容器在任意位置插入元素的四种方法

image-20230413092532259
void test3(){
    list<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
	cout << endl << "在list的任意位置进行插入元素" << endl;
    list<int>::iterator it = number.begin();
    ++it;
    ++it; //迭代器向后移动2个位置
    
    cout << "*it = " << *it << endl;
    number.insert(it, 100); //1、找一个位置,插入一个元素
    display(number);
    cout << "*it = " << *it << endl;

    cout << endl << endl;
    number.insert(it, 30, 200);//2、找一个位置,插入count30个相同元素
    display(number);
    cout << "*it = " << *it << endl;

    cout << endl << endl;
    vector<int> vec = {77, 99, 33, 55};
    number.insert(it, vec.begin(), vec.end()); //3、找一个位置,插入迭代器范围的元素
    display(number);
    cout << "*it = " << *it << endl;

    cout << endl << endl;
    number.insert(it, {111, 444, 666, 888, 333}); //4、找一个位置,插入一个大括号范围的元素
    display(number);
    cout << "*it = " << *it << endl;

}

总结:对于list而言,插入的时候,迭代器是跟着结点走的,它在使用的时候,不会存在什么问题。但是对于vector而言,插入元素的大小会影响vector底层的扩容,有可能会导致迭代器失效。所以为了解决这个问题,每次操作迭代器时候,最好可以提前更新迭代器的位置,让迭代器指向新的空间的位置,这样就保证不会出错。

特意的,对于deque而言,去进行insert操作的时候,也有可能迭代器失效,所以最好还是可以每次使用迭代器的时候,像vector一样,将迭代器的位置进行更新,指向新的空间的位置。

插入一个链接

vector删除连续重复元素

vector删除连续重复元素的隐患:

void test()
{
    vector<int> number = {1, 3, 6, 6, 6, 6, 7, 5};
    display(number);
    //这种方式不能删除连续重复元素
    for(auto it = number.begin(); it != number.end(); ++it)
    {
        if(6 == *it)
        {
            number.erase(it);
        }
    }
    display(number);
}
image-20230413113028660

原因是:每删除一个元素,vector的元素便向前移动位置,但是迭代器的位置不变,迭代器所在位置的数值发生变化,接着进入下一次的循环更新迭代器的位置,导致连续重复的元素逃过了判断,从而漏掉。

正确解决办法:

void test2()
{
    vector<int> number = {1, 3, 6, 6, 6, 6, 7, 5};
    display(number);
    for(auto it = number.begin(); it != number.end(); )  //不用在更新迭代器的位置
    {
        if(6 == *it)
        {
            number.erase(it);
        }
        else //如果不是该数字,才会去更新迭代器的位置
        {
            ++it; 
        }
    }
    display(number);
}

void test3()
{
    vector<int> number = {1, 3, 6, 6, 6, 6, 7, 5};
    display(number);
    for(auto it = number.begin(); it != number.end(); ++it)
    {
        while(6 == *it) //循环检测要删除的元素,如果不是连续元素就跳出(不更新迭代器的位置,在原位置上检测)
        {
            number.erase(it); 
        }
    }
    display(number);
}

deque与list就不会出现,因为vector是属于动态数组的,deque与list是属于链表的。

元素的清空

cout << endl << "清空vector容器中的元素" << endl;
number.clear();//只是将元素清空了,但是没有回收内存
number.shrink_to_fit();//将剩余的空间进行了回收
cout << "number.size() = " << number.size() << endl
     << "number.capacity() = " << number.capacity() << endl;
//------------------------------------------------------------------
cout << endl << "清空deque容器中的元素" << endl;
number.clear();//只是将元素清空了,但是没有回收内存
number.shrink_to_fit();//将剩余的空间进行了回收
cout << "number.size() = " << number.size() << endl;
//-----------------------------------------------------------------------
//对于list而言,元素删除了也会将结点删除,就不存在回收空间函数
cout << endl << "清空list容器中的元素" << endl;
number.clear();
cout << "number.size() = " << number.size() << endl;

emplace_back的使用

在容器末尾就地构造元素

void test()
{
    vector<Point> number;
    Point pt(1, 2);
    number.push_back(Point(1, 2));
    //number.push_back(1, 2); error!
    number.push_back((1, 2));
    number.push_back({1, 2});
    number.push_back(pt);
    number.emplace_back(3, 4); //免去大括号与Point声明
}

size()获取容器中元素个数,三者都有该函数

capacity()获取容器申请的空间大小,只有vector有这个函数。

list的特殊用法

排序函数sort

image-20230413134629671

存在两种形式的sort函数

void test()
{
    list<int> number = {1, 3, 5, 5, 9, 9, 8, 6, 2};
    display(number);

    cout << endl << "测试list的sort函数" << endl;
    number.sort();//1、默认情况,会按照从小到大的顺序进行排序
    
    //std::less<T> 如果lhs < rhs 就返回true,否则返回false
    std::less<int> les; //从小到大进行排序
    number.sort(std::less<int>()); //传递右值
    number.sort(les); //传递左值 
    
    //std::greater<T> 如果lhs > rhs 就返回true,否则返回false
    number.sort(std::greater<int>()); //从大到小进行排序
    display(number);
}

Compare是个类,比较的是对象。

可以看一下源码:

image-20230413153256848 image-20230413153428133

_StrictWeakOrdering 是个什么东西呢? 翻译为:严格弱排序。

严格是说在判断的时候会用"<",而不是"<=",弱排序是因为,一旦"<"成立便认为存在"<"关系,返回ture,而忽略了"="关系和">"区别,把它们归结为false。

https://www.boost.org/sgi/stl/StrictWeakOrdering.html

严格弱排序是一个二元谓词,它对两个对象进行比较,如果第一个对象在第二个对象之前,则返回真。这个谓词必须满足严格弱排序的标准数学定义。准确的要求在下面说明,但它们的大致意思是,严格弱排序的行为方式必须是 "小于 "的行为方式:如果a小于b,则b不小于a,如果a小于b且b小于c,则a小于c,以此类推。

image-20230413154122470

所以我们了解到无参sort的排序是按照严格弱排序的,而有参sort给出了一个模板,_StrictWeakOrdering告诉你,sort也应该是弱排序,但毕竟是模板,可以传入的不一定非要是严格弱排序,也可以是其他数学定义的排序方式。所以也可以传入严格强排序。

template <class _Tp> //严格弱排序
struct less : public binary_function<_Tp,_Tp,bool> 
{
  bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } // < 运算符重载
};
template <class _Tp> //严格强排序
struct greater : public binary_function<_Tp,_Tp,bool> 
{
  bool operator()(const _Tp& __x, const _Tp& __y) const { return __x > __y; } // > 运算符重载
};

实现list的sort

//自定义函数对象(仿函数)
template <typename T>
struct CompareList //模仿less和greater的定义
{
    bool operator()(const T &lhs, const T &rhs) const
    {
        cout << "bool operator()(const T &, const T &) const" << endl;
        return lhs < rhs;
    }
};

void test()
{
    list<int> number = {1, 3, 5, 5, 9, 9, 8, 6, 2};
    display(number);

    cout << endl << "测试list的sort函数" << endl;
    number.sort(CompareList<int>()); //自己实现比较原则
    display(number);
}

函数对象:

函数对象是重载函数调用操作符的类的对象。即函数对象是行为类似函数的对象,又称仿函数,是一个能被当做普通函数来调用的对象。

reverse 反转序列 与 unique去重元素

void test()
{
	list<int> number = {1, 3, 5, 5, 9, 9, 8, 6, 2, 5, 9, 5};
    display(number);

    cout << endl << "测试list的reverse函数" << endl;
    //reverse实现的是反转容器序列
    number.reverse();
    display(number);

    cout << endl << "测试list的unique函数" << endl;
    number.sort();
    number.unique();//需要先将元素进行排序,然后才能去除重复元素
    display(number);
    
}

merge 合并vector

两个链表要进行merge的时候,需要先各自进行相同的排序,然后再进行merge,否则不排序后的合并没有意义。

void test()
{
	cout << endl << "---------测试list的merge函数-----------" << endl;
    list<int> number2 = {11, 99, 88, 33, 7};
    number2.sort();
    cout << "number2链表: ";
    display(number2);
    cout << "number 链表: ";
    display(number);
    //两个链表要进行merge的时候,需要先分别将两个链表进行从小
    //到大排序,然后再进行merge,否则就达不到合并后排序的效果
    cout << "合并操作: number.merge(number2); " << endl;
    number.merge(number2);
    cout << "number2链表: ";
    display(number2);
    cout << "number链表: ";
    display(number);

    

    cout << endl << "从大到小合并:"<< endl;
    number.sort(std::greater<int>());
    number2.sort(std::greater<int>());
    number.merge(number2);
     cout << "number2链表: ";
    display(number2);
    cout << "number链表: ";
    display(number);
}
image-20230413191045657

splice操作

从一个 list 转移元素给另一个。不复制或移动元素,仅重指向链表结点的内部指针。

1、移动一个容器内的所有元素

void test(){
    cout << endl << "测试splice函数" << endl;
    list<int> number = {1,2,3,5,6,8,9 };
    list<int> number3 = {111, 555, 777, 333, 666};
    display(number3);
    auto it = number.begin();
    ++it;
    ++it;
    cout << "*it = " << *it << endl;
    number.splice(it, number3); 
    display(number); 
    display(number3);
}
image-20230413200952776

结果分析:splice也如同merge一样,将number3的元素移动到number后,number3自身的元素啥也没有了。和merge不一样的是,splice可以在指定的位置下进行转移元素。

2、移动一个容器内的一个元素

void test(){
	cout << endl << "测试splice函数" << endl;
    list<int> number = {1,2,3,5,6,8,9 };
    list<int> number3 = {111, 555, 777, 333, 666};
    cout << "number链表: ";
    display(number);
    cout << "number3链表: ";
    display(number3);
    
    auto it = number.begin();
    ++it;
    ++it;
    cout << "*it = " << *it << endl;
    auto it2 = number3.begin();
    ++it2;
    cout << "*it2 = " << *it2 << endl;
    number.splice(it, number3, it2); //将number3中it2位置下的元素移动到number中的it位置下
    
    cout << "number链表: ";
    display(number);
    cout << "number3链表: ";
    display(number3);
}
image-20230413202121065

3、移动一个容器内的范围元素

void test()
{
	cout << endl << "测试splice函数" << endl;
    list<int> number = {1,2,3,5,6,8,9 };
    list<int> number3 = {111, 555, 777, 333, 666};
    cout << "number链表: ";
    display(number);
    cout << "number3链表: ";
    display(number3);
    auto it = number.begin();
    ++it;
    ++it;
    cout << "*it = " << *it << endl;

    auto it2 = number3.begin();
    ++it2;
    cout << "*it2 = " << *it2 << endl;
    auto it3 = number3.end();
    --it3;
    cout << "*it3 = " << *it3 << endl;
    //将number3中it2到it3范围内的元素移动到number中的it位置下
    number.splice(it, number3, it2, it3);//[it2, it3)左闭右开
    cout << "number链表: ";
    display(number);
    cout << "number3链表: ";
    display(number3);
}
image-20230413202930821

4、移动容器自身内的元素 (避免使用,会出现问题)

void test(){
	auto it4 = number.begin();
    ++it4;
    ++it4;
    cout << "*it4 = " << *it4 << endl;
    auto it5 = number.end();
    --it5;
    --it5;
    cout << "*it5 = " << *it5 << endl;
    
    number.splice(it4, number, it5); //迭代器it4与it5都是number自身的元素
    display(number);
}

vector的insert插入扩容(了解)

void test()
{
    vector<int> number = {1, 3, 5, 9, 7, 4, 6, 2, 10, 3};
    display(number);
    cout << "number.size() = " << number.size() << endl
         << "number.capacity() = " << number.capacity() << endl;

    cout << endl << "在vector的尾部进行插入与删除" << endl;
    number.push_back(33);
    display(number);
    cout << "number.size() = " << number.size() << endl
         << "number.capacity() = " << number.capacity() << endl;

    cout << endl << "在vector的任意位置进行插入元素" << endl;
    vector<int>::iterator it = number.begin();
    ++it;
    ++it;
    cout << "*it = " << *it << endl;
    number.insert(it, 100);
    display(number);
    cout << "number.size() = " << number.size() << endl
         << "number.capacity() = " << number.capacity() << endl;
    cout << "*it = " << *it << endl;
}
image-20230413205710784

结果分析:因为push_back每次只能插入一个元素,所以按照2 * capacity进行扩容是没有任何问题的,但是insert进行插入元素的时候,插入元素

的个数是不确定的,所以不能按照2 * capacity进行扩容

设置 size() = m = 12, capacity() = n = 20, 待插入的元素个数t

  1. 当插入元素的个数t < n - m,此时就不会扩容
  2. 当插入元素的个数 n - m < t < m,此时就按照2 * m进行扩容
  3. 当插入元素的个数 n - m < t, m < t < n,此时就按照t + m进行扩容
  4. 当插入元素的个数 n - m < t, t > n,此时就按照t + m进行扩容

你可以试一下。

关联式容器

关联容器实现能快速查找(O(log n) 复杂度)的数据结构。

set 唯一键的集合,按照键排序 (类模板)
map 键值对的集合,按照键排序,键是唯一的 (类模板)
multiset 键的集合,按照键排序 (类模板)
multimap 键值对的集合,按照键排序 (类模板)

set 使用

set的特征:

  • key值是唯一的,不能重复
  • 默认情况下,会按照key值升序排列
  • set的底层使用的是红黑树数据结构

const 与 find 查找

void test(){
	cout << "------------set的初始化------------" << endl;
    set<int> number = {1, 3, 5, 5, 7, 5, 9, 2, 4};
    /* set<int, std::greater<int>> number = {1, 3, 5, 5, 7, 5, 9, 2, 4}; */
    display(number);
    cout << endl ;

    cout << "----------set中的count查找----------" << endl;
    size_t cnt1 = number.count(7);
    cout << "查找为7的元素是否在容器内? cnt1为1表示存在, 为0表示不存在" << endl;
    cout << "cnt1 = " << cnt1 << endl << endl;

    cout << "----------set中的find查找----------" << endl;
    /* set<int>::const_iterator it = number.find(10);  //OK的*/
    set<int>::iterator it = number.find(10);
    if(it == number.end()) //迭代器判空的独特方法,不能用nullptr
    {
        cout << "该为10的元素不存在set中, 查找失败" << endl;
    }
    else{   
        cout << "该为10的元素存在set中" << *it << endl;
    }
    cout << endl;
}
image-20230413214649264

insert 插入

注意:set容器内如果插入已经存在的元素,则不会重复插入,会被过滤掉。

void test(){
    set<int> number = {1, 3, 5, 5, 7, 5, 9, 2, 4};
    
	cout << "----------set中的insert插入----------" << endl;
    cout << "原number的元素有: ";
    display(number);
    
    //set的insert单独插入一个数返回的是pair迭代器类型的
    pair<set<int>::iterator, bool> ret = number.insert(8);
    if(ret.second)
    {
        cout << "该元素插入成功 " << *ret.first << endl;
    }
    else
    {
        cout << "该元素插入失败,该元素已经存在set中" << endl;
    }
    display(number);
    cout << endl ;
    
    //set的insert插入另一个容器范围的元素
    vector<int> vec = {1, 3, 7, 9, 11, 45, 7, 9, 5, 7};
    number.insert(vec.begin(), vec.end());
    display(number);
    cout << endl ;

    //set的insert插入大括号内的元素
    number.insert({13, 56, 78, 23, 11, 14});
    display(number);
}
image-20230413215201616

下标和修改操作

void test(){
    set<int> number = {1, 3, 5, 5, 7, 5, 9, 2, 4};
	cout << endl << "set的下标操作" << endl;
    //set是不支持下标的
    /* cout << "number[1] = " << number[1] << endl; */

    cout << endl << "set的修改操作" << endl;
    it = number.end();
    --it;
    --it;
    cout << "*it = " << *it << endl;
    /* *it = 123;//error, 不能修改 */
}

实现 set 的类对象类型元素的排序

先了解一下

template<
    class Key,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<Key>
> class set;

//看一下构造函数有关于大括号的构造
set( std::initializer_list<value_type> init,
     const Compare& comp = Compare(),
     const Allocator& alloc = Allocator() );

//less的比较运算符重载
constexpr bool operator()(const T &lhs, const T &rhs) const 
{
    return lhs < rhs; // 
}
#include <math.h>
#include <iostream>
#include <set>
#include <utility>
#include <vector>

using std::cout;
using std::endl;
using std::set;
using std::pair;
using std::vector;

template <typename Container>
void display(const Container &con)
{
    for(auto &elem : con)
    {
        cout << elem << "  ";
    }
    cout << endl;
}


class Point
{
public:
    Point(int ix = 0, int iy = 0)
    : _ix(ix)
    , _iy(iy)
    {
        /* cout << "Point(int = 0, int = 0)" << endl; */
    }

    double getDistance() const
    {
        return hypot(_ix, _iy);
    }

    void print() const
    {
        cout << "(" << _ix 
             << ", " << _iy
             << ")";
    }

    ~Point()
    {
        /* cout << "~Point()" << endl; */
    }

    friend std::ostream &operator<<(std::ostream &os, const Point &rhs);
    friend bool operator<(const Point &lhs, const Point &rhs);

private:
    int _ix;
    int _iy;
};

std::ostream &operator<<(std::ostream &os, const Point &rhs)
{
    os << "(" << rhs._ix 
       << ", " << rhs._iy
       << ")";

    return os;
}

bool operator<(const Point &lhs, const Point &rhs) //会在less的函数对象中触发 < 
{
    cout << "bool operator<(const Point &, const Point &)" << endl;
    //按照距离的远近进行比较
    if(lhs.getDistance() < rhs.getDistance())
    {
        return true;
    }
    else if(lhs.getDistance() == rhs.getDistance())
    {
        if(lhs._ix < rhs._ix)
        {
            return true;
        }
        else if(lhs._ix == rhs._ix)
        {
            if(lhs._iy < rhs._iy)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    else
    {
        return false;
    }
}
void test2()
{
    set<Point> number = {
        Point(-1, 2),
        Point(1, -2),
        Point(1, 2),
        Point(2, -2),
        Point(3, 5),
        Point(1, 2)
    };

    display(number);
}

int main(int argc, char **argv)
{
    test2();
    return 0;
}


image-20230413224604737

运行结果:之所以set中插入Point对象数据后,能够排序,是因为在less中比较两个对象时,触发了 < 比较运算符的我们自己写的重载。第二种方式:当然你也可以用函数对象去实现。不过要设计成友元的方式,不然无法访问内部私有成员。

image-20230414084950887

第3种方式,对std::less的模板进行特化处理。

namespace std
{
//模板的特化
template<>
struct less <Point>
{
	bool operator()(const Point &lhs,const Point &rhs){
		..... //不过要用get方法去获取私有成员
	}
};
} //end of namespace std

针对于自定义类型的三种形式(重要)

1、模板的特化
image-20230414090634321
2、函数对象的形式
image-20230414090644223
3、运算符重载形式
image-20230414090655235

multiset的使用

multiset的特征:

  • key值是不唯一的,可以重复
  • 默认情况下,会按照key值升序排列
  • multiset的底层使用的是红黑树数据结构

除去insert的返回值是pair类型的双迭代器,其他的操作均与set效果相同。看一下multiset独有的bound函数

bound函数

image-20230414092838960 image-20230414092851539

注意:set与multiset针对于自定义类型的写法完全等同,都可以针对于自定义类型的三种形式完全一样。

map 使用

map的特征

  • map中存放的是pair,也就是key-value类型,key值是惟一的
  • 不能重复,但是value是可以重复的,也可以不重复
  • 默认情况下,也会按照key值进行升序排列
  • 底层实现也是使用的红黑树

初始化

template <typename Container>
void display(const Container &con)
{
    for(auto &elem : con)
    {
        cout << elem.first << "  " << elem.second << endl;
    }
}
void test()
{
    /* map<int, string, std::greater<int>> number = { */
	map<int, string> number = {
    	pair<int, string>(8, "wuhan"),
    	pair<int, string>(6, "wuhan"),
    	pair<int, string>(9, "hubei"),
    	{1, "beijing"},
    	{1, "dongjing"},
    	{2, "nanjing"},
    	make_pair(3, "tianjin"),
    	make_pair(8, "wuhan"),
    	make_pair(10, "shenzhen")
    };
    display(number);
}

map的查找

void test()
{
	cout << endl << "map的查找操作" << endl;
    //返回拥有关键 key 的元素数,但key是唯一的,只会返回 0或1
    size_t cnt = number.count(8);
    cout << "cnt = " << cnt << endl;

    cout << endl;
    //查找key为10的键值对
    map<int, string>::iterator it = number.find(10);
    //指向键等于 key 的元素的迭代器。若找不到这种元素,则返回尾后(见 end() )迭代器。
    if(it == number.end())
    {
        cout << "该元素不存在map中" << endl;
    }
    else
    {
        cout << "该元素存在map中 " 
             << it->first << "  "
             << it->second << endl;
    }
}

map的插入

std::pair<iterator, bool> insert( const value_type& value );

map的插入是一个pair类型,不是一个key,所以需要构建pair类型



void test()
{
	cout << endl << "map的insert操作" << endl;
    pair<map<int, string>::iterator, bool>
        // 3种插入方式:1、pair类型数据 2、打括号插入 3、make_pair插入
        /* ret = number.insert(pair<int, string>(4, "chongqing")); */
        /* ret = number.insert({4, "chongqing"}); */
        ret = number.insert(make_pair(4, "chongqing"));
    
    if(ret.second) //pair第二个元素类型为bool,若成功插入则为true
    {
        cout << "插入成功 " 
             << ret.first->first << "   "
             << ret.first->second << endl;
    }
    else
    {
        cout << "插入失败, 证明该元素已经存在map中" << endl;
    }
    display(number);

    cout << endl << endl;
    map<int, string> number2 = {
        {7, "sichuan"},
        {5, "sichuan"},
    };
    //insert的范围插入
    number.insert(number2.begin(), number2.end());
    display(number);

    cout << endl << endl;
    //insert的大括号多个插入大括号嵌套大括号(一个个都是pair类型)
    number.insert({ {11, "hello"}, {2, "nanjing"}, {13, "hangzhou"} });
    display(number);

}

下标和修改操作

支持下标操作和修改操作。

void test()
{
    cout << endl << "map的下标操作" << endl;
    
    //map的下标中传的是key类型,然后返回的是value类型
    cout << "number[1] = " << number[1] << endl;//查找
    
    //如果下标操作访问的key不存在,就会变为插入操作,value为空
    cout << "number[12] = " << number[12] << endl;//插入
    display(number);

    cout << endl << endl;
    //T &operator[](key)
    number.operator[](12) = "kiki";
    /* number[12] = "kiki";//修改 等价于上面*/ 
    
    number[5] = "kiki";
    display(number);
    
    /* const map<int, string> number3 = {make_pair(1, "hehhe")}; */
    /* number3[1]; error 因为是const是不允许通过下标访问修改value*/

}

针对于自定义类型

如果自定义类型是string,是不需要重载小于符号的,因为其可以进行直接比较。

void test2()
{
    /* map<Point, string> number = {//error */
    //之所以报错是因为,map会自动对key进行排序,但Point没有实现对比较运算符的重载,所以会报错
    map<string, Point> number = {
        {"hello", Point(1, 2)},
        {"world", Point(2, 2)},
        make_pair("wuhan", Point(3, 2)),
        make_pair("wuhan", Point(3, 2)),
        {"nanjing", Point(1, 2)},
        {"dongjing", Point(1, 2)},
    };
    display(number);
}
image-20230414192505673

multimap 的使用

multimap 特征

image-20230414193944903

不支持下标(重点)

image-20230414194031626

无序关联式容器

无序关联容器提供能快速查找(均摊 O(1) ,最坏情况 O(n) 的复杂度)的无序(哈希)数据结构。

基本概念

底层使用的是哈希。

哈希函数:通过key值找到key值存放的位置。size_t index = H(key)

哈希函数的构建:目的就是为了设计哈希函数,尽量能减少冲突。

哈希冲突:H(key1) = H(key2),key值不一样,但是key值对应的位置值index是一样。

解决哈希冲突:开放地址法、在散列法、建立公共溢出区、链地址法(STL中使用就是这种方法)

image-20230414194900802

装载因子

装填因子。实际的数据个数/表的长度 = m。m越大的话,那么冲突的概率就比较高,但是空间的利用率也比较高。m越小的话,冲突的概率也会比较小,空间的利用率也会比较小。尽量将装载因子的大小设计在[0.5, 0.75]. 可以将数组看成是完美的哈希。

unordered_set的使用

image-20230414203049541

总结:其他的查找方法与set完全相同,insert插入的方法、以及erase方法与set完全相同。也没有下标访问运算符。

针对自定义类型

template<
    class Key,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<Key>
> class unordered_set;

自定义实现:

https://gitee.com/boldness2012/cpp47_code/blob/master/20230413/unordered_set.cc#

image-20230414214822719

针对于std::hash的写法:

image-20230414214926837
image-20230414214947041

针对于std::equal_to的写法:

image-20230414215042580
image-20230414215115833

unordered_multiset的使用

image-20230414224828513

注意:unordered_multiset的查找、插入、删除,可以参照set与multiset之间的区别。针对于自定义类型,完全与unordered_set等价。

unordered_map的使用

特征:

image-20230414221155382

下标访问(重点):

image-20230414221232041

unordered_multimap的使用

基本特征

image-20230414221616365

不支持下标:

image-20230414221727501

优先级队列

template<
    class T,
    class Container = std::vector<T>,
    class Compare = std::less<typename Container::value_type> //类型萃取
> class priority_queue;
image-20230414221841817

针对于自定义类型

针对于自定义类型的时候,需要根据第二个模板参数中容器的value_type进行std::less的改写。std::less有常规的三种形式,模板的特化、函数对象的形式、运算符重载的形式(在前面讲set的时候重点讲解过)。

容器的选择

如果说容器可以取下标?

vector、deque、map、unordered_map

总结:所有只要是带set的都没有下标,所有带multi的都与下标没有关系,list没有下标。

元素是否有序?

关联式容器是有顺序,无序关联式容器没有,对于序列式容器而言,默认情况下与顺序没有关系,但是可以借助于对应排序函数进行排序。

时间复杂度?

无序关联式容器:底层使用的是哈希,如果没有冲突的话,时间复杂度O(1).

关联式容器:底层使用的是红黑树,时间复杂度O(logN).

序列式容器:时间复杂度O(N)

迭代器的类型?

随机访问迭代器:vector、deque

双向迭代器:list、关联式容器。

前向迭代器:无序关联式容器。

没有迭代器:容器适配器stack、queue、priority_queue

迭代器

输入输出流之所以可以看成是容器,因为会对应的有缓冲区,而缓冲区是可以存数据的,就可以当容器看待。

迭代器的头文件在 #include

image-20230415110426657 image-20230415112537534

输出流迭代器

image-20230415110523741

理解的要点是将输入/输出流作为容器看待。因此,任何接受迭代器参数的算法都可以和流一起工作。使用流迭代器必须要包含头文件

先介绍一个算法库的copy函数

Defined in header <algorithm>

template< class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );

摘取的ostream_iterator源码部分实现细节

class ostream_iterator
{
public:
    //ostream_iterator<int>  osi(cout, "\n");
    //ostream_type& __s = cout;  _M_stream = &cout;
    //const _CharT* __c = "\n";_M_string = "\n"
    ostream_iterator(ostream_type& __s, const _CharT* __c) 
    : _M_stream(&__s) //取得是输出流地址
    , _M_string(__c)  
    {
        
    }
    
    //osi = 1  osi对象内实现对赋值运算符的重载
    //__Tp(int) = __value(1)
    ostream_iterator<_Tp>& operator=(const _Tp& __value) 
    { 
        *_M_stream << __value;//cout << 1
        if (_M_string) //_M_string如果不为空就输入到cout内
            *_M_stream << _M_string;//cout << "\n"
        return *this;
    }
    ostream_iterator<_Tp>& operator*() 
    { 
        return *this; 
    }
    ostream_iterator<_Tp>& operator++() 
    { 
        return *this; 
    } 
    ostream_iterator<_Tp>& operator++(int) 
    { 
        return *this; 
    } 
private:
    ostream_type* _M_stream;
    const _CharT* _M_string;  
};

摘取的copy源码部分实现细节

//copy()函数内实现细节:
template <class _InputIter, class _OutputIter>
inline _OutputIter copy(_InputIter __first, _InputIter __last, _OutputIter __result) 
{	
	return __copy_aux(__first, __last, __result, __VALUE_TYPE(__first));//走到__copy_aux()函数内实现
}

//__copy_aux()函数内实现细节:
template <class _InputIter, class _OutputIter, class _Tp>
inline _OutputIter __copy_aux(_InputIter __first, _InputIter __last,
                              _OutputIter __result, _Tp*) 
{
	typedef typename __type_traits<_Tp>::has_trivial_assignment_operator  _Trivial;
	return __copy_aux2(__first, __last, __result, _Trivial()); //走到__copy_aux2()函数内实现
}
//__copy_aux2()函数内实现细节:
template <class _InputIter, class _OutputIter>
inline _OutputIter __copy_aux2(_InputIter __first, _InputIter __last,
                               _OutputIter __result, __false_type) 
{
    return __copy(__first, __last, __result,//走到__copy()函数内实现
                __ITERATOR_CATEGORY(__first),
                __DISTANCE_TYPE(__first));
}

//__copy()函数内实现细节:
template <class _InputIter, class _OutputIter, class _Distance>
inline _OutputIter __copy(_InputIter __first, _InputIter __last,
                          _OutputIter __result,
                          input_iterator_tag, _Distance*)
{
    for ( ; __first != __last; ++__result, ++__first)
    *__result = *__first;
    return __result;
}

//__copy函数实现剖析:
//vector<int> vec = {1, 3, 5, 9, 7};
//copy(vec.begin(), vec.end(), osi)
__first = vec.begin();
__last = vec.end();
__result = osi
_OutputIter __copy(_InputIter __first, _InputIter __last,
                          _OutputIter __result,
                          input_iterator_tag, _Distance*)
{
    for ( ; __first != __last; ++__result, ++__first)
        *__result = *__first; //osi = 3 请看上面osi对赋值运算符的重载
    return __result;
}
              last
1, 3, 5, 9, 7
              f
                  
    //osi = 1
    //__value = 1;3
    //__Tp = int
    ostream_iterator<_Tp>& operator=(const _Tp& __value) 
    { 
        *_M_stream << __value;//cout << 3
        if (_M_string) 
            *_M_stream << _M_string;//cout << "\n"
        return *this;
    }   

输入流迭代器

image-20230415165502571

gdb的使用

list,简写为l,查看代码;breakpoint,简写为b,设置断点的;run,简写为r,让程序跑起来;step,简写为s,单步调试;next,简写为n,下一步;info b,查看断点;bt打印堆栈信息;print,简写为p,可以打印变量的值;ptype,可以打印变量类型。

摘取的ostream_iterator源码部分实现细节

class istream_iterator
{
public:
    istream_iterator() 
    : _M_stream(0)
    , _M_ok(false) {}
    //istream_iterator<int> isi(cin)
    //istream_type& __s = cin;
    //_M_stream = &cin;
    istream_iterator(istream_type& __s) 
    : _M_stream(&__s) 
    { 
        _M_read(); 
    }
    
     void _M_read() 
     {	 // &cin && cin 
         _M_ok = (_M_stream && *_M_stream) ? true : false;
         if (_M_ok) 
         {	 
             *_M_stream >> _M_value;//cin >> _M_value  2
             _M_ok = *_M_stream ? true : false;
         }
     }
     reference operator*() const 
     { 
         return _M_value; 
     }
    
     istream_iterator& operator++() 
     { 
         _M_read(); 
         return *this;
     }
    istream_iterator operator++(int)  {
    istream_iterator __tmp = *this;
    _M_read();
    return __tmp;
  }
    //isi
    //__x = istream_iterator<int>()
   bool _M_equal(const istream_iterator& __x) const
   { 
       //_M_ok = 0;
       return (_M_ok == __x._M_ok) && (!_M_ok || _M_stream == __x._M_stream); 
   }
private:
    istream_type* _M_stream;
    _Tp _M_value;
    bool _M_ok;
};

//__first = isi = __x;
//__last = istream_iterator<int>() = __y;
template <class _Tp, class _CharT, class _Traits, class _Dist>
inline bool 
operator!=(const istream_iterator<_Tp, _CharT, _Traits, _Dist>& __x,
           const istream_iterator<_Tp, _CharT, _Traits, _Dist>& __y) {
  return !__x._M_equal(__y);
}

摘取的copy源码部分实现细节

//__first = isi;
//__last = istream_iterator<int>();
//__result = back_insertter(vec);
_OutputIter __copy(_InputIter __first, _InputIter __last,
                          _OutputIter __result,
                          input_iterator_tag, _Distance*)
{
  for ( ; __first != __last; ++__result, ++__first)
    *__result = *__first;
  return __result;
}

*__result   = 1;
*__result   = 2;
    
template <class _Container>
inline back_insert_iterator<_Container> back_inserter(_Container& __x) {
  return back_insert_iterator<_Container>(__x);
}
Point back_inserter(_Container& __x)
{
    return Point(x);
}

摘取的back_insert源码部分实现细节

注意:

void test()
{
    istream_iterator<int> isi(cin);
    vector<int> vec;
    /* copy(isi, istream_iterator<int>(), vec.begin()); */
    //对于vector而言,如果没有预留空间的话,最好使用push_back插入元素
    copy(isi, istream_iterator<int>(), std::back_inserter(vec));
 
    //可以看成是遍历vector
    copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, "  "));
    cout << endl; //看打印是否成功
    
}
class back_insert_iterator 
{
public:
    explicit back_insert_iterator(_Container& __x) 
    : container(&__x) 
    {}
    
    back_insert_iterator<_Container>& 
    operator=(const typename _Container::value_type& __value) 
    { 
        container->push_back(__value); //插入元素
        return *this;
    }
    
    back_insert_iterator<_Container>& operator*() { return *this; }
    back_insert_iterator<_Container>& operator++() { return *this; }
    back_insert_iterator<_Container>& operator++(int) { return *this; }
protected:
  _Container* container;
}

迭代适配器

插入迭代器

void test()
{
    vector<int> vecNumber= {1, 3, 7, 9};
    list<int> listNumber = {11, 55, 88, 66};
    //back_inserter与back_insert_iterator底层执行了push_back函数
    /* 1、第一种方式,使用函数模板back_inserter
    copy(listNumber.begin(), listNumber.end(), back_inserter(vecNumber)); */
    
    // 2、第二种方式,使用类模板back_insert_iterator
    copy(listNumber.begin(), listNumber.end(), back_insert_iterator<vector<int>>(vecNumber));
    
    copy(vecNumber.begin(), vecNumber.end(), ostream_iterator<int>(cout, "  "));
    cout << endl;

    cout << endl << endl;
    //front_inserter与front_insert_iterator底层执行了push_front函数
    copy(vecNumber.begin(), vecNumber.end(), front_insert_iterator<list<int>>(listNumber));
    copy(listNumber.begin(), listNumber.end(), ostream_iterator<int>(cout, "  "));
    cout << endl;

    cout << endl << endl;
    //将vector中的数据插入到set中
    set<int> setNumber = {55, 33, 2, 8, 12, 19};
    auto it = setNumber.begin();
    //inserter与insert_iterator底层执行了insert函数
    copy(vecNumber.begin(), vecNumber.end(), insert_iterator<set<int>>(setNumber, it));
    copy(setNumber.begin(), setNumber.end(), ostream_iterator<int>(cout, "  "));
    cout << endl;
}

posted @ 2023-04-16 12:46  雪国北风  阅读(54)  评论(0编辑  收藏  举报