生怕你看不懂的C语言“数组”详解(万字讲解)

一.前言

相信老铁们在高中数学中,没少接触到数列。因其优美简洁的形式和灵活多变的做法,让庄哥至今难以遗忘(好像高中数学也只记得这个了qwq)。而在我们的编程中,则有着由多个相同类型元素组成的数组。今天这篇博客,庄哥就带老铁们了解数组在编程领域中如何发挥着独特的作用。

二.一维数组的创建格式与初始化

2.1一维数组的创建格式

数组的定义:数组是一组相同类型元素的集合
创建格式:type_t arr_name [const_n]
type_t为元素创建类型
arr_name为数组的名称
[const_n]是常量表达式,用来指定数组大小(指元素的个数)

例如:

int arr[5];//创建一个大小为5的整形数组

数组创建的四种类型:

  1. 给定一个数组名称,并指定它的大小:int arr[10]
  2. 用const赋予变量a常属性,使得a能够指定数组的大小:
    int const a=10;
    int arr[a]
  3. 在[ ]中使用常量表达式指定数组的大小:int arr[5+6]
  4. 空数组,不指定数组的大小:int arr[ ]

温馨提示:在C99标准之前,[ ]中只给使用常量,不能使用变量。但在C99标准中,引入变长数组,即[ ]中可以使用变量,这时不可以对数组进行初始化。 而我们现在常用的VS编译器是不支持变长数组的。

2.2一维数组的初始化

创建好数组后,我们就需要对数组部内部进行初始化(可以理解为赋值)啦。在C语言中,数组属于构造数据类型。一个数组又可以拆分为多个数组元素,这些元素可以是基本数据类型或是构造类型。因此,根据数据类型的不同,数组又可以分为数值数组、字符数组、指针数组、结构数组等各种类别。(因为本文章是针对初学者而言,所以庄哥在这里只围绕数值数组与字符数组展开讲解)

温馨提示:数组的初始化是在编译时完成的

接下来依次列举数组初始化的7种情况:

//完全初始化
1int arr1[5]={1,2,3,4,5};
2int arr2[10]={0};
3char arr3[5]="abcd\0";
4char arr4[]={'a','b','c',65,'\0'};
//不完全初始化
5int arr5[5]={1,2,3};
6char arr6[5]={'a','b','c'};
7char arr7[5]="abcd";

下面庄哥依次讲解这7种情况:

2.2.1完全初始化

  • 类型:整形数组
  • 大小:5个元素
  • 元素:存放了五个整形元素1,2,3,4,5
  • 格式:int arr1[5]={1,2,3,4,5}

    非常标准的初始化
  • 类型:整形数组
  • 大小:10个元素
  • 元素:全是整形元素0

    有些老铁看到这里或许会想:既然数组元素全为0,那不对它进行初始化数组元素会不会是0呢?
    答案是:不会,若不对数组进行初始化,那么编译器会给它的所有元素随机初始化为一个值,如下图所示

    因此,在编写代码的时候各位老铁一定一定要记得初始化数组哦~
  • 类型:字符串(属于类,先不要关心它是什么,只需要知道它能以数组的形式访问,后期会介绍的哈)
  • 大小:5个元素
  • 元素:a、b、c、d、‘ \0 ’
  • 格式:char arr3[5]=“abcd\0”

    这里一些老铁会有疑问,为什么字符串的结尾庄哥要加上转义字符‘ \0 ’ ,它在这里有什么特殊的作用吗?

请看下面这段代码:

#include<stdio.h>
int main()
{
	char arr[4]="abcd";
	int i=0;
	while(arr[i]!='\0')//在循环次数不超过元素个数的情况下,用for循环打印就可以不需要'\0',感兴趣的老铁可以试一下哦~
	{
		printf("%c",arr[i]);
		i++;
	}
}

其实呀,‘ \0 ’作为字符串结束的标识符,若像上述代码不使用,在打印字符串的时候就会出现如下图的问题

打印abcd后出现了一串“烫烫烫…”。因为编译器没有遇到‘ \0 ’,这时就会以为字符串里的内容并没有结束,而abcd后的空间并未进行初始化,于是打印出“烫烫烫…”,理论上将一直打印下去,这里庄哥为了让老铁们好观察,还是让它停下来吧!

  • 类型:字符数组
  • 大小:5个元素
  • 元素:a、b、c、A、‘ \0 ’
  • 格式:char arr4[5]={‘a’,‘b’,‘c’,‘d’,‘\0’}

    对于这种情况老铁们需要注意三点:
    (1).当我们未指定数组的大小时,初始化往里面放进多少个元素,数组的大小就为多少。
    (2).虽然第四个元素初始化填入65,但是该数组为字符数组,编译器会通过ASCII码表进行翻译,将对应的字符‘ A ’存入第四个元素中。
    (3).字符数组与字符串同样要在结尾加上结束标识符‘ \0 ’

