在C语言中,我们无法使用与该对象的有效类型具有不兼容类型的左值表达式来访问对象,因为这会导致未定义的行为。基于这一事实,严格别名规则规定如果两个指针具有不兼容的类型,则它们不能相互别名(在内存中引用相同的对象)。但是在C11标准的p6.2.4中,允许访问带有签名版本左值的无符号有效类型,反之亦然。
由于最后一段,两个指针int *a
和unsigned *b
可能会互为别名,并且其中一个指向的对象值的更改可能会导致另一个指向的对象的值发生更改(因为它是同一个对象) )。
让我们在编译器级别演示:
int f (int *a, unsigned *b)
{
*a = 1;
*b = 2;
return *a;
}
生成的上述函数的程序集在GCC 6.3.0上与-O2一样:
0000000000000000 <f>:
0: movl $0x1,(%rdi)
6: movl $0x2,(%rsi)
c: mov (%rdi),%eax
e: retq
这是非常期待的,因为GCC没有优化返回值,并且在写入*a
之后仍然再次读取值*b
(因为*b
的变化可能导致*a
的变化)。
但是有了这个其他功能:
int ga;
unsigned gb;
int *g (int **a, unsigned **b)
{
*a = &ga;
*b = &gb;
return *a;
}
生成的组件非常令人惊讶(GCC -O2):
0000000000000010 <g>:
10: lea 0x0(%rip),%rax # 17 <g+0x7>
17: lea 0x0(%rip),%rdx # 1e <g+0xe>
1e: mov %rax,(%rdi)
21: mov %rdx,(%rsi)
24: retq
返回值已优化,写入*b
后不再读取。我知道int *a
和unsigned *b
不兼容类型但是P6.2.4中的规则怎么样(允许访问带有签名版本左值的无符号有效类型,反之亦然)?为什么不适用于这种情况?为什么编译器会在这种情况下进行这种优化?
关于兼容类型和严格别名的整个故事,我有些不明白。有人可以启发我们吗? (请解释为什么两个指针有不兼容的类型,但可以互为别名,想想int *a
和unsigned *b
)。
给定int **a
和unsigned **b
,*a
的类型不是对应于有效类型*b
的有符号或无符号类型,*b
也不是对应于有效类型*a
的有符号或无符号类型。因此,此规则允许通过相应的有符号或无符号类型进行别名不适用。由于没有其他规则允许别名适用,编译器有权假设写入*b
不会修改*a
,因此编译器在*a
中写入*a = &ga;
的值仍然存在于*a
中,用于return *a;
语句。
int *
指向签名的int
这一事实并不能使其成为签名类型。这是一个指针。 int *
和unsigned *
是不同类型的指针。即使它们被认为是有符号或无符号的,它们也会是有符号或无符号的指针:如果int *
是一个带符号的指针,它将是一个指向int
的带符号的指针,相应的无符号版本将是一个指向int
的无符号指针,而不是任何指向unsigned
的指针。
要理解签名/未签名豁免的预期含义,首先必须了解这些类型的背景。 C语言最初没有“无符号”整数类型,而是设计用于二进制补码机器,溢出时安静环绕。虽然有一些操作,最值得注意的是关系运算符,除法,余数和右移,其中有符号和无符号行为会有所不同,对有符号类型执行大多数操作会产生与对无符号类型执行相同操作相同的位模式,从而最大限度地减少对后者的需求。
虽然无符号类型在安静环绕的二进制补码机器上肯定是有用的,但它们在不支持安静环绕二进制补码语义的平台上是必不可少的。但是,因为C最初并不支持这样的平台,所以很多代码在逻辑上“应该”使用了使用过的无符号类型,并且如果它们早已存在就会使用它们,而是编写使用签名类型。标准的作者不希望类型访问规则在使用签名类型的代码之间产生任何困难,因为无符号类型在编写时不可用,而代码使用无符号类型因为它们可用并且它们的使用会合理。
互换地处理int
和unsigned
的历史原因同样适用于允许使用int*
类型的左值来访问unsigned*
类型的对象,反之亦然,使用int**
等访问unsigned**
等。虽然标准没有明确规定任何此类应该允许使用它,它也忽略了显然应该允许的一些其他用途,因此不能合理地被视为完全和完整地描述实现应该支持的所有内容。
标准无法区分涉及基于指针的类型双关语的两种情况 - 涉及别名的情况,以及那些不超出非规范性脚注的情况,说明规则的目的是指示什么时候可以别名。区别如下:
int *x;
unsigned thing;
int *usesAliasingUnlessXandPDisjoint(unsigned **p)
{
if (x)
*p = &thing;
return x;
}
如果x
和*p
识别相同的存储,那么*p
和x
之间就会出现别名,因为p
的创建和*p
的写入将通过使用左值x
对存储的冲突访问来分开。但是,考虑到这样的事情:
unsigned thing;
unsigned writeUnsignedPtr(unsigned **p)
{ *p = &thing; }
int *x;
int *doesNotUseAliasing(void)
{
if (x)
writeUnsignedPtr((unsigned**)&x);
return x;
}
在*p
参数和x
之间不存在别名,因为在传递的指针p
的生命周期内,x
和任何其他不是从p
派生的其他指针或左值都用于访问与*p
相同的存储。我认为很明显标准的作者想要允许后一种模式。我认为他们是否想要允许前者即使对于signed
和unsigned
类型的左值[而不是signed*
或unsigned*
]也不太清楚,或者没有意识到将规则的应用限制在实际涉及混叠的情况下就足够了允许后者。
gcc和clang解释别名规则的方式没有扩展int
和unsigned
与int*
和unsigned*
之间的兼容性 - 这是一个允许的限制,考虑到标准的措辞,但是 - 至少在不涉及别名的情况下,我会视为违反标准的既定目的。
你的特定例子在*a
和*b
重叠的情况下确实涉及别名,因为a
是首先创建的,并且通过*b
在*a
的最后一次使用之间发生冲突访问,或者首先创建b
并且通过*a
之间发生冲突访问这样的创作和b
的最后一次使用。我不确定标准的作者是否打算允许这样使用,但是同样理由允许使用int
和unsigned
的理由同样适用于int*
和unsigned*
。另一方面,gcc和clang的行为似乎并不是由标准的作者所说的由发表的理由所表达的,而是由他们未能要求编译器所做的事情决定的。