【C++】深入浅出:string类模拟实现全解析

发布于:2025-09-04 ⋅ 阅读:(19) ⋅ 点赞:(0)

1 实现一个简单的string

首先,我们实现一个最简单的 string 类,它只包含一个指向动态字符数组的指针 _str

1.1 简单 string 类框架

namespace practice_string {
    class string {
    public:
        // 构造函数:使用C字符串初始化
        string(const char* str = "")
            : _str(new char[strlen(str) + 1]) // 多分配1个字节存放'\0'
        {
            strcpy(_str, str);
        }

        // 析构函数:释放动态分配的内存
        ~string() {
            delete[] _str;
            _str = nullptr;
        }

        // 获取字符串长度(不包含'\0')
        size_t size() const {
            return strlen(_str);
        }

        // 重载[]运算符,支持像数组一样访问字符
        char& operator[](size_t i) {
            return _str[i];
        }

        // 获取C风格字符串(只读)
        const char* c_str() const {
            return _str;
        }

    private:
        char* _str; // 核心:指向动态字符数组的指针
    };
}

关键点:

  1. 构造函数:参数使用 const char* 而不是 char*char* 无法传入常量字符串,一旦传入常量字符串,就相当于 char* _str = const char* str。这是访问权限的放大,会导致报错。(访问权限的缩放规则对指针和引用生效)

  2. 内存管理:在堆上(new[])分配内存,并在析构时释放(delete[]),这是管理动态资源的类的典型特征。

  3. 默认参数:const char* str = "" 使得默认构造函数和带参构造函数合二为一。

1.2 拷贝构造

        假设我们自己不写拷贝函数,编译器会调用自动生成的拷贝函数(浅拷贝)。

        编译器默认生成的「拷贝构造」和「赋值重载」是浅拷贝(仅拷贝指针值,不拷贝指针指向的内容),会导致严重问题:两个对象的 _str 指向同一块内存,析构时会「重复释放同一块空间」,触发程序崩溃。程序如下:

void test_string() {
    string s1("hello");
    string s2(s1); // 编译器默认浅拷贝:s2._str = s1._str(同一块内存)
} 
// 函数结束时:s2先析构(释放内存),s1再析构(释放已释放的内存→崩溃)

详细解析:

        浅拷贝也称之为“值拷贝”,会把一块空间中的数据直接拷贝到另一块空间。也就是说,s1中 _str 指针的值被直接赋给了s2中的 _str 指针,这两个 string 对象的 _str 指针指向的是同一块空间,当函数退出时,s1 和 s2 结束生命周期,调用析构函数,就会将 _str 指向的空间释放 2 次,而一块空间是不能被重复释放的,所以导致了报错。

        而与“浅拷贝”对应的,“深拷贝”可以解决这个问题。

        深拷贝的核心逻辑:为新对象重新分配一块独立的内存,再拷贝原对象的内容,让两个对象的 _str 指向不同内存,彼此独立。

深拷贝版拷贝构造:

string(const string& s) 
    : _str(new char[strlen(s._str) + 1]) { // 为s2新分配内存
    strcpy(_str, s._str); // 拷贝s1的内容到新内存
}

1.3 深拷贝版赋值重载

需注意两个细节:

① 防止「自赋值」(如 s1 = s1);

② 先释放原内存,再指向新内存(避免内存泄漏)。

string& operator=(const string& s) 
{
    // 1. 防止自赋值:若自己赋值给自己,直接返回(避免释放自身内存后拷贝)
    if (this != &s) { 
        // 2. 先分配新内存,拷贝内容(若new失败,原内存不会被破坏)
        char* tmp = new char[strlen(s._str) + 1];
        strcpy(tmp, s._str);

        // 3. 释放原内存,指向新内存
        delete[] _str;
        _str = tmp;
    }
    return *this; // 支持链式赋值(如 s1 = s2 = s3)
}

2. 支持增删查改的 string 类

基础版 string 仅满足简单需求,实际使用需「动态扩容、尾插、插入、删除、查找」等功能。此时需新增两个成员变量:

  • _size:有效字符个数(不包含 '\0'),替代 strlen(避免每次调用都遍历字符串,提高效率);

  • _capacity:当前内存可容纳的最大有效字符数(不包含 '\0'),用于动态扩容管理。

