C++8(string类)

📑 目录
1 为啥偏要学 string? 告别 C 语言字符串的 “坑”,少踩雷
2 标准库 string 类:必学招式 从创建到修改,手把手教你用对接口
3 不同编译器的小秘密 知道这些,调试不懵圈
4 OJ 刷题实战:5 道题练会核心用法 学完就上手,成就感拉满
5 模拟实现 string:搞定面试高频题 从原理到代码,初学者也能看懂的深拷贝
6 避坑 & 复习指南 划重点、防踩坑,复习不迷路

  1. 为啥偏要学 string?
    作为刚学 C++ 的小白,一开始我也疑惑:C 语言的char[]不是也能存字符串吗?为啥还要专门学string类?直到踩了这些坑才明白 ——
    1.1 C 语言字符串的 “血泪教训”
    新手写 C 语言字符串,最容易犯这几个错:
  • ✖️ 手动分配空间:比如char str[10]存 “hello world”,直接越界,程序崩了都不知道为啥;
  • ✖️ 忘记加’\0’:用strcpy拷贝时,少加结束符,打印出来全是乱码;
  • ✖️ 手动释放内存:用malloc申请的字符串,忘了free,妥妥的内存泄漏。
    举个我踩过的坑:
// 新手错误示范:空间不够+忘加'\0'
char str[5];
strcpy(str, "hello"); // "hello"是6个字符(含'\0'),直接越界
printf(str); // 输出乱码,甚至程序崩溃

1.2 string 类的 “新手友好 buff”

用string类之后,这些问题全没了:

  • ✅ 自动管理空间:存多长的字符串都不用手动分配,底层会自动扩容;
  • ✅ 自带操作函数:拼接、查找、比较都有现成方法,不用记strcat/strcmp这些零散函数;
  • ✅ 更安全:访问字符时越界会有提示(新手能快速定位问题),而 C 语言直接崩。

比如同样存 “hello”,string 只要一行:

string str = "hello"; // 不用管空间,不用加'\0',新手友好!

2. 标准库 string 类:必学招式
先划重点:用string必须加头文件,还要写using namespace std;(新手别忘,不然编译报错)。

2.1 先搞懂:string 到底是个啥?

  • 对新手来说,不用记复杂的模板定义,简单理解:
  • string就是 C++ 专门用来存字符串的 “容器”,把字符串的 “存、取、改、删” 都封装好了,我们不用关心底层咋实现,只管调用方法就行。

2.2 必会:创建 string 对象(构造函数)
这 4 种是最常用的,记牢就行:
在这里插入图片描述

#include <iostream>
#include <string>
using namespace std; // 新手别漏!

int main() {
    string s1; // 空字符串
    string s2("我是新手,学string"); // 存中文也没问题(注意编码)
    string s3(3, '*'); // 存"***"
    string s4(s2); // 拷贝s2,s4也是"我是新手,学string"
    
    cout << "s1是空的:" << s1 << endl;
    cout << "s2:" << s2 << endl;
    cout << "s3:" << s3 << endl;
    cout << "s4:" << s4 << endl;
    return 0;
}

2.3 必懂:容量操作(别被 “空间” 搞晕)
起初我们最容易搞混size()、capacity()、resize()、reserve(),用 “装水的杯子” 比喻帮你理解:

  • size():杯子里实际装了多少水(有效字符数);
  • capacity():杯子总共能装多少水(底层空间大小);
  • resize(n):把水的量改成 n 杯,不够就加水(补 ‘\0’),多了就倒掉;
  • reserve(n):把杯子的容量改成 n 杯,水的量不变(提前留空间,避免频繁换杯子)。
int main() {
    string s = "hello";
    cout << "当前长度:" << s.size() << endl; // 输出5
    cout << "当前容量:" << s.capacity() << endl; // 不同编译器结果不同,比如VS可能是15
    
    s.resize(8, '!'); // 改成8个字符,补"!!!"
    cout << "resize后:" << s << endl; // 输出"hello!!!"
    cout << "resize后长度:" << s.size() << endl; // 8
    cout << "resize后容量:" << s.capacity() << endl; // 还是15(没超原容量)
    
    s.reserve(20); // 预留20个空间
    cout << "reserve后容量:" << s.capacity() << endl; // 变成20
    cout << "reserve后长度:" << s.size() << endl; // 还是8(长度不变)
    
    s.clear(); // 清空内容
    cout << "clear后:" << s << endl; // 空字符串
    cout << "clear后容量:" << s.capacity() << endl; // 还是20(空间没释放)
    return 0;
}

2.4 必练:访问和遍历(3 种简单方法)
遍历字符串就是 “逐个看字符”,先掌握这 3 种,足够应付大部分场景:
方法 1:数组式访问

