C++从入门到起飞之——string类的模拟实现 全方位剖析!

news2024/9/19 13:38:07

🌈个人主页:秋风起,再归来~
🔥系列专栏:C++从入门到起飞          
🔖克心守己,律己则安

目录

1、多文件之间的关系

2、模拟实现常用的构造函数

2.1 无参构造函数

2.2 有参的构造函数

 2.3 析构函数(顺便实现)

3、size()、capacity()、[]运算符重载

4、模拟实现简单的正向迭代器

5、reserve、push_back、append

6、operator+=、insert、erase

7、find、substr

8、非成员函数operator比较系列

9、非成员函数operator<<、operator>>

10. 完结散花


1、多文件之间的关系

>string.h

在string.h中我们用来包含各种头文件以及定义我们的string类和非成员函数的声明

注意:在string.h中string类的定义非成员函数的声明放到我们自己定义的命名空间my_string中(原因就是为了和库里面的std:string类进行区分!)

>string.cpp

在string.cpp中我们来完成string类中一些(类里面短小频繁调用的函数声明和定义不用分离)成员函数和非成员函数的定义!

注意:在string.cpp中我们要包含“string.h”并且要在命名空间域中完成成员函数和非成员函数的定义!

>test.cpp

这个文件用来测试我们的接口是否有bug!

2、模拟实现常用的构造函数

这里我们实现的是简单的string类,没有搞vs下用buff数组来存放字符串那一套,所以我们就只有三个成员变量:

private:
	char* _str;//指向字符串的指针
	size_t _size;//有效字符个数(不包含'\0')
	size_t _capacity;//空间大小(不包含'\0')

注意:这里的有效字符个数_size和空间大小_capacity都不包含'\0'!但我们实际开空间时都会多开一个来存放’\0‘ 

2.1 无参构造函数

声明:以下实现的函数都是直接在类里面定义(短小频繁调用,在类里面直接默认为inline)

>有误的无参构造函数

	string()
	:_str(nullptr)//有问题,标准库里面的是可以输出空字符串的
	, _size (0)
	,_capacity (0)
{}

在写构造函数时,一般我们都是建议显示写初始化列表的,于是我们上来可能就会写出如上的代码!不过上面的代码并不符合C++标准规定,当我们空参构造时,库里面输出的是一个空字符串,而上面写的构造函数却是不能直接访问的空指针!

在测试代码前,因为我们还没有重载流提取和流插入函数,那我们就先实现一个简单的c_str()函数来帮助我们实现打印输出!

>返回C字符串(c_str())

//返回C字符串
const char* c_str() const
{
	return _str;
}

>指定命名空间使用库里面的string

std::string s1;//指定命名空间用的是库里面的string
cout << s1.c_str() << endl;

通过调试我们发现,库里面的string空参构造一个string对象s1时, s1里面存放的是一个’\0‘,并输出一个空字符串!

>未指定命名空间,在命名空间my_string内,优先使用自己实现的string

//未指定命名空间,在命名空间my_string内,优先使用自己实现的string
string s1;
cout << s1.c_str() << endl;

 通过调试我们发现,我们模拟实现的string空参构造一个string对象s1时, s1里面存放的是一个nullptr,所以我们在输出时,程序就直接崩溃了!

>正确的无参构造函数 

那我们就按照标准库的规定来写,在走初始化列表时开一个空间来存放’\0‘即可!

string()
	:_str(new char[1]{'\0'})//实际的空间大小要比capacity大1来存放'\0'
	, _size (0)
	,_capacity (0)
{}

2.2 有参的构造函数

走初始化列表通过计算str的长度来开辟空间并确定_size和_capacity的大小,然后在函数体内将str的值拷贝到_str中,我们就完成了带参数的构造函数!

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

 不过这里并不建议走初始化列表,因为每次都要调用strlen(),有性能和效率的消耗。

建议写下面这一种版本!

string(const char* str)//加上缺省值合二为一
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];//记住开空间时多开一个
	strcpy(_str, str);//会把str的'\0'拷贝进来
}

当然,我们还可以将无参构造函数和带参构造函数合二为一! 

string(const char* str="")//加上缺省值合二为一
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];//记住开空间时多开一个
	strcpy(_str, str);//会把str的'\0'拷贝进来
}

 注意:合二为一之后我们就要将之前写的无参构造函数屏蔽掉,不然编译器不知道调用谁就会报错!(一个类中只能有一个默认构造函数(即可以无参调用的构造函数)!)

 2.3 析构函数(顺便实现)

因为有资源的申请,所以我们要显示实现我们的析构函数!

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

3、size()、capacity()、[]运算符重载

声明:以下实现的函数都是直接在类里面定义(短小频繁调用,在类里面直接默认为inline)

