C 数组
由于数组有存储多个相同类型值的能力,我们能够做更多有趣的事情。首先我们要了解数组,然后再聊一聊数组可以做什么。
数组的声明和初始化
数组类型形式如右侧:数据类型 数组名[数组大小]={值1,值2,值3,...};
比如下面代码,声明了一些数组变量:
#include <stdio.h>
int main(void) {
//声明了一个长度为3的整型数组
int arr[3];
//声明了一个长度为3的整型数组,并使用3、4、5给数组进行了初始化
int arr3[3] = {3, 4, 5};
//声明了一个长度为3的整型数组 并且使用了1、2对数组进行了不完全初始化
int arr2[3] = {1, 2};
//声明了一个长度为6的字符数组 并且使用a、b、c三个字符对数组进行了不完全初始化
char str[6] = {'a', 'b', 'c'};
//不指定数组长度,由系统初始化列表自动推断长度
char str2[] = {'a', 'b', 'c'};
char str3[];
str2={'c','b','d'};
return 0;
}
对于数组的声明和初始化有以下几点总结:
- 函数内只声明了数组arr,未赋初值;这种数组不会自动初始化,所以,数组中的元素的值是随机的,不确定的;
- 对于arr3,这是一种在声明时就将数组初始化的形式;右侧花括号中的参数列表,有三个逗号分隔开的值,这是一种完全初始化;
- 对于arr2,这是一种不完全初始化形式;剩余的最后一个元素会被默认初始化为0;
- 对于str,这是一个字符数组,它也没有被完全初始化;
- 对于str2,这是一种不指定数组大小的形式,系统会根据初始化列表推断出数组的大小;
- 对于str3,这种形式不被允许,编译器会报错--如果在声明时不指定数组大小,那么一定要在数组声明时就初始化;
- str2={1,2,3},这种编译器会报错,因为数组名就像是一栋楼,比如6号楼,它永远就是6号楼,不允许再改变--即:不允许再对数组变量再进行赋值操作;
- 其实数组名是一个地址类型常量--指针常量。不允许再次对数组地址进行修改。
如何通过声明认识数组
一个小技巧,拿int arr[10];来讲:先看数组名arr,然后往右和arr紧贴着的一对方括号,得知这是一个含有10个元素的数组、接下来将arr[10]忽略掉,再看剩下的部分就是数组元素的类型。好了,有了这个小技巧之后,我们来认识一下这个数组?
int *arr[6];
int arr2[6][2];
先来看arr
- 第一步:arr右边紧挨着的方括号,得知arr是一个含有6个元素的数组;
- 第二步:去掉arr和紧挨着的方括号,剩下 int * --数组元素为:整型指针;
- 最后一步:arr是一个由6个整型指针组成的数组
再来看arr2:
- 第一步:arr2右边紧挨着的方括号,得知arr2是一个含有6个元素的数组;
- 第二步:去掉arr2和紧挨着的方括号,剩下 int [2] -- 数组元素为:数组类型;
- 最后一步:arr2是一个由6个数组组成的数组。
数组名意味着什么
数组名在表达式中退化为指针
数组名存储了整个数组在内存中的首地址;由于数组元素在内存中是连续存储的,所以数组首元素的地址和数组的首地址是相同的;
由于使用数组名在表达式中,数组名会“退化”成指针
比如:可以使用数组名给指针变量赋值int arr[10]; int * p_arr=arr;
比如:可以使用数组名做指针运算int arr_item=*(arr+1);
但是你不要认为数组名就是指针类型!!!因为数组名有自己的类型,即数组类型!!!
#include <stdio.h>
int main(void) {
int arr3[3] = {3, 4, 5};
printf("arr3的地址是:%p\n", &arr3);
printf("arr3的地址是:%p\n", arr3);
printf("arr3的第一个元素的地址是:%p\n", &arr3[0]);
return 0;
}
输出
arr3的地址是:0x16b2b7528
arr3的地址是:0x16b2b7528
arr3的第一个元素的地址是:0x16b2b7528
使用下标访问数组元素
声明并初始化一个数组后,如何访问或修改数组某个位置的元素呢?答案是使用数组名+元素下标位置;
当我们声明一个整型数组后,它的各个元素所处的下标如下图:
#include <stdio.h>
#include <string.h>
#define NUMBER 3
int main(void) {
int arr3[NUMBER] = {3, 4, 5};
for (int i = 0; i < NUMBER; i++) {
printf("第%d个元素的值:%d\n", i + 1, arr3[i]);
}
return 0;
}

