2016-12-16 3 views
12

Насколько я знаю, многомерный массив в стеке будет занимать непрерывную память в порядке строк. Является ли неопределенное поведение индексированием многомерного массива с использованием указателя на элементы в соответствии со стандартом ISO C++? Например:Индексирование многомерных массивов с использованием указателя на элементы

#include <iostream> 
#include <type_traits> 
int main() { 
    int a[5][4]{{1,2,3,4},{},{5,6,7,8}}; 
    constexpr auto sz = sizeof(a)/sizeof(std::remove_all_extents<decltype(a)>::type); 
    int *p = &a[0][0]; 
    int i = p[11]; // <-- here 
    p[19] = 20; // <-- here 
    for (int k = 0; k < sz; ++k) 
    std::cout << p[k] << ' '; // <-- and here 
    return 0; 
} 

Приведенный выше код будет компилироваться и работать правильно, если указатель не выходит за пределы границы массива a. Но происходит ли это из-за того, что поведение компилятора или язык стандартно? Любая ссылка из стандарта ISO C++ была бы лучше всего.

+1

Ну, я не мог найти что-либо непосредственно связаны с стандартом ISO C++. Но да, автоматически выделяемые массивы гарантированно сохраняются в памяти. И когда вы используете индексный оператор, т. Е. 'P [11]' на простой указатель, он эквивалентен '* (p + 11)', поэтому, если есть законные данные типа '* p', поведение определяется. – George

+0

@George Layout гарантии и юридический доступ - это не одно и то же. Не путайте реализацию с требованиями абстракции. – Yakk

+3

Я думаю, что '[expr.add]/5' может запретить это, но' [dcl.array]/1' гарантирует, что хранилище смежно. – NathanOliver

ответ

6

Проблема здесь является правилом строгого сглаживания, которая существует в моих проектах n3337 для C++ 11 в 3.10 Lvalues ​​и rvalues ​​[basic.lval] § 10. Это исчерпывающий перечень, который не позволяет explicetely псевдонима многомерный массив до одномерного целого размера.

Таким образом, даже если это действительно необходимо, чтобы массивы выделяются последовательно в памяти, что доказывает, что размер многомерного массива, скажем, например T arr[n][m] является продуктом является размер по размеру элемента: n * m *sizeof(T). При преобразовании в указатели на символы вы можете даже выполнять операции арифметического указателя по всему массиву, потому что любой указатель на объект может быть преобразован в указатель char, и этот указатель char может использоваться для доступа к последовательным байтам объекта (*), ,

Но, к сожалению, для любого другого типа, стандарт только позволяют операции арифметического указателя внутри одного массива (и по определению dereferening элемента массива тот же, как разыменования указателя после указателя арифметики: a[i]является*(a + i)). Так что, если вы оба соблюдать правила на стрелочных арифметике и строгое правило наложения спектров, глобальное индексирование многомерного массива не определяется C++ 11 стандарта, если не пройти через полукокса указатель арифметика:

int a[3][4]; 
int *p = &a[0][0]; // perfectly defined 
int b = p[3];  // ok you are in same row which means in same array 
b = p[5];   // OUPS: you dereference past the declared array that builds first row 

char *cq = (((char *) p) + 5 * sizeof(int)); // ok: char pointer arithmetics inside an object 
int *q = (int *) cq; // ok because what lies there is an int object 
b = *q;   // almost the same as p[5] but behaviour is defined 

То, что char pointer arithmetics вместе со страхом сломать много существующего кода объясняет, почему весь известный компилятор молчаливо принимает наложение многомерного массива с 1D одного и того же глобального размера (он приводит к тому же внутреннему коду), но технически глобальная арифметика указателя действительна только для указателей на символы.


(*) Стандарт объявляет в 1.7 ++ модели памяти C [intro.memory], что

Устройство хранения фундаментального в модели памяти C++ является байтами ... Память доступна программа на C++ состоит из одной или нескольких последовательностей смежных байтов. Каждый байт имеет уникальный адрес.

и более поздние версии 3.9 Типы [основные.Типы] § 2

Для любого объекта (кроме базового класса подобъекта) из тривиального Copyable типа Т, выполняется ли или нет объекта действительного значение типа T, лежащие в основе байты, составляющие объект могут скопировать в массив символа char или unsigned.

и скопировать их, вы должны получить доступ к ним через char *unsigned char * или

+1

Я думаю, что там нет никакого наложения. У нас есть указатель на первый элемент массива размером 5 в 'int * p'. Я полагаю, вы говорите, что если указатель на 'T [a] [b]' был гарантирован законным псевдонимом в указатель на 'T [a * b], операция была бы неявно законной? – Yakk

+1

