"Int * nums = {5, 2, 1, 4}" provoca un error de segmentación

81
int *nums = {5, 2, 1, 4};
printf("%d\n", nums[0]);

causa un defecto de segmento, mientras que

int nums[] = {5, 2, 1, 4};
printf("%d\n", nums[0]);

no lo hace. Ahora:

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);

impresiones 5.

Basado en esto, he conjeturado que la notación de inicialización de matriz, {}, carga ciegamente estos datos en cualquier variable que esté a la izquierda. Cuando es int [], la matriz se completa como se desee. Cuando es int *, el puntero se llena con 5, y las ubicaciones de memoria después de donde se almacena el puntero se completan con 2, 1 y 4. Entonces nums [0] intenta eliminar 5, causando un error de segmentación.

Si me equivoco, corrígeme. Y si estoy en lo correcto, por favor explique, porque no entiendo por qué los inicializadores de matriz funcionan de la manera en que lo hacen.

usuario1299784
fuente
3
Compile con todas las advertencias habilitadas y su compilador debería decirle qué sucede.
Jabberwocky
1
@GSerg Eso no es ni de lejos un duplicado. No hay un puntero de matriz en esta pregunta. Aunque algunas respuestas en esa publicación son similares a las de aquí.
Lundin
2
@Lundin Estaba 30% seguro, así que no voté para cerrar, solo publiqué el enlace.
GSerg
3
Adquiera el hábito de ejecutar GCC con -pedantic-errorsbandera y observe el diagnóstico. int *nums = {5, 2, 1, 4};no es válido C.
AnT

Respuestas:

113

Hay una regla (estúpida) en C que dice que cualquier variable simple puede inicializarse con una lista de inicializadores entre llaves, como si fuera una matriz.

Por ejemplo, puede escribir int x = {0};, que es completamente equivalente a int x = 0;.

Entonces, cuando escribe int *nums = {5, 2, 1, 4};, en realidad está dando una lista de inicializadores a una sola variable de puntero. Sin embargo, es solo una variable, por lo que solo se le asignará el primer valor 5, el resto de la lista se ignora (en realidad, no creo que el código con un exceso de inicializadores deba compilarse con un compilador estricto); no es así escribir en la memoria en absoluto. El código es equivalente a int *nums = 5;. Lo que significa que numsdebe apuntar a la dirección 5 .

En este punto, ya debería haber recibido dos advertencias / errores del compilador:

  • Asignación de un entero al puntero sin conversión.
  • Exceso de elementos en la lista de inicializadores.

Y luego, por supuesto, el código se bloqueará y se quemará, ya que 5lo más probable es que no sea una dirección válida con la que pueda eliminar la referencia nums[0].

Como nota al margen, debe señalar las printfdirecciones con el %pespecificador o, de lo contrario, está invocando un comportamiento indefinido.


No estoy muy seguro de lo que está tratando de hacer aquí, pero si desea establecer un puntero para apuntar a una matriz, debe hacer:

int nums[] = {5, 2, 1, 4};
int* ptr = nums;

// or equivalent:
int* ptr = (int[]){5, 2, 1, 4};

O si desea crear una matriz de punteros:

int* ptr[] = { /* whatever makes sense here */ };

EDITAR

Después de investigar un poco, puedo decir que la "lista de inicializadores de elementos en exceso" no es C válida, es una extensión GCC .

La inicialización estándar 6.7.9 dice (el énfasis es mío):

2 Ningún inicializador intentará proporcionar un valor para un objeto que no esté contenido en la entidad que se está inicializando.

/ - /

11 El inicializador de un escalar será una sola expresión, opcionalmente encerrada entre llaves. El valor inicial del objeto es el de la expresión (después de la conversión); Se aplican las mismas restricciones de tipo y conversiones que para la asignación simple, tomando el tipo del escalar como la versión no calificada de su tipo declarado.

"Tipo escalar" es un término estándar que se refiere a variables individuales que no son de tipo matriz, estructura o unión (se denominan "tipo agregado").

