LINUX.ORG.RU

Тип многомерного массива.

 , ,


0

2

Привет ЛОР.

Есть недопонимание того, как работает C (равно как и С++). Проясните, пожалуйста. Собственно код:

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

int main() {
    int64_t *p = (int64_t*)malloc(24*sizeof(int64_t));
    for(size_t i = 0; i < 24; ++i) {
        p[i] = i;
    }
    int64_t (*p6)[6] = (int64_t (*)[6])p;
    printf("%d\n", p6[3]);
    printf("%d\n", (*(p6 + 3)));
    free(p);
}
$ gcc --std=c11 test.c && ./a.out
13795488
13795488

Собственно, как видим, p6[3] возвращает адрес в памяти. Я не понимаю, почему. Моя логика такова:

  • p - адрес в куче, где лежит подряд 24 инта, от 0 до 24.
  • p6 - тот же адрес.
  • Из-за того, что p6 указатель на int64_t[6], при его индексации сдвиг происходит на 48 байт.
  • При взятии индекса должно происходить разыменование, т.е. должно возвращаться значение 4-ых 48 байт, а не их адрес.
  • То же верно и для предпоследней строки. p6 + 3 - адрес 4-ых 48 байт.
  • *(p6 + 3) должно вернут значение, а не адрес.

Вопрос, почему работают всякие p6[3][2]?

★★★★★

почему работают всякие p6[3][2]?

Как раз потому, что p6[3] возвращает адрес. В данном случае тип p6[3] это int64_t[6]. Т.е. [3] смещается на «строку» матрицы, а [2] смещается внутри строки.

При взятии индекса должно происходить разыменование, т.е. должно возвращаться значение 4-ых 48 байт, а не их адрес.

Это не скаляр, массив нельзя разименовать во все его значения.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Как раз потому, что p6[3] возвращает адрес.

Я это понимаю, но я не понимаю почему он его возвращает.

Это не скаляр, массив нельзя разименовать во все его значения.

Ну если, например, поменять на массив чаров, длинну строки матрицы сделать 8 и скастовать в int64, то в принципе можно. В памяти те же байты. Но это не важно. Пусть хоть падает.

Адрес то откуда взялся? Там нет ячеек, в которых записан адрес. Разыменованием его не получить.

Ivan_qrt ★★★★★
() автор топика

Так выходит. p6 - это int64_t p6[4][6] .

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

int main()
{
    int64_t *p = (int64_t *) malloc(24 * sizeof(int64_t));
    for (size_t i = 0; i < 24; ++i) {
        p[i] = i;
    }
    int64_t(*p6)[6] = (int64_t(*)[6]) p;

    for (size_t i = 0; i < 4; ++i) {
        for (size_t j = 0; j < 6; ++j)
            printf("%d,", p6[i][j]);
        printf("\n");
    }

    free(p);
}

/*
$ gcc --std=c11 test.c && ./a.out
0,1,2,3,4,5,
6,7,8,9,10,11,
12,13,14,15,16,17,
18,19,20,21,22,23,
*/

anonymous
()
Ответ на: комментарий от Ivan_qrt

Адрес то откуда взялся? Там нет ячеек, в которых записан адрес.

Там тип выражения это массив, а массив это по сути указатель на его первый элемент. Для типа int a[x][y], a[x] имеет тип int[y] и вычисляется как смещение от a.

Не надо использовать семантику двухуровневых массивов, когда на предыдущем уровне хранятся указатели на следующий. Двухмерный массив это именно массив массивов. Первый индекс переходит на x*sizeof(*a) байт вперёд и отбрасывает один уровень. Эти смещения вычисляются без обращения к памяти так как при известных размерностях и индексах никаких указателей не требуются, подмассив можно найти просто смещаясь от начала. Это самое обычное i*ncols + j, но выраженное для компилятора в системе типов.

xaizek ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Это можно объяснить в абстрактных терминах следующим образом: все типы в C кроме массивов передаются по значению (копируется значение), а массивы передаются по ссылке(копируется указатель на значение). В этих терминах разименование двумерного массива вполне себе «возвращает» одномерный массив. Просто для printf нет модификатора, который бы такой массив печатал. Но результат такого разименования вполне можно передать в другую ожидающую массив функцию в сигнатуре которой как раз указан массив (не указатель на него!).

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

void print_six_array(int64_t a[6])
{
    printf("a = %p\n", a);	
    printf("a[0] = %lld\n", a[0]);
    printf("a[1] = %lld\n", a[1]);
}
int main() {
    int64_t *p = (int64_t*)malloc(24*sizeof(int64_t));
    for(size_t i = 0; i < 24; ++i) {
        p[i] = i;
    }
    int64_t (*p6)[6] = (int64_t (*)[6])p;
    printf("%p\n", p6[3]);
    printf("%p\n", (*(p6 + 3)));
    print_six_array(p6[3]);
    free(p);
}
desktop:~/m$ ./a.out 
0xa023098
0xa023098
a = 0xa023098
a[0] = 18
a[1] = 19
GPFault ★★★
()
Ответ на: комментарий от anonymous

