转换指针不会产生左值。 为什么?

在这里发表我最有争议的答案之一后,我敢问几个问题,最终填补了我的知识空白。

为什么不将类型((type_t *) x)的表达式视为有效的左值,假设x本身是指针和左值,而不仅仅是某个表达式?

我知道很多人会说“标准不允许它”,但从逻辑的角度看它似乎是合理的。 标准不允许的原因是什么? 毕竟,任何两个指针都具有相同的大小,指针类型只是一个编译时抽象,指示在进行指针运算时应该应用的适当偏移量。

更好的例子,一元+产生一个右值, x+0

根本原因是所有这些东西,包括你的演员,都会创造一个新的价值。 将值转换为它已经存在的类型,同样会创建一个新值,更不用说指向不同类型的指针是否具有相同的表示forms。 在某些情况下,新值恰好等于旧值,但原则上它是一个新值,它不打算用作旧对象的引用,这就是为什么它是一个rvalue。

对于这些是左值,标准必须添加一些特殊情况,当在左值上使用某些操作时会导致对旧对象的引用,而不是新值。 AFAIK对这些特殊情况没有太大需求。

演员表演的结果本身并不是左右。 但是*((type_t *) x)是一个左值。

因为强制转换表达式通常不会产生左值。

好吧,强制执行类型转换。 通常情况下,类型转换是一种非平凡的操作,它完全改变了值的表示。 在这种情况下,任何转换的结果都不可能是左值,这应该是非常明显的。

例如,如果你有一个int i = 0; 变量,你可以将它转换为(double) i类型(double) i 。 你怎么可能期望这个转换的结果是左值? 我的意思是,它没有任何意义。 你显然希望能够做到(double) i = 3.0; …或double *p = &(double) i; 那么,在前一个例子中i的值会发生什么,考虑到double类型甚至可能与int类型的大小不同? 即使它们具有相同的尺寸,您期望会发生什么?

您对所有具有相同大小的指针的假设是不正确的。 在一般情况下的C语言中(少数例外),不同的指针类型具有不同的大小,不同的内部表示和不同的对齐要求。 即使它们保证具有相同的表示,我仍然不明白为什么指针应该与所有其他类型分开并在显式转换情况下给予特殊处理。

最后,你似乎在这里提倡的是你的原始转换应该执行原始内存重新解释一个指针类型作为另一个指针类型。 在几乎所有情况下,原始内存重新解释都是一种黑客攻击。 为什么这个hack应升级到语言function的水平对我来说完全不清楚。

由于它是一个黑客,执行这样的重新解释应该需要用户的有意识的努力。 如果你想在你的例子中执行它,你应该做*(type_t **) &x ,它确实会将x重新解释为类型为type_t *的左值。 但是通过单纯的(type_t *) x允许同样的事情将是与C语言的设计原则完全脱节的灾难。

编写C标准是为了支持exception的机器架构,这些机器架构需要奇怪的黑客来为所有指向类型实现C指针模型。 为了允许编译器对每个指向类型使用最有效的指针表示,C标准不需要不同的指针表示来兼容。 在这种奇特的体系结构中,void指针类型必须使用最一般且因此最慢的不同表示。 C FAQ中有一些现有过时体系结构的具体示例: http : //c-faq.com/null/machexamp.html

注意,如果x是指针类型, *(type_t **)&x是左值。 但是,除非在非常有限的情况下访问它,否则会因为锯齿违规而调用未定义的行为。 它唯一可能合法的是指针类型是对应的signed / unsigned还是void / char指针类型,但即便如此,我也很怀疑。

(type_t *)x不是左值,因为(T)x绝不是左值,无论类型T还是表达式x(type_t *)只是(T)的特例。

从顶层来看,它通常没有用处。 而不是’((type_t *)x)=’也可以继续做’x =’,假设x是你示例中的指针。 如果希望直接修改地址’x’指向的值,但在将其解释为指向新数据类型的指针的同时,则*((type_t **)&x)=是前进的方法。 再次((type_t **)&x)=没有任何意义,更不用说它不是一个有效的左值。

