C语言中哪些部分最具可移植性?

16
我最近读到一篇关于Lua联合创始人Luiz H. de Figueredo和Roberto Ierusalimschy的访谈,他们在讨论Lua的设计和实现。这令我感到非常有趣。然而,讨论中的一部分让我想到了一些问题。Roberto将Lua称为“独立应用程序”(也就是说,它是纯ANSI C,不使用操作系统的任何功能)。他说,Lua的核心是完全可移植的,并且由于其“纯洁性”,已经能够更容易地被移植到以前从未考虑过的平台上(例如机器人和嵌入式设备)。
现在这让我想知道,C语言通常是一种非常可移植的语言。那么,C语言的哪些部分(特别是标准库中的部分)是最不可移植的?哪些部分可以预期在大多数平台上工作?是否只应使用有限的数据类型(例如避免使用short和可能的float)?文件和stdio系统怎么样?malloc和free? Lua似乎避免了所有这些。这是把事情带到了极端吗?还是它们是可移植性问题的根源?除此之外,还有什么其他方法可以使代码具有极高的可移植性?
我之所以问所有这些问题,是因为我正在用纯C89编写一个应用程序,而且它尽可能地需要具有可移植性。我愿意在实现过程中采取一定的中间路线(足够可移植,但不至于让我不得不从头开始编写)。无论如何,我只是想看看编写最佳C代码的关键是什么。
最后要注意的是,所有这些讨论都与C89有关。

4
如果你所指的采访是在《编程大师》中,他们的意思是Lua库是独立的。没有任何一个完整的应用程序可以提供这种可移植性;它要么必须与操作系统交互(在某种程度上可以实现可移植性),要么必须直接与硬件交互(这是不可能的)。 - Fred Foo
6个回答

14

在 Lua 中,我们对 C 语言本身没有太多抱怨,但我们发现 C 标准库包含许多函数,这些函数看起来无害且易于使用,直到您考虑到它们不会检查其输入的有效性(如果不方便就没事了)。C 标准规定处理错误输入是未定义行为,允许这些函数做任何它们想做的事情,甚至崩溃主机程序。例如,考虑 strftime。某些 libc 只是忽略无效的格式说明符,但其他 libc(如 Windows 中的 libc)则会崩溃!现在,strftime 不是一个关键函数。为什么要崩溃而不是做一些明智的事情呢?因此,在调用 strftime 之前,Lua 必须进行自己的输入验证,并导出 strftime 到 Lua 程序变得非常麻烦。因此,我们试图通过针对核心进行 freestanding 来避免在 Lua 核心中出现这些问题。但 Lua 标准库不能这样做,因为它们的目标是向 Lua 程序导出功能,包括 C 标准库中可用的内容。


9
在C语言中,“自立式”有特定的含义。大致上,自立式主机不需要提供任何标准库,包括库函数malloc/freeprintf等。某些标准头文件仍然是必需的,但它们只定义类型和宏(例如stddef.h)。

6
C89允许两种类型的编译器:主机编译器和独立编译器。基本区别在于,主机编译器提供所有C89库,而独立编译器只需要提供、、和。如果您限制自己使用这些头文件,您的代码将可以在任何C89编译器上移植。

但代码将会做什么可能仍是实现定义的。 - Fred Foo
2
@larsmans:这几乎相当于说“代码可能有 bug”。避免实现定义行为并不那么难,特别是如果你的目标是 C 的 freestanding 子集。 - caf

5

这是一个非常广泛的问题。我不会给出确切的答案,而是提出一些问题。

注意,C标准将某些内容指定为“实现定义”;符合规范的程序在任何符合规范的平台上都可以编译和运行,但其行为可能因平台而异。具体来说,有以下内容:

  • 字长sizeof(long)在一个平台上可能是四个字节,在另一个平台上可能是八个字节。 shortintlong等的大小各自有一些最小值(通常相对于其他类型),但否则没有保证。
  • 字节序int a = 0xff00; int b = ((char *)&a)[0]; 在一个平台上可能将b赋值为0,在另一个平台上可能赋值为-1
  • 字符编码\0始终是空字节,但其他字符的显示方式取决于操作系统和其他因素。
  • 文本模式I/Oputchar('\n')在一个平台上可能会产生一个换行符,在下一个平台上可能会产生回车符,在另一个平台上可能会产生两者的组合。
  • char的有符号性char是否能够取负值可能与平台有关。
  • 字节大小。虽然现在几乎所有地方的字节大小都是8位,但C仍然适用于少数不常见的平台。

