作者:小树苗渴望变成参天大树
作者:认真写好每一篇博客
作者gitee:link
如果你觉得文章对你有帮助, 就支持关注博主,如有不足还还请指点,博主及时改正
数组
前言
Hello,各位友友们,我们又见面了,大家最近有没有努力学习啊,相信你们一定在学习并且收获了很多知识。今天作者又来讲述新的知识点了,关于数组,不知道数组的希望你通过这篇博客,能更好理解数组,知道数组的大佬也希望能知道我的不足。我将从一维和二维两个部分被带大家去接触数组,并且没有特别说明的情况下以整型数组来讲解。那我i们废话不多说,接入正文!!!
一、一维数组的创建和初始化
1.1数组的创建
数组就是一组相同类型元素的集合
数组的创建方式:
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
我们来举几个例子:
//代码1
int arr1[10];
int arr2[3+7];
//代码2
int count = 10;
int arr3[count];//数组时候可以正常创建?
//代码3
char arr4[10];
float arr5[1];
double arr6[20];
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念。
代码1和代码3创建都是正确的,但代码3在vs编译器是错误的,在gcc编译器上是正确的。
说明VS编译器不支持C99标准,而gcc编译器支持。
1.2数组的初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
初始化的原因是避免在使用的时候出现随机值,就跟变量的初始化一样的道理。
让我们看一些正确的代码:
int arr1[10] = {1,2,3};
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";
arr1里面有10个元素。这是不完全初始化,后面的元素默认为0;
arr2是没有指定元素个数,会根据初始化内容来确定元素个数;
arr3里面有5个元素。这是完全初始化;
arr4是字符数组的完全初始化;
arr5是没有指定元素个数的初始化;
arr6是字符串数组中的没有指定元素个数的初始化,每个位置放一个字符;
来看一下,这6个数组里面倒低放什么元素吧
特别注意arr5和arr6在内存怎么分配的!!!
char arr7[4]=“ab”;"ab"本身里面就有一个\0。这种不完全初始化后面默认的元素是\0.
我们来看一下不初始化带来的后果,我们先不管怎么使用,一会我会讲。如果不初始化,就会像图片中的一样,会出现随机值。所以在使用之前最好初始化一下,避免后期的错误。
1.3一维数组的使用
对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。
我们来看代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};//数组的不完全初始化
//计算数组的元素个数
int sz = sizeof(arr)/sizeof(arr[0]);
//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
int i = 0;//做下标
for(i=0; i<10; i++)//这里写10,好不好?
{
arr[i] = i;
}
//输出数组的内容
for(i=0; i<10; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
解释:arr这个数组名本身是第一个元素的地址,但在sizeof这个操作符中arr代表整个数组地址,单位是字节。
sz=sizeof(arr)/sizeof (arr[0]);表示整个数组的大小除以每一个元素的大小。通过这种方式就可以计算数组里面有多少元素。
我们可以看出来sz的大小是10。
第一个for循环就是给arr数组的每一个元素进行赋值,第二个for循环就是将arr数组打印出来。
- 数组是使用下标来访问的,下标是从0开始。
- 数组的大小可以通过计算得到。
1.4一维数组在内存中的存储
在内存的存储其实就是看他在内存中的地址,想要看到数据在内存的地址就需要使用到&操作符。那我们接下来看数组在内存中的存储。
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
看输出的结果是这样的
补充一个小知识,我在之前的初始C语言中应该也介绍过,一个int类型占4个字节的空间,一个字节对应八个比特位,每个比特位占据一个二进制位。
所以我们看到这每个元素的地址单元是连续存放的,地址是从低到高一直变化的
那我们之前在初始C语言那片博客也介绍到指针相关的知识,我们可以把地址存放在指针变量中,那我们将输出数组内容的代码做一个修改。
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
int* p = &arr[0];//p指针保存第一个元素的地址,通过改变地址,在解引用来找到对应的元素;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们可以看到结果是一样的这样更能说明数组在内存的存储是连续的。
二、二维数组的创建和初始化
2.1二维数组的创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
二维数组的创建其实和一维数组类似我们可以在脑海里把他想象成几行几列的矩阵就可以了。
第一个[]里面代表行下标,第二个[]里面代表列下标。
2.2二维数组的初始化
二维数组的初始化有很多种,我们要学会正确的初始化方法并且要避免错误的初始化。那我们来看看那些是正确的,那些是错误的
//数组初始化
int arr1[3][4] = {1,2,3,4};
int arr2[3][4] = {{1,2},{4,5}};
int arr3[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
这三个都是一个不完全初始化。我们可以把二维数组时这样的存储方式,但在内存中却不是这样的,后面会介绍到。
1.不完全初始化的没有的元素默认为0;每一个{}代码块代表每一行元素,好比arr2这个数组,如果像arr1这样的写只能按照顺序先存完一行再往下一行继续存储,直至元素存储完,不够补0;
2.对于第三个数组,我们发现行下标没有也是可以的,我们可以理解为当由列可以确定一行元素什么时候结束,才到下一行开始存储,这样最夯为多少据默认多少行,这时候我们就有一个错误的初始化。
int arr4[3][]={1,2,3,4};
我们没有列下标的时候,我们可以理解为,往第一行里面放元素的时候,对于没有列下标确定时,我们不知道第一行从什么时候开始结束,所以永远不知道啥时候才往第二行里面存储数据的时候数据的存储完了,所以这是一个错误的初始化;
还有两个错误的初始化:
int arr5[][4]=(1,2,3,4,5);
int arr6[][4]={1,2,,3};
arr5是因为初始化时候把数据放在()里面了而不是{}代码块里面了,arr6是因为不允许有这种情况存在。
2.3二维数组的使用
我们有了一维数组的基础后,二维数组也是通过下标来访问数组的,并且下标从0开始的。
看代码:
#include <stdio.h>
int main()
{
int arr[3][4] = {0};
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)//给每一行赋值
{
arr[i][j] = j;
}
}
//代码1
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)//打印每一行
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
我们可以看到结果如我们想象的打印在屏幕中了。
2.4二维数组在内存中的存储
像一维数组一样,这里我们尝试打印二位数组的每一个元素的地址
#include <stdio.h>
int main()
{
int arr[3][4];
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
}
}
return 0;
}
*有没有发现一个奇特的现象,这和一位数组的存储非常的类似,我们发现地址也是连续的,但我们还是通过代码来验证一下,我们所看到的结果。
//代码2
int* p = &arr[0][0];//找到第一个元素的地址,
int j = 0;
for (j = 0; j < 12; j++)
{
printf("%d ", *(p + j));//P+j是依次往后面找元素的地址,在解引用找到内容
}
我们发现代码1和代码2的结果时一样,所以二维数组在内存里面的存储也是连续的。那既然存储的地址时连续的,那我们不妨做一个这样的猜想:先看图:
好我们来验证一下,是不是和我们想的一样???
#include <stdio.h>
int main()
{
int arr[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)//打印每一行
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
1.在图中说行下标为多少,就有多少个元素,第一个循环刚好是控制行数的所以我们通过计算这个3来验证我们的猜想,
sizeof(arr)/sizeof(arr[0]);arr代表整个数组的地址,arr[0]代表一维数组的第一个元素地址(相当于第一行所有元素的地址)。
2.刚才在图中的一维数组中的每一个元素代表的又是一个一维数组,列下标刚好代表每一行元素的数据个数,也就相当于,这个每个元素代表的一维数组里面有多少个元素。第二个for循环刚好控制列数来计算这个4来验证我们的猜想。
sizeof(arr[0])/sizeof(arr[0][0]);arr[0]代表一行的数组大小,arr[0][0]代表每一个数据元素的大小。
看验证代码:
我们可以看到结果和我们所猜想的完全一样
那我们继续将代码改造看的更直观一些
#include<stdio.h>
int main()
{
int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int* p = arr[0];
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4;j++)
{
printf("%d ", (p+4*i)[j]);
}
printf("\n");
}
return 0;
}
p保存我们第一行的地址,相当于一维数组第一个元素的地址p+4i相当于找我们每一行的地址的其实地址,为什么加4i,因为每一行有四个元素,加4*i跳过一行的地址,打印的结果还是一样的,请读者自己下来去测试一下。
这样的意义其实不大,到后来我们学习了数组指针的时候,会有更深的理解。
三、数组越界
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就
是正确的,
所以我们在写代码时,最好自己做越界的检查
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i = 0;
for(i=0; i<=10; i++)//超出了
{
printf("%d\n", arr[i]);//当i等于10的时候,越界访问了
}
return 0;
}
出现越界访问不会报错,但结果会是随机值,在gcc下会报错。
二维数组的行和列也可能存在越界。所以我们在编程的时候,要注意数组的越界,不要给自己和别人找麻烦。
四. 数组作为函数参数
往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里我会讲一下算法思想)函数
将一个整形数组排序。
冒泡排序就是将一组无序的数,排成有序的数,这里我将讲解升序排序,我们的算法思想就是第一趟把所有数里最大的数排到最后,通过两两比较交换达到的,接下来我将画一个图直观的理解冒泡排序:
我先将代码给大家看,然后对着图来理解:
#include <stdio.h>
void bubble_sort(int arr[])
{
int sz = sizeof(arr)/sizeof(arr[0]);//计算数组的元素个数
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])//大于后面的一个数就交换
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
bubble_sort(arr);//是否可以正常排序?
int i=0;
for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
说明:这里我包装成一个函数了我们将一个数组传进去,用相同类型的数组接收,这里我在函数部分详细的介绍过。我们乍一看这个代码没有问题,我们来一下运行结果
怎么回事,我们代码写的不对吗?那我们来看看在运行的时候哪一步出错的
这样我们清楚的看到了,我们把arr传进去,以为函数接收到了我们这个数组的内容,但结果不是和我们想的一样, 那这是为什么呢?这里就要在介绍一个东西:
数组名是什么:
//代码1
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%d\n", *arr);
//输出结果
return 0;
}
我们通过代码发现,arr居然和首元素地址一样,*arr对地址解引用找到里面的内容也是首元素,说明arr是首元素地址。
那数组名代表首元素地址的话,那接下来的代码结果呢?
int arr[10] = {0};
printf("%d\n", sizeof(arr));
这个结果是40,因为数组里面有10个元素,每个元素的大小是四个自己二,所以是40,那么问题出来了,数组名代表首元素地址,大小应该是4个字节啊,这时候就要补充两个arr不是数组名的例外:
- sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
- &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。
那我们把代码1在修改一下:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%p\n", arr);//1
printf("%p\n", &arr);//2
printf("%p\n", arr + 1);//3
printf("%p\n", &arr[0]);//4
printf("%p\n", &arr[0]+1);//5
printf("%p\n", &arr+1);//6
//输出结果
return 0;
大家看到了1和2的结果了吗,刚才不是说&arr代表整个数组的地址,那为什么和arr的地址是一样的呢?
通过3和6我们更直观看到arr+1跳过一个元素的地址,所以增加了四个字节,&arr+1跳过整个数组的地址,地址增加40个字节
解释:指针加1到底跳过几个字节和指针类型有关,指针就是代表地址,侧面表示了首元素地址加1和数组加1的类型肯定不一样。
说了这么多,我们回归正题,刚才那个代码怎么修改呢?让我们看一下正确的代码:
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
//代码同上面函数
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);//是否可以正常排序?
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
由于刚才计算数组出现了错误,刚才sz=sizeof(arr)/sizeof(arr[0]),因为传过去的是首元素地址,所以sizeof(arr)=4,sizeof(arr[0])=4所以结果为1,我们也可以把传参部分修改成如下部分:
void bubble_sort(int* arr, int sz)
这样看到更加直观写,指针就代表地址,对于刚接触数组这样写很容易不懂,但结果都是对的
那我们是怎么访问到数组里面的元素的呢?
这里还是来补充一下,在前面我们也说到过,数组的存储单元是连续的,是通过[]操作符和下标来访问的,这里的arr[i]=*(p+1);int *p=arr;这两种访问数组的方式是一样的,前面也通过代码说明了。
相信到这里了你也知道数组作为函数参数代表什么意思了吧。
五、总结
通过这个我想你对于数组的理解肯定高了一个层次,博主将自己所学与你们一起分享,如果有不对的地方,还请指正出来,博主会及时修改的,在接下来的时间,博主会更新关于数组方面的实战小游戏,三子棋,如果你想了解这个游戏,就关注博主吧,带你玩这个三子棋游戏!!那这期文章就到此结束了,我们下期再见。