数据结构听不懂?赶快学会教学妹用指针吧!(C Primer Plus第六版)

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

学前服用

动画版的这个听完再去看书上的实例真的很不错! www.bilibili.com/video/BV1MJ…

前言

刚升本科学校开了一门数据结构,自己在b站往后学了一下发现不搞懂C语言的指针很难去理解,于是我又重温经典C Primer Plus

这本书写的真的很细致,加上看书和看优质的b站视频,我对指针有了一点简单的了解

纸上得来终觉浅,绝知此事要躬行

好了,开始奥里给了~

资源在这

Screenshot_20211001_171431_com.tencent.mm.jpg

它来了,它来了。C语言的灵魂!

没错它就是指针

上干货!!!

一、什么是指针

从根本上看,指针是一个值为内存地址的变量,如int类型的值是整数

char类型的值是字符

指针变量的值就是内存地址

二、 指针的用法

把指针作为函数参数使用,以及为何这样用

假设一个指针变量名是ptr,可以编写如下语句:

ptr = &pooh; //把pooh的地址赋给ptr

对于这条语句,我们说ptr“指向”pooh。

ptr和&pooh 的区别是ptr是变量,而&pooh是常量。或者,ptr是可修改的左值,而&pooh是右值。

当然还可以把ptr指向别处: ptr = &bah; //把ptr指向bah,而不是pooh 现在ptr的值是bah的地址。 要创建指针变量,先要声明指针变量的类型。假设想把ptr声明为储存int类型变量地址的指针,就要使用下面的新运算符。

2.1 间接运算符 *

假设已知 ptr指向bah,

如下所示:

ptr = &bah; 然后使用间接运算符* (indirection operator)找出储存在 bah 中的值,该运算符有时也称为解引用运算符(dereferencing operator)。不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。

val = *ptr;//找出ptr指向的值

语句ptr = &bah;和val = *ptr;放在一起相当于下面的语句;

val = bah

我的理解:

指针变量里面存放的是内存地址,既然是存放,那就有对于的取出操作,怎么取出?就在指针变量前面加上*即可!

小总结:

地址运算符:&

后面跟一个变量名时,&给出该变量的地址

示例:

&nurse表示变量nurse的地址

地址运算符:*

后跟一个指针名或地址时,*给出存储在指针指向地址上的值

示例:

nurse = 22;

ptr = &nurse; //指向nurse的指针

val = *ptr; //ptr指向的地址上的值赋给val

以上三条语句的结果是把22赋给val

2.2 声明指针

声明指针变量时,必须指定指针所指向变量的类型。咋听着这么绕呢?

用我的理解来说就是:

指针的变量类型要和指针所指向变量的类型一样,指针值的变量是啥类型,指针变量就是啥类型

例如:

int * pi; // pi是指向int类型变量的指针

char * pc; // pc是指向char类型变量的指针

float * pf, * pg; // pf、pg都是指向float类型变量的指针

类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。int * pi;声明的意思是pi是一个指针,*pi是int类型

在这里插入图片描述

这个图是从上往下看,能简单理解下使用指针的过程

*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。

2.3 使用指针在函数间通信

#include <stdio.h>

void interchange(int * u,int * v);//声明在函数里传入一个int类型指针u和int类型指针v



int main(void)

{

 //解决交换函数的问题

 int x = 5, y = 10;

 printf("Originally x = %d and y = %d.\n", x, y);

 interchange(&x,&y); //把地址发送给函数

 printf("Now x = %d and y = %d.\n",x ,y);



 return 0;

}



void interchange(int * u, int * v)

{

 int temp;

 temp = *u;

 *u = *v;

 *v = temp;

}
复制代码

程序分析

首先看函数调用,

interchange(&x,&y);这里的两个参数,不是x和y的值,而是x和y的地址

后来就是通过一个temp变量进行变量的地址交换

小总结:

变量:名称、地址和值

编写程序时,可以认为变量有两个属性:名称和值(还有其他性质,如类型,暂不讨论)。

计算机编译和加载程序后,认为变量也有两个属性:地址和值。地址就是变量在计算机内部的名称。

简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。

三、指针和数组

指针提供一种以符号形式使用地址的方法。因为计算机硬件非常依赖于地址,指针在某种程度上把程序员想要传达的指令以更尽皆机器的方式表达。

