纯C(非C++)的编码规范

41

我有Java编程背景(在我的计算机科学课程中学过)和一学期的C++。我刚刚完成了一个OpenCV项目,该项目是用纯C编写的,所以我有些晚才问这个问题。

纯C的设计流程和编码标准是什么?

我熟悉面向对象编程、设计和最佳实践。但对于非面向对象语言,比如C,我有点不知所措。每个变量和函数似乎都是全局的,这让我感觉很混乱。


函数是“全局的”,但变量可以有不同的作用域... - paercebal
3
函数当然可以是全局的或文件静态的。 - anon
1
@GMan,如果有足够数量的不同用户编辑了这个问题,它就会自动变成CW(社区维护)。我已经投了我的CW票... - bdonlan
4
在我看来这是对编辑功能的严重误用。 - anon
11个回答

37

你可能会对我不久前提出的一个类似问题的答案感兴趣。此外,如果你对C语言风格指南感兴趣,可以看看这个页面,因为它是C(和C ++)风格指南的存储库。如果你想要一些欢乐时光,请看看NASA C语言风格指南。特别是那个巨大的注释...你会知道我在说哪个。请不要写这样的注释。

我个人推荐Indian Hill C Style Guide并进行一些修改。此外,如果您在设计C语言的大型程序时遇到困难,可以考虑购买C Interfaces and Implementations这本书。


6
+1是因为这篇文章提供了多个链接到C标准的网页。 - paercebal
遗憾的是,印第安山的链接现在已经404了。然而,通过谷歌搜索“indian hill c style guide pdf”很快就可以找到许多可能的来源。C和C++风格指南似乎特别有用,至少对于像我这样的古董来说。 - Jonathan Leffler
如果你在寻找NASA C风格指南的“搞笑”部分,可以看看第64页! - Trent
1
印度山指南的链接无法使用。我在网络存档中找到了它,在这里 - sushant_padha

15

我认为在StackOverflow上获得的任何答案都无法教授你如何设计和编写良好结构化的C程序。你需要阅读一本好书,而最明显的选择是由Kernighan和Ritchie所写的The C Programming Language


7
K&R并没有涉及设计流程和编码规范方面的内容。它是C语言入门的好选择。 - novelistparty

9

我在C语言方面没有专业经验(只有C++),所以请不要太认真地对待我的建议、技巧和提示,因为它们是“对象式导向”的。

几乎像Object C?

模拟基本的对象式特性可以很容易地完成:

在头文件中,前向声明你的类型,typedef它,并声明“方法”。例如:

/* MyString.h */

#include <string.h>

/* Forward declaration */
struct StructMyString ;

/* Typedef of forward-declaration (note: Not possible in C++) */
typedef struct StructMyString MyString ;

MyString *       MyString_new() ;
MyString *       MyString_create(const char * p_pString) ;
void             MyString_delete(MyString * p_pThis) ;
size_t           MyString_length(const MyString * p_pThis) ;

MyString *       MyString_copy(MyString * p_pThis, const MyString * p_pSource) ;
MyString *       MyString_concat(MyString * p_pThis, const MyString * p_pSource) ;

const char *     MyString_get_c_string(const MyString * p_pThis) ;
MyString *       MyString_copy_c_string(MyString * p_pThis, const char * p_pSource) ;
MyString *       MyString_concat_c_string(MyString * p_pThis, const char * p_pSource) ;

你会看到每个函数都有前缀。我选择了“struct”的名称,以确保不会与其他代码发生冲突。

你也会看到,我使用“p_pThis”来与面向对象的思想保持一致。

在源文件中,定义你的类型,并定义函数:

/* MyString.c */

#include "MyString.h"

#include <string.h>
#include <stdlib.h>

struct StructMyString
{
   char *      m_pString ;
   size_t      m_iSize ;
} ;

MyString * MyString_new()
{
   MyString * pMyString = malloc(sizeof(MyString)) ;

   pMyString->m_iSize = 0 ;
   pMyString->m_pString = malloc((pMyString->m_iSize + 1) * sizeof(char)) ;
   pMyString->m_pString[0] = 0 ;

   return pMyString ;
}

