C++基础 [五] - String的模拟实现

news2025/3/19 15:33:19

目录

前言

string类的模拟实现 

成员函数的实现

构造函数

拷贝构造函数

赋值运算符重载 

析构函数

元素访问的实现

operator[ ]

Iterator - 迭代器

容量大小的实现

size

capacity

reserve

​编辑resize

内容修改的实现

push_back

append

operator+=(char ch)

operator+=(const char* s)

 insert 

erase

内容查找的实现

find - 字符

find - 字符串

substr 

非成员函数的重载

relational operators 

operator<< 流插入

 operator>> 流提取

getline 

string 类的模拟实现整体代码 

string.h

string.cpp 

test.cpp 


前言

本模块呢,我将会带大家一起从 0~1去模拟实现一个STL库中的 string类,当然模拟实现的都是一些常用的接口,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类中的6大默认成员函数等

string类的模拟实现 

成员函数的实现

首先想,对于一个自定义函数,在调用的时候第一步是干嘛呢?答案肯定是我们之前讲到的,默认成员函数的实现。因为我们是模拟实现的string,也就是说库里面已经有了,所以我们要加上namespace,防止跟库文件发生冲突。

构造函数

我们先回想一下,string函数都要干嘛呢?首先肯定是要有数据,然后也要保存数据的空间,最后也要有个大小用于计算数据的多少。所以我们的成员变量就是 char* _str; size_t _size;  size_t _capacity; 

能不能这样初始化呢?获取到char*的str之后,初始化_str;答案是不能的,1.因为你的成员变量_str是char*类型的,构造函数的形参是const类型的,会涉及到权限的放大  2.万一你传的是个常量字符串,也就是不能修改的,所以我们要去开空间初始化。 我们之前也说过要尽量用初始化列表去初始化

我们同学可能就是这样想的,先去读字符串的大小,然后再进行计算容量,最后计算完再开空间

可是我们运行之后就报错了呀,这是为什么呢?

我们会发现这个地址为啥是错的呢?

这有个之前的问题,初始化列表初始化的顺序是按照成员变量的顺序,而不是初始化列表写的顺序。 

也就是说,它会先去走 _str(new char[strlen(str)+1]),而此时capacity还没初始化,就是个随机值,要是很大的话,这个new就会崩掉的了。这里有两种改法。1.让顺序保持一致

接着开了空间之后,我们就要把值拷进来了。

此时我们就发现可以了。

注意:此写法有个大坑,就是成员变量的顺序要声明正确!

2.就是把内置类型_size,_capacity 放到函数里面去初始化

拷贝构造函数

我们有提到过若是一个类在没有显示定义拷贝构造对于内置类型不做处理,而对于自定义类型会去调用 类中默认提供的拷贝构造函数 此时就会造成浅拷贝的问题

  • 浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
  • 深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。

 很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法: 

写法一:传统写法 

传统写法的思想简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的

//拷贝构造
string(const string& s)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}
  • 通过调试再去观察的话,我们可以发现,此时 对象s1 和 对象s2 中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响

写法二:现代写法

现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。 

//拷贝构造
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}

赋值运算符重载 

 对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个

  • 但是呢默认生成的这个也会造成一个 浅拷贝 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1和s3就会指向一块同一块空间,此时便造成了下面这些问题
  • 在修改其中任何一者时另一者都会发生变化;
  • 在析构的时候就也会造成二次析构的;
  • 原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题

  • 那么此时我们应该自己去开出一块新的空间,将s3里的内容先拷贝到这块空间中来,然后释放掉s1所指向这块空间中的内容,然后再让s1指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间
  • 最后的话不要忘记去修改一下s1的【_size】和【_capacity】,因为大小和容量都发生了改变

下面是具体的代码 

写法一:传统写法 

// 赋值运算符重载
		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& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //返回左值(支持连续赋值)
}

有关这个swap()函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模板 的时候有提到过库中的这个 swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据
可以看到在我们自己实现的这个swap(string& s) 函数中就去调用了std标准库中的函数然后交换一个string对象的所有成员变量

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

接下去来解释一下这里的原理,我们在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题

透过上面这个图解读者应该对新的这种拷贝构造有了一定的理解:反正你这个tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果

还有种写法

string& operator=(string tmp) {
    swap(tmp);  // 用交换的方法避免了多余的内存操作
    return *this;  // 返回当前对象,支持链式赋值
}

下面我们以一个实例讲解一下

#include <iostream>
#include <string>

class MyString {
private:
    std::string str;  // 内部使用标准 string 存储字符串

public:
    // 构造函数
    MyString(const std::string& s) : str(s) {}

    // 赋值运算符重载
    MyString& operator=(MyString tmp) {
        swap(tmp);  // 使用 swap 交换 tmp 和当前对象的内容
        return *this;  // 返回当前对象,支持链式赋值
    }

    // 打印函数
    void print() const {
        std::cout << str << std::endl;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2("World");

    // 使用重载的赋值运算符
    s1 = s2;  // 赋值:s1 变为 "World"

    // 打印 s1 和 s2 的内容
    std::cout << "s1: ";
    s1.print();  // 应该输出 "World"
    std::cout << "s2: ";
    s2.print();  // 应该输出 "World"

    return 0;
}

此时可能就会有同学问了,在operator=中 return了*this 但是就一个s1=s2 我看没有人接收啊 是不是s1=s2可以转换成别的形式?解答如下:

当operator这个函数结束的时候,会调用析构函数,此时的this就是temp的this,就会把temp的空间给带走了

析构函数

我们要析构,就是要把类中的数据给清除了。那什么才是我们需要清楚的呢?也就是这三个char* _str; size_t _size;  size_t _capacity;

这里的顺序不需要进行刻意,因为大小和容量并不是存在delete释放的是_str所指向的内存块

它们是存在栈内存中的

目前我们看数据都是再调试窗口查看的,为了更方便些,我们先定义个c_str,直接返回const char*类型的_str;

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

这里建议加个const,表示 该成员函数不会修改类的成员变量 为什么要加const呢?const对象调const可以达到一个平传的效果。修饰的是this指针指的对象。权限不能放大但是可以缩小

此时我们就可以打印数据了

 但是我们有时候也需要个无参的构造,然后再进行增加修改的操作。

默认构造函数包括三种,1.我们不写,编译器自动生成的。2.全缺省的 3.无参的。这里我们写个无参的,那该怎么写呢?我们能不能这样写?

 

答案是不能的,如果我们这样给_str空的话,那c_str就悬空了,此时返回个空指针那不是就崩了吗?那该怎么做呢?

我们观察库里的string,即便是空的也会有个'\0'

所以我们要对_str先开个空间,然后再给个'\0',注意这里不是"\0",不是字符串\0,而是字符\0

此时,我们说能不能合并下呢?用全缺省的,那这两种写法你们觉得可以吗?

答案是都不能,第一个是类型不匹配。一个是字符,一个是字符串指针

第二个不能给空,如果给空了之后,_size(strlen(str)) 就会崩了

最优写法是这样的

常量字符串默认是有"\0"的。

元素访问的实现

operator[ ]

operator有两种形式,1.可读可写 2.只读

首先最常用的就是这【下标 + [ ]】的形式去进行一个访问,那很简单,我们通过当前所传入的下标值去访问对应的数据即可

可读可写的实现

这里我们先用传值返回,看看会有什么问题呢

这里为什么会报错呢?

问题出在代码的 operator[] 函数部分。你正在定义一个 operator[] 来访问 _str 数组的元素,但是函数的返回值是 char 类型这意味着返回的是一个值,而不是引用。如果你试图修改 s1[i]++ 中的值,这会导致错误,因为 operator[] 返回的是不可修改的副本。

所以这里要换成引用

 只读的实现

读写和读的operator构成函数重载,我们可以加入const对权限进行限定

// 可读不可写                            
const char& operator[](size_t pos) const// 最后也加上const 因为防止该函数对const进行修改                                      
{                        
	assert(pos < _size);
	return _str[pos];
}

 const加在前面是为了防止返回的数据被修改const加在后面是为了防止传过来的数据被修改

Iterator - 迭代器

那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了【下标 + []】的形式,我们还可以使用迭代器的形式去做一个遍历,迭代器指向的是位置,而不是数据

  • 而对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的
  •  string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。 

typedef char* iterator;
typedef const char* const_iterator;

 注:不是所有的迭代器都是指针。

我们知道,对于begin来说,也就是_str所指向的的位置,那对于end呢?end的意思是返回最后一个字符的下一个位置。那既然str_是第一个位置,size又是这个数据的大小,那最后一个位置的下一位不就是_str+size吗?

iterator begin()
{
	return _str;// 返回指向字符数组起始位置的指针
}
 
iterator end()
{
	return _str + _size;
}

  • 实现了普通版本的迭代器之后,我们再来看看常量迭代器。很简单,只需要修改一下返回值,然后在后面加上一个【const成员】,此时就可以构成函数重载了
const_iterator begin() const
{
	return _str;
}
 
const_iterator end() const
{
	return _str + _size;
}

范围for的底层实现就是用iterator迭代器实现的

容量大小的实现

size

首先是 size(),这里的话我们直接返回_size即可,因为不会去修改成员变量,所以我们可以加上一个【const成员】(因为它是不可被修改滴)

size函数用于获取字符串当前的有效长度(不包括’\0’)

size_t size() const
{
	return _size;
}

注意:这里的 const 是用来修饰 this指针的

capacity

capacity函数用于获取字符串当前的容量

size_t capacity() const
{
	return _capacity;
}

reserve

reserve规则: 

  1. 扩容n大于对象当前的capacity时,将capacity扩大到n或大于n
  2. 扩容n小于对象当前的capacity时,什么也不做

很明显,只有当这个 新容量大于旧容量的时候,才会去选择去开空间,这里的扩容逻辑和我们在实现旧版本的拷贝构造函数时类似的:也是先开出一块新的空间(这里主要使用这个newCapacity 去开),然后再将原本的数据拷贝过来,让_str指向新空间然后释放旧空间的数据。最后的话不要忘了去更新一下容量大小

void reserve(size_t newCapacity)// 扩容(修改_capacity)
{
	// 当新容量大于旧容量的时候,就开空间
	if (newCapacity > _capacity)
	{
		// 1.以给定的容量开出一块新空间
		char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'
		// 2.将原本的数据先拷贝过来
		strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
		// 3.释放旧空间的数据
		delete[] _str;
		// 4.让_str指向新空间
		_str = tmp;
		// 5.更新容量大小
		_capacity = newCapacity;
	}
}

不能先赋值再delete _str,这样会把新地址的str给释放掉。 

注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。

resize

首先我们来分析一下,对于【resize】而言主要对对象中的数据去做一个变化,用于调整字符串的大小。那么该怎么调呢

如果调的大小很大,该考虑什么呢?如果调的很小,又该考虑什么呢?接下来我们就要进行分类讨论了

