STL讲解(二)—string类的模拟实现

文章目录

  • 一、前言
    • [1.1 string类各函数接口总览](#1.1 string类各函数接口总览)
    • [1.2 前情提要](#1.2 前情提要)
  • 二、模拟实现string类
    • [2.1 成员函数(Member functions)](#2.1 成员函数(Member functions))
      • [2.1.1 构造函数](#2.1.1 构造函数)
      • [2.1.2 拷贝构造函数](#2.1.2 拷贝构造函数)
      • [2.1.3 赋值运算符重载](#2.1.3 赋值运算符重载)
      • [2.1.4 析构函数](#2.1.4 析构函数)
    • [2.2 元素访问(Element access)](#2.2 元素访问(Element access))
      • [2.2.1 operator[ ]](#2.2.1 operator[ ])
      • [2.2.2 迭代器(Iterator)](#2.2.2 迭代器(Iterator))
    • [2.3 容量(Capacity)](#2.3 容量(Capacity))
      • [2.3.1 size](#2.3.1 size)
      • [2.3.2 capacity](#2.3.2 capacity)
      • [2.3.3 clear](#2.3.3 clear)
      • [2.3.4 empty](#2.3.4 empty)
      • [2.3.5 reserve](#2.3.5 reserve)
      • [2.3.6 resize](#2.3.6 resize)
    • [2.4 修改器(Modifiers)](#2.4 修改器(Modifiers))
      • [2.4.1 push_back](#2.4.1 push_back)
      • [2.4.2 append](#2.4.2 append)
      • [2.4.3 +=](#2.4.3 +=)
      • [2.4.4 insert](#2.4.4 insert)
      • [2.4.5 erase](#2.4.5 erase)
      • [2.4.6 swap](#2.4.6 swap)
    • [2.5 字符串操作(String Operations)](#2.5 字符串操作(String Operations))
      • [2.5.1 find函数](#2.5.1 find函数)
      • [2.5.2 rfind函数](#2.5.2 rfind函数)
      • [2.5.3 substr](#2.5.3 substr)
    • [2.6 非成员函数重载](#2.6 非成员函数重载)
      • [2.6.1 关系运算符(relational operators)](#2.6.1 关系运算符(relational operators))
      • [2.6.2 流插入 operator<<](#2.6.2 流插入 operator<<)
      • [2.6.3 流提取 operator>>](#2.6.3 流提取 operator>>)
      • [2.6.4 getline](#2.6.4 getline)
  • 三、string类的模拟实现整体代码
    • [3.1 string.h](#3.1 string.h)
    • [3.2 string.cpp](#3.2 string.cpp)
    • [3.3 test.cpp](#3.3 test.cpp)
  • 四、写时拷贝(了解)

一、前言

在经过漫长的类和对象与STL 学习之后,对于 STL中的 string类有了一个基本的认识,本模块呢,我将会带大家一起从 0~1去模拟实现一个STL库中的 string类,当然模拟实现的都是一些常用的接口,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类中的6大默认成员函数等

1.1 string类各函数接口总览

cpp 复制代码
class string
	{
	public:
		// ...

		typedef char* iterator;             // 迭代器某种意义上就是 指针
		typedef const char* const_iterator;


	//默认成员函数
		//string();   //无参构造函数
		//string(const char* str); //带参构造函数
		string(const char* str = "");//二合一构造函数,常量字符串默认末尾是有 ' \0 '

		const char* c_str() const;  // 添加c_str()函数声明
		
		string(const string& s); //拷贝构造函数
		void swap(string& s);    //交换函数
		string& operator=(const string& s);//赋值运算符重载
		~string();//析构函数

	//元素访问
		char& operator[](size_t pos);//下标访问-可读可写
		const char& operator[](size_t pos) const;//下标访问-可读不可写
		iterator begin();//迭代器的实现-可读可写
		iterator end();
		const_iterator begin() const;//迭代器的实现-可读不可写
		const_iterator end() const;
		


	//容量
		size_t size() const;//获取当前字符串的有效长度
		size_t capacity() const;//获取字符串当前的容量
		void clear();//清除当前对象的数据
		bool empty() const;//判断对象中是否有数据
		void reserve(size_t newCapacity);//扩容(修改_capacity)
		void resize(size_t newSize, char c = '\0');//改变大小

	//修改器
		void push_back(char ch);//追加一个字符
		void append(const char* s);//追加一个字符串
		string& operator+=(char ch);//+=一个字符
		string& operator+=(const char* s);//+=一个字符串
		void insert(size_t pos, size_t n, char ch);//在pos位置插入n个字符
		void erase(size_t pos, size_t len = npos);//从pos位置删除长度为len的字符串

	//字符串操作
		size_t find(char ch, size_t pos) const;//寻找一个字符
		size_t find(const char* s, size_t pos) const;//寻找一个字符串
		size_t rfind(char ch, size_t pos = npos)const;//反向寻找一个字符
		size_t rfind(const char* s, size_t pos = npos)const;//反向寻找一个字符串
		string substr(size_t pos, size_t len);//从字符串中提取子串

	//非成员函数重载
		bool operator<(const string& s)const;//函数重载<
		bool operator==(const string& s)const;//函数重载==
		bool operator<=(const string& s)const;//函数重载<=
		bool operator>(const string& s)const;//函数重载>
		bool operator>=(const string& s)const;//函数重载>=
		bool operator!=(const string& s)const;//函数重载!=

	private:
		char* _str;        //指向字符数组的指针
		size_t _size;      //字符数组的有效数据的长度
		size_t _capacity;  //字符串数组的容量
		

	public:
		//const static size_t npos = -1; // 虽然可以这样用,但是不建议

		//const static double x;
		// 类内声明
		const static size_t npos;//静态成员变量
	};
	void print_str(const string& s);// const对象的输出 ,打印这个字符串  --- 不能修改

	std::ostream& operator<<(std::ostream& out, const string& s);//流插入
	std::istream& operator>>(std::istream& in , string& s);//流提取
	std::istream& getline(std::istream& in, string& s);//读取一行含有空格的字符串

1.2 前情提要

首先第一点,为了不和库中的string类发生冲突,我们可以包上一个名称为xt_string的命名空间,此时因为作用域的不同,就不会产生冲突了

cpp 复制代码
#pragma once

#include<assert.h>

//为了不和std库中的string类发生冲突,创建我们自己的作用域xt_string
namespace xt_string
{
	class string
	{
	public:
		// ...
	private:
		char* _str;        //指向字符数组的指针
		size_t _size;      //字符数组的有效数据的长度
		size_t _capacity;  //字符串数组的容量
	};
}

接下来呢,就在string.cpp中进行定义,在test.cpp中进行测试即可,其中我们需要包含一下string.h头文件

二、模拟实现string类

2.1 成员函数(Member functions)

2.1.1 构造函数

1、无参构造函数

我们默认_size和_capacity的大小为0,然后给字符数组开一个大小的空间,并且将其初始化为\0

cpp 复制代码
//无参构造函数
//在 xt_string作用域中的string类 的string()函数
xt_string::string::string()
	:_str(new char[1])
	, _size(0)
	, _capacity(0)
{
	_str[0] = '\0';
}

然后,我们在test.cpp中测试一下,因为我们自己实现的string类是包含在了命名空间xt_string中的,所以我们在使用这个类的时候就要使用到域作用限定符::

cpp 复制代码
xt_string::string s1;
cpp 复制代码
#include<string>
using namespace std;

#include"string.h"

void test1()
{
	xt_string::string s1;
	cout << s1 << endl;
}

int main()
{
	test1();
	return 0;
}

开始运行,但是出现了错误

这个错误是因为我们xt_string::string 类没有定义 operator<<,所以无法用 cout 输出,对于输入输出流运算符,我们后面会讲到,目前,我们先用c_str()函数来代替输出

c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可

cpp 复制代码
//c_str函数实现
const char* xt_string::string::c_str() const  //内部不进行修改的文件,可以加上const防止权限放大
{
	return _str;
}

然后打印就可以看到正常运行,结果是一个空串

注意:

cpp 复制代码
cout << s1 << endl;  // ❌ 默认不能工作

这里不能工作是因为:s1 是 xt_string::string 类型,cout 不认识这个自定义类型,没有对应的 << 重载

cpp 复制代码
cout << s1.c_str() << endl;  // ✅ 可以工作

这里能工作是因为:c_str() 返回 const char*(C 风格字符串指针),cout 本身就有 operator<<(const char*) 的重载,所以 cout 知道怎么输出 const char*

2、带参构造函数

我们在初始化_size时先计算了字符串str的长度,因为_size取的是到\0为止的有效数据个数(不包含\0),那么" strlen "刚好可以起到这个功能

然后再_str这一部分,我们为其开辟的空间大小是容量的大小+1(+1的作用是为' \0 '开辟空间)

最后的话还要在把有效的数据拷贝到这快空间中,使用到的是" strcpy "

cpp 复制代码
//带参构造函数
xt_string::string::string(const char* str)
{
	_str = new char[strlen(str) + 1];  // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1
	_size = strlen(str);
	_capacity = strlen(str);
	strcpy(_str, str);
}

注意:为什么要new一块新空间

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;
namespace yzq
{
    class string
    {
    public:
    string()//无参构造函数
        //初始化列表
        :_str(nullptr)
        ,_size(0)
        ,_capaicty(0)
    {

    }
    string(const char*str)//带参构造函数
        :_str(str),
         _size(strlen(str)),
        _capaicty(strlen(str))
    {

    }
    private:
        char* _str;
        size_t _size;
        size_t _capaicty;

    };
    void test()
    {
        string s1;
        string s2("hello world");
    }
}

若写成两个构造函数,一个设置成无参,一个设置成带参,若调用如上的带参构造函数就会报错,将str传给_str,属于权限放大

为了解决这个问题,可以将_str改为const char*类型,但是无法修改_str所指向的内容,调用operator[]函数就会报错

因为后续要考虑扩容等问题,所以最好是new一块空间

而无参的构造函数为了保持析构都用delete[],所以使用new[]

3、二合一写法

上面两种写法太繁琐了,我们能不能把他们融合在一起呢?

构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括'\0')

不可以将缺省值设置成nullptr,strlen(str)对于str指针解引用,遇到'\0'终止,解引用NULL会报错

将缺省值设置成一个空字符串,结尾默认为'\0'

常量字符串默认末尾是有 ' \0 '

string.h

cpp 复制代码
//常量字符串默认末尾是有 ' \0 '
string(const char* str = "");//二合一构造函数
cpp 复制代码
//二合一构造函数
xt_string::string::string(const char* str)
{
	_size = strlen(str); //初始时,字符串大小设置为字符串长度
	_capacity = _size; //初始时,字符串容量设置为字符串长度
	_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
	strcpy(_str, str); //将C字符串拷贝到已开好的空间
}

升级版本:strcpy 改成 memcpy

strcpy 在复制时遇到 '\0' 就会立即停止,而 memcpy 则按照指定的字节数进行复制,不会被中间的 '\0' 打断。因此,如果字符串中间包含 '\0'(如 "hello\0world"),使用 strcpy 只能复制到 "hello" 就停止了,而 memcpy(_str, str, _size+1) 会完整复制指定长度的所有字节,包括中间的 '\0' 字符,从而保证数据的完整性

cpp 复制代码
xt_string::string::string(const char* str)
{
	_size = strlen(str); //初始时,字符串大小设置为字符串长度
	_capacity = _size; //初始时,字符串容量设置为字符串长度
	_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
	//strcpy(_str, str); //将C字符串拷贝到已开好的空间
	memcpy(_str, str, _size + 1);
}

补充说明:不过需要注意的是,这段代码中 _size = strlen(str) 本身也会在第一个 '\0' 处停止计算长度,所以如果真要处理含 '\0' 的字符串,还需要额外传入长度参数或使用其他方式获取真实长度。

2.1.2 拷贝构造函数

拷贝构造函数详解中若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数是浅拷贝,或者值拷贝

  • 浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响
  • 深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响

我们不希望拷贝出来的两个对象之间存在互相影响,因此,我们这里需要用到深拷贝

下面提供两种深拷贝的写法

1、传统写法

思想:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的

cpp 复制代码
//拷贝构造函数-传统写法
xt_string::string::string(const string& s)
{
	_str = new char[s._capacity + 1];
	//strcpy(_str, s._str);
	memcpy(_str, s._str, s._size + 1);
	_size = s._size;
	_capacity = s._capacity;
}

通过调试,我们观察发现,此时对象s1和对象s2中的数据存放在不同的空间中,此时去修改或者析构都不会受到影响

2、现代写法

首先,我们实现一下swap()函数,他是一个函数模板,可以根据模板参数的自动类型推导来交换不同类型的数据,我们直接调用库里的swap模板函数将对象的各个成员变量进行交换即可。

但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上"::"(作用域限定符),告诉编译器优先在全局范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(就近原则)

可以看到在我们自己实现的这个swap(string& s)函数中就去调用了std标准库中的函数然后交换一个string对象中的所有成员变量

cpp 复制代码
//交换函数
void xt_string::string:: swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);

}

重点:若想让编译器优先在全局范围寻找某函数,则需要在该函数前面加上"::"(作用域限定符)

接着我们实现拷贝构造函数

思想:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的

cpp 复制代码
//拷贝构造函数-现代写法
xt_string::string::string(const string& s)
	:_str(nullptr)
	, _size(0)
	,_capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}

注意:为什么现代写法需要初始化但是传统写法不需要呢?

现代写法的执行流程

cpp 复制代码
string(const string& s)
    :_str(nullptr)      // ① 先初始化为安全值
    , _size(0)
    , _capacity(0)
{
    string tmp(s._str); // ② 创建临时对象,tmp 有正确的数据
    swap(tmp);          // ③ 交换 this 和 tmp 的成员
}                       // ④ 函数结束,tmp 被析构!

如果不初始化的话

cpp 复制代码
string(const string& s)
    // 没有初始化列表,成员是垃圾值!
    // _str = 0xCCCCCCCC(随机野指针)
    // _size = 随机数
    // _capacity = 随机数
{
    string tmp(s._str);  
    swap(tmp);  // 交换后:
                // this->_str = 正确的数据
                // tmp._str = 0xCCCCCCCC(野指针!)
}   
// tmp 析构时调用 delete[] _str
// 对野指针 delete → 程序崩溃!💥

图示对比:

有初始化:

复制代码
交换前:
  this: _str=nullptr, _size=0        (安全值)
  tmp:  _str=有效指针, _size=正确值   (正确数据)

交换后:
  this: _str=有效指针, _size=正确值   ✅ 我们要的结果
  tmp:  _str=nullptr, _size=0        ✅ 析构时 delete nullptr 是安全的

没有初始化:

复制代码
交换前:
  this: _str=野指针, _size=垃圾值     (危险!)
  tmp:  _str=有效指针, _size=正确值

交换后:
  this: _str=有效指针, _size=正确值   ✅ 结果正确
  tmp:  _str=野指针, _size=垃圾值     💥 析构时崩溃!

传统写法为什么不需要

cpp 复制代码
string(const string& s)
{
    _str = new char[s._capacity + 1];  // 直接赋值,覆盖垃圾值
    strcpy(_str, s._str);
    _size = s._size;                    // 直接赋值
    _capacity = s._capacity;            // 直接赋值
}

传统写法直接给每个成员赋值,垃圾值被覆盖了,所有不会有问题

总结:

现代写法用 swap,交换后 tmp 会带着 this 的旧值去析构。如果旧值是垃圾(野指针),析构就会崩溃。所以必须初始化为安全值。

2.1.3 赋值运算符重载

赋值运算符重载也是属于类的默认成员函数,如果我们不自己写的话,类中也会默认生成一个

但是默认生成的话也会造成一个浅拷贝的问题,如下图,我们要执行s1=s3,此时若不去开辟一块新空间的话,那么s1和s3就会指向同一块空间,此时便造成了下面这些问题:

1.在修改其中任何一者时另一者都会发生变化

2.在析构的时候会造成二次析构的

3.原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题

那么此时我们应该自己去开出一块新的空间,将s3里的内容先拷贝到这块空间中来,然后释放掉s1所指向这块空间中的内容,然后再让s1指向这块新的空间

这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间

最后不要忘记去修改一下s1的【_size】和【_capacity】,因为大小和容量都发生了改变

1、传统写法

cpp 复制代码
//赋值运算符重载-传统写法
xt_string::string& xt_string::string::operator=(const string& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[]_str;

		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

2、现代写法

思想:在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题

透过上面这个图解应该对新的这种拷贝构造有了一定的理解:反正tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果

cpp 复制代码
//赋值运算符重载-现代写法
xt_string::string& xt_string::string::operator=(const string& s)
{
	if (this != &s)//防止自己给自己赋值
	{
		string tmp(s);//用s拷贝构造出对象tmp
		swap(tmp);//交换这两个对象
	}
	return *this;//返回左值(支持连续赋值)
}

**代码升级:传值传参**

cpp 复制代码
//赋值运算符重载-现代写法
xt_string::string& xt_string::string::operator=(string tmp)
{
	swap(tmp);//交换这两个对象
	return *this;//返回左值(支持连续赋值)
}
  1. 先回顾:两种传参方式的区别
cpp 复制代码
void func1(string s);      // 传值:s 是实参的【副本】
void func2(string& s);     // 传引用:s 是实参的【别名】
void func3(const string& s); // 传常引用:s 是实参的【只读别名】
  1. 传值传参会发生什么

当我们调用一个传值的函数时,编译器会自动调用拷贝构造函数来创建副本:

cpp 复制代码
void func(string s)  // 传值
{
    // s 是一个全新的对象,是实参的拷贝
}

int main()
{
    string s1("hello");
    func(s1);  // 这一步会调用拷贝构造函数,创建 s 作为 s1 的副本
}

执行流程:

复制代码
调用 func(s1)
    ↓
编译器自动执行:string s(s1);  ← 调用拷贝构造函数!
    ↓
进入函数体,s 是 s1 的独立副本
    ↓
函数结束,s 析构
  1. 应用到赋值运算符

对比两种写法:

cpp 复制代码
// 写法1:传引用(传统)
string& operator=(const string& s)
{
    if (this != &s)
    {
        string tmp(s);  // 【手动】调用拷贝构造
        swap(tmp);
    }
    return *this;
}

// 写法2:传值(现代)
string& operator=(string tmp)  // 【自动】调用拷贝构造!
{
    swap(tmp);
    return *this;
}
  1. 传值写法的详细执行过程

假设执行 s1 = s2;

复制代码
步骤1:调用 s1.operator=(s2)
        ↓
步骤2:因为参数是 string tmp(传值),
       编译器自动执行:string tmp(s2);  ← 拷贝构造!
       此时 tmp 是 s2 的深拷贝副本
        ↓
步骤3:进入函数体,执行 swap(tmp);
       交换 this(s1) 和 tmp 的内部指针
        ↓
步骤4:函数结束,tmp 析构
       tmp 现在持有的是 s1 的【旧数据】
       析构函数自动释放这些旧内存!

图解:

复制代码
调用前:
s1._str  ──►  "old data"     (s1的旧数据)
s2._str  ──►  "new data"     (s2的数据)

步骤2 - 拷贝构造 tmp:
s1._str  ──►  "old data"
s2._str  ──►  "new data"
tmp._str ──►  "new data"     (tmp是s2的深拷贝)

步骤3 - swap 交换:
s1._str  ──►  "new data"     (s1指向新数据!)
tmp._str ──►  "old data"     (tmp接管了旧数据)

步骤4 - tmp 析构:
s1._str  ──►  "new data"     (s1保留)
tmp._str ──►  "old data"  💥 被delete释放!
  1. 为什么不需要判断自赋值

传引用写法必须判断 if (this != &s),否则自赋值会出问题

但传值写法天然安全:

cpp 复制代码
s1 = s1;  // 自赋值

// 执行过程:
string tmp(s1);  // tmp 是 s1 的独立副本,有自己的内存
swap(tmp);       // 交换后,s1 得到 tmp 的副本数据
                 // tmp 得到 s1 的原数据
// tmp 析构,释放的是【原来 s1 的数据】
// 但 s1 现在指向的是【tmp 的副本数据】,没有被释放!

因为 tmp 是独立副本,所以交换后各自持有独立的内存,不会冲突

  1. 传值的妙处

传值传参的好处在于:当你把参数声明为 string tmp 而不是 const string& s 时,编译器会在调用函数的那一刻自动帮你调用拷贝构造函数 创建一个独立的副本,这样你就不用在函数体内手动写 string tmp(s) 了;同时,这个副本 tmp 是函数的局部变量 ,当函数结束时会自动析构 ,而此时 tmp 通过 swap 已经接管了原对象的旧数据,所以旧内存也被自动释放了------整个过程中,"拷贝"和"清理"这两件最容易出错的事都交给了编译器自动完成,你只需要写一行 swap(tmp) 就够了,代码既简洁又安全,还天然支持自赋值。

2.1.4 析构函数

cpp 复制代码
//析构函数
xt_string::string::~string()
{
	delete[] _str;//释放_str指向的空间
	_str = nullptr;//及时置空,防止非法访问
	_size = _capacity = 0;
}

2.2 元素访问(Element access)

我们把基本的成员函数讲解完了,string对象也被构造出来了,接下来访问一下对象里面的内容吧

2.2.1 operator[ ]

\]运算符的重载是为了让string对象能像C字符串一样,通过\[ \] +下标的方式获取字符串对应位置的字符。 在C字符串中我们通过\[ \] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现\[ \] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了 **1、可读可写** ```cpp //下标访问-可读可写 char& xt_string::string::operator[](size_t pos) { assert(pos < _size); return _str[pos]; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/086016eeb4a544c7801fdf263525da42.png) 这里的size()函数会在后面讲解 **2、可读不可写** ```cpp //下标访问-可读不可写 const char& xt_string::string::operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0ef598aa4b97411399924b080cbd1b78.png) 注意: 末尾 const:给函数"上锁"------允许在 const 对象上用(承诺不改对象) 返回 const:给结果"上锁"------不允许通过返回值去改内部字符 ```cpp #include using namespace std; struct Box { char data[3] = {'A','B','C'}; // ① 非 const 成员函数:返回 char&(可写) char& operator[](size_t i) { return data[i]; } // ② const 成员函数:返回 const char&(只读) const char& operator[](size_t i) const { return data[i]; } }; int main() { Box a; // 非 const 对象 const Box b; // const 对象 a[0] = 'X'; // ✅ OK:调用①,返回 char&,能写 cout << a[0] << endl; cout << b[0] << endl; // ✅ OK:只能调用②(因为 b 是 const) // b[0] = 'Y'; // ❌ 编译错误:②返回 const char&,不允许写 // 如果你把②的返回类型改成 char&(危险): // char& operator[](size_t) const; // 那么下面这句居然会变成"能编译",就等于改了 const 对象: // b[0] = 'Y'; // 这会破坏 const 语义 } ``` #### 2.2.2 迭代器(Iterator) 要遍历访问一个string对象的时候,除了下标的形式,我们还可以使用迭代器的形式去做一个遍历 string类中的迭代器实际上就是字符指针,只是给字符指针起了一个叫做iterator的别名罢了,注意,并不是所有的迭代器都是指针 对于迭代器,我们也要实现两种,一个是非const,一个是const 这里就实现一下最常用的begin和end,首位的话就是_str所指向的这个位置,而末位的话则是_str+_size所指向的这个位置 首先要在string.h 里面把 iterator 定义出来了 ```cpp typedef char* iterator; // 迭代器某种意义上就是 指针 typedef const char* const_iterator; ``` 注意:在头文件(比如自定义 string.h)里写 iterator begin(); 之前一定要在类内先把 iterator 定义清楚(如 using iterator = char\*; / typedef char\* iterator;),否则编译器找不到该类型时,会在 .cpp 的环境里继续"猜";如果你的 .cpp 在 #include "string.h" 之前写了 using namespace std;,头文件会被文本展开进来并受到它影响,导致 iterator 可能被误解析成 std::iterator(它是类模板,需要模板参数),从而出现"std::iterator 缺少模板参数"的报错;因此要么在类里明确定义迭代器类型,要么避免在全局随意 using namespace std;(尤其别放在 include 之前),并尽量别把自定义头文件命名为 string.h 以免与标准库混淆 **1、可读可写** ```cpp //迭代器的实现-可读写 xt_string::string::iterator xt_string::string::begin() { return _str; } xt_string::string::iterator xt_string::string::end() { return _str + _size; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/45a0f86eef1d444eba44b8a847a73b9c.png) **2、可读不可写** ```cpp //迭代器的实现-可读不可写 xt_string::string::const_iterator xt_string::string::begin() const { return _str; } xt_string::string::const_iterator xt_string::string::end() const { return _str + _size; } ``` 这里要实现一个const对象输出函数 ```cpp // 针对------const 对象的访问 // 打印这个字符串 --- 不能修改 void xt_string::print_str(const string& s) { for (size_t i = 0; i < s.size(); i++) { std::cout << s[i] << " "; } std::cout << std::endl; string::const_iterator it = s.begin(); while (it != s.end()) { //内容不能修改 std::cout << *it << " "; //指针可以修改 it++; } std::cout << std::endl; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9fea38323f244118a7ec6f0975a20dfd.png) **3、遍历字符串** 在明白了string类中迭代器的底层实现,再来看看我们用迭代器遍历string的代码,其实就是用指针在遍历字符串而已 ```cpp //初始化测试 xt_string::string s1("hello world"); cout << s1.c_str() << endl; //普通迭代器 xt_string::string::iterator it = s1.begin(); while(it != s1.end()) { std::cout << *it << " "; it++; } std::cout << std::endl; ``` 在[string的介绍](https://blog.csdn.net/roguexue/article/details/155579607?spm=1001.2014.3001.5501)中我们还说到,可以用范围for来遍历string,可能很多初学者都会觉得范围for是个很神奇的东西,只需要一点点代码就能实现string的遍历。 实际上范围for并不神奇,因为在代码编译的时候,编译器会自动将范围for替换为迭代器的形式,也就是说范围for是由迭代器支持的,现在我们已经实现了string类的迭代器,自然也能用范围for对string进行遍历: ```cpp //初始化测试 xt_string::string s1("hello world"); cout << s1.c_str() << endl; //范围for for (auto ch : s1) { std::cout << ch << " "; } std::cout << std::endl; ``` ### 2.3 容量(Capacity) #### 2.3.1 size size函数用于获取字符串当前的有效长度,不包括 ' \\0 ' 我们直接返回_size即可,因为不会去修改成员变量,所以我们可以加上一个const成员 ```cpp //获取当前字符串的有效长度 size_t xt_string::string::size() const { return _size; } ``` 注意:这里的const是用来修饰this指针的 #### 2.3.2 capacity capacity函数用于获取字符串当前的容量 ```cpp //获取字符串当前的容量 size_t xt_string::string::capacity() const { return _capacity; } ``` #### 2.3.3 clear clear()函数用于清除当前对象的数据 我们可以直接在_str\[0\]这个位置放上一个\\0,并且将_size设置为0即可 因为我们修改了其成员变量_size,所以这个接口我们不要去加const成员 ```cpp //清除当前对象的数据 void xt_string::string::clear() { _str[0] = '\0'; _size = 0; } ``` #### 2.3.4 empty empty()函数判断对象中有没有数据,那么使用0 == _size即可 ```cpp //判断对象中是否有数据 bool xt_string::string::empty() const { return 0 == _size; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b0cbbe7a8fc540a891b66bdfc3173186.png) #### 2.3.5 reserve **`改变容量,大小不变`** `有效字符的空间,实际开辟有效字符+1的空间` reserve扩容规则: 1. 当扩容n大于对象当前的capacity时,将capacity扩大到n或大于n 2. 当扩容n小于对象当前的capacity时,什么也不做 很明显,只有当这个新容量大于旧容量的时候,才会去选择去开空间,这里的扩容逻辑和我们在实现旧版本的拷贝构造函数时类似的:也是先开出一块新的空间(这里主要使用这个newCapacity 去开),然后再将原本的数据拷贝过来,释放旧空间的数据后让_str指向新空间即可。最后的话不要忘了去更新一下容量大小 ```cpp //扩容(修改_capacity) void xt_string::string::reserve(size_t newCapacity) { if (newCapacity > _capacity) { // 1.以给定的容量开出一块新空间 char* tmp = new char[newCapacity + 1];//多开一个空间用于存放'\0' // 2.将原本的数据先拷贝过来 strncpy(tmp, _str, _size + 1);//将对象原本的C字符串拷贝过来(包括'\0') // 3.释放旧空间的数据 delete[]_str; // 4.让_str指向新空间 _str = tmp; // 5.更新容量大小 _capacity = newCapacity; } } ``` 注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符'\\0'而无法拷贝(strcpy拷贝到第一个'\\0'就结束拷贝了)或者_str 因为某些操作没有正确以 '\\0' 结尾,就会一直读下去产生越界风险 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c54be41bac6c45fda8156b2f3530ecfd.png) 测试结果: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e6de3e1ca89e4681af5f7bac194eaaba.png) #### 2.3.6 resize **`大小改变`** resize改变大小规则: 1. 当n大于当前的size时,将size扩大到n,扩大的字符为c,若c未给出,则默认为'\\0' 2. 当n小于当前的size时,将size缩小到n 这里我们实现resize就需要分类讨论: 1. newSize \< _size ,要选择去删除数据 2. 如果newSize \> _size,但是 newSize \< _capacity ,此时要做的就是新增数据但是不去做扩容 3. 如果 newSize \> _size 并且 newSize \> _capacity,我们便要选择去进行扩容了 实现思路: 首先判断 newSize 是否大于_size,然后在内部做进一步判断,只有当newSize \> _capacity时,才去执行【reserve】的扩容逻辑 如果newSize并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数【memset】,我们从_str + _size 的位置开始填充,填充的个数是newSize - _size个,填充的内容是c 若是newSize \<= _size的话,我们所要做的就是去截取数据,到newSize为止直接设置一个 \\0,然后更新一下当前对象的_size大小 string.h ```cpp void resize(size_t newSize, char c = '\0');//改变大小 ``` string.cpp ```cpp //改变大小 void xt_string::string::resize(size_t newSize, char c) { // 1.当新的_size比旧的_size来得小的话,则进行删除数据 if (newSize > _size) { // 只有当新的size比容量还来的大,才去做一个扩容 if (newSize > _capacity) { reserve(newSize); } // 如果newSize <= _capacity,填充新数据即可 memset(_str + _size, c, newSize - _size); } // 2.如果 newSize <= _size,不考虑扩容和新增数据 _size = newSize; //size更新 _str[newSize] = '\0'; } ``` 测试结果: 1、首先是resize(8),可以看到这里发生了一个数据截断的情况,_size也相对应地发生了一个变化 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/949623f0c5834390a999bfb2596a174e.png) 2、接下去的话是resize(12),这并没有超过其容量值,但是却超出了_size 大小,所以我们要去做一个增加数据 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bfc3dd6a030545a8ad9900ec883e7331.png) 3、最后一个则是resize(20),此时的话就需要去走一个扩容逻辑了,并且在扩完容之后还要再进一步去填充数据 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/deef1cf34f0041dfaa2093fe15376e25.png) ### 2.4 修改器(Modifiers) #### 2.4.1 push_back 追加一个字符 当_size == _capacity的时候,要进行扩容,我们使用三目运算符进行扩容,若容量的大小为0,则默认开一个大小为4的空间,其他的情况则以2倍的形式进行扩充 扩容结束后,在末尾添加数据,因为_size指向的是\\0的位置,所以把字符放在该位置上,同时后移_size,添加'\\0',否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是'\\0' ```cpp //追加一个字符 void xt_string::string::push_back(char ch) { if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size] = ch; _size++; _str[_size] = '\0'; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/54ab75a5baa14e5a92500ff266c7fe74.png) #### 2.4.2 append append:追加一个字符串 首先,要计算字符串的长度 其次,判断在加上这个长度后是否需要做一个扩容 最后,使用【memcpy】通过字节的形式一一拷贝到_str + _size的位置(注意拷贝len + 1个,带上最后 \\0),再把大小_size给增加一下即可 ```cpp //追加一个字符串 void xt_string::string::append(const char* s) { int len = strlen(s);// 获取到待插入字符串的长度 // 若是加上len长度后超出容量大小了,那么就需要扩容 if (_size + len > _capacity) { reserve(_size + len); } // 将字符串拷贝到末尾的_size位置 memcpy(_str + _size, s, len + 1); // 大小增加 _size += len; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d0d95a0357e14ef58d6493c520533c25.png) 注意:在这段代码中选择 memcpy 而非 strcpy,核心原因是避免重复遍历字符串。由于前面 strlen(s) 已经遍历过一次字符串获取了长度 len,此时用 memcpy 可以直接按已知的字节数(len + 1)批量复制;而 strcpy 会再次逐字符遍历直到遇到 '\\0',相当于做了重复工作。因此,在长度已知的情况下,memcpy 效率更高。 #### 2.4.3 += **1、+= 一个字符** 这里我们直接复用前面的push_back()接口即可,因为【+=】改变的是自身,所以我们return \*this,如果返回一个出了作用域不会销毁的对象,可以采取引用返回减少拷贝 ```cpp //+=一个字符 xt_string::string& xt_string::string::operator+=(char ch) { push_back(ch); return *this; } ``` **2、+= 一个字符串** 对于【+=】一个字符串,我们则是去复用前面的append()即可 ```cpp //+=一个字符串 xt_string::string& xt_string::string::operator+=(const char* s) { append(s); return *this; } ``` 测试结果: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c48ff3c3c69b49d590492f488b0979b5.png) #### 2.4.4 insert 从pos位置开始插入n个字符 在实现insert函数之前,我们要先声明并初始化一个静态的成员变量npos,它是最大的无符号整数值 对于静态成员变量,我们需要在**类内声明并且在类外进行初始化**,静态成员变量是类共享的、独立于对象存在的,需要在某个源文件中单独分配一块内存。如果在类内(头文件)初始化,会导致多个源文件重复定义。因此 C++ 规定:类内声明、类外定义。 ```cpp // 类内声明 const static size_t npos;//静态成员变量 ``` ```cpp // 类外初始化 const size_t xt_string::string::npos = -1; ``` insert实现思路: 首先是要在pos位置插入n个字符,所以我们要判断这个pos位置是否合理 其次,考虑扩容问题,如果_size + n之后的大小大于_capacity的话那就要调用【reserve】接口去实现一个扩容的逻辑了 然后就是并不是直接去插入数据,而是要先给需要插入的n个字符腾出位置。从_size位置开始,让字符以n个单位地从后往前的向后挪动,若是从前往后则会造成覆盖的问题 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bd112ff11c4d4383af8227571a61f8ec.png) ```cpp // 挪动数据 size_t end = _size; while (end >= pos) { _str[end + n] = _str[end]; --end; } ``` 最后,当这个挪动的逻辑结束后,我们就可以从pos位置插入n个字符了,然后更新一下_size的大小 ```cpp //在pos位置插入n个字符 void xt_string::string::insert(size_t pos, size_t n, char ch) { assert(pos <= _size); if (_size + n > _capacity) { reserve(_size + n); } size_t end = _size; while (end >= pos) { _str[end + n] = _str[end]; end--; } //插入n个字符 for (size_t i = 0; i < n; i++) { _str[pos] = ch; pos++; } _size += n; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/26913bdbb5c146cfb1b22c4655f4bbba.png) 上面的代码看着很完美,但是,我们在这里要考虑一种极端的情况,**如果这个pos == 0** 的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是当这个end超出pos的范围时,也就减到了-1,但是这个end的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1652f875d4634c4aafba1d4c8ab24f9d.png) 当这个end在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos的值了,此时就会造成一个死循环,导致程序崩溃 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1a3fe1e12db6483f8f147fc128d9782b.png) 所以,我们应该将无符号类型改为有符号类型,也就是size_t ---\> int ```cpp //在pos位置插入n个字符 void xt_string::string::insert(size_t pos, size_t n, char ch) { assert(pos <= _size); if (_size + n > _capacity) { reserve(_size + n); } int end = _size; while (end >= (int)pos) { _str[end + n] = _str[end]; end--; } //插入n个字符 for (size_t i = 0; i < n; i++) { _str[pos] = ch; pos++; } _size += n; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2804809635294f0e9dedcb7c676a44ba.png) 注意:虽然 end 是 int,但比较时会被隐式转换成 size_t,导致负数变成超大正数。**把 pos 也转成 int,才能保证比较按有符号数的规则进行** 小贴士:**隐式类型转换** 是指编译器在不同类型的数据进行运算或比较时,**自动将一种类型转换为另一种类型** 的过程,无需程序员手动干预。转换遵循一定的优先级规则:首先,**小类型向大类型转换** (如 `char` → `int` → `long` → `double`),以避免数据丢失;其次,**有符号向无符号转换** ,当 `int` 和 `unsigned int`(或 `size_t`)一起运算时,`int` 会被转成无符号类型,这就是上面例子中 `-1` 变成超大正数的原因。这种转换虽然方便,但也容易引发隐蔽的 bug,特别是有符号数和无符号数混用时,负数会被错误地解释为很大的正数,导致逻辑错误。因此在涉及不同类型比较或运算时,建议使用**显式类型转换** (如 `(int)pos`)来明确告诉编译器你想要的转换方式,避免意外行为 #### 2.4.5 erase 删除从pos位置开始的len个有效长度字符 erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况: **1、pos位置及其之后的有效字符都需要被删除** 这时我们只需在pos位置放上'\\0',然后将对象的size更新即可 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fde55823904b4a8b81fc5e3bfdcf6a39.gif) **2、pos位置及其之后的有效字符只需删除一部分** 这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加'\\0',因为在此之前字符串末尾就有'\\0'了 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b48e88ea9ef64dbcbfc7306cc5612159.gif) string.h ```cpp void erase(size_t pos, size_t len = npos);//从pos位置删除长度为len的字符串 ``` ```cpp //从pos位置删除长度为len的字符串 void xt_string::string::erase(size_t pos, size_t len) { assert(pos < _size); if (len == npos || len + pos > _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e45eacd1e16d4db19cc56d55db22b6cf.png) #### 2.4.6 swap 对于swap函数,我们在上面已经讲解过了,这里就不再赘述 ```cpp //交换函数 void xt_string::string:: swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } ``` ### 2.5 字符串操作(String Operations) #### 2.5.1 find函数 **1、寻找一个字符** 这个很简单,就是去遍历一下当前对象中的_str,若是在遍历的过程中发现了字符ch的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos这个最大的无符号数 ```cpp //寻找一个字符 size_t xt_string::string::find(char ch, size_t pos) const { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } ``` **2、寻找一个字符串** 直接使用的是C语言中的库函数 strstr函数,这个的话我们在字符串函数与内存函数解读的时候有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针 如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d5bc81e19193478badbab72144137233.png) #### 2.5.2 rfind函数 实现rfind函数时,我们可以考虑复用已经写好了的两个find函数,但rfind函数是从后往前找,所以我们需要将对象的C字符串逆置一下,若是查找字符串,还需将待查找的字符串逆置一下,然后调用find函数进行查找,但注意传入find函数的pos不需要对称,但是从find函数接收到的pos需要镜像对称一下 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4939d19d7c954128be0ce9f1f2ddd88d.png) **1、反向寻找一个字符** 首先我们需要用对象拷贝构造一个临时对象tmp,因为我们并不希望调用rfind函数后对象的C字符串就被逆置了。我们将tmp对象的C字符串逆置,然后将所给pos镜像对称一下再调用find函数,再将从find函数接收到的返回值镜像对称一下作为rfind函数的返回值返回即可 ```cpp //反向寻找一个字符 size_t xt_string::string::rfind(char ch, size_t pos)const { string tmp(*this);//拷贝构造对象tmp reverse(tmp.begin(), tmp.end());//调用reverse逆置对象tmp的c字符串 //所给pos大于字符串有效长度 if (pos >= _size) { pos = _size - 1;//重新设置pos为字符串最后一个字符的下标 } size_t ret = tmp.find(ch, pos);//复用find函数 if (ret != npos) { return _size - 1 - ret;//找到了。返回ret镜像对称后的位置 } else { return npos;//没找到,返回npos } } ``` 注:rfind函数规定,当所给的pos大于等于字符串的有效长度时,看作所给pos为字符串最后一个字符的下标 **2、反向寻找一个字符串** 首先我们还是需要用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,同时我们还需要拷贝一份待查找的字符串,也将其逆置。 注意:此时我们将从find函数接收到的值镜面对称后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置,而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置,所以还需做进一步调整后才能作为rfind函数的返回值返回 ```cpp //反向寻找一个字符串 size_t xt_string::string::rfind(const char* s, size_t pos)const { string tmp(*this);//拷贝构造对象tmp reverse(tmp.begin(), tmp.end());//调用reverse逆置对象tmp的c字符串 size_t len = strlen(s);//待查找的字符串的长度 char* arr = new char[len + 1];//开辟arr字符串(用于拷贝str字符串) strcpy(arr, s);//拷贝str给arr size_t left = 0, right = len - 1;//设置左右指针 //逆置字符串arr while (left < right) { std::swap(arr[left], arr[right]); left++; right--; } //所给pos大于字符串的有效长度 if (pos >= _size) { pos = _size - 1;//重新设置pos为字符串最后一个字符的下标 } size_t ret = tmp.find(arr, pos); //复用find函数 delete[] arr; //销毁arr指向的空间,避免内存泄漏 if (ret != npos) return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置 else return npos; //没找到,返回npos } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0cee459ed1094e5fb1604d9c4175ee99.png) #### 2.5.3 substr substr:从字符串中提取子串 首先,如果我们从pos位置开始所要取的子串长度大于剩余的串长,那最多能取到的有效范围也就是从pos位置开始的到末尾的_size结束这段距离,所以当这个所取长度过长的话,我们就要考虑去更新一下取子串长度的有效范围,可以看到,以n作为可取的子串长度,一开始让其等于传入进来的len长,如果长度超出了有效范围后,我们便去更新这个n = _size - pos ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4817c749bb1f4783b40b2363b627837e.png) 然后我们就可以去取这个子串了,使用循环的方式从pos位置开始取,取【n】个即可,然后追加到临时的string对象中去,最后将其返回即可,因为我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】 ```cpp //从字符串中提取子串 xt_string::string xt_string::string::substr(size_t pos, size_t len) { assert(pos < _size); size_t n = len; if (pos + n > _size || len == npos) { n = _size - pos; } string tmp; tmp.reserve(n); for (size_t i = pos; i < pos+n; i++) { tmp += _str[i]; } return tmp; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c594e9b04ec2412db3c7c4405726b9bf.png) ### 2.6 非成员函数重载 #### 2.6.1 关系运算符(relational operators) 关系运算符有 \>、\>=、\<、\<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现 **1、\<** ```cpp //函数重载< bool xt_string::string::operator<(const string& s)const { return strcmp(_str, s._str) < 0; } ``` 如果遇到包含 ' \\0 ' 的字符串,则无法处理,故我们看下面的代码 ```cpp bool xt_string::string::operator<(const string& s) const { int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size); // "hello" "hello" false // "helloxx" "hello" false // "hello" "helloxx" true return ret == 0 ? _size < s._size : ret < 0; } ``` 总结:strcmp 把 \\0 当作字符串结束符,适合处理普通文本;memcmp 按实际长度比较所有字节,适合处理可能包含 \\0 的二进制数据。如果你的 string 类只存普通文本,两者效果一样;如果要支持二进制数据,必须用 memcmp **2、=** ```cpp //函数重载== bool xt_string::string::operator==(const string& s)const { //return strcmp(_str, s._str) == 0; return _size == s._size && memcmp(_str, s._str, _size) == 0; } ``` **3、\<=** ```cpp //函数重载<= bool xt_string::string::operator<=(const string& s)const { return *this < s || *this == s; } ``` **4、\>** ```cpp //函数重载> bool xt_string::string::operator>(const string& s)const { return !(*this <= s); } ``` **5、\>=** ```cpp //函数重载>= bool xt_string::string::operator>=(const string& s)const { return !(*this < s); } ``` **6、!=** ```cpp //函数重载!= bool xt_string::string::operator!=(const string& s)const { return !(*this == s); } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/cb54e0b30c9e41a69cf926dc989576d8.png) #### 2.6.2 流插入 operator\<\< 我们通过[类和对象](https://blog.csdn.net/roguexue/article/details/155105676?spm=1001.2014.3001.5502)的学习,可以知道,为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到【友元】这个东西,不过我们不建议使用这个,会破坏类的封装性 string.h ```cpp std::ostream& operator<<(std::ostream& out, const string& s);//流插入 ``` ```cpp //流插入 ostream& xt_string::operator<<(ostream& out, const string& s) { for (size_t i = 0; i < s.size(); i++) { out << s[i]; } return out; } //<<运算符的重载 ostream& operator<<(ostream& out, const string& s) { //使用范围for遍历字符串并输出 for (auto e : s) { cout << e; } return out; //支持连续输出 } ``` `ostream` 是标准库中的输出流类,`cout` 是这个类的一个对象。当我们写 `cout << s` 时,实际上调用的是 `operator<<(cout, s)`,此时参数 `out` 就接收了 `cout` 的引用,所以在函数里对 `out` 的操作就是对 `cout` 的操作,最后返回 `out` 是为了支持 `cout << a << b` 这样的链式调用 注意:对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个防拷贝的效果,即在后面加上一个= delete ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7bf18d04a5424e1e89f644689d67c52d.png) 下面我们来分析一下cout \<\< s.c_str() 和 cout \<\< s 的区别 * c的字符数组, 以\\0为终止算长度 ,打印的是内置类型也就是const char\*,打印的原则是遇到了 ' \\0 '就终止 * string不看\\0, 以size为终止算长度 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/584d08e12378416d9d37539d19bb1ecd.png) #### 2.6.3 流提取 operator\>\> 重载\>\>运算符是为了让string对象能够像内置类型一样使用\>\>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到' '或是'\\n'便停止读取 string.h ```cpp std::istream& operator>>(std::istream& in , string& s);//流提取 ``` ```cpp //流提取 istream& xt_string::operator>>(istream& in, string& s) { s.clear(); //清空字符串 char ch = in.get(); //读取一个字符 while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取 { s += ch; //将读取到的字符尾插到字符串后面 ch = in.get(); //继续读取字符 } return in; //支持连续输入 } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3e424f8a5c3e4a339248bfbb3ce0a657.png) 注意:我们这里不能使用 " in\>\>ch " 进行读取,因为他读不到空格和\\0,相当于我们在屏幕上输入的时候,会用空格代替对字符的分割,他这里就相当于读不到那个空格 ```cpp char ch; in >> ch; // 自动跳过空白字符 ``` **特点**: * **自动跳过** 所有前导空白(空格、`\n`、`\t` 等) * 只读取第一个"有效"字符 * 把空白当作**分隔符**,而非数据 **示例**: 输入缓冲区: " A B" ^^^ 这些空格会被自动跳过 in >> ch; // ch = 'A',空格被忽略了 ```cpp char ch; ch = in.get(); // 读取任何字符,包括空白 ``` **特点**: * **原样读取**下一个字符,不做任何跳过 * 空格就是空格,换行就是换行 * 是"原始"的字符读取方式 **示例**: 输入缓冲区: " A B" ch = in.get(); // ch = ' '(第一个空格) ch = in.get(); // ch = ' '(第二个空格) ch = in.get(); // ch = ' '(第三个空格) ch = in.get(); // ch = 'A' **进阶版** 不是逐字符 s += ch(效率低,频繁内存分配),而是先攒满一个缓冲区,再批量追加(减少内存操作次数) ```cpp istream& xt_string::operator>>(istream& in, string& s) { //模拟标准 >> 的行为------跳过输入开头的所有空格和换行符 s.clear(); // 清空目标字符串 char ch = in.get(); // 读取第一个字符 while (ch == ' ' || ch == '\n') // 如果是空格或换行 { ch = in.get(); // 继续读下一个字符 } //使用缓冲区读取有效字符 char buff[128]; // 128字节的临时缓冲区 int i = 0; // 缓冲区索引 while (ch != ' ' && ch != '\n') // 遇到空格或换行就停止 { buff[i++] = ch; // 把字符存入缓冲区 if (i == 127) // 缓冲区快满时(留1位给'\0') { buff[i] = '\0'; // 添加字符串结束符 s += buff; // 追加到结果字符串 i = 0; // 重置索引,继续使用缓冲区 } ch = in.get(); // 读取下一个字符 } //处理缓冲区剩余字符 if (i != 0) // 如果缓冲区还有未处理的字符 { buff[i] = '\0'; // 添加结束符 s += buff; // 追加到结果字符串 } return in; // 返回流引用 } ``` `istream` 是标准库中的输入流类,`cin` 是这个类的一个对象。当我们写 `cin >> s` 时,实际上调用的是 `operator>>(cin, s)`,此时参数 `in` 就接收了 `cin` 的引用,所以在函数里对 `in` 调用 `in.get()` 就是从键盘读取字符,最后返回 `in` 是为了支持 `cin >> a >> b` 这样的链式调用 #### 2.6.4 getline getline函数用于读取一行含有空格的字符串。实现时与\>\>运算符的重载基本相同,只是当读取到'\\n'的时候才停止读取字符 string.h ```cpp std::istream& getline(std::istream& in, string& s);//读取一行含有空格的字符串 ``` ```cpp //读取一行含有空格的字符串 istream& xt_string::getline(istream& in, string& s) { s.clear(); //清空字符串 char ch = in.get(); //读取一个字符 while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取 { s += ch; //将读取到的字符尾插到字符串后面 ch = in.get(); //继续读取字符 } return in; } ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/39ed2f0fcf944d54973f3014ec15bc5a.png) ## 三、string类的模拟实现整体代码 ### 3.1 string.h ```cpp #pragma once #include //为了不和std库中的string类发生冲突,创建我们自己的作用域xt_string namespace xt_string { class string { public: // ... typedef char* iterator; // 迭代器某种意义上就是 指针 typedef const char* const_iterator; //默认成员函数 //string(); //无参构造函数 //string(const char* str); //带参构造函数 string(const char* str = "");//二合一构造函数,常量字符串默认末尾是有 ' \0 ' const char* c_str() const; // 添加c_str()函数声明 string(const string& s); //拷贝构造函数 void swap(string& s); //交换函数 string& operator=(const string& s);//赋值运算符重载 ~string();//析构函数 //元素访问 char& operator[](size_t pos);//下标访问-可读可写 const char& operator[](size_t pos) const;//下标访问-可读不可写 iterator begin();//迭代器的实现-可读可写 iterator end(); const_iterator begin() const;//迭代器的实现-可读不可写 const_iterator end() const; //容量 size_t size() const;//获取当前字符串的有效长度 size_t capacity() const;//获取字符串当前的容量 void clear();//清除当前对象的数据 bool empty() const;//判断对象中是否有数据 void reserve(size_t newCapacity);//扩容(修改_capacity) void resize(size_t newSize, char c = '\0');//改变大小 //修改器 void push_back(char ch);//追加一个字符 void append(const char* s);//追加一个字符串 string& operator+=(char ch);//+=一个字符 string& operator+=(const char* s);//+=一个字符串 void insert(size_t pos, size_t n, char ch);//在pos位置插入n个字符 void erase(size_t pos, size_t len = npos);//从pos位置删除长度为len的字符串 //字符串操作 size_t find(char ch, size_t pos) const;//寻找一个字符 size_t find(const char* s, size_t pos) const;//寻找一个字符串 size_t rfind(char ch, size_t pos = npos)const;//反向寻找一个字符 size_t rfind(const char* s, size_t pos = npos)const;//反向寻找一个字符串 string substr(size_t pos, size_t len);//从字符串中提取子串 //非成员函数重载 bool operator<(const string& s)const;//函数重载< bool operator==(const string& s)const;//函数重载== bool operator<=(const string& s)const;//函数重载<= bool operator>(const string& s)const;//函数重载> bool operator>=(const string& s)const;//函数重载>= bool operator!=(const string& s)const;//函数重载!= private: char* _str; //指向字符数组的指针 size_t _size; //字符数组的有效数据的长度 size_t _capacity; //字符串数组的容量 public: //const static size_t npos = -1; // 虽然可以这样用,但是不建议 //const static double x; // 类内声明 const static size_t npos;//静态成员变量 }; void print_str(const string& s);// const对象的输出 ,打印这个字符串 --- 不能修改 std::ostream& operator<<(std::ostream& out, const string& s);//流插入 std::istream& operator>>(std::istream& in , string& s);//流提取 std::istream& getline(std::istream& in, string& s);//读取一行含有空格的字符串 } ``` ### 3.2 string.cpp ```cpp #define _CRT_SECURE_NO_WARNINGS 1 #include #include using namespace std; #include // 用了 strlen、strcpy #include"string.h" ////无参构造函数 ////在 xt_string作用域中的string类 的string()函数 //xt_string::string::string() // :_str(new char[1]) // , _size(0) // , _capacity(0) //{ // _str[0] = '\0'; //} //c_str函数实现 const char* xt_string::string::c_str() const //内部不进行修改的文件,可以加上const防止权限放大 { return _str; } ////带参构造函数 //xt_string::string::string(const char* str) //{ // _str = new char[strlen(str) + 1]; // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1 // _size = strlen(str); // _capacity = strlen(str); // strcpy(_str, str); //} //二合一构造函数 //xt_string::string::string(const char* str) //{ // _size = strlen(str); //初始时,字符串大小设置为字符串长度 // _capacity = _size; //初始时,字符串容量设置为字符串长度 // _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') // strcpy(_str, str); //将C字符串拷贝到已开好的空间 //} xt_string::string::string(const char* str) { _size = strlen(str); //初始时,字符串大小设置为字符串长度 _capacity = _size; //初始时,字符串容量设置为字符串长度 _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') //strcpy(_str, str); //将C字符串拷贝到已开好的空间 memcpy(_str, str, _size + 1); } ////拷贝构造函数-传统写法 //xt_string::string::string(const string& s) //{ // _str = new char[s._capacity + 1]; // strcpy(_str, s._str); // _size = s._size; // _capacity = s._capacity; //} //交换函数 void xt_string::string:: swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //拷贝构造函数-现代写法 xt_string::string::string(const string& s) :_str(nullptr) , _size(0) ,_capacity(0) { string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象 swap(tmp); //交换这两个对象 } ////赋值运算符重载-传统写法 //xt_string::string& xt_string::string::operator=(const string& s) //{ // if (this != &s) // { // char* tmp = new char[s._capacity + 1]; // strcpy(tmp, s._str); // delete[]_str; // // _str = tmp; // _size = s._size; // _capacity = s._capacity; // } // return *this; //} //赋值运算符重载-现代写法 xt_string::string& xt_string::string::operator=(const string& s) { if (this != &s)//防止自己给自己赋值 { string tmp(s);//用s拷贝构造出对象tmp swap(tmp);//交换这两个对象 } return *this;//返回左值(支持连续赋值) } //析构函数 xt_string::string::~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; } //下标访问-可读可写 char& xt_string::string::operator[](size_t pos) { assert(pos < _size); return _str[pos]; } //下标访问-可读不可写 const char& xt_string::string::operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } //获取当前字符串的有效长度 size_t xt_string::string::size() const { return _size; } //迭代器的实现-可读写 xt_string::string::iterator xt_string::string::begin() { return _str; } xt_string::string::iterator xt_string::string::end() { return _str + _size; } //迭代器的实现-可读写 xt_string::string::const_iterator xt_string::string::begin() const { return _str; } xt_string::string::const_iterator xt_string::string::end() const { return _str + _size; } // 针对------const 对象的访问 // 打印这个字符串 --- 不能修改 void xt_string::print_str(const string& s) { for (size_t i = 0; i < s.size(); i++) { std::cout << s[i] << " "; } std::cout << std::endl; string::const_iterator it = s.begin(); while (it != s.end()) { //内容不能修改 std::cout << *it << " "; //指针可以修改 it++; } std::cout << std::endl; } //获取字符串当前的容量 size_t xt_string::string::capacity() const { return _capacity; } //清除当前对象的数据 void xt_string::string::clear() { _str[0] = '\0'; _size = 0; } //判断对象中是否有数据 bool xt_string::string::empty() const { return 0 == _size; } //扩容(修改_capacity) void xt_string::string::reserve(size_t newCapacity) { if (newCapacity > _capacity) { // 1.以给定的容量开出一块新空间 char* tmp = new char[newCapacity + 1];//多开一个空间用于存放'\0' // 2.将原本的数据先拷贝过来 strncpy(tmp, _str, _size + 1);//将对象原本的C字符串拷贝过来(包括'\0') // 3.释放旧空间的数据 delete[]_str; // 4.让_str指向新空间 _str = tmp; // 5.更新容量大小 _capacity = newCapacity; } } //改变大小 void xt_string::string::resize(size_t newSize, char c) { // 1.当新的_size比旧的_size来得小的话,则进行删除数据 if (newSize > _size) { // 只有当新的size比容量还来的大,才去做一个扩容 if (newSize > _capacity) { reserve(newSize); } // 如果newSize <= _capacity,填充新数据即可 memset(_str + _size, c, newSize - _size); } // 2.如果 newSize <= _size,不考虑扩容和新增数据 _size = newSize; //size更新 _str[newSize] = '\0'; } //追加一个字符 void xt_string::string::push_back(char ch) { if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size] = ch; _size++; _str[_size] = '\0'; } //追加一个字符串 void xt_string::string::append(const char* s) { int len = strlen(s);// 获取到待插入字符串的长度 // 若是加上len长度后超出容量大小了,那么就需要扩容 if (_size + len > _capacity) { reserve(_size + len); } // 将字符串拷贝到末尾的_size位置 memcpy(_str + _size, s, len + 1); // 大小增加 _size += len; } //+=一个字符 xt_string::string& xt_string::string::operator+=(char ch) { push_back(ch); return *this; } //+=一个字符串 xt_string::string& xt_string::string::operator+=(const char* s) { append(s); return *this; } // 类外初始化 const size_t xt_string::string::npos = -1; //在pos位置插入n个字符 void xt_string::string::insert(size_t pos, size_t n, char ch) { assert(pos <= _size); if (_size + n > _capacity) { reserve(_size + n); } int end = _size; while (end >= (int)pos) { _str[end + n] = _str[end]; end--; } //插入n个字符 for (size_t i = 0; i < n; i++) { _str[pos] = ch; pos++; } _size += n; } //从pos位置删除长度为len的字符串 void xt_string::string::erase(size_t pos, size_t len) { assert(pos < _size); if (len == npos || len + pos > _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } //寻找一个字符 size_t xt_string::string::find(char ch, size_t pos) const { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } //寻找一个字符串 size_t xt_string::string::find(const char* s, size_t pos) const { assert(pos < _size); const char* ptr = strstr(_str+pos, s); if (ptr) { return ptr - _str; } return npos; } //反向寻找一个字符 size_t xt_string::string::rfind(char ch, size_t pos)const { string tmp(*this);//拷贝构造对象tmp reverse(tmp.begin(), tmp.end());//调用reverse逆置对象tmp的c字符串 //所给pos大于字符串有效长度 if (pos >= _size) { pos = _size - 1;//重新设置pos为字符串最后一个字符的下标 } size_t ret = tmp.find(ch, pos);//复用find函数 if (ret != npos) { return _size - 1 - ret;//找到了。返回ret镜像对称后的位置 } else { return npos;//没找到,返回npos } } //反向寻找一个字符串 size_t xt_string::string::rfind(const char* s, size_t pos)const { string tmp(*this);//拷贝构造对象tmp reverse(tmp.begin(), tmp.end());//调用reverse逆置对象tmp的c字符串 size_t len = strlen(s);//待查找的字符串的长度 char* arr = new char[len + 1];//开辟arr字符串(用于拷贝str字符串) strcpy(arr, s);//拷贝str给arr size_t left = 0, right = len - 1;//设置左右指针 //逆置字符串arr while (left < right) { std::swap(arr[left], arr[right]); left++; right--; } //所给pos大于字符串的有效长度 if (pos >= _size) { pos = _size - 1;//重新设置pos为字符串最后一个字符的下标 } size_t ret = tmp.find(arr, pos); //复用find函数 delete[] arr; //销毁arr指向的空间,避免内存泄漏 if (ret != npos) return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置 else return npos; //没找到,返回npos } //从字符串中提取子串 xt_string::string xt_string::string::substr(size_t pos, size_t len) { assert(pos < _size); size_t n = len; if (pos + n > _size || len == npos) { n = _size - pos; } string tmp; tmp.reserve(n); for (size_t i = pos; i < pos+n; i++) { tmp += _str[i]; } return tmp; } //函数重载< bool xt_string::string::operator<(const string& s)const { return strcmp(_str, s._str) < 0; } //函数重载== bool xt_string::string::operator==(const string& s)const { return strcmp(_str, s._str) == 0; } //函数重载<= bool xt_string::string::operator<=(const string& s)const { return *this < s || *this == s; } //函数重载> bool xt_string::string::operator>(const string& s)const { return !(*this <= s); } //函数重载>= bool xt_string::string::operator>=(const string& s)const { return !(*this < s); } //函数重载!= bool xt_string::string::operator!=(const string& s)const { return !(*this == s); } //流插入 ostream& xt_string::operator<<(ostream& out, const string& s) { for (size_t i = 0; i < s.size(); i++) { out << s[i]; } return out; } ////<<运算符的重载 //ostream& operator<<(ostream& out, const string& s) //{ // //使用范围for遍历字符串并输出 // for (auto e : s) // { // cout << e; // } // return out; //支持连续输出 //} //流提取 //istream& xt_string::operator>>(istream& in, string& s) //{ // s.clear(); //清空字符串 // char ch = in.get(); //读取一个字符 // while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取 // { // s += ch; //将读取到的字符尾插到字符串后面 // ch = in.get(); //继续读取字符 // } // return in; //支持连续输入 //} istream& xt_string::operator>>(istream& in, string& s) { //模拟标准 >> 的行为------跳过输入开头的所有空格和换行符 s.clear(); // 清空目标字符串 char ch = in.get(); // 读取第一个字符 while (ch == ' ' || ch == '\n') // 如果是空格或换行 { ch = in.get(); // 继续读下一个字符 } //使用缓冲区读取有效字符 char buff[128]; // 128字节的临时缓冲区 int i = 0; // 缓冲区索引 while (ch != ' ' && ch != '\n') // 遇到空格或换行就停止 { buff[i++] = ch; // 把字符存入缓冲区 if (i == 127) // 缓冲区快满时(留1位给'\0') { buff[i] = '\0'; // 添加字符串结束符 s += buff; // 追加到结果字符串 i = 0; // 重置索引,继续使用缓冲区 } ch = in.get(); // 读取下一个字符 } //处理缓冲区剩余字符 if (i != 0) // 如果缓冲区还有未处理的字符 { buff[i] = '\0'; // 添加结束符 s += buff; // 追加到结果字符串 } return in; // 返回流引用 } //读取一行含有空格的字符串 istream& xt_string::getline(istream& in, string& s) { s.clear(); //清空字符串 char ch = in.get(); //读取一个字符 while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取 { s += ch; //将读取到的字符尾插到字符串后面 ch = in.get(); //继续读取字符 } return in; } ``` ### 3.3 test.cpp ```cpp #define _CRT_SECURE_NO_WARNINGS 1 #include #include #include"string.h" using namespace std; void test1() { //初始化测试 xt_string::string s1("hello\0world"); cout << s1.c_str() << endl; //拷贝构造测试 xt_string::string s2(s1); cout << s2.c_str() << endl; //赋值运算符重载 xt_string::string s3("hello string"); s1 = s3; cout << s1.c_str() << endl; //下标访问,可以修改 for (size_t i = 0; i < s3.size(); i++) { s3[i]++; } cout << s3.c_str() << endl; //普通迭代器 xt_string::string::iterator it = s1.begin(); while(it != s1.end()) { std::cout << *it << " "; it++; } std::cout << std::endl; //范围for for (auto ch : s1) { std::cout << ch << " "; } std::cout << std::endl; std::cout << std::endl; //常量迭代器-常量对象 print_str(s1); } void test2() { xt_string::string s1("hello world"); cout << s1.size() << endl; cout << s1.capacity() << endl; cout << s1.empty() << endl; s1.clear();//清空数据 cout << s1.empty() << endl; cout << endl; cout << s1.size() << endl; cout << s1.capacity() << endl; s1.reserve(100); cout << s1.size() << endl; cout << s1.capacity() << endl; } //void test3() //{ // xt_string::string s4("abcdefghigk"); // cout << s4.c_str() << endl; // cout << s4.size() << endl; // cout << s4.capacity() << endl; // // cout << endl; // // s4.resize(8); // xt_string::string s1("abcdefghigk"); // cout << s4.c_str() << endl; // cout << s4.size() << endl; // cout << s4.capacity() << endl; // //} //void test3() //{ // xt_string::string s4("abcdefghigk"); // s4.reserve(15); // cout << s4.c_str() << endl; // cout << s4.size() << endl; // cout << s4.capacity() << endl; // // cout << endl; // // s4.resize(12,'a'); // xt_string::string s1("abcdefghigk"); // cout << s4.c_str() << endl; // cout << s4.size() << endl; // cout << s4.capacity() << endl; // //} void test3() { xt_string::string s4("abcdefghigk"); s4.reserve(15); cout << s4.c_str() << endl; cout << s4.size() << endl; cout << s4.capacity() << endl; cout << endl; s4.resize(20, 'a'); xt_string::string s1("abcdefghigk"); cout << s4.c_str() << endl; cout << s4.size() << endl; cout << s4.capacity() << endl; } void test4() { /*xt_string::string s5("abcdefghigk"); cout << s5.c_str() << endl; cout << s5.size() << endl; cout << s5.capacity() << endl; s5.push_back('x'); cout << s5.c_str() << endl; cout << s5.size() << endl; cout << s5.capacity() << endl;*/ xt_string::string s5("abcdefghigk"); cout << s5.c_str() << endl; cout << s5.size() << endl; cout << s5.capacity() << endl; s5.append("aaaaaaaaaa"); cout << s5.c_str() << endl; cout << s5.size() << endl; cout << s5.capacity() << endl; } void test5() { xt_string::string s6("hhhhhhhhhhh"); s6 += 'e'; cout << s6.c_str() << endl; cout << endl; s6 += "!!!!"; cout << s6.c_str() << endl; } void test6() { xt_string::string s7("hello world"); s7.insert(0, 3, '#'); cout << s7.c_str() << endl; } void test7() { xt_string::string s8("abcdefghijk"); cout << s8.c_str() << endl; cout << endl; s8.erase(2, 5); cout << s8.c_str() << endl; } void test8() { xt_string::string s9("abcdefghij"); cout << s9.c_str() << endl; size_t pos1 = s9.find('b', 0); cout << pos1 << endl; size_t pos2 = s9.find("fghi", 0); cout << pos2 << endl; cout << endl; size_t pos3 = s9.rfind('b', 0); cout << pos3 << endl; size_t pos4 = s9.rfind("fghi", 0); cout << pos4 << endl; cout << endl; xt_string::string s10 = s9.substr(5, 4); cout << s10.c_str() << endl; } void test9() { xt_string::string s11("hello world"); cout << s11.c_str() << endl; xt_string::string s12("hello string"); cout << s12.c_str() << endl; cout << (s11 < s12) << endl; cout << (s11 > s12) << endl; cout << (s11 == s12) << endl; cout << (s11 != s12) << endl; cout << (s11 <= s12) << endl; cout << (s11 >= s12) << endl; } void test10() { xt_string::string s13("hello"); s13 += '\0'; s13 += "#######"; cout << s13.c_str() << endl; cout << s13 << endl; cout << endl; /*xt_string::string s14; cin >> s14; cout << s14; cout << endl;*/ xt_string::string s15; getline(cin, s15); cout << s15; } int main() { test1(); return 0; } ``` ## 四、写时拷贝(了解) 写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的 引用计数:用来记录资源使用者的个数。 在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/09143efea313465b827524b5113bdce8.png)

相关推荐
带土117 小时前
2. Linux下FFmpeg C++音视频解码+推流开发
linux·c++·ffmpeg
星火开发设计17 小时前
C++ set 全面解析与实战指南
开发语言·c++·学习·青少年编程·编程·set·知识
scx2013100417 小时前
20260105 莫队总结
c++
Q741_14718 小时前
海致星图招聘 数据库内核研发实习生 一轮笔试 总结复盘(1) 作答语言:C/C++ 链表 二叉树
开发语言·c++·经验分享·面试·笔试
咔咔咔的18 小时前
1970. 你能穿过矩阵的最后一天
c++
_OP_CHEN18 小时前
【从零开始的Qt开发指南】(十九)Qt 文件操作:从 I/O 设备到文件信息,一站式掌握跨平台文件处理
开发语言·c++·qt·前端开发·文件操作·gui开发·qt文件
CSDN_RTKLIB18 小时前
【std::map】双向迭代器说明
c++·stl
王老师青少年编程18 小时前
信奥赛C++提高组csp-s之欧拉回路
c++·算法·csp·欧拉回路·信奥赛·csp-s·提高组
No0d1es18 小时前
2025年12月 GESP CCF编程能力等级认证C++六级真题
c++·青少年编程·gesp·ccf·6级