C语言:数组

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

数组是C语言中最基础且核心的数据结构,本质是相同类型元素的连续集合,广泛用于存储批量数据(如成绩、坐标、字符串等)。

一、数组的核心概念

1.1 数组的定义

  • 定义:数组是一组具有相同数据类型的元素,在内存中连续存储的集合。
  • 核心特性
    1. 元素类型统一:不能在一个int数组中存储char或float类型数据;
    2. 长度固定:创建时必须指定大小(常量值),后续无法动态扩容(C99出现的VLA边长数组针对此做出了便利性更新);
    3. 连续存储:元素在内存中紧密排列,无间隙,便于快速访问。
  • 分类:按维度分为一维数组(最常用)、二维数组(矩阵形式)、多维数组(极少使用)。

1.2 为什么用数组?

  • 替代多个独立变量:存储100个学生成绩,用int scores[100]比100个独立变量简洁;
  • 支持随机访问:通过下标直接定位元素(时间复杂度O(1));
  • 便于批量处理:结合循环可快速遍历、修改所有元素。

二、一维数组

一维数组是最常用的形式,核心围绕“创建→初始化→访问→遍历”展开,同时需注意下标、内存等关键细节。

2.1 一维数组的创建

语法格式:type arr_name[常量值];

  • type:元素数据类型(int、char、float等);
  • arr_name:数组名(符合标识符规则);
  • 常量值:数组长度(必须是编译时确定的常量,不能是变量)。

示例:

int arr1[5];          // 定义int型数组,长度5,元素默认随机值
char arr2[10];        // 定义char型数组,长度10
float arr3[3];        // 定义float型数组,长度3
// int arr4[n];        // 错误:n是变量,C语言不支持动态长度数组(C99变长数组除外)

2.2 一维数组的初始化

初始化是创建数组时赋予初始值,避免默认随机值,有多种灵活方式:

    // 1. 完全初始化:所有元素显式赋值
    int arr1[5] = {1, 2, 3, 4, 5};
    // 2. 不完全初始化:未赋值元素默认置0
    int arr2[5] = {1, 2}; // 等价于 {1,2,0,0,0}
    // 3. 省略长度:编译器根据初始化元素个数自动推断长度
    int arr3[] = {1, 2, 3}; // 长度为3
    // 4. 指定位置初始化(C99及以上):仅给特定下标赋值,其余为0
    int arr4[5] = {[0]=10, [3]=20}; // arr4[0]=10, arr4[3]=20,其余为0
    // 错误示例:初始化元素个数不可以超过数组长度
    int arr5[3] = {1,2,3,4};
    

2.3 一维数组的访问与遍历

  • 访问方式:通过下标引用运算符[] 访问,下标从0开始(第一个元素下标0,最后一个元素下标长度-1);
  • 遍历方式:结合for循环生成下标,批量访问所有元素
    示例:
    int arr[] = {10, 20, 30, 40, 50};
    int len = sizeof(arr) / sizeof(arr[0]); // 通用计算数组长度:总大小/单个元素大小
    
    // 1. 直接访问指定元素
    printf("arr[0] = %d\n", arr[0]); // 输出:10(第一个元素)
    printf("arr[3] = %d\n", arr[3]); // 输出:40(第四个元素)
    
    // 2. 遍历数组(正序)
    printf("正序遍历:");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]); // 输出:10 20 30 40 50
    }
    printf("\n");
    
    // 3. 遍历数组(逆序)
    printf("逆序遍历:");
    for (int i = len - 1; i >= 0; i--) {
        printf("%d ", arr[i]); // 输出:50 40 30 20 10
    }
    printf("\n");
   

2.5 内存布局:连续存储

一维数组的元素在内存中连续排列,每个元素占用的字节数等于其数据类型大小(如int占4字节),下标递增时地址依次递增。

#include <stdio.h>

int main() {
    int arr[] = {1, 2, 3};
    for (int i = 0; i < 3; i++) {
        printf("arr[%d] 的地址:%p\n", i, &arr[i]);
    }
    // 输出示例(地址值因系统而异,但相邻元素差4字节):
    // arr[0] 的地址:006FFD7C
    // arr[1] 的地址:006FFD80(+4字节)
    // arr[2] 的地址:006FFD84(+4字节)
    return 0;
}

