探究C语言数组的奥秘:大小可省略的定义、内存存储、数组名、传参、指针遍历、数组指针和指针数组、柔性数组等

news2024/11/23 6:16:09

在这里插入图片描述
也许你认为,C语言中的数组非常好理解,就是把一组相同类型的元素存储在同一块空间里。但是你可能并没有真正理解数组的本质,不信的话请回答一下下面的几个小问题,如果你能非常清晰的回答这些问题,那么你对C语言中的数组的理解就入门了。

  1. 一维数组和二维数组在定义时,哪些大小可以省略,哪些不可以省略?如果可以省略,在什么时候是可以省略的呢?
  2. 一维数组和二维数组在内存中是如何存储的?
  3. 一维数组和二维数组的数组名分别表示什么意思?
  4. sizeof(数组名) 和 &数组名 分别表示什么?
  5. 一维数组和二维数组传参时,形参应该如何写?
  6. 数组和指针有什么联系?有什么区别?
  7. 如何使用指针来遍历一维数组和二维数组?
  8. 如何理解数组指针和指针数组?
  9. 什么是柔性数组?

如果你想了解这些内容,那就花点时间阅读下本篇博客吧,希望你能有所收获。

什么是数组

数组,指的是相同类型的元素的集合,比如,想要存储10个整数,除了定义10个int类型的变量,也可以定义一个数组来存储。

数组分为一维数组和二维数组,三维以上的数组就不常见了,所以本篇博客重点讨论一维数组和二维数组。

所谓一维数组,就是很正常的把一些元素放到一起。比如存储1~10,就这么存储:[1 2 3 4 5 6 7 8 9 10]。

而二维数组是有行和列的,比如,用一个3行5列的二维数组来存储1~15,可以这么理解:

在这里插入图片描述

数组的定义和初始化

数组的定义和初始化是要按照指定的个数来的。

对于一维数组,只需要表示出数组名、数组容量和数组元素类型即可。比如,一个能存储10个int类型的数组就应该这么定义:int arr[10];

这样的数组如果是一个局部的数组,也就是在大括号内定义的话,默认会被初始化为随机值。如果你想要手动初始化,比如把1~10放进去,可以这么写:

int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

如果手动进行初始化,可以省略数组的元素个数,编译器会根据你初始化的元素个数来给数组开辟空间。

int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

以上代码中,虽然我把表示数组大小的方括号里的10给省略了,编译器也会开辟一个能够存储10个int数据的数组,因为我手动给这个数组初始化了。

当然,我也可以只给数组的部分元素初始化,比如:

int arr[10] = {1, 2, 3};

如果像上面这么写,这个数组就只会初始化前3个元素为1~3,剩下的7个元素会被默认初始化成0,所以数组实际存储的是:[1 2 3 0 0 0 0 0 0 0]。

一个很常见的写法是,把整个数组都初始化成全0。

int arr[10] = {0};

根据我们对一位数组不完全初始化的理解,如果像上面这么写,就只把第一个元素初始化为0,其余9个元素会被默认初始化为0,其实效果就是把整个数组的所有元素都初始化为0。

讲完了一维数组,那二维数组呢?

二维数组的定义需要指定行数和列数,比如一个3行5列的数组应该这么定义:int arr[3][5];

如果要对其进行初始化,比如初始化为:第一行1~5,第二行6~10,第三行11~15,就这么写:

int arr[3][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

效果可以理解成:
在这里插入图片描述
也可以把每一行也用大括号括起来,这样更加清晰:

int arr[3][5] = { {1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15} };

建议再换个行:

int arr[3][5] = { {1, 2, 3, 4, 5}, 
                  {6, 7, 8, 9, 10}, 
                  {11, 12, 13, 14, 15} };

那能不能像一维数组那样省略大小呢?答案是:行可以省略,列不能省略!也就是说,可以省略行,写成这样:

int arr[][5] = { {1, 2, 3, 4, 5}, 
                 {6, 7, 8, 9, 10}, 
                 {11, 12, 13, 14, 15} };

编译器看到后,就会理解成,由于你放了3行,所以行数就是3。

同样,也可以进行不完全初始化,比如:

int arr[3][5] = { {1, 2, 3, 4, 5}, 
                  {6, 7, 8, 9, 10} };

由于只放了2行,第三行就会被默认初始化为0,也就是这样的布局:
在这里插入图片描述
当然每一行也可以不完全初始化,比如:

int arr[3][5] = { {1, 2, 3}, 
                  {6, 7} };

效果就是,第一行前3个元素初始化为1~3,后2个元素初始化为0,第二行的前2个元素初始化为6和7,后3个元素初始化为0,第三行全部初始化为0。
在这里插入图片描述
当然,如果内层的大括号省略掉,也有类似的效果,不过会一行一行放,放满一行再放下一行,比如:

int arr[3][5] = {1, 2, 3, 4, 5, 6, 7};

会先把第一行放满,第一行就是1~5,第二行前2个元素就是6和7,后3个元素初始化为0,第三行所有元素都是0。
在这里插入图片描述
和一维数组一样,如果想把所有元素都初始化为0,就这么写:

int arr[3][5] = {0};

这样就只放了一个0,其余元素都被初始化为0,就相当于把整个数组的所有元素都初始化为0。

数组在内存中的存储

数组在内存中的存储满足以下2点:

  1. 数组在内存中是连续存储的。
  2. 随着数组下标的增长,地址是由低到高变化的。

先举一个一维数组的例子。写一个程序,把一维数组的每一项的地址都打印出来:

#include <stdio.h>

int main()
{
	int arr[5];
	for (int i = 0; i < 5; ++i)
	{
		printf("&arr[%d] = %p\n", i, &arr[i]);
	}

	return 0;
}

输出结果如下:

在这里插入图片描述
观察到,数组的相邻2项之间的地址都相差了4个字节。为啥是4个字节呢?因为每个int是4个字节,由于数组在内存中是连续存储的,相邻两项的地址自然隔了4个字节。除此之外,随着数组下标从0增长到4,地址也是从低到高变化的。

再举一个二维数组的例子:

#include <stdio.h>

int main()
{
	int arr[3][5];
	for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 5; ++j)
		{
			printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
		}
	}

	return 0;
}

输出结果如下:
在这里插入图片描述
相邻2项的地址仍然间隔4个字节,且随着下标的增长,地址也在增长,这说明数组的内存分布规律同样适用于二维数组。

神奇的数组名

先说结论。在C语言中,数组名表示数组首元素地址,但是有2个例外:

  1. sizeof(数组名),数组名直接放在sizeof()内部,此时数组名不表示首元素地址,而是表示整个数组,求的是整个数组的大小,单位是字节。
  2. &数组名,在&符号后面的数组名,此时数组名不表示首元素地址,而是表示整个数组,取出的是整个数组的地址。详见后面讲解的“数组指针”。

关于sizeof,大家应该很熟悉了,可以计算类型的大小。如果想要计算数组的大小,就sizeof(数组名)即可。比如:

int arr[10];
printf("%d\n", sizeof(arr)); // 40

以上代码中,由于数组有10个元素,每个元素是int,数组总大小是40个字节,所以会输出40。

下面来对比一下3个代码:

int arr[10];

printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);

其实3个玩意打印出来结果是一样的。但是我们要理解它们的本质:

  1. arr是数组名,数组名表示数组首元素地址,也就是arr[0]的地址。
  2. &arr[0],就是数组首元素的地址。
  3. &arr,数组名表示整个数组,取出的是整个数组的地址。

“整个数组的地址”和“数组首元素地址”在值上是一样的。但是如果+1,或者解引用,效果是不一样的,这就要牵扯对指针的理解了。指针类型决定了指针+1时跳过的步长。比如一个int*的指针+1会跳过4个字节,也就是跳过一个int,而一个char*的指针+1会跳过1个字节,也就是跳过了一个char。

在上面的例子中,arr和&arr[0]都表示数组首元素的地址,也就是一个int的地址,类型是int*,+1后会跳过一个int,也就是跳过4个字节。而&arr表示整个数组的地址,如果+1,会跳过整个数组。观察一下以下程序:

#include <stdio.h>

int main()
{
	int arr[10];

	printf("        arr = %p\n", arr);
	printf("    arr + 1 = %p\n", arr + 1);
	printf("    &arr[0] = %p\n", &arr[0]);
	printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
	printf("       &arr = %p\n", &arr);
	printf("   &arr + 1 = %p\n", &arr + 1);

	return 0;
}

