C: Почему неназначенные указатели указывают на непредсказуемую память и НЕ указывают на NULL? - программирование
Подтвердить что ты не робот

C: Почему неназначенные указатели указывают на непредсказуемую память и НЕ указывают на NULL?

Давно я учился в школе для С. Я помню то, что я действительно ненавидел о C: неназначенные указатели не указывают на NULL.

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

Ответ был якобы производительностью, но я никогда этого не покупал. Я думаю, что многие ошибки в истории программирования можно было бы избежать, если бы C по умолчанию был NULL.

Здесь некоторый код 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: (nil), nullB: (nil)

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

Если вы скажете это внутри функции:

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

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

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

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

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

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

Короче говоря, когда вы просите компилятор C прыгать, он не спрашивает, насколько высок. Полученный код, вероятно, даже не вернется.

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

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

int a;
double b;

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

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

int *a[20000];

Ответ 6

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

Указатель не "установлен" на "случайное" значение. Перед распределением стек стека ниже указателя стека (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.

Ответ 7

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

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

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

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

Ответ 8

Итак, чтобы подвести итог тому, что объяснил ниндзял, если вы немного измените свою примерную программу, указатели будут инициализированы до 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

Ответ 9

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

Ответ 10

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

Ответ 11

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

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