输出:
第1个元素的值:3
第2个元素的值:4
第3个元素的值:5
使用下标还可以修改此位置处的元素的值,这里不再演示。
使用指针对数组进行遍历
#include <stdio.h>
#define NUMBER 3
int main(void) {
int arr3[NUMBER] = {3, 4, 5};
int *arr3_ptr = arr3;
for (int i = 0; i < NUMBER; i++) {
printf("第%d个元素的值:%d\n", i + 1, *(arr3_ptr + i));
}
return 0;
}
由于知道数组名存储的值与数组首元素的地址相同,所以int *arr3_ptr = arr3;用数组名赋值给int类型指针逻辑上是合理的。
在for循环中,arr3_ptr+1操作,就是数组下一个元素所在的地址。而后通过解引用的操作,便可获取到下一个元素的值。
在sizeof和&取地址符号中的表现
- 在sizeof面前,数组名又充当起了代表整个数组的角色,所以sizeof(数组名)得到的大小是整个数组的大小;而非其它普通指针,得到的是一个固定大小;
#include <stdio.h>
#define NUMBER 3
int main(void) {
int arr3[NUMBER] = {3, 4, 5};
printf("arr3大小是:%lu\n", sizeof(arr3));
int *arr3_ptr = arr3;
printf("arr3_ptr大小是:%lu\n", sizeof(arr3_ptr));
return 0;
}
输出:
arr3大小是:12
arr3_ptr大小是:8
- 在取地址&符号前,数组名也充当起了数组的角色,所在当&数组名+1后得到的地址并不是简单的往后移动一个数组元素的空间大小,而是数组长度*单个数组元素字节 大小;
这两个点比较特殊,初学者需要特别关注一下这两个点。
数组元素在内存中的分布
从编辑器的内存视图来看,&数组名,得到的数组内存分布:

由上图可以看到数组元素在内存中是连续存储的;每个数组元素占用4字节。
&数组名+1

做一些有趣的事
上面说了这么多干的,也得来点儿湿的好消化,那么知道了数组的这些基础知识后,我们已经可以做一些有趣的东西了.
在开始前,先要讲一个写代码最重要的前提:一定要想好了,再去干!在动手完成功能前,先在脑子里和纸上写写画画,把程序的功能点、思路理好理完整;千万、千万、千万不要听到需求没想好就开干!!!
后面的小练习,都是秉持着先有思路,再动手的原则进行的。
找最大值
思路:认为第一个元素就是最大值;搞一个临时变量存储第一个变量的值;对数组开始遍历,在当前迭代中若发现比这个临时变量还要大的元素,则拿此元素的值更新这个临时变量;下一次迭代仍然是这个逻辑;直至数组遍历结束;那么临时变量存储的就已经是数组的最大值了。
#include <stdio.h>
#define NUMBER 13
int main(void) {
int arr3[NUMBER] = {3, 4, 5, 1, 2, 19, 2, 5, 7, 8, 10, 9, 12};
int max = arr3[0];
for (int i = 0; i < NUMBER; i++) {
if (max < arr3[i]) {
max = arr3[i];
}
}
printf("数组最大值是:%d\n", max);
return 0;
}
输出
数组最大值是:19
数组逆置
思路:数组的大小能先确定、然后定义两个临时变量x、y分别存储数组的最小下标和最大下标;对数组开始遍历,在当前迭代中交换两个下标处元素的数据,然后让x值加1、让y值减1;下一次迭代仍然是这个逻辑;但是需要注意的是:x和y不能“碰面”--即x一定要小与y。
#include <stdio.h>
#define NUMBER 13
int main(void) {
int arr3[NUMBER] = {3, 4, 5, 1, 2, 19, 2, 5, 7, 8, 10, 9, 12};
for (int i = 0; i < NUMBER; i++) {
printf("%d ", arr3[i]);
}
printf("\n");
int x = 0;
int y = NUMBER - 1;
for (int i = 0; i < NUMBER; i++) {
if (x < y) {
int tmp = arr3[x];
arr3[x] = arr3[y];
arr3[y] = tmp;
x++;
y--;
}
}
for (int i = 0; i < NUMBER; i++) {
printf("%d ", arr3[i]);
}
return 0;
}
输出
3 4 5 1 2 19 2 5 7 8 10 9 12
12 9 10 8 7 5 2 19 2 1 5 4 3
特殊的数组之二维数组
上面我们学习认识数组声明小技巧时,提到过二维数组int arr[6][2];它是有数组组成的数组--集:数组的数组,称为二维数组。
二维数组的初始化形式有:
#include <stdio.h>
#include <string.h>
int main(void) {
int arr[6][2] = {
{4, 5},
{3, 4},
{2, 3},
{1, 2}
};
return 0;
}
把二维数组看成由6个一维数组组成的数组的角度来声明的;花括号中的参数列表中,每一个元素都是一个一维数组。
或
#include <stdio.h>
#include <string.h>
int main(void) {
int arr[6][2] = {4, 5, 3, 4, 2, 3, 1, 2};
return 0;
}
由于数组在内存中是连续存储的,所以二维数组的初始化可以使用,一组整型数做参数列表进行初始化。
二维数组在内存中的分布
通过编辑器的内存视图,来看二维数组的内存分布情况:

二维数组总结
- 从数组的数组的角度来看二维数组,那么arr[m][n],我们通常看作:m行、n列;
- 由此而为数组的访问便是,先访问第几行再访问第几列,如
arr[1][2]则访问到了第2行第三列的元素; - 若是要遍历二维数组,则需要两层for循环,第一层负责访问行、第二层负责访问列。
#include <stdio.h>
#include <string.h>
int main(void) {
int arr[4][2] = {
{4, 5},
{3, 4},
{2, 3},
{1, 2}
};
//遍历二维数组
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 2; j++) {
printf("arr[%d][%d] = %d\n", i, j, arr[i][j]);
}
}
return 0;
}
输出
arr[0][0] = 4
arr[0][1] = 5
arr[1][0] = 3
arr[1][1] = 4
arr[2][0] = 2
arr[2][1] = 3
arr[3][0] = 1
arr[3][1] = 2
特殊的数组之字符数组
这里先给结论,
- c程序中没有字符串类型;所以使用char 类型数组存储字符串;
- 字符串字面量如"i love you !",是包含一个看不到的
\0字符的; - 字符串字面量,可以直接给字符数组初始化的;
![image]()
- 字符串一定是字符数组,但字符数组不一定是字符串--如果字符数组中没有\0字符的话,字符数组就不是字符串
这一点,可以使用printf("%s\n",数组名)来看出区别:
#include <stdio.h>
int main(void) {
char arr[] = {'a', 'b', 'c'};
int int_arr[] = {1, 2, 3, 4};
char str[] = "abc";
printf("%s\n", arr);
printf("%s\n", str);
return 0;
}

可以看出,字符数组arr打印出来的带有特殊符号;str打印出来和预期一致;为什么arr打印出来有特殊符号?因为arr存储的就不是字符串,printf函数得到的指令确实打印字符串,所以它会从arr首元素开始找字符串结束符号\0,这个结束符号究竟在什么位置,是位置的;这也是arr打印时会出现特殊符号的由来。
- 现在有一个存储了字符串的字符数组,使用字符串长度计算函数strlen得到的值和使用sizeof得到的值是不一样的
#include <stdio.h>
#include <string.h>
int main(void) {
char arr[] = {'a', 'b', 'c'};
int int_arr[] = {1, 2, 3, 4};
char str[] = "abc";
printf("str的内存占用为:%lu\n", sizeof(str));
printf("str的长度为:%lu\n", strlen(str));
return 0;
}
输出
str的内存占用为:4
str的长度为:3
提一下在网上看到的一位网友讲的:strlen 就像老师让我们读作文时不会把标点符号读出声; sizeof就像老师统计我们作文字数时,倒是会把标点符号统计在内。


浙公网安备 33010602011771号