什么是strncpy()的最佳替代品?

15

strncpy()函数并不总是以空字符结尾,因此我想知道什么是始终以空字符结尾的最佳替代方法?我需要一个函数,如果:

strlen(src) >= n /*n is the number of characters to be copied from source*/

不需要再添加类似这样的代码:

buf[sizeof(buf)-1] = 0; 

3
请查看 strncpy_s 函数的文档,该函数属于 C 语言中处理字符串的一部分。 - Yuriy Ivaskevych
3
除非缓冲区大小为0,否则snprintf始终会添加字符串结束符。 - M.M
3
你自己添加空结束符或者函数为你添加,这有什么重要的区别吗?即使你有这种不合理的要求,也可以简单地将代码封装在另一个函数中。 - Lundin
2
请注意,strncpy 的第三个参数是输出缓冲区的大小,而不是要复制的字符数。 - M.M
1
您的要求仍然不够明确。例如,请解释一下这个函数是否应该处理 strlen(src) < n 的情况(如果是,那么在这种情况下您希望发生什么)。 - M.M
显示剩余6条评论
6个回答

15
如果你不知道要复制的字符串长度,你可以在这里使用 snprintf。这个函数将格式化输出发送到str。它的作用类似于sprintf(),但是不会写入比str分配的更多字节。如果结果字符串长度超过n-1个字符,则其余字符将被省略。它还始终包含空终止符\0,除非缓冲区大小为0
如果你真的不想使用strncpy()strcpy(),这将是一种替代方法。然而,使用strcpy()手动添加一个空终止符总是一个简单、高效的方法。在C中,在任何处理的字符串末尾添加一个空终止符是很正常的。
下面是一个使用sprintf()的基本示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE 1024

int main(void) {
    const size_t N = SIZE;
    char str[N];
    const char *example = "Hello World";

    snprintf(str, sizeof(str), "%s", example);

    printf("String = %s, Length = %zu\n", str, strlen(str));

    return 0;
}

哪个会打印出:

String = Hello World, Length = 11

这个例子展示了snprintf()"Hello World"复制到str中,并在结尾添加了一个\0终止符。

注意:strlen()仅适用于以空字符结尾的字符串,如果字符串没有以空字符结尾,则会导致未定义的行为snprintf()还需要更多的错误检查,可以在man页面上找到相关信息。

正如其他人所说,这不是一种高效的方法,但如果你有需要的话,它也能解决问题。


小示例会很好。我们不希望初学者做一些可怕的事情,比如snprintf(dst, DST_SIZE, src); - user694733
2
snprintf非常低效。如果您只是想用它来复制字符串,那么您正在浪费资源。 - Lundin
@M.M OP之前提到他只是想要一个自动将空终止符添加到字符串的函数。我认为,如果这是OP的要求,那么snprintf()是一个有效的选择。从他的问题来看,效率似乎不是他最高的优先级。 - RoadRunner
1
@lundin 不同意这是非常低效的。优化编译器可以查看格式内部并创建非常高效的代码。 - chux - Reinstate Monica
@chux 我对此表示怀疑。而且无论执行速度如何,这个函数都会占用程序内存的大部分空间,对于这样一个琐碎的任务来说太浪费了。 - Lundin
显示剩余4条评论

11

如果您想要的行为是 strcpy 的截断版本,它将源字符串的最长初始前缀复制到已知大小的缓冲区中,那么有多种选项可供选择:

  • 您可以编写一个专门的函数来完成此任务:

char *safe_strcpy(char *dest, size_t size, char *src) {
    if (size > 0) {
        size_t i;
        for (i = 0; i < size - 1 && src[i]; i++) {
             dest[i] = src[i];
        }
        dest[i] = '\0';
    }
    return dest;
}

