BUAA程设第三周上机总结
写在前面
上周的上机题有些太难了,然后这周的题经过了超级削弱之后同学们做起来可能感觉十分轻松,然后暴露出来的问题也比较少。所以今天除了知识点外再稍微提提做题的技巧吧。
左移、右移与位运算
有、无符号位运算
在C语言中,位运算分为两类,分别是有符号位运算和无符号位运算。这两种位运算的区别主要体现在右移过程中。
因为位运算是直接针对数据的补码进行按位操作,那么在处理有符号负数的补码时会出现一些歧义。这里拿 − 1 -1 −1 做例子:
−
1
-1
−1 的补码是 1111 1111 1111 1111 1111 1111 1111 1111
,对它进行左移操作毫无疑问是是在右侧补零的,这没有任何异议。即对
−
1
-1
−1 左移一位后他的补码会变成 1111 1111 1111 1111 1111 1111 1111 1110
,对应的数字是
−
2
-2
−2 。
但是在右移操作时,左侧补零与否就是一个问题:补零的话就会改变这个数字的符号(由负数变成正数),但是如果只看补码的话左侧补零也是有着道理的。这就引出了有符号右移和无符号右移。
先说结论,在C语言中对有符号类型数据(如int
,long long
,char
)进行右移操作是有符号右移;对无符号类型数据(如unsigned int
,unsigned long long
,unsigned char
)进行右移操作时无符号右移。
这两者的区别就在于左侧是否补零:有符号数字在右移时左侧会默认补一(不改变符号位),无符号数字在右移时左侧会补零(无符号数字没有符号位)。
举例来说,1111 1111 1111 1111 1111 1111 1111 1111
这个补码既可以表示 int
类型的
−
1
-1
−1,也可以表示 unsigned int
的
4
,
294
,
967
,
295
4,294,967,295
4,294,967,295 。如果是 int
类型,那么对他右移的结果是左侧会补一,补码仍是 1111 1111 1111 1111 1111 1111 1111 1111
,依然表示
−
1
-1
−1 。如果是 unsigned int
类型,那么对他右移的结果是左侧补零,补码会变成 0111 1111 1111 1111 1111 1111 1111 1111
,表示
2
,
147
,
483
,
647
2,147,483,647
2,147,483,647 。
左移、右移的位数不要超过类型本身的位数
这句话的意思是,举例来说 int
的位数是 32
位,那么在左移 / 右移的时候移位的位数不要超过32。如果移位的位数超过了32,那么结果是未知的。(进入了混沌领域)
同学们记住不要这么做就可以了。
数组的二三事
数组初始化
在声明数组的时候,可以直接对数组进行初始化。常用的是直接在数组中存入我们可能需要用到的数字,或者是全部初始化为零。
int month[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; //在声明数组的时候直接进行初始化
int num[1000] = {0}; //将数组中的1000个元素全部置零
//对于字符串数组,可以直接用字符串进行初始化,如:
char str[100] = "Hello world!";
其中全部置零的写法比较常见,希望同学们记住并掌握。(其实还有一些其他的规则,但是防止同学们学习后写出一些奇怪的代码所以就不多加介绍了,同学们目前只需要掌握这两种初始化的方法就可以了)
char类型数组
关于 char
数组的问题主要在于,一般而言我们都是将 char
类型的数组视为字符串。而字符串需要一个结尾标识符 \0
。即你的 char
类型数组的容量应该至少比其存储的字符串长度多
1
1
1。这个错误同学们常常在使用字符数组读取字符串时出错,数组开的不够大导致 REG
错误。由于同学们目前还没有系统地接触字符串及字符串处理函数,所以这里不进行过多拓展。
数组容量在合理范围内稍微开大一点
比如题目给的数据范围是一千以内,在开数组的时候就不要正好开一个一千大小的数组去存数据,要稍微开的大一点,比如一千一百。
这样做的好处是,防止你在写出这样的代码时出错:
int num[1000];
for(int i = 1; i <= 1000; i++)
scanf("%d", &num[i]); //ERROR!
这个代码错在只给 num
数组定义了
1000
1000
1000 容量,那么可用的下标范围是
0
~
999
0~999
0~999 ,访问到 num[1000]
时就会出错。
防止此类问题的最好方式就是把数组在合理范围内稍微开的大一点。(球球不要卡着数据范围开数组了)
0到1000有1001个数
我发现很多同学在使用循环时不是很理解 0 0 0 到 n n n 总共有 n + 1 n+1 n+1 个数字这件事。
但是我又不知道怎么解释这个事情。。。
从 0 0 0 到 n − 1 n-1 n−1 或从 1 1 1 到 n n n 都有 n n n 个数字,但是从 0 0 0 到 n n n 就会有 n + 1 n+1 n+1 个。。。希望同学们能记住这个错误。
总之就给出两个遍历读取方式:
for(int i = 0; i < n; i++)
scanf("%d", &num[i]);
//数字将存储在num[0]到num[n-1],总计n个数
for(int i = 1; i <= n; i++)
scanf("%d", &num[i]);
//数字将存储在num[1]到num[n],总计n个数
遍历数组也是使用相同的方式。
数据类型要和格式符相对应
不要出现这样的代码:
long long a;
scanf("%d", &a); //Wrong!
对于一种类型的数据,就要使用相对应的格式符读取、输出,不要使用不匹配的格式符,很容易出bug。(在以后同学们水平高了之后可能会接触相关的知识,但是现在千万不要这么尝试)
养成良好的编程习惯
唔,有同学询问如何养成良好的编程习惯,这里依据我自身的经验给出一点建议吧。
保持良好的代码风格
缩进和空格和换行
虽然之前已经说过一次,但是同学们还是很难做到。打空格这个事情我只能说是打习惯之后就会成为一个习惯。打了空格的代码的可读性真的会强很多。
ans = (a - b) > 0 ? (a - b) : (b - a);
ans=(a-b)>0?(a-b):(b-a);
明明是两行相同的代码,但是为什么美观性就差了这么多呢,这是怎么肥事呢.jpg
此外在代码块直接之间加上空行,会让代码的分块更加明显,便于阅读。
还是希望同学们能养成打空格、打缩进的习惯,这真的很重要!
保持良好的命名习惯与声明变量习惯
不要在程序开头的一行里一下子声明一堆变量。错误示范:
int a,b,c,i,j,k,sum,ans1,ans2;//这是令人死亡的定义方式
即使真的要一下声明很多变量,也要分开一行一行声明,并且要对每个变量的命名赋予意义,不要使用 i , j i,j i,j 这样读起来毫无意义的变量存储有意义的数据。 i , j i,j i,j 这样的变量通常用于控制循环,即定义在 f o r for for 循环内。而如果使用 a , b a,b a,b 这样的变量用作临时变量存取临时数也是可以理解的。
一个变量尽量在用到他的时候再进行定义。同时也要注意变量的作用域和生命周期。这个同学们之后便会学到。
此外在声明的时候,若有必要,尽量给变量赋上初值。这样可以杜绝许多bug。
多写注释
在代码中多添加注释,并且尽量是简短的英文,防止不同IDE对中文字符的编码不同导致不能阅读注释。特别是在声明变量时以及在一些程序的关键地方。多写注释不仅可以帮助别人理解自己的代码,也可以帮助自己理解自己的代码。
在写代码之前先整理好思路
在开始码代码之前务必先对代码有整体规划和思路,不要打一点想一点。可以先在草稿纸上写下大体思路,构思代码结构,大体规划好需要的变量。这样在码代码的时候能保证思路连贯。打一点想一点的弊端很大,常见的比如打到后面忘了前面的某些代码是干什么的等等,来回检查代码、回想思路就会平白浪费许多时间。
保持全局观再码代码是很重要的。这样做的好处包括但不仅限于:保持连贯的思路,bug更容易发现,更容易一口气打出代码…同学们尝试几次就会发现这样的好处了。
就结论而言,这样码出的代码出现bug的可能性更小,一次性 A C AC AC 的概率更大。一次性 A C AC AC 真的既酷又爽的好不好!
确保在读懂题之后再码代码
在做题前要先读懂题意,认认真真读题干,认认真真理解样例输入和输出,认认真真阅读给出的 h i n t hint hint ,在任何一步没有完全理解之前都不要妄然动手打代码。确保自己在完全理解了题目,脑海中已经想好了题目的解法后,再开始动手码代码。
切忌在没读懂题的情况下就开始码代码!
示范代码
下面给一个代码风格良好(大概)的代码,是我自己写的E2卡牌数数那题的代码。
#include <stdio.h>
int main(){
int num[10]; //store num of cards
long long ans;
for(int i = 0; i < 10; i++)
scanf("%d", &num[i]);
scanf("%lld", &ans);
while(1){
long long tem = ans;
int flag = 0;
if(tem == 0) //special judge
num[0]--;
while(tem != 0){
num[tem % 10]--;
if(num[tem % 10] < 0){
flag = 1;
break;
}
tem /= 10;
}
if(flag == 1) //if exist num < 0
break;
ans++;
}
printf("%lld\n", ans - 1); //cant count to ans
return 0;
}
仅做参考。
代码调试的简单方法
一种常见的方法是在代码中使用 printf()
输出中间变量,观察变量的变化是否如自己所愿。比如我上面的代码,如果我想要调试:
#include <stdio.h>
int main(){
int num[10]; //store num of cards
long long ans;
for(int i = 0; i < 10; i++)
scanf("%d", &num[i]);
scanf("%lld", &ans);
while(1){
long long tem = ans;
int flag = 0;
printf("%d ", ans);//添加printf以观察ans值的变化
if(tem == 0) //special judge
num[0]--;
while(tem != 0){
num[tem % 10]--;
if(num[tem % 10] < 0){
flag = 1;
break;
}
tem /= 10;
}
if(flag == 1) //if exist num < 0
break;
ans++;
}
printf("%lld\n", ans - 1); //cant count to ans
return 0;
}
观察其他的变量也是同理。这是新手最容易掌握的一种调试代码的方法,希望同学们都可以掌握。(掌握起来没有任何难度)
记得换行
在输出多组数据的时候记得在该换行的时候添加换行符。如果不添加换行符,评测机显示的结果大概率是 WA
而不是 PE
!
不要抄袭代码
在上次上机的时候对同学们提交的代码进行了查重,最后发现了一部分同学存在抄袭的现象。令我欣慰的是咱班同学的抄袭现象很少,而且应该不是有意的。这里再次强调不要直接抄袭代码!上机的时候讨论乃至搜索都是允许的(开卷),但是对于抄袭代码的行为是绝对零容忍的。之后再出现抄袭代码的情况将会做出严厉的惩罚,希望同学们都能够对自己负责。