您的位置:首页 > 编程语言 > C语言/C++

C/C++编码标准(转载)

2009-04-24 17:20 232 查看
             C/C++编码标准
 
  
1.   绪论
本文档定义C/C++程序的编码风格。良好、统一的编码风格可以极大地提高代码的可读性和可维护性,并能很好地预防缺陷(包括BUG) 的产生,进而有效地改善软件的品质。所以应尽可能的采用那些已经被证实可提高代码可读性及可维护性的约定风格。
用C/C++编写的代码在进行代码评审(代码走查,CDI CoDe Inspection)前,编程人员必须保证其代码是按照这些书写风格编写的。这是评审的先决条件。在本文档的最后附有一份检查表,用以帮助确认代码的风格。在代码评审过程中,如果代码没有按约定的风格编写,将被看作是一个缺陷。
2.   文件的内容。
应按程序的功能来划分文件。每一个文件应仅包含一个相互关联的功能集合,应避免功能分散在多个文件中。如果不同文件包含相似的功能,那就应考虑将公共功能提取出来,放到一个单独的文件中,以提高代码的共用性。如对C++程序,每一个文件中应仅存放一个类或一组紧密相关的类。
应避免强耦合的函数或类在多个文件中实现。如果两个对象是强耦合的以至于必须相互联合才能使用,那么就应该将其放到同一个文件中。
在头文件(.h 后缀文件)中声明公共接口,在代码文件(.c,.cc, .cpp后缀文件)中定义功能的实现。典型的例子:你在一个文件中编写的一个相对独立的功能集,应该有和它相对应的头文件或接口文件,这样其他人就可以将你所编写的头文件包含在他们的代码中来利用你的功能。
#include语句的声明必须严谨。显式地包含进必需的头文件,并且仅在必需的地方包含它。例如:如果你要调用一个外部函数,就应将其相对应的头文件包含在你的代码文件中,而不是将其包含在代码文件相应的头文件中。只有当你的公共函数接口或数据类型定义需要用到其它文件中的定义时才将该文件包含在你自己代码的头文件中。
应避免单纯为了方便而将一些 #include 指令放在一个头文件中使用。#include嵌套会造成使阅读者难以理清的文件依赖关系,而且这样也会造成包含了顶层头文件的模块之间的耦合。所以最好在需要时单独使用 #include 指令,而不要将这些#include 指令放在一个头文件中嵌套使用。除非那些模块紧密相关且用到同样的 #include 指令。
 
 
 
 
 
2.1            头文件或接口文件的内容。
  头文件所包含的内容及顺序如下:
1) 版权声明注释。
2) 模块摘要注释。
3) 修改信息注释。
4) 多重包含的条件预处理指令 #ifdef 。
5) 其它预处理指令,如#include 、#define。
6) C/C++ #ifdef/#endif 指令。
#ifdef _cpluplus
extern “C” {
#endif
7) 数据(类、结构)类型定义。
8) 类型定义。
9) 函数声明。
10)    C/++ #ifdef/#endif 指令。
#ifdef _cpluplus
}
#endif
11)    多重包含的条件预处理指令 #endif.
例1.        标准 C  头文件风格
/*
 * Copyright (c) 1999 Fred C. Richards.
 * All rights reserved.
 *
 * Module for computing basic statistical measures on
 * an array of real values.
 *
 *  $Id$
 */
 
#ifndef STATISTICS_H
#define STATISTICS_H
 
#include <math.h>
#include <values.h>
 
#define MAXCOMPLEX { MAXINT, MAXINT }
 
#ifdef _cplusplus
extern "C" {
#endif
 
struct complex {
     int r; /* real part */
     int i; /* imaginary part */
};
typedef struct complex Complex;
     ...
 
/*
 * Compute the average of a given set.
 * Input - array of real values, array length.
 * Output - average, 0 for empty array.
 */
float
ave(float* v, unsigned long length);
 ...
#ifdef _cplusplus
}
#endif
 
#endif /* STATUS_H */
 
2.2            代码文件。
 C/C++ 的代码文件遵循与头文件类似的结构,应包含如下顺序的内容。
