在 ISO C90 标准中 C 语言负数比正数大?

cnblogs 2024-10-03 08:09:00 阅读 74

演示环境

    <li>OS: Arch Linux x86_64
  • Kernel: linux-6.11.1
  • GCC: 14.2.1

演示代码(main.c)

<code>int main(void)

{

return -2147483648 < 2147483647;

}

编译和链接

gcc -std=c90 -m32 main.c #

gcc 输入警告:

main.c:3:9: warning: this decimal constant is unsigned only in ISO C90

运行并查看结果

./a.out

echo $?

输出结果为:0

先看一下编译生成的汇编代码

    <li>执行命令

gcc -S -std=c90 -m32 main.c # 添加 -masm=intel 选项可以生成 intel 语法的汇编

    <li>生成的汇编代码如下(只截取了关键部分):

<code>main:

.LFB0:

.cfi_startproc

pushl%ebp

.cfi_def_cfa_offset 8

.cfi_offset 5, -8

movl%esp, %ebp

.cfi_def_cfa_register 5

call__x86.get_pc_thunk.ax

addl$_GLOBAL_OFFSET_TABLE_, %eax

movl$0, %eax # 11 行

popl%ebp

.cfi_restore 5

.cfi_def_cfa 4, 4

ret

.cfi_endproc

.LFE0:

看一下第 11 行汇编指令 movl$0, %eax,返回值 0 是 gcc 在编译阶段计算(优化)的结果(意料之中!特别是现代编译器,哪怕是在默认的优化级别下,也没理由不进行优化)。

关于 ISO C90 标准

在 ISO C90 标准中,-2147483648 是由一个负号和 2147483648 两部分组成。根据 C90 标准的规定,整数常量(不带后缀)会根据其大小自动决定是 int 类型、long int 类型还是 unsigned long int 类型。

2147483648 超出了 32 位 int 的最大范围(2147483647),因此编译器会把它当成 unsigned long int 类型。所以,-2147483648 其实是 - 和一个 unsigned long int 类型 2147483648 组成,因为这里的负号是尝试对一个无符号数取负,这将引发类型问题,这样的操作在 C 语言中是合法的,但会导致值的环绕(wrap around),最终得到一个很大的正数。

解决办法

    <li>以表达式代替 -2147483648

既然问题出在 -2147483648 的绝对值太大了,如果将 -2147483648 改为其它的表达式形式,只要表达式的值不变且表达式中的每个子表达式不超出范围不就行了吗?这里以表达式 -2147483647 - 1 进行演示:

int main(void)

{

return (-2147483647 - 1) < 2147483647;

}

重复之前的编译链接步骤,发现不但没有了警告,而且运行的结果也是对的。

    <li>使用宏(推荐)

<code>#include <limits.h>

int main(void)

{

return INT_MIN < INT_MAX;

}

我们知道宏是在预处理(预编译)阶段进行处理(替换)的,那么宏 INT_MININT_MAX 会分别替换成什么呢?

使用命令 gcc -E -std=c90 -m32 main.c 预处理的结果为(只截取了关键部分):

int main(void)

{

return (-0x7fffffff - 1) < 0x7fffffff;

}

-0x7fffffff0x7fffffff 不就分别是 -21474836472147483647 的十六进制形式吗?所以,方法 1 和 2 本质是一样的。

    <li>使用变量(不推荐)

<code>int main(void)

{

int min = -2147483648;

return min < 2147483647;

}

依然有相同的警告,但结果居然是对的?还是先看一下编译生成的汇编代码(同样只截取关键部分):

main:

.LFB0:

.cfi_startproc

pushl%ebp

.cfi_def_cfa_offset 8

.cfi_offset 5, -8

movl%esp, %ebp

.cfi_def_cfa_register 5

subl$16, %esp

call__x86.get_pc_thunk.ax

addl$_GLOBAL_OFFSET_TABLE_, %eax

movl$-2147483648, -4(%ebp)

cmpl$2147483647, -4(%ebp)

setne%al

movzbl%al, %eax

leave

.cfi_restore 5

.cfi_def_cfa 4, 4

ret

.cfi_endproc

可以看到,比较大小是在运行期间进行的,这必将影响性能。所以,不推荐这种方式。

movl $-2147483648, -4(%ebp) 的意思是将 -2147483648 压栈,其实就是放到内存中,由于 -2147483648 是负数,在内存中存放的是补码形式(也就是 0x80000000)。

cmpl $2147483647, -4(%ebp) 的意思是比较 2147483647(0x7FFFFFFF)和 0x80000000 的大小,比较的结果存放到标志寄存器的对应位中。

setne%al 的意思是如果比较的结果为不相等(明显 0x7FFFFFFF 和 0x80000000 并不相等),将 al 寄存器置 1(0x01)

movzbl 指令负责拷贝一个字节,并用 0 填充其目的操作数中的其余各位。因此,movzbl%al, %eax 指令执行后,寄存器 eax(也就是返回值)为 0x00000001



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。