[C++] vector入门迭代器失效问题详解

Kevin的技术博客.png

文章目录

  • vector介绍
    • **vector iterator 的使用**
  • `vector`迭代器失效问题
    • 由扩容或改变数据引起的迭代器失效
      • `reserve`的实现(野指针)
      • `insert`实现(迭代器位置意义改变)
        • `insert`修改后失效的迭代器
      • `it`迭代器失效
    • `erase`后的问题
    • 总结:`std::vector` 中的迭代器失效和避免方法
      • **插入操作**
        • **解决方法**
      • **删除操作及解决方法**
    • 一定要注意迭代器的更新!!!
  • 其他问题
    • 依赖名称
      • 模板与依赖名称
      • typename关键字
      • 具体示例分析
    • 类外定义成员函数
    • 类内定义函数模板
      • 函数模板的应用
      • 使用的注意事项
  • **使用memcpy拷贝问题**
    • 问题引出
    • 调试分析
    • 解决措施
  • 理解使用 `vector` 构造动态二维数组
    • 什么是二维数组?
    • 使用 `std::vector` 构造动态二维数组
      • 构造方法
      • 解析
        • 定义二维数组
        • 初始化二维数组
        • 打印二维数组
      • 动态调整大小

vector介绍

使用模版指针作为迭代器的方式使用vector

typedef T* iterator;
typedef const T* const_iterator;

成员变量:

iterator _start = nullptr; // 容器的头
iterator _finish = nullptr; // 容器内最后一个数据
iterator _end_of_storage = nullptr; // 容器的最大容量处
  • _start:通常表示容器的开始位置,即指向容器中第一个元素的指针或迭代器。在某些实现中,这可能不是实际存储数据的地址,而是一个指向存储开始的指针。
  • _finish:通常表示容器中最后一个有效元素的下一个位置。这意味着_finish指向的位置是容器中最后一个元素之后的位置,但它本身并不指向一个有效的元素。在C++的std::vector中,finish可能用来表示容器的结束,但实际使用时应该使用end()成员函数(end()_finish指向相同)。
  • _end_of_storage:表示容器分配的内存的末尾。这通常比_finish要远,因为它包括了容器当前使用的所有元素以及可能预留的额外空间,以便于将来的元素扩展,而不需要重新分配内存。

image.png

iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}

vector iterator 的使用

iterator的使用接口说明
begin + end (重点)获取第一个数据位置的iterator/const_iterator,获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator

image.png
image.png

vector迭代器失效问题

迭代器失效主要是由于 vector 在执行某些操作时会重新分配内存或改变数据的位置,导致原有的迭代器指向的内存地址不再有效。以下是一些常见的会导致迭代器失效的操作:

由扩容或改变数据引起的迭代器失效

reserve的实现(野指针)

例如在模拟实现vector中的reserve时:

void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();T* tmp = new T[n];memcpy(tmp, _start, size() * sizeof(T));delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

可能出现迭代器失效具体代码为:

_start = tmp;
_finish = tmp + old_size;
_end_of_storage = tmp + n;

内部扩容的时候直接申请一块新的tmp空间,此时如果改为以下:

_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n;

由于size()接口实现如下:

size_t size()
{return _finish - _start;
}

当调用sizeof()接口时,此时里面的_finish还是曾经未使用memcpy(tmp, _start, size() * sizeof(T));时原来的_finish指向的位置,所以此时使用_finish = _start + size();来计算_finish时就会出现迭代器失效的问题。

insert实现(迭代器位置意义改变)

模拟实现insert()时,pos会出现失效问题:

由于数据挪动,已经不是指向2,所以insert以后我们认为迭代器失效,不要访问

iterator insert(iterator pos, const T& x)
{// 扩容if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;return pos;
}

在扩容部分,通过reserve后,此时的pos指向依然是未交换空间前空间中指向的位置。所以在以上代码中使用size_t len = pos - _start来保存交换空间前pos位置距离_start的距离len,在交换后再通过pos = _start + len;将失效的迭代器重新指向正确。

**推荐:每次使用完进行更新(用返回值接受) | ****insert**使用会返回插入后新的数据的位置

图示:
交换前
image.png

交换后
image.png

