进阶-指针详解

指针的进阶

1.字符指针
2.数组指针
3.指针数组
4.数组传参和指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数
9.指针和数组面试题的解析

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

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

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

一般使用:

int main()
{
    char ch = 'w';
    char* pc = &ch;
    *pc = 'wc';
    printf("%c\n", ch);//wc
    return 0;
}

还有一种使用方式如下:

int main()
{
    char* p = "abcdef";
    printf("%s\n", p);//abcdef
    return 0;
}
本质是把字符串首字符a的地址,赋值给了p
因为s一定是打印字符串所以printf会从首地址开始向后遍历一直到\0
char arr[] = "abcdef";这种写法才是把abcdef放到数组arr里面去。

题目:

int main()
{
    const char1* p1 = "abcdef";
    const char2* p2 = "abcdef";

    char arr1[] = "abcdef";
    char arr2[] = "abcdef";

    if (p1 == p2)
    {
        printf("p1==p2\n");
    }
    else
    {
        printf("p1!=p2\n");
    }

    if (arr1 == arr2)
    {
        printf("arr1==arr2\n");
    }
    else
    {
        printf("arr1!=arr2");
    }
    return 0;
}

结果是:
p1 = p2; arr1 != arr2;

"abcdef\0"是一个常量字符串,不能被改,内存中存一份就够了, * p1, * p2都是对它的解引用,p1,p2指针里面放的都是a的地址。
而arr1[]、arr2[]创建的是两个独立的数组,用"abcdef\0"来初始化它们,它们有自己独立的空间。它们的起始地址肯定不一样,而数组名恰好是首元素的地址所以arr1和arr2不同

用户输入一行字符串,统计此字符串中每一个小写字母出现的次数
input a string : abckckchcAD
a : 1
b : 1
c : 4
h : 1
k : 2

int main()
{
    char arr[13] = "abckckchcAD";
    int arr1[26] = { 0 };
    int i = 0;
    for (i = 0; arr[i] != '\0'; i++)
    {
        if (arr[i] >= 97 && arr[i] <= 122)
        {
            int tmpe = 97;
            arr1[arr[i] - 97]++;
        }
    }
    for (int i = 0; i < 26; i++)
    {
        if (arr1[i] > 0)
        {
            printf("%c : %d\n", 97 + i, arr1[i]);
        }
    }

    return 0;
}

先遍历这个字符串,遇到小写,放到一个初始化为全0的数组,arr[i] - 97代表当前字符 arr[i] 相对于 'a' 的偏移量。
若 arr[i] 是 'a',那么 'a' - 'a' 就等于 0;若 arr[i] 是 'b',则 'b' - 'a' 等于 1;以此类推,若 arr[i] 是 'z','z' - 'a' 等于 25。
刚好把每个小写字母映射到了 0 到 25 这个区间,而这个区间正好对应着 arr1 数组的索引范围。
arr1[arr[i] - 'a'] 表示访问 arr1 数组中索引为 arr[i] - 'a' 的元素。
比如,当 arr[i] 是 'a' 时,arr1[arr[i] - 'a'] 就是 arr1[0];当 arr[i] 是 'b' 时,arr1[arr[i] - 'a'] 就是 arr1[1]。
arr1[arr[i] - 'a']++; 的作用是将 arr1 数组中索引为 arr[i] - 'a' 的元素的值加 1。
也就是说,每当在字符串中遇到一个小写字母,就把 arr1 数组中对应这个字母的计数加 1。
假设 arr 数组存储的字符串是 "abc",下面是代码执行过程中 arr1 数组的变化情况:
当 i = 0 时,arr[i] 是 'a',arr[i] - 'a' 等于 0,arr1[0] 加 1,此时 arr1 数组变为{ 1, 0, 0, ... }。
当 i = 1 时,arr[i] 是 'b',arr[i] - 'a' 等于 1,arr1[1] 加 1,此时 arr1 数组变为{ 1, 1, 0, ... }。
当 i = 2 时,arr[i] 是 'c',arr[i] - 'a' 等于 2,arr1[2] 加 1,此时 arr1 数组变为{ 1, 1, 1, ... }。
printf("%c : %d", i + 97, arr1[i]);

可以把,97直接改成'a', 122改成'z'.

强化一下指针:

int main()
{
    char arr[13] = "abckckchcAD";
    int arr1[26] = { 0 };
    char* p = arr;
    int* p1 = arr1;
    while (*p != '\0')
    {
        if (*p >= 97 && *p <= 122)
        {
            (*(p1 + (*p - 97)))++;
        }
        p++;
    }
    int i = 0;
    for (i = 0; i < 26; i++)
    {
        if (*(p1 + i) > 0)
        {
            printf("%c : %d\n", 97 + i, *(p1 + i));
        }
    }

    return 0;
}

2. 指针数组
在《指针》章节我们也学了指针数组,指针数组是一个存放指针的数组,这里我们再复习一下,下面指针数组是什么意思 ?

int arr[10];//整型数组
char ch[5];//字符数组
int* arr1[10];//存放整形指针的数组
char* arr2[4];//存放字符指针的数组(一级字符指针的数组)
char** arr3[5];//二级字符指针的数组(二级指针是用来存放一级指针变量的地址的)

指针数组 顾名思义,重点是数组,所以指针数组- 是数组,是用来存放指针的数组。

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 };


    return 0;
}

      parr
int*     arr1         1 2 3 4 5 arr1
int*     arr2         2 3 4 5 6 arr2
int*     arr3         3 4 5 6 7 arr3

这样就模拟出了一个二维数组

int i=0;
for (i = 0; i < 3; i++)
{
    int j = 0;
    for (j = 0; j < 5; j++)
    {
        printf("%d ", *(parr[i] + j));
        //*(p+i)==p[i]
        // *(parr[i] + j))==parr[i][j]
    }
    printf("\n");
}

