C数组知识点(越界风险、数组传递、栈溢出)
一、一维数组
1.数组初始化
数组初始化2.数组越界会导致的风险
数组越界数组越界就是访问数组元素的时候,索引超过了定义的数组长度,导致访问了申请内存空间之外的内存地址,这样会带来很大的风险。如上图中的b[10]赋值操作,就会导致风险。
首先,在b[10]=100;语句前打了断点,这时候分别打印下a、b数组的地址,在打印一下a的各元素值。
过掉断点,再进行a数组各元素的打印。
这时候会发现,a[2]的值竟然由3变成了100。这个数值的改变其实就是数组b的越界操作b[10]=100;导致的,接下来分析一下原因。
由上面打印的a、b数组的地址分析:
a数组的起始地址:0x00007ffeefbff520
b数组的起始地址:0x00007ffeefbff500
a比b先进行定义,而a的地址高于b的地址,说明栈空间是由上至下(高地址到低地址)分配空间的。
通过打印b[0]和b[1]的地址:0x00007ffeefbff500和0x00007ffeefbff504可知一个元素分配了四个字节空间
故可推知:b[4]地址为0x00007ffeefbff510,与数组a的首地址相差16字节空间,打印证实一下
所以可以类比推测,如果用数组b的话,只要使用下标8~12便能访问到a数组的1~5号元素,果不其然
通过对b[10]的赋值操作,成功的对a[2]的元素值进行了更改。
故数组越界访问是很危险的,有可能会改掉我们无法预料到的地址空间的数值,甚至可能导致程序崩溃
3.数组打印,数组的传递(用一维举例)
首先在main函数中初始化一个数组,而后调用函数传入数组,对数组元素打印。
在打印的时候,有时候我们会这样判断数组的长度,企图利用sizeof求出数组总长度,除以每个元素的长度,就能求出元素的个数
但输出却不尽人意
按理来说数组初始化五个整形元素,大小应该是20字节,而一个int元素是4字节,应该是五个元素都能打印出来。为什么会这样?我们分别打印一下sizeof(arr)和sizeof(int)
打印发现sizeof(int)正常,但是sizeof(arr)却不是我们想象中的20字节,这是为什么呢。接下来我们再分别打印一下main函数中a数组和子函数中arr数组的类型。
可以看出a和arr完全不是同一种类型,a是数组类型,而arr却是(int *)类型。
其实是因为在main函数中定义的数组a的类型为(int [5])类型,但是经过参数传递到arr_print()方法中的,并不是这个数组本身,而是传的一个(int *)类型的指针变量,即子函数中的arr,其实是一个指针变量,该指针变量中保存着a数组的首地址
故当我们使用sizeof(arr)/sizeof(int)来计算数组的元素个数的时候,其实sizeof(arr)计算的结果是指针变量
arr的大小,而不是数组的大小。
总结一下:一维数组名其实就是代表了该数组的首元素地址,数组名作为参数传递的时候,传递的是数组的首地址(由指针变量装载),而不是整个数组
4.栈空间溢出
1.一般使用数组不会导致栈空间溢出(stackoverflow),当程序运行的时候,运行到某一个函数时,系统会将函数压入栈,为该函数分配一定大小的栈空间,windows下是1MB,linux系统下是10MB(可以更改),当我们初始化数组时,如果数组的长度过大,便会导致栈空间溢出,报stackoverflow的错误。
2.第二种常见的栈溢出,便是递归所导致的栈溢出,由于递归层层调用函数,如果递归次数过多,便有可能将分配到的栈空间消耗殆尽,发生栈溢出。
二、二维数组
1.初始化
2.数组输出,数组传递
首先使用上图中的a数组进行测试说明。先写一个负责输出的子函数,第一个参数接收数组,第二个参数为数组行数。
这里要对二维数组的传递做一下说明:(使用的是上面定义的a数组)
二维数组的传递,跟一维数组不太一样
(1)一维数组名作为参数传递,其实子函数接收到的是一个(int *)类型的整型指针变量,其中装载着数组的首元素的地址
(2)二维数组名作为参数传递,其实子函数接收到的是一个{ int(*)[4] }类型的指针变量(上图中arr),即arr是数组指针。如下图打印,可以看出a的类型为int [3][4];而传到子函数中后,arr为int (*)[4]类型。
本例中二维数组a是三行四列,该数组指针arr指向二维数组的第一行(即第一行的一维数组int[4])的内存地址空间,注意是第一行的一维数组的整个地址空间,而不是数组的首元素的地址空间,即当我们对该数组指针进行自加移动操作,移动步长不再是一个整形变量的长度,而是一个int[4]数组的长度,即如果我们对(arr+1)进行内容打印,其实是比arr的内容多出16个字节,正好是每一行的四个整形元素所占空间,如图
arr作为一个数组指针,指向a数组的第一行的一维数组的地址空间,(arr+1)后便是指向第二行的一维数组的地址空间。
弄清楚二维数组的传递后,做一个小拓展,我们将原本的子函数做如下改动。
将arr[][]的列数由原来的4变为了3,而在传参的时候,将行数改成4行,即原本的a数组是3*4的结构,现在我们试图将数组按照4*3的结构传过去试图打印。
其实这样表面上是更改了数组的形状范围,看似会发生错误,但其实是可以正常输出的,C的灵活就在于此处。因为a数组的地址在内存中是连续的,原来的三行四列,只是在那一片连续的地址空间中,分为了三份,每份16字节,而我们传参的时候,把行列互换了,其实就是将a原本的连续地址空间分为了四份,每一份12字节,并没有超出数组a的地址空间,所以是可以正常打印的,打印结果如下
这种做法会导致子函数调用的时候报警告,因为辨析器判断出我们子函数中arr的接收类型与a数组的类型不匹配,虽然不影响打印输出,但是也可以通过一个类型转换让警告消失,即将a强转为我们设定的参数类型即可。
二维数组部分在指针笔记中也将进行较详细的记录。
三、字符数组
如图我们可以看见,无论是a还是b,我们在初始化的时候,定义的数组长度都要比赋的字符数多一。那么为什么数组长度要比赋值的字符数多1呢?
因为其实每个字符串都是以'\0'作为结束的,在我们对字符数组赋值时,系统会自动在末尾加上一个'\0'作为结束符。这样在我们输出打印字符串的时候,打印到'\0'时,就会停止。如果多留一个位置,'\0'就无法录入数组。
那么为什么一定要加'\0'呢,或者说,如果偏要写成b[5] = "hello";这样呢
这就要说%s打印了,众所周知%s是专门用来输出字符串的,而%s输出字符串的时候,它判断何时停止的标志就是'\0',如果我们对数组赋值的时候,没有留出'\0'的位置,或者没有手动加上'\0',那么打印就不会停止。
他(%s)首先会在字符数组的地址空间中取值打印出来,而后因为没有'\0'标志停止,打印就会继续打印b字符数组的后续地址空间中的数值,这时候就会出现我们所谓的"乱码"。如图用%s打印b[5]的结果:
那么乱码有时候也不是无穷打印的(如上图),那么打印什么时候结束呢。
答案是:当打印遇到后续地址空间中的0x00字节时,即'\0',才会停止打印。
注:%s虽然看起来是直接输出字符串,但其实依旧是通过for循环进行单个字符依次输出的方式实现的。