第10章 指针与数组(4 指针操作)

发布于 2021-09-02  526 次阅读


10.5 指针操作

可以对指针进行那些操作?
C提供了一些基本的指针操作,下面程序示例中演示了8中不同的操作。
为了显示每种操作的结果,该程序打印了指针的值(该指针指向的地址)、存储在指针指向地址上的值,以及指针自己的地址。
如果编译器不支持%p转换说明,可以用%u或%lu代替%p;
如果编译器不支持用%td转换说明打印地址的差值,可以用%d或%ld来代替。
下述程序演示了指针变量的8种基本操作。除了这些操作,还可以使用关系运算符来比较指针。
//ptr_ops.c     --指针操作
#include<stdio.h>
int main(void)
{
    int urn[5]={100,200,300,400,500};
    int *ptr1,*ptr2,*ptr3;

    ptr1 = urn;
    ptr2 = &urn[2];
                        //解引用指针,以及获得指针的地址
    printf("pointer value,dereferenced pointer,pointer address:\n");
    printf("ptr1 = %p,*ptr1 = %d,&ptr1 = %p\n",ptr1,*ptr1,&ptr1);         //&urn,100,&ptr1

    //指针加法
    ptr3 = ptr1 + 4;
    printf("\nadding an int to a pointer:\n");
    printf("ptr1 + 4 = %p,*(ptr1+4)=%d",ptr1+4,*(ptr1+4));        //&,500

    ptr1++;             //递增指针
    printf("\nvalues after ptr1++:\n");
    printf("ptr1 = %p,*ptr1 = %d,&ptr1 = %p\n",ptr1,*ptr1,&ptr1); //&+4,200,&ptr1

    ptr2--;             //递减指针
    printf("\nvalues after --ptr2:\n");
    printf("ptr2 = %p,*ptr2 = %d,&ptr2 = %p\n",ptr2,*ptr2,&ptr2); //&+4*1,200,& 

    --ptr1;             //恢复初始值 
    ++ptr2;             //恢复初始值 

    printf("\nPointers reset to original values:\n");
    printf("ptr1 = %p.ptr2 = %p\n",ptr1,ptr2);        
    //一个指针减去另外一个指针
    printf("\n subtracting one pointer from anthor:\n");
    printf("ptr2 = %p,ptr1 = %p,ptr2 - ptr1 = %td\n",ptr2,ptr1,ptr2 - ptr1);          //8bytes2距离
    //一个指针减去一个整数
    printf("\nsubtracting an int from a pointer:\n");
    printf("ptr3 = %p,ptr3 -2=%p,*ptr3 = %d\n",ptr3,ptr3-2,*(ptr3-2));            //300 

    return 0; 
 } 

file

下面分别描述了指针变量的基本操作
- 赋值:可以把地址赋给指针。注意:地址应该和指针类型兼容。
- 解引用:*运算符给出的指针指向低智商存储的值。
- 取址:和所以变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
- 指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。整数都会和指针所指向的类型大小(以字节为单位)相乘,然后把结果与初始地址相加。
- 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。

file

- 指针减去一个整数:可以使用-运算符从一个指针种减去一个整数。指针必须是第一个运算对象,整数是第2个运算对象。改整数将乘以指针指向类型的大小(以字节为大小),然后用初始地址减去乘积。
- 递减指针
- 指针求差:可以计算出两个指针的差值,通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。
- 比较:使用关系运算符可以比较两个指针的值,前提条件是两个指针都指向相同类型的对象,
注意:这里的减法有两种。可以用一个指针减去另外一个指针得到一个整数,或者用一个指针减去一个整数得到另外一个指针。
在递减或递增指针时还要注意一些问题,编译器不会检查指针是狗仍指向数组元素。C只能保证向数组任意元素的指针和指向数组后面的第一个位置的指针有效。但是如果递增或者递减了一个指针后超出这个范围,则是未定义的。另外可以解引用指向数组任意元素的指针。但是,即使指针指向数组后面一个位置是有效的,也能引用这样的越界指针。
解引用未初始化的指针:
说到注意实想,一定要牢记一点:
千万不要解引用未初始化的指针。
int *pt;    //未初始化的指针
*pt = 5;    //严重的错误
第2行的意思是把5存储在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5被存放在何处。这可能不会出什么错误,也可能会擦写数据或代码,或者导致程序奔溃。
切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。
例如:可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况)。
或者使用malloc()函数先分配内存。
//假设
int urn[3];
int *ptr1,*ptr2;
//下面是一些有效和无效语句
//有效语句          无效语句
    prt1++;         urn++;
    ptr2=ptr1+2;    ptr2 = ptr2+ptr1;
    ptr2=urn+1;     ptr2 = urn*ptr1;
基于这些有效的操作,C程序员创建了指针数组、函数指针、指向指针的指针数组、指向函数的指针数组等。
指针的第1个基本用法是在函数间传递信息。如果希望在被调函数中改变主调函数的变量,必须使用指针。
指针的第2个基本用法是用在处理数组的函数种。

10.6 保护数组中的数据

编写一个处理基本类型(如int)的函数时,要选择的是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。
对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数值按值传递数组,则必须分配足够的空间老存储原数组的副本,然后把原数组中的所有的数据都拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。
传递地址会导致一些问题。C通常都按照值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外的修改原始数据。但是,处理数组的函数通常都需要直接用到原始数据,因此这样的函数可以修改原数组,有时,这正是我们需要的。
Q:下面的函数给数组元素的每个元素都加上一个相同的值。:
void add_to(double ar[].int n,double val)
{
    int i;
    for(i=0;i<n;i++)
        ar[i]+=val;
}
因此,调用了该函数后,prices中的每个元素的值都增加了2.5:
add_to(prices,100,2.50);
该函数修改了数组中的数据。之所以可以这样做是因为函数通过指针直接使用了原始数据。
然而,其他函数并不需要修改
计算数组中所有元素之和,它不用改变数组的数据。但是由于ar实际上是一个指针,所以编程错误可能会破坏原始数据。例如,下面示例中的ar[i]++会导致数组中的每个元素的值都加上1.
int sum(int ar[],int n)     //error
{
    int i,
    int total = 0;

    for(i=0;i<n;i++)
        total +=ar[i]++;        //error add to 每个值
    return total;
}

10.6.1 对形式参数使用const

在K&R C的年代,避免类似错误的唯一方法就是提高警惕。
ANSI C提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。
例如,sum()函数的原型和定义如下:
int sum(const int ar[],int n);  //函数原型

int sum(const int ar[],int n)   //函数定义
{
    int i;
    int total = 0;

    for(i=0;i<n;i++)
        total+=ar[i];
    return 0;
}
以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内容。
如果在函数中不小心使用了类似ar[i]++的表达式,编译器会捕捉这个错误,并生成一条错误信息。
这样使用const并不是要求原数组是常量,而实该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。一般而言,如果编写的函数需要修改数组,不适用const。如果编写函数,不用修改数组,那么在声明形参时做好使用const。
Q:一个函数显示数组的内容,另一个函数给数组每个元素都乘以一个给定值。因为第一个函数不用改变数组,所以在声明数组形参时使用了const;而第二个韩式需要修改数组元素的值,所以不用const
//arf.c     --处理数组的函数
#include<stdio.h>
#define SIZE 5
void show_array(const double ar[],int n);
void mult_array(double ar[],int n,double mult);
int main(void) 
{
    double dip[SIZE]={20.0,17.66,8.2,15.3,22.22};

    printf("The orignal dip array:\n");
    show_array(dip,SIZE);
    mult_array(dip,SIZE,2.5);
    printf("The dip array after calling mult_array():\n");
    show_array(dip,SIZE);

    return 0;
}
void show_array(const double ar[],int n)
{
    int i=0;
    for(i=0;i<n;i++)
        printf("%8.3f",ar[i]);
    putchar('\n');
}
void mult_array(double ar[],int n,double mult)
{
    int i=0;
    for(i=0;i<n;i++)
    {
        ar[i]=ar[i]*mult;
    }
}

file

虽然mult_array()函数更新了dip数组的值,但是并未使用return机制

10.6.2 const的其他内容

//使用const创建过变量:
const double PI = 3.14159;
//虽然用#define指令可以创建类似功能的符号常量,但是const的用法更加灵活。可以创建const数组,const指针和指向const的指针。
//如何使用const关键字保护数组;
#define MONTHS 12
...
const int dayS[SIZE]={..};
//如果编译器稍后尝试改变数组元素的值,编译器将生成一个编译期错误消息:
days[9]= 44;    //编译错误
//指向const的指针不能用于改变值。
double rates[5] = {88.99,100.12,59.45,183.11,340.5};
const double *pd = rates;   //pd指向数组首元素
//第二行代码把pd指向的double类型的值声明为const,这表明不能使用pd来更改它所指向的值:
*pd = 29.89;    //不允许
pd[2] = 222.22;     //不允许
rates[0] = 99.99;   //允许,因为rates本身并没有被const限定
//无论是使用指针表示法还是数组表示法,都不允许使用pd修改它所指向数据的值。但要注意的是,因为rates并未被声明为const,所以仍然可以通过rates修改元素的值。另外可以让pd指向别处:
pd++;       //让pd指向rates[1]--没问题
//指向const的指针通常用于函数形参中,表明该函数不会使用指针改变数据。例如:
void show_array(const double *ar,int n);
//关于指针赋值和const需要注意一些规则。首先,把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的:
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double locked[4]={0.08,0.075,0.0725,0.07};
const double *pc = rates;   //有效
pc = locked;            //有效
pc = &rates[3];         //有效
//然而,只能把非const数据的地址赋给普通的指针
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double locked[4]={0.08,0.075,0.0725,0.07};
double *pc = rates; //有效
pc = locked;            //无效
pc = &rates[3];         //有效
//这个规则非常合理,否则就可以通过指针改变const数组中的数据
//应用以上规则,如show_array()函数可以接受普通数组名和const数组名作为参数,因为这两种参数都可以用来初始化指向const的指针:
 show_array(rates,5);   //有效
 show_array(locked,4);  //有效
 //因此,对函数的形参使用const不仅能保护数据,还能让函数处理const数组。
 //另外,不应该把const数组名作为实参传递给mult_array()这样的函数
 mult_array(rates,5,1.2);   //有效
 mult_array(locked,4,1.2);  //不要这样做
//C标准规定,使用非const标识符修改const数据导致的结果是未定义的。
//const还有其他用法
//可以声明并初始化一个不能指向别处的指针,关键是const的位置
double rates[5]={88.99,100.12,59.45,183.11,340.5};
double *const pc = rates;   //pc指向数组的开始
pc = &rates[2];     //不允许,该指针不能指向别处
*pc = 92.99;    //没问题,更改rates[0]的值
//可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址
//最后,在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double *const pc = rates; //pc指向数组的开始
pc = &rates[2];     //不允许
*pc = 92.99;    //不允许

擦肩而过的概率