Colocación de declaración variable en C

129

Durante mucho tiempo pensé que en C, todas las variables tenían que declararse al comienzo de la función. Sé que en C99, las reglas son las mismas que en C ++, pero ¿cuáles son las reglas de ubicación de declaración variable para C89 / ANSI C?

El siguiente código se compila correctamente con gcc -std=c89y gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

¿No deberían las declaraciones cy scausar un error en modo C89 / ANSI?

mcjabberz
fuente
54
Solo una nota: las variables en ansi C no tienen que declararse al comienzo de una función, sino al comienzo de un bloque. Entonces, char c = ... en la parte superior de su bucle for es completamente legal en ansi C. Sin embargo, los char * s no lo serían.
Jason Coco

Respuestas:

149

Se compila con éxito porque GCC permite la declaración scomo una extensión GNU, aunque no es parte del estándar C89 o ANSI. Si desea cumplir estrictamente con esos estándares, debe pasar la -pedanticbandera.

La declaración cal comienzo de un { }bloque es parte del estándar C89; El bloque no tiene que ser una función.

mipadi
fuente
41
Probablemente valga la pena señalar que solo la declaración de ses una extensión (desde el punto de vista C89). La declaración de ces perfectamente legal en C89, no se necesitan extensiones.
ANT
77
@AndreyT: Sí, en C, las declaraciones de variables deberían ser @ el comienzo de un bloque y no una función per se; pero la gente confunde bloque con función ya que es el ejemplo principal de un bloque.
legends2k
1
Moví el comentario con +39 votos en la respuesta.
MarcH
78

Para C89, debe declarar todas sus variables al comienzo de un bloque de alcance .

Por lo tanto, su char cdeclaración es válida ya que está en la parte superior del bloque de alcance del bucle for. Pero, la char *sdeclaración debería ser un error.

Kiley Hykawy
fuente
2
Muy correcto Puede declarar variables al comienzo de cualquier {...}.
Artelius el
55
@Artelius No del todo correcto. Solo si los curlies son parte de un bloque (no si son parte de una estructura o declaración de unión o un inicializador arriostrado).
Jens
Solo para ser pedante, la declaración errónea debe notificarse al menos de acuerdo con el estándar C. Por lo tanto, debería ser un error o una advertencia gcc. Es decir, no confíe en que un programa puede compilarse para significar que es compatible.
jinawee
35

La agrupación de declaraciones de variables en la parte superior del bloque es un legado probablemente debido a las limitaciones de los compiladores C primitivos y antiguos. Todos los lenguajes modernos recomiendan y a veces incluso hacen cumplir la declaración de variables locales en el último punto: donde se inicializan por primera vez. Porque esto elimina el riesgo de usar un valor aleatorio por error. Separar la declaración y la inicialización también le impide usar "const" (o "final") cuando pueda.

Desafortunadamente, C ++ sigue aceptando la antigua forma de declaración superior para la compatibilidad con C (una compatibilidad de C se arrastra de muchas otras ...) Pero C ++ intenta alejarse de ella:

  • El diseño de referencias de C ++ ni siquiera permite dicha agrupación de la parte superior del bloque.
  • Si separa la declaración y la inicialización de un objeto local de C ++ , paga el costo de un constructor adicional por nada. Si el constructor sin argumentos no existe, ¡nuevamente ni siquiera se le permite separar ambos!

C99 comienza a mover C en esta misma dirección.

Si le preocupa no encontrar dónde se declaran las variables locales, significa que tiene un problema mucho mayor: el bloque de cierre es demasiado largo y debe dividirse.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions

Marzo
fuente
Vea también cómo forzar declaraciones de variables en la parte superior del bloque puede crear agujeros de seguridad: lwn.net/Articles/443037
MarcH
"C ++ desafortunadamente sigue aceptando la antigua forma de declaración superior para la compatibilidad con C": en mi humilde opinión, es la forma más clara de hacerlo. Otro lenguaje "resuelve" este problema inicializando siempre con 0. Bzzt, que solo enmascara los errores lógicos si me preguntas. Y hay bastantes casos en los que NECESITA declaración sin inicialización porque hay varias ubicaciones posibles para la inicialización. Y es por eso que el RAII de C ++ es realmente un gran dolor en el trasero: ahora debe incluir un estado no válido "inicializado" en cada objeto para permitir estos casos.
Jo So
1
@JoSo: Estoy confundido, ¿por qué crees que haber leído las variables no inicializadas produce efectos arbitrarios hará que los errores de programación sean más fáciles de detectar que hacer que arrojen un valor constante o un error determinista? Tenga en cuenta que no hay garantía de que una lectura de almacenamiento no inicializado se comportará de manera coherente con cualquier patrón de bits que la variable podría haber tenido, ni siquiera que dicho programa se comportará de manera coherente con las leyes habituales de tiempo y causalidad. Dado algo así como int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat
1
@JoSo: Para los punteros, especialmente en implementaciones en las que se capturan operaciones null, todo-bits-cero suele ser un valor de trampa útil. Además, en los lenguajes que especifican explícitamente que las variables están predeterminadas en todos los bits cero, la confianza en ese valor no es un error . Los compiladores no todavía tienden a ser excesivamente loco con sus "optimizaciones", pero los autores de compiladores siguen tratando de obtener más y más inteligente. Una opción del compilador para inicializar variables con variables pseudoaleatorias deliberadas puede ser útil para identificar fallas, pero el simple hecho de dejar el almacenamiento con su último valor a veces puede enmascarar fallas.
supercat
22

Desde un punto de vista de mantenibilidad, en lugar de sintáctico, hay al menos tres líneas de pensamiento:

  1. Declare todas las variables al comienzo de la función para que estén en un solo lugar y pueda ver la lista completa de un vistazo.

  2. Declare todas las variables lo más cerca posible del lugar donde se usaron por primera vez, para saber por qué se necesitan.

  3. Declare todas las variables al comienzo del bloque de alcance más interno, por lo que saldrán del alcance lo antes posible y permitirán que el compilador optimice la memoria y le diga si las usó accidentalmente donde no había pensado.

Por lo general, prefiero la primera opción, ya que encuentro que los otros a menudo me obligan a buscar códigos para las declaraciones. Definir todas las variables por adelantado también facilita la inicialización y la observación desde un depurador.

A veces declaro variables dentro de un bloque de alcance más pequeño, pero solo por una Buena Razón, de la cual tengo muy pocas. Un ejemplo podría ser después de a fork(), para declarar variables que solo necesita el proceso secundario. Para mí, este indicador visual es un recordatorio útil de su propósito.

Adam Liss
fuente
27
Uso la opción 2 o 3 para que sea más fácil encontrar las variables, porque las funciones no deberían ser tan grandes que no se puedan ver las declaraciones de variables.
Jonathan Leffler
8
La opción 3 no es un problema, a menos que use un compilador de los años 70.
edgar.holleis
15
Si usó un IDE decente, no necesitaría buscar códigos, ya que debería haber un comando IDE para encontrar la declaración por usted. (F3 en Eclipse)
edgar.holleis
44
No entiendo cómo puede garantizar la inicialización en la opción 1, muchas veces solo puede obtener el valor inicial más adelante en el bloque, llamando a otra función o realizando una caclulación.
Plumenator
44
@Plumenator: la opción 1 no garantiza la inicialización; Elegí inicializarlos después de la declaración, ya sea a sus valores "correctos" o a algo que garantice que el código posterior se romperá si no se configuran adecuadamente. Digo "eligió" porque mi preferencia ha cambiado a # 2 desde que escribí esto, tal vez porque estoy usando Java más que C ahora, y porque tengo mejores herramientas de desarrollo.
Adam Liss
6

Como han señalado otros, GCC es permisivo en este sentido (y posiblemente en otros compiladores, dependiendo de los argumentos con los que se les llama) incluso cuando está en modo 'C89', a menos que use la verificación 'pedante'. Para ser honesto, no hay muchas buenas razones para no ser pedante; el código moderno de calidad siempre debe compilarse sin advertencias (o muy pocas cuando sabe que está haciendo algo específico que es sospechoso para el compilador como un posible error), por lo que si no puede compilar su código con una configuración pedante, probablemente necesite algo de atención.

C89 requiere que las variables se declaren antes que cualquier otra declaración dentro de cada alcance, los estándares posteriores permiten una declaración más cercana al uso (que puede ser más intuitiva y más eficiente), especialmente la declaración e inicialización simultánea de una variable de control de bucle en bucles 'for'.

Gaidheal
fuente
0

Como se ha señalado, hay dos escuelas de pensamiento sobre esto.

1) Declarar todo en la parte superior de las funciones porque el año es 1987.

2) Declarar el más cercano al primer uso y en el menor alcance posible.

¡Mi respuesta a esto es HACER AMBOS! Dejame explicar:

