Можно ли использовать неинициализированные указатели в C или C++?

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

Это хорошо, но почему что-то подобное имеет значение? Я бы подумал, что единственный раз, когда это действительно имело бы значение, это если бы мне по какой-то причине захотел неинициализированный указатель. Но я не могу придумать причину, по которой я хотел бы иметь это.

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


person John    schedule 15.12.2009    source источник
comment
Некоторые методы загрузки на месте ТРЕБУЮТ, чтобы указатели не инициализировались конструктором C++.   -  person Adisak    schedule 16.12.2009
comment
На самом деле, методы загрузки на месте — это хаки. Если бы С++ был другим, были бы и другие хаки. Например. отбрасывать необработанную память, затем memcpy() указатели vtable.   -  person MSalters    schedule 16.12.2009
comment
Полные хаки... Я согласен, но очень часто используемый хак в моей отрасли (программирование видеоигр). Они работают с VC++, GCC и любым другим компилятором C/C++, который я использовал. И рабочие хаки привыкают. По стандарту вы должны осознавать, что такая простая вещь, как простое предположение о сдвиге вправо целого числа со знаком, будет арифметическим, а не логическим, также является технически хаком.   -  person Adisak    schedule 16.12.2009
comment
@MSalters: Кстати, метод, который вы описываете с помощью cast и memcpy(), можно использовать в чистом C для загрузки на месте, если вы эмулируете виртуальные функции через явную запись указателя VFTable в структуре POD. Удобнее делать это на C++, потому что после первоначального исправления код для использования объектов (обычный C++) намного чище, чем прыгать через обручи VF-эмуляции.   -  person Adisak    schedule 16.12.2009


Ответы (10)


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

По сути, мы создаем объекты на стороне консоли (Playstation) в варочном устройстве для ПК. Затем, чтобы уменьшить перегрузку фрагментацией, мы упаковываем объекты данных в непрерывный буфер с помощью одного alloc. Затем ссылки на объекты данных в этом буфере будут изменены, чтобы вычесть базу из указателей на смещения (вызов unfix — у нас также были виртуальные вызовы fix/unfix, которые брали базу буфера и могли конвертировать между смещениями и указателями).

Когда мы загружали данные, они загружались одним большим блоком. Все данные, на которые ссылается корень, были вне корневого объекта. Мы могли бы сделать «новое» на месте в корне, которое инициализировало бы правильные таблицы VF для объекта и исправило бы все присоединенные блоки (выполнив новое на месте, а затем исправив присоединенные блоки соответственно).

Нам нужны были вызываемые конструкторы (вместо новых) для создания правильных VF-таблиц в объектах. Однако если бы указатели были автоматически очищены до NULL во время конструктора, мы бы потеряли данные смещения и не смогли бы воссоздать указатели между объектами в непрерывном блоке.


FWIW, это обычная техника в мире видеоигр. Эта статья Gamasutra (не написанная мной или моими коллегами) объясняет в подробно опишите то же самое, что они сделали в другой компании:

Кроме того, эта тема обсуждения на SourceForge.

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

Поиск в Google по запросу «загрузка на месте» даст много других примеров людей, использующих этот метод, который в основном требует неинициализированных указателей.


ПРИМЕЧАНИЕ. В настоящее время это единственный ответ, который фактически отвечает на заданный вопрос («Есть ли применение для неинициализированных указателей в C или C++?»), Указав конкретное использование указателей, которые должны оставаться унитаризированными.

Все остальные ответы являются лучшими ответами на исходный вопрос, на который ссылается («[C++] Почему указатели не инициализируются NULL по умолчанию?»), что заставило автора задать этот вопрос.

person Adisak    schedule 15.12.2009
comment
Вы понимаете, что то, что вы описали, является одним огромным непереносимым, специфичным для реализации хаком, верно? В частности, нет никакой гарантии, что любые манипуляции с байтами, которые вы можете выполнить в памяти, в которую вы затем поместите новый объект, каким-либо образом отразится в значениях полей этого объекта. Не только это, но и не-POD (и вы упоминаете vtables, так что это не-POD) имеют определяемый реализацией объем дополнительных данных при запуске, и, конечно, заполнение между полями также определяется реализацией. - person Pavel Minaev; 16.12.2009
comment
Да. Мы использовали его в очень специфических случаях на PS и XBOX. Вопрос задавался, есть ли причина что-то делать. Мы сделали это, потому что загрузка нашей игры заняла секунды, а не минуты (загрузка одного большого блока вместо множества созданных и загруженных маленьких объектов). Это также сэкономило массу памяти и фрагментации. В мире видеоигр (и встраиваемых системах в целом) такие хаки часто необходимы для достижения целей производительности и памяти. - person Adisak; 16.12.2009
comment
обратите внимание, он сказал встроенные системы. мне кажется, что в том, что касается оптимизации, она была оправдана бесплатной для всех. - person Matt Joiner; 16.12.2009
comment
Да... это то удовольствие, которое мы получали в старые времена, работая с системами, у которых было меньше общей памяти, чем у современных ПК для кэша L1. - person Adisak; 16.12.2009

