C++string类详解(模拟实现)

C++中的string类是标准库提供的用于处理字符串的类,它比C风格的字符数组(char[])更安全、更方便。

1. 基本使用

包含头文件

#include <string>
using namespace std; // 或者使用 std::string

声明和初始化

string s1;              // 默认初始化,空字符串
string s2 = "hello";    // 拷贝初始化
string s3("world");     // 直接初始化
string s4(5, 'a');      // 包含5个'a'的字符串 "aaaaa"
string s5(s2);          // 拷贝构造,s5是s2的副本

2. 常用操作

字符串连接

string s = "hello";
s += " world";          // s变为 "hello world"
string s2 = s + "!";    // s2为 "hello world!"

访问字符

string s = "hello";
char c1 = s[0];         // 'h' (不检查边界)
char c2 = s.at(1);      // 'e' (会检查边界,越界抛出异常)

字符串比较

string s1 = "apple";
string s2 = "banana";
if (s1 < s2) {          // 按字典序比较
    cout << "apple comes before banana";
}

容量操作

string s = "hello";
cout << s.size();   // 输出5
cout << s.length(); // 输出5

string s;
s.reserve(100);//返回当前为字符串分配的总空间大小(以字符为单位)
//capacity >= size
//字符串自动增长时,capacity通常会以某种策略(如倍增)增加
cout << s.capacity(); // 输出至少100(可能更大)

string s1;
string s2 = "hello";
cout << s1.empty(); // 输出1(true)
cout << s2.empty(); // 输出0(false)

string s = "hello";
s.clear();//清空字符串内容,使其变为空字符串
cout << s.size(); // 输出0
cout << s.capacity(); // 可能仍为之前的大小
//注意:只清空内容,不释放内存(capacity通常不变)等效于erase(begin(), end());
reserve()详细
void reserve(size_t n = 0);
  • 功能:预留存储空间,避免频繁重新分配
  • 特点
    • 如果n大于当前capacity,则重新分配至少能容纳n个字符的空间
    • 如果n小于当前capacity,实现可能忽略此请求(非强制缩小)
    • 不影响字符串内容或size
  • 使用场景:预先知道字符串大致大小时,可提高性能
  • 示例
string s;
s.reserve(1000); // 预先分配空间
for(int i = 0; i < 1000; ++i) {
    s += 'a';    // 不会频繁扩容
}
resize()详细
void resize(size_t n);
void resize(size_t n, char c);
  • 功能:改变字符串的size(有效字符数)
  • 参数
    • n:新的size值
    • c:填充字符(仅用于第二个重载)
  • 行为
    • 如果n < size():截断到前n个字符
    • 如果n > size():
      • 单参数版本:用'\0'填充(不推荐)
      • 双参数版本:用字符c填充
    • 可能影响capacity(如果需要更多空间)
  • 示例
string s = "hello";
s.resize(3);     // s变为"hel"
s.resize(5, 'x'); // s变为"helxx"
s.resize(10);    // s变为"helxx\0\0\0\0\0"(不推荐这样用)

综合比较表

函数功能是否改变内容是否可能改变capacity
size()/length()获取长度
capacity()获取容量
empty()检查是否为空
clear()清空内容通常否
reserve()预留空间可能增加
resize()调整大小可能增加

使用技巧

  1. 需要频繁追加内容时,使用reserve()预先分配空间
  2. 检查字符串是否为空时,优先使用empty()而非size() == 0
  3. 使用resize()时,建议指定填充字符,避免不可见字符
  4. 需要彻底释放内存时(C++11起):
string s;
s.resize(1000);
// ...使用后...
s.clear();
s.shrink_to_fit(); // 请求减少capacity以匹配size

常用成员函数

string s = "hello";

