¿Dónde puede y no puede declarar nuevas variables en C?

77

Escuché (probablemente de un maestro) que uno debe declarar todas las variables encima del programa / función, y que declarar nuevas entre las declaraciones podría causar problemas.

Pero luego estaba leyendo K&R y me encontré con esta oración: "Las declaraciones de variables (incluidas las inicializaciones) pueden seguir a la llave izquierda que introduce cualquier declaración compuesta, no solo la que comienza una función". Sigue con un ejemplo:

Jugué un poco con el concepto y funciona incluso con matrices. Por ejemplo:

Entonces, ¿cuándo exactamente no puedo declarar variables? Por ejemplo, ¿qué pasa si mi declaración de variable no está justo después de la llave de apertura? Como aquí:

¿Podría esto causar problemas dependiendo del programa / máquina?

Daniel Scocco
fuente
5
gcces bastante laxo. Estás usando declaraciones y matrices de longitud variable c99. Compile con gcc -std=c89 -pedanticy le gritarán. Sin embargo, de acuerdo con c99, todo eso es kosher.
Dave
6
El problema es que has estado leyendo K&R, que está desactualizado.
Lundin
1
@Lundin ¿Existe un reemplazo apropiado para K&R? No hay nada después de la edición ANSI C, y el lector de este libro puede leer claramente a qué estándar se refiere
Brandin

Respuestas:

126

También escucho a menudo que poner variables en la parte superior de la función es la mejor manera de hacer las cosas, pero no estoy de acuerdo. Prefiero limitar las variables al ámbito más pequeño posible para que tengan menos posibilidades de ser mal utilizadas y así tenga menos cosas que llenen mi espacio mental en cada línea del programa.

Si bien todas las versiones de C permiten el alcance del bloque léxico, donde puede declarar las variables depende de la versión del estándar C a la que se dirige:

C99 en adelante o C ++

Los compiladores de C modernos, como gcc y clang, admiten los estándares C99 y C11 , que le permiten declarar una variable en cualquier lugar donde pueda ir una declaración. El alcance de la variable comienza desde el punto de la declaración hasta el final del bloque (siguiente llave de cierre).

También puede declarar variables internas para inicializadores de bucle. La variable solo existirá dentro del ciclo.

ANSI C (C90)

Si está apuntando al estándar ANSI C más antiguo , entonces está limitado a declarar variables inmediatamente después de una llave de apertura 1 .

Sin embargo, esto no significa que tenga que declarar todas sus variables en la parte superior de sus funciones. En C, puede colocar un bloque delimitado por llaves en cualquier lugar donde pueda ir una declaración (no solo después de cosas como ifo for) y puede usar esto para introducir nuevos alcances de variables. La siguiente es la versión ANSI C de los ejemplos anteriores de C99:


1 Tenga en cuenta que si está utilizando gcc, debe pasar la --pedanticbandera para que realmente haga cumplir el estándar C90 y quejarse de que las variables están declaradas en el lugar incorrecto. Si solo lo usa -std=c90, gcc acepta un superconjunto de C90 que también permite las declaraciones de variables C99 más flexibles.

hugomg
fuente
1
"El alcance de la variable comienza desde el punto de la declaración hasta el final del bloque", lo que, en caso de que alguien se pregunte, no significa que crear manualmente un bloque más estrecho sea útil / necesario para que el compilador use el espacio de la pila de manera eficiente. He visto esto un par de veces, y es una inferencia falsa del refrán falso de que C es 'ensamblador portátil'. Debido a que (A) la variable podría asignarse en un registro, no en la pila, & (B) si una variable está en la pila pero el compilador puede ver que deja de usarla, por ejemplo, el 10% del camino a través de un bloque, puede reciclar fácilmente ese espacio para otra cosa.
underscore_d
3
@underscore_d Tenga en cuenta que las personas que desean ahorrar memoria a menudo tratan con sistemas integrados, donde uno se ve obligado a ceñirse a niveles de optimización más bajos y / o versiones de compilador más antiguas debido a aspectos de certificación y / o cadena de herramientas.
apilador de clases
el hecho de que declare una variable en medio de un alcance no hace que su alcance sea más corto. simplemente hace que sea más difícil ver qué variables están en el alcance y cuáles no. Lo que hace que los ámbitos sean más cortos es hacer ámbitos anónimos, no declarar en el medio de un ámbito (que es solo un truco que mueve efectivamente la declaración a la parte superior y mantiene la asignación en su lugar, solo hace que sea más difícil razonar sobre el entorno del alcance, que es efectivamente isomorfo a tener una estructura anónima en cada alcance que el producto de todas las variables declaradas).
Dmitry
2
No sé de dónde sacaste la idea de que declarar variables en medio de un alcance es solo un "truco que efectivamente mueve la declaración a la parte superior". Este no es el caso y si intentas usar una variable en una línea y la declaras en la siguiente, obtendrás un error de compilación "variable no declarada".
hugomg
3

