目录

一 模拟实现string类的补充

1 resize

2 find

3 substr

4 流提取和流输出

5  getline

6 operator =

 clear

8 其他运算符重载

9 三种swap

​编辑

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

1 传统写法

2 现代写法

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

1 传统写法

2 现代写法

3 二者使用有什么区别?


一 模拟实现string类的补充

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

1 resize

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

​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 查找字符:

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)中的一个字符串处理函数,用于在一个字符串(主串)中查找另一个字符串(子串)的首次出现位置。

// 在当前字符串中查找子串 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 过长则提取到字符串末尾)

// 从当前字符串中提取子串(从 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 流提取和流输出

不一定必须写成友元函数


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

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

此处的s不能加const

//   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

// 从输入流中读取一行字符串,直到遇到分隔符 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

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

清除数据

void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

8 其他运算符重载

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

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 的 “指针、大小、容量” 等成员(类似 “指针交换”),无需深拷贝字符数据。

2. std::swap(string)(针对 string 的特化函数)

  • 形式:void swap(string& x, string& y);
  • 特点:标准库为 string 专门优化的全局 swap 函数(属于对通用 swap 模板的 “特化”)。
  • 效率:与 string::swap 一致(内部通常直接调用 string::swap 实现,因此效率相同)。

3. 通用 std::swap 模板(早期实现,非专用于 string

  • 形式:template <class T> void swap(T& a, T& b);

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

前两个的模拟实现:

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 传统写法

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 现代写法

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 传统写法

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)

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

(2)

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

3 二者使用有什么区别?

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



四 面试中实现string类

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