这些接口都比较简单我就不赘述了

//返回size和capacity
size_t size() const
{
	return _size;
}
size_t capacity() const
{
	return _capacity;
}
//[]运算符重载
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];
}

好啦!到这里,我们配合一下前面实现的一些接口,测试一下有没有什么问题! 

string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
	cout << s1[i];
}
cout << endl;
for (size_t i = 0; i < s1.size(); i++)
{
	s1[i] += 2;
	cout << s1[i];
}

好,这里看到结果也是没有任何问题的呢!

4、模拟实现简单的正向迭代器

声明:以下实现的函数都是直接在类里面定义(短小频繁调用,在类里面直接默认为inline)

上面用下标访问遍历了我们的string对象,范围for这么方便,那我们也来尝试用它来遍历一下吧!

string s1("hello world");
for (auto ch :s1 )
{
	cout << ch;
}

完蛋了!一写出来就给我们报了一大堆的错误!

不过,我在上一篇博客里写到了范围for的底层就是迭代器,我们冷静下来思考并结合报错就会发现原来我们自己实现的string类中目前还没有迭代器,所以我们不能用范围for来遍历s1

那我们怎么来实现string类的迭代器呢?

这里我就直接告诉大家,所有的迭代器iterator都是typedef出来的!在这里迭代器其实就是典型的封装的一种体现,所有的容器(链表,队列,树等)都有迭代器,并且使用他们的迭代器的方式都是一样的(即迭代器给我们提供了统一的接口,但其底层的实现并不相同,不过我们在使用时并不关心它底层的细节,我们只要掌握了迭代器的使用方式,就会对所有容器进行操作。这种封装的方式大大方便了我们对容器的使用!)

好啦!到这里我们就来实现一下string类里面简单的一个迭代器吧!

我们上篇文章就说过在string中,正向迭代器的使用就可以把它当做指针来看(不一定是指针),那我们在实现时不就可以参考使用原始指针的方式来实现我们的简单迭代器呢?

1. 我们将char*重新命名为iterator(即iterator就是char* 的类型)

2. 然后我们再实现begin()和end()俩个接口来返回_str的开头和结尾

typedef char* iterator;
//正向迭代器
iterator begin()const 
{
	return _str;
}
iterator end() const
{
	return _str+_size;
}

好啦!到这里我们就实现好了一个简单的正向迭代器!我们来测试一下:

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

 我们再简单的实现一下正向常量迭代器!

//正向常量迭代器
typedef const char* const_iterator;
const_iterator cbegin() const
{
	return _str;
}
const_iterator cend() const
{
	return _str + _size;
}

 这里,我们就不实现反向迭代器了(用原始指针已经解决不了了),因为它要用到一个叫适配器的东西(目前我也不知道那是啥玩意),还挺复杂的~

5、reserve、push_back、append

>reserve

标准库里面的reserve只有在预留空间大于容量时才会扩容并且决不改变有效字符个数,strcpy会拷贝到\0!

	void string::reserve(size_t n)
	{
		//标准库里面的reserve只有在预留空间大于容量时才会扩容
		//并且决不改变有效字符
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//开新空间,记得加一
			strcpy(tmp, _str);//拷贝数据到新空间
			delete[] _str;//释放旧空间
			_str = tmp;//_str指向新空间
			_capacity = n;
		}
		return;
	}

  好啦!写到这里,我们来测试一下上面的接口是否有问题!

string s1("hello world");
cout << "capacity:" << s1.capacity() << endl;
//reserve
s1.reserve(20);
cout << "capacity:" <<s1.capacity() << endl << endl;

string s1("hello world");
cout << "capacity:" << s1.capacity() << endl;
//reserve
s1.reserve(10);
cout << "capacity:" <<s1.capacity() << endl << endl;

好啦, 也是没有任何问题的!

>push_back

先判断是否需要扩容,空间为0,则开4个空间,否则二倍扩,记得要手动放一个'\0'!

void string::push_back(char c)
{
	//先判断是否需要扩容
	if (_size == _capacity)
	{	//空间为0,则开4个空间,否则二倍扩!
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size++] = c;//有效字符已经更新
	_str[_size] = '\0';//记得要手动放一个'\0'
}

  好啦!写到这里,我们来测试一下上面的接口是否有问题! 

string s1("hello world");
cout << s1.c_str() << endl;
s1.push_back('x');
cout << s1.c_str() << endl<<endl;

 OK啊!也是没有任何问题的!

>append 

