前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >今天你学C++了吗?——string(下)

今天你学C++了吗?——string(下)

作者头像
用户11352420
发布2025-03-06 08:50:54
发布2025-03-06 08:50:54
3500
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

成员变量

结合我们的使用,我们可以发现string事实上就是一个字符串,但是里面添加了统计容量和字节大小的两个成员变量~

因此,私有成员我们可以像下面这样:

代码语言:javascript
代码运行次数:0
复制
class my_string//与库里面的进行区分
{
private:
	char* _str;//字符指针
	size_t _size;//统计长度
	size_t _capacity;//统计最大容量
};

npos

我们可以看到在库里面的string类里面,有npos被定义为一个成员常量,这个npos是有什么作用呢?我们先来了解一下~

std::string 类里的 npos 是一个静态常量,用于表示一个无效的位置或未找到的位置。npos 的类型是 std::string::size_type,它通常是一个无符号整数类型,例如 size_tnpos 的值通常是该无符号整数类型所能表示的最大值。在查找操作(例如 findrfindfind_first_offind_last_offind_first_not_offind_last_not_of)失败时,这些函数会返回 npos

所以我们自己实现的时候也要注意加上npos这样一个静态常量,同时在类外进行初始化~

代码语言:javascript
代码运行次数:0
复制
class my_string//与库里面的进行区分
{
private:
	char* _str;//字符指针
	size_t _size;//统计长度
	size_t _capacity;//统计最大容量
	const static size_t _npos;//静态常量,用于表示一个无效的位置或未找到的位置
public:
	//成员函数

};

事实上,我们这里还可以像这样优化,为了与库里面的函数更好地区分,我们可以使用前面我们提到过的命名空间域~

代码语言:javascript
代码运行次数:0
复制
namespace Xiaodu
{
	class my_string//与库里面的进行区分
	{
	private:
		char* _str;//字符指针
		size_t _size;//统计长度
		size_t _capacity;//统计最大容量
		const static size_t _npos;//静态常量,用于表示一个无效的位置或未找到的位置
	public:
		//成员函数

	};
}

知道了内部大致的结构,接下来我们就来试一试实现它的接口~首先因为总体代码量较大,所以我们把声明和定义分开在不同文件中,这里就需要再创建一个string.cpp源文件~同时包含我们自己的头文件string.h

构造

我们知道string的构造有很多种调用方法,这里我们来模拟实现一下比较常见的接口~

注意:通过前面一篇博客我们知道成员变量里面的_size和_capacity都是不包含字符串结束标志'\0'的,但是在开空间的时候我们需要注意多开一个保证字符串正常结束~

常量字符串进行初始化

1、使用常量字符串进行初始化~ 像以前,我们初始化会建议在初始化列表进行初始化,这一次为了代码更加简便,我们首先使用初始化列表初始化string的长度~剩下的就可以使用已经初始化的长度在函数体里面进行初始化,更加高效~

代码语言:javascript
代码运行次数:0
复制
namespace Xiaodu
{
	const  size_t my_string::_npos = -1;
	//构造
	my_string::my_string(const char* str)//常量字符串
		:_size(strlen(str))//使用strlen求有效长度来初始化
	{
		//使用已经初始化的_size来初始化剩下的
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
	}
}

由于这里我们还没有对流插入流提取运算符进行重载,所以我们通过监视窗口进行查看:

默认构造

我们给了需要参数的构造函数,那么系统就不会再提供默认构造函数了,所以我们还需要自己写一个默认构造函数~

代码语言:javascript
代码运行次数:0
复制
//默认构造
my_string::my_string()
{
	_str = new char[1];
	_str[0] = '\0';//一个字符保存'\0'
	_size = 0;
	_capacity = 0;
}

n个字符进行初始化

2.使用n个字符进行初始化,那么这里就需要两个参数,一个是我们希望的字符长度,一个是初始化的字符~这里的初始化我们就可以完全使用初始化列表进行初始化,因为字符串的长度是已经知道的~

