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

数组,相信大家都使用过。本文将由浅入深的讨论数组,探索一些更高级的数组话题,如多维数组、数组与指针及数组的初始化等。

一、一维数组

在讨论多维数组之前,先来学习下一维数组的知识。首先我们学习一个概念,它被许多人认为是C语言设计的一个缺陷。但实际上,这个概念是以一种相当优雅的方式把一些完全的不同的概念联系在了一起。

1.数组名

考虑下面声明:

int a;

int b[10];

我们把变量a称为标量,因为它是一个单一的值,这个变量的类型是一个整数。我们把变量b称为数组,因为它是一些值的集合。下标和数组名一起使用,用于标识该集合中某个特定的值。例如,b[0]标识数组b的第一个值。每个特定值都是一个标量,可以用于任何可以使用标量数据的上下文环境中。

b[4]的类型是整型,但b的类型又是什么?它所表示的又是什么?一个合乎逻辑的答案是它标识整个数组,但事实并非如此。在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型;如果数组元素是int,那么数组名的类型就是“指向int的常量指针”;如果是其他类型,那么数组名的类型就是“指向其他类型的常量指针”。

请不要根据这个事实得出数组和指针是相同的结论。数组具有一些和指针完全不同的特征。例如,数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。

注意这个值是指针常量,而不是指针变量。你不能修改常量的值。你只要稍微回想一下,就会认为这个限制是合理的;指针常量所指向的是内存中数组的起始位置,如果修改这个指针常量,唯一可行的操作就是把整个数组移动到内存的其他位置。但是,在程序完成链接之后,内存中数组的位置是固定的,所以当程序运行时,再想移动数组就为时已晚了。因此数组名的值是一个指针常量。

只有在两种场合下,数组名不用指针常量来表示---就是当数组名作为sizeof操作符或单目操作符&的操作数时。sizeof返回整个数组的长度,而不是指向数组的指针的长度。取一个数组名的地址产生的是一个指向数组的指针,而不是指向某个指针常量的指针。

举个例子:

int a[10];

int b[10];

int *c;

...

c = &a[0];

表达式&a[0]是一个指向数组第一个元素的指针。但那正是数组名本身的值,所以下面这条赋值语句和上面那条赋值语句所执行的任务是完全一样的。

c = a;

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

b = a;

是非法的,你不能使用赋值符把一个数组的所有元素复制到另一个数组。你必须使用一个循环,每次复制一个元素。

考虑下面这条语句:

a = c;

c被声明为了一个指针变量,这条语句看上去像是执行某种形式的指针赋值,把c的值复制到a,但这个赋值是非法的,和上面的例子一样:记住!在这个表达式中,a的值是个常量,不能被修改。

2.下标引用

在前面声明的上下文环境中,下面的表达式是什么意思?

*(b + 3)

首先,b的值是一个指向整型的指针,所以3这个值根据整型值的长度进行调整。b+3的结果是另一个指向整型的指针,它所指向的是数组第一个元素向后移3个整型长度的位置。然后,*间接访问操作符访问这个新的位置,当整个表达式是右值的情况下,它会取得那里的值,当整个表达式是左值的情况下,它会将某个新值存储于该处。

这个过程听上去是不是很熟悉?这是因为它和下标引用的执行过程完全相同。事实上,除了优先级外,下标引用和间接访问完全相同。例如,下面两个表达式是完全等同的:

array[subscript]

*(array + (subscript))

3.指针与下标

如果你可以互换指针表达式和下标表达式,那么你应该使用哪一个呢?这里并没有一个明确的答案,对于大多数人而言,下标更容易理解,尤其是在多维数组中。所以在可读性方面,下标有一定的优势。但在另一方面,这个选择可能会影响运行时效率。

假定这两种方法都正确,下标绝不会比指针更有效率,但指针有时会比下标更有效率。

为了理解这个效率问题,让我们来研究两个循环。它们执行相同的任务。首先,我们使用下标方案将数组中的所有元素都设置为0。

int array[10], i;

for (i = 0; i < 10; i++)

    array[i] = 0;

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

现在我们来看看下面这个循环,它所执行的任务和前面的循环完全一样。

int array[10], *p;

for (p = array; p < array + 10; p++)

    *p = 0;

尽管这里并不存在下标,但还是存在乘法运算。

现在这个乘法运算出现在for语句的调整部分。++的1必须与整型的长度相乘,然后再与指针相加。但这里存在一个重大区别:循环每次执行时,执行乘法运算的都是两个相同的数(自加运算的1和整型长度的4)。结果,这个乘法只在编译时执行一次----程序现在包含了一条指令,把4与指针相加。程序在运行时并不执行乘法运算。

这个例子说明了指针比下标更有效率的场合----当你在数组中1次1步(或某个固定的数字)地移动时,与固定数字相乘的运算在编译时完成,所以在运行时所需的指令就少一些。在绝大多数机器上,程序会更小一些、更快一些。

现在考虑下面的代码段:

a = get_value();                                  a = get_value();

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

两边的语句所产生的代码并无区别。a可能是任何值,在运行时方知。所以两种方案都需要乘法指令,用于对a的值进行调整。这个例子说明了指针和下标的效率完全相同的场合。

4.指针的效率

前面说过,指针有时比下标更有效率,前提是它们被正确的使用,它的结果可能不同,这取决于编译器和机器。然而,程序的效率主要取决于你所编写的代码,和使用下标一样,使用指针也很容易写出质量低劣的代码,而且通常这个可能性更大。

由于涉及到了汇编、篇幅、实用性等原因。这边不具体展开讲指针优化的写法,只对其作评价。

通常,综合评价来讲,通过指针优化的写法对效率的提升极为有限,而且会使得非常容易理解的代码变成“莫名其妙”的代码。对于极少数情况,这种写法值得使用,但在绝大多数情况下,为了一点点运行效率,使得程序难以维护,这是非常不值得的。

你很容易争辩说,经验丰富的C程序员在使用指针时不会遇到太大麻烦。但这个论断存在两个荒谬之处,首先“不会遇到太大麻烦”意味着“还是会遇到麻烦”。从本质上说,复杂的用法比简单的用法所涉及的风险大太多。其次维护代码的程序员可能不如阁下经验丰富,程序维护是软件产品的主要成本所在。所以那些使得程序维护工作更为困难的编程技巧应慎重使用。

同时,有些机器在设计时用了特殊的指令,用于执行数组下标操作,目的是为了使这种极常用的操作更加快速,在这种机器上的编译器将使用一些特殊指令来实现下标表达式,但编译器并不一定会用这些指令实现指针表达式。这样,在这种机器上,下标可能比指针效率高。

5.数组和指针

指针和数组并不是相等的。为了说明这个概念,请考虑下面这两个声明:

int a[5];

int *b;

a和b都具有指针值,都可以进行间接访问和下标引用操作。但是,它们还是存在相当大的区别。

声明一个数组时,编译器将根据所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间。而且指针变量并未被初始化为指向任何现有内存空间,如果它是一个自动变量,它甚至根本不会被初始化。把这两个声明用途的方法来表示,你可以发现他们之间存在显著的不同。

因此,上述声明之后,表达式*a是完全合法的,但表达式*b却是非法的。*b将访问内存中某个不确定的位置,或者导致程序终止。另一方面,表达式b++可以通过编译,但a++却不行,因为a的值是个常量。

你必须清楚地理解它们之间的区别,这是非常重要的,因为我们所讨论的下一个话题可能把水搅浑。​

你可能感兴趣的:(C-数组篇(一维数组-上))