C++ STL 容器 vector

目录

  • 1. vector 对象
  • 2. vector 大小 size 和 容量 capacity
  • 3. vector 成员函数
    • 3.1 迭代器
    • 3.2 容量
    • 3.3 元素访问
    • 3.4 插入
    • 3.5 删除
    • 3.6 动态扩充与收缩
  • 4. vector 迭代器失效问题
  • 总结
    • 其他补充

本文测试环境为 编译器 gcc 13.1

vector 是 STL 中的一个顺序容器,它给我们提供了一个动态数组,那它的动态扩充和收缩到底是如何实现的呢?


1. vector 对象

我们在 main 函数中使用默认构造函数创建一个 vector<int> vec; 对象

vector<int> vec;

这个对象的大小为 24 字节,3 个指针的大小

对象的内部表示如图所示
在这里插入图片描述
这是源码中的定义

struct _Vector_impl_data {
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;
// ...
}

这三个指针是什么含义后面再介绍

这三个指针变量本身是在栈上的,它们的值,也就是地址,是堆上的地址

也就是说,vec 对象在栈上,它通过这三个指针维护了堆上的一块连续空间,来实现一个连续、动态的数组

2. vector 大小 size 和 容量 capacity

想要了解 vec 的底层模型就必须先了解这两个概念

  • size:是指数组元素的个数
  • capacity:是指当前可用空间能够容纳的元素的个数

上面默认构造的 vec 对象的大小和容量都为 0

现在我们再看一下 vec 对象中的三个指针的含义,分别为:

  • start:指向第一个元素
  • finish:指向最后一个元素的下一个位置
  • end_of_storage:指向当前可用空间的下一个位置

现在我们改变一下 vec 对象的大小和容量

vec.reserve(4);
vec.resize(2);

现在 vec 的大小是 2,容量是 4

看下面的图就清楚大小和容量了
在这里插入图片描述
上图所示的空间是堆上的空间,vec 容器所管理的元素是在堆上的

start 和 finish 之间就是我们口头说的数组

start 和 end_of_storage 之间则是当前可用的空间,一共有 4 个容量,我们已经使用了 2 个元素了,还能再使用 2 个,如果使用完了,再向这个 vec 对象添加元素,就需要进行动态扩充了

3. vector 成员函数

通过上面两张图,相信大家已经大概了解 vector 在内存中的模型了

vec 对象通过这三个指针来完成动态数组的功能,下面来看看 vector 提供的成员方法是如何实现的吧

3.1 迭代器

我们首先搞懂 vector 的迭代器是怎么实现的

vector 迭代器是一个随机访问迭代器,需要提供 ++、- - 、+ i、- i、[ i ] 、比较等操作

这样的话完全可以使用指针来实现

1. __normal_iterator

在 vector 定义里,gcc 中是这样定义的

typedef __normal_iterator<pointer, vector> iterator;
typedef __normal_iterator<const_pointer, vector> const_iterator;

其中的 pointer 其实就是 T*

来看看这个 __normal_iterator

template<typename _Iterator, typename _Container>
class __normal_iterator
{
protected:
  _Iterator _M_current;
  // ...
}

内部就是一个 T* 指针

下面是部分它提供的接口

_GLIBCXX_CONSTEXPR __normal_iterator() _GLIBCXX_NOEXCEPT
: _M_current(_Iterator()) { }

通过 T* 指针来构造

_GLIBCXX20_CONSTEXPR
reference
operator*() const _GLIBCXX_NOEXCEPT
{ return *_M_current; }

_GLIBCXX20_CONSTEXPR
pointer
operator->() const _GLIBCXX_NOEXCEPT
{ return _M_current; }

_GLIBCXX20_CONSTEXPR
reference
operator[](difference_type __n) const _GLIBCXX_NOEXCEPT
{ return _M_current[__n]; }

_GLIBCXX20_CONSTEXPR
__normal_iterator&
operator++() _GLIBCXX_NOEXCEPT
{
	++_M_current;
	return *this;
}

// ...

可以看到,其实就是对普通指针 T* 的封装

2. vector 迭代器

那么从 vector 中得到迭代器访问数组,其实就是得到 vector 中的 start 和 finish 指针了,这两个指针之间就是当前管理的元素

