如何更新旧的C代码?

我这周在工作上一直在做一些10年前的C代码,经过一些修改之后,我去找老板,问他是否需要做其他事情。 那是他放下炸弹的时候 我的下一个任务是通过7000左右的线路,了解更多的代码, 在一定程度上模块化代码。 我问他如何将源代码模块化,他说开始把旧的C代码放到C ++类中。

作为一名优秀的员工,我点点头,回到桌前,现在坐在那里,想知道如何在世界上采取这些代码,并“模块化”它。 它已经在20个源文件中,每个都有自己的目的和function。 另外还有三个“主要”结构。 这些结构中的每一个都有30个以上的字段,其中许多是其他较小的结构。 这是一个完全混乱的尝试去理解,但是程序中的几乎每一个函数都会传递一个指向其中一个结构体的指针,并大量使用这个结构体。

有什么干净的方法让我把这个问题搞成课? 我决心要做到这一点,我只是不知道如何开始。

首先,你很幸运有一个老板认识到代码重构可以是一个长期的节约成本的策略。

我已经做了很多次了,也就是将旧的C代码转换成C ++。 好处可能会让你大吃一惊。 完成后,最终的代码可能是原始大小的一半,而且读起来更简单。 另外,你可能会发现一些棘手的C错误。 以下是我会采取的步骤。 小的步骤很重要,因为重构大量代码时不能从A跳​​到Z。 您必须经历小的中间步骤,这些步骤可能永远不会部署,但可以在您使用的任何RCS中进行validation和标记。

  1. 创build一个回归/testing套件。 每次您完成一批代码更改时,您将运行testing套件。 你应该已经有了,而且这对于这个重构任务来说是非常有用的。 花点时间使其全面。 创buildtesting套件的练习将使您熟悉代码。
  2. 将项目分支到您select的版本控制系统中。 有了testing套件和操场分支,你将被授权对代码进行大的修改。 你不会害怕打破一些鸡蛋。
  3. 使这些结构字段私有。 这一步需要很less的代码更改,但可以有很大的回报。 一次继续一个字段。 尝试使每个字段private (是,或保护),然后隔离访问该字段的代码。 最简单,最不干扰的转换就是使代码成为friend function 。 考虑让代码成为一种方法。 将代码转换为方法很简单,但您也必须转换所有的调用网站。 一个不一定比另一个好。
  4. 缩小每个function的参数。 任何函数都不可能访问作为其parameter passing的结构的所有30个字段。 而不是传递整个结构,只传递所需的组件。 如果一个函数实际上似乎需要访问结构的许多不同的字段,那么这可能是一个很好的候选者,可以转换为一个实例方法。
  5. build立尽可能多的variables,参数和方法。 很多旧的C代码都不能使用const 。 通过从下往上扫描(即调用图的底部),您将为代码添加更强的保证,并且您将能够从非增变器中识别增变器。
  6. 用合适的引用replace指针 。 这一步的目的与更多的C ++无关 – 就像为了更像C ++一样。 目的是识别永远不会为NULL且永远不会被重新分配的参数。 把一个引用看作是一个编译时断言, 这个断言说, 这是一个有效对象的别名,并且在整个当前范围内表示同一个对象。
  7. char*replace为std::string 。 这一步应该是显而易见的。 你可能会大大减less代码行数。 另外,用一行代替10行代码是很有趣的。 有时候可以删除整个函数,其目的是执行C ++中标准的Cstring操作。
  8. 将C数组转换为std::vectorstd::array 。 再次,这一步应该是显而易见的。 这个转换比从charstd::string的转换简单得多,因为std::vectorstd::array的接口被devise为匹配C数组语法。 其中一个好处是可以消除传递给数组中每个函数的额外lengthvariables。
  9. malloc / free转换为new / delete 。 这一步的主要目的是为将来的重构做准备。 仅仅将C代码从malloc改为new并不会直接获得很多。 这个转换允许你为这些结构添加构造函数和析构函数,并使用内置的C ++自动记忆工具。
  10. std::auto_ptr系列replace本地化new / delete操作。 这一步的目的是让你的代码exception安全。
  11. 通过冒泡来处理返回代码的地方抛出exception。 如果C代码通过检查特殊的错误代码来处理错误,然后将错误代码返回给调用者,依此类推,将错误代码冒泡到调用链中,那么C代码可能是使用exception的候选方式。 这个转换实际上是微不足道的。 只需在最低级别throw返回码(C ++允许你抛出任何你想要的types)。 在处理错误的代码中插入try{} catch(){}语句。 如果没有合适的地方来处理错误,请考虑将main()的主体包装在try{} catch(){}语句中并logging下来。

