strncpy(d,s,0),带有一个过去的指针

时间:2017-07-27 12:13:48

标签: c language-lawyer

我想了解以下代码是否(总是,有时或从不)根据C11明确定义:

#include <string.h>
int main() {
  char d[5];
  char s[4] = "abc";
  char *p = s;
  strncpy(d, p, 4);
  p += 4; // one-past end of "abc"
  strncpy(d+4, p, 0); // is this undefined behavior?
  return 0;
}

C11 7.24.2.4.2说:

  

strncpy函数从s2指向的数组复制不超过n个字符(空字符后面的字符不被复制)到s1指向的数组。

请注意,s2是一个数组,而不是一个字符串(因此p == s+4不存在问题时缺少空终止符。)

7.24.1(字符串函数约定)适用于此(强调我的):

  

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

上述7.1.4的相关部分是(强调我的):

  

7.1.4库函数的使用

     

除非在以下详细说明中另有明确说明,否则以下每个陈述都适用:如果函数的参数具有无效值(,例如函数域外的值,或者指针外的指针程序的地址空间,或空指针,或当相应参数不是const限定时指向不可修改存储的指针)或具有可变数量参数的函数不期望的类型(提升后) ,行为未定义。 如果一个函数参数被描述为一个数组,那么实际传递给该函数的指针应该具有一个值,使得所有地址计算和对对象的访问(如果指针确实指向这个对象的第一个元素,那么它将是有效的数组)实际上是有效的

我在解析最后一部分时遇到了一些麻烦。 &#34;所有地址都计算和访问对象&#34; n == 0如果可以假设我的实现在这种情况下不会计算任何地址,那么似乎很容易满足。

换句话说,在严格解释标准时,我是否应该一直拒绝该计划?我应该一直允许吗?或者它的正确性是否依赖于实现(即,如果实现在检查n之前计算第一个字符的地址,那么上面的代码有UB,否则它没有?)

4 个答案:

答案 0 :(得分:3)

  

char *strncpy(char * restrict s1, const char * restrict s2, size_t n);

     

strncpy函数从n指向的数组中复制不超过s2个字符(...)“C11§7.24.4.53

strncpy()的详细信息无法完全回答“带有一个过去指针的strncpy(d, s, 0)”。当然不会访问*s2,但访问*s2是否需要n==0有效?

7.24.1(字符串函数约定)

7.1.4使用库函数可以回答,具体取决于()部分是否部分或全部适用于之前的“this and that”

  

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

  1. 如果“(如果指针确实指向这样一个数组的第一个元素就有效)”只适用于“访问对象”,那么strncpy(d, s, 0)可以作为指针值不需要数组特征。它只需要是一个有效的可计算值。

  2. 如果“(如果指针确实指向此类数组的第一个元素则有效)”也适用于“地址计算”,那么strncpy(d, s, 0)是UB,因为指针值需要具有数组特征。其中包括一次通过s的有效地址计算。然而,当s本身是一个通过的值时,一次通过的有效计算地址是不确定的。

  3. 当我阅读规范时,第一个适用,因此定义的行为有两个原因。 1)从英语的角度来看,括号部分适用于第2部分,2)执行该功能不需要访问。

    第二个是可能的读数,但是一段时间。

答案 1 :(得分:2)

您突出显示的部分:

  

实际传递给函数的指针应具有一个值,使得所有地址计算和对象的访问实际上都是有效的。

清楚地表明您的代码确实无效。在讨论零size_t参数的部分中:

  

在这样的调用中,定位字符的函数不会发生,比较两个字符序列的函数返回零,复制字符的函数复制零个字符。

无法保证复制功能不会尝试访问任何内容。

因此,从“另一方面”看这个,以下strncpy()实现将符合:

char *strncpy(char *s1, const char *s2, size_t n)
{
    size_t i = 0;
    char c = *s2;

    while (i < n)
    {
        if (c) c = s2[i];
        s1[i++] = c;
    }
    return s1;
}

当然,这是愚蠢的代码,一个理智的实现将是例如只是初始化char c = 1,所以如果你在野外找到一个会对你的代码产生意外行为的C实现,我会感到惊讶。

还有一个参数支持在任何情况下允许符合条件的实现访问*s2:C中不允许使用零大小的数组。因此,如果s2应该是指向数组的指针,*s2必须有效。这与您引用的§7.1.4

的措辞密切相关

答案 2 :(得分:2)

p + 4计算的地址不是无效值。明确允许指向数组的一个结尾(C11 6.5.6 / 8),以及将这些指针用作函数参数的常见用法。所以,代码是正确的。

根据以下文字您怀疑有问题:

  

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

对于具有长度参数strncpy的{​​{1}}的调用,指定不复制任何字符,因此不存在对对象的访问。它可能涉及向指针添加0,但定义良好,可以将0添加到过去的指针。

有些评论家被挂在&#34;这个阵列的第一个元素&#34;。您不能在C中声明一个零大小的数组,尽管您可以创建一个(例如,0允许返回非空指针,该指针不是无效指针)。我认为将上述引用的文本视为包含过去的指针是明智的。

答案 3 :(得分:1)

令人惊讶的是,标准从未定义过数组是什么。它定义了数组对象是什么,但很明显strncpy的定义不可能意味着数组对象。首先是因为类型错误(指向数组对象的指针不能具有类型char*)。其次,因为通过这种解释,人们无法将字符串操纵到任何有用的程度。确实strncpy (p, s+1, n)会变得始终无效,因为s+1永远不会将指向实际的数组对象。

因此,如果我们想要生成一个至少有用的C实现,我们必须采用“指向的数组”的另一种解释(不仅仅是{{1}的定义但是在标准中出现这样的短语的地方)。也就是说,这些单词别无选择,只能表示数组对象的一部分,它从指针实际指向的元素开始。当指针指向数组末尾时,相关部分的大小为零。

一旦建立了这个关键事实,剩下的就很容易了。没有禁止数组对象的零大小部分(没有理由将它们单独出来)。当命令标准函数遍历这样的部分时,不会发生任何事情,因为它不包含任何元素。

我们是否允许采用此解释超出了此答案的范围。