Чи можна ініціалізувати вказівник C на NULL?


90

Я писав такі речі

char *x=NULL;

припускаючи, що

 char *x=2;

створив би charвказівник на адресу 2.

Але в підручнику з програмування GNU C сказано, що int *my_int_ptr = 2;ціле число зберігається 2до будь-якої випадкової адреси, my_int_ptrколи воно виділяється.

Здається, це означає, що мій власний char *x=NULLприсвоює будь-яке значення NULLприведення charдо якоїсь випадкової адреси в пам'яті.

Поки

#include <stdlib.h>
#include <stdio.h>

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

насправді друкує

дорівнює NULL

коли я компілюю і запускаю його, мене турбує те, що я покладаюся на невизначену поведінку або, принаймні, недостатньо визначену поведінку, і що я повинен писати

char *x;
x=NULL;

замість цього.


72
Існує дуже заплутана різниця між тим, що int *x = whatever;робить, і тим, що int *x; *x = whatever;робить. int *x = whatever;насправді поводиться як int *x; x = whatever;ні *x = whatever;.
user2357112 підтримує Моніку

78
Здається, цей підручник неправильно сприйняв це заплутане розмежування.
user2357112 підтримує Моніку

51
Так багато дерьмових підручників в Інтернеті! Негайно припиніть читати. Нам дійсно потрібен чорний список ТАК, де ми можемо публічно ганьбити безглузді книги ...
Лундін,

9
@MM, що не робить його менш дерьмовим у 2017 році. З огляду на еволюцію компіляторів та комп’ютерів з 80-х років, це фактично те саме, що якби я був лікарем і читав книги з медицини, написані протягом 18 століття.
Лундін,

13
Я не думаю , що цей підручник кваліфікується як « The GNU C Програмування Підручник» ...
marcelm

Відповіді:


114

Чи можна ініціалізувати вказівник C на NULL?

TL; DR Так, дуже.


Фактичне твердження , зроблене на направляючої читається як

З іншого боку, якщо ви використовуєте лише одне початкове призначення, int *my_int_ptr = 2;програма спробує заповнити вміст місця, на яке вказує символ, my_int_ptrзначенням 2. Оскільки my_int_ptrзаповнене сміття, це може бути будь-яка адреса. [...]

Ну, вони є неправильно, ви маєте рацію.

Для твердження ( ігноруючи, наразі, той факт, що вказівник на цілочисельне перетворення є поведінкою, визначеною реалізацією )

int * my_int_ptr = 2;

my_int_ptrє змінною (тип вказівника на int), вона має власну адресу (тип: адреса вказівника на ціле число), ви зберігаєте значення 2в цій адресі.

Зараз, my_int_ptrбудучи типом вказівника, ми можемо сказати, він вказує на значення "type" у місці пам'яті , вказане значенням, що зберігається в my_int_ptr. Таким чином, ви істотно привласнюючи значення з змінної покажчика, а НЕ значення осередки пам'яті , на яку вказує покажчик.

Отже, для висновку

 char *x=NULL;

ініціалізує змінну вказівника xна NULL, а не значення за адресою пам'яті, на яку вказує вказівник .

Це те саме , що

 char *x;
 x = NULL;    

Розширення:

Тепер, будучи суворо відповідним, твердження типу

 int * my_int_ptr = 2;

є незаконним, оскільки передбачає порушення обмежень. Щоб бути зрозумілим,

  • my_int_ptr є змінною типу, тип int *
  • ціла константа, 2має тип int, за визначенням.

і вони не є "сумісними" типами, тому ця ініціалізація недійсна, оскільки порушує правила простого присвоєння, згадані в розділі §6.5.16.1 / P1, описаному у відповіді Лундіна .

Якщо когось цікавить, як ініціалізація пов’язана з простими обмеженнями присвоєння, цитування C11, глава §6.7.9, P11

Ініціатором для скаляра повинен бути єдиний вираз, необов’язково укладений у фігурні дужки. Початкове значення об'єкта - значення виразу (після перетворення); застосовуються ті самі обмеження типу і перетворення, що і для простого присвоєння, приймаючи тип скаляра як некваліфіковану версію заявленого типу.


@ Random832n Вони є неправильно. Я цитував відповідну частину у своїй відповіді, будь ласка, виправте мене, якщо інакше. О, і наголос навмисно.
Sourav Ghosh,

