【问题标题】:Casting a pointer does not produce an lvalue. Why?转换指针不会产生左值。为什么?
【发布时间】:2011-11-18 18:38:22
【问题描述】:

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

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

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

【问题讨论】:

  • 我认为,“任何两个指针的大小相同”已经是错误的。不确定,但我认为这实际上取决于架构。见stackoverflow.com/questions/1241205/…
  • 标准 C 保证指向对象的指针大小相同,但允许指向函数的指针大小不同(彼此之间以及指向对象的指针)。 POSIX 要求指向对象的指针与指向函数的指针大小相同。
  • @Jonathan:在n1256.pdf 中阅读 6.2.5/27:“...指向其他类型的指针不需要具有相同的表示或对齐要求。”
  • 我说错了。 ISO 9899:1999 §6.2.5¶26 说:指向 void 的指针应具有与指向字符类型的指针相同的表示和对齐要求。类似地,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。所有指向结构类型的指针都应具有彼此相同的表示和对齐要求。所有指向联合类型的指针都应具有彼此相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。
  • FWIW,POSIX 2008 标准说(系统接口,§2.12.3 指针类型): 所有函数指针类型都应具有与指向 void 的类型指针相同的表示。将函数指针转换为void * 不应改变表示。通过这种转换产生的void * 值可以使用显式转换转换回原始函数指针类型,而不会丢失信息。注意:ISO C 标准不要求这样做,但符合 POSIX 要求。

标签: c pointers casting lvalue


【解决方案1】:

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

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

为了使它们成为左值,标准必须添加一些特殊情况,即某些操作在左值上使用时会导致对旧对象的引用,而不是新值。 AFAIK 对这些特殊情况的需求并不大。

【讨论】:

  • 先生,我不明白一元加 (+a) 是如何产生右值的,如果它产生右值,那么这个 (+a)++ 应该有 lvalue required 错误;但它正在工作.....请解释一下thnks
【解决方案2】:

强制转换的结果本身绝不是左值。但是*((type_t *) x) 是一个左值。

【讨论】:

  • 但是((type *)x) 不是左值,所以*((type *)x)++ 失败。
【解决方案3】:

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

【讨论】:

  • 这是最好的答案。期望(type *)foo 是左值的人通常对C 有误解,并认为强制转换是对位的重新解释。这是错误的。转换是对值的操作,因此,即使“在转换发生之前”,左值也“消失”了,因为转换的输入操作数是表达式的值(有时称为右值),而不是左值.
  • @R..,“值或表达式”和右值并不意味着同样的事情,因为表达式也可以计算为左值。
  • 属于,不是或。在短语“表达式的值”中,关键字是。请参阅 6.3.2.1 左值、数组和函数指示符中的脚注。
【解决方案4】:

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

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

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

最后,您似乎在这里提倡的是,您的原始转换应该将一种指针类型重新解释为另一种指针类型的原始内存重新解释。在几乎所有情况下,原始内存重新解释都是一种黑客行为。为什么这个 hack 应该被提升到语言功能的级别,我完全不清楚。

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

【讨论】:

  • 从提问者在问题中引用的代码来看,他真正想要的是将p = (int*)p + 1,其中p 的类型为void*,缩写为((int*)p)++。我不清楚这是试图争辩说标准应该允许这样做,还是只是为了纠正导致提问者认为后者可行的误解......
【解决方案5】:

其实你是对的,也是错的。

在 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 **)&amp;ptr)++,而不是错误的*((int *)ptr)++。这也确保了这个表达式中的ptr 必须已经是一个左值。或者在 C 预处理器的帮助下编写:

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

所以对于任何传入的void *ptr(可能伪装成char *ptr),我们可以这样写:

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

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

【讨论】:

    【解决方案6】:

    编写 C 标准是为了支持奇异的机器架构,这些架构需要奇怪的 hack 来为所有指向的类型实现 C 指针模型。为了允许编译器对每个指向的类型使用最有效的指针表示,C 标准不要求不同的指针表示是兼容的。在这样一个奇特的架构上,void 指针类型必须使用最通用的,因此也是最慢的不同表示形式。 C 常见问题解答中有一些此类现已过时架构的具体示例:http://c-faq.com/null/machexamp.html

    【讨论】:

      【解决方案7】:

      请注意,如果x 是指针类型,则*(type_t **)&amp;x 是左值。但是,除非在非常有限的情况下,否则访问它会调用由于别名违规而导致的未定义行为。 可能唯一合法的情况是指针类型是对应的有符号/无符号或 void/char 指针类型,但即便如此我也很怀疑。

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

      【讨论】:

        【解决方案8】:

        从顶层看,它通常没有任何作用。假设 x 在您的示例中是一个指针,而不是 '((type_t *) x) = ',不如继续执行 'x = '。如果希望直接修改地址“x”指向的值,但同时将其解释为指向新数据类型的指针,那么 *((type_t **) &x) = 是前进的方向。同样 ((type_t **) &x) = 将毫无用处,更不用说它不是有效的左值这一事实了。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-02-09
          • 2018-05-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多