“Int * nums = {5, 2, 1, 4}” спричиняє помилку сегментації


81

викликає сегментацію, тоді як

ні. Зараз:

відбитки 5.

Виходячи з цього, я здогадався, що нотація ініціалізації масиву, {}, сліпо завантажує ці дані в будь-яку змінну, яка знаходиться зліва. Коли це int [], масив заповнюється за бажанням. Коли це int *, покажчик заповнюється на 5, а місця пам'яті, після яких зберігається вказівник, заповнюються на 2, 1 і 4. Тож Nums [0] намагається скинути 5, викликаючи сегментацію.

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



3
Компілюйте з усіма попередженнями, і ваш компілятор повинен повідомити вам, що відбувається.
Jabberwocky

1
@GSerg Це ніде не поруч з дублікатом. У цьому питанні немає вказівника на масив. Хоча деякі відповіді в цій публікації схожі на відповіді тут.
Лундін,

2
@Lundin Я був упевнений на 30%, тому я не проголосував за закриття, лише розмістив посилання.
GSerg

3
Візьміть собі звичку запускати GCC з -pedantic-errorsпрапором і стежте за діагностикою. int *nums = {5, 2, 1, 4};не є дійсним C.
АНТ

Відповіді:


113

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

Наприклад, ви можете писати int x = {0};, що повністю еквівалентно int x = 0;.

Отже, коли ви пишете, int *nums = {5, 2, 1, 4};ви фактично надаєте список ініціалізатора одній змінній покажчика. Однак це лише одна змінна, тому їй буде призначено лише перше значення 5, решта списку ігнорується (насправді я не думаю, що код із надлишком ініціалізаторів повинен навіть компілюватись із суворим компілятором) - це не записуватись на пам’ять взагалі. Код еквівалентний int *nums = 5;. Що означає, numsслід вказувати на адресу 5 .

На цей момент ви вже мали отримати два попередження / помилки компілятора:

  • Присвоєння цілого числа покажчику без приведення.
  • Надлишки елементів у списку ініціалізатора.

І тоді, звичайно, код вийде з ладу і згорить, оскільки 5, швидше за все, це не дійсна адреса, з якої вам дозволено відмінюватися nums[0].

Як допоміжне зауваження, слід вказати printfадреси на %pспецифікатор, інакше ви використовуєте невизначену поведінку.


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

Або якщо ви хочете створити масив покажчиків:


РЕДАГУВАТИ

Після деяких досліджень я можу сказати, що «список ініціалізаторів надлишкових елементів» дійсно не є дійсним C - це розширення GCC .

Стандарт 6.7.9 Ініціалізація говорить (наголос на моєму):

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

/ - /

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

"Скалярний тип" - це стандартний термін, що стосується одиничних змінних, які не мають масиву, структури або об'єднання (вони називаються "агрегатним типом").

Отже, простою англійською мовою стандарт говорить: "коли ви ініціалізуєте змінну, сміливо кидайте кілька зайвих дужок навколо виразу ініціалізатора, просто тому, що можете".


11
Немає нічого "дурного" у можливості ініціалізувати скалярний об'єкт одним вкладеним значенням {}. Навпаки, це полегшує одну з найважливіших і найзручніших ідіом мови С - { 0 }як універсальний нульовий ініціалізатор. Все в C може бути нульовою ініціалізацією = { 0 }. Це дуже важливо для написання коду, незалежного від типу.
АНТ

3
@AnT Не існує такого поняття, як "універсальний нульовий ініціалізатор". У випадку агрегатів, {0}просто означає ініціалізацію першого об'єкта до нуля та ініціалізацію решти об'єктів так, ніби вони мають статичну тривалість зберігання. Я б сказав, що це скоріше випадково, а не навмисне мовне проектування якогось "універсального ініціалізатора", оскільки {1}не ініціалізує всі об'єкти до 1.
Лундін,

3
@Lundin C11 6.5.16.1/1 охоплює p = 5;(жоден із перерахованих випадків не відповідає призначенню цілого числа покажчику); і 6.7.9 / 11 говорить, що обмеження для призначення також використовуються для ініціалізації.
М.М.

4
@ Лундін: Так, є. Зовсім неважливо, який механізм ініціалізує яку частину об’єкта. Також абсолютно неважливо, чи {}дозволяється спеціально ініціалізація скалярів з цією метою. Важливо лише те, що = { 0 }ініціалізатор гарантовано нульово ініціалізує весь об’єкт , що саме зробило його класичним та однією з найвишуканіших ідіом мови C.
АНТ

2
@Lundin: Мені також абсолютно незрозуміло, яке відношення {1}має ваше зауваження до цієї теми. Ніхто ніколи не стверджував, що {0}трактує це 0як мультиініціалізатор для кожного члена сукупності.
АНТ

28

СЦЕНАРІЙ 1

Чому це один segfault?

Ви оголосили numsяк вказівник на int - тобто він numsповинен містити адресу одного цілого числа в пам'яті.

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

