【C++】string的模拟实现

目录

  • 一、`string`的模拟实现
    • [1.1 解决命名冲突问题](#1.1 解决命名冲突问题)
    • [1.2 `string`的成员](#1.2 string的成员)
    • [1.2 构造和析构函数](#1.2 构造和析构函数)
    • [1.3 `c_str`、重载`<<、[]`、`reverse`、`push_back`](#1.3 c_str、重载<<、[]reversepush_back)
    • [1.4 `append`、重载`+=`](#1.4 append、重载+=)
    • [1.5 `pop_back()`](#1.5 pop_back())
    • [1.6 `insert()`和`erase()`](#1.6 insert()erase())
    • [1.7 `find()`](#1.7 find())
    • [1.8 `sustr()`、`拷贝构造`、`operator=`和`clear()`](#1.8 sustr()拷贝构造operator=clear())
    • [1.9 `范围for`](#1.9 范围for)
    • [1.10 `operator>>`和`getline`](#1.10 operator>>getline)
    • [1.11 关系运算符重载](#1.11 关系运算符重载)
  • 二、拷贝构造和赋值运算符重载的现代写法
  • 三、关于库中`string`类的补充

个人主页<---请点击
C++专栏<---请点击

一、string的模拟实现

前面的博客我们了解了string的使用,那我们就一起来看看如何模拟实现吧,string的模拟实现部分,为了便于代码管理,我们依旧会实现三个部分,分别是test.cpp、string.h、string.cpp

1.1 解决命名冲突问题

我们设计的string会和库里面的string名字相同,为了避免和库里的产生冲突,所以我们要把string放在一个我们定义的命名空间中 ,相同的名称的命名空间编译器会认为是同一个命名空间,所以我们会在头文件和.cpp文件中定义两个相同的命名空间。

cpp 复制代码
//string.h:
namespace STR
{
	class string
	{
	public:

	private:
	};
}
//string.cpp:
namespace STR
{

}

1.2 string的成员

cpp 复制代码
private:
	char* _str = nullptr;
	int _size = 0;
	int _capacity = 0;
public:
	const static size_t npos;

其中我们知道有一个npos它是库里面定义的静态成员变量 ,而且是公有的 ,因为我们可以单独使用npos,那它的初始化要在外面初始化,所以我们在string.cpp中实现初始化。

cpp 复制代码
namespace STR
{
	const size_t string::npos = -1;
}

1.2 构造和析构函数

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

构造函数

cpp 复制代码
string::string(const char* str)
	:_size(strlen(str))
{
	_capacity = _size;
	_str = new char[_size + 1];
	memcpy(_str, str, _size + 1);
}

注意点
为什么不都在初始化列表初始化呢 ?因为那样计算调用的strlen函数比较多,由于成员变量走初始化列表是由定义时的顺序走的,所以这样写是最佳方案,只需要调用计算一次。如果你把成员定义的顺序改变了,那也可以,但如果等之后有其他程序员维护时,再给你改过来,就极有可能出错。

使用测试

cpp 复制代码
std::string s1("heihei\0\0\0ok~");
STR::string s2("heihei\0\0\0ok~");

由于暂时没有重载<<操作符,所以不能打印,我们可以通过内存查看:

从内存中可以看出s1s2中存放的字符串相同,我们的构造函数没有问题。

析构函数

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

1.3 c_str、重载<<、[]reversepush_back

  • c_str
    我们知道c_str函数是将string对象转换为const char*类型的C语言风格字符串。
cpp 复制代码
const char* string::c_str() const
{
	return _str;
}

这样就可以执行打印操作:

当然这和重载<<有着本质的区别 ,它只能输出'\0'之前的字符,但string类对象中是可能有'\0'的。

  • reverse
    reserve函数的主要功能是为字符串预先分配内存空间 ,避免在后续操作(如添加字符)时频繁进行内存重新分配,从而提高性能。
cpp 复制代码
void reserve(size_t n);
void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* str = new char[n + 1];
		memcpy(str, _str, _size + 1);
		delete[] _str;
		_str = str;
		_capacity = n;
	}
}
  • push_back
cpp 复制代码
void push_back(char ch);
void string::push_back(char ch)
{
	if (_size >= _capacity)
	{
		int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

现在我们实现了push_backreverse就可以继续讨论c_str重载 <<的区别:

cpp 复制代码
std::string s1("heihei");
STR::string s2("heihei");
s1.push_back('\0');
s1.push_back('h');
s2.push_back('\0');
s2.push_back('u');
cout << s1 << endl;
cout << s2.c_str() << endl;


从内存中可以看出s1s2字符串中都存在'\0',再从s1s2打印的区别可以看出,库中的cin输出不受中间'\0'的影响,而c_str中得到的本身就是C语言风格的字符串,受到'\0'的影响。这也为我们实现<<重载函数提供了思路。

  • operator<<:
    在此之前我们可能要用到size()operator[]函数,我们先实现一下它:
    size()
cpp 复制代码
int size() const;
int string::size() const
{
	return _size;
}

operator[]:

cpp 复制代码
char& operator[](size_t i);
const char& operator[](size_t i) const;
cpp 复制代码
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];
}

注意C++string类在重载[]运算符时返回char&(字符引用)主要目的 就是允许通过[]运算符直接修改原字符串中的字符引用提供了对原始数据的直接访问,而非副本,因此任何通过引用进行的修改都会反映到原始对象上

operator<<

根据之前博客实现的函数重载 ,我们为了不倒反天罡,我们依旧实现成全局函数 ,因为设置成成员函数 ,它的默认第一个形参this指针

cpp 复制代码
ostream& operator<<(ostream& out, const string& s);
ostream& operator<<(ostream& out, const string& s)
{
	for (int i = 0;i < s.size();i++)
	{
		out << s[i];
	}
	return out;
}

再次运行

1.4 append、重载+=

append:

我们这里只模拟实现append追加C风格字符串。

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

	memcpy(_str + _size, str, len + 1);
	_size += len;
}

operator+=

cpp 复制代码
string& operator+=(char ch);
string& operator+=(const char* str);

我们要模拟实现这两个函数,此时我们之前实现的push_backappend就派上用场了。

cpp 复制代码
string& string::operator+=(char ch)
{
	push_back(ch);

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

	return *this;
}

测试

cpp 复制代码
STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;

结果

1.5 pop_back()

pop_back用于删除尾部字符。前提是字符串中要有字符

cpp 复制代码
void pop_back();
void string::pop_back()
{
	assert(_size > 0);
	_size--;
	_str[_size] = '\0';
}

测试

cpp 复制代码
STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;
s.pop_back();
cout << s << endl;

1.6 insert()erase()

insert可以在字符串的指定位置添加字符、字符串或者子串
erase可以移除字符串中指定位置或范围内的字符

cpp 复制代码
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos = 0, size_t len = npos);

insert

cpp 复制代码
string& string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size >= _capacity)
	{
		int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	//挪动数据
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size++;
	return *this;
}
string& string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		int newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
		reserve(newcapacity);
	}

	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}
	for (size_t i = 0;i < len;i++)
	{
		_str[pos + i] = str[i];
	}
	_size += len;
	return *this;
}

测试

cpp 复制代码
STR::string s("hello world!");
cout << s << endl;
s.insert(6, 'n');
s.insert(7, "ew ");
cout << s << endl;

运行

erase

cpp 复制代码
string& string::erase(size_t pos, size_t len)
{
	assert(pos <= _size);

	//全部删除
	if (len == npos || len >= _size - pos)
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		size_t i = pos + len;
		memmove(_str + pos, _str + i, _size + 1 - i);
		_size -= len;
	}
	return *this;
}

测试1

cpp 复制代码
std::string s1("This is an example sentence.");
s1.erase(10, 8);
cout << "    库中:" << s1 << endl;
STR::string s2("This is an example sentence.");
s2.erase(10, 8);
cout << "模拟实现:" << s2 << endl;

运行1

测试2

cpp 复制代码
std::string s1("This is an example sentence.");
s1.erase(10);
cout << "    库中:" << s1 << endl;
STR::string s2("This is an example sentence.");
s2.erase(10);
cout << "模拟实现:" << s2 << endl;

运行2

1.7 find()

find用于查找子串或字符的重要方法 。它能在字符串中搜索指定内容,并返回首次出现的位置

cpp 复制代码
size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0)  const;
cpp 复制代码
size_t string::find(char ch, size_t pos) const
{
	for (size_t i = pos;i < _size;i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}
size_t string::find(const char* str, size_t pos)  const
{
	const char* pr = strstr(_str + pos, str);
	if (pr == nullptr)
	{
		return npos;
	}
	else return pr - _str;
}

测试

cpp 复制代码
std::string s1("This is an example sentence.");
size_t pos = s1.find('s');
size_t pos1 = s1.find("exam", pos);
cout << "    库中:";
cout << pos << " " << pos1 << endl;
STR::string s2("This is an example sentence.");
size_t pos3 = s1.find('s');
size_t pos4 = s1.find("exam", pos3);
cout << "模拟实现:";
cout << pos3 << " " << pos4 << endl;

运行

1.8 sustr()拷贝构造operator=clear()

substr方法用于从当前字符串中提取并返回一个子串
clear方法用于清空当前字符串的内容,使其变为空字符串

substr

cpp 复制代码
string substr(size_t pos, size_t len) const;
string string::substr(size_t pos, size_t len) const
{
	if (len == npos || 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;
}

测试

cpp 复制代码
std::string s1("This is an example sentence.");
std::string s6 = s1.substr(5);
std::string s3 = s6;
cout << "    库中:" << s3 << endl;
STR::string s2("This is an example sentence.");
STR::string s7= s2.substr(5);
STR::string s4 = s7;
cout << "模拟实现:" << s4 << endl;

此时执行以上代码时,编译器就会崩溃,原因是在执行STR::string s4 = s7;语句时涉及到拷贝构造 ,但我们现在没有实现拷贝构造函数,在这种情况下,编译器会是自动生成拷贝构造函数,也就是执行浅拷贝s4也确实得到了该得到的字符串,此时s4s7中的_str指向同一块空间,当return 0;的时候,同一块空间析构了两次,编译器崩溃了

s4._strs7._str的地址相同:


为了解决这个问题,我们可以将拷贝构造函数实现一下,顺便也将赋值重载函数实现一下

拷贝构造

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

再次运行代码

operator=

cpp 复制代码
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;
}

clear:

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

1.9 范围for

我们知道库里面的string类是支持使用范围for的,而范围for的底层就是转换成迭代器 (具体是begin和end)迭代器是一种用于遍历和访问字符串中字符的对象,它的本质是对指针的抽象。通过迭代器,你可以像操作指针一样遍历字符串 ,库中string类提供了很多迭代器 其中包括beginend,那我们也可以简单模拟实现一下这两个迭代器,这样就可以使用范围for了。

cpp 复制代码
typedef char* iterator;
typedef const char* const_iterator;

begin

cpp 复制代码
string::iterator string::begin()
{
	return _str;
}
string::const_iterator string::begin() const
{
	return _str;
}

end

cpp 复制代码
string::iterator string::end()
{
	return _str + _size;
}
string::const_iterator string::end() const
{
	return _str + _size;
}

这样我们就把beginend迭代器简单实现出来了,我们来测试一下吧!

测试

cpp 复制代码
STR::string s("This is an example sentence.");
for (auto& ch : s)
{
	cout << ch;
}

运行

虽然我们实现的迭代器可以使用范围for但迭代器是很复杂 的,C++中有一个typeid运算符,它能获取表达式的真实类型信息,我们可以使用它来窥知一二。

cpp 复制代码
cout << typeid(STR::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;


库中实现的迭代器类型是类类型!

1.10 operator>>getline

operator>>:

在实现这个函数时有一个注意点,就是内部不能用cin来读取数据,因为cin它不会读取空格或者换行,它会认为空格或者换行是分隔符 ,也就是说如果你用cin来实现读取,将永远不会停止 ,你输入一个任意字符cin读取或者忽略。

这里的解决方法 就是使用getchar或者get,我们推荐用后者,因为getcharscanf它们都属于C语言的流,我们在实现C++的东西,所以使用get更合理。

cpp 复制代码
istream& operator>>(istream& in, string& s)
{
	//库中每次读取都会清空
	s.clear();

	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

测试

cpp 复制代码
std::string s1;
cin >> s1;
cout <<"    库中:" << s1 << endl;
STR::string s2;
cin >> s2;
cout <<"模拟实现:" << s2 << endl;

运行

getline:

库中实现了两个,我们可以把这两个合并成一个:

cpp 复制代码
istream& getline(istream& in, string& s, char delim = '\n');

默认情况下,读到换行符'\n'不就是图片中第二种嘛

cpp 复制代码
istream& getline(istream& in, string& s, char delim)
{
	s.clear();

	char ch = in.get();
	while (ch != delim)
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

测试

cpp 复制代码
STR::string s;
STR::getline(cin, s);
cout << "输出:" << s << endl;
STR::getline(cin, s, 'l');
cout << "输出:" << s << endl;

运行

虽然我们把operator>>getline实现出来了,但这样写是很不好的一种写法,因为当读取的字符太多会带来很多次的扩容。

我在reserve函数中加了这样一句话:cout << "_capacity:" << _capacity << endl;,我们一起看一下读取字符很多的情况:

cpp 复制代码
STR::string s;
cin >> s;
cout << s << endl;

结果

这种情况下有7、8次扩容,扩容频率很高,那这时的代码就不好,那我们怎么优化呢?

我们可以提前开辟一定空间的字符数组,然后等数组满了,再将它转移到字符类对象中去,这样可以减少扩容次数。

优化

cpp 复制代码
istream& operator>>(istream& in, string& s)
{
	//库中每次读取都会清空
	s.clear();
	char buff[128];
	int i = 0;

	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

istream& getline(istream& in, string& s, char delim)
{
	s.clear();
	char buff[128];
	int i = 0;

	char ch = in.get();
	while (ch != delim)
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

再次运行

这样就大大减少了扩容次数,同时这样写还有一个优点,就是buff数组的大小是可变的如果还嫌扩容次数多,你可以开到256、1024,这样就很少了!

1.11 关系运算符重载

cpp 复制代码
bool operator<(const string& s) const;
bool operator<=(const string& s) const;
bool operator>(const string& s) const;
bool operator>=(const string& s) const;
bool operator==(const string& s) const;
bool operator!=(const string& s) const;

它们没有必要全部实现,只要实现一部分,其他都可以调用已经实现的来实现。

operator<

cpp 复制代码
bool string::operator<(const string& s) const
{
	size_t i1 = 0, i2 = 0;
	while (i1 < _size && i2 < _size)
	{
		if (_str[i1] < s._str[i2])
		{
			return true;
		}
		else if (_str[i1] > s._str[i2])
		{
			return false;
		}
		i1++;
		i2++;
	}
	return i2 < s._size;
}

最后一行中,如果条件为真,说明s1s2比较完成后,s2依旧剩余字符串,说明s1小于s2,否则s1不小于s2

operator==

cpp 复制代码
bool string::operator==(const string& s) const
{
	size_t i1 = 0, i2 = 0;
	while (i1 < _size && i2 < _size)
	{
		if (_str[i1] < s._str[i2])
		{
			return true;
		}
		else if (_str[i1] > s._str[i2])
		{
			return false;
		}
		i1++;
		i2++;
	}
	return i1 == _size && i2 == s._size;
}

其余的

cpp 复制代码
bool string::operator<=(const string& s) const
{
	return *this < s || *this == s;
}
bool string::operator>(const string& s) const
{
	return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
	return !(*this < s);
}
bool string::operator!=(const string& s) const
{
	return !(*this == s);
}

测试

cpp 复制代码
STR::string s1("hello");
STR::string s2("hello~");
cout << (s1 < s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;

二、拷贝构造和赋值运算符重载的现代写法

我们上面实现的是传统的写法,较为复杂,而现代的写法简洁,C++库中有一个swap函数,它可以交换任意两者。

从上图可以看出它使用了函数模板进行实现。

我们可以利用swap来实现这两个函数。
swap

cpp 复制代码
void swap(string& s);
void string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

拷贝构造

cpp 复制代码
//传统写法
string::string(const string& s)
{
	_str = new char[s._capacity + 1];
	memcpy(_str, s._str, s._size + 1);
	_size = s._size;
	_capacity = s._capacity;
}
//现代写法
string::string(const string& s)
{
	string tmp(s._str);
	swap(tmp);
}

operator=

cpp 复制代码
//传统写法
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& string::operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s);
		swap(tmp);
	}
	return *this;
}
//再次优化
string& string::operator=(string s)
{
	if (this != &s)
	{
		swap(s);
	}
	return *this;
}

现代写法更简洁,但效率是没有优化的 ,你可以将这种写法理解为"资本",让别人干完活,然后交换。

关于string类中的全局函数swap

在观察string类的时候,你会发现string中还有一个全局的swap函数,既然库中已经有了swap函数,string类也有成员函数swap,那为什么string中还要定义一个全局函数swap呢?仔细观察库中的swap函数你会发现,swap函数中对于string类对象通过一次拷贝构造,两次赋值操作完成交换 ,这样它的效率低下 ,对于string而言不是最优解 ,所以string中又定义了一个全局的swap函数。

这个全局的swap函数内部通常调用string类的成员函数swap来进行指针和数值的交换,从而完成string类对象的交换

cpp 复制代码
void swap(string& x, string& y);
void swap(string& x, string& y)
{
	x.swap(y);
}

注意当有函数模板swap和现成可以使用的函数swap时,编译器不会再次生成swap函数,而是使用现有的swap函数

三、关于库中string类的补充

当我们执行调试以下代码:

cpp 复制代码
std::string s("hello");
s += "1111111111111111111111111111111111111111";
cout << s << endl;

你会发现当s中的字符比较短时,它会存放在定长数组_buf中,而当s中的字符比较长时,它就转移到了_ptr中,当你反复尝试,你会发现当字符长度len<16时串存在_buf数组中,而当len>=16时就转移到了_ptr中,也就是说这个定长数组长度为16这样的设计能够有效避免刚开始扩容次数多的问题,我们可以通过一段代码,来观察奥妙。

cpp 复制代码
std::string s;
int n = s.capacity();
for (int i = 0;i < 300;i++)
{
	s += '1';
	if (s.capacity() != n)
	{
		cout << "s.capacity():" << n << endl;
		n = s.capacity();
	}
}

从运行结果上可以看出,s的空间一开始不是从0开始的,而是从15,而且第一次扩容和第二次扩容和其它的情况明显不同,第一次扩容是以2倍,而其余都是约为1.5倍,因为_buf数组的存在才导致的差异。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
struggle202515 分钟前
torchmd-net开源程序是训练神经网络潜力
c++·人工智能·python·深度学习·神经网络
xuanzdhc18 分钟前
C++重点知识详解(命名空间,缺省参数,函数重载)
开发语言·c++
虾球xz1 小时前
CppCon 2017 学习:Howling at the Moon: Lua for C++ Programmers
开发语言·c++·学习·lua
monicaaaaan2 小时前
旋转图像C++
开发语言·c++
无影无踪的青蛙2 小时前
[C++] STL数据结构小结
开发语言·数据结构·c++
景彡先生2 小时前
C++中的指针与引用
开发语言·c++
多吃蔬菜!!!2 小时前
C++模板基础
java·c++·算法
灵性花火2 小时前
Qt + C++ 入门2(界面的知识点)
开发语言·c++·qt
大白菜13242 小时前
C++11的一些特性
开发语言·c++
whoarethenext3 小时前
使用 C++/OpenCV 构建中文 OCR 系统:实现账单、发票及 PDF 读取
c++·opencv·ocr