insert修改后失效的迭代器
int main() {std::vector<int> v = {1, 2, 3, 4, 5};int x;std::cin >> x;auto p = std::find(v.begin(), v.end(), x);if (p != v.end()) {// insert以后p就是失效,不要直接访问,要访问就要更新这个失效的迭代器的值//v.insert(p, 40);//(*p) *= 10;// 插入新元素并更新迭代器p = v.insert(p, 40);// 修改插入位置之后的元素(*(p + 1)) *= 10;}print_vector(v);return 0;
}

v.insert(p, 40);后,p指向的依旧是原来空间的p,所以最好使用p = v.insert(p, 40);,在每一次使用可能修改或者转移新空间的成员函数时都对迭代器进行更新,这样就会避免了迭代器的失效。

it迭代器失效

有以下程序:

vector<int> v{1,2,3,4,5,6}; // 
auto it = v.begin(); v.assign(100, 8); // 改变容器内容,如果内容数量大于原本数量,会扩容,交换空间,迭代器失效while(it != v.end())
{cout<< *it << " " ;++it;
}
cout<<endl;// 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容
// v.resize(100, 8);
// reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变
// v.reserve(100);
// 插入元素期间,可能会引起扩容,而导致原空间被释放
// v.insert(v.begin(), 0);
// v.push_back(8);
// 给vector重新赋值,可能会引起底层容量改变
// v.assign(100, 8)

出错原因:

  • 以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
  • **解决方式:**在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可

erase后的问题

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

例如删除pos位置的数据:
image.png
当执行代码逻辑删除,pos后的所有元素向前覆盖,删除后的pos指向依然是之前的位置,只是后面的数据覆盖在了之前pos上数据的位置上:
image.png

注意:

正是因为删除后的pos位置指向的是覆盖后的数据,所以在使用erase的时候需要注意注意迭代问题,也就是说在erase过后注意当前pos指向的位置再决定是否迭代pos。如果直接迭代可能造成数据检查的遗失。

示例:

// 删除所有的偶数
auto it = v.begin();
while (it != v.end())
{if (*it % 2 == 0){it = v.erase(it);}++it;}

上示代码就是滥用迭代器造成迭代器失效的例子,在每一次使用erase后都会进行迭代,如此就会将覆盖在pos位置上的未迭代的数据给跳过,导致了数据的遍历遗失,迭代器失效。

// 删除所有的偶数
auto it = v.begin();
while (it != v.end())
{if (*it % 2 == 0){it = v.erase(it);}else{++it;}
}

通过以上修改即可解决问题。

总结:std::vector 中的迭代器失效和避免方法

插入操作

  • 当向std::vector中插入元素时,如果插入操作导致重新分配内存(即容量不够,需要扩展),所有的迭代器都会失效。
  • 如果插入操作没有导致重新分配内存,则插入点之后的所有迭代器都会失效
解决方法

在插入元素后,更新所有受影响的迭代器

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // it 指向 vec[2]
vec.insert(it, 10); // 插入后 it 失效,需要重新获取 it
it = vec.begin() + 2; // 更新 it

删除操作及解决方法

当从std::vector中删除元素时,被删除元素之后的所有迭代器都会失效。

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // it 指向 vec[2]
vec.erase(it); // 删除后 it 失效,需要重新获取 it
it = vec.begin() + 2; // 更新 it

在插入元素后,更新所有受影响的迭代器。

一定要注意迭代器的更新!!!

其他问题

依赖名称

模板与依赖名称

在类模板中,某些名称的解析依赖于模板参数。例如,在vector<T>中,T是一个模板参数,而vector<T>::const_iterator则是依赖于T的名称。这种名称被称为“依赖名称”。

typename关键字

在模板中,编译器在解析依赖名称时可能会产生歧义,特别是在编译器不知道某个依赖名称是类型还是变量的情况下。例如,在vector<T>::const_iterator这个名称中,如果T是一个模板参数,编译器需要知道const_iterator是一个类型而不是一个静态成员变量。

为了解决这种歧义,C++引入了**typename**关键字,用来显式地告诉编译器某个依赖名称是一个类型。

具体示例分析

假设我们有一个模板类,它使用了std::vector。在这个类中,我们需要声明一个const_iterator类型的变量:

template <typename T>
class MyClass {
public:void myFunction() {std::vector<T> v;typename std::vector<T>::const_iterator it = v.begin(); // 使用typename关键字// ... 其他代码 ...}
};

在上面的代码中,如果我们没有使用typename关键字:

std::vector<T>::const_iterator it = v.begin(); // 消除编译器的歧义

编译器会报错,因为在模板的上下文中,编译器无法确定std::vector<T>::const_iterator是一个类型还是一个静态成员变量。为了消除这种歧义,我们需要在类型前面加上typename关键字:

typename std::vector<T>::const_iterator it = v.begin();

这样,编译器就能够正确地解析const_iterator为一个类型。

类外定义成员函数

长的成员函数可以在类外定义,需要重新声明模板参数。
image.png

类内定义函数模板

在C++中,类模板允许我们定义一个通用的类,而这个类可以操作任意类型的数据。此外,类模板的成员函数也可以是模板函数。这使得我们可以编写更加灵活和通用的代码。

// 类模板的成员函数,还可以继续是函数模版
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}

函数模板的应用

很多时候会使用一种容器来初始化另一种容器,以此来弥补该种容器在性能上的问题,例如,将list数据用来初始化vector

//template <typename Container> void print_container(const Container& container) 
//是一个函数模板,用于打印任何容器的内容。std::list<int> myList = {10, 20, 30, 40, 50};
print_container(myList);  // 输出:10 20 30 40 50// 使用 std::list 的迭代器范围初始化 MyVector
MyVector<int> myVector(myList.begin(), myList.end());
myVector.print();  // 输出:10 20 30 40 50// 创建一个 std::vector 并初始化
std::vector<int> myVec = {1, 2, 3, 4, 5};
print_container(myVec);  // 输出:1 2 3 4 5// 使用 std::vector 的迭代器范围初始化 MyVector
MyVector<int> anotherVector(myVec.begin(), myVec.end());
anotherVector.print();  // 输出:1 2 3 4 5

使用的注意事项

注意调用的优先级匹配机制:

// 类模板的成员函数,还可以继续是函数模版
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}vector(size_t n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}
vector<int> v2(v1.begin(), v1.begin() + 3);
vector<int> v3(lt.begin(), lt.end());
vector<string> v4(10, "1111111");
vector<int> v5(10);
vector<int> v6(10, 1);
vector<int> v7(10, 1);

