C++——string模拟实现

目录

前言

一、string的结构

二、默认成员函数

构造函数

析构函数

拷贝构造

赋值重载

三、下标访问

operator[]

四、迭代器访问

begin/end 

五、容量相关

size/capacity

empty

clear

reserve

resize 

六、插入数据

push_back

append 

operator+=

 insert

关于insert的隐式类型转换引起的坑

七、删除数据

pop_back

erase

八、字符串的其他操作

c_str

find

substr

九、比较大小

十、全局函数

operator<<

operator>>

operator>>的特殊操作

getline

十一、拷贝构造和赋值重载的现代写法

swap

总结


前言

本篇文章将要来讲述string的模拟实现,对于STL的容器,我们不仅要会用,还要了解他的底层

所以我们有必要去模拟实现一下,那我们接下来就开始模拟实现啦


一、string的结构

模拟实现的时候为了跟库里的区分开,所以要加上命名空间,这样还有一点好处就是随时可以做到跟库里去对比,验证我们实现的对不对

string底层是一个char类型的顺序表,所以结构也和顺序表类似

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

_str指向存储数据的空间

_size表示有效数据的元素个数

_capacity表示当前最大的存储容量


二、默认成员函数

构造函数

构造函数呢用一个字符串来初始化,同时还有一个默认构造,也就是空串,所以我们可以用缺省参数来给值

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

这里开空间的时候需要多开一个空间给\0,同理memcpy拷贝的时候也需要多带一个\0

但是这里的效率是并不好的,strlen是一个O(N)的接口,在这里算了三次,我们算一个_size即可,剩下两个直接来使用。

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

注意:这样写还会出现一个问题,就是成员变量声明的顺序就是他在初始化列表中初始化的顺序,会先初始化_str,这时候_size还是一个随机值,开空间就开出问题了,那应该调换一下成员变量声明的顺序,最终就是如下的方式:

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

private:
	size_t _size;
	size_t _capacity;
	char* _str;
};

析构函数

析构函数负责来释放空间,new[]申请的,需要delete[]来释放,然后把指针置空,同时把

_size和_capacity变成0

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

拷贝构造

拷贝构造是需要我们自己实现的,如果浅拷贝会出现一块空间析构两次的情况

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

所以我们要自己去手动开一块空间,然后把数据带过来,再把_size和_capacity进行赋值


赋值重载

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

赋值也是同样的道理,需要自己手动开空间,并把原空间释放,然后再重新改变_str的指向,注意赋值要判断自己给自己赋值的情况以及带上返回值。


我们说剩下的两个默认成员函数取地址运算符重载我们不需要管,用编译器默认生成的即可


三、下标访问

operator[]

顺序表因为是连续的物理空间,所以可以通过下标来访问,库中提供了两个版本,一个是普通版本,一个是const版本,普通对象就调用普通版本,也可以调用const版本,是权限缩小,const对象就只能调用const版本,普通版本可读可写,const版本只读,返回字符串的引用即可

char& operator[](size_t pos)
{
	assert(pos < _size);

	return _str[pos];
}

const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

我们在这里需要断言一下,要小于_size才可以访问,所以这也就是[]和at的区别,[]是断言报错,at是抛异常。


四、迭代器访问

首先我们来看一下迭代器是如何使用的,是怎么访问数据的

std::string s1("hello world");
std::string::iterator it = s1.begin();
while (it != s1.end())
{
	cout << *it;
	++it;
}
cout << endl;

迭代器是嵌在容器里面的,迭代器就像指针一样,模拟的是指针的行为,容器的底层结构各不相同,而成员变量又是私有,类外访问不到,迭代器则提供了统一的方式访问容器,不需要暴露底层实现,而对于string来说,迭代器直接就模拟的是原生指针,所以可以直接把char*的指针typedef

typedef char* iterator;
typedef const char* const_iterator;

begin/end 

然后再提供一个begin和end的接口,begin返回的是首字符的地址,end返回的是最后一个有效字符下一个位置的地址,因为迭代器都是左闭右开,遍历到最后一个位置才能去判断结束了,所以要加上_size,begin/end和[]是一样的,也有两个版本,普通对象调用普通版本,也可以调用const版本,是权限缩小,const对象只能调用const版本,普通版本可读可写,const版本只读,而支持了迭代器就支持范围for,范围for就是单纯的傻瓜式的替换

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator begin() const
{
	return _str;
}

const_iterator end() const
{
	return _str + _size;
}

这里我们先不对反向迭代器做介绍,只需要会使用即可,后面会专门写一遍文章来讲反向迭代器,反向迭代器都是一样的,会了一个就会其他的了。


五、容量相关

size/capacity

size_t size() const
{
	return _size;
}
size_t capacity() const
{
	return _capacity;
}

提供相对应的size和capacity接口用来返回元素个数,在函数内部也不改变成员,建议加上const

empty

bool empty() const
{
    return _size == 0;
}

判空只需要判断_size是否为零就可以

clear

<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值