C++ string 全面解析与模拟实现

前言

string 是 STL(Standard Template Library)容器家族中的一员,它本质上是一个封装了字符数组的动态序列容器,具备自动内存管理、迭代器支持、丰富的成员函数等现代 C++ 特征。本篇博客不仅介绍 string 的常见用法,更深入探讨其底层内存管理机制,并动手模拟实现一个简化版的 string 类,从构造、容量扩容、迭代器支持到增删查改,让你对 string 的实现原理有一个全景式理解。

一. C/C++ 中字符串的区别

在C语言中, 字符串本质上是以 '\0' 结尾的字符数组, 例如:

char str[] = "hello\0";

C语言在<string.h>中也提供了一系列字符串操作函数, 但是这一设计有很多痛点, 比如库函数和字符串是分开的, 不符合OOP思想, 而且空间需要手动管理, 操作越界时程序容易崩溃等

而在C++中, 标准库为我们提供了更现代, 更安全的 string类, 相对比C语言的字符串 string支持自动内存管理, 动态扩容, 运算符重载等功能

string str1 = "hello";
string str2 = str1 + "world"; // 此时 str2被初始化为hello world
cout << str2 << endl; // 输出 hello world

注意: string 一定不能使用 printf访问, 因为 string本质上是自定义类型不是 char* 类型, 如果强行访问会造成未定义行为

二. string类的常见用法以及模拟实现

string 类在C++中的出现时间早于STL(标准模板库),后来才被纳入STL库。由于其早期设计较为冗余,包含了多达106个接口函数,其中许多接口并不常用,这一点常被 C++程序员诟病。所以我们无需记忆所有接口,只需掌握并熟练使用核心接口即可。对于不常用的接口,遇到时查阅相关文档即可解决问题。

1 string类常见的构造函数

在 C98 中, string类的构造函数通常有七种:

虽然 string类提供了七种构造方式,但我们在手动模拟时不必照搬所有接口。下面我将挑选其中最常用、最具代表性的几种来进行实现,重点是理解它们的逻辑和语义。

string类基本成员函数及变量

class String {
public:
    // 释放资源, 并把底层数据清空
    ~String()
    {
        delete[] _data;
        _data = nullptr;
        _size = 0;
    }

    // 返回字符串大小
    size_t size() const
    {
        return _size;
    }

    // 返回字符串实际存储空间
    size_t capacity() const
    {
        return _capacity;
    }
    
    // 返回底层字符串
    const char* c_str() const 
    {
        return _data;
    }

private:
    char* _data;      // 指向堆上分配的字符数组, 包含'\0'
    size_t _size;     // 实际字符数,不含 '\0'
    size_t _capacity; // 当前已分配的存储空间, 不含 '\0'

    static const size_t npos = -1; // 在删除等操作时使用
};

1.1 默认构造

创建一个空字符串对象, 不包含任何字符, 长度为0

string()
    :_data(new char[1]{"\0"})
    ,_size(0)
    ,_capacity(0)
{}

1.2 从C字符串构造

参数为C字符串的构造函数用于将C风格字符串转换为 string类

string(const char* str = "")
{
    _size = strlen(str);        // 获取字符串的长度. 不包含'\0'
    _capacity = _size;          // 此时容量边界等于数组大小
    _data = new char[_size + 1] // 多开辟空间用于存储'\0'
} 

1.3 拷贝构造

用于通过已有的 string类初始化一个新对象。 当发生类似于传值传参, 返回对象等对象复制时, 会自动调用拷贝构造

string(const string& s)
    :_data(new char[s._size + 1]) // 多开辟一个空间用来存储'\0'
    ,_size(s._size)
    ,_capacity(s._capacity)
{    
    // 拷贝s的数据, 这里也可以使用for循环来拷贝
    strcpy(_data, s._data);
}

拷贝构造的现代写法

