BUAA-OS-2023-Lab1-Exam
lab1-Exam
课下一时爽,课上
debug
火葬场
课下出锅力!
问题出在%ld/%d
时负数的判定上(也就是判定neg_flag
上):我的判定方法是,if (num < 0)
则neg_flag = 1; num = -num;
,这样对ld
来说还说得过去,但是对于int
类型就出问题了,long
类型的num
变量装了一个int
的值,然后取相反数的时候却按着long
类型跑的,然后补码尊贵的符号位就登场搞事情了(寄),还好课上一个多小时修出来了,要不然计组P7
历史重演力!
lab1-Exam
题目要求:修改我们课下编写的
vprintfmt
函数,使其能够支持新格式符(format specifiers
) range :R
,同时要求格式符R
也能使用与其他格式符同等的副格式符:即<flag>
、<width>
、<length>
三个副格式符。 格式符Range
的作用为:使用函数中接下来的两个整数参数a, b
,输出(<a>,<b>)
字符串。这两个参数共用一组副格式符,受到的修饰相同。
- 样例:对于下面的函数
range_1_check
,应该有其之后的输出:
void range_1_check(void) { |
- 解题流程:仔细看下来其实就是要在
vprintfmt()
函数里增加一种格式符的判断:case 'R':
,作用是输出三个字符和两个整数(%c%d%c%d%c
,当然这个%d
是可能有副格式符的);只需要判断一下输出的整数是int
还是long
就解决了,(然后课下出锅寄了一个多小时) - BUG与坑点:课下对了就是对了,剩下的都可以抄
%c
和%d
那里的
lab1-Extra
题目要求:实现一个类似
fprintf
的函数int sprintf(char *buf, const char *fmt, ...);
,将fmt
字符串格式化输出到buf
所在的内存空间中,且返回值应是buf
中存放字符串的长度。 提示:可以调用vprintfmt
函数进行协助输出
- 样例:就不写了,可以拿个
fprintf
逝逝,效果一样的 - 这个考题完美回答了对于
vprintfmt
函数中不起作用的void *data
形参和看起来有点复杂的函数形参fmt_callback_t out
的问题:- 函数中的
data
可以承载输出的目的地指针,在printk
中我们输出至控制台,所以data
的取值并不重要(最终以调用了printcharc
函数作为具体输出);但在这里实现的sprintf
函数含有一个目的地buf
,所以我们需要把buf
传入,负责接收所有需要输出的字符(写入buf
指向的内存) - 函数中的
out
被称为回调函数参数,不过这个概念在这里并不重要,我们在解完题之后再来回顾。
- 函数中的
- 解题流程:实现思路与
printk
类似,顶层和中间层的调用都相同,不同的是最底层的输出方式:-
printk
使用printcharc
写入控制台 -
sprintf
需要写入buf
指向的内存 - (这是不是刚说过一遍)所以顶层函数照抄,我们重点实现这个替代原有
outputk
的新函数outputbuf
(忘了啥是outputk
的快回去看lab1
讲解那篇文!)
-
-
outputbuf
的实现:void outputbuf(void *data, const char *buf, size_t len);
如果能把vprintfmt
整明白也挺容易看的,本函数应输出的字符串序列已经被安放在buf
内,需要输出的长度为len
,目的地为data
;需要做的就是一个循环把字符搬运过去(记得末尾加一个\0
) 这里的\0
不需要考虑是否会导致下次再调用时没法接上,先取一下当前data
的strlen
,再从下一位(实际上是上一次写进去的\0
)开始继续拼就可以了,最后又会是一个\0
结束 附一种可能可行的实现方法:
void outputbuf(void *data, const char *buf, size_t len) { |
而我课上的实现方法严格意义上不符合要求,因为传入的参数形式不同,但是给了警告也能照样跑(
void outputbuf(void **data, const char *buf, size_t len) { |
回调函数
好了,课上的部分大概就是这些了,现在我们回来谈谈这个回调函数fmt_callback_t
到底是个买
回调函数,是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,并且被传参的函数使用了这个函数指针,我们就认为这是一个回调函数。 回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方(被传参函数)调用的,用于对某事件或条件进行响应。
说白了,回调函数就是一个只能被他人调用的函数,而回调函数参数就是某个函数中已经固定好返回值、自身形参的参数(类型是函数指针) 那我们还拿vprintfmt
举例:
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap); |
这里fmt_callback_t out
,就是一个回调参数,实际类型是一个函数指针,接收者vprintfmt
在自身内部对out
函数进行调用,用于处理某些事件(这个例子中就是输出处理好的字符串),这样的处理虽然看起来有些繁琐,但是实际上给了vprintfmt
函数非常强的可拓展性,输出函数的接口化,意味着它可以通过传入不同的输出函数,完成更复杂情况下、更多种类的输出任务(就比如lab1-extra
这个输出到内存)。 如果只考虑输出控制台,实际上完全可以不传入out
参数,直接使用printcharc
函数完成任务。但强拓展性可不是一个简简单单的printcharc
能受的住的() 顺便提一嘴最好让同一个回调参数对应的函数类型都一致,否则可不一定能保证功能正确(反正编译器会先给你个难看的警告.jpg) 实际上,在C语言库中还有很多这样的回调函数,其中我们用的相当多的一个就是qsort
中的第四个参数:cmp
。每次我们调用qsort
,都需要事先写好cmp
函数,并且传入qsort
中,让其按照我们程序员事先设定好的规则进行排序。 写到这里我也才注意到,当时程设 + DS绕的一圈一圈的cmp
参数格式,实际上正是C库的高拓展性所在:
int cmp(const void* e1, const void* e2); |
首先为了符合回调函数要求,无论编写怎样的cmp
函数都要保证参数类型一致;而为了能够处理更多种类的数据(甚至结构体),形参被设置成了const void*
,这样不管比较的是什么结构,传入形参后强制类型转换把引用解开就可以进一步处理了。所以虽然程序员用起来有点绕,但对C库来说,这样写绝对是件好事。 在编程过程中使用回调机制,可以更好地分离代码,使得应用层和驱动层尽可能分离,降低总代码的耦合性。 受教了。