utilizo - ¿Qué le sucede a una variable declarada y no inicializada en C? ¿Tiene un valor?
se utilizo la variable local sin inicializar c++ (10)
Si en CI escribe:
int num;
Antes de asignar algo a num
, ¿el valor de num
indeterminado?
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 son globales o static
, se pondrán a cero. Si es auto
, el valor es indeterminado .
Este fue el caso en los compiladores anteriores a C89 y fue especificado por K & R y en 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ática 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 tipo aritmético se le asignara 0 y a cada miembro que tiene tipo de puntero se le asignó una constante de puntero nulo.
Este fue el caso en C99, ver la sección 6.7.8 Inicialización .
Si un objeto que tiene una duración de almacenamiento automática no se inicializa explícitamente, su valor es indeterminado. Si un objeto que tiene una duración de almacenamiento estática no se inicializa explícitamente, entonces:
- si tiene tipo de puntero, se inicializa a 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 es una unión, el primer miembro nombrado se inicializa (recursivamente) de acuerdo con estas reglas.
En cuanto a qué significa exactamente indeterminado , no estoy seguro para C89, C99 dice:
3.17.2
valor indeterminado
ya sea un valor no especificado o una representación de 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 mira los valores de clase de almacenamiento auto
, ve lo que quedó de su propio programa cuando utilizó por última vez esas direcciones de pila . Si asigna una gran cantidad de arreglos auto
verá que eventualmente comienzan claramente con ceros.
Tal vez se pregunte, ¿por qué es así? Una respuesta de SO diferente se refiere a esa pregunta, consulte: https://.com/a/2091505/140740
Debido a que las computadoras tienen capacidad de almacenamiento finita, las variables automáticas generalmente se mantendrán en los elementos de almacenamiento (ya sean registros o RAM) que hayan sido utilizados previamente para algún otro propósito arbitrario. Si se utiliza una de esas variables antes de que se le haya asignado un valor, ese almacenamiento puede contener lo que tenía anteriormente, y entonces el contenido de la variable será impredecible.
Como una arruga adicional, muchos compiladores pueden mantener las variables en registros que son más grandes que los tipos asociados. Aunque se requerirá un compilador para garantizar que cualquier valor que se escriba en una variable y se vuelva a leer se truncará y / o se extenderá a su tamaño adecuado, muchos compiladores realizarán dicho truncamiento cuando se escriban variables y esperan 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 a foo()
. Como x
no 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 el mode
es 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 exigir a los compiladores que realicen un trabajo adicional para garantizar que las variables no inicializadas nunca contengan valores fuera de su dominio, y eviten la necesidad de especificar comportamientos indeterminados con excesivo detalle, el Estándar dice que el uso de variables automáticas no inicializadas es Comportamiento no definido. 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 al invocar moo()
con un modo que es mayor que 3 inevitablemente conducirá al programa que invoca Comportamiento no definido, el compilador puede omitir cualquier código que solo sería relevante si el mode
es 4 o mayor, como el código lo que normalmente evitaría el lanzamiento de armas nucleares en tales casos. Tenga en cuenta que ni la filosofía del compilador Standard ni la moderna se preocupan por el hecho de que el valor de retorno de "hey" se ignora; el acto de intentar devolverlo le da al compilador una licencia ilimitada para generar código arbitrario.
Depende de la duración de almacenamiento de la variable. Una variable con duración de almacenamiento estática 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 que las variables a
y b
reciban 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" no es remotamente correcta. El comportamiento de la variable no inicializada es diferente del de una variable inicializada con basura.
El valor de num será un valor de basura de la memoria principal (RAM). es mejor si inicializas la variable justo después de crear.
Eso depende. Si esa definición es global (fuera de cualquier función), entonces num
se inicializará a cero. Si es local (dentro de una función), entonces 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 deben establecerse de maneras específicas para que usted obtenga resultados definidos de la lectura de la variable.
La respuesta básica es sí, no está definido.
Si observa un comportamiento extraño debido a esto, puede depender de dónde se declare. Si dentro de una función en la pila, los contenidos probablemente serán diferentes cada vez que se llame a la función. Si se trata de un ámbito estático o de un módulo, no está definido, pero no cambiará.
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 inicialmente un valor absurdo: algunos compiladores incluso pueden poner valores específicos y fijos para hacerlo obvio al buscar en un depurador, pero estrictamente hablando, el compilador es libre de hacer cualquier cosa, desde interrumpir hasta convocar. demonios a través de sus pasajes 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 indicador 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 marcador establecidos puede dar como resultado una excepción de CPU en una operación que realmente no debería fallar (por ejemplo, adición de enteros o asignación a otra variable). Y si va y deja una variable sin inicializar, el compilador puede recoger algo de basura al azar con estos bits de bandera configurados, lo que significa que tocar esa variable no inicializada puede ser mortal.
Por lo que yo había ido, en su mayoría depende del compilador, pero en general la mayoría de los casos el valor es asumido como 0 por los cumplidores.
Obtuve el valor de basura en el caso de VC ++, mientras que TC dio valor como 0. Lo imprimo como a continuación
int i;
printf(''%d'',i);
Si la clase de almacenamiento es estática o global, entonces durante la carga, el BSS inicializa la ubicación de la variable o 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 trampa se asigna a la ubicación de la memoria. Entonces, si el compilador sobreescribe 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 trama que tiene patrones de bits que representan valores indefinidos para los tipos de datos excepto para char. Cuando tomé un char sin inicializar obtuve un valor por defecto de cero debido a la representación de trampas. Esto podría ser útil para cualquier1 que use necv850es
Ubuntu 15.10, Kernel 4.2.0, x86-64, ejemplo de GCC 5.2.1
Bastantes 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 utilizó lo que estaba allí anteriormente.
#include <stdio.h>
int main() {
int i;
printf("%d/n", i);
}
compilar con:
gcc -O0 -std=c99 a.c
productos:
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:
%rdi
es el primer argumento de printf, por lo tanto, la cadena"%d/n"
en la dirección0x4005e4
%rsi
es el segundo argumento printf, por lo tantoi
.Viene de
-0x4(%rbp)
, que es la primera variable local de 4 bytes.En este punto,
rbp
está en la primera página de la pila que ha sido asignada por el kernel, por lo que para comprender ese valor deberíamos examinar el código del kernel y descubrir qué le asigna.TODO ¿el kernel 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 finalizados, con información filtrada. Ver: ¿Los valores no inicializados son un riesgo de seguridad?
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);
}
Variables globales
Estándares: 0
Implementación: sección .bss
.
#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 0x601044
en la dirección 0x601044
y:
readelf -SW a.out
contiene:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
que dice que 0x601044
está justo en el medio de la sección .bss
, que comienza en 0x601040
y tiene 8 bytes de longitud.
El estándar ELF luego garantiza que la sección llamada .bss
está completamente llena de ceros:
.bss
Esta 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 no ocupa espacio de archivos, como lo indica el tipo de sección,SHT_NOBITS
.
Además, el tipo SHT_NOBITS
es eficiente y no ocupa espacio en el archivo ejecutable:
sh_size
Este miembro proporciona el tamaño de la sección en bytes. A menos que el tipo de sección seaSHT_NOBITS
, la secciónsh_size
bytes en el archivo. Una sección de tipoSHT_NOBITS
puede tener un tamaño distinto de cero, pero no ocupa espacio en el archivo.
Luego depende del kernel de Linux poner a cero esa región de memoria cuando se carga el programa en la memoria cuando se inicia.