5、数组与字符串:一维数组、二维数组、字符数组、C风格字符串、string类、常用字符串操作

数组和字符串,说白了就是C++里最基础的数据容器。我刚开始学的时候,觉得这东西太简单了,不就是一堆数据排排坐嘛。结果真到项目里,踩过的坑一个接一个。今天咱们就把这块彻底捋清楚。

5.1 一维数组:最朴素的连续内存

一维数组,就是同一类型元素的线性集合。它在内存里是连续存放的,这一点非常关键。

int scores[5] = {88, 92, 76, 85, 90};
// 内存布局:scores[0] scores[1] ... scores[4] 紧挨着

声明与初始化

  • 类型固定:所有元素类型必须一致
  • 大小固定:编译时确定,运行时不能变
  • 下标从0开始:这是C/C++的传统,也是新手最容易犯迷糊的地方

核心要点:数组名就是首元素的地址。scores 等价于 &scores[0]。这个特性在函数传参时特别重要。

我在项目中遇到过一个问题:用 sizeof 求数组长度,结果传参后就不灵了。为什么?因为数组作为参数传递时,会退化为指针,sizeof 只能拿到指针的大小,而不是整个数组的大小。

void printArray(int arr[]) {
    // arr 已经退化为指针了
    int len = sizeof(arr) / sizeof(arr[0]); // 错误!拿不到正确长度
}

int main() {
    int arr[10];
    int len = sizeof(arr) / sizeof(arr[0]); // 正确:10
    printArray(arr);
}

避坑指南:我曾经在项目里用 sizeof 算数组长度,结果传参后长度变成了 2(64位系统下指针8字节,int4字节)。排查了半天才发现是数组退化的问题。后来我养成了习惯:要么传长度参数,要么用 std::array 或 std::vector。

5.2 二维数组:表格数据的天然选择

二维数组,你可以把它想象成一个表格——行和列。但内存里它仍然是连续存储的,按行优先排列。

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
// 访问:matrix[行][列]

内存布局:matrix[0][0], matrix[0][1], ..., matrix[0][3], matrix[1][0], ... 全部连续。

嗯,这里要注意:二维数组作为函数参数时,除了第一维可以省略,其他维度必须指定。否则编译器不知道每行有多少列,没法计算偏移量。

void printMatrix(int arr[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }
}

小技巧:如果你需要动态大小的二维数组,别用 int arr[n][m](这是C99的VLA,C++不支持)。用 vector<vector<int>> 或者自己手动分配一维数组模拟二维。

5.3 字符数组与C风格字符串

C风格字符串,说白了就是以 '\0'(空字符)结尾的字符数组。这是C语言留下的遗产,C++里依然广泛使用。

char str1[] = "Hello";      // 自动添加 '\0',实际长度6
char str2[] = {'H','e','l','l','o','\0'}; // 手动加 '\0'
char str3[10] = "Hello";    // 预留空间,后面自动补 '\0'

为什么需要 '\0'? 因为C风格字符串没有长度信息,必须靠这个结束标记来知道字符串到哪里结束。你想想看,如果没有 '\0',像 strlen、strcpy 这些函数怎么知道字符串有多长?

避坑指南:我曾经在项目里犯过一个低级错误——声明字符数组时忘了给 '\0' 留位置。比如 char name[5] = "Alice"; 看起来没问题,但 "Alice" 实际需要6个字符(5个字母+1个'\0')。结果 strcpy 的时候直接越界,把相邻内存的数据给覆盖了。排查了整整一下午。

常用C风格字符串函数(需要 #include <cstring>)

函数作用注意事项
strlen(s)返回字符串长度(不含'\0')O(n)复杂度,不是常数时间
strcpy(dest, src)复制字符串确保 dest 空间足够
strcat(dest, src)拼接字符串确保 dest 空间足够
strcmp(s1, s2)比较字符串返回0表示相等
strchr(s, c)查找字符返回指针或NULL

5.4 string类:C++的现代方案

说实话,C风格字符串用起来太痛苦了。手动管理内存、担心越界、拼接还要小心翼翼。C++的 string 类把这些麻烦全解决了。

#include <string>
using namespace std;

string s1 = "Hello";
string s2("World");
string s3 = s1 + " " + s2;  // 直接拼接,不用操心内存

cout << s3 << endl;        // "Hello World"
cout << s3.length() << endl; // 11

string 的优势

  • 自动管理内存:不用手动分配释放
  • 动态增长:可以随时追加字符
  • 运算符重载:+、+=、==、< 等直接用
  • 安全:不会出现缓冲区溢出

个人习惯:我现在的项目里,除非是跟C库接口对接,否则一律用 std::string。C风格字符串只作为底层交互的桥梁,业务逻辑里完全用 string。

5.5 常用字符串操作详解

不管是C风格还是 string,有些操作是绕不开的。咱们挑几个最常用的说说。

1. 查找子串

string text = "Hello, welcome to C++ world!";
size_t pos = text.find("welcome");
if (pos != string::npos) {
    cout << "找到了,位置:" << pos << endl;
}

// C风格:strstr
const char* result = strstr(text.c_str(), "welcome");

2. 截取子串

string sub = text.substr(7, 7);  // 从位置7开始,取7个字符
cout << sub << endl;            // "welcome"

3. 插入与删除

string s = "I C++";
s.insert(2, "love ");  // "I love C++"
s.erase(2, 5);         // 又变回 "I C++"

4. 替换

string s = "I like Java";
s.replace(7, 4, "C++"); // "I like C++"

5. 转换大小写

string s = "Hello World";
for (char &c : s) {
    c = toupper(c);  // 或者 tolower
}
// s 变成 "HELLO WORLD"

小技巧:string 的 c_str() 方法可以返回C风格字符串,方便跟C库函数交互。但要注意,c_str() 返回的指针在 string 对象修改后可能失效。

5.6 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到,数组是基础,字符数组是数组的特例,C风格字符串是字符数组的约定用法,而 string 类是对C风格字符串的封装和升级。

数组与字符串 一维数组 连续内存,固定大小 下标从0开始 数组名 = 首地址 二维数组 表格结构,行优先存储 传参需指定列数 字符数组 char 类型的数组 以 '\0' 结尾 C风格字符串 strlen/strcpy/strcat 手动管理内存 容易越界 string 类 自动管理内存 运算符重载 find/substr/replace 核心:数组是基础,string 是现代C++的首选

说白了,数组是C++的底层基石,理解它你才能理解指针和内存管理。而 string 类则是C++对字符串操作的现代化封装,让你写代码更高效、更安全。我个人建议:新项目里优先用 string,但一定要理解底层数组的原理——因为很多面试题和底层调试,最终还是绕不开数组和指针。

本章总结:一维数组是线性存储,二维数组是表格存储,字符数组是字符串的底层实现,C风格字符串靠 '\0' 结尾,string 类提供了安全便捷的操作接口。掌握这些,你就掌握了C++数据存储的半壁江山。


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