在这里发布我最有争议的答案之一后,我敢于提出几个问题,并最终填补了我的知识空白。
为什么这种表达式不被((type_t *) x)
认为是有效的左值,假设它x
本身是一个指针和一个左值,而不仅仅是一些表达式?
我知道很多人会说“标准不允许这样做”,但从逻辑的角度来看,这似乎是合理的。标准不允许的原因是什么?毕竟,任何两个指针的大小都相同,并且指针类型只是一个编译时抽象,它指示在进行指针运算时应该应用的适当偏移量。
强制转换的结果本身永远不是左值。但是*((type_t *) x)
是一个左值。
一个更好的例子,一元+
产生一个右值,就像x+0
.
根本原因是所有这些东西,包括你的演员,都创造了新的价值。将一个值转换为它已经存在的类型,同样会创建一个新值,不管指向不同类型的指针是否具有相同的表示。在某些情况下,新值恰好等于旧值,但原则上它是一个新值,它不打算用作对旧对象的引用,这就是它是右值的原因。
为了使它们成为左值,标准必须添加一些特殊情况,即某些操作在左值上使用时会导致对旧对象的引用,而不是新值。AFAIK 对这些特殊情况没有很大的需求。
因为转换表达式通常不会产生左值。
好吧,强制转换执行类型转换。通常情况下,类型转换是一个不平凡的操作,它完全改变了值的表示。在这种情况下,任何转换的结果都不可能是左值应该是非常明显的。
例如,如果您有一个int i = 0;
变量,您可以将其转换为类型double
为(double) i
. 你怎么可能期望这个转换的结果是一个左值呢?我的意思是,这没有任何意义。您显然希望能够做到(double) i = 3.0;
……或者,考虑到 type甚至可能与 type 的大小不同,前一个示例double *p = &(double) i;
中的值应该怎么办?即使它们的大小相同,您希望会发生什么?i
double
int
您对所有具有相同大小的指针的假设是不正确的。在 C 语言中,一般情况下(除了少数例外)不同的指针类型具有不同的大小、不同的内部表示和不同的对齐要求。即使保证它们具有相同的表示形式,我仍然不明白为什么指针应该与所有其他类型分开并在显式转换情况下给予特殊处理。
最后,您似乎在这里提倡的是,您的原始转换应该将一种指针类型重新解释为另一种指针类型。在几乎所有情况下,对原始内存的重新解释都是一种 hack。为什么这个 hack 应该被提升到语言功能的级别,我完全不清楚。
由于它是一种 hack,因此执行此类重新解释应该需要用户有意识的努力。如果你想在你的例子中执行它,你应该这样做*(type_t **) &x
,这确实会重新解释x
为 type 的左值type_t *
。但是,仅仅允许同样的事情发生(type_t *) x
将是一场与 C 语言设计原则完全脱节的灾难。
其实你是对的,也是错的。
在 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
可以分配给)。++
(如 in *(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 的正确之处在于,要么有左值,要么没有左值,这不应取决于某些可能令人惊讶的任意上下文。
正如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。
C 标准是为了支持奇异的机器架构而编写的,这些架构需要奇怪的 hack 来为所有指向的类型实现 C 指针模型。为了允许编译器对每个指向的类型使用最有效的指针表示,C 标准不要求不同的指针表示是兼容的。在这样一个奇特的架构上,void 指针类型必须使用最通用的,因此也是最慢的不同表示形式。在 C 常见问题解答中有一些此类现已过时架构的具体示例:http: //c-faq.com/null/machexamp.html
从最高层面来看,它通常没有任何作用。假设 x 在您的示例中是一个指针,而不是 '((type_t *) x) = ',不如继续执行 'x = '。如果希望直接修改地址“x”指向的值,但同时将其解释为指向新数据类型的指针,那么 *((type_t **) &x) = 是前进的方向。同样 ((type_t **) &x) = 将毫无用处,更不用说它不是有效的左值这一事实了。
同样在 ((int *)x)++ 的情况下,至少 'gcc' 不会按照 'lvalue' 的方式抱怨,它可能会将其重新解释为 'x = (int *)x + 1'
注意 ifx
是指针类型,*(type_t **)&x
是左值。但是,除非在非常有限的情况下,否则访问它会调用由于别名违规而导致的未定义行为。唯一可能合法的情况是指针类型是对应的有符号/无符号或 void/char 指针类型,但即便如此我也很怀疑。
(type_t *)x
不是左值,因为(T)x
无论类型T
或表达式如何,都不是左值x
。(type_t *)
只是 的一个特例(T)
。