C数组篇(一维数组-下)

紧接着上篇,继续来讲讲C中的数组。

6.作为函数参数的数组名

当一个数组名作为函数参数传递给一个函数时会发生什么情况呢?

数组名的值就是一个指向数组第一个元素的指针,所以很容易明白此时传递给函数的是一份该指针的拷贝。函数如果执行了下标引用,实际上是对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素。

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

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

所以,此处不存在矛盾。所有的参数都是通过传值方式传递的。当然,如果你传递了一个指向变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以修改那个变量。尽管看上去并不明显,但数组名作为参数时发生的正式这种情况。这个参数(指针)实际上是通过传值方式传递的,函数得到的是该指针的一份拷贝,它可以被修改,但调用程序所传递的实参并不受影响。

下面是一个简单的函数,用于说明这些观点。

void strcpy(char *buffer, char const *string){  /* 复制字符,直到遇到NULL字节 */  while((*buffer++ = *string++) != '\0');}

它把第2个参数中的字符串复制到第1个参数所指向的缓冲区。调用程序的缓冲区将被修改,因为函数对参数进行了间接访问操作。但是,无论函数对参数(指针)如何进行修改,都不会修改调用程序的指针实参本身(但可能修改它所指向的内容)。

注意while语句中的*string++表达式。它取得string所指向的那个字符,并产生一个副作用,就是修改string,使它指向了下一个字符。用这种方式修改形参并不会影响调用程序的实参,因为只有传递给函数的那份拷贝进行了修改。

扫描二维码关注公众号,回复: 8729871 查看本文章

7.声明数组参数

这里有一个有趣的问题。如果你想把一个数组名参数传递给函数,正确的函数形参应该是怎样的?它应该被声明为一个指针还是一个数组?

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

int strlen(char *string);

int strlen(char string[]);

这个相等性暗示指针和数组名实际上是相等的,但千万不要被它糊弄了!这两个声明确实相等,但只是在当前这个上下文环境中。如果出现在别处,可能完全不同,就像前面讨论的那样。但对于数组形参,你可以使用任何一种形式的声明。

你可以使用任何一种形式的声明,但哪个“更加准确”呢?答案是指针。因为实参实际上是个指针,而不是数组。同样,sizeof(string)的值是指向字符的指针的长度,而不是数组的长度。

现在你应该清楚为什么函数原型中的一位数组形参无需写明它的元素数目,因为函数并不为数组参数分配内存空间。形参只是一个指针,它指向的是已经在其他地方分配好内存的空间。这个事实解释了为什么数组形参可以与任何长度的数组匹配----它实际传递的只是指向数组第一个元素的指针。另一方面,这种实现方法使函数无法知道数组的长度。如果需要知道数组的长度,它必须作为一个显式的参数传递给函数。

8.初始化

就像标量变量可以在它们的声明中进行初始化一样,数组也可以这样做。唯一的区别是数组的初始化需要一系列的值。这些值位于一对花括号中,每个值之间用逗号分隔。如下面的例子所示:

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

初始化列表给出的值逐个赋值给数组的各个元素,所以vector[0]获得的值是10,vector[1]获得的值是20,以此类推。

静态和自动初始化

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

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

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

9.不完整的初始化

在下面两个声明中会发生声明情况呢?

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

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

在这两种情况下,初始化值的数目和数组元素的数目并不匹配。第1个声明是错误的,我们没有办法把6个整型值装到5个整型变量中。但是,第2个声明却是合法的,它为数组的前4个元素提供了初始值,最后一个元素则初始化为0。

那么,我们可不可以省略列表中间的那些值呢?

int vector[5] = {1, 5};

编译器只知道初始值不够,但它无法知道缺少的是那些值。所以只允许省略最后几个初始值。

10.自动计算数组长度

这里是另一个有用技巧的例子

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

如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能够容纳所有的初始值的长度。如果初始值列表经常修改,这个技巧尤其有用。

11.字符数组的初始化

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

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

这个方法当然可行,但除了非常短的字符串,这种方法确实很笨拙。因此,语言标准提供了一种快速方法用于初始化字符数组:

char message[] = "Hello";

尽管它看上去像是一个字符串常量,实际上并不是。它只是前例的初始化列表的另一种写法。

如果它们看上去完全相同,你如何分辨字符串常量和这种初始化列表快速记法呢?它们是根据它们所处的上下文环境进行区分的。当用于初始化一个字符数组时,它就是一个初始化列表。在其他任何地方,它都表示一个字符串常量。

这里有一个例子:

char message1[] = "Hello";

char *message2 = "Hello";

这两个初始化看上去很像,但它们具有不同的含义。前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量。这个指针变量被初始化为指向这个字符串常量的存储位置,如下图所示:


发布了60 篇原创文章 · 获赞 18 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/BadAyase/article/details/101369236
今日推荐