各种字长和字节序都很常见。 字符编码问题可能出现在任何文本处理应用程序中。具有9位字节的计算机最有可能出现在博物馆中。这绝不是一个详尽无遗的列表。

(请不要写C89,那是过时的标准。C99添加了一些相当有用的内容,例如固定宽度的整数int32_t等,用于可移植性。)


1
C99添加的大部分有用功能只需通过更新头文件和可能添加一些库函数即可轻松附加到传统的C89编译器上,因此真的没有理由避免使用它。另一方面,一些C99功能(如VLA)需要编译器支持,但它们并不是非常有用。 - R.. GitHub STOP HELPING ICE
@R..:我并不是说 C99 的所有内容都对 OP 的目的有用。另外,请告诉我如何以可移植的方式定义 int32_t。(或者如何将 snprintf 定义为宏。) - Fred Foo
1
我同意你的观点。我从未说过你可以可移植地实现int32_t(尽管假设在标准整数类型中存在32位类型,使用#iflimits.h相当容易),也没有说过你可以将snprintf作为宏来实现。我只是说,你可以(不一定是可移植的)在不修改编译器的情况下添加它。 - R.. GitHub STOP HELPING ICE

3

C语言的设计使得编译器可以生成适用于任何平台的代码,并称其所编译的语言为“C”。这种自由性与C语言作为编写可用于任何平台代码的语言相矛盾。

任何编写C语言代码的人都必须(无论是故意还是默认)决定要支持什么大小的int;虽然可以编写适用于任何合法int大小的C代码,但这需要大量的努力,而且结果代码通常比设计针对特定整数大小的代码难以阅读得多。例如,如果有一个类型为uint32_t的变量x,并且希望将其乘以另一个y,计算结果模4294967296,则语句x*=y;会在int为32位或更小的平台上工作,在int为65位或更大的平台上也能工作,但在int为33到64位之间时将引发“未定义行为”,并且如果将操作数视为整数而不是代数环的成员,那么乘积将超过INT_MAX。可以通过将其重写为x*=1u*y;来使该语句独立于int的大小,但这样做会使代码变得不够清晰,并且有意或无意地省略一个乘法中的1u*可能会带来灾难性后果。

根据现行规定,如果代码仅在整数大小符合预期的计算机上使用,则C语言具有相当的可移植性。如果在整数大小不符合预期的计算机上使用代码,则除非包括足够多的类型强制转换以使大多数语言的类型规则变得无关紧要,否则代码很难移植。


2

符合C89标准的任何内容都应该可以在遵循该标准的编译器上移植。如果您坚持使用纯C89,那么移植应该相当容易。任何可移植性问题都应该归因于编译器错误或代码调用特定于实现的行为的地方。


1
我认为关键在于,C89存在一些比较黑暗的角落,不同的编译器对其解释有所不同。 - Alexander Gladysh
1
提高可移植性的另一种方法是使用像gcc这样的便携式编译器构建应用程序。这样,任何“特定于实现”的行为在不同平台上应该是相同的。 - bta
2
这是无稽之谈。GCC会根据平台分配不同大小的整数。 - Fred Foo
@larsmans- 这是真的,变量大小取决于平台和编译器。我主要是指亚历山大·格拉迪什(Alexander Gladysh)上面提到的“更深的角落”。 - bta
2
也许“monsense”这个词用得太强了,但是仅有一个“便携式”编译器并不能为OP带来太多好处;在MinGW上的GCC与Linux、FreeBSD或OpenVMS上的GCC提供了不同的库和头文件集合。 - Fred Foo

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