Объяснение того, как указатели и многомерные массивы работают в C - PullRequest
0 голосов
/ 21 апреля 2020

Я пытаюсь понять следующий код.

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

void print2(int (* a)[2]) {
    int i, j;
    for (i = 0; i < 3; i++ ) {
        for (j = 0; j < 2; j++ ) {
            printf("%d", a[i][j]);
        }
    printf("\n");
    }
}

void print3(int (* a)[3]) {
    int i, j;
    for (i = 0; i < 2; i++ ) {
        for (j = 0; j < 3; j++ ) {
            printf("%d", a[i][j]);
        }
    printf("\n");
    }
}

int main() {
    int a[] = { 1, 2, 3, 4, 5, 6 };
    print2((int (*)[2]) a);
    print3((int (*)[3]) a);
    return 0;
}

Запуск кода возвращает следующий вывод в консоли:

12
34
56
123
456

Моя проблема в том, что я не понимаю, где эти числа родом из. У меня проблемы с пониманием того, что на самом деле происходит в этом коде. В частности, я не уверен, что это значит:

int( (* a)[2])

Я надеюсь, что кто-нибудь сможет объяснить мне этот код, потому что я действительно хочу понять, как работают указатели и многомерные массивы в C.

Ответы [ 4 ]

1 голос
/ 21 апреля 2020

Код пытается переосмыслить массив int a[6], как если бы он был int a[3][2] или int a[2][3], то есть как если бы массив из шести int в памяти был тремя массивами из двух intprint2) или два массива из трех intprint3).

Хотя стандарт C не полностью определяет преобразования указателей, можно ожидать, что он будет работать в общих реализациях C (в основном потому, что этот вид преобразования указателей используется в существующем программном обеспечении, что обеспечивает мотивацию для его поддержки компиляторами.)

В (int (*)[2]) a, a служит указателем на свой первый элемент. 1 Приведение преобразует этот указатель в int в указатель на массив из двух int. Это преобразование частично определено C 2018 6.3.2.3 7:

  • Поведение не определено, если выравнивание a не подходит для типа int (*)[2]. Однако компиляторы, которые имеют более строгое выравнивание для массивов, чем для их типов элементов, встречаются редко, и ни один практический компилятор не имеет более строгого выравнивания для массива из шести int, чем для массива из двух или трех int, поэтому на практике этого не происходит.
  • Когда результирующий указатель конвертируется обратно в int *, он будет сравниваться с исходным указателем.

Последнее свойство говорит нам, что результирующий указатель содержит всю информацию об исходном указателе, так как он должен содержать информацию, необходимую для восстановления исходного указателя. Он не говорит нам, что результирующий указатель фактически указывает на память, где находится a.

Как отмечалось выше, обычные реализации C позволяют это. Я знаю, что версии Apple для G CC и Clang поддерживают это изменение формы массивов, хотя я не знаю, была ли эта гарантия добавлена ​​Apple или есть в вышестоящих версиях.

Учитывая, что (int (*)[2]) пройдено до print2 как a, тогда a[i][j] относится к элементу j массива i. То есть a указывает на массив из двух int, поэтому a[0] - это массив, a[1] - это массив из двух int, который следует за ним в памяти, а a[2] - это массив из двух int после этого. Тогда a[i][j] является элементом j выбранного массива. В действительности, a[i][j] в print2 равно a[i*2+j] в main.

Обратите внимание, что никакие правила псевдонимов не нарушаются, так как a[i][j] не обращается к массивам: a является указателем, a[i] - это массив, но к нему нет доступа (он автоматически преобразуется в указатель, согласно сноске 1 ниже), а a[i][j] имеет тип int и обращается к объекту с эффективным типом int, поэтому правила псевдонимов C в C 2018 6.5 7 удовлетворены.

Сноски

1 Это потому, что когда массив используется в выражении, он автоматически преобразуется в указатель на его первый Элемент, за исключением случаев, когда он является операндом sizeof, является операндом унарного & или строковым литералом, используемым для инициализации массива.

1 голос
/ 21 апреля 2020

Было бы намного легче понять, если бы вы разбили это и поняли вещи. Что если вы передадите весь массив, чтобы сказать функцию print4, которая перебирает массив и печатает элементы? Как бы вы передали массив такой функции.

Вы можете написать что-то вроде

print4( (int *) a); 

, которое можно упростить и записать как print4(a);

Теперь в вашем случае, выполнив print2((int (*)[2]) a);, вы на самом деле создаете указатель на массив из двух элементов типа int. Таким образом, теперь a является указателем в массиве из двух элементов, т.е. каждое приращение к указателю будет увеличивать смещение на 2 int с в массиве a

Представьте, что приведенное выше моделирование выполнено, ваш оригинал массив становится двухмерным массивом из 3 строк с 2 элементами в каждой. Вот как ваш элемент print2() перебирает массив a и печатает int s. Представьте себе функцию print2a, которая работает, беря локальный указатель на a и увеличивая на каждой итерации точку до следующих двух элементов

void print2a(int (* a)[2]) {
    int (* tmp)[2] = a;
    for( int i = 0; i < 3; i++ ) {
        printf("%d%d\n", tmp[0][0], tmp[0][1] );
        tmp++;
    }
}

То же самое относится к print3(), в котором вы передаете указатель на массив 3 int с, который теперь моделируется как двумерный массив из 2 строк с 3 элементами в нем.

1 голос
/ 21 апреля 2020

TL; DR

Этот код содержит некорректные и бессмысленные хаки. Из этого кода мало что можно узнать.

Ниже приведено подробное объяснение.


Прежде всего, это простой одномерный массив, который печатается разными способами.

Эти строки являются строго говоря ошибками:

print2((int (*)[2]) a);
print3((int (*)[3]) a);

В обоих случаях происходит недопустимое преобразование указателя, поскольку a имеет тип int[6] и указатель на массив a должен быть int (*)[6]. Но операторы печати неверны и по-другому: a при использовании в выражении, подобном этому, «разлагается» на указатель на первый элемент. Таким образом, код преобразуется из int* в int(*)[2] et c, что недопустимо.

Теоретически эти ошибки могут вызывать такие вещи, как неправильный доступ, представления прерываний или оптимизацию кода. На практике он, скорее всего, будет «работать» на всех основных компьютерах, даже если код полагается на неопределенное поведение.


Если мы проигнорируем эту часть и предположим, что void print2(int (*a)[2]) получает допустимый параметр, то a - указатель на массив типа int[2].

a[i] - арифметика указателя c на такой тип, что означает, что каждый i будет соответствовать int[2], и если мы записав a++, указатель будет прыгать вперед sizeof(int[2]) в памяти (вероятно, 8 байт).

Поэтому функция использует этот арифметический указатель c для a[i], чтобы получить номер массива i, затем выполните [j] в этом массиве, чтобы получить элемент в этом массиве.


Если у вас фактически был двумерный массив для начала, тогда может иметь смысл объявить функции как:

void print (size_t x, size_t y, int (*a)[x][y])

Хотя это будет раздражать, так как нам придется обращаться к массиву как (*a)[i][j]. Вместо этого мы можем использовать тот же трюк, что и в вашем коде:

void print (size_t x, size_t y, int (*a)[x][y])
{
  int(*arr)[y] = a[0];
  ...
  arr[i][j] = whatever; // now this syntax is possible

Этот трюк также использует арифметику указателя c на указателе массива arr, затем отменяет ссылки на массив, на который указывает.

Связанное чтение, которое объясняет эти понятия на примерах: Правильное размещение многомерных массивов

1 голос
/ 21 апреля 2020

void print2(int (*a)[2]) { /*...*/ }

внутри функции print2 a - указатель на массивы из 2-х значений

void print3(int (*a)[3]) { /*...*/ }

внутри функции print3 a - указатель на массивы из 3-х дюймов

int a [] = {1, 2, 3, 4, 5, 6};

внутри функции main a представляет собой массив из 6-ти точек.
В большинстве контекстов (включая контекст вызова функции) a преобразуется в указатель на первый элемент: значение типа " указатель на int ".

Типы" указатель на int "," указатель на массив из 2/3 целых "несовместимы, поэтому вызов любой из функций с print2(a) (или print3(a)) силами Диагностика c от компилятора.

Но вы используете приведение, чтобы сказать компилятору: «Не выдавайте никакой диагностики c. Я знаю, что делаю»

   print3(a); // type of a (after conversion) and type of argument of print3 are not compatible
// print3((cast)a); // I know what I'm doing
   print3((int (*)[3])a); // change type of a to match argument even if it does not make sense
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...