14 数组的基本概念、定义及初始化、长度的计算、元素的遍历与访问(越界访问与内存调试),大小端字节序

目录

1 数组的概念

1.1 为什么需要数组

1.1.1 需求分析1

1.1.2 需求分析2

1.1.3 容器的概念

1.2 什么是数组

1.2.1 认识数组

1.2.2 数组名

1.2.3 元素

1.2.4 下标(索引、角标)

1.2.5 数组的长度

1.3 数组的特点

2 数组的定义

2.1 静态数组的定义

2.2 可变长度数组的定义

3 数组的初始化

4 访问数组元素

4.1 有效访问

4.2 越界访问

4.2.1 越界访问的危险性

4.2.2 VS Code 调试数组内存

4.2.3 注意事项

5 大端字节序与小端字节序

6 数组长度的计算

7 数组元素的遍历

7.1 输出各个元素

7.2 初始化各个元素

7.3 计算元素之和与平均数

7.4 获取最值元素

8 测试题


1 数组的概念

1.1 为什么需要数组

1.1.1 需求分析1

        为了统计某公司 50 名员工的工资情况,例如计算平均工资、找到最高工资等,如果我们使用之前的编程知识,首先需要声明 50 个单独的变量来分别记录每位员工的工资。这样做不仅繁琐,而且容易出错。因此,我们可以将所有员工的工资数据存储在一个容器中进行统一管理,并利用这个容器来进行各种计算。

1.1.2 需求分析2

        在左侧图例中,有许多条新闻信息。如果用变量来表示一条新闻信息,我们将需要大量的变量。这样做不仅复杂,而且难以管理和维护。因此,将多条新闻信息存储到一个容器中进行统一管理将会非常方便。同样的道理,右侧图例中有许多条外卖商家数据。如果将多条外卖商家数据存储到一个容器中进行统一管理,也会非常方便。

1.1.3 容器的概念

        生活中的容器:水杯(装水等液体),衣柜(装衣服等物品),集装箱(装货物等)。

        程序中的容器:将多个数据存储到一起,每个数据称为该容器的元素

1.2 什么是数组

1.2.1 认识数组

        数组(Array)多个相同类型数据按一定顺序排列的集合,并使用一个标识符命名(即数组名),通过编号(或索引、下标、角标)的方式对这些数据进行统一管理。

1.2.2 数组名

        数组名是一个标识符,用于引用一个特定的数组它遵循编程语言中标识符的命名规则,例如,不能以数字开头,不能包含特殊字符(除了下划线),并且区分大小写。

        数组名在程序中用来访问数组中的数据,可以通过数组名结合索引来获取或修改数组中的元素。

1.2.3 元素

        数组是由一系列相同类型的元素组成的集合。每个元素都属于相同的类型,如整型、浮点型或结构体类型。

        由于所有元素的数据类型一致,因此可以有效地在内存中连续存储这些元素,这有助于提高访问速度和内存管理效率。

1.2.4 下标(索引、角标)

        数组中的每个元素都有一个与之关联的下标,用以标识其在数组中的位置

        在 C 语言中,数组的下标是从 0 开始的,即第一个元素的下标为 0,最后一个元素的下标为数组长度减一。

        通过下标可以访问数组中的特定元素。例如,如果有一个整型数组 int arr[5];,那么 arr[0] 表示第一个元素,arr[4] 表示最后一个元素。

1.2.5 数组的长度

        数组的长度是指数组中元素的数量在声明数组时通常需要指定长度,除非使用的是动态分配的数组

        静态数组的长度在编译时确定,而动态数组的长度可以在运行时确定。

        数组的长度对于循环遍历数组和进行边界检查非常重要,可以帮助避免访问越界的情况。

1.3 数组的特点

        内存分配当创建一个数组时,系统会在内存中分配一块连续的空间来存储数组的所有元素。所需空间的大小取决于数组的长度以及数组元素的数据类型。

        元素存储数组中的元素在内存中是紧密相邻且顺序排列。这意味着每个元素占用的内存地址是前一个元素地址的基础上增加了一个固定的偏移量(即元素大小)。

        固定长度一旦数组被初始化并分配了内存,它的长度就固定不变了。这意味着在程序执行过程中,不能增加或减少数组中的元素数量。

        快速访问:由于数组元素在内存中是连续存储的,可以通过索引(也称为下标)直接访问特定位置的元素。这种方式非常高效,因为计算机可以直接计算出元素的内存地址。


2 数组的定义

