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

正如在其他地方所解释的那样,即使长度参数为零,调用具有无效或NULL指针的memcpy函数也是未定义的行为。 在这样一个函数的上下文中,尤其是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); } 

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

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 ; 

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

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

更新:

这部分也支持结论:

6.5.9平等运营商

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

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

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

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

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

基本问题是,如果移动了0个字节,“一个超过结束指针”是否是memmove的有效第一个函数参数:

 T array[length]; memmove(array + length, array + length - 1u, 0u); 

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

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所指出的那样 – 我们在这里有一个解释,“实现只需要提供一个额外的字节”。 虽然,我无法通过引用来certificate我怀疑这是一个提示,标准意味着要求实现在程序地址空间内包含内存,位于过去指针指向的位置。

2.空指针

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

3.指向const内存

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

4.数组参数的有效性

第7.21.1节描述了字符串处理头 ,第一个子句说明:

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

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

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

7.21.1.p2

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

7.21.2.1中memcpy描述中没有明确的陈述

7.1.4.p1

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

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

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

7.21.1.p1

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

memcpystring.h中
所以我认为memcpy会将参数视为字符数组。 因为提到的宏是NULL ,所以句子的“有用……”部分清楚地适用于函数。