前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【C++】深入探索string类的实现(上)

【C++】深入探索string类的实现(上)

作者头像
TANGLONG
发布2025-03-19 13:24:23
发布2025-03-19 13:24:23
3200
代码可运行
举报
运行总次数:0
代码可运行

    这篇文章我们会深入讲解string类的实现,但是我们首先要对string类有一定的了解,所以推荐可以先看看string类的使用,推荐:【C++】模板编程入门指南:零基础掌握泛型编程核心(初阶)

一、string类的结构

    string类的结构非常简单,它实际就是一个char类型的顺序表,它的结构和顺序表一致,都是一个指针再加size和capacity,如下:

代码语言:javascript
代码运行次数:0
运行
复制
namespace TL
{
	class string
	{
	public:

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

    在这里我们说明一下,string类的实现我们将采用声明和定义分离的方式实现,之后我们给出的函数实现都是cpp定义文件的实现,头文件的声明就不具体给出了

二、string类的默认成员函数

构造

    string类的构造我们就实现三个就可以了,三个接口就够我们了解string类的构造了,这三个接口分别如下:

    这三个接口还可以进行合并,比如将字符串的构造和默认构造进行合并,方法就是让这个字符串的缺省值为空串,这样不传参数就默认生成一个空串“”,我们可以通过strlen函数得到字符串的长度,然后再开空间,使用strcpy初始化我们的_str

    这里要注意的是开空间要始终比_capacity多开一个,因为字符串的特点是以\0为结尾,如果string类对象的容量为10,也就是可以存放10个字符,那么就必须多开一个空间来存放\0,接下来我们把默认构造和字符串构造结合的构造写出,如下:

代码语言:javascript
代码运行次数:0
运行
复制
//这里是cpp文件,缺省值在声明时给的空串“”
string::string(const char* s)
	:_size(strlen(s))
{
	_capacity = _size;
	//注意要多开一个空间存放\0
	_str = new char[_capacity + 1];
	//strcpy会拷贝\0
	strcpy(_str, s);
}

    接下来我们来实现另一个填充n个字符c的构造,这个构造也相对比较简单,只需要开好空间,然后通过循环填充字符即可,如下:

代码语言:javascript
代码运行次数:0
运行
复制
string::string(size_t n, char c)
	:_size(n)
{
	_capacity = _size;
	//注意要多开一个空间存放\0
	_str = new char[_capacity + 1];
	for (size_t i = 0; i < _size; i++)
	{
		_str[i] = c;
	}
	//这里不会自动给\0,需要我们手动给出
	_str[_size] = '\0';
}

析构

