C++8:模拟实现list

news2024/11/14 14:13:37

目录

最基础的链表结构以及迭代器实现

链表节点结构

构造函数

push_back

list的迭代器

增删查改功能实现

insert

erase

pop_front        

pop_back

push_front

clear

默认成员函数

析构函数

拷贝构造函数

赋值操作符重载

 list的完善

 const迭代器

赋值操作符重载优化

swap

size

类型名问题

->重载问题


list容器的本身其实是一个双向带头循环链表,具体的结构其实并不复杂本人曾在学习数据结构时有所记述,有需要的读者可以跳转至这篇文章数据结构4:双向链表+OJ题_lanload的博客-CSDN博客_双向链表题

那么既然是一个链表,那就少不了节点指针以及节点结构,模拟实现list也需要套入模板,那么我们先实现一个最基本具有数据存放功能的链表来试试看。

最基础的链表结构以及迭代器实现

链表节点结构

     一个链表结构,需要下一个节点的指针,存放当前数据的变量,和上一个节点的指针
    那么作为一个容器,使用模板必不可少。

	template<class T>
	struct List_node
	{
		T _data;
		List_node* _prev;
		List_node* _next;
        
        //节点结构的构造函数,处理数据的传入
		List_node(const T& x)
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{}

	};

构造函数

  无参构造函数
  由于需要设定节点结构内部的变量,不走初始化列表

namespace mylist
{
    template<class T>
    class mylist
    {
    public:
		typedef List_node<T> Node;
		mylist()
		{
			_head = new Node(T());

			//_head->_data = T();匿名构造防止自定义类型偷家.不过这一步NEW已经做过了
			_head->_prev = _head;
			_head->_next = _head;
		}


    };
}

Node* == List_node<T>
list的基础结构是一个带头双向循环链表,那么有一个哨兵位非常合理

private:
    Node* _head;

push_back

  • 双向带头循环链表不需要考虑边边角角,直接尾插
  • 需要一个尾部节点,尾节点的next指向新节点,哨兵位头节点的prev指向新节点,新节点的next指向头节点

		void push_back(const T& val)
		{
			Node* newnode = new Node(val);

			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_next = _head;
			newnode->_prev = tail;
			_head->_prev = newnode;

		}

list的迭代器

 上文我们已经实现了一个具有最基础插入数据功能的链表,但是还差一个访问方式,那么我们就创建一个迭代器。

但是list的迭代器不能同vector不包装直接实现:

  1. 由于不同于连续容器支持连续的指针访问,链表的迭代器不能使用原生指针
  2.  这也就导致了我们实现迭代器的时候需要重新封装一个类,这个类用于获取mylist的指针节点
  3.  所以需要重写++以及*运算符,使得这个类的运作方式形同原生指针

迭代器的结构类
这个类的作用其实很像一个打包袋,既然我们没法使用原生指针来实现++和解引用,那么在原生指针上面套上一个类,就可以间接的实现这些功能了


	template <class T>
	struct _list_iterator
	{    

        //节点类型重定义
		typedef List_node<T> Node;

		//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;


	};

为了很方便的直接把节点指针放进这个类里面,我们直接使用构造函数,然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行

 _list_iterator( Node* p)
      :_pnode(p)
       {}

那么一个迭代器的标准访问还需要解引用以及++的功能才能迭代访问,这些也实现一下,实现的逻辑已用注释给出

	template <class T>
	struct _list_iterator
	{
		typedef List_node<T> Node;

//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;

//然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行
		_list_iterator( Node* p)
			:_pnode(p)
			{}

//运算符重载,*解引用,传进来一个节点,返回节点里面的值
		T& operator*()
		{
			return(_pnode->_data);
		}


//运算符重载,++指向下一个节点,返回下一个节点
//这里为什么返回_list_iterator<T>?因为我们包装了这个原生指针,但是不需要得到里面的东西
//只需要让它往下走一个节点然后返回就行,并且能实现链式访问
		_list_iterator<T>& operator ++()
		{
			_pnode = _pnode->_next;
			return *this;

		}


	};

 重写完了这个类还没有结束,我们还需要给出begin的接口以及end的接口。在这里我们重定义迭代器类的名称以方便使用

		typedef _list_iterator<T> iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

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

试试效果,使用迭代器和范围for进行访问

 没有问题。

增删查改功能实现

  • 链表的增删查改,insert,earse,pop_back,pop_front,push_front,clear,析构,拷贝构造,赋值操作符重载,其中的尾插尾删头插头删都可以借由insert以及erase便捷实现,其余的实现也不算困难

insert

		void insert(iterator pos, const T& val)
		{
			//先创建一个节点
			Node* newnode = new Node(val);
//这里需要节点的指针,但是iterator并不能解引用,理解稍微有点不到位,被存放的指针拿去初始化这个类了
			Node* cur = pos._pnode;
			
			Node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

		}

erase

 删除,然后链接被删除的节点,但是需要注意的是别把哨兵位节点给干掉了

        iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._pnode;
			Node* prev = cur->_prev;
			Node* next = cur->_next;


			prev->_next = next;
			next->_prev = prev;

			delete pos._pnode;

			return iterator(next);
		}

pop_front        

		//头删
		void pop_front()
		{
			erase(begin());//等价于this->erase( this-> begin())
		}

pop_back

		//尾删
		void pop_back()
		{
			erase(--end());
		}

push_front

    //头删
		void pop_front()
		{
			erase(begin());//等价于this->erase( this-> begin())
		}

clear

		void clear()
		{

			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

默认成员函数

析构函数

        ~mylist()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

拷贝构造函数

 拷贝构造的传参我们暂时不使用const修饰,因为还没有实现const迭代器,我们后文实现

		mylist(mylist<T>& l2)
		{
			//我们先置空

			_head = new Node(T());

			_head->_next = _head;
			_head->_prev = _head;

			iterator il2 = l2.begin();
			while (il2 != l2.end())
			{
				push_back(*il2);
				++il2;
			}
		}

赋值操作符重载

		//赋值操作符重载
		mylist<T>& operator = ( mylist<T>& l2)
		{
			if (this != &l2)
			{
				clear();

				for (auto& e : l2)
				{
					push_back(e);
				}
			}
			return *this;
		}

 list的完善

 const迭代器

  •  我们前文提到过,之所以不使用const对象做拷贝构造函数的参数是因为没有实现conts对象的迭代器,那么为什么没实现就不能用呢?

在这里,先再次回顾一次const迭代器的作用,当一个类生成一个对象的时候,可以分为普通对象以及conts对象,普通对象可读可写,const对象只能读不能写,那么针对const对象的这个特性,一部分成员函数以及迭代器都需要额外的实现const版本,不然const对象无法调用对应的成员函数。

回到话题上来,在尝试给拷贝构造的参数加上const之后,程序就报错了

 那么根据以上的报错,我们很可能会借助前车之鉴也就是vector的const迭代器来尝试改写成如下形式以修复bug

 但其实这样子与const迭代器的使用目的不相同,const修饰一个变量的时候有两种形式

const T* p1;//1
T* const p2;//2
  • 代码1的const保护的是p1指针所指向的对象,而p1本身则可以被修改
  • 代码2的const保护的则是p2这个指针本身,其对象依旧可以被修改。
  • 我们希望const迭代器遵从的修饰规则是代码1,也就是保护所指向的对象,但是我们的迭代器是一个被封装好的类,我们对其加上了const只能让迭代器类本身不能被修改。
  • const迭代器和普通迭代器都可以解引用以及执行++操作区别则在于const迭代器并不能对解引用出来的值进行修改

归根结底:使用const做传递参数,需要额外实现针对const对象而编写的const迭代器以支持访问,但其实同普通的迭代器所实现的成员函数相比,const迭代器的不同则体现在解引用时的返回值的不同。普通迭代器返回T&     而const迭代器需要返回const T&

 那么很简单我们直接上手,对症下药不就好了吗

		T& operator*()
		{
			return(_pnode->_data);
		}

		const T& operator*() const
		{
			return(_pnode->_data);
		}

诶,这样不就解决了吗?const对象调用下面的const成员函数,普通对象调用上面的!完美!

但很显然我们忽略了++这个问题,但你可能会想:“那有什么难的?跟上面一样再重载一个不就成了吗?”

但++的逻辑我们回顾一下

 这个_pnode可是不能动的!没法重写。


所以综上所述:以上的实现方法,可以解引用,但是不能++,这还是有悖于我们的const迭代器没有实现对应功能的问题。

那么我们其实可以尝试多实现一个类,称之为_mylist_const_itreator,其中与当前的迭代器中唯一的区别就是解引用的返回值不同即可,其他的除去类名都不需要变换,当我们需要使用const迭代器的时候使用_mylist_const_itreator这个类名替代即可,不仅能++还能解引用。

	template<class T>
	struct _list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;

		_list_const_iterator(node* p)
			:_pnode(p)
		{}

		const T& operator*()
		{
			return _pnode->_data;
		}

		_list_const_iterator<T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		_list_const_iterator<T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const _list_const_iterator<T>& it)
		{
			return _pnode != it._pnode;
		}
	};

但是这样冗杂的实现方法虽然说可以成立,但是对于STL的实现大佬来说这肯定是不合格的,那么库里是如何实现的?

我们先回顾一个概念:不同的模板参数,会生成不同的类

举例,对于vector而言:

vector<int>
vector<string>
vector<vector<int>>

如上的三种不同的模板参数,生成了3个不同的类。

那么大佬则是利用了这个特点,不同的模板参数,同一个类型,生成不同的对象

	template <class T,class Ref>
	struct _list_iterator

先额外加一个模板参数,Ref是reference(引用)的英文。你可能会觉得奇怪,多加一个模板参数可以改变现状吗?答案是完全可以,而且非常巧妙,只需要加上以下的语句就可以了。

typedef _list_iterator<T,T&> iterator;
typedef _list_iterator<T, const T&> const_iterator;
  • 我们分析这段语句就可以发现其巧妙之处,借助不同的模板参数生成不同的类这个特质,相当于以一个类特化生成了两个不同的类,当我们使用const迭代器的时候,const T&会直接被模板参数套用生成一个const版本的迭代器,我们只需要再完善迭代器类内部的名称就可以实现这种特化了。

在额外的添加了一个模板参数之后,_list_iterator<T>&这个语句段的模板参数里面也需要额外加上一个Ref,因为之后还需要添加一个模板参数,我们直接typedef一下迭代器来方便更改。

	template <class T,class Ref>
	struct _list_iterator
	{
		typedef List_node<T> Node;
		typedef _list_iterator<T,Ref> Self;


		//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;

		//然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行
		_list_iterator( Node* p)
			:_pnode(p)
			{}


		Ref operator*()
		{
			return(_pnode->_data);
		}

		Self& operator ++()
		{
			_pnode = _pnode->_next;
			return *this;

		}

		Self& operator --()
		{
			_pnode = _pnode->_prev;
			return *this;

		}

		bool operator != (const Self& it)
		{
			return _pnode != it._pnode;
		}


	};

赋值操作符重载优化

 前文所记述的赋值操作符重载是“传统写法”

		//赋值操作符重载
		mylist<T>& operator = ( mylist<T>& l2)
		{
			if (this != &l2)
			{
				clear();

				for (auto& e : l2)
				{
					push_back(e);
				}
			}
			return *this;
		}

 为了实现“现代写法”也就是“摇人打工法”我们需要自己先实现list自己的成员函数swap

swap

swap的逻辑非常简单,不必一个个的交换节点,既然我们有哨兵位头节点,我们直接交换他俩即可完成交换

		void swap(mylist<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

为什么不直接使用算法库内部的swap来直接交换两个链表?

因为算法库的算法消耗还是比较大的,毕竟为了适配所有类型的交换,消耗远大于我们自己单纯的交换两个指针。

那么我们的赋值操作符重载就非常简单了,注意需要使用传值传参,触发拷贝构造


		mylist<T>& operator = ( mylist<T> tmp)
		{
			swap(tmp);
			return *this;
		}

那为什么拷贝构造不使用“现代写法”?

我们在实现vector的现代拷贝构造的时候,为了防止当前的this指针交换过去的时候析构一个没有初始化过的指针,我们会给予一个空指针来防止此事的发生,那么套用到list这上面反而就不行了,因为我们在迭代器里面还是需要访问到当前头节点的,也就是_head不可以为空。还是需要初始化,那么跟我们的传统写法差比不大,实现也可没有也罢。

size

size的实现非常简单,我们直接复用就好了,在mylist的成员变量之中加入一个size,由于我们复用了insert和erase来构筑了mylist的增删查改,我们只需要在触发了insert和erase的时候对size++或者--就可以了。

		size_t size()
		{
			return _size;
		}

不过由于复用了push_back一类函数在拷贝构造以及赋值操作符重载内部,其中size的变化还需要额外处理,也就是赋值时也需要更新被赋值的变量的_size。这一部分不做记述,逻辑简单也不复杂。

类型名问题

我们查阅官方文档的时候,可能会对赋值操作符的重载产生一定的疑惑

 啥玩意?list后面的模板参数怎么没了?

在解释这个问题之前我们再次回顾一边类和对象中类型的问题。

  • 普通类的类名 == 类型
  • 类模板的类名 !=  类型   而类模板<模板参数> == 类型

那么根据我们上面的回顾,这里应该是list<T>才对,怎么官方文档是这样子的呢?那么换成我们自己实现的试一试

  •  也是没有问题的,这里其实算是C++语言设计的一个陷阱,对于赋值操作符重载来说,类模板的名称也被归为了类型,这其实并不符合我们的使用习惯,但是从语法角度来讲是合理的。

不过平常我们能不用就不用,毕竟还是容易造成歧义。


->重载问题

我们创建一个自定义类型来尝试我们的list能否存储和读取

	struct Location
	{
		Location(int x = 0,int y = 0)
			:_x(x),_y(y)
		{}


		int _x;
		int _y;
	};

当我们想要遍历访问这个类的时候,由于我们没有重载这个类的流插入运算符,将会报错

 但是重载一个流插入未免有些麻烦,Location的成员变量是内置类型,也是一个结构体,那么我们简单一点直接取出来访问就好,那么一个结构体访问其中的成员变量非常简单,使用->是我们常用的手段。

  • 但是->一般是用于结构体指针的,我们还没有重载,但是p->data等价于(*p).data,所以使用(*it)._x这个语句是可行的。

 不过这样子的可读性还是比较差的,所以我们还是需要重载->操作符以方便我们访问结构体。

  • 既然->作用于一个结构体指针,那么我们就直接把存放于当前节点的数据指针捞出来即可,那么我们在迭代器类里面重载一下,把它的地址捞出来放到指针里头去
		T* operator->()
		{
			return &_pnode->_data;
		}

那么根据我们的理解,整个过程应该是这样的:

it->返回的是当前节点里面的值的地址也就是T*,根据我们当前的程序,返回的应该是Location*,那么我们想要取得里面的数据就应该再加上一个->,因为原生的结构体指针已经提取出来了,使用->是可以的

为了验证我们的猜想,我们先上一个->试一试,此时应该提取出的是当前值的指针

 怎么回事?不应该是指针吗,怎么直接提取出了其中的值?

原因则是:在这段过程中,如果按照我们理论上的语法来实现,应该写成it->->_x 才可以,但是这样子的写法可读性太低了,所以编译器自己优化了这个过程,实际上确实是使用了两次->操作符,但是我们写一次即可

那么这样就万事大吉了?我们不能忘记const对象的问题。

		T* operator->()
		{
			return &_pnode->_data;
		}

我们的确对普通对象的->进行了重载,但是当我们调用const迭代器的时候,这段代码可不会返回const T* 而是T* ,const对象随便更改这种情况绝对不是我们想看到的。

那么前文我们对模板的灵活运用在这里就可以再次发光发热了,我们多加上一个模板参数称之为Ptr


	template <class T,class Ref,class Ptr>
	struct _list_iterator
	{
		typedef List_node<T> Node;
		typedef _list_iterator<T,Ref,Ptr> Self;
    }
		typedef List_node<T> Node;
		typedef _list_iterator<T,T&,T*> iterator;
		typedef _list_iterator<T, const T&,const T*> const_iterator;
		Ptr operator->()
		{
			return &_pnode->_data;
		}

 这样const就成功的生效了。


到这,一个仿照STL具有基础功能的list就实现完毕了,希望对你有点帮助!

 

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

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

相关文章

使用BP神经网络诊断恶性乳腺癌(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 1.1.算法简介 BP&#xff08;Back Propagation&#xff09;网络是1986年由Rumelhart和McCelland为首的科学家小组提出&#xf…

c语言编程规范第三部分

3、头文件应向稳定的方向包含 头文件的包含关系是一种依赖&#xff0c;一般来说&#xff0c;应当让不稳定的模块依赖稳定的模块&#xff0c;从而当不稳定的模块发生变化时&#xff0c;不会影响&#xff08;编译&#xff09;稳定的模块。就我们的产品来说&#xff0c;依赖的方向…

数据复制 软件 SnapMirror:统一复制,更快恢复

数据复制 软件 SnapMirror&#xff1a;统一复制&#xff0c;更快恢复 预测未知领域是一项棘手的工作。让 SnapMirror 软件来处理则轻松得多。 通过数据的高可用性和快速数据复制&#xff0c;可即时访问业务关键型数据。放松一下&#xff0c;它会让你满意的。 为什么用 SnapMi…

3D目标检测(一)—— 基于Point-Based方法的PointNet系列

3D目标检测&#xff08;一&#xff09;—— PointNet&#xff0c;PointNet&#xff0c;PointNeXt&#xff0c; PointMLP 目录 3D目标检测&#xff08;一&#xff09;—— PointNet&#xff0c;PointNet&#xff0c;PointNeXt&#xff0c; PointMLP 前言 零、网络使用算法 …

AQS与Synchronized异曲同工的加锁流程

在并发多线程的情况下&#xff0c;为了保证数据安全性&#xff0c;一般我们会对数据进行加锁&#xff0c;通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现&#xff0c;而ReentrantLock是基于Java代码层面实现的&#xff0c;底层是继承的AQS。 AQS全称…

c++函数对象(仿函数)、谓词、内建函数对象

1、函数对象 1.1 概念 重载函数调用操作符的类&#xff0c;这个类的对象就是函数对象&#xff0c;在使用这个函数对象对应使用重载的&#xff08;&#xff09;符号时&#xff0c;行为类似于函数调用&#xff0c;因此这个函数也叫仿函数。 注意&#xff1a;函数对象&#xff0…

多个任务并行的时候,你是否总是会手忙脚乱?

很多重要事情之所以变得迫在眉睫&#xff0c;需要立刻处理、应付&#xff0c;是因为被延误或没有进行足够的预防和准备&#xff0c;筹划。 面对多个任务并行的时候&#xff0c;你是否总是会手忙脚乱&#xff1f; 在项目工作中&#xff0c;管理者每天要面对各种工作&#xff…

移动WEB开发二、流式布局

零、文章目录 文章地址 个人博客-CSDN地址&#xff1a;https://blog.csdn.net/liyou123456789个人博客-GiteePages&#xff1a;https://bluecusliyou.gitee.io/techlearn 代码仓库地址 Gitee&#xff1a;https://gitee.com/bluecusliyou/TechLearnGithub&#xff1a;https:…

【Linux】线程函数和线程同步详细整理(金针菇般细)

目录 一&#xff0c;线程函数 1.获取当前线程ID 2.创建线程 3.退出线程 4.阻塞线程 5.分离线程 6.取消线程 7.线程比较 8.测试代码&#xff08;线程函数总结&#xff09; 二&#xff0c;线程同步 1.互斥锁 2.读写锁 3.条件变量 4.信号量 一&#xff0c;线程函数 …

【阿旭机器学习实战】【29】产品广告投放实战案例---线性回归

【阿旭机器学习实战】系列文章主要介绍机器学习的各种算法模型及其实战案例&#xff0c;欢迎点赞&#xff0c;关注共同学习交流。 目录问题描述数据处理过程及源码通过数据可视化分析数据训练线性回归模型可视化训练好的线性回归模型结果预测问题描述 你所在的公司在电视上做产…

mybatis狂神(附自学过程中疑问解决)

首先先附上mybatis的官方文本链接mybatis – MyBatis 3 | 简介一、Mybatis介绍MyBatis 是一款优秀的持久层框架&#xff0c;它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来…

Comparator和Comparable的区别以及Collections.sort排序原理

一、概述 Comparable和Comparator都是两个接口&#xff0c;接口都可以用来实现集合中元素的比较、排序&#xff0c;Comparator位于包java.util下&#xff0c;而Comparable位于包java.lang下&#xff0c;Comparable接口将比较代码嵌入自身类中&#xff0c;而Comparator既可以嵌…

非标题党:前端Vue React 项目编程规范化配置(大厂规范)

前端项目编程规范化配置 下述例子主要是从 代码规范化 以及 git 提交规范化 两方面进行配置。内容很多&#xff0c;请做好心理准备 一、代码检测工具 ESLint 在我们通过 vue create “项目名” 时&#xff0c;我们可以通过手动配置的方式&#xff0c;来配置 ESLint 来对代码进…

QDateTime的11种显示方式

QDateTime datetime QDateTime::currentDateTime(); datetime.toString(“hh:mm:ss\nyyyy/MM/dd”); datetime.toString(“hh:mm:ss ap\nyyyy/MM/dd”); datetime.toString(“hh:mm:ss\nyyyy-MM-dd”); datetime.toString(“hh:mm:ss ap\nyyyy-MM-dd”); datetime.to…

【分享】订阅用友YonSuite集简云连接器同步销售出库数据至用友YonSuite

方案场景 在企业中因多种系统孤立导致数据割裂&#xff0c;是现企业中现阶段面临的最大问题&#xff0c;而钉钉作为常用的OA审批系统&#xff0c;用友YonSuite作为ERP系统&#xff0c;原方式钉钉内完成审批再由人工将数据同步到用友YonSuite系统&#xff0c;数据同步过程中不仅…

将HTTP接口配置成HTTPS

一、使用Java的keytool.exe程序生成本机的TLS许可找到Java的jdk目录进入bin默认安装路径C:\Program Files\Java\jdk1.8.0_91\bin 进入命令面板&#xff0c;在bin的路径栏中输入cmd敲击回车即可使用keytoolkeytool -genkeypair -alias tomcat_https -keypass 123456 -keyalg RSA…

linux线程的基本知识

这里用的是Linux的pthread线程库&#xff0c;需要加pthread线程库。 线程的创建 第一个参数是线程id的地址。第二个参数是线程属性&#xff0c;一般为NULL。第三个是要执行的函数。第四个是函数的参数&#xff0c;一般也为NULL 线程的等待&#xff0c;第一个参数是线程的id,第…

VBA提高篇_27 OptionBOX_CheckBox_Frame_Image_VBA附加控件

文章目录1.单选按钮OptionBOX:2.复选框CheckBox:3.框架Frame:4.图像Image: (loadPictrue)5. VBA附加控件:6. 适用于很多控件的重要属性:1.单选按钮OptionBOX: 默认时,同一窗体的所有单选按钮均属于同一组,只能选中一个 可通过Frame控件进行分组解决. 2.复选框CheckBox: 一次可以…

备考软考系统分析师-1

系统分析师教程网盘资源&#xff1a;链接: https://pan.baidu.com/s/1ekHuCJJ3o5RrW1xeMkxhdA 提取码: 6666 信息系统战略规划 信息系统开发方法&#xff1a; 结构化法 瀑布模型 原型法 自顶向下 用于需求阶段较多 面向对象 自底向上 面向服务的方法 系统建模 政府信息…

在Excel接入 chatgtp (图文教学)

效果图 话不多说&#xff0c;开始教学 首先点击插入&#xff0c;然后点击获取加载项 office Excel 加载加载项时出错 解决办法_long_songs的博客-CSDN博客今天在添加维基百科的时候&#xff0c;怎么都添加不了&#xff0c;网上的办法都是关闭&#xff0c;重启&#xff0c;或者…