STL——string

目录

1.C++为什么要封装string

2.string类

3.默认成员函数         

  构造函数

  operator=

4.遍历

  Way1:下标访问

  Way2:迭代器iterator(主流) 

5.容量相关的函数

1.capacity

2.reserve 

3.resize

4.元素访问

5. 增删查改

 6.和字符串相关的函数 ​

        1.c_str

        2.substr     

        3.find_first_of

7. 非成员函数

8.模拟是为了更好的理解

        1.构造函数

        2.迭代器简单模拟

        3. namespace个人版string

9.string的大小


1.C++为什么要封装string

        c语言中,字符串是以‘\0’结尾的字符的集合,为了方便操作,C标准库中提供了一系列str函数,但是这些库函数与字符串是分离开的,不符合OOP思想,并且底层空间由用户自己管理,容易出现各种内存问题。

2.string类

借助网站学习

https://legacy.cplusplus.com/

         string是字符序列类,可以视为一种数据结构/容器——串(C++把数据结构称为STL里面的容器(containers))。

这个网站把string归入了Other,左键查看详细内容 

        根据这个文档的介绍,string是一个类,是一个对类模板实例化的typedef 

3.默认成员函数

         

        构造函数

        构造函数重载了多种形式

// 默认成员函数
int main()
{
	string s1;//调用1
	string s2("hello world");//调用4
	string s3 = s2;//调用2
    string s4(s2);//调用2
}

这是第三个构造函数的介绍

//用法
	string s5(s2, 1, 6);
	cout << s5 << endl;

        operator=

 有不同的三种赋值

    s1 = s2;
	cout << s1 << endl;

	s1 = "world";
	cout << s1 << endl;

	s1 = 'x';
	cout << s1 << endl;

4.遍历

        Way1:下标访问

        string类把运算符[]重载了两个,用const修饰的可以被const 对象调用。

        也有size和length函数可以求string长度

int main()
{	
    string s1("Xiaomi Honor");

	//下标遍历
	size_t i = 0;
	for (i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << "|";
	}
}

 如果要逆置string,可以直接调用STL中的swap

	size_t begin = 0, end = s1.size() - 1;
	while (begin < end)
	{
		swap(s1[begin], s1[end]);
		++begin;
		--end;
	}

        Way2:迭代器iterator(主流) 

	//迭代器遍历
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << '-';
		it++;
	}

 迭代器对象it是类似于指针的东西,begin函数返回值是迭代器类型

如果要逆置,可以直接用STL库里面的算法reverse,参数类型是迭代器

reverse(s1.begin(), s1.end());
cout << s1 << endl;

如果string被const修饰,迭代器类型应该是const_iterator

	//迭代器遍历
	const string s2 = s1;
	string::const_iterator it2 = s2.begin();
	while (it2 != s2.end())
	{
		cout << *it2 << '-';
		it2++;
	}

5.容量相关的函数

        1.capacity

        该函数返回容量大小

        可以用这个函数来验证string的扩容机制

int main(void)
{
	string s1("Max");
	cout << "初始容量" << s1.capacity() << endl;
	size_t init_cp = s1.capacity();//初始的容量

	size_t i = 0;
	for (i = 0; i < 500; i++)
	{
		s1.push_back('x');
		if (s1.capacity() != init_cp)
		{
			cout << "发生了扩容,当前容量" << s1.capacity() << endl;
			init_cp = s1.capacity();
		}
	}
	return 0;
}

        总结,VS编译器,初始大小给15(不含'\0'),之后以1.5倍的比例扩容

         相比之下,GNU的g++以2倍速度扩容

        2.reserve 

         这里区别一下string的两个概念,数据size表示字符串大小,像函数size和length都返回的是这个值,数据capactiy表示字符串预留空间,即容量,比如函数capacity返回这个值。

        reserve就是用来改变容量的

          如果使用reserve来缩小容量,如果是空字符串

int main()
{
	string s1;
	cout << s1.capacity() << endl;
	s1.reserve(10);//这句代码表示你想把容量设置为10
	cout << s1.capacity() << endl;
	return 0;
}

        如果是普通字符串

