在刚刚超过数组末尾的指针上调用长度为零的memcpy是否合法?

fuz*_*fuz 22 c pointers undefined-behavior language-lawyer

正如其他地方所解释的那样,调用memcpy无效或NULL指针等函数是未定义的行为,即使length参数为零.在这样的功能,特别是上下文memcpymemmove,是一个指针刚刚过去的阵列的有效指针的结束?

我问这个问题是因为一个指针刚好超过一个数组的末尾是合法的(相反,例如一个指针超过一个数组末尾的两个元素),但你不能取消引用它,但是脚注106 ISO 9899:2011表明这样的指针指向程序的地址空间,这是指针根据§7.1.4有效所需的标准.

这种用法发生在我希望将项目插入数组中间的代码中,要求我在插入点之后移动所有项目:

void make_space(type *array, size_t old_length, size_t index)
{
    memmove(array + index + 1, array + index, (old_length - index) * sizeof *array);
}
Run Code Online (Sandbox Code Playgroud)

如果我们想在数组的末尾插入,index则等于lengtharray + index + 1指向刚好超过数组的末尾,但复制元素的数量为零.

Pix*_*ist 9

将过去指针传递给第一个参数memmove有几个陷阱,可能导致鼻子恶魔攻击.严格地说,没有明确的保证.

(遗憾的是,标准中没有太多关于"过去最后一个元素"的信息.)

注意:抱歉现在有另一个方向......

基本问题是"一个超过结束指针"是否是一个有效的第一个函数参数,用于memmove移动0个字节:

T array[length];
memmove(array + length, array + length - 1u, 0u);
Run Code Online (Sandbox Code Playgroud)

有问题的要求是第一个论点的有效性.

N1570,7.1.4,1

如果一个函数参数被描述为一个数组,那么实际传递给该函数的指针应该具有一个值,使得所有地址计算和对象的访问(如果指针确实指向这样一个数组的第一个元素,这将是有效的)事实上是有效的.

如果函数的参数具有无效值(例如函数域外的值,或程序地址空间外的指针,或空指针,或指向不可修改存储的指针,则相应参数不具有const限定条件)或具有可变数量参数的函数不期望的类型(提升后),行为未定义.

如果指针使参数有效

  1. 不在地址空间之外,
  2. 不是空指针,
  3. 不是指向const内存的指针

如果参数类型

  1. 不是数组类型.

1.地址空间

N1570,6.5.6,8

此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素.

N1570,6.5.6,9

此外,如果表达式P指向数组对象的元素或者指向数组对象的最后一个元素,并且表达式Q指向同一数组对象的最后一个元素,则表达式((Q)+1) - (P)具有与((Q) - (P))+ 1和 - ((P) - ((Q)+1))相同的值,并且如果表达式P指向一个,则值为零数组对象的最后一个元素,即使表达式(Q)+1没有指向数组对象的元素.106

106接近指针运算的另一种方法是首先将指针转换为字符指针:在此方案中,首先将转换后的指针中添加或减去的整数表达式乘以最初指向的对象的大小,并将结果指针转换回原始类型.对于指针减法,字符指针之间差异的结果类似地除以最初指向的对象的大小.

当以这种方式查看时,实现仅需要在对象结束之后提供一个额外字节(其可以与程序中的另一个对象重叠)以满足"超过最后一个元素"的要求.

虽然脚注不是规范性的 - 正如Lundin所指出的那样 - 我们在这里有一个解释,"实现只需要提供一个额外的字节".虽然,我无法通过引用来证明我怀疑这是一个暗示,标准意味着要求实现在程序地址空间内包含内存,在过去指向的位置指向的位置.

2.空指针

过去的结束指针不是空指针.

3.指向常量内存

除了提供有关几个操作的结果的一些信息之外,该标准对过去的结束指针没有进一步的要求,并且(再次非正常;))脚注澄清它可以与另一个对象重叠.因此,不能保证过去指针指向的存储器不是恒定的.由于第一个参数memove是指向非常量内存的指针,因此不能保证传递结束指针不是有效且可能是未定义的行为.

4.数组参数的有效性

第7.21.1节描述了字符串处理头<string.h>和第一个子句:

头部声明了一个类型和几个函数,并定义了一个宏,用于操作字符类型的数组和被视为字符类型数组的其他对象.

