En C, siempre se definen todas las variables al principio de cada bloque, justo después de la llave de apertura. Ése es un requisito habitual en los lenguajes de programación, y la razón que se da a menudo es que se considera «buenas prácticas de programación». En este tema, yo tengo mis sospechas. Eso siempre me pareció un inconveniente, como programador, volver al principio del bloque cada vez que necesitaba definir una nueva variable. También encuentro más legible el código cuando la definición de la variable está certa del punto donde se usa.
Quizá esos argumentos son estilísticos. En C++, sin embargo, existe un problema significativo si se fuerza a definir todos los objetos al comienzo un ámbito. Si existe un constructor, debe invocarse cuando el objeto se crea. Sin embargo, si el constructor toma uno o más argumentos, ¿cómo saber que se dispone de la información de inicialización al comienzo del ámbito? Generalmente no se dispone de esa información. Dado que C no tiene el concepto de privado, la separación entre definición e inicialización no es un problema. Además, C++ garantiza que cuando se crea un objeto, es inicializado simultáneamente. Esto asegura que no se tendrán objetos no inicializados ejecutándose en el sistema. C no tiene cuidado, de hecho, C promueve esta práctica ya que obliga a que se definan las variables al comienzo de un bloque, antes de disponer de la información de inicialización necesaria [55].
En general, C++ no permite crear un objeto antes de tener la información de inicialización para el constructor. Por eso, el lenguaje no sería factible si tuviera que definir variables al comienzo de un bloque. De hecho, el estilo del lenguaje parece promover la definición de un objeto tan cerca como sea posible del punto en el que se usa. En C++, cualquier regla que se aplica a un «objeto» automáticamente también se refiere a un objeto de un tipo básico. Esto significa que cualquier clase de objeto o variable de un tipo básico también se puede definir en cualquier punto del bloque. Eso también significa que puede esperar hasta disponer de la información para una variable antes de definirla, de modo que siempre puede definir e inicializar al mismo tiempo:
//: C06:DefineInitialize.cpp // Defining variables anywhere #include "../require.h" #include <iostream> #include <string> using namespace std; class G { int i; public: G(int ii); }; G::G(int ii) { i = ii; } int main() { cout << "initialization value? "; int retval = 0; cin >> retval; require(retval != 0); int y = retval + 3; G g(y); } ///:~
Listado 6.2. C06/DefineInitialize.cpp
Puede ver que se ejecuta parte del código, entonces se define
>retval
, que se usa para capturar datos de la
consola, y entonces se definen y
y
g
. C, al contrario, no permite definir una
variable en ningún sitio que no sea el comienzo de un bloque.
En general, debería definir las variables tan cerca como sea posible del punto en que se usa, e inicializarlas siempre cuando se definen. (Ésta es una sugerencia de estilo para tipos básicos, en los que la inicialización es opcional.) Es una cuestión de seguridad. Reduciendo la duración de disponibilidad al bloque, se reduce la posibilidad de que sea usada inapropiadamente en otra parte del bloque. En resumen, la legibilidad mejora porque el lector no teiene que volver al inicio del bloque para ver el tipo de una variable.
En C++, a menudo verá bucles for
con el
contador definido dentro de la propia expresión.
for (int j = 0; j < 100; j++) { cout << "j = " << j << endl; } for (int i = 0; i < 100; i++) cout << "i = " << i << endl;
Las sentencias anteriores son casos especiales importantes, que provocan confusión en los programadores novatos de C++.
Las variables i
y j
están
definidas directamente dentro la expresión
for
(algo que no se puede hacer en C). Esas
variables están disponibles para usarlas en el bucle. Es una
sintaxis muy conveniente porque el contexto disipa cualquier
duda sobre el proposito de i
y
j
, asi que no necesita utilizar nombres
extraños como contador_bucle_i
para quede
más claro.
Sin embargo, podría resultar confuso si espera que la vida de
las variables i
y j
continúe después del bucle - algo que no ocurre[56]
El capítulo 3 indica que las sentencias while
y switch
también permiten la definición de
objetos en sus expresiones de control, aunque ese uso es menos
importante que con el bucle for
.
Hay que tener cuidado con las variables locales que ocultan las variables del ámbito superior. En general, usar el mismo nombre para una variable anidada y una variable que es global en ese ámbito es confuso y propenso a errores[57]
Creo que los bloques pequeños son un indicador de un buen diseño. Si una sola función requiere varias páginas, quizá está intentando demasiadas cosas en esa función. Funciones de granularidad más fina no sólo son más útiles, tambíén facilitan la localización de errores.
[55] C99, la versión actual del Estándar de C, permite definir variables en cualquier punto del bloque, como C++
[56] Un reciente borrador del estándar C++ dice que la vida de
la variable se extiende hasta el final del ámbito que encierra
el bucle for
. Algunos compiladores lo
implementan, pero eso no es correcto de modo que su código sólo
será portable si limita el ámbito al bucle
for
.
[57] El lenguaje Java considera esto una idea tan mala que lo considera un error.