1) 版权声明注释。
2) 模块摘要注释。
3) 预处理指令,如#include 、#define。
4) 修改信息变量。
5) 其它的模块特定的变量定义。
6) 局部函数接口原型。
7) 类/函数定义。
 
不像在头文件中那样修改信息写在注释中 ,在代码文件中修改信息应保存在程序变量中。这样就可以鉴别编译后的目标文件的源代码的版本。在C 语言中用法如下:
static const char rcs_id[] __attribute__ ((unused)) =
"$Id$";
 
其中__attribute__使得编译器不会报告变量从没使用的错误。
在c++ 语言中使用如下的改版信息。
static const char rcs_id[] __attribute__ ((unused)) =
"$Id$";
在每一个函数或类的方法实现的开头加一个换页符,这样可保证在打印程序代码是每一个函数都从新的一页开始。
 
例 2. 标准C++ 代码/实现文件。
 
//
// Copyright (c) 1999 Fred C. Richards.
// All rights reserved.
//
// Module for computing basic statistical measures on
// an array of real values.
//
 
#include "Class.h"
#include <string>
 
namespace {
     const char rcs_id[] = "$Id$";
}
 
// Utility for prompting user for input.
string
get_user_response();
 
^L
Class::Class(const int len)
{
     private_array_ = new[len];
}
 
Class::~Class()
{
     delete private_array_;
}
 
^L
...
 
3       文件格式。
这里介绍的格式风格是 Stroustrup 的 The C++ Programming Language 一书中最基本的一种。如果你使用的是 Emacs 编辑软件,通过在 .emacs 文件中加入以下代码,你就可以将该风格设置为默认风格。
(defun my-c-mode-common-hook ()
   (c-set-style "stroustrup"))
 
(add-hook ’c-mode-common-hook ’my-c-mode-common-hook)
 
安排好代码的布局,这样通过代码的空间结构就可以勾画出代码的逻辑结构。用空行将代码分成不同的小功能段,这样每一小段都有一个完整的、特定的含义,思路清晰。用文字缩排来表现其逻辑关系。函数之间也要留出一定的空间。每一代码块应只作一件事情。
所有的函数定义、声明都要顶格写,将函数返回类型、接口信息(函数名及参数)和函数的开始括号(‘{’)分别放在不同的行上。如果函数体比较长应将函数名作为注释放在函数体结束括号(‘}’)后边。
 
例 3。函数定义、声明的书写格式。
 
void
debug(const string& message);
 
int
Class::method(const int x, const string& str)
{
   .
   .
   .
} // method
 
 
除 ”->”,”.”, ”()”和”[]”这些操作符应和操作数紧连以外,其他操作符 应用一个空格将其与操作符分开。如果运算式须跨行书写,这时最好将操作符留在上一行的结尾,而不要将其放在下一行的开头。
统一用水平制表符 Tab 进行缩排,而不要用空格进行缩排,尤其不要用空格和Tab混合进行缩排,因为不同的编辑器对Tab的解释不同,容易造成代码的错落,影响代码的可读性(每次缩排留四个空格),应避免一行长度超过80。如果要分行书写,断点要和乎逻辑自然分行。对接续行的缩排,要考虑到前后的逻辑性。例如:对于函数,应将后续行的参数与参数列表开始扩号对齐书写。
 
例 4.  语句多行书写格式。
new_shape = affine_transform(coords, translation,
                                   rotation);
if ( ( (new_shape.x > left_border) &&
        (new_shape.x < right_border) ) &&
      ( (new_shape.y > bottom_border) &&
        (new_shape.y < top_border) ) )
{
    draw(new_shape);
}
 
代码块分明。对于条件分支语句,即使只有一条语句也应该用大括号将其括住,作为一个代码块。开始括号应放在条件句的最后,条件句分行写时,开始括号单独放在下一行的开头(如上例)。
例 5.  完全括起来的块风格。
if (value < max) {
    if (value != 0) {
        func(value);
    }
} else {
    error("The value is too big.");
}
 
