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

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

目录

1、多文件之间的关系

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

[2.1 无参构造函数](#2.1 无参构造函数)

[2.2 有参的构造函数](#2.2 有参的构造函数)

[2.3 析构函数(顺便实现)](#2.3 析构函数(顺便实现))

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

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

5、reserve、push_back、append

6、operator+=、insert、erase

7、find、substr

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

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

[10. 完结散花](#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. 完结散花

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

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

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

我们下期不见不散~~

​​​​

相关推荐
无敌岩雀1 分钟前
C++设计模式行为模式———状态模式
c++·设计模式·状态模式
Peter_chq11 分钟前
【计算机网络】数据链路层
linux·c语言·开发语言·网络·c++·后端·网络协议
重生之我是数学王子34 分钟前
QT简易项目 数据库可视化界面 数据库编程SQLITE QT5.12.3环境 C++实现
数据库·c++·qt
Darkwanderor37 分钟前
迄今为止的排序算法总结
数据结构·c++·算法·排序算法
Want5951 小时前
C/C++绘制爱心
c语言·开发语言·c++
黑不溜秋的1 小时前
C++ 编程指南06 - 不要泄漏任何资源
c++
TANGLONG2222 小时前
【初阶数据结构和算法】leetcode刷题之设计循环队列
java·c语言·数据结构·c++·python·算法·leetcode
OTWOL2 小时前
零基础学指针(下)
c语言·c++
bigbig猩猩2 小时前
C/C++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
小志biubiu2 小时前
【C++11】可变参数模板/新的类功能/lambda/包装器--C++
开发语言·c++·笔记·学习·c++11·c11