数组与指针

xiaoxiao2021-02-28  3

一、数组和指针: 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转化为指针加偏移量的引用。 我们写了这样的语句

int a; a = 3;

编译器为了完成这两句代码,首先在编译过程中要创建一个符号表,样子大概如下图: 然后在运行过程中,编译器发现a=3这句代码时,会在符号表里找a对应的地址,然后把3放入对应的地址,即这里的0x1000。那如果是一个指针呢?即如果是*p=3会怎么做呢?首先,符号表变成了这个样子: 在运行过程中,编译器遇到p=3时,首先要从符号表中找到p的地址0x1004,然后取出0x1004中的内容,这里假设为0x2000,最后把3放到0x2000内存地址中,即(0x1000)=3。

相信大家已经看明白了,想比于利用普通变量,利用指针存取数据的过程中多了一部取地址的的过程。这也就是指针变量于普通变量最大的不同。

下面再来看一下指针加偏移量的引用方式,还以上面的指针p为例,让我们来看一下(p+2)=3的实现过程。首先,编译器从符号表中找到p然后,取出里面的内容0x2000,再根据其类型(int),做一个运算,0x2000+2×sizeof(int)=0x2008。所以编译器会把3放入0x2008这个内存地址。整个过程可表示为((0x1004)+2)=3。从这里也可以看出为什么指针必须有类型,因为在引用过程中要用到指针所指类型的长度。

最后来看一下,数组元素的引用是如何实现的,假设我们定义了一个数组,并对其元素进行了引用:

int b[10]; b[4] = 3;

对应的符号表变成了这个样子: 那b[4]=3如何完成呢?首先,找到符号b,然后发现其类型为int[](假想表达方式,C语言中不支持这样写),所以计算式变成了0x1008+4×sizeof(int)=0x1018,然后把3放入0x1018就可以了。用一个式子表达就是*(0x1008+4)=3。

从上面的寻址式子可以看出,普通变量、指针、数组三者对于编译器的区别。具体到数组,它即具有普通变量的直接性,即不用取两次地址里的内容而是取一次,同时又具有和指针相同的偏移量引用方式,即下标的实现实际是由指针加偏移量实现的。

C语言对指针与数组的引用方式做了可以“交叉”使用的语法规定。就上面的例子来说,如果p指针指向数组b时,b[i]、(b+i)、p[i]、(p+i)都是对数组第i个元素的正确引用方式,这也给很多C语言学习者制造了“指针和数组一样”的错觉。

二、数组作为函数参数:

(1)在普通变量或下标变量作函数参数时,形参变量和实参变量是由编译系统分配的两个不同的内存单元。在函数调用时发生的值传送是把实参变量的值赋予形参变量。在用数组名作函数参数时,不是进行值的传送,即不是把实参数组的每一个元素的值都赋予形参数组的各个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存。那么,数据的传送是如何实现的呢? 数组名就是数组的首地址。因此在数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组。实际上是形参数组和实参数组为同一数组,共同拥有一段内存空间。 上图说明了这种情形。图中设a为实参数组,类型为整型。a占有以2000为首地址的一块内存区。b为形参数组名。当发生函数调用时,进行地址传送,把实参数组a的首地址传送给形参数组名b,于是b也取得该地址2000。于是a,b两数组共同占有以2000为首地址的一段连续内存单元。从图中还可以看出a和b下标相同的元素实际上也占相同的两个内存单元(整型数组每个元素占二字节)。例如a[0]和b[0]都占用2000和2001单元,当然a[0]等于b[0]。类推则有a[i]等于b[i]。

在变量作函数参数时,所进行的值传送是单向的。即只能从实参传向形参,不能从形参传回实参。形参的初值和实参相同,而形参的值发生改变后,实参并不变化,两者的终值是不同的。而当用数组名作函数参数时,情况则不同。由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。当然这种情况不能理解为发生了“双向”的值传递。但从实际情况来看,调用函数之后实参数组的值将由于形参数组值的变化而变化。

#include <stdio.h> void nzp(int a[8]){ int i; printf("\nvalues of array aare:\n"); for(i=0;i<8;i++){ if(a[i]<0)a[i]=0; printf("%d ",a[i]); } } int main(void){ int b[5],i; printf("\ninput 5 numbers:\n"); for(i=0;i<5;i++) scanf("%d",&b[i]); printf("initial values of array b are:\n"); for(i=0;i<5;i++) printf("%d ",b[i]); nzp(b); printf("\nlast values of array b are:\n"); for(i=0;i<5;i++) printf("%d ",b[i]); return 0; }

本例中,代码能通过编译并运行,最后结果也如以上分析,但是由于实参数组和形参数组维度不一样,最后会出现“Stack around the variable ‘b’ was corrupted”错误。

(2)在函数形参表中,允许不给出形参数组的长度,或用一个变量来表示数组元素的个数。例如,可以写为:

void nzp(int a[])

或写为

void nzp( int a[], int n )

其中形参数组a没有给出长度,而由n值动态地表示数组的长度。n的值由主调函数的实参进行传送。 多维数组也可以作为函数的参数。在函数定义时对形参数组可以指定每一维的长度,也可省去第一维的长度。因此,以下写法都是合法的:

int MA(int a[3][10])

int MA(int a[][10])//第二维度不能省略

(3)防止数组越界的处理方法:如果数组本身包含一个结束标志,如C风格字符串,可以这样处理:

void print(const char* cp) { if (cp) while(*cp) { cout << *cp++ << " "; } }

利用函数begin和end:它们定义在头文件中

void print(const int* beg, const int* ed) { while (beg != ed) cout << *beg++ << " "; } int main() { int j[] = {12}; print(begin(j), end(j));//begin(),end()函数并不是标准库的成员函数 }

(4)使用数组引用形参避免退化

void print(int (&arr)[10]) //必须指明维度 { for(auto e : arr) cout << e << " "; } //遗憾的是这种方法编写的函数只能接受维度为10的数组

优化的方法是使用具有非类型模板参数的模板:

template<unsigned N> void print(int (&arr)[N]) { for(auto e : arr) cout << e << " "; } //这样调用函数时编译器会推断模板参数来实例化,可以接受任意合法长度的数组
转载请注明原文地址: https://www.6miu.com/read-1750096.html

最新回复(0)