C++之string类

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

string是一个对象,使用字符的顺序表实现的,就是一个字符顺序表。

基本构造:

class string
{
private:
	size_t size;
	size_t capacity;
	char* str;//指向一个数组动态开辟的
};

2.string类的接口使用:使用前记得写上#include

*可以直接实例化不给数据,也可以给字符串去构造string,也可以拷贝构造:

void test1()
{
	string s1;
	string s2("hello world");
	string s3(s2);//拷贝构造也可以

	//cin >> s1;
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
}

* string(const string & str, size_t pos, size_t len = npos);nops是缺省值,该接口是为了拷贝string的一部分,nops是string的const静态的成员变量,值为-1,但其实是把-1给了一个无符号的len瞬间就变成整型的最大值了,42亿九千万字节。

 *通过string(size_t len,string & str)构造对象初始化为len个对应的字符。

*通过s[n]可以访问到字符串对应下标的字符,类似数组,非常方便。

void test1()
{
	
	string s2("hello world");

	//string(const string & str, size_t pos, size_t len = npos);nops是缺省值
	//拷贝string的一部分
	//nops是string的const静态的成员变量,值为-1,但其实是把-1给了一个无符号的len瞬间就变成整型的最大值了,42亿九千万字节
	string s4(s2, 6, 5);//第六个位置开始拷贝五个字符
	cout << s4 << endl;

	string s5(s2, 6);//取到字符串结尾,不包含'\0'
	cout << s5 << endl;

	string s6("hello world", 5);//取前五个字符
	cout << s6 << endl;

	string s7(10, 'x');//十个x初始化
	cout << s7 << endl;

	s6[0] = 'x';//可以像数组一样修改
	cout << s6 << endl;

	return 0;
}

*利用迭代器和auto关键字遍历字符串:

以前遍历字符串的方式:

void test2()
{
	string s1("hello world");
	cout << s1 << endl;

	//遍历字符串s1:
	for (size_t i = 0; i < s1.size(); i++)//size()可以获取字符长度
	{
		cout << s1[i] << " ";
	}
	cout << endl;
}

下面是利用迭代器和auto的方式遍历字符串,感受一下三者的区别:

void test2()
{

	string s1("hello world");
	cout << s1 << endl;

    string s2(s1);
    s2[0] = 'x';

	//迭代器遍历:
	//迭代器规定,不管底层什么定义的,首先属于对应容器的类域
	string::iterator it = s2.begin();//用迭代器定义一个对象,有点像指针但不一定是指针 
	while (it != s2.end())//begin是返回空间开始位置的迭代器,所以it指向了开始
	{
		//end是\0位置的迭代器
		cout << *it << "";//运算符重载*
		//也可以*it+=2,使里面的asci码值改变换字母
		++it;//直到it到end位置结束
	}
	cout << endl;
}
void test2()
{

	string s1("hello world");
	cout << s1 << endl;
    string s2(s1);
    s2[0] = 'x';
	for (auto ch : s2)//自动从s2里面取每一个值给这个ch变量
	{  //auto表示自动推导成char,自动赋值、自动迭代、自动判断结束
		ch -= 2;//修改字符对应的asci码值换字母了,但是s2没变,因为ch是局部变量
		//如果想要修改的话for里面auto加引用
		cout << ch << " ";
	}
	//底层是迭代器
	cout << endl;
}

 auto ch : s2 表示创建一个叫ch的变量,然后从s2里面一个一个的把字符传给ch去读取。底层依旧是迭代器。

附赠一个迭代器遍历链表:

void test()
{
    //链表迭代器遍历
    list lt = { 1,2,3,4,5,6,7 };
    list ::iterator lit = lt.begin();
    while (lit != lt.end())
    {
	   cout << *lit << " ";
	   ++lit;
    }
    cout << endl;
}

*auto的意义:

//auto用法和意义:
int func()
{
	//...
	return 1;
}
void test3()
{
	int a = 10;
	auto b = a;//自动推导b的类型为int
	auto c = 'a';//自动推导c的类型为char
	auto d = func();//自动推导返回值,但是像这些都没什么意义,但如果是一个函数里面有多层其他函数的迭代,那么就可以瞬间知道函数的返回值类型

	//auto e;//编译报错,e必须要有初始值

	cout << typeid(d).name() << endl;//打印类型

	//真正的意义是简化代码:
	/*mapdict;
	map::iterator mit = dict.begin();可以替换为:
	auto mit= dict.begin();*/

	//以前遍历数组的方式:
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		arr[i] *= 2;
	}
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << endl;
	}
	//利用auto遍历:
	for (auto& e : arr)
		e *= 2;
	for (auto e : arr)
		cout << e << " " << endl;

	//一些小点:
	//auto a = 1, b = 1;//可以
	//auto c = 2, d = 2.2;//不可以

	//void func(auto a);//不能做参数但可以做返回值,谨慎使用

	//逆向迭代器遍历容器:
	string s1("hello world");
	string::reverse_iterator rit = s1.rbegin();//末尾
	while (rit != s1.rend())//其实这是头
	{
		cout << *rit << " " << endl;
		rit++;//这里的++是重载的,倒着++
	}
	cout << endl;

	const string s3("hello world");
	string::const_iterator cit = s3.begin();//const只能读不能写
	//==auto cit = s3.begin();
	while (cit != s3.end())
	{
		cout << *cit << endl;
		cit++;
	}
	string::const_reverse_iterator rcit = s3.rbegin();//反向遍历
	//==auto rcit = s3.rbegin();
	while (rcit != s3.rend())
	{
		cout << *rcit << endl;
		rcit++;
	}

}

 *注意,auto不能作为函数的参数,可以作为返回值,但谨慎使用。auto不能用来直接声明数组。

*其他接口:

void test4()
{
	string s1("hello world");
	s1.max_size();//获取最大长度,了解
	s1.length();//获取长度
	s1.capacity();//获取容量

	int n = 100;
	s1.reserve(n);//提前开好n个字节的空间,不包含\0。
	//大于原来空间就扩容,小的话不一定会缩小,但肯定不会对内容造成影响
	s1.clear();//清理数据,但可能会清掉容量

}
void test5()
{
	string s("hello world");
	s.push_back(' ');
	s.push_back('x');
	s.append("xxxxxx");//在后面补上字符串
}

void test6()
{
	string s("hello world");
	s.erase(6, 1);//在第六个位置删除一个字符
	cout << s << endl;

	s.erase(s.begin());//迭代器头删
	cout << s << endl;

	s.erase(s.end());
	cout << s << endl;

	string ss("hello world");
	s.replace(5, 1, "%%");//第五个位置替换为一个字符

	string sss("hello world en");
	size_t pos = sss.find(' ');//查找某个位置的字符串,返回第一个匹配的字符的下标位置
    while (pos != string::npos)
	{
		sss.replace(pos, 1, "%%");//替换
		pos = sss.find(' ');//替换后继续找
		//但是这个是每次都要从头开始找
		//所以可以写成:
		//pos = sss.find(" ", pos + 2);
		//但是效率还是很低,一个空格替换一次
	}
	cout << sss << endl;

	//解决一个空格替换一次的低效:
	string tmp;
	for (auto ch : sss)
	{
		if (ch == ' ')
			tmp += "%%";
		else
			tmp += ch;
	}
	cout << tmp << endl;

}

void test7()
{
	//调用文件:
	string file;
	cin >> file;
	file* fout = fopen(file.c_str(), "r");
	char ch = fgetc(fout);
	while (ch != eof)
	{
		cout << ch << endl;
		ch = fgetc(fout);
	}
	fclose(fout);
}

三个练习:

仅仅、方向迭代,输入abcd输出dcba

class solution1
{
	//判断是否是字符:
	bool isleter(char ch)
	{
		if (ch >= 'a' && ch <= 'z')
		{
			return true;
		}
		if (ch >= 'a' && ch <= 'z')
		{
			return true;
		}
		else return false;
	}
	string resveronlyletters(string s)
	{
		int left = 0, right = s.size() - 1;
		while (left < right)
		{
			while (left < right && !isleter(s[left]))
			{
				++left;
			}
			while (left < right && !isleter(s[right]))
			{
				--right;
			}
			swap(s[left++], s[right--]);
		}
		return s;
	}
};

字符串中第一个唯一字符

class solution2
{
	int firstunichar(string s)
	{
		int count[26] = { 0 };
		//利用映射统计各个字母出现次数
		for (auto ch : s)
		{
			count[ch - 'a']++;
		}
		//循环判断是否只有出现一次的字母
		for (size_t i = 0; i < s.size(); i++)
		{
			if (count[s[i]-'a'] == 1)
			{
				return i;
			}
		}
		return -1;
	}
};

 字符串相加

class solution3
{
	string addstrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1;
		int end2 = num2.size() - 1;
		//进位:
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = 0 ? num1[end1--] - '0' : 0;
			int val2 = 0 ? num1[end2--] - '0' : 0;

			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;

			str.insert(str.begin(), '0'+ret);
		}

		if (next == 1)
			str.insert(str.begin(), '1');

		return str;
	}
};

*find接口:

