【C++标准库】模拟实现string类

news2024/9/20 20:23:46

模拟实现string类

  • 一.命名空间与类成员变量
  • 二.构造函数
    • 1.无参(默认)构造
    • 2.有参构造
    • 3.兼容无参和有参构造
    • 4.拷贝构造
      • 1.传统写法
      • 2.现代写法
  • 三.析构函数
  • 四.string类对象的容量操作
    • 1.size
    • 2.capacity
    • 3.clear
    • 4.empty
    • 5.reserve
    • 6.resize
  • 五.string类对象的访问及遍历操作
    • 1.operator[]
    • 2.实现迭代器:begin+end
  • 六.string类对象的增删查改操作
    • 1.operator=
      • 1.传统写法
      • 2.现代写法
    • 2.push_back
    • 3.append
    • 4.operator+=
    • 5.insert
    • 6.erase
    • 7.find
    • 8.substr
    • 9.c_str
    • 10.swap
  • 七.非成员函数
    • 1.string比较函数
    • 2.流插入与流提取
    • 3.getline

一.命名空间与类成员变量

  根据string的结构,显然可知string实质就是字符数组,但有一点区别就是,string可以扩容,再类比动态顺序表,就不难得出string的成员变量。在模拟实现string时,为了与C++标准库中的string作区分,可以给定命名空间。

成员变量:

  1. char* str:指向string第一个字符的指针。
  2. size_t size:string中有效数据的个数。
  3. size_t capacity:string可以存放有效数据的容量。
  4. static const size_t npos:静态成员。

大体结构如下:

namespace xzy
{
	class string
	{
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

		static const size_t npos; //静态成员类内声明
	};
	const size_t string::npos = -1; //类外初始化
}

二.构造函数

class string
{
public:
	string()
		:_str(nullptr)
		,_size(0)
		,_capacity(0)
	{}
	
	string(const char* str)
		:_size(strlen(str))
		, _capacity(_size)
		,_str(new char[_capacity + 1])
	{}
	
	const char* c_str() const
	{
		return _str;
	}
	
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

在这里插入图片描述
在这里插入图片描述

  1. 第一种由于将_str初始化为nullptr,通过C语言中的返回_str直到遇到’\0’停止打印字符串的方法,而_str为nullptr,打印nullptr导致程序崩溃。
  2. 第二种看似程序正常但真的是正确的吗?其实:初始化列表出现的顺序,并不是初始化的顺序,而是按照成员变量声明的顺序初始化成员变量,先初始化_str,而_capacity是随机值,导致开辟的空间不确定,导致出现错误。

正确的方法如下:

1.无参(默认)构造

 由于string默认含有’\0’,可以提前开辟一个’\0’,而’\0’不是有效的数据,也不算入容量之中。

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

2.有参构造

 注意:容量中不包含’\0’,而string中有包含’\0’,所以在开辟空间时要加上一个’\0’的空间。

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

3.兼容无参和有参构造

string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}
  1. 不传参时:用缺省值,str为空的常量字符串,strlen(str)为0,且sizeof(str)为1,含有一个隐藏的’\0’,刚好满足无参构造。
  2. 传参时:就用实参,满足有参构造。

4.拷贝构造

string(const string& str)
{
	_str = str._str;
	_size = str._size;
	_capacity = str._capacity;
}
int main()
{
	xzy::string s1;
	xzy::string s2(s1);

	return 0;
}

在这里插入图片描述

分析:当我们未提供拷贝构造时,编译器会提供拷贝构造,进行简单的值拷贝(浅拷贝),正如以上代码。但是存在很大的漏洞,s1的_str与s2的_str指向堆区同一块空间,程序结束时分别调用各自的析构函数,从而对同一块空间释放两次,这是未定义行为,导致程序崩溃。

1.传统写法

思路:先开空间,再利用strcpy拷贝,最后修改有效数据大小与容量。

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

2.现代写法

构造一个临时对象,进行交换。

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

string(const string& str)
{
	string tmp(str._str);
	swap(tmp);
}

在这里插入图片描述

注意:由于没有初始化列表,不确定s2_str被初始化为nullptr,取决于编译器,可以在类成员变量声明时加上缺省值,确保s2.str为nullptr,而避免s2._str为随机值,交换给tmp变成野指针,函数结束时tmp调用析构函数释放不合法的空间导致程序崩溃。

class string
{
private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
};

三.析构函数

 _str是在堆区开辟的空间,要用delete[]释放空间,否则造成内存泄漏。

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

四.string类对象的容量操作

1.size

size_t size() const
{
	return _size;
}

2.capacity

size_t capacity() const
{
	return _capacity;
}

3.clear

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

4.empty

5.reserve

扩容时:先开辟新空间,千万记得多开一个空间保存’\0’,再将旧空间拷贝到空间,释放旧空间,修改_str指向新空间,最后修改容量。学了C++,new就取代realloc了。

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

6.resize

修改有效数据的个数时:先比较修改后的有效数据与原有数据的大小,若小于则修改_size,若大于再比较容量与修改后的有效数据的大小,判断是否扩容,利用memset函数初始化。

void string::resize(size_t n, char c)
{
	if (n > _size)
	{
		// 如果newSize大于底层空间大小,则需要重新开辟空间
		if (n > _capacity)
		{
			reserve(n);
		}
		memset(_str + _size, c, n - _size);
	}
	_size = n;
	_str[n] = '\0';
}

五.string类对象的访问及遍历操作

1.operator[]

char& operator[](int pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

const char& operator[](int pos) const
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}
  1. 提供两个版本的operator[]:普通重载[]与const修饰的重载[]。若初始化一个常量字符串时:const string s(“123”); 由于存在权放大问题,就无法调用普通重载[],而const修饰的重载[]就可以使用。

  2. 重载operator[],本质就是函数重载,而函数的返回值是不支持函数重载条件的,为了让两个operator[]满足函数重载的条件,可以const随便修饰一个成员函数。隐藏了this指针,实际const修饰的是this所指的对象。

  3. 第一个函数的参数列表的第一个位置隐藏了string* const this;第二个函数的参数列表的第一个位置隐藏了const string* const this;函数的参数不同就满足了函数重载的条件,可以共存。

2.实现迭代器:begin+end

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

为了与标准库里的类似,重定义char* 为iterator。同理提供两个版本的迭代器iterator与const_iterator。

六.string类对象的增删查改操作

1.operator=

注意:operator=只能写成成员函数,不能写成成员函数。

1.传统写法

与传统写法的拷贝构造类似。

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

注意:如果没写 if (this != &str) 自己给自己赋值时,delete[] _str 后_str为野指针,自己给自己拷贝程序崩溃。

2.现代写法

与现代写法的拷贝构造类似。

string& operator=(const string& str)
{
	if (this != &str)
	{
		//string tmp(str.c_str()); //调用构造
		string tmp(str); //调用拷贝构造
		swap(tmp); //刚好函数结束时,tmp将赋值前的空间释放,相当的完美
	}
	return *this;
}

//更完美的方法:一行搞定
string& operator=(string tmp)
{
	swap(tmp);

	return *this;
}

2.push_back

尾插时:先检查容量,再进行尾插。注意:最后要补上'\0'

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

3.append

追加时:先要判断容量是否大于有效数据+所追加的字符串大小。若小于则无需扩容;若大于两倍则需要多少就扩容多少;小于两倍就按照两倍扩容。最后拷贝字符串即可。

void 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 += len;
}

4.operator+=

  1. +=一个字符:直接调用push_back即可。
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
  1. +=一个字符串:直接调用append即可。
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

5.insert

  1. 插入一个字符:先检查容量,再整体往后挪动一位,最后插入即可。

但是存在一些坑如下:
在这里插入图片描述

  • 当在pos=0位置插入字符时:end=0时进入循环,- -end,由于end类型为无符号整形size_t,则end不是-1而是一个非常大的值,进入死循环。
  • 就算将end修改为int ,循环条件end>=pos时,两边类型不同会进行算数转换,int转换成size_t,end转换成size_t类型,依旧进入死循环。

正确写法:

void insert(size_t pos, char ch)
{
	assert(pos >= 0 && pos <= _size);
	
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

    //第一种:强转size_t为int
	//int end = _size;
	//while (end >= (int)pos)
	//{
	//	  _str[end + 1] = _str[end];
	//	  --end;
	//}
	//_str[pos] = ch;
	//++_size;

	//推荐这种:end始终大于0
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;
}
  1. 插入一个字符串:先检查容量,再整体往后挪动为插入的字符串预留空间,最后插入字符串即可。
