C/C++语言编程规范

目录

前言

一、文件结构

1.1 版权和版本的声明

1.2 头文件的结构

1.3 头文件依赖

1.4 包含文件的次序

1.5 目录结构

二、程序的版式

2.1 空格还是制表位

2.2 空行

2.3 代码行

2.4 代码行内的空格

2.5 对齐

2.6 长行拆分

2.7 修饰符的位置

2.8 函数参数顺序

2.9 预处理指令

2.10 类格式

2.11 命名空间格式化

三、命名规则

3.1 共性规则

3.2 简单的 Windows 应用程序命名规则

 

四、表达式和基本语句

4.1 运算符的优先级

4.2 复合表达式

4.3 if 语句

4.4 循环语句的效率

4.5 for 语句的循环控制变量

4.6 switch 语句

4.7 goto 语句

五、函数设计

5.1 参数的规则

5.2 返回值的规则

5.3 函数内部实现的规则

5.4 使用断言

5.5 其它建议

六、注释

6.1 类注释

6.2 函数注释

6.3 注释风格

七、其他编程经验

7.1 引用参数

7.2 类型转换

7.3 整型

7.4 预处理宏

7.5 0和NULL

7.6 sizeof( sizeof)

7.7 内存管理

7.8 一些有益的建议

前言

这里参考了《高质量C++C 编程指南 林锐》、《google C++编程指南》以及《华为C++语言编程规范》编写了这份C++语言编程规范文档,以合理使用 C++。

原文链接:https://www.cnblogs.com/linuxAndMcu/p/11303688.html

一、文件结构

每个 C++/C 程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现,称为定义(definition)文件。

C++/C 程序的头文件以 “.h” 为后缀,C 程序的定义文件以 “.c” 为后缀,C++ 程序的定义文件通常以 “.cpp” 为后缀(也有一些系统以 “.cc” 或 “.cxx” 为后缀)。

1.1 版权和版本的声明

版权和版本的声明位于头文件的开头,主要内容有:

1)版权信息;

2)文件名称,标识符,摘要;

3)当前版本号,作者/修改者,完成日期;

4)版本历史信息 。

/*
* Copyright (c) 2019,google
* All rights reserved.
* 
* 文件名称:fileName.h
* 摘 要:简要描述本文件的功能和用法
* 
* 当前版本:1.1
* 作 者:输入作者(或修改者)名字
* 完成日期:2019 年 7 月 20 日
* 
* 取代版本:1.0
* 原作者 :输入原作者(或修改者)名字
* 完成日期:2019 年 5 月 10 日
*/

1.2 头文件的结构

头文件由三部分内容组成:

(1)头文件开头处的版权和版本声明。
(2)预处理块。
(3)函数和类结构声明等。

【规则 1-2-1】为了防止头文件被重复引用,应当用 ifndef/define/endif结构产生预处理块或者#Pragma once

【规则 1-2-2】用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。

【规则 1-2-3】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。

【规则 1-2-4】头文件中只存放 “声明” 而不存放 “定义” 。

在 C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。

【规则 1-2-5】不提倡使用全局变量,尽量不要在头文件中出现像 extern int value 这类声明。

// 版权和版本声明见示上例,此处省略。

#ifndef GRAPHICS_H // 防止 graphics.h 被重复引用
#define GRAPHICS_H

#include <math.h> // 引用标准库的头文件
… 
#include “myheader.h” // 引用非标准库的头文件
…
void fun(…); // 全局函数声明
…
class Box // 类结构声明
{
    …
};

#endif

1.3 头文件依赖

【规则 1-3-1】使用前置声明尽量减少 .h 文件中 #include 的数量。

当一个头文件被包含的同时也引入了一项新的依赖 ,只要该头文件被修改,代码就要重新编译。 如果你的头文件包含了其他头文件, 这些头文件的任何改变也将导致那些包含了你的头文件的代码重新编译。因此,我们宁可尽量少包含头文件,尤其是那些包含在其他头文件中的。