现代写法的核心在于使用临时变量存储被拷贝对象的数据,然后通过交换操作实现资源转移。 传统写法需要手动分配内存并逐字节拷贝数据, 更冗长并且容易出错

// 执行交换操作, 调用标准库的 swap来完成
void swap(const string& s)
{
    std::swap(_data, s._data);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

// 拷贝构造的现代写法
string(const string& s)
    :_data(nullptr)
    ,_size(s._size)
    ,_capacity(s._capacity)
{
    string temp(s._data); // 复用C风格字符串构造, 由于是栈上的变量, 出作用域后会自动析构
    swap(temp);           // 直接把资源交换过来即可
}

1.4 拷贝赋值算符

C++ 中另一个与对象初始化密切相关的重要成员函数是拷贝赋值运算符(operator++)。
虽然它本质上不是构造函数,但其功能却与拷贝构造函数非常相似, 都是用一个已有对象来为当前对象赋值或构造内容,只是应用场景不同

  • 拷贝构造:在创建新对象时复制已有对象

  • 拷贝赋值:在已有对象存在的前提下被重新赋值

string& operator=(const string& s)
{
    // 避免自己给自己赋值的情况
    if(&s != this)
    {
        // 这里推荐使用临时变量, new分配失败抛出异常的话下面的代码就不会继续执行
        // 能够保护原字符串的数据
        char* temp = new char[strlen(s._data) + 1];
        strcpy(temp, s._data);
        delete[] _data; // 先清理原数据
        _data = temp;   // 再拷贝新数据
    }
    // 返回 this指针, 支持连续赋值(s1 = s2 = s3)
    return this;
}

拷贝赋值的现代写法

在现代 C++ 中,通常采用拷贝交换惯用法来实现拷贝赋值运算符。通过传值参数触发拷贝构造函数,然后交换资源,从而实现安全且简洁的赋值操作。

string& operator=(string s)
{
    if(&s != this)
    {
        // s是栈上的临时变量, 出作用域后会自动调用析构回收资源
        swap(s);
    }
    return *this;
}

1.5 重复字符构造

通过重复单个字符来构造新字符串, 例如 "a" * 3 生成 "aaa", 常用于生成固定模式文本

string(size_t n, char c)
    :_data(new char[n + 1])
    ,_size(n)
    ,_capacity(n)
{
    // 初始化 _data所有元素为 c
    for(size_t i = 0; i < _size; i++)
    {
        _data[i] = c;
    }
    // 将最后一个元素置为 '\0', 用来表示字符串的结束
    _data[_size] = '\0';
}

2 string类的容量操作

在 string类中,capacity 表示底层分配的内存的大小,而 size 仅反映当前存储的字符串的实际长度。通过 resize() 和 reserve() 方法可以主动调整字符串的容量,既能优化性能(比如减少分配次数),也能精确控制内存占用。

2.1 reserve: 预留容量

调用 reserve(size_t n) 会请求字符串内部容量至少为 n 个字符。若 n 大于当前容量也就是 capacity 会重新分配内存,  否则无操作。通常用来避免频繁追加字符所导致的多次内存分配来提升性能

void reserve(size_t n)
{
    // reserve 只影响 _capacity,不改变 _size 和字符串内容
    if(n > _capacity)
    {
        char* temp = new char[n + 1]; // 多开一个空间用来存储 '\0'
        strcpy(temp, _data); // 拷贝原本的内容, strcpy 也会拷贝 '\0'
        delete[] _data;      
        _data = temp;        // _data 指向新开辟并拷贝完内容的空间
        _capacity = n;       // _capacity 记录扩容后的空间
    }
}
        

2.2 resize: 修改逻辑长度

resize 用于调整 string 的大小,可以扩展或缩小元素数量,新增元素会默认初始化或填充指定值;

n ≤ size
直接截断字符串至长度n,丢弃超出部分。

size < n ≤ capacity
重新分配内存以容纳n个元素,但不扩展总容量。

n > capacity
重新分配更大的内存空间,扩展容量以满足需求,增加内存使用量。

void resize(size_t n, const char ch = '\0')
{
    if(n > _size)
    {
        // 第二, 三种情况可以一起处理, 直接复用 reverse即可
        // n > capacity的话就会扩容, size < n <= capacity 的话则直接写数据即可
        reserve(n);
        for(size_t i = _size; i < n; i++)
        {
            _data[i] = ch;
        }
        _data[n] = '\0';
        _size = n;
    }
    // 小于 _size的情况
    else
    {
        _data[n] = '\0'; // 在下标 n截断字符串
        _size = n;
    }
}

2.3 简单接口一览

除了容量控制,string 类还提供了一些日常使用频率很高的轻量级接口。它们语义明确,实现简单,但同样是模拟类中不可或缺的部分。

size(): 返回当前字符串长度

长度不包含 '\0', 常用于遍历, 比较等逻辑中

// 通过 const修饰 this指针, 这样 const string也可以调用该接口
size_t size() const
{
    return _size;
}

字符串的 length 和 size 接口功能相同。由于 string 类出现较早,当时只有 length 接口。后来为了与其他 STL 容器保持统一,才新增了 size 接口

empty(): 判断字符串是否为空

实际上就是判断 _size == 0, 和 C语言中判断 str[0] == '\0' 类似, 但是更安全

bool empty() const
{
    return _size == 0;
}

clear(): 清空字符串内容

用于清空字符串内容,将其长度设置为 0,但不会释放底层内存,保留 capacity

// 由于 clear会修改成员状态, const string无法调用这个函数
void clear()
{
    _size = 0;
    _data[0] = '\0';
}

使用 clear() 方法后,string 会保留原有容量,所以即使之前分配了大量空间,清空后其状态等同于空容器。

string str("hello");
str.reserve(10000);          // 开辟一万个空间
str.clear();
cout << str.empty() << endl; // 输出 1

3 string类的访问遍历操作 

3.1 什么是迭代器

在 C++ 中,迭代器(iterator)是 STL 中的核心概念之一。可以将它简单理解为 可以在容器上顺序移动的指针。不同容器对迭代器的实现方式不同:某些容器将其封装为类,而字符串我们则可以视为简单的 char* 指针。

string类为我们提供了四种类型的迭代器:

正向迭代器, 反向迭代器, const正向迭代器, const反向迭代器

end() 指向容器中最后一个有效元素的后一个位置,而 rend() 指向第一个有效元素前一个位置的内存地址。这两个位置都不能直接解引用,否则会导致未定义行为。

3.2 使用迭代器遍历字符串

string类的迭代器实现

typedef char* iterator; // string类的迭代器可视为字符型指针
typedef const char* const_iterator; // const迭代器不可修改

iterator begin()
{
    return _data;         // 指向首元素
}

iterator end()
{
    return _data + _size; // 指向尾元素的后面一个位置
}

const_iterator begin() const { return _data; }
const_iterator end() const { return _data + _size; }

补充知识: for each循环 (范围for) 和 auto

auto 的本质与注意事项

在 C++11 中,auto 被重新定义为类型自动推导关键字。它告诉编译器:“根据表达式自动推导变量的类型。”

特性 / 限制说明
自动推导发生在编译期类型是在编译时由初始值推断出来的
声明多个变量必须同类型auto a = 10, b = 'x';  (错误:类型不一致)
指针写法 auto vs auto*auto 和 auto* 推导结果相同
auto 不能声明函数参数函数参数不能直接用 auto 
auto 返回值需谨慎使用返回值自动推导在模板、lambda 等场景下常见,但易出错需注意类型一致性
auto 不能声明数组如 auto arr[10]; 是不合法的

范围for

在传统循环写法中, 我们需要手动指定循环范围与递进步长

std::string s("hello");
for(int i = 0; i < s.size(); i++)
{
    cout << s.c_str()[i];
}

但是对于数组, 容器而言, 由我们手动指定范围是重复, 易错的。比如写错边界条件, 多写了一堆本应该由编译器推导的逻辑

为此, C++引入了基于范围的for循环, 简化了这一过程

std::string s("hello");
for(auto& e : s) // 自动迭代, 自动结束
{
    cout << e;
}

范围for的语法:

for(类型 变量 : 被遍历的对象)

本质上是这段代码的语法糖

for(auto& it = s.begin(); it != s.end(); ++i)
{
    cout << *it;
}

也就是说, 编译器会自动生成迭代器代码来遍历容器

只要我们实现了 begin(), end() 成员函数, 我们的模拟string类也可以被范围for使用

范围 for 循环是一种更自然、更安全的遍历方式,既能减少代码量,又能提升可读性。背后依赖的仅仅是 begin() 和 end(),如果我们理解了迭代器,就完全可以掌握它的本质。

使用迭代器的遍历

我们可以通过迭代器对string类中的字符进行增删查改; 在string类中,我们可将其视为指针;

// 使用范围for进行正向遍历
for(auto& e : s)
{
    cout << ++e; // 可以遍历的过程中修改
}
// 反向迭代器进行逆向遍历
auto rit = s.rbegin();
while(rit != s.rend())
{
    cout << *rit;
    ++rit;
}

其中,const类型迭代器不允许修改对象的值;所有以"c"开头的接口都会返回const迭代器。

string::const_iterator cit = s.cbegin();
while(cit != s.cend())
{
    // *cit = '0'; 错误: const_iterator不允许修改元素
    cout << *cit;
    ++cit;
}

 3.3 元素访问

string类的元素访问有以下四个接口

关于 operator[] 和 at, 他们都是通过索引下标来访问字符串中的各个元素, 而不同点则在于边界检查

operator[] 通常不进行边界检查, 如果访问越界的话会导致未定义行为, 而 at 则会进行边界检查, 弱访问越界则会抛出异常

因为 operator[] 不会做边界检查, 所以性能上略高于 at, 当我们需要高频访问并且确定所以南区时, 通常使用 operator[] 更高效

char& operator[](size_t index)
{
    return _data[index]; // 无边界检查, 直接返回
}

char& at(size_t index)
{
    if(index >= _size)
    {
        // 检查是否越界, 由于还没有介绍异常, 所以这里暂不使用
        cout << "Index out of range" << endl;
        assert(false);
    }
    return _data[index];
}

补充说明:front() 和 back() 分别返回字符串首字符和尾字符的引用,它们本质上是对 _data[0] 与 _data[_size-1] 的封装,行为类似于 operator[],可读可写,也无需额外实现。

4 string类的增删查改

4.1 插入

尾插字符/字符串

这类操作用于将字符或字符串追加到末尾,是最常见的插入方式。

// 尾部插入字符
void push_back(char ch)
{
    // 如果空间满了执行扩容
    if(_size == _capacity)
    {
        // 如果当前字符串为空, 则增加4个空间, 否则就2倍扩容
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }
    _data[_size] = ch;     // 尾插元素
    _data[++_size] = '\0'; // _size加1同时仍以'\0'结尾
}
// 末尾添加字符串
void append(const char* str)
{
    // 先判断是否需要扩容
    size_t len = strlen(str);
    if(_size + len > _capacity)
    {
        // 只需在原长度基础上增加待追加字符串长度,不必2倍扩容。
        reserve(_size + len);
    }

    // 直接从 _data + _size的位置拷贝 len个字符
    memcpy(_data + _size, str, len);
    _size += len;
    _data[_size] = '\0'; // 手动补上字符串结束符
}

operator+= 也常用于在字符串末尾追加内容,它的功能与 append() 类似,但语义上更偏向于简洁直观的拼接操作。相比之下,append() 作为成员函数,通常提供了更多的重载版本(如接受子串、重复字符等),在复杂场景中更加灵活。

string& operator+=(const char* str)
{
    append(str);  // 直接复用即可
    return *this; // 返回当前对象以支持连续调用
}

指定位置插入字符/字符串

在指定位置插入字符/字符串通常由 insert()的几种重载版本来完成

// 指定位置插入字符
iterator insert(iterator pos, char ch)
{
    // 边界检查, 防止用户插入非法的范围
    // 在 end()位置插入是被允许的, 相当于 push_back尾插
    if(pos < begin() || pos > end())
        assert(false);    

    // 检查是否需要扩容
    if(_size == _capacity)
    {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    }

    iterator end = end();
    while(end != pos)
    {    
        // 挪动数据
        *end = *(end - 1);
        --end;
    }
    *pos = ch;    

    _size++;
    _data[_size] = '\0'; // 结尾添加'\0'
    
    return pos; // 返回插入字符的位置 
}
// 指定位置插入字符串
string& insert(size_t pos, const char* str)
{
    // 允许在 _size位置插入, 等同于 append()
    assert(pos <= _size);
    
    // 检查越界
    size_t len = strlen(str);
    if(_size + len >= _capacity)
        reserve(_size + len);
    
    // 挪数据
    size_t end = _size + len;
    while(end >= pos + len)
    {
        _data[end] = _data[end - len];
        end--;
    }
    // 拷贝新字符串到空出的位置, 这里注意要使用 strncpy, 不会添加'\0'
    strncpy(_data + pos, str, len);

    _size += len;
    _data[_size] = '\0';
    return *this;
}

4.2 删除

字符串删除通常分为两种:

  • 删除某个位置上的字符

  • 删除一段连续区间的字符

而这两种我们通常使用 erase 的三个重载版本完成

在字符串接口中,npos 是 string类的一个静态成员常量,通常被定义为:

static const size_t npos = -1;

由于 size_t 是无符号整型,将 -1 赋值给它后,npos 实际上变成了一个非常大的正整数,代表无穷长度。因此,当我们调用 erase(pos, npos) 时,等价于从 pos 开始删除字符串中所有剩余字符。

 删除某个位置上的字符

// 当 len == npos 或删除区间超出字符串长度时,直接将字符串裁剪到 pos 位置
// 否则,就将删除区间后的字符逐个向前覆盖
string& erase(size_t pos, size_t len = npos)
{
    assert(pos < _size); // 越界检查
    
    // 如果删除的长度超过了字符串末尾, 直接裁剪即可
    if(pos + len >= _size || len == npos)
    {
        _size = pos;
        _data[_size] = '\0';
    }
    // 挪数据
    else
    {
        size_t end = pos + len;
        while(end < _size)
        {
            _data[end - len] = _data[end];
            end++;
        }
        _size -= len;
        _data[_size] = '\0';
    }
    return *this;
}
iterator erase(iterator pos)
{
    // 判断删除位置是否合法
    if(pos < begin() || pos >= end())
        assert(false);
    
    // 将 pos 后的数据前移覆盖
    iterator next = pos + 1;
    while(next != end())
    {
        *(next - 1) = *next;
        ++next;
    }
    _size--;
    _data[_size] = '\0'; // 保证字符串合法性
    
    return pos; // 返回删除位置后的迭代器
}

删除一段连续区间的字符

erase(iterator first, iterator last) 删除 [ first, last ) 范围内的字符。其原理是将 last 之后的内容依次前移覆盖 first 起始的位置,再更新 _size 和末尾的 \0 

iterator erase(iterator first, iterator last)
{
    assert(first < last);
    assert(first >= begin() && last < end());
    size_t len = last - first; // 被删除的区间长度

    // 将区间后的数据前移覆盖
    while(last != end())       
    {
        *first = *last;
        ++first;
        ++last;
    }
    
    _size -= len;
    _data[_size] = '\0';
    
    return first;
}

4.3 查找

string类的查找函数会在找到匹配内容时返回首个匹配项的位置索引,未找到则返回npos常量值。

查找字符

size_t find(char ch, size_t pos = 0) const
{
    // 边界检查
    assert(pos < _size);
    
    // 遍历整个字符串进行查找
    for(int i = pos; i < _size; ++i)
    {
        // 找到了则返回下标
        if(_data[i] == ch)
            return i;
    }
    // 找不到则返回 npos
    return npos;
}

查找子串

在查找子串时我们可以使用C语言标准库中的 strstr 来完成

size_t find(const char* str, size_t pos = 0) const
{
    assert(str != nullptr && pos < _size);
    const char* ptr = strstr(_data + pos, str);
    
    // strstr查找到会返回对应位置的指针
    // 与字符串起始指针相减,可以得到匹配位置的索引下标
    if(ptr)
        return ptr - _data;
    
    // 没找到则返回npos
    return npos;
}

4.4 修改

字符串的“改”操作通常通过 replace() 完成,其内部原理是先删除原有字符,再插入新内容。这个接口既简洁又高效,实质是对已有 erase + insert 的组合复用,逻辑清晰而且易于维护。

string& replace(size_t pos, size_t len, const char* str)
{
    assert(pos < _size);

    // 复用erase清理[pos, pos + len)区间
    erase(pos, len);   

    // 在已清理的区间上进行插入已达到替换的效果
    insert(pos, str);

    return *this;
}

5. string类的非成员函数

在string 中,一些常见的非成员函数能大幅提升类的实用性,例如支持流插入/提取运算符,使其能像原生字符串一样用于输入输出;再比如字符串之间的比较操作,为容器排序和查重提供了便利。

我们重点介绍三种常见的实现方式:

5.1 流插入运算符 

当需要访问类的私有成员变量(如string类)时,可以将流插入运算符(<<)和流提取运算符(>>)声明为该类的友元函数。

friend ostream& operator<<(ostream& out, const string& str)
{
    out << str._data << endl;
    return out;
}

5.2 流提取运算符

在实现流提取运算符时,我们可以采用缓冲区策略:先将用户输入的数据暂存到一个临时缓冲区中,等输入完成后再一次性赋值给字符串对象。这种做法不仅简化了处理流程,还能避免频繁扩容和重复插入操作带来的性能开销

friend istream& operator>>(istream& is, string& str)
{
    str.clear(); // 先清空数据

    const size_t N = 1024; // 定义缓冲区大小
    char buffer[N];        // 定义缓冲区
    
    char ch = is.get();    // get从流中将读到的字符返回
    int i = 0;
    while(ch != ' ' && ch != '\n')
    {
        buffer[i++] = ch;
        
        // 如果缓冲区满了, 赋值给字符串
        if(i == N - 1)
        {
            buffer[i] = '\0';
            str += buffer;
            i = 0;
        }
        ch = is.get();
    }
    
    // 处理用户输入空格或者回车后缓冲区没来得及刷新的问题
    if(i > 0)
    {
        buffer[i] = '\0';
        str += buffer;
    }

    return is;
}

流提取运算符会跳过输入前导空白(如空格、换行、Tab),读取第一个连续的非空白字符串。通过使用缓冲区的策略,每次将读取的字符先填入缓冲区,等缓冲区满或输入结束后再刷新到字符串中,避免频繁扩容,提升性能。

 

5.3 getline

与流提取运算符不同,getline 会从输入流中读取整行文本,包括空格,直到遇到指定的分隔符(默认是换行符 \n)为止而流提取运算符则会将空格、换行符等视为分隔符,因此它通常只能读取一个单词而不是一整行。

 

istream& getline(istream& is, string& str, char delim = '\n') // 分隔符默认为\n
{
    str.clear();
    
    const size_t N = 1024; // 缓冲区大小
    char buffer[N];        // 定义缓冲区

    char ch = is.get();
    size_t i = 0;
    
    while(ch != delim)
    {
        buffer[i++] = ch;
        // 如果缓冲区满了则全部追加到字符串
        if(i == N - 1)
        {
            buffer[i] = '\0';
            str += buffer;
            i = 0;
        }

        ch = is.get();
    }
    // 处理读取结束后缓冲区残留的问题
    if(i > 0)
    {
        buffer[i] = '\0';
        str += buffer;
    }
    
    return is;
}

getline 通常用于读取整行输入,包括空格,直到遇到指定的分隔符(默认是换行符 \n)为止

5.4 比较运算符重载

我们模拟实现的 string类同样可以重载比较运算符,使我们能够像使用原生 std::string 一样,直接对字符串对象进行大小比较、相等性判断等操作。这些运算符本质上都是基于字符串的字典序比较进行实现的。

下面我们挑选两个最常见的进行实现,其余可通过调用这些基础运算符间接实现。

bool operator==(const string& str1, const string& str2)
{ return strcmp(str1.c_str(), str2.c_str()) == 0; }
bool operator>(const string& str1, const string& str2)
{ return strcmp(str1.c_str(), str2.c_str()) > 0; }

其他比较运算符(如 !=、>、<=、>=)可基于 == 和 < 进行复合实现,例如:

bool operator!=(const string& st1, const string& str2)
{ return !(str1 == str2); }
bool operator>=(const string& str1, const string& str2)
{ return (str1 > str2) || (str1 == str2); }

三. 完整接口一览表

类别函数名 / 操作符说明
构造函数MyString(const char*)C风格字符串构造
拷贝构造MyString(const MyString&)深拷贝
赋值运算符operator=拷贝赋值,释放旧空间
容量相关size(), capacity(), reserve(), resize()控制字符串大小与容量
访问与遍历operator[], at(), begin(), end()元素访问与遍历
修改append(), insert(), erase(), clear(),repace()字符串内容变更
比较操作==, <, !=, >字典序比较
IO操作operator<<, operator>>, getline()流输入输出

四. 不同平台下的 std::string 结构

虽然我们平时使用的是统一的 std::string 接口,但在不同编译器和标准库实现下,其底层结构与优化机制可能有显著差异,尤其是 是否启用 SSO(Small String Optimization) 和是否 使用引用计数(Copy-On-Write)

1. MSVC(Visual Studio)下的 str::string

采用 SSO(小字符串优化)机制

内部通常内置一个固定大小的字符数组(例如16字节),用于存储短字符串以避免堆内存分配。当字符串长度超出预置容量时,系统会自动转为动态分配方式(使用指针存储)。

其结构大致如下:

union {
    char _buf[16];  // 小字符串缓冲区
    char* _ptr;     // 大字符串堆空间指针
};
size_t _size;
size_t _capacity;

通过让 _buf 和 _ptr 共用内存空间来避免冗余字段,达到了在不增加对象大小的前提下,支持小字符串快速存储和大字符串灵活扩容的双重目的。

2. g++ 下的 std::string

C++11 之前:使用引用计数机制(Copy-On-Write)

多个字符串对象可共享一段内存,只在写入时才进行拷贝(懒拷贝)。其具体实现机制和工作原理如下:

  • 内存共享机制 当创建新的字符串对象时,如果内容与已有字符串相同,系统不会立即分配新的内存,而是让多个对象引用同一内存块。例如:
string str1 = "hello";
string str2 = str1; // 此时 str2 与 str1 共享内存
  • 写时复制触发条件 只有当某个字符串对象需要被修改时,系统才会真正执行拷贝操作:
string str1 = "hello"
string str2 = str1 // 共享内存
str2 += " world"   // 此时才会进行实际的内存拷贝

C++11 之后:禁用 COW,启用 SSO

和 MSVC 一样,当字符串长度较小时使用栈上空间存储,提高性能。超过限制时转为堆分配。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值