C 标准是否保证缓冲区不会超过其空终止符?

Does the C standard guarantee buffers are not touched past their null terminator?

在标准库的很多字符串函数提供缓冲区的各种情况下,是否保证缓冲区不会被修改超出空终止符?例如:

char buffer[17] = "abcdefghijklmnop";
sscanf("123", "%16s", buffer);

buffer 现在需要等于 "123[=13=]efghijklmnop" 吗?

另一个例子:

char buffer[10];
fgets(buffer, 10, fp);

如果读取的行只有 3 个字符长,是否可以确定第 6 个字符与调用 fgets 之前相同?

Is buffer now required to equal "123[=18=]efghijklmnop"?

此处 buffer 仅由 123 保证以 NUL 终止的字符串组成。

是的,分配给数组 buffer 的内存不会被取消分配,但是您正在 sure/restricting 您的字符串 buffer 最多只能有 16 个字符元素您可以随时阅读它。现在取决于你是只写一个字符还是最大 buffer 可以接受的字符。

例如:

char buffer[4096] = "abc";` 

实际上做了下面的事情,

memcpy(buffer, "abc", sizeof("abc"));
memset(&buffer[sizeof("abc")], 0, sizeof(buffer)-sizeof("abc"));

标准坚持认为,如果 char 数组的任何部分被初始化,那么在服从其内存边界之前的任何时刻,它都是由它组成的。

标准没有任何保证,这就是为什么建议使用 sscanffgets 函数(关于缓冲区的大小),如您在问题中所示(与 gets 相比,使用 fgets 被认为更可取)。

但是,一些标准函数在其工作中使用空终止符,例如strlen(但我想你问的是字符串修改)

编辑:

在你的例子中

fgets(buffer, 10, fp);

保证第10个字符不变(buffer的内容和长度不会被fgets考虑)

编辑2:

此外,在使用 fgets 时请记住 '\n' 将存储在缓冲区中。例如

 "123\n[=11=]fghijklmnop"

而不是预期的

 "123[=12=]efghijklmnop"

is it guaranteed that the buffer will not be modified beyond the null terminator?

不,不能保证。

Is buffer now required to equal "123[=13=]efghijklmnop"?

是的。但这只是因为您对字符串相关函数使用了正确的参数。如果你弄乱了缓冲区长度,输入修饰符 sscanf 等等,那么你的程序就会编译。但它很可能会在运行时失败。

If the read line is only 3 characters long, can one be certain that the 6th character is the same as before fgets was called?

是的。一旦 fgets() 表示您有一个 3 个字符的输入字符串,它将输入存储在提供的缓冲区中,并且根本不关心提供的 space 的重置。

C99 draft 标准没有明确说明在这些情况下应该发生什么,但通过考虑多种变体,您可以证明它必须以某种方式工作,以便在所有情况下都符合规范。

标准说:

%s - Matches a sequence of non-white-space characters.252)

If no l length modifier is present, the corresponding argument shall be a pointer to the initial element of a character array large enough to accept the sequence and a terminating null character, which will be added automatically.

这里有两个例子表明它必须按照您提议的方式工作才能达到标准。

示例 A:

char buffer[4] = "abcd";
char buffer2[10];  // Note the this could be placed at what would be buffer+4
sscanf("123 4", "%s %s", buffer, buffer2);
// Result is buffer =  "123[=10=]"
//           buffer2 = "4[=10=]"

示例 B:

char buffer[17] = "abcdefghijklmnop";
char* buffer2 = &buffer[4];
sscanf("123 4", "%s %s", buffer, buffer2);
// Result is buffer = "123[=11=]"

请注意,sscanf 的接口没有提供足够的信息来真正知道它们是不同的。所以,如果示例 B 要正常工作,它一定不能弄乱示例 A 中空字符后的字节。这是因为根据这一点规范,它必须在两种情况下都工作。

所以隐含地它必须按照你说的那样工作,因为规范。

可以为其他函数放置类似的参数,但我认为您可以从这个例子中看出思路。

注意: 在格式中提供大小限制,例如“%16s”,可能 会改变行为。根据规范,sscanf 在将数据写入缓冲区之前将缓冲区清零到其限制在功能上是可接受的。在实践中,大多数实现都选择性能,这意味着他们不理会其余部分。

当规范的目的是进行这种归零时,通常会明确指定。 strncpy 就是一个例子。如果字符串的长度小于指定的最大缓冲区长度,它将用空字符填充 space 的其余部分。事实上,这个 "string" 函数也可以 return 一个非终止字符串,这使得它成为人们推出自己版本的最常用函数之一。

就fgets而言,可能会出现类似的情况。唯一的问题是规范明确指出,如果没有读入任何内容,缓冲区将保持不变。一个可接受的功能实现可以通过在将缓冲区清零之前检查是否至少有一个字节要读取来回避这一点。

标准在这方面有些模棱两可,但我认为对它的合理解读是答案是:是的,不允许向缓冲​​区写入比读取+空更多的字节。另一方面,更严格的 reading/interpretation 文本可以得出结论,答案是否定的,没有保证。这是 publicly avaialble draftfgets.

的评价

char *fgets(char * restrict s, int n, FILE * restrict stream);

The fgets function reads at most one less than the number of characters specified by n from the stream pointed to by stream into the array pointed to by s. No additional characters are read after a new-line character (which is retained) or after end-of-file. A null character is written immediately after the last character read into the array.

The fgets function returns s if successful. If end-of-file is encountered and no characters have been read into the array, the contents of the array remain unchanged and a null pointer is returned. If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.

关于应该从输入中读取多少是有保证的,即在换行符或EOF处停止读取并且读取的字节数不超过n-1。虽然没有明确说明允许向缓冲区写入多少,但常识是fgetsn参数用于防止缓冲区溢出.有点奇怪的是,该标准使用了模棱两可的术语 read,这可能并不一定意味着 gets 不能 write缓冲超过 n 字节,如果你想挑剔它使用的术语。但请注意,相同的“读取”术语用于两个问题:n-limit 和 EOF/newline limit。因此,如果您将与 n 相关的“读取”解释为缓冲区写入限制,那么 [为了保持一致性] 您 can/should 以相同的方式解释其他“读取”,即不写超过它的内容当字符串比缓冲区短时读取。

另一方面,如果您区分短语-动词“读入”(="write") 和“阅读”的用法,那么您就无法以相同的方式阅读委员会的文本。你保证它不会“读入”(=“写入”)数组超过 n 字节,但是如果输入字符串被换行符或 EOF 更快地终止,你只能保证剩下的(输入的)不会被“读取”,但是在这种更严格的阅读下,这是否意味着不会被“读入”(=“写入”)缓冲区尚不清楚。关键问题是关键字是“into”,它被省略了,所以问题是我在以下修改后的引用中括号中给出的完成是否是预期的解释:

No additional characters are read [into the array] after a new-line character (which is retained) or after end-of-file.

坦率地说,一个 postcondition 表述为公式(在这种情况下会很短)会比我引用的废话更有帮助...

我懒得去尝试分析他们关于 *scanf 家族的文章,因为我怀疑考虑到这些函数中发生的所有其他事情,它会变得更加复杂;他们为 fscanf 写的文章大约有五页长……但我怀疑类似的逻辑也适用。

缓冲区中的每个字节都是一个对象。除非 sscanffgets 的函数描述的某些部分提到修改这些字节,或者甚至暗示它们的值可能会改变,例如通过声明它们的值变得未指定,则适用一般规则:(强调我的)

6.2.4 Storage durations of objects

2 [...] An object exists, has a constant address, and retains its last-stored value throughout its lifetime. [...]

同样的原则保证

#include <stdio.h>
int a = 1;
int main() {
  printf ("%d\n", a);
  printf ("%d\n", a);
}

尝试打印 1 两次。 a虽然是全局的,但是printf可以访问全局变量,printf的描述中没有提到不是修改a .

fgetssscanf 的描述都没有提到修改缓冲区超过实际应该写入的字节(读取错误的情况除外),所以这些字节不'得到修改。

取决于正在使用的功能(以及在较小程度上它的实现)。 sscanf 会在遇到第一个非空白字符时开始写入,并继续写入,直到它的第一个空白字符,它会添加一个结束 0 和 return。但是像 strncpy 这样的函数(著名的)会将缓冲区的其余部分清零。

然而,C 标准中没有任何内容规定这些函数的行为方式。