掌握了这些,才算真正了解C语言数组

news2024/11/25 11:29:05

在这里插入图片描述
也许你认为,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/478305.html

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

相关文章

TF-IDF (BigData, Data Mining)

TF-IDF&#xff08;term frequency–inverse document frequency&#xff09;是一种用于信息检索与数据挖掘的常用加权技术。 TF是词频(Term Frequency)&#xff0c; IDF是逆文本频率指数(Inverse Document Frequency)。 简介 TF-IDF是一种统计方法&#xff0c;用以评估一字词…

【国际象棋】棋盘游戏-微信小程序开发流程详解

与中国象棋类似的&#xff0c;还有国际象棋&#xff0c;知道有人爱玩&#xff0c;于是凭着好奇心&#xff0c;网上研究了一下&#xff0c;跟中国象棋有相似之处&#xff0c;玩法是有些许不一样&#xff0c;不知道象棋最早出于谁之手呢&#xff0c;抽空做一做&#xff0c;最终完…

倾斜摄影超大场景的三维模型的顶层合并,提升模型在WEB三维展示效果

倾斜摄影超大场景的三维模型的顶层合并&#xff0c;提升模型在WEB三维展示效果 倾斜摄影超大场景的三维模型的顶层合并后&#xff0c;可以采取以下措施来提升模型在WEB三维展示效果&#xff1a; 1、优化模型数据&#xff1a;对于倾斜摄影超大场景的三维模型&#xff0c;需要进…

网络安全:namp扫描工具

-sP可以扫描一个网段ip以及状态和基本信息&#xff0c;10.1.1.2-3就是扫描2和3这两个ip的主机 -p可以扫描指定ip对应主机的端口号&#xff0c;可以是一个范围 nmap简单扫描&#xff1a;nmap 地址 检查地址是否在线以及open的端口号 在端口开放&#xff0c;不一定可以与对方正常…

数据库之事务隔离级别详解

事务隔离级别详解 一、事务的四大特性&#xff08;ACID&#xff09;1. 原子性(atomicity)&#xff1a;2. 一致性(consistency)&#xff1a;3. 隔离性(isolation)&#xff1a;4. 持久性(durability)&#xff1a; 二、事务的四种隔离级别1. 读未提交(Read uncommitted)&#xff1…

WSL怎么使用本机进行代理联网

文章目录 WSL怎么使用本机代理进行联网问题来源设置v2rayN设置wsl总结参考 WSL怎么使用本机代理进行联网 问题来源 使用WSL克隆github的代码网速很慢&#xff0c;无响应&#xff0c;导致项目无法下载&#xff0c;真的愁人。就想到为WSL设置xx上网&#xff0c;是否就会好很多。…

Photoshop如何使用文字之实例演示?

文章目录 0.引言1.给图像素材添加透明水印2.创建路径文字3.创建每日一签海报4.给图像添加复杂水印5.制作个人简历模板 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对PS进行了学习&#xff0c;本文通过《Photoshop2021入门教程》及其配套素材结合网上相关资料进行学…

React | React组件化开发

✨ 个人主页&#xff1a;CoderHing &#x1f5a5;️ React .js专栏&#xff1a;React .js React组件化开发 &#x1f64b;‍♂️ 个人简介&#xff1a;一个不甘平庸的平凡人&#x1f36c; &#x1f4ab; 系列专栏&#xff1a;吊打面试官系列 16天学会Vue 11天学会React Node…

不良条件视觉感知专栏(一)任务前言

前言 随着深度学习的流行&#xff0c;CNN的强大特征学习能力给计算机视觉领域带来了巨大的提升。2D/3D目标检测、语义分割是常见的视觉感知任务&#xff0c;本专栏我们将围绕着它们展开阐述。 本教程禁止转载。同时&#xff0c;本教程来自知识星球【CV技术指南】更多技术教程&…

Photoshop如何使用图像调色之实例演示?

文章目录 0.引言1.将一张偏冷调的图像调整成暖调2.将图像调整成不同季节色彩倾向3.变换花朵的颜色4.创建人像轮廓风景5.修饰蓝天白云6.调换花草颜色 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对PS进行了学习&#xff0c;本文通过《Photoshop2021入门教程》及其配…

