- C语言中只有一维数组,而且数组的大小必须在编译期就作为一 个常数确定下来。不过,C语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要“仿真"出一个多维数组就不是一件难事。
- 对于一个数组,我们只能做两件事:确定该数组的大小以及获得指向该数组下标为0的指针元素。其他有关数组的操作,实际上都是通过指针进行的。
1. 数组名
在C语言中,在几乎所有使用数组名的表达式中,数组名的值是个指针常量,也就是数组第1个元素的地址。它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型就是“指向其他类型的常量指针”。
请不要根据这个事实得出“数组和指针是相同的”结论。数组具有一些和指针完全不同的特征。例如,数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。
注意,这个值是指针常量,而不是指针变量。常量的值是不能修改的。只要稍微回想一下,就会认为这个限制是合理的:指针常量所指向的是内存中数组的起始位置,如果修改这个指针常量,唯一可行的操作就是把整个数组移动到内存的其他位置。但是,在程序完成链接之后,内存中数组的位置是固定的,所以当程序运行时,再想移动数组就为时已晚了。因此,数组名的值是一个指针常量。
只有在两种场合下,数组名并不用指针常量来表示——当数组名作为sizeof
操作符或单且操作符&的操作数时。sizeof
返回整个数组的长度,而不是指向数组的指针的长度。取一个数组名的地址所产生的是-个指向数组的指针,而不是一个指向某个指针常量值的指针。
不能用赋值符直接把一个数组的所有元素复制给另一个数组,而是必须使用一个循环,每次复制一个元素。
2.为什么在C语言不检查下标的正确性
标准并未提出这项要求。最早的C编译器并不检查下标,而最新的编译器依然不对它进行检查。这项任务之所以很困难,是因为
下标引用可以作用于任意的指针,而不仅仅是数组名。作用于指针的下标引用的有效性既依赖于该指针当时怡好指向什么内容,也依赖于下标的值。
结果,C的下标检查所涉及的开销比刚开始想象的要多。编译器必须在程序中插入指令,证实下标表达式的结果所引用的元素和指针表达式所指向的元素属于同一个数组。 这个比较操作需要程序中所有数组的位置和长度方面的信息,这将占用一些空间。 当程序运行时,这些信息必须进行更新,以反映自动和动态分配的数组,这又将占用.一定的时间。因此,即使是那些提供了下标检查的编译器通常也会提供一个开关,允许去掉下标检查。
3. 2[array]
将这个表达式转换为对等的间接访问表达式,就会发现它的有效性:
*( 2 + ( array ) )
它和下面的表达式是完全一样的:
*( array + 2 )
也就是array[2]。
这个表达式会大大影响程序的可读性,最好不要使用。
4. 为什么函数原型中的一维数组形参无需写明它的元素类型
因为函数并不为数组分配内存空间形参只是一个指针,它指向的是已经在其他地方分配好了的内存空间。这个事实解释了为什么数组形参可以与任何长度的数组相匹配——它实际上传递的只是数组第一个元素的指针。
另一方面,这种实现使函数无法知道数组的长度。如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。
5. 字符数组的初始化
char m[]={'H','e','l','l','o',0};
语言标准提供了一种用于初始化字符数组的快速方法
char m[]="Hello";
尽管它看上去像是一个字符串常量,实际上并不是。它只是前例的初始化列表的另一种写法。如果它们看上去完全相同,该如何分辨字符串常量和这种初始化列表快速记法呢?它们是根据所处的上下文环境进行区分的。当用于初始化一个字符数组时,它就是一个初始化列表。在其他任何地方,它都表示一个字符串常量。
这里有一个例子:
char message1[] = "Hello";
char *message2 = "Hello";
这两个初始化看上去很像,但它们具有不同的含义。前者初始化个字符数组的元素, 而后者则是一个真正的字符串常量。这个指针变量被初始化为指向这个字符串常量的存储位置。
6. 多维数组
6.1 多维数组的下标引用
int a[3][10];
对表达式:
*( a + 1 ) + 5;
执行间接访问操作:
*( *( a + 1) + 5)
它和表达式:
*( a[1] + 5 )
是一样的效果,也就是a[1][5]
。
6.2 a[4,3]
逗号操作符首先对第一个表达式进行求值,但随即丢弃这个值。最后的结果使第2个表达式的值。因此,这个表达式和下面的表达式是相等的:
a[3];
6.3 指向多维数组的指针
int a[10];*b = a; //合法int c[3][10];*d = c; //非法
声明一个指向整型数组的指针:
int (*p)[10];
p是一个指向整型数组的指针。在声明中加上初始化:
int (*p)[10] = c;
它使p指向数组c的第一行。
p是一个指向拥有10个整型元素的数组的指针。当把p与一个整数相加时,该整数值首先根据10个整型值的长度进行调整,然后再执行加法。所以可以使用这个指针一行一行地在数组a中移动。
如果需要一个指针逐个访问整型元素而不是逐行在数组中移动,应该怎么办呢?下面两个声明都创建了一个简单的整型指针,并以两种不同的方式进行初始化,指向matrix 的第1个整型元素:
int *pi = &a[0][0];int *pi = a[0];
增加这个指针的值使它指向下一个整型元素。
警告:
如果打算在指针.上执行任何指针运算,应该避免这种类型的声明:int (*p)[] = a;
p仍然是一个指向整型數组的指针,但数组的长度却不见了。当某个整数与这种类型的指针执行指针运算时,它的值将根据空数组的长度进行调整(也就是说,与零相乘), 这很可能不是你所设想的。有些编译器可以捕捉到这类错误,但有些编译器却不能。
7. 非数组的指针
在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符(\0
)的内存区域。因为C语言要求字符串常量以空字符作为结束标志。
假定有两个字符串s和t,希望将这两个字符串连接成单个字符串r。
char *r;strcpy(r,s);strcat(r,t);
这不满足目标,因为不知r指向何处。我们还应该看到,不仅要让r指向一个地址,而且r所指同的地址处还应该有内存空间可供容纳字符串,这个内存空间应该是某种方式已经被分配了的。
尝试给r分配一定的内存空间:
char r[100];strcpy(r, s);strcat(r, t);
只要s和t指向的字符串并不是太大,那么现在我们所用的方法就能够正常工作。不幸的是,C语言强制要求我们必须声明数组大小为一个常量,因此我们不敢确保r足够大。然而,大多数C语言实现为我们提供了一个库函数malloc,该函数接受一个整数,然后分配能够容纳同样数目的字符的一块内存。大多数C语言实现还提供了一个库函数strlen,该函数返回一个字符串中所包括的字符数。有了这两个库函数,似乎我们就能够像下面这样操作了:
char *r, *malloc() ;r = malloc(strlen(s) + strlen(t)) ;strcpy(r, s) ;strcat(r, t);
这个例子还是错的,原因归纳起来有三个。
-
第一个原因,malloc 函数有可能无法提供请求的内存,这种情况下malloc函数会通过返回一个空指针来作为“内存分配失败”事件的信号。
-
第二个原因,给r分配的内存在使用完之后应该及时释放,这一点务必要记住。因为在前面的程序例子中r是作为一个局部变量声明的,所以当离开r作用域时,r自动被释放了。修订后的程序显式地给r分配了内存,为此就必须显式地释放内存。
-
第三个原因,也是最重要的原因,就是前面的例程在调用malloc函数时并未分配足够的内存。我们再回忆一下字符串以空字符作为结束标志的惯例。库函数
strlen
返回参数中字符串所包括的字符数目,而作为结束标志的空字符并未计算在内。因此,如果strlen(s)
的值是n,那么字符串实际需要n+1
个字符的空间。所以,我们必须为r多分配-一个字符的空间。做到了这些,并且注意检查了函数malloc是否调用成功,我们就能得到正确的结果:char *r,*malloc() ;r = malloc(strlen (s) + strlen(t) + 1) ;if(!r) { complain() ; exit(1) ;} strcpy(r, s) ; strcat (r, t) ; /* 一段时间之后再使用 */ free(r) ;
8. 指针和数组
int calendar[12] [31] ;int (*monthp) [31] ;monthp = calendar;
这样,monthp将指向数组calendar的第1个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。
假定在新的一年开始时, 我们需要清空calendar数组:
int month;for(month=0;month<12;month++){ int day; for(day = 0;day<31; day++) calendar[month][day] = 0;}
将上面的代码换成采用指针的形式:
*(*(calendar+month)+day) = 0;
如果指针monthp 指向一个拥有31个整型元素的数组,而calendar的元素在此也是一个拥有31个整型元素的数组,因此就像在其他情况中我们可以使用一个指针遍历一个数组一样,这里同样可以使用指针monthp以步进的方式遍历数组calendar:
int (*monthp)[31];for (monthp = calendar; monthp < &calendar[12]; monthp++)/*处理一个月份的情况*/
同样地,我们可以像处理其他数组一样,处理指针monthp 所指向的数组的元素:
int (*monthp) [31] ;for (monthp = calendar; monthp < &calendar[12]; monthp++){ int *dayp;for(dayp = *monthp; dayp<& (*monthp) [31]; dayp++) *dayp = 0;}
9. 作为参数的数组声明
在C语言中,我们无法将一个数组作为函数参数直接传递。如果我们将数组名作为参数,那么数组名会立刻被转换为指向该数组第1个元素的指针。例如,下面的语句:
char hello[] = "hello";
声明了hello 是一个字符数组。如果将该数组作为参数传递给一个函数:
printf ("%s\n", hello);
实际上与将该数组第1个元素的地址作为参数传递给函数的作用完全等效,即:
printf("%s\n", &hel1o[0]);
因此,将数组作为函数参数毫无意义。所以,C语言中会自动地将作为参数的数组声明转换为相应的指针声明。也就是说,像这样的写法:
int strlen(char s[]){ /*具体内容*/}
与下面的写法完全相同:
int strlen(char* s){ /*具体内容*/}
C程序员经常错误地假设,在其他情形下也会有这种自动转换。
extern char *hello;
这个语句与下面的语句有着天渊之别:
extern char hello[];
如果一个指针参数并不实际代表一个数组, 即使从技术上而言是正确的,采用数组形式的记法也经常会起到误导作用。
如果一个指针 参数代表一个数组,情况又是如何呢?一个常见的例子就是函数main的第二个参数:
main(int arge, char* argv[]){ /*具体内容*/}
这种写法与下面的写法完全等价:
main(int argc, char** argv){ /*具体内容*/}
需要注意的是,前一种写法 强调的重点在于argv是个指向某数组的起始元素的指针,该数组的元素为字符指针类型。因为这两种写法是等价的,所以读者可以任选种最能清楚反映自己意图的写法。
10. 避免"举隅法"
char *p,*q;p="xyz";
p的值是一个指向由’x’、‘y’、‘z’和’\0’这四个字符组成的数组的起始元素的指针。
q=p;
p和q现在是两个指向内存中同一地址的指针。这个复制语句并没有同时复制内存中的字符。
复制指针并不同时复制指针所指向的数据。
ANSI C标准禁止对字符串常量进行修改。
11. 总结
-
array[ value ];*(array + value );
除了优先级不一样,下标表达式和间接访问表达式是完全一样的。因此,下标不仅可以用于数组名,也可以用于指针表达式中。
-
指针表达式可能比下标表达式效率更高,但下标表达式绝不可能比指针表达式效率高。
-
**指针和数组并不一样,指针和数组的属性大相径庭。**当声明一个数组时,它同时分配了一些内存空间,用于容纳数组元素。但是,当声明一个指针时,它只分配了用于指针本身的空间。
-
当数组名作为函数参数传递时,实际传递给函数的是一个指向数组第1个元素的指针。但是,对指针参数执行间接访问操作允许函数修改原来的数组元素。
-
多维数组实际上是一维数组的一种特例,就是它的每个元素本身也是一个数组。多维数组的最右边的下标最先变化。多维数组名的值是一个指向它第1个元素的指针,也就是指向数组的指针。