string 类的模拟实现

内容 网站
个人主页 艾iYYY
文章代码仓库 string模拟实现

前言

我们前面了解了, string的一些基础用法, 现在我们来看看string类的底层到底是什么 .

下面我会带着大家模拟实现一下,主要是实现string类的构造, 拷贝构造, 赋值运算符重载, 以及析构函数.大家可以参考一下.

我们首先可以将代码放到不同文件管理

定义这三个文件, 头文件包含所需头文件和函数,类声明

.cpp文件写对应所需的函数, test.cpp测试写的函数.

模拟实现构造函数析构函数

我们写的函数接口尽量和库里函数接口保持一致, 这样方便我们进行检查

首先我们可以定义一个命名空间在命名空间写我们的类.

cpp 复制代码
namespace zy
{
    class string

    {}
}

由于这些构造函数都是短小且频繁调用我们可以, 将直接写在头文件中, 当inline函数使用可以提高效率.

默认构造和字符串构造

cpp 复制代码
//string s
string()
    :_str(new char[1])
    ,_size(0)
    ,_capacity(0)
{
    _str = '\0';
}
cpp 复制代码
//string = "hello world"
string(const char* str)

{
    size_t size = strlen(str);
    _size = size;
    _capacity = size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

第一个是默认构造, 第二个是带参构造, 不过这两个也可以写成一个.

cpp 复制代码
string(const char* str="")

{
    size_t size = strlen(str);
    _size = size;
    _capacity = size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

这里的""就是'\0'的意思, 需要注意的是, 我们开空间时要给'\0'开一个空间的位置, 但是string规定的,size和capacity的大小都是不包含'\0'的,所以在new空间的时候要new char[_capacity+1].

拷贝构造

cpp 复制代码
string(const string& s)
{
    
    _capacity = s._capacity;
    _size = s._size;
    _str = new char[_capacity + 1];
    strcpy(_str,s._str);
}

在拷贝构造中, 拷贝对象时, 一定是深拷贝, 而不是浅拷贝.

浅拷贝:_ str=s._ str;

这样写的话, 如果是s1=s2,就是s1和s2共同指向同一块空间

就像上图这样的场景, 当出了s1和s2所在的作用域时, 会调用析构函数, 那么这块空间就会析构两次, 就会出现程序报错.

而且当你改变其中一个对象的内容时, 另一个对象也会接着发生改变.

正确的写法就是, 新开辟一块空间, 然后再将s2的内容拷贝到新开辟的空间中.

这样析构的时候就不会受到影响了.

赋值重载

cpp 复制代码
string& operator=(const string& s)
{
    if (*this != s)
    {
        _size = s._size;
        _capacity = s._capacity;
        _str = new char[_capacity + 1];
        strcpy(_str, s._str);
    }
    return *this;
}

同理赋值重载时同样要进行深拷贝.

析构函数

cpp 复制代码
~string()
{
    delete[]_str;
    _size = _capacity = 0;
}

这里的delete\[\] 别忘写\[\] 与前面对应.

string类的遍历的模拟实现

首先是operator\[\] 重载

cpp 复制代码
char& operator[](size_t index)
{
    assert(index < _size);
    return _str[index];
}

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

这样我们就可以通过 \[\] 遍历数组了

迭代器的实现

这里我们以指针的形式实现一下简单的迭代器.

typedef char* iterator;我们在类中typedef一下, 这时iterator就是指针的形式了

这样一来begin和end的迭代器就简单实现了

cpp 复制代码
iterator begin()
{
    return _str;
}

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

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

有了begin和end我们就可以使用范围for来遍历数组了

不过需要注意的是这里的begin和end需要一个字母都不差的写好, 如果Begin这样写就会发生报错. 这侧面也说明了范围for就是傻的进行替换.

string空间和内容的操作

cpp 复制代码
size_t size()const
{
    return _size;
}

size_t capacity()const
{
    return _capacity;
}

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

这些短小函数也可以直接放在头文件中.

扩容函数

cpp 复制代码
void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* temp = new char[n + 1];
		strcpy(temp, _str);
		delete[]_str;
		_str = temp;
		_capacity = n;
	}
}

扩容时先申请一块空间, 然后再将原字符串的内容拷贝过来, 拷贝完后即可将,原空间释放销毁, 最将新开空间赋值给_str 并跟新一下空间大小.

这里一定注意给'\0' 额外多开一个空间

而且一定是先复制拷贝再销毁原空间

尾插和+=

cpp 复制代码
void string::push_back(char c)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size] = c;
	_size++;
	_str[_size] = '\0';
}

尾插的时候要注意添加末尾的'\0'

cpp 复制代码
void string::append(const char* str)
{
	int len = strlen(str);
	
	if (len > _capacity - _size)
	{
		reserve(len+_size > 2 * _capacity ? len + _size : _capacity * 2);
	}
	strcat(_str, str);
	_size += len;
}

这里扩容的时候如果插入的字符串长度很大,就不要2倍扩容了, 直接给len+_ size的空间

cpp 复制代码
string& string::operator+=(char c)
{
	
	this->push_back(c);
	return *this;
}
string& string::operator+=(const char* str)
{
	this->append(str);
	return *this;
}

