(ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0])) 的类型是什么?

4

我有一个二维数组 A 和一个指针 ptr,它指向其中的某个位置。我知道如何计算行号,但是表达式的实际类型似乎不可移植:

#include <stdio.h>

int main(void) {
    int A[100][100];
    int *ptr = &A[42][24];

    printf("row number is %d\n", (ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0])));
    printf("col number is %d\n", (ptr - A[0]) % (sizeof(A[0]) / sizeof(A[0][0])));
    return 0;
}

在OS/X系统上,clang编译器会以以下方式发出警告:
warning: format specifies type 'int' but the argument has type 'unsigned long' [-Wformat]

在Linux上,gcc会显示类似的警告,但在Windows上,我得到了不同的诊断:
warning: format specifies type 'int' but the argument has type 'unsigned long long' [-Wformat]

此表达式的实际类型是什么?

是否有一种方法可以将表达式传递给printf而不需要丑陋的强制转换?

2个回答

7

两个指针的差异类型为ptrdiff_t,这是一个有符号整数类型,在<stddef.h>中定义。对于此类型的printf长度修饰符为t。两个指针的差异可以直接打印:

printf("pointer difference: %td\n", ptr - A[42]);

子数组维度的大小 (sizeof(A[0]) / sizeof(A[0][0]))size_t 类型,这是由 <stddef.h> 中定义的 sizeof 运算符的无符号结果。

此类型的 printf 长度修饰符为 z。对象的大小可以直接打印:

printf("array size in bytes: %zu\n", sizeof(A));

ptrdiff_t是C标准所要求的能够表示至少在-6553565535之间的值,而size_t必须具有至少065535的范围。

问题是:ptrdiff_t除以size_t的类型是什么?

类型由应用 6.3.1.8通常算术转换 中指定的算术转换确定:

否则(如果两个操作数都具有整数类型),则对两个操作数执行整数提升。然后将以下规则应用于提升的操作数:

  • 如果两个操作数具有相同的类型,则不需要进一步转换。 否则,如果两个操作数都具有带符号整数类型或都具有无符号整数类型,则具有较小整数转换等级的操作数转换为具有更大等级的操作数的类型。

  • 否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数的类型的等级,则具有带符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。

  • 否则,如果具有带符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有带符号整数类型的操作数的类型。

  • 否则,两个操作数都将转换为与具有带符号整数类型的操作数相对应的无符号整数类型。

根据实际用于ptrdiff_tsize_t的类型,结果的类型可能不同:

整数提升的工作方式是:如果类型int可以表示其类型的所有值,则将其转换为int,如果unsigned可以,则将其转换为unsigned,否则类型不变。

因此,如果size_t小于int,则size_t会被提升为int,一个带符号类型,并且除法将作为带符号除法执行,首先将两个操作数转换为intptrdiff_t的较大类型(在这种情况下很可能也是int)。

如果size_tunsigned int类型,而ptrdiff_tint类型,则ptrdiff_t将转换为unsigned int,并且除法将作为无符号除法执行,并具有结果类型unsigned int

相反,如果size_tunsigned int类型且ptrdiff_tlong int类型,则size_t操作数会转换为long int类型,此除法将成为带符号的除法,并且结果类型为long int
如果size_tunsigned long int类型且ptrdiff_tlong int类型(Linux和OS/X 64位),则将ptrdiff_t转换为unsigned long int,并且此除法将进行无符号除法,并且结果类型为unsigned long int
如果size_tunsigned long long int类型且ptrdiff_tlong long int类型(Windows 64位),则将ptrdiff_t转换为unsigned long long int,并且此除法将进行无符号除法,并且结果类型为unsigned long long int
更奇特的架构可能会有其他组合的size_tptrdiff_t类型,从而导致产生更多结果类型的可能性,例如long long int
因此,行计算的类型是实现定义的:它可以是带符号或无符号的,并且不同于size_tptrdiff_t
有多种方法可以为printf语句产生一致的类型和格式:
使用强制转换:
printf("row number is %d\n", (int)((ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0]))));

使用中间变量:

int row = (ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0])));
printf("row number is %d\n", row);

使用额外操作来强制转换到更大的类型(不完美,因为 size_t 可能比 unsigned long long 更大):

printf("row number is %llu\n", 0ULL + (ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0])));

请注意,printf格式中的%zu%td用于size_tptrdiff_t类型,但由于表达式的类型不一定是size_tptrdiff_t,因此不能使用。更糟糕的是,对于Windows用户来说,Microsoft C运行时库不支持这些标准长度说明符。如Jean-François Fabre所建议的那样,对于使用gcc编译C代码并生成本地Windows应用程序的MingW用户,有一个解决方法:在命令行上指定-D__USE_MINGW_ANSI_STDIO,告诉gcc使用自己的版本的printf而不是Microsoft运行时的版本。
最后需要注意的是,如果ptr不指向数组的第一行,那么表达式ptr - A[0]实际上会导致未定义的行为,如6.5.6加法运算符所述:当两个指针相减时,它们都必须指向同一个数组对象的元素,或者是数组对象的最后一个元素之后的位置;结果是两个数组元素下标之间的差。

使用-Wconversion选项,这个语句int row = (ptr - A[0]) / (sizeof(A[0]) / sizeof(A[0][0])));会被警告。 - alk
@alk:是的,如果启用了所有关于转换的警告,我不知道如何避免强制类型转换。 - chqrlie
@chqrlie,我看到我们昨天的问题启发了你,做得好。也许你可以提一下%zu在Microsoft实现的printf中不起作用。你必须启用-D__USE_MINGW_ANSI_STDIO来使用gcc内置的printf,这样就能正常工作了(我找了很久才找到)。 - Jean-François Fabre
@Jean-FrançoisFabre:确实找到了好东西!对于那些仍然使用Windows过时的C环境的人来说非常有用。我个人在26年内使用过各种Unix版本,从DOS提示符开始,八年前放弃了。所以我深表同情。回答已更新。 - chqrlie

0
"

很难说,我认为没有硬性规定适用于所有平台的类型定义。这样的表达式没有标准库typedef。

如果您使用C ++,考虑使用auto声明此类类型,使用std :: cout打印此类类型的值:依赖于编译器选择适当的重载<<。

"

确实有C++特定的解决方案来解决这个问题。我倾向于使用类似 printf() 的转换方式,<< 重载的语法很糟糕,但是可扩展且支持类型感知。 - chqrlie
1
@alk ptrdiff_t自C89以来就一直存在。 - P.P

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