如何写好 C main 函数 | Linux 中国

论坛 期权论坛 期权     
Linux中国   2019-6-30 08:18   2243   0
学习如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。-- Erik O'shaughnessy
我知道,现在孩子们用 Python 和 JavaScript 编写他们的疯狂“应用程序”。但是不要这么快就否定 C 语言 —— 它能够提供很多东西,并且简洁。如果你需要速度,用 C 语言编写可能就是你的答案。如果你正在寻找稳定的职业或者想学习如何捕获[url=]空指针解引用[/url],C 语言也可能是你的答案!在本文中,我将解释如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。
我:一个顽固的 Unix 系统程序员。
你:一个有编辑器、C 编译器,并有时间打发的人。
让我们开工吧。
一个无聊但正确的 C 程序


Parody O'Reilly book cover, "Hating Other People's Code"
C 程序以
  1. main()
复制代码
函数开头,通常保存在名为
  1. main.c
复制代码
的文件中。
    1. /* main.c */
    复制代码
    1. int main(int argc, char *argv[]) {
    复制代码
    1. [/code][*][code]}
    复制代码
这个程序可以编译但不干任何事。
    1. $ gcc main.c
    复制代码
    1. $ ./a.out -o foo -vv
    复制代码
    1. $
    复制代码
正确但无聊。
main 函数是唯一的。
  1. main()
复制代码
函数是开始执行时所执行的程序的第一个函数,但不是第一个执行的函数。第一个函数是
  1. _start()
复制代码
,它通常由 C 运行库提供,在编译程序时自动链入。此细节高度依赖于操作系统和编译器工具链,所以我假装没有提到它。
  1. main()
复制代码
函数有两个参数,通常称为
  1. argc
复制代码
  1. argv
复制代码
,并返回一个有符号整数。大多数 Unix 环境都希望程序在成功时返回
  1. 0
复制代码
(零),失败时返回
  1. -1
复制代码
(负一)。
< 如显示不全,请左右滑动 >参数名称描述
  1. argc
复制代码
参数个数参数向量的个数
  1. argv
复制代码
参数向量字符指针数组参数向量
  1. argv
复制代码
是调用你的程序的命令行的标记化表示形式。在上面的例子中,
  1. argv
复制代码
将是以下字符串的列表:
    1. argv = [ "/path/to/a.out", "-o", "foo", "-vv" ];
    复制代码
参数向量在其第一个索引
  1. argv[0]
复制代码
中确保至少会有一个字符串,这是执行程序的完整路径。
main.c 文件的剖析
当我从头开始编写
  1. main.c
复制代码
时,它的结构通常如下:
    1. /* main.c */
    复制代码
    1. /* 0 版权/许可证 */
    复制代码
    1. /* 1 包含 */
    复制代码
    1. /* 2 定义 */
    复制代码
    1. /* 3 外部声明 */
    复制代码
    1. /* 4 类型定义 */
    复制代码
    1. /* 5 全局变量声明 */
    复制代码
    1. /* 6 函数原型 */
    复制代码
    1. [/code][*][code]int main(int argc, char *argv[]) {
    复制代码
    1. /* 7 命令行解析 */
    复制代码
    1. }
    复制代码
    1. [/code][*][code]/* 8 函数声明 */
    复制代码
下面我将讨论这些编号的各个部分,除了编号为 0 的那部分。如果你必须把版权或许可文本放在源代码中,那就放在那里。
另一件我不想讨论的事情是注释。
    1. “评论谎言。”
    复制代码
    1. - 一个愤世嫉俗但聪明又好看的程序员。
    复制代码
与其使用注释,不如使用有意义的函数名和变量名。
鉴于程序员固有的惰性,一旦添加了注释,维护负担就会增加一倍。如果更改或重构代码,则需要更新或扩充注释。随着时间的推移,代码会变得面目全非,与注释所描述的内容完全不同。
如果你必须写注释,不要写关于代码正在做什么,相反,写下代码为什么要这样写。写一些你将要在五年后读到的注释,那时你已经将这段代码忘得一干二净。世界的命运取决于你。不要有压力。
1、包含
我添加到
  1. main.c
