浅谈指针(五)——数组与指针

0.序言

对于下面代码:

int a;
int b[10];

其中a称为标量,因为它是一个单一的值,变量b称为数组,即一些值的集合。下标和数组名一起使用,用于标识该集合中某个特定的值。每个特定值都是一个标量。

在C中,在几乎所有使用数组名的表达式中,数组名是一个指针常量,也就是数组第1个元素的地址。它的类型取决于数组元素的类型;但是指针和数组不同,数组具有一些和指针完全不同的特征,数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名来记住这些属性,只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。其所指向的是内存中数组的起始位置,如果修改这个指针常量,唯一可行的操作就是把这个数组移动到内存的其他位置。

只有当数组名作为sizeof操作符或者&操作符操作数时,数组名并不用指针常量表示。

int a[10];
int b[10];
int *c = &a[0];//指向数组第一个元素的指针
c = a;//同上
b = a;//非法
a = c;//非法

如果数组名表示整个数组,在b = a;中,这条语句就表示整个数组被复制到一个新的数组,而实际被赋值的是一个指针的拷贝,c指向的时数组的第一个元素,所以不能使用赋值符把一个数组的所有元素复制到另一个数组,必须使用一个循环,每次赋值一个元素。在a = c;中,c被声明为一个指针变量,但a的值是一个常量,不能被修改。

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

int array[10];
int *ap = array + 2;

在使用下标引用的地方,可以使用对等的指针表达式来代替。

ap&array[2]
*aparray[2]
ap[0]array[2]
ap+6&array[8]
*ap+6array[2]+6
*(ap+6)array[8]
ap[6]array[8]
&ap未知
ap[-1]array[1]
ap[9]越界

下标引用和间接访问表达式是一样的。而2[array]也是合法的,但为了可读性,写成array[2]。

1.指针还是下标

对于大部分人,下标更容易理解,尤其在多维数组中,在可读性方面,下标有一定优势,但在另一方面,这个选择可能会影响运行时效率、下标绝不会比指针更有效率,但指针有时会比下标更有效率。对于下面例子:

//第一种
int arrat[10],a;
for(a = 0;a<10;a+=1)
arrat[a]=0;
//第二种
int array[10],*ap;
for(ap = array;ap<array+10;ap++)
{
    *ap=0;
}

这两种代码的for循环转换成会变后,会存在乘法运算:1这个值必须与整型长度相乘,然后再与指针相加。但这里有一个重大区别:循环每次执行时,执行乘法运算的都是两个相同的数(1和4),结构,结果,这个乘法只在编译时执行一次——程序现在包含了一条指令,把4与指针相加,程序在运行时并不执行乘法运算。当在数组中1次1步地移动时,与固定数字相乘的运算在编译时完成,所以在运行时所需的指令就少一些。

指针比下标更有效率取决于它们被正确使用。但是使用指针也更容易写出质量地下的代码,但是不要为了效率上的细微差别而牺牲可读性。

#define SIZE 50
int x[SIZE];
int y[SIZE];
int i;
int *p1,*p2;

void try1()
{
    for(i=0;i<SIZE;i++)
    x[i]=y[i];
}

void try2()
{
    for(p1=x,p2=y;p1-x<SIZE)
    *p1++=*p2++
}

void try3()
{
    for(i=0,p1=x,p2=y;i<SIZE;i++)
    *p1++ = *p2++;
}

void try4()
{
    register int *p1,*p2,i;
    for(i=0,p1=x,p2=y;i<SIZE;i++)
    *p1++=*p2++;
}

void trt5()
{
    register int *p1,*p2,i;
    for(p1=x,p2=y;p1<&x[SIZE])
    *p1++=*p2++;
}

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

当一个数组名作为参数传递给一个函数时,函数如果指向了下标引用,实际上时对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的元素数组。所有的参数都是通过传值方式传递的,如果传递了一个指向某个变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以修改那个变量。数组参数调用函数时实际传递的是一个指针,所以函数的形参实际上是个指针,但编译器也接受数组形式的函数形参。因此,下面两个函数原型是相等的:

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

2.初始化

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

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

char message[ ]="hello"

当数组列表中元素个数少于下标声明个数,则会以0或'/0'补充;如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能够容纳所有的初始值的长度。如果初始化列表经常修改,这个技巧尤其有用。

数组的初始化取决于标量变量的初始化方式,也就是取决于它们的存储类型。存储于静态内存的数组只初始化一次,也就是在程序开始之前。程序并不需要执行指令把这些值放到合适的值对数组元素进行初始化。如果数组未被初始化,数组元素的初始值将会自动设置为零。当这个文件载入到内存中准备执行时,初始化后的数组值和程序指令一样也被载入内存。因此,当程序执行时,静态数组已经初始化完毕。但是,对于自动变量而言,初始化过程没有那么浪漫了,因为自动变量位于运行时堆栈中,执行流每次进行它们所在的代码块时,这类变量每次所处的内存位置可能不相同。在程序开始之前,编译器没有办法对这些位置进行初始化。所以,自动变量在缺省情况下是未初始化的,如果自动变量的声明给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化,这条隐式的赋值语句和普通的赋值语句一样需要时间和空间来执行。

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

3.多维数组

如果某个数组的维数不止1个,它就被称为多位数组。例如:int matrix[6][10];从一个不同的观点观察多维数组,

int a;//简单整数
int b[10];//向量
int c[6][10];//一维数组的一维数组
int d[3][6][10];//整型三维数组

多维数组的元素存储顺序按照最右边的小标率先变化的原则,称为行主序。但是,你不能修改内存中数组元素的实际存储方式。比如是二维数组是一个指向整型元素的数组的指针。如果要标识一个多维数组的某个元素,必须按照与数组声明时相同的顺序为每一维都提供一个下标,而且每个下标都单独位于一对方括号内,但是,下标引用实际上只是间接访问表达式的一种伪装形式,即使在多维数组中也是如此。

int martix[6][10];
int *mp;
mp = &matrix[3][8];
*++mp;//matrix[3][9];
martix;//martix[0][0];
martix+1;//指向数组的指针
*(martix+1);//martix[1][0]
*(martix+1)+5;//mattix[1][5]
*(*(martix+1)+5);

对于多位数组的自动长度计算,只有第一维能够根据初始化列表缺省地提供。剩余的几个维必须显式地写出,这样编译器就能推断处每个子数组维数的长度。

4.指针数组

egc:int *api[10];

其中下标引用优先级高于间接访问,所以在这个表达式中,首先执行下标引用。因此,api是某种类型的数组,在取得一个数组元素之后,随即执行的是间接访问操作。其结果是一个整数值。其元素类型是指向整型的指针。

矩阵的效率低一些,因为其每一个字节都被固定为关键字,但是不需要任何指针。指针数组本身也要占用空间,但是每个元素占据的内存空间只是它本身的长度。

5.指向数组的指针

egc:int (*p)[10] = matrix;

p是个指针,指向某种类型的数组,对其执行间接访问操作是,我们的得到的是个数组,对该数组进行下标引用操作得到的是一个整型值。如果需要一个指针逐个访问,则可以用int *pi = &martix[0][0]; int *pi=matrix[0];

int (*p)[]是报错的!

6.总结

//1.cpp:
int mango[100];

//2.cpp
extern int *mango

这是错误的用法,数组定义不一定等同于指针。

指针保存数据的地址,间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。如果指针有一个下标,就把指针的内容加上地址值作为新的地址,从中提取数据,这个通常用于动态数据结构,通常匿名指向数据。

数组直接保存数据,直接访问数据,通常用于存储固定数目呃数据类型相同的元素,隐式分配和删除,自身即为数据名。

数组和指针都可在它们的定义中用字符串常量进行初始化,底层极值却不相同。

char *p = "breadfruit";//只有字符串常量才是如此
float *pip = 3.141;//错误

在ANSI C中,初始化指针时所创建的字符串常量被定义为只读。在有些编译器里面,字符串常量被存放在只允许读取的文本段中,以防止它被修改。与指针相反,由字符串常量初始化的数组是可以修改的。

所有作为函数参数的数组名总算可以通过编译器转换为指针,在使用数组(语句或表达式中引用时),数组总算可以写成指针的形式,两种可以互换。当表达式中的数组名被编译器当作一个指向数组第一个元素的指针,下标总是与指针的偏移量相同,在函数参数声明中,数组名被编译器当作指向该数组第一个元素的指针。

在表达式中的数组名就是指针,C把数组下标作为指针的偏移量,作为函数参数的数组名等同于指针。

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值