先判断是否需要扩容,原字符串与追加的字符串总长度大于2 * _capacity,就开len + _size,否则二倍扩,不要忘了更新_size,strcpy会拷贝到\0!

	string& string::append(const char* s)
	{
		size_t len = strlen(s);
		if (len + _size > _capacity)
		{
			//原字符串与追加的字符串总长度大于2 * _capacity,就开len + _size,否则二倍扩
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		strcpy(_str + _size, s);
		_size += len;//不要忘了更新_size
		return *this;
	}

 好啦!写到这里,我们来测试一下上面的接口是否有问题!  

string s1("hello world");
cout << s1.c_str() << endl;
s1.append("test append ");
cout << s1.c_str() << endl << endl;

6、operator+=、insert、erase

>operator+=

直接复用push_back即可!

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

因为是复用的代码,就不测试了! 

>insert(任意位置前插入一个字符)

//任意位置前插入一个字符
string& string::insert(size_t pos, char c)
{
	assert(pos <=_size);
	//先判断是否需要扩容
	if (_size == _capacity)
	{	//空间为0,则开4个空间,否则二倍扩!
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	for (size_t i = _size; i >=pos; i--)
	{
		//把pos位置开始 的字符全部后移一个位置
		_str[i + 1] = _str[i];
	}
	_str[pos] = c;
	_size++;
	return *this;
}

 好啦!写到这里,我们来测试一下上面的接口是否有问题!

我们先尾插一个字符!

string s1("hello world");
cout << s1.insert(s1.size(), '*').c_str() << endl << endl;

没有什么问题!

我们再头插一个字符!

string s1("hello world");
cout << s1.insert(0, '&').c_str() << endl << endl;

 我的发!坏了,程序直接崩溃了。我们程序员最害怕的就是自己写的程序测试出bug来,不过我们不要慌,我们调试一下来解决问题!

按照我们的挪动逻辑,循环结束前后i的位置应该如下! 

我们来调试检查检查一下哪里出了问题!

通过调试我们发现头插前 i 的值雀氏为11

所有的数据都按我们的想法挪 动到后面去了,不过!i的值却不是-1,而是一个非常大的数值!

好了,这里我们就大概明白哪里出问题了,这里其实就是C语言遗留下来的一个坑,i的类型是size_t是无符号的整型,当i的值为-1时,其在内存中的补码是全一的序列,而它又是无符号的整型(正数),因此这全一的序列会被认为是该值的原码 (即这是整型的最大值!)

解决这个问题的方法有很多,比如可以用int来解决,不过我这里就直接改变一下挪动的逻辑啦!

for (size_t i = _size+1; i >pos; i--)
{
	//把pos位置开始 的字符全部后移一个位置
	_str[i] = _str[i-1];
}

这样挪i就不会到-1 

>insert(任意位置前插入一个字符串)

注释很详细啦,友友们认真看哦~

//任意位置前插入一个字符串
string& string::insert(size_t pos, const char* s)
{
	assert(pos <=_size);
	size_t len = strlen(s);
	if (len + _size > _capacity)
	{
		//原字符串与追加的字符串总长度大于2 * _capacity,就开len + _size,否则二倍扩
		reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
	}
	//把pos位置开始 的字符全部后移len个位置
	//1、用库函数memmove一个一个字节的拷贝挪动(注意一定要多挪动一个字节,把'\0'也挪动到后面去)
	memmove(_str + pos + len, _str + pos, (len+1) * sizeof(char));
	//2、手动挪
	/*for (size_t i = _size + len; i > pos+len-1; i--)
	{
		_str[i] = _str[i - len];
	}*/
    //一个一个字符拷贝
	for (size_t i = 0; i <  len ; i++)
	{
		_str[pos+i] =s[i];
	}
	_size += len;
	return *this;
}

>erase 

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

上面的代码是类里面的成员函数声明有缺省值npos(这是在类里面声明的一个静态的常量成员)

static const size_t npos;

注意:

//static const size_t npos=-1;
//特殊的可以在声明(类内部)处定义的static成员变量
//而且只有整形可以
//static const double d = 1.1;报错:const double类型不能包含类内初始值设定项 

不过,我们这里还是建议让静态成员变量定义到类外中,不过,我们要注意定义一定不要在头文件中,不然我们在test.cpp和string.cpp中包含了俩次npos的定义,这时在链接时编译器就找不到重复定义的成员从而发生链接错误!

所以我们在string.cpp中定义npos!

声明处给了缺省值,定义处就不能显示写缺省值了!先判断pos的有效性,再判断从pos位置开始的字符够不够删,如果不够,直接在pos位置放\0,并更新有效字符的个数为pos。如果够删,就走挪动的逻辑!

从任意位置开始删除len个字符
	string& string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			//把pos+len位置开始的字符全部前移len个位置
			//1、用库函数memmove一个一个字节的挪动(注意一定要多挪动一个字节,把'\0'也挪动到后前                
            //面去)
			//memmove(_str + pos, _str +pos+ len, (_size -pos+1) * sizeof(char));
			//2、手动挪
			for (size_t i = pos + len; i <=_size; i++)
			{
				_str[i-len] = _str[i];
			}
			_size -= len;
		}
		return *this;
	}

