【初阶C++】string

前言

在 C 语言里我们处理字符串依靠char*字符指针 + strcpystrcatstrlen等库函数。C 语言字符串是以\0作为结束标记的字符集合。但是 C 风格字符串有非常多痛点:

  1. 字符串数据和操作函数分离,不符合面向对象 OOP 封装思想;
  2. 字符内存空间需要程序员手动申请、手动释放,极易内存泄漏;
  3. 越界访问风险高,库函数不会做边界检查;

所以 C++ 标准库提供了std::string字符串类,把字符串数据和成员方法封装在一起。现在算法 OJ、工业开发绝大多数场景优先使用string,很少直接使用 C 字符串函数。

两个经典面试前置题(后面代码实现):

把字符串转换成整数_牛客题霸_牛客网

415. 字符串相加 - 力扣(LeetCode)


一、前置语法:C++11 auto 与范围 for 循环

使用 string 的时候经常会用到 C++11 两个语法特性:auto类型推导、范围 for 循环

1.1 auto 关键字

C++11 之前auto代表局部自动存储变量,这个用法被废弃。C++11 重新定义 auto:编译期自动推导变量的实际类型

auto 核心规则
  1. auto 推导指针:autoauto*效果完全等价;
  2. auto 推导引用:想要引用必须显式写auto&
  3. 同一行多个 auto 变量,推导出来必须是同一种类型,编译报错;
  4. auto不能做函数形参;可以作为函数返回值,但不推荐频繁使用;
  5. auto 不能直接用来定义数组;
  6. auto 定义变量必须给初始值,编译器没有初始值无法推导类型。

示例代码:

复制代码
#include<iostream>
#include<string>
#include<map>
using namespace std;

int func1()
{
    return 10;
}
// 错误 auto不能做函数参数
// void func2(auto a){}

// 允许auto返回值,谨慎使用
auto func3()
{
    return 3;
}

int main()
{
    int a = 10;
    auto b = a;        // b推导为int
    auto c = 'a';      // c推导为char
    auto d = func1();  // d推导int

    // auto e; // 编译报错!没有初始值无法推导

    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;

    int x = 10;
    auto y = &x;   // int*
    auto* z = &x;  // int* 和auto效果一样
    auto& m = x;   // int& 引用必须加&

    // auto aa = 1, bb = 2; // OK,都是int
    // auto cc = 3, dd = 4.0; // 编译报错,推导类型不一致

    // auto array[] = {4,5,6}; // 报错 auto不能定义数组

    // auto最常用场景:迭代器简化书写
    map<string, string> dict = {{"apple","苹果"},{"orange","橙子"},{"pear","梨"}};
    // std::map<std::string,std::string>::iterator it = dict.begin();
    auto it = dict.begin();
    while(it != dict.end())
    {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
    return 0;
}

1.2 范围 for 循环(C++11)

对于有范围的集合,不需要手动控制下标,自动遍历全部元素,自动判断结束。 语法格式:for(变量 : 被遍历对象)

  • 底层本质:编译器会替换成迭代器实现,汇编代码可以验证;
  • 支持数组、string、STL 容器;
  • 如果想要修改容器内元素,必须使用引用auto&;只读遍历用普通auto

示例代码:

复制代码
#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;
    }

    // C++11 范围for,引用才能修改原数组
    for(auto& e : array)
    {
        e *= 2;
    }

    // 只读遍历
    for(auto e : array)
    {
        cout << e << " ";
    }
    cout << endl;

    string str("hello world");
    for(auto ch : str)
    {
        cout << ch << " ";
    }
    return 0;
}

二、std::string 常用接口大全

使用 string 头文件:#include <string>,属于 std 命名空间。

2.1 string 常见构造函数

表格

构造函数 功能说明
string() 构造空字符串对象
string(const char* s) C 风格char*字符串构造 string
string(size_t n, char c) 生成 n 个字符 c 组成的字符串
string(const string& s) 拷贝构造函数
复制代码
void TestStringConstruct()
{
    string s1;                // 空字符串
    string s2("hello bit");   // char*构造
    string s3(5, 'x');        // "xxxxx"
    string s4(s2);            // 拷贝构造 s4复制s2内容
}

2.2 string 容量操作

表格

