【C++ STL】string 容器(介绍、使用、深浅拷贝、模拟实现、写时拷贝)

【C++ STL】string 容器(介绍、使用、深浅拷贝、模拟实现、写时拷贝),第1张

文章目录
      • 前言
      • 一、STL - string 的介绍
      • 二、string 的使用(常用接口介绍)
        • 2.1 常见构造
        • 2.2 容量 *** 作
        • 2.3 访问 *** 作
        • 2.4 迭代器及遍历 *** 作
        • 2.5 修改 *** 作
        • 2.6 string 类的非成员函数重载
        • 2.7 补充一些接口
      • 三、string 类的模拟实现
        • 3.1 深浅拷贝(⭐重要)
          • ① 浅拷贝 & 深拷贝
          • ② 拷贝构造 & 赋值运算符重载(传统写法)
          • ③ 拷贝构造 & 赋值运算符重载(现代写法)
        • 3.2 string 类的结构
        • 3.3 string 类的迭代器
        • 3.4 一些成员函数的实现
          • ① 默认成员函数
          • ② 访问 *** 作 `[]` 运算符重载
          • ③ 容量 *** 作
          • ④ 修改 *** 作
          • ⑤ 字符串 *** 作
        • 3.5 一些非成员函数的实现
          • ① 流插入 << 运算符重载
          • ② 流提取 >> 运算符重载
          • ③ getline 函数
          • ④ 关系运算符 > 重载
        • 3.6 补充:string 类对象的大小?
      • 四、写时拷贝(了解)
      • 五、拓展阅读

前言

为什么学习 string 类呢?

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

C++ STL string 是对字符串进行管理的类。实际上就是一个管理字符数组的顺序表。

在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 语言库中的字符串 *** 作函数。


一、STL - string 的介绍

文档介绍:string - C++ Reference (cplusplus.com)

  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. 不能 *** 作多字节或者变长字符的序列。

补充:

  • 编码:计算机中只存储二进制数(0 / 1),那如何去表示文字呢,需要制定对应的编码表,规定用哪些二进制数字表示(映射)哪个符号,当然每个人都可以约定自己的一套,而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,比如美国有关的标准化组织就出台了 ASCII 码表(基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言)

  • ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符,到目前为止共定义了 128 个字符。

  • 所以早期的计算机中只能表示英文,不能表示其它国家的文字,当全世界各个国家都开始使用计算机了,就需要建立出对应语言的编码表。

  • UTF - 8 是对不同范围的字符使用不同长度的编码,这样能够适应不同的语言,比如有些语言单字节编码就够了,有些语言需要多字节编码才够。

  • 还有其它编码表,GBK码(对多达2万多的简繁汉字进行了编码,简体版的Win95和Win98都是使用GBK作系统内码)等等。

注意:使用 string 类需要包含头文件


二、string 的使用(常用接口介绍)

string 类的成员函数(接口)非常的多,我们学习一些常用的就行了,其它不常用的,需要时去查文档就好了。

2.1 常见构造

构造函数:

string();                         // 默认构造,构造空的string类对象,即空字符串"" ⭐
string (const string& str);       // 拷贝构造,用已有的string类对象去构造string类对象 ⭐
string (const char* s);           // 用c-string来构造string类对象 ⭐
string (const char* s, size_t n); // 用c-string前n个字符来构造string类对象
string (size_t n, char c);        // 用n个c字符来构造string对象
template     // 用迭代器[first,last)范围内的字符序列构造string类对象
  string  (InputIterator first, InputIterator last);

👉Example:

// string constructor
#include 
#include 

int main()
{
    std::string s0 ("Initial string");

    std::string s1;                                    // s1: ""
    std::string s2 (s0);                               // s2: Initial string
    std::string s4 ("A character sequence");           // s4: A character sequence
    std::string s5 ("Another character sequence", 12); // s5: Another char
    std::string s6 (10, 'x');                          // s6: xxxxxxxxxx
    std::string s7 (s0.begin(), s0.begin() + 7);       // s7: Initial
    return 0;
}

👉思考:空串是什么都没有吗,存储空间为空吗?


2.2 容量 *** 作
函数名称功能说明
size⭐返回字符串有效字符长度(为了统一设计,所有容器都是用 size 表示有效元素个数)
length返回字符串有效字符长度(这是早期提供的接口)
resize⭐将字符串大小调整为 n 个有效字符的长度
capacity返回有效字符的最大容量(即已分配 size 的大小)
reserve⭐更改容量(capacity)的大小
clear⭐清空字符串的内容,变为空字符串(size 变为 0,不改变 capacity 的大小)
empty⭐检测字符串是否为空串,是返回 true,否则返回 false

👉Example1:

resize 函数的两种重载形式:

void resize (size_t n);
void resize (size_t n, char c);
void Test1()
{
	string s("hello");

	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// "helloaaaaa"
	s.resize(10, 'a');
	cout << s.size() << endl;     // 10
	cout << s.capacity() << endl; // 15
	cout << s << endl;            // s: "helloaaaaa"

	// 将s中有效字符个数增加到20个,多出位置用缺省值'void reserve (size_t n = 0);
'进行填充
	// 如果resize参数大于原有 capacity 大小,会进行增容
	// "helloaaaaavoid Test2()
{
	string s("hellohellohello");

	// 如果reserve参数大于原有 capacity 大小,会进行增容
	s.reserve(20);
	cout << s.size() << endl;     // 15
	cout << s.capacity() << endl; // 31

	// 测试reserve参数小于原有 capacity 大小,是否会将空间缩小呢?
	// VS2019下,如果字符串有效长度size大于参数10,不会缩小,如果字符串长度小于参数10,会缩小
	// 当然,这个也和编译器平台有关系
	s.reserve(10);
	cout << s.size() << endl;     // 15
	cout << s.capacity() << endl; // 31(VS2019)
}
void Test3()
{
	string s;

	cout << "initial value: " << s.capacity() << endl;

	size_t sz = s.capacity();
	for (size_t i = 0; i < 500; i++)
	{
		s.push_back('a');
		if (s.capacity() != sz)
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}
函数名称功能说明operator[]⭐返回对字符串中 pos 位置处的字符的引用(string 类对象支持随机访问)(一般物理地址是连续的才支持)char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
函数名称功能说明begin(iterator)"
	s.resize(20);
	cout << s.size() << endl;     // 15
	cout << s.capacity() << endl; // 31
	cout << s << endl;            // s: "helloaaaaa"

    // 如果resize参数x原有 capacity 大小
	// 将s中有效字符个数缩小到2个
	// "he"
	s.resize(2);
	cout << s.size() << endl;     // 2
	cout << s.capacity() << endl; // 31
	cout << s << endl;            // s: "he"

	// 将s的内容清空,注意清空时只是将size置0,不改变capacity的大小
	s.clear();
	cout << s.size() << endl;     // 0
	cout << s.capacity() << endl; // 31
	cout << s << endl;            // s: ""

	return 0;
}

👉Example2:

reserve 函数介绍:

返回指向第一个有效字符的迭代器
  • 如果 n 大于当前字符串容量,则该函数使容器将其容量增加到 n 个字符(或更大)。

  • 在所有其他情况下,缩小字符串容量被视为非绑定请求:容器可以自由实现优化,但要保留容量大于 n 的字符串。

  • 此函数对字符串长度没有影响,并且不能更改其内容。

end

测试:reserve 是如何进行增容呢?

返回指向字符串末尾字符的迭代器(即最后一个有效字符的下一个位置)

经过测试,VS2019 下大概是 1.5 倍增容:

Linux g++下是 2 倍增容:

思考:resize 和 reserve 的意义在哪里呢?

reserve 的作用:如果知道需要多大的空间,可以利用 reserve 提前一次性把空间开好,避免增容带来的开销。

resize 的作用:既要开好空间,还要对这些空间初始化,就可以使用 resize


2.3 访问 *** 作

⭐有了这个运算符重载,我们就可以像使用数组一样去使用 string 类对象。

rbegin(reverse_iterator)反向迭代器(可以反向遍历对象)
rend反向迭代器

operator[] 函数的两种重载形式:

范围 for

operator[] 函数会检查越界(pos 必须小于 size)


2.4 迭代器及遍历 *** 作

⭐所有容器都有迭代器,迭代器提供了用统一类似的方式去访问容器。

iterator begin(); // 可读可写 const_iterator begin() const; // 只读 void test(const std::string& s) { // const对象必须要用const迭代器 std::string::const_iterator it = s.begin(); while (it != s.end()) { std::cout << *it; it++; } } int main() { std::string s1; std::string s2("hello"); // for+operator[]遍历 for (size_t i = 0; i < s.size(); ++i) cout << s2[i] << endl; // 正向迭代器遍历 // 注意:这里不建议写成it < s2.end(),比如链式结构的容器,就没法用了 // 所以统一写成 it != s2.end() std::string::iterator it = s2.begin(); while (it != s2.end()) { std::cout << *it; it++; } // 反向迭代器遍历 for (std::string::reverse_iterator rit = s2.rbegin(); rit != s2.rend(); ++rit) std::cout << *rit; for (auto rit = s2.rbegin(); rit != s2.rend(); ++rit) // 用auto简化代码 std::cout << *rit; // 范围for遍历,支持迭代器的容器就支持范围for for (auto& e : s2) { std::cout << e; } return 0; } void push_back (char c);
C++11支持更简洁的范围 for 的新遍历方式(底层其实是被替换成迭代器,所以支持迭代器就支持范围 for)
函数名称功能说明
operator+=⭐⭐在当前字符串末尾追加字符串(追加 string / char* / char 类型的都可以)
append在当前字符串末尾追加字符串
push_back
swap

迭代器有两个版本:普通迭代器和 const 迭代器

交换两个字符串的内容(注意:还存在一个具有相同名称的非成员函数 swap)

👉Example:

c_str⭐

2.5 修改 *** 作

尽量不要用 insert 和 erase 函数,因为要挪动字符,时间效率太低。

const char* c_str() const;static const size_t npos = -1;void Test4() { string s1("hello"); string s2("world"); s1 += ' '; // 追加字符 s1 += s2; // 追加string类对象 s1 += "!!!"; // 追加字符串 } // 从pos位置开始往后找,默认从第一个字符的位置(即pos = 0)开始找 size_t find (const string& str, size_t pos = 0) const; size_t find (const char* s, size_t pos = 0) const; size_t find (const char* s, size_t pos, size_t n) const; // 从pos位置往后匹配n个字符 size_t find (char c, size_t pos = 0) const; void Test5() { // 取出文件1的后缀 string file1("test.txt"); size_t pos = file1.find("."); if (pos != string::npos) { cout << file1.substr(pos) << endl; // .txt // cout << file.substr(pos, file1.size() - pos) << endl; } // 取出文件2的后缀 string file2("test.txt.zip"); size_t rpos = file2.rfind("."); if (rpos != string::npos) { cout << file2.substr(rpos) << endl; // .zip } } void Test6() { // 取出url中的协议、域名、uri string url("http://www.cplusplus.com/reference/"); size_t pos1 = url.find("://"); if (pos1 != string::npos) { cout << url.substr(0, pos1 - 0) << endl; } size_t pos2 = pos1 + 3; // 'w'的位置 size_t pos3 = url.find('/', pos2); // '/'的位置,从pos2开始查找 if (pos3 != string::npos) { cout << url.substr(pos2, pos3 - pos2) << endl; } cout << url.substr(pos3) << endl; } void Test7() { string s("hello"); printf("%s\n", s.c_str()); // z }
find⭐
从 pos 位置开始往后找字符,返回该字符在字符串中的位置,如果未找到返回 nposnpos⭐
rfind
从 pos 位置开始往前找字符,返回该字符在字符串中的位置,如果未找到返回 npos将一个字符附加到字符串的末尾(尾插)(substr⭐
在字符串中从 pos 位置开始,截取 len 个字符,然后将其作为新的 string 类对象返回
返回指向 C 格式字符串的数组的指针(// len: 从pos位置开始,要截取字符的个数 string substr (size_t pos = 0, size_t len = npos) const; // 若len缺省,默认截取到字符串最后
作为返回值,通常用于表示不匹配(npos是一个静态成员变量 函数名称
功能说明流提取运算符重载
std::operator<<⭐流插入运算符重载

👉Example1:

std::getline⭐

👉Example2:

find 函数介绍:

获取一行字符串,直到遇到换行符 ‘\n’

substr 函数介绍:

relational operators⭐
关系运算符,进行大小比较

👉Example3:

std::operator+

👉Example4:

尽量少用,因为是传值返回,导致深拷贝效率低

2.6 string 类的非成员函数重载 // istream& getline (istream& is, string& str); string s; getline(cin, s);
std::swap交换两个字符串的值
std::operator>>⭐
函数名称功能说明
int isalpha(int c)检查字符是否为字母,是返回非零(true),不是则返回0(false)
int isdigit(int c)检查字符是否为十进制数字,是返回非零(true),不是则返回0(false)
函数名称功能说明
int tolower(int c)把字母转换成小写字母,返回转换之后的字符

getline 函数介绍:

int toupper(int c)

2.7 补充一些接口

C 语言库文件 中的处理 C 字符的接口

  • 字符处理函数:

    // 也可传数组,因为指向数组空间的指针是天然的迭代器 int arr[] = { 1, 5, 4, 2, 3 }; sort(arr, arr + 5); namespace winter { class string { private: char* _str; public: // 构造函数 string(const char* s) :_str(new char[strlen(s) + 1]) { strcpy(_str, s); } // 析构函数 ~string() { delete[] _str; _str = nullptr; } }; void test() { string s1("hello"); // 用一个常量字符串去构造string类对象s1 string s2(s1); // s2调用编译器默认生成的拷贝构造函数 } } _str_str
  • 同一块空间会被析构多次
  • 把字母转换成大写字母,返回转换之后的字符
  • 字符转换函数:

  • 一个对象修改会影响另外一个对象
  • // 显式定义拷贝构造函数(深拷贝) // s2() string(const string& s) // 保护形参不被改变,加引用防止无穷递归 :_str(new char[strlen(s._str) + 1]) // 给新对象申请一段和原对象一样大小的空间 { strcpy(_str, s._str); // 把原对象的数据一一拷贝给新对象 } // 显式定义赋值运算符重载(深拷贝) // s1 = s2 string& operator=(const string& s) { if (this != &s) // 防止自己给自己赋值 { delete[] _str; // 释放自己的空间 _str = new char[strlen(s._str) + 1]; // 重新开辟一块和s一样大小的空间 strcpy(_str, s._str); // 把s的数据拷贝过来 } return *this; } // ... test code string s1("hello"); string s2("hello world"); s1 = s2; // ...
    // 显式定义赋值运算符重载(深拷贝) string& operator=(const string& s) { if (this != &s) // 防止自己给自己赋值 { char* tmp = new char[strlen(s._str) + 1]; // 重新开辟一块和s一样大小的空间 delete[] _str; // 释放自己的空间 _str = tmp; strcpy(_str, s._str); // 把s的数据拷贝过来 } return *this; }

头文件 中:

  • 函数 std::to_string(C++11):将数值转换为字符串,返回 string 类对象。

  • 函数 std::stoi(C++11):将字符串转换为整数,返回 int 整数。

头文件 中:

  • 函数 std::reverse:反转范围 [first,last) 中元素的顺序。

    // 传一段迭代器区间 [first, last)
    template  // 双向迭代器
      void reverse (BidirectionalIterator first, BidirectionalIterator last);
    
  • 函数 std::sort:将 [first,last) 范围内的元素按升序排序。

    // 传一段迭代器区间 [first, last),默认排升序,若要排降序,需要传仿函数
    template  // 随机访问迭代器
      void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
    

    👉Example:

    // 拷贝构造函数(深拷贝)
    // s2(s1)
    string(const string& s)
        :_str(nullptr) // 当前对象是一个正在构造的对象,成员变量还未初始化,是一个随机值,所以先置空
    {
    	string tmp(s._str);   // 拿s的内容,调用构造函数构造临时对象tmp
    	swap(_str, tmp._str); // 将临时对象tmp和当前对象的成员变量_str进行交换
    }
    

三、string 类的模拟实现

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

3.1 深浅拷贝(⭐重要) ① 浅拷贝 & 深拷贝

需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成「深拷贝」的。

👉请看下面这个浅拷贝的例子:

这里必须是「深拷贝」,编译器默认生成的拷贝构造函数是「浅拷贝」,会导致两个 string 对象中的字符指针 // 赋值运算符重载(深拷贝) // s1 = s2 /* 写法一: string& operator=(const string& s) // 传引用 { if (this != &s) // 防止自己给自己赋值 { string tmp(s._str); // 拿s的内容,调用构造函数构造临时对象tmp swap(_str, tmp._str); // 将临时对象tmp和当前对象的成员变量_str进行交换 } return *this; } */ // 写法二: string& operator=(string s) // 重点:传值 { // 传参时,调用拷贝构造函数,拷贝构造了一个string类对象s // 将拷贝构造出来的string类对象s和当前对象的成员变量_str进行交换 swap(_str, s._str); return *this; // 返回当前对象 } 指向的是同一个字符数组。(因为浅拷贝只拷贝了

数组指针的 4 个字节的内容)

如图:指向了同一块空间

那么会引发什么问题呢?当 test() 函数结束时,会先调用析构函数清理 s2,此时 _str 指向空间已经还给 *** 作系统了,然后再调用析构函数清理 s1,导致 _str 指向的空间被释放两次,引发程序崩溃。

所以在上述类中必须要显式定义拷贝构造函数,否则编译器默认生成的拷贝构造函数无法正常完成拷贝。

总结:上述 string 类没有显式定义其拷贝构造函数与赋值运算符重载函数,此时编译器会默认生成一个,当用 s1 构造 s2 时,编译器会调用默认生成的拷贝构造函数。最终导致:s1 和 s2 共用同一块内存空间,在调用析构函数清理对象资源时,同一块空间被释放多次,引起程序崩溃,这种拷贝方式是浅拷贝。

浅拷贝:也称位拷贝,编译器只是将对象中的数据「按字节序」拷贝过来。如果对象中管理的有其它资源(比如堆上的资源),那就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而另一些对象不知道该资源已经被释放,以为还有效,就继续对资源进行 *** 作(比如增删查改),此时就会发生违规访问。

浅拷贝引发的问题:

    class string { private: char* _str; // 指向字符数组 size_t _size; // 有效字符数 size_t _capacity; // 有效字符容量,不包含最后作标识的'void test3() { string s1("hello world"); // iterator是内嵌类型,在stirng类域里面定义的类型 // 告诉编译器,要到string类域里面去找 string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it; it++; } cout << endl; // 范围for的原理就是被替换成迭代器 for (auto e : s1) { cout << e; } cout << endl; } ' static const size_t npos; public: /*******************************************************/ // 迭代器 // iterator是内嵌类型,在stirng类域里面定义的类型 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; } // 返回指向最后一个字符下一个字符的迭代器 // ... // ... }; const size_t string::npos = -1; // 默认构造函数 string(const char* str = "") // 空串并不是什么都没有,第一个字符为'// s1.swap(s2); void swap(string& s) { // 函数名冲突,指定去调用全局域里面的::swap ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } ' :_size(strlen(str)) ,_capacity(_size) { _str = new char[_capacity + 1]; // 多开一个空间是存放'// 析构函数 ~string() { delete[] _str; _str = nullptr; } '的 strcpy(_str, str); // 拷贝数据 }

为了解决浅拷贝问题,C++ 中引入了「深拷贝」。

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

深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为共享资源问题而造成多次释放资源,导致程序崩溃。


② 拷贝构造 & 赋值运算符重载(传统写法)

👉👉拷贝构造函数的深拷贝(传统写法):

[]

string s2(s1); 调用显式定义的拷贝构造函数,实现深拷贝:

如图:string 类对象 s1 和 s2 中的 _str 指向不同空间:


👉👉赋值运算符重载函数的深拷贝(传统写法):

需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义赋值运算符重载函数来完成「深拷贝」的。

定义赋值运算符重载函数,有两个 string 类对象 s1 和 s2,要把其中一个赋值给另外一个,但是我们不知道这两个字符串谁长谁短,那该如何实现呢?

  • 对于普通数组而言,越界读一般是检查不出来的,越界写是抽查,可能会被检查出来。
  • 仔细观察,这个代码有没有什么问题呢?new 开辟空间失败了怎么办?

    所以我们先开辟空间,如果开辟空间没有失败,再去释放自己的空间。

  • 对于string类而言,越界读和越界写都会被检查出来,因为在[]中进行了严格的检查。
  • // 普通版本和const版本
    char& operator[](size_t pos) // 可读可写
    {
        assert(pos < _size);
        return _str[pos]; // *(_str + pos)
    }
    const char& operator[](size_t pos) const // 只读不能写
    {
        assert(pos < _size);
        return _str[pos]; // *(_str + pos)
    }
    

    ③ 拷贝构造 & 赋值运算符重载(现代写法)

    上面的拷贝构造、赋值运算符重载函数的深拷贝实现,我们称之为传统写法。

    接下来介绍拷贝构造、赋值运算符重载函数的深拷贝实现的「现代写法」。

    • 传统写法,想要做深拷贝,都是自己去做,自己去开辟空间,自己去拷贝想要的内容。

    • 现代写法,想要做深拷贝,没有自己去做,而是去构造一个的临时对象,临时对象中就是自己想要的内容,然后将临时对象与当前对象的成员变量分别交换,这样当前对象就拿到了自己想要的内容,当函数调用结束后,临时对象出作用域时会被自动析构。

    👉👉拷贝构造函数的深拷贝(现代写法):

    string& operator+=(const char ch)
    {
        push_back(ch);
        return *this;
    }
    string& operator+=(const char* str)
    {
        append(str);
        return *this;
    }
    string& operator+=(const string& s)
    {
        append(s._str);
        return *this;
    }
    

    👉👉赋值运算符重载函数的深拷贝(现代写法):

    通过参数间接调用拷贝构造函数,将「拷贝构造出来的 string 类对象 s」和「当前对象」的成员变量分别进行交换即可,这样当前对象就拿到了自己想要的内容,当函数调用结束后,拷贝构造出来的对象 s 出了作用域会被自动析构。

    // 流插入运算符重载
    ostream& operator<<(ostream& out, const string& s)
    {
        // 遍历string类对象 s ,一个一个字符的插入
        for (size_t i = 0; i < s.size(); i++)
        {
            out << s[i];
        }
    
        return out;
    }
    
    void test6()
    {
        // 一般场景下,以下两种输出没有差别
        string s1("hello");
        cout << s1 << endl;         // 编译器当成自定义类型对象处理,匹配该类型重载的<<运算符
        cout << s1.c_str() << endl; // C_str()返回char*,编译器当成字符数组处理(内置类型)
    
        // 但是这种场景下有区别
        string s2("hello");
        s2.resize(10);
        s2[9] = 'x'; // 最后一个字符设为'x'
    
        // s2 = "hello' ''\n'std::istream函数名称x"
    
        cout << s2 << endl;         // 输出所有有效字符,"hello功能说明std::istream::get从流中提取一个字符,不管是什么字符,都可以获取(好比C中的getchar()函数)
    x" cout << s2.c_str() << endl; // 遇到'
    '终止,"hello" }

    3.2 string 类的结构

    string 是对字符串进行管理的类。实际上就是一个管理字符数组的顺序表。

    istream& getline(istream& in, string& s) { // 先清空所有有效字符 s.clear(); // 一个一个字符的输入 // 遇到换行符终止输入 char ch; ch = in.get(); // 从流中获取一个字符 while (ch != '\n') { s += ch; // 把获取的字符追加到string类对象 s 中去 ch = in.get(); // 继续从流中获取下一个字符 } return in; }
    #include
    #include
    #include
    using namespace std;
    
    namespace winter
    {
        class string
        {
    	private:
            char* _str;       // 指向字符数组
            size_t _size;     // 有效字符数
            size_t _capacity; // 有效字符容量,不包含最后作标识的'\0'
    
            static const size_t npos;
    
    	public:
            /*******************************************************/
            // 迭代器
            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; } // 返回指向最后一个字符下一个字符的迭代器
    
            /*******************************************************/
            // 默认成员函数:
    
            string(const char* str = ""); // 默认构造函数
            void swap(string& s); // 交换两个对象的内容
            string(const string& s); // 拷贝构造函数(深拷贝)
            string& operator=(string s); // 赋值运算符重载(深拷贝)
            ~string(); // 析构函数
    
            /*******************************************************/
            // 访问元素,[]运算符重载
            
            char& operator[](size_t pos);             // 可读可写
            const char& operator[](size_t pos) const; // 只读不能写
    
            /*******************************************************/
    		// 容量 *** 作:
            
            // 获取字符串有效元素个数
            size_t size() const { return _size; } 
            // 获取字符串容量(有效字符的最大容量)
            size_t capacity() const { return _capacity; } 
            // 清空有效字符
            void clear() 
            {
                _str[0] = '\0';
                _size = 0;
            }
            // 更改容量(capacity)的大小
            void reserve(size_t n); 
            // 调整字符串有效字符的长度
            void resize(size_t n, char ch = '\0'); 
    
    		/*******************************************************/
            // 修改 *** 作:
            
            string& insert(size_t pos, const char ch); // 在pos位置插入一个字符
            
            string& insert(size_t pos, const char* str); // 在pos位置插入一个字符串
            
            void push_back(const char ch); // 尾插一个字符
            
            void append(const char* str); // 在当前字符串末尾追加一个字符串
            
            string& operator+=(const char ch); // 当前字符串末尾追加一个字符/字符串
            string& operator+=(const char* str);
            string& operator+=(const string& s);        
            
            string& erase(size_t pos = 0, size_t len = npos); // 删除从pos位置开始的len个字符
            
            /*******************************************************/
            // String operations
            
            // 从pos位置开始查找字符,若找到,则返回该字符的下标,若没找到,则返回npos
            size_t find(char ch, size_t pos = 0) const;
    
            // 从pos位置开始查找子串,若找到,则返回该子串首字符的下标,若没找到,则返回npos
            size_t find(const char* str, size_t pos = 0) const;
    		
            // 返回指向 C 格式字符串的数组的指针
            char* c_str() const { return _str; }
        };
    
        const size_t string::npos = -1;
    };
    

    3.3 string 类的迭代器
    int main()
    {
    	string s1("xxx");              // 3个有效字符
    	string s2("xxxxxxxxxxxxxxxx"); // 16个有效字符
    
    	cout << sizeof(s1) << endl; // 输出:?
    	cout << sizeof(s2) << endl; // 输出:?
    
    	return 0;
    }
    

    👉测试:

    // VS下string的实现,大概是这样样子
    class string
    {
    private:
        char _buf[16];
        char* _str;
        size_t _size;
        size_t _capacity;
    // ...
    };
    

    3.4 一些成员函数的实现 ① 默认成员函数

    👉 默认构造函数:

    👉 交换两个容器的内容(即一一交换两个对象的成员变量),为了方便实现现代写法:

    👉 拷贝构造函数:现代写法

    // 拷贝构造函数(深拷贝)
    // s2(s1)
    string(const string& s)
        :_str(nullptr) // 当前对象是一个正在构造的对象,成员变量还未初始化,是一个随机值,所以先置空
    	,_size(0)
    	,_capacity(0)
    {
    	string tmp(s._str); // 拿s的内容,调用构造函数构造临时对象tmp
    	this->swap(tmp);    // 将临时对象tmp和当前对象的成员变量分别进行交换
    }
    

    👉 赋值运算符重载函数:现代写法

    // 赋值运算符重载(深拷贝)
    // s1 = s2
    string& operator=(string s)
    {
        this->swap(s); // 将拷贝构造的对象s和当前对象的成员变量分别进行交换
        return *this;  // 返回当前对象
    }
    

    👉 析构函数:

  • 同一块空间会被析构(释放)多次

  • ② 访问 *** 作
  • 一个对象修改会影响另外一个对象
  • 运算符重载

    注意:

    • 写时拷贝技术:C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
    • 写时拷贝在读取时的缺陷:C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell

    ③ 容量 *** 作

    👉 reserve 函数:更改容量(capacity)的大小

    // 更改容量(capacity)的大小
    void reserve(size_t n)
    {
        if (n > _capacity)
        {
            // 开辟新空间
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);  // 旧空间数据拷贝到新空间
    
            // 释放旧空间,使用新空间
            delete[] _str;
            _str = tmp;    // 指向新空间
            _capacity = n; // 更新容量
        }
    }
    

    👉 resize 函数:调整字符串有效字符的长度

    // 调整字符串有效字符的长度
    void resize(size_t n, char ch = '\0')
    {
        // 要调整的有效字符的长度小于原有 _size 大小
        if (n < _size)
        {
            _size = n;          // 更新有效字符个数
            _str[_size] = '< n; i++)
            {
                _str[i] = ch;
            }
            _size = n;          // 更新有效字符个数
            _str[_size] = '
    '; // 补上字符串结束标志'
    ' } }
    '; // 补上字符串结束标志'

    ' } // 要调整的有效字符的长度大于原有 _size 大小 else if (n > _size) { // 要调整的有效字符的长度大于原有 _capacity 大小,先进行增容 if (n > _capacity) reserve(n); // 多出的位置用字符 ch(缺省值'\0')进行填充 for (size_t i = _size; i

     
     
    ④ 修改 *** 作 
    <= _size);
    
        // 先检查是否需要扩容
        if (_size == _capacity)
        {
            // 防止是空串"",容量为0,扩容失败
            size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
    
            reserve(newcapacity);
        }
    
        // 挪动字符
        for (size_t i = _size + 1; i >👉 insert 函数:在pos位置插入一个字符 
    

    // 在pos位置插入一个字符 string& insert(size_t pos, const char ch) { assert(pos >= 0 && pos

    pos; i--) // 注意 { _str[i] = _str[i - 1]; } _str[pos] = ch; // 插入字符 _size++; // 有效字符个数+1 return *this; }
     
    <= _size);
    
        // 先检查是否需要扩容
        size_t len = strlen(str);
        if (_size + len >insert 函数,在pos位置插入一个字符串,画图演示:< len; i++)
        {
            _str[pos++] = str[i];
        }
        _size += len; // 更新有效字符个数
    
        return *this;
    }
     
    

    // 在pos位置插入一个字符串 string& insert(size_t pos, const char* str) { assert(pos >= 0 && pos

    = _capacity) { reserve(_size + len); } // 挪动字符 for (size_t i = _size + len; i >= pos + len; i--) { _str[i] = _str[i - len]; } // 插入字符 for (size_t i = 0; i
     
    👉 push_back 函数:尾插一个字符

    void push_back(const char ch) { /* // 先检查是否需要扩容 if (_size >= _capacity) { // 防止是空串"",容量为0,扩容失败 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); // 扩2倍容 } _str[_size] = ch; // 尾插字符 _size++; // 有效字符个数+1 _str[_size] = '\0'; // 补上字符串结束标志'\0' */ /* 复用 insert 函数的代码 */ insert(_size, ch); }
     
    👉 append 函数:在当前字符串末尾追加一个字符串

    void append(const char* str) { /* // 先检查是否需要扩容 size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); // 扩容 } strcpy(_str + _size, str); // 尾插字符串(strcpy会拷贝'\0',并在该点停止) _size += len; // 有效字符个数+len */ /* 复用 insert 函数的代码 */ insert(_size, str); }

    👉 += 运算符重载函数:在当前字符串末尾追加一个字符 / 字符串

    直接复用前面的代码

  • C++面试中string类的一种正确写法 | 酷 壳 - CoolShell
    • 👉 erase 函数:删除从 pos 位置(包含 pos 位置)开始的 len 个字符
    • 情况1:

    • 如果 len 缺省,表示从 pos 位置开始,后面的字符删除完

    如果 pos + len >= _size,表示删除 [ pos, _size - 1 ] 区间的字符

     
    < _size);
    
        // 1. 从pos位置开始,后面的字符删除完,这是一个O(1)的操作
        if (len == npos || pos + len >情况2: 
    
  • (25条消息) STL 的string类怎么啦?_haoel的博客-CSDN博客

  • string& erase(size_t pos = 0, size_t len = npos) { assert(pos >= 0 && pos

    = _size) { _str[pos] = '\0'; _size = pos; } // 2. 从pos位置开始,后面的字符删除一部分,这是一个O(n)的 *** 作 else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; }

     
     
    ⑤ 字符串 *** 作 
    < _size);
    
        for (size_t i = pos; i < _size; i++)
        {
            if (_str[i] == ch) return i;
        }
        return npos;
    }
    👉 find 函数:从 pos 位置开始查找字符,若找到,返回该字符第一次出现的下标;若没找到,返回npos

    size_t find(char ch, size_t pos = 0) const { assert(pos >= 0 && pos
     
    < _size);
    
        // 在原串中去匹配子串str
        // 匹配成功,返回子串str首字符的地址
        // 匹配失败,返回空指针
        const char* p = strstr(_str + pos, str);
    
        if (p) return p - _str; // 通过子串str首字符的地址,计算出首字符的下标
        else return npos;
    }
    👉 find 函数:从pos位置开始查找子串,若找到,返回该子串第一个字符的下标;若没找到,返回npos

    << 运算符重载
    size_t find(const char* str, size_t pos = 0) const
    {
        assert(pos >= 0 && pos 

    3.5 一些非成员函数的实现 ① 流插入

     
     
    ② 流提取 >> 运算符重载 
    << s1 << endl;
    }
    👉先来看看这个写法,是哪里出了问题呢:

    // 流提取运算符重载 istream& operator>>(istream& in, string& s) { // 一个一个字符输入 // 遇到空格或者换行符终止输入 char ch; in >> ch; // 从流中获取一个字符 while (ch != ' ' && ch != '\n') { s += ch; // 把提取的字符追加到sting类对象 s 中去 in >> ch; // 继续从流中获取下一个字符 } return in; } void test7() { string s1; cin >> s1; // operator>>(cin, s1); cout

    上面代码测试发现,一直卡在这里没有反应:

    输入流中有 ‘1’、‘2’、‘3’、‘4’、‘\n’ 这些字符,

    用 in >> ch 从输入流中获取字符,是获取不到空格和换行的,

    因为 istream 对象 in 从输入流中获取字符时,会自动忽略掉输入流中的空格字符 和换行字符 ,它会认为这是你输入两个字符之间的间隔,所以需要借助 类中的一个成员函数:

     
    << s1 << endl;
    }
    👉流提取运算符重载函数中,改成用 get() 函数获取字符:但还是存在一个小问题

    // 流提取运算符重载 istream& operator>>(istream& in, string& s) { // 一个一个字符输入 // 遇到空格或者换行符终止输入 char ch; ch = in.get(); // 从流中获取一个字符 while (ch != ' ' && ch != '\n') { s += ch; // 把提取的字符追加到sting类对象 s 中去 ch = in.get(); // 继续从流中获取下一个字符 } return in; } void test7() { string s1; cin >> s1; // operator>>(cin, s1); cout

    运行结果:

    但还是存在一些问题,比如:

    流提取运算符,会对之前对象中的内容进行覆盖的,所以我们需要先清空字符串内容 
    👉正确写法:

    class string { private: // ... public: // ... void clear() // 清空所有有效字符 { _str[0] = '\0'; _size = 0; } // ... }; // 流提取运算符重载 istream& operator>>(istream& in, string& s) { // 先清空所有有效字符 s.clear(); // 一个一个字符输入 // 遇到空格或者换行符终止输入 char ch; ch = in.get(); // 从流中获取一个字符 while (ch != ' ' && ch != '\n') { s += ch; // 把提取的字符追加到sting类对象 s 中去 ch = in.get(); // 继续从流中获取下一个字符 } return in; }

     
    
    ③ getline 函数
    获取一行字符串,直到遇到换行符 ‘\n’

     
     
    ④ 关系运算符 > 重载 
    < s1.size() && j < s2.size(); i++, j++) // 同时遍历两个字符串
        {
            if (s1[i] != s2[j]) return s1[i] >关系运算符,进行大小比较< s2
        else if (j == s2.size()) return true;  // s2先被遍历完,说明 s1 > 
    < s1.size() && j < s2.size(); i++, j++) // 同时遍历两个字符串
        {
            if (s1[i] != s2[j]) return false;
        }
    
        if (i == s1.size() && j == s2.size()) return true; // 同时被遍历完,说明 s1 = s2
        else return false; // 有一个字符串没被遍历完
    }
    
    // 下面的关系运算符重载,全都可以复用上面的代码
    
    // s1 != s2
    bool operator!=(const string& s1, const string& s2)
    {
        return !(s1 == s2);
    }
    
    // s1 >< s2
    bool operator<(const string& s1, const string& s2)
    {
        return !(s1 >// 比较两个对象的大小(按字符ascii码比较)
    // s1 > s2
    bool operator>(const string& s1, const string& s2)
    {
        // 指针 i 和 j 分别指向两个字符串的第一个字符
        size_t i = 0, j = 0;
        for (; i <= s2
    bool operator<=(const string& s1, const string& s2)
    {
        return !(s1 > s2[j];
        }
    
        if (i == s1.size() && j == s2.size()) return false; // 同时被遍历完,说明 s1 = s2
        else if (i == s1.size()) return false; // s1先被遍历完,说明 s1  s2
    }
    
    // s1 == s2
    bool operator==(const string& s1, const string& s2)
    {
        // 指针 i 和 j 分别指向两个字符串的第一个字符
        size_t i = 0, j = 0;
        for (; i 
    = s2 bool operator>=(const string& s1, const string& s2) { return s1 > s2 || s1 == s2; } // s1
    = s2); } // s1

    s2); }

     
    
    3.6 补充:string 类对象的大小?

    👉下面代码的运行结果是什么呢?

    🔨 VS2019 下测试,输出 28,按照我们上面模拟实现的版本应该是 12,为什么会是 28 呢?

    这其实和 VS 下 PJ 版本的 STL string 的实现有关:< 16,不会去堆上开空间,而是存到一个名叫 _buf 的数组空间上,即存到对象中。

    如果有效字符个数

    如果有效字符个数 >= 16,则会存到 _str 指向的堆空间上。

    这样做可以减少内存碎片,提高效率。

    🔨 Linux 下(SGI 版本的 STL)测试,输出 8

    这个和 Linux 下 SGI 版本的 STL string 的实现有关。

    因为没有看过源码,这里猜测 string 类对象中应该是只存了一个指向字符数组的指针(指针的大小是 8 字节,Linux默认是编译成64位的可执行程序)

    那么对象的 _size 和 _capacity 是存在哪里的呢?猜测可能是这样存的,存在字符数组的前面12字节的空间中。


    结论:所以当我们计算 string 类对象大小时,不要觉得奇怪,因为各家实现的版本可能会有所差异。
      四、写时拷贝(了解)
    首先回顾一下浅拷贝引发的问题:

    1. 为了解决这两个问题:

    2. 为了应对同一块空间会被析构多次这个问题,提出了引用计数。
    3. 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

      为了应对一个对象修改会影响另外一个对象这个问题,提出了写时拷贝计数。

      写时拷贝就是一种拖延症,是在「浅拷贝」的基础之上增加了引用计数的方式来实现的。

    4. 多个对象共用同一块内存空间,哪个对象去写数据,哪个对象就再进行深拷贝,本质是一种延迟深拷贝。当然,如果不进行写数据,那就不用进行深拷贝,提高了效率。

    但这种方案也是有副作用的,现在基本上也被放弃了。

      推荐文章:

       
      👉我们来验证一下 STL string 是否用的是写时拷贝技术 
      
      #include

      #include

      int main() { std::string s1("hello world"); std::string s2(s1); // 拷贝构造 printf("%p\n", s1.c_str()); // c_str()函数返回其指向字符数组的地址 printf("%p\n", s2.c_str()); // 修改 s2[0] = 'x'; printf("%p\n", s1.c_str()); printf("%p\n", s2.c_str()); return 0; }

      运行结果(VS2019 下 PJ 版本的 STL):没有用写时拷贝技术,直接深拷贝。


      运行结果(Linux 下 SGI 版本的 STL):这个早期15年的版本用了写时拷贝技术,加上了引用计数。
      最新版本的 gcc 编译器
      五、拓展阅读

      欢迎分享,转载请注明来源:内存溢出

      原文地址:https://www.54852.com/langs/1295684.html

      (0)
      打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
      上一篇 2022-06-10
      下一篇2022-06-10

      发表评论

      登录后才能评论

      评论列表(0条)

        保存