C 编码风格

本文档介绍了 GNOME 中 C 程序的首选编码风格。

虽然编码风格很大程度上取决于个人品味,但在 GNOME 中,我们倾向于一种能够促进一致性、可读性和可维护性的编码风格。

我们提供了良好编码风格的示例,以及不被 GNOME 接受的糟糕风格的示例。请尽量提交符合 GNOME 编码风格的补丁;这表明您已经尽了您的本分,以尊重项目长期可维护性的目标。采用 GNOME 编码风格的补丁也更容易审查!

重要提示

本文档适用于 C 代码。与 C 不同,其他编程语言有其自身的官方编码风格建议;我们鼓励您在适用时遵循它们。

这些准则深受 GTK 编码风格文档;Linux 内核编码风格;以及 GNU 编码标准的影响。这些彼此之间略有差异,针对每个项目的特定需求和文化进行了特别修改,GNOME 的版本也不例外。

最重要的规则

编写代码时最重要的规则是:检查周围的代码并尝试模仿它

作为维护者,收到明显与周围代码风格不同的补丁会令人沮丧。这是不尊重的行为,就像有人穿着泥泞的鞋子闯入一尘不染的房子一样。

因此,无论本文档推荐什么,如果您已经编写了代码并且正在为其贡献代码,请保持其当前风格的一致性,即使它不是您最喜欢的风格。

最重要的是,不要让您对项目的第一次贡献是更改编码风格以适应您的口味。这非常不尊重人。

行宽

尝试使用 80 到 120 个字符长的代码行。这个长度的文本很容易适应大多数具有合理字体大小的显示器。超过该长度的行会难以阅读,并且意味着您可能应该重构您的代码。如果您有太多的缩进级别,这意味着您应该修复您的代码。

缩进

通常,GNOME 中代码的首选缩进风格有两种

  1. Linux 内核风格。使用长度为 8 个字符的制表符进行缩进,并采用 K&R 大括号放置

for (i = 0; i < num_elements; i++) {
        foo[i] = foo[i] + 42;

        if (foo[i] < 35) {
                printf ("Foo!");
                foo[i]--;
        } else {
                printf ("Bar!");
                foo[i]++;
        }
}
  1. GNU 风格。每个新级别缩进 2 个空格,大括号单独成行,并且也缩进

for (i = 0; i < num_elements; i++)
  {
    foo[i] = foo[i] + 42;

    if (foo[i] < 35)
      {
        printf ("Foo!");
        foo[i]--;
      }
    else
      {
        printf ("Bar!");
        foo[i]++;
      }
  }

这两种风格各有优缺点。最重要的是与周围的代码保持一致。例如,GNOME 的小部件工具包 GTK 库使用 GNU 风格编写。GNOME 的文件管理器 Nautilus 使用 Linux 内核风格编写。这两种风格在您习惯后都非常易读且一致。

当不得不学习或处理没有您首选缩进风格的代码时,您最初的感觉可能是,怎么说呢,令人沮丧。您应该抵制重新缩进所有内容或为您的补丁使用不一致风格的冲动。记住第一条规则:保持一致并尊重代码的习俗,您的补丁更有可能被接受,而不会对正确的缩进风格进行大量争论。

制表符

切勿更改编辑器中制表符的大小;保持它们为 8 个空格。更改制表符的大小意味着您没有编写的代码将永久错位。

相反,根据您正在编辑的代码设置适当的缩进大小。在编写非 Linux 内核风格的代码时,您甚至可能希望告诉您的编辑器自动将所有制表符转换为 8 个空格,以便消除对预期空格量的歧义。

大括号

不应为单语句块使用花括号

/* valid */
if (condition)
        single_statement ();
else
        another_single_statement (arg1);

“无单语句块”规则只有四个例外

  1. 在 GNU 风格中,如果 ifelse 语句的任一侧有大括号,则两侧都应有,以匹配缩进

/* valid */
if (condition)
  {
    foo ();
    bar ();
  }
else
  {
    baz ();
  }
  1. 如果单语句跨越多行,例如对于具有许多参数的函数,并且后面跟着 elseelse if

/* valid Linux kernel style */
if (condition) {
        a_single_statement_with_many_arguments (some_lengthy_argument,
                                                another_lengthy_argument,
                                                and_another_one,
                                                plus_one);
} else
        another_single_statement (arg1, arg2);

/* valid GNU style */
if (condition)
  {
    a_single_statement_with_many_arguments (some_lengthy_argument,
                                            another_lengthy_argument,
                                            and_another_one,
                                            plus_one);
  }
else
  {
    another_single_statement (arg1, arg2);
  }
  1. 如果条件由多行组成

/* valid Linux kernel style */
if (condition1 ||
    (condition2 && condition3) ||
    condition4 ||
    (condition5 && (condition6 || condition7))) {
        a_single_statement ();
}

/* valid GNU style */
if (condition1 ||
    (condition2 && condition3) ||
    condition4 ||
    (condition5 && (condition6 || condition7)))
  {
    a_single_statement ();
  }

注意

这样的长条件通常很难理解。一个好的做法是将条件设置为一个布尔变量,并为该变量指定一个好的名称。另一种方法是将长条件移动到一个函数中。

  1. 嵌套 if,在这种情况下,块应放置在最外层的 if

/* valid Linux kernel style */
if (condition) {
        if (another_condition)
                single_statement ();
        else
                another_single_statement ();
}

/* valid GNU style */
if (condition)
  {
    if (another_condition)
      single_statement ();
    else
      another_single_statement ();
  }

通常,新块应放置在新缩进级别上,如下所示

int retval = 0;

statement_1 ();
statement_2 ();

{
        int var1 = 42;
        gboolean res = FALSE;

        res = statement_3 (var1);

        retval = res ? -1 : 1;
}

虽然函数定义的花括号应位于新行上,但它们不应添加缩进级别

/* valid Linux kernel style*/
static void
my_function (int argument)
{
        do_my_things ();
}

/* valid GNU style*/
static void
my_function (int argument)
{
  do_my_things ();
}

条件

不要检查布尔值是否相等。通过使用隐式比较,生成的代码可以更像口语化的英语来阅读。另一个原因是“true”值不一定等于 TRUE 宏使用的值。例如

if (found)
  do_foo ();

if (!found)
  do_bar ();

C 语言将值 0 用于许多目的。作为数值,字符串的结尾、空指针和 FALSE 布尔值。为了使代码更清晰,您应该编写突出显示 0 使用方式的代码。因此,在读取比较时,可以知道变量类型。对于布尔变量,隐式比较是合适的,因为它已经是逻辑表达式。其他变量类型本身不是逻辑表达式,因此显式比较更好

if (some_pointer == NULL)
        do_blah ();

if (number == 0)
        do_foo ();

if (str != NULL && *str != '\0')
        do_bar ();

函数

函数应通过将返回值放在函数名称单独的一行上来声明

void
my_function (void)
{
        // ...
}

参数列表必须为每个参数分成新行,参数名称右对齐,考虑到指针

void
my_function (some_type_t      type,
             another_type_t  *a_pointer,
             double_ptr_t   **double_pointer,
             final_type_t     another_type)
{
        // ...
}

提示

如果您使用 Emacs,可以使用 M-x align 自动执行这种对齐。只需将光标和标记放在函数的原型周围,然后调用该命令即可。

在不违反行宽限制的情况下调用函数时,对齐方式也适用

align_function_arguments (first_argument,
                          second_argument,
                          third_argument);

空格

始终在开括号前放置一个空格,但从不在开括号后放置

if (condition)
        do_my_things ();

switch (condition) {
}

在声明结构体类型时,使用换行符分隔结构的逻辑部分

struct _GtkWrapBoxPrivate
{
  GtkOrientation orientation;
  GtkWrapAllocationMode mode;

  GtkWrapBoxSpreading horizontal_spreading;
  GtkWrapBoxSpreading vertical_spreading;

  guint16 spacing[2];

  guint16 minimum_line_children;
  guint16 natural_line_children;

  GList *children;
};

不要仅仅因为某些内容可以放在一行上而消除空格和换行符

/* invalid */
if (condition) foo (); else bar ();

消除任何行尾的空格,最好作为单独的补丁或提交。不要在文件开头或结尾使用空行。

switch 语句

switch 应该在新缩进级别上打开一个块,并且每个 case 应该从与大括号相同的缩进级别开始,case 块在新缩进级别上

/* valid Linux kernel style */
switch (condition) {
case FOO:
        do_foo ();
        break;

case BAR:
        do_bar ();
        break;
}

/* valid GNU style */
switch (condition)
  {
  case FOO:
    do_foo ();
    break;

  case BAR:
    do_bar ();
    break;

  default:
    do_default ();
  }

提示

最好,但不是强制性的,用换行符分隔各种 case。

提示

default case 的 break 语句不是必需的。

如果切换枚举类型,则 case 语句必须存在于枚举类型的每个成员中。对于您不想处理的成员,将它们的 case 语句别名为 default

switch (enumerated_condition) {
case HANDLED_1:
        do_foo ();
        break;

case HANDLED_2:
        do_bar ();
        break;

case IGNORED_1:
case IGNORED_2:
default:
        do_default ();
}

提示

如果枚举类型的多数成员不应处理,请考虑使用 ifelse if 语句而不是 switch

如果 case 块需要声明新变量,则规则与内部块相同(见上文);break 语句应放置在内部块之外

switch (condition)
  {
  case FOO:
    {
      int foo;

      foo = do_foo ();
    }
    break;

  // ...
  }

头文件

头文件的主要规则是函数定义应垂直对齐成三列

return_type          function_name           (type   argument,
                                              type   argument,
                                              type   argument);

每列的最大宽度由该列中最长的元素给出

void        gtk_type_set_property (GtkType     *type,
                                   const char  *value,
                                   GError     **error);
const char *gtk_type_get_property (GtkType     *type);

也可以将列对齐到下一个制表符,以避免每次添加新函数时都必须重新格式化头文件

void          gtk_type_set_prop           (GtkType *type,
                                           float    value);
float         gtk_type_get_prop           (GtkType *type);
int           gtk_type_update_foobar      (GtkType *type);

如果您正在创建一个公共库,请尝试导出一个单个公共头文件,该头文件又将所有较小的头文件包含到其中。这样可以确保公共头文件绝不直接包含;而是使用单个包含项在应用程序中。

// The __GTK_H_INSIDE__ symbol is defined in the gtk.h header
// The GTK_COMPILATION symbol is defined only when compiling
// GTK itself
#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gtk/gtk.h> can be included directly."
#endif

对于库,所有头文件都应具有包含保护程序(用于内部使用)和 C++ 保护程序。这些提供了 C++ 需要包含纯 C 头文件的“extern”C”魔术

#pragma once

#include <gtk/gtk.h>

G_BEGIN_DECLS

// ...

G_END_DECLS

提示

您可以不使用 once 伪指令,而是使用显式的基于符号的包含保护程序:#ifndef FILE_H #define FILE_H ... #endif

GObject 类

GObject 类定义和实现需要一些额外的编码风格注意事项,并且应始终正确地命名空间。

类型声明应放置在文件的开头

typedef struct _GtkBoxedStruct       GtkBoxedStruct;
typedef struct _GtkMoreBoxedStruct   GtkMoreBoxedStruct;

这包括枚举类型

typedef enum
{
  GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT,
  GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH
} GtkSizeRequestMode;

以及回调类型

typedef void (* GtkCallback) (GtkWidget *widget,
                              gpointer   user_data);

实例结构应使用 G_DECLARE_FINAL_TYPE()G_DECLARE_DERIVABLE_TYPE() 宏声明

#define GTK_TYPE_FOO (gtk_foo_get_type ())
G_DECLARE_FINAL_TYPE (GtkFoo, gtk_foo, GTK, FOO, GtkWidget)

对于最终类型,私有数据可以存储在对象结构中,该结构应在 C 文件中定义

struct _GtkFoo
{
  GObject   parent_instance;

  guint     private_data;
  gpointer  more_private_data;
};

对于可派生类型,私有数据必须存储在 C 文件中的私有结构中,使用 G_DEFINE_TYPE_WITH_PRIVATE() 配置,并使用生成的 _get_instance_private() 函数访问

#define GTK_TYPE_FOO gtk_foo_get_type()
G_DECLARE_DERIVABLE_TYPE (GtkFoo, gtk_foo, GTK, FOO, GtkWidget)

struct _GtkFooClass
{
  GtkWidgetClass parent_class;

  void (* handle_frob)  (GtkFrobber *frobber,
                         guint       n_frobs);

  // Padding, for ABI compatible expansion of the class
  gpointer padding[12];
};

始终使用 G_DEFINE_TYPE()G_DEFINE_TYPE_WITH_PRIVATE()G_DEFINE_TYPE_WITH_CODE() 宏,或它们的抽象变体 G_DEFINE_ABSTRACT_TYPE()G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE()G_DEFINE_ABSTRACT_TYPE_WITH_CODE();此外,使用类似的宏来定义接口和装箱类型。

接口应使用 G_DECLARE_INTERFACE() 宏声明

#define GTK_TYPE_FOOABLE gtk_fooable_get_type()
G_DECLARE_INTERFACE (GtkFooable, gtk_fooable, GTK, FOOABLE, GObject)

内存分配

在堆上动态分配数据时,使用 g_new()

公共结构体类型应始终在归零后返回,显式地为每个成员归零,或使用 g_new0()

除非绝对必要,否则请尝试避免私有宏。请记住在需要它们的块或一系列函数结束时使用 #undef 它们。

内联函数通常优于私有宏。

公共宏不应用于评估为常量。

公共 API

避免将变量作为公共 API 导出,因为这在某些平台上很麻烦。最好添加 getter 和 setter 代替。此外,小心全局变量。

为了避免在共享库中暴露私有 API,建议默认使用 hidden 符号可见性,并在头文件中显式注释公共符号。

仅需要在单个源文件中使用的非导出函数应声明为该文件的 static