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

Давным-давно я занимался программированием на Си для школы. Я помню кое-что, что я действительно ненавидел в 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: (ноль), nullB: (ноль)

Ответы [ 11 ]

41 голосов
/ 23 июня 2010

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

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

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

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

26 голосов
/ 23 июня 2010

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

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

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

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

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

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

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

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

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

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

14 голосов
/ 23 июня 2010

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

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

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

8 голосов
/ 23 июня 2010

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

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

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

7 голосов
/ 23 июня 2010

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

int a;
double b;

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

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

int *a[20000];
4 голосов
/ 23 июня 2010

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

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

3 голосов
/ 23 июня 2010

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

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

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

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

1 голос
/ 25 июня 2010

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

0 голосов
/ 26 июня 2010

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

0 голосов
/ 23 июня 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...