2.2.2不完全初始化

  • 类型:整型数组
  • 大小:5个元素
  • 元素:1,2,3,0,0

    初始化时,当我们放入的元素个数小于指定的数组大小,编译器会将剩下的元素自动初始化为0。
  • 类型:字符数组
  • 大小:5个元素
  • 元素:a、b、c、‘ \0 ’、‘ \0 ’

    在字符数组中,若进行不完全初始化,编译器则会将剩下的元素自动初始化为转义字符‘ \0 ’
  • 类型:字符串
  • 大小:5个元素
  • 元素:a、b、c、d、‘ \0 ’

    通过上图,可以看到字符串在进行不完全初始化时,编译器也会自动将剩下元素初始化为‘ \0 ’。因此,我们在指定字符串大小的时候,可以多留一个空位,让编译器自动将空位初始化为‘ \0 ’。

三.一维数组的使用

那么创建好数组后,我们又该如何调用其中的元素呢?

这就需要用到下标引用操作符‘ [ ] ’,又称数组访问操作符。

  1. 格式:数组名 + [ 下标编号 ] 例如:arr[0]

  2. 注意区分:
    (1). 在创建数组的时候,[ ]中存入的是数组的大小,这时[ ]不是操作符,而是所需要的语法格式。
    (2). 在使用数组的时候,[ ]中存入的是数组元素的下标

  3. 重点:数组元素的下标是从0开始

我们试着打印出数组元素:

#include<stdio.h>
int main()
{
	int arr[5]={1,2,3,4,5};
	int i;
	for(i=0;i<5;i++)//思考:i为什么不小于6呢?
	{
		printf("%d",arr[i]);//通过下标引用操作符访问数组的元素
	}
	return 0;
}

我们也可以通过监视窗口直接观察到数组的每一个元素及其下标

冷知识:在使用数组时,格式也可以写成 i[arr]
老铁们现在只需要记住[ ]只是个操作符,正如5+3可以写成3+5一样,其本质原因庄哥会在后期指针的章节提及,这里先留个印象。

四.一维数组在内存中的存储

接下来我们再深层次探讨数组的本质

数组在内存内存中是连续存放的

每一个方框代表着一个数组元素,随着下标的增长,元素的存储地址由低到高。当元素类型为整形的时候,一个元素则需要四个字节来存储,即一个方框对应着四个地址;当元素类型为字符的时候,一个元素则需要一个字节来存储,即一个方框对应着一个地址。

下面通过打印出数组元素的地址加深理解

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i;
	for (i = 0; i < 10; i++)
	{
		printf("%p\n", &arr[i]);//地址的打印格式为‘%p’
	}
	return 0;
}

从运行结果可以以看出,每个地址相差了4个字节,正好为一个整形元素。由此可以得出:数组在内存内存中是连续存放的。
通过下图更加直观的看到数组在内存中的存储形式。

注:上图每一个方框代表一个地址,即一个字节。

五.二维数组的创建格式与初始化

上面讲完一维数组,下面我们就继续来探讨二维数组。

5.1二维数组的创建格式

格式:type_t arr_name [const_n][const_n]
举例:int arr[5][5]
区别:比一维数组多一个[ ]
含义:第一个[ ]代表着行,第二个[ ]代表着列。比如上述的例子中,就是一个5行5列的二维数组。
其它创建格式:int arr[ ][5],二维数组的创建只能省略掉行,但不能省略掉列

老铁们可以将二维数组想象成一个棋盘帮助理解,或类比线性代数中的矩阵。

5.2二维数组的初始化

与一维数组一样,二维数组也只针对字符数组和整型数组展开讲解
二维数组初始化的大致分为以下4种情况