现在退一步,看看你有多less改进的代码, 没有任何东西转换为类 。 (是的,从技术上讲,你的结构已经是类了。)但是,你还没有抓破OO的表面,而是设法大大简化和巩固原来的C代码。

如果您将代码转换为使用类,多态和一个inheritance图? 我拒绝。 C代码可能没有一个适合OO模型的整体devise。 请注意,上面每个步骤的目标与将OO原则注入C代码无关。 目标是通过执行尽可能多的编译时间限制来改进现有代码,并消除或简化代码。

最后一步。

考虑添加基准,以便在完成后将其显示给老板。 不只是性能基准。 比较代码行,内存使用情况,function数量等

真的,7000行代码不是很多。 对于这样less量的代码,可以按顺序进行完整的重写。 但是这个代码是如何被调用的呢? 大概呼叫者期待一个C API? 或者这不是一个图书馆?

无论如何,重写或不重写,在你开始之前,确保你有一套testing,你可以很容易地运行,而不需要人为干预。 然后,对每一个你所做的改变,运行新代码的testing。

对C ++来说,这似乎是任意的,问问你的老板他为什么需要这样做,弄清楚你是否可以不那么痛苦地达到同样的目标,看看你是否可以以一种不那么痛苦的方式创build一个子集,然后去演示你的老板,build议你遵循不那么痛苦的方式。

首先,告诉你的老板你不会继续,直到你有:

http://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672

在较小程度上:

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

其次,没有办法通过将其编程成C ++类来模块化代码。 这是一个巨大的任务,你需要把重构高度程序代码的复杂性传达给你的老板。

归结为做一个小的改变(提取方法,移动方法到课堂等),然后testing – 这个没有捷径。

我确实感到你的痛苦

我想这里的想法是,增加模块化将隔离代码片段,以便将来的变化得到促进。 我们有信心改变一个片断,因为我们知道它不会影响其他片断。

我看到两个噩梦场景:

  1. 你有很好的结构化C代码,它很容易转换成C ++类。 在这种情况下,它可能已经非常模块化了,你可能没有做任何有用的事情。
  2. 这是一个相互关联的东西。 在这种情况下,解决这个问题将非常困难。 增加模块化将是一件好事,但这将是一个漫长的艰难时刻。

但是,也许有一个快乐的媒介。 难道有那些重要的,概念上孤立的逻辑,但由于缺乏数据隐藏等原因,这些逻辑很脆弱(是的,C不会受到这个影响,但是我们没有这个,否则我们会离开的单独)。

拿出一个类来拥有这个逻辑及其数据,封装这个块可能是有用的。 用C还是C ++来做这件事情是最好的。 (我这个玩世不恭的人说:“我是C程序员,伟大的C ++有机会学习新东西!”)

所以:我会把它当成一头吃大象的。 首先决定是否应该吃,不好的elephent只是没有乐趣,结构良好的C应该被单独留下。 第二次find合适的第一口。 我会回应Neil的评论:如果你没有一个好的自动化testing套件,你注定会失败的。

我认为一个更好的方法可以完全重写代码,但是你应该问你的老板,他希望你“ 开始把旧的C代码放入C ++类 ”。 你应该要求更多的细节

这当然是可以做到的 – 问题是成本是多less? 这是一个巨大的任务,即使是7K LOC也是如此。 你的老板必须明白,这将花费很多时间,而你不能在有光泽的新function等工作。如果他不完全明白这一点,和/或不愿意支持你,没有一点启动。

正如@大卫已经build议,重构书是必须的。

从你的描述来看,这听起来像是很大一部分代码已经是“类方法”,其中函数获得一个指向结构实例的指针,并在该实例上工作。 所以它可以很容易地转换成C ++代码。 诚然,这不会使代码更容易理解或更好的模块化,但如果这是您老板的主要愿望,那么可以这样做。

还要注意,这部分重构是一个相当简单的机械过程,因此在没有unit testing的情况下可以相当安全地完成(当然,编译过程也是如此)。 但是对于任何事情你还需要进行unit testing,以确保你的改变不会破坏任何东西。

