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 代码里随处可见,但后面咱们会讲到它的局限性。
访问元素:下标从 0 开始
咱们先记住: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
咱们把这一章的核心知识点整合进一个程序:
展开代码收起代码共 109 行
// 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
预期输出:
展开代码收起代码共 22 行
=== 初始化方式 ===
完全初始化: 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)。验证方法:咱们手动加一遍,和程序输出对比。
参考答案
展开代码收起代码共 35 行
#include <iostream>
constexpr int sum(const int a[], int n)
{
int total = 0;
for (int i = 0; i < n; i++)
{
total += a[i];
}
return total;
}
constexpr double average(const int a[], int n)
{
if (n == 0 || n < 0)
return 0.0;
return static_cast<double>(sum(a, n)) / n;
}
int main()
{
constexpr int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
constexpr int size = sizeof(arr) / sizeof(arr[0]);
constexpr int total = sum(arr, size);
constexpr double avg = average(arr, size);
std::cout << "总和: " << total << std::endl;
std::cout << "平均值: " << avg << std::endl;
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main运行结果:
总和: 55
平均值: 5.5练习二:矩阵转置
请您写一个函数,将 N × M 的二维数组转置为 M × N。先用固定大小(2×3 转置为 3×2)实现,再思考:如果行数、列数要在运行时传入,标准 C++ 应该如何表示这块矩阵内存?
参考答案
2x3 转置为 3x2
constexpr void transpose_2x3(const int (&matrix)[2][3], int (&result)[3][2])
{
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 3; j++)
{
result[j][i] = matrix[i][j];
}
}
}咱们也可以看看 C99 中的 VLA 写法(不是标准 C++):
void transpose(int m, int n, const int matrix[m][n], int result[n][m])
{
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
result[j][i] = matrix[i][j];
}
}
}咱们看到上面的函数参数使用了变长数组(variable-length array,VLA)语法:matrix[m][n] 和 result[n][m] 的边界由运行时参数决定。这是 C99 支持的特性(C11 将 VLA 列为可选特性), 但它不是标准 C++ 的语法;即使某些 C++ 编译器接受,也属于编译器扩展,不能写进可移植的 C++ 代码。
标准 C++ 的运行时尺寸写法
标准 C++ 中,内置数组的边界属于类型的一部分,咱们不能用普通函数参数表达运行时尺寸。因此, "行数和列数都是参数"并不意味着 C 风格数组完全不能参与函数调用,而是不能直接写成 int matrix[m][n] 这种 VLA 形式。一个简单且可移植的办法是把矩阵按行连续存储为一维数组, 再额外传入行数和列数:
void transpose(int rows, int cols, const int* matrix, int* result)
{
for (int row = 0; row < rows; ++row)
{
for (int col = 0; col < cols; ++col)
{
result[col * rows + row] = matrix[row * cols + col];
}
}
}调用者需要保证 matrix 至少包含 rows * cols 个元素,result 至少包含同样多的元素。 咱们如果希望保留二维下标形式,可以使用模板表示编译期固定的行列数,或使用 std::vector、 std::span 等更适合表达运行时尺寸的类型。
练习三:修复越界 bug
下面这段代码有越界访问的 bug,请您找到并修复它:
int data[5] = {10, 20, 30, 40, 50};
for (int i = 0; i <= 5; ++i) { // 提示:仔细看循环条件
std::cout << data[i] << std::endl;
}参考答案
int data[5] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; ++i) {
std::cout << data[i] << std::endl;
}这种 off-by-one 错误,咱们初学阶段很容易写出来。