我有一个用C实现的little VM for a programming language。它支持在32位和64位架构以及C和C ++下编译。
我正在尝试使用尽可能多的警告来干净地编译它。当我打开CLANG_WARN_IMPLICIT_SIGN_CONVERSION
时,我得到一连串的新警告。
我想有一个很好的策略,什么时候使用int
与显式无符号类型和/或明确大小的类型。到目前为止,我无法确定该策略应该是什么。
将它们混合使用 - 主要是使用int
来处理局部变量和参数以及对结构体中的字段使用较窄的类型 - 确实会导致大量的隐式转换问题。
我喜欢为结构字段使用更具体的大小类型,因为我喜欢显式控制堆中对象的内存使用量的想法。此外,对于散列表,我在散列时依赖于无符号溢出,因此如果散列表的大小存储为uint32_t
,那就太好了。
但是,如果我尝试在任何地方使用更具体的类型,我发现自己到处都是曲折的迷宫。
其他C项目有什么作用?
在任何地方使用int
可能看起来很诱人,因为它最大限度地减少了对铸造的需求,但是你应该注意几个潜在的陷阱:
int
可能比你想象的要短。即使在大多数桌面平台上,int
通常是32位,the C standard only guarantees a minimum length of 16 bits。您的代码是否需要大于216-1 = 32,767的数字,即使是临时值?如果是这样,请不要使用int
。 (您可能希望使用long
;保证long
至少为32位。)long
也可能不够长。特别是,不能保证数组(或字符串,即char
数组)的长度适合long
。使用size_t
(或ptrdiff_t
,如果你需要签名的差异)。
特别是,a size_t
is defined to be large enough to hold any valid array index,而int
甚至long
可能不是。因此,例如,当迭代一个数组时,你的循环计数器(及其初始值/最终值)通常应该是一个size_t
,至少除非你确定该数组足够短以使较小的类型能够工作。 (但是当向后迭代时要小心:size_t
是无符号的,所以for(size_t i = n-1; i >= 0; i--)
是一个无限循环!使用i != SIZE_MAX
或i != (size_t) -1
应该可以工作;或者使用do
/ while
循环,但要注意n == 0
的情况!)int
。特别是,这意味着int
overflow is undefined behavior.如果您的价值可能合法地溢出的风险,请不要使用int
;使用unsigned int
(或unsigned long
,或uintNN_t
)代替。所有这一切,也有理由避免一直使用固定长度类型:不仅int32_t
一直打字很难,但强制编译器总是使用32位整数并不总是最优,特别是在平台上本地int
大小可能是64位。你可以使用,例如,C99 int_fast32_t
,但这更难以打字。
因此,以下是我个人对最大安全性和便携性的建议:
#include <limits.h>
typedef int i16;
typedef unsigned int u16;
#if UINT_MAX >= 4294967295U
typedef int i32;
typedef unsigned int u32;
#else
typedef long i32;
typedef unsigned long i32;
#endif
将这些类型用于任何类型的确切大小无关紧要的任何类型,只要它们足够大。我建议的类型名称既简短又自我记录,因此它们应该在需要时易于使用,并最大限度地减少因使用过窄类型而导致错误的风险。
方便地,如上定义的u32
和u16
类型保证至少与unsigned int
一样宽,因此可以安全使用而不必担心它们是promoted to int
and causing undefined overflow behavior.size_t
,但在它和任何其他整数类型之间进行转换时要小心。或者,如果您不想键入这么多下划线,typedef
也是一个更方便的别名。uintNN_t
,或者只使用上面定义的u16
/ u32
和使用&
的显式位掩码。如果您选择使用uintNN_t
,请务必保护自己免受意外晋升到int
;一种方法是使用像:
#define u(x) (0U + (x))
应该让你安全地写下:
uint32_t a = foo(), b = bar();
uint32_t c = u(a) * u(b); /* this is always unsigned multiply */
typedef int32_t fooint32; /* foo ABI needs 32-bit ints */
同样,这种类型的名称是自我记录的,关于它的大小和目的。
如果ABI实际上可能需要16位或64位in而不是取决于平台和/或编译时选项,您可以更改类型定义以匹配(并将类型重命名为fooint
) - 但是你真的需要小心,无论何时向该类型转换或从该类型转换,因为它可能会意外溢出。uintNN_t
,但你会失去一些自我文档。_MIN
和_MAX
常量以便于边界检查。这可能听起来像很多工作,但它只是在单个头文件中的几行。最后,记住要小心整数数学,特别是溢出。例如,请记住,两个n位有符号整数的差异可能不适合n位int。 (它将适合n位unsigned int,如果你知道它是非负的;但是请记住,你需要将输入强制转换为无符号类型,然后才能避免未定义的行为!)同样,要找到平均值两个整数(例如用于二分搜索),不要使用avg = (lo + hi) / 2
,而是使用例如avg = lo + (hi + 0U - lo) / 2
;如果总和溢出,前者将会破裂。
你似乎知道你在做什么,从链接的源代码判断,我瞥了一眼。
你自己说过 - 使用“特定”类型会让你有更多的演员阵容。无论如何,这不是一条最佳路线。对于那些不要求更专业的类型的东西,尽可能多地使用int
。
int
的美妙之处在于它被抽象出你所说的类型。在所有需要不将构造暴露给不知道int
的系统的情况下,它是最佳的。它是您自己的工具,用于为您的程序抽象平台。它也可能会产生速度,尺寸和对齐优势。
在所有其他情况下,例如如果你想故意保持接近机器规格,int
可以,有时应该放弃。典型案例包括数据在网络上传输的网络协议,以及互操作性设施 - C和其他语言之间的桥梁,访问C结构的内核组装例程。但是不要忘记,有时你甚至会想要在这些情况下使用int
,因为它遵循平台自己的“原生”或首选字大小,你可能想要依赖那个属性。
对于像uint32_t
这样的平台类型,内核可能希望在其数据结构中使用这些(尽管它可能没有),如果这些是从C和汇编程序访问的,因为后者通常不知道int
应该是什么。
总而言之,尽可能使用int
并在任何可能需要的情况下从更抽象的类型转换为“机器”类型(字节/八位字节,单词等)。
至于size_t
和其他“用法提示”类型 - 只要语法遵循该类型固有的语义 - 比如说,使用size_t
以及所有类型的大小值 - 我不会参加竞争。但我不会自由地将它应用于任何东西只是因为它保证是最大的类型(无论它是否真的如此)。这是一块水下石头,你不想再踩到它。代码必须在可能的程度上自我解释,我会说 - 有一个size_t
,其中没有一个是自然期望的,会引起人们的注意,这是有充分理由的。使用size_t
尺寸。使用offset_t
进行偏移。使用[u]intN_t
作为八位字节,单词和其他东西。等等。
这是关于将特定C类型中固有的语义应用于源代码,以及对正在运行的程序的影响。
此外,正如其他人所说,不要回避typedef
,因为它赋予你有效定义自己类型的能力,这是我个人所重视的抽象设施。一个好的程序源代码甚至可能不暴露单个int
,但依赖于int
在众多目的定义类型后面别名。我不会在这里报道typedef
,其他答案希望如此。
保留用于访问数组成员的大数字,或控制缓冲区为size_t
。
有关使用size_t
的项目示例,请参阅GNU's dd.c, line 155。
这是我做的一些事情。不确定他们是否适合所有人,但他们为我工作。
int
或unsigned int
。似乎总是有一个更恰当的命名类型的工作。uint32_t
)。uint_fast16_t
),选择基于类型的类型访问所有数组元素所需的最小大小。例如,如果我有一个for
循环将迭代24个元素max,我将使用uint_fast8_t
并让编译器(或stdint.h,取决于我们想要得到多少迂腐)决定哪个是该操作的最快类型。如果您不同意这些或推荐的替代品,请在评论中告诉我们!这就是软件开发人员的生活......我们不断学习或变得无关紧要。
总是。
除非您有特定原因要使用更具体的类型,包括您使用的是16位平台并且需要大于32767的整数,否则您需要确保通过网络或文件进行数据交换的正确字节顺序和标志(除非您受资源限制,否则请考虑以“纯文本”传输数据,如果您愿意,则表示ASCII或UTF8。
我的经验表明,“只使用'int'”是一个很好的格言,可以让每次都能快速地生成工作,易于维护,正确的代码。但是您的具体情况可能有所不同,因此请将此建议与一些当之无愧的审查。