Confusión sobre la inicialización de la matriz en C

102

En lenguaje C, si inicializa una matriz como esta:

int a[5] = {1,2};

entonces todos los elementos de la matriz que no se inicializan explícitamente se inicializarán implícitamente con ceros.

Pero, si inicializo una matriz como esta:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

salida:

1 0 1 0 0

No entiendo, ¿por qué a[0]imprime en 1lugar de 0? ¿Es un comportamiento indefinido?

Nota: esta pregunta se hizo en una entrevista.

msc
fuente
35
La expresión se a[2]=1evalúa como 1.
tkausl
14
Una pregunta muy profunda. Me pregunto si el entrevistador sabe la respuesta por sí mismo. Yo no. De hecho, aparentemente el valor de la expresión a[2] = 1es 1, pero no estoy seguro de si se le permite tomar el resultado de una expresión de inicializador designada como el valor del primer elemento. El hecho de que haya agregado la etiqueta de abogado significa que creo que necesitamos una respuesta citando el estándar.
Betsabé
15
Bueno, si esa es su pregunta favorita, es posible que haya esquivado una bala. Personalmente, prefiero que un ejercicio de programación escrito (con acceso a un compilador y depurador) se lleve a cabo durante unas pocas horas en lugar de preguntas de estilo "as" como las anteriores. Podría conjeturar una respuesta, pero no creo que tenga una base fáctica real.
Betsabé
1
@Bathsheba Haría lo contrario, ya que la respuesta aquí ahora responde a ambas preguntas.
Adiós SE
1
@Bathsheba sería lo mejor. Aún así, le daría el crédito por la pregunta a OP, ya que se le ocurrió el tema. Pero no me corresponde a mí decidir qué creo que sería "lo correcto".
Adiós SE

Respuestas:

95

TL; DR: No creo que el comportamiento de int a[5]={a[2]=1}; esté bien definido, al menos en C99.

La parte divertida es que lo único que tiene sentido para mí es la parte por la que estás preguntando: a[0]está configurada para1 porque el operador de asignación devuelve el valor que se asignó. Es todo lo demás lo que no está claro.

Si el código hubiera sido así int a[5] = { [2] = 1 }, todo hubiera sido fácil: esa es una configuración de inicializador designada a[2]para 1y todo lo demás para 0. Pero con { a[2] = 1 }tenemos un inicializador no designado que contiene una expresión de asignación, y caemos en un agujero de conejo.


Esto es lo que encontré hasta ahora:

  • a debe ser una variable local.

    6.7.8 Inicialización

    1. Todas las expresiones en un inicializador para un objeto que tiene una duración de almacenamiento estática serán expresiones constantes o literales de cadena.

    a[2] = 1no es una expresión constante, por lo que adebe tener almacenamiento automático.

  • a está dentro del alcance en su propia inicialización.

    6.2.1 Ámbitos de identificadores

    1. Las etiquetas de estructura, unión y enumeración tienen un alcance que comienza justo después de la aparición de la etiqueta en un especificador de tipo que declara la etiqueta. Cada constante de enumeración tiene un alcance que comienza justo después de la aparición de su enumerador de definición en una lista de enumeradores. Cualquier otro identificador tiene un alcance que comienza justo después de completar su declarador.

    El declarador es a[5], por lo que las variables están dentro del alcance en su propia inicialización.

  • a está vivo en su propia inicialización.

    6.2.4 Duraciones de almacenamiento de objetos

    1. Un objeto cuyo identificador se declara sin vinculación y sin el especificador de clase de almacenamientostatic tiene una duración de almacenamiento automática .

    2. Para un objeto de este tipo que no tiene un tipo de matriz de longitud variable, su vida se extiende desde la entrada al bloque con el que está asociado hasta que la ejecución de ese bloque termina de alguna manera. (Entrar en un bloque cerrado o llamar a una función suspende, pero no finaliza, la ejecución del bloque actual.) Si el bloque se introduce de forma recursiva, se crea una nueva instancia del objeto cada vez. El valor inicial del objeto es indeterminado. Si se especifica una inicialización para el objeto, se realiza cada vez que se alcanza la declaración en la ejecución del bloque; de lo contrario, el valor se vuelve indeterminado cada vez que se alcanza la declaración.

  • Hay un punto de secuencia después a[2]=1.

    6.8 Declaraciones y bloques

    1. Una expresión completa es una expresión que no forma parte de otra expresión o de un declarador. Cada uno de los siguientes es una expresión completa: un inicializador ; la expresión en una declaración de expresión; la expresión de control de una declaración de selección ( ifo switch); la expresión controladora de una declaración whileo do; cada una de las expresiones (opcionales) de una fordeclaración; la expresión (opcional) en una returndeclaración. El final de una expresión completa es un punto de secuencia.

    Tenga en cuenta que, por ejemplo, en int foo[] = { 1, 2, 3 }la { 1, 2, 3 }parte hay una lista de inicializadores entre llaves, cada uno de los cuales tiene un punto de secuencia después.

  • La inicialización se realiza en el orden de la lista de inicializadores.

    6.7.8 Inicialización

    1. Cada lista de inicializadores entre llaves tiene un objeto actual asociado . Cuando no hay designaciones presentes, los subobjetos del objeto actual se inicializan en orden según el tipo de objeto actual: elementos de matriz en orden de subíndice creciente, miembros de estructura en orden de declaración y el primer miembro nombrado de una unión. [...]

     

    1. La inicialización ocurrirá en el orden de la lista de inicializadores, cada inicializador proporcionado para un subobjeto particular anulando cualquier inicializador previamente listado para el mismo subobjeto; todos los subobjetos que no se inicializan explícitamente se inicializarán implícitamente igual que los objetos que tienen una duración de almacenamiento estático.
  • Sin embargo, las expresiones de inicializador no se evalúan necesariamente en orden.

    6.7.8 Inicialización

    1. El orden en el que se producen los efectos secundarios entre las expresiones de la lista de inicialización no está especificado.

