Одномерный доступ к многомерному массиву: это четко определенное поведение?

Я полагаю, мы все согласны с тем, что C считается идиоматичным для доступа к настоящему многомерному массиву путем разыменования указателя (возможно, смещения) на его первый элемент одномерным способом, например:

void clearBottomRightElement(int *array, int M, int N)
{
    array[M*N-1] = 0;  // Pretend the array is one-dimensional
}


int mtx[5][3];
...
clearBottomRightElement(&mtx[0][0], 5, 3);

Тем не менее, языковой юрист во мне нуждается в том, чтобы убедиться, что на самом деле это хорошо определенный C! Особенно:

  1. Гарантирует ли стандарт, что компилятор не будет помещать заполнение между, например. mtx[0][2] и mtx[1][0]?

  2. Обычно индексирование конца массива (кроме одного конца) не определено (C99, 6.5.6/8). Таким образом, следующее явно не определено:

    struct {
        int row[3];           // The object in question is an int[3]
        int other[10];
    } foo;
    int *p = &foo.row[7];     // ERROR: A crude attempt to get &foo.other[4];
    

    Таким образом, по тому же правилу можно было бы ожидать, что следующее будет неопределенным:

    int mtx[5][3];
    int (*row)[3] = &mtx[0];  // The object in question is still an int[3]
    int *p = &(*row)[7];      // Why is this any better?
    

    Так почему это должно быть определено?

    int mtx[5][3];
    int *p = &(&mtx[0][0])[7];
    

Итак, какая часть стандарта C явно разрешает это? (Давайте предположим, что для обсуждения c99.)

ИЗМЕНИТЬ

Обратите внимание, что я не сомневаюсь, что это прекрасно работает во всех компиляторах. Я спрашиваю, разрешено ли это явно стандартом.


person Oliver Charlesworth    schedule 09.06.2011    source источник
comment
Отправка этого как комментарий, так как я не уверен. Массивы AFAIK гарантированно будут непрерывными в памяти, тогда как структуры могут иметь заполнение между этими элементами. Если вы посмотрите на ассемблерный код доступа к массиву, вы сможете увидеть, что операция, выполняемая для доступа [][], такая же, как и для *(array + x * index + y).   -  person RedX    schedule 09.06.2011
comment
Я не языковой юрист, поэтому не буду добавлять ответ, однако именно так это работает для растровых изображений. В основном все, что у вас есть, это байты, и вы знаете, сколько байтов находится в строке. Чтобы перейти к следующей строке, вы должны сместить исходный указатель на количество строк * ширину. Так что в случае четко определенных данных я бы сказал, что это совершенно нормальное кодирование.   -  person Wouter Simons    schedule 09.06.2011
comment
@Wouter: О, я не сомневаюсь, что все в порядке! Я использую этот принцип каждый день, как и все остальные. Я чисто спрашиваю с точки зрения педантичности юриста по языку!   -  person Oliver Charlesworth    schedule 09.06.2011
comment
@Oli: Ну, юристы — ужасные разработчики. В памяти массив не имеет заполнения, поэтому индексирование многомерных массивов как одномерных всегда будет работать. Приращение вашего указателя определяется из указателя базового массива, поэтому arr[10] должен быть arr + 10 * sizeof(arr), который, я уверен, указан в спецификациях. Это означает, что arr[1][5] со вторым измерением, всегда равным 5: arr + 1 * 5 * sizeof(arrType) + 5 * sizeof (arrType)...   -  person Wouter Simons    schedule 09.06.2011
comment
У меня нет времени писать это, но абзацы 3 и 4 C99 6.5.2.1, похоже, четко определяют это.   -  person Hasturkun    schedule 09.06.2011
comment
@Hasturkun: Да, я рассматривал эти абзацы. Я не уверен, что это прямо определяет это; все, что он говорит, это то, что имя N-мерного массива распадается на указатель на (N-1)-мерный массив. Итак, в моем примере mtx на самом деле является int[5][3], но распадается на int(*)[3].   -  person Oliver Charlesworth    schedule 09.06.2011


Ответы (4)


Единственным препятствием для того типа доступа, который вы хотите сделать, является то, что объекты типа int [5][3] и int [15] не могут создавать псевдонимы друг друга. Таким образом, если компилятор знает, что указатель типа int * указывает на один из массивов int [3] первого, он может наложить ограничения на границы массива, которые предотвратят доступ к чему-либо за пределами этого массива int [3].

Возможно, вы сможете обойти эту проблему, поместив все в объединение, которое содержит как массив int [5][3], так и массив int [15], но мне действительно неясно, действительно ли четко определены приемы объединения, которые люди используют для каламбура. Этот случай может быть немного менее проблематичным, поскольку вы не будете указывать отдельные ячейки, а только логику массива, но я все еще не уверен.

Следует отметить один особый случай: если бы ваш тип был unsigned char (или любым другим типом char), доступ к многомерному массиву как к одномерному массиву был бы совершенно четко определен. Это связано с тем, что одномерный массив unsigned char, который перекрывает его, явно определен стандартом как «представление» объекта и по своей сути разрешен для его псевдонима.

person R.. GitHub STOP HELPING ICE    schedule 09.06.2011
comment
Калибровка типов через объединения не более определена, чем через приведения указателей, но документация GCC выходит за рамки стандарта для первого и гарантирует, что программа будет делать то, что ожидает программист. Даже с параметром -fstrict-aliasing разрешена каламбуризация типов при условии, что доступ к памяти осуществляется через тип объединения. gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html - person Pascal Cuoq; 09.06.2011
comment
@Pascal: C99 позволяет каламбурить типы через союзы - это явно упоминается в сноске 82 (стр. 73), которая была добавлена ​​​​в TC3. - person Christoph; 10.06.2011
comment
Я надеялся, что кто-то ответит Нет, нет! Ты не прав! Посмотрите, стандарт явно разрешает это здесь..., но, по-видимому, нет. Итак, я принял этот ответ, так как он наиболее кратко формулирует проблему (о псевдонимах). - person Oliver Charlesworth; 10.06.2011
comment
Обратите внимание, что в Приложении J.2 стандарта этот вид доступа к многомерному массиву OOB явно указан как пример UB. - person R.. GitHub STOP HELPING ICE; 10.06.2011
comment
Я согласен с ответом, за исключением той части, в которой утверждается, что типы символов сходят с рук. В частности, когда указатель символа увеличивается, чтобы указывать на другой объект, в данном случае на другой элемент внешнего массива. Можете ли вы уточнить или предоставить стандартную цитату. 6.5 не делает исключений для char. Спасибо. - person 2501; 21.07.2016
comment
Я снова наткнулся на этот ответ случайно. Я был бы очень признателен, если бы вы ответили на мой предыдущий комментарий или, по крайней мере, сказали, что не хотите, если это так. Спасибо. - person 2501; 28.07.2016
comment
@ 2501: Это следствие представления типов (наложение unsigned char [sizeof T]) и применения эквивалентности/конвертируемости указателей между представлением, структурой и членами структуры. Короче говоря, один и тот же unsigned char * законно указывает как на элемент массива представления для всей структуры, так и на элемент массива элементов внутри структуры. В силу первого допустим более широкий диапазон арифметических операций. - person R.. GitHub STOP HELPING ICE; 17.06.2017

Все массивы (включая многомерные) не содержат отступов. Даже если это никогда не упоминалось явно, об этом можно судить по sizeof правилам.

Подписка на массив является частным случаем арифметики указателей, и в разделе 6.5.6, §8 C99 четко указано, что поведение определяется только в том случае, если операнд указателя и результирующий указатель лежат в одном и том же массиве (или на один элемент позади), что делает Возможны реализации проверки границ языка C.

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

mtx[0] + 5 

идентично четко определенным аналогам, таким как