//完全初始化
1.int arr1[2][3]={{1,2,3},{4,5,6}};
2.int arr2[2][3]={1,2,3,4,5,6};
3.char arr3[2][3]={'a','b','c','d','e','f'};
//不完全初始化
4.char arr4[2][3]={"abc","d"};
  • 大小:6个元素
  • 元素:第一行:1,2,3;第二行:4,5,6
  • 格式:外面的{ }表示有多少行,里面的{ }表示有多少列

    二维数组中,行与列的下标也是从0开始
    其实也可以理解为,二维数组就是由多个相邻的一维数组组合而成的。
  • 大小:6个元素
  • 元素:第一行:1,2,3;第二行:4,5,6

    虽然比上一种初始化方式少了表示列的{ },但是运行结果与前者相同,说明表示列的{ }可以省略。在行数较少时,庄哥并不建议这样做,因为加上{ }明显更加直观,更有利于我们检查。但行数较多时那就另一回事了。
  • 大小:6个元素
  • 元素:第一行:a,b,c;第二行:d,e,f
    格式:int arr3[2][3]={‘a’,‘b’,‘c’,‘d’,‘e’,‘f’};

    通过监视窗口可以看到,二维字符数组的初始化也分为行与列。但在这里庄哥想强调的是,‘ \0 ’仅仅只是作为字符串结束的标识符,那么字符串和字符数组中一定要包含吗?
    其实不一定,请看下面这段代码:
#include<stdio.h>
int main()
{
	char arr3[2][3] = { 'a','b','c','d','e','f'};
	int i,j;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%c", arr3[i][j]);
		}
	}
}

就算我不加‘ \0 ’,也不影响我打印字符数组。所以,‘ \0 ’仅仅只是作为一种标识符而已,编译器遇到’ \0 ‘只会停止对它打印,而不会停止对后续内容的打印。

例如我在字符数组中插入‘ \0 ’:

#include<stdio.h>
int main()
{
	char arr3[2][3] = { 'a','\0','c','d','\0','f'};
	int i,j;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%c", arr3[i][j]);
		}
	}
}


由运行结果可以看出,编译器跳过对‘\0’的打印,但不影响对后续内容的打印。不过,因为其中‘\0’将ac与df隔开,所以这里ac与df是两个字符串。

  • 大小:6个元素
  • 元素:第一行:a、b、c;第二行:d、\0、\0
  • 格式:char arr4[2][3]={“abc”,“d”};

    字符串同样可以通过二维数组来表示,而对于未初始化的部分,编译器自动初始化为‘\0’。对于整形数组未初始化部分,编译器则会初始化为0,这里就不再过多说明了。

六.二维数组的使用

二维数组的使用也是通过下标的方式

使用方法与一维数组类似,我们直接尝试打印二维数组:

#include<stdio.h>
int main()
{
	int arr[2][3] = { 1,2,3,4,5,6 };
	int i,j;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d", arr[i][j]);
		}
		printf("\n");//打印完一行后,进行换行
	}
}

运行结果:

注意:二维数组中,行与列的下标同样是从0开始

6.1下标为什么从0开始

看到这里,老铁们会不会很疑惑为什么数组的下标要从0开始呢,从1开始不是更符合我们的常规思想吗?

其实不然,当我们创建数组的时候,编译器会根据以下的寻址公式在内存中开辟空间
a[i]_address=base_address+i*data_type_size

当下标从1开始时,公式就变成这样

a[i]_address=base_address+(i-1)*data_type_size
可以看到,这时编译器再寻找该数组元素的地址时,都要-1。或许在现在看来,这小小的-1是多么的渺小。但在工程里,要创建的数组有很多个,这时每次寻址都要-1,将会减慢我们程序的运行速度并且占用内存。对于精益求精的前辈们来说,这是绝对不允许的,因此我们的下标就从0开始。

七.二维数组在内存中的存储

像一维数组一样,我们尝试打印二维数组每一个元素的地址

#include <stdio.h>
int main()
{
	int arr[2][3],i,j;
	for(i=0;i<2;i++)
	{
		for(j=0;j<3;j++)
		{
			printf("%p",&arr[i][j]);
		}
	}
	return 0;
}

可以看出,即使二维数组在形式上与一维数组有所不同,但是元素的地址也是由低地址向高地址排列,且每个地址之间相差4个字节。

所以,前面介绍二维数组初始化时,庄哥说二维数组也可以理解为多个相邻的一维数组组成,请看下面图解。

八.关于数组越界问题

  1. 越界原因:数组的下标是有范围限制的,如果在使用含有n个元素的数组时,下标小于0或大于n-1,就属于数组越界访问了
  2. 温馨提示:在C语言中是不做数组下标越界检查,编译器也不一定报错,但是代码本身肯定是有问题的。所以,各位老铁在使用数组时,一定要谨慎,防止数组越界访问。

其实在前面介绍一维数组初始化的时候,就已经涉及数组越界的问题了。

不知老铁是否还记得一维字符数组初始化时,出现“烫烫烫…”的原因(忘记的老铁可以返回去看一下代码哈)。前面庄哥说的原因是:没有出现字符串结束标识符‘ \0 ’,告诉编译器停止打印。但这里庄哥告诉你们更加本质的原因:超出数组合法空间的访问。

