C 编程/常见实践
随着 C 语言的广泛使用,许多常见的实践和约定已经发展起来,以帮助避免 C 程序中的错误。这些同时证明了将良好的软件工程原则应用于一种语言,也表明了 C 语言的局限性。虽然很少有人被普遍使用,并且有些人存在争议,但这些方法中的每一种都被广泛使用。
虽然使用 malloc 可以轻松创建动态的一维数组,并且使用内置语言功能可以轻松创建固定大小的多维数组,但动态多维数组则更加复杂。有许多不同的方法可以创建它们,每种方法都有不同的权衡。创建它们最流行的两种方法是
- 它们可以作为一块内存分配,就像静态多维数组一样。这要求数组是矩形(即低维子数组是静态的并且具有相同的大小)。缺点是声明指针的语法对于初学者来说有点棘手。例如,如果想创建一个具有 3 列和行的整数数组行,则应执行
int (*multi_array)[3] = malloc(rows * sizeof(int[3]));
- (注意这里multi_array是指向包含 3 个整数的数组的指针。)
- 由于数组指针的可互换性,可以像静态多维数组一样对其进行索引,即multi_array[5][2]是第 6 行第 3 列的元素。
- 动态多维数组可以通过先分配一个指针数组,然后分配子数组并将它们的地址存储在指针数组中来分配。[1] (这种方法也称为Iliffe 向量)。访问元素的语法与上面描述的多维数组相同(即使它们的存储方式非常不同)。这种方法的优点是可以创建锯齿状数组(即子数组的大小不同)。但是,它也使用更多空间,需要更多级别的间接寻址才能进行索引,并且缓存性能可能更差。它还需要许多动态分配,每个动态分配都可能很昂贵。
有关更多信息,请参阅comp.lang.c FAQ,问题 6.16。
在某些情况下,使用多维数组最好作为结构数组来处理。在用户定义的数据结构可用之前,一种常见的技术是定义一个多维数组,其中每一列包含有关该行的不同信息。这种方法也经常被初学者使用。例如,二维字符数组的列可能包含姓氏、名字、地址等。
在这种情况下,最好定义一个结构来包含存储在列中的信息,然后创建一个指向该结构的指针数组。当给定记录的数据点数量可能不同时,这一点尤其重要,例如专辑中的曲目。在这种情况下,最好创建一个包含专辑信息的结构,以及专辑歌曲列表的动态数组。然后,可以使用指向专辑结构的指针数组来存储集合。
- 另一种创建动态多维数组的有用方法是将数组展平并手动索引。例如,一个大小为 x 和 y 的二维数组具有 x*y 个元素,因此可以通过
int dynamic_multi_array[x*y];
索引比以前稍微复杂一些,但仍然可以通过 y*i+j 获得。然后可以使用
static_multi_array[i][j];
dynamic_multi_array[y*i+j];
来访问数组。一些更高维度示例
int dim1[w];
int dim2[w*x];
int dim3[w*x*y];
int dim4[w*x*y*z];
dim1[i]
dim2[w*j+i];
dim3[w*(x*i+j)+k] // index is k + w*j + w*x*i
dim4[w*(x*(y*i+j)+k)+l] // index is w*x*y*i + w*x*j + w*k + l
请注意,w*(x*(y*i+j)+k)+l 等于 w*x*y*i + w*x*j + w*k + l,但使用更少的运算(请参见霍纳规则)。它使用的运算数量与通过 dim4[i][j][k][l] 访问静态数组相同,因此使用起来不应该慢。
使用这种方法的优点是,数组可以在函数之间自由传递,而无需在编译时知道数组的大小(因为 C 将其视为一维数组,尽管仍然需要某种传递维度的方法),并且整个数组在内存中是连续的,因此访问连续元素应该很快。缺点是,刚开始可能很难习惯如何索引元素。
在大多数面向对象的语言中,对象不能由希望使用它们的客户端直接创建。相反,客户端必须要求类使用称为构造函数的特殊例程来构建对象的实例。构造函数很重要,因为它们允许对象在其整个生命周期内强制其内部状态的不变性。析构函数在对象生命周期的末尾被调用,在系统中很重要,在这些系统中,对象持有对某些资源的独占访问权限,并且希望确保它释放这些资源供其他对象使用。
由于 C 不是面向对象的语言,因此它没有对构造函数或析构函数的内置支持。客户端通常显式地分配和初始化记录和其他对象。但是,这会导致潜在的错误,因为如果对象没有正确初始化,则对对象的运算可能会失败或行为不可预测。更好的方法是使用一个函数来创建对象的实例,该函数可能会采用初始化参数,如以下示例所示
struct string {
size_t size;
char *data;
};
struct string *create_string(const char *initial) {
assert (initial != NULL);
struct string *new_string = malloc(sizeof(*new_string));
if (new_string != NULL) {
new_string->size = strlen(initial);
new_string->data = strdup(initial);
}
return new_string;
}
类似地,如果让客户端正确销毁对象,他们可能无法这样做,从而导致资源泄漏。最好有一个始终使用的显式析构函数,例如以下析构函数
void free_string(struct string *s) {
assert (s != NULL);
free(s->data); /* free memory held by the structure */
free(s); /* free the structure itself */
}
将析构函数与#将释放的指针置空结合起来通常很有用。
有时隐藏对象的定义很有用,以确保客户端不会手动分配它。为此,结构在源文件(或用户无法使用的私有头文件)中定义,而不是在头文件中定义,并在头文件中进行前向声明
struct string;
struct string *create_string(const char *initial);
void free_string(struct string *s);
如前所述,在对指针调用free()
后,它会变成悬空指针。更糟糕的是,大多数现代平台无法检测到这种指针在被重新分配之前是否被使用。
一个简单的解决方案是确保任何指针在被释放后立即被设置为一个空指针:[2]
free(p);
p = NULL;
与悬空指针不同,当空指针被解除引用时,许多现代架构会在硬件上引发异常。此外,程序可以包含对空值的错误检查,但不能包含对悬空指针值的错误检查。为了确保在所有位置都执行此操作,可以使用宏
#define FREE(p) do { free(p); (p) = NULL; } while(0)
(要了解为什么宏以这种方式编写,请参见#宏约定。)此外,当使用这种技术时,析构函数应该将传递给它们的指针清零,并且它们的实参必须通过引用传递以允许这样做。例如,以下来自#构造函数和析构函数的析构函数已更新
void free_string(struct string **s) {
assert(s != NULL && *s != NULL);
FREE((*s)->data); /* free memory held by the structure */
FREE(*s); /* free the structure itself */
}
不幸的是,这种习惯用法不会对可能指向已释放内存的任何其他指针做任何事情。因此,一些 C 专家认为这种习惯用法很危险,因为它会产生一种错误的安全感。
由于 C 中的预处理器宏使用简单的标记替换工作,因此它们容易出现许多令人困惑的错误,其中一些错误可以通过遵循一组简单的约定来避免
- 在任何可能的地方将括号放在宏参数周围。这样可以确保,如果它们是表达式,则运算顺序不会影响表达式的行为。例如
- 错误:
#define square(x) x*x
- 更好:
#define square(x) (x)*(x)
- 错误:
- 如果它是单个表达式,则将括号放在整个表达式周围。同样,这避免了由于运算顺序而导致的含义更改。
- 错误:
#define square(x) (x)*(x)
- 更好:
#define square(x) ((x)*(x))
- 危险,记住它会直接替换文本。假设你的代码是
square (x++)
,在宏调用后,x 会增加 2
- 错误:
- 如果一个宏生成多个语句,或声明变量,它可以被包装在一个 do { ... } while(0) 循环中,没有终止分号。这样可以使宏像单个语句一样在任何地方使用,例如 if 语句的语句体,同时仍然允许在宏调用后放置分号而不会创建空语句。[3][4][5][6][7][8] 必须注意,任何新变量都不能潜在地掩盖宏参数的一部分。
- 错误:
#define FREE(p) free(p); p = NULL;
- 更好:
#define FREE(p) do { free(p); p = NULL; } while(0)
- 错误:
- 尽可能避免在宏内两次或多次使用宏参数;这会导致宏参数包含副作用(如赋值)的问题。
- 如果一个宏将来可能被函数替换,可以考虑将其命名为函数。
- 按照惯例,由
#define
定义的预处理器值和宏以全大写字母命名。[9][10][11][12][13]
进一步阅读
[edit | edit source]- ↑ Adam N. Rosenberg. [http://www.the-adam.com/adam/rantrave/st02.pdf "A Description of One Programmer’s Programming Style Revisited"]. 2001. p. 19-20.
- ↑ comp.lang.c FAQ list: "Why isn't a pointer null after calling free?" mentions that "it is often useful to set [pointer variables] to NULL immediately after freeing them".
- ↑ "comp.lang.c FAQ: What's the best way to write a multi-statement macro?".
- ↑ "The C Preprocessor: Swallowing the Semicolon"
- ↑ "Why use apparently meaningless do-while and if-else statements in macros?"
- ↑ "do {...} while (0) in macros"
- ↑ "KernelNewbies: FAQ / DoWhile0".
- ↑ "PRE10-C. Wrap multistatement macros in a do-while loop".
- ↑ "What is the history for naming constants in all uppercase?"
- ↑ "The Preprocessor".
- ↑ "C Language Style Guide".
- ↑ "non capitalized macros are always evil".
- ↑ "Exploiting the Preprocessor for Fun and Profit".
C 语言风格指南有很多。
- "C and C++ Style Guides" by Chris Lott lists many popular C style guides.
- 汽车行业软件可靠性协会 (MISRA) 发布了 "MISRA-C: Guidelines for the use of the C language in critical systems"。(Wikipedia: MISRA C; [1])。