    析构非常简单,相当于就是之前顺序表的Destroy函数,进行资源空间的释放,如下:

代码语言:javascript
代码运行次数:0
运行
复制
string::~string()
{
	if (_str)
		delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

swap的实现

    等下在讲拷贝构造和赋值重载时我们会用到string类的swap函数,所以我们这里先简单讲解一下swap的实现,想要交换两个string类对象其实很简单,只需要把对应的指针、size以及capacity进行交换即可,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::swap(string& str)
{
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}

拷贝构造(普通写法与现代写法)

    string类的拷贝构造其实很简单,我们会讲两个写法,讲到现代写法一定会让你眼前一亮,我们先来想想如果按之前我们学习的深拷贝,我们是不是需要给当前对象开辟和被拷贝对象一样的空间,然后将对应的capacity和size也修改了即可,这就是普通写法,如下:

代码语言:javascript
代码运行次数:0
运行
复制
string::string(const string& str)
{
	//以前的写法
	if(_str)
		delete[] _str;
	_str = new char[str._capacity + 1];
	_size = str._size;
	_capacity = str._capacity;
	strcpy(_str, str._str);
}

    这样写是一定没有问题的,但是我们其实还有一个更妙的方法,就是借助编译器帮我们做这些事情,具体思路是在函数中创建一个局部对象,这个对象是str的拷贝,然后我们直接将这个局部对象的资源拿过来,也就是进行交换

    这个时候我们目标对象已经得到对应的资源了,也就是拷贝成功了,而之前的资源被我们交换到了这个局部对象上,出了作用域这个局部对象就会调用析构函数将之前的资源销毁,也就成功让编译器帮我们做事了,具体实现如下:

代码语言:javascript
代码运行次数:0
运行
复制
string::string(const string& str)
{
	//现代写法(利用构造)
	string tmp(str._str);
	swap(tmp);
	//出了作用域tmp调用析构后销毁之前的资源
}

赋值重载(普通写法与现代写法)

    赋值重载的普通写法以及现代写法和拷贝构造差不多,这里直接给出实现:

代码语言:javascript
代码运行次数:0
运行
复制
string& string::operator=(const string& str)
{
	//注意判断,不要自己给自己赋值
	if (this != &str)
	{
		//普通写法
		/*if (_str)
			delete[] _str;
		_str = new char[str._capacity + 1];
		strcpy(_str, str._str);
		_size = str._size;
		_capacity = str._capacity;*/

		//利用拷贝构造,也可以利用构造
		string tmp(str);
		swap(tmp);
	}
	return *this;
}

三、string类的修改接口

扩容reserve

    在我们实现修改接口之前我们要先来实现一下扩容接口reserve,因为在插入数据时数据可能满了,我们需要进行扩容,至于扩容逻辑我们还是按照两倍去扩容数组,同时注意判断扩容参数,如果小于当前的容量我们就不进行扩容,因为这个接口还要开放给用户使用,万一用户传错误的值就不行了,实现如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::reserve(size_t n)
{
	//如果参数小于等于容量就不予处理
	if (n <= _capacity)
	{
		return;
	}
	//2倍扩容
	size_t newCapacity = _capacity * 2;
	//如果2倍扩容不够就直接扩容到n
	if (newCapacity < n)
		newCapacity = n;
	//开新空间,注意还是要多开一个空间以后放\0用
	char* tmp = new char[newCapacity + 1];
	//数据的拷贝
	strcpy(tmp, _str);
	delete[] _str;
	_str = tmp;
	_capacity = newCapacity;
}

push_back

    尾插一个字符非常简单,跟我们顺序表的尾插差不多,但是唯一要注意的是我们的string类的底层是一个字符串,字符串以\0为结尾,而_size位置就存放的是\0,我们将其用来填充了要插入的字符,那么这个字符串就没有\0作为结尾了,在我们插入字符后要将\0添上,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::push_back(char c)
{
	//注意查看空间是否满了,如果满了就扩容
	if (_size == _capacity)
	{
		reserve(2 * _capacity);
	}
	_str[_size++] = c;
	_str[_size] = '\0';
}

pop_back

    尾删我们已经写过很多次了,实际上就是让size–就可以了,但是要注意这是删除接口,所以要保证数组中有数据,可以进行断言,同时这是一个字符串,所以还要把\0补上,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::pop_back()
{
	//确保数组不为空
	assert(_size > 0);
	//size--相当于就把它删除了
	_size--;
	//补上\0
	_str[_size] = '\0';
}

append

    我们来看看append我们实现哪些接口,如下:

    我们会实现string对象和n个字符c的追加,字符串可以通过隐式类型转换为string类对象,所以可以不用专门实现,接下来我们一个一个来讲解

    追加一个string类对象实际上就是将这个string类对象的字符串拷贝到当前对象的末尾,但是要注意算出这个字符串的长度以便于看看是否需要扩容,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::append(const string& str)
{
	size_t len = strlen(str._str);
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
	//将str的字符串拷贝到当前对象
	//注意\0会跟着一起拷贝,不需要专门给
	strcpy(_str + _size, str._str);
	_size += len;
}

    接下来我们来实现n个c字符的append,和上面插入字符串不同的就是要使用循环插入n个字符c,并且要自己手动设置\0,其它逻辑几乎相同,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::append(size_t n, char c)
{
	if (n + _size > _capacity)
	{
		reserve(n + _size);
	}
	for (size_t i = _size; i < n + _size; i++)
	{
		_str[i] = c;
	}
	_size += n;
	_str[_size] = '\0';
}

insert

    我们来看看insert我们实现哪些接口,如下:

    insert接口和append接口最大的不同就是insert是往任意位置插入元素,而append是追加,所以这里涉及到元素的挪动,至于往后挪动几个位置就要看我们要插入的字符串的长度是多少,我们要将刚好留这个字符串的长度,我们画个图分析分析:

    接下来有了上面图中的示意,我们实现insert就很简单了,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::insert(size_t pos, const string& str)
{
	assert(pos >= 0 && pos <= _size);
	size_t len = strlen(str._str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	size_t src = _size - 1;
	size_t dest = src + len;
	//这里如果不强转为int就会导致死循环和越界访问
	//因为src类型是size_t,pos为0再--会变成非常大的数
	while ((int)src >= (int)pos)
	{
		_str[dest--] = _str[src--];
	}
	//下面这句strcpy也能实现上面的拷贝功能
	//strcpy(_str + pos + len, _str + pos);
	//接下来将str中的字符串拷贝过来,注意不要拷贝\0
	strncpy(_str + pos, str._str, strlen(str._str));
	_size += len;
	_str[_size] = '\0';
}

    接下来我们来实现第二个接口,也就是在指定位置插入n个c字符,和上面的过程都差不多,只是变成了字符,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::insert(size_t pos, size_t n, char c)
{
	assert(pos >= 0 && pos <= _size);
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}
	size_t src = _size - 1;
	size_t dest = src + n;
	while ((int)src >= (int)pos)
	{
		_str[dest--] = _str[src--];
	}
	//循环填充字符
	for (size_t i = pos; i < pos + n; i++)
	{
		_str[i] = c;
	}
	_size += n;
	_str[_size] = '\0';
}

erase

    我们来看看erase的接口:

    erase接口我们就实现第一个就够用了,从pos位置开始删除len个字符,要注意它们的默认含义,就是从0位置开始删除npos个字符,由于npos非常大,所以默认相当于就是删完所有元素,在实现这个接口之前我们要把npos这个成员变量添上

    之前我们也介绍过npos,它其实是一个size_t类型的静态成员变量,并且是一个常量,它的值非常大,这个npos可以给用户使用,所以它的声明要放在类的public的限制下,那么我们怎么给它一个非常大的值呢?很简单,它是无符号整型,我们给一个-1就是size_t的最大值了,如下:

代码语言:javascript
代码运行次数:0
运行
复制
const size_t string::npos = -1;

    接下来我们就来实现erase函数,它的实现也涉及到挪动数据,只不过erase是从后往前挪动,把要删除的数据覆盖,而insert是从前往后挪动,给插入的数据留出位置,它们的原理差不多,这里我们直接给出实现,如下:

代码语言:javascript
代码运行次数:0
运行
复制
void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);
	if (len > _size - pos)
		len = _size - pos; 
	size_t src = pos + len;
		size_t dest = pos;
	for (size_t i = pos + len; i < _size; i++)
	{
		_str[dest--] = _str[src--];
	}
	//下面这句strcpy也可以实现数据的覆盖拷贝,达到删除的目的
	//strcpy(_str + pos, _str + pos + len);
	_size -= len;
	_str[_size] = '\0';
}

operator+=重载

