发布于  更新于 

程序设计课程常见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
2
3
int size = 10000;

int arr[size];

在创建数组时必须用常量作为尺寸,比如:

1
2
3
4
const int size = 10000;

int arr1[size];
int arr2[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
2
int size = 1000;
int* arr = (int*)malloc(sizeof(int) * size);

其中,sizeof(int) 代表一个int变量所占内存空间大小(8bytes),可以用单个对应变量所占字节数数值代替。

在完成使用后,必须手动释放该空间,否则可能会造成内存泄漏。方法如下:

1
free(arr);

Visual Studio内建了自动的内存回收系统,但应当养成手动释放空间的好习惯。特别地,如果申请了多级指针,或指针构成了一些数据结构(如链表、数),必须遍历所有空间的指针并析构!

注意:以下代码声明的数组无法存储1000个整数,尽管他依旧能正常运行。

1
2
int size = 1000;
int* arr = (int*)malloc(size);

事实上,如果向申请到的空间中存储数据时越界,即向该空间外的内存写数据,程序一般仍可正常编译、执行,但在释放该空间时会出现问题。在Visual Studio中表现为程序卡在这一行

关于malloc的更多信息见 Microsoft Docs

Q: 笔者在子函数申请的空间,怎么主函数访问不了了?

Answer

一般的编译器可能(不太确定)不会遇到这个问题,但VS的阴间编译器特别怕你不free内存,所以会依据“谁声明谁释放原则” ,在每个函数结束时主动帮你把函数里申请的,且不作为参数返回的内存free掉。因此若要用指针形参的方式传回内存,可能会出现题中的问题。

例如,如下的代码在一些IDE/编译器下会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void genStr(char* str, int len){
str = (char*)malloc(sizeof(char) * len);
char* iter;
for(iter = str; iter < str + len; iter++) *iter = '.';
*(iter + 1) = '\0';

// 这里str申请的内存会被genStr()函数管理进程释放掉。
}

int main(){
char* str;
genStr(str, 10);
printf("%s", str); // 报错。

return 0;
}

正确的做法是借助多级指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void genStr(char** ptr, int len){
*ptr = (char*)malloc(sizeof(char) * len); // ptr指向的str并不是genStr(...)声明的。
char* iter;
for(iter = *str; iter < str + len; iter++) *iter = '.';
*(iter + 1) = '\0';
}

int main(){
char* str;
genStr(&str, 10);
printf("%s", str); // 正常输出。

return 0;
}

上述错误做法相当于main()租一间房子给genStr(),要求:“给你房子地址,你去里面开个数组、申请内存、存入字符。”房子(内存)以后还要租给别人(分配用作其它用途),作为租客,genStr()用完后有责任打扫干净房子(尤其是把数组扫走)再离开。离开后,main()当然不能访问到被“扫走”的内存里的数组。

上述这一做法相当于告诉genStr():“给你个房间地址,你去那个房间里面申请内存、存入字符。然后就滚谢谢。”作为工具人,genStr()只是申请内存、存入字符,声明数组不是它干的,它也不负责“清扫”。

感谢笔者的一位dalao同学提供一种新思路:在上述错误做法的代码中,为main()里声明的str添加static关键字,相当于告诉租客:“你走的时候不用清扫。”

关于计算机原理

请问以下代码的运行结果是什么?

1
2
int a[4] = { 1, 2, 3, 4};
printf("%x", (int*)((int)a + 1));

正确答案是: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. 如果想要永远获得一个正的余数,可以这样写:
1
int res = ((-10 % 26) + 26) % 26;
  1. 如果想掷随机数,以1~10中随机选取一个:

先引入工具库 import stdlib #include <stdlib.h>,然后调用其中地rand()函数并操作一波:

1
int rand_ = (rand() % 10) + 1;

关于字符串

Q: scanf(), scanf_s()及其替代品

Answer

一般以这种方式读入一个字符串:

1
2
char* str;
scanf_s("%s", &str);

使用VS内置的编译器,’\n’、’\0’等字符会是scanf_s()函数判断读入/打印字符串中止的条件。特别地,读入终止后,scanf_s还会在读入的最后一个字符后面填上’\0’——打印时也到此为止。

然而VS给的scanf_s()个人觉得挺反人类,纯粹的用着不舒服,尽管从安全考虑出发应当按照要求使用它。几乎每天都有人问博主各种相关问题,包括但不限于:

  • 什么时候读入会停止
  • 缓冲区里会不会留一个 ‘\n’
  • 缓冲区里的 ‘\n’ 什么时候需要清除掉
  • scanf_s()的长度参数有什么要求/坑
  • ……

笔者一般的答复是:“你可以自己写个程序测试一下”。惭愧的是,笔者几乎没有进行过相关的测试,就算测试过也忘了结果,因为笔者很讨厌反复考虑这些琐碎的内容

不过在考试的时候,因为没有安全的隐患,笔者会用scanf_s()读入字符串以外的内容。当涉及字符串读入时,笔者一般会写:

1
2
3
4
5
6
int readIn(char* str){
int i = 0; char ch;
while((ch = getchar()) != '\0' && ch != '\n' && ch != NULL) str[i++] = ch;
str[i] = '\0';
return i;
}

指针版本:

1
2
3
4
5
6
7
char* readIn() {
char* str = (char*)malloc(sizeof(char) * MaxN);
char* iter = str, ch;
while ((ch = getchar()) != '\0' && ch != '\n') *(iter++) = ch;
*iter = '\0';
return str;
}

如果要从主函数指定地址,需要一些其他技巧,前文有所提及,函数实现如下:

1
2
3
4
5
6
7
int readIn(char** ptr) {
*ptr = (char*)malloc(sizeof(char) * MaxN);
char* iter = *ptr, ch;
while ((ch = getchar()) != '\0' && ch != '\n') *(iter++) = ch;
*iter = '\0';
return iter - *ptr;
}

它的好处包括但不限于:

  • 不会在缓冲区里留下任何幻影字符
    • 因为在判断是否需要结束读入的时候,已经用getchar()把潜在的中止标志读了出来
  • 可以自定义结束条件
    • 你可以通过改变while()的判断条件,通过适当地组合同’\0’、’\n’、’ ‘、EOF、NULL等字符的判断
  • 可以返回长度,不需要再用strlen()函数
    • 代价是,为了利用这一数据,传参时往往要多加字符串长度的int变量
    • 但是肯定要比反复调用strlen()效率高
  • 自己造的轮子用起来放心

当然,为了彰显一个C程序员对内存的使用技能(就像5APU用尽跑道的每一米一样),你还可以在完成读入后调用malloc()申请一个刚好能放下整个字符串和’\0’地内存空间,但是这样可能需要对参数适当调整。

至于其余的getch()、gets()等,除非用到它们的某些特殊性质(如实时回显等),否则笔者不会用。

当然,个人认为,对于这些规则乱得离谱的东西,如果可以自己实现类似功能的话可以用自己写的函数,因为你能以最高效率知道每一个输入通过这个函数对应着什么输出。下面笔者们来学习映射的相关知识。

关于算法

Q: 如何优雅地做模式匹配

Answer

使用看起来很优雅、学起来很不优雅、学会了用很优雅的KMP算法——这可能是费过笔者脑子最多的一个算法——具体过程笔者考完期中考试再更吧hhhhh。

可以先参考:阮一峰的网络日志:字符串匹配的KMP算法