7、find、substr

>find(从pos位置开始找字符c)

循环遍历查找即可,没什么好说的,但要注意如果没有找到返回npos

//从pos位置开始找字符c
size_t string::find(char c, size_t pos )
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return npos;
}

 >find从(pos位置开始找字符串s)

找子串问题,可以用kmp算法,但该算法在实际应用中用的并不多(C语言中strstr用的就不是该算法),而BM算法就用的较多,不过BM算法较为复杂,有兴趣的小伙伴可以自己去查阅一下资料哦!我们下面就直接使用库里面的函数来匹配子串了!        

//从pos位置开始找字符串s
size_t string::find(const char* s, size_t pos )
{
	assert(pos < _size);
	char* ret=strstr(_str + pos, s);
	if (ret == nullptr)
	{
		return npos;
	}
	return ret - _str;
}

>substr(从pos位置开始取子串)

//从pos位置开始取子串
string string::substr(size_t pos, size_t len)
{
	if (len > _size-pos)
	{
		len = _size - pos;
	}
	string sub;
	sub.reserve(len);
	for (size_t i = pos; i < _size; i++)
	{
		sub += _str[i];
	}
	return sub;
}

测试代码: 

string s1("hello world");
string s2 = s1.substr(6);
cout << s2.c_str() << endl;

这里我就直接说结论,我们上面的代码还是有问题的,我们在vs2022debug版本上测试倒不会出现什么问题,但vs2019或更早一点的版本就会有运行时错误。因为我还没有安装更早的版本,这里就没办法演示了。

那到底是哪里出了问题呢?我们在函数里面创建了一个局部变量sub来暂时存放取到的子串,并传值返回,在传值返回时,函数还会先调用拷贝构造来拷贝一个临时的string对象,然后再用临时的string对象来拷贝构造我们在函数外面用来接收返回值的string对象s2

 不过,编译器默认的拷贝构造是浅拷贝,即将对象一个一个字节的拷贝构造另一个对象

通过调试我们发现s2的_str和sub的_str指向的是同一块空间!当sub出函数的局部作用域时,sub对象就会调用析构函数而销毁,而其所指向的空间也同时被释放! 

所以,当一个类里面有成员向内存申请资源时,我们就不能使用编译器默认生成的拷贝构造了,必须自己显示生成拷贝构造进行深拷贝

//拷贝构造(深拷贝)
string(const string& s)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

那为什么在vs2022debug版本上测试倒不会出现什么问题呢?这里简单的说一下,在进行传值拷贝构造时,编译器可能不会进行sub对象的创建,直接用临时对象拷贝构造s2,而在这里,编译器的优化更为激进直接和三为一,sub和临时对象都不创建了,直接拷贝构造s2!所以也就不存在同一块空间被多次析构的问题了!

8、非成员函数operator比较系列

这个系列比较简单,大家看看代码就明白了!

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

9、非成员函数operator<<、operator>>

> operator<<

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

> operator>>

istream& operator>>(istream& in, string& s)
{
	s.clear();//先清理有效字符

	char ch;
	in >> ch;//将流里面的字符插入到ch中
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}
	s += '\0';//末尾记得加上'\0'
	return in;
}

上面的代码看似没问题,其实这样写in读取不到缓冲区里面的换行和空格,与scanf一样,cin在读取字符时,默认将空格和换行视为字符分割符不进行读取(记住就行)!所以我们就可以用in.get()来读取每一个字符,作用和getc一样!

istream& operator>>(istream& in, string& s)
{
	s.clear();//先清理有效字符

	char ch;
	ch = in.get();//将流里面的字符插入到ch中
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	s += '\0';//末尾记得加上'\0'
	return in;
}

如果字符串很长,会有频繁的扩容消耗,可以优化一下

istream& operator>>(istream& in, string& s)
{
	s.clear();//先清理有效字符
	const size_t N = 256;
	int i = 0;
	char buff[N];//用一个buff数组做我们的缓冲
	char ch= in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == N - 1)//数组满了再把字符加进s中,避免频繁扩容
		{
			buff[i] = '\0';
			i = 0;
			s += buff;
		}
		ch = in.get();
	}
	if (i > 0)//把最后一组没有满的也加上
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

10、完整代码

>string.h

#pragma once

#include<iostream>
#include<assert.h>
using namespace std;

namespace my_string
{
	class string
	{
	public:
		//类里面短小频繁调用的函数声明和定义不用分离
		//1、无参构造函数
		//string()
		//	:_str(new char[1]{'\0'})//实际的空间大小要比capacity大1来存放'\0'
		//	, _size (0)
		//	,_capacity (0)
		//{}
		
