这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战
指针进阶续
续前文《C语言进阶:指针进阶》
回调函数
回调函数定义
回调函数:通过函数指针调用的函数,或者说使用函数指针调用函数这样的机制被称为回调函数。回调函数不由实现方直接调用,而是作为特殊条件下的响应。
概念无关紧要,理解并熟练运用这种方法才更为重要。
快速排序 qsort
qsort
函数逻辑
1 | c复制代码void qsort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2)); |
qsort
无返回值,有四个参数。分别为base
:起始地址,num
:元素个数,width
:元素大小以及compare
:比较函数。可与冒泡排序作对比。
1 | c复制代码//冒泡排序 |
与冒泡排序作对比发现,冒泡排序仅需起始地址和元素个数即可,暗含了其他信息。由于过度具体化,冒泡排序只能排序整型数组,且比较函数过于简单无需单独列出。
因为qsort
排序可适用于多种类型如浮点型,字符型,自定义类型的数据,故无法规定具体类型,所以需要多个参数去描述元素的基本信息。
qsort
之所以能够适应多种数据,是因为参数void* base
再搭配上num
和width
就描述出任意一种类型。
为什么将参数
base
的类型定义为void*
呢?如下述代码所示。
1 | c复制代码char* p1 = &a; |
确定类型的地址之间直接赋值会提示类型不兼容,强制转化也可能会导致精度丢失。
故使用无(具体)类型void*
,又称通用类型,即可以接收任意类型的指针,但是无法进行指针运算(解引用,±±±整数等)。
1 | c复制代码p1++; *p1; p1 - p2; p1 > p2;//表达式必须是指向完整对象类型的指针 |
base
:用于存入数据的起始地址。类型定义为void*
,可接受任意类型的指针。num
:待排序的元素个数。width
:元素宽度,所占字节大小。
明确了排序的起始位置,元素个数和元素大小,貌似已经够了。但是并无法排序所有类型,因此必须自定义一个抽象的比较函数指定元素的比较方式。
cmp
:比较函数,用于指定元素的比较方式。
* `elem1`小于`elem2`,返回值小于0
* `elem1`大于`elem2`,返回值大于0
* `elem1`等于`elem2`,返回值为0
elem1
,elem2
:进行比较的两个元素的地址作参数。
qsort
可以说是一个半库函数半自定义函数。自定义在于其函数最后一个参数为比较函数,该函数内部实现自由,但返回值必须按照规定返回相应的数值。
小结
需要qsort
函数排序各种类型的数据,
- 故
base
起始地址不可为固定的指针类型,只能用void*
。 - 既然是通用类型还要明确比较元素的个数和大小。
- 最后,排序最核心的比较大小,为适应不同的类型元素必须自定义专门的比较函数。
qsort
实现冒泡排序
1 | c复制代码//比较函数:整型 |
比较函数int_com
不需要传参,作为回调函数由qsort
直接调用。比较函数的传参过程由qsort
内部实现。
qsort
实现结构体排序
1 | c复制代码#include <stdlib.h> |
由此可得,提取出一个比较函数,具体交换的方式由qsort
内部实现。
模拟实现qsort
用
qsort
的函数逻辑,实现冒泡排序。
1 | c复制代码//打印函数 |
地址统一强转为char*
,以最小字节单位一个字节进行比较和交换,使代码更具有普适性。
如果需要排序结构体则只需要在前文代码中主函数里替换my_qsort
且把比较函数替换Name_cmp
即可。
1 | c复制代码//1. |
指针和数组笔试题解析
数组辨析题
注意点。数组名代表整个数组:
sizeof(数组名)
&数组名
除此以外,数组名都是代表首元素地址。
一维数组
1 | c复制代码int a[] = { 1,2,3,4 }; |
- 只有数组名单独放在
sizeof
内部才是整个数组。
a+0
放在sizeof
内部表示首元素地址+0。
2. 只要是地址,不管是什么类型的地址大小都是4/8
基本类型指针,数组指针,函数指针大小都是4/8个字节,故sizeof(&a)=sizeof(int(*)[4])=4
。sizeof()
求指针所占字节而不是解引用访问权限大小。
3. *
和&
在一起会抵消。
sizeof(*&a)
,&a为整个数组的地址类型int(*)[4]
,解引用后int[4]
大小为16。
字符数组
1 | c复制代码char arr[] = { 'a','b','c','d','e','f' }; |
sizeof(*arr)
,*arr
对首元素地址解引用,计算首元素所占空间大小。
strlen(*arr)
,*arr
依然是首元素,strlen
把a也就是97当成地址,访问到非法内存所以报错。
2.strlen(&arr)
虽然是整个数组的地址,但依然是从首元素开始的,所以strlen
依然从第一个元素开始找。
strlen(&arr+1)
,先计算&arr+1
然后再传参过去,也就是跳过了整个数组去找。
sizeof
和strlen
的区别
sizeof
— 操作符 — 以字节为单位,求变量或类型所创建变量的所占空间的大小
sizoef
不是函数,计算类型是必须带上类型说明符()
。sizoef
内容不参与运算,在编译期间便转化完成。
strlen
— 库函数 — 求字符串长度即字符个数,遇\0
停止。
库函数,计算字符串长度没有遇到
\0
就会一直持续下去。返回类型size_t
,参数char* str
,接收的内容都会认为是char*
类型的地址。
一个求变量所占空间,一个求字符串大小,二者本身是没有关系的,但总有人把二者绑在一起“混淆视听”。
字符串数组
首先明确二者的区别:
1 | c复制代码//1.字符初始化数组 |
字符初始化数组,存了什么元素数组里就是什么元素。而字符串初始化数组,除了字符串中可见的字符外,还有字符串末尾隐含的
\0
。\0
存在于字符串的末尾,是自带的,虽不算字符串内容,但是字符串中的字符。
1 | c复制代码char arr[] = "abcdef"; |
sizeof
计算变量的长度,变量可以是数组,数组元素以及指针。数组就是整个数组的大小,数组元素则是数组元素的大小,指针大小都为4/8。strlen
把传过来的参数都当作地址,是地址就从该地址处向后遍历找\0
,不是地址当作地址非法访问就报错。
常量字符串
1 | c复制代码char* p = "abcdef"; |
"abcdef"
是常量字符串,用一个字符指针p
指向该字符串,实质是p
存入了首字符a
的地址。由于字符串在内存中连续存放,依此特性便可以遍历访问整个字符串。
1 | c复制代码char* p = "abcdef"; |
p
,p+1
,&p
,&p+1
,&p[0]+1
都是地址对于地址sizeof
都求得4/8,*p
,p[0]
是数组元素,sizeof
计算元素大小。p
,p+1
,&p
,&p+1
,&p[0]+1
都是地址对于地址strlen
都向后遍历访问找\0
,*p
,p[0]
是数组元素其对于ASCII值当作地址会访问到非法内存。
p
,p+1
,&p[0]+1
都是字符串字符的地址,&p
,&p+1
都是指针变量p
或其之后的地址。
二维数组
访问数组元素的方式是数组名+[j]
。若将二维数组的每一行可以看成一个一维数组,则a[0],a[1],a[2]
可以看成“每行“的数组名,和一维数组的数组名具有同样的效果。
- 数组名单独放在
sizeof()
内部代表整个数组&
数组名同样代表整个数组(每行的数组名同样适用)
1 | c复制代码int a[3][4] = { 0 }; |
- 对于二维数组来说,
sizeof(a[0])
求首行的整个数组大小。若是sizeof(a[0]+1)
代表首行数组名没有单独放在sizeof()
内部,故a[0]
退化成了首元素地址。 sizeof(a+1)
代表第二行的地址仅为地址,但并不能第二行该“一维数组”的数组名,不可与sizeof(a[1])
混淆。&a[1]
等价于a+1
。sizeof(*(a+1))
,对第二行的地址解引用,相当于sizeof(int[4])
。*(&a[0]+1)
第二行数组地址解引用为数组名。数组地址解引用代表整个数组,相当于数组名。切莫将数组地址和数组名混淆。(*&arr=arr
)
总结
搞清楚二维数组数组名的意义,必须搞清楚如下变量的含义。
1 | c复制代码a[0]//首行数组名 |
a
是二维数组名,a[0]
是首行数组名。- 参与运算后
a
退化为首行地址,a[0]
退化为首元素地址。 &a+1
跳过一个二维数组,&a[0]+1
跳过一个一维数组。
指针笔试题
Example 1
1 | c复制代码int main() |
指针运算要考虑指针类型,&a+1
跳过了int[4]
的长度,得到这个位置的地址后指针转化成int*
型,此时再+1就只能跳过一个int
。
本题考察指针类型决定指针±整数的长度。
Example 2
1 | c复制代码//由于还没学习结构体,这里告知结构体的大小是20个字节 |
p本是struct Test*
的指针,后分别强制转换成unsigned long
和unsigned int*
类型分别+1跳过多少字节。struct Test*
的指针+1跳过一个struct Test
字节长度。unsigned long
为整数类型+1即整数+1,不属于指针运算。
Example 3
1 | c复制代码int main() |
ptr1
和ptr2
都是把不同的意义的变量强转成int*
类型的地址。先进行一系列的操作后再读取该地址处的后4个字节。
&a
类型为int(*)[4]
故+1跳过1个数组;a
首先为首元素地址强转为int
型整数再+1执行整数加法,由于内存以字节为单位,一个字节一个地址,故+1相当于下一个字节的地址。最后都强制转换为int*
的指针,都向后访问4个字节。
由于系统为小端存储方案,也就按小端的方式读取数据。以%x的形式打印故不需要我们再去转换成十进制,答案分别为2000000,4。
Example 4
1 | c复制代码#include <stdio.h> |
这题相对来说很简单,需注意到()
内部为逗号表达式,所以数组元素分别为1,3,5,0,0,0 。
Example 5
1 | c复制代码int main() |
本题一眼就可以看到二维数组a[5][5]
,本应用int(*)[5]
的数组指针接收,为什么用4个元素的数组指针接收呢?
其实可以看出,数组在内存中都是连续存放的,对于这“一排“的数据,怎么看是我们的事,把它当成3列的4列的5列甚至是10列的都可以。所以数组指针大小仅仅决定一次访问几个元素,或是说决定了所指数组的列数。
本质上列数的改变并不会影响该二维数组,仅仅影响的是编译器如何看待该数组。
可以看出指针ptr1-ptr2为-4,故%d打印为-4,若以%p打印,因内存中存储的是-4的补码,再以无符号数的十六进制形式打印:
1 | 复制代码10000000 00000000 00000000 00000100 |
Example 6
1 | c复制代码int main() |
&aa取出数组地址并+1跳过整个数组,aa相当于首行地址+1为第二行地址并解引用得第二行数组名,数组地址解引用得数组名。
aa+1得到第二行数组的数组地址,再解引用即对数组地址解引用,得到整个数组也就是数组名,
*(&arr)=arr
。
Example 7
1 | c复制代码#include <stdio.h> |
指针数组a分别存入"work"
,"at"
,"alibaba"
三个字符串的首字符地址。又将指针数组名即首元素地址存入二级指针变量中。指针++访问第二个元素a的地,随后%s打印整个字符串。
Example 8
1 | c复制代码int main() |
首先字符指针数组
c
存有字符串首地址,其次指针数组cp
存有”与指针c相关“的二级指针,最后三级指针cpp
指向二级指针cp
。
本题是最有难度的一题,需要注意到的是指针++--
属于自增自减,会影响到本值。
cpp
+1指向了数组cp
的第二个元素,并解引用得到c+2
。再解引用得到"POINT"
的首地址。
cpp
+1指向数组cp
的第三个元素并解引用得到c+1
,(c+1)--
后将数组cp
的第三个元素修改为c
,解引用访问数组c
的首元素即"ENTER"
的首地址再+3,打印出ER
。
cp[-2]=*(cp-2)
即cpp
前移2个元素指向并访问了c+3
,并解引用得数组c
的第4个元素也就得到了"FIRST"
的首地址+3,访问到ST
并打印。
cpp-1
不是cpp--
,虽然效果一样,但是对cpp
的意义不同。(cpp-2)
并没有改变cpp
,所以cpp
仍指向cp
的第三个元素。
cpp[-1][-1]
就相当于*(*(cpp-1)-1)
即cpp
-1解引用访问到了c+2
-1再解引用访问到了数组c
的第二个元素再+1,打印出EW
。
研究清楚之后再回头看代码其实非常简单,首先了解cpp
和cp
和c
的关系:都是指针并从左向右一次指向,再看有关的操作。
1 | c复制代码**++cpp; |
这四行代码其实本质上完全相同,都是1.cpp
±整数并解引用;2.cp
元素±整数并解引用;3.c
元素±整数并解引用。如图所示:
本文转载自: 掘金