Para funciones largas, 1) hace que la refactorización sea muy difícil. Si trabaja en una base de código donde los desarrolladores están en contra de la idea de las subrutinas, tendrá 50 declaraciones de variables al comienzo de la función y algunas de ellas podrían ser una "i" para un ciclo for que parte inferior de la función.

Por lo tanto, desarrollé declaración de TEPT a partir de esto e intenté hacer la opción 2) religiosamente.

Regresé a la opción uno por una cosa: funciones cortas. Si sus funciones son lo suficientemente cortas, tendrá pocas variables locales y, dado que la función es corta, si las coloca en la parte superior de la función, aún estarán cerca del primer uso.

Además, el antipatrón de "declarar y establecer en NULL" cuando desea declarar en la parte superior pero no ha realizado algunos cálculos necesarios para la inicialización se resuelve porque las cosas que necesita inicializar probablemente se recibirán como argumentos.

Así que ahora pienso que debería declarar en la parte superior de las funciones y lo más cerca posible del primer uso. ¡Por lo tanto! Y la forma de hacerlo es con subrutinas bien divididas.

Pero si está trabajando en una función larga, ponga las cosas más cercanas al primer uso porque de esa manera será más fácil extraer métodos.

Mi receta es esta Para todas las variables locales, tome la variable y mueva su declaración al final, compile, luego mueva la declaración justo antes del error de compilación. Ese es el primer uso. Haga esto para todas las variables locales.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Ahora, defina un bloque de alcance que comience antes de la declaración y mueva el final hasta que el programa compile

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Esto no se compila porque hay más código que usa foo. Podemos notar que el compilador pudo pasar por el código que usa bar porque no usa foo. En este punto, hay dos opciones. La mecánica es simplemente mover el "}" hacia abajo hasta que se compile, y la otra opción es inspeccionar el código y determinar si el orden se puede cambiar a:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Si se puede cambiar el orden, eso es probablemente lo que desea porque acorta la vida útil de los valores temporales.

Otra cosa a tener en cuenta es que el valor de foo necesita ser preservado entre los bloques de código que lo usan, o podría ser un foo diferente en ambos. Por ejemplo

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

Estas situaciones necesitan más que mi procedimiento. El desarrollador tendrá que analizar el código para determinar qué hacer.

Pero el primer paso es encontrar el primer uso. Puede hacerlo visualmente, pero a veces es más fácil eliminar la declaración, intentar compilar y volver a colocarla por encima del primer uso. Si ese primer uso está dentro de una declaración if, colóquelo allí y verifique si se compila. El compilador identificará otros usos. Intente hacer un bloque de alcance que abarque ambos usos.

Una vez realizada esta parte mecánica, se hace más fácil analizar dónde están los datos. Si se usa una variable en un bloque de gran alcance, analice la situación y vea si solo está usando la misma variable para dos cosas diferentes (como una "i" que se usa para dos para bucles). Si los usos no están relacionados, cree nuevas variables para cada uno de estos usos no relacionados.

Philippe Carphin
fuente
0

Debe declarar todas las variables en la parte superior o "localmente" en la función. La respuesta es:

Depende del tipo de sistema que esté utilizando:

1 / Sistema integrado (especialmente relacionado con vidas como Avión o Coche): le permite utilizar memoria dinámica (por ejemplo: calloc, malloc, new ...). Imagine que está trabajando en un proyecto muy grande, con 1000 ingenieros. ¿Qué sucede si asignan nueva memoria dinámica y se olvidan de eliminarla (cuando ya no se usa)? Si el sistema incrustado se ejecuta durante mucho tiempo, provocará un desbordamiento de la pila y el software se corromperá. No es fácil asegurarse de la calidad (la mejor manera es prohibir la memoria dinámica).

Si un avión se ejecuta en 30 días y no se apaga, ¿qué sucede si el software está dañado (cuando el avión todavía está en el aire)?

2 / El otro sistema como web, PC (tiene un gran espacio de memoria):

Debe declarar la variable "localmente" para optimizar la memoria utilizando. Si estos sistemas funcionan durante mucho tiempo y se produce un desbordamiento de pila (porque alguien olvidó eliminar la memoria dinámica). Simplemente haga lo simple para reiniciar la PC: P No tiene impacto en las vidas