Sin embargo, eso todavía deja algunas preguntas sin respuesta:

  • ¿Son los puntos de secuencia siquiera relevantes? La regla básica es:

    6.5 Expresiones

    1. Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado como máximo una vez mediante la evaluación de una expresión . Además, el valor anterior se leerá solo para determinar el valor que se almacenará.

    a[2] = 1 es una expresión, pero la inicialización no lo es.

    Esto se contradice levemente con el Anexo J:

    J.2 Comportamiento indefinido

    • Entre dos puntos de secuencia, un objeto se modifica más de una vez, o se modifica y el valor anterior se lee de otra manera que para determinar el valor a almacenar (6.5).

    El anexo J dice que cualquier modificación cuenta, no solo modificaciones por expresiones. Pero dado que los anexos no son normativos, probablemente podamos ignorarlo.

  • ¿Cómo se secuencian las inicializaciones de subobjetos con respecto a las expresiones del inicializador? ¿Se evalúan primero todos los inicializadores (en algún orden) y luego los subobjetos se inicializan con los resultados (en el orden de la lista de inicializadores)? ¿O se pueden intercalar?


Creo que int a[5] = { a[2] = 1 }se ejecuta de la siguiente manera:

  1. Almacenamiento para a se asigna cuando se ingresa su bloque contenedor. El contenido es indeterminado en este momento.
  2. El (único) inicializador se ejecuta ( a[2] = 1), seguido de un punto de secuencia. Esto almacena 1en a[2]y retornos1 .
  3. Eso 1se usa para inicializar a[0](el primer inicializador inicializa el primer subobjeto).

Pero aquí las cosas se ponen borrosa debido a que los elementos restantes ( a[1], a[2], a[3], a[4]) se supone que deben ser inicializado a 0, pero no está claro cuándo: ¿Ocurre antes de que a[2] = 1se evalúa? Si es así, a[2] = 1¿"ganaría" y sobrescribiría a[2], pero esa asignación tendría un comportamiento indefinido porque no hay un punto de secuencia entre la inicialización cero y la expresión de asignación? ¿Son los puntos de secuencia incluso relevantes (ver arriba)? ¿O ocurre la inicialización cero después de evaluar todos los inicializadores? Si es así, a[2]debería acabar siendo 0.

Dado que el estándar C no define claramente lo que sucede aquí, creo que el comportamiento no está definido (por omisión).

melpomene
fuente
1
En lugar de indefinido, diría que no está especificado , lo que deja las cosas abiertas a la interpretación de las implementaciones.
Un tipo programador
1
"Caemos en una madriguera de conejo" ¡LOL! Nunca escuché eso para una UB o cosas no especificadas.
BЈовић
2
@Someprogrammerdude No creo que pueda ser sin especificar (" comportamiento en el que esta Norma Internacional proporciona dos o más posibilidades y no impone más requisitos sobre cuál se elige en cualquier caso ") porque la norma realmente no proporciona ninguna posibilidad entre las cuales escoger. Simplemente no dice lo que sucede, lo que creo que se incluye en "El comportamiento indefinido está [...] indicado en esta Norma Internacional [...] por la omisión de cualquier definición explícita de comportamiento " .
melpomene
2
@ BЈовић También es una descripción muy agradable no solo para el comportamiento indefinido, sino también para el comportamiento definido que necesita un hilo como este para explicarlo.
gnasher729
1
@JohnBollinger La diferencia es que en realidad no puede inicializar el a[0]subobjeto antes de evaluar su inicializador, y la evaluación de cualquier inicializador incluye un punto de secuencia (porque es una "expresión completa"). Por tanto, creo que modificar el subobjeto que estamos inicializando es un juego limpio.
melpomene
22

