C++(四十三):数组

数组

C 风格数组是 C++ 语言核心部分之一,直接继承自 C 语言。它是最基本、最底层的数组形式。

1. 定义与核心特性

C 风格数组是在内存中连续存储的、具有固定数量类型相同的元素的集合。

  • 固定大小 (Fixed Size): 数组的大小在声明时必须指定,并且必须是一个编译时常量(Constant Expression)。一旦声明,其大小在运行时不能改变。
    const int ARRAY_SIZE = 10;
    int fixedSizeArray[ARRAY_SIZE]; // 正确,ARRAY_SIZE 是编译时常量
    // int size_variable = 5;
    // int variableSizeArray[size_variable]; // C++ 标准不允许 (虽然某些编译器扩展支持 VLA)
    
  • 相同数据类型 (Homogeneous): 数组中的所有元素必须是同一种数据类型(如 int, float, char, 自定义 classstruct 等)。
  • 连续内存 (Contiguous Memory): 数组元素在内存中是紧密相邻存放的,没有任何间隙。这使得通过指针进行地址算术来遍历数组成为可能(例如 *(arr + i) 等价于 arr[i])。整个数组占据的内存大小为 sizeof(elementType) * numberOfElements
  • 零基索引 (Zero-based Indexing): 数组的第一个元素的索引是 0,第二个是 1,最后一个元素的索引是 N-1(其中 N 是数组的大小)。通过 arrayName[index] 形式访问元素。

2. 声明与初始化

声明语法:
dataType arrayName[arraySize];

初始化方法:

  • 完整初始化列表:
    int scores[5] = {90, 85, 92, 78, 88}; // 初始化所有5个元素
    
  • 部分初始化列表: 未指定的元素会被初始化为其类型的零值(对于数值类型是 0,字符是 \0,指针是 nullptr,类类型会调用默认构造函数)。
    int numbers[10] = {1, 2, 3}; // numbers[0]=1, numbers[1]=2, numbers[2]=3
                                // numbers[3] 到 numbers[9] 都被初始化为 0
    
  • 全部零初始化:
    int zeros[100] = {0}; // 所有元素初始化为 0 (常用C风格)
    int more_zeros[100] {}; // C++11 起,使用空花括号初始化为零值
    
  • 根据初始化列表推断大小: 如果提供了初始化列表,可以省略数组大小。
    int ages[] = {25, 30, 22}; // 编译器自动推断大小为 3
    
  • 字符串字面量初始化 (特例): char 数组可以用字符串字面量初始化,会自动在末尾添加空终止符 \0
    char greeting[] = "Hello"; // greeting 大小为 6 (H, e, l, l, o, \0)
    char name[10] = "Alice"; // name 大小为 10,前 6 个字符是 A, l, i, c, e, \0,后面 4 个也是 \0
    

3. 访问元素与边界问题

使用 arrayName[index] 访问元素。

最关键的特性/风险:无边界检查 (No Bounds Checking)

C 风格数组的 operator[] 不进行任何边界检查。访问超出数组有效索引范围(即小于 0 或大于等于 arraySize)的内存位置会导致未定义行为 (Undefined Behavior, UB)

  • 后果: 程序可能崩溃、产生错误结果、覆盖其他变量的数据,甚至看似正常运行但埋下安全隐患(如缓冲区溢出漏洞)。
  • 责任: 程序员必须自行确保所有数组访问都在 [0, arraySize - 1] 的有效范围内。
int data[3] = {10, 20, 30};
int first = data[0]; // OK, first = 10
int last = data[2];  // OK, last = 30

// 以下均为未定义行为!非常危险!
// int error1 = data[3];  // 越界访问 (索引超出上限)
// int error2 = data[-1]; // 越界访问 (索引低于下限)
// data[3] = 40;         // 越界写入,可能破坏其他数据或导致崩溃

4. 数组到指针的退化 (Array-to-Pointer Decay)

这是 C 风格数组在 C++ 中最令人困惑和容易出错的特性之一。在大多数表达式中使用数组名时(例外情况包括作为 sizeof 的操作数、& 地址运算符的操作数、用字符串字面量初始化 char 数组时),数组名会自动退化 (decay) 为指向其首元素指针

