【C++ STL】深入理解string类的底层实现

string类的模拟实现

一.string的构造与析构函数

1.普通构造函数与析构函数

我们首先实现一下string类的基本框架,包括成员变量,构造以及析构函数

string底层其实是一个字符数组,与普通意义上的数组不同的是,string支持自动扩容,并且可以通过断言(assert)的方式来更加严格的检查越界问题,使用起来要更加的安全有效

cpp 复制代码
class string
{
public:
	//默认构造函数
	string(const char* str="")
		:_size(strlen(str))
	{
		_str = new char[_size + 1];
		_capacity = _size;
		strcpy(_str, str);
	}
	//析构函数
	~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
private:
	char* _str;
	size_t _size;
	size_t capacity;
};

分析代码:

  1. 构造函数:申请空间,并初始化string
  2. 析构函数:释放空间,避免出现内存泄露

2.拷贝构造的浅拷贝所带来的问题

我们接下来再考虑拷贝构造如何实现?
首先,通过类和对象部分的学习.我们知道:我们不实现,编译器为我们实现的默认的拷贝构造函数完成的是值拷贝也就是浅拷贝,我们的string类如果浅拷贝的话,程序就会直接崩溃.

浅拷贝的危害:

  1. 对同一块空间会析构两次,第二次1析构时程序会崩溃,因为此时的空间已经被释放,是未被申请利用的.
    2.如果我们对其中一个string进行修改,那么另一个string对象也会受到影响.

3.如何实现深拷贝

经过上述分析,为了避免浅拷贝带来的问题,我们需要在拷贝构造函数中实现深拷贝。深拷贝确保每个对象都有自己独立的内存空间,不会与其他对象共享内存。

代码示例:

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

上述是传统写法(我们自己手动释放旧空间,申请新空间,并完成内容拷贝),下面我们介绍一下现代写法:

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

现代写法就是借助构造函数来完成空间的申请和内容的拷贝,再利用swap函数来交换,这样当函数调用完成即函数栈帧销毁时,由于tmp是临时对象,会调用析构函数完成资源的清理.

二.运算符重载

1.赋值运算符重载

在C++中,当我们将一个对象赋值给另一个对象时,同拷贝构造,默认情况下,编译器会为我们生成一个浅拷贝的赋值运算符。这意味着赋值后的对象和原对象会共享同一个内存空间,这会导致和浅拷贝相同的潜在问题,特别是在一个对象被销毁时,另一个对象继续使用该内存区域会引发错误。

因此这里依然会出现浅拷贝的问题,我们都思路和拷贝构造一样,完成深拷贝.

代码示例:

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

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

	return *this;
}

整体思路和拷贝构造相似,都是先创建新空间,拷贝数据,释放旧空间.
下面我们看看令人惊叹的现代写法,只需要一行代码即可搞定:

cpp 复制代码
string& operator=(string s)
{
	swap(s);
	return *this;
}

代码分析:由于此时参数不再是引用,所以s就是一个临时对象,直接交换,出了作用域s销毁自动调用析构函数.

2.大小比较相关的运算符重载

string的大小比较是按照ASCII码来进行,依次取两个string的第一个字符,第二个字符以此类推,直至出现大小不同.

例如:

aaaabc > aaaaaaaa

*为了方便,我们主要实现<和==,其他的比较都可以复用这两个函数

代码示例:

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

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

	bool operator<=(const string& s) const
	{
		return *this < s || *this == s;
	}

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

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

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

三.迭代器的实现

迭代器是我们访问容器的一种通用方式,它可以让我们在不了解具体某个数据结构的底层的情况下,依旧完成对容器的遍历,通常情况下,我们可以将迭代器理解为像"指针"一样的东西.

这里由于string的底层是连续的物理空间,我们直接使用char* 作为string的迭代器即可.当然,在Linux和VS环境下官方库中的string的迭代器并不是简答的指针,而是将指针封装之后的一个类

这里我们可以打印迭代器的类型看一下:

cpp 复制代码
int main()
{
	cout << typeid(string::iterator).name() << endl;
	return 0;
}

在VS下,这段程序的运行结果是:

class std::_String_iterator<class std::_String_val<struct std::_Simple_types<char>> >

可以看出,是经过复杂封装之后的模板类.

我们自己实现的迭代器代码示例:

cpp 复制代码
class string
{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
	    iterator string::begin()
		{
			return _str;
		}
		
		iterator string::end()
		{
			return _str + _size;
		}
		
		const_iterator string::begin() const
		{
			return _str;
		}
		
		const_iterator string::end() const
		{
			return _str + _size;
		}
};

在定义了begin和end函数之后,我们自己的string也就支持了C++11中的范围for遍历.

四.string常用操作的实现

1.静态const成员npos的定义

通过查看文档,我们会发现在插入,删除,查找等函数中,都用了一个缺省值npos,它的类型是const static size_t,值为-1,也就是说它表示无符号整数的最大值,大约是42亿多.

对于静态成员变量的声明与定义,之前的类和对象篇有过讲解,如有需要可以前往:类和对象(下).

代码示例:

cpp 复制代码
class string {
public:
    static const size_t npos = -1;  // 可以在类内初始化
};

2.插入操作

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

			delete[] _str;
			_str = tmp;

			_capacity = n;
		}
	}
void insert(size_t pos, char ch)
{
	assert(pos < _size);

	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}

	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}

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

这里注意一个点即可: size_t(无符号整数) 与 0 相比较作为循环条件时,容易出现死循环

例如:

cpp 复制代码
size_t end = _size ;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
//当pos等于0时,会出现死循环!!!

3.查找操作

代码示例:

cpp 复制代码
	//查找一个子串
	size_t find(const char* str, size_t pos) const
	{
		char* sub = strstr(_str + pos, str);
		return sub - _str;
	}
	//查找一个字符
	size_t find(char ch, size_t pos) const
	{
		for (int i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}

4.删除操作

代码示例:

cpp 复制代码
	//从pos位置开始,删除len个字符
	void erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len > _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}

希望能让你对string的理解更加透彻,感谢观看!!!

相关推荐
lxyzcm5 分钟前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
蜀黍@猿23 分钟前
C/C++基础错题归纳
c++
古希腊掌管学习的神25 分钟前
[搜广推]王树森推荐系统笔记——曝光过滤 & Bloom Filter
算法·推荐算法
qystca26 分钟前
洛谷 P1706 全排列问题 C语言
算法
古希腊掌管学习的神31 分钟前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
赵钰老师32 分钟前
【R语言遥感技术】“R+遥感”的水环境综合评价方法
开发语言·数据分析·r语言
浊酒南街32 分钟前
决策树(理论知识1)
算法·决策树·机器学习
雨中rain38 分钟前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
就爱学编程40 分钟前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
学术头条1 小时前
清华、智谱团队:探索 RLHF 的 scaling laws
人工智能·深度学习·算法·机器学习·语言模型·计算语言学