C++ vector 深度解析:从原理到实战的全方位指南

news2025/5/24 16:03:17

一、引言

在 C++ 编程中,我们经常需要处理一组数据。比如,你想存储一个班级所有学生的成绩,或者保存用户输入的一组数字。最容易想到的方法是使用数组:

int scores[100]; // 定义一个能存储100个成绩的数组

但数组有两个明显的缺点:

  1. 大小固定:一旦定义,无法动态调整。如果实际学生超过 100 人,数组就不够用了。
  2. 内存管理麻烦:需要手动分配和释放内存(在使用动态数组时)。

vector 就是为解决这些问题而生的! 它是 C++ 标准库提供的动态数组,可以自动管理内存,还支持各种方便的操作

二、vector 基础:快速上手

2.1 如何使用 vector?

要使用 vector,首先需要包含头文件:

#include <vector>
using namespace std; // 为了简化代码,使用标准命名空间

2.2 创建 vector 对象

vector 的使用非常灵活,可以根据需要创建不同类型和初始值的 vector:

// 1. 创建空的 vector(最常用)
vector<int> v1; // 存储整数的 vector

// 2. 创建包含 n 个元素的 vector
vector<double> v2(5); // 创建包含 5 个 double 的 vector,初始值为 0.0

// 3. 创建包含 n 个指定值的 vector
vector<string> v3(3, "hello"); // 创建包含 3 个 "hello" 的 vector

// 4. 使用现有数组或其他 vector 初始化
int arr[] = {1, 2, 3, 4};
vector<int> v4(arr, arr + 4); // 使用数组初始化

vector<int> v5(v4); // 使用另一个 vector 初始化

2.3 常用操作:增删查改

下面是 vector 最常用的操作,新手掌握这些就可以应对大部分场景:

vector<int> v; // 创建空的 vector

// 1. 添加元素(最常用)
v.push_back(10); // 在尾部添加元素 10
v.push_back(20); // 在尾部添加元素 20
v.push_back(30); // 在尾部添加元素 30
// 此时 v 中的元素是:[10, 20, 30]

// 2. 访问元素
cout << v[0] << endl; // 输出第 0 个元素:10
cout << v.at(1) << endl; // 输出第 1 个元素:20(更安全,会检查越界)

// 3. 修改元素
v[0] = 100; // 将第 0 个元素修改为 100
// 此时 v 中的元素是:[100, 20, 30]

// 4. 删除元素
v.pop_back(); // 删除最后一个元素
// 此时 v 中的元素是:[100, 20]

// 5. 获取 vector 大小
cout << v.size() << endl; // 输出 2(当前有 2 个元素)

// 6. 判断 vector 是否为空
if (v.empty()) {
    cout << "vector 为空" << endl;
} else {
    cout << "vector 不为空" << endl;
}

// 7. 清空 vector
v.clear(); // 删除所有元素
cout << v.size() << endl; // 输出 0(vector 现在为空)

三、如何遍历 vector

遍历 vector 中的元素是常见需求,有多种方法可以实现:

3.1 使用下标遍历(类似数组)

vector<int> v = {1, 2, 3, 4, 5};
for (int i = 0; i < v.size(); i++) {
    cout << v[i] << " ";
}
// 输出:1 2 3 4 5

3.2 使用迭代器遍历(更通用)

迭代器是一种类似指针的对象,用于访问容器中的元素。所有标准库容器都支持迭代器:

vector<int> v = {1, 2, 3, 4, 5};

// 正向迭代器
for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " "; // 使用 *it 访问当前元素
}
// 输出:1 2 3 4 5

// 反向迭代器(从后往前遍历)
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
    cout << *rit << " ";
}
// 输出:5 4 3 2 1

3.3 使用范围 for 循环(C++11 及以后,最简单)

vector<int> v = {1, 2, 3, 4, 5};
for (int num : v) {
    cout << num << " ";
}
// 输出:1 2 3 4 5

// 如果需要修改元素,可以使用引用
for (int& num : v) {
    num *= 2; // 将每个元素乘以 2
}
// 此时 v 中的元素是:[2, 4, 6, 8, 10]

四、vector 的高级用法

4.1 存储自定义类型

vector 可以存储任何类型,包括自定义的类或结构体:

struct Student {
    string name;
    int age;
};

vector<Student> students;