void printArray(int* arr, int size) { // 函数接收的是指针,不知道原始数组大小
    // sizeof(arr) 在这里得到的是指针的大小(如 4 或 8 字节),而不是数组的总字节数!
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " "; // 可以用指针像数组一样访问
    }
    std::cout << std::endl;
}

int main() {
    int my_array[5] = {1, 2, 3, 4, 5};
    std::cout << sizeof(my_array) << std::endl; // 输出: sizeof(int) * 5 (例如 20)

    int* ptr = my_array; // 数组名 my_array 退化为指向 my_array[0] 的指针

    printArray(my_array, 5); // 传递数组名时发生退化,必须额外传递大小
    return 0;
}

由于退化,函数通常无法知道传递给它的 C 风格数组的原始大小,因此必须将大小作为单独的参数传递。

5. 获取大小

可以使用 sizeof 运算符来计算 C 风格数组在其定义所在的作用域内的总字节大小。结合单个元素的字节大小,可以计算出元素数量:

int data[] = {10, 20, 30, 40, 50};
int element_count = sizeof(data) / sizeof(data[0]); // element_count = 5

重要限制: 这个方法仅在数组名未退化为指针时有效。一旦数组传递给函数(发生退化),在函数内部使用 sizeof 将得到指针的大小,而不是数组的大小。

6. C 风格字符串 (char 数组)

以空字符 \0 (null terminator) 结尾的 char 数组是 C 语言处理字符串的方式。C++ 继承了这一点,并提供了 <cstring> (或 C 的 <string.h>) 头文件中的函数(如 strlen, strcpy, strcat, strcmp 等)来操作它们。

  • 风险: 这些 C 库函数通常不检查目标缓冲区的大小,极易导致缓冲区溢出,是常见的安全漏洞来源。例如,strcpy 会一直复制直到遇到源字符串的 \0,如果目标数组不够大,就会发生越界写入。
  • 替代品: 在 C++ 中,强烈推荐使用 std::string来处理字符串,它自动管理内存,提供边界检查(通过 .at()),并且有更丰富、更安全的操作接口。

7. 多维 C 风格数组

可以声明维度大于 1 的数组。

int matrix[3][4]; // 一个 3 行 4 列的二维数组 (可以看作 3 个包含 4 个 int 的数组)
matrix[1][2] = 100; // 访问第 2 行(索引1)、第 3 列(索引2)的元素

在内存中,多维数组仍然是连续存储的,通常按行优先 (row-major) 顺序排列。即 matrix[0][0], matrix[0][1], ..., matrix[0][3], matrix[1][0], ...

8. 优点与缺点

优点:

  • 语法简单直观(用于基本声明)。
  • 与 C 代码和底层 API 兼容性好。
  • 内存布局紧凑,理论上直接访问可能非常快(但现代编译器对 std::vector/std::array 的优化也很好)。

缺点:

  • 大小固定: 无法在运行时调整大小。
  • 无边界检查: 极易发生越界访问,导致严重错误和安全问题。这是最大的缺点。
  • 指针退化: 传递给函数时丢失大小信息,使用不便且容易出错。
  • 缺乏功能: 没有成员函数,需要手动管理或使用全局函数(如 C 字符串函数)。
  • 不能直接复制或赋值: 不能用 = 直接将一个数组赋给另一个数组(数组名会退化为指针,导致只复制地址)。需要逐元素复制。

9. 总结与现代 C++ 建议

C 风格数组是 C++ 中数组功能的基础,但它缺乏现代编程语言所期望的安全性(边界检查)和便利性(动态大小、成员函数)。

在现代 C++ (C++11 及以后) 中,强烈建议:

  • 使用 std::array<T, N> 替代需要编译时固定大小的 C 风格数组。它提供了类型安全、大小信息不丢失、成员函数和可选的边界检查 (.at())。
  • 使用 std::vector<T> 替代需要运行时动态大小的数组。它提供了自动内存管理、丰富的成员函数和可选的边界检查。
  • 使用 std::string 替代 C 风格字符串 (char[])。