输出结果如下:
在这里插入图片描述
观察到确实是这样的。arr+1和&arr[0]+1都跳过4个字节,而&arr+1跳过了16进制的0x28,也就是10进制的40个字节。

那二维数组的数组名表示什么呢?实际上,二维数组的数组名也表示首元素的地址,而二维数组的首元素是第一行,也就是说,二维数组的数组名表示第一行的地址。比如一个二维数组是int arr[3][5];,数组名arr表示的是第一行的地址,也就是一个容量是5个int的一维数组的地址,类型就是int (*)[5]。

数组和指针的区别和联系

数组和指针有什么区别呢?这个问题其实很奇怪,因为这2个玩意完全就不是一个东西。数组是一组相同类型的元素的集合,而指针是用来存储地址的,它们八竿子打不着。但是确实存在着一个关联,那就是前面讲解到的:数组名表示数组首元素的地址。根据这一点,就可以拿到一个指向数组首元素的指针。又由于数组在内存中是连续存放的,就可以通过这个指针来遍历这个数组。比如:

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	while (p < arr + 10)
	{
		printf("%d ", *p++);
	}

	return 0;
}

输出结果如下:
在这里插入图片描述

数组传参

有些时候,我们要把数组作为实参,传递给函数,函数的形参应该如何写呢?

先看一维数组传参。假设有这样的场景:

int arr[10];
test(arr);

此时函数test的形参应该如何写呢?

  1. 我们传过去了一个数组,自然可以用一个数组来接收:void test(int arr[10]);
  2. 数组的大小可以省略,而且建议省略:void test(int arr[]);
  3. 由于数组名表示首元素地址,我们本质上是传过去了一个地址,所以应该用一个指针来接收:void test(int* arr);
  4. 既然传过去的是地址,那前2种写法中,数组的大小重要吗?一点都不重要,所以你哪怕乱写其实也不会出问题,比如:void test(int arr[1000]);

接着看二维数组传参:

int arr[3][5];
test(arr);

此时test的形参应该如何写呢?

  1. 我们传过去了一个数组,自然可以用一个数组来接收:void test(int arr[3][5]);
  2. 注意:二维数组的行可以省略,但是列不能省略!比如:void test(int arr[][5]);
  3. 数组名表示首元素地址,二维数组的数组名表示第一行的地址,也就是一个容量为5个int的数组的地址,类型是int (*)[5],形参可以这么写:void test(int (*arr)[5]);
  4. 既然传过去的是地址,那前2种写法中,数组的大小重要吗?一点都不重要,所以你哪怕乱写其实也不会出问题,但是在二维数组中,列是很重要的,必须写对,但是行随便。比如:void test(int arr[1000][5]);

指针数组和数组指针

指针数组,本质是数组,只不过存放的是指针。比如:int* arr[10];就是一个指针数组,能存储10个int*类型的指针。

数组指针,本质是指针,只不过是指向数组的指针,存放的是数组的地址。详见我之前写过的一篇博客,戳这里跳转。

柔性数组

柔性数组是结构体内的大小可以变化的数组,这个知识点和动态内存管理也有联系,这里展开讲解就太长了。还好我之前写了一篇博客来讲解这个知识点,戳这里跳转。

总结

现在我们可以回答开头的问题了。

  1. 一维数组和二维数组在定义时,哪些大小可以省略,哪些不可以省略?如果可以省略,在什么时候是可以省略的呢?一维数组如果初始化,可以不指定大小,编译器会根据初始化的情况来给数组开辟空间。二维数组如果初始化,行可以省略,但是列不能省略,编译器会根据初始化的情况来决定有几行。
  2. 一维数组和二维数组在内存中是如何存储的?数组在内存中是连续存放的,随着数组下标的增长,地址是由低到高变化的,这点对一维数组和二维数组都适用。
  3. 一维数组和二维数组的数组名分别表示什么意思?数组名表示首元素地址,但是有2个例外:sizeof(数组名),&数组名,数组名都表示整个数组。除此之外,数组名都表示首元素地址,其中二维数组的“首元素”指的是第一行。
  4. sizeof(数组名) 和 &数组名 分别表示什么?sizeof(数组名)求的是整个数组的大小,&数组名取出来的是整个数组的地址。
  5. 一维数组和二维数组传参时,形参应该如何写?可以用数组接收,一维数组的大小可以省略,二维数组的行可以省略,列不能省略。对于省略的大小,其实乱写也是符合语法的,但是不建议。除此之外,根据“数组名表示数组首元素地址”这个知识点,也可以用指针接收。
  6. 数组和指针有什么联系?有什么区别?数组是一组相同类型元素的集合,指针是存储地址的。它们之间被“数组名”这个桥梁关联起来,因为数组名表示数组首元素地址。
  7. 如何使用指针来遍历一维数组和二维数组?根据数组在内存中是连续存储的,只要拿到首元素地址,就能遍历整个数组。
  8. 如何理解数组指针和指针数组?数组指针是一个指针,存储的是数组的地址。指针数组是一个数组,存储的元素类型是指针。
  9. 什么是柔性数组?柔性数组是结构体内的最后一个成员数组,且大小可以变化(大小不确定)的。管理柔性数组,要使用动态内存管理的方式。

感谢大家的阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/490469.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Git】制造冲突以及解决冲突的详细方法

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

CentOS安装Redis数据库流程by阿里云服务器

使用阿里云服务器ECS安装Redis数据库流程&#xff0c;操作系统为CentOS 7.6镜像&#xff0c;在CentOS上安装Redis 4.0.14&#xff0c;云服务器选择的是持久内存型re6p实例&#xff0c;新手站长分享阿里云CentOS服务器安装Redis流程方法&#xff1a; 目录 在CentOS系统中部署R…

2023-05-04 线性DP_力扣练习

线性DP的力扣题目练习 这一章将会介绍线性动态规划的相关概念和经典问题&#xff0c;并给出一些练习题供大家演练。 用动态规划解决问题的过程有以下几个关键点&#xff1a;状态定义&#xff0c;状态的转移&#xff0c;初始化和边界条件。 状态定义 就是定义子问题&#xff…

【IM苹果推iMessage】苹果真机推送自动分配任务,自动分配任务,让您瞄准中高端客户

推荐内容IMESSGAE相关 作者✈️IMEAE推荐内容iMessage苹果推软件 *** 点击即可查看作者要求内容信息作者✈️IMEAE推荐内容1.家庭推内容 *** 点击即可查看作者要求内容信息作者✈️IMEAE推荐内容2.相册推 *** 点击即可查看作者要求内容信息作者✈️IMEAE推荐内容3.日历推 *** …

代码命名规范的套路是真优雅呀,命名如歌,代码如诗

日常编码中&#xff0c;代码的命名是个大的学问。能快速的看懂开源软件的代码结构和意图&#xff0c;也是一项必备的能力。那它们有什么规律呢&#xff1f; Java项目的代码结构&#xff0c;能够体现它的设计理念。Java采用长命名的方式来规范类的命名&#xff0c;能够自己表达…

ansible常用命令

目录 1、列出默认清单文件中的所有受管主机 2. 列出自定义清单文件中的所有受管主机&#xff08;自定义清单文件&#xff1a;inventory&#xff09; 3、运行playbook 4、创建需要输入文件密码的加密的文件 5、创建用密码文件的加密的文件 6、查看加密的文件内容 7、向已有…

学会使用Git,看这一篇文章就够了

文章目录 一、背景二、Git的安装2.1 Windows下安装Git&#xff1a;下载安装包安装Git配置Git 2.2 Linux下安装Git&#xff1a;更新系统安装Git配置Git 三、Git 基本使用3.1 初始化 Git 仓库3.2添加文件3.3 提交代码3.4 查看历史记录3.5创建分支3.6 修改文件3.7 查看文件状态3.8…

【实用工具】JSR-269 插入式注解处理器AbstractProcessor

JSR-269原理浅析 初次使用lombok时&#xff0c;都需要在idea安装lombok插件&#xff0c;这让我们怀疑lombok的实现是通过提供自己的编译器实现的&#xff0c;然而实际情况并非如此&#xff0c;在脱离idea使用javac编译时&#xff0c;只要类路径有lombok的jar包&#xff0c;项目…

Android-源码分析-分析手机热点里的AP Band(频段)被隐藏/置灰的原因?

本博文记录寻找手机热点中AP Band(频段)被隐藏/置灰的原因&#xff0c;相似问题同理去查找解决。 先放上一张MTK平台手机-热点-AP Band界面效果图&#xff1a; 很明显&#xff0c;界面中的AP Band选项变灰&#xff0c;无法点击编辑修改内容&#xff0c;如果是AP Band 被隐藏或…

C#,生信软件实践(01)——DNA序列数据库FASTA文件合并工具的源代码

1 生物信息学简介 生物信息学&#xff08;BioInformatics&#xff09;是研究生物信息的采集、处理、存储、传播&#xff0c;分析和解释等各方面的学科&#xff0c;也是随着生命科学和计算机科学的迅猛发展&#xff0c;生命科学和计算机科学相结合形成的一门新学科。它通过综合…

JavaWeb ( 四 ) JavaEE

2.JavaEE 2.1.Java版本 J2SE : 适用于桌面系统的Java 2平台标准版&#xff08;Java 2 Platform Standard Edition&#xff0c;J2SE&#xff09; J2EE : 适用于创建服务器应用程序和服务的Java 2平台企业版&#xff08;Java 2 Platform Enterprise Edition&#xff0c;J2EE&a…

学企业管理

工业社会的代表产品是交通运输设备&#xff0c;如火车/高铁、汽车、飞机/火箭、船舶/航母&#xff0c;其核心是发动机。信息社会的代表产品是计算设备&#xff0c;如大型机小型机、工作站/PC台式机电脑/PC笔记本电脑/PC平板电脑、智能手机。 汽车这个产品&#xff0c;既属于高精…

调试别人的API,一般有哪些步骤?

当我们使用了一些由别人实现的API接口时&#xff0c;该如何进行调试呢&#xff1f;当我们使用的API返回一些意想不到错误时&#xff0c;该怎么办呢?这个问题可能是由于用户输入或者API本身&#xff0c;或者其他完全无关的内容等引起的。调试是我们进行定位并修复由单个API调用…

漫天花雨HTML特效+3D相册

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

软件工程期末复习(背题家速成)

文章目录 前言一、选择题1、第一章 软件工程综述2、第二章 软件过程3、第三章 可行性研究4、第四章 结构化需求分析5、第五章 结构化软件设计6、第六章 面向对象的需求分析7、第七章 第7章面向对象设计8、第八章 基于构件的开发9、第九章 软件项目的测试10、第十章 软件实施、维…

每天一道算法练习题--Day2 第一章 --算法专题 --- ----------位运算

我这里总结了几道位运算的题目分享给大家&#xff0c;分别是 136 和 137&#xff0c; 260 和 645&#xff0c; 总共加起来四道题。 四道题全部都是位运算的套路&#xff0c;如果你想练习位运算的话&#xff0c;不要错过哦&#xff5e;&#xff5e; 前菜 开始之前我们先了解下…

Umi 插件实战教程

引言 笔者最近开发了一款 umi 插件&#xff1a;plugin-umi-cmdk[1],该插件的功能主要是&#xff1a;在 umi 项目里可以方便的集成 cmd k &#xff0c;实现菜单等搜索。 主体功能并不复杂&#xff0c;但是在集成作为 umi 插件过程中踩了不少坑&#xff0c;主要是 umi 官方文档的…

【计算机网络】面试高频问题汇总及详细解答

【C语言部分】面试高频问题汇总及详细解答 【操作系统(Linux)】面试高频问题汇总及详细解答 【数据库】面试高频问题汇总及详细解答 本文目录 1. 简述网络七层参考模型及每一层的作用2. 简述静态路由和动态路由3. 说说有哪些路由协议&#xff0c;都是如何更新的4. 简述域名解析…

PostgreSQL Explain 复杂执行计划怎么看 --- 逐个分解PG执行计划的那些操作

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

Codeforces-Round-805-Div-3-E-Split-Into-Two-Sets

title: Codeforces Round 805 (Div. 3) E. Split Into Two Sets date: 2023-04-25 18:14:41 categories: AlgorithmCodeforces tags:codeforces并查集1600 E. Split Into Two Sets 题目大意 给你n组数&#xff0c;每组里面有两个数字&#xff0c;问你能不能把这n组数分为两组…