No entiendo, ¿por qué a[0]imprime en 1lugar de 0?

Presumiblemente se a[2]=1inicializa a[2]primero y el resultado de la expresión se usa para inicializara[0] .

Desde N2176 (borrador C17):

6.7.9 Inicialización

  1. Las evaluaciones de las expresiones de la lista de inicialización tienen una secuencia indeterminada entre sí y, por lo tanto, no se especifica el orden en el que se producen los efectos secundarios. 154)

Entonces parecería que la salida 1 0 0 0 0 también habría sido posible.

Conclusión: No escriba inicializadores que modifiquen la variable inicializada sobre la marcha.

user694733
fuente
1
Esa parte no se aplica: aquí solo hay una expresión inicializadora, por lo que no es necesario secuenciarla con nada.
melpomene
@melpomene Existe la {...}expresión que se inicializa a[2]en 0y la subexpresión a[2]=1que se inicializa a[2]en 1.
user694733
1
{...}es una lista de inicializadores entre corchetes. No es una expresión.
melpomene
@melpomene Ok, puede que tengas razón. Pero todavía diría que todavía hay 2 efectos secundarios en competencia para que el párrafo se mantenga.
user694733
@melpomene hay dos cosas para secuenciar: el primer inicializador y la configuración de otros elementos en 0
MM
6

Creo que el estándar C11 cubre este comportamiento y dice que el resultado no está especificado , y no creo que C18 haya realizado cambios relevantes en esta área.

El lenguaje estándar no es fácil de analizar. La sección relevante de la norma es §6.7.9 Inicialización . La sintaxis se documenta como:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Tenga en cuenta que uno de los términos es expresión de asignación , y dado a[2] = 1que indudablemente es una expresión de asignación, se permite dentro de los inicializadores para matrices con duración no estática:

§4 Todas las expresiones en un inicializador para un objeto que tiene una duración de almacenamiento estática o en subprocesos serán expresiones constantes o literales de cadena.

Uno de los párrafos clave es:

§19 La inicialización ocurrirá en el orden de la lista de inicializadores, cada inicializador proporcionado para un subobjeto particular anulando cualquier inicializador previamente listado para el mismo subobjeto; 151) todos los subobjetos que no se inicializan explícitamente se inicializarán implícitamente igual que los objetos que tienen una duración de almacenamiento estático.

151) Cualquier inicializador para el subobjeto que se anula y, por lo tanto, no se usa para inicializar ese subobjeto, podría no ser evaluado en absoluto.

Y otro párrafo clave es:

§23 Las evaluaciones de las expresiones de la lista de inicialización tienen una secuencia indeterminada entre sí y, por lo tanto, no se especifica el orden en el que ocurren los efectos secundarios. 152)

152) En particular, el orden de evaluación no necesita ser el mismo que el orden de inicialización del subobjeto.

Estoy bastante seguro de que el párrafo 23 indica que la notación en la pregunta:

int a[5] = { a[2] = 1 };

conduce a un comportamiento no especificado. La asignación a a[2]es un efecto secundario, y el orden de evaluación de las expresiones está secuenciado indeterminadamente entre sí. En consecuencia, no creo que haya una forma de apelar al estándar y afirmar que un compilador en particular está manejando esto correcta o incorrectamente.

Jonathan Leffler
fuente
Solo hay una expresión de lista de inicialización, por lo que §23 no es relevante.
melpomene
2

Mi comprensión a[2]=1devuelve el valor 1, por lo que el código se convierte

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}asignar valor a [0] = 1

Por lo tanto, imprime 1 para un [0]

Por ejemplo

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;
Karthika
fuente
2
Esta es una pregunta de [abogado de idiomas], pero esta no es una respuesta que funcione con el estándar, por lo que es irrelevante. Además, también hay 2 respuestas mucho más detalladas disponibles y su respuesta no parece agregar nada.
Adiós SE
Tengo una duda ¿Es incorrecto el concepto que publiqué? ¿Podrías aclararme con esto?
Karthika
1
Simplemente especula por razones, mientras que ya se ha dado una muy buena respuesta con partes relevantes del estándar. Decir simplemente cómo podría suceder no es de lo que se trata la pregunta. Se trata de lo que la norma dice que debería suceder.
Adiós SE
Pero la persona que publicó la pregunta anterior preguntó el motivo y ¿por qué sucede? Así que solo dejé esta respuesta, pero el concepto es correcto, ¿verdad?
Karthika
OP preguntó " ¿Es un comportamiento indefinido? ". Tu respuesta no lo dice.
melpomene
1