函数 功能说明
size() 返回有效字符长度(推荐优先使用)
length() 返回有效字符长度,和 size 底层完全等价
capacity() 获取底层开辟空间总容量大小
empty() 判断字符串是否为空串,空返回 true
clear() 清空有效字符,不会释放底层内存空间
reserve(n) 预分配预留空间,改变 capacity,不改变有效字符 size
resize(n) / resize(n, c) 修改有效字符个数到 n,扩容时填充字符
⚠️重点注意事项
  1. size()length()底层一模一样;引入 size 是为了和 vector 等其他 STL 容器接口保持统一,开发统一写size()
  2. clear():只把有效字符清空,底层堆空间 capacity 不变,不会释放内存;
  3. resize
    • 如果新 n > 当前 size:扩容;resize(n)'\0'填充新增位置;resize(n,c)用字符 c 填充;扩容有可能改变 capacity;
    • 如果新 n < 当前 size:截断字符串,底层 capacity 容量不会缩小
  4. reserve(n)只预分配堆内存,不改变有效字符数量 size。传入参数小于当前 capacity,不会做任何操作。

性能优化技巧:如果预先知道字符串大概多大,提前 reserve 预留空间,避免频繁扩容拷贝。

2.3 string 访问与遍历

表格

接口 说明
operator[] 下标访问 s[pos];普通对象可读可写,const 对象只读
begin() + end() 正向迭代器;begin 指向首字符,end 指向最后一个字符的下一位
rbegin() + rend() 反向迭代器;rbegin 指向末尾字符,rend 指向第一个字符前
范围 for C++11 简洁遍历方式
复制代码
void TestVisit()
{
    string s("hello");
    // 下标访问
    for(size_t i=0;i<s.size();i++)
    {
        cout << s[i];
    }
    cout << endl;

    // 正向迭代器
    string::iterator it = s.begin();
    while(it != s.end())
    {
        cout << *it;
        ++it;
    }
    cout << endl;

    // 反向迭代器
    string::reverse_iterator rit = s.rbegin();
    while(rit != s.rend())
    {
        cout << *rit;
        ++rit;
    }
    cout << endl;

    //范围for
    for(auto ch : s)
        cout << ch;
}

2.4 string 修改操作

表格

接口 功能
push_back(c) 尾部插入单个字符 c
append(str) 尾部追加字符串
operator+= ⭐最常用,追加字符 / 字符串
c_str() 返回 C 风格const char*字符串指针(带\0
find(pos,c) 从 pos 位置向后查找字符 / 子串,返回下标;找不到返回string::npos
rfind(pos,c) 从 pos 位置向前逆向查找字符 / 子串
substr(pos,len) 从 pos 下标截取 len 长度子串返回

补充: 尾部追加三种写法: s.push_back('a');s.append(1,'a');s += 'a';;实际开发优先用+=,写法最简单,既支持字符,也支持字符串。 c_str()非常重要:用来和 C 语言接口交互。 string::npos是 size_t 无符号静态常量,代表查找失败。

2.5 string 非成员函数

表格

函数 功能
operator+ 字符串拼接;⚠️传值返回会产生深拷贝,性能差,尽量少用,优先 +=
operator>> cin 输入字符串,遇到空格 / 换行停止读取
operator<< cout 输出字符串
getline(cin,s) ⭐读取一整行,包含空格,直到换行符停止
关系运算符> < == != 重载字符串字典序比较

坑点:cin >> s遇到空格直接截断;读取带空格文本必须用getline

三、VS 与 g++ 下 std::string 底层结构差异(32 位平台)

32 位下指针占 4 字节,两个编译器 string 底层模型完全不一样。

VS (MSVC) string :短字符串优化 SSO,总共 28 字节

内部有联合体_Bxty

  • 字符串长度 <16 字节:使用对象内部栈上固定数组_Buf[16]存储字符串,不需要堆内存,效率高;
  • 字符串长度 >=16 字节:联合体切换,指针指向堆上开辟空间;

对象成员:联合体 (16 字节)+size 有效长度 (4)+capacity 容量 (4)+ 额外指针 (4) = 28 字节。

SSO 短字符串优化:绝大多数短字符串不走堆分配,减少内存申请开销。

g++(libstdc++) string:写时拷贝 Copy‑On‑Write COW,对象占 4 字节

string 对象本身只有一个指针,指向堆上一块结构体:

复制代码
struct _Rep_base
{
    size_type _M_length;    //有效长度
    size_type _M_capacity;  //总容量
    _Atomic_word _M_refcount; //引用计数
};

多个 string 对象可以共享同一份堆字符串数据,依靠引用计数记录多少对象共用资源。只有修改内容的时候,才真正拷贝一份独立数据(写时拷贝)

⚠️注意:新版本部分 STL 实现已经弃用 COW 写时拷贝,因为迭代器失效等坑较多。

四、OJ 实战 string 算法练习题(2026 刷题必备)

案例 1:仅仅反转字母917. 仅仅反转字母 - 力扣(LeetCode)

题目:字符串只反转字母,其他符号保持原位

复制代码
#include<string>
using namespace std;
class Solution {
public:
    bool isLetter(char ch)
    {
        if(ch >= 'a' && ch <= 'z') return true;
        if(ch >= 'A' && ch <= 'Z') return true;
        return false;
    }
    string reverseOnlyLetters(string S) {
        if(S.empty()) return S;
        size_t begin = 0, end = S.size()-1;
        while(begin < end)
        {
            while(begin < end && !isLetter(S[begin])) begin++;
            while(begin < end && !isLetter(S[end])) end--;
            swap(S[begin], S[end]);
            begin++;
            end--;
        }
        return S;
    }
};

案例 2:第一个只出现一次的字符 387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        int count[256] = {0};
        int sz = s.size();
        for(int i=0;i<sz;i++)
        {
            count[(unsigned char)s[i]]++;
        }
        for(int i=0;i<sz;i++)
        {
            if(count[(unsigned char)s[i]] == 1)
                return i;
        }
        return -1;
    }
};