(int *)((char *)mtx + 5 * sizeof (int))

который хорошо определен, потому что любой объект (включая весь двумерный массив) всегда можно рассматривать как одномерный массив типа char.


При дальнейшем размышлении над формулировкой раздела 6.5.6, разбивая доступ за пределами границ на, казалось бы, четко определенные подвыражения, такие как

(mtx[0] + 3) + 2

рассуждение о том, что mtx[0] + 3 является указателем на один элемент за концом mtx[0] (что делает первое добавление четко определенным), а также указателем на первый элемент mtx[1] (что делает второе дополнение четко определенным), неверно:

Несмотря на то, что mtx[0] + 3 и mtx[1] + 0 гарантированно равны при сравнении (см. раздел 6.5.9, §6), они семантически различны. Например, первое нельзя разыменовать, и поэтому не указывает на элемент mtx[1].

person Christoph    schedule 09.06.2011
comment
Я согласен с большей частью того, что вы сказали. Я не уверен, что могу согласиться с тем, что (mtx[0] + 3) + 2) допустимо, потому что все добавления указателя за границы могут быть рекурсивно выражены как (((p+1)+1)+1) и т. д. И если бы это было четко определено, чтобы выразить их таким образом, то что было бы точка 6.5.6/8? - person Oliver Charlesworth; 09.06.2011
comment
@Oli: арифметика C не ассоциативна - (a+b)+c не обязательно совпадает с a+(b+c); суть вопроса в том, что в случае многомерных массивов указатель может «принадлежать» двум массивам одновременно, и арифметика указателя не отслеживает исходный массив, поэтому вам нужно только проверить каждое подвыражение; насколько я могу судить, действительно возможно перебирать многомерный массив с одношаговым приращением - person Christoph; 09.06.2011
comment
@Christoph: я согласен с вашим мнением об ассоциативности. Я предполагаю, что единственный оставшийся вопрос заключается в том, допустимо ли использовать псевдоним для объекта с указателем на элемент «один за концом» для предыдущего объекта. Например, в моем примере со структурой хорошо ли определено поведение для реализации, которая гарантирует отсутствие заполнения между row и other? - person Oliver Charlesworth; 09.06.2011
comment
@Oli: перечитав раздел 6.5.6 и немного поразмыслив, я передумал;) указатели после последнего элемента массива являются «особыми» и не могут использоваться так, как я первоначально описал - person Christoph; 09.06.2011
comment
Да, добавление указателя - это то, что потенциально проблематично. Обратите внимание, что это не проблема, если базовый тип является типом char, так как тогда любой указатель на него также является указателем на массив representation, который является массивом типа unsigned char [sizeof whole_multi_dim_array], и, таким образом, все арифметические операции является действительным. - person R.. GitHub STOP HELPING ICE; 09.06.2011
comment
@Christoph: Да, именно этого я и боялся! Спасибо за ваш вклад и +1. - person Oliver Charlesworth; 09.06.2011

  1. Уверен, что между элементами массива нет отступов.

  2. Предусмотрена возможность вычисления адресов меньшего размера, чем полное адресное пространство. Это можно использовать, например, в огромном режиме 8086, чтобы часть сегмента не всегда обновлялась, если компилятор знал, что вы не можете пересечь границу сегмента. (Я слишком давно напоминаю, воспользовались ли этим компиляторы, которые я использовал, или нет).

С моей внутренней моделью -- я не уверен, что она точно такая же, как стандартная, и проверять ее слишком больно, информация распространяется повсюду --

  • то, что вы делаете в clearBottomRightElement, действительно.

  • int *p = &foo.row[7]; не определено

  • int i = mtx[0][5]; не определено

  • int *p = &row[7]; не компилируется (со мной согласен gcc)

  • int *p = &(&mtx[0][0])[7]; находится в серой зоне (в прошлый раз, когда я проверял детали примерно так, я закончил тем, что рассмотрел неверный C90 и действительный C99, это может быть здесь, или я мог что-то упустить).

