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

时间:2011-09-16 14:50:16

标签: c pointers casting lvalue

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

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

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

8 个答案:

答案 0 :(得分:8)

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

答案 1 :(得分:7)

一个更好的例子,一元+产生一个右值,x+0也是如此。

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

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

答案 2 :(得分:3)

因为一般中的强制转换表达式不会产生左值。

答案 3 :(得分:2)

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

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

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

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

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

实际上你同时也是对的。

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

左值指针可以在C中以类似的方式转换为不同类型的左值指针

char *ptr;

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

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

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由于演员而失败,导致纯正的左值。

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

(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的正确之处在于有左值或没有值,这不会依赖于某些可能令人惊讶的任意上下文。

作为noted by Jens,已经有一个运算符来创建左值。它是指针解引用运算符,“一元*”(如*ptr中所示)。

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

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

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

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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)++。这也确保了此表达式中的ptr必须已经是左值。或者在C预处理器的帮助下编写:

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

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

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

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