• 万字指针超详细总结


    🏠个人主页:泡泡牛奶

    🌵系列专栏:C语言从入门到入土

    本期将会带大家来认识什么是指针、指针的类型有哪些、指针运算、一级指针、二级指针、指针数组、数组指针、函数指针、函数指针数组等等相关知识,真正从0开始认识指针到指针的各种操作,让你在日常刷题、准备考试也能得心应手(≧∀≦)ゞ

    赶快让我们看看今天的内容吧( ̄︶ ̄)>[GO!]

    99749488_p0

    🚀1. 什么是指针?

    要理解指针, 我们首先要了解2个要点:

    • 指针是内存中一个最小单元的编号,也就是地址

    • 平时所说的指针,指的是指针变量,即用来存放内存地址变量

    内存

    image-20220831200608387

    指针变量

    我们可以通过& (取地址操作符) 取出变量的内存起始地址,将地址存放到一个变量之中,这个变量就是我们所称的指针变量

    #include 
    
    int main()
    {
        int a = 10;//在内存中申请一块空间
        int* p = &a;//用 变量p 存放a的地址
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    类比:

    1. 整型的变量,叫整型变量
    2. 地址的变量,叫指针变量

    注意:

    一个地址占用一个字节,而指针访问的字节数量由指针的类型来决定

    image-20220831215824390 image-20220831220120695

    ✨1.1 如何进行编址?

    对于32位机器,假设有32根地址线,那么每根地址线在寻址的时候产生的高电平(高电压 )和 低电平(低电压)就是 (1 或者 0)

    那么32根地址线产生的地址就会是:

    0000 0000 0000 0000 0000 0000 0000 0000

    0000 0000 0000 0000 0000 0000 0000 0001

    1111 1111 1111 1111 1111 1111 1111 1111

    这里就有 2 32 2^{32} 232 个地址,按每个地址是一个字节来算
    2 32 B y t e = 2 32 1024 k B = 2 32 1024 ∗ 1024 M B = 2 32 1024 ∗ 1024 ∗ 1024 G B = 4 G B 2^{32} Byte = \frac{2^{32}}{1024} kB= \frac{2^{32}}{1024*1024} MB = \frac{2^{32}}{1024*1024*1024} GB = 4GB 232Byte=1024232kB=10241024232MB=102410241024232GB=4GB
    32位机器下有4G的闲置空间进行编址

    同理,如果是64位的机器,就有 2 64 2^{64} 264 根地址线,到底有多大,不妨自己进行计算一下( •̀ ω •́ )✧

    通过上面的介绍,我们可以明白:

    • 在32位机器就需要 32个二进制位 进行二进制排序,那么就需要用到 4B 的空间来存储,故一个32位平台下的指针就应该是4个字节。
    • 同理,在64位机器上,就有64个地址线,那么指针变量的大小就是8个字节。

    总结:

    • 指针是用来存放地址的,地址是唯一一块地址空间
    • 指针在32位平台是4个字节,在64位平台是8个字节

    🚀2. 指针和指针类型

    ✨2.1 指针的类型

    我们知道,变量都是有不同类型的,例如整型、浮点型等。那指针有没有类型呢?

    准确的说是有的,但是指针的类型并不会改变指针的大小。

    有这样一段代码:

    int num = 10;
    p = #
    
    • 1
    • 2

    如果我们想要将 &num (num的地址) 保存到 p 中,已知 p是一个指针变量,那么它的类型就应该是 int* 类型。

    char* pc = NULL;//字符型
    short* ps = NULL;//短整型
    int* pi = NULL;//整型
    long* pl = NULL;//长整型
    
    float* pf = NULL;//单精度浮点型
    double* pd = NULL;//双精度浮点型
    
    void* pv = NULL;//泛型指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:

    有些人可能会有一个疑问,* 到底该靠左边写呢?还是右边写呢?

    • int* pi 可以很容易理解 piint* 类型的,但是这样写也相对会带来一个风险,当出现int* p1, p2; 这样多个命名的写法时,有些人会认为 p2 也是 int* 类型的,但这却是错的p2 实际上的类型是 int 类型。
    • int *p1, *p2 这样的写法解决了上面的风险,但却相较于*靠左边写比较难看出来类型

    总之,只要能分的清楚* 在左边 和 在右边 的 优势和劣势,怎么写都行φ(゜▽゜*)♪

    通过上面可以看到,指针的定义方式是: type + *

    其实:

    char* 类型的指针是为了存放 char 类型变量的地址

    int* 类型的指针是为了存放 int 类型变量的地址

    ✨2.2 指针类型的意义

    1. 进行解引用操作时,访问字节数量

    #include 
    
    int main()
    {
        int n = 0x11223344;
        
        char *pc = (char*)&n;
        int *pi = &n;
        
        *pc = 0;
        *pi = 0;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    指针1

    可以看到 char 类型进行解引用操作时访问的是1个字节,而 int 类型进行解引用操作访问的是4个字节

    2. 决定指针 向前向后 走一步所跨过的字节 (步长)

    #include 
    int main()
    {
        int arr[5] = { 0 };
    
        char* pc = (char*)arr;
        int* pi = arr;
    
        *pi = 0x11223344;
        *pc = 0;
    
        *(pi + 1) = 0x11223344;
        *(pc + 1) = 0;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    指针2

    通过上面的例子,我们可以看到 char 类型是跳过1个字节, 而 int 类型跳过4个字节

    🚀3. 野指针

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

    ✨3.1 野指针成因

    1. 指针未初始化

    #include 
    int main()
    {
        int* p;//局部指针变量未初始化,默认随机值
        *p = 20;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2. 指针越界访问

    #include 
    
    int main()
    {
        int arr[5] = {0};
        int *pi = arr;
        int i = 0;
        for (i=0; i<=5; ++i)
        {
            //当指针指向数组规定的范围时,p就是野指针
            *(pi++) = i;
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3. 指针所指向的空间被释放

    #include 
    #include 
    
    int main()
    {
        int* nums = (int*)malloc(sizeof(int)*5);
        if (!nums)
        {
            return -1;
        }
        
        for (int i = 0; i<5; ++i)
        {
            nums[i] = i;
        }
        
        free(nums);
        
        for (int i = 0; i<5; ++i)
        {
            nums[i] = i;
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    指针3

    上面可以看到,被 malloc 开辟的空间被释放后,指针nums 就变为野指针,再去访问这块空间就会被当成随机访问

    ✨3.2 如何规避野指针

    1. 指针初始化
    2. 注意指针越界
    3. 指针所指向的空间被释放后,使其置为NULL
    4. 避免返回局部变量的地址
    5. 指针使用之前检查其有效性

    🚀4. 指针运算

    ✨4.1 指针 ± 整数

    用于表示指针 向前 或 向后 走的步数

    int arr[5];
    int *pi = arr+3;
    
    *(pi+1) = 4;
    *(pi-1) = 3;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ✨4.2 指针 - 指针

    用于表示 前后两个指针 之间的 元素个数

    int my_strlen(char* str)
    {
        char* p = s;
        while (*p != '\0')
        {
            ++p;
        }
        return p-s;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ✨4.3 指针的关系运算

    C语言标准规定:

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

    image-20220902172905316

    安全写法:

    for (vp = &a[N]; vp > &a[0]; )
    {
        *--vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    危险写法:

    for (vp = &a[N-1]; vp >= &a[0]; vp--)
    {
        *vp = 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    注意: 虽然这样的写法再绝大部分编译器上使可以顺利完成任务的,但我们还是应该尽量避免这样的写法,因为标准规定不保证它可行。

    🚀5. 指针和数组

    请看下面例子:

    #include 
    int main()
    {
        int arr[5] = { 0,1,2,3,4 };
        printf("%p\n", arr);
        printf("%p\n", &arr[0]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果:

    image-20220902180042784

    可以看到数组名和数组首元素地址是一样的。

    结论: 数组名 表示的是 数组首元素地址。

    注意:以下2种情况除外

    1. sizeof(arr) 表示整个数组的(字节)大小
    2. &arr 表示整个数组,&arr + 1 跳过整个数组

    那么,现在我们知道数组名可以表示数组的首元素地址,那么我们是否可以将数组首元素地址存放到一个指针中,利用指针来访问数组呢?

    答案当然是可以的:

    #include 
    
    int main()
    {
        int arr[] = { 1,1,4,5,1,4,1,9,1,9,8,1,0 };
        int* p = arr;
        int sz = sizeof(arr)/sizeof(arr[0]);
        
        for (int i = 0; i<sz; ++i)
        {
            printf("%d ", *(p+i));
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    🚀6. 字符指针(char*)

    一般使用:

    int main()
    {
        char ch = 'w';
        char* pc = &ch;
        *pc = 'w';
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    还有一种使用方法如下:

    int main()
    {
        const char* pstr = "hello world!";//常量字符串
        printf("%s\n", pstr);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    指针指向字符串可以类比数组,同样是指向首元素地址。

    那么,有这样一个问题:

    #include 
    
    int main()
    {
        char str1[] = "hello world!";
        char str2[] = "hello world!";
        
        char *str3 = "hello world.";
        char *str4 = "hello world.";
        
        if (str1 == str2)
            printf("str1 和 str2 相同\n");
        else
            printf("str1 和 str2 不相同\n");
        
        if (str3 == str4)
            printf("str3 和 str4 相同\n");
        else
            printf("str3 和 str4 不相同\n");
        
        return  0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    最终输出结果为:

    image-20220903145140406

    原因: str1str2 分别是两个不同的数组,即使数组内容相同,所占用的空间依然不同;str3str4 指向的是同一个相同的常量字符串,C/C++会把相同的常量字符串存放在一个单独的空间,当存在几个指针指向同一个字符串的时候,它们实际会指向同一块内存。

    对比:

    #include 
    
    int main()
    {
        char str1[] = "hello world!";
        char str2[] = "hello world!";
    
        char* str3 = "hello world.";
        char* str4 = "hello world.";
        char* str5 = "hello world!";
    
        printf("%p\n", str1);
        printf("%p\n", str2);
        printf("%p\n", str3);
        printf("%p\n", str4);
        printf("%p\n", str5);
    
        return  0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220903145312120

    str3str4 指向的常量字符串相同,所以str3 == str4,而str5 所指向的常量字符串不相同,故 str3 != str5

    🚀7. 泛型指针(void*)

    泛型指针,顾名思义,指针具有泛用性,什么类型的指针都可以接收,而这通常也用于函数中进行使用。

    泛型指针特点:

    1. 可以 接收任何类型 的指针
    2. 泛型指针访问字节数0

    注意: void* 只能接收,不能 指向数据(不能通过void*访问数据)

    **思考:**那么这样的指针到底有什么用呢🤔?

    • 可以利用可以接收任何类型的特点,来实现一个可以应对各种类型的数据
    • 在使用过程中可以将泛型指针转换成其它类型

    例如:

    int fun(void* p, void* q)
    {
        return *((int*)p) - *((int*)q);
    }
    
    • 1
    • 2
    • 3
    • 4

    如果想知道更多,请接着往下看哦( •̀ ω •́ )✧

    🚀7. 二级指针

    我们知道,指针变量也是变量,既然是变量就有地址,那么指针变量的地址存放在哪里?

    image-20220903174810955

    二级指针 可用于存放一级指针的地址

    对于二级指针的运算有:

    • *pi 通过对 pi 中地址进行解引用,这样找到的是i*pi 其实访问 的就是 i

    • **ppi 先通过 *ppi 找到 pi 内存放的值, 然后再通过 *pi 找到 i 内存放的值

    int i = 10;
    int *pi = &i;//pa 等价于 &a
    int **ppi = &pi;
    
    **ppi = 30;
    //等价于 *pa = 30;
    //等价于 a = 30;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    🚀8. 指针数组

    **Q:**指针数组到底是 指针 还是 数组 🤔?

    **A:**是 数组

    类比:

    • 整型数组:int arr1[5] 用于连续存放一组整型数据
    • 字符数组:char arr2[5] 用于连续存放一组字符型数据

    image-20220913202917668

    那么指针数组是什么样的?

    int* arr3[5];
    
    • 1

    解释:arr3首先与[]结合,说明arr3是一个数组,数组里面存放了5个元素,每个元素是int*类型

    按操作符的优先级来说,arr3首先会与[]结合,再看到*

    image-20220913203707711

    #include 
    
    int main()
    {
        int arr1[5] = { 1,2,3,4,5 };
        int arr2[5] = { 2,3,4,5,6 };
        int arr3[5] = { 3,4,5,6,7 };
    
        int* arr[3] = { arr1,arr2,arr3 };
    
        for (int i = 0; i < 3; ++i)
        {
            for (int j = 0; j < 5; ++j)
            {
                //写法一
                printf("%d ", *(arr[i] + j));
                //写法二
                //printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们知道,数组名代表其元素的首元素地址,通过arr[i] 访问到具体哪个地址,再通过地址访问到数组元素

    总结:

    什么是指针数组🤔?

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

    🚀9.数组指针

    指针是数组是数组,那么数组指针就是指针。

    通过上面类比,我们可以思考思考:

    什么是 数组指针🤔?

    如同上面所说,按优先级来看,如果写成int* arr[5] 又是指针数组,那么该如何表示呢?

    类比:

    • 整型指针:int* pi = &a( 假设定义了 int a = 0

    • 浮点型指针:float* pf = &b ( 假设定义了 float b = 0.0f

    int (*p)[10];
    
    • 1

    解析: p先与*结合,说明p是一个指针变量,然后指向一个大小为10的整型数组。所以p是一个指针,指向一个数组,脚数组指针。

    使用实例:

    #include 
    
    int main()
    {
        int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
        
        int(*pa)[10] = arr;
        //error
        //arr是首元素地址,为int*类型
        
        int(*pa)[10] = &arr;
        //指向整个数组
        
        for (int i = 0; i<10; ++i)
        {
            printf("%d ",(*pa)[i] );
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    小结:

    • Q:什么是 数组指针🤔?

    • A:数组指针就是一个指向 (整个)数组 的指针。

    注意:

    指针的类型需要与数组的类型相同,以上面为例,arr 的类型是int[10],那么(*pa)的类型也应该是int[10]

    - 小试牛刀

    现在我们已经对 指针数组数组指针 有了一定的了解了,那么就来试试下面几个例子吧φ(゜▽゜*)♪

    int *parr1[10];
    int (*parr2)[10];
    int (*parr3[10])[5];
    
    • 1
    • 2
    • 3
    1. int *parr1[10]

    按优先级结合顺序来说,parr1首先与[]结合,确定是数组,是什么数组?向外看,类型是int*类型,是 一个存放int*类型的 指针数组

    1. int (*parr2)[10]

    parr2首先与*结合,是指针,是什么指针?向外看类型为int [10],是一个指向有10个元素的整型数组的 数组指针

    1. int (*parr3[10])[5]

    parr3首先与[]结合,说明()内部是数组,去除parr3[10]后剩下int (*)[5],可以看出这是一个数组指针,那么组合在一起就是一个数组指针数组

    image-20220917233250473

    🚀10. 数组参数、指针参数

    在写代码的时候难免要将数组或者指针传给数组,那么函数的参数该如何设计呢?

    ✨10.1 一维数组传参

    我们知道,数组名就是首元素的地址,而指针又是可以接收数组的存在,那么我们是否可以这样写?

    void test1(int arr[])//    1
    {}
    void test2(int arr[10])//  2
    {}
    void test3(int* arr)//     3
    {}
    void test4(int* arr[10])// 4
    {}
    void test5(int** arr)//    5
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. void test1(int arr[])
    2. void test2(int arr[10])
    3. void test3(int* arr)
    #include 
    
    //假设定义了以上代码
    
    int main()
    {
        int arr1[20] = {0};
        test1(arr1);
        test2(arr1);
        test3(arr1);
        
        int *arr2[10];
        test4(arr2);
        test5(arr2);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1、2、3的调用方式都相同,虽然void test2(int arr[10])中参数为int arr[10]但是还是能访问到20个元素,因为数组在内存中是连续存放

    ✨10.2 二维数组的传参

    void test1(int arr[3][5])//有具体数值
    {}
    void test2(int arr[][5])//省略行
    {}
    
    //error
    //不能都省略
    void test3(int arr[][])
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    总结:

    二维数组传参,函数参数的设计只能省略 第一个[]的数字。对于一个二维数组而言,可以不知道有多少行,但是必须知道一行有对少个元素,这样才能方便运算。

    在数组一章中 🚀点此传送门[数组] ,我们知道二维数组在内存中存储实际上与一维数组相同,都是由低地址向高地址连续存储,那么我们是否也是可以将二维数组以一维数组的方式进行传参呢🤔?答案是可以的。

    void test4(int* arr)
    {}
    
    • 1
    • 2

    结合指针数组,也可以写成下面这样

    #include 
    #include 
    
    void test5(int** arr, int row, int col)
    {
        int i = 0;
        for (i = 0; i < row; ++i)
        {
            int j = 0;
            for (j = 0; j < col; ++j)
            {
                arr[i][j] = i + j;
                printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
    }
    
    void test6(int* arr[5], int row, int col)
    {}
        
    int main()
    {
        int i = 0;
        int* arr[5];
        
        //构建二维数组
        for (i=0; i<5; ++i)
        {
            int* tmp = (int*)malloc(sizeof(int)*10);
            assert(tmp);
            arr[i] = tmp;
        }
        
        //传参
        test5(arr, 5, 10);
        test6(arr, 5, 10);
        
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行结果:

    image-20220917234342833

    而这样的传参方法多可能会在OJ(例如:力扣)上遇到 ( •̀ ω •́ )✧暗示

    ✨10.3 一级指针传参

    #incldue <stdio.h>
    
    void test(int *p, int sz)
    {
        int i = 0;
        for (i=0; i<sz; ++i)
        {
            *(p+i) = i;
            printf("%d ", *(p+i));
        }
    }
    
    int main()
    {
        int arr[10] = {0};
        int *p = arr;
        int sz = sizeof(arr)/sizeof(arr[10]);
        
        //一集指针p,传给函数
        test(arr, sz);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    思考🤔

    当函数参数为一级指针时,函数可以传什么值?

    1. 传数组

    主要用于对数组进行操作

    1. 传单个数值

    用于修改某个数的值 (在之后的学习中,相信大家会遇到很多这样的情况☺️)

    ✨10.4 二级指针传参

    #include 
    
    void test(int **ptr)
    {
        printf("%d\n", **ptr);
    }
    
    int main()
    {
        int n = 10;
        int *p = &n;
        int **pp = &p;
        test(pp);
        test(&p);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    二级指针接收一集指针地址,传参的时候需要参数为一级指针地址

    🚀11. 函数指针

    首先我们来看一下下面一段代码:

    #include 
    
    void test()
    {
        printf("hehe\n");
    }
    
    int main()
    {
        printf("%p\n", test);
        printf("%p\n", &test);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出结果为:

    image-20220918153731193

    输出的是两个相同的地址,而这两个就是test函数的地址,那么要怎样将函数的地址存起来呢?

    //假设有以下函数定义
    void test(int i)
    {
        printf("haha\n");
    }
    
    //那么函数指针应该为
    void (*pfun)(int) = test;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    pfun首先与*结合,说明pfun是指针,其指向的是一个函数,指向的函数参数为int,返回类型为void

    看两段有意思的代码:

    //代码1
    (*(void (*)())0)();
    //代码2
    void (*signal(int , void(*)(int)))(int);
    
    • 1
    • 2
    • 3
    • 4
    • (* (void (*)()) 0 )()

      对代码稍微进行简化一下

      typedef void (*type_t)();
      
      //函数指针在进行调换是要将名字放在()里
      //理解上:效果等同于
      //typedef void (*)() type_t;
      
      //简化后
      (* (type_t) 0 )()
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      可以发现,0被强制类型转换成了函数指针类型,再进行函数调用。

    • void (* signal( int , void(*)(int) ) )(int)

      我们按照同样的方法对大妈进行简化

      typedef void(*type_t)(int);
      //简化后
      type_t signal(int, type_t);
      
      • 1
      • 2
      • 3

      可以轻易看出:

      1. signal是一个函数声明,一个参数是int,另一个参数是函数指针参数int返回类型void
      2. signal的返回类型也是一个函数函数指针,该指针指向一个函数参数int返回类型void

    🚀12. 函数指针数组

    数组是一个存放相同类型数据的储存空间,类比指针数组:

    int* arr[10];
    //arr先与[]结合,每个元素是int*
    
    • 1
    • 2

    仿照指针数组

    1. 首先名字要与[]先结合,再写出类型int (*)() parr[10]

    2. 再根据函数指针的语法规定,将数组放入()内部,于是就出现了下面这样

      int (* parr[10] )();
      
      • 1

    那么对于这样的函数指针数组,它的用途是什么呢?

    答:转移表

    可以将 函数 参数相同返回类型相同 的一类函数用一个数组存起来,最后可以通过数组的下标调用函数

    而最简单的实例就是:计算器

    🚀13. 指向 函数指针数组 的指针

    对比指向 指针数组 的指针:

    指针指向一个数组,数组每个元素都是指针(指路✨->二维数组传参)

    int* arr[10];
    
    int **p = &arr;
    
    • 1
    • 2
    • 3

    那么我们接下来看看指向函数指针数组的指针要怎么定义吧😎。

    void test(const char* str)
    {
        printf("%s\n", str);
    }
    
    int main()
    {
        //函数指针
        void (*pfun)(const char*) = test;
        
        //将函数指针存入数组
        void (*pfarr[5])(const char*);
        pfarr[0] = test;
        
        //指向函数指针数组pfarr的指针ppfarr
        void (* (*ppfarr[5]) )(const char*) = &pfarr;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    🚀14. 回调函数

    回调函数就是通过函数指针调用的函数。将函数的地址作为参数传递给另一个函数, 当这个函数指针用来调用其所指向的函数时,我们就说这是回调函数。

    qsort这个函数就是利用了回调函数,下面为大家简单介绍一下这个函数吧😎

    image-20220918194754335

    base - 需要排序的数组

    num - 数组元素个数

    size - 每个元素的大小

    compare - 比较两个数的函数

    #include 
    
    //回调函数
    int cmp(const void * p1, const void * p2)
    {
    	return (*( int *)p1 - *(int *) p2);
    }
    
    int main()
    {
    	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
        int sz = sizeof(arr) / sizeof(arr[0]);
    	int i = 0;
        
    	qsort(arr, sz, sizeof(int), cmp);
        
        //打印
    	for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
    	{
    		printf( "%d ", arr[i]);
    	}
    	printf("\n");
        
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    现在我们已经基本了解 什么是回调函数 与 qsort的基本使用方法 ,那么能不能试着仿照 qsort 写一个冒泡排序呢?

    void _swap(void *p1, void * p2, int size)
    {
    	int i = 0;
    	for (i = 0; i< size; i++)
    	{
    		char tmp = *((char *)p1 + i);
    		*(( char *)p1 + i) = *((char *) p2 + i);
    		*(( char *)p2 + i) = tmp;
    	}
    }
    
    void BubbleSort(void* base, size_t num, size_t size,
                int (*cmp)(const void*,const void*))
    {
        int i = 0;
        int j = 0;
        for (i=0; i<num-1; ++i)
        {
            int flag = 1;
            for (j=0; j<num-i-1; ++j)
            {
                if ( cmp( (char*)base + j*size, 
                          (char*)base + (j+1)*size ) > 0)
                {
                    _swap((char*)base + j*size, 
                          (char*)base + (j+1)*size,
                           size );
                    flag = 0;
                }
            }
            if (flag)
            {
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    注意:

    base需要强制类型转换成char*,因为void*的访问的是0字节,传入具体参数需要具体参数,至于转成char*是因为,char*每次跳过一个字节,方便访问地址

    如果有其它想要了解的函数可以考虑去cplusplus官网看看,连接我就放在这里啦(≧∀≦)ゞ


    好啦ヾ(*))) 本期的内容就到这里,如果觉得对你有帮助的话,还不忘三连支持一下,谢谢ο(=•ω<=)ρ⌒☆

    这一次真的是爆肝了(;´д`)ゞ超长篇的大总结,如果对你有帮助的话,还不忘动动手指给予我一点点支持吧o(TヘTo)

  • 相关阅读:
    二阶段面试题——JavaScript
    设计模式(13)适配器模式
    nginx502常见502错误问题解决办法
    【笔试强训day01】组队竞赛 && 删除公共字符
    python计算斜率以及给定一组点两两求斜率
    陈宥维《虎鹤妖师录》“显眼包”太子成长记 表演灵动获好评
    网络安全和信息安全
    Linux设置N天未登录强制冻结
    [原创][开源]C# Winform DPI自适应方案,SunnyUI三步搞定
    MySQL字符串函数
  • 原文地址:https://blog.csdn.net/xiao_feng0211/article/details/126944118