// 添加元素
students.push_back({"Alice", 20});
students.push_back({"Bob", 21});

// 访问元素
for (const auto& s : students) {
    cout << s.name << " " << s.age << endl;
}
// 输出:
// Alice 20
// Bob 21

4.2 二维 vector

可以创建多维 vector,最常见的是二维 vector(类似二维数组):

// 创建一个 3x4 的矩阵,初始值为 0
vector<vector<int>> matrix(3, vector<int>(4, 0));

// 赋值
matrix[0][0] = 1;
matrix[1][2] = 5;

// 遍历
for (const auto& row : matrix) {
    for (int num : row) {
        cout << num << " ";
    }
    cout << endl;
}
// 输出:
// 1 0 0 0
// 0 0 5 0
// 0 0 0 0

4.3 排序和查找

vector没有排序和查找的函数,但是可以结合标准库算法进行排序和查找:

#include <algorithm> // 需要包含算法库

vector<int> v = {3, 1, 4, 1, 5, 9};

// 排序
sort(v.begin(), v.end()); // 升序排序
// v 现在是:[1, 1, 3, 4, 5, 9]

// 查找
auto it = find(v.begin(), v.end(), 4);
if (it != v.end()) {
    cout << "找到元素 4,位置是:" << (it - v.begin()) << endl;
} else {
    cout << "未找到元素 4" << endl;
}

五、vector 底层原理与内存管理

5.1 核心数据结构

vector 的底层通过三个指针实现动态数组的管理(不同平台实现不同):

  • T* _start:指向数据存储区的起始位置。
  • T* _finish:指向最后一个有效元素的下一个位置。
  • T* _end_of_storage:指向已分配内存空间的末尾位置。

这三个指针构成了 vector 的内存模型:

template<typename T>
class vector {
private:
    T* _start;
    T* _finish;
    T* _end_of_storage;
};

5.2 扩容机制详解

vector 的最大优势之一是可以动态扩容,不需要我们手动管理内存。但它是如何做到的呢?

5.2.1 容量 vs 大小
  • 大小(size):当前实际存储的元素个数。
  • 容量(capacity):当前分配的内存能够容纳的元素个数。

当 vector 空间不足时,会触发扩容:

  1. 计算新容量:不同编译器采用不同策略:

    • GCC(SGI STL):以 2 倍扩容(如容量为 4 时,扩容后为 8)。
    • MSVC(VS):以 1.5 倍扩容(如容量为 4 时,扩容后为 6)。
    • CLANG:与 MSVC 类似,采用 1.5 倍扩容。
  2. 分配新内存:使用 operator new 分配更大的内存块。

  3. 数据迁移:通过 uninitialized_copy 将旧数据迁移到新空间。

  4. 释放旧内存:调用 operator delete 释放旧内存块。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    size_t cap = v.capacity();
    cout << "初始容量:" << cap << endl; // 0
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
        if (cap != v.capacity()) {
            cap = v.capacity();
            cout << "扩容后容量:" << cap << endl;
            // GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
        }
    }
    return 0;
}
5.2.2 如何避免不必要的扩容?

如果事先知道需要存储多少元素,可以使用 reserve() 方法预分配内存:

vector<int> v;
v.reserve(100); // 预分配 100 个元素的空间

for (int i = 0; i < 100; i++) {
    v.push_back(i); // 不会触发扩容,效率更高
}

扩容策略的数学分析

  • 若扩容因子为 m,插入 n 个元素的均摊时间复杂度为 O(n),因为每次扩容复制的元素数量呈几何级数增长,总和为 O(n)

代码示例:

观察扩容行为

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    size_t cap = v.capacity();
    cout << "初始容量:" << cap << endl; // 0
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
        if (cap != v.capacity()) {
            cap = v.capacity();
            cout << "扩容后容量:" << cap << endl;
            // GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
        }
    }
    return 0;
}

5.3 模拟实现关键函数

