Learning C++ No.13【STL No.3】

简介: Learning C++ No.13【STL No.3】

引言:

北京时间:2023/3/7/15:33,还有27分钟就要去上课啦!刚刚把最近因为考试原因欠的课给还干净了,已经准备好今天晚上接受航哥的毒打了,毒打就毒打,咱不怕,只要不欠钱,咱就光明磊落;所以这篇博客今天肯定是发不了,不过明天肯定可以,哈哈!对,最重要的一点就是,我的新键盘到了,开心!不过没有按照预期码字到40w,不过也大差不差有37w了,上述的内容以及接下来的内容,我就将会使用我的新键盘,哈哈哈!所以今天我们的学习内容大致还是紧接着上篇有关C++的内容,继续深入学习vector,并且自我实现vector和解决vector中会引出的一些重要问题【如:迭代器失效】


103.png


上篇博客,我们已经大致的了解了STL库中的vector类,和vector类的基本使用原理,所以此时我们就带着vector类中的基本接口来自己实现一下vector类,在我们自己实现vector类之前,我们一起去看看C++大佬写的代码,在STL中看vector源码。


STL中的vector源码

104.png


大佬写的vector源码非常的长,图片不易展示,感兴趣的同学可以点击该链接vector源码

我们只是展示部分,通过这部分的代码,我们也是可以学到一些干货的,特别就是大佬爱使用重定义(typedef),并且从上述有的接口中可以看出,大佬写代码时,在从堆区上开辟空间时,并不是和我们平时一样使用的是new或者malloc接口,使用的是STL六大天王之一的 空间配置器(内存池) 的写法,并且可以得出,此时我们连空间配置器(内存池)具体是什么都还不清楚,更别谈使用了,所以我们离STL基础还是有距离的,起码空间配置器还在等着我们学习(博客必更);从源码中我们学到了很多大佬的写法,此时在自己实现vector类之前,我们先偷偷地练习一下大佬的写法,如下代码:


#include<iostream>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
using namespace std;
namespace wwx
{
  template<class T>
  class vector
  {
  public:
    typedef T value_type;
    typedef value_type* iterator;
    typedef const value_type* const_iterator;
    typedef size_t size_type;
    iterator begin()
    {
      return start;
    }
    iterator end()
    {
      return finish;
    }
    vector()
      :start(0), finish(0), end_of_storage(0)
    {};
    size_type size()const
    {
      return size_type(end() - begin());
    }
    size_type capacity()const
    {
      return size_type(end_of_storage - begin());
    }
  private:
    iterator start;
    iterator finish;
    iterator end_of_storage;
  };
}

浅浅的实现一下vector类,并且浅浅的模仿一下大佬的写法,typedef 各种数据类型,只要自己看得懂,管他那么多(就是豪横!)哈哈哈!这是大佬的姿态,不是我的哦!


自我实现vector类

简简单单,还是那句话,无论是看源码还是自己实现,第一步都是搞定成员变量,所以接下来我们就先来搞定一下vector类中的私有成员变量吧!如下图:


105.png

可以发现,vector类中的私有成员变量中有三个变量,_start、_finish、_end_of_storage,并且此时它们的类型都是 iterator(T*) ,所以我们可以知道,此时vector类中的三个变量,都是指针变量,和我们之前学习的顺序表数组是有非常大的区别的,以前的顺序表使用只有一个指针变量(int*arr),剩下的两个都是整形(size、capacity),并且vector的本质就是一个数组,为什么它的成员变量会这么的奇怪呢?其实本质上就是为了可以使vector在某些方面更加高效,可以更好的使用C++大佬发明的神器(迭代器)。


搞定了类的一把手(成员变量),此时就轮到类的二把手(构造函数),如下图:


106.png