Пардон, не дочитал до вопроса. В " *(p6 + 3) " не хватает звездочки или скобок «[]» .

...
    for (size_t i = 0; i < 4; ++i) {
        for (size_t j = 0; j < 6; ++j)
            printf("%d,%d,%d; ",
                *((*(p6+i))+j),
                (*(p6+i))[j],
                p6[i][j]
              );
        printf("\n");
    }
...
$ gcc --std=c11 test.c && ./a.out
0,0,0; 1,1,1; 2,2,2; 3,3,3; 4,4,4; 5,5,5; 
6,6,6; 7,7,7; 8,8,8; 9,9,9; 10,10,10; 11,11,11; 
12,12,12; 13,13,13; 14,14,14; 15,15,15; 16,16,16; 17,17,17; 
18,18,18; 19,19,19; 20,20,20; 21,21,21; 22,22,22; 23,23,23; 

anonymous
()

По простому. Блин, ну ты область памяти из 24*int64_t разбиваешь на области по 6*int64_t. Сколько у тебя получится таких областей? Четыре. То есть у тебя получится массив [4][6]. Следовательно что ты получишь при обращении вида p6[3]? Указатель на начало последней из полученных областей (размером 6*int64_t).

baldman88
()
Ответ на: комментарий от xaizek

Я это всё прекрасно понимаю. Я не понимаю почему операция разыменования не производится в случае, если тип указателя int64_t (*)[x]. Но производится, если это int64_t *.

    printf("%p\n", p6[3]);
    printf("%p\n", (*(p6 + 3)));
    printf("%p\n", (p6 + 3));

Возвращают одинаковые адреса.

В компиляторе для оператора разыменования специальный костыль (полиморфизм по возвращаемому значению?)? Или я всё-таки чего-то не понимаю?

baldman88, GPFault вопрос не в том, как это работает, а в том, почему это работает.

Ivan_qrt ★★★★★
() автор топика
Последнее исправление: Ivan_qrt (всего исправлений: 1)
Ответ на: комментарий от Ivan_qrt

вопрос не в том, как это работает, а в том, почему это работает.

Потому что массив при использовании в выражениях автоматически «разлагается» («decays») до указателя на первый элемент? Просто не понятно, что, собственно, не понятно.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Например, почему при разыменовании *(p6 + 3) происходит только смена типа (на int64_t[6]), но не получение значения.

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

int main() {
    int64_t *p = (int64_t*)malloc(24*sizeof(int64_t));
    for(size_t i = 0; i < 24; ++i) {
        p[i] = i;
    }
    int64_t (*p6)[6] = (int64_t (*)[6])p;
    printf("%d\n", (*(p6 + 3)));
    printf("%d\n", (p6 + 3));
    free(p);
}

Потому что массив при использовании в выражениях автоматически «разлагается» («decays»)

Можно по-подробнее, каким образом он разлагается при *(p6 + 3). Будет ли он разлагаться, если вместо 3 будет переменная?

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от Ivan_qrt

Нет никаких костылей. Смотри. Что возвращает р6[3]? Указатель на первый элемент четвертого массива из 6-ти элементов. Что возвращает (p6 + 3)? Совсем не то, что р6[3]. Но массивы так уж устроены (см. пояснение ниже). А что возвращает (*(p6 + 3))? Да, есть разыменование. Но по сути: что есть *(p6 + 3)? Указатель на первый элемент четвертого массива (аналогично р6[3]). Вот тебе и возвращается адрес. У тебя же массив указателей на указатели на int64_t. Элементы первого уровня содержат указатели на элементы второго уровня. Элементы второго уровня содержат указатели на элементы третьего уровня. Абстрактно твой массив выглядит так:

0x00 ->

  • 0x00 -> 0
  • 0x01 -> 1
  • 0x02 -> 2
  • 0x03 -> 3
  • 0x04 -> 4
  • 0x05 -> 5

0x06 ->

  • 0x06 -> 6
  • 0x07 -> 7
  • 0x08 -> 8
  • 0x09 -> 9
  • 0x0A -> 10
  • 0x0B -> 11

0x0C ->

  • 0x0C -> 12
  • 0x0D -> 13
  • 0x0E -> 14
  • 0x0F -> 15
  • 0x10 -> 16
  • 0x11 -> 17

0x12 /* сюда указывает (p6 + 3) */ ->

  • 0x12 /* сюда указывают p6[3] и *(p6 + 3) */ -> 18
  • 0x13 -> 19
  • 0x14 -> 20
  • 0x15 -> 21
  • 0x16 -> 22
  • 0x17 -> 23

