【问题标题】:How does this pointer-based strcat work in C?这个基于指针的 strcat 如何在 C 中工作?
【发布时间】:2020-09-19 18:17:09
【问题描述】:

我对 C 有一些基本的经验,但是我通常需要一点时间来弄清楚如何实现一些东西;使用指针等对我来说仍然有点神秘。

然后我看到一个像strcat implementation 这样的例子,我无法理解。有人介意向 C 新手解释一下吗?

char *
my_strcat(char *dest, const char *src)
{
    char *rdest = dest;

    while (*dest)
      dest++;
    while (*dest++ = *src++)
      ;
    return rdest;
}

当我读到那句话时,我想“rdest = ?,也许是真正的目的地”。所以设置一个指向原始目的地的指针。然后“while (*dest) dest++;”,这是在做什么?与下一行相同。我不关注。

它是否对原始的两个部分(src 和 dest)使用了任何额外的内存?就像在 JS 中一样,如果你连接 2 个字符串,它会为结合这两个字符串的第三个字符串创建内存,所以你有双倍的内存。在这个 C 实现中如何避免这种情况(如果是的话)?

【问题讨论】:

  • 正如提示:rdest 可能只是代表“返回目的地”。

标签: c pointers strcat


【解决方案1】:
char * my_strcat(char *dest, const char *src)
{
    // Standard dictates strcat() to return dest.
    // That is pretty useless (returning a pointer to the
    // *end* of dest would have been better), but that's
    // the way it is.
    // Since we iterate dest as part of the implementation,
    // we need to "remember" its original value.
    char *rdest = dest;

    // Iterate over the characters pointed to by dest until
    // we found the end (null byte terminator), which is "false"    
    while (*dest)
      dest++;

    // An assignment evaluates to the value assigned. So assigning
    // one character at a time (*dest = *src) will eventually
    // evaluate to false when we assigned the null byte terminator
    // from src (incidentially also terminating dest). Since we
    // postfix-increment both pointers during the assignment, we
    // don't need any actual body for the loop.
    while (*dest++ = *src++)
      ;

    // Return the "remembered" original dest value.
    return rdest;
}

它是否对原始的两个部分(src 和 dest)使用了任何额外的内存?就像在 JS 中一样,如果你连接 2 个字符串,它会为结合这两个字符串的第三个字符串创建内存,所以你有双倍的内存。在这个 C 实现中如何避免这种情况(如果是的话)?

strcat 的前提条件是dest 必须有足够的空间来保存最终结果。所以,不,它不需要/分配额外的内存。您需要确保足够的内存,或者realloc 更多的内存之前你调用strcat

【讨论】:

  • 你能更深入地解释一下while (*dest),和while (*dest++ = *src++)一样吗?在这种情况下,指针是如何工作的? (*dest) 得到了什么,为什么不是 while (dest)?等
  • @LancePollard 您需要取消引用指针以复制字符值本身。如果dest 不是空指针,while (dest) 将始终评估为true。当您使用dest 时,您使用的是指针的值(实际上是一个地址),而不是引用对象内部的值。
  • @LancePollard:dest 是指向的地址,*dest,即本例中的字符,指向的地址。 C 字符串以零字节 ('\0') 结束。零是假的,任何不为零的都是真的。所以while (*dest) 将循环直到找到终止的零字节。我们检查的是字符,而不是地址。
  • @LancePollard:评论已扩展。
  • 顺便说一句,这段代码清楚地说明了为什么应该小心使用这个将数据从源地址复制到目标地址的函数(和其他函数)。没有传入大小,因此该函数根据 C 标准假设 2 件事:1-src 以零字节结尾,2-dest 有足够的空间来保存复制的数据。如果这些条件中的任何一个不成立,那么我们就有“未定义的行为”
【解决方案2】:

const char *src

src 不应被函数修改,因此使用 const 正确性 将其标记为只读。

char *rdest = dest;

将原始位置保存到以后,因为要求strcat 应返回指向合并字符串的第一个元素的指针 (return rdest;)。

while (*dest)
dest++;

while 循环隐式查找空终止符。含义:找到第一个字符串的结尾,以便在这个循环之后,dest 指向该字符串的空终止符。

while (*dest++ = *src++)

这是一个常见的,虽然在 C 中令人困惑的习惯用法。(它实际上在这一行中实现了strcpy。)运算符优先级表示后缀++ 优先于前缀* 而不是赋值=

所以首先对每个指针进行求值,并将 ++ 应用于指针,而不是指向的数据。但是由于是后缀,所以指针地址的实际递增直到表达式的末尾才会发生。

* 获取此增量之前每个指针的内容,然后= 将内容从*src 复制到*dest。同样,这发生在地址增加之前。

最后,有一个针对空终止的隐式检查,因为实际上可以检查= 操作数的结果——它等同于它的左操作数,在本例中为*dest。请注意,空终止符也会被复制。

你可以用一种不那么容易混淆的方式重写这个 while 循环:

*dst = *src;
while(*src != '\0')
{
  dst++;
  src++;
  *dst = *src;
}

【讨论】:

  • srcconstness 在返回时会抛出警告“return discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers]”。或许值得一提。
  • @RobertSsupportsMonicaCellio 代码有错字,已修复。
【解决方案3】:

让我们从函数声明开始:

 char * my_strcat(char *dest, const char *src)

这个函数将返回一个指向char的指针,它的参数也是指向char的指针,它们将指向作为参数传递的每个char数组的开头。由于src是不可修改的,所以可以传递为const

这个作业:

char *rdest = dest;

声明一个指针并使其指向通过dest指针传递的数组的开头。

循环:

while (*dest)
  dest++;

您可能知道 C 中的任何字符串都以 '\0' 为空终止符,事实证明该空终止符的 ASCII 值为 0,因此您可以将其用作停止条件。

所以本质上,这个指向dest 开头的指针正在递增,直到找到字符串的结尾。

循环:

while (*dest++ = *src++)
  ;

现在dest 指针指向dest 字符串的结尾,它只是递增两个指针,并附加src 字符串中的每个字符,从src 中的第一个字符开始,到结尾dest 字符串。当添加 \0 时,这将再次成为停止条件,表达式将评估为 0,false,并且字符串将具有空终止符。

回报:

return rdest;

该指针在函数中保持不变,并指向dest 字符串的开头,该字符串现在也附加了src。这就是我们想要返回的。

