C语言 实际上如何计算sizeof运算符

7nbnzgx9  于 2023-02-03  发布在  其他
关注(0)|答案(4)|浏览(85)

我的项目要求完全理解sizeof操作符实际上是如何工作的。在这方面,C标准规范是模糊的,依赖于我对它的解释是危险的。我特别感兴趣的是 * 何时 * 以及 * 如何 * 应该处理sizeof
1.我以前的知识表明它是一个编译时操作符,对此我从未质疑过,因为我从未过多地滥用sizeof

int size = 0;
scanf("%i", &size);
printf("%i\n", sizeof(int[size]));

例如,无论如何都不能在编译时对它求值。

char c = '\0';
char*p = &c;
printf("%i\n", sizeof(*p));

我不记得产生U/B的确切代码了,但是这里,*p是一个实际的表达式(RTL一元解引用),这是否意味着sizeof(c+c)是一种通过表达式强制编译时求值的方法,或者它将由编译器优化?

  1. sizeof是否返回int类型的值,它是size_t(在我的平台上为NULL),还是实现定义的。
    1.本文指出“sizeof的操作数不能是类型转换”,这是不正确的。类型转换与sizeof运算符具有相同的优先级,这意味着在两者都使用的情况下,它们只是从右到左求值。sizeof(int) * p可能不起作用,因为如果操作数是大括号中的类型,则首先处理它,但是sizeof((int)*p)工作得很好。
    我要求对sizeof是如何实现的进行一些技术上的详细说明,这对任何不想传播错误信息、不准确信息的人都是有用的,或者就像我的情况一样--从事一个直接依赖于它的项目。
lnlaulya

lnlaulya1#

1.我以前的知识表明它是一个编译时运算符,对此我从未质疑过,因为我从未过多地滥用sizeof...

C 20186.5.3.42指定了sizeof的行为,并指出:
...如果操作数的类型是可变长度数组类型,则计算操作数;否则,不计算操作数,结果是整数常量。
sizeof(int[size])的示例中,int[size]的类型是一个可变长度数组类型,因此操作数赋值为1,从而在程序执行期间有效地计算大小。
sizeof(*p)的示例中,*p的类型不是可变长度数组类型,因此不计算操作数。p可能指向在程序执行期间创建的自动存储持续时间的对象这一事实无关紧要;*p类型在编译期间是已知的,因此不计算*p,并且sizeof的结果是整数常量。

2. sizeof是否返回int类型的值,它是size_t(在我的平台上为NULL),还是实现定义的。

C 20186.5.3.45指出“两个运算符[sizeof_Alignof]的结果值是实现定义的,其类型(无符号整数类型)是size_t,在<stddef.h>(和其他头文件)中定义。”

3.本文声明“sizeof的操作数不能是类型转换”,这是不正确的。类型转换与sizeof运算符具有相同的优先级,这意味着在两者都使用的情况下,它们只是从右到左求值。sizeof(int) * p可能不起作用,因为如果操作数是大括号中的类型,则首先处理它,但sizeof((int)*p)工作正常。

项目表示操作数不能直接是 cast-expression(C 2018 6.5.4)形式为(type-name)cast-expression``,这是由于C的形式语法是如何构造的。形式上,sizeof的表达式操作数是 * 一元表达式 *(6.5.3),并且一个 unary-expression 可以通过一个语法产生式链成为一个括号内的 cast-expression

脚注

1我们经常认为 type-name(类型的规范,如int [size])更多的是被动声明,而不是可执行语句或表达式,但C 2018 6.8 4告诉我们“还有一个隐式完整表达式,其中对可变修改类型的非常量大小表达式求值...”

13z8s7eq

13z8s7eq2#

根据(草案)C11标准,sizeof()的语义:
sizeof运算符生成其操作数的大小(以字节为单位),操作数可以是表达式或带括号的类型名称。大小由操作数的类型确定。结果为整数。如果操作数的类型为可变长度数组类型,则计算操作数;否则,不计算操作数,结果是整数常量。
注意"如果操作数的类型是可变长度数组类型,则计算操作数"。这意味着在运行时计算VLA的大小。
"否则,不计算操作数,结果为整型常量"表示在编译时计算结果。

    • 返回类型为size_t。句号**:

两个运算符(sizeof()_Alignof())的结果值是实现定义的,其类型(无符号整数类型)是size_t,在(和其他头文件)中定义。<stddef.h> (and other headers).
注意,类型是size_t。不要使用unsigned longunsigned long long,也不要使用其他类型。始终使用size_t

fkvaft9z

fkvaft9z3#

你想得太多了。
是的,当sizeof的操作数是一个变长数组表达式时,那么它必须在运行时求值,否则,它是一个编译时操作,操作数 * 不求值 *。