+=就是对上面函数的复用.

在任意位置插入

cpp 复制代码
string& string::insert(size_t pos, char c)
{
	assert(pos < _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	for (int i = _size+1; i > pos; i--)
	{
		_str[i] = _str[i - 1];
	}
	_str[pos] = c;
	_size++;
	return *this;
}

在任意位置后插入需要挪动数据, 挪动时需要注意的时要从后往前的挪动, 不能从前往后会发生数据覆盖.

根据插入一个数据就可以写出插入字符串的函数

cpp 复制代码
string& string::insert(size_t pos, const char* str)
{
	int len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);

	}
	for (int i = _size + len; i > pos; i--)
	{
		_str[i] = _str[i-len];
	}
	
	for (int i = pos ; i < pos + len; i++)
	{
		_str[i] = str[i - pos];
	}
	_size += len;
	return *this;
}

整体思路: 计算出需要插入字符串长度, 观察是否需要扩容, 先将pos后的数据往后挪动len个位置, 接着将字符串插入pos位置后, 最后调整_size的大小.

清除数据

cpp 复制代码
string& string::erase(size_t pos , size_t len )
{
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (int i = pos; i < _size - len; i++)
		{
			_str[i] = _str[i + len];
		}
		_size -= len;
		_str[_size] = '\0';
	}
	return *this;
}

整体思路: 先判断给定的len是否大于pos后面的数据个数, 如果大于等于后面就全部删除了, 直接在pos位置加'\0'就行.

如果小于, 需要挪动数据, 要把pos+len后面的数据挪动到, pos位置. 这时在循环中就需要找到两个位置, 一个是pos,一个是pos+len , 很明显pos+len大, 那么循环条件就是pos+len<_size .

现在第一个位置就是i=pos , 第二个就是i+len循环结束条件就是i<_size-len.

清空字符串

cpp 复制代码
void clear()
{
    _str[0]='\0';
    _size = 0;
}

查找数据

cpp 复制代码
size_t string::find(char c, size_t pos) const
{
	assert(pos < _size);
	for(int i=pos;i<_size;i++)
	{
		if (_str[i] == c)
		{
			return i;
		}
	}
	return -1;
}

size_t string::find(const char* s, size_t pos) const
{

	char* p = strstr(_str + pos, s);
	if (p != nullptr)
	{
		return _str - p;
	}
	else
	{
		return -1;
	}

遍历查找

cpp 复制代码
string string::substr(size_t pos, size_t len)
{
	assert(pos < _size);
	if (len > _size - pos)
	{
		len = _size - pos;
	}
	string sub;
	sub.reserve(len);
	for (int i = pos; i < _size; i++)
	{
		sub += _str[i];
	}
	return sub;
}

substr可以配合find一起使用,

整体模拟思路: 判断len是否大于_size-pos的大小, 如果大于调整len的大小

定义一个新类, 将pos后的数据插入.

比较大小

cpp 复制代码
bool operator<(const string& s)
{
    return strcmp(_str, s._str) < 0;
}

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

bool operator>(const string& s)
{
    return strcmp(_str, s._str) > 0;
}

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

bool operator==(const string& s)
{
    return strcmp(_str, s._str) == 0;
}

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

可以借助strcmp函数实现, 写两个之后就可以复用了.

流插入流提取

cpp 复制代码
	ostream& operator<<(ostream& _cout, const string& s)
	{
		for (auto c : s)
		{
			_cout << c;
		}
		_cout << endl;
		return _cout;
	}

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

这里的流输入要写成in.get()

  • in.get() 读取下一个字符,无论它是不是空白 (空格、换行、制表符等)。

    所以当遇到空格或换行时,ch 会被赋值为 ' ''\n',循环条件立即失败,正常退出。

  • in >> ch 则是一个格式化输入 ,它会自动跳过所有前导空白 ,然后才读取一个非空白字符。

    也就是说,in >> ch 永远不可能把空格或换行读到 ch

    因此 ch 永远不等于 ' ' 且不等于 '\n',循环条件永远为真 → 死循环

结语

以上就是string基础的模拟实现, 感觉大家观看, 欢迎大家多多讨论,谢谢大家!!!

相关推荐
cjp5601 小时前
003.LINQ在WEB API中的应用
服务器·linq
为何创造硅基生物1 小时前
C++ virtual void StartNetwork() = 0; // 纯虚:子类必须实现,否则不能 new。
c++
Lsk_Smion2 小时前
力扣实训 _ [75].颜色分类 _ 杨辉三角
数据结构·算法·leetcode
周小码2 小时前
10分钟搭建私有Git服务器:Soft Serve实战
运维·服务器·git
知无不研2 小时前
对套接字的深入理解
linux·服务器·网络·c++·socket·网络套接字
xyzzklk2 小时前
解决Salesforce无法向外发送邮件
android·java·开发语言·网络·crm·salesforce·客户关系管理
jidaowansui2 小时前
P11375 [GESP202412 六级] 树上游走
数据结构·算法
ZStack开发者社区3 小时前
VMware替代:从POC通过到生产可用,差距在哪里
服务器·云计算·gpu算力