【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.mfbz.cn/a/228.html

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

相关文章

对于从事芯片行业的人来说,有哪些知识是需要储备的?

近两年芯片行业大火&#xff0c;不少同学想要转行&#xff0c;却不知道该如何下手&#xff0c;需要学习哪些基础知识&#xff0c;下面就来看看资深工程师怎么说&#xff1f; 随着工艺的发展&#xff0c;芯片肯定是尺寸越来越小&#xff0c;至于小到什么样的程度是极限&#xf…

【小破站下载工具】Python tkinter 实现网站下载工具,所有数据一键获取

目录前言开发环境本次项目案例步骤先展示下完成品的效果界面导入模块先创建个窗口功能按键主要功能代码编写功能一功能二功能三前言 最近很多同学想问我&#xff0c;怎么把几个代码的功能集合到一起&#xff1f; 很简单&#xff0c;写一个界面就行了&#xff0c;想要哪个代码…

CSS的三大特性

&#x1f31f;所属专栏&#xff1a;前端只因变凤凰之路&#x1f414;作者简介&#xff1a;rchjr——五带信管菜只因一枚&#x1f62e;前言&#xff1a;该系列将持续更新前端的相关学习笔记&#xff0c;欢迎和我一样的小白订阅&#xff0c;一起学习共同进步~&#x1f449;文章简…

用ChatGPT生成Excel公式,太方便了

ChatGPT 自去年 11 月 30 日 OpenAI 重磅推出以来&#xff0c;这款 AI 聊天机器人迅速成为 AI 界的「当红炸子鸡」。一经发布&#xff0c;不少网友更是痴迷到通宵熬夜和它对话聊天&#xff0c;就为了探究 ChatGPT 的应用天花板在哪里&#xff0c;经过试探不少人发现&#xff0c…

vue3+vite项目移动端适配:postcss-pxtorem和amfe-flexible

一&#xff0c;定义 postcss-pxtorem PostCSS 的一个插件&#xff0c;可以从像素单位生成 rem 单位。 amfe-flexible amfe-flexible是配置可伸缩布局方案&#xff0c;主要是将1rem设为viewWidth/10。 二&#xff0c;使用 1. 设置 viewport 在 index.html 中&#xff1a; &l…

学生信息表

目录 一、功能说明 二、核心思想 三、所用知识回顾 四、基本框架 五、js功能实现部分 一、功能说明 &#xff08;1&#xff09;输入对应的信息&#xff0c;点击录入可以为下面的表格添加一条记录&#xff0c;注意当所填信息不完整时不允许进行提交。 &#xff08;2&…

UE实现建筑生长(材质遮罩方式)效果

文章目录 1.实现目标2.实现过程2.1 遮罩2.2 生长动画3.参考资料1.实现目标 在UE中实现建筑的生成动画效果,GIF动图如下: 2.实现过程 通过动态设置材质遮罩OpacityMask的参数,即通过材质方式来实现建筑生长效果 2.1 遮罩 现有的教程中大多通过BoxMask-3D材质节点实现,但是…

扫地机器人(蓝桥杯C/C++)

题目描述 小明公司的办公区有一条长长的走廊&#xff0c;由 NN 个方格区域组成&#xff0c;如下图所示。 走廊内部署了 KK 台扫地机器人&#xff0c;其中第 ii 台在第 A_iAi​ 个方格区域中。已知扫地机器人每分钟可以移动到左右相邻的方格中&#xff0c;并将该区域清扫干净。…

Linux信号

目录 信号入门 1. 生活角度的信号 2. 技术应用角度的信号 3. 注意 4. 信号概念 5. 用kill -l命令可以察看系统定义的信号列表 6. 信号处理常见方式概览 产生信号 1. 通过终端按键产生信号 2. 调用系统函数向进程发信号 3. 由软件条件产生信号 4. 硬件异常产生信号 核…

DHCP原理简析及交互实践

环境&#xff1a; os&#xff1a;centos7 dnsmasq&#xff1a;version 2.76 一. dhcp工作原理 首先补充几个dhcp相关的基本概念&#xff1a; 1、动态主机配置协议DHCP&#xff08;Dynamic Host Configuration Protocol&#xff09;是一种网络管理协议&#xff0c;用于集中对用…

程序员必会技能—— 使用日志

目录 1、为什么要使用日志 2、自定义日志打印 2.1、在程序中得到日志对象 2.2、使用日志对象打印日志 2.3、日志格式 3、日志的级别 3.1、日志级别的分类 3.2、日志级别的设置 4、持久化日志 5、更简单的日志输出——lombok 5.1、如何在已经创建好的SpringBoot项目中添加…

Python+ChatGPT实战之进行游戏运营数据分析

文章目录一、数据二、目标三、解决方案1. DAU2. 用户等级分布3. 付费率4. 收入情况5. 付费用户的ARPU最近ChatGPT蛮火的&#xff0c;今天试着让ta写了一篇数据分析实战案例&#xff0c;大家来评价一下&#xff01;一、数据 您的团队已经为您提供了一些游戏数据&#xff0c;包括…

MySQL数据库的基础语法总结(1)

MySql一.数据库,数据表的基本操作1.数据库的基本操作2. 数据表的基本操作2.1 数据库的数据类型2.1.1 整数类型2.1.2 浮点数类型和定点数类型2.1.3 字符串类型2.1.4 日期与时间类型2.2 数据表的基本操作2.2.1 创建一个数据表2.2.2 查看数据表2.2.3 查看表的基本信息的MySQL指令2…

【拳打蓝桥杯】最基础的数组你真的掌握了吗?

文章目录一&#xff1a;数组理论基础二&#xff1a;数组这种数据结构的优点和缺点是什么&#xff1f;三&#xff1a;数组是如何实现随机访问的呢&#xff1f;四&#xff1a;低效的“插入”和“删除”原因在哪里&#xff1f;五&#xff1a;实战解题1. 移除元素暴力解法双指针法2…

前端开发神器VS Code安装教程

✅作者简介&#xff1a;CSDN一位小博主&#xff0c;正在学习前端 &#x1f4c3;个人主页&#xff1a;白月光777的CSDN博客 &#x1f4ac;个人格言&#xff1a;但行好事&#xff0c;莫问前程 安装VS CodeVS Code简介VS Code安装VS Code汉化结束语&#x1f4a1;&#x1f4a1;&…

嵌入式学习笔记——STM32的USART收发字符串及串口中断

USART收发字符串及串口中断前言字符串的收发发送一个字符串接收字符串需求利用串口实现printf中断中断是什么前言 上一篇中&#xff0c;介绍了串口收发相关的寄存器&#xff0c;通过代码实现了一个字节的收发&#xff0c;本文接着上面的内容&#xff0c;通过功能函数实现字符串…

Elasticsearch:集群管理

在今天的文章中&#xff0c;我们应该学习如何管理我们的集群。 备份和分片分配是我们应该能够执行的基本任务。 分片分配过滤 Elasticsearch 将索引配到一个或多个分片中&#xff0c;我们可以将这些分片保存在特定的集群节点中。 例如&#xff0c;假设你有多个数据集群节点&am…

项目实战-瑞吉外卖day02(B站)持续更新

瑞吉外卖-Day02课程内容完善登录功能新增员工员工信息分页查询启用/禁用员工账号编辑员工信息分析前端页面效果是如何实现的为什么点击左边 右边会根着变化首先 我们先来看一下菜单是如何展示出来的 在来看一下 为啥点击菜单时 右边会跟着变第一 &#xff1a;菜单是如何展示出来…

【剑指offer-C++】JZ32:从上往下打印二叉树

题目描述 描述&#xff1a;不分行从上往下打印出二叉树的每个节点&#xff0c;同层节点从左至右打印。例如输入{8,6,10,#,#,2,1}&#xff0c;如以下图中的示例二叉树&#xff0c;则依次打印8,6,10,2,1(空节点不打印&#xff0c;跳过)&#xff0c;请你将打印的结果存放到一个数…

我用Python写了一个下载网站所有内容的软件,可见即可下,室友表示非常好用

Python 写一个下载网站内容的GUI工具&#xff0c;所有内容都能下载&#xff0c;真的太方便了&#xff01;前言本次要实现的功能效果展示代码实战获取数据GUI部分最后前言 哈喽大家好&#xff0c;我是轻松。 今天我们分享一个用Python写下载视频弹幕评论的代码。 之前自游写了…