1. 前言🔶
啊~~,很久没有更新C语言知识了,各位久等了,本篇文章在了解了数组的基本知识后, 着重于给大家实现两个小游戏:三子棋和扫雷
2. 一维数组🔶
2.1 一维数组的创建🔷
type_t arr_name [const_n]; //type_t 是指数组的元素类型 //arr_name是数组名,是自己取的 //const_n 是一个常量表达式 1
比如我们可以依次定义一个整型数组,一个浮点型数组,一个字符型数组:
int a[10];//可以存放十个整型的数组 float b[20];//可以存放二十个整型的数组 char c[10];
值得注意的是,方括号 [ ] 里面必须是常量表达式,假如我们这样初始化数组就会出问题:
int size=10; int a[size];
这里 size 虽然被定义为10了,但是它是变量不是常量,所以这样不对,假如我们加上一个static关键字呢?答案是这样也是不可以的,因为被static关键字修饰的变量虽然具有的常量属性,但是本质上它还是一个变量.
注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。
特别的:
vs2022和2019编译器中不支持C99中变长数字组.
2.2 数组的初始化🔷
以下是几种初始化的方式:
int main() { int arr1[10] = {1,2,3,4,5,6,7,8,9,10};//完全初始化 int arr2[10] = { 1,2,3 };//不完全初始化,剩余的元素默认都是0 int arr3[10] = { 0 };//不完全初始化,剩余的元素默认都是0 int arr4[] = { 0 };//省略数组的大小,数组必须初始化,数组的大小是根据初始化的内容来确定 int arr5[] = { 1,2,3 };//前三个元素为1 2 3,后面两个元素默认为0 int arr6[];//错误的写法 char arr1[] = "abc"; char arr2[] = {'a', 'b', 'c'}; char arr3[] = { 'a', 98, 'c' }; return 0; }
我们要区分下面这两种初始化数组的方式的区别:
char a[]="abc"; char b[]={'a','b','c'};
画个内存图理解一下:
2.3 一维数组的使用🔷
对于数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。我们来看代码:
(下标从0开始!)
#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++) { arr[i] = i; } //输出数组的内容 for(i=0; i<10; ++i) { printf("%d ", arr[i]); } return 0; }
值得注意的是,数组初始化时,方括号[ ]内不允许使用变量只能用常量,但是对数组的使用中,方括号[ ]内是可以为变量的
这里第一个引出求数组元素个数的一种求法: size= sizeof(数组名)/sizeof(任意一个数组中元素).虽然我们经常说,数组名代表首元素地址, 但是有特例!当数组名放在sizeof中时,这时求出的是整个数组所占空间的大小(单位是字节),然而把单个数组元素放进sizeof中即求出数组中单个元素所占空间大小,讲它们两个相除就可以得到数组元素个数.
比如我们来举个例子:
#include<stdio.h> int main() { int a[10] = { 1,2,3,4,5,6,7,8,9,0 }; int x = sizeof(a);//等于40,4*10,一个整型元素四个字节,共10个 printf("%d\n", x); int y = sizeof(a[0]);//等于4,大小为一个整型的大小.这里你也可以写成a[1].a[2].随便一个数组中的元素就行 printf("%d", y); return 0; }
2.4 一维数组在内存中的存储🔷
我们先定义一个整型数组并将它所有元素的地址打印出来:(%p是打印地址)
#include<stdio.h> int main() { int a[10] = { 1,2,3,4,5,6,7,8,9,0 }; for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) { printf("%p \n", &a[i]); } return 0; }
我们来研究一下数组中元素在内存中是怎么存放的:
我们知道数组在内存中存储时用的是16进制,所以这里我们来找一下这些地址的规律:(只看最后四个)
第一个元素地址是F4E8,E8转换为10进制是232,第二个元素的EC转换为十进制为236,相差4.
第二个元素十进制为236,第三个元素转换为10进制为240,也相差4.
倒数第二个元素为08,最后一个元素为0C,C在十六进制是12的意思,这里8和12也相差4.
可以发现,每一个元素之间都相差四个字节,并且我们这里定义的数组是整型数组,每一个元素所占空间刚好也是四个字节,这刚好能说明数组中元素是连续存放的!
我们按照这个编译器打印出的地址再来画一个内存图理解一下:
我们之前提到过,一个整型占四个字节,四个字节拥有四个地址,这个整型的地址是第一个字节的地址,这里就说的通了! 数组确实是连续存放的
值得注意的是,不同电脑不同编译器在不同时刻为数组开辟的空间是不一样的,所以你的电脑的地址中不必和我一模一样,只需要查看每个元素之间是不是紧挨着的.
3. 二维数组🔶
3.1 二维数组的创建🔷
//数组创建 int arr[3][4]; char arr[3][5]; double arr[2][4];
和一维数组相似,只不过要多写一个方括号
3.2 二维数组的初始化🔷
有几种初始化的方式:
//数组初始化 int arr[3][4] = {1,2,3,4};//这样初始化代表第一行为1 2 3 4,而2,3行所有元素默认为0 int arr[3][4] = {{1,2},{4,5}};//这种加上大括号的表示第一行为1 2 0 0,第二行为4 5 0 0,第三行默认全部为0. int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略 1
一种不加大括号的初始化方式就是挨着往后初始化,没有被初始化到的位置默认为0
一种是加了大括号的初始化方式就是第一个大括号内代表第一行元素,没有被初始化到的位置,默认为0
并且行可以省略但是列不行!
3.3 二维数组的使用🔷
假如我们想挨个打印二维数组中的内容,我们可以这样写:(二维数组的下标也是从0开始)
#include<stdio.h> int main() { int arr[4][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7},{5,6,7,8,9} }; printf("%d\n", arr[2][3]);//打印第三行第四列元素 int i = 0; //行号 for (i = 0; i < 4; i++)//最外层for循环代表行,i等于0就是第一行,进入第二个循环将第一行所有列打印出来再到第二行 { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", arr[i][j]); } printf("\n");//每打印完一列就换一次行 } return 0; }
我们就把每一个元素很好的打印出来了:
这里二维数组的使用与一维数组大同小异,直接往下走!
3.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; }
和一维数组一样,二维数组每个元素之间相差也是四个字节,可见,二维数组在内存中的存放和我们看见打印出的矩阵有所不同,它每一行之间是相连的,而不是分开的.这里再画一个存储图理解一下
可以看见内存中真实的存储是这样的,我们可以把二维数组理解为存储数组的数组,你看每一个红方格里面存放了一个数组,实际上就是二维数组的第几行,然后一共有四行,代表这个数组存储了四个数组,被存储的数组每个数组元素为5个.
4. 数组的越界访问🔶
数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就
是正确的,所以程序员写代码时,应该自己检查越界问题
比如像下面这段代码,当i等于10时数组就越界了,但是你的编译器可能不会给你警告提醒你
#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; }
5. 数组作为函数参数🔶
我们在写代码的时候常常会将数组作为参数传递给函数实现某些功能,这里我就总结一下一维数组和二维数组传参的方式:
一维数组: void test(int a[]) void test(int a[10]) void test(int* a)//使用方法和上面一样 二维数组 void test(int a[3][5]) void test(int a[][5]) void test(int (*a)[5]) void test(int** a)
我们举一个例子来说明一下数组传参有什么坑!
5.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-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[ ]数组传过去时,实际上这个arr是一个指针,用来接受数组首元素地址的指针,所以当我们使用sizeof求arr的时候相当于是求了指针变量的大小,我们机器是64位,所以指针大小为8个字节,然而一个整型是4个字节,将它们相除就得到了2.
所以这个地方是错误的写法,我们将它修改一下:
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-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}; int sz = sizeof(arr)/sizeof(arr[0]); bubble_sort(arr, sz);//是否可以正常排序? for(i=0; i<sz; i++) { printf("%d ", arr[i]); } return 0; }
将数组的大小做为函数的参数传入函数中就可以解决这个问题了🫵🫵
5.2 数组名到底是什么?🔷
我们用一段打印地址的代码来为大家阐述:
#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; }
我们可以发现,数组名和首元素地址是相同的,所以其实数组名就是首元素的地址,将数组名解引用就可以得到首元素.
但是,这里有两个特例中,数组名不是首元素地址:
当数组名放在sizeof当中时,这时数组名代表整个数组的地址
当数组名前面加一个取地址符号的,这时代表整个数组的地址
6. 总结
当我们有了数组相关知识之后,我们就可以尝试去写一些小程序了,比如经典的小游戏:三子棋和扫雷等.