C++第八讲:string 类

C++第八讲:string 类

string 是STL 中最常用的容器,也是所有 C++ 开发者每天都会用到的工具。它彻底解决了 C 语言字符串操作繁琐、容易越界、需要手动管理内存的痛点。


一、为什么必须学 string 类?

1. C 语言字符串的致命缺陷

C 语言中字符串是以\0结尾的字符数组,操作依赖<string.h>库函数,存在以下问题:

  1. 内存手动管理:需要自己申请 / 释放空间,容易内存泄漏

  2. 容易越界访问:strcpy、strcat 等函数不检查边界,导致缓冲区溢出

  3. 不符合面向对象思想:数据和操作分离,使用麻烦

  4. 功能单一:很多常用操作(如查找、替换、截取)需要自己实现

2. C++ string 类的优势

  • 自动管理内存,无需手动申请释放

  • 重载了常用运算符(++=[]==等),操作直观

  • 提供了丰富的成员函数,满足绝大多数字符串操作需求

  • 类型安全,不容易出现越界错误

一句话:工作中 99% 的字符串场景都用 string,几乎没人用 C 语言的字符数组


二、前置知识:C++11 两个核心语法

在学习 string 之前,先掌握两个 C++11 的重要语法,后面会频繁用到。

1. auto 关键字:自动类型推导

作用

让编译器自动推导变量的类型,不用手动写复杂的类型名。

语法

cpp 复制代码
auto 变量名 = 初始值;

示例

cpp 复制代码
int a = 10;
auto b = a; // b自动推导为int
auto c = 'a'; // c自动推导为char
auto d = 3.14; // d自动推导为double
​
// 最常用场景:简化复杂类型
#include <map>
map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}};
// 不用写map<string, string>::iterator
auto it = dict.begin(); 

易错点

  1. 必须初始化auto e; 编译报错,没有初始值无法推导类型

  2. 同一行变量类型必须一致auto aa=1, bb=2.0; 报错,int 和 double 类型不同

  3. 声明引用必须加 &auto& m = a; 是引用,auto m = a; 是拷贝

  4. 不能作为函数参数void func(auto a); 编译报错

2. 范围 for 循环:简化遍历

作用

自动遍历数组、容器等有范围的集合,不用手动控制下标或迭代器。

语法

cpp 复制代码
for (元素类型 变量名 : 遍历范围) {
    // 操作变量
}

示例

cpp 复制代码
// 遍历数组
int arr[] = {1,2,3,4,5};
for (auto e : arr) {
    cout << e << " ";
}
​
// 遍历string
string s = "hello";
for (auto ch : s) {
    cout << ch << " ";
}
​
// 修改元素:加引用
for (auto& ch : s) {
    ch -= 32; // 转大写
}

原理

范围 for 的底层就是迭代器,编译器会自动替换为迭代器遍历。


三、string 类常用接口(重点)

使用 string 必须包含头文件:#include <string>,且所有接口都在std命名空间中。

1. 构造函数(4 个最常用)

构造函数 功能说明 示例
string() 构造空字符串 string s1;
string(const char* s) 用 C 风格字符串构造 string s2("hello");
string(size_t n, char c) 构造 n 个字符 c 的字符串 string s3(5, 'a'); // "aaaaa"
string(const string& s) 拷贝构造 string s4(s2);

代码示例

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;
​
int main() {
    string s1; // 空字符串
    string s2("hello world"); // 用C字符串构造
    string s3(5, 'x'); // "xxxxx"
    string s4(s2); // 拷贝s2
​
    cout << s1 << endl; // 空
    cout << s2 << endl; // hello world
    cout << s3 << endl; // xxxxx
    cout << s4 << endl; // hello world
    return 0;
}

2. 容量操作

函数 功能说明 注意事项
size_t size() const 返回有效字符长度 ✅ 推荐使用,和其他容器接口统一
size_t length() const 返回有效字符长度 和 size () 完全一样,历史遗留
size_t capacity() const 返回总容量(能存多少字符) 容量≥size,预留空间避免频繁扩容
bool empty() const 判断是否为空 空返回 true,否则 false
void clear() 清空有效字符 不改变底层容量
void reserve(size_t n) 预留 n 个字符的空间 只改容量,不改 size;n < 当前容量时无作用
void resize(size_t n, char c='\0') 把有效字符改为 n 个 n>size:用 c 填充;n<size:截断;可能改变容量

核心区别:reserve vs resize

  • reserve:只预留空间,不改变有效字符个数,用于提前预估大小,避免频繁扩容

  • resize:改变有效字符个数,会初始化新增的字符

代码示例

