首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++】string的使用与模拟实现

【C++】string的使用与模拟实现

作者头像
苏兮
发布2026-01-13 17:40:38
发布2026-01-13 17:40:38
700
举报

string的使用与模拟实现

前言:string 是 C++ 中最常用的类之一,但你真的了解它的底层实现吗?本文将带你快速掌握 string 的核心用法,并深入模拟实现其关键功能,理解深浅拷贝等核心概念。 📖专栏【C++成长之旅】

一、标准库中的string类

首先,对于string类的学习我们也可以参考: 【string类文档介绍】 然后,在此之前,我们在这里学习2个C++11的小语法,方便我们后面的学习:

1.1 auto和范围for

auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

代码语言:javascript
复制
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	return 0;
}

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

代码语言:javascript
复制
int main()
{
	int x = 10;
	auto y = &x;
	auto* z = &x;
	auto& m = x;
	return 0;
}

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

代码语言:javascript
复制
int main()
{
	//编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
	auto cc = 3, dd = 4.0;
	return 0;
}

auto不能作为函数的参数,可以做返回值,但是建议谨慎使用

代码语言:javascript
复制
// 不能做参数
void func2(auto a)
{
}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
	return 3;
}

auto不能直接用来声明数组

代码语言:javascript
复制
int main()
{
	//编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
	auto array[] = { 4, 5, 6 };
	return 0;
}
代码语言:javascript
复制
int main()
{
	// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项,不然无法推导
	auto e;
	return 0;
}

范围for

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。 范围for可以作用到数组和容器对象上进行遍历 范围for的底层很简单,容器遍历实际就是替换为迭代器

对于迭代器简单解释一下:

我们可以把迭代器在行为上看做是指针(因为它模拟指针的用法),但实际上它的实现不一定是指针,而是一个为了统一遍历各种数据结构而设计的抽象工具。

其实这种“统一接口”的特性,正是C++ STL(标准模板库)的基石。(慢慢体会)

代码语言:javascript
复制
#include<iostream>
#include <string>
using namespace std;
int main()
{
	int array[] = { 1, 2, 3, 4, 5 };
	// C++98的遍历
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
		array[i] *= 2;
	}
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
		cout << array[i] << endl;
	}
	// C++11的遍历
	for (auto& e : array)
		e *= 2;
	for (auto e : array)
		cout << e << " " << endl;
	string str("hello world");//用"hello world"构造了一个string
	for (auto ch : str)
	{
		cout << ch << " ";
	}
	cout << endl;
	return 0;
}

有了上面两个的基础,我们在C++中遍历容器就比较方便。

1.2 string类的常用接口说明

对于string类我只讲解最常用的接口,因为各种原因导致string类有些冗余。

  1. string类对象的常见构造

函数名称

功能说明

string() (重点)

构造空的 string 类对象,即空字符串

string(const char* s) (重点)

用 C-string 来构造 string 类对象

string(size_t n, char c)

string 类对象中包含 n 个字符 c

string(const string& s) (重点)

拷贝构造函数

代码语言:javascript
复制
void test()
{
    string s1;              // 构造空的 string 类对象 s1
    string s2("hello world"); // 用 C 格式字符串构造 string 类对象 s2
    string s3(s2);          // 拷贝构造 s3
}

在这里插入图片描述
在这里插入图片描述
  1. string类对象的容量操作

函数名称

功能说明

size (重点)

返回字符串有效字符长度

length

返回字符串有效字符长度

capacity

返回空间总大小

empty (重点)

检测字符串是否为空串,是返回 true,否则返回 false

clear (重点)

清空有效字符

reserve (重点)

为字符串预留空间**

resize (重点)

将有效字符的个数改成 n 个,多出的空间用字符填充