5.3.1 拷贝构造函数
template<typename InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}
vector(const vector<T>& v)
{
    vector<T> tmp(v.begin(), v.end()); // 利用迭代器区间构造临时对象
    swap(tmp); // 交换资源,实现深拷贝
}
vector(const vector<T>& v)		
{
    reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

5.3.2 赋值运算符重载
vector<T>& operator=(vector<T> tmp) { // 参数为值传递,自动调用拷贝构造
    swap(tmp); // 交换资源,实现异常安全
    return *this;
}

六、迭代器失效场景与解决方案

6.1 导致迭代器失效的操作

  1. 空间重新分配resizereserveinsertpush_backassign 等操作可能触发扩容,导致原有迭代器失效。
  2. 元素删除erase 操作会使被删除位置之后的迭代器失效。
  3. 容器交换swap 操作会交换两个 vector 的内容,导致原迭代器指向其他容器。
  4. 容器清空clear 操作会删除所有元素,迭代器失效。

6.2 典型错误与修正

6.2.1 错误示例:未处理扩容导致的迭代器失效
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
v.insert(it, 0); // 可能触发扩容
cout << *it << endl; // 未定义行为,it 已失效
6.2.2 正确做法:重新获取迭代器
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
it = v.insert(it, 0); // insert 返回新元素的迭代器
cout << *it << endl; // 正确输出 0

6.3 安全遍历与删除

6.3.1 错误示例:未更新迭代器
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
        v.erase(it); // erase 后 it 失效
    }
}
6.3.2 正确做法:使用 erase 的返回值
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 == 0) {
        it = v.erase(it); // 更新 it 到下一个有效位置
    } else {
        ++it;
    }
}

七、常见问题与陷阱

7.1 浅拷贝问题

  • 场景:当 vector 存储包含动态资源的自定义类型时,默认拷贝构造函数会导致浅拷贝。
  • 解决方案
    1. 为自定义类型实现深拷贝构造函数和赋值运算符。
    2. 使用 vector 的默认深拷贝机制(基于元素的拷贝构造函数)。

7.2 迭代器失效与编译器差异

  • VS 编译器:对迭代器失效检测严格,访问失效迭代器可能直接崩溃。
  • GCC 编译器:检测较宽松,但仍可能导致未定义行为。
  • 建议:避免依赖编译器行为,严格处理迭代器失效。

7.3 内存泄漏

  • 场景:使用 reserve 预分配空间后,直接通过下标访问未初始化的元素。
  • 示例
    vector<int> v;
    v.reserve(10);
    v[5] = 42; // 未定义行为,未初始化的内存
    
  • 修正:使用 resize 初始化元素:
    vector<int> v;
    v.resize(10);
    v[5] = 42; // 合法
    

八、实战案例:高效算法实现

8.1 杨辉三角

vector<vector<int>> generate(int numRows) {
    vector<vector<int>> vv(numRows);
    for(int i=0;i<numRows;i++)
    {
        vv[i].resize(i+1,1);
    }
    for(int i=2;i<numRows;i++)
    {
        for(int j=1;j<i;j++)
        {
            vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
        }
    }
    return vv;
}

8.2 只出现一次的数字

int singleNumber(vector<int>& nums) {
    int result = 0;
    for (int num : nums) {
        result ^= num; // 异或相同数得 0,0 异或任意数得原数
    }
    return result;
}

九、总结与最佳实践

7.1 核心优势

  • 随机访问高效:支持 O (1) 时间复杂度的下标访问。
  • 动态扩容:自动管理内存,避免手动分配 / 释放。
  • 丰富接口:提供 push_backinserterase 等便捷操作。

7.2 最佳实践

  1. 预分配空间:使用 reserve 避免频繁扩容。
  2. 优先使用 emplace_back:减少拷贝 / 移动开销。
  3. 处理迭代器失效:在 insert/erase 后更新迭代器。

通过深入理解 vector 的底层原理和正确使用其接口,开发者可以高效地处理动态数据,避免常见错误,并在实际项目中充分发挥其性能优势。建议结合模拟实现和算法练习(如 LeetCode 题目)进一步巩固知识。

附录

模拟实现(代码)

