【STL】STL最常用容器之——动态数组容器vector(超详细)
本文详细介绍了C++标准库中的vector容器的基本特性、常用接口实现和底层原理剖析。文章首先讲解了vector的构造方法、容量操作、访问遍历和增删改查等核心接口,随后深入剖析了其底层实现机制,包括迭代器失效(⭐)、深拷贝(⭐)等关键问题,并提供了完整的模拟实现代码。通过具体示例演示了vector的使用方法和注意事项,帮助读者全面理解这一重要容器的实现原理和使用技巧。
目录
3.2、测试接口(用于打印数据)——print_Container
一、vector简介
std::vector:是 C++ 标准库中的一个容器,也是STL最常用容器之一,定义在<vector>头文件中,它本质是一个动态数组,具有数组随机访问的特性,同时又能在运行时动态调整大小,自动管理内存,在 C++ 开发中应用广泛。
基本特性:
- 动态调整大小:无需在创建时指定固定长度,可根据需要添加或删除元素,在元素数量超过当前容量时自动扩容。
- 连续内存存储:
vector中的元素在内存中是连续存储的,这使得它支持像普通数组一样通过下标快速访问元素。- 自动内存管理:内部实现了对内存的自动分配和释放,无需手动调用
new和delete,减少了内存泄漏的风险。
std::vector的文档(方便大家随时查阅):(⭐)
https://legacy.cplusplus.com/reference/vector/vector/?kw=vector
二、常用接口说明
1、vector类对象的常用构造
实现vector对象在定义的同时初始化
| 构造函数(construct)(⭐) | 功能 |
| vector() | 默认构造 |
| vector(size_t n, const value_type& val = value_type()) | 构造一个对象并初始化为为n个val,如果不传第二个参数,初始化为value_type()的默认值 |
| vector(const value_type& v) | 拷贝构造,用v对象来初始化 |
| vector(InputIterator first,InputIterator end) | 使用迭代器区间来初始化 |
#include<iostream>
using namespace std;
#include<vector> //头文件
void test1()
{
vector<int> v1;
vector<int> v2(5,1); // 使用5个1来初始化int类型的对象v2
vector<int> v3(v1); // 使用v2对象来初始化v3对象
vector<int> v4(v1.begin(), v1.end()); // 用迭代器区间初始化v4对象
}
2、vector对象容量操作
| 函数接口 | 功能 |
| ⭐ size() | 获取有效数据个数 |
| ⭐ capacity() | 获取空间大小 |
| empty() | 判断是否为空 |
⭐resize (size_type n, value_type val = value_type())
|
扩容并完成初始化 |
⭐reserve (size_type n) |
申请n个空间 |
void test2()
{
vector<int> v1(5, 1);
cout << v1.size() << endl; // 获取v1对象的有效数据个数
cout << v1.capacity() << endl; // 获取v1对象的空间大小
}
可以通过下面一段代码来感受reserve扩容的过程:
void test3()
{
vector<int> v2;
v2.push_back(0);
size_t sz = v2.capacity();
cout << "capacity:" << sz << endl;
int i = 0;
while (i < 100)
{
v2.push_back(i);
if (sz != v2.capacity()) // 当原来空间大小变化即扩容时,打印新的空间大小
{
sz = v2.capacity();
cout << "reserve:" << sz << endl;
}
i++;
}
}

// resize
void test4()
{
vector<int> v3(10, 1);
v3.resize(15, 2);
v3.resize(10, 0); // resize()不会让v3缩容,但是会改变有效数据个数
cout << v3.capacity() << endl;
}
注意:std::vector 的 reserve(n) 函数只是预留内存空间,并不会真正构造元素,也不会改变 vector 的大小(size())。 此时 vector 中没有任何有效元素,size() 仍为 0。而代码中使用 v[i]去访问和修改下标为 0 到 4 的元素,相当于访问了 “不存在的内存区域”,属于越界访问。
// 错误示范
void test6()
{
vector<int> v;
v.reserve(5);
for (int i = 0; i < 5; i++)
{
v[i] = i + 1;
}
}
3、vector类对象的访问及遍历操作
| 函数 | 功能 |
| ⭐operator[ ] | 重载运算符[],通过访问数组元素的方式返回pos位置的元素 |
| ⭐v.begin() | begin获取一个字符的迭代器(地址),用于从前往后遍历数组。 |
| ⭐v.end() | end获取最后一个元素下一个位置的迭代器(地址) |
| v.rbegin() | rbegin()返回的是反向迭代器,指向数组的最后一个元素,用于从后往前遍历数组。 |
| v.rend() | end获取第一个元素前一个位置的迭代器(地址) |
| ⭐范围for | C++11支持更简洁的范围for的新遍历方式,与auto关键字联用 |