void test8()//find系列
{
	//size_t pos = s.find(' ');//查找某个位置的字符串,返回第一个匹配的字符的下标位置
	string s("test.cpp");
	size_t pos = s.find('.');//获取文件的后缀
	string suffix = s.substr(pos);//从pos位置开始的len个字符单独拿出来变成一个新string
	//没给第二个值就是有多长去多长
	cout << suffix << endl;

	//假如文件名变为:
	string ss("test.cpp.zip");//这时候要取后缀为.zip,就可以倒着找
	size_t poss = ss.rfind('.');
	string suffixx = ss.substr(poss);

	string sss("ainizhoujielun");//将里面的aijn替换为*
	size_t posss = sss.find_first_of("aijn");//找到一个替换为*
	while (posss != sss.npos)//小于这个字符串的最长值
	{
		sss[posss++] = '*';
		posss = sss.find_first_of("aijn", posss + 1);
	}
	cout << sss << endl;

	string str1("/usr/bin/man");//路径还要同时兼容windows和linux
	string str2("c:\\windows\\winhelp.exe");//linux系统下的份分割是\
	//就可以用find_last_of 倒着找,如果是l的\或者w的/都进行返回
	void spilitfilename(const std::string & str); 
	spilitfilename(str1);
	spilitfilename(str2);

	//find_not_last_of不是的返回,将非目标替换
}
void spilitfilename(const std::string& str)
{
	std::cout << "spilitfilename" << str << '\n';
	std::size_t found = str.find_last_of("/\\");//倒着找\,返回的数字,之前的分割
	std::cout << "path" << str.substr(0, found) << '\n';
	std::cout << "file" << str.substr(found + 1) << '\n';
}

void test9()
{
	string s1("hello");
	string s2 = s1 + " world";
	string s3 = "world " + s1;//但是重载的+得是全局的,因为左操作数被string牢牢占据
}

练习:查找字符串最后一个单词,例如输入hello world输出5

//查找字符串最后一个单词:
//例如输入hello world输出5
int main()
{
	string str;
	getline(cin, str);//默认遇到换行才会停止,也可以自定义,比如遇到星号才停止getline(cin, str,'*')

	size_t pos = str.rfind(' ');
	cout << str.size() - (pos + 1) << endl;

	return 0;
}

3.string类的模拟实现:

.h文件:

#pragma once
#define _CRT_SECURE_NO_WARNINGS

//模拟实现string
#include 
#include 
#include 
using namespace std;

namespace byd
{
	class string
	{
	public:
		//构造:
		string()
			:_str(new char[1] {'\0'})
			,_size(0)
			,_capacity(0)
			//初始化列表先声明的先走
		{}

		//构造:
		string(const char* str='\0')//常量字符串后面会给\0
		{
			_size = strlen(str);
			//_capacity没有包含\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);//先拷贝再判断,遇到\0截止
		}


		//自己写拷贝构造:
		//但是这个是传统写法,就是自己开空间自己赋值拷贝
		string(const string& s)//s2(s1)左边是this
		{
			_str = new char[s._capacity + 1];//开和str一样大的空间
			//拷贝:
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}//不深拷贝的问题就是拷贝之后指针是一样的,指向了同一块空间,会被析构两次,一个被修改另一个也会被修改
		//所以有了新的写法:
		//s2(s1);
		void 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(_str, tmp._str);
			swap(_size, tmp._size);
			swap(_capacity, tmp._capacity);*/
			//==:封装成上面这个swap函数
			swap(tmp);
		}//假设有个s1叫hello,s2指向空,tmp被初始化为hello,然后交换之后s2变成了hello

		//同理,重载=也可以这样写:
		//s1 = s2;
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s._str);
				swap(tmp);//交换完之后tmp里面的东西是s2的,出了作用域就都被销毁了
			}
			return *this;
		}
		//重载:
		//string& operator=(const 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;
		//	}
		//}
		//也可以这样:
		string& operator=(string tmp)//传值调用拷贝构造,tmp是一个新的东西,自己和s3拷贝构造了
		{
			swap(tmp);
			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		const char* c_str()
		{
			return _str;
		}

		//一些基本函数接口:
		size_t size() const//返回长度
		{
			return _size;
		}
		char& operator[](size_t pos)//返回对应下标字符
		{
			assert(pos < _size);

			return _str[pos];
		}
		const char& operator[](size_t pos) const//返回const不能修改
		{
			assert(pos < _size);

			return _str[pos];
		}
		//迭代器伪实现:
		typedef char* iterator;//屏蔽了底层的实现细节,提供了统一的类似访问容器的方式,不需要关心容器底层结构和细节
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		size_t capacity()
		{
			return _capacity;
		}

		void reserve(size_t n);//声明和定义分离
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char str);
		string& operator+=(const char str);
		void insert(size_t pos, char ch);
		void inserts(size_t pos, char* str);
		void erase(size_t pos, size_t len);
		string str(size_t pos, size_t len);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos, size_t len);
		string operator+=(const char* str);
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		static const size_t npos = -1;//静态不能在类里给缺省值,因为不走初始化列表,要走声明和定义分离,但是这个地方可以,因为有了static const。
	};
	void test_string1();
	void test_string2();
	bool operator<(const string& s1, const string& s2);
	ostream& operator<<(ostream& out, const string& s);
}