iterator begin() 
{ return iterator(start); }

iterator end()
{ return iterator(finish); }

所以 end( ) 不是最后一个元素,而是最后元素的下一个

得到了 iterator 就可以像普通指针一样访问这个数组了

在这里插入图片描述

3.2 容量

通过上面的图,我们很容易想到 size( ) 和 capacity( ) 如何实现

size 只需要 start 和 finish 两个指针相减

size_t size() const 
{ return size_t(finish - start); }

capacity 只需要 start 和 end_of_storage 两个指针相减

size_t capacity() const 
{ return size_t(end_of_storage - start); }

在这里插入图片描述

empty( ) 用来检查 vector 是否为空,是指容器管理的元素个数是否为 0

bool empty() const 
{ return start == finish; }

// 源码是这样,一个意思
_GLIBCXX_NODISCARD _GLIBCXX20_CONSTEXPR
bool
empty() const _GLIBCXX_NOEXCEPT
{ return begin() == end(); }

3.3 元素访问

可以通过下标和 at 函数来访问 vector 中的元素

reference operator[](size_t n)
{ return *(start + n); }

reference at(size_t n)
{
	if (n >= this->size())
	  __throw_out_of_range_fmt(__N("vector::_M_range_check: __n "
				       "(which is %zu) >= this->size() "
				       "(which is %zu)"),
				   n, this->size());
	return (*this)[n];
}

[ ] 和 at 区别就是 at 会进行越界检查,越界会抛出异常

可以使用 front 和 back 来访问第一个和最后一个元素

reference front()
{
	__glibcxx_requires_nonempty();
	return *begin();
}

reference back()
{
	__glibcxx_requires_nonempty();
	return *(end() - 1);
}

最后一个元素是 end() - 1 啦

最后 vector 还提供了底层指针的获取,就是 start,指向堆空间中的第一个元素

T* data()
{ return empty() ? nullptr : start; }

3.4 插入

后面的函数涉及对象的创建和销毁,默认情况下,STL 容器都是通过 std::allocator 来实现的,主要是这 4 个步骤

  1. 申请空间 allocate
  2. 构造对象 construct
  3. 析构对象 destroy
  4. 释放空间 deallocate

这 4 个步骤其实就对应了 new 和 delete。简单实现如下:

1.申请空间使用 operator new,里面调用 malloc 来申请空间

template <class T>
T* allocate()
{
  return static_cast<T*>(::operator new(sizeof(T)));
}

2.构造对象调用对象的构造函数

template <class Ty>
void construct(Ty* ptr)
{
  ::new ((void*)ptr) Ty();
}

template <class Ty1, class Ty2>
void construct(Ty1* ptr, const Ty2& value)
{
  ::new ((void*)ptr) Ty1(value);
}

template <class Ty, class... Args>
void construct(Ty* ptr, Args&&... args)
{
  ::new ((void*)ptr) Ty(mystl::forward<Args>(args)...);
}

3.析构对象调用对象的析构函数

template <class Ty>
void destroy(Ty* ptr)
{
  ptr->~Ty();
}

template <class ForwardIter>
void destroy(ForwardIter first, ForwardIter last)
{
  for (; first != last; ++first)
    destroy(&*first);
}

4.释放空间使用 operator delete ,里面调用 free 来释放空间

template <class T>
void deallocate(T* ptr)
{
  if (ptr == nullptr)
    return;
  ::operator delete(ptr);
}

析构函数可以直接通过指针调用,但构造函数不能,需要使用 operator new(size_t, void* p) 来调用,可以在 p 这个地址上构造一个对象

下面是 vector 常用的函数实现(仅提供关键信息,具体请自行查看源码)

1. push_back

void push_back(const T& x)
{
	if (finish != end_of_storage)
	{
		construct(finish, x);
		++finish;
	}
	else
		//realloc_insert
		//...
}

在 finish 上构造一个新对象

2. pop_back

void pop_back()
{
	__glibcxx_requires_nonempty();
	--finish;
	destroy(finish);
}

从实现上便可以看出,vector 在末尾插入或移除元素复杂度——均摊常数 O(1)

