深究C语言数组那点事(一维数组)

数组名和指针的相似:

在C语言中,几乎所有的数组在使用时,数组名都是作为一个指针常量,即数组第一个元素的地址,来使用的。其类型,完全取决于数组元素的类型,如果他们是int类型,那么数组名的类型就是指向“int的常量指针”,如果是其他类型,那么数组名就是指向其他类型的常量指针。如此看来是不是二者非常相似。

数组名与指针的不同:

如果仅以他俩的相似点就一概而论说他俩是一个玩意那就大错特错了,他们的不同点也非常干脆。例如数组具有确定数量的元素,可能你想说我可以定义一个不定长的数组啊,但是想一下,你在使用它时,是不是事先装数据,或者干脆空数组,但是无论怎样使用时数组的状态是确定的,长度无非就是空的,或者你装了一些数据,那么此刻的数组是不是就是固定的长度了。而指针仅仅只是一个标量值。编译器用数组名来记住数组内的元素属性,只有当数组名在表达式中使用时才会为他产生一个指针常量。

注意:这个值是指针常量,而不是指针变量。常量的值是无法修改的。这是因为指针常量指向的可是数组的起始位置,如果你真的把这个值修改了,那就相当于把整个数组的位置都给改变了。这貌似也并无不可,但是程序在完成链接后,内存中的数组的位置都是固定的,在程序运行时想要移动数组貌似已经晚了,所以数组名的值是一个指针常量,而非指针变量。

数组名不作为指针常量的情景:

只有两种情况时,数组名才不是作为指针常量出现的,即当数组名作为sizeof操作符或者&操作符的操作数时。sizeof返回整个数组的长度(数组元素个数*每个元素类型的字节数),而不是指向操作数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。

注:如果sizeof使用时,数组是函数外作为参数传入的,需要特别注意,数组是传址调用!!!,此时数组名是一个指针,具体分析可以看下我的另一篇博客:https://blog.csdn.net/qq_41004932/article/details/110749426

一些例子:

int a[10];
int b[10];
int *c;

c = &a[0];

在表达式中,&a[0]是一个指向数组第一个元素的指针。但那正是数组名本身的值,所以下面的赋值语句执行的任务效果是一样的:
 

c = a;

这条赋值语句说明了为什么理解表达式中的数组名的真正含义是非常重要的。如果数组名表示整个数组,这条语句就表示整个数组被复制到另一个新的数组。但事实上却完全不是这样,实际上赋值的是一个指针的拷贝,c指向的时数组的第一个元素。所以不难理解下面的表达式为啥是错的:

b = a;

更准确的说是非法的,因为因为不能使用赋值符把一个数组的所有元素复制到另一个数组。正确的做法是使用循环,遍历并复制每一个元素。

下面的语句:

a = c;

嗯,看起来没什么问题,a是一个数组指针,c是一个指针变量,但是很遗憾这也是非法的,因为a是数组啊!!!,他的指针变量可是一个常量,不可被改变的那种,c是变量,所以a的值可以直接给c,但反过来可就完全不是那么回事了啊。

下标引用:

来看下这条语句:

*( b + 3 )

首先,b的的值是一个指向整形的指针,所以3这个值要根据整形值的长度进行调整,即sizeof(int)*3,而加法的运算的结果就是另一个指向整形的指针,他所指向的是数组第一个元素向后移动3个整数长度的位置,然后间接访问操作访问这个新位置,或者取得那里的值(作为右值),或者把一个新的值存储于该处(左值),也就是说指针的加法(数字),会将这个数字根据指针的类型进行调整最后加到指针的长度,最后的访问位置其实就是从当前位置,向后访问这个数字这么多的位置。这个过程是不是很熟悉,这不就是基本上和下标引用一样吗。所以:除了优先级之外,下标引用和间接访问完全相同。例如下面的语句结果是完全相同的:

array[subscript]
*(array + (subscript))

在执行b+3时不难理解,就是指针b指向的位置向后偏移3个类型长度,但是如果是b-1呢?,这时要看做b + (-1),即b所指向的位置要向前偏移1个类型单位长度,如果此时b不是第一个元素的位置,那就没什么问题,但是要是首元素的话。。。,可就要小心了,指针的间接访问可以访问最后一个元素后面的一个位置,但是貌似只可以读,而坚决不能访问首元素的前一个位置,鬼知道他指向啥玩意。

给出一个好玩的例子(PS:看看就行,千万不要这么写,可读性太差了,别人以为你是傻子):

2[array]

这个表达式是不是一脸懵,不过他是个对的,先转换为间接表达式:

*(2 + (array) )

咦。。。是不是有点那味了,内部的括号是冗余的,将他去掉后:

*(2 + array )

而加法运算是可以交换位置的:

*(array + 2 )

这是不是就是非常直观了。他和array[2],访问的结果是一样的。但是千万千万不要这么写!!!

指针与下标:

综上在使用数组时,指针与下标都可以实现访问数组元素,可是如何去选用呢。比较公认的一个说法是,下标绝不会比指针更有效率,但指针有时会比下标更有效率。下面通过分别用下标和指针完成将一个数组的所有元素都设置为0来比较下二者的效率。

int arrray[10], a;
for ( a = 0; a < 0; a += 1)
    array[a] = 0;

为了对下标表达式求值,编译器在程序中插入指令,取得a的值,并把它与整形的长度(也就是4)相乘。这个乘法需要花费一定的时间和空间。

下面的表达式,执行的任务和前面的循环完全一样:

int array[10], *ap;
for (ap = array; ap < array + 10; ap++)
    *ap = 0;

这段代码不存在下标,但是还是有乘法运算的,即1这个数字要与整形的长度相乘,然后再与指针相加。但这里有一个很大的区别,循环执行时,执行乘法运算的都是两个相同的数(1or4)。结果,此段程序只是在编译时执行一次-----程序会增加一条指令,把4与指针相加。程序在运行时并不执行乘法运算。这个例子说明指针比下标更有效率的场合--当在数组中一次一步(或固定的数字)移动时,与固定数字相乘的运算在编译时完成,所以在运行时所需要的的指令就更少一些。在绝大多数机器上,程序会更小一些(编译后的)、更快一些。

再看下下面的片段:

a = get_value();
    array[1] = 0;
a = get_value();
    *(array + a) = 0;

这两段代码编译后所产生的代码就没有区别了,因为a的值可能是任何一个值,只有在程序运行时才可能知道。所以两种方案都会有乘法指令,用于对a的值的调整。此时二者之间效率是相同的。

指针的效率:

如果指针被正确使用,那么效率一定是远远高于下标的,但是由于不同的人写程序的风格不同、思路不同、还可能因为各种错误,从而导致指针乱指等等问题,指针未必就比下标更高效,相当于秘籍给你了,练成啥样就看自己喽。

数组和指针:

指针和数组虽然有着很多共同点,但是二者并不是相等的,看下下面的代码;

int a[5];
int *b;

这里a和b可以互换使用吗,它们都具有指针价值,都可以进行下标引用和间接访问,但是二者区别也还是蛮大的。

在声明数组时,编译器会根据声明所指定的元素数量为其保留内存空间,然后再创建数组名,他的值是一个常量,指向这段空间的起始位置。而声明指针变量时,编译器只为指针本身保留内存空间,他并不为任何整型值分配内存空间。而且,指针变量未被初始化为指向任何现有的内存空间,如果他是一个自动变量,甚至不会被初始化。所以在进行上述声明后,表达式*a是合法的,因为a是一个数组,有自己的内存空间,a会指向数组的首元素地址。*b却是非法的,因为*b将访问内存中某个不确定的位置,甚至会导致程序终止。但表达式b++可以通过编译,但a++却不行,因为a的值是个常量。

所以知晓二者之间的区别还是非常重要的。

作为函数参数的数组名:

这一块的问题之前在其他博文里已经写过了,但是为了整体的完整性,还是在叙述一遍。

当一个数组名作为函数的参数传入时,数组名的值就是一个指向数组第一个元素的指针,所以很容易明白此时传递给函数的是一份该指针的拷贝。如果函数执行了下标引用,实际上是对这个指针进行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素,因为一个常量指针无论拷贝多少次,最后指向的都是一个地址。

现在就可以很容易的说明C关于参数传递的表面上的矛盾之处。之前说过所有传递给函数的参数都是经过传值方式进行的,但数组名参数的行为却仿佛是通过传址调用传递的。传址调用是通过传递一个指向所需元素的指针,然后在函数中对该指针执行间接访问操作实现对数据的访问。作为参数的数组名是个指针,下标引用实际就是间接访问。

那么数组的传值调用又是表现在什么地方呢,传递给函数的是一份拷贝(指向数组起始位置的指针的拷贝),所以函数可以自由的操作它的指针形参,而不会担心修改对应的实参的指针。

所以此处并不矛盾:所有参数都是通过传值方式传递的。当然,如果传递了一个指向某个变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以改变那个变量。尽管初看上去并不太明显,但数组名作为参数时所发生的就是这种情况。这个参数(指针)实际上就是传值方式传递的,函数得到的是该指针的一份拷贝,他可以被修改,但调用程序所传递的实参并不受影响。PS:无论拷贝多少份都是指向同一片地址空间,所以最终都是可以在函数内部改变数组的值。

给出一个例子:

