标签 C语言 下的文章

ACM 批评最新的 C 语言标准草案 C23

将于今年发布的 C23 标准带来了一系列新的特性和改变,“使编写安全、正确和可靠的代码变得更容易”。ACM 杂志最近撰文称,新标准的非特性、错误特性和缺陷都足够多和严重,程序员在没有仔细权衡风险和收益的情况下不应该 “升级” —— C23 “把几十年来完全合法的程序变成了燃烧弹”,如 C23 现在宣布 realloc(ptr,0) 为未定义行为。它还批评了 C23 的新的不可达注解,以及它在指针方面缺乏改进。

消息来源:ACM
老王点评:看来这次的 C23 未必能给 C 语言带来更好的发展,而另外一边,Rust 虎视眈眈。

VoIP 公司 3CX 最初以为供应链攻击是误报

VoiP 软件提供商 3CX 遭遇了一次供应链攻击,该公司称收到警告之后在 VirusTotal 上测试了其桌面应用,没发现问题,认为是一次误报。几天后又检查了一下,结果相同。但直到网络安全公司提供了完整细节之后 3CX 才认识到漏洞的情况。3CX 的日活用户高达 1200 万,包括诸多知名公司客户。该事件是自 2020 年 SolarWinds 被攻击和 2021 年 Kaseya 被攻击以来最突出的供应链攻击。

消息来源:The Register
老王点评:即便是这样大的公司,也可能面对供应链攻击会措手不及。

谷歌云端硬盘撤销 500 万文件数量限制

此前,有用户报告,谷歌云端硬盘服务对能储存的文件数量设定了 500 万的数量上限,超过这个限额之后就必须删除旧文件,否则将无法使用。它的最高容量为 30 TB,因此达到 500 万文件数量也是有可能的。这个上限只适用于你在云端硬盘中创建的文件数量,而不是分享到云端硬盘的文件总数。谷歌最初称这一做法是为了“保持强大的性能和可靠性”,有助于防止“滥用”。在听到用户反对之后,谷歌承认这一限制影响到了少部分用户,取消 500 万文件数量限制。

消息来源:The Verge
老王点评:这件事里面最糟糕的是,谷歌在没有征询用户意见和给出更弹性的解决方案时,就实施了限制。

KDE 成为 Steam Deck 开发者模式的默认桌面

Steam Deck 已经发货,KDE Plasma 成为“开发者模式”下的默认桌面。对于 KDE 开发者来说,为了能顺利地在 Steam Deck 上运行,他们经历了忙碌的一周,进行了大量修复和改进。

老王点评:虽然这不应该意外,因为 Steam Deck 是基于 Linux 的。

Linux 内核准备采用现代 C 语言标准

Linux 内核仍然采用的是 1989 版的 C 语言标准,它已有三十多年历史了。由于一些特性在 C89 中不支持,因此在处理一些问题时格外麻烦。Torvalds 说,内核代码一直停留在 C89 的原因之一是旧版本的 gcc 编译器会出现奇怪的问题,现在内核要求的 gcc 最低版本已经提高到了 v5.1,那些 bug 可能不再相关了。在讨论之后,他宣布将在下一个内核版本 5.18 中 尝试下新的 C 语言标准。如果一切顺利,C 语言标准有望在下一个内核版本中迁移到 C11。

老王点评:虽然 Linux 内核存在一些历史遗留问题,但是好在内核社区是一个活跃的社区,能不断前进。

富士通准备停止其大型机和 UNIX 业务

富士通将在 2030 年之前 停止生产和销售其大型机系统,并将在 2029 年底之前停止发售其 Unix 服务器系统。对这两个组合的支持服务将再持续五年。对于大多数富士通大型机客户来说,他们需要在这个期限之前将他们的大型机应用迁移到另一个平台上。

老王点评:虽然这看起来很遥远,但对于大多数使用大型机的组织来说,大型机是一项巨大的长期投资,这对他们来说是一件大事。

只需掌握一点 C 语言的知识,你就可以做很多事情。

 title=

我精通几种编程语言,但我最喜欢的是 C 语言。C 语言开发于 20 世纪 70 年代,作为一种系统编程语言,在 2021 年仍然是最受欢迎的编程语言之一。如果你想探索 C 语言的几个特点,可以从去年的这些热门文章开始:

在 C 语言中使用 getopt 进行短选项解析

如果你的程序每次运行时都能对用户作出反应,你就可以使它变得更加灵活。让你的用户告诉你的程序使用什么文件或如何以不同的方式做事情。为此,你需要读取命令行。这篇文章 告诉你如何使用 argv 直接读取命令行,并使用 getopt 读取短命令行选项。

在 Linux 中用 ncurses 定位屏幕上的文本

大多数 Linux 工具只是从屏幕的底部滚动文本。但如果你想在屏幕上定位文本,比如游戏或数据显示,该怎么办?这就是 ncurses 的作用。

用 DOS conio 开始编程