  • 如果这个 newSize < _size 的话,那我们要选择去删除数据
  • 如果这个 newSize > _size但是呢 newSize < _capacity 的话,此时要做的就是新增数据但是呢不去做扩容
  • 如果这个 newSize > _size 并且 newSize > _capacity,我们便要选择去进行扩容了

当 _size = 10 , _capacity = 15 时 

  • 在分析完了之后,我们立即来实现一下相关的代码。可以看到,一上来我就直接去判断了newSize 是否大于_size,然后在内部又做了一层判断,只有当newSize > _capacity时,才去执行【reserve】的扩容逻辑
  • 如果newSize并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数memset
    • 我们从_str + _size 的位置开始填充;
    • 填充的个数是newSize - _size个;
    • 填充的内容是c
  • 若是newSize <= _size的话,我们所要做的就是去截取数据,到newSize为止直接设置一个 \0,然后更新一下当前对象的_size大小
// 改变大小
void resize(size_t newSize, char c = '\0')
{
	// 1.当新的_size比旧的_size来得小的话,则进行删除数据
	if (newSize > _size)
	{
		// 只有当新的size比容量还来的大,才去做一个扩容
		if (newSize > _capacity)
		{
			reserve(newSize);
		}
		// 如果newSize <= _capacity,填充新数据即可
		memset(_str + _size, c, newSize - _size);
	}
 
	// 如果 newSize <= _size,不考虑扩容和新增数据
	_size = newSize;
	_str[newSize] = '\0';
}

有几点是易错的,reserve的时候要扩容到新的newSize,而不是扩容newSize-_size。

内容修改的实现

push_back

首先第一块的话简单一点,我们去追加一个字符,那首先要考虑到的也是一个扩容逻辑,因为我们是一个字符一个字符去进行插入的,那么当这个_size == _capacity的时候,就要去执行一个扩容的逻辑了,这边的话是运用到了这个三目运算符,若是容量的大小为0的话,默认开个大小为4的空间就可以了;其他的情况都是以2倍的形式去进行扩充

最后在扩完容之后我们就在末尾去增加数据了,因为此时_size指向的就是 \0 的位置,所以就把字符放在这个位置上就可以了,顺带地记得去后移一下这个_size,再放上一个 \0

// 追加一个字符
void push_back(char ch)
{
	// 如果数据量大于容量的话,则需要进行扩容
	if (_size == _capacity)
	{ 	
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size] = ch;
    ++size;
	_str[_size] = '\0';
}

append

接下去的话是【append】,要追加的是一个字符串,所以我们要先去算出它的长度,接下去判断一下在加上这个长度后是否要去做一个扩容,最后的话还是通过我们熟悉的【memcpy】通过字节的形式一一拷贝到_str + _size的位置(注意拷贝len + 1个,带上最后 \0),最后再把大小_size给增加一下即可

// 追加一个字符串
void append(const char* s)
{
	int len = strlen(s);	// 获取到待插入字符串的长度
	// 若是加上len长度后超出容量大小了,那么就需要扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
 
	// 将字符串拷贝到末尾的_size位置
	memcpy(_str + _size, s, len + 1);
	// 大小增加
	_size += len;
}

注意这里的_size + len > _capacity; 用原来的大小加上新追加的字符串的长度,然后再去和容量去比较

为啥要用c_str打印呢?

因为 c_str() 返回的是 const char*,即一个 C 风格的字符串指针,表示你的 MyString 对象内部存储的字符数组

operator+=(char ch)

首先的话是去【+=】一个字符,这里我们直接复用前面的push_back()接口即可,最后因为【+=】改变的是自身,所以我们return *this,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝

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

operator+=(const char* s)

而对于【+=】一个字符串,我们则是去复用前面的append()即可

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

 这里为什么不用char&而是用string&呢

char& 代表一个单个字符的引用而 operator+= 是一个修改 MyString 对象的操作,而不是修改单个字符

 insert 

接下去我们就要来实现一下【insert】这个接口了-- 从 pos 位置 开始插入n 个字符

不过在这之前呢我们先要去声明并初始化一个静态的成员变量npos,它是最大的无符号整数值。但是对于 静态的成员变量 来说我们需要 在类内声明并且在类外进行初始化

// 类内声明
const static size_t npos;
// 类外初始化
const static size_t npos = -1;