当然,我们肯定是没有C++大佬那么牛,写那么多的构造函数,并且我也不知道为什么会有这么多的构造函数,毕竟我们连简单的条件语句都还没有玩明白(#if),所以上述就是我们简单实现vector类的一个构造函数,将三个成员变量初始化为空就行,剩下的工作,就交给编译器去调用默认构造就行,哈哈哈!开心!毕竟默认构造函数身为六大默认函数的老大,也不是浪得虚名的。


构造函数搞定,剩下的工作也就不多了,反正就还是那些东西(增删查改),我相信大多数同学在学习数据结构的时候,都已经学烂了,所以我们对增删查改的原理不多做讲解,我们具体对增删查改的一些细节方面进行学习,因为细节决定成败


首先是增

谈到增就涉及到插入数据,头插、尾插、任意位置插,这里我们就谈谈任意位置插入就行,但当谈任意位置插之前,我们要先搞定扩容的问题,reserve、resize

首先是resize函数,如下图:


107.png


不管是从上篇博客,还是之前的string类中,我们都可以了解到,resize和reserve的区别就是resize不仅可以扩容,而且还可以初始化,而且还可以删除数据,所以实现resize函数,就必须要把这些点都给考虑到。

搞定这些点,resize函数我们就差不多搞定了,此时我们就谈谈resize函数在vector类中最重要的一个点,就是resize函数的第二个模板参数,T val = T(),这个到底是个什么鬼,为什么要写成这个鬼样子?


首先谈目的:目的本质还是为了实现resize的初始化特性(当调用该函数时,没有给初始化值,就可以使用这个缺省参数来进行对resizes开辟的空间进行初始化)。

其次谈为什么是T val = T()而不是T val = 0,原因就是:因为此时的vector是一个泛型编程的类,没有具体类型,所以不可以用0来初始化,所以就要用一个模板参数,并且通过一个匿名对象的方式去调用默认构造函数,构造出相应的数据类型,给给val,最后完成相应的数据初始化工作

最后谈,出现的原因,目的就是为了让任意类型都可以调用默认构造,然后任意类型都可以初始化,不会有内置类型和自定义类型初始化的障碍

如下代码就是这种写法的一些例子:(注释非常详细)


  template<class T>
  void Function()
  { 
    T x = T();//这个就直接用一个int类型来辅助理解就行(int x = int();) 可以看出此时的int();就是我们之前学的匿名对象;其中int 空表示的就是匿名对象,而int()后面的括号其实表示的是,此时拿着这个int空,匿名对象去调用某一个函数,()括号中存放的本就应该是你要调用的这个函数需要的参数
    cout << x << endl;
  }
  void test_my_vector3()
  {
    int i = int();//这个的意思就是:使用int(),匿名对象,然后去调用构造函数,然后延长匿名对象的声明周期(把它给给新的对象)
    int j = int(1);//支持这种写法,本质上还是为了支持模板可以使用(也就是上述resize的缺省参数的写法;很好的解决模板初始化问题)
    int* p = int*();//指针类型不支持直接把匿名对象给给指针类型
    Function<int>();
    Function<int*>();
    Function<double>();
  }

总而言之:这种写法的发明就是为了可以更好的配合泛型编程中模板的使用,让模板变得更加的无懈可击!

搞定了resize函数,此时我们就来看一下reserve函数,如下图:

0.png


这个函数唯一要注意的点就是开辟完新空间之后,进行的一些列的赋值操作,这些赋值操作,一不小心就会出问题,所以要注意。

搞定了扩容问题,此时我们就正式进入,数据的插入,也就是增

头插和尾插我们直接复用任意位置插入,所以这边我们重点学习一下任意位置插入就行,如下图:


1.png


以上就是一个插入函数的正常代码理解,但是也有不正常理解,我们目前还没有搞定(也就是:迭代器失效问题),也就是上述为什么要记录len,为什么要更新pos,为什么insert函数有返回值等一系列奇奇怪怪的写法,在增这个位置我们先不做详细的学习,先了解就行,我们把这个问题集中起来放在博客的最后,一起解决。


其次就是删

2.png


删除数据明面上是非常的简单的,但是暗地里还涉及了迭代器失效的问题,此时想要解决这个问题和上述一样,还是通过使用返回值的方式来解决,这里不多做学习,下面一起搞定。


迭代器失效问题

这个问题是自我实现vector类中最为关键的一个问题,解决这个问题,可以为我们以后理解迭代器打下很好的基础,谁让迭代器是C++大佬发明的一个神器呢?不深入学习不行啊!


3.png

如上图,和我们之前在string类中学习的迭代器是一样的,在vector类中,我们依然可以把它给抽象理解成是两个指针,一个是头指针,一个是尾指针(指向最后一个数据的后一个位置(类似于string类中的\0的位置)),这就是大佬发明的迭代器,分分钟给它搞定(浅显)


认识了迭代器,正式进入迭代器失效问题(浅谈)

1.首先是迭代器失效的原因,可以发现,我们在上述的插入和删除中都碰到了迭代器失效的问题,所以原因如下:


插入是因为扩容(异地扩),所以导致pos自己本身的地址没有改变,但是pos指向的空间发生了改变(异地扩,原空间被delete),pos指针此时指向的空间并不是原来的那个迭代器区间,而是一个内存中的未知空间(经典的野指针问题)。

删除是因为位置关系会发生改变(本质上是因为地址不连续),所以也会导致迭代器失效问题


2.其次是迭代器失效的解决方法,从上述可知,是使用给返回值的方式解决这个问题,结合上述迭代器失效的原因,可以知道,给给返回值的原因

插入函数是因为,返回pos的位置,就是更新pos的位置,让pos指向的空间保持迭代器区间的地址

删除使用返回值的原因是,将被删除数据的后一个数据的地址返回给我,这样我依然可以保持迭代器区间地址的连续性,只要能保持连续性,问题就解决了


感兴趣的同学,可以去看看下面这个链接中的文章:详细迭代器内容可见该博客


并且此时还会涉及到双向迭代器、单向迭代器和随机迭代器(string类中的迭代器就是属于随机迭代器),基本使用原理:


一个双向迭代器,此时就不允许传单向迭代器,但是允许传随机迭代器

一个单向迭代器,此时就允许传双向迭代器,也允许传随机迭代器

反正随机就是什么迭代器都可以传,双向不允许传单向


所以上述的迭代器失效问题就是经典的迭代器失效问题中的一个,说明C++中还有非常多的坑在等着我们踩哦!(讨厌!!!)

4.png

上图参考下面这个链接:迭代器的本质和基本使用

简单的vector自我实现代码

namespace wwx2
{
  template<class T>
  class my_vector
  {
  public://可以看出上述是把T给直接定义成了一个value_type,然后使用value_type定义了const指针和普通指针(所以本质上:都只是T类型而已)
    typedef T* iterator;//有普通版本的迭代器,此时无论是因为模仿STL还是真的会用到,我们都应该要给一个const类型的迭代器
    typedef const T* const_iterator;
    typedef T& value_type;
    typedef size_t size_type;
    my_vector()
      :_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)//初始化列表初始化
    {}
    iterator begin()
    {
      return _start;
    }
    iterator end()
    {
      return _finish;
    }
    const_iterator begin()const
    {
      return _start;
    }
    const_iterator end()const
    {
      return _finish;
    }
    size_type capacity()const//像这种函数一定要记得加上我们的const
    {
      return _end_of_storage - _start;
    }
    size_type size()const
    {
      return _finish - _start;
    }
    void resize(size_type n, T val = T())//实现了reserve,此时resize复用就行,只是初始化的细节注意一下和size的大小注意一下就行 (所以此时我们就需要多给一个缺省参数,来进行默认的初始化新开辟的空间),就是防止你没有给第二个参数,只给了第一个参数,那此时我也可以把空间初始化
    {//上面那个缺省值的初始化是非常的神奇的(目的:因为此时的vector是一个泛型编程,是没有具体类型的,所以不可以用0来初始化,所以就要用一个模板参数,然后就是通过一个匿名对象的方式去调用默认构造函数)
      if (n < size())//这个不是缩容(这个属于删除数据,因为size是和数据紧紧挂钩的)
      {
        _finish = _start + n;//这个条件在尾插的时候,扩容,是不可能存在缩容删除数据的(唯一可能的就是我们直接调用这个函数,然后进行传参的时候,只有这种情况才有可能导致删除)
      }
      else
      {
        if (n > capacity()) //这个条件就是传说中的只有你比我小,我才扩容 
        {
          reserve(n);//开空间
        }
        while (_finish != _start + n)//加 初始化
        {
          *_finish = val;//此时已经有空间了,所以就不需要使用定位new的方法,直接赋值就行
          ++_finish;
        }
      }
    }
    value_type operator[](size_type pos)//并且此时返回的是一个引用,就是返回别名,目的:节省构造,防止临时变量的常属性
    {
      assert(pos < size());
      return _start[pos];//就是返回这个pos位置在_statr中的那个下标位置(因为此时是指针,所以准确的来说,应该是一个地址位置)
    }
    const value_type operator[](size_type pos)const//就是多重载一个const类型的,给给const对象使用(萝卜青菜给有所爱)
    {
      assert(pos < size());
      return _start[pos];
    }
    void reserve(size_type n)
    {
      //这边肯定是要涉及深浅拷贝问题的(并且为了防止缩容问题,这边还要进行判断)
      if (n > capacity())
      {
        //使用下面调换两个指针的顺序是可以解决的,但是不是很好,所以我们这边直接先把size()的值给接收起来就行
        size_type sz = size();//这样直接使用sz就行了,不需要再使用size(),也就是不需要再考虑finish和start的位置(随便它去变,跟我都没关系)
        iterator tmp = new T[n];//此时这里不需要考虑加1的问题(只有像string这种,需要存一个\0的这种,我们才需要多开一个空间给给它使用)
        if (_start != nullptr)//此时的new是不需要想malloc一样就行开辟成功和失败的判断的,这个只是为了单纯判断一下_start中是否有数据
        {
          memcpy(tmp, _start, sizeof(T) * size());
          delete[]_start;//这个就是经典的防止空间太大太小问题,直接开空间,然后拷贝,然后直接把原空间给删除
        }
        //_start = tmp;//注意:因为此时是重新开空间,释放旧空间,所以此时的tmp就是我们的start
        //注意:此时下述的size()是不会因为finish和start的地址改变而改变的 (不会因为重复调用而改变),一直都是同一个值,也就导致可以直接使用tmp+size()
        //_finish = tmp + size(); //如果此时是先把tmp给给_start,然后再加加size(),此时就会导致finish还是0,而start已经是tmp,然后又因为size就是finish-start,就会导致size是一个负值,也就是-start,然后再用start+size,那么刚好就是0,最后赋值给finish,此时finish就还是0,所以就会导致后面push_back的时候,对空指针解引用的问题,所以此时为了解决这个问题,此时就不敢直接先给给start
        _start = tmp;//先给给finish,再给给start就行,很好的解决finish为空的问题
        _finish = tmp + sz;//提前记录size的好处
        _end_of_storage = tmp + n;//此时的这个是通过tmp指针的首地址,然后加上16个字节,就是首地址向后走走16个字节,得到的就是此时的容量
      }
    }
    void push_back(const T& x)
    {//因为使用的是模板类,所以这里的参数类型都是直接给一个T参数类型就可以了
      //从刚刚的STL源码中,我们可以发现的是,它的push_back使用的是内存池开空间的形式(就是定义new加定义构造)
      //我们这里使用不了,我们就直接使用正常的形式就行(如果想要使用的话,就要在定义模板参数的位置给一个内存池的参数)
      if (_finish == _end_of_storage)
      {
        reserve(capacity() == 0 ? 4 : capacity() * 2);
      }
      *_finish = x;//这步就是尾插的经典步骤,上述的判断只是为了防止空间不足而已(但是由于我们的_end是一个原生指针,所以这里想要直接在尾部赋值,就需要对这个尾部指针进行解引用)
      ++_finish;
    }
    void pop_back()
    {
      assert(!empty());//切记assert给的是真
      --_finish;//这种如果直接用,不检查的话,就会导致删多了的话有越界问题(并且因为此时是迭代器的写法,地址问题),就会导致循环找地址,然后就导致无限打印地址,直到地址匹配到
    }
    iterator insert(iterator pos, const T& val)
    {
      assert(pos >= _start);
      assert(pos <= _finish);//还是那个道理,assert中的条件是真条件
      //并且由于这边,我们使用了_finish这个位置,就会导致,如果容量刚好等于_finish的时候,越界,所以这个位置也要进行一个容量的检查
      if (_finish == _end_of_storage)
      {
        size_type len = pos - _start;//先记录(len的作用就是解决迭代器失效问题,目的:更新pos指针)
        reserve(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;//扩容后再更新(解决迭代器失效问题)
      }
      iterator end = _finish - 1;
      while (end >= pos)//此时因为pos的类型是地址,所以不存在-1变成无符号(size_t),所以不存在死循环,没有问题
      {
        *(end + 1) = *end;//此时的意思就是把最后一个数据赋值给0,然后把刚刚那个位置腾出来,然后循环
        --end;
      }
      *pos = val;
      ++_finish;
      return pos;
    }
    void erase(iterator pos)
    {
      assert(pos >= _start);
      assert(pos < _finish);
      iterator start = pos + 1;
      while (start != _finish)
      {
        *(start - 1) = *start;
        ++start;
      }
      --_finish;
    }
    bool empty()
    {
      return _start == _finish;//当_finish--到_start的时候,就是空,就是不允许的
    }
  private:
    iterator _start;//因为STL源码中是直接使用三个原生指针,所以这边我们模仿它,我们也直接使用三个原生指针就行
    iterator _finish;
    iterator _end_of_storage;
  };
}

测试代码

  void test_my_vector1()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    for (size_t i = 0; i < v.size(); ++i)
    {
      cout << v[i] << " ";
    }
    cout << endl;
    my_vector<int>::iterator it = v.begin();//所以本质,我发现,使用三指针的写法,好像就是为了可以更好的使用迭代器(因为迭代器的本质就是地址的加加减减,前提:地址要连续)
    while (it != v.end())//不连续就会导致迭代器失效问题
    {
      cout << *it << " ";
      ++it;
    }
    cout << endl;
    for (auto e : v)//有了迭代器,就有迭代器的小儿子范围for
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector2()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.pop_back();
    v.pop_back();
    my_vector<int>::iterator it = v.begin();
    my_vector<int>::const_iterator cit = v.begin();//反正就是萝卜青菜给有所爱,数据类型是什么就调用什么,不怕没有,想要什么就有什么
    while (it != v.end())
    {
      cout << *it << " ";
      ++it;
    }
    cout << endl;
  }
  template<class T>
  void Function()
  { 
    T x = T();//这个就直接用一个int类型来辅助理解就行(int x = int();) 可以看出此时的int();就是我们之前学的匿名对象;其中int 空表示的就是匿名对象,而int()后面的括号其实表示的是,此时拿着这个int空,匿名对象去调用某一个函数,()括号中存放的本就应该是你要调用的这个函数需要的参数
    cout << x << endl;//总而言之:目的就是为了让任意类型都可以调用默认构造,然后任意类型都可以初始化
  }
  void test_my_vector3()
  {
    //int i = int();//这个的意思就是:使用int(),匿名对象,然后去调用构造函数,然后延长匿名对象的声明周期(把它给给新的对象)
    //int j = int(1);//支持这种写法,本质上还是为了支持模板可以使用(也就是上述resize的缺省参数的写法;很好的解决模板初始化问题)
    //int* p = int*();//指针类型不支持直接把匿名对象给给指针类型
    Function<int>();
    Function<int*>();
    Function<double>();
  }
  void test_my_vector4()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    cout << v.size() << endl;
    cout << v.capacity() << endl;
    v.resize(10);
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector5()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);//切记push_back也是会调用扩容函数的
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);//此时就很神奇的会涉及到,insert是否扩容问题,如果数据是在push_back的过程,insert就不涉及扩容,如果,insert涉及扩容,那么此时就有迭代器失效的问题,本质就是地址不连续
    //v.insert(3, 30);//聪明的我,居然写成了这样,真的是函数都调不明白啊(但是也刚刚好,可以引出我们的find函数)
    //当然此时也不一定一定要使用find,因为此时可以直接使用迭代器(充分表明我的不聪明)
    v.insert(v.begin(), 0);
    v.insert(v.end(), 6);
    v.insert(v.begin() + 3, 30);//充分表明迭代器是真的好用(促进我们通过变量看地址的好习惯)
    auto pos = find(v.begin(), v.end(), 3);//此时就是把我的迭代器区间传给了find函数,这样就可以使用find函数在迭代器区间中寻找3这个元素
    if (pos != v.end())//这里这样写的目的:主要是为了防止越界
    {
      pos = v.insert(pos, 30);//像这种一直插一直插这种,就会导致一个问题(经典的迭代器失效问题),本质就是:地址不连续了
    }//并且此时如果上述这样写,那么pos在内存中的位置是不会改变的,但是由于insert因为内存不足开空间,导致start和finish的位置发生了改变,间接导致begin和end(迭代器)发生了改变,导致迭代器失效
    (*pos)++;//意思就是:拿更新后的pos地址中的元素加加一下(但是本质上是想要在,3的地方加加,最后会变成在30的地方加加),原因就是pos指向的那个地址是不变(但是地址中的数据在挪动),并且此时因为insert中的pos是个形参,所以不会影响我们外部的pos,所以此时最好使用返回值,直接把pos返回给我们
    //并且此时由于使用引用返回需要有临时变量(常属性),所以不推荐使用,所以这边,我们是通过返回值的方式解决这个问题的
    //总:pos失效后,我们都不推荐使用(*pos)++;所以不敢这样写
    for (auto e : v)//迭代器一失效(也就是begin和end的地址发生了改变),那么此时就会导致pos地址虽然不改变(但是它指向的空间发生了改变(从原来的begin和end的地址变成了一块未知的地址(因为begin和end已经因为扩容拥有了新地址,并且把原来的地址给释放了))),所以pos指向的地址就是一个未知地址,此时pos指针就是一个野指针
    {//总:开空间之后,导致pos指向的迭代器地址被释放,导致野指针问题(这也就是第一种经典的迭代器失效问题)
      cout << e << " ";//所以明白了原理之后,此时的解决方法就是:更新一下pos指针指向的空间(依据pos和start的相对位置不改变来更新)
    } 
    cout << endl;
  }
  void test_my_vector6()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4); 
    v.push_back(5);
    auto pos = find(v.begin(), v.end(), 2);
    if (pos != v.end())
    {
      v.erase(pos);
    }
    (*pos)++;//此时这个不是解引用的意思,可以算是对*这个运算符重载的意思(本质上*的运算符重载就是在获取一下我想要的功能或者是数据)
    for (auto e : v)//并且此时这种删除之后再访问的方式是很不好的,因为如果按照erase代码来说,删除最后一个及时把finish的位置往前挪动一个,此时如果再去访问刚刚4的pos位置,就会导致对一个空地址解引用,此时就出问题了
    {               //所以总的来说:删除数据后,是不允许进行数据访问的(并且此时数据删除也是伴随着迭代器失效的问题的),因为位置关系发生了改变,也就是导致地址不连续(迭代器失效的本质)
      cout << e << " ";
    }
    cout << endl;
  }
int main()
{
  wwx::vector<int>::iterator it;
  wwx2::test_my_vector1();//测试尾插
  wwx2::test_my_vector2();//测试尾删
    wwx2::test_my_vector3();//测试匿名对象
  wwx2::test_my_vector4();//测试resize
  wwx2::test_my_vector5();//测试insert和erase(本质:扩容导致迭代器失效问题)
  wwx2::test_my_vector6();
  return 0;
}

北京时间:2023/3/9/7:58

image.png


总结:欠的钱总算又还了一点了,咱们明天见,主要是因为有早8

相关文章
|
1月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
101 10
|
1月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
52 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
1月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
71 5
|
1月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
58 1
|
1月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化2
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
62 6
|
1月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化1
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
60 7
|
1月前
|
算法 安全 Linux
【C++STL简介】——我与C++的不解之缘(八)
【C++STL简介】——我与C++的不解之缘(八)
|
1月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
54 5
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
57 2
|
1月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
25 0