Что происходит с объявленной неинициализированной переменной в C? Имеет ли это ценность?

142

Если в C я пишу:

int num;

Прежде чем я что-нибудь назначу num, является ли значение numнеопределенным?

атп
источник
4
Гм, разве это не определенная переменная, а не объявленная ? (Прошу прощения, если это мой C ++ сияет ...)
SBI
6
Нет. Я могу объявить переменную, не определяя ее: extern int x;однако определение всегда подразумевает объявление. Это неверно в C ++, со статическими переменными-членами класса можно определить без объявления, так как объявление должно быть в определении класса (а не в объявлении!), А определение должно быть вне определения класса.
bdonlan
ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Похоже, что определенный означает, что вам тоже нужно инициализировать его.
atp

Ответы:

193

Статические переменные (область действия файла и статическая функция) инициализируются нулем:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Нестатические переменные (локальные переменные) не определены . Их чтение перед присвоением значения приводит к неопределенному поведению .

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

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

Что касается того, почему это поведение undefined, а не просто «неопределенное / произвольное значение», существует ряд архитектур ЦП, которые имеют в своем представлении дополнительные биты флагов для различных типов. Современный пример - Itanium, в регистрах которого есть бит Not a Thing ; конечно, разработчики стандарта C рассматривали некоторые более старые архитектуры.

Попытка работать со значением с установленными этими флаговыми битами может привести к исключению ЦП в операции, которая действительно не должна завершиться неудачей (например, сложение целых чисел или присвоение другой переменной). И если вы оставите переменную неинициализированной, компилятор может собрать какой-то случайный мусор с установленными битами флага - это означает, что прикосновение к этой неинициализированной переменной может быть смертельным.

бдонлан
источник
3
о нет, они не такие. Они могут быть в режиме отладки, когда вы не на глазах у клиента, в месяцы с R, если вам повезет,
Мартин Беккет,
8
что нет? статическая инициализация требуется стандартом; см. ИСО / МЭК 9899: 1999 6.7.8 # 10
bdonlan
3
Первый пример хорош, насколько я могу судить. Я меньше понимаю, почему компилятор может
6
@Stuart: есть вещь, называемая «представление ловушки», которая в основном представляет собой битовый шаблон, который не обозначает допустимое значение и может вызывать, например, аппаратные исключения во время выполнения. Единственный тип C, для которого есть гарантия, что любой битовый шаблон является допустимым значением, - это char; все остальные могут иметь представления ловушек. В качестве альтернативы - поскольку доступ к неинициализированной переменной в любом случае осуществляется через UB - соответствующий компилятор может просто выполнить некоторую проверку и решить сообщить о проблеме.
Павел Минаев
5
bdonian правильный. C всегда определялся довольно точно. До C89 и C99 в начале 1970-х годов все эти вещи были описаны в одной из статей dmr. Даже в самой грубой встраиваемой системе требуется всего одна функция memset (), чтобы все делать правильно, так что нет оправдания несоответствующей среде. В своем ответе я привел стандарт.
DigitalRoss,
57

0, если статический или глобальный, неопределенный, если класс хранения автоматический

C всегда очень точно определял начальные значения объектов. Если глобальные или static, они будут обнулены. Если auto, значение неопределенное .

Так было в компиляторах до C89 и было указано K&R и в исходном отчете DMR на C.

Так было в C89, см. Раздел 6.5.7 Инициализация .

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

Так было в C99, см. Раздел 6.7.8 Инициализация .

Если объект, который имеет автоматическую продолжительность хранения, не инициализирован явно, его значение не определено. Если объект со статической продолжительностью хранения не инициализирован явно, то:
- если он имеет тип указателя, он инициализируется нулевым указателем;
- если он имеет арифметический тип, он инициализируется (положительным или беззнаковым) нулем;
- если это агрегат, каждый член инициализируется (рекурсивно) в соответствии с этими правилами;
- если это объединение, первый указанный член инициализируется (рекурсивно) в соответствии с этими правилами.

Что касается того, что именно означает неопределенное , я не уверен для C89, C99 говорит:

3.17.2
неопределенное значение

: неопределенное значение или представление прерывания.

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

Вы можете спросить, почему это так? Другой ответ SO касается этого вопроса, см .: https://stackoverflow.com/a/2091505/140740