说明: size()与length(): 方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。 clear(): 只是将string中有效字符清空,不改变底层空间大小。 resize(size_t n) 与 resize(size_t n, char c): 都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。 reserve(size_t res_arg=0): 为string预留空间,不改变有效元素个数,但是reserve的参数小于string的底层空间总大小时,reserver会不会缩容是不确定的。 我们来看官方说法:

在这里插入图片描述
在这里插入图片描述

不确定的,也就是说reserve的参数小于string的底层空间总大小时,reserver会不会缩容是不确定的。但是:

在这里插入图片描述
在这里插入图片描述

此函数不会影响字符串长度,也无法修改其内容。其实可以看出来reserve()函数还是很温柔的,它只是建议保留n个空间。

  1. string类对象的访问及遍历操作

函数名称

功能说明

operator[] (重点)

返回 pos 位置的字符,const string 类对象调用

begin + end

begin 获取第一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 (左闭右开)

rbegin + rend

rbegin 获取最后一个字符的迭代器,rend 获取第一个字符前一个位置的迭代器

范围for (C++11)

C++11 支持更简洁的范围 for 的新遍历方式

代码语言:javascript
复制
#include<string>
#include<iostream>
using namespace std;
int main()
{
    string str = "hello world";

    // 1. operator[] 访问
    for (size_t i = 0; i < str.size(); ++i)
    {
        cout << str[i] << " ";
    }
    cout << endl;

    // 2. 迭代器遍历
    for (auto it = str.begin(); it != str.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;

    // 3. 反向迭代器遍历
    for (auto rit = str.rbegin(); rit != str.rend(); ++rit)
    {
        cout << *rit << " ";
    }
    cout << endl;

    // 4. 范围for遍历 (C++11)
    for (char ch : str)
    {
        cout << ch << " ";
    }
    cout << endl;

    return 0;
}

在这里插入图片描述
在这里插入图片描述
  1. string类对象的修改操作

函数名称

功能说明

push_back

在字符串后尾插字符 c

append

在字符串后追加一个字符串

operator+= (重点)

在字符串后追加字符串 str

c_str (重点)

返回 C 格式字符串 (const char* 类型)

find + npos (重点)

从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置

rfind

从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置

substr

在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回

代码语言:javascript
复制
std::string str = "hello";

// 1. push_back - 尾插字符
str.push_back('!');  // str becomes "hello!"

// 2. append - 追加字符串
str.append(" world");  // str becomes "hello! world"

// 3. operator+= - 追加字符串(更常用)
str += " C++";  // str becomes "hello! world C++"

// 4. c_str - 返回C格式字符串
const char* cstr = str.c_str();  // 可用于C语言接口

// 5. find - 查找字符/字符串
size_t pos = str.find('o');  // 返回第一个'o'的位置
if (pos != std::string::npos) 
{  // npos表示未找到
    std::cout << "Found at: " << pos << std::endl;
}

// 6. rfind - 反向查找
size_t rpos = str.rfind('o');  // 从后往前找第一个'o'

// 7. substr - 截取子串
std::string sub = str.substr(0, 5);  // 截取前5个字符:"hello"

对于npos解释:

在这里插入图片描述
在这里插入图片描述

它的字面意思是 “not a position” 或 “no position”,用于表示无效的或未找到的位置。 由于 size_t 是无符号类型,-1 会变成该类型能表示的最大值,即0xFFFFFFFF,我们的string的大小不可能是npos,就用npos表示无效的或未找到的位置 简单来说,npos 就是一个官方定义的、用来判断字符串查找操作是否成功的“失败标志”。

注意(习惯):

  1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
  3. string类非成员函数

函数名称

功能说明

operator+

字符串拼接,但尽量少用,因为传值返回导致深拷贝,效率低

operator>> (重点)

输入运算符重载(用于从流中提取字符串)

operator<< (重点)

输出运算符重载(用于将字符串插入到流中)

getline (重点)

获取一行字符串(可指定分隔符)

relational operators (重点)

大小比较(包括 ==, !=, <, <=, >, >= 等)

代码语言:javascript
复制
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s1 = "Hello";
    string s2 = "World";

    // 1. operator+ (不推荐频繁使用)
    string s3 = s1 + " " + s2; // 产生临时对象,深拷贝效率低
    cout << s3 << endl; // Output: Hello World

    // 2. operator>> 输入
    string input;
    cout << "Please enter a string: ";
    cin >> input; // 遇到空格停止
    cout << "You entered: " << input << endl;

    // 3. operator<< 输出
    cout << s1 << " " << s2 << endl; // Output: Hello World

    // 4. getline 获取一行(推荐)
    cin.ignore(); // 清除输入缓冲区
    cout << "Please enter a line: ";
    getline(cin, input); // 读取整行,包括空格
    cout << "You entered: " << input << endl;

    // 5. relational operators 比较
    if (s1 == s2) {
        cout << "Strings are equal" << endl;
    } else if (s1 < s2) {
        cout << s1 << " is less than " << s2 << endl;
    } else {
        cout << s1 << " is greater than " << s2 << endl;
    }

    return 0;
}

