首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

C语言深度解剖读书笔记(4.指针的故事)

C语言深度解剖读书笔记(4.指针的故事)

指针这一节是本书中最难的一节,尤其是二级指针和二维数组直接的关系。
本节知识点:1.指针基础,一张图说明什么是指针:


2.跨过指针,直接去访问一块内存:
    只要你能保证这个地址是有效的 ,就可以这样去访问一个地址的内存*((unsigned int *)(0x0022ff4c))=10;  但是前提是 0x0022ff4c是有效地址。对于不同的编译器这样的用法还不一样,一些严格的编译器,当你定义一个指针,把这个指针赋值为一个这样的地址的时候,当检测到地址无效,编译的时候就会报错!!!如果一些不太严格的编译器,不管地址有效无效都会编译通过,但是对于无效地址,当你访问这块地址的时候,程序就会运行停止!
3.a     &a    &a[0]三者的区别:
首先说三者的值是一样的,但是意义是不一样的。(这里仅仅粗略的说说,详细见文章<c语言中数组名a和&a>)
     &a[0]:这个是数组首元素的地址
     a : 的第一个意义是 数组首元素的地址,此时与&a[0]完全相同
                第二个意义是 数组名  sizeof(a)  为整体数组有多少个字节

    &a :这个是数组的地址 。跟a的区别就是,a是一个 int* 的指针(在第一种意义的时候) ,而&a是一个 int (*p)[5]类型的数组指针,指针运算的结果不一样。(此处的int* 仅仅是为了举例子,具体应该视情况而定)
4.指针运算(本节最重要的知识点,但并不是最难的,所以的问题都来源于这儿):   
对于指针的运算,首先要清楚的是指针类型(在C语言中,数据的类型决定数据的行为),然后对于加减其实就是对这个指针的大小加上或者减去,n*sizeof(这个指针指向的数据的类型)。即:一个类型为T的指针的移动,是以sizeof(T)为单位移动的。如:int* p;  p+1就是p这个指针的值加上sizeof(int)*1,即:(unsigned int)p + sizeof(int)*1。对于什么typedef的,struct的,数组的都是一样的。
这个有一个例子,代码如下:
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>

  • int main(int argc, char *argv[])   
  • {  
  • /*  int a[20]={1,2,4};
  •     printf("%d\n",sizeof(a));
  •     printf("%p\n",a);
  •     printf("%p\n",&a);
  •     printf("%p\n",&a[0]);   
  • */


  • /*  int a[5]={1,2,3,4,5};
  •     int (*p)[5]=&a;
  •     printf("%d\n",*((int *)(p+1)-1));
  • */

  •     int a[5]={1,2,3,4,5};  
  •     int* p=(int *)(&a+1);  
  • //  int *p=&a+1;  //这个条语句是  把&a这个数组指针 进行了指针运算后  的那个地址  强制类型转换成了 int *指针
  •     printf("%d\n",*(p-1));  
  •     return 0;  

  • }  

5.访问指针和访问数组的两种方式:

    分别是以下标方式访问和以指针的方式访问,我觉得没有任何区别,*(p+4)和p[4]是一样的 ,其实都可以理解成指针运算。如果非要说出区别,我觉得指针的方式会快些,但是在当前的硬件和编译器角度看,不会太明显。同样下标的方式可读性可能会高些。
6.切记数组不是指针:

    数组是数组,指针是指针,根本就是两个完全不一样的东西。当然要是在宏观的内存角度看,那一段相同类型的连续空间,可以说的上是数组。但是你可以尝试下,定义一个指针,在其他地方把他声明成数组,看看编译器会不会把两者混为一谈,反过来也不会。

    但是为什么我们会经常弄混呢?第一,我们常常利用指针的方式去访问数组。第二,数组作为函数参数的时候,编译器会把它退化成为指针,因为函数的参数是拷贝,如果是一个很大的数组,拷贝是很浪费内存的,所以数组会被退化成指针(这里一定要理解好,退化的是数组成员的类型指针,不一定是数组指针的哈)。