DigitalRoss
источник
3
неопределенный обычно (раньше?) означает, что он может все. Это может быть ноль, это может быть значение, которое было там, это может вызвать сбой программы, это может заставить компьютер производить блины с черникой из слота для компакт-диска. у вас нет абсолютно никаких гарантий. Это может вызвать разрушение планеты. По крайней мере, в том, что касается спецификации ... любой, кто создал компилятор, который действительно делал что-либо подобное, был бы очень недоволен B-)
Брайан Постоу
В проекте C11 N1570 определение indeterminate valueможно найти в 3.19.2.
user3528438
Так ли это, что всегда зависит от компилятора или ОС, какое значение он устанавливает для статической переменной? Например, если кто-то напишет мою собственную ОС или компилятор, и если они также установят начальное значение по умолчанию для статики как неопределенное, возможно ли это?
Адитья Сингх
1
@AdityaSingh, ОС может упростить компилятору, но, в конечном итоге, основная ответственность компилятора - запуск существующего в мире каталога кода C и вторичная ответственность - соответствовать стандартам. Конечно, можно было бы поступить иначе, но почему? Кроме того, сложно сделать статические данные неопределенными, потому что ОС действительно захочет сначала обнулить страницы по соображениям безопасности. (Авто переменных только внешне непредсказуемы , потому что ваша собственная программа, как правило , использовали эти адреса стеки на более раннем этапе.)
DigitalRoss
@BrianPostow Нет, это неверно. См. Stackoverflow.com/a/40674888/584518 . Использование неопределенного значения приводит к неопределенному поведению, а не к неопределенному поведению, за исключением случаев представления ловушек.
Lundin
12

Это зависит от продолжительности хранения переменной. Переменная со статической продолжительностью хранения всегда неявно инициализируется нулем.

Что касается автоматических (локальных) переменных, то неинициализированная переменная имеет неопределенное значение . Неопределенное значение, помимо прочего, означает, что любое «значение», которое вы можете «увидеть» в этой переменной, не только непредсказуемо, но даже не гарантирует стабильности . Например, на практике (т.е. игнорирование UB на секунду) этот код

int num;
int a = num;
int b = num;

не гарантирует, что переменные aи bполучат одинаковые значения. Интересно, что это не какая-то педантичная теоретическая концепция, это часто случается на практике как следствие оптимизации.

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

Муравей
источник
Я не могу понять (ну я очень хорошо могу ) , почему это имеет гораздо меньше upvotes , чем один из DigitalRoss всего минуту после того, как : D
Антти Haapala
8

Пример Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Достаточно стандартов, давайте посмотрим на реализацию :-)

Локальная переменная

Стандарты: неопределенное поведение.

Реализация: программа выделяет пространство стека и никогда ничего не перемещает по этому адресу, поэтому используется все, что было там ранее.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

компилировать с помощью:

gcc -O0 -std=c99 a.c

выходы:

0

и декомпилируется с помощью:

objdump -dr a.out

кому:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

Из наших знаний о соглашениях о вызовах x86-64:

  • %rdiэто первый аргумент printf, поэтому строка "%d\n"по адресу0x4005e4

  • %rsiэто второй аргумент printf, таким образом i.

    Это -0x4(%rbp)первая 4-байтовая локальная переменная.

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

    TODO устанавливает ли ядро ​​этой памяти на что-то, прежде чем повторно использовать ее для других процессов, когда процесс умирает? В противном случае новый процесс сможет читать память других готовых программ, что приводит к утечке данных. См.: Являются ли неинициализированные значения угрозой безопасности?

Затем мы также можем поиграть с нашими собственными модификациями стека и написать такие забавные вещи, как:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Локальная переменная в -O3

Анализ реализации на: Что означает <value optimized out> в gdb?

Глобальные переменные

Стандарты: 0

Реализация: .bssраздел.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

компилируется в:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>говорит, что iнаходится по адресу 0x601044и:

readelf -SW a.out

содержит:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

который говорит, что 0x601044находится прямо в середине .bssраздела, который начинается с 0x601040и имеет длину 8 байт.

Затем стандарт ELF гарантирует, что указанный раздел .bssполностью заполнен нулями:

.bssВ этом разделе хранятся неинициализированные данные, которые вносят вклад в образ памяти программы. По определению, система инициализирует данные нулями, когда программа начинает работать. Раздел не занимает файлового пространства, на что указывает тип раздела SHT_NOBITS.

Кроме того, тип SHT_NOBITSэффективен и не занимает места в исполняемом файле:

sh_sizeЭтот член указывает размер раздела в байтах. Если тип SHT_NOBITSраздела не задан , он занимает sh_size байты в файле. Типовой раздел SHT_NOBITSможет иметь ненулевой размер, но он не занимает места в файле.

Затем ядро ​​Linux должно обнулить эту область памяти при загрузке программы в память при ее запуске.

Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
4

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

Джерри Гроб
источник
1

Поскольку компьютеры имеют ограниченную емкость памяти, автоматические переменные обычно хранятся в элементах памяти (будь то регистры или ОЗУ), которые ранее использовались для некоторых других произвольных целей. Если такая переменная используется до того, как ей было присвоено значение, это хранилище может содержать все, что было ранее, и поэтому содержимое переменной будет непредсказуемым.

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

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

вполне может привести к wow()сохранению значений 1234567 в регистрах 0 и 1 соответственно и вызову foo(). Поскольку xв "foo" не требуется, и поскольку функции должны помещать свое возвращаемое значение в регистр 0, компилятор может выделить регистр 0 для q. Если mode1 или 3, в регистр 0 будет загружено 2 или 4, соответственно, но если это какое-то другое значение, функция может вернуть все, что было в регистре 0 (т.е. значение 1234567), даже если это значение находится вне диапазона из uint16_t.

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

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

компилятор может сделать вывод, что, поскольку вызов moo()с режимом, превышающим 3, неизбежно приведет к вызову программы Undefined Behavior, компилятор может пропустить любой код, который будет иметь значение, только если он modeравен 4 или больше, например, код, который обычно предотвращает запуск ядерного оружия в таких случаях. Обратите внимание, что ни Стандарт, ни современная философия компилятора не заботятся о том, что возвращаемое значение из «эй» игнорируется - попытка вернуть его дает компилятору неограниченную лицензию на генерацию произвольного кода.

суперкар
источник
0

Основной ответ - да, это не определено.

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

Саймон
источник
0

Если класс хранения является статическим или глобальным, то во время загрузки BSS инициализирует переменную или ячейку памяти (ML) до 0, если переменной изначально не присвоено какое-либо значение. В случае локальных неинициализированных переменных представление ловушки назначается ячейке памяти. Поэтому, если какой-либо из ваших регистров, содержащих важную информацию, будет перезаписан компилятором, программа может аварийно завершить работу.

но у некоторых компиляторов может быть механизм, позволяющий избежать такой проблемы.

Я работал с серией nec v850, когда понял, что существует представление ловушки, в котором есть битовые шаблоны, которые представляют неопределенные значения для типов данных, кроме char. Когда я взял неинициализированный char, я получил нулевое значение по умолчанию из-за представления ловушки. Это может быть полезно для любого1, использующего necv850es

ханиш
источник
Ваша система не соответствует требованиям, если вы получаете представление прерывания при использовании unsigned char. Им явно не разрешено содержать представления прерываний, C17 6.2.6.1/5.
Lundin
-2

Значение num будет неким мусорным значением из основной памяти (RAM). лучше, если вы инициализируете переменную сразу после создания.

Шрикант Сингх
источник
-4

Насколько я понимаю, это в основном зависит от компилятора, но в большинстве случаев компиляторы заранее принимают значение 0.
Я получил значение мусора в случае VC ++, в то время как TC дал значение 0. Я распечатываю его, как показано ниже.

int i;
printf('%d',i);
Раджив Кумар
источник
Если вы получаете детерминированное значение, например, 0ваш компилятор, скорее всего, предпримет дополнительные шаги, чтобы убедиться, что он получает это значение (в любом случае добавляя код для инициализации переменных). Некоторые компиляторы делают это при "отладочной" компиляции, но выбор значения 0для них - плохая идея, поскольку это скроет ошибки в вашем коде (более правильным было бы гарантировать действительно маловероятное число, подобное 0xBAADF00Dили что-то подобное). Я думаю, что большинство компиляторов просто оставит любой мусор, который занимает память, как значение переменной (то есть, как правило, он не собирается как 0).
skyking