vector对象的遍历与string对象的遍历方式相同,前面string类中已经做了详细的说明,这里简单地
说明:
void test5()
{
vector<int> v1(5, 1);
//遍历(v1.size()+[])
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
//迭代器
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
4、vector 增删查改
| 函数 | 功能 |
| ⭐push_back() | 尾插 |
| pop_back() | 尾删 |
| ⭐find() | 查找 |
⭐insert (iterator position, const value_type& val) |
指定位置position之前插入数据val |
⭐erase (iterator position) |
删除pos位置的数据 |
| swap() | 交换两个vector的数据空间 |
三、vector容器深度剖析及模拟实现
说明:对于vector容器的实现,我们模拟标准库<vector>中的实现方式,即在头文件(vector.h)中完成vector容器常用接口的实现。同时,为了我们实现的vector容器能够适用于不同的数据类型,我们采用类模板的形式。此外,为了避免命名的冲突,我们将所有的实现过程都放在一个我们自己的命名空间中。
我们想要模拟实现vector容器,那就要理解vector容器的底层结构。前面已经提到,其本质就是动态数组,在vector类中就有三个指向这片动态内存空间的指针(即vector类的三个成员变量)。

namespace hds
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//...
private: // 成员变量(设定缺省值)
iterator _start = nullptr; // 指向数组开头的指针
iterator _finish = nullptr; // 指向数组最后一个元素后一个位置的指针
iterator _end_of_storage = nullptr; // 指向数组空间最后一个位置的指针
};
}
3.1、类对象的初始化
1、默认构造
// 默认构造函数
/*vector()
{}*/
// C++11强制生成默认构造
vector() = default;
2、拷贝构造(⭐)
vector(const vector<T>& v)
{
reserve(v.size());
for (auto e : v)
{
push_back(e);
}
}
这里v是const对象,所以size()函数必须是const成员函数,否则就会报错。
3、迭代器区间构造(⭐)
用迭代器区间初始化,同时用InputItrerator代替iterator,可以让函数模板不仅仅只能用来初始化vector对象。
// 类模板的成员函数还可以继续是函数模板
template<class InputIterator>
vector(InputIterator first, InputIterator end)
{
while (first != end)
{
push_back(*first);
++first;
}
}
测试:
void test05()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
print_vector(v);
vector<int> v2(v.begin(), v.begin() + 3);
print_container(v2);
// list容器
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
print_container(lt);
// 使用list容器初始化vector容器
vector<int> v3(lt.begin(), lt.end());
print_container(v3);
}
4、用n个val初始化
// 用n个val来初始化
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
测试:
void test05()
{
vector<string> v4(5, "abcd"); // 使用5个字符串"acbd"来初始化string类型的vector对象v4
vector<int> v5(10); // 使用10个int类型的缺省值来初始化int类型的vector对象v5
print_container(v4);
print_container(v5);
vector<int> v6(10u, 1);
print_container(v6);
}
使用10个1初始化,由于编译器默认都是int类型的参数,会优先调用函数模板来初始化,存在对整数的解引用(报错)。
解决方案:1、重载一个函数:vector(int n, const T& val = T());让编译器优先选择调用该函数
2、在实参后面加上 u,声明实参是size_t(unsigned int)类型的
5、赋值重载——operator=(⭐)
// 传统写法
vector<T>& operator=(const vector<T>& v)
{
if (_start != v._start)
{
clear();
reserve(v.size());
for (auto e : v)
{
push_back(e);
}
}
return *this;
}
// 现代写法
void swap(const vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(const vector<T> v)
{
swap(v);
return *this;
}
4、析构函数
_start指向动态数组,用delete[] 释放动态申请的空间。
// 析构函数
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
3.2、测试接口(用于打印数据)——print_Container
我们为了打印函数可以实现不同类型vector对象的打印,将其实现为函数模板。
规定,没有实例化的类模板里面取东西,编译器不能区分这里const_iterator是类型还是静态成员变量。用typename声明const_iterator是类型。
template<class T>
void print_vector(const vector<T>& v)
{
typename vector<T>::const_iterator it = v.begin();
// auto it = v.begin(); // 也可以直接用auto
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
范围for+auto
template<class T>
void print_vector(const vector<T>& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
采用Container,可以让打印函数模板适用于更多的容器,不仅仅只是vector。(推荐写法)
template<class container>
void print_Container(const container& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
3.3、功能函数接口
1、迭代器(⭐)
对于不同的类型的指针,typedef重命名为不同的迭代器。
typedef T* iterator;
typedef const T* const_iterator;
当我们了解了vector容器的底层结构就是动态数组后,我们就可以快速地实现指向容器的一众迭代器了。
// 返回指向容器中第一个元素的迭代器
iterator begin()
{
return _start;
}
// 返回指向容器中最后一个元素下一个位置的迭代器
itereator end()
{
return _finish;
}
// 返回指向容器中第一个元素的迭代器(const对象)
const_iterator begin()const
{
return _start;
}
// 返回指向容器中最后一个元素下一个位置的迭代器(const对象)
const_iterator end()const
{
return _finish;
}
2、获取有效数据个数——_size

由于两点指针相减就是这两个指针之间的元素个数,求_size(有效元素个数),即让_finish减去_start就是有效元素的个数。
// const成员函数
size_t size()const
{
return _finish - _start;
}
3、获取数组空间大小——_capacity
size_t capacity()const
{
return _end_of_storage - _start;
}
4、判空——empty
根据_start和_finish指针是否指向同一个位置来判断。
bool empty()
{
return _start == _finish;
}
5、访问操作符重载——operator[ ](⭐)

T& operator[](size_t pos)
{
assert(pos < size());
return *(_start + pos);
}
const对象
const T& operator[](size_t pos)const
{
assert(pos < size());
return *(_start + pos);
}
6、清理空间——clear
清理空间即将_start指向空间中的数据清除,但是并不释放空间,所以只需要让_finish指针指向_start指向的位置。
void clear()
{
_finish = _start;
}
7、清除迭代器指向位置的数据——erase(⭐)

void erase(iterator pos)
{
assert(pos < _finish);
assert(pos >= _start);
while (pos < _finish - 1)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
}

8、申请空间——reserve(⭐)

当我们需要扩容时,我们可以通过new的方式来另外开辟一块空间tmp,然后将_start指针指向空间中的数据拷贝到tmp空间,并释放_start指向的空间,最后让_start指针指向tmp空间,就实现了扩容。但是,如果我们用memcpy函数来拷贝数据时,只能实现对内置类型的浅拷贝,对于自定义类型(如string类对象的_str指向了资源),就需要深拷贝。
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
iterator tmp = new T[n];
// memcpy(tmp, _start, old_size * sizeof(T)); // 只能浅拷贝
// 实现深拷贝
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + old_size;//使用old_size,而size()函数由于_start=tmp更新了,此时_finish-_start已经不再表示有效数据个数(重点)
_end_of_storage = _start + n;
}
}
下面我们来感受一下拷贝的过程
void test06()
{
vector<string> v0;
// 插入四个字符串(不需要扩容)
v0.push_back("abcdefg");
v0.push_back("abcdefg");
v0.push_back("abcdefg");
v0.push_back("abcdefg");
print_Container(v0);
// 继续插入一个字符串(扩容)
v0.push_back("abcdefg");
print_Container(v0);
}

可以看到,当我们用vector创建一个string类类型的对象时,_start指向的空间中内置类型成员_str还指向了资源,所以,这时候我们想要扩容就要实现深拷贝。
9、扩容——resize(⭐)

当空间不够时还可以使用resize来扩容,resize在扩容的同时还会实现初始化。但是,当我们想要缩小空间时,resize只能减少有效数据个数,并不会减小capacity。同时,第二个参数我们使用缺省值。
void resize(size_t n, const T& val = T())
{
if (n <= size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}

10、尾插——push_back(⭐)
想要插入数据,就要先判断空间是否足够,不够就需要扩容。
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t sz = capacity() == 0 ? 4 : 2 * capacity();
reserve(sz);
_end_of_storage = _start + sz;
}
*_finish = x;
_finish++;
}
11、尾删——pop_back
void pop_back()
{
assert(!empty());
--finish;
}
12、指定位置插入——insert(⭐)
// 错误示范:
void insert(iterator pos, const T& x)
{
assert(pos < _finish && pos >= _start);
// 扩容
if (_finish == _end_of_storage)
{
size_t sz = capacity() == 0 ? 4 : capacity() * 2;
reserve(sz);
}
iterator end = _finish;
// 将pos位置及之后的数据整体向后移动
while (end > pos) {
*end = *(end - 1);
end--;
}
*pos = x; // 插入数据
_finish++;
}
我们最容易用这样的代码去插入数据,下面我们来测试一下:
void test02() {
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
print_vector(v);
auto p = find(v.begin(), v.end(), 3);
p = v.insert(p, 30); // 在3的之前插入30
print_vector(v);
*(p + 1) *= 10; // 修改迭代器指向位置处的值
print_vector(v);
}
输出结果:

我们发现,此时insert函数并没有实现在3之前插入30的效果,这就是因为迭代器失效而引起的。
那为什么会迭代器失效呢?
因为,当我们在插入了1,2,3和4这四个数据后,此时_start指向的数组空间被有效数据占满了,即size()=capacity()。当我们再想在3之前插入30时,就需要扩容,扩容会将原来_start指向的空间释放,而迭代器pos指向原来空间中的3,这就导致了迭代器失效。这时候我们就要注意:pos就像野指针一样指向随机的位置,我们不能再访问pos指向位置的值。所以,我们想要访问pos,或者在pos位置插入数据,就必须更新迭代器。

更新迭代器,我们可以通过记下pos迭代器相对于_start的位置,然后在扩容之后让pos迭代器指向新空间中对应的位置。同时,如果我们想要继续使用pos迭代器,我们可以让insert函数返回pos迭代器。

iterator insert(iterator pos, const T& x)
{
assert(pos < _finish && pos >= _start);
if (_finish == _end_of_storage)
{
size_t p = pos - _start; // 记下迭代器对于_start的相对距离
size_t sz = capacity() == 0 ? 4 : capacity() * 2;
reserve(sz);
pos = _start + p; // 更新迭代器
}
iterator end = _finish;
// 移动数据
while (end > pos) {
*end = *(end - 1);
end--;
}
*pos = x; // 插入
_finish++;
return pos; // 返回迭代器
}
再次测试:

四、完整源码
头文件:vector.h
#include<iostream>
using namespace std;
#include<assert.h>
#include<vector>
#include<string>
#include<list>
namespace hds
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
// 构造函数
//vector() // 默认构造
//{ }
// C++11 强制生成默认构造
vector() = default;
// this = v
vector(const vector<T>& v)
{
reserve(v.size());
for (auto e : v)
{
push_back(e);
}
}
// 类模板的成员函数还可以继续是函数模板
template<class InputIterator>
/* 用迭代器区间初始化,同时用InputItrerator代替iterator,可以让函数模板不仅仅只能用来初始化vector对象*/
vector(InputIterator first, InputIterator end)
{
while (first != end)
{
push_back(*first);
++first;
}
}
// 用n个val来初始化
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
/*vector(int n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}*/
void clear()
{
_finish = _start;
}
// 传统写法
/*vector<T>& opereator = (const vector<T>& v)
{
if (this != &v)
{
clear();
reserve(v.size());
for (auto e : v)
{
push_back(e);
}
}
return *this;
}*/
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
// 现代写法
vector<T>& operator=(vector<T> v) // 这里用传值的方式,(函数调用结束,栈帧销毁)防止在交换的时候改变v的值
{
swap(v);
return *this;
}
// 析构函数
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _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_t size()const
{
return _finish - _start;
}
size_t capacity()const
{
return _end_of_storage-_start;
}
T& operator[](size_t pos)
{
assert(pos < size());
return *(_start + pos);
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return *(_start + pos);
}
void resize(size_t n, T val = vector())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish<_start+n)
{
*_finish = val;
_finish++;
}
}
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
iterator tmp = new T[n];
// memcpy(tmp, _start, old_size * sizeof(T));
/* memcpy只能实现对内置类型的浅拷贝,对于自定义类型(如string类对象的_str指向了资源),就需要深拷贝*/
// 实现深拷贝
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + old_size;//使用old_size,而size()函数由于_start=tmp更新了,此时_finish-_start已经不再表示有效数据个数(重点)
_end_of_storage = _start + n;
}
}
void push_back(const T& a)
{
if (_finish == _end_of_storage)
{
size_t sz = capacity() == 0 ? 4 : capacity() * 2;
reserve(sz);
_end_of_storage = _start + sz;
}
*_finish = a;
++_finish;
}
void earse(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
while (pos < _finish-1)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
}
bool empty()
{
return _start == _finish;
}
void pop_back()
{
assert(!empty());
--_finish;
}
//插入,同时将pos作为返回值返回,防止迭代器失效
iterator insert(iterator pos, const T& a)
{
if (_finish == _end_of_storage)
{
size_t n = pos - _start; //用来更新迭代器
size_t sz = capacity() == 0 ? 4 : 2 * capacity();
reserve(sz);
pos = _start + n; // 更新迭代器
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end+1) = *end;
end--;
}
*pos = a;
_finish++;
return pos;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
// 打印(实现为模板函数)
template<class T>
void print_vector(const vector<T>& v)
{
// 规定,没有实例化的类模板里面取东西,编译器不能区分这里const_iterator是类型还是静态成员变量
// 用typename声明const_iterator是类型
typename vector<T>::const_iterator it = v.begin();
// auto it = v.begin(); // 也可以直接用auto
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 方法二:(重点)
/*for (auto e : v)
{
cout << e << " ";
}
cout << endl;*/
}
// 采用Container,可以让打印函数模板适用于更多的容器,不仅仅只是vector
template<class Container>
void print_container(const Container& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
}
测试代码:test.cpp
namespace hds
{
void test01()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
print_vector(v);
v.insert(v.begin() + 1, 0);
print_vector(v);
}
void test02()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
print_vector(v);
int x;
cin >> x;
auto p = find(v.begin(), v.end(), 2);
p = v.insert(p, 30);
print_vector(v);
// insert以后p就是失效,不要直接访问,要访问就要更新这个失效的迭代器的值,可以用insert函数返回pos并用p接收,更新迭代器p的值
*(p+1) *= 10;
print_vector(v);
}
void test03()
{
vector<int> v;
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.push_back(1);
print_vector(v);
v.resize(10, 2);
print_vector(v);
v.resize(2, 3);
print_vector(v);
}
void test04()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
//v.push_back(5);
print_vector(v);
auto it = v.begin();
while (it!=v.end())
{
if (*it % 2 == 0)
{
v.earse(it);
}
else
{
it++;
}
}
print_vector(v);
}
void test05()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
v.push_back(5);
print_vector(v);
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v = v1;
print_container(v);
print_container(v1);
vector<int> v2(v.begin(), v.begin() + 3);
print_container(v2);
// list容器
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
print_container(lt);
// 使用list容器初始化vector容器
vector<int> v3(lt.begin(), lt.end());
print_container(v3);
vector<string> v4(5, "abcd"); // 使用5个字符串"acbd"来初始化string类型的vector对象v4
vector<int> v5(10); // 使用10个int类型的缺省值来初始化int类型的vector对象v5
print_container(v4);
print_container(v5);
vector<int> v6(10u, 1); // 使用10个1初始化,由于编译器默认都是int类型的参数,会优先调用函数模板来初始化,存在对整数的解引用(报错)
// 解决方案:1、重载一个函数:vector(int n, const T& val = T());让编译器优先选择调用该函数
// 2、在实参后面加上 u,声明实参是size_t(unsigned int)类型的
print_container(v6);
}
void test06()
{
vector<string> v0;
// 插入四个字符串(不需要扩容)
v0.push_back("abcdefg");
v0.push_back("abcdefg");
v0.push_back("abcdefg");
v0.push_back("abcdefg");
print_container(v0);
// 继续插入一个字符串(扩容)
v0.push_back("abcdefg");
print_container(v0);
}
}
int main()
{
hds::test06();
return 0;
}
本期的分享就到此结束,大家快去上手练习吧!!!如果觉得还不错,请大家点个赞支持一下吧(笔芯笔芯)。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)