printf("%i\n", sizeof(*p));

我不记得产生U/B的确切代码,但在这里,*p是一个实际的表达式(RTL一元解引用)。
没关系--表达式*p不作为sizeof操作的一部分进行计算,重要的是*ptype,它在翻译时是已知的,这是一个非常有效的动态内存分配习惯用法:

size_t size = some_value();
int *p = malloc( sizeof *p * size );

根据推测,这是否意味着sizeof(c+c)是一种通过表达式强制编译时求值的方法,或者它将由编译器优化?
同样,表达式c+c不会被求值--重要的是类型。
sizeof是否返回int类型的值,它是size_t(在我的平台上是NULL),还是实现定义的。
size_t。这在语言定义中有明确说明:

6.5.3.4 sizeof_Alignof运算符

...
5这两个运算符的结果值是由实现定义的,其类型(无符号整数类型)是size_t,定义在<stddef.h>(和其他头文件)中。* C 2011 Online Draft *
本文指出“sizeof的操作数不能是类型转换”,这是不正确的。类型转换与sizeof运算符具有相同的优先级,这意味着在两者都使用的情况下,它们只是从右到左求值。sizeof(int) * p可能不起作用,因为如果操作数是大括号中的类型,则首先处理该操作数,但sizeof((int)*p)工作正常。
这篇文章所说的是,作为 cast-expression 的操作数不能被正确地 * 解析 *。

unary-expression:
    ...
    sizeof unary-expression
    sizeof ( type-name )

cast-expression 的语法是

cast-expression:
    unary-expression
    ( type-name ) cast-expression

如果你写一个表达式

sizeof (int) *p;

它不会被 * 解析 * 为

sizeof ((int) *p);

相反,它将被解析为

(sizeof (int)) *p;

并被解释为 * 乘法表达式 *:

multiplicative-expression * cast-expression

因此,编译器会认为您试图将sizeof (int)的结果乘以p的值(这将导致诊断)。如果您将 cast-expression 括在括号中,则它将被正确解析。
类型转换的优先级与sizeof运算符相同
这是不正确的。一元表达式(包括sizeof表达式)的优先级高于强制转换表达式。这就是sizeof (int) *p被分析为(sizeof (int)) *p的原因。

xmakbtuz

xmakbtuz4#

下面是对sizeof操作符及其许多怪癖的完整指南。警告:这个帖子可能包含大量的“语言律师”。

正式语法和有效形式

sizeof是C语言中的关键字,语法在C17 6.5.3中定义为:
sizeof * 一元表达式 *
sizeof( * 类型名称 * )
这意味着有两种可能的使用方式:sizeof opsizeof(op)。在前一种情况下,操作数必须是表达式(例如sizeof my_variable),在后一种情况下,操作数必须是类型(例如sizeof(int))。
当我们使用sizeof时,我们几乎总是使用圆括号。总是使用圆括号被认为是一个好习惯(Linus Torvalds曾经有过一个著名的childish tantrums)但是我们使用sizeof的哪种形式取决于我们传递的是表达式还是类型,所以即使我们在表达式周围使用括号,我们实际上也不会使用第二种形式,而是前者。例如:

int x;
printf("%zu\n", sizeof(x));

在本例中,我们传递了一个表达式给sizeof,表达式是(x),括号是一个正则(“主表达式”)括号,我们可以在C语言中的任何表达式周围使用它--在本例中,它不属于sizeof运算符。

“sizeof的操作数不能是类型转换”-优先级和结合性还是...?

根据上面的解释,每当我们写sizeof (int) * p时,它就会被解释为第二个带有类型名的形式,为什么?
为什么这一点都不明显呢?事实上,这是非常微妙的。很容易被你链接的“运算符优先级表”所欺骗。它声明像sizeof这样的强制转换运算符是一个具有从右到左结合性的一元运算符。但当深入挖掘C语法的肮脏细节时,这实际上并不是真的。
在C标准中实际上没有优先级表这样的东西,也没有显式定义结合性,而是由操作符优先级决定(尽可能复杂)由第6. 5章中的一长串语法定义组成。在每一子章中,运算符组指的是形式语法中的前一个,有时是下一个运算符组,从而表明当前组的优先级低于前一组。对于6.5.3一元运算符,如下所示:

  • 一元表达式:*
  • 后缀表达式 *
  • ++一元表达式 *
  • --一元表达式 *
  • 一元运算符强制转换表达式 *

sizeof * 一元表达式 *
sizeof( * 类型名称 * )
_Alignof( * 类型名称 * )

  • 一元运算符:* 之一

