如何编写优秀的 C main 函数

学习如何构建 C 文件并编写 C main 函数,像冠军一样处理命令行参数。
285 位读者喜欢这个。
Hand drawing out the word "code"

我知道,Python 和 JavaScript 是现在孩子们用来编写他们疯狂的“应用程序”的语言。 但是,不要急于否定 C,它是一种功能强大且简洁的语言,有很多优点。 如果你需要速度,用 C 编写可能是你的答案。 如果你正在寻找工作保障和学习如何追捕 空指针解引用 的机会,C 也可能是你的答案! 在本文中,我将解释如何构建 C 文件并编写 C main 函数,像冠军一样处理命令行参数。

:一个经验丰富的 Unix 系统程序员。

:拥有编辑器、C 编译器和一些空闲时间的人。

开始吧。

一个枯燥但正确的 C 程序

Parody O'Reilly book cover, "Hating Other People's Code"

C 程序以 main() 函数开始,通常保存在名为 main.c 的文件中。

/* main.c */
int main(int argc, char *argv[]) {

}

这个程序可以编译,但不会任何事情。

$ gcc main.c
$ ./a.out -o foo -vv 
$

正确且枯燥。

Main 函数是独特的

main() 函数是你的程序开始执行时执行的第一个函数,但它不是第一个被执行的函数。 第一个函数是 _start(),它通常由 C 运行时库提供,在你的程序编译时自动链接进来。 细节高度依赖于操作系统和编译器工具链,所以我假装我没提过它。

main() 函数有两个参数,传统上称为 argcargv,并返回一个有符号整数。 大多数 Unix 环境期望程序在成功时返回 0(零),在失败时返回 -1(负一)。

参数 名称 描述
argc 参数计数 参数向量的长度
argv 参数向量 字符指针数组

参数向量 argv 是调用你的程序的命令行的标记化表示。 在上面的示例中,argv 将是以下字符串的列表

argv = [ "/path/to/a.out", "-o", "foo", "-vv" ];

参数向量保证始终在第一个索引 argv[0] 中至少有一个字符串,它是执行程序的完整路径。

main.c 文件的结构

当我从头开始编写 main.c 时,它通常结构如下

/* main.c */
/* 0 copyright/licensing */
/* 1 includes */
/* 2 defines */
/* 3 external declarations */
/* 4 typedefs */
/* 5 global variable declarations */
/* 6 function prototypes */

int main(int argc, char *argv[]) {
/* 7 command-line parsing */
}

/* 8 function declarations */

我将在下面讨论这些编号的部分,除了零。 如果你必须在源代码中放置版权或许可文本,请将其放在那里。

另一件我不会谈论添加到你的程序中的是注释。

"Comments lie."
- A cynical but smart and good looking programmer.

与其使用注释,不如使用有意义的函数和变量名。

利用程序员固有的惰性,一旦你添加了注释,你就使你的维护负担增加了一倍。 如果你更改或重构代码,则需要更新或扩展注释。 随着时间的推移,代码会逐渐演变成与注释描述的任何内容都不相似的东西。

如果你必须编写注释,请不要写关于代码正在做什么。 相反,写关于代码为什么要做它正在做的事情。 编写你希望在五年后阅读的注释,那时你已经忘记了关于这段代码的一切。 而世界的命运取决于你。 没有压力

1. 包含

我添加到 main.c 文件的第一件事是包含,以使我的程序可以使用大量的标准 C 库函数和变量。 标准 C 库做了很多事情; 探索 /usr/include 中的头文件,以了解它可以为你做什么。

#include 字符串是一个 C 预处理器 (cpp) 指令,它导致将引用的文件完整地包含在当前文件中。 C 中的头文件通常以 .h 扩展名命名,并且不应包含任何可执行代码; 仅包含宏、定义、typedef 和外部变量和函数原型。 字符串 <header.h> 告诉 cpp 在系统定义的头文件路径(通常是 /usr/include)中查找名为 header.h 的文件。

/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libgen.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>
#include <sys/types.h>

这是我默认包含的用于以下内容的最小全局包含集

#include 文件 它提供的功能
stdio 提供 FILE、stdin、stdout、stderr 和 fprint() 函数系列
stdlib 提供 malloc()、calloc() 和 realloc()
unistd 提供 EXIT_FAILURE、EXIT_SUCCESS
libgen 提供 basename() 函数
errno 定义外部 errno 变量及其可以采用的所有值
string 提供 memcpy()、memset() 和 strlen() 函数系列
getopt 提供外部 optarg、opterr、optind 和 getopt() 函数
sys/types Typedef 快捷方式,如 uint32_t 和 uint64_t

