нотация массива против нотации указателя C


Есть ли какое-либо преимущество в использовании нотации указателя над нотацией массива? Я понимаю, что могут быть некоторые частные случаи, когда нотация указателя лучше, но мне кажется, что нотация массива яснее. Мой профессор сказал нам, что он предпочитает обозначения указателей "потому что это C", но это не то, что он будет отмечать. И я знаю, что есть различия с объявлением строк в виде символьных массивов против объявления указателя в виде строки - я просто говорю о том, что в общем цикле через массив.

2 4

2 ответа:

Если вы пишете простой цикл, то и массив, и форма указателя обычно компилируются в один и тот же машинный код.

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

Итак, как насчет того, чтобы мы рассмотрели пример реального мира, который опирается на оба?

Эти типы реализуют матрицу с плавающей запятой двойной точности динамически определяемых размер, с отдельным хранилищем данных с учетом ссылок:

struct owner {
    long          refcount;
    size_t        size;
    double        data[];    /* C99 flexible array member */
};

struct matrix {
    long          rows;
    long          cols;
    long          rowstep;
    long          colstep;
    double       *origin;
    struct owner *owner;
};
Идея состоит в том, что когда вам нужна матрица, вы описываете ее с помощью локальной переменной типаstruct matrix. Все упомянутые данные хранятся в динамически распределенных структурах struct owner, в гибком элементе массива C99. После того, как вы больше не нуждаетесь в матрице, вы должны явно "отбросить" ее. Это позволяет нескольким матрицам ссылаться на одни и те же данные: вы даже можете иметь отдельные строки, столбцы или диагональные векторы с любым изменением на один сразу же отражаются на всех остальных (потому что они ссылаются на одни и те же значения данных).

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

Все это предполагает однопоточный процесс; многопоточная обработка намного сложнее.

Для доступа к элементу в матрице struct matrix m, строке r, столбце c, предполагая 0 <= r < m.rows и 0 <= c < m.cols, вы используете m.origin[r*m.rowstep + c*m.colstep].

Если вы хотите транспонировать матрицу, вы просто меняете местами m.rows и m.cols, и m.rowstep и m.colstep. Все, что меняется, - это порядок, в котором данные (хранящиеся в структуре владельца) считываются.

(обратите внимание, что origin указывает на двойник, который появляется в строке 0, столбце 0 в Матрице; и что rowstep и colstep могут быть отрицательными. Это позволяет создавать всевозможные странные "виды" на скучные обычные данные, такие как зеркала, диагонали и т. д.)

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

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

#define MATRIXELEM(m, r, c)  ((m).origin[(r)*(m).rowstep + (c)*(m).colstep])

, который, по общему признанию, имеет обратную сторону, что он оценивает первый параметр, m, три раза. (Это означает, что MATRIXELEM(m++,0,0) фактически попытается увеличить m в три раза.) В этом конкретном случае m обычно является локальной переменной типа struct matrix, которая должна минимизировать сюрпризы. Можно было бы и так например

struct matrix m1, m2;

/* Stuff that initializes m1 and m2, and makes sure they point
   to valid matrix data */

MATRIXELEM(m1, 0, 0) = MATRIXELEM(m2, 0, 0);

"лишние" скобки в таких макросах гарантируют, что при использовании вычисления, например i + 4*j в качестве строки, вычисление индекса правильно ((i + 4*j)*m.rowstep, а не i + 4*j*m.rowstep). В макросах препроцессора эти скобки на самом деле вовсе не являются "лишними". Кроме того, чтобы обеспечить правильный расчет, наличие" лишних " скобок также говорит другим программистам, что автор макросов был осторожен, избегая таких ошибок, связанных с арифметикой. (Я, например, считаю "хорошим тоном" ставить свою скобки есть даже в тех случаях, когда они не нужны для синтаксической однозначности, если она передает эту "уверенность" другим разработчикам, читающим код.)

И это, после всего этого текста, приводит к моему самому важному пункту: некоторые вещи легче выразить и понять нами, программистами-людьми, используя нотацию массива, чем нотацию указателя, и наоборот. "Foo"[1] довольно очевидно равно 'o', тогда как *("Foo"+1) не так очевидно. (Опять же, ни то, ни другое не является 1["foo"], но вы можете обвинить в этом людей из C-стандартизации.)

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

На самом деле, если вы, скажем, передаете аргумент массива функции в C, вы фактически передаете указатель на ее начало. Это действительно не передает массив в обычном смысле, во-первых, потому что передача массива будет включать передачу его фактической длины, во-вторых, потому что передача массива (как значения) будет означать его копирование. Другими словами, вы действительно передаете итератор, указывающий на начало массива (например, std::vector::begin() в C++), но вы делаете вид, что передаете сам массив. На самом деле это очень запутанно. Итак, используя указатели представляют вещи, которые действительно происходят, гораздо более ясно, и это определенно должно быть предпочтительным.

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