string s2("Maxnap");
cout << s2 <<  endl;
cout << s2.capacity() <<  endl;
s2.reserve(10);
cout << s2.capacity() << endl;

        说明VS编译器对reserve缩容的处理是,在初始化开辟一定大小的空间,缩容是不会改变容量大小的。

        以上代码用g++编译后,可以得出结论:

        g++对于空字符串不开辟空间,而缩容时,会缩小容量,但不会改变字符串大小。

string s2("Maxnap");
cout << s2 <<  endl;
cout << s2.capacity() <<  endl;
s2.reserve(100);
cout << s2.capacity() << endl;

        使用reserve扩容时不一定严格扩容,可能会多开辟空间。

        3.resize

        resize用来改变string的size大小

        可以用resize来改变字符串大小,如果改变后的n大于原来的字符串长度,那么这个函数会扩大容量并且增加字符串长度

        


int main()
{
	string s1("Maxnap and Kk");
	cout << "容量" << s1.capacity() << endl;
	cout << "大小" << s1.size() << endl;
	cout << s1 << endl;

	s1.resize(90, 'x');
	cout << "容量" << s1.capacity() << endl;
	cout << "大小" << s1.size() << endl;
	cout << s1 << endl;
	return 0;
}

         


        如果n介于大小和容量之间,只增加字符串长度,但是不改变容量大小

string s1("Maxnap");
cout << "容量" << s1.capacity() << endl;
cout << "大小" << s1.size() << endl;
cout << s1 << endl;

s1.resize(10, 'x');
cout << "容量" << s1.capacity() << endl;
cout << "大小" << s1.size() << endl;
cout << s1 << endl;


        如果n要小于字符串的长度,这个函数会毫不犹豫的把字符串删减

4.元素访问

         这两个函数都可以用来访问元素,区别是如果发生越界访问,警告处理方式有差异,下标访问会断言失败终止程序,at会打印错误信息。

5. 增删查改

增:最常用的是重载的+=,还有push_back,append,insert

具体的用法可以看这个网站手册的介绍

查找和修改都可以用迭代器或者下标 

erase可以用来删除下标pos后面的n个字符

​
int main()
{
	string s1 = "Madnap";
	cout << s1 << endl;
	s1.erase(3);
	cout << s1 << endl;

	return 0;
}

​

assign类似=赋值,比较少用

 replace的功能是替换,比如要求把字符串的空格替换为 ‘-’

可以使用这个函数:

int main()
{
	string s1 = "Madnap is singer";
	//把空格替换为'-'
	size_t pos = s1.find(' ', 0);
	while (pos != string::npos)
	{
		s1.replace(pos, 1, "-");
		pos = s1.find(' ', pos + 1);
	}
	cout << s1 << endl;
	return 0;
}

        但是不推荐使用replace,因为底层需要挪动数据,消耗太大了,直接PASS

        那不用replace怎么实现替换,新开辟一个空间,遍历该字符串,把空格替换为字符,再把新字符串拷贝给旧字符串,缺点是空间复杂度高,但是时间复杂度为O(N)

	//法2
	string s2 = "Slow down is possesed by Madnap";
	string s3;

	for (auto ch : s2)
	{
		if (ch ==' ')
		{
			s3 += '-';
		}
		else
		{
			s3 += ch;
		}
	}
	s2.swap(s3);
	cout << s2 << endl;
	cout << s3 << endl;

这里用了范围for和成员函数swap,该swap不同于算法里面的swap

 6.和字符串相关的函数  

        1.c_str

        c++的字符串是string类型的,是自定义类型,有些场景需要c语言的字符类型,c_str就是把提供这个接口。

        比如文件操作,把test.cpp文件内容输出到终端

int main()
{
	string s1 = "test.cpp";
	FILE* pf = fopen(s1.c_str(), "r");
	char ch = fgetc(pf);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(pf);
	}
	return 0;
}

        2.substr     

         一般和find搭配使用,还有rfind(从尾开始找),找到返回下标,没找到返回-1

        

