¿Qué le sucede a una variable declarada no inicializada en C? ¿Tiene un valor?


139

Si en CI escribe:

int num;

Antes de asignarle algo num, ¿el valor de numindeterminado?


44
Um, ¿no es esa una variable definida , no una declarada ? (Lo siento si eso es mi C ++ que brilla a través ...)
OSE

66
No. Puedo declarar una variable sin definirla: sin extern int x;embargo, definir siempre implica declarar. Esto no es cierto en C ++, con las variables miembro de clase estáticas se puede definir sin declarar, ya que la declaración debe estar en la definición de clase (¡no declaración!) Y la definición debe estar fuera de la definición de clase.
bdonlan

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Parece definido, significa que también debe inicializarlo.
atp

Respuestas:


188

Las variables estáticas (alcance del archivo y función estática) se inicializan a cero:

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

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

Las variables no estáticas (variables locales) son indeterminadas . Leerlos antes de asignar un valor da como resultado un comportamiento indefinido.

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

En la práctica, tienden a tener un valor absurdo allí inicialmente (algunos compiladores incluso pueden poner valores fijos específicos para que sea obvio cuando se busca en un depurador), pero estrictamente hablando, el compilador es libre de hacer cualquier cosa, desde fallar hasta invocar demonios a través de sus fosas nasales .

En cuanto a por qué es un comportamiento indefinido en lugar de simplemente "valor indefinido / arbitrario", hay una serie de arquitecturas de CPU que tienen bits de bandera adicionales en su representación para varios tipos. Un ejemplo moderno sería el Itanium, que tiene un bit "No es una cosa" en sus registros ; Por supuesto, los redactores estándar de C estaban considerando algunas arquitecturas más antiguas.

Intentar trabajar con un valor con estos bits de marca establecidos puede dar como resultado una excepción de CPU en una operación que realmente no debería fallar (por ejemplo, suma de enteros o asignación a otra variable). Y si va y deja una variable sin inicializar, el compilador puede recoger algo de basura aleatoria con estos bits de bandera establecidos, lo que significa que tocar esa variable sin inicializar puede ser mortal.


2
oh no, no lo son. Pueden ser, en modo de depuración, cuando no está frente a un cliente, en meses con una R, si tiene suerte
Martin Beckett

8
que no son la inicialización estática es requerida por el estándar; ver ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan

2
El primer ejemplo está bien por lo que puedo decir. Estoy menos en cuanto a porqué el compilador puede bloquearse en el segundo aunque :)

66
@Stuart: hay una cosa llamada "representación de trampa", que es básicamente un patrón de bits que no denota un valor válido y puede causar, por ejemplo, excepciones de hardware en tiempo de ejecución. El único tipo C para el que hay una garantía de que cualquier patrón de bits es un valor válido es char; todos los demás pueden tener representaciones de trampa. Alternativamente, dado que el acceso a la variable no inicializada es UB de todos modos, un compilador conforme podría simplemente verificar y decidir señalar el problema.
Pavel Minaev

55
bdonian es correcto. C siempre se ha especificado con bastante precisión. Antes de C89 y C99, un artículo de dmr especificaba todas estas cosas a principios de los años setenta. Incluso en el sistema embebido más crudo, solo se necesita un memset () para hacer las cosas bien, por lo que no hay excusa para un entorno no conforme. He citado el estándar en mi respuesta.
DigitalRoss

57

0 si estático o global, indeterminado si la clase de almacenamiento es automática

C siempre ha sido muy específico sobre los valores iniciales de los objetos. Si es global o static, se pondrán a cero. Si auto, el valor es indeterminado .

Este fue el caso en los compiladores anteriores a C89 y así lo especificaron K&R y el informe C original de DMR.

Este fue el caso en C89, ver sección 6.5.7 Inicialización .

Si un objeto que tiene una duración de almacenamiento automático no se inicializa explícitamente, su valor es indeterminado. Si un objeto que tiene una duración de almacenamiento estático no se inicializa explícitamente, se inicializa implícitamente como si a cada miembro que tiene un tipo aritmético se le asignara 0 y a cada miembro que tiene un tipo de puntero se le haya asignado una constante de puntero nulo.