使用前置声明可以显著减少需要包含的头文件数量。举例说明:头文件中用到类 File,但不需要访问File的定义,则头文件中只需前置声明 class File;无需 #include "file/base/file.h"

 

在头文件如何做到使用类 Foo 而无需访问类的定义?

1) 将数据成员类型声明为 Foo *或Foo &;

2) 参数、返回值类型为 Foo的函数只是声明(但不定义实现);

3) 静态数据成员的类型可以被声明为 Foo,因为静态数据成员的定义在类定义之外。

 

1.4 包含文件的次序

【规则 1-4-1】将包含次序标准化可增强可读性,次序如下: C库、 C++库、其他库的.h、项目内的.h。

项目内头文件应按照项目源代码目录树结构排列,并且避免使用 UNIX文件路径 .(当前目录)和 ..(父目录)。例如, google-project/src/base/logging.h 应像这样被包含:#include "base/logging.h 。示例如下:

#include <sys/types.h> // C库
#include <unistd.h> // C库
#include <hash_map> // C++库
#include <vector> // C++库
#include "base/basictypes.h" // 其他库的.h
#include "base/commandlineflags.h" // 其他库的.h
#include "foo/public/bar.h //项目内的.h

1.5 目录结构

【规则 1-5-1】如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。

例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级目录)。

如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

 

二、程序的版式

版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。

可以把程序的版式比喻为“书法”。好的“书法”可让人对程序一目了然,看得兴致勃勃。差的程序“书法”如螃蟹爬行,让人看得索然无味,更令维护者烦恼有加。 所以学习程序的“书法”, 很有必要。

2.1 空格还是制表位

只使用空格,每次缩进 4 个空格。有些人更加青睐每次缩进 2 个空格,也是可以的,这个纯粹看个人喜好,但如果是团队协作的话需要统一。

【规则 2-1-1】只使用空格进行缩进,不要在代码中使用 tabs,设定编辑器将 tab 转为空格。(有些编辑器已经这样默认设置了,例如Qt)

2.2 空行

空行起着分隔程序段落的作用。空行得体(不过多也不过少)能将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。

【规则 2-2-1】在每个类声明之后、每个函数定义结束之后都要加空行。

【规则 2-2-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。

while (condition)
{
    statement1;
    // 空行
    if (condition)
    {
        statement2;
    }
    // 空行
    statement3;
}

2.3 代码行

【规则 2-3-1】 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便写注释。

// 风格良好的代码行
int width; // 宽度
int height; // 高度
int depth; // 深度

// 风格不良的代码行
int width, height, depth; // 宽度 高度 深度

【规则 2-3-2】 if、for、while、do 等语句自占一行,执行语句不得紧跟其后。 不论执行语句有多少都要加{}。这样可以防止书写失误。

// 风格良好的代码行
if (width < height)
{
    doSomething();
}

// 风格不良的代码行
if (width < height) doSomething();

【建议 2-3-3】尽可能在定义变量的同时初始化该变量(就近原则)

如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会导致程序错误。本建议可以减少隐患。例如 :

int width = 10; // 定义并初绐化 width
int height = 10; // 定义并初绐化 height
int depth = 10; // 定义并初绐化 depth

2.4 代码行内的空格