void insert(size_t pos, const char* str)
{
	assert(pos >= 0 && pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}
	//整体后移
	//memmove(_str + len, _str, sizeof(char) * len);
	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] = str[i];
	}
	_size += len;
}

6.erase

删除时:比较要删除的子串长度与pos及其以后字符串的的大小,判断是否pos及其以后得字符全删除。

void erase(size_t pos, size_t len = npos
)
{
	assert(pos >= 0 && pos < _size);

	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		//memmove(_str + pos, _str + pos + len, sizeof(char) * (_size - pos - len + 1));
		for (size_t i = pos; i <= _size - len; i++)
		{
			_str[i] = _str[i + len];
		}
		_size -= len;
	}
}

7.find

  1. 查找字符:找到返回下标,未找到返回npos。
size_t find(char ch, size_t pos = 0)
{
	assert(pos >= 0 && pos < _size);

	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}
  1. 查找字符串:利用C语言接口strstr查找子串函数,找到返回下标,未找到返回npos。
size_t find(const char* str, size_t pos = 0)
{
	assert(pos >= 0 && pos < _size);
	
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}

8.substr

返回子串:比较要返回子串长度与pos及其以后字符串的的大小,判断是否pos及其以后得字符全返回。

注意:深浅拷贝问题;由于是返回局部string,而局部string出函数被销毁。此时会拷贝构造一个临时string作为返回,而默认的拷贝构造是浅拷贝(简单的值拷贝),局部string销毁时,临时变量string中的_str变成野指针,外面又拷贝构造接收该临时string,本身就是无效的string,程序结束前调用析构函数释放空间,重复的delete导致程序崩溃。解决方法:自己写一个深拷贝构造。

string substr(size_t pos = 0, size_t len)
{
	assert(pos >= 0 && pos < _size);

	if (len > _size - pos)
	{
		len = _size - pos;
	}

	string sub;
	sub.reserve(len);
	for (size_t i = 0; i < len; i++)
	{
		sub += _str[pos + i];
	}
	return sub;
}

9.c_str

返回字符串首字符的地址:用于调用C语言接口,例如strcpy,memmove等。

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

10.swap

调用std::swap进行对象(值)交换。

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

七.非成员函数

1.string比较函数

只需要利用strcmp函数比较,实现两个函数,就可以调用实现多个函数。

bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}

bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}

bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}

bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}

bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

2.流插入与流提取

  在C++中,屏幕和键盘分别通过标准输出流(std::cout)和标准输入流(std::cin)来实现数据的流插入(输出)和流提取(输入)。以下是针对屏幕(输出)和键盘(输入)的流插入与流提取的详细介绍:

  1. 屏幕(输出)与流插入(operator<<):流插入(operator<<)用于将数据发送到输出流中,在C++中,标准输出流std::cout是与屏幕(通常是控制台或命令行界面)相关联的。当你使用<<操作符将数据发送到std::cout时,数据会被格式化(如果需要的话)并显示在屏幕上。
  2. 键盘(输入)与流提取(operator>>):流提取(operator>>)用于从输入流中读取数据,在C++中,标准输入流std::cin是与键盘(或任何标准输入设备)相关联的。当你使用>>操作符从std::cin中读取数据时,它会从键盘获取输入,并根据需要将其存储在提供的变量中。

注意:

  1. 流插入与流提取不推荐写成成员函数,例如ostream& operator<<(ostream& out); 因为<<左边是类对象,调用时要写成s<<out,非常别扭。
  2. 不需要写成友元函数,可以做到不用访问类内的私有成员,完成流插入与流提取。
ostream& operator<<(ostream& out, const string& str)
{
	/*string::const_iterator it = str.begin();
	while (it != str.end())
	{
		cout << *it;
		++it;
	}*/
	
	for (auto ch : str)
	{
		out << ch;
	}
	return out;
}
	
istream& operator>>(istream& in, string& str)
{
	str.clear();

	char ch;
	//in >> ch; //错误,ch不会提取空白字符,陷入死循环
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		str += ch;
		//in >> ch;
		ch = in.get();
	}
	return in;
}

注意:流提取cin默认跳过空白字符(不会读取空白字符),例如:空格、换行,可以用cin.get()函数从键盘获得空白字符,类似C语言中的getc()函数。

优化方法:减少扩容,临时存放到字符数组中,等到满了时,再+=到其中。

istream& operator>>(istream& in, string& str)
{
	str.clear();

	const int N = 256;
	char buff[N];
	int i = 0;

	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == N - 1)
		{
			buff[i] = '\0';
			str += buff;
			i = 0;
		}
		ch = in.get();
	}

	if (i > 0)
	{
		buff[i] = '\0';
		str += buff;
	}
	return in;
}

3.getline

getline函数:可以读取含有空格的字符串,将’\n’作为分隔符。

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

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

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

相关文章

傅里叶变换与FFT应用

一、傅里叶变换 1.1 变换 我们先给例子&#xff0c;假设在直角坐标系上有A(2,1),B(1,2);数和图之间存在的关系&#xff0c;称作变换&#xff1b;在图上我们想找对角线C&#xff0c;通过计算我们就知道C(3,3)&#xff1b;我们知道&#xff0c;在坐标系上有单位向量&#xff0c;…

Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 之 [ 选择并训练模型 ] [ 模型微调 ] | 3/3(含分析过程)

Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 之 [ 选择并训练模型 ] [ 模型微调 ] | 3/3&#xff08;含分析过程&#xff09; 目录 Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 之 [ 选择并训练模型 ] [ 模型微调 ] | 3/3&#xff08;含分析…

Vue使用flex将图片并排居中且左对齐

先看效果&#xff1a; 在看代码 <template><div class"outer"><div class"inner"><div classeach_image v-for"(item,index) in image_list" :key"index"><img class"image_class" src"./…

培训孵化公司必备ERP的系统,跟卖和铺货以及订单物流发货打包

培训孵化必备的 ERP 系统&#xff0c;贴牌定制独立部署&#xff0c;跟卖铺货&#xff0c;物流对接。 说说新手与培训孵化学员如何使用 ERP&#xff01; 1. ERP 系统对于新手来说非常简单且容易操作&#xff0c;上面的跟卖功能很全面。比如铺货方面&#xff0c;可以采集 1688、…

发现SOLIDWORKS设计活页夹

您有没有遇到过将模型文件转交给同事时丢失附件的文档信息的&#xff1f;您有没有遇到过您的业务同事使用您的模型时仍然搞不清模型和业务项目之间的关系&#xff1f; 在纸制图纸的“旧时代”中&#xff0c;会有一整套信息&#xff08;文档或者表格&#xff09;与模型和图纸一…

电路原理--基础电路工具

1.正弦信号激励下的动态电路分析法-----频域相量法 课本第六章269页。 2.阻抗 3.滤波器简单理解 先介绍下滤波&#xff0c;芯片和元器件在相互工作的时候&#xff0c;会相互影响&#xff0c;在线路上产生寄生电阻或者寄生电容&#xff0c;这种现象叫耦合&#xff0c;耦合会带…

2024 巴黎奥运会:科技点亮体育盛会

一、引言 巴黎奥运会作为全球瞩目的体育盛事&#xff0c;承载着深厚的历史与文化底蕴。它不仅是运动员们展现卓越竞技能力的舞台&#xff0c;也是科技成果大放异彩的平台。科技在巴黎奥运会中的地位举足轻重&#xff0c;为赛事的各个方面带来了革新与突破。 从赛事的筹备到运…

Threejs中导入GLTF模型克隆后合并

很多场景中会需要同一个模型很多次&#xff0c;但是如果多次加载同一个模型会占用很高的带宽&#xff0c;导致加载很慢&#xff0c;因此就需要使用clone&#xff0c;也就是加载一个模型后&#xff0c;其他需要使用的地方使用clone的方式复制出多个同样的模型&#xff0c;再改变…

静态路由与默认路由和实验以及ARP工作原理