3. insert

iterator insert(const_iterator __position, const T& __x)
{
	const size_t __n = __position - begin();
	if (finish != end_of_storage)
	{
		if (__position == end())
		{
			construct(finish, __x);
			++finish;
		}
		else
		{

			const auto __pos = begin() + (__position - cbegin());
			// __x could be an existing element of this vector, so make a
			// copy of it before _M_insert_aux moves elements around.
			_Temporary_value __x_copy(this, __x);
			_M_insert_aux(__pos, std::move(__x_copy._M_val()));
		}
	}
	else
		_M_realloc_insert(begin() + (__position - cbegin()), __x);
	return iterator(start + __n);
}

void _M_insert_aux(iterator __position, _Arg&& __arg)
{
	construct(finish, _GLIBCXX_MOVE(*(finish - 1)));
	++finish;
	
	_GLIBCXX_MOVE_BACKWARD3(__position.base(),
							finish - 2,
							finish - 1);

	*__position = std::forward<_Arg>(__arg);
}

其中 _GLIBCXX_MOVE_BACKWARD3 最后是这样的

__copy_move_b(_BI1 __first, _BI1 __last, _BI2 __result)
{
  while (__first != __last)
    *--__result = *--__last;
  return __result;
}

所以在中间插入元素,后面的元素需要后移,与到 vector 结尾的距离成线性 O(n)

3.5 删除

1. clear

void clear()
{
	destroy(start, finish);
	finish = start;
}

clear 影响 size ,不影响 capacity

2. erase

iterator erase(const_iterator __position)
{ return _M_erase(begin() + (__position - cbegin())); }

iterator _M_erase(iterator __position)
{
	if (__position + 1 != end())
		_GLIBCXX_MOVE3(__position + 1, end(), __position);
	--finish;
	destroy(finish);
	return __position;
}

其中 _GLIBCXX_MOVE3 最后可能是这样

__copy_m(_II __first, _II __last, _OI __result)
{
	typedef typename iterator_traits<_II>::difference_type _Distance;
	for(_Distance __n = __last - __first; __n > 0; --__n)
	{
		*__result = std::move(*__first);
		++__first;
		++__result;
	}
	return __result;
}

仔细分析一下,要 erase 的元素是先通过 move 获得后面一个元素的资源,被析构的是最后一个元素,当然,析构之前这个元素的资源已经转移给前一个了

所以,vector 插入或移除元素复杂度——与到 vector 结尾的距离成线性 O(n)

3.6 动态扩充与收缩

1. reserve

void reserve(size_type __n)
{
	if (__n > max_size())
		__throw_length_error(__N("vector::reserve"));
	if (capacity() < __n)
	{
		const size_type __old_size = size();
		pointer __tmp;
		#if __cplusplus >= 201103L
		if _GLIBCXX17_CONSTEXPR (_S_use_relocate())
		  {
		  	// 申请新空间
		    __tmp = this->_M_allocate(__n);
		    // 移动构造,析构旧元素(此时旧元素资源已转移)
		    _S_relocate(this->_M_impl._M_start, this->_M_impl._M_finish,
		  __tmp, _M_get_Tp_allocator());
		  }
		else
		#endif
		  {
		  	// 申请新空间,拷贝构造
		    __tmp = _M_allocate_and_copy(__n,
		_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(this->_M_impl._M_start),
		_GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR(this->_M_impl._M_finish));
			// 析构旧元素
		    std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
		    _M_get_Tp_allocator());
		  }
		_GLIBCXX_ASAN_ANNOTATE_REINIT;
		
		// 释放旧空间
		_M_deallocate(this->_M_impl._M_start,
		this->_M_impl._M_end_of_storage
		- this->_M_impl._M_start);
		
		this->_M_impl._M_start = __tmp;
		this->_M_impl._M_finish = __tmp + __old_size;
		this->_M_impl._M_end_of_storage = this->_M_impl._M_start + __n;
	}
}

vector 的扩容就是三点

  • 申请新空间
  • 转移元素到新空间(构造新元素, 析构旧元素)
  • 释放旧空间

debug 发现,代码中 if _GLIBCXX17_CONSTEXPR (_S_use_relocate()) 这个条件判断应该是元素是否支持移动,大概是这样

可移动则走第一个分支,先分配需要的全部空间,然后通过移动将元素转移,具体是遍历,在新空间移动构造一个元素(获得旧空间一个元素的资源),析构旧空间的一个元素,即 move, destroy, move, destroy, …

否则走第二个分支,_M_allocate_and_copy 内部是先分配需要的全部空间,然后挨个拷贝过去,拷贝完毕使用 std::_Destroy 析构旧空间元素,即 copy, copy … destroy, destroy,…

所以转移元素时,该类型 T 支持移动则调用移动,否则复制

具体如何移动请看下面

// _S_relocate 调用
static pointer
_S_do_relocate(pointer __first, pointer __last, pointer __result,
_Tp_alloc_type& __alloc, true_type) noexcept
{
	return std::__relocate_a(__first, __last, __result, __alloc);
}
// __relocate_a 调用
inline _ForwardIterator
__relocate_a_1(_InputIterator __first, _InputIterator __last,
 _ForwardIterator __result, _Allocator& __alloc)
{
  //...
  _ForwardIterator __cur = __result;
  // 遍历,转移
  for (; __first != __last; ++__first, (void)++__cur)
	std::__relocate_object_a(std::__addressof(*__cur),
	 std::__addressof(*__first), __alloc);
  return __cur;
}

inline void
__relocate_object_a(_Tp* __restrict __dest, _Up* __restrict __orig,
_Allocator& __alloc)
{
  typedef std::allocator_traits<_Allocator> __traits;
  // 构造
  __traits::construct(__alloc, __dest, std::move(*__orig));
  // 析构
  __traits::destroy(__alloc, std::__addressof(*__orig));
}

每一次都是使用 move 将一个 orig 转移到 dest,然后析构 orig

2. shrink_to_fit

void shrink_to_fit()
{ _M_shrink_to_fit(); }

bool _M_shrink_to_fit()
{
	if (capacity() == size())
		return false;
	_GLIBCXX_ASAN_ANNOTATE_REINIT;
	return std::__shrink_to_fit_aux<vector>::_S_do_it(*this);
}

bool _S_do_it(_Tp& __c) noexcept
{
	#if __cpp_exceptions
	try
	{
		// 与临时对象交换
		_Tp(__make_move_if_noexcept_iterator(__c.begin()),
		__make_move_if_noexcept_iterator(__c.end()),
		__c.get_allocator()).swap(__c);
		return true;
	}
	catch(...)
		{ return false; }
	#else
	return false;
	#endif
}

shrink_to_fit 就是创建一个临时 vector ,然后进行交换,最后 size 和 capacity 相等

3. resize

void resize(size_type __new_size)
{
	if (__new_size > size())
		_M_default_append(__new_size - size());
	else if (__new_size < size())
		_M_erase_at_end(this->_M_impl._M_start + __new_size);
}

参数 size 大于原 size 时,末尾添加

参数 size 小于原 size 时,末尾删除

4. vector 迭代器失效问题

首先,迭代器失效到底是什么意思?是迭代器指向的地址不能访问了吗?

个人认为,迭代器失效是指,一开始我们从 vector 中获得一个迭代器,然后进行一些操作(插入、删除等等),后面再使用这个迭代器可能会把我们想做的事情搞砸,这个迭代器指向的元素已经不是我们想当然认为的元素了

vector 因重新分配的特点(元素转移到新空间了),迭代器失效的问题较多:

  • 插入元素

    末尾插入:size < capacity 时,首迭代器不失效尾迭代失效(未重新分配空间),size == capacity 时,所有迭代器均失效(需要重新分配空间)

    中间插入:size < capacity 时,首迭代器不失效但插入元素之后所有迭代器失效,size == capacity 时,所有迭代器均失效

  • 删除元素

    末尾删除:只有尾迭代器失效

    中间删除:删除位置之后所有迭代器失效

总结

vector 的特点就是它的重新分配机制了,这样实现了动态数组的功能,但另一方面,需要转移元素到新空间,释放旧空间造成了一定的开销,这就要求我们善于利用 reserve 以及 移动 来减少一些不必要的开销了

其他补充

这里记录一个例子,怎么正确地删除 vector 容器中的元素

vector<int> vec(6);
std::iota(vec.begin(), vec.end(), 1);

auto it = vec.begin();
for (; it != vec.end(); it++) {
    // if (...)
    vec.erase(it);
}
for (auto n : vec) {
    cout << n << endl;
}

结果是

2
4
6

要正确实现需要这样修改

for (; it != vec.end(); ) {
    // if (...)
    it = vec.erase(it);
}

这里要搞明白 erase 的返回值是什么,erase 返回的迭代器还是指向了要 erase 的那个元素的位置,只不过现在这个元素的内容其实是之前的下一个元素

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何将静态网页资源“打包“成.exe或者.apk

Hello , 我是小恒不会java。最近有音乐播放器win桌面应用程序的需求&#xff0c;那就说说上手electron 又想到很多人对apk文件不太了解&#xff0c;apk文件就是安卓桌面应用程序&#xff0c;比如你手机现在打开的微信 当然&#xff0c;exe文件基本都清楚&#xff0c;windows可执…

正则表达式(Regular Expression)

正则表达式很重要&#xff0c;是一个合格攻城狮的必备利器&#xff0c;必须要学会&#xff01;&#xff01;&#xff01; &#xff08;参考视频&#xff09;10分钟快速掌握正则表达式&#xff08;奇乐编程学院&#xff09;https://www.bilibili.com/video/BV1da4y1p7iZ在线测试…

React Hooks(常用)笔记

一、useState&#xff08;保存组件状态&#xff09; 1、基本使用 import { useState } from react;function Example() {const [initialState, setInitialState] useState(default); } useState(保存组件状态) &#xff1a;React hooks是function组件(无状态组件) &#xf…

再拓信创版图-Smartbi 与东方国信数据库完成兼容适配认证

近日&#xff0c;思迈特商业智能与数据分析软件 [简称&#xff1a;Smartbi Insight] V11与北京东方国信科技股份有限公司 &#xff08;以下简称东方国信&#xff09;CirroData-OLAP分布式数据库V2.14.1完成兼容性测试。经双方严格测试&#xff0c;两款产品能够达到通用兼容性要…

浪潮信息成功打造大规模、高性能、高可靠的单存储集群方案!

为帮助企业应对商业智能应用中面临的关于海量数据存储及实时分析的难题&#xff0c;浪潮信息日前通过技术研发&#xff0c;创新推出全球首个SAP HANA集群方案&#xff0c;该方案实现了最大可支持HANA集群服务器节点数量的翻倍&#xff0c;单存储即可支持16节点的&#xff0c;大…

图片高效批量管理,一键批量旋转150度,高效整理您的图片库

在数字化时代&#xff0c;我们的生活中充满了各种图片。从手机拍照到网络下载&#xff0c;从社交媒体到工作文档&#xff0c;图片无处不在。然而&#xff0c;随着图片数量的不断增加&#xff0c;如何高效管理这些图片&#xff0c;让它们有序、易于查找&#xff0c;成为了许多人…

Vue3从入门到实战:深度了解相关API

shallowRef 作用&#xff1a;创建一个响应式数据&#xff0c;但只对顶层属性进行响应式处理。 用法&#xff1a; let myVar shallowRef(initialValue); 特点&#xff1a;只跟踪引用值的变化&#xff0c;不关心值内部的属性变化。 shallowReactive 作用&#xff1a;创建一个…

【MySQL】表的基本约束

文章目录 1、约束类型1.1NOT NULL约束1.2UNIQUE&#xff1a;唯一约束1.3DEFAULT&#xff1a;默认值约束1.4PRIMARY KEY&#xff1a;主键约束1.5FOREIGN KEY&#xff1a;外键约束 2、表的设计2.1一对一2.2一对多2.3多对多 1、约束类型 关键字解释NOT NULL指示某列不能存储NULL值…

点赞列表查询列表

