Що відбувається з оголошеною, неініціалізованою змінною в C? Чи має це значення?


139

Якщо в CI пишіть:

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

Відповіді:


188

Статичні змінні (область файлу та статичні функції) ініціалізуються до нуля:

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
}

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

Що стосується того, чому це невизначена поведінка замість просто "невизначеного / довільного значення", існує ряд архітектур процесора, які мають додаткові біти прапора у своєму представленні для різних типів. Сучасним прикладом може бути Itanium, який має в своїх регістрах біт "Не річ" ; звичайно, розробники стандартів C розглядали деякі старіші архітектури.

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


2
о ні, вони ні. Вони можуть бути в режимі налагодження, коли ви не перед клієнтом, місяцями з R в, якщо вам пощастить
Мартін Бекетт

8
що ні? статична ініціалізація вимагається стандартом; див. ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan

2
Перший приклад прекрасний, наскільки я можу сказати. Я менше, чому компілятор може

6
@Stuart: є річ під назвою "представлення пастки", яка в основному є бітовим шаблоном, який не позначає дійсне значення, і може спричинити, наприклад, технічні винятки під час виконання. Єдиний тип C, для якого існує гарантія того, що будь-який біт-шаблон є дійсним значенням char; всі інші можуть мати уявлення про пастку. Крім того - оскільки доступ до неініціалізованої змінної все одно є UB - відповідний компілятор може просто зробити деяку перевірку і вирішити сигналізувати про проблему.
Павло Мінаєв

5
бдоніан правильний. C завжди було вказано досить точно. До початку C89 та C99 документ, що випускався dmr, конкретизував усі ці речі на початку 1970-х. Навіть у найбільш грубій вбудованій системі потрібен лише один мемсет (), щоб зробити все правильно, тому немає приводу для невідповідного середовища. Я цитував стандарт у своїй відповіді.
DigitalRoss

57

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

C завжди був дуже специфічним щодо початкових значень об'єктів. Якщо глобальний або static, вони будуть нульовими. Якщо autoзначення є невизначеним .

Так було у компіляторах, що передували C89, і так було визначено K&R та в оригінальному звіті C на DMR.

Так було в С89, див. Розділ 6.5.7 Ініціалізація .

Якщо об’єкт, що має тривалість автоматичного зберігання, не ініціалізується експліцитно, його значення невизначене. Якщо об'єкт, який має статичну тривалість зберігання, не ініціалізується експліцитно, він ініціалізується неявно так, як якщо кожному члену, який має арифметичний тип, присвоєно 0, а кожному члену, який має тип вказівника, присвоєна константа нульового покажчика.

Так було у справі C99, див. Розділ 6.7.8 Ініціалізація .

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

Що стосується того, що саме означає невизначеність , я не впевнений у C89, C99 говорить:

3.17.2
невизначене значення

або неуточнене значення, або подання пастки

Але незалежно від того, що говорять про стандарти, в реальному житті кожна сторінка стека насправді починається з нуля, але коли ваша програма розглядає будь-які autoзначення класу зберігання, вона бачить все, що було залишено вашою власною програмою, коли вона востаннє використовувала ці адреси стека. Якщо ви виділите багато autoмасивів, ви побачите, що з часом вони починаються акуратно з нулів.

Ви можете задатися питанням, чому це так? Інше відповідь ТА стосується цього питання, дивіться: https://stackoverflow.com/a/2091505/140740


3
невизначений звичайно (раніше?) означає, що він може робити все, що завгодно. Це може бути нульове значення, це може бути значення, яке було там, це може розбити програму, він може змусити комп'ютер виробляти млинці з чорниць із слота для компакт-дисків. у вас абсолютно немає гарантій. Це може спричинити руйнування планети. Принаймні, що стосується специфікації ... кожен, хто зробив компілятор, який насправді робив щось подібне, був би дуже нахмурений на B-)
Brian Postow

У проекті С11 N1570 визначення indeterminate valueможна знайти в 3.19.2.
користувач3528438

Це так, що від компілятора чи ОС завжди залежить, яке значення він встановлює для статичної змінної? Наприклад, якщо хтось пише ОС або моїй компілятор, і якщо вони також встановлюють початкове значення за замовчуванням для статики як невизначене, це можливо?
Адітя Сінгх

1
@AdityaSingh, ОС може полегшити компілятор, але в кінцевому підсумку головна відповідальність компілятора є запускати існуючий у світі каталог коду С і вторинна відповідальність за відповідність стандартам. Звичайно, це можна було б зробити інакше, але чому? Крім того, складно зробити статичні дані невизначеними, оскільки ОС дійсно захоче спустити на нуль сторінки спочатку з міркувань безпеки. (Автоматичні змінні є лише поверхнево непередбачуваними, оскільки ваша власна програма зазвичай використовує ці адреси стека в більш ранній момент.)
DigitalRoss

@BrianPostow Ні, це невірно. Див. Stackoverflow.com/a/40674888/584518 . Використання невизначеного значення спричиняє не визначене поведінку, а не визначену поведінку, крім випадків уявлення про пастку.
Лундін

12

Це залежить від тривалості зберігання змінної. Змінна зі статичною тривалістю зберігання завжди неявно ініціалізується з нулем.

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

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

не гарантує, що змінні aта bотримають однакові значення. Цікаво, що це не якась педантична теоретична концепція, це легко відбувається на практиці як наслідок оптимізації.

Так загалом, популярна відповідь про те, що "вона ініціалізована тим, що сміття було в пам'яті" навіть не є коректною віддалено. UNINITIALIZED поведінку змінної відрізняється від змінної инициализируется зі сміттям.


Я не можу зрозуміти (ну, я дуже добре можу ), чому це набагато менше результатів, ніж у DigitalRoss лише через хвилину після: D
Анті Хаапала

7

Приклад Ubuntu 15.10, ядро ​​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на першій сторінці стека було виділено ядро, тому, щоб зрозуміти це значення, ми б заглянули в код ядра і дізналися, для чого це встановлено.

    TODO чи ядро ​​встановлює цю пам'ять на щось перед тим, як використовувати її для інших процесів, коли процес гине? Якщо ні, новий процес міг би прочитати пам'ять інших готових програм, витікаючи дані. Див.: Чи неініціалізовані значення колись становлять небезпеку для безпеки?

Потім ми можемо також грати з нашими власними модифікаціями стека і писати цікаві речі, такі як:

#include <assert.h>

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

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

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

Локальна змінна в -O3

Аналіз впровадження: Що значить <оптимізоване значення> у 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розділу, який починається з 0x6010408 байт.

Тоді стандарт 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. Якщо modeце 1 або 3, регістр 0 буде завантажений відповідно 2 або 4, але якщо це якесь інше значення, функція може повернути те, що було в регістрі 0 (тобто значення 1234567), навіть якщо це значення не знаходиться в межах діапазону від uint16_t.

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

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

компілятор міг би зробити висновок, що оскільки виклик moo()у режимі, що перевищує 3, неминуче призведе до того, що програма посилається на Невизначене поведінку, компілятор може опустити будь-який код, який був би релевантним лише у випадку mode4 або більше, наприклад код, який, як правило, перешкоджає запуск ядер в таких випадках. Зауважте, що ні Стандарт, ні сучасна філософія компілятора не піклуються про те, щоб повернене значення з "hey" ігнорувалося - акт спроби повернути його дає компілятору необмежену ліцензію на генерування довільного коду.


0

Основна відповідь: так, це не визначено.

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


0

Якщо клас зберігання є статичним або глобальним, то під час завантаження BSS ініціалізує місце змінної або пам'яті (ML) до 0, якщо спочатку змінній не присвоєно якесь значення. У разі локальних неініціалізованих змінних представлення пастки присвоюється місцем пам'яті. Тож якщо будь-який з ваших регістрів, що містять важливу інформацію, буде перезаписаний компілятором, програма може вийти з ладу.

але деякі компілятори можуть мати механізм уникнення такої проблеми.

Я працював з Nec v850 серія, коли я зрозумів, що існує представлення пастки, яке має бітові шаблони, які представляють невизначені значення для типів даних, крім char. Коли я взяв неініціалізований знак, я отримав нульове значення за замовчуванням через представлення пастки. Це може бути корисно для any1, що використовує necv850es


Ваша система не сумісна, якщо ви отримуєте уявлення про пастку під час використання неподписаних знаків. Їм явно не дозволяється містити уявлення про пастки, C17 6.2.6.1/5.
Лундін

-2

Значення num буде деяким значенням сміття з основної пам'яті (ОЗП). краще, якщо ви ініціалізуєте змінну відразу після створення.


-4

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

int i;
printf('%d',i);

Якщо ви отримуєте детерміновані значення, наприклад, 0ваш компілятор, швидше за все, виконує додаткові кроки, щоб переконатися, що він отримує це значення (додавши код для ініціалізації змінних у будь-якому випадку). Деякі компілятори роблять це під час компіляції "налагодження", але вибір значення 0для них є поганою ідеєю, оскільки він приховає недоліки у вашому коді (більш правильна річ гарантувала б дійсно малоймовірну кількість на кшталт 0xBAADF00Dабо щось подібне). Я думаю, що більшість компіляторів просто залишать будь-яке сміття, яке трапляється, щоб зайняти пам'ять як значення змінної (тобто взагалі це не вважається 0).
скачаючи
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.