2.6 数组名的本质:arr、&arr[0]、&arr的区别

数组名arr的行为特殊,不同场景下含义不同,核心区别如下:

表达式地址值类型sizeof结果(64位系统)+1运算效果
arr首元素地址int[3](sizeof中)/int*(其他场景)12(数组总大小)移动4字节(一个int)
&arr[0]首元素地址int*(指向首元素)8(指针大小)移动4字节(一个int)
&arr首元素地址int(*)[3](数组指针)8(指针大小)移动12字节(整个数组)

示例验证:

#include <stdio.h>

int main() {
    int arr[3] = {1,2,3};
    printf("arr = %p\n", arr);
    printf("&arr[0] = %p\n", &arr[0]);
    printf("&arr = %p\n", &arr);
    printf("\n");
    printf("arr + 1 = %p\n", arr + 1);
    printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
    printf("&arr + 1 = %p\n", &arr + 1);
    return 0;
}

在这里插入图片描述

结论

  • arr在大多数场景下退化为指向首元素的指针(int*),但在sizeof(arr)中保持数组类型,返回总大小;
  • &arr[0]是明确的首元素指针(int*),与退化后的arr功能一致;
  • &arr是数组指针,指向整个数组,+1运算移动整个数组的长度。

三、二维数组

二维数组可理解为每个元素是一个一维数组的一维数组,常用于存储矩阵、表格等二维数据。

4.1 二维数组的创建与初始化

(1)创建语法

type arr_name[行数][列数];

  • 行数:一维数组个数;
  • 列数:每个一维数组的元素个数;
  • 行数可省略(编译器自动推断),但列数必须指定,列数是二维数组类型的一部分
(2)初始化
    // 1. 完全初始化(按行排列)
    int arr1[2][3] = {1,2,3,4,5,6}; //等价于 {{1,2,3},{4,5,6}}
    // 2. 按行显式初始化(推荐,可读性高)
    int arr2[2][3] = {{1,2}, {4,5}}; // 等价于 {{1,2,0},{4,5,0}}
    // 3. 省略行数(编译器根据列数和元素个数推断)
        //连续初始化:行数=总元素数量/列数1,向上取整
        int arr3[][4]={1}// 等价于 {1,0,0,0}
        //嵌套初始化:行数=内层嵌套{}数量
        int arr4[][3]={{1},{2},{3}}; // 等价于 {{1,0,0},{2,0,0},{3,0,0}}
      
    int arr3[][3] = {1,2,3,4,5}; // 推断为2行3列(5个元素,需2行容纳)
    // 错误示例:省略列数,二维数组初始化中,列数不可省略
    // int arr4[2][] = {1,2,3,4};

注意事项:赋值时区分 (){},()会执行逗号表达式取尾值,{}则会确定当前一维数组内部情况;

4.2 二维数组的访问与遍历

  • 访问方式arr[行下标][列下标],行、列下标均从0开始;
  • 遍历方式:嵌套for循环(外层遍历行,内层遍历列),优先按行遍历(利用内存局部性,效率更高)。

示例:

#include <stdio.h>

int main() {
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    int rows = sizeof(arr) / sizeof(arr[0]); // 计算行数:总大小/一行的大小
    int cols = sizeof(arr[0]) / sizeof(arr[0][0]); // 计算列数:一行大小/一个元素大小
    
    // 1. 直接访问指定元素
    printf("arr[1][2] = %d\n", arr[1][2]); // 输出:6(第2行第3列)
    
    // 2. 按行遍历(高效)
    printf("按行遍历:");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]); // 输出:1 2 3 4 5 6
        }
    }
    printf("\n");
    
    // 3. 按列遍历
    printf("按列遍历:");
    for (int j = 0; j < cols; j++) {
        for (int i = 0; i < rows; i++) {
            printf("%d ", arr[i][j]); // 输出:1 4 2 5 3 6
        }
    }
    printf("\n");
   /*行的遍历是快于列的遍历,这一点将在4.3提及*/
    
  // 4. 按地址遍历
    printf("按列遍历:");
     int total = sizeof(arr) / sizeof(arr[0][0]);
    int* p = &arr[0][0];
    
    for (int i = 0; i < total; i++) {
        printf("%d ", p[i]);  // 使用数组索引,避免p++操作
    }
    printf("\n");

