为什么学习string类?

C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问

标准库中的string类

string类(了解)

https://cplusplus.com/reference/string/string/?kw=string

在使用string类时,必须包含#include头文件以及using namespace std;

string类的常用接口说明

string类对象的常见构造

函数名称 功能说明
string()(重要) 构造空的string类对象,即空字符串
string(const char* s)(重要) 用c-string来构造string类对象
string(size_t n, char c) string类对象中包含n个字符c
string(const string&s)(重要) 拷贝构造函数

范例:

void String()
{
	string str;    //构造空的对象
	string str2("Hello World"); //用c格式的字符串构造string类对象
	string str3(str2); //拷贝构造
	string str4(str2, 1, 6);  
	
	cout << str << endl;
	cout << str2 << endl;
	cout << str3 << endl;
	cout << str4 << endl;
}

int main()
{
	String();
	return 0;
}

string类对象的容量操作

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

size()和length()的底层逻辑几乎一样,引入size()的原因是要和其他的接口保持一致,正常情况下都是用size()

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

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

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

补充:

iterator和const_iterator

这俩都是迭代器,一个可以修改指向内容的数据一个无法对指向的内容进行修改,通常搭配begin和end使用

代码示例:

void String()
{
	string s1("123456");
	const string s2("Hello World");

	string::iterator str = s1.begin();
	while (str != s1.end())
	{
		(*str)--;

		++str;
	}
	cout << s1 << endl;

	string::const_iterator str2 = s2.begin();
	while (str2 != s2.end())
	{
		//(*str2)--; //不能修改

		++str2;
	}
	cout << s2 << endl;
}

如果对const_iterator修改则会报错

运行结果:

除了上述两个外还有reverse_iterator和const_reverse_iterator,这俩和iterator和const_iterator相似,不过这俩是倒过来读取的

代码范例:

void String()
{
	string s1("123456");
	const string s2("Hello World");

	string::reverse_iterator str3 = s1.rbegin();
	while (str3 != s1.rend())
	{
		cout << *str3 << " ";
		++str3;
	}
	cout << endl;

	string::const_reverse_iterator str4 = s2.rbegin();
	while (str4 != s2.rend())
	{
		cout << *str4 << " ";
		++str4;
	}
	cout << endl;
}

运行结果:

string类对象的修改操作

函数名称 功能说明
push_back 在字符串后尾插字符c
appned 在字符串后追加一个字符串
operator+=(重要) 在字符串后追加字符串str
c_str(重要) 返回C格式字符串
find+npos 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr 在str中从pos位置开始,截取n个字符,然后将其返回

注意:

在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串

对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好

auto和范围for

auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

在C++中可以使用auto来进行类型推导

void String2()
{
	auto x = 20;
	auto z = 23.4;
	cout << x << endl;
	cout << z << endl;
}

int main()
{
	String2();
	return 0;
}

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加

当auto要声明引用类型时要加上引用操作符(“&”)

void String2()
{
	int& y = x;
	auto& i = y;
	i++;

}

我们来进行调试:

此时x为20,我们接着往下走

y也变成了20,继续往下

i也变成了20,接着往下走

再往下走后就变成了21

那么如果auto后不加引用会发生什么事?

此时的i是y的拷贝,并非引用

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量

auto不能作为函数的参数,可以做返回值,但是建议谨慎使用

auto不能直接用来声明数组

范围for

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束

for (auto ch : s1)
{
	cout << ch << " ";
}
cout << endl;

范围for可以作用到数组和容器对象上进行遍历

int array[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
	cout << array[i] << endl;
}
for (auto a : array)
{
	cout << a << " ";
}
cout << endl;

范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到

string模拟实现

初始化

这里我们写上const的和非const版本

namespace MyString
{
	String::String()
		:_str(new char[1]{'\0'})
		,_size(0)
		,_capacity(0)
	{}

	String::String(const char* str)
		:_str(new char[strlen(str)+1])
		,_size(strlen(str))
		,_capacity(strlen(str))
	{
		memcpy(_str, str);
	}

}

取得字符串

namespace MyString
{
	const char* String::c_str() const
	{
		return _str;
	}
}

我们来运行看看

运行结果:

取得字符串中的长度

namespace MyString
{
	size_t String:: size()
	{
		return _size;
	}
}

这里我们要将字符串中的每个字符都往后+1

运行结果:

下标操作符重载

这里也写上const和非const的版本,这里需要使用迭代器

namespace MyString
{
	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];
	}

}

这里补充一些东西,这里遍历时无法使用迭代器,因为自定义类型并没有调用库中的东西

所以这我们begin和end要自己实现

这要先将char*进行重命名

typedef char* iterator;

我们知道begin是要找到字符串中的第一个字符,end则是找到最后一个字符

namespace MyString
{
	String::iterator String::begin()
	{
		return _str;
	}

	String::iterator String::end()
	{
		return _str + _size;
	}
}

运行结果:

尾插

在写尾插之前我们先模拟实现一个函数“reserve”,“resever”是用于分配内存空间的一个函数,它可以避免内存多次进行分配,有效提升元素插入时的运行效率

现在开始写代码:

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

接下来继续写尾插代码