		//	string()
		//	:_str(nullptr)//有问题,标准库里面的是可以输出空字符串的
		//	, _size (0)
		//	,_capacity (0)
		//{}
		

		//2、有参的构造函数
		string(const char* str="")//加上缺省值合二为一
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];//记住开空间时多开一个
			strcpy(_str, str);//会把str的'\0'拷贝进来
		}
		/*
		* 不建议走初始化列表,每次都要调用strlen()!
			string(const char* str)
				:_str(new char[strlen(str) + 1])
				, _size(strlen(str))
				,_capacity(strlen(str))
			{
				strcpy(_str, str);
			}
		*/
		
		//3、析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_capacity = _size = 0;
		}
		//4、返回C字符串
		const char* c_str() const
		{
			return _str;
		}
		//5、返回size和capacity
		size_t size() const
		{
			return _size;
		}
		size_t capacity() const
		{
			return _capacity;
		}
		//6、[]运算符重载
		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];
		}

		//7、实现两个简单的迭代器
		typedef char* iterator;
		//正向迭代器
		iterator begin()const 
		{
			return _str;
		}
		iterator end() const
		{
			return _str+_size;
		}

		//正向常量迭代器
		typedef const char* const_iterator;
		const_iterator cbegin() const
		{
			return _str;
		}
		const_iterator cend() const
		{
			return _str + _size;
		}

		//拷贝构造(深拷贝)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		//显示赋值运算符重载
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;
				_str = new char[s._capacity + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

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

		//下面的成员函数声明和定义分离
		//0、预留空间
		void reserve(size_t n);
		
		//1、尾插一个字符
		void push_back(char c);

		//2、追加一个字符串
		string& append(const char* s);

		//3、+=一个字符
		string& operator+=(char s);

		//4、+=一个字符串
		string& operator+=(const char* s);

		//5、任意位置插入一个字符
		string& insert(size_t pos, char c);

		//6、任意位置插入一个字符串
		string& insert(size_t pos,const char* s);

		//7、从任意位置删除len个字符
		string& erase(size_t pos, size_t len=npos);

		//8、从pos位置开始找字符c
		size_t find(char c, size_t pos = 0);

		//9、从pos位置开始找字符串s
		size_t find(const char* s, size_t pos = 0);

		//10、从pos位置开始取子串
		string substr(size_t pos = 0, size_t len = npos);

	private:
		char* _str;//指向字符串的指针
		size_t _size;//有效字符个数(不包含'\0')
		size_t _capacity;//空间大小(不包含'\0')
		static const size_t npos;
		//static const size_t npos=-1;
		//特殊的可以在声明(类内部)处定义的static成员变量
		//而且只有整形可以
		//static const double d = 1.1;报错:const double类型不能包含类内初始值设定项
		friend ostream& operator<<(ostream& out, const string& s);

	};
	//const size_t string::npos = -1;
	//非成员函数!
	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
}

>string.cpp

#define _CRT_SECURE_NO_WARNINGS

#include"string.h"

