C++入门小馆: 深入string类(二)

嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!

我的博客:yuanManGan

我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记  闲言碎语小记坊 题山采玉 领略算法真谛

目录

string的成员变量

成员变量

c_str和size( ),capacity( )

默认成员函数:

string的默认构造

无参构造:

带参构造: 

string的析构函数:

string 的拷贝构造:

赋值运算符重载:

尾插相关操作

string 的reserve(扩容)

string的push_back

string的append(追加字符串)

string重载运算符+=

string的遍历:

重载[ ]:

迭代器:

范围for:

string 在任意位置插入删除 

insert

​编辑erase 

​编辑 string中的查找和裁剪

find:

 substr:

 补充拷贝构造和赋值运算符重载的现代写法:

swap

拷贝构造

迭代区间构造

重载赋值运算符

流插入流提取操作符的重载

cout

​编辑

cin

clear


本章来模拟实现一下string类,不是按照模板实现,而是按照容易理解的实现。

string的成员变量

我们的string类本质还是字符数组,但我们可以动态开辟,用_str字符指针来指向数组,我们还需要知道数组的空间大小,以及有效字符个数,跟之前实现的顺序表有点类似,但这里用类来实现。

我们将string放在一个命名空间里面,以防和库里的冲突。

namespace refrain
{
	class string
	{
	public:
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

成员变量

c_str和size( ),capacity( )

这里为了方便打印,我们先实现这个返回c类型的字符串,就是j将_str返回,随便也实现另外俩个成员变量的返回

我们将声明与定义分离,写在不同的文件里。

const char* string::c_str() const
{
	return _str;
}
size_t string::capacity() const
{
	return _capacity;
}
size_t string::size() const
{
	return _size;
}

默认成员函数:

string的默认构造

无参构造:

_size 和_capacity好处理,都是0但_str应该初始化为什么,是空指针还是什么,不如看看库里面是怎么实现的?

库里面是'\0'那我们就按照它的来实现吧!那就意味着我们一开始就得开一个'\0'的空间。但我们的capacity和size不要记录这个'\0'的空间。

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

带参构造: 

我们带参构造就将传入的参数直接拷贝过去就好了。

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

看看这种写法,用了三次strlen时间成本大大提高了,我们可不可以在初始化列表先将_size初始化,然后复用_size呢? 

我们试试:

这里为什么_str没有创建空间呢?我们回忆一下初始化列表是按照怎么顺序,对是按照变量声明的顺序,我们先声明的_str,但此时_size还未初始化,_size的值看编译器实现,这里vs将_size初始化为了0,所以只有一个空间。

那有同学就要说了,那我们将声明顺序改一下能不能实现呢,我们试试。

ok了,但这样真的好吗,你这不是自己给自己埋雷吗,万一别人不知道,在这里乱改一下,那怎么办?

我们可以考虑只在初始化列表初始化_size让_str和_capacity走初始化函数。

依旧ok。

还有个问题,我们可不可以给缺省值,就不用写默认无参构造了?

最终版本:

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

string的析构函数:

这个就简单了,但要判断一下,如果_str为空就不能析构

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

string 的拷贝构造:

//传统写法
string::string(const string& s)
{
	_str = new char[s._capacity + 1];
	memcpy(_str, s._str, s._size + 1);
	_size = s._size;
	_capacity = s._capacity;
}

赋值运算符重载:

// s1 = s2
string& string::operator=(const string& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);
		delete[] _str;

		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}

	return *this;
}

尾插相关操作

string 的reserve(扩容)

扩容是一个会频繁调用的操作,所以我们先来实现一下这个操作。

这是库里实现的。

void reserve(size_t n);

 先在string.h写个声明,在string.cpp里实现这个函数。我们要将容量扩容到n,如果n>capacity,就直接新创建一块空间然后拷贝,有人说不能用relloc吗,我的建议是不要使用,因为relloc扩容扩的空间大了,也是重新创建一块空间进行扩容,然后拷贝。

那如果n <capacity呢,我们看编译器,可能缩容,但一般不缩容,我们就不实现这个了,缩容是典型的以时间换空间的案例。

void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		//注意这里是n+1给'\0'留一点空间
		char* tmp = new char[n + 1];
        //要判断_str是否为nullptr对空指针解引用要报错
		if (_str)
		{
			memcpy(tmp, _str, _size + 1);
			delete[] _str;
		}
		_str = tmp;
		_capacity = n;
	}
}