У будь-якому випадку (призначити кілька показників покажчику або змінній int), тоді відбувається те, що змінна отримає перше значення, яке є 5, тоді як інші значення ігноруються. Цей код відповідає, але ви отримаєте попередження для кожного додаткового значення, яке не повинно бути в присвоєнні:

warning: excess elements in scalar initializer.

У випадку присвоєння кількох значень змінної покажчика програма здійснює сегментацію, коли ви отримуєте доступ nums[0], що означає, що ви захищаєте те, що зберігається в адресі 5, буквально. Ви не виділили жодної дійсної пам'яті для вказівникаnums цьому випадку .

Варто зауважити, що для випадку присвоєння кількох значень змінній int не існує сегментатора (тут ви не відміняєте жодного недійсного вказівника).


СЦЕНАРІЙ 2

Цей не є сегментарним, оскільки ви легально виділяєте масив з 4-х дюймів у стеку.


СЦЕНАРІЙ 3

Цей параметр не виконується за замовчуванням, як очікувалося, оскільки ви друкуєте значення самого вказівника - НЕ те, що це розмежування (що є недійсним доступом до пам'яті).


Інші

Це майже завжди приречене на segfault, коли ви жорстко кодуєте значення такого вказівника (оскільки завдання операційної системи визначає, який процес може отримати доступ до якого місця пам'яті).

Отже, основним правилом є завжди ініціалізувати вказівник на адресу деякої виділеної змінної, наприклад:

або,


2
+1 Це хороша порада, але "ніколи" насправді не надто сильне, враховуючи чарівні адреси на багатьох платформах. (Використання константних таблиць для цих фіксованих адрес не вказує на існуючі змінні, і тому порушує ваше правило, як зазначено.) Такі речі низького рівня, як розробка драйверів, досить часто займаються подібними справами.
Нейт

3
"Це дійсно" - ігнорування надлишку ініціалізаторів є розширенням GCC; у Стандарт C заборонено
MM

1
@TheNate - так, ти маєш рацію. Я відредагував базу вашого коментаря - дякую.
artm

@MM - дякую, що вказали на це. Я редагував, щоб видалити це.
artm

25

int *nums = {5, 2, 1, 4};є неправильно сформованим кодом. Існує розширення GCC, яке трактує цей код так само, як:

спроба сформувати вказівник на адресу пам’яті 5. (Це не здається мені корисним розширенням, але я думаю, що база розробників цього хоче).

Щоб уникнути такої поведінки (або, принаймні, отримати попередження), ви можете скомпілювати в стандартному режимі, наприклад -std=c11 -pedantic .

Альтернативною формою дійсного коду буде:

який вказує на змінний літерал тієї ж тривалості зберігання, що і nums. Однак int nums[]версія, як правило, краща, оскільки вона використовує менше місця для зберігання, і ви можете використовувати sizeofдля виявлення тривалості масиву.


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

@supercat так, це автоматично, якщо nums автоматичний, і статичний, якщо nums статичний
MM

@MM: Чи застосовуватиметься це, навіть якщо numsстатична змінна оголошена у функції, або компілятор матиме право обмежувати час життя масиву тим самим, що включає блок, навіть якщо він був призначений статичній змінній?
supercat

@supercat так (перший біт). Другий варіант означав би UB під час другого виклику функції (оскільки статичні змінні ініціалізуються лише під час першого виклику)
MM

12

numsє покажчиком типу int. Отже, ви повинні вказати цей пункт на якесь дійсне місце в пам'яті.num[0]ви намагаєтесь розмежувати якесь випадкове розташування пам'яті, а отже, і помилку сегментації.

Так, вказівник містить значення 5, і ви намагаєтесь розмежувати його, що є невизначеною поведінкою у вашій системі. (Виглядає наче5 у вашій системі не є дійсним місцем пам’яті)

Тоді як

є дійсним оголошенням, де ви говорите nums, що це масив типу, intі пам'ять виділяється на основі кількості елементів, переданих під час ініціалізації.


1
"Так, вказівник містить значення 5, і ви намагаєтесь розмежувати його, що є невизначеною поведінкою." Зовсім не, це цілком чудова і чітко визначена поведінка. Але в системі, яку використовує операційна програма, це не дійсна адреса пам'яті, отже, збій.
Лундін,

@Lundin Згоден. Але я думаю, що OP ніколи не знав, що 5 є дійсним місцем пам'яті, тому я говорив на цих рядках. Сподіваюся, редагування допоможе
Гопі

Має бути таким? int *nums = (int[]){5, 2, 1, 4};
Іслам Азаб

10

Присвоюючи {5, 2, 1, 4}

Ви присвоюєте 5 nums (після неявної трансляції типу з int на покажчик на int). Якщо його відкласти, він здійснює виклик доступу до пам'яті в 0x5. Можливо, це не дозволить вашій програмі отримати доступ.

Спробуйте

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.