C 编码风格¶
本文档介绍了 GNOME 中 C 程序的首选编码风格。
虽然编码风格很大程度上取决于个人品味,但在 GNOME 中,我们倾向于一种能够促进一致性、可读性和可维护性的编码风格。
我们提供了良好编码风格的示例,以及不被 GNOME 接受的糟糕风格的示例。请尽量提交符合 GNOME 编码风格的补丁;这表明您已经尽了您的本分,以尊重项目长期可维护性的目标。采用 GNOME 编码风格的补丁也更容易审查!
重要提示
本文档适用于 C 代码。与 C 不同,其他编程语言有其自身的官方编码风格建议;我们鼓励您在适用时遵循它们。
这些准则深受 GTK 编码风格文档;Linux 内核编码风格;以及 GNU 编码标准的影响。这些彼此之间略有差异,针对每个项目的特定需求和文化进行了特别修改,GNOME 的版本也不例外。
最重要的规则¶
编写代码时最重要的规则是:检查周围的代码并尝试模仿它。
作为维护者,收到明显与周围代码风格不同的补丁会令人沮丧。这是不尊重的行为,就像有人穿着泥泞的鞋子闯入一尘不染的房子一样。
因此,无论本文档推荐什么,如果您已经编写了代码并且正在为其贡献代码,请保持其当前风格的一致性,即使它不是您最喜欢的风格。
最重要的是,不要让您对项目的第一次贡献是更改编码风格以适应您的口味。这非常不尊重人。
行宽¶
尝试使用 80 到 120 个字符长的代码行。这个长度的文本很容易适应大多数具有合理字体大小的显示器。超过该长度的行会难以阅读,并且意味着您可能应该重构您的代码。如果您有太多的缩进级别,这意味着您应该修复您的代码。
缩进¶
通常,GNOME 中代码的首选缩进风格有两种
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]++;
}
}
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);
“无单语句块”规则只有四个例外
在 GNU 风格中,如果
if…else语句的任一侧有大括号,则两侧都应有,以匹配缩进
/* valid */
if (condition)
{
foo ();
bar ();
}
else
{
baz ();
}
如果单语句跨越多行,例如对于具有许多参数的函数,并且后面跟着
else或else 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);
}
如果条件由多行组成
/* 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 ();
}
注意
这样的长条件通常很难理解。一个好的做法是将条件设置为一个布尔变量,并为该变量指定一个好的名称。另一种方法是将长条件移动到一个函数中。
嵌套
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 ();
}
提示
如果枚举类型的多数成员不应处理,请考虑使用 if … else 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。