尽管那些只有一行的块看起来有点冗余,但是这样做在它们以后扩充时会大大减少出错概率。
 
3.1             C++ 的独特点。
在定义类时,public, protected, private 和friend 定义要顶格书写。对所有结构的公共域应该用 public 显式说明,对类的所有私有成员也要用private 显式说明。
类成员的定义顺序如下:
1) 声明public数据成员和类型定义。
2) 声明private或protected数据成员、或函数成员初始化列表及inline函数中用到的类型定义。
3) 声明所有的public成员函数(以构造函数、析构函数开头)。
4) 声明剩余的private或protected数据成员、数据类型。
5) 声明private或protected成员函数。
6) 声明friend函数。
 
对于简单的inline函数可将声明和定义放在同一行上,而对于较长的inline函数,可用缩进的块式风格书写。总之,应避免将复杂的实现代码放在头文件中。
 
例 6.  类声明格式。
 
 
class Type : public Parent {
private:
    int x_;
    int y_;
public:
    Type();
    Type(int x) : x_(x) { }
    ~Type();
    int get_x() const { return x_; }
    void set_x(const int new_x) { x_ = new_x; }
    ...
    void display() {
         ...
    }
}
 
 
 
 
 
 
 
4. 选择有意义的名字。
4.1            变量名。
所有的变量名都采用小写字母组成。对于含有多个单词的变量名,在单词间用下划线(’_’)分开,或者采用匈牙利命名法,即单词的首字母大写。采用缩写的方法命名时,应用注释的方式说明缩写前的单词组成。常量名采用大写字母组成(如:常数或枚举类型)。
认真选取变量名。研究发现变量名的选择对调试代码所用的时间影响很大,但遗憾的是目前还没有关于选取变量名的成型的规则。这里列举几条概要的规则:
1) 注意一致性。最重要的一点就是为你的代码建立一种清晰、易懂的风格,这样才能让其他的人尽可能快的理解你的思路及意图。
2) 类型相近的数据用相似的名字,反之,类型相差较远的则采用相差较大的名字。
3) 应避免使用多个读音相近的变量名,而且不要仅仅依靠大小写来区分两个变量。
4) 变量名应说明它代表什么,而不是说明它怎么用(比如:变量名尽量用名词)。变量名应采用应用领域的术语,避免采用反映编程细节的计算机术语。
5) 避免使用象 tmp,buf, reg 这样笼统的名字。
6) 避免使用象 lo ,lite 这样拼写错误的名字。
 
总之,短的名字可以用于生存期较短的或在C/C++中有公共用法(如:下标变量 i, j, k  等)的变量,简短明了有助于提高代码的可读性。而那些有独特、重要用途或在你代码重要的地方出现的变量,就应该采用一个完整的描述性的名字。研究表明变量名长度在10~16之间时,调试代码所需的时间最短。
 
4.2            函数名。
与变量名一样,公共函数名采用小写字母书写,单词间用下划线“_”分割开,或者采用匈牙利命名法,即单词的首字母大写。采用缩写的方法命名时,应用注释的方式说明缩写前的单词组成。
对于没有返回值的函数(即返回值类型为void),采用能确切表达函数目的的动词。最常用的如将动词的宾语包含在函数名内。例如:
void
remove_dc_offset(short *signal, const unsigned long length);
 
void
set_output_gain(const float gain);
 
因为函数较之于变量要达到更复杂的目的,所以长名字更可取。
如果函数有返回值,那么用一个能表示返回值意义的单词来作函数名会更好。例如:
/*
 * Compute the DC offset of the given signal.
 */
float
dc_offset(const short * const signal,
            const unsigned long length);
/*
 * Poll the D/A and return the current gain setting.
 */
float
gain(void);
 
 总之,变量/函数名的选取应本着提高代码的可读性的原则。尽量做到见其文知其义。
 
4.3             类、结构及类型定义。
这里所说的名字的约定风格,都是 Stroustrup 在它的关于C++的书中最基本的。
 
