程序设计课程常见FAQs
前言
博主身边有很多人问程设课程相关问题,特写此推统一解答。内容不定期更新,暂时还没有部署推文更新Pipeline,各位可以通过RSS订阅笔者的博客:
笔者都不知道啥时候更新的博客的RSS。
博主的环境是Visual Studio 2019 Community,课程推荐环境是VS2012,二者布局基本一致。课程语言为C/C++
FAQs
关于课程与Visual Studio
Q: 为什么要学习C
Answer
一般地,一道选择题你做不出来,你会选择()
Q: 如何为开发环境更换背景/字体
Answer
以VS2012及后期版本为例,在软件菜单栏选择 工具 - 选项,在 环境 - 常规 中可以选择主题(深色/浅色),在 环境 - 颜色和字体 中可以自定义代码的字体。
推荐几个个人比较喜欢的字体:
- FiraCode Family: 个人使用超过4年,强烈推荐;
- Monano: 当前正在和FiraCode混搭使用,看起来很舒服;
- Deja Vu Sans Mono: 一些字符便于区分;
- Consolas:Windows内置,还可以;
宋体:绝对是程设课上使用人数最多的一款(手动狗头)。
Q: 能不能定期发题解
Answer
不能。
关于数组
Q: 如何优雅地开数组
Answer
C、C++在定义数组时不支持使用int变量作为数组大小,比如下面的代码是非法的:
1 | int size = 10000; |
在创建数组时必须用常量作为尺寸,比如:
1 | const int size = 10000; |
常用的创建数组的方法有两种:
根据数据范围直接声明最大的数组
一般地,题目中会给出数据范围。此时一种常见的做法(也是OI常见的做法)是在全局定义一个常量,然后在创建数组的时候使用。如题目说明输入$n$个整数(假定int
),可以在全局中声明:
1 | const int MaxN = 10000; |
在创建数组时:
1 | int arr[MaxN]; |
习惯是:偶尔适当扩大MaxN,比如
MaxN = 10000 + 10
,个人这么做是为了容下’\0’或在各种Stack Overflow时不那么难堪x。
但执行这种操作前请务必确认题目设置的内存限制是否够用,就算你是用的是刘连臣老师的人肉OJ!x
用malloc申请内存
如果想要节省内存,可以通过malloc关键字申请内存。malloc定义于stdlib
.h库中,其声明如下:
1 | void *malloc(size_t size); |
参数说明:
- size:要申请的内存的大小,单位为bytes
返回值:
- 返回一个void指针
- 如果申请成功,则指针指向申请到的空间
- 如果申请失败,则是一个空指针(指向NULL)
说明:
- malloc函数能确保返回的指针所指向的内存空间(如果成功申请到)足以储存不超过其大小的任何数据。
- malloc返回void指针,在当作数组使用时需要通过显示转换转换为指向数组的指针。
在申请内存时调用该函数:
1 | int size = 1000; |
其中,sizeof(int) 代表一个int变量所占内存空间大小(8bytes),可以用单个对应变量所占字节数数值代替。
在完成使用后,必须手动释放该空间,否则可能会造成内存泄漏。方法如下:
1 | free(arr); |
Visual Studio内建了自动的内存回收系统,但应当养成手动释放空间的好习惯。特别地,如果申请了多级指针,或指针构成了一些数据结构(如链表、数),必须遍历所有空间的指针并析构!
注意:以下代码声明的数组无法存储1000个整数,尽管他依旧能正常运行。
1 | int size = 1000; |
事实上,如果向申请到的空间中存储数据时越界,即向该空间外的内存写数据,程序一般仍可正常编译、执行,但在释放该空间时会出现问题。在Visual Studio中表现为程序卡在这一行。
关于malloc的更多信息见 Microsoft Docs。
Q: 笔者在子函数申请的空间,怎么主函数访问不了了?
Answer
一般的编译器可能(不太确定)不会遇到这个问题,但VS的阴间编译器特别怕你不free内存,所以会依据“谁声明谁释放原则” ,在每个函数结束时主动帮你把函数里申请的,且不作为参数返回的内存free掉。因此若要用指针形参的方式传回内存,可能会出现题中的问题。
例如,如下的代码在一些IDE/编译器下会报错:
1 | void genStr(char* str, int len){ |
正确的做法是借助多级指针:
1 | void genStr(char** ptr, int len){ |
上述错误做法相当于main()租一间房子给genStr(),要求:“给你房子地址,你去里面开个数组、申请内存、存入字符。”房子(内存)以后还要租给别人(分配用作其它用途),作为租客,genStr()用完后有责任打扫干净房子(尤其是把数组扫走)再离开。离开后,main()当然不能访问到被“扫走”的内存里的数组。
上述这一做法相当于告诉genStr():“给你个房间地址,你去那个房间里面申请内存、存入字符。然后就滚谢谢。”作为工具人,genStr()只是申请内存、存入字符,声明数组不是它干的,它也不负责“清扫”。
感谢笔者的一位dalao同学提供一种新思路:在上述错误做法的代码中,为main()里声明的str添加static关键字,相当于告诉租客:“你走的时候不用清扫。”
关于计算机原理
请问以下代码的运行结果是什么?
1 | int a[4] = { 1, 2, 3, 4}; |
正确答案是:2000000
试错
有点迷惑?试着把printf参数改成:
1 | printf("%x", (int*)((int)a + 2)); |
结果是:200000
嗯?有点头绪了?再改:
1 | printf("%x", (int*)((int)a + 3)); |
结果是:2000
好像这个2就是a[]的第二个元素。
Answer
这要和内存存储机制挂钩。笔者们知道,sizeof(int) = 4
,$i.e.$int占4字节。在内存中,一个数组的内容连续地、从低向高位存储。
不妨假设a[0]
的地址是0x00
,那么a指向这样一片区域(的最低位):
00 00 00 04 <- 0x0c
00 00 00 03 <- 0x08
00 00 00 02 <- 0x04
00 00 00 01 <- 0x00
这么些只是为了让1、2和笔者们的书写习惯一致。事实上,计算机存储的值从低位开始为:
10 00 00 00 20 00 00 00 …
原本(int*)((int)a)
指向的是0x00开始的8个16进制位:
10 00 00 00 20 00 00 00 …
于是(int*)((int)a + 1)
指向的是0x01开始的8个16进制位:
10 00 00 00 20 00 00 00 …
于是16进制就是0x02000000。
关于计算
Q: 取模运算符 % 有什么妙用
Answer
取模运算符每个人的IDE都有,那么取模运算符有什么妙用吗?下面就和小编一起看看取模运算符的妙用吧!
- 如果想要永远获得一个正的余数,可以这样写:
1 | int res = ((-10 % 26) + 26) % 26; |
- 如果想掷随机数,以1~10中随机选取一个:
先引入工具库 import stdlib
#include <stdlib.h>
,然后调用其中地rand()
函数并操作一波:
1 | int rand_ = (rand() % 10) + 1; |
关于字符串
Q: scanf(), scanf_s()及其替代品
Answer
一般以这种方式读入一个字符串:
1 | char* str; |
使用VS内置的编译器,’\n’、’\0’等字符会是scanf_s()函数判断读入/打印字符串中止的条件。特别地,读入终止后,scanf_s还会在读入的最后一个字符后面填上’\0’——打印时也到此为止。
然而VS给的scanf_s()个人觉得挺反人类,纯粹的用着不舒服,尽管从安全考虑出发应当按照要求使用它。几乎每天都有人问博主各种相关问题,包括但不限于:
- 什么时候读入会停止
- 缓冲区里会不会留一个 ‘\n’
- 缓冲区里的 ‘\n’ 什么时候需要清除掉
- scanf_s()的长度参数有什么要求/坑
- ……
笔者一般的答复是:“你可以自己写个程序测试一下”。惭愧的是,笔者几乎没有进行过相关的测试,就算测试过也忘了结果,因为笔者很讨厌反复考虑这些琐碎的内容。
不过在考试的时候,因为没有安全的隐患,笔者会用scanf_s()读入字符串以外的内容。当涉及字符串读入时,笔者一般会写:
1 | int readIn(char* str){ |
指针版本:
1 | char* readIn() { |
如果要从主函数指定地址,需要一些其他技巧,前文有所提及,函数实现如下:
1 | int readIn(char** ptr) { |
它的好处包括但不限于:
- 不会在缓冲区里留下任何幻影字符
- 因为在判断是否需要结束读入的时候,已经用getchar()把潜在的中止标志读了出来
- 可以自定义结束条件
- 你可以通过改变while()的判断条件,通过适当地组合同’\0’、’\n’、’ ‘、EOF、NULL等字符的判断
- 可以返回长度,不需要再用strlen()函数
- 代价是,为了利用这一数据,传参时往往要多加字符串长度的int变量
- 但是肯定要比反复调用strlen()效率高
自己造的轮子用起来放心
当然,为了彰显一个C程序员对内存的使用技能(就像5APU用尽跑道的每一米一样),你还可以在完成读入后调用malloc()申请一个刚好能放下整个字符串和’\0’地内存空间,但是这样可能需要对参数适当调整。
至于其余的getch()、gets()等,除非用到它们的某些特殊性质(如实时回显等),否则笔者不会用。
当然,个人认为,对于这些规则乱得离谱的东西,如果可以自己实现类似功能的话可以用自己写的函数,因为你能以最高效率知道每一个输入通过这个函数对应着什么输出。下面笔者们来学习映射的相关知识。
关于算法
Q: 如何优雅地做模式匹配
Answer
使用看起来很优雅、学起来很不优雅、学会了用很优雅的KMP算法——这可能是费过笔者脑子最多的一个算法——具体过程笔者考完期中考试再更吧hhhhh。
可以先参考:阮一峰的网络日志:字符串匹配的KMP算法