
猪脚说第十三期
大作业相关问题
常见问题 & 基本注意事项
- 文件的读入:推荐大家使用以下方式读取hashvalue.txt的内容:
| 1 | char Hash[10001][130]; | 
如果大家在本地运行时样例输出完全正确(包括键盘输出和result.txt的输出),而提交小数据点时输出错误,不妨把读入article与sample的模式也改为"rb",如果发现输出结果出现变化,大概率是因为你对于\r\n的处理不够仔细(大多是因为你对换行符进行了形如if (c == '\n')的特判)。
一种建议的修改方式即为将读入模式改为"rb",这样就可以原封不动地得到txt文件中的所有数据。此时,你就可以将\r与\n看作独立的两个字符,分别进行特判即可。
- 换页符只出现在文章与文章之间,最后一篇文章是没有\f的,有些同学把“读到换页符”这一条件作为读完一篇文章的标志,那么就需要检查最后一篇文章有没有被读入;
- 换页符\f紧跟着的那一行不一定就是下一篇文章的标识号,有可能隔着很多空行,因此不能在读到\f后直接执行fgets()并把读到的内容作为文章标识号。以下代码作为正确读取文章标题的参考:
| 1 | if (c == '\f') { | 
- 题目中没有任何一句话表明文章标识号的形式一定是XX-XXXX的数字形式,事实上任意字符串都可以是文章的标识号(包括空格,因此也请不要使用fscanf()等读不进来空格的函数),因此不要自作主张在输出时直接写成printf("1-%d", count);的形式;
- 用fgetc()读入字符时,请一定要用int型的变量存储:
| 1 | int c; | 
- 数组的大小要仔细考量,保证读入的数据不会越界。相关数据范围参考课程群中给出的消息;
- 检查数组存储下标的一致性。
优化
优化 I/O
- printf、- scanf改成- getchar、- putchar快读快写;
- 先使用fread将文件内容整个读进内存,再对内存进行后续操作;
- 尽可能减少重复读入,比如不要连续读多遍文件。
减少遍历次数
- 不要写出如下形式的代码
| 1 | while (c = fgetc(fp)) { | 
因为strlen与memset的原理就是对整个数组进行for循环,再把这样的函数放入循环里,时间复杂度一下就上去了。当然论效率memset还是相对较高的,在必要的初始化、“新的开始”之处,不要因为缺省了一些操作而导致错误。
压榨性能
- 存储结构 - 顺序存储(数组)?链式存储(指针)?
- 一般情况下,访问数组的效率明显高于通过指针的间接访问
 
- 查找方式 - 二叉查找树?bsearch函数?trie树?
- 如果使用trie树,请思考在建立树时是否一定需要把单词一起存储进去(浪费大量时间)?
 
- 二叉查找树?
- 将逻辑简单的函数改写为宏,可以减少函数调用的时间,例如 - isalpha、- tolower等;
- 尽量少调用 C 库函数,一般建议只需使用 - stdio.h与- stdlib.h即可;
- 合理封装功能模块,例如把“获取一个单词”写成函数,它会被调用非常多次,则开销巨大;把“读取 article 并完成词频统计”封装成一个函数,在逻辑上较为清晰,且开销较小(当然只有一个 - main理论上最高效,但不建议写成一坨);
- 频繁访问的变量可以声明为 - register,“建议”编译器将这个变量存储在寄存器中,提供更高速的访问;- 注意,这种类型的变量只能是局部变量,并且通常用于循环变量,如在 - main的开头写- register int i = 0;或者- for循环的括号中写- for (register int j = 0; j < n; j++),切勿滥用!
- 其他:内嵌汇编 - asm(...)、内联函数- inline、编译优化(昵称“开火车”)- #pregma GCC optimize("O3")等方法(想要把时间压缩到极致的同学,建议了解一下汇编语言)。
时间度量
- 可以使用本地操作系统的库以进行高精度的时间测量。(上网搜索:C 语言,时间, - <time.h>等)
- 使用 Visual Studio 自带的时间分析工具,可参考https://blog.csdn.net/tfb760/article/details/96900098 
- 可针对性地对某个函数、某个语句进行优化。 
函数指针
这部分在本课程用处不大,但在以后很有用。
你想过吗,
qsort函数的那个cmp到底是什么呢?
引入
我们有如下的观点
- 指针变量是一种特殊的整型变量,用于存放地址值,地址就是一个整数
- 通过指针可以间接访问一个普通变量、一个结构体、一个数组等
- 地址和内存息息相关,地址就是某个内存中的存储单元的编号
- 数组名就是指针
现在我们再提出以下观点
- 函数也储存在内存中,函数也有“首地址”
- 函数名就是指针,是指向该函数的指针,称为“函数指针”
- 在函数指针后加上括号,表示对该函数的调用(call);这里没有用*解引用的过程,这是 C 语言提供的便捷写法,事实上,要解引用也可以。
我们通过两个例子验证上述观点。
C 语言中,
%p表示用十六进制输出一个地址值,指针中存放的地址值就可以用这种格式输出,当然,地址是一个整数,也可以用%d%ld%lld%u等形式输出,只是可能有 warning。
2
3
4
5
6
7
8
int main() {
int a;
printf("NULL: %p\n", NULL);
printf("&a: %p\n", &a);
return 0;
}一种可能的输出是
2
&a: 0x16b913578当然,“可能”指的是
a的地址,NULL必然是恒定的值 0,表示一个“无效地址”。
| 1 | // 案例 1 | 
我们用地址的格式输出main函数和f函数的地址,得到的可能结果是
| 1 | main: 0x1004c3f30 | 
显然,这两个函数在内存中都是有地址的,它们(的机器代码)就存放于内存中。
| 1 | // 案例 2 | 
输出为
| 1 | The number is 10 | 
这说明函数名就是指针,是指向该函数的指针,函数指针也可以解引用,得到对应的函数后再进行调用,但没必要。
函数指针类型
通过上述两个案例,我们意识到,函数本身也是一个地址值,只要知道了函数的地址,就可以实现对该函数的调用。显然,函数名就是函数的地址;但是另一方面,地址也需要存储在一个指针变量中。
我们知道,指针是有类型的,指针值本身是一个整数,它指向什么类型的变量,则需要由指针的类型表示。换言之,地址值是一个整数,但是怎么解释、怎么访问那个地址处的内容,需要由指针的类型决定。
我们通过一个例子,探索函数指针的类型。
| 1 | 
 | 
上例输出为
| 1 | i + 100 = 120 | 
这个简单的例子足以明确如何定义、如何使用函数指针,归结起来,函数指针的通用格式就是
| 1 | [函数返回值类型] (*[函数指针变量名])(函数参数列表) | 
如此一来,我们再考察qsort函数,就会豁然开朗,这个函数的原型如下
| 1 | void qsort(void *base, // 数组首地址 | 
第四个参数cmp就是一个函数指针,它指向一个返回值为int、参数为两个const void *的函数。显然qsort内部并不会把排序时元素比较的规则写死:if (a[i] < a[i + 1]),而是通过调用cmp函数从而实现用户自定义规则的比较
| 1 | if (cmp(&a[i], &a[i + 1]) > 0) { | 
以上一段只是形象说明,实际上,为了容纳任意类型的数组,qsort采用了**void*通用指针的手段,并要求传入单个元素大小width。事实上,内存中的[base, base + width)这一段空间对应数组首元素,[base + width, base + 2 * width)对应第二个元素,以此类推。在交换元素时,并不是简单的赋值,而是用memcpy、memset等方法修改内存**。另一方面,正因为如此,cmp函数的两个参数也都是通用指针,我们在实现cmp时必须强转类型,从而实现对某一特定类型数据的比较。
函数指针数组
函数指针是单个变量,一堆函数指针就是一个数组。函数指针数组在操作系统内核代码中十分常见,我们用一个简单的例子,模拟一个情形:用户不停发来请求,操作系统根据请求的不同,调用不同的处理函数(handler),响应用户请求。
| 1 | 
 | 
返回函数指针的函数(雾)
函数指针是一种指针,是一个整数,一个函数当然可以返回一个指针,这个指针当然可以是函数指针。写着比较套娃,直接看例子,这种情况一般不会用到,但是在有的地方可能会看到。
| 1 | 
 | 
如果你能理解这段奇怪的代码,说明你对函数指针的认识到位啦!
Author: diandian, Riccardo
- Title: 猪脚说第十三期
- Author: Diandian
- Created at : 2023-07-14 22:51:36
- Updated at : 2023-07-14 22:54:39
- Link: https://cutedian.github.io/2023/07/14/猪脚说第十三期/
- License: This work is licensed under CC BY-NC-SA 4.0.