.cpp文件:

#include "String.h"
#include 
using namespace std;

namespace byd
{
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n+1];//创建新空间
			strcpy(tmp, _str);//拷贝原来空间到新空间
			delete[] _str;
			_str = tmp;//改变原来_str的地址
			_capacity = n;
		}
	}
	void string::push_back(char ch)
	{
		//先扩容:
		if (_size = _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';//输出程序碰到\0结束,必须要写,不然会出现乱码。
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len > _capacity * 2 ?  _size + len : _capacity * 2);//如果大于2倍,需要多少开多少
		}
		strcpy(_str + _size, str);
		_str += len;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	void string::insert(size_t pos, char ch)//插入一个字符
	{
		assert(pos < _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//挪动数据:
		size_t end = _size + 1;
		//while (end >= pos)//大坑,当一个操作符两边操作数不一样时两边会发生转换,pos会变成无符号去和它比较。
		while(end>=(int)pos)//强转解决
		{
			_str[end + 1] = _str[end];
			end--;
		}//这样子挪动数据会把\0移除
		_str[pos] = ch;
		_size++;
	}
	//用指针的方式改写insert:
	void string::inserts(size_t pos, char* str)//插入字符串
	{
		assert(pos < _size);
		size_t len = strlen(str);
		if (len == 0)
		{
			return;
		}
		if (_size == _capacity)
		{
			reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);
		}
		size_t end = _size + len;
		//挪动数据:
		while (end > pos + len - 1)
		{
			_str[end - len] = _str[end];
			end--;
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = str[i];
		}
	}
	void string::erase(size_t pos, size_t len)//len是字符长度
	{
		if (len >= _size - pos)
		{
			_str[pos] = '0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i < _size; i++)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}
	size_t string::find(const char* str, size_t pos = 0)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else return ptr - _str;
	}
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		//len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}
		string sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		return sub;
	}

	bool operator<(const string & s1, const string & s2)
	{
		return strcmp(s1.c_str, s2.c_str);
	}
	string string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		const int N = 1024;
		char buff[N];
		int i = 0;
		char ch;
		//char ch;
		//in >> ch;//默认提取不到空格和换行
		//ch = in.get();
		//while (ch != ' ' && ch != '\n')
		//{
		//	s += ch;

		//	ch = in.get();//但是这种是一次一次来然后扩容的,需改进

		//}
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;

				i = 0;
			}

			ch = in.get();
		}
		return in;
	}
	void test_string1()
	{
		//string s1;
		string s2("hello world");
		//cout << s1.c_str<< endl;//当初始化列表中:_str(nullptr)这样子程序会崩溃,因为s1啥都没有只有空指针,空指针去解引用没遇到\0不会停下
		//cout << s1.c_str << endl;
		byd::string::iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	for (auto e : s2)
	{
		cout << e << " ";
	}
	cout << endl;
	}

	void test_string2()
	{
		string s1("hello world");
		s1 += 'x';
		s1 += '#';
	}
}

*单独一提:深拷贝的现代实现方式:

void 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(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);*/
	//==:封装成上面这个swap函数
	swap(tmp);
}//假设有个s1叫hello,s2指向空,tmp被初始化为hello,然后交换之后s2变成了hello

有字符串s1和s2,创建tmp然后用s1拷贝构造tmp,然后让tmp和s2里面东西去交换,这样就可以简单方便的实现深拷贝并且tmp出了作用域之后会调用析构函数自动销毁,把原来那部分需要自己做的事情全部交给了编译器去完成实现。相比于原来的:

string(const string& s)//s2(s1)左边是this
{
	_str = new char[s._capacity + 1];//开和str一样大的空间
	//拷贝:
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

同理,重载=函数就可以写成:

string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s._str);
		swap(tmp);//交换完之后tmp里面的东西是s2的,出了作用域就都被销毁了
	}
	return *this;
}

如果做一下优化:

string& operator=(string tmp)//传值调用拷贝构造,tmp是一个新的东西,自己和s3拷贝构造了
{
	swap(tmp);
	return *this;
}

巧妙地利用了传值调用拷贝构造让编译器自动实现了拷贝构造,大大简化了代码,相比于原来的重载operator=:

string& operator=(const 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;
	}
}

当然底层的运行并没有简化,只是看上去代码更简洁了而已。

你可能感兴趣的:(c++)