 首先第一个的话就是要在pos位置插入n个字符

void insert(size_t pos, size_t n, char ch)

因为这里会传入一个pos位置,所以第一步我们就是要去考虑这个pos位置是否合法

assert(pos <= _size);

接下去第二步的话就是去考虑过扩容的问题了,如果_size + n之后的大小大于_capacity的话那就要调用【reserve】接口去实现一个扩容的逻辑了

// 考虑扩容
if (_size + n > _capacity)
{
	reserve(_size + n);
}

第三步呢并不是直接去插入数据,而是要先给需要插入的n个字符腾出位置。从_size位置开始,让字符以n个单位地从后往前挪即可,若是从前往后挪的话就会造成覆盖的问题

再次强调这里是挪动end + n 不是 end + 1个位置 

// 挪动数据
size_t end = _size;
while (end >= pos)
{
	_str[end + n] = _str[end];
	--end;
}

不过呢,我们在这里还要考虑一种极端的情况,如果这个pos == 0的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是呢当这个end超出pos的范围时,也就减到了-1,但是呢这个end的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数

我们可以来看看当这个end在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos的值了,此时就会造成一个死循环,导致程序崩溃

// 字符插入测试
void test6()
{
	xas_string::string s1("abcdefghijk");
	s1.insert(0, 3, '#');
	cout << s1.c_str() << endl;
 
}
 
int main()
{
	test6();
	return 0;
}

所以我们应该将无符号该有 有符号类型 size_t ----> int

// 挪动数据
	int end = _size;
	while (end >=(int)pos)
	{
		_str[end + n] = _str[end];
		--end;
	}

 当这个挪动的逻辑结束后,我们就可以从pos这个位置去插入n个字符了。最后再去更新一下这个_size的大小即可

// 插入n个字符
for (size_t i = 0; i < n; i++)
{
	_str[pos + i] = ch;
}
_size += n;

erase

删除从pos位置开始的len个有效长度字符

  • erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:

1.pos位置及其之后的有效字符都需要被删除

  • 这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。 

2.pos位置及其之后的有效字符只需删除一部分。

这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。

// 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
 
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}

 不建议使用 因为挪动数据代价挺大的

内容查找的实现

find - 字符

这个很简单,就是去遍历一下当前对象中的_str,若是在遍历的过程中发现了字符ch的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos这个最大的无符号数

find - 字符串

我直接使用的是C语言中的库函数 strstr函数详解,这个的话我们在 字符串函数与内存函数解读 的时候也有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针。那我们如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可

size_t find(const char* s, size_t pos)	const
{
	assert(pos < _size);
	char* tmp = strstr(_str, s);
	if (tmp)
	{
		// 指针相减即为距离
		return tmp - _str;
	}
	return npos;
}

substr 

 上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?

 首先要考虑的问题是长度的问题,如果我们从pos位置取的子串长度大于剩余子串的长度,那么最多能取得范围也是从pos位置到size_t的位置。所以当取得长度过高的时候,我们就要更新下子串长度的有效范围

  • 可以看到,我以这个n作为可取的子串长度,一开始得让其等于传入进来的len长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len
  • 但是如果呢这个长度超出了有效范围后,我们便要去更新这个n = _size - pos
size_t n = len;
if (len == npos || pos + len > _size)
{
	// 就算要取再大的长度,也只能取到pos - _size的位置
	n = _size - pos;
}
  • 那接下去的话我们就可以去取这个子串了,使用循环的方式从pos位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】
string substr(size_t pos, size_t len = npos)
{
    size_t n = len;
	if (len = npos || len + pos > _size)
	{
        n = _size - pos;
	}
	string temp;
	temp.reserve(n);
	for (int i = pos; i < n + pos; i++)
	{
        temp += _str[i]; //这里用到了操作符重载 //等于是追加的意思
	}

    return temp;
}

  为什么是string类型

substr 需要返回一个新的字符串对象,因为原字符串中的一部分需要作为独立的字符串进行存储和操作。

  • char 类型 是用来存储单个字符的基本数据类型,而 std::string 是用来存储多个字符的容器。

  • 如果 substr 返回 char,就意味着它只能返回一个单一字符,但实际上我们要提取的是 多个字符(即一个子字符串)。这就不适合用 char 类型来返回。

为啥不能temp = _str[i]; temp++; 

  • 这行代码是递增 temp,也就是将 temp 的值加一。如果 temp 是一个字符类型的变量(char),那么 temp++ 实际上将其字符值增加一个。
  • ,在这种情况下,temp++ 会使 temp 的值变成下一个字符,而不是将字符追加到一个字符串中。

这里i的取值范围为什么是n+pos,为什么不是n

首先我们要知道i是从pos位置开始的,就好比substr(5,2);指的是从第五个位置,取两个字符串。那你难道就写成i = 5;i < 2吗?这肯定是错误的,应该是pos+n才是最后的位置

非成员函数的重载

最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多

relational operators 

小于

//< 运算符重载
bool operator<(const string& s)const
{
	return strcmp(_str, s._str) < 0;
}

 等于 

//==运算符重载
bool operator==(const string& s)const
{
	return strcmp(_str, s._str) == 0;
}

小于等于 

bool operator<=(const string& s)
{
	return *this < s || *this == s;
}

大于

bool operator>(const string& s)
{
	return !(*this <= s);
}

大于等于

bool operator>=(const string& s)
{
	return !(*this < s);
}

不等于

bool operator!=(const string& s)
{
	return !(*this == s);
}

operator<< 流插入

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

 放在类内就会提示运算符的参数太多了,因为会有个隐藏的this

我们知道为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到友元这个东西,不过呢我们不建议使用这个,会破坏类的封装性

还有一点要提醒的是对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个 防拷贝 的效果,即在后面加上一个= delete

 好,那到这里的话,我们是时候来讲讲这个cout << s.c_str() 和 cout << s 的区别了 

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

 operator>> 流提取