将你定义的数据类型名的第一个字母大写,这包括结构、类、类型定义和枚举。象 C 语言一样用下划线作为单词与单词的分割。
类的实例变量用小写字母开头。同样,用下划线分割各个单词。对public和protected成员(变量和函数)应用同样规则。而对private成员在其名字后加下划线结尾。
 
例 7. 用户定义类型的大写。
 
/* Straight C */
struct complex {
    int r; /* real */
    int i; /* imaginary */
};
typedef struct complex Complex;
 
// C++ interface example
class Canvas {
public:
     enum Pen_style {
          NONE = 0,
          PENCIL,
          BRUSH,
          BUCKET
     };
     Canvas();
     ~Canvas();
     void set_pen_style(Pen_style p);
     ...
private:
     int cached_x_; // to avoid recomputing coordinates
     int cached_y_;
};
 
// C++ usage example
 
Canvas sketch_pad;
 
sketch_pad.set_pen_style(Canvas::BRUSH);
 
在使用C++类和对象时应注意冗余的名字元素。牢记一点:没有必要在类成员元素的名字中重复类的信息,因为类成员是由类实例名字来标识的。
  例 8.  下例中的变量名选取不合适。
 
// Notice how redundant "stack" becomes.
 
template <Type>
class Stack {
public:
     int stack_size;
     add_item_to_stack(Type item);
     ...
};
 
Stack my_stack;
 
my_stack.add_item_to_stack(4);
int tmp = my_stack.stack_size;
 
 
5.注释
通常情况下,好的代码能够把自己表达清楚。清楚简明的变量和函数名称,一致的(代码)格式和空间(布局)结构,清晰的语法结构,所有这些共同构成了易读的代码。然而,有时候复杂的逻辑结构还得借助于清晰的描述。注意不要使用注释来弥补你的代码中的不足。如果你发现你的代码需要许多注释或者它常常难于描述,那么你应该重写这些代码以使它更加简单和清晰。
 
5.1.风格
对于纯C代码,使用/*…*/风格的注释。对于C++的代码,使用//…风格的注释。遵循这些约定,你可以通过使用下列命令快速地估计出代码中注释的行数。
% grep "^[ /t]*/* "
% grep "^[ /t]*////"
太少或太多的注释都预示着代码可能难于维护。
除了变量声明和标注 #if / #endif 声明,尽量避免在行尾使用注释。注释单独占一行。对于较长的描述复杂逻辑的注释,最好使用块风格的注释把它从代码中分离出来。使用块风格的注释描述函数。使用大块注释(Bold Comment)把你的代码文件划分为几个主要部分。在所有的大块注释(Bold Comment)和介绍函数的块注释前面插入换页符,从而使他们出现在打印页的页首。下面的例子说明了C风格中的不同注释类型:
 
Example 9. C comment types
^L
/*
 * ************************************************
 * Bold comment.
 * ************************************************
 */
/*
 * Block comment.
 */
/* Short (single-line) comment. */
int i; /* end-line comment */
 
5.2. 内容
使用行尾注释来描述变量声明。使用注释来描述任何无法直接通过名字来理解其用途的变量。
使用注释来表达你的意图。不要描述你的代码是如何工作的,因为,它在执行时是显而易见的。相反应该描述为什么要这么做。避免使用注释来解释那些技巧性的代码。相反,要重写这些代码以使其清晰易懂。注释要使用完整的有恰当的拼写和标点的句子。
在你写代码之前或同时写注释,而不要在写完代码后再写注释。如果你是从写注释开始的,你可以为你的代码实现作出底层次的设计。当你完成代码测试后,从头回顾一下你所有的注释以确认他们仍然是正确的。
对那些有广泛影响的东西写出注释(说明)。如果一个函数假设了输入变量的条件,请注释出来。如果由于速度优化的需要而使代码难于读懂,也要在注释中解释这种要求。如果你的代码使用了或改变了任何全局变量,也都请注释出来。
使用大块注释(Bold Comment)把你的代码文件划分为几个主要部分。例如,你可以实现一些私用的实用函数。使用大块注释(Bold Comment)来标注代码的开始。在每个函数前面使用块注释来描述函数功能、输入变量的意义以及返回值的意义。没有必要包含函数名,因为它就在注释下面。
 