7.弄清数组的类型:
   数组类型是由数组元素类型数组长度两个因素决定的,这一点在数组中体现的不明显,在数组指针的使用中体现的很好。[cpp] view plaincopy


  • char a[5]={'a','b','c','d','e'};  
  • char (*p)[3]=&a;  

   上面的代码是错误的,为什么?因为数组指针和数组不是一个类型,数组指针是指向一个数组元素为char 长度为3的类型的数组的,而这个数组的类型是数组元素是char长度是5,类型不匹配,所以是错的。

8.字符串问题:
   a.C语言中没有真正的字符串,是用字符数组模拟的,即:字符串就是以'\0'结束的字符数组。
   b.要注意下strlen,strcmp等这个几个函数的返回值,是有符号的还是无符号的,这里很容易忽略返回值类型,造成操作错误。
   c.使用一条语句实现strlen,代码如下(此处注意assert函数的使用,安全性检测很重要):
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <assert.h>

  • int strlen(const
    char* s)  
  • {  
  •     return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );  
  • }  

  • int main()  
  • {  
  •     printf("%d\n", strlen( NULL));  

  •     return 0;  
  • }  

    d.自己动手实现strcpy,代码如下:

[cpp] view plaincopy


  • #include <stdio.h>
  • #include <assert.h>

  • char* strcpy(char* dst, const
    char* src)  
  • {  
  •     char* ret = dst;  

  •     assert(dst && src);  

  •     while( (*dst++ = *src++) != '\0' );  

  •     return ret;  
  • }  

  • int main()  
  • {  
  •     char dst[20];  

  •     printf("%s\n", strcpy(dst, "hello!"));  

  •     return 0;  
  • }  

     e.推荐使用strncpy、strncat、strncmp这类长度受限的函数(这些函数还能在字符串后面自动补充'\0'),不太推荐使用strcpy、strcmpy、strcat等长度不受限仅仅依赖于'\0'进行操作的一系列函数,安全性较低。
     f.补充问题,为什么对于字符串char a[256] = "hello";,在printf和scanf函数中,使用a行,使用&a也行?代码如下:

[cpp] view plaincopy


  • #include <stdio.h>
  • int main()  
  • {  
  •     char* p ="phello";  
  •     char a[256] = "aworld";  
  •     char b[25] = {'b','b','c','d'};  
  •     char (*q)[256]=&a;  

  •     printf("%p\n",a);  //0022fe48
  •     //printf("%p\n",&a);
  •     //printf("%p\n",&a[0]);


  •     printf("tian %s\n",(0x22fe48));   
  •     printf("%s\n",q);    //q就是&a
  •     printf("%s\n",*q);   //q就是a

  •     printf("%s\n",p);  

  •     printf("%s\n",a);  
  •     printf("%s\n",&a);  
  •     printf("%s\n",&a[0]);  

  •     printf("%s\n",b);  
  •     printf("%s\n",&b);  
  •     printf("%s\n",&b[0]);     
  • }  

对于上面的代码:中的0x22fe48是根据打印a的值获得的。
printf("tian %s\n",(0x22fe48));这条语句,可以看出来printf真的是不区分类型啊,完全是根据%s来判断类型。后面只需要一个值,就是字符串的首地址。a、&a、&a[0]三者的值还恰巧相等,所以说三个都行,因为printf根本就不判断指针类型。虽然都行但是我觉得要写有意义的代码,所以最好使用a和*p。还有一个问题就是,char* p = "hello"这是一个char*指针指向hello字符串。所以对于这种方式只能使用p。因为*p是hello字符串的第一个元素,即:‘h’,&p是char* 指针的地址,只有p是保存的hello字符串的首地址,所以只有p可以,其他都不可以。scanf同理,因为&a和a的值相同,且都是数组地址。

9.二维数组(本节最重要的知识点):
      a.对于二维数组来说,二维数组就是一个一维数组 数组,每一个数组成员还是一个数组,比如int a[3][3],可以看做3个一维数组,数组名分别是a[0]  a[1]   a[2]   sizeof(a[0])就是一维数组的大小  ,*a[0]是一维数组首元素的值,&a[0]是 一维数组的数组指针。
      b.也可以通过另一个角度看这个问题。a是二维数组的数组名,数组元素分别是数组名为a[0]、a[1]、a[2]的三个一维数组。对a[0]这个数组来说,它的数组元素分别是a[0][0]  a[0][1]  、 a[0][2]三个元素。a和a[0]都是数组名,但是是两个级别的,a作为数组首元素地址的时候等价于&a[0](最容易出问题的地方在这里,这里一定要弄清此时的a[0]是什么,此时的a[0]是数组名,不是数组首元素的地址,不可以继续等价下去了,千万不能这样想 a是&a[0]    a[0]是&a[0][0]     a就是&&a[0][0] 然后再弄个2级指针出来,自己就蒙了!!!这是一个典型的错误,首先&&a[0][0]就没有任何意义,跟2级指针一点关系都没有,然后a[0]此时不代表数组首元素地址,所以这个等价是不成立的。Ps:一定要搞清概念,很重要!!! ),a[0]作为数组首元素地址的时候等价于&a[0][0]。但是二维数组的数组头有很多讲究,就是a(二维数组名)、&a(二维数组的数组地址)、&a[0](二维数组首元素地址  即a[0]一维数组的数组地址 a有的时候也表示这个意思)、a[0](二维数组的第一个元素 即a[0]一维数组的数组名)、&a[0][0](a[0]一维数组的数组首元素的地址 a[0]有的时候也表示这个意思),这些值都是相等,但是他们类型不相同,行为也就不相同,意义也不相同。分析他们一定要先搞清,他们分别代表什么。
下面是一个,二维数组中指针运算的练习(指针运算的规则不变,类型决定行为):
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <stdbool.h>

  • int main(int argc, char *argv[])   
  • {  
  •     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  •     printf("%d\n",sizeof(a[0]));  
  •     printf("%d\n",*a[2]);  
  •     printf("%d\n",*(a[0]+1));  

  •     printf("%p\n",a[0]);  
  •     printf("%p\n",a[1]);  
  •     printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一样  指针类型不一样   &a[0]+1这个是数组指针  a[1]是&a[1][0] 是int*指针

  •     printf("%d\n",*((int *)(&a[0]+1)));  

  •     printf("%d\n",*(a[1]+1));  

  •     printf("%p\n",a);  
  •     printf("%p\n",&a);  
  •     printf("%p\n",&a[0]);  

  •     printf("%d\n",sizeof(a));   //这是a当作数组名的时候

  •     printf("%d\n",*((int *)(a+1))); //此时 a是数组首元素的地址  数组首元素是a[0]  
  •                  //首元素地址是&a[0]  恰巧a[0]是数组名 &a[0]就变成了数组指针
  •     return 0;  
  • }  

总结:对于a和a[0]、a[1]等这些即当作数组名,又当作数组首元素地址,有时候还当作数组元素(即使当作数组元素,也无非就是当数组名,当数组首元素地址两种),这种特殊的变量,一定要先搞清它现在是当作什么用的

      c.二维数组中一定要注意,大括号,还是小括号,意义不一样的。
10.二维数组和二级指针:
     很多人看到二维数组,都回想到二级指针,首先我要说二级指针跟二维数组毫无关系,真的是一点关系都没有。通过指针类型的分析,就可以看出来两者毫无关系。不要在这个问题上纠结。二级指针只跟指针数组有关系,如果这个二维数组是一个二维的指针数组,那自然就跟二级指针有关系了,其他类型的数组则毫无关系。切记!!!还有就是二级指针与数组指针也毫无关系!!
11.二维数组的访问:
     二维数组有以下的几种访问方式:
     int   a[3][3];对于一个这样的二位数组
     a.方式一:printf("%d\n",a[2][2]);
     b.方式二:printf("%d\n",*(a[1]+1));
     c.方式三:printf("%d\n",*(*(a+1)+1));
     d.方式四:其实二维数组在内存中也是连续的,这么看也是一个一维数组,所以就可以使用这个方式,利用数组成员类型的指针。
[cpp] view plaincopy


  • int *q;  
  • q = (int *)a;  
  • printf("%d\n",*(q+6));  

     e.方式五:二维数组中是由多个一维数组组成的,所以就可以利用数组指针来访问二维数组。
[cpp] view plaincopy


  • int (*p)[3];  
  • p = a;  
  • printf("%d\n",*(*(p+1)+1));  

给一个整体的程序代码:
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <string.h>
  • int main()  
  • {  
  •     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  •     int (*p)[3];  
  •     int *q;   
  •     printf("%d\n",*(*(a+1)+1));   //a        *(&a[0]+1)
  •     p = a;  
  •     q = (int *)a;  
  •     printf("%d\n",*(*(p+1)+1));  
  •     printf("%d\n",*(a[1]+1));  
  •     printf("%d\n",a[1][1]);  
  •     printf("%d\n",*(q+6));  
  • }  
  • <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>  



总结:对于二位数组int a[3][3]  要想定义一个指针指向这个二维数组的数组元素(即a[0]等一维数组),就要使用数组指针,这个数组指针要跟数组类型相同。a[0]等数组类型是元素类型是int,长度是3,所以数组指针就要定义成int (*p)[3]。后面的这个维度一定要匹配上,不然的话类型是不相同的。
这里有一个程序,要记得在c编译器中编译,这个程序能看出类型相同的重要性:

[cpp] view plaincopy


  • <span style="color:#000000;">#include <stdio.h>  

  • int main()  
  • {  
  •     int a[5][5];  
  •     int(*p)[4];  

  •     p = a;  

  •     printf("%d\n", &p[4][2] - &a[4][2]);  
  • }</span>  


12.二级指针:
    a.因为指针同样存在传值调用和传址调用,并且还有指针数组这个东西的存在,所以二级指针还是有它的存在价值的。
    b.常使用二级指针的地方:
          (1)函数中想要改变指针指向的情况,其实也就是函数中指针的传址调用,如:重置动态空间大小,代码如下:
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <malloc.h>

  • int reset(char**p, int size, int new_size)  
  • {  
  •     int ret = 1;  
  •     int i = 0;  
  •     int len = 0;  
  •     char* pt = NULL;  
  •     char* tmp = NULL;  
  •     char* pp = *p;  

  •     if( (p != NULL) && (new_size > 0) )  
  •     {  
  •         pt = (char*)malloc(new_size);  

  •         tmp = pt;  

  •         len = (size < new_size) ? size : new_size;  

  •         for(i=0; i<len; i++)  
  •         {  
  •             *tmp++ = *pp++;        
  •         }  

  •         free(*p);  
  •         *p = pt;  
  •     }  
  •     else
  •     {  
  •         ret = 0;  
  •     }  

  •     return ret;  
  • }  

  • int main()  
  • {  
  •     char* p = (char*)malloc(5);  

  •     printf("%0X\n", p);  

  •     if( reset(&p, 5, 3) )  
  •     {  
  •         printf("%0X\n", p);  
  •     }  

  •     return 0;  
  • }  


             (2)函数中传递指针数组的时候,实参(指针数组)要退化成形参(二级指针)。

             (3)定义一个指针指向指针数组的元素的时候,要使用二级指针。
      c.指针数组:char* p[4]={"afje","bab","ewrw"};  这是一个指针数组,数组中有4个char*型的指针,分别保存的是"afje"、"bab"、"ewrw"3个字符串的地址。p是数组首元素的地址即保存"afje"字符串char*指针的地址。
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <stdbool.h>

  • int main(int argc, char *argv[])   
  • {     
  •     char* p[4]={"afje","bab","ewrw"};  
  •     char* *d=p;   
  •     printf("%s\n",*(p+1));   
  •     printf("%s\n",*(d+1));  //d  &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指针的地址   
  •     return 0;  
  • }  

       d.子函数malloc,主函数free,这是可以的(有两种办法,第一种是利用return 把malloc的地址返回。第二种是利用二级指针,传递一个指针的地址,然后把malloc的地址保存出来)。记住不管函数参数是,指针还是数组, 当改变了指针的指向的时候,就会出问题,因为子函数中的指针就跟主函数的指针不一样了,他只是一个复制品,但可以改变指针指向的内容。这个知识点可以看<在某培训机构的听课笔记>这篇文章。
13.数组作为函数参数:数组作为函数的实参的时候,往往会退化成数组元素类型的指针。如:int a[5],会退化成int*   ;指针数组会退化成二级指针;二维数组会退化成一维数组指针;三维数组会退化成二维数组指针(三维数组的这个是我猜得,如果说错了,希望大家帮我指出来,谢谢)。如图:

二维数组作为实参的例子:
[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <stdbool.h>

  • int fun(int (*b)[3])  //此时的b为  &a[0]
  • {  
  •     printf("%d\n",*(*(b+1)+0));  
  •     printf("%d\n",b[2][2]);// b[2][2] 就是  (*(*(b+2)+2))
  •     printf("%d\n",*(b[1]+2));  
  • }  

  • int main(int argc, char *argv[])   
  • {  
  •     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  •      fun(a);//与下句话等价
  •      fun(&a[0]);      
  •     return 0;  
  • }  

       数组当作实参的时候,会退化成指针。指针当做实参的时候,就是单纯的拷贝了!
14.函数指针与指针函数:
      a.对于函数名来说,它是函数的入口,其实函数的入口就是一个地址,这个函数名也就是这个地址。这一点用汇编语言的思想很容易理解。下面一段代码说明函数名其实就是一个地址,代码如下:

[cpp] view plaincopy


  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <stdbool.h>

  • void abc()  
  • {  
  •     printf("hello fun\n");  
  • }  
  • int main(int argc, char *argv[])   
  • {  
  •     void (*d)();  
  •     void (*p)();  
  •     p = abc;  
  •     abc();  
  •     printf("%p\n",abc);  
  •     printf("%p\n",&abc);//函数abc的地址0x40138c
  •     p();  
  •     (*p)();      
  •     d = ((unsigned int*)0x40138c);  //其实就算d= 0x40138c这么给赋值也没问题
  •     d();  
  •     return 0;  
  • }     


可见函数名就是一个地址,所以函数名abc与&abc没有区别,所以p和*p也没有区别。
    b.我觉得函数指针最重要的是它的应用环境,如回调函数(其实就是利用函数指针,把函数当作参数进行传递)代码如下,还有中断处理函数(同理)详细见<
ok6410学习笔记(16.按键中断控制led)>中的 中断注册函数,request_irq。还有就是函数指针数组,第一次见到函数指针数组是在zigbee协议栈中。
回调函数原理代码:
[cpp] view plaincopy


  • #include <stdio.h>

  • typedef
    int(*FUNCTION)(int);  

  • int g(int n, FUNCTION f)  
  • {  
  •     int i = 0;  
  •     int ret = 0;  

  •     for(i=1; i<=n; i++)  
  •     {  
  •         ret += i*f(i);  
  •     }  

  •     return ret;  
  • }  

  • int f1(int x)  
  • {  
  •     return x + 1;  
  • }  

  • int f2(int x)  
  • {  
  •     return 2*x - 1;  
  • }  

  • int f3(int x)  
  • {  
  •     return -x;  
  • }  

  • int main()  
  • {  
  •     printf("x * f1(x): %d\n", g(3, f1));  
  •     printf("x * f2(x): %d\n", g(3, &f2));  
  •     printf("x * f3(x): %d\n", g(3, f3));  
  • }  

注意:可以使用函数名f2,函数名取地址&f2都可以,但是不能有括号。
       c.所谓指针函数其实真的没什么好说的,就是一个返回值为指针的函数而已。
15.赋值指针的阅读:
       a.char* (*p[3])(char* d); 这是定义一个函数指针数组,一个数组,数组元素都是指针,这个指针是指向函数的,什么样的函数参数为char*  返回值为char*的函数。
分析过程:char (*p)[3] 这是一个数组指针、char* p[3] 这是一个指针数组  char* 是数组元素类型、char* p(char* d) 这个是一个函数返回值类型是char* 、char (*p)(char* d)这个是一个 函数指针。可见char* (*p[3])(char* d)是一个数组  数组中元素类型是 指向函数的指针,char* (* )(char* d) 这是函数指针类型,char* (* )(char* d) p[3] 函数指针数组 这个不好看 就放里面了。(PS:这个看看就好了~~~当娱乐吧)
      b.函数指针数组的指针:char* (*(*pf)[3])(char* p) //这个就看看吧  我觉得意义也不大 因为这个逻辑要是一直下去 就递归循环了。
分析过程:char* (* )(char *p) 函数指针类型,char* (*)(char *p) (*p)[3]  函数指针 数组指针  也不好看 就放里面了。
返回列表