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

时间:2015-04-24 09:51:59

标签: c pointers language-lawyer undefined-behavior

作为answered elsewhere,调用具有无效或memcpy指针的NULL函数是未定义的行为,即使长度参数为零。在这样一个函数的上下文中,特别是memcpymemmove,是一个指针,它刚好超过了数组末尾的一个有效指针吗?

我问的是这个问题,因为一个指针刚好超过一个数组的末尾是合法的(相反,例如一个指针超过一个数组末尾的两个元素),但是你不允许取消引用它ISO 9899:2011的脚注106表明这样的指针指向程序的地址空间,这是指针根据§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 个答案:

答案 0 :(得分:9)

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

(不幸的是,没有太多关于"过去最后一个元素"标准中的conecpt的信息。)

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

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

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

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

N1570,7.1.4,1

  

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

     

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

如果指针

,则使参数有效
  1. 不在地址空间之外,
  2. 不是空指针
  3. 不是指向const内存的指针
  4. 如果参数类型

    1. 不是数组类型。
    2. 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。指向const内存

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

      4。数组参数的有效性

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

        

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

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

答案 1 :(得分:8)

  

3.15对象

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

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

  

7.24.2.1 memcpy函数

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

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

  

6.5.3.4 sizeof和_Alignof运算符

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

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

  

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

     
      
  1. 左值是一个潜在的表达式(对象类型不是void)   指定一个对象; 64)如果一个左值在评估时没有指定一个对象,那么   行为未定义。
  2.   

我认为过去指向数组对象或对象的指针都不允许指向对象。

int a ;
int* p = a+1 ; 

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

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

更新

这部分也支持结论:

  

6.5.9平等运营商

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

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

答案 2 :(得分:5)

如果我们查看C99 standard,就会有:

<强> 7.21.1.p2

  

声明为size_t n的参数指定的长度   对于函数的数组,n在调用它时可以为零   功能。除非在a的描述中另有明确说明   本子条款中的特定功能,指针参数就是这样的   如7.1.4所述,呼叫仍应具有有效值。在这样的   call,一个找到一个字符的函数找不到,a   比较两个字符序列的函数返回零,和a   复制字符的函数复制零个字符。   ...

7.21.2.1

memcpy的说明中没有明确说明

<强> 7.1.4.p1

  

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

重点补充。似乎指针必须指向有效位置(在解除引用的意义上),并且关于指针算术允许指向结束+ 1的段落在这里不适用。

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

7.21.1.p1

  

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

memcpy位于 string.h 所以我认为memcpy 将参数视为字符数组。 因为提到的宏是NULL,所以&#34;对于...&#34;非常有用。句子的一部分明确适用于这些职能。