4.3 二维数组的内存布局

二维数组的元素在内存中依然是连续存储的,按“行优先”顺序排列(先存第0行所有元素,再存第1行,以此类推)。

示例验证:

#include <stdio.h>

int main() {
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("arr[%d][%d] 的地址:%p\n", i, j, &arr[i][j]);
        }
    }
    // 输出示例(相邻元素差4字节,跨行元素也连续):
    // arr[0][2] 的地址:006FFD68
    // arr[1][0] 的地址:006FFD6C(+4字节,连续存储)
    return 0;
}
4.3.plus:硬件层面上的读取

地址: 100 104 108 112 116 120 124 128
数据: 1 2 3 4 5 6 7 8

1.按行遍历(快)
for(i=0;i<2;i++)
  for(j=0;j<4;j++)
    printf("%d", arr[i][j]); // 1,2,3,4,5,6,7,8

访问顺序:100→104→108→112→116→120→124→128
特点:连续访问,缓存友好

2.按列遍历(慢)
for(j=0;j<4;j++)
  for(i=0;i<2;i++)
    printf("%d", arr[i][j]); // 1,5,2,6,3,7,4,8

访问顺序:100→116→104→120→108→124→112→128
特点:跳跃访问,缓存不友好

CPU加载数据的局部性原理:会将内存邻近的存储数据也加载到CPU缓存中

  • 时间局部性:若一个位置被访问,则可能短时间内再次被访问
  • 空间局部性:若一个位置被访问,则可能短时间内邻近位置被访问

由于二维数组数据存储的按行连续性,行遍历时,局部性原理使预读的CPU缓存容易命中,故更为高效

按行:访问arr[0][0]时,CPU把1,2,3,4,5,6,…都加载到缓存,后续访问都在缓存中
按列:访问arr[0][0]加载1-16到缓存,但接着访问arr[1][0](第5个元素),然后访问arr[0][1]时可能已不在缓存中,需要重新加载

4.4arr、&arr[0]、&arr[0][0]的区别与联系

#include <stdio.h>

