对C指针的理解

Posted 一日思考

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对C指针的理解相关的知识,希望对你有一定的参考价值。

阅读到《深入理解计算机系统》,对C指针的理解这一部分内容感觉写的特别好,摘录出来加深理解并作为分享。

指针是C语言的一个核心特色,以一种统一的方式,对不同数据结构中的元素产生引用。下面重点介绍一些指针和它们映射到机器代码的关键原则。

  • 每个指针都对应一个类型。这个类型表明该指针指向的是哪一类对象。以下面的指针声明为例:int *ip;,变量ip是一个指向int类型对象的指针,通常如果对象类型为T,那么指针的类型为T*。特殊的void *类型代表通用指针。比如说,void *malloc(size_t size)函数返回一个通用指针,然后通过强制类型转换或者赋值操作那样的隐私强制类型转换,将它转换成一个有类型的指针。指针类型不是机器代码中的一部分,是C语言提供的一种抽象,帮助程序员避免寻址错误。
  • 可以这么理解,指针的具体地址值相当于是一个寻址开始的地址,什么时候结束是由指针类型的size大小决定何时结束,比如char*,从起始地址读一个字节就结束,int*,从起始地址读4个字节就结束。下面我们写一段C代码,验证我们对指针的理解。

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

    typedef enum NodeType 
        INT,
        CHAR
     NodeType;

    typedef struct Node 
        NodeType type;
     Node;

    typedef struct IntNode 
        NodeType type;
        int value;
     IntNode;

    typedef struct CharNode 
        NodeType type;
        char value;
     CharNode;

    charprintNodeType(NodeType type) 

        switch (type)
        
        case INT:
            return "INT";    
        case CHAR:
            return "CHAR";
        default:
            return "UNKNOW";
        

        return NULL;


    // 重点理解这个函数
    void printNode(Node* n) 
        assert(n != NULL);

        switch (n->type)
        
        case INT:
            
                IntNode* in = (IntNode*)n;
                printf("type:%s value:%d \\n", printNodeType(in->type), in->value);
            
            break;
        case CHAR:
            
                CharNode* cn = (CharNode*)n;
                printf("type:%s value:%d \\n", printNodeType(cn->type), cn->value);
            
            break;
        default:
            printf("unknow node \\n");
            break;
        


    int main() 
        IntNode *a = (IntNode*)malloc(sizeof(IntNode));
        a->type = INT;
        a->value = 1024;
        printNode(a);

        CharNode *b = (CharNode*)malloc(sizeof(CharNode));
        b->type = CHAR;
        b->value = \'a\';
        printNode(b);

        return 0;

    运行结果:

    type:INT value:1024 
    type:CHAR value:97 

    这个代码可以验证上面的理解。

  • 每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的NULL(0)值表示该指针没有指向任何地方。
  • 将指针从一种类型强制转换成另一种类型,只改变了它的类型,并不改变它的值。强制类型转换的一个效果是改变指针运算的伸缩。
  • 从上面的代码示例可以验证上面这一点。

  • 指针也可以指向函数。函数指针的值是该函数机器代码表示中第一条指令的地址。

  • 参考书目:《深入理解计算机系统》

    C语言中的定位,谈谈对指针的基本理解

    在C语言中和地址相遇-指针

    1、指针是什么?

    在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(pointsto)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

    • 谈谈我们对内存的了解
      地址指向的一块空间,其空间能够存放数据,下图这种形象了解
    • 指针就是指向内存编码的,指向了一个确定的内存空间,所以地址形象的被称为指针。
      指针是个变量,存放内存单元的地址(编号)。
    • 一个小的内存单元为一个字节
    • 内存的地址是如何编址的
      经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
      对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)那么32根地址线产生的地址就会是:
    00000000 00000000 00000000 00000000
    00000000 00000000 00000000 00000001
    ...
    11111111 11111111 11111111 11111111
    
    这里就有232次方个地址。可以管理232次方个内存单元
    
    每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
    2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。
    

    这里我们就明白:

    1、在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
    2、那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

    • 总结:

    • 指针是用来存放地址的,地址是唯一标示一块地址空间的。

    • 指针的大小在32位平台是4个字节,在64位平台是8个字节。

    3、指针和指针类型

    在C语言中,我们了解到不同的数据有不同的类型,那么我们的指针有没有它自己的类型?
    准确的说:有的。下面让我们来了解一下。对于数据类型有他特有的意义,下面让我们来解析一下。

    • 指针类型的意义1:指针的解引用
      指针类型决定了指针解引用操作的时候,一次访问几个字节(访问内存的大小)
      1、char* 指针解引用访问一个字节
      2、int * 指针解引用访问四个字节
      3、short*指针解引用访问俩个字节
    int main()
    
        int a=0x11223344;
        int *p1=&a;
        *p1=0;
    //解引用访问四个字节,四个字节都改为0,a=0;
        char *p2=&a;
        *p2=0;
    //解引用访问一个字节,四个字节只有一个字节改为0,a!=0.
    
    

    图片解析:


    总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)

    • 指针类型的意义2:指针±整数
      指针类型决定了指针加减整数时候的步长(指针±整数的时候跳过了多少字节)

      int 指针 +1 跳过四个字节
      char指针 +1 跳过一个字节等等

      图片解析:

    3、野指针

    概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

    • 野指针的成因:
      1、指针未初始化
    #include <stdio.h>
    int main()
     
      int *p;//局部变量指针未初始化,默认为随机值
        *p = 20;
    //通过p中存的随机值作为地址,找到一个空间,这个空间不属于我们当前的程序
    //就造成了非法访问,p就是野指针
      return 0;
    
    

    2、越界访问

    #include <stdio.h>
    int main()
    
        int arr[10] = 0;
        int *p = arr;
        int i = 0;
        for(i=0; i<=11; i++)
       
            //当指针指向的范围超出数组arr的范围时,p就是野指针
            *(p++) = i;
       
        return 0;
    
    

    3、指针指向的空间释放

    int *test()
    
        int a=10;
        return &a;
    
    //局部变量的生命周期在出函数时也随之销毁
    //在返回地址时,a的空间已被还给了操作系统
    int main()
    
        int *p=test();
    //p虽然接收了a的地址,但是a的空间已经还给操作系统,没有对a的使用权。
        return 0;
    
    

    对test函数的解析:

    main函数先在栈区开辟空间,在mian函数所开辟的空间为指针变量p分配了空间,接着调用了test函数,随之在栈区为test函数开辟空间,在test函数中为整型变量a开辟空间,当test函数结束时,为test函数开辟的空间都还给操作系统,但数据仍然保留在原先的栈区,走到第一个printf函数时,printf函数的栈区从test函数的初始位置进行分配,此时原先没有删除的数据被printf函数的内容覆盖了。

    如果没有第一个printf函数,只第二个printf函数为什么没有覆盖?

    如果没有第一个函数,在调用第二个printf函数的时候,在函数调用时,传参是先行的,*p以printf函数的参数传给printf,此时已经找到了了p所指的空间,发生了释放空间的访问

    4、如何规避野指针

    1. 指针初始化
    //明确指针的初始化,确定执行
    int main()
    
        int a=0;
        int *p=&a;//明确p所指的内容
        return 0;
    
    //不知道一个指针当前指向哪里时,初始化为空
    int main()
    
        int *p=NULL;
        //指向空的指针不能解引用,会出现语法错误
        //在使用指针前可以先进行判断是否为空
        if(*p2!=NULL)
    
    
    1. 小心指针越界
    2. 指针指向空间释放即赋值NULL
    3. 避免返回局部变量的地址
    4. 指针使用之前检查有效性
    //指针在使用前一定要判断
    int main()
    
        int a=0;
        int *p=&a;
        if(*p!=NULL)
        
            *p=200;
        
    
    

    5、指针运算

    • 指针±整数
    //指针+整数;指针的关系运算
    define N_VALUES 5
    float values[N_VALUES];
    float *vp;
    for (vp = &values[0]; vp < &values[N_VALUES];)
    
         *vp++ = 0;
    
    //指针-整数
    int main()
    
        int arr[10]=1,2,3,4,5,6,7,8,9,0;
        int *=&arr[9];
        printf("%p\\n",p);
        printf("%p\\n"p-1);
        //地址差四个字节
    
    
    
    • 指针-指针

    指针-指针 得到的数字的绝对值是指针和指针之间元素的个数.前提是两个指针指向同一块区域。

    int main()
    
        int arr[10]=1,2,3,4,5,6,7,8,9,0;
        printf("%d\\n",&arr[9]-&arr[0]);//9 
        char ch[5]=0;
        printf("%d\\n",&arr[9]-&ch[0]);//这种行为本身就是错误的语法
        return 0;
    
    

    指针-指针的应用:求字符串长度:

    int my_strlen(char *p)
    
       int count=0;
       char *start=p;
       while(*p!='\\0')
       
          p++;
       
       return p-start;
    
    int main()
    
     char arr[]="abcdef";
     int len=my_strlen(arr);
    
    
    • 指针的关系运算
    //1
    for(vp = &values[N_VALUES]; vp > &values[0];)
    
        *--vp = 0;
    
    //2
    for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
    
        *vp = 0;
    
    

    标准规定:

    允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

    6、指针和数组

    数组:是一块连续的空间,放的是相同类型的元素,数组大小和元素类型,元素个数有关系

    指针:是一个变量,放地址;指针变量的大小是4(32bit)/8(64bit)个字节

    前面说过数组名是首元素的地址(两种情况除外:1、sizeof(数组名)这里的数组名不是首元素的地址,是表示整个数组的,这里计算的是整个数组的大小,单位是字节。2、&数组名,这里的数组名不是首元素的地址,是表示整个数组的,拿到的是这个数组的地址。),所以数组和指针可以连接起来。

    7、二级指针

    指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针 。

    对于二级指针的运算有:
    *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .

    int b = 20;
    *ppa = &b;//等价于 pa = &b;
    

    **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .

    **ppa = 30;
    //等价于*pa = 30;
    //等价于a = 30;
    
    

    指针的实例:

    int mian()
    
        int a=10;//4byte
        int *p=&a;
        int* *pp=&p;//pp是二级指针
        //*pp说明pp是指针,int*说明pp指向的对象是int*类型。
        int* **ppp=&pp;//ppp三级指针
        **p=20;//将a改为20
    
    

    内存结构图:

    8、指针数组

    指针数组是数组,存放指针的数组

    int main()
    
        int *arr3[5];
    
    

    以上是关于对C指针的理解的主要内容,如果未能解决你的问题,请参考以下文章

    指针学习笔记

    指针学习笔记

    什么导致 SIGSEGV

    考试C语言,指针不会?看这一篇博客就够了

    在我的脚本中理解指向哈希的 Perl 指针

    对C指针的理解