在C语言中,如何构建复杂项目的结构?

82
我只有初级的C语言技能,想知道在C语言中是否有任何实际的“标准”来构建一个相对复杂的应用程序,包括基于GUI的应用程序。我一直使用Java和PHP中的OO范式,现在想学习C语言,但我担心我可能会以错误的方式构建我的应用程序。我不知道要遵循哪些指南才能在过程式语言中实现模块化、解耦和DRY原则。你有什么阅读建议吗?我找不到任何C语言的应用程序框架,即使我不使用框架,浏览它们的代码也能获得不错的想法。

3
Unix哲学对于组织大型项目非常有用:http://www.faqs.org/docs/artu/ch01s06.html - newDelete
9个回答

56

关键在于模块化。这样更容易设计、实现、编译和维护。

  • 识别应用程序中的模块,就像面向对象应用程序中的类一样。
  • 为每个模块分离接口和实现,在接口中只放置其他模块所需的内容。请记住,在C语言中没有命名空间,因此您必须使接口中的所有内容都是唯一的(例如,使用前缀)。
  • 在实现中隐藏全局变量,并使用访问器函数进行读/写操作。
  • 不要考虑继承,而是考虑组合。一般来说,不要尝试在C语言中模仿C++,这会使代码非常难以阅读和维护。

如果您有时间学习,请看看Ada应用程序如何结构化,以及其强制性的package(模块接口)和package body(模块实现)。

这是关于编码的。

对于维护(请记住,您只编写一次代码,但需要多次维护),我建议记录您的代码;Doxygen对我来说是一个不错的选择。我还建议构建一个强大的回归测试套件,以便您进行重构。


33

常见误解认为面向对象技术无法应用于 C 语言。其实大部分技术是可以的,只不过相对于那些专门为此设计语法的语言来说,会稍微麻烦一些。

健壮系统设计的基础之一是将实现封装在接口后面。FILE* 和与其一起工作的函数(如 fopen()、fread() 等)是在 C 语言中将封装应用于接口的好例子。(当然,由于 C 缺乏访问限定符,你无法强制执行不要窥视文件结构体 struct FILE 内部的规定,但只有受虐狂才会这样做。)

必要时,可以使用函数指针表在 C 语言中实现多态行为。是的,语法很丑陋,但效果与虚函数相同:

struct IAnimal {
    int (*eat)(int food);
    int (*sleep)(int secs);
};

/* "Subclass"/"implement" IAnimal, relying on C's guaranteed equivalence
 * of memory layouts */
struct Cat {
    struct IAnimal _base;
    int (*meow)(void);
};

int cat_eat(int food) { ... }
int cat_sleep(int secs) { ... }
int cat_meow(void) { ... }

/* "Constructor" */
struct Cat* CreateACat(void) {
    struct Cat* x = (struct Cat*) malloc(sizeof (struct Cat));
    x->_base.eat = cat_eat;
    x->_base.sleep = cat_sleep;
    x->meow = cat_meow;
    return x;
}

struct IAnimal* pa = CreateACat();
pa->eat(42);                       /* Calls cat_eat() */

((struct Cat*) pa)->meow();        /* "Downcast" */

11
一个纯粹的C语言编程人员阅读这段代码会感到迷惑... - mouviciel
15
@mouviciel:胡说!大多数C程序员都理解函数指针(或者至少应该理解),这里没有什么特别的事情发生。至少在Windows上,设备驱动程序和COM对象都是通过这种方式提供其功能的。 - j_random_hacker
10
我的观点不是关于无能,而是关于不必要的复杂化。对于C语言编程人员来说,函数指针很常见(例如回调),而继承并不常见。我更喜欢看到C++程序员在编写C代码时花时间学习C,而不是构建伪C++类。尽管如此,你的方法在某些情况下可能会有用。 - mouviciel
3
实际上,您甚至可以使用C++ pimpl习惯用法模拟访问说明符。如果将类型的private成员封装在仅在实现中可见的类型中(也称为“ .c文件”),则接口的用户将难以更改它们(当然,如果他们想要有意干扰您,他们可以向pimpl指针写入垃圾数据,但是您在C++中也可以这样做)。 - bitmask
1
@j_random_hacker:CreateACat() 不应该返回 struct Cat* 吗?我很惊讶没有其他人提到这一点,这让我觉得有些事情我不理解? - eklektek
显示剩余5条评论