person AProgrammer    schedule 09.06.2011
comment
Вы правы, я ошибся в синтаксисе int *p = &row[7]. Я отредактирую свой вопрос. - person Oliver Charlesworth; 09.06.2011
comment
То, что я действительно ищу, - это аргумент, основанный на формулировке стандарта... - person Oliver Charlesworth; 09.06.2011

Насколько я понимаю стандарт C99, существует нет требования, чтобы многомерные массивы располагались в памяти в непрерывном порядке. Следуя единственной релевантной информации, которую я нашел в стандарте (каждое измерение гарантировано является непрерывным).

Если вы хотите использовать доступ x[COLS*r + c], я предлагаю вам придерживаться одномерных массивов.

Подписка массива

Последовательные операторы нижнего индекса обозначают элемент объекта многомерного массива. Если E является n-мерным массивом (n ≥ 2) с размерами i × j × . . . × k, то E (используется не как lvalue) преобразуется в указатель на (n − 1)-мерный массив с размерами j × . . . × к. Если унарный оператор * применяется к этому указателю явно или неявно в результате индексации, результатом будет указанный (n − 1)-мерный массив, который сам преобразуется в указатель, если используется не как lvalue. . Из этого следует, что массивы хранятся в построчном порядке (быстрее всего меняется последний индекс).

Тип массива

— Тип массива описывает непрерывно размещенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента. 36) Типы массивов характеризуются типом их элементов и количеством элементов в массиве. Говорят, что тип массива является производным от его типа элемента, и если его тип элемента T , тип массива иногда называют «массивом T». Построение типа массива из типа элемента называется «производным типом массива».

person nimrodm    schedule 09.06.2011
comment
Точно, поэтому, если вы рассматриваете непрерывный буфер памяти как многомерный массив, это нормально, но наоборот это может быть не так. Это звучит правильно для меня. - person Wouter Simons; 09.06.2011
comment
@nimrodm: Ваша интерпретация стандарта во многом совпадает с моей. (Что обнадеживает, я думаю!) - person Oliver Charlesworth; 09.06.2011
comment
@oli - Такая формулировка - это то, что вы получаете, когда формируете комитет :) На вашем месте я бы по-прежнему использовал многомерные массивы и просто добавил какое-то статическое утверждение, чтобы убедиться, что они непрерывны. - person nimrodm; 09.06.2011
comment
Первая фраза откровенно ложна. Расположение в памяти точно определяется семантикой sizeof и арифметикой указателя. Только из-за правил псевдонимов это использование не определено и, следовательно, только для типов, отличных от char. - person R.. GitHub STOP HELPING ICE; 09.06.2011
comment
Я не согласен с тем, что многомерность не требует непрерывности. Массив с 3 элементами внутри массива с 3 элементами (arr[3][3]) должен быть непрерывным, чтобы соответствовать описанию, иначе второй массив (тот, который содержит остальные 3 массива) не сможет называть себя массив, так как его макет не будет непрерывным. Внутренний массив (arr[3]) имеет массив X, тогда как внешний массив представляет собой массив X[sizeof(внутренний массив)]. - person RedX; 09.06.2011
comment
@RedX - вы уверены, что внешний массив на самом деле не является массивом типа X [sizeof (тип внутреннего массива *)]? (Реальный вопрос, не помню.) - person detly; 09.06.2011
comment
@detly: внешний массив в примере OP представляет собой массив из 5 объектов int [3], а не 15 объектов int. - person R.. GitHub STOP HELPING ICE; 09.06.2011
comment
@R.. - нет, я понимаю - звездочка означает указатель на ie. мой вопрос заключался в том, реализован ли внешний массив как массив указателей на начало каждого внутреннего массива или как массив целых int[3] объектов. - person detly; 10.06.2011
comment
Это последнее - массив из int[3] объектов. - person R.. GitHub STOP HELPING ICE; 10.06.2011