目录 1.静态路由和默认路由 1.1 静态路由 1.2 默认路由 1.3 主要区别总结 2.实验 2.1 实验 2.1.1 实验top 2.1.2 实验要求 2.2 实验配置 2.2.1 ip信息配置 2.2.2 配置静态 2.2.3配置默认 2.3 实验结果查看 3.为什么第一个ping会显示丢包&#xff1f; 3.1 ARP 工…

15.3 zookeeper实现分布式锁

1. 简介 2. 代码演示 2.1 客户端连接类 package com.ruoyi.common.zookeeper;import com.ruoyi.common.exception.UtilException; import

操作系统-硬件结构学习心得

1. 程序执行基本过程 那CPU执行程序的过程如下: ●第一步&#xff0c;CPU读取[程序计数器」的值&#xff0c;这个值是指令的内存地址&#xff0c;然后CPU的「控制单元操作 「地址总线」指定需要访问的内存地址&#xff0c;接着通知内存设备准备数据&#xff0c;数据准备好后通…

【每日刷题】Day90

【每日刷题】Day90 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 单词缩写_牛客题霸_牛客网 (nowcoder.com) 2. 面试题 01.03. URL化 - 力扣&#xff08;LeetCode&am…

深度剖析Google黑科技RB-Modulation:告别繁琐训练,拥抱无限创意生成和风格迁移!

给定单个参考图像,RB-Modulation提供了一个无需训练的即插即用解决方案,用于(a)风格化和(b)具有各种提示的内容样式组合,同时保持样本多样性和提示对齐。例如,给定参考样式图像(例如“熔化的黄金3d渲染样式”)和内容图像(例如(a)“狗”),RB-Modulation方法可以坚持所需的提…

内存泄漏 与 内存溢出

1.内存溢出(Memory Overflow) 生活样例&#xff1a; 内存容量就像一个桶&#xff0c;内存就是水&#xff0c;水 溢出 就是水满了。定义: 内存溢出是指程序试图使用超过其可用内存限制的内存。这种情况通常会导致程序崩溃或异常。内存溢出一般是由于分配了过多…

Mixture of Experts with Attention论文解读

注意这篇论文没有代码&#xff0c;文章所谓的注意力是加性注意力&#xff0c;找scaled dot-product的伙计可以避坑了&#xff0c;但还是有值得学习的地方。 score是啥&#xff1f; 这个score标量怎么计算得到&#xff0c;请假设一下x和z的值&#xff0c;计算演示一下 expert是…

第十二章(重点 元数据管理)

语境关系图&#xff1a; 1. 元数据概念&#xff1a; 元数据从技术的角度叫元数据 从业务的角度叫数据资源管理目录 技术 元数据 业务 数据资源管理目录 但是并不是数据资产目录 如果没有可靠的原数据&#xff0c;组织就不知道它拥有什么数据&#xff0c;数据表示什么&#xff…

运行ruoyi

创建数据库 根据ry_20240629.sql创建ry-cloud数据库 根据ry_config_20231204.sql创建ry-config数据库 nacos 数据库配置 修改nacos/conf/application.properties 单机版运行 startup.cmd -m standalone redis 运行后端 运行gateway,auth,modules/system模块 可能遇到的问…

怎么给电脑选一款合适的固态硬盘?就看这个参数!

前言 前段时间有很多小伙伴找小白修电脑&#xff0c;在修电脑的过程中&#xff0c;小白也会稍微看一下硬件配置。 小白就发现一个事情&#xff1a;很多小伙伴其实都不太懂电脑硬件。 为啥这么说呢&#xff1f;简单来说就是主板上使用了“不合适”的固态硬盘作为主系统硬盘。…

VulnHub-Tomato靶机渗透教程 简单易懂 报错链接

Tomato靶机是一个用于渗透测试和漏洞研究的虚拟机。 环境准备 攻击机&#xff08;Kali Linux&#xff09;IP&#xff1a;192.168.252.134 目标机 IP&#xff1a;192.168.252.133 这里我两台虚拟机都是NAT模式 渗透步骤 1.端口扫描 这里我没用kali自带的 我用的物理机上…

【Python学习手册(第四版)】学习笔记12.1-语法规则拓展

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文是对【学习笔记10】-语句编写的通用规则 介绍过的语法概念进行复习并扩展。非常简单&#xff0c;应该是我写过的最简单的文章&#xff0c;阅读时间&#xff1a…