前言
本文由林锐编写的《高质量 C/C++ 编程指南》整理而来。希望帮助大家,快速了解一些简单编码规范。
C/C++ 编程风格有很多,它们之间并没有对错之分,重要的是保持统一编程风格。统一规范的目的是加强代码的一致性,让其他程序员可以快速读懂你的代码。本文内容比较简单,如果想深入了解 C++ 编程风格,可以看谷歌的:C++ 编程风格指南
一、文件结构
每个 C/C++ 程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。
C/C++ 程序的头文件通常以 “.h” 为后缀,C 程序的定义文件以 “.c” 为后缀,C++ 程序的定义文件通常以 “.cpp/.cc” 为后缀。
1.1 头文件的结构
- 【规则 1-1-1】为了防止头文件被重复引用,应当在头文件的第一行添加
#pragma once
,更常见的做法是用 ifndef/define/endif 结构产生预处理块 - 【规则 1-1-2】用 #include <filename.h> 格式来引用标准库的头文件
- 编译器将在标准库目录搜索,该目录保存在系统的环境变量中
- 【规则 1-1-3】用 #include “filename.h” 格式来引用非标准库的头文件
- 编译器将在当前的工作目录搜索,如果找不到会到标准库目录搜索
- 【建议 1-1-1】头文件中只存放声明,而不存放定义
#prama once // 防止头文件被重复包含
#include <stdio.h> // 引用标准库的头文件
#include "myheader.h" // 引用非标准库的头文件
void Function(...); // 全局函数声明
struct ClassName { // 类结构声明
...
};
1.2 定义文件的结构
定义文件有两部分内容:
- 对头文件的引用
- 程序的实现体(包括数据和代码)
#include "myheader.h" // 引用头文件
// 全局函数的实现
void Function(...) {
...
}
// 类成员函数的实现
void ClassName::Fun(...) {
...
}
1.3 头文件的作用
- 通过头文件来调用库功能
- 在很多场合,源代码不便(或不准)向用户公布,只向用户提供头文件和二进制的库即可
- 用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的
- 头文件能加强类型安全检查
- 如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一规则大大减轻程序员调试、改错的负担
二、程序的版式
版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。
2.1 空行
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。
- 【规则 2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行
- 【规则 2-1-2】在一个函数体内,逻辑上密切相关的语句之间不加空行,其他地方应加空行分隔
2.2 代码行
- 【规则 2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句
- 【规则 2-2-2】if、for、while、do 等语句独占一行,执行语句不得紧随其后。不论执行语句有多少都要加 { },这样可以防止书写失误
- 【建议 2-2-1】尽可能在定义变量的同时初始化该变量
2.3 注释
注释通常用于:
- 函数接口的说明
- 重要代码行或段落的提示
- 【规则 2-3-1】注释是对代码的「提示」,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱,注释的花样要少
- 【规则 2-3-2】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性,不再有用的注释要删除
- 【规则 2-3-3】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可以放在下方
- 【规则 2-3-4】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读
三、命名规则
- 【规则 3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行解码
- 【规则 3-1-2】程序中不要出现仅靠大小写来区分的标识符
int x, X; // 变量 x 与 X 容易混淆
void foo(int x); // 函数 foo 与 FOO 容易混淆
void FOO(float x);
- 【规则 3-1-3】变量的名字应当使用「名称」或者「形容词 + 名词」
float value;
float oldValue;
float newValue;
- 【规则 3-1-4】全局函数的名字应当使用「动词」或者「动词 + 名词」
- 类的成员函数应当只使用「动词」,被省略的名词就是对象本身
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
- 【规则 3-1-5】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等
int minValue;
int maxValue;
int SetValue(...);
int GetValue(...);
- 【规则 3-2-1】类名和函数名用大写字母开头的单词组合而成
class Node; // 类名
class LeafNode; // 类名
void Draw(...); // 函数名
void SerValue(...); // 函数名
- 【规则 3-2-2】变量和参数用小写字母开头的单词组合而成
int drawMode;
float minValue;
- 【规则 3-2-3】常量全用大写字母,用下划线分隔单词
const int MAX = 100;
const int MAX_LENGTH = 100;
- 【规则 3-2-4】静态变量加前缀 s_(表示 static)
static int s_initValue; // 静态变量
- 【规则 3-2-5】全局变量加前缀 g_(表示 global)
int g_howManyPeople; // 全局变量
int g_howMuchMoney; // 全局变量
- 【规则 3-2-6】类的数据成员函数加前缀 m_(表示 member,也可以加前缀 _ 或 后缀 _),这样可以避免数据成员与成员函数的参数同名
void Object::SetValue(int width, int height) {
// 或者 _width、width_
m_width = width;
m_height = height;
}
四、表达式和基本语句
4.1 运算符的优先级
- 【规则 4-1-1】如果代码中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级
word = (high << 8) | low;
if ((a | b) && (a & c)) { }
4.2 复合表达式
如 a = b = c = 0
这样的表达式称为复合表达式。
允许复合表达式存在的理由是:
-
书写简洁
-
可以提高编译效率
-
【规则 4-2-1】不要编写太过复杂的复合表达式
i = a >= b && c < d && c + f <= g + h; // 复合表达式过于复杂
- 【规则 4-2-2】不要有多用途的复合表达式
// 该表达式既求 a 值又求 d 值,应当拆分为两个独立的语句
d = (a = b = c) + r;
a = b + c;
d = a + r;
- 【规则 4-2-3】不要把程序中的复合表达式与真正的数学表达式混淆
// a < b < c 是数学表达式而不是真正的程序表达式
if (a < b < c)
// 并不表示
if ((a < b) && (b < c))
// 而是
if ((a < b) < c)
4.3 if 语句
4.3.1 布尔变量与零值比较
- 【规则 4-3-1】不可将布尔变量直接与 true、false 或是 1、0 进行比较
- 根据布尔类型的语义,零值为假(记为 false),任何非零值都为真(记为 true)
- true 的值究竟是什么并没有统一的标准
假设布尔变量名字为 flag,它与零值比较标准的 if 语句如下:
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
4.3.2 整型变量与零值比较
- 【规则 4-3-2】应当将整型变量用 == 或 != 直接与 0 比较
假设整型变量的名字为 value,它与零值比较的标准 if 语句如下:
if (value == 0)
if (value != 0)
// 不可模仿布尔变量的风格而写成下面这样
// 会让人误解 value 是布尔变量
if (value)
if (!value)
4.3.3 浮点变量与零值比较
- 【规则 4-3-3】不可将浮点变量用 == 或 != 与任何数字比较
- 因为无论是 float 还是 double 类型的变量,都有精度限制。所以要设法转化成 >= 和 <= 形式
假设浮点变量的名字为 x,它与零值比较的标准 if 语句如下:
// 隐含错误的比较
if (x == 0.0)
// 转化为
// 其中 EPSINON 是允许的误差(即精度)
if ((x >= -EPSINON) && (x <= EPSINON))
4.3.4 指针变量于零值比较
- 【规则 4-3-4】应当将指针变量用 == 或 != 直接与 NULL 比较
- 指针变量的零值是空(记为 NULL),C++11 应与 nullptr 比较
- 尽管 NULL 的值与 0 相同,但是两者意义不同
假设指针变量的名字为 p,它与零值比较的标准 if 语句如下:
// p 与 NULL 显式比较,强调 p 是指针变量
if (p == NULL)
if (p != NULL)
// 不要写成,容易让人误解 p 是整型变量
if (p == 0)
if (p != 0)
// 容易让人误解 p 是布尔变量
if (p)
if (!p)
4.4 for 循环
- 【规则 4-4-1】不可在 for 循环内修改循环变量,防止 for 循环失去控制
- 【建议 4-5-1】建议 for 语句的循环控制变量的取值采用半开半闭区间写法(更加直观)
for (int x = 0; x < N; ++x) // 起点到终点间隔为 N,循环次数为 N
for (int x = 0; x <= N - 1; ++x) // 起点到终点间隔为 N-1,循环次数为 N
4.5 switch 语句
- 【规则 4-5-1】每个 case 语句的结尾不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)
- 【规则 4-5-2】不要忘记最后加上 default 分支,即使程序不需要 default 处理,为了防止别人误以为你忘了 default 处理
五、常量
5.1 为什么需要常量
如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?
- 程序的可读性变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么
- 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误
- 如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错
- 【规则 5-1-1】尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串
#define MAX 100 // 宏常量,结尾不能加分号
const int MAX = 100; // const 常量
5.2 const 与 #define 的比较
C/C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。
但是前者比后者有更多的优点:
- const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)
- 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试(因为宏替换发生在预处理阶段)
- 【规则 5-2-1】在 C/C++ 中只使用 const 常量而不使用宏替换