C语言初阶-数组
目录
一、一维数组的创建和初始化
1.数组的创建
数组是一种相同元素类型的集合。
数组的创建:
type_t arr_name [const_n];
/ / type_t 是指数组的元素类型
/ / const_n 是一个常量表达式,用来指定数组的大小
举例:
int arr1[10];
char arr2[10];
float arr3[1];
double arr4[20];
其中,int,char,float,double就是数组的元素类型,10,10,1,20分别是数组的大小。
下面是一个错误示例:
int main()
{
int n = 0;
int arr[n] = { 0 };
return 0;
}
运行时编译器会报错:
这个原因其实前面也讲过:有些编译器在定义数组时,数组的大小必须是一个常量表达式,而上述代码中的n是变量。
注意:C99中引入了变长数组的概念,允许数组的大小用变量来指定,如果编译器不支持C99中的变长数组,那就不能使用,相反,支持C99的编译器可以使用变量来指定数组的大小。
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";
不完全初始化:
int main()
{
int arr1[10] = { 1,2,3 };
return 0;
}
像这种,定义的数组大小是10,而大括号内的元素数只有3个,这就叫做不完全初始化,系统会默认后面7个元素为0。打开监视页面可以看到:
注意: 初始化的元素数可以小于定义数组的大小,但是一定不能超过数组的大小。
我们在定义数组时,也可以不写数组的大小:
int arr1[10] = { 1,2,3 };
int arr2[] = { 1,2,3 };
这时系统会根据初始化数组中元素的个数自动确定数组的大小,运行并打开监视页面:
在初始化字符数组时,以下两种写法的结果是一样的:
char arr4[] = { 'a',98,'c' };
char arr5[] = { 'a','b','c' };
因为字符b的ASCII值就是98。
不初始化:
对局部变量不进行初始化,会生成随机值,而对全局变量不进行初始化系统会默认值为0。
局部变量:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr1[10];//局部变量
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d\n", arr1[i]);
}
return 0;
}
代码中数组arr1没有初始化,运行结果为:
全局变量:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int arr1[10];//全局变量
int main()
{
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d\n", arr1[i]);
}
return 0;
}
运行结果:
全局变量和局部变量不初始化的结果不同,本质原因是:存储的区域不同,存储在栈区的不初始化就是随机值,存储在静态区的不初始化就是0。
3.一维数组的使用
前面讲过,数组的下标是从0开始的,数组元素可以通过下标来访问:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
//下标: 0 1 2 3 4
printf("%d\n", arr[4]);//打印下标为4的数组元素
return 0;
}
运行结果是:5。下标为4的数组元素arr[4]是5。其中,[ ]是下标引用操作符,arr和4是[ ]的两个操作数。
当定义数组的大小比较大时,我们不用一个一个输入去赋值,可以通过写代码的方式对其赋值:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };//数组的不完全初始化
//计算数组的元素的个数
int sz = sizeof(arr) / sizeof(arr[0]);
//对数组内容赋值
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i + 1;
}
//打印出数组
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果是:1 2 3 4 5 6 7 8 9 10.
另外,数组的元素个数是可计算的:
int arr[10] = { 0 };
//计算数组的元素的个数
int sz = sizeof(arr) / sizeof(arr[0]);
用整个数组的大小除以数组首元素的大小就可以计算出数组元素个数。
4.一维数组在内存中的存储
要知道一维数组在内存中的存储,就要知道它的元素的地址,下面我们写一段代码打印一下数组元素的地址:
#define _CRT_SECURE_NO_WARNINGS 1
#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("&arr[%d] = %p\n", i,&arr[i]);
}
return 0;
}
运行结果:
%p打印的是十六进制的数,观察打印结果会发现,每个地址之间都相差4,为什么是4呢?因为是整型数组,int在内存中存储时占4个字节。
由于每两个数组元素之间地址相差的值一样,我们可以得出,一维数组在内存中是连续存放的,并且随着下标的增长,数组元素的地址由低到高变化。
通过指针访问数组元素:
因为一维数组在内存中是连续存储的,所以不但可以通过下标访问指针元素,还可以通过指针访问。
先看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//打印出数组元素的地址
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for (i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <==>%p\n",i,&arr[i],p+i);
}
return 0;
}
运行结果:
通过代码运行结果可以看到:&arr[i] 和 p+i 的结果是一致的。代码中,指针变量p存放数组元素的地址,因为*p是整型指针,所以p+i代表每次跳过4个字节,例如p+1代表跳过4个字节,p+2代表跳过8个字节。
了解了这些,我们就可以用指针来访问数组元素了:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//打印出数组元素的地址
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for (i = 0; i < sz; i++)
{
printf("%d ",*(p+i));
}
return 0;
}
因为&arr[i] 和 p+i 的结果是一致的,所以通过*(p+i)对p+i解引用就可以通过地址找到并打印出数组元素了。
二、二维数组的创建和初始化
1.二维数组的创建
int arr[3][4];
char arr1[3][5];
double arr[2][4];
创建时第一个括号中代表行,第二个括号中代表列,比如:int arr[3][5]就代表创建一个三行五列的数组。(如下图所示)
2.二维数组的初始化
int arr[3][4] = { 1,2,3,4 };
char arr1[3][5] = { {1,2},{4,5} };
double arr[][4] = { {2,3},{4,5} };//二维数组的行可以省略,列不能省略
不完全初始化:
int main()
{
int arr[3][4] = { 1,2,3,4,5 };
return 0;
}
打开监视页面可以看到:
数组大小为3*4=12,只定义了5个元素,其余元素默认为0。数组是3行4列,第一行只能放4个元素,多出来的元素从第二行开始存放。
如果想把1,2放在第一行,3,4放在第二行,5放在第三行,初始化时可以这么写:
int main()
{
int arr[3][4] = { {1,2},{3,4},{5} };
return 0;
}
其实我们可以把二维数组的每一行看做一个一维数组,例如:{1,2}在大括号内再写一个大括号,相当于把第一行前两个元素初始化为1,2,其他元素默认为0,那{3,4}相当于把第二行的前两个元素初始化为3,4,其他元素默认为0,同理{5}也一样。
我们在初始化二维数组时可以把行省略,但是不能省略列:
省略行:
int main()
{
int arr[][4] = {1,2,3,4,5};
return 0;
}
这时系统会根据列数以及存放的元素数自动确定行数,如上述代码,5个元素,列为4,最多存放到第二行,这时的行大小就是2.
打开监视页面就可以看到:
当然我们以下写法的结果没有改变,还是3行4列:
int main()
{
int arr[][4] = { {1,2},{3,4},{5} };
return 0;
}
3.二维数组的使用
二维数组的元素也有下标,也可以通过下标来访问,它的行号和列号也都是从0开始的(如下图)
如果要访问第一行第3列的元素用:arr[1][3]。
下面用下标来打印一个二维数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[][4] = { {1,2},{3,4},{5}};
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");
}
return 0;
}
运行结果:
二维数组的行数和列数也可以计算:
sizeof(arr) / sizeof(arr[0]);//行
sizeof(arr[0]) / sizeof(arr[0][0]);//列
用整个数组的大小除以第一行元素的大小得出有几行,
用第一行元素的大小除以第一行第一个元素的大小得出有几列。
那么前面的代码也可以这样写:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[][4] = { {1,2},{3,4},{5}};
int i = 0;
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)//行
{
int j = 0;
for (j = 0; j < sizeof(arr[0]) / sizeof(arr[0][0]); j++)//列
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
4.二维数组在内存中的存储
先写一段代码打印一下二维数组元素的地址:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[3][4] = { {1,2},{3,4},{5}};
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;
}
运行结果:
可以看到每个元素地址之间都相差4,包括第一行最后一个元素和第二行第一个元素地址之间也是相差4,由此可见,我们想像中的数组存储方式和实际中的是不同的:
所以二维数组在内存中的存储也是连续的!
如果把二维数组每一行看做一维数组,那第一行的数组名为arr[0],第二行的数组名为arr[1],第三行的数组名是arr[2](如下图)
三、数组越界
数组的下标是有范围的。
数组的下标规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1.
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
看代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
下标最大是9,当访问下标为10的数组元素时就发生了数组越界。
这时候编译器可能还能打印出来,但是打印的是一个随机值。
所以程序员写代码时,数组越界访问需要自己去自查。
四、数组作为函数参数
往往我们在写代码的时候,会将数组作为一个参数传给函数,比如:要实现一个冒泡排序的函数来将一个整型数组排序。
冒泡排序:
冒泡排序就是将数组中两个相邻的数进行比较,不满足顺序就互相交换,如图第一趟,将第一个元素9与它后面的元素进行比较,交换,直到与其他9个元素比较完后,9的位置就确定下来了,第二趟就剩除9以外的其他9个元素的比较,以此类推,直到经历9趟比较、交换的过程,最终实现10个元素按照升序排列。
那我们就可以写两个for循环嵌套,第一个 i 循环是趟数,n个元素需要n-1趟排序,第二个 j 循环写相邻两数的比较,第一趟10个元素,比较9次,第二趟9个元素,比较8次,...以此类推,每趟需要比较的次数为n-1-i,在 j 循环中写 if 语句进行具体的比较、交换过程。
1.冒泡排序函数的错误设计
了解了冒泡排序的原理,让我们来写一个冒泡排序函数吧!
#define _CRT_SECURE_NO_WARNINGS 1
#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-1-i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
bubble_sort(arr);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
上述代码运行结果:
显然,上述代码并没有实现升序排列,那么问题出在哪里了?
打开调试监视页面可以看到:
正常来说,数组元素个数(sz)计算出来应该是10,但是此时却是1。
原因在之前的学习中也讲过很多遍了, 因为数组在传参的时候传递的不是整个数组,而是数组首元素的地址,这样做是为了节省空间,又因为数组在内存中的存储是连续的,只传递数组首元素的地址就可以顺藤摸瓜找出所有元素,而不用再次创建同样大小的数组,那样会浪费空间。
只传递数组首元素的地址,那么参数int arr[ ]就相当于是指针int* arr,又因为此时是x86系统,指针大小始终是4,除以数组第一个元素大小4,算出来的数组长度sz就是1。
下面我们来具体学习一下数组名。
2.数组名是什么?
我们经常说数组名就是数组首元素的地址,下面我们写代码把它们打印出来对比一下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
运行结果:
可以看到结果是一样的,那么我们说数组名就是数组首元素的地址是没错的,但是注意其中有两个特例:
数组名就是数组首元素的地址。
但是有2个例外:
1. sizeof(数组名),数组名不是首元素地址,数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,数组名不是首元素地址,数组名表示整个数组,取出的是整个数组的地址。
1. sizeof(数组名)
看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr);
printf("%d\n", sz);
return 0;
}
运行结果:
如果说 sizeof(数组名)是首元素地址,那么计算一个整型数组的首元素地址大小应该为4,但是结果却是40。这就说明了它数组名表示整个数组,计算的是整个数组的大小。
2. &数组名
看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);;
return 0;
}
运行结果:
这时候问题又来了,不是说&arr取出的是整个数组的地址吗?
为什么它打印出来的结果与前面的arr和&arr[0]一样?
别着急,我们给它们每个加一,然后再打印出来看看。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr);
printf("%p\n", &arr+1);
return 0;
}
运行结果:
结果很明显:前面两个加一后输出的地址都相差4个字节,而&arr加一后输出的地址相差40个字节(90和B8是十六进制,它们之间相差40),这就说明了 &数组名,数组名不是首元素地址,数组名表示整个数组,取出的是整个数组的地址。
下图可以表现它们所指地址之间的关系:
现在我们可以看看前面所讲冒泡排序的正确设计了。
3.冒泡排序函数的正确设计
上述错误代码只将数组名arr传给参数,但由于传的是首元素的地址,导致计算数组元素个数(sz)时出现错误,显然行不通,下面只要将计算数组元素个数(sz)的代码从函数bubble_sort函数中移除,并写在主函数中,然后将数组名arr和sz两个参数都传过去,就可以实现冒泡排序了。
代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void bubble_sort(int arr[],int sz)
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int j = 0;
for (j = 0; j < sz-1-i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
bubble_sort(arr,sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
今天就学到这里啦,未完待续。。。