& * + - ˜ !
从标准语翻译成英语,这个语法goo可以大致读作:
“下面是一元表达式组。它们是前缀++--运算符,或者是一元运算符之一(分别列出)、两种不同形式的sizeof_Alignof。它们可以跟在后缀表达式后面,意味着任何后缀表达式(或语法链中更高的运算符组)比一元运算符具有更高的优先级。它们后面可以跟一个强制转换表达式,从而具有比一元运算符“更低”的优先级。
因此,根据您的说法,链接中实际上存在一个细微的错误,或者他们本可以更好地解释这一点(我甚至不确定我自己是否做到了,所以我真的不怪他们)在正式的C标准之外,“右到左关联性”的概念不除非强制转换运算符在该表中被列为一元运算符的一部分,否则它不起作用,即使它实际上在语法中具有较低的优先级。
总之,sizeof(type-name)运算符是一元表达式,在语法上优先于强制转换运算符,这就是为什么编译器不会把它当作两个运算符sizeof(cast),而是当作运算符sizeof(type)后面跟着二进制乘法运算符。
所以sizeof (int) * p变成了(sizeof(int)) * p的等价物,sizeof加上二进制乘法,这可能是毫无意义的,也许这里的实际意图是解引用指针p,强制转换,然后取大小。
但是我们可以写类似sizeof ((int)*p))的东西,然后解析顺序是:括号,然后(由于一元运算符从右到左的结合性)解引用,然后强制转换,然后sizeof。

sizeof返回的类型是什么?

它返回一个特殊的大的无符号整数类型size_t(C17 6.5.3.4/5),通常被认为“足够大”以容纳系统中允许的最大对象。该类型通常在我们想要获取某个对象的大小时使用,比如在迭代数组时。
例如,当迭代数组时,你可能会看到一些代码以for(size_t i=0; i<n; i++)的形式出现在SO上,因为这是最正确的类型“large enough”来包含数组的大小。(int可能太小了,而且它也有符号,我们不能有负的大小。)
size_t可以在stddef.h中找到,而stddef.h又包含在许多其他标准头文件中,如stdio.h,它可以保存stdint.h中定义的最大值SIZE_MAX

通过使用%zu转换说明符,size_tprintf一起打印,这就是我前面的示例printf("%zu\n", sizeof(x));

    • 编译时还是运行时?**

sizeof通常是一个编译时运算符,意味着操作数不会被求值。但有一个例外,那就是变长数组(VLA),它的大小在编译时是未知的。
C17网站6.5.3.4/2:
sizeof运算符生成其操作数的大小(以字节为单位),操作数可以是表达式或带括号的类型名称。大小由操作数的类型确定。结果为整数。如果操作数的类型为可变长度数组类型,则计算操作数;否则,不计算操作数,结果是整数常量。
大多数情况下,这并不重要,然而,我们可以编造一些像这样的人工例子:

#include <stdio.h>

int main (void)
{
  int size;
  scanf("%d",&size); // enter 2
  int arr[5][size];

  printf("%zu ", sizeof(size++)); // size++ not executed
  printf("%d ", size); // print 2

  printf("%zu ", sizeof(arr[size++])); // size++ is executed
  printf("%d ", size);
}

当我试着输入2时,它会打印4 2 8 3

  • 4,因为这是此系统上int的大小。
  • 2,因为未执行/计算操作数size++
  • 因为2 * sizeof(int)是8。
  • 3,因为操作数arr[size++] * 已 * 执行/求值,因为arr[n]生成VLA操作数。

这种操作数是否被求值的行为是明确定义和保证的。
这就是int* ptr = malloc(n * sizeof *ptr);的一个常用技巧。如果*ptr被求值,它是一个未初始化的指针,我们绝对不能解引用它,这将是一个未定义的行为。但是因为它被保证不会被求值,所以这个技巧是安全的。

  • "数组衰减"的例外*

sizeof是少数几个"数组衰减"规则的例外操作数之一:
C17网站6.3.2.1/3
除非它是sizeof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串文本,否则类型为"类型数组"的表达式将转换为类型为"类型指针"的表达式,该表达式指向数组对象的初始元素,并且不是左值。

    • sizeof用于C的字节定义**

C中字节的大小根据C17 3.6定义
3.6

    • 字节**

数据存储器中的一种可寻址单元,其容量足以容纳执行环境中基本字符集的任何成员
然后是6.5.3.4/4:
sizeof应用于类型为charunsigned charsigned char(或其限定版本)的操作数时,结果为1
由于这个原因,写malloc(n * sizeof(char)这样的东西没有多大意义,因为sizeof(char)根据定义保证总是1。
(The然而,不保证char中的 * 位 * 的数目为8。

相关问题