什么内容需要放入主函数?

22

我正在寻找使用C++编写程序的main函数最佳实践。当前我认为有两种可行方式(尽管这些方法的“边界”可以彼此接近):

1:编写一个“主”类,该类接收传递给main函数的参数并在该“主”类中处理整个程序(当然,您也可以利用其他类)。因此,main函数将被缩减到最少的行数。

#include "MasterClass.h"
int main(int args, char* argv[])
{
MasterClass MC(args, argv);
}

2: 在主函数中编写“完整”的程序,当然要使用用户定义的对象!但是,还涉及全局函数,而且主函数可能会变得有些庞大。

我正在寻找一些关于如何编写C++程序的主函数的通用指导方针。我遇到了这个问题,因为我试图为第一种方法编写一些单元测试,但由于大多数方法都是私有的,所以有点困难。


通常情况下,使用argc代替args,因为argc表示参数的数量。 - Will
10个回答

24
为什么要有一个主类?它的责任范围是什么?
"Master"或"应用程序"类往往变成臃肿的大块,做了太多不同的事情。最终,这样做的目的是什么?它为你带来了什么好处?
为什么不使用主函数来提供这个主要功能?在main中定义高级应用程序生命周期。它接受一些命令行参数并解析它们(最好通过将其委派给另一个函数或类来完成),然后调用一些设置功能,然后可能进入某种主循环,最后进行一些清理工作。总而言之,这可能会给你一个主函数,大约有10-15行,但可能不会超过这个范围。它应该尽可能地委派给其他类和函数,因此 main 本身保持简短而简洁,但仍然有一个目的。
将这种超高级别的流程放在 main 中意味着易于查找,因为无论如何 main 都是您的起点。如果要理解代码,这也是您要开始查看的位置。因此,在其中放置读者想要了解代码的内容。
当然,你可以把所有这些东西都放在一个“主类”中,除了满足那些觉得“所有东西都必须在一个类中”的Java保守派之外,你不会获得任何好处。

1
如果只是为了参数解析,那就+1。如果想要编写“模块化”的代码,那么必须将参数解析(作为其中一个接口)与实际代码解耦,并使用适当的结构来收集所有这些“设置”。注意,参数解析也可以包括使用环境变量,因为大多数情况下它们用于设置默认值。然后就是简单的设置/循环直到完成/清理的高级视图。 - Matthieu M.
1
“最终,这有何意义?” 虽然此处未加演示,但它可以提供依赖注入。 例如,不是有代码写入 std::cout,而是 Master关联到一个 standardOutput 流,即 std::ostreammain() 函数创建一个带有 std::coutMaster 对象以进行该关联,但单元测试可以使用 std::ostringstream。 然后,可以轻松地对行为要求进行单元测试,例如:“如果命令行有误,则程序必须将错误消息写入其标准错误输出流”。 - Raedwald
1
但是你强调了一个有效的观点,那就是那个句子。“有什么意义”是一个实际的问题,而不仅仅是一个修辞上的“没有意义”。这是你应该问自己的问题,因为在某些情况下,创建一个主类可能是有意义的。我只是说,通常情况下,这只是Java思维的一种锻炼,会创建不必要的间接层。如果你无法清楚地回答“有什么意义”的问题,那么你就不应该创建一个“主”类。 - jalf
但我会认为,在许多情况下,main函数可以保持简单,因此几乎没有必要进行单元测试。如果无法保持简单,那么它本身就太复杂了。 (但是,我知道有些纯粹主义者会坚持100%的测试覆盖率,然后 main 函数可能不可能超过一行代码(严格来说,甚至那也会使您低于100%的覆盖率)。 - jalf
1
测试主函数是否进行“单元测试”有意义吗?如果有的话,我认为更应该将其视为功能测试或其他类型的测试。 - João Portela
显示剩余2条评论

7
你描述了两种“极端”的方法,我都觉得不太合适。在实现任何非平凡的程序时,既不能只有单个God Class,也不能只有单个God Function。
main()中只调用一次MasterClass可能是可以的(尽管我更喜欢更好地分区功能,例如在main()中执行任何特定于命令行的处理,以将MasterClass与命令行参数的详细信息解耦)。但是,如果该类难以进行单元测试,则表明存在设计问题,通常的解决方案是将其可测试的功能委托给其他类,在其中可以通过公共接口轻松地进行单元测试。
您的第二种方法可能会再次导致单元测试出现问题,因此您应该努力提取方法(然后最好将它们移动到单独的类中),以使细粒度的单元测试成为可能。
你希望处于的最佳状态是在两个极端之间,受制于使您的代码可测试的要求。值得思考的是如何一般性地构建程序,而不仅仅是在main()上下文中。基本思想是将其分割成“块”(类和方法),这些块应当足够小,易于理解、测试和维护,并且逻辑上具有内聚性。

2
我不同意。必须有一个入口点-无论是神函数还是神类,至少必须有一个。 - Puppy
3
当然可以。但是引用原帖:“为第一种方法编写一些单元测试[...]可能有点困难,因为大多数方法都是私有的。”根据我的理解,这意味着他的'Master'类是一个God Class,没有将任何内容委托给其他地方。 - Péter Török
1
哦,我明白你的意思了。是的,我完全同意。 - Puppy

3
通常我会在我的应用程序命名空间中调用一个具有相同签名的主要函数:
namespace current_application {
    int main( int argc, char **argv ) {
        // ...
    }
}
int main( int argc, char **argv ) {
    return current_application::main( argc, argv );
}

然后我通常使用我的实际主要函数(在命名空间中)来初始化应用程序相关的事项:
- 设置标准输入/输出/错误的语言环境 - 解析应用程序参数 - 实例化我的主类对象(如果存在),相当于类似于QApplication的东西 - 调用主函数(如果存在),相当于类似于QApplication::run的东西
通常我会添加一个try catch块,以便在崩溃时打印更多调试信息。
所有这些都高度主观,它是你编码风格的一部分。

我不明白这能给你带来什么,除了让维护程序员感到困惑。 :) - John Dibling
1
因为你试图在C++中做一些类似Java的事情。这种架构,其中你只是将main()委托给命名空间内的一个自由函数,没有任何好处,并且是出乎意料的。命名空间中的main对你有什么作用,非命名空间的main没有吗? - John Dibling
@John Dibling:我的应用程序主要功能在逻辑上属于我的应用程序命名空间,因此我认为将其放在那里更加清晰。此外,通过将main放在我的命名空间中,我可以使用命名空间中定义的符号而无需显式声明它们(或具有using指令)。最后,这增加了进一步的抽象级别:我有一个函数在我的主函数之外,它经常用于快速hack/测试,并且很容易让我将我的应用程序用作库:如果我需要在其他项目中使用大部分代码,只需获取整个内容并重写::main(这在Python中很流行)。 - peoro