Example 10. Commenting functions and function groups
^L
/*
 * ****************************************
 * Statistics utilities used to
 * optimize performance on the fly.
 * ****************************************
 */
/*
 * Compute the standard deviation or "variance"
 * for a set.
 *
 * Input: v - set of values and set size.
 * len - size of input set.
 * Output: Dev = Expect (x - x_ave)^ 2
 * 0 for the empty set
 */
static float
std_dev(const float *v, const unsigned long len)
{
    ...
}
 
使用行尾注释来标注一段非常长的代码块的结束。这也同样适用于函数和条件代码。包括用于 if/else 分支和 for/while/do 跳转的控制条件。
也可以使用行尾注释来标注由#if或#ifdef声明的部分所对应的#endif的结尾。在注释中包含把代码分为几部分的条件。当使用#else条件时,#else和#endif都要使用否定条件标注出。
 
Example 11. Commenting long code blocks
#ifdef DEBUG
.
.
.
#else // not DEBUG
void
function()
{
if (position != END)
    .
    .
    .
} // position != END
.
.
.
} // function()
#endif // not DEBUG
 
6. 语法和Language Issues
随后的部分略述了一些通用的实例. 请假设其他人将不得不阅读和维护你的代码,并通过你的注释来得到帮助。同样,请假定错误和过失是不可避免的,因此尽可能快地隔离它们并抑制它们的影响。后面的这些实例不时被作为“防火墙” 式的代码提及。宽松的检查函数传入的参数的合法性, 并总是检查你所调用的函数的返回值。
 
6.1. 概要
避免在同一行中放置多条指令。一行只做一件事。这也适用于关于分支和循环跳转的控制声明。考虑如下例子:
 