namespace my_string
{
	const size_t string::npos = -1;
	void string::reserve(size_t n)
	{
		//标准库里面的reserve只有在预留空间大于容量时才会扩容
		//并且决不改变有效字符
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//开新空间,记得加一
			strcpy(tmp, _str);//拷贝数据到新空间
			delete[] _str;//释放旧空间
			_str = tmp;//_str指向新空间
			_capacity = n;
		}
		return;
	}

	void string::push_back(char c)
	{
		//先判断是否需要扩容
		if (_size == _capacity)
		{	//空间为0,则开4个空间,否则二倍扩!
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size++] = c;
		_str[_size] = '\0';//记得要手动放一个'\0'
	}
	string& string::append(const char* s)
	{
		size_t len = strlen(s);
		if (len + _size > _capacity)
		{
			//原字符串与追加的字符串总长度大于2 * _capacity,就开len + _size,否则二倍扩
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		strcpy(_str + _size, s);
		_size += len;//不要忘了更新_size
		return *this;
	}
	string& string::operator+=(char c)
	{
		push_back(c);
		return *this;
	}
	string& string::operator+=(const char* s)
	{
		append(s);
		return *this;
	}
	//5、任意位置前插入一个字符
	string& string::insert(size_t pos, char c)
	{
		assert(pos <=_size);
		//先判断是否需要扩容
		if (_size == _capacity)
		{	//空间为0,则开4个空间,否则二倍扩!
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		
			//for (size_t i = _size; i >=pos; i--)
			//{
			//	//把pos位置开始 的字符全部后移一个位置
			//	_str[i + 1] = _str[i];
			//	//这种写法有bug
			//}
		
		for (size_t i = _size+1; i >pos; i--)
		{
			//把pos位置开始 的字符全部后移一个位置
			_str[i] = _str[i-1];
		}
		_str[pos] = c;
		_size++;
		return *this;
	}

	//6、任意位置前插入一个字符串
	string& string::insert(size_t pos, const char* s)
	{
		assert(pos <=_size);
		size_t len = strlen(s);
		if (len + _size > _capacity)
		{
			//原字符串与追加的字符串总长度大于2 * _capacity,就开len + _size,否则二倍扩
			reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
		}
		//把pos位置开始 的字符全部后移len个位置
		//1、用库函数memmove一个一个字节的拷贝挪动(注意一定要多挪动一个字节,把'\0'也挪动到后面去)
		memmove(_str + pos + len, _str + pos, (len+1) * sizeof(char));
		//2、手动挪
		/*for (size_t i = _size + len; i > pos+len-1; i--)
		{
			_str[i] = _str[i - len];
		}*/
		for (size_t i = 0; i <  len ; i++)
		{
			_str[pos+i] =s[i];
		}
		_size += len;
		return *this;
	}

	//7、从任意位置开始删除len个字符
	string& string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			//把pos+len位置开始的字符全部前移len个位置
			//1、用库函数memmove一个一个字节的挪动(注意一定要多挪动一个字节,把'\0'也挪动到后前面去)
			//memmove(_str + pos, _str +pos+ len, (_size -pos+1) * sizeof(char));
			//2、手动挪
			for (size_t i = pos + len; i <=_size; i++)
			{
				_str[i-len] = _str[i];
			}
			_size -= len;
		}
		return *this;
	}
	//8、从pos位置开始找字符c
	size_t string::find(char c, size_t pos )
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == c)
			{
				return i;
			}
		}
		return npos;
	}
	//9、从pos位置开始找字符串s
	size_t string::find(const char* s, size_t pos )
	{
		assert(pos < _size);
		char* ret=strstr(_str + pos, s);
		if (ret == nullptr)
		{
			return npos;
		}
		return ret - _str;
	}
	//10、从pos位置开始取子串
	string string::substr(size_t pos, size_t len)
	{
		if (len > _size-pos)
		{
			len = _size - pos;
		}
		string sub;
		sub.reserve(len);
		for (size_t i = pos; i < _size; i++)
		{
			sub += _str[i];
		}
		//注意一定要自己实现拷贝构造,sub是局部的
		return sub;
	}
	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);
	}
	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);
	}

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

	//>1、这样写有问题!读取不到换行和'\0'
	//istream& operator>>(istream& in, string& s)
	//{
	//	s.clear();//先清理有效字符

	//	char ch;
	//	in >> ch;//将流里面的字符插入到ch中
	//	while (ch != ' ' && ch != '\n')
	//	{
	//		s += ch;
	//		in >> ch;
	//	}
	//	s += '\0';//末尾记得加上'\0'
	//	return in;
	//}

	//>2、如果字符串很长,会有频繁的扩容消耗,可以优化一下
	//istream& operator>>(istream& in, string& s)
	//{
	//	s.clear();//先清理有效字符

	//	char ch;
	//	ch = in.get();//将流里面的字符插入到ch中
	//	while (ch != ' ' && ch != '\n')
	//	{
	//		s += ch;
	//		ch = in.get();
	//	}
	//	s += '\0';//末尾记得加上'\0'
	//	return in;
	//}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();//先清理有效字符
		const size_t N = 256;
		int i = 0;
		char buff[N];//用一个buff数组做我们的缓冲
		char ch= in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)//数组满了再把字符加进s中,避免频繁扩容
			{
				buff[i] = '\0';
				i = 0;
				s += buff;
			}
			ch = in.get();
		}
		if (i > 0)//把最后一组没有满的也加上
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}

>test.cpp


#include"string.h"
using namespace my_string;
namespace my_string
{
	void test_my_string1()
	{
		string s1;
		cout << s1.c_str() << endl;
		
		
		/*string s2("hello world");
		cout << s2.c_str() << endl;*/
	}
	void test_my_string2()
	{
	/*	string s1("hello world");
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i];
		}
		cout << endl;
		for (size_t i = 0; i < s1.size(); i++)
		{
			s1[i] += 2;
			cout << s1[i];
		}
		cout << endl;*/
		//string::const_iterator it = s1.cbegin();
		//while (it != s1.cend())
		//{
		//	//*it += 2;报错:表达式必须是可修改的左值
		//	cout << *it;
		//	it++;
		//}
		//cout << endl;
		string s1("hello world");
		for (auto ch :s1 )
		{
			cout << ch;
		}
		cout<< endl;
	}