missingno cubre lo que permite ANSI C, pero no aborda por qué sus maestros le dijeron que declarara sus variables en la parte superior de sus funciones. Declarar variables en lugares extraños puede hacer que su código sea más difícil de leer y eso puede causar errores.

Tome el siguiente código como ejemplo.

Como puede ver, lo he declarado idos veces. Bueno, para ser más precisos, he declarado dos variables, ambas con el nombre i. Podría pensar que esto causaría un error, pero no es así, porque las dos ivariables están en diferentes ámbitos. Puede ver esto con mayor claridad cuando observa el resultado de esta función.

Primero, asignamos 20 y 30 a iy jrespectivamente. Luego, dentro de las llaves, asignamos 88 y 99. Entonces, ¿por qué entonces jmantiene su valor, pero ivuelve a ser 20? Es por las dos ivariables diferentes .

Entre el conjunto interno de llaves, la ivariable con el valor 20 está oculta e inaccesible, pero como no hemos declarado una nueva j, seguimos usando la jdel ámbito externo. Cuando dejamos el conjunto interior de llaves, la iretención del valor 88 desaparece, y nuevamente tenemos acceso al icon el valor 20.

A veces, este comportamiento es bueno, otras veces, tal vez no, pero debe quedar claro que si usa esta característica de C indiscriminadamente, realmente puede hacer que su código sea confuso y difícil de entender.

Haydenmuhl
fuente
30
Hiciste que tu código fuera difícil de leer porque usaste el mismo nombre para dos variables, no porque declaraste variables no al comienzo de la función. Son dos problemas distintos. Estoy totalmente en desacuerdo con la afirmación de que declarar variables en otros lugares hace que su código sea difícil de leer, creo que es todo lo contrario. Al escribir código, si declaras la variable cerca de cuándo se va a usar, siguiendo el principio de localidad temporal y espacial, al leer podrás identificar muy fácilmente qué hace, por qué está ahí y cómo se usa.
Havok
3
Como regla general, declaro todas las variables que se utilizan en varias ocasiones en el bloque al comienzo del bloque. Alguna variable temporal que es solo para un cálculo local en algún lugar, tiendo a declarar dónde se usa, ya que no tiene interés fuera de ese fragmento.
Lundin
5
Declarar una variable donde se necesita, no necesariamente en la parte superior de un bloque, a menudo le permite inicializarla. En lugar de { int n; /* computations ... */ n = some_value; }poder escribir { /* computations ... */ const int n = some_value; }.
Keith Thompson
@Havok "usaste el mismo nombre para dos variables" también conocidas como "variables sombreadas" ( man gccluego busca -Wshadow). así que estoy de acuerdo en que las variables sombreadas se demuestran aquí.
Trevor Boyd Smith
1

Si su compilador lo permite, entonces está bien declarar en cualquier lugar que desee. De hecho, el código es más legible (en mi humilde opinión) cuando declaras la variable que usas en lugar de en la parte superior de una función porque facilita la detección de errores, por ejemplo, olvidando inicializar la variable u ocultando accidentalmente la variable.

AndersK
fuente
0

Una publicación muestra el siguiente código:

y creo que la implicación es que son equivalentes. Ellos no son. Si int z se coloca en la parte inferior de este fragmento de código, provoca un error de redefinición contra la primera definición z pero no contra la segunda.

Sin embargo, varias líneas de:

funciona. Mostrando la sutileza de esta regla C99.

Personalmente, rechazo con pasión esta función del C99.

El argumento de que reduce el alcance de una variable es falso, como se muestra en estos ejemplos. Bajo la nueva regla, no puede declarar una variable de manera segura hasta que haya escaneado todo el bloque, mientras que anteriormente solo necesitaba comprender lo que estaba sucediendo al principio de cada bloque.