我不认为标准在这里是否非常明确"处理为字符类型数组的对象"是指函数还是仅指宏.如果这句话实际上意味着memove将第一个参数视为一个字符数组,那么将过去的结束指针传递给memmove的行为是根据7.1.4(它需要一个指向有效对象的指针)的未定义行为.

  • @Lundin当标准文本不清楚或有缺陷时,它们可以用作作者意图的线索 (2认同)
  • 有人可能会争辩说这个额外的字节是不可写的,因为解除引用它是未定义的行为,因此写入它是未定义的行为. (2认同)

250*_*501 8

3.15对象

  1. 执行环境中数据存储的对象区域,其内容可以表示值

指向数组对象或对象的最后一个元素指向的内存,指针不能表示值,因为它无法解除引用(6.5.6附加运算符,第8段).

7.24.2.1 memcpy函数

  1. memcpy函数将s2指向的对象中的 n个字符复制到s1指向的 对象中.如果在重叠的对象之间进行复制,则行为未定义.

传递给memcpy的指针必须指向一个对象.

6.5.3.4 sizeof和_Alignof运算符

  1. 当sizeof应用于具有char,unsigned char或signed char(或其限定版本)类型的操作数时,结果为1.当应用于具有数组类型的操作数时,结果是总字节数数组.当应用于具有结构或联合类型的操作数时,结果是此类对象中总字节数,包括内部和尾部填充.

sizeof运算符不将one-past元素计为对象,因为它不计入对象的大小.然而它清楚地给出了整个物体的大小.

6.3.2.1左值,数组和函数指示符

  1. 左值是一个表达式(对象类型不是void)可能指定一个对象; 64)如果左值在评估时没有指定对象,则行为未定义.

我认为过去指向数组对象或对象的指针(两者都被允许指向)并不代表对象.

int a ;
int* p = a+1 ; 
Run Code Online (Sandbox Code Playgroud)

p已定义,但它不指向对象,因为它不能被解除引用,它指向的内存不能表示值,而sizeof不会将该内存计为对象的一部分.Memcpy需要一个指向对象的指针.

因此,将一个指针传递给memcpy会导致未定义的行为.

更新:

这部分也支持结论:

6.5.9平等运营商

  1. 两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和开头的子对象)或函数,两者都是指向同一数组的最后一个元素的指针对象,或者一个指向一个数组对象末尾的指针,另一个是指向紧跟在地址空间中第一个数组对象之后的另一个数组对象的开头的指针.

这意味着指向对象的指针如果增加到一个对象,可以指向另一个对象.在这种情况下,它当然不能指向它最初指向的对象,表明一个经过对象的指针不指向一个对象.


ala*_*ain 5

如果我们看一下C99标准,就有:

7.21.1.p2

如果声明为size_t n的参数指定了函数数组的长度,则在调用该函数时,n的值可以为零.除非在本子条款中对特定函数的描述中另有明确说明,否则此类调用上的指针参数仍应具有有效值,如7.1.4中所述.在这样的调用中,定位字符的函数不会发生,比较两个字符序列的函数返回零,复制字符的函数复制零个字符....

有一个在说明没有明确的说法memcpy7.21.2.1

7.1.4.p1

...如果函数参数被描述为一个数组,那么实际传递给函数的指针应该有一个值,使得所有 地址计算和对对象的访问(如果指针确实指向这样的第一个元素,这将是有效的一个数组)实际上是有效的.

强调补充说.似乎指针必须指向有效位置(在解除引用的意义上),并且关于指针算术允许指向结尾+ 1的段落不适用于此处.

如果参数是否memcpy为数组,则存在问题.当然它们不是声明为数组,而是

7.21.1.p1

string.h声明了一个类型和几个函数,并定义了一个宏,用于处理字符类型的数组和 被视为字符类型数组的其他对象.

memcpystring.h中.
所以,我认为memcpy 没有治疗的参数为字符数组.因为提到的宏NULL,句子的"有用......"部分清楚地适用于这些功能.

  • 短语"所有地址计算和对对象的访问"似乎与您传递指针的函数完成的地址计算和访问有关.由于不允许带有零参数的`memmove`取消引用指针(因为它移动零字节),因此有效访问是空集.上一句仍然排除了"NULL"指针.不过我可能错了这个概念. (2认同)