Este fue el caso en C99, ver sección 6.7.8 Inicialización .

Si un objeto que tiene una duración de almacenamiento automático no se inicializa explícitamente, su valor es indeterminado. Si un objeto que tiene una duración de almacenamiento estático no se inicializa explícitamente, entonces:
- si tiene un tipo de puntero, se inicializa en un puntero nulo;
- si tiene tipo aritmético, se inicializa a cero (positivo o sin signo);
- si es un agregado, cada miembro se inicializa (recursivamente) de acuerdo con estas reglas;
- si se trata de una unión, el primer miembro nombrado se inicializa (recursivamente) de acuerdo con estas reglas.

En cuanto a lo que significa exactamente indeterminado , no estoy seguro de C89, C99 dice:

3.17.2
valor indeterminado,

ya sea un valor no especificado o una representación trampa

Pero independientemente de lo que digan los estándares, en la vida real, cada página de pila realmente comienza como cero, pero cuando su programa analiza autolos valores de la clase de almacenamiento, ve lo que su propio programa dejó atrás la última vez que utilizó esas direcciones de pila. Si asigna muchas automatrices, verá que eventualmente comienzan perfectamente con ceros.

Te preguntarás, ¿por qué es así? Una respuesta SO diferente trata esa pregunta, consulte: https://stackoverflow.com/a/2091505/140740


3
indeterminado generalmente (¿acostumbrado?) significa que puede hacer cualquier cosa. Puede ser cero, puede ser el valor que estaba allí, puede bloquear el programa, puede hacer que la computadora produzca panqueques de arándanos fuera de la ranura del CD. No tienes absolutamente ninguna garantía. Podría causar la destrucción del planeta. Al menos en lo que respecta a las especificaciones ... cualquiera que haya hecho un compilador que realmente haya hecho algo así estaría muy mal visto B-)
Brian Postow

En el borrador C11 N1570, la definición de indeterminate valuese puede encontrar en 3.19.2.
user3528438

¿Es para que siempre dependa del compilador o del sistema operativo que el valor que establece para la variable estática? Por ejemplo, si alguien escribe un sistema operativo o un compilador propio, y si también establece el valor inicial por defecto para las estadísticas como indeterminado, ¿es eso posible?
Aditya Singh

1
@AdityaSingh, el sistema operativo puede facilitar el compilador pero, en última instancia, es la responsabilidad principal del compilador ejecutar el catálogo de código C existente en el mundo, y una responsabilidad secundaria cumplir con los estándares. Ciertamente sería posible hacerlo de manera diferente, pero ¿por qué? Además, es complicado hacer que los datos estáticos sean indeterminados, porque el sistema operativo realmente querrá poner a cero las páginas primero por razones de seguridad. (Las variables automáticas son superficialmente impredecibles porque su propio programa usualmente ha estado usando esas direcciones de pila en un punto anterior).
DigitalRoss

@BrianPostow No, eso no es correcto. Ver stackoverflow.com/a/40674888/584518 . El uso de un valor indeterminado provoca un comportamiento no especificado , no un comportamiento indefinido, salvo para el caso de las representaciones de trampa.
Lundin

12

Depende de la duración del almacenamiento de la variable. Una variable con duración de almacenamiento estático siempre se inicializa implícitamente con cero.

En cuanto a las variables automáticas (locales), una variable no inicializada tiene un valor indeterminado . El valor indeterminado, entre otras cosas, significa que cualquier "valor" que pueda "ver" en esa variable no solo es impredecible, ni siquiera se garantiza que sea estable . Por ejemplo, en la práctica (es decir, ignorar el UB por un segundo) este código

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

no garantiza esas variables ay brecibirá valores idénticos. Curiosamente, este no es un concepto teórico pedante, esto sucede fácilmente en la práctica como consecuencia de la optimización.

Entonces, en general, la respuesta popular de que "se inicializa con cualquier basura que haya en la memoria" ni siquiera es remotamente correcta. El comportamiento de la variable no inicializada es diferente del de una variable inicializada con basura.


No puedo entender (bueno, muy bien puedo ) por qué esto tiene muchos menos votos positivos que el de DigitalRoss solo un minuto después: D
Antti Haapala

7

Ubuntu 15.10, Kernel 4.2.0, x86-64, ejemplo de GCC 5.2.1

Suficientes estándares, veamos una implementación :-)

Variable local

Estándares: comportamiento indefinido.

Implementación: el programa asigna espacio de pila, y nunca mueve nada a esa dirección, por lo que se usa lo que estaba allí anteriormente.

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

compilar con:

gcc -O0 -std=c99 a.c

salidas:

0

y descompila con:

objdump -dr a.out

a:

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

De nuestro conocimiento de las convenciones de llamadas x86-64:

  • %rdies el primer argumento printf, por lo tanto, la cadena "%d\n"en la dirección0x4005e4

  • %rsies el segundo argumento printf, por lo tanto i.

    Proviene de -0x4(%rbp), que es la primera variable local de 4 bytes.

    En este punto, rbpestá en la primera página de la pila que ha sido asignada por el núcleo, por lo que para comprender ese valor, deberíamos analizar el código del núcleo y averiguar en qué se establece.

    TODO ¿el núcleo establece esa memoria en algo antes de reutilizarla para otros procesos cuando un proceso muere? De lo contrario, el nuevo proceso podría leer la memoria de otros programas terminados, filtrando datos. Ver: ¿Los valores no inicializados son siempre un riesgo para la seguridad?

Entonces también podemos jugar con nuestras propias modificaciones de pila y escribir cosas divertidas como:

#include <assert.h>

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

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

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

Variable local en -O3

Análisis de implementación en: ¿Qué significa <valor optimizado fuera> en gdb?

Variables globales

Estándares: 0

Implementación: .bsssección.

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

gcc -00 -std=c99 a.c

compila a:

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>dice que iestá en la dirección 0x601044y:

readelf -SW a.out

contiene:

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

que dice 0x601044está justo en el medio de la .bsssección, que comienza en 0x601040y tiene 8 bytes de longitud.

El estándar ELF garantiza que la sección nombrada .bssesté completamente llena de ceros:

.bssEsta sección contiene datos no inicializados que contribuyen a la imagen de memoria del programa. Por definición, el sistema inicializa los datos con ceros cuando el programa comienza a ejecutarse. La sección ocu- pasteles sin espacio de archivos, como lo indica el tipo de sección, SHT_NOBITS.

Además, el tipo SHT_NOBITSes eficiente y no ocupa espacio en el archivo ejecutable:

sh_sizeEste miembro da el tamaño de la sección en bytes. A menos que el tipo de sección sea SHT_NOBITS, la sección ocupa sh_size bytes en el archivo. Una sección de tipo SHT_NOBITSpuede tener un tamaño distinto de cero, pero no ocupa espacio en el archivo.

Luego, corresponde al núcleo de Linux poner a cero esa región de memoria al cargar el programa en la memoria cuando se inicia.


4

Eso depende. Si esa definición es global (fuera de cualquier función) num, se inicializará a cero. Si es local (dentro de una función), su valor es indeterminado. En teoría, incluso intentar leer el valor tiene un comportamiento indefinido: C permite la posibilidad de bits que no contribuyen al valor, pero que deben configurarse de manera específica para que incluso obtenga resultados definidos al leer la variable.


1

Debido a que las computadoras tienen una capacidad de almacenamiento finita, las variables automáticas generalmente se mantendrán en elementos de almacenamiento (ya sean registros o RAM) que se hayan utilizado previamente para algún otro propósito arbitrario. Si se usa una variable de este tipo antes de que se le haya asignado un valor, ese almacenamiento puede contener lo que sea que haya tenido anteriormente, por lo que el contenido de la variable será impredecible.

Como una arruga adicional, muchos compiladores pueden mantener variables en registros que son más grandes que los tipos asociados. Aunque se requeriría un compilador para garantizar que cualquier valor que se escriba en una variable y se lea de nuevo se truncará y / o se extenderá con un signo a su tamaño adecuado, muchos compiladores realizarán dicho truncamiento cuando se escriban las variables y esperen que tenga realizado antes de que se lea la variable. En tales compiladores, algo como:

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);
 }

bien podría resultar en wow()almacenar los valores 1234567 en los registros 0 y 1, respectivamente, y llamar foo(). Como xno es necesario dentro de "foo", y dado que se supone que las funciones ponen su valor de retorno en el registro 0, el compilador puede asignar el registro 0 a q. Si modees 1 o 3, el registro 0 se cargará con 2 o 4, respectivamente, pero si es algún otro valor, la función puede devolver lo que estaba en el registro 0 (es decir, el valor 1234567) aunque ese valor no esté dentro del rango de uint16_t.

Para evitar requerir que los compiladores realicen un trabajo adicional para garantizar que las variables no inicializadas nunca parezcan tener valores fuera de su dominio, y evitar la necesidad de especificar comportamientos indeterminados con excesivo detalle, el Estándar dice que el uso de variables automáticas no inicializadas es Comportamiento indefinido. En algunos casos, las consecuencias de esto pueden ser aún más sorprendentes que un valor que esté fuera del rango de su tipo. Por ejemplo, dado:

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

un compilador podría inferir que debido a que invocar moo()con un modo que es mayor que 3 inevitablemente conducirá al programa que invoca Comportamiento indefinido, el compilador puede omitir cualquier código que solo sería relevante si modees 4 o mayor, como el código que normalmente evitaría el lanzamiento de armas nucleares en tales casos. Tenga en cuenta que ni el estándar ni la filosofía moderna del compilador se preocuparían por el hecho de que se ignora el valor de retorno de "hey": el acto de intentar devolverlo le otorga al compilador una licencia ilimitada para generar código arbitrario.


0

La respuesta básica es, sí, no está definida.

Si ve un comportamiento extraño debido a esto, puede depender de dónde se declare. Si dentro de una función en la pila, entonces el contenido será muy diferente cada vez que se llama a la función. Si es un alcance estático o de módulo, no está definido pero no cambiará.


0

Si la clase de almacenamiento es estática o global, durante la carga, el BSS inicializa la variable o ubicación de memoria (ML) a 0 a menos que a la variable se le asigne inicialmente algún valor. En el caso de variables locales no inicializadas, la representación de la trampa se asigna a la ubicación de la memoria. Por lo tanto, si el compilador sobrescribe cualquiera de sus registros que contienen información importante, el programa puede bloquearse.

pero algunos compiladores pueden tener un mecanismo para evitar tal problema.

Estaba trabajando con la serie nec v850 cuando me di cuenta de que hay una representación de trampa que tiene patrones de bits que representan valores indefinidos para tipos de datos, excepto para char. Cuando tomé un carácter no inicializado, obtuve un valor predeterminado cero debido a la representación de trampa. Esto podría ser útil para any1 usando necv850es


Su sistema no es compatible si obtiene representaciones de trampas cuando usa char sin firmar. No se les permite explícitamente contener representaciones de trampas, C17 6.2.6.1/5.
Lundin

-2

El valor de num será un valor basura de la memoria principal (RAM). es mejor si inicializa la variable justo después de crear.


-4

Hasta donde había llegado, depende principalmente del compilador, pero en general la mayoría de los casos el valor es asumido como 0 por los compiladores.
Obtuve un valor de basura en el caso de VC ++, mientras que TC dio un valor de 0. Lo imprimo como a continuación

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

Si obtiene un valor determinista como, por ejemplo, 0su compilador probablemente siga pasos adicionales para asegurarse de que obtiene ese valor (agregando código para inicializar las variables de todos modos). Algunos compiladores hacen esto cuando compilan "depuración", pero elegir el valor 0para estos es una mala idea ya que ocultará fallas en su código (lo más apropiado sería garantizar un número realmente improbable 0xBAADF00Do similar). Creo que la mayoría de los compiladores dejarán cualquier basura que ocupe la memoria como el valor de la variable (es decir, en general no se ensambla como 0).
Skyking
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.