【经典论文解读】YOLACT 实例分割(YOLOv5、YOLOv8实例分割的基础)

前言 YOLACT是经典的单阶段、实时、实例分割方法&#xff0c;在YOLOv5和YOLOv8中的实例分割&#xff0c;也是基于 YOLACT实现的&#xff0c;有必要理解一下它的模型结构和设计思路。 论文&#xff1a;YOLACT: Real-time Instance Segmentation 开源地址&#xff1a;https://gi…

01-Shiro550漏洞流程

1. 漏洞原理 Apache Shiro框架提供了记住密码的功能&#xff08;RememberMe&#xff09;&#xff0c;用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值&#xff0c;先base64解码然后AES解密再反序列化&#xff0c;就导致了反序列化RCE漏洞。 那么…

hadoop伪分布式搭建教程

官方参数文档 Apache Hadoop 3.3.5 – HDFS Users Guide Hadoop是一个分布式存储和计算框架&#xff0c;由以下几个组件组成&#xff1a; 1. Hadoop Distributed File System (HDFS)&#xff1a;Hadoop分布式文件系统&#xff0c;用于存储大量数据&#xff0c;并提供高可靠性和…

Spring源码:动态代理的增强顺序(AOP与事务的先后)

前文&#xff1a; 《Spring AOP源码&#xff1a;开启注解读取》 《Spring AOP源码2&#xff1a;查找增强器》 《Spring AOP源码3&#xff1a;实现代理》 《Spring事务源码&#xff1a;创建代理类》 《Spring事务源码&#xff1a;事务创建》 《Spring事务源码&#xff1a;…

(05)基础强化:字符串拘留池,格式化,StringBuilder,垃圾回收,弱引用

一、复习 1.什么是接口&#xff1f;说说你对接口的理解。 &#xff08;提示&#xff1a;概念、语法、应用场景&#xff0c;与抽象类的区别。说出最特别的&#xff09; 接口是一种规范、标准&#xff0c;一种抽象的概念&#xff0c;所以本身无法实现&#…

Redis基础——Java客户端Jedis

2.1.Jedis客户端 Jedis的官网地址&#xff1a; https://github.com/redis/jedis 2.1.1.快速入门 我们先来个快速入门&#xff1a; 1&#xff09;引入依赖&#xff1a; <!--jedis--> <dependency><groupId>redis.clients</groupId><artifactId&…

山东专升本计算机第九章-信息安全

信息安全 计算机病毒 考点 4病毒的定义与特点 定义 • 一组人为设计的程序满足一定条件即被激活 特点 • 可执行性 • 破坏性 • 占用系统资源 • 破坏或删除程序或数据文件 • 传染性 • 潜伏性 • 隐蔽性 • 针对性 • 宏病毒只感染docx • 衍生性 • 抗反病毒软…

102-Linux_I/O复用方法之poll

文章目录 1.poll系统调用的作用2.poll的原型3.poll支持的事件类型4.poll实现TCP服务器(1)服务器端代码:(2)客户端代码:(3)运行结果截图: 1.poll系统调用的作用 poll 系统调用和 select 类似&#xff0c;也是在指定时间内轮询一定数量的文件描述符&#xff0c;以测试其中是否有…

docker容器原理及简单且详细的使用

docker原理简单介绍 docker是一种虚拟化容器技术。 虚拟化&#xff1a;早期为了节约成本和学习只有在宿主机中基于 kvm&#xff08;基于内核的虚拟机&#xff09;等技术虚拟出来完整的操作系统&#xff0c;而这个完整的操作系统会大量的占用宿主机的硬件资源&#xff0c;当创建…

spring的安装 -- IEDA-创建 Java 工程的jar包教程--以及spring5的核心组件

目录 Spring Spring 基本介绍 Spring5 下载 进入 Spring5 进入 Spring5 的 github 进入 Spring5 的 github下拉 Access to Binaries, 进入 Spring -----拷贝下载地址&#xff0c;打开--- -----选择 5.3.8, 点击进入, 即可下载- 在线文档: 离 线 文 档 : 离 线 API: …