... просто декрементація вказівника за межами виділеного діапазону здається мені дуже схематичною. Це "дозволена" поведінка в С?
Дозволено? Так. Гарна ідея? Не зазвичай.
C - це скорочення для мови складання, а в мові складання немає вказівників, а лише адреси пам'яті. Покажчики C - це адреси пам'яті, які мають побічну силу збільшення або зменшення за розміром, на що вони вказують, коли вони піддаються арифметиці. Це робить це просто добре з точки зору синтаксису:
double *p = (double *)0xdeadbeef;
--p; // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];
Масиви - це насправді не річ у С; вони просто вказівки на суміжні діапазони пам'яті, які ведуть себе як масиви. []
Оператор є узагальнюючим для виконання арифметичних операцій над покажчиками і разименованія, тому на a[x]
самому ділі означає *(a + x)
.
Для цього є поважні причини, такі як деякі пристрої вводу / виводу, які мають пару зображень double
, відображених у 0xdeadbee7
та 0xdeadbeef
. Для цього знадобиться дуже мало програм.
Коли ви створюєте адресу чогось, наприклад, за допомогою &
оператора чи дзвінка malloc()
, ви хочете зберегти первинний вказівник недоторканим, щоб ви знали, що те, на що вказує, насправді щось дійсне. Зменшення покажчика означає, що якийсь біт помилкового коду може спробувати знеструмити його, отримавши помилкові результати, щось втрутившись чи, залежно від вашого оточення, допустити порушення сегментації. Це особливо стосується того malloc()
, що ви поклали тягар на того, хто закликає free()
пам’ятати про те, щоб передати оригінальне значення, а не якусь змінену версію, яка призведе до того, що всякий чорт зламається.
Якщо вам потрібен масив на основі С на C, ви можете це зробити безпечно за рахунок виділення одного додаткового елемента, який ніколи не буде використаний:
double *array_create(size_t size) {
// Wasting one element, so don't allow it to be full-sized
assert(size < SIZE_MAX);
return malloc((size+1) * sizeof(double));
}
inline double array_index(double *array, size_t index) {
assert(array != NULL);
assert(index >= 1); // This is a 1-based array
return array[index];
}
Зауважте, що це не робить нічого для захисту від перевищення верхньої межі, але це досить просто впоратися.
Додаток:
Деякі розділи та вірші із проекту C99 (вибачте, це все, на що я можу посилатись):
У § 6.5.2.1.1 сказано, що другий ("інший") вираз, який використовується з оператором підписника, має цілочисельний тип. -1
є цілим числом, і це робить p[-1]
дійсним, а тому також робить покажчик &(p[-1])
дійсним. Це не означає, що доступ до пам’яті в цьому місці створює певну поведінку, але покажчик все-таки є дійсним вказівником.
§6.5.2.2 говорить, що оператор індексів масиву оцінює еквівалент додавання номера елемента до вказівника, тому p[-1]
еквівалентний *(p + (-1))
. Досі діє, але може не спричинити бажаної поведінки.
§6.5.6.8 говорить (наголос мій):
Коли вираз, який має цілий тип, додається до вказівника або віднімається в ньому, результат має тип операнда вказівника.
... якщо вираз P
вказує на i
-й елемент об’єкта масиву, вирази (P)+N
(еквівалентно N+(P)
) та (P)-N
(де N
має значення n
) вказують відповідно на i+n
-і та
i−n
-і елементи об’єкта масиву, за умови їх існування .
Це означає, що результати арифметики вказівника повинні вказувати на елемент масиву. Це не говорить про те, що арифметику потрібно робити все відразу. Тому:
double a[20];
// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];
double *p = a - 1; // This is just a pointer. No dereferencing.
double e = p[0]; // Does not point at any element of a; behavior is undefined.
double f = p[1]; // Points at element 0 of a; behavior is defined.
Я рекомендую робити такі дії? Ні, і моя відповідь пояснює, чому.