代码风格

本附录主要讨论有关代码风格的几个小问题。

命名

命名法

我们希望变量的命名是有意义的。这里介绍一些常见的变量命名规则:

小驼峰命名法

将所有单词无缝隙连接在一起,但是:

  • 第一个单词全部小写;
  • 其余的单词首字母大写,但后续字母小写;
  • 缩写的大小写与首字母保持相同。

这种命名法的例子有:myFirstName myLastName nextStudentName intCount 等。一般地,这种命名法用于变量名。

大驼峰命名法

将所有单词无缝隙连接在一起,且所有单词首字母大写,后续字母小写。缩写单词全部大写。这种命名法的例子有:GameMode CoordinateUtil ConfigHelper IOStatus 等。一般地,这种命名法用于类名、结构体名,在有的语言里也用于函数命名。

下划线分隔命名法

所有单词使用下划线分隔,全部单词小写或大写。这种命名法的例子有:MINUMUM_REQUIRED_VERSION GRID_SIZE usb_client out_of_range 等。一般地,大写命名用于定义,小写字母用于文件名,有时也用于函数形式参数。

匈牙利命名法

用一些小写字母的前缀指明该变量的数据类型,紧随其后是一些无缝衔接的单词,其中首字母大写,后续字母小写。这种命名法的例子有:chGrade nLength bOnOff strStudentName 等。一般地,只有动态类型语言(比如 Visual Basic,JavaScript 等)推荐使用这种命名方法,因为这样可以显式地指明它们的类型从而避免错误。

推荐命名风格

关于命名的风格和建议,我们已经在数据成分的结尾讲了很多了。这里列出本书的基本命名风格:

  1. 一般的变量名和函数名(对象名)使用小驼峰命名法;
  2. 命名空间、枚举名和枚举项使用大驼峰命名法;
  3. 结构体名(类名)使用大驼峰命名法;
  4. 宏、只读变量使用大写的下划线分隔命名法;
  5. 文件名使用小写的下划线分隔命名法。

不过除此之外,还有一些关于命名的建议,主要针对如何起一个严谨的、易于理解的名字。

在写代码的时候,任何名字都要有意义

比如:

int num, sum, result; // 简单的标记
bool isOK, isFinish, shouldExit; // 布尔型使用isXX的命名,与其含义对应
int sumOfPeoplesWhoHaveDisease; // 也不建议太长的名字,看起来费劲。

如果非要写单字母变量,比如在考试、竞赛等追求效率的场合,也建议有一定的含义:

int n, s, r; // 可以指 number, sum, result
int a1;      // 不好

尽量避免汉语拼音命名

尽量避免汉语拼音命名,更不能使用拼音首字母。由于汉语拼音在缺乏音调时拥有歧义性,阅读起来有一定困难。

int cxjg;         // 这是啥?
int chaXunJieGuo; // 还是有些模糊
int queryResult;  // 就比较好

非成员的函数命名建议使用祈使语气

祈使语气即 动词-宾语 的结构,如:

bool checkResult();
void getData(int seed);
int calculateSum();
void f();              // 这又是啥?

慎用非英文命名

某些编译环境(比如 VS)支持非英文字母的声明符。你可以使用中文命名,但是可能会出现编码问题等难以调试的错误。而且在团队编程中,不能保证他人的编译环境也支持英文命名。因此如非必要,仍然建议使用英文命名。

int 最终结果;       // 或许可以,但不建议
void 初始化();      // 或许可以,但不建议
void доСвидания(); // 同志,不要为难自己

缩进

关于缩进,我们在感性认识章节已经有所讨论了。整体来说,对于一个复合语句要进行缩进。缩进可以为 4 个空格,或者一个制表符 Tab。

if (condition) {
    // four spaces
    i++;
}

对于非复合语句的 if else forwhile ,也可以换行后缩进:

for (int i{0}; i < n; i++)
    a[i] = i;

有些情况需要逆缩进,即向左取消缩进。这种情况一般只发生在非 switch 的标号和成员访问说明符上。

int main() {
    int i = 0;
loop: // 向左取消缩进
    i++;
    if (i < 10) goto loop;
}
struct Student {
public: // 向左取消缩进
    std::string name;
    int no;
private: // 向左取消缩进
    double gpa;
};

也有一些场合使用 2 空格缩进,但如今很少用场合使用 Tab 缩进(基本上只有 Makefile 要求必须如此)。因此建议读者使用空格缩进,可以将键盘上的 Tab 键映射为 4 个空格的输入。

空格与空行

C/C++非常不同于当时的其它语言(如 BASIC、Pascal 等),它频繁地使用各种各样的符号而非字母作为语言的一部分。比如下面这个程序:

PascalC/C++
program division;
var a,b,c,d,i:integer;
begin
    for i:=1 to 5 do
    begin
        readln(a,b);
        if not(b=0) then
        begin
            c:= a div b;
            d:= a mod b;
            writeln(c,' ',d)
        end
        else writeln('div by 0');
    end;
end.
```
// division
#include <stdio.h>
int main(){
    int a,b,c,d,i;
    for(i=1;i<=5;i++){
        scanf("%d%d",&a,&b);
        if(!(b==0)){
            c=a/b;
            d=a%b;
            printf("%d %d\n",c,d);
        }
        else printf("div by 0\n");
    }
}
```

可以看到 C/C++ 运用了更多的类似 % & ! # <> {} 等符号,而同时代的 Pascal 语言却大多用 not then begin end div mod 等英文单词来实现这些功能。这就导致了相同的程序,用 C/C++ 写会更加紧凑,可读性也相对较差。这就体现出空格的重要性了:空格有助于将紧凑的符号语言拆散。比如:

a==c&&c!=b?(max++,false):check(a);
a == c && c != b ? (max++, false) : check(a);
a == c  &&  c != b ? ( max ++ , false ) : check ( a ) ;

第二行就会少一些紧张感,更易阅读;但第三行又显得过于松散。因此我们建议以下的空格规范:

  1. 单目运算符与它的操作数之间不要保留空格。比如 a++ !flag check()
  2. 非逗号的双目运算符和三目运算符在操作数和运算符之间留一个空格。比如 a == b 3 + 5 cout << endl
  3. 逗号运算符和形/实参列表中的逗号靠左,右侧留有空格(和英文书写规范相同)。比如 max(a, b) int calculate(int, float, bool);
  4. 括号与表达式之间不必留有空格,句尾的分号也不用留空格。

另外,空行也可以让程序变得不那么难懂。你可以在全局变量与函数之间,函数与函数之间留下一行空行,可以让界限变得清晰。如果函数内部有一大块是在完整地做一件事情,也可以前后加一些空行突出,并用注释表明它们在做什么。

大括号

“大括号换行”是编程届历史最悠久、持续时间最漫长的争议之一。因为这个问题,大家分成了两派:“换行派”认为左侧大括号{应当另起一行:

if (condition)
{
    // my code ...
}

而“不换行派”认为左侧大括号{应当接在上一行的末尾:

if (condition) {
    // my code ...
}

“换行派”认为换行之后,程序不会显得很“挤”,同时上下大括号的位置相同方便对应,称之为“良好的对齐”。而且对于按照总行数算绩效的企业,换行有助于获得更高的薪水。(当然这里是开玩笑,请不要为此这样做。)

“不换行派”认为不换行可以保证程序“单行入栈、单行出栈”,避免视觉上的冲断。它们认为将单独一行留给大括号是多余的。而且,大括号不换行在版本控制时可以获得相对更少的冲突。 某些编程语言对于大括号是否换行给出了标准的规范。比如 Java 约定大括号不要换行,而 C# 却建议大括号另起一行。很不幸,C/C++ 没有给出这方面的建议,因此你在独立编程时可以随意选择你的风格,甚至可以混用两种风格。

本书出于压缩篇幅的目的,大括号基本不换行。当然,你也会见到一些教材的大括号换行,甚至推荐读者使用某一种风格。这里,我想引用《C++ Primer》的一句话:“你追随那种风格并不重要,重要的是坚持一种风格走下去。”

指针声明

我们已经知道了指针可以类似这样声明:

int* ptr; // 星号左对齐

但其实还可以用这些方式声明:

int *ptr;  // 星号右对齐
int * ptr; // 星号居中
int*ptr;   // 不留空格

这四种声明风格是完全等价的。事实上,除了最后一种,前三种风格都大有人用。其实这些风格代表了不同人对C/C++ 这一语言中指针的理解。

int* ptr; 这种写法代表了 C++ 程序员对指针的理解。他们认为 int* 整体为 ptr 的类型,因此声明的时候 int* 应该写在一起。他们的理解可以说是有道理的,因为只有 int* 合在一起才构成了 类型标识,它能够出现在 reinterpret_cast 等场合中;剩下的 ptr 就是单纯的名字,不包含任何其他的声明符成分。一些其他的面向对象语言也认同这种观念,如 C# 语言的数组声明形如 int[] arr; ,表明 int[] 是一个类型。即,C++ 程序员认为类型是更重要的,不能将一个独立的类型分开来写,也不能将它和名字混在一起。

int *ptr; 这种写法代表了 C 程序员对指针的理解。他们认为 int 就是表达式 *ptr 的类型,因此 ptr 就是一个可以解地址得到 int 的类型。它们的理解也是有道理的,因为这符合 C 语言设计的初衷。C 语言认为指针、数组和函数的声明其实是在声明表达式——比如 int f(int, int); 是因为 f(1,1) 表达式的结果为 intint arr[10]; 是因为 arr[0] 表达式的结果为 int 。复杂的数组指针、函数指针的声明方式也体现了这种思想。即,C 程序员认为表达式这一概念更重要,声明则是表达式的求解过程。

int * ptr; 这种写法代表了函数式泛型编程的观点。它们认为 * 是作为一个模板类型别名,它接受一个类型,并转换为这个类型的指针。因此,它既不属于 ptr 的“表达式的部分”,也不属于 int 的类型的一部分。它们的理解也是有道理的。C++ 也支持这样的写法:

template<typename T>
using Pointer<T> = T*;

就可以使用 Pointer<int> ptr 这种声明。

因为 C++ 是一种“不限制程序员思维”的语言,它既继承了传统的 C 风格思想,又在面向对象、泛型编程、函数式编程有所拓展。所以说这三种方式都有它们的道理。本书为了加强读者对指针类型的理解,一律采用星号左对齐的写法。但是遇到函数指针和数组指针时,由于括号的限制,就不得不使用右对齐的写法了。但是,我们现在很少用到这两种指针,所以不必特别纠结。

命名空间

using namespace std; 并不是良好的编程习惯。因为 std 命名空间中存在太多的声明,很容易与我们自定义的变量发生命名冲突。如果不使用 using namespace std; 的话,可以有以下两种写法:

一是对标准库的对象使用 std:: 前缀:

#include <iostream>
int main() {
	std::cout << "Hello, world!" << std::endl;
}

二是对经常需要的标准库对象,用 using 引入:

#include <iostream>
using std::cout;
int main() {
    cout << "Hello, world!" << std::endl;
}

我这里并不是禁止使用 using namespace std; 的意思。在考试、竞赛等追求效率的场合当然可以使用;但是在一些项目的开发过程中,尤其是结构复杂、声明较多的项目,希望大家不要盲目 using namespace std; ,将命名空间的作用发挥好,避免冲突。

最近更新:
代码未运行