当使用以上函数模板来构造对象的时候,当遇到vector<int> v6(10, 1);vector<int> v7(10, 1);这种构造时编译器会对进入的模板函数产生异常,会优先进入vector(InputIterator first, InputIterator last),当解引用int类型的时候程序就会异常。

所以在写函数模板的是需要注意注意构造时的匹配机制,应该写的更准确一些,这样才能避免被不属于该类型构造的构造函数模板调用:

vector(int n, const T& val = T())
{reserve(n);for (int i = 0; i < n; i++){push_back(val);}
}

当有一个更明确的构造函数的时候,当编译vector<int> v6(10, 1);的时候就会进入该函数模板实例化的函数进行构造,正常运行。

使用memcpy拷贝问题

问题引出

以下是push_backresereve的逻辑代码:

void push_back(const T& x)
{// 扩容if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish = x;++_finish;
}
void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();T* tmp = new T[n];memcpy(tmp, _start, old_size * sizeof(T));delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

执行以下测试代码:

void test_vector()
{vector<string> v;v.push_back("11111111111111111111");v.push_back("11111111111111111111");v.push_back("11111111111111111111");v.push_back("11111111111111111111");print_container(v);v.push_back("11111111111111111111");print_container(v);
}

程序崩溃:
image.png

调试分析

前四个stringpush_back正常执行,当调试到第五个string时:

  • 此时tmp空间已经申请成功

image.png

  • 当执行完delete后,发生异常

image.png
image.png

_start被delete释放空间后,监视到tmp空间也被释放,由此可得,_start与tmp可能指向同一块空间

image.png
image.png
有原始视图_Ptr地址观察可得,在memcpy时,执行的是浅拷贝,会直接令tmp指向_start的那块空间,所以才会导致执行delete[],调用析构函数,将vector中存放的string数据全部析构,程序崩溃,_start指向的空间被销毁,tmp也就没有数据了。

解决措施

该问题由memcpy的浅拷贝引出,所以需要手动进行深拷贝来解决空间释放问题:

void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();T* tmp = new T[n];// 避免memcpy的浅拷贝问题for (size_t i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

image.png

当使用深拷贝进行拷贝数据后,就不会出问题了

注意:在涉及空间扩容时用深拷贝进行,避免空间的重复指向。(深拷贝的数据类型都不行:vector<string>,vector<vector<string>>…)

理解使用 vector 构造动态二维数组

什么是二维数组?

一个二维数组可以被看作是一个数组的数组。例如,一个 3x3 的二维数组可以表示为:

1 2 3
4 5 6
7 8 9

使用 std::vector 构造动态二维数组

std::vector 是C++标准模板库(STL)中的一个动态数组类模板。与静态数组不同,std::vector 可以在运行时动态调整其大小。我们可以使用 std::vector 来构造一个动态的二维数组。

