NULL == nullptr в C++11?
не совcем. NULL большинством компиляторов с легкостью трактуется как 0 (на самом деле это обычно define). Но нет 100 гарантии, что это так. А вот nullptr — это известная для компилятора константа и компилятор знает, что это такое.
6 дек 2013 в 13:29
@KoVadim, можно сказать, что надежнее будет инициализировать указатели nullptr, для дальнейшей проверки на корректность, нежели NULL?
6 дек 2013 в 13:58
nullptr как раз специально ввели, что бы решить часть неоднозначных ситуаций с NULL. Это все лучше утрясется в голове, если понимать, то компилятор не видет NULL, он видит обычно вместо него 0 (препроцессор постарался). А вот nullptr виден. И компилятор может сделать анализ.
6 дек 2013 в 14:43
@SoloMio, не совсем. Везде где идет речь про указатели НУЖНО использовать null_ptr. Это гарантия того, что компилятор попробует застраховать программиста от ошибки.
Нулевые указатели (null и nullptr) в C++. Учимся ходить по граблям изящно
В этом материале для новичков мы рассуждаем про обнаружение в коде C++ распространенного дефекта «разыменование нулевого указателя», попутно объясняя его скрытую коварность.
Курс Англійської.
Онлайн-навчання англійської за методикою Кембриджу — вибір понад мільярда людей.
1. Разыменование нулевого указателя
Сегодня рассмотрим причину дефекта в коде С++, который получается, если программа обращается по некорректному указателю к какому-то участку памяти. Такое обращение ведет к неопределенному поведению программы, что приводит в большинстве случаев к аварийному завершению. Данный дефект получил название разыменование нулевого указателя ( CWE-476 ). Мы поговорим о том, что такое NULL и nullptr и для чего они нужны.
По сути, это почти одинаковые вещи, но есть нюансы.
Курс Управління командою в бізнесі.
Онлайн-курс для ефективного управління командою, спрямований на створення проактивних та самостійних команд, де мікроменеджмент не потрібний.
#include #include using namespace std; /* * Работа с динамической памятью. Нулевые указатели */ void main()
Язык С++ не имеет автоматического сборщика мусора, как, например, в Java или C#. Если мы выделяем область под данные, то никто кроме нас не позаботится о том, чтобы область памяти была очищена. Если в памяти находится одно число, это не является проблемой.
Однако, если, например, программа работает в цикле, используя миллион итераций, и в каждой итерации запрашиваются в операционной системе новое место под данные, то тут возможны проблемы. На каком-то этапе место в памяти заканчивается и наша программа аварийно завершает работу. Поэтому, чтобы избежать подобной ситуации, следует использовать delete для очистки неиспользуемых данных, указав указатель на область данных.
Если вы начинающий программист, возьмите себе за правило, каждый раз, как только вы используете оператор new, выделяя под что-то память, тут же (в этой же функции) писать deletе , чтобы потом не забыть сделать это. Это избавит вас от потенциальных проблем с утечкой памяти.
Итак, в нашем коде (пример выше) с помощью оператора new мы выделили место для нашей оперативной памяти. После очистки места, которое мы выделяли под данные в динамической части оперативной памяти, сами данные исчезают. В следствии действия оператора delete данные уничтожаются и система может выделять память, которую мы уже не используем, для любых других своих нужд.
Однако, у нас остается проблема! В нашем указателе *pa все еще сохранен адрес на тот участок памяти, где у нас лежали данные и, в принципе, нам никто не запрещает туда обращаться.
Мы можем туда что-нибудь записать или получить данные, которые там находятся. А можем что-нибудь повредить либо получить некорректные данные, далее невольно начать с ними работать если у нас есть подобная ошибка в логике. В нашем случае, например, мы можем по указателю получить вот такое число — 842150451 .
Короче говоря, надеюсь, вы поняли эту тонкость, что после деинициализации ранее занятой памяти наш старый указатель может продолжать указывать в удаленное место памяти, что потенциально может привести к краху программы.
Курс Микросервисная архитектура.
програма, яка допоможе опанувати головні принципи розробки мікросервісної архітектури, щоби ви могли проєктувати незалежні сервіси, а потім інтегрувати їх в одну систему. Практики буде багато.
2. Нулевое значение и нулевые указатели
Если мы ведем какие-то расчеты и вдруг получаем некоторое случайное число (не имеющее отношение к расчетам), мы можем даже не узнать, что что-то пошло не так. Неплохо было бы определиться и отметить указатель каким-то таким образом, чтобы он никуда не указывал (чтобы однозначно исключить патовую ситуацию описанную в примере выше).
Для этого и существуют NULL и nullptr . Обратите внимание, что если у нас сейчас вызывается оператор delete на нашем указателе (мы очищаем находящуюся по нему память), то оттуда данные теряются.
Если мы опять принудительно выведем на консоль эти данные (из освобожденного участка памяти), то в принципе, у нас может случиться чудо — мы увидим в консоли тот «мусор», который сейчас в памяти (куда указывает наш указатель, после того как мы его почистили).
void main()
Но, если мы еще раз возьмем и вызовем оператор delete pa , то все закончится очень плохо — мы увидим на экране сообщение об ошибке. Она говорит о том, что возникла проблема при работе с кучей, то есть с динамической памятью.
void main()
Для того, чтобы избежать такой проблемы мы можем использовать NULL .
3. NULL и nullptr
В таких языках программирования как Java или C#, NULL является отдельным типом данных и там ситуация несколько иная. В случае С++ мы имеем дело с NULL и nullptr .
nullptr — это более новая разработка, добавленная в С++ 11, и она уже работает аналогично тому как это реализовано в Java или C#. nullptr это отдельный тип данных, с которым компилятор ничего спутать не может. Что же касается NULL , то это просто эквивалент записи 0 (ноль).
Если мы напишем pa = 0 , то это значит, что наш указатель pa теперь не будет хранить адрес в памяти, а будет нулевым указателем, указывать на ноль. Вместо pa = 0 , мы можем записать pa = NULL — эти записи абсолютно равнозначны. Все дело в том, что NULL — это просто макрос.
Если мы наведем на него мышку, поставим курсор и нажимаем f12 , то увидим #define NULL 0 .
Курс Python basic.
Після курсу ви зможете впевнено працювати з чатботами, скриптами, вбудованими системами, веб- та мобільними застосунками, а також навіть ігровими програмами.
Строка pa = NULL говорит указателю, который до момента выполнения данной строки (где мы уже почистили память) указывает на определенный адрес оперативной памяти, чтобы он этот адрес забыл, чтобы мы к этому адресу впоследствии случайно не обратились. После того как мы присвоили NULL , у нас одни нули, т.е. нулевой указатель.
Если после такой операции мы попробуем еще раз сделать delete pa , то у нас все пройдет без проблем. Оператор delete посмотрит на то, что указатель указывает на NULL и не будет пытаться там что-то очистить, поэтому ошибку не получим. Теперь также мы явно можем проверять наш указатель на NULL , то есть на то, содержит ли он какой-то адрес или нет.
Если сейчас попробовать обратиться через cout , то в консоль будет выведен наш адрес — одни нули.
Добавим проверку if pa != 0 или if pa != NULL с возможностью выводить наш адрес указателя. В данном случае адрес не вывелся, поскольку указатель указывает на NULL . А раз он указывает на NULL , то он в принципе ничего не может хранить.
Таким образом мы перестраховываемся от того, чтобы получить или случайно записать некорректные данные.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Если мы уберем запись pa = NULL , то не сможем знать, куда указывает указатель, мы не можем перебрать все возможные адреса и знать что там лежит. Поэтому мы получим вывод нашего адреса и ошибку.
4. Тип данных nullptr
Возникает вопрос — для чего нужен отдельный тип данных nullptr? В принципе, мы можем использовать и NULL . В данном случае работать это будет точно так же.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Это уже не просто макрос и не просто нолик, а целочисленный тип Int . Это уже — отдельный тип данных. Мы его присваиваем и, на первый взгляд, разницы никакой нет. Однако для компилятора разница есть, он никогда не перепутает указатель nullptr с целочисленным типом данных.
К примеру, если у вас будет какая-то функция, она будет перегружена для типа Int и для указателя. И вы захотите передать в вашу функцию указатель с целочисленным нулем pa = 0 :
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Ваша функция принимает либо указательную Int , либо просто целочисленный тип Int . В таком случае у компилятора могут быть проблемы какую функцию вызвать — с реализацией получается неоднозначность.
В С++ 11 nullptr это отдельный тип данных и компилятор никогда не перепутает его с обычным int . Поэтому в случаях когда вы будете работать с указателями, рекомендуется использовать именно его.
Если вы встретите где-то старый код, вы можете увидеть запись с присвоением нуля pa = NULL; (pa = 0;) . Теперь вы будете знать, что это такое и какие могут быть проблемы. Справедливости ради нужно сказать, что на самом деле проблемы возникают редко, но чтобы исключить их вообще, лучше использовать nullptr . Это хоть и редкий тип проблем, но очень коварный и трудно вычислимый.
Также стоит обратить внимание на еще один тип ошибок. Если вам нужно очистить динамическую память, в которой выделено место под ваши данные, то обязательно сначала нужно вызвать delete , ну, а затем, если нужно затереть адрес — присваивать нашему указателю nullptr .
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Если вы сделаете наоборот, то есть сначала присвоите указателю nullptr , а затем присвоите указателю delete , то такое ваше действие приведет к утечке памяти.
void main() < int *pa = new int; *pa = 10; cout delete pa; >
Представьте себе, что мы имеем дело с программой, где указатель ссылается на определенные данные в оперативной памяти.
Если мы вызываем сначала delete , а затем присваиваем NULL , то сначала убиваются данные, а затем теряется и адрес, который хранил указатель. Однако, если вы сначала используете nullptr , тогда вы просто убираете адрес, но данные никуда не деваются, они так и остаются висеть в оперативной памяти.
Получается, что после того, как вы присвоили nullptr , но не удалили данные, вы уже никаким образом к ним не достучитесь — они останутся там висеть до тех пор, пока выполняется ваша программа.
Если в результате таких утечек памяти ваша программа упадет (если подобных утечек будет много), либо просто аварийно завершится, операционная система сама почистит память. Поэтому всегда нужно сначала удалять данные из динамической памяти, а затем присваивать nullptr , но ни коем случае не наоборот!
Заключение
Надеемся, что наш материал поможет вам избежать частых проблем при работе с памятью в C++. В заключение рекомендуем посмотреть видео, в котором рассказывается про указатели в С++
а также про работу с динамической памятью при работе с массивами
c++ Null vs nullptr
Добрый день, вчера пытался разобраться в чем преимущество (конкретно, а не в виде абстрактных понятий типа ООП и прочая эзотерика) nullptr против NULL. Ничего внятного (для простых инженеров, а не всяких там страуструпов) не нашел, кроме синтетического примера
void foo(int *) < coutvoid foo(int) < coutfoo(NULL); foo(nullptr);
foo(int) foo(int *)
Но этого что-то мало и вроде я такое раз в год вызываю, может кто-нибудь покажет доступные, бытовые примеры когда nullptr лучше и удобней чем NULL.
da17 ★
28.01.19 13:25:53 MSK
Но этого что-то мало и вроде я такое раз в год вызываю
Так можно про многие правила из стандарта сказать, типа я это почти не использую, а потому не нужно.
seiken ★★★★★
( 28.01.19 13:44:42 MSK )
Ответ на: комментарий от seiken 28.01.19 13:44:42 MSK
Так из-за этого и спрашиваю, где это используется?
da17 ★
( 28.01.19 13:51:37 MSK ) автор топика
у меня твой пример не компилируется.
error: call of overloaded ‘foo(NULL)’ is ambiguous
conalex ★★★
( 28.01.19 13:57:09 MSK )
Ответ на: комментарий от conalex 28.01.19 13:57:09 MSK
MSVC 2013 собирал, там норм прошло
da17 ★
( 28.01.19 14:16:43 MSK ) автор топика
Ответ на: комментарий от da17 28.01.19 13:51:37 MSK
NULL это просто 0, ещё и определённый макросом, т.е. компилятор получит просто 0 в этом куске кода, некоторые конструкции аля auto не поймут что именно ты хотел использовать число или указатель
sparks ★★★
( 28.01.19 14:33:13 MSK )
Effective Modern C++ Майерса.
Item 8: Prefer nullptr to 0 and NULL.
Показано развитие приведённого тобою примера для случая когда такие перегрузки за шаблонами скрыты.
mkam ★
( 28.01.19 14:36:12 MSK )
nullptr нельзя по ошибке присвоить переменной, не являющейся указателем, с NULL такое скомпилируется и может потом привести к многочасовой отладке
annulen ★★★★★
( 28.01.19 14:40:07 MSK )
Ответ на: комментарий от annulen 28.01.19 14:40:07 MSK
da17 ★
( 28.01.19 15:08:26 MSK ) автор топика
На будущее: когда тебе говорят что NULL это легаси и нужно пользоваться nullptr, тебе нужно просто это сделать а не задаваться бесполезными вопросами.
Если же тебе неймётся, все ответы есть на cppreference. NULL - это implementation-defined макрос который может в зависимости от реализации раскрываться в 0 или nullptr, поэтому вопрос сводится к тому почему нельзя использовать 0 (или, что эквивалентно, макрос который может в него раскрыться). Потому что это нарушает типобезопасность - 0 можно передать в функцию принимающую int, или сравнить с int’ом. Если ты сравниваешь NULL с int’ом, или передаёшь в качестве int’а - это явная ошибка, но в этом случае компилятор её не отловит. У nullptr таких проблем нет. Кроме того, он может участвовать в перегрузках (https://en.cppreference.com/w/cpp/types/nullptr_t).
slovazap ★★★★★
( 28.01.19 15:09:25 MSK )
Ответ на: комментарий от da17 28.01.19 15:08:26 MSK
void f(int a); f(nullptr); // error
annulen ★★★★★
( 28.01.19 15:14:36 MSK )
Ответ на: комментарий от slovazap 28.01.19 15:09:25 MSK
сравниваю NULL с int
#include #include class Keker< >; int main()
Компилятор ругается In function 'int main()': 12:16: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
da17 ★
( 28.01.19 15:16:36 MSK ) автор топика
Дальше не читал
Gvidon ★★★★
( 28.01.19 15:19:10 MSK )
Ответ на: комментарий от Gvidon 28.01.19 15:19:10 MSK
Я прочитал пример https://en.cppreference.com/w/cpp/types/nullptr_t т.е. если я все правильно понял, nullptr помогает явно указать, что должна вызываться ф-ия принимающая указатель и если у программиста функция, принимающая foo(int), а он где-то в дебрях кода передает ей нулевой указатель путем foo(NULL), то возможно он ошибся и nullptr от таких косяков уберегает, т.е. если даже программист забудет, чего принимает ф-ия, компилятор его поправит при nullptr, чего не будет при NULL?
da17 ★
( 28.01.19 15:30:21 MSK ) автор топика
Ответ на: комментарий от Gvidon 28.01.19 15:19:10 MSK
да я пошутил, а то иногда напишут вместо пояснения «НАРУШАЕТ ПРИНЦИПЫ ООП» и гадай, что оно там нарушает
da17 ★
( 28.01.19 15:32:18 MSK ) автор топика
История примерно такая:
1) В С++ запретили неявное приведение void* к любому другому указателю, что в С разрешено. Внезапно, отломался код
int* foo = NULL;
т.к. NULL всегда определялся как (void*)0.
2) Не унывая, в С++ решили эту проблему переиспользованием константы 0, что привело к вот такой вот радости (примерно):
#ifdef __cplusplus #define NULL 0 #else #define NULL (void*)0 #endif
Что, в свою очередь, начало весело стрелять при перегрузках и в шаблонах.
3) Компиляторы от такой весёлой жизни начали заводить свои нестандартные «нулевые указатели», чтобы с этим адом хоть как-то бороться. Например __null в gcc.
4) Пункт 3) узаконили в стандарте как nullptr
Всё это время сишники смотрят на плюсовиков как на стадо дебилов, пишут свой родной NULL и в ус не дуют.
Gvidon ★★★★
( 28.01.19 15:59:55 MSK )
Ответ на: комментарий от da17 28.01.19 15:16:36 MSK
И что вы хотели сказать этим примером? К NULL vs. nullptr он вообще никакого отношения не имеет.
slovazap ★★★★★
( 28.01.19 16:32:37 MSK )
В чистом Си у тебя ведь перегрузки функций нет? Вот там это в ногу не выстрелит. А в костылях выстрелит и будет неприятно.
Вот и пиши nullptr , чтобы избежать двусмысленности.
Deleted
( 28.01.19 16:41:55 MSK )
Ответ на: комментарий от da17 28.01.19 15:08:26 MSK
Например такой: #include
int main() < char str[] = "hello"; char* p = str; /* что-то делаем со строкой */ p = 0; >
То есть мы работаем со строкой, теперь захотели ограничить строку на текущем символе, и вместо
*p = '\0';
написали просто
p = 0;
Какой-то пример с массивом строк и присваиванием нуля, вместо '\0', был в одном из разборов PVS-studio какого-то опенсурс проекта, так что несмотря на то что пример искусственный, что-то подобное есть и в реальных проектах.
Но в gcc/clang есть полезное предупреждение
-Wzero-as-null-pointer-constant
10.9 – Нулевые указатели
Как и обычные переменные, указатели не инициализируются при создании экземпляров. Если указателю значение не присвоено, он по умолчанию будет указывать на какой-то мусорный адрес.
Помимо адресов памяти, есть еще одно дополнительное значение, которое может содержать указатель: нулевое значение. Нулевое значение – это специальное значение, которое означает, что указатель ни на что не указывает. Указатель, содержащий нулевое значение, называется нулевым указателем.
В C++ мы можем присвоить указателю нулевое значение, инициализировав или присвоив ему литерал 0:
float* ptr < 0 >; // ptr теперь является нулевым указателем float* ptr2; // ptr2 не инициализирован ptr2 = 0; // ptr2 теперь нулевой указатель
Указатели преобразуются в логическое значение false , если они равны нулю, и в логическое значение true , если они не равны нулю. Следовательно, мы можем использовать условное выражение, чтобы проверить, является ли указатель нулевым или нет:
double* ptr < 0 >; // указатели преобразуются в логическое значение false, если они равны нулю, // и в логическое значение true, если они не равны нулю if (ptr) std::cout
Лучшая практика
Если при создании вы не присваиваете указателям какое-либо значение, инициализируйте их нулевым значением.
Косвенное обращение через нулевые указатели
В предыдущем уроке мы отметили, что косвенное обращение через мусорный указатель приведет к неопределенным результатам. Косвенное обращение через нулевой указатель также приводит к неопределенному поведению. В большинстве случаев это приведет к сбою вашего приложения.
Концептуально в этом есть смысл. Косвенное обращение через указатель означает «перейти по адресу, на который указывает указатель, и получить доступ к значению там». У нулевого указателя нет адреса. Что делать, когда вы пытаетесь получить доступ к значению по этому адресу?
Макрос NULL
В C++ есть специальный макрос препроцессора под названием NULL (определен в заголовке ). Этот макрос был унаследован от C, где он обычно используется для обозначения нулевого указателя.
#include // для NULL double* ptr < NULL >; // ptr - нулевой указатель
Значение NULL определяется реализацией, но обычно определяется как целочисленная константа 0. Примечание. Начиная с C++11, NULL можно определить как nullptr (что мы обсудим позже).
Лучшая практика
Поскольку NULL – это макрос препроцессора со значением, определяемым реализацией, избегайте использования NULL .
Опасности использования 0 (или NULL ) для нулевых указателей
Обратите внимание, что значение 0 не является типом указателя, поэтому присвоение 0 (или NULL , до C++11) указателю для обозначения того, что указатель является нулевым, немного противоречиво. В редких случаях, когда ноль используется в качестве литерального аргумента, это может даже вызвать проблемы, потому что компилятор не может определить, имеет ли мы в виду нулевой указатель или целое число 0:
#include #include // для NULL void print(int x) < std::cout void print(int* x) < if (!x) std::cout int main() < int* x < NULL >; print(x); // вызывает print(int*), потому что x имеет тип int* print(0); // вызывает print(int), потому что 0 - целочисленный литерал print(NULL); // скорее всего, вызывает print(int), хотя мы, вероятно, хотели print(int*) return 0; >
В вероятном случае, когда NULL определяется как значение 0, print(NULL) вызовет print(int) , а не print(int*) , как вы могли бы ожидать от литерала нулевого указателя.
nullptr в C++11
Для решения вышеуказанных проблем в C++11 введено новое ключевое слово nullptr . nullptr – это ключевое слово, очень похожее на логические ключевые слова true и false .
Начиная с C++11, когда нам нужен нулевой указатель, следует отдавать предпочтение ему, а не нулю:
int* ptr < nullptr >; // примечание: ptr по-прежнему является указателем int, // просто он установлен в нулевое значение
C++ неявно преобразует nullptr в любой тип указателя. Итак, в приведенном выше примере nullptr неявно преобразуется в указатель int , а затем значение nullptr присваивается ptr . Это приводит к тому, что ptr , указатель int , становится нулевым указателем.
Мы также можем вызвать функцию с литералом nullptr , который будет соответствовать любому параметру, принимающему значение указателя:
#include void print(int x) < std::cout void print(int* x) < if (!x) std::cout int main() < int* x < nullptr >; print(x); // вызывает print(int*) print(nullptr); // вызывает print(int*), как и ожидалось return 0; >
Для продвинутых читателей
Функция со списком других параметров является новой функцией, даже если функция с таким же именем существует. Мы рассмотрели это в уроке «8.9 – Перегрузка функций».
Лучшая практика
Используйте nullptr для инициализации указателей нулевым значением.
std::nullptr_t
В C++11 также представлен новый тип std::nullptr_t (в заголовке ). std::nullptr_t может содержать только одно значение: nullptr ! Хотя это может показаться странным, в одной ситуации это полезно. Если мы хотим написать функцию, которая принимает только аргумент nullptr , какого типа мы сделаем параметр? Ответ – std::nullptr_t .
#include #include // для std::nullptr_t void doSomething(std::nullptr_t ptr) < std::cout int main() < doSomething(nullptr); // вызываем doSomething с аргументом типа std::nullptr_t return 0; >
Возможно, вам никогда не понадобится это использовать, но на всякий случай знать полезно.