Entonces, en inglés simple, el estándar dice: "cuando inicializa una variable, no dude en agregar algunas llaves adicionales alrededor de la expresión del inicializador, solo porque puede".

Lundin
fuente
11
No hay nada "estúpido" en la capacidad de inicializar un objeto escalar con un solo valor incluido {}. Por el contrario, facilita uno de los modismos más importantes y convenientes del lenguaje C: { 0 }como inicializador cero universal. Todo en C se puede inicializar a cero mediante = { 0 }. Esto es muy importante para escribir código independiente del tipo.
2016
3
@AnT No existe un "inicializador universal de cero". En el caso de agregados, {0}simplemente significa inicializar el primer objeto a cero e inicializar el resto de los objetos como si tuvieran una duración de almacenamiento estático. Yo diría que esto es una coincidencia en lugar de un diseño de lenguaje intencional de algún "inicializador universal", ya {1}que no inicializa todos los objetos a 1.
Lundin
3
@Lundin C11 6.5.16.1/1 cubre p = 5;(ninguno de los casos enumerados se cumple para asignar un entero al puntero); y 6.7.9 / 11 dice que las restricciones para la asignación también se utilizan para la inicialización.
MM
4
@Lundin: Sí, lo hay. Es completamente irrelevante qué mecanismo inicializa qué parte del objeto. También es completamente irrelevante si {}se permite la inicialización de escalares con ese propósito específicamente. Lo único que importa es que = { 0 }se garantiza que el inicializador inicializará a cero todo el objeto , que es exactamente lo que lo convirtió en un clásico y uno de los modismos más elegantes del lenguaje C.
2016
2
@Lundin: Tampoco me queda del todo claro qué {1}tiene que ver su comentario sobre el tema. Nadie afirmó que lo {0}interpreta 0como un multiinicializador para todos y cada uno de los miembros del agregado.
2016
28

ESCENARIO 1

int *nums = {5, 2, 1, 4};    // <-- assign multiple values to a pointer variable
printf("%d\n", nums[0]);    // segfault

¿Por qué este segrega?

Declaraste numscomo un puntero a int, que se numssupone que contiene la dirección de un entero en la memoria.

Luego intentó inicializar numsa una matriz de múltiples valores. Entonces, sin profundizar en muchos detalles, esto es conceptualmente incorrecto : no tiene sentido asignar múltiples valores a una variable que se supone que tiene un valor. En este sentido, vería exactamente el mismo efecto si hace esto:

int nums = {5, 2, 1, 4};    // <-- assign multiple values to an int variable
printf("%d\n", nums);    // also print 5

En cualquier caso (asigne varios valores a un puntero o una variable int), lo que sucede entonces es que la variable obtendrá el primer valor que es 5, mientras que los valores restantes se ignoran. Este código cumple, pero recibirá advertencias por cada valor adicional que se supone que no debe estar en la asignación:

warning: excess elements in scalar initializer.

Para el caso de asignar múltiples valores a la variable de puntero, el programa segrega cuando accede nums[0], lo que significa que está eliminando lo que esté almacenado en la dirección 5 literalmente. En numseste caso, no asignó ninguna memoria válida para el puntero .

Vale la pena señalar que no hay un error de segmentación para el caso de asignar varios valores a la variable int (no está eliminando la referencia a ningún puntero no válido aquí).


ESCENARIO 2

int nums[] = {5, 2, 1, 4};

Este no segmenta, porque está asignando legalmente una matriz de 4 entradas en la pila.


ESCENARIO 3

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);   // print 5

Este no segmenta como se esperaba, porque está imprimiendo el valor del puntero en sí , NO lo que está desreferenciando (que es un acceso inválido a la memoria).


Otros

Casi siempre está condenado a la falla de segmentación cada vez que codifica el valor de un puntero como este (porque es la tarea del sistema operativo determinar qué proceso puede acceder a qué ubicación de memoria).

int *nums = 5;    // <-- segfault