只有在与 C 库交互或进行非常底层的性能优化(且明确知道风险并能妥善管理)时,才有理由直接使用 C 风格数组。对于大多数应用开发,std::arraystd::vector 是更优、更安全的选择。


  • 老师: (拿出粉笔和地面,或者大纸和笔) "我们来玩个游戏,在地上画一排格子,就像跳房子那样。我们一起画 5 个 连在一起的方格子,好吗?" (老师和学生一起画或老师示范画)

    (地面上)  [ ]---[ ]---[ ]---[ ]---[ ]
    
  • 老师: "画好了!数一数,我们正好画了几个格子?"

  • 学生: "5 个!"

  • 老师: "对!不多不少,正好 5 个。这个格子的数量是我们画的时候就定好的,对不对?我们能随便擦掉一个,或者在旁边轻松加上第 6 个吗?"

  • 学生: "不能!/ 不容易!"

  • 老师: "是的,这个数量是固定的,画好了就定了。(固定大小)而且你看,这些格子是不是紧紧挨在一起的?" (连续

  • 老师: "这个用粉笔画的、数量固定的、挨在一起的格子,就很像我们今天要认识的这种最简单的‘电脑盒子’—— C 风格数组!"

  • 老师: (在黑板或纸上画出 5 个连着的方框,并在下面标号 0 到 4)

      粉笔画的盒子 (C 风格数组)
      +---+---+---+---+---+
      |   |   |   |   |   |
      +---+---+---+---+---+
        0   1   2   3   4
    
  • 老师: "我们给这些‘粉笔格子’编上号,记住电脑是从 0 开始数数的!所以是 0 号、1 号、2 号、3 号、4 号。"

  • 老师: "我们可以在格子里放东西,比如放我们收集的小石子数量:" (在格子里填上数字)

      +---+---+---+---+---+
      | 3 | 5 | 2 | 6 | 4 |
      +---+---+---+---+---+
        0   1   2   3   4
    
  • 老师: "现在,我要告诉你们这种‘粉笔盒子’一个非常非常重要的秘密!它有点‘傻’,它不知道自己边界在哪里!" (在 0 号格子左边和 4 号格子右边画上红色的叉叉或波浪线,表示危险区域)

      危险! <--- +---+---+---+---+---+ ---> 危险!
                 | 3 | 5 | 2 | 6 | 4 |
                 +---+---+---+---+---+
                   0   1   2   3   4
    
  • 老师: "如果我们不小心,让电脑去找 5 号格子(那里根本没有格子!),或者 -1 号格子,电脑就会跑到‘危险区域’去!它可能会‘摔跤’(程序出错)或者把旁边的东西弄乱(破坏数据)!所以,用这种‘粉笔盒子’时,我们自己必须非常小心,只能在 0 号到 4 号这些画好的格子里活动!" (强调无安全防护)

  • 老师: "这种最老式的‘数组盒子’,我们叫它 C 风格数组。我们来总结一下它的特点:"

    • "格子数量是固定的:就像我们画的粉笔格,一旦定了 5 个,就不能随便变成 6 个或 4 个了。" (代码示意:int chalk_boxes[5]; 就是固定要 5 个)
    • "格子从 0 开始编号:找东西要用 0, 1, 2, 3... 这样的号码(索引)。"
    • "没有‘护栏’,要小心边界!:这是最重要的一点!如果你告诉电脑去拿一个不存在的号码的格子(比如 5 号),它不会提醒你‘嘿,那里没格子!’,它可能会直接出错!所以用它的时候,程序员自己要特别小心,不能‘跳出格子’。"
    • "有点‘健忘’:如果把这个‘粉笔格子’交给别人(比如程序的其他部分),它有时会‘忘记’自己到底有几个格子长,你得另外告诉别人‘这排格子有 5 个哦!’" (简单比喻指针退化)
  • 老师: "因为这种老式盒子有这些‘小毛病’(特别是没有护栏很危险),所以现在的大人程序员们,除非有特殊情况,更喜欢用我们之前提到的那些更‘聪明’、更‘安全’的新式盒子(像 std::arraystd::vector)。但了解这种最基础的‘粉笔盒子’也很重要,因为它是所有盒子的‘祖先’!"

总结

"今天我们认识了最古老的一种电脑‘数组盒子’—— C 风格数组。它就像我们画在地上的固定数量的粉笔格子,格子挨着格子,从 0 开始编号。它最特别的地方是没有安全护栏,我们用它的时候必须自己非常小心,千万不能跑到格子外面去!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容