    我们来看看+=重载有哪些接口,如下:

    我们可以发现+=的重载实际上就是append和push_back的结合体,我们直接复用代码就好了,如下:

代码语言:javascript
代码运行次数:0
运行
复制
string& string::operator+=(const string& str)
{
	append(str);
	return *this;
}

string& string::operator+=(char c)
{
	push_back(c);
	return *this;
}

operator+重载

    我们来看看+重载有哪些接口:

    C++没有将+重载写成成员函数的原因就是,可能是字符+string类对象,或者是字符串+string类对象,这两种情况用成员函数就实现不出来,所以没有将它写成成员函数

    这些接口也很好实现其实,因为我们上面大部分接口都写了,我们还是直接复用就好了,接下来我们就简单写几个,没有写到的可以自己尝试一下,复用接口就行了,如下:

代码语言:javascript
代码运行次数:0
运行
复制
//不是成员函数
string operator+(char c, const string& str)
{
	//之前实现的n个c字符的构造
	string tmp(1, c);
	//直接复用+=
	return tmp += str;
}

string operator+(const char* s, const string& str)
{
	//字符串的构造
	string tmp(s);
	//复用+=
	return tmp += str;
}

string operator+(const string& str, char c)
{
	string tmp(str);
	//利用匿名对象
	return tmp += string(1, c);
}

string operator+(const string& str, const char* s)
{
	string tmp(str);
	//利用隐式类型转换,将s转换为string然后+=
	return tmp += s;
}

    那么今天string类的实现(上)就讲到这里了,今天讲的都是string类的核心接口,希望大家都能掌握,敬请期待string实现的下篇吧     bye~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、string类的结构
  • 二、string类的默认成员函数
    • 构造
    • 析构
    • swap的实现
    • 拷贝构造(普通写法与现代写法)
    • 赋值重载(普通写法与现代写法)
  • 三、string类的修改接口
    • 扩容reserve
    • push_back
    • pop_back
    • append
    • insert
    • erase
    • operator+=重载
    • operator+重载
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档