我正在努力将一些旧的K&R代码移植到ANSI C,所以我正在编写缺少的函数原型声明。很多函数定义都有寄存器存储类的参数,但我不确定寄存器存储类说明符是否可以在函数原型中省略?
无论是否使用寄存器存储类特定的声明,代码都可以正确编译(我尝试了GCC,VC++和Watcom C)。我在 ISO/ANSI C89 标准中找不到任何关于正确方法的信息 - 如果我只是将寄存器关键字放在函数定义中可以吗?
int add(register int x, register int y);
int add(register int x, register int y)
{
return x+y;
}
这也正确构建:
int add(int x, int y);
int add(register int x, register int y)
{
return x+y;
}
我想确保根据标准真正考虑寄存器存储说明符(我的目标是使用非常旧的编译器进行编译,其中此存储类说明符很重要)。两者都可以,这只是编码风格的问题,还是不?
关键规定是函数的每个声明都必须为其指定兼容的类型。 这需要兼容的返回类型,对于包含参数列表的声明,每对相应参数的返回类型都兼容。
那么问题就变成了存储类说明符是否区分类型。 他们没有,尽管标准间接地指出,在类型派生的讨论中省略了存储类说明符。 因此,存储类说明符在对象的声明中指定的属性与该对象的类型是分开的。
此外,C89特别说
参数声明的声明说明符中的存储类说明符(如果存在)将被忽略,除非声明的参数是函数定义的参数类型列表的成员之一。
(着重号后加)。 函数定义是带有函数体的声明,而不是前向声明,因此两个代码具有相同的语义。
使用和不使用寄存器存储类特定声明, 代码编译正确(我尝试了gcc,VC ++和Watcom),我无法 在 ISO/ANSI C89 标准中查找有关什么是 正确的方法,或者如果我只是将注册关键字放入是否可以 函数定义?
就个人而言,我倾向于使每个前向声明与相应函数定义中的声明相同。 如果函数定义本身是正确的,这永远不会错。
然而
register
关键字是遗物。 编译器根本没有义务尝试将register
变量实际分配给寄存器,现代编译器在决定如何将变量分配给寄存器以及无论如何生成快速代码方面比人类要好得多。 只要您正在转换旧代码,我就会借此机会删除register
关键字的所有外观。C89 已过时。 该标准的最新版本是C 2018;C 2011被广泛部署;C99(从技术上讲也是过时的)几乎无处不在。 也许您有充分的理由以 C89 为目标,但您应该强烈考虑以 C11 或 C18 为目标,或者至少以 C99 为目标。
根据 gcc 和 clang 的经验,函数参数上的register
存储类 的行为与参数上的顶级限定符相同:只有定义中的限定符(而不是以前的原型)才计数。
(至于顶级限定符,当考虑类型兼容性时,它们也会被丢弃,即void f(int);
和void f(int const);
是兼容的原型,但存储类不是类型的一部分,因此类型兼容性首先不是它们的问题)
从 C 程序员的角度来看,C 语言中register
的唯一可观察到的结果是编译器不会让你获取声明对象的地址。
当我这样做时:
void f(int A, int register B);
void f(int register A, int B)
{
/*&A;*/ //doesn't compile => A does have register storage here
&B; //compiles => B doesn't have register storage here;
//the register from the previous prototype wasn't considered
}
然后&B
编译,但&A
不编译,因此只有定义中的限定符才算数。
我认为,如果您确实需要这些register
,最好的选择是在两个地方一致地使用它(原型中的register
理论上可以修改调用的方式)。
C89标准确实这样说(§ 3.5.4.3 外部定义):
参数声明中唯一出现的存储类说明符是
register
。
因此,虽然register
作为函数参数存储类说明符是允许的,但我仍然认为是否遵守这实际上取决于函数的体系结构和调用约定。
既然你提到了Watcom和C89,我假设你的目标是x86-16。x86-16(pascal
、stdcall
和cdecl
)的典型调用约定都要求参数推送到堆栈上,而不是在寄存器中,所以我怀疑关键字实际上会修改参数在调用站点传递给函数的方式。
考虑一下,您有以下函数定义:
int __stdcall add2(register int x, register int y);
该函数根据 stdcall 的要求_add2@4
进入目标文件。@4 指示在函数返回时要从堆栈中删除多少字节。在这种情况下使用ret imm16
(返回到调用过程并从堆栈弹出 imm16 字节)指令。
然后,add2
末尾将有以下ret
:
ret 4
如果在调用站点的堆栈上没有推送 4 个字节(即因为参数实际上在寄存器中),则程序现在具有未对齐的堆栈并崩溃。
由于您正在为一个奇怪的平台使用旧的编译器,因此有时只查看编译器的功能比假设它完全符合 C 规范更重要。
这意味着您希望通过编译器运行示例的每个变体,并将编译器选项设置为生成程序集而不是可执行文件。 查看程序集,看看是否可以判断它是否以每种方式使用寄存器。 在 gcc 中,这是S
选项;例如:
gcc myfile.c -S -o myfile.s
从 C17 标准6.7.1 存储类说明符:
具有存储类的对象的标识符声明 说明符寄存器建议对对象的访问速度与 可能。这些建议的有效程度是 实现定义。
暗示编译器将尝试或不依赖于编译器来加速参数访问,但并不意味着对调用约定进行任何修改(调用端基本上没有修改)。
所以它应该存在于函数定义中,但在原型中是微不足道的。