【c++】:模拟实现STL模板中的string

 

 

文章目录

  • 前言
  • 一.string的模拟实现
  • 总结

 


前言

上一篇文章我们详细介绍了STL中的string的一些常用的接口,这一篇文章我们将从底层实现string类,当然我们只是实现一些重要的,经常使用的接口,并且不是完全按照STL中的string去走的。


 

一、string的模拟实现

首先我们为了防止我们写的string类与库中的string产生命名冲突,所以我们将要实现的string写在我们自己的命名空间中,如下图:

514cb193f46e46c4a7cb4d88af571b4d.png

首先我们创建需要的变量,众所周知string是一个字符串类,所以底层我们就用char* str搞定,然后还需要capacity来查看字符串的容量,size就是字符串的长度了,如下图所示:

b1a73cbf7bae4207bdc854819134e8e8.png 接下来我们就需要搞定构造函数和析构函数了,构造函数我们要实现的功能有:可以直接用const char*类型构造一个字符串,如果是一个空串必须给string开一个空间用来存放\0,下面我们来实现一下:

namespace sxy
{

  class string
{
  public:
        string()
			:_str(new char[1])
			, _size(0)
			,_capacity(0)
		{
            _str[0] = '\0';
		}
		string(const char* str)
			:_size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

  private:
       char* _str;
       size_t _capacity;
       size_t _size;

}

}

 刚刚我们说到空串必须开一个空间用来放\0,所以我们在初始化列表直接开了1个char类型的空间,那么看到上面的代码会不会有一个疑问,我们完全可以直接用空串代表\0并且用缺省值的方式完成空串的实现,所以我们将这两个合二为一:

string(const char* str = "")
			:_size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

 这样我们就完成了构造函数的实现,我们先实现一下析构函数然后再测试:

~string()
		{
			delete[] _str;
			_str = nullptr;
			_capacity = _size = 0;
		}

 析构函数实现起来就非常简单了,我们只需要把给_str开的空间释放掉,这里必须要注意的是,我们开空间用的new []在释放空间的时候一定要用delete[] ,对于c++动态内存我们前面一篇文章当重点讲过,详细的说明了不匹配的危害。释放完空间后将指针置为空,最后再将capacity和size置为0即可。

8159b1205b46497da3b75e9d8a5fdcba.png

744c7f46586c49509345cee26998f42c.png74a46d70b9334961a85cceeda46d6cad.png

 可以看到空串确实有一个\0并且用字符串构造也是正常的,在s2中的capacity为4是因为后面实现reserve接口有一个小bug我们实现的时候再说。c_str是返回字符串类型,这样方便我们去打印因为我们还没有重载流输出操作符。

const char* c_str() const
		{
			return _str;
		}

接下来实现一个[]接口,因为这个接口分const成员和非const成员,所以我们要实现两个[]接口,如下图:

char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

 const的类型是为了防止数据被修改这里就不详细介绍了。我们之前说过[]与at的区别在于,[]会报错,at访问会抛异常,所以我们用assert断言下标小于_size.

接下来我们实现拷贝构造的接口:

string(const string& str)
			:_capacity(str._capacity)
			,_size(str._size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str._str);
		}

 拷贝构造的实现其实就是深拷贝问题,我们先用初始化列表将str的size和capacity给_str的size和capacity初始化,然后直接开和str的capacity一样的空间最后将数据拷贝到_str中即可。

接下来我们实现一下赋值重载:

string& operator=(const string& str)
		{
			if (this != &str)
			{
				char* tmp = new char[str._capacity + 1];
				strcpy(tmp, str._str);
				delete[] _str;
				_str = tmp;
				_capacity = str._capacity;
				_size = str._size;
			}
			return *this;
		}

赋值重载是对两个已经初始化过的对象进行使用的,这样就会有三种情况,如下图:

a8b60d27193641ba8ce68121b2c0d864.png

 第一种是str的空间远大于_str,第二种是str的空间远小于_str,第三种是两个空间相差不多。如果我们如上图细细划分去赋值会很麻烦,所以我们干脆直接开和用来赋值对象一样大小的空间(这里每次开空间我们都多开一个用来存放\0),当然由于开空间可能会失败,我们为了防止一旦开空间失败了原先字符串里的空间也没有了,所以我们先用开一个临时变量去开空间,然后将数据拷贝到临时变量中,然后再将原来的旧空间释放掉,让_str指向刚刚临时变量开的空间,再将用来赋值的变量的capacity和size给被赋值的变量,为了实现连续赋值所以我们返回*this,同时为了避免有人在使用的时候写错自己给自己赋值了,所以我们做一下判断只能不相同时才能成功赋值。

下面我们来实现一下迭代器,迭代器分为无const和const版本,分别是针对普通对象和const对象的。如下:

        typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}

首先typedef一下char*类型和const char*类型,然后实现begin(),begin就是指向首元素的指针所以返回数组名,数组名就是首元素地址。end我们前面说过,由于迭代器是左闭右开所以end只能指向\0的位置,而首元素的位置加上字符串长度size刚好指向\0.const迭代器也同理,只不过const迭代器不支持修改罢了。测试如下图:

void func(const string& s)
	{
		string::const_iterator it = s.begin();
		while (it != s.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}
	void test4()
	{
		string s("hello world");
		string::iterator it = s.begin();
		while (it != s.end())
		{
			(*it)++;
			cout << *it << " ";
			it++;
		}
		cout << endl;
		func(s);
	}

98199961a8e24f13a43634953cc2cd1d.png

 搞定迭代器后我们再去搞定字符串比较的运算符重载,这里就非常简单了,只需要用strcmp字符串比较即可,如下:

        bool operator==(const string& str) const
		{
			return strcmp(_str, str._str) == 0;
		}
		bool operator>(const string& str) const 
		{
			return strcmp(_str, str._str) > 0;
		}
		bool operator<(const string& str) const
		{
			return strcmp(_str, str._str) < 0;
		}
		bool operator!=(const string& str) const
		{
			return !(*this == str);
		}
		bool operator<=(const string& str) const
		{
			return !(*this > str);
		}
		bool operator>=(const string& str) const
		{
			return !(*this < str);
		}

strcmp这个函数是依次比较两个字符串的ascll值,当返回值大于0时,第一个字符串大于第二个字符串,当返回值等于0时,第一个字符串等于第二个字符串,当返回值小于0时,第一个字符串小于第二个字符串。建议:1.在我们写代码的时候,有些函数能复用就尽量去复用。2.对于不涉及修改的成员函数建议给函数加上const这样一来非const成员调用这个函数只是权限的缩小不会放大,const成员调用权限一致也可以调用。如果我们不加const,那么const成员调用函数的时候就无法调用,因为权限放大了。

接下来我们实现push_back接口,push_back本身实现起来并不复杂,但是每次尾插一个字符都要涉及到是否需要扩容,所以重点是实现扩容函数,如下图:

        void reserve(size_t n)
		{
            if (n>capacity)
			{
            char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
            } 
		}
		
		void push_back(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

是否扩容的条件很简单,我们在实现的时候每次都会多开一个空间用来放\0,所以当size+1>capacity的时候我们就扩容,并且扩为原来容量的两倍:

df7a04305629450a9f4bf5fbd220ca9e.png

我们在空间的时候还是延续之前的传统每次多开一个空间。看过c++内存管理的都知道c++是没有办法在已经有空间的基础上继续扩容的,所以我们只能和实现赋值运算符重载一样,我们先用一个临时变量开空间,然后将原先字符串的数据拷贝到新空间内,再将旧空间释放掉,然后让_str指向刚刚开好的新空间,因为reserve只改变容量所以我们让容量为n,这里为什么不是两倍的capacity呢?因为用户也要用reserve空间,reserve是根据用户的需求开空间的不是只能开2倍capacity。最后我们一定要加上一个判断条件,只有当n大于capacity的时候我们才进行扩容。这样做是因为c++中很少会去进行缩容,因为缩容是有缺点的,所以要避免缩容。push_back的原理我们在图上已经画出来了,当我们尾插一个字符后字符串长度加1就让size++,然后size的位置放上\0即可。

到了这里我们前面的那个关于capacity的小bug就可以解释了,我们发现当一个字符串为空串时,这个时候size为0,capacity也是0,然后我们尾插一个字符,0+1>0就进去扩容了,扩容为2倍capacity但是capacity还是0所以开的空间为0,这个时候放入数据字符串就为随机值,所以我们在构造函数的时候判断当size为0的时候capacity给个初始值4,只有当size不为0再将size给capacity,如下:

        string(const char* str = "")
			:_size(strlen(str))
		{
			_capacity = _size == 0 ? 4 : _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

这也就解释了为什么我前面演示空串的时候capacity为4.

接下来我们实现append接口:

        void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		}

为了大家理解的更清楚我们画个图:

02ea7648923f4f07a3519a39e6be5ece.png

 所以我们第一步是计算要插入的字符串的长度,然后判断是否需要重新开空间,有足够的空间后我们将要插入的字符串拷贝到指定的位置,通过上图我们可以看到此位置是_str+size,最后不要忘记了将len加给size。

下面我们通过push_back和append接口去复用运算符重载中的+=接口:

        string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

下面我们来实现resize这个接口:

resize的功能有:开空间并且初始化,如果所开空间小于capacity,就将原来多出来的数据删除,也就是说resize会改变size和capacity:

        void resize(size_t n, char ch = '\0')
		{
			if (n < _capacity)
			{
				_str[n] = '\0';
				_size = n;
			}
			else if (n > _capacity)
			{
				reserve(n);
				size_t pos = _size;
				while (pos < n)
				{
					_str[pos++] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

 当所开空间小于capacity的时候我们直接在n这个位置放入\0,这样就相当于删除这个字符串n后面的字符了,然后我们将size置为n。这里是不需要真的将后面的字符删除的,因为析构函数会自己释放我们所开的空间。当要开的空间大于容量时,我们就用reserve开n个大小的空间,用一个变量去记录刚刚字符串\0的位置从这个位置到n依次放入我们要初始化的字符,这里用了缺省值如果我们不输入指定初始化的字符那么就用\0。到最后记得将size置为n并且把\0放到size的位置。

下面实现insert接口:

        string& insert(size_t pos, char c)
		{
			assert(pos >= 0 && pos <= _size);
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end-1];
				end--;
			}
			_str[pos] = c;
			_size++;
			return *this;
		}

 插入一个字符同样要判断是否扩容,只是需要注意的是下面这样:

1e9c9ade997b48dd86d6145d4de10133.png 

 如果按照上图中这样实现的话是有问题的,因为我们的end变量是size_t类型,是大于等于0的,当我们要插入的位置是0时,end>=pos这个条件永远会使循环永远不会停止,所以为了避免这个问题我们用str[end] = str[end-1]来实现,如下图:

84b7dbbe9d9047169d968e92df700c1d.png

这样实现的好处是循环结束条件是end>pos,只要不是等于就不会出现上面我们说的那种情况,将字符插入后记得将size++,并且我们可以让别人用这个接口可以接收到插入后的字符串所以返回*this.

下面我们实现插入一个字符串:

        string& insert(size_t pos, const char* str)
		{
			assert(pos >= 0 && pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end >= pos + len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			memcpy(_str + pos, str, sizeof(char) * len);
			_size += len;
			return *this;
		}

 思想与我们插入一个字符一样,如下图:

6a5ea563d80d4aa6acab81a19837cdd1.png

 在这里一定要看好循环结束的条件,当end==pos+len的时候还有最后一个字符没移动所以循环不能结束,在这里我们就不能再用strcpy了,因为strcpy会拷贝字符串结尾的\0,所以我们必须用能按字节拷贝的函数,这里我用了memcpy,大小就是sizeof(char)*len。实现了插入接口我们就可以用插入复用push_back和append了,如下图:

        void push_back(char ch)
		{
			insert(_size, ch);
			_str[_size] = '\0';
		}
        void append(const char* str)
		{
			insert(_size, str);
		}

接下来我们实现find接口:

        size_t find(char ch, size_t pos = 0)
		{
			for (int i = 0; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}

 我们上一篇中看了库中find函数的实现,当找不到字符时会返回npos,现在我们先去定义一个npos.如下图:

18cf843b754a49fba6d86ea844a2a5e3.png

 因为npos这个变量是string类中公共的并不是每个对象私有的,所以我们定义为静态变量,静态变量的初始化必须在类外初始化,然后我们将npos初始化为-1,在这里一定要加域名限定符,不然就新定义了一个npos。find接口的实现很简单,依次去遍历只要遇到要查找的字符就停下返回下标,如果找不到就返回npos。

下面实现查找一个子串的接口:

        size_t find(const char* str, size_t pos = 0)
		{
			char* p = strstr(_str, str);
			if (p == NULL)
			{
				return npos;
			}
			else
			{
				return p - _str;
			}
		}

查找一个子串我们用strstr函数即可,如果忘了我们可以看一下strstr的说明:

0033d514e19f4ef591b8eb532fcfc17b.png

strstr的第一个参数是要查找子串的字符串,第二个参数是要查找的子串,比如在"hello world"中查找world,那么就会返回w的下标,如果没找到返回空指针。当返回空指针说明找不到子串,那么就返回npos即可,返回值为size_t类型是无法返回指针的,这个时候我们想到指针-指针就是指针之间的元素个数,如下图:

d95d3084d0d0487c9c9ad59dc57b3d14.png

两个指针之间的元素为4个,而4正好是查找到的子串的第一个字符的下标,返回即可。 

下面我们再来实现一些简单的接口,比如返回size,返回capacity。

        size_t size() const
		{
			return _size;
		}
        size_t capacity() const
		{
			return _capacity;
		}

clear这个接口是清空字符串,这个接口实现很简单直接在_str[0]的位置放入一个\0,然后将size置为0即可。

        void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

 empty接口是判断字符串是否为空,如下:

        bool empty() const
		{
			return _size == 0;
		}

接下来我们在实现一个swap接口,直接用库函数即可:

        void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_capacity, s._capacity);
			std::swap(_size, s._size);
		}

string中的交换是非常简单的,只需要将两个字符串的指针指向互换,再将capacity和size互换即可,如果我们直接用一个swap去交换两个字符串就会发现效率非常低,因为tmp会调一次拷贝构造,剩下的两个变量也会调用拷贝构造也就是三次拷贝构造。

接下来我们在实现一个erase接口:

        string& erase(size_t pos, size_t len = npos)
		{
			assert(pos >= 0&&pos<_size);
			if (len==npos||pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

 删除从某个位置起的len个字符,如果没有给len,那么len默认是npos也就是整形的最大值意思就是说将pos位置后的全部字符都删除,再使用前我们断言一下pos指针只能大于等于0并且小于size。这里分两种情况,当pos+len大于等于_size的时候我们就将pos位置后面的数全部删除,并且把size置为pos,在这里为什么条件是(len==npos||pos+len>=size)呢,是因为如果我们不写len==npos这个条件的话一旦有人没有给len,那么len就是整形的最大值加上pos就溢出了。当pos+len小于size就说明要删除的字符是在范围内的,在这里我们也不用将数据挨个去移动,只需要将后面的字符拷贝到str+pos位置,然后让size将要删除的len个字符减掉。如下图:

6c932a3ade35442080d83b0a91536998.png

8418589a5a194b1fa2199922b54902ed.png 接下来我们实现流插入函数:

对于流插入的函数重载,我们在Date类的时候就说过要将流插入的实现放到类外,否则第一个参数是*this无法直接cout<<打印,cout打印string和C_str是不一样的,c_str是按照字符串进行打印,遇到\0就停止,而流插入是按照size进行打印的,也就是说不管你字符串里面有没有\0,所以实现如下图:

    ostream& operator<<(ostream& out, const string& str)
	{
		for (int i = 0; i < str.size(); i++)
		{
			out << str[i];
		}
		return out;
	}

 我们直接用for循环依次去打印str中的每个字符即可,最后记得返回out即可。

流插入的实现很简单,下面我们来实现流提取:

    istream& operator>>(istream& in, string& str)
	{
		str.clear();
		char ch = in.get();
		char buf[128];
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buf[i++] = ch;
			if (i == 127)
			{
				buf[i] = '\0';
				str += buf;
				i = 0;
			}
			ch = in.get();
		}
		if (i != 0)
		{
			buf[i] = '\0';
			str += buf;
		}
		return in;
	}

首先如果原先的string中有数据的话我们要将原来的数据清除,如果单单用in的话我们发现无法结束,因为流提取是从缓冲区中拿字符,而空格和换行是不会进入缓冲区的,因为输入多个字符而中间的空格和换行是区分字符的间隔,由于C语言和c++的缓冲区不一样,c中的是getchar,c++中有get和getline,getline是遇到换行符才停,而get函数则是遇到空格和换行停。如下图:

3bc0fb74e8c44fdeab2ce7efd2a1a70b.png

 由于我们不确定用户输入的字符串是多少,如果字符串很短就需要频繁的开空间释放空间,这一定会让我们的效率大幅度下降,所以我们直接开一个数组用来存放较短的字符串,定义一个变量i用来访问数组中的字符,只要数组没有满我们就将读取到的字符放入数组中,由于数组中要留一个空间放\0所以在最后一个位置停下放入\0并且将字符串给string,让i重置为0,循环中需要连续读取字符所以要写ch = in.get().如果数组没有满就遇到空格或者换行符,我们就在数组中i的位置放入\0,然后把字符串加到string中去最后返回in即可。

到这里我们就将string中常用的接口全都实现了一遍,这样也能加深我们对string的理解并且可以复习到我们学习的c++6个默认函数。


 

总结

string由于c++历史原因很多接口都是功能相近的,一共一百多个接口显得太冗余,通过我们的模拟实现string能加深我们对string中常用接口的认识,并且在使用的过程中也能更加游刃有余,下一篇我们将讲解STL中vector的常用接口。

 

 

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

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

相关文章

系统重装漏洞

zzcms系统重装漏洞 一、配置zzcms环境 1. 使用小皮搭建zzcms框架 2. 安装zzcms 按照下面的操作进行,傻瓜式操作即可 3. 打开网站 二、漏洞利用 在访问install目录的默认文件后,会出现zzcms安装向导 http://www.zzcms.com/install/index.php 但是会显示 “安装向导…

【项目实现典型案例】12.数据库数据类型不一致导致查询慢

目录一&#xff1a;背景介绍二&#xff1a;索引失效复现四&#xff1a;索引实现的六种情况1、类型转换&#xff0c;函数2、ISNULL3、通配符开头4、范围查询5、组合索引&#xff0c;不符合最左匹配原则6、WHERE子句中的OR四&#xff1a;总结一&#xff1a;背景介绍 MySql数据库…

Visual Studio 2022 安装.NET Framework4.5及以下目标包

Visual Studio 2022不再支持.NET4.5&#xff0c;如果打开.Net4.8目标包或.NET 4.8以下的.NET版本项目时&#xff0c;会提示不再支持。 即使在Visual Studio Installer中也找不到.NET4.5及以下的选项。 那么去官网下载.Net 4.5呢&#xff1f;很抱歉&#xff0c;安装时提示 &am…

利用蜜罐捕捉攻击实验(31)

预备知识 1、蜜罐的含义和作用 蜜罐(Honeypot)是一种在互联网上运行的计算机系统。它是专门为吸引并诱骗那些试图非法闯入他人计算机系统的人(如电脑黑客)而设计的&#xff0c;蜜罐系统是一个包含漏洞的诱骗系统&#xff0c;它通过模拟一个或多个易受攻击的主机&#xff…

初入了解——什么是VUE

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

在 4G 内存的机器上,申请 8G 内存会怎么样?

在 4GB 物理内存的机器上&#xff0c;申请 8G 内存会怎么样&#xff1f; 这个问题在没有前置条件下&#xff0c;就说出答案就是耍流氓。这个问题要考虑三个前置条件&#xff1a; 操作系统是 32 位的&#xff0c;还是 64 位的&#xff1f;申请完 8G 内存后会不会被使用&#x…

想要成为高级网络工程师,只需要具备这几点

首先&#xff0c;成为高级网络工程师的目的&#xff0c;就是为了搞钱。高级网络工程师肯定是不缺钱的&#xff0c;但成为高级网络工程师你一定要具备以下几点&#xff1a;第一 心态作为一个高级网工&#xff0c;首先你必须情绪要稳定&#xff0c;在碰到重大故障的时候不慌&…

C语言函数调用栈

栈溢出&#xff08;stack overflow&#xff09;是最常见的二进制漏洞&#xff0c;在介绍栈溢出之前&#xff0c;我们首先需要了解函数调用栈。 函数调用栈是一块连续的用来保存函数运行状态的内存区域&#xff0c;调用函数&#xff08;caller&#xff09;和被调用函数&#xf…

论文阅读:NeRF Representing Scenes as Neural Radiance Fields for View Synthesis

论文阅读–NeRF Representing Scenes as Neural Radiance Fields for View Synthesis 这是 2020 ECCV 的一篇文章&#xff0c;记得好像还获得了最佳论文奖的提名&#xff0c;这篇文章相当于将自由视点生成这个方向开辟出了一个新的解决思路。 文章的作者们提出了一种可以对复…

23.3.14打卡 2022年江西省大学生程序设计竞赛(正式赛)ABL

就写了签到, 其他题没写, 这场好像3题就银了 纪念一下3.14原粥率日 比赛链接:https://ac.nowcoder.com/acm/contest/43898 A题 Special Adjustment Method 题意 给出非负整数x, y, z 你可以让其中两个数字-1, 另外一个2, 使得x2y2z2x^2y^{2}z^{2}x2y2z2最大 题解 这题很容…

Maven的安装与配置

目录 一、安装本地Maven下载 Maven官网下载 下载完成后&#xff0c;选择一个路径进行解压 配置path环境变量 验证安装是否成功 二、配置settings文件 1. 修改mirror 2. 修改jdk 一、安装本地Maven下载 Maven官网下载 下载完成后&#xff0c;选择一个路径进行解压 记住…

断崖式难度的春招,可以get这些点

前言 大家好&#xff0c;我是bigsai&#xff0c;好久不见&#xff0c;甚是想念。 开学就等评审结果&#xff0c;还好擦边过了&#xff0c;上周答辩完整理材料&#xff0c;还好都过了(终于可以顺利毕业了)&#xff0c;然后后面就是一直安享学生时代的晚年。 最近金三银四黄金…

Redis7之事务(五)

五 Redis 事务 5.1 介绍 可以一次执行多个命令&#xff0c;本质是一组命令的集合。一个事务中的所有命令都会序列化&#xff0c; 按顺序地串行化执行而不会被其他命令插入&#xff0c;不许加塞一个队列中&#xff0c;一次性、顺序性、排他性的执行一系列命令 5.2 Redis事务和…

Golang每日一练(leetDay0005)

目录 13. 罗马数字转整数 Roman to Integer ★ 14. 最长公共前缀 Longest Common Prefix ★ 15. 三数之和 3Sum ★★★ &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 13. 罗马数字转…

Windows逆向安全(一)之基础知识(二)

反汇编分析C语言 空函数反汇编 #include "stdafx.h"//空函数 void function(){}int main(int argc, char* argv[]) {//调用空函数function();return 0; }我们通过反汇编来分析这段空函数 函数外部 12: function(); 00401048 call ILT5(func…

「Vue面试题」动态给vue的data添加一个新的属性时会发生什么?怎样去解决的?

一、直接添加属性的问题 我们从一个例子开始 定义一个p标签&#xff0c;通过v-for指令进行遍历 然后给botton标签绑定点击事件&#xff0c;我们预期点击按钮时&#xff0c;数据新增一个属性&#xff0c;界面也 新增一行 <p v-for"(value,key) in item" :key&q…

【微信小程序】-- 网络数据请求(十九)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

嵌入式软件开发之Linux 用户权限管理

目录 Ubuntu 用户系统 权限管理 权限管理命令 权限修改命令 chmod 文件归属者修改命令 chown Ubuntu 用户系统 Ubuntu 是一个多用户系统&#xff0c;我们可以给不同的使用者创建不同的用户账号&#xff0c;每个用户使用各自的账号登陆&#xff0c;使用用户账号的目的一是方便…

java-正装照换底色小demo-技术分享

文章目录前言java-正装照换底色小demo-技术分享01 实现思路02 效果02::01 原图:02::02 执行单元测试:02::03 效果:03 编码实现前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞…

用Python Flask为女朋友做一个简单的网站(附可运行的源码)

&#x1f31f;所属专栏&#xff1a;献给榕榕&#x1f414;作者简介&#xff1a;rchjr——五带信管菜只因一枚&#x1f62e;前言&#xff1a;该专栏系为女友准备的&#xff0c;里面会不定时发一些讨好她的技术作品&#xff0c;感兴趣的小伙伴可以关注一下~&#x1f449;文章简介…
最新文章