【讨论】:

    【解决方案4】:

    在这段代码中要理解的关键是 C 处理字符串(以'\0' 结尾的字符数组)的方式。首先要做的是将字符串类比为一个单词,并在逐个值的基础上考虑它。

    函数的dest 参数表示指向目标字符串第一个字符的指针。要在dest 字符串之后添加更多字符,我们需要到达它的'\0' 终止符,因为这是第二个字符串所在的位置。这就是这个循环的目的:

    while (*dest)
          dest++;
    

    (*dest)条件等价于(*dest != '\0'),因为'\0'的数值为0,等价于false

    在我们到达第二个字符串需要开始的位置后,我们开始逐个字符地复制它:

    while (*dest++ = *src++)
          ;
    

    请注意,(*dest++ = *src++) 有一个“=”字符,表示它是一个赋值,而不是比较。括号内测试的值是被分配的东西,即*src。因此,只要(*src != '\0')(恰好是第二个字符串结束的地方),它就会一直持续下去。另请注意,'\0' 字符也被复制到这些分配中,这是绝对必须的,因为没有它,生成的字符串将不会被终止(因此,从技术上讲,它甚至不是有效的字符串)。

    太好了,现在我们已经将字符串复制到需要的位置,我们需要将指针返回到第一个字符。啊,但是我们已经在第一个循环中移动了指针!这就是 rdest 出现的地方,保存循环之前的初始位置,以便我们可以在最后返回它。

    【讨论】:

      【解决方案5】:

      稍微改一下rdest的作用会更清晰

      char * my_strcat(char *dest, const char *src)
      {
          char *workdest = dest;
      
          while (*workdest) workdest++;
          while (*workdest++ = *src++);
          return dest;
      }
      

      现在我们使用工作指针来迭代并返回原始目标

      是否对原始两部分(src 和 目的地)?就像在 JS 中一样,如果你连接 2 个字符串,它会为 第三个字符串结合了两者,所以你有双倍的内存。 在这个 C 实现中如何避免这种情况(如果是的话)?

      这个版本(以及标准库strcat)不分配任何内存,调用者必须确保dest是可写的,并且足够大以容纳连接的字符串

      你需要编写另一个版本的函数:

      char * my_strcat_s(char *dest, const char *src)
      {
          size_t destlen = strlen(dest);
          char *workdest = malloc(destlen + strlen(src) + 1);
      
          if(workdest)
          {
              strcpy(workdest, dest);
              strcpy(workdest + destlen, src);
          }
          return workdest;
      }
      

      但释放分配的内存是程序员的责任

      【讨论】:

        【解决方案6】:

        string 只是chars 的数组(缓冲区)。基本上,一个 8 位 unsigned ints 的数组。数组中的最后一个元素是'\0'。实际的数组可能比占据它的字符串大得多,并且 strcat 确实要求dest 足够大以同时包含dest 字符串和source 字符串。 strcat 不是像高级语言那样的即用型方法。它的用例如下所示:

        1. char* buffer = malloc(strlen(string1) + strlen(string2) +1) 创建一个足以容纳两个字符串的缓冲区。
        2. strpy(buffer, string1)将第一个字符串复制到缓冲区中
        3. strcat(buffer, string2) 将第二个字符串附加到第一个字符串结束的缓冲区中。

        ++-- 运算符允许将指针用作枚举器。将它们视为.next().prev()。这里需要注意的是,它们在移动枚举器之前返回(或接受)该值。这在这里很关键,这基本上就是让 C 变得如此困难的原因 ;) 如果你想在更高级别重新创建它,它将是 getAndNext()setAndNext()

        * 是一个访问器,双向工作,所以它是枚举器的getValue()setValue()

        第一个块只是跳过 dest 缓冲区,直到它到达其中字符串的末尾 - 但不是缓冲区的末尾。

        while (*dest)
            dest.next();
        

        伪代码为:

        while (dest.get() != '\0')
            dest.next();
        

        这是因为\0int 含义中的实零,而int 零是布尔含义中的false。任何非零都是true。这意味着 -1、42 和 'A'true 和 1 一样。所以在 C 中,我们只需跳过 != 0,这与在具有真正布尔值的语言中编写 != false 一样毫无意义。

        while (*dest++ = *src++)
          ;
        

        可以重述为:

        while (dest.setAndNext(src.getAndNext()) != '\0')
        

        或没有复合:

        char value;
        do
        {
            dest.set(src.get());
            value = src.get();
            src.next();
            dst.next();
        }
        while (value != '\0');
        

        这是因为在 C 中赋值是有值的。所以(*dest++ = *src++) 最终会返回被复制的字符。这就像一个内联函数,它复制、前进然后返回复制的内容。

        指针可以合法地指向数组之外。这就像枚举器已经到了尽头,没有更多了。最大的区别在于高级枚举器可以并且会告诉您(通过异常),而指针将继续运行,即使它不再有意义。这就是为什么 srcdest 指针都被多次 ++ed 的原因,但我们不在乎,因为我们已经注意在此之后不再使用它们。

        rdest 只是保存缓冲区开始的位置。我们不能返回dest,因为那个枚举数已经用完了,现在它在字符串的末尾,而我们需要返回开头。 “r”可能代表“return”,因为这个变量的整个点都是要返回的。

        【讨论】:

          猜你喜欢
          • 2011-04-19
          • 2023-01-10
          • 1970-01-01
          • 2018-10-21
          • 2021-12-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多