字符串处理:C风格字符串、string类、常用成员函数

字符串处理,说白了就是编程里的基本功。我做了这么多年C++,几乎每个项目都离不开它。无论是解析配置文件、处理用户输入,还是拼接网络协议包,字符串操作都是家常便饭。

C++里有两套字符串体系:一套是老祖宗C语言留下的C风格字符串,另一套是C++标准库提供的string类。我个人习惯,能用string类就尽量用,但C风格字符串的知识你也得懂——毕竟很多底层接口、遗留代码还在用。

C风格字符串:老派但必须懂

C风格字符串本质上就是个字符数组,以空字符'\0'结尾。你想想看,没有这个结束符,程序根本不知道字符串到哪结束。

// C风格字符串的几种定义方式
char str1[] = "Hello";          // 自动添加'\0',实际占6字节
char str2[6] = "Hello";         // 显式指定大小,必须留一个给'\0'
char* str3 = "Hello";           // 字符串常量,不可修改

// 常见操作
#include <cstring>

char src[] = "World";
char dst[20];
strcpy(dst, src);               // 复制字符串
strcat(dst, "!");               // 拼接字符串
int len = strlen(dst);          // 获取长度(不含'\0')
int cmp = strcmp("abc", "abd"); // 比较,返回负值/0/正值

⚠️ 避坑指南

我曾经在一个网络模块里用strcpy复制用户输入的数据,结果对方发了个超长字符串,直接缓冲区溢出,程序崩溃了整整一晚上。后来我养成了习惯:能用strncpy就别用strcpy,能指定最大长度就别偷懒。

C风格字符串最大的问题就是不安全。你手动管理内存,手动保证缓冲区够大,稍不留神就出bug。而且操作起来也麻烦——复制、拼接、比较都要调函数,不像面向对象那样直观。

string类:现代C++的标配

std::string是C++标准库提供的字符串类,封装了动态内存管理。说白了,你只管用,它自己会扩容、会释放,省心多了。

#include <string>

std::string s1 = "Hello";
std::string s2("World");
std::string s3 = s1 + " " + s2;  // 拼接,s3 = "Hello World"

// 直接比较,不用调函数
if (s1 == "Hello") {
    // 相等
}

// 获取长度
size_t len = s3.length();  // 或者 s3.size()

我在项目中遇到过好几次这样的场景:用C风格字符串时,拼接三个字符串得先算总长度、分配缓冲区、再一个个strcat。换成string类,一行加号搞定。代码干净,人也舒服。

常用成员函数:你一定会用到的

string类的成员函数很多,但常用的就那么十几个。我按功能分了几类,你重点掌握这些就够了。

分类 函数 说明
容量相关 size() / length() 返回字符个数
empty() 判断是否为空字符串
capacity() 返回当前分配的内存容量
访问与修改 at(pos) / operator[] 访问指定位置的字符
front() / back() 访问首/尾字符
push_back(c) / pop_back() 在末尾添加/删除字符
查找与子串 find(str, pos) 从pos开始查找子串,返回位置
rfind(str) 从末尾向前查找
substr(pos, len) 提取子串
插入与删除 insert(pos, str) 在pos位置插入字符串
erase(pos, len) 删除从pos开始的len个字符
转换 c_str() 返回const char*,用于C风格接口
data() 同c_str(),C++11起返回字符数组

💡 重点提示

at()和operator[]的区别:at()会做边界检查,越界时抛出out_of_range异常;operator[]不检查,越界是未定义行为。调试阶段我建议用at(),上线后如果性能敏感可以换成operator[]。

实战:一个完整的字符串处理示例

光说不练假把式。我写个小例子,把上面这些函数串起来用一遍。

#include <iostream>
#include <string>

int main() {
    std::string text = "C++ programming is fun!";
    
    // 1. 获取长度
    std::cout << "长度: " << text.length() << std::endl;
    
    // 2. 查找子串
    size_t pos = text.find("programming");
    if (pos != std::string::npos) {
        std::cout << "'programming' 的位置: " << pos << std::endl;
    }
    
    // 3. 提取子串
    std::string sub = text.substr(4, 11);  // "programming"
    std::cout << "子串: " << sub << std::endl;
    
    // 4. 插入
    text.insert(0, "Hello, ");
    std::cout << "插入后: " << text << std::endl;
    
    // 5. 替换
    size_t start = text.find("fun");
    text.replace(start, 3, "awesome");
    std::cout << "替换后: " << text << std::endl;
    
    // 6. 转换为C风格字符串,传给旧接口
    const char* cstr = text.c_str();
    std::cout << "C风格: " << cstr << std::endl;
    
    return 0;
}

📌 小技巧

find()找不到子串时返回std::string::npos,这个值通常是-1(转换成size_t后是最大值)。所以判断时一定要用 != npos,别写成 >= 0,因为size_t是无符号的。

string vs C风格字符串:怎么选?

我个人的经验法则很简单:

  • 新代码:无脑用std::string。安全、方便、可读性好。
  • 调用C库函数:用c_str()转一下。比如fopen、printf这些。
  • 性能极致场景:比如高频日志、网络包序列化,可以考虑C风格字符串或自定义缓冲区。但先profile,别瞎优化。
  • 嵌入式/内存受限:C风格字符串更可控,没有动态分配的开销。

你想想看,大部分业务代码里,string类的性能完全够用。我见过有人为了省那几微秒,手写一堆C风格字符串操作,结果bug不断,维护成本翻倍。得不偿失。

知识体系总览

下面这张图把本章的核心知识点串起来了,方便你整体把握。

字符串处理知识体系 C风格字符串 std::string类 字符数组 + '\0' 结束符 strcpy / strcat / strlen / strcmp ⚠️ 缓冲区溢出风险 动态内存管理,自动扩容 + 拼接 / == 比较 / find / substr c_str() 兼容C接口 新代码用string,底层接口用c_str()

嗯,到这里字符串处理的核心内容就差不多了。C风格字符串是基础,你得理解它的内存模型和风险。string类是日常开发的主力,常用成员函数要烂熟于心。两者之间的转换(c_str())也是高频操作。

我个人建议你写代码时多练练string类的各种操作,尤其是find和substr的组合使用,解析文本时特别管用。遇到性能瓶颈再回头优化,别一开始就想着手写C风格字符串——那是给自己挖坑。

公众号:蓝海资料掘金营,微信deep3321