重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。

	//重载>>
	std::istream& operator>> (std::istream& in, string& s)
	{
		//读取前要先清理掉原来存在的字符
		s.clear();
		//用get获取字符
		char ch = in.get();
		//先用一个数组存起来,再一起加
		char buff[128];
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			//原始方法,一个字符一个字符加太麻烦,先用一个数组存起来,再一起加
			//s += ch;
			buff[i++] = ch;
			if (i == 127)
			{
				buff[127] = '\0';
				s += buff;
				i = 0;//重置i
			}
			ch = in.get();
		}
		//循环结束后可能还要一些字母没有存进去
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

getline 

getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。 

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in;
}

string 类的模拟实现整体代码 

string.h

#pragma once
#include <iostream>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cout;
using std::cin;
using std::endl;
 
// 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域
namespace xas_string
{
	class string
	{
	public:
 
		typedef char* iterator;             // 迭代器某种意义上就是 指针
		typedef const char* const_iterator; 
 
 
		//  默认成员函数
		string(const char* str = "");       // 有参构造函数
		string(const string& s);            // 拷贝构造
		string& operator=(const string& s); // 赋值运算符重载
		~string();                          // 析构函数
 
 
		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;
 
 
		//容量和大小相关函数
		size_t size()  const;                 // 返回目前 字符串的有效字符个数
		size_t capacity() const;              // 返回目前 字符串的容量
		void clear();                         // 清空字符串
		bool empty() const;                   // 判断字符串是否为空
		void reserve(size_t newCapacity = 0); // 扩容(修改_capacity)
		void resize(size_t newSize, char c = '\0'); // 改变大小
 
 
		// 修改字符串相关函数
		void push_back(char ch);               // 追加一个字符
		void swap(string& s);                  // 交换 --- 交换两个字符串
		void append(const char* s);            // 追加一个字符串
		string& operator+=(char ch);           // 追加一个字符
		string& operator+=(const char* s);     // 追加一个字符串
		void insert(size_t pos, const char* str); // 插入n个字符
		void erase(size_t pos, size_t len = npos); // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
 
 
 
		// 访问字符串相关函数
		char& operator[](size_t pos);             // 可读可写
		const char& operator[](size_t pos) const; // 可读不可写
		const char* c_str()const;                 // 用 C语言的方式返回
		size_t find(char ch, size_t pos = 0);     // 寻找字符
		size_t find(const char* str, size_t pos = 0); // 寻找 字符串
		string substr(size_t pos = 0, size_t len = npos); // 截取字符串 从某个位置 取 len 个字符
 
 
        // 关系运算符重载
		bool operator<(const string& s)const;      // < 运算符重载 
		bool operator==(const string& s)const;     // ==运算符重载
		bool operator<=(const string& s);          // <=运算符重载
		bool operator>(const string& s);          // >运算符重载
		bool operator>=(const string& s);         // >=运算符重载
		bool operator!=(const string& s);         // !=运算符重载
 
 
	private:
		char* _str;         // 指向字符数组的指针
		size_t _size;       // 字符数组的有效数据的长度
		size_t _capacity;   // 字符串数组的容量
		const static size_t npos = -1; //静态成员变量(整型最大值)
	};
 
	// 流插入
	ostream& operator<<(ostream& out, const string& s);
	//>>运算符的重载
	istream& operator>>(istream& in, string& s);
	//读取一行含有空格的字符串
	istream& getline(istream& in, string& s);
 
 
 
	void print_str(const string& s);    // const对象的输出 
}

string.cpp 

#include "string.h"
 
 
// 有参构造函数
xas_string::string::string(const char* str)    // ""  --- 为空的字符串
{
	_str = new char[strlen(str) + 1];   // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1
	_size = strlen(str);
	_capacity = strlen(str);           // capacity 不包括 '\0'
	strcpy(_str, str);
}
 
 
//拷贝构造
xas_string::string::string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}
 
// 赋值运算符重载
// 传统写法
//xas_string::string& xas_string::string::operator=(const string& s)
//{
//	if (this != &s)
//	{
//		char* tmp = new char[s._capacity + 1];
//		//memcpy(tmp, s._str, s._size + 1);
//		strcpy(tmp, s._str);
//		delete[] _str;
//
//		_str = tmp;
//		_size = s._size;
//		_capacity = s._capacity;
//	}
//	return *this;
//}
 
//现代写法2
xas_string::string& xas_string::string::operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //返回左值(支持连续赋值)
}
 
 
// 析构函数
xas_string::string::~string()
{
	delete[] _str;
	_str = nullptr;
	_size = 0;
	_capacity = 0;
}
 
 
char& xas_string::string::operator[](size_t pos)        // 可读可写
{
	assert(pos <= _size);
	return _str[pos];
}
 
const char& xas_string::string::operator[](size_t pos) const  // 可读不可写
{
	assert(pos < _size);
	return _str[pos];
}
 
 
xas_string::string::iterator xas_string::string::begin()
{
	return _str;
}
 
xas_string::string::iterator xas_string::string::end()
{
	return _str + _size;
}
 
xas_string::string::const_iterator xas_string::string::begin() const
{
	return _str;
}
 