2.1 框架与核心接口

namespace practice_string {
    class string {
    public:
        // 基础接口(复用+优化)
        size_t size() const { return _size; }
        size_t capacity() const { return _capacity; }
        char& operator[](size_t i) { assert(i < _size); return _str[i]; } // 越界检查
        const char* c_str() const { return _str; }

        // 核心功能:扩容、尾插、插入、删除、查找
        void reserve(size_t n);                // 扩容(仅扩大容量,不改变有效字符)
        void resize(size_t n, char ch = '\0'); // 调整size(可补字符)
        void push_back(char ch);               // 尾插单个字符
        void append(const char* str);          // 尾插字符串
        string& insert(size_t pos, const char ch); // 插入字符
        string& erase(size_t pos, size_t len); // 删除字符
        size_t find(const char ch, size_t pos = 0); // 查找字符

    private:
        char* _str;
        size_t _size;             // 有效字符数
        size_t _capacity;         // 容量(最大有效字符数)
        static const size_t npos; // 静态常量:表示“未找到”(值为-1,size_t最大值)
    };
    // 静态成员初始化(类外)
    const size_t string::npos = -1;
}

2.2 构造函数与析构函数

/* 默认构造函数 */
string(const char* str = "")
{
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];        // 多加一个空间给'\0'
        strcpy(_str, str);
}

/* 拷贝构造函数 */
string(const string& s)
{
        _size = s._size;
        _capacity = _size;
        _str = new char[_capacity + 1];        // 多加一个空间给'\0'
        strcpy(_str, s._str);
}

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

2.3 扩容(reserve)

  • 作用:仅当 n > _capacity 时,扩大内存容量(避免频繁扩容,提高效率);

  • 细节:扩容后需拷贝原字符串内容,释放原内存。

void practice_string::reserve(size_t n) {
    if (n > _capacity) { // 仅当需要的容量大于当前容量时才扩容
        char* tmp = new char[n + 1]; // 多1字节存'\0'
        strcpy(tmp, _str); // 拷贝原内容
        delete[] _str;     // 释放原内存
        _str = tmp;        // 指向新内存
        _capacity = n;     // 更新容量
    }
}

2.4 赋值重载

/* 赋值重载函数 */
string& operator=(const string& s)
{
        if (this != &s)
        {
                // 开辟一块新的空间,拷贝数据过去
                char* tmp = new char[s._capacity + 1];
                strcpy(tmp, s._str);
                // 释放原来的空间防止内存泄露,指向新空间
                delete[] _str;
                _str = tmp;
        }
        return *this;
}
string& operator=(const char* str)
{
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];        // 多加一个空间给'\0'
        strcpy(_str, str);
        return *this;
}

2.5 迭代器

/* 迭代器 */
typedef char* iterator;

iterator begin()
{
        return _str;
}

iterator end()
{
        return _str + _size;
}

2.6 resize

  • 作用:同时管理「容量」和「有效字符数」:

    • n < _size:截断字符串(仅修改 _size,不释放内存);

    • n > _size:先扩容(若需),再用 ch 补全新增的位置。

void practice_string::resize(size_t n, char ch) {
    if (n < _size) // 截断:直接在n位置放'\0',修改size
    { 
        _str[n] = '\0';
        _size = n;
    } 
    else    // 扩容+补字符
    { 
        if (n > _capacity) reserve(n); // 容量不足则先扩容
        // 补字符(从原size到n)
        for (size_t i = _size; i < n; ++i) {
            _str[i] = ch;
        }
        _size = n;       // 更新size
        _str[_size] = '\0'; // 确保字符串结束符
    }
}

2.7 尾插字符/字符串

  • 尾插单个字符(push_back):先检查容量(满则扩容,默认扩为 2 倍或初始 2),再插入字符并更新 _size

/* 尾插单个字符 */
void push_back(char ch)
{
        // 空间不足,扩容 hello xxxxxxx &tmp xxxxxxxxxxxx 
        if (_size == _capacity)
        {
                size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;
                reserve(new_capacity);
        }
        // 放入字符
        _str[_size] = ch;
        ++_size;
        _str[_size] = '\0';
}