对于string类的接口还有很多,上面的几个接口大家了解一下,后续慢慢练习就熟悉了。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查前面说过的文档即可。

二、string类的模拟实现

2.1 string.h

上面已经对string类进行了简单的介绍,我们只要能够正常使用即可。但是在面试中,面试官总喜欢让我们自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。 string的底层我们可以看作是:

在这里插入图片描述
在这里插入图片描述

当然,也可以更为简化,但是,我们为了方便书写代码,就可以像我这种,下面我们来完成string类的实现:

可以根据下面头文件中的声明来试着实现一下: string.h:

代码语言:javascript
复制
class string
{
    friend std::ostream& operator<<(std::ostream& _out, const sxn::string& s);
    friend std::istream& operator>>(std::istream& _in, sxn::string& s);
public:
    //迭代器重命名
    typedef char* iterator;
public:
    string(const char* str = "");
    string(const string& s);
    //string& operator=(const string& s);
    string& operator=(string s);
    ~string();
    // iterator
    iterator begin();
    iterator end();
    // modify
    void swap(string& s);
    void push_back(char c);
    string& operator+=(char c);
    void append(const char* str);
    string& operator+=(const char* str);
    void clear();
    void swap(string& s);
    const char* c_str()const;
    // capacity
    size_t size()const;
    size_t capacity()const;
    bool empty()const;
    void resize(size_t n, char c = '\0');
    void reserve(size_t n);
    // access
    char& operator[](size_t index);
    const char& operator[](size_t index)const;
    //relational operators
    bool operator<(const string& s);
    bool operator==(const string& s);
    bool operator<=(const string& s);
    bool operator>(const string& s);
    bool operator>=(const string& s);
    bool operator!=(const string& s);
    // 返回c在string中第一次出现的位置
    size_t find(char c, size_t pos = 0) const;
    // 返回子串s在string中第一次出现的位置
    size_t find(const char* s, size_t pos = 0) const;
    // 在pos位置上插入字符c/字符串str,并返回该字符的位置
    string& insert(size_t pos, char c);
    string& insert(size_t pos, const char* str);
    // 删除pos位置上的元素,并返回该元素的下一个位置
    string& erase(size_t pos, size_t len);

private:
    char* _str = NULL;
    size_t _capacity = 0;
    size_t _size = 0;
};
2.2浅拷贝与深拷贝

这里就有个问题:拷贝构造和赋值重载需要我们自己实现吗?

在这里插入图片描述
在这里插入图片描述

回答是,要,必须要。

这里就涉及到了深浅拷贝的问题,前面的文章也说过了,但是都没有实操的机会,这里真好赶上了,就展开再说说,就以拷贝构造为例,假如是编译器自己生成的,就是浅拷贝/值拷贝,会导致:

在这里插入图片描述
在这里插入图片描述

