vector模拟实现


  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页 吃透C语言专栏 数据结构 Gitee仓库 C++ Linux 算法

文章目录

  • [一,vector 类模板](#一,vector 类模板)
  • 二,迭代器
  • [三,vector 成员变量](#三,vector 成员变量)
  • [四,capacity() 和 size()](#四,capacity() 和 size())
  • [五,begin() 和 end()](#五,begin() 和 end())
  • [六,vector 构造函数](#六,vector 构造函数)
  • [七,initializer_list 类模板](#七,initializer_list 类模板)
  • 八,析构函数
  • [九,reserve( )分配空间](#九,reserve( )分配空间)
  • [十,swap 交换](#十,swap 交换)
  • [十一,operator 运算符重载函数](#十一,operator[ ]运算符重载函数)
  • [十二,operator= 运算符重载函数](#十二,operator= 运算符重载函数)
  • 十三,push_back尾插
  • 十四,pop_back尾删
  • [十五,insert 迭代器版本,任意位置插入 val](#十五,insert 迭代器版本,任意位置插入 val)
  • [十六,erase 迭代器版本,任意位置删除](#十六,erase 迭代器版本,任意位置删除)

一,vector 类模板

c 复制代码
namespace bit:
{
  template<typename T>
  class vector
  {
    public:
    
    private:
    
  }
}
  • 如果展开std::命名空间的话,vector类模板会和库里面的vector冲突,所以封在bit命名空间中,使用的时候指定类域。

  • 类模板的定义(具体代码),必须在编译器需要实例化它的那个翻译单元(文件)中可见。如果不可见,且没有提前显式实例化,就会导致链接错误

  • ⬆️⬆️⬆️解析:如果将类模板中的成员函数声明和定义分离在不同的文件中,在mian( )主函数中类模板实例化类实例化对象调用成员函数的时候,就会出现链接错误,因为函数调用时模板会通过函数参数推演模板参数,实例化出类和成员函数,但是函数的声明和定义分离,main()函数所在文件中是通过.h文件中的函数声明去寻找函数定义,而类模板中的函数声明仅仅是一个声明,通过模板参数推演出了实参类型但是因为函数定义同一个文件中,所以无法实例化出对应的成员函数,而在.c文件中的成员函数需要知道模板参数推演出的类型才能实例化出对应的成员函数,但是因为函数调用是通过.h文件中函数声明找.c文件中的函数定义,所以只有.h文件中的声明能够推演类型,而.c文件中定义是无法推演类型的,而.h文件中因为只有声明没有定义所以推演整个函数,也找不到函数体,而.c文件中的定义又因为拿不到推演所以无法实例化。所以最终就导致函数没有定义的链接错误

  • stack.cpp中的类模板的成员函数定义必须声明模板参数吧,声明模板参数的话,为什么也看不到需要int版本,是因为调用成员函数的时候是通过声明去找定义,所以只会在声明出类模板实例化类吧,但是定义处不会实例化类

  • 总结:只有在函数声明处才能进行推演,定义处无法推演,就是因为.h 和 .c 是不同文件,mian()所在的文件包的是.h文件,只能通过声明去找定义,所以只有声明处能进行推演,而定义处无法得到函数实参进行推演,但是声明出推演不出整个函数体,因为没有定义,无法推演自然无法去找这个定义,所以链接错误,最后类模板中成员函数的声明和定义要在同一个文件中,vector库里面就是这样的,如果成员函数太长就在.h中声明定义分离,定义在类外,如果短的话就直接定义在类中内联了

二,迭代器

c 复制代码
typedef T* iterator;
typedef const T* const_iterator
  • vector容器支持迭代器,可以通过vector的迭代器让容器和外部产生联系,外层都封装成iterator,但底层实现只要满足可以解引用以及++等功能和效果即可。vector底层iterator迭代器的实现就是指针,所以iterator 底层是 T*,const_iterator 底层是 const T*,而 const_iterator 底层是 const T*

三,vector 成员变量

c 复制代码
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
  • STL源码中,vector底层是由三个指针控制的,1:首元素的地址(存储空间的起始地址)_start 2:最后一个有效元素下一个位置的地址 _finish 3:存储空间(内存块)中结束位置的下一个位置的地址 _end_of_storage

四,capacity() 和 size()

c 复制代码
size_t capacity()const
{
	return _end_of_storage - _start;
}

size_t size()const
{
	return _finish - _start;
}
  • vector中是由三个指针控制,所以capacity( ) 是由 _end_of_storage - _start 得到存储空间的大小,size( ) 是由 _finish - _start 得到有效元素的个数

五,begin() 和 end()

c 复制代码
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin()const
		{
			return _start;
		}

		const_iterator end()const
		{
			return _finish;
		}
  • begin( ) 和 end( ) 迭代器作用就是返回起始位置的迭代器和最后一个有效元素下一个位置的迭代器,所以begin( )函数体中直接返回_start 即可,end( )函数体中直接返回 _finish 即可。而 const版本 就是限制了this指针,无法通过 this指针 修改指向空间的数据,确保了取出的迭代器只读,返回的迭代器 const_iterator 访问容器中的内容也是只读的

六,vector 构造函数

迭代器构造

c 复制代码
template<typename InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back( *first );
		++first;
	}
}
  • STL库中vector构造函数支持迭代器构造,在类实例化对象的时候调用带参构造,通过迭代器区间作为构造函数的参数进行初始化对象。而vector本质是一个类模板,可以接收任意类型的数据,所以迭代器构造也就需要可以接收任意类型的迭代器,所以迭代器构造写成函数模板的形式,支持任意类型的迭代器,模板自动推演迭代器类型然后实例化出对应类型的函数,函数体中就是通过迭代器遍历容器插入到对象中。

构造n 个元素,并初始化成val

c 复制代码
vector(size_t n,const T& val = T())
{
    reverse(n);
     
    for(size_t i = 0;i < n;i++)
    {
         push_back(val);
    }
}

vector(int n,const T& val = T())
{
    reverse(n);
     
    for(int i = 0;i < n;i++)
    {
         push_back(val);
    }
}
  • 这个版本的构造函数作用是通过参数n 在对象中初始化n 个 val值,第二个参数是缺省参数,如果不传则使用缺省值。函数体中的实现就是for循环把n个val插入到对象中。

  • 缺省参数const T& val = T()中,因为vector可以接收任意类型的数据,所以写成类模板,而构造函数也需要接收任意类型的数据并初始化对象,所以写成const T& val。而作为缺省参数需要给缺省值,如果缺省参数是自定义类型,自定义类型初始化通过构造函数初始化,所以缺省值写成 T( ),匿名对象实例化调用构造函数初始化自己,缺省值拷贝构造给自定义类型达到初始化自定义类型的目的。但是任意类型的参数包括了内置类型,内置类型初始化直接赋值,内置类型没有构造函数的概念,所以缺省值需要写成 T。这样就导致了作为模板参数的缺省参数需要支持任意类型,模板参数是通过推演支持任意类型的,而缺省参数如果是自定义类型缺省值需要同类对象,作为模板参数的缺省参数和缺省值需要支持任意自定义作为缺省值,所以只能写成匿名对象实例化调用构造函数初始化匿名对象再通过拷贝构造初始化自定义类型,但是内置类型模板参数缺省值不需要这么麻烦,直接给用模板参数赋值就好了。所以为了强行兼容模板参数可以是任意类型,C++给内置类型进行了升级,C++中内置类型也有构造函数,内置类型也可以调用构造函数,所以T()中T可以是内置类型,也就是内置类型调用构造函数初始化。C++中内置类型有构造函数,能调用构造函数初始化,就是为了强行兼容模板参数可以是任意类型而已,最后初始化就是本身

  • 重载int版本的原因:编译器的选择问题,也叫做编译器对于函数模板和函数同时存在的时候编译器的匹配原则,编译器最优先匹配参数符合的,其次才是匹配可以勉强兼容的,也就是可以形参和实参不匹配但是可以通过隐式类型转换兼容形参,如果模板函数和函数的形参都是一致完美匹配,则优先选择函数,因为模板函数毕竟只是模板,还需要推演和实例化。

  • 迭代器版本的函数模板原本是想兼容任意类型的迭代器,所以可以兼容任意类型,而兼容的迭代器肯定是兼容同一个容器中的迭代器,所以两个形参中是同一个模板参数。但调用带参构造实参是(int,int)如(12,13)的话,函数模板中形参是同一种类型,而函数中第一个形参是size_t,函数调用时两个实参都是int类型会优先匹配类型匹配的,而函数中第一个参数形参是size_t类型,需要实参隐式类型转换,而函数模板中模板参数都是同一类型同时不需要隐式类型转换,所以函数调用优先匹配函数模板,但是函数模板是给任意类型迭代器设计的,函数体中是对迭代器的操作,如果接收的是int的话,函数中把int当成迭代器操作程序直接崩溃,所以为了不让(int,int)作为函数实参的时候匹配到迭代器版本,我们的解决办法是让(int int)实参不匹配到模板参数,1:(10u,12),函数调用的时候第一个实参后缀加u,编译器会识别成size_t类型,这样实参类型就是(size_t,int)类型,就会优先匹配形参类型适配的函数而不是函数模板 2 :重载一个int版本的n个val构造,形参n改成int类型,模板形参和函数形参同时适配实参的前提下,会优先匹配函数,避免了匹配错误

拷贝构造

c 复制代码
vector(const vector<T>& v)
{
 reserve(v.capacity())
 for(auto e : v)
 {
   push_back(e);
 }
}
  • 拷贝构造作用时构造同类对象,所以参数接收同类对象以后reserve一块和同类对象一样大的空间,使用范围for把参数中的所有元素push_back尾插到对象中完成拷贝构造

七,initializer_list 类模板

  • std::initializer_list(注意拼写,是 initializer_list)是 C++11 引入的一个非常核心且轻量级的类模板。它使得 C++ 拥有了统一初始化的能力,也就是用大括号 { } 来初始化对象

  • 核心作用:让自定义的类(或类模板)能够像内置数组一样,接收一个由同类型元素组成的大括号列表
  • initializer _list本质上是一个类模板,initializer _list<int> il1 = {1,2,3,4}是通过类模板实例化出类再通过类实例化对象。只有当编译器发现"目标需要 initializer_list"或者"使用 auto 推导"时,它才会把花括号里的东西打包成 initializer_list<T>,例如vector<int> v1 = {1}或者 vector<int> v2 = {1,2}对于类的构造函数中有对应的单参和多参构造,这时候就会自动发生隐式类型转换 = 号本质上就是拷贝构造的调用语法,调用拷贝构造的参数需要的是同类对象,所以把内置类型作为类中对应的构造函数的参数构造成具有常性的临时对象,再将构造出来的同类临时对象作为拷贝构造的参数进行拷贝构造,本质上就是隐式类型转换,内置类型调用构造转换成同类对象了。但是隐式类型转换的前提是类中有对应参数的构造函数,而像vector<int> v1 = {1,2,3,4,5}中,花括号中可以是任意数量,类中是不可能重载这么多对应参数的构造函数的,所以这时候,{1,2,3,4,5,6}常量数组没有对应的构造函数进行构造成具有常性的临时对象 然后进行拷贝构造完成隐式类型转换的过程,这时候就会把常量数组中的内容推导成initializer _list类型,而只有类中有initializer _list为参数的构造函数,才会把initializer _list类型的对象进行隐式类型转换,通过initializer _list类型为参数的构造函数进行构造,构造成具有常性的同类临时对象,也就是隐式类型转换为同类临时对象,再调用拷贝函数拷贝同类临时对象完成类的初始化

  • 所以vector<int> v1 = { }C++11统一了用{ }可以进行任意初始化,本质上就是增加了initializer _list类模板统一将那些类中没有对应构造函数,不能隐式类型转换成同类对象再进行拷贝构造初始化的多参数情况,统一将他们合并为initializer _list类型,编译器发现类中没有对应的构造函数可以进行构造完成隐式类型转换的时候,就将常量数组推导成initializer _list类型,调用类中以initializer _list类型为参数的构造函数对initializer _list类型对象进行构造,完成隐式转换,通过initializer _list类型的构造函数构造成同类临时对象,然后再将具有常性的同类临时对象作为拷贝构造的参数进行拷贝构造,完成对象的初始化,所以本质上还是隐式类型转换成具有常性的临时对象,再进行拷贝构造,最后连续的拷贝构造和构造优化成直接构造,如果类中没有对应的initializer _list类型为参数的构造函数,那就常量数组就无法进行隐式类型转换成同类对象,因为隐式类型转换成同类对象就是通过有对应参数的构造函数进行构造,构造出来的同类临时对象

  • ⬆️⬆️⬆️上面的逻辑基本都是对的,只不过有一点需要纠正,因为C++标准委员会想一同天下,所有无论什么参数的常量数组,都会被优先匹配initializer _list类型的构造函数,在不支持initializer _list类型的构造函数的时候才会去调用正常参数的构造函数

C++ 编译器处理 {} 初始化的两个阶段

当编译器看到 MyClass obj{1, 2}; 时,它找构造函数分为两个阶段(Phase 1 和 Phase 2):

阶段 核心策略 具体流程与规则
阶段一 (Phase 1) 无脑优先找 initializer_list 1. 编译器首先会去 MyClass 里找,看有没有接收 std::initializer_list<T> 的构造函数。 2. 只要找到了,并且花括号里的所有参数,都能隐式转换(或者完全匹配)为 T 类型,编译器就会立刻、无条件地选中这个构造函数。 3. 注意: 在这个阶段,编译器根本就不会去管其他的普通构造函数,哪怕普通构造函数的参数完全匹配!
阶段二 (Phase 2) 退而求其次,找普通构造函数 1. 只有在阶段一失败 的情况下(比如没写 initializer_list 构造函数,或者花括号里的参数根本不可能转成 list 的元素类型,比如你要传个 std::string,但 list 是 double),编译器才会进入阶段二。 2. 在阶段二,编译器才会把所有的普通构造函数拉出来,按照你熟悉的"重载决议"规则,去挑一个最匹配的。

initializer _list类模板的内部实现

  • 内部本质定义了两个指针_First 和 _Last ,分别指向常量数组的开始和结束,还有一个size来记录元素个数,指针是最存粹的迭代器,,甚至可以说:迭代器在诞生之初,就是用来"泛化指针",迭代器(Iterator)这个概念并不是 C++ 发明的,而是源自设计模式中的"迭代器模式"。但在 C++ 的 STL(标准模板库)中,迭代器的设计目标就是"像指针一样工作"

  • initializer_list支持迭代器,迭代器底层本质上也是T*,其实本质上就是指向首尾两个元素的指针

  • 所以一个容器支持{}构造,本质上容器就是支持了一个initialzer_list类型对象作为参数的构造

initializer_list 类型构造函数

c 复制代码
vector(std::initializer_list<T> il)
{
  reserve(il.size())
  for(auto e : il)
  {
    push_back(e);
  }
}
  • 类支持initializer_list 类型构造函数,这样用花括号括起来的,也就是常量数组被识别为initializer_list类型之后,因为类中支持initializer_list类型的构造函数,所以initializer_list类型的对象会隐式类型转换,调用initializer_list类型的构造函数进行构造,构造成具有常性的临时对象,再进行拷贝构造拷贝到对象中,连续的构造+拷贝构造最后被优化成直接构造

  • 补充:vector() = default 强制生成默认构造
c 复制代码
struct A {
    A(int x) : a(x) {}
    // 隐式默认构造已被抑制
    int a;
};
A a1;            // 错误:没有默认构造函数

struct B {
    B(int x) : a(x) {}
    B() = default;   // 显式要回默认构造
    int a;
};
B b1;            // OK,a 未初始化
  • 当我们显示定义了构造函数,编译器就不会生成自己的默认构造了,但是我们显示定义的带参构造或者拷贝构造,没有显示定义无参构造,当我们类实例化对象的时候,必须传参调用带参构造,因为我们没有实现无参构造而且编译器因为我们显示定义了构造函数无法生成编译器自己的默认构造,就会导致我们呢无法调用无参构造,这时候我们加上 类名()= default 就可以强制编译器生成默认构造

八,析构函数

c 复制代码
~vector()
{
	if (_start)
	{
		delete[]_start;
		_start = _finish = _end_of_storage = nullptr;
	}
}
  • 先判断对象是否有空间,是否需要释放,再释放空间,把三个成员变量指针都置为nullptr

九,reserve( )分配空间

正确写法

c 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t oldsize = size();

		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * oldsize);
			delete[]_start;
		}

		_start = tmp;
		_finish = _start + oldsize;
		_end_of_storage = _start + n;
	}
}

  • reserve( )的作用的扩容,开辟空间,所以只有 n > capacity( ) 的时候 才需要扩容,new T[n]大小的空间给tmp指针存入。同时提前保持size( ) 存入 size_t oldsize。因为size()的原理是同一块空间中的_finish - _start,而扩容之后_start 是新空间的_start 但是 而_finish 是旧空间的_finish,如果更新_finish 用 _start + size() 的话,一个size()计算的size 是用旧空间的_finish - 新空间的_start,计算出来的size就是错的,错误的size() + _start 更新的_finish就是错误的。解决方法1:让size( )计算出正确的size,所以就需要用同一块空间的_finish - _start ,所以我们要在_start = tmp 更新_start 之前,先更新_finish ,我们需要_finish = tmp + size() 再进行 _start = tmp,这时候size() 调用的时候在 _start = tmp 更新之前,size( )计算出来的size就是旧空间的_finish - 旧空间的_start ,计算出来的是正确的size( ),_finish = tmp + size( ),新空间地址+正确的size,就能计算出正确的_finish,但是这种写法依然顺序,不稳定,容易出问题。我们可以采用解决方法2:上一个方案的目的就是通过size( )计算出正确的size,我们最终就是为了拿到正确的size,所以我们可以提前保存一份size,就是size_t oldsize = size(),有了oldsize以后,就不需要通过size()计算出size,_start 和 _finish 的更新顺序不会影响size,size通过提前保存出来的,不再是通过size()计算出来的了,避免的更新顺序的影响。_end_of_storage = _start + n 用新_start + n 就可以求出来

  • reserve 的作用是扩容和分配空间,如果对象中原本没有空间,那我们需要做的就是开辟一块新空间T* tmp = new T[n],size_t oldsize = size(),然后更新_start 和 _finish 和 _end_of_storage指向新空间,但是如果对象原本有空间,我们需要做的不仅仅是分配空间的工作,分配空间之后,还有拷贝旧空间的数据到新空间,释放旧空间,完成异地扩容。

十,swap 交换

c 复制代码
void swap(vector<T>& v)
{
std::swap(_start,v._start);
std::swap(_finish,v._finish);
std::swap(_end_of_storage,v._end_of_storage)
}
  • 交换函数是和同类对象交换数据,对象的空间不变,函数体中调用std::库里面的swap对两个对象的三个内置类型的成员变量指针交换即可

十一,operator 运算符重载函数

c 复制代码
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
			assert(pos < size());
			return _start[pos];
		}
  • operator [ ] 函数,是对[ ]运算符进行重载的运算符重载函数,功能是取出自定义类型中[ ]索引位置的元素,函数体中assert断言pos < size( ),确保pos索引位置下标的合法性,保证了访问位置的合法性,不会越界访问。直接return _start[pos]通过下标引用操作符和pos完成对指针位置偏移,最后解引用返回对应_start偏移pos位置的数据

  • const版本是保证_start不被修改拿到对象中正确的数据,返回值const 让拿出来的数据变成只读了的

十二,operator= 运算符重载函数

c 复制代码
	vector<T>& operator=(vector<T> v)
	{
		swap(v);
		return *this;
	}
  • 赋值运算符重载函数作用是拷贝一个同类对象的数据,形参是实参的临时拷贝,形参v 通过实参拷贝赋值对象得到,函数体中swap(v),将拷贝对象v和被拷贝对象的内部数据进行交换,也就是三个内置类型的成员变量指针进行交换,完成了赋值,最后return *this,v会随着函数调用结束,函数栈帧的销毁而销毁

十三,push_back尾插

c 复制代码
void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}

	*_finish = val;
	++_finish;
}
  • 尾插是插入单个元素,所以通过 if (_finish == _end_of_storage)代表size == capacity 空间满了,需要扩容,之后在_finish位置,也就是最后一个有效元素的下一个位置,就是尾插的位置,进行插入val,最后更新_finish ,_finish ++

十四,pop_back尾删

c 复制代码
void pop_back()
{
	assert(size()>0);
	--_finish;
}
  • 尾插的前提是空间中存在有效数据,所以 assert ( size() > 0 ),--_finish 对有效元素结束位置的修改旧完成了尾删

十五,insert 迭代器版本,任意位置插入 val

c 复制代码
	iterator insert(iterator pos, const T& val)
	{
		assert(pos >= _start);
		assert(pos <= _finish);
		if (_finish == _end_of_storage)
		{
			size_t len = pos - _start;

			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);

			pos = _start + len;
		}

		//上面len是处理迭代器失效,但是形参是实参的临时拷贝,所以实际上迭代器pos依然指向旧空间
		//所以学习erase的用法,返回更新后的pos,不要使用原来失效的迭代器pos

		iterator end = _finish - 1;
		while (pos >= end)
		{
			*(end + 1) = *end;
			--end;
		}

		*pos = val;
		++_finish;
		
		return pos;
	}
  • insert 的参数是传入迭代器 pos 和 需要插入的元素val ,插入位置需要断言 >= _start 和 <= _finish,只要是增加操作,就需要扩容,if (_finish == _end_of_storage),而insert 一旦触发扩容,就会导致迭代器失效,因为pos迭代器是指向的是旧空间的pos位置,扩容之后旧空间被释放,数据拷贝到新空间中,pos迭代器也就失效了,所以进行扩容之后需要更新失效的迭代器,如果使用失效的迭代器pos,无法进行正确的插入,len 记录 pos 到 _start 的举例,扩容之后,用 _start + len 更新迭代器 pos 指针,插入操作需要挪动数据,永远都是最后一个有效元素开始挪动,最后把 pos 位置挪开就行,利用迭代器解引用拿到数据挪动,最后在更新的 pos 迭代器位置插入 val ,最后更新 _finish ,++_finish

  • 形参是实参的临时拷贝,迭代器pos在insert函数中进行了更新,但是形参是实参的临时拷贝,迭代器pos在main函数的函数栈帧中没有改变,pos仅仅是在insert函数中更新,在insert函数中是有效迭代器,但是不会影响insert函数外的迭代器,形参的改变不会影响实参,所以我们最后的解决办法就是return pos返回更新的迭代器(原本STL 中 vector 中的 insert 就是不返回 pos 的,就是要提醒我们,扩容会导致迭代器失效)

c 复制代码
iterator begin()
{
	return _start;
}

  • 为什么不传引用iterator& pos,begin( )中取出迭代器是传值返回,传值返回本质是拷贝了一个具有常性的临时对象,是只读的,所以 iterator& pos 是权限的放大,如果 const iterator& pos 的话,迭代器在 insert 中直接没法用了

  • 所以遇到会失效的迭代器就不去使用它是最安全的


十六,erase 迭代器版本,任意位置删除

c 复制代码
		void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			while ((pos + 1) !=  _finish)
			{
				*pos = *(pos + 1);
				++pos;
			}
			//最后pos停下来的位置是挪完最后一个有效数据

			--_finish;
		}
  • erase删除,就是通过迭代器把需要删除位置的后面数据往前挪动覆盖掉,最后更新_finish,--_finish

  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页 吃透C语言专栏 数据结构 Gitee仓库 C++ Linux 算法

相关推荐
蜗牛一步步往上爬3 小时前
第十六章 C++语言在深度学习中的应用
开发语言·c++·深度学习
wo_jack6 小时前
C++ 入门笔记(十六):第16课 vector 模拟实现十问十答 —— 从三指针到迭代器失效
c++·后端·面试·stl·vector
D e re k ZH7 小时前
C++ 标准库算法全景指南
数据结构·c++·算法
雪落漂泊8 小时前
C++算法 二分查找篇(上)
开发语言·c++·算法
不会算法的笨小孩9 小时前
异常的使用以及会出现的问题
开发语言·c++·c++11·异常
Hhy_110711 小时前
《C++深度解构06》手撕 string —— 从底层内存管理到深浅拷贝彻底吃透
c语言·开发语言·c++·学习·visual studio
青少儿编程课堂12 小时前
蓝桥杯青少组图形化省考报名倒计时·全国青少年信息素养大赛·电子学会图形化考级:校园黑白棋人机对战解析
c++·python·算法·bfs·信息学竞赛
YSL07012412 小时前
C++基础
开发语言·c++
eggwyw13 小时前
关于C++为什么不加入垃圾回收机制解析
开发语言·c++