【C++】深入理解string类(5)

目录

[一 模拟实现string类的补充](#一 模拟实现string类的补充)

[1 resize](#1 resize)

[2 find](#2 find)

[3 substr](#3 substr)

[4 流提取和流输出](#4 流提取和流输出)

[5 getline](#5 getline)

[6 operator =](#6 operator =)

clear

[8 其他运算符重载](#8 其他运算符重载)

[9 三种swap](#9 三种swap)

​编辑

[二 string拷贝构造的传统写法和现代写法](#二 string拷贝构造的传统写法和现代写法)

[1 传统写法](#1 传统写法)

[2 现代写法](#2 现代写法)

[三 运算符重载(=)的传统写法和现代写法](#三 运算符重载(=)的传统写法和现代写法)

[1 传统写法](#1 传统写法)

[2 现代写法](#2 现代写法)

[3 二者使用有什么区别?](#3 二者使用有什么区别?)


一 模拟实现string类的补充

之前写的string类如下:【c++】深入理解string类(4)

1 resize

功能是将字符串的有效长度(_size)修改为指定值 n,并根据需要填充新字符或截断字符串

cpp 复制代码
​void string::resize(size_t n, char ch)
	{
		if (n <= _size)
		{
			// 删除,保留前n个
			_size = n;
			_str[_size] = '\0';
		}
		else
		{
			reserve(n);
         //调用 reserve(n) 预分配至少能容纳 n 个字符的内存(避免后续填充时频繁扩容)
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_size = n;
			_str[_size] = '\n';
		}
	}

​

2 find

查找字符或子串出现的位置:

1 查找字符:

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

		return npos;
	}

2 查找子串:

strstr 是 C 语言标准库(string.h)中的一个字符串处理函数,用于在一个字符串(主串)中查找另一个字符串(子串)的首次出现位置。

cpp 复制代码
// 在当前字符串中查找子串 str 的第一次出现位置
// 参数:
//   str:待查找的 C 风格字符串(以 '\0' 结尾)
//   pos:起始查找位置(从 pos 开始向后查找,默认从 0 开始)
// 返回值:找到时返回子串首字符在当前字符串中的位置;未找到返回 npos(通常定义为 -1)
size_t string::find(const char* str, size_t pos)
{
  
    assert(pos < _size);

    // strstr(a, b) 功能:在字符串 a 中查找子串 b 第一次出现的位置,返回指向该位置的指针;
    // 若未找到,返回 nullptr。
    // 这里从 _str + pos 开始查找(即从当前字符串的 pos 位置向后)
    const char* ptr = strstr(_str + pos, str);

    if (ptr)
    {
        // 若找到子串,计算其在当前字符串中的位置:
        // 指针 ptr 减去当前字符串的起始地址 _str,得到偏移量(即位置)
        return ptr - _str;  // 注意:原代码中写的是 ptr - str,这里修正为 ptr - _str(原代码可能笔误)
    }
    else
    {
        // 未找到子串,返回 npos(表示"不存在的位置")
        return npos;
    }
}

3 substr

功能:从 pos 位置开始,提取长度为 len 的子串(若 len 过长则提取到字符串末尾)

cpp 复制代码
// 从当前字符串中提取子串(从 pos 位置开始,长度为 len)
// 参数:
//   pos:子串的起始位置(必须小于当前字符串长度 _size)
//   len:子串的长度(默认或传入 npos 时,表示提取到字符串末尾)
// 返回值:提取出的子串(string 对象)
string string::substr(size_t pos, size_t len)
{
    assert(pos < _size);

    // 处理 len 过长或为 npos 的情况:
    // 若 len 是 npos,或 len 超过从 pos 到末尾的剩余字符数,则将 len 修正为剩余字符数
    if (len == npos || len > _size - pos)
    {
        len = _size - pos;  // 剩余字符数 = 总长度 - 起始位置
    }

    // 创建一个空的 string 对象,用于存储子串
    string sub;

    // 提前为子串预留足够的空间(容量设为 len),避免循环中频繁扩容
    sub.reserve(len);

    // 从 pos 位置开始,拷贝 len 个字符到子串中
    for (size_t i = 0; i < len; i++)
    {
        sub += _str[pos + i];  // 逐个字符追加到子串(利用 operator+= 操作)
    }

    // 返回提取到的子串
    return sub;
}

4 流提取和流输出

不一定必须写成友元函数

cpp 复制代码
//   out:输出流对象(如 cout)
//   s:要打印的自定义 string 对象(加 const 确保不修改原对象)
std::ostream& operator<<(std::ostream& out, const string& s)
{
    for (auto ch : s)
    {
        out << ch;  // 将每个字符逐个输出到流中
    }

    return out;  // 返回输出流,支持链式操作
}

此处的s不能加const

cpp 复制代码
//   in:输入流对象(如 cin)
//   s:接收输入的 string 对象(非 const,需修改其内容)
// 返回值:输入流对象(支持链式调用,如 cin >> s1 >> s2)
std::istream& operator>>(std::istream& in, string& s)
{
    s.clear();  // 先清空原字符串,避免输入内容追加到原有数据后

    char buff[256];  // 临时缓冲区,每次最多存 255 个字符(+1 个 '\0')
    int i = 0;       // 缓冲区当前填充位置

    char ch;
    // 用 in.get() 读取字符(相比 >> 运算符,get() 会读取空格和换行符,此处用于自定义终止条件)
    ch = in.get();

    // 循环读取字符,直到遇到换行符 '\n' 或空格 ' '(默认以空白字符作为输入结束标志)
    while (ch != '\n' && ch != ' ')
    {
        buff[i++] = ch;  // 将字符存入缓冲区

        // 若缓冲区已满(i=255,此时 buff[0..254] 已填满)
        if (i == 255)
        {
            buff[i] = '\0';  // 手动添加字符串结束符
            s += buff;       // 将缓冲区内容追加到 s 中
            i = 0;           // 重置缓冲区索引,准备接收下一批字符
        }

        ch = in.get();  // 继续读取下一个字符
    }

    // 循环结束后,若缓冲区中还有未处理的字符(i > 0)
    if (i > 0)
    {
        buff[i] = '\0';  // 添加结束符
        s += buff;       // 追加到 s 中
    }

    return in;  // 返回输入流,支持链式操作
}

5 getline

cpp 复制代码
// 从输入流中读取一行字符串,直到遇到分隔符 delim 为止
// 参数:
//   in:输入流对象(如 cin)
//   s:接收读取内容的 string 对象
//   delim:自定义分隔符(默认通常是 '\n',即换行符)
// 返回值:输入流对象(支持链式操作)
std::istream& getline(std::istream& in, string& s, char delim)
{
    s.clear();  // 清空目标字符串,确保读取的是新内容(而非追加到原有数据后)

    char buff[256];  // 临时缓冲区,每次最多存储 255 个字符(预留 1 个位置给 '\0')
    int i = 0;       // 缓冲区当前填充的字符索引

    char ch;
    // 用 in.get() 读取单个字符(包括空格、制表符等空白字符,不会像 >> 那样自动跳过)
    ch = in.get();

    // 循环读取字符,直到遇到指定的分隔符 delim 为止
    while (ch != delim)
    {
        // 将读取到的字符存入缓冲区
        buff[i++] = ch;

        // 若缓冲区已满(i=255,此时 buff[0] 到 buff[254] 已存满)
        if (i == 255)
        {
            buff[i] = '\0';  // 手动添加字符串结束符,确保 buff 是合法的 C 风格字符串
            s += buff;       // 将缓冲区内容追加到目标 string 对象 s 中
            i = 0;           // 重置缓冲区索引,准备接收下一批字符
        }

        // 继续读取下一个字符
        ch = in.get();
    }

    // 循环结束后,若缓冲区中还有未处理的字符(i > 0)
    if (i > 0)
    {
        buff[i] = '\0';  // 添加结束符
        s += buff;       // 将剩余字符追加到 s 中
    }

    return in;  // 返回输入流对象,支持链式调用(如 getline(cin, s1) >> s2)
}

// 定义 string 类的静态成员 npos(表示"无效位置"或"到末尾")
// npos 通常被定义为 size_t 的最大值(-1 转换为无符号类型后即为最大值)
const size_t string::npos = -1;

6 operator =

赋值操作

将s3赋值给s1,此处是深拷贝。开辟一个和s3一样大的空间,s1指向该空间,释放s1原本的旧空间,将s3的内容拷贝给s1

cpp 复制代码
string& string::operator=(const string& s)
	{
		if (this != &s)
		{
			char* tmp = new char[s._capacity + 1];
			//strcpy(tmp, s._str);
			memcpy(tmp, s._str, s._size + 1);

			delete[] _str;
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;
		}

		return *this;
	}

注意区分拷贝构造函数和赋值运算符重载:

但是上面的代码还是有bug:

如果在strcpy拷贝的时候,遇到\0,就会直接停止拷贝(例如:hello world\0yyy\0),如果是在字符串的中间有\0,那么就会造成拷贝的不完全,所以不能使用strcpy,而是用memcpy

相应的,在使用strcpy的部分都换成memecpy

所以,上面的拷贝构造就优化为:


clear

清除数据

cpp 复制代码
void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

8 其他运算符重载

实现了一个后面的就可以复用

cpp 复制代码
bool string::operator<(const string& s) const
	{
		return strcmp(_str, s._str) < 0;
	}

	bool string::operator<=(const string& s) const
	{
		return *this < s || *this == s;
	}

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

	bool string::operator>=(const string& s) const
	{
		return !(*this > s);
	}

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

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

9 三种swap

  1. std::string::swap(成员函数)
  • 形式:void swap(string& str);
  • 特点:string 类自身的成员函数,用于交换当前 string 对象与参数 str 的内容
  • 效率:极高 。因为内部只需交换 string 的 "指针、大小、容量" 等成员(类似 "指针交换"),无需深拷贝字符数据。
  1. std::swap(string)(针对 string 的特化函数)
  • 形式:void swap(string& x, string& y);
  • 特点:标准库为 string 专门优化的全局 swap 函数(属于对通用 swap 模板的 "特化")。
  • 效率:string::swap 一致 (内部通常直接调用 string::swap 实现,因此效率相同)。
  1. 通用 std::swap 模板(早期实现,非专用于 string
  • 形式:template <class T> void swap(T& a, T& b);

第三个是标准库里的swap,在实现的时候可能会造成三次深拷贝(string使用时),代价太大

前两个的模拟实现:

cpp 复制代码
template <class T>
void swap(T& a, T& b) {
    T c(a); // 1. 拷贝构造临时对象 c,存储 a 的值
    a = b;  // 2. 把 b 的值赋给 a
    b = c;  // 3. 把临时对象 c(原 a 的值)赋给 b
}

inline void swap(string& a, string& b) {
    a.swap(b); // 调用 string 自身的 swap 成员函数
}

二 string拷贝构造的传统写法和现代写法

1 传统写法

cpp 复制代码
string::string(const string& s)
{
    _str = new char[s._capacity + 1];       // 分配新内存
    memcpy(_str, s._str, s._size + 1);      // 拷贝字符串(包括'\0')
    _size = s._size;                        // 同步长度
    _capacity = s._capacity;                // 同步容量
}

2 现代写法

cpp 复制代码
string::string(const string& s)
{
    string tmp(s._str);  // 先构造临时对象(利用已有的构造逻辑)
    swap(tmp);           // 与临时对象交换资源(指针、大小、容量)
}

代码解析:

(1)

复制代码
string tmp(s._str);  // 构造临时对象 tmp

这里的逻辑是:

  1. 利用源对象的底层字符串s._str 是源对象 s 中存储字符串的字符数组指针(C 风格字符串,以 '\0' 结尾)。
  2. 调用 const char* 构造函数string 类通常有一个接收 const char* 类型参数的构造函数(带参构造函数),其作用是根据 C 风格字符串初始化 string 对象(分配内存、拷贝字符串内容、设置 _size_capacity)。
  3. 临时对象 tmp 的状态 :通过 s._str 构造的 tmp,其内部的 _str 指针指向一块新分配的内存,存储的字符串内容与 s._str 完全相同,且 _size_capacity 也与 s 一致(因为拷贝了相同的字符串)。

为什么要这一步?

  • 复用已有逻辑const char* 构造函数已经实现了 "根据字符串内容分配内存、拷贝数据、初始化成员变量" 的完整逻辑。通过构造 tmp,可以直接复用这部分代码,避免在拷贝构造函数中重复编写相同的逻辑(减少冗余,降低出错风险)。
  • 为后续交换做准备tmp 此时已经是一个与源对象 s 内容完全一致的 "副本",接下来只需要通过 swap 函数,将 tmp 的资源(新分配的内存、大小、容量)转移给当前正在构造的对象即可。

(2)

string 类的 swap 成员函数的作用是将当前对象(this 指向的对象)与另一个对象的资源进行交换。它的函数原型通常是:

复制代码
void swap(string& other);  // 成员函数,仅需一个参数
  • 调用者是当前对象(*this),参数 other 是要交换的另一个对象。
  • 函数内部通过交换两者的 _str 指针、_size_capacity 等成员,完成资源互换。

三 运算符重载(=)的传统写法和现代写法

1 传统写法

cpp 复制代码
string& string::operator=(const string& s)
{
 if(*this != &s)
 {
   char* tmp = new char[s.capacity+1];
   memcpy(tmp,s._str,s._szie+1);
   detlete[] _str;
   _str = tmp;
   _size = s._size;
   _capacity = s._capacity;
 }
 return *this;
}

2 现代写法

(1)

cpp 复制代码
string& string::operator=(const string& s)
{
    if (this != &s) // 防止自赋值(如 s1 = s1)
    {
        string tmp(s._str); // 用 s 的 _str 构造临时对象 tmp
        swap(tmp); // 交换当前对象与 tmp 的资源
    }
    return *this;
}

(2)

cpp 复制代码
string& string::operator=(string tmp) // 注意:参数是值传递,会先拷贝构造 tmp
{
    swap(tmp); // 交换当前对象与 tmp 的资源
    return *this;
}
  • 逻辑
    • 利用 值传递的特性 :调用这个函数时,编译器会自动拷贝一份实参(即要赋值的对象)到 tmp 中(这一步是 "拷贝构造")。
    • 直接交换当前对象与 tmp 的资源,tmp 销毁时会带走当前对象原来的旧资源,当前对象则获得 tmp 拷贝来的新资源。
    • 这种写法连 "自赋值判断" 都省略了 :因为值传递的 tmp 是独立拷贝,即使自赋值,交换后也不影响正确性(只是多一次拷贝)。

3 二者使用有什么区别?

传统写法和现代写法的算法效率是一样的,只是现代写法的代码较短,代码写法不同,充分利用了复用,本质上区别不大



四 面试中实现string类

相关推荐
Dontla21 小时前
React惰性初始化函数(Lazy Initializer)(首次渲染时执行一次,只执行一次,应对昂贵初始化逻辑)(传入一个函数、传入函数)
前端·javascript·react.js
地平线开发者21 小时前
新版 perf 文件解读与性能分析
算法·自动驾驶
lypzcgf21 小时前
FastbuildAI新建套餐-前端代码分析
前端·智能体平台·ai应用平台·agent平台·fastbuildai
lingran__21 小时前
算法沉淀第五天(Registration System 和 Obsession with Robots)
c++·算法
chrispang21 小时前
浅谈 Tarjan 算法
算法
莱茶荼菜21 小时前
一个坐标转换
c++·算法
豆沙沙包?21 小时前
2025年--Lc187--120. 三角形最小路径和(多维动态规划,矩阵)--Java版
java·矩阵·动态规划
南囝coding1 天前
Claude Code 插件系统来了
前端·后端·程序员
左灯右行的爱情1 天前
ImportCandidates 类详细解析
java·spring boot
摇滚侠1 天前
Spring Boot 3零基础教程,WEB 开发 默认的自动配置,笔记25
前端·spring boot·笔记