2. 定义

/* main.c */
<...>

#define OPTSTR "vi:o:f:h"
#define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
#define ERR_FOPEN_INPUT  "fopen(input, r)"
#define ERR_FOPEN_OUTPUT "fopen(output, w)"
#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
#define DEFAULT_PROGNAME "george"

现在这可能没有太多意义,但是 OPTSTR 定义是我将声明程序将推荐哪些命令行开关的地方。 请查阅 getopt(3) 手册页,了解 OPTSTR 将如何影响 getopt() 的行为。

USAGE_FMT 定义是一个 printf() 风格的格式字符串,在 usage() 函数中被引用。

我也喜欢将字符串常量作为 #defines 收集在文件的这一部分中。 收集它们可以更轻松地修复拼写、重用消息以及在需要时国际化消息。

最后,在命名 #define 时使用所有大写字母,以将其与变量和函数名称区分开来。 你可以根据需要将单词连在一起,或者用下划线分隔单词; 只要确保它们都是大写。

3. 外部声明

/* main.c */
<...>

extern int errno;
extern char *optarg;
extern int opterr, optind;

extern 声明将该名称带入当前编译单元(又名“文件”)的命名空间,并允许程序访问该变量。 在这里,我们引入了三个整数变量和一个字符指针的定义。 以 opt 开头的变量由 getopt() 函数使用,而 errno 被标准 C 库用作带外通信通道,以传达函数可能失败的原因。

4. Typedefs

/* main.c */
<...>

typedef struct {
  int           verbose;
  uint32_t      flags;
  FILE         *input;
  FILE         *output;
} options_t;

在外部声明之后,我喜欢为结构、联合和枚举声明 typedefs。 命名 typedef 本身就是一种宗教; 我强烈偏爱 _t 后缀来指示该名称是一种类型。 在此示例中,我已将 options_t 声明为一个包含四个成员的 struct。 C 是一种空白中立的编程语言,因此我使用空白将字段名称在同一列中对齐。 我只是喜欢它的外观。 对于指针声明,我在名称前面加上星号,以明确表明它是一个指针。

5. 全局变量声明

/* main.c */
<...>

int dumb_global_variable = -11;

全局变量是一个坏主意,你永远不应该使用它们。 但是,如果你必须使用全局变量,请在此处声明它们,并确保为它们提供默认值。 认真地说,不要使用全局变量

6. 函数原型

/* main.c */
<...>

void usage(char *progname, int opt);
int  do_the_needful(options_t *options);

当你编写函数时,在 main() 函数之后而不是之前添加它们,请在此处包含函数原型。 早期的 C 编译器使用单遍策略,这意味着你在程序中使用的每个符号(变量或函数名称)都必须在使用之前声明。 现代编译器几乎都是多遍编译器,它们在生成代码之前构建完整的符号表,因此严格来说不需要使用函数原型。 但是,有时你无法选择在你的代码上使用哪个编译器,因此请编写函数原型并继续前进。

通常,我总是包含一个 usage() 函数,当 main() 不理解你从命令行传入的内容时,它会调用该函数。

7. 命令行解析

/* main.c */
<...>

