C语言(或面向过程编程)的设计原则、最佳实践和设计模式是什么?

97

在设计C项目时,有哪些已知的设计原则、最佳实践和设计模式可以遵循?或者说,在设计过程式(命令式)编程的时候有哪些有用的设计原则呢?

(我是“面向对象的一代”,第一次设计一个大型的C项目)


1
你可能对这个问题的答案感兴趣:https://dev59.com/3nRB5IYBdhLWcg3wUVtd - mouviciel
7
在我发布问题之前,我进行了一些互联网和大学图书馆的研究,但并没有找到很多关于C语言软件设计方面的书籍。我想请你推荐一些你喜欢的(不是一般的C语言书籍,也不是关于编码规范比如有意义的变量名之类的内容,而是更高层次的软件架构)。此外,我不同意你对“依赖他人”的指责。你的意思是每个程序员都应该自己发现最佳实践和良好的设计模式吗?这当然是需要借鉴他人经验的问题。 - Dimi
2
抱歉,Dimi,那不是特指你,而且我表达得不够清楚。这些东西过去通常是通过口头传统来传授的,没有一个名义上的官方“模式”,Jonathon的回答是你在书中能找到的,但是每个人都知道信息隐藏。看起来口头传统正在失传,许多年轻程序员认为面向对象编程发明了封装和分离。这个社区似乎比我想象中更缺乏自己的历史意识。因此,我承认我处于一个脾气暴躁的老人领域。 - dmckee --- ex-moderator kitten
1
我不能分享你的回顾观点,因为我刚刚踏入这个领域,但我接受你的建议。谢谢你更清晰地表达自己,读到经验丰富的人的观点总是非常有价值的。我非常感激你的贡献。 - Dimi
SEI CERT C 编码标准提供了一套良好的规则和常见的最佳实践,同时也提供了应该避免使用的内容。 - Rand0m
4个回答

67

信息隐藏-如Parnas所提倡(软件基础)。

仔细管理标头和可见性:

  • 源文件中可以隐藏在外部世界之外的所有内容,只有记录的外部接口应该暴露出来。
  • 所有公开的内容都在标头中声明。
  • 在需要功能的地方(以及定义它的地方)使用该标头。
  • 标头是自包含的-当您需要它时,您使用它,并且您不必担心“我还必须包括哪些其他标头”,因为标头通过包括任何使其工作所需的东西来确保其工作。
  • 标头是自我保护的,因此无论是否多次包含都没有关系。

#ifndef HEADER_H_INCLUDED
#define HEADER_H_INCLUDED
...rest of header contents, including other #include lines if necessary
#endif /* HEADER_H_INCLUDED */
  • 设计一组可以处理“对象”(通常为结构)的函数,并在使用该对象的代码中使用这些函数,而不是直接操作结构本身。将其视为自我限制的封装。


  • 很好的观点,谢谢你,Jonathan。抽象数据类型是信息隐藏的另一个很好的例子,具有干净的使用和实现分离(已知外部接口和未知内部实现)。 - Dimi

    25

    我的三个建议:

    • 编写单元测试。随着项目的推进,它们将帮助您针对问题零散地设计,比仅依赖预先考虑更好。
    • 安装并运行内存泄漏检测器(有各种库可用)。让该库在程序/测试退出时打印出所有泄漏信息。这将使您能够尽早捕获内存泄漏,从而使其修复变得不那么痛苦。
    • 使用 C 编写面向对象(OOP)代码。这并不难。虽然可以模拟方法重写,但我建议您从简单对象的模拟开始。即使是这种简单机制也可以为您带来很大的收益。

    这里是一个例子:

    typedef struct Vector {
      int size;
      int limit;
      int* ints; 
    } Vector;
    
    Vector* Vector_new() {
      Vector* res = (Vector*) malloc(sizeof(Vector));
      res->limit = 10;
      res->size = 0;
      res->ints = (int*) malloc(sizeof(int) * res.limit);
    
      return res;
    }
    
    
    void Vector_destroy(Vector* v) {
      free(v->ints);
      free(v);
    }
    
    void Vector_add(Vector* v, int n) {
      if(v->size == v->limit) {
        v->limit = v->limit * 2 + 10;
        v->ints = realloc(v->ints, v->limit);     
      }
    
      v->ints[v->size] = n;
      ++v->size;
    }
    
    int Vector_get(Vector* v, int index) {
      if(index >= 0 && index < v->size)
        return v->ints[index];
    
      assert false;
    }
    

    1
    1. 不要转换 malloc 的结果。
    - S.S. Anne

    22

    有一本好的、免费的在线书籍,名为《Object-Oriented Programming With ANSI-C》,介绍了在C语言中编写面向对象代码的主题。通过谷歌搜索“面向对象C”,还可以找到许多其他好的例子和资源。

    如果您的项目是安全关键型的,则MISRA-C是一个不错的规则集。虽然它主要是针对嵌入式C语言的,但在其他领域也可能很有用。

    我认为自己是个面向对象的程序员,并且经常使用嵌入式C进行开发。我能给出的最好建议,特别是对于大项目,就是不要过度做。在ANSI C之上创建完整的面向对象框架可能会非常诱人,但要想做得正确需要花费大量的时间和精力。你越是追求花哨的东西,你花在调试框架上的时间就越多而非真正开发项目。务必冷静思考,并牢牢掌握YAGNI。祝您好运!


    谢谢,e.James。我不想在ANSI C之上创建面向对象的框架,而是寻找特殊和适当的过程式编程设计原则。MISRA-C提示非常有用,特别是因为它实际上是一个嵌入式项目。我将仔细研究它。 - Dimi
    啊,嵌入式C的乐趣。别忘了你必须在函数的顶部(或任何 { } 块的顶部)声明你的变量。这个问题总是会咬我一两次 :) - e.James

    8

    OOP是一种方法论而不是技术。所以我的第一条建议是停止将其视为过程式编程。

    针对e.James的观点,你不想尝试重新创建面向对象的语言或者假装你有这样的能力。你仍然可以通过坚持几个简单的原则来做到正确的事情:

    1. 测试驱动所有。
    2. 找到变化并将其封装。
    3. 设计接口。

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