10.模拟实现s

news2024/10/11 20:33:46

前面我们了解了string类的常用接口使用,那么现在就来模拟实现一下。

1.constructor

string.h

namespace Ro
{
	class string
	{
	public:
		string()
		{

		}
		string(const char* str)
		{

		}
		~string()
		{

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

为了和库里面的string进行区分,我们使用命名空间将它包含进来。

1.1构造函数和析构函数

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

大家可以看一下这样写有没有什么问题呢?

首先我们来看一下带参构造,这样写感到会太麻烦了,每次都要调用strlen,我们干脆就不写初始化列表,虽然初始化列表不写也会走,但是我们没写的话我们的成员就不会初始化,之前有讲过在语法理解上初始化列表是成员变量定义的地方,我们定义的成员变量并不是引用成员变量和const成员变量,所以可以不初始化

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

注意这里多开一个空间是留给\0的

然后再看一下我们的无参构造,其实这里是有问题的,我们可以验证一下。

不过在这之前,我们先把c_str()这个接口实现一下,方便我们打印出来查看,因为流插入和流提取我们还没有重载实现

这还是很好实现的,直接返回_str就行

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

我们测试一下:

void test_string1()
{
	string s1;
	string s2("hello world");

	cout << s1.c_str() << endl;
	cout << s2.c_str() << endl;
}

这里直接就崩掉了,我们先把s1屏蔽,测试s2看一下

没崩,我们再来看看库里面的是什么样的

void test_string1()
{
	std::string s1;
	std::string s2("hello world");

	cout << s1.c_str() << endl;
	cout << s2.c_str() << endl;
}

库里面的string是可以的,说明我们无参构造是有问题的

再来看一下我们的无参构造是这么写的

string()
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{}

这里直接将_str 初始化为空指针,那我们通过s1.c_str()就会对空指针解引用从而引发空指针问题,所以这里我们不能初始化为空指针,我们直接将它初始化为\0不就行了吗

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

再来测试一下:

可以了,那干脆直接将无参和带参合并在一起,给个缺省参数。

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

没有问题

1.2拷贝构造和赋值重载

void test_string1()
{
	/*string s1;
	string s2("hello world");*/
	string s1("hello world");
	string s2(s1);
	cout << s1.c_str() << endl;
	cout << s2.c_str() << endl;
}

如果我们不写,直接使用编译器自己默认的呢

来测试一下:

直接崩了,这是为什么呢?是因为这里编译器自己默认的拷贝构造是浅拷贝

那浅拷贝是什么?又为什么不行呢?

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致
多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该
资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给
出。一般情况都是按照深拷贝方式提供。
所以需要我们自己来实现深拷贝
string(const string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

此时再来测试一下:

同样再把赋值重载实现出来

string& operator=(const string& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}
void test_string1()
{
	/*string s1;
	string s2("hello world");*/
	string s1("hello world");
	string s2(s1);
	string s3("xxxxxx");

	s3 = s1;
	cout << s1.c_str() << endl;
	cout << s2.c_str() << endl;
	cout << s3.c_str() << endl;
}

测试一下:

2.string类的容量接口

2.1 size(),capacity()和empty()

size_t size() const
{
	return _size;
}
size_t capacity() const
{
	return _capacity;
}
bool empty() const
{
	return _size == 0;
}

这几个接口都非常简单,就不多说,直接上代码

2.2 reserve()

扩容操作,一般不会缩容

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

这就不测试了,还是比较简单的,后面会用到

2.3 resize()

库里面有两个,一个是n<size,那么就只需要把size缩到n就行,其他数据不变,另一个就是n>size(此时如果n>capacity就还需要扩容),然后就将[size, n]之间的数据初始化为c。

不过我们将两者合并在一起,给个缺省值\0(缺省值不能声明和定义的时候都出现,所以我们在声明的头文件中给)

void string::resize(size_t n, char ch)
{
	if (n <= _size)
	{
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		if (n > _capacity)
		{
			reserve(n > 2 * _capacity ? n : 2 * _capacity);
		}
		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\0';
	}
}

如果n<=size,我们直接将数据截断,给个\0就行,没必要把数据都给删除。

如果n>size,判断是否需要扩容,然后再初始化数据。(不过最后不要忘记加\0)

测试一下:

void test_string2()
{
	string s("hello world");
	cout << s.c_str() << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.resize(20, 'x');
	cout << s.c_str() << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.resize(0);
	cout << s.c_str() << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

2.4 clear()

clear()清空有效字符,直接将所有数据截断,在下标0处的数据修改为\0,容量不变

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

这里同样比较简单就不测试了

3.string类的访问和遍历

3.1访问

通过下标访问元素,一个可读可写,还有一个可读不可写

char& operator[](size_t index)
{
	return _str[index];
}
const char& operator[](size_t index) const
{
	return _str[index];
}

也比较简单,不过这个待会可以和遍历一起测试。

3.2迭代器实现遍历

之前有提到迭代器是一个像指针的东西,因为迭代器要模仿指针的行为,虽然在这里它确实是一个指针,但是在其他容器中,比如在list中,它就不是一个指针,而是通过封装来实现对指针行为的模仿,在list中,对迭代器++,--的操作,来让它指向下一个或者上一个节点,但是在链表中,直接对指针++,--是没有用的,因为链表不是一块连续的空间,但迭代器却能实现这种操作,所以说它不一定是指针,而是一个像指针的东西

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;
}

这里实现了begin和end迭代器,同样也有可读可写和可读不可写两种,所以这里有一个iterator(可读可写)和const_iterator(可读不可写)

测试一下:

void test_string3()
{
	string s("hello world");
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
	for (char& c : s)
	{
		c++;
		cout << c << ' ';
	}
	cout << endl;
}

这里范围for我们没有实现就能用,这是因为范围for的底层就是迭代器,所以只要实现了的迭代器,范围for就可以使用。

4.string类对象的修改操作接口

4.1 push_back()

尾插一个字符

void string::push_back(char c)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	_str[_size] = c;
	_size++;
	_str[_size] = '\0';
}

先判断是否需要扩容,然后再插入数据,最后不要忘记\0

测试一下:

void test_string4()
{
	string s("hello world");
	cout << s.capacity() << endl;
	s.push_back('x');
	s.push_back('x');
	s.push_back('x');
	s.push_back('x');
	s.push_back('x');
	s.push_back('x');
	cout << s.c_str() << endl;
	cout << s.capacity() << endl;
}

4.2 append()

尾插一个字符串

void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	strcpy(_str + _size, str);
	_size = _size + len;
}

和push_back的步骤是一样的。

测试一下:

string s("hello world");
cout << s.capacity() << endl;
s.append("xxxxxxxxxxx");
cout << s.c_str() << endl;
cout << s.capacity() << endl;

4.3 operator+=

我们不需要去实现,直接复用push_back和append

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

这里就不测试了。

4.4 insert()

在pos位置插入一个字符

string& string::insert(size_t pos, char c)
{
	assert(pos <= _size);

	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

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

	_str[pos] = c;
	++_size;
	return *this;
}

操作和上面类似,只不过需要挪动数据。

其实这里是有bug的,我们可以来测试一下看看

void test_string5()
{
	string s1("hello world");
	s1.insert(5, 'c');
	cout << s1.c_str() << endl;
	s1.insert(0, 'c');
	cout << s1.c_str() << endl;
}

我们可以看到第二个测试崩掉了,说明在边界情况是有问题的

其实是因为pos为0时,由于end是size_t(无符号整型),end为0之后再--的话就是整型的最大值,不会比0小,那么发现问题后应该怎么修改呢?将end改为整型吗?我们来试试看

还是崩了,为什么呢?这是由于C语言遗留的问题,一个整型end和无符号整型pos比较的话,会发生什么?没错就是类型提升,整型会提升为无符号整型,这样一来就又变成了和上面一样的问题。

那应该怎么做呢?将pos强制转换为int吗?试一下

确实可以,但是我们这里推荐另一种方法。

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

运行也没问题。

再来实现一个在pos位置插入一个字符串

string& string::insert(size_t pos, const char* s)
{
	assert(pos <= _size);
	size_t len = strlen(s);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}

	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = s[i];
	}
	_size += len;
	return *this;
}