xas_string::string::const_iterator xas_string::string::end() const
{
	return _str + _size;
}
 
 
// 针对------const 对象的访问
// 打印这个字符串  --- 不能修改
void xas_string::print_str(const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		std::cout << s[i] << " ";
	}
	std::cout << std::endl;
 
 
	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		// 内容不能修改
		std::cout << *it << " ";
		// 指针可以修改
		it++;
	}
	std::cout << std::endl;
}
 
void xas_string::string::push_back(char ch)   // 追加一个字符
{
	// 如果数据量大于容量的话,则需要进行扩容
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}
 
// 返回目前 字符串的有效字符个数
size_t xas_string::string::size()  const    // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _size;
}
 
size_t  xas_string::string::capacity() const  // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _capacity;
}
 
bool xas_string::string::operator<=(const string& s)          // <=运算符重载
{
	return *this < s || *this == s;
}
 
 
// 清空字符串
void xas_string::string::clear()
{
	_str[0] = '\0';
	_size = 0;
}
// 判断字符串是否为空
bool xas_string::string::empty() const
{
	return 0 == _size;
}
void xas_string::string::reserve(size_t newCapacity)// 扩容(修改_capacity)
{
	// 当新容量大于旧容量的时候,就开空间
	if (newCapacity > _capacity)
	{
		// 1.以给定的容量开出一块新空间
		char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'
		// 2.将原本的数据先拷贝过来
		strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
		// 3.释放旧空间的数据
		delete[] _str;
		// 4.让_str指向新空间
		_str = tmp;
		// 5.更新容量大小
		_capacity = newCapacity;
	}
}
 
void xas_string::string::resize(size_t newSize, char c) // 改变大小
{
	// 1.当新的_size比旧的_size来得小的话,则进行删除数据
	if (newSize > _size)
	{
		// 只有当新的size比容量还来的大,才去做一个扩容
		if (newSize > _capacity)
		{
			reserve(newSize);
		}
		// 如果newSize <= _capacity,填充新数据即可
		memset(_str + _size, c, newSize - _size);
	}
 
	// 如果 newSize <= _size,不考虑扩容和新增数据
	_size = newSize;
	_str[newSize] = '\0';
}
 
void xas_string::string::append(const char* s)             // 追加一个字符串
{
	int len = strlen(s);	// 获取到待插入字符串的长度
	// 若是加上len长度后超出容量大小了,那么就需要扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
 
	// 将字符串拷贝到末尾的_size位置
	memcpy(_str + _size, s, len + 1);
	// 大小增加
	_size += len;
}
 
xas_string::string& xas_string::string::operator+=(char ch)            // 追加一个字符
{                                      
	push_back(ch);
	return *this;
}
 
xas_string::string& xas_string::string::operator+=(const char* s)    // 追加一个字符串
{
	append(s);
	return *this;
}
 
void xas_string::string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
 
	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--;
	}
	// 这里不能用 strcpy 会把 '\0' 拷贝过来
	strncpy(_str + pos, str, len);
	_size += len;
 
}
 
void xas_string::string::erase(size_t pos, size_t len) // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
{
	assert(pos < _size);
 
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}
 
size_t xas_string::string::find(char ch, size_t pos)      // 寻找字符
{
	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}
 
size_t xas_string::string::find(const char* str, size_t pos) // 寻找字符串
{
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}
xas_string::string xas_string::string::substr(size_t pos, size_t len ) // 截取字符串 从某个位置 取 len 个字符
{
	assert(pos < _size);
	size_t end = pos + len;
	if (len == npos || pos + len >= _size)
	{
		end = _size;
	}
 
	string str;
	str.reserve(end - pos);
	for (size_t i = pos; i < end; i++)
	{
		str += _str[i];
	}
	return str;
}
 
bool xas_string::string::operator<(const string& s)const       // < 运算符重载 
{
	return strcmp(_str, s._str) < 0;
}
 
bool xas_string::string::operator==(const string& s)const      //==运算符重载
{
	return strcmp(_str, s._str) == 0;
}
bool xas_string::string::operator>(const string& s)           // >运算符重载
{
	return !(*this <= s);
}
bool xas_string::string::operator>=(const string& s)
{
	return !(*this < s);
}
bool xas_string::string::operator!=(const string& s)
{
	return !(*this == s);
}
 
// 用 C语言的方式返回
const char* xas_string::string::c_str() const     // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _str;
}
 
// 流插入
ostream& xas_string::operator<<(ostream& out, const xas_string::string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}
//>>运算符的重载
istream& xas_string::operator>>(istream& in, xas_string::string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in; //支持连续输入
}
//读取一行含有空格的字符串
istream& xas_string::getline(istream& in, xas_string::string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in;
}
 
 
// 交换
void xas_string::string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

test.cpp 

