C语言笔记(第n版):数组

       Array is a type consisting of a contiguously allocated nonempty sequence of objects with a particular element type. The number of those objects (the array size) never changes during the array lifetime.

        数组是一种类型,由具有特定元素类型的连续分配非空对象序列组成。这些对象的数量(数组大小)在数组生命周期内永远不会改变。

        以上是C文档对于数组(Array)的说明,比较术语化,可能不太直白,在阅读完本篇后,希望小白对此能有所体悟。

        数组,展开说是数据元素的组合,实质来讲,在C语言中,是一片内存空间,这片空间分成若干份,每一份的空间大小都相同并用来存储相同类型的变量。

        比如,我有十个数据量,在之前,我可能需要定义十个float变量,而对于更多的数据量,这个工程量就会加剧,但是如果使用数组我就可以快速创建可以容纳我想要数据量个数的空间。

        这就好比,你有三个苹果在手里,和你有一个篮子和三个苹果是不一样的,有了篮子我就可以方便“统一”我的苹果。

        Array, 在古法语中,“arraiare”的意思是“按顺序排列”或“准备”。在拉丁语中,“arraiare”的意思是“按顺序排列”。这些词根与以结构化方式排列元素的概念非常一致,就像在数组中所做的那样。

       So, Here We go!

一、数组

(一)定义与声明

        先看看数组的定义语法:

dataType \ identifier[expression];

        这和之前变量的定义语法,十分相似:

dataType \ identifier;

        多出的部分\left [ expression \right ]        可分为两部分来看:

\left [\, \; \right ] \Rightarrow array \ subscript \ operator

expression

        array \ subscript \ operator,即数组下标运算符,但是,注意,在定义时,这仅仅是作为一种声明,声明其为一个数组类型,而作为一个真正的运算符来使用,我们后面将。

        expression,一个可求值为整型结果的非逗号表达式,用于指定数组的大小。

        示例,类似定义变量,定义数组:

#define SIZE 10

int main(int argc, char* argv[])
{

    int a[10];  // 定义一个大小为10个int类型大小的数组变量a
    int b[6 + 4]; // 定义一个大小为10个float类型大小的数组变量 b
    int c[SIZE]; // 定义一个大小为10个char类型大小的数组变量   c 
    return 0;

}
  • 元素类型可以是任意类型数据类型(整型、浮点型、字符型或者其他类型)。
  • 定义时,[]中必须是一个大于0的整型常量,尽量不要用变量指定(在C99中可以用变量作为数组定义时的大小,如int n = 2; int a[n];),建议使用常量;
  • 定义处,数组长度n表示合法的下标个数(可以是整型表达式、宏常量,如int a[3 + 3]),定义后,在非定义处[]中用于存放表示元素下标的值(可以是常量或变量,或者整型表达式);
  • 类型标识符决定了数组元素的数据类型,也决定了每个元素的存储空间;
  • 数组名为符合规范的标识符

       通过上面你会发现,数组是一个复合类型(这或许是本笔记系列的第一个),因为无论说明类型,我只要加一个数组下标运算符,它都变成了一个这个类型的数组,这里我称组成数组的元素类型称之为“基类型”。

(二)初始化

 数值数组与字符数组

        这是我的一个划分,之所以这么划分是因为在基类型为基本数据类型的数组在赋值等其它一些操作上是有差异的。

        数值数组:泛指那些基类型为,内存数据意义为数字,的数据类型的数组,比如intfloat

        字符数组:指基类型为char的数据类型的数组

下面说明这些在赋值与初始化行为上的差异

初始化

数值数组大致有四种初始化方式(个人说法):

(依序):完全显式初始化、部分初始化、完全默认初始化  

指序):指示器初始化 

// 类型标识符 数组名[元素长度]={初值列表}
// 初值列表是一组用逗号隔开的值,每一个值赋值给对应的数组元素。
// 初值列表可以少于数组元素,但不应该多于数组元素。
// 若只显式初始化部分元素,其它元素将被带初始化为默认值0
int a[5] = {1, 2, 3, 4, 5}; // 完全初始化
int b[] = {1, 2, 3, 4, 5};  // 省略长度的初始化,数组长度由初始化列表的值个数决定
int c[5] = {1, 2, 3};      // 未被初始化元素的值为0
int d[5] = {0};            // 可以理解为第一个元素初始化为0,其它未赋值的元素被“带”赋值为0

int n[5] = {[4]=5,[0]=1,2,3,4}; // 结果 => 1, 2, 3, 4, 5
  • 当定义数组时就初始化,可以不写元素长度时,初值列表值的个数就是数组的元素个数。
  • 对于不初始化的数组,其元素的值是随机的,没有意义。
  • 对于整型数组,int 数组名[数组长度]={0},即可将所有元素赋值为0
  • 通过指示器(designator)可以指定为某个指定位置上的元素赋初值,而对于未被指定的位置的其它初值,将继续前一个已经指定了位置元素的顺序。

对于字符数组,只存在完全初始化和部分初始化,在形式上可分为紧凑型和分散型

char string1[] = "hello world\0";
char string2[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'};
char string3[] = {"hello world"};

char string4[11] = "hello world";
char string5[20] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'};
char string6[20] = {"hello world"};

 和数值数组一样,如果不指定数组的大小,则由初始化给的字符数决定,

        但是,对于不指定数组大小的,紧凑型的数组,的空间大小是字符数+1,这个1是编译器加上的结束符(terminating null,'\0'),这是一个非常重要的结束标志,以后会经常用到。

        如果指定大小,而又没有空间给编译器放置结束符,则就会忽略,比如string4。而如果有空间剩余,则剩余空间会全部初始化为'\0'。

        只有分散型初始化的字符数组手动添加的结束符有效

                

(三)引用与遍历

        一个数组定义完成后,我们必然要在其它地方再使用,数值数组和字符数组在这个地方也存在差异。

元素引用

        对数组元素的引用,要用到之前所提到过的数组下标运算符,它的功能是取得第nth个元素

例如,对于int \: array[\: 5 \; ] = \left \{ 1, 2, 3, 4, 5 \right \};

        数组下标运算符的语法如下:

        pointer-expression [ integer-expression ] \\ integer-expression [ pointer-expression ]

        对于pointer-expression现在你只需要把它当作数组的名字即可,而integer-expression就是一个值为整数的表达式,通俗的说我们可称之为索引(index),例如我想得到上面数组的第二个元素,可以这样

array[\: 1\: ] \\ array[ \: 2 - 1\; ] \\ 1[array] \\ (2 -1)[array]

        字符数组和数值数组在元素引用上没有差异,如

 

数组遍历

         所谓数组的遍历,就是得到所有的元素,这一般通过循环,例如,对于上面的数值数组

int array[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++)
{
   printf("%d ", array[i]);
}

 

        和数值数组一样,字符数组也可以这样,不过更常见的是整体输出,这是字符数组和数值数组在输出不同的一点:

        

数组越界

        数组是没有围栏的内存场地。

数组边界(Array Bounds)

        C语言中的数组是保存相同数据类型元素的连续内存块。数组的索引从“0”开始,一直到“size-1”,其中“size”是数组中元素的数量。访问此范围内的元素是安全且定义的行为。

超出范围访问(Out-of-Range Access

        当尝试使用超出定义范围的索引(小于“0”或大于或等于“size”)访问或修改数组的元素时,可能会出现以下几个问题:

1.未定义的行为:C在运行时不执行边界检查。因此,超出其边界访问数组会导致未定义的行为。这意味着编译器不保证任何特定的行为——程序可能会崩溃、产生不正确的结果或表现出不可预测的行为。

int array[5] = {1, 2, 3, 4, 5};
printf("the value is %d\n", array[5]);

2.内存访问冲突:如果索引过大或为负,您的程序可能会尝试访问不属于数组的内存。这可能会导致某些平台上的内存访问冲突(如分段错误),尤其是在写入内存时。

3.安全漏洞:攻击者可以利用超出范围的访问来执行任意代码或崩溃程序,从而可能导致安全漏洞。

        在其他一些高级语言中,对数组越界有着错误、异常的提醒,但是在C语言中,不会有任何反应,需要我们去检查。

        就比如字符串的越界问题,如果我们没有能够在字符数组后面及时添加上终止符,如下,则输出将不可预料

         根据输出,很明显可以看到两条输出都终止到了a附近,如果你学习到了后面的知识,你就会懂得,第二条输出其实是越界访问到了第一条字符串,所以有两个“Hello World”,这仅仅是输出,如果修改了,数据就会被污染了。

        正确添加终止符后         

         

(四)数组与函数

        数组可以和变量一样,传递给函数,如果要将一个整型数组传递给函数,则目标函数的形参可用下面三种方式之一,经过编译后,本质上都是第三种形式

  • 形参是一个已定义大小的数组的函数原型:void \ func(int \ arr[10], int \ size);
  • 形参是一个未定义大小的数组的函数原型:void \ func(int \ arr[], int \ size);
  • 形参是一个指针的函数原型(后面将会说明):void \ func(int *arr, int \ size);

        这三种方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个数组的首地址。就函数而言,数组的长度是无关紧要的,因为C语言不会对表示数组形参的下标边界进行检查,但是通常还要传递一个表示数组大小的整数。

        一个小示例:

#include <stdio.h>


void printSum(int arr[], int size);

int main() {
    int myArray[] = {1, 2, 3, 4, 5};  
    printSum(myArray, 5); 
    return 0;
}


void printSum(int arr[], int size) {
    int sum = 0;

   
    for (int i = 0; i < size; ++i) {
        sum += arr[i];  
    }

    printf("Sum of array elements: %d\n", sum); 
}

二、多维数组

        一般情况下,我们说数组通常就是前文那样的,但是在语法在也允许多维,就是在定义上多几个数组下标运算符,在多维数组中,比较常用的就是二维数组。

        如果一个一维数组的每个元素都是类型相同(包括大小相同和数据相同)的一维数组,则构成一个二维数组

(一)定义与声明

        定义一个二维数组,只需多一个数组下标运算符。例如,一个3行4列的二维数组可以这样声明:

int \ array[3][4];

        这里,array是一个包含3个元素的一维数组,每个元素又是一个包含4个int元素的数组。

        对于其它基类型的同样。

(二)初始化

        对于初始化二维数组,和一维数组相比,只是形式上有些不同,但是基本上相似:

        如果以内存存储的角度来看,二维数组也是差不多和一维数组一条,所以可以和一维数组一样初始化,比如:

        int \ array[3][4] = \left \{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12\right \};

int \ array2[3][4] = \left \{ [1][1]=1, [1][2]=2, [2][0]=3, [2][1]=4, [2][2]=5, [2][3]=6, 7, 8, 9, 10, 11, 12 \right \};int \ array3[3][4] = \left \{ 0 \right \}; 

          如果以二维的角度来看,也可以这样,一个花括号表示一行的初始化,在这个花括号又可以类似一维数组那样初始化

        二维数组,也可以省略维度,但是只能省略第一维(即行数),这是因为正如前面所说,二维数组是按行存储的,如果不给定二维数组的列,则无法分配空间

(三)引用与遍历


 

        除了下标的增加,在其它地方,二维数组的引用与遍历与一维数组几乎无差别

#include <stdio.h>

int main() {
    // 声明并初始化二维数组
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 遍历并打印数组
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 4; col++) {
            printf("%d ", matrix[row][col]);
        }
        printf("\n");
    }

    return 0;
}

致读者:

        本文略显简洁地讨论了数组的一些方面 ,事实上,使用也相当简洁。但是,简洁的背后往往是复杂的封装,后面将看到隐藏在这背后的一些巧妙机制……

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值