详解c++---string模拟实现

news2024/12/23 19:28:40

这里写目录标题

  • 前言
  • 准备工作
  • 构造函数
  • 析构函数
  • 迭代器的实现
  • 插入数据有关的函数实现
    • reserve
    • push_back
    • operator+=
    • append
    • insert
  • erase
  • find
  • resize
  • [ ]
  • clear
  • >>
  • >>
  • 新式拷贝构造函数
  • 新式赋值重载

前言

在前面的文章里我们学习了c++中string的用法,那么这篇文章我们将带着大家学习如何来模拟实现string这个类,当然我们这里的实现只是为了带着大家来更好的了解这个类,并不是完整的复刻一份相同的类出来,所以我们这里就只实现string类中的主要的常用的功能,那么在开始阅读下面内容之前,大家可以看看下面两篇文章来先了解了解string类的用法以:
string的介绍(上)
string的介绍(下)

准备工作

首先为了防止命名冲突,我得创建一个命名空间,在这个命名空间里面来实现我们的类,那这里我们就称这个空间为:YCF,然后在这个命名空间里面创建一个类,该类类名为string,那么上述的代码如下:

namespace YCF
{
	class ycf
	{
	public:

	private:

	};
}

因为这个类处理的对象是字符串,要对字符串做增删查改的话,我们这里得创建三个变量来描述这里字符串的关键信息,那这三个变量就分别是char类型的指针 _str,记录字符串结尾的位置 _size,和记录当前对象容量的_capacity,我们将这三个成员变量放到private里面以防被使用者不经意间修改了,那么我们这里的代码就如下:

namespace YCF
{
	class string
	{
	public:

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

构造函数

对于类来说构造函数是非常重要的,我们这里就先来实现用一个字符串来初始化一个类的情形,因为传参传过来的参数是一个字符串而字符串具有常性,所以我们这里在接收的时候就得用const char*类型的参数来进行接收

		string(const char* _str)
		{

		}

因为传过来的内容是字符串,而这个字符串放的地方是常量区,该区域的内容是不能被改变的,所以我们这里就不能简单的将str的值复制成_str,而是得我们亲自开辟一个空间将字符串的内容拷贝到该空间里面去,然后再将size和capacity的值赋值为该字符串的长度,那么这里capacity的意思是能够容纳多少个有效字符,所以当我们申请空间的时候就得申请capacity+1个大小的空间而不是capacity个大小的空间,之所以这么做是为了方便我们后面的计算,那么这里我们的代码就如下:

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

那么这里为了方便我们查看这个对象中的字符串内容,我们就可以来实现一个c_str函数,通过这个函数来返回对象中的字符指针,再通过字符指针来打印字符串的内容,那么c_str函数的实现就非常的简单,直接在函数里面返回类中的str指针就行,但是这里的返回值大家要注意一下,我们得加个const以防止使用者通过c_str函数来修改对象中字符串的内容,并且我们还要防止this指针所导致的权限放大的问题所以得在括号的后面加上const,以用来修饰this执政,那么该函数的实现就如下:

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

有了c_str函数我们就可以来测试第一个构造函数写的是否是对的,测试的代码就如下:

#include"string.h"
using namespace std;
void test1()
{
	YCF::string s1("hello world");
	cout << s1.c_str() << endl;
}
int main()
{
	test1();
	return 0;
}

该代码的运行结果为:
在这里插入图片描述
这里成功的打印出来了初始化的内容,所以该函数的实现大致是真确的,为了确保完全正确我们就得观察另外两个变量的值,那么这里我们就可以根据c_str函数的原理再来实现size函数和capacity函数,其实现的代码如下:

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

测试代码的修改如下:

void test1()
{
	YCF::string s1("hello world");
	cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
	cout <<"对象中字符串的长度为:" << s1.size() << endl;
	cout <<"对象的容量为:"<< s1.capacity() << endl;
}

该代码的运行结果如下:
在这里插入图片描述
我们可以看到这里的有效字符确实是有11个,构造函数将_capacity的值复制为和_size的值一摸一样所以这里打印出来的两个变量的值都为11,那么这就说明我们的构造函数实现的没有问题,当然构造函数的类型肯定是不止一个的,我们这里可以写一个无参的构造函数,既然无参的话那么这个构造函数的结果就是对象中的字符串只有一个\0其他内容都没有了,所以在这个构造函数里面我们就只用给他申请1个字节的空间用来存放\0,然后将_size和_capaciy的值初始化为0就可以了,那么这里的代码就如下:

		string()
			:_size(0)
			, _capacity(0)
		{
			_str = new char[1] {'\0'};
		}

该构造函数的测试代码如下:

void test1()
{
	YCF::string s1("hello world");
	cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
	cout <<"对象中字符串的长度为:" << s1.size() << endl;
	cout <<"对象的容量为:"<< s1.capacity() << endl;
	YCF::string s2;
	cout << "对象中字符串的内容为:" << s2.c_str() << endl;
	cout << "对象中字符串的长度为:" << s2.size() << endl;
	cout << "对象的容量为:" << s2.capacity() << endl;
}

代码的运行结果为:
在这里插入图片描述
符合我们的预期,但是大家有没有发现一个可以简化的地方,无参构造函数的结果是_size和_capacity的值都等于0字符串中的内容也只有\0,而我们知道如果一个字符串中只有\0的话strlen的结果也是0,如果strlen的结果为0的话那么第一个构造函数里面的_capacity和_size不就也为0了嘛!如果源字符串的内容为空字符串的话那strcpy的结果不就也为空字符串了嘛?所以这里我们能不能用第一个构造函数来代替这个无参的构造函数呢?答案是可以的,既然不传参数也能调用第一种构造函数的话,我们这里就得给该函数添加一个缺省值上去,因为无参构造函数的内容为空字符串,所以我们这里的缺省值就可以给他一个空字符串,那么该函数的声名就是这样:string(const char* str="")将第二个构造函数去掉,再运行上面的测试代码我们可以看到这里的运行结果是一样的,那么这就是我们构造函数的内容,希望大家可以理解。

析构函数

构造函数里面是通过new来申请的动态空间,那么我们析构函数要干的事情就是将申请的动态空间进行释放,前面我们学过c++释放动态空间的函数是delete,那这里我们就可以用该函数来达到目的,那么析构函数对应的实现就如下:

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

那这里我们测试该函数的方法就是通过调试来看当该对象的生命周期结束时,对象中的内容会发生什么样的变化?那么测试的代码就如下:

YCF::string* test2()
{
	YCF::string s1("hello world");
	cout << s1.c_str() << endl;
	return &s1;
}
int main()
{
	const YCF::string*p1= test2();
	return 0;
}

我们开启调试,当编译器执行完构造函数之后对象中的内容变为:
在这里插入图片描述
然后我们就打印这个对象中的字符串,并将这个对象的地址作为test2函数的返回值返回main函数,在main函数里面我们就创建一个该类型的指针用来查看此对象经历析构函数之后的内容:
在这里插入图片描述
那么这里我们就可以看到该对象中的所有成员变量的内容全部都初始化为了0,所以这就说明析构函数的实现是正确的没有问题。

迭代器的实现

我们说迭代器是一个像指针的东西,他可能是指针也可能不是指针,但是在我们string类里面迭代器他就是通过指针来实现的,所以我们这里的iterator就是char*的typedef的命名重载:

typedef char* iterator;

跟迭代器有关的函数有begin和end,这两个函数的功能的就分别是返回字符串开头的位置和字符串结尾的位置,那么这两个函数就可以直接通过对象中的_size来实现,begin就是直接返回_str,而end就是返回_str+_size
返回的类型就是iterator,那代码的实现就如下:

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

我们可以通过下面的代码来进行一下测试:

void test3()
{
	YCF::string s1("hello world");
	YCF::string::iterator it1=s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1;
		it1++;
	}
}

该代码的运行结果如下:在这里插入图片描述
那么这就是迭代器的实现,看到这里我们顺便来聊聊范围for的实现,大家第一次看到这个东西的时候一定非常的好奇这个范围for是如何实现的?那么这里我们就可以通过下面的代码再来理解一下范围for:

void test3()
{
	YCF::string s1("hello world");
	YCF::string::iterator it1=s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1;
		it1++;
	}
	cout << endl;
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
}

我们首先是通过迭代器来将字符串进行循环打印,然后再用范围for再来实现循环打印,每打印一个字符时再加一个空格,那么该代码的运行结果如下:
在这里插入图片描述
那么这里大家有没有想过一个问题:我们类中没有实现范围for那他是如何来运行的呢?有人会说是c++帮我们实现的,可是如果是c++实现的范围的话,那他又是如何来识别自定义类型的呢?所以这里的范围for并不是别人帮我们实现的,而是我们自己通过迭代器来实现的,也就是说范围for只是一个虚伪的外表,真正运行的代码其实还是迭代器,那我们这里如何来证明呢?我们将这个类中的begin函数的名字改成Begin再来运行一下这个代码看看会发生什么,首先常规的迭代器是没有任何问题的:

	YCF::string s1("hello world");
	YCF::string::iterator it1=s1.Begin();
	while (it1 != s1.end())
	{
		cout << *it1;
		it1++;
	}
	cout << endl;

这段代码的运行结果也是正常的:
在这里插入图片描述
但是范围for的代码却不正常了:

	YCF::string s1("hello world");
	//YCF::string::iterator it1=s1.Begin();
	//while (it1 != s1.end())
	//{
	//	cout << *it1;
	//	it1++;
	//}
	//cout << endl;
	for (auto ch : s1)
	{
		cout << ch << " ";
	}

他报出来许多错误:
在这里插入图片描述
那这说明了什么?是不是就可以验证范围for的底层就是通过迭代器来实现的,当这段代码编译完之后范围for对应的代码就会替换成迭代器对应的代码而且这个替换他还是死的,一旦我们修改了begin函数或者其他有关的函数他就会报错,大家可以将这里替换与宏联系起来一起理解,那以上就是有关迭代器的实现希望大家能够理解。

插入数据有关的函数实现

reserve

既然要插入数据,那么这里我们就得先来实现一下扩容函数,首先这个函数接受的参数就是一个无符号整型:

void reserve(size_t n)

因为c++中没有像c语言一样有扩容函数realloc,所以我们这里采用的扩容策略就是再创建一个新的空间出来再将原来老空间中的内容复制到新空间里面去,然后释放原来的老空间将对象中的_str指针指向新的空间,最后再修改_capacity的值,那么下面就是对应代码的实现:

void reserve(size_t n)
{
	char* tmp = new char[n + 1];//这里的加1是给\0留空间
	strcpy(tmp, _str);
	delete[] _str;
	_str = tmp;
	_capacity = n;
}

但是这里并没有结束,学过之前的数据结构我们应该能够知道一件事就是:面对数据容量的变化我们采取的策略是只扩容不缩容,所以在执行上述函数体之前我们得先用一个if函数来进行判断,如果你给的n大于_capacity的值的话我们才执行上述代码,否则我们什么都不执行:

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];//这里的加1是给\0留空间
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

那么这就是reserve函数的实现,希望大家能够理解。

push_back

sting库中的push_back每次调用只能尾插一个字符,所以该函数就只有一个参数用来接收你要插入的字符:

void push_back(char ch)

