C++ string类

目录

1. 为什么要学习string类

1.1 C语言中的字符串

1.2 两个面试题(暂不做讲解)

2. 标准库中的string类

2.1 string类(了解)

 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

1. string类对象的常见构造

 2. string类对象的容量操作

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

4. string类对象的修改操作

 5. string类非成员函数

6. 牛刀小试

3. string类的模拟实现

3.1 经典的string类问题

3.2 浅拷贝

3.3 深拷贝

 3.3.1 传统版写法的string类

 3.3.2 现代版写法的string类

 3.3 写时拷贝(了解)

3.4 string类的模拟实现

4. 扩展阅读


1. 为什么要学习string

1.1 C语言中的字符串

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

1.2 两个面试题(暂不做讲解)

把字符串转换成整数_牛客题霸_牛客网
力扣  字符串相加
OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string 类,很少有人去使用 C 库中的字符串操作函数。

2. 标准库中的string

2.1 string(了解)

string - C++ Reference

1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string 类是使用 char( 即作为它的字符类型,使用它的默认 char_traits 和分配器类型 ( 关于模板的更多信息,请参阅basic_string)
4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits和allocator 作为 basic_string 的默认参数 ( 根于更多的模板信息请参考 basic_string)
5. 注意,这个类独立于所使用的编码来处理字节 : 如果用来处理多字节或变长字符 ( UTF-8) 的序列,这个类的所有成员( 如长度或大小 ) 以及它的迭代器,将仍然按照字节 ( 而不是实际编码的字符 ) 来操作。
总结:
1. string 是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。

3. string 在底层实际是: basic_string 模板类的别名, typedef basic_string string;
4. 不能操作多字节或者变长字符的序列。
使用 string 类时,必须包含 #include 头文件以及 using namespace std ;

 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

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. string类对象的容量操作

函数名称 功能说明
string::size - C++ Reference(重点) 返回字符串有效字符长度
string::length - C++ Reference 返回字符串有效字符长度
string::capacity - C++ Reference 返回空间总大小
string::empty - C++ Reference(重点) 检测字符串释放为空串,是返回true,否则返回false
string::clear - C++ Reference(重点) 清空有效字符
string::reserve - C++ Reference(重点) 为字符串预留空间**
string::resize - C++ Reference(重点) 将有效字符的个数改成n个,多出的空间用字符c填充
// size/clear/resize
void Teststring1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, bit!!!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}
//================================================================================
====
void Teststring2()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}
// 利用reserve提高插入数据的效率,避免增容带来的开销
//================================================================================
====
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}
void TestPushBackReserve()
{
	string s;
	s.reserve(100);
	size_t sz = s.capacity();

	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}
注意:
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 不会改变容量大小。

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

函数名称 功能实现
string::operator[] - C++ Reference(重点) 返回pos位置的字符,const string类对象调用
string::begin - C++ Reference+string::end - C++ Reference begin获取一个字符的迭代器+end获取最后一个字符下一个位置的迭代器
string::rbegin - C++ Reference+string::rend - C++ Reference begin获取一个字符的迭代器+end获取最后一个字符下一个位置的迭代器
范围for C++11支持更简洁的范围for的新遍历方式
void Teststring()
{
	string s1("hello Bit");
	const string s2("Hello Bit");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;

	s1[0] = 'H';
	cout << s1 << endl;

	// s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改
}
void Teststring()
{
	string s("hello Bit");
	// 3种遍历方式:
	// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
	// 另外以下三种方式对于string而言,第一种使用最多
	// 1. for+operator[]
	for (size_t i = 0; i < s.size(); ++i)
		cout << s[i] << endl;

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

	string::reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
		cout << *rit << endl;

	// 3.范围for
	for (auto ch : s)
		cout << ch << endl;
}