string的push_back

加入函数得先判断一下是否需要扩容,当_size == _capacity 时需要扩容。然后将最后一个字符改成要加入的值,再将_size++最后将最后一个位置弄成'\0'

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

string的append(追加字符串)

这里的扩容逻辑就得考虑一下了,如果我们插入的字符串的长度是len,如果len + _size > _capaticy时才会扩容,我们是扩二倍,还是len + _size 呢,如果给多少扩多少时,我们会面临一个问题:就是如果我们频繁扩小字符串,就会频繁扩容;如果我们扩二倍,如果我们扩容的字符串很大,len + _size > 2 * _capacity就出现了一个很严重的问题,我存的值不见了,就好比你去银行存了几百万,结果一查就省几十万了,谁还敢存钱在你们银行。

我们这里就得分类讨论一下了,如果len+_size > 2*capacity,我们就扩容到len  +_size,没有就扩到2倍。

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

string重载运算符+=

实现了push_back和append实现+=运算符就易如反掌了,只需要复用加重载就完成了。

string& string::operator+=(char ch)
{
	push_back(ch);

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

	return *this;
}
char& string::operator[](size_t i)
{
	return _str[i];
}
const char& string::operator[](size_t i) const
{
	return _str[i];
}

我们再来实现一下string的遍历吧

string的遍历:

重载[ ]:

这个实现很简单。直接返回*(_size + i);

char& string::operator[](size_t i)
{
	assert(i < _size);

	return _str[i];
}
const char& string::operator[](size_t i) const
{
	assert(i < _size);

	return _str[i];
}

迭代器:

这里实现迭代器就使用原生指针了,但底层实现不一定是原生指针,可能是其他的主要看编译器想怎么实现。

string::iterator string::begin()
{
	return _str;
}
string::iterator string::end()
{
	return _str + _size;
}
string::const_iterator string::begin() const
{
	return _str;
}
string::const_iterator string::end() const
{
	return _str + _size;
}

范围for:

实现了迭代器就实现了范围for,范围for的实质就是替换为迭代器。

string 在任意位置插入删除 

insert

insert有很多个版本,我们就实现其中比较实用的两个吧,第二个就实现插入一个吧

在pos位置插入一个字符: 

就将pos位置之后的字符全部向后挪一步,然后将pos位置改成插入的值。

void string::insert(size_t pos, char ch)
{
	assert(pos < _size);
	//判断是否需要扩容
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	size_t end = _size + 1;
	while (pos < end)
	{
		_str[end] = _str[end - 1];
		end--;
	}

	_str[pos] = ch;
	_size++;
}

在任意位置插入字符串

void string::insert(size_t pos, const char* str)
{
	assert(pos < _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
		reserve(newcapacity);
	}
	size_t end = _size + len;
	while (end >= pos + len)
	{
		_str[end] = _str[end - len];
		end--;
	}
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += len;
}

erase 

任意位置删除n个字符

这里得分类讨论一下,如果删的字符个数过多就等于把后面全删了,这种情况比较好处理,当我们第二个参数不传时,默认删完,那我们该咋实现呢?对给缺省值给你npos我们得先定义一下npos,定义成const静态成员变量

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

在c++这里可以这样给缺省值哦。

 erase函数就成这样了。

void erase(size_t pos, size_t len = npos);

 注意声明和定义不能同时给缺省值。

当删不完的时候

我们可以使用c语言的库函数memmove来移动数据(也是懒得实现了)

void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);
	//删完
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else 
	{
		memmove(_str + pos, _str + pos + len, _size - (pos + len) + 1);
		_size -= len;
	}
}

 string中的查找和裁剪

find:

我们也是简单实现这两个哦

最后一个好实现直接遍历一遍即可:

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

从第pos位置开始查找字符串sub返回最先的找到的下标

这里我们直接调用库函数里面的strstr来查找。

size_t string::find(const char* sub, size_t pos) const
{
	assert(pos < _size);
	const char* p = strstr(_str + pos, sub);
	if (p == nullptr)
	{
		return npos;
	}
	else
	{
		return p - _str;
	}
}

 substr:

创建一个string类型的ret,直接+=

string string::substr(size_t pos, size_t len)const
{
	assert(pos < _size);

	if (len > _size - pos)
	{
		len = _size - pos;
	}
	string ret;
	ret.reserve(len);
	for (size_t i = 0; i < len; i++)
	{
		ret += _str[pos + i];
	}
	return ret;
}

 补充拷贝构造和赋值运算符重载的现代写法:

swap

我们先来实现一下swap函数,有人就要问了,库里面不是有swap函数吗

看看库里面的swap

template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

看看这里是创建了一个c对象拷贝a对象,然后再赋值交换,要付出的代价有点太大了 。我们在string这个类中仅仅需要交换一下指针和_size 和_capacity就行了。

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

注意这里的函数里面swap函数必须要制定std空间,不然会认为自己调用自己导致无限递归。

但我们学习C++的有两种人,一种是只了解怎么使用string的,一种是像我们这样深入学习string库,了解底层原理,他们并不知道那种更高效,为了避免这种情况发生,我们编译器会自动调用string库里面的swap函数,无论你是下面那种代码:

swap(s1, s2);
s1.swap(s2);

 然后我们来实现一下构造函数:

我们可以将传入的对象先默认构造一份然后交换给this

拷贝构造

string::string(const string& s)
{
	string tmp(s_str);
	swap(tmp);
}

但当我们实现以下操作时得到的不是我们想要的答案 

	void test_string01()
	{
		string s1("hello world");
		s1 += '\0';
		s1 += "xxxxxx";
		string s2(s1);
		cout << s1 << endl;
		cout << s2 << endl;
	}

为什么打印不了后面的呢?

问题出在了我们进入拷贝构造后,要将目标字符串默认构造一份,此时的默认构造除了问题,其中计算_size时只会计数到'\0',会导致出现问题。

那我们咋解决呢?

在string中可以用迭代区间构造,需要使用模版,这里为什么要使用模版呢?有人说直接用string里面的迭代器不就好了。我们不只是可以使用string的迭代器,还可以用其他容器的迭代器。

迭代区间构造

template <class InputIterator>
string(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

 我们将拷贝构造改成这样就ok了。

string::string(const string& s)
{
	string tmp(s.begin(),s.end());
	swap(tmp);
}

重载赋值运算符

赋值运算符也是同样的思路

string& string::operator=(const string& s)
{
    string tmp(s.begin(), s.end());
    
    swap(s);

    return *this;
}

还有一种更简单的写法 

string& string::operator=(string tmp)
{
	swap(tmp);
	return *this;
}

我们这里自己传值传参,传值传参调用构造函数, 然后直接交换,返回*this,出作用于,tmp直接销毁。

流插入流提取操作符的重载

cout

要将该重载定义在string类外。

这个实现就很简单直接打印就行

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

看这种情况,我们打印s1的c_str( )时出现了我们不想要的结果,这是为什么呢,c_str()返回的是c类型的字符串,而c类型的字符串它以'\0'为结尾,只要发现的'\0'就返回。

cin

我们先来简单实现一个我们都爱犯的错误的代码

istream& operator>>(istream& is, string& s)
{
	char ch; is >> ch;
	s += ch;
	while (ch != '\0' && ch != '\n')
	{
		is >> ch;
		s += ch;
	}
	return is;
}

 

我们发现为什么一直得不到结果呢?因为我们的流输入操作,以空格或者换行为间隔,读取下一个,输入流(如键盘、文件)不会直接读取到 '\0''\0' 是字符串的结束符,不是输入字符)。

那我们该怎么解决呢?

c++io流中里面有一个get函数用来读取单个字符

istream& operator>>(istream& is, string& s)
{
	char ch; is.get(ch);
	s += ch;
	while (ch != '\0' && ch != '\n')
	{
		is.get(ch);
		s += ch;
	}
	return is;
}

 这里还存在一些问题就是,要把之前的数据清除掉。

这又得写个clear函数了

简单实现一下。

clear

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

这里就不实现缩容了,没必要。

 

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

最后一个小问题,我们如果频繁输入小的数据,就又会频繁扩容的问题出现,那又该怎么解决了,我们都不知道我们要输入多少的字符,也不能提前扩容。

我们可以实现一个内存池,比如开个255空间大小的内存池,当输入的小于255时就放在内存池中。

实现如下:

istream& operator>>(istream& is, string& s)
{
	s.clear();
	char buff[256];
	size_t i = 0;
	char ch = is.get();
	while (ch != '\0' && ch != '\n')
	{
		buff[i++] = ch;
		ch = is.get();
		if (i == 255)
		{
			buff[i] = '\0';
			s += buff;

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

	return is;
}

over!感谢观看! 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值