【规则 2-4-1】关键字之后要留空格。像 if、for、while 等关键字之后应留一个空格再跟左括号 ‘(’,以突出关键字。

【规则 2-4-2】函数名之后不要留空格,紧跟左括号 ‘(’,以与关键字区别。

【规则 2-4-3】‘,’ 之后要留空格,如 fun(x, y, z)。如果 ‘;’ 不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)

【规则 2-4-4】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如 “=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”、“^” 等二元操作符的前后要加上空格。

【规则 2-4-5】一元操作符如 “!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。

【规则 2-4-6】像 “[]”、“.”、“->” 这类操作符前后不加空格。

【建议 2-4-7】对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去掉一些空格,如 for (i=0; i<10; i++) 和 if ((a<=b) && (c<=d))

void fun(int x, int y, int z); // 良好的风格
void fun (int x,int y,int z); // 不良的风格

if ((a>=b) && (c<=d)) // 良好的风格
if(a>=b&&c<=d) // 不良的风格

for (i=0; i<10; i++) // 良好的风格
for(i=0;i<10;i++) // 不良的风格
for (i = 0; i < 10; i ++) // 过多的空格

array[5] = 0; // 不要写成 array [ 5 ] = 0;
a.Function(); // 不要写成 a . Function();
b->Function(); // 不要写成 b -> Function();  

2.5 对齐

【规则 2-5-1】程序的分界符 ‘{’ 和 ‘}’ 应独占一行并且位于同一列,同时与引用它们的语句左对齐。

【规则 2-5-2】{ } 之内的代码块在 ‘{’ 右边缩进后再左对齐。

// 良好的风格
void function(int x)
{
    doSomething();
    other();
}

// 不良的风格
void function(int x) {
    doSomething();
    other();
}

2.6 长行拆分

【规则 2-6-1】代码行最大长度宜控制在 70 至 80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。

【规则 2-6-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

【规则 2-6-3】构造函数初始化列表过长,可以按 4 格缩进并排几行。

// 良好的风格
if (theOneThing > ONE
    && theThridThing == TWO
    && yetAnother == LAST)
{
    doSomething();
}

// 良好的风格
MyClass::MyClass(int var)
    : some_var_(var),
      some_other_var_(var + 1)
{
    doSomething();
}

2.7 修饰符的位置

修饰符 * 和 & 应该靠近数据类型还是该靠近变量名,是个有争议的话题。

若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即 x 是 int 类型的指针。

上述写法的弊端是容易引起误解,例如:int* x, y; 此处 y 容易被误解为指针变量。虽然将 x 和 y 分行定义可以避免误解,但并不是人人都愿意这样做。

【规则 2-7-1】应当将修饰符 * 和 & 紧靠变量名。

// 良好的风格
char *name;
int *x, y; // 此处 y 不会被误解为指针

2.8 函数参数顺序

【规则 2-8-1】定义函数时,参数顺序为:输入参数在前,输出参数在后。

C/C++ 函数参数分为输入参数和输出参数两种, 有时输入参数也会输出 (值被修改时)。
输入参数一般传值或常数引用 ,输出参数或输入/输出参数为非常数指针 。

对参数排序时,将所有输入参数置于输出参数之前。不要仅仅因为是新添加的参数,就将其置于最后,而应该依然置于输出参数之前。

2.9 预处理指令

【规则 2-9-1】预处理指令不要缩进,从行首开始。即使预处理指令位于缩进代码块中,指令也应从行首开始。

// 良好的风格
void main()
{
    if (condition) 
    {
#if DISASTER_PENDING // Good
        dropEverything(); // 预处理指令中的程序仍然正常缩进
#endif
        backToNormal();
    }
}

2.10 类格式

【规则 2-10-1】类的声明属性依次序是public:protected:private:,都位于行首。除第一个关键词(一般是public)外,其他关键词前空一行。

class MyClass : public OtherClass 
{
public:
    MyClass();
    ~MyClass() {}
    
    void someFunction();
    void setSomeVar(int var) { m_someVar = var; }
    int someVar() const { return m_someVar; }
    
protected:
    bool someInternalFunction();
    
private:    
    int m_someVar;
    int m_someOtherVar;
}

2.11 命名空间格式化

【规则 2-11-1】命名空间内容不缩进。命名空间不添加额外缩进层次。

namespace {
void foo() { // Correct. No extra indentation within namespace.
...
}

注意:命名空间的左大括号可以在 namespace 所在一行,之间留一个空格。

三、命名规则

比较著名的命名规则当推 Microsoft 公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以 ch 为前缀,若是指针变量则追加前缀 p。

“匈牙利”法最大的缺点是烦琐,例如:

int i, j, k;
float x, y, z;

倘若采用“匈牙利”命名规则,则应当写成:

int iI, iJ, ik; // 前缀 i 表示 int 类型
float fX, fY, fZ; // 前缀 f 表示 float 类型

如此烦琐的程序会让绝大多数程序员无法忍受。

据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名规则。命名规则对软件产品而言并不是“成败悠关”的事,我们不要花太多精力试图发明世界上最好的命名规则,而应当制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施。

3.1 共性规则

本节论述的共性规则是被大多数程序员采纳的,我们应当在遵循
这些共性规则的前提下,再扩充特定的规则。

【规则 3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行 “解码”。

标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应当准确。例如不要把 CurrentValue 写成 NowValue。

【规则 3-1-2】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。

例如 Windows 应用程序的标识符通常采用 “大小写” 混排的方式,如 addChild。而 Unix 应用程序的标识符通常采用 “小写加下划线” 的方式,如 add_child。别把这两类风格混在一起用。

【规则 3-1-3】程序中不要出现仅靠大小写区分的相似的标识符。

int x, X; // 变量 x 与 X 容易混淆

void foo(int x); // 函数 foo 与 FOO 容易混淆
void FOO(float x);

【规则 3-1-4】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。

【规则 3-1-5】变量的名字应当使用“名词”或者“形容词+名词”。

float value;
float oldValue;
float newValue;

【3-1-6】函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。

drawBox(); // 普通函数
box->draw(); // 类的成员函数

【规则 3-1-7】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。

int minValue;
int maxValue;

int SetValue(…);
int GetValue(…);

【规则 3-1-8】尽量避免名字中出现数字编号,如 Value1,Value2 等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。

【规则 3-1-8】除非缩写放到项目外也非常明确,否则不要使用缩写。

// 良好的风格
int num_dns_connections; // Most people know what "DNS" stands for
int price_count_reader; // OK, price count. Makes sense

// 不良的风格
int wgc_connections; // Only your group knows what this stands for
int pc_reader; // Lots of things can be abbreviated "pc"

3.2 简单的 Windows 应用程序命名规则

作者对 “匈牙利” 命名规则做了合理的简化,下述的命名规则简单易用,比较适合于Windows 应用软件的开发。

【规则 3-2-1】文件命名使用 “小驼峰命名法”,除第一个单词之外,其他单词首字母大写。

lockScreenW.h
changePasswdW.cpp

【规则 3-2-2】无论是普通函数还是成员函数的命名,都使用 “小驼峰命名法”,除第一个单词之外,其他单词首字母大写。

// 普通函数
void setAge()
{
...
}

// 成员函数
class MyClass
{
public:
    void setAge(int age);   
}

【规则 3-2-3】结构体、类型定义( typedef)、枚举等所有类型,均使用 “大驼峰命名法”,所有单词首字母大写。

// classes and structs
class UrlTable { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTable*, string> UrlTableMap;

// enums
enum UrlTableErrors { ...

【规则 3-2-4】变量和参数命名使用 “小驼峰命名法”,除第一个单词之外,其他单词首字母大写。

bool flag;
int drawMode;

class MyClass :
{ 
private:
    string m_tableName;
};

【规则 3-2-5】无论是宏常量还是普通常量的命名,都全用大写的字母,用下划线分割单词。

// 宏常量
#define MAX_ARRAY_LEN 100

// 普通常量
const int MAX_ARRAY_LEN = 100;
const float PI = 3.14159;

【规则 3-2-6】如果不得已需要全局变量,则使全局变量加前缀 g_(表示global),即 “匈牙利+小驼峰命名法”。

int g_howManyPeople; // 全局变量

【规则 3-2-7】静态变量加前缀 s_(表示 static),即 “匈牙利+小驼峰命名法”。

void init()
{
    static int s_initValue; // 静态变量
}

【规则 3-2-8】类的数据成员加前缀 m_(表示 member),即 “匈牙利+小驼峰命名法”,这样可以避免数据成员与成员函数的参数同名。

void Object::SetValue(int width, int height)
{
    m_width = width;
    m_height = height;
}

【规则 3-2-9】枚举值推荐全部大写,单词间以下划线相连。

enum UrlTableErrors 
{
    OK = 0,
    ERROR_OUT_OF_MEMORY,
    ERROR_MALFORMED_INPUT,
};

 

四、表达式和基本语句

表达式和语句都属于 C++/C 的短语结构语法。它们看似简单,但使用时隐患比较多。本节归纳了正确使用表达式和语句的一些规则与建议。

4.1 运算符的优先级

【规则 4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。

word = (high << 8) | low
if ((a | b) && (a & c))

4.2 复合表达式

如 a = b = c = 0 这样的表达式称为复合表达式。允许复合表达式存在的理由是:

(1)书写简洁;

(2)可以提高编译效率。但要防止滥用复合表达式

【规则 4-2-1】不要编写太复杂的复合表达式。

i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂

【规则 4-2-2】不要有多用途的复合表达式。

d = (a = b + c) + r ; // 该表达式既求 a 值又求 d 值。

4.3 if 语句

if 语句是 C++/C 语言中最简单、最常用的语句,然而很多程序员用隐含错误的方式写 if 语句。本节以 “与零值比较” 为例,展开讨论。

【规则 4-3-1】不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。

// 良好的风格
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
    
// 不良的风格
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)

【规则 4-3-2】整型变量与零值比较。

// 良好的风格
if (value == 0)
if (value != 0)
    
// 不良的风格
if (value) // 会让人误解 value 是布尔变量
if (!value)

【规则 4-3-3】不可将浮点变量用 “==” 或 “!=” 与任何数字比较。

// 良好的风格
if ((f>=-EPSINON) && (f<=EPSINON)) // EPSINON 是允许的误差(即精度)
    
// 不良的风格
if (f == 0.0) // 隐含错误的比较

【规则 4-3-4】应当将指针变量用 “==” 或 “!=” 与 NULL 比较。

// 良好的风格
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针
if (p != NULL)    
    
// 不良的风格
if (p == 0) // 容易让人误解 p 是整型变量
if (p != 0)    
if (p) // 容易让人误解 p 是布尔变量
if (!p)    

【规则 4-3-5】提倡 if 和左圆括号间保留一个空格,且不在圆括号中添加空格 。

// 良好的风格
if (condition) // if 和左圆括号间有个空格,且不在圆括号中添加空格
{
    ...
} 
else // 关键字else另单独起一行
{ 
    ...
}

4.4 循环语句的效率

C++/C 循环语句中,for 语句使用频率最高,while 语句其次,do 语句很少用。本节重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。

【规则 4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。

//  低效率:长循环在最外层
for (row=0; row<100; row++)
{
    for (col=0; col<5; col++ )
    {
        sum = sum + a[row][col];
    }
}

// 高效率:长循环在最内层
for (col=0; col<5; col++ )
{
    for (row=0; row<100; row++)
    {
        sum = sum + a[row][col];
    }
}

【规则 4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。

//  效率低但程序简洁
for (i=0; i<N; i++)
{
    if (condition)
    {
        doSomething();
    }
    else
    {
        doSomething();
    }
}    

// 效率高但程序不简洁
if (condition)
{
    for (i=0; i<N; i++)
        doSomething();
    }
else
{
    for (i=0; i<N; i++)
        doSomething();
}

4.5 for 语句的循环控制变量

【规则 4-5-1】如不可在 for 循环体内修改循环变量,防止 for 循环失去控制。

【规则 4-5-2】建议 for 语句的循环控制变量的取值采用 “半开半闭区间” 写法。

// (a)循环变量属于半开半闭区间
for (int x=0; x<N; x++)
{
    doSomething();
}   

// (b)循环变量属于闭区间
for (int x=0; x<=N-1; x++)
{
    doSomething();
}

示例 (a) 中的 x 值属于半开半闭区间 “0 =< x < N”,起点到终点的间隔为 N,循环次数为 N。

示例 (b) 中的 x 值属于闭区间 “0 =< x <= N-1”,起点到终点的间隔为 N-1,循环次数为N。

相比之下,示例 (a) 的写法更加直观,尽管两者的功能是相同的。

4.6 switch 语句

【规则 4-6-1】switch 语句中的 case 块可以使用大括号也可以不用, 取决于你的喜好。

【规则 4-6-2】如果有不满足 case 枚举条件的值,要总是包含一个 default(如果有输入值没有 case 去处理,编译器将报警)。如果 default 永不会执行,可以简单的使用assert。

switch (var) 
{
    case 0:
    {
        ...
        break;
    }
        
    default: 
    {
        assert(false);
    }
}

4.7 goto 语句

自从提倡结构化设计以来,goto 就成了有争议的语句。首先,由于 goto 语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格。其次,goto 语句经常带来错误或隐患。它可能跳过了某些对象的构造、变量的初始化、重要的计算等语句,例如:

goto state;
String s1, s2; // 被 goto 跳过
int sum = 0; // 被 goto 跳过
…
state:
…

如果编译器不能发觉此类错误,每用一次 goto 语句都可能留下隐患。

很多人建议废除 C++/C 的 goto 语句,以绝后患。但实事求是地说,错误是程序员自己造成的,不是 goto 的过错。goto 语句至少有一处可显神通,它能从多重循环体中咻地一下子跳到外面,用不着写很多次的 break 语句。例如:

{ …
    { …
        { …
            goto error;
        }
    }
}

error:
…

就象楼房着火了,来不及从楼梯一级一级往下走,可从窗口跳出火坑。所以我们主张少用、慎用 goto 语句,而不是禁用。

五、函数设计

函数是 C++/C 程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本节重点论述函数的接口设计和内部实现的一些规则。

5.1 参数的规则

【规则 5-1-1】参数命名要恰当,顺序要合理。

例如编写字符串拷贝函数 stringCopy,它有两个参数。如果把参数名字起为
str1 和 str2,例如 :

void stringCopy(char *str1, char *str2);

那么我们很难搞清楚究竟是把 str1 拷贝到 str2 中,还是刚好倒过来。可以把参数名字起得更有意义,如叫 strSource 和 strDest。这样从名字上就可以看出应该把 strSource 拷贝到 strDest。

还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。 如果将函数声明为:

void stringCopy(char *strSource, char *strDest);

别人在使用时可能会不假思索地写成如下形式:

char str[20];
StringCopy(str, “Hello World”); // 错误,参数顺序颠倒了

【规则 5-1-2】如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。

void stringCopy(char *strDest,const char *strSource);

【规则 5-1-3】如果输入参数以值传递的方式传递对象,则宜改用 “const &” 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

【规则 5-1-4】避免函数有太多的参数,参数个数尽量控制在 5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。

5.2 返回值的规则

【规则 5-2-1】函数名字与返回值类型在语义上不可冲突。

违反这条规则的典型代表是 C 标准库函数 getchar。 例如:

char c;
c = getchar();
if (c == EOF)
…

按照 getchar 名字的意思,将变量 c 声明为 char 类型是很自然的事情。但不幸的是 getchar 的确不是 char 类型,而是 int 类型,其原型为:int getchar(void); 。

由于 c 是 char 类型,取值范围是 [-128,127],如果宏 EOF 的值在 char 的取值范围之外,那么 if 语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数 getchar 误导了使用者。

【规则 5-2-2】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。

回顾上例,C 标准库函数的设计者为什么要将 getchar 声明为令人迷糊的 int 类型呢?他会那么傻吗?

在正常情况下,getchar 的确返回单个字符。但如果 getchar 碰到文件结束标志或发生读错误,它必须返回一个标志 EOF。为了区别于正常的字符,只好将 EOF 定义为负数(通常为负 1)。因此函数 getchar 就成了 int 类型。

我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return 语句返回。

函数 getchar 可以改写成:

bool getChar(char *c); 

虽然 gechar 比 GetChar 灵活,例如 putchar(getchar()); 但是如果 getchar 用错了,它的灵活性又有什么用呢?

【规则 5-2-3】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。

例如字符串拷贝函数 strcpy 的原型:

char *strcpy(char *strDest,const char *strSrc);

strcpy 函数将 strSrc 拷贝至输出参数 strDest 中,同时函数的返回值又是 strDest。这样做并非多此一举,可以获得如下灵活性:

char str[20];
int length = strlen( strcpy(str, “Hello World”) );

【规则 5-2-4】函数返回时,return 表达式中不要使用圆括号。

return x; //not return(x);

5.3 函数内部实现的规则

不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的 “入口处” 和 “出口处” 从严把关,从而提高函数的质量。

【规则 5-3-1】在函数体的 “入口处”,对参数的有效性进行检查。

很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。 详见 5.5 节“使用断言”。

 

【规则 5-3-2】在函数体的“出口处”,对 return 语句的正确性和效率进行检查。

如果函数有返回值,那么函数的“出口处”是 return 语句。我们不要轻视 return 语句。如果 return 语句写得不好,函数要么出错,要么效率低下。

注意事项如下:

(1)return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。例如:

char * fun(void)
{
    char str[] = “hello world”; // str 的内存位于栈上
    …
    return str; // 将导致错误
}

(2)要搞清楚返回的究竟是 “值”、“指针” 还是 “引用”。

5.4 使用断言

程序一般分为 Debug 版本和 Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。

断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。下例是一个内存复制函数。在运行过程中,如果 assert 的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了 assert)。

void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
    assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言
    
    byte *pbTo = (byte *) pvTo; // 防止改变 pvTo 的地址
    byte *pbFrom = (byte *) pvFrom; // 防止改变 pvFrom 的地址    
    while(size -- > 0 )
        *pbTo ++ = *pbFrom ++;
    
    return pvTo;
}

为了不在程序的 Debug 版本和 Release版本引起差别,assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。程序员可以把 assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在 assert 处终止了,并不是说含有该 assert 的函数有错误,而是调用者出了差错,assert 可以帮助我们找到发生错误的原因。

5.5 其它建议

【规则 5-5-1】函数的功能要单一,不要设计多用途的函数。

【规则 5-5-2】函数体的规模要小,尽量控制在 50 行代码之内。

【规则 5-5-3】尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。

带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C/C++语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static局部变量,除非必需。

【规则 5-5-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。

六、注释

C++语言中,程序块的注释常采用 “/*…*/”,行注释一般采用 “//…”。注释通常用于:

(1)版本、版权声明;
(2)函数接口说明;
(3)重要的代码行或段落提示。

虽然注释有助于理解代码,但注意不可过多地使用注释。

6.1 类注释

【规则 6-1-1】每个类的定义要添加描述类的功能和用法的注释。

如果你觉得已经在文件顶部详细描述了该类,想直接简单的来上一句“完整描述见文件顶部”的话,还是多少在类中加点注释吧。

如果该类的实例可被多线程访问,使用时务必注意备注说明一下。

6.2 函数注释

函数声明处注释描述函数功能,定义处描述函数实现。

函数声明

【规则 6-2-1】函数声明处的注释,只描述函数功能及用法,而不会描述函数如何实现,因为那是定义部分的事情。

void setAge(int age); // 设置学生年龄

函数定义:

【规则 6-2-2】每个函数定义时要以注释说明函数功能和实现要点,如使用的漂亮代码、实现的简要步骤、如此实现的理由等等。

不要从 .h 文件或其他地方的函数声明处直接复制注释,简要说明函数功能是可以的,但重点要放在如何实现上。

/*
* 函数介绍:设置学生年龄
* 函数实现:将参数age的值赋给成员变量m_age
* 输入参数:age-传入的学生年龄值
* 返回值 :NULL
* 注意事项:NULL
*/
void setAge(int age)
{
    m_age = age;
}

6.3 注释风格

关于注释风格,很多 C++ 的 coders 更喜欢行注释, C coders 或许对块注释依然情有独
钟,或者在文件头大段大段的注释时使用块注释。

【规则 6-3-1】对于行注释,注释与 // 留一个空格,若是注释在程序右侧,则 // 与程序之间留一个空格。

// 注释1
fun1();

fun2(); // 注释2

七、其他编程经验

下面介绍一些使 C++ 代码更加健壮的技巧和使用方式。

7.1 引用参数

【规则 7-1-1】按引用传递的参数必须加上 const。

void Foo(const string &in, string *out);

事实上这是一个硬性约定:输入参数为值或常数引用,输出参数为指针;输入参数可以是常数指针,但不能使用非常数引用形参。

7.2 类型转换

【规则 7-2-1】使用 static_cast<>() 等 C++ 的类型转换,不要使用 int y = (int)x

C 语言的类型转换问题在于操作比较含糊:有时是在做强制转换(如 (int)3.5),有时是在做类型转换(如 (int)"hello")。另外, C++ 的类型转换查找更容易、更醒目。

7.3 整型

【规则 7-3-1】C++ 内建整型中, 唯一用到的是 int, 如果程序中需要不同大小的变量, 可以使用<stdint.h>中的精确宽度的整型,如 int16_t。

<stdint.h> 定义了 int16_t、 uint32_t、 int64_t 等整型,在需要确定大小的整型时可以
使用它们代替 short、 unsigned long long 等。适当情况下,推荐使用标准类型如 size_t 和 ptrdiff_t。

最常使用的是,对整数来说,通常不会用到太大,如循环计数等,可以使用普通的 int。

7.4 预处理宏

【规则 7-4-1】使用宏时要谨慎,尽量以内联函数、枚举和常量代替之。

宏意味着你和编译器看到的代码是不同的, 因此可能导致异常行为, 尤其是当宏存在于全局作用域中。

值得庆幸的是, C++ 中,宏不像C中那么必要。宏内联效率关键代码可以用内联函数替代; 宏存储常量可以 const 变量替代; 宏“缩写”长变量名可以引用替代;

下面给出的用法模式可以避免一些使用宏的问题,供使用宏时参考:

1) 可能被多个C++文件用到的宏定义,一般都放在头文件中(.h),如果只需被一个文件所用,放在 .cpp 或 .h 里面都可以;

2) 使用前正确 #define,使用后正确 #undef。

