12

我在假期周末学习了一点 C,我开始研究用 C 编写的其他程序。最后我研究了 GNU Netcat,认为它会是一个很好的例子。

看到600线的main()功能,我有点震惊。这是正常的吗?如果这是正常的,这是否被认为是良好的 C 编码实践?

4

12 回答 12

23

有一位美国总统(林肯?)被问到男人的腿应该有多长。“足够长,可以从他的身体伸到地面,”他说。

回到主题:

诸如“清洁代码”之类的书籍的作者宣传每个函数只做一件事(我在这里大大简化了),所以理论上你main()应该调用一个初始化函数,然后调用另一个函数来协调应用程序的工作,仅此而已。

在实践中,许多程序员发现很多微小的函数很烦人。一个可能更有用的指标是,一个功能通常应该适合一个屏幕,如果只是为了更容易看到和思考。

如果一个程序很复杂,并且它的大部分功能都在main(). 本质上,您应该努力实现可管理性、可理解性和可读性。通常没有充分的理由让 main() 变得很大。

于 2009-12-28T00:10:28.220 回答
7

我经常在某些类型的应用程序中发现 main() 有数百行初始化,然后是大约 20 行顶级循环。

我的习惯是在我需要调用它们两次之前不中断函数。这有时会导致我编写一个 300 行的函数,但是一旦我看到同一个块出现两次,我就打破了那个块。

至于main,初始化例程通常是一次,所以600行听起来不合理。

于 2009-12-28T00:32:52.470 回答
4

限制是编辑器窗口...

嘿,这太可怕了,但我见过更糟的。我见过大型的、数千行的 fortran 程序,根本没有子例程。

我相信答案是:它应该适合编辑器窗口,并且应该具有低圈复杂度

如果一个主程序只是一系列函数调用或计算,那么我想它可以是必要的,并且它可以免除编辑器窗口的约束。即使那样,我也会有点惊讶,没有一种自然的方法可以提取有意义的离散方法。

但是,如果它是测试和分支以及returning 和breaking 和continue-ing,那么它需要被分解成单独的和单独测试的功能组件。

于 2009-12-28T06:18:41.187 回答
4

无论使用哪种语言,我都会尝试将子例程方法限制为大致在一页代码中可见的内容,并尽可能将功能提取到子例程中。

对于任何实现来说,600 行听起来都相当长。也许有一些压倒一切的原因。传递论点和清晰性(我没有看过你发布的例子),但它听起来是在通常实践的远端,应该可以细分这个任务。

我怀疑它是通过多年来不断增加的功能而开发的,并且没有人停止并对其进行重构以使其更具可读性/可维护性。如果没有针对此的单元测试(并且根据我的经验,main()方法通常不会得到书面测试——无论出于何种原因),那么人们不愿意重构它是可以理解的。

于 2009-12-28T00:09:31.933 回答
3

希望他们正在计划重构。这看起来非常粗糙。

  443   while (optind < argc) {
  444     const char *get_argv = argv[optind++];
  445     char *q, *parse = strdup(get_argv);
  446     int port_lo = 0, port_hi = 65535;
  447     nc_port_t port_tmp;
  448 
  449     if (!(q = strchr(parse, '-')))    /* simple number? */
  450       q = strchr(parse, ':');     /* try with the other separator */
  451 
  452     if (!q) {
  453       if (netcat_getport(&port_tmp, parse, 0))
  454   netcat_ports_insert(old_flag, port_tmp.num, port_tmp.num);
  455       else
  456   goto got_err;
  457     }
  458     else {        /* could be in the forms: N1-N2, -N2, N1- */
  459       *q++ = 0;
  460       if (*parse) {
  461   if (netcat_getport(&port_tmp, parse, 0))
  462     port_lo = port_tmp.num;
  463   else
  464     goto got_err;
  465       }
  466       if (*q) {
  467   if (netcat_getport(&port_tmp, q, 0))
  468     port_hi = port_tmp.num;
  469   else
  470     goto got_err;
  471       }
  472       if (!*parse && !*q)     /* don't accept the form '-' */
  473   goto got_err;
  474 
  475       netcat_ports_insert(old_flag, port_lo, port_hi);
  476     }
  477 
  478     free(parse);
  479     continue;
  480 
  481  got_err:
  482     free(parse);
  483     ncprint(NCPRINT_ERROR, _("Invalid port specification: %s"), get_argv);
  484     exit(EXIT_FAILURE);
  485   }
于 2009-12-28T00:24:32.403 回答
3

600 线主干线是一个警告信号。但是如果你看它,除了这样做之外,看不到任何将它分解成更小部分的方法。

void the_first_part_of_main(args...);
void the_second_part_of_main(args...);
...

main()
{
   the_first_part_of_main();
   the_second_part_of_main();
   ...
}

那么你应该不管它。

于 2009-12-28T00:45:09.880 回答
2

按照某些标准,任何类型的 600 行函数都是一个坏主意,但没有理由将 main 与任何其他函数区别对待。

我能想到出现这种情况的唯一原因是程序开发得很快,随着它的发展,没有人会费心将它分成更多的逻辑单元。

于 2009-12-28T00:11:42.197 回答
1

我想说你的例程应该尽可能长/短,以便有效、可靠和自动测试。一个 600 条语句的例程可能有多个路径通过它,并且例程的组合可能会很快变得非常大。我尝试将功能分解为易于阅读的功能。功能要么是“功能性的”,要么是“叙述性的”。一直包括单元测试。

于 2009-12-28T00:47:21.510 回答
1

我个人的编码风格是尝试仅使用 main 函数进行命令行参数解析,以及程序需要的任何大额初始化。

于 2009-12-28T01:05:53.287 回答
1

我见过的几乎所有的 600 行函数也都是愚蠢的。不必如此。

然而,在这些情况下,程序员未能提供一些缩小的视图,并为部分赋予有意义的名称 - 高级(如 Initialize())和低级(需要常见的 3 -line 模式并将其隐藏在一个名称下,并带有参数)。

在极端愚蠢的情况下,他们会在不需要时优化函数调用性能。

于 2009-12-28T12:34:03.167 回答
1

尽可能短。通常,每当有一个操作我可以指定一个名称时,我都会为它创建一个新方法。

于 2009-12-28T00:12:18.170 回答
0

main(),就像任何函数一样,应该与它需要的一样大。“正如它所需要的那样”会根据它需要做什么而有很大的不同。话虽如此,在大多数情况下,它不应该超过几百行。600 行有点大,其中一些可以/应该重构为单独的函数。

举一个极端的例子,我所在的一个团队的任务是加速一些代码以驱动 3D 显示。代码最初是由一个线头编写的,他显然是用老式的 FORTRAN 自学编程的;main()超过五千行代码,#include到处都是随机位。他没有将代码分解成函数,而是简单地分支到main()via内的子例程goto(在 13 到 15 个 goto 之间的某个地方,看似随机地分支两个方向)。作为第一步,我们简单地打开了 1 级优化;编译器迅速吞噬了所有可用的内存和交换空间并使内核恐慌。代码太脆弱以至于我们无法编写任何代码改变而不破坏某些东西。我们最终告诉客户他们有两个选择:让我们从头开始重写整个系统或购买更快的硬件。

他们购买了更快的硬件。

于 2009-12-28T14:22:12.207 回答