cpp 复制代码
int main() {
    string s = "hello";
    cout << s.size() << endl; // 5
    cout << s.capacity() << endl; // 15(VS下短字符串优化)
​
    s.reserve(100); // 预留100个字符空间
    cout << s.size() << endl; // 5(不变)
    cout << s.capacity() << endl; // 100
​
    s.resize(10, 'a'); // 有效字符改为10个,新增的用'a'填充
    cout << s << endl; // helloaaaaa
    cout << s.size() << endl; // 10
​
    s.resize(3); // 截断为3个字符
    cout << s << endl; // hel
    cout << s.size() << endl; // 3
    cout << s.capacity() << endl; // 100(不变)
​
    s.clear(); // 清空
    cout << s.size() << endl; // 0
    cout << s.capacity() << endl; // 100(不变)
    return 0;
}

3. 访问与遍历(3 种方式)

方式 语法 特点
operator[] s[pos] ✅ 最常用,像数组一样访问,支持读写
迭代器 begin()/end() 通用所有容器,支持反向迭代器rbegin()/rend()
范围 for for (auto ch : s) ✅ C++11 推荐,最简洁

代码示例

cpp 复制代码
int main() {
    string s = "hello";
​
    // 1. []访问(推荐)
    for (int i=0; i<s.size(); ++i) {
        cout << s[i] << " ";
        s[i] += 1; // 可以修改
    }
    cout << endl;
​
    // 2. 迭代器
    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;
​
    // 3. 范围for(最简洁)
    for (auto ch : s) {
        cout << ch << " ";
    }
    cout << endl;
​
    return 0;
}

4. 修改操作(最常用)

函数 功能说明 推荐度
void push_back(char c) 尾插一个字符 ⭐⭐
string& operator+=(const string& str) 追加字符串 / 字符 ⭐⭐⭐ 最常用
string& append(const char* s) 追加 C 风格字符串
const char* c_str() const 返回 C 风格字符串(const char*) ⭐⭐⭐ 用于和 C 语言接口交互
size_t find(char c, size_t pos=0) const 从 pos 开始找 c,返回下标,找不到返回string::npos ⭐⭐⭐
size_t rfind(char c, size_t pos=npos) const 从 pos 开始往前找 c ⭐⭐
string substr(size_t pos=0, size_t len=npos) const 从 pos 开始截取 len 个字符返回 ⭐⭐⭐

代码示例

cpp 复制代码
int main() {
    string s = "hello";
​
    // 追加
    s += ' '; // 追加字符
    s += "world"; // 追加字符串
    cout << s << endl; // hello world
​
    // 查找
    size_t pos = s.find('o');
    if (pos != string::npos) {
        cout << "找到'o'在位置:" << pos << endl; // 4
    }
​
    pos = s.find("world");
    if (pos != string::npos) {
        cout << "找到'world'在位置:" << pos << endl; // 6
    }
​
    // 截取子串
    string sub = s.substr(6, 5); // 从位置6开始截取5个字符
    cout << sub << endl; // world
​
    // 转C风格字符串
    const char* cstr = s.c_str();
    printf("%s\n", cstr); // hello world
​
    return 0;
}

5. 非成员函数

函数 功能说明 注意事项
operator+ 字符串拼接 ❌ 尽量少用,传值返回会产生深拷贝,效率低
operator>> 输入字符串 遇到空格、换行结束
operator<< 输出字符串 正常输出
getline(istream& in, string& s) 读取一行字符串 ✅ 读取带空格的字符串,遇到换行结束
relational operators 大小比较(==!=<>等) 按字典序比较

易错点:cin vs getline

  • cin >> s:遇到空格、制表符、换行就停止,无法读取带空格的字符串

  • getline(cin, s):读取整行,直到遇到换行符,会丢弃换行符

cpp 复制代码
int main() {
    string s;
    // 错误:输入"hello world"只会读取"hello"
    // cin >> s;
​
    // 正确:读取整行
    getline(cin, s);
    cout << s << endl;
    return 0;
}

四、string 的底层结构(面试高频)

不同编译器的 string 实现不同,主要有两种:VS 的短字符串优化G++ 的写时拷贝

1. VS 下的 string:短字符串优化(SSO)

结构(32 位平台占 28 字节)

  • 一个联合体:

    • 长度 < 16:用内部 16 字节的字符数组存储(栈上)

    • 长度≥16:用堆空间存储,联合体存指向堆的指针

  • size_t _Mysize:有效字符长度

  • size_t _Myres:总容量

  • 一个指针:用于其他管理

优势

大多数字符串长度都小于 16,直接用栈空间,不需要申请堆内存,效率更高。

2. G++ 下的 string:写时拷贝(COW)

结构(32 位平台占 4 字节)

  • 只有一个指针,指向堆上的控制块:

    • size_t _M_length:有效长度

    • size_t _M_capacity:总容量

    • _Atomic_word _M_refcount:引用计数

    • 后面跟着实际的字符串数据

原理

多个 string 对象共享同一块堆内存,只有当某个对象修改字符串时,才会重新分配空间并拷贝数据(写时才拷贝)。

优势

