为什么应该使用strncpy而不是strcpy?

109

编辑:我已经添加了示例的来源。

我发现了这个示例

char source[MAX] = "123456789";
char source1[MAX] = "123456789";
char destination[MAX] = "abcdefg";
char destination1[MAX] = "abcdefg";
char *return_string;
int index = 5;

/* This is how strcpy works */
printf("destination is originally = '%s'\n", destination);
return_string = strcpy(destination, source);
printf("after strcpy, dest becomes '%s'\n\n", destination);

/* This is how strncpy works */
printf( "destination1 is originally = '%s'\n", destination1 );
return_string = strncpy( destination1, source1, index );
printf( "After strncpy, destination1 becomes '%s'\n", destination1 );

这段代码产生了以下输出:

目标初始值=`abcdefg`
执行strcpy后,目标变为`123456789`
目标1初始值=`abcdefg` 执行strncpy后,目标1变为`12345fg`

这让我想知道为什么会有人想要这种效果。看起来会很混乱。这个程序让我觉得你基本上可以用Tom Bro763覆盖某人的姓名(例如Tom Brokaw)。

使用strncpy()相比于使用strcpy()有哪些优势?


97
我认为你想问的是“为什么会有人使用strcpy而不是strncpy?” - Sam Harwell
9
我对“strncpy()”的不满这篇文章探讨了C语言函数“strncpy()”的问题。很多人认为“strncpy()”比“strcpy()”更安全,但作者指出了它的一些缺陷。作者认为,“strncpy()”虽然可以限制复制的字符数量,但它并没有确保目标字符串以空字符结尾。如果源字符串长度超过目标字符串的长度,则没有空字符,这可能导致访问未定义的内存并引起程序故障。此外,“strncpy()”在处理不足长度的源字符串时会用空字符来填充目标字符串,这会使目标字符串变得比实际需要更长。这可能会浪费内存并且给程序带来潜在的风险。最后,作者提到了其他替代方案,例如使用更安全的函数“strlcpy()”或手动添加空字符以确保目标字符串的正确性。总之,作者建议开发人员在使用“strncpy()”时要小心谨慎,考虑它的潜在风险并寻找更好的解决方案。 - Keith Thompson
1
@KeithThompson:从设计的角度来看,我认为strncatstrncpy更愚蠢;我们多少次会知道一个未知长度字符串后面的缓冲区还剩多少空间?如果目标字符串的长度是已知的,应该找到源字符串的长度(如果未知),将该值夹在可用的缓冲区空间内,然后使用memcpy复制适合的部分,并手动存储零。如果目标字符串的长度未知,则通常需要找到其长度以了解超出多少空间可用,在这种情况下仍适用上述方法。 - supercat
1
我很惊讶看到@SamHarwell的评论在这里得到了这么多的赞同; 它似乎完全忽略了OP完全合理的问题的要点,即strncpy具有超出您所期望的奇怪行为,这是“更安全的strcpy版本”。 - xdavidliu
11个回答

209

strncpy()函数被设计用于解决一种非常特定的问题:操作以原始UNIX目录条目存储方式存储的字符串。这些目录使用短固定大小的数组(14个字节),只有在文件名比数组短时才使用空结束符。

这就是strncpy()两个奇怪之处的原因:

  • 如果目标完全填满,它不会在目标上放置空结束符;
  • 它总是完全填充目标,如有必要,使用空字符。

如果需要“更安全的strcpy()”,最好使用strncat(),像这样:

if (dest_size > 0)
{
    dest[0] = '\0';
    strncat(dest, source, dest_size - 1);
}

这将始终对结果进行空字符终止,并且不会复制更多的内容。


但是,当然,strncpy 也不总是你想要的:strncpy 接受要添加的最大字符数,而不是目标缓冲区大小... 但这只是一个小问题,所以除非你试图将一个字符串连接到另一个字符串上,否则可能不会成为问题。 - David Wolever
我不知道其中的原因,而它与我目前正在进行的工作非常相关。 - Matt Joiner
strncpy()函数旨在以定长、空字符填充的格式存储字符串。这种格式用于原始的Unix目录条目,但由于它可以在N个字节的存储空间中存储0-N个字节的字符串,因此在无数其他地方也被使用。即使在今天,许多数据库也在其定长字符串字段中使用了空字符填充的字符串。strncpy()函数引起混淆的原因在于它将字符串转换为FLNP格式。如果需要的是FLNP字符串,那太好了。但是,如果需要一个空终止字符串,则必须自行提供终止符。 - supercat
我不理解“接受要添加的最大字符数而不是目标缓冲区大小”-它既不接受前者也不接受后者,它接受一个整数:它恰好触及dest的n个字符,不多也不少。它从源中检查(并复制)字符,直到它复制了n个字符或遇到NUL为止-它从未检查超过n个字符的源。 - Spike0xff
3
在调用strncat之前,为什么需要写dest[0] = '\0';?您能解释一下吗? - Soner from The Ottoman Empire
6
strncat()函数将源字符串连接到目标字符串的末尾。我们只想将源字符串复制到目标字符串中,因此我们首先将目标字符串设置为空字符串——这就是dest[0]='\0'的作用。 - caf

104

strncpy通过要求您在其中放入一个长度来防止缓冲区溢出。strcpy依赖于尾随的\0,这可能并不总是发生。

其次,你选择仅在7个字符的字符串中复制5个字符超出了我的理解,但它产生了预期结果。它只复制前n个字符,其中n是第三个参数。

所有的n函数都用作防御性编码以防止缓冲区溢出。请使用它们来代替较旧的函数,例如strcpy


53
请参见http://www.lysator.liu.se/c/rat/d11.html: strncpy最初引入到C库中是为了处理结构体中的固定长度名称字段,例如目录条目。 这些字段与字符串的使用方式不同:对于最大长度字段而言,尾随的空值是不必要的,并且将较短名称的尾部字节设置为空可以确保有效的字段比较。 strncpy并非源自“有界strcpy”,委员会更倾向于承认现有做法,而不是修改函数以更好地适应这种用法。 - Sinan Ünür
41
我不确定为什么这个帖子会得到很多赞 - strncpy从未被设计成strcpy的更安全的替代品,实际上它并不比strcpy更安全,因为它没有对字符串进行零终止。此外,它还具有不同的功能,即使用NUL字符填充提供的长度。 正如caf在他的回复中所说 - 它是用于覆盖固定大小数组中的字符串。 - Dipstick
32
事实仍然是,strncpy不是strcpy的更安全版本。 - Sinan Ünür
11
所有的n个函数都用作防御性编码以避免缓冲区溢出。请使用它们来代替较旧的函数,如strcpy。对于“snprintf”而言,这是正确的,但对于“strncat”则不相关,而对于“strncpy”则完全不正确。怎么会有这么多人赞同这个答案呢?这表明了这个虚假函数情况的糟糕程度。使用它并不是防御性的:在大多数情况下,程序员并不理解其语义,并可能创建一个潜在的非零结尾字符串。 - chqrlie
8
@Eric你没有提到一个显而易见的事实,即如果源字符串长度大于或等于nstrncpy将使目标字符串未终止。因此,它完全不具有防御性,只会导致段错误。 - user3386109
显示剩余3条评论

39

虽然我知道strncpy的意图,但它并不是一个好的函数。如果你正在处理以空字符结尾的字符串,请避免使用它及其所有类似函数。Raymond Chen解释

就个人而言,我的结论很简单:如果你正在处理以空字符结尾的字符串,请避免使用strncpy及其所有类似函数。尽管名称中有"str",但这些函数并不生成以空字符结尾的字符串。它们将以空字符结尾的字符串转换为原始字符缓冲区。在第二个缓冲区期望得到以空字符结尾的字符串的情况下使用它们是错误的。如果源太长,则无法获得适当的空终止符,如果源太短,则会获得不必要的空填充。

另请参见为什么strncpy不安全?


28

strncpy并不比strcpy更安全,它只是用一种错误换了另一种错误。在C语言中处理C字符串时,你需要知道缓冲区的大小,这是无法避免的。strncpy在处理目录时是有合理性的,并且除此之外,你永远不应该使用它:

  • 如果你知道你的字符串和缓冲区的长度,为什么要使用strncpy?最好情况下这只是浪费计算能力(添加无用的0)
  • 如果你不知道长度,那么你会冒着悄悄截断字符串的风险,这并不比缓冲区溢出好多少

我认为这是对strncpy的一个很好的描述,所以我已经投了一票。strncpy有它自己的一套麻烦。我猜这就是为什么像glib这样的库有它自己的扩展的原因。而且,作为程序员,很不幸你必须意识到所有数组的大小。把以0结尾的字符数组作为字符串的决定,给我们带来了巨大的代价... - Friedrich
2
零填充字符串在存储数据于固定格式文件时非常常见。当然,像数据库引擎和XML这样的东西的流行,以及用户期望的不断发展,使得固定格式文件比20年前更少见了。尽管如此,这种文件通常是存储数据最高效的方式。除非记录中预期长度和最大长度之间存在巨大差异,否则将记录作为包含一些未使用数据的单个块读取要比将记录分成多个块读取快得多。 - supercat
刚接手维护遗留代码,其中使用了g_strlcpy()函数,因此不会出现填充效率低下的问题,但是确实没有维护传输的字节数计数,因此代码在默默地截断结果。 - user2548100

23
你需要的是函数strlcpy(),它总是用0终止字符串并初始化缓冲区,还能检测溢出。唯一的问题是它不(真正地)可移植,并且只存在于一些系统上(BSD、Solaris)。这个函数的问题在于,就像可以从http://en.wikipedia.org/wiki/Strlcpy的讨论中看到的那样,它会引发另一个问题。
我个人认为它比strncpy()strcpy()要更实用,性能更优,是snprintf()的好伴侣。对于没有这个函数的平台,相对容易实现。(在应用程序的开发阶段),我使用一个捕获版本来替换这两个函数(snprintf()strlcpy()),在缓冲区溢出或截断时,无情地中止程序。这可以快速捕捉到最严重的错误,特别是当你在从其他人的代码库中工作时。
编辑:strlcpy()可以很容易地实现:
size_t strlcpy(char *dst, const char *src, size_t dstsize)
{
  size_t len = strlen(src);
  if(dstsize) {
    size_t bl = (len < dstsize-1 ? len : dstsize-1);
    ((char*)memcpy(dst, src, bl))[bl] = 0;
  }
  return len;
}

3
除了 Linux 和 Windows 外,几乎所有系统都可以使用 strlcpy 函数!但是它是 BSD 许可证的,所以你可以将其直接添加到你的库中并从那里使用。 - Michael van der Westhuizen
你可能想为 dstsize > 0 添加一个测试,如果不是,则什么也不做。 - chqrlie
是的,你说得对。我会添加检查,因为如果没有它,dstsize将触发在目标缓冲区上长度为lenmemcpy并溢出它。 - Patrick Schlüter
在你提供的第二个链接中,你提到了有关strlcpy()的glibc维护者争议。为了证明他对这个问题的无知,他提出的一个回答是,strlcpy()可以用*(char*)mempcpy(dst, src, bl) = 0来替换。如果你看一下我的实现(以及我遇到的bug),你就会看到他的回答为什么是荒谬的。 - Patrick Schlüter
@PatrickSchlüter: strncpy 的设计目的是将以零结尾的字符串转换为固定大小的零填充格式。虽然可以使用它来复制固定大小的零填充字符串,但对于这种情况,通常使用memcpy更有效率。 - supercat
显示剩余4条评论

3

strncpy()函数是比较安全的函数:您必须传递目标缓冲区可以接受的最大长度。否则,源字符串可能没有正确地以0结尾,在这种情况下,strcpy()函数可能会向目标写入更多字符,破坏目标缓冲区后的任何内容。这就是许多攻击中使用的缓冲区溢出问题。

对于像read()这样的POSIX API函数,它不会在缓冲区中放置终止的0,但会返回读取的字节数,您将手动放置0,或使用strncpy()进行复制。

在您的示例代码中,index实际上不是索引,而是一个计数器 - 它告诉您从源中最多复制多少个字符到目标。如果在源的前n个字节中没有空字节,则放置在目标中的字符串将不以null结尾。


1

strncpy会用'\0'填充目标字符串,填充的长度为源字符串的长度,即使目标字符串比源字符串的长度要小...

man手册:

如果src的长度小于n,strncpy()会用空字符填充dest的剩余部分。

而且不仅是剩余部分...还有在这之后直到达到n个字符。因此可能会导致溢出...(请参阅man手册中的实现)


3
把源字符串复制到目标字符串时,即使目标字符串的长度小于源字符串的长度,strncpy也会在目标字符串中填充'\0',直到达到指定大小。但是,这个说法有误并且容易令人困惑:如果源字符串长度小于指定大小参数,则strncpy会使用'\0'填充目标字符串,该参数并不是源字符串的大小,也不是从源字符串复制的最大字符数(如strncat中),而是目标字符串的大小。 - chqrlie
@chqrlie:是否需要一个尾随的空字节取决于数据应该表示什么。在结构中使用零填充而不是零终止数据不像以前那样常见,但如果例如一个目标文件格式使用8字节的段名称,则能够在结构内部处理最多8个字符的char[8]可能比使用char[8]但只能处理7个字符或必须将字符串复制到char[9]缓冲区然后memcpy到目标位置更好。 - supercat
@supercat:确实,strncpy()早在70年代初就为此目的而发明,但是这样的结构在C程序中从未流行过,今天已经变得极为罕见。如果目标不需要以空字符结尾,则strncpy()可能是正确的工具,否则它是一个笨重且容易出错的野兽,应该避免使用。 - chqrlie
@chqrlie:与其谈论目标“需要”零终止,我会考虑它是否“可以”。如果链接器或汇编器将标识符限制为例如8个字节,我会说很可能它们会在8个字节的零填充中保存它们。现在内存很丰富,支持更长的名称的价值足以证明为任意长度名称分配存储空间而不是与相关符号表条目分开,但如果要在结构体内部存储字符串,则通常更明智地使用零填充... - supercat
@supercat:我不同意:这并没有更有意义!这样的非零终止字符数组很难操作:大多数标准C字符串函数不能与它们一起使用。您似乎来自一个将字符串作为一等对象的不同文化,但在C中并非如此。您当然可以编写一组函数来处理固定长度的字符串,但是这项任务非常艰巨,因为您需要包装所有期望以空终止C字符串为参数的库API...微软采用了这种方法来处理Unicode字符串,在事后看来是一个可怕的错误。 - chqrlie
显示剩余7条评论

0
对我来说,将一个字符串复制到另一个字符串并在达到目标长度时停止的最简洁方法是使用snprintf
#include <stdio.h>
#include <string.h>

int main()
{
    char src[] = "Some example...";
    char dest1[50], dest2[5];

    snprintf(dest1, sizeof(dest1), "%s", src);
    snprintf(dest2, sizeof(dest2), "%s", src);

    printf("%s\n", dest1);
    printf("%s\n", dest2);

    return 0;
}

输出:

Some example...
Some

但可能不是最高效的。

-1

这可以用于许多其他场景,其中您需要仅将原始字符串的一部分复制到目标位置。使用strncpy()函数,您可以仅复制原始字符串的有限部分,而不是像strcpy()函数那样复制整个字符串。我看到您发布的代码来自publib.boulder.ibm.com


-2

这取决于我们的需求。 对于Windows用户

当我们不想复制整个字符串或者只想复制n个字符时,我们使用strncpy。但是strcpy会复制整个字符串,包括终止的空字符。

以下链接将帮助您更好地了解strcpy和strncpy以及它们的使用场景。

关于strcpy

关于strncpy


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接