/* Bad practice! */
if (!eof && ((count = get_more()) > min_required) {
    ...
}
 
这就需要重写,将取得剩余数据的动作和检查是否有剩余数据的任务分离开来。
 
/* Safer version */
if (!eof) {
count = get_more();
if (count > min_required) {
    ...
}
}
 
避免使用副作用。下面这一行可能由于使用不同的编译器而产生不同的结果。
a[ i] = i++; /* a[ 0] or a[ 1] == 1 ? */
 
再次强调,每一行应该只包含一条语句,而且每一条语句只做一件事。
避免类型转化,并且永远不要把指针转化为void*类型。C++的强项之一就是强大的数据类型和支持任意的数据类型的能力。如果你觉得有必要将数据转化为其他的类型,或许你应该考虑定义一个继承关系或使用模板。
不要用指针来定义任何数据类型(例如, typedef char* String)。
避免使用预处理指令定义常量(也就是 #defines)。通过使用恰当的C/C++数据类型加上const前缀来代替,并使用他们。对于一些相关的整数常量可以定义为枚举(enum)类型。通过以上技巧可以使用编译器执行预编译所不能实现的数据类型检查。
尽可能的限制变量的作用域。在C++中,使用括弧来组织功能和临时变量。在将要使用前声明变量,在用完后马上释放变量。
在分支和循环跳转控制结构中,使用圆括弧对逻辑条件进行组织。大多数人并不十分熟悉运算符的优先级,使用圆括弧可以使人们更加容易地解析理解逻辑运算。不加入额外的圆括弧就不能明确地表示出下面的第一个例子与其它三个例子(它们是等同的)的区别,尽管这只是3个布尔变量x,y,z的八种组合中的一种。
Example 12. One of these things is not like the other
(x || y && y || z)
(x && y || z)
( (x && y) || z )
( (x || z) && (y || z) )
 
6.2. 结构化编程
尽可能使你的代码保持清晰的结构。不要从库函数中调用exit()函数。使用return配上恰当的错误状态码。每个函数只调用return一次。避免使用break和 continue跳出循环和分支代码。考虑用增加或修改控制退出的条件来代替。不要使用goto。
当分支条件不是很琐碎时,建议使用if/else/else/...代替switch/case/case/...。对于这两种结构来说,使用default条件只有在处理合法的默认值时或者在没有默认值时生成错误状态。只有当事件有着同样的代码以至错误显而易见时,使用条件交迭的switch/case块。
建议使用while() { ... }代替do { ... } while();如果人们能够在进入循环之前知道退出条件将更易于理解控制结构。do { ... } while();结构把退出条件隐藏在了循环的末尾。
避免过长的控制结构。如果你发现循环或分支结构占了许多打印页或屏幕,请考虑重写这个结构或者创建一个新的函数。最少也要在结构的末尾加上注释来标示条件的结束。
避免代码深度嵌套。人们在同一时间跟踪三、四个事件是十分困难的。尽量避免那些用三、四层缩进结构来表示一条常见规则的代码结构。再次强调,如果你的代码中有太深层次的逻辑时,还是考虑创建一个新的函数吧。
避免使用全局变量。他们会使你的代码在多线程环境中难于被支持。如果你确实使用了全局变量,要明白他们会对你的模块的重入产生什么样的影响。
6.3. 函数和错误检查
不要使用预处理宏实现函数。有太多的问题与他们有关,而且现代计算机的速度和编译器的优化已经消除了任何由预处理宏来实现函数所带来的好处。用函数来代替它。
为所有函数写出声明/原形,并把他们或者作为公共函数放在.h文件中,或者作为私有,内部函数放在.c文件头部。函数声明列表看上去应该象你的代码的一个目录表。
描述清楚你的程序所用输入数据的假设条件。用assertions来测试编程错误,使用exceptions(C++)或返回值(C)来报告正常使用中产生的错误状态。不要在assertions中放入任何实现逻辑,因为他们在最后的发布代码中不会被保留(特别是库函数)。当一个库函数必须报告计算结果和清晰的错误码时,通过变量传递计算结果,return错误码。
尽可能使用系统、基盘或本程序其它部份提供的库函数/公用函数。在要利用一项功能之前应尽可能调查系统、基盘或本程序其它部份是否已经提供了实现相同/相近功能的函数,如有,则使用它,而不要重新实现它,除非证明现有的函数不能满足功能或性能方面的要求。
尽量用公共子程序去代替重复的功能代码段。
用调用公共函数去代替重复使用的表达式。
检查所有库函数调用的返回值,这一点对那些提供系统资源访问的库函数(例如,malloc(), fopen() 等.)尤其重要。
确保所有的变量在使用前都有确定的初始值。尤其是,声明指针变量后,应立即对其进行初始化,如:char*pointer = NULL;
使用指针之前必需判断其是否为非空,如:if ( NULL == pointer) …;
释放内存后应立即将指针变量置为空,如:
  free(pointer);
  pointer = NULL;
用常量对变量进行相等判断检查时,应将常量放在布尔表达式的左边,如:
if ( NULL == pointer )
而不要写成
if ( pointer == NULL )
这样,一旦判断表达式误写成赋值表达式NULL = pointer,编译时就能发现错误,而不是等到运行时花费大量的时间去跟踪调试。
  使用数组变量时一定要注意(判断) 数组的下标值,避免数组越界的发生,尤其在分配字符串型数组时,不要忘记所分配数组的大小应比字符串的实际长度大1。另外,在进行字符串操作时,也应十分注意越界的问题,如:
strcpy(a,b); 中的a的空间大小不能小于b的长度+1,strcat(a,b); 中a的空间大小不能小于 a的长度+b的长度+1。等等。
生成说明性的错误信息。这些信息应该是用户可以理解的。而且简洁完整,避免使用计算机技术词汇。并指出引起错误的可能的原因。在UNIX/Linux环境中应在出错信息中尽可能地包含进errno的值,在Windows环境中则应尽量在错误信息中加进GetLastError() 的返回值。
 
7.结论
本文档所述规范适用于所有新生成的代码。在同其他人共同编程时遵守这些规范尤为重要。
本文档的最后附加了一个检查表。它包括了大部分被提到过的条目。在对你的代码实施REVIEW之前,请对照一下该检查表。任何unaddressed issue都被认为是编码缺陷(失误)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