同样在((int *)x)++的情况下,至少’gcc’不会沿’lvalue’的行抱怨它可能会将它重新解释为’x =(int *)x + 1′

实际上你同时也是对的。

在C中,有能力将任何左值安全地类型转换为任何左值。 但是,语法与您的直接方法略有不同:

左值指针可以被转换为 C语言中不同类型的左值指针

 char *ptr; ptr = malloc(20); assert(ptr); *(*(int **)&ptr)++ = 5; 

由于malloc()需要满足所有对齐要求,因此这也是可接受的用途。 但是,以下是不可移植的,并且可能由于某些机器上的错误对齐而导致exception:

 char *ptr; ptr = malloc(20); assert(ptr); *ptr++ = 0; *(*(int **)&ptr)++ = 5; /* can throw an exception due to misalignment */ 

把它们加起来:

  • 如果你施放一个指针,这会导致一个右值。
  • 在指针上使用*会导致左值( *ptr可以赋值)。
  • ++ (比如*(arg)++ )需要一个左值来操作( arg必须是一个左值)

因此((int *)ptr)++失败,因为ptr是左值,但是(int *)ptr不是。 ++可以重写为((int *)ptr += 1, ptr-1) ,并且它是(int *)ptr += 1 ,由于转换而导致纯rvalue失败。


请注意,这不是语言缺点。 铸造不得产生左值。 请看以下内容:

 (double *)1 = 0; (double)ptr = 0; (double)1 = 0; (double *)ptr = 0; 

前3个不编译。 为什么有人会期望第4行编译? 编程语言不应该暴露出这种令人惊讶的行为。 更重要的是,这可能会导致程序的一些不明确的行为。 考虑:

 #ifndef DATA #define DATA double #endif #define DATA_CAST(X) ((DATA)(X)) DATA_CAST(ptr) = 3; 

这不能编译,对吧? 但是,如果你的期望持续,这突然编译cc -DDATA='double *' ! 从稳定性的角度来看,重要的是不要为某些演员表引入这样的上下文左值。

C的正确之处在于有左值或没有值,这不应取决于某些可能令人惊讶的任意上下文。


正如Jens所指出的 ,已经有一个运算符来创建左值。 它是指针解引用运算符,“一元* ”(如*ptr )。

注意*ptr可写为0[ptr]*ptr++可写为0[ptr++] 。 数组下标是左值,因此*ptr也是左值。

等等,什么? 0[ptr]必定是错误,对吗?

实际上,没有。 试试吧! 这是有效的C.以下C程序在所有方面都在Intel 32/64位上有效,因此它编译并成功运行:

 #include  #include  #include  int main() { char *ptr; ptr = malloc(20); assert(ptr); 0[(*(int **)&ptr)++] = 5; assert(ptr[-1]==0 && ptr[-2]==0 && ptr[-3]==0 && ptr[-4]==5); return 0; } 

在C中,我们可以同时拥有它。 演员,永远不会创造左撇子。 并且能够以某种方式使用强制转换,以便我们可以保持左值属性。

但是为了从铸造中获得左值,还需要两个步骤:

  • 在演员表之前,获取原始左值的地址。 由于它是一个左值,你总能得到这个地址。
  • 转换为所需类型的指针(通常所需类型也是指针,因此您有一个指向该指针的指针)。
  • 演员之后,取消引用这个额外的指针,再次给你一个左值。

因此,我们可以编写*(*(int **)&ptr)++来代替错误的*((int *)ptr)++ *(*(int **)&ptr)++ 。 这也确保了这个表达式中的ptr必须已经是一个左值。 或者在C预处理器的帮助下编写:

 #define LVALUE_CAST(TYPE,PTR) (*((TYPE *)&(PTR))) 

因此,对于任何传递的void *ptr (可能伪装成char *ptr ),我们可以写:

 *LVALUE_CAST(int *,ptr)++ = 5; 

除了通常的指针算术警告(exception程序终止或不兼容类型的未定义行为,这主要源于对齐问题),这是正确的C.