int main()
{
	//题目1,要求找出后缀
	string s1 = "test.cpp";//后缀为.cpp
	string s2 = "head.tar.zip";//后缀为.zip

	size_t pos1 = s1.find('.', 0);
	if (pos1 != string::npos)
	{
		string suffix = s1.substr(pos1);
		cout << suffix << endl;
	}
	size_t pos2 = s2.rfind('.', s2.size());
	if (pos2 != string::npos)
	{
		string suffix2 = s2.substr(pos2);
		cout << suffix2 << endl;
	}
	//题目2,要求分割出网址的协议,域名,路径
	string s3 = "https://fanyi.youdao.com/index.html#/";
	string agremt;//协议
	size_t posAgremt = s3.find(':', 0);
	if (posAgremt != string::npos)
	{
		agremt = s3.substr(0, posAgremt - 0);
		cout << agremt << endl;
	}
	string daname;//域名
	size_t posdaname = s3.find('/', posAgremt + 3);
	if (posdaname != string::npos)
	{
		daname = s3.substr(posAgremt + 3, posdaname - (posAgremt + 3));
		cout << daname << endl;
	}
	string path;
	path = s3.substr(posdaname + 1);
	cout << path << endl;
	return 0;

}

        3.find_first_of

        要求:把一个句子中的脏话相关的字母用‘x’替代

int main()
{
	string s1 = "what happend fuck you,oh,shit,shut up";
	const char* arr = "fuckst";

	size_t pos = s1.find_first_of(arr, 0);
	while (pos != string::npos)
	{
		s1[pos] = 'x';
		pos = s1.find_first_of(arr, pos + 1);
	}
	cout << s1 << endl;
	return 0;
}

find用来查找单个字符,或者一个字符串

find_first_of则用来查找一个字符串中的任意一个字符

7. 非成员函数

        当用cin输入字符串时,如果是一个有空格的句子,则有效输入是空格前面的字符。 


int main()
{
	string s1;
	cin >> s1;
	cout << s1 << endl;

}

全局函数getline可以解决这个问题,这个函数用换行来结束字符输入

getline(cin, s1);
cout << s1 << endl;

8.模拟是为了更好的理解

        1.构造函数

        string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);
		}

 1.这里为什么要给缺省值,并且缺省值为啥什么也没有?

        字符串是用双引号“”引起来的内容。

        缺省值不能是nullptr,因为空字符串表示没有内容,用nullptr初始化_str表示野指针,不表示指向内容为空。如果用cout来打印用nullptr初始化的string,程序会终止,原因是cout 识别类型char*后会打印内容,需要解引用,值为nullptre的_str被解引用,出现崩溃。

        缺省值也不能是'\0',单引号表示字符,不能赋值给char*。

        所以这里是“\0”或者“”,是同一个意思。

2.为什么_size先初始化?

        为了复用strlen的值,这种写法只调用了一次strlen,减少消耗。

3._capacity为什么和_size值相等?

        _capacity表示可以存储有效字符串的容量,不推荐带上'\0’计算

        2.迭代器简单模拟

        对指针typedef就可以简单的模拟迭代器,当然,真正的迭代器远不止此。

        对于C++11支持的范围for,底层就是替换为迭代器,在调试窗口查看汇编代码 :

        3. namespace个人版string

