Las funciones son trozos de código que se reutiliza, esto para evitar escribir lo mismo muchas veces. Antes para evitar tener que escribir lo mismo muchas veces nos las ingeniamos con instrucciones de saltos . Pero al solo ser un salto, dificulta hacer que el resultado o procedimiento cambie siempre que se necesite, como la función suma, si solo hace una suma (2+2) siempre retorna 4. Ahora imagina que quisiéramos hacer otra suma, tendríamos que modificar el código que hay en esa instrucción y se vuelve muy poco práctico, para solucionar esto se usa un espacio de memoria llamado pila para almacenar parámetro y llamar a la función, ahora podemos usar dirección de memoria en lugar de números estaticos, logrando que se pueda hacer la suma con cualquier número que se encuentre en la pila. Estos saltos que son usados en un lenguaje Ensamblador (Assambler en ingles). El Assembler es un lenguaje de bajo nivel que usa mucho los saltos y por ello una simple operación se volvia compleja de leer. Llego el lenguaje "C" basado en "B" con instrucciones que busca evitar esta complejidad, claro en código de maquina siguen siendo las mismas instrucciones. Por estas instrucciones el lenguaje de programación C se le conoce como lenguaje de programación de nivel medio (realmente la mayoría lo conoce como bajo nivel, pero aquí está el esamblador).
Nota: No me quise centrar en la historia de C en este tutorial, pero quiero mostrar cómo son las funciones realmente (no son más que saltos, que cuando termina la función salta a la instrucción siguiente de donde se llamó. Ejemplo ). Pero si te interesa saber más de C aquí te dejo unas lecturas:
- Una breve historia de C - Prof. Bolaños D https://www.bolanosdj.com.ar/MOVIL/BrevehistoriadellenguajeC.pdf
- Historia del lenguaje C - www.Studocu.com
Estructuras de una función
La estructura es simple, solo debe seguir lo siguiente: int nombre_function();. Donde "int" es el retorno y puede ser cualquier tipo de dato, el "nombre_funcion" es el nombre que se usará para llamar a la función, y por ultimo el "()" que indica que es una función y puede tener parámetros (variables usadas para pasar datos por la función) separados por comas.
// https://github.com/dabl03/C-tutorial-code/tree/main/cap-3 #include <stdio.h> // Prototipos de función (los veremos más a delante): void msg(); void line_break(); /*Esta es la estructura de una función que retorna algo y tiene parámetros: tipo_de_dato_de_retorno nombre_de_funcion(tipo_de_paratmetro param1,int param2,...){ return tipo_de_dato_de_retorno;// Retornamos el dato que queremos retornar } */ int main(int argc, char** argv){ // Mostramos el mensaje de bienvenida. msg(); line_break(); printf("La cantidad de parámetros que se pasó a la función main es: %d.\n",argc); printf("La ubicación del programa es: %s.\n",argv[0]); line_break(); puts("Si quieres escribe algo y después presione enter para terminar"); const char key_firts=getchar();// Obtenemos un carácter de lo que escribiste en la consola. line_break(); printf("El primer carácter que escribiste es: %c\nTu programa ha terminado...\n",key_firts); return 0;// Retornamos 0. Ahora quien llama a la función main podrá hacer: int valor_main=main(); } // Función que no retorna nada y no tiene parámetros: void msg(){ puts("----------------------------------"); puts(" Tutorial C "); puts(" Cap 3 - FUNCIONES "); puts("----------------------------------"); puts(" - Ejemplo 1."); } void line_break(){ puts("\n\n");// 3 saltos de líneas. (Recuerda que puts agrega un salto de línea al final). }
En el código anterior se puede ver algunas observaciones:
-
Main: Main es una función que es llamada por la función
_start
(En windows se llama a WinMain). Otra cosa que vimos son los siguientes parámetros:
- int argc - Este argumento indica la cantidad de parámetros pasados por la consola al llamar a esta aplicación.
- char** argv - Este es un array de cadenas que contiene todos los argumentos pasados al programa.
Ejepmlo:
echo "Para windows:" gcc -o function.exe function.c function.exe "Segundo parámetro que se le pasa al programa" "Tercer parámetro, con este argc valdrá 3" "El primer parámetro será la ubicación del programa - Ahora argc vale 4" echo "Para Linux:" gcc -o function function.c function "Arroz" "Arepa" "./my-database.db" "Lo anterior es un ejemplo de que se puede pasar" ElNota: En anteriores capítulos vimos que main siempre lo escribía conint main(void)
- Significa que main retorna un entero, este entero representa el código de error con que finalizará el programa.void: int main(void){
. Ese void era para indicarle que permita que se pase parámetros por la pila, pero que no serán usados y tampoco necesitamos acceder a ellos. Esto significa que la forma correcta de escribir la función main es:int main(int argc, char** argv){return 0;}
- Declarar una función parece que es igual a declarar una variable, lo unico que cambia es que no se puede usar const y siempre hay que ponerle los paréntesis "( )" seguido de los "{" para el inicio y "}" para el fin de la misma.
- Los parámetros se separan con "," y se declaran como las variables.
- Al declarar la función void le decimos al compilador que no retornará nada. Y si intentas retornar algo dará error, pero se puede usar el operador return para terminarla:
void saludar(){ puts("Hola, ¿cómo estás?. Eres increíble"); return; puts("Esto no saldrá por la consola."); //return 0;// esto daría error. }
Visibilidad de variables.
En C hay un problema, se trata de los nombres usados para definir las funciones "No se pueden repetir". Este problema lo apaciguamos un poco usando nombres creativos y detallados, para que, al importar la biblioteca no haya conflictos con los nombres de las funciones. Por ejemplo la API de windows usa la notación Húngara . También recomiendo usar la anotación CamelCase o nake_case , en lo personal me gusta usar una combinación de ambos estilos.
También para mitigar este problema, el compilador nos da lo llamado la "visibilidad de variable", esto significa que la variable solo será accesible dentro de la función donde se declara (variables locales), pero si está afuera de toda función se podrá usar en todo el programa (Variables globales). Ejemplo:
// https://github.com/dabl03/C-tutorial-code/tree/main/cap-3 #include <stdio.h> // fgets #include <stdlib.h> // malloc, free //El * es indicativo de puntero, pero todavía no enseñaré sobre los punteros. Antes enseñaré los operadores en el siguiente capítulo. char* dato_personas(){ char* sData=malloc(350);// En el capítulo de "punteros y arrays" explicaré sus usos. puts("Ingresa los datos separados con comas(\",\"): Nombre, edad, sexo.\nLos caracteres que estén después de los 350 se ignorarán."); fgets(sData, 350, stdin); printf("0 - Dentro de la función \"dato_personas\" sData vale: %s. Y apunta a: %p.\n-------------------------------\n",sData,sData); return sData; } int main(int argc, char** argv){ //Aquí data no es accesible y no se podrá usar. Ejemplo: //sData=NULL;// Error: Variable no se ha declarado. Elimina el "//" para que veas el error. char* sData;// Ahora si lo declaramos. char* sData_2; printf("1- Por ahora sData vale: %s, la dirección a que apunta es: %p.\n",sData,sData);// Tiene un valor, pero no es el mismo anterior sData_2=dato_personas(); printf("2- Por ahora sData vale: %s. la dirección a que apunta es: %p.\n",sData,sData);// Sigue valiendo lo mismo. printf("3- sData_2 vale: %s. La dirección a que apunta es: %p\n.",sData_2,sData); sData=sData_2; printf("4- Ahora sData vale: %s. la dirección a que apunta es: %p.\n------------------------\n",sData);// Ahora vale lo mismo que sData_2 printf("Los datos pasados son: %s.\n",sData); free(sData);// Liberamos la memoria usada en la cadena. Ya no puedes usar sData ni sData_2, así que lo ponemos en NULL (0): sData=NULL; sData_2=NULL; return 0; }
En el ejemplo anterior, se puede ver las variables sData en diferentes funciones, tienen como valor diferentes direcciones de memoria, esto porque son variables locales. Las variables locales son variables que solo pueden ser accedidas dentro de la función o el bloque de código donde se crean.
También si te atreviste a quitarle el comentario a la línea sData=NULL;, entonces has visto que da error de compilación, esto porque dentro de la función main no existe esa variable, nunca se ha definido, aunque llames a dato_personas la función main tendrá dato_personas. Todo esto porque aunque usemos el mismo nombre el compilador usará diferente memoria para cada variable.
Sabiendo esto dime: ¿Que significa declarar una variable fuera de toda funciones?, ¿Quién podría acceder a ella?.
fgets(sData, 350, stdin);usamos "fgets". Esta función sirve para leer archivos, pero como queremos leer lo que se pase por la consola le pasamos "stdin" que es un puntero al buffer de la consola. Los parámetros de "fgets" son:
char * fgets(char * str_out, int num, FILE * stream);. El "str_out" es donde se guardarán los caracteres leídos del archivo "stream" y "num" es el número de caracteres a leer.
Variables globales.
Las variables globales están fuera de todas las funciones. Al estar fuera de todas las funciones entonces son accesibles para cualquier función, son útiles, pero no es aconsejable usarlas. El problema de las variables globales reside en el hecho que son accesible en todo el código, al serlo se vuelve difícil debugearla. También pueden ocurrir errores inesperados, cuando se trabaja con multiples hilos puede suceder que se modifique o libere la memoria a que apunte la variable, el otro hilo puede leerla antes o después generando un comportamiento impredecible. Recomiendo leer: When is it ok to use a global variable in C? - StackOverflow y Why should we avoid using global variables in C++ - Ankitha Reddy in tutorialspoint.com.
Ejemplos de variables globales:
Dato interesante: Puedes declarar una variable del mismo nombre dentro de la función, pero esta variable no afectará en nada a la global:
Prototipos de función.
Los prototipos de funciones son una declaración de cómo será la estructura que la función va a tener, como qué parámetros va a recibir y qué tipo de dato será su retorno. Esto se hace cuando necesitas que una función sea accesible incluso antes de que se crease. Por ejemplo, este código no sirve:
En el código anterior verás que aunque definas la función, el compilador no la conseguirá al momento de tu llamarla, porque la has definido después, por lo tanto, al llamarla no existe. Al no existir da error de linkeado, pero qué pasaría si la definimos:
Vemos que el compilador ahora si consigue la función y compila sin error. Lo que pasa es que el
linkeador
esperará a que la definas, para guardar ese lugar y poder saltar, y ejecutar la función. Pero ¿qué pasaría si no lo consigue?. Intenta compilar esto y mira el mensaje de error:
Eso es todo, los prototipos son solo para poder llamar a la función antes de implementarla. Te preguntarás ¿por qué hacer esto?, esto se hace cuando tienes un proyecto muy grande y separas el código en diferentes archivos, como es mucho código compilarlo todo puede tardar mucho tiempo. Hay códigos que tardan 30 minutos en compilarse de lo grande que son. Ahora imagínate si tienes un simple error, como que no pusiste un ";" y ahora tienes que volver a esperar los 30 minutos a que se compile todo otra vez, para arreglar un simple error o cambiar una pequeña cosa. La solución que se hizo fue aprovechar qué el código está en diferentes archivos, para compilarlos por separados y después unir todo usando el ld (o linker). Pero ¿cómo puedo usar una función sin definirla?, ¿cómo sabe el compilador si escribí mal el nombre de la función o realmente se define más adelante?, con estos problemas, se empezó a usar los prototipos de función que definen como será la función y poder compilar el archivo sin errores. Al no tener que compilar todos los archivos cada rato, y solo el que tú modificaste, nos ahorra mucho tiempo pudiendo disminuir los 30 minutos a unos minutos o incluso segundos, claro, eso sí, al compilar todo el proyecto por primera vez vas a tardar un rato:).
Si intentas abrir el encabezado "stdio.h" solo conseguirás los prototipos, no las funciones imprementadas. Esto porque la biblioteca estandar normalmente no se modifica y compilar código que nunca se modifica seria desperdiciar tiempo.
Nota: El compilador al ser la biblioteca estandar la linkea automáticamente, si quiere linkear un archivo o una biblioteca de tercero debes hacerlo tu mismo (en este episodio no enseñare como).