int main(int argc, char *argv[]) {
    int opt;
    options_t options = { 0, 0x0, stdin, stdout };

    opterr = 0;

    while ((opt = getopt(argc, argv, OPTSTR)) != EOF) 
       switch(opt) {
           case 'i':
              if (!(options.input = fopen(optarg, "r")) ){
                 perror(ERR_FOPEN_INPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }
              break;

           case 'o':
              if (!(options.output = fopen(optarg, "w")) ){
                 perror(ERR_FOPEN_OUTPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }    
              break;
              
           case 'f':
              options.flags = (uint32_t )strtoul(optarg, NULL, 16);
              break;

           case 'v':
              options.verbose += 1;
              break;

           case 'h':
           default:
              usage(basename(argv[0]), opt);
              /* NOTREACHED */
              break;
       }

    if (do_the_needful(&options) != EXIT_SUCCESS) {
       perror(ERR_DO_THE_NEEDFUL);
       exit(EXIT_FAILURE);
       /* NOTREACHED */
    }

    return EXIT_SUCCESS;
}

好的,内容很多。 main() 函数的目的是收集用户提供的参数,执行最少的输入验证,然后将收集的参数传递给将使用它们的函数。 此示例声明一个 options 变量,该变量已使用默认值初始化并解析命令行,并在必要时更新 options

main() 函数的核心是一个 while 循环,它使用 getopt() 遍历 argv 以查找命令行选项及其参数(如果有)。 文件前面 OPTSTR #define 是驱动 getopt() 行为的模板。 opt 变量采用 getopt() 找到的任何命令行选项的字符值,并且程序对检测到命令行选项的响应发生在 switch 语句中。

那些注意听讲的人现在会质疑为什么 opt 被声明为 32 位 int,但期望采用 8 位 char? 事实证明,当 getopt() 到达 argv 的末尾时,它会返回一个取负值的 int,我将其与 EOF (文件结尾 标记)进行检查。 char 是一个有符号量,但我喜欢将变量与其函数返回值匹配。

当检测到已知的命令行选项时,会发生特定于选项的行为。 某些选项有一个参数,在 OPTSTR 中用尾随冒号指定。 当一个选项有一个参数时,程序可以通过外部定义的变量 optarg 访问 argv 中的下一个字符串。 我使用 optarg 打开文件进行读取和写入,或将命令行参数从字符串转换为整数值。

这里有几个关于风格的要点

  • opterr 初始化为 0,这将禁用 getopt 发出 ?
  • main() 的中间使用 exit(EXIT_FAILURE);exit(EXIT_SUCCESS);
  • /* NOTREACHED */ 是我喜欢的 lint 指令。
  • 在返回 int 的函数末尾使用 return EXIT_SUCCESS;
  • 显式转换隐式类型转换。

如果编译此程序,则其命令行签名将如下所示

$ ./a.out -h
a.out [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]

事实上,这就是编译后 usage() 将输出到 stderr 的内容。

8. 函数声明

/* main.c */
<...>

void usage(char *progname, int opt) {
   fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
   exit(EXIT_FAILURE);
   /* NOTREACHED */
}

int do_the_needful(options_t *options) {

   if (!options) {
     errno = EINVAL;
     return EXIT_FAILURE;
   }

   if (!options->input || !options->output) {
     errno = ENOENT;
     return EXIT_FAILURE;
   }

   /* XXX do needful stuff */

   return EXIT_SUCCESS;
}

最后,我编写的函数不是样板代码。 在此示例中,函数 do_the_needful() 接受指向 options_t 结构的指针。 我验证 options 指针是否不是 NULL,然后继续验证 inputoutput 结构成员。 如果任何一个测试失败,则返回 EXIT_FAILURE,并且通过将外部全局变量 errno 设置为传统错误代码,我向调用者发出一个一般原因信号。 调用者可以使用便捷函数 perror() 根据 errno 的值发出人类可读的错误消息。

函数几乎总是应该以某种方式验证其输入。 如果完全验证成本很高,请尝试执行一次并以不可变的方式处理验证后的数据。 usage() 函数使用 fprintf() 调用中的条件赋值来验证 progname 参数。 usage() 函数无论如何都要退出,所以我不会费心设置 errno 或对使用正确的程序名称大惊小怪。

我在此处尝试避免的一大类错误是取消引用 NULL 指针。 这将导致操作系统向我的进程发送一个名为 SYSSEGV 的特殊信号,从而导致不可避免的死亡。 用户最不想看到的是由于 SYSSEGV 导致的崩溃。 最好捕获 NULL 指针,以便发出更好的错误消息并优雅地关闭程序。

有些人抱怨在一个函数体中有多个 return 语句。 他们就“控制流的连续性”和其他内容提出论点。 老实说,如果函数中间出现问题,那么现在是返回错误条件的好时机。 编写大量的嵌套 if 语句只是为了有一个返回绝不是一个“好主意”。™

最后,如果你编写一个接受四个或更多参数的函数,请考虑将它们捆绑在一个结构中并传递指向该结构的指针。 这使得函数签名更简单,使它们更容易记住,并且在以后调用时不会出错。 这也使得调用函数稍微快一些,因为需要复制到函数堆栈帧中的内容更少。 在实践中,只有当函数被调用数百万或数十亿次时,这才会成为一个考虑因素。 如果这没有意义,请不要担心。

等等,你说不要注释!?!!

do_the_needful() 函数中,我编写了一种特定类型的注释,它旨在作为占位符而不是记录代码

/* XXX do needful stuff */

当你进入状态时,有时你不想停下来编写一些特别棘手的代码。 你稍后会回来做,只是不是现在。 那就是我会给自己留下一小块面包屑的地方。 我插入一个带有 XXX 前缀的注释和一个简短的备注,描述需要做什么。 稍后,当我有更多时间时,我将通过源代码 grep 查找 XXX。 你使用什么并不重要,只要确保它不太可能在你的代码库中的另一个上下文中出现,例如作为函数名或变量。

将所有内容放在一起

好的,当你编译并运行这个程序时,它仍然几乎什么都不做。 但是现在你有一个坚实的骨架来构建你自己的命令行解析 C 程序。

/* main.c - the complete listing */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libgen.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>

#define OPTSTR "vi:o:f:h"
#define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
#define ERR_FOPEN_INPUT  "fopen(input, r)"
#define ERR_FOPEN_OUTPUT "fopen(output, w)"
#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
#define DEFAULT_PROGNAME "george"

extern int errno;
extern char *optarg;
extern int opterr, optind;

typedef struct {
  int           verbose;
  uint32_t      flags;
  FILE         *input;
  FILE         *output;
} options_t;

int dumb_global_variable = -11;

void usage(char *progname, int opt);
int  do_the_needful(options_t *options);

int main(int argc, char *argv[]) {
    int opt;
    options_t options = { 0, 0x0, stdin, stdout };

    opterr = 0;

    while ((opt = getopt(argc, argv, OPTSTR)) != EOF) 
       switch(opt) {
           case 'i':
              if (!(options.input = fopen(optarg, "r")) ){
                 perror(ERR_FOPEN_INPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }
              break;

           case 'o':
              if (!(options.output = fopen(optarg, "w")) ){
                 perror(ERR_FOPEN_OUTPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }    
              break;
              
           case 'f':
              options.flags = (uint32_t )strtoul(optarg, NULL, 16);
              break;

           case 'v':
              options.verbose += 1;
              break;

           case 'h':
           default:
              usage(basename(argv[0]), opt);
              /* NOTREACHED */
              break;
       }

    if (do_the_needful(&options) != EXIT_SUCCESS) {
       perror(ERR_DO_THE_NEEDFUL);
       exit(EXIT_FAILURE);
       /* NOTREACHED */
    }

    return EXIT_SUCCESS;
}

void usage(char *progname, int opt) {
   fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
   exit(EXIT_FAILURE);
   /* NOTREACHED */
}

int do_the_needful(options_t *options) {

   if (!options) {
     errno = EINVAL;
     return EXIT_FAILURE;
   }

   if (!options->input || !options->output) {
     errno = ENOENT;
     return EXIT_FAILURE;
   }

   /* XXX do needful stuff */

   return EXIT_SUCCESS;
}

现在你已准备好编写更易于维护的 C 代码。 如果你有任何问题或反馈,请在评论中分享它们。

标签
XENON coated avatar will glow red in the presence of aliens.
Erik O'Shaughnessy 是一位有主见但友好的 UNIX 系统程序员,在德克萨斯州过着美好的生活。 在过去的二十年(或更长时间!)里,他曾在 IBM、Sun Microsystems、Oracle 以及最近的 Intel 工作,从事计算机系统性能相关的工作。

38 条评论

这是一篇关于编写优秀 C 代码的 *优秀* 文章。 干得好,Erik!

函数原型和全局变量应放置在与定义它们的 `*.c` 相对应的 `*.h` 中。 始终保持 API 与其代码紧密关联。

虽然我通常会同意你的观点,但本文的范围是如何编写 main.c,而不是如何构建多文件 C 程序。 我想我知道接下来要写什么了 :)

回复 作者 Shawn H Corey

这绝对会是一篇很好的下一篇文章! 这里还有一些其他的想法... 你提到了 _start() 函数,请告诉我们更多! ;) 当然,这取决于许多参数,但如果它仅限于一些常用且流行的东西,例如(x86_64、Linux、gcc),那么在 1-3 篇文章中深入探讨该主题是可行的。 另一个好主题是调试,一些关于如何调试代码的示例,而不是程序被 coredumped 并被不同的信号杀死。 你还稍微提到了“errno 被标准 C 库用作带外通信通道,以传达函数可能失败的原因”。 我认为,这值得单独写一篇文章。 通过示例展示调用者如何与程序交互,什么是 EINVAL 和 ENOENT。 告诉我们更多关于你如何处理错误的信息。

回复 作者 JnyJny

同意 Jim 的观点,文章很棒!

我非常喜欢文本的风格,尤其是内容。 它很独特,因为它代表了一种提炼的经验。 没有唯一的正确解决方案。 C 仍然是 C,但了解这门语言并不意味着每个人都会以相同的方式使用它。 经验很重要。 我会等待更多关于 C/Unix/底层编程的文章!

关于代码的一个小提示
void usage(char *progname, int opt);

此函数接受第二个参数,但它未在函数体中使用。

感谢你的赞扬,当然你是正确的,opt 参数在函数体中未使用。 我本打算在 fprintf 中发出有问题的选项,但一定是在写作的恍惚中忘记了。

回复 作者 Oleksii Tsvietnov

由于两个问题,我无法编译代码
1. 程序的完整列表丢失了 #include
2. 由于某些原因,我的 Linux 系统上没有 uint32_t

$ uname -r
5.0.7-200.fc29.x86_64

$ grep int32_ /usr/include/sys/types.h
typedef unsigned int u_int32_t;

在我修复了这两个问题后,我成功编译了代码。
而且,关于 usage() 输出的格式化有一个小提示。 我认为,在 add 中添加 \n 可能有意义,否则在 shell 中看起来不太好

#define USAGE_FMT "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]\n"

回复 作者 JnyJny

感谢你的这篇文章。 每当我开始一个新的 C 程序时,我都会再次阅读这篇文章。

这是一个好的开始,但有一个明显的遗漏。 你需要一个选项将 stderr 重定向到日志文件,并在 options_t 结构中添加一个匹配的 FILE 条目。 添加后,你可能还希望能够设置日志级别,尽管如果你使用多个 'v',verbose 计数器也可以工作。

我在调试新代码时遇到的最大问题之一是弄清楚导致崩溃的原因。 单步执行一个在崩溃前重复数千次的循环并不有趣。 此外,调试器会更改时序,并且可能根本不会崩溃。 在这种情况下,带有几个简单面包屑的日志文件可以创造奇迹。

感谢你的建议 Bob,但是我想知道在 shell 中将 stderr 重定向到文件如何在没有代码维护开销的情况下完成相同的事情? 我并不是说你想要日志文件是错误的,但我总是对仅编写我需要完成工作所需的代码量感兴趣,而不是更多。

回复 作者 Bob McConnell (未验证)

你能保证在 shell 中执行此操作不会为每次写入添加一层内核调用吗? 如果不能,则将每条消息推送到文件的时间可能会有显着差异。 当我第一次遇到这个问题时,我花了三天时间才弄清楚重定向正在减慢进程的速度,以至于崩溃不再发生。 有了日志文件,它仍然不经常发生,但至少我能够找到故障。 时序就是一切。

回复 作者 JnyJny

感谢你分享你的经验; 修复海森堡 bug 可能非常困难。 我认为我们可以同意,我在本文中介绍的函数和文件布局可以进行调整以适应调试此类问题,但是我在撰写本文的主要目的是为那些刚开始旅程的人演示 C 程序的基本结构。

回复 作者 Bob McConnell (未验证)

如果你将 main 声明为 int,你也应该返回一个 int。

你是否反对返回 EXIT_SUCCESS 或 EXIT_FAILURE 而不是零或一? 虽然 C 中充满了“魔术常量”的实例,但我发现尽可能使用宏定义常量更好。 而且我特别喜欢使用标准库提供的常量,因为我知道我可以依靠它们在兼容的运行时环境中定义。

回复 作者 oxagast (未验证)

太棒了,谢谢! 如果有时间,请告诉我 C 运行时如何工作以及编译器和链接器文件的布局/结构。

这非常有帮助。 特别是在面试中

好文章!

如果我可以完成 “usage” 函数(通过使用 'opt' 参数),以便在遇到错误时消息更具信息性,我建议这样做

...
#define OPTSTR ":vi:o:f:h"
...
void usage(char *progname, int opt) {
if (opt == '?') fprintf(stderr, "Unknown option '-%c'\n", optopt);
if (opt == ':') fprintf(stderr, "Missing argument for option '-%c'\n", optopt);
fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
exit(EXIT_FAILURE);
/* NOTREACHED */
}

所有这些都是有价值的补充,并说明了当你在坚实的基础上构建时会发生什么; 你可以开始专注于那些小的可用性改进,这些改进很容易,但对你的工具用户来说意义重大。

我认为 SYSSEGV 应该是 SIGSEGV。

好的,我什至不想撒谎。 我回去检查了我的源注释,果然,那里也是 SYSSEGV。 这只是表明,当没有编译器让你保持诚实时,编辑是多么困难。 感谢你的额外编辑,你的支票正在邮寄中! ;)

回复 作者 Rares Aioanei (未验证)

将代码复制/粘贴到我最喜欢的 Linux 机器上,然后砰... 编译错误。 :) 它在各种 RedHat 版本(CentOS-5 和 Fedora-29)上因 uint32 类型而失败。 但是,在 MacOS 上一切正常。

在 RedHat 中,真正的 “uint” typedefs 是在文件中完成的,该文件包含在 bits/types.h 中。 但是,更优雅的方法是简单地添加一个 “#include

我的评论本应具有指导意义,对 RedHat 用户有所帮助。 但是,当你在消息中使用 gt/lt 字符时,一切都会搞砸,东西会被删除。 在这种情况下,完全删除了我的消息的本质。 :-)

这里是新的尝试,希望我的记忆力能帮上忙

-----

将代码复制/粘贴到我最喜欢的 Linux 机器上,然后砰... 编译错误 :)。 它在各种 RedHat 版本(CentOS-5 和 Fedora-29)上因 uint32 类型而失败。 但是,在 MacOS 上一切正常。

在 RedHat 中,真正的 “uint” typedefs 是在文件 /usr/include/bits/stdint-uintn.h 中完成的,该文件包含在 bits/types.h 中,然后再次包含在 sys/types.h 中。 但是,更优雅的方法是在 main.c 的第一部分中简单地添加一行 #include (lt) stdint.h (gt)。

回复 作者 WWWillem (未验证)

小于/大于号吃掉了你的帖子,我回复了空无一物,所以我认为我们都处于亏损状态 :) 对这些 typedef 所在位置的出色描述,它有助于说明并非所有 C 环境都相同。