s.size();       // 或 s.length(),返回5
s.empty();      // 判断是否为空
s.clear();      // 清空字符串
s.substr(1, 3); // 从位置1开始,取3个字符,返回"ell"
s.find("ll");   // 返回2,
s.find("ll", 1, 3);// 只查找"ell",返回2
//首次出现的位置如果找到,返回子串/字符首次出现的位置(从0开始的下标)
//如果未找到,返回string::npos(一个特殊常量,通常是size_t的最大值)
s.replace(2, 2, "xx"); // 从位置2开始,替换2个字符为"xx",变为"hexxo"
s.insert(1, "yy"); // 在位置1插入"yy",变为"hyyello"
s.erase(1, 2);  // 从位置1开始删除2个字符,变为"hlo"

3. 输入输出

输入

string s;
cin >> s;               // 读取一个单词(遇到空格停止)
getline(cin, s);         // 读取一行(包括空格)

输出

string s = "hello";
cout << s << endl;

4. 字符串与数值转换

C++11引入了方便的转换函数:

// 字符串转数值
int i = stoi("42");      // string to int
double d = stod("3.14"); // string to double

// 数值转字符串
string s1 = to_string(42);    // "42"
string s2 = to_string(3.14);  // "3.140000"

5. 字符串遍历

可以使用多种方式遍历字符串:

string s = "hello";

// 1. 下标访问
for (size_t i = 0; i < s.size(); ++i) {
    cout << s[i];
}

// 2. 迭代器
for (auto it = s.begin(); it != s.end(); ++it) {
    cout << *it;
}

// 3. 范围for循环 (C++11)
for (char c : s) {
    cout << c;
}

6. 注意事项

  1. string是动态大小的,不需要预先指定长度
  2. string会自动管理内存,不需要手动分配/释放
  3. string'\0'结尾
  4. 与C字符串互操作:
    string s = "hello";
    const char* cstr = s.c_str(); // 获取C风格字符串
    

7.vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

  • vs下string的结构
    string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
  • 当字符串长度小于16时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
 value_type _Buf[_BUF_SIZE];
 pointer _Ptr;
 char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
在这里插入图片描述

  • g++下string的结构
    G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个
    指针,该指针将来指向一块堆空间,内部包含了如下字段:
  • 空间总大小
  • 字符串有效长度
  • 引用计数
struct _Rep_base
{
 size_type               _M_length;
 size_type               _M_capacity;
 _Atomic_word            _M_refcount;
};
  • 指向堆空间的指针,用来存储字符串。

8.模拟实现

深浅拷贝问题

1. 默认拷贝构造函数的行为

当类中没有显式定义拷贝构造函数时,C++编译器会为我们生成一个默认的拷贝构造函数,它的行为是:

对类的每个成员进行逐成员拷贝

  • 对于基本类型(int, float, pointer等):直接复制值
  • 对于类类型:调用该类的拷贝构造函数

对于我们的string类:

class string {
    char* _str;       // 指针
    size_t _size;     // 基本类型
    size_t _capacity; // 基本类型
};

默认拷贝构造函数相当于:

string(const string& other) 
    : _str(other._str),      // 仅复制指针值
      _size(other._size),    // 复制值
      _capacity(other._capacity) {} // 复制值
2. 浅拷贝的问题演示

假设有以下代码:

string s1("hello");
string s2 = s1;  // 调用默认拷贝构造函数

内存布局如下:

s1:
_str     -> "hello\0"
_size    -> 5
_capacity-> 5

s2:
_str     -> (与s1._str相同的内存地址)
_size    -> 5 
_capacity-> 5
3. 浅拷贝导致的问题
问题1:双重释放(Double Free)

当这两个对象析构时:

~string() { delete[] _str; }

会发生:

  1. s2先析构,释放了_str指向的内存
  2. s1再析构,尝试释放同一块内存
  3. 程序崩溃(未定义行为)
问题2:修改相互影响
s1[0] = 'H';  // 修改s1
std::cout << s2[0]; // 输出'H',因为s2和s1共享同一内存
问题3:悬空指针(Dangling Pointer)

如果其中一个对象先被修改:

string s3("world");
s1 = s3;  // s1现在指向"world"
// s2._str现在指向什么?
4. 图解浅拷贝与深拷贝
浅拷贝(错误方式)
s1: [_str:0x1000] -> "hello"
s2: [_str:0x1000] 

两个对象共享同一内存

深拷贝(正确方式)
s1: [_str:0x1000] -> "hello"
s2: [_str:0x2000] -> "hello" 

每个对象有自己的内存副本

5. 为什么其他类成员没问题?
  • _size_capacity是基本类型,直接复制值是完全安全的
  • 只有指针成员_str需要特殊处理,因为我们需要复制指针指向的内容,而不是指针本身的值
6. 解决方案:实现深拷贝

正确的拷贝构造函数应该:

string(const string& other) 
    : _str(new char[other._capacity + 1]), // 分配新内存
      _size(other._size),
      _capacity(other._capacity) {
    strcpy(_str, other._str); // 复制内容
}

这样每个string对象都拥有自己独立的字符数组存储。

传统写法和现代写法

  • 传统写法
namespace sty {

class string {
public:
    // 传统写法 - 默认构造函数
    string(const char* str = "") {
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

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

    // 传统写法 - 赋值运算符
    string& operator=(const string& s) {
        if (this != &s) {
            // 先删除原有数据
            delete[] _str;
            
            // 分配新空间并拷贝数据
            _size = s._size;
            _capacity = s._capacity;
            _str = new char[_capacity + 1];
            strcpy(_str, s._str);
        }
        return *this;
    }

    ~string() {
        delete[] _str;
    }

private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

}
  • 现代写法

传值调用调用时隐式调用拷贝构造函数创建临时对象,交换资源将临时对象的资源与当前对象交换,临时对象析构时自动清理旧资源,从而实现现代写法

namespace sty {

class string {
public:
    // 现代写法 - 默认构造函数
    string(const char* str = "") 
        : _size(strlen(str))
        , _capacity(_size)
        , _str(new char[_capacity + 1]) {
        strcpy(_str, str);
    }

    // 现代写法 - 拷贝构造函数 (copy-and-swap)
    string(const string& s) 
        : _str(nullptr)
        , _size(0)
        , _capacity(0) {
        string temp(s._str);
        swap(temp);
    }

    // 现代写法 - 赋值运算符 (copy-and-swap)
    string& operator=(string s) { // 注意这里是传值,不是引用
        swap(s);
        return *this;
    }

    ~string() {
        delete[] _str;
    }

    void swap(string& other) noexcept {
        std::swap(_str, other._str);
        std::swap(_size, other._size);
        std::swap(_capacity, other._capacity);
    }

private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

}

总代码

#include <iostream>
#include <cstring>    // strlen, strcpy等
#include <algorithm>  // std::swap
#include <stdexcept>  // std::out_of_range
#include <cassert>    // assert

namespace sty {

    class string {
    public:
        // 类型定义
        typedef char* iterator;