那么在执行这个尾插函数之前我们得先来做一个判断:当对象中的容量不够用的时候调用reserve函数来进行扩容然后再将数据插入,这里的扩容我们就扩成原来容量的两倍:

		void push_back(char ch)
		{
			if (_capacity == _size)
			{
				reserve(ch * 2);
			}
		}

然后我们再来实现数据的插入,把_size看作下标他指向的是字符串的结尾也就是\0,那么这里就可以直接将下标为_size的元素修改成我们要尾插的元素,然后让_size的值+1再让下标为_size的元素赋值为\0以免字符串没有结束的标志,那么这里我们的代码就如下:

		void push_back(char ch)
		{
			if (_capacity == _size)
			{
				reserve(_capacity * 2);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

该函数实现完之后,我们就可以写一段来测试这里函数实现的正确性:

void test1()
{
	YCF::string s1("hello world");
	cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
	cout <<"对象中字符串的长度为:" << s1.size() << endl;
	cout <<"对象的容量为:"<< s1.capacity() << endl;
	YCF::string s2;
	cout << "对象中字符串的内容为:" << s2.c_str() << endl;
	cout << "对象中字符串的长度为:" << s2.size() << endl;
	cout << "对象的容量为:" << s2.capacity() << endl;
}

这段代码的运行结果为:
在这里插入图片描述我们可以看到字符串的结尾确实增加了两个字符,而且字符串的长度和容量也发生了改变,长度变成了13容量变成了22,但是这能够说明该函数的实现是完全正确的吗?我们再来看看下面的测试代码:

void test4()
{
	YCF::string s1("hello world");
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	s1.push_back('x');
	s1.push_back('x');
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	YCF::string s2;
	cout << "字符串的内容为:" << s2.c_str() << endl;
	cout << "字符串的长度为:" << s2.size() << endl;
	cout << "字符串的容量为:" << s2.capacity() << endl;
	s2.push_back('x');
	s2.push_back('x');
	s2.push_back('x');
	cout << "字符串的内容为:" << s2.c_str() << endl;
	cout << "字符串的长度为:" << s2.size() << endl;
	cout << "字符串的容量为:" << s2.capacity() << endl;
}

我们将代码运行起来就可以发现这里报错了:

在这里插入图片描述
这里报错的原因就是因为我们拿空字符串来初始化对象,这个对象中的_capacity等于0,而扩容的时侯我们是拿2*capacity进行扩容,capacity的值等于0那扩容之后的结果不还是0了嘛,那不就没有开辟新的空间了嘛,那还谈啥插入数据呢,对吧!所以这里的问题就出在了_capacity上,所以当我们插入数据扩容的时候我们就得先来判断_capacity是否为0,如果为0的话我们就将它的值赋值为4,如果不为0我们就将它的值乘以2倍,那么修改后的代码就如下:

		void push_back(char ch)
		{
			if (_capacity == _size)
			{
				size_t _newcapacity = _capacity == 0 ? 4: 2 * _capacity;
				reserve(_newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

我们再运行上面的代码就不会报错了:
在这里插入图片描述

operator+=

有了前面的push_back我们就可以偷个懒来实现operator+=中的一个形式就是尾插一个字符,该函数的声明是这样的:

string& operator+=(char ch)

函数体中就是调用push_back函数来实现尾插,然后返回*this来结束该函数:

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

我们来看看上面的实现能否通过下面的测试代码:

void test5()
{
	YCF::string s2;
	s2 += 'Y';
	s2 += 'C';
	s2 += 'F';
	cout << "字符串的内容为:" << s2.c_str() << endl;
	cout << "字符串的长度为:" << s2.size() << endl;
	cout << "字符串的容量为:" << s2.capacity() << endl;
}

这段代码的运行结果如下:
在这里插入图片描述
我们可以看到这里的字符串内容,长度和容量都是正确的,那么这就说明我们的代码实现的是正确的。当然该函数还有一种形式就是往后加上一个字符串,那这种形式我们就可以通过append函数来实现:

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

那append函数如何来实现呢?这里我们就可以继续往下看:

append

这个函数的功能就是在字符串的尾部插入另一个字符串,那么该函数的声明就如下:

void append(const char* str)

实现这个函数的第一步就是要判断该对象是否需要扩容以及扩容多大个空间,有了上面的经验有些小伙伴就说啊当_size的值等于_capacity的值时就要扩容,扩到原来的_capacity的两倍,可这样实现是对的吗?很明显不是的,因为我们尾插的是一个字符串而不是一个字符,当_size的值不等于_capacity的值时候,你尾插一个字符串容量也可能不够,而且你扩容两倍也不见得会够,比如说对象中有2个字符,此时的容量为4,可这时我要插入长度为100 的字符串,你觉得扩容两倍够吗?答案很明显是不够的,所以我们这里判断是否需要扩容的条件就是当
_size+strlen()大于_capacity时我们就进行扩容,扩容的大小就是_size+strlen(),那么下面就是我们第一步的代码:

void append(const char* str)
{
	if (_size + strlen(str)>_capacity)
	{
		reserve(_size + strlen(str));
	}
}

将空间的问题解决之后,我们下面干的事情就是要插入数据,这里的插入数据我们就可以使用strcpy函数,第一个参数放目的位置,第二个参数放源字符串的位置,那么这里的目标位置就是_str+_size指向了对象中的字符串的结尾,源字符串就是str,将数据插入完之后我们就要更改_size使其加上strlen(str)的值,那么下面就是完整的代码:

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

我们用下面的代码来测试一下这个函数的正确性:

void test6()
{
	YCF::string s1("hello world ");
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	s1.append("hello c++");
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

这段代码的运行结果如下:
在这里插入图片描述
符合我们的预期,那么这就是该函数的实现。

insert

相较于前面的插入函数,insert函数能够实现在任意位置进行插入数据,所以这个函数在实现的时候就会多出一个参数pos用来表示数据插入的位置,这里的插入可以只插入单个字符也可以插入一个字符串,所以该函数的实现有两种不同的形式风别如下:

string& insert(size_t pos, char ch)
string& insert(size_t pos,const char* str)

首先来实现第一种任意位置的插入形式插入单个字符,那么这种插入首先干的第一件事就是判断是否需要扩容,如果_size的值等于_capacity的值的话我们就进行扩容其大小为原来大小的两倍:

		string& insert(size_t pos, char ch)
		{
			if (_size == _capacity)
			{
				reserve(2 * _capacity);
			}
		}

将空间开辟完之后我们要干的事情就是将pos位置往后的数据全部往后挪动一个位置,然后再将要插入的数据放到pos位置上去,那么这里的挪动数据我们就得创建一个变量end,将他的值复制为字符串末尾的下标,也就是_size的值,然后再创建一个while循环在循环里面将end位置的元素复制到end+1位置上去,再将end的值减1,那么这就是循环所干的内容这个循环结束的条件就是当end的值小于pos的时候我们就结束循环,最后再将元素插入到pos位置上去并使_size的值加1:

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

那么这就是该函数的具体实现,我们来用下面的代码来测试一下这个函数的正确性:

void test7()
{
	YCF::string s1("hello world");
	s1.insert(3, 'x');
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

这个代码的运行结果如下:
在这里插入图片描述
通过运行的结果来看这里代码的实现是正确的,但是我们再用下面的代码来测试的话获取就会出现一些问题:

void test7()
{
	YCF::string s1("hello world");
	s1.insert(3, 'x');
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	s1.insert(0, 'x');
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

在这里插入图片描述
我们发现这里在执行到第二次插入数据的时候发生了崩溃,崩溃的原因就是死循环导致的,当我们想在0位置插入数据的时候end的值为0,pos的值也为0,而这两个元素的类型都是无符号整型并且while循环结束的条件是end<pos,那pos等于0,end为无符号整型的话那这个条件不就永远不可能成立了嘛!所以这就是我们崩溃的原因,当然这里有小伙伴会说将end的类型改成int不就可以变成负数了吗?但是大家得记得一个东西叫整型提升,当end为int类型,pos为无符号整型时,表示式end<pos会发生整型提升将end的类型变成无符号整型,依然无法解决问题,所以我们这里解决问题的方法就是将end的类型改为int然后在表达中加入强制类型转换依次来解决整型提升的问题:

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

我们再运行一下上面的测试代码就可以看到运行结果正常了:
在这里插入图片描述
当然上面的只是一种解决方法,另外一种方法就是将end的值赋值为_size+1,然后将循环体内部的

_str[end + 1] = _str[end];

改成_str[end ] = _str[end-1];这样我们循环继续的条件就变成了end>pos当pos为0,end也为0的时候我们就会停止循环并且还将数据全部都挪动完了,那么修改后的代码就如下:

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

上面的测试代码运行的结果如下:
在这里插入图片描述
结果是对的,那么这就说明我们函数的实现是正确的没有问题,接下来我们再来看看另外一种形式的实现。相较于插入字符的形式,插入字符串形式的最大区别就在于我们要把pos位置往后数据挪动strlen个长度而不是一个长度,所以我们这里的代码大致实现是一样的,首先创建一个变脸len来记录插入字符串的长度,然后再来判断是否需要扩容?这里的判断条件就是_size+len>capacity,如果成立的话我们这里就得进行扩容,将容量扩容成_size+len,然后就是挪数据将pos位置往后的数据全部都往后挪动len个长度:

		string& insert(size_t pos, char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size;
			while (end >= pos)
			{
				_str[end + len] = _str[end];
				end--;
			}
		}

数据挪动完之后我们要干的事情就是将这个字符串的数据拷贝到挪动出来的位置,那么这里的拷贝我们就得用到strcpy函数,目标位置为_size+pos,源字符串位置为_str,拷贝完之后我们就要将_size的值加上len,那么我们的代码就如下:

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

我们来用下面的代码测试一下上面代码的正确性:

void test8()
{
	YCF::string s1("hello world");
	s1.insert(5, "ycf");
	cout << "字符串的内容为:" << s1.c_str() << endl;
}

我们将这个代码运行一下就可以看到这里出了问题:
在这里插入图片描述
这里打印到ycf就不打印了,这是为什么呢?我们知道字符串是以\0来作为结尾,而这里打印到ycf就不打印了说明我们这里在插入数据数据的时候顺带的将源字符串ycf后面的\0也插入进去了,所以要解决这个问题的话我们就不能使用strcpy函数,而得使用strncpy函数,将len的值传给这个函数的第三个参数,那么这里修改后的代码实现就如下:

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

上面的测试代码运行的结果也就正常了:
在这里插入图片描述
但是这里的代码依然不是完美的,因为上面的代码在面对插入位置为0的情况时,依然会发生死循环的所以我们这里依然会有两种不同的解决方案,第一种就是将end的类型改成int,然后在表达式里面进行强制类型转换,那么这里的代码就如下:

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

有了上面的经验有些小伙伴们就说第二种修改的方法我知道怎么干了,将end的值改成_size+1,将循环体内的

_str[end + len] = _str[end];

修改成

_str[end] = _str[end-end];

就完成了第二种修改,其完整的形式如下:

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

然后将下面的测试代码运行一下发现运行结果确实是对的:

void test8()
{
	YCF::string s1("hello world");
	s1.insert(0, "ycf");
	cout << "字符串的内容为:" << s1.c_str() << endl;
}

在这里插入图片描述
我们可以看到这里的打印结果是真确的,但是代码的实现一定是正确的吗?while循环结束的条件是end大于pos,而while的循环体里面干的事情是_str[end] = _str[end-len];假如len的值为6,pos的值为0的话,那end等于1 或者 2 3 4 5 的时候这个循环体里面不就发生越界的行为了嘛,虽然这里不会报错,但是当插入的字符串长度非常长的话这里就会拷贝很多不需要的内容,这不就间接的影响了效率嘛,那为什么会出现这种情况呢?我们可以通过下面的图片来发现一下问题所在:

在这里插入图片描述
首先这是我们的数组,我们想在下标为2的位置插入字符串abcd,那么这里pos的值就为2,len的值就为4,end的值为_size+len也就等于14所以此时这些值对应的位置就如下:
在这里插入图片描述
每次循环都将end-len对应的值复制到end位置上再让end的值减一,那么第一次循环的结果就如下:
在这里插入图片描述
第二次循环的结果如下:
在这里插入图片描述
那么这样不停的循环,当end-len等于pos位置的时候,所有的数据都挪动到了对应的位置:
在这里插入图片描述

但是循环结束的条件是end小于pos,所以这里还会将end的值减一继续执行循环:
在这里插入图片描述
那么从这里开始我们就做了一些无用的拷贝导致了效率的降低,那有些小伙伴就说啊:我们将循环的条件修改一下,当end-len小于pos的时候结束循环:while(end-len>=pos)但是这样修改的话不就又会造成一个问题吗?当pos等0的时候end-len也等于0,而end和len都是无符号整型,所以这个表达式的结果也会是无符号整型,也就不可能出现负数所以该修改方法是不对的,我们要想解决上面的问题右边表达式的结果就不能出现0,所以我们得将目光看向前面的end
在这里插入图片描述
当end等于pos+len的时候他就已经完成了最后一个数据的拷贝,所以当end等于或者小于pos+len-1的时候循环得结束,所以while循环的判断条件就得变为: while (end > pos+len-1)那么完整的代码就如下:

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

我们再来测试一下代码:

void test8()
{
	YCF::string s1("hello world");
	s1.insert(0, "ycf");
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

运行的结果是正常的:
在这里插入图片描述
这里那么这就是我们第二种形式的完整实现,希望大家能够理解。

erase

erase函数的作用就是删除对象中的数据,这里的参数形式就是给定删除的位置以及要从该位置删除多少个字符,所以该函数的声明为:

string& erase(size_t pos, size_t len = npos)

我们这里给len函数一个缺省值,当我们想将pos往后的所有字符全部都删除的话,我们就可以在调用该函数的时候只传一个参数给pos,len参数就可以使用其缺省值,接下来我们就得实现函数体,首先得判断一下这里的删除是往后的部分删除还是往后的全部删除,当len的长度等于pos或者len的长度加上pos的值大于等于字符串的长度的话我们我们就可以认为这里的是往后的全部删除,那么对于这种情况我们的做法就是直接将pos位置的元素变为\0这样在输出内容的时候就不会输出\0往后的内容,然后再将size的值进行修改将其别为pos,下面就是该情况的实现:

		string& erase(size_t pos, size_t len)
		{
			if (pos + len >= _size||len==npos)
			{
				_str[pos] = '\0';
				_size = pos ;
			}
			else
			{

			}
		}

第二种情况就是部分的删除,那么对于这种情况我们要干的第一件事情就是先将后面没有删除的数据向前挪动,我们来看看下面的图:
在这里插入图片描述
假设pos的位置为2,要往后删除3个字符所以这里的2 3 4 都会被删除,将5往后的元素往前挪动,而5的下标为pos+len,那么我们这里就创建两个变量begin和end,将begin的值初始化为pos,将end的值初始化为pos+len,每次循环就将begin对应的值赋值到end对应的值上,然后再将begin和end的值加一,当end的值大于_size的值时我们就结束循环,那么我们这里的代码就如下:

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

当然我们这里的循环用的就非常的麻烦我们可以直接通过strcpy来实现这里循环的功能:

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

我们来看看下面的测试代码:

void test9()
{
	YCF::string s1("hello world");
	s1.erase(2, 3);
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	YCF::string s2("hello world");
	s2.erase(4);
	cout << "字符串的内容为:" << s2.c_str() << endl;
	cout << "字符串的长度为:" << s2.size() << endl;
	cout << "字符串的容量为:" << s2.capacity() << endl;
}

我们将这里的代码运行一下就可以发现这里的代码实现是真确的:
在这里插入图片描述

find

这个函数我们分为两种情况,第一个是在字符串中指定位置中寻找字符,第二种就是在字符串的指定位置中寻找子字符串,我们这里首先来实现第一种情况,该情况的声明如下:

size_t find(char ch,size_t pos=0)

因为这里是寻找字符所以我们可以直接通过循环遍历的方式来实现该函数,代码如下:

size_t find(char ch,size_t pos=0)
{
	assert(pos<_size);
	int i = pos;
	while (i < _size)
	{
		if (_str[i] == ch)
		{
			return i;
		}
		i++;
	}
	return -1;
}

测试代码如下:

void test10()
{
	YCF::string s1("hello world");
	cout << s1.find('o',5)<<endl;
	cout << s1.find('x');
}

运行结果如下:
在这里插入图片描述

第二种形式就是在字符串中寻找子串,该形式的函数声明如下:

		size_t find(char* str ,size_t pos = 0)

该函数的实现其实就用不着我们出手,因为c语言的库中就提供了与该功能相同的函数strstr
在这里插入图片描述
str1是源字符串,str2是我们要查找的字串,所以我们这里就先判断一下pos的位置是否合理,然后我们再调用strstr函数,创建一个变量来接收这个函数的返回值,根据这个变量再来做出具体的判断,strstr函数如果找到了就会返回子字符串第一次出现的位置,如果没有找到就返回空指针,那么我们这里就会用if else语句来对这里做出判断,那么代码就如下:

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

我们的测试代码就如下:

void test10()
{
	YCF::string s1("hello world");
	cout << s1.find('o',5)<<endl;
	cout << s1.find('x')<<endl;
	cout << s1.find("llo");
}

这个代码的运行结果就如下:

在这里插入图片描述
那么这么看的话我们这里的函数实现是正确的。

resize

这个函数功能就是调节对象中字符串的长度,这里我们得分三种情况,第一个是当len的值小于_size的时候,当len的长度大于_size小于_capacity的时候,当len的值大于_capacity的时候,那么当我们需要增长字符串的长度的时候,我们就得往这个字符串中添加字符,那么这个字符可以由操作者提供,当操作者不提供的时候就由库默认提供,那么这里的函数声明就如下:

		void resize(size_t len, char ch = '0')

这里我们可以将上面的三种情况分为两种,因为我们实现的reserve函数它不会进行缩容,所以我们可以将len的值小于_size分为一种,将其他情况分为另一种,那么这里我们就可以使用if else语句,其判断情况如下:

		void resize(size_t len, char ch = '0')
		{
			if (len < _size)
			{

			}
			else
			{

			}
		}

对于len小于_size的情况我们就可以直接将len对应位置的元素赋值为’\0’,然后将_size的值修改为len:

		void resize(size_t len, char ch = '0')
		{
			if (len < _size)
			{
				_str[len] = '\0';
				_size = len;
			}
			else
			{

			}
		}

对于两外的两种情况我们可以先使用reserve函数将其容量修改成len,因为reserve函数不会进行缩容,所以就算len小于_capacity的时候我们也可以使用reserve函数,将容量处理完之后我们就可以使用while循环就刚刚扩容出来的新空间全部初始化为使用者给的字符,然后在字符串的结尾添加一个\0用来结束字符串,最后再将_size 的值赋值为len,下面是对应的代码的实现:

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

下面是该函数的测试代码:

void test11()
{
	YCF::string s1("hello world");
	s1.resize(20, 'x');
	YCF::string s2("hello world");
	s2.resize(4);
	cout << "字符串的内容为:" << s1.c_str() << endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
	cout << "字符串的内容为:" << s2.c_str() << endl;
	cout << "字符串的长度为:" << s2.size() << endl;
	cout << "字符串的容量为:" << s2.capacity() << endl;
}

运行的结果如下:
在这里插入图片描述
那么该函数的实现就是真确的。

[ ]

这里就是对[ ]操作符进行重载用来得到对应位置的数据,并且可以通过该操作符来修改对应的数据,那么这里我们就是通过引用返回的方式来进行实现,其对应的代码如下:

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

我们上面是可读可写的版本,但是为了应对一些其他的情况我们这里还得实现一个其他的版本就是只读的版本,所以我们这里就得添加两个const,其代码如下:

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

clear

这个函数的功能就是将对象的内容全部都清空,但是并不会将容量缩小,所以实现这个函数的时候就可以直接通过修改_size的值来实现将原始的数据清空,那这里的代码就如下:

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

>>

这里我们要实现的就是流提取,这个函数的实现就非常的简单我们直接利用循环遍历的方式将对象中的每个元素全部都打印出来,首先为了符合我们平时的使用规律我们这里得在函数外来实现这个函数,然后在类中使用友元来获取对象中的数据,那么这里我们的函数声明就是这样:

ostream& operator<<(ostream& out, const string& s)

在类中我们也得声明一下这个函数,并且得在声明的开头加一个friend用来表示有缘:

friend ostream& operator<<(ostream& out, const string& s)

然后在函数体里面我们就通过循环和[ ]的形式来获取每一个元素并将其打印出来,那么这里就非常的简单直接通过for循环来实现:

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

我们可以用下面的代码来测试一下:

void test12()
{
	YCF::string s1("hello world");
	cout << s1;
}
int main()
{
	test12();
	return 0;
}

其运行的结果如下:
在这里插入图片描述
那么我们这里的运行结果就是正确的。

>>

对于这个函数的重载我们想要实现的功能就是将原来的内容清空将原来然后再将新的数据插入到对象里面,那么我们这里实现该函数的时候就可以先使用clear函数将内容全部清空,然后再使用while循环和+=将使用者给的内容一个一个的输入到对象里面,因为这里要对参数s进行修改,在函数声明的时候我们不要对参数加上const来进行修饰,所以该函数的声明就如下:

	istream& operator>>(istream& in,  string& s)	

因为该函数需要用到类里面的数据,所以我们这里还得将该函数在类中声明为友元的形式:

	friend istream& operator>>(istream& in,  string& s);

因为我们平时在使用>>的时候他是不会接受空格字符和换行字符的,所以我们这里就可以将这两个字符作为循环结束的条件放到while循环里面,因为库中的>>它本身也不接收空格和换行符,所以这里在接收字符的时候我们就得使用get函数,那么这里我们的代码就如下:

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

我们可以通过下面的代码来测试一下该函数实现的真确性:

void test12()
{
	YCF::string s1("hello world");
	cin >> s1;
	cout << s1<<endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

我们将这段代码运行一下并在输入栏中输入下面的数据:
在这里插入图片描述
按一下回车就可以看到这里输出了这样的数据:
在这里插入图片描述
那么这里就说明我们的函数的实现是正确的没有问题,但是这里有个点需要我们优化一下就是,我们这里是不停的调用+=来插入数据而+=函数是通过push_back函数实现的,使用这个函数会进行扩容,每次都会将容量扩大成两倍,所以当我们要插入一个非常长的字符串的时候,采用这种方式会进行很多次扩容,而不停的扩容会导致效率的降低,所以为了将这里的效率提高一点我们这里得对其进行改进,我们先创建一个数组将这个数组的容量赋值为128,那么每次读取数据的时候我们就先不要使用+=来插入数据而是先将内容放到数组里面,等数组满了之后我们再使用+=函数将数组的内容一下子全部尾插到对象里面,然后将数组清空再将剩余的字符串继续往数组里面插入,这样我们就减少了扩容的次数,从而提高了效率,那么上面的所述的代码第一步如下先创建了数组通过循环将数据插入到数组里面:

	istream& operator>>(istream& in,  string& s)
	{
		s.clear();
		int i = 0;
		char arr[128] = { 0 };
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			arr[i] = ch;
			i++;
			ch = in.get();
		}
	}

然后我们再添加一个if语句,当i的值等于128的时候我们就将数组的内容尾插到对象里面,然后将i的值赋值为0:

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

当这里的循环结束的时候,数组里面还可能会有数据没有被尾插到对象里面,所以在循环结束之后我们还得加一个判断语句,当i大于0的时候我们就得将i对应的位置的元素赋值为\0,然后再将数据尾插到对象里面,那么我们完整的代码就如下:

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

我们来看看下面的测试代码:

void test12()
{
	YCF::string s1("hello world");
	cin >> s1;
	cout << s1<<endl;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}

我们往输入栏中输入多个数据:
在这里插入图片描述
再按一下回车:
在这里插入图片描述
我们这里的输入输出的值是一样的,所以这里我们的函数实现就是对的没有问题。

新式拷贝构造函数

我们首先来看看原来的拷贝构造函数的写法:

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

这种写法就是先申请一个动态的空间,再将_capacity和_size的值赋值为对象s中的_capacity和_size的值,然后再将对象s中的数据拷贝到申请的动态空间里面,那么这是传统写法,我们的现代写法就是利用构造函数先用传过来的对象创建一个临时对象,再用swap函数将本对象的数据与临时对象的数据进行一下交换,这样在该函数结束的时候调用析构函数,就不会将有用的数据进行删除,但是为了保证它不会删除其他的随机数据我们得先使用初始化列表给本对象一个空值,避免析构出错,那么这里的代码就如下:

		string(const string& s)
			:_str(nullptr)
			, _capacity(0)
			, _size(0)
		{
			string tmp(s._str);						
			swap(_str, tmp._str);
			swap(_capacity, tmp._capacity);
			swap(_size, tmp._size);
		}

我们可以用下面的代码测试一下:

void test13()
{
	YCF::string s1("hello world");
	YCF::string s2(s1);
	cout << s2;
	cout << "字符串的长度为:" << s1.size() << endl;
	cout << "字符串的容量为:" << s1.capacity() << endl;
}
}

我们将这个代码运行一下看一下这个代码执行的结果为:
在这里插入图片描述
那么这个代码的运行结果就是对的,当然我们这里的使用了三次swap看上去有点臃肿,我们可以将这三个swap写到我们自己创建的一个swap函数里面比如说这样:

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

这里在使用swap函数的时候得在前面加上一个std用来指明用的哪个命名空间的swap函数以免发生命名冲突,但是我们这里用了三次swap函数交换了不同的值,有小伙伴知道库中的swap函数是可以直接交换类的对象的,但是库中的swap是这样实现的:

template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

我们调用swap函数传过去的是一个string的对象的话,他这里就会进行三次深拷贝,一次拷贝构造,两次赋值重载,所以这样使用会大大的降低使用的效率,而且我们是在拷贝构造函数调用的swap函数,在swap函数里面他又会调用该对象的拷贝构造这不就矛盾了,所以得分三次写那么完整的代码就是这样:

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

新式赋值重载

有了上面的经验我们这里的赋值重载也可以采用同样的方法来进行升级,首先使用拷贝构造创建一个临时对象,然后将临时对象的数据与你要赋值的对象的数据进行交换,这样就完成了赋值重载,那么这里的代码就是这样:

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

测试的代码就是这样:

void test14()
{
	YCF::string s1("hello world");
	YCF::string s2;
	s2 = s1;
	cout << s2;
}

代码运行的结果如下:
在这里插入图片描述
这样我们这里的代码实现就是正确的,当然我们这里可以再进行一次简化,我们在这里可以不使用引用而是直接用形参,因为实参在传递给形参的时候会经历一次临时拷贝,调用的也是拷贝构造,所以我们这里就可以再进一步简化,其代码如下:

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

上面就是我们的全部内容,希望大家能够理解。

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

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

相关文章

Vue的双向绑定(数据劫持)

双向绑定所谓的双向绑定其实就是&#xff0c;ui或者数据有一方做了修改&#xff0c;那么另外一个也会随着改变。简单来说&#xff0c;视图驱动数据&#xff0c;同时数据也能驱动视图。视图驱动数据&#xff0c;只需要绑定事件。数据驱动视图&#xff0c;则需要去对数据做监听&a…

DC-DC PCB layout经验-含走线宽度和载流量表格

在DC-DC芯片的应用设计中&#xff0c;PCB布板是否合理对于芯片能否表现出其最优性能有着至关重要的影响。不合理的PCB布板会造成芯片性能变差如线性度下降&#xff08;包括输入线性度以及输出线性度&#xff09;、带载能力下降、工作不稳定、EMI辐射增加、输出噪声增加等&#…

不同Nodejs版本的TypeScript的建议配置

Node Target Mapping microsoft/TypeScript Wiki GitHubTypeScript is a superset of JavaScript that compiles to clean JavaScript output. - Node Target Mapping microsoft/TypeScript Wikihttps://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping以上是tsc…

SpringBoot+Vue项目知识管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

macOS 安装 Frama-C 及使用

操作系统&#xff1a;macOS 12.6 Monterey 官网安装指导&#xff1a;Get Frama-C 一、操作与避坑 &#x1f573;️ 1、macOS 包管理绕不开 Homebrew 工具&#xff0c;确保安装好。 2、安装 Frama-C 的必要依赖 brew install opam gmp gtk gtksourceview libgnomecanvas在安装…

MATLAB-最大值与最小值

在MATLAB中&#xff0c;用于计算最大值的函数是max函数&#xff0c;用于计算最小值的函数是min函数&#xff0c;其调用格式如下。Bmax(A) %计算最大值 &#xff0c;若A为向量&#xff0c;则计算并返回向量中的最大值;若A为矩阵&#xff0c;则计算并返回%一个含有各列最大值的行…

从0到1完成一个Vue后台管理项目(九、引入Breadcrumb面包屑,更改bug)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

ansible(第四天)

三&#xff1a;编写playbook 1.Ansible playbook 临时命令可以作为一次性对一组主机运行简单的任务。不过&#xff0c;若要真正发挥Ansible的力量&#xff0c;需要了解如 何使用playbook可以轻松重复的方式对一组主机执行多项复杂的任务。 play是针对对清单中选定的主机运行…

汽车电子系统网络安全组织管理

声明 本文是学习GB-T 38628-2020 信息安全技术 汽车电子系统网络安全指南. 下载地址 http://github5.com/view/764而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 汽车电子系统网络安全组织管理 6.1 组织机构设置 组织应高度重视网络安全&#xff0c…

基于Prometheus+Grafana搭建监控平台(Windows/Linux环境exporter部署)

1. 介绍 1.1 Prometheus是什么?Prometheus&#xff08;普罗米修斯&#xff09;是一个最初在SoundCloud上构建的监控系统。自2012年成为社区开源项目&#xff0c;拥有非常活跃的开发人员和用户社区。为强调开源及独立维护&#xff0c;Prometheus于2016年加入云原生云计算基金会…

【从零开始学习深度学习】43. 算法优化之Adam算法【RMSProp算法与动量法的结合】介绍及其Pytorch实现

Adam算法是在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均 【可以看做是RMSProp算法与动量法的结合】。 目录1. Adam算法介绍2. 从零实现Adam算法3. Pytorch简洁实现Adam算法--optim.Adam总结1. Adam算法介绍 Adam算法使用了动量变量vt\boldsymbol{v}_tvt​和RMS…

LVGL官方UI设计软件——SquareLine Studio micropython 使用简单测评

经常去LVGL官网逛的人一定都知道这个软件&#xff0c;作为官方的亲儿子&#xff0c;使用体验如何呢&#xff0c;我简单体验了一周左右&#xff0c;简单做个测评&#xff0c;本测评仅代表我个人意见&#xff0c;并且仅限micropython的使用体验&#xff01; 首先是价格&#xff0…

TCP报文段(segment)首部格式

TCP传给IP的数据单元称作TCP报文段或简称为TCP段&#xff08;TCP segment&#xff09;。 IP传给链路层的数据单元称作IP数据报(IP datagram)。 通过以太网传输的比特流称作帧(Frame)。 逐层封装&#xff1a; 源端口号发送端端口号&#xff0c;字段长16位&#xff08;2字节&…

计算机网络第二章

物理层的基本概念物理层的作用&#xff1a;物理层解决如何在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。物理层的主要任务&#xff1a;确定与传输媒体接口有关的一些特性 &#x1f51c;本质&#xff1a;定义一些固定标准物理层的四大特性&a…

Word怎么转PDF?8个Word转PDF工具分析

Word 到 PDF 转换工具是用于将 Microsoft Word&#xff08;DOC 或 DOCX&#xff09;文档转换为 PDF 格式的程序。根据操作模式&#xff0c;它可以是在线或离线软件。当然&#xff0c;考虑到市场上充斥着此类工具&#xff0c;获得最好的 DOCX 到 PDF 转换器可能会让人头疼。正是…

MySQL基础篇第12章(MySQL数据类型)

1. MySQL中的数据类型 常见的数据类型的属性&#xff1a; 2. 整数介绍 2.1 类型介绍 整数类型一共有 5 种&#xff0c;包括 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT。 它们的区别如下表所示&#xff1a; 2.2 可选属性 整数类型的可选…

javaweb-异步请求AjaxaxiosJSON

1&#xff0c;Ajax 1.1 概述 AJAX (Asynchronous JavaScript And XML)&#xff1a;异步的 JavaScript 和 XML。 我们先来说概念中的 JavaScript 和 XML&#xff0c;JavaScript 表明该技术和前端相关&#xff1b;XML 是指以此进行数据交换。而这两个我们之前都学习过。 ####…

JavaWeb基础——从入门到超神(笔记,持续更新)

day00综述 需要学习SpringBoot&#xff0c;但是JavaWeb是基础&#xff0c;来补一下 JavaWeb就是将数据库中的数据用好看的样式在网页上呈现出来 day01MySQL基础 接下来就是MySQL的安装什么的 mysqld --initialize-insecure mysqld -install net start mysql至此我的电脑上已…

【蓝桥杯-筑基篇】基础入门

&#x1f353;系列专栏:蓝桥杯 &#x1f349;个人主页:个人主页 目录 1.数位翻转 2.三个数求最大值的写法 3.两数交换的几种方法 4.身份证第18位合法性校验 5.黑洞数&#xff08;陷阱数&#xff09; 1.数位翻转 如: 整数 12345 返回结果为整数: 54321 当第一次看到这个题…

【零基础】学python数据结构与算法笔记7

文章目录前言41.查找排序部分习题42.查找排序习题143.查找排序习题244.查找排序习题345.查找排序习题4总结前言 学习python数据结构与算法&#xff0c;学习常用的算法&#xff0c; b站学习链接 41.查找排序部分习题 选题部分来自leetcode 42.查找排序习题1 242. 有效的…