回复 作者 WWWillem (未验证)

欢迎来到 C 的无限乐趣! 我曾短暂考虑过不在我的代码中使用 uint32_t(或同源词),但本文最初的标题是“如何像我一样编写 C Main 函数”,所以我按照我通常编写的方式编写了它。 当我定期编写 C 时,它几乎总是为了支持特定的软件/硬件组合,该组合相对“静态”,并且仅针对主要/次要 OS 更新或叉车硬件升级而更改。 今天的编程环境更加流动,你必须决定你想从环境中“觅食”多少,以及你愿意重新实现多少以避免像你遇到的那样的问题。

你在某个地方提到了 Solaris。 我曾在 Sun Microsystems 工作了十年(2000-2009 年)。 我的索尼笔记本电脑是 100% Solaris X64 和 StarOffice。

当时 X86 和 Linux 在 Sun 是脏话,但我仍然偷偷地在我的地下室里做。 :-)

当这种情况发生变化,X64 和 Linux 突然变得可以接受时,我突然成为当地的多启动大师和 RedHat 专家。

WWWillem

回复 作者 WWWillem (未验证)

嘿,Sun 校友!

我在奥斯汀 TX 办事处从 2000 年到 2017 年从事性能工作,x86 在 Sun 肯定很长一段时间是脏话。 我大部分时间都在使用 SPARC 硬件,偶尔会进行一些奇怪的游览(AMD、固件、服务处理器、架构模拟器等)。

回复 作者 WWWillem (未验证)

我想你应该放弃 sys/types.h,转而使用 stdint.h,后者在 c99 中实现,以标准化这些跨系统

这是一篇关于如何编写好的C语言程序的优秀文章,我从中学习到了一些关于C编程的新知识。

感谢您的积极反馈!请继续关注,我还有更多文章正在准备中 :)

如果C语言中的Main函数是INT MAIN(),那么你必须为该函数添加返回值,这里的VALUE可能是0或1。

注意:这里的大写字母表示语法或C编程函数名称、关键字等。

这太棒了!我真希望几年前就能看到这样的例子。

既然你提到你也用Python编程,那么要求提供一个使用Python标准库的本质上相同的模板是否太过分了?(拜托了,求求你了?!)

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.