这是我们想要的结果吗,不是,它还会导致在调用析构函数的时候,会调用两次,出现问题。 所以这里我们就要实现深拷贝,深拷贝之后要达到的效果:

在这里插入图片描述
在这里插入图片描述

其实也很好理解,因为编译器不知道你这里到底指向资源还是干什么,它只知道在这块空间上放着_str、_size、_capacity这三个,所以它只能完成浅拷贝。

有了深浅拷贝的理解,这些代码就很好完成了。

2.3 string.cpp
代码语言:javascript
复制
#include"string.h"

const size_t string::npos = -1;

//构造函数
string::string(const char* str)
    :_str(new char[strlen(str) + 1])
{
    _size = strlen(str);
    strcpy(_str, str);
    _capacity = _size;
    // std::cout << "string(const char* str)" << std::endl;
}
//拷贝构造函数
/*string::string(const string& s)
{
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}*/

//现代写法
string::string(const string& s)
{
    string tmp(s._str);
    swap(tmp);
}

//析构函数
string::~string()
{
    delete[] _str;
    _str = nullptr;
    _size = _capacity;
    //std::cout << "~string()" << std::endl;
}
// iterator
string::iterator string::begin()
{
    return _str;
}
string::iterator string::end()
{
    return _str + _size;
}
//赋值
/*string& string::operator=(const string& s)
{
    delete[] _str;
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
    return *this;
}*/
//现代写法
/*string& string::operator=(const string& s)
{
    string tmp(s._str);
    swap(tmp);
    return *this;
}*/
string& string::operator=(string s)
{
    swap(s);
    return *this;
}

// modify
//尾插
void string::push_back(char c)
{
    reserve(_size + 1);
    _str[_size++] = c;
}
string& string::operator+=(char c)
{
    push_back(c);
    return *this;
}
//追加
void string::append(const char* str)
{
    int len = strlen(str);
    reserve(_size + len);
    strcpy(_str + _size, str);
    _size = _size + len;
}
string& string::operator+=(const char* str)
{
    append(str);
    return *this;
}
void string::clear()
{
    _size = 0;
    _str[0] = '\0';
}
void string::swap(string& s)
{
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}
const char* string::c_str()const
{
    return _str;
}
/////////////////////////////////////////////////////////////
// capacity
size_t string::size()const
{
    return _size;
}
size_t string::capacity()const
{
    return _capacity;
}
bool string::empty()const
{
    return _size == 0;
}
void string::resize(size_t n, char c)
{
    if (n <= _size)
    {
        _str[n] = '\0';
        _size = n;
    }
    else
    {
        reserve(n);
        while (_size < n)
        {
            _str[_size++] = c;
        }
        _str[_size] = '\0';
    }
}
void string::reserve(size_t n)
{
    static size_t count = 0;
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
        std::cout << "reserve->" << ++count << std::endl;
    }
}
/////////////////////////////////////////////////////////////
// access
char& string::operator[](size_t index)
{
    assert(index < _size);
    return _str[index];
}
const char& string::operator[](size_t index)const
{
    assert(index < _size);
    return _str[index];
}
/////////////////////////////////////////////////////////////
//relational operators
bool string::operator<(const string& s)
{
    int i = 0;
    int j = 0;
    while (i < _size && j < s._size)
    {
        if (_str[i++] >= s._str[j++])
        {
            return false;
        }
    }
    if (i < _size && j == s._size)
        return false;
    return true;
}
bool string::operator==(const string& s)
{
    int i = 0;
    int j = 0;
    while (i < _size && j < s._size)
    {
        if (_str[i++] != s._str[j++])
        {
            return false;
        }
    }
    if (i != _size || j != s._size)
        return false;
    return true;
}
bool string::operator<=(const string& s)
{
    return *this == s || *this < s;
}
bool string::operator>(const string& s)
{
    return !(*this <= s);
}
bool string::operator>=(const string& s)
{
    return *this == s || *this > s;
}

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

