C11中是否定义了memcpy(&a + 1, &b + 1, 0)?

32

这个问题是关于memcpy(0, 0, 0)定义性的,它已经被确定为未定义行为,这与先前的问题有关。

正如链接的问题所示,答案取决于C11条款7.1.4:1的内容:

除非在随后的详细说明中明确说明,否则将适用以下每个语句:如果函数参数具有无效值(如函数域外的值或程序地址空间外的指针或空指针[...]),则行为未定义。[...]

标准函数memcpy()期望指向voidconst void的指针,如下所示:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

这个问题的提出是有意义的,因为标准中存在两种“有效”指针的概念:一种是可以通过指针算术运算得到并可以与同一对象内的其他指针进行有效比较的指针;另一种是用于解引用的指针。前者包括“过界”指针,例如以下代码片段中的&a + 1&b + 1,而后者不包括这些指针作为有效指针。
char a;
const char b = '7';
memcpy(&a + 1, &b + 1, 0);

考虑到memcpy()的参数被定义为指向void的指针,因此它们的有效性问题不能涉及对它们进行解引用操作。那么上述代码片段应该被视为已定义行为吗?或者&a + 1&b + 1应该被视为“程序地址空间之外”?

这对我很重要,因为我正在规范标准C函数的影响。我曾将memcpy()的一个前置条件写成requires \valid(s1+(0 .. n-1));,直到有人指出我的注意,即GCC 4.9已经开始积极地优化此类库函数调用,超出了上述公式所表达的范围 (确实如此)。在这种特定的规范语言中,公式\valid(s1+(0 .. n-1))等同于true,当n0时,并且不能捕捉到GCC 4.9依赖于优化的未定义行为。


你的例子(我相信有人会用适当的“之前提问过”的链接标记它)会引发未定义的行为,因为你正在访问程序未分配或定义的内存。 - JohnH
4
我相信这并不是未定义行为(UB),因为表达式&a + 1&b + 1在不被解引用的情况下是有效的(C11 sec 6.5.6)。但是,如果将memcpy(0, 0, 0)视为UB,则这也将是UB。 - Drew McGowen
虽然我不是专家,但我理解的“one past rule”规则是这样的指针只有在更广泛的表达式上下文中才应被视为有效,该表达式以某种方式回指到基础对象(即“足够聪明的编译器”总是可以将其优化或识别为它们所代表的内容)。但这并不是这种情况的上下文。 - Alex Celeste
1
@Leushenko:在某些不涉及基础对象的情况下,“超出末尾”的指针是完全有效的。例如:int i = 42; int *p = &i + 1; int *q = p;。在q的初始化器中引用p是有效且无害的,只要pq都没有被解引用即可。 - Keith Thompson
@Leushenko:如果使用memcpy函数时长度为零,那么它可以对指针进行的任何操作都是“授权”的吗?即使长度为零,传递null作为srcdest也是被禁止的,因为合法的memcpy允许使用自上而下的复制操作,这又需要计算src+lengthdest+length。如果srcdest为空,则此类计算将是未定义行为,但如果srcdest是“one-past”指针且length为零,则是合法的。 - supercat
显示剩余5条评论
2个回答

18

C11规定:

(C11, 7.24.2.1p2) "memcpy函数将从由s2指向的对象中复制n个字符到由s1指向的对象中。"

&a + 1本身是整数加法的有效指针,但&a + 1不是一个对象指针,因此该调用会引发未定义行为。


6
参见C11 7.24.1p2:“……在调用该函数时,**n** 的值可以为零。除非特定函数的描述在本子句中明确说明,否则在这种调用中的指针参数仍然必须具有有效的值,如7.1.4所述。”(强调添加)。如果您愿意,可以将此内容添加到您的答案中。 - Keith Thompson
2
我并不认同。这里的指针并非无效值,因此导致 memcpy(0, 0, 0) 的推理并不适用。引用的文本并未说 s2 必须指向一个对象;“由 s2 指向”的是一个谓词,用于描述从哪里取出字符,但如果没有取出任何字符,则不存在不包含值的内存读取。 - M.M
1
也许有人应该提交DR;我们正在讨论这个问题的事实表明标准不够清晰。 - M.M
2
@MattMcNabb 我之前想提交一个与C标准无关的问题的DR(惊喜,惊喜),但不清楚是否需要成为某种国家组织的付费会员才能这样做。我想我会将我的DR作为SO问题提交,并希望有合适权限的人能够注意到它们。 - Pascal Cuoq
1
你如何理解这句话中的"s2指向一个对象"?在这里,memcpy没有从任何对象中复制任何字节。 - tmyklebu
显示剩余7条评论

5

尽管根据标准来看,"正确"的答案似乎与此不同,但我发现在执行 int a[6]; int b[6]; 之后

memcpy(a+0, b+0, 6);
memcpy(a+1, b+1, 5);
memcpy(a+2, b+2, 4);
memcpy(a+3, b+3, 3);
memcpy(a+4, b+4, 2);
memcpy(a+5, b+5, 1);

应该是有效的(并复制以数组结尾结束的区域),同时
memcpy(a+6, b+6, 0);

在计数方面是有效的,但在地址方面是无效的。这是复制区域的相同结尾!

个人而言,我更倾向于将memcpy(0,0,0)定义为有效(理由是要求有效指针但不需要对象),但至少这是一个特例,而“数组末尾”情况是对在数组末尾复制区域的常规模式的实际异常。


a+6 可能导致 UB 的原因是函数期望“有效”的指向内存的指针,并且在检查 n==0 之前可能对它们进行指针运算。当加上 +1 时,指针应该被允许具有有效的结果。已知 &a[0]&a[5] 是指向内存的有效指针,每个指针都可以通过加上 +1 获得用于指针运算的有效指针。a+6 的结果是用于指针运算的有效指针,但是 a+6+1memcpy() 可能会这样做)似乎推动了事情太远。虽然听起来很傻,memcpy() 需要 进行那个计算,但必须允许。为您的想法加上 +1。 - chux - Reinstate Monica
处理限制情况非常重要。 - Pete Becker
@chux: a+6+1 不好,但 a+1+6 也不好,这并不使得本回答的第二行无效。memcpy 不应该计算超出指定范围的指针。 - Ben Voigt
@Ben Voigt 百分之百同意 memcpy(s1=a+6,..., n=0) 不应该需要计算 s1+1。但是,memcpy() 期望接收 s1 作为指向对象的有效指针 (C11dr §7.24.2.1),并且不是每个有效指向对象的指针 +1 对于指针运算也有效吗?在我看来,memcpy(any_value, any_value, 0) 根据规范是无效的,但规范应该改变以允许它。 - chux - Reinstate Monica
@chux:在某些系统上,实现void memcpy(void const *src, void *dest, size_t length) { char const*s = (char*)src; char *d = (char*)dest; char const *e=s+length; while(s!=e) {*d++ = *s++;}可能是合理的。这样的实现会导致memcpy(0,0,0)出现未定义行为,但是当给出“一过”指针时,它将非常好。 - supercat
@Ben Voigt 同意。注意:我猜你的意思是 void memcpy(void *dest, void const *src, size_t length) - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接