代码风格(4)——格式

一、总述

每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规则需要一定时间的适应,但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人轻松地阅读和理解代码。

二、行

2.1 行长度

每一行代码字符数不超过 120
注释行可以超过 120 个字符,但最大不超过 150

2.2 行宽度

函数体行宽原则上不超过 80
80 行限制事实上有助于避免代码可读性失控,比如超多重嵌套块,超多重函数调用等等。

三、非ASCII字符

尽量不使用非 ASCII 字符,使用时必须使用 UTF-8 编码。

即使是英文,也不应将用户界面的文本硬编码到源代码中,因此非 ASCII 字符应当很少被用到。特殊情况下可以适当包含此类字符。例如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非 ASCII 字符串;更常见的是 (不需要本地化的) 单元测试代码可能包含非 ASCII 字符串。此类情况下,应使用 UTF-8 编码,因为很多工具都可以理解和处理 UTF-8 编码。

四、空格还是制表位

只使用空格,每次缩进 4 个空格。(使用记事本打开检查是否使用空格还是制表符)

如果要在代码中使用制表符。你应该设置编辑器将制表符转为空格。

五、函数声明与定义

返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参分行,分行方式如下 六、函数调用 一致。

函数看上去像这样:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) 
{
    DoSomething();  // 4 space indent
    ...
}

如果同一行文本太多,放不下所有参数:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) 
{
    DoSomething();  // 4 space indent
    ...
}

甚至连第一个参数都放不下:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
        Type par_name1,  // 8 space indent
        Type par_name2,
        Type par_name3) 
{
    DoSomething();  // 4 space indent
    ...
}

注意以下几点:

  • 使用好的参数名。
  • 只有在参数未被使用或者其用途非常明显时,才能省略参数名。
  • 如果返回类型和函数名在一行放不下,分行。
  • 如果返回类型与函数声明或定义分行了,不要缩进。
  • 左圆括号总是和函数名在同一行。
  • 函数名和左圆括号间永远没有空格。
  • 圆括号与参数间没有空格。
  • 左大括号 另起新行
  • 右大括号总是单独位于函数 最后一行
  • 所有形参应尽可能对齐。
  • 缺省缩进为 4 个空格。
  • 换行后的参数保持 8 个空格的缩进。或尽量对齐上一行参数。

属性,和展开为属性的宏,写在函数声明或定义的最前面,即返回类型之前:

MUST_USE_RESULT bool IsOK();

六、函数调用

要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进 8 格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。

函数调用遵循如下形式:

bool retval = DoSomething(argument1, argument2, argument3);

如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

参数也可以放在次行,缩进8格:

if(...) 
{
    DoSomething(
        argument1, argument2,  // 8 空格缩进
        argument3, argument4);
}

七、Lambda 表达式

Lambda 表达式对形参和函数体的格式化和其他函数一致;捕获列表同理,表项用逗号隔开。

若用引用捕获,在变量名和 & 之间不留空格。

int x = 0;
auto add_to_x = [&x](int n) { x += n; };

短 lambda 就写得和内联函数一样。

std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) 
{
    return blacklist.find(i) != blacklist.end();
}), digits.end());

八、列表初始化格式

如果列表初始化伴随着名字,比如类型或变量名,格式化时将将名字视作函数调用名,{} 视作函数调用的括号。如果没有名字,就视作名字长度为零。

// 一行列表初始化示范.
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};

// 当不得不断行时.
SomeFunction(
    {"assume a zero-length name before {"},  // 假设在 { 前有长度为零的名字.
    some_other_function_parameter);

SomeType variable
{
    some, other, values,
    {"assume a zero-length name before {"},  // 假设在 { 前有长度为零的名字.
    SomeOtherType
    {
        "Very long string requiring the surrounding breaks.",  // 非常长的字符串, 前后都需要断行.
        some, other values
    },
    SomeOtherType
    {
        "Slightly shorter string",  
        some, other, values
    }
};

SomeType variable
{
    "This is too long to fit all in one line"
};  

MyType m = 
{  
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {
        interiorwrappinglist,
        interiorwrappinglist2
    }
};

九、条件语句