案例 3:最后一个单词长度字符串最后一个单词的长度_牛客题霸_牛客网

注意不能用 cin >> line,cin 空格截断,要用 getline 读取整行;使用 rfind 反向找空格。

复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string line;
    while(getline(cin, line))
    {
        size_t pos = line.rfind(' ');
        cout << line.size() - pos - 1 << endl;
    }
    return 0;
}

案例 4:验证回文字符串 125. 验证回文串 - 力扣(LeetCode)

只考虑字母数字,忽略大小写、符号

复制代码
class Solution {
public:
    bool isLetterOrNumber(char ch)
    {
        return (ch >= '0' && ch <= '9')
            || (ch >= 'a' && ch <= 'z')
            || (ch >= 'A' && ch <= 'Z');
    }
    bool isPalindrome(string s) {
        //统一转大写
        for(auto& ch : s)
        {
            if(ch >= 'a' && ch <= 'z')
                ch -= 32;
        }
        int begin = 0, end = s.size()-1;
        while(begin < end)
        {
            while(begin < end && !isLetterOrNumber(s[begin])) begin++;
            while(begin < end && !isLetterOrNumber(s[end])) end--;
            if(s[begin] != s[end])
                return false;
            begin++;
            end--;
        }
        return true;
    }
};

案例 5:字符串大数相加(字符串模拟数字加法)415. 字符串相加 - 力扣(LeetCode)

数字字符串长度很大,超出 long long 范围,从后往前模拟竖式加法

复制代码
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1;
        int end2 = num2.size()-1;
        int value1 = 0, value2 = 0, next = 0;
        string addret;
        while(end1 >=0 || end2 >=0)
        {
            if(end1 >= 0)
                value1 = num1[end1--] - '0';
            else
                value1 = 0;

            if(end2 >=0)
                value2 = num2[end2--] - '0';
            else
                value2 = 0;

            int valueret = value1 + value2 + next;
            if(valueret >9)
            {
                next = 1;
                valueret -=10;
            }
            else
            {
                next =0;
            }
            addret += (valueret + '0');
        }
        if(next ==1)
        {
            addret += '1';
        }
        reverse(addret.begin(), addret.end());
        return addret;
    }
};

课后 OJ 练习清单:

  1. 翻转字符串 II(区间翻转)541. 反转字符串 II - 力扣(LeetCode)
  2. 翻转字符串 III(翻转字符串里单词)557. 反转字符串中的单词 III - 力扣(LeetCode)
  3. 字符串相乘43. 字符串相乘 - 力扣(LeetCode)
  4. 找出字符串第一个只出现一次字符找出字符串中第一个只出现一次的字符_牛客题霸_牛客网

五、模拟实现 string:浅拷贝、深拷贝、写时拷贝(面试高频)

面试高频考点:手写简易 String 类;构造、拷贝构造、赋值重载、析构。

5.1 浅拷贝问题

如果类内部管理堆资源,不自己写拷贝构造、赋值重载,编译器自动生成默认拷贝,做浅拷贝(值拷贝)

示例错误代码:

复制代码
#include<cassert>
#include<cstring>
class String
{
public:
    String(const char* str = "")
    {
        if(nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str)+1];
        strcpy(_str, str);
    }
    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

void TestString()
{
    String s1("hello bit!!!");
    String s2(s1); //编译器默认拷贝构造,浅拷贝!
}

问题分析: s1s2内部_str指针指向同一块堆内存 。函数结束析构对象,s2 先析构释放堆空间;s1 析构的时候再次对同一块内存执行delete[]重复释放内存,程序直接崩溃

浅拷贝定义:位拷贝,仅仅拷贝成员变量的值。多个对象共享同一份堆资源;一个对象销毁释放资源,剩下对象变成野指针,访问崩溃。

解决办法:深拷贝,每个对象拥有独立堆内存资源。

5.2 深拷贝实现 String 类

