【问题标题】:Why can't I copy an array by using `=`?为什么我不能使用 `=` 复制数组?
【发布时间】:2009-06-20 19:46:32
【问题描述】:

我开始通过阅读 K&R 并完成一些练习来学习 C。经过一番折腾,我终于用下面的代码完成了练习1-19:

/* reverse: reverse the character string s */
void reverse(char s[], int slen)
{
  char tmp[slen];
  int i, j;

  i = 0;
  j = slen - 2;    /* skip '\0' and \n */

  tmp[i] = s[j];
  while (i <= slen) {
    ++i;
    --j;
    tmp[i] = s[j];
  }

  /* code from copy function p 29 */
  i = 0;
  while ((s[i] = tmp[i]) != '\0')
    ++i;

}

我的问题是关于将tmp 字符数组复制到s 的最后一段代码。为什么简单的s = tmp; 不起作用?为什么必须逐个索引地遍历数组复制?

【问题讨论】:

  • K&R 没有使用您的解决方案 - 他们为 C89 而不是 C99 编写,但您的解决方案使用了仅在 C99 中添加的 VLA(可变长度数组)。此外,不能保证字符串中有换行符 - AFAIK。并且按照惯例,字符串的长度已经排除了终端空值。这意味着您的“-2”可能不正确。第一个 while 循环中的条件应使用 '
  • 感谢您的洞察力。你是对的,我今天早上在考虑 -2,因为那是我昨晚凌晨 3 点为了让它工作而做的一个 hack。我可能应该在第一个 while 循环中放一个 if (s[i] != '\n' || s[i] != '\0') 。
  • 旧评论,但 (s[i] != '\n' || s[i] != '\0') 始终为真。
  • 没有合乎逻辑的答案 - 完全有可能将这样的功能添加到语言中 - 实际上我目前正在使用修改后的 gcc 编译器,即使类型不兼容也接受数组和结构分配(因为谁有时间写愚蠢的演员表)。
  • 你甚至可以做类似char arr[20] = *p; 的事情,因为*p 是一个左值。更改花了我大约 5 分钟的时间 - 我只添加了一个 if

标签: c arrays


【解决方案1】:

也许我只是又老又脾气暴躁,但我看到的其他答案似乎完全没有抓住重点。

C 不做数组赋值,句号。与其他一些语言不同(例如,PL/1;Pascal 及其许多后代——Ada、Modula、Oberon 等),您不能通过简单的赋值将一个数组分配给另一个数组。 C 也没有真正的字符串类型。它只有字符数组,如果不使用循环或函数调用,就不能复制字符数组(就像复制任何其他类型的数组一样)。 [字符串字面量并不真正算作字符串类型。]

复制数组的唯一时间是当数组嵌入到结构中并且您执行结构分配时。

在我的 K&R 第 2 版副本中,练习 1-19 要求函数 reverse(s);在我的 K&R 第 1 版副本中,它是练习 1-17 而不是 1-19,但提出了相同的问题。

由于在这个阶段还没有涉及到指针,所以解决方案应该使用索引而不是指针。我相信这会导致:

#include <string.h>
void reverse(char s[])
{
    int i = 0;
    int j = strlen(s) - 1;
    while (i < j)
    {
        char c = s[i];
        s[i++] = s[j];
        s[j--] = c;
    }
}

#ifdef TEST
#include <stdio.h>
int main(void)
{
    char buffer[256];
    while (fgets(buffer, sizeof(buffer), stdin) != 0)
    {
        int len = strlen(buffer);
        if (len == 0)
            break;
        buffer[len-1] = '\0';  /* Zap newline */
        printf("In:  <<%s>>\n", buffer);
        reverse(buffer);
        printf("Out: <<%s>>\n", buffer);
    }
    return(0);
}
#endif /* TEST */

使用 -DTEST 编译它以包含测试程序,而无需定义函数 reverse()

使用问题中给出的函数签名,您可以避免每行输入两次调用strlen()。注意fgets() 的使用——即使在测试程序中,使用gets() 也是一个坏主意。 fgets()gets() 相比的缺点是 fgets() 不会像 gets() 那样删除尾随换行符。 fgets() 的好处是您不会遇到数组溢出,并且您可以在遇到换行符之前判断程序是否找到了换行符或者是否空间(或数据)不足。

【讨论】:

    【解决方案2】:

    您的tmp 数组已在stack 上声明,因此当您的方法完成时,用于保存值的内存将因scoping 而被释放。

    s = tmp 表示s 应该指向与tmp 相同的内存位置。这意味着当tmp 被释放时,s 仍将指向一个现在可能无效的、已释放的内存位置。

    这种类型的错误称为dangling pointer

    编辑:这不是这个答案的 cmets 中指出的悬空修饰符。问题是 s = tmp 只会改变参数指向的内容,而不是实际传递的数组。

    此外,您可以一次执行反向操作,而无需在内存中分配整个数组,只需将值一个一个交换到位:

    void reverse(char s[], int slen) {
        int i = 0;        // First char
        int j = slen - 2; // Last char minus \n\0
        char tmp = 0;     // Temp for the value being swapped
    
        // Iterate over the array from the start until the two indexes collide.
        while(i < j) {
            tmp = s[i];  // Save the eariler char
            s[i] = s[j]; // Replace it with the later char
            s[j] = tmp;  // Place the earlier char in the later char's spot
            i++;         // Move forwards with the early char
            j--;         // Move backwards with the later char
        }
    }
    

    【讨论】:

    • +1 - 因为与其他答案的负载不同,您实际上说 tmp 是一个数组,而 not 是一个指针。如果我是他,其他答案会让我感到困惑,想“嗯,他们在谈论什么指针??tmp 是一个数组!”
    • 不把 tmp 说成一个数组就像让他更长时间地处于混乱状态。 tmp 是指向数组的指针,这是正确的定义。他学得越早越好,不是吗?
    • !litb temp 绝对是一个指针。它是指向数组第一个字符的指针。了解如何将 [] 运算符编写为宏需要您完全理解这一点。
    • 他的代码中没有悬空指针,将 tmp 赋值给 s 只是修改了指向数组的指针的本地副本。函数返回时,原来的s指针和数组完全没有动过。
    • Elitecoder,其定义是一个数组(可变长度数组)。指向数组(那个数组)的指针的定义是 char(ptmp)[slen] = &tmp;如果我们将 tmp 称为数组,我不明白为什么它会“让他感到困惑”,因为它*是一个数组。 sizeof(tmp) 会给你 slen。不是数组的是 s。真相可能令人困惑,但这并不意味着我们应该通过说错话来使事情变得更简单——只会让他感到困惑!请在取笑我之前先阅读标准
    【解决方案3】:

    因为 s 和 tmp 都是内存地址。如果你 s = tmp,两个指针都会指向同一个数组。

    假设我们有

    char s[] ="ab"; 
    
    /*
    * Only for explanatory purposes.
    * 
    */
    void foo(char s[]){ 
        char tmp [] = "cd";
        s= tmp;
     }
    
    foo(s);
    

    在 s=tmp 之后,你会拥有

    s[0] : 'c'
    s[1] : 'd'
    s[2] : '\0'
    

    即使两个数组具有相同的数据,tmp 的变化也会影响它们,因为两个数组实际上是相同的。它们都包含相同内存地址中的数据。所以通过改变 tmp 数组的任意位置,或者破坏 tmp 数组,s 都会受到同样的影响。

    通过遍历数组,您所做的就是将一段数据从一个内存地址移动到另一个内存地址。

    在我的 K&R 副本中,第 4 章解释了指针。快速浏览第一页可能会有所帮助。

    【讨论】:

    • Ben S:感谢您的反馈。我已经重新安排了我的答案。
    • 你不能做作业 s = tmp;数组不能在 C 中赋值。指针可以,但指针不是数组。 (不过,数组的名称在用作值时被提升为指向其第一个元素的指针,所以如果 's' 是一个指针,那么你是对的。
    • 我推荐这个讨厌鬼stackoverflow.com/questions/423823/…
    • @litb: 现在我知道什么是讨厌鬼了:)。
    • "s 将在 foo 结束时指向一个堆栈变量,这将产生未定义的结果。"那是不对的。只有 foo 的变量指向“cd”。调用者的 s 没有被修改。它仍将指向 ab。试试看!
    【解决方案4】:

    为了结束这里的讨论,还有另外两种可能的反转为字符串的方法:

    void reverse(char string1[], char string2[])
    {
      int i = 0, len = 0;
    
      while(string2[len] != '\0')   // get the length of the string
          len++;
    
      while(len > 0)
      {
        string1[i] = string2[len-1]; // copy the elements in reverse
        i++;
        len--;
      }
      string1[i] = '\0'; // terminate the copied string 
    }
    

    或递归:

    void reverse (const char *const sPtr)
    {
      //if end of string
      if (sPtr[0] == '\0')
      {
        return;
      }
      else  //not end of the string...
       {
        reverse(&sPtr[1]);  //recursive step
        putchar(sPtr[0]);   //display character
       }
    }
    

    【讨论】:

    • 第二种解决方案在不更改字符串的情况下以相反的顺序显示字符串 - 而练习要求字符串在不打印的情况下反转。如果第一个解决方案使用 strlen() 并且下标 '[len-1]' 避免重复减法(通过在开始循环之前减少 len),那么第一个解决方案会更好。
    【解决方案5】:

    因为 tmp 是一个指针,你需要得到一个副本,而不是一个“链接”。

    【讨论】:

    【解决方案6】:

    在 s=tmp 的情况下,tmp 的值也是数组的起始地址,将被复制到 s。

    这样 s 和 tmp 都会指向内存中的同一个地址,我认为这不是目的。

    干杯

    【讨论】:

      【解决方案7】:

      尝试进行实验,看看当你这样做时会发生什么:

      void modifyArrayValues(char x[], int len)
      {
          for (int i = 0; i < len; ++i)
              x[i] = i;
      }
      
      void attemptModifyArray(char x[], int len)
      {
          char y[10];
          for (int i = 0; i < len; ++i)
              y[i] = i;
          x = y;
      }
      
      
      int main()
      {
          int i = 0;
          char x[10];
          for (i = 0; i < 10; ++i)
              x[i] = 0;
      
          attemptModifyArray(x, 10);
          for (i=0; i < 10; ++i)
              printf("%d\n", x[i]); // x is still all 0's
      
          modifyArrayValues(x, 10);
          for (i=0; i < 10; ++i)
              printf("%d\n", x[i]); // now x has 0-9 in it
      }
      

      当您直接在attemptModifyArray 中修改数组时会发生什么,您只是覆盖了数组x 的地址的本地副本。当您返回时,原始地址仍在main 的x 副本中。

      当您修改modifyArrayValues 中数组中的值时,您正在修改实际数组本身,该数组的地址存储在modifyArrayValues 的本地副本x 中。当您返回时,x 仍然保留同一个数组,但您已经修改了该数组中的值。

      【讨论】:

        【解决方案8】:

        这个线程中有一个关于数组和指针的有趣子线程 我在维基百科上找到了this link,上面有一个特殊的代码 sn-p,它显示了“橡皮泥”C 的含义!

        /* x designates an array */
        x[i] = 1;
        *(x + i) = 1;
        *(i + x) = 1;
        i[x] = 1; /* strange, but correct: i[x] is equivalent to *(i + x) */
        

        当然,在 C 语言中更令人困惑的是我可以这样做:

        unsigned int someval = 0xDEADD00D;
        char *p = (char *)&someval;
        
        p[2] = (char)0xF0;
        

        因此,指针和数组的互换性在 C 语言中似乎根深蒂固,几乎是有意为之。
        其他人怎么看?

        ---原帖---
        s 和 tmp 都是指针,因此执行 s = tmp 只会使 s 指向 tmp 在内存中的地址。
        您概述的另一个问题是 tmp 是一个局部变量,因此当它超出范围时(即函数返回时)将变为“未定义”。

        确保你彻底掌握这三个概念,你不会走错路

        1. 范围
        2. 栈和堆的区别
        3. 指针

        希望有所帮助并继续前进!

        【讨论】:

          【解决方案9】:

          一个非常直接的答案是 - s 和 tmp 都是指向内存位置的指针,而不是数组本身。 换句话说,s 和 tmp 是存储数组值的内存地址,而不是值本身。 访问这些数组值的常用方法之一是使用 s[0] 或 tmp[0] 等索引。

          现在,如果您尝试简单地复制 s = tmp,则 tmp 数组的内存地址将被复制到 s。这意味着,原来的 s 数组将丢失,甚至 s 内存指针现在将指向 tmp 数组。

          您将在适当的时候很好地理解这些概念,因此请继续阅读本书。 我希望这个基本解释会有所帮助。

          【讨论】:

          • 我推荐这个讨厌鬼stackoverflow.com/questions/423823/…
          • -1: tmp 是一个数组,而不是一个指针。数组不是指针,尽管它们密切相关。不幸的是,只有 10k 用户可以看到指向的信息。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-03-08
          • 2014-08-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-30
          • 1970-01-01
          相关资源
          最近更新 更多