011 Programacion en C++
011 Programacion en C++
de estado para el entorno desde el que se ha ejecutado el programa (este valor es considerado el estado de retorno del programa). En UNIX, MS-DOS y MS Windows, los 8 bits ms bajos del valor devuelto son pasados al programa invocante o al intrprete de comandos. Este "estado de retorno" se utiliza en ocasiones para cambiar el curso de un programa, un proceso por lotes o un guin para el ejecutor de comandos. Algunos compiladores pueden generar un aviso de error si se intenta compilar un programa cuya funcin main no devuelva un int. Por contra, algunas plataformas pueden provocar fallos cuando estos programas arrancan o a su terminacin, ya que esperan un int de retorno. En los programas C++ no es necesario devolver nada desde la funcin main, aunque en realidad la definicin de esta sea int main(). La razn es que el Estndar garantiza que si main llega a su final alcanzando el corchete de cierre "}" sin haber encontrado una sentencia return, el compilador debe devolver automticamente un 0 indicando un final correcto, de forma que en ausencia de ningn retorno el compilador proporciona automticamente un return 0; [2]. Nota: no obstante lo anterior, el comportamiento concreto vara de un compilador a otro. Por ejemplo, un programa en el que se alcanza el corchete de cierre de la funcin main sin ninguna sentencia de retorno compila sin dificultad ni aviso de ningn tipo en Borland C++ 5.5, mientras que MS Visual C++ 6.0 avisa: warning C4508: 'main' : function should return a value; 'void' return type assumed. En cualquier caso, es prctica de buena programacin incluir un valor de retorno para la funcin main. Tradicionalmente 0 significa una terminacin correcta del programa, cualquier otro valor es seal de terminacin anormal (error o excepcin). Si el programa termina por una invocacin a la funcin exit, el valor devuelto por main es el argumento pasado a exit ( 1.5.1). Por ejemplo, si el programa contiene la llamada exit(1), el valor devuelto es 1.
4.1 Existen solamente tres valores completamente estndar y portables que puedan utilizarse como retorno para la funcin main, o como argumento para la funcin exit: El clsico entero de valor 0. La constante simblica ( 1.4.1a) EXIT_SUCCESS definida en <stdlib.h> La constante EXIT_FAILURE definida en <stdlib.h>
Si se utiliza cualquiera de los dos valores 0 o EXIT_SUCCESS se garantiza que el compilador los trasladar a un cdigo que sea considerado terminacin correcta por el Sistema Operativo. Si se utiliza EXIT_FAILURE el compilador lo trasladar a un cdigo que ser considerado como terminacin errnea por el Sistema Operativo.
4.2 Algunos sistemas, como Unix, MS-DOS y Windows, truncan el entero pasado a exit o devuelto por main, a un unsigned char, con objeto de utilizarlos en el fichero de rdenes del intrprete de comandos, en un fichero de ejecucin por lotes, o en el proceso que invoc el programa. Algunos programadores utilizan valores positivos para indicar diferentes razones de
fallo, excepcin o finalizacin del programa, pero estos usos no son necesariamente portables ni funcionan en todas las implementaciones. Sin embargo C++ proporciona la posibilidad de devolver valores que son totalmente portables mediante la funcin atexit ( 1.5.1).
2 La sintaxis utilizada para la declaracin de la lista de parmetros formales es similar a la utilizada en la declaracin de cualquier identificador. A continuacin se exponen varios ejemplos: int func(void) {...} // sin parmetros inf func() {...} // dem. int func(T1 t1, T2 t2, T3 t3=1) {...} // tres parmetros simples, // uno con argumento por defecto int func(T1* ptr1, T2& tref) {...} // los argumentos son un puntero y // una referencia. int func(register int i) {...} // Peticin de uso de registro para // argumento (entero) int func(char* str,...) {...} /* Una cadena y cierto nmero de otros argumentos, o un nmero fijo de argumentos de tipos variables */
3 Los argumentos son siempre objetos. Sus tipos pueden ser: escalares; estructuras; uniones, o enumeraciones; clases definidas por el usuario; punteros o referencias a estructuras y uniones, o punteros a funciones, a clases o a matrices. El tipo void est permitido como nico parmetro formal. Significa que la funcin no recibe ningn argumento. Nota: recuerde que cuando coloquialmente se dice que se pasa una matriz como argumento de una funcin, en realidad se est pasando un puntero a su primer elemento ( 4.3.8). Recuerde tambin que que las funciones no pueden ser utilizadas como argumentos, pero s pueden serlo los punteros-a-funcin, lo que a la postre, viene a ser equivalente.
3.1 Es un error realizar una declaracin en la lista de parmetros [2]: int func (int x, class C{...} c) { ... } // Error!!
4 Todos los parmetros de una funcin tienen mbito del bloque de la propia funcin y la misma duracin automtica que la funcin ( 4.1.5).
5 El nico especificador de almacenamiento que se permite es register ( 4.1.8b). En la declaracin de parmetros tambin pueden utilizarse los modificadores volatile ( 4.1.9) y const ( 3.2.1c). Este ltimo se utiliza cuando se pasan argumentos por referencia y queremos garantizar que la funcin no modificar el valor recibido. Ejemplo: int dimension(X x1, const X& x2) // x2 NO se puede modificar!!
6 Argumentos por defecto C++ permite tener valores por defecto para los parmetros. Esto supone que, si no se pasa el parmetro correspondiente, se asume un valor predefinido [1]. La forma de indicarlo es declararlo en el prototipo de la funcin, como se muestra en el ejemplo (ambas expresiones son equivalentes). float mod (float x, float y = 0); float mod (float, float = 0); Ms tarde no es necesario, ni posible (6.1 ), indicarlo de nuevo en la definicin. Por ejemplo:
float mod (float, float = 0); // prototipo ... float mod (float x, float y = 0) { return x + y; } // Error!! float mod (float x, float y) { return x + y; } // definicin Ok Si declaracin y definicin estn en una sola sentencia entonces si es necesario indicar los valores por defecto. Ejemplo, en la sentencia: float mod (float x, float y = 0) { return pow(x*x + y*y, 0.5); } la ausencia de un segundo argumento en la invocacin hace que se adopte para l un valor 0 (podra haber sido cualquier otro valor). En este contexto, la funcin mod aceptara estas dos llamadas como correctas y de resultados equivalentes: m1 = mod (2.0 , 0); m2 = mod (2.0);
6.1 Un argumento por defecto no puede ser repetido o cambiado en una siguiente declaracin dentro del mismo mbito. Por ejemplo: void func (int x = 5); ... void func (int x = 5); { void func (x = 7);
// Error: repeticin de argumento por defecto // nuevo mbito // L.4 Correcto: esta funcin oculta a la
// el mbito anterior vuelve a ser visible // Error: cambiar argumento por defecto
Nota: a pesar de que la expresin de la lnea 4 es correcta, tenga en cuenta que las declaraciones en mbitos anidados que ocultan declaraciones del mismo nombre en mbitos externos, suele ser fuente de errores y confusiones.
Es muy de tener en cuenta esta regla en la definicin de clases, ya que en ellas es frecuente que la declaracin de mtodos se realice en un punto (el cuerpo de la clase), y la definicin se realice "off-line" (fuera del cuerpo de la clase). En caso que el mtodo tenga argumentos por defecto recuerde no repetirlos ms tarde en la definicin.
6.2 La gramtica de C++ exige que los parmetros con valores por defecto deben ser los ltimos en la lista de parmetros, y que si en una ocasin falta algn argumento, los que le siguen tambin deben faltar (adoptar tambin los valores por defecto). Nota: como puede verse, C++ no admite la posibilidad de otros lenguajes de saltarse parmetros por defecto incluyendo una coma sin ningn valor en la posicin correspondiente, por ejemplo: int somefunc (int, int = 1, char, long); .... x = somefunc ( 33, , 'c', 3.14); x = somefunc (int, char*, char* = 0); // Incorrecto!! // Error! No aceptable en C++
Observe que en este ltimo caso, el espacio de separacin entre char* y = es importante, ya que *= es un operador de asignacin ( 4.9.2). As pues: x = somefunc (int, char*, char*= 0); // Error!
6.3 Los argumentos por defecto de mtodos (funciones-miembro de clases ser otros miembros a no ser que sean estticos ( 4.11.7). Ejemplo: class C { int v1; void foo(char, int = v1); }; class B { static int v1; void foo(char, int = v1); }
4.11) no pueden
// Error!!
// Ok.
Un posible diseo de la clase C podra ser: class C { int v1; void foo(char, int = -1); };
// Ok
ms tarde, en la definicin del mtodo hacer: void C::foo(char a, int x) { if (x == -1) x = v1; ... }
6.4 Los argumentos por defecto no pueden ser otros argumentos: x = somefunc (int x, int y = x); // Error!
6.5 Los argumentos pasados por referencia (7.3 ) solo pueden adoptar valores por defecto estticos, globales, o de un subespacio cualificado. Ejemplo: namespace ALPHA { long n = 20.10L; } long n = 10.10L; void f1 (long& lg = ALPHA::n); void f2 (long& lg = n); void f3 (long lg = 10.2L); void f4 (long& lg = 10.2L );
7 Argumentos: por valor y por referencia Existen dos formas de pasar argumentos a las funciones: por valor y por referencia. El primero es utilizado por defecto con la declaracin usual de parmetros. En el paso "por valor", se crean copias de los argumentos pasados a la funcin, los cuales, junto a las variables locales (incluyendo el posible valor devuelto), y la direccin de vuelta a la rutina que efecta la invocacin, son pasados a la pila en la secuencia de llamada. Ms tarde, cuando termina su ejecucin definitivamente, es decir, cuando el control vuelve a la funcin que la invoc, toda esta informacin es sacada de la pila mediante la secuencia de retorno (y se pierde). Estos procesos suponen un consumo de tiempo y espacio (memoria), a veces considerable.
7.1 Paso por valor Hemos visto que el paso de parmetros por valor significa que existen copias de los argumentos formales (estas copias son variables locales de la funcin llamada), y que una funcin no puede alterar ninguna variable de la funcin que la invoc. La nica excepcin es el caso de las matrices. Cuando se utiliza una matriz como argumento en la llamada a una funcin, el valor pasado es un puntero a la direccin de memoria del principio de la matriz ( 4.3.2).
7.1.1 Cuando los argumentos pasan por valor pero no hay concordancia entre el tipo de los argumentos actuales y los argumentos formales utilizados en la declaracin de la funcin, entonces se produce un modelado de tipo antes de la asignacin. Supongamos el ejemplo:
void func(int x) { // definicin de func. Acepta un entero x = x * 2; } float f = 3.14; func(f); // f es promovido a int antes de asignacin a 'x' // x == 6
Lo que sucede en estos casos es que la copia local f en func(x) es modificada para hacerla coincidir con el tipo esperado por la funcin, mientras que el valor original ( f) permanece inalterado.
7.2 Pasar un puntero En C clsico, cuando se desea que la funcin llamada pueda alterar el valor de variables de la funcin que la invoca, o ahorrar el espacio que supone la copia local de los argumentos (que pueden ser estructuras de datos muy grandes), la solucin consista en utilizar punteros a las variables respectivas como argumentos para la funcin (en vez de pasar las variables en s mismas). A su vez, la funcin llamada deba declarar el parmetro como puntero, y acceder a la variable indirectamente a travs de l. En otras palabras: cuando en C se desea que un valor X pase a una funcin F y que esta pueda alterar el valor de X en la funcin que la invoc, el argumento utilizado es &X (la direccin de X). De esta forma, aunqueF recibe una copia de &X, puede alterar el valor original a travs de esta direccin. Esta tcnica puede tener sus ventajas. Por ejemplo, si X es una estructura muy grande, pero puede tener efectos colaterales peligrossimos y ser una fuente de errores difciles de detectar.
7.3 Paso por referencia Por supuesto, C++ permite utilizar la tcnica del C clsico descrita arriba, pero tambin utilizar el paso de argumentos por referencia (en realidad es una variante semntica del proceso anteriormente descrito). Para ello se utiliza el declarador de referencia & ( 4.2.3). Las referencias presentan las ventajas de los punteros, en el sentido que permiten modificar los valores de los objetos pasados como argumento, y de que permiten ahorrar espacio si hay que pasar objetos muy grandes, pero no presentan los peligros potenciales de aquellos. En caso necesario las referencias pueden declararse constantes, indicando as que la funcin invocada no modificar estos valores. En estos casos, la utilizacin de referencias obedece casi exclusivamente a razones de eficacia en el mecanismo de llamada ( 4.2.3). Nota: en ocasiones el paso por referencia tiene una justificacin de tipo fsico. Es el caso en que los objetos utilizados como argumento representan dispositivos fsicos. Por ejemplo, ficheros externos o dispositivos de comunicacin. En estas circunstancias, el objeto no puede ser copiado alegremente por el mecanismo de invocacin de funciones (que utilizara el constructor-copia de la clase) si se utilizaran pasos por valor, y es necesario recurrir al paso por referencia. Otro uso muy comn de las referencias es cuando la funcin debera devolver distintos valores. Por ejemplo, supongamos que nos interesa que una funcin foo, devuelva cuatro valores: dos int posX y posY; una cadena de caracteres char* nombre, y un float altura. Como una funcin no puede devolver ms de un valor (al menos, no de forma
directa), recurrimos a construir una estructura con los valores adecuados. Esta estructura ser pasada a la funcin por referencia, de forma que sus miembros podrn ser modificados desde el cuerpo de la funcin. Una vez realizada la invocacin, puede considerarse que la mencionada estructura contiene los valores "devueltos" [3], aunque en realidad la funcin no devuelva nada (en el ejemplo que sigue devuelve void). El esquema de funcionamiento sera el siguiente: struct ST { int posX; int posY; char* nombre; float altura; }; void foo (ST& st) { ... st.posX = value1; st.posY = value2; st.nombre = value3; st.altura = value4; } int main () { // ========= ... ST st; foo(st); cout << "Las coordenadas son: x = " << st.posX << "; y = " << st.posY << endl; cout << "es el punto << st.nombre << " de altitud << st.altura << " metros" << endl; ... }
7.4 Comparativa A continuacin se muestran tres implementaciones de una misma funcin; cada una con una forma distinta para paso del argumento. Implementacin-1: Sistema clsico, paso "por valor" // n entero; pasa "por valor"
Observe que la ltima sentencia no es un paso por referencia, sino por valor (a pesar de que el argumento actual sea una referencia).
Implementacin-2: Sistema clsico, paso de "punteros por valor" (seudo-referencia) void pru2(int* np) { *np = (*np) * 3; } . . . int x = 4; pru2(&x); // np puntero-a-entero; pasa "por valor"
// ahora x = 12
Observe que en este caso, pasar el valor &x (direccin de x) como argumento, es equivalente a pasar un puntero a dicha variable (que es lo exigido en la definicin de pru2). Es decir, la ltima lnea se puede sustituir por las siguientes: int* ptr = &x pru2(ptr); // define puntero-a-x // pasa el puntero como argumento
Implementacin-3:
void pru3(int& n) { // n tipo "referencia-a-int"; pasa "por referencia" n = 3 * n; } . . . int x = 4; pru3(x); // ahora x = 12 Atencin a la sintaxis: aqu la invocacin a pru3 tiene la forma: pru3(x), no pru3(&x) como en el caso anterior. Es decir, la notificacin de que el argumento pasa por referencia hay que hacerla en la definicin de la funcin, y no es necesario indicarlo en el momento de la invocacin. En este ltimo caso, la declaracin int& n como parmetro de la funcin pru3, establece que este n sea declarado como "referencia-a-entero", de forma que cuando se pasa el argumento x, la funcin crea un valor n que es una especie de alias o espejo de x, de forma que la expresin n = 3*n tiene el mismo efecto que x = 3*x.
7.4.1 Ya hemos visto (Referencias 4.2.3) que cuando, en la declaracin de una referencia, el iniciador es una constante, o un objeto de tipo diferente que el referenciado, se crea un objeto temporal para el que la referencia acta como un alias. Esta creacin de objetos temporales es lo que permite la conversin de tipos referencia-a-tipoX cuando se utilizan como parmetros de funciones y no hay concordancia entre el valor recibido y el esperado (suponiendo que exista posibilidad de conversin). Este sera el mecanismo utilizado en el siguiente caso: void pru(int& n) { // n tipo "referencia-a-int" (pasa "por referencia") n = 3 * n; } . . . float f = 4.1; pru(f); // ahora f = 12
7.4.2 Ejemplo En el programa que sigue se muestra un caso de paso por referencia y acceso a subespacios. #include <iostream.h> namespace ALPHA { class CAlpha { int x; public: int getx(void) { return x; } void putx(int i) { x = i; } }; CAlpha CA1; // instancia de CAlpha (en ALPHA) } int func (ALPHA::CAlpha& ca, int i); /* prototipo: ca es la referencia a un objeto de la clase CAlpha en ALPHA */ int main (void) { // ======================== int x = 0; cout << "x = " << x << endl; ALPHA::CA1.putx(10); // invocacin al mtodo putx de CA1 x = func(ALPHA::CA1, 3); cout << "x = " << x << endl; } int func (ALPHA::CAlpha& ca, int i) { return (i + ca.getx()); } Salida: x = 0 x = 13 // definicin
Recordemos que la sintaxis del lenguaje permite tambin la invocacin de funciones a travs de punteros a funciones ( 4.2.4b) e incluso de referencias ( 4.2.3), aunque esto ltimo sea menos frecuente. Cuando las funciones son miembros de clases la invocacin sigue una sintaxis especial ( 4.11.2e). En estos casos incluso existen operadores especiales para invocarlas a travs de sus punteros ( 4.9.16).
2 Evaluacin de argumentos La gramtica C++ permite utilizar expresiones como argumentos en la invocacin de funciones. Estas expresiones son evaluadas, y sus posibles efectos laterales tienen efecto, antes que la funcin sea cargada en la pila ( 4.4.6b). Sin embargo, tales prcticas son en general desaconsejadas, pues dan lugar a cdigo difcil de leer. Ejemplo: int x foo (int x) { return x+2; } ... int main() { int n = 3; cout << "foo -> " << foo(n++) << endl; cout << "foo -> " << foo(++n) << endl; ... } Salida: foo ->5 foo ->7 Hay que advertir que el orden de evaluacin de los argumentos es indefinido (depende del compilador), por lo que no es recomendable utilizar expresiones que dependan del orden de evaluacin de los parmetros. Ejemplo: int x foo (int x, int y) { return x + y; } ... int main() { int n = 3; cout << "foo -> " << foo(n++, n) << endl; ... } En estas condiciones es imposible predecir si la salida ser 7 u 8.
3 Conversin de argumentos A continuacin de la evaluacin, los valores resultantes de las expresiones son convertidos automticamente a los mismos tipos que los declarados para los parmetros formales. Ejemplo (suponiendo la definicin del caso anterior):
// -> 8
3.1 Cuando no se ha declarado previamente un prototipo de la funcin, C++ realiza una invocacin a la funcin convirtiendo la totalidad de los argumentos segn las reglas de Conversiones aritmticas estndar ( 2.2.5). En cambio, si existe un prototipo de funcin en el mbito, C++ convierte los argumentos al tipo declarado para los parmetros.
3.2 Cuando un prototipo incluye puntos suspensivos ( ...), el compilador convierte todos los argumentos (si los hay) como en cualquier otro caso hasta la elipsis, despus conforma todos los dems parmetros (que han sido declarados variables) segn las reglas usuales para argumentos de funciones que no tienen prototipo.
3.3 Si existe un prototipo, el nmero de argumentos debe coincidir con los declarados (a menos que existan puntos suspensivos). El tipo de los argumentos tambin debe coincidir, pero solo hasta el punto en que una asignacin pudiera realizar legalmente una conversin (del tipo realmente pasado al tipo esperado). Tambin existe el recurso de hacer una conversin explcita ("cast") para convertir un argumento en un tipo que sea aceptable por el prototipo ( 4.9.9 Modelado de tipos).
4 En C++ los parmetros son pasados por valor, lo que significa que existen copias locales de los argumentos formales, estas copias son variables locales de la funcin invocada. Nota: recordemos que cuando se utiliza una matriz como argumento en la invocacin a una funcin, el valor pasado es un puntero a la direccin de memoria del principio de la matriz, pero habida cuenta que el identificador de una matriz puede ser tomado como puntero a su primer elemento ( 4.3.2), tambin es vlida la regla general. El argumento es pasado por valor, y existe una copia local del argumento formal; en este caso la copia de un puntero. Ejemplo: #include <iostream.h> void fun1(char* str) { cout << str << endl; } void fun2(char str[]) { cout << str << endl; } int main() { char ch[10] = "Hola mundo"; fun1(ch); fun2(ch); } Salida: Hola mundo Hola mundo // recibe puntero a carcter // recibe matriz de caracteres
// ======================
Observe que el compilador acepta con total naturalidad que ch es un puntero a carcter en la invocacin func1(ch), y una matriz de caracteres en func2(ch). La razn es que en ambos casos, el compilador supone que ch es un puntero a carcter (en el ejemplo, al primer elemento dech[], una matriz de caracteres), lo que coincidira con la definicin de func1, y que en la definicin de func2, char str[] es tomado comochar* str.
5 Cuando se desea que la funcin llamada pueda alterar el valor de las variables de la funcin que la invoca, los argumentos actuales utilizados son punteros a las variables respectivas en la funcin invocante. A su vez la funcin invocada debe declarar el parmetro como un puntero y acceder a la variable indirectamente a travs de l. En el ejemplo que sigue deseamos que la funcin func pueda alterar el valor de la variable x. Observe que pasamos un puntero a dicha variable, y como func trata el argumento recibido p como tal puntero: { int x = 10; int* ptr = &x; func(ptr); } ... void func (int* p) { *p = 3 * (*p); } // pasamos puntero a x // ahora x = 30 // p es declarado como puntero // y tratado como tal puntero!!
6 Recordar que C++ tambin permite pasar los argumentos por referencia (Paso de argumentos 4.4.5).
7 Ms sobre conversin de parmetros Es posible incluir sentencias de asignacin en la lista de argumentos de invocacin de funciones, aunque sea una prctica desaconsejable, pues da lugar a cdigo difcil de interpretar y en ocasiones sin mucho sentido. Por ejemplo, en C++ es vlido el siguiente cdigo que compila sin problema: #include <iostream.h> void fun (char* p, int n) { for (int i = 1; i <= n ; i++) cout << p << i << endl; } void main() { // ============= char* pt1 = "Hola, que tal!! "; char* pt2; int x; fun(pt2 = pt1, x =5); // desaconsejado !! } Salida: Hola, que tal!! 1 Hola, que tal!! 2 Hola, que tal!! 3
En los siguientes epgrafes se detallan algunos aspectos relativos al tratamiento de Identificadores, la convencin utilizada para el paso de parmetros ( 4.4.6a); el rendimiento ( 4.4.6b) y la recursin ( 4.4.6c)
Los detalles sobre la forma de proceder en cada caso dependen de la plataforma. En la mayora de compiladores es posible fijar ciertas directrices sobre la forma de proceder en estos casos, tanto a nivel global como a nivel particular de algunos identificadores. Los comentarios que siguen se refieren al compilador Borland C++, aunque salvando algunas cuestiones de detalle, pueden hacerse extensivos al resto de plataformas (MS Visucal C++, GNU g++, etc.) Nota: en el caso de palabras que utilizan dos guiones bajos, _ _, se han separado con un espacio para mayor legibilidad (en el cdigo deben aparecer juntos).
2 Tratamiento de identificadores Una caracterstica que distingue a unos compiladores (lenguajes) de otros, es el tratamiento dado a los identificadores (nombres) de los objetos; lo que se conoce como sistema de codificacin de nombres ("name encoding scheme"). De este sistema depende que durante las fases intermedias de la compilacin, los identificadores sean guardados tal como los escribe el programador o sufran mutaciones ms o menos importantes. En BC++, cuando est activada la opcin -u (lo que ocurre por defecto), el compilador guarda todos los identificadores globales en su grafa original (maysculas, minsculas o mixta), aadiendo automticamente un guin bajo "_" ("Underscore" ASCII 95 2.2.1a) delante de cualquieridentificador global, ya sea de funcin (todas lo son), o de variable. Para modificar este
comportamiento se puede utilizar la opcin -u- como parmetro en la lnea de comando del compilador ( 1.4.3). En el caso de identificadores que han sido modificados con el identificador Pascal, no se aade ningn guin, pero el identificador es convertido a maysculas. Ejemplos:
En el fuente int x;
En el mdulo .obj
int _x
Este tratamiento para los identificadores puede ser tambin especificado a nivel individual mediante algunos especificadores (palabras clave que se relacionan ) situadas en la declaracin. Ejemplo: int __cdecl x; int _pascal y; sin embargo, cuando estos especificadores se aplican a funciones, adems de afectar al tratamiento del identificador de la funcin como smbolo de mbito global, suponen tambin una forma concreta en la convencin de llamada utilizada. Ejemplos: int _cdecl y; int _cdecl func1(int); // especificador de variable global // especificador de funcin
2.1 La tabla adjunta resume el efecto de un modificador aplicado a una funcin. Por cada modificador, se muestra el orden en que son colocados en la pila los parmetros de la funcin. Despus se indica si es la funcin que realiza la invocacin ("Caller"), o la funcin llamada ("Called"), la responsable de sacar los parmetros fuera de la pila. Finalmente, se muestra el efecto en el nombre de una funcin global.
__fastcall Izq. a derecha func. invocada se aade '@' como prefijo __pascal __stdcall
Izq. a derecha func. invocada se convierte a Maysculas Derecha a izq. func. invocada Sin cambio
Nota: en C++ __fastcall y __stdcall son siempre nombres "planchados". Recordar que existe una directiva de compilacin extern "C", que se utiliza en la declaracin de funciones para indicar al compilador que el nombre de la funcin no debe ser planchado ( 1.4.2).
3 Convencin de llamada En informtica se han consagrado diversas formas de invocacin de funciones, las ms frecuentes son las siguientes: Rpida ; C ; Pascal ; Registro y Estndar . Estas convenciones se diferencian en: La forma que cada una utiliza para la limpieza de la pila (stack). El orden de paso de parmetros (derecha a izquierda o a la inversa). El uso o no de maysculas y minsculas, y ciertos prefijos en los identificadores globales.
La especificacin de la forma que se utilizar en el programa, puede hacerse a nivel global o solo a nivel particular de algunas funciones especficas. Para indicarlo a nivel global se utiliza alguno de los comandos especficos del compilador (son los indicados en cada caso). En estos casos, las palabras clave: _ _pascal, _ _fastcall, o _ _stdcall pueden utilizarse para declarar que una rutina o funcin utiliza especficamente una convencin distinta de la sealada como general. La forma de indicarlo a nivel particular es mediante el uso de ciertas palabras reservadas para que sea utilizada una forma especfica en lugar de la que tenga asignada el compilador por defecto . Estas palabras deben indicarse en la declaracin o prototipo, y delante del especificador de invocacin de la funcin. Son las siguientes: _ _cdecl, _ _pascal, _ _fastcall, _ _msfastcall y _ _stdcall. Nota: en la programacin Windows estas convenciones de llamada se utilizan a travs de sus propios typedefs. En concreto se utilizan las siguientes equivalencias
Por ejemplo, en un programa C++ normal: __pascal int funcionUno(x); int __pascal funcionUno(x); // Error!! // Ok. // Ok.
... } En un programa C++ para Windows (que incluye la cabecera <windows.h>): int PASCAL functionUno(x); // Ok. int WINAPI WinMain (HINSTANCE hTh, HINSTANCE hPr, LPSTR lps, int nFu) { // Ok. ... }
3.1 Invocacin rpida Esta opcin se ha incluido en el compilador BC++ para compatibilidad con el de Microsoft. Indica al compilador que utilice convencin de llamada de MS VC++ para todas las funciones que no tengan explcitamente declarada otra forma. En esta convencin, las dos primeras DWORD [1] o argumentos ms pequeos pasan a los registros ECX y EDX; todos los dems pasan de derecha a izquierda. La funcin invocada es responsable de desalojar los argumentos de la pila. Formas de indicarlo al compilador: Especificador global: comando de compilacin -pm Especificador particular: especificador _ _msfastcall
3.2 Invocacin C Esta opcin indica al compilador utilizar la secuencia de llamada estndar C para funciones, es decir: generar guiones de subrayado -guiones bajos-; distinguir maysculas de minsculas (no transformar minsculas en maysculas); pasar parmetros de derecha a izquierda. En BC++ se utiliza por defecto esta convencin de llamada. Como indicbamos anteriormente , en este tipo de invocacin, la colocacin de parmetros en la pila se realiza de derecha a izquierda, lo que significa que el ltimo argumento de la funcin es colocado el primero y el primero es colocado el ltimo. Esto hace que estas funciones puedan utilizar un nmero variable de parmetros en cada invocacin. Es decir, no tienen que pasar necesariamente el mismo nmero de parmetros en todas las invocaciones que se realicen a dicho cdigo. Las funciones que gozan de esta particularidad son denominadas "variadic" en la literatura inglesa. La funcin que realiza la llamada ("caller") es la encargada de limpiar la pila, lo que provoca que el cdigo de este tipo de ejecutables sean ligeramente mayores que en el resto de convenciones, en las que es la funcin invocada ("called") la que limpia la pila. Por ejemplo, supongamos que la declaracin de una funcin y su posterior invocacin adoptan la forma sealada en el siguiente esquema:
void __cdecl foo(int x, ...); ... int x; char a; int b; double c; ... foo(x, a, b, c);
En estas condiciones, durante la compilacin, el analizador semntico solo puede verificar la concordancia del primer argumento de la invocacin de foo con el indicado en la declaracin; el resto permanece sin verificar. Posteriormente, durante la invocacin de foo en run-time, el compilador asume que los argumentos c, b, a y x (en este orden) se corresponden con valores situado a partir de cierta distancia ("offset") del principio de la pila. En este caso, con los valores situados en los desplazamientos 64; 96 (64+32); 104 (96+8) y 136 (104+32). Formas de indicarlo al compilador: Especificador global: comandos de compilacin -pc, -p- ( 1.4.1a) Especificador particular: especificadores cdecl, _cdecl y _ _cdecl en la declaracin de la funcin o identificador global (las tres formas son equivalentes).
Ejemplos: int __cdecl FileCount; long __cdecl HisFunc(int x); Nota: Con frecuencia, en las expresiones C/C++ aparecen los especificadores _RTLENTRY y _USERENTRY. Se trata de definesestablecidos en los ficheros de cabecera del compilador. El primero indica la convencin de llamada utilizada por la RTL ("Run Time Library"). El segundo la convencin de llamada que esperan las funciones de la RTL para las funciones compiladas por el usuario ("callbacks"). Generalmente se resuelven en la convencin __cdecl. Un ejemplo paradigmtico de su uso es la funcin qsort de la RTL de C/C++, que realiza la ordenacin de los elementos de una tabla en base a un criterio proporcionado por el usuario en forma de una funcin que devuelve un entero menor, igual o mayor que cero segn que el primero de los elementos comparados sea menor, igual o mayor que el segundo. Esta funcin es utilizada por qsort mediante una retrollamada ("callback"), cada vez que tiene que comparar dos elementos. El prototipo de qsort es como sigue (consulte el manual de su compilador para una explicacin del significado de los argumentos): void qsort(void* base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void*, const void*));
3.3 Invocacin Pascal Esta opcin indica al compilador utilizar la secuencia de llamada de Pascal para las funciones: no generar guiones de subrayado, forzar identificadores a maysculas, limpieza de la pila por la funcin que realiza la llamada y paso de parmetros de izquierda a derecha ( 1.4.1a).
Generalmente las llamadas de funcin resultantes son ms pequeas y rpidas que las generadas con la convencin C , y tambin utilizan la pila para el paso de parmetros. En este caso, las funciones deben pasar el nmero exacto de argumentos y del tipo adecuado. Los identificadores de las funciones declaradas con este modificador estn sujetos al planchado de nombres ( 1.4.2). Nota: en la programacin para entornos Windows 3.x se exiga que las llamadas al Sistema utilizaran esta convencin. Formas de indicarlo al compilador: Especificador global: comando de compilacin -p Especificador particular: especificadores pascal, _pascal o _ _pascal (las tres formas son equivalentes).
3.4 Invocacin Registro Esta opcin indica que se deben generar todas las funciones utilizando la convencin de paso de parmetros a registro. Esto supone que los tres primeros parmetros (de izquierda a derecha) se colocan en los registros EAX, EDX y ECX ( H3.2). En caso que se trate de objetos que no quepan en los registros, por ejemplo, si son nmeros fraccionarios o estructuras, no se utilizan los registros, sino la pila como es usual. Las funciones declaradas con los identificadores _cdecl o _pascal no pueden utilizar simultneamente este especificador, porque ambas utilizan la pila para el paso de parmetros. Los identificadores de estas funciones estn sujetos al planchado de nombres ( 1.4.2), adems el compilador les aade una arroba '@' como prefijo. Las formas de solicitar esta convencin son: Especificador global: comando de compilacin -pr Especificador particular: especificadores _fastcall y _ _fastcall (ambas formas son equivalentes).
3.5 Invocacin estndar Esta opcin indica al compilador que debe utilizar la secuencia de llamada estndar para funciones: no generar guiones bajos; conservar maysculas y minsculas; la funcin invocada limpia la pila, y colocacin de parmetros de derecha a izquierda. A diferencia de la invocacin C, esta convencin exige pasar el nmero y tipo de argumentos exacto. Estas funciones cumplen con la convencin de argumentos de Win32, y estn sujetas a planchado de nombres ( 1.4.2). Las formas de indicarlo al compilador son: Especificador global: comando de compilacin -ps
Ejemplo: #define WINAPI __stdcall #include <windows.h> int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { MessageBox (NULL, TEXT ("Hola Windows 98"), TEXT ("Saludo"), 0); return 0; } Dado que en la programacin Windows, la funcin WinMain es equivalente a la main de C/C++, la sentencia anterior constituye el inicio de cualquier aplicacin Windows. Su sintaxis seala explcitamente al compilador que debe utilizar para ella la convencin de llamada estndar.
2 Carga y descarga de funciones Es importante conocer que C y C++ son lenguajes orientados a pila y estructurados alrededor del concepto de funcin; el funcionamiento de ambos est ntimamente relacionado. Desde el punto de vista del programador, la invocacin de una funcin es una sentencia del tipo: func1(); aunque finalmente aparece como una llamada a la direccin donde se encuentra el recurso correspondiente. En ensamblador sera algo as: call 0x4000000
En realidad, lo que ocurre en las tripas de la mquina cuando se invoca una funcin es un proceso bastante complejo, ya que la invocacin, ejecucin y retorno de una funcin no es solo cuestin de
algoritmo. Tambin hay datos de entrada bajo la forma de los argumentos "pasados" a la funcin (en base a los cuales el cdigo realizar cierta computacin 4.4.5) y datos de salida en forma del valor "devuelto". Nota: observe que si el la llamada a funciones solo interviniese el cdigo, el mecanismo de invocacin quedara reducido a un salto "jump" al punto de entrada del nuevo bloque de cdigo. El asunto es que, aparte de seguir el camino ("path") de ejecucin adecuado, el programa necesita preparar el entorno de ejecucin (datos) para el nuevo trozo de ejecutable y apuntar l mismo cierta informacin que le permita volver al punto de partida. Para entender el mecanismo, es imprescindible desterrar la idea del argumentos "pasados" o valores "devueltos" como datos que van y vienen desde/hacia la funcin invocante a/desde la funcin invocada. La anterior es una imagen didctica, diramos de "alto nivel" y adecuada para una explicacin bsica de los mecanismos de invocacin de funciones. Pero como decamos al principio, C++ es en ciertos aspectos un lenguaje de bajo nivel (pegado a la mquina) y si se quiere entender y manejar con xito (en especial si se utilizan programas con mdulos compilados en otros lenguajes), es fundamental una mirada ms cercana al proceso. En realidad no existe en absoluto algo como "paso" de argumentos (ni por valor ni por referencia). Realmente los datos (los argumentos actuales) de la funcin invocante se copian [6] a una zona de memoria que se crea ex-profeso en la pila ( 1.3.2), denominada marco de activacin o marco de pila ("Stack frame"). Nota: por extensin tambin se denomina a veces registro de activacin (aunque no debera decirse as). La razn es que la direccin de comienzo de esta zona queda almacenada un registro del procesador denominado por esta razn puntero de pila o SP ("Stack pointer"). Ver figura . Como veremos a continuacin, el marco de pila es un trozo de memoria en este rea, que sirve como zona temporal de datos para uso de la funcin que entra en ejecucin. En ella se almacenan los argumentos pasados a la funcin, sus variables locales y otros datos, como direccin de retorno a la rutina que efectu la llamada, y estado de los registros en el momento de la invocacin. Nota: en determinadas circunstancias ciertos argumentos pueden pasar a los registros del procesador, aunque no hay reglas fijas al respecto. Como en todo lo que en C++ se relaciona con el uso de los registros (register 4.1.8b), depende de las circunstancias y del procesador. Ver nota adjunta sobre uso de registros para paso de parmetros en BC++ ( 4.4.6bw1).
Por supuesto, toda esta informacin tiene que ser colocada en la pila cada vez que se produce la llamada a una funcin. El proceso de construir el marco de pila es lo que se denomina secuencia de llamada. A su vez, cuando termina su ejecucin definitivamente y se devuelve el control a la funcin que la invoc, la informacin debe ser sacada de la pila [5]. El proceso de desmontar el marco de pila se conoce como secuencia de retorno. Ambos procesos consumen su tiempo, a veces bastante.
Las secuencias de llamada y retorno son realizadas por unos trozos especiales de cdigo denominados prlogo y eplogo que incluye por su cuenta el compilador junto con cada invocacin a funcin. Aunque en ocasiones esto puede evitarse; son las denominadas funciones desnudas ( 4.4.1b). Recuerde que la secuencia de llamada implica la creacin de todas las variables locales de la funcin (incluyendo los posibles valores que sern devueltos por esta), as como la invocacin del constructor-copia para todos los argumentos que no han sido pasados por referencia. Por su parte, en la secuencia de retorno se destruyen todas las variables colocadas en la pila (automticas) invocando los destructores correspondientes y sus valores se pierden. Recordemos tambin que los valores estticos que hubiese en la funcin tienen espacio de almacenamiento independiente y pueden conservarse. Conviene resaltar que en el proceso de invocacin de una funcin solo intervienen dos actores: la funcin que realiza la invocacin ("Caller") y la funcin que es invocada ("Called"); nosotros las denominamos funcin invocante y funcin invocada (o llamada). Entrambas tienen que repartirse el trabajo de las secuencias de llamada y retorno. El asunto de "quin" hace "qu" es una de las diferencias entre las distintas convenciones de llamada que hemos visto en el captulo anterior ( 4.4.6a). Normalmente en C++ es la funcin invocante la encargada de limpiar la pila y de colocar all los parmetros. Esto es precisamente lo que hace posible el uso de funciones con nmero variable de parmetros, ya que en tiempo de compilacin, la funcin invocada no sabe cuantos argumentos recibir.
3 El marco de pila La figura 1 muestra la forma de ocupacin de la pila cuando es invocada una nueva funcin y se crea el correspondiente marco de activacin. Observe que la pila crece "hacia abajo", es decir, desde posiciones altas de memoria hacia posiciones ms bajas. La funcin invocante ("Caller") ocupa una primera zona con copia de los
argumentos pasados a la funcin invocada. Generalmente esta copia se realiza empezando por el ltimo y terminando por el primero (derecha-izquierda). Por ejemplo, en la invocacin: func(a, b, c); Los argumentos seran pasados en el orden c, b, a. En la figura su colocacin sera de abajo a arriba a partir del punto comienzo del nuevo registro de activacin. Nota: el paso de argumentos incluye naturalmente su evaluacin previa (recuerde que la sintaxis C/C++ permite utilizar argumentos que son el resultado de una expresin). En lo que respecta al orden en que son pasados, y evaluados, los argumentos, aunque la convencin derecha-izquierda es la usual en plataformas Intel, puede variar en otras, por lo que en orden a la portabilidad, y como regla de buena prctica, NO se deben hacerse suposiciones relativas al orden en que sern evaluados los argumentos en las funciones C/C++, y mucho menos, utilizar argumentos cuyo valor dependa de este orden. A continuacin se incluye informacin sobre el estado de la mquina (valores de los registros). Esta informacin ser utilizada ms tarde en el proceso de restaurar la ejecucin de la funcin invocante, de forma que la ejecucin siga en la instruccin siguiente a la invocacin. A continuacin se sita un valor denominado enlace dinmico o de control. Se trata de un puntero que seala al enlace dinmico del registro de activacin anterior, que a su vez seala al precedente (as para toda la secuencia de funciones invocadas en un momento dado). Evidentemente la funcin anterior es la invocante, y en caso de llamadas recursivas (cuando la funcin se invoca a s misma), es la activacin previa de la misma funcin. Cuando se inspecciona el estado de la pila con un depurador, la primera funcin que se encuentra es main; sobre ella, la cadena de funciones invocadas que en ese momento no han tenido retorno. La direccin del enlace dinmico est contenida en un registro del procesador denominado puntero base BP ("Base Pointer" H.3.2). Esta direccin es importante porque en el proceso de enlazado desaparecen todos los nombres de variables. En el fichero objeto los nombres son sustituidos por las direcciones de almacenamiento correspondientes, y en el caso de las variables locales dinmicas de las funciones, incluyendo las copias de los argumentos, estas posiciones estn expresadas como desplazamientos en bytes ("Offset") a partir de esta posicin (de ah el nombre de puntero "base"). Por ejemplo: para traer un valor cuyo desplazamiento es 8 de la pila al acumulador (registro AX), al procesador le basta una sola instruccin que en ensamblador puede tener el siguiente aspecto: ... mov ...
ax,[bp+8]
A partir de la posicin del enlace dinmico se sita un rea, rellenada por la funcin invocada ("Called"), que contiene todas las variables locales dinmicas de la nueva funcin (las variables estticas disponen de su propio espacio de almacenamiento ( 1.3.2). A continuacin viene un rea denominada de variables temporales que contiene datos auxiliares del compilador.
4 Sustitucin inline
Ocurre con frecuencia, sobre todo en la invocacin a funciones pequeas, que el costo de las secuencias de llamada y retorno suponen mucho ms que el costo de memoria necesario para el cuerpo de la propia funcin que se invoca. De hecho, C++ dispone de un especificador de tipo de almacenamiento (que solo es aplicable en la definicin de funciones), especialmente concebido para atender este problema. Se trata de la directiva inline (palabra-clave 3.2.1). Durante la fase de enlazado ( 1.4), en cada punto del cdigo donde aparece la invocacin a una funcin, se coloca una direccin que seala la situacin del recurso correspondiente (el cdigo compilado de la misma), pero mediante la directiva inline, se indica al compilador que en vez del comportamiento habitual, sustituya esta direccin por una copia del cdigo de la funcin, lo que se denomina expansin inline. Resulta evidente que de esta forma se eliminan las secuencias de llamada y retorno, lo que se traduce en una ejecucin mucho ms rpida. La contrapartida es que el tamao del ejecutable resultante es mayor, ya que existe ms de una copia de la funcin; tantas como sustituciones inline se hayan efectuado. Adems, el artificio presenta algunos inconvenientes que sern comentados a continuacin.
La declaracin de una funcin como sustituible inline, se realiza en el sitio de su definicin, y tiene la forma general: inline <tipo_dev> <funcin> (<parmetros>) {<sentencias>;} En cualquier sitio donde el cdigo encuentre una invocacin a <funcin>, el compilador sustituir la invocacin por el cdigo contenido en<sentencias>, incluyendo la creacin de las variables locales pertinentes. Por ejemplo [2]: inline float mod (float x, float y) { return sqrt(x*x + y*y); } inline char* cat_func(void) { return char*; } Nota: algunos compiladores exigen que la definicin inline se realice antes que cualquier invocacin a la funcin.
En ocasiones, el compilador puede hacer caso omiso de la indicacin inline; se trata de la misma situacin que con las peticiones register ( 4.1.8b), es decir, un mandato no imperativo para el compilador. En cambio, otras veces el compilador supone una sustitucin inline aunque no se indique explcitamente. Es el caso de las denominadas funciones inline, mtodos cuya declaracin y definicin se realizan dentro del cuerpo de la clase ( 4.11.2a), o el de determinadas invocaciones a funciones muy pequeas incluidas en el cuerpo de otras. En estos casos, el mecanismo de optimizacin del compilador pueden decidir que su cdigo sea incluido en el cuerpo de la funcin invocante [1]. En cualquier caso, las correspondientes directivas de compilacin, suelen permitir al programador bastante control al respecto de este tipo de sustituciones, incluyendo posturas intermedias y extremas. Por ejemplo, con objeto de facilitar la depuracin del programa, es posible indicar al compilador que provisionalmente no realice este tipo de sustituciones . En otros casos, se le puede ordenar que utilice su criterio para establecer que funciones pequeas son merecedoras de la sustitucin inline [3]. Finalmente, cabe la opcin de dejar la sustitucin exclusivamente a criterio del programador [4].
5 Casos especiales 5.1 Las funciones con especificador de excepcin ( sustitucin inline. 1.6.4), no son susceptibles de
5.2 Evidentemente, estas funciones no son susceptibles de recursin (invocarse a s mismas) por lo que generalmente el compilador ignora la directiva inline en estos casos. Nota: los compiladores de Microsoft permiten la substitucin inline si la profundidad de recursin puede ser deducida en tiempo de compilacin, y siempre que esta profundidad no sobrepase un lmite previamente especificado por el programador.
532 Dependiendo de su estructura interna, algunas funciones no son susceptibles de este tipo de sustitucin. Por ejemplo, en el compilador Borland C++ 5.5 no pueden ser sustituidas inline las funciones que contengan alguna sentencia de iteracin while; do... while y for ( 4.10.3). Algunos compiladores rehsan efectuar la substitucin si el cuerpo de la funcin es muy grande, y tampoco realizan la sustitucin en algunos casos en que la invocacin de la funcin se realiza mediante punteros.
5.4 Las funciones que acepten algn parmetro que sea del tipo "clase con un destructor", no pueden ser objeto de expansin inline. Sin embargo, esta restriccin no es aplicable si se trata de un paso por referencia. En el primer caso el compilador lanza un mensaje de aviso anunciando que la directiva inline no ser tenida en cuenta. Ejemplo: struct est { ... est(); // Constructor por defecto ~est(); // Destructor }; inline void f1(est& e) { /* ... */ } inline void f2(est e) { /* ... */ } La definicin de f1 compilar sin problema, ya que el parmetro es una clase con destructor, pero pasa por referencia. En la compilacin de f2se producir un mensaje de aviso: Functions taking class-by-value argument(s) are not expanded inline in function f2(est).
5.5 Cualquier funcin que devuelva una clase con destructor no puede ser objeto de expansin inline, cuando dentro de la expresin de retorno puedan existir variables u objetos temporales que necesiten ser destruidos. Ejemplos: struct est { est(); // constructor por defecto ~est(); // destructor
}; inline est f1() { return est(); } inline est f2() { est e2; return est(); }
// Ok:
// Aviso:
Esta funcin no puede ser sustituida, porque el objeto e2 necesita ser destruido, en consecuencia, se genera un aviso del compilador:Functions containing some return statements are not expanded inline in function f2(). En esta otra: inline est f3() { // Aviso: No sustituible inline return ( est(), est() ); } se genera un mensaje de aviso anlogo al anterior. Tampoco puede ser sustituida porque el valor devuelto contiene objetos temporales.
6 Criterio de uso En aras a la velocidad de ejecucin, es preferible evitar en lo posible la utilizacin de funciones pequeas, especialmente en bucles que se repiten un gran nmero de veces. En caso de tener que utilizarlas es preferible acudir a la sustitucin inline. Tambin son buenas candidatas a esta sustitucin las funciones-operador ( 4.9.18). Aunque lo anterior supone ir contra dos reglas generales de la buena programacin: la reutilizacin del cdigo, y la compartimentacin de datos y procedimientos. En este sentido, la sustitucin inline supone una situacin intermedia; sin las desventajas de la llamada y retorno a funcin, pero (desde el punto de vista del programador), con las ventajas de la utilizacin de funciones en cuanto suponen el encapsulamiento del cdigo en un nico sitio. La opcin a elegir en cada caso (funcin tradicional o sustitucin inline), es como siempre una cuestin definir prioridades entre el cronmetro y el tamao del cdigo resultante. Nota: a menos que el compilador permita otro tipo de medida al respecto , cuando sea importante reducir el tamao del cdigo, debe recordar definir las funciones miembro fuera del cuerpo de la definicin de la clase, para evitar la sustitucin inline antes aludida, que en estos casos es realizada automticamente por el compilador.
Cuando se trata de optimizar funciones que no son escritas por el programador. Por ejemplo, cuando se utilizan los recursos de la Librera Estndar, es conveniente recordar que los modernos compiladores traen algunas de estas libreras en dos formatos: como funciones y comomacros, y el programador puede optar entre una y otra forma 8 .
Puesto que en estas sustituciones el compilador reemplaza la llamada a funcin por "su versin" del cdigo de la misma, an cuando en la compilacin se hayan incluido las opciones de depuracin ( 1.4), no existe una correspondencia entre las lneas de cdigo del fuente y el ejecutable, lo que hace difcil la depuracin de este tipo de funciones. Para evitar estas dificultades, los compiladores ofrecen una serie de opciones con las que se pueden controlar diversos aspectos de la construccin del ejecutable en las versiones de depuracin. Nota: para facilitar la depuracin de estas funciones, algunos compiladores ignoran la directiva inline cuando se compila en modo "debug". En el caso concreto del compilador BC++ se ofrece el siguiente abanico de posibilidades de compilacin ( 1.4.3): -v -v-vi -viOpciones de depuracin ON; expansin inline OFF Opciones de depuracin OFF; expansin inline ON Expansin inline ON Expansin inline OFF (las funciones inline son expendidas fuera de lnea)
El compilador GNU Cpp ofrece la opcin -fno-default-inline, que hace que las funciones miembro no sean consideradas inline por el mero hecho de haber sido declaradas dentro del cuerpo de la clase.
8 Macro-sustitucin La macro-sustitucin, a la que hemos hecho referencia en el prrafo anterior, es una tcnica similar a la sustitucin inline que ha sido ampliamente utilizada (como herencia del C), pero que no debe ser confundida con esta ltima. Su utilizacin ha cado bastante en desuso y est desaconsejado, dado que presenta algunos inconvenientes que se detallan en el captulo correspondiente (#define 4.9.10b). Se basa en la utilizacin del preprocesador C/C++ para simular la invocacin de funciones que no son tales. Ejemplo: #define abs (x) (x < 0? (-x) > x) ... func f(x) { int y = abs(x); ... } Es frecuente que algunas rutinas de librera, que adoptan la forma de funciones pequeas, puedan venir en formas dos formatos: como funcin y como macro, pudiendo optar el programador entre una y otra forma. Esta ltima (las macros) viene a ser el equivalente de la sustitucin inline para tales funciones de librera. Ver "Funciones y macros" ( 5.1) para una clarificacin sobre esta cuestin.
4.4.6c Recursin
1 Sinopsis
Se dice que una funcin es recursiva cuando se llama a s misma. Es clsico el ejemplo de clculo del factorial de un nmero mediante el uso de este tipo de funciones: int factorial (int x) { return ( x > 1) ? x * factorial(x-1): 1; }
2 Cada invocacin sucesiva de estas funciones crea un nuevo juego de todas las variables automticas, con independencia de cuales sean sus valores en el conjunto previo. En cambio existe una sola copia de las variables estticas, que es compartida por todas las instancias de la funcin. Esta circunstancia debe ser tenida en cuenta, cuando dichas variables sean utilizadas en funciones que puedan llamarse a s mismas.
3 La recursin ocupa espacio en la pila, porque se debe guardar una copia de cada juego de variables y tampoco es muy rpida, pero tiene la ventaja de que resulta un cdigo fcil de interpretar. Es especialmente adecuada para estructuras que se definen recursivamente (algunos miembros de estas estructuras son punteros que referencian a objetos del mismo tipo que la que se est definiendo), como rboles y listas enlazadas (las hemos denominado estructuras autoreferenciadas 4.5.8).
4 Hay que prestar atencin a que en algunos casos de funciones recursivas [1] es difcil de manejar el valor devuelto. Como ejemplo tomemos la funcin bsqueda de un valor ( key) en un rbol binario ( 4.5.8-3) que reproducimos aqu. Esta funcin aparentemente devuelve un puntero al nodo buscado, o un nulo si fracasa la bsqueda. struct base* busca(char key, struct base* ptr) { if (key == ptr->let) return ptr; if (key < ptr->let) { if (ptr->izq == NULL) return NULL; busca(key, ptr->izq); } if (key > ptr->let) { if (ptr->der == NULL) return NULL; busca(key, ptr->der); } return NULL; } // buscar key // Ok. encuentro!
El problema aqu, es que la funcin busca en el rbol de forma recursiva, pero es imposible saber "a priori" cuantas invocaciones anidadas se producirn. Adems, ninguna funcin aprovecha el valor devuelto por la anterior, por lo que salvo la primera, cuyo retorno si puede ser utilizado por la funcin invocante, todos los dems valores de retorno se pierden (una funcin puede devolver un valor, pero es potestad de la funcin invocante aprovecharlo o no 4.4.7). En este contexto no tiene sentido manejar directamente el valor devuelto por busca tal como se ha presentado. Por ejemplo, sera intil el siguiente trozo de cdigo para saber si la bsqueda ha tenido xito:
La situacin puede esquematizarse en la Fig.1. Cada nueva recursin recibe parmetros de la anterior, pero esta no utiliza el valor devuelto. La funcin usuaria (que hace la primera invocacin a busca) siempre recibir el resultado de la primera invocacin, de modo que si el resultado se ha producido a partir de la segunda, el valor que recibe la funcin usuaria es irrelevante.
Para resolver el problema puede utilizarse una variable global que sea modificada por la instancia que realiza el hallazgo, o un sistema ms refinado. Por ejemplo el manejador de excepciones, que como se ha indicado ( 1.6), puede utilizarse como mecanismo de return o breakmultinivel, y adems pasar un valor desde el origen hasta el punto de captura. En el primer caso, la funcin modificada podra tener el siguiente diseo: void busca(char key, struct base* ptr) { if (key == ptr->let){gptr = ptr; return; } if (key < ptr->let) { if (ptr->izq == NULL) return; busca(key, ptr->izq); } if (key > ptr->let) { if (ptr->der == NULL) return; busca(key, ptr->der); } return; } // buscar key // Ok. encuentro!
El cambio consiste en que la funcin devuelve siempre void, pero cuando se produce el encuentro modifica adecuadamente una constante globalgptr (esta variable podra ser inicializada a NULL antes de la primera invocacin a busca). En el epgrafe dedicado a la bsqueda en un rbol binario ( 4.5.8-3), se exponen sendos ejemplos concretos de lo anteriormente expuesto: una versin utilizando una variable global, y otra utilizando el manejador de excepciones. Otro ejemplo de funcin recursiva: obtener el equivalente hexadecimal de un valor expresado en decimal ( 9.5)
programa (entorno del sistema operativo), pero tngase en cuenta que aunque una funcin devuelva un valor, es potestad de la funcin invocante recibirlo o ignorarlo. Un caso especial es cuando una funcin no devuelve nada [4]. Esto se seala en su definicin y en su prototipo con la indicacin void. Ejemplo: void somefunc ( ...) { ...; } En otros lenguajes las funciones que no devuelven nada reciben el nombre de procedimientos ("Procedures"). As pues, los procedimientos seran un caso particular de las funciones C++. Nota: como regla de buena programacin C++, una funcin debe devolver siempre un valor al entorno de procedencia, aunque solo sea una indicacin de que ha terminado sin problema o con error. Como se ha sealado, en todos los dems casos la funcin devuelve un valor de cualquier tipo, excepto una matriz o una funcin. tipoX somefunc ( ...) { ... return <expresion>; // el resultado de <expresion> es promovido a tipoX } Nota: lo anterior es una verdad a medias (en C++ casi todo tiene alguna excepcin o caso especial). Existe un tipo de funciones que no devuelve nada, ni siguiera void, son los constructores y destructores ( 4.11.2d). Por otra parte, los compiladores Borland C++ y MS Visual C++ disponen de mecanismos para avisar que una funcin no tiene retorno ( 4.4.1b).
1.1 Recuerde que el operador de referencia no puede ser aplicado al valor devuelto por una funcin ( 4.9.11). Ejemplo: x = &func(x); // Error!!
La razn (ya sealada al hablar de los punteros 4.2), est en que no es posible obtener la direccin de una variable de registro, y el valor devuelto es de este tipo. Sin embargo, lo anterior no es bice para que una funcin pueda devolver una referencia .
1.2 Salvo cuando devuelve una referencia, el valor devuelto por una funcin es un Rvalue, que no puede ser utilizado como Lvalue. Por ejemplo: int func(); ... x = func(); func() = x; Sin embargo:
// Ok. // Error!
// Ok. // Ok.
2 La sentencia return Esta instruccin tiene un doble uso: definir el punto en que se devuelve el control de ejecucin al punto de llamada y en su caso, devolver un valor. 2.1 Sintaxis return [<valor-devuelto>] ; <valor-devuelto> es opcional; puede ser cualquier expresin que pueda reducirse a un valor del tipo a devolver por la funcin. Una funcin puede tener ms de un punto de retorno (situacin que debe ser evitada en la medida de lo posible), pero debe devolver el mismo tipo de dato en todos ellos [2]. Es ilegal incluir una declaracin en la expresin <valor-devuelto>: return int x = 10; // Error!!
En muchos casos el valor devuelto es a su vez el resultado de otra invocacin a funcin: int somefunc() { ... return foo(); } En este caso, el valor devuelto por foo es promovido a int antes de ser devuelto a su vez por somefunc. En caso de que la conversin no fuese posible, se generara un error de compilacin.
2.2 Si le ejecucin de una funcin llega al cierre del bloque principal } (externo) sin haber encontrado un return, el control vuelve a la funcin que la invoc, es decir: la sentencia return no es estrictamente necesaria en ninguna funcin, a menos que se desee devolver un valor, o devolver el control a la funcin invocante antes del final del cuerpo que se ejecuta. Si la funcin invocante espera un valor y la funcin llamada llega a su fin sin haber encontrado un return adecuado, el valor recibido es basura. Nota: existe una excepcin, la funcin main, en la que si se alcanza el final sin haber encontrado un return, el compilador proporciona automticamente un return int adecuado, ya que por definicin, esta funcin devuelve int ( 4.4.4) En el ejemplo siguiente se utiliza la sentencia return solo para devolver el control antes de alcanzar el final.
void valor (int x) { if (x == 0) return ; cout << "Valor = " << x << endl; return; // este return es innecesario }
2.3 Observe que cuando la funcin no devuelve ningn valor (formalmente decimos que devuelve void), la instruccin de retorno es simplementereturn, sin aadirle ningn valor: void func (...) { ... return ; return void; return (void)0; return void(0); }
// // // //
correcto Error! correcto (se trata de un modelado de tipo) correcto (variacin sintctica del anterior)
2.4 Aunque coloquialmente hemos dicho que la funcin que devuelve void "no devuelve nada", el resultado de una invocacin a este tipo de funcin es equivalente a "no-valor", por lo que puede ser utilizada como <valor-devuelto> en la expresin return de este tipo de funciones (que deban devolver void), es decir: void func1 (int * ptr); ... void func2 (int * ptr) { ... return func1(ptr); }
// Ok. correcto
2.5 Devolver un valor depende no solo de que se incluya la expresin return (<expresin>) en el cuerpo de la funcin; tambin que haya sido previsto tal extremo en el prototipo y en la definicin de la funcin. Recurdese que en ausencia de otra especificacin, toda funcin devuelve int, en consecuencia, las dos definiciones que siguen son equivalentes [3]: int sum (int x, int y) { return x+y; } sum (int x, int y) { return x+y; }
2.6 El valor devuelto, sealado por return <expresin>, es convertido (promovido) al tipo sealado en la definicin (y en el prototipo) de la funcin antes que se produzca el retorno. Como ejemplo consideremos el siguiente caso: func (double dob) { return dob+3; }
La funcin debe devolver un int (valor por defecto), pero devuelve un double. Como consecuencia, debe realizarse una conversin de tipo con prdida de informacin, por lo que el compilador puede mostrar un mensaje de aviso. Para evitarlo, puede forzarse explcitamente una conversin ("casting") de tipo ( 4.9.9), con lo que el compilador no sealar ninguna advertencia.
func (double
3 Devolver por referencia Cuando el tipo de valor devuelto por la funcin es una referencia ( 4.2.3) se dice que la funcin devuelve "por referencia". En estos casos debe avisarse de esta circunstancia al compilador mediante la utilizacin del declarador de referencia & despus del indicador del tipo de valor devuelto. La sintaxis es la siguiente: <tipo-devuelto>& nombre-funcion (<tipo> <parametro>, ... ) Ejemplo: int & funcion (int x) { .... return z; }
Observe que no es necesario poner return &z; basta la indicacin relativa al tipo de valor devuelto (int &). En los ejemplos y explicaciones que siguen aprender que las devoluciones por referencia pueden ser muy problemticas, y que los valores devueltos son del tipo correspondiente, lo que permite utilizarlos como tales. Por ejemplo: int & func(int x) { ... return y; } ... int& z = func(5); // Ok el valor devuelto es referencia-a-int Recuerde tambin lo indicado en 1.1 relativo a que no es posible utilizar el operador de referencia con el valor devuelto por la funcin. Es decir, en el caso anterior no est permitida una expresin como [7]: z = & func(5); // Error !!
3.1 Un ejemplo: int& max (int a, int b) { if (a >= b) return a; return b; } La definicin anterior tiene un serio problema: que en cualquier caso, la funcin devuelve una referencia a una variable local, y es sabido ( 4.4.6b) que cuando la funcin termina, sus variables locales son sacadas de la pila y destruidas, sin embargo, supuestamente la funcin invocante
todava podra estar utilizndolas [6]. Por esta razn, cuando se intenta una definicin de este tipo, el compilador devuelve un mensaje de error:Attempting to return a reference to local variable ....
3.2 Est permitido devolver una referencia a una constante local a la funcin [1], aunque el resultado no est garantizado y depende del compilador; algo como el ejemplo que sigue funciona en BC++ (por supuesto, no es equivalente al ejemplo 3.1 anterior): const int& max (int a, int b) { const int kte = 100; if (a >= b) a = kte; else b = kte; return kte; }
3.3 Es totalmente correcto devolver una referencia a una variable esttica, aunque sea local a la funcin, ya que permanecer an cuando la funcin sea terminada [5]. Por ejemplo: int& max (int a, int b) { static int mx = 100 if (a >= b) mx = a; else mx = b; return mx; }
3.4 Aunque puedan ser admitidas con determinados compiladores y en determinadas circunstancias, las situaciones anteriores pueden ser calificadas como de "verdadera chapuza". Utilizar referencias devueltas por funciones es siempre problemtico si el origen de la referencia est en el interior de la funcin invocada. Un caso distinto es cuando el origen est fuera de la funcin (la referencia devuelta pas a su vez a la funcin por referencia 3.5 ) o es un valor externo conocido por la funcin 3.8 . Incluso situaciones aparentemente correctas, como 3.5 , pueden resultar incontroladas en determinadas circunstancias. Por ejemplo, si existen varias invocaciones en la misma sentencia: if ( max(a, b) > 5 && max(c, d) > 10) { /* ... */ } o si la funcin es invocada recursivamente: int& max (int a, int b) { static int mx = 100 if (a >= b) mx = a; else mx = max(a, 10*b); return mx; } En estas condiciones es preferible devolver los objetos "por valor", de forma que se reciba una copia del valor devuelto. Cuando esto no es posible, se puede recurrir a crear el objeto a devolver en la zona de almacenamiento persistente (montn ( 1.3.2), aunque es necesario recordar su // 3.4a
destruccin posterior. En cualquier caso estas situaciones son propensas a errores, tienen muy difcil solucin y deben evitarse. 3.5 Cuando se devuelve una referencia, lo ms frecuente es que sea justamente uno de los argumentos, que a su vez ha pasado por referencia ( 4.4.5), con lo que no habra problema, pues el valor devuelto referencia a un objeto fuera de la funcin. El ejemplo 3.1 se compilara sin problemas aadiendo esta pequea modificacin que afecta a la definicin de los parmetros: int& max (int& a, int& b) { if (a >= b) return a; return b; }
3.6 En este contexto, una llamada a la funcin en la forma: max(x, y); proporciona una referencia (alias) al mayor de los dos valores que pasan como argumento, en consecuencia es posible la utilizacin siguiente: #include <iostream.h> int& max (int& a, int& b) {if (a >= b) return a; return b; } int main () { // ================== int x =12, y = 22; cout << "Mximo: " << max(x, y) << endl; cout << "Valor inicial: " << y << endl; int& mx = max(x, y); // M.4: Ok asignacin del mismo tipo mx = 30; // M.5: cout << "Valor final: " << y << endl; } Salida: Mximo: 22 Valor inicial: 22 Valor final: 30 Comentario: Como era de esperar la primera salida nos muestra el mayor valor entre x e y. En M.4 la asignacin es correcta ya que mx es tipo referencia-a-int, igual que el valor devuelto por la funcin; a partir de este momento mx es sinnimo de la variable y (de mayor valor). Por su parte la asignacin en M.5 equivale a y = 30; lo que queda reflejado en la tercera salida. 3.7 El ejemplo se podra sofisticar un poco ms combinando M.4 y M.5.9 en una sola sentencia: #include <iostream.h> int& max (int& a, int& b) { return a >= b? a: b; }
int main () { // ================== int x =12, y = 22; cout << "Mximo: " << max(x, y) << endl; cout << "Valor inicial: " << y << endl; max(x, y) = 30; // M.4bis: Ok asignacin del mismo tipo cout << "Valor final: " << y << endl; } La salida es idntica al caso anterior. La asignacin en M.4bis, con la funcin a la izquierda (como un Lvalue), puede parecer un poco sorpresiva, pero perfectamente lgica si sustituimos mentalmente max(z, k) por una referencia a la mayor de las dos variables x e y. Esta capacidad de las referencias, comportarse como un Rvalue cuando estn a la derecha de una asignacin (L.8 ), y como un Lvalue cuando estn a la izquierda (M.4bis), incluso cuando el valor es el resultado de una invocacin a funcin, es exclusiva y nica de las referencias, y la razn ltima de su introduccin en el lenguaje, ya que este comportamiento no puede ser simulado mediante punteros y es requerido en circunstancias muy especiales. 3.8 Otra circunstancia en que pueden utilizarse referencias devueltas por funciones es cuando el valor devuelto es conocido por la funcin. #include <iostream.h> class Vector { int x, y; // private por defecto public: void setX(int x) { this->x = x; } int getX() { return x;} int& Y() { return y; } Vector() { x = 0; y = 0; } // constructor por defecto }; void main() { // ============= Vector v; int y = v.Y(); // M.2 cout << "Valores iniciales\n" << "x = " << v.getX() << "\n" << "y = " << y << "\n"; // modificar miembros v.setX(10); v.Y() = 20; // M.5 cout << "Valores modificados\n" << "x = " << v.getX() << "\n" << "y = " << v.Y() << "\n"; } Salida: Valores iniciales x = 0 y = 0
Valores modificados x = 10 y = 20 Comentario El ejemplo muestra dos procedimientos para manejar las propiedades privadas x e y de la clase. El primero utiliza dos mtodos pblicos, getX ysetX, para manejar el componente x. En el segundo se utiliza una sola funcin Y, capaz de obtener el valor de la variable y, y al mismo tiempo modificarla. Puede verse como el valor devuelto por esta funcin, una referencia a la propiedad y, puede ser utilizado indistintamente como Rvalue de una asignacin (M.2) y como Lvalue (M.5). Observe que la referencia devuelta por Y es una propiedad de la clase que no pertenece al cuerpo de la funcin ni le ha sido pasada por referencia. En realidad se aprovecha la circunstancia de que las propiedades de la clase son accesible desde sus mtodos ( 4.11.2e).
4.5 Estructuras
1 Sinopsis Las estructuras C++ son una herencia de C; existen (se implementaron) por compatibilidad hacia atrs con el cdigo del C clsico, pero en el contexto de C++ no tienen demasiado sentido ya que las clases ( 4.11), un concepto mucho ms general, las engloban, dejndolas arrinconadas como un caso particular de un mundo mucho ms rico y potente [1]. Nota: de hecho, las estructuras C++ son una especie de hbrido entre las estructuras C tradicionales (de las que son un superconjunto) y las clases C++, de las que son una variedad con propiedades muy especficas. Por ejemplo, todos los miembros son pblicos ( 4.5a1). Realmente el programador C++ no debera necesitar nunca utilizar estructuras como tales, ya que pueden ser sustituidas por clases definidas con ciertas peculiaridades. En concreto, las estructuras C pueden ser consideradas como un tipo especial de clases que no tienen mtodos; el acceso por defecto es pblico, y para la clase base tambin es pblico por defecto. Sin embargo, puesto que las clases C++ disponen de tres tipos de modificadores de acceso: pblico, privado y protegido, el considerarlas como tales, permite un mayor control de acceso de las estructuras que el ofrecido por el C clsico. De hecho, el programador que quiera mentalmente sustituir la palabra struct por class (sabiendo que es un tipo particular de clase), no necesita realmente estudiar este captulo, siempre naturalmente que conozca a fondo el de las clases.
2 Existen circunstancias en que las estructuras son especialmente importantes y tiles, ya sean consideradas en el sentido clsico de C, o en su versin como clases. Constituyen un medio excelente para agrupar un conjunto de elementos heterogneos y manejarlos como un todo, facilitando as el manejo de datos complejos. Por ejemplo, las estructuras pueden ser pasadas como argumentos a funciones o devueltas por estas. Tambin cuando se trata de almacenar y recuperar registros de datos en medios externos (disco por ejemplo). En estos casos son especialmente idneas para escribir y recuperar informacin heterognea en una sola operacin de lectura/escritura.
3 Como tales casos particulares de clases, las estructuras son tipos nuevos definidos por el usuario (nuevos en el sentido de que no estn predefinidos en el lenguaje), que se engloban dentro de la categora general de tipos derivados. Representan conjuntos de miembros que pueden ser a su vez de tipo bsico ( 2.2.1) o derivado ( 2.2.2) -con ciertas restricciones que se sealarn ms tarde-. Adems tambin pueden ser miembros los campos de bits (este tipo no puede aparecer en ninguna otra parte 4.5.9). En otros lenguajes se les denomina registros (records); sus componentes se denominan "miembros", aunque en ocasiones tambin se suelen llamar "campos". Aparte de estos mecanismos opcionales de acceso y de las diferencias que se irn sealando, la explicacin que sigue sobre la sintaxis y uso de estructuras es vlida tanto para C como para C++.
2 Definir la clase En contra de lo que ocurre con el resto de los tipos, que estn pre-definidos en el lenguaje. Por ejemplo, al declarar char ch; ya se sabe exactamente que cosa es ch, en este caso hay que definir previamente el tipo. Los tipos estructura se declaran mediante la palabra clave struct. Sera algo as como: struct struct Punto; Punt2 {int x; int y; int z; };
La primera sentencia es una declaracin incompleta (ver ms abajo); la segunda es una definicin completa de una nueva clase tipo structdenominado Punt2; tiene tres componentes perfectamente definidos (tres int: x, y, z respectivamente). Como puede verse, los componentes se determinan de forma anloga a los parmetros de las funciones, determinando tipo y nombre. Aqu es imprescindible el punto y coma ";" para separar los miembros (sin olvidar poner otro despus del ltimo).
2.1 Ya hemos sealado que en C++ las estructuras son un tipo de clases; entonces, si nos referimos a la terminologa de la POO diramos que en esta fase estamos definiendo la clase. El conjunto de declaraciones dentro de los corchetes {...; ...; ...; } declara los nombres y tipos de sus miembros. Los miembros pueden ser de cualquier tipo con una excepcin: 2.2 Un miembro no puede ser la estructura que se declara porque dara lugar a una declaracin circular (lo definido est dentro de su definicin). Ejemplo: struct mystr { mystr s }; // Ilegal
2.3 Uno, o varios, de los miembros puede ser un puntero a la estructura que se est declarando. Ejemplo: struct mystr { mystr *ps } // Ok: Correcto
Nota: esta interesantsima posibilidad constituye la base de estructuras auto referenciadas; una tcnica de programacin que tiene amplias posibilidades de aplicacin. Por ejemplo, listas enlazadas y rboles ( 4.5.8).
2.4 Tambin es posible que los miembros de una estructura sean a su vez estructuras previamente definidas, dando lugar a estructuras anidadas. Por ejemplo: struct Punto { int x; int y; }; struct Linea { struct Punto p1; struct Punto p2; } c1; declara Linea como un tipo struct con dos miembros, cada uno de los cuales es un tipo struct Punto. Nota: en C, un miembro no puede ser del tipo "funcin devolviendo...". Es decir, las funciones no pueden ser miembros de la estructuras C (de lo contrario seran clases). En cambio, s estn admitidos los "punteros a funcin devolviendo..." (ya que son variables, no mtodos). Las estructuras C++ s pueden incluir funciones miembro (de hecho son clases), adems, en C++ la palabra struct puede omitirse.
2.5 Es importante tener en cuenta que, en este punto (definicin), solo est permitido sealar tipo y nombre de los miembros, sin que se pueda efectuar ninguna asignacin; ni an en el caso de que se trate de una constante [1]. Por ejemplo, las definiciones que siguen seran ilegales, pues todas implican una asignacin en la definicin del segundo miembro: struct struct struct struct Str Str Str Str {int {int {int {int x; x; x; x; int y = 2; }; const int y = 3; }; char c = 'X'; }; char * st = "Hola"; }; // // // // Error! dem dem dem
struct struct
// dem // dem
3 Crear un objeto (instanciar la clase) Un segundo paso es declarar una variable como perteneciente al nuevo tipo. Del mismo modo que para declarar una variable ch como de tipo chardeclarbamos: char ch;, en este caso, para declarar una variable st como estructura tipo punto se utiliza: struct Punto pt; // C y C++
En C++ no es necesario sealar que Punto es una estructura (suponemos que ya lo sabe el compilador por las sentencias anteriores), de forma que si no existe ninguna otra variable punto en el mismo mbito de nombres, no hay ambigedad y se puede poner directamente: Punto pt; // C++
Usando la terminologa de la POO diramos que estamos instanciando la clase, es decir, creando un objeto concreto pt, con espacio en memoria, que pertenece a (es derivado de) dicha clase. Dichos objetos s pueden ser asignados con valores concretos en sus miembros. 3.1 Advertir que cada declaracin de (tipo de) estructura introduce un nuevo tipo, distinto de los dems (una nueva clase), de forma que las declaraciones: struct StA { int i,j; } a, a1; struct StB { int i,j; } b; definen dos tipos de estructura distintas: StA y StB; los objetos a y b1 son del tipo StA, pero a y b son de tipo distinto. Desde la ptica de la POO la frase anterior se enunciara como sigue: "definen dos clases distintas, StA y StB; los objetos a y b1 son instancias de StA, pero b lo es de la clase StB. Por tanto, a y b son objetos de tipo distinto.
3.2 De la misma forma que ocurre con el resto de variables, puede declararse ms de un elemento en la misma sentencia: struct Punto p1, p2, p3,... pn; sin embargo, de una variable de tipo estructura no es posible derivar nuevas variables, es decir no es lcita la sentencia que sigue (utilizando la terminologa de C++ lo enunciaramos diciendo: de un objeto no puede instanciarse otro objeto).
// NO!!
4 Iniciar el objeto Un tercer paso es inicializar dicha variable. Del mismo modo que para iniciar ch se realizaba una asignacin del tipo: ch = 'x', en este caso se utiliza la asignacin (anloga a la de matrices): st = { 12, 25, 3}; Los pasos 2 y 3 se pueden realizar en la misma sentencia. Por ejemplo: struct punto st = { 12, 25, 3}; Tambin puede realizarse los tres pasos en una misma sentencia: struct punto { int x; int y; intz; } p1 = { 12, 25, 3};
Una vez definido el nuevo tipo, pueden declararse punteros y matrices de estructuras de dicho tipo. Por ejemplo: struct stA { ... }; // define la estructura tipo stA struct stA st, *pst, arst[10]; La segunda lnea declara que st es una estructura de tipo stA; que pst es un puntero a dicho tipo, y que arst es un array de 10 estructuras tipostA.
4.1 Como se ver a continuacin , es posible incluso declarar estructuras sin asignar un nombre al tipo correspondiente. Por ejemplo: struct { ... } st, *pst, arst[10];
5 Espacio de nombres de estructuras Los nombres de tipos de estructura comparten el espacio de nombres con las uniones y enumeraciones (las enumeraciones dentro de una estructura estn en un espacio de nombres diferente). Ver L.4 y L.11 en el ejemplo. Los nombres de estructura estn en un espacio de nombres diferente que el de nombres de tipos de estructura y nombres de etiqueta (en C++ solo si no tienen un constructor). Ver L.8 y L.18 del ejemplo que sigue . Los miembros de estructuras disponen de un espacio de nombres cerrado y particular (espacio de nombres de miembros de clases), de forma que sus nombres se pueden repetir con los de miembros de otras estructuras o con los de otros espacios (ver L.6, L.7, L.12 y L.16 del ejemplo ).
La consecuencia es que dentro del mismo mbito, los nombres de estructuras y uniones deben ser nicos (ambos comparten el espacio de nombres de clases), aunque no hay inconveniente que compartan los nombres con los miembros de los otros tres espacios: El de etiquetas; el de miembros (de clases) y el de enumeraciones [3]. ( 4.1.11 Espacion de Nombres y 4.11.4 Ambito de nombres de clase). 5.1 Ejemplo goto s; ... s: struct s { int s; float s; } s;
// // // // // // // // // //
L.3 Etiqueta L.4 OK: nombres de tipo_de_estructura y etiqueta en espacios diferentes L.6 OK: nombres miembro de estructura en espacio privado L.7 ILEGAL: nombre de miembro duplicado L.8 OK: nombre_de_estructura en espacio diferente de nombre de etiqueta (L.3) y de tipo_de_estructura (L4) En C++, esto solo es posible si s no tiene un constructor L.11 ILEGAL: nombre duplicado con el de tipo s (L.4) L.12 OK: nuevo espacio de miembros
// L.14 OK: espacio diferente que el de miembros (ver L.8) // L.16 OK: nuevo espacio de miembros // L.18 ILEGAL: nombre duplicado con el de estructura s (L8)
6 Declaraciones incompletas Las declaraciones incompletas, conocidas tambin como declaraciones anticipadas o adelantadas; consisten en que un puntero a una estructura tipoA puede aparecer en la declaracin de otra, tipoB antes que tipoA haya sido declarada. Ejemplo: struct A; // declaracin anticipada struct B { struct A *pa }; struct A { struct B *pb };
En este caso, la primera declaracin de A se denomina incompleta o anticipada, porque no existe definicin para ella (cuales son sus miembros). Este tipo de declaracin sera correcta aqu, porque en la declaracin de B no se necesita conocer el tamao de A (solo el de un puntero a estructura). Ms sobre declaraciones adelantadas de clases en 4.11.4.
7 Hay que advertir que en C++ es muy frecuente utilizar typedefs en la declaracin de estructuras. De hecho, los ficheros de cabecera de los compiladores C++ estn repletos de ellos. Es muy frecuente que utilicen expresiones como [4]:
typedef struct { unsigned char *curp; unsigned char *buffer; int level; int bsize; unsigned short istemp; unsigned short flags; wchar_t hold; char fd; unsigned char token; } FILE;
// // // // // // // // // //
Current active pointer Data transfer buffer fill/empty level of buffer Buffer size Temporary file indicator File status flags Ungetc char if no buffer File descriptor Used for validity checking This is the FILE object
Por tanto, es posible escribir sentencias como: #include <stdio.h> int main(void) { FILE *in, *out; ... // define punteros a estructuras FILE
Sin embargo, dentro del mismo mbito de nombres, C++ no permite que una estructura (o clase) tenga el mismo nombre que un typedef declarado para definir un tipo diferente. Ejemplo: typedef void (*C)(); ... class C { // Error declaracin mltiple para C. ... };
8 Tipos annimos El nombre de un tipo de estructura puede omitirse, tenindose lo que se llama un tipo annimo (sin nombre). Pueden utilizarse en declaraciones de componentes separados por comas cuando se quiere indicar que los componentes son (o derivan de) un tipo de estructura determinado. Presenta el problema de que al no poderse referir ms al tipo (por no tener nombre), no pueden declararse ms objetos adicionales en ningn otro sitio. Ejemplo: struct { ..; ..; ..; } s, *ps, arrs[10]; // tipo sin nombre
Es posible crear un typedef al mismo tiempo que se declara una estructura, con o sin nombre, como se ve en los ejemplos. Generalmente no se necesitan un typedef y un nombre al mismo tiempo, ya que cualquiera de ellos sirve para las declaraciones. typedef struct mystruct { ..;..; } MST; MST s, *ps, arrs[10]; // igual que struct mystruct s, etc. typedef struct { ..; ..; } YST; // sin nombre YST y, *yp, arry[20];
Cuando son miembros de clases, las estructuras y uniones annimas son ignoradas durante la inicializacin.
Nota: el ANSI C++ solamente permite estructuras annimas que declaren un objeto. Por su parte, el ANSI C no permite estructuras annimas, y el compilador C++ de Borland permite varios tipos de estructuras annimas que extienden el estndar ANSI ( 4.11.3a).
8.1 Debido a que no hay propiedades de instancia, la sintaxis C++ para las estructuras annimas no permite referenciar un puntero this ( 4.11.6). Por consiguiente, mientras que una estructura C++ puede tener funciones miembro, las estructuras annimas C++ no pueden tenerlas. Los miembros de las estructuras annimas pueden ser accedidos directamente en el mbito en que las estructuras han sido declaradas sin la necesidad de utilizar la sintaxis x.y o p->y. Por ejemplo: struct my_struct { int x; struct { int i; }; inline int func1(int y) { return y + i + x; } } S; int main() { S.x = 6; S.i = 4; int y = S.func1(3); printf(y is %d, y); return 0; }