使用指针能让程序更有效率

flizny == &flizny[ 0]; //数组名是该数组首元素的地址 flizny和sflizny[0] 都表示数组首元素的内存地址(&是地址运算符)。两者都是常量,在程序的运行过程中,不会改变。但是,可以把它们赋值给指针变量,然后可以修改指针变量的值

#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;

    for (index = 0;index < SIZE; index++)
    {
      printf("pointers+%d : %p  %p\n",index,pti + index,ptf+index);
    }
    return 0;
}



pointers+0 : 000000000061FE00  000000000061FDE0
pointers+1 : 000000000061FE02  000000000061FDE8
pointers+2 : 000000000061FE04  000000000061FDF0
pointers+3 : 000000000061FE06  000000000061FDF8
复制代码

我们的系统中,地址按字节编址,short类型占用2字节,double类型占用8字节。在C中,指针加1指的是增加一个存储单元。

对数组而言,**这意味着把加1后的地址是下一个元素的地址,而不是下一个字节的地址,这是为什么必须声明指针所指向对象类型的原因之一。**只知道地址不够,因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则*pt就无法正确地取回地址上的值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1lGRJaX-1634202380884)(E:\Typora\Image\image-20211012153924351.png)]

对于上图的理解:

在C语言中,指针加1是增加一个存储单元,因为pti指针的类型是short类型,所以指针+1,其值每次递增两个字节

  • 指针的值是它所指向对象的地址。地址的表示方式依赖于计算机内部的硬件。许多计算机(包括PC和 Macintosh)都是按字节编址,意思是内存中的每个字节都按顺序编号。这里,一个较大对象的地址(如double类型的变量)通常是该对象第一个字节的地址。

  • 在指针前面使用*运算符可以得到该指针所指向对象的值。

  • 指针加1,指针的值递增它所指向类型的大小(以字节为单位)。

    data + 2 == &data[2]; //相同地址

    *(data + 2) == data[2]; //相同的值

容易混淆的概念

*(dates + 2) //dates第3个元素的值

*dates + 2 //dates的第一个元素的值加2

实例

#include <stdio.h>
#define MONTH 12

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

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

    return 0;
}

复制代码

四、函数、数组和指针

假设要编写一个处理数组的函数,该函数返回数组中所有元素之和,待处理的是名为marbles的int类型数组。应该如何调用该函数?也许是下面这样:

total = sum(marbles);//可能的函数调用

那么,该函数的原型是什么?记住,数组名是该数组首元素的地址,所以实际参数marbles是一个存储int类型值的地址,应把它赋给一个指针形式参数,即该形参是一个指向int的指针

int sum(int * ar); //对应的原型

sum()从该参数获得了什么信息?它获得了该数组首元素的地址,知道要在该位置上找出一个整数,注意,该参数并未包含数组元素个数的信息。第一种方法是,在函数代码中写上固定的数组大小;

实例

#include <stdio.h>
#define SIZE 10
int sum(int ar[],int n);
int main(void)
{
    int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
    long answer;

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

    return 0;
}

int sum(int ar[], int n) //数组的大小是?
{
    int i;
    int total = 0;

    for ( i = 0; i < n; i++)
        total += ar[i];
    
    
    printf("The size of ar is %zd bytes.\n", sizeof ar);//ar是一个指向
    return total;//sum函数会返回一个数组所有元素之和
    
}



输出结果为:
The size of ar is 8 bytes.
The total number of marbles is 190.
The size of marbles is 40 bytes.
复制代码

注意,marbles的大小是40字节。这没问题,因为marbles内含10个int类型的值,每个值占 4字节,所以整个marbles的大小是40字节。

但是,ar才8字节。这是因为ar并不是数组本身,它是一个指向marbles数组首元素的指针。

我们的系统中用8字节储存地址,所以指针变量的大小是8字节(其他系统中地址的大小可能不是8字节)。

ar是一个指向marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针ar。

五、使用指针形参

函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯一方法。 还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同), 第2个指针指明数组的结束处。同时该程序也表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个 元素。

实例

#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 += *start;    //把数组元素的值加起来
        start++;    //让指针指向下一个元素
    }
    return total;
    
}


输出结果为:
The total number of marbles is 190 .
复制代码

程序解析

​ 指针start开始指向marbles数组的首元素,所以赋值表达式total += start把首元素(20)加给total。然后,表达式start++递增指针变量start,使其指向数组的下一个元素。因为start是指向int的指针,start 递增1相当于其值递增int类型的大小。

​ 注意,sump ()函数用另一种方法结束加法循环。sum()函数把元素的个数作为第2个参数,并把该参数作为循环测试的一部分:

​ for(i=0;i<n;i++)

​ 而sump()函数则使用第2个指针来结束循环:

​ while (start < end)

​ 因为while循环的测试条件是-一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。这使得while 循环的测试条件是有效的,因为start在循环中最后的值是end。注意,使用这种“越界”指针的函数调用更为简洁:

​ answer = sump (marbles,marbles + SIZE) ;

​ 因为下标从0开始,所以marbles + SIZE 指向数组末尾的下一个位置。如果end指向数组的最后一个元素而不是数组末尾的下一个位置,则必须使用下面的代码:

answer = sump (marbles,marbles + SIZE - 1);

这种不推荐

​ 还可以把循环体压缩成一行代码

​ total += *start++;

​ 一元运 算符和+ +的优先级相同,但结合律是从右往左,**所以start++先求值,然后才是start。也就是说,指针start先递增后指向。**使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。

**使用++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(start)++,则先使用start指向的值,再递增该值,而不是递增指针。**这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然*start++的写法比较常用,但是

*(start++)这样写更清楚。
复制代码

指针的自增

#include <stdio.h>
int data[2] = {100,200};
int moredata[2] = {300,400};

int main(void)
{
    int *p1,*p2,*p3;//定义三个指针

    p1 = p2 = data; //指针p1等于指针p2等于数组data首元素
    p3 = moredata; //指针p3等于数组moredata首元素
    printf("*p1 = %d, *p2 = %d,*p3 = %d\n",*p1,*p2,*p3);//打印三个指针的值
    printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++,*++p2,(*p3)++);
    printf("*p1 = %d , *p2 = %d, *p3 = %d\n",*p1,*p2,*p3);

    return 0;
}


运算结果为:
*p1 = 100, *p2 = 100,*p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
*p1 = 200 , *p2 = 200, *p3 = 301
复制代码

解析

只有(*p3)++ 改变了元素的值

其他两个操作分别把p1和p2指向数组的下一个元素

我的小理解:

和a++,++a意思一样,(a)++先使用a的值,再递增该值,而不是递增指针(相当于变成值++了),a++和++a都是对指针进行递增,指向数组下一个元素

六、指针表示法和数组表示法

从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。

至于C语言,ar [ i]和* (ar+1)这两个表达式都是等价的。无论 ar 是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。

指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要任务是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。

哈哈哈,这本书写的还挺有意思呢!

七、指针操作

总结一下指针的常见操作

实例

#include <stdio.h>

