第10章 指针与数组(3 指针与数组)

发布于 2021-09-01  537 次阅读


10.3 指针与数组

指针提供一种以符号形式使用地址的方法。
因为计算机的硬件指令非常依赖地址,指针某种程度上把程序员要传达的指令以更接近机器的方式表达。使用指针更有效率。指针能有效的处理数组。
数组表示法其实就是变相的使用指针。
例如:数组名是数组首元素的地址,如果flizny是一个数组:
flizny ==&flizny[0];        //数组名是数组首元素的地址
flizny和&flizny[0]都表示数组首元素的内存地址。两者都是常量,在程序的执行过程中,不会改变,但是可以把它们赋值给指针变量,然后可以修改指针变量的值。
如下程序
(%p通常以十六进制显示指针的值)
// pnt_add.c        --指针地址
#include<stdio.h>
#define SIZE 4
int main(void)
{
    short dates[SIZE];
    short *pti;
    short index;
    double bills[SIZE];
    double *ptf;
    pti = dates;        //把数组地址赋给指针
    ptf = bills;

    printf("%23s %15s\n","short","double");
    for(index=0;index<SIZE;index++)
    {
        printf("pointers + %d: %10p %10p\n",index,pti+index,ptf+index);
     } 

    return 0;
 } 

file

我们的系统中,地址按字节编址,short类型占用2个字节,double类型占用8字节。在C中,指针加1指的是增加一个存储单元。
对数组而言,这意味着加1后的地址是下一个元素的地址,而不是下1个字节地址。这也是为什么必须声明指针所指向对象类型的原因之一。只知道地址不够,因为计算机要直到存储对象需要多少字节(即使指针指的是标量变量,也要知道变量的类型,负责*pt就无法正确的取回地址上的指)

file

现在可以更清楚的定义指向int的指针、指向float的指针,以及指向其他数据对象的指针。
- 指针的值是它所指向对象的地址。地址的表示方式依赖于计算机内部的硬件。
许多计算机(包括PC和Macintosh)都是按字节编址,意思是内存中的每个字母都按顺序编号。一个较大对象的地址(如double类型的变量)通常是该对象第一个字节的地址。
- 在指针前面使用*运算符可以得到该指针所指向对象的值。
- 指针加1,指针的值递增它所指向的类型大小(以字节为单位)
下面等形式体现了C的灵活性:
detes+2==&dates[2];     //相同地址
*(detes + 2) ==detes[2];    //相同的值
以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。
从本质上看,同一个对象有两种表示法。实际上C语言标准在描述数组表述法时确实借助了指针。
也就是说ar[n]的意思是*(ar + n).可以认为*(ar+n)的意思是"到内存的ar位置,然后移动了n个单元,检索存储在那里的值"
不要混淆*(dates+2)和*dates+2
间接运算符(*)的优先级高于+所以*dates+2相当于(*dates)+2:
*(dates+2)  //相当dates数组的第3个元素
*dates+2    //dates的第一个元素的值+2
明白了数组和指针的关系,便可在编写程序时使用数组表示法或指针表示法。
//day_mon3.c
#include<stdio.h>
#define MONTHS 12

int main(void)
{
    int days[MONTHS]={31,28,31,30,31,30,31,31,30,31,30,31};
    int index;

    for(index = 0; index<MONTHS;index++)
    {
        printf("Month %2d has %d days.\n",index+1,*(days+index));
    }

    return 0;
 } 
指针表示法和数组表示法是两种等效的方法。
该例演示了可以用指针表示数组,反过来也可以用数组表示指针。

10.4 函数、数组和指针

假设要编写一个处理数组的函数,该函数返回数组中所有元素之和,待处理的是名为marbles的int类型数组。
应该如何调用该函数呢?也许是:
total = sum(marbles);       //可能的函数调用
那么,该函数的原型是什么?数组名是该数组首元素的地址,所以实际参数marbles是一个储存int类型的值的地址,应把它赋给一个指针形式的参数,即该形参是一个指向int的指针:
int sum(int *ar);   //对应的函数原型
sum()从该参数获得了什么信息?它获得了该数组首元素的地址,知道要在该位置找一个整数。
注意:该参数并未包含数组元素个数的信息。我们有两种办法让函数获得这一信息,第一种方法是,在函数代码中协商固定数组的大小:
int sum(int *ar)    //对应的函数定义
{
    int i;
    int total = 0;

    for(i=0;i<10;i++)        //假设数组有10个元素
    total+=ar[i];   //ar[i]/*(ar+i)
    return toral;
}
既然能使用指针表示数组名,也可以用数组名表示指针。
该函数定义有限制,只能计算10个int类型的元素。
另外一种比较灵活的方法是把数组大小作为第二个参数:
int sum(int *ar,int n)
{
    int i;
    int total = 0;

    for(i=0;i<n;i++)
    {
        total+=ar[i];
    }
    return 0;
}
关于函数形参,只有在函数原型或函数定义头中,才可以用int ar[]代替int *ar:
int sum(int ar[],int n);
int *ar形式和int ar[]形式都表示ar是一个指向int的指针,但是int ar[]只能用于声明形式参数。第二种形式(int ar[])提醒指针ar指向的不仅仅是一个int类型值,还是一个int类型数组元素。
#include<stdio.h>
#define SIZE 10
int pointer(int *arr);
int main(void)
{
    int open[SIZE];
    int i;
    int total = 0;

    for(i=0;i<SIZE;i++)
    {
        scanf("%d",&open[i]);
    }
    total = pointer(open);
    printf("%d",total);

    return 0;
}

int pointer(int *arr)
{
    int i;
    int total=0;
    for(i=0;i<SIZE;i++)
        total+=arr[i];

    return total;
}
注意 声明数组形参
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。
只有在这种情况下,C才会把int ar[]和int *ar解释称一样,也就是说ar是指向int的指针。
由于函数原型可以省略参数名,所以下面4中原型都是等价的:
int sum(int *ar,int n);
int sum(int *,int );
int sum(int ar[],int n);
int sum(int [],int);
但是在函数定义中不能省略参数名,下面两种形式的函数定义等价:
int sum(int *ar,int n)
{

}
int sum(int ar[],int n)
{

}
Q:使用sum函数,该程序打印原始数组的大小和表示该数组的函数形参的大小
#include<stdio.h>
#define SIZE 10
int sum(int *arr,int n);
int main(void)
{
    int open[SIZE];
    int i;
    long answer;

    for(i=0;i<SIZE;i++)
    {
        scanf("%d",&open[i]);
    }
    answer = sum(open,SIZE);

    printf("the total number of marbles is %ld.\n",answer);
    printf("The size of open is %zd bytes.\n",sizeof(open));

    return 0;
}

int sum(int *arr,int n)
{
    int i;
    int total=0;
    for(i=0;i<SIZE;i++)
        total+=arr[i];
    printf("The size of arr is %zd bytes\n",sizeof(arr));

    return total;
}

// int a[x];

file

open的大小是40个字节。
open内含10个int类型的值。每个值占4个字节,所以open是40个字节。
ar才8字节。这是因为ar并不是数组本身,它是一个指向open数组首元素的指针。我们系统中用8位存储地址,所以指针变量的大小是8字节。
简而言之,上述代码,open是一个数组,arr是指向open数组元素首元素的指针。利用C语言数组和指针的特殊关系,可以用数组表示法来表示指针arr。

10.4.1 使用指针形参

函数要处理数组必须要知道何时开始、何时结束。
sum()函数使用了一个指针形参标识数组开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。
但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第一个指针指明数组开始处,第二个指针指明数组的结束处。
下程序演示了这种方法,同时也表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。
//sum_arr2.c        --数组元素之和
#include<stdio.h>
#define SIZE 10
int sump(int *start,int *end);
int main(void)
{
    int marbles[SIZE]={20,10,5,39,4,16,19,26,31,20};
    long answer;

    answer = sump(marbles,marbles+SIZE);
    printf("The total number of marbles is %ld.\n",answer);

    return 0;
 } 
int sump(int *start,int *end)
{
    int total = 0;

    while(start<end)
    {
        total=total+*start;
        start++;
    }

    return total;
}

file

指针start开始指向marbles数组的首元素,所以赋值total+=*start把首元素(20)加给total,然后表达式start++递增执政变量start,使其指向数组的下一个元素。因为start是指向int的指针,start递增1相当于其值递增int类型的大小。
sump()函数采用另外一种方式结束加法循环。sum()函数把元素的个数作为第2个参数,并把该参数作为循环测试的一部分:
for(i=0;i<n;i++)
而sump采用的是第二个指针结束循环:
while(start<end)
while循环的测试条件是一个不等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组元素最后一个元素后面。c保证在给数组分配空间时,指向数组最后面第一个位置的指针仍是有效指针。这使得while循环的测试条件是有效的。
因为start在循环最后值是end。使用这种”越界“执政的函数调用更为简洁。
anwer = sump(marbles,marbles+SIZE);
因为从下标0开始,所以marbles+SIEZ指向数组末尾的下一个位置,如果end指向数组的最后一个元素而不是数组末尾的下一个元素,则必须使用下面代码:
anwer = sump(marbles,marbless+SIZE-1);
虽然C保证了marbles+SIZE有效,但是对marbles[SIZE]未左任何保证,所以程序不能访问该位置。
还可以把循环体压缩成一行代码”
total +=*start++;
 一元运算符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。也就是说,指针start先递增后指向。使用后缀表达式(即start++而不是++start)意味者先把指针指向位置的值加到total上,然后再递增指针。如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,在递增该值,而不是递增指针。虽然*start++写法常用,但是*(start++)这样写更清楚。
//order.c       --指针运算中的优先级
#include<stdio.h>
int data[2]={100,200};
int moredata[2]={300,400};
int main(void)
{
    int *p1,*p2,*p3;
    p1 = p2 =data;
    p3 = moredata;
    printf("*p1=%d,*p2=%d,*p3=%d\n",*p1,*p2,*p3);     //100,100,300
    printf("*p1++=%d,*++p2=%d,(*p3)++=%d\n",*p1++,*++p2,(*p3)++); //100,200,301(300
    printf("*p1=%d,*p2=%d,*p3=%d\n",*p1,*p2,*p3);     //200,100(200),300(301)

    return 0;
 } 

file

只有(*p3)++改变了数组元素的值,其他两个操作分别把p1和p2指向数组的下一个元素。

擦肩而过的概率