Прежде всего, инициализация указателей (или любых других переменных) по умолчанию не нарушает совместимость с C. И C, и C++ утверждают, что значение неинициализированной переменной неопределенно; на практике это означает, что он может содержать любое значение (включая представление ловушки), но обратите внимание, что 0 принадлежит множеству «любых значений»! Таким образом, соответствующая реализация может прекрасно инициализировать все указатели на 0. Однако ваша программа, если она будет полагаться на это, не будет соответствовать требованиям.

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

void foo(int*& p) {
   p = new int;
}

int* p; // why initialize? we overwrite it anyway
foo(p);

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

bool try_parse_int(const char* s, int& n)
{
    // if parsed successfully, assign result to n and return true
    // if there was error parsing, don't touch n and return false
    ...
}

int n;
if (try_parse_int(s, n)) {
    // use n here
    ...
} else {
   // don't use n here
   ...
}

Компилятору гораздо труднее проанализировать его, даже если у него есть полные определения всех функций.

person Pavel Minaev    schedule 15.12.2009

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

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

person Mark Ransom    schedule 15.12.2009
comment
Не так ли: не платите за то, чем не пользуетесь? - person pmr; 16.12.2009
comment
Заставляет меня вспомнить старую рекламу масляного фильтра Fram: заплати мне сейчас или заплати мне позже. Когда я делал коммерческие продукты на C, мы использовали собственный распределитель памяти. В режиме отладки мы бы инициализировали все блоки памяти 0xa5a5a5a5. Когда что-то взрывалось, вы могли сразу сказать, что что-то не было инициализировано. - person Peter Rowell; 16.12.2009
comment
@Peter: я видел, как помнил 0XDEADBEEF из прошлого, когда память Amiga освобождалась для ядра. Отладка MS VC++ также использует 0xFD, 0XDD, 0XCC и 0xCD для шаблонов отладки. - person Adisak; 16.12.2009
comment
@Adisak, компилятор может инициализировать память, даже если вы не делаете этого явно. Обычно это зарезервировано для отладочных сборок. У некоторых людей возникают проблемы с явной инициализацией указателей на бессмысленные значения, но я думаю, что это слишком остро. - person Mark Ransom; 16.12.2009
comment
@Mark: Да, и это именно то, что VC++ делает в отладочных сборках для глобальных, статических и локальных (стековых) переменных, а также создает шаблоны для первоначально выделенной памяти. Однако он не выполняет никакой инициализации памяти во время конструкторов, поэтому вы можете выполнить хак с новыми предварительно отформатированными данными на месте. Я только что написал консольную систему памяти, и я добавляю к ней инициализацию бессмысленного шаблона для целей отладки (ее можно легко включить/выключить) - person Adisak; 16.12.2009

Использовать для унифицированного указателя? См. std::strtol().

char * bad;
long n = std::strtol( "123xyz", &bad, 10 );

после этого вызова bad будет содержать указатель на 'x'. Нет смысла его инициализировать.

person Community    schedule 15.12.2009
comment
+1, но опять же, это именно тот случай, когда он инициализируется позже ;-) - person Michael Krelin - hacker; 16.12.2009
comment
char* плохой; это, вероятно, самый простой пример на данный момент - person ; 16.12.2009

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

person Bill K    schedule 15.12.2009

Это не нарушит совместимости с C — реализация C, в которой все неинициализированные указатели на самом деле инициализируются 0 (или вообще чем угодно!) будет идеально соответствовать.

person caf    schedule 15.12.2009

Отсутствие необходимости платить за то, что вам не нужно, здесь не для совместимости с C. Это также один из самых важных принципов C++.

person KeatsPeeks    schedule 15.12.2009

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

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

Короткий ответ, однако, нет. Просто присвойте ему значение NULL.

person McPherrinM    schedule 15.12.2009
comment
Если, конечно, вы не OPENSSH и не используете его для генерации источника случайных данных :-) - person kdgregory; 16.12.2009

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

person Michael Krelin - hacker    schedule 15.12.2009
comment
Комментарий Нила Баттерворта к моему ответу. - person John; 16.12.2009
comment
Если я правильно понимаю, комментарий Нила был о предложении Роберта Страуструпу. Это нарушит совместимость. - person Michael Krelin - hacker; 16.12.2009
comment
О, ладно... неправильно понял контекст, но, видимо, это все еще интересный вопрос. ;) - person John; 16.12.2009
comment
И, надеюсь, у вас достаточно ответов на выбор. - person Michael Krelin - hacker; 16.12.2009

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

void interruptHandler()
{
    char* ptr0;
    ...
    char* ptrX-1;
    char* ptrX;
}

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

person Doug T.    schedule 15.12.2009
comment
На самом деле в наши дни это гораздо менее важно, потому что современный оптимизирующий компилятор в большинстве случаев сможет доказать себе, что инициализированное значение всегда было перезаписано, и поэтому безопасно опустить его в скомпилированном виде. (Кстати, ptrX-1 не является допустимым именем переменной;) - person caf; 16.12.2009
comment
@caf, вероятно, это правда, если на вашей платформе нет только компилятора c, написанного Joe's Compiler Shop :) - person Doug T.; 16.12.2009
comment
Эй... ты издеваешься над нашими компиляторами или просто скромный?? В любом случае, «демы дерутся» со словами. - person San Jacinto; 16.12.2009