• 二级指针与二维数组


    最近看《Linux C程序设计大全》这本书,虽然书中有一些错误,但整体来说,书写得还算可以。

    当看到网络编程【第23.2.4小节 获得主机信息】时,遇到了一段代码,原文如下:

    “一台主机有许多和网络相关的信息,例如,主机名称、IP地址、主机提供的服务等。这些信息一般都保存在系统中的某个文件里(例如/etc/hosts等),用户程序可以通过系统提供的函数读取这些文件上的内容。Linux环境下使用gethostent函数读取和主机有关的信息,该函数的原型如下:

     

    1 #include <netdb.h>
    2 
    3 struct hostent * gethostent(void);

    该函数从系统的/etc/hosts文件中读取主机相关信息,并将其内容存储在系统中的一个静态缓冲区中,返回该静态缓冲区的首地址;如果失败则返回NULL。该结构定义在netdb.h文件中(netdb.h位于/usr/include目录下),其原型如下:

    1 #include <netdb.h>
    2 
    3 struct hostent{
    4     char * h_name;  /* 正式主机名,每个主机只有一个  */
    5     char **h_aliases; /* 主机别名列表,可以有很多个,以二维数组形式存储  */
    6     int h_addrtype; /* IP地址类型,可以选择IPv4或者IPv6  */
    7     int h_length; /* IP地址长度,IPv4对应4字节的地址长度  */
    8     char **h_addr_list;  /* IP地址列表,h_addr_list[0]为主机的IP地址  */
    9 };

    而在下面实例获取主机信息中的代码中,作者先用如下代码声明了 指向struct hostent类型的指针,并通过获取结构体的成员变量获取主机信息,其中有这样一段代码我没有弄懂。

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <netdb.h>
     4 #include <arpa/inet.h>
     5 
     6 #define    NET_ADDR    16    /*16个字节, 用于存放点分十进制IP地址的字符串 */
     7 
     8 int main(void)
     9 {
    10 
    11     struct hosten * host;/* 用于存放主机信息 */
    12     char addr_p[NET_ADDR];    /*用于存储点分十进制IP地址的字符串*/
    13     int i;
    14  
    15     if ( (host = gethosten()) == NULL ) { /*获得主机信息*/
    16         perror("fail to get host's information
    ");
    17         exit(1);
    18     }  
    19    
    20     printf("%s
    ", host->h_name);/*打印主机名*/
    21     for (i=0; host->h_aliases[i] != NULL; i++) {/* 打印主机别名 */
    22         printf("%s
    ", host->h_aliases[i]);
    23     }
    24   
    25     if (host->h_addrtype == AF_INET) { /* 打印地址类型 */
    26         printf("af_inet
    ");
    27     }
    28     else {
    29         printf("unix_inet
    ");
    30     }
    31 
    32     printf("%d
    ", host->h_length);  /* 打印地址长度*/
    33 
    34     for(i=0; host->h_addr_list[i] != NULL; i++) {/*打印主机IP地址*/
    35            /*该地址以二进制数形式存储,转换为字符串形式*/
    36         printf("%s
    ", inet_ntop(host->h_addrtype,
    37             host->h_addr_list[i], addr_p, NET_ADDR));
    38     }
    39 
    40 
    41     return 0;
    42 
    43 }

     “

    其中结构体类型struct hostent中的成员,char **h_aliases;     这里的h_aliases表示的是二级指针,它指向的是  指向char类型的指针, 即它是指向char类型指针的指针。

    困惑:为何二级指针可以直接在下面第22行处直接h_aliases[i]这样使用呢?二级指针和二维数组到底是什么关系? 这个问题先放在这里,段暂且不表。

    通过翻阅资料,联想到自己以前指针和数组这部分也学得不够扎实,正好弥补下。

    先从数组的名字和对数组的名字进行取地址运算得到的值,开始说起。

    在http://bbs.csdn.net/topics/310254311论坛下的第2楼,ID为hit_flying的解答是:有一句比较拗口的话,你对数组名取地址当然取到的是数组的地址,而不幸的是c又规定数组名的值就是数组地址。

    以及hit_flying在12楼的补充:

      对于数组int a[3][4],都会说是2维数组a,那么&a当然是取数组的地址,这个有什么可怀疑的,而a本身又是数组的首地址,所以&a和a的值是一样的,而sizeof的区别就看编译器是把你当成指针来处理还是当成数组来处理,这点借鉴一下4楼的说法,可能&a会被有些编译器当成一个指针

    并且经过如下代码验证:

    《插入代码处》

    得出的结论:

    假设有如下声明

    1 int a[3][4]

    那么 这里的数组名字a表示的是数组的首地址,也就是相当于 &a[0][0]的值,也就是说,a的值就是数组元素a[0][0]的地址。

    而,对数组的名字取地址运算,即&a,得到的是整个数组所占用的内存空间的起始地址,也就是整个数组的起始地址,又已知数组是按行存储的,第一个元素一定是a[0][0],所以,

    所以,对&a得到的是数组元素a[0][0]的地址,由此我们得出:&a的值和a的值是相同的

    正好在C语言中文网看到这篇文章讲述指针和数组的,也一并转到这里。 

    http://c.biancheng.net/cpp/html/477.html

    多维数组与多级指针也是初学者感觉迷糊的一个地方。超过二维的数组和超过二级的指针其实并不多用。如果能弄明白二维数组与二级指针,那二维以上的也不是什么问题了。所以本节重点讨论二维数组与二级指针。

    一、二维数组

    1、假想中的二维数组布局
    我们前面讨论过,数组里面可以存任何数据,除了函数。下面就详细讨论讨论数组里面存数组的情况。Excel 表,我相信大家都见过。我们平时就可以把二维数组假想成一个excel表,比如:
       char a[3][4];


    2、内存与尺子的对比
    实际上内存不是表状的,而是线性的。见过尺子吧?尺子和我们的内存非常相似。一般尺子上最小刻度为毫米,而内存的最小单位为1 个byte。平时我们说32 毫米,是指以零开始偏移32 毫米;平时我们说内存地址为0x0000FF00 也是指从内存零地址开始偏移0x0000FF00 个byte。既然内存是线性的,那二维数组在内存里面肯定也是线性存储的。实际上其内存布局如下图:


    以数组下标的方式来访问其中的某个元素:a[i][j]。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。a[3]这个一维数组的三个元素分别为:
    a[0],a[1],a[2]。每个元素的大小为sizeof(a[0]),即sizof(char)*4。由此可以计算出a[0],a[1],a[2]三个元素的首地址分别为& a[0],& a[0]+ 1*sizof(char)*4,& a[0]+ 2*sizof(char)*4。亦即a[i]的首地址为& a[0]+ i*sizof(char)*4。这时候再考虑a[i]里面的内容。就本例而言,a[i]内有4个char 类型的元素,其每个元素的首地址分别为&a[i],&a[i]+1*sizof(char),&a[i]+2*sizof(char)&a[i]+3*sizof(char),即a[i][j]的首地址为&a[i]+j*sizof(char)。再把&a[i]的值用a 表示,得到a[i][j]元素的首地址为:a+ i*sizof(char)*4+ j*sizof(char)。同样,可以换算成以指针的形式表示:*(*(a+i)+j)。

    经过上面的讲解,相信你已经掌握了二维数组在内存里面的布局了。下面就看一个题:
    #include <stdio.h>
    intmain(int argc,char * argv[])
    {
       int a [3][2]={(0,1),(2,3),(4,5)};
       int *p;
       p=a [0];
       printf("%d",p[0]);
    }
    问打印出来的结果是多少?

    很多人都觉得这太简单了,很快就能把答案告诉我:0。不过很可惜,错了。答案应该是1。如果你也认为是0,那你实在应该好好看看这个题。花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式!其实这个赋值就相当于
       int a [3][2]={ 1, 3,5};
    所以,在初始化二维数组的时候一定要注意,别不小心把应该用的花括号写成小括号
    了。

    3、&p[4][2] - &a[4][2]的值为多少?
    上面的问题似乎还比较好理解,下面再看一个例子:
       int a[5][5];
       int (*p)[4];
       p = a;
    问&p[4][2] - &a[4][2]的值为多少?

    这个问题似乎非常简单,但是几乎没有人答对了。我们可以先写代码测试一下其值,然后分析一下到底是为什么。在Visual C++6.0 里,测试代码如下:
    intmain()
    {
       int a[5][5];
       int (*p)[4];
       p = a;
       printf("a_ptr=%#p,p_ptr=%#p ",&a[4][2],&p[4][2]);
       printf("%p,%d ",&p[4][2] - &a[4][2],&p[4][2] - &a[4][2]);
       return 0;
    }
    经过测试,可知&p[4][2] - &a[4][2]的值为-4。这到底是为什么呢?下面我们就来分析一下:前面我们讲过,当数组名a 作为右值时,代表的是数组首元素的首地址。这里的a 为二维数组,我们把数组a 看作是包含5 个int 类型元素的一维数组,里面再存储了一个一维数组。

    如此,则a 在这里代表的是a[0]的首地址。a+1 表示的是一维数组a 的第二个元素。a[4]表示的是一维数组a 的第5 个元素,而这个元素里又存了一个一维数组。所以&a[4][2]表示的是&a[0][0]+4*5*sizeof(int) + 2*sizeof(int)。

    根据定义,p 是指向一个包含4 个元素的数组的指针。也就是说p+1 表示的是指针p 向后移动了一个“包含4 个int 类型元素的数组”。这里1 的单位是p 所指向的空间,即4*sizeof(int)。所以,p[4]相对于p[0]来说是向后移动了4 个“包含4 个int 类型元素的数组”,即&p[4]表示的是&p[0]+4*4*sizeof(int)。由于p 被初始化为&a[0],那么&p[4][2]表示的是&a[0][0]+4*4*sizeof(int)+2* sizeof(int)。

    再由上面的讲述,&p[4][2] 和&a[4][2]的值相差4 个int 类型的元素。现在,上面测试出来的结果也可以理解了吧?其实我们最简单的办法就是画内存布局图:


    这里最重要的一点就是明白数组指针p 所指向的内存到底是什么。解决这类问题的最好办法就是画内存布局图。

    二、二级指针

    1、二级指针的内存布局
    二级指针是经常用到的,尤其与二维数组在一起的时候更是令人迷糊。例如:
       char **p;
    定义了一个二级指针变量p。p 是一个指针变量,毫无疑问在32 位系统下占4 个byte。

    它与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地址。下图帮助理解:

    我们试着给变量p 初始化:
    A)
    p = NULL;
    B)
    char *p2; p = &p2;
    任何指针变量都可以被初始化为NULL(注意是NULL,不是NUL,更不是null),二级指针也不例外。也就是说把指针指向数组的零地址。联想到前面我们把尺子比作内存,如果把内存初始化为NULL,就相当于把指针指向尺子上0 毫米处,这时候指针没有任何内存可用。

    当我们真正需要使用p 的时候,就必须把一个一级指针的地址保存到p 中,所以B)的赋值方式也是正确的。

    看完此文,还是有点不够过瘾,于是又找到了这篇文章:

    http://www.360doc.com/content/11/0506/22/6903212_114913991.shtml

    先看个简单的:char *p,这定义了一个指针,指针指向的数据类型是字符型,char  *(p)定义了一个指针P;

    char *p[4], 为指针数组,由于[]的优先级高于*,所以p先和[]结合,p[]是一个数组,暂时把p[]看成是q,也就是char *(q),定义了一个指针q,只不过q是一个数组罢了,故定义了一个数组,数组里面的数据是char *的,所以数组里面的数据为指针类型。所以char *p[4]是四个指针,这四个指针组成了一个数组,称为指针数组,既有多个指针组成的数组。

    char(*p)[4],数组指针,强制改变优先级,*先与p结合,使p成为一个指针,这个指针指向了一个具有4个char型数据的数组。故p中存放了这个char型数组的首地址,可用数组指针动态内存申请:

                                          char (*p)[10];

                                          p=(char*)malloc(sizeof(char[x])*N);

    char *f(char,char),指针函数,()的优先级高于*,故f先与()结合,成为函数f(),函数的返回值是char *类型的,故返回值是一个指针。

    char (*f)(char,char),函数指针,*与f结合成为一个指针,这个指针指向函数的入口地址。函数名就是函数的首地址。函数指针是指向函数的指针变量。 因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是一致的。函数指针有两个用途:调用函数和做函数的参数。

     int func(int x); /* 声明一个函数 */ 
     int (*f) (int x); /* 声明一个函数指针 */ 
     f=func; /* 将func函数的首地址赋给指针f */

    以后如果要调用函数func(),也就可以这样调用:(*f)();

    终于回到正题了,关于二级指针和二维数组

    /****************************************************二级指针**************************************************/

    二级指针简单来说就是指向指针的指针。

    char a=200;

    char *p;

    char **q;//q是一个二级指针

    p=&a;

    q=&p;  //q指向指针p

    假设变量a在内存中的地址为2000H,则它们的关系就如下面的示意图:

    指针数组,数组指针,指针函数,函数指针,二级指针详解 - chopin_tech - chopin_tech的博客

    指针p指向a,p的值是2000H,*p就是取地址2000H中的值即a为200,而p本身的地址是4000H,q指向指针p,*q就是取地址4000H中的值即p的值为2000H,而**q就是取地址2000H中的值即200。

    所以:

    *p==200;

    *q=2000H;

    **q=200;

    以上的q是一个指针指针的二级指针,然而还有指向数组的二级指针。

     当一个指针变量指向另一个指针变量时,则形成二级指针。使用二级指针可以在建立复杂的数据结构时提供较大的灵活性,能够实现其他语言所难以实现的一些功能。定义二级指针的形式是:

      类型标识符**二级指针变量名

      定义指针的同时可以对其赋值,然后就可以使用了。

      如果定义一个指针数组,则指针数组名就是一个二级指针。用指针数组元素值指向长度同的字符串,操作时可以节省内存空间,而对地址进行操作,提高了运行效率。

    char   s[3][5]={ "abc ", "uio ", "qwe "}; 
    可以看成是三个指向字符串的一级指针(s[0],s[1],s[2]),由s[3]得。 
    而s本身又是一个一维数组存储s[0],s[1],s[2]三个一级指针,则s就可以看作是一个二级指针,即指向指针的指针。 
    这时定义一个二级指针char**p;就能通过p访问二维数组了。

    也可以这样char *p[] = {“ab“, “cd“, “ef“};定义了一个指针数组.

    char **sp = p;

    就可以使用sp[i]来访问字符串了。

     

    大家都知道,要想在函数中改变形参的值,形参用指针传递就行了。

    比如:

    void f(char *p1,char *p2)

    {

        *p1=10;

        *p2=20;

    }

    void main()

    {

          char a,b;

          char *p,*q;

          p=&a;

          q=&b;

          f(p,q);

    }

     执行后此时a=10,b=20;

    原理如下:

    当调用函数f后,p1指向a,p2指向b;

    指针数组,数组指针,指针函数,函数指针,二级指针详解 - chopin_tech - chopin_tech的博客
     接着*p1=10; *p2=20;使p1指向的地址空间的值赋为10,p2指向的地址空间的值赋为20;
    指针数组,数组指针,指针函数,函数指针,二级指针详解 - chopin_tech - chopin_tech的博客
     然后函数调用结束,这时a=10,b=20;
    如果要在函数中改变指针的值,比如改变p,q的值就需要用到二级指针。
    void GetMemory(char **p, int num)
    {
           *p = (char *)malloc(sizeof(char) * num);
    }

    以上函数,就实现了在函数中改变指针的值,使指针指向新申请的空间。

    (To be continued...)

    参考:  

    http://bbs.csdn.net/topics/310254311

    http://c.biancheng.net/cpp/html/477.html

    http://www.360doc.com/content/11/0506/22/6903212_114913991.shtml

    转载本Blog文章请注明出处,否则,本作者保留追究其法律责任的权利。 本人转载别人或者copy别人的博客内容的部分,会尽量附上原文出处,仅供学习交流之用,如有侵权,联系立删。
  • 相关阅读:
    laravel tinker的使用
    清空表中数据
    不要为过多思考浪费你的精力
    #tomcat#启动过程分析(上)
    #hashMap冲突原理#详细
    #数组集合知识#HashMap的实现原理
    #数据库#连接数据库的几个步骤
    #数据库#JDBC基础知识
    #数据库#查询语句 1=1的使用条件
    #tomcat#虚拟主机配置及访问(三)
  • 原文地址:https://www.cnblogs.com/drfxiaoliuzi/p/4795951.html
Copyright © 2020-2023  润新知