构造方法

#include <iostream>
#include <vector>int main() {int m = 3; // 行数int n = 4; // 列数// 创建一个 m 行 n 列的二维数组std::vector<std::vector<int>> matrix(m, std::vector<int>(n));// 初始化数组int value = 1;for (int i = 0; i < m; ++i) {for (int j = 0; j < n; ++j) {matrix[i][j] = value++;}}// 打印数组for (const auto& row : matrix) {for (int elem : row) {std::cout << elem << " ";}std::cout << std::endl;}return 0;
}

解析

定义二维数组
std::vector<std::vector<int>> matrix(m, std::vector<int>(n));
  • std::vector<int>(n) 创建了一个包含 nint 元素的向量。
  • std::vector<std::vector<int>> matrix(m, ...) 创建了一个包含 m 个向量的向量,即一个 m x n 的二维数组。
初始化二维数组
int value = 1;
for (int i = 0; i < m; ++i) {for (int j = 0; j < n; ++j) {matrix[i][j] = value++;}
}

使用双重循环遍历二维数组,并将每个元素初始化为一个递增的值。

打印二维数组
for (const auto& row : matrix) {for (int elem : row) {std::cout << elem << " ";}std::cout << std::endl;
}

使用范围 for 循环遍历并打印二维数组的内容。

动态调整大小

使用 std::vector 构造的二维数组可以在运行时动态调整大小。我们可以使用 resize 方法调整二维数组的行和列。例如,增加行和列:

// 增加行
matrix.resize(new_m);// 增加列
for (auto& row : matrix) {row.resize(new_n);
}

范围forrow就是一维数组,然后通过改变一维数组中每一个对应的二维空间的大小来改变列的大小。

使用 std::vector 构造动态二维数组为我们提供了极大的灵活性。与静态数组不同,std::vector 可以在运行时动态调整大小,使其更适合处理动态数据集。


image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3269896.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

中断中使用事件组

文章目录 在中断里面使用事件组来改造程序配置MPU6050中断&#xff0c;PB5 INT![image.png](https://img-blog.csdnimg.cn/img_convert/14b1358946990cf8bd07a6e65c1fb7f0.png)配置mpu6050的中断引脚怎么使能mpu6050中断寄存的如下 注意&#xff1a;如果使用了中断后没出现想要…

移植江科大OLED显示汉字需要设置UTF-8格式

1.并且需要添加 --no-multibyte-chars //为了让软件能够识别到UTF-8的字符

zh echarts样式

记录一下&#xff1a; 一个图的配置 在echarts官网demo界面 option {title: {text: },legend: {data: [xxx前, xxx后]},radar: {// shape: circle,name: {// 雷达图各类别名称文本颜色textStyle: {color: #000,fontSize: 16}},indicator: [{ name: 完整性, max: 1 },{ name:…

二叉树_堆(下卷)

前言 接前面两篇的内容&#xff0c;接着往下讲二叉树_堆相关的内容。 正文 那么&#xff0c;回到冒泡排序与堆排序的比较。 我们知道冒泡排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2)&#xff0c;这个效率是不太好的。 那么&#xff0c;我们的堆排序的时间复杂度如何呢&…

《数据结构:链表递归实现二叉树》

文章目录 一、链式结构二叉树二、链式二叉树的遍历1、遍历方式2、前序遍历3、中序遍历4、后序遍历 三、链式二叉树结点个数和高度等1、二叉树结点的个数2、二叉树叶子结点的个数3、第K层结点的个数4、树的深度5、找值所在的结点6、二叉树销毁 四、借助队列完成二叉树的操作1、队…

react.16+

1、函数式组件 在vite脚手架中执行&#xff1a; app.jsx: import { useState } from react import reactLogo from ./assets/react.svg import viteLogo from /vite.svg import ./App.cssfunction App() {console.log(this)return <h2>我是函数式组件</h2> }exp…

leetcode-104. 二叉树的最大深度

题目描述 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3示例 2&#xff1a; 输入&#xff1a;root [1,n…

全能数据分析工具:Tableau Desktop 2019 for Mac 中文激活版

Tableau Desktop 2019 一款专业的全能数据分析工具&#xff0c;可以让用户将海量数据导入并记性汇总&#xff0c;并且支持多种数据类型&#xff0c;比如像是编程常用的键值对、哈希MAP、JSON类型数据等&#xff0c;因此用户可以将很多常用数据库文件直接导入Tableau Desktop&am…

