C 风格数组
到目前为止,我们处理数据的方式都是"一个变量存一个值"。但现实世界的数据很少是孤立存在的——一组传感器读数、一串字符、一个矩阵、一张成绩表,这些东西天然就是"一堆相同类型的数据排成一排"。数组(array)就是 C 和 C++ 提供的最原始的、用来存储这种"同类型连续数据"的机制。
C 风格数组的问题不少——不能赋值、不能返回、传参丢长度信息、没有边界检查——但它是理解内存布局的绝佳入口。只有搞明白了这些痛点,才能理解为什么 C++ 要引入 std::array。这一章我们把 C 风格数组从里到外拆一遍。
声明和初始化——数组长什么样
声明一个数组,核心语法就是在变量名后面加方括号,里面写上元素个数:
int scores[5]; // 5 个 int,未初始化(值是不确定的)这段代码告诉编译器:在栈上连续分配 5 个 int 的空间。注意,未初始化的局部数组里装的是垃圾值——不是零。所以我们几乎总是在声明的同时进行初始化。
int scores[5] = {90, 85, 78, 92, 88};这五个值按顺序填入数组的五个位置。如果初始值少于数组大小,剩下的元素会被自动初始化为零:
int data[5] = {10, 20}; // data = {10, 20, 0, 0, 0}反过来,初始值多于数组大小则编译直接报错。
如果初始化列表给了足够多的值,数组的大小可以省略,让编译器自己数:
int primes[] = {2, 3, 5, 7, 11, 13}; // 编译器推断大小为 6这种写法的好处是以后增减元素时不需要同步修改方括号里的数字。
知道数组有多少个元素,有一个经典公式:
int primes[] = {2, 3, 5, 7, 11, 13};
constexpr int kCount = sizeof(primes) / sizeof(primes[0]); // kCount = 6sizeof(primes) 是整个数组占的字节数,sizeof(primes[0]) 是单个元素占的字节数,相除得到元素个数。这个技巧在 C 代码里随处可见,但后面我们会讲到它的局限性。
访问元素——从零开始的世界
C++ 数组的下标从 0 开始。一个大小为 5 的数组,有效下标是 0 到 4。这不是随意的设计选择——arr[i] 在底层等价于 *(arr + i),即从数组起始地址向后偏移 i 个元素的位置。
int scores[5] = {90, 85, 78, 92, 88};
std::cout << scores[0] << std::endl; // 90(第一个元素)
std::cout << scores[4] << std::endl; // 88(最后一个元素)踩坑预警:C 风格数组不进行任何边界检查。
scores[5]、scores[100]、scores[-1]这些越界访问编译时不报错,运行时也不抛异常——它们会默默地读写数组之外的内存。这种未定义行为可能恰好"看上去正常",也可能立刻崩溃,还可能悄无声息地修改其他变量的值。调试这种问题的时候血压真的会拉满。
修改数组元素也是通过下标:
scores[2] = 80; // 把第三个元素从 78 改成 80遍历数组有几种方式。最传统的是下标循环,C++11 引入的范围 for 更简洁:
// 范围 for 遍历(只在声明作用域内有效)
for (int s : scores) {
std::cout << s << " ";
}
// 输出: 90 85 80 92 88范围 for 只能用于"知道自身大小"的数组——传给函数后就不好使了,后面会解释原因。
多维数组——矩阵的内存真相
C++ 支持多维数组,本质上是"数组的数组"。最常见的是二维数组,用来表示矩阵或表格:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};这段代码声明了一个 3 行 4 列的矩阵。matrix[0] 是第一行(本身是一个包含 4 个 int 的数组),matrix[0][2] 是第一行第三个元素,值为 3。
关键问题:这个矩阵在内存里长什么样?答案是按行连续存储(row-major),所有元素紧密排列在一块连续内存里:
地址: 低地址 →→→→→→→→→→→→→→→→→→→→→→→ 高地址
内容: 1 2 3 4 5 6 7 8 9 10 11 12
↑--- 行0 ---↑--- 行1 ---↑--- 行2 ---↑matrix[1][0] 在内存中紧挨着 matrix[0][3]。理解这一点对后续理解指针和数组的关系至关重要。
遍历二维数组用嵌套循环:
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << matrix[i][j] << "\t";
}
std::cout << std::endl;
}输出:
1 2 3 4
5 6 7 8
9 10 11 12这里有一个性能细节:因为内存按行存储,外层遍历行、内层遍历列是最缓存友好的方式。如果颠倒内外层循环,CPU 每次访问都在内存中跳跃,缓存命中率大幅下降,大规模数据中差异可达数倍。
数组传参——一切噩梦的起点
现在来到 C 风格数组最大的坑:把数组传给函数时,它会发生退化(decay)。
void print_array(int arr[])
{
std::cout << "sizeof(arr) = " << sizeof(arr) << std::endl;
}
int main()
{
int data[5] = {1, 2, 3, 4, 5};
std::cout << "sizeof(data) = " << sizeof(data) << std::endl;
print_array(data);
return 0;
}输出:
sizeof(data) = 20
sizeof(arr) = 8在 main 里,sizeof(data) 是 20(5 个 int,每个 4 字节)。但到了函数里,sizeof(arr) 变成了 8——这是 64 位系统上指针的大小,不是数组的大小。
这就是数组退化:数组作为参数传递时自动退化为指向首元素的指针,函数签名里的 int arr[] 和 int* arr 完全等价。
踩坑预警:数组退化意味着函数内部完全丢失了数组的大小信息。你没法用
sizeof算出元素个数,也没法用范围for循环遍历它。如果你在函数里写sizeof(arr) / sizeof(arr[0]),得到的不是数组长度,而是"一个指针除以一个 int"的无意义结果。这就是为什么 C 风格的函数几乎总是要求你把数组长度作为额外的参数传进来。
所以正确的做法是显式传递大小:
void print_array(const int arr[], int size)
{
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}用 const 修饰是因为函数只读取不修改,这是一个好习惯——编译器会在你不小心修改它时报错。
多维数组传参
多维数组传参更麻烦——你必须告诉编译器第二维(及更高维)的大小,否则编译器无法计算元素地址:
// 编译器需要知道第二维是 4 才能正确计算 matrix[i][j] 的地址
void print_matrix(int matrix[][4], int rows)
{
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << matrix[i][j] << "\t";
}
std::cout << std::endl;
}
}这直接导致函数只能接受第二维恰好是 4 的数组,3x3 矩阵就不适用了。这也是 C 风格数组在实际项目中非常难用的原因之一。
C 数组 vs 现代替代品
说了这么多,我们已经感受到了 C 风格数组的各种痛点。它们不能直接赋值——int b[3] = a; 编译器直接拒绝;不能作为函数返回值——返回局部数组的指针更危险,因为栈帧回收后内存已无效;会退化成指针丢失大小信息;长度必须在编译期确定,不支持运行时动态大小。
踩坑预警:C 风格数组还有一个容易被忽略的陷阱——你无法用
auto推断数组类型。auto a = {1,2,3};推断出的是std::initializer_list<int>,不是数组。auto b = arr;(arr是数组)推断出的是指针,不是数组的拷贝。这些隐式行为都和数组退化有关,稍不留神就会写出和预期完全不同的代码。
这些问题正是 C++11 引入 std::array 的原因——它在栈上分配内存(和 C 数组一样),但提供了赋值、比较、范围 for、.size() 等现代特性,而且不会退化为指针。但理解 C 风格数组依然重要,因为你会在遗留代码、C 语言库、嵌入式代码中不断遇到它们。
实战演练——arrays.cpp
把这一章的核心知识点整合到一个程序里:
// arrays.cpp
// C 风格数组综合演练:初始化、遍历、函数传参、矩阵操作
#include <iostream>
/// @brief 打印一维数组
void print_array(const int arr[], int size)
{
for (int i = 0; i < size; ++i) {
std::cout << arr[i];
if (i < size - 1) {
std::cout << ", ";
}
}
std::cout << std::endl;
}
/// @brief 计算数组元素之和
int array_sum(const int arr[], int size)
{
int total = 0;
for (int i = 0; i < size; ++i) {
total += arr[i];
}
return total;
}
/// @brief 打印矩阵(第二维固定为 4)
void print_matrix(const int matrix[][4], int rows)
{
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << matrix[i][j] << "\t";
}
std::cout << std::endl;
}
}
/// @brief 将 3x4 矩阵转置为 4x3 矩阵
void transpose_3x4(const int src[][4], int dst[][3])
{
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
dst[j][i] = src[i][j];
}
}
}
int main()
{
// --- 初始化方式展示 ---
std::cout << "=== 初始化方式 ===" << std::endl;
int full_init[5] = {10, 20, 30, 40, 50};
std::cout << "完全初始化: ";
print_array(full_init, 5);
int partial_init[5] = {1, 2}; // 后面自动填 0
std::cout << "部分初始化: ";
print_array(partial_init, 5);
int zero_init[5] = {}; // 全部填 0
std::cout << "零初始化: ";
print_array(zero_init, 5);
int deduced[] = {2, 3, 5, 7, 11, 13};
constexpr int kDeducedCount = sizeof(deduced) / sizeof(deduced[0]);
std::cout << "大小推断: ";
print_array(deduced, kDeducedCount);
std::cout << std::endl;
// --- 遍历与求和 ---
std::cout << "=== 遍历与求和 ===" << std::endl;
int scores[] = {90, 85, 78, 92, 88};
constexpr int kScoreCount = sizeof(scores) / sizeof(scores[0]);
std::cout << "成绩: ";
print_array(scores, kScoreCount);
int total = array_sum(scores, kScoreCount);
double average = static_cast<double>(total) / kScoreCount;
std::cout << "总分: " << total << std::endl;
std::cout << "均分: " << average << std::endl;
std::cout << std::endl;
// --- 矩阵操作 ---
std::cout << "=== 矩阵操作 ===" << std::endl;
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
std::cout << "原始矩阵 (3x4):" << std::endl;
print_matrix(matrix, 3);
int transposed[4][3] = {};
transpose_3x4(matrix, transposed);
std::cout << std::endl << "转置矩阵 (4x3):" << std::endl;
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << transposed[i][j] << "\t";
}
std::cout << std::endl;
}
return 0;
}编译运行:g++ -std=c++17 -Wall -Wextra -o arrays arrays.cpp && ./arrays
预期输出:
=== 初始化方式 ===
完全初始化: 10, 20, 30, 40, 50
部分初始化: 1, 2, 0, 0, 0
零初始化: 0, 0, 0, 0, 0
大小推断: 2, 3, 5, 7, 11, 13
=== 遍历与求和 ===
成绩: 90, 85, 78, 92, 88
总分: 433
均分: 86.6
=== 矩阵操作 ===
原始矩阵 (3x4):
1 2 3 4
5 6 7 8
9 10 11 12
转置矩阵 (4x3):
1 5 9
2 6 10
3 7 11
4 8 12验证一下:90 + 85 + 78 + 92 + 88 = 433,均分 86.6,没问题。矩阵转置后第 0 行变成第 0 列,正确。
动手试试
光看不练等于没学,建议每题都动手写一遍。
练习一:数组求和与均值
写一个程序,声明一个包含 10 个整数的数组,写两个函数分别计算总和和平均值(平均值返回 double)。验证方法:手动加一遍,和程序输出对比。
练习二:矩阵转置
写一个函数,将 N x M 的二维数组转置为 M x N。先用固定大小(2x3 转置为 3x2)实现,再思考:如果行数列数也要是参数,C 风格数组能做到吗?
练习三:修复越界 bug
下面这段代码有越界访问的 bug,找到并修复它:
int data[5] = {10, 20, 30, 40, 50};
for (int i = 0; i <= 5; ++i) { // 提示:仔细看循环条件
std::cout << data[i] << std::endl;
}这种 off-by-one 错误在初学者代码里非常常见。
小结
这一章我们拆解了 C 风格数组。数组在内存中连续存储,下标从 0 开始,sizeof(arr) / sizeof(arr[0]) 可以获取元素个数(但只在声明作用域内有效)。多维数组按行连续存储,按行优先遍历对缓存更友好。数组传参时退化为指针,丢失大小信息。不能赋值、不能返回、没有边界检查——这些痛点正是 std::array 存在的理由。
下一章我们就来看看 std::array——在保持 C 数组性能优势的同时,补齐了所有短板的现代替代品。