2.1 静态数组的定义

type arrayName[arraySize];
  • type:表示数组中元素的数据类型,例如 int, float, char 等。
  • arrayName:是数组的名称,遵循 C 语言的标识符命名规则。
  • arraySize:是数组的大小,即数组中元素的数量。对于静态数组,这个大小必须在编译时已知;而对于可变长度数组(VLA),大小可以在运行时确定。

        以下是一些静态数组定义的例子:

int numbers[10]; // 定义一个包含 10 个整数的数组
float grades[5]; // 定义一个包含 5 个浮点数的数组
char name[20];   // 定义一个包含 20 个字符的字符数组

2.2 可变长度数组的定义

        在 C 语言中,数组的长度通常是静态确定的即在编译时就需要知道数组的确切大小

        在 C99 标准之后,C 语言引入了可变长度数组 (VLA, Variable-Length Array) 的概念,允许在运行时确定数组的大小。然而,需要注意的是,VLA 的支持并不是所有编译器都默认开启的,特别是在严格的标准模式下(如 -std=c99 或 -std=gnu99),GCC 和 Clang 支持 VLA,但最好还是不使用 VLA !

        可变长度数组允许在运行时确定数组的大小,这种数组的定义形式如下:

int n = 5; // 运行时确定的大小
int array[n]; // 定义一个大小为n的整型数组
  • 使用 VLA 需要编译器支持 C99 标准或更高版本。例如,在 GCC 中,需要使用 -std=c99 或 -std=gnu99 选项来编译。
  • VLA 只能在定义它的函数内有效,不能跨越函数调用。


3 数组的初始化

        方式一:先指定元素的个数和类型,后面再进行初始化。

// 定义数组,数组名字是 arr1,元素类型是 int,元素个数是 3 个  
int arr1[3];

// 定义完成后再给元素赋值
arr1[0] = 100;
arr1[1] = 200;
arr1[2] = 300;

        方式二:指定元素的类型和个数并同时进行初始化。

        当初始化的元素个数与定义的数组长度相等时,所有的元素都会被赋值。

        当初始化的元素个数少于定义的数组长度时,未初始化的元素将被自动设置为 0(对于数值类型)或空字符 \0(对于字符数组)

        当初始化的元素个数多于定义的数组长度时,编译器会报错,因为多余的初始化值没有对应的位置存储。

// 定义完数组直接进行初始化
int arr2[3] = {4,5,6};

// 初始化的元素个数少于定义的数组长度
int arr4[5] = {4,5};   
// arr4[0] = 4, arr4[1] = 5, arr4[2] …… arr4[4] 均为 0

// 初始化的元素个数多于定义的数组长度
int arr3[3] = {4,5,6,7};   // 报错

        当初始化的元素个数多于定义的数组长度时,编译器会报错,如下所示: 

        方式三:如果在初始化列表中给出了所有元素的值,可以省略方括号中的大小,编译器会自动推断出数组的大小

// 没有指定元素个数,系统会自动计算
int arr3[] = {7,8,9,10};

4 访问数组元素

4.1 有效访问

        在 C 语言中,可以通过 “数组名[下标]” 来访问数组中的元素。下标是从 0 开始的整数,表示数组中元素的位置。

        数组的下标范围是从 0 到数组长度减 1。例如,对于一个长度为 n 的数组,有效的下标范围是 0 到 n-1。

#include <stdio.h>

int main()
{
    // 定义一个包含 4个 整数的数组,并初始化这些元素为 10, 20, 30, 40
    int nums[4] = {10, 20, 30, 40};

    // 修改数组中元素的值
    nums[0] += 10;
    nums[1] += 20;
    nums[2] += 30;
    nums[2] = 66; // 再次修改
    nums[3] = 88;

    // 读取并打印数组中每个元素的值
    printf("第一个元素的值:%d\n", nums[0]); // 输出第一个元素的值:20
    printf("第二个元素的值:%d\n", nums[1]); // 输出第二个元素的值,40
    printf("第三个元素的值:%d\n", nums[2]); // 输出第三个元素的值:66
    printf("第四个元素的值:%d\n", nums[3]); // 输出第四个元素的值:88

    return 0;
}

4.2 越界访问

        数组下标必须在指定的范围内使用,即从 0 到数组长度减 1。如果访问的下标超出了这个范围,就称为越界访问(out-of-bounds access)。