int main()
{
    // 定义一个3行4列的二维数组
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    printf("============== 二维数组内存布局分析 ==============\n\n");
     // 1. 打印数组内容
    printf("1. 数组内容:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("arr[%d][%d] = %-2d  ", i, j, arr[i][j]);
        }
        printf("\n");
    }

    printf("\n============== 地址对比分析 ==============\n\n");

    // 2. 打印各种地址
    printf("2. 地址值(十六进制):\n");
    printf("arr         = %p\n", (void*)arr);
    printf("&arr[0]     = %p\n", (void*)&arr[0]);
    printf("&arr[0][0]  = %p\n", (void*)&arr[0][0]);

    printf("\n3. 地址值的数值分析:\n");
    printf("arr         = %lu\n", (unsigned long)arr);
    printf("&arr[0]     = %lu\n", (unsigned long)&arr[0]);
    printf("&arr[0][0]  = %lu\n", (unsigned long)&arr[0][0]);

    printf("\n============== 类型分析 ==============\n\n");

    // 3. 类型分析
    printf("4. 类型分析:\n");
    printf("sizeof(arr)          = %lu bytes\n", sizeof(arr));
    printf("sizeof(&arr[0])      = %lu bytes(指针大小)\n", sizeof(&arr[0]));
    printf("sizeof(&arr[0][0])   = %lu bytes(指针大小)\n", sizeof(&arr[0][0]));

    printf("\n5. 指针类型详细分析:\n");
    printf("arr         的类型是:int (*)[4](指向含4个int的数组的指针)\n");
    printf("&arr[0]     的类型是:int (*)[4](同上)\n");
    printf("&arr[0][0]  的类型是:int *      (指向int的指针)\n");

    printf("\n============== 指针运算演示 ==============\n\n");

    // 4. 指针运算演示
    printf("6. 指针运算(+1操作):\n");
    printf("arr + 1      = %p  (移动了 %lu bytes)\n",
        (void*)(arr + 1), (unsigned long)(arr + 1) - (unsigned long)arr);
    printf("&arr[0] + 1  = %p  (移动了 %lu bytes)\n",
        (void*)(&arr[0] + 1), (unsigned long)(&arr[0] + 1) - (unsigned long)&arr[0]);
    printf("&arr[0][0] + 1 = %p  (移动了 %lu bytes)\n",
        (void*)(&arr[0][0] + 1), (unsigned long)(&arr[0][0] + 1) - (unsigned long)&arr[0][0]);

    printf("\n7. 字节计算:\n");
    printf("一个int大小:%lu bytes\n", sizeof(int));
    printf("一行大小(4个int):%lu × %lu = %lu bytes\n",
        sizeof(int), 4ul, sizeof(int) * 4);
    printf("整个数组大小:3 × 4 × %lu = %lu bytes\n",
        sizeof(int), sizeof(arr));

    printf("\n============== 访问方式演示 ==============\n\n");

    // 5. 不同访问方式
    printf("8. 访问arr[1][2]元素(值为7):\n");
    printf("arr[1][2]                = %d\n", arr[1][2]);
    printf("*(*(arr + 1) + 2)        = %d\n", *(*(arr + 1) + 2));
    printf("*(arr[1] + 2)            = %d\n", *(arr[1] + 2));
    printf("*(&arr[0][0] + 1*4 + 2)  = %d\n", *(&arr[0][0] + 1 * 4 + 2));

    printf("\n9. 不同访问方式解析:\n");
    printf("arr + 1          指向第1行(跳过一行)\n");
    printf("*(arr + 1)       指向第1行第0列元素\n");
    printf("*(arr + 1) + 2   指向第1行第2列元素\n");

    printf("\n============== 内存连续性验证 ==============\n\n");

    // 6. 内存连续性验证
    printf("10. 所有元素的内存地址:\n");
    printf("行 列  地址            偏移量\n");
    printf("--------------------------------\n");

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("[%d][%d] %p  +%ld\n",
                i, j,
                (void*)&arr[i][j],
                (long)(&arr[i][j] - &arr[0][0]) * sizeof(int));
        }
    }

    printf("\n11. 通过指针遍历所有元素:\n");
    int* ptr = &arr[0][0];
    printf("使用 &arr[0][0] 指针:");
    for (int i = 0; i < 12; i++) {
        printf("%d ", *(ptr + i));
    }

    printf("\n\n============== 关键总结 ==============\n\n");

    printf("重要结论:\n");
    printf("1. arr 和 &arr[0] 值相同,但 arr 是数组名(特殊)\n");
    printf("2. arr 和 &arr[0] 类型相同(int (*)[4])\n");
    printf("3. &arr[0][0] 是 int* 类型,指向单个元素\n");
    printf("4. arr + 1 移动一行大小(4个int = %lu字节)\n", sizeof(int) * 4);
    printf("5. &arr[0][0] + 1 移动一个int大小(%lu字节)\n", sizeof(int));
    printf("6. 二维数组在内存中是连续存储的\n");

    printf("\n============== 验证 &arr 的区别 ==============\n\n");

    // 额外:&arr 的演示
    printf("12. &arr 的特殊性:\n");
    printf("&arr         = %p\n", (void*)&arr);
    printf("&arr 的类型是:int (*)[3][4](指向整个二维数组的指针)\n");
    printf("&arr + 1     = %p  (移动了整个数组大小:%lu bytes)\n",
        (void*)(&arr + 1), sizeof(arr));

    return 0;
}

代码运行结果

五、数组常见问题

1. 下标越界

  • 表现:程序崩溃、数据错乱、无报错但结果异常;
  • 避免:用sizeof计算数组长度,遍历下标严格控制在0~len-1

2. 数组名误用

  • 误区:将数组名当作指针修改(如arr++);
  • 原因:数组名是“常量指针”,不能被赋值或自增/自减。

3. 数组作为函数参数退化

  • 现象:数组作为函数参数时,会退化为指针,sizeof(arr)返回指针大小(8字节,64位系统);
  • 解决:手动传递数组长度作为参数,而非在函数内计算。
示例:
```c
// 错误:函数内sizeof(arr)返回指针大小,无法得到数组长度
void test(int arr[]) {
    printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出:8(指针大小)
}

// 正确:传递长度参数
void test(int arr[], int len) {
    // 遍历数组
}

4. 二维数组列数不可省略

  • 误区:定义二维数组时省略列数(如int arr[2][]);
  • 原因:编译器需要通过列数计算每行的元素个数,进而推断内存布局。

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值