【问题标题】:my version of strlcpy我的 strlcpy 版本
【发布时间】:2010-05-29 03:58:04
【问题描述】:

gcc 4.4.4 c89

我的程序做了很多字符串处理。我不想使用 strncpy 因为它不会终止。而且我不能使用 strlcpy 因为它不可移植。

只是几个问题。我怎样才能让我的功能通过它的步伐,以确保它是完全安全和稳定的。单元测试?

这足以用于生产吗?

size_t s_strlcpy(char *dest, const char *src, const size_t len)
{
    size_t i = 0;

    /* Always copy 1 less then the destination to make room for the nul */
    for(i = 0; i < len - 1; i++)
    {
        /* only copy up to the first nul is reached */
        if(*src != '\0') {
            *dest++ = *src++;
        }
        else {
            break;
        }
    }

    /* nul terminate the string */
    *dest = '\0';

    /* Return the number of bytes copied */
    return i;
}

非常感谢您的任何建议,

【问题讨论】:

  • “我怎样才能让我的功能按照它的节奏来确保它完全安全和稳定。单元测试?”是的。
  • James 为什么不将其添加为答案而不是评论? :)
  • 是我还是你所有的空值都只有一个'l'?缺少“l”是怎么回事?
  • NULL 是指针,nul 是字符'\O'。
  • 如果你有强迫症,你可能可以证明这样一个函数的正确性。

标签: c


【解决方案1】:

虽然您可以按照另一篇文章的建议简单地使用另一个 strlcpy 函数,或者使用 snprintf(dest, len, "%s", src)(它总是终止缓冲区),但我在查看您的代码时注意到以下几点:

size_t s_strlcpy(char *dest, const char *src, const size_t len)
{
    size_t i = 0;

无需在此处设置len const,但它会很有帮助,因为它会检查以确保您没有修改它。

    /* Always copy 1 less then the destination to make room for the nul */
    for(i = 0; i < len - 1; i++)
    {

哎呀。如果 len 为 0 怎么办? size_t 通常是无符号的,所以 (size_t)0 - 1 最终会变成类似于 4294967295 的东西,导致您的例程在程序内存中翻滚并崩溃到一个未映射的页面。

        /* only copy up to the first nul is reached */
        if(*src != '\0') {
            *dest++ = *src++;
        }
        else {
            break;
        }
    }

    /* nul terminate the string */
    *dest = '\0';

上面的代码对我来说很好。

    /* Return the number of bytes copied */
    return i;
}

根据Wikipediastrlcpy返回strlen(src)(字符串的实际长度),而不是复制的字节数。因此,您需要继续计数src 中的字符,直到您点击'\0',即使它超过了len

此外,如果您的 for 循环在 len - 1 条件下终止,您的函数将返回 len-1,而不是像您期望的那样返回 len。


当我编写这样的函数时,我通常更喜欢使用开始指针(称为 S)和结束指针(称为 E)。 S 指向第一个字符,而 E 指向一个字符最后一个字符之后(这使得 E - S 是字符串的长度)。尽管这种技术可能看起来丑陋且晦涩难懂,但我发现它相当强大。

这是我如何编写 strlcpy 的一个过度注释的版本:

size_t s_strlcpy(char *dest, const char *src, size_t len)
{
    char *d = dest;
    char *e = dest + len; /* end of destination buffer */
    const char *s = src;

    /* Insert characters into the destination buffer
       until we reach the end of the source string
       or the end of the destination buffer, whichever
       comes first. */
    while (*s != '\0' && d < e)
        *d++ = *s++;

    /* Terminate the destination buffer, being wary of the fact
       that len might be zero. */
    if (d < e)        // If the destination buffer still has room.
        *d = 0;
    else if (len > 0) // We ran out of room, so zero out the last char
                      // (if the destination buffer has any items at all).
        d[-1] = 0;

    /* Advance to the end of the source string. */
    while (*s != '\0')
        s++;

    /* Return the number of characters
       between *src and *s,
       including *src but not including *s . 
       This is the length of the source string. */
    return s - src;
}

【讨论】:

  • 请注意,开始和过去的结束是在 C++ STL 中使用的相同技术,效果很好。
  • s_strlcpy(char *dest, const char *src, size_t len)s_strlcpy(char * restrict dest, const char * restrict src, size_t len) 更好,因为当dest, src 重叠时,此代码会出现问题。
  • @chux-ReinstateMonica:感谢您的建议。这是一个很好的优化,但我将其省略以保持代码简单。
【解决方案2】:

恕我直言,只需使用 original strlcpy,Ignacio Vazquez-Abram 简洁地说。 OpenBSDs 代码经过了实战测试,许可条款很重要;)。

至于您的代码,我会在其他人已经说过的内容中添加一些内容,这只是个人喜好问题:

/* only copy up to the first nul is reached */
if(*src != '\0') {
    *dest++ = *src++;
}
else {
    break;
}

我会这样写:

if(*src == '\0') {
    break;
}
*dest++ = *src++;

既是因为它减少了人们需要阅读的不必要代码的数量,又因为我的“风格”是始终如一地这样写,而不是if (ok) { do } else { handle error }。 if 上面的注释也是多余的(参见 for 循环上面的注释)。

【讨论】:

  • 我完全同意风格(很多其他人也这样做,stackoverflow.com/questions/114342/…); “正常”代码应该(如果可能)在没有额外缩进的情况下编写,它应该是测试的异常。
【解决方案3】:

为什么不使用 memccpy() 之类的东西而不是自己滚动?你只需要以一个空字节结束,但是增加一个标准函数比从头开始更容易而且通常更快。