/* s1 += 'a' */
string& operator+=(const char ch)
{
        this->push_back(ch);
        return *this;
}
  • 尾插字符串(append):计算字符串长度,若 _size + 长度 > _capacity 则扩容,再拷贝字符串到末尾。

/* 尾插字符串 */
void append(const char* str)
{
        size_t len = strlen(str);
        // 如果空间不足,则增容
        if (_size + len > _capacity)
        {
                size_t new_capacity = _size + len;
                reserve(new_capacity);
        }
        // 拷入新的字符串到原字符串后面
        strcpy(_str + _size, str);
        _size += len;
}

/* s1 += "abc" */
string& operator+=(const char* str)
{
        this->append(str);
        return *this;
}

2.8 insert

  • 逻辑:先检查越界(pos 需小于 _size),再扩容(若需),然后「挪动数据」(从后往前挪,避免覆盖),最后插入字符 / 字符串。

  • pos下标位置插入字符:

/* 在pos下标位置插入字符 */
string& insert(size_t pos, const char ch)
{
        assert(pos < _size);
        // 空间不够就增容
        if (_size == _capacity)
        {
                size_t new_capacity = _capacity == 0 ? 2 : _capacity * 2;
                reserve(new_capacity);
        }

        // 挪动数据,从'\0'开始挪
        int end = _size;
        while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,
                                // 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转
        {
                _str[end + 1] = _str[end];
                --end;
        }
        // 插入数据
        _str[end] = ch;
        ++_size;

        return *this;
}
  • 在pos下标位置插入字符串

两种方式:

/* 在pos下标位置插入字符串 */
string& insert(size_t pos, const char* str)
{
        assert(pos < _size);
        size_t len = strlen(str);
        // 空间不足则扩容
        if (_size + len > _capacity)
        {
                reserve(_size + len);        // 函数内会自动多开一个空间给'\0'
        }

        // 挪动数据
        int end = _size;
        while (end >= (int)pos) // pos是一个无符号数,end在与他比较时也会转化成无符号,
                                // 如果pos给0,end减到-1也会比pos大(无符号),所以这里要强转
        {
                _str[end + len] = _str[end];
                --end;
        }

        // 放字符串数据(或者使用memcpy也行)
        for (size_t i = 0; i < len; ++i)
        {
                _str[pos] = str[i];
                ++pos;
        }
        _size += len;

        return *this;
}
  • memcpy搬数据的方法:

/* 在pos下标位置插入字符串 */
/* memcpy搬数据的方法 */
void insert(size_t pos, const char* str)
{
        assert(pos < _size);
        size_t len = strlen(str);
        // 空间不够就增容
        if (_capacity < _size + len)
        {
                size_t new_capacity = _capacity + len;
                reserve(new_capacity);
        }
        // 得到要搬运有效数据的长度
        size_t memcpy_len = strlen(_str + pos);
        // 将数据先存在另一个空间,之后再搬回来
        char* tmp = new char[memcpy_len + 1];
        memcpy(tmp, _str + pos, memcpy_len + 1);        // 加上的1是'\0'的一个字节
        _size -= memcpy_len;
        // pos位置加上字符串str
        memcpy(_str + _size, str, len + 1);        // 加上的1是'\0'的一个字节
        _size += len;
        // 把另一个空间中的数据搬回来
        memcpy(_str + _size, tmp, memcpy_len + 1);        // 加上的1是'\0'的一个字节
        delete[] tmp;
        tmp = nullptr;
        _size += memcpy_len;
}

2.9 erase

/* 从pos位置开始删除len个字符 */
string& erase(size_t pos, size_t len)
{
        assert(pos < _size);
        if (len >= _size - pos)        // pos后面的都被删了
        {
                _str[pos] = '\0';
                _size = pos;
        }
        else // 删除后pos后面还有数据存留
        {
                size_t i = pos + len;
                while (i <= _size)        // 移动数据
                {
                        _str[i - len] = _str[i];
                        ++i;
                }

                _size -= len;
        }

        return *this;
}

2.10 find