C 语言并不只适用于 Linux。你可以在许多操作系统上找到 C,包括 DOS。许多 C 程序员控制控制台输入和输出的标准方法是使用 conio 库。学习如何 使用 conio` 来更新 DOS 程序的控制台,包括文本颜色和文本窗口。

使用位域和掩码编写国际象棋游戏

在 C 语言程序中关联信息的标准方法是使用一个结合了两个或多个相关字段的结构体。例如,你可以用一个结构体来跟踪棋盘上的棋子。但是有一种更直接的方法来跟踪同样的信息,同时使用更少的数据和内存。使用 二进制位域和掩码,你可以存储一个单一的值来识别每个方格中的棋子和颜色。

C 语言编程中的 5 个常见错误以及如何修复它们

即使是最好的程序员也会产生编程错误。根据你的程序所做的事情,这些 bug 可能会引入安全漏洞,导致程序崩溃,或产生意外的行为。但是,通过一点额外的代码,你可以避免最常见和最严重的 C 语言编程错误。这里有 五个可能破坏你的应用的 bug 以及你如何避免它们

只需一点 C 语言知识,你就可以做很多事情。探索新的方法,为你的下一个 C 语言编程项目增加功能。


via: https://opensource.com/article/22/1/c-programming

作者:Jim Hall 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

增强 C 语言程序的弹性和可靠性的五种方法。

 title=

即使是最好的程序员也无法完全避免错误。这些错误可能会引入安全漏洞、导致程序崩溃或产生意外操作,具体影响要取决于程序的运行逻辑。

C 语言有时名声不太好,因为它不像近期的编程语言(比如 Rust)那样具有内存安全性。但是通过额外的代码,一些最常见和严重的 C 语言错误是可以避免的。下文讲解了可能影响应用程序的五个错误以及避免它们的方法:

1、未初始化的变量

程序启动时,系统会为其分配一块内存以供存储数据。这意味着程序启动时,变量将获得内存中的一个随机值。

有些编程环境会在程序启动时特意将内存“清零”,因此每个变量都得以有初始的零值。程序中的变量都以零值作为初始值,听上去是很不错的。但是在 C 编程规范中,系统并不会初始化变量。

看一下这个使用了若干变量和两个数组的示例程序:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int i, j, k;
  int numbers[5];
  int *array;

  puts("These variables are not initialized:");

  printf("  i = %d\n", i);
  printf("  j = %d\n", j);
  printf("  k = %d\n", k);

  puts("This array is not initialized:");

  for (i = 0; i < 5; i++) {
    printf("  numbers[%d] = %d\n", i, numbers[i]);
  }

  puts("malloc an array ...");
  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc'ed array is not initialized:");

    for (i = 0; i < 5; i++) {
      printf("  array[%d] = %d\n", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

这个程序不会初始化变量,所以变量以系统内存中的随机值作为初始值。在我的 Linux 系统上编译和运行这个程序,会看到一些变量恰巧有“零”值,但其他变量并没有:

These variables are not initialized:
  i = 0
  j = 0
  k = 32766
This array is not initialized:
  numbers[0] = 0
  numbers[1] = 0
  numbers[2] = 4199024
  numbers[3] = 0
  numbers[4] = 0
malloc an array ...
This malloc'ed array is not initialized:
  array[0] = 0
  array[1] = 0
  array[2] = 0
  array[3] = 0
  array[4] = 0
Ok

很幸运,ij 变量是从零值开始的,但 k 的起始值为 32766。在 numbers 数组中,大多数元素也恰好从零值开始,只有第三个元素的初始值为 4199024。

在不同的系统上编译相同的程序,可以进一步显示未初始化变量的危险性。不要误以为“全世界都在运行 Linux”,你的程序很可能某天在其他平台上运行。例如,下面是在 FreeDOS 上运行相同程序的结果:

These variables are not initialized:
  i = 0
  j = 1074
  k = 3120
This array is not initialized:
  numbers[0] = 3106
  numbers[1] = 1224
  numbers[2] = 784
  numbers[3] = 2926
  numbers[4] = 1224
malloc an array ...
This malloc'ed array is not initialized:
  array[0] = 3136
  array[1] = 3136
  array[2] = 14499
  array[3] = -5886
  array[4] = 219
Ok

永远都要记得初始化程序的变量。如果你想让变量将以零值作为初始值,请额外添加代码将零分配给该变量。预先编好这些额外的代码,这会有助于减少日后让人头疼的调试过程。

2、数组越界

C 语言中,数组索引从零开始。这意味着对于长度为 10 的数组,索引是从 0 到 9;长度为 1000 的数组,索引则是从 0 到 999。

程序员有时会忘记这一点,他们从索引 1 开始引用数组,产生了 “大小差一” off by one 错误。在长度为 5 的数组中,程序员在索引“5”处使用的值,实际上并不是数组的第 5 个元素。相反,它是内存中的一些其他值,根本与此数组无关。

这是一个数组越界的示例程序。该程序使用了一个只含有 5 个元素的数组,但却引用了该范围之外的数组元素:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int i;
  int numbers[5];
  int *array;

  /* test 1 */

  puts("This array has five elements (0 to 4)");

  /* initalize the array */
  for (i = 0; i < 5; i++) {
    numbers[i] = i;
  }

  /* oops, this goes beyond the array bounds: */
  for (i = 0; i < 10; i++) {
    printf("  numbers[%d] = %d\n", i, numbers[i]);
  }

  /* test 2 */

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc'ed array also has five elements (0 to 4)");

    /* initalize the array */
    for (i = 0; i < 5; i++) {
      array[i] = i;
    }

    /* oops, this goes beyond the array bounds: */
    for (i = 0; i < 10; i++) {
      printf("  array[%d] = %d\n", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

可以看到,程序初始化了数组的所有值(从索引 0 到 4),然后从索引 0 开始读取,结尾是索引 9 而不是索引 4。前五个值是正确的,再后面的值会让你不知所以:

This array has five elements (0 to 4)
  numbers[0] = 0
  numbers[1] = 1
  numbers[2] = 2
  numbers[3] = 3
  numbers[4] = 4
  numbers[5] = 0
  numbers[6] = 4198512
  numbers[7] = 0
  numbers[8] = 1326609712
  numbers[9] = 32764
malloc an array ...
This malloc'ed array also has five elements (0 to 4)
  array[0] = 0
  array[1] = 1
  array[2] = 2
  array[3] = 3
  array[4] = 4
  array[5] = 0
  array[6] = 133441
  array[7] = 0
  array[8] = 0
  array[9] = 0
Ok

引用数组时,始终要记得追踪数组大小。将数组大小存储在变量中;不要对数组大小进行 硬编码 hard-code 。否则,如果后期该标识符指向另一个不同大小的数组,却忘记更改硬编码的数组长度时,程序就可能会发生数组越界。

3、字符串溢出

字符串只是特定类型的数组。在 C 语言中,字符串是一个由 char 类型值组成的数组,其中用一个零字符表示字符串的结尾。

因此,与数组一样,要注意避免超出字符串的范围。有时也称之为 字符串溢出

使用 gets 函数读取数据是一种很容易发生字符串溢出的行为方式。gets 函数非常危险,因为它不知道在一个字符串中可以存储多少数据,只会机械地从用户那里读取数据。如果用户输入像 foo 这样的短字符串,不会发生意外;但是当用户输入的值超过字符串长度时,后果可能是灾难性的。

下面是一个使用 gets 函数读取城市名称的示例程序。在这个程序中,我还添加了一些未使用的变量,来展示字符串溢出对其他数据的影响:

#include <stdio.h>
#include <string.h>

int
main()
{
  char name[10];                       /* Such as "Chicago" */
  int var1 = 1, var2 = 2;

  /* show initial values */

  printf("var1 = %d; var2 = %d\n", var1, var2);

  /* this is bad .. please don't use gets */

  puts("Where do you live?");
  gets(name);

  /* show ending values */

  printf("<%s> is length %d\n", name, strlen(name));
  printf("var1 = %d; var2 = %d\n", var1, var2);

  /* done */

  puts("Ok");
  return 0;
}

当你测试类似的短城市名称时,该程序运行良好,例如伊利诺伊州的 Chicago 或北卡罗来纳州的Raleigh

var1 = 1; var2 = 2
Where do you live?
Raleigh
<Raleigh> is length 7
var1 = 1; var2 = 2
Ok

威尔士的小镇 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 有着世界上最长的名字之一。这个字符串有 58 个字符,远远超出了 name 变量中保留的 10 个字符。结果,程序将值存储在内存的其他区域,覆盖了 var1var2 的值:

var1 = 1; var2 = 2
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
var1 = 2036821625; var2 = 2003266668
Ok
Segmentation fault (core dumped)

在运行结束之前,程序会用长字符串覆盖内存的其他部分区域。注意,var1var2 的值不再是起始的 12

避免使用 gets 函数,改用更安全的方法来读取用户数据。例如,getline 函数会分配足够的内存来存储用户输入,因此不会因输入长值而发生意外的字符串溢出。

4、重复释放内存

“分配的内存要手动释放”是良好的 C 语言编程原则之一。程序可以使用 malloc 函数为数组和字符串分配内存,该函数会开辟一块内存,并返回一个指向内存中起始地址的指针。之后,程序可以使用 free 函数释放内存,该函数会使用指针将内存标记为未使用。

但是,你应该只使用一次 free 函数。第二次调用 free 会导致意外的后果,可能会毁掉你的程序。下面是一个针对此点的简短示例程序。程序分配了内存,然后立即释放了它。但为了模仿一个健忘但有条理的程序员,我在程序结束时又一次释放了内存,导致两次释放了相同的内存:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int *array;

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("malloc succeeded");

    puts("Free the array...");
    free(array);
  }

  puts("Free the array...");
  free(array);

  puts("Ok");
}

运行这个程序会导致第二次使用 free 函数时出现戏剧性的失败:

malloc an array ...
malloc succeeded
Free the array...
Free the array...
free(): double free detected in tcache 2
Aborted (core dumped)

要记得避免在数组或字符串上多次调用 free。将 mallocfree 函数定位在同一个函数中,这是避免重复释放内存的一种方法。

例如,一个纸牌游戏程序可能会在主函数中为一副牌分配内存,然后在其他函数中使用这副牌来玩游戏。记得在主函数,而不是其他函数中释放内存。将 mallocfree 语句放在一起有助于避免多次释放内存。

5、使用无效的文件指针

文件是一种便捷的数据存储方式。例如,你可以将程序的配置数据存储在 config.dat 文件中。Bash shell 会从用户家目录中的 .bash_profile 读取初始化脚本。GNU Emacs 编辑器会寻找文件 .emacs 以从中确定起始值。而 Zoom 会议客户端使用 zoomus.conf 文件读取其程序配置。

所以,从文件中读取数据的能力几乎对所有程序都很重要。但是假如要读取的文件不存在,会发生什么呢?

在 C 语言中读取文件,首先要用 fopen 函数打开文件,该函数会返回指向文件的流指针。你可以结合其他函数,使用这个指针来读取数据,例如 fgetc 会逐个字符地读取文件。

如果要读取的文件不存在或程序没有读取权限,fopen 函数会返回 NULL 作为文件指针,这表示文件指针无效。但是这里有一个示例程序,它机械地直接去读取文件,不检查 fopen 是否返回了 NULL

#include <stdio.h>

int
main()
{
  FILE *pfile;
  int ch;

  puts("Open the FILE.TXT file ...");

  pfile = fopen("FILE.TXT", "r");

  /* you should check if the file pointer is valid, but we skipped that */

  puts("Now display the contents of FILE.TXT ...");

  while ((ch = fgetc(pfile)) != EOF) {
    printf("<%c>", ch);
  }

  fclose(pfile);

  /* done */

  puts("Ok");
  return 0;
}

当你运行这个程序时,第一次调用 fgetc 会失败,程序会立即中止:

Open the FILE.TXT file ...
Now display the contents of FILE.TXT ...
Segmentation fault (core dumped)

始终检查文件指针以确保其有效。例如,在调用 fopen 打开一个文件后,用类似 if (pfile != NULL) 的语句检查指针,以确保指针是可以使用的。

人都会犯错,最优秀的程序员也会产生编程错误。但是,遵循上面这些准则,添加一些额外的代码来检查这五种类型的错误,就可以避免最严重的 C 语言编程错误。提前编写几行代码来捕获这些错误,可能会帮你节省数小时的调试时间。


via: https://opensource.com/article/21/10/programming-bugs

作者:Jim Hall 选题:lujun9972 译者:unigeorge 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

软件库是重复使用代码的一种简单而合理的方式。

 title=

软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 Linux 为例,但创建、发布和使用这些库的步骤也可以应用于其它类 Unix 系统。

这些示例库使用 C 语言编写,非常适合该任务。Linux 内核大部分由 C 语言和少量汇编语言编写(Windows 和 Linux 的表亲如 macOS 也是如此)。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库等主要由 C 语言编写。所以使用 C 语言编写库就是使用 Linux 的原生语言来编写。除此之外,C 语言的性能也在一众高级语言中鹤立鸡群。

还有两个来访问这些库的示例 客户程序 client (一个使用 C,另一个使用 Python)。毫无疑问可以使用 C 语言客户程序来访问 C 语言编写的库,但是 Python 客户程序示例说明了一个由 C 语言编写的库也可以服务于其他编程语言。

静态库和动态库对比

Linux 系统存在两种类型库:

  • 静态库(也被称为归档库):在编译过程中的链接阶段,静态库会被编译进程序(例如 C 或 Rust)中。每个客户程序都有属于自己的一份库的拷贝。静态库有一个显而易见的缺点 —— 当库需要进行一定改动时(例如修复一个 bug),静态库必须重新链接一次。接下来要介绍的动态库避免了这一缺点。
  • 动态库(也被称为共享库):动态库首先会在程序编译中的链接阶段被标记,但是客户程序和库代码在运行之前仍然没有联系,且库代码不会进入到客户程序中。系统的动态加载器会把一个共享库和正在运行的客户程序进行连接,无论该客户程序是由静态编译语言(如 C)编写,还是由动态解释语言(如 Python)编写。因此,动态库不需要麻烦客户程序便可以进行更新。最后,多个客户程序可以共享同一个动态库的单一副本。

通常来说,动态库优于静态库,尽管其复杂性较高而性能较低。下面是两种类型的库如何创建和发布:

  1. 库的源代码会被编译成一个或多个目标模块,目标模块是二进制文件,可以被包含在库中并且链接到可执行的二进制中。
  2. 目标模块会会被打包成一个文件。对于静态库,标准的文件拓展名是 .a 意为“ 归档 archive ”;对于动态库,标准的文件拓展名是 .so 意为“ 共享目标 shared object ”。对于这两个相同功能的示例库,分别发布为 libprimes.a (静态库)和 libshprimes.so (动态库)。两种库的文件名都使用前缀 lib 进行标识。
  3. 库文件被复制到标准目录下,使得客户程序可以轻松地访问到库。无论是静态库还是动态库,典型的位置是 /usr/lib 或者 /usr/local/lib,当然其他位置也是可以的。

构建和发布每种库的具体步骤会在下面详细介绍。首先我将介绍两种库里涉及到的 C 函数。

示例库函数

这两个示例库都是由五个相同的 C 函数构建而成的,其中四个函数可供客户程序使用。第五个函数是其他四个函数的一个工具函数,它显示了 C 语言怎么隐藏信息。每个函数的源代码都很短,可以将这些函数放在单个源文件中,尽管也可以放在多个源文件中(如四个公布的函数都有一个文件)。

这些库函数是基本的处理函数,以多种方式来处理质数。所有的函数接收无符号(即非负)整数值作为参数:

  • is_prime 函数测试其单个参数是否为质数。
  • are_coprimes 函数检查了其两个参数的 最大公约数 greatest common divisor (gcd)是否为 1,即是否为互质数。
  • prime_factors:函数列出其参数的质因数。
  • glodbach:函数接收一个大于等于 4 的偶数,列出其可以分解为两个质数的和。它也许存在多个符合条件的数对。该函数是以 18 世纪数学家 克里斯蒂安·哥德巴赫 Christian Goldbach 命名的,他的猜想是任意一个大于 2 的偶数可以分解为两个质数之和,这依旧是数论里最古老的未被解决的问题。

工具函数 gcd 留在已部署的库文件中,但是在没有包含这个函数的文件无法访问此函数。因此,一个使用库的客户程序无法调用 gcd 函数。仔细观察 C 函数可以明白这一点。

更多关于 C 函数的内容

每个在 C 语言中的函数都有一个存储类,它决定了函数的范围。对于函数,有两种选择。

  • 函数默认的存储类是 extern,它给了函数一个全局域。一个客户程序可以调用在示例库中用 extern 修饰的任意函数。下面是一个带有显式 extern 声明的 are_coprimes 函数定义:
extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  ...
}
  • 存储类 static 将一个函数的的范围限制到函数被定义的文件中。在示例库中,工具函数 gcd 是静态的(static):
static unsigned gcd(unsigned n1, unsigned n2) {
  ...
}

只有在 primes.c 文件中的函数可以调用 gcd,而只有 are_coprimes 函数会调用它。当静态库和动态库被构建和发布后,其他的程序可以调用外部的(extern)函数,如 are_coprimes ,但是不可以调用静态(static)函数 gcd。静态(static)存储类通过将函数范围限制在其他库函数内,进而实现了对库的客户程序隐藏 gcd 函数。

primes.c 文件中除了 gcd 函数外,其他函数并没有指明存储类,默认将会设置为外部的(extern)。然而,在库中显式注明 extern 更加常见。

C 语言区分了函数的 定义 definition 声明 declaration ,这对库来说很重要。接下来让我们开始了解定义。C 语言仅允许命名函数不允许匿名函数,并且每个函数需要定义以下内容:

  • 一个唯一的名字。一个程序不允许存在两个同名的函数。
  • 一个可以为空的参数列表。参数需要指明类型。
  • 一个返回值类型(例如:int 代表 32 位有符号整数),当没有返回值时设置为空类型(void)。
  • 用一对花括号包围起来的函数主体部分。在一个特制的示例中,函数主体部分可以为空。

程序中的每个函数必须要被定义一次。

下面是库函数 are_coprimes 的完整定义:

extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */
  return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */
}

函数返回一个布尔值(0 代表假,1 代表真),取决于两个整数参数值的最大公约数是否为 1。工具函数 gcd 计算两个整数参数 n1n2 的最大公约数。

函数声明不同于定义,其不需要主体部分:

extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */

声明在参数列表后用一个分号代表结束,它没有被花括号包围起来的主体部分。程序中的函数可以被多次声明。

为什么需要声明?在 C 语言中,一个被调用的函数必须对其调用者可见。有多种方式可以提供这样的可见性,具体依赖于编译器如何实现。一个必然可行的方式就是当它们二者位于同一个文件中时,将被调用的函数定义在在它的调用者之前。

void f() {...}     /* f 定义在其被调用前 */
void g() { f(); }  /* ok */

当函数 f 被在调用前声明,此时函数 f 的定义可以移动到函数 g 的下方。

void f();         /* 声明使得函数 f 对调用者可见 */
void g() { f(); } /* ok */
void f() {...}    /* 相较于前一种方式,此方式显得更简洁 */

但是当如果一个被调用的函数和调用它的函数不在同一个文件中时呢?因为前文提到一个函数在一个程序中需要被定义一次,那么如何使得让一个文件中被定义的函数在另一个文件中可见?

这个问题会影响库,无论是静态库还是动态库。例如在这两个质数库中函数被定义在源文件 primes.c 中,每个库中都有该函数的二进制副本,但是这些定义的函数必须要对使用库的 C 程序可见,该 C 程序有其自身的源文件。

函数声明可以帮助提供跨文件的可见性。对于上述的“质数”例子,它有一个名为 primes.h 的头文件,其声明了四个函数使得它们对使用库的 C 程序可见。

/** 头文件 primes.h:函数声明 **/
extern unsigned is_prime(unsigned);
extern void prime_factors(unsigned);
extern unsigned are_coprimes(unsigned, unsigned);
extern void goldbach(unsigned);

这些声明通过为每个函数指定其调用语法来作为接口。

为了客户程序的便利性,头文件 primes.h 应该存储在 C 编译器查找路径下的目录中。典型的位置有 /usr/include/usr/local/include。一个 C 语言客户程序应使用 #include 包含这个头文件,并尽可能将这条语句其程序源代码的首部(头文件将会被导入另一个源文件的“头”部)。C 语言头文件可以被导入其他语言(如 Rust 语言)中的 bindgen,使其它语言的客户程序可以访问 C 语言的库。

总之,一个库函数只可以被定义一次,但可以在任何需要它的地方进行声明,任一使用 C 语言库的程序都需要该声明。头文件可以包含函数声明,但不能包含函数定义。如果头文件包含了函数定义,那么该文件可能会在一个 C 语言程序中被多次包含,从而破坏了一个函数在 C 语言程序中必须被精确定义一次的规则。

库的源代码

下面是两个库的源代码。这部分代码、头文件、以及两个示例客户程序都可以在 我的网页 上找到。

#include <stdio.h>
#include <math.h>

extern unsigned is_prime(unsigned n) {
  if (n <= 3) return n > 1;                   /* 2 和 3 是质数 */
  if (0 == (n % 2) || 0 == (n % 3)) return 0; /* 2 和 3 的倍数不会是质数 */

  /* 检查 n 是否是其他 < n 的值的倍数 */
  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* 不是质数 */

  return 1; /* 一个不是 2 和 3 的质数 */
}

extern void prime_factors(unsigned n) {
  /* 在数字 n 的质因数分解中列出所有 2 */
  while (0 == (n % 2)) {  
    printf("%i ", 2);
    n /= 2;
  }

  /* 数字 2 已经处理完成,下面处理奇数 */
  unsigned i;
  for (i = 3; i <= sqrt(n); i += 2) {
    while (0 == (n % i)) {
      printf("%i ", i);
      n /= i;
    }
  }

  /* 还有其他质因数?*/
  if (n > 2) printf("%i", n);
}

/* 工具函数:计算最大公约数 */
static unsigned gcd(unsigned n1, unsigned n2) {
  while (n1 != 0) {
    unsigned n3 = n1;
    n1 = n2 % n1;
    n2 = n3;
  }
  return n2;
}

extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  return 1 == gcd(n1, n2);
}

extern void goldbach(unsigned n) {
  /* 输入错误 */
  if ((n <= 2) || ((n & 0x01) > 0)) {
    printf("Number must be > 2 and even: %i is not.\n", n);
    return;
  }

  /* 两个简单的例子:4 和 6 */
  if ((4 == n) || (6 == n)) {
    printf("%i = %i + %i\n", n, n / 2, n / 2);
    return;
  }
 
  /* 当 n > 8 时,存在多种可能性 */
  unsigned i;
  for (i = 3; i < (n / 2); i++) {
    if (is_prime(i) && is_prime(n - i)) {
      printf("%i = %i + %i\n", n, i, n - i);
      /* 如果只需要一对,那么用 break 语句替换这句 */
    }
  }
}

库函数

这些函数可以被库利用。两个库可以从相同的源代码中获得,同时头文件 primes.h 是两个库的 C 语言接口。

构建库

静态库和动态库在构建和发布的步骤上有一些细节的不同。静态库需要三个步骤,而动态库需要增加两个步骤即一共五个步骤。额外的步骤表明了动态库的动态方法具有更多的灵活性。让我们先从静态库开始。

库的源文件 primes.c 被编译成一个目标模块。下面是命令,百分号 % 代表系统提示符,两个井字符 # 是我的注释。

% gcc -c primes.c ## 步骤1(静态)

这一步生成目标模块是二进制文件 primes.o-c 标志意味着只编译。

下一步是使用 Linux 的 ar 命令将目标对象归档。

% ar -cvq libprimes.a primes.o ## 步骤2(静态)

-cvq 三个标识分别是“创建”、“详细的”、“快速添加”(以防新文件没有添加到归档中)的简称。回忆一下,前文提到过前缀 lib 是必须的,而库名是任意的。当然,库的文件名必须是唯一的,以避免冲突。

归档已经准备好要被发布:

% sudo cp libprimes.a /usr/local/lib ## 步骤3(静态)

现在静态库对接下来的客户程序是可见的,示例在后面。(包含 sudo 可以确保有访问权限将文件复制进 /usr/local/lib 目录中)

动态库还需要一个或多个对象模块进行打包:

% gcc primes.c -c -fpic ## 步骤1(动态)

增加的选项 -fpic 指示编译器生成与位置无关的代码,这意味着不需要将该二进制模块加载到一个固定的内存位置。在一个拥有多个动态库的系统中这种灵活性是至关重要的。生成的对象模块会略大于静态库生成的对象模块。

下面是从对象模块创建单个库文件的命令:

% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2(动态)

选项 -shared 表明了该库是一个共享的(动态的)而不是静态的。-Wl 选项引入了一系列编译器选项,第一个便是设置动态库的 -soname,这是必须设置的。soname 首先指定了库的逻辑名字(libshprimes.so),接下来的 -o 选项指明了库的物理文件名字(libshprimes.so.1)。这样做的目的是为了保持逻辑名不变的同时允许物理名随着新版本而发生变化。在本例中,在物理文件名 libshprimes.so.1 中最后的 1 代表是第一个库的版本。尽管逻辑文件名和物理文件名可以是相同的,但是最佳做法是将它们命名为不同的名字。一个客户程序将会通过逻辑名(本例中为 libshprimes.so)来访问库,稍后我会进一步解释。

接下来的一步是通过复制共享库到合适的目录下使得客户程序容易访问,例如 /usr/local/lib 目录:

% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3(动态)

现在在共享库的逻辑名(libshprimes.so)和它的物理文件名(/usr/local/lib/libshprimes.so.1)之间设置一个符号链接。最简单的方式是将 /usr/local/lib 作为工作目录,在该目录下输入命令:

% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4(动态)

逻辑名 libshprimes.so 不应该改变,但是符号链接的目标(libshrimes.so.1)可以根据需要进行更新,新的库实现可以是修复了 bug,提高性能等。

最后一步(一个预防措施)是调用 ldconfig 工具来配置系统的动态加载器。这个配置保证了加载器能够找到新发布的库。

% sudo ldconfig ## 步骤5(动态)

到现在,动态库已为包括下面的两个在内的示例客户程序准备就绪了。

一个使用库的 C 程序

这个示例 C 程序是一个测试程序,它的源代码以两条 #include 指令开始:

#include <stdio.h>  /* 标准输入/输出函数 */
#include <primes.h> /* 我的库函数 */

文件名两边的尖括号表示可以在编译器的搜索路径中找到这些头文件(对于 primes.h 文件来说在 /usr/local/inlcude 目录下)。如果不包含 #include,编译器会抱怨缺少 is_primeprime_factors 等函数的声明,它们在两个库中都有发布。顺便提一句,测试程序的源代码不需要更改即可测试两个库中的每一个库。

相比之下,库的源文件(primes.c)使用 #include 指令打开以下头文件:

#include <stdio.h>
#include <math.h>

math.h 头文件是必须的,因为库函数 prime_factors 会调用数学函数 sqrt,其在标准库 libm.so 中。

作为参考,这是测试库程序的源代码:

#include <stdio.h>
#include <primes.h>

int main() {
  /* 是质数 */
  printf("\nis_prime\n");
  unsigned i, count = 0, n = 1000;
  for (i = 1; i <= n; i++) {
    if (is_prime(i)) {
      count++;
      if (1 == (i % 100)) printf("Sample prime ending in 1: %i\n", i);
    }
  }
  printf("%i primes in range of 1 to a thousand.\n", count);

  /* prime_factors */
  printf("\nprime_factors\n");
  printf("prime factors of 12: ");
  prime_factors(12);
  printf("\n");
 
  printf("prime factors of 13: ");
  prime_factors(13);
  printf("\n");
 
  printf("prime factors of 876,512,779: ");
  prime_factors(876512779);
  printf("\n");

  /* 是合数 */
  printf("\nare_coprime\n");
  printf("Are %i and %i coprime? %s\n",
         21, 22, are_coprimes(21, 22) ? "yes" : "no");
  printf("Are %i and %i coprime? %s\n",
         21, 24, are_coprimes(21, 24) ? "yes" : "no");

  /* 哥德巴赫 */
  printf("\ngoldbach\n");
  goldbach(11);    /* error */
  goldbach(4);     /* small one */
  goldbach(6);     /* another */
  for (i = 100; i <= 150; i += 2) goldbach(i);

  return 0;
}

测试程序

在编译 tester.c 文件到可执行文件时,难处理的部分时链接选项的顺序。回想前文中提到两个示例库都是用 lib 作为前缀开始,并且每一个都有一个常规的拓展后缀:.a 代表静态库 libprimes.a.so 代表动态库 libshprimes.so。在链接规范中,前缀 lib 和拓展名被忽略了。链接标志以 -l (小写 L)开始,并且一条编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译指令,使用动态库作为示例:

% gcc -o tester tester.c -lshprimes -lm

第一个链接标志指定了库 libshprimes.so,第二个链接标志指定了标准数学库 libm.so

链接器是懒惰的,这意味着链接标志的顺序是需要考虑的。例如,调整上述实例中的链接顺序将会产生一个编译时错误:

% gcc -o tester tester.c -lm -lshprimes ## 危险!

链接 libm.so 库的标志先出现,但是这个库中没有函数被测试程序显式调用;因此,链接器不会链接到 math.so 库。调用 sqrt 库函数仅发生在 libshprimes.so 库中包含的 prime_factors 函数。编译测试程序返回的错误是:

primes.c: undefined reference to 'sqrt'

因此,链接标志的顺序应该是通知链接器需要 sqrt 函数:

% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes

链接器在 libshprimes.so 库中发现了对库函数 sqrt 的调用,所以接下来对数学库 libm.so做了合适的链接。链接还有一个更复杂的选项,它支持链接的标志顺序。然而在本例中,最简单的方式就是恰当地排列链接标志。

下面是运行测试程序的部分输出结果:

is_prime
Sample prime ending in 1: 101
Sample prime ending in 1: 401
...
168 primes in range of 1 to a thousand.

prime_factors
prime factors of 12: 2 2 3
prime factors of 13: 13
prime factors of 876,512,779: 211 4154089

are_coprime
Are 21 and 22 coprime? yes
Are 21 and 24 coprime? no

goldbach
Number must be &gt; 2 and even: 11 is not.
4 = 2 + 2
6 = 3 + 3
...
32 =  3 + 29
32 = 13 + 19
...
100 =  3 + 97
100 = 11 + 89
...

对于 goldbach 函数,即使一个相当小的偶数值(例如 18)也许存在多个一对质数之和的组合(在这种情况下,5+13 和 7+11)。因此这种多个质数对是使得尝试证明哥德巴赫猜想变得复杂的因素之一。

封装使用库的 Python 程序

与 C 不同,Python 不是一个静态编译语言,这意味着 Python 客户示例程序必须访问动态版本而非静态版本的 primes 库。为了能这样做,Python 中有众多的支持 外部语言接口 foreign function interface (FFI)的模块(标准的或第三方的),它们允许用一种语言编写的程序来调用另一种语言编写的函数。Python 中的 ctypes 是一个标准的、相对简单的允许 Python 代码调用 C 函数的 FFI。

任何 FFI 都面临挑战,因为对接的语言不大可能会具有完全相同的数据类型。例如:primes 库使用 C 语言类型 unsigned int,而 Python 并不具有这种类型;因此 ctypes FFI 将 C 语言中的 unsigned int 类型映射为 Python 中的 int 类型。在 primes 库中发布的四个 extern C 函数中,有两个在具有显式 ctypes 配置的 Python 中会表现得更好。

C 函数 prime_factorsgoldbach 返回 void 而不是返回一个具体类型,但是 ctypes 默认会将 C 语言中的 void 替换为 Python 语言中的 int。当从 Python 代码中调用时,这两个 C 函数会从栈中返回一个随机整数值(因此,该值无任何意义)。然而,可以对 ctypes 进行配置,让这些函数返回 None (Python 中为 null 类型)。下面是对 prime_factors 函数的配置:

primes.prime_factors.restype = None

可以用类似的语句处理 goldbach 函数。

下面的交互示例(在 Python3 中)展示了在 Python 客户程序和 primes 库之间的接口是简单明了的。

>>> from ctypes import cdll

>>> primes = cdll.LoadLibrary("libshprimes.so") ## 逻辑名

>>> primes.is_prime(13)
1
>>> primes.is_prime(12)
0

>>> primes.are_coprimes(8, 24)
0
>>> primes.are_coprimes(8, 25)
1

>>> primes.prime_factors.restype = None
>>> primes.goldbach.restype = None

>>> primes.prime_factors(72)
2 2 2 3 3

>>> primes.goldbach(32)
32 = 3 + 29
32 = 13 + 19

primes 库中的函数只使用一个简单数据类型:unsigned int。如果这个 C 语言库使用复杂的类型如结构体,如果库函数传递和返回指向结构体的指针,那么比 ctypes 更强大的 FFI 更适合作为一个在 Python 语言和 C 语言之间的平滑接口。尽管如此,ctypes 示例展示了一个 Python 客户程序可以使用 C 语言编写的库。值得注意的是,用作科学计算的流行的 Numpy 库是用 C 语言编写的,然后在高级 Python API 中公开。

简单的 primes 库和高级的 Numpy 库强调了 C 语言仍然是编程语言中的通用语言。几乎每一个语言都可以与 C 语言交互,同时通过 C 语言也可以和任何其他语言交互。Python 很容易和 C 语言交互,作为另外一个例子,当 Panama 项目 成为 Java Native Interface(JNI)一个替代品后,Java 语言和 C 语言交互也会变的很容易。


via: https://opensource.com/article/21/2/linux-software-libraries

作者:Marty Kalin 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

理解 I/O 有助于提升你的效率。

 title=

如果你打算学习 C 语言的输入、输出,可以从 stdio.h 包含文件开始。正如你从其名字中猜到的,该文件定义了所有的标准(“std”)的输入和输出(“io”)函数。

大多数人学习的第一个 stdio.h 的函数是打印格式化输出的 printf 函数。或者是用来打印一个字符串的 puts 函数。这些函数非常有用,可以将信息打印给用户,但是如果你想做更多的事情,则需要了解其他函数。

你可以通过编写一个常见 Linux 命令的副本来了解其中一些功能和方法。cp 命令主要用于复制文件。如果你查看 cp 的帮助手册,可以看到 cp 命令支持非常多的参数和选项。但最简单的功能,就是复制文件:

cp infile outfile

你只需使用一些读写文件的基本函数,就可以用 C 语言来自己实现 cp 命令。

一次读写一个字符

你可以使用 fgetcfputc 函数轻松地进行输入输出。这些函数一次只读写一个字符。该用法被定义在 stdio.h,并且这也很浅显易懂:fgetc 是从文件中读取一个字符,fputc 是将一个字符保存到文件中。

int fgetc(FILE *stream);
int fputc(int c, FILE *stream);

编写 cp 命令需要访问文件。在 C 语言中,你使用 fopen 函数打开一个文件,该函数需要两个参数:文件名和打开文件的模式。模式通常是从文件读取(r)或向文件写入(w)。打开文件的方式也有其他选项,但是对于本教程而言,仅关注于读写操作。

因此,将一个文件复制到另一个文件就变成了打开源文件和目标文件,接着,不断从第一个文件读取字符,然后将该字符写入第二个文件。fgetc 函数返回从输入文件中读取的单个字符,或者当文件完成后返回文件结束标记(EOF)。一旦读取到 EOF,你就完成了复制操作,就可以关闭两个文件。该代码如下所示:

  do {
    ch = fgetc(infile);
    if (ch != EOF) {
      fputc(ch, outfile);
    }
  } while (ch != EOF);

你可以使用此循环编写自己的 cp 程序,以使用 fgetcfputc 函数一次读写一个字符。cp.c 源代码如下所示:

#include <stdio.h>

int
main(int argc, char **argv)
{
  FILE *infile;
  FILE *outfile;
  int ch;

  /* parse the command line */

  /* usage: cp infile outfile */

  if (argc != 3) {
    fprintf(stderr, "Incorrect usage\n");
    fprintf(stderr, "Usage: cp infile outfile\n");
    return 1;
  }

  /* open the input file */

  infile = fopen(argv[1], "r");
  if (infile == NULL) {
    fprintf(stderr, "Cannot open file for reading: %s\n", argv[1]);
    return 2;
  }

  /* open the output file */

  outfile = fopen(argv[2], "w");
  if (outfile == NULL) {
    fprintf(stderr, "Cannot open file for writing: %s\n", argv[2]);
    fclose(infile);
    return 3;
  }

  /* copy one file to the other */

  /* use fgetc and fputc */

  do {
    ch = fgetc(infile);
    if (ch != EOF) {
      fputc(ch, outfile);
    }
  } while (ch != EOF);

  /* done */

  fclose(infile);
  fclose(outfile);

  return 0;
}

你可以使用 gcc 来将 cp.c 文件编译成一个可执行文件:

$ gcc -Wall -o cp cp.c

-o cp 选项告诉编译器将编译后的程序保存到 cp 文件中。-Wall 选项告诉编译器提示所有可能的警告,如果你没有看到任何警告,则表示一切正常。

读写数据块

通过每次读写一个字符来实现自己的 cp 命令可以完成这项工作,但这并不是很快。在复制“日常”文件(例如文档和文本文件)时,你可能不会注意到,但是在复制大型文件或通过网络复制文件时,你才会注意到差异。每次处理一个字符需要大量的开销。

实现此 cp 命令的一种更好的方法是,读取一块的输入数据到内存中(称为缓存),然后将该数据集合写入到第二个文件。这样做的速度要快得多,因为程序可以一次读取更多的数据,这就就减少了从文件中“读取”的次数。

你可以使用 fread 函数将文件读入一个变量中。这个函数有几个参数:将数据读入的数组或内存缓冲区的指针(ptr),要读取的最小对象的大小(size),要读取对象的个数(nmemb),以及要读取的文件(stream):

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

不同的选项为更高级的文件输入和输出(例如,读取和写入具有特定数据结构的文件)提供了很大的灵活性。但是,在从一个文件读取数据并将数据写入另一个文件的简单情况下,可以使用一个由字符数组组成的缓冲区。

你可以使用 fwrite 函数将缓冲区中的数据写入到另一个文件。这使用了与 fread 函数有相似的一组选项:要从中读取数据的数组或内存缓冲区的指针,要读取的最小对象的大小,要读取对象的个数以及要写入的文件。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

如果程序将文件读入缓冲区,然后将该缓冲区写入另一个文件,则数组(ptr)可以是固定大小的数组。例如,你可以使用长度为 200 个字符的字符数组作为缓冲区。

在该假设下,你需要更改 cp 程序中的循环,以将数据从文件读取到缓冲区中,然后将该缓冲区写入另一个文件中:

  while (!feof(infile)) {
    buffer_length = fread(buffer, sizeof(char), 200, infile);
    fwrite(buffer, sizeof(char), buffer_length, outfile);
  }

这是更新后的 cp 程序的完整源代码,该程序现在使用缓冲区读取和写入数据:

#include <stdio.h>

int
main(int argc, char **argv)
{
  FILE *infile;
  FILE *outfile;
  char buffer[200];
  size_t buffer_length;

  /* parse the command line */

  /* usage: cp infile outfile */

  if (argc != 3) {
    fprintf(stderr, "Incorrect usage\n");
    fprintf(stderr, "Usage: cp infile outfile\n");
    return 1;
  }

  /* open the input file */

  infile = fopen(argv[1], "r");
  if (infile == NULL) {
    fprintf(stderr, "Cannot open file for reading: %s\n", argv[1]);
    return 2;
  }

  /* open the output file */

  outfile = fopen(argv[2], "w");
  if (outfile == NULL) {
    fprintf(stderr, "Cannot open file for writing: %s\n", argv[2]);
    fclose(infile);
    return 3;
  }

  /* copy one file to the other */

  /* use fread and fwrite */

  while (!feof(infile)) {
    buffer_length = fread(buffer, sizeof(char), 200, infile);
    fwrite(buffer, sizeof(char), buffer_length, outfile);
  }

  /* done */

  fclose(infile);
  fclose(outfile);

  return 0;
}

由于你想将此程序与其他程序进行比较,因此请将此源代码另存为 cp2.c。你可以使用 gcc 编译程序:

$ gcc -Wall -o cp2 cp2.c

和之前一样,-o cp2 选项告诉编译器将编译后的程序保存到 cp2 程序文件中。-Wall 选项告诉编译器打开所有警告。如果你没有看到任何警告,则表示一切正常。

是的,这真的更快了

使用缓冲区读取和写入数据是实现此版本 cp 程序更好的方法。由于它可以一次将文件的多个数据读取到内存中,因此该程序不需要频繁读取数据。在小文件中,你可能没有注意到使用这两种方案的区别,但是如果你需要复制大文件,或者在较慢的介质(例如通过网络连接)上复制数据时,会发现明显的差距。

我使用 Linux time 命令进行了比较。此命令可以运行另一个程序,然后告诉你该程序花费了多长时间。对于我的测试,我希望了解所花费时间的差距,因此我复制了系统上的 628 MB CD-ROM 镜像文件。

我首先使用标准的 Linux 的 cp 命令复制了映像文件,以查看所需多长时间。一开始通过运行 Linux 的 cp 命令,同时我还避免使用 Linux 内置的文件缓存系统,使其不会给程序带来误导性能提升的可能性。使用 Linux cp 进行的测试,总计花费不到一秒钟的时间:

$ time cp FD13LIVE.iso tmpfile

real    0m0.040s
user    0m0.001s
sys     0m0.003s

运行我自己实现的 cp 命令版本,复制同一文件要花费更长的时间。每次读写一个字符则花了将近五秒钟来复制文件:

$ time ./cp FD13LIVE.iso tmpfile

real    0m4.823s
user    0m4.100s
sys     0m0.571s

从输入读取数据到缓冲区,然后将该缓冲区写入输出文件则要快得多。使用此方法复制文件花不到一秒钟:

$ time ./cp2 FD13LIVE.iso tmpfile

real    0m0.944s
user    0m0.224s
sys     0m0.608s

我演示的 cp 程序使用了 200 个字符大小的缓冲区。我确信如果一次将更多文件数据读入内存,该程序将运行得更快。但是,通过这种比较,即使只有 200 个字符的缓冲区,你也已经看到了性能上的巨大差异。


via: https://opensource.com/article/21/3/file-io-c

作者:Jim Hall 选题:lujun9972 译者:wyxplus 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出