我想了解以下代码是否(总是,有时或从不)根据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,否则它没有?)
答案 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”
...如果函数参数被描述为数组,实际传递给函数的指针应具有一个值,使得所有地址计算和访问对象(如果指针指向这样一个数组的第一个元素则有效)实际上是有效的....
如果“(如果指针确实指向这样一个数组的第一个元素就有效)”只适用于“访问对象”,那么strncpy(d, s, 0)
可以作为指针值不需要数组特征。它只需要是一个有效的可计算值。
如果“(如果指针确实指向此类数组的第一个元素则有效)”也适用于“地址计算”,那么strncpy(d, s, 0)
是UB,因为指针值需要具有数组特征。其中包括一次通过s
的有效地址计算。然而,当s
本身是一个通过的值时,一次通过的有效计算地址是不确定的。
当我阅读规范时,第一个适用,因此定义的行为有两个原因。 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}的定义但是在标准中出现这样的短语的地方)。也就是说,这些单词别无选择,只能表示数组对象的一部分,它从指针实际指向的元素开始。当指针指向数组末尾时,相关部分的大小为零。
一旦建立了这个关键事实,剩下的就很容易了。没有禁止数组对象的零大小部分(没有理由将它们单独出来)。当命令标准函数遍历这样的部分时,不会发生任何事情,因为它不包含任何元素。
我们是否允许采用此解释超出了此答案的范围。