• 从零开始C语言精讲篇4:数组



    前言

    这里记录本文的大概内容:

    1. 一维数组的创建和初始化
    2. 一维数组的使用
    3. 一维数组在内存中的存储
    4. 二维数组的创建和初始化
    5. 二维数组的使用
    6. 二维数组在内存中的存储
    7. 数组作为函数参数

    在这里插入图片描述

    提示:以下是本篇文章正文内容,下面案例可供参考

    一、一维数组

    1.1数组的创建

    type_t   arr_name   [const_n];
    //type_t 是指数组的元素类型
    //const_n 是一个常量表达式,用来指定数组的大小
    
    • 1
    • 2
    • 3

    举个例子:

    //代码1
    int arr1[10];
    
    //代码2
    int count = 10;
    int arr2[count];//数组时候可以正常创建?
    
    //代码3
    char arr3[10];
    float arr4[1];
    double arr5[12];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ps:数组创建时,[ ]里面最好使用常量,C99之后才允许使用变量,
    有些编译器对C99标准支持不是很好,你在[ ]里面使用变量可能会报错

    没有初始化之前,数组里面放的是一些随机值
    在这里插入图片描述

    1.2数组的初始化

    int main()
    {
    	//初始化
    	int arr1[10] = { 1,2,3 };
    	//不完全初始化,开头三个初始化为1、2、3,其他的默认为0
    	int arr2[] = { 1,2,3 };
    	//没有指定大小,根据初始化内容来默认指定大小,
    	//比如这里初始化3个数,那么数组大小就是3
    
    	char arr3[] = "abc";//abc后默认有一个\0,arr3大小为4字节
    	char arr4[] = { 'a','b','c' };//abc后没有\0,arr4大小为3字节
    	char arr5[] = { 'a',98,'c' };//如果用数字初始化字符数组,是对应的ASCII码的字母
    	//比如这里的98对应的是'b'
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    1.3一维数组的使用

    对于数组的使用我们之前介绍了一个操作符: [ ] ,下标引用操作符。
    而我们数组就是使用下标来访问元素的,比如arr[下标],示例如下:

    int main()
    {
    	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//数组下标从0开始,往后依次+1
    	//printf("%d", arr[4]);//如果想打印数组第5个元素,那么你访问的应该是下标4
    	int i = 0;
    	for (i = 0;i < 10;i++)
    	{
    		printf("%d ", arr[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然而,上面代码也是有一定缺陷的,我们的for循环的判断条件i<10,是我们提前知道数组一共10个元素的情况下,将来如果有人修改了数组元素个数,我们这里还用i<10就容易出错了。

    所以我们这里建议使用sizeof函数把数组元素个数计算出来,然后替换掉for循环里面的10

    int main()
    {
    	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//数组下标从0开始,往后依次+1
    	//printf("%d", arr[4]);//如果想打印数组第5个元素,那么你访问的应该是下标4
    	int i = 0;
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	//sizeof(arr)是数组总大小,sizeof(arr[0])是数组一个元素大小
    	//两者相除就是数组元素个数
    	for (i = 0;i < sz;i++)
    	{
    		printf("%d ", arr[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    1.4一维数组在内存中的存储

    我们一维数组在内存中是连续开辟的一块空间,
    要想知道它的布局,只要把它每个元素的地址打印下来就可以了

    int main()
    {
    	int arr[] = { 1,2,3,4,5 };
    	int i = 0;
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	for (i = 0;i < sz;i++)
    	{
    		printf("%p\n", &arr[i]);//打印地址用%p,&arr[i]取出arr[i]的地址
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    可以看到,我们这里每隔一个数组元素地址差4,
    为什么差4?因为一个int型是4字节啊(我们这里是整形数组)

    相应的,如果是其他类型的数组,每个数组元素地址相差一个类型的大小
    仔细观察输出的结果,我们知道,随着数组下标的增长,
    元素的地址,也在有规律的递增。 由此可以得出结论:数组在内存中是连续存放的。

    在这里插入图片描述

    二、二维数组

    2.1二维数组的创建

    int main()
    {
    	int arr[3][5];
    	char brr[2][4];
    	double crr[4][2];
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    示意图如下:
    在这里插入图片描述

    2.2二维数组的初始化

    int main()
    {
    	int arr1[3][5] = {1,2,3,4,5,6,7,8,9,10,11};
    	//3行5列,前两行放满,第3行放11,剩余的用0补
    	
    	int arr2[3][5] = { {1,2},{3,4},{5,6} };
    	//第1行放1,2剩下的用0补;第2行放3,4剩下的用0补;第3行放5,6剩下的用0补
    
    	int arr3[][5] = { 1,2,3,4,5,6 };
    	//对二维数组来说,可以省略掉行数,但不能省略掉列数
    	//有了列数,可以根据初始化内容,推出行数
    	//比如列是5,那么arr3的第一行就要放5个元素,剩下一个6放第二行
    	//6后面没有数了那么就用0补齐第二行,
    	//也就是推出了行数为2
    
    	//为什么不能没有列号呢?
    	//答:没有列号我怎么知道你一行放多少个?
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    其他类型是数组也是差不多的形式:

    int main()
    {
    	char ch1[2][3] = { 'a','b' };
    	char ch2[2][3] = { {'a'},{'b'} };
    	char ch3[4][6] = { "abc","def","qwe" };
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    2.3二维数组的使用

    二维数组的使用也是通过下标的方式。
    行号列号都是从0开始,往后依次+1

    int main()
    {
    	int arr[3][5] = { {1,2,3},{4,5},{6,7,8,9,10} };
    	int i = 0;//行号
    	for (int i = 0;i < 3;i++)//行号范围0-2
    	{
    		int j = 0;//列号
    		for (j = 0;j < 5;j++)
    		{
    			printf("%d ", arr[i][j]);
    		}
    		printf("\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    2.4二维数组在内存中的存储

    我们通过上面的讲解,可以把二维数组理解为一个n*m的列表
    但是它在内存里存储真的是这样吗?
    我们通过和一维数组一样的方法“看地址”来看它在内存中到底是怎么存储的

    int main()
    {
    	int arr[3][5] = { {1,2,3},{4,5},{6,7,8,9,10} };
    	int i = 0;//行号
    	for (int i = 0;i < 3;i++)//行号范围0-2
    	{
    		int j = 0;//列号
    		for (j = 0;j < 5;j++)
    		{
    			printf("&arr[%d][%d]=%p\n", i,j,&arr[i][j]);
    		}
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    也就是说,我们的二维数组在内存中也是连续存放的
    在这里插入图片描述
    在这里插入图片描述
    你可以把二维数组的每个元素理解为一个一维数组,
    比如第一行是一个一维数组;第二行是一个一维数组…

    三、数组越界

    数组下标是有范围限制的,比如arr[3],这个数组下标范围是[0,2]

    数组下标规定是从0开始,如果数组有n个元素,那么最后一个元素下标n-1

    所有的数组,如果下标小于0,或者大于n-1,就构成了数组越界访问

    C语言本身不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错不代表程序就是正确的,所以程序员写代码的时候,应该自己做一下越界检查。

    int main()
    {
    	int arr[5] = { 1,2,3,4,5 };
    	int i = 0;
    	for (i = 0;i <= 5;i++)//越界访问了下标5
    	{
    		printf("%d ", arr[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    如上图,最后一个数就是我们越界访问的。虽然编译器没有报错,但是越界访问是很严重的问题。

    举个例子:电脑一些程序正在用一块空间,你突然越界访问了那块空间,可能访问,看一下没有啥问题,但是一旦你代码中有一些赋值或者改变了那块空间的某些东西,你电脑很容易就崩溃了!

    四、数组作为函数参数

    往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里要讲算法思想)函数将一个整形数组排序。 那我们将会这样使用该函数:

    //我们以冒泡排序(升序)为例
    //冒泡排序核心思想:两两相邻的元素进行比较
    void sort(int arr[],int sz)//这里参数int arr[]本质上和int* arr是一样的,都是指针
    {//所以这里参数也可以写int* arr,只不过int arr[]更容易让c语言新手理解
    	
    	int i = 0;//冒泡排序的趟数
    	for (int 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[] = { 1,321,32,2,6,8,65,2,77 };
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	//数组名单独放sizeof内部时,这时表示的是整个数组,而不是数组首元素地址
    	sort(arr,sz);
    	//数组名传参本质传的是首元素地址(是一个指针)
    	for (int i = 0;i < sz;i++)
    	{
    		printf("%d ", arr[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    特别注意:数组名在进行函数传参的时候,就是传的数组首元素地址(指针)
    但是sizeof(数组名),传的是完整数组,不是首元素地址。

    注:如果数组名作为函数参数传递,我们也可以通过数组名(指针)来打印数组元素
    比如我们这里把main函数里面的打印部分抽离出来,做成一个函数

    //我们以冒泡排序(升序)为例
    //冒泡排序核心思想:两两相邻的元素进行比较
    void sort(int arr[],int sz)//这里参数int arr[]本质上和int* arr是一样的,都是指针
    {
    	
    	int i = 0;//冒泡排序的趟数
    	for (int 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;
    			}
    		}
    	}
    }
    void print1(int* p,int sz)
    {
    	//打印法1
    	for (int i = 0;i < sz;i++)
    	{
    		printf("%d ", *p);
    		p++;
    		//上面两行也可以简写成printf("%d ", *(p+i));
    	}
    	printf("\n");
    
    	
    
    }
    
    void print2(int* p, int sz)
    {
    	//打印法2
    	//我们之前说过数组名arr作为参数,int arr[]=int *p
    	//那么我们也可以把p看成数组名
    	for (int i = 0;i < sz;i++)
    	{
    		printf("%d ", p[i]);
    	}
    
    }
    
    
    int main()
    {
    	int arr[] = { 1,321,32,2,6,8,65,2,77 };
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	sort(arr,sz);//数组名传参本质传的是首元素地址(是一个指针)
    	print1(arr, sz);
    	print2(arr, sz);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    在这里插入图片描述

    注:冒泡排序的一些改进(非数组知识点)
    我们上面的冒泡排序假如遇到了1 2 3 4 5 6这样原本就是升序的数组,你还要对他一趟一趟的测试?
    我们可以置一个flag,如果跑了一趟,数组元素一个也没有发生交换,说明是已经有序了,后续就不用再来排序了。改进代码如下:

    void sort(int arr[],int sz)//这里参数int arr[]本质上和int* arr是一样的,都是指针
    {
    	
    	int i = 0;//冒泡排序的趟数
    	for (int i = 0;i < sz - 1;i++)
    	{
    		//一趟冒泡排序的过程
    		int j = 0;
    		int flag = 1;//假设已经有序
    		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;
    				flag = 0;
    			}
    		}
    		if (flag == 1)
    		{
    			break;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    另外还有数组名作为函数参数:是首元素地址(有两个例外)

    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6 };
    	printf("%p\n", arr);//数组首元素地址
    	printf("%p\n", &arr[0]);//数组首元素地址
    	printf("%p\n", &arr);//数组地址
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    数组首元素地址和数组地址打印出来是一样的,但是本质是不同的,我们来看下面的代码示例

    int main()
    {
    	int arr[10] = { 1,2,3,4,5,6 };
    	printf("%p\n", arr);//数组首元素地址
    	printf("%p\n", &arr[0]);//数组首元素地址
    	printf("%p\n", &arr);//数组地址
    
    	printf("\n");
    
    	printf("%p\n", arr+1);//数组首元素地址+1
    	printf("%p\n", &arr[0]+1);//数组首元素地址+1
    	printf("%p\n", &arr+1);//数组地址+1
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    我们知道数组首元素地址+1,跳过4字节(一个整形),那数组地址+1呢?

    我们用计算机可以算一下十六进制的CC-A4=28,28转换成十进制是40,为啥是40呢?
    因为我们int arr[10],数组总大小为40字节,我们数组地址+1跳到的是下一个数组地址

    数组名作为参数的两个例外

    1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。 2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

    总结

    本文介绍了一维数组、二维数组的使用,及在内存空间中的存储,数组越界相关知识点
    另外还有数组名作为函数参数:是首元素地址(有两个例外)
    本文的冒泡排序改进读者们也可以研究一下,对将来的数据结构排序也是一种铺垫

  • 相关阅读:
    Java初阶——This与代码块
    多头注意力机制的计算流、代码解析
    php strtr其他语言实现
    uni-app小程序开发踩过的坑之ref无法获取当前元素的宽高数据
    【Meetup明天见】OpenMLDB+AutoX:整合自动特征工程,拥抱高效机器学习​
    超详细教程—快速创建PD
    list的用法
    Java多线程的不同实现方式
    四、MySQL 提权方式
    Springboot整合Mybatis-Plus
  • 原文地址:https://blog.csdn.net/m0_57180439/article/details/125974395