Индекс массива вне границ в C
Почему C
дифференцируется в случае индекса массива вне границы
#include <stdio.h>
int main()
{
int a[10];
a[3]=4;
a[11]=3;//does not give segmentation fault
a[25]=4;//does not give segmentation fault
a[20000]=3; //gives segmentation fault
return 0;
}
Я понимаю, что он пытается получить доступ к памяти, выделенной для процесса или потока в случае a[11]
или же a[25]
и это выходит за пределы стека в случае a[20000]
,
Почему компилятор или компоновщик не выдает ошибку, они не знают о размере массива? Если нет то как sizeof(a)
работать правильно?
9 ответов
Проблема в том, что C/C++ фактически не выполняет никакой проверки границ в отношении массивов. От операционной системы зависит доступ к действительной памяти.
В этом конкретном случае вы объявляете массив на основе стека. В зависимости от конкретной реализации, доступ за пределы массива будет просто осуществлять доступ к другой части уже выделенного стекового пространства (большинство ОС и потоков резервируют определенную часть памяти для стека). Пока вы просто играете в заранее выделенном стековом пространстве, все не рухнет (заметьте, я не сказал, что работает).
В последней строке происходит то, что вы получили доступ к той части памяти, которая выделена для стека. В результате вы индексируете часть памяти, которая не выделена вашему процессу или выделена только для чтения. ОС видит это и отправляет ошибку сегмента процессу.
Это одна из причин того, что C/C++ настолько опасен, когда дело доходит до проверки границ.
Segfault не является целевым действием вашей C-программы, которое сообщило бы вам о том, что индекс выходит за пределы. Скорее, это непреднамеренное следствие неопределенного поведения.
В C и C++, если вы объявляете массив как
type name[size];
Вам разрешен доступ только к элементам с индексами из 0
вплоть до size-1
, Все, что находится за пределами этого диапазона, вызывает неопределенное поведение. Если индекс был рядом с диапазоном, скорее всего, вы читаете память своей собственной программы. Если индекс был в значительной степени вне диапазона, скорее всего, ваша программа будет убита операционной системой. Но вы не можете знать, все может случиться.
Почему С это позволяет? Ну, основной смысл C и C++ - не предоставлять функции, если они стоят производительности. C и C++ использовались целую вечность для систем с высокой производительностью. C использовался в качестве языка реализации для ядер и программ, где доступ вне границ массива может быть полезен для получения быстрого доступа к объектам, расположенным рядом в памяти. Если компилятор запретит это, это будет напрасно.
Почему это не предупреждает об этом? Ну, вы можете поставить высокий уровень предупреждения и надеяться на милость компилятора. Это называется качеством реализации (QoI). Если какой-то компилятор использует открытое поведение (например, неопределенное поведение), чтобы сделать что-то хорошее, он имеет хорошее качество реализации в этом отношении.
[js@HOST2 cpp]$ gcc -Wall -O2 main.c
main.c: In function 'main':
main.c:3: warning: array subscript is above array bounds
[js@HOST2 cpp]$
Если вместо этого он отформатирует ваш жесткий диск, увидев доступ к массиву вне границ - что было бы для него законным - качество реализации было бы довольно плохим. Мне нравилось читать об этом в документе ANSI C Обоснование.
Обычно вы получаете ошибку сегментации, только если пытаетесь получить доступ к памяти, которой ваш процесс не принадлежит.
Что вы видите в случае a[11]
(а также a[10]
кстати) это память, которая принадлежит вашему процессу , но не принадлежит a[]
массив. a[25000]
так далеко от a[]
это, вероятно, у вас в памяти вообще.
изменения a[11]
гораздо более коварен, так как он незаметно влияет на другую переменную (или кадр стека, который может вызвать другую ошибку сегментации, когда ваша функция вернется).
С не делает этого. Подсистема виртуальной памяти ОС есть.
В случае, если вы только немного вышли за пределы допустимого, вы обращаетесь к памяти, выделенной для вашей программы (в данном случае к стеку вызовов стека). В случае, если вы далеко за пределами допустимого диапазона, вы обращаетесь к памяти, не переданной вашей программе, и ОС выдает ошибку сегментации.
В некоторых системах также существует принудительная концепция "записываемой" памяти в ОС, и вы, возможно, пытаетесь записать в память, которой владеете, но которая помечена как недоступная для записи.
Как я понимаю вопрос и комментарии, вы понимаете, почему могут случиться плохие вещи, когда вы получаете доступ к памяти за пределами границ, но вам интересно, почему ваш конкретный компилятор не предупредил вас.
Компиляторы могут предупреждать вас, и многие делают это на самых высоких уровнях предупреждения. Однако стандарт написан для того, чтобы люди могли запускать компиляторы для всех видов устройств и компиляторы со всевозможными функциями, поэтому стандарт требует как можно меньше, гарантируя, что люди могут выполнять полезную работу.
Стандарт несколько раз требует, чтобы определенный стиль кодирования генерировал диагностику. Есть несколько других случаев, когда стандарт не требует диагностики. Даже когда требуется диагностика, я не знаю ни одного места, где стандарт говорит, какой должна быть точная формулировка.
Но вы не совсем в холоде здесь. Если ваш компилятор не предупреждает вас, Lint может. Кроме того, существует ряд инструментов для обнаружения таких проблем (во время выполнения) для массивов в куче, одним из наиболее известных из которых является Electric Fence (или DUMA). Но даже Electric Fence не гарантирует, что перехватит все ошибки переполнения.
Просто добавьте то, что говорят другие, вы не можете полагаться на то, что в этих случаях программа просто рухнет, нет гарантии того, что произойдет, если вы попытаетесь получить доступ к области памяти за пределами "границ массива". Это так же, как если бы вы сделали что-то вроде:
int *p;
p = 135;
*p = 14;
Это просто случайно; это может сработать. Возможно, нет. Не делай этого. Код для предотвращения подобных проблем.
Как уже упоминалось, некоторые компиляторы могут обнаруживать некоторые обращения к массиву вне пределов во время компиляции. Но проверка границ во время компиляции не поймает все:
int a[10];
int i = some_complicated_function();
printf("%d\n", a[i]);
Чтобы обнаружить это, нужно использовать проверки во время выполнения, и их избегают в C из-за их влияния на производительность. Даже зная размер массива a во время компиляции, то есть sizeof(a), он не может защитить от этого без вставки проверки во время выполнения.
С философией всегда доверяй программисту. А также отсутствие проверки границ позволяет программе на C работать быстрее.
Это не проблема C, это проблема операционной системы. Вашей программе предоставлено определенное пространство памяти, и все, что вы делаете внутри, это нормально. Ошибка сегментации происходит только тогда, когда вы обращаетесь к памяти за пределами вашего пространства процесса.
Не все операционные системы имеют отдельные адресные пространства для каждого процесса, и в этом случае вы можете без предупреждения повредить состояние другого процесса или операционной системы.
Как сказал ДжаредПар, C/C++ не всегда выполняет проверку диапазона. Если ваша программа обращается к области памяти за пределами выделенного массива, ваша программа может аварийно завершить работу или нет, потому что она обращается к какой-либо другой переменной в стеке.
Чтобы ответить на ваш вопрос об операторе sizeof в C: вы можете надежно использовать sizeof(array)/size(array[0]) для определения размера массива, но его использование не означает, что компилятор будет выполнять любую проверку диапазона.
Мое исследование показало, что разработчики C / C++ считают, что вы не должны платить за то, что не используете, и доверяют программистам, что они знают, что они делают. (см. принятый ответ на это: доступ к массиву за пределами границ не дает ошибок, почему?)
Если вы можете использовать C++ вместо C, может быть, использовать вектор? Вы можете использовать vector[], когда вам нужна производительность (но без проверки диапазона), или, что более предпочтительно, использовать vector.at() (который имеет проверку диапазона за счет производительности). Обратите внимание, что вектор не увеличивает емкость автоматически, если он заполнен: в целях безопасности используйте push_back(), который при необходимости автоматически увеличивает емкость.
Дополнительная информация о векторе: http://www.cplusplus.com/reference/vector/vector/