//vector.h
#include<iostream>
#include<assert.h>
namespace My_vector
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector()
		{ }
		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);
			}
		}
		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		template<typename InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		vector(std::initializer_list<T> il)
		{
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		~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 capacity()const
		{
			return _end_of_storage - _start;
		}
		size_t size()const
		{
			return _finish - _start;
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				iterator tmp = new T[n];
				if (_start)
				{
					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;
			}

		}
		void push_back(const T& val)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = 0 == capacity() ? 4 : 2 * capacity();
				reserve(newcapacity);
			}
			*_finish = val;
			_finish++;
		}
		void pop_back()
		{
			assert(_finish > _start);
			--_finish;
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		void resize(size_t n, const T& val=T())
		{
			if (n > size())
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish != _end_of_storage)
				{
					push_back(val);
				}
			}
			else
			{
				_finish = _start + n;
			}
		}
		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;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = val;
			_finish++;
			return pos;
		}
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			iterator it = pos;
			while (it < _finish)
			{
				*it = *(it + 1);
				it++;
			}
			_finish--;
			return pos;
		}
		T& operator[](size_t n)
		{
			assert(n < size());
			return _start[n];
		}
		vector<T>& operator=(vector<T> tmp)
		{
			swap(tmp);
			return *this;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

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

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

相关文章

鸿蒙进阶——Framework之Want 隐式匹配机制概述

文章大纲 引言一、Want概述二、Want的类型1、显式Want2、隐式Want3、隐式Want的匹配 三、隐式启动Want 源码概述1、有且仅有一个Ability匹配2、有多个Ability 匹配需要弹出选择对话框3、ImplicitStartProcessor::ImplicitStartAbility3.1、GenerateAbilityRequestByAction3.1.1…

antv/g6 图谱封装配置(二)

继上次实现图谱后&#xff0c;后续发现如果要继续加入不同样式的图谱实现起来太过麻烦&#xff0c;因此考虑将配置项全部提取封装到js文件中&#xff0c;图谱组件只专注于实现各种不同的组件&#xff0c;其中主要封装的点就是各个节点的横坐标&#xff08;x&#xff09;,纵坐标…

OpenCV CUDA模块图像过滤------用于创建一个最小值盒式滤波器(Minimum Box Filter)函数createBoxMinFilter()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 该函数创建的是一个 最小值滤波器&#xff08;Minimum Filter&#xff09;&#xff0c;它对图像中每个像素邻域内的像素值取最小值。常用于&…

网络抓包命令tcpdump及分析工具wireshark使用

文章目录 环境文档用途详细信息 环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 8,Linux x86-64 Red Hat Enterprise Linux 7,Linux x86-64 SLES 12,银河麒麟 &#xff08;鲲鹏&#xff09;,银河麒麟 &#xff08;X86_64&#xff09;,银河麒麟&#xff08;龙…

车载诊断架构 --- 车载诊断有那些内容(上)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…

【Hadoop】大数据技术之 HDFS

目录 一、HDFS 概述 1.1 HDFS 产出背景及定义 1.2 HDFS 优缺点 1.3 HDFS 组成架构 1.4 HDFS 文件块大小 二、HDFS 的Shell 操作 三、HDFS 的读写流程&#xff08;面试重点&#xff09; 3.1 HDFS 写数据流程 3.2 HDFS 读数据流程 四、DataNode 4.1 DataNode 的工作机制…

聊一下CSS中的标准流,浮动流,文本流,文档流

在网络上关于CSS的文章中&#xff0c;有时候能听到“标准流”&#xff0c;“浮动流”&#xff0c;“定位流”等等词语&#xff0c;还有像“文档流”&#xff0c;“文本流”等词&#xff0c;这些流是什么意思&#xff1f;它们是CSS中的一些布局方案和特性。今天我们就来聊一下CS…

ATGM332D-F8N22单北斗多频定位导航模块

ATGM332D-F8N 系列模块是 12.216mm 尺寸的高性能单北斗多频定位导航模块。该系列模块产品基于中科微新一代 SOC 单北斗多频芯片 AT9880B&#xff0c;支持北斗二号和北斗三号的 B1I、B1C、B2I、B3I、B2a 和 B2b 频点信号。 主要特征 多频点单北斗接收机 支持北斗二号、北斗三号…

2024年热门AI趋势及回顾

人工智能的崛起 2024 年可能会被铭记为人工智能不再是一种技术新奇事物&#xff0c;而是成为现实的一年。微软、Salesforce 和 Intuit 等巨头将人工智能融入主流企业解决方案&#xff1b;从文案写作到数据分析&#xff0c;专门的人工智能应用程序和服务如雨后春笋般涌现&#…

3. OpenManus-RL中使用AgentGym建立强化学习环境

AgentGym概述 AgentGym是为评估和开发大模型agent而设计的支持多环境和多任务的框架。该框架统一采用ReAct格式&#xff0c;提供多样化的交互环境和任务&#xff0c;支持实时反馈和并发操作。 What is Ai Agent&#xff08;基于大模型的智能体&#xff09;? 首先是人造实体&…

C++性能测试工具——sysprof的使用

一、sysprof sysprof相对于前面的一些性能测试工具来说&#xff0c;要简单不少。特别是其图形界面的操作&#xff0c;非常容易上手&#xff0c;它还支持分析文件的保存和导入功能&#xff0c;这是一个非常不错的功能。做为一款系统性能测试工具&#xff0c;它支持多种硬件平台…

树莓派内核源码的下载,配置,编译和替换

共享文件夹的创建 ubuntu创建共享文件夹可以实现和本地windows跨系统文件共享 下面是创建步骤 先在windows准备一个文件夹来当做共享文件夹 树莓派内核源码下载 1.在树莓派终端输入以下指令查看内核版本 uname -r我这里是已经编译替换过后的版本 2.选择树莓派对应的版本号下…

CentOS停止维护了,解决yum不能安装软件的问题

最近在使用CentOS的yum命令安装软件时&#xff0c;出现了如下错误&#xff1a; 原因&#xff1a; 这是因为CentOS在2024 年 6 月 30 日停止维护了&#xff0c;同时也移除了相关的软件镜像仓库&#xff0c;导致网站地址访问不了&#xff0c;从而下载不了软件。 解决方法&#xf…

过压保护电路设计和计算

设备供电电压因各种原因变得过高会烧坏设备,因此可以在前级加过压保护电路。 稳压二极管+PMOS 电路分析 1、当输入电压 Vin < 5.1V 时:(下图以输入电压 Vin = 5V 举例) D1是5.1V稳压管,此时输入电压Vin才5V,小于5.1V,所以稳压管D1未进入稳压状态,不导通。 5.1V稳…

20250523-BUG:无法加载“GameLib/Framework.h“头文件(已解决)

BUG&#xff1a;无法加载"GameLib/Framework.h"头文件&#xff08;已解决&#xff09; 最近在打开新的C项目时报了这个错&#xff0c;我是按照以下步骤来排除的BUG&#xff0c;希望对您有所帮助~ 检查【C/C】-【附加包含目录】中的路径有无问题&#xff0c;一般需要加…

OpenCv高阶(8.0)——答题卡识别自动判分

文章目录 前言一、代码分析及流程讲解&#xff08;一&#xff09;初始化模块正确答案映射字典&#xff08;题目序号: 正确选项索引&#xff09;图像显示工具函数 &#xff08;二&#xff09;轮廓处理工具模块&#xff08;三&#xff09;几何变换核心模块 二、主处理流程图像读取…

Python语法特点与编码规范

注释 单行注释 把#号当做注释符号 多行注释 python中并没有规定多行注释标记&#xff0c;通常使用单引号作为多行注释 中文注释 规定文件所用编码&#xff0c;当时是为解决python2不支持中文的问题 #codingutf-8代码缩进 python采用代码缩进和冒号区分代码层次&#xff0c…

反本能---如何对抗你的习以为常

目录 一、概述 二、自我提升 &#xff08;一&#xff09;我们为什么总想拖延 &#xff08;二&#xff09;如何有效应对拖延 &#xff08;三&#xff09;如何更好的自我控制 &#xff08;四&#xff09;为啥付出了没有回报 &#xff08;五&#xff09;如何提高学习效率 三…

(15)关于窗体的右键菜单的学习与使用,这关系到了信号与事件 event

&#xff08;1&#xff09;起因来源于 4.11 的老师讲的例题&#xff0c;标准的&#xff0c;规范的使用右键菜单的代码及参考资料如下&#xff1a; &#xff08;2&#xff09; 接着脱离上面的那个复杂的环境&#xff0c;用简单的例子测试一下 &#xff1a; 说明老师讲的都是对…

Ubuntu Desktop 24.04 常用软件安装步骤

文章目录 Ubuntu Desktop 24.04 常用软件安装步骤Snipaste F1快捷截图&#xff08;超方便 | 我6台电脑每台都用&#xff09;搜狗输入法快速浏览工具 | 空格键快速预览文件壁纸工具 | varietySSH 工具 | Termius 终端分屏工具 | TmuxCaffeine | 避免息屏小工具 一些设置将启动台…