C - 指针

作者: xx_cc | 来源:发表于2017-04-02 16:12 被阅读1420次

    C语言指针的总结

    1. 变量

    1. 不同类型的变量在内存中占据不同的字节空间。
    2. 内存中存储数据的最小基本单位是字节,每一个字节都有一个内存地址,这个地址是一个十六进制的数。
    3. 声明一个变量,在内存中是从高字节向低字节分配连续的指定字节数的空间。
    4. 任何数据在内存中都是以其二进制的补码形式存储的,低位存储在低字节,高位存储在高字节。
    5. 变量的值:存储在变量中的数据,叫做变量的值。
    6. 变量的地址:一个变量是由一个或者多个字节组成的,组成这个变量的低字节的地址,就是这个变量的地址。
    7. 如何取出变量的地址:使用&(取地址运算符)运算符,&变量名;这个表达式的值就是这个变量的地址。使用%p控制度输出变量的地址。
    8. 什么是指针:变量的地址叫做指针,指针就是地址,地址就是指针。

    下面通过一张图可以更直观的理解内存中的地址


    内存中的地址演示图

    2. 指针

    1. 指针是C语言的灵魂。指针变量占据8个字节。
    2. 变量在内存中的存储。
      变量的值:存储在变量中的数据,叫做变量的值。
      变量的地址:组成这个变量的低字节的地址,就是这个变量的地址。
    3. 取出变量的地址,用&运算符 %p输出变量的地址。
    4. 变量的地址就叫做指针,我们可以使用一个指针变量来存储变量的地址。
    指针变量:
    1. 指针变量就是专门用来存储 地址 的变量,那么我们就说指针变量指向了另外一个变量,存储着另外一个变量的地址。

    2. 指针可以使访问一个变量的方式分为两种。
      a. 直接访问
      b. 可以通过指针变量,找到这个指针指向的变量
      所以通过指针变量可以间接的访问指针变量指向的另外一个变量。

    3. 如何声明一个专门用来存储地址的指针变量
      数据类型 * 指针变量的名称 --- int * p1;
      指针变量的名字叫做p1,这个指针变量的类型是int* 读作int指针。
      *表示这个变量不是一个普通变量,而是一个专门用来存储地址的指针变量,所以有哪些普通的数据类型,就可以有哪些类型的指针。
      声明的时候注意,*的位置 建议 int* p这样提醒我们这是一个int*类型的指针。
      ** 一个指针变量并不是可以存储任意类型的变量的地址,而是有限定的,只能存储和这个指针类型相同的普通变量的地址。** 所以p 指针变量中只能存储int类型变量的地址。

    4. 指针变量的初始化

       int num = 10;
       int *p =  # 建议int* p = #这样写
    

    p指针指向了num变量。因为p指针的值就是num变量的地址,不能直接赋值一个非地址类型的常量数据,也不能直接赋值一个变量给指针。

    1. p指针自己也有地址, 因为指针变量也是一个变量,&p取到指针p的地址。

    2. p操作的是p这个指针变量,可以取p得值,也可以为p赋值

    3. 指针变量的使用
      可以使用指针间接的操作指针指向的变量。
      *p 代表 p 指针指向的变量。
      *p 完全等价于num*p = 100 完全等价于 num = 100
      *p = 100; 表示将100赋值给p指针指向的变量,也就是num变量

    使用指针变量的时候注意点
    int* p1 ,p2, p3 ; 此时p1int *指针,而p2,p3int类型数据 如果希望全部都是指针需要 int *p1, * p2, * p3;

    1. 野指针
      我们声明一个指针变量,如果没有为其初始化,那么这个时候这个指针变量中是有值的,是垃圾值,随机数。因为这个时候,这个指针变量有可能指向了一块随机的空间,这个空间可能无人使用,也可能别的程序在用,也可能系统在用,这个时候,去访问指针指向的的变量的时候,就会报错。BAD_ACCESS坏地址访问错误,像这样的指针我们就叫做野指针。

    2. NULL值 完全等价于0
      为了防止野指针的产生,建议声明一个指针变量后,最好为其初始化,如果没有变量的地址初始化给这个指针变量。那么就初始化一个NULL值。NULL值代表指针变量不指向内存中的任何地址,这样就不会出现野指针,NULL完全等价于0,所以也可以直接赋值给一个指针变量0。
      如果一个指针变量的值是NULL,那么去访问这个指针指向的变量的时候一定会报错。

    3. 多个指针指向同一个变量,修改其中一个所有指针指向的值都会改变。因为多个个指针指向的是同一块地址。即 * 会使指针间接的操作指针指向的变量。

    4. 指针作为函数的参数
      如果函数的参数是一个指针,那么就必须要为这个指针传递一个和指针类型相同的普通变量的地址,这个时候,在函数的内部去访问参数指针的变量的时候,其实访问的就是实参变量

    5. 指针作为函数的参数,可以实现什么效果?
      函数的内部可以修改实参变量的值。那么什么时候使用指针作为参数呢?
      一般函数只能返回一个数据,那么当函数需要返回多个数据的时候就可以使用指针作为参数,让调用者将自己的变量的地址传递给函数内部,函数内部通过指针就可以修改参数,函数无需将数值传回来,就已经修改了参数的值。其实scanf函数传递的就是指针,因此当函数需要多个返回值的时候就可以使用指针作为参数。

    // 从下面代码中就可以看出,我们可以直接在函数中修改两个变量的值。相当于函数有两个返回值。
    void  changeValue (int* p1 ,int* p2){
        *p1 = 100;
        *p2 = 200;
    }
    int main(int argc, const char * argv[]) {
        int num1 = 1;
        int num2 = 2;
        changeValue(&num1, &num2);
        printf("num1 = %d\n",num1);
        printf("num2 = %d",num2);
        return 0;
    }
    
    1. 指针为什么要分类型
      指针变量既然是一个变量就要在内存中占用字节空间
      指针变量在内存中占据多少字节数?
      无论指针是什么类型在内存中都是占据8个字节。
      那为什么指针还要分类呢?
      p指针变量中存储的是num变量的地址,也就是num变量低字节的地址,通过p指针只能找到这个地址的字节,这个时候,通过p指针找到这个字节,操作的时候,操作多少个字节是则是根据指针的类型来决定的。
      所以指针变量的类型决定了通过这个指针找到字节以后,连续操作多少个字节空间。
      int 指针 连续操作4个字节空间
      double 指针 连续操作8个字节空间
      float 指针 连续操作4个字节空间
      char 指针 连续操作1个字节空间
      因此,指针的类型如果不和指向的变量的类型相同的话,那么通过指针就无法正确的操作指向的变量,所以,指针的变量一定要指向一个和自己类型相同的普通变量才可以。

      指针为什么要分类型?
    2. 多级指针
      一个指针变量中存储的是一个一级指针的地址,那么它就是二级指针,一个指针变量中存储的是一个二级指针的地址,那么它就是三级指针。
      二级指针:数据类型 ** 指针名
      二级指针只能存储一级指针变量的地址。
      多级指针在开发中很少用到,遇到多级指针耐心分析一定可以理清其中的关系。

    3. 指针与整数的加减法
      指针可以和整数进行加减运算,指针+1并不是在指针地址的基础之上加一个字节的地址,而是在这个指针地址的基础之上加一个单位变量占用的字节数,例如:如果指针类型是int * 则+1代表加4个字节地址,以此类推。

    4. 指针与数组
      我们可以使用指针来遍历数组,因为数组的本质其实就是指针,当我们创建数组的时候,系统会在内存中由高地址向低地址分配连续的类型所占的空间字节数 * 数组内元素的个数的字节控件。而数组名则代表了数组的低字节地址,也就是数组的地址。
      1). 使用指针遍历数组的第一种方式.
      int arr[7] = {10,20,30,40,50,60,70}; //在内存中高地址向低地址分配连续的 类型所占的空间字节数 * 数组内元素的个数 7 * 4 = 28 个字节空间
      int* p1 = arr; //p1指针指向了数组的第0个元素.
      for(int i = 0; i < 7; i++)
      {
      printf("%d\n",*(p1+i));
      }

      2). 使用指针遍历数组的第二种方式.

       int arr[7] = {10,20,30,40,50,60,70};
       for(int i = 0; i < 7; i++)
       {
          printf("%d\n",*(arr+i));
       }
      

      3). 使用指针遍历数组的第三种方式.

       int arr[7] = {10,20,30,40,50,60,70};
       int* p1 = arr;
       for(int i = 0; i < 7; i++)
       {
          printf("%d\n",*(p1++));
       }
       注意的地方,每次循环,p1的值都会变化。
       最后1次执行完毕之后,p1指针指向数组外面去了,p1就不再执行数组中的任何元素了。
      

      注意: 数组名代表数组的地址,而数组一旦创建,数组的地址就确定了,不能改变。
      所以,我们不能为数组名赋值也不能修改数组名的值,但是可以使用数组名的值。
      arr是数组的地址,也是数组中第0个元素的地址,arr+1就是数组中第一个元素的地址,数据名就是一个地址常量,无法改变。

    5. 数组作为函数的参数的本质
      当数组作为函数的参数的时候,在声明这个参数数组的时候,并不是去创建一个数组,而是去创建一个用来存储地址的指针变量,如果我们为函数写了一个数组作为参数,其实编译器在编译的时候,已经把这个数组变成了指针,这也就是为什么我们通过sizeof计算参数数组得到的永远都是8,所以以后我们的函数如果带了一个数组参数,建议直接写一个指向数组的第0个元素的指针,在传入数组的长度

    6. 索引的本质
      指针变量后面可以使用中括号,在中括弧中写上下标来访问数据。
      p[n];前提p是一个指针变量,完全等价于*(p + n);
      所以arr[0] 就等价于 * [arr + 0]
      操作数组我们虽然使用中括弧下标来操作,实际上内部本质仍然是使用的指针来操作。

    7. 存储指针的数组
      如果一个数组是用来存储指针类型的数据的话,那么这个数组就叫做存储指针的数组
      格式 :元素类型 数组名[数组长度]; int * arr[3];
      arr数组里面存储int指针数据,最多存储3个。

    8. 指针与指针之间的减法运算
      指针与指针之间可以做减法运算,结果是一个long类型的数据,
      结果的意义代表两个指针指向的变量之间相差多少个单位变量,绝大多数情况下,我们用在判断数组的两个元素之间相差多少个元素
      如果参与减法运算的两个指针不指向同一个数组,结果就会出现问题
      结果 = 两个指针的差 / 每一个指针变量对应的普通变量占用的字节数。
      并且只能做减法运算,用在数组当中判断两个元素之间相差多少个元素。

    9. 指针与指针在之间的比较运算 <, <=, >, >=, ==, !=都可以使用
      可以用来判断两个指针指向的变量的字节,谁在高字节,谁在低字节。或者两个指针的地址是不是同一个地址。

    10. 指针和字符变量
      char *name = "jack";表示直接将一个字符串数据初始化给一个字符指针。

      字符指针存储和字符数组存储的区别

    // 字符数组存储:将字符串数据的每一个字符存储到字符数组的元素中,并追加一个 \n 表示结束
    char name[5] = "jack";
    // 直接为字符指针初始化一个字符串数据
    char *name = "jack";
    
    1.) 当他们都是局部变量的时候
    

    字符数组是申请在栈区的,字符串的每一个字符存储在字符数组的每一个元素中。
    指针变量是声明在栈区的。但是此时字符串数据是以字符数组的形式存储在常量区的。此时指针变量中存储的是字符串在常量区的地址

    2.) 当他们作为全局变量的时候
    

    字符数组是存储在常量区的,字符串的每一个字符存储在这个数组中的每一个元素中。
    字符指针也是存储在常量区,字符串也是以字符数组的形式存储在常量区,指针中存储的是字符串在常量区的地址。

    **以字符数组存储的字符串数据,可以修改字符数组的元素。可变
    以字符指针的形式存储字符串数据,这个时候字符指针指向的字符串数据是无法修改的,不可变**
    
    1. 字符串的恒定型
      前提:以字符指针形式存储的字符串
      1.) 当我们以字符指针的形式存储字符串的时候,无论如何,字符串数据是存储在常量区的,并且,一旦存储到常量去中去,这个字符串数据就无法更改。

      2.) 当我们以字符指针的形式要将字符串数据存储到常量区的时候,并不是直接将字符串存储到常量区,而是先检查常量区中是否有相同内容的字符串,如果有,直接将这个字符串的地址拿过来返回,如果没有,才会将这个字符串数据存储到常量区中。

      3.) 当我们重新为字符指针初始化一个字符串的时候,并不是修改原来的字符串,因为原来的字符串数据是不可更改的,而是重新的创建了一个字符串,把这个新的字符串的地址赋给他。建议使用字符指针来存储字符串数据。优势:长度任意。

    char *name = "jack";
    nsme = "rose";
    // 这样可以 但是并不是把jack改成了rose,而是重新创建了一个"rose",把rose地址赋值给name
    
    1. 字符串数组
      char *names[4] = {"aa","bb","cc","dd"};
      names数组的元素的类型是char指针,初始化给元素的字符串数据是存储在常量区的。元素中存储的是字符串在常量区的地址
      因此这是一个存储指针的数组,每一个元素的类型是一个指针,占用得内存为8个字节。

    2. 指向函数的指针。
      程序在运行的时候,会将程序加载到内存,内存的代码段中主要存储的就是程序的代码,而程序的代码就包括函数。既然函数要存储在内存中,那么肯定要用1块空间来存储,那么这个块空间一定有1个地址。
      因此我们就可以声明1个指针用来存储这个函数的地址,也就是说让这个指针指向这个函数。这样我们就可以使用指针来间接的调用函数
      优势: 调用函数有了两种方式。
      1.) 直接使用函数名调用
      2.) 使用指向函数的指针间接调用.

    3. 指向函数的指针的声明
      一个指向函数的指针,并不是任意的函数都可以指向,而是有限定的,要求指向的函数的返回值类型和参数描述必须要与指针的描述一样
      声明语法

    返回值类型 (*指针名)([参数列表]);
    void (*pFunction)();
    // 表示声明了1个指向函数的指针,名字叫做pFunction。
    // 这个指针只能指向没有返回值,并且没有参数的函数。
    
    int (*pFun)(int num1,int num2);
    // 表示声明了1个指向函数的指针,名字叫做pFun.
    // 这个指针只能指向返回值为int类型 并且有两个整型的参数的函数.
    
    1. 指向函数的指针的初始化
      函数的名称就代表函数的地址,因此我们直接将符合条件的函数的名称赋值给这个指针。
      并且我们有两种方法可以通过指针来调用这个函数。
    int MaxValue (int a, int b){
           return a > b ? a : b;
    }
    int main(int argc, const char * argv[]) {    
           int(*pMaxValue)(int a, int b) = MaxValue; // 创建一个返回int 并且有两个int型参数的函数指针,并赋值。
           printf("%d\n", pMaxValue(5,10)); // 通过指针调用函数方法1
           printf("%d\n",(*pMaxValue)(6,9)); // 通过指针调用函数方法2
           printf("%d\n",MaxValue(9, 13)); // 调用函数
           printf("%p\n",MaxValue); // 输出函数的地址
        return 0;
    }
    

    文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

    相关文章

      网友评论

      • 不安的白衬衣:指针就是地址,地址就是指针???指针是一个变量,地址是一个常量。并且指针只是保存地址的一个变量而已,指针本身也是有地址的。可能这样说有一些挑字眼,但地址也可以形象的称为“指针”,通过它能找到以它为地址的内存单元。能够讨论一下,我也有一些不太明白的地方
        不安的白衬衣:@xx_cc 好吧!两个月前京东的一个面试我说block捕获局部变量copy是把局部变量的地址拷贝到的堆中的,结果我就gg了
        xx_cc:@不安的白衬衣 你说的不错啊,看来你已经理解的很透彻了。这么说是为了便于理解,简化了中间的指向部分。
      • 轻叶:没太看懂16.1和16.3中p1加法的操作是怎么实现的,可以仔细讲讲吗😂
        xx_cc:@轻叶 这倒是,但是这不是这里的重点呀,这里在循环中 *(p1+i) 完全等价 *(p1++)的
        轻叶:@xx_cc 是不是p1++和p1+1有所不同,p1++会直接将加过之后的值覆盖掉p1的值😵
        xx_cc:又重新看了一下,三种方法其实没有什么区别,核心在于 “指针+1并不是在指针地址的基础之上加一个字节的地址,而是在这个指针地址的基础之上加一个单位变量占用的字节数” 所以指针加1 就等于 arr[i + 1]
      • Eugene_iOS:很棒!:+1:

      本文标题:C - 指针

      本文链接:https://www.haomeiwen.com/subject/hhxgottx.html