4.2.1 越界访问的危险性

  • 未定义行为:越界访问会导致未定义行为(Undefined Behavior),这意味着程序的行为是不可预测的。程序可能会崩溃,也可能继续执行但产生错误的结果。
  • 安全风险:越界访问可能会导致数据损坏或内存破坏,从而引发安全漏洞。
  • 难以调试:由于越界访问可能导致程序状态的改变,这使得调试变得更加困难。

        下面是一个简单的示例代码,演示了越界访问的危险性:

#include <stdio.h>

int main()
{
    // 定义一个包含 4个 整数的数组,并初始化这些元素为 10, 20, 30, 40
    int nums[5] = {10, 20, 30, 40, 50};

    // 修改数组中元素的值
    nums[0] += 10;
    nums[1] += 20;
    nums[2] += 30;
    nums[2] = 66; // 再次修改
    nums[3] = 88;
    nums[4] = 99;

    // 越界访问,修改到了其它内存空间,属于危险行为
    nums[6] = 300;

    // 读取并打印数组中每个元素的值
    printf("第一个元素的值:%d\n", nums[0]); // 输出第一个元素的值:20
    printf("第二个元素的值:%d\n", nums[1]); // 输出第二个元素的值,40
    printf("第三个元素的值:%d\n", nums[2]); // 输出第三个元素的值:66
    printf("第四个元素的值:%d\n", nums[3]); // 输出第四个元素的值:88
    printf("第五个元素的值:%d\n", nums[4]); // 输出第五个元素的值:99

    // 越界访问
    // 尝试访问下标为 -1,5的元素,这是未定义行为,可能输出垃圾值
    printf("越界访问,垃圾数据 nums[-1]:%d\n", nums[-1]); // 不确定的垃圾值
    printf("越界访问,垃圾数据nums[5]:%d\n", nums[5]);    // 不确定的垃圾值
    // 上面 nums[6] = 300; 修改了这个内存地址的值为 300,属于危险行为
    printf("越界访问,不安全行为 nums[6]:%d\n", nums[6]); // 越界访问 nums[6]:300

    return 0;
}

        输出结果如下所示:

4.2.2 VS Code 调试数组内存

        对于上面这个越界访问的程序,为什么输出结果是这样的呢?

        我们可以通过打断点来调试运行该程序,数组初始化完成后的内存空间,如下所示:

        如下图所示,如果尝试访问 a[5],实际上是访问了数组之外的内存,该内存位置的显示的值为十六进制 12 02 00 00(小端字节序,实际上是:00 00 02 12),转换为十进制为 530。因此,输出 a[5] 将得到 530。同理,a[6] 也访问到了后面的内存位置,该内存位置的显示的值为十六进制 2C 01 00 00(小端字节序,实际上是:00 00 01 2C),转换为十进制为 300。因此,输出 a[6] 将得到 300。

4.2.3 注意事项

        C 语言在越界访问数组时通常不会直接报错(即不会像一些高级语言那样在运行时抛出异常或错误)。这是因为 C 语言是一种低级语言,它直接与硬件打交道,提供了对内存的直接访问能力,但同时也要求程序员负责内存的管理和安全。

        在 C 语言中,数组是通过指针进行访问的,数组名在表达式中会被转换成指向数组首元素的指针。当通过数组名加上索引来访问数组元素时,实际上是在进行指针运算,即根据索引值计算出目标元素的地址,然后访问该地址处的数据。如果索引超出了数组分配的内存范围,编译器并不会检查这一点,因为 C 语言的设计哲学之一就是“相信程序员”。因此,编译器会生成直接访问该内存地址的代码,如果那个地址是可访问的(比如没有超出进程的地址空间),程序就会继续执行,但是访问到的数据可能不是预期的,这可能导致数据损坏、程序崩溃或安全漏洞。 


5 大端字节序与小端字节序

        大端字节序(Big-Endian):在这种字节序中,数据的高位字节存储在内存的低地址端,而位字节存储在内存的高地址端。这意味着,从内存的低地址到高地址,数据的字节顺序与它们从高位到低位的顺序相同。

        小端字节序(Little-Endian):与大端字节序相反,小端字节序中,数据的低位字节存储在内存的低地址端,而高位字节存储在内存的高地址端。这意味着,从内存的低地址到高地址,数据的字节顺序与它们从低位到高位的顺序相同。在大多数现代系统中,使用小端序格式,即最低字节在前。

        假设有一个 32 位整数 0x12345678,其在大端存储和小端存储下的内存布局如下:

大端存储(Big-Endian):
内存地址:低地址端 -> 高地址端
存储内容:0x12 0x34 0x56 0x78

