Могу ли я рассматривать двумерный массив как непрерывный одномерный массив? - PullRequest
33 голосов
/ 01 сентября 2011

Рассмотрим следующий код:

int a[25][80];
a[0][1234] = 56;
int* p = &a[0][0];
p[1234] = 56;

Вызывает ли вторая строка неопределенное поведение?Как насчет четвертой строки?

Ответы [ 7 ]

9 голосов
/ 01 сентября 2011

Да, вы можете (нет, это не UB), это косвенно гарантируется стандартом.Вот как: 2D-массив - это массив массивов.Массив гарантированно имеет непрерывную память, а sizeof(array) - это sizeof(elem) количество элементов.Из этого следует, что то, что вы пытаетесь сделать, совершенно законно.

8 голосов
/ 01 сентября 2011

Это зависит от интерпретации. Несмотря на то, что требования к смежности массивов не оставляют большого воображения в плане компоновки многомерных массивов (на это уже указывалось ранее), обратите внимание, что когда вы делаете p[1234], вы индексируете 1234-й элемент нулевой ряд только 80 столбцов. Некоторые интерпретируют единственные действительные индексы как 0,79 (&p[80] - особый случай).

Информация из C FAQ , которая является накопленным мнением Usenet по вопросам, относящимся к C. (Я не думаю, что C и C ++ расходятся в этом вопросе, и что это очень важно.)

3 голосов
/ 11 июня 2018

Обе строки do приводят к неопределенному поведению.

Подписка интерпретируется как добавление указателя с последующим косвенным указанием, то есть a[0][1234] / p[1234] эквивалентно *(a[0] + 1234) / *(p + 1234). Согласно [expr.add] / 4 (здесь я цитирую новейший черновик, в то время как на данный момент предлагается OP, вы можете сослаться на этот комментарий , и вывод такой же ):

Если выражение P указывает на элемент x [i] объекта массива x с n элементами, выражения P + J и J + P (где J имеет значение j) указывают на (возможно, гипотетический) элемент x [i + j], если 0≤i + j≤n; в противном случае поведение не определено.

, поскольку a[0] (с указателем на a[0][0]) / p указывает на элемент a[0] (в виде массива), а a[0] имеет только размер 80, поведение не определено.


Как указал Language Lawyer в комментарии , следующая программа не компилирует .

constexpr int f(const int (&a)[2][3])
{
    auto p = &a[0][0];
    return p[3];
}

int main()
{
    constexpr int a[2][3] = { 1, 2, 3, 4, 5, 6, };
    constexpr int i = f(a);
}

Компилятор обнаружил такое неопределенное поведение, когда оно появляется в константном выражении.

0 голосов
/ 10 апреля 2019

На языке, который был написан для описания стандарта, не было бы проблем с вызовом такой функции, как:

void print_array(double *d, int rows, int cols)
{
  int r,c;
  for (r = 0; r < rows; r++)
  {
    printf("%4d: ", r);
    for (c = 0; c < cols; c++)
      printf("%10.4f ", d[r*cols+c]);
    printf("\n");
  }
}

для double[10][4], double[50][40] или любого другого размера,при условии, что общее количество элементов в массиве было меньше rows*cols.Действительно, гарантия того, что шаг строки для T[R][C] будет равен C * sizeof (T), была разработана, среди прочего, чтобы можно было писать код, который мог бы работать с многомерными массивами произвольного размера.

НаС другой стороны, авторы Стандарта признали, что когда реализациям присваивается что-то вроде:

double d[10][10];
double test(int i)
{
  d[1][0] = 1.0;
  d[0][i] = 2.0;
  return d[1][0]; 
}

, позволяющее им генерировать код, который предполагает, что d[1][0] будет по-прежнему держать 1.0 при выполнении return, илипозволяя им генерировать код, который будет перехватывать, если i больше 10, позволит им быть более подходящим для некоторых целей, чем требование, чтобы они молча возвращали 2.0, если вызвано с i==10.

Ничегов стандарте проводится различие между этими сценариями.Хотя для Стандарта было бы возможно включить правила, в которых говорилось бы, что второй пример вызывает UB, если i >= 10 без влияния на первый пример (например, скажем, что применение [N] к массиву не приводит к его затуханию доуказатель, но вместо этого выдает N-й элемент, который должен существовать в этом массиве), вместо этого стандарт опирается на тот факт, что реализациям разрешено вести себя полезным образом, даже если это не требуется, и авторы компилятора должны, вероятно, иметь возможностьраспознавание ситуаций, подобных первому примеру, когда это пойдет на пользу их клиентам.

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

0 голосов
/ 20 июня 2014

Память, на которую ссылается a, имеет значение int[25][80] и int[2000].Так говорит Стандарт, 3.8p2:

[Примечание: время жизни объекта массива начинается, как только получается хранилище с правильным размером и выравниванием, и его время жизни заканчивается, когда хранилище, которое занимает массивиспользуется повторно или выпущен.12.6.2 описывает время жизни базовых и членских подобъектов.- примечание конца]

a имеет определенный тип, это lvalue типа int[25][80].Но p это просто int*.Это не «int*, указывающее на int[80]» или что-то подобное.Таким образом, на самом деле int указывает на элемент int[25][80] с именем a, а также элемент int[2000], занимающий то же пространство.

Поскольку p и p+1234 являютсяоба элемента одного и того же int[2000] объекта, арифметика указателя четко определена.А поскольку p[1234] означает *(p+1234), оно также четко определено.

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


Так как std::array упоминается в комментариях:

Если у кого есть std::array<std::array<int, 80>, 25> a;, то не существует a std::array<int, 2000>.Там существует int[2000].Я ищу все, что требует sizeof (std::array<T,N>) == sizeof (T[N])== N * sizeof (T)).В отсутствие этого вы должны предположить, что могут быть пробелы, которые могут испортить обход вложенных std::array.

0 голосов
/ 01 сентября 2011

Ваш компилятор выдаст кучу предупреждений / ошибок из-за нижнего индекса вне диапазона (строка 2) и несовместимых типов (строка 3), но до тех пор, пока фактическая переменная (в данном случае int) является одной из внутренних основ-типы это сохранить в C и C ++.(Если переменная является классом / структурой, она, вероятно, все еще будет работать в C, но в C ++ все ставки выключены.)

Почему вы хотите это сделать ... Для 1-го варианта: Если вашкод, основанный на подобном беспорядке, будет подвержен ошибкам, и его трудно поддерживать в долгосрочной перспективе.

Я вижу некоторую пользу для второго варианта, когда циклы оптимизации производительности по сравнению с двумерными массивами заменяются на1D-указатель работает над пространством данных, но хороший оптимизирующий компилятор часто все равно делает это сам.Если тело цикла настолько большое / сложное, что компилятор не может оптимизировать / заменить цикл на 1D-прогон самостоятельно, выигрыш в производительности при выполнении этого вручную, скорее всего, также не будет значительным.

0 голосов
/ 01 сентября 2011

Вы можете переосмыслить память любым удобным вам способом.Пока кратность не превышает линейную память.Вы даже можете переместить a на 12, 40 и использовать отрицательные индексы.

...