C: Почему неназначенные указатели указывают на непредсказуемую память и НЕ указывают на ПУСТОЙ УКАЗАТЕЛЬ?

Давным-давно я раньше программировал в C для школы. Я помню что-то, что я действительно ненавидел о C: неназначенные указатели не указывают на ПУСТОЙ УКАЗАТЕЛЬ.

Я спросил многих людей включая учителей, почему в мире будет они заставлять поведение по умолчанию неназначенного указателя не указать на ПУСТОЙ УКАЗАТЕЛЬ, поскольку кажется намного более опасным для него быть непредсказуемым.

Ответ был, предположительно, производительностью, но я никогда не покупал это. Я думаю много, многих ошибок в истории программирования, возможно, избежали, имел C, принявший значение по умолчанию к ПУСТОМУ УКАЗАТЕЛЮ.

Здесь некоторые C кодируют для указания (предназначенная игра слов), о чем я говорю:

#include <stdio.h>

void main() {

  int * randomA;
  int * randomB;
  int * nullA = NULL;
  int * nullB = NULL;


  printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", 
     randomA, randomB, nullA, nullB);
}

Который компилирует с предупреждениями (Ее хорошее, чтобы видеть, что компиляторы C намного более хороши чем тогда, когда я был в школе), и выводы:

randomA: 0xb779eff4, randomB: 0x804844b, nullA: (ноль), nullB: (ноль)

46
задан Adam Gent 23 June 2010 в 17:49
поделиться

11 ответов

На самом деле, это зависит от хранения указателя. Указатели со статическим хранением инициализируются нулевыми указателями. Указатели с автоматической длительностью хранения не инициализируются. См. ISO C 99 6.7.8.10:

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

  • если он имеет тип указателя, он инициализируется нулевым указателем;
  • если он имеет арифметический тип, он инициализируется (положительным или беззнаковым) нулю;
  • если это агрегат, то каждый член инициализируется (рекурсивно) в соответствии с этими правилами;
  • если это объединение, то первый названный член инициализируется (рекурсивно) в соответствии с этими правилами.

И да, объекты с автоматической длительностью хранения не инициализируются по причинам производительности. Только представьте себе инициализацию массива размером 4K при каждом вызове функции протоколирования (то, что я видел в проекте, над которым работал, к счастью, C позволил мне избежать инициализации, что дало хороший прирост производительности).

41
ответ дан 26 November 2019 в 20:18
поделиться

Потому что в C объявление и инициализация - это намеренно разные шаги. Они намеренно разные, потому что так устроен язык Си.

Когда вы говорите это внутри функции:

void demo(void)
{
    int *param;
    ...
}

Вы говорите: "Мой дорогой компилятор Си, когда вы создаете стековый кадр для этой функции, пожалуйста, не забудьте зарезервировать sizeof(int*) байт для хранения указателя". Компилятор не спрашивает, что там происходит - он предполагает, что вы скоро ему об этом скажете. Если нет, то, возможно, для вас есть язык получше ;)

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

Если ваша function1 вызывает другую function2 и хранит ее возвращаемое значение, или, возможно, есть некоторые параметры, переданные в function2, которые не изменяются внутри function2... нам не нужно создавать дополнительное пространство, не так ли? Просто используйте одну и ту же часть стека для обоих! Обратите внимание, что это прямо противоречит концепции инициализации стека перед каждым использованием.

Но в более широком смысле (и, на мой взгляд, более важном) это согласуется с философией языка Си о том, что не нужно делать больше, чем это абсолютно необходимо. И это применимо независимо от того, работаете ли вы на PDP11, PIC32MX (для чего я его использую) или Cray XT3. Именно почему люди предпочитают использовать C вместо других языков.

  • Если я хочу написать программу без следов malloc и free, мне не придется этого делать! Никакое управление памятью мне не навязывается!
  • Если я хочу побитово упаковать и напечатать объединение данных, я могу это сделать! (Конечно, при условии, что я прочитаю примечания моей реализации о соблюдении стандартов.)
  • Если я точно знаю, что я делаю со своим стековым фреймом, компилятору не нужно больше ничего делать за меня!

Короче говоря, когда вы просите компилятор Си прыгнуть, он не спрашивает, как высоко. Результирующий код, вероятно, даже не будет спускаться обратно.

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

26
ответ дан 26 November 2019 в 20:18
поделиться

Это для производительности.

C был впервые разработан примерно во времена PDP 11, для которого 60k было обычным максимальным объемом памяти, у многих было намного меньше. Ненужные присваивания были бы особенно дорогими в такой среде

В наши дни существует множество встраиваемых устройств, использующих C, для которых 60k памяти кажется бесконечным, PIC 12F675 имеет 1k памяти.

14
ответ дан 26 November 2019 в 20:18
поделиться

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

Компилятор языка Си может присвоить этому указателю значение, но в большинстве случаев это будет пустой тратой времени, так как в некоторых частях кода вам придется присваивать значение самостоятельно.

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

8
ответ дан 26 November 2019 в 20:18
поделиться

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

int a;
double b;

printf("%d, %f\n", a, b);

Причина проста: требование к среде выполнения установить неинициализированные значения в известное значение добавляет накладные расходы на каждый вызов функции. Накладные расходы могут быть невелики при использовании одного значения, но если у вас есть большой массив указателей:

int *a[20000];
7
ответ дан 26 November 2019 в 20:18
поделиться