int main(void)
{
    int urn[5] = {100, 200, 300, 400, 500}; //定义一个数组并赋值
    int *ptr1, *ptr2, *ptr3;                //定义三个指针

    ptr1 = urn;     //把数组首元素的地址赋给指针
    ptr2 = &urn[2]; //把数组第2个元素地址赋给指针

    printf("pointer value,dereferenced pointer,pointer address:\n");
    printf("ptr1 = %p,*ptr1 = %d,&ptr1 = %p\n", ptr1, *ptr1, &ptr1); //两个地址,一个值

    //指针的加法
    ptr3 = ptr1 + 4; //urn数组中第4个元素的地址
    printf("\nadding an int to a pointer:\n");
    printf("ptr1 + 4 = %p, *(ptr + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));

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

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

    --ptr1; //恢复为初始值
    ++ptr2; //恢复为初始值
    printf("\nPointers reset to original values:\n");
    printf("ptr1 = %p,ptr2 = %p\n", ptr1, ptr2);

    //一个指针减去另一个指针
    printf("\nsubtracting one pointer from another:\n");
    printf("ptr2 = %p,ptr1 = %p,ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);

    //一个指针减去一个整数
    printf("\nsubtracting an int from a poniter:\n");
    printf("ptr3 = %p,ptr3 - 2 = %p\n", ptr3, ptr3 - 2);

    return 0;
}

输出的结果为:
PS D:\Code\C\指针> cd "d:\Code\C\指针\" ; if ($?) { gcc 指针Demo05.c -o 指针Demo05 } ; if ($?) { .\指针Demo05 }
pointer value,dereferenced pointer,pointer address:
ptr1 = 000000000061FE00,*ptr1 = 100,&ptr1 = 000000000061FDF8

adding an int to a pointer:
ptr1 + 4 = 000000000061FE10, *(ptr + 4) = 500

values after ptr1++:
ptr1 = 000000000061FE04,*ptr1 = 200,&ptr1 = 000000000061FDF8

values after ptr2--:
ptr2 = 000000000061FE04,*ptr2 = 200,&ptr2 = 000000000061FDF0

Pointers reset to original values:
ptr1 = 000000000061FE00,ptr2 = 000000000061FE08

subtracting one pointer from another:
ptr2 = 000000000061FE08,ptr1 = 000000000061FE00,ptr2 - ptr1 = 2

subtracting an int from a poniter:
ptr3 = 000000000061FE10,ptr3 - 2 = 000000000061FE08
复制代码

解析

  • 赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。在该例中,把urn数组的首地址赋给了ptr1,该地址的编号恰好是000000000061FE00。变量ptr2获得数组urn 的第3个元素(urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把 double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。

  • 解引用: 运算符给出指针指向地址上储存的值。因此,ptr1的初值是100,该值储存在编号为0x7fff5fbff8d0的地址上。

    取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,s运算符给出指针本身的地址。本例中,ptr1储存在内存编号为000000000061FDF8 的地址上,该存储单元储存的内容是000000000061FE00,即urn的地址。因此&ptr1是指向ptrl的指针,而ptr1是指向utn[0]的指针。

  • 指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptrl +4与&urn [ 4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

  • 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节),ptr1指向urn[1]。现在ptr1的值是Ox7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[ 1]的值)。注意,ptr1本身的地址仍是0x7fff5fbff8c8。毕竟,变量不会因为值发生变化就移动位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iA5PPWYe-1634202380888)(E:\Typora\Image\image-20211013114511154.png)]

  • **指针减去一个整数:**可以使用-运算符从一个指针中减去一个整数。指针必须是第Ⅰ个运算对象,整数是第⒉个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与&urn [2]等价,因为ptr3指向的是&arn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

  • **递减指针:**当然,除了递增指针还可以递减指针。在本例中,递减ptr3使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn [ 1]。

  • **指针求差:**可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,**ptr2 - ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。**只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。

  • **比较:**使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

小知识点

解引用未初始化的指针 说到注意事项,一定要牢记一点:千万不要解引用未初始化的指针。例如,考虑下面的例子:

int * pt; 	//未初始化的指针
*pt = 5;	//严重的错误
复制代码

为何不行?第2行的意思是把5储存在pt 指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将储存在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃.切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。例如,可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况)。或者还可以使用第12章将介绍的malloc()函数先分配内存。无论如何,使用指针时一定要注意,不要解引用未初始化的指针!

八、保护数组中的数据

编写一个处理基本类型(如, int)的函数时,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。 传递地址会导致一些问题。C通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。

对形参使用const

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E12FwCbB-1634202380890)(E:\Typora\Image\image-20211013115342783.png)]

以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似ar [ i]++的表达式,编译器会捕获这个错误,并生成一条错误信息。 这里一定要理解,**这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。**一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用const:如果编写的函数不用修改数组,那么在声明数组形参时最好使用const。

九、指针和多维数组的关系

书上写的也很详细

b站有个视频写的也挺好,分享出来

www.bilibili.com/video/BV1MJ…

下面这个是我做的笔记(下次一定把字写好) 在这里插入图片描述

此时,C和C[0]的值都是二维数组的首地址

实例

#include <stdio.h>

int main(void)
{
    int zippo[4][2] = {{2, 4}, {6, 8}, {1, 3}, {5, 7}};

    //zippon 拉链
    printf("zippo = %p,zippo + 1 = %p\n", zippo, zippo + 1);            //zippo和zippo[0]都是二维数组的首地址
    printf("zippo[0] = %p, zippo[0]+1 = %p\n", zippo[0], zippo[0] + 1); //zippo+1是指向第二个一维数组,zippo[0]+1是第一个一维数组的第二个元素
    printf("*zippo = %p,*zippo+1 = %p\n", *zippo, *zippo + 1);
    printf("zippo[0][0] = %d\n", zippo[0][0]);
    printf("*zippo[0] = %d\n", *zippo[0]);
    printf("**zippo = %d \n", **zippo);
    printf("zippon[2][1] = %d\n", zippo[2][1]);
    printf("*(*(zippo+2)+1) = %d\n", *(*(zippo + 2) + 1));

    return 0;
}