        // 默认构造函数
        string(const char* str = "") {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

        // 拷贝构造函数
        string(const string& s) : _str(nullptr), _size(0), _capacity(0) {
            string tmp(s._str);
            this->swap(tmp);
        }

        // 赋值运算符
        string& operator=(const string& s) {
            if (this != &s) {
                string temp(s);
                this->swap(temp);
            }
            return *this;
        }

        // 析构函数
        ~string() {
            if (_str) {
                delete[] _str;
                _str = nullptr;
            }
        }

        /******************** 迭代器相关 ********************/
        iterator begin() { return _str; }
        iterator end() { return _str + _size; }

        /******************** 修改操作 ********************/

        // 在末尾添加字符
        void push_back(char c) {
            if (_size == _capacity) {
                reserve(_capacity * 2);
            }
            _str[_size++] = c;
            _str[_size] = '\0';
        }

        // += 字符重载
        string& operator+=(char c) {
            push_back(c);
            return *this;
        }

        // 追加字符串
        void append(const char* str) {
            size_t len = strlen(str);
            if (_size + len > _capacity) {
                reserve(_size + len);
            }
            strcpy(_str + _size, str);
            _size += len;
        }

        // += 字符串重载
        string& operator+=(const char* str) {
            append(str);
            return *this;
        }

        // 清空字符串
        void clear() {
            _size = 0;
            _str[_size] = '\0';
        }

        // 交换两个字符串
        void swap(string& s) {
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }

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

        /******************** 容量相关 ********************/

        // 获取字符串长度
        size_t size() const {
            return _size;
        }

        // 获取容量
        size_t capacity() const {
            return _capacity;
        }

        // 判断是否为空
        bool empty() const {
            return _size == 0;
        }

        // 调整字符串大小
        void resize(size_t newSize, char c = '\0') {
            if (newSize > _size) {
                if (newSize > _capacity) {
                    reserve(newSize);
                }
                memset(_str + _size, c, newSize - _size);
            }
            _size = newSize;
            _str[newSize] = '\0';
        }

        // 预留空间
        void reserve(size_t newCapacity) {
            if (newCapacity > _capacity) {
                char* str = new char[newCapacity + 1];
                strcpy(str, _str);
                delete[] _str;
                _str = str;
                _capacity = newCapacity;
            }
        }

        /******************** 访问元素 ********************/

        // 下标运算符重载
        char& operator[](size_t index) {
            assert(index < _size);
            return _str[index];
        }

        const char& operator[](size_t index) const {
            assert(index < _size);
            return _str[index];
        }

        /******************** 关系运算符 ********************/

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

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

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

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

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

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

        /******************** 查找操作 ********************/

        // 查找字符
        size_t find(char c, size_t pos = 0) const {
            for (size_t i = pos; i < _size; ++i) {
                if (_str[i] == c) {
                    return i;
                }
            }
            return npos;
        }

        // 查找子串
        size_t find(const char* s, size_t pos = 0) const {
            assert(s);
            assert(pos < _size);
            const char* src = _str + pos;
            while (*src) {
                const char* match = s;
                const char* cur = src;
                while (*match && *match == *cur) {
                    ++match;
                    ++cur;
                }
                if (*match == '\0') {
                    return src - _str;
                }
                else {
                    ++src;
                }
            }
            return npos;
        }

        /******************** 插入操作 ********************/

        // 插入字符
        string& insert(size_t pos, char c) {
            assert(pos <= _size);
            if (_size == _capacity) {
                reserve(_capacity * 2);
            }
            for (int i = _size; i >= (int)pos; --i) {
                _str[i + 1] = _str[i];
            }
            _str[pos] = c;
            _size++;
            return *this;
        }

        // 插入字符串
        string& insert(size_t pos, const char* str) {
            size_t len = strlen(str);
            if (_size + len > _capacity) {
                reserve(_size + len);
            }
            for (int i = _size; i >= (int)pos; --i) {
                _str[len + i] = _str[i];
            }
            while (*str != '\0') {
                _str[pos++] = *str++;
            }
            _size += len;
            return *this;
        }

        /******************** 删除操作 ********************/

        // 删除字符
        string& erase(size_t pos, size_t len) {
            assert(pos < _size);
            if (pos + len >= _size) {
                _str[pos] = '\0';
                _size = pos;
            }
            else {
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
            }
            return *this;
        }

    private:
        char* _str;          // 字符串数据指针
        size_t _capacity;    // 容量(不包括'\0')
        size_t _size;        // 当前长度(不包括'\0')

        // 静态成员
        static const size_t npos = -1;

        // 友元函数声明
        friend std::ostream& operator<<(std::ostream& out, const string& s);
        friend std::istream& operator>>(std::istream& in, string& s);
    };

    /******************** 流操作符重载 ********************/

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

    // 输入流重载
    std::istream& operator>>(std::istream& in, string& s) {
        s.clear();  // 先清空原有内容

        char ch;
        while (in.get(ch) && !isspace(ch)) {
            s.push_back(ch);
        }

        return in;
    }

} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值