和pos位置插入字符一样的操作,不过要注意将字符串s拷贝过来的时候注意不能将\0也拷过来,所以这里如果要使用拷贝函数的话需要注意不能使用strcpy,而是使用strncpy。

测试一下:

4.5 erase()

我们来实现一下库里面的第一个,删除pos位置len个字符,不给参数的话,默认删除全部有效字符,只给第一个参数的话,默认删除pos位置后的所有字符

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

在成员变量中给可以直接给静态常量npos初始化

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

还记得之前讲到静态成员变量的时候,有说过静态成员要在类外初始化,那为什么这里可以在类里面初始化呢?

在 C++中, static const 成员变量可以在类的内部声明并初始化,尤其是当类型为整数类型且初始化值为常量表达式时。
 
对于 static const size_t npos = -1 , size_t 通常是无符号整数类型,而  -1 在这种情况下会被自动转换为该无符号类型的最大值。这样定义的目的通常是为了表示一个特殊的、无效的位置或状态。
 
这样定义在成员变量里可以方便在整个类的范围内使用这个特殊的值,并且由于是 static const ,它在内存中只有一份实例,不会因为多个类对象的存在而重复占用空间。同时,在编译期间就可以确定其值,提高了程序的效率。

不过也可以在类外初始化,这里只是提到一下。

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

