string类

1. 为什么学习string类?

1.1 C语言中的字符串

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

2. 标准库中的string类

2.1 string类的常用接口说明

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

2.1.1 string类对象的常见构造

(constructor)函数功能说明
string()构造空的string类对象(即空字符串)
string(const char* s) (重点)用C-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s) (重点)拷贝构造函数
void TestString1()
{
	string s1;
	cin >> s1;
	cout << "s1:"<<s1 << endl;

	string s2("123456");
	cout << "s2:" << s2 << endl;
	string s3(s2);
	cout << "s3:" << s3 << endl;
	string s4("123456", 5);//赋前五个字符
	cout << "s4:" << s4 << endl;
	string s5(10, 's');//赋10个字符's';
	cout << "s5:" << s5 << endl;
}

2.1.2 string类对象的容量操作

函数名称功能说明
size(重点)返回字符串有效字符长度
capacity返回空间总大小
empty检测字符串释放为空串,是返回true,否则返回false
clear 清空有效字符
reserve (重点)为字符串预留空间
resize(重点)将有效字符的个数该成n个,多出的空间用字符c填充
void TestString2()  // 容量相关
{
	string s("hello");
	cout << s.size() << endl; // 一般用size比较多一些,查看有效元素个数
	cout << s.length() << endl;
	cout << s.capacity() << endl; //看容量

	// s如果是空字符串 "" 返回true  否则返回false
	if (!s.empty())
	{
		cout << s << endl;
	}

	s.clear();
	if (s.empty())
	{
		cout << "空字符串" << endl;
	}
}

// reserve: 扩容,不会改变有效元素的个数
// reserve(size_t newcapacity)
// 假设当前扩容的string对象底层旧容量为 oldcapacity
// newcapacity > oldcapacity:reserve方法才会真正扩容(vs下:1.5倍扩容)
// newcapacity < oldcapacity
//   newcapacity >= 16: reserve不会将空间缩小的
//   newcapacity <= 15: reserve才会将空间缩小--15

void TestString3()
{
	string s("hello");
	//是 reserve 不是 reverse

	s.reserve(20);  // 31
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(30);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(40);   // 47
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(50);   // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(60);   // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(50);     // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(40);   // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(30);   // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(20);   // 70
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	s.reserve(15);     // 15
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

void TestString4()
{
	/*
	char*
	size_t size
	size_t capacity
	总共占用12字节
	*/
	cout << sizeof(string) << endl; // 28字节的大小
}
//

// void resize(size_t newsize, char ch) //增加
// void resize(size_t newsize)          //减少
// newsize: 将string对象中有效字符个数修改到newsize个
// ch: 如果是增多,多出的元素使用ch填充
// 将string对象中有效元素个数增加到newsize,多出的位置使用ch填充
// 注意:在增多的过程中可能会扩容
//       在减少的过程中容量不变
void TestString5()
{
	
	string s("hello");   // size:5  capacity:15
	s.resize(10, '!');   // size:10  capacity:15
	s.resize(20, '$');   // size:20  capacity:31
	s.resize(30, '%');   // size:30  capacity:31
	s.resize(20, '8');
	s.resize(8);
	s.resize(3);
}

注意

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

  1. clear()只是将string中有效字符清空,不改变底层空间大小。
  1. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字
    符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的
    元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大
    小,如果是将元素个数减少,底层空间总大小不变
  1. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于
    string的底层空间总大小时,reserve不会改变容量大小。
    (以我的vs2019为例,是PJ版本的STL的string,内部维护了一个固定大小的数组大小是16个字节在这里插入图片描述

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

函数名称功能说明
operator[] (重点)返回pos位置的字符,const string类对象调用
at与operator[]类似,越界时警告与其不同
begin+ endbegin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendrend获取第一个字符的迭代器 +rbegin获取最后一个字符下一个位置的迭代器
#include <assert.h>
void TestString7()
{
	string s("hello");
	
	cout << s[0] << endl;
	s[0] = 'H';
	cout << s << endl;

	cout << s.at(0) << endl;
	s.at(0) = 'h';
	cout << s << endl;

	// cout << s[100] << endl;    // []越界:assert
	cout << s.at(100) << endl; // at越界:exception: std::out_of_range 
}

2.1.4 string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串
operator+= (重点)在字符串后追加字符串str
c_str(重点)返回C格式字符串
find (重点)从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回
npos是string类内部维护的一个静态成员变量
void TestString6()
{
	/*
	1. 可以知道string内部的扩容机制
	2. 如果大概知道要往string中放多少个元素,在push_back之前
	   可以提前先将容量给好,否则一边插入一边扩容效率非常低
	*/
	string s;
	// s.reserve(100);
	size_t cap = s.capacity();
	
	//void push_back(char c);
	for (size_t i = 0; i < 100; ++i)
	{
		s.push_back('A');
		if (cap != s.capacity())
		{
			cap = s.capacity();
			cout << cap << endl;
		}
	}
}
void TestString8()
{
	string s("hello");
	s += ' ';
	s += "world";

	string ss("!!!");
	s += ss;

	s.append(10, 'A');
	s.clear();
	s.append("abcd");
	s.append("1234",2);
}

void TestString13()
{
	string s("hello world");
	string s1(s);
	s.c_str();
	if (s.c_str() == s1.c_str())
	{
		cout << s.c_str()<< endl;
		cout << s1.c_str() << endl;
	}
	printf("%p\n", s.c_str());
	printf("%p\n", s1.c_str());
	
}
void TestString14()
{
	int array[] = { 1, 2, 3 };
	

	string s("hello");
	// 借助下标+[]
	for (size_t i = 0; i < s.size(); ++i)
	{
		cout << s[i];  // char& operator[](size_t index)
	}
	cout << endl;

	// 范围for
	for (auto e : s)
		cout << e;
	cout << endl;

	// 使用正向迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;

	// 使用反向迭代器

	// string::reverse_iterator rit = s.rbegin();
	auto rit = s.rbegin();
	// auto a = 10;  int a = 10;
	while (rit != s.rend())
	{
		cout << *rit;
		++rit;
	}
	cout << endl;

	reverse(s.begin(), s.end());
}

// 获取一个文件的后缀
void TestString11()
{
	string filename("1.t22111.txt");
	
	cout << filename.substr(filename.rfind('.') + 1) << endl;
	// string substr (size_t pos = 0, size_t len = npos) const;
}

2.1.5 string类非成员函数

函数功能说明
operator>>输入运算符重载
operator<<输出运算符重载
getline获取一行字符串
// 一行单词,单词和单词之间使用空格隔开,最后一个单词的长度
void TestString12()
{
	string words;
	// cin >> words;
	while (getline(cin, words))
	{
		//cout << words.substr(words.rfind(" ") + 1).size() << endl;
		cout << words.substr(words.rfind(' ') + 1).size() << endl;
	}
}

例题:
找字符串中第一个只出现一次的字符
字符串里面最后一个单词的长度

3. string类的模拟实现

3.1 经典的string类问题

模拟实现string类,最主要是实现string类的构造拷贝构造赋值运算符重载以及析构函数

以下string类的实现是否有问题?

class string
{
public:
	 
string(const char* str = "")
 {
	 // 构造string类对象时,如果传递nullptr指针,认为程序非法
	 if(nullptr == str)
	 {
		 assert(false);
		 return;
 	 }	
	 _str = new char[strlen(str) + 1];
	 strcpy(_str, str);
 }
 
 ~string()
 {
	 if(_str)
	 {
		 delete[] _str;
		 _str = nullptr;
     }
 }
 
private:
 	char* _str;
};
// 测试
void Teststring()
{
 	string s1("hello world");
 	string s2(s1);
}

在这里插入图片描述

说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝

3.2 浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来(按照字节方式进行拷贝)。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。

		//浅拷贝
		string(const string& s)
			: _str(s._str)
		{}

		string& operator=(const string& s)
		{
			_str = s._str;
			return *this;
		}

3.3 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

在这里插入图片描述

3.3.1 传统版写法的string类

string(const string& s)
{
	_str = new char[strlen(s._str) + 1];
	strcpy(_str, s._str);
}

string& operator=(const string& s)
{
	if (this != &s)
	{
		//先释放掉被赋值对象的空间,以免造成内存泄漏
		delete[] _str;

		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}
	return *this;
}

3.3.2 现代版写法的string类

		//高级写法
		// 深拷贝
		string(const string& s)
			: _str(nullptr) //此时最好赋值为空,
			//不然_str就是系统默认值0xcccc cccc,文章后面有解释
		{
			string strTemp(s._str);
			swap(_str, strTemp._str);
		}

		// 深拷贝
		// s2 = s1;   s1--->s--->strtemp
		string& operator=(string s)
		{
			swap(_str, s._str);
			return *this;
		}

		~string()
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
			}
		}

------>深拷贝完整代码<-------

拷贝构造函数图解:

1.先通过构造函数在函数调用栈上,创建一个临时空间 strTemp,此时这个临时空间的成员 _str 指向构造函数给其新开辟的地址空间 0x22334455,里面的内容是"hello"。

2.交换 s2._str 和 strTemp._str 的值。此时s2._str指向0x22334455。 strTemp._str 指向nullptr
3.出了拷贝构造函数作用域,销毁临时对象(黄色部分)
----------------------------------------------------------------------------------------------------在这里插入图片描述

注意:初始化列表处给 _str 最好赋一个空。不然编译器有可能会给_str一个默认值0xcccc cccc ,出了拷贝构造函数作用域,销毁临时对象时,临时对象的 _str不为空(此时_str相当于是一个野指针),在析构函数中 delete[] 释放资源,报错。

赋值运算符现代写法原理和上面类似,就不再赘述了

3.4 写时拷贝

写时拷贝是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

3.5 string类的模拟实现

string类的模拟实现

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值