//一般是头文件写声明,源文件写定义,这里把所有内容写到了头文件
namespace ljy
{
	class string
	{
	public:
		//普通构造函数
		string(const char* str = "")//缺省值是空字符串
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);
		}
		//string类的拷贝构造是深拷贝
		string(const string& s)
		{
			string tmp = s._str;
			swap(tmp);
		}
		//赋值重载
		string& operator=(string s)
		{
			swap(s);
			return *this;
		}
		//析构
		~string()
		{
			delete[]_str;
			_size = _capacity = 0;
		}
		//库中的c_str
		const char* c_str()const
		{
			return _str;
		}
		size_t size()const
		{
			return _size;
		}
		//库中的resrve
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[]_str;

				_str = tmp;
				_capacity = n;
			}
		}
		//push_back
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				size_t NewCapacity = (_size == 0) ? 4 : _capacity * 2;
				reserve(NewCapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//append
		void append(const char* s)
		{
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			//strcpy会拷贝\0
			strcpy(_str + _size, s);
			_size += len;
		}
		//+=
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* s)
		{
			append(s);
			return *this;//运算符一般都有返回值
		}
		//在某个位置插入
		void insert(size_t pos,char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				size_t NewCapacity = (_capacity == 0) ? 4 : _capacity * 2;
				reserve(NewCapacity);
			}

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}
			_str[end] = ch;
			++_size;
		}
		void insert(size_t pos, const char* s)
		{
			assert(pos <= _size);
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			int end = (int)_size;
			while (end >=(int)pos)
			{
				_str[end + len] = _str[end];
				--end;
			}
			strncpy(_str+pos, s, len);
			_size += len;
		}
		//删除pos开始的n个字符
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}
		//同库中的[]一样,需要实现const和非const,非const由const修饰的对象调用,只读不写
		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}
		const char& operator[](size_t i)const
		{
			assert(i < _size);
			return _str[i];
		}
		//简单的迭代器
		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;
		}
		//swap
		void swap(string& s)
		{
			if (this != &s)
			{
				std::swap(_str, s._str);
				std::swap(_size, s._size);
				std::swap(_capacity, s._capacity);
			}
		}
		//从pos开始找字符ch,返回下标
		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 -1;
		}
		//从下标pos开始找字符串,返回下标
		size_t find(const char* s, size_t pos = 0)
		{
			assert(pos <= _size);
			const char* tmp = strstr(_str, s);
			if (tmp == nullptr)
			{
				return -1;
			}
			else
			{
				return tmp - _str;
			}
		}
		//substr的意义在于取字符串,参数本质是区间,找字符串的工作由程序员做
		string substr( size_t pos = 0,size_t len = npos)const
		{
			assert(pos <= _size);
			size_t end = pos + len;
			if (len == npos || pos + len >= _size)
			{
				end = _size;
			}
			string ret;
			ret.reserve(end - pos);
			for (size_t i = pos; i < end; i++)
			{
				ret += _str[i];
			}
			return ret;
		}
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}
		const static size_t npos = -1;
	private:
		char* _str;
		size_t  _size;
		size_t _capacity;
	};
	ostream& operator<<(ostream& out, const string& s)
	{
		out << s.c_str();
		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		//库中的cin遇到空白符会停下来,不再读取空白符,所以对于字符串,重载的时候如果用cin,是读取不到\0
		s.clear();
		//由于不知道要输入字符串的长度,所以每次读取后都可能会发生扩容,为了减少这部分消耗,也可以采用类似缓冲区的数组,先把读取到的字符存入数组	
		char buff[128] = {0};
		char ch = in.get();
		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;
	}

9.string的大小

        按照我们仿写的string类,string类的对象的大小是12个字节 

        char* _str;
		size_t  _size;
		size_t _capacity;

        但是C++语言并不严格拘束类中具体是如何实现的,只需提供库中应有的接口即可,这就使不同的编译器有不同的实现方式。比如g++

        由于Linux默认是64位,所以8字节即一个指针的大小,该指针指向一块堆空间,指针右是字符串,指针左是一个结构体,包含:

        空间总大小(容量)

        字符串有效长度(size)

        引用计数

为了解释引用计数,我先来说一下关于浅拷贝的问题:

        1.由于浅拷贝,可能会出现对同一块空间析构两次的情况,这时有野指针问题

        2.由于同一块空间被多个变量指向,其中一个变量修改会引发其他变量指向的空间也发生改变。

为了解决浅拷贝问题,用写时拷贝+引用计数的方法来解决,简单来说,就是计数有多少个变量指向同一块空间,当一个变量销毁后,引用计数就会减一,这样可以解决浅拷贝的第一个问题。而写时拷贝,就是当你需要修改这块空间的时候,才拷贝一份,这样可以让许多不需要修改空间的场景不用拷贝空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值