这个练习是不太可能的。 良好的C代码已经比C ++更加模块化 – 通常使用指向结构的指针可以使编译单元独立于C ++中的pImpl – 在C中,您不必将结构中的数据公开暴露其界面。 所以,如果你打开每个C函数

 // Foo.h typedef struct Foo_s Foo; int foo_wizz (const Foo* foo, ... ); 

到一个C ++类中

 // Foo.hxx class Foo { // struct Foo members copied from Foo.c int wizz (... ) const; }; 

与C代码相比,您将减less系统的模块性 – 如果将任何私有实现函数或成员variables添加到Footypes,则Foo的每个客户端现在都需要重新构build。

C ++中的类有很多东西可以给你,但是模块化不是其中之一。

向你的老板询问这个练习所达到的商业目标。

关于术语的说明:

系统中的一个模块是一个具有良好定义的接口的组件,可以用另一个具有相同接口的模块replace,而不影响系统的其余部分。 由这些模块组成的系统是模块化的。

对于这两种语言,模块的接口通常是一个头文件。 考虑将string.hstring定义为C和C ++中的简单string处理模块的接口。 如果在string.h的实现中有一个bug,则会安装一个新的libc.so。 这个新模块具有相同的接口,dynamic链接到它的任何东西立即得到新实现的好处。 相反,如果在std::string中有string处理的错误,那么每个使用它的项目都需要重build。 C ++引入了非常大量的耦合到系统中,语言没有任何缓解 – 实际上,充分利用其特性的C ++的更好的用途往往比等效的C代码更加紧密耦合。

如果您尝试使C ++模块化,那么通常最终会得到类似于COM的东西,其中每个对象都必须同时具有一个接口(纯虚拟基类)和一个实现,并且用间接替代高效的模板生成代码。

如果你不关心你的系统是否由可replace的模块组成,那么你就不需要采取行动来使它成为模块化的,并且可以使用C ++的一些特性,例如类和模板,可以提高模块内部的凝聚力。 如果你的项目要生成一个单一的,静态链接的应用程序,那么你没有一个模块化的系统,你可以负担不起模块化。 如果你想创build类似反粒面几何的东西,这就是使用模板将不同algorithm和数据结构耦合在一起的一个很好的例子,那么你需要在C ++中做到这一点 – 没有其他广泛的强大。

所以要非常小心你的经理用'模块化'来表示。

如果每个文件已经有了“自己的目的和function”,并且“程序中的每一个函数都被传递了一个指向其中一个结构体的指针”,那么将它改为类的唯一区别就是将指针replace为结构体隐含this指针。 事实上(如果结构只是在C文件中而不是在头文件中定义的),那么这将不会影响系统的模块化,这将减less模块化。

使用“只有”7000行的C代码,从头开始重写代码可能会更容易,甚至无需了解当前的代码。

而且没有自动化的方法来做甚至可以协助您设想的模块化和重构。

7000 LOC可能听起来很多,但很多这将是样板。

尝试看看在将代码更改为c ++之前是否可以简化代码。 基本上,虽然我认为他只是希望你将函数转换为类方法,并将结构转换为类数据成员(如果它们不包含函数指针,那么将它们转换为实际方法)。 你可以联系这个程序的原始编码器吗? 他们可以帮助你完成一些理解,但主要是我会寻找那个是整个事物的“引擎”的代码,并从那里开发新的软件。 另外,我的老板告诉我,有时最好简单地重写整个事情,但现有的程序是模仿运行时间行为的一个很好的参考。 当然,专门的algorithm很难重新编码。 我可以向你保证的一件事是,如果这个代码不是最好的,那么你以后会遇到很多问题。 我会去你的老板,并促进事实,你需要重做程序的一部分从零开始。 我刚刚在那里,我的上司给了我重写的能力,我真的很高兴。 现在的2.0版本比原始版本早了几年。

我从http://www.javaworld.com/javaworld/jw-03-2001/jw-0323-badcode.html?page=7阅读了这篇题为“让糟糕的代码变得更好”的文章。; 它针对Java用户,但其所有的想法,我认为适合你的情况。 虽然标题使声音听起来像只是坏的代码,我认为这篇文章是一般的维护工程师。

总结法雷尔博士的想法,他说:

  1. 从容易的事情开始。
  2. 修复评论
  3. 修复格式
  4. 遵循项目惯例
  5. 编写自动化testing
  6. 分解大文件/function
  7. 重写你不明白的代码

我想在遵循其他人的build议之后,这可能是一个很好的文章,当你有空闲的时候阅读。

祝你好运!

Interesting Posts