// 返回c在string中第一次出现的位置
size_t string::find(char c, size_t pos) const
{
    if (pos >= _size)
        return npos;
    while (pos < _size)
    {
        if (_str[pos] == c)
            return pos;
    }
    return npos;
}
// 返回子串s在string中第一次出现的位置
size_t string::find(const char* s, size_t pos) const
{
    char* str = strstr(_str + pos, s);
    if (str == nullptr)
    {
        return npos;
    }
    return str - _str;
}
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& string::insert(size_t pos, char c)
{
    assert(pos <= _size);
    reserve(_size + 1);
    size_t i = _size + 1;
    while (i > pos)
    {
        _str[i] = _str[i - 1];
        --i;
    }
    _str[pos] = c;
    ++_size;
    return *this;
}
string& string::insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    size_t len = strlen(str);
    if (len == 0)
        return *this;
    reserve(_size + len);
    size_t i = _size + len;

    while (i >= pos + len)
    {
        _str[i] = _str[i - len];
        --i;
    }

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

// 删除pos位置上的元素,并返回该元素的下一个位置
string& string::erase(size_t pos, size_t len)
{
    assert(pos < _size);
    if (pos + len >= _size)
    {
        _str[pos] = '\0';
        _size = pos;
    }
    else
    {
        int i = 0;
        while (len <= _size)
        {
            _str[pos + i] = _str[pos + len];
            i++;
            len++;
        }
        _size = pos + i - 1;
    }
    return *this;
}

//输入输出
std::ostream& operator<<(std::ostream& _out, const sxn::string& s)
{
    int n = 0;
    while (n < s._size)
    {
        _out << s._str[n++];
    }
    return _out;
}

/*  std::istream& operator>>(std::istream& _in, sxn::string& s)
  {
      s.clear();
      char ch = (char)_in.get();
      while (ch != '\n')
      {
          s.reserve(++s._size);
          s._str[s._size-1] = ch;
          s._str[s._size] = '\0';//不写这个reserve函数中的strcpy()拷贝就会出错
          ch = _in.get();
      }
      s._str[s._size] = '\0';
      return _in;
  }*/
std::istream& operator>>(std::istream& _in, sxn::string& s)
{
    s.clear();
    //用一个数组做缓冲,减少开辟内存的时间
    const int N = 5;
    char buffer[N];
    //要使用get函数才行,因为cin以及输入时将空格忽视
    int ch = _in.get();
    int i = 0;
    while (ch != '\n')
    {
        //先写入buffer
        if (i < N)
        {
            buffer[i++] = ch;
        }
        else
        {
            s.reserve(s._size + N);
            strncpy(s._str + s._size, buffer, N);
            s._size += N;
            s._str[s._size] = '\0';
            i = 0;
        }
        ch = _in.get();
    }
    if (i > 0)
    {
        s.reserve(s._size + i + 1);
        strncpy(s._str + s._size, buffer, i + 1);
        s._size += i + 1;
        s._str[s._size] = '\0';
    }
    return _in;
}

在这里再提一个点:

在这里插入图片描述
在这里插入图片描述

可以看看区别,巧妙:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

函数之后,s自动析构,只能说,完美。

对于string类的模拟实现不仅仅是一个编程练习,更是理解C++面向对象、资源管理和运算符重载等核心概念的绝佳范例,要时间都可以练习练习。

三、简单总结

记住,真正掌握一个类,不仅要会用,更要知其所以然。正是我们迈向C++高手之路的重要一步!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • string的使用与模拟实现
    • 一、标准库中的string类
      • 1.1 auto和范围for
      • 1.2 string类的常用接口说明
    • 二、string类的模拟实现
      • 2.1 string.h
      • 2.2浅拷贝与深拷贝
      • 2.3 string.cpp
    • 三、简单总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档