代码语言:javascript
代码运行次数:0
复制
my_string::my_string(size_t n, char ch)
	:_size(n),_str(new char[n+1]),_capacity(n)
{
	for (int i = 0; i < n; i++)
	{
		_str[i] = ch;
	}
	_str[_size] = '\0';//结束标志\0
}

我们可以发现通过这些构造函数,成功进行了初始化~

析构

有了构造,当然不能少了析构函数~通过前面的成员变量,我们知道字符指针是我们需要处理的~

以及将一些成员变量更改~

代码语言:javascript
代码运行次数:0
复制
//析构
my_string::~my_string()
{
	delete _str;//new,delete匹配使用
	_str = nullptr;
	_size = 0;
	_capacity = 0;
}

<<运算符重载

为了后面更好的使用,我们这里先进行<<的运算符重载~

前面我们就知道这个运算符在类外进行定义才更加符合我们平时的使用习惯~这里我们可以让它输出我们想输出的内容~这里为了后面好观察,我们先让它把成员都进行输出,后面再进行修改~

代码语言:javascript
代码运行次数:0
复制
ostream& operator<<(ostream& out, my_string& s)
{
	out << "_str:" << s._str << endl;
	out << "_size:" << s._size << endl;
	out << "_capacity:" << s._capacity << endl;
	return out;
}

拷贝构造

拷贝构造也是一个十分重要的点,这里我们自己实现一下~

代码语言:javascript
代码运行次数:0
复制
//拷贝构造
//s2(s1)
my_string::my_string(const my_string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

这是比较老老实实的写法,我们还有一种投机取巧的方法就是使用构造函数,让它给我构造一份,让别人实现~

代码语言:javascript
代码运行次数:0
复制
//现代写法
my_string::my_string(const my_string& s)
{
	my_string tmp(s._str);//让构造帮我们写
	swap(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);
}

赋值运算符重载

与拷贝构造相比,赋值运算符有下面的特点:

  • 已存在对象的操作
    • 赋值运算符重载是对已存在的对象进行操作的。当使用赋值运算符(=)将一个对象的值赋给另一个同类型的已存在对象时,会调用重载的赋值运算符。
  • 避免自赋值
    • 在实现赋值运算符重载时,必须考虑自赋值问题,即对象赋值给自己的情况。如果不处理自赋值,可能会导致资源释放或重新分配等不必要的操作,从而引发错误。
  • 资源的释放与重新分配
    • 如果对象包含动态分配的资源(如内存、文件句柄等),赋值运算符重载需要负责释放旧资源并重新分配新资源,以实现深拷贝。这是与拷贝构造的一个主要区别,因为拷贝构造通常只涉及新资源的分配。
  • 返回引用
    • 赋值运算符重载通常被实现为返回对象的引用(T&),这允许进行链式赋值操作。例如,a = b = c; 这样的表达式在重载了赋值运算符后是可以正常工作的。
代码语言:javascript
代码运行次数:0
复制
//赋值运算符重载
//my_string::operator=——这样指定类域
//s4=s3
my_string& my_string::operator=(my_string& s)
{
	//判断是不是自己给自己赋值
	if (this != &s)
	{
		delete _str;//释放原来的,重新开辟空间
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

reserve

在C++的std::string类中,reserve成员函数的作用是请求改变字符串的容量(capacity)。这里的“容量”指的是字符串在不进行内存重新分配的情况下可以存储的最大字符数。需要注意的是,容量一般情况下并不等同于字符串当前的长度(size),而是指字符串内部为存储字符所预留的空间大小~

这里我们自己来实现一下,通过上一篇博客,我们知道reserve只有在更改容量比它原来的容量的时候会进行扩容~其他情况下,容量不会发生变化~

代码语言:javascript
代码运行次数:0
复制
void my_string::reserve(size_t n)
{
	if (n > _capacity)//修改容量比原来的大就进行扩容
	{
		char* tmp = new char[n + 1];
		//创建一个中间指针变量修改容量
		strcpy(tmp, _str);
		delete _str;
		_str = tmp;
		_capacity = n;
	}
}

push_back

std::string类的push_back成员函数用于在字符串末尾添加单个字符。它自动处理内存管理,若当前容量不足则增加容量。通过push_back,可以逐个字符地构建或扩展字符串,无需手动管理内存分配和字符串大小调整。

代码语言:javascript
代码运行次数:0
复制
void my_string::push_back(char ch)
{
	//判断容量是否足够
	if (_capacity == _size)//容量不够进行扩容
	{
		//这里就可以使用reserve进行代码复用
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size++]=ch;//插入字符后长度增加
	_str[_size] = '\0';
}

append

在C++中std::string 类提供append 的成员函数,用于在字符串的末尾追加内容。我们知道这个函数有多个重载版本,允许我们以不同的方式追加数据。

这里我们同样实现一个常用的接口~增加一个常量字符串内容~

代码语言:javascript
代码运行次数:0
复制
void my_string::append(const char* s)
{
	//求出插入字符串长度
	size_t l = strlen(s);
	//容量不够进行扩容
	if (_size + l > _capacity)
	{
		//默认两倍扩容,还是不够就按需扩容
		size_t newcapacity = _capacity * 2;
		if (_size + l > newcapacity)
		{
			newcapacity = _size + l;
		}
		//1.自己重新写
		char* tmp = new char[newcapacity + 1];//容量记得+1,需要一个字符串结束标志
		strcpy(tmp, _str);
		delete _str;
		_str = tmp;
		_capacity = _size + l;
		//2.代码复用
		//reserve(newcapacity);
	}
	//拷贝剩下的
	strcpy(_str + _size, s);
	//_size增加
	_size += l;
}

+=运算符重载

+= 运算符可以用于将一个字符串(或字符)追加到另一个字符串的末尾~

操作内容与我们前面写到的尾插和append有一些类似,这里我们就可以进行代码复用,更加方便快捷~

代码语言:javascript
代码运行次数:0
复制
//+=一个字符
my_string& my_string::operator+=(char c)
{
	push_back(c);
	return *this;
}
//返回引用,减少拷贝
//+=一个字符串
my_string& my_string::operator+=(const char* c)
{
	append(c);
	return *this;
}

insert

库里面实现的insert也有很多的接口,这里我们只实现常用的接口~该函数用于在字符串中的指定位置插入另一个字符串或字符

1.指定位置开始插入n个相同字符

代码语言:javascript
代码运行次数:0
复制
//insert字符
void my_string::insert(size_t pos, size_t n, char c)
{
	//判断插入位置和插入个数是否合法
	assert(pos < _size);
	assert(n > 0);
	//判断容量是否足够
	//不够进行扩容
	if (_size + n > _capacity)
	{
		//默认两倍扩容,还是不够就按需扩容
		size_t newcapacity = _capacity * 2;
		if (_size + n > newcapacity)
		{
			newcapacity = _size + n;
		}
		//代码复用
		reserve(newcapacity);
	}
	//移动后面的字符
	size_t end = _size + n - 1;
	//从后面开始移动,避免覆盖
	while (end > pos + n - 1)
	{
		_str[end] = _str[end - n];
		end--;
	}
	//插入字符
	for (size_t i = pos; i < pos + n; i++)
	{
		_str[i] = c;
	}
	_size += n;
	//字符串末尾置为'\0'
	_str[_size] = '\0';
}

2.指定位置开始插入字符串

这里与前面插入一个字符逻辑是相似的,我们可以进行代码复用,也可以重新写~

方法一:重新写

代码语言:javascript
代码运行次数:0
复制
void my_string::insert(size_t pos, const char* s)
{
	//方法一:重新写
	//判断插入位置和插入个数是否合法
	size_t n = strlen(s);
	assert(pos < _size);
	assert(n);
	//判断容量是否足够
	//不够进行扩容
	if (_size + n > _capacity)
	{
		//默认两倍扩容,还是不够就按需扩容
		size_t newcapacity = _capacity * 2;
		if (_size + n > newcapacity)
		{
			newcapacity = _size + n;
		}
		//代码复用
		reserve(newcapacity);
	}
	//移动后面的字符
	size_t end = _size + n - 1;
	//从后面开始移动,避免覆盖
	while (end > pos + n - 1)
	{
		_str[end] = _str[end - n];
		end--;
	}
	//插入字符串里面的字符
	for (size_t i = pos; i < pos + n; i++)
	{
		_str[i] = s[i - pos];
	}
	_size += n;
	//字符串末尾置为'\0'
	_str[_size] = '\0';
}

方法二:代码复用

代码语言:javascript
代码运行次数:0
复制
void my_string::insert(size_t pos, const char* s)
{
	//方法二:代码复用
	size_t n = strlen(s);
	//先占位置,再重新赋值
	insert(pos, n, 'x');
	for (size_t i = 0; i < n; i++)
	{
		_str[pos + i] = s[i];
	}
}

我们可以看见,它们达到了相同的效果,显然代码复用代码量大大减少,所以我们更加推荐使用代码复用~

erase

插入我们都会了,相信删除更是不在话下~

代码语言:javascript
代码运行次数:0
复制
//erase
void my_string::erase(size_t pos, size_t n)
{
	//判断删除位置
	if (pos + n >= _size)
		//也就是后面的全部删除
	{
		_str[pos] = '\0';
		_size -= n;
	}
	else
		//删除直接进行覆盖就好了
	{
		//把后面的字符向前面移动
		size_t begin = pos + n;
		while (begin < _size)
		{
			_str[begin - n] = _str[begin];
			begin++;
		}
		_size -= n;
		_str[_size] = '\0';
	}
}

find

在C++中std::string 类提供了一个名为 find 的成员函数,用于在字符串中查找子字符串或字符~

1.查找字符 返回值:如果找到字符,则返回其位置索引;否则返回std::string::npos

代码语言:javascript
代码运行次数:0
复制
//find字符
size_t my_string::find(char c,size_t pos)
	//默认从开头开始找
{
	//遍历查找
	size_t n = strlen(_str);
	for (size_t i = pos; i < n; i++)
	{
		if (_str[i] == c)
		{
			//找到了返回下标
			return i;
		}
	}
	//找不到返回_npos
	return _npos;
}

2.查找子字符串 返回值:如果找到子字符串,则返回其第一个字符在字符串中的位置索引;否则返回std::string::npos

查找子字符串,我们可以使用库里面为我们提供的strstr标准库函数,用于在一个字符串中查找另一个字符串的第一次出现,这个函数是定义 <cstring>(或旧式的 <string.h>)头文件中~

  • str1 是要搜索的字符串(也称为源字符串)
  • str2 是要在 str1 中查找的子字符串

如果 str2str1 的子串,则 strstr 返回一个指向 str1 中第一次出现 str2 的起始位置的指针。如果 str2 不是 str1 的子串,则返回 nullptr~

有了这个库函数,我们实现就十分方便~

代码语言:javascript
代码运行次数:0
复制
//find字符串
size_t my_string::find(const char* s, size_t pos)
{
	const char* p = strstr(_str + pos, s);//从指定位置开始查找子字符串
	if (p == nullptr)//没有找到,返回_npos
	{
		return _npos;
	}
	else//找到了,返回下标索引
	{
		return p - _str;
	}
}

substr

在C++中,substr用于获取字符串的一个子串,这个函数允许你指定开始的位置(索引)以及要复制的字符数,从而从原始字符串中提取出所需的部分~需要注意的是它不修改原始字符串substr 函数不会修改调用它的原始字符串对象,而是返回一个新的字符串对象,该对象包含提取的子串~

代码语言:javascript
代码运行次数:0
复制
//substr
my_string my_string::substr(size_t pos, size_t len)
{
	size_t n = strlen(_str);
	assert(pos >= 0);
	assert(pos < n);//判断拷贝位置有效
	my_string tmp;//调用默认构造
	tmp._str = new char[len + 1];
	//进行拷贝
	for (size_t i = pos; i < pos + len; i++)
	{
		tmp._str[i - pos] = _str[i];
	}
	tmp._str[len] = '\0';
	tmp._size = tmp._capacity = len;//修改容量和长度
	return tmp;
}

字符串比较

strcmp

前面C语言阶段,我们就知道有strcmp函数可以用来进行字符串比较,当然C++兼容C语言,我们简单回顾一下,实现代码复用~

一、函数原型 strcmp用于比较两个字符串的大小~

该函数接受两个参数,分别为要比较的两个字符串的指针(const char*类型),并返回一个整数来表示两个字符串的大小关系~ 二、返回值 strcmp函数的返回值是一个整数,用于表示两个字符串的大小关系,具体规则如下:

  • 若str1小于str2,则返回一个负整数(即小于0的数)。
  • 若str1等于str2,则返回0。
  • 若str1大于str2,则返回一个正整数(即大于0的数)。

三、比较原理 strcmp函数比较两个字符串是按照字典序进行比较的,即逐个字符进行比较,直到遇到不同字符或字符串结束符'\0'。比较规则如下:

  • 首先比较两个字符串的第一个字符,若相等则继续比较下一个字符。
  • 若两个字符不相等,则返回它们的ASCII码差值(ASCII码值大的字符串大)。
  • 若在比较过程中遇到字符串结束符'\0',则停止比较。此时,若一个字符串已结束而另一个字符串还未结束,则认为未结束的字符串大于已结束的字符串。

==、!=、<、>、>=、<=

结合前面的经验,事实上,实现其中的几个,实现的通过代码复用就可以实现了,我们一起来看看代码~

代码语言:javascript
代码运行次数:0
复制
//==
//s1==s2
bool my_string::operator==(const my_string& s)const
{
	return strcmp(_str, s._str) == 0;//判断返回值是否等于0
}
//!=
bool my_string::operator!=(const my_string& s)const
{
	return !(*this == s);//代码复用
}
//<
bool my_string::operator<(const my_string& s)const
{
	return strcmp(_str, s._str) < 0;
}
//<=
bool my_string::operator<=(const my_string& s)const
{
	return (*this == s) || (*this < s);
}
//>
bool my_string::operator>(const my_string& s)const
{
	return !(*this <= s);
}
//>=
bool my_string::operator>=(const my_string& s)const
{
	return !(*this < s);
}

当然,代码复用有很多种方式,选择自己喜欢的就好了~

测试:

再看流插入/流提取运算符重载

  1. 流插入运算符(<<)
    • 定义:将一个对象或数据插入到输出流中,如cout。
    • 用途:主要用于输出数据到控制台、文件等输出流。
  2. 流提取运算符(>>)
    • 定义:从输入流中提取数据,并将其赋值给某个对象。
    • 用途:主要用于从控制台、文件等输入流中读取数据。

前面我们重载的流插入运算符重载,是为了我们方便观察,而不使用监视进行观察,接下来我们来实现真正的模拟string类里面的流插入/流提取运算符~

流插入运算符(<<)

我们先来看看库里面的<<实现连续输入的效果:

我们可以发现不同字符串之间是没有进行换行处理的,所以我们也类似这样处理~

代码语言:javascript
代码运行次数:0
复制
ostream& operator<<(ostream& out, my_string& s)
{
	cout << s._str;
	return out;
}

流提取运算符(>>)

接下来,我们来看看流提取运算符,首先看看库里面的效果~

我们不难发现,默认空格就是分隔符,所以我们输入两个字符串也就分别在s3、s4中~

我们也就可以写出下面的代码:

代码语言:javascript
代码运行次数:0
复制
//>>运算符重载
istream& operator>>(istream& in, my_string& s)
{
	char ch;
	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}
	return in;
}

可是测试的时候,我们无论是输入空格还是换行符都不会发生变化,这是因为cin像scanf函数一样根据提供的格式字符串来解析输入,会忽略起始的空白字符(如空格、制表符、换行符),并在遇到与格式不符的字符时停止读取,而cin则更加自动化,它会忽略所有的空白字符,直到遇到非空白字符为止,或者达到文件结束符(EOF),cin 默认会忽略输入流中的前导空白字符,这包括空格、制表符以及换行符,所以我们前面输入的空格和换行符被它忽略了,这个时候我们就需要调用get来获取一个个字符~

事实上,上面的代码还有一个问题就是字符串在原来的基础上增加,而我们库里面是直接覆盖的,所以再输入内容前还需要对原来的内容进行清空~

代码语言:javascript
代码运行次数:0
复制
//>>运算符重载
istream& operator>>(istream& in, my_string& s)
{
	//清理原来的字符串
	s.clear();
	char ch = in.get();//调用get
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

这样也就达到了我们想要的效果~

我们还可以继续将上面的代码进行优化,使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据,这样如果输入一个大的字符串减少扩容次数,提高效率~

代码语言:javascript
代码运行次数:0
复制
//>>运算符重载(优化版)
istream& operator>>(istream& in, my_string& s)
{
	//清理原来的字符串
	s.clear();
	const size_t N = 1024;//可以自己指定大小
    char arr[N];//使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据
	int i = 0;
	char ch = in.get();//调用get
	while (ch != ' ' && ch != '\n')
	{
		arr[i++] = ch;
		if (i == N - 1)
		{
			arr[i] = '\0';
			s += arr;//字符串很长的情况下,调用+=可以按需扩容
			i = 0;//i重新开始保存数据
		}
		ch = in.get();
	}
	if (i > 0)//arr里面还有字符数据
	{
		arr[i] = '\0';//末尾置为'\0'再进行插入
		s += arr;
	}
	return in;
}

这比前面一个个字符写入就更加高效~

getline

getline 是一个用于从输入流(如 std::cin、文件流等)读取一整行文本的函数~getline 本身不是 std::string 类的一个成员函数

这个与>>运算符重载就十分类似了,只需要把循环条件修改一下就可以了~

代码语言:javascript
代码运行次数:0
复制
//getline
istream& getline(istream& in, my_string& s, char end)
{
	//清理原来的字符串
	s.clear();
	const size_t N = 1024;
	char arr[N];//使用一个临时数组(出了当前作用域就销毁)来存储写入的字符数据
	int i = 0;
	char ch = in.get();//调用get
	while (ch != end)
	{
		arr[i++] = ch;
		if (i == N - 1)
		{
			arr[i] = '\0';
			s += arr;//字符串很长的情况下,调用+=可以按需扩容
			i = 0;//i重新开始保存数据
		}
		ch = in.get();
	}
	if (i > 0)//arr里面还有字符数据
	{
		arr[i] = '\0';//末尾置为'\0'再进行插入
		s += arr;
	}
	return in;
}

是不是十分巧妙~string类的底层实现到此就结束啦~这篇博客只进行了部分接口的实现,小伙伴们有兴趣也可以自己实现更多的接口~

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 成员变量
    • npos
  • 构造
    • 常量字符串进行初始化
    • 默认构造
    • n个字符进行初始化
  • 析构
  • <<运算符重载
  • 拷贝构造
  • 赋值运算符重载
  • reserve
  • push_back
  • append
  • +=运算符重载
  • insert
  • erase
  • find
  • substr
  • 字符串比较
    • strcmp
    • ==、!=、<、>、>=、<=
  • 再看流插入/流提取运算符重载
    • 流插入运算符(<<)
    • 流提取运算符(>>)
  • getline
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档