Когда вы объявляете переменную (указатель) в начале функции, компилятор компилятор сделает одно из двух: выделит регистр для использования в качестве этой переменной, или выделит для нее место в стеке. Для большинства процессоров, выделение памяти для всех локальных переменных в стеке выполняется одной инструкцией; она подсчитывает, сколько памяти потребуется всем локальным переменным, и вытягивает из стека место. локальные переменные, и опускает вниз (или поднимает вверх, на некоторых процессорах) указатель стека. процессорах) указатель стека на эту величину. Все, что уже находится в памяти, не изменяется, пока вы явно не измените его. его.

Указатель не "устанавливается" на "случайное" значение. Перед распределением стековая память под указателем стека (SP) содержит все, что там находится от предыдущего использования:

         .
         .
 SP ---> 45
         ff
         04
         f9
         44
         23
         01
         40
         . 
         .
         .

После выделения памяти для локального указателя единственное, что изменилось, - это указатель стека. изменился только указатель стека:

         .
         .
         45
         ff |
         04 | allocated memory for pointer.
         f9 |
 SP ---> 44 |
         23
         01
         40
         . 
         .
         .

Это позволяет компилятору выделить все локальные переменные в одной инструкции, которая перемещает указатель стека вниз по стеку (и освободить их все за одну инструкцию, перемещая указатель стека обратно вверх), но заставляет вас инициализировать их самостоятельно, если это необходимо. сделать это.

В C99 вы можете смешивать код и декларации, поэтому вы можете отложить ваше объявление в коде до тех пор, пока вы не сможете его инициализировать. Это позволит вам избежать необходимости устанавливать его в NULL.

4
ответ дан 26 November 2019 в 20:18
поделиться

Я думаю, это происходит из-за следующего: нет причин, по которым память должна содержать (при включении) определенные значения (0, NULL или что-то еще). Итак, если это не было специально написано ранее, ячейка памяти может содержать любое значение, которое, с вашей точки зрения, в любом случае является случайным (но это самое местоположение могло раньше использоваться каким-либо другим программным обеспечением и, таким образом, содержать значение, которое было значимым для это приложение, например счетчик, но с "вашей" точки зрения, это просто случайное число). Чтобы инициализировать его конкретным значением, вам нужно как минимум на одну инструкцию больше; но есть ситуации, когда вам не нужна эта инициализация априори , например v = malloc (x) присвоит v действительный адрес или NULL, независимо от исходного содержимого v. Таким образом, его инициализация может считаться пустой тратой времени, и язык (например, C) может выбирать не делать этого априори . Конечно, в настоящее время это в основном несущественно, и есть языки, в которых неинициализированные переменные имеют значения по умолчанию (ноль для указателей, если поддерживается; 0 / 0,0 для числовых ... и так далее; ленивая инициализация, конечно, делает инициализацию не такой дорогой. массив, скажем, из 1 миллиона элементов, поскольку они инициализируются по-настоящему, только если к ним обращаются до назначения).

0
ответ дан 26 November 2019 в 20:18
поделиться

Для того, чтобы он указывал на NULL, ему должен был быть назначен NULL (даже если это было сделано автоматически и прозрачно).

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

-1
ответ дан 26 November 2019 в 20:18
поделиться

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

Во-вторых, часто можно сделать собственную инициализацию. Вместо int *p; напишите int *p = NULL; или int *p = 0;. Используйте calloc() (который инициализирует память нулем), а не malloc() (который этого не делает). (Нет, все биты нулевые не обязательно означают NULL-указатели или значения с плавающей точкой, равные нулю. Да, в большинстве современных реализаций это так.)

В-третьих, философия C (и C++) заключается в том, чтобы дать вам средства для быстрого выполнения чего-либо. Предположим, у вас есть выбор: реализовать в языке безопасный способ сделать что-то и быстрый способ сделать что-то. Вы не можете сделать безопасный способ быстрее, добавив больше кода вокруг него, но вы можете сделать быстрый способ более безопасным. Более того, иногда можно сделать операции быстрыми и безопасными, гарантируя, что операция будет безопасной без дополнительных проверок - при условии, конечно, что у вас изначально есть быстрый вариант.

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

3
ответ дан 26 November 2019 в 20:18
поделиться

Итак, чтобы подвести итог тому, что объяснил ninjalj, если вы немного измените свой пример программы, вы указатели будут фактически инициализировать NULL:

#include <stdio.h>

// Change the "storage" of the pointer-variables from "stack" to "bss"  
int * randomA;
int * randomB;

void main() 
{
  int * nullA = NULL;
  int * nullB = NULL;

  printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", 
     randomA, randomB, nullA, nullB);
}

На моей машине это печатает

randomA: 00000000, randomB: 00000000, nullA: 00000000, nullB: 00000000

1
ответ дан 26 November 2019 в 20:18
поделиться

Идея о том, что это имеет какое-то отношение к случайному содержимому памяти при включении машины, ошибочна, за исключением встроенных систем. Любая машина с виртуальной памятью и многопроцессной/многопользовательской операционной системой инициализирует память (обычно до 0), прежде чем отдать ее процессу. Неспособность сделать это будет серьезным нарушением безопасности. Случайные" значения в переменных автоматического хранения происходят от предыдущего использования стека тем же процессом. Аналогично, "случайные" значения в памяти, возвращаемые malloc/new/etc., являются результатом предыдущих выделений (которые впоследствии были освобождены) в том же процессе.

0
ответ дан 26 November 2019 в 20:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: