Если в C я пишу:
int num;
Прежде чем я что-нибудь назначу num
, является ли значение num
неопределенным?
Если в C я пишу:
int num;
Прежде чем я что-нибудь назначу num
, является ли значение num
неопределенным?
extern int x;
однако определение всегда подразумевает объявление. Это неверно в C ++, со статическими переменными-членами класса можно определить без объявления, так как объявление должно быть в определении класса (а не в объявлении!), А определение должно быть вне определения класса.
Ответы:
Статические переменные (область действия файла и статическая функция) инициализируются нулем:
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 рассматривали некоторые более старые архитектуры.
Попытка работать со значением с установленными этими флаговыми битами может привести к исключению ЦП в операции, которая действительно не должна завершиться неудачей (например, сложение целых чисел или присвоение другой переменной). И если вы оставите переменную неинициализированной, компилятор может собрать какой-то случайный мусор с установленными битами флага - это означает, что прикосновение к этой неинициализированной переменной может быть смертельным.
char
; все остальные могут иметь представления ловушек. В качестве альтернативы - поскольку доступ к неинициализированной переменной в любом случае осуществляется через UB - соответствующий компилятор может просто выполнить некоторую проверку и решить сообщить о проблеме.
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
indeterminate value
можно найти в 3.19.2.
Это зависит от продолжительности хранения переменной. Переменная со статической продолжительностью хранения всегда неявно инициализируется нулем.
Что касается автоматических (локальных) переменных, то неинициализированная переменная имеет неопределенное значение . Неопределенное значение, помимо прочего, означает, что любое «значение», которое вы можете «увидеть» в этой переменной, не только непредсказуемо, но даже не гарантирует стабильности . Например, на практике (т.е. игнорирование UB на секунду) этот код
int num;
int a = num;
int b = num;
не гарантирует, что переменные a
и b
получат одинаковые значения. Интересно, что это не какая-то педантичная теоретическая концепция, это часто случается на практике как следствие оптимизации.
В общем, популярный ответ, что «он инициализируется тем мусором, который был в памяти», даже отдаленно неверен. Поведение неинициализированной переменной отличается от поведения переменной, инициализированной с помощью мусора.
Пример 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 должно обнулить эту область памяти при загрузке программы в память при ее запуске.
Это зависит от. Если это определение является глобальным (вне какой-либо функции), оно num
будет инициализировано нулем. Если он локальный (внутри функции), то его значение не определено. Теоретически, даже попытка прочитать значение имеет неопределенное поведение - C допускает возможность битов, которые не вносят вклад в значение, но должны быть установлены определенным образом, чтобы вы даже могли получить определенные результаты от чтения переменной.
Поскольку компьютеры имеют ограниченную емкость памяти, автоматические переменные обычно хранятся в элементах памяти (будь то регистры или ОЗУ), которые ранее использовались для некоторых других произвольных целей. Если такая переменная используется до того, как ей было присвоено значение, это хранилище может содержать все, что было ранее, и поэтому содержимое переменной будет непредсказуемым.
В качестве дополнительной проблемы многие компиляторы могут хранить переменные в регистрах, размер которых превышает размер соответствующих типов. Хотя компилятор должен гарантировать, что любое значение, записываемое в переменную и считываемое обратно, будет усечено и / или расширено знаком до своего надлежащего размера, многие компиляторы будут выполнять такое усечение при записи переменных и ожидают, что оно будет иметь было выполнено до чтения переменной. На таких компиляторах что-то вроде:
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
. Если mode
1 или 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 или больше, например, код, который обычно предотвращает запуск ядерного оружия в таких случаях. Обратите внимание, что ни Стандарт, ни современная философия компилятора не заботятся о том, что возвращаемое значение из «эй» игнорируется - попытка вернуть его дает компилятору неограниченную лицензию на генерацию произвольного кода.
Основной ответ - да, это не определено.
Если из-за этого вы видите странное поведение, это может зависеть от того, где оно объявлено. Если внутри функции в стеке, то содержимое, скорее всего, будет отличаться каждый раз при вызове функции. Если это статическая или модульная область видимости, она не определена, но не изменится.
Если класс хранения является статическим или глобальным, то во время загрузки BSS инициализирует переменную или ячейку памяти (ML) до 0, если переменной изначально не присвоено какое-либо значение. В случае локальных неинициализированных переменных представление ловушки назначается ячейке памяти. Поэтому, если какой-либо из ваших регистров, содержащих важную информацию, будет перезаписан компилятором, программа может аварийно завершить работу.
но у некоторых компиляторов может быть механизм, позволяющий избежать такой проблемы.
Я работал с серией nec v850, когда понял, что существует представление ловушки, в котором есть битовые шаблоны, которые представляют неопределенные значения для типов данных, кроме char. Когда я взял неинициализированный char, я получил нулевое значение по умолчанию из-за представления ловушки. Это может быть полезно для любого1, использующего necv850es
Значение num будет неким мусорным значением из основной памяти (RAM). лучше, если вы инициализируете переменную сразу после создания.
Насколько я понимаю, это в основном зависит от компилятора, но в большинстве случаев компиляторы заранее принимают значение 0.
Я получил значение мусора в случае VC ++, в то время как TC дал значение 0. Я распечатываю его, как показано ниже.
int i;
printf('%d',i);
0
ваш компилятор, скорее всего, предпримет дополнительные шаги, чтобы убедиться, что он получает это значение (в любом случае добавляя код для инициализации переменных). Некоторые компиляторы делают это при "отладочной" компиляции, но выбор значения 0
для них - плохая идея, поскольку это скроет ошибки в вашем коде (более правильным было бы гарантировать действительно маловероятное число, подобное 0xBAADF00D
или что-то подобное). Я думаю, что большинство компиляторов просто оставит любой мусор, который занимает память, как значение переменной (то есть, как правило, он не собирается как 0
).