复制代码
文件的第一个东西是包含文件,它们为程序提供大量标准 C 标准库函数和变量。C 标准库做了很多事情。浏览
  1. /usr/include
复制代码
中的头文件,你可以了解到它们可以做些什么。
  1. #include
复制代码
字符串是 [url=]C 预处理程序[/url](cpp)指令,它会将引用的文件完整地包含在当前文件中。C 中的头文件通常以
  1. .h
复制代码
扩展名命名,且不应包含任何可执行代码。它只有宏、定义、类型定义、外部变量和函数原型。字符串
  1. [/code] 告诉 cpp 在系统定义的头文件路径中查找名为 [code]header.h
复制代码
的文件,它通常在
  1. /usr/include
复制代码
目录中。
    1. /* main.c */
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
这是我默认会全局包含的最小包含集合,它将引入:
< 如显示不全,请左右滑动 >#include 文件提供的东西stdio提供
  1. FILE
复制代码
  1. stdin
复制代码
  1. stdout
复制代码
  1. stderr
复制代码
  1. fprint()
复制代码
函数系列stdlib提供
  1. malloc()
复制代码
  1. calloc()
复制代码
  1. realloc()
复制代码
unistd提供
  1. EXIT_FAILURE
复制代码
  1. EXIT_SUCCESS
复制代码
libgen提供
  1. basename()
复制代码
函数errno定义外部
  1. errno
复制代码
变量及其可以接受的所有值string提供
  1. memcpy()
复制代码
  1. memset()
复制代码
  1. strlen()
复制代码
函数系列getopt提供外部
  1. optarg
复制代码
  1. opterr
复制代码
  1. optind
复制代码
  1. getopt()
复制代码
函数sys/types类型定义快捷方式,如
  1. uint32_t
复制代码
  1. uint64_t
复制代码
2、定义
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. #define OPTSTR "vi:o:f:h"
    复制代码
    1. #define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
    复制代码
    1. #define ERR_FOPEN_INPUT  "fopen(input, r)"
    复制代码
    1. #define ERR_FOPEN_OUTPUT "fopen(output, w)"
    复制代码
    1. #define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
    复制代码
    1. #define DEFAULT_PROGNAME "george"
    复制代码
这在现在没有多大意义,但
  1. OPTSTR
复制代码
定义我这里会说明一下,它是程序推荐的命令行开关。参考 [url=]getopt(3)[/url] man 页面,了解
  1. OPTSTR
复制代码
将如何影响
  1. getopt()
复制代码
的行为。
  1. USAGE_FMT
复制代码
定义了一个
  1. printf()
复制代码
风格的格式字符串,它用在
  1. usage()
复制代码
函数中。
我还喜欢将字符串常量放在文件的
  1. #define
复制代码
这一部分。如果需要,把它们收集在一起可以更容易地修正拼写、重用消息和国际化消息。
最后,在命名
  1. #define
复制代码
时全部使用大写字母,以区别变量和函数名。如果需要,可以将单词放连在一起或使用下划线分隔,只要确保它们都是大写的就行。
3、外部声明
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. extern int errno;
    复制代码
    1. extern char *optarg;
    复制代码
    1. extern int opterr, optind;
    复制代码
  1. extern
复制代码
声明将该名称带入当前编译单元的命名空间(即 “文件”),并允许程序访问该变量。这里我们引入了三个整数变量和一个字符指针的定义。
  1. opt
复制代码
前缀的几个变量是由
  1. getopt()
复制代码
函数使用的,C 标准库使用
  1. errno
