智慧治水网站系统建设网站推广优化技巧
前言
数组是C语言中的一种自定义数据类型,它的使用非常广泛。但是很多新手在使用数组时,经常在一些细节上出问题,导致程序崩溃或者无法编译。今天,我就来详细聊聊数组的使用和我注意到的一些细节。
一、常见的数组类型与数组的创建
1. 常见数组类型
与常见指针类型类似,常见的数组类型也是与常见的数据类型一一对应的,这里简单列举几个最常用的。
int arr1[10]; //整型数组
char arr2[20]; //字符数组
double arr3[5]; //双精度浮点型数组
struct stru arr[6]; //结构体数组
2. 数组的创建
int arr1[10]; //创建数组
int arr2[5] = {0,0,0,0,0}; //创建数组并完全初始化
int arr3[10] = {0}; //创建数组并不完全初始化
int arr4[] = {1,2,3,4,5}; //创建未给定大小的数组并初始化
我们以整型数组为例,上方为整形数组的四种创建与初始化方式。
首先来介绍第一行,也是最基础的数组创建方式,“数据类型 + 数组名 + [数组大小]”。其中,数组名和[ ]之间不能出现空格,数组大小只能为常量(可以为标识符常量)且只能为正整数。那么我们来解读第一行,就是一个名为“arr1”,可以存放10个整型数据的数组(数组大小为10个整型,即40个字节),该数组的数据类型为"int[10]"。
我们可以用“sizeof()”来计算数组在内存中占用的空间大小,如下图。
接下来看第二行,数组arr2为一个大小为5的整型数组,它的五个数据均被初始化为0。其中花括号中的第一个0即对应数组的第一个数据,称为数组的第一个元素,以此类推。
同理,第三行的arr3数组为一个大小为10的整型数组,它的第一个元素被初始化为0,剩余的9个元素未初始化。
第四行的arr4数组在创建时未给定大小,那么它的大小就由初始化的元素个数决定。由此得出arr4的大小为5。
补充:变长数组 (VS编译器不支持)
变长数组与常规数组的区别在于,常规数组在定义时,其数组大小只能为常量,而变长数组的大小可以为变量。因此,变长数组可以精准地开辟内存空间,从而避免空间浪费。
3. 字符数组的初始化
char ch1[] = {'a','b','c'};
char ch2[] = "abc";
字符数组可以按照常规数组初始化方式进行初始化,也可以按照上图第二行的方式,直接用字符串进行初始化。那么上图中的ch1,ch2的大小分别是多少呢?显然,ch1的大小为3,那么ch2的大小也为3吗?我们来看下图。
我们发现,数组ch2的大小为4,这又是为什么呢?我们不妨直接将这两个数组以字符串形式打印出来看看。
可以看见,第一行出现了许多乱码,而第二行则是正常的"abc"。这是为什么呢?关于这个问题,等到介绍数组在内存中的存储方式时,我再来详细解释。
二、数组元素的访问
当我们创建了一个数组后,我们应该如何调用数组中的元素呢?换句话说,如何访问数组的元素呢?
1. 下标引用操作符
// [] - 下标引用操作符 int arr[10];
arr[0] = 1;
数组的每个元素都有自己对应的下标,通过其下标即可访问该元素。我们可以通过下标引用操作符“[ ]”。如图,arr为一个大小为10的整型数组,当我们创建了数组之后,我们通过“数组名 + [元素下标]”,访问了arr数组中下标为0的元素,并将其赋值为1。
注意,上方代码中创建数组和访问数组元素均使用了“[ ]”,但这两个“[ ]”的含义不同,第一行的“[ ]”代表arr为一个"int[10]"类型的数组,而第二行的“[ ]”则是下标引用操作符,用来访问数组元素。
数组中第n个元素的下标为n-1,例如,第一个元素的下标为0。
2. 指针访问
int arr[10];
*(arr+1) = 2; //等价于: arr[1] = 2;
本质上,数组名就是指向数组起始地址的指针。由于数组arr为整型数组,故访问数组元素时,其数组名本质上为" int* "类型的指针,可通过指针的解引用操作符访问其指向的元素。图中“arr+1”即指针向后偏移一位,指向了数组中的第二个元素,因此等价于访问数组第二个元素。
int arr[10];
int* p = arr;
*(p+1) = 2;
同样地,我们也可以用其它指针储存arr所指向的地址,并通过该指针访问数组元素,如图中的指针p。
三、数组在内存中的存储
相信通过上面的访问方式,聪明的你一定已经猜到数组在内存中的存储形式了。数组在内存上占用一块连续的空间。我们通过VS的调试器来观察数组在内存中的存储,如下图。每个整型数据占用四个字节的空间,故地址每隔四存放一个数据。
而数组名arr即是首元素的地址,我们可以通过打印地址来查看。这也就解释了为什么数组元素可以通过指针的方式来访问。
接下来我们来解决之前字符数组留下来的问题。
通过监视窗口,我们可以看到,ch1中只有三个元素,相比之下,ch2多出了第四个元素'\0'。'\0'是一个字符,用来终止字符串。
由于ch2中存在'\0',打印ch2时才不会出现后面的随机值。而ch1中没有'\0',因此当ch1的元素全部打印完后,编译器会继续打印数组外的内容,由于未被赋值,这些内容通常为随机值,因此一直打印到遇到随机的'\0',打印才结束。
使用双引号" "引用的字符串的末尾都会自带一个'\0',这就是为什么用"abc"初始化ch2后会多出一个'\0'。
四、二维数组
1. 二维数组的创建
int arr1[3][3] = {0};
int arr2[][3] = {0};
二维数组有以上两种创建方式,其中前后两个“[ ]”中的数字我们分别称为行数和列数。二维数组的大小等于行数乘以列数。
在第一行中,我们给定了数组arr1的行数和列数,均为3,因此arr1的大小为9,即可容纳九个元素。同样,如果行数和列数都给定了,就可以选择不初始化。
在第二行中,我们给定了数组arr2的列数,但没有给定行数,这样一来数组的大小就根据初始化的值来确定了,如图中将第一个元素初始化为0,那么为了存储元素0,数组必须开辟一行的空间,由于一行有三列,因此数组arr2的大小为3。从中我们可以看出,为给定行数的二维数组的大小是由列数和初始化元素个数决定的。其大小必须为列数的整数倍,并且必须大于元素个数。
2. 二维数组元素的访问
int arr[3][3];
arr[0][0]=1;
二维数组的元素也是通过下标来访问的,只不过由于二维数组有行和列,因此下标也分为行标和列标。同样的,行标和列标也都是从0开始,如图中arr[0][0]即是访问第一行第一列的元素。
关于二维数组的指针访问,本文就不过多解释了,因为相对来说比较复杂,涉及到二维数组的原理。有兴趣的小伙伴可以自行推导。(提示:要用到二级指针)
3. 二维数组在内存中的存储
在我们理解二维数组时,我们把二维数组分成行和列,如上图。我们知道,一维数组在内存中是占用一块连续的空间。那么二维数组在内存中又是如何存储的呢?我们再次通过VS的内存窗口观察二维数组arr在内存中的存储情况。我们发现,二维数组在内存中也是连续存储的,并不是像我们理解的那样分为行和列来存储。
五、数组问题中一些常见的错误和技巧
1. 越界访问
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int a = arr[10];
我们来看上面这段代码。首先创建一个大小为10的整型数组并完全初始化,然后创建一个整型变量a,并初始化为arr数组中下标为10的元素。但是数组arr的大小为10,那么它最大的下标应该为9,也就是说不存在下标为10的元素。这就是所谓的越界访问。
由于数组创建时只开辟了10个整型大小的合法空间,而上方代码访问了第11个整型的空间,因此形成非法访问。
2. 初始化与修改数组元素
//错误示范
int arr[] = {1,2,3,4,5};
arr = {1,2,3,0,0};
char ch[] = "hello";
ch = "world";//正确做法
int arr[] = {1,2,3,4,5};
arr[3] = 0;
arr[4] = 0;
char ch[] = "hello";
ch = strcpy(ch,"world");
在讲数组的初始化时,我提到可以使用花括号和字符串进行初始化。但是要注意,当数组创建完毕并初始化后,就不能再使用花括号或字符串来修改数组了。如果数组创建完成后需要修改数组元素,则需通过下标访问需要修改的元素进行修改,若需要整体修改则可借助循环遍历数组。
3. 数组传参
void test(int* arr)
{arr[0] = arr[1];
}int main()
{int arr[3] = {0,1,2};test(arr);return 0;
}
之前介绍访问数组的方法时就提到,数组名其实就是指向数组首地址的指针,也就是说,数组传参其实也就等价于指针传参。在声明函数的参数时,若需接受数组,只需要声明一个同类型的指针变量即可。
结束语
以上就是我对数组的一些基础知识的总结啦。关于数组,我个人还是更喜欢将其理解为指针的一种变体,因为C语言中,指针可以说与内存息息相关,而数组则是通过一个指针(数组名)来管理该数组空间内的所有数据。不知道大家都是如何理解的呢?欢迎在评论区留言。