如果len >= _size-pos的话,说明pos位置后的字符都要删除,那我们直接将\0放在pos位置上就可以了,否则就是删除pos位置后len个字符,但后面还有字符,这个时候需要挪动字符。

测试一下:

void test_string6()
{
	string s1("hello world");
	s1.erase();
	cout << s1.c_str() << endl;
	string s2("hello world");
	s2.erase(0, 5);
	cout << s2.c_str() << endl;
	string s3("hello world");
	s3.erase(5);
	cout << s3.c_str() << endl;
}

没有问题

4.6 find()

这里来模拟实现(2)(4)接口

size_t find(char c, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;

注意:find是找第一次出现的位置

size_t string::find(char c, size_t pos) const
{
	assert(pos < _size);
	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return npos;
}

找到了返回下标,没找到返回npos。

找字符串的话我们这里可以用到一个函数

返回str2在str1中第一次出现的地址,否则就返回空指针,这样可以直接达成我们想要完成的步骤

size_t string::find(const char* s, size_t pos = 0) const
{
	assert(pos < _size);
	const char* ptr = strstr(_str + pos, s);
	if (ptr)
	{
		return ptr - _str;
	}
	return npos;
}

如果ptr不为空指针,说明找到了,直接将ptr-_str就是它此时所在的下标,为空说明没找到,就返回npos。

这里我们就不测试了

5.relational operators

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

直接利用字符串比较函数strcmp,然后其他的复用。

非常简单,就不一一说明了。

6.传统写法和现代写法

这里我们来介绍一下拷贝构造和赋值重载的传统写法和现代写法

传统写法就是我们之前的写法

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

老老实实的自己去深拷贝

但是在现代写法中我们可以利用一个工具人来给我们打工

拷贝构造的现代写法

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

这里利用tmp来调用构造函数,构造一个拥有和s对象一样内容的对象(注意这里使用s对象的字符串去构造,但他们不是指向同一块空间,他们两个指向不同的空间,不过存放的数据是一样的),这时让tmp成为我们的工具人,把tmp刚构造好的东西占为己有(利用swap交换给我们自己)。

不过我们自己的_str需要初始化为空指针,不然不初始化的话就是一个随机值,把随机值换给tmp后,拷贝构造结束后,tmp的生命周期也结束了,调用析构函数销毁时,你是一个随机值,指向的是一个随机的地址,但是这个随机地址我们并没有开辟空间,此时我们去给它释放空间,就会崩溃

既然我们需要交换,不如直接写一个交换函数

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

再把拷贝构造也改一下

string(const string& s)
	:_str(nullptr)
{
	string tmp(s._str);
	Swap(tmp);
}

赋值构造的现代写法

这里也是一样的利用swap来达到我们想要的效果

string& operator=(const string& s)
{
	string tmp(s._str);
	Swap(tmp);
	return *this;
}

这样一看,swap用起来更加简洁

7.流插入和流提取

流插入

ostream& operator<<(ostream& _cout, const string& s)
{
	for (auto c : s)
	{
		_cout << c;
	}
	return _cout;
}

使用范围for一个一个的输入到流中

测试一下

void test_string7()
{
	string s("hello world");
	cout << s << endl;
}

流提取

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

我们先要将s对象中的有效数据清除,我们知道库里面的流提取是会覆盖之前的数据的,如果这里不清除之前的有效数据的话,会造成s中原来的数据加上我们输入的数据,这就与库里面的不一样了。

这里我们使用get函数从输入流中读取单个字符,然后让s += c来实现

但是我们仔细看一下会发现,每次让s += c的话效率是不是会很低,为什么呢?如果这是一个刚定义的string类对象s,我们在实现构造函数的时候是将_capacity初始化为0的,那我们每次让s += c的话都需要扩容,扩容的效率很低,所以效率整体有点低。

那我们可以改进一下,为了减少扩容,那应该怎么做?是不是可以给一个适当大小的数组,每次从流中读取到的字符存入到数组中,当数组存满时再让s += 数组,这样就可以减少扩容的频率了。

istream& operator>>(istream& _cin, string& s)
{
	s.clear();
	const int N = 256;
	char buff[N];
	int i = 0;
	char c = _cin.get();
	while (c != '\n')
	{
		buff[i++] = c;
		if (i == N - 1)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		c = _cin.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return _cin;
}

测试一下:

没有问题,这样的话效率也能的到提升

今天的内容就到这里了

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

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

相关文章

Jenkins+kubernetes流水线构建java项目

在传统的业务环境中&#xff0c;我们的应用部署或者更新都是采用手工的方式&#xff0c;但是在企业内部&#xff0c;应用架构一般都采用微服务&#xff0c;大部分项目都会对应几十个、上百甚至上千个微服务&#xff0c;并且还不仅仅只有一个项目&#xff0c;所以采用收工方式上…

godot帧同步-关于“显示与逻辑分离”

很多教程说帧同步的关键是“显示与逻辑分离”&#xff0c;但是又没有具体讲解&#xff0c;我起初也没有搞懂这句话的意思&#xff0c;就直接上手开发帧同步了。在开发的过程中&#xff0c;一下子就悟了&#xff0c;所以分享一下。 显示与逻辑未分离&#xff08;单机&#xff0…

嵌入式中单链表基本实现

第一:单链表基本原理 依次读入表L=(a0,.....,an-1)中每一元素ai(假设为整型),若ai≠结束符(-1),则为ai创建一结点,然后插入表尾,最后返回链表的头结点指针H。 第二:单链表具体实现方法 1:实现单链表的时候,需要先定义基本文件link.h #ifndef __LINKLIST_H__ #define…

考华为认证拼了命,怎么还是没工作啊

在当今竞争激烈的就业市场中&#xff0c;网络工程领域的发展备受关注。当你疯狂地在某 BOSS 或者某联等招聘平台上浏览时&#xff0c;你必然会惊讶地发现&#xff0c;华为认证已赫然成为网络方向至关重要的资格认证之一&#xff0c;频繁地出现在形形色色的岗位 JD 里。 这一现…

如何设置 GitLab 密码长度?

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 60天专业…

【RabbitMQ——消息应答机制——分布式事务解决方式】

1. RabbitMQ高级-消息确认机制的配置 NONE值是禁用发布确认模式&#xff0c;是默认值 CORRELATED值是发布消息成功到交换器后会触发回调方法&#xff0c;如1示例SIMPLE值经测试有两种效果&#xff0c;其一效果和CORRELATED值一样会触发回调方法&#xff0c;其二在发布消息成功…

UE5 TimeLine入门

UE5 TimeLine入门 时间轴曲线 共计三个关键帧&#xff08;0,0&#xff09;(1.5,10) (3,0) 蓝图 1.按下空格键执行。 2.时间轴TimeLine函数。 3.动画播放结束后执行。 4.每一帧都执行。

GR-ConvNet论文 学习笔记

GR-ConvNet 文章目录 GR-ConvNet前言一、引言二、相关研究三、问题阐述四、方法A.推理模块B.控制模块C.模型结构D.训练方法E.损失函数 五、评估A.数据集B.抓取评判标准 六、实验A.设置B.家庭测试物体C.对抗性测试物体D.混合物体 七、结果A.康奈尔数据集B.Jacquard数据集C.抓取新…

Java—继承性与多态性

目录 一、this关键字 1. 理解this 2. this练习 二、继承性 2.1 继承性的理解 2.1.1 多层继承 2.2 继承性的使用练习 2.2.1 练习1 2.2.2 练习2 2.3 方法的重写 2.4 super关键字 2.4.1 子类对象实例化 三、多态性 3.1 多态性的理解 3.2 向下转型与多态练习 四、Ob…

使用node.js控制CMD命令——修改本机IP地址

设置每次打开cmd命令行窗口都是以管理员身份运行&#xff1a; 1. 按下Ctrl Shift Esc键组合&#xff0c;打开任务管理器。 2. 在任务管理器中&#xff0c;点击“文件”菜单&#xff0c;选择“运行新任务”。 3. 在“创建新任务”对话框中&#xff0c;输入cmd&#xff0c;勾…

无人机之信息管理系统篇

一、系统概述 无人机信息管理系统通过整合软件和硬件设备&#xff0c;实现对无人机的全面监控、管理、调度和数据分析。它能够帮助用户实时掌握无人机的飞行状态、位置信息等重要数据&#xff0c;确保飞行安全和隐私保护。 二、系统组成 无人机信息管理系统通常由以下几个关键…

达梦8-SQL日志配置与分析工具

以 dmsql_数据库实例名.log 类型命名的文件为跟踪日志文件&#xff0c;跟踪日志内容包含系统各会话执行的 SQL 语句、参数信息、错误信息等。跟踪日志主要用于分析错误和分析性能问题&#xff0c;比如&#xff0c;可以挑出系统现在执行速度较慢的 SQL 语句&#xff0c;进而对其…

React学习过程(持续更新......)

React学习过程&#xff08;持续更新…&#xff09; 创建react的hello项目 使用node创建create-react-app脚手架项目 //首先你得先安装node&#xff0c;这里不做详细教程&#xff0c;我使用的node为20.18.0 npm isntall create-react-app -g //全局安装create-react-app crea…

Web安全常用工具 (持续更新)

前言 本文虽然是讲web相关工具&#xff0c;但在在安全领域&#xff0c;没有人是先精通工具&#xff0c;再上手做事的。鉴于web领域繁杂戎多的知识点&#xff08;工具是学不完的&#xff0c;哭&#xff09;&#xff0c;如果你在本文的学习过程中遇到没有学过的知识点&#xff0…

【笔记】Day2.3.2数据校验

此项目中有两种数据校验方式 1.hibernate-validated注解方式 在controller头上开启数据校验模式需要加入Validated 然后就可以 在参数前面加入任意的数据校验里的注解 例如;:NotNull() NotEmpty()等 面对字符串型的数据校验 参数前可以使用NotBlank()等 而面对对象/DTO实体的…

mongodb GUI工具(NoSQLBooster)

介绍 跨平台的MongoDB GUI工具&#xff0c;支持Windows、macOS和Linux。自带服务器监控工具、Visual Explain Plan、查询构建器、SQL查询等功能。提供免费版本&#xff0c;但功能相比付费版本有所限制。 免费版可供个人/商业使用&#xff0c;但功能有限。 安装成功后&#x…

让你的Mac电脑风扇工作起来,能够控制风扇的实用小工具

不知道你们有没有这个苦恼&#xff0c;Mac电脑明明自带散热风扇&#xff0c;但是很少工作&#xff0c;所以总是会有发热的问题&#xff0c;虽然电脑支架能够一定程度解决热量无法散出的问题&#xff0c;但是总归是不如风扇工作散热的效果好 那么如何让你的Mac风扇工作起来呢&a…

UE4 材质学习笔记08(雨滴流淌着色器/雨水涟漪着色器)

一.雨滴流淌着色器 法线贴图在红色通道和绿色通道上&#xff0c;那是法线的X轴和Y轴&#xff0c;在蓝色通道中 我有个用于雨滴流淌的蒙版&#xff0c;在Alpha通道中&#xff0c;有个时间偏移蒙版。这些贴图都是可以在PS上制作做来的&#xff0c;雨滴流淌图可以直接用笔刷画出来…

ModelMapper的常见用法 ,号称是beanUtils.copyProp....的升级版??,代码复制粘贴即可复现效果,so convenient

官网案例 以下将官网案例做一个解释 1&#xff09;快速入门 递归遍历源对象的属性拷贝给目标对象 拷贝对象下对象的属性值 Data class Order {private Customer customer;private Address billingAddress; }Data class Customer {private Name name; }Data class Name {pr…

ubuntu24 finalshell 无法连接ubuntu服务器, 客户端无法连接ubuntu, 无法远程连接ubuntu。

场景&#xff1a; 虚拟机新创建一个最小化的ubuntu服务器&#xff0c;使用finalshell连接服务&#xff0c;发现连接不上。 1. 查看防火墙ufw 是否开启&#xff0c;22端口是否放行 2. 查看是否安装openssh server, 并配置 我的问题是安装了openssh server 但是没有配置root可…