«Int * nums = {5, 2, 1, 4}» вызывает ошибку сегментации

81

вызывает segfault, тогда как

нет. В настоящее время:

печатает 5.

Основываясь на этом, я предположил, что обозначение инициализации массива, {}, слепо загружает эти данные в любую переменную слева. Когда это int [], массив заполняется по желанию. Когда это int *, указатель заполняется на 5, а ячейки памяти после того, где хранится указатель, заполняются на 2, 1 и 4. Таким образом, nums [0] пытается разыграть 5, вызывая segfault.

Если я ошибаюсь, поправьте меня. И если я прав, пожалуйста, уточните, потому что я не понимаю, почему инициализаторы массива работают так, как они.

user1299784
источник
3
Скомпилируйте со всеми включенными предупреждениями, и ваш компилятор должен сообщить вам, что происходит.
Jabberwocky
1
@GSerg Это не дубликат. В этом вопросе нет указателя на массив. Хотя некоторые ответы в этом посте похожи на те, что здесь.
Lundin
2
@Lundin Я был уверен на 30%, поэтому я не голосовал за закрытие, а только разместил ссылку.
GSerg
3
Получите привычку запускать GCC с -pedantic-errorsфлагом и смотреть диагностику. int *nums = {5, 2, 1, 4};недействителен C.
AnT

Ответы:

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
Нет ничего «глупого» в возможности инициализировать скалярный объект с одним значением, заключенным в {}. Напротив, он упрощает использование одной из самых важных и удобных идиом языка C - { 0 }универсального инициализатора нуля. Все в C может быть инициализировано нулями = { 0 }. Это очень важно для написания кода, независимого от типа.
AnT
3
@AnT Не существует такой вещи, как «универсальный инициализатор нуля». В случае агрегатов это {0}просто означает инициализировать первый объект до нуля и инициализировать остальные объекты, как если бы они имели статическую продолжительность хранения. Я бы сказал, что это скорее совпадение, чем намеренный языковой дизайн какого-то «универсального инициализатора», поскольку {1}не инициализирует все объекты значением 1.
Lundin
3
@Lundin C11 6.5.16.1/1 охватывает p = 5;(ни один из перечисленных случаев не выполняется для назначения целого числа указателю); а 6.7.9 / 11 говорит, что ограничения для назначения также используются для инициализации.
MM
4
@Lundin: Да, есть. Совершенно неважно, какой механизм инициализирует какую часть объекта. Также совершенно неважно, {}разрешена ли инициализация скаляров специально для этой цели. Единственное, что имеет значение, это то, что = { 0 }инициализатор гарантированно инициализирует нулем весь объект , что и сделало его классической и одной из самых элегантных идиом языка C.
AnT
2
@Lundin: Мне также совершенно непонятно, какое отношение {1}имеет ваше замечание к теме. Никто никогда не утверждал, что {0}интерпретирует это 0как мультиинициализатор для каждого члена агрегата.
AnT
28

СЦЕНАРИЙ 1

Почему это один segfault?

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

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

В любом случае (назначьте несколько значений указателю или переменной типа int), тогда произойдет то, что переменная получит первое значение, которое 5 , а остальные значения игнорируются. Этот код соответствует требованиям, но вы получите предупреждения для каждого дополнительного значения, которого не должно быть в назначении:

warning: excess elements in scalar initializer.

В случае присвоения нескольких значений переменной-указателю, программа перестает работать при доступе nums[0], что означает, что вы буквально отсылаете все, что хранится по адресу 5 . В этом случае вы не выделили допустимую память для указателя nums.

Стоит отметить, что для случая присвоения нескольких значений переменной int нет segfault (здесь вы не разыменовываете какой-либо недопустимый указатель).


СЦЕНАРИЙ 2

Это не segfault, потому что вы на законных основаниях размещаете в стеке массив из 4 целых чисел.


СЦЕНАРИЙ 3

Это не segfault, как ожидалось, потому что вы печатаете значение самого указателя - НЕ то, что он разыменовывает (что является недопустимым доступом к памяти).


Другие

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

Итак, практическое правило - всегда инициализировать указатель на адрес некоторой выделенной переменной, например:

или же,

artm
источник
2
+1 Хороший совет, но «никогда» на самом деле слишком сильно, учитывая магические адреса на многих платформах. (Использование таблиц констант для этих фиксированных адресов не указывает на существующие переменные и, следовательно, нарушает ваше установленное правило.) Такие вещи, как разработка драйверов, довольно часто имеют дело с подобными вещами.
The Nate
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
@supercat: да, это автоматически, если число является автоматическим, и статическим, если число статическое
MM
@MM: Будет ли это применяться, даже если numsэто статическая переменная, объявленная в функции, или компилятор будет иметь право ограничить время жизни массива временем жизни включающего блока, даже если он был назначен статической переменной?
supercat
@supercat да (первый бит). Второй вариант будет означать UB при втором вызове функции (поскольку статические переменные инициализируются только при первом вызове)
ММ
12

numsуказатель типа int. Таким образом, вы должны указать на некоторую допустимую область памяти. num[0]вы пытаетесь разыменовать какую-то случайную ячейку памяти и, следовательно, ошибку сегментации.

Да, указатель имеет значение 5, и вы пытаетесь разыменовать его, что является неопределенным поведением в вашей системе. (Выглядит как5 это недопустимая ячейка памяти в вашей системе)

В то время как

- допустимое объявление, в котором вы говорите, numsчто это массив типа, intа память выделяется на основе количества элементов, переданных во время инициализации.

Гопи
источник
1
«Да, указатель имеет значение 5, и вы пытаетесь разыменовать его, что является неопределенным поведением». Вовсе нет, это прекрасно и четко определенное поведение. Но в системе, которую использует OP, это недействительный адрес памяти, отсюда и сбой.
Lundin
@Lundin Согласен. Но я думаю, что OP никогда не знал, что 5 является допустимой ячейкой памяти, поэтому я говорил об этих строках. Надеюсь, редактирование поможет
Гопи
Должно быть так? int *nums = (int[]){5, 2, 1, 4};
Islam Azab
10

Назначив {5, 2, 1, 4}

вы назначаете 5 nums(после неявного преобразования типа из int в указатель на int). Отмена ссылки делает вызов доступа к ячейке памяти по адресу 0x5. Это может быть запрещено вашей программе.

Пытаться

Фахад Сиддики
источник