C++第十六节课 万字详细手动实现string类!

news2024/12/23 18:04:45

std::basic_string

std::basic_string 是 C++ 标准库中定义的一个模板类,它用于表示字符串。C++ 中的 `std::string` 实际上是 `std::basic_string<char>` 的一个特化版本。也就是说,`std::string` 是 `std::basic_string` 这个模板类的一个具体实现,专门用于处理以字符为基础的字符串。

通过类模板实例化的出了string还是wstringu16stringu32string

其中u16string表示一个字符16个字节;

u32string表示一个字符32个字节;

GTP总结:

string类的模拟实现:

为了与标准库做区分,我们可以自定义一个命名空间,在命名空间内实现string类!

实现string的构造函数

  • _str不能直接直接等于参数中的str(_str是char*,str是const char*如果直接等于会出现权限的放大,且如果传入的参数是常量字符串,那么_str此时无法进行修改!)
  • size和capacity都不会考虑/0的结果;
  • 类中成员变量的声明应该和初始化列表的顺序一致;

最终我们实现第一版的string的构造函数

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

对于没有参数的默认构造函数:

		string()
			:_size(0)
			,_capacity(0)
			//, _str(nullptr)
			, _str(new char[1])
		{
			_str[0] = '/0';
		}
  • C 风格字符串的要求:C 风格字符串是由字符数组组成的,并且以空字符 ('\0') 结束。_str 必须指向有效的内存,以便能表示一个字符串。
  • 一个有效的空字符串应该至少包含一个字符:这个空字符 ('\0')。避免空指针解引用:如果 _str 被初始化为 nullptr,当任何试图访问 _str 的成员方法(例如 c_str())被调用时,程序将试图解引用空指针,造成未定义行为(runtime error 或 segmentation fault)。

接下来我们可以尝试利用缺省参数将两个string写到一个函数里面:(常量字符串末尾自带/0)

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

这两种写法都可以:第一种相当于里面有两个/0第二种相当于里面有一个/0,但是最好还是采用第二种写法!

实现string的析构函数

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

		const char *c_str()
		{
			return _str;
		}

实现string的一些接口函数:

c_str

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

size

		size_t size()const
		{
			return _size;
		}

一般希望c_str和size后面参数+上const:使得const对象和普通对象都可以传递!

operator[] 

首先,size和c_str可以只有一个版本(不需要const版本!)因为这两个函数只是返回参数,并不会对成员变量进行修改(只读不写)!

但是[]需要提供两个版本,因为[]需要提供修改变量的功能!

且编译器会选择最合适的版本:如果有两种实现,那次此时普通的变量会去调用普通的函数,被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];
		}

遍历对象

因为前面我们实现了size函数,我们可以直接用for循环来遍历对象:

	for (size_t i = 0; i < s1.size(); i++)
	{
		s1[i]++;
	}

	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	//const只能读取不能修改
	const hello::string s3("hello world");
	s3[0];

其中要注意的是:const只能读取,不能修改!(且const会匹配最适合自己的函数!)

接下来我们尝试使用迭代器实现遍历:

string里面的迭代器中:begin指向第一个字符串,end指向有效字符的下一个位置:/0不是有效字符;

在这里:string的迭代器实际上就是一个char*的指针!

因此,我们尝试实现以下迭代器:、

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

使用迭代器完成遍历的任务:

	hello::string::iterator it = s1.begin();
	while (it !=s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

除此之外,还可以使用范围for实现任务:

	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

虽然我们的类没有实现范围for,但是我们还可以使用,这是因为范围for底层就是迭代器,使用范围for的时候,系统自动调用迭代器!

且如果将自定义的iterator修改名字,例如End,此时范围for就找不到对应的迭代器!(是一种傻瓜式的底层应用!-- 将迭代器换名字End,系统还是会自动去找end而出错!) 

在底层的汇编中:还是调用对应的迭代器函数!

接下来我们实现一个const版本的迭代器:

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

使用const遍历对象只能读不能改写,如下所示:

	const hello::string s3("hello world");
	s3[0];

	hello::string::const_iterator cit = s3.begin();
	while (cit != s3.end())
	{
		cout << *cit << "";
		++cit;
	}
	cout << endl;

在这里,如果我们需要实现push_back和append,那么我们需要先实现resreve!即先完成扩容的功能! 

实现reserve

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

实现push_back

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				// 2倍扩容
				// 如果初始为空字符串,那么给4;否则为原来的2倍
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

实现append

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len >= _capacity)
			{
				// 进行扩容
				// 至少扩容到_size + len
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		}

同理!在我们实现了上述三种接口函数之后,我们可以复用功能来实现+=的运算符重载!

实现opeartot+=

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

在这里我们通过函数重载实现operator+=!

实现Insert

这里我们实现两种类型的insert:在pos位置插入n个字符和在pos位置插入毅哥字符串!

实现insert之前我们需要考虑下面几点:

  • pos位置是否在[0,size]之内;
  • 首先先进行扩容;
  • 数据向后面移动;

接下来我们考虑向pos位置插入n个字符的情况:

		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);
			// 扩容
			if (_size + n >_capacity)
			{
			reserve(_size + n);
			}
			// 挪动数据
			// pos位置插入一个数据
			// (size-pos)个数据整体向后面移动1位;
			size_t end = _size;
			while (pos <= end)
			{
				_str[end + n] = _str[end];
				end--;
			}
			// 插入数据
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}
		}

其中,因为我们在pos位置插入数据,因此我们需要把pos位置之后的数据向后移动,即将(size-pos)个数据向后移动n位;

最终我们可以得到上面的第一代实现!

但是上面的函数有一个问题:当pos = 0时,此时pos位置永远小于end!程序会进入死循环!

这个地方,end是size_t的,pos也是size_t的,当pos为0时,end >=0 是永远成立的,虽然end在一直- -。其实每次到0的时候又变成一个非常大的数。就死循环了!

如果只把end变为int类型,当end = -1的时候还会进入循环!

int end = 0;

这是因为有符号和无符号进行比较,此时有符号会转化为无符号的数字,然后再进行比较!

此时可以考虑将end和pos都改为int类型,但是库里面的实现都是size_t类型!因为我们还是尽量按照库的标准来实现!

解决方法一:

强转类型:

			int end = _size;
			while ((int)pos <= end)

此时是两个整形进行比较;

解决方法二:

通过定义npos:

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

此时我们在类外定义npos的值:

静态成员变量不能在类中给出缺省值,静态成员变量不属于某个类中,不走初始化列表;

但是,注意:

const修饰的静态成员变量可以在类中给缺省值(只有整形)!

	private:
		size_t _size;
		size_t _capacity;
		char* _str;
		const static size_t npos = -1;
	size_t string::npos = -1;

但是double类型的const static修饰的成员变量又不能在类中给缺省值!

			size_t end = _size;
			while (pos <= end && pos != npos)
			{
				_str[end + n] = _str[end];
				end--;
			}

在循环处增添判断条件:pos<=end && pos!= npos

此时如果pos为空则不会进入循环,而直接进行插入!

解决方法三:

			size_t end = _size + n;
			while (pos < end)
			{
				_str[end] = _str[end - n];
				end--;
			}

通过使end = _size+1;则循环判断条件没有=的时候,当pos = end的时候循环结束!

接下来我们考虑插入字符串的类型:

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			// 挪动数据
			size_t end = _size + len;
			while (pos < end)
			{
				_str[end] = _str[end - len];
				end--;
			}
			// 插入数据
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += len;
		}

这里字符串类型和字符类型的思想基本一致!

实现earse

如果我们需要实现earse我们需要考虑两种情况:

  • 直接将pos位置后面的字符全部删除;
  • 删除pos位置后来的有限个字符;

		void earse(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size = _size - len;

			}
		}

实现find

find函数我们主要实现两种:从pos位置开始,查找一个字符和

从pos位置开始,查找字符串!找到后返回下标

如果没找到,则返回npos;

查找一个字符:

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

查找一个字符串:

		size_t find(const char* str, size_t pos)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr) // 此时查找到了对应的字符串
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

此时我们巧妙用C语言的strstr函数来实现:如果查找到对应的字符串,则会返回查找的字符串的指针;

实现substr

从pos位置开始取一个长度为n的字串,如果找到则返回字串,返回类型为string;

		string substr(size_t pos = 0,size_t len = npos)
		{
			assert(pos < _size);
			size_t n = len;
			if (len >= npos || pos + len >=_size)
			{
				n = _size - pos;
			}
			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos+len; i++)
			{
				tmp += _str[i];
			}
			return tmp;
		}

但是对于上面代码,存在一个问题:最后返回临时变量tmp,但是tmp是一个自定义类型,需要调用拷贝构造函数(然后继续调用析构函数),但是此时我们没有写对应的拷贝构造函数,所以默认是浅拷贝,返回的临时对象所在的空间已经被销毁,再调用对应的接口函数会出现问题!

解决方法:构造一个深拷贝!

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

开辟一个同样大的空间,然后再将值拷贝过去!

实现resize

resize官方给了两种类型:我们可以通过缺省参数将两种类型合并一起!

接下来我们考虑三种情况:

假如_size = 10,_capacity = 15;

分别考虑上述三种情况:

		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else {
				reserve(n);   //如果需要扩容会自动扩容
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

实现流插入<<

流插入和流提取的函数我们要写在类外面,我们我们想要第一个参数为ostream/istream;

ostream设置的有防拷贝,因此返回类型和传参类型必须是返回类型,防止我们使用拷贝构造(后面会详细讲)

下面为流插入的代码实现:

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

注意点:这里的ostream是std里面的,所以我们用的时候需要将命名空间展开或者指定作用域! 

这里打印C类型的字符串和打印string类型的区别:C类型字符串本质是打印const char*,遇到 \0 就停止,但是打印s是在使用循环,打印完size()就停止!

下面两种情况下打印出现差别: c_str()遇到 \0 就停止,即使后面又添加了字符也不会打印,但是流插入依然会打印!(vs13系列对待中间\0会按照空格打印;vs19以后会直接不打印)

s._str这种打印方式不可取!(内置成员变量为私有的!)

因此,上述我们实现的函数,当我们开辟一个空间,想把原来的内容拷贝进去,我们使用的是strcpy,但是strcpy遇到 \0 就中止,\0后面如果还有数据则无法拷贝,因此我们建议替换为memcpy!

复习下memcpy的用法:

void *memcpy(void *str1, const void *str2, size_t n)

参数

  • str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
  • n -- 要被复制的字节数。

尤其是拷贝构造:需要将strcpy换成memcpy (应该将string全部内容都拷贝过来,如果使用strcpy则中间遇到 \0 就停止 --- string对象中间包含又 \0);

总结:

  • c的字符数组,以 \0 为终止算长度;
  • string不看 \0,以size为终止算长度;

实现流提取>>

将向终端控制台输入的数据提取出来!

输入的数据不能+const!因为我们输入的数据会传入终端控制台,+const数据不能修改;

问题一:能否直接输入s._str?

不能!首先_str是私有的成员变量,我们不能使用;其实,如果我们想进行输入s._str,那么这个变量应该占用多少空间呢?直接进行输入的话系统无法给出确切的空间大小!

问题二:流提取 / scanf怎么进行输入数据的分割?

遇到' '或者'\n'自动进行分割!(默认不会读取空格 / 换行)

注意点:这里我们不能使用>>!因为>>不会区分空格和换行,会使得程序一直进行,没办法停止!

在这里我们使用get()函数,get函数的作用是遇到' '和'\0'就会自动停止!

get 是istream 中的成员函数,用于读取单个字符

	istream& operator>>(istream& in, string& s)
	{
		//char ch;
		char ch = in.get();
		in>>ch;
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			//in >> ch;
			ch = in.get();
		}
		return in;
	}

输出的结果遇到空格或者 \0 会自动停止,与库中的实现的效果一样!(且这里的+=会自动完成扩容)

但是上面的代码还存在一些问题,我们先看一些库里面的实现:

void test10()
{
	std::string s;
	cin >> s;
	cout << s;
	cin >> s;
	cout << s;
}

我们发现两次连续的输入,库里面的流输入会将之前的内容清空! 

接下来尝试我们自己的实现:

void test10()
{
	shy::string s;
	cin >> s;
	cout << s << endl;;
	cin >> s;
	cout << s << endl;;
}

如果连续进行输入,库里面的实现是对之前的内容进行覆盖!

但是我们自己实现的函数中,内容并没有被覆盖!

因此我们自己完成一个清除数据的接口函数:

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

然后再对之前的流提取进行修改:

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		//char ch;
		char ch = in.get();
		//in>>ch;
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			//in >> ch;
			ch = in.get();
		}
		return in;
	}

改进点:

当我们使用+=的时候,如果我们初始给定的字符串很长,那么函数会持续扩容,使得整体效率不高!接下来我们来查看一下:在扩容函数中增加打印语句查看调用多少次

 输入上面这么长的字符串,经历了6次扩容,效率较低!

解决方法:使用一个数组;

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		//创建一个类似桶的东西存储数据
		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			// 用通存储数据
			buff[i++] = ch;
			if (i == 127)
			{
				// 此时桶装满了,将数据导出,再重接接数据
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		// 此时桶还没装满,提前退出
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}		
		return in;
	}

可以发现,此时输入超长的字符串,也只进行了两次扩容;

接下来还有一个问题:

当我们先输入空格或者换行的时,接下来输入的内容无法显示:

 这是因为get函数的作用是遇到' '和'\0'就会自动停止!

我们试验std库的实现,发现可以打印

接下来我们对自己的库进行改进:

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		// 处理缓存区前面的空格或者换行
		while (ch == ' ' && ch == '\n')
		{
			char ch = in.get();
		}
		//创建一个类似桶的东西存储数据
		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			// 用通存储数据
			buff[i++] = ch;
			if (i == 127)
			{
				// 此时桶装满了,将数据导出,再重接接数据
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		// 此时桶还没装满,提前退出
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}		
		return in;
	}

实现比较<

我们给出第一种写法:

		bool operator<(const string& s)
		{
			return strcmy(_str, s._str) < 0;
		}

这个代码看似没有问题,但是如果字符串中间又 \0 的话,那么无法正常进行比较! 

因此我们使用memcpy来实现,复习下memcpy函数:

int memcmp(const void *str1, const void *str2, size_t n)

参数

  • str1 -- 指向内存块的指针。
  • str2 -- 指向内存块的指针。
  • n -- 要被比较的字节数。

返回值

  • 如果返回值 < 0,则表示 str1 小于 str2。
  • 如果返回值 > 0,则表示 str1 大于 str2。
  • 如果返回值 = 0,则表示 str1 等于 str2。

但是这里使用memcmp比较会出现一个问题:按照谁的_size大小来比较?

应该按照两个字符串中较短的来进行比较!

但是如果按照小的进行比较,那么上述情况又会出现问题!

因此,这里我们可以不借用库中的函数,自己来实现:

		bool operator<(const string& s)
		{
			size_t i1 = 0;
			size_t i2 = 0;
			while (i1 <_size && i2 <s._size)
			{
				if (_str[i1] < s._str[i2])
				{
					return true;
				}
				else if (_str[i1] > s._str[i2])
				{
					return false;
				}
				else {
					i1++;
					i2++;
				}
			}
		// 出循环后,此时说明两端字符串肯定有一段执行完了
		// 可能有下面三种情况:
		// "hello" "hello" --> false
		// "hello" "helloxxx"  -->true
		// "helloxxx" "hello  --->false
			// 写法一:
			//if (i1 == _size && i2 != s._size)
			//{
			//	return false;
			//}
			//else 
			//{
			//	return true;
			//}
			// 写法二:
			return _size < s._size;
		}

接下来我们再来提供一个复用库函数来实现:

		bool operator<(const string& s)
		{
			// 内存比较:按照两个字符串中长度较短的进行比较
			// s1 < s2
			bool ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
			// 可能有下面三种情况:
			// "hello" "hello" --> false
			// "helloxxx" "hello  --->false
			// "hello" "helloxxx"  -->true
			return ret == 0 ? _size < s._size:ret<0;
			// 其中_size<s._size代表上面三种情况
			// ret < 0    ---> 如果按内存比较s1<s2 == ret<0		
			}

实现其他比较运算符

实现了上面的<运算比较的时候,我们可以进行服用实现其他函数:

		bool operator==(const string& s)const
		{
			return _size == s._size && memcmp(_str, s._str, _size);
		}
		bool operator<=(const string& s)const
		{
			return (*this == s) || (*this < s);
		}
		bool operator>(const string& s)const
		{
			return !(*this <= s);
		}
		bool operator>=(const string& s)const
		{
			return !(*this < s);
		}
		bool operator!=(const string& s)const
		{
			return !(*this == s);
		}

且因为比较运算符都是可读的,我们可以加上const进行修饰!(const 对象和普通对象都可以调用) 

实现赋值运算符=

进行s1 = s3;此时如果是浅拷贝,那么s1指向的空间直接指向s3,最终同一块空间进行两次析构而报错;

此时正确的做法是:s1再开辟一段新的空间,将原来的空间释放掉,再将s3的内容拷贝过去;

与拷贝构造不同的是,拷贝构造直接开辟与原对象一样大的空间,再将数据拷贝过去;

传统的赋值运算符=写法:
		string& operator=(const string& s)
		{
			if (*this != s)
			{
				char* tmp = new char[s._capacity + 1];
				memcpy(_str, s._str, s._size + 1);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
		return *this;
		}
现代的赋值运算符=写法: 

现代赋值运算是借用拷贝构造,拷贝一个一样的对象,再将其值复制过去;

		string& operator=(const string& s)
		{
			if (*this != s)
			{
				string tmp(s);
				std::swap(_str, tmp._str);
				std::swap(_size, tmp._size);
				std::swap(_capacity, tmp._capacity);
			}
			return *this;
		}

tmp是一个局部对象,出了作用域就会被销毁!

考虑下面一种问题:能否这样子调用swap函数?

 不可以!会造成死循环递归而导致栈溢出!

swap就会调用赋值运算符,而这里我们就是实现赋值运算符的!

实现swap交换

string库里面实现的有string类型的交换:

接下来我们尝试写一下:

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

接下来我们可以尝试修改下上面实现的代码:

写法三:

		string& operator=(const string& s)
		{
			if (*this != s)
			{
				string tmp(s);
				swap(tmp);
			}
			return *this;
		}

写法四:

		string& operator=(string& tmp)
		{
			swap(tmp);
			return *this;
		}

但是写法四中的参数不能是const修饰的,因为此时tmp会被修改!

这里的tmp是s3的深拷贝,然后再调用tmp进行转换;

此时s3的值不会发生改变;tmp的值会发生改变!

 

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

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

相关文章

DAY29|| 93.复原ip地址 |78.子集 |90.子集Ⅱ

93.复原ip地址 题目&#xff1a;93. 复原 IP 地址 - 力扣&#xff08;LeetCode&#xff09; 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.…

国外电商系统开发-运维系统上传脚本

创建脚本的方式有两种&#xff1a;第一种脚本文件&#xff0c;第二种在线写脚本。并且友好的支持中文的显示和脚本的中文名。 第一种是从您的PC电脑上传一个脚本文件&#xff0c;当然了&#xff0c;还是以老用法&#xff0c;直接拖动就行&#xff1a; 第二种上传方式&#xff0…

『网络游戏』服务器日志工具类优化【18】

创建脚本&#xff1a;PECommon.cs 编写脚本&#xff1a;PECommon.cs 修改脚本&#xff1a;LoginSys 替换 替换完成 修改客户端脚本&#xff1a;ResSvc.cs 本章结束

仓储物流行业--仓储服务升级经典案例

在当今内外贸竞争激烈的仓储物流行业&#xff0c;高效的数据处理和管理是企业提升竞争力的关键。杭州某供应链公司作为一家专注为中小卖家提供定制仓储服务方案的第三方云仓&#xff0c;在业务发展过程中面临着数据处理方面的诸多挑战。本文将详细介绍云仓项目如何通过轻易云数…

上海交通大学震撼发布:首个OpenAI O1项目复现报告,揭秘独家经验!

来源 | 机器之心 团队介绍&#xff1a;本项目的核心开发团队主要由上海交通大学 GAIR 研究组的本科三年级、四年级学生以及直博一年级研究生组成。项目得到了来自 NYU 等一线大型语言模型领域顶尖研究科学家的指导。 详细作者介绍见&#xff1a;https://github.com/GAIR-NLP/…

FireFox简单设置设置

文章目录 一 设置不显示标签页1原来的样子2新的样子3操作方法 二 设置竖直标签页栏1 效果图2 设置方法 三 设置firefox不提醒更新 一 设置不显示标签页 1原来的样子 2新的样子 3操作方法 地址栏输入 about:config搜索icon,双击选项列表中browserchrome.site icons的值&#…

HWS赛题 入门 MIPS Pwn-Mplogin(MIPS_shellcode)

解题所涉知识点&#xff1a; 泄露或修改内存数据&#xff1a; 堆地址&#xff1a;栈地址&#xff1a;栈上数据的连带输出(Stack Leak) && Stack溢出覆盖内存libc地址&#xff1a;BSS段地址&#xff1a; 劫持程序执行流程&#xff1a;[[MIPS_ROP]] 获得shell或flag&am…

关于css文字下划线动画实现

直接上代码 html部分 <div><span class"txt">文字下滑动画</span></div>css部分 .txt {background: linear-gradient(270deg, #4f95fd 0%, #1059df 100%) no-repeat left bottom;background-size: 0px 2px;background-position-x: right;tr…

汽车微控制器 (MCU)市场报告:未来几年年复合增长率CAGR为5.8%

汽车微控制器是一种高度集成的电路芯片&#xff0c;集成了中央处理器&#xff08;CPU&#xff09;、存储器&#xff08;ROM、RAM、EEPROM等&#xff09;和各种输入输出接口&#xff08;I/O&#xff09;&#xff0c;能够通过软件编程实现对汽车各种电子设备的控制和管理。在汽车…

字符设备驱动模块 dev和misc

字符设备驱动模块 用户空间和内核空间数据拷贝&#xff1a; copy_from_user(); 用户空间数据传给内核 copy_to_user(); 内核数据传给用户空间 动态和静态分别编译 设置Kconfig属性 编译内核文件&#xff08;静态、动态内核文件&#xff09; 启动动态驱动模块 查看动态驱动…

深度学习每周学习总结J2(ResNet-50v2算法实战与解析 - 鸟类识别)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 目录 0. 总结1. 设置GPU2. 导入数据及处理部分3. 划分数据集4. 模型构建部分5. 设置超参数&#xff1a;定义损失函数&#xff0c;学习率&a…

Web开发:总结常见的批处理脚本(.bat)

一、一键复制多个文件 echo off setlocalset source01.pngcopy "%source%" "a.png" copy "%source%" "b.png" copy "%source%" "c.png"endlocal说明&#xff1a; 将上述代码复制到一个新的文本文件中。将文件保…

4.STM32-中断

STM32-中断 需求&#xff1a;红灯每两秒进行闪烁&#xff0c;按键key1控制绿灯亮灭 简单的程序代码无法满足要求 如何让STM32既能执行HAL_DELAY这种耗时的任务&#xff0c;同时又能快速响应按键按下这种突发情况呢 设置中断步骤 1.接入中断 将KEY1输入模式由原先的GPIO_In…

React学习02 -事件处理、生命周期和diffing算法

文章目录 react事件处理非受控组件受控组件高阶函数函数柯里化 生命周期引出生命周期旧版生命周期新版生命周期 Diffing算法 react事件处理 1.react通过onXXX属性指定事件处理函数&#xff0c; a.react使用的是自定义事件&#xff0c;将原生js事件方法重写并改为小驼峰写法&am…

大数据新视界 --大数据大厂之大数据驱动下的物流供应链优化:实时追踪与智能调配

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

前端自定义指令控制权限(后端Spring Security)

1. 新建 directives/auth.ts &#xfeff; &#xfeff; //导入自定义指令 import auth from /directives/auth// 注册全局自定义指令 v-auth app.directive(auth, auth);&#xfeff;1.1完整的authDirective.ts import { wmsStore } from "/store/pinia"// 判断用…

dmdfm5安装部署

dmdfm5安装部署 1 环境说明2 命令行安装dmfdm52.1 创建 dmdba 用户2.2 命令行安装 dmdfm2.3 配置自启动脚本服务2.4 web端 访问 dmdfm 3 安装过程错误记录4 更多达梦数据库学习使用列表 1 环境说明 cpu x86OS 麒麟v10(sp2)dmfdm5 版本 : dmdfm_V5.0.1.1_rev157137_x86_linux_6…

计算机网络803-(4)网络层

目录 1.虚电路服务 虚电路是逻辑连接 2.数据报服务 3.虚电路服务与数据报服务的对比 二.虚拟互连网络-IP网 1.网络通信问题 2.中间设备 3.网络互连使用路由器 三.分类的 IP 地址 1. IP 地址及其表示方法 2.IP 地址的编址方法 3.分类 IP 地址 &#xff08;1&#x…

双通讯直流电能计量装置功能介绍

DJSF1352系列电子式直流电能表是为满足现代直流电力计量需求而设计的高性能设备。其主要特点包括液晶显示和RS485通讯功能&#xff0c;方便与微机进行数据交互&#xff0c;适用于充电桩、蓄电池、太阳能电池板等多种直流信号设备的电量监测。该产品由测量单元、数据处理单元、通…

python数学运算库numpy的使用

数组 numpy创建数组的方法 可以用np.array()将一个列表作为参数 import numpy as npd1 np.array(range(1,7))print(d1) # 输出数据 print(d1.size) # 输出元素个数 print(d1.ndim) # 输出数组维度 print(d1.shape) # 输出数组形状&#xff08;长宽高&#xff09; 可以…