Подтвердить что ты не робот

Каков тип указателя на массив переменной длины в C?

Здесь короткая программа на языке C, которая запрашивает у пользователя число, создает массив переменных длины этого размера и затем использует арифметику указателя для перехода по выделенным элементам:

#include <stdio.h>

int main() {
    /* Read a size from the user; inhibits compiler optimizations. */
    int n;
    scanf("%d", &n); // Yes, I should error-check. :-)

    /* We now have a VLA. */
    int arr[n];

    /* What is the type of &arr? */
    void* ptr = (&arr) + 1;

    /* Seems like this skipped over things properly... */
    printf("%p\n", arr);
    printf("%p\n", ptr);
}

Вы можете попробовать на ideone, если хотите. Вывод предполагает, что строка

void* ptr = (&arr) + 1;

принимает адрес arr и, с точки зрения размера, выполняет шаги по всем элементам n в массиве переменной длины.

Если бы это не был массив переменной длины, мне было бы совершенно удобно, как это работает. Компилятор будет знать тип arr (он будет int (*) [K] для некоторой константы K), поэтому, когда мы добавим его в &arr, он может пропустить правильное количество байтов.

Это ясно, как во время выполнения мы могли бы оценить (&arr) + 1. Компилятор фиксирует размер arr где-то в стеке, и когда мы добавляем его в (&arr), он знает, чтобы загрузить этот размер, чтобы вычислить, сколько байтов пропустить.

Однако я не знаю, что говорит язык типа выражения &arr. Назначен ли какой-то статический тип, который указывает на массив переменной длины (что-то вроде int (*) [??])? Говорит ли спецификация "тип выражения int (*) [K], где K - какой размер назначается массиву во время выполнения?" Не разрешает ли спецификация обращение к адресу массива переменной длины, и компилятор просто разрешает его?

4b9b3361

Ответ 1

С помощью VLA оператор sizeof не является константой времени компиляции. Он оценивается во время выполнения. В вашем вопросе тип &arr есть int (*)[n]; - указатель на массив значений n типа int, где n - значение времени выполнения. Следовательно, как вы заметили, &arr + 1 (круглые скобки не нужны, кроме круглых скобок, отметив, что скобки не нужны) - это начало массива после arr - адрес sizeof(arr) байтов превышает значение arr.

Вы можете напечатать размер arr; он даст вам соответствующий размер (printf() модификатор z). Вы можете распечатать разницу между &arr + 1 и arr, и вы получите размер как ptrdiff_t (printf() модификатор t).

Следовательно:

#include <stdio.h>

int main(void)
{
    int n;
    if (scanf("%d", &n) == 1)
    {
        int arr[n];

        void* ptr = (&arr) + 1;

        printf("%p\n", arr);
        printf("%p\n", ptr);
        printf("Size: %zu\n", sizeof(arr));
        printf("Diff: %td\n", ptr - (void *)arr);
    }
    return 0;
}

И два прогона прогона (название программы vla59):

$ vla59
23
0x7ffee8106410
0x7ffee810646c
Size: 92
Diff: 92
$ vla59
16
0x7ffeeace7420
0x7ffeeace7460
Size: 64
Diff: 64
$

Нет повторной компиляции, но sizeof() является правильным при каждом запуске программы. Он рассчитывается во время выполнения.

В самом деле, вы можете даже использовать цикл с разным размером каждый раз:

#include <stdio.h>

int main(void)
{
    int n;
    while (printf("Size: ") > 0 && scanf("%d", &n) == 1  && n > 0)
    {
        int arr[n];

        void* ptr = (&arr) + 1;

        printf("Base: %p\n", arr);
        printf("Next: %p\n", ptr);
        printf("Size: %zu\n", sizeof(arr));
        printf("Diff: %td\n", ptr - (void *)arr);
    }
    return 0;
}

Пример прогона vla11:

$ vla11
Size: 23
Base: 0x7ffee3e55410
Next: 0x7ffee3e5546c
Size: 92
Diff: 92
Size: 16
Base: 0x7ffee3e55420
Next: 0x7ffee3e55460
Size: 64
Diff: 64
Size: 2234
Base: 0x7ffee3e53180
Next: 0x7ffee3e55468
Size: 8936
Diff: 8936
Size: -1
$