"... є незаконним, оскільки передбачає порушення обмежень. ... цілочисельний літерал, 2 має тип int, за визначенням." є проблематичним. Це звучить як тому 2, що це int, завдання є проблемою. Але це більше, ніж це. NULLтакож може бути an int, an int 0. Це просто char *x = 0;чітко визначено і char *x = 2;ні. 6.3.2.3 Покажчики 3 (До речі: С не визначає цілочисельний літерал , тільки строковий літерал і з'єднання Літерал . 0Являє собою ціле число , константа )
chux - Моніка відновило

@chux Ви дуже праві, але чи не так char *x = (void *)0;, щоб відповідати? чи це лише з іншими виразами дає значення 0?
Sourav Ghosh,

10
@SouravGhosh: цілочисельні константи зі значенням 0є особливими: вони неявно перетворюють на нульові покажчики окремо від звичайних правил для явного перекидання загальних цілочисельних виразів на типи покажчиків.
Стів Джессоп,

1
Мова, описана в Довідковому посібнику 1974 року, не дозволяла деклараціям визначати вирази ініціалізації, а відсутність таких виразів робить "використання дзеркал оголошень" набагато практичнішим. Синтаксис int *p = somePtrExpressionIMHO досить жахливий, оскільки схоже, що він встановлює значення, *pале насправді встановлює значення p.
supercat

53

Підручник неправильний. В ISO C int *my_int_ptr = 2;є помилка. У GNU C це означає те саме, що і int *my_int_ptr = (int *)2;. Це перетворює ціле число 2на адресу пам'яті, певним чином, як визначає компілятор.

Він не намагається зберігати що-небудь у місці, адресованому цією адресою (якщо воно є). Якщо ви продовжували писати *my_int_ptr = 5;, то він спробував би зберегти номер 5у місці, адресованому цією адресою.


1
Я не знав, що перетворення цілого числа на покажчик визначається реалізацією. Спасибі за інформацію.
taskinoor або

1
@taskinoor Будь ласка, зверніть увагу, що перетворення відбувається лише у тому випадку, якщо ви змусите його виконати приклад, як у цій відповіді. Якби не акторський склад, код не повинен компілюватися.
Лундін,

2
@taskinoor: Так, різні конверсії в C досить заплутані. Це Q містить цікаву інформацію про перетворення: C: Коли кастинг між типами покажчиків не є невизначеною поведінкою? .
sleske

17

Щоб пояснити, чому підручник помиляється, int *my_int_ptr = 2;є "порушенням обмеження", це код, який заборонено компілювати, і компілятор повинен надати вам діагностику при зустрічі з ним.

Відповідно до 6.5.16.1 Просте призначення:

Обмеження

Має місце одне з наступного:

  • лівий операнд має атомний, кваліфікований або некваліфікований арифметичний тип, а правий має арифметичний тип;
  • лівий операнд має атомну, кваліфіковану або некваліфіковану версію структури або типу об'єднання, сумісну з типом правого;
  • лівий операнд має атомний, кваліфікований або некваліфікований тип покажчика, і (враховуючи тип, який буде мати лівий операнд після перетворення lvalue), обидва операнди є вказівниками на кваліфіковані або некваліфіковані версії сумісних типів, а тип, на який вказується ліворуч, має всі кваліфікатори типу, на який вказано праворуч;
  • лівий операнд має атомарний, кваліфікований або некваліфікований тип покажчика, і (враховуючи тип, який буде мати лівий операнд після перетворення lvalue), один операнд є покажчиком на тип об'єкта, а другий - вказівником на кваліфіковану або некваліфіковану версію void, а тип, на який вказує ліворуч, має всі кваліфікатори типу, на який вказує справа;
  • лівий операнд - атомарний, кваліфікований або некваліфікований покажчик, а правий - нульова константа вказівника; або
  • лівий операнд має тип атомного, кваліфікованого або некваліфікованого _Bool, а правий - вказівник.

У цьому випадку лівий операнд є некваліфікованим покажчиком. Ніде не згадується, що правим операндом дозволено бути цілим числом (арифметичний тип). Отже, код порушує стандарт C.

Відомо, що GCC поводиться погано, якщо ви прямо не скажете, що це стандартний компілятор C. Якщо ви скомпілюєте код як -std=c11 -pedantic-errors, він правильно дасть діагностику, як це потрібно робити.


4
проголосував за припущення -пемантичних помилок. Хоча я скоріше за все буду використовувати пов'язаний -Wpedantic.
fagricipni

2
Виняток із вашого твердження про те, що правим операндом заборонено бути цілим числом: Розділ 6.3.2.3 говорить: „Цілочисельний константний вираз зі значенням 0 або такий вираз, відлитий до типу void *, називається константою нульового покажчика”. Зверніть увагу на останню крапку у вашій цитаті. Отже, int* p = 0;це легальний спосіб писати int* p = NULL;. Хоча останнє є більш чітким і більш умовним.
Девіслор,

1
Що робить патологічне затухання int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;законним теж.
Девіслор,

@Davislor, про який йдеться у пункті 5 у стандартній цитаті в цій відповіді (погодьтеся, що згодом у резюме, мабуть, слід про це згадати)
М. М.,

1
@chux Я вважаю, що добре сформована програма повинна буде intptr_tявно перетворити на один із дозволених типів праворуч. Тобто є void* a = (void*)(intptr_t)b;законним за пунктом 4, але (intptr_t)bне є ні сумісним типом покажчика, ні void*константою нульового покажчика, void* aні арифметичним типом, ні _Bool. Стандарт говорить, що перетворення є законним, але не те, що воно є неявним.
Девіслор

15

int *my_int_ptr = 2

зберігає ціле значення 2 до будь-якої випадкової адреси в my_int_ptr, коли воно призначене.

Це абсолютно неправильно. Якщо це насправді написано, будь ласка, отримайте кращу книгу чи підручник.

int *my_int_ptr = 2визначає цілочисельний покажчик, який вказує на адресу 2. Ви, швидше за все, отримаєте збій, якщо спробуєте отримати доступ до адреси 2.

*my_int_ptr = 2, тобто без intрядка, зберігає значення два до будь-якої випадкової адреси my_int_ptr, на яку вказує. Сказавши це, ви можете призначити NULLпокажчик, коли він визначений. char *x=NULL;цілком дійсний C.

Редагувати: Під час написання цього повідомлення я не знав, що перетворення цілого числа на покажчик - це поведінка, визначена реалізацією. Будь ласка, ознайомтеся з хорошими відповідями @MM та @SouravGhosh для деталей.


1
Це абсолютно неправильно, оскільки це порушення обмеження, а не з будь-якої іншої причини. Зокрема, це неправильно: "int * my_int_ptr = 2 визначає цілочисельний покажчик, який вказує на адресу 2".
Лундін,

@Lundin: Ваша фраза "не з будь-якої іншої причини" сама по собі є неправильною та оманливою. Якщо ви вирішите проблему сумісності типів, ви все ще залишаєтеся з тим фактом, що автор навчального посібника грубо спотворює, як працюють ініціалізації показників та призначення.
Гонки легкості на орбіті

14

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

int *x = NULL;правильний C, але це дуже оманливе, я б навіть сказав безглузде, і це заважає розумінню мови багатьом новачкам. Це змушує думати, що пізніше ми могли б зробити це, *x = NULL;що, звичайно, неможливо. Розумієте, тип змінної не є int, а ім’я змінної - ні *x, а *також декларація не відіграє жодної функціональної ролі у співпраці з =. Це суто декларативно. Отже, набагато більше сенсу є ось що:

int* x = NULL;що також є правильним C, хоча воно не відповідає оригінальному стилю кодування K&R. Це чітко дає зрозуміти, що тип є int*, а змінна вказівника є x, тому навіть для непосвячених стає очевидним, що значення NULLзберігається x, яке є вказівником на int.

Крім того, це полегшує виведення правила: коли зірка знаходиться далеко від імені змінної, тоді це декларація, тоді як зірка, прикріплена до імені, є розмежуванням покажчика.

Отже, тепер стає набагато зрозумілішим, що далі ми можемо або робити, x = NULL;або *x = 2;іншими словами, новачкові легше бачити, як це variable = expressionведе до pointer-type variable = pointer-expressionта dereferenced-pointer-variable = expression. (Для ініційованих, під виразом 'я маю на увазі' rvalue '.)

Невдалий вибір у синтаксисі мови полягає в тому, що при оголошенні локальних змінних ви можете сказати, int i, *p;яке оголошує ціле число та вказівник на ціле число, тому це змушує вважати, що *це корисна частина імені. Але це не так, і цей синтаксис - це просто химерний особливий випадок, доданий для зручності, і, на мій погляд, він ніколи не мав би існувати, оскільки він анулює правило, яке я запропонував вище. Наскільки мені відомо, ніде більше в мові цей синтаксис не має значення, але навіть якщо він є, це вказує на розбіжність у способі визначення типів покажчиків у C. Всюди, в оголошеннях з однією змінною, у списках параметрів, у членах struct та ін. Ви можете оголосити свої вказівники type* pointer-variableзамість type *pointer-variable; це цілком законно і має більше сенсу.


int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Я повинен погодитися не погодитися. It makes one think.... перестань думати, спочатку прочитай книгу С, не ображайся.
Sourav Ghosh

^^ це мало б для мене цілковитий сенс. Отже, я вважаю, що це суб’єктивно.
Mike Nakis

5
@SouravGhosh Щодо думки, я думаю, що C повинен був бути розроблений таким чином, щоб int* somePtr, someotherPtrдекларувати два вказівники, насправді, я раніше писав, int* somePtrале це призводить до помилки, яку ви описуєте.
fagricipni

1
@fagricipni Я через це перестав використовувати синтаксис декларації декількох змінних. Я оголошую свої змінні по одному. Якщо я дійсно хочу, щоб вони були в одному рядку, я розділяю їх крапками з комою, а не комами. "Якщо місце погане, не ходіть туди".
Mike Nakis

2
@fagricipni Ну, якби я міг розробити Linux з нуля, я б використав createзамість creat. :) Справа в тому, що це так, і нам потрібно формувати себе, щоб адаптуватися до цього. Все зводиться до особистого вибору в кінці дня, погодьтеся.
Sourav Ghosh

6

Я хотів би додати щось ортогональне до багатьох чудових відповідей. Насправді ініціалізація до NULLє далеко не поганою практикою і може бути корисною, якщо цей вказівник може використовуватися чи не використовуватись для зберігання динамічно виділеного блоку пам'яті.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Оскільки згідно зі стандартом ISO-IEC 9899 free є аргументом nop, коли аргумент є NULL, код вище (або щось більш значуще в тому ж напрямку) є законним.


5
Надлишковим є викид результату malloc на C, якщо тільки цей код C також не повинен компілюватися як C ++.
кішка

Ви маєте рацію, void*конвертується за потреби. Але наявність коду, який працює з компілятором C та C ++, може мати переваги.
Luca Citi,

1
@LucaCiti C та C ++ - це різні мови. Помилки чекають на вас лише в тому випадку, якщо ви намагаєтесь скомпілювати вихідний файл, написаний для одного, використовуючи компілятор, призначений для іншого. Це все одно, що спробувати написати код C, який можна скомпілювати за допомогою інструментів Pascal.
Evil Dog Pie

1
Хороша порада. Я (намагаюся) завжди ініціалізувати свої константи вказівника чимось. У сучасній C це, як правило, може бути їх кінцевим значенням, і вони можуть бути constпокажчиками, оголошеними в medias res , але навіть тоді, коли вказівник повинен бути змінним (як той, який використовується в циклі або by realloc()), встановлюючи його для NULLлову помилок там, де він використовувався раніше він встановлюється з його реальною вартістю. У більшості систем розмежування посилань NULLвикликає сегментацію в момент відмови (хоча є винятки), тоді як неініціалізований покажчик містить сміття, а запис у нього пошкоджує довільну пам'ять.
Девіслор

1
Крім того, дуже легко побачити в налагоджувачі, що містить покажчик NULL, але дуже важко відрізнити покажчик сміття від дійсного. Тож корисно забезпечити, щоб усі вказівники завжди були або дійсними, або NULLз моменту оголошення.
Девіслор,


1

Це вірно.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Ця функція є правильною для того, що вона робить. Він призначає адресу 0 покажчику символу x. Тобто вказує покажчик x на адресу пам'яті 0.

Альтернатива:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Я думаю, що ви хотіли:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.

"Він призначає адресу 0 покажчику символу x." -> Можливо. C не вказує значення покажчика, лише це char* x = 0; if (x == 0)буде правдою. Покажчики не обов'язково цілі числа.
chux

Це не 'спрямовує покажчик x на адресу пам'яті 0'. Він встановлює значення покажчика на невизначене недійсне значення, яке можна перевірити , порівнявши його з 0 або NULL. Фактична операція визначається реалізацією. Тут немає нічого, що відповідає на актуальне питання.
Маркіз Лорнський,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.