void String::push_back(char str)
{
	if (_size >= _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}
	_str[_size] = str;
	++str;
	_str[_size] = '\0';
}

这里写的和之前的不同,这里多加了一个‘\0’

运行结果:

追加字符串

append是用于追加字符串的函数,可以追加任何形式的字符而不要进行重新分配,接下来进行模拟实现

void String::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
		reserve(newcapacity);
	}
	//strcpy(_str + _size, str);
	memcpy(_str, str, len + 1);    //这里可以使用strcpy,但是使用memcpy会更好
	_size += len;
}

运行结果:

重载函数”+=“:

这里写const和非const版本

String& String::operator+=(char ch)
{
	push_back(ch);
	return *this;
}

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

这里和尾插和追加字符串相似,当我们想要追加字符和字符串可以使用这个符号来代替

运行结果:

流输出符号重载

这个需要在类外面进行实现,需要使用友元才能使用私有成员,不过这并不需要访问类的私有成员

ostream& operator<<(ostream& out, const string& s)
{
	//out << s.c_str();    //如果使用这个会有”坑“
	for (size_t i = 0; i < s.size();i++)
	{
		out << s[i];
	}
	return out;
}

我们来进行调试:

首先走到断点处

往下走,此时s1的值变为“hello world”

我们看到终端画面

这样就是输出成功,调试结束

指定位置插入字符

string& string::insert(size_t pos, char ch)
{
	assert(pos <= _size);

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

	/*int end = _size;
	while(end>= (int) pos)  //size_t是无符号变量,end会从int变成unsigned int。这称为整型提升,因此这里要强转
	{
		_str[end + 1] = _str[end];
		--end;
	}*/

	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 >= _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(newcapacity);
	}

	/*int end = _size;
	while(end >= (int)pos)
	{
		_str[end + len] = _str[end];
		--end;
	}*/

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

运行结果:

删除指定字符

在写这个代码前需要写npos的定义

const size_t string::npos = -1;//定义

继续写erase的代码

string& string::erase(size_t pos, size_t len)
{
	assert(pos < _size);
	//要删除的数据,要大于pos后面的字符个数
	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;
}

运行结果:

寻找指定字符

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* p1 = strstr(_str + pos, str);
	if (p1 == nullptr)
	{
		return npos;
	}
	else
	{
		return p1 - _str;
	}
}

这里还需要写上substr

substr是用于提取字符串中提取子字符串的函数

代码:

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

这里find和substr要搭配使用

代码:

void spliturl(const string& url)
{
	size_t i1 = url.find(':');
	if (i1 != string::npos)
	{
		cout << url.substr(0, i1) << endl;
	}

	size_t i2 = i1 + 3;
	size_t i3 = url.find('/', i2);
	if (i3 != string::npos)
	{
		cout << url.substr(i2, i3 - i2) << endl;
		cout << url.substr(i3 + 1) << endl;
	}
	cout << endl;
}

运行结果:

大于、大于等于、小于、小于等于、等于和不等于符号重载

bool string::operator<(const string& s)const //大于
{
	size_t s1 = 0, s2 = 0;
	while (s1 < _size && s2 < s._size)
	{
		if (_str[s1] < s[s2])
		{
			return true;
		}
		else if(_str[s1] > s[s2])
		{
			return false;
		}
		else
		{
			s1++;
			s2++;
		}
	}
	return s2 < s._size;
}

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//等于
{
	size_t s1 = 0, s2 = 0;
	while (s1 < _size && s2 < s._size)
	{
		if (_str[s1] != s[s2])
		{
			return false;
		}
		else
		{
			s1++;
			s2++;
		}
	}
	return s1 == _size && s2 ==s._size;
}

bool string::operator!=(const string& s)const//不等于
{
	return !(*this == s);
}

这里我们以大于来做测试,这里如果是大于返回0否则返回1,最后的输出结果为:0、0、1

运行结果:

流输入符号重载

在写这个之前我们需要写一个clear的模拟实现,如果没写会出现bug

void string::clear()
{
	_str[0] = '\0';
	_size = 0;
}

接下来继续写写上流输入符号重载的代码

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 += i;
			i = 0;
		}
		s += ch;
		ch = in.get();	
	}

	if (i < 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}

我们来进行调试:

先走到断点处

我们往下走并输入“Hello”和“world”

之后看到监视窗口,此时s1和s2分别为“hello”和“world”

调试结束

从流中提取字符串

istream& getline(istream& is, string& str, char delim)
{
	str.clear();
	char ch = is.get();
	while (ch != delim)
	{
		str += ch;
		ch = is.get();
	}

	return is;
}

运行结果:

拷贝构造函数

拷贝构造函数分为传统写法和现代写法,这里先写上传统写法

传统写法:

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

现代写法:

在写现代写法前要先写swap代码

void string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

接下来写上现代写法的拷贝构造函数

string::string(const string& s)
{
	string tmp(s._str);
	swap(tmp);
}

赋值符号重载

赋值符号重载也分为两种写法,一个是传统写法一个是现代写法,先来写传统写法

传统写法:

string& string::operator=(const string& s)//赋值操作符重载(传统)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, _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& tmp) //赋值操作符重载(现代)
{	
	swap(tmp);
	return *this;
}

更多推荐