¿Qué entendemos por “pasar una variable”? Básicamente es entregarle a una función, al momento de ser llamada, alguna variable ya existente. Si queremos que una función acepte valores externos a ella, es necesario que en su declaración coloquemos parámetros. En el mundo de la programación existen dos formas muy famosas de pasar variables a alguna función: una se conoce como paso por valor y la otra es conocida como paso por referencia.
El Paso por Valor en C
La invocación de funciones implica, generalmente y de manera predefinida, un paso de parámetros por valor. En esta lección, aprenderemos a transferir parámetros por valor en el lenguaje de programación C. De acuerdo a nuestras clases, la primera forma nos dice que nuestra función recibirá una copia de la variable que pasemos y, cualquier modificación que realicemos, solo afectará a dicha copia. En la llamada a la función almacenarSueldoAnual, estamos indicando que esta función tome como parámetro de entrada el valor de la variable sueldoAnualTemporal y, dicho valor, lo copie a la variable que estamos creando en el ámbito de la función. Y esto que acabamos de mencionar es muy importante.
En nuestra función no estamos trabajando con el valor original, sino con una copia del mismo. Esta variable es creada localmente en el ámbito de la función e inicializada con el valor que le proporcionamos en la llamada. Este proceso de envío de parámetros desde la llamada de la función hasta el ámbito propio de la misma, donde el valor se copia en una nueva variable local con la que se va a trabajar, es lo que se conoce como paso de parámetros por valor. Esto se denomina paso de parámetros por valor. Como los parámetros formales son variables distintas de los parámetros reales, los cambios efectuados en los parámetros formales no afectan para nada a los parámetros reales. Al retornar a main, a y b conservan sus valores originales.
Declaración de Variables en C
Todo lo expuesto en relación a variables en el curso Bases de la programación nivel I será válido y útil con el lenguaje C, pero con matices, ya que cada lenguaje sigue sus propias normas y pautas. El lenguaje C obliga a declarar una variable antes de ser usada. Declarar una variable no significa que se le asigne contenido, sino simplemente se indica que la variable existe. Sin embargo en C no está permitido usar una variable sin antes haberle asignado un contenido, a lo que se denomina “inicializar la variable”. Por tanto, al tratar de ejecutar un programa donde una variable está sin inicializar puede producirse un error de compilación. Nosotros vamos a trabajar con C por lo que habremos de declarar e inicializar las variables que usemos siempre.
Conociendo ya los tipos de variables básicas y cómo nombrarlas, veamos ahora cómo declararlas. La declaración de variables en C debe hacerse al principio del programa. Usaremos para ello el tipo de variable int (entero). Para facilitar la corrección y claridad de nuestros programas el lugar donde realizaremos la declaración de variables, al menos de momento, será después de la línea int main(), que constituye el inicio del código ejecutivo de nuestro programa.
Lea también: Longitud del RFC para personas morales en México
Formas de Declaración y Tipos de Datos
En C podemos declarar variables en múltiples líneas una por línea pero también varias variables en una línea. Existen distintas formas para realizar declaraciones. Por ejemplo, se declaran tres variables de tipo entero denominadas i, j, k. Las opciones escritas de forma extendida, también son válidas y con el mismo resultado. Este tipo de escritura nos permite declarar e inicializar la variable con un valor en una sola línea. No siempre lo haremos así, pero en algunas ocasiones nos puede resultar de interés. Has declarado la variable numeroDePlantas como tipo entero. Nosotros nos decantamos por no usar declaraciones donde en una misma línea aparezcan variables de un tipo de forma abreviada porque pensamos que resulta menos claro a la hora de leer un programa.
Es importante elegir siempre el tipo de variable más sencillo posible. Consideraremos que el grado de sencillez viene definido por la memoria requerida (a menor memoria requerida mayor es la sencillez). A diferencia de la mayoría de lenguajes, C no incorpora entre sus tipos predefinidos el tipo booleano (true / false). Para el caso del valor interruptor se supone que este tipo de variable sólo tendrá uno entre dos valores posibles: true ó false, 1 ó 0, ‘t’ ó ‘f’. Los tipos enumerados (enum) son variables especiales que tienen un nombre “general” y luego n valores posibles, por ejemplo la variable “color” podría tener como valores posibles “rojo”, “verde” y “azul”.
Una peculiaridad de C respecto a otros lenguajes es que no tiene un tipo predefinido “String” o cadena de caracteres. Cuando queramos declarar una variable como tipo alfanumérico tendremos que hacerlo declarando un array de caracteres. Es decir, en C las cadenas se declaran como vectores de tipo char. Hemos utilizado el tipo float para cubrirnos las espaldas. No sabemos con qué tipo de valores vamos a trabajar y por ello cogemos un tipo de variables bastante amplio como es el float. En este caso hemos supuesto que vamos a requerir la mayor precisión posible, de ahí que hayamos cogido tipos double. También hubiera resultado válido usar tipo float. El número pi es un valor constante, por lo que podremos declararlo como una constante en lugar de cómo una variable, como veremos más adelante. Se dispone de una matriz (una tabla) de dimensiones MxN, formada por elementos de tipo double.
Punteros y el Paso por Referencia "simulado" en C
Lo anteriormente dicho luce muy bien y tal parece que no debería existir razón alguna para que nos lleguemos a confundir pero… ¿es así de sencillo? Si lo fuera, ¿por qué siguen ocurriendo confusiones respecto a ambos términos? Es momento de otorgar respuestas. El paso de parámetros en C está construido de tal modo que los valores resultantes de evaluar los parámetros formales se copian en los parámetros formales. Esto es cierto e insalvable, pero disponemos de un auxiliar sumamente interesante: los punteros.
Por definición, un apuntador es un tipo especial de variable que almacena una dirección de memoria. Los punteros, independientemente del tipo de datos con el que se declaren, no son más que posiciones de memoria. Solo partiendo de esta afirmación se podrán entender los punteros. Y es que cualquier tipo de dato, en el lenguaje que sea, necesita tener un espacio lógico dentro la memoria donde almacenarse. Por ejemplo, si se declara una variable de tipo entero, dependiendo de la arquitectura con la que se ejecute el programa, ocupará una cantidad de bytes y tendrá asociada una posición de memoria que será accesible a partir del nombre de la variable. Eso quiere decir que, cuando se llame a la variable dentro del código, en realidad se le estará diciendo que vaya a la posición de memoria vinculada a ese identificador (ese nombre de variable) y lea tantos bytes como ocupa ese tipo de dato. Uno de estos lenguajes es C y es justamente en este punto donde entran en juego los punteros. En este lenguaje existe una implementación muy particular de una referencia: el apuntador.
Lea también: Funcionamiento del RFC en México
Este procedimiento existe, y consiste en pasar punteros como parámetros formales. La forma más evidente de pasar parámetros por referencia es usar un puntero. Cuando se pasa un valor por parámetro a una función, si este es manipulado, no se verá afectado fuera de esta. No obstante, sí que existe una manera de poder conservar de forma correcta una modificación en una función: con los punteros. Cuando una función tiene un asterisco en uno de sus parámetros es que está esperando una dirección de memoria del tipo de dato que tiene declarado. Justamente, al llamar esta función, se ha convertido una variable de tipo entero sin ser puntero, a un puntero de tipo entero. Por lo tanto, el tipo de dato es el esperado y, por el hecho de ser un puntero se accederá a su posición de memoria y el valor será conservado al salir de ella.
Teniendo ya la implementación, resta saber la forma de acceder al dato haciendo uso del apuntador; para esto, C integra un operador conocido como “de desreferencia” *. Con lo cual, hay que acceder a esa posición de memoria para obtener el valor deseado. Para resumirlo, se podría decir que usar el asterisco delante de una variable tipo puntero ya declarada es acceder al valor que contiene, sin embargo, poner un ampersand (&) delante de una variable que no es puntero, es acceder a la dirección de memoria, es decir, proporcionar un dato de tipo puntero. De hecho, no es casual, porque si se piensa conceptualmente, no se estará cambiando el contenido del puntero como tal, puesto que solo es una dirección de memoria y seguirá siendo la misma, sino que se modifica directamente el contenido que hay en esa dirección de memoria. Se debe a que no hicimos cambio alguno en el valor del apuntador que tenemos como parámetro, la asignación fue realizada sobre el campo de la estructura a la que nos estamos refiriendo con ese apuntador.
Y desde luego, tanto las variables estándar como los punteros pasan por valor, esto es, las modificaciones efectuadas en los parámetros (variables estándar o punteros) dentro de la función no afectan a las variables estándar o punteros empleados como parámetros reales. Podría pensarse, entonces, que no se puede modificar el valor de un puntero cuando éste pasa como parámetro, y así es. Nada impide utilizar un puntero de puntero como parámetro de una función, luego dentro de la función se dispone de la dirección del puntero, así que es posible modificar su valor. Bueno, resulta que nuestra referencia, o más específicamente, nuestro apuntador, fue pasado por valor a la función crearNodo(). Esto quiere decir exactamente lo que estás pensando, recibimos una copia de la dirección de memoria y esta se almacena en el parámetro con nombre n. En el interior de nuestra función, desechamos dicha copia y le otorgamos a n un nuevo valor por medio de malloc().
El compilador de C trata las listas monodimensionales como si fuesen punteros del tipo base de la lista en cuestión. A todos los efectos, una lista monodimensional se trata como un puntero cuyo valor es la dirección del primer elemento de la lista. Como las listas equivalen a la dirección de su primer elemento, se admite el parámetro real nombre. Se pide construir una función que proporcione como resultado una estructura a través de un puntero.
Confusiones Comunes sobre Referencias en Otros Lenguajes
Saber identificar referencias se convertirá en una habilidad muy importante que nos permitirá hacer un correcto uso de ellas (y de paso ahorrarnos unas horas de debug). Las otras dos funciones como son almacenarNombre y almacenarApellidos no pasan los parámetros por valor, sino que pasan los parámetros por referencia. Este otro lenguaje es muy parecido a C y, de hecho, también incluye a los apuntadores en su repertorio. La referencia en C++ es una característica específica de este lenguaje y que tiene un comportamiento muy particular; lo que se logra con esto es darle un alias a una variable que ya se encuentra en existencia para, de esta forma, lograr realizar cambios sobre dicha variable por medio de ese alias. Puedes apreciar que, a diferencia de los apuntadores, aquí no es necesario realizar algo en particular sobre la referencia (o alias) para lograr acceder al valor original, dicha referencia puede utilizarse como una variable común y corriente. No obstante, nunca debes perder de vista que estas referencias son diferentes a los apuntadores.
Lea también: Número de letras y números en el RFC
¿Qué podría ser peor que tener un lenguaje de programación que utiliza el término referencia con un significado distinto? Fácil, tener dos lenguajes que utilizan el término referencia con significados totalmente distintos. En Java, los manejos explícitos de memoria no existen, él es quien se encarga de realizar todo el trabajo sucio por ti. Cuando tú instancias objetos en este lenguaje, dichos objetos se crean en algún lugar de la memoria; ellos no son algo que puedas tomar y guardarlo en una variable, necesitas de una entidad que te permita comunicarte con dicho objeto para poder trabajar sobre él. Aunque las referencias de Java son totalmente diferentes de sus homónimas en C++, sí que guardan cierta similitud con los apuntadores. En Java no es posible que sus referencias se comporten como algo que no son.
Como te puedes dar cuenta, un apuntador, una referencia de C++ y una referencia de Java son, en esencia, la misma cosa. Los tres son meras implementaciones del concepto de referencia que definimos en un principio, es decir, son valores que se refieren a otro valor. Si tú pasas cualquiera de estas referencias a una función, no puedes estar equivocado, es un paso por referencia. Sin embargo, un último reto nos espera y radica en reconocer que, dependiendo del tipo de referencia que se trate, esta también podría ser pasada por valor o por referencia.
Resulta que con la idea que todos teníamos sobre el paso por referencia, es como llegaron a surgir todos los problemas y confusiones que aquejan a varios programadores. En ocasiones, mantener dicha idea nos lleva a generar “excepciones” en nuestras interpretaciones para tratar de evitar caer en alguna contradicción. Si ahora volvemos a la definición que hemos trabajado con tanto empeño en esta nota, verás que ambas versiones de la función ya descrita caen, sin ningún problema, en el modo de “paso por referencia”. ¿Qué es lo que falta? Conocer el funcionamiento de la implementación de la referencia que estemos utilizando. En un principio puede parecer poca cosa pero, en esta ocasión, te has dado cuenta cuán importante es conocer la definición de algún término dependiendo del contexto en el que este se encuentre. Casos como este aparecerán en multitud de lugares y por eso es importante que investigues bien los significados de acuerdo al área donde te encuentres.
La Paradoja de Pasar Punteros por Valor
Si utilizas todo lo que hemos visto hasta ahora, deberías llegar a la conclusión de que se están utilizando ambos [paso por valor y por referencia]. ¿Cómo es eso? Bueno, si hablas del nodo que se está pasando, este está siendo pasado por referencia, la referencia en cuestión es la dirección de memoria del nodo. Se ha comentado que son los punteros los que permiten mantener una modificación sobre su contenido. En resumen, los punteros son una herramienta muy potente que incluyen lenguajes como C. Sin embargo, es muy necesario entender lo que realmente representan estos.
