小言_互联网的博客

指针剖析(下)

256人阅读  评论(0)

指针常见的使用方式和误区

指针剖析(上)主要在概念和行为上介绍了指针的特点。这篇将对指针的常用使用方式做一下介绍。比如指针的运算以及它和普通变量运算的区别、常见的指针类型(int *pint *p[n]int (*p)[n]int (* p)()int * fun(void)int **p之间的区别和含义)以及二级指针用在何处。

指针的运算

对于指针的运算,玩c的都很熟悉。比如p++、p--、p1==p2、p1-p2等。但使用过程中发现有很多对于p++之后p等于多少理解比较模糊,纯靠调试尝试。先记住一句话,后面会解释,指针的运算和变量的运算不一样,指针变量的运算和指针的类型有关。

p++ 到底加了几

指针变量的加减运算一般都用于操作数组。读者首先需要理清,指针变量p++的物理意义与整型变量a++的意义是不同的。指针运算p++的意义是指向下一个数据元素的首地址。因此p++到底加了多少取决于p的指针类型(charintfloatstruct结构体 等)。所以sizeof(类型)多大,p++就加多大。如定义int *p,假设p的地址为0x0000,sizeof(int)等于4,那么p++后,p就等于0x0004。可以看到,p++的实质是p=p+sizeof(int).

void main(void)
{
    int aInt[10];
    int *pInt = aInt;
    pInt ++;                        //pInt = pInt + sizeof(int);
                                    //pInt由指向aInt[0]变成了指向aInt[1]
    char aChar[10];
    char *pChar = aChar;
    pChar ++;                       //pChar = pChar + sizeof(char);

    struct Info{int a;int b;char c;float d;};
    struct Info myInfo[2];
    struct Info *pMyInfo = myInfo;
    pMyInfo ++;                     //pMyInfo = pMyInfo + sizeof(struct Info);

    /* p_void = p_void + sizeof(void) 而不是 p_void + sizeof(struct Info)。因为它取决于自己的指针类型,跟赋值是什么没有关系  */
    void *p_void = &myInfo;
    p_void ++;                      //p_void = p_void + sizeof(void);

    union test_union{char a;float b;};
    union test_union mem[2];
    union test_union *pMem = mem;
    pMem++;                         //pMem = pMem + sizeof( union test_union);

    enum weekday{ Sunday,Monday,Tuesday};
    enum weekday tadayIs[2];
    enum weekday *pTayIs = tadayIs;
    pTayIs++;                       //pTayIs = pTayIs + sizeof( enum weekday);
}

还有一个比较常见的且初学者特别容易迷惑的。指针指向二维数组如int a[3][4],*p; p=&a[0][0];那么p+1指向a数组的哪个地址呢?答案是指向了a[0][1]。还是上面那句话,指针p是int型的,所以p+1就指向了a[0][1].

p - q 等于几

两个指针可以进行相减运算,一般用于计算两个指针所指数组元素之间的包含的元素个数。指针运算p-q的意义是 两个指针所指数组元素之间的包含的元素个数。如定义int a[10]; int *q = &a[1]; int *p = &a[4],假设a数组的起始地址为0x0000,那么q的内容就为0x0004,p的内容为0x0010。根据上面p-q的定义,p-q就为4-1=3(a[1]与a[4]之间的元素个数).对,没错p-q的值就应该是3,而不是0x0010 - 0x0004 = 12;这是很多对指针有一定理解人的误区。所以一定不要把指针的运算和整型变量的运算搞混。指针变量的运算和指针类型有关。比如(p-q的实质是0x0010-0x0004)/sizeof(int)=3.p++的实质是p=p+sizeof(int).
注意,p-q只有指向同一个数组做减法才有意义。除此之外,两个指针做加法没有意义,如p+q。

int a[10];          //分配起始地址为0x0000
int *p = &a[4];     //p内容为0x0010
int *p = &a[1];     //q内容为0x0004
int a = p - q;      //a = 3

总之,指针的运算和变量的运算不一样,指针变量的运算和指针的类型有关。

*p[4](*p)[4]int (* p)() 这三个都是什么鬼

*p[4](*p)[4]int (* p)()这三个都是什么鬼?我想第一个都应该都很眼熟。再简单一点,int *p眼熟吧,就是定义一个指针变量。而int *p[4]就是定义一个指针数组。跟int a[4]差不多。(*p)[4]就不怎么熟悉了,其实它的用途是为了解决指针和多维数组值之间的关系。c语言专门为多维数组定义了一种指针变量,叫行指针变量,记住int (*p)[4]的学名就叫行指针变量,其中方括号中的值是二维数组的列数(暂且不讨论多维数组)。(*p)[4]的含义就是指向每行包括4个元素的二维数组,如可以指向数组a[8][4]。int (* p)()就是我们常说的函数指针,具体见后文详细介绍。

细说(*p)[4]

上面已经说了它的前世,但它怎么用了,指针变量*p也能指向二维数组,为何又要用行指针变量(*p)[n]呢。这就又回到了指针与二维数组的渊源了。我们都知道二维数组中的元素在内存中是按行的顺序存放的,即在内存中先存放第一行的元素,再存放第二行的元素。其实指针变量在操作二维数组是对数组进行了降维(看成一位数组操作),如定义二维数组int a[8][4],令指针变量int *p指向首地址p=a。那么要操作数组元素a[2][1],只能是用*(p+4*2 +1)或者p[4*2+1]。看吧,它只能降维来处理。但用行指针变量就不一样了。令行指针变量 int (*prow)[4]指向数组首地址prow=a.那么同样操作数组元素a[2][1],就可以用*(*(prow+2)+1)或者prow[2][1]。这样比较直观、易懂。