Dang_Ho
fuente
No estoy seguro de que esto sea correcto. ¿Supongo que está diciendo que es más fácil auditar las pérdidas de memoria si declara todas sus variables locales en un solo lugar? Eso puede ser cierto, pero no estoy tan seguro de comprarlo. En cuanto al punto (2), ¿dice que declarar la variable localmente "optimizaría el uso de la memoria"? Esto es teóricamente posible. Un compilador podría optar por cambiar el tamaño del marco de la pila en el transcurso de una función para minimizar el uso de memoria, pero no conozco ninguno que haga esto. En realidad, el compilador simplemente convertirá todas las declaraciones "locales" en "inicio de funciones detrás de escena".
QuinnFreedman
1 / El sistema incorporado en algún momento no permite la memoria dinámica, por lo que si declara todas las variables en la parte superior de la función. Cuando se construye el código fuente, puede calcular la cantidad de bytes que necesitan en la pila para ejecutar el programa. Pero con la memoria dinámica, el compilador no puede hacer lo mismo.
Dang_Ho
2 / Si declara una variable localmente, esa variable solo existe dentro de "{}" abrir / cerrar paréntesis. Entonces el compilador puede liberar el espacio de la variable si esa variable está "fuera de alcance". Eso puede ser mejor que declarar todo en la parte superior de la función.
Dang_Ho
Creo que estás confundido acerca de la memoria estática vs dinámica. La memoria estática se asigna en la pila. Todas las variables que se declaran en una función, sin importar dónde se declaren, se asignan estáticamente. La memoria dinámica se asigna en el montón con algo parecido malloc(). Aunque nunca he visto un dispositivo que sea incapaz de hacerlo, es una buena práctica evitar la asignación dinámica en sistemas embebidos ( ver aquí ). Pero eso no tiene nada que ver con donde declaras tus variables en una función.
QuinnFreedman
1
Si bien estoy de acuerdo en que esta sería una forma razonable de operar, no es lo que sucede en la práctica. Aquí está el ensamblaje real de algo muy parecido a su ejemplo: godbolt.org/z/mLhE9a . Como puede ver, en la línea 11, sub rsp, 1008se asigna espacio para toda la matriz fuera de la instrucción if. Esto es cierto para clangy gccen cada versión y nivel de optimización que probé.
QuinnFreedman
-1

Citaré algunas declaraciones del manual para gcc versión 4.7.0 para una explicación clara.

"El compilador puede aceptar varios estándares básicos, como 'c90' o 'c ++ 98', y dialectos GNU de esos estándares, como 'gnu90' o 'gnu ++ 98'. Al especificar un estándar base, el compilador aceptará todos los programas que sigan ese estándar y aquellos que usen extensiones GNU que no lo contradicen. Por ejemplo, '-std = c90' desactiva ciertas características de GCC que son incompatibles con ISO C90, como las palabras clave asm y typeof, pero no otras extensiones de GNU que no tienen un significado en ISO C90, como omitir el término medio de una expresión?: ".

Creo que el punto clave de su pregunta es por qué gcc no se ajusta a C89 incluso si se usa la opción "-std = c89". No sé la versión de tu gcc, pero creo que no habrá una gran diferencia. El desarrollador de gcc nos ha dicho que la opción "-std = c89" solo significa que las extensiones que contradicen a C89 están desactivadas. Por lo tanto, no tiene nada que ver con algunas extensiones que no tienen un significado en C89. Y la extensión que no restringe la ubicación de la declaración de variable pertenece a las extensiones que no contradicen C89.

Para ser sincero, todos pensarán que debería conformar C89 totalmente a primera vista de la opción "-std = c89". Pero no lo hace. En cuanto al problema que declara que todas las variables al principio es mejor o peor es solo cuestión de costumbre.

junwanghe
fuente
conformarse no significa no aceptar extensiones: siempre que el compilador compile programas válidos y produzca los diagnósticos necesarios para otros, se conforma.
Recuerda a Monica el
@Marc Lehmann, sí, tiene razón cuando se usa la palabra "conformar" para diferenciar los compiladores. Pero cuando la palabra "conformar" se usa para describir algunos usos, puede decir "Un uso no cumple con el estándar". Y todos los principiantes tienen la opinión de que los usos que no cumplen con el estándar deberían causar un error.
junwanghe
@Marc Lehmann, por cierto, no hay diagnóstico cuando gcc ve el uso que no cumple con el estándar C89.
junwanghe
Su respuesta sigue siendo incorrecta, porque afirmar que "gcc no se ajusta" no es lo mismo que "algún programa de usuario no se ajusta". Su uso de conformar es simplemente incorrecto. Además, cuando era principiante, no era de la opinión que usted afirmaba, así que eso también está mal. Por último, no es necesario que un compilador conforme diagnostique código no conforme y, de hecho, esto es imposible de implementar.
Recuerda a Mónica el