大多数BSD系统都有一个函数strlcpy(char *dest, const char *src, size_t n);, 它的功能与这个函数类似。但是,它们参数的顺序让人困惑,因为n通常是dest数组的大小,但却位于src参数之后。

  • 你可以使用strncat()函数:

  • char *safe_strcpy(char *dest, size_t size, char *src) {
        if (size > 0) {
            *dest = '\0';
            return strncat(dest, src, size - 1);
        }
        return dest;
    }
    
  • 您可以使用snprintf()sprintf(),但感觉就像用液压压力机来驱动钉子一样:

    snprintf(dest, size, "%s", src);
    

    另外一种选择:

    if (size > 0) {
        sprintf(dest, "%.*s", (int)(size - 1), src);
    }
    
  • 如果您知道源指针指向以空字符结尾的字符串,那么可以使用 strlen()memcpy() 函数。但是,如果源字符串比目标数组长得多,这种方法也不如上面两种解决方案有效率:

  • char *safe_strcpy(char *dest, size_t size, char *src) {
        if (size > 0) {
            size_t len = strlen(src);
            if (len >= size)
                len = size - 1;
            memcpy(dest, src, len);
            dest[len] = '\0';
        }
        return dest;
    }
    

    如果目标系统可用,可以使用strnlen()避免低效率:

    char *safe_strcpy(char *dest, size_t size, char *src) {
        if (size > 0) {
            size_t len = strnlen(src, size - 1);
            memcpy(dest, src, len);
            dest[len] = '\0';
        }
        return dest;
    }
    
  • 您可以使用 strncpy() 并强制添加空终止字符。但如果目标数组很大,这样做效率不高,因为strncpy()在源字符串较短时还会用空字节填充目标数组的其余部分。此函数的语义非常反直觉、难以理解且容易出错。即使正确使用时,strncpy() 的使用也会导致潜在的错误,因为下一个程序员可能会更加大胆但不够精通,试图对他不完全理解的代码进行优化并引入错误。安全起见:请避免使用此函数。

  • 另一个问题是调用者能否检测到截断。上述实现中的 safe_strcpy 返回目标指针,就像 strcpy 一样,因此不向调用者提供任何信息。snprintf() 返回一个表示如果目标数组足够大将要复制的字符数的整数值,在本例中,返回值为 strlen(src) 转换为 int,这使得调用者可以检测到截断和其他错误。

    以下是另一个更适合由不同部分组成字符串的函数:

    size_t strcpy_at(char *dest, size_t size, size_t pos, const char *src) {
        size_t len = strlen(src);
        if (pos < size) {
            size_t chunk = size - pos - 1;
            if (chunk > len)
                chunk = len;
            memcpy(dest + pos, src, chunk);
            dest[pos + chunk] = '\0';
        }
        return pos + len;
    }
    

    此函数可在序列中使用,而不会出现未定义行为:

    void say_hello(const char **names, size_t count) {
        char buf[BUFSIZ];
        char *p = buf;
        size_t size = sizeof buf;
    
        for (;;) {
            size_t pos = strcpy_at(p, size, 0, "Hello");
            for (size_t i = 0; i < count; i++) {
                pos = strcpy_at(p, size, pos, " ");
                pos = strcpy_at(p, size, pos, names[i]);
            }
            pos = strcpy_at(p, size, pos, "!");
            if (pos >= size && p == buf) {
                // allocate a larger buffer if required
                p = malloc(size = pos + 1);
                if (p != NULL)
                    continue;
                p = buf;
            }
            printf("%s\n", p);
            if (p != buf)
                free(p);
            break;
        }
    }
    

    snprintf提供一种等效的方法也很有用,通过地址传递pos

    size_t snprintf_at(char *s, size_t n, size_t *ppos, const char *format, ...) {
        va_list arg;
        int ret;
        size_t pos = *ppos;
    
        if (pos < n) {
            s += pos;
            n -= pos;
        } else {
            s = NULL;
            n = 0;
        }
        va_start(arg, format);
        ret = snprintf(s, n, format, arg);
        va_end(arg);
    
        if (ret >= 0)
            *ppos += ret;
    
        return ret;
    }
    

    通过地址传递 pos 而不是值,使得 snprintf_at 可以返回 snprintf 的返回值,如果出现编码错误,则可能为 -1


    9
    作为对strncpy()的替代方案的示例,考虑Git 2.19(2018年第三季度),发现滥用系统API函数如strcat(); strncpy(); ...这些选择的函数现在在此代码库中被禁止,并将导致编译失败。该补丁列出了几个替代方案,使其与此问题相关。

    请查看由Jeff King (peff)提交的提交 e488b7a, 提交 cc8fdae, 提交 1b11b64(2018年7月24日)和提交 c8af66a(2018年7月26日)。
    (由Junio C Hamano -- gitster --提交 e28daf2中合并,于2018年8月15日)

    banned.h:将strncpy()标记为禁用

    strncpy()函数比strcpy()好一些,但由于其有趣的终止语义,仍然很容易被误用。
    即,如果它截断了字符串,则会省略NUL终止符,您必须自己记得添加它。即使您正确使用它,有时读者也很难在不查找代码的情况下验证它是否正确。
    如果您考虑使用它,请考虑使用以下替代方法:

    • strlcpy(),如果您只需要一个被截断但带NUL终止符的字符串(我们提供了兼容版本,因此始终可用)
    • xsnprintf(),如果您确定要复制的内容可以放下
    • strbufxstrfmt(),如果您需要处理任意长度的堆分配字符串。

    请注意,在compat/regex/regcomp.c中有一个strncpy实例,这是可以接受的(在复制之前它会分配足够大的字符串)。
    但是,即使使用NO_REGEX=1编译,这也不会触发禁用列表,因为:

    1. 编译它时我们不使用git-compat-util.h(而是依赖于上游库的系统包含文件);以及
    2. 它在一个“#ifdef DEBUG”块中

    由于它不会触发banned.h代码,因此最好保留它,以使我们与上游的差异最小化。


    注意:一年后,随着Git 2.21(2019年第1季度),“strncat()”函数本身也成为被禁止的函数之一。
    请参见提交ace5707(由Eric Wong (ele828)于2019年1月2日提交)。 (由Junio C Hamano -- gitster --合并至提交81bf66b,2019年1月18日)

    banned.h: 将 strncat() 标记为被禁用的函数

    strncat()strcat() 一样具有二次行为,而且难以阅读和容易出错。尽管它在 Git 中还没有成为问题,但 strncat() 已经出现在 'cgit' 的 'master' 分支中,并在我的系统上导致了段错误。


    在 Git 2.24 (2019年第四季度) 中,它使用显式形式的 'vsprintf' 作为其自身被禁用的版本,而不是 'sprintf'。

    请参见 提交记录60d198d (2019年8月25日),作者为 Taylor Blau (ttaylorr)
    (由 Junio C Hamano -- gitster --提交记录37801f0 中合并,于2019年9月30日)


    4

    作为对建议使用snprintf()答案的替代方案:(注意:如果n <= 0,会出现问题)

    size_t sz = sizeof buf;
    /*n is the number of characters to be copied from source*/
    int n = (int) sz - 1;
    snprintf(buf, sz, "%s", src);
    

    代码可使用以下精度

    "... 对于s转换,要写入的最大字节数。..." C11 §7.21.6.1 4

    sprintf(buf, "%.*s", n, src);
    

    它有微妙的优势,即 src 不必是一个字符串,只需是字符数组。

    另一个用于字符串的工具。


    @chqrlie 翻译:*n 是要从源中复制的字符数,而不是缓冲区的大小。 - chux - Reinstate Monica

    3
    使用strlcpy()函数。 strlcpy()函数使用目标缓冲区的完整大小,并保证在有空间的情况下以NULL结尾。更多信息请参考man页面。

    2
    我认为strlcpy不是标准函数,我个人更喜欢使用标准函数。 - cadaniluk
    strcpy_s函数,它是C11标准的一部分,但编译器不一定支持它。尽管如此,它仍比strlcpy更标准。而且没有真正的理由使用它们中的任何一个... - Lundin
    1
    我相信你指的是 strncpy_s。但是,strncpy_s 容易出现偏移错误。显然,必须指定目标缓冲区大小减一。strlcpy 更加自然。 - Sven

    1

    strcpy函数总是以空字符结尾。当然,您应该包含代码来防止缓冲区溢出,例如:

    char buf[50];
    
    if (strlen(src) >= sizeof buf)
    {
        // do something else...
    }
    else
        strcpy(buf, src);
    

    3
    不确定为什么这个帖子被踩了。如果您知道缓冲区的大小,这是一个明显的替代方案。如果您不知道缓冲区的大小,无论如何都无法进行安全复制。 - Andrew Henle

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