#include "string.h"
 
 
//  测试初始化,与循环打印 迭代器
void test1()
{
	// 初始化测试
	xas_string::string s1("hello string");
	cout << s1.c_str() << endl;
 
	cout << endl;
 
	// 拷贝构造测试
	xas_string::string s2(s1);
	cout << s2.c_str() << endl;
 
	cout << endl;
 
	//赋值运算符重载
	xas_string::string s3("hello world!");
	s1 = s3;
	cout << s1.c_str() << endl;
 
 
	// 引用返回 是可以进行修改的
	for (size_t i = 0; i < s3.size(); i++)
	{
		s3[i]++;
	}
	cout << s3.c_str() << endl;
	cout << endl;
 
	// 迭代器
	xas_string::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;
 
	// 范围 for
	for (auto ch : s1)
	{
		std::cout << ch << " ";
	}
	cout << endl;
	cout << endl;
 
	// 常量对象
	print_str(s1);
 
}
 
 
// 测试容量相关的函数
void test2()
{
	xas_string::string s1("hello string");
 
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	cout << s1.empty() << endl;
	s1.clear();
	cout << s1.empty() << endl;
 
	cout << endl;
 
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
 
	s1.reserve(100);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}
 
// 测试 resize()函数
void test3()
{
	xas_string::string s1("abcdefghijk");
	s1.reserve(15);
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
 
	cout << endl;
 
	s1.resize(18,'x');
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}
 
// 测试修改函数
void test4()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
 
	cout << endl;
	cout << "追加字符:xxxxx" << endl;
	cout << endl;
 
	s1.append("xxxxx");
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}
 
void test5()
{
	xas_string::string s1("abcdefghijk");
	s1 += 'x';
	cout << s1.c_str() << endl;
 
	s1 += "yyyy";
	cout << s1.c_str() << endl;
}
 
 
// 字符插入测试
void test6()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << endl;
	s1.insert(0,"###");
	cout << s1.c_str() << endl;
 
}
 
// 字符串删除测试
void test7()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << endl;
	s1.erase(3, 3);
	cout << s1.c_str() << endl;
 
}
 
 
// 访问字符串相关函数测试
void test8()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
 
	size_t pos1 = s1.find('c', 0);
	cout << pos1 << endl;
 
	size_t pos2 = s1.find("def", 0);
	cout << pos2 << endl;
	cout << endl;
 
	xas_string::string s2 = s1.substr(5, 5);
	cout << s2.c_str() << endl;
}
 
// 非成员函数重载
void test9()
{
	xas_string::string s1("hello string");
	cout << s1.c_str() << endl;
	xas_string::string s2("hello world");
	cout << s2.c_str() << endl;
 
 
	cout << (s1 < s2) << endl;
	cout << (s1 > s2) << endl;
	cout << (s1 == s2) << endl;
	cout << (s1 != s2) << endl;
}
 
// 输入输出流测试
void test10()
{
	xas_string::string s1("hello");
	s1 += '\0';
	s1 += "*******";
 
	cout << s1.c_str() << endl;
	cout << s1 << endl;
 
	xas_string::string s2;
	getline(cin,s2);
	cout << s2;
}
 
 
int main()
{
	test10();
	return 0;
}

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

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

相关文章

AIAgent有哪些不错的开源平台

AIAgent领域有许多优秀的开源平台和框架&#xff0c;以下是一些值得推荐的开源平台&#xff1a; AutoGPT AutoGPT 是一个基于 OpenAI 的 GPT-4 和 GPT-3.5 大型语言模型的开源框架&#xff0c;能够根据用户给定的目标自动生成所需提示&#xff0c;并利用多种工具 API 执行多步骤…

Python刷题:流程控制(上)

今天刷的是PythonTip的Python 入门挑战中的题&#xff0c;整体难度不高&#xff0c;适合小白练手以及巩固知识点。下面会进行详细讲解。 每日一句 每一个拼命努力的人&#xff0c;都像是独自穿越黑暗森林的行者&#xff0c; 没有并肩的身影&#xff0c;唯有孤独如影随形&…

vulhub/Billu_b0x靶机----练习攻略

1.Billu_b0x靶场下载链接&#xff1a; https://download.vulnhub.com/billu/Billu_b0x.zip 2.下载后&#xff0c;解压出ova文件&#xff0c;直接拖至VMware中&#xff0c;重命名和选择存储位置&#xff0c;点击导入&#xff0c;报错点击重试即可。修改网卡为NAT模式。 打开靶…

【YOLOv8】YOLOv8改进系列(8)----替换主干网络之Swin Transformer

主页&#xff1a;HABUO&#x1f341;主页&#xff1a;HABUO &#x1f341;YOLOv8入门改进专栏&#x1f341; &#x1f341;如果再也不能见到你&#xff0c;祝你早安&#xff0c;午安&#xff0c;晚安&#x1f341; 【YOLOv8改进系列】&#xff1a; 【YOLOv8】YOLOv8结构解读…

Qwen2-Audio:通义千问音频大模型技术解读

引言:从llm到mlm(audio) 大型语言模型(LLM)的发展日新月异,它们在文本理解、生成、推理等方面展现出惊人的能力。然而,交互模态不仅仅依赖于文字,语音、语调、环境音等听觉信息同样承载着丰富的内容。阿里巴巴通义千问团队,推出了 Qwen-Audio 系列模型,这里我们一起…

解决Java多张图合成JPG时出现红色前景及多列自适应适配

目录 前言 一、追本溯源 1、回到最开始 2、合成JPG的异常 二、解决问题 1、关于ImageType 2、TYPE_INT_RGB和TYPE_INT_ARGB 3、问题修复 4、列数自适应的问题 三、总结 前言 在当今数字化信息飞速发展的时代&#xff0c;图像处理技术在各个领域都占据着举足轻重的地位…

SpringBoot实现发邮件功能+邮件内容带模版

发送简单邮件模版邮件 1.pom引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId><version>2.5.13</version></dependency><dependency><groupId&…

npm 报错 unable to resolve dependency tree

如下图&#xff1a; 解决&#xff1a;npm install --legacy-peer-deps 其实提示上有&#xff1a;npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps

【蓝桥杯每日一题】3.17

&#x1f3dd;️专栏&#xff1a; 【蓝桥杯备篇】 &#x1f305;主页&#xff1a; f狐o狸x 他们说内存泄漏是bug&#xff0c;我说这是系统在逼我进化成SSR级程序员 OK来吧&#xff0c;不多废话&#xff0c;今天来点有难度的&#xff1a;二进制枚举 二进制枚举&#xff0c;就是…

Linux:冯诺依曼体系结构、操作系统、进程概念(一.初识进程)

文章目录 1.冯诺依曼体系结构总线与数据传输通路为什么有内存这个部分计算机存储结构 2.操作系统(Operator System)2.1 概念2.2 设计OS的目的2.3 理解“管理”先描述再组织 2.4 用户使用系统调用和库函数&#xff08;lib&#xff09;概念 总结 3.初识进程3.1 基本事实与引入3.2…

动手学深度学习:CNN和LeNet

前言 该篇文章记述从零如何实现CNN&#xff0c;以及LeNet对于之前数据集分类的提升效果。 从零实现卷积核 import torch def conv2d(X,k):h,wk.shapeYtorch.zeros((X.shape[0]-h1,X.shape[1]-w1))for i in range(Y.shape[0]):for j in range(Y.shape[1]):Y[i,j](X[i:ih,j:jw…

删除排序链表中的重复元素(js实现,LeetCode:83)

看到这道题的第一反应是使用快慢指针&#xff0c;之前做过类似的题&#xff1a;删除有序数组中的重复项&#xff08;js实现&#xff0c;LeetCode&#xff1a;26&#xff09;原理都是一样,区别是这题需要将重复项删除&#xff0c;所以只需要走一遍单循环就可以实现 /*** Defini…

单片机自学总结

自从工作以来&#xff0c;一直努力耕耘单片机&#xff0c;至今&#xff0c;颇有收获。从51单片机&#xff0c;PIC单片机&#xff0c;直到STM32&#xff0c;以及RTOS和Linux&#xff0c;几乎天天在搞:51单片机&#xff0c;STM8S207单片机&#xff0c;PY32F003单片机&#xff0c;…

Unity教程(二十二)技能系统 分身技能

Unity开发2D类银河恶魔城游戏学习笔记 Unity教程&#xff08;零&#xff09;Unity和VS的使用相关内容 Unity教程&#xff08;一&#xff09;开始学习状态机 Unity教程&#xff08;二&#xff09;角色移动的实现 Unity教程&#xff08;三&#xff09;角色跳跃的实现 Unity教程&…

HTML5扫雷游戏开发实战

HTML5扫雷游戏开发实战 这里写目录标题 HTML5扫雷游戏开发实战项目介绍技术栈项目架构1. 游戏界面设计2. 核心类设计 核心功能实现1. 游戏初始化2. 地雷布置算法3. 数字计算逻辑4. 扫雷功能实现 性能优化1. DOM操作优化2. 算法优化 项目亮点技术难点突破1. 首次点击保护2. 连锁…

【Git学习笔记】Git分支管理策略及其结构原理分析

【Git学习笔记】Git分支管理策略及其结构原理分析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;Git学习笔记 文章目录 【Git学习笔记】Git分支管理策略及其结构原理分析前言一.合并冲突二. 分支管理策略2.1 分支策略2.2 bug分支2.3 删除临…

Spring Cloud Alibaba Nacos 2023.X 配置问题

文章目录 问题现象&#xff08;一&#xff09;解决方法&#xff08;一&#xff09;问题现象&#xff08;二&#xff09;解决方法&#xff08;二&#xff09;问题现象&#xff08;三&#xff09;解决方法&#xff08;三&#xff09; 问题现象&#xff08;一&#xff09; Spring…

厨卫行业供应链产销协同前中后大平台现状需求分析报告+P120(120页PPT)(文末有下载方式)

资料解读&#xff1a;厨卫行业供应链产销协同前中后大平台现状需求分析报告 详细资料请看本解读文章的最后内容。在当前厨卫行业竞争激烈的市场环境下&#xff0c;企业的发展战略和业务模式创新至关重要。本次解读的报告围绕某厨卫企业展开&#xff0c;深入探讨其供应链产销协同…

我在哪,要去哪

在直播间听到一首好听的歌《我在哪&#xff0c;要去哪》-汤倩。 遇见的事&#xff1a;21~24号抽调去招生。 感受到的情绪&#xff1a;公假吗&#xff1f;给工作量吗&#xff1f;月工作量不够扣钱吗&#xff1f;报销方便吗&#xff1f;有事情&#xff0c;从来不解决后顾&#x…

SpringBoot-2整合MyBatis以及基本的使用方法

目录 1.引入依赖 2.数据库表的创建 3.数据源的配置 4.编写pojo类 5.编写controller类 6.编写接口 7.编写接口的实现类 8.编写mapper 1.引入依赖 在pom.xml引入依赖 <!-- mysql--><dependency><groupId>com.mysql</groupId><artifac…