То есть при разыменовывании конструкции вида *(p6 + 3) ты получаешь указатель на первый элемент четвертого массива, но не его значение. Вот если применишь разыменование дважды, то поучишь значение конкретного элемента. И да, выражение вида a[x] равносильно выражению *(a + x).
Вышло сумбурно, но сегодня день тяжелый был ... Надеюсь ты хоть что-то да понял.

baldman88
()
Ответ на: комментарий от baldman88

У тебя же массив указателей на указатели на int64_t. Элементы первого уровня содержат указатели на элементы второго уровня. Элементы второго уровня содержат указатели на элементы третьего уровня

Это не так. Есть только один указатель на элемент [0][0].

0x12 /* сюда указывает (p6 + 3) */ ->
0x12 /* сюда указывают p6[3] и *(p6 + 3) */ -> 18

0x12 /* сюда указывает (p6+3), p6[3] и *(p6+3), только у (p6+3) тип отличается */

То есть при разыменовывании конструкции вида *(p6 + 3) ты получаешь указатель на первый элемент четвертого массива, но не его значение.

Собственно, в этом и вопрос, почему не происходит взятия значения из ячейки, на которую указывает указатель?

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от Ivan_qrt

Не, ну ты настырный. Ты же сам создаешь УКАЗАТЕЛЬ на массив:

int64_t (*p6)[6] = (int64_t (*)[6])p;
Что ты получишь при разыменовывании этого указателя? Адрес первого элемента этого массива. А вот чтобы получить значение элемента этого массива нужно еще раз его разыменовать. Но ты этого не делаешь. По такому же принципу при прибавлении числа n к р6 ты сдвигаешь этот указатель на 6*int64_t*n. Но р6 по прежнему указатель на массив. Разыменовав такую конструкцию ты опять получишь адрес первого элемента. И его опять же нужно разыменовать чтобы получить значение. Это для p6[3] и *(p6 + 3).
Для случая (р6 + 3) ты просто сдвигаешь указатель на 6*int64_t*3. И по этому адресу находится тот же адрес начала массива (так уж вышло по воле судьбы).
Так что нет никаких костылей и никакой магии. Ты разыменовываешь только указатель на массив и получаешь адрес первого элемента. Если и теперь непонятно, то я сдаюсь.

baldman88
()
Ответ на: комментарий от Ivan_qrt

Пы.Сы.: Каюсь, с многомерными массивами вчера по пьяне натупил.

baldman88
()
Ответ на: комментарий от baldman88

Не, ну ты настырный.

Хочу понять. Пока не получается.

Что ты получишь при разыменовывании этого указателя? Адрес первого элемента этого массива.

А почему я получу адрес первого элемента? Ответ «потому что получишь адрес первого элемента» немного не устраивает.

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от Ivan_qrt

int64_t (*p6)[6]; обозначает, что:

выражение (*p6)[6] даёт int64_t

выражение *p6 даёт шесть идущих подряд int64_t == адрес первого числа

выражение p6 даёт указатель на шесть int64_t == адрес этих шестерых == адрес первого числа из этих шестерых

выражение p6 + 3 даёт смещённый на 3 указатель, то есть указатель на четвёртую такую «шестёрку» == адрес этих шестерых == адрес 19-го числа

выражение *(p6 + 3) даёт саму четвёртую шестёрку == адрес этих шестерых == адрес 19-го числа

kvap
()

Пробовал почитать стандарт C1x, ничего про указатели на массивы я там не нашёл.

Собственно я пришёл к следующему выводу: Операция разыменования указателя ведёт себя по разному в зависимости от типа, на который она указывает. Например есть mytype *p = ... Тогда *p:

  • В большинстве случаев меняет тип возвращаемого значения на тип mytype, читает sizeof(mytype) байт по адресу p и возвращает полученное значение.
  • Если mytype имеет тип anothertype[X] (массив), то оператор * меняет тип на mytype и возвращает адрес p. Чтения данных по адресу не происходит.

Собственно подтверждением может служить то, что во втором случае можно без проблем разыменовывать NULL, и ничего за это не будет:

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

int main() {
    int64_t *n = NULL;
    int64_t (*pn)[5] = (int64_t (*)[5])n;
    printf("Null: %p\n", pn[0]);
    printf("Null[1]: %p\n", pn[1]);
    printf("*Null: %p\n", *pn);
    printf("*Null + 5: %p\n", *(pn + 1));
}
$ gcc --std=c11 test2.c && ./a.out
Null: (nil)
Null[1]: 0x28
*Null: (nil)
*Null + 5: 0x28

Получается, таки, костылик в компиляторе специально для массивов? Есть ли ещё такие исключения?

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от kvap

шесть идущих подряд int64_t == адрес первого числа