void main(void)
{
    int a[8][4];
    int *p,(*prow)[4];

    p = a[0];
    prow = a;
    a[2][1] = 2019;

    /* 运行结果都是2019 */
    printf("p is %d \r\n",p[4*2 + 1]);
    printf("p is %d \r\n",*(p + 4*2 + 1));
    printf("prow is %d \r\n",prow[2][1]);
    printf("prow is %d \r\n",*(*(prow + 2) + 1) );
}

细说 int (* p)()

对于函数指针,除了要和指针函数区别开来,也没其他误区了。网上都喜欢用int (*p)()int *p()来提示让读者区分。哈哈,这哪是提示,简直是在引导混淆。读者只要记住指针函数和普通函数一样,如void main(void),唯一的区别就是返回类型变成了指针类型,如int *testfunction(void)。他们都是函数,和变量没有任何关系的。要注意一点,函数也有自己的起始地址(学名好像叫入口地址),可以用强制类型转化查看函数的地址,如(uint32_t)(main)返回的就是main()函数的地址常量值。因为函数是有地址的所以函数指针int (* p)()的存在便有了意义。在初始化中给函数指针赋函数的地址,在后面程序中就可以用函数指针来调用此函数。例如函数回调等。除此之外,还可以用于形参中,这样就可以函数动态调用函数了。两者的例子如下。

#include <stdio.h>
#include <windows.h>

/* 函数指针用于形参 */
int min_max(int (* pFun)(),int a,int b)
{
    return (* pFun)(a,b);/* 函数指针的调用 */
}

int MinFun(int a, int b)
{
    return a < b ? a : b;
}

int MaxFun(int a, int b)
{
    return a > b ? a : b;
}

void main(void)
{
    /* 获取main函数的起始地址 */
    long long programAddr = (long long)main;
    printf("main函数起始地址: 0X%016X \r\n",programAddr);

    int a = 4,b = 5,maxVal,minval;
    int (* pFun)(); /* 定义函数指针 */

    pFun = MaxFun;  /* 给函数指针赋值 */
    maxVal = min_max(pFun,a,b);

    pFun = MinFun;
    minval = min_max(pFun,a,b);

    printf("最大值: %d  最小值: %d \r\n",maxVal,minval);
    /* 打印结果为 俩地址相同 */
    printf("pFun is: 0X%016X  MinFun is: 0X%016X\r\n",pFun,(long long)MinFun);

    system("pause");
}

二级指针 **p

在上一篇文章中已经剖析了二级指针,但没有讨论它实际的用途以及使用过程中的误区。总给人一种摸不到头脑的感觉。二级指针的使用主要是为了让子函数能够修改调用函数中指针变量的内容。例子如下


static int treeNode;

/* 此处的形参必须是二级指针 */
void GetTopNode(int **pNode)
{
    /* pNode = &pTemp => *pNode = pTemp */
    *pNode = &treeNode;
}

void main(void)
{
    int *pTemp; /* 目的就是给pTemp赋值 */

    GetTopNode(&pTemp); /* 通过函数形参返回变量指针,此处必须是&pTemp */
    /* pTemp在子函数中被修改,所以打印结果是 pTemp 与 &treeNode 地址一样 */
    printf("treeNode is: 0x%08X   pTemp is: 0x%08X \r\n",&treeNode,pTemp);

    system("pause");
}

上面的程序是通过函数形参返回变量指针的例子,有很多的细节需要细读。第一点,main函数里语句GetTopNode(&pTemp)中的&pTemp,因为函数里要对pTemp进行修改、赋值,所以pTemp必须传入的是它本身的地址。还记得上篇文章中讲的&pp*p的区别吗?可以重新翻看一下。第二点,因为pTmep本身是指针,又需要对其取地址,那么GetTopNode()函数里的形参就必须为二级指针。第三点,*pNode = &treeNode,因为treeNode需要将指针传给形参,所以必须取地址。那*pNode是怎么出现的了,因为要改变pTemp的内容,而pNode是pTmep的地址,根据上一篇讲解的知识,*pNode就指向了pTemp的内容,所以便有了*pNode = &treeNode。上面的例子可以简化成更让人理解的形式,代码如下:

int *pTmep,**pNode;
pNode = &pTmep;
*pNode = &treeNode;

有很多人对这儿存在错误的理解,如用GetTopNode(int *pNode)不行吗?回答是肯定不行。因为如果函数形参是一级指针,那么实体就也是一级指针pTemp而不是&pTemp.这个问题就变成了类似如下的代码。你说,最后的pTemp能等于&treeNode?当然不能,都没对pTmep赋值,它怎么能变了。

int *pTmep,*pNode;
pNode = pTmep;
pNode = &treeNode;

总结一下,指针数据类型有如下一些,如果对这些都掌握了,估计看见指针都就认识了。

  1. int *p---------->指针变量
  2. int *p[n]------->指针数组,由n个指针元素组成
  3. int (*p)[n]----->行指针变量,配合二维数组使用
  4. int * fun(void)->指针函数
  5. int (* p)()----->函数指针
  6. int **p--------->二级指针(指向指针的指针)

关于技术交流

此处后的文字已经和题目内容无关,可以不看。
qq群:825695030
微信公众号:嵌入式的日常
如果上面的文章对你有用,欢迎打赏、点赞、评论。


转载:https://blog.csdn.net/qq_23274715/article/details/102490860
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场