小端存储(Little-Endian):
内存地址:低地址端 -> 高地址端
存储内容:0x78 0x56 0x34 0x12

        所以,当看到内存空间显示为 12 02 00 00(低地址端 -> 高地址端)时,这个表示依赖于系统的字节序。

  • 如果系统是大端字节序:这四个字节 12 02 00 00 表示的是一个整数(假设是 32 位无符号整数),其中 12 是最高有效字节,02 是次高有效字节,00(重复两次)是最低有效字节。
  • 如果系统是小端字节序:同样的四个字节 12 02 00 00 将表示一个不同的整数。在这种情况下,00 是最高有效字节(但因为是 0,所以不影响值),00 是次高有效字节(同样不影响值),02 是次低有效字节,12 是最低有效字节但位置最高。

6 数组长度的计算

        静态数组长度(即元素个数)是在数组定义时明确指定且固定的,我们不能在运行时直接获取数组长度,但是,我们可以通过 sizeof 运算符间接计算出数组长度,计算步骤如下:

  1. 使用 sizeof 运算符计算出整个数组的字节长度。sizeof(array)
  2. 由于数组成员是同一类型,每个元素字节长度相等,用整个数组的字节长度除以单个元素的字节长度就可以得到数组的长度。 常用:sizeof(array) / sizeof(array[0]);
  3. 也可以用整个数组的字节长度除以数组的数据类型得到数组的长度。 sizeof(array) / sizeof(数组的基本数据类型);

#include <stdio.h>

int main()
{
    // 定义一个整型数组,如果没有显式指定数组的长度
    // 编译器会根据初始化时提供的元素数量自动确定长度
    int nums1[] = {10, 20, 30, 40, 50, 60, 70};
    int nums2[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};

    // 使用 sizeof 运算符计算整个数组所占用的字节长度
    // sizeof 是编译时运算符,所以 arrByteLen 的值是在编译时就确定的

    // sizeof 计算基本数据类型的大小,必须使用括号将数据类型关键字包裹起来
    // 对于字面量和变量,sizeof 运算符可以直接作用于它们,括号是可选的,可以省略括号
    int arrByteLen1 = sizeof nums1;  // 可以不加括号
    int arrByteLen2 = sizeof(nums2); // 加上括号更直观

    // 用整个数组的字节长度除以数组中单个元素(即 int 类型)的字节长度
    int arrLen1 = arrByteLen1 / sizeof nums1[0];
    int arrLen2 = arrByteLen2 / sizeof(nums1[0]);
    // 随便一个元素都行(因为都是同类型的)
    int arrLen3 = arrByteLen2 / sizeof(nums1[1]);
    // 或者除以数组的基本数据类型
    int arrLen4 = arrByteLen2 / sizeof(int);

    // 打印计算出的数组长度
    printf("数组nums1的长度:%d\n", arrLen1); // 输出:数组的长度:7
    printf("数组nums2的长度:%d\n", arrLen2); // 输出:数组的长度:10
    printf("数组nums2的长度:%d\n", arrLen3); // 输出:数组的长度:10
    printf("数组nums2的长度:%d\n", arrLen4); // 输出:数组的长度:10

    return 0;
}

         输出结果如下所示:

提示:

        sizeof 计算基本数据类型的大小,必须使用括号将数据类型关键字包裹起来。对于字面量和变量,sizeof 运算符可以直接作用于它们,括号是可选的,可以省略括号。


7 数组元素的遍历

7.1 输出各个元素

        遍历数组是指按顺序访问数组中的每个元素,以便读取或修改它们,编程中一般使用循环结构对数组进行遍历。

#include <stdio.h>

int main()
{
    int arr[10] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};

    // 计算数组的总长度(即元素个数)
    // sizeof arr 计算的是整个数组所占用的字节数
    // sizeof arr[0] 计算的是数组中单个元素所占用的字节数
    // 将两者相除得到的就是数组的长度(元素个数)
    int len = sizeof arr / sizeof arr[0];

    // 遍历数组中的每个元素
    // 使用 for 循环,循环变量 i 从 0 开始,直到小于数组的长度(即最后一个元素的索引)
    printf("遍历数组中的元素:\n");
    for (int i = 0; i < len; i++)
    {
        printf("%d: %d \n", i, arr[i]);
    }

    return 0;
}

        输出结果如下所示:

