一、数组介绍
C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。
ps:再C99之前的标准不支持变长数组,C99及之后的标准支持变长数组,及数组的长度可以是变量。
数组的声明并不是声明一个个单独的变量,比如 num0、num1、... 、num99,而是声明一个数组变量,比如 num[],然后使用 num[0]、num[1]、...、num[99] 来代表一个个单独的变量。
所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。
数组中的特定元素可以通过索引访问,第一个索引值为 0。
C 语言还允许我们使用指针来处理数组,这使得对数组的操作更加灵活和高效。
二、一维数组的创建
1、一维数组的声明
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
array_type array_name[array_size];
这叫做一维数组。array_size 必须是一个大于零的整数常量,array_type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 int 的包含 10 个元素的数组 num,声明语句如下:
int num[10];
现在 num 是一个可用的数组,可以容纳 10 个类型为 int 的数据。
2、一维数组的初始化
在 C 中,您可以逐个初始化数组,如下所示:
int num[2];//逐个初始化
num[0] = 1;
num[1] = 2;
也可以使用一个初始化语句,如下所示:
int num[2] = {1,2};
大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。
如果省略掉了数组的大小,数组的大小则会是初始化时元素的个数。
因此,如果:
int num[] = {1,2};
这个数组与之前的数组是相同的。
在没有指定数组的大小的时候就必须初始化数组。
有一种情况是不完全初始化,例如:
int num[10] = {1,2};
这里的数组num有10个空间,而初始化时只填入了两个元素,所以空间没有完全被利用,是不完全初始化,这时数组num未初始化的位置是全部被初始化为0了的。
3、一维数组初始化中的知识
#include <stdio.h>int main()
{char ch[3] = {'a','b','c'};printf("%s",ch);return 0;
}
这里的运行结果会有乱码:
为什么呢,是因为这里是完全初始化,数组之中呢没有\0,所以打印完abc不会停止,直到遇见\0才会停止,而后面的数据是不确定的,打印的东西和结束也是不确定的。
#include <stdio.h>int main()
{char ch[10] = {'a','b','c'};printf("%s",ch);return 0;
}
这里的运行结果没有乱码:
原因是这里是不完全初始化,因为\0的ASCII码值为0,所以剩余的部分被初始化为0,故导致剩余的部分都被解析成\0,所以存储会在遇到剩余的部分解析成的\0时停止,就不会出现乱码。
也可以在数组的最后一个元素填入\0主动结束。
#include <stdio.h>int main()
{char ch[] = {'a','b','c','\0'};printf("%s",ch);return 0;
}
因为\0的ASCII码值是数字0,所以数字0与\0的作用一样。
#include <stdio.h>int main()
{char ch[] = {'a','b','c',0};printf("%s",ch);return 0;
}
这里的0是作为\0的ASIIC码,会转换为\0,依旧能实现结束的效果。
char ch[] = "abcde";//数组有6个空间,其中一个是\0
字符串的最后自带\0,也能实现结束的效果。
三、一维数组的使用
1、访问一维数组元素
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:
int num[2] = {1,2};
int firstnum = num[0];
这里是访问num数组的第一个元素,将num数组的第一个元素赋值给firstnum。
下面是一个为数组中某个元素赋值的实例:
int num[2] = {1,2};
num[0] = 6;
上述的语句把数组num中第1个元素的值赋为 6。所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。以下是上面所讨论的数组的的图形表示:
2、数组的大小
对于数组,可以用sizeof()函数求出数组存储大小,再除以数组类型的大小,就是数组的大小。例如:
int num[] = {1,2,3,4,5,6};
int sz = sizeof(num) / sizeof(int);
也可以是除以数组的第一个元素。例如:
int num[] = {1,2,3,4,5,6};
int sz = sizeof(num) / sizeof(num[0]);
对于最后一个元素为\0的字符型数组可以用strlen()函数求字符串长度(不包含\0),数组的长度就是字符串长度加一。例如:
#include <stdio.h>
#include <string.h>int main()
{char ch[] = "abc";printf("%zu", strlen(ch) + 1);return 0;
}
3、数组的存储
#include <stdio.h>
#include <string.h>int main()
{int num[10] = {1,2,3,4,5,6,7,8,9,10};for (int i = 0; i < 10;i++){printf("num[%d]的地址是 %p\n",i,&num[i]);}return 0;
}
我们发现数组的地址差值正好是4,原因是这是整型数组,整型数据的大小是四个字节,每个字节一个地址,恰好每个元素地址差值是4。
这样我们能看到数组中的每个元素的存储大小以及存储内容。
随着数组下标增大,地址也在有规律增大,所以我们发现数组中元素是连续的。
四、二维数组的创建
1、二维数组的声明
格式:
type array_name[row_size][column_size];
例如:
int num[10][10];
2、二维数组的初始化
有多种初始化格式:
(1)
int num[3][3] = {1,2,3,4,5,6,7,8,9};
这样会按顺序放置
如果不完全初始化的话,没初始化的会被初始化为0
int num[3][3] = {1,2,3,4,5,6,7};
(2)
int num[3][4] = { {1,2,3,4},{1,2,3,4},{1,2,3,4} };
这个每一个花括号中是每一行的元素
int num[3][4] = { {1,2,3},{1,2},{1} };
这里不完全初始化也会被初始化为0,但位置是不一样的
(3)
char ch[3][4] = {'a','b','c','d','b','c','d','e','c','d','e','\0'};
char ch[3][4] = {'a','b','c','d','b','c','d','e','c'};
不完全初始化的会被初始化为0
这里与整型数组相似
(4)
char ch[3][4] = {"abc","bcd","cde"};
这里的\0是字符串最后自带的。
不完全初始化也会被初始化为0
char ch[3][4] = {"abc","bc","c"};
3、初始化中的小问题
ps:在初始化二维数组是必须指定列数,行数可忽略,及第二个方括号中必须有值,只要有列数,编译器可以推算出初始化需要几行。
其实可以把二维数组看为多个一维数组(看成一维数组的数组),你必须要指出这些的一维数组的大小(不然就不知道要分成几个数组,如果把全部数据都放到一个数组中,那就变成了一维数组,那就乱套了),可以不指出一维数组的数量,对应来看就是列数是必须的,而行数不是必须的。
在C语言中,当你在二维数组中初始化时指定列数是必须的,而行数是可选的,这是因为C语言的设计使得它在内存中按行顺序存储多维数组。
考虑以下情况:
int array[][3] =
{{1, 2, 3},{4, 5, 6},{7, 8, 9}
};
在这个例子中,我们初始化了一个二维数组,但没有指定行数,只指定了列数。编译器可以从初始化器中看到每一行有三个元素,因此它知道每个子数组的大小。由于C是按行优先顺序存储数组的,编译器只需要知道每一行的大小就能计算整个数组需要多少内存,并正确地将元素放置在内存中。
然而,如果我们没有指定列数,如下所示:
int array[][] =
{{1, 2, 3},{4, 5, 6},{7, 8, 9}
};
这个声明是非法的,因为编译器不知道每个子数组应该有多大。这会引起问题,例如在内存中分配空间时不知道该分配多少,以及在通过索引访问元素时不知道每一行的偏移量应该是多少。
因此,为了让编译器能够确定数组在内存中的布局,必须至少知道除了最外层之外的所有维度的大小。这允许编译器在内存中正确地布局数组,以及在需要时能够计算出到达任何指定元素需要跳过多少字节。这对于多维数组同样适用,当然,二维数组就是多维数组的最简单的情况。
而对于最外层的维度,即行数,它可以从初始化数据中推断出来,因为你已经提供了完整的初始化列表,编译器可以计算出有多少行。在上面的示例中,编译器会看到有三组花括号,意味着有三行。
五、二维数组的使用
1、二维数组的访问
二维数组的访问与访问一维数组相似,访问二维数组元素需要两个索引:
int value = num[2][2]; // 访问第3行第3列的元素
2、二维数组的打印
这样使二维数组更可视化。
打印二维数组是我们可以用两个变量,一个控制行一个控制列,使用嵌套循环。
#include <stdio.h>int main()
{int num[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };int i = 0, j = 0;for (i = 0; i < 3; i++){for (j = 0; j < 4; j++){printf("%d ",num[i][j]);}printf("\n");}return 0;
}
3、二维数组的存储
#include <stdio.h>int main()
{int num[3][4] = { {1,2,3,4},{2, 3, 4, 5},{3, 4, 5, 6} };int i = 0, j = 0;for (i = 0; i < 3; i++){for (j = 0; j < 4; j++){printf("num[%d][%d]的地址是 %p\n",i,j,&num[i][j]);}}return 0;
}
我们发现每个元素的地址的大小相差4,所以二维数组也是连续的,是一行连一行的。
实际上应该是这样的:
六、更高维度的数组
对于更高维度的数组,声明和使用的方式类似,只是会有更多的索引。
可以将二维数组看作一维数组的数组,每一行就是二维数组的子数组,三维数组看作二维数组的数组,以此类推。
1、三维数组的声明
声明三维数组:
type arrayName[xSize][ySize][zSize];
例如,声明一个2x3x4的三维整数数组:
int threeDArray[2][3][4];
2、三维数组的初始化
初始化三维数组:
int threeDArray[2][3][4] =
{{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}},{{13, 14, 15, 16},{17, 18, 19, 20},{21, 22, 23, 24}}
};
3、三维数组的访问
访问三维数组元素
int value = threeDArray[0][1][2]; // 访问第一个2D数组,第2行第3列的元素
在使用多维数组时,务必注意数组的边界。在C语言中,数组索引是从0开始的,所以最后一个元素的索引总是比数组的长度小1。
还要注意的是,C语言中没有数组边界检查,所以如果你尝试访问数组外的元素,可能会导致未定义的行为,包括程序崩溃。
多维数组通常在内存中是连续存储的,对于二维数组,第一维通常对应行,第二维对应列。在内存中,这些数据是行优先存储的,也就是说,数组的第一行的元素在前,第二行的元素在后,以此类推。
更高维度的数组与这些相似。
一般使用一维数组和二维数组。
七、数组越界
C语言中的数组越界访问是当一个程序试图访问数组索引之外的内存位置时发生的。C语言本身不会检查数组的边界,因此当你尝试访问超出其声明大小的数组元素时,你可能会读取或写入相邻的内存区域。这可能导致不可预测的行为,包括数据损坏、程序崩溃或安全漏洞。
数组索引是从0开始的,如果数组的大小是n,则数组的最后一个元素的索引是n - 1。
如果访问小于0的索引对应的元素,或大于n - 1的索引对应的元素就会越界,这就是越界访问。
下面演示了一个数组越界访问的情况:
#include <stdio.h>int main()
{int num[5] = {1,2,3,4,5};for (int i = 0; i < 10; i++){printf("%d\n",num[i]);}return 0;
}
这里的数组num的大小是5,而循环打印会打印到第十个元素,及下标为9的元素,超过数组范围的值是不确定的。
为了避免数组越界,你应该总是确保你的索引在数组的有效范围内。下面是一些常见的做法来避免数组越界错误:
- 使用常量或宏定义数组的大小。
- 使用循环时,确保循环计数器不会超过数组的边界。
- 在对数组进行索引之前,检查索引值是否在有效范围内。
八、数组作为函数参数
如果想要在函数中传递一个一维数组作为参数,必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,也可以传递一个多维数组作为形式参数。
(1)数组名加[]
void BubbleSort(int num[], int length)
(2)指针
void BubbleSort(int *num, int length)
(3)数组名加[size]
void BubbleSort(int num[size], int length)
冒泡排序:
#include <stdio.h>void BubbleSort(int num[], int length)
{for (int step = 0; step < length - 1; step++){for (int i = 0; i < length - step - 1; i++){if (num[i] > num[i + 1]){int temp = num[i];num[i] = num[i + 1];num[i + 1] = temp;}}}
}int main()
{int num[10] = { 1,23,2,45,22,19,23,45,98,37 };int length = sizeof(num) / sizeof(int);BubbleSort(num, length);for (int j = 0; j < 10; j++){printf("%d ",num[j]);}return 0;
}
这里传参的形参是数组名加[]。
九、数组名
1、正常情况
在C语言中,数组名通常代表数组首元素的地址
普通使用下的数组名:当数组名用在大多数表达式中时,它会被编译器解释为数组首元素的地址。例如,如果有一个数组 int arr[10],那么表达式 arr 就会被转换为指向 arr[0] 的指针。
数组名作为函数参数:当数组名作为参数传递给一个函数时,它实际上是传递了指向数组首元素的指针,而不是整个数组的拷贝。这意味着在函数内部,数组名代表的是首元素的地址。
2、两个特殊情况
(1)sizeof 运算符:
sizeof(数组名)
当数组名与 sizeof 运算符一起使用时,它不是首元素的地址。例如,sizeof(arr) 会返回整个数组的大小,而不是数组首元素的大小。
(2)& 运算符:
&数组名
&arr 会给出整个数组的地址,其类型是指向整个数组的指针,而不仅仅是首元素的地址。这里的整个数组的地址虽然与数组首元素地址数值上相等,但是它们的类型不同。
如果 int arr[10],那么
printf("%p\n", arr);
和
printf("%p\n",&arr);
输出的值看起来会是一样的,不过它们的类型是不同的:
&arr 得到的是整个数组的地址,它的类型是指向整个数组的指针,即 int (*)[10]。
arr 会退化成指向数组首元素的指针,它的类型是 int*。
在内存中,数组的起始地址与它的第一个元素的地址确实是相同的位置。但从类型系统的角度来看,&arr 和 arr 的类型是不同的,这在一些特定的情况下(比如传递给需要类型精确匹配的函数参数时)会变得重要。
3、arr和&arr[0]和&arr
#include <stdio.h>int main()
{int arr[10] = {0};printf("%p\n", arr);printf("------------------\n");printf("%p\n", &arr[0]);printf("------------------\n");printf("%p\n", &arr);return 0;
}
运行结果:
我们发现数值是一样的,但其实类型是不一样的,下面我们开始证明:
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
我们知道这样操作会打印两个地址,加一是跳到下一个元素,所以运行结果是:
C转化为十进制为12,所以两个十六进制数字差值为4,恰好是一个整型的大小,刚好是整型数组,数组的一个元素的大小是4,所以跳了一个元素,这里的元素是一个数组的元素,所以这里的&arr[0]是数组的首元素的地址。
#include <stdio.h>int main()
{int arr[10] = {0,1,2,3,4,5,6,7,8,9};printf("%p\n", arr);printf("%p\n", arr + 1);printf("------------------\n");printf("%p\n", &arr[0]);printf("%p\n", &arr[0] + 1);printf("------------------\n");printf("%p\n", &arr);printf("%p\n", &arr + 1);return 0;
}
运行结果:
我们发现,这里的arr和&arr[0]都是跳了一个数组的元素,正好是4字节,所以他们两个是一样的,都是表示数组的首元素地址。
而&arr跳的不是4个字节,而是40十个字节,实际上是跳了一个数组,这个数组是十个元素,有是整型数组,正好是40个字节,有此可说明&arr的类型是int(*)[10],它是一个指向整个数组的指针。
十、高维数组数组名
1、正常情况
二维数组的数组名一般也是作为首元素的地址,但与一维数组不同的是,二维数组的首元素是它的子数组,及二维数组的首元素也是数。更高维数组的数组名仍然可以看作是指向数组首元素的指针。但对于高维数组来说,首元素本身还是一个数组。所以,对于高维数组,数组名表示的是指向第一个子数组的指针。
所以高维数组与一维数组不同的就是(这里用二维数组举例):
int arr[3][4] = {0};
arr;
这里的arr是高维数组的首元素地址,但首元素地址指向的是一个子数组(这里可以看作高维数组是数组的数组)。
#include <stdio.h>int main()
{int arr[3][4] = {0};printf("%p\n", arr);printf("%p\n", arr + 1);printf("------------------\n");printf("%p\n", &arr[0]);printf("%p\n", &arr[0] + 1);return 0;
}
运行结果:
这里的arr和&arr[0]是高维数组的首元素地址,但首元素地址指向的是一个子数组。所以会跳一个子数组,这里一个子数组是四个整型,所以跳了16个字节。
2、两种特殊情况
一维数组的理论对于一维数组是正确的,对于二维及其他高维数组也是适用的,高维数组的数组名也有这两个特殊情况。
(1)sizeof(数组名)会输出整个数组的大小
int arr[3][4] = {0};
int sz = sizeof(arr);
printf("%d\n", sz);
运行结果:
(2)&arr是指向整个数组的指针
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
运行结果:
两个地址相差了48个字节,正好跳了一个数组。
3、二维数组的行列
#include <stdio.h>int main()
{int arr[3][4] = {0};int row = sizeof(arr) / sizeof(arr[0]);//总大小除以行的大小,得到有几行int column = sizeof(arr[0]) / sizeof(arr[0][0]);//一行的大小除以单个元素的大小,得到一行有几个,即有几列printf("%d %d",row,column);return 0;
}
求二维数组的行数就是求二维数组的二维的尺寸,求二维数组的列数就是求二维数组的一维的尺寸。这就是通过sizeof求高维数组的每一维的尺寸。
4、高维数组的每一维的尺寸
对于更高维度的数组,可以用类似的方式计算每个维度的尺寸,只要是针对静态分配的数组进行操作。例如,对于一个三维数组int arr[2][3][4]可以按以下方式计算各个维度的尺寸:
#include <stdio.h>int main()
{int arr3D[2][3][4] = {0};int depth = sizeof(arr3D) / sizeof(arr3D[0]); // 总大小除以第一层的大小,得到深度int row = sizeof(arr3D[0]) / sizeof(arr3D[0][0]); // 第一层大小除以第一行的大小,得到行数int column = sizeof(arr3D[0][0]) / sizeof(arr3D[0][0][0]); // 第一行大小除以单个元素的大小,得到列数return 0;
}
请注意,这种方法只适用于数组的尺寸在编译时是已知的情况。对于动态分配的数组或者通过指针传递到函数的数组,您无法使用 sizeof 来获取它们的尺寸,因为 sizeof 对指针来说只能得到指针本身的大小,而不是它指向的数据的大小。