что ограничивает его только арифметикой указателя char?в вашем примере (((char *) p) + 5 * sizeof (int)) может быть легальным на любой платформе, но если в математике есть ошибка, которая будет перемещать указательный адрес, соответствующий началу элемента int, на платформе со строгими выравнивание данных, например SPARC, что приведет к катастрофической ситуации, это незаконный код операции. Стандарт должен преследовать более упорядоченный и универсальный рабочий путь, почему он не может быть * (p + 5)? – Swift

+0

@Yakk: Я говорю, что 'a [i]' является законным только в том случае, если 'a' и' a + i' указывают внутри одного массива. И это было бы так, если бы мы могли псевдоним 'T [a] [b]' и 'T [a * b]'. Но я согласен с вами, что законности наложения указателей будет достаточно. –

0

, если вы объявите

int arr[3][4][5]; 

типа arr является int[3][4][5], типа arr[3] является int[4][5] и т.д. Массив массива массивов, но не массив указателей. Посмотрим, что произойдет, если мы увеличим первый индекс? Он сдвигал бы указатель вперед по размеру элемента массива, но элемент массива из arr был двумерным массивом! Это эквивалентно приращением: arr + sizeof(int[4][5])/sizeof(int) или обр + 20.

Итерация таким образом мы находим, что arr[a][b][c] равна *(*(*(arr + a) + b) + c), при условии, что там никогда не бывает набивка с массивами (в соответствии с обязательной совместимости типов боб с C99) :

*((int*)arr + 20*a + 5*b + c) 

Когда выражение, которое имеет целочисленный тип добавляется или вычитается из указателя, результат имеет тип указателя операнда. Если операнд указателя указывает на элемент объекта массива, а массив достаточно велик, результат указывает на смещение элемента от исходного элемента , так что разница в нижних индексах в результате и исходных элементов массива равна интегральное выражение

+0

@NathanOliver извините, я исправил его, автокоррекция добавила * после почти каждого int с [ – Swift

+0

Вопрос задает вопрос об использовании указателя 'int', хотя и не разлагает' arr' на указатель. –

+1

@ Daniel H arr распадается на указатель, который указывает на тот же объект, что и указатель, оцененный символом & (первым элементом элемента arr) – Swift

9

Я считаю, что поведение в вашем примере технически не определено.

Стандарт не имеет понятия многомерного массива. То, что вы на самом деле объявили, представляет собой «массив из 5 массивов из 4 целых чисел». То есть a[0] и a[1] фактически представляют собой два разных массива из 4-х целых чисел, оба из которых содержатся в массиве a. Это означает, что a[0][0] и a[1][0] не являются элементами одного и того же массива.

[expr.add]/4 говорит следующее (курсив мой)

Когда выражение, которое имеет целочисленный тип добавляется или вычитается из указателя, результат имеет тип указателя операнда , Если операнд указателя указывает на элемент объекта массива, а массив достаточно большой, результат указывает на смещение элемента от исходного элемента, так что разница индексов результирующих и исходных элементов массива равна интегралу выражение. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на i + n-й и i-й-элементы элемента массива , если они существуют.Более того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, а если выражение Q указывает мимо последнего элемента массива объект, выражение (Q) -1 указывает на последний элемент массива объект. Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или один последний последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае, поведение неопределенными

Таким образом, поскольку p[11] расширяется до *(p + 11) и так как p и p + 11 не являются элементами одного и того же массива (один элемент a[0], а другой более одного элемента мимо конец a[0]), поведение этого добавления не определено.

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

+1

Но не было бы корректно определено '((p + 4) + 4) + 3', учитывая, что' p + 4' - один за конец 'a [0]', а также является указателем на 'а [1] [0]'? Я уверен, что правила выравнивания и дополнений гарантируют, что 'sizeof (a [0]) == 4 * sizeof (int)' и что 'a [1]' is 'sizeof (a [0])' байт после ' а [0] '. –

+0

Стандарт не определяет многомерный arry, потому что он определен рекурсивно. Подумайте об этом как о рекурсивном шаблоне. Но в отличие от класса, массив гарантированно хранится в непрерывном режиме. Компилятор будет прав бросить предупреждение, если в массиве int a [4] [5] вы попытаетесь получить доступ к элементу [4] [0] ... потому что он будет таким же, как * (a + 20), который в этом конкретном случай находится вне границы массива. Он будет предупреждать об [0] [6]. Такое появление может указывать на ошибку \ typo в коде. Если индексы заданы во время выполнения, компилятор не будет знать, какие значения используются, и обычная математика указателя будет работать. – Swift

+0

@ DanielH Не путайте реализацию с абстракцией. В абстракции указано, что требуется определить. Указатели и индексирование массива не определены в терминах модели линейной памяти в C++. Вместо этого определяются различные операции. Один из способов их реализации - модель с линейной памятью; но даже там оптимизаторы могут предположить, что будут выполняться только определенные операции.Классическим примером является объединение двойного и целого числа: изменение одного, а затем чтение другого - UB, хотя бит гарантированно перекрывается. – Yakk

Смежные вопросы