7.2 初始化各个元素

        我们还可以通过遍历数组完成数组的初始化赋值,看下面案例。

        创建长度为 10 的数组,元素依次赋值为 100,200,300,400,500,600,700,800,900,1000,并按逆序输出每个元素,代码如下:

#include <stdio.h>

int main()
{
    // 声明一个整型数组 arr,大小为 10,初始时数组中的元素值未定义
    int arr[10];

    int len = sizeof arr / sizeof arr[0];

    // 遍历数组进行初始化赋值,依次将元素赋值为 100……1000
    for (int i = 0; i < len; i++)
    {
        arr[i] = (i + 1) * 100;
    }

    // 通过遍历逆序输出数组元素
    // 从数组的最后一个元素开始,直到第一个元素
    // 注意逆序第一个下标不是 len 而是 len - 1
    for (int i = len - 1; i >= 0; i--)
    {
        printf("%d ", arr[i]); // 1000 900 800 700 600 500 400 300 200 100
    }

    return 0;
}

7.3 计算元素之和与平均数

        计算数组中所有元素的和以及平均数。

#include <stdio.h>

int main()
{
    // 定义并初始化一个整型数组 arr,包含 10 个元素
    int arr[10] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};

    // 计算数组的长度
    int len = sizeof arr / sizeof arr[0];

    // 定义一个整型变量 sum,用于存储数组所有元素的总和
    int sum = 0;

    // 使用 for 循环遍历数组中的每个元素
    // i 是循环变量,从 0 开始,直到小于数组的长度 len
    for (int i = 0; i < len; i++)
    {
        // 在每次循环中,将当前元素的值加到 sum 上
        sum += arr[i];
    }

    // 计算平均值
    // 将总和 sum 除以元素数量 len,得到平均值
    double avg = (double)sum / len;

    printf("数组元素之和:%d\n", sum);     // 375
    printf("数组元素平均值:%.2f\n", avg); // 37.50

    return 0;
}

7.4 获取最值元素

        取出数组中值最大和最小的元素。

#include <stdio.h>

int main()
{
    // 定义一个整型数组 arr,包含 10 个元素,并初始化它们
    int arr[10] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};

    // 计算数组的长度。
    int len = sizeof arr / sizeof arr[0];

    // 定义一个整型变量 max,用于存储数组中的最大值,初始化为数组的第一个元素的值
    // 先假设第一个元素为最大值,然后循环遍历
    int max = arr[0];

    // 方法一:使用 for 循环遍历数组中的每个元素
    for (int i = 0; i < len; i++)
    {
        // 在循环体内,使用 if 语句检查当前元素(arr[i])是否大于 max
        // 如果是,就将当前元素的值赋给 max,更新 max 为当前找到的最大值
        if (arr[i] > max)
        {
            max = arr[i];
        }
    }
    printf("最大的元素值:%d\n", max); // 108

    // 方法二:使用 for 循环遍历数组中的每个元素
    for (int i = 0; i < len; i++)
    {
        // 或者用三元运算符
        max = arr[i] > max ? arr[i] : max;
    }
    printf("最大的元素值:%d\n", max); // 108

    // 定义一个整型变量 min,用于存储数组中的最小值,初始化为数组的第一个元素的值
    // 先假设第一个元素为最小值,然后循环遍历
    int min = arr[0];

    // 方法一:使用 for 循环遍历数组中的每个元素
    for (int i = 0; i < len; i++)
    {
        // 在循环体内,使用 if 语句检查当前元素(arr[i])是否小于 min
        // 如果是,就将当前元素的值赋给 min,更新 min 为当前找到的最小值
        if (arr[i] < min)
        {
            min = arr[i];
        }
    }
    printf("最小的元素值:%d\n", min); // 2

    // 方法二:使用 for 循环遍历数组中的每个元素
    for (int i = 0; i < len; i++)
    {
        // 或者用三元运算符
        min = arr[i] < min ? arr[i] : min;
    }
    printf("最小的元素值:%d\n", min); // 2

    return 0;
}

8 测试题

1. 如何计算数组的长度

【答案】

  1. 使用 sizeof 运算符计算出整个数组的字节长度。sizeof(array)
  2. 由于数组成员是同一类型,每个元素字节长度相等,用整个数组的字节长度除以单个元素的字节长度就可以得到数组的长度。 常用:sizeof(array) / sizeof(array[0]);
  3. 也可以用整个数组的字节长度除以数组的数据类型得到数组的长度。 sizeof(array) / sizeof(数组的基本数据类型);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thanks_ks

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值