Hace tiempo estuve programando unas cosillas en el lenguaje C. En eso que estuve programando había una serie de estructuras de datos, y había hecho funciones que reservaban espacio en memoria para esas estructuras, las inicializaban y devolvían el espacio recién inicializado. O sea, que tenía funciones similares a esta:
struct datos *datos_new(void) {
struct datos *ret = malloc(sizeof(struct datos));
ret->duracion = 0;
}
Compilé este código y lo probé, y funcionó. Sin embargo, unas horas más tarde se me ocurrió hacer que el compilador avisara de todos los problemas que encontrara, y salió este error:
datos.c: In function 'datos_new':
datos.c:10: warning: control reaches end of non-void function
Este error significa que, aunque había declarado que la función devolvía un valor (un puntero a una posición de memoria), no le había añadido la orden para hacerlo. Sin embargo, a pesar de tener este error, el código funcionaba. ¿Cómo era posible?
Esto es fácil de explicar, aunque para hacerlo tengo que explicar brevemente cómo se compila un programa.
Yo le proporciono ese código en C a un compilador de C, y este compilador transforma mi programa en una serie de instrucciones de código máquina. Estas instrucciones son las que entiende el ordenador directamente.
Hay una serie de convenciones obedecidas por todos los compiladores para un lenguaje, sistema operativo y ordenador dado. Estas convenciones gobiernan, por ejemplo, cómo se organizan los datos y cómo se hacen las llamadas a función.
Las convenciones para los compiladores de C para Linux en un PC dicen que para llamar a una función, primero hay que introducir sus argumentos en una zona de la memoria llamada “pila”; luego se pasa el control a la función mediante la orden “CALL”, y al volver la función, el resultado tiene que estar en el registro EAX.
(Los registros de la CPU son zonas de memoria extremadamente pequeñas situadas dentro de la CPU y que son las que se utilizan para operar, en lugar de utilizar la memoria del sistema. Los datos se copian de memoria a los registros, se opera y el resultado queda en los registros. Luego se lo puede copiar de los registros a la memoria o dejarlo en los registros para usarlo en posteriores operaciones).
Sabiendo esto, es fácil saber por qué la función “datos_new” devolvía la posición de memoria a pesar de no tener una instrucción “return” que devolviera este valor: después de llamar a la función “malloc”, el resultado de esta llamada estaba en EAX. Como no se modificaba después el contenido del registro EAX, este valor seguía ahí al volver de la función “datos_new”. Por lo tanto, la función “datos_new” devolvía el resultado de “malloc”, que era el valor correcto.
Y por eso mi función, a pesar de tener un error gordo, funcionaba.
Aún así, al final le puse una instrucción “return” para asegurarme :-)