第五章 数组与字符串:从内存布局到实战技巧
说实话,数组和字符串这块内容,看起来简单,但坑特别多。我见过不少工作两三年的同事,还在C风格字符串上翻车。今天咱们就把这块彻底捋清楚。
5.1 一维数组:最基础的数据容器
数组说白了就是一块连续的内存空间。你声明一个 int arr[5],编译器就给你分配了 5 个 int 大小的空间,挨个排好。
int scores[5] = {88, 92, 76, 85, 90};
// 内存布局:scores[0] 在低地址,scores[4] 在高地址
for (int i = 0; i < 5; ++i) {
std::cout << scores[i] << " ";
}
小技巧:我个人习惯用 constexpr 定义数组大小,而不是硬编码数字。这样改起来方便,也不容易越界。
注意:C++ 不会检查数组下标是否越界!我曾经在项目里见过一个 bug,就是因为数组越界写坏了相邻变量的值,排查了两天才找到。所以,自己心里要有根弦。
5.2 二维数组:表格数据的自然表达
二维数组其实就是「数组的数组」。比如 int matrix[3][4],你可以理解为 3 行 4 列的表格。它在内存里还是线性存储的——按行优先。
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 遍历所有元素
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 4; ++col) {
std::cout << matrix[row][col] << "\t";
}
std::cout << "\n";
}
你想想看,如果我们要把二维数组传给函数,写法就有点讲究了。除了第一维,其他维度的大小必须指定:
void printMatrix(int arr[][4], int rows) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << arr[i][j] << " ";
}
std::cout << "\n";
}
}
核心理解:二维数组名其实是一个指向「包含4个int的数组」的指针。类型是 int (*)[4],不是 int**。这两个容易搞混,我刚开始学的时候也踩过这个坑。
5.3 字符数组与C风格字符串
C 风格字符串就是以 '\0' 结尾的字符数组。这个 '\0' 是终结符,告诉函数「字符串到这儿结束」。
char str1[] = "Hello"; // 自动加 '\0',实际长度 6
char str2[] = {'H','e','l','l','o','\0'}; // 手动加 '\0'
char str3[10] = "Hello"; // 剩余位置自动填 '\0'
// 常见操作
#include <cstring>
std::cout << strlen(str1); // 输出 5,不包含 '\0'
std::cout << sizeof(str1); // 输出 6,包含 '\0'
我曾经踩过的坑:用 strcpy 时目标数组不够大,导致缓冲区溢出。后来我改用 strncpy 或者直接用 std::string,再也没出过这种问题。
5.4 std::string:现代C++的字符串方案
说真的,除非你在写嵌入式或者跟老代码打交道,否则我建议你直接用 std::string。它帮你管理内存,自动处理 '\0',还提供了丰富的操作接口。
#include <string>
std::string s1 = "Hello";
std::string s2 = " World";
std::string s3 = s1 + s2; // 拼接,输出 "Hello World"
// 常用方法
s3.size(); // 长度
s3.empty(); // 是否为空
s3.substr(0, 5); // 取子串 "Hello"
s3.find("World"); // 查找,返回下标
// 和C风格字符串互转
const char* cstr = s3.c_str();
个人建议:在函数参数传递时,如果只是读取字符串内容,用 const std::string&;如果需要修改,用 std::string&;尽量不要按值传递,避免不必要的拷贝。
5.5 数组与指针的关系:剪不断理还乱
数组名在很多场合会隐式转换为指针。比如 int arr[5],arr 的类型其实是 int*,指向数组第一个元素。但 sizeof(arr) 返回的是整个数组的大小,不是指针的大小——这是它俩最大的区别。
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // 等价于 int* ptr = &arr[0];
// 指针运算
std::cout << *(ptr + 2); // 输出 30,等价于 arr[2]
std::cout << ptr[2]; // 也输出 30,指针也可以用下标
// 区别:sizeof
std::cout << sizeof(arr); // 输出 20(5 * 4)
std::cout << sizeof(ptr); // 输出 8(64位系统指针大小)
一句话总结:数组名不是指针,但大多数时候表现得像指针。记住 sizeof 和取地址 &arr 时的行为差异,就能避开大部分坑。
知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着梳理一下自己的理解:
本章小结
数组和字符串是 C++ 编程的基石。我个人觉得,理解它们的关键在于两点:一是内存布局(连续、线性),二是数组名和指针的微妙关系。把这些搞明白了,后面学 STL 容器会轻松很多。
嗯,关于字符串,再多说一句:能用 std::string 就别用 C 风格字符串。除非你明确知道自己在做什么,并且需要极致性能控制。这是我在实际项目中用血泪换来的经验。