7.5 0和NULL

【规则 7-5-1】整数用0,实数用0.0,指针用NULL,字符(串)用'\0'。

7.6 sizeof( sizeof)

【规则 7-6-1】尽可能用 sizeof(varname) 代替 sizeof(type)。

使用 sizeof(varname) 是因为当变量类型改变时代码自动同步,有些情况下 sizeof(type) 或许有意义,还是要尽量避免,如果变量类型改变的话不能同步。

Struct data;
memset(&data, 0, sizeof(data)); //Good - 变量类型改变时,代码自动同步
memset(&data, 0, sizeof(Struct)) //Bad - 变量类型改变时,代码不会自动同步

7.7 内存管理

【规则 7-7-1】用 malloc 或 new 申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL 的内存。

【规则 7-7-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

【规则 7-7-3】动态内存的申请与释放必须配对,防止内存泄漏。

【规则 7-7-4】用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产生 “野指针”。

7.8 一些有益的建议

【规则 7-8-1】当心那些视觉上不易分辨的操作符发生书写错误。

我们经常会把 “==” 误写成 “=”,象 “||”、“&&”、“<=”、“>=” 这类符号也很容易发生 “丢 1” 失误。然而编译器却不一定能自动指出这类错误。

【规则 7-8-2】变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。

【规则 7-8-3】当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。

【规则 7-8-4】当心变量发生上溢或下溢,数组的下标越界。

【规则 7-8-5】当心忘记编写错误处理程序,当心错误处理程序本身有误。

【规则 7-8-6】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。

【规则 7-8-7】尽量使用标准库函数,不要“发明”已经存在的库函数。

【规则 7-8-8】尽量不要使用与具体硬件或软件环境关系密切的变量。

【规则 7-8-9】把编译器的选择项设置为最严格状态。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值