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) {
printk("%s%R%s", "This is a testcase: ", 2023, 2023, "\n");
printk("the range is %R, size = %d\n", 1, 9, 9 - 1);
}
//This is a testcase: (2023,2023)
//the range is (1,9), size = 8
  • 解题流程:仔细看下来其实就是要在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不需要考虑是否会导致下次再调用时没法接上,先取一下当前datastrlen,再从下一位(实际上是上一次写进去的\0)开始继续拼就可以了,最后又会是一个\0结束 附一种可能可行的实现方法:

void outputbuf(void *data, const char *buf, size_t len) {
char *temp = (char *)data + strlen(data);
for (int i = 0; i < len; i++) {
*temp = buf[i];
temp++;
}
*temp = '\0;'
}

而我课上的实现方法严格意义上不符合要求,因为传入的参数形式不同,但是给了警告也能照样跑(

void outputbuf(void **data, const char *buf, size_t len) {
for (int i = 0; i < len; i++) {
**(char **)data = buf[i];
(*data) = (*data) + 1;
}
**(char **)data = '\0';
}
// 主要不同体现在没有充当临时变量的char *temp,全程都是void **data取一层值后*data这个char *的自由移动,效果类似,不使用strlen也许效率会高一点?

// 由于outputbuf传参不同,所以这里给vprintfmt传参也需要发生改变,所以说这样的做法不太合适,相当于改变了函数结构
int sprintf(char *buf, const char *fmt, ...) {
char *place = buf;
va_list ap;
va_start(ap, fmt);
vprintfmt(outputbuf, &buf, fmt, ap);
va_end(ap);

return (int)(buf - place - 1);
}

回调函数

好了,课上的部分大概就是这些了,现在我们回来谈谈这个回调函数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) image-20230320232412341 实际上,在C语言库中还有很多这样的回调函数,其中我们用的相当多的一个就是qsort中的第四个参数:cmp。每次我们调用qsort,都需要事先写好cmp函数,并且传入qsort中,让其按照我们程序员事先设定好的规则进行排序。 写到这里我也才注意到,当时程设 + DS绕的一圈一圈的cmp参数格式,实际上正是C库的高拓展性所在:

int cmp(const void* e1, const void* e2);

首先为了符合回调函数要求,无论编写怎样的cmp函数都要保证参数类型一致;而为了能够处理更多种类的数据(甚至结构体),形参被设置成了const void*,这样不管比较的是什么结构,传入形参后强制类型转换把引用解开就可以进一步处理了。所以虽然程序员用起来有点绕,但对C库来说,这样写绝对是件好事。 在编程过程中使用回调机制,可以更好地分离代码,使得应用层和驱动层尽可能分离,降低总代码的耦合性。 受教了。