复制代码
作为带外通信通道来传达函数可能的失败原因。
4、类型定义
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. typedef struct {
    复制代码
    1.   int           verbose;
    复制代码
    1.   uint32_t      flags;
    复制代码
    1.   FILE         *input;
    复制代码
    1.   FILE         *output;
    复制代码
    1. } options_t;
    复制代码
在外部声明之后,我喜欢为结构、联合和枚举声明
  1. typedef
复制代码
。命名一个
  1. typedef
复制代码
是一种传统习惯。我非常喜欢使用
  1. _t
复制代码
后缀来表示该名称是一种类型。在这个例子中,我将
  1. options_t
复制代码
声明为一个包含 4 个成员的
  1. struct
复制代码
。C 是一种空格无关的编程语言,因此我使用空格将字段名排列在同一列中。我只是喜欢它看起来的样子。对于指针声明,我在名称前面加上星号,以明确它是一个指针。
5、全局变量声明
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. int dumb_global_variable = -11;
    复制代码
全局变量是一个坏主意,你永远不应该使用它们。但如果你必须使用全局变量,请在这里声明,并确保给它们一个默认值。说真的,不要使用全局变量。
6、函数原型
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. void usage(char *progname, int opt);
    复制代码
    1. int do_the_needful(options_t *options);
    复制代码
在编写函数时,将它们添加到
  1. main()
复制代码
函数之后而不是之前,在这里放函数原型。早期的 C 编译器使用单遍策略,这意味着你在程序中使用的每个符号(变量或函数名称)必须在使用之前声明。现代编译器几乎都是多遍编译器,它们在生成代码之前构建一个完整的符号表,因此并不严格要求使用函数原型。但是,有时你无法选择代码要使用的编译器,所以请编写函数原型并继续这样做下去。
当然,我总是包含一个
  1. usage()
复制代码
函数,当
  1. main()
复制代码
函数不理解你从命令行传入的内容时,它会调用这个函数。
7、命令行解析
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. int main(int argc, char *argv[]) {
    复制代码
    1.     int opt;
    复制代码
    1.     options_t options = { 0, 0x0, stdin, stdout };
    复制代码
    1. [/code][*][code]    opterr = 0;
    复制代码
    1. [/code][*][code]    while ((opt = getopt(argc, argv, OPTSTR)) != EOF)
    复制代码
    1.        switch(opt) {
    复制代码
    1.            case 'i':
    复制代码
    1.               if (!(options.input = fopen(optarg, "r")) ){
    复制代码
    1.                  perror(ERR_FOPEN_INPUT);
    复制代码
    1.                  exit(EXIT_FAILURE);
    复制代码
    1.                  /* NOTREACHED */
    复制代码
    1.               }
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'o':
    复制代码
    1.               if (!(options.output = fopen(optarg, "w")) ){
    复制代码
    1.                  perror(ERR_FOPEN_OUTPUT);
    复制代码
    1.                  exit(EXIT_FAILURE);
    复制代码
    1.                  /* NOTREACHED */
    复制代码
    1.               }   
    复制代码
    1.               break;
    复制代码
    1.               
    复制代码
    1.            case 'f':
    复制代码
    1.               options.flags = (uint32_t )strtoul(optarg, NULL, 16);
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'v':
    复制代码
    1.               options.verbose += 1;
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'h':
    复制代码
    1.            default:
    复制代码
    1.               usage(basename(argv[0]), opt);
    复制代码
    1.               /* NOTREACHED */
    复制代码
    1.               break;
    复制代码
    1.        }
    复制代码
    1. [/code][*][code]    if (do_the_needful(&options) != EXIT_SUCCESS) {
    复制代码
    1.        perror(ERR_DO_THE_NEEDFUL);
    复制代码
    1.        exit(EXIT_FAILURE);
    复制代码
    1.        /* NOTREACHED */
    复制代码
    1.     }
    复制代码
    1. [/code][*][code]    return EXIT_SUCCESS;
    复制代码
    1. }
    复制代码