/* 从pos下标位置开始查找字符的下标位置 */
size_t find(const char ch, size_t pos = 0)
{
        for (size_t i = pos; i < _size; ++i)
        {
                if (_str[i] == ch)
                {
                        return i;        // 找到返回下标
                }
        }
        return npos;        // 没找到返回-1(无符号的-1,size_t的最大值)
}
/* 从pos下标位置开始查找字符串的下标位置 */
size_t find(const char* str, size_t pos = 0)
{
        char* p = strstr(_str, str);
        if (p == NULL)
        {
                return npos;
        }
        else
        {
                return p - _str;        // p - _str刚好是中间相差的元素个数,即 p 指向元素的下标
        }
}

2.11 关系运算符重载

/* 字符串比较大小 */
bool operator<(const string& s)
{
        int ret = strcmp(_str, s._str);
        return ret < 0;
}

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

bool operator<=(const string& s)
{
        return *this < s || *this == s;        // 尽量提高对代码的复用度,方便后续修改
}

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

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

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

2.12 输入输出重载

/* 输入重载 */
istream& operator>>(istream& in, string& s)
{
        while (1)
        {
                char ch;
                //in >> ch;        不能使用>>,因为ch是一个char类型的变量,>>默认无法接收' '和'\n'
                //                        导致下面的if判断无法成功,陷入死循环
                ch = in.get();        // get函数可以直接获取字符,不会跳过
                if (ch == ' ' || ch == '\n')        // getline()函数和>>重载唯一的区别就是getline这里的判断没有ch == ' '
                {
                        break;
                }
                else
                {
                        s += ch;
                }
        }
        return in;
}

/* 输出重载 */
ostream& operator<<(ostream& out, const string& s)
{
        for (size_t i = 0; i < s.size(); ++i)
        {
                cout << s[i];
        }

        return out;
}

2.13 模拟实现的string功能测试代码

// 遍历与迭代器test
void test_string1()
{
        string s1;
        string s2("hello");

        cout << s1 << endl;
        cout << s2 << endl;

        cout << s1.c_str() << endl;
        cout << s2.c_str() << endl;

        // 三种遍历方式
        // 1.[]
        for (size_t i = 0; i < s2.size(); ++i)
        {
                s2[i] += 1;
                cout << s2[i] << " ";
        }
        cout << endl;

        // 2.迭代器
        string::iterator it2 = s2.begin();
        while (it2 != s2.end())
        {
                *it2 -= 1;
                cout << *it2 << " ";
                ++it2;
        }
        cout << endl;

        // 3.范围for
        // 范围for是由迭代器支持的,也就是说这段代码最终会被编译器替换成迭代器
        // 想要支持范围for,需要先支持 iterator begin() end()
        for (auto e : s2)
        {
                cout << e << " ";
        }
        cout << endl;
}

// pushback&insert_test
void test_string2()
{
        string s1("hello");
        s1.push_back(' ');
        s1.push_back('w');
        // char* arr = {0};
        // strcpy(arr, s1.c_str());
        cout << s1.c_str() << endl;
        // cout << arr << endl;
        s1.append("orld");
        cout << s1 << endl;

        string s2;
        s2 += "abcd";
        cout << s2 << endl;
        s2 += 'e';
        cout << s2 << endl;
        s2.insert(1, "XYZ");
        cout << s2 << endl;
        s2.insert(1, "XYZ");
        cout << s2 << endl;

}

// resize_test
void test_string3()
{
        string s1("hello");
        s1.resize(1);
        cout << s1 << endl;
        s1.resize(11,'x');
        cout << s1.size() << endl;
        cout << s1 << endl;
        //for (int i = 0; i < s1.size(); ++i)
        //{
        //        cout << (int)s1[i] << endl;
        //}
}

// erase_test
void test_string4()        
{
        string s1("hello world");
        s1.erase(2, 3);
        cout << s1 << endl;
}

// find_test
void test_string5()
{
        string s1("abcdefghijklmn");
        cout << s1.find('c') << endl;
        cout << s1.find("defg") << endl;
        cout << s1.find("asdgf") << endl;
}

// cin >> s 和 cout << s
void test_string6()
{
        string s1;
        cin >> s1;
        cout << s1;
}

3. 深浅拷贝问题(传统vs现代)

C++ 的一个常见面试提示让你实现一个 string 类。

限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源,也就是完成 默认构造 + 析构 + 拷贝 + operator=()

  下面是默认构造+析构的简单代码:

class string
{
public:
        string(const char* str = "")
                :_str(new char[strlen(str) + 1])
        {
                strcpy(_str, str);
        }
        ~string()
        {
                delete[] _str;
                _str = nullptr;
        }

        size_t size()
        {
                return strlen(_str);
        }

        char& operator[](size_t i)
        {
                return _str[i];
        }

private:
        char* _str;
};

3.1 传统写法(深拷贝)

手动分配新内存并复制数据。

// 拷贝构造函数(传统写法)
string(const string& s)
    : _str(new char[strlen(s._str) + 1])
{
    strcpy(_str, s._str);
}

// 赋值运算符重载(传统写法)
string& operator=(const string& s) {
    if (this != &s) { // 1. 防止自我赋值
        char* tmp = new char[strlen(s._str) + 1]; // 2. 分配新空间
        strcpy(tmp, s._str);                      // 3. 拷贝数据
        delete[] _str;                            // 4. 释放旧空间
        _str = tmp;                               // 5. 指向新空间
    }
    return *this; // 6. 返回自身引用以支持连续赋值
}

关键点:

  • 自我赋值判断:if (this != &s) 至关重要,否则 delete[] _str 会先释放自身资源,导致后续操作出错。

  • 异常安全:先分配新空间和拷贝数据,成功后再释放旧空间。如果 new 失败抛出异常,旧数据依然完好。

3.2 现代写法

利用“拷贝-交换” ,更简洁且异常安全。

// 深拷贝 - 现代写法(更简洁)
// 拷贝构造
string(const string& s)
        :_str(nullptr)
{
        string tmp(s._str);
        swap(_str, tmp._str);        // tmp._str变成nullptr,析构时也不会出错
}

// 赋值
string& operator=(const string& s)
{
        if (this != &s)
        {
                string tmp(s);
                swap(_str, tmp._str);        // tmp是一个临时局部变量,出函数时就会析构,把_str的值给tmp._str,tmp析构会自动释放原_str的空间
        }
        return *this;
}

// 赋值更简洁的写法
// 这里最巧的是用了传值操作,相当于一次拷贝构造,此时的s就是我想要的东西
// 直接把_str和s._str一换,大功告成
string& operator=(string s)
{
        swap(_str, s._str);
        return *this;
}

关键点:

  • 巧妙利用传值:operator=(string s) 的参数 s 是通过拷贝构造生成的实参副本。函数体内只需交换 thiss 的资源。

  • 自动管理:函数结束时,形参 s 析构,自动释放了 this 对象原来的资源。代码极其简洁且安全。

  • 自我赋值:现代写法天然处理了自我赋值。如果是 s1 = s1,传参时 ss1 的副本,交换后,副本 s 带着 s1 原来的资源被析构,s1 的资源没变。

3.3 支持增删查改的string的拷贝构造和赋值

同样的,复杂一点的string同样可以使用现代写法来写拷贝构造和赋值函数。

传统写法:

/* 拷贝构造函数 */
string(const string& s)
{
        _size = s._size;
        _capacity = _size;
        _str = new char[_capacity + 1];        // 多加一个空间给'\0'
        strcpy(_str, s._str);
}

/* 赋值重载函数 */
string& operator=(const string& s)
{
        if (this != &s)
        {
                // 开辟一块新的空间,拷贝数据过去
                char* tmp = new char[s._capacity + 1];
                strcpy(tmp, s._str);
                // 释放原来的空间防止内存泄露,指向新空间
                delete[] _str;
                _str = tmp;
        }
        return *this;
}
string& operator=(const char* str)
{
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];        // 多加一个空间给'\0'
        strcpy(_str, str);
        return *this;
}

现代写法:

void swap(string& s)
{
        // ::的意思是我调用的不是这里的这个swap,而是全局域的swap
        ::swap(_str, s._str);
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);

}

/* 拷贝构造现代写法 */
string(const string& s)
        :_str(nullptr)
        , _size(0)
        , _capacity(0)
{
        string tmp(s._str);
        this->swap(tmp);
}

/* 赋值现代写法 */
string& operator=(string s)
{
        this->swap(s);
        return *this;
}

4. 扩展阅

C++面试中string类的一种正确写法 | 酷 壳 - CoolShell

STL 的string类怎么啦?_stl 字符串-CSDN博客


网站公告

今日签到

点亮在社区的每一天
去签到