目录
一.前言
相信老铁们在高中数学中,没少接触到数列。因其优美简洁的形式和灵活多变的做法,让庄哥至今难以遗忘(好像高中数学也只记得这个了qwq)。而在我们的编程中,则有着由多个相同类型元素组成的数组。今天这篇博客,庄哥就带老铁们了解数组在编程领域中如何发挥着独特的作用。
二.一维数组的创建格式与初始化
2.1一维数组的创建格式
数组的定义:数组是一组相同类型元素的集合
创建格式:type_t arr_name [const_n]
type_t为元素创建类型
arr_name为数组的名称
[const_n]是常量表达式,用来指定数组大小(指元素的个数)
例如:
int arr[5];//创建一个大小为5的整形数组
数组创建的四种类型:
- 给定一个数组名称,并指定它的大小:int arr[10]
- 用const赋予变量a常属性,使得a能够指定数组的大小:
int const a=10;
int arr[a]- 在[ ]中使用常量表达式指定数组的大小:int arr[5+6]
- 空数组,不指定数组的大小:int arr[ ]
温馨提示:在C99标准之前,[ ]中只给使用常量,不能使用变量。但在C99标准中,引入变长数组,即[ ]中可以使用变量,这时不可以对数组进行初始化。 而我们现在常用的VS编译器是不支持变长数组的。
2.2一维数组的初始化
创建好数组后,我们就需要对数组部内部进行初始化(可以理解为赋值)啦。在C语言中,数组属于构造数据类型。一个数组又可以拆分为多个数组元素,这些元素可以是基本数据类型或是构造类型。因此,根据数据类型的不同,数组又可以分为数值数组、字符数组、指针数组、结构数组等各种类别。(因为本文章是针对初学者而言,所以庄哥在这里只围绕数值数组与字符数组展开讲解)
温馨提示:数组的初始化是在编译时完成的
接下来依次列举数组初始化的7种情况:
//完全初始化
1、int arr1[5]={1,2,3,4,5};
2、int arr2[10]={0};
3、char arr3[5]="abcd\0";
4、char arr4[]={'a','b','c',65,'\0'};
//不完全初始化
5、int arr5[5]={1,2,3};
6、char arr6[5]={'a','b','c'};
7、char arr7[5]="abcd";
下面庄哥依次讲解这7种情况:
2.2.1完全初始化
- 类型:整形数组
- 大小:5个元素
- 元素:存放了五个整形元素1,2,3,4,5
- 格式:int arr1[5]={1,2,3,4,5}
非常标准的初始化
- 类型:整形数组
- 大小:10个元素
- 元素:全是整形元素0
有些老铁看到这里或许会想:既然数组元素全为0,那不对它进行初始化数组元素会不会是0呢?
答案是:不会,若不对数组进行初始化,那么编译器会给它的所有元素随机初始化为一个值,如下图所示
因此,在编写代码的时候各位老铁一定一定要记得初始化数组哦~
- 类型:字符串(属于类,先不要关心它是什么,只需要知道它能以数组的形式访问,后期会介绍的哈)
- 大小:5个元素
- 元素:a、b、c、d、‘ \0 ’
- 格式:char arr3[5]=“abcd\0”
这里一些老铁会有疑问,为什么字符串的结尾庄哥要加上转义字符‘ \0 ’ ,它在这里有什么特殊的作用吗?
请看下面这段代码:
#include<stdio.h>
int main()
{
char arr[4]="abcd";
int i=0;
while(arr[i]!='\0')//在循环次数不超过元素个数的情况下,用for循环打印就可以不需要'\0',感兴趣的老铁可以试一下哦~
{
printf("%c",arr[i]);
i++;
}
}
其实呀,‘ \0 ’作为字符串结束的标识符,若像上述代码不使用,在打印字符串的时候就会出现如下图的问题
打印abcd后出现了一串“烫烫烫…”。因为编译器没有遇到‘ \0 ’,这时就会以为字符串里的内容并没有结束,而abcd后的空间并未进行初始化,于是打印出“烫烫烫…”,理论上将一直打印下去,这里庄哥为了让老铁们好观察,还是让它停下来吧!
- 类型:字符数组
- 大小:5个元素
- 元素:a、b、c、A、‘ \0 ’
- 格式:char arr4[5]={‘a’,‘b’,‘c’,‘d’,‘\0’}
对于这种情况老铁们需要注意三点:
(1).当我们未指定数组的大小时,初始化往里面放进多少个元素,数组的大小就为多少。
(2).虽然第四个元素初始化填入65,但是该数组为字符数组,编译器会通过ASCII码表进行翻译,将对应的字符‘ A ’存入第四个元素中。
(3).字符数组与字符串同样要在结尾加上结束标识符‘ \0 ’
2.2.2不完全初始化
- 类型:整型数组
- 大小:5个元素
- 元素:1,2,3,0,0
初始化时,当我们放入的元素个数小于指定的数组大小,编译器会将剩下的元素自动初始化为0。
- 类型:字符数组
- 大小:5个元素
- 元素:a、b、c、‘ \0 ’、‘ \0 ’
在字符数组中,若进行不完全初始化,编译器则会将剩下的元素自动初始化为转义字符‘ \0 ’
- 类型:字符串
- 大小:5个元素
- 元素:a、b、c、d、‘ \0 ’
通过上图,可以看到字符串在进行不完全初始化时,编译器也会自动将剩下元素初始化为‘ \0 ’。因此,我们在指定字符串大小的时候,可以多留一个空位,让编译器自动将空位初始化为‘ \0 ’。
三.一维数组的使用
那么创建好数组后,我们又该如何调用其中的元素呢?
这就需要用到下标引用操作符‘ [ ] ’,又称数组访问操作符。
格式:数组名 + [ 下标编号 ] 例如:arr[0]
注意区分:
(1). 在创建数组的时候,[ ]中存入的是数组的大小,这时[ ]不是操作符,而是所需要的语法格式。
(2). 在使用数组的时候,[ ]中存入的是数组元素的下标重点:数组元素的下标是从0开始
我们试着打印出数组元素:
#include<stdio.h>
int main()
{
int arr[5]={1,2,3,4,5};
int i;
for(i=0;i<5;i++)//思考:i为什么不小于6呢?
{
printf("%d",arr[i]);//通过下标引用操作符访问数组的元素
}
return 0;
}
我们也可以通过监视窗口直接观察到数组的每一个元素及其下标
冷知识:在使用数组时,格式也可以写成 i[arr]
老铁们现在只需要记住[ ]只是个操作符,正如5+3可以写成3+5一样,其本质原因庄哥会在后期指针的章节提及,这里先留个印象。
四.一维数组在内存中的存储
接下来我们再深层次探讨数组的本质
数组在内存内存中是连续存放的
每一个方框代表着一个数组元素,随着下标的增长,元素的存储地址由低到高。当元素类型为整形的时候,一个元素则需要四个字节来存储,即一个方框对应着四个地址;当元素类型为字符的时候,一个元素则需要一个字节来存储,即一个方框对应着一个地址。
下面通过打印出数组元素的地址加深理解
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i;
for (i = 0; i < 10; i++)
{
printf("%p\n", &arr[i]);//地址的打印格式为‘%p’
}
return 0;
}
从运行结果可以以看出,每个地址相差了4个字节,正好为一个整形元素。由此可以得出:数组在内存内存中是连续存放的。
通过下图更加直观的看到数组在内存中的存储形式。
注:上图每一个方框代表一个地址,即一个字节。
五.二维数组的创建格式与初始化
上面讲完一维数组,下面我们就继续来探讨二维数组。
5.1二维数组的创建格式
格式:type_t arr_name [const_n][const_n]
举例:int arr[5][5]
区别:比一维数组多一个[ ]
含义:第一个[ ]代表着行,第二个[ ]代表着列。比如上述的例子中,就是一个5行5列的二维数组。
其它创建格式:int arr[ ][5],二维数组的创建只能省略掉行,但不能省略掉列
老铁们可以将二维数组想象成一个棋盘帮助理解,或类比线性代数中的矩阵。
5.2二维数组的初始化
与一维数组一样,二维数组也只针对字符数组和整型数组展开讲解
二维数组初始化的大致分为以下4种情况
//完全初始化
1.int arr1[2][3]={{1,2,3},{4,5,6}};
2.int arr2[2][3]={1,2,3,4,5,6};
3.char arr3[2][3]={'a','b','c','d','e','f'};
//不完全初始化
4.char arr4[2][3]={"abc","d"};
- 大小:6个元素
- 元素:第一行:1,2,3;第二行:4,5,6
- 格式:外面的{ }表示有多少行,里面的{ }表示有多少列
二维数组中,行与列的下标也是从0开始
其实也可以理解为,二维数组就是由多个相邻的一维数组组合而成的。
- 大小:6个元素
- 元素:第一行:1,2,3;第二行:4,5,6
虽然比上一种初始化方式少了表示列的{ },但是运行结果与前者相同,说明表示列的{ }可以省略。在行数较少时,庄哥并不建议这样做,因为加上{ }明显更加直观,更有利于我们检查。但行数较多时那就另一回事了。
- 大小:6个元素
- 元素:第一行:a,b,c;第二行:d,e,f
格式:int arr3[2][3]={‘a’,‘b’,‘c’,‘d’,‘e’,‘f’};
通过监视窗口可以看到,二维字符数组的初始化也分为行与列。但在这里庄哥想强调的是,‘ \0 ’仅仅只是作为字符串结束的标识符,那么字符串和字符数组中一定要包含吗?
其实不一定,请看下面这段代码:
#include<stdio.h>
int main()
{
char arr3[2][3] = { 'a','b','c','d','e','f'};
int i,j;
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%c", arr3[i][j]);
}
}
}
就算我不加‘ \0 ’,也不影响我打印字符数组。所以,‘ \0 ’仅仅只是作为一种标识符而已,编译器遇到’ \0 ‘只会停止对它打印,而不会停止对后续内容的打印。
例如我在字符数组中插入‘ \0 ’:
#include<stdio.h>
int main()
{
char arr3[2][3] = { 'a','\0','c','d','\0','f'};
int i,j;
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%c", arr3[i][j]);
}
}
}
由运行结果可以看出,编译器跳过对‘\0’的打印,但不影响对后续内容的打印。不过,因为其中‘\0’将ac与df隔开,所以这里ac与df是两个字符串。
- 大小:6个元素
- 元素:第一行:a、b、c;第二行:d、\0、\0
- 格式:char arr4[2][3]={“abc”,“d”};
字符串同样可以通过二维数组来表示,而对于未初始化的部分,编译器自动初始化为‘\0’。对于整形数组未初始化部分,编译器则会初始化为0,这里就不再过多说明了。
六.二维数组的使用
二维数组的使用也是通过下标的方式
使用方法与一维数组类似,我们直接尝试打印二维数组:
#include<stdio.h>
int main()
{
int arr[2][3] = { 1,2,3,4,5,6 };
int i,j;
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d", arr[i][j]);
}
printf("\n");//打印完一行后,进行换行
}
}
运行结果:
注意:二维数组中,行与列的下标同样是从0开始
6.1下标为什么从0开始
看到这里,老铁们会不会很疑惑为什么数组的下标要从0开始呢,从1开始不是更符合我们的常规思想吗?
其实不然,当我们创建数组的时候,编译器会根据以下的寻址公式在内存中开辟空间
a[i]_address=base_address+i*data_type_size
当下标从1开始时,公式就变成这样
a[i]_address=base_address+(i-1)*data_type_size
可以看到,这时编译器再寻找该数组元素的地址时,都要-1。或许在现在看来,这小小的-1是多么的渺小。但在工程里,要创建的数组有很多个,这时每次寻址都要-1,将会减慢我们程序的运行速度并且占用内存。对于精益求精的前辈们来说,这是绝对不允许的,因此我们的下标就从0开始。
七.二维数组在内存中的存储
像一维数组一样,我们尝试打印二维数组每一个元素的地址
#include <stdio.h>
int main()
{
int arr[2][3],i,j;
for(i=0;i<2;i++)
{
for(j=0;j<3;j++)
{
printf("%p",&arr[i][j]);
}
}
return 0;
}
可以看出,即使二维数组在形式上与一维数组有所不同,但是元素的地址也是由低地址向高地址排列,且每个地址之间相差4个字节。
所以,前面介绍二维数组初始化时,庄哥说二维数组也可以理解为多个相邻的一维数组组成,请看下面图解。
八.关于数组越界问题
- 越界原因:数组的下标是有范围限制的,如果在使用含有n个元素的数组时,下标小于0或大于n-1,就属于数组越界访问了
- 温馨提示:在C语言中是不做数组下标越界检查,编译器也不一定报错,但是代码本身肯定是有问题的。所以,各位老铁在使用数组时,一定要谨慎,防止数组越界访问。
其实在前面介绍一维数组初始化的时候,就已经涉及数组越界的问题了。
不知老铁是否还记得一维字符数组初始化时,出现“烫烫烫…”的原因(忘记的老铁可以返回去看一下代码哈)。前面庄哥说的原因是:没有出现字符串结束标识符‘ \0 ’,告诉编译器停止打印。但这里庄哥告诉你们更加本质的原因:超出数组合法空间的访问。
下面请看代码:
#include<stdio.h>
int main()
{
char arr[4]={1,2,3,4};
int i=0;
for(i=0;i<4;i++)
{
printf("%c",arr[i]);
}
}
调试代码后,我们调出内存窗口。可以看到,那些在数组后面未初始化的空间中存入的都是"cc cc cc cc…“这样的十六进制数字。而在数组越界访问时,系统通常读取一次内存空间,就会向后访问两个字节,即为"cc cc”,而这个十六进制数字所对应的ASCII码表上的“ 烫 ”。因此编译器在未遇到’ \0 '时,越界打印的字符就是“ 烫 ”。偶尔会剩一个字节,即为"cc",它对应的字符是“ ?”,所以有时候在末端会冒个问号。
九.数组作为函数参数
那么说了这么久的数组,数组名在C语言中到底代表着什么含义呢?
我们通过下面这道题来进行探讨:
用冒泡排序法对一个数组进行从小到大排序
首先,我们得知道冒泡排序法的原理:冒泡排序的意思就是对一个无序数组进行排序,依次比较两个相邻数之间的较大值,较大值的下标如果小于较小值,那么就将它们两个的位置进行交换,直到数组呈现随着下标的增大,元素的值也不断增大的结果。
看完流程图后,庄哥直接上代码(老铁们不急着自己敲哦,可以先复制到编译器上运行一下~)
#include<stdio.h>
void bubble_sort(int arr[])
{
int i,j,tmp,sz;
sz = sizeof(arr) / sizeof(arr[0]);//不懂这一步的老铁先记住:sizeof是一种计算字节大小的单目运算符,通过此种方法可以计算出数组含有的元素个数,本文后面会讲到
for (i = 0; i <sz-1; i++)
{
for (j = 0; j < sz - i-1; j++)
{
if (arr[j] > arr[j + 1])
{
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1 },k;
bubble_sort(arr);
for (k = 0; k < 10; k++)
{
printf("%d ", arr[k]);
}
return 0;
}
运行后,老铁们会发现没有元素进行交换
这到底是怎么回事呢?
我们赶紧来调试一下:
发现sz的值怎么是1呀,不应该是数组的元素个数吗,这时我们猜测会不会arr里存放东西的字节数与arr[0]里存放的东西字节数相等?
温馨提示:这是X86的编译环境,X64的运行结果会跟上述不一样
于是我们赶紧打开监视窗口,看看arr里面究竟放着什么呢?
通过上图可以看到,arr的值为0x0057fb9c,这不就是一个地址吗。竟然是地址的话,那这个地址又是谁的呢?
庄哥又将所有元素的地址取出:
发现数组名中存放的地址与首元素的地址相同,于是我们得出结论
数组名就是首元素地址
但是有两个例外:
(1).sizeof(数组名),可以计算整个数组的字节大小,此时的数组名代表整个数组
(2).&数组名,取出的是数组的地址,此时数组名也代表整个数组
这时敏锐的老铁就疑惑了,既然sizeof中的数组名代表的是整个数组,那为什么在刚刚的冒泡排序法中数组名只有4个字节呢,不应该是40个字节吗?
其实呀,在数组传参的时候,数组名只是作为首元素的地址传给了目标函数
所以,即使在函数形参部分写成数组的形式: int arr[ ] 表示的依然是一个指针,而不是数组。
既然如此,那么我们就应该在传参前就计算好数组的元素个数赋给一个变量,让这个变量将数组元素个数传给函数。
#include<stdio.h>
void bubble_sort(int arr[],int sz)
{
int i,j,tmp;
for (i = 0; i <sz-1; i++)
{
for (j = 0; j < sz - i-1; j++)
{
if (arr[j] > arr[j + 1])
{
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1 },k,sz;
sz=sizeof(arr)/sizeof(arr[0]);//在传参前就计算数组元素的个数
bubble_sort(arr,sz);
for (k = 0; k < 10; k++)
{
printf("%d ", arr[k]);
}
return 0;
}
运行后,效果如下:
可以看到,我们的数组成功进行冒泡排序,将所有元素从小到大进行排序
十.总结
至此,数组基础部分庄哥就全部介绍完毕。本文中,部分内容关乎到算法与计算机底层逻辑,内容多且难,庄哥也是反复琢磨如何让老铁们能够一看就懂。但庄哥也希望老铁们能够静下心来,不清楚的地方要反复阅读,好好感受数组在C语言的奇妙作用。
下面附上庄哥的写作思路:
由于本人的专业水平有限,文章有需要改进的地方欢迎老铁们在评论区批评、指正,庄哥感激不尽!