string s = "hello";
for (int i = 0; i < s.size(); i++) {
    cout << s[i] << " "; // 输出h e l l o
}

✅ 备注:s[i]可以直接改字符,比如s[0] = ‘H’,s 就变成 “Hello”。

方法 2:范围 for(C++11,最简洁)

string s = "hello";
for (char ch : s) { // 逐个取字符存到ch里
    cout << ch << " ";
}
// 想改字符就加&:for (char& ch : s) { ch = toupper(ch); }

✅ 备注:不用记下标,适合只想遍历不想改下标的场景

方法 3:迭代器(了解即可,后续学 STL 再深入)

string s = "hello";
for (string::iterator it = s.begin(); it != s.end(); it++) {
    cout << *it << " ";
}

✅ 备注:迭代器是 STL 通用的遍历方式,现在先知道有这个东西就行。

2.5 必用:修改字符串(拼接 / 查找 / 截取)
这部分是 string 最实用的功能,重点记+=、find、substr。
核心函数表
在这里插入图片描述

int main() {
    string s = "hello";
    s += " world"; // 拼接成"hello world"
    cout << s << endl;
    
    int pos = s.find('w'); // 找'w'的位置,返回6
    if (pos != string::npos) { // 找到才处理(npos表示没找到)
        cout << "找到'w',位置:" << pos << endl;
    }
    
    string sub = s.substr(6, 5); // 从6开始取5个字符,得到"world"
    cout << "截取的子串:" << sub << endl;
    
    // 转C语言字符串(新手用printf时需要)
    printf("C语言风格输出:%s\n", s.c_str());
    return 0;
}

✅ 避坑:find没找到会返回string::npos(值是 - 1),一定要判断,不然会出错!

2.6 必知:输入输出(别踩 cin 的坑)

  • cin >> s:输入字符串,但遇到空格 / 回车就停(比如输入 “hello world”,s 只存 “hello”);
  • getline(cin, s):输入整行字符串,包括空格(新手处理带空格的输入必用)。
int main() {
    string s1, s2;
    cout << "输入带空格的字符串(cin):";
    cin >> s1; // 输入"hello world"
    cout << "cin结果:" << s1 << endl; // 只输出"hello"
    
    cin.ignore(); // 清空缓冲区(不然getline会读空)
    cout << "输入带空格的字符串(getline):";
    getline(cin, s2); // 输入"hello world"
    cout << "getline结果:" << s2 << endl; // 输出"hello world"
    return 0;
}

✅ 备注:用cin之后接getline,一定要加cin.ignore()清空缓冲区,不然getline会直接读空!

3. 不同编译器的小秘密(不用深究,了解即可)
不用纠结底层实现,但知道这些能避免调试时懵圈:
3.1 VS 编译器(Windows 常用)

  • 用 “小字符串优化”:字符串长度 < 16 时,直接存在对象里(不用堆空间),效率高;
  • 长度≥16 时,才从堆上分配空间;
  • 新手看到capacity()初始值是 15 别慌(比如空字符串的 capacity 是 15),正常现象。
    3.2 g++ 编译器(Linux 常用)
  • 早期用 “写时拷贝”:多个对象共享空间,改的时候才拷贝;
  • 现在也逐渐用小字符串优化,新手知道 “不同编译器 capacity 结果不同” 就行。

4. OJ 刷题实战:5 道题练会核心用法

学完接口一定要刷题,这 5 道题都是新手能上手的,覆盖核心用法:
题 1:仅仅反转字母(双指针入门)
**题目:**只反转字符串中的字母,非字母保留原位(比如输入 “a-bC-dEf-ghIj”,输出 “j-Ih-gfE-dCba”)。
思路:
左指针从开头找字母,右指针从结尾找字母;
找到就交换,然后左指针右移,右指针左移;
直到左指针≥右指针。

#include <iostream>
#include <string>
#include <cctype> // isalpha函数头文件
using namespace std;

class Solution {
public:
    string reverseOnlyLetters(string s) {
        int left = 0; // 左指针
        int right = s.size() - 1; // 右指针
        while (left < right) {
            // 左指针找字母(跳过非字母)
            while (left < right && !isalpha(s[left])) {
                left++;
            }
            // 右指针找字母(跳过非字母)
            while (left < right && !isalpha(s[right])) {
                right--;
            }
            // 交换字母
            swap(s[left], s[right]);
            left++;
            right--;
        }
        return s;
    }
};

int main() {
    Solution sol;
    string s = "a-bC-dEf-ghIj";
    cout << "原字符串:" << s << endl;
    cout << "反转后:" << sol.reverseOnlyLetters(s) << endl;
    return 0;
}

题 2:找第一个只出现一次的字符(计数入门)
题目:返回字符串中第一个只出现一次的字符的下标(比如输入 “leetcode”,返回 0;输入 “loveleetcode”,返回 2)。
思路:
用数组统计每个字符出现的次数(ASCII 字符共 256 个,数组大小设 256);
再遍历字符串,找第一个计数为 1 的字符。

#include <iostream>
#include <string>
using namespace std;

class Solution {
public:
    int firstUniqChar(string s) {
        int count[256] = {0}; // 初始化为0
        // 第一步:统计每个字符出现次数
        for (char ch : s) {
            count[ch]++; // 字符转ASCII码当下标
        }
        // 第二步:找第一个计数为1的字符
        for (int i = 0; i < s.size(); i++) {
            if (count[s[i]] == 1) {
                return i;
            }
        }
        return -1; // 没找到返回-1
    }
};

int main() {
    Solution sol;
    string s1 = "leetcode";
    string s2 = "loveleetcode";
    cout << "s1第一个唯一字符下标:" << sol.firstUniqChar(s1) << endl; // 0
    cout << "s2第一个唯一字符下标:" << sol.firstUniqChar(s2) << endl; // 2
    return 0;
}

题 3:最后一个单词的长度(rfind 入门)
题目:输入一个字符串,返回最后一个单词的长度(比如输入 “Hello World”,返回 5;输入 “fly me to the moon”,返回 4)
思路:

  • 用rfind找最后一个空格的位置;
  • 总长度减去空格位置再减 1,就是最后一个单词的长度;
  • 没有空格说明只有一个单词,返回总长度。
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    // 注意:用getline读带空格的输入
    while (getline(cin, s)) {
        size_t pos = s.rfind(' '); // 找最后一个空格
        // 处理末尾有空格的情况(比如"abc   ")
        while (pos == s.size() - 1) {
            s.erase(pos); // 删除末尾空格
            pos = s.rfind(' ');
        }
        if (pos == string::npos) { // 没有空格
            cout << s.size() << endl;
        } else { // 有空格
            cout << s.size() - pos - 1 << endl;
        }
    }
    return 0;
}

题 4:验证回文字符串(双指针 + 忽略非字母数字)
题目:忽略非字母数字、不区分大小写,判断是否是回文(比如输入 “A man, a plan, a canal: Panama”,返回 true;输入 “race a car”,返回 false)。
思路:

  • 双指针分别从开头和结尾出发;
  • 跳过非字母数字的字符;
  • 把字母转成小写(或大写),比较是否相等;
  • 只要有一对不相等,就不是回文。
#include <iostream>
#include <string>
#include <cctype>
using namespace std;

class Solution {
public:
    bool isPalindrome(string s) {
        int left = 0;
        int right = s.size() - 1;
        while (left < right) {
            // 跳过非字母数字
            while (left < right && !isalnum(s[left])) {
                left++;
            }
            while (left < right && !isalnum(s[right])) {
                right--;
            }
            // 转小写比较
            if (tolower(s[left]) != tolower(s[right])) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
};

int main() {
    Solution sol;
    string s1 = "A man, a plan, a canal: Panama";
    string s2 = "race a car";
    cout << "s1是否回文:" << boolalpha << sol.isPalindrome(s1) << endl; // true
    cout << "s2是否回文:" << boolalpha << sol.isPalindrome(s2) << endl; // false
    return 0;
}

题 5:字符串相加(模拟加法,必练)
题目:给定两个字符串形式的非负整数,返回它们的和(不能转成 int/long,因为数字太长)。
思路:

  • 双指针从两个字符串末尾开始;
  • 逐位相加,记录进位;
  • 结果先尾插(避免头插效率低),最后反转。
#include <iostream>
#include <string>
#include <algorithm> // reverse函数头文件
using namespace std;

class Solution {
public:
    string addStrings(string num1, string num2) {
        int i = num1.size() - 1; // num1末尾指针
        int j = num2.size() - 1; // num2末尾指针
        int carry = 0; // 进位
        string res; // 结果
        
        while (i >= 0 || j >= 0 || carry > 0) {
            // 取当前位的数字,没有就取0
            int n1 = (i >= 0) ? (num1[i] - '0') : 0;
            int n2 = (j >= 0) ? (num2[j] - '0') : 0;
            // 计算当前位和
            int sum = n1 + n2 + carry;
            carry = sum / 10; // 更新进位
            res.push_back((sum % 10) + '0'); // 存当前位(转字符)
            // 指针左移
            i--;
            j--;
        }
        // 反转结果(因为是尾插,顺序反了)
        reverse(res.begin(), res.end());
        return res;
    }
};

int main() {
    Solution sol;
    string num1 = "123456789";
    string num2 = "987654321";
    cout << num1 << " + " << num2 << " = " << sol.addStrings(num1, num2) << endl; // 1111111110
    return 0;
}

5. 模拟实现 string:搞定面试高频题(易懂)
新手一开始不用写完整的 string 类,但要搞懂深拷贝(面试必考),先从核心函数入手。

5.1 新手先懂:为啥要深拷贝?
如果用编译器默认的 “浅拷贝”,多个 string 对象会共享同一块空间:

  • 比如string s1(“hello”); string s2 = s1;,s1 和 s2 的指针都指向同一块内存;
  • 销毁 s1 时释放了内存,s2 的指针就成了 “野指针”,程序崩了!
    深拷贝就是:每个对象都有自己的内存空间,互不影响。

5.2 新手能写的:深拷贝(传统版)
先写核心的构造、拷贝构造、赋值重载、析构,能看懂的写法:

#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;

class MyString {
public:
    // 构造函数:创建字符串
    MyString(const char* str = "") {
        assert(str != nullptr); // 防止传空指针
        // 申请空间:长度+1(存'\0')
        _str = new char[strlen(str) + 1];
        strcpy(_str, str); // 拷贝内容
    }

    // 拷贝构造:深拷贝
    MyString(const MyString& s) {
        // 重新申请空间
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str); // 拷贝内容
    }

    // 赋值运算符重载:深拷贝
    MyString& operator=(const MyString& s) {
        // 防止自赋值(s = s)
        if (this != &s) {
            // 先释放旧空间
            delete[] _str;
            // 再申请新空间
            _str = new char[strlen(s._str) + 1];
            strcpy(_str, s._str);
        }
        return *this; // 支持连续赋值
    }

    // 析构函数:释放空间
    ~MyString() {
        if (_str) { // 防止重复释放
            delete[] _str;
            _str = nullptr; // 置空,避免野指针
        }
    }

    // 辅助函数:打印字符串(新手调试用)
    void print() {
        cout << _str << endl;
    }

private:
    char* _str; // 存储字符串的指针
};

// 测试:深拷贝是否生效
int main() {
    MyString s1("hello");
    MyString s2 = s1; // 拷贝构造
    MyString s3("world");
    s3 = s1; // 赋值重载

    s1.print(); // hello
    s2.print(); // hello
    s3.print(); // hello
    return 0;
}

5.3 进阶一点:深拷贝(现代版,了解)
用 “交换” 简化代码,更优雅:

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

    // 拷贝构造:用临时对象交换
    MyString(const MyString& s) : _str(nullptr) {
        MyString tmp(s._str); // 创建临时对象
        swap(_str, tmp._str); // 交换指针
    }

    // 赋值重载:传值参数(自动拷贝)
    MyString& operator=(MyString s) {
        swap(_str, s._str); // 交换后,临时对象销毁旧空间
        return *this;
    }

    ~MyString() {
        if (_str) {
            delete[] _str;
            _str = nullptr;
        }
    }

    void print() {
        cout << _str << endl;
    }

private:
    char* _str;
};

✅手备注:现代版不用手动申请 / 释放空间,靠临时对象的析构自动处理,新手先理解传统版,再看现代版。

6. 小白避坑 & 复习指南

6.1 常见坑(避坑清单)

  • ❌ 忘记加头文件:编译报错 “未定义的标识符 string”;
  • ❌ 用cin读带空格的字符串:只读到空格前,改用getline;
  • ❌ 忽略find的返回值:没找到时返回npos(-1),一定要判断;
  • ❌ 混淆resize和reserve:resize改长度,reserve改容量;
  • ❌ 浅拷贝导致崩溃:模拟实现时一定要写深拷贝。

6.2 复习路线
1.第一遍:记核心接口(+=、size、find、getline),能写简单的遍历和拼接;
2.第二遍:做 5 道实战题,掌握双指针、计数、模拟加法等思路;
3.第三遍:理解深拷贝的原理,能手写传统版深拷贝代码;
4.第四遍:回顾避坑点,调试自己写的代码,解决常见错误。

6.3 提问:常见疑问解答
Q:string 能存中文吗?
A:能!但要注意编码(比如 UTF-8),size()返回的是字节数(一个中文占 3 字节),不是字符数。
Q:为什么capacity()的初始值不一样?
A:不同编译器的扩容策略不同,新手不用纠结,知道reserve能预留空间就行。
Q:模拟实现 string 需要写所有函数吗?
A:不用!新手先写构造、拷贝构造、赋值重载、析构这 4 个核心函数,面试足够了

总结
核心用法:新手先掌握string的构造、+=拼接、find查找、getline输入、双指针遍历,能覆盖 80% 的刷题场景;
核心原理:深拷贝是string的灵魂,解决浅拷贝导致的内存问题,面试必考;
新手技巧:用 “杯子装水” 比喻理解容量操作,用刷题巩固接口使用,避开cin读空格、忘记判npos等坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值