当前位置 博文首页 > HyDraZya的博客:【C语言进阶】C指针详解

    HyDraZya的博客:【C语言进阶】C指针详解

    作者:[db:作者] 时间:2021-09-05 09:43

    目录

    1. 字符指针

    2. 指针数组

    3. 数组指针

    数组指针的定义:

    &数组名VS数组名?

    数组指针的使用

    4. 数组传参和指针传参

    一维数组传参

    二维数组传参

    一级指针传参

    二级指针传参

    5. 函数指针

    6. 函数指针数组

    7. 指向函数指针数组的指针

    8. 回调函数


    首先我们先来说下指针的概念:

    1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
    2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
    3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
    4. 指针的运算。

    1. 字符指针

    在指针的类型中我们知道有一种指针类型为字符指针 char*

    一般使用

    #include <stdio.h>
    int main()
    {
        char arr[] = "abcedf";
        //arr数组中存储了字符串"abcedf"
        char *pc = arr;
        //解引用pc指向了arr数组所占用的地址/开辟的空间
        printf("%s\n", arr); //abcdef
        printf("%s\n", pc);  //abcdef
    
        //打印结果均为abcdef
        return 0;
    }

    ?还有一种:

    int main()
    {
        char *p = "abcdef";//abcdef是一个常量字符串
        printf("%c\n", *p);//%c输出的是第一个字符的地址,可以看出p中存的是a的地址
        printf("%s\n", p);//%s输出的是一整个字符串,abcdef
        return 0;
    }

    此处代码的本质其实是字符串 abcdef?的首字符地址存到了 p中

    也就是说把一个常量字符串的首字符 a 的地址存放到指针变量 p?中

    一道面试题:

    #include <stdio.h>
    int main()
    {
        char arr1[] = "abcdef";
        char arr2[] = "abcdef";
        const char *p1 = "abcdef";
        const char *p2 = "abcdef";
        if(arr1 == arr2)
            printf("arr1 and arr2 are same\n");
        else
            printf("arr1 and arr2 are not same\n");
        if(p1 == p2)
            printf("p1 and p2 are same\n");
        else
            printf("p1 and p2 are not same\n");
        return 0;
    }

    这里最终输出的是:

    这里arr1和arr2他们用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存空间,那么两个数组的数组名,肯定是不同的首元素地址,这两个地址在不同的空间时自然是不相等的。

    p1和p2指向的是一个同一个常量字符串,而指向同一个字符串的时候,由于字符串不能修改,他们实际会指向同一块内存。

    2. 指针数组

    指针数组是一个存放指针的数组

    那么指针数组的含义是什么呢??

    int* arr1[10];? ? //整形指针的数组?

    char *arr2[4];? //一级字符指针的数组

    char **arr3[5]; //二级字符指针的数组

    int arr[10] = {0}; //整型指针
    char ch[5] = {0};  //字符指针
    int *parr[4]; //存放整型指针的数组-指针数组
    char *pch[5]; //存放字符指针的数组-指针数组

    指针数组的打印:?

    ?eg.

    int main()
    {
        int a = 10;
        int b = 20;
        int c = 30;
        int d = 40;
        int *arr[4] = {&a, &b, &c, &d};
        int i = 0;
        for(i = 0; i < 4; i++)
        {
            printf("%d ", *(arr[i]));
        }
        return 0;
    }

    ?进阶(多个数组):

    int main()
    {
        int arr1[] = {1,2,3,4,5};
        int arr2[] = {2,3,4,5,6};
        int arr3[] = {3,4,5,6,7};
        int *parr[] = {arr1, arr2, arr3};
        int i = 0;
        for(i = 0; i < 3; i++)
        {
            int j = 0;
            for(j = 0; j < 5; j++)
            {
                printf("%d ",*(parr[i] + j));
            }
            printf("\n");
        }
        return 0;
    }

    3. 数组指针

    数组指针的定义:

    数组指针是指针?还是数组?

    答案:指针

    那数组指针应该是:能够指向数组的指针。

    int main()
    {
        int arr1[10] = {1,2,3,4,5,6,7,8,9,10};
        int (*p)[10] = &arr1;//存储数组
        //这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
        //上面的p就是数组指针
    
        char *arr2[5];
        char *(*pa)[5] = &arr2;
        //pa是指针变量的名字
        //*说明pa是指针
        //[5]表示pa指向的数组是5个元素的
        //char*说明pa指向的数组元素类型是char*
        return 0;
    }

    &数组名VS数组名?

    我们首先来看一段代码:

    #include <stdio.h>
    int main()
    {
        int arr[10] = {0};
        printf("%p\n", arr);
        printf("%p\n", &arr);
        return 0;
    }

    arr 和 &arr 分别是什么呢?

    我们知道arr是数组名,数组名表示数组首元素的地址。

    那&arr数组名到底是啥?让我们来看下运行结果

    运行结果:

    由此可见数组名和&数组名打印的地址是一样的。

    让我们再看一段代码:

    #include <stdio.h>
    int main()
    {
        int arr[10] = { 0 };
        printf("arr = %p\n", arr);
        printf("&arr= %p\n", &arr);
    
        printf("arr+1 = %p\n", arr+1);
        printf("&arr+1= %p\n", &arr+1);
        return 0;
    }

    结果如下:

    根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

    实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。

    数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。

    取地址名+1,跳过1个数组? ? ? ? ? ? ? ? 地址名+1,跳过一个元素

    数组指针的使用

    数组指针是怎么使用的呢?

    既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

    #include <stdio.h>
    int main()
    {
        int arr[10] = {1,2,3,4,5,6,7,8,9,0};
        int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
        return 0;
    }

    ?一个数组指针的使用:

    #include <stdio.h>
    void print1(int arr[3][5], int x, int y)
    {
         int i = 0;
         int j = 0;
         for(i = 0; i < x; i++)
         {
              for(j = 0; j < y; j++)
              {
                   printf("%d ", arr[i][j]);
              }
         printf("\n");     
         } 
    }
    
    void print2(int (*p)[5] , int x, int y)
    {
         int i = 0;
         for(i = 0; i < x; i++)
         {
              int j = 0;
              for(j = 0; j < y; j++)
              {
                   printf("%d ",p[i][j]);
                   //printf("%d ",*(*(p+i)) + j);
                   //printf("%d ",(*(p+i))[j]);
                   //printf("%d ",*(p[i] + j);
              }
         printf("\n");
         }
    }
    
    int main()
    {
         int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
         //首先把arr想象为一维数组
         print1(arr,3,5);//arr:数组名,即首元素地址
         print2(arr,3,5);//arr 
         return 0;
    }

    ?我们来一段代码方便于我们理解:

    int main()
    {
        int arr[10] = {1,2,3,4,5,6,7,8,9,10};
        int i = 0;
        int *p = arr;
        for(i = 0; i < 10; i++)
        {
            printf("%d ", p[i]);       
            printf("%d ", arr[i]);  
            printf("%d ", *(p+i));   
            printf("%d ", *(arr+i));
            //以上四种打印方式输出的结果均相同
        }
        return 0;
    }

    ?看完了指针数组和数组指针我们来一起回顾并看看下面代码的意思

    int arr[5];
    //arr是一个内含5个元素的整型数组(每个元素均为int整型)
    int *parr1[10];
    //parr1是一个内含10个元素的数组,每个元素的类型为int*,所以parr1是一个指针数组
    int (*parr2)[10];
    //parr2是一个指针,指针指向了内含10个元素的数组,每个元素的类型为int整型,所以parr2是一个数组指针
    int (*parr3[10])[5];
    //parr3是一个内含10个元素的数组,每个元素都是一个数组指针,该数组指针指向一个有5个元素的数组,每个元素的类型都是int整型

    4. 数组传参和指针传参

    在写代码的时候难免要把【数组】或者【指针】传给函数

    一维数组传参:

    #include <stdio.h>
    void test(int arr[])
    {}
    void test(int arr[10])
    {}
    void test(int *arr)
    {}
    
    void test2(int *arr[20])
    {}
    void test2(int **arr)
    {}
    //以上这5种传参都是正确的
    int main()
    {
     int arr[10] = {0};
     int *arr2[20] = {0};
     test(arr);
     test2(arr2);
    }

    二维数组传参:

    #include <stdio.h>
    void test(int arr[3][5])
    {}//ok
    void test1(int arr[][5])
    {}//ok
    void test2(int arr[3][])
    {}//no
    
    int main()
    {
        int arr[3][5] = {0};
        test(arr);//二维数组传参
        test1(arr);
        test2(arr);
        return 0;
    }

    总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。

    因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

    void test3(int *arr) //no
    {}
    //arr此时是二维数组,数组名是首元素地址,也就是第一行的代码,
    //它是一个一维数组的地址,无法传到一个整型指针里去(整型指针是用来存放int整型的)
    void test4(int **arr) //no
    {}//二级指针是用来存放一级指针的地址,所以肯定是错的
    void test5(int(*arr)[5]) //ok
    {}//arr此时是一个指针,指向有5个元素的数组,每个元素的类型为int,
    //数组名传过去后也就是第一行的地址,第一行正好是5个元素,所以正确
    int main()
    {
        int arr[3][5] = {0};
        test3(arr);
        test4(arr);
        test5(arr);
        return 0;
    }

    一级指针传参:

    #include <stdio.h>
    void print(int *p, int sz)
    {
        int i = 0;
        for(i=0; i<sz; i++)
        {
            printf("%d\n", *(p+i));
        }
    }
    int main()
    {
        int arr[10] = {1,2,3,4,5,6,7,8,9};
        int *p = arr;
        int sz = sizeof(arr)/sizeof(arr[0]);
        //一级指针p,传给函数
        print(p, sz);
        return 0;
    }

    当一个函数的参数部分为一级指针的时候,函数能接收的参数:

    void test1(int *p)
    {}//整形指针
    
    void test2(char *p)
    {}//字符指针
    
    int main()
    {
        int a = 10;
        int *p1 = &a;
        test1(&a);
        test1(p1);
    
        char ch = 'w';
        char *p2 = &ch;
        test2(&ch);
        test2(p2);
        return 0;
    }

    二级指针传参:?

    void test(int **ptr)
    {
        printf("num = %d\n", **ptr);
    }
    
    int main()
    {
        int n = 10;
        int *p = &n;
        test(&p);
        int **pp = &p;
        test(pp);
        system("pause");
        return 0;
    }

    当一个函数的参数部分为二级指针的时候,函数能接收的参数:

    #include <stdio.h>
    void test(char **p)
    {}
    
    int main()
    {
        char c = 'b';
        char*pc = &c;
        char**ppc = &pc;
        char* arr[10];
        test(&pc);
        test(ppc);
        test(arr);//指针数组也可以
        return 0;
    }

    5. 函数指针

    根据上方的学习中,我们知道了数组指针指向数组的指针

    那么函数指针,顾名思义就是指向函数的指针

    首先我们看段代码:

    #include <stdio.h>
    int Add(int x, int y)
    {
        int z = 0;
        z = x + y;
        return z;
    }
    int main()
    {
        int a = 10;
        int b = 20;
        int arr[10] = {0};
        int (*p)[10] = &arr;
        printf("%p\n", Add);
        printf("%p\n", &Add); 
        return 0;
    }

    输出结果:

    由此我们可以得出&函数名 和 函数名 都是函数的地址

    那我们的函数的地址该如何保存起来呢?

    再来看段代码:

    #include <stdio.h>
    int Add(int x, int y)
    {
        int z = 0;
        z = x + y;
        return z;
    }
    int main()
    {
        int (*pa)(int, int) = Add;
        printf("%d\n", (*pa)(2,3)); //5
        return 0;
    }

    此时我们就可以总结出:函数指针是一个指向函数的指针,且可以存放函数的地址

    补充:

    int Add(int x, int y)
    {
        int z = 0;
        z = x + y;
        return z;
    }
    int main()
    {
        int a = 10;
        int b = 20;
        int (*pa)(int, int) = Add;
        printf("%d\n", pa(2,3));//5
        printf("%d\n", Add(2,3));//5
        printf("%d\n", (*pa)(2,3));//5
        //此时函数指针前面的*虽然没有什么效果,但是加上可以解引用找到这个函数,不过记得一定要加上()
        system("pause");
        return 0;
    }

    6. 函数指针数组

    数组是一个存放相同类型数据的存储空间,那要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组,那函数指针的数组如何定义呢?

    举个例子:

    int Add(int x, int y) //加法
    {
        return x + y;
    }
    int Sub(int x, int y) //减法
    {
        return x - y;
    }
    int Mul(int x, int y) //乘法
    {
        return x * y;
    }
    int Div(int x, int y) //除法
    {
        return x / y;
    }
    int main()
    {
        //指针数组
        int *arr[5];
        //需要一个函数指针的数组,可以存放四个函数的地址 Add/Sub/Mul/Div 
        int (*pa)(int, int) = Add;
        int (*parr[4])(int, int) = {Add, Sub, Mul, Div};//函数指针的数组
        int i = 0;
        for(i = 0; i < 4; i++)
        {
            printf("%d\n", parr[i](2, 3));// 5 -1 6 0
        }
        system("pause");
        return 0;
    }

    ?给定一个函数类型,写出它的函数指针和函数指针数组

    char * my_strcpy(char *dest, const char* src);
    //1.写一个函数指针pf,能够指向my_strcpy
    char * (*pf)(char*, const char*);
    //2.写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址 
    char * (*pfArr[4](char*, const char*));

    函数指针数组的用途:转移表?

    例子:(计算器)

    void menu()
    {
        printf("**********************\n");
        printf("***  1.add  2.sub  ***\n");
        printf("***  3.mul  4.div  ***\n");
        printf("***     0. exit    ***\n");
        printf("**********************\n");
    }
    
    int Add(int x, int y)
    {
        return x + y;
    }
    int Sub(int x, int y)
    {
        return x - y;
    }
    int Mul(int x, int y)
    {
        return x * y;
    }
    int Div(int x, int y)
    {
        return x / y;
    }
    int main()
    {
        int input = 0;
        int x = 0;
        int y = 0;
        do
        {
            menu();
            printf("请选择:>");
            scanf("%d", &input);
            switch(input)
            {
            case 1:
                printf("请输入两个操作数");
                scanf("%d%d", &x, &y);
                printf("%d\n",Add(x,y));
                break;
            case 2:
                printf("请输入两个操作数");
                scanf("%d%d", &x, &y);
                printf("%d\n",Sub(x,y));
                break;
            case 3: 
                printf("请输入两个操作数");
                scanf("%d%d", &x, &y);
                printf("%d\n",Mul(x,y));
                break;
            case 4:
                printf("请输入两个操作数");
                scanf("%d%d", &x, &y);
                printf("%d\n",Div(x,y));
                break;
            case 0:
            printf("退出\n");
                break;
            default:
                printf("选错误\n'");
                break;
            }
        }while(input);
        return 0;
    }

    使用函数指针数组去实现:

    #include <stdio.h>
    void menu()
    {
        printf("**********************\n");
        printf("***  1.add  2.sub  ***\n");
        printf("***  3.mul  4.div  ***\n");
        printf("*** 5.Xor  0. exit ***\n");
        printf("**********************\n");
    }
    
    int Add(int x, int y)
    {
        return x + y;
    }
    int Sub(int x, int y)
    {
        return x - y;
    }
    int Mul(int x, int y)
    {
        return x * y;
    }
    int Div(int x, int y)
    {
        return x / y;
    }
    int Xor(int x, int y)
    {
        return x ^ y;
    }
    int main()
    {
        int input = 0;
        int x = 0;
        int y = 0;
        //pfArr是一个函数指针数组 - 转移表
        int (*pfArr[])(int, int) = {0, Add, Sub, Mul, Div, Xor};
        do
        {
            menu();
            printf("请选择:>");
            scanf("%d", &input);
            if(input >= 1 && input <= 5)
            {
                printf("请输入两个操作数:>");
                scanf("%d%d", &x, &y);
                int ret = pfArr[input](x, y);
                printf("%d\n", ret);
            }
            else if(input == 0)
            {
                printf("退出\n");
            }
            else
            {
                printf("选择错误\n");
            }
        }while(input);
        return 0;
    }

    7. 指向函数指针数组的指针

    指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针 ;

    如何定义?

    #include <stdio.h>
    int main()
    {
        int arr[10] = {0};
        int (*p)[10] = &arr; //取出数组的地址
        int (*pf)(int, int);//函数指针
        int (*pfArr[4])(int, int);//pfArr是一个数组,函数指针数组
        //ppfArr是一个指向[函数指针数组]的指针
        int (*(*ppfArr)[4])(int, int);
        //ppfArr 是一个数组指针,指针指向的数组有4个元素
        //指向的数组的每个元素的类型是一个函数指针 int(*)(int, int)
        return 0;
    }

    8. 回调函数

    回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一 个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或 条件进行响应。?

    我们采用之前的计算器来:

    #include <stdio.h>
    void menu()
    {
        printf("**********************\n");
        printf("***  1.add  2.sub  ***\n");
        printf("***  3.mul  4.div  ***\n");
        printf("***  5.Xor  0.exit ***\n");
        printf("**********************\n");
    }
    int Add(int x, int y)
    {
        return x + y;
    }
    int Sub(int x, int y)
    {
        return x - y;
    }
    int Mul(int x, int y)
    {
        return x * y;
    }
    int Div(int x, int y)
    {
        return x / y;
    }
    int Xor(int x, int y)
    {
        return x ^ y;
    }
    void Calc(int (*pf)(int, int))
    {
        int x = 0;
        int y = 0;
        printf("请输入两个操作数:>");
        scanf("%d%d", &x, &y);
        printf("%d\n", pf(x, y));
    }
    
    int main()
    {
        int input = 0;
        do
        {
            menu();
            printf("请选择:>");
            scanf("%d", &input);
            switch(input)
            {
            case 1:
                Calc(Add);
                break;
            case 2:
                Calc(Sub);
                break;
            case 3: 
                Calc(Mul);
                break;
            case 4:
                Calc(Div);
                break;
            case 5:
                Calc(Xor);
                break;
            case 0:
            printf("退出\n");
                break;
            default:
                printf("选错误\n'");
                break;
            }
        }while(input);
        return 0;
    }

    这里的Calc函数就回调了前面的加减乘除等等函数

    我们再来认识一个函数qsort

    void qsort(void *base,   //目标数组的起始位置,
               size_t num,   //数组的大小,单位是元素
               size_t width, //元素大小,即字节大小
               int(*cmp)(const void *e1, const void *e2) //比较函数
               );
    //void *类型的指针 接收任意类型的地址
    //void *类型的指针 不能进行解引用操作
    //void *类型的指针 不能进行+-整数的操作

    使用回调函数,模拟实现qsort(采用冒泡的方式)。

    注意:这里第一次使用 void* 的指针,讲解 void* 的作用。?

    #include <stdio.h>
    #include <string.h>
    int cmp_int(const void *e1, const void *e2)
    {
        //*e1和*e2接收两个比较值的地址
        return *(int*)e1 - *(int*)e2;
    }
    struct Stu
    {
        char name[20];
        int age;
    };
    
    void test1()//整型比较
    {
        int arr[10] = {9,8,7,6,5,4,3,2,1,0};
        int sz = sizeof(arr)/sizeof(arr[0]);
        qsort(arr, sz, sizeof(arr[0]), cmp_int);
        int i = 0;
        for(i = 0; i < sz; i++)
        {
            printf("%d ", arr[i]);
        }
    }
    int cmp_float(const void *e1, const void *e2)
    {
        if( *(float*)e1 == *(float*)e2)
            return 0;
        else if( *(float*)e1 > *(float*)e2)
            return 1;
        else
            return -1;
        //return ( (int)*(float*)e1 - *(float*)e2); 通过强制转换成int型去做比较
    }
    void test2()//浮点型比较
    {
        float f[] = {9.0, 8.0, 7.0, 6.0, 5.0, 4.0};
        
        int sz = sizeof(f)/sizeof(f[0]);
        qsort(f, sz, sizeof(f[0]), cmp_int);
        int j = 0;
        printf("\n");
        for(j = 0; j < sz; j++)
        {
            printf("%f ", f[j]);
        }
    }
    
    int cmp_stu_by_age(const void *e1, const void *e2)//学生年龄
    {
        return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
    }
    int cmp_stu_by_name(const void*e1, const void *e2)//学生名字
    {
        //比较名字就是比较字符串
        //字符串不能直接用<>=来比较,应该用strcmp函数
        return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
    }
    
    void test3()//结构体比较
    {
        struct Stu s[3]= {{"zhangsan",20},{"lisi",25},{"wangwu",30}};
        int sz = sizeof(s)/sizeof(s[0]);
        qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
        //第一个参数:待排序数组的首元素地址
        //第二个参数:待排序数组的元素个数
        //第三个参数:待排序数组的元素大小,单位字节
        //第四个参数:是函数指针,比较两个元素所用函数的地址(需要自己实现)
        //函数指针的两个参数是:待比较的两个元素的地址
    }
    int main()
    {
         test1();
         test2();
         test3();
        return 0;
    }

    cs