三种遍历方式

 int main()
{
	string s1;
	string s2("hello bit");

	// 三种遍历
	// 1、下标+[]
	for (size_t i = 0; i < s2.size(); ++i)
	{
		// s2.operator[](i)
		s2[i] = 'x';
	}
	cout << endl;

	for (size_t i = 0; i < s2.size(); ++i)
	{
		// s2.operator[](i)
		cout << s2[i] << " ";
		//cout << s2.at(i) << " ";
	}
	cout << endl;

	//s2[10]; // 越界断言报错
	try 
	{
		s2.at(10);  // 越界抛异常
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}

	// 2、迭代器
	// [begin(), end() ) end()返回的不是最后一个数据位置的迭代器,返回是最后一个位置下一个位置
	// 也要注意的是,C++中凡是给迭代器一般都是给的[)左闭右开的区间
	// 迭代器是类似指针一样东西,具体是什么我们讲了底层实现才能知道
	string::iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 迭代器意义:像string、vector支持[]遍历,但是list、map等等容器不支持[]
	// 我们就要用迭代器遍历,所以迭代器是一种统一使用的方式
	vector v = { 1, 2, 3, 4 };
	vector::iterator vit = v.begin();
	while (vit != v.end())
	{
		cout << *vit << " ";
		++vit;
	}
	cout << endl;

	list lt = { 1, 2, 3, 4 };
	list::iterator ltit = lt.begin();
	while (ltit != lt.end())
	{
		cout << *ltit << " ";
		++ltit;
	}
	cout << endl;

	// 反向迭代器
	string s3("123456");
	string::iterator it3 = s3.begin();
	while (it3 != s3.end())
	{
		*it3 += 5;

		++it3;
	}
	cout << endl;

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

	// 3、C++11 提供 范围for, 特点:写起来简洁
	// 依次取容器中的数据,赋值给e,自动判断结束
	//for (auto& e : s3)
	for (char& e : s3)
	{
		e += 1;
	}
	cout << endl;

	for (auto e : s3)
	{
		cout << e << " ";
	}
	cout << endl;

	for (auto x : v)
	{
		cout << x << " ";
	}
	cout << endl;

	for (auto x : lt)
	{
		cout << x << " ";
	}
	cout << endl;


	return 0;
}

C++ string类_第1张图片

 C++ string类_第2张图片

4. string类对象的修改操作

函数名称 功能说明
string::push_back - C++ Reference
在字符串后尾插字符 c
string::append - C++ Reference
在字符串后追加一个字符串
string::operator+= - C++ Reference(重点) 在字符串后追加字符str
string::c_str - C++ Reference(重点) 返回C格式字符串
string::find - C++ Reference+string::npos - C++ Reference(重点)
从字符串 pos 位置开始往后找字符 c ,返回该字符在字符串中的位置
string::rfind - C++ Reference
从字符串 pos 位置开始往前找字符 c ,返回该字符在字符串中的位置
string::substr - C++ Reference
str 中从 pos 位置开始,截取 n 个字符,然后将其返回
void Teststring()
{
	string str;
	str.push_back(' '); // 在str后插入空格
	str.append("hello"); // 在str后追加一个字符"hello"
	str += 'b'; // 在str后追加一个字符'b' 
	str += "it"; // 在str后追加一个字符串"it"
	cout << str << endl;
	cout << str.c_str() << endl; // 以C语言的方式打印字符串

	// 获取file的后缀
	string file1("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;

	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;

	// 取出url中的域名
	sring url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");
	if (start == string::npos)
	{
		cout << "invalid url" << endl;
		return;
	}
	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;

	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}
int main()
{
	string s1;
	s1.push_back('h');
	s1.push_back('e');
	s1.push_back('l');
	s1.push_back('l');
	s1.push_back('o');

	s1.append("world");
	cout << s1 << endl;

	string s2("!!!!");
	//s1.append(s2);
	s1.append(s2.begin(), s2.end());
	cout << s1 << endl;

	// 实际中最喜欢用这个+=
	s1 += ' ';
	s1 += "比特";
	s1 += s2;
	cout << s1 << endl;

	// 尽量少用insert,因为底层实现是数组,头部或者中间插入需要挪动数据
	s1.insert(0, "x");
	cout << s1 << endl;
	s1.insert(3, "yyyy");
	cout << s1 << endl;
	s1.insert(0, "yyyy");
	cout << s1 << endl;
	//s1.insert(300, "yyyy");
	cout << s1 << endl;

	s1.erase(0, 1);
	cout << s1 << endl;

	s1.erase(0, 3);
	cout << s1 << endl;

	s1.erase(3, 10);
	cout << s1 << endl;

	//s1.erase(3, 100);
	s1.erase(3);
	cout << s1 << endl;

	s1.erase();
	cout << s1 << endl;

	return 0;
}
C++ string类_第3张图片

 c_str用于配合其他函数接口

C++ string类_第4张图片

 假设要求取出文件名的后缀

	// 假设要求取出文件名的后缀
	string filename = "test.txt.zip";
	size_t pos = filename.rfind('.');//实现只取最后一个后缀
	if (pos != string::npos)
	{
		//string suff(filename, pos, filename.size() - pos);
		//string suff(filename, pos);
		//string suff = filename.substr(pos, filename.size() - pos);
		string suff = filename.substr(pos);

		cout << suff << endl;
	}
要求写一个程序分别取出域名和协议名
string GetDomain(const string& url)
{
	size_t pos = url.find("://");
	if (pos != string::npos)
	{
		size_t start = pos + 3;
		size_t end = url.find('/', start);
		if (end != string::npos)
		{
			return url.substr(start, end - start);
		}
		else
		{
			return string();
		}
	}
	else
	{
		return string();
	}
}

string GetProtocol(const string& url)
{
	size_t pos = url.find("://");
	if (pos != string::npos)
	{
		return url.substr(0, pos - 0);
	}
	else
	{
		//string s;
		//return s;
		return string();//返回一个匿名对象
	}
}
    string url1 = "http://www.cplusplus.com/reference/string/string/rfind/";
	string url2 = "https://tower.im/users/sign_in";
	string url3 = "tower.im/users/sign_in";

	cout << GetDomain(url1) << endl;
	cout << GetProtocol(url1) << endl;

	cout << GetDomain(url2) << endl;
	cout << GetProtocol(url2) << endl;

	cout << GetProtocol(url3) << endl;

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

 5. string类非成员函数

函数 功能说明
operator+ (string) - C++ Reference
尽量少用,因为传值返回,导致深拷贝效率低
operator>> (string) - C++ Reference(重点)
输入运算符重载
operator<< (string) - C++ Reference(重点) 输出运算符重载
getline (string) - C++ Reference(重点) 获取一行字符串
relational operators (string) - C++ Reference(重点) 大小比较

 上面的几个接口大家了解一下,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。

6. 牛刀小试

见博客

3. string类的模拟实现

3.1 经典的string类问题

上面已经对 string 类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string 类,最主要是实现 string 类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string 类的实现是否有问题?
class string
{
public:
	/*string()
	:_str(new char[1])
	{*_str = '\0';}
	*/
	//string(const char* str = "\0") 错误示范
	//string(const char* str = nullptr) 错误示范
	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 bit!!!");
	string s2(s1);
}

C++ string类_第5张图片

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

3.2 浅拷贝

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

3.3 深拷贝

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

C++ string类_第6张图片

 3.3.1 传统版写法的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(const string& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	string& operator=(const string& s)
	{
		if (this != &s)
		{
			char* pStr = new char[strlen(s._str) + 1];
			strcpy(pStr, s._str);
			delete[] _str;
			_str = pStr;
		}

		return *this;
	}

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

private:
	char* _str;
};

 3.3.2 现代版写法的string

class string
{
public:
	string(const char* str = "")
	{
		if (nullptr == str)
			str = "";

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	string(const string& s)
		: _str(nullptr)
	{
		string strTmp(s._str);
		swap(_str, strTmp._str);
	}

	// 对比下和上面的赋值那个实现比较好?
	string& operator=(string s)
	{
		swap(_str, s._str);
		return *this;
	}

	/*
	string& operator=(const string& s)
	{
	if(this != &s)
	{
	string strTmp(s);
	swap(_str, strTmp._str);
	}

	return *this;
	}
	*/

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

private:
	char* _str;
};

 3.3 写时拷贝(了解)

C++ string类_第7张图片

 C++ string类_第8张图片

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1 ,每增加一个对象使用该资源,就给计数增加1 ,当某个对象被销毁时,先给该计数减 1 ,然后再检查是否需要释放资源,如果计数为 1 ,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell

3.4 string类的模拟实现与补充

见博客

 C++ string类_第9张图片

 

补充:
C++ string类_第10张图片

4. 扩展阅读

C++面试中string类的一种正确写法 | 酷 壳 - CoolShell

STL 的string类怎么啦?_haoel的博客-CSDN博客

你可能感兴趣的:(C++初阶,c语言,c++,STL)