关于 C 中数组的概念(基本)问题

Conceptual (Basic) Questions about Arrays in C

我真的是 C 编程的新手,对于我们的家庭作业之一,我们基本上必须从存储卡中恢复 jpg 图像。我的代码是这样的:

#include <cs50.h>       
#include <stdio.h>
#include <stdlib.h>

// typedef uint8_t BYTE; 
// #define BUFFER_SIZE 512

int main(int argc, char *argv[])
{
    // ensure proper usage
    if (argc != 2)
    {
        fprintf(stderr, "Usage: ./recover image\n");
        return 1;
    }

    //opens the memory card for reading
    FILE* mem = fopen(argv[1], "r");
    if (mem == NULL)
    {
        fprintf(stderr, "Could not open %s.\n", argv[1]);
        return 1;
    }

    unsigned char buffer[512];
    FILE* img = NULL;
    int found = 0;
    int count = 0;

    while (fread(&buffer, 512,1,mem) == 1)
    {
        if (buffer[0] == 0xff && buffer[1] == 0xd8 && buffer[2] == 0xff && (buffer[3] & 0xe0) == 0xe0)
        {
            if (found == 1)
                fclose(img);
            else
                found = 1;


        char filename[8];
        sprintf(filename, "%03d.jpg", count);
        img = fopen(filename, "a");
        count++;
        }

        if (found ==1)
        {
            fwrite(&buffer,512,1,img);
        }
    }

    fclose(mem);
    fclose(img);
    return 0;
}

// #1 为什么它的 &buffer 和 not buffer 无关紧要 // #2 从概念上讲,附加到数组是如何工作的

这行得通,但我只是想知道:

  1. 当我使用 unsigned char buffer[512] 时,就像我在这种情况下所做的那样,它可以工作,但是当我尝试使用 char buffer[512] 时,它由于分段错误而中断,所以我只是想知道 char 和 unsigned char 数组在内存方面有什么区别?

  2. 我在概念上有点困惑为什么这会起作用,因为我一直认为数组具有固定大小,但在这种情况下:

    char filename[8]; sprintf(filename, "%03d.jpg", count); img = fopen(filename, "a"); count++;

我不确定发生了什么,因为我在内存块上打开一个数组,然后...到那个数组?

一部分代码是在家庭作业的视频演练中提供给我们的,现在我已经完成了它,我只是对一些概念有点困惑 - 感谢我能得到的任何帮助!

1 why does it not matter if its &buffer and not buffer // #2 how does appending to an array work, conceptually

好的,我们开始吧:C 中的数组在很多方面在概念上是一个相当混乱的东西。它们与 charint 等原始类型有一些共同特征,因为它们本质上是值,它们分配在堆栈上,当它们超出范围时会自动释放,所以你不必担心释放它们。但是,它们与指针共享它们的接口;使用方括号访问数组的第一个、第二个、第三个等元素的语法与访问堆中缓冲区的第一个、第二个、第三个等元素的语法相同一个指针。这本身并不一定非常混乱。如果该接口在两种情况下都有意义,那么有两个不同的类型共享一个相似的接口并不是那么不合理。

然而:数组有一小部分 "magic";如果您将数组赋值给任何东西——无论是变量,还是传递给函数的参数——它将自动转换为指向数组第一个元素的指针。

char foo[512];   // an array of size 512
char *bar = foo; // a *pointer* to the first element in the array

在 "close-to-the-metal" 这样的 "close-to-the-metal" 语言中,这种自动转换有点令人惊讶,因为 C 语言通常需要您准确地拼出要执行的操作;此外,指针和数组如此可互换的事实使得很容易假设数组实际上 指针,而指针实际上 数组。但是,它们并不相同,一个明显的区别是您在此处提出的问题的答案:为什么无论您通过 buffer 还是 &buffer,您对 fread 的调用都有效?好吧,假设您有以下变量:

int foo;
char bar[8];
int baz;

假设一台机器int的大小是4,你可以想象这些在内存中的布局是这样的:

-------------------------------------
||f¦o¦o¦ ||b¦a¦r¦ ¦ ¦ ¦ ¦ ||b¦a¦z¦ ||
||1¦2¦3¦4||1¦2¦3¦4¦5¦6¦7¦8||1¦2¦3¦4||
-------------------------------------

看着这个视觉抽象,你能看到一些东西; bar 所在的地址(显然)与其第一个元素 b 所在的地址相同,因此,当您将 bar 传递给采用 char * 的对象时,它是转换为指向其第一个元素的指针,该地址与数组本身的地址相同。 这就是为什么,如果您同时记录数组和数组的地址,您会得到两次相同的值:

char foo[512];
printf("%p %p\n", (void *)foo, (void *)&foo); // these will both log the same address

相比之下,如果foo是一个指针而不是一个数组(也就是说,它的类型是char *而不是char []),你实际上会得到 foo&foo 不同的 值,并将 &foo 传递给像 fread 这样的函数将无法正常工作。这是因为与数组不同,指针不代表数据本身,而是可以被视为指示您指向存储在其他位置的数据的路标,因此,它的地址是 not数据的地址。

这个魔法存在的原因基本上是为了方便,这样你就可以像使用指针一样使用数组。但是,这会产生新的陷阱,您必须提防。例如,您不能 return 来自函数的数组:

char *foo() {
    char bar[4] = "Bar";

    return bar; // This won't work. Don't do this!
}

你看到这里的问题了吗?基本上,一旦我们尝试 return bar,它就会变成指向数组中第一个元素的指针。然而,一旦 foo() returns,bar 数组就会超出范围并被释放。调用者现在有一个指向垃圾内存的指针。这是特别阴险的,因为事情 似乎仍然有效 ;数组以前占用的内存将继续包含数组具有的任何值,直到其他东西决定覆盖该内存,并且不能保证这迟早会发生。这种不确定性导致 未定义的行为 ,这可能是非常微妙且难以追踪的错误的来源。

因此,总而言之:无论是否包含与号 (&),传递数组都会得到相同的值,这仅仅是因为数组的工作方式。您可以并且可能应该将数组当作指针来传递,不带“&”符号。但是,您仍应始终注意处理的是指针还是数组,以避免导致未定义的行为。

This works, but I was just wondering:

  1. When I use an unsigned char buffer[512], as I did in this case, it works, but when I try it with a char buffer[512], it breaks due to segmentation fault, and so I was just wondering what' the difference between a char and an unsigned char array in terms of memory?

char 更改为 unsigned char 应该不会导致崩溃。它在哪条线上崩溃?

  1. I'm a little confused conceptually about why this would work, because I've always thought that arrays have a fixed size and yet in this case:

    char filename[8]; sprintf(filename, "%03d.jpg", count); img = fopen(filename, "a"); count++;

这个数组有固定的大小。您的 sprintf 语句生成一个包含以下内容的字符串:三个数字、一个句点,然后是字符 "jpg"。那是七个字符,加上 C 字符串所需的空终止符就是八个字符。

做这样的事情时非常小心。如果您不小心尝试写入一个大于数组大小的字符串,C 不会阻止您,然后您将覆盖内存中数组之后发生的任何事情。这会导致未定义的行为,这意味着无法保证会发生什么。你的程序可能会崩溃。您可能会悄悄地破坏程序中其他地方的一些数据。虫洞可能会打开到第五维度,导致地球被邪恶的食人 space 三叶虫入侵。几乎任何事情都会发生,所以在使用缓冲区时要小心。

实际上,在这个程序中有一种方法可以做到这一点;如果 count 变为 1000 或更大,sprintf 将给它多于三位数,即使你只要求三位数。这将导致缓冲区溢出。在生产应用程序中,您需要 1) 添加检查以确保 count 永远不会大于 999,2) 检查 count 的值以确定合适的大小对于字符串,而不是将其硬编码为 8,或 3) 对字符串的大小进行硬编码,以便能够保存 int 可以存储的最大值中的位数(在 Intel x86 上,那是 2147483647,它是十位数字,所以加一位作为点,三位作为扩展名,再加一位作为终止符,你会使字符串长 15 个字节)。

编辑

我最初草草写下的这个问题的答案被一些人误解了,所以现在我有更多的时间,我正在重写它,使其尽可能清晰和详细。如果您仍然不相信,请阅读 section 6 of the comp.lang.c FAQ

使用 &buffer 和 buffer 是两个不同的东西。 &buffer 给你缓冲区变量的内存地址,而使用 buffer 给你缓冲区指向的值(它的值)。 对于 fread 你应该只使用缓冲区(或 &buffer[0])而不是 &buffer.

我不明白你所说的 "appending to an array" 到底是什么意思,但我会尽力回答: 数组是固定大小的内存块,在内存中连续存储。

  1. 如果您想在不超过其大小的情况下向其附加更多数据,有很多方法可以做到这一点。一种是简单地保留指向数组下一个索引的指针(从 0 开始),每次向它存储更多数据时,指针都会随着写入的字节数增加。
  2. 如果您的数据量超过数组大小,您应该使用动态分配的数组并根据需要重新分配它。

unsigned char buffer[512]和char buffer[512]在内存中存储的完全一样。但请注意,您使用的是有符号字符,您存储的值会溢出。

char c = 0xff // Overflow
unsigned char c = 0xff // OK

但这与分段错误无关。

你问题的后半部分没看懂

When I use an unsigned char buffer[512], as I did in this case, it works, but when I try it with a char buffer[512], it breaks due to segmentation fault, and so I was just wondering what' the difference between a char and an unsigned char array in terms of memory?

None - unsigned char 不大于 signed char 或者只是普通的 char:

6.2.5 Types
...
5     An object declared as type signed char occupies the same amount of storage as a ‘‘plain’’ char object. A ‘‘plain’’ int object has the natural size suggested by the architecture of the execution environment (large enough to contain any value in the range INT_MIN to INT_MAX as defined in the header <limits.h>).

6     For each of the signed integer types, there is a corresponding (but different) unsigned integer type (designated with the keyword unsigned) that uses the same amount of storage (including sign information) and has the same alignment requirements. The type _Bool and the unsigned integer types that correspond to the standard signed integer types are the standard unsigned integer types. The unsigned integer types that correspond to the extended signed integer types are the extended unsigned integer types. The standard and extended unsigned integer types are collectively called unsigned integer types. 40)
40) Therefore, any statement in this Standard about unsigned integer types also applies to the extended unsigned integer types.

C 2011 Online Draft

只是简单地看一下代码,我没有看到任何 明显的 为什么它会在 buffer 签名与未签名时崩溃的原因,但可能有我缺少的东西。

why does it not matter if its &buffer and not buffer

坐下来放松一下,这需要一段时间...

数组表达式比较特殊:

6.3.2.1 Lvalues, arrays, and function designators
...
3     Except when it is the operand of the sizeof operator, the _Alignof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

表达式 buffer 的类型为 unsigned char [512]。当该表达式 不是 sizeof 或一元 & 运算符的操作数时,它被转换(“衰减”)为 [=41= 类型的表达式],表达式的值将是第一个元素的地址。

所以,如果你写了

fread( buffer, 512, 1, rem ); // do not use & operator here

函数fread 将接收一个指针作为其第一个参数,而不是数组对象。给定声明

unsigned char buffer[512];

以下全部正确:

Expression    Type                    "Decays" to        Value
----------    ----                    -----------        -----
    buffer    unsigned char [512]     unsigned char *    Address of first element
   &buffer    unsigned char (*)[512]  n/a                Address of array
   *buffer    unsigned char           n/a                Value of first element
 buffer[i]    unsigned char           n/a                Value of i'th element

表达式 buffer&buffer 的计算结果都是数组第一个元素的地址,但是 类型 的表达式不同 - unsigned char *unsigned char (*)[512]。在某些情况下,指向 unsigned char 的指针将不同于指向 unsigned char 数组 的指针。对于像 fread 这样的函数,他们期望指向单个元素的指针,而不是指向数组的指针。

此时你会问,“为什么是这样?”

C 派生自一种叫做 B 的更早的语言。在 B 中,像

这样的声明的结果
auto vec[10];

看起来像这样:

     +---+      +---+
vec: |   | ---> |   | vec[0] 
     +---+      +---+
                |   | vec[1]
                +---+
                 ...
                +---+
                |   | vec[9]
                +---+
       

B 会留出一个额外的存储单元作为指向数组第一个元素的(某种类型的)指针。在B中,数组下标操作a[i]被定义为*(a + i)——即给定一个起始地址a,偏移量i个元素 从该地址并取消引用结果。

Ritchie 在设计 C 时,他想保留 B 的数组语义,但他不想必须保留指向数组第一个元素的单独指针。因此,他摆脱了它 - 相反,他添加了一条规则,即任何不是 sizeof 或一元 & 的操作数的数组表达式都将转换为指针表达式,并且指针将计算到第一个元素的地址。因此,当您在 C 中声明一个数组时,例如

int vec[10];

看起来像这样:

     +---+
vec: |   | vec[0]
     +---+
     |   | vec[1]
     +---+
      ...
     +---+
     |   | vec[9]
     +---+

没有为指针预留单独的内存 - 除了数组元素本身之外没有 vec 对象。每当编译器在不是 sizeof 或一元 & 运算符的操作数的上下文中看到 vec 时,它会将其转换为等效于 &vec[0] 的表达式。这就是为什么 buffer&buffer 的计算结果相同 - 数组第一个元素的地址与整个数组的地址相同。

数组下标在 C 中的工作方式与在 B 中的工作方式相同 - a[i] == *(a + i)。只是在这种情况下,必须先将数组表达式转换(“衰减”)为指针表达式。

这仅适用于数组 - 没有其他聚合类型(例如 structunion 类型)以这种方式处理。访问 structunion 成员的机制与访问数组元素的机制不同。

how does appending to an array work, conceptually

数组在其生命周期内的大小是固定的 - “附加”到数组通常意味着写入可用或未使用的元素。例如,声明

char buf[100] = "foo";

这给了我们

     +---+
buf: |'f'| buf[0]
     +---+
     |'o'| buf[1]
     +---+
     |'o'| buf[2]
     +---+
     | 0 | buf[3]
     +---+
     | ? | buf[4]
     +---+
      ...
     +---+
     | ? | buf[99]
     +---+

元素 4 到 99 尚未写入,因此我们可以添加到字符串中:

strcat( buf, "bar" );

现在给我们

     +---+
buf: |'f'| buf[0]
     +---+
     |'o'| buf[1]
     +---+
     |'o'| buf[2]
     +---+
     |'b'| buf[3]
     +---+
     |'a'| buf[4]
     +---+
     |'r'| buf[5]
     +---+
     | 0 | buf[6]
     +---+
     | ? | buf[7]
     +---+
      ...
     +---+
     | ? | buf[99]
     +---+

buf 现在包含序列 {'f', 'o', 'o', 'b', 'a', 'r', 0},剩下 93 个元素可用。我们可以继续这样做,直到我们有一个长度为 99 个字符的字符串(为字符串终止符保留 1 个元素)。但是数组的size固定为100,无法更改。如果我们尝试将超过 100 个字符存储到 buf,将会发生的情况是这些额外的字符将存储在数组的边界之外并覆盖其他对象。根据被破坏的内容,您可能会得到错误的数据,您的程序可能会崩溃,您可能会分支到不同的例程,或者您的代码 似乎可以正常工作 。这就是代码片段中发生的情况,在该代码片段中,您尝试创建一个比缓冲区大小更长的文件名来保存它。缓冲区后面的内存中的内容并不“重要”,因此代码 似乎 可以正常工作。

在 C99 中引入了一种叫做 可变长度数组 的东西,其中数组的大小直到运行时才确定:

int n = get_some_value_at_runtime();
int array[n];

但是,与常规的固定大小数组一样,您无法在其整个生命周期内更改 VLA 的长度。 “可变长度”只是意味着每次创建数组实例时,它的大小都可以不同。

如果您需要可以根据需要进行物理增长或收缩的存储,则必须使用动态内存管理例程(malloccallocreallocfree).