信息学竞赛:字符数组 vs string 类 完全指南

在信息学竞赛(CSP-J/S, NOIP, NOI)中,字符串处理是必考内容。C++ 提供了两种主要的字符串处理方式:C 风格字符数组C++ std::string

一、字符数组 (C-style String)

字符数组是 C 语言遗留下来的处理方式,本质是 char 类型的数组,以空字符 \0 (ASCII 码为 0) 作为结束标志。

1. 定义与初始化

char s1[105];             // 定义长度为 105 的字符数组
char s2[] = "Hello";      // 自动推断长度 (实际为 6,包含 '\0')
char s3[105] = "Hello";   // 初始化

2. 输入与输出

  • 不含空格
    cin >> s1;            // 遇到空格、换行、Tab 停止
    cout << s1 << endl;
    scanf("%s", s1);      // 注意:不需要取地址符 &
    printf("%s", s1);
    
  • 含空格(读取整行)
    cin.getline(s1, 105); // 读取一行,最多读 104 个字符,自动补 '\0'
    // 或者 C 风格
    fgets(s1, 105, stdin); // 读取一行,会保留末尾的 '\n'
    
    注意:gets(s1) 因不安全已在 C++11 中被废弃,竞赛中严禁使用。

3. 常用函数 (<cstring> 头文件)

函数 功能 示例 注意事项
strlen(s) 求长度 int len = strlen(s1); 不包含 \0
strcpy(s1, s2) 复制 strcpy(s1, s2); 将 s2 复制到 s1,s1 空间需足够
strcat(s1, s2) 连接 strcat(s1, s2); 将 s2 拼接到 s1 后,s1 空间需足够
strcmp(s1, s2) 比较 if(strcmp(s1, s2)==0) 相等返回 0,s1>s2 返回正数
sprintf 格式化写入 sprintf(s1, "%d", 123); 将数字转为字符串存入 s1
sscanf 格式化读取 sscanf(s1, "%d", &n); 从字符串读取数字

二、string 类 (C++ String)

std::string 是 C++ 标准库提供的字符串类,封装了动态内存管理,使用更安全、方便。需包含 <string> 头文件。

1. 定义与初始化

string s1;                // 空字符串
string s2 = "Hello";      // 直接赋值
string s3(5, 'a');        // "aaaaa"
string s4 = s2;           // 拷贝构造

2. 输入与输出

  • 不含空格
    cin >> s1;
    cout << s1 << endl;
    
  • 含空格(读取整行)
    getline(cin, s1);     // 读取整行,遇到换行符停止,不保留换行符
    
    注意:若前面有 cin >> n,需先用 getchar() 吃掉换行符,再 getline

3. 常用成员函数

函数 功能 示例 注意事项
s.length() / s.size() 求长度 int len = s1.length(); 返回 size_t (无符号整型)
s1 + s2 连接 string s3 = s1 + s2; 支持 string + char / string + string
s1 == s2 比较 if(s1 == s2) 支持 >, <, >=, <=
s.substr(pos, len) 截取子串 s1.substr(1, 3); 从下标 1 开始,长度 3
s.find(str) 查找 s1.find("abc"); 找不到返回 string::npos
s.insert(pos, str) 插入 s1.insert(1, "X"); 在下标 1 处插入
s.erase(pos, len) 删除 s1.erase(1, 2); 从下标 1 开始删 2 个
s.replace(...) 替换 s1.replace(1, 2, "X"); 从下标 1 开始,长度 2 替换为 "X"
stoi(s) / to_string(n) 数值互转 int n = stoi(s1); C++11 特性,需 <string>

三、核心对比总结

特性 字符数组 (char[]) string 类
所属 C 语言风格 C++ 标准库类
内存管理 静态/手动,需预判最大长度 动态自动管理
结束标志 必须有 \0 内部维护长度,无需 \0
赋值 strcpy (不能直接用 =) 可以直接 =
连接 strcat (需确保空间) ++=
比较 strcmp (不能直接用 >) 可直接用 ==, >
效率 略高 (无动态分配开销) 略低 (但现代编译器优化后差距很小)
安全性 低 (易溢出、忘加 \0) 高 (自动扩容、边界检查)
适用场景 极端性能要求、卡内存、C 接口交互 绝大多数竞赛题目

四、信奥赛实战技巧与避坑

1. 什么时候用哪个?

  • 首选 string:95% 的题目推荐使用 string,因为写起来快,不易出错(如赋值、比较、拼接)。
  • 使用字符数组
    • 题目内存限制极其严格(如 2MB)。
    • 需要与 C 风格 API 交互(如 fopen, scanf 大量读入)。
    • 涉及极其频繁的字符修改且 string 超时(stringinsert/eraseO(n)O(n) 的)。

2. 常见坑点

  • getline 的换行符残留
    int n;
    string s;
    cin >> n;
    // getchar(); // 必须吃掉 cin 留下的换行符,否则 getline 会读入空串
    getline(cin, s); 
    
  • string 的下标越界s[i] 不会自动检查越界,访问 s[s.length()] 是未定义行为(虽然通常是 \0),访问更大下标会 RTE。
  • find 的返回值string::npos 是一个很大的无符号数(通常是 -1 的补码)。
    if (s.find("a") != string::npos) { ... } // 正确
    if (s.find("a") != -1) { ... }           // 警告:有符号与无符号比较
    
  • 字符数组的空间: 定义 char s[100] 最多只能存 99 个可见字符,第 100 位必须是 \0。若题目说字符串长度 100,数组要开 105。

3. 高效互转技巧

  • stringchar[]
    string s = "hello";
    const char* c = s.c_str(); // 获取 C 风格字符串指针
    
  • char[]string
    char c[] = "hello";
    string s = c; // 直接赋值
    string s2(c); // 构造函数
    
  • 数字转字符串 (C++11)
    int n = 123;
    string s = to_string(n); // "123"
    
  • 字符串转数字 (C++11)
    string s = "123";
    int n = stoi(s); 
    long long ln = stoll(s);
    

4. 性能优化建议

如果题目数据量达到 10510610^5 \sim 10^6 级别,且涉及大量字符串拼接:

  • 避免s = s + "a" (每次可能重新分配内存)
  • 推荐s += "a" (通常有预留容量,效率更高)
  • 推荐stringreserve() 预分配空间。
    string s;
    s.reserve(100000); // 预分配 10 万字符空间,避免多次扩容
    

总结:在 CSP-J/S 及 NOIP 比赛中,无脑用 string 通常是最佳策略,除非你明确遇到了 TLE 或 MLE 且确定是字符串操作引起的。