/* etc. */

如果您想要“私有”函数(或私有全局变量),请在C源代码中将它们声明为静态的。这样,它们就不会在外部可见:
static void doSomethingPrivate()
{
   /* etc. */
}

static int g_iMyPrivateCounter = 0 ;

如果你想要继承,那么你几乎是处境艰难的。如果你认为C语言中的所有内容都是全局的,包括变量,那么在尝试思考如何模拟继承之前,你应该获得更多的C语言经验。

杂项提示

避免多个代码路径。

例如,多个返回值是有风险的。例如:

void doSomething(int i)
{
   void * p = malloc(25) ;

   if(i > 0)
   {
      /* this will leak memory ! */
      return ;
   }

   free(p) ;
}

避免非const全局变量

这包括“静态”变量(不是静态函数)。

全局非const变量几乎总是一个坏主意(例如,查看C API strtok以获取糟糕函数的示例),如果生成多线程安全代码,则处理它们会很麻烦。

避免名称冲突

为您的函数和定义选择一个“命名空间”。这可以是:

#define GROOVY_LIB_x_MY_CONST_INT 42

void GroovyLib_dosomething() ;

今日免费次数已满, 请开通会员/明日再来
#define MAX(a, b) (a > b) ? (a) : (b)

void doSomething()
{
   int i = 0, j = 1, k ;
   k = MAX(i, j) ;   /* now, k == 1, i == 0 and j == 1 */
   k = MAX(i, j++) ; /* now, k == 2, i == 0 and j == 3, NOT 2, and NOT 1 !!! */
}

初始化你的变量

避免声明未初始化的变量:

int i = 42 ; /* now i = 42 */
int j ;      /* now j can have any value */
double k ;   /* now f can have any value, including invalid ones ! */

未初始化的变量是痛苦错误的原因。

了解所有C API

在K&R中描述的C API函数列表非常小。您可以在20分钟内阅读整个列表。您必须知道这些函数。

想要一些经验吗?

重写C API。例如,尝试编写自己版本的string.h函数,以查看如何完成。


1
我对任何以“我在C语言方面没有专业经验”开头的答案持谨慎态度。但是这个建议还可以。也许你应该更好地解释一下你的观点和推理,这样别人就能清楚地知道他们是否应该遵循你的建议。 - novelistparty
“继承”可以通过结构体组合来模拟。MSVC 可以毫无问题地处理这个过程。GCC 可能需要使用 -fms-extensions 选项。 - Mike

8
你可以通过声明“static”来使函数或对象的作用域局限于其源文件。这有一点帮助。
否则,我看到避免命名空间冲突的典型习惯用法是在名称上加上某种设施标识符。例如,foo.c中的所有内容都可以以foo_*命名。

3

和其他优秀的C程序员一起工作。与他们进行代码审查。不仅让他们查看你的代码,也要查看他们的代码。


3
好消息是,您可以在C语言中以半面向对象的方式进行编程。您可以保护数据,公开访问函数等等。虽然它可能没有C++的所有花哨功能,但从我所见其他人的C++代码来看,许多人也不使用这些花哨的功能。换句话说,人们在类内部编写C代码,在C语言中,您会在没有类容器的情况下编写相同的代码。
首先,阅读一本关于C语言编程和风格的书籍,K&R是可以的。其次,我建议查看CERT编程标准。尽管该网站主要关注“安全”编码标准,但这里的许多内容都是每个人都应遵循的通用代码质量标准。执行此处提到的操作将改进您的质量,消除令人烦恼的错误,并作为副作用使您的代码更加安全。

更新链接至:SEI CERT C 编码标准 - Ricardo Barroso

2

你可能希望仔细研究Linux内核的源代码......顺便说一句,这不是最容易入手的代码,但由于你有编程背景,它可能会有所帮助......作为一个面向对象的程序员,你可能会发现在C语言中处理错误是一个艰巨的任务。也许这可以帮助你——> http://www.freetype.org/david/reliable-c.html


