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

What is the best alternative to strncpy()?

函数 strncpy() 并不总是以 null 终止,所以我想知道始终以 null 终止的最佳替代方案是什么? 我想要一个函数,如果:

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

无需像这样添加更多代码:

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

使用strlcpy()函数。

strlcpy() 占用目标缓冲区的全部大小并保证 NULL-termination 如果有空间。阅读 man 页面了解更多信息。

strcpy 函数总是 null-terminates。当然你应该包括防止缓冲区溢出的代码,例如:

char buf[50];

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

如果你想复制的字符串的长度未知,你可以在这里使用snprintf。此函数将格式化输出发送到 str。它的行为类似于 sprintf(),但不会写入更多由 str 分配的字节。如果生成的字符串长于 n-1 个字符,则剩余的字符将被忽略。它还始终包含空终止符 [=15=],除非缓冲区大小为 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,并在末尾添加了一个 [=15=] 终止符。

注意: strlen() 仅适用于空终止字符串,并且会导致 undefined behaviour if the string is not null terminated. snprintf() also needs more error checking, which can be found on the man page

正如其他人所说,这不是一种有效的方法,但如果您去寻找,它就在那里。

作为建议 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);

代码可以使用以下精度

"... the maximum number of bytes to be written for s conversions. ..." C11 §7.21.6.1 4

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

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

另一个字符串工具。

如果您想要的行为是 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] = '[=10=]';
        }
        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 = '[=11=]';
            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] = '[=14=]';
        }
        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] = '[=15=]';
        }
        return dest;
    }
    
  • 您可以使用 strncpy() 并强制空终止。如果目标数组很大,这将是低效的,因为如果源字符串较短,strncpy() 也会用空字节填充目标数组的其余部分。这个函数的语义很counter-intuitive,不好理解而且error-prone。即使使用正确,strncpy() 的出现也是等着咬人的错误,因为下一个程序员,更大胆但不那么精明,可能会更改代码并引入它们以尝试 优化 他不完全理解的代码。谨慎行事:避免使用此功能。

这个问题的另一个方面是调用者检测截断的能力。 safe_strcpy return 目标指针的上述实现与 strcpy 一样,因此不向调用者提供任何信息。 snprintf() returns 一个 int 表示如果目标数组足够大将被复制的字符数,在这种情况下,return 值为 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] = '[=16=]';
    }
    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 到 return snprintf 的 return 值,可以是 -1 在编码错误的情况下。

作为必须使用替代 strncpy() 的示例,请考虑 Git 2.19(2018 年第 3 季度),它发现滥用系统 API 功能太容易了,例如strcat(); strncpy(); ...这些选定的函数现在在此代码库中被禁止,将导致编译失败。

那个补丁确实列出了几个备选方案,这使得它与这个问题相关。

参见 commit e488b7a, commit cc8fdae, commit 1b11b64 (24 Jul 2018), and commit c8af66a (26 Jul 2018) by Jeff King (peff)
(由 Junio C Hamano -- gitster -- in commit e28daf2 合并,2018 年 8 月 15 日)

banned.h: mark strncpy() as banned

The strncpy() function is less horrible than strcpy(), but is still pretty easy to misuse because of its funny termination semantics.
Namely, that if it truncates it omits the NUL terminator, and you must remember to add it yourself. Even if you use it correctly, it's sometimes hard for a reader to verify this without hunting through the code.
If you're thinking about using it, consider instead:

  • strlcpy() if you really just need a truncated but NUL-terminated string (we provide a compat version, so it's always available)
  • xsnprintf() if you're sure that what you're copying should fit
  • strbuf or xstrfmt() if you need to handle arbitrary-length heap-allocated strings.

Note that there is one instance of strncpy in compat/regex/regcomp.c, which is fine (it allocates a sufficiently large string before copying).
But this doesn't trigger the ban-list even when compiling with NO_REGEX=1, because:

  1. we don't use git-compat-util.h when compiling it (instead we rely on the system includes from the upstream library); and
  2. It's in an "#ifdef DEBUG" block

Since it's doesn't trigger the banned.h code, we're better off leaving it to keep our divergence from upstream minimal.


注意:1 年多后,Git 2.21(2019 年第一季度),“strncat()”函数本身现在 被禁止函数。

参见 commit ace5707 (02 Jan 2019) by Eric Wong (ele828)
(由 Junio C Hamano -- gitster -- in commit 81bf66b 合并,2019 年 1 月 18 日)

banned.h: mark strncat() as banned

strncat() has the same quadratic behavior as strcat() and is difficult-to-read and bug-prone. While it hasn't yet been a problem in Git iself, strncat() found it's way into 'master' of cgit and caused segfaults on my system.


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

参见 commit 60d198d (25 Aug 2019) by Taylor Blau (ttaylorr)
(由 Junio C Hamano -- gitster -- in commit 37801f0 合并,2019 年 9 月 30 日)