下面请看代码:

#include<stdio.h>
int main()
{
	char arr[4]={1,2,3,4};
	int i=0;
	for(i=0;i<4;i++)
	{
		printf("%c",arr[i]);
	}
}

调试代码后,我们调出内存窗口。可以看到,那些在数组后面未初始化的空间中存入的都是"cc cc cc cc…“这样的十六进制数字。而在数组越界访问时,系统通常读取一次内存空间,就会向后访问两个字节,即为"cc cc”,而这个十六进制数字所对应的ASCII码表上的“ 烫 ”。因此编译器在未遇到’ \0 '时,越界打印的字符就是“ 烫 ”。偶尔会剩一个字节,即为"cc",它对应的字符是“ ?”,所以有时候在末端会冒个问号。

九.数组作为函数参数

那么说了这么久的数组,数组名在C语言中到底代表着什么含义呢?
我们通过下面这道题来进行探讨:
用冒泡排序法对一个数组进行从小到大排序

首先,我们得知道冒泡排序法的原理:冒泡排序的意思就是对一个无序数组进行排序,依次比较两个相邻数之间的较大值,较大值的下标如果小于较小值,那么就将它们两个的位置进行交换,直到数组呈现随着下标的增大,元素的值也不断增大的结果。

看完流程图后,庄哥直接上代码(老铁们不急着自己敲哦,可以先复制到编译器上运行一下~)

#include<stdio.h>
void bubble_sort(int arr[])
{
	int i,j,tmp,sz;
	sz = sizeof(arr) / sizeof(arr[0]);//不懂这一步的老铁先记住:sizeof是一种计算字节大小的单目运算符,通过此种方法可以计算出数组含有的元素个数,本文后面会讲到
	for (i = 0; i <sz-1; i++)
	{
		for (j = 0; j < sz - i-1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

int main()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 },k;
	bubble_sort(arr);
	for (k = 0; k < 10; k++)
	{
		printf("%d ", arr[k]);
	}
	return 0;
}

运行后,老铁们会发现没有元素进行交换
这到底是怎么回事呢?
我们赶紧来调试一下:

发现sz的值怎么是1呀,不应该是数组的元素个数吗,这时我们猜测会不会arr里存放东西的字节数与arr[0]里存放的东西字节数相等?
温馨提示:这是X86的编译环境,X64的运行结果会跟上述不一样
于是我们赶紧打开监视窗口,看看arr里面究竟放着什么呢?

通过上图可以看到,arr的值为0x0057fb9c,这不就是一个地址吗。竟然是地址的话,那这个地址又是谁的呢?
庄哥又将所有元素的地址取出:

发现数组名中存放的地址与首元素的地址相同,于是我们得出结论

数组名就是首元素地址
但是有两个例外
(1).sizeof(数组名),可以计算整个数组的字节大小,此时的数组名代表整个数组
(2).&数组名,取出的是数组的地址,此时数组名也代表整个数组

这时敏锐的老铁就疑惑了,既然sizeof中的数组名代表的是整个数组,那为什么在刚刚的冒泡排序法中数组名只有4个字节呢,不应该是40个字节吗?

其实呀,在数组传参的时候,数组名只是作为首元素的地址传给了目标函数
所以,即使在函数形参部分写成数组的形式: int arr[ ] 表示的依然是一个指针,而不是数组。

既然如此,那么我们就应该在传参前就计算好数组的元素个数赋给一个变量,让这个变量将数组元素个数传给函数。

#include<stdio.h>
void bubble_sort(int arr[],int sz)
{
	int i,j,tmp;
	for (i = 0; i <sz-1; i++)
	{
		for (j = 0; j < sz - i-1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

int main()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 },k,sz;
	sz=sizeof(arr)/sizeof(arr[0]);//在传参前就计算数组元素的个数
	bubble_sort(arr,sz);
	for (k = 0; k < 10; k++)
	{
		printf("%d ", arr[k]);
	}
	return 0;
}

运行后,效果如下:

可以看到,我们的数组成功进行冒泡排序,将所有元素从小到大进行排序

十.总结

至此,数组基础部分庄哥就全部介绍完毕。本文中,部分内容关乎到算法与计算机底层逻辑,内容多且难,庄哥也是反复琢磨如何让老铁们能够一看就懂。但庄哥也希望老铁们能够静下心来,不清楚的地方要反复阅读,好好感受数组在C语言的奇妙作用。
下面附上庄哥的写作思路:

由于本人的专业水平有限,文章有需要改进的地方欢迎老铁们在评论区批评、指正,庄哥感激不尽!

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值