一些架构会大量优化甚至使用汇编字符串函数来发挥它们的良好性能。

无需构建或调试:

str = memccpy (dest, src, '\0', len);
if(str)
    *str = '\0';

【讨论】:

  • 我是一名初级程序员,所以只是看看我犯了什么错误。好吧,不要重新发明轮子。但开发自己的会更有趣。
  • @robUK 重新发明轮子作为一种学习练习没有错,但正如你所问的,我永远不会在生产代码中这样做。此外,您贬低了标准库函数,但如果您尝试,基本上可以让它们工作,而且对于生产代码来说,这也是一种更好的方法。
  • @robUK 我更愿意扩充标准库的一个基本原因是如果我遇到了错误。如果我看到一个标准库调用,我可能不会把它当作一个 bug,但是如果它是一个自制的函数,那么 bug 的概率会更高,特别是如果它没有被大量调用,所以我会花更多的调试时间调查它。
  • OP 不能使用memccpy,原因与他们不能使用strlcpy 的原因相同。它不是便携式的。没有像memccpy这样的C标准库函数。
【解决方案4】:

是的,单元测试。检查大量随机生成的字符串。

不过在我看来还不错。

【讨论】:

  • 随机测试输入没有多大意义——如果测试发现失败,你将如何复制它?每次测试运行都可能测试算法的不同部分,并可能完全错过一些边缘情况。
  • @Bryan Oakley “如果测试发现失败,你将如何复制它?”使用伪随机输入,因此您可以随时复制它。
【解决方案5】:

我建议White-box testing 对于这样的功能(一种单元测试形式)很有用。

【讨论】:

    【解决方案6】:

    DRY 原则是“不要重复自己”。换句话说,不要创建新的代码来做已经完成的事情——检查标准 C 库,如上面的示例 (WilrWind) 所示。

    一个原因是提到的测试。标准 C 库已经过多年测试,因此可以肯定它可以像宣传的那样工作。

    通过玩代码来学习是个好主意,请继续尝试。

    【讨论】:

    • 问题的关键在于标准库没有提供直接的方法来做 OP 想做的事情。您所指的答案(WhirlWind)是基于非标准手段。
    【解决方案7】:

    单元测试? 这足以用于生产吗?

    对于像这样的“简单”函数可能就足够了,尽管测试函数的唯一真正方法是尝试破坏它。

    向它传递 NULL 指针、10k 字符长的字符串、len 的负值、以某种方式损坏的数据等等。一般认为:如果你是一个恶意用户试图破解它,你会怎么做?

    查看我的回复here中的链接

    【讨论】:

      【解决方案8】:

      我认为如此依赖长度并对其进行算术运算是错误的。

      size_t 类型是无符号的。考虑一下如果使用 0 大小的目标调用您的函数将如何表现。

      【讨论】:

        【解决方案9】:

        【讨论】:

        • 在我看来,该链接应该指向您发现有用的 C 静态代码分析器,而不是指向抽象定义它的页面。
        【解决方案10】:

        嗯,没想到这是一篇旧帖。

        这足以用于生产吗?
        完全安全稳定(?)

        缺点:
        无法正确处理 len == 0 - 易于修复。
        当源很长时返回值有问题 - 易于修复。
        (尚未讨论)不考虑重叠dest, src

        if(*src != '\0') { *dest++ = *src++; } 在读取之前覆盖 null 字符 很容易导致意外结果,因此迭代冒险超出了原始 '\0'

        // pathological example                 
        char buf[16] = "abc";
        const char *src = buf;       // "abc"
        const char *dest = buf + 2;  // "c"
        size_t dest_sz = sizeof buf - 2;
        s_strlcpy(dest, src, dest_sz);
        puts(dest); // "ababababababa", usual expectation "abc"
        

        两个解决方案:

        restrict
        从 C99 开始,C 具有restrict,这向编译器表明它可以假设通过src 读取和通过dest 写入的数据不会重叠。这允许编译器使用某些优化,否则它不能使用。 restrict 还通知用户不应提供重叠缓冲区。

        • 代码仍然会像上面那样失败,但那是调用者违反合同,而不是s_strlcpy()

        注意:const size_t len 中的 const 是函数声明中的干扰项。使用size_t size 也比使用size_t len 更清晰。

        size_t s_strlcpy(char * restrict dest, const char * restrict src, size_t size);
        

        这种restrict 用法类似于标准库strcpy() 等。

        char *strcpy(char * restrict s1, const char * restrict s2);
        

        处理重叠
        另一种是让s_strlcpy() 容忍重叠内存,如下所示。这几乎意味着代码需要使用memmove()

        size_t s_strlcpy(char *dest, const char *src, const size_t dest_size) {
          size_t src_len = strlen(src);
          if (src_len < dest_size) {
            memmove(dest, src, src_len + 1);  // handles overlap without UB
          } else if (dest_size > 0) {
            // Not enough room
            memmove(dest, src, dest_size - 1);  // handles overlap without UB
            dest[dest_size - 1] = '\0';
          }
          return src_len;  // I do not think OP's return value is correct. S/B src length.
        }
        

        希望我正确编码了strlcpy() 的所有功能。边缘情况需要时间来解决。

        【讨论】:

          猜你喜欢
          • 2017-08-15
          • 2021-12-19
          • 2011-10-22
          • 2022-11-05
          • 2020-09-11
          • 1970-01-01
          • 1970-01-01
          • 2013-09-04
          • 1970-01-01
          相关资源
          最近更新 更多