【c++】vector模拟实现与深度剖析

Alt

🔥个人主页Quitecoder

🔥专栏c++笔记仓

Alt

vector涉及到许多细节问题,比如双层深拷贝,迭代器失效等,本篇文章我们通过模拟实现来深度理解这块的内容

目录

  • `1.基本框架`
  • `2.构造和销毁`
  • `3.元素访问`
  • `4.获取迭代器与容量操作`
    • `reserve开空间`
  • `5.对内容的修改`
    • `迭代器失效`

1.基本框架

namespace own 
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

	private:
		iterator _start;// 指向数据块的开始
		iterator _finish;// 指向有效数据的尾
		iterator _endOfstorage;  // 指向存储容量的尾
	};
}

我们首先定义了一个模版类,这里的vector三个成员均为迭代器而Vector的迭代器是一个原生指针,我们这里为其定义别名iterator

在这里插入图片描述

私有成员:

iterator _start;         // 指向数据块的开始
iterator _finish;        // 指向有效数据的尾
iterator _endOfstorage;  // 指向存储容量的尾

这些成员变量用于管理vector内部的动态数组

  • _start: 这是一个指针,指向分配给vector的内存区域的开始。这是数组的第一个元素
  • _finish: 这个指针指向数组中最后一个实际存在的元素的下一个位置。这意味着它指向结束后的第一个元素,它用来表示存储在vector中的实际元素的结束
  • _endOfstorage: 这个指针指向分配给vector的内存块的末尾。这不是最后一个有效元素的位置,而是整个内存块的结束位置,在这之后可能会有额外的未初始化空间,预留以实现当vector增长时无需重新分配整个数组

2.构造和销毁

在这里插入图片描述
🔥vector()

空值初始化:

vector()
	: _start(nullptr)
	, _finish(nullptr)
	, _endOfStorage(nullptr)
{}

我们也可以直接利用缺省值来完成:

vector()
{}

private:
	iterator _start=nullptr;		
	iterator _finish=nullptr;		
	iterator _endOfStorage=nullptr;  
};

🔥vector(size_t n, const T& value = T())

这个函数的功能是用n个value元素来构造一个vector

实现如下:

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

const T& value = T() 是使用了一个默认参数和引用的函数参数声明。

  • = T() 这部分声明了默认值,如果在调用函数时没有提供这个参数,就会使用它。T() 创建了 T 类型的一个临时对象,这是通过类型的默认构造函数完成的。这意味着如果没有提供具体的 value 值时,构造函数将使用 T 类型默认构造出的一个新对象作为默认值。

例如,如果 Tint,那么 T() 就是 0。如果 T 是某个类类型,并且该类有一个无参数的构造函数,那么 T() 就会调用这个默认构造函数来创建一个新对象。

因此,这个参数声明使得构造函数可以具有灵活性:你既可以用特定的初始值来构造 vector,也可以不提供初始值,让 vector 用类型 T 的默认值来填充

🔥vector(InputIterator first, InputIterator last)

template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

这个函数是vector 类的一个范围构造函数(range constructor),它允许你根据一对迭代器 firstlast 来构造一个新的 vector 对象。这个构造函数遍历从 first 开始一直到 last 结束的序列,并将每个元素添加到新构造的 vector

下面是详细的说明:

  • template<class InputIterator> 这一行表述了模板参数 InputIterator,它是一种迭代器类型,用于表示输入序列中的位置。它可以是指针或支持 ++(前置递增)和 *(解引用)操作的任何类型的迭代器。

  • vector(InputIterator first, InputIterator last) 这是构造函数的声明,它接受两个参数,firstlast,代表输入序列的开始和结束迭代器。序列不包括迭代器 last 指向的元素。序列由 [first, last) 间的元素组成,是一个左闭右开的区间

函数体内的代码逻辑如下:

  • while (first != last) 循环,将一直执行,直到 first 迭代器等于 last 迭代器,这表示已经到达了输入序列的末尾。
  • push_back(*first) 在循环体内部调用,这个函数应该是 vector 类中的成员函数,它会添加解引用迭代器 first 指向的当前元素到 vector 的末尾。
  • ++first 然后迭代器 first 递增以便在下一次迭代中指向序列中的下一个元素。

这个构造函数可以用来构造一个 vector,使其包含现存容器(如另一个 vectorlistarray)中某个子序列的元素,或者任何由迭代器定义的元素序列。例如:

注意,除了这两个函数,我们模拟实现时需要手动增加一个函数:

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

理论上将,提供了vector(size_t n, const T& value = T())之后vector(int n, const T& value = T())就不需要提供了,但是对于:vector<int> v(10, 5);编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型就不会走vector(size_t n, const T& value = T())这个构造方法,最终选择的是:vector(InputIterator first, InputIterator last)因为编译器觉得区间构造两个参数类型一致,因此编译器就会将InputIterator实例化为int但是10和5根本不是一个区间,编译时就报错了故需要增加该构造方法

🔥vector(const vector& v)

拷贝构造函数实现,只需要分配好空间对元素依次尾插即可

vector(const vector<T>& v)
{
	reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

上述有关reserve和push_back函数的模拟实现,我们后文会讲到

🔥~vector()

对于析构函数,我们需要释放空间并置指针指向空:

~vector()
{
	delete[] _start;
	_start = _finish = _endofstorage = nullptr;
}

3.元素访问

🔥operator[ ]

T& operator[](size_t pos)
{
	assert(pos < size());
	return _start[pos];
}

const T& operator[](size_t pos)const
{
	assert(pos < size());
	return _start[pos];
}

🔥获取首尾元素

T& front()
{
	return *_start;
}

const T& front()const
{
	return *_start;
}

T& back()
{
	return *(_finish - 1);
}

const T& back()const
{
	return *(_finish - 1);
}

4.获取迭代器与容量操作

🔥iterator begin(),iterator end()

iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

🔥size(),capacity()

获取容量大小与有效元素个数,只需要进行指针相减即可

size_t size() const
{
	return _finish - _start;
}
size_t capacity() const
{
	return _endofstorage - _start;
}

reserve开空间

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

这里我们开空间完成的是一个深拷贝的过程用 memcpy 将旧数组中的元素复制到新数组,memcpy 在这里用于基于字节的拷贝,memcpy是一个浅拷贝,那么,如果我们vector实例化为string类,这里string类进行浅拷贝会涉及到二次释放等问题

在这里插入图片描述
虽然我的_start指向了新空间完成深拷贝,但是string类完成的是浅拷贝,仍指向原来的空间,这里为了解决上述问题,我们不能使用memcpy来进行拷贝,我们需要进行赋值操作来进行二次深拷贝

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();
		//memcpy(tmp, _start, size() * sizeof(T));
		for (size_t i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;

		_start = tmp;
		_finish = tmp + old_size;
		_endofstorage = tmp + n;
	}
}

通过一个循环,使用拷贝赋值操作符逐个拷贝旧数组中的元素到新数组

在这里插入图片描述
🔥resize()

void resize(size_t n, const T& val = T())
{
	if (n > size())
	{
		reserve(n);
		// 插入
		while (_finish < _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	else
	{
		// 删除
		_finish = _start + n;
	}
}
  • 若resize传入的n大于capacity,进行扩容并用val来填满新位置
  • 若n大于有效元素个数并小于capacity,不进行扩容,用val填满空位置
  • 若n小于有效元素个数,进行删除操作

5.对内容的修改

🔥insert()

void insert(iterator pos, const T& val)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	iterator it = _finish - 1;
	while (it >= pos)
	{
		*(it + 1) = *it;
		--it;
	}
	*pos = val;
	++_finish;
}

首先是否判断需要扩容,接着进行挪动数据,由于这里是指针,挪动数据我们就不用考虑越界问题,指针不会指向零

迭代器失效

注意,上述代码我们忽略了pos的位置

if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

这里就会有迭代器失效的问题

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃,即如果继续使用已经失效的迭代器,程序可能会崩溃

在这里插入图片描述
扩容后,我原先pos指向的位置被释放,这里pos变的不可用

所以这里我们需要更新pos位置

if (_finish == _endofstorage)
{
	size_t len = pos - _start;
	reserve(capacity() == 0 ? 4 : capacity() * 2);

	// 如果扩容了要更新pos
	pos = _start + len;
}

首先,记录pos到起始位置的大小,更新后新的start加上距离即可

在C++标准模板库(STL)中,迭代器失效(Iterator invalidation)是指当底层容器(例如vectorlistmap等)发生改变时,其迭代器可能不再指向正确的元素,或者变得完全不可用。迭代器失效通常会发生在执行插入、删除或重新分配操作后

对于不同类型的容器,迭代器失效的条件会有所不同。对于vector

  1. 增加容器中的元素(例如通过push_backinsert等)可能会导致存储空间重新分配,从而使所有指向容器元素的迭代器、指针和引用失效。如果容器在插入新元素前还有足够的capacity(未使用的预留空间),一般来说,除了指向插入点之后元素的迭代器之外,其他的迭代器、指针和引用会保持有效。

  2. 删除容器中的元素(例如通过erasepop_back等)会使所有指向被删除元素以及之后元素的迭代器、指针和引用失效。

  3. 调整容器的大小(例如通过resize)至大于当前size可能会导致重新分配,这也将导致所有迭代器、指针和引用失效。

当涉及vector类的成员函数时,需要确保任何可能导致迭代器失效的操作之后都不使用旧的迭代器。例如,在调用insert的例子中,如果进行了扩容操作,之前的pos迭代器就将失效,因为reserve可能会导致动态数组的重新分配。所以代码中重新计算了pos的值来防止迭代器失效

要安全地使用迭代器,最好的实践是避免在迭代过程中修改容器的大小和结构,或者如果确实需要修改,则应在每次修改后重新获取迭代器

🔥erase()

在这里插入图片描述
注意!erase返回的值是迭代器

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator it = pos + 1;
	while (it < _finish)
	{
		*(it - 1) = *it;
		++it;
	}

	--_finish;

	return pos;
}

erase函数的返回值是一个迭代器,它指向被删除元素的原位置。由于元素已经被移动了,这个位置现在包含了前一个被删除元素位置之后的元素。

🔥push_back和pop_back
我们这两个函数直接复用上面的函数即可:

void push_back(const T& val)
{
	insert(end(), val);
}

void pop_back()
{
	erase(end() - 1);
}

本节内容到此结束!!感谢大家阅读,文章完整代码如下:

namespace own 
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}
		vector()
		{}
		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 n, const T& val = T())
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		bool empty()
		{
			return _start == _finish;
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}
		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _endofstorage - _start;
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t old_size = size();
				//memcpy(tmp, _start, size() * sizeof(T));
				for (size_t i = 0; i < old_size; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;

				_start = tmp;
				_finish = tmp + old_size;
				_endofstorage = tmp + n;
			}
		}
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}

		T& front()
		{
			return *_start;
		}

		const T& front()const
		{
			return *_start;
		}

		T& back()
		{
			return *(_finish - 1);
		}

		const T& back()const
		{
			return *(_finish - 1);
		}
		void resize(size_t n, const T& val = T())
		{
			if (n > size())
			{
				reserve(n);
				// 插入
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
			else
			{
				// 删除
				_finish = _start + n;
			}
		}

		void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);

				// 如果扩容了要更新pos
				pos = _start + len;
			}

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

			iterator it = pos + 1;
			while (it < _finish)
			{
				*(it - 1) = *it;
				++it;
			}

			--_finish;

			return pos;
		}
		void push_back(const T& val)
		{
			insert(end(), val);
		}

		void pop_back()
		{
			/*assert(!empty());

			--_finish;*/

			erase(end() - 1);
		}
	private:
		iterator _start=nullptr;		
		iterator _finish=nullptr;		
		iterator _endOfstorage=nullptr;  
	};
	template<class T>
	void print_vector(const vector<T>& v)
	{
		for (size_t i = 0; i < v.size(); i++)
		{
			cout << v[i] << " ";
		}
		cout << endl;

		//typename vector<T>::const_iterator it = v.begin();
		/*auto it = v.begin();
		while (it != v.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;*/
	}
}

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

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

相关文章

STM32-DMA(软件出发、硬件触发)

DMA --为cpu减负 DMA简介 直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预&#xff0c;数据可以通过DMA快速地移动&#xff0c;这就节省了CPU的资源来做其他操作。两个DMA控制器有12个通道(DMA1有7个通道&#xff0c;DMA2…

广西桂林最大的模板厂——贵港市能强优品木业有限公司

贵港市能强优品木业有限公司是广西桂林地区最大的建筑模板厂家&#xff0c;拥有着25年的丰富生产经验。该公司以生产高品质的建筑覆膜板而闻名&#xff0c;其产品质量稳定&#xff0c;使用寿命长&#xff0c;深受广大客户的一致好评。 作为一家知名的建筑模板生产厂家&#xff…

Idea:通义千问插件

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 一、通义千问大模型 二、程序编写助手 三、Idea安装通义千问插件 总结 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、通义千问大模型…

为什么感觉 C/C++ 不火了?

所谓火不火的&#xff0c;说白了就是用的人数的多少。哪个东西使用的人或者说围观的人多了&#xff0c;自然就被认为是火了。 编程语言层级从低级语言到高级语言熟悉的人数从来都是一个金字塔模型&#xff1a;在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了…

MySQL常用命令和函数的讲解以及表之间的联结

Mysql的中一些语句的用法&#xff1a; 有表&#xff1a; CREATE TABLE book (id int(20) NOT NULL,book_name varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 书名,press varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NUL…

数据结构之二叉搜索树底层实现洞若观火!

目录 题外话 正题 二叉搜索树 底层实现 二叉搜索树查找操作 查找操作思路 查找代码实现详解 二叉搜索树插入操作 插入操作思路 插入代码详解 二叉搜索树删除操作 删除操作思路 删除代码详解 小结 题外话 我的一切都是党给的,都是人民给的,都是家人们给的!! 十分感…

IDEA使用中, 设置平展软件包。使用IDEA遇到的问题:src里为什么创建包为什么不在包里面

使用IDEA遇到的问题&#xff1a;src里为什么创建包为什么不在包里面 如下图所示 &#xff1a; 点击齿轮设置 如何搞回来&#xff1f; 看下面的Flatten Packages&#xff08;平展软件包&#xff09;取消掉。

C++学习之C++11标准

目录 一&#xff0c;列表初始化 二&#xff0c;initializer_list 三&#xff0c;auto与decltype 1&#xff09;auto 2&#xff09;decltype 四&#xff0c;nullptr 五&#xff0c;范围for 六&#xff0c;新加容器 1&#xff09;array 2&#xff09;forward_list 3&a…

Zabbix 监控系统:监控Windows端

目录 前言 1、zabbix Windows客户端安装包下载 2、安装zabbix Windows客户端 3、 查看zabbix.Agent是否正在运行 4、Zabbix Web 界面配置 5、模拟故障&#xff08;关闭Windows 10机器&#xff09; 6、Zabbix Web 界面验证故障信息 前言 Zabbix是一种开源的网络监控系统…

小扎万字深度访谈:最强开源大模型Llama 3发布,Meta的AGI路径和开源哲学

今天Meta发布了史上最强开源大模型Llama 3&#xff0c;一口气发布了 8B 和 70B 2个预训练和指令微调模型&#xff0c;对比同级别的参数模型&#xff0c;性能上均达到了最佳。 此外&#xff0c;Meta还发布了基于Llama 3的AI助手Meta AI&#xff0c;可以在Facebook、Instagram、W…

优化器与优化策略的搭配

在深度学习中不同的optimizer 通常会选择不同 优化策略 lr_sheduler 与之搭配&#xff1b; 1. SGD 与 Adam 优化器 Adam 与经典 SGD 的不同之处在于&#xff0c; Adam 执行局部参数更新&#xff08;即在参数级别进行更改&#xff09;&#xff0c;而不是全局执行此操作的 SGD…

非计算机专业考软考高项有必要吗?

我认为这非常重要。 看了你的介绍&#xff0c;如果你已经考取了会计证书&#xff0c;而且想要考取计算机专业的证书&#xff0c;或者你的职业规划涉及到计算机岗位&#xff0c;又或者你对计算机感兴趣&#xff0c;我建议你优先考虑软考&#xff0c;因为这个证书的含金量是有保…

冯喜运:4.22晚间欧市支撑阻力:现货黄金+美原油走势及操作建议

【黄金消息面解析 】&#xff1a;周一(4月20日)欧市早盘&#xff0c;现货黄金短线加速跳水&#xff0c;金价目前跌向2350美元/盎司关口&#xff0c;日内崩跌逾40美元。美国定于周五公布的个人消费支出(PCE)物价指数预计将显示&#xff0c;3月PCE物价指数同比增幅将从2月份的2.5…

Linux 安装 Docker +Docker Compose + cucker/get_command_4_run_container

TIP&#xff1a;下面演示的 Linux 系统为 CentOS 7.9。 Docker 更新你的系统并安装必要的依赖项&#xff1a; sudo yum update -y sudo yum install -y yum-utils device-mapper-persistent-data lvm2添加 Docker 的官方仓库&#xff1a; sudo yum-config-manager --add-rep…

什么是 PCIe 及其工作原理?

什么是外围组件互连 Express (PCIe)&#xff1f; 外围组件互连 Express (PCIe) 是一种高速串行计算机扩展总线标准&#xff0c;可将设备连接到主板。 它于 2004 年首次推出&#xff0c;作为以前 PCI 和 AGP 方式的替代。 PCIe 允许处理器和各种扩展卡&#xff08;例如显卡、声…

上市公司数字化转型速度测-含代码及原始数据(2000-2022年)

数据来源&#xff1a;Wind数据库、企业年报时间跨度&#xff1a;2000-2022年 其中吴非、赵宸宇版本的数据是从2000到2022年&#xff1b;袁淳版本和李瑛玫版本的数据均是从2001-2022年。数据范围&#xff1a;上市公司数据指标&#xff1a;计算了三份测算数字化转型速度的数据。其…

关系抽取与属性补全

文章目录 实体关系抽取的任务定义机器学习框架属性补全 实体关系抽取的任务定义 从文本中抽取出两个或者多个实体之间的语义关系&#xff1b;从文本获取知识图谱三元组的主要技术手段&#xff0c;通常被用于知识图谱的补全。美丽的西湖坐落于浙江省的省会城市杭州的西南面。&am…

基于SSM+Jsp+Mysql的文物管理系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

可视化大屏可不是花架子,绝对有实用价值。

Hello&#xff0c;我是大千UI工场&#xff0c;不少老铁觉得可视化大屏就是花架子&#xff0c;是取悦领导的&#xff0c;那真是不懂可视化大屏的价值。欢迎友友们关注、评论&#xff0c;如果有订单可私信。 可视化大屏并不是花架子&#xff0c;而是一种实际有效的工具&#xff0…

实战|哈尔滨等保2.0 Linux主机测评过程之身份鉴别

一、身份鉴别 a)应对登录的用户进行身份标识和鉴别&#xff0c;身份标识具有唯一性&#xff0c;身份鉴别信息具有复杂度要求并定期更换。 输入 more /etc/shadow,得知系统所有用户&#xff0c;此语句字段格式有九段。 第一字段&#xff1a;用户名&#xff08;也被称为登录名…