点赞列表查询列表 BlogController GetMapping("/likes/{id}") public Result queryBlogLikes(PathVariable("id") Long id) {return blogService.queryBlogLikes(id); }BlogService Override public Result queryBlogLikes(Long id) {String key BLOG_…

【C++航海王:追寻罗杰的编程之路】C++11(上)

目录 1 -> C11简介 2 -> 统一的列表初始化 2.1 -> {}初始化 2.2 -> std::initializer_list 3 -> 声明 3.1 -> auto 3.2 -> decltype 3.3 -> nullptr 1 -> C11简介 在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)&#xff0c;使得C…

Debian

文章目录 前言一、使用root用户操作二、配置用户使用sudo命令三、添加桌面图标显示1.打开终端2.执行安装命令3.执行成功后重启4. 打开扩展&#xff0c;配置图标 四、图形化界面关闭和打开五、设置静态IP1.查询自己系统网络接口2.修改网络配置文件 总结 前言 Debian 系统在安装…

基于Springboot+Vue的Java项目-在线文档管理系统开发实战(附演示视频+源码+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &am…

RUOYI 若依 横向菜单

保留移动端适配 小屏适配 菜单权限等 可轻松进行深度自定义菜单样式 以及分布 仅支持横向布局 如需源码 教程等 ➕ wx 技术支持 wx : 17339827025

【IEEE出版 | 中山大学主办 | 往届会后2-4个月EI检索】第五届电子通讯与人工智能学术会议(ICECAI 2024)

第五届电子通讯与人工智能国际学术会议&#xff08;ICECAI 2024&#xff09; 2024 5th International Conference on Electronic communication and Artificial Intelligence 第五届电子通讯与人工智能国际学术会议&#xff08;ICECAI 2024&#xff09;将于2024年5月31日-6月…

淘宝订单交易详情查询API是淘宝开放平台提供的接口,可以通过该接口获取淘宝订单的详细信息。

淘宝订单交易详情查询API是淘宝开放平台提供的接口&#xff0c;可以通过该接口获取淘宝订单的详细信息。通过该API&#xff0c;你可以获取订单的基本信息、商品信息、买家信息、物流信息等。 具体使用该API需要进行以下步骤&#xff1a; 在淘宝开放平台注册开发者账号&#xf…

QA测试开发工程师面试题满分问答15: 讲一讲InnoDB和MyISAM

InnoDB和MyISAM是MySQL中两种常见的存储引擎&#xff0c;它们在数据存储和处理方面有着显著的区别。让我们逐一来看一下它们的区别、原理以及适用场景。 区别&#xff1a; 事务支持&#xff1a;InnoDB是一个支持事务的存储引擎&#xff0c;而MyISAM不支持事务。事务是一种用于维…

L2-045 堆宝塔

L2-045 堆宝塔 分数 25 全屏浏览 切换布局 作者 陈越 单位 浙江大学 堆宝塔游戏是让小朋友根据抓到的彩虹圈的直径大小&#xff0c;按照从大到小的顺序堆起宝塔。但彩虹圈不一定是按照直径的大小顺序抓到的。聪明宝宝采取的策略如下&#xff1a; 首先准备两根柱子&#xff…

C++运算符重载和日期类的实现

运算符重载 参数个数与操作个数应该一致(双目操作符就是2个参数,同时参数中包括this) 不能被重载的运算符 " .* "运算符的作用 .*就是用来调用成员函数指针的 调用 1.显式调用 运算符重载可以显式调用 eg. 2.转换调用 运算符重载增强了程序的可读性 bool operato…

SpringBoot版本配置问题与端口占用

前言 ​ 今天在配置springboot项目时遇到了一些问题&#xff0c;jdk版本与springboot版本不一致&#xff0c;在使用idea的脚手架创建项目时&#xff0c;idea的下载地址是spring的官方网站&#xff0c;这导致所下载的版本都是比较高的&#xff0c;而我们使用最多的jdk版本是jdk…

使用不锈钢微型导轨的优势!

微型导轨是一种专门用于在紧凑空间内执行高精度的机器运动控制的导轨设备。其特点是尺寸小、精确度高、刚性好、平稳性好以及使用寿命长。微型导轨的材质种类多样&#xff0c;一般包括钢、不锈钢、铝合金等。目前来说&#xff0c;不锈钢材质的使用率最为频繁&#xff0c;那么使…