核心思想:拷贝构造 / 赋值重载的时候,给新对象独立开辟堆内存,复制内容,不共享空间。

版本 1:传统写法深拷贝
复制代码
#include<cassert>
#include<cstring>
#include<iostream>
using namespace std;

class String
{
public:
    String(const char* str = "")
    {
        if(nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str)+1];
        strcpy(_str, str);
    }
    //拷贝构造:深拷贝
    String(const String& s)
    {
        _str = new char[strlen(s._str)+1];
        strcpy(_str, s._str);
    }
    //赋值运算符重载
    String& operator=(const String& s)
    {
        if(this != &s) //防止自己给自己赋值
        {
            char* pNew = new char[strlen(s._str)+1];
            strcpy(pNew, s._str);
            delete[] _str; //释放旧空间
            _str = pNew;
        }
        return *this;
    }
    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};
版本 2:现代写法深拷贝(swap 交换技巧,更优雅)

思路:借助临时对象完成资源开辟,swap 交换指针资源,临时对象出作用域自动释放旧资源。

复制代码
class String
{
public:
    String(const char* str = "")
    {
        if(nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str)+1];
        strcpy(_str, str);
    }
    //拷贝构造现代版
    String(const String& s)
        :_str(nullptr)
    {
        String tmp(s._str); //调用构造函数开辟资源
        swap(_str, tmp._str); //交换指针
    }
    //赋值重载:传值参数,自动生成临时对象
    String& operator=(String s)
    {
        swap(_str, s._str);
        return *this;
    }
    ~String()
    {
        if(_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

现代写法优点:代码简洁;new 失败抛出异常的时候,不会破坏当前对象原有资源;不需要手动释放旧空间。

5.3 写时拷贝 COW(了解)

写时拷贝相当于 "拖延版浅拷贝":

  1. 拷贝的时候不立刻复制内存,多个对象共享同一份堆资源;堆上增加引用计数统计多少对象共用;
  2. 新增对象共享资源,引用计数 ++;对象析构,引用计数 --;计数减到 0,才真正释放堆内存;
  3. 只有当某个对象要修改字符串内容的时候,才真正拷贝一份独立空间出来再修改(写的时候才拷贝)。

优点:大量拷贝字符串但不修改的时候节省内存,减少拷贝开销。 缺点:读操作也需要操作引用计数;部分场景迭代器失效;现在 MSVC string 没有使用 COW,g++ 老版本 libstdc++ 实现 COW。现代 STL 更倾向短字符串优化 SSO。

总结

  1. C 语言字符串需要手动管理内存,容易越界,C++ 优先用std::string
  2. auto编译期类型推导;范围 for 简化遍历容器数组;
  3. string 接口分清:容量操作 (size/reserve/resize/clear)、修改操作 (+=、find、substr、c_str);getline 读取带空格字符串;
  4. VS 短字符串优化 SSO;g++ 旧版本写时拷贝 COW;
  5. 资源管理类:默认拷贝是浅拷贝;共享堆内存会重复释放崩溃;必须实现深拷贝;分为传统写法和现代 swap 写法;
  6. OJ 高频字符串题:反转字符、大数相加、回文判断;
  7. 面试必背:手写 string 深拷贝,区分浅拷贝、深拷贝、写时拷贝三者差异。

拓展阅读:

面试中 string 类踩坑点、STL string 底层源码分析。

STL 的string类怎么啦?_string 截取复杂度 c++-CSDN博客

相关推荐
Dream Cosmos18 分钟前
C++继承详解(上):继承规则、父子类转换与作用域隐藏
c++
skr爱码士19 分钟前
15_国际化和本地化:tr()、ts 文件、QM 文件、多语言切换
c++·qt·ui·客户端
小玮看世界34 分钟前
[Python]动态规划三步走:从爬楼梯到打家劫舍,再到最大子数组和
开发语言·python·动态规划
AI视觉网奇39 分钟前
3d子部件高亮选中
开发语言·javascript·3d
汉克老师43 分钟前
CSP-J 初赛(以满分为目标):第二十一课 《树与二叉树基础—— 一棵树,为什么会成为计算机最重要的数据结构之一?》
c++·csp-j·小学生·学c++编程
zzzll11111 小时前
深入理解 Java HashMap:从原理到实战
java·开发语言·哈希算法
猫猫不是喵喵.1 小时前
Java开发高频面试题汇总(通俗易懂版)
java·开发语言·java高频面试
k4m7v2pz1 小时前
用 rust-verb-shell 重写进程管理:从 game.sh 到 .rvs 的迁移实录
开发语言·后端·rust
2603_965896621 小时前
JavaScript 对象零基础详解|属性、方法、遍历、增删改查
开发语言·javascript·ecmascript