将 strncpy 转换为没有空终止符空间的字符串是不是安全?

Posted

技术标签:

【中文标题】将 strncpy 转换为没有空终止符空间的字符串是不是安全?【英文标题】:Is it Safe to strncpy Into a string That Doesn't Have Room for the Null Terminator?将 strncpy 转换为没有空终止符空间的字符串是否安全? 【发布时间】:2019-01-10 16:03:25 【问题描述】:

考虑以下代码:

const char foo[] = "lorem ipsum"; // foo is an array of 12 characters
const auto length = strlen(foo); // length is 11
string bar(length, '\0'); // bar was constructed with string(11, '\0')

strncpy(data(bar), foo, length);
cout << data(bar) << endl;

我的理解是strings 总是分配有一个隐藏的空元素。如果是这种情况,那么bar 确实分配了 12 个字符,其中第 12th 是隐藏的'\0',这是非常安全的......如果我错了,那么@987654326 @ 将导致未定义的行为,因为没有空终止符。

谁能帮我确认一下?这合法吗?


关于为什么使用strncpy 而不是仅仅使用string(const char*, const size_t) 构造函数,存在很多问题。我的意图是让我的玩具代码接近包含vsnprintf 的实际代码。不幸的是,即使在这里得到了很好的答案,我发现vsnprintf 的行为与strncpy 不同,我在这里提出了一个后续问题:Why is vsnprintf Not Writing the Same Number of Characters as strncpy Would?

【问题讨论】:

怎么可能不呢?当然,假设您复制的字节数不超过可用缓冲区空间。 @TrebuchetMS 是的,谢谢,我已经修正了该评论。 你有这方面的实际用例吗?如果你给 std::string 一个 c 字符串,它会做同样的事情而不会让人头疼。 @NathanOliver 是的,我正在使用vsnprintf 来填充string。似乎只是增加了要问的问题的复杂性,并没有强迫这个问题。 那为什么不用std::string bar( foo, length ); 而不是讨厌的strncpy() 【参考方案1】:

这是安全的,只要将[0, size()) 字符复制到字符串中即可。每[basic.string]/3

在所有情况下,[data(), data() + size()] 都是有效范围,data() + size() 指向值为 charT()(“空终止符”)的对象,size() &lt;= capacity()true

所以string bar(length, '\0') 为您提供了一个size() 为11 的字符串,末尾带有一个不可变的空终止符(实际大小总共有12 个字符)。只要您不覆盖该空终止符,或尝试将其写过去,就可以。

【讨论】:

不确定他是否会好起来 - std::string::length() 会给出“错误”的信息 @Slava 怎么会出错?字符串以11 的大小开始,除非他们使用字符串函数,否则无法更改。如果他们只复制 5 个字符,他们仍然有一个大小为 11 的字符串,它只是有 6 个额外的空值终止它。 我把“错误”放在双引号中。是的,它会显示正确大小的缓冲区,但将其用作字符串可能会导致丑陋的问题,在使用此字符串时很难捕捉和修复。 @Slava a std::string 允许包含空字符,这与 C 字符串不同。 @Slava “因为大多数开发人员都希望 length() 为您提供字符串的长度,而不是缓冲区的大小。” 但这正是它的作用。字符串是字节序列。这个结尾有一些空字节。您应该改掉将“string”视为等同于“c-string”的习惯。 (同时,由于.reserve() 等等,缓冲区可能会更大一些)【参考方案2】:

这里有两种不同的东西。

首先,strncpy 在这种情况下是否添加了额外的\0(11 个非\0 元素将被复制到大小为 11 的字符串中)。答案是否定的:

将src指向的字节串的最多count个字符(包括终止的空字符)复制到dest指向的字符数组中。

如果在复制整个字符串 src 之前达到 count,则生成的字符数组不是以 null 结尾的。

所以通话完全没问题。

然后data() 给你一个正确的\0-终止字符串:

c_str() 和 data() 执行相同的功能。 (C++11 起)

看来,对于 C++11,你是安全的。该字符串是否分配了额外的\0 似乎没有在文档中说明,但API很清楚你所做的一切都很好。

【讨论】:

为了扩展你的陈述,你是说string 没有分配了一个隐藏的空终止符? 分配必须以这种方式工作才能使c_str()/data() 成为O(1),因为索引str[str.size()] 为您提供'\0'(C++11 起)。在实践中,他们总是以这种方式工作。【参考方案3】:

您分配了一个 11 个字符的 std::string。您不会尝试读取或写入超出此范围的任何内容,因此该部分将是安全的。

所以真正的问题是你是否弄乱了字符串的内部结构。既然你没有做过任何不允许的事情,那怎么可能呢?如果字符串需要在内部保留一个 12 字节的缓冲区并在末尾填充一个空值以履行其约定,那么无论您执行什么操作都是如此。

【讨论】:

【参考方案4】:

是的,根据char * strncpy(char* destination, const char* source, size_t num) 是安全的:

从字符串中复制字符

将源的前 num 个字符复制到目标。如果在复制 num 个字符之前找到源 C 字符串的结尾(由空字符表示),则用零填充目标,直到总共写入了 num 个字符。

【讨论】:

问题是有一个额外的字符需要复制。 不是问题,显然strncpy 不会写出越界。问题是cout read 是否会越界。请更正答案以解决问题或删除。 @Matthieu - 不一定。如果这是std::string,那就是目标,它会自我意识到它的长度。 @StoryTeller 虽然缺少\0(与data(bar) 一起使用时)。使用字符串本身确实很好。 @Matthieu - 只有当操作需要目标缓冲区来保存以空字符结尾的字符串时,这才是问题。

以上是关于将 strncpy 转换为没有空终止符空间的字符串是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

C memset - 优雅地添加一个空终止符

为啥需要空终止符?

释放字符串直到空终止符

C++ char 数组空终止符位置

字符串不是空终止错误

如何连接字符串,但保留每个单独的空终止符?