好吧,代码有点多。这个
  1. main()
复制代码
函数的目的是收集用户提供的参数,执行最基本的输入验证,然后将收集到的参数传递给使用它们的函数。这个示例声明一个使用默认值初始化的
  1. options
复制代码
变量,并解析命令行,根据需要更新
  1. options
复制代码
  1. main()
复制代码
函数的核心是一个
  1. while
复制代码
循环,它使用
  1. getopt()
复制代码
来遍历
  1. argv
复制代码
,寻找命令行选项及其参数(如果有的话)。文件前面定义的
  1. OPTSTR
复制代码
是驱动
  1. getopt()
复制代码
行为的模板。
  1. opt
复制代码
变量接受
  1. getopt()
复制代码
找到的任何命令行选项的字符值,程序对检测命令行选项的响应发生在
  1. switch
复制代码
语句中。
如果你注意到了可能会问,为什么
  1. opt
复制代码
被声明为 32 位
  1. int
复制代码
,但是预期是 8 位
  1. char
复制代码
?事实上
  1. getopt()
复制代码
返回一个
  1. int
复制代码
,当它到达
  1. argv
复制代码
末尾时取负值,我会使用
  1. EOF
复制代码
(文件末尾标记)匹配。
  1. char
复制代码
是有符号的,但我喜欢将变量匹配到它们的函数返回值。
当检测到一个已知的命令行选项时,会发生特定的行为。在
  1. OPTSTR
复制代码
中指定一个以冒号结尾的参数,这些选项可以有一个参数。当一个选项有一个参数时,
  1. argv
复制代码
中的下一个字符串可以通过外部定义的变量
  1. optarg
复制代码
提供给程序。我使用
  1. optarg
复制代码
来打开文件进行读写,或者将命令行参数从字符串转换为整数值。
这里有几个关于代码风格的要点:
  1. opterr
复制代码
初始化为
  1. 0
复制代码
,禁止
  1. getopt
复制代码
触发
  1. ?
复制代码
。 在
  1. main()
复制代码
的中间使用
  1. exit(EXIT_FAILURE);
复制代码
  1. exit(EXIT_SUCCESS);
复制代码
  1. /* NOTREACHED */
复制代码
是我喜欢的一个 lint 指令。 在返回 int 类型的函数末尾使用
  1. return EXIT_SUCCESS;
复制代码
。 显示强制转换隐式类型。这个程序的命令行格式,经过编译如下所示:
    1. $ ./a.out -h
    复制代码
    1. a.out [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]
    复制代码
事实上,在编译后
  1. usage()
复制代码
就会向
  1. stderr
复制代码
发出这样的内容。
8、函数声明
    1. /* main.c */
    复制代码
    1. [/code][*][code]
    复制代码
    1. void usage(char *progname, int opt) {
    复制代码
    1.    fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
    复制代码
    1.    exit(EXIT_FAILURE);
    复制代码
    1.    /* NOTREACHED */
    复制代码
    1. }
    复制代码
    1. [/code][*][code]int do_the_needful(options_t *options) {
    复制代码
    1. [/code][*][code]   if (!options) {
    复制代码
    1.      errno = EINVAL;
    复制代码
    1.      return EXIT_FAILURE;
    复制代码
    1.    }
    复制代码
    1. [/code][*][code]   if (!options->input || !options->output) {
    复制代码
    1.      errno = ENOENT;
    复制代码
    1.      return EXIT_FAILURE;
    复制代码
    1.    }
    复制代码
    1. [/code][*][code]   /* XXX do needful stuff */
    复制代码
    1. [/code][*][code]   return EXIT_SUCCESS;
    复制代码
    1. }
    复制代码
我最后编写的函数不是个样板函数。在本例中,函数
  1. do_the_needful()
复制代码
接受一个指向
  1. options_t
复制代码
结构的指针。我验证
  1. options
复制代码
指针不为
  1. NULL