	void test_my_string3()
	{
		//string s1("hello world");
		//cout << "capacity:" << s1.capacity() << endl;
		reserve
		//s1.reserve(10);
		//cout << "capacity:" <<s1.capacity() << endl << endl;

		//push_back
		/*string s1("hello world");
		cout << s1.c_str() << endl;
		s1.push_back('x');
		cout << s1.c_str() << endl<<endl;*/
		+=
		//cout << s1.c_str() << endl;
		//s1 += '&';
		//cout << s1.c_str() << endl << endl;
		//append
		/*string s1("hello world");
		cout << s1.c_str() << endl;
		s1.append("test append ");
		cout << s1.c_str() << endl << endl;*/
		//append
		//cout << s1.c_str() << endl;
		//s1+="test+= ";
		//cout << s1.c_str() << endl << endl;
	}
	void test_my_string4() 
	{
		//Test insert
		/*string s1("hello world");
		cout << s1.insert(s1.size(), '*').c_str() << endl << endl;*/
		string s1("hello world");
		cout << s1.insert(0, '&').c_str() << endl << endl;
		//cout << s1.insert(s1.size(), "test insert s").c_str() << endl << endl;
		/*string s2("hello world");
		s2.erase(6,2);
		cout << s2.c_str()<<endl;
		s2.erase(0,8);
		cout << s2.c_str() << endl;*/
	}
	void test_my_string5()
	{
		//Test find
		string s1("hello world");
		/*size_t ret = s1.find("llo");
		cout << ret << endl;*/
		string s2 = s1.substr(6);
		cout << s2.c_str() << endl;
	}
	void test_my_string6()
	{
		//Test << >>
		string s1="hello world";
		cout << s1 << endl;
		cin >> s1;
		cout << s1;
	}
}

int main()
{
	test_my_string5();
}

11. 完结散花

好了,这期的分享到这里就结束了~

如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~

如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~

我们下期不见不散~~

​​​​

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

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

相关文章

应急响应-主机安全之文件相关命令(Linux操作系统)

目录 概述常用命令file-探测给定文件的类型选项常用选项组合 stat-查看文件信息find-不止查找文件选项测试操作常用选项 locate-比find好用的查找命令选项常用选项组合 chmod-修改文件权限suidsbit chown、chgrp 修改文件所有者/所在组umask-显示或设置创建文件的权限掩码常用选…

理解Spring框架4:事务

理解Spring框架4&#xff1a;事务 (qq.com)

等保密评整改应该申请哪种SSL证书

在等保&#xff08;信息安全等级保护&#xff09;和密评&#xff08;商用密码应用安全性评估&#xff09;整改过程中&#xff0c;申请SSL证书是提升系统安全性的重要环节。下面是等保密评应该申请什么样证书的详细建议 类型选择 1 选择国密或者双算法证书 应优先考虑使用采用…

揭秘新型安卓间谍软件LianSpy的攻击手段

自2021年起&#xff0c;俄罗斯用户已成为一种新型未被记录的安卓后门间谍软件“LianSpy”的攻击目标。 网络安全公司卡巴斯基在2024年3月发现了这款恶意软件&#xff0c;并指出其利用俄罗斯的云服务Yandex Cloud进行命令和控制&#xff08;C2&#xff09;通信&#xff0c;以避免…

2024高中生必备物品有哪些?快收下这份必备物品清单!

随着新学期的脚步临近&#xff0c;为确保学习和生活都能顺利进行&#xff0c;挑选一些实用且高效的好物是非常重要的。在如今的数字化时代下&#xff0c;即使是学生&#xff0c;仍需要一系列智能电子产品&#xff0c;这些产品不仅能够提升学习效率&#xff0c;也能让学生党们的…

声明式UI语法

一、ArkTS的基本组成 Entry // 装饰器 Component // 装饰器 struct Hello { // 自定义组件State myText: string World;build() { // UI描述Column() { // 系统组件Text(Hello ${this.myText}).fontSize(50)Divider()Button(Click me).onClick(() > { // 事件方法t…

一次性讲清AI外呼系统,再也不用人工打电话

相信大家都有了解现在接到的机器人电话越来越多&#xff0c;那么真正操作机器人代替人工打电话其实很简单&#xff0c;学会了自然是节省大量人工拨打电话的时间 为什么电销要用外呼系统|||在现代科技的迅猛发展中&#xff0c;AI机器人已逐渐在各行各业崭露头角&#xff0c;与传…

022_java.lang.ThreadLocal

ThreadLocal使用案例 在并发编程中有时候需要让线程互相协作&#xff0c;而协作可以使用共享数据的方式来实现。针对共享数据的操作就需要锁机制来控制并发行为。锁虽好&#xff0c;但是毕竟会在一定程度上让线程之间互相阻塞。前辈们认为在线程需要互相协作的前提下&#xff…

服务器测试之RAID知识梳理

最近开始整理RAID卡相关规格信息&#xff0c;所以再重新汇总整理一下RAID相关的知识点及细节&#xff0c;尽量写的详细简单使用图示让大家更好理解 1.什么是Raid&#xff1f; RAID 是英文 Redundant Array of Independent Disks 的缩写&#xff0c;中文简称为独立磁盘冗余阵列…

Nuxt3所有页面使用服务端渲染需要注意些什么?

其实服务端渲染很多时候并不是所有页面都需要使用的&#xff0c;但是如果有些项目真的需要所有页面都使用服务端渲染&#xff0c;此时服务器压力很大&#xff0c;那要如何处理更好呢&#xff1f; 一、是否所有页面都需要使用服务端渲染呢&#xff1f; 大家可参考以下这篇文…

【深度学习】基于YOLOV5模型的图像识别-目标检测的性能指标详解与计算方法

目标检测是计算机视觉中的重要任务&#xff0c;主要目的是在图像中识别并定位特定的物体。YOLO&#xff08;You Only Look Once&#xff09;系列模型作为目标检测领域的代表性方法之一&#xff0c;凭借其高效和准确的特点&#xff0c;广泛应用于实际场景中。本文通过详细介绍目…

三十一、【人工智能】【机器学习】- 自编码器 (Autoencoders)

系列文章目录 第一章 【机器学习】初识机器学习 第二章 【机器学习】【监督学习】- 逻辑回归算法 (Logistic Regression) 第三章 【机器学习】【监督学习】- 支持向量机 (SVM) 第四章【机器学习】【监督学习】- K-近邻算法 (K-NN) 第五章【机器学习】【监督学习】- 决策树…

趣测系统源码获取,搭建系统详细教程,流量主+佣金+图文+挂载

一、趣测系统是什么&#xff1f; 趣测系统是一款集合了多种趣味测试的应用软件或小程序&#xff0c;以其独特的玩法和广泛的测试种类&#xff0c;为用户提供了全新的娱乐体验。该系统涵盖了心理测试、星座测试、性格测试、能力测试&#xff08;如IQ、EQ&#xff09;、情感测试…

数据结构(5.5_1)——哈夫曼树

带权路径长度&#xff1a; 结点的权 有某种现实含义的数值(如表示结点的重要性等) 结点的带权路径长度 从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积 树的带权路径长度 树中所有的叶结点的带权路径长度之和(WPL,Weighted Path Length) 哈夫曼树的定义&…

PyTorch深度学习实践——卷积神经网络

卷积神经网络 说明 卷积神经网络就是特征提取器&#xff0c;前一部分叫Feature Extraction&#xff0c;后一部分叫classification。卷积神经网络的过程是&#xff1a;卷积&#xff08;线性变换&#xff0c;提取出重要的特征&#xff09;、激活函数&#xff08;非线性变换&…

【C++】:智能指针 -- RAII思想shared_ptr剖析

目录 一&#xff0c;内存泄漏二&#xff0c;智能指针的使用及原理2.1 RAII思想2.2 auto_ptr2.3 unique_ptr 三&#xff0c;shared_ptr(重点)3.1 shared_ptr的原理及使用3.2 shared_ptr的模拟实现1. 基本框架2. 引用计数的设计3. 拷贝构造4. 析构函数5. 赋值拷贝 3.3 shared_ptr…

Vue 3+Vite+Eectron从入门到实战系列之(三)一Electron热身运动(一)

前面我们已经把基础环境配置好了,在开始我们编写第一个页面之前,先尝试几个小的实验,体验下 electron 的乐趣。 更改我们应用的名称 系统默认的名字是从 package.json 中读取的,我们可以在这里更改。 {"name": "electron-vue3" }更改后,我们重新启动…

解决pycharm日志总是弹出“无法运行Git,未安装Git”的问题

需求分析 我电脑中安装了git&#xff0c;但是打开pycharm&#xff0c;右下角总是弹出 无法运行Git,未安装Git的日志。 解决方法 首先打开pycharm&#xff0c;按照以下路径&#xff0c;依次点击。 file -----settings-----version control -----Git----Git path(选择自己下载…

【Matplotlib】在 ax(Axes 对象)上使用 seaborn(简称 sns)绘图

在 ax&#xff08;Axes 对象&#xff09;上使用 seaborn&#xff08;简称 sns&#xff09;绘图时&#xff0c;你可以通过将 ax 作为参数传递给 seaborn 的绘图函数。这允许你将 seaborn 的图形绘制在指定的 ax 对象上&#xff0c;从而将多个图形组合在一个图形布局中。 示例代…