Я пытаюсь понять, как работают указатели на статически выделенные объекты и где они могут пойти не так, как надо.
Я написал этот код:
int* pinf = NULL;
for (int i = 0; i<1;i++) {
int inf = 4;
pinf = &inf;
}
cout<<"inf"<< (*pinf)<<endl;
Я был удивлен, что это работало becasue, я думал это inf
был бы dissapear, когда программа оставила блок, и указатель укажет на что-то, что больше не существует. Я ожидал отказ сегментации при попытке получить доступ pinf
. В том, какой этап в программе был бы inf
умереть?
Ваше понимание верно. inf
исчезает, когда вы покидаете область видимости цикла, и поэтому обращение к *pinf
приводит к неопределенному поведению. Неопределенное поведение означает, что компилятор и/или программа могут делать все, что угодно, что может привести к аварийному завершению, или, в данном случае, к простому движению.
Это происходит потому, что inf
находится в стеке. Даже когда он находится вне области видимости pinf
все равно указывает на пригодную для использования область памяти на стеке. Для среды выполнения адрес стека является нормальным, и компилятор не утруждает себя вставкой кода для проверки того, что вы не обращаетесь к местам за пределами стека. Это было бы непомерно дорого в языке, рассчитанном на скорость.
По этой причине вы должны быть очень осторожны, чтобы избежать неопределенного поведения. Си и Си++ не так хороши, как Java или C#, где незаконные операции практически всегда приводят к немедленному исключению и краху вашей программы. Вы, программист, должны быть бдительны, потому что компилятор будет пропускать все виды элементарных ошибок, которые вы делаете.
Вы не обязательно получите SIGSEGV (ошибка сегментации). Память inf
, вероятно, выделена в стеке. И область памяти стека, вероятно, все еще выделена вашему процессу на этом этапе, поэтому, вероятно, поэтому вы не получаете ошибку seg.
Вероятно, он никогда не умрет, потому что pinf укажит на что-то на стеке.
Стеки не часто сжимаются.
Измените его, и вам в значительной степени будет гарантирована перезапись.
Поведение не определено, но на практике "уничтожение" int
является бесполезным, поэтому большинство компиляторов оставляют число в стеке до тех пор, пока не появится что-то другое для повторного использования этого конкретного слота.
Некоторые компиляторы могут установить int в 0xDEADBEEF
(или какой-нибудь подобный мусор), когда он выходит из области видимости в режиме отладки, но это не заставит cout << ...
; он просто выведет бессмысленное значение.
Память может содержать или не содержать 4, когда она дойдет до вашей строки cout. Она может содержать 4 совершенно случайно. :)
Прежде всего: ваша операционная система может обнаружить сбившийся доступ к памяти только на границах страниц. Так что, если вы ошиблись на 4k или 8k или 16k или больше. (Проверьте /proc/self/maps
в системе Linux как-нибудь, чтобы увидеть расположение памяти процесса; любые адреса в перечисленных диапазонах разрешены, любые вне перечисленных диапазонов - запрещены. Каждая современная ОС на процессорах с защищенной памятью поддерживает подобный механизм, так что это будет поучительно, даже если вы просто не интересуетесь Linux. Я просто знаю, что в Linux это легко). Итак, ОС не может помочь вам, когда ваши данные так малы.
Кроме того, ваша int inf = 4;
вполне может быть спрятана в сегментах .rodata
, .data
или .text
вашей программы. Статические переменные могут быть засунуты в любой из этих разделов (я понятия не имею, как компилятор/линкер принимает решение; я считаю это магией), и поэтому они будут действительны в течение всего времени работы программы. Проверьте размер /bin/sh
в следующий раз, когда будете работать в Unix-системе, чтобы понять, сколько данных помещается в какие секции. (И посмотрите readelf(1)
- слишком много информации. objdump(1)
, если вы работаете на более старых системах.)
Если вы измените inf = 4
на inf = i
, то память будет выделена на стеке, и у вас будет гораздо больше шансов, что она быстро перезапишется.
Ошибка защиты возникает, когда страница памяти, на которую вы указываете, больше не подходит для процесса.
К счастью, в большинстве ОС не создается отдельная страница для каждого целого числа стекового пространства.
Вы используете так называемый висячий указатель. Это приведет к неопределенному поведению по стандарту C++.
Если вы спрашиваете об этом:
int main() {
int* pinf = NULL;
for (int i = 0; i<1;i++){
int inf = 4;
pinf = &inf;
}
cout<<"inf"<< (*pinf)<<endl;
}
Тогда то, что вы имеете, является неопределенным поведением. Автоматически выделенный (не статический) объект inf вышел из области видимости и условно уничтожен, когда вы обращаетесь к нему по указателю. В этом случае может произойти все, что угодно, в том числе и то, что он "работает".