复制代码
,然后继续验证
  1. input
复制代码
  1. output
复制代码
结构成员。如果其中一个测试失败,返回
  1. EXIT_FAILURE
复制代码
,并且通过将外部全局变量
  1. errno
复制代码
设置为常规错误代码,我可以告知调用者常规的错误原因。调用者可以使用便捷函数
  1. perror()
复制代码
来根据
  1. errno
复制代码
的值发出便于阅读的错误消息。
函数几乎总是以某种方式验证它们的输入。如果完全验证代价很大,那么尝试执行一次并将验证后的数据视为不可变。
  1. usage()
复制代码
函数使用
  1. fprintf()
复制代码
调用中的条件赋值验证
  1. progname
复制代码
参数。接下来
  1. usage()
复制代码
函数就退出了,所以我不会费心设置
  1. errno
复制代码
,也不用操心是否使用正确的程序名。
在这里,我要避免的最大错误是解引用
  1. NULL
复制代码
指针。这将导致操作系统向我的进程发送一个名为
  1. SYSSEGV
复制代码
的特殊信号,导致不可避免的死亡。用户最不希望看到的是由
  1. SYSSEGV
复制代码
而导致的崩溃。最好是捕获
  1. NULL
复制代码
指针以发出更合适的错误消息并优雅地关闭程序。
有些人抱怨在函数体中有多个
  1. return
复制代码
语句,他们喋喋不休地说些“控制流的连续性”之类的东西。老实说,如果函数中间出现错误,那就应该返回这个错误条件。写一大堆嵌套的
  1. if
复制代码
语句只有一个
  1. return
复制代码
绝不是一个“好主意”。
最后,如果你编写的函数接受四个以上的参数,请考虑将它们绑定到一个结构中,并传递一个指向该结构的指针。这使得函数签名更简单,更容易记住,并且在以后调用时不会出错。它还可以使调用函数速度稍微快一些,因为需要复制到函数堆栈中的东西更少。在实践中,只有在函数被调用数百万或数十亿次时,才会考虑这个问题。如果认为这没有意义,那也无所谓。
等等,你不是说没有注释吗!?!!
  1. do_the_needful()
复制代码
函数中,我写了一种特殊类型的注释,它被是作为占位符设计的,而不是为了说明代码:
    1. /* XXX do needful stuff */
    复制代码
当你写到这里时,有时你不想停下来编写一些特别复杂的代码,你会之后再写,而不是现在。那就是我留给自己再次回来的地方。我插入一个带有
  1. XXX
复制代码
前缀的注释和一个描述需要做什么的简短注释。之后,当我有更多时间的时候,我会在源代码中寻找
  1. XXX