Не могу с этим согласится. Откуда среди этих шести int64_t возьмётся их же адрес?

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от Ivan_qrt

Можно по-подробнее, каким образом он разлагается при *(p6 + 3). Будет ли он разлагаться, если вместо 3 будет переменная?

typeof(   p6 + 3  ) == int64_t(*)[6]
typeof( *(p6 + 3) ) == int64_t[6]

Разложение (int64_t[6] => int64_t*) происходит при использовании выражения *(p6 + 3) где-нибудь. С массивами работают как с указателями всегда, поэтому это происходит автоматически.

Это как функции разлагаются до их адреса. Не удивляет же, что *&main не равно куску кода, а всегда возвращает адрес? Или скажем строковые литералы ведут себя либо как список инициализации (при инициализации) либо как массивы/указатели в других выражениях.

Грубо говоря, массивы не являются first-class citizens и поэтому для работы с ними их тип меняется автоматически. Структуры, например, являются и если обернуть массив в структуру, то он будет копироваться в аргументах и возвращаемом значении и его можно будет даже присваивать.

Будет ли он разлагаться, если вместо 3 будет переменная?

Будет, речь идёт преобразованиях системы типов, конкретные элементы значения не имеют.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 2)
Ответ на: комментарий от xaizek

Спасибо за разъяснения. То что p[x] раскрывается в *(p+x), я понимаю.

строковые литералы ведут себя либо как список инициализации (при инициализации) либо как массивы/указатели в других выражениях.

Ну это исключение из общего поведения строк (инициализация массива строкой). Просто оно понятно и про это много где написано.

А вот про то, что оператор * ведёт себя по-другому, если разыменовывает указатель на массив, было как-то не очевидно. Да и не написано про это нигде. Даже в стандарте не нашёл (возможно, плохо искал).

В общем, если тут я всё правильно написал, то тему можно закрывать.

Спасибо всем поучаствовавшим.

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от Ivan_qrt

Пробовал почитать стандарт C1x, ничего про указатели на массивы я там не нашёл.

6.5.2.1 Array subscripting
...
3 Successive subscript operators designate an element of a multidimensional array object.
If E is an n-dimensional array (n ≥ 2) with dimensions i × j × . . . × k, then E (used as
other than an lvalue) is converted to a pointer to an (n − 1)-dimensional array with
dimensions j × . . . × k. If the unary * operator is applied to this pointer explicitly, or
implicitly as a result of subscripting, the result is the referenced (n − 1)-dimensional
array, which itself is converted into a pointer if used as other than an lvalue. It follows
from this that arrays are stored in row-major order (last subscript varies fastest).
xaizek ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Почему тебя массивы удивляют, а такое нет?

#include <stdio.h>

int add(int a, int b) {
  return a+b;
}

int main() {
  int (*addFunc)(int, int);

  addFunc = ******************add;
  int z = addFunc(4, 7);
  printf("%d\n", z);
  return 0;
}

pftBest ★★★★
()
Ответ на: комментарий от xaizek

Спасибо. Всё встало на свои места. Искать я не умею, это да.

Ivan_qrt ★★★★★
() автор топика
Ответ на: комментарий от pftBest

Да в общем-то тоже, удивляет. Но так я никогда не делал, и над этим не задумывался. Но то, что функции в общую систему типов не совсем вписываются более понятно, чем то, что не вписываются массивы. Но всё прояснилось, всем спасибо.

Ivan_qrt ★★★★★
() автор топика

вот смотри

Breakpoint 1, main () at main.c:11
11	  printf("%d\n", *p6[3]);
(gdb) x/24d p6
0x602010:	0	0	1	0
0x602020:	2	0	3	0
0x602030:	4	0	5	0
0x602040:	6	0	7	0
0x602050:	8	0	9	0
0x602060:	10	0	11	0
а вот значения.
(gdb) x/d p6
0x602010:	0
(gdb) x/d p6 + 1
0x602040:	6
(gdb) x/d p6 + 2
0x602070:	12
(gdb) x/d p6 + 3
0x6020a0:	18
когда ты создавал
int64_t *p
ты создал указатель на тип int64_t. в следующем примере
int64_t (*p6)[6] = (int64_t (*)[6])p;
ты создал указатель на указатель. в этом случае, чтобы разыменовать его, используй.

  printf("%d\n", *p6[3]);
  printf("%d\n", *(*(p6 + 3)));
u0atgKIRznY5
()

вот ещё для наглядности я заменю 9 на 56

(gdb) x/24 p6
0x602010: 	0	0	1	0
0x602020:	2	0	3	0
0x602030:	4	0	5	0
0x602040:	6	0	7	0
0x602050:	8	0	56    0
0x602060:	10	0	11	0

так как указатель на указатель с шагом в 6 позиций имеет тип int64_t, то *(p6[1] + 3) будет 56.

u0atgKIRznY5
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.