输出的结果:
PS D:\Code\C\指针> cd "d:\Code\C\指针\" ; if ($?) { gcc 指针Demo06.c -o 指针Demo06 } ; if ($?) { .\指针Demo06 }
zippo = 000000000061FE00,zippo + 1 = 000000000061FE08
zippo[0] = 000000000061FE00, zippo[0]+1 = 000000000061FE04
*zippo = 000000000061FE00,*zippo+1 = 000000000061FE04
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippon[2][1] = 3
*(*(zippo+2)+1) = 3
复制代码

解析

​ 其他系统显示的地址值和地址形式可能不同,但是地址之间的关系与以上输出相同。该输出显示了二维数组zippo的地址和一维数组zippo[0]的地址相同。它们的地址都是各自数组首元素的地址,因而与&zippo [0] [0]的值也相同。 ​ 尽管如此,它们也有差别。在我们的系统中, int是4字节。前面讨论过,zippo[0]指向一个4字节的数据对象。zippo[0]加1,其值加4。

数组名zippo 是一个内含2个int类型值的数组的地址,所以zippo指向一个8字节的数据对象。因此,zippo 加1,它所指向的地址加8字节

​ 该程序演示了zippo[0]和zippo完全相同,实际上确实如此。然后,对二维数组名解引用两次,得到储存在数组中的值。使用两个间接运算符(*)或者使用两对方括号([])都能获得该值(还可以使用一个和一对[ ],但是我们暂不讨论这么多情况)。

​ 要特别注意,与 zippo[2] [1]等价的指针表示法是 ((zippo+2) + 1)。看上去比较复杂,应最好能理解。下面列出了理解该表达式的思路:

zippo 二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)

zippo + 2 二维数组的第三个一维数组的地址

*(zippo + 2)二维数组的第三个一维数组的地址首元素(一个int类型的值)地址

*(zippo + 2)+ 1 二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址

*(*(zippo + 2)+ 1) 二维数组的第3个元素(即一维数组)的第2个元素的值,即zippo[2][1]
复制代码

以上分析并不是为了说明用指针表示法(* (*(zippo+2) + 1))代替数组表示法(zippo[2][1]),而是提示读者,如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jtxhshh3-1634202380894)(E:\Typora\Image\image-20211013145906395.png)]

十、总结

数组是一组数据类型相同的元素。数组元素按顺序储存在内存中,通过整数下标(或索引)可以访问各元素。在c中,数组首元素的下标是0,所以对于内含n 个元素的数组,其最后一个元素的下标是n-1。作为程序员,要确保使用有效的数组下标,因为编译器和运行的程序都不会检查下标的有效性。

把数组名解释为该数组首元素的地址。换言之,数组名与指向该数组首元素的指针等价。概括地说,数组和指针的关系十分密切。如果ar是一个数组,那么表达式ar[i]和 (ar+i)等价。*

二维数组即是数组的数组。例如,下面声明了一个二维数组:

double sales [5] [12] ;
复制代码

该数组名为sales,有5个元素(一维数组),每个元素都是一个内含12个double类型值的数组。第1个一维数组是sales [0],第2个一维数组是sales [1],以此类推,每个元素都是内含12个double类型值的数组。使用第2个下标可以访问这些一维数组中的特定元素。例如,sales [2] [5]是slaes[2]的第6个元素,而sales [ 2]是sales的第3个元素。

十一、复习题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8jObRjC-1634202380895)(E:\Typora\Image\image-20211013150637462.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iP9K5D57-1634202380897)(E:\Typora\Image\image-20211013150649614.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bg0cTyIl-1634202380899)(E:\Typora\Image\image-20211013150722678.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeNDrLnu-1634202380900)(E:\Typora\Image\image-20211013150731511.png)]

答案再电子书有,可以自行下载

编程题不再放了,都在电子书,

猜你喜欢

转载自juejin.im/post/7018849307063222285