字符串的引入和注意事项

首先&#xff0c;他和整型一样————int data[ ]{1,2,3,4,5}; 和整型数组一个道理————char str[ ]{h,a,l,l,o}; 还可以直接表达成这样————char str[ ]"hallo";字符串变量&#xff0c;可以被修改 或者用另一种方式————char *p"hallo";字符…

C# 使用pythonnet 迁入 python 初始化错误解决办法

pythonnet 从 3.0 版本开始&#xff0c;必须设置Runtime.PythonDLL属性或环境变量 例如&#xff1a; string pathToVirtualEnv ".\\envs\\pythonnetTest"; Runtime.PythonDLL Path.Combine(pathToVirtualEnv, "python39.dll"); PythonEngine.PythonHom…

.Net 检验信息采集及管理系统LIS,成熟的医院实验室管理系统源码

检验管理系统LIS实现了检验信息电子化、检验信息管理自动化&#xff0c;具备与医嘱双向沟通、采用条码管理手段、财务自动计费、仪器双向控制等重要功能特点。其工作流程为通过门诊医生和住院工作站提出检验申请&#xff0c;生成相应患者的化验条码标签&#xff0c;在生成化验单…

计算机专业MEM工程管理硕士课程介绍

计算机专业MEM&#xff08;工程管理硕士&#xff09;的主要学习内容涵盖了工程技术、管理学和经济学等多个领域&#xff0c;特别是结合了计算机专业的特点&#xff0c;注重在项目管理、工程管理、信息系统管理等方面的培养。以下是对计算机专业MEM工程管理硕士主要学习课程的详…

leetcode105. 从前序与中序遍历序列构造二叉树,步骤详解附代码

leetcode105. 从前序与中序遍历序列构造二叉树 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder…

ubuntu22.04单个网口两个IP

其中 4网段IP可用来上网&#xff0c;3 网段用来内网 界面显示: 配置文件&#xff1a; 01-network-manager-all.yaml 放在 /etc/netplan/ # Let NetworkManager manage all devices on this systemnetwork:version: 2renderer: networkdethernets:eth0:dhcp4: falsedhcp6: …

大学生暑期三下乡社会实践通讯稿怎样投稿?

在这个充满挑战与机遇的暑期,我有幸成为学院暑期三下乡社会实践活动的一员,并肩负起志愿服务团队向媒体投稿宣传报道的重任。起初,面对这项任务,我满怀激情与期待,以为只需凭借一腔热血和手中的笔,就能将实践的点点滴滴生动呈现给世人。然而,现实却给我上了一堂生动的课。 之初…

使用vscode连接开发机进行python debug

什么是debug&#xff1f; 当你刚开始学习Python编程时&#xff0c;可能会遇到代码不按预期运行的情况。这时&#xff0c;你就需要用到“debug”了。简单来说&#xff0c;“debug”就是能再程序中设置中断点并支持一行一行地运行代码&#xff0c;观测程序中变量的变化&#xff…

在线心里咨询系统的设计与实现2024(代码+论文+开题报告+ppt)

下载在最后 技术栈: vuemysqlspringboot 展示: 下载地址: https://download.csdn.net/download/hhtt19820919/89583101 备注: 运行有问题请私信我,私信按钮在文章左边)

鱼哥好书分享活动第28期:看完这篇《终端安全运营》终端安全企业基石,为你的终端安全保驾护航!

鱼哥好书分享活动第28期&#xff1a;看完这篇《终端安全运营》终端安全企业基石&#xff0c;为你的终端安全保驾护航&#xff01; 读者对象&#xff1a;主要内容&#xff1a;本书目录&#xff1a;了解更多&#xff1a;赠书抽奖规则: 在当前网络威胁日益复杂化的背景下&#xff…

nginx转发netty长链接(nginx负载tcp长链接配置)

首先要清楚一点&#xff0c;netty是长链接是tcp连接不同于http中负载在http中配置server监听。长连接需要开启nginx的stream模块(和http是并列关系) 安装nginx时注意开启stream&#xff0c;编译时加上参数 --with-stream &#xff08;其他参数根据自己所需来加&#xff09; …

基于Pytorch框架的深度学习densenet121神经网络鸟类行为识别分类系统源码

第一步&#xff1a;准备数据 5种鸟类行为数据&#xff1a;self.class_indict ["bowing_status", "grooming", "headdown", "vigilance_status", "walking"] &#xff0c;总共有23790张图片&#xff0c;每个文件夹单独放一…