复制代码
。使用什么前缀并不重要,只要确保它不太可能在另一个上下文环境(如函数名或变量)中出现在你代码库里。
把它们组合在一起
好吧,当你编译这个程序后,它仍然几乎没有任何作用。但是现在你有了一个坚实的骨架来构建你自己的命令行解析 C 程序。
    1. /* main.c - the complete listing */
    复制代码
    1. [/code][*][code]#include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. #include
    复制代码
    1. [/code][*][code]#define OPTSTR "vi:o:f:h"
    复制代码
    1. #define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
    复制代码
    1. #define ERR_FOPEN_INPUT  "fopen(input, r)"
    复制代码
    1. #define ERR_FOPEN_OUTPUT "fopen(output, w)"
    复制代码
    1. #define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
    复制代码
    1. #define DEFAULT_PROGNAME "george"
    复制代码
    1. [/code][*][code]extern int errno;
    复制代码
    1. extern char *optarg;
    复制代码
    1. extern int opterr, optind;
    复制代码
    1. [/code][*][code]typedef struct {
    复制代码
    1.   int           verbose;
    复制代码
    1.   uint32_t      flags;
    复制代码
    1.   FILE         *input;
    复制代码
    1.   FILE         *output;
    复制代码
    1. } options_t;
    复制代码
    1. [/code][*][code]int dumb_global_variable = -11;
    复制代码
    1. [/code][*][code]void usage(char *progname, int opt);
    复制代码
    1. int  do_the_needful(options_t *options);
    复制代码
    1. [/code][*][code]int main(int argc, char *argv[]) {
    复制代码
    1.     int opt;
    复制代码
    1.     options_t options = { 0, 0x0, stdin, stdout };
    复制代码
    1. [/code][*][code]    opterr = 0;
    复制代码
    1. [/code][*][code]    while ((opt = getopt(argc, argv, OPTSTR)) != EOF)
    复制代码
    1.        switch(opt) {
    复制代码
    1.            case 'i':
    复制代码
    1.               if (!(options.input = fopen(optarg, "r")) ){
    复制代码
    1.                  perror(ERR_FOPEN_INPUT);
    复制代码
    1.                  exit(EXIT_FAILURE);
    复制代码
    1.                  /* NOTREACHED */
    复制代码
    1.               }
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'o':
    复制代码
    1.               if (!(options.output = fopen(optarg, "w")) ){
    复制代码
    1.                  perror(ERR_FOPEN_OUTPUT);
    复制代码
    1.                  exit(EXIT_FAILURE);
    复制代码
    1.                  /* NOTREACHED */
    复制代码
    1.               }   
    复制代码
    1.               break;
    复制代码
    1.               
    复制代码
    1.            case 'f':
    复制代码
    1.               options.flags = (uint32_t )strtoul(optarg, NULL, 16);
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'v':
    复制代码
    1.               options.verbose += 1;
    复制代码
    1.               break;
    复制代码
    1. [/code][*][code]           case 'h':
    复制代码
    1.            default:
    复制代码
    1.               usage(basename(argv[0]), opt);
    复制代码
    1.               /* NOTREACHED */
    复制代码
    1.               break;
    复制代码
    1.        }
    复制代码
    1. [/code][*][code]    if (do_the_needful(&options) != EXIT_SUCCESS) {
    复制代码
    1.        perror(ERR_DO_THE_NEEDFUL);
    复制代码
    1.        exit(EXIT_FAILURE);
    复制代码
    1.        /* NOTREACHED */
    复制代码
    1.     }
    复制代码
    1. [/code][*][code]    return EXIT_SUCCESS;
    复制代码
    1. }
    复制代码
    1. [/code][*][code]void usage(char *progname, int opt) {
    复制代码
    1.    fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
    复制代码
    1.    exit(EXIT_FAILURE);
    复制代码
    1.    /* NOTREACHED */
    复制代码
    1. }
    复制代码
    1. [/code][*][code]int do_the_needful(options_t *options) {
    复制代码
    1. [/code][*][code]   if (!options) {
    复制代码
    1.      errno = EINVAL;
    复制代码
    1.      return EXIT_FAILURE;
    复制代码
    1.    }
    复制代码
    1. [/code][*][code]   if (!options->input || !options->output) {
    复制代码
    1.      errno = ENOENT;
    复制代码
    1.      return EXIT_FAILURE;
    复制代码
    1.    }
    复制代码
    1. [/code][*][code]   /* XXX do needful stuff */
    复制代码
    1. [/code][*][code]   return EXIT_SUCCESS;
    复制代码
    1. }
    复制代码
现在,你已经准备好编写更易于维护的 C 语言。如果你有任何问题或反馈,请在评论中分享。
via: [url=]https://opensource.com/article/19/5/how-write-good-c-main-function[/url]
作者:[url=]Erik O'Shaughnessy[/url] 选题:[url=]lujun9972[/url] 译者:[url=]MjSeven[/url] 校对:[url=]wxy[/url]
本文由 [url=]LCTT[/url] 原创编译,[url=]Linux中国[/url] 荣誉推出
[url=][/url]
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:126
帖子:8
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP