指针的进阶
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"。
指针完结!