3

我会分析主程序中过程的参数,并通过传递比argc和argv更易读的参数来创建一个类实例。


3

首先,我很少使用C++,但我认为这不是一个特定于语言的问题。
嗯,我想这除了一些实用性问题外,还取决于个人喜好。 个人倾向于使用布局#1,但不要将命令行解析例程放在MasterClass中。对我来说,这明显属于主函数。 MasterClass应该获得已解析的参数(整数、FileStream等)。


2

您的第一种方法非常常见,尽管类通常被命名为“Application”(或至少包含单词“Application”),因此请这样做。


1

如果异常没有处理程序,则未指定在std::terminate之前是否调用本地对象的析构函数。

如果您想使该行为可预测,则main是一个很好的地方,可以有一个最顶层的异常处理程序,并进行某种报告。

通常这就是我放在main中的所有内容,否则它只会调用cppMain... ;-)

干杯 & hth.,


1
另一个技巧是,而不是在全局范围内拥有对象,你可以在全局范围内拥有指针,然后在主函数内构造对象(从而控制构造顺序),并将指针指向这些局部对象,最后调用“真正”的主函数。因此实现了确定性的构造、析构和其顺序。 - Karl Knechtel

0

你提供的例子只是将主函数移动到命名空间中。我不认为这里有什么优势。对我来说,一种介于中庸和大师课程模型之间的方法是可行的。大师课程对象通常很大,最好在堆上创建。我让主函数创建该对象并处理可能发生的任何错误。

class MasterClass {
public:
static MasterClass* CreateInstance( int argc, char **argv );
    // ...
}

int main(int argc, char** argv)
{
    try
    {
         MasterClass mc = MC::CreateInstance(argc, argv);
    }
    catch(...)
    {
        // ...
    }
}

这也有一个优点,即任何与实际程序逻辑无关的处理(例如读取环境等)都不必放在MasterClass中。它们可以放在main()中。一个很好的例子是Lotus Domino系统的服务器插件任务。在这里,任务只应在Domino调度程序任务将控制权移交给它时运行。在这种情况下,main()可能如下所示:
STATUS LNPUBLIC AddInMain(HMODULE hModule, int argc, char far *argv[])
{
     MasterClass mc = MC::CreateInstance(argc, argv);
     while(/* wait for scheduler to give control */)
     {
          STATUS s = mc->RunForTimeSlice();
          if (s != SUCCESS)
               break;
     }
     // clean up
}

与调度程序交互的所有逻辑都在主函数中,因此程序的其余部分不必处理任何内容。


0
通常情况下,我会执行必要的平台特定设置操作,然后委托给一个对象。相比于函数,对象并没有太多优势,但是对象可以从接口继承,这对于使用接口实现回调的跨平台库非常有用。

0

我更喜欢使用IOC(控制反转)方法来编写我的程序。

因此,我的“主函数”使用命令参数来确定程序的“选项”和“配置”。所谓选项是指解释在命令行中传递的某些标志,而所谓配置则是加载配置文件。

用于加载配置文件的对象然后具有一个“运行”命令,但运行的内容(如果有)也取决于命令行参数。


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