这样就把三个数组的元素都打印出来了

3.数组指针

3.1 数组指针的定义

数组指针是指针 ? 还是数组 ?
答案是 : 指针。

我们已经熟悉
整形指针 : int* pint; 能够指向整形数据的指针。
浮点型指针 : float* pf; 能够指向浮点型数据的指针。
那数组指针应该是 : 能够指向数组的指针。

下面哪个是指针?

int* p1[10];
int(*p2)[10];

p1,p2分别是什么?

p1是指针数组,数组类型是int*.
p2先和*结合,p2是指针,指向一个10个元素的数组,数组类型是int.
p2就是一个数组指针。

3.2 &数组名VS数组名

再次讨论数组名:
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0;
}

打印结果一样,数组名就是首元素地址。

但是:

int sz = sizeof(arr);
printf("%d\n", sz);
打印结果是40,而非4.

总结:
数组名通常表示的都是数组首元素的地址。
但是有两个例外:
1. sizeof(数组名),(单独放一个数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节。
2. &数组名,这里的数组名,依然表示的是整个数组,所以&数组名取出的是整个数组的地址。

int arr[10] = { 0 }; 

printf("%p\n",arr);//84
printf("%p\n",arr + 1);//88

printf("%p\n", &arr[0]); &arr[0] + 1);//84
printf("%p\n", &arr[0] + 1);//88

printf("%p\n", &arr);//84
printf("%p\n", &arr + 1);//AC(相差40)

&arr + 1;跳过的是整个数组。

int main()
{
    int arr[10] = { 0 };
    int* p = arr;
    int(*p2)[10] = &arr;
    return 0;
}

整型指针是用来存放整型的地址
字符指针是用来存放字符的地址
数组指针是用来存放数组的地址(&arr)

取地址数组名,拿到数组的地址,放到一个数组指针里面去p2,*p2,代表p2是指针,int,[10],代表指向10个整型元素的数组。

p的类型是int*,是一个整数类型的指针
p2的类型就是int(*)[10],这就是一个数组指针的类型

&arr 要放到int(*p2)[10] = &arr;中,而不是int* p = &arr;这样。

3.3 数组指针的使用

int main()
{
    char* arr[5] = { 0 };
    char* (*pc)[5] = &arr;
    return 0;
}

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int(*p)[10] = &arr;

    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (i = 0; i < sz; i++)
    {
        printf("%d ", *(*p + i));
    }//p是指向数组的,*p其实就相当于数组名,数组名又是数组首元素的地址,所以*p本质上是数组首元素的地址
    return 0;
}
这样写麻烦

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int i = 0;
    for (i = 0;, i < 10; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}
一般这样写

void printl(int arr[3][5], int r, int c)
{
    int i = 0;
    for (i = 0; i < r; i++)
    {
        int j = 0;
        for (j = 0; j < c; j++)
        {
            printf("%d ", arr[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 };
    print1(arr,3,5);
    return 0;
}
打印二维数组

这里函数传参传的是数组,但是数组是首元素地址,也可以用指针来接收,如下:

int arr[3][5];
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
arr是数组名,表示数组首元素的地址
二维数组首元素的地址是它的第一行,就是一个一维数组的地址
用数组指针int(*p)[5]来接收

void print2(int (*p)[5], int r, int c)
{
    int i = 0;
    for (i = 0; i < r; i++)
    {
        int j = 0;
        for (j = 0; j < c; j++)
        {
            printf("%d ", *(*(p + i) + j));
        }//p+i代表第i行整行的一维数组的指针*(p + i)
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
    print2(arr,3,5);
    return 0;
}

传过去的arr是二维数组的数组名,它表示数组首元素的地址,
二维数组首元素的地址是它的第一行,就是一个一维数组的地址,
用指向一维数组的int(*p)[5]数组指针接收,
指针变量p是指向整个数组的,* p其实就相当于数组名,数组名又是数组首元素的地址,所以* p本质上是数组首元素的地址
指针变量(p + i)是指向第i + 1行的整个一维数组, * (p + i)是第i + 1行的整个一维数组的数组名,
数组名又是数组首元素的地址, * (p + i) + j,表示第i + 1行的整个一维数组的第j + 1歌元素的地址,
对其解引用 * (*(p + i) + j),表示的就是第i + 1行第j + 1个元素。

重点:数组指针int(*p)[5]中的指针变量p是指向整个数组的, * p其实就相当于数组名,数组名又是数组首元素的地址,所以 * p本质上是数组首元素的地址

整理:

1. 二维数组名作为参数传递
在 C 语言中,二维数组名本质上是数组首元素的地址。
对于二维数组 arr[3][5] 而言,其首元素是第一行,也就是一个包含 5 个 int 类型元素的一维数组。
所以,arr 代表的是第一行这个一维数组的地址。

2. 数组指针接收二维数组名
函数 print2 的第一个参数 int(*p)[5] 是一个数组指针,它指向的是包含 5 个 int 类型元素的一维数组。
当把二维数组名 arr 传递给 print2 函数时,p 就指向了二维数组的第一行。

3. 对数组指针解引用
p 是指向整个一维数组的指针, * p 相当于这个一维数组的数组名。
数组名在大多数表达式中会 “退化” 为指向数组首元素的指针,所以 * p 本质上是数组首元素的地址。
p + i 指向二维数组的第 i 行(数组下标从 0 开始), * (p + i) 相当于第 i 行这个一维数组的数组名,同样会 “退化” 为指向第 i 行首元素的指针。
* (p + i) + j 表示第 i 行第 j 个元素的地址,对其解引用 * (*(p + i) + j) 就得到了第 i 行第 j 个元素的值。

补充:
* (*(p + i) + j) == p[i][j];
在 C 语言中,p[i] 其实就等价于* (p + i)。这是因为[] 是一个下标运算符,p[i] 会被编译器解释为* (p + i)。
同理,p[i][j] 就等价于* (p[i] + j),而 p[i] 又等价于* (p + i),所以 p[i][j] 最终等价于* (*(p + i) + j)。

int(*p)[5];
p 的类型是 : int(*)[5];
p 是指向一个整型数组的,数组5个元素 int[5]
p + 1->跳过一个5个int元素的数组

int arr[10]; 
int(*p)[10]
int(*p)[10] = &arr;    类型为int(*)[10];
int* p2; 
p2 + 1-- > 跳过一个整型
& arr + 1 -- > 40

& arr--->int(*)[10]--->40
arr---- > int* ---- > 4

回顾一下:

int arr[5]; arr是整型数组
int* parr1[10]; parr1是指针数组
int(*parr2)[10]; parr2是数组指针
int(*parr3[10])[5]; parr3是存放数组指针的数组

1 2 3 4 5 arr1
2 3 4 5 6 arr2
3 4 5 6 7 arr3

int(*parr3[10])[5] = { &arr1,&arr2,&arr3 };        

parr3  &arr1,arr2,arr3...

4. 数组参数、指针参数

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

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int* arr)//ok
{}

void test2(int* arr[20])//ok
{}
void test2(int* arr[])//ok
{}
void test2(int** arr)//ok,用指针来接收,存放指针的数组,arr2是数组名,是首元素地址,一个int*元素的地址,用int**来接收。二级指针是用来存放一级指针变量的地址
{}
int main()
{
    int arr[10] = { 0 };
    int* arr2[20] = { 0 };//指针数组,每个元素的类型是int*
    test(arr);
    test2(arr2);
    return 0;
}

4.2 二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//不ok,二维数组传参,形参行可以省略,但是列不可以省略
{}
void test(int arr[][5])//ok
{}

void test(int *arr)//不ok,arr表示第一行一维数组的地址,不能放到一级指针里
{}
void test(int* arr[5])//不ok,这是一个指针数组
{}
void test(int(*arr)[5])//ok
{}
void test(int** arr)//不ok,一维数组不能放到二级指针里
{}

int main()
{
    int arr[3][5] = { 0 };
    test(arr);//二维数组的数组名表示首元素的地址,二维数组的首元素是第一行的整个一维数组,所以二维数组的数组名表示第一行的地址
    return 0;
}

4.3 一级指针传参

#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;
}

4.4 二级指针传参

#include <stdio.h>
void test(int** ptr)
{
    printf("num = %d\n", **ptr);
}

int main()
{
    int n = 10;
    int* p = &n;
    int** pp = &p;
    test(pp);//ok
    test(&p);//ok
    return 0;
}

5. 函数指针

数组指针--->指向数组的指针就是数组指针
函数指针--->指向函数的指针就是函数指针

int Add(int x, int y)
{
    return x + y;
}
int main()
{
    int arr[5] = { 0 };//&数组名-取出的数组的地址
    int(*p)[5] = &arr;//数组指针
    printf("%pn", &Add);//&函数名-取出的就是函数的地址
    printf("%pn", Add);//对于函数来说,&函数名和函数名都是函数的地址
    //那么要把这个指针存起来,应该用什么变量呢?
    int (*pf)(int, int) = &Add;
    //那么函数指针怎么用呢
    int ret = (*pf)(2, 3);//int ret = Add(2,3);
    int ret = pf(2, 3);//*也可以不写,写了更容易理解,但是写了就要用括号
    printf("%d", ret);
    return 0;
}

int Add(int x, int y)
{
    return x + y;
}
void calc(int(*pf)(int, int))
{
    int a = 3;
    int b = 5;
    int ret = pf(a, b);
    printf("%d\n",ret);
}
int main()
{
    calc(Add);
    return 0;
}

函数指针的用途

写一个计算器
加法、减法、乘法、除法

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;
    int ret = 0;
    do
    {
        menu();
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = add(x, y);
            printf("%d\n", ret);
            break;
        case 2:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = sub(x, y);
            printf("%d\n", ret);
            break;
        case 3:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = mul(x, y);
            printf("%d\n", ret);
            break;
        case 4:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = div(x, y);
            printf("%d\n", ret);
            break;
        case 0:
            printf("退出\n");
            break;
        default:
            printf("选择错误重新选择\n");
            break;
        }
    } while (input);

        return 0;
}

但是很冗余,printf("请输入两个操作数:");scanf("%d %d", &x, &y);这两个重复出现

使用函数指针:

void calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ret = 0;
    printf("请输入两个操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("%d\n", ret);
}

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 0:
            printf("退出\n");
            break;
        default:
            printf("选择错误重新选择\n");
            break;
        }
    } while (input);

    return 0;
}

6. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组比如 :
int* arr[10];//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢 ?

int(*parr1[10])();
int* parr2[10]();
int(*)()parr3[10];

答案是:parr1

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(*pf)(int,int)= add;//pf是函数指针
    int(*arr[4])(int, int) = { add,sub,mul,div };//arr是函数指针数组,把pf名字换成数组就行
    for (i = 0; i < 4; i++)
    {
        int ret = arr[i](8, 4);
        printf("%d\n", ret);
    }
    return 0;
}

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

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

int main()
{
    int(*pfarr[])(int, int) = { 0,add,sub,mul,div };//函数指针数组
    int(*(*ppfarr)[5])(int, int) = &pfarr; //指向 函数指针数组 的指针
    return 0;
}

8. 回调函数

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

冒泡排序:

9 8 7 6 5 4 3 2 1 0
8 9 7 6 5 4 3 2 1 0
8 7 9 6 5 4 3 2 1 0
...
8 7 6 5 4 3 2 1 0 9

这叫一套冒泡排序,下一次只用针对8 7 6 5 4 3 2 1 0

8 7 6 5 4 3 2 1 0 9
7 8 6 5 4 3 2 1 0 9
7 6 8 5 4 3 2 1 0 9
...
7 6 5 4 3 2 1 0 8 9

......

有n个元素的时候,需要进行n - 1套冒泡排序
第一套需要n - 1次排序
第二套需要n - 2次排序
...
n - 1,n - 2,...2,1
等差数列
n - 1 + n - 2 + ... + 2 + 1 = (n - 1)n / 2;
所以,无论数组初始状态如何,冒泡排序的比较次数固定为(n - 1)n / 2 次

void bubble_sort(int arr[], int sz)
{
    int i = 0;
    //趟数
    for (i = 0; i < sz - 1; i++)
    {
        //一趟冒泡排序的过程
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main()
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    //把数组排成升序
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

这是基础版,如果元素是0 1 2 3 4 5 6 7 8 9,还是会循环这么多次,只是没有交换。

可以加个变量flag判断是否进行交换,如果第一趟冒泡排序的时候发现一对都没有交换,就不用继续排序了

void bubble_sort(int arr[], int sz)
{
    int i = 0;
    //趟数
    for (i = 0; i < sz - 1; i++)
    {
        int flag = 1;//假设数组是排好序
        //一趟冒泡排序的过程
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = 0;
            }
        }
        if (flag == 1)
        {
            break;
        }
    }
}

但是这个冒泡排序只能排整型

有个库函数叫qsort,使用快速排序的思想实现的一个排序函数。

void qsort(void* base, //你要排序的数据起始位置
           size t num,//待排序的数据元素的个数
           size_t width,//待排序的数据元素的大小(单位是字节)
           int(_cdecl* compare)(const void* eleml, const void* elem2)//函数指针,比较函数
           // compare是一个比较函数的地址,eleml、elem2是需要比较的两个元素的地址,因为qsort有可能要排序各种各样的数据,不止整型。
           );
//_cdecl 函数调用约定,可以省略

int main()
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    //把数组排成升序
    int sz = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, sz, sizeof(arr[0]), compare_int); //compare_int 会隐式转换为指向 compare_int 函数的指针

    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}
//现在需要比较的是两个整型元素
//elem1指向一个整数,elem2指向另外一个整数
int compare_int(const void* elem1, const void* elem2)
{
    if (*(int*)elem1 > *(int*)elem2)//强制类型转换为int*
        return 1;
    else if ((*int*)elem1 == *(int*)elem2)
        return 0;
    else
        return -1;//这个返回值是库函数规定的
    //优化一下,可以直接写成这个
    //return  (*(int*)elem1 - *(int*)elem2);
}

//下次想排序其它类型的数据的时候就可以直接改compare_int函数,qsort函数通过compare_int函数指针调用compare_int函数
//总之,通过修改比较函数,qsort 函数可以灵活地对不同类型的数据进行排序。
补充:
int main()
{
    int a = 10;
    char* pa = &a;//int*
    void* pv = &a;//void*是无具体类型的指针,可以接受任意类型的地址
    //void*是无具体类型的指针,所以不能直接解引用操作,也不能+-整数

    return 0;
}

整理一下,接下来排序其它类型的数据:


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

int compare_int(const void* elem1, const void* elem2)
{
    return  (*(int*)elem1 - *(int*)elem2);
}

void test1()//排序整型
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    //把数组排成升序
    int sz = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, sz, sizeof(arr[0]), compare_int); //compare_int 会隐式转换为指向 compare_int 函数的指针

    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n\n");
}

//定义结构体
struct stu
{
    char name[20];
    int age;
};

//按照名字排序
int compare_stu_byname(const void* elem1, const void* elem2)
{
    //比较字符串大小用strcmp函数
    //strcmp恰好返回的值就是>0,<0,==0的值
    return strcmp(((struct stu*)elem1)->name, ((struct stu*)elem2)->name);//强制类型转换为(struct stu*)
    //使用变量 . 成员名((*(struct stu*)elem1).name ) 操作符时会进行结构体的复制操作,这可能会带来额外的开销。
    //而使用 -> 操作符直接通过指针访问成员可以避免复制操作,通常效率更高。所以在实际编程中,更推荐使用 -> 操作符。
}
/*
strcmp 函数不是比较字符串长度,而是比较字符串的内容。
它按照字符的 ASCII 码值逐个比较两个字符串中的对应字符,直到发现不同的字符或者到达字符串末尾。
从两个字符串的第一个字符开始,依次比较相同位置的字符。
例如,比较 "hello" 和 "world",首先比较 'h' 和 'w',由于 'h' 的 ASCII 码值小于 'w' 的 ASCII 码值,
所以 strcmp 函数会判定 "hello" 小于 "world",而不会继续比较后面的字符。
根据比较结果返回不同的值。如果两个字符串相等,返回 0;如果第一个字符串大于第二个字符串,返回一个正整数;
如果第一个字符串小于第二个字符串,返回一个负整数。例如,strcmp("abc", "abd") 返回一个负整数,因为 'c' 的 ASCII 码值小于 'd' 的 ASCII 码值。
*/

//按照年龄排序
int compare_stu_byage(const void* elem1, const void* elem2)
{
    return ((struct stu*)elem1)->age - ((struct stu*)elem2)->age;//强制类型转换为(struct stu*)
}

void test2()
{
    //使用qsort来排序结构体
    struct stu s[] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), compare_stu_byname);

    printf("Sorted by name:\n");
    for (int i = 0; i < sz; i++)
    {
        printf("Name: %s, Age: %d\n", s[i].name, s[i].age);
    }
    printf("\n");
}

void test3()
{
    //使用qsort来排序结构体
    struct stu s[] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), compare_stu_byage);

    printf("Sorted by age:\n");
    for (int i = 0; i < sz; i++) {
        printf("Name: %s, Age: %d\n", s[i].name, s[i].age);
    }
    printf("\n");
}


int main()
{
    test1();
    test2();
    test3();
    return 0;
}

下面基于排序算法模拟实现一下qsort函数:

#include <stdio.h>

// 比较函数
int compare(const void* elem1, const void* elem2)
{
    return  (*(int*)elem1 - *(int*)elem2);
}

// 交换函数
void Swap(char* buf1, char* buf2, size_t width)
{
    size_t i = 0;
    for (i = 0; i < width; i++)
    {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

// 自定义的 qsort 函数
void my_qsort(void* base, size_t sz, size_t width, int(*cmp)(const void* elem1, const void* elem2))
{
    size_t i = 0;
    // 趟数
    for (i = 0; i < sz - 1; i++)
    {
        int flag = 1; // 假设数组是排好序
        // 一趟冒泡排序的过程
        size_t j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
            {
                // 交换
                Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
                flag = 0;
            }
        }
        if (flag == 1)
        {
            break;
        }
    }
}

// 测试代码
int main()
{
    int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    size_t sz = sizeof(arr) / sizeof(arr[0]);

    my_qsort(arr, sz, sizeof(arr[0]), compare);

    for (size_t i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

9. 指针和数组笔试题解析

int main()
{
    int a[] = { 1,2,3,4 };

    printf("%d\n", sizeof(a));
    //16, sizeof(数组名)的时候表示整个数组的大小,单位是字节
    printf("%d\n", sizeof(a + 0));
    //4/8,sizeof(数组名)只有括号内有唯一一个数组名的时候,才表示整个数组,此时代表首元素地址
    printf("%d\n", sizeof(*a));
    //4,*a就是1,a=&a[0],再解引用就抵消了
    printf("%d\n", sizeof(a + 1));
    //4/8,第二个元素的地址大小
    printf("%d\n", sizeof(a[1]));
    //4,第二个元素的大小
    printf("%d\n", sizeof(&a));
    //4/8,&a取出的是数组的地址,但是数组的地址也是个地址,大小为4或者8
    printf("%d\n", sizeof(*&a));
    //16,*和&抵消了
    printf("%d\n", sizeof(&a + 1));
    //4/8,数组的地址加一,跳过整个数组,但还是个地址
    printf("%d\n", sizeof(&a[0]));
    //4/8,首元素的地址
    printf("%d\n", sizeof(&a[0] + 1));
    //4/8,第二个元素的地址 

    return 0;
}

int main()
{
    char arr[] = { 'a','b','c','d','e','f' };

    printf("%d\n", sizeof(arr));
    //6,此时arr是整个数组大小
    printf("%d\n", sizeof(arr + 0));
    //4/8,arr不是单独放在sizeof内,arr + 0是首元素地址
    printf(" % d\n", sizeof(*arr));
    //1,数组首元素
    printf(" % d\n", sizeof(arr[1]));
    //1,第二个元素
    printf(" % d\n", sizeof(&arr));
    //4/8,&arr是整个数组的地址,但是是地址
    printf("%d\n", sizeof(&arr + 1));
    //4/8,&arr+1是跳过整个数组的地址,但是是地址
    printf("%d\n", sizeof(&arr[0] + 1));
    //4/8,第二个元素的地址
    sizeof(arr[0] + 1);
    //'a'+1,会整型提示,结果是整型不是字符了。所以是4

    return 0;
}

#include <string.h>
//strlen是一个求字符串长度的库函数。遇到'\0'停止,长度不包括'\0'

int main()
{
    char arr[] = { 'a','b','c','d','e','f' };

    printf("%dn", strlen(arr));
    //随机值,arr首元素地址,但是只知道首元素地址,前后都是未知的,strlen遇到'\0'才停止
    printf("%d\n", strlen(arr + 0));
    //随机值,同上
    printf("%d\n", strlen(*arr));
    //报错,传给strlen的应该是一个地址,strlen(97),野指针
    printf("%d\n", strlen(arr[1]));
    //strlen(98),同上
    printf("%d\n", strlen(&arr));
    //随机值,&arr是数组的地址,但是还是没有结束标志符,'\0'
    printf("%d\n", strlen(&arr + 1));
    //随机值-6,&arr+1是跳过整个数组的地址,但是还是没有结束标志符,'\0'
    printf("%d\n", strlen(&arr[0] + 1));
    //随机值-1,&arr[0] + 1第二个元素的地址

    return 0;
}

int main()
{
    char arr[] = "a, b, c, d, e, f";//""内会默认带一个'\0',即,[a,b,c,d,e,f,\0];

    printf("%dn", sizeof(arr));
    //7,arr单独在sizeof内代表整个数组,7个元素
    printf("%d\n", sizeof(arr + 0));
    //4/8,arr是首元素是地址+0不变,最终还是地址
    printf("%d\n", sizeof(*arr));
    //1,arr首元素地址,*arr是首元素
    printf("%d\n", sizeof(arr[1]));
    //1,第二个元素
    printf("%d\n", sizeof(&arr));
    //4/8,&arr是整个数组的地址,但还是地址
    printf("%d\n", sizeof(&arr + 1));
    //4/8,&arr+1跳过整个数组之后的地址,但还是地址
    printf("%d\n", sizeof(&arr[0] + 1));
    //4/8,第二个元素的地址,但还是地址

    return 0;
}


int main()
{
    char arr[] = "a, b, c, d, e, f";//""内会默认带一个'\0',即,[a,b,c,d,e,f,\0];
    
    printf("%dn", strlen(arr));
    //6,arr不在单独在sizeof内,也没有&arr,此时arr代表首元素地址,遇到'\0'停止,strlen不计算'\0'
    printf("%d\n", strlen(arr + 0));
    //6,同上
    printf("%d\n", strlen(*arr));
    //报错,传给strlen的应该是一个地址,strlen(97),野指针
    printf("%d\n", strlen(arr[1]));
    //strlen(98),同上
    printf("%d\n", strlen(&arr));
    //6,&arr是数组的地址
    printf("%d\n", strlen(&arr + 1));
    //随机值,&arr+1是跳过整个数组的地址,跳过了'\0'
    printf("%d\n", strlen(&arr[0] + 1));
    //5,&arr[0] + 1第二个元素的地址

    return 0;
}

int main()
{
    char* p = "abcdef";
    /* [a b c d e f \0] 常量字符串,
    实际上是把字符串字面量 "abcdef" 的第一个字符 'a' 的地址赋值给了指针 p。
    之后,就能通过这个指针访问整个字符串。
    由于字符串字面量存储在只读区域,不能通过指针 p 来修改字符串的内容。要
    是尝试修改,会导致未定义行为。若你需要一个可修改的字符串,建议使用字符数组来存储字符串。*/

    printf("%d\n", sizeof(p));
    //4/8,p是个指针变量
    printf("%d\n", sizeof(p + 1));
    //4/8,p是个指针变量
    printf("%d\n", sizeof(*p));
    //1,*p访问的是a
    printf("%d\n", sizeof(p[0]));
    //1,p[0]--->*(p+0)--->*p
    printf("%d\n", sizeof(&p));
    //4/8,p是二级指针变量
    printf("%d\n", sizeof(&p + 1));
    //4/8,p是二级指针变量,+1跳过它,但还是指针变量
    printf("%d\n", sizeof(&p[0] + 1));
    //4/8,&p[0] + 1是b的地址,还是个指针变量


    printf("%d\n", strlen(p));
    //6,p里面放的是a的地址
    printf("%d\n", strlen(p + 1));
    //5,p里面放的是b的地址
    printf("%d\n", strlen(*p));
    //错误,*p是a,传给strlen的应该是个地址
    printf("%d\n", strlen(p[0]));
    //错误,p[0]是a,传给strlen的应该是个地址
    printf("%d\n", strlen(&p));
    //随机,二级指针,指针变量的指针,什么时候遇到\0是不可知的
    printf("%d\n", strlen(&p + 1));
    //随机,二级指针,指针变量的指针+1,什么时候遇到\0是不可知的
    printf("%d\n", strlen(&p[0] + 1));
    //5,&p[0]是a的地址,&p[0] + 1是b的地址
    //p[0]--->*(p+0)
    return 0;
}

int main()
{
    int a[3][4] = { 0 };

    printf("%d\n", sizeof(a));
    //48,a单独在sizeof内,取出出的是整个三行四列二维数组的地址3x4x4
    printf("%d\n", sizeof(a[0][0]));
    //4,第一行第一列的元素
    printf("%d\n", sizeof(a[0]));
    //16,a[0]是第一行这个一维数组的数组名,单独放在sizeof内,代表整个一维数组4x4
    printf("%d\n", sizeof(a[0] + 1));
    //4/8,这里的a[0]不是单独放在sizeof内,所以代表的不是整个一维数组,而是这个一维数组的第一个元素的地址,+1代表a[0][1]的地址,但还是地址
    printf("%d\n", sizeof(*(a[0] + 1)));
    //4,上面说到a[0] + 1是第一行第二个元素的地址,那么*(a[0] + 1))就是第一行第二个元素
    printf("%d\n", sizeof(a + 1));
    //4/8
    //a虽然是二维数组的地址,但是并没有单独放在sizeof内部,也没取地址
    //a表示首元素的地址,二维数组的首元素是它的第一行,a就是第一行的地址
    //a+1就是跳过第一行,表示第二行的地址,是地址
    printf("%d\n", sizeof(*(a + 1)));
    //16,上面说到a + 1是第二行的地址,那么*(a + 1)就是第二行数组的元素
    printf("%d\n", sizeof(&a[0] + 1));
    //4/8,&a[0]对第一行的数组名取地址,拿出的是第一行的地址,&a[0] + 1是第二行的地址,是地址
    printf("%d\n", sizeof(*(&a[0] + 1)));
    //16,上面说到&a[0] + 1是第二行的地址,那么*(&a[0] + 1)就是第二行的元素
    printf("%d\n", sizeof(*a));
    //16,a表示首元素的地址,二维数组首元素的地址,为第一行一维数组的地址,*a是对第一行地址的解引用,拿到的就是第一行
    printf("%d\n", sizeof(a[3]));
    //16,第四行一维数组名,代表第四行整个数组,虽然不存在,但是
    /*
    sizeof 是一个编译时运算符,它的作用是计算数据类型或者变量所占用的内存字节数。
    在编译阶段,编译器就会根据数据类型来确定 sizeof 表达式的值,而不需要在运行时执行具体的计算。
    当 sizeof 运算符作用于 a[3] 时,编译器并不会去检查 a[3] 是否越界。
    编译器仅仅根据 a 的定义,知道 a[i] 是一个包含 4 个 int 类型元素的一维数组
    所以 sizeof(a[3]) 就等同于计算一个包含 4 个 int 类型元素的一维数组所占用的内存字节数。
    */

    return 0;
}

总结:
strlen是求字符串长度的,关注的是字符串中的\0,计算的是\0之前出现的字符的个数
strlen是库函数,只针对字符串
sizeof只关注占用内存空间的大小,不在乎内存中放的是什么
sizeof是操作符
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
 & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外所有的数组名都表示首元素的地址。

10. 指针笔试题

int main()
{
    int a[5] = { 1,2,3,4,5 };
    int* ptr = (int*)(&a + 1);//如果不强制转化类型的话,它的类型应该是数组指针int(*)[5]类型
    //&a,取出的是整个数组的地址,+1跳过整个数组
    printf("%d,%d", *(a + 1), * (ptr - 1));//2,5
    //a是首元素地址,+1代表第二个元素地址,再解引用就是元素2
    //ptr的类型是int*,-1后跳过一个整型, 再解引用* (ptr - 1)代表着a[4],第五个元素5
    return 0;
}


struct Test
{
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}}*p = (struct Test*)0x100000;//把这个数字值强制转换为(struct Test*)类型当成一个地址
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
//x86
int main()
{
    printf("%p\n", p + 0x1);
    //结构体指针加一,应该跳过一个结构体
    //0x100000+1--->0x100000+20(十进制)=0x100014
    printf("%p\n"(unsigned long)p + 0x1);
    //0x100014转换为十进制是1,048,576,+1,整数加一就是直接加一,最后为0x100001
    printf("%p\n", (unsigned int*)p + 0x1);
    //unsigned int*四个字节,unsigned int*的p+1应该是跳过一个unsigned int*,即加4,最后得0x100004
    return 0;
}

int main()
{
    int a[4] = { 1,2,3,4 };
    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);
    printf("%x,%x", ptr1[-1], * ptr2);
    //ptr1[-1]--->*(ptr1-1),&a,取出整个数组的地址,再加一跳过整个数组,指向元素4后面的一个地址,再减一,跳过4个字节,指向元素4
    //(int)a + 1,代表数组a的首元素地址转换为整型了再加一,再转换为指针,即指向元素1的第二个字节00  
    //当前环境数据存储模式是小端
    //低地址 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  高地址
    //*ptr2,整型指针解引用,应该向后访问一个整型4个字节,00 00 00 02
    //拿出来就是02 00 00 00
    return 0;
}

int main()
{
    int a[3][2] = { (0,1),(2,3),(4,5) };//逗号表达式,等价于{ 1,3,5 };
    //初始化过程中会把1 赋值给 a[0][0],3 赋值给 a[0][1],5 赋值给 a[1][0],而剩余的数组元素(即 a[1][1]、a[2][0] 和 a[2][1])会被自动初始化为 0。
    int* p; 
    p = a[0];//a[0]第一行的数组名,没有取地址,也没有单独放在sizeof内,退化为指针,首元素的地址,区别于&a[0]不要搞混。
    printf("%d", p[0]);//*(p+0)=1
    return 0;
}
//ps:如果想把{ 0, 1 }、{ 2, 3 } 和{ 4, 5 } 分别赋值给二维数组的三行,应该使用花括号来初始化,int a[3][2] = { {0, 1}, {2, 3}, {4, 5} };

int main()
{
    int a[5][5];
    int(*p)[4];
    //p 是一个指向包含 4 个 int 类型元素的一维数组的指针。
    p = a;
    //将二维数组 a 的首地址赋值给指针 p。二维数组的首地址是第一行整个一维数组的地址
    //所以 a 为指向其第一行(包含 5 个 int 元素的一维数组)的指针,然后赋值给 p。
    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    //两个指向同一数组或同一连续内存块的指针相减,结果是它们之间的元素个数。(差值的绝对值),差值的正负表示相对位置。
    // &p[4][2] - &a[4][2] 计算的是这两个指针之间相差的 int 元素个数。
    //&p[4][2]:p每次移动会跳过4个int类型的元素(因为p是int(*)[4]类型),p[4]表示p向后移动4次,每次移动4个int元素,然后p[4][2]表示在移动后的位置再偏移2个int元素。
    //即*(*(p+4)+2),p+4会跳过4*4个元素,再偏移2,得到a[3][2]的元素
    //&a[4][2]是二维数组 a 中第 5 行(索引从 0 开始)第 3 列元素的地址。
    //a[3][2]与a[4][2]之间相差了4个元素,-4

    //%p 是用于输出指针地址的格式说明符,但这里将一个 int 类型的差值 - 4 以指针地址的形式输出
    //在大多数系统中,指针地址是无符号整数,所以 - 4 会被解释为一个很大的无符号整数。
/*
    -4
    10000000 00000000 00000000 00000100 原码
    11111111 11111111 11111111 11111011 反码
    11111111 11111111 11111111 11111100 补码
    无符号整型打印
    11111111 11111111 11111111 11111100 原反补码
    十进制为:4, 294, 967, 292
    十六进制为:FFFF FFFC
 */

    return 0;
}
/*  
p+1表示每次移动4个整型
p  0 0 0 0 0
   0 0 0 0 0
   0 0 0 0 0
   0 0 0 0 0
   0 0 0 0 0
   */

int main()
{
    int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
    int* ptr1 = (int*)(&aa + 1);
    // &aa,&数组名,代表整个数组的地址,类型为int (*)[2][5],+1,跳过整个数组,现在指向了元素10,后面一个元素的地址,并强制转换为int*类型
    int* ptr2 = (int*)(*(aa + 1));
    // aa,数组名表示数组首元素地址,二维数组的首元素地址为第一行的整个一维数组的地址,类型为int (*)[5],+1,跳过一行,现在指向了元素6的地址
    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    //ptr1为int*类型,-1,向后跳过一个整型,现在指向的为元素10的地址,解引用就是10
    //ptr2为int*类型,-1,向后跳过一个整型,现在指向的为元素5的地址,解引用就是5
    return 0;
}
/*
1 2 3 4 5
6 7 8 9 10
*/

int main()
{
    char* a[] = { "work","at","alibaba" };
    //定义了一个字符指针数组 a。数组 a 中的每个元素都是一个 char* 类型的指针,这些指针分别指向不同的字符串字面量。
    //"work"、"at" 和 "alibaba" 都是字符串字面量,它们存储在内存的只读区域。
    //数组 a 的元素 a[0]、a[1] 和 a[2] 分别指向这些字符串字面量的首字符地址。
/*
1. 定义字符串字面量并创建字符指针
    char* ptr1 = "work";
    char* ptr2 = "at";
    char* ptr3 = "alibaba";

2. 定义字符指针数组
    char* a[3];

3. 初始化数组元素
    a[0] = ptr1;
    a[1] = ptr2;
    a[2] = ptr3;
*/
    char** pa = a;
    //创建一个二级字符指针变量pa,存放字符指针数组a的首元素地址,a的首元素为a[0]。
/*
+-------------------+          +----------------+
| 只读数据 a        |         | 栈区           |
+-------------------+          +----------------+
| "work"            |          | a[0]  -------> | "work"
| "at"              |          | a[1]  -------> | "at"
| "alibaba"         |          | a[2]  -------> | "alibaba"
+-------------------+          +----------------+
                               | pa   ------->  | a[0]
                               +----------------+

a[0]、a[1]、a[2] 分别是 char* 类型的指针,指向不同的字符串字面量。
pa 是 char** 类型的指针,它指向 a[0],也就是指向存储 "work" 这个字符串首地址的位置。
*/
    pa++;//通过pa移动到下一个字符串
    printf("%s\n", *pa);//at
    return 0;
}

/*
                  a
char* *pa -->   char* -->"work"
                char* -->"at"
                char* -->"alibaba"

int * p; p+1跳过一个整型
char* *pa; pa+1跳过一个char*类型
*/

int main()
{
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    char** cp[] = { c + 3,c + 2,c + 1,c };
    char*** cpp = cp;
    printf("%s\n", **++cpp);
    printf("%s\n", *-- * ++cpp + 3);
    printf("%s\n", *cpp[-2] + 3); 
    printf("%s\n", cpp[-1][-1] + 1);
    return 0;
}

cp              c
char** -- >   char* -- > "ENTER"
    +1        char* -- > "NEW"
    +2        char* -- > "POINT"
    +3          char* -- > "FIRST"

cpp            cp
char*** -- >  char** -- > c + 3(指向数组c中的"FIRST")
    +1        char** -- > c + 2(指向数组c中的"POINT")
    +2        char** -- > c + 1(指向数组c中的"NEW")
    +3          char** -- > c    (指向数组c中的"ENTER")

** ++cpp:
++cpp现在cpp指向cp+1,解引用找到c + 2,c + 2又是"POINT"的地址,再解引用得到"POINT"这块空间的内容,即P的地址,%s打印得到"POINT"

* --* ++cpp + 3:
++cpp,经过上一次的++后++,现在cpp指向cp + 2,解引用找到 c + 1,再--得到c,再解引用得到E的地址,+3,指向S的地址,%s打印得到"ER"

* cpp[-2] + 3:
*cpp[-2]-->**(cpp-2),经过上面两次++后现在再-2,抵消了,cpp又重新指向cp,解引用找到c + 3,再解引用得到"FIRST"中F的地址,+3指向S的地址,%s打印得到"ST"

cpp[-1][-1] + 1:
cpp[-1][-1]-->*(*(cpp-1)-1),上一轮打印运算并没有改变cpp的值,cpp还是指向cp+2,cpp - 1,现在cpp指向cp+1,解引用找到c + 2再-1得到c + 1,
再解引用得到"NEW"中N的地址,+1指向ED的地址,%s打印得到"EW"

整理一下:

char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;

c 是一个字符指针数组,数组中的每个元素分别指向 "ENTER"、"NEW"、"POINT"、"FIRST" 这几个字符串字面量。
cp 是一个二级字符指针数组,cp[0] 指向 c + 3(即指向 "FIRST" 的指针),cp[1] 指向 c + 2(即指向 "POINT" 的指针),
cp[2] 指向 c + 1(即指向 "NEW" 的指针),cp[3] 指向 c(即指向 "ENTER" 的指针)。
cpp 是一个三级字符指针,初始时指向 cp 的首地址。

printf("%s\n", **++cpp);

++cpp:cpp 先自增,此时 cpp 指向 cp + 1。
* ++cpp:对 cpp 解引用,得到 cp[1],也就是 c + 2,它指向 "POINT" 的指针。
* *++cpp:再对 * ++cpp 解引用,得到 "POINT" 字符串的首地址,使用 % s 格式输出 "POINT"。

printf("%s\n", *--* ++cpp + 3);

++cpp:cpp 再次自增,此时 cpp 指向 cp + 2。
* ++cpp:对 cpp 解引用,得到 cp[2],即 c + 1,它指向 "NEW" 的指针。
-- * ++cpp: * ++cpp 自减,得到 c,它指向 "ENTER" 的指针。
* -- * ++cpp:对-- * ++cpp 解引用,得到 "ENTER" 字符串的首地址。
* -- * ++cpp + 3:将 "ENTER" 字符串的首地址向后偏移 3 个字符,指向 'E'("ENTER" 中的第二个 'E'),使用 % s 格式输出 "ER"。

printf("%s\n", *cpp[-2] + 3);

cpp[-2] 等价于* (cpp - 2):cpp 减去 2 后指向 cp,对其解引用得到 cp[0],即 c + 3,它指向 "FIRST" 的指针。
* cpp[-2]:对 cpp[-2] 解引用,得到 "FIRST" 字符串的首地址。
* cpp[-2] + 3:将 "FIRST" 字符串的首地址向后偏移 3 个字符,指向 'S',使用 % s 格式输出 "ST"。

printf("%s\n", cpp[-1][-1] + 1);

cpp[-1][-1] 等价于* (*(cpp - 1) - 1)
cpp - 1:cpp 减去 1 后指向 cp + 1。
* (cpp - 1):对 cpp - 1 解引用,得到 cp[1],即 c + 2,它指向 "POINT" 的指针。
* (cpp - 1) - 1: * (cpp - 1) 减去 1 后得到 c + 1,它指向 "NEW" 的指针。
* (*(cpp - 1) - 1):对 * (cpp - 1) - 1 解引用,得到 "NEW" 字符串的首地址。
cpp[-1][-1] + 1:将 "NEW" 字符串的首地址向后偏移 1 个字符,指向 'E',使用 % s 格式输出 "EW"。

指针完结!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值