Intento dar una respuesta breve y sencilla al acertijo: int a[5] = { a[2] = 1 };

  1. Primero a[2] = 1se establece. Eso significa que la matriz dice:0 0 1 0 0
  2. Pero he aquí, dado que lo hizo { }entre corchetes, que se usan para inicializar la matriz en orden, toma el primer valor (que es 1) y lo establece en a[0]. Es como si int a[5] = { a[2] };se quedara, donde ya llegamos a[2] = 1. La matriz resultante es ahora:1 0 1 0 0

Otro ejemplo: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Aunque el orden es algo arbitrario, asumiendo que va de izquierda a derecha, iría en estos 6 pasos:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3
Batalla
fuente
1
A = B = C = 5no es una declaración (o inicialización). Es una expresión normal que se analiza A = (B = (C = 5))porque el =operador es asociativo a la derecha. Eso realmente no ayuda a explicar cómo funciona la inicialización. La matriz realmente comienza a existir cuando se ingresa el bloque en el que está definida, lo que puede ser mucho antes de que se ejecute la definición real.
melpomene
1
" Va de izquierda a derecha, cada uno comenzando con la declaración interna " es incorrecto. El estándar C dice explícitamente " El orden en el que ocurren los efectos secundarios entre las expresiones de la lista de inicialización no está especificado " .
melpomene
1
" Prueba el código de mi ejemplo suficientes veces y ve si los resultados son consistentes " . No es así como funciona. Parece que no comprendes qué es el comportamiento indefinido. Todo en C tiene un comportamiento indefinido por defecto; es solo que algunas partes tienen un comportamiento definido por el estándar. Para demostrar que algo tiene un comportamiento definido, debe citar el estándar y mostrar dónde define lo que debería suceder. En ausencia de tal definición, el comportamiento no está definido.
melpomene
1
La afirmación en el punto (1) es un salto enorme sobre la pregunta clave aquí: ¿ocurre la inicialización implícita del elemento a [2] a 0 antes de a[2] = 1que se aplique el efecto secundario de la expresión del inicializador? El resultado observado es como si lo fuera, pero el estándar no parece especificar que ese debería ser el caso. Ese es el centro de la controversia, y esta respuesta lo pasa por alto por completo.
John Bollinger
1
"Comportamiento indefinido" es un término técnico con un significado limitado. No significa "comportamiento del que no estamos realmente seguros". La idea clave aquí es que ninguna prueba, sin compilador, puede mostrar que un programa en particular se comporta bien o no de acuerdo con el estándar , porque si un programa tiene un comportamiento indefinido, el compilador puede hacer cualquier cosa , incluido trabajar de una manera perfectamente predecible y razonable. No se trata simplemente de un problema de calidad de implementación en el que los redactores del compilador documentan las cosas, es un comportamiento no especificado o definido por la implementación.
Jeroen Mostert
0

La asignación a[2]= 1es una expresión que tiene el valor 1y usted esencialmente escribió int a[5]= { 1 };(con el efecto secundario que también a[2]se le asigna 1).

Yves Daoust
fuente
Pero no está claro cuándo se evalúa el efecto secundario y el comportamiento puede cambiar según el compilador. Además, el estándar parece indicar que este es un comportamiento indefinido, lo que hace que las explicaciones para las realizaciones específicas del compilador no sean útiles.
Adiós SE
@KamiKaze: seguro, el valor 1 aterrizó allí por accidente.
Yves Daoust
0

Creo que ese int a[5]={ a[2]=1 };es un buen ejemplo para un programador que se dispara a sí mismo en su propio pie.

Podría estar tentado a pensar que lo que quería decir era int a[5]={ [2]=1 };cuál sería un elemento de configuración de inicializador designado C99 2 a 1 y el resto a cero.

En el raro caso de que realmente int a[5]={ 1 }; a[2]=1;quisieras decirlo , sería una forma divertida de escribirlo. De todos modos, esto es a lo que se reduce su código, aunque algunos aquí señalaron que no está bien definido cuándo a[2]se ejecuta realmente la escritura . El problema aquí es que a[2]=1no es un inicializador designado, sino una asignación simple que en sí misma tiene el valor 1.

Sven
fuente
Parece que este tema de abogados de idiomas solicita referencias de borradores estándar. Es por eso que te votaron negativamente (no lo hice, ya que ves que me votaron negativamente por la misma razón). Creo que lo que escribiste está bien, pero parece que todos estos abogados de idiomas aquí son del comité o algo así. Por lo tanto, no están pidiendo ayuda en absoluto, están tratando de verificar si el borrador cubre el caso o no y la mayoría de los chicos aquí se activan si usted responde como si los estuviera ayudando. Supongo que eliminaré mi respuesta :) Si las reglas de este tema se pusieran claramente, habría sido útil
Abdurrahim