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



我想了解以下代码是否(总是,有时或从不)根据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 函数将不超过 n 个字符(不复制空字符后面的字符)从 s2 指向的数组复制到 s1 指向的数组。

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

7.24.1(字符串函数约定)在这里适用(强调我的):

声明为 size_t n 的参数指定函数的数组长度,n 在调用该函数时的值可以为零。除非在本子句中对特定函数的描述中另有明确说明,否则此类调用上的指针参数仍应具有有效值,如 7.1.4 中所述。在此类调用中,定位字符的函数未发现任何匹配项,比较两个字符序列的函数返回零,复制字符的函数复制零 字符。

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

7.1.4 库函数的使用

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

我在解析最后一部分时遇到了一些麻烦。"所有地址计算和对象访问"似乎在n == 0时得到微不足道的满足,如果我可以假设我的实现在这种情况下不会计算任何地址。

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

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

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

strncpy()的细节不足以回答"strncpy(d, s, 0)与过去指针"。 当然,访问*s2是不需要的,但是访问*s2是否需要对n==0有效?

7.24.1(字符串函数约定)也没有。

7.1.4 库函数的使用确实有答案,这取决于()部分是部分还是全部适用于之前的"这个和那个">

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

如果">
  1. (如果指针确实指向此类数组的第一个元素,则有效)"仅适用于"访问对象",则strncpy(d, s, 0)很好,因为指针值不需要具有数组特征。 它只需要是一个有效的可计算值。

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

当我阅读规范时,第一个适用,因此定义了行为有两个原因。 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的措辞密切相关

p + 4

计算的地址不是无效值。 明确允许指向数组的末尾 (C11 6.5.6/8),并且通常使用此类指针作为函数参数。 因此,代码是正确的。

根据以下文本,您怀疑存在问题:

如果函数

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

对于使用长度参数0调用strncpy,指定不复制任何字符,因此无法访问对象。它可能涉及向指针添加0,但将0添加到过去结束的指针是明确定义的。

一些评论者挂在"这种数组的第一个元素"上。 你不能用 C 声明一个零大小的数组,尽管你可以创建一个(例如 允许malloc(0)返回不是无效指针的非空指针)。 我认为将上述引用的文本视为旨在包含过去结束的指针是明智的。

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

因此,如果我们想生成一个至少稍微有用的 C 实现,我们必须采用对"指向的数组"的另一种解释(不仅在strncpy的定义中,而且在出现此类短语的标准中无处不在)。 也就是说,这些词别无选择,只能表示数组对象的一部分,该数组对象从指针实际指向的元素开始。当指针指向数组的末尾时,相关部分的大小为零。

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

我们是否被允许采用这种解释超出了本答案的范围。

相关内容

最新更新