17

所有都是好的答案。

我只想补充一点“尽量减少数据结构”。这在C语言中甚至可能更容易,因为如果说C++是“带有类的C语言”,面向对象编程则试图鼓励你把脑海中的每个名词/动词都变成类/方法。这可能非常浪费资源。

例如,假设您有一个时间序列温度读数数组,并且希望将它们显示为Windows中的折线图。Windows有一个PAINT消息,当您收到它时,可以循环遍历数组并进行LineTo函数操作,同时缩放数据以将其转换为像素坐标。

但是我已经看到过太多次的情况是,由于图表由点和线组成,人们会构建一个包含点对象和线对象的数据结构,每个对象都可以DrawMyself,并使其持久化,理论上认为这样会“更有效率”,或者他们可能需要能够用鼠标悬停在图表的某些部分并数值地显示数据,因此他们在对象中构建了用于处理此类问题的方法,这当然涉及创建和删除更多的对象。

因此,您最终得到了大量的代码,这些代码看起来非常清晰易读,但实际上只花费了90%的时间来管理对象。

所有这些都是为了“良好的编程实践”和“效率”而进行的。

至少在C语言中,简单有效的方法将更加明显,构建金字塔的诱惑也会更小。


1
喜欢你的回答,而且完全正确。面向对象编程必须消亡! - Jo So
@JoSo:我使用面向对象编程,但是很少用到。 - Mike Dunlavey
对我来说,“最小化”(虽然我不知道这对你意味着什么)并不算作面向对象编程,因为它是基于对象的编程 - 默认情况下使用对象。 - Jo So
我也使用了一些“面向对象编程”。主要是用于我的C模块,其中许多都有init()和exit()例程。 - Jo So

11

GNU编码标准已经发展了几十年。即使您不完全遵循它们,阅读它们仍然是一个好主意。思考其中提出的观点可以为您构建自己的代码结构提供更坚实的基础。


5
并非每个人都喜欢它们,来自http://lxr.linux.no/linux+v2.6.29/Documentation/CodingStyle的内容:“首先,我建议打印出GNU编码标准的副本,但不要阅读它。把它们烧掉,这是一个伟大的象征性姿态。”我多年来没有阅读过它们,但Linus提出了一些有效的反对意见。 - hlovdal
1
@hlovdal:当然,并不是每个人都喜欢任何一种编码标准,这就是为什么有多种类似用例的标准。重要的是在自己的项目中保持一致,遵循至少某种标准,而不是默认的临时不一致性。 - J. M. Becker

4
如果您知道如何在Java或C++中构建代码结构,那么您可以按照相同的原则编写C代码。唯一的区别是您没有编译器在身边,需要手动非常小心地完成所有事情。
因为没有包和类,所以您需要首先仔细设计模块。最常见的方法是为每个模块创建一个单独的源文件夹。您需要依靠命名约定来区分不同模块之间的代码。例如,使用模块名称作为函数前缀。
您不能在C中拥有类,但可以轻松实现“抽象数据类型”。为每个抽象数据类型创建一个.C和.H文件。如果您愿意,可以有两个头文件,一个公共的和一个私有的。全部结构、常量和需要导出的函数都放到公共头文件中。
您的工具也非常重要。对于C语言,lint是一个有用的工具,它可以帮助您找到代码中的问题。另一个您可以使用的工具是Doxygen,它可以帮助您生成文档

4

封装无论使用什么开发语言都是成功开发的关键。

我在C语言中用过的一个技巧是,在".h"文件中不包含"私有"方法的原型。


3
我建议您查看任何流行的开源C语言项目的代码,例如... 嗯... Linux内核或Git,并了解它们是如何组织代码的。

3
复杂应用的准则:易读性应该很高。
为了使复杂的应用程序更简单,我采用分而治之

3
我建议首先阅读一本C/C++教材。例如,《C Primer Plus》是一个很好的参考书。浏览示例将让您了解如何将您的Java面向对象映射到更加过程化的语言,如C。

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