/*
**把第二个参数中的字符串复制到第一个参数指定的缓存区。
*/
strcpy(char *buffer, char const *string )
{
    /*
    **重复复制字符,知道遇到NULL字节
    */
    while( (*buffer++ = *string++) != '\0' );
}

注意while语句中的*string表达式。他取得string所指向的那个字符,并产生一个副作用,就是修改string,使他指向下一个字符。用这种方式并不会影响调用程序的实参,因为只有传递给函数的那份拷贝进行了修改。*string++那条语句,先将*string赋值*buffer,然后自身向后偏移一个单位。

 

声明数组参数:

调用函数时传递的实际是一个指针,所以函数的形参实际上是一个指针。但为了使程序员新手更容易上手一些,编译器也接受数组形式的函数形参。因此,下面两个函数原型是相等的:

int strlen( char *string );
int strlen(char string[] );

这两种声明实际上是相等的,但是只在当前这个上下文中,即第char *string声明的确实目的是一个数组,如果是在其他地方就可能完全不同。但对于数组形参可以选择任意一种方式的声明。

但是最准确的方式是使用指针,因为实参实际上是一个指针,而且在函数内部使用表达式sizeof string的值是指向字符的指针的长度,而不是数组的长度。

这就是为什么在函数原型中的数组形参不需要指明元素数目,因为传入的本质上就是一个指针,他指向的内存空间已经在其他地方分配好内存的空间。这其实也是为什么数组形参可以与任何长度的数组匹配--他传递的只是数组内第一个元素的指针。这种方法无法知道数组的长度。如果函数需要知道数组的长度,他必须以一个显式的参数传递给函数。

初始化:

数组的初始化需要一系列的值,这很好理解,很多人也都会:

int vector[5] = {10, 20, 30, 40, 50 };

静态和自动初始化:

数组初始化的方式类似于标量变量的初始化方式--也就是取决于他们的存储类型。存储于静态内存的数组只初始化一次,也就是在程序开始执行之前。程序并不执行指令把这些值放到合适的位置上,它们一开始就在那里里了。这个魔术是由链接器完成的,它用包含可执行程序的文件中合适的值对数组进行初始化,如果数组未被初始化,数组元素会被自动设置为0,当这个文件载入到内存中准备执行时,初始化后的数组和程序指令一样也被载入到内存中。因此,当程序执行时,静态数组已经初始化完毕。

但是,对于自动变量而言,初始化过程就没有那么浪漫了。因为自动变量位于运行时堆栈中, 执行流每次进入它们所在的代码块时,这类变量每次所处的内存位置可能并不相同。在程序开始之前,编译器没有办法对这些位置进行初始化。所以,自动变量在缺省情况下是未初始化的。如果自动变量的声明中给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被-条隐式的赋值语句初始化。这条隐式的赋值语句和普通的赋值语句一样需要时间和空间来执行。数组的问题在于初始化列表中可能有很多值,这就可能产生许多条赋值语句。对于那些非常庞大的数组,它的初始化时间可能非常可观。

因此,这里就需要权衡利弊。当数组的初始化局部于-一个函数(或代码块)时,你应该仔细考虑一下,在程序的执行流每次进入该函数(或代码块)时,每次都对数组进行重新初始化是不是值得。如果答案是否定的,你就把数组声明为static, 这样数组的初始化只需在程序开始前执行一次。


不完整的初始化:

下面两个声明会发生什么呢:
 

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

声明1肯定是错误的,因为容量是5个元素,装不下6个乃至更多啊。

声明2是正确的,因为当数组容量大于给定的初始值时,会将余下部分自动填0.

但是值得注意的是编译器只知道初始值不够给你补0,但是不知道缺哪些值,所以关键的初始值,一定要自己给出,因为编译器只会在最后补0.

自动计算数组长度:

在定义数组时可以不定长,如果声明中未给出数组的长度,编译器就把数组的长度设置为刚好能容纳所有的初始值的长度。如果初值列表经常修改,这个技巧就很有用。

int vector[] = {1, 2, 3, 4, 5 };

字符数组的初始化:

根据目前所学到的知识,你可能认为字符数组将以以下的形式进行初始化:

char message[] = {'H', 'e', 'l', 'l', 'o', 0 };

这种方式肯定不可行,太麻烦了,虽然在短字符串时也可以使用,但是如果好几十个字符呢,岂不是累死了。

使用为下面的方式:

char message[] = "Hello";

虽然看起来非常像一个字符串常量,然而并不是,只是前例化列表的另一种写法。

看下一个例子:

char message1[] = "Hello";
char *message2[] = "Hello";

这两个初始化,非常相似,但是含义却不同,第一个是初始化一个字符数组的元素,第二个则是一个真正的字符串常量。这个指针变量被初始化为指向这个字符传常量的存储位置。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值