usuario2073625
fuente
1
La mayoría de las personas que están dispuestas a asumir la responsabilidad de realizar un seguimiento de su código, dan la bienvenida a 'declarar en cualquier lugar' con los brazos abiertos debido a los muchos beneficios que ofrece la legibilidad. Y fores una comparación irrelevante
underscore_d
No es tan complicado como lo haces parecer. El alcance de una variable comienza en su declaración y termina en la siguiente }. ¡Eso es! En el primer ejemplo, si desea agregar más líneas que usan zdespués de printf, lo haría dentro del bloque de código, no fuera de él. Definitivamente no es necesario "escanear todo el bloque" para ver si está bien definir una nueva variable. Tengo que confesar que el primer fragmento es un ejemplo un poco artificial y tiendo a evitarlo debido a la sangría adicional que produce. Sin embargo, el {int i; for(..){ ... }}patrón es algo que hago todo el tiempo.
hugomg
Su afirmación es inexacta porque en el segundo fragmento de código (ANSI C) ni siquiera puede poner una segunda declaración de int z en la parte inferior del bloque ANSI C porque ANSI C solo le permite poner declaraciones de variables en la parte superior. Entonces el error es diferente, pero el resultado es el mismo. No puede poner int z al final de ninguno de esos fragmentos de código.
RTHarston
Además, ¿cuál es el problema de tener varias líneas de ese bucle for? El int i solo vive dentro del bloque del ciclo for, por lo que no hay fugas ni definiciones repetidas del int i.
RTHarston
0

Según el lenguaje de programación C de K&R -

En C, todas las variables deben declararse antes de que se utilicen, generalmente al principio de la función antes de cualquier declaración ejecutable.

Aquí puedes ver la palabra que normalmente no es imprescindible.

Gagandeep kaur
fuente
En estos días, no todo C es K&R: muy poco código actual se compila con compiladores antiguos de K&R, entonces, ¿por qué usarlo como referencia?
Toby Speight
La claridad y su capacidad de explicar es asombrosa. Creo que es bueno aprender de los desarrolladores originales. Sí, es antiguo, pero es bueno para los principiantes.
Gagandeep kaur
0

Con clang y gcc, encontré problemas importantes con lo siguiente. gcc versión 8.2.1 20181011 clang versión 6.0.1

ni al compilador le gustaba que f1, f2 o f3 estuvieran dentro del bloque. Tuve que reubicar f1, f2, f3 en el área de definición de funciones. al compilador no le importaba la definición de un número entero con el bloque.

Leslie Satenstein
fuente
0

Internamente, todas las variables locales de una función se asignan en una pila o dentro de los registros de la CPU, y luego el código de máquina generado se intercambia entre los registros y la pila (llamado derrame de registros), si el compilador es malo o si la CPU no tiene suficientes registros para Mantenga todas las bolas haciendo malabares en el aire.

Para asignar cosas en la pila, la CPU tiene dos registros especiales, uno llamado Stack Pointer (SP) y otro: Base Pointer (BP) o puntero de marco (es decir, el marco de pila local al alcance de la función actual). SP apunta dentro de la ubicación actual en una pila, mientras que BP apunta al conjunto de datos de trabajo (arriba) y los argumentos de la función (debajo). Cuando se invoca la función, empuja el BP de la función principal / llamante a la pila (señalado por SP) y establece el SP actual como el nuevo BP, luego aumenta SP por el número de bytes derramados de los registros a la pila, hace el cálculo , y al regresar, restaura el BP de su padre, sacándolo de la pila.

Generalmente, mantener sus variables dentro de las suyas {} alcance podría acelerar la compilación y mejorar el código generado al reducir el tamaño del gráfico que el compilador tiene que recorrer para determinar qué variables se usan, dónde y cómo. En algunos casos (especialmente cuando se trata de goto), el compilador puede perder el hecho de que la variable ya no se usará, a menos que le indique explícitamente al compilador su alcance de uso. Los compiladores podrían tener un límite de tiempo / profundidad para buscar en el gráfico del programa.

El compilador podría colocar las variables declaradas cerca una de la otra en la misma área de pila, lo que significa que cargar una precargará todas las demás en la caché. De la misma manera, declarar variable registerpodría darle al compilador una pista de que desea evitar que dicha variable se derrame en la pila a toda costa.

El estricto estándar C99 requiere explícito { declaraciones antes, mientras que las extensiones introducidas por C ++ y GCC permiten declarar vars más en el cuerpo, lo que complica las declaraciones gotoy case. Además, C ++ permite declarar cosas dentro para la inicialización del ciclo, que está limitado al alcance del ciclo.

Por último, pero no menos importante, para otro ser humano que lea su código, sería abrumador ver la parte superior de una función llena de medio centenar de declaraciones de variables, en lugar de ubicarlas en sus lugares de uso. También facilita comentar su uso.

TLDR: usar {}para establecer explícitamente el alcance de las variables puede ayudar tanto al compilador como al lector humano.

SmugLispWeenie
fuente
"El estándar estricto C99 requiere {" explícito no es correcto. Supongo que te refieres a C89 allí. C99 permite declaraciones después de declaraciones.
Mecki