【c++】深入理解string类(4)

目录

[一 常见接口补充](#一 常见接口补充)

[1 c_str](#1 c_str)

[二 string类问题的模拟实现](#二 string类问题的模拟实现)

[1 打印函数](#1 打印函数)

[2 构造函数 析构函数](#2 构造函数 析构函数)

[3 扩容函数](#3 扩容函数)

[4 尾插函数](#4 尾插函数)

[5 测试](#5 测试)

[6 迭代器实现](#6 迭代器实现)

[7 insert](#7 insert)

[8 erase](#8 erase)


一 常见接口补充

1 c_str

这个接口就是为了兼容C语言,C++有时候会去调用C的接口,因为C++的库里面有时候提供api时会直接按照C的方式提供。就意味着就算当前我们的程序是用C++!写的,也不可避免地会调用C风格的接口。例如我们后面学习网络工程的时候,用到的send()这个接口,就会调用C_str.

2 不同类型之间的相互转换

(1)其他类型转换成浮点数

(2)浮点数转换成不同类型

每一个看最后的字母就可以判断出是什么类型转换:例如第一个最后一个字母是i,就表示是浮点数转换成整型,第三个ul表示 unsigned long


二 string类问题的模拟实现

先包含一下头文件:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<string>
#include<algorithm>
#include<list>
using namespace std;

1 打印函数

博主直接将代码的解析附录在注释中:

cpp 复制代码
// 函数功能:打印字符串的正序和逆序字符
// 参数:const string& s - 传入的字符串常量引用,保证原字符串不会被修改
void Print(const string& s)
{
    // 1. 正序遍历字符串
    // 使用const_iterator迭代器,用于遍历常量字符串,只能读取不能修改
    // 注意:不能用const string::iterator,因为s是const类型,其begin()返回const_iterator
    string::const_iterator it1 = s.begin();
    
    // 循环遍历直到字符串末尾(end()指向最后一个字符的下一位)
    while (it1 != s.end())
    {
        // *it1 = 'x'; // 编译错误:const_iterator不允许修改指向的内容
        cout << *it1 << " ";  // 输出当前迭代器指向的字符
        ++it1;                // 迭代器向后移动一位,指向 next 字符
    }
    cout << endl;  // 正序输出结束,换行


    // 2. 逆序遍历字符串
    // 使用const_reverse_iterator逆序迭代器,用于逆序遍历常量字符串,只能读取不能修改
    string::const_reverse_iterator it2 = s.rbegin();
    
    // 循环遍历直到逆序末尾(rend()指向第一个字符的前一位)
    while (it2 != s.rend())
    {
        // *it2 = 'x'; // 编译错误:const_reverse_iterator同样不允许修改内容
        cout << *it2 << " ";  // 输出当前逆序迭代器指向的字符
        ++it2;                // 逆序迭代器"++"表示向前前移动,指向 previous 字符
    }
    cout << endl;  // 逆序输出结束,换行
}

我们来测试一下:

cpp 复制代码
#include <iostream>
#include <string>
#include <list>
#include <algorithm> // 用于find函数
using namespace std;

// 假设已有之前定义的Print函数
void Print(const string& s);

// 测试字符串操作及相关C++特性的函数
void test_string2()
{
    // 用字符串常量初始化string对象
    string s1("hello world");
    cout << s1 << endl;  // 输出: hello world


    // 通过下标[]修改字符串中的字符([]不做越界检查)
    s1[0] = 'x';
    cout << s1 << endl;  // 输出: xello world
    cout << s1[0] << endl;  // 输出: x


    // 越界访问的两种方式及区别
    // s1[12];  // 用[]越界访问会触发断言(debug模式下),直接崩溃
    // s1.at(12); // 用at()越界访问会抛出out_of_range异常,可以捕获处理


    // 获取字符串长度的两种方法
    cout << s1.size() << endl;    // 输出: 11 (推荐使用size())
    cout << s1.length() << endl;  // 输出: 11 (与size()功能相同,历史原因保留)


    // 1. 使用下标+[]遍历并修改字符串
    for (size_t i = 0; i < s1.size(); i++)
    {
        s1[i]++;  // 每个字符的ASCII值加1('x'->'y','e'->'f'等)
    }
    cout << s1 << endl;  // 输出: yfmmp!xpsme


    // 2. 使用迭代器遍历字符串(iterator支持修改)
    // 迭代器是一种类似指针的对象,用于访问容器元素
    string::iterator it1 = s1.begin();  // begin()返回指向第一个元素的迭代器
    while (it1 != s1.end())  // end()返回指向最后一个元素下一位的迭代器
    {
        // (*it1)--;  // 取消注释可将字符改回原来的值
        cout << *it1 << " ";  // 解引用迭代器获取字符
        ++it1;  // 迭代器向后移动
    }
    cout << endl;  // 输出: y f m m p ! x p s m e 


    // 演示list容器的迭代器使用(与string迭代器用法一致,体现容器迭代器的统一性)
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    list<int>::iterator lit = lt.begin();
    while (lit != lt.end())
    {
        cout << *lit << " ";  // 输出: 1 2 3 
        ++lit;
    }
    cout << endl;


    // 调用Print函数,打印字符串的正序和逆序(使用const迭代器)
    Print(s1);


    // 使用标准库find函数查找元素(需要包含<algorithm>)
    // find返回迭代器,找到则指向该元素,否则指向end()
    // string::iterator ret1 = find(s1.begin(), s1.end(), 'x');
    auto ret1 = find(s1.begin(), s1.end(), 'x');  // 使用auto简化类型声明
    if (ret1 != s1.end())
    {
        cout << "找到了x" << endl;  // 此例中会输出该信息
    }


    // 在list中查找元素,迭代器用法与string一致
    // list<int>::iterator ret2 = find(lt.begin(), lt.end(), 2);
    auto ret2 = find(lt.begin(), lt.end(), 2);  // auto自动推导为list<int>::iterator
    if (ret2 != lt.end())
    {
        cout << "找到了2" << endl;  // 此例中会输出该信息
    }


    // C++11特性:auto关键字(自动类型推导)
    int i = 0;
    auto j = i;  // j被推导为int类型
    auto k = 10;  // k被推导为int类型
    auto p1 = &i;  // p1被推导为int*类型(指针)
    auto* p2 = &i;  // p2显式指定为指针类型,同样是int*
    cout << p1 << endl;  // 输出i的地址
    cout << p2 << endl;  // 输出i的地址(与p1相同)


    // auto与引用的结合
    int& r1 = i;  // r1是i的引用
    auto r2 = r1;  // r2被推导为int类型(不是引用),是r1所指值的拷贝
    auto& r3 = r1;  // r3被推导为int&类型(是r1的引用,即i的引用)
    
    // 打印地址验证
    cout << &r2 << endl;  // 输出r2的地址(与i不同)
    cout << &r1 << endl;  // 输出i的地址
    cout << &i << endl;   // 输出i的地址
    cout << &r3 << endl;  // 输出i的地址(与r1相同)


    // C++11特性:范围for循环(语法糖,简化迭代器遍历)
    // 范围for会自动遍历容器中所有元素,自动判断结束
    // for (auto ch : s1)  // 传值方式,修改ch不影响原字符串
    for (auto& ch : s1)   // 传引用方式,修改ch会影响原字符串
    {
        ch -= 1;  // 每个字符ASCII值减1(恢复之前的++操作)
    }
    cout << endl;


    // 用范围for遍历并打印字符串(const引用方式,防止意外修改)
    for (const auto& ch : s1)
    {
        cout << ch << ' ';  // 输出: x e l l o   w o r l d 
    }
    cout << endl;


    // 用范围for遍历list容器
    for (auto e : lt)
    {
        cout << e << ' ';  // 输出: 1 2 3 
    }
    cout << endl;


    // 范围for也支持数组(编译器做了特殊处理)
    int a[10] = { 1,2,3 };  // 初始化前3个元素,其余为0
    for (auto e : a)
    {
        cout << e << " ";  // 输出: 1 2 3 0 0 0 0 0 0 0 
    }
    cout << endl;
}

2 构造函数 析构函数

cpp 复制代码
namespace bit
{
	string::string(const char* str)
		:_size(strlen(str))
	{
		// Ӧ
		_str = new char[_size + 1];
		_capacity = _size;
		strcpy(_str, str);
	}

	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
}

代码解析:

复制代码
strcpy(_str, str);    // 将C风格字符串复制到已分配的内存中

 delete[] _str;        // 释放字符数组占用的内存(注意用delete[]匹配new[])

3 扩容函数

cpp 复制代码
void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			// 
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

创建了一块新的空间来存出数据,tmp就是新的空间

复制代码
 // 3. 释放原有内存,避免内存泄漏
        delete[] _str;
        
        // 4. 将字符串指针指向新内存
        _str = tmp;
        
        // 5. 更新容量为n(新容量)
        _capacity = n;

4 尾插函数

cpp 复制代码
void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}


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

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

区分:

  1. push_back 函数

    • 用于在字符串末尾添加单个字符
    • 扩容策略:当容量不足时,空容量时初始化为 4,否则翻倍扩容
    • 每次操作都确保保证字符串以 '\0' 结尾,维持 C 风格字符串的兼容性
  2. append 函数

    • 用于在字符串末尾添加一个完整的 C 风格字符串
    • 扩容策略:取 "所需总长度" 和 "当前容量翻倍" 的最大值,平衡内存利用率和扩容效率
    • 利用strcpy直接复制字符串,自动包含终止符,简化实现
复制代码
// 检查现有容量是否足够容纳追加后的所有字符
    if (_size + len > _capacity)
    {
        // 扩容到"当前长度+追加长度"和"当前容量*2"中的较大值
        // 保证既能容纳新内容,又能减少后续扩容次数
        reserve(std::max(_size + len, _capacity * 2));
    }

 // 更新有效长度(原有长度 + 追加的长度)
    _size += len;
    
    // 注意:strcpy会复制原字符串的'\0',因此无需额外手动添加终止符

注意:这里的_size是string类的一个成员变量,你哦啊是当前字符串的有效数据个数。

那么我们就可以分别用push_back和append对字符和字符串进行尾插操作:

cpp 复制代码
string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

5 测试

我们来测试一下上面自己实现的string类:

cpp 复制代码
​
#include <iostream>
// 假设包含了自定义string类的头文件
using namespace std;

// 测试自定义string类的各种功能和特性
void test_string1()
{
    // 1. 测试默认构造函数(创建空字符串)
    bit::string s1;
    // c_str()返回C风格字符串指针(以'\0'结尾),用于输出
    cout << s1.c_str() << endl;  // 输出空字符串


    // 2. 测试带参构造函数及字符串修改
    string s2("hello world");
    cout << s2.c_str() << endl;  // 输出: hello world
    
    // 通过[]运算符修改字符串第一个字符
    s2[0] = 'x';                 // s2变为: xello world
    
    // 遍历并修改每个字符(ASCII值+1)
    for (size_t i = 0; i < s2.size(); i++)
    {
        s2[i]++;                 // 每个字符递增:x->y, e->f, l->m等
    }
    cout << s2.c_str() << endl;  // 输出: yfmmp!xpsme


    // 3. 测试字符串初始化方式
    // 隐式类型转换:const char* -> string(编译器优化为直接构造,避免拷贝)
    string s3 = "hello world";   
    // 直接构造(与s3等价,两种初始化方式效果相同)
    string s4("hello world");    
    // 常量字符串对象(内容不可修改)
    const string s5("hello world");  


    // 4. 测试常量字符串的访问(const对象只能读不能写)
    for (size_t i = 0; i < s2.size(); i++)
    {
        // s5[i] = 'a'; // 编译错误:const对象不能修改
        cout << s5[i] << "-";    // 输出: h-e-l-l-o- -w-o-r-l-d-
    }
    cout << endl;


    // 5. 测试范围for循环遍历(普通对象,可读写)
    for (auto ch : s4)
    {
        cout << ch << " ";       // 输出: h e l l o   w o r l d 
    }
    cout << endl;


    // 6. 测试普通迭代器(可修改元素)
    string::iterator it4 = s4.begin();
    while (it4 != s4.end())
    {
        *it4 += 1;               // 每个字符ASCII值+1(h->i, e->f等)
        cout << *it4 << " ";     // 输出: i f m m p ! x p s m e 
        ++it4;
    }
    cout << endl;


    // 7. 测试范围for遍历const字符串(只读)
    for (auto ch : s5)
    {
        // ch = 'a'; // 编译错误:范围for遍历const对象时元素是只读的
        cout << ch << " ";       // 输出: h e l l o   w o r l d 
    }
    cout << endl;


    // 8. 测试const迭代器(只能读不能修改)
    string::const_iterator it5 = s5.begin();
    while (it5 != s5.end())
    {
        // *it5 += 1; // 编译错误:const迭代器不能修改指向的元素
        cout << *it5 << " ";     // 输出: h e l l o   w o r l d 
        ++it5;
    }
    cout << endl;
}

​

6 迭代器实现

cpp 复制代码
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;
		}

7 insert

insert是在指定位置插入字符串或字符

cpp 复制代码
void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);

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

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

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

字符移动:从后往前将原有字符串向后挪动一位(最后一位指向\0,这样增加完新的字符之后就不需要单独处理\0了)

为什么要将pos 强转为int类型?

因为在二目操作符中,如果先后两个类型会把小的类型自动强转为大的类型,此处就是把无符号转化成有符号,循环的终止条件是size<0(因为有可能是头插),无符号类型的-1是最大的整型,这样就会出现问题,所以需要把pos强转为int类型

有符号和无符号比较时会把无符号转换成有符号

那么除了把pos强转成int类型,还有什么其他的办法吗?

挪动数据的时候可以为end-1挪给end,判断循环条件变为end>pos

两种版本对比:

改进版本:改进版本为要插入长度为len的字符

cpp 复制代码
// 在当前字符串的 pos 位置插入 C 风格字符串 str
void string::insert(size_t pos, const char* str)
{
    // 断言:插入位置必须合法(pos 不能超过当前字符串长度)
    // 若 pos > _size,属于越界插入,Debug 模式下直接崩溃提示
    assert(pos <= _size);

    // 若插入的字符串为空(str 是 nullptr),直接断言失败(避免后续 strlen 崩溃)
    assert(str != nullptr);

    // 计算待插入字符串的有效长度(不含末尾的 '\0')
    size_t len = strlen(str);

    // 若插入的是空字符串(len=0),无需操作,直接返回
    if (len == 0)
    {
        return;
    }

    // 检查是否需要扩容:插入后总长度(原长度 + 插入长度)是否超过当前容量
    if (_size + len > _capacity)
    {
        // 扩容策略:取「插入后所需最小容量」和「原容量的2倍」中的较大值
        // 避免扩容后仍不足,同时兼顾减少未来扩容次数
        reserve(std::max(_size + len, _capacity * 2));
    }

    // 数据挪动:将原字符串中 pos 及之后的字符整体向后挪动 len 个位置
    // 从原字符串末尾(_size)向后偏移 len 个位置开始挪动(避免覆盖未处理的数据)
    size_t end = _size + len;

    // 终止条件:当 end 挪到「pos + len - 1」时,说明已腾出插入所需的空间
    // 循环中每次向前移动一个位置,直到 end 不大于目标位置
    while (end > pos + len - 1)
    {
        // 将当前位置的字符替换为「向前偏移 len 个位置」的字符(即原位置的字符)
        _str[end] = _str[end - len];
        --end; // 向前移动一个位置,继续处理前一个字符
    }

    // 将待插入字符串 str 拷贝到腾出来的 pos 位置
    // 从 _str + pos 开始,拷贝 len 个字符(str 中恰好有 len 个有效字符)
    strncpy(_str + pos, str, len);

    // 更新字符串的有效长度(原长度 + 插入的字符数)
    _size += len;
}

挪动长度为len的两种版本对比:

注意循环的条件!!end是到pos+len的位置停止循环


8 erase

用于删除字符串中指定长度和位置的字符

删除的时候要确保删除的位置是有效字符,判断是否合法

有两种情况:1删除pos后全部的字符 2 删除一部分

思路一:使用strcpy

思路二:使用memcpy

cpp 复制代码
void string::erase(size_t pos, size_t len)
{
   assert(pos < _size);

    // 情况1:删除长度为 npos(通常定义为 -1,无符号下表示最大值),
    // 或删除长度超过「从 pos 到末尾的剩余字符数」(即删除到字符串末尾)
    if (len == npos || len >= _size - pos)
    {
        // 直接将字符串长度截断到 pos 位置(pos 及之后的字符全部删除)
        _size = pos;
        // 在新的末尾添加 '\0',确保字符串符合 C 风格规范(避免后续输出乱码)
        _str[_size] = '\0';
    }
    else
    {
        // 情况2:删除部分字符(未删完,需要将后续字符前移覆盖)
        // 计算需要前移的字符长度:从 pos+len 到原末尾(包含 '\0')的总长度
        // +1 是为了将原末尾的 '\0' 也前移(确保新字符串末尾有 '\0')
        size_t move_len = _size - (pos + len) + 1;

        // 将 pos+len 位置开始的字符,拷贝到 pos 位置(覆盖被删除的部分)
        // 使用 memcpy 比 strcpy 更高效(直接按字节拷贝,无需检查 '\0')
        memcpy(_str + pos, _str + pos + len, move_len);

        // 更新有效长度:原长度减去删除的字符数
        _size -= len;
    }
}

还有一些模拟实现的内容没有讲完,博主放到下一篇中

相关推荐
郝学胜-神的一滴3 小时前
Effective Python 第44条:用纯属性与修饰器取代旧式的 setter 与 getter 方法
开发语言·python·程序人生·软件工程
木子.李3474 小时前
数据结构-算法C++(额外问题汇总)
数据结构·c++·算法
yolo_guo4 小时前
sqlite 使用: 03-问题记录:在使用 sqlite3_bind_text 中设置 SQLITE_STATIC 参数时,处理不当造成的字符乱码
linux·c++·sqlite
程序员莫小特5 小时前
老题新解|计算2的N次方
开发语言·数据结构·算法·青少年编程·信息学奥赛一本通
white-persist6 小时前
XXE 注入漏洞全解析:从原理到实战
开发语言·前端·网络·安全·web安全·网络安全·信息可视化
人生导师yxc6 小时前
Java中Mock的写法
java·开发语言
半路程序员6 小时前
Go语言学习(四)
开发语言·学习·golang
青岛少儿编程-王老师7 小时前
CCF编程能力等级认证GESP—C++5级—20250927
java·数据结构·c++
沐知全栈开发7 小时前
C# 枚举(Enum)
开发语言