拷贝构造和赋值效率极高,只需要拷贝指针和增加引用计数。


五、面试必考题:string 类的模拟实现

面试官几乎 100% 会让你手写 string 类的核心函数(构造、拷贝构造、赋值重载、析构),考察你对深拷贝和浅拷贝的理解。

1. 浅拷贝的问题

如果不自己实现拷贝构造和赋值重载,编译器会生成默认的浅拷贝,导致多个对象共享同一块内存,析构时重复释放,程序崩溃。

cpp 复制代码
// 错误的string实现(浅拷贝)
class String {
public:
    String(const char* str = "") {
        _str = new char[strlen(str)+1];
        strcpy(_str, str);
    }
​
    ~String() {
        delete[] _str;
        _str = nullptr;
    }
​
private:
    char* _str;
};
​
int main() {
    String s1("hello");
    String s2(s1); // 浅拷贝,s1和s2的_str指向同一块内存
    // 程序结束时,s2先析构释放内存,s1再析构时释放同一块内存,崩溃
    return 0;
}

2. 深拷贝的两种实现方式

方式 1:传统版(容易理解)

自己申请独立的空间,拷贝数据,每个对象有自己的资源。

cpp 复制代码
class String {
public:
    // 构造函数
    String(const char* str = "") {
        if (str == nullptr) {
            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* tmp = new char[strlen(s._str)+1];
            strcpy(tmp, s._str);
            // 释放旧空间
            delete[] _str;
            // 指向新空间
            _str = tmp;
        }
        return *this;
    }
​
    // 析构函数
    ~String() {
        if (_str) {
            delete[] _str;
            _str = nullptr;
        }
    }
​
private:
    char* _str;
};

方式 2:现代版(更简洁高效)

利用局部对象的析构函数自动释放资源,通过 swap 交换指针。

cpp 复制代码
class String {
public:
    String(const char* str = "") {
        if (str == nullptr) {
            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); // 交换指针,tmp析构时释放旧空间
    }
​
    // 赋值运算符重载(现代版)
    String& operator=(String s) { // 传值参数,自动拷贝构造
        swap(_str, s._str); // 交换指针,s析构时释放旧空间
        return *this;
    }
​
    ~String() {
        if (_str) {
            delete[] _str;
            _str = nullptr;
        }
    }
​
private:
    char* _str;
};

六、经典 OJ 实战(笔试必练)

1. 反转字符串中的字母

cpp 复制代码
class Solution {
public:
    bool isLetter(char ch) {
        return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
    }
​
    string reverseOnlyLetters(string s) {
        int left = 0, right = s.size()-1;
        while (left < right) {
            // 跳过非字母
            while (left < right && !isLetter(s[left])) left++;
            while (left < right && !isLetter(s[right])) right--;
            swap(s[left], s[right]);
            left++;
            right--;
        }
        return s;
    }
};

2. 字符串中第一个只出现一次的字符

cpp 复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        int count[256] = {0};
        // 统计每个字符出现次数
        for (char ch : s) {
            count[ch]++;
        }
        // 找第一个出现一次的字符
        for (int i=0; i<s.size(); ++i) {
            if (count[s[i]] == 1) {
                return i;
            }
        }
        return -1;
    }
};

七、本章核心总结

  1. string 是 STL 最常用的容器,自动管理内存,操作简单安全

  2. 常用接口:构造、size、operator []、+=、find、substr、c_str、getline

  3. 底层实现:VS 用短字符串优化,G++ 用写时拷贝

  4. 面试必考点:深拷贝的实现,浅拷贝的问题

  5. 易错点:cin 和 getline 的区别,operator + 的效率问题,clear 不改变容量

相关推荐
ch.ju1 小时前
Java programming Chapter Three——Array
java·开发语言
MOONICK1 小时前
bit7z压缩与解压
c++
Chase_______1 小时前
LeetCode 1493 & 3634 题解:滑动窗口双指针,从“删一个元素的全1子数组“到“最少移除使数组平衡“
算法·leetcode
努力努力再努力wz1 小时前
【Qt入门系列】第一个 Qt Widgets 程序:项目创建、UI 文件、Hello World、对象树与 qDebug 日志
java·c语言·开发语言·数据结构·c++·qt·ui
电子云与长程纠缠1 小时前
UE5 GameFeature创建与使用
开发语言·学习·ue5·游戏引擎
Hua-Jay1 小时前
OpenCV联合C++/Qt 学习笔记(十五)----形态学操作及应用
c++·笔记·qt·opencv·学习·计算机视觉
悲伤小伞1 小时前
LeetCode 热题 100_4-283. 移动零
算法·leetcode·职场和发展
_Evan_Yao2 小时前
零基础学编程,第一门语言选Python还是C?
c语言·开发语言·python
程序员老舅2 小时前
深入底层:Linux MMU 工作原理全解
linux·服务器·网络·c++·linux内核·内存管理·linux内存