1

你可以在纯C中进行面向对象设计。一种简单的方法是将模块视为一个带有公共方法的class,这些公共方法作为普通函数,需要将this参数作为显式的第一个参数。

如果class名称是函数名称的前缀,并且所有私有函数和类数据都声明为static,则会有所帮助。您可以使用malloc()构建构造函数以获取内存,并明确初始化数据字段。

具有完全私有数据成员的对象的构造函数可以公开不透明指针(甚至可以类型为void *,或者作为指向不完整类型的指针,如果需要类型安全性)。如果您只想拥有公共数据成员,则指向公开定义的struct的指针效果很好。

许多库都遵循此模式。初始化函数返回必须传回所有库方法的cookie,而一个方法则充当析构函数。

当然,在纯C中还有其他良好的设计方式,但如果面向对象适用于您,您不必完全放弃它。


1
请注意,这种设计中唯一的面向对象特性是封装。继承和多态性需要更多的功能,但无论使用哪种语言,封装始终是一个好东西。 - paercebal
@paercebal,没错。使用函数指针可以实现相当数量的多态性,但是如果没有名称修饰,就无法进行重载。继承也可以以直接的方式建模。值得记住的是,毕竟C++最初是作为一个预处理器来编写纯C的。 - RBerteig

1

您可以将文件范围的变量和函数的可见性限制在它们各自的源文件中(尽管这并不能阻止您在对象周围传递指针)。

例如:

/** foo.c */
static void foo_helper() {...} /* foo_helper cannot be called by name 
                                  outside of foo.c */
static int local_state;        /* local state is visible at file scope,
                                  but is not exported to the linker */

0
C不是C++。
有一些概念适用于两者,我认为学习如何编写干净的C代码会让你成为一名更好的C++程序员。
思考“面向数据”而不是“面向对象”。
C++之所以保留了结构体而不将所有东西转换为类,是有原因的。它们可以做同样的事情,但我们对它们的思考方式不同。 我会说,首先在心里接受一个“类”只是一个结构体,类方法只是将结构体作为第一个参数传递的函数。
不,不是一切都是全局的。有三个作用域,是的,四个。
  1. 全局变量应该尽量少用,只有在结构定义和必须为全局变量时才使用。头文件用于明确指定函数、类型定义和结构定义的可见性。
  2. 模块(文件)作用域通常是你应该使用的全局变量的替代方案。函数和变量可以(并且应该)被定义为静态变量,限制在模块(文件)作用域内,除非它们是外部 API 的一部分。也就是说,任何全局变量或函数,但不被外部头文件引用的变量或函数,都应该被定义为静态变量。
  3. 函数作用域非常简洁清晰,将数据传递给函数甚至比 C++ 的做法更加简洁,不会因为每个小的跟踪变量而使对象膨胀。
  4. 块作用域是最好的选择,如果你的函数变得很大。我的感觉是,如果任何东西可以限制在块作用域内,那就应该这样做。
总结一下,C语言是一种来自更文明时代的优雅武器。简洁性是它的主要优点之一。它拥有几乎无尽的可用库列表,并且对于什么是良好的C代码有着几代人的意见。也就是说,你可能认为在更现代的语言中作为语言的一部分而被视为理所当然的结构和算法,往往要么来自库,要么需要自己编写。关于隐私和继承,我可以说一些事情,但是我似乎无法写出我想要表达的内容而不显得傲慢,所以简单地说,它们并不是计算的基本要素,可以不用它们进行操作。如果真的需要,可以通过预编译库在C语言中实现隐私。
C语言存在已经很长时间了,它最大的缺点不是缺乏面向对象,而是内存安全性。值得花时间去了解堆栈溢出、数组越界、超出作用域的变量暴露等问题是如何发生的。大多数语言中都存在这些问题的类似情况,但在C语言中,你更容易直接面对这些问题的原始形式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接