Entonces, una regla general es inicializar siempre un puntero a la dirección de alguna variable asignada , como:

int a;
int *nums = &a;

o,

int a[] = {5, 2, 1, 4};
int *nums = a; 
artm
fuente
2
+1 Ese es un buen consejo, pero "nunca" realmente es demasiado fuerte, dadas las direcciones mágicas en muchas plataformas. (El uso de tablas constantes para esas direcciones fijas no apunta a variables existentes y, por lo tanto, viola su regla como se indica). Las cosas de bajo nivel, como el desarrollo de controladores, tratan con ese tipo de cosas con bastante frecuencia.
The Nate
3
"Esto es válido": ignorar el exceso de inicializadores es una extensión de GCC; en el estándar C que no está permitido
MM
1
@TheNate - sí, tienes razón. Edité la base en tu comentario, gracias.
artm
@MM - gracias por señalar eso. Edité para eliminar eso.
artm
25

int *nums = {5, 2, 1, 4};es un código mal formado. Hay una extensión GCC que trata este código de la misma manera que:

int *nums = (int *)5;

intentando formar un puntero a la dirección de memoria 5. (Esto no me parece una extensión útil, pero supongo que la base de desarrolladores la quiere).

Para evitar este comportamiento (o al menos, recibir una advertencia), puede compilar en modo estándar, por ejemplo -std=c11 -pedantic.

Una forma alternativa de código válido sería:

int *nums = (int[]){5, 2, 1, 4};

que apunta a un literal mutable de la misma duración de almacenamiento que nums. Sin embargo, la int nums[]versión es generalmente mejor ya que usa menos almacenamiento y puede usarla sizeofpara detectar cuánto tiempo dura la matriz.

MM
fuente
¿Se garantizaría que la matriz en la forma literal compuesta tenga una vida útil de almacenamiento al menos tan larga como la de nums?
supercat
@supercat sí, es automático si nums es automático y estático si nums es estático
MM
@MM: ¿Se aplicaría eso incluso si numsse declara una variable estática dentro de una función, o el compilador tendría derecho a limitar la vida útil de la matriz a la del bloque circundante incluso si se estuviera asignando a una variable estática?
supercat
@supercat sí (el primer bit). La segunda opción significaría UB la segunda vez que se llama a la función (ya que las variables estáticas solo se inicializan en la primera llamada)
MM
12
int *nums = {5, 2, 1, 4};

numses un puntero de tipo int. Por lo tanto, debe señalar este punto en una ubicación de memoria válida. num[0]está intentando eliminar la referencia de alguna ubicación de memoria aleatoria y, por lo tanto, la falla de segmentación.

Sí, el puntero tiene el valor 5 y está tratando de eliminar la referencia, lo que es un comportamiento indefinido en su sistema. (Parece que 5no es una ubicación de memoria válida en su sistema)

Mientras

int nums[] = {1,2,3,4};

es una declaración válida en la que está diciendo que numses una matriz de tipo inty la memoria se asigna en función del número de elementos pasados ​​durante la inicialización.

Gopi
fuente
1
"Sí, el puntero tiene el valor 5 y está tratando de eliminar la referencia, lo cual es un comportamiento indefinido". En absoluto, es un comportamiento perfectamente correcto y bien definido. Pero en el sistema que está usando el OP, no es una dirección de memoria válida, de ahí el bloqueo.
Lundin
@Lundin De acuerdo. Pero creo que OP nunca supo que 5 es una ubicación de memoria válida, así que hablé en esas líneas. Espero que la edición ayude
Gopi
¿Debería ser así? int *nums = (int[]){5, 2, 1, 4};
Islam Azab
10

Asignando {5, 2, 1, 4}

int *nums = {5, 2, 1, 4};

está asignando 5 a nums(después de un encasillado implícito de int a puntero a int). Deserferirlo hace una llamada de acceso a la ubicación de memoria en 0x5. Es posible que su programa no pueda acceder a eso.

Tratar

printf("%p", (void *)nums);
Fahad Siddiqui
fuente