第4章 数组与字符串:从内存布局到实战避坑

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

4.1 一维数组:最朴素的数据集合

一维数组,你可以想象成酒店里的一排房间。每个房间有个门牌号(下标),里面住着客人(数据)。

// 声明一个能装5个整数的数组
int scores[5];

// 声明并初始化
int scores[5] = {88, 92, 76, 85, 90};

// 也可以省略大小,编译器自动算
int scores[] = {88, 92, 76, 85, 90};

我个人习惯在声明数组时,永远显式指定大小。为什么?因为代码可读性更好,别人一看就知道你要存几个数据。

小技巧:const int 定义数组大小,而不是直接写数字。这样改起来方便,也不容易出错。
const int STUDENT_COUNT = 5;
int scores[STUDENT_COUNT] = {88, 92, 76, 85, 90};

4.2 二维数组:表格思维

二维数组,其实就是「数组的数组」。我习惯把它想象成Excel表格——行和列。

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

// 访问第2行第3列的元素
int val = matrix[1][2];  // 结果是7

嗯,这里要注意:C++里数组下标从0开始。我见过太多新手在这上面栽跟头了。matrix[1][2] 实际上是第2行第3列,不是第1行第2列。

我曾经踩过的坑: 用二维数组做图像处理时,把行和列搞反了。结果图像显示出来是旋转了90度的。从那以后,我每次用二维数组都会在注释里标明「行(row)」和「列(col)」。

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

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

// 字符数组
char name[10] = {'J', 'o', 'h', 'n', '\0'};

// 字符串字面量(自动加'\0')
char name[10] = "John";

// 注意:数组大小要足够容纳'\0'
char name[5] = "John";  // 没问题,刚好5个字符(包括'\0')
char name[4] = "John";  // 危险!没有空间放'\0'

为什么会这样?因为C风格字符串的结尾标志就是 '\0'。如果你忘了它,字符串函数(比如 strlenstrcpy)就会一直往后读,直到碰见内存里的某个 '\0' 为止——这通常会导致程序崩溃。

核心要点: C风格字符串的大小 = 字符数 + 1(给'\0'留位置)。

4.4 string类:C++的救星

说实话,我刚开始用C++时,最烦的就是C风格字符串。每次都要手动管理内存、检查边界、记得加'\0'……直到我发现了 std::string

#include <string>
#include <iostream>

std::string name = "John";
std::string greeting = "Hello, " + name + "!";

std::cout << greeting << std::endl;  // 输出:Hello, John!
std::cout << "长度:" << name.length() << std::endl;  // 输出:4

你想想看,用 string 类,你不用再关心内存分配、不用再担心缓冲区溢出、不用再手动复制字符串。它自己会管理好一切。

操作 C风格字符串 std::string
复制 strcpy(dest, src) str2 = str1;
拼接 strcat(dest, src) str3 = str1 + str2;
获取长度 strlen(str) str.length()
比较 strcmp(s1, s2) s1 == s2
我的建议: 在C++项目里,优先使用 std::string。只有在调用C语言库函数、或者需要极致性能优化时,才考虑C风格字符串。

4.5 数组与指针的关系

数组名,其实就是一个指针常量——它指向数组的第一个元素。我当年学到这里时,脑子转了好几个弯才想明白。

int arr[5] = {10, 20, 30, 40, 50};

// arr 等价于 &arr[0]
int* ptr = arr;  // 数组名可以直接赋值给指针

// 通过指针访问数组元素
std::cout << *(ptr + 2) << std::endl;  // 输出30,等价于 arr[2]

// 指针也可以像数组一样用下标
std::cout << ptr[3] << std::endl;  // 输出40

说白了,arr[i] 在编译器眼里就是 *(arr + i)。这就是为什么数组下标从0开始——因为 arr + 0 就是第一个元素的地址。

注意区别: 数组名是常量,不能修改;指针是变量,可以重新指向别处。
int arr[5];
int* ptr = arr;

ptr = ptr + 1;  // 合法,指针可以移动
// arr = arr + 1;  // 编译错误!数组名不能修改

4.6 知识体系总览

下面这张图,是我梳理的本章知识结构。你把它存下来,学完这章后再回头看,会更有感觉。

第4章:数组与字符串知识体系 数组与字符串 数组 一维数组 二维数组 字符串 C风格字符串 string类 数组与指针的关系 数组名=指针常量 指针运算

4.7 实战经验总结

我在项目里用过不少数组和字符串,有几个经验想分享给你:

  • 能用 std::vector 就别用原生数组——除非你明确知道数组大小且不会变。vector 更安全、更灵活。
  • 字符串拼接用 string+ 运算符——别用 strcat,那玩意儿容易缓冲区溢出。
  • 二维数组传参时,第二维大小必须指定——这是C++的语法规定,没办法。
  • 遍历数组用范围for循环——C++11开始支持,代码更简洁、更安全。
// 范围for循环示例
int scores[] = {88, 92, 76, 85, 90};
for (int score : scores) {
    std::cout << score << " ";
}
// 输出:88 92 76 85 90

嗯,数组和字符串这块,说白了就是C++的基础设施。你把它搞透了,后面学指针、学容器、学算法,都会轻松很多。我当年就是在这块花了足够多的时间,后面才越学越顺。


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