9.1 if语句格式

if(condition)  // '('左边和后边都没有空格,')'左边没有空格
{              // '{'换行
    ...        // 4 空格缩进
}
else           // else 换行  
{  
    ...
}

如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用 else 子句时使用:

if(x == kFoo) return new Foo();
if(x == kBar) return new Bar();

但最好还是使用大括号:

if(x == kFoo)
{
    return new Foo();
}

如果语句有 else 分支则不允许:

// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();

9.2 布尔变量与零值比较

不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。
根据布尔类型的语义,零值为“假”(记为 FALSE),任何非零值都是“真”(记为TRUE)。TRUE 的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE 定义为1,而 Visual Basic 则将 TRUE 定义为 -1。
假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下:
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
其它的用法都属于不良风格,例如:
if (flag == TRUE)
if (flag == 1)
if (flag == FALSE)
if (flag == 0)

9.3 浮点变量与零值比较

不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避
免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为 x,应当将
if(x == 0.0) // 隐含错误的比较
转化为
if((x>=-EPSINON) && (x<=EPSINON))
其中 EPSINON 是允许的误差(即精度)。

9.4 指针变量与零值比较

应当将指针变量用“==”或“!=”与 NULL 比较。
指针变量的零值是“空”(记为 NULL)。尽管 NULL 的值与 0 相同,但是两者意义不
同。假设指针变量的名字为 p,它与零值比较的标准 if 语句如下:
if(p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量
if(p != NULL)
不要写成
if(p == 0) // 容易让人误解 p 是整型变量
if (p != 0)
或者
if(p) // 容易让人误解 p 是布尔变量
if(!p)

十、循环和开关选择语句

switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {}continue

switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。

如果有不满足 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert

switch(var)  // '('左边和右边没有空格,')'左边没有空格
{
case 0:  // 无空格
    { 
        ...      // 8 空格缩进
        break;
    }
case 1: 
    {
        ...
        break;
    }
default: 
    assert(false);
}

循环语句:

for(int i = 0; i < kSomeNumber; ++i) 
{
    printf("I take it back\n");
}

空循环体应使用 {}continue,而不是一个简单的分号。

while(condition) 
{
    // 反复循环直到条件失效.
}
for(int i = 0; i < kSomeNumber; ++i)  // 可 - 空循环体.
{
} 
while(condition) continue;  // 可 - contunue 表明没有逻辑.
while(condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一.

十一、指针和引用表达式

句点或箭头前后不要有空格。指针/地址操作符 (*, &) 之后不能有空格。

下面是指针和引用表达式的正确使用范例:

x = *p;
p = &x;
x = r.y;
x = r->y;

注意:

  • 在访问成员时,句点或箭头前后没有空格。
  • 指针操作符 *& 后没有空格。

在声明指针变量或参数时,星号与类型或变量名紧挨都可以:

// 好, 空格前置,推荐使用
char *c;
const string &str;

// 好, 空格后置.
char* c;
const string& str;
int x, *y;  // 不允许 - 在多重声明中不能使用 & 或 *
char * c;  // 差 - * 两边都有空格
const string & str;  // 差 - & 两边都有空格.

在单个文件内要保持风格一致,所以,如果是修改现有文件,要遵照该文件的风格。

十二、布尔表达式

如果一个布尔表达式超过 标准行宽 120个字符,断行方式要统一一下。

下例中,逻辑与 (&&) 操作符总位于行尾:

if(this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) 
{
    ...
}

注意,上例的逻辑与 (&&) 操作符均位于行尾。

十三、函数返回值

不要在 return 表达式里加上非必须的圆括号。

只有在写 x = expr 要加上括号的时候才在 return expr;里使用括号。

return result;                  // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
        another_condition);
return (value);                // 差 - 毕竟您从来不会写 var = (value);
return(result);                // 差 - return 可不是函数!

十四、预处理指令

预处理指令不要缩进,从行首开始。
即使预处理指令位于缩进代码块中,指令也应从行首开始。

// 好 - 指令从行首开始
    if(lopsided_score) 
    {
#if DISASTER_PENDING      // 正确 - 从行首开始
        DropEverything();
# if NOTIFY               // 非必要 - # 后跟空格
        NotifyClient();
# endif
#endif
    BackToNormal();
    }
// 差 - 指令缩进
    if (lopsided_score) 
    {
        #if DISASTER_PENDING  // 差 - "#if" 应该放在行开头
        DropEverything();
        #endif                // 差 - "#endif" 不要缩进
        BackToNormal();
    }

十五、类格式

访问控制块的声明依次序是 public:protected:private:,每个都缩进 2 个空格。

class MyClass : public OtherClass 
{
  public:      // 注意有2个空格的缩进
    MyClass();  // 标准的4空格缩进
    explicit MyClass(int var);
    ~MyClass() {}

    void SomeFunction();
    void SomeFunctionThatDoesNothing() {}

    void set_some_var(int var) { some_var_ = var; }
    int some_var() const { return some_var_; }

  private:
    bool SomeInternalFunction();

    int some_var_;
    int some_other_var_;
};

注意事项:

  • 所有基类名应在 80 列限制下尽量与子类名放在同一行。
  • 关键词 public:protected:private: 要缩进 1 个空格。
  • 除第一个关键词 (一般是 public) 外,其他关键词前要空一行。如果类比较小的话也可以不空。
  • 这些关键词后不要保留空行。
  • public 放在最前面,然后是 protected,最后是 private

十六、构造函数初始值列表

构造函数初始化列表放在同一行或按8格缩进并排多行。

// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : some_var_(var) 
{
    DoSomething();
}

// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var)
        : some_var_(var), some_other_var_(var + 1) 
{
    DoSomething();
}

// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
        : some_var_(var),             // 8 space indent
        some_other_var_(var + 1) 
{ 
    DoSomething();
}

// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var)
        : some_var_(var) {}

十七、命名空间格式化

命名空间内容不缩进。

命名空间不要增加额外的缩进层次,例如:

namespace 
{
void foo()  // 正确. 命名空间内没有额外的缩进. 
{ 
    ...
}

}  // namespace

不要在命名空间内缩进:

namespace
{
  // 错, 缩进多余了.
    void foo() 
    {
        ...
    }
}  // namespace

声明嵌套命名空间时,每个命名空间都独立成行。

namespace foo {
namespace bar {

十八、水平留白

18.1 通用

水平留白的使用根据在代码中的位置决定。永远不要在行尾添加没意义的留白。

void f(bool b)  
{  
    ...
    int i = 0;  // 分号前不加空格.
    // 列表初始化中大括号内的空格是可选的.
    // 如果加了空格, 那么两边都要加上.
    int x[] = { 0 };
    int x[] = {0};

    // 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar 
{
  public:
    // 对於单行函数的实现, 在大括号内加上空格
    // 然后是函数实现
    Foo(int b) : Bar(), baz_(b) {}  // 大括号里面是空的话, 不加空格.
    void Reset() { baz_ = 0; }  // 用括号把大括号与实现分开.
    ...

18.2 循环和条件语句

if(b)   // 圆括号内部不紧邻空格.
{          
} 
else 
{          
}
while(test) {}  
switch(i) 
{
    for(int i = 0; i < 5; ++i) 
    {
        switch(i) 
        {    
            if(test) 
            {   
                for(int i = 0; i < 5; ++i) 
                {
                    for( ; i < 5 ; ++i)   // 循环里内 ; 后恒有空格, ;  前可以加个空格.
                    {  
                        switch(i) 
                        {
                        case 1:         // switch case 的冒号前无空格.
                            ...
                        case 2: 
                            break;  

18.3 操作符

// 赋值运算符前后总是有空格.
x = 0;

// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if(x && !y)
  ...

18.4 模板和转换

// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char *>(x);

// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;

十九、垂直留白

这不仅仅是规则而是原则问题了:不在万不得已,不要使用空行。尤其是:两个函数定义之间的空行不要超过 2 行,函数体首尾不要留空行。函数体中也不要随意添加空行。

基本原则是:同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。但通常是垂直留白越少越好。

下面的规则可以让加入的空行更有效:

  • 用于区别概念层次,如函数体内变量定义与调用函数用空行区分开。
  • 在多重 if-else 块里加空行或许有点可读性。

19.1 概念间垂直方向上的区隔

几乎所有的代码都是 从上往下 读,从左往右 读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。

如下。在封包声明/导入声明和每个函数之间,都有空白行隔开。这条极其简单的规则极大地影响到代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。往下读代码,你的目光总会停留于空白行之后那一行。

package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
  public static final String REGEXP = "'''.+?'''";
  private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
    Pattern.MULTILINE + Pattern.DOTALL
  );

  public BoldWidget(ParentWidget parent, String text) throws Exception {
    super(parent);
    Matcher match = pattern.matcher(text);
    match.find();
    addChildWidgets(match.group(1));
  }

public String render() throws Exception {
  StringBuffer html = new StringBuffer("</b>");
  return html.toString();
  }
}  

如下。抽掉这些空白行,代码可读性减弱了不少。

package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
  public static final String REGEXP = "'''.+?'''";
  private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
    Pattern.MULTILINE + Pattern.DOTALL
  );
  public BoldWidget(ParentWidget parent, String text) throws Exception {
    super(parent);
    Matcher match = pattern.matcher(text);
    match.find();
    addChildWidgets(match.group(1));
  }
public String render() throws Exception {
  StringBuffer html = new StringBuffer("</b>");
  return html.toString();
  }
}  

在你不特意注视时,后果就更严重了。在第一个例子中,代码组会跳到你眼中,而第二个例子就像一堆乱麻。两段代码的区别,展示了垂直方向上区隔的作用。

二十、附录

20.1 .c文件

/**===========================================================================
  Copyright (C) XXX Technologies Co., Ltd. All rights reserved.

  @file     user_udp.c
  @brief    本文件用于UDP通讯接口
  @author   Leung Man-Wah
  @version  r0.1
  @date     2019/07/04
  @license XXX
----------------------------------------------------------------------------
  Remark: (备注描述)
----------------------------------------------------------------------------
                                History
----------------------------------------------------------------------------
  <Date>     | <Version> | <Author>       | <Description>
-------------|-----------|----------------|---------------------------------
  2019/07/04 | r0.1      | Leung Man-Wah  | 创建
-------------|-----------|----------------|---------------------------------
             |           |                |
-------------|-----------|----------------|---------------------------------
             |           |                |
-------------|-----------|----------------|---------------------------------
             |           |                |
============================================================================*/

/*********************************************************************
 * INCLUDES
 */
// 系统/协议栈/库头文件(使用时去掉此行注释)
#include "osapi.h"
#include "user_interface.h"
#include "espconn.h"
#include "mem.h"

// 自定义头文件(使用时去掉此行注释)
#include "user_udp.h"
#include "user_httpserver.h"
#include "cJSON.h"
#include "wb_protocol.h"
#include "common.h"

// 静态函数声明(使用时去掉此行注释)
static void udpDeviceInfoEspconn(void);
static void udpBleDataEspconn(void);
static void udpSendDataCallback(void *arg);

/*********************************************************************
 * CONSTANT
 */
const uint16 UDP_DEV_INFO_PERIOD = 1000;

/*********************************************************************
 * GLOBAL VARIABLES
 */
uint8 g_bleDeviceRssiStandrdValue = BLE_DEV_RSSi_DEFAULT_VALUE;
bool g_httpClientConnectCloudFlag = false;										    // 联网标志,0 - 断网;1 - 联网
bool g_httpClientHeartbeatFlag = false;											    // 心跳标志,0 - 非发心跳中;1 - 发心跳中
uint8 g_httpClientRenewalRecordFlag = 0;										    // 续传标志,0 - 非续传中;1 - 续传中;>=2 - 续传失败

/*********************************************************************
 * LOCAL VARIABLES
 */
static struct espconn s_devInfoUdpEspconn;											// 设备信息UDP广播结构体
static struct espconn s_bleDataUdpEspconn;											// 蓝牙数据UDP广播结构体
static os_timer_t s_udpDevInfoTimer;												// UDP广播设备信息的定时器
static uint8 s_udpDevInfoSendCount;													// UDP广播设备信息次数

/*********************************************************************
 * PUBLIC FUNCTIONS
 */
/**
 @brief UDP通讯初始化
 @param 无
 @return 无
*/
void UdpInit(void)
{
	udpDeviceInfoEspconn();
	udpBleDataEspconn();
}

/*********************************************************************
 * LOCAL FUNCTIONS
 */
/**
 @brief UDP发送继电器状态
 @param deviceType -[in] 设备类型
 @param pMacAddr -[in] MAC地址
 @param relayStatus -[in] 继电器状态
 @return 无
*/
static void udpSendRelayStatus(uint8 deviceType, uint8 *pMacAddr, uint8 relayStatus)
{
	char sendData[UDP_SEND_BUFFER_SIZE] = {0};

	jsonPackageRelayStatus(sendData, deviceType, pMacAddr, relayStatus);

	espconn_send(&s_devInfoUdpEspconn, sendData, strlen(sendData));					// UDP发送数据
}

/****************************************************END OF FILE****************************************************/

20.2 .h文件

/**===========================================================================
  Copyright (C) XXX Technologies Co., Ltd. All rights reserved.

  @file     user_udp.h
  @brief    本文件用于UDP通讯接口
  @author   Leung Man-Wah
  @version  r0.1
  @date     2019/07/04
  @license XXX
----------------------------------------------------------------------------
  Remark: (备注描述)
----------------------------------------------------------------------------
                                History
----------------------------------------------------------------------------
  <Date>     | <Version> | <Author>       | <Description>
-------------|-----------|----------------|---------------------------------
  2019/07/04 | r0.1      | Leung Man-Wah  | 创建
-------------|-----------|----------------|---------------------------------
             |           |                |
-------------|-----------|----------------|---------------------------------
             |           |                |
-------------|-----------|----------------|---------------------------------
             |           |                |
============================================================================*/

#ifndef _USER_UDP_H_
#define _USER_UDP_H_

/*********************************************************************
 * INCLUDES
 */

/*********************************************************************
 * DEFINITIONS
 */
#define UDP_JSON_ERROR				0x00
#define UDP_START_SEND_DEV_INFO		0x01
#define UDP_STOP_SEND_DEV_INFO		0x02
#define UDP_CONFIG_CLOUD_ERROR		0x03
#define UDP_SEND_RELAY_STATUS		0x04
#define UDP_RESTART_SEND_DEV_INFO	0x05
#define UDP_RESEND_RELAY_STATUS		0x06

#define DEV_TYPE_BLE				0x01
#define DEV_TYPE_WIFI				0x02

#define UDP_SEND_BUFFER_SIZE 		1024

/*********************************************************************
 * TYPEDEFS
 */
typedef struct
{
    uint8 devkey[40];
    uint8 token[40];
    uint8 activeStatus;
    ip_addr_t tcpServerIp;
    uint16 tcpServerPort;
    char tcpServerDomain[32];
    ip_addr_t httpServerIp;
	uint16 httpServerPort;
	char httpServerDomain[32];
	char httpServerPath[32];
	ip_addr_t dhcpIp;
	ip_addr_t dhcpNetmask;
	ip_addr_t dhcpGateway;
	uint8 dhcpFlag;
    uint8 bleDeviceRssiStandrdValue;
    uint8 productType;
} FlahSavedParam_t;

/*********************************************************************
 * GLOBAL VARIABLES
 */
extern uint8 g_bleDeviceRssiStandrdValue;
extern bool g_httpClientConnectCloudFlag;						
extern bool g_httpClientHeartbeatFlag;									
extern uint8 g_httpClientRenewalRecordFlag;										 

/*********************************************************************
 * API FUNCTIONS
 */
void UdpInit(void);
void UdpSendDeviceInfo(void);
void UdpSendRelayStatus(uint8 deviceType, uint8 *pMacAddr, uint8 relayStatus);
void UdpSendBleScanData(uint8 *pSendData);

#endif /* _USER_UDP_H_ */

• 由 Leung 写于 2019 年 10 月 30 日

• 参考:Google 开源项目风格指南——9. 格式
    [代码整洁之道]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章