内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将与15个工作日内将博客设置为仅粉丝可见。
目录
代码风格与规范化辅助工具
代码没有规范的危害
现代软件产业经过了几十年的发展,一个软件由一个人单枪匹马完成已经非常少见了,大多数的软件都是在多人合作中开发完成的。几名工程师合作开发一个项目时,做的最多的一件事情就是“看代码”,每个人都需要能够看懂其他人的代码。这个时候,代码规范就变得尤为重要。
缩进
缩进究竟是用 Tab 键好,还是用 2、4、8 个空格好呢?
答案是:
- 用空格好
- 具体几个空格要参考规范
- 不管规范如何约定,要确保团队内缩进规则的统一性
即使你喜欢使用 Tab 键,现在也有很多的 IDE可以通过设置来将 Tab 键扩展定义为几个空格。不使用 Tab 键的理由是,Tab 键在不同的情况下会显示为不同的长度。
小贴士
为了让团队开发的缩进的统一,在开发时,最好在项目里配置好 EditorConfig 文件,并且在开发工具里读取这个配置。
进一步了解一下 EditorConfig,你会发现它可以给你带去一些惊喜,它不仅可以控制缩进,还可以空格行尾换行符;另外,对不同语言的文件支持配置为不同的约定标准也是它非常强大的功能之一。
下图中展示了一段在不同行分别使用了 Tab 键、2 个空格、4 个空格的程序。左侧的 IDE 很贴心的自动统一了缩进,而右侧的编辑器则将 Tab 显示成了 8 个空格的长度,让代码显得参差不齐。
命名风格之不要缩写
当我们的程序写的越来越复杂之后,给变量、函数或者类命名这样的简单的事情也不那么简单了,使用一致的命名风格是非常重要的。但是使用一致的命名风格并不代表着是要全用a
,b
,c
,d
来做变量名,而是要做到形式一致,并且有意义,能根据变量名看出它的作用。
所以,千万不要为了看起来短来使用缩写作为变量名,假设你使用res
作为变量名,你觉得其他人如何才能知道你的意思究竟是resource
、response
、resolution
还是reset
呢?
小贴士
- 命名的时候,如果是一个实体,可以采用名词或复合名词,如果是一个方法则采用动词+名词 或者 动词+形容词 的形式。
- 尽可能不管任何时候都使用完整的单词来进行命名,永远不要担心变量名、方法名太长,但是一定要注意避免不必要的命名导致表达的意思不清。
下面的这一段代码就是一个命名风格不一致的典范。因为它不仅使用了缩写,还分别用全部大写、下划线、驼峰命名三种方式来为函数名和变量名命名。
#include <iostream>
using namespace std;
int FIBONACCI(int countN) {
if ( countN == 1 || countN == 2 ) {
return 1;
}
else {
return FIBONACCI(countN - 1) + FIBONACCI(countN - 2);
}
}
int main() {
int num_1;
cin >> num_1;
cout << FIBONACCI(num_1) << endl;
return 0;
}
代码每行的缩进是否正确
我们已经知道了很多开发者们会约定一个统一的缩进标准。假设某一个开发团队约定了使用 4 个空格来进行代码缩进,针对给定的这一段代码,列出的选项中哪两个说法是正确的呢?
#include <iostream>
using namespace std;
int main() {
int a, b, c;
cin >> a >> b >> c;
cout << a + b + c << endl;
return 0;
}
每行不要写太长
在我们写代码的过程中,每一行代码的行宽也是需要进行限制的。以前的计算机和打印机显示的行宽为 80 字符,所以我们一般每一行代码的行宽也会限制在 80 字符以内。不过,由于现在的显示器等原因,很多时候有些团队也会将行宽的限制扩展到 120 字符。
与之前的情况类似,这也是一个需要全团队进行统一约定的规范,一旦约定团队成员们就应该尽力遵守。
超出行宽的情况一般会有两个,一个是当函数的参数表非常长时,我们会将每个参数都单独写在一行;另一个是if
语句中的逻辑判断特别多时,我们会将每个&&
逻辑单独写成一行。
string reverseString(string sourceString, int reverseBegin, int reverseEnd) {
// Coding...
}
上面这段代码的参数表就长到需要将每个参数都单独写在一行了,所以我们要把它写成下面这种形式:
string reverseString(string sourceString,
int reverseBegin,
int reverseEnd) {
// Coding...
}
if(sourceString != "" || sourceString.length() != 0 && reverseBegin >= 0 && reverseEnd < sourceString.length() && reverseEnd >= reverseBegin) {
// Coding...
}
上面这一段代码则是由于逻辑判断过多,需要将每个&&
逻辑单独写成一行,所以我们会把它写成下面这种形式:
if(sourceString != "" || sourceString.length() != 0
&& reverseBegin >= 0
&& reverseEnd < sourceString.length()
&& reverseEnd >= reverseBegin) {
// Coding...
}
小贴士
一般
if
中的条件由大于等于 33 个的多个部分组成,更好的做法会是将这个条件单独写成一个函数,在if
语句中直接调用这个单独的函数。
正确使用小括号
蒜头君虽然刚学会写代码,但是他很勤奋,喜欢写各种各样的小程序玩。这不,蒜头君又写了一个程序,输入一个数字,判断它是不是满足“可以被 2 或 3 整除,但不能被 6 整除”这个条件。但是蒜头君遇到了一些问题,因为 6 这个数字是不符合上面的条件的,但他的程序却输出的是Yes
。
根据判断,一定是因为蒜头君的逻辑语句中,不同运算符的优先级弄混了。所以,我们需要通过加括号的方式,来改变它们的逻辑优先级,将num % 2 == 0 || num % 3 == 0
这段代码用括号括起来 就可以了。
#include <stdio.h>
int main() {
int num;
scanf("%d", &num);
if(num % 2 == 0 || num % 3 == 0 && num % 6 != 0) {
printf("Yes\n");
} else {
printf("No\n");
}
return 0;
}
在复杂的条件表达式中,一定记得要用括号清楚的表示它们的逻辑优先级。特别是在你记不住它们的优先级分别是怎样的时候,使用括号就更有必要了!
加上必要的大括号
蒜头君写了这样一个程序,输入两个数字,如果第一个数大于第二个数,就将这两个数分别加上 1 和 2,再判断一次,如果这次第一个数仍然大于第一个数,就把它们输出出来。
#include <stdio.h>
int main() {
int i, j;
scanf("%d%d", &i, &j);
if (i > j)
i += 1, j += 2;
if (i > j)
printf("%d,%d\n", i, j);
return 0;
}
虽然蒜头君的代码并没有错误,但是这里有一个很不好的习惯,就是他将如下的两个赋值语句写在了一行当中,虽然这样可以节省行数,但是在程序调试的时候会很不方便。
if (i > j)
i += 1, j += 2;
将这一行代码分成两行来写,并用大括号将这两行代码包围起来。
#include <stdio.h>
int main() {
int i, j;
scanf("%d%d", &i, &j);
if (i > j){
i += 1;
j += 2;
}
if (i > j){
printf("%d,%d\n", i, j);
}
return 0;
}
在历史上,有很多的超级 bug (其中多个事故都导致了上亿美元的浪费)都是由于没有写大括号而导致的。一定要记住前人的教训哦!
命名风格
当我们的程序写的越来越复杂之后,给变量、函数或者类命名这样的简单的事情也不那么简单了,使用一致的命名风格是非常重要的。那么什么样的命名风格是比较好的呢?
常见的命名方式有下面这几种,具体在什么场景下使用,需要参考对应语言的命名规范:
- 全部大写(SCREAMING_SNAKE_CASE):一般用于常量,多个单词之间下划线做分隔。
- 小驼峰(camelCase):形如
mouse
,isMouse
,常见于类 C 语言的函数名、方法名。 - 大驼峰(PascalCase):形如
Mouse
,AsianMouse
,常见于面向对象语言的类名、组件化开发模式的组件名、文件的文件名。 - 下划线命名(snake_case):形如
mouse
,is_mouse
,常见于一些语言的变量名命名。 - 中划线命名(kebab-case):形如
mouse
,is-mouse
,常见于一些在系统路径中可能出现的命名,如接口命名、文件命名等。此外,CSS 的类名也非常常见的典型中划线命名。
在这里,强烈建议你花一些时间了解一下 JavaScript、CSS、HTML、Python 这四种语言的命名规范。这将对你之后进行工程实践训练带去很大的帮助。
部分语言会用一些特殊字符放在命名中用于特殊用途——比如在开头加上 `$`、`_` 或 `__`,这些标记往往起到了标记“系统作用域”的作用,表示这些变量、函数等是用于系统中特殊用途的,和常规的变量、函数等是需要区分开的。
遵守一定的命名规范,是在多人协作开发过程中的必备素养哦!
命名方法
蒜头君在声明类的时候,总是会把成员变量都声明为private
的,这是一个很好的习惯。但是它在声明成员方法来操作私有变量时,却总是把方法命名得不伦不类。在一般的情况下,我们在操作类的私有变量时,会将给成员变量赋值的方法以set
开头来命名,取私有变量的值的时候则是以get
开头来命名。
蒜头君写过一个叫做square()
的方法,这个方法返回一个bool
型的值,表示这个矩形是或者不是一个正方形。对于这一类的方法,在命名的时候应当以is
开头,来代表这是一个返回true
或者false
的方法。
这些都是类中非常常见的方法,使用统一风格的命名有助于看懂这个方法的作用。无论是方法还是变量,一定要起一个有语义的名字,这样别人才能更容易的看懂你的代码!
注释
注释是计算机语言的一个重要组成部分,用于解释代码的功用,可以增强程序的可读性和可维护性。
那么,需要注释什么呢?不要注释程序是怎么工作的,因为程序本身就应当能够说明这个问题。注释更多是为了解释程序做什么、为什么这样做。
在大多数的编程语言中,注释分为“行注释”和“块注释”两种。
行注释和块注释
行注释和块注释并不是语义上的“一行的注释”或“一大段的注释”,而是指语法中的意思。例如在 C++、JavaScript 中,//
开头的就是行注释,/*
和 */
包裹就是块注释。在 Python 中 #
开头的就是行注释,两个 '''
或两个 """
包裹就是块注释。
什么时候使用行注释
我们建议总是使用行注释,因为这样我们可以非常方便地去对一整块代码进行注释。举个例子,假设我们在代码中一直使用块注释:
cout << "Hello, World!" << endl; /* 输出 Hello World*/
return 0;
如果我们想对这一段代码进行注释,我们可能会这样做:
/*
cout << "Hello, World!" << endl; /* 输出 Hello World*/
return 0;
*/
这时你就会发现,当第一个/*
检测到第一个*/
时,它就会以为这里就是结束的地方,所以最后面的那个*/
并没有被匹配,这样就会造成语法出错。
所以我们建议在开发当中,总是使用行注释,而不是块注释。
什么时候使用块注释
只有 3 种情况下可以使用块注释:
- 当我们需要把一大段代码注释掉的时候;
- 当自动化文档生成工具的语法要求使用块注释,或约定俗成使用块注释作为代码内文档的时候;
- 编程语言只有块注释语法的时候。
在其他情况下,我们建议永远都不要使用块注释。
多行注释
如果我们的注释是需要跨行的,我们仍然需要使用行注释:
// 这是第一段注释,这是第一段注释
// 这是第一段注释,这是第一段注释,这是第一段注释
// 这是第二段注释,这是第二段注释
// 这是第二段注释,这是第二段注释,这是第二段注释
另外,我们还需要注意一点,由于注释和代码一样,在开发过程中都可能会被二次修改。而如果我们写了大量的注释就可能导致在修改代码的时候需要同步更新注释,这会带来很高的维护成本。
所以更多的时候我们会鼓励将代码中的变量名、函数名等命名得尽可能清晰,对于代码片段做更多必要的抽象,让代码实现“自注解”。相比于“多写注释”,实现代码的“自注解”是一种更好的工程习惯。
代码风格
根据前面课程的内容,请将选项中的操作和对应的方法进行连线配对。假设现在的项目使用的语言是 C++。
请分别点击选中应该对应成对的选项进行配对,对于被选中的选项再次点击可以取消选中状态
-
【操作】常量命名
-
【操作】每行代码的宽度
-
8080 个字符
-
统一个数的空格
-
小驼峰或下划线,需要统一
-
全部大写
-
【操作】对代码进行缩进
-
块注释
-
【操作】对一段代码进行注释
-
【操作】变量命名
-
小驼峰
-
【操作】类命名
-
行注释
-
【操作】方法、函数命名
-
【操作】写一段多行的文字注释
-
大驼峰
以下显示的是正确的配对结果:
-
块注释
【操作】对一段代码进行注释
-
统一个数的空格
【操作】对代码进行缩进
-
行注释
【操作】写一段多行的文字注释
-
8080 个字符
【操作】每行代码的宽度
-
小驼峰或下划线,需要统一
【操作】变量命名
-
小驼峰
【操作】方法、函数命名
-
大驼峰
【操作】类命名
-
全部大写
【操作】常量命名
改善代码风格
老师给蒜头君布置了一个 C 语言的大作业——实现字符串类,蒜头君完成以后自信满满地提交给老师了。可是,老师却对蒜头君说:“你的程序运行结果都是对的,但是代码风格真的很糟糕”。
在蒜头君的追问下,老师给了他一份代码的问题清单:
- 缩进不统一
- 部分地方缺少大括号
- 缺少空格或存在多余空格
于是蒜头君找到了拥有良好代码风格的你,你能帮他把代码改好吗?
分数计算
在本地执行make lint
,之后打开目录下的lint_result
文件,你将会发现所有代码中的风格问题,共 18 个。
本题共 100 分,因为本地测试和远程测试一致,因此只有当完全解决本地make lint
提示的所有错误后,才会获得 100 分,否则将只得到 0 分。
代码清单
你需要将蒜头君的src/mydate.cpp
、include/mydate.h
这两个文件的代码风格问题全部修正。虽然文件的扩展名是 cpp,但实际上是 C 语言,这里使用 .cpp 是为了测试方便。
src/mydate.cpp:6: Missing space after , [whitespace/comma] [3]
src/mydate.cpp:9: Extra space before ( in function call [whitespace/parens] [4]
src/mydate.cpp:10: Missing spaces around != [whitespace/operators] [3]
src/mydate.cpp:13: Missing space before { [whitespace/braces] [5]
src/mydate.cpp:15: Mismatching spaces inside () in if [whitespace/parens] [5]
src/mydate.cpp:15: Missing space before { [whitespace/braces] [5]
src/mydate.cpp:19: Missing spaces around = [whitespace/operators] [4]
src/mydate.cpp:20: Extra space before last semicolon. If this should be an empty statement, use {} instead. [whitespace/semicolon] [5]
src/mydate.cpp:21: Missing space before ( in if( [whitespace/parens] [5]
src/mydate.cpp:23: If an else has a brace on one side, it should have it on both [readability/braces] [5]
src/mydate.cpp:23: Missing space before else [whitespace/braces] [5]
src/mydate.cpp:24: Missing spaces around = [whitespace/operators] [4]
src/mydate.cpp:29: Missing space after , [whitespace/comma] [3]
src/mydate.cpp:35: Extra space before ( in function call [whitespace/parens] [4]
src/mydate.cpp:48: Extra space for operator -- [whitespace/operators] [4]
include/mydate.h:9: Extra space before ( in function call [whitespace/parens] [4]
include/mydate.h:10: Extra space before ( in function call [whitespace/parens] [4]
include/mydate.h:17: Could not find a newline character at the end of the file. [whitespace/ending_newline] [5]
修改后的mydate.cpp:
#include <stdio.h>
#include <stdlib.h>
#include "../include/mydate.h"
int days[12]={
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};
int isLeapYear(struct MyDate *date) {
return ((date->year%100 != 0 && date->year%4 == 0) || date->year%400 == 0);
}
void addOneDay(struct MyDate *date) {
date->day++;
if ( date->day > days[date->month - 1] ) {
date->day = 1;
date->month++;
if (date->month > 12) {
date->month = 1;
date->year++;
if (isLeapYear(date)) {
days[1] = 29;
} else {
days[1] = 28;
}
}
}
}
void init(struct MyDate *date, int yearInput, int month_input, int day_input) {
date->year = yearInput;
date->month = month_input;
date->day = day_input;
}
int getYear(struct MyDate *date) {
return date->year;
}
int getMonth(struct MyDate * date) {
return date->month;
}
int getDay(struct MyDate* date) {
return date->day;
}
void addDay(struct MyDate *date, int inc) {
while ( inc-- ) {
addOneDay(date);
}
}
char* toString(struct MyDate *date) {
char *buf = (char*)malloc(sizeof(char) * 11);
snprintf(buf, 10, "%d-%d-%d", date->year, date->month, date->day);
return buf;
}
修改后的mydate.h:
#ifndef MYDATE_H
#define MYDATE_H
extern int days[12];
struct MyDate {
int year, month, day;
};
int isLeapYear(struct MyDate *date);
void addOneDay(struct MyDate *date);
void init(struct MyDate *date, int year, int month, int day);
int getYear(struct MyDate *date);
int getMonth(struct MyDate *date);
int getDay(struct MyDate *date);
void addDay(struct MyDate *date, int inc);
char* toString(struct MyDate *date);
#endif