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 trata de un «buen estilo de programación». En este tema, yo tengo mis sospechas. Como programador, siempre me ha parecido incómodo 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á cerca 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 de 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. Sin embargo, C++ garantiza que cuando se crea un objeto, es inicializado simultáneamente. Esto asegura que no habrá objetos no inicializados rondando por el sistema. A C no le importa; 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 [58].
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 predefinido. Esto significa que cualquier objeto de una clase o variable de un tipo predefinido 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 e inicializa 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 usan, e inicializarlas siempre cuando se definen. (Ésta es una sugerencia de estilo para tipos predefinidos, en los que la inicialización es opcional.) Es una cuestión de seguridad. Al reducir el tiempo durante el que la variable está disponible dentro del bloque, se reduce la posibilidad de que sea usada inapropiadamente en otra parte del bloque. Además, la legibilidad mejora porque el lector no tiene 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 de 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 propósito de i y j, así que no necesita utilizar nombres tan aparatosos como contador_bucle_i para que 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[59]
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 parece mucho 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[60]
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 hacer demasiadas cosas en esa función. Funciones de granularidad más fina no sólo son más útiles, sino que también facilitan la localización de errores.
[58] C99, la versión actual del Estándar de C, permite definir variables en cualquier punto del bloque, como C++
[59] Una versión anterior del borrador del estándar C++ decía que la vida de la variable se extendía hasta el final del ámbito que encierra el bucle for. Algunos compiladores todavía lo implementan así, pero eso no es correcto de modo que su código sólo será portable si limita el ámbito al bucle for.
[60] El lenguaje Java considera esto una idea tan mala que señala ese código como un error.