【C++】String的语法及常用接口底层模拟实现

发布于:2025-05-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、string类(了解)

  1. string是表示字符串的字符串类
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
  4. 不能操作多字节或者变长字符的序列。在使用string类时,必须包含#include头文件以及using namespace std;
  • C++ 的 std::string 是一个动态字符数组,支持多种操作,且是可变的。在使用string类时,必须包含#include头文件以及using namespace std;

二、string类的常用接口

2.1 string类对象的常见构造

  • 函数名称 -------功能说明
  • string() -------- 构造空的string类对象,即空字符串
  • string(const char * s) -------- 用C-string来构造string类对象
  • string(size_t n, char c) ------- string类对象中包含n个字符c
  • string(const string&s) -------- 拷贝构造函数
void Teststring()
{
	 string s1; // 构造空的string类对象s1
	 string s2("hello bit"); // 用C格式字符串构造string类对象s2
	 string s3(s2); // 拷贝构造s3
}

2.2 string类对象的容量操作

  • 函数名称 ------- 功能说明
  • size ------- 返回字符串有效字符长度
  • length ------- 返回字符串有效字符长度
  • capacity -------返回空间总大小
  • empty ------- 检测字符串释放为空串,是返回true,否则返回false
  • clear ------- 清空有效字符
  • reserve ------- 为字符串预留空间
  • resize ------- 将有效字符的个数该成n个,多出的空间用字符c填充

在这里插入图片描述

注意:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一
致,一般情况下基本都是用size()。

2. clear()只是将string中有效字符清空,不改变底层空间大小。
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。
[注意]:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。

4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。

2.3 string类对象的访问及遍历操作

  • 函数名称 ------- 功能说明
  • operator[] ------- 返回pos位置的字符,const string类对象调用
  • begin+ end ------- begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
  • 范围for ------- C++11支持更简洁的范围for的新遍历方式

在这里插入图片描述

2.4 string类对象的修改操作

  • 函数名称 ------- 功能说明
  • push_back ------- 在字符串后尾插字符c
  • append ------- 在字符串后追加一个字符串
  • operator+= ------- 在字符串后追加字符串str
  • c_str ------- 返回C格式字符串
  • find + npos ------- 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
  • rfind ------- 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
  • substr ------- 在str中从pos位置开始,截取n个字符,然后将其返回
    在这里插入图片描述

注意:
1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

2.5 string类非成员函数

  • 函数名称 ------- 功能说明
  • operator+ ------- 尽量少用,因为传值返回,导致深拷贝效率低
  • operator>> ------- 输入运算符重载
  • operator<< ------- 输出运算符重载
  • getline ------- 获取一行字符串
  • <、>、=、>=、<=、== ------- 大小比较

三、string类常用接口的模拟实现

3.1 初建构造

  • 我们运用一个字符数组来存string,私有部分包括字符大小size,和空间容量capacity,_str字符数组
  • 构造函数的形参运用缺省参数,当不传参时,将会变为空串,运用strlen得到传入的字符串大小,再new一个容量较大的字符数组,让_str指向这个数组,最后用memcpy,将传入的字符串内容拷贝给_str
  • 拷贝构造思路与构造函数思路相同
  • 析构函数将_str释放,再将size与capacity置为0
  • 再写一个c_str接口,方便打印查看,与c语言适配
class string
{
public:
	const static size_t npos = -1;
	//设置一个npos后面要用,类型为size_t所以这里的npos是一个非常大的整数
	
	//构造函数
	string(const char* str = "")
	{
		_size = strlen(str);
		_capacity = _size;
		_str = new char[_capacity + 1];
		memcpy(_str, str, _size + 1);
	}
	//拷贝构造
	string(const string& s)
	{
		_str = new char[s._capacity + 1];
		memcpy(_str, s._str, s._size + 1);
		_size = s._size;
		_capacity = s._capacity;
	}
	//析构函数
	~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
	//c_str
	const char* c_str()const
	{
		return _str;
	}
private:
	size_t _size;
	size_t _capacity;
	char* _str;
};

3.2 赋值重载

先写一个自定义的交换string的函数,将传入的形参与this的数据交换,最后返回*this

//赋值重载
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{
	swap(tmp);
	return *this;
}

3.3 返回容量大小与【】

//size,capacity
size_t size()const
{
	return _size;
}
size_t capacity()const
{
	return _capacity;
}
//[]模拟实现
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

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

3.4 reserve,resize

  • reserve:如果需要的空间n大于原空间,则要进行扩容,创建一个临时变量tmp指向new的新空间,运用memcpy将原数据拷贝给tmp,将原来_str所在空间释放,再将_str只向tmp所指向的空间,size不变,capacity变为扩容后的n
  • resize:如果是缩小,n小于原来的size,直接在下标为n的位置置为\0。如果是扩大,先用reserve扩容,再将ch放入字符数组
//reserve模拟实现
void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		memcpy(tmp, _str,_size + 1);
		delete[]_str;
		_str = tmp;
		_capacity = n;
	}
}
//resize模拟实现
void resize(size_t n, char ch = '\0')
{
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		reserve(n);

		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;

		}
		_size = n;
		_str[_size] = '\0';
	}
}

3.5 插入(push_back,append,operator+=,insert)

  • 插入的主要思想,就是空间不够就扩容,然后将字符放入数组,写出push_back和append,就可以复用写出+=,insert
//push_back模拟实现
void push_back(char ch)
{
	if (_size == _capacity)
	{
		//2倍扩容
		reserve(_capacity == 0 ? 4 : _capacity * 2);

	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

//append模拟实现
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	memcpy(_str + _size, str, len + 1);

    _size += len;
}

//+=模拟实现
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

//insert模拟实现
void insert(size_t pos, size_t n, char ch)
{
	assert(pos <= _size);
    if(_size + n > _capacity)
	{
		reserve(_size + n);
	}
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		//当end==-1时,因为类型为size_t 所以会变为整数的最大值
		//也就是npos,所以多加一个条件控制whlie
		_str[end + n] = _str[end];
		--end;
	}
	for (int i = 0; i < n; i++)
	{
		_str[i + pos] = ch;
	}
	_size += n;
}
void insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (len + _size > _capacity)
	{
		reserve(len + _size);
	}
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		_str[end + len] = _str[end];
		--end;
	}
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += len;
}

3.6 删除(erase),清空(clear)

  • erase:实现的从某个位置开始删除,删除长度为 len 的字符。len有一个缺省参数,为npos(npos是一个很大的数,也就是不传参给 len 的话,默认删除到最后)。如果 len 本就很大,删除的长度超过从pos开始所剩余的长度,那么默认也是pos后的删除完。
  • claer:与erase不同,它是将整个字符串全部清空
//erase模拟实现
void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);

	if (len == npos || len + pos >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		size_t end = len + pos;
		while (end <= _size)
		{
			_str[pos++] = _str[end++];
		}
		_size -= len;
	}
}
//clear模拟实现
void clear()
{
	_str[0] = '\0';
	_size = 0;
}

3.7 查找(find)

从pos位置开始查找,查找的内容可能是一个字符,也可能是一个子串。如果找到,则返回其下标。没找到就返回npos。

//find模拟实现
size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size);

	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
		
	}
	return npos;
}
size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size);

	const char* ptr = strstr(_str + pos, str);
	if (ptr)
	{
		return ptr - _str;

	}
	else
	{
		return npos;
	}

}

3.8 返回子串(substr)

从某个位置开始查找,查找长度为 len 的字符。len有一个缺省参数,为npos(npos是一个很大的数,也就是不传参给 len 的话,默认返回到最后)。如果 len 本就很大,返回子串的长度超过从pos开始所剩余的长度,那么默认也是pos后的子串全部返回。

//substr模拟实现
string substr(size_t pos = 0, size_t len = npos)
{
	assert(pos < _size);

	size_t n = len;
	if (len == npos || len + pos >=_size)
	{
		n = _size - pos;
	}

	string tmp;
	tmp.reserve(n);
	for (size_t i = pos; i < pos + n; i++)
	{
		tmp += _str[i];
	}
	return tmp;
}

3.9 比较符号(<,>,==,<=,>=,!=)

字符串的比较并非比较其长度,而是与其相同位置字符的大小有关,也就是我们所说的字典序。我们这里只需要实现其中的两个,其他均可复用。

bool operator<(const string& s)const
{
	//先用memcmp比较两个字符串较短的部分(前半段长度相等的部分)
	//如果相等,ret为0,
	//则size小的字符串小
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
	// "hello" "hello"   false
	// "helloxx" "hello" false
	// "hello" "helloxx" true
	return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s)const
{
	return _size == s._size
		&& memcmp(_str, s._str, _size) == 0;
}
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 !(*this < s);
}
bool operator!=(const string& s)const
{
	return !(*this == s);
}

3.10 迭代器(iterator)

这里的begin()就是返回的字符串的首元素地址,end()返回的是字符串最后一个元素的后一个地址!

public:
	typedef char* iterator;
	typedef const char* const_iterator;
//迭代器模拟实现
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator begin() const
{
	return _str;
}

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

3.11 流插入(<<),流提取(>>)

//流插入,流提取
ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}
istream& operator>>(istream& in, string& s)
{
	//这里我们采用一个一个字符的输入
	
	//覆盖之前输入数据
	s.clear();

	char ch = in.get();
	//处理前缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}

	//每次输入的字符大小不确定,如果提前用reserve开空间可能导致浪费
	//因此先用一个大小为128字符数组存输入的字符,当数组满时再放入string
	char buff[128];
	int i = 0;

	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;
}

四、总结

string是C++中比较重要的且是入门时必须所学的容器。在平常中使用的频率较高,所以我们不仅要掌握其简单的用法,更应该去了解其底层的实现。这有助于我们后续的使用和理解。string常用的语法和接口的底层实现,这些都是我们应该熟练掌握的内容。

关于string类的讲解就到这里,希望对你有所帮助,感谢观看ovo!