猪脚说第十三期
大作业相关问题
常见问题 & 基本注意事项
- 文件的读入:推荐大家使用以下方式读取
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。
1
2
3
4
5
6
7
8
int main() {
int a;
printf("NULL: %p\n", NULL);
printf("&a: %p\n", &a);
return 0;
}一种可能的输出是
1
2 NULL: 0x0
&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.