当前位置:   article > 正文

【爱上C++】详解string类2:模拟实现、深浅拷贝

【爱上C++】详解string类2:模拟实现、深浅拷贝

在上一篇文章中我们介绍了string类的基本使用,本篇文章我们将讲解string类一些常用的模拟实现,其中有很多细小的知识点值得我们深入学习。Let’s go!


类声明

namespace Mystring
{
    // 定义一个字符串类
    class string
    {
    public:
        // 公共成员函数和接口

    private:
        // 私有成员变量,限制直接访问底层数据

        size_t _capacity = 0;   // 字符串的容量
        size_t _size = 0;       // 字符串的长度
        char* _str = nullptr;   // 指向字符串数据的指针
        const static size_t npos = -1;  // 静态常量,表示未找到位置或无效位置
    };
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在C++中,静态成员变量(static)的定义通常需要在类的外部进行,而非静态成员变量则需要在类的内部进行定义。然而,对于静态成员变量如果其为 const 且为整数类型(包括枚举类型),则可以在类内部直接进行初始化。因此,对于 const static size_t npos = -1; 这样的声明,其允许在类内部直接进行初始化。
对于npos的初始化,下面两种方式都可以

class string {
public:
    static const size_t npos = -1; 
};

  • 1
  • 2
  • 3
  • 4
  • 5
class string {
public:
    static const size_t npos;
};

const size_t string::npos = -1;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

声明变量时可以顺便初始化,这样可以确保对象在创建时具有合适的初始值。
结构上使用了命名空间Mystring来避免与标准库中的 std::string 冲突。
本篇文章的代码采用声明与定义分离的方式。声明放在string.h文件,定义放在string.cpp文件。.cpp文件中通过包对应头文件以及声明命名空间,然后通过类名::成员的方式定义和实现函数。

默认成员函数

构造函数

声明:

string(const char*str="");
//(提供了一个缺省值表示在没有提供参数时,str 默认初始化为一个空字符串
//(即一个以 null 结尾的字符数组,其中只有一个字符 '\0'))
  • 1
  • 2
  • 3

这里是 "“不是” "。后者不为空, 有一个空格.

定义:

string(const char* str )		
{
	_size = strlen(str); 	// 计算字符串长度
	_capacity = _size;		// 初始容量与字符串长度相同
	_str = new char[_capacity + 1];// 为字符串分配内存空间,多开一个空间用于存放 '\0'
	strcpy(_str, str);		// 将参数 str 的内容拷贝到 _str 中
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如 string::string s1("Hello");

析构函数

~string()
{
	delete[] _str;// 释放字符串的内存空间,使用 delete[] 因为 _str 是数组形式的字符串
	_size = 0; 	 // 将字符串长度置为 0,表示字符串已经被释放
	_capacity = 0;// 将容量置为 0,表示容量无效
	_str = nullptr;  // 将指向字符串数据的指针置为 nullptr,防止出现悬空指针
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

析构函数在对象被销毁时自动调用,通常用来释放对象所持有的资源,例如动态分配的内存。

**拓展:**悬空指针
是指指向已经被释放或者无效的内存地址的指针。当一个指针被赋予了 nullptr 或者指向的内存已经被释放时,这个指针就变成了悬空指针。
在C++中,如果一个对象的析构函数中没有将指针设置为 nullptr,那么当对象被销毁时,其指针成员可能会成为悬空指针。悬空指针引发的问题主要有两个:

  1. 未定义行为(Undefined Behavior):如果试图通过悬空指针访问内存,则会导致未定义行为,这可能会导致程序崩溃或者产生难以预料的结果。
  2. 内存泄漏或重复释放:悬空指针可能会导致内存泄漏,因为释放过的内存没有被正确释放,或者在程序的其他地方被重新分配,导致对同一块内存的多次释放。

在编程中,为了避免悬空指针的问题,通常有以下建议:

  • 析构函数中将指针置为 nullptr:在对象被销毁时,确保将指针成员设置为nullptr,这样可以避免在对象的生命周期结束后访问悬空指针。
  • 使用智能指针:C++11引入的智能指针(如std::unique_ptrstd::shared_ptr)可以帮助自动管理动态内存,避免手动释放内存和悬空指针问题。
  • 注意指针的生命周期:确保在指针可能成为悬空指针的情况下,适时将其置为nullptr,或者避免在对象生命周期结束后继续使用该指针。

通过良好的编程实践和注意内存管理,可以有效避免悬空指针带来的问题,提高程序的健壮性和可靠性。

拷贝构造函数

深浅拷贝问题

如果我们不写拷贝构造函数,编译器会默认生成一个浅拷贝的拷贝构造函数。但是,默认生成的拷贝构造函数只会简单地逐成员进行赋值拷贝,这在处理指针成员变量时会导致严重问题
当使用默认的浅拷贝构造函数时,两个对象会共享同一个内存空间,会导致以下问题
image.png

  • 共享内存:s1 和 s2 共享同一块内存,这意味着修改一个对象会影响另一个对象。
  • 悬空指针:当 s1 或 s2 析构时,内存会被释放,另一个对象的指针会变成悬空指针。
  • 双重释放:当 s1 和 s2 都析构时,会尝试释放同一块内存两次,导致程序崩溃。

为了解决浅拷贝带来的问题,我们需要实现一个深拷贝的拷贝构造函数。深拷贝会为新对象分配独立的内存空间,并将原对象的数据复制到新对象中,从而避免共享内存的问题。

image.png

传统写法

    string(const string& s) {
        // 为新对象分配独立的内存空间,并且多分配一个字节用于存储终止符 '\0'
        _str = new char[s._capacity + 1];

        // 将原对象的字符串数据复制到新对象的内存空间
        strcpy(_str, s._str);

        // 复制原对象的大小和容量
        _size = s._size;
        _capacity = s._capacity;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

现代写法

		void swap(string& s)
		{
			std::swap(_str, s._str);//使用 std::swap 交换当前对象和临时对象的 _str 指针。
			std::swap(_size, s._size);//使用 std::swap 交换当前对象和临时对象的 _size 值。
			std::swap(_capacity, s._capacity);//使用 std::swap 交换当前对象和临时对象的 _capacity 值
		}
        //s2(s1)    //下面的s 就是s1
		string(const string& s)  
        :_str(nullptr)
        ,_size(0)
        ,_capacity(0)
		{
			string tmp(s._str); // 注意!是构造 
// 使用 s 对象的内部 C 风格字符串 _str 构造一个临时的字符串对象 tmp
			swap(tmp); 
// 交换当前对象和临时对象的数据,使当前对象的内容变为 tmp 的内容,临时对象则被销毁
		}
//解析:tmp和s1有一样大的空间,一样的值。然后s2和tmp一交换,那s2就和s1一样了,就完成了。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

图解: 交换前
image.png
交换后
image.png
在C++中,当我们用string s2(s1)来创建string对象时,s1是用来初始化string s2的源对象。现代写法中的string(const string& s)构造函数会被调用来实现这一点。
理解现代写法:
在这个构造函数中,我们可以理解成:

  1. 调用构造函数
    • 当我们写string s2(s1)时,编译器调用string类的拷贝构造函数string(const string& s)
    • 这里的s就是s1,表示用s1对象来初始化新创建的s2对象。
  2. 创建临时对象
    • 在构造函数内部,首先使用s对象(即s1)的内部 C 风格字符串_str来构造一个临时对象tmp
    • string tmp(s._str)这行代码会调用另一个构造函数string(const char* str),用s1对象的字符串数据来初始化临时对象tmp
  3. 交换数据
    • 调用swap(tmp)将当前对象(即s2)的成员变量与临时对象tmp的成员变量进行交换。
    • 在交换之后,s2对象持有了tmp的数据,即持有了s1的数据副本,而tmp则持有了s2的初始数据(在这时通常为空或者默认值)。
  4. 析构临时对象
    • 当构造函数结束时,临时对象tmp离开作用域,自动析构,释放它持有的资源。
    • 由于tmp持有的是s2的初始数据(在构造时通常是无效数据),所以释放时不会影响s2,也不会造成资源泄漏。

这种现代写法通过创建临时对象和交换数据,确保了拷贝构造的简洁性和异常安全性,同时避免了资源泄漏和浅拷贝带来的问题。

❓为什么要在初始化列表中给 _str 初始化为空指针?
string(const string& s)
: _str(nullptr)
如果不对它进行处理,一开始指向的是 未定义的(随机值)。在交换之后,这个随机值就给了tmp了,tmp出了作用域后调用析构函数进行释放会对随机值指向的空间进行释放。 这种情况下,系统可能无法正确处理释放操作,从而导致程序崩溃或者其他未定义行为。
delete 或者 free 一个空指针是安全的操作,不会导致运行时错误,所以这里把它初始化为nullptr,tmp最后释放空,不会出现问题。

赋值运算符重载

传统写法

string& operator=(const string& s)
{
    if (this != &s) // 防止自我赋值
    {
        char* tmp = new char[s._capacity + 1]; // 为临时存储空间分配内存,大小为 s 对象的容量加一(用于存放字符串末尾的 '\0')
        strcpy(tmp, s._str); // 将 s 对象的字符串复制到临时存储空间 tmp
        delete[] _str; // 删除当前对象已有的字符串内存
        _str = tmp; // 将当前对象的 _str 指向新分配的字符串内存
        _size = s._size; // 更新当前对象的字符串长度
        _capacity = s._capacity; // 更新当前对象的容量
    }
    return *this; // 返回当前对象的引用,支持连续赋值操作
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

传统写法图解:
image.png

现代写法

//s1=s3
string& operator=(string s) // 使用传值方式传入参数 s,利用了移动语义
{
    swap(s); // 使用交换函数进行赋值操作,此时 s 是通过拷贝构造函数传入的临时对象
    return *this; // 返回当前对象的引用,支持连续赋值操作
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

交换前
image.png
交换后
image.png
string& operator=(string s) 中使用传值传参主要有以下几个原因:

  1. 移动语义的利用
    • 传值传参允许编译器在需要的时候使用移动语义,这样可以避免不必要的深拷贝,提升性能。
    • 如果传递的参数是右值(例如,s1 = std::move(s3)),则会调用移动构造函数而不是拷贝构造函数,从而避免了数据的复制。
  2. 简化代码
    • 通过传值传参,可以在函数体内直接交换当前对象和参数对象的数据。这使得代码更简洁,并且更容易理解和维护。
  3. 异常安全性
    • 传值传参结合交换操作可以确保资源的正确释放,避免资源泄漏和其他异常问题。

详细过程解释
假设我们有以下赋值操作:s1 = s3;

  1. 传值传参
string s(s3); // 临时对象 s 通过拷贝构造或移动构造函数创建
  • 1
  • 当调用s1 = s3;时,会创建一个临时对象s。这个临时对象s是通过拷贝构造函数(如果s3是左值)或移动构造函数(如果s3是右值)创建的。
  1. 交换操作
swap(s); // 交换 s1 和 s 的数据
  • 1
  • 在赋值运算符的实现中,调用swap(s);。这会交换当前对象s1和临时对象s的内部数据指针。
  1. 临时对象销毁
// 临时对象 s 离开作用域,被销毁,释放旧资源
  • 1
  • 在赋值运算符函数结束时,临时对象s离开作用域并被销毁,其析构函数会释放它所持有的资源。这些资源实际上是原来属于s1的旧资源。
  1. 返回当前对象
return *this; // 返回当前对象的引用
  • 1
  • 返回当前对象s1的引用,以支持连续赋值操作。

容器操作

获取长度:size

		size_t size() const  
//考虑到不需要修改,我们加上 const。
		{
			return _size;
		}
  • 1
  • 2
  • 3
  • 4
  • 5

获取当前容量:capacity

		size_t capacity() const
		{
			return _capacity;
		}
  • 1
  • 2
  • 3
  • 4

查询是否为空:empty

		bool empty() const
		{
			return _size == 0;
		}
  • 1
  • 2
  • 3
  • 4

扩容:reserve

void reserve(size_t n)
{
    // 如果请求的容量大于当前的容量,才需要重新分配内存
    if (n > _capacity)
    {
        char* tmp = new char[n + 1]; // 分配新的内存空间,比请求的容量多一个字符
        // 这个额外的字符用于存放字符串结尾的空字符 '\0',确保字符串的有效性和正确性

        strcpy(tmp, _str); // 将原字符串内容拷贝到新内存,也会拷贝结尾的 '\0'

        delete[] _str; // 释放原来的内存

        _str = tmp; // 更新指针,使其指向新的内存
        _capacity = n; // 更新容量
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

扩容扩容,所以n要≥_capacity

调整字符串大小:resize

记得用缺省值,用户在调用 resize 函数时可以选择性地提供第二个参数.
假如有一个字符串对象 str,当前大小为 5,内容为 “hello”,容量为 10。调用 str.resize(8, ‘x’) 后 就是 helloxxx\0
声明
void resize(size_t, char c = '\0');
定义

void resize(size_t n, char c)
{
    // 如果新的大小大于当前大小,需要扩展字符串
    if (n > _size)
    {
        // 如果新的大小大于当前容量,需要扩展内存
        if (n > _capacity)
        {
            reserve(n); // 调用 reserve 函数扩展容量
        }
        
        // 将新的字符填充到扩展后的字符串中
        for (size_t i = _size; i < n; i++)
        {
            _str[i] = c;
        }
    }
    else if (n < _size)
    {
        // 如果新的大小小于当前大小,只需更新大小
        _size = n; 
        // 注意:此时容量不会改变
    }

    // 更新字符串的实际大小,并确保字符串以空字符结尾
    _str[_size] = '\0';
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

缩容就直接在下标为n的位置设置为\0即可。

字符串访问

[]访问

		//仅能访问
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);//assert 括号里为假的时候才会报错
			return _str[pos];
		}
		//访问+修改
		char& operator[](size_t pos)
		{
			assert(pos <= _size);
			return _str[pos];
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

迭代器访问

迭代器在 C++ 中常常被描述为类似指针的对象,它提供了对容器(比如字符串)中元素的访问和操作。
对于模拟实现的字符串类,我们可以直接使用原生指针来作为迭代器,通过 typedef 进行重命名,这样就可以在类中直接使用迭代器。
首先,我们使用 typedef 将指针重命名为迭代器,同时定义了常量迭代器:
typedef char* iterator;
typedef const char* const_iterator;

        // 返回字符串的起始位置
		iterator begin() {
			return _str;
		}

		// 返回字符串的结束位置'\0' 的下一个位置(即 null 字符的位置)
		iterator end() {
			return _str + _size;
		}

		// 返回字符串的起始位置(const 版本,不能修改数据)
//常量成员函数
//const_iterator begin() const 和 const_iterator end() const 被声明为常量成员函数。
//这意味着它们不会修改对象的任何成员变量,并且它们可以被常量对象调用。
   //为什么需要最右边的const???
//如果没有最后的 const 修饰符,编译器将认为 begin() 和 end() 可能会修改对象。因此,当你试图在一个常量对象上调用这些函数时,会产生编译错误,因为编译器不允许通过常量对象调用非常量成员函数。
		const_iterator begin() const {
			return _str;
		}

		// 返回字符串的结束位置的下一个位置(const 版本)
		const_iterator end() const {
			return _str + _size;
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这些函数使得我们可以像操作指针一样操作迭代器,比如使用 ++ 和 – 来移动迭代器指向的位置,或者使用 * 来访问迭代器指向的元素。这样,我们就可以通过迭代器来遍历字符串中的字符了。

插入类

尾插一个字符push_back

		void push_back(char ch)
		{
			if (_size == _capacity)//大小和总容量一样的时候,说明不够用了
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newCapacity);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';//一定要处理好\0
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

append

在尾部追加一个string对象

string& append(const string& str)
{
    // 检查是否需要扩展容量
    if (str._size >= _capacity - _size) // 判断是否需要扩容
    {
        reserve(_capacity + str._size + 1); // 扩容并预留足够空间
    }
    
    // 复制传入的字符串到当前字符串的末尾
    strcpy(_str + _size, str._str); // _str + _size 是当前字符串尾部
    
    // 更新当前字符串的大小
    _size += str._size; // 更新_size
    
    // 手动设置字符串的结尾
    _str[_size] = '\0'; // 手动设置字符串尾部的\0
    
    // 返回当前对象的引用
    return *this; // 返回string对象
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在尾部追加一个C风格字符串

		void append(const char* str) //注意传的是指针
		{	
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			//char *strcpy(char *dest, const char *src);   strcat(_str,str)也行,但是效率不行
			strcpy(_str + _size, str);
			_size += len;
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在尾部追加n个字符

		void append(size_t n, char ch)
		{
			// 检查是否需要扩展容量
			if (_size + n > _capacity)
			{
				reserve(_size + n); // 扩展容量以容纳新字符
			}

			// 将字符 ch 追加 n 次到字符串末尾
			for (size_t i = 0; i < n; i++)
			{
				_str[_size + i] = ch;
			}

			// 更新字符串的大小
			_size += n;

			// 确保字符串以 '\0' 结尾
			_str[_size] = '\0';
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

注意:_size和_capacity是不计算\0的

insert

在pos位置插入一个字符

在 C++ 中,通常情况下,字符串的位置索引 pos 是从 0 开始的,即第一个字符的位置为 0,第二个为 1,依此类推。这种习惯是因为 C++ 中的数组和字符串的索引都是从 0 开始计数的。
_str 表示字符串的起始位置,即第一个字符的地址。
_str + 1 表示字符串中第二个字符的地址。
_str + pos 表示字符串中第 pos 个位置的地址,即要进行插入或其他操作的位置。

void insert(size_t pos, char ch)
{
    // 确保插入位置在有效范围内
    assert(pos <= _size); // pos 等于 _size 时表示尾插
    
    // 检查是否需要扩展容量
    if (_size == _capacity) // 大小和总容量一样的时候,说明不够用了
    {
        size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; // 扩展容量,最小扩展到 4
        reserve(newCapacity); // 调用 reserve 函数扩展容量
    }

    // 从后往前 移动数据以腾出插入位置
    int end = _size;
    while (end >= (int)pos) // 循环直到 end 小于 pos
    {
        _str[end + 1] = _str[end]; // 将当前位置的数据向后移动一位
        --end; // end 减 1
    }

    // 在指定位置插入新字符
    _str[pos] = ch; // 在 pos 位置插入字符 ch
    _size++; // 更新大小
    _str[_size] = '\0'; // 确保字符串以 '\0' 结尾
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

为什么 while (end >= (int)pos) 要强制转换成int类型呢?
因为end 是一个 size_t 类型的变量,这是一个无符号整数类型。pos 也是 size_t 类型。如果直接比较 end 和 pos,即使 end 被减到负值,由于 size_t 是无符号类型,负值会被当成一个非常大的正整数。这可能会导致无限循环和访问越界。
通过将 pos 强制转换为 int,确保 end 和 pos 在比较时都是有符号整数类型,从而避免了无符号整数类型转换的问题。这种做法保证了在 end 小于 pos 时,循环能正确退出。

在pos位置插入一个字符串

void insert(size_t pos, const char* str)
{
    // 确保插入位置在当前字符串长度范围内
    assert(pos <= _size);
    
    // 计算要插入字符串的长度
    size_t len = strlen(str);
    
    // 如果当前容量不足以容纳插入后的新字符串,则增加容量
    if (_size + len > _capacity)
    {
        reserve(_size + len); // 调用 reserve 函数扩展容量
    }
    
    // 使用有符号整数类型的 end 变量,以避免无符号整数类型带来的潜在问题
    int end = _size;
    
    // 从字符串末尾向前移动字符,以腾出插入位置
    while (end >= (int)pos)
    {
        _str[end + len] = _str[end]; // 将当前位置的数据向后移动 len 位
        --end; // end 减 1
    }
    
    // 将新的字符串插入到指定位置
    strncpy(_str + pos, str, len); // 使用 strncpy 复制字符串内容,但不包括末尾的 '\0'
    
    // 更新字符串的大小
    _size += len; // 新字符串的长度增加
    _str[_size] = '\0'; // 确保字符串以 '\0' 结尾
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

operator+=

		string& operator+=(char ch)//+=一个字符
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)//+=一个 char* 字符串
		{
			append(str);
			return *this;
		}
        string& operator+= (const string& str) //+=一个string对象
        {
        	append(str);
        	return *this;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

删除类

erase

从pos位置开始,删除长度为len的字符串。若未给出len,则默认删完.
void erase(size_t pos, size_t len = npos);

		void erase(size_t pos,size_t len)  //pos 是下标,删除1个就是pos位置的那个
		{
			//assert(pos <_size);// xxxx size=4,
			//assert(_size > 0);
			assert(pos < _size); // 这里不需要检查 pos >= 0,因为 pos 是无符号类型
			if (len == npos||pos+len>=_size)//要删完
			{//但我们不用删,直接缩大小,
				_str[pos] = '\0';
				_size = pos;
			}
			else   
			{
                //后面数据挪过去覆盖
				// hello,wordl
				//       ↑  ↑:pos+len
				//      pos   删3个
				strcpy(_str + pos, _str + pos + len);
				_size -= len;//覆盖之后减少_size即可

			}
			//"abcdefghi"。假如pos是3,len是4。pos是下标
			//_str 指向字符串的第一个字符,即 'a'。
			//_str + pos 指向字符串的第 4 个字符,即 'd'。
			//_str + pos + len 指向字符串的第 8 个字符,即 'h'。
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

其他操作

swap

尽管标准库中的 std::swap 可以用于交换两个对象,但是它仅在你提供的交换操作对你特定类的成员变量的交换上不能直接进行。
标准库的 std::swap 无法直接处理类的私有成员变量的交换,而必须通过类提供的接口进行交换操作。
所以 自定义类型要自己写,上面的拷贝构造和赋值重载的现代写法都用到了此处的swap函数

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

find

返回字符c在string中第一次出现的位置

size_t find(char ch,size_t pos=0)

		size_t  find(char ch,size_t pos)//半缺省
		{
			for (size_t i =pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

返回子串s在string中第一次出现的位置

size_t find(const char* str,size_t pos=0);

size_t find(const char* str, size_t pos )
{
    // 使用 strstr 函数从 _str + pos 位置开始查找子字符串 str
    const char* ptr = strstr(_str + pos, str);
    
    // 如果 ptr 为空,表示没有找到子字符串
    if (ptr == nullptr)
    {
        // 返回 npos 表示查找失败
        return npos;
    }
    else
    {
        // 返回子字符串在字符串中的起始位置
        return ptr - _str;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

strstr 是 C 语言标准库 (或 <string.h>)中的函数,用于在一个字符串中查找第一次出现另一个字符串的位置。
constchar* strstr(constchar* str1, constchar* str2);

  • str1:要在其中搜索的主字符串。
  • str2:要搜索的子字符串。
  • 如果 str2 是 str1 的子串,则返回指向 str1 中第一次出现 str2 的位置的指针。
  • 如果 str2 不是 str1 的子串,则返回 nullptr。

substr

从当前字符串中提取子串。
string substr(size_t pos = 0, size_t len = npos);

		string substr(size_t pos , size_t len )
		{

			assert(pos < _size);
			size_t end = pos + len;
			if (len == npos || pos + len >= _size)
			{
				end = _size;
			}
			string str;
			str.reserve(end - pos);
			for (size_t i = pos; i < end; i++)
			{
				str += _str[i];
			}
			return str;//返回str的拷贝
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

printf_str

打印C风格字符串。C风格字符串是以空字符 ‘\0’ 结尾的字符数组

        void printf_str(const string& s)//权限放大,上面的末尾要加const
//在C++中,成员函数末尾的const关键字用于指示该函数不会修改对象的状态。这种函数被称为const成员函数,它们对于保证对象的不可变性非常重要。
//c_str() 和 size() 都是访问器函数,它们不会修改字符串对象的内容,因此应该声明为const成员函数以确保它们可以在const对象上调用。
//这样做不仅符合面向对象的设计理念,还允许用户在const对象上调用这些函数,以便于在const上下文中使用你的类。
//而对于const char& operator[](size_t pos) const,它是一个重载的下标运算符,用于访问字符串中指定位置的字符。由于该函数不会修改对象的内容,因此也应该声明为const成员函数。
		{
			for (size_t i = 0; i < s.size(); i++)
			{
				//	s[i]++;    参数加const 就是为了防止这里进行修改。
				cout << s[i] << " ";

			}
			cout << endl;
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

clear

清空当前字符串对象,使其变为空字符串

        void clear()
		{
			_size = 0;
			_str[0] = '\0';//
		}
  • 1
  • 2
  • 3
  • 4
  • 5

c_str

获取字符串源指针
有些场景下,例如使用C语言的字符串操作函数,处理字符串时只能使用char*指针去传参,string为了兼容C字符串操作函数,支持获取字符串源指针,为了不破坏string的数据结构,这个返回的源字符串指针不支持修改,只能访问内容!
这个函数非常短小,直接在类中实现!

		const char* c_str() const 
		{
            assert(_str);
			return _str;
		}
  • 1
  • 2
  • 3
  • 4
  • 5

逻辑判断

实现了小于和等于,其他的直接复用.
都被声明为 const 成员函数。 const 关键字的作用是告诉编译器这些成员函数不会修改类的成员变量 _str 和 _size。

在 C++ 中,类的 const 成员函数可以确保在函数内部不会修改对象的任何成员变量,从而提供了对调用者的额外保证。这样的设计有助于代码的可维护性和可理解性。
如果没有将比较操作符声明为 const,则无法在常量对象上调用这些操作符,因为常量对象只能调用 const 成员函数。例如,对于声明为 const 的对象或者在常量上下文中使用的对象(如 const String s1, s2;),可以正常地执行比较操作。

bool operator<(const string& s) const //小于
{
	return (strcmp(_str, s._str) < 0);
}

bool operator==(const string& s) const //等于
{
	return (strcmp(_str, s._str) == 0);
}


bool operator<=(const string& s) const //小于等于
{
	return (*this < s) || (*this == s);
}

bool operator>(const string& s) const //大于
{
	return !(*this <= s);
}

bool operator>=(const string& s) const //大于等于
{
	return !(*this < s);
}

bool operator!=(const string& s) const //不等
{
	return !(*this == s);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

流操作

当我们在 C++ 中定义流插入运算符 << 和流提取运算符 >> 时,如果将它们定义为类的成员函数,会遇到一个问题:类的成员函数默认会有一个隐含的 this 指针作为第一个参数。这样的话,如果我们试图将 operator<< 或 operator>> 定义为成员函数,形式上会与预期不符,因为它们需要接受两个参数(左操作数和右操作数),而类成员函数形式下只能接受一个参数(除非将其定义为静态成员函数,但这不符合重载运算符的惯用方式)。
因此,为了正确地重载这些运算符,我们将它们定义为类的友元函数。友元函数可以在不通过对象接口(即不使用 this 指针)的情况下访问类的私有成员和受保护成员。这种做法不仅符合语法要求,还能保持类的封装性和安全性,因为只有特定的函数(即声明为友元的函数)才能直接访问类的私有部分。

流插入<<

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;//返回ostream对象 以支持cout<<s1<<s2<<s3
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

流提取>>

	istream& operator >>(istream& in, string& s)
	{
		s.clear(); // 清空当前字符串,以免变成尾插了
		char buff[128] = {0}; // 创建一个缓冲区用于暂存读取的字符序列
		char ch = in.get(); // 从输入流中读取一个字符
		int i = 0; // 初始化缓冲区索引

		// 循环读取字符直到遇到空格或换行符
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch; // 将读取的字符存储到缓冲区中
			if (i == 127) // 如果缓冲区即将满了
			{
				buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符
				s += buff; // 将缓冲区中的字符序列插入到字符串对象中
				i = 0; // 重置缓冲区索引
			}
			ch = in.get(); // 读取下一个字符
		}

		// 处理剩余的字符序列
		if (i > 0)
		{
			buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符
			s += buff; // 将缓冲区中的字符序列插入到字符串对象中
		}

		return in; // 返回输入流对象的引用
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

拓展:关于string其他常用函数

to_string

to_string 是 C++ 中的一个标准库函数,用于将各种类型的数据转换为对应的字符串表示形式。
头文件:#include<string>
语法:std::string to_string(类型 value); 类型可以是整数、浮点数。value: 要转换为字符串的数值。 返回转换后的 std::string 类型对象,表示数值的字符串形式。 image.png

stoi

stoi 是 C++ 中的一个标准库函数,用于将字符串转换为对应的整数类型。
头文件:#include<string>
int stoi(const std::string& str, size_t* pos = 0, int base = 10);

  • str: 要转换的字符串。
  • pos (可选): 指向 size_t 类型的指针,用于存储第一个无效字符的索引。
  • base (可选): 数字的基数,默认为 10。

返回:

  • 返回转换后的整数值。

image.png

完整代码展示

.h文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
using namespace std;
namespace Mystring
{
	class string
	{
	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;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity()
		{
			return _capacity;
		}

		bool empty() const
		{
			return _size == 0;
		}
		void printf_str(const string& s)
		{
			for (size_t i = 0; i < s.size(); i++)
			{
				cout << s[i] << " ";
			}
			cout << endl;
		}
		void clear()
		{
			_size = 0;
			_str[0] = '\0';//
		}

		const char* c_str() const
		{
			assert(_str);
			return _str;
		}

		string(const char* str = "");
		~string();
		string(const string& s);
		string& operator=(string s);
	
		void reserve(size_t n);
		void resize(size_t, char c = '\0');
	

		const char& operator[](size_t pos) const;
		char& operator[](size_t pos);
	

		void push_back(char ch);
		void append(const char* str);
		void append(size_t n, char ch);
		string& append(const string& str);
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		string& operator+=(const char* str);
		string& operator+=(char ch);
		string& operator+= (const string& str);

		void erase(size_t pos, size_t len = npos);

		void swap(string& s);
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);
	
		bool operator<(const string& s) const //小于
		{
			return (strcmp(_str, s._str) < 0);
		}

		bool operator==(const string& s) const //等于
		{
			return (strcmp(_str, s._str) == 0);
		}


		bool operator<=(const string& s) const //小于等于
		{
			return (*this < s) || (*this == s);
		}

		bool operator>(const string& s) const //大于
		{
			return !(*this <= s);
		}

		bool operator>=(const string& s) const //大于等于
		{
			return !(*this < s);
		}

		bool operator!=(const string& s) const //不等
		{
			return !(*this == s);
		}

	private:
		size_t _capacity = 0;
		size_t _size = 0;
		char* _str = nullptr;

		const static size_t npos = -1;
	};

	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139

.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1
//这个是声明和定义分离的版本
#include"string16.h"



namespace Mystring
{
	//构造函数
	string::string(const char* str)
	{
		_size = strlen(str);
		_capacity = _size;
		_str = new char[_capacity + 1];

		strcpy(_str, str);
	}
	//析构函数
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}

	//拷贝构造 现代写法
	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}
	//运算符重载
	string& string::operator=(string s)
	{
		swap(s);
		return *this;
	}


	//仅能访问
	const char& string::operator[](size_t pos) const
	{
		assert(pos < _size);//assert 括号里为假的时候才会报错
		return _str[pos];
	}
	//访问+修改
	char& string::operator[](size_t pos)
	{
		assert(pos <= _size);
		return _str[pos];
	}


	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void string::resize(size_t n, char c)
	{
		if (n > _size)
		{
			if (n > _capacity)
			{
				reserve(n); 
			}
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = c;
			}
		}
		else if (n < _size)
		{
			_size = n;
		}
		_str[_size] = '\0';
	}



	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newCapacity);
		}

		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}

		strcpy(_str + _size, str);
		_size += len;
	}

	string& string::append(const string& str)
	{
		// 检查是否需要扩展容量
		if (str._size >= _capacity - _size) // 判断是否需要扩容
		{
			reserve(_capacity + str._size + 1); // 扩容并预留足够空间
		}

		// 复制传入的字符串到当前字符串的末尾
		strcpy(_str + _size, str._str); // _str + _size 是当前字符串尾部

		// 更新当前字符串的大小
		_size += str._size; // 更新_size

		// 手动设置字符串的结尾
		_str[_size] = '\0'; // 手动设置字符串尾部的\0

		// 返回当前对象的引用
		return *this; // 返回string对象
	}

	void string::append(size_t n, char ch)
	{
		// 检查是否需要扩展容量
		if (_size + n > _capacity)
		{
			reserve(_size + n); // 扩展容量以容纳新字符
		}

		// 将字符 ch 追加 n 次到字符串末尾
		for (size_t i = 0; i < n; i++)
		{
			_str[_size + i] = ch;
		}

		// 更新字符串的大小
		_size += n;

		// 确保字符串以 '\0' 结尾
		_str[_size] = '\0';
	}




	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);

		if (_size == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newCapacity);
		}

		/*int end = _size;
		while (end >= (int)pos)
		{
			_str[end + 1] = _str[end];
			--end;
		}*/

		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		_str[pos] = ch;
		_size++;
	}

	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}

		int end = _size;
		while (end >= (int)pos)
		{
			_str[end + len] = _str[end];
			--end;
		}

		strncpy(_str + pos, str, len);
		_size += len;
	}

	string& string::operator+=(char ch)
	{
		push_back(ch);

		return *this;
	}

	string& string::operator+=(const char* str)
	{
		append(str);

		return *this;
	}


	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len == npos || pos + len >= _size)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}

	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}

		return npos;
	}

	size_t string::find(const char* str, size_t pos)
	{
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		size_t end = pos + len;
		if (len == npos || pos + len >= _size)
		{
			end = _size;
		}

		string str;
		str.reserve(end - pos);
		for (size_t i = pos; i < end; i++)
		{
			str += _str[i];
		}

		return str;
	}


	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128] = { 0 };
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328

07c03ae6d77b4b153f6d1ec710be7c14_7a80245f0b5f4021a033b3789a9efdeb.png

  1. 声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/797208
推荐阅读
相关标签