Pensar en C++ V1 - 2
Pensar en C++ V1 - 2
Pensar en C++ V1 - 2
Bruce Eckel
Puede encontrar la versin actualizada de este libro e informacin adicional sobre el proyecto de traduccin en http://arco.inf-cr.uclm.es/~david.villa/pensarC++.html
ndice de contenido
1 Introduccin a los Objetos 1.1 1.2 1.3 1.4 1.5 El progreso de abstraccin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cada objeto tiene una interfaz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La implementacin oculta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reutilizar la implementacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia: reutilizacin de interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.1 1.6 1.7 1.8 1.9 Relaciones es-un vs. es-como-un . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 32 34 34 35 37 38 41 41 42 43 43 44 46 47 48 48 48 49 49 50 50 51 51 52 52 52 53 53 53 53 53 3
Objetos intercambiables gracias al polimorsmo . . . . . . . . . . . . . . . . . . . . . . . . . . . Creacin y destruccin de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gestin de excepciones: tratamiento de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . Anlisis y diseo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.9.1 Fase 0: Hacer un plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.9.1.1 1.9.2 1.9.3 Declaracin de objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Fase 1: Qu estamos haciendo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 2: Cmo podemos construirlo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.9.3.1 1.9.3.2 Las cinco etapas del diseo de objetos . . . . . . . . . . . . . . . . . . . . . . Directrices para desarrollo de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Fase 4: Iterar los casos de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fase 5: Evolucin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los planes valen la pena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.10 Programacin Extrema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.1 Escriba primero las pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.2 Programacin en parejas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11 Porqu triunfa C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.1 Un C mejor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.2 Usted ya est en la curva de aprendizaje . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.3 Eciencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.4 Los sistemas son ms fciles de expresar y entender . . . . . . . . . . . . . . . . . . . . 1.11.5 Aprovechamiento mximo con libreras . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.6 Reutilizacin de cdigo fuente con plantillas . . . . . . . . . . . . . . . . . . . . . . . . 1.11.7 Manejo de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11.8 Programar a lo grande . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ndice de contenido 1.12 Estrategias de transicin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1 Directrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1.1 Entrenamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1.2 Proyectos de bajo riesgo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1.3 Modelar desde el xito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1.4 Use libreras de clases existentes . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1.5 No reescriba en C++ cdigo que ya existe . . . . . . . . . . . . . . . . . . . . 1.12.2 Obstculos de la gestin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.2.1 Costes iniciales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.2.2 Cuestiones de rendimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.2.3 Errores comunes de diseo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.13 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Construir y usar objetos 2.1 El proceso de traduccin del lenguaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 2.1.2 2.1.3 Intrpretes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compiladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El proceso de compilacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.3.1 2.2 Comprobacin esttica de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . 54 54 54 54 54 54 54 55 55 55 56 56 57 57 57 58 58 59 59 59 60 60 60 61 62 62 63 63 64 64 64 65 65 65 66 67 67 68 68 69 69 70
Herramientas para compilacin modular . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Declaraciones vs deniciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1.1 2.2.1.2 2.2.1.3 2.2.1.4 2.2.1.5 2.2.1.6 2.2.2 2.2.3 Sintaxis de declaracin de funciones . . . . . . . . . . . . . . . . . . . . . . . Una puntualizacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Denicin de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sintaxis de declaracin de variables . . . . . . . . . . . . . . . . . . . . . . . . Incluir cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formato de inclusin del estndar C++ . . . . . . . . . . . . . . . . . . . . . .
Enlazado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Uso de libreras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.3.1 2.2.3.2 2.2.3.3 Cmo busca el enlazador una librera . . . . . . . . . . . . . . . . . . . . . . . Aadidos ocultos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Uso de libreras C plano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3
Su primer programa en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 Uso de las clases iostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Espacios de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fundamentos de la estructura de los programa . . . . . . . . . . . . . . . . . . . . . . . . Hello, World! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilizar el compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4
Ms sobre iostreams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 2.4.2 2.4.3 Concatenar vectores de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Leer de la entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Llamar a otros programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5 4
ndice de contenido 2.6 2.7 2.8 2.9 3 Lectura y escritura de cheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduccin a los vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 72 75 76 77 77 78 79 79 80 80 80 81 82 82 83 84 85 86 87 87 87 88 88 89 90 91 93 95 96 97 98
C en C++ 3.1 Creacin de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 3.1.2 3.1.3 3.2 Valores de retorno de las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Uso de funciones de libreras C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creacin de libreras propias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Control de ujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 Verdadero y falso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . if-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Las pasabras reservadas break y continue . . . . . . . . . . . . . . . . . . . . . . . switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Uso y maluso de goto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3
3.4
Introduccin a los tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.4.7 Tipos predenidos bsicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . booleano, verdadero y falso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Especicadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduccin a punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modicar objetos externos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduccin a las referencias de C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . Punteros y Referencias como modicadores . . . . . . . . . . . . . . . . . . . . . . . .
3.5
3.6
Especicar la ubicacin del espacio de almacenamiento . . . . . . . . . . . . . . . . . . . . . . 100 3.6.1 3.6.2 Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 3.6.2.1 3.6.3 3.6.4 Variables registro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.6.5
3.6.6
Volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 5
ndice de contenido 3.7 Los operadores y su uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 3.7.1 3.7.2 Asignacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Operadores matemticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 3.7.2.1 3.7.3 3.7.4 3.7.5 3.7.6 3.7.7 3.7.8 3.7.9 Introduccin a las macros del preprocesador . . . . . . . . . . . . . . . . . . . 106
Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 Operadores lgicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Operadores para bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Operadores de desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Operadores unarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 El operador ternario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 El operador coma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.7.10 Trampas habituales cuando se usan operadores . . . . . . . . . . . . . . . . . . . . . . . 112 3.7.11 Operadores de moldeado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 3.7.12 Los moldes explcitos de C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 3.7.12.1 static_cast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 3.7.12.2 const_cast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 3.7.12.3 reinterpret_cast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 3.7.13 sizeof - un operador en si mismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 3.7.14 La palabra reservada asm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 3.7.15 Operadores explcitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 3.8 Creacin de tipos compuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 3.8.1 3.8.2 Creacin de alias usando typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Usar struct para combinar variables . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 3.8.2.1 3.8.3 Punteros y estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Programas ms claros gracias a enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 3.8.3.1 Comprobacin de tipos para enumerados . . . . . . . . . . . . . . . . . . . . . 121
3.8.4 3.8.5
Cmo ahorrar memoria con union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 3.8.5.1 3.8.5.2 3.8.5.3 Punteros y arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 El formato de punto otante . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 Aritmtica de punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
3.9
Consejos para depuracin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 3.9.1 Banderas para depuracin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 3.9.1.1 3.9.1.2 3.9.2 3.9.3 Banderas de depuracin para el preprocesador . . . . . . . . . . . . . . . . . . 131 Banderas para depuracin en tiempo de ejecucin . . . . . . . . . . . . . . . . 131
3.10 Direcciones de funcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 3.10.1 Denicin de un puntero a funcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 3.10.2 Declaraciones y deniciones complicadas . . . . . . . . . . . . . . . . . . . . . . . . . . 134 3.10.3 Uso de un puntero a funcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 3.10.4 Arrays de punteros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 6
ndice de contenido 3.11 Make: cmo hacer compilacin separada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 3.11.1 Las actividades de Make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 3.11.1.1 Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 3.11.1.2 Reglas de sujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 3.11.1.3 Objetivos predeterminados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 3.11.2 Los Makeles de este libro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 3.11.3 Un ejemplo de Makele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 3.12 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 3.13 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 4 Abstraccin de Datos 4.1 143
Una librera pequea al estilo C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 4.1.1 4.1.2 Asignacin dinmica de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 Malas suposiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Qu tiene de malo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 El objeto bsico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 Qu es un objeto? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Tipos abstractos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Detalles del objeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Conveciones para los cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 Importancia de los cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 El problema de la declaracin mltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 Las directivas del preprocesador #dene, #ifndef y #endif . . . . . . . . . . . . . . . . . 158 Un estndar para los cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . 159 Espacios de nombres en los cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . 160 Uso de los cheros de cabecera en proyectos . . . . . . . . . . . . . . . . . . . . . . . . 160
4.8
4.9
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Establecer los lmites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 Control de acceso en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 5.2.1 protected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
5.3
Friends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 5.3.1 5.3.2 Amigas anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Es eso puro? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
5.4 5.5
Capa de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 La clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 5.5.1 5.5.2 Modicaciones en Stash para usar control de acceso . . . . . . . . . . . . . . . . . . . . 175 Modicar Stack para usar control de acceso . . . . . . . . . . . . . . . . . . . . . . . . . 176
5.6
ndice de contenido 5.6.1 5.6.2 5.7 5.8 6 Ocultar la implementacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 Reducir la recompilacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Inicializacin garantizada por el constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 Limpieza garantizada por el destructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 Eliminacin del bloque de deniciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 6.3.1 6.3.2 para bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 Alojamiento de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Stash con constructores y destructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 Stack con constructores y destructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 Inicializacin de tipos agregados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 Constructores por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 197
Ms decoracin de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 7.1.1 7.1.2 Sobrecarga en el valor de retorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 Enlace con tipos seguros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
Ejemplo de sobrecarga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 Uniones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 Argumentos por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 7.4.1 Argumentos de relleno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
Eleccin entre sobrecarga y argumentos por defecto . . . . . . . . . . . . . . . . . . . . . . . . . 206 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 211
Constantes 8.1
Sustitucin de valores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 8.1.1 8.1.2 8.1.3 8.1.4 const en archivos de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 constantes seguras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 Diferencias con C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
8.2
Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 8.2.1 8.2.2 Puntero a constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 Puntero constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 8.2.2.1 8.2.3 Formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
8.3 8
ndice de contenido 8.3.1 8.3.2 Paso por valor constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Retorno por valor constante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 8.3.2.1 8.3.3 Temporarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
8.4
Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 8.4.1 const en las clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 8.4.1.1 8.4.1.2 8.4.2 La lista de inicializacin del constructor. . . . . . . . . . . . . . . . . . . . . . 223 Constructores para los tipos del lenguaje . . . . . . . . . . . . . . . . . . . . . 223
Las constantes en tiempo de compilacin dentro de las clases. . . . . . . . . . . . . . . . 224 8.4.2.1 El enumerado en codigo antiguo . . . . . . . . . . . . . . . . . . . . . . . . . 226
8.4.3
Objetos y mtodos constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 8.4.3.1 FIXME mutable: bitwise vs. logical const . . . . . . . . . . . . . . . . . . . . 229
9.2
Funciones inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 9.2.1 9.2.2 inline dentro de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 Funciones de acceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 9.2.2.1 Accesores y mutadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
9.3 9.4
Stash y Stack con inlines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 Funciones inline y el compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 9.4.1 9.4.2 9.4.3 Limitaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 Referencias adelantadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 Actividades ocultas en contructores y destructores . . . . . . . . . . . . . . . . . . . . . 248
9.5 9.6
Reducir el desorden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Ms caractersticas del preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 9.6.1 Encolado de smbolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
10 Control de nombres
10.1 Los elementos estticos de C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 10.1.1 Variables estticas dentro de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 10.1.1.1 Objetos estticos dentro de funciones . . . . . . . . . . . . . . . . . . . . . . . 258 10.1.1.2 Destructores de objetos estticos . . . . . . . . . . . . . . . . . . . . . . . . . 259 10.1.2 Control del enlazado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260 10.1.2.1 Confusin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 9
ndice de contenido 10.1.3 Otros especicadores para almacenamiento de clases . . . . . . . . . . . . . . . . . . . . 262 10.2 Espacios de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 10.2.1 Crear un espacio de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 10.2.1.1 Espacios de nombres sin nombre . . . . . . . . . . . . . . . . . . . . . . . . . 263 10.2.1.2 Amigas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 10.2.2 Cmo usar un espacio de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 10.2.2.1 Resolucin del mbito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 10.2.2.2 La directiva using . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 10.2.2.3 La declaracin using . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 10.2.3 El uso de los espacios de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 10.3 Miembros estticos en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 10.3.1 Denicin del almacenamiento para miembros de datos estticos . . . . . . . . . . . . . . 268 10.3.1.1 Inicializacin de vectores estticos . . . . . . . . . . . . . . . . . . . . . . . . 270 10.3.2 Clases anidadas y locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 10.3.3 Mtodos estticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 10.4 FIXME static initialization dependency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 10.4.1 Qu hacer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 10.5 Especicaciones de enlazado alternativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 10.6 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 10.7 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 11 Las referencias y el constructor de copia 285
11.1 Punteros en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 11.2 Referencias en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 11.2.1 Referencias en las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286 11.2.1.1 Referencias constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 11.2.1.2 Referencias a puntero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 11.2.2 Consejos para el paso de argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 11.3 El constructor de copia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 11.3.1 Paso y retorno por valor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 11.3.1.1 Paso y retorno de objetos grandes . . . . . . . . . . . . . . . . . . . . . . . . . 289 11.3.1.2 mbito de la pila para una llamada a una funcin . . . . . . . . . . . . . . . . 290 11.3.1.3 Re-entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290 11.3.1.4 Copia bit a bit vs. inicializacin . . . . . . . . . . . . . . . . . . . . . . . . . . 291 11.3.2 Construccin por copia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292 11.3.2.1 Objetos temporales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295 11.3.3 Constructor copia por defecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 11.3.4 Alternativas a la construccin por copia . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 11.3.4.1 Prevencin del paso por valor . . . . . . . . . . . . . . . . . . . . . . . . . . . 298 11.3.4.2 Funciones que modican objetos externos . . . . . . . . . . . . . . . . . . . . 298 11.4 Punteros a miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 11.4.1 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 11.4.1.1 Un ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 10
ndice de contenido 11.5 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 11.6 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303 12 Sobrecarga de operadores 12.1 Precaucin y tranquilidad 305 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
12.2 Sintaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 12.3 Operadores sobrecargables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 12.3.1 Operadores unarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 12.3.1.1 Incremento y decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310 12.3.2 Operadores binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310 12.3.3 Argumentos y valores de retorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318 12.3.3.1 Retorno por valor como constante . . . . . . . . . . . . . . . . . . . . . . . . . 319 12.3.3.2 Optimizacin del retorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 12.3.4 Operadores poco usuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320 12.3.4.1 El operador coma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320 12.3.4.2 El operador -> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321 12.3.4.3 Un operador anidado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322 12.3.4.4 Operador ->* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324 12.3.5 Operadores que no puede sobrecargar . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325 12.4 Operadores no miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326 12.4.1 Directrices bsicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 12.5 Sobrecargar la asignacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 12.5.1 Comportamiento del operador = . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328 12.5.1.1 Punteros en clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329 12.5.1.2 Contabilidad de referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 12.5.1.3 Creacin automtica del operador = . . . . . . . . . . . . . . . . . . . . . . . . 335 12.6 Conversin automtica de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 12.6.1 Conversin por constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 12.6.1.1 Prevenir la conversin por constructor . . . . . . . . . . . . . . . . . . . . . . 336 12.6.2 Conversin por operador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 12.6.2.1 Reexividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338 12.6.3 Ejemplo de conversin de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 12.6.4 Las trampas de la conversin automtica de tipos . . . . . . . . . . . . . . . . . . . . . . 340 12.6.4.1 Actividades ocultas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341 12.7 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342 12.8 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342 13 Creacin dinmica de objetos 345
13.1 Creacin de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345 13.1.1 Asignacin dinmica en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346 13.1.2 Operador new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 13.1.3 Operador delete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 13.1.4 Un ejemplo sencillo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 11
ndice de contenido 13.1.5 Trabajo extra para el gestor de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . 349 13.2 Rediseo de los ejemplos anteriores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349 13.2.1 delete void* probablemente es un error . . . . . . . . . . . . . . . . . . . . . . . . . . 349 13.2.2 Responsabilidad de la limpieza cuando se usan punteros . . . . . . . . . . . . . . . . . . 350 13.2.3 Stash para punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 13.2.3.1 Una prueba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 13.3 new y delete para vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354 13.3.1 Cmo hacer que un puntero sea ms parecido a un vector . . . . . . . . . . . . . . . . . . 355 13.3.2 Cuando se supera el espacio de almacenamiento . . . . . . . . . . . . . . . . . . . . . . 355 13.3.3 Sobrecarga de los operadores new y delete . . . . . . . . . . . . . . . . . . . . . . . 356 13.3.3.1 Sobrecarga global de new y delete . . . . . . . . . . . . . . . . . . . . . . . 357 13.3.3.2 13.3.3.3 Sobrecarga de new y delete especca para una clase . . . . . . . . . . . . 358 Sobrecarga de new y delete para vectores . . . . . . . . . . . . . . . . . . 360
13.3.3.4 Llamadas al constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362 13.3.3.5 Operadores new y delete de [FIXME emplazamiento (situacin)] . . . . . . 363
14.1 Sintaxis de la composicin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367 14.2 Sintaxis de la herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369 14.3 Lista de inicializadores de un constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370 14.3.1 Inicializacin de objetos miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370 14.3.2 Tipos predenidos en la lista de inicializadores . . . . . . . . . . . . . . . . . . . . . . . 370 14.3.3 Combinacin de composicin y herencia . . . . . . . . . . . . . . . . . . . . . . . . . . 371 14.3.3.1 Llamadas automticas al destructor . . . . . . . . . . . . . . . . . . . . . . . . 372 14.3.4 Orden de llamada de constructores y destructores . . . . . . . . . . . . . . . . . . . . . . 372 14.4 Ocultacin de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374 14.5 Funciones que no heredan automticamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 14.5.1 Herencia y mtodos estticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 14.5.2 Composicin vs. herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 14.5.2.1 Subtipado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380 14.5.2.2 Herencia privada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382 14.5.2.2.1 Publicar los miembros heredados de forma privada . . . . . . . . . . 382
14.6 Protected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383 14.6.1 Herencia protegida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384 14.7 Herencia y sobrecarga de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384 14.8 Herencia mltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385 14.9 Desarrollo incremental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386 14.10FIXME Upcasting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386 14.10.1 Por qu FIXME "upcasting"? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387 14.10.2 FIXME Upcasting y el constructor de copia . . . . . . . . . . . . . . . . . . . . . . . . . 387 14.10.3 Composicin vs. herencia FIXME (revisited) . . . . . . . . . . . . . . . . . . . . . . . . 389 12
ndice de contenido 14.10.4 FIXME Upcasting de punteros y referencias . . . . . . . . . . . . . . . . . . . . . . . . . 390 14.10.5 Una crisis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390 14.11Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 14.12Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 15 Polimorsmo y Funciones virtuales 395
15.1 Evolucin de los programadores de C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 15.2 Upcasting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396 15.3 El problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 15.3.1 Ligadura de las llamadas a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 15.4 Funciones virtuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 15.4.1 Extensibilidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398 15.5 Cmo implementa C++ la ligadura dinmica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400 15.5.1 Almacenando informacin de tipo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400 15.5.2 Pintar funciones virtuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402 15.5.3 Detrs del teln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402 15.5.4 Instalar el vpointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403 15.5.5 Los objetos son diferentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403 15.6 Por qu funciones virtuales? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404 15.7 Clases base abstractas y funciones virtuales puras . . . . . . . . . . . . . . . . . . . . . . . . . . 405 15.7.1 Deniciones virtuales puras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407 15.8 Herencia y la VTABLE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408 15.8.1 FIXME: Object slicing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410 15.9 Sobrecargar y redenir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 15.9.1 Tipo de retorno variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 15.10funciones virtuales y constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 15.10.1 Orden de las llamadas a los constructores . . . . . . . . . . . . . . . . . . . . . . . . . . 414 15.10.2 Comportamiento de las funciones virtuales dentro de los constructores . . . . . . . . . . . 415 15.10.3 Destructores y destructores virtuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415 15.10.4 Destructores virtuales puros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 15.10.5 Mecanismo virtual en los destructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418 15.10.6 Creacin una jerarqua basada en objetos . . . . . . . . . . . . . . . . . . . . . . . . . . 419 15.11Sobrecarga de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 15.12Downcasting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423 15.13Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425 15.14Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426 16 Introduccin a las Plantillas 429
16.1 Contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429 16.1.1 La necesidad de los contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431 16.2 Un vistazo a las plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431 16.2.1 La solucin de la plantilla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432 16.3 Sintaxis del Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433 13
ndice de contenido 16.3.1 Deniciones de funcin no inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434 16.3.1.1 Archivos cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434 16.3.2 IntStack como plantilla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435 16.3.3 Constantes en los Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436 16.4 Stack y Stash como Plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437 16.4.1 Cola de punteros mediante plantillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439 16.5 Activando y desactivando la propiedad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443 16.6 Manejando objetos por valor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445 16.7 Introduccin a los iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447 16.7.1 Stack con iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453 16.7.2 PStash con iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 456 16.8 Por qu usar iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460 16.8.1 Plantillas Funcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462 16.9 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463 16.10Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463 A Estilo de codicacin 465
A.1 General . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465 A.2 Nombres de chero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466 A.3 Marcas comentadas de inicio y n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466 A.4 Parntesis, llaves e indentacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466 A.5 Nombres para identicadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 A.6 Orden de los #includes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 A.7 Guardas de inclusin en cheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470 A.8 Uso de los espacios de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470 A.9 Utilizacin de require() y assure() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470 471 479
C.1 Sobre C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479 C.2 Sobre C++ en general . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479 C.2.1 Mi propia lista de libros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
C.3 Los rincones oscuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480 C.4 Sobre Anlisis y Diseo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
14
ndice de tablas
3 C en C++ 3.1 3.2 3.3 Expresiones que utilizan booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Moldes explcitos de C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 Nuevas palabras reservadas para operadores booleanos . . . . . . . . . . . . . . . . . . . . . . . 117
12 Sobrecarga de operadores 12.1 Directrices para elegir entre miembro y no-miembro . . . . . . . . . . . . . . . . . . . . . . . . . 327
15
Prlogo a la traduccin
Este trabajo de traduccin ha sido realizado ntegramente por voluntarios. Le agradecemos que nos comunique cualquier error de traduccin o transcripcin en el texto. Tambin ser bienvenido si desea colaborar ms activamente en la traduccin. Puede encontrar informacin para ello en nuestra pgina web 1 . Aydenos a hacer de esta traduccin un trabajo de calidad. Tenga presente que el trabajo de traduccin no ha terminado. Si utiliza el libro en el estado actual es probable que encuentre innumerables errores. Por favor, no nos los notique por ahora. Le agradeceremos su colaboracin para corregir posibles erratas cuando el trabajo se d por terminado, pero ese momento an no ha llegado (falta poco). Ello no implica que el libro no pueda serle til en su estado actual. Este captulo se actualiza frecuentemente y no lo dar por terminado hasta que concluya el proceso de traduccin y revisin de este volumen al menos. La traduccin del Volumen 2 acaba de comenzar.
El texto original de estas directrices est accesible en la pgina web del autor.
17
ndice de tablas
El proyecto de traduccin
Si desea saber ms sobre este proyecto visite http://arco.inf-cr.uclm.es/~david.villa/pensarC++.html.
Tecnicismos
Se han traducido la mayor parte de los trminos especcos tanto de orientacin a objetos como de programacin en general. Para evitar confusiones o ambigedades a los lectores que manejen literatura en ingls hemos incluido entre parntesis el trmino original la primera vez que aparece traducido. Para traducir tecnicismos especialmente complicados hemos utilizado como referencia la segunda edicin de El lenguaje de Programacin C++ (en castellano) as como la Wikipedia. En contadas ocasiones se ha mantenido el trmino original en ingls. En benecio de la legibilidad, hemos preferido no hacer traducciones demasiado forzadas ni utilizar expresiones que pudieran resultar desconocidas en el argot o en los libros especializados disponibles en castellano. Nuestro propsito es tener un libro que pueda ser comprendido por hispano-hablantes. Es a todas luces imposible realizar una traduccin rigurosa acorde con las normas lingsticas de la RAE, puesto que, en algunos casos, el autor incluso utiliza palabras de su propia invencin.
Cdigo fuente
Por hacer
Produccin
El texto ha sido escrito en el lenguaje de marcado DocBook versin 4.3 en su variante XML. Cada captulo est contenido en un chero independiente y todos ellos se incluyen en un chero maestro utilizando XInclude. Debido a que muchos procesadores de DocBook no soportan adecuadamente la caracterstica XInclude, se usa la herramienta xsltproc para generar un nico chero XML que contiene el texto de todo el libro.
Cdigo fuente
Tambin se utiliza XInclude para aadir en su lugar el contenido de los cheros de cdigo fuente escritos en C++. De ese modo, el texto de los listados que aparecen en el libro es idntico a los cheros C++ que distribuye el autor. De ese modo, la edicin es mucha ms limpia y sobretodo se evitan posibles errores de transcripcin de los listados. Utilizando un pequeo programa escrito en lenguaje Python3 , se substituyen los nombres etiquetados de los cheros por la sentencia XInclude correscpondiente:
//: V1C02:Hello.cpp
pasa a ser:
<xi:include parse="text" href="./code_v1/C02/Hello.cpp"/>
Una ver realizada esta substitucin, se utiliza de nuevo xsltproc para montar tanto el texto como los listados en un nico chero XML.
./utils/x_includes.py
18
ndice de tablas
El equipo
Las siguientes personas han colaborado en mayor o menor medida en algn momento desde el comienzo del proyecto de traduccin de Pensar en C++: David Villa Alises (coordinador) dvilla#gmx.net Mguel ngel Garca miguelangel.garcia#gmail.com Javier Corrales Garca jcg#damir.iem.csic.es Brbara Teruggi bwire.red#gmail.com Sebastin Gurin Gloria Barbern Gonzlez globargon#gmail.com Fernando Perfumo Velzquez nperfumo#telefonica.net Jos Mara Gmez josemaria.gomez#gmail.com David Martnez Moreno ender#debian.org Cristbal Tello ctg#tinet.org Jess Lpez Mollo (pre-Lucas) Jos Mara Requena Lpez (pre-Lucas) Javier Fenoll Rejas (pre-Lucas)
Agradecimientos
Por hacer
19
Prefacio
Como cualquier lenguaje humano, C++ proporciona mtodos para expresar conceptos. Si se utiliza de forma correcta, este medio de expresin ser signicativamente ms sencillo y exible que otras alternativas cuando los problemas aumentan en tamao y complejidad.
No se puede ver C++ slo como un conjunto de caractersticas, ya que algunas de esas caractersticas no tienen sentido por separado. Slo se puede utilizar la suma de las partes si se est pensando en el diseo, no slo en el cdigo. Y para entender C++ de esta forma, se deben comprender los problemas existentes con C y con la programacin en general. Este libro trata los problemas de programacin, porque son problemas, y el enfoque que tiene C++ para solucionarlos. Adems, el conjunto de caractersticas que explico en cada captulo se basar en la forma en que yo veo un tipo de problema en particular y cmo resolverlo con el lenguaje. De esta forma espero llevar al lector, poco a poco, de entender C al punto en el que C++ se convierta en su propia lengua. Durante todo el libro, mi actitud ser pensar que el lector desea construir en su cabeza un modelo que le permita comprender el lenguaje bajando hasta sus races; si se tropieza con un rompecabezas, ser capaz de compararlo con su modelo mental y deducir la respuesta. Tratar de comunicarle las percepciones que han reorientado mi cerebro para Pensar en C++.
ndice de tablas
Requisitos
En la primera edicin de este libro, decid suponer que otra persona ya le haba enseado C y que el lector tena, al menos, un nivel aceptable de lectura del mismo. Mi primera intencin fue hablar de lo que me result difcil: el lenguaje C++. En esta edicin he aadido un captulo como introduccin rpida a C, acompaada del seminario en-CD Thinking in C, pero sigo asumiendo que el lector tiene algn tipo de experiencia en programacin. Adems, del mismo modo que se aprenden muchas palabras nuevas intuitivamente, vindolas en el contexto de una novela, es posible aprender mucho sobre C por el contexto en el que se utiliza en el resto del libro.
Aprender C++
Yo me adentr en C++ exactamente desde la misma posicin en la que espero que se encuentren muchos de los lectores de este libro: como un programador con una actitud muy sensata y con muchos vicios de programacin. Peor an, mi experiencia era sobre porgramacin de sistemas empotrados a nivel hardware, en la que a veces se considera a C como un lenguaje de alto nivel y excesivamente ineciente para ahorrar bits. Descubr ms tarde que nunca haba sido un buen programador en C, camuando as mi ignorancia sobre estructuras, malloc() y free(), setjmp() y longimp(), y otros conceptos sosticados, y murindome de vergenza cuando estos trminos entraban en una conversacin, en lugar de investigar su utilidad. Cuando comenc mi lucha por aprender C++, el nico libro decente era la auto-proclamada Gua de expertos de Bjarne Stroustrup 4 as que simpliqu los conceptos bsicos por m mismo. Esto se acab convirtiendo en mi primer libro de C++ 5 , que es esencialmente un reejo de mi experiencia. Fue descrita como una gua de lectura para atraer a los programadores a C y C++ al mismo tiempo. Ambas ediciones 6 del libro consiguieron una respuesta entusiasta. Ms o menos al mismo tiempo que apareca Using C++, comenc a ensear el lenguaje en seminarios y presentaciones. Ensear C++ (y ms tarde, Java) se convirti en mi profesin; llevo viendo cabezas asintiendo, caras plidas, y expresiones de perplejidad en audiencias por todo el mundo desde 1989. Cuando comenc a dar formacin interna a grupos ms pequeos, descubr algo durante los ejercicios. Incluso aquella gente que estaba sonriendo y asintiendo se encontraba equivocada en muchos aspectos. Creando y dirigiendo las pruebas de C++ y Java durante muchos aos en la Conferencia de Desarrollo de Software, descubr que tanto otros oradores como yo tendamos a tocar demasiados temas, y todo demasiado rpido. As que, de vez en cuando, a pesar de la variedad del nivel de la audiencia e independientemente de la forma en que se presentara el material, terminara perdiendo alguna parte de mi pblico. Quiz sea pedir demasiado, pero como soy una de esas personas que se resisten a una conferencia tradicional (y para la mayora de las personas, creo, esta resistencia est causada por el aburrimiento), quise intentar mantener a cada uno a su velocidad. Durante un tiempo, estuve haciendo presentaciones en orden secuencial. De ese modo, termin por aprender experimentando e iterando (una tcnica que tambin funciona bien en el diseo de programas en C++). Al nal, desarroll un curso usando todo lo que haba aprendido de mi experiencia en la enseanza. As, el aprendizaje se realiza en pequeos pasos, fciles de digerir, y de cara a un seminario prctico (la situacin ideal para el aprendizaje) hay ejercicios al nal de cada presentacin. Puede encontrar mis seminarios pblicos en www.BruceEckel.com, y tambin puede aprender de los seminarios que he pasado a CD-ROM. La primera edicin de este libro se gest a lo largo de dos aos, y el material de este libro se ha usado de muchas
4 5 6
Bjarne Stroustrup, The C++ Programming Language, Addison-Wesley, 1986 (rst edition). Using C++, Osborne/McGraw-Hill 1989. Using C++ and C++ Inside & Out, Osborne/McGraw-Hill 1993.
22
ndice de tablas formas y en muchos seminarios diferentes. Las reacciones que he percibido de cada seminario me han ayudado a cambiar y reorientar el material hasta que he comprobado que funciona bien como un medio de enseanza. Pero no es slo un manual para dar seminarios; he tratado de recopilar tanta informacin como he podido en estas pginas, intentando estructurarlas para atraer al lector hasta la siguiente materia. Ms que nada, el libro est diseado para servir al lector solitario que lucha con un lenguaje de programacin nuevo.
Objetivos
Mis objetivos en este libro son: 1. Presentar el material paso a paso, de manera que el lector pueda digerir cada concepto fcilmente antes de continuar. 2. Usar ejemplos tan simples y cortos como sea posible. Esto a veces me impide manejar problemas del mundo real, pero he descubierto que los principiantes normalmente quedan ms contentos cuando pueden comprender cada detalle de un ejemplo que siendo impresionados por el mbito del problema que soluciona. Adems, hay un lmite en la cantidad de cdigo que se puede asimilar en una clase. Por ello, a veces recibo crticas por usar ejemplos de juguete, pero tengo la buena voluntad de aceptarlas en favor de producir algo pedaggicamente til. 3. La cuidadosa presentacin secuencial de capacidades para que no se vea algo que no ha sido explicado. De acuerdo, esto no siempre es posible; en esos casos, se ofrece una breve descripcin introductoria. 4. Indicarle lo que creo que es importante para que se comprenda el lenguaje, ms que todo lo que s. Creo que hay una "jerarqua de la importancia de la informacin", y hay algunos hechos que el 95 por ciento de los programadores nunca necesitar saber y que slo podran confundirles y aanzar su percepcin de la complejidad del lenguaje. Tomando un ejemplo de C, si memoriza la tabla de precedencia de los operadores (yo nunca lo hice), puede escribir cdigo ms corto. Pero si lo piensa, esto confundir al lector/mantenedor de ese cdigo. As que olvide la precedencia, y utilice parntesis cuando las cosas no estn claras. Esta misma actitud la utilizar con alguna otra informacin del lenguaje C++, que creo que es ms importante para escritores de compiladores que para programadores. 5. Mantener cada seccin sucientemente enfocada como para que el tiempo de lectura -y el tiempo entre bloques de ejercicios- sea razonable. Eso mantiene las mentes de la audiencia ms activas e involucradas durante un seminario prctico, y adems le da al lector una mayor sensacin de avance. 6. Ofrecer a los lectores una base slida de manera que puedan comprender las cuestiones lo sucientemente bien como para pasar a otros cursos y libros ms difciles (en concreto, el Volumen 2 de este libro). 7. He tratado de no utilizar ninguna versin de C++ de ningn proveedor en particular porque, para aprender el lenguaje, no creo que los detalles de una implementacin concreta sean tan importantes como el lenguaje mismo. La documentacin sobre las especicaciones de implementacin propia de cada proveedor suele ser adecuada.
Captulos
C++ es un lenguaje en el que se construyen caractersticas nuevas y diferentes sobre una sintaxis existente (por esta razn, nos referiremos a l como un lenguaje de programacin orientado a objetos hbrido). Como mucha gente pasa por una curva de aprendizaje, hemos comenzado por adaptarnos a la forma en que los programadores pasan por las etapas de las cualidades del lenguaje C++. Como parece que la progresin natural es la de una mente entrenada de forma procedural, he decidido comprender y seguir el mismo camino y acelerar el proceso proponiendo y resolviendo las preguntas que se me ocurrieron cuando yo aprenda el lenguaje y tambin las que se les ocurrieron a la gente a la que lo enseaba. El curso fue diseado con algo en mente: hacer ms eciente el proceso de aprender C++. La reaccin de la audiencia me ayud a comprender qu partes eran difciles y necesitaban una aclaracin extra. En las reas en las que me volva ambicioso e inclua demasiadas cosas de una vez, me d cuenta -mediante la presentacin de material- de que si incluyes demasiadas caractersticas, tendrs que explicarlas todas, y es fcil que la confusin de los estudiantes se agrave. Como resultado, he tenido muchos problemas para introducir las caractersticas tan lentamente como ha sido posible; idealmente, slo un concepto importante a la vez por captulo. 23
ndice de tablas As pues, el objetivo en cada captulo es ensear un concepto simple, o un pequeo grupo de conceptos asociados, en caso de que no haya ms conceptos adicionales. De esa forma puede digerir cada parte en el contexto de su conocimiento actual antes de continuar. Para llevarlo a cabo, dej algunas partes de C para ms adelante de lo que me hubiese gustado. La ventaja es que se evita la confusin al no ver todas las caractersticas de C++ antes de que stas sean explicadas, as su introduccin al lenguaje ser tranquila y reejar la forma en que asimile las caractersticas que dejo en sus manos. He aqu una breve descripcin de los captulos que contiene este libro: Captulo 1: Introduccin a los objetos. Cuando los proyectos se vuelven demasiado grandes y difciles de mantener, nace la crisis del software, que es cuando los programadores dicen: No podemos terminar los proyectos, y cuando podemos, son demasiado caros!. Eso provoc gran cantidad de reacciones, que se discuten en este captulo mediante las ideas de Programacin Orientada a Objetos (POO) y cmo intenta sta resolver la crisis del software. El captulo le lleva a travs de las caractersticas y conceptos bsicos de la POO y tambin introduce los procesos de anlisis y diseo. Adems, aprender acerca de los benecios y problemas de adaptar el lenguaje, y obtendr sugerencias para adentrarse en el mundo de C++. Captulo 2: Crear y usar objetos. Este captulo explica el proceso de construir programas usando compiladores y libreras. Presenta el primer programa C++ del libro y muestra cmo se construyen y compilan los programas. Despus se presentan algunas de las libreras de objetos bsicas disponibles en C++ Estndar. Para cuando acabe el captulo, dominar lo que se reere a escribir un programa C++ utilizando las libreras de objetos predenidas. Captulo 3: El C de C++. Este captulo es una densa vista general de las caractersticas de C que se utilizan en C++, as como gran nmero de caractersticas bsicas que slo estn disponibles en C++. Adems introduce la utilidad make, que es habitual en el desarrollo software de todo el mundo y que se utiliza para construir todos los ejemplos de este libro (el cdigo fuente de los listados de este libro, que est disponible en www.BruceEckel.com, contiene los makefiles correspondientes a cada captulo). En el captulo 3 supongo que el lector tiene unos conocimientos bsicos slidos en algn lenguaje de programacin procedural como Pascal, C, o incluso algn tipo de Basic (basta con que haya escrito algo de cdigo en ese lenguaje, especialmente funciones). Si encuentra este captulo demasiado difcil, debera mirar primero el seminario Pensar en C del CD que acompaa este libro (tambin disponible en www.BruceEckel.com). Captulo 4: Abstraccin de datos. La mayor parte de las caractersticas de C++ giran entorno a la capacidad de crear nuevos tipos de datos. Esto no slo ofrece una mayor organizacin del cdigo, tambin es la base preliminar para las capacidades de POO ms poderosas. Ver cmo esta idea es posible por el simple hecho de poner funciones dentro de las estructuras, los detalles de cmo hacerlo, y qu tipo de cdigo se escribe. Tambin aprender la mejor manera de organizar su cdigo mediante archivos de cabecera y archivos de implementacin. Captulo 5: Ocultar la implementacin. El programador puede decidir que algunos de los datos y funciones de su estructura no estn disponibles para el usuario del nuevo tipo hacindolas privadas. Eso signica que se puede separar la implementacin principal de la interfaz que ve el programador cliente, y de este modo permitir que la implementacin se pueda cambiar fcilmente sin afectar al cdigo del cliente. La palabra clave class tambin se presenta como una manera ms elaborada de describir un tipo de datos nuevo, y se desmitica el signicado de la palabra objeto (es una variable elaborada). Captulo 6: Inicializacin y limpieza. Uno de los errores ms comunes en C se debe a las variables no inicializadas. El constructor de C++ permite garantizar que las variables de su nuevo tipo de datos (objetos de su clase) siempre se inicializarn correctamente. Si sus objetos tambin requieren algn tipo de reciclado, usted puede garantizar que ese reciclado se realice siempre mediante el destructor C++. Captulo 7: Sobrecarga de funciones y argumentos por defecto. C++ est pensado para ayudar a construir proyectos grandes y complejos. Mientras lo hace, puede dar lugar a mltiples libreras que utilicen el mismo nombre de funcin, y tambin puede decidir utilizar un mismo nombre con diferentes signicados en la misma biblioteca. Con C++ es sencillo gracias a la sobrecarga de funciones, lo que le permite reutilizar el mismo nombre de funcin siempre que la lista de argumentos sea diferente. Los argumentos por defecto le permiten llamar a la misma funcin de diferentes maneras proporcionando, automticamente, valores por defecto para algunos de sus argumentos. Captulo 8: Constantes. Este captulo cubre las palabras reservadas const y volatile, que en C++ tienen un signicado adicional, especialmente dentro de las clases. Aprender lo que signica aplicar const a una denicin de puntero. El captulo tambin muestra cmo vara el signicado de const segn se utilice dentro o fuera de las clases y cmo crear constantes dentro de clases en tiempo de compilacin. Captulo 9: Funciones inline. Las macros del preprocesador eliminan la sobrecarga de llamada a funcin, pero el preprocesador tambin elimina la valiosa comprobacin de tipos de C++. La funcin inline le ofrece todos
24
ndice de tablas los benecios de una macro de preprocesador adems de los benecios de una verdadera llamada a funcin. Este captulo explora minuciosamente la implementacin y uso de las funciones inline. Captulo 10: Control de nombres. La eleccin de nombres es una actividad fundamental en la programacin y, cuando un proyecto se vuelve grande, el nmero de nombres puede ser arrollador. C++ le permite un gran control de los nombres en funcin de su creacin, visibilidad, lugar de almacenamiento y enlazado. Este captulo muestra cmo se controlan los nombres en C++ utilizando dos tcnicas. Primero, la palabra reservada static se utiliza para controlar la visibilidad y enlazado, y se explora su signicado especial para clases. Una tcnica mucho ms til para controlar los nombres a nivel global es el namespace de C++, que le permite dividir el espacio de nombres global en distintas regiones. Captulo 11: Las referencias y el constructor de copia. Los punteros de C++ trabajan como los punteros de C con el benecio adicional de la comprobacin de tipos ms fuerte de C++. C++ tambin proporciona un mtodo adicional para manejar direcciones: C++ imita la referencia de Algol y Pascal, que permite al compilador manipular las direcciones, pero utilizando la notacin ordinaria. Tambin encontrar el constructor-de-copia, que controla la manera en que los objetos se pasan por valor hacia o desde las funciones. Finalmente, se explica el puntero-a-miembro de C++. Captulo 12: Sobrecarga de operadores. Esta caracterstica se llama algunas veces azcar sintctico; permite dulcicar la sintaxis de uso de su tipo permitiendo operadores as como llamadas a funciones. En este captulo aprender que la sobrecarga de operadores slo es un tipo de llamada a funcin diferente y aprender cmo escribir sus propios operadores, manejando el -a veces confuso- uso de los argumentos, devolviendo tipos, y la decisin de si implementar el operador como mtodo o funcin amiga. Captulo 13: Creacin dinmica de objetos. Cuntos aviones necesitar manejar un sistema de trco areo? Cuntas guras requerir un sistema CAD? En el problema de la programacin genrica, no se puede saber la cantidad, tiempo de vida o el tipo de los objetos que necesitar el programa una vez lanzado. En este captulo aprender cmo new y delete solventan de modo elegante este problema en C++ creando objetos en el montn. Tambin ver cmo new y delete se pueden sobrecargar de varias maneras, de forma que puedan controlar cmo se asigna y se recupera el espacio de almacenamiento. Captulo 14: Herencia y composicin. La abstraccin de datos le permite crear tipos nuevos de la nada, pero con composicin y herencia, se puede crear tipos nuevos a partir de los ya existentes. Con la composicin, se puede ensamblar un tipo nuevo utilizando otros tipos como piezas y, con la herencia, puede crear una versin ms especca de un tipo existente. En este captulo aprender la sintaxis, cmo redenir funciones y la importancia de la construccin y destruccin para la herencia y la composicin. Captulo 15: Polimorsmo y funciones virtuales. Por su cuenta, podra llevarle nueve meses descubrir y comprender esta piedra angular de la POO. A travs de ejercicios pequeos y simples, ver cmo crear una familia de tipos con herencia y manipular objetos de esa familia mediante su clase base comn. La palabra reservada virtual le permite tratar todos los objetos de su familia de forma genrica, lo que signica que el grueso del cdigo no depende de informacin de tipo especca. Esto hace extensibles sus programas, de manera que construir programas y mantener el cdigo sea ms sencillo y ms barato. Captulo 16: Introduccin a las plantillas. La herencia y la composicin permiten reutilizar el cdigo objeto, pero eso no resuelve todas las necesidades de reutilizacin. Las plantillas permiten reutilizar el cdigo fuente proporcionando al compilador un medio para sustituir el nombre de tipo en el cuerpo de una clase o funcin. Esto da soporte al uso de bibliotecas de clase contenedor, que son herramientas importantes para el desarrollo rpido y robusto de programas orientados a objetos (la Biblioteca Estndar de C++ incluye una biblioteca signicativa de clases contenedor). Este captulo ofrece una profunda base en este tema esencial. Temas adicionales (y materias ms avanzadas) estn disponibles en el Volumen 2 del libro, que se puede descargar del sitio web www.BruceEckel.com.
Ejercicios
He descubierto que los ejercicios son excepcionalmente tiles durante un seminario para completar la comprensin de los estudiantes, as que encontrar algunos al nal de cada captulo. El nmero de ejercicios ha aumentado enormemente respecto a la primera edicin. Muchos de los ejercicios son sucientemente sencillos como para que puedan terminarse en una cantidad de tiempo razonable en una clase o apartado de laboratorio mientras el profesor observa, asegurndose de que todos los estudiantes asimilan el material. Algunos ejercicios son un poco ms complejos para mantener entretenidos a los estudiantes avanzados. El grueso de los ejercicios estn orientados para ser resueltos en poco tiempo y se 25
ndice de tablas intenta slo probar y pulir sus conocimientos ms que presentar retos importantes (seguramente ya los encontrar por su cuenta -o mejor dicho-, ellos lo encontrarn a usted).
Cdigo fuente
El cdigo fuente de los listados de este libro est registrado como freeware, distribuido mediante el sitio Web www.BruceEckel.com. El copyright le impide publicar el cdigo en un medio impreso sin permiso, pero se le otorga el derecho de usarlo de muchas otras maneras (ver ms abajo). El cdigo est disponible en un chero comprimido, destinado a extraerse desde cualquier plataforma que tenga una utilidad zip (puede buscar en Internet para encontrar una versin para su platarforma si an no tiene una instalada). En el directorio inicial donde desempaquete el cdigo encontrar la siguiente nota de registro:
Copyright (c) 2000, Bruce Eckel Source code file from the book "Thinking in C++" All rights reserved EXCEPT as allowed by the following statements: You can freely use this file for your own work (personal or commercial), including modifications and distribution in executable form only. Permission is granted to use this file in classroom situations, including its use in presentation materials, as long as the book "Thinking in C++" is cited as the source. Except in classroom situations, you cannot copy and distribute this code; instead, the sole distribution point is http://www.BruceEckel.com (and official mirror sites) where it is available for free. You cannot remove this copyright and notice. You cannot distribute modified versions of the source code in this package. You cannot use this file in printed media without the express permission of the author. Bruce Eckel makes no representation about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty of any kind, including any implied warranty of merchantability, fitness for a particular purpose, or non-infringement. The entire risk as to the quality and performance of the software is with you. Bruce Eckel and the publisher shall not be liable for any damages suffered by you or any third party as a result of using or distributing this software. In no event will Bruce Eckel or the publisher be liable for any lost revenue, profit, or data, or for direct, indirect, special, consequential, incidental, or punitive damages, however caused and regardless of the theory of liability, arising out of the use of or inability to use software, even if Bruce Eckel and the publisher have been advised of the possibility of such damages. Should the software prove defective, you assume the cost of all necessary servicing, repair, or correction. If you think youve found an error, please submit the correction using the form you will find at www.BruceEckel.com. (Please use the same form for non-code errors found in the book.)
26
ndice de tablas
Se puede usar el cdigo en proyectos y clases siempre y cuando se mantenga la nota de copyright.
ndice de tablas
Errores
No importa cuntos trucos emplee un escritor para detectar los errores, algunos siempre se escapan y saltan del papel al lector atento. Si encuentra algo que crea que es un error, por favor, utilice el formulario de correcciones que encontrar en www.BruceEckel.com. Se agradece su ayuda.
Sobre la portada
La primera edicin de este libro tena mi cara en la portada, pero para la segunda edicin yo quera desde el principio una portada que se pareciera ms una obra de arte, como la portada de Pensar en Java. Por alguna razn, C++ parece sugerirme Art Dec con sus curvas simples y pinceladas cromadas. Tena en mente algo como esos carteles de barcos y aviones con cuerpos largos. Mi amigo Daniel Will-Harris, (www.Will-Harris.com) a quien conoc en las clases del coro del instituto, iba a llegar a ser un diseador y escritor de talla mundial. l ha hecho prcticamente todos mis diseos, includa la portada para la primera edicin de este libro. Durante el proceso de diseo de la portada, Daniel, insatisfecho con el progreso que realizbamos, siempre preguntaba: Qu relacin hay entre las personas y las computadoras?. Estbamos atascados. Como capricho, sin nada en mente, me pidi que pusiera mi cara en el escner. Daniel tena uno de sus programas grcos (Corel Xara, su favorito) que autotraz mi cara escaneada. l lo describe de la siguente manera: El autotrazado es la forma en la que la computadora transforma un dibujo en los tipos de lneas y curvas que realmente le gustan. Entonces jug con ello hasta que obtuvo algo que pareca un mapa topogrco de mi cara, una imagen que podra ser la manera en que la computadora ve a la gente. Cog esta imagen y la fotocopi en papel de acuarela (algunas copiadoras pueden manejar papeles gruesos), y entonces comenz a realizar montones de experimentos aadiendo acuarela a la imagen. Seleccionamos las que nos gustaban ms, entonces Daniel las volvi a escanear y las organiz en la portada, aadiendo el texto y otros elementos de diseo. El proceso total requiri varios meses, mayormente a causa del tiempo que me tom hacer las acuarelas. Pero me he divertido especialmente porque consegu participar en el arte de la portada, y porque me dio un incentivo para hacer ms acuarelas (lo que dicen sobre la prctica realmente es cierto).
28
ndice de tablas Microsoft Word para Windows Versiones 8 y 9 para escribir el libro y crear la versin para impresin, incluyendo la generacin de la tabla de contenidos y el ndice (cre un servidor automatizado COM en Python, invocado desde las macros VBA de Word, para ayudarme en el marcado de los ndices). Python (vea www.python.com) se utiliz para crear algunas de las herramientas para comprobar el cdigo, y lo habra utilizado como herramienta de extraccin de cdigo si lo hubiese descubierto antes. Cre los diagramas utilizando Visio. Gracias a Visio Corporation por crear una herramienta tan til. El tipo de letra del cuerpo es Georgia y los ttulos utilizan Verdana. La versin denitiva se cre con Adobe Acrobat 4 y el chero generado se llev directamente a la imprenta - muchas gracias a Adobe por crear una herramienta que permite enviar documentos listos para impresin por correo electrnico, as como permitir que se realicen mltiples revisiones en un nico da en lugar de recaer sobre mi impresora lser y servicios rpidos 24 horas (probamos el proceso Acrobat por primera vez con Pensar en Java, y fui capaz de subir la versin nal de ese libro a la imprenta de U.S. desde Sudfrica). La versin HTML se cre exportando el documento Word a RTF, y utilizando entonces RTF2HTML (ver http://www.sunpack.com/RTF/) para hacer la mayor parte del trabajo de la conversin HTML (gracias a Chris Hector por hacer una herramienta tan til y especialmente able). Los cheros resultantes se limpiaron utilizando un programa Python que truqu, y los WMFs se transformaron en GIFs utilizando el PaintShop Pro 6 de JASC y su herramienta de conversin por lotes (gracias a JASC por resolver tantos de mis problemas con su excelente producto). El realce del color de la sintaxis se aadi con un script Perl amablemente cedido por Zar Anjum.
Agradecimientos
Lo primero, agradecer a todo aquel que present correcciones y sugerencias desde Internet; han sido de tremenda ayuda para mejorar la calidad de este libro, y no podra haberlo hecho sin ustedes. Gracias en especial a John Cook. Las ideas y comprensin de este libro han llegado de varias fuentes: amigos como Chuck Allison, Andrea Provaglio, Dans Sakx, Scott Meyers, Charles Petzold y Michael Wilk; pioneros del lenguaje como Bjarne Stroustrup, Andrew Koenig y Rob Murray; miembros del Comit de Estndares de C++ como Nathan Myers (que fue de particular ayuda y generosidad con sus percepciones), Bill Plauger, Reg Charney, Tom Penello, Tom Plum, Sam Druker y Uwe Steinmueller; gente que ha hablado en mis charlas de C++ en la Conferencia de Desarrollo de Software; y a menudo estudiantes de mis seminarios, que preguntan aquello que necesito or para aclarar el material. Enormes agradecimientos para mi amigo Gen Kiyooka, cuya compaa Digigami me proporcion un servidor web. Mi amigo Richard Hale Shaw y yo hemos enseado C++ juntos; las percepciones de Richard y su apoyo han sido muy tiles (y las de Kim tambin). Gracias tambin a DoAnn Vikoren, Eric Faurot, Jennifer Jessup, Tara Arrowood, Marco Pardi, Nicole Freeman, Barbara Hanscome, Regina Ridley, Alex Dunne y el resto del reparto y plantilla de MFI. Un agradecimiento especial para todos mis profesores y todos mis estudiantes (que tambin son profesores). Y para mis escritores favoritos, mi ms profundo aprecio y simpata por vuestros esfuerzos: John Irving, Neal Stephenson, Robertson Davies (te echaremos de menos), Tom Robbins, William Gibson, Richard Bach, Carlos Castaneda y Gene Wolfe. A Guido van Rossum, por inventar Python y donarlo desinteresadamente al mundo. Has enriquecido mi vida con tu contribucin. Gracias a la gente de Prentice Hall: Alan Apt, Ana Terry, Scott Disanno, Toni Holm y mi editora de copias electrnicas Stephanie English. En mrqueting, Bryan Gambrel y Jennie Burger. Sonda Donovan me ayud con la produccin del CD ROM. Daniel Will-Harris (por supuesto) cre el diseo de la portada que se encuentra en el propio CD. Para todos los grandes amigos de Crested Butte, gracias por hacer de l un lugar mgico, especialmente a Al Smith (creador del maravilloso Camp4 Coffee Garden), mis vecinos Dave y Erika, Marsha de la librera Hegs Place, Pat y John de Teocalli Temale, Sam de Barkery Caf, y a Tiller por su ayuda con la investigacin en audio. Y a toda la gente fenomenal que anda por Camp4 y hace interesantes mis maanas. La lista de amigos que me han dado soporte incluye, pero no est limitada, a Zack Urlocker, Andrew Binstock, Neil Rubenking, Kraig Brocschmidt, Steve Sinofsky, JD Hildebrandt, Brian McElhinney, Brinkey Barr, Larry 29
ndice de tablas OBrien, Bill Gates en Midnight Engineering Magazine, Larry Constantine, Lucy Lockwood, Tom Keffer, Dan Putterman, Gene Wang, Dave Mayer, David Intersimone, Claire Sawyers, los Italianos (Andrea Provaglio, Rossella Gioia, Laura Fallai, Marco & Lella Cantu, Corrado, Ilsa y Christina Giustozzi), Chris y Laura Strand (y Parker), los Alquimistas, Brad Jerbic, Marilyn Cvitanic, el Mabrys, el Halingers, los Pollocks, Peter Vinci, los Robbins, los Moelters, Dave Stoner, Laurie Adams, los Cranstons, Larry Fogg, Mike y karen Sequeira, Gary Entsminger y Allison Brody, Kevin, Sonda & Ella Donovan, Chester y Shannon Andersen, Joe Lordi, Dave y Brenda Barlett, los Rentschlers, Lynn y Todd y sus familias. Y por supuesto, a Mam y Pap.
30
Captulo 1. Introduccin a los Objetos El mtodo orientado a objetos va un paso ms all, proporcionando herramientas para que el programador represente los elementos en el espacio del problema. Esta representacin es lo sucientemente general como para que el programador no est limitado a un tipo particular de problema. Nos referimos a los elementos en el espacio del problema, y a sus representaciones en el espacio de la solucin, como objetos (por supuesto, necesitar otros objetos que no tengan analogas en el espacio del problema). La idea es que permita al programa adaptarse al lenguaje del problema aadiendo nuevos tipos de objetos de modo que cuando lea el cdigo que describe la solucin, est leyendo palabras que adems expresan el problema. Es un lenguaje de abstraccin ms exible y potente que los que haya usado antes. De esta manera, la POO permite describir el problema en trminos del problema, en lugar de usar trminos de la computadora en la que se ejecutar la solucin. Sin embargo, todava existe una conexin con la computadora. Cada objeto se parece un poco a una pequea computadora; tiene un estado y operaciones que se le puede pedir que haga. Sin embargo, no parece una mala analoga a los objetos en el mundo real; todos ellos tienen caractersticas y comportamientos. Algunos diseadores de lenguajes han decidido que la programacin orientada a objetos en s misma no es adecuada para resolver fcilmente todos los problemas de programacin, y abogan por una combinacin de varias aproximaciones en lenguajes de programacin multiparadigma. 1 Alan Kay resumi las cinco caractersticas bsicas de Smalltalk, el primer lenguaje orientado a objetos con xito y uno de los lenguajes en los que est basado C++. Esas caractersticas representan una aproximacin a la programacin orientada a objetos: 1. Todo es un objeto. Piense en un objeto como una variable elaborada; almacena datos, pero puede hacer peticiones a este objeto, solicitando que realice operaciones en s mismo. En teora, puede coger cualquier componente conceptual del problema que est intentando resolver (perros, edicios, servicios, etc.) y representarlos como un objeto en su programa. 2. Un programa es un grupo de objetos enviando mensajes a otros para decirles qu hacer. Para hacer una peticin a un objeto, enva un mensaje a ese objeto. Ms concretamente, puede pensar en un mensaje como una peticin de invocacin a una funcin que pertenece a un objeto particular. 3. Cada objeto tiene su propia memoria constituida por otros objetos. Visto de otra manera, puede crear un nuevo tipo de objeto haciendo un paquete que contenga objetos existentes. Por consiguiente, puede hacer cosas complejas en un programa ocultando la complejidad de los objetos. 4. Cada objeto tiene un tipo. Usando el argot, cada objeto es una instancia de una clase, en el que clase es sinnimo de tipo. La caracterstica ms importante que lo distingue de una clase es Qu mensajes puede enviarle? 5. Todos los objetos de un tipo particular pueden recibir los mismos mensajes. En realidad es una frase con doble sentido, como ver ms tarde. Como un objeto de tipo crculo es tambin un objeto de tipo figura, est garantizado que un crculo aceptar los mensajes de gura. Esto signica que puede escribir cdigo que habla con objetos figura y automticamente funcionar con cualquier otro objeto que coincida con la descripcin de figura. Esta sustituibilidad es uno de los conceptos ms poderosos en la POO.
Ver Multiparadigm Programming in Leda de Timothy Budd (Addison-Wesley 1995). Puede encontrar una implementacin interesante de este problema en el Volumen 2 de este libro, disponible en www.BruceEckel.com
32
1.2. Cada objeto tiene una interfaz objeto decide qu hacer con l). Los miembros (elementos) de cada clase tienen algo en comn: cada cuenta tiene un balance, cada cajero puede aceptar un depsito, etc. Al mismo tiempo, cada miembro tiene su propio estado, cada cuenta tiene un balance diferente, cada cajero tiene un nombre. De este modo, cada cajero, cliente, cuenta, transaccin, etc., se puede representar con una nica entidad en el programa de computador. Esta entidad es un objeto, y cada objeto pertenece a una clase particular que dene sus caractersticas y comportamientos. Por eso, lo que hace realmente un programa orientado a objetos es crear nuevos tipos de datos, prcticamente todos los lenguajes de programacin orientados a objetos usan la palabra reservada class. Cuando vea la palabra type, piense en class y viceversa 3 . Dado que una clase describe un conjunto de objetos que tienen idnticas caractersticas (elementos de datos) y comportamientos (funcionalidad), una clase es realmente un tipo de datos porque un nmero de punto otante, por ejemplo, tambin tiene un conjunto de caractersticas y comportamientos. La diferencia est en que el programador dene una clase para resolver un problema en lugar de estar obligado a usar un tipo de dato existente diseado para representar una unidad de almacenamiento en una mquina. Ampla el lenguaje de programacin aadiendo nuevos tipos de datos especcos segn sus necesidades. El sistema de programacin acoge las nuevas clases y les presta toda la atencin y comprobacin de tipo que da a los tipos predenidos. El enfoque orientado a objetos no est limitado a la construccin de simulaciones. Est o no de acuerdo con que cualquier problema es una simulacin del sistema que est diseando, el uso de tcnicas POO puede reducir fcilmente un amplio conjunto de problemas a una solucin simple. Una vez establecida una clase, puede hacer tantos objetos de esta clase como quiera, y manipularlos como si fueran elementos que existen en el problema que est intentando resolver. De hecho, uno de los desafos de la programacin orientada a objetos es crear una correspondencia unvoca entre los elementos en el espacio del problema y objetos en el espacio de la solucin. Pero, cmo se consigue que un objeto haga algo til por usted? Debe haber una forma de hacer una peticin al objeto para que haga algo, como completar una transaccin, dibujar algo en la pantalla o activar un interruptor. Y cada objeto puede satisfacer slo ciertas peticiones. Las peticiones que puede hacer un objeto estn denidas por su intefaz, y es el tipo lo que determina la interfaz. Un ejemplo simple puede ser una representacin de una bombilla:
Luz
encender( ) apagar( ) intensificar( ) atenuar( )
La interfaz establece qu peticiones se pueden hacer a un objeto particular. Sin embargo, se debe codicar en algn sitio para satisfacer esta peticin. sta, junto con los datos ocultos, constituyen la implementacin. Desde el punto de vista de la programacin procedural, no es complicado. Un tipo tiene una funcin asociada para cada posible peticin, y cuando se hace una peticin particular a un objeto, se llama a esa funcin. Este proceso normalmente se resume diciendo que ha enviado un mensaje (ha hecho una peticin) a un objeto, y el objeto sabe qu hacer con este mensaje (ejecuta cdigo). Aqu, el nombre del tipo/clase es Luz, el nombre de este objeto particular de Luz es luz1, y las peticiones que se le pueden hacer a un objeto Luz son encender, apagar, intensicar o atenuar. Puede crear un objeto Luz declarando un nombre (luz1) para ese objeto. Para enviar un mensaje al objeto, escriba el nombre del objeto y conctelo al mensaje de peticin con un punto. Desde el punto de vista del usuario de una clase predenida, eso es prcticamente todo lo que necesita para programar con objetos. El diagrama mostrado arriba sigue el formato del Lenguaje Unicado de Modelado (UML). Cada clase se
3 Hay quien hace una distincin, armando que type determina la interfaz mientras class es una implementacin particular de esta interfaz.
33
Captulo 1. Introduccin a los Objetos representa con una caja, con el nombre del tipo en la parte de arriba, los atributos que necesite describir en la parte central de la caja, y los mtodos (las funciones que pertenecen a este objeto, que reciben cualquier mensaje que se enve al objeto) en la parte inferior de la caja. A menudo, en los diagramas de diseo UML slo se muestra el nombre de la clase y el nombre de los mtodos pblicos, y por eso la parte central no se muestra. Si slo est interesado en el nombre de la clase, tampoco es necesario mostrar la parte inferior.
34
1.5. Herencia: reutilizacin de interfaces nos referimos a la composicin como una relacin tiene-un, como en un coche tiene-un motor.
Coche
Motor
(El diagrama UML anterior indica composicin con el rombo relleno, lo cual implica que hay un coche. Tpicamente usar una forma ms simple: slo una lnea, sin el rombo, para indicar una asociacin. 5 ) La composicin es un mecanismo muy exible. Los objetos miembro de su nueva clase normalmente son privados, hacindolos inaccesibles para los programadores clientes que estn usando la clase. Eso permite cambiar esos miembros sin perturbar al cdigo cliente existente. Tambin puede cambiar los miembros del objeto en tiempo de ejecucin, para cambiar dinmicamente el comportamiento de su programa. La herencia, descrita ms adelante, no tiene esta exibilidad dado que el compilador debe imponer restricciones durante la compilacin en clases creadas con herencia. Como la herencia es tan importante en la programacin orientada a objetos, se suele enfatizar mucho su uso, y puede que el programador novel tenga la idea de que la herencia se debe usar en todas partes. Eso puede dar como resultado diseos torpes y demasiado complicados. En lugar de eso, debera considerar primero la composicin cuando tenga que crear nuevas clases, ya que es ms simple y exible. Si acepta este enfoque, sus diseos sern ms limpios. Una vez que tenga experiencia, los casos en los que necesite la herencia resultarn evidentes.
Base
(En el diagrama UML anterior, la echa apunta desde la clase derivada hacia la clase base. Como puede ver, puede haber ms de una clase derivada.) Un tipo hace algo ms que describir las restricciones de un conjunto de objetos; tambin tiene una relacin con otros tipos. Dos tipos pueden tener caractersticas y comportamientos en comn, pero un tipo puede contener ms caractersticas que otro y tambin puede manipular ms mensajes (o hacerlo de forma diferente). La herencia lo expresa de forma similar entre tipos usando el concepto de tipos base y tipos derivados. Un tipo base contiene todas las caractersticas y comportamientos compartidos entre los tipos derivados de l. Cree un tipo base para representar lo esencial de sus ideas sobre algunos objetos en su sistema. A partir del tipo base, derive otros tipos para expresar caminos diferentes que puede realizar esa parte comn. Por ejemplo, una mquina de reciclado de basura clasica piezas de basura. El tipo base es basura, y cada pieza de basura tiene un peso, un valor, y tambin, se puede triturar, fundir o descomponer. A partir de
5
Normalmente esto es suciente para la mayora de los diagramas y no necesita especicar si est usando agregacin o composicin.
35
Captulo 1. Introduccin a los Objetos ah, se obtienen ms tipos especcos de basura que pueden tener caractersticas adicionales (una botella tiene un color) o comportamientos (el aluminio puede ser aplastado, el acero puede ser magntico). Adems, algunos comportamientos pueden ser diferentes (el valor del papel depende del tipo y condicin). Usando la herencia, se puede construir una jerarqua de tipos que exprese el problema que se intenta resolver en trminos de sus tipos. Un segundo ejemplo es el clsico ejemplo gura, tal vez usado en un sistema de diseo asistido por computador o juegos de simulacin. El tipo base es figura, y cada gura tiene un tamao, un color, una posicin y as sucesivamente. Cada gura se puede dibujar, borrar, mover, colorear, etc. A partir de ah, los tipos especcos de guras derivan (heredan) de ella: crculo, cuadrado, tringulo, y as sucesivamente, cada uno de ellos puede tener caractersticas y comportamientos adicionales. Ciertas guras pueden ser, por ejemplo, rotadas. Algunos comportamientos pueden ser diferentes, como cuando se quiere calcular el rea de una gura. La jerarqua de tipos expresa las similitudes y las diferencias entre las guras.
Modelar la solucin en los mismos trminos que el problema es tremendamente benecioso porque no se necesitan un montn de modelos intermedios para transformar una descripcin del problema en una descripcin de la solucin. Con objetos, la jerarqua de tipos es el principal modelo, lleva directamente desde la descripcin del sistema en el mundo real a la descripcin del sistema en cdigo. Efectivamente, una de las dicultades que la gente tiene con el diseo orientado a objetos es que es demasiado fcil ir desde el principio hasta el nal. Una mente entrenada para buscar soluciones complejas a menudo se confunde al principio a causa de la simplicidad. Cuando se hereda de un tipo existente, se est creando un tipo nuevo. Este nuevo tipo contiene no slo todos los miembros del tipo base (aunque los datos privados private estn ocultos e inaccesibles), sino que adems, y lo que es ms importante, duplica la interfaz de la clase base. Es decir, todos los mensajes que se pueden enviar a los objetos de la clase base se pueden enviar tambin a los objetos de la clase derivada. Dado que se conoce el tipo de una clase por los mensajes que se le pueden enviar, eso signica que la clase derivada es del mismo tipo que la clase base. En el ejemplo anterior, un crculo es una gura. Esta equivalencia de tipos va herencia es uno de las claves fundamentales para comprender la programacin orientada a objetos. Por lo que tanto la clase base como la derivada tienen la misma interfaz, debe haber alguna implementacin que corresponda a esa interfaz. Es decir, debe haber cdigo para ejecutar cuando un objeto recibe un mensaje particular. Si simplemente hereda de una clase y no hace nada ms, los mtodos de la interfaz de la clase base estn disponibles en la clase derivada. Esto signica que los objetos de la clase derivada no slo tienen el mismo tipo, tambin tienen el mismo comportamiento, lo cual no es particularmente interesante. Hay dos caminos para diferenciar la nueva clase derivada de la clase base original. El primero es bastante sencillo: simplemente hay que aadir nuevas funciones a la clase derivada. Estas nuevas funciones no son parte de la interfaz de la clase base. Eso signica que la clase base simplemente no hace todo lo que necesitamos, por lo que se aaden ms funciones. Este uso simple y primitivo de la herencia es, a veces, la solucin perfecta a muchos problemas. Sin embargo, quiz debera pensar en la posibilidad de que su clase base puede necesitar tambin funciones adicionales. Este proceso de descubrimiento e iteracin de su diseo ocurre regularmente en la programacin orientada a objetos.
36
reflejoVertical( ) reflejoHorizontal( )
Aunque la herencia algunas veces supone que se van a aadir nuevas funciones a la interfaz, no es necesariamente cierto. El segundo y ms importante camino para diferenciar su nueva clase es cambiar el comportamiento respecto de una funcin de una clase base existente. A esto se le llama reescribir (override) una funcin.
dibujar( ) borrar( )
dibujar( ) borrar( )
dibujar( ) borrar( )
Para reescribir una funcin, simplemente hay que crear una nueva denicin para esa funcin en la clase derivada. Est diciendo, Estoy usando la misma funcin de interfaz aqu, pero quiero hacer algo diferente para mi nuevo tipo.
Captulo 1. Introduccin a los Objetos se ampla la interfaz y se crea un tipo nuevo. El nuevo tipo todava puede ser sustituido por el tipo base, pero la sustitucin no es perfecta porque sus nuevas funciones no son accesibles desde el tipo base. Esta relacin se conoce como es-como-un; el nuevo tipo tiene la interfaz del viejo tipo, pero tambin contiene otras funciones, por lo que se puede decir que es exactamente el mismo. Por ejemplo, considere un aire acondicionado. Suponga que su casa est conectada con todos los controles para refrigerar; es decir, tiene una interfaz que le permite controlar la temperatura. Imagine que el aire acondicionado se avera y lo reemplaza por una bomba de calor, la cual puede dar calor y fro. La bomba de calor es-como-un aire acondicionado, pero puede hacer ms cosas. Como el sistema de control de su casa est diseado slo para controlar el fro, est rentringida a comunicarse slo con la parte de fro del nuevo objeto. La interfaz del nuevo objeto se ha extendido, y el sistema existente no conoce nada excepto la interfaz original.
Tesmostato
temperaturaMnima( )
controla
Sistema de fro
fro( )
Aire Acondicionado
Bomba de calor
fro( )
fro( ) calor( )
Por supuesto, una vez que vea este diseo queda claro que la clase base sistema de fro no es bastante general, y se debera renombrar a sistema de control de temperatura, adems tambin puede incluir calor, en este punto se aplica el principio de sustitucin. Sin embargo, el diagrama de arriba es un ejemplo de lo que puede ocurrir en el diseo y en el mundo real. Cuando se ve el principio de sustitucin es fcil entender cmo este enfoque (sustitucin pura) es la nica forma de hacer las cosas, y de hecho es bueno para que sus diseos funcionen de esta forma. Pero ver que hay ocasiones en que est igualmente claro que se deben aadir nuevas funciones a la interfaz de la clase derivada. Con experiencia, ambos casos puede ser razonablemente obvios.
1.6. Objetos intercambiables gracias al polimorsmo comportamiento del Pjaro. Entonces, qu hace que cuando se invoca mover() ignorando el tipo especco de Pjaro, puede ocurrir el comportamiento correcto (un Ganso corre, vuela, o nada, y un Pingino corre o nada)?
ControladorDePjaro
recolocar( )
Qu ocurre cuando se invoca mover( )?
Pjaro
mover( )
Ganso
mover( )
Pingino
mover( )
La respuesta es el primer giro en programacin orientada a objetos: el compilador no hace una llamada a la funcin en el sentido tradicional. La llamada a funcin generada por un compilador no-OO provoca lo que se llama una ligadura temprana (early binding), un trmino que quiz no haya odo antes porque nunca ha pensado en que hubiera ninguna otra forma. Signica que el compilador genera una llamada al nombre de la funcin especca, y el enlazador resuelve esta llamada con la direccin absoluta del cdigo que se ejecutar. En POO, el programa no puede determinar la direccin del cdigo hasta el momento de la ejecucin, de modo que se necesita algn otro esquema cuando se enva un mensaje a un objeto genrico. Para resolver el problema, los lenguajes orientados a objetos usan el concepto de ligadura tarda (late binding). Cuando enva un mensaje a un objeto, el cdigo invocado no est determinado hasta el momento de la ejecucin. El compilador se asegura de que la funcin existe y realiza una comprobacin de tipo de los argumentos y el valor de retorno (el lenguaje que no realiza esta comprobacin se dice que es dbilmente tipado), pero no sabe el cdigo exacto a ejecutar. Para llevar a cabo la ligadura tarda, el compilador de C++ inserta un trozo especial de cdigo en lugar de la llamada absoluta. Este cdigo calcula la direccin del cuerpo de la funcin, usando informacin almacenada en el objeto (este proceso se trata con detalle en el Captulo 15). De este modo, cualquier objeto se puede comportar de forma diferente de acuerdo con el contenido de este trozo especial de cdigo. Cuando enva un mensaje a un objeto, el objeto comprende realmente qu hacer con el mensaje. Es posible disponer de una funcin que tenga la exibilidad de las propiedades de la ligadura tarda usando la palabra reservada virtual. No necesita entender el mecanismo de virtual para usarla, pero sin ella no puede hacer programacin orientada a objetos en C++. En C++, debe recordar aadir la palabra reservada virtual porque, por defecto, los mtodos no se enlazan dinmicamente. Los mtodos virtuales le permiten expresar las diferencias de comportamiento en clases de la misma familia. Estas diferencias son las que causan comportamientos polimrcos. Considere el ejemplo de la gura. El diagrama de la familia de clases (todas basadas en la misma interfaz uniforme) apareci antes en este captulo. Para demostrar el polimorsmo, queremos escribir una nica pieza de cdigo que ignore los detalles especcos de tipo y hable slo con la clase base. Este cdigo est desacoplado de la informacin del tipo especco, y de esa manera es ms simple de escribir y ms fcil de entender. Y, si tiene un nuevo tipo - un Hexgono, por ejemplo - se aade a travs de la herencia, el cdigo que escriba funcionar igual de bien para el nuevo tipo de Figura como para los tipos anteriores. De esta manera, el programa es extensible. Si escribe una funcin C++ (podr aprender dentro de poco cmo hacerlo):
void hacerTarea(Figura& f) { f.borrar(); // ... f.dibujar(); }
Esta funcin se puede aplicar a cualquier Figura, de modo que es independiente del tipo especco del objeto 39
Captulo 1. Introduccin a los Objetos que se dibuja y borra (el & signica toma la direccin del objeto que se pasa a hacerTarea(), pero no es importante que entienda los detalles ahora). Si en alguna otra parte del programa usamos la funcin hacerTarea():
Circulo c; Triangulo t; Linea l; hacerTarea(c); hacerTarea(t); hacerTarea(l);
Las llamadas a hacerTarea() funcionan bien automticamente, a pesar del tipo concreto del objeto. En efecto es un truco bonito y asombroso. Considere la lnea:
hacerTarea(c);
Lo que est ocurriendo aqu es que est pasando un Crculo a una funcin que espera una Figura. Como un Crculo es una Figura se puede tratar como tal por parte de hacerTarea(). Es decir, cualquier mensaje que pueda enviar hacerTarea() a una Figura, un Crculo puede aceptarlo. Por eso, es algo completamente lgico y seguro. A este proceso de tratar un tipo derivado como si fuera su tipo base se le llama upcasting (moldeado hacia arriba6 ). El nombre cast (molde) se usa en el sentido de adaptar a un molde y es hacia arriba por la forma en que se dibujan los diagramas de clases para indicar la herencia, con el tipo base en la parte superior y las clases derivadas colgando debajo. De esta manera, moldear un tipo base es moverse hacia arriba por el diagrama de herencias: upcasting
"Upcasting"
Figura
Crculo
Cuadrado
Tringulo
Todo programa orientado a objetos tiene algn upcasting en alguna parte, porque as es como se despreocupa de tener que conocer el tipo exacto con el que est trabajando. Mire el cdigo de hacerTarea():
f.borrar(); // ... f.dibujar();
Observe que no dice Si es un Crculo, haz esto, si es un Cuadrado, haz esto otro, etc.. Si escribe un tipo de cdigo que comprueba todos los posibles tipos que una Figura puede tener realmente, resultar sucio y tendr que cambiarlo cada vez que aada un nuevo tipo de Figura. Aqu, slo dice Eres una gura, s que te puedes borrar() y dibujar() a ti misma, hazlo, y preocpate de los detalles. Lo impresionante del cdigo en hacerTarea() es que, de alguna manera, funciona bien. Llamar a dibujar() para un Crculo ejecuta diferente cdigo que cuando llama a dibujar() para un Cuadrado o una Lnea, pero cuando se enva el mensaje dibujar() a un Figura annima, la conducta correcta sucede en base en el tipo real de Figura. Esto es asombroso porque, como se mencion anteriormente, cuando el compilador
6
N. de T: En el libro se utilizar el trmino original en ingls debido a su uso comn, incluso en la literatura en castellano.
40
1.7. Creacin y destruccin de objetos C++ est compilando el cdigo para hacerTarea(), no sabe exactamente qu tipos est manipulando. Por eso normalmente, es de esperar que acabe invocando la versin de borrar() y dibujar() para Figura, y no para el Crculo, Cuadrado, o Lnea especco. Y an as ocurre del modo correcto a causa del polimorsmo. El compilador y el sistema se encargan de los detalles; todo lo que necesita saber es que esto ocurre y lo que es ms importante, cmo utilizarlo en sus diseos. Si un mtodo es virtual, entonces cuando enve el mensaje a un objeto, el objeto har lo correcto, incluso cuando est involucrado el upcasting.
Captulo 1. Introduccin a los Objetos La gestin de excepciones conecta la gestin de errores directamente en el lenguaje de programacin y a veces incluso en el sistema operativo. Una excepcin es un objeto que se lanza desde el lugar del error y puede ser capturado por un manejador de excepcin apropiado diseado para manipular este tipo particular de error. Es como si la gestin de errores fuera una ruta de ejecucin diferente y paralela que se puede tomar cuando las cosas van mal. Y como usa un camino separado de ejecucin, no necesita interferir con el cdigo ejecutado normalmente. Eso hace que el cdigo sea ms simple de escribir ya que no se fuerza al programador a comprobar los errores constantemente. Adems, una excepcin no es lo mismo que un valor de error devuelto por una funcin o una bandera jada por una funcin para indicar una condicin de error, que se puede ignorar. Una excepcin no se puede ignorar, de modo que est garantizado que habr que tratarla en algn momento. Finalmente, las excepciones proporcionan una forma para recuperar una situacin consistente. En lugar de salir simplemente del programa, a menudo es posible arreglar las cosas y restaurar la ejecucin, lo que produce sistemas ms robustos. Merece la pena tener en cuenta que la gestin de excepciones no es una caracterstica orientada a objetos, aunque en lenguajes orientados a objetos las excepciones normalmente se representan con objetos. La gestin de excepciones exista antes que los lenguajes orientados a objetos. En este Volumen se usa y explica la gestin de excepciones slo por encima; el Volmen 2 (disponible en www.BruceEckel.com) cubre con ms detalle la gestin de excepciones.
42
1.9. Anlisis y diseo la implementacin. Desde luego, cuando crea un SGBD (Sistema Gestor de Bases de Datos), conviene entender la necesidad de un cliente a fondo. Pero un SGBD est en una clase de problemas que son muy concretos y bien entendidos; en muchos programas semejantes, la estructura de la base de datos es el problema que debe afrontarse. El tipo de problema de programacin tratado en este captulo es de la variedad comodn (con mis palabras), en el que la solucin no es simplemente adaptar una solucin bien conocida, en cambio involucra uno o ms factores comodn -elementos para los que no hay solucin previa bien entendida, y para los que es necesario investigar 8 . Intentar analizar minuciosamente un problema comodn antes de pasar al diseo y la implementacin provoca un anlisis-parlisis porque no se tiene suciente informacin para resolver este tipo de problema durante la fase de anlisis. Resolver estos problemas requiere interaccin a travs del ciclo completo, y eso requiere comportamientos arriesgados (lo cual tiene sentido, porque est intentando hacer algo nuevo y los benecios potenciales son mayores). Puede parecer que el riesgo est compuesto por prisas en una implementacin preliminar, pero en cambio puede reducir el riesgo en un proyecto comodn porque est descubriendo pronto si es viable un enfoque particular para el problema. El desarrollo del producto es gestin de riesgos. A menudo se propone que construya uno desechable. Con la POO, todava debe andar parte de este camino, pero debido a que el cdigo est encapsulado en clases, durante la primera iteracin inevitablemente producir algunos diseos de clases tiles y desarrollar algunas ideas vlidas sobre el diseo del sistema que no necesariamente son desechables. De esta manera, la primera pasada rpida al problema no produce slo informacin crtica para la siguiente iteracin de anlisis, diseo, e implementacin, sino que adems crea el cdigo base para esa iteracin. Es decir, si est buscando una metodologa que contenga detalles tremendos y sugiera muchos pasos y documentos, es an ms difcil saber cundo parar. Tenga presente lo que est intentando encontrar: 1. Cules son los objetos? (Cmo divide su proyecto en sus partes componentes?) 2. Cules son sus interfaces? (Qu mensajes necesita enviar a otros objetos?) Si slo cuenta con los objetos y sus interfaces, entonces puede escribir un programa. Por varias razones podra necesitar ms descripciones y documentos, pero no puede hacerlo con menos. El proceso se puede realizar en cinco fases, y una fase 0 que es simplemente el compromiso inicial de usar algn tipo de estructura.
Declaracin de objetivos
Cualquier sistema construido, no importa cuan complicado sea, tiene un propsito fundamental, el negocio que hay en l, la necesidad bsica que satisface. Si puede ver la interfaz de usuario, el hardware o los detalles especcos
8 Mi regla general para el clculo de semejantes proyectos: Si hay ms de un comodn, no intente planear cunto tiempo le llevar o cunto costar hasta que haya creado un prototipo funcional. Tambin hay muchos grados de libertad.
43
Captulo 1. Introduccin a los Objetos del sistema, los algoritmos de codicacin y los problemas de eciencia, nalmente encontrar el ncleo de su existencia, simple y sencillo. Como el as llamado concepto de alto nivel de una pelcula de Hollywood, puede describirlo en una o dos frases. Esta descripcin pura es el punto de partida. El concepto de alto nivel es bastante importante porque le da el tono a su proyecto; es una declaracin de principios. No tiene porqu conseguirlo necesariamente la primera vez (podra tener que llegar a una fase posterior del proyecto antes de tenerlo completamente claro), pero siga intentndolo hasta que lo consiga. Por ejemplo, en un sistema de control de trco areo puede empezar con un concepto de alto nivel centrado en el sistema que est construyendo: El programa de la torre sigue la pista a los aviones. Pero considere qu ocurre cuando adapta el sistema para un pequeo aeropuerto; quiz slo haya un controlador humano o ninguno. Un modelo ms til no se preocupar de la solucin que est creando tanto como la descripcin del problema: Llega un avin, descarga, se revisa y recarga, y se marcha.
44
Retirar Fondos
Cliente
Cajero
Cada monigote representa un actor, que tpicamente es un humano o algn otro tipo de agente libre. (Incluso puede ser otro sistema de computacin, como es el caso del ATM). La caja representa el lmite del sistema. Las elipses representan los casos de uso, los cuales son descripciones de trabajo vlido que se puede llevar a cabo con el sistema. Las lneas entre los actores y los casos de uso representan las interacciones. No importa cmo est implementado realmente el sistema, mientras se lo parezca al usuario. Un caso de uso no necesita ser terriblemente complejo, incluso si el sistema subyacente es complejo. Lo nico que se persigue es mostrar el sistema tal como aparece ante el usuario. Por ejemplo:
Invernadero
Mantener temperatura
Jardinero
Los casos de uso producen las especicaciones de requisitos determinando todas las interacciones que el usuario puede tener con el sistema. Intente descubrir una serie completa de casos de uso para su sistema, y una vez que lo haya hecho tendr lo esencial sobre lo que se supone que hace su sistema. Lo bueno de centrarse en casos de uso es que siempre le lleva de vuelta a lo esencial y le mantiene alejado de los asuntos no crticos para conseguir terminar el trabajo. Es decir, si tiene una serie completa de casos de uso puede describir su sistema y pasar a la siguiente fase. Probablemente no lo har todo perfectamente en el primer intento, pero no pasa nada. Todo le ser revelado en su momento, y si pide una especicacin del sistema perfecta en este punto se atascar. Si se ha atascado, puede reactivar esta fase usando una herramienta tosca de aproximacin: describir el sistema en pocos prrafos y despus buscar sustantivos y verbos. Los nombres pueden sugerir actores, contexto del caso de uso (ej. lobby), o artefactos manipulados en el caso de uso. Los verbos pueden sugerir interaccin entre actores y casos de uso, y pasos especcos dentro del caso de uso. Adems descubrir que nombres y verbos producen objetos y mensajes durante la fase de diseo (y observe que los casos de uso describen interacciones entre subsistemas, as que la tcnica nombre y verbo slo se puede usar como una herramienta de lluvia de ideas puesto que no genera casos de uso) 10 . El lmite entre un caso de uso y un actor puede mostrar la existencia de una interfaz de usuario, pero no la dene. Si le interesa el proceso de denicin y creacin de interfaces de usuario, vea Software for Use de Larry Constantine y Lucy Lockwood, (Addison Wesley Longman, 1999) o vaya a www.ForUse.com. Aunque es un arte oscuro, en este punto es importante hacer algn tipo de estimacin de tiempo bsica. Ahora
10 Puede encontar ms informacin sobre casos de uso en Applying Use Cases de Schneider & Winters (Addison-Wesley 1998) y Use Case Driven Object Modeling with UML de Rosenberg (Addison-Wesley 1999).
45
Captulo 1. Introduccin a los Objetos tiene una visin general de qu est construyendo as que probablemente ser capaz de tener alguna idea de cunto tiempo llevar. Aqu entran en juego muchos factores. Si hace una estimacin a largo plazo entonces la compaa puede decidir no construirlo (y usar sus recursos en algo ms razonable -eso es bueno). O un gerente puede tener ya decidido cunto puede durar un proyecto e intentar inuir en su estimacin. Pero es mejor tener una estimacin honesta desde el principio y afrontar pronto las decisiones difciles. Ha habido un montn de intentos de crear tcnicas de estimacin precisas (como tcnicas para predecir la bolsa), pero probablemente la mejor aproximacin es conar en su experiencia e intuicin. Utilice su instinto para predecir cunto tiempo llevar tenerlo terminado, entonces multiplique por dos y aada un 10%. Su instinto visceral probablemente sea correcto; puede conseguir algo contando con este tiempo. El doble le permitir convertirlo en algo decente, y el 10% es para tratar los renamientos y detalles nales 11 . Sin embargo, usted quiere explicarlo, y a pesar de quejas y manipulaciones que ocurren cuando publique la estimacin, parece que esta regla funciona.
46
1.9. Anlisis y diseo hacer diseos orientado a objetos no revisando ejemplos abstractos, sino trabajando sobre un diseo que era ms interesante para ellos en ese momento: los suyos. Una vez que tenga con una serie de tarjetas CRC, quiz quiera crear una descripcin ms formal de su diseo usando UML 12 . No necesita usar UML, pero puede servirle de ayuda, especialmente si quiere poner un diagrama en la pared para que todo el mundo lo tenga en cuenta, lo cual es una buena idea. Una alternativa a UML es una descripcin textual de los objetos y sus interfaces, o, dependiendo de su lenguaje de programacin, el propio cdigo 13 . UML tambin proporciona una notacin de diagramas adicional para describir el modelo dinmico de su sistema. Eso es til en situaciones en las que las transiciones de estado de un sistema o subsistema son bastante ms dominantes de lo que necesitan sus propios diagramas (como en un sistema de control). Tambin puede necesitar describir las estructuras de datos, para sistemas o subsistemas en los que los propios datos son un factor dominante (como una base de datos). Sabr qu est haciendo con la fase 2 cuando haya descrito los objetos y sus interfaces. Bien, en muchos de ellos hay algunos que no se pueden conocer hasta la fase 3. Pero est bien. Todo lo que le preocupa es que eventualmente descubra todo sobre sus objetos. Es bueno descubrirlos pronto pero la POO proporciona suciente estructura de modo que no es grave si los descubre ms tarde. De hecho, el diseo de un objeto suele ocurrir in cinco etapas, durante todo el proceso de desarrollo del programa.
Para novatos, recomiendo el mencionado UML Distilled. Python (www.python.org) suele utilizarse como pseudocdigo ejecutable.
47
1.9. Anlisis y diseo para los clientes, los cuales pueden ver en el estado actual del producto exactamente donde se encuentra todo. Esto puede reducir o eliminar la necesidad de abrumadoras reuniones de control y aumentar la conanza y el apoyo de los clientes.
16 Esto es algo como prototipado rpido, donde se propone construir un borrador de la versin rpida y sucia que se puede utilizar para aprender sobre el sistema, y entonces puede tirar su prototipo y construir el bueno. El problema con el prototipado rpido es que la gente no tir el prototipo, y construy sobre l. Combinado con la falta de estructura en la programacin procedural, esto produca a menudo sistemas desordenados que eran difciles de mantener.
49
Captulo 1. Introduccin a los Objetos de esbozo para guiarle en su camino. El desarrollo de software ha llegado a extremos. Durante mucho tiempo, la gente tena poca estructura en sus desarrollos, pero entonces grandes proyectos empezaron a fracasar. Como resultado, se acab utilizando metodologas que tenan una cantidad abrumadora de estructura y detalle, se intent principalmente para esos grandes proyectos. Estas metodologas eran muy complicadas de usar - la sensacin era que se estaba perdiendo todo el tiempo escribiendo documentos y no programando (a menudo era as). Espero haberle mostrado aqu sugerencias a medio camino - una escala proporcional. Usar una propuesta que se ajusta a sus necesidades (y a su personalidad). No importa lo pequeo que desee hacerlo, cualquier tipo de plan supondr una gran mejora en su proyecto respecto a no planear nada. Recuerde que, segn la mayora de las estimaciones, alrededor del 50% de proyectos fracasan (algunas estimaciones superan el 70%!). Seguir un plan - preferiblemente uno simple y breve - y esbozar la estructura del diseo antes de empezar a codicar, descubrir qu cosas caen juntas ms fcilmente que si se lanza a programar, y tambin alcanzar un mayor grado de satisfaccin. Mi experiencia me dice que llegar a una solucin elegante es profundamente satisfactorio en un nivel completamente diferente; parece ms arte que tecnologa. Y la elegancia siempre vale la pena; no es una bsqueda frvola. No slo le permite tener un programa fcil de construir y depurar, tambin es ms fcil de comprender y mantener, y ah es donde recae su valor econmico.
1.11. Porqu triunfa C++ que las mejoras reales en la tecnologa giran realmente alrededor del proceso de prueba. El lenguaje ensamblador slo se ja en la sintaxis, pero C impone algunas restricciones de semntica, y stas le impiden cometer ciertos tipos de errores. Los lenguajes POO imponen incluso ms restricciones semnticas, si lo piensa son realmente formas del proceso de prueba. Se utiliza apropiadamente este tipo de datos? Se invoca esta funcin del modo correcto? son el tipo de pruebas que se llevan a cabo por el compilador en tiempo de ejecucin del sistema. Se han visto los resultados de tener estas pruebas incorporadas en el lenguaje: la gente ha sido capaz de escribir sistemas ms complejos, y han funcionado, con mucho menos tiempo y esfuerzo. He tratado de comprender porqu ocurre eso, pero ahora me doy cuenta de que son las pruebas: el programador hace algo mal, y la red de seguridad de las pruebas incorporadas le dice que hay un problema y le indica dnde. Pero las pruebas incorporadas que proporciona el diseo del lenguaje no pueden ir ms lejos. En este punto, el programador debe intervenir y aadir el resto de las pruebas que producen un juego completo (en cooperacin con el compilador y el tiempo de ejecucin del sistema) que verica el programa completo. Y, del mismo modo que tiene un compilador vigilando por encima de su hombro, no querra que estas pruebas le ayudaran desde el principio? Por eso se escriben primero, y se ejecutan automticamente con cada construccin del sistema. Sus pruebas se convierten en una extensin de la red de seguridad proporcionada por el lenguaje. Una de las cosas que he descubierto sobre el uso de lenguajes de programacin cada vez ms poderosos es que estoy dispuesto a probar experimentos ms descarados, porque s que el lenguaje me ahorra la prdida de tiempo que supone estar persiguiendo errores. El esquema de pruebas de XP hace lo mismo para el proyecto completo. Como el programador conoce sus pruebas siempre cazar cualquier problema que introduzca (y regularmente se aadirn nuevas pruebas), puede hacer grandes cambios cuando lo necesite sin preocuparse de causar un completo desastre. Eso es increblemente poderoso.
Aunque esto puede ser una perspectiva americana, las historias de Hollywood llegan a todas partes.
18 Incluyendo (especialmente) el sistema PA. Una vez trabaj en una compaa que insista en anunciar pblicamente cada llamada de telfono que llegaba a los ejecutivos, y constantemente interrumpa nuestra productividad (pero los directores no conceban el agobio como un servicio importante de PA). Finalmente, cuando nadie miraba empec a cortar los cables de los altavoces.
51
Captulo 1. Introduccin a los Objetos equipaje que viene con los lenguajes procedurales. Puede ser cierto, a largo plazo. Pero a corto plazo, mucho de este equipaje era valioso. Los elementos ms valiosos podan no estar en el cdigo base existente (el cual, con las herramientas adecuadas, se podra traducir), sino en el conocimiento adquirido. Si usted es un programador C y tiene que tirar todo lo que sabe sobre C para adoptar un nuevo lenguaje, inmediatamente ser mucho menos productivo durante muchos meses, hasta que su mente su ajuste al nuevo paradigma. Mientras que si puede apoyarse en su conocimiento actual de C y ampliarlo, puede continuar siendo productivo con lo que realmente sabe mientras se pasa al mundo de la programacin orientada a objetos. Como todo el mundo tiene su propio modelo mental de la programacin, este cambio es lo sucientemente turbio sin el gasto aadido de volver a empezar con un nuevo modelo de lenguaje. Por eso, la razn del xito de C++, en dos palabras: es econmico. Sigue costando cambiarse a la POO, pero con C++ puede costar menos 19 . La meta de C++ es mejorar la productividad. sta viene por muchos caminos, pero el lenguaje est diseado para ayudarle todo lo posible, y al mismo tiempo dicultarle lo menos posible con reglas arbitrarias o algn requisito que use un conjunto particular de caractersticas. C++ est diseado para ser prctico; las decisiones de diseo del lenguaje C++ estaban basadas en proveer los benecios mximos al programador (por lo menos, desde la visin del mundo de C).
1.11.1. Un C mejor
Se obtiene una mejora incluso si contina escribiendo cdigo C porque C++ ha cerrado muchos agujeros en el lenguaje C y ofrece mejor control de tipos y anlisis en tiempo de compilacin. Est obligado a declarar funciones de modo que el compilador pueda controlar su uso. La necesidad del preprocesador ha sido prcticamente eliminada para sustitucin de valores y macros, que eliminan muchas dicultades para encontrar errores. C++ tiene una caracterstica llamada referencias que permite un manejo ms conveniente de direcciones para argumentos de funciones y retorno de valores. El manejo de nombres se mejora a travs de una caracterstica llamada sobrecarga de funciones, que le permite usar el mismo nombre para diferentes funciones. Una caracterstica llamada namespaces (espacios de nombres) tambin mejora la seguridad respecto a C.
1.11.3. Eciencia
A veces es apropiado intercambiar velocidad de ejecucin por productividad de programacin. Un modelo econmico, por ejemplo, puede ser til slo por un periodo corto de tiempo, pero es ms importante crear el modelo rpidamente. No obstante, la mayora de las aplicaciones requieren algn grado de eciencia, de modo que C++ siempre yerra en la parte de mayor eciencia. Como los programadores de C tienden a ser muy concienzudos con la eciencia, sta es tambin una forma de asegurar que no podrn argumentar que el lenguaje es demasiado pesado y lento. Algunas caractersticas en C++ intentan facilitar el anado del rendimiento cuando el cdigo generado no es lo sucientemente eciente. No slo se puede conseguir el mismo bajo nivel de C (y la capacidad de escribir directamente lenguaje ensamblador dentro de un programa C++), adems la experiencia prctica sugiere que la velocidad para un programa C++ orientado a objetos tiende a ser 10% de un programa escrito en C, y a menudo mucho menos 20 . El diseo producido por un programa POO puede ser realmente ms eciente que el homlogo en C.
19 Dije puede porque, debido a la complejidad de C++, realmente podra ser ms econmico cambiarse a Java. Pero la decisin de qu lenguaje elegir tiene muchos factores, y en este libro asumir que el lector ha elegido C++. 20 Sin embargo, mire en las columnas de Dan Saks en C/C++ Users Journal sobre algunas investigaciones importantes sobre el rendimiento de libreras C++.
52
53
1.12.1. Directrices
Aqu hay algunas pautas a considerar cuando se hace la transicin a POO y C++:
Entrenamiento
El primer paso es algn tipo de estudio. Recuerde la inversin que la compaa tiene en cdigo C, e intente no tenerlo todo desorganizado durante seis o nueve meses mientras todo el mundo alucina con la herencia mltiple. Elija un pequeo grupo para formarlo, preferiblemente uno compuesto de gente que sea curiosa, trabaje bien junta, y pueda funcionar como su propia red de soporte mientras estn aprendiendo C++. Un enfoque alternativo que se sugiere a veces es la enseanza a todos los niveles de la compaa a la vez, incluir una visin general de los cursos para gerentes estratgicos es tan bueno como cursos de diseo y programacin para trabajadores de proyectos. Es especialmente bueno para compaas ms pequeas al hacer cambios fundamentales en la forma en la que se hacen cosas, o en la divisin de niveles en compaas ms grandes. Como el coste es mayor, sin embargo, se puede cambiar algo al empezar con entrenamiento de nivel de proyecto, hacer un proyecto piloto (posiblemente con un mentor externo), y dejar que el equipo de trabajo se convierta en los profesores del resto de la compaa.
1.12. Estrategias de transicin en C++ no es la mejor manera de aprovechar su tiempo. (Si tiene que convertirlo en objetos, puede envolver el cdigo C en clases C++). Hay benecios incrementales, especialmente si es importante reutilizar el cdigo. Pero esos cambios no le van a mostrar los espectaculares incrementos en productividad que espera para sus primeros proyectos a menos que ese proyecto sea nuevo. C++ y la POO destacan ms cuando un proyecto pasa del concepto a la realidad.
Costes iniciales
El coste del cambio a C++ es ms que solamente la adquisicin de compiladores C++ (el compilador GNU de C++, uno de los mejores, es libre y gratuito). Sus costes a medio y largo plazo se minimizarn si invierte en formacin (y posiblemente un mentor para su primer proyecto) y tambin si identica y compra libreras de clases que resuelvan su problema ms que intentar construir las libreras usted mismo. Hay costes que se deben proponer en un proyecto realista. Adems, estn los costes ocultos en prdidas de productividad mientras se aprende el nuevo lenguaje y posiblemente un nuevo entorno de programacin. Formar y orientar puede minimizar ese efecto, pero los miembros del equipo deben superar sus propios problemas para entender la nueva tecnologa. A lo largo del proceso ellos cometern ms errores (esto es una ventaja, porque los errores reconocidos son el modo ms rpido para aprender) y ser menos productivos. Incluso entonces, con algunos tipos de problemas de programacin, las clases correctas y el entorno de programacin adecuado, es posible ser ms productivo mientras se est aprendiendo C++ (incluso considerando que est cometiendo ms errores y escribiendo menos lneas de cdigo por da) que si estuviera usando C.
Cuestiones de rendimiento
Una pregunta comn es, La POO no hace automticamente mis programas mucho ms grandes y lentos? La respuesta es: depende. Los lenguajes de POO ms tradicionales se disearon con experimentacin y prototipado rpido en mente ms que operacin FIXME:lean-and-mean. De esta manera, prcticamente garantiza un incremento signicativo en tamao y una disminucin en velocidad. C++ sin ambargo, est diseado teniendo presente la produccin de programacin. Cuando su objetivo es un prototipado rpido, puede lanzar componentes juntos tan rpido como sea posible ignorando las cuestiones de eciencia. Si est usando una libreras de otros, normalmente ya estn optimizadas por sus vendedores; en cualquier caso no es un problema mientras est en un modo de desarrollo rpido. Cuando tenga el sistema que quiere, si es bastante pequeo y rpido, entonces ya est hecho. Si no, lo puede anar con una herramienta de perlado, mire primero las mejoras que puede conseguir aplicando las caractersticas que incorpora C++. Si esto no le ayuda, mire las modicaciones que se pueden hacer en la implementacin subyacente de modo que no sea necesario necesario cambiar ningn cdigo que utilice una clase particular. nicamente si ninguna otra cosa soluciona el problema necesitar cambiar el diseo. El hecho de que el rendimiento sea tan crtico en esta fase del diseo es un indicador de que debe ser parte del criterio del diseo principal. FIXME:Usar un desarrollo rpido tiene la ventaja de darse cuenta rpidamente. Como se mencion anteriormente, el nmero dado con ms frecuencia para la diferencia en tamao y velocidad entre C y C++ es 10%, y a menudo menor. Incluso podra conseguir una mejora signicativa en tamao y velocidad cuando usa C++ ms que con C porque el diseo que hace para C++ puede ser bastante diferente respecto al que hizo para C. La evidencia entre las comparaciones de tamao y velocidad entre C y C++ tienden a ser anecdticas y es probable que permanezcan as. A pesar de la cantidad de personas que sugiere que una compaa intenta el mismo proyecto usando C y C++, probablemente ninguna compaa quiere perder dinero en el camino a no ser que sea muy grande y est interesada en tales proyectos de investigacin. Incluso entonces, parece que el dinero se
21
55
Captulo 1. Introduccin a los Objetos puede gastar mejor. Casi universalmente, los programadores que se han cambiado de C (o cualquier otro lenguaje procedural) a C++ (o cualquier otro lenguaje de POO) han tenido la experiencia personal de una gran mejora en su productividad de programacin, y es el argumento ms convincente que pueda encontrar.
1.13. Resumen
Este captulo intenta darle sentido a los extensos usos de la programacin orientada a objetos y C++, incluyendo el porqu de que la POO sea diferente, y porqu C++ en particular es diferente, conceptos de metodologa de POO, y nalmente los tipos de cuestiones que encontrar cuando cambie su propia compaa a POO y C++. La POO y C++ pueden no ser para todos. Es importante evaluar sus necesidades y decidir si C++ satisfar de forma ptima sus necesidades, o si podra ser mejor con otros sistemas de programacin (incluido el que utiliza actualmente). Si sabe que sus necesidades sern muy especializadas en un futuro inmediato y tiene restricciones especcas que no se pueden satisfacer con C++, entonces debe investigar otras alternativas 22 . Incluso si nalmente elige C++ como su lenguaje, por lo menos entender qu opciones haba y tendr una visin clara de porqu tom esa direccin. El lector conoce el aspecto de un programa procedural: deniciones de datos y llamadas a funciones. Para encontrar el signicado de un programa tiene que trabajar un poco, revisando las llamadas a funcin y los conceptos de bajo nivel para crear un modelo en su mente. Esta es la razn por la que necesitamos representaciones intermedias cuando diseamos programas procedurales - por eso mismo, estos programas tienden a ser confusos porque los trminos de expresin estn orientados ms hacia la computadora que a resolver el problema. Como C++ aade muchos conceptos nuevos al lenguaje C, puede que su asuncin natural sea que el main() en un programa de C++ ser mucho ms complicado que el equivalente del programa en C. En eso, quedar gratamente sorprendido: un programa C++ bien escrito es generalmente mucho ms simple y mucho ms sencillo de entender que el programa equivalente en C. Lo que ver son las deniciones de los objetos que representan conceptos en el espacio de su problema (en lugar de cuestiones de la representacin en el computador) y mensajes enviados a otros objetos para representar las actividades en este espacio. Ese es uno de los placeres de la programacin orientada a objetos, con un programa bien diseado, es fcil entender el cdigo leyndolo. Normalmente hay mucho menos cdigo, en parte, porque muchos de sus problemas se resolvern utilizando cdigo de libreras existentes.
22
56
2.1.1. Intrpretes
Un intrprete traduce el cdigo fuente en actividades (las cuales pueden comprender grupos de instrucciones mquina) y ejecuta inmediatamente estas actividades. El BASIC, por ejemplo, fue un lenguaje interpretado bastante popular. Los intrpretes de BASIC tradicionales traducen y ejecutan una lnea cada vez, y despus olvidan la lnea traducida. Esto los hace lentos debido a que deben volver a traducir cualquier cdigo que se repita. BASIC tambin ha sido compilado para ganar en velocidad. La mayora de los intrpretes modernos, como los de Python, traducen el programa entero en un lenguaje intermedio que es ejecutable por un intrprete mucho ms rpido 1 . Los intrpretes tienen muchas ventajas. La transicin del cdigo escrito al cdigo ejecutable es casi inmediata, y el cdigo fuente est siempre disponible, por lo que el intrprete puede ser mucho ms especco cuando ocurre un error. Los benecios que se suelen mencionar de los intrpretes es la facilidad de interaccin y el rpido desarrollo (pero no necesariamente ejecucin) de los programas. Los lenguajes interpretados a menudo tienen severas limitaciones cuando se construyen grandes proyectos
Los lmites entre los compiladores y los intrpretes tienden a ser difusos, especialmente con Python, que tiene muchas de las caractristicas y el poder de un lenguaje compilado pero tambin tiene parte de las ventajas de los lenguajes interpretados.
1
57
Captulo 2. Construir y usar objetos (Python parece ser una excepcin). El intrprete (o una versin reducida) debe estar siempre en memoria para ejecutar el cdigo e incluso el intrprete ms rpido puede introducir restricciones de velocidad inaceptables. La mayora de los intrpretes requieren que todo el cdigo fuente se les enve de una sola vez. Esto no slo introduce limitaciones de espacio, sino que puede causar errores difciles de detectar si el lenguaje no incluye facilidades para localizar el efecto de las diferentes porciones de cdigo.
2.1.2. Compiladores
Un compilador traduce el cdigo fuente directamente a lenguaje ensamblador o instrucciones mquina. El producto nal suele ser uno o varios cheros que contienen cdigo mquina. La forma de realizarlo suele ser un proceso que consta de varios pasos. La transicin del cdigo escrito al cdigo ejecutable es signicativamente ms larga con un compilador. Dependiendo de la perspicacia del escritor del compilador, los programas generados por un compilador tienden a requerir mucho menos espacio para ser ejecutados, y se ejecutan mucho ms rpido. Aunque el tamao y la velocidad son probablemente las razones ms citadas para usar un compilador, en muchas situaciones no son las ms importantes. Algunos lenguajes (como el C) estn diseados para admitir trozos de programas compilados independientemente. Estas partes terminan combinando en un programa ejecutable nal mediante una herramienta llamada enlazador (linker). Este proceso se conoce como compilacin separada. La compilacin separada tiene muchos benecios. Un programa que, tomado de una vez, excedera los lmites del compilador o del entorno de compilacin puede ser compilado por piezas. Los programas se pueden ser construir y probar pieza a pieza. Una vez que una parte funciona, se puede guardar y tratarse como un bloque. Los conjuntos de piezas ya funcionales y probadas se pueden combinar en libreras para que otros programadores puedan usarlos. Como se crean piezas, la complejidad de las otras piezas se mantiene oculta. Todas estas caractersticas ayudan a la creacin de programas grandes, 2 . Las caractersticas de depuracin del compilador han mejorado considerablemente con el tiempo. Los primeros compiladores simplemente generaban cdigo mquina, y el programador insertaba sentencias de impresin para ver qu estaba ocurriendo, lo que no siempre era efectivo. Los compiladores modernos pueden insertar informacin sobre el cdigo fuente en el programa ejecutable. Esta informacin se usa por poderosos depuradores a nivel de cdigo que muestran exactamente lo que pasa en un programa rastreando su progreso mediante su cdigo fuente. Algunos compiladores solucionan el problema de la velocidad de compilacin mediante compilacin en memoria. La mayora de los compiladores trabajan con cheros, leyndolos y escribindolos en cada paso de los procesos de compilacin. En la compilacin en memoria el compilador se mantiene en RAM. Para programas pequeos, puede parecerse a un intrprete.
Python vuelve a ser una excepcin, debido a que permite compilacin separada.
58
2.2. Herramientas para compilacin modular para buscar trozos de cdigo que contengan sentencias redundantes de lenguaje ensamblador. Usar la palabra objeto para describir pedazos de cdigo mquina es un hecho desafortunado. La palabra comenz a usarse antes de que la programacin orientada a objetos tuviera un uso generalizado. Objeto signica lo mismo que FIXME:meta en este contexto, mientras que en la programacin orientada a objetos signica una cosa con lmites. El enlazador combina una lista de mdulos objeto en un programa ejecutable que el sistema operativo puede cargar y ejecutar. Cuando una funcin en un mdulo objeto hace una referencia a una funcin o variable en otro mdulo objeto, el enlazador resuelve estas referencias; se asegura de que todas las funciones y los datos externos solicitados durante el proceso de compilacin existen realmente. Adems, el enlazador aade un mdulo objeto especial para realizar las actividades de inicializacin. El enlazador puede buscar en unos archivos especiales llamados libreras para resolver todas sus referencias. Una librera contiene una coleccin de mdulos objeto en un nico chero. Una librera se crea y mantiene por un programa conocido como bibliotecario (librarian).
Captulo 2. Construir y usar objetos Una declaracin presenta un nombre -identicador- al compilador. Le dice al compilador Esta funcin o esta variable existe en algn lugar, y ste es el aspecto que debe tener. Una denicin, sin embargo, dice: Crea esta variable aqu o Crea esta funcin aqu. Eso reserva memoria para el nombre. Este signicado sirve tanto para una variable que para una funcin; en ambos casos, el compilador reserva espacio en el momento de la denicin. Para una variable, el compilador determina su tamao y reserva el espacio en memoria para contener los datos de la variable. Para una funcin, el compilador genera el cdigo que nalmente ocupar un espacio en memoria. Se puede declarar una variable o una funcin en muchos sitios diferentes, pero en C o en C++ slo se puede denir una vez (a se conoce a veces como Regla de Denicin nica (ODR) 3 . Cuando el enlazador une todos los mdulos objeto, normalmente se quejar si encuentra ms de una denicin para la misma funcin o variable. Una denicin puede ser tambin una declaracin. Si el compilador no ha visto antes el nombre x y hay una denicin int x;, el compilador ve el nombre tambin como una declaracin y asigna memoria al mismo tiempo.
La primera palabra reservada es el valor de retorno: int. Los argumentos estn encerrados entre parntesis despus del nombre de la funcin en el orden en que se utilizan. El punto y coma indica el nal de la sentencia; en este caso le dice al compilador esto es todo - aqu no est la denicin de la funcin!. Las declaraciones en C y C++ tratan de mimetizar la forma en que se utilizar ese elemento. Por ejemplo, si a es otro entero la funcin de arriba se debera usar de la siguiente manera:
a = func1(2, 3);
Como func1() devuelve un entero, el compilador de C/C++ comprobar el uso de func1() para asegurarse que a puede aceptar el valor devuelto y que los argumentos son vlidos. Los argumentos de las declaraciones de funciones pueden tener nombres. El compilador los ignora pero pueden ser tilies como nemotcnicos para el usuario. Por ejemplo, se puede declarar func1() con una apariencia diferente pero con el mismo signicado:
int func1(int length, int width);
Una puntualizacin
Existe una diferencia signicativa entre C y el C++ para las funciones con lista de argumentos vaca. En C, la declaracin:
int func2();
signica una funcion con cualquier nmero y tipo de argumentos, lo cual anula la comprobacin de tipos. En C++, sin embargo, signica una funcin sin argumentos.
Denicin de funciones
La denicin de funciones se parece a la declaracin excepto en que tienen cuerpo. Un cuerpo es un conjunto de sentencias encerradas entre llaves. Las llaves indican el comienzo y el nal del cdigo. Para dar a func1() una denicin con un cuerpo vaco (un cuerpo que no contiene cdigo), escriba:
3
60
Note que en la denicin de la funcin las llaves sustituyen el punto y coma. Como las llaves contienen una sentencia o grupo de sentencias, no es necesario un punto y coma. Tenga en cuenta adems que los argumentos en la denicin de la funcin deben nombres si los quiere usar en el cuerpo de la funcin (como aqu no se usan, son opcionales).
podra declarar la variable a como un entero usando la lgica usada anteriormente. Pero aqu est el conicto: existe suciente informacin en el cdigo anterior como para que el compilador pueda crear espacio para un entero llamado a y es exactamente lo que ocurre. Para resolver el dilema, fue necesaria una palabra reservada en C y C++ para decir Esto es slo una declaracin; esta variable estar denida en algn otro lado. La palabra reservada es extern que puede signicar que la denicin es externa al chero, o que la denicin se encuentra despus en este chero. Declarar una variable sin denirla implica usar la palabra reservada extern antes de una descripcin de la variable, como por ejemplo:
extern int a;
extern tambin se puede aplicar a la declaracin de funciones. Para func1() sera algo as:
extern int func1(int length, int width);
Esta sentencia es equivalente a las declaraciones anteriores para func1() . Como no hay cuerpo de funcin, el compilador debe tratarla como una declaracin de funcin en lugar de como denicin. La palabra reservada extern es bastante suprua y opcional para la declaracin de funciones. Probablemente sea desafortunado que los diseadores de C no obligaran al uso de extern para la declaracin de funciones; hubiera sido ms consistente y menos confuso (pero hubiera requerido teclear ms, lo cual probablemente explica la decisin). Aqu hay algunos ejemplos ms de declaraciones:
//: C02:Declare.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Declaration & definition examples extern int i; // Declaration without definition extern float f(float); // Function declaration float b; // Declaration & definition float f(float a) { // Definition return a + 1.0; }
61
En la declaracin de funciones, los identicadores de los argumentos son opcionales. En la denicin son necesarios (los identicadores se requieren solamente en C, no en C++).
hacen que el preprocesador busque el chero como si fuera particular a un proyecto, aunque normalmente hay un camino de bsqueda que se especica en el entorno o en la lnea de comandos del compilador. El mecanismo para cambiar el camino de bsqueda (o ruta) vara entre maquinas, sistemas operativos, e implementaciones de C++ y puede que requiera un poco de investigacin por parte del programador. Los cheros entre comillas dobles, como:
#include "header"
le dicen al preprocesador que busque el chero en (de acuerdo a la especicacin) un medio de denicin de implementacin, que normalmente signica buscar el chero de forma relativa al directorio actual. Si no lo encuentra, entonces la directiva se preprocesada como si tuviera parntesis angulares en lugar de comillas. Para incluir el chero de cabecera iostream, hay que escribir:
#include <iostream>
El preprocesador encontrar el chero de cabecera iostream (a menudo en un subdirectorio llamado include) y lo incluir.
2.2. Herramientas para compilacin modular ocho caracteres y permite eliminar la extensin. Por ejemplo en vez de escribir iostream.h en el estilo antiguo, que se asemejara a algo as:
#include <iostream.h>
El traductor puede implementar la sentencia del include de tal forma que se amolde a las necesidades de un compilador y sistema operativo particular, aunque sea necesario truncar el nombre y aadir una extensin. Evidentemente, tambin puede copiar las cabeceras que ofrece el fabricante de su compilador a otras sin extensiones si quiere usar este nuevo estilo antes de que su fabricante lo soporte. Las libreras heredadas de C an estn disponibles con la extensin tradicional .h. Sin embargo, se pueden usar con el estilo de inclusin ms moderno colocando una c al nombre. Es decir:
#include <stdio.h> #include <stdlib.h>
Se transformara en:
#include <cstdio> #include <cstdlib>
Y as para todas cabeceras del C Estndar. Eso proporciona al lector una distincin interesante entre el uso de libreras C versus C++. El efecto del nuevo formato de include no es idntico al antiguo: usar el .h da como resultado una versin ms antigua, sin plantillas, y omitiendo el .h le ofrece la nueva versin con plantillas. Normalmente podra tener problemas si intenta mezclar las dos formas de inclusin en un mismo programa.
2.2.2. Enlazado
El enlazador (linker) agrupa los mdulos objeto (que a menudo tienen extensiones como .o .obj), generados por el compilador, en un programa ejecutable que el sistema operativo puede cargar y ejecutar. Es la ltima fase del proceso de compilacin. Las caractersticas del enlazador varan de un sistema a otro. En general, simplemente se indican al enlazador los nombres de los mdulos objeto, las libreras que se desean enlazar y el nombre del ejecutable de salida. Algunos sistemas requieren que sea el programador el que invoque al enlazador, aunque en la mayora de los paquetes de C++ se llama al enlazador a travs del compilador. En muchas situaciones, de manera transparente. Algunos enlazadores antiguos no buscaban cheros objeto ms de una vez y buscaban en la lista que se les pasaba de izquierda a derecha. Esto signica que el orden de los cheros objeto y las libreras puede ser importante. Si se encuentra con algn problema misterioso que no aparece hasta el proceso de enlazado, una posible razn es el orden en el que se indican los cheros al enlazador.
Captulo 2. Construir y usar objetos Estos pasos tambin se aplican cuando los mdulos objeto no se combinan para formar una librera. Incluir el chero cabecera y enlazar los mdulos objeto es la base para la compilacin separada en C y en C++.
Aadidos ocultos
Cuando se crea un programa ejecutable en C/C++, ciertos elementos se enlazan en secreto. Uno de estos elementos es el mdulo de arranque, que contiene rutinas de inicializacin que deben ejecutarse cada vez que arranca un programa C o C++. Estas rutinas preparan la pila e inicializan ciertas variables del programa. El enlazador siempre busca la librera estndar para las versiones compiladas de cualquier funcin estndar llamada en el programa. Debido a que se busca siempre en la librera estndar, se puede usar cualquier cosa de esta librera simplemente aadiendo a su programa la cabecera apropiada; no necesita indicar dnde hay que buscar la librera estndar. Las funciones de ujo de entrada-salida (iostream), por ejemplo, estn en la Librera Estndar de C++. Para usarla, slo debe incluir el chero de cabecera <iostream>. Si se est usando una librera, se debe aadir explcitamente su nombre de sta a la lista de cheros manejados por el enlazador.
4 Yo le recomendara usar Perl o Python para automatizar estas tareas como parte de su proceso de empaquetamiento de libreras (ver www.Perl.org www.Python.org).
64
El primer programa usa el concepto de salida estndar, que signica un lugar de propsito general, al que se le pueden enviar cosas. Ver otros ejemplos que utilizan la salida estndar de otras formas, pero aqu simplemente usaremos la consola. El paquete iostream dene una variable (un objeto) llamado cout de forma automtica que es capaz de enviar todo tipo de datos a la salida estndar. Para enviar datos a la salida estndar, se usa el operador <<.Los programadores de C lo conocen como operador de desplazamiento a la izquierda, que se explicar en el siguiente captulo. Baste decir que el desplazamiento a la izquierda no tiene nada que ver con la salida. Sin embargo, C++ permite que los operadores sean sobrecargados. Cuando se sobrecarga un operador, se le da un nuevo signicado siempre que dicho operador se use con un objeto de determinado tipo. Con los objetos de iostream, el operador << signica enviar a. Por ejemplo:
cout << "Qu tal?";
enva la cadena Qu tal? al objeto llamado cout (que es un diminutivo de console output (salida por consola). De momento ya hemos visto suciente sobrecarga de operadores como para poder empezar. El Captulo 12 cubre la sobrecarga de operadores con detalle.
Captulo 2. Construir y usar objetos estndar casi exclusivamente, ver la siguiente directiva using en casi todos los programas.
using namespace std;
Esto signica que quiere usar todos los elementos del espacio de nombres llamado std. Despus de esta sentencia, ya no hay que preocuparse de si su componente o librera particular pertenece a un espacio de nombres, porque la directiva using hace que el espacio de nombres est disponible para todo el chero donde se escribi la directiva using. Exponer todos los elementos de un espacio de nombres despus de que alguien se ha molestado en ocultarlos, parece contraproducente, y de hecho, el lector deber tener cuidado si considera hacerlo (como aprender ms tarde en este libro). Sin embargo, la directiva using expone solamente los nombres para el chero actual, por lo que no es tan drstico como suena al principio. (pero pienselo dos veces antes de usarlo en un chero cabecera, eso es temerario). Existe una relacin entre los espacios de nombres y el modo en que se incluyes los cheros de cabecera. Antes de que se estandarizara la nueva forma de inclusin de los cheros cabecera (sin el .h como en <iostream>), la manera tpica de incluir un chero de cabecera era con el .h como en <iostream.h>. En esa poca los espacios de nombres tampoco eran parte del lenguaje, por lo que para mantener una compatibilidad hacia atrs con el cdigo existente, si se escriba:
#include <iostream.h>
En realidad, signicaba:
#include <iostream> using namespace std;
Sin embargo en este libro se usar la forma estndar de inclusin (sin el .h) y haciendo explcita la directiva using. Por ahora, esto es todo lo que necesita saber sobre los espacios de nombres, pero el Captulo 10 cubre esta materia en profundidad.
La funcin de arriba tiene una lista vaca de argumentos y un cuerpo que contiene nicamente un comentario. Puede haber varios pares de llaves en la denicin de una funcin, pero siempre debe haber al menos dos que envuelvan todo el cuerpo de la funcin. Como main() es una funcin, debe seguir esas reglas. En C++, main() siempre devuelve un valor de tipo int (entero). C y C++ son lenguajes de formato libre. Con un par de excepciones, el compilador ignora los espacios en blanco y los saltos de lnea, por lo que hay que determinar el nal de una sentencia. Las sentencias estn delimitadas por punto y coma. Los comentarios en C empiezan con /* y nalizan con */. Pueden incluir saltos de lnea. C++ permite este 66
2.3. Su primer programa en C++ estilo de comentarios y aade la doble barra inclinada: //. La // empieza un comentario que naliza con el salto de lnea. Es ms til que /* */ y se usa ampliamente en este libro.
El objeto cout maneja una serie de argumentos por medio de los operadores <<, que imprime los argumentos de izquierda a derecha. La funcin especial endl provoca un salto de lnea. Con los iostreams se puede encadenar una serie de argumentos como aqu, lo que hace que se una clase fcil de usar. En C, el texto que se encuentra entre comillas dobles se denomina cadena (string). Sin embargo, la librera Estndar de C++ tiene una poderosa clase llamada string para manipulacin de texto, por lo que usaremos el trmino ms preciso array de caracteres para el texto que se encuentre entre dobles comillas. El compilador pide espacio de memoria para los arrays de caracteres y guarda el equivalente ASCII para cada caracter en este espacio. El compilador naliza automticamente este array de caracteres aadiendo el valor 0 para indicar el nal. Dentro del array de caracteres, se pueden insertar caracteres especiales usando las secuencias de escape. Consisten en una barra invertida (\) seguida de un cdigo especial. por ejemplo \n signica salto de lnea. El manual del compilador o la gua concreta de C ofrece una lista completa de secuencia; entre otras se incluye: \t (tabulador), \\ (barra invertida), y \b (retroceso). Tenga en cuenta que la sentencia puede continuar en otras lneas, y la sentencia completa termina con un punto y coma. Los argumentos de tipo array de caracteres y los nmeros constantes estn mezclados en la sentencia cout anterior. Como el operador << est sobrecargado con varios signicados cuando se usa con cout, se pueden enviar distintos argumentos y cout se encargar de mostrarlos. A lo largo de este libro notar que la primera lnea de cada chero es un comentario (empezando normalmente con //), seguido de dos puntos, y la ltima lnea de cada listado de cdigo acaba con un comentario seguido de /-. Se trata de una una tcnica que uso para extraer fcilmente informacin de los cheros fuente (el programa que lo hace se puede encontrar en el Volumen 2 de este libro, en www.BruceEckel.com). La primera lnea tambin tiene el nombre y localizacin del chero, por lo que se puede localizar fcilmente en los chero de cdigo fuente dele libro (que tambin se puede descargar de www.BruceEckel.com).
67
Captulo 2. Construir y usar objetos Otros compiladores tendrn una sintaxis similar aunque tendr que consultar la documentacin para conocer los detalles particulares.
Este ejemplo muestra cmo la clase iostreams imprime nmeros en decimal, octal, y hexadecimal usando manipuladores (los cuales no imprimen nada, pero cambian el estado del ujo de salida). El formato de los nmeros en punto otante lo determina automticamente el compilador. Adems, cualquier se puede enviar cualquier caracter a un objeto stream usando un molde (cast) a char (un char es un tipo de datos que manipula un slo caracter). Este molde parece una llamada a funcin: char(), devuelve un valor ASCII. En el programa de arriba, el char(27) enva un escape a cout.
68
Al principio, el cdigo de arriba puede parecer errneo porque no est el ya familiar punto y coma al nal de cada lnea. Recuerde que C y C++ son lenguajes de formato libre, y aunque normalmente ver un punto y coma al nal de cada lnea, el requisito real es que haya un punto y coma al nal de cada sentencia, por lo que es posible encontrar una sentencia que ocupe varias lneas.
a decimal number: "; in octal = 0" number << endl; in hex = 0x" number << endl;
Este programa convierte un nmero introducido por el usuario en su representacin octal y hexadecimal.
69
Para usar la funcin system(), hay que pasarle un array de caracteres con la lnea de comandos que se quiere ejecutar en el prompt del sistema operativo. Puede incluir los parmetros que utilizara en la lnea de comandos, y el array de caracteres se puede fabricar en tiempo de ejecucin (en vez de usar un array de caracteres esttico como se mostraba arriba). El comando se ejecuta y el control vuelve al programa. Este programa le muestra lo fcil que es usar C plano en C++; slo incluya la cabecera y utilice la funcin. Esta compatibilidad ascendente entre el C y el C++ es una gran ventaja si est aprendiendo C++ y ya tena conocimientos de C.
Las dos primeras cadenas, s1 y s2 empiezan estando vacas, mientras que s3 y s4 muestran dos formas de inicializar los objetos string con arrays de caracteres (puede inicializar objetos string igual de fcil con otros objetos string). Se puede asignar a un objeto string usando =. Eso sustituye el contenido previo de la cadena con lo que se encuentra en el lado derecho de la asignacin, y no hay que preocuparse de lo que ocurre con el contenido anterior porque se controla automticamente. Para combinar las cadenas simplemente debe usar el operador de suma +, que tambien le permite concatenar cadenas (strings) con arrays de caracteres. Si quiere aadir una cadena o un array de caracteres a otra cadena, puede usar el operador +=. Finalmente, dse cuenta que iostream sabe como tratar las cadenas, por lo que usted puede enviar una cadena (o una expresin que produzca un string, que es lo 70
2.6. Lectura y escritura de cheros que sucede con s1 + s2 + "!">) directamente a cout para imprimirla.
Para abrir los cheros, nicamente debe controlar los nombres de chero que se usan en la creacin de los objetos ifstream y ofstream. Aqu se presenta un nuevo concepto: el bucle while. Aunque ser explicado en detalle en el siguiente captulo, la idea bsica consiste en que la expresin entre parntesis que sigue al while controla la ejecucin de la sentencia siguiente (pueden ser mltiples sentencias encerradas entre llaves). Mientras la expresin entre parntesis (en este caso getline(in, s) produzca un resultado verdadero, las sentencias controladas por el while se ejecutarn. getline() devuelve un valor que se puede interprer como verdadero si se ha leido otra lnea de forma satisfactoria, y falso cuando se llega al nal de la entrada. Eso implica que el while anterior lee todas las lneas del chero de entrada y las enva al chero de salida. getline() lee los caracteres de cada lnea hasta que descubre un salto de lnea (el caracter de terminacin se puede cambiar pero eso no se ver hasta el captulo sobre iostreams del Volumen 2). Sin embargo, descarta el caracter de nueva lnea y no lo almacena en el objeto string. Por lo que si queremos copiar el chero de forma idntica al original, debemos aadir el caracter de nueva lnea como se muestra arriba. Otro ejemplo interesante es copiar el chero entero en un nico objeto string:
Actualmente existen variantes de getline(), que se discutirn profusamente en el captulo de iostreams en el Volumen 2
71
Debido a la naturaleza dinmica de los strings, no hay que preocuparse de la cantidad de memoria que hay qye reservar para el string. Simplemente hay que aadir cosas y el string ir expandindose para dar cabida a lo que le introduzca. Una de las cosas agradables de poner el chero entero en una cadena es que la clase string proporciona funciones para la bsqueda y manipulacin que le permiten modicar el chero como si fuera una simple lnea. Sin embargo, tiene sus limitaciones. Por un lado, a menudo, es conveniente tratar un chero como una coleccin de lneas en vez de un gran bloque de texto. Por ejemplo, si quiere aadir numeracin de lneas es mucho ms fcil si tiene un objeto string distinto para cada lnea. Para realizarlo, necesitamos otro concepto.
2.7. Introduccin a los vectores casos el uso que se hace es adecuado. La clase vector es una plantilla, lo que signica que se puede aplicar a tipos de datos diferentes. Es decir, se puede crear un vector de figuras, un vector de gatos, un vector de strings, etc. Bsicamente, con una plantilla se puede crear un vector de cualquier clase. Para decirle al compilador con qu clase trabajar (en este caso que va a manejar el vector), hay que poner el nombre del tipo deseado entre llaves angulares. Por lo que un vector de string se denota como vector<string>. Con eso, se crea un vector a medida que solamente contendr objetos string, y recibir un mensaje de error del compilador si intenta poner otra cosa en l. Como el vector expresa el concepto de contenedor, debe existir una manera de meter cosas en l y sacar cosas de l. Para aadir un nuevo elemento al nal del vector, se una el mtodo push_back(). Recuerde que, como es un mtodo, hay que usar un . para invocarlo desde un objeto particular. La razn de que el nombre de la funcin parezca un poco verboso - push_back() en vez de algo ms simple como put - es porque existen otros contenedores y otros mtodos para poner nuevos elementos en los contenedores. Por ejemplo, hay un insert() para poner algo en medio de un contenedor. vector la soporta pero su uso es ms complicado y no necesitamos explorarla hasta el segundo volumen del libro. Tambin hay un push_front() (que no es parte de vector) para poner cosas al principio. Hay muchas ms funciones miembro en vector y muchos ms contenedores en la Librera Estndar, pero le sorprender ver la de cosas que se pueden hacer con slo un par de caractersticas bsicas. As que se pueden introducir elementos en un vector con push_back() pero cmo puede sacar esos elementos? La solucin es inteligente y elegante: se usa la sobrecarga de operadores para que el vector se parezca a un array. El array (que ser descrito de forma ms completa en el siguiente captulo) es un tipo de datos que est disponible prcticamente en cualquier lenguaje de programacin por lo que debera estar familiarizado con l. Los arrays son agregados lo que signica que consisten en un nmero de elementos agrupados. La caracterstica distintiva de un array es que estos elementos tienen el mismo tamao y estn organizados uno junto a otro. Y todava ms importante, que se pueden seleccionar mediante un ndice, lo que signica que puede decir: Quiero el elemento nmero n y el elemento ser producido, normalmente de forma rpida. A pesar de que existen excepciones en los lenguajes de programacin, normalmente se indica la indexacin mediante corchetes, de tal forma que si se tiene un array a y quiere obtener el quinto elemento, slo tiene que escribir a[4] (fjese en que la indexacin siempre empieza en cero). Esta forma compacta y poderosa de notacin indexada se ha incorporado al vector mediante la sobrecarga de operadores como el << y el >> de los iostreams. De nuevo, no hay que saber cmo se ha implementado la sobrecarga de operadores - lo dejamos para un captulo posterior - pero es til que sea consciente que hay algo de magia detrs de todo esto para conseguir que los corchetes funcionen con el vector. Con todo esto en mente, ya puede ver un programa que usa la clase vector. Para usar un vector, hay que incluir el chero de cabecera <vector>:
//: C02:Fillvector.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Copy an entire file into a vector of string #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; int main() { vector<string> v; ifstream in("Fillvector.cpp"); string line; while(getline(in, line)) v.push_back(line); // Add the line to the end // Add line numbers: for(int i = 0; i < v.size(); i++) cout << i << ": " << v[i] << endl; }
73
Casi todo este programa es similar al anterior; se abre un chero abierto y se leen las lneas en objetos string (uno cada vez). Sin embargo, estos objetos string se introducen al nal del vector v. Una vez que el bucle while ha terminado, el chero entero se encuentra en memoria dentro de v. La siguiente sentencia en el programa es un bucle for. Es parecido a un bucle while aunque aade un control extra. Como en el bucle while, en el for hay una expresin de control dentro del parntesis. Sin embargo, esta expresin est dividida en tres partes: una parte que inicializa, una que comprueba si hay que salir del bucle, y otra que cambia algo, normalmente da un paso en una secuencia de elementos. Este programa muestra el bucle for de la manera ms habitual: la parte de inicializacin int i = 0 crea un entero i para usarlo como contador y le da el valor inicial de cero. La comprobacin consiste en ver si i es menor que el nmero de elementos del vector v. (Esto se consigue usando la funcin miembro size() -tamao- que hay que admitir que tiene un signicado obvio) El ltimo trozo, usa el operador de autoincremento para aumentar en uno el valor de i. Efectivamente, i++ dice coge el valor de i adele uno y guarad el resultado en i. Conclusin: el efecto del bucle for es aumentar la variable i desde cero hasta el tamao del vector menos uno. Por cada nuevo valor de i se ejecuta la sentencia del cout, que construye un linea con el valor de i (mgicamente convertida a un array de caracteres por cout), dos puntos, un espacio, la lnea del chero y el carcter de nueva lnea que nos proporciona endl. Cuando lo compile y lo ejecute ver el efecto de numeracin de lneas del chero. Debido a que el operador >> funciona con iostreams, se puede modicar fcilmente el programa anterior para que convierta la entrada en palabras separadas por espacios, en vez de lneas:
//: C02:GetWords.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Break a file into whitespace-separated words #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; int main() { vector<string> words; ifstream in("GetWords.cpp"); string word; while(in >> word) words.push_back(word); for(int i = 0; i < words.size(); i++) cout << words[i] << endl; }
La expresin:
while (in >> word)
es la que consigue que se lea una palabra cada vez, y cuando la expresin se evala como falsa signica que ha llegado al nal del chero. De acuerdo, delimitar una palabra mediante caracteres en blanco es un poco tosco, pero sirve como ejemplo sencillo. Ms tarde, en este libro, ver ejemplos ms sosticados que le permiten dividir la entrada de la forma que quiera. Para demostrar lo fcil que es usar un vector con cualquier tipo, aqu tiene un ejemplo que crea un vector de enteros:
//: C02:Intvector.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com
74
2.8. Resumen
// (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Creating a vector that holds integers #include <iostream> #include <vector> using namespace std; int main() { vector<int> v; for(int i = 0; i < 10; i++) v.push_back(i); for(int i = 0; i < v.size(); i++) cout << v[i] << ", "; cout << endl; for(int i = 0; i < v.size(); i++) v[i] = v[i] * 10; // Assignment for(int i = 0; i < v.size(); i++) cout << v[i] << ", "; cout << endl; }
Para crear un vector que maneje un tipo diferente basta con poner el tipo entre las llaves angulares (el argumento de las plantillas). Las plantillas y las libreras de plantillas pretenden ofrecer precisamente esta facilidad de uso. Adems este ejemplo demuestra otra caracterstica esencial del vector en la expresin
v[i] = v[i] * 10;
Puede observar que el vector no est limitado a meter cosas y sacarlas. Tambin puede asignar (es decir, cambiar) cualquier elemento del vector mediante el uso de los corchetes. Eso signica que el vector es un objeto til, exible y de propsito general para trabajar con colecciones de objetos, y haremos uso de l en los siguientes captulos.
2.8. Resumen
Este captulo pretende mostrarle lo fcil que puede llegar a ser la programacin orientada a objetos - si alguien ha hecho el trabajo de denir los objetos por usted. En este caso, slo hay que incluir el chero de cabecera, crear los objetos y enviarles mensajes. Si los tipos que est usando estn bien diseados y son potentes, entonces no tendr mucho trabajo y su programa resultante tambin ser potente. En este proceso para mostrar la sencillez de la POO cuando se usan libreras de clases, este captulo, tambin introduce algunos de los tipos de datos ms bsicos y tiles de la Librera Estndar de C++: La familia de los iostreams (en particular aquellos que leen y escriben en consola y cheros), la clase string, y la plantilla vector. Ha visto lo sencillo que es usarlos y ahora es probable que se imagine la de cosas que se pueden hacer con ellos, pero hay muchas ms cosas que son capaces de realizar 6 . A pesar de estar usando un pequeo subconjunto de la funcionalidad de estas herramientas en este principio del libro, supone un gran avance frente a los rudimentarios comienzos en el aprendizaje de un lenguaje de bajo nivel como C. Y aunque aprender los aspectos de bajo nivel de C es educativo tambin lleva tiempo. Al nal usted es mucho ms productivo si tiene objetos que manejen las caractersticas de bajo nivel. Despus de todo, el principal objetivo de la POO es esconder los detalles para que usted pueda pintar con una brocha ms gorda. Sin embargo, debido al alto nivel que la POO intenta tener, hay algunos aspectos fundamentales de C que no se pueden obviar, y de eso trata el siguiente captulo.
6 Si est especialmente interesado en ver todas las cosas que se pueden hacer con los componentes de la Librera Estndar, vea el Volumen 2 de este libro en www.BruceEckel.com y tambin en www.dinkumware.com
75
2.9. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Modique Hello.cpp para que imprima su nombre y edad (o tamao de pie, o la edad de su perro, si le gusta ms). Compile y ejecute el programa. 2. Utilizando Stream2.cpp y Numconv.cpp como guas, cree un programa que le pida el radio de un crculo y le muestre el rea del mismo. Puede usar el operador * para elevar el radio al cuadrado. No intente imprimir el valor en octal o en hexadecimal (slo funciona con tipos enteros). 3. Cree un programa que abra un chero y cuente las palabras (separadas por espacios en blanco) que contiene. 4. Cree un programa que cuente el nmero de ocurrencias de una palabra en concreto en un chero (use el operador == de la clase string para encontrar la palabra) 5. Cambie Fillvector.cpp para que imprima las lneas al revs (de la ltima a la primera). 6. Cambie Fillvector.cpp para que concatene todos los elementos de la clase vector en un nico string antes de imprimirlo, pero no aada numeracin de lneas 7. Muestre un chero lnea a lnea, esperando que el usuario pulse Enter despus de cada lnea. 8. Cree un vector<float> e introduzca en l 25 nmeros en punto otante usando un bucle for. Muestre el vector. 9. Cree tres objetos vector<float> y rellene los dos primeros como en el ejercicio anterior. Escriba un bucle for que sume los elementos correspondientes y los aada al tercer vector. Muestre los tres vectores. 10. Cree un vector<float> e introduzca 25 nmeros en l como en el ejercicio anterior. Eleve cada nmero al cuadrado y ponga su resultado en la misma posicin del vector. Muestre el vector antes y despus de la multiplicacin.
76
3: C en C++
Como C++ est basado en C, debera estar familiarizado con la sintaxis de C para poder programar en C++, del mismo modo que debera tener una uidez razonable en lgebra para poder hacer clculos.
Si nunca antes ha visto C, este captulo le dar una buena base sobre el estilo de C usado en C++. Si est familiarizado con el estilo de C descrito en la primera edicin de Kernighan & Ritchie (tambin llamado K&R) encontrar algunas caractersticas nuevas o diferentes tanto en C++ como en el estndar C. Si est familiarizado con el estndar C debera echar un vistazo al captulo en busca de las carastersticas particulares de C++. Note que hay algunas caractersticas fundamentales de C++ que se introducen aqu, que son ideas bsicas parecidas a caractersticas de C o a menudo modicaciones en el modo en que C hace las cosas. Las caractersticas ms sosticadas de C++ se explicarn en captulos posteriores Este captulo trata por encima las construciones de C e introduce algunas construcciones bsicas de C++, suponiendo que tiene alguna experiencia programando en otro lenguaje. En el CD-ROM que acompaa a este libro hay una introduccin ms suave a C, titulada Thinking in C: Foundations for Java & C++ de Chuck Alison (publicada por MidView, Inc. y disponible tambin en www.MindView.net). Se trata de un seminario en CD-ROM cuyo objetivo es guiarle cuidadosamente a travs de los fundamentos del lenguaje C. Se concentra en el conceptos necesarios para permitirle pasarse a C++ o a Java, en lugar de intentar convertirle en un experto en todos los oscuros recovecos de C (una de las razones para usar un lenguaje de alto nivel como C++ o Java es precisamente evitar muchos de estos recovecos). Tambin contiene ejercicios y soluciones guiadas. Tenga presente que este captulo va despus del CD Thinking in C, el CD no reemplaza a este captulo, sino que debera tomarse como una preparacin para este captulo y para el libro.
No se puede usar la misma sintaxis para declarar los argumentos en el prototipo de una funcin que en las deniciones ordinarias de variables. Esto signica que no se puede escribir: float x, y, z. Se debe indicar 77
Captulo 3. C en C++ el tipo de cada argumento. En una declaracin de funcin, lo siguiente tambin es correcto:
int translate(float, float, float);
Ya que el compilador no hace ms que chequear los tipos cuando se invoca la funcin, los identicadores se incluyen solamente para mejorar la claridad del cdigo cuando alguien lo est leyendo. En la denicin de la funcin, los nombres son necesarios ya que los argumentos son referenciados dentro de la funcin:
int translate(float x, float x = y = z; // ... } y, float z) {
Esta regla slo se aplica a C. En C++, un argumento puede no tener nombrado en la lista de argumentos de la denicin de la funcin. Como no tiene nombre, no se puede utilizars en el cuerpo de la funcin, por supuesto. Los argumentos sin nombre se permiten para dar al programador una manera de reservar espacio en la lista de argumentos. De cualquier modo, la persona que crea la funcin an as debe llamar a la funcin con los parametros apropiados. Sin embargo, la persona que crea la funcin puede utilizar el argumento en el futuro sin forzar una modicacin en el cdigo que llama a la funcin. Esta opcin de ignorar un argumento en la lista tambin es posible si se indica el nombre, pero siempre aparecera un molesto mensaje de advertencia, informando que el valor no se utiliza, cada vez que se compila la funcin. La advertencia desaparece si se quita el nombre del argumento. C y C++ tienen otras dos maneras de declarar una lista de argumentos. Si se tiene una lista de argumentos vacia, se puede declarar esta como func() en C++, lo que indica al compilador que hay exactamente cero agumentos. Hay que tener en cuenta que esto slo signica una lista de argumentos vaca en C++. En C signica un nmero indeterminado de argumentos (lo que es un agujero en C ya que desabilita la comprobacin de tipos en ese caso). En ambos, C y C++, la declaracion func(void); signica una lista de argumentos vaca. La palabra clave void signica nada en este caso (tambin puede signicar sin tipo en el caso de los punteros, como se ver mas adelante en este captulo). La otra opcin para las listas de argumentos se produce cuando no se sabe cuantos argumentos o qu tipos tendrn los argumentos; esto se conoce como lista de argumentos variable. Esta lista incierta de agumentos se representada con puntos suspensivos (...). Denir una funcin con una lista de argumentos variable es signicativamente ms complicado que denir una funcin normal. Se puede utilizar una lista de argumentos variable para una funcin que tiene un grupo de argumentos jos si (por alguna razn) se quiere deshabilitar la comprobacin del prototipo de funcin. Por eso, se debe restringir el uso de listas de argumentos variables en C y evitarlas en C++ (en el cual, como aprender, hay alternativas mucho mejores). El manejo de listas de argumentos variables se describe en la seccin de libreras de la documentacin de su entorno C particular.
Para devolver un valor desde una funcin, se utiliza la sentencia return. Esta sentencia termina la funcin y salta hasta la sentencia que se halla justo despus de la llamada a la funcin. Si return tiene un argumento, se convierte en el valor de retorno de la funcin. Si una funcin indica que retornara un tipo en particular, entonces cada sentencia return debe retornar un valor de ese tipo. Puede haber ms de una sentencia return en una 78
En cfunc(), el primer if que comprueba que la condicin sea true sale de la funcin con la sentencia return. Fjese que la declaracin de la funcin no es necesaria puesto que la denicin aparece antes de ser utilizada en main(), de modo que el compilador sabe de su existencia desde dicha denicin.
Captulo 3. C en C++ comandos, pero la idea general es la siguiente: si se desea crear una librera, se debe hacer un chero cabecera que contenga prototipos de todas las funciones de la librera. Hay que ubicar este chero de cabecera en alguna parte de la ruta de bsqueda del preprocesador, ya sea en el directorio local (de modo que se podr encontrar mediante #include "header") o bien en el directorio include (por lo que se podr encontrar mediante #include <header>). Luego se han de juntar todos los mdulos objeto y pasarlos al FIXME:bibliotecario junto con un nombre para la librera recin construida (la mayora de los bibliotecrios requieren una extensin comn, como por ejemplo .lib o .a). Se ha de ubicar la librera completa donde residan todas las dems, de manera que el enlazador sabr buscar esas funciones en dicha librera al ser invocadas. Pueden encontrar todos los detalles en su documentacin particular, ya que pueden variar de un sistema a otro.
3.2.2. if-else
La sentencia if-else puede existir de dos formas: con o sin el else. Las dos formas son:
if (expresin) sentencia
La expresin se evala como true o false. La sentencia puede ser una simple acabada en un punto y coma, o bien una compuesta, lo que no es ms que un grupo de sentencias simples encerradas entre llaves. Siempre que se utiliza la palabra sentencia, implica que la sentencia es simple o compuesta. Tenga en cuenta que dicha sentencia puede ser incluso otro if, de modo que se pueden anidar.
//: C03:Ifthen.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Demonstration of if and if-else conditionals #include <iostream> using namespace std; int main() { int i;
80
Por convenio se indenta el cuerpo de una sentencia de control de ujo, de modo que el lector puede determinar fcilmente donde comienza y dnde acaba 1 .
3.2.3. while
En los bucles de control while, do-while, y for, una sentencia se repite hasta que la expresin de control sea false. La estructura de un bucle while es:
while(expresin) sentencia
La expresin se evalua una vez al comienzo del bucle y cada vez antes de cada iteracin de la sentencia. Este ejemplo se mantiene en el cuerpo del bucle while hasta que introduzca el nmero secreto o presione Control-C.
//: C03:Guess.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Guess a number (demonstrates "while") #include <iostream> using namespace std; int main() { int secret = 15; int guess = 0; // "!=" is the "not-equal" conditional: while(guess != secret) { // Compound statement cout << "guess the number: "; cin >> guess; } cout << "You guessed it!" << endl; }
1 Fjese en que todas las convenciones parecen acabar estando de acuerdo en que hay que hacer algn tipo de indentacin. La pelea entre los estilos de formateo de cdigo no tiene n. En el Apndice A se explica el estilo de codicacin que se usa en este libro.
81
Captulo 3. C en C++ La expresin condicional del while no est restringida a una simple prueba como en el ejemplo anterior; puede ser tan complicada como se desee siempre y cuando se produzca un resultado true o false. Tambin puede encontrar cdigo en el que el bucle no tiene cuerpo, slo un simple punto y coma:
while(/* hacer muchas cosas */) ;
En estos casos, el programador ha escrito la expresin condicional no slo para realizar la evaluacin, sino tambin para hacer el trabajo.
3.2.4. do-while
El aspecto de do-while es
do sentencia while(expresin);
El do-while es diferente del while ya que la sentencia siempre se ejecuta al menos una vez, an si la expresin resulta false la primera vez. En un while normal, si la condicion es falsa la primera vez, la sentencia no se ejecuta nunca. Si se utiliza un do-while en Guess.cpp, la variable guess no necesitara un valor cticio inicial, ya que se inicializa por la sentencia cin antes de que la variable sea evaluada:
//: C03:Guess2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The guess program using do-while #include <iostream> using namespace std; int main() { int secret = 15; int guess; // No initialization needed here do { cout << "guess the number: "; cin >> guess; // Initialization happens } while(guess != secret); cout << "You got it!" << endl; }
For alguna razn, la mayora de los programadores tienden a evitar el do-while y se limitan a trabajar con while.
3.2.5. for
Un bucle for realiza una inicializacin antes de la primera iteracin. Luego ejecuta una evaluacin condicional y, al nal de cada iteracin, efecta algn tipo de siguiente paso. La estructura del bucle for es:
for(initializacin; condicional; paso) sentencia
82
3.2. Control de ujo Cualquiera de las expresiones de inicializacin, condicional, o paso pueden estar vacas. El cdigo de inicializacin se ejecuta una nica vez al principio. La expresin condicional se evala antes de cada iteracin (si se evala a false desde el principio, el cuerpo del bucle nunca llega a ejecutarse). Al nal de cada iteracin del bucle, se ejecuta paso. Los bucles for se utilizan generalmente para tareas de conteo:
//: C03:Charlist.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Display all the ASCII characters // Demonstrates "for" #include <iostream> using namespace std; int main() { for(int i = 0; i < 128; i = i + 1) if (i != 26) // ANSI Terminal Clear screen cout << " value: " << i << " character: " << char(i) // Type conversion << endl; }
Puede ocurrir que la variable i sea denida en el punto en el que se utiliza, en vez de al principio del bloque delimitado por la apertura de la llave {. Esto diere de los lenguajes procedurales tradicionales (incluyendo C), en los que se requiere que todas las variables se denan al principio del bloque. Esto se discutir ms adelante en este captulo.
83
Captulo 3. C en C++
cin >> c; if(c == a) { cout << "you chose a" << endl; continue; // Back to main menu } if(c == b) { cout << "you chose b" << endl; continue; // Back to main menu } else { cout << "you didnt choose a or b!" << endl; continue; // Back to main menu } } if(c == r) { cout << "RIGHT MENU:" << endl; cout << "select c or d: "; cin >> c; if(c == c) { cout << "you chose c" << endl; continue; // Back to main menu } if(c == d) { cout << "you chose d" << endl; continue; // Back to main menu } else { cout << "you didnt choose c or d!" << endl; continue; // Back to main menu } } cout << "you must type l or r or q!" << endl; } cout << "quitting menu..." << endl; }
Si el usuario selecciona q en el menu principal, se utiliza la palabra reservada break para salir, de otro modo, el programa contina ejecutndose indenidamente. Despus de cada seleccin de sub-menu, se usa la palabra reservada continue para volver atrs hasta el comienzo del bucle while. La sentencia while(true) es el equivalente a decir haz este bucle para siempre. La sentencia break permite romper este bucle innito cuando el usuario teclea q.
3.2.7. switch
Una sentencia switch selecciona un fragmento de cdigo entre varios posibles en base al valor de una expresin entera. Su estructura es:
switch(selector) { case valor-entero1 : case valor-entero2 : case valor-entero3 : case valor-entero4 : case valor-entero5 : (...) default: sentencia; }
84
3.2. Control de ujo selector es una expresin que produce un valor entero. El switch compara el resultado de selector para cada valor entero. Si encuentra una coincidencia, se ejecutar la sentencia correspondiente (sea simple o compuesta). Si no se encuentra ninguna coincidencia se ejecutar la sentencia default. Se puede obeservar en la denicin anterior que cada case acaba con un break, lo que causa que la ejecucin salte hasta el nal del cuerpo del switch (la llave nal que cierra el switch). Esta es la forma convencional de construir una sentencia switch, pero la palabra break es opcional. Si no se indica, el case que se ha cumplido cae al siguiente de la lista. Esto signica, que el cdigo del siguiente case, se ejecutara hasta que se encuentre un break. Aunque normalmente no se desea este tipo de comportamiento, puede ser de ayuda para un programador experimentado. La sentencia switch es una manera limpia de implementar una seleccin multi-modo (por ejemplo, seleccionando de entre un nmero de paths de ejecucin), pero reequire un selector que pueda evaluarse como un entero en el momento de la compilacin. Si quisiera utilizar, por ejemplo, un objeto string como selector, no funcionar en una sentencia switch. Para un selector de tipo string, se debe utilizar una serie de sentencias if y comparar el string dentro de la condicin. El ejemplo del menu demostrado anteriormente proporciona un ejemplo particularmente interesante de un switch:
//: C03:Menu2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // A menu using a switch statement #include <iostream> using namespace std; int main() { bool quit = false; // Flag for quitting while(quit == false) { cout << "Select a, b, c or q to quit: "; char response; cin >> response; switch(response) { case a : cout << "you chose a" << endl; break; case b : cout << "you chose b" << endl; break; case c : cout << "you chose c" << endl; break; case q : cout << "quitting menu" << endl; quit = true; break; default : cout << "Please use a,b,c or q!" << endl; } } }
El ag quit es un bool, abreviatura para booleano, que es un tipo que slo se encuentra en C++. Puede tener unicamente los valores true o false. Seleccionando q se asigna el valor true al ag quit. La prxima vez que el selector sea evaluado, quit == false retornar false de modo que el cuerpo del bucle while no se ejecutar.
Captulo 3. C en C++ un problema que no puede ser resuelto de otra manera, pero, an as, se debe considerer cuidadosamente. A continuacin aparece un ejemplo que puede ser un candidato plausible:
//: C03:gotoKeyword.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The infamous goto is supported in C++ #include <iostream> using namespace std; int main() { long val = 0; for(int i = 1; i < 1000; i++) { for(int j = 1; j < 100; j += 10) { val = i * j; if(val > 47000) goto bottom; // Break would only go to the outer for } } bottom: // A label cout << val << endl; }
La alternativa sera dar valor a un booleano que sea evaluado en el for externo, y luego hacer un break desde el for interno. De todos modos, si hay demasiados niveles de for o while esto puede llegar a ser pesado.
3.2.9. Recursividad
La recursividad es una tcnica de programacin interesante y a veces til, en donde se llama a la funcin desde el cuerpo de la propia funcin. Por supuesto, si eso es todo lo que hace, se estara llamando a la funcin hasta que se acabase la memoria de ejecucin, de modo que debe existir una manera de escaparse de la llamada recursiva. En el siguiente ejemplo, esta escapada se consigue simplemente indicando que la recursin slo continuar hasta que cat exceda Z: 2
//: C03:CatsInHats.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Simple demonstration of recursion #include <iostream> using namespace std; void removeHat(char cat) { for(char c = A; c < cat; c++) cout << " "; if(cat <= Z) { cout << "cat " << cat << endl; removeHat(cat + 1); // Recursive call } else cout << "VOOM!!!" << endl; } int main() { removeHat(A);
2
86
En removeHat(), se puede ver que mientras cat sea menor que Z, removeHat() se llamar a s misma, efectuando as la recursividad. Cada vez que se llama removeHat(), su argumento crece en una unidad ms que el cat actual de modo que el argumento contina aumentando. La recursividad a menudo se utiliza cuando se evala algn tipo de problema arbitrariamente complejo, ya que no se restringe la solucin a nign tamao particular - la functin puede simplemente efecutar la recursividad hasta que se haya alcanzado el nal del problema.
3.3.1. Precedencia
La precedencia de operadores dene el orden en el que se evala una expresin con varios operadores diferentes. C y C++ tienen reglas especcas para determinar el orden de evaluacin. Lo ms fcil de recordar es que la multiplicacin y la divisin se ejecutan antes que la suma y la resta. Luego, si una expresin no es transparente al programador que la escribe, probablemente tampoco lo ser para nadie que lea el cdigo, de modo que se deben usar parntesis para hacer explcito el orden de la evaluacin. Por ejemplo:
A = X + Y - 2/2 + Z;
Tiene un signicado muy distinto de la misma expresin pero con un conguracin de parntesis particular:
A = X + (Y - 2)/(2 + Z);
87
Captulo 3. C en C++
// Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Shows use of auto-increment // and auto-decrement operators. #include <iostream> using namespace std; int main() { int i = 0; int j = 0; cout << ++i cout << j++ cout << --i cout << j-}
// // // //
Si se ha estado preguntando acerca del nombre C++, ahora lo entiender. Signica un paso ms all de C
3
88
int main() { // Definition without initialization: char protein; int carbohydrates; float fiber; double fat; // Simultaneous definition & initialization: char pizza = A, pop = Z; int dongdings = 100, twinkles = 150, heehos = 200; float chocolate = 3.14159; // Exponential notation: double fudge_ripple = 6e-4; }
La primera parte del programa dene variables de los cuatro tipos bsicos sin inicializarlas. Si no se inicializa una variable, el Estndar dice que su contenido es indenido (normalmente, esto signica que contienen basura). La segunda parte del programa dene e inicializa variables al mismo tiempo (siempre es mejor, si es posible, dar un valor inicial en el momento de la denicin). Note que el uso de notacin exponencial en la contante 6e-4, signica 6 por 10 elevado a -4.
TABLA 3.1 Expresiones que utilizan booleanos Elemento && || ! < > <= >= == != if, for, while, do ?: Uso con booleanos Toman argumentos booleanos y producen valores bool Producen resultados bool Las expresiones condicionales se convierten en valores bool El primer operando se convierte a un valor bool
Como hay mucho cdigo existente que utiliza un int para representar una bandera, el compilador lo convertir implcitamente de int a bool (los valores diferentes de cero producirn true, mientras que los valores cero, producirn false). Idealmente, el compilador le dar un aviso como una sugerencia para corregir la situacin. Un modismo que se considera estilo de programacin pobre es el uso de ++ para asignar a una bandera el valor true. Esto an se permite, pero est obsoleto, lo que implica que en el futuro ser ilegal. El problema es que se est haciendo una conversin implcita de un bool a un int, incrementando el valor (quiz ms all del rango de valores booleanos cero y uno), y luego implcitamente convirtindolo otra vez a bool. Los punteros (que se describen ms adelante en este capitulo) tambin se convierten automticamente a bool cuando es necesario. 89
Captulo 3. C en C++
3.4.3. Especicadores
Los especicadores modican el signicado de los tipos predenidos bsicos y los expanden a un conjunto ms grande. Hay cuatro especicadores: long, short, signed y unsigned. long y short modican los valores mximos y mnimos que un tipo de datos puede almacenar. Un int plano debe tener al menos el tamao de un short. La jerarqua de tamaos para tipos enteros es: short int, int, long int. Todos pueden ser del mismo tamao, siempre y cuando satisfagan los requisitos de mnimo/mximo. En una maquina con una palabra de 64 bits, por defecto, todos los tipos de datos podran ser de 64 bits. La jerarqua de tamao para los nmeros en coma otante es: oat, double y long double. long oat no es un tipo vlido. No hay nmeros en coma otantes de tamao short. Los especicadores signed y unsigned indican al compilador cmo utilizar el bit del signo con los tipos enteros y los caracteres (los nmeros de coma otante siempre contienen un signo). Un nmero unsigned no guarda el valor del signo y por eso tiene un bit extra disponible, de modo que puede guardar el doble de nmeros positivos que pueden guardarse en un nmero signed. signed se supone por defecto y slo es necesario con char, char puede ser o no por defecto un signed. Especicando signed char, se est forzando el uso del bit del signo. El siguiente ejemplo muestra el tamao de los tipos de datos en bytes utilizando el operador sizeof, descripto ms adelante en ese captulo:
//: C03:Specify.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Demonstrates the use of specifiers #include <iostream> using namespace std; int main() { char c; unsigned char cu; int i; unsigned int iu; short int is; short iis; // Same as short int unsigned short int isu; unsigned short iisu; long int il; long iil; // Same as long int unsigned long int ilu; unsigned long iilu; float f; double d; long double ld; cout << "\n char= " << sizeof(c) << "\n unsigned char = " << sizeof(cu) << "\n int = " << sizeof(i) << "\n unsigned int = " << sizeof(iu) << "\n short = " << sizeof(is) << "\n unsigned short = " << sizeof(isu) << "\n long = " << sizeof(il) << "\n unsigned long = " << sizeof(ilu) << "\n float = " << sizeof(f) << "\n double = " << sizeof(d) << "\n long double = " << sizeof(ld) << endl; }
90
3.4. Introduccin a los tipos de datos Tenga en cuenta que es probable que los resultados que se consiguen ejecutando este programa sean diferentes de una maquina/sistema operativo/compilador a otro, ya que (como se mencionaba anteriormente) lo nico que ha de ser consistente es que cada tipo diferente almacene los valores mnimos y mximos especicados en el Estndar. Cuando se modica un int con short o long, la palabra reservada int es opcional, como se muestra a continuacin.
Cada uno de los elementos de este programa tiene una localizacin en memoria mientras el programa se est ejecutando. Incluso las funciones ocupan espacio. Como ver, se da por sentado que el tipo de un elemento y la forma en que se dene determina normalmente el rea de memoria en la que se ubica dicho elemento. Hay un operador en C y C++ que permite averiguar la direccin de un elemento. Se trata del operador &. Slo hay que anteponer el operador & delante del nombre identicador y obtendr la direccin de ese identicador. Se puede modicar YourPets1.cpp para mostrar las direcciones de todos sus elementos, del siguiente modo:
//: C03:YourPets2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int dog, cat, bird, fish; void f(int pet) { cout << "pet id number: " << pet << endl; }
91
Captulo 3. C en C++
int main() { int i, j, k; cout << "f(): " << (long)&f << endl; cout << "dog: " << (long)&dog << endl; cout << "cat: " << (long)&cat << endl; cout << "bird: " << (long)&bird << endl; cout << "fish: " << (long)&fish << endl; cout << "i: " << (long)&i << endl; cout << "j: " << (long)&j << endl; cout << "k: " << (long)&k << endl; }
El (long) es una molde. Indica No tratar como su tipo normal, sino como un long. El molde no es esencial, pero si no existiese, las direcciones apareceran en hexadecimal, de modo que el moldeado a long hace las cosas ms legibles. Los resultados de este programa variarn dependiendo del computador, del sistema operativo, y de muchos otros tipos de factores, pero siempre darn un resultado interesante. Para una nica ejecucin en mi computador, los resultados son como estos:
f(): 4198736 dog: 4323632 cat: 4323636 bird: 4323640 fish: 4323644 i: 6684160 j: 6684156 k: 6684152
Se puede apreciar como las variables que se han denido dentro de main() estn en un rea distinta que las variables denidas fuera de main(); entender el porque cuando se profundice ms en el lenguaje. Tambin, f() parece estar en su propia rea; el cdigo normalmente se separa del resto de los datos en memoria. Otra cosa a tener en cuenta es que las variables denidas una a continuacin de la otra parecen estar ubicadas de manera contigua en memoria. Estn separadas por el nmero de bytes requeridos por su tipo de dato. En este programa el nico tipo de dato utilizado es el int, y la variable cat est separada de dog por cuatro bytes, bird est separada por cuatro bytes de cat, etc. De modo que en el computador en que ha sido ejecutado el programa, un entero ocupa cuatro bytes. Qu se puede hacer con las direcciones de memoria, adems de este interesante experimento de mostrar cuanta memoria ocupan? Lo ms importante que se puede hacer es guardar esas direcciones dentro de otras variables para su uso posterior. C y C++ tienen un tipo de variable especial para guardar una direccin. Esas variables se llaman punteros. El operador que dene un puntero es el mismo que se utiliza para la multiplicacin: *. El compilador sabe que no es una multiplicacin por el contexto en el que se usa, tal como podr comprobar. Cuando se dene un puntero, se debe especicar el tipo de variable al que apunta. Se comienza dando el nombre de dicho tipo, despus en lugar de escribir un identicador para la variable, usted dice Espera, esto es un puntero insertando un asterisco entre el tipo y el identicador. De modo que un puntero a int tiene este aspecto:
int* ip; // ip apunta a una variable int
La asociacin del * con el tipo parece prctica y legible, pero puede ser un poco confusa. La tendencia podra ser decir puntero-entero como un si fuese un tipo simple. Sin embargo, con un int u otro tipo de datos bsico, se puede decir:
int a, b, c;
La sintaxis de C (y por herencia, la de C++) no permite expresiones tan cmodas. En las defniniciones anteriores, slo ipa es un puntero, pero ipb e ipc son ints normales (se puede decir que * est mas unido al identicador). Como consecuencia, los mejores resultados se pueden obtener utilizando slo una denicin por lnea; y an se conserva una sintaxis cmoda y sin la confusin:
int* ipa; int* ipb; int* ipc;
Ya que una pauta de programacin de C++ es que siempre se debe inicializar una variable al denirla, realmente este modo funciona mejor. Por ejemplo, Las variables anteriores no se inicializan con ningn valor en particular; contienen basura. Es ms fcil decir algo como:
int a = 47; int* ipa = &a;
Ahora tanto a como ipa estn inicializadas, y ipa contiene la direccin de a. Una vez que se inicializa un puntero, lo ms bsico que se puede hacer con l es utilizarlo para modicar el valor de lo que apunta. Para acceder a la variable a travs del puntero, se dereferencia el puntero utilizando el mismo operador que se us para denirlo, como sigue:
*ipa = 100;
Ahora a contiene el valor 100 en vez de 47. Estas son las normas bsicas de los punteros: se puede guardar una direccin, y se puede utilizar dicha direccin para modicar la variable original. Pero la pregunta an permanece: por qu se querra cambiar una variable utilizando otra variable como intermediario? Para esta visin introductoria a los punteros, podemos dividir la respuesta en dos grandes categoras: 1. Para cambiar objetos externos desde dentro de una funcin. Esto es quizs el uso ms bsico de los punteros, y se examinar ms adelante. 2. Para conseguir otras muchas tcnicas de programacin ingeniosas, sobre las que aprender en el resto del libro.
93
Captulo 3. C en C++
} int main() { int x = 47; cout << "x = " << x << endl; f(x); cout << "x = " << x << endl; }
En f(), a es una variable local, de modo que existe nicamente mientras dura la llamada a la funcin f(). Como es un argumento de una funcin, el valor de a se inicializa mediante los argumentos que se pasan en la invocacin de la funcin; en main() el argumento es x, que tiene un valor 47, de modo que el valor es copiado en a cuando se llama a f(). Cuando ejecute el programa ver:
x a a x = = = = 47 47 5 47
Por supuesto, inicialmente x es 47. Cuando se llama f(), se crea un espacio temporal para alojar la variable a durante la ejecucin de la funcin, y el valor de x se copia a a, el cual es vericado mostrndolo por pantalla. Se puede cambiar el valor de a y demostrar que ha cambiado. Pero cuando f() termina, el espacio temporal que se haba creado para a desaparece, y se puede observar que la nica conexin que exista entre a y x ocurri cuando el valor de x se copi en a. Cuando est dentro de f(), x es el objeto externo (mi terminologa), y cambiar el valor de la variable local no afecta al objeto externo, lo cual es bastante lgico, puesto que son dos ubicaciones separadas en la memoria. Pero y si quiere modicar el objeto externo? Aqu es donde los punteros entran en accin. En cierto sentido, un puntero es un alias de otra variable. De modo que si a una funcin se le pasa un puntero en lugar de un valor ordinario, se est pasando de hecho un alias del objeto externo, dando la posibilidad a la funcin de que pueda modicar el objeto externo, tal como sigue:
//: C03:PassAddress.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; void f(int* p) { cout << "p = " << p << endl; cout << "*p = " << *p << endl; *p = 5; cout << "p = " << p << endl; } int main() { int x = 47; cout << "x = " << x << endl; cout << "&x = " << &x << endl; f(&x); cout << "x = " << x << endl; }
Ahora f() toma el puntero como un argumento y dereferencia el puntero durante la asignacin, lo que modica el objeto externo x. La salida es: 94
Tenga en cuenta que el valor contenido en p es el mismo que la direccin de x - el puntero p de hecho apunta a x. Si esto no es sucientemente convincente, cuando p es dereferenciado para asignarle el valor 5, se ve que el valor de x cambia a 5 tambin. De ese modo, pasando un puntero a una funcin le permitir a esa funcin modicar el objeto externo. Se vern muchos otros usos de los punteros ms adelante, pero podra decirse que ste es el ms bsico y posiblemente el ms comn.
En la lista de argumentos de f(), en lugar de escribir int* para pasar un puntero, se escribe int& para pasar una referencia. Dentro de f(), si dice simplemente r (lo cual producira la direccin si r fuese un puntero) se obtiene el valor en la variable que r est referenciando. Si se asigna a r, en realidad se esta asignado a la variable a la que que r referencia. De hecho, la nica manera de obtener la direccin que contiene r es con el operador &. 95
Captulo 3. C en C++ En main(), se puede ver el efecto clave de las referencias en la sintaxis de la llamada a f(), que es simplemente f(x). Aunque eso parece un paso-por-valor ordinario, el efecto de la referencia es que en realidad toma la direccin y la pasa, en lugar de hacer una copia del valor. La salida es:
x = 47 &x = 0065FE00 r = 47 &r = 0065FE00 r = 5 x = 5
De manera que se puede ver que un paso-por-referencia permite a una funcin modicar el objeto externo, al igual que al pasar un puntero (tambin se puede observar que la referencia esconde el hecho de que se est pasando una direccin; esto se ver ms adelante en el libro). Gracias a esta pequea introduccin se puede asumir que las referencias son slo un modo sintcticamente distinto (a veces referido como azcar sintctico) para conseguir lo mismo que los punteros: permitir a las funciones cambiar los objetos externos.
Los punteros y las referencias entran en juego tambin cuando se pasan objetos dentro y fuera de las funciones; aprender sobre ello en un captulo posterior. Hay otro tipo que funciona con punteros: void. Si se establece que un puntero es un void*, signica que cualquier tipo de direccin se puede asignar a ese puntero (en cambio si tiene un int*, slo puede asignar la direccin de una variable int a ese puntero). Por ejemplo: 96
3.5. Alcance
//: C03:VoidPointer.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { void* vp; char c; int i; float f; double d; // The address of ANY type can be // assigned to a void pointer: vp = &c; vp = &i; vp = &f; vp = &d; }
Una vez que se asigna a un void* se pierde cualquier informacin sobre el tipo de la variables. Esto signica que antes de que se pueda utilizar el puntero, se debe moldear al tipo correcto:
//: C03:CastFromVoidPointer.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { int i = 99; void* vp = &i; // Cant dereference a void pointer: // *vp = 3; // Compile-time error // Must cast back to int before dereferencing: *((int*)vp) = 3; }
El molde (int*)vp toma el void* y le dice al compilador que lo trate como un int*, y de ese modo se puede dereferenciar correctamente. Puede observar que esta sintaxis es horrible, y lo es, pero es peor que eso - el void* introduce un agujero en el sistema de tipos del lenguaje. Eso signica, que permite, o incluso promueve, el tratamiento de un tipo como si fuera otro tipo. En el ejemplo anterior, se trata un int como un int mediante el moldeado de vp a int*, pero no hay nada que indique que no se lo puede moldear a char* o double*, lo que modicara una cantidad diferente de espacio que ha sido asignada al int, lo que posiblemente provocar que el programa falle.. En general, los punteros void deberan ser evitados, y utilizados nicamente en raras ocasiones, que no se podrn considerar hasta bastante ms adelante en el libro. No se puede tener una referencia void, por razones que se explicarn en el captulo 11.
3.5. Alcance
Las reglas de mbitos dicen cuando es vlida una variable, dnde se crea, y cundo se destruye (es decir, sale de mbito). El mbito de una variable se extiende desde el punto donde se dene hasta la primera llave que empareja con la llave de apertura antes de que la variable fuese denida. Eso quiere decir que un mbito se dene por su juego de llaves ms cercanas. Para ilustrarlo:
//: C03:Scope.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com
97
Captulo 3. C en C++
// (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // How variables are scoped int main() { int scp1; // scp1 visible here { // scp1 still visible here //..... int scp2; // scp2 visible here //..... { // scp1 & scp2 still visible here //.. int scp3; // scp1, scp2 & scp3 visible here // ... } // <-- scp3 destroyed here // scp3 not available here // scp1 & scp2 still visible here // ... } // <-- scp2 destroyed here // scp3 & scp2 not available here // scp1 still visible here //.. } // <-- scp1 destroyed here
El ejemplo anterior muestra cundo las variables son visibles y cuando dejan de estr disponibles (es decir, cuando salen del mbito). Una variable se puede utilizar slo cuando se est dentro de su mbito. Los mbitos pueden estar anidados, indicados por parejas de llaves dentro de otras parejas de llaves. El anidado signica que se puede acceder a una variable en un mbito que incluye el mbito en el que se est. En el ejemplo anterior, la variable scp1 est dispobible dentro de todos los dems mbitos, mientras que scp3 slo est disponible en el mbito ms interno.
En el mbito ms interno, se dene p antes de que acabe el mbito, de modo que realmente es un gesto intil (pero demuestra que se puede denir una variable en cualquier sitio). La variable p en el mbito exterior est en la misma situacin. La denicin de i en la expresin de control del bucle for es un ejemplo de que es posible denir una variable exactamente en el punto en el que se necesita (esto slo se puede hacer en C++). El mbito de i es el mbito de la expresin controlada por el bucle for, de modo que se puede re-utilizar i en el siguiente bucle for. Se trata de un modismo conveniente y comn en C++; i es el nombre habitual para el contador de un for y as no hay que inventar nombres nuevos. A pesar de que el ejemplo tambin muestra variables denidas dentro de las sentencias while, if y switch, este tipo de deniciones es menos comn que las de expresiones for, quizs debido a que la sintaxis es ms restrictiva. Por ejemplo, no se puede tener ningn parntesis. Es decir, que no se puede indicar:
while((char c = cin.get()) != q)
Aadir los parntesis extra parecera una accin inocente y til, y debido a que no se pueden utilizar, los resultados no son los esperados. El problema ocurre porque != tiene orden de precedencia mayor que =, de modo que el char c acaba conteniendo un bool convertido a char. Cuando se muestra, en muchos terminales se vera el carcter de la cara sonriente. 99
Captulo 3. C en C++ En general, se puede considerar la posibilidad de denir variables dentro de las sentencias while, if y switch por completitud, pero el nico lugar donde se debera utilizar este tipo de denicin de variables es en el bucle for (dnde usted las utilizar ms a menudo).
El espacio para la variable globe se crea mediante la denicin en Global.cpp, y esa misma variable es accedida por el cdigo de Global2.cpp. Ya que el cdigo de Global2.cpp se compila separado del cdigo de Global.cpp, se debe informar al compilador de que la variable existe en otro sitio mediante la declaracin
extern int globe;
100
3.6. Especicar la ubicacin del espacio de almacenamiento Cuando ejecute el programa, observar que la llamada fun() afecta efectivamente a la nica instancia global de globe. En Global.cpp, se puede ver el comentario con una marca especial (que es dise mo): //{L} Global2 Eso indica que para crear el programa nal, el chero objeto con el nombre Global2 debe estar enlazado (no hay extensin ya que los nombres de las extensiones de los cheros objeto dieren de un sistema a otro). En Global2.cpp, la primera lnea tiene otra marca especial {O}, que signica No intentar crear un ejecutable de este chero, se compila para que pueda enlazarse con otro chero. El programa ExtractCode.cpp en el Volumen 2 de este libro (que se puede descargar de www.BruceEckel.com) lee estas marcas y crea el makefile apropiado de modo que todo se compila correctamente (aprender sobre makeles al nal de este captulo).
Variables registro
Una variable registro es un tipo de variable local. La palabra reservada register indica al compilador Haz que los accesos a esta variable sean lo ms rpidos posible. Aumentar la velocidad de acceso depende de la implementacin, pero, tal como sugiere el nombre, a menudo se hace situando la variable en un registro del microprocesador. No hay garanta alguna de que la variable pueda ser ubicada en un registro y tampoco de que la velocidad de acceso aumente. Es una ayuda para el compilador. Hay restricciones a la hora de utilizar variables registro. No se puede consular o calcular la direccin de una variable registro. Una variable registro slo se puede declarar dentro de un bloque (no se pueden tener variables de registro globales o estticas). De todos modos, se pueden utilizar como un argumento formal en una funcin (es decir, en la lista de argumentos). En general, no se debera intentar inuir sobre el optimizador del compilador, ya que probablemente l har mejor el trabajo de lo que lo pueda hacer usted. Por eso, es mejor evitar el uso de la palabra reservada register.
3.6.3. Static
La palabra reservada static tiene varios signicados. Normalmente, las variables denidas localmente a una funcin desaparecen al nal del mbito de sta. Cuando se llama de nuevo a la funcin, el espacio de las variables se vuelve a pedir y las variables son re-inicializadas. Si se desea que el valor se conserve durante la vida de un programa, puede denir una variable local de una funcin como static y darle un valor inicial. La inicializacin se realiza slo la primera vez que se llama a la funcin, y la informacin se conserva entre invocaciones sucesivas de la funcin. De este modo, una funcin puede recordar cierta informacin entre una llamada y otra. Puede surgir la duda de porqu no utilizar una variable global en este caso. El encanto de una variable static es que no est disponible fuera del mbito de la funcin, de modo que no se puede modicar accidentalmente. Esto facilita la localizacin de errores. A continuacin, un ejemplo del uso de variables static:
//: C03:Static.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Using a static variable in a function #include <iostream> using namespace std; void func() { static int i = 0;
101
Captulo 3. C en C++
cout << "i = " << ++i << endl; } int main() { for(int x = 0; x < 10; x++) func(); }
Cada vez que se llama a func() dentro del bucle, se imprime un valor diferente. Si no se utilizara la palabra reservada static, el valor mostrado sera siempre 1. El segundo signicado de static est relacionado con el primero en el sentido de que no est disponible fuera de cierto mbito. Cuando se aplica static al nombre de una funcin o de una variable que est fuera de todas las funciones, signica Este nombre no est disponible fuera de este chero. El nombre de la funcin o de la variable es local al chero; decimos que tiene mbito de chero. Como demostracin, al compilar y enlazar los dos cheros siguientes aparece un error en el enlazado:
//: C03:FileStatic.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // File scope demonstration. Compiling and // linking this file with FileStatic2.cpp // will cause a linker error // File scope means only available in this file: static int fs; int main() { fs = 1; }
Aunque la variable fs est destinada a existir como un extern en el siguiente chero, el enlazador no la encontrara porque ha sido declarada static en FileStatic.cpp.
//: C03:FileStatic2.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Trying to reference fs extern int fs; void func() { fs = 100; }
El especicador static tambin se puede usar dentro de una clase. Esta explicacin se dar ms adelante en este libro, cuando aprenda a crear clases.
3.6.4. extern
La palabra reservada extern ya ha sido brevemente descripta. Le dice al compilador que una variable o una funcin existe, incluso si el compilado an no la ha visto en el chero que est siendo compilado en ese momento. Esta variable o funcin puede denirse en otro chero o ms abajo en el chero actual. A modo de ejemplo:
102
Cuando el compilador encuentra la declaracin extern int i sabe que la denicin para i debe existir en algn sitio como una variable global. Cuando el compilador alcanza la denicin de i, ninguna otra declaracin es visible, de modo que sabe que ha encontrado la misma i declarada anteriormente en el chero. Si se hubiera denido i como static, estara indicando al compilador que i se dene globalmente (por extern), pero tambin que tiene el mbito de chero (por static), de modo que el compilador generar un error.
Enlazado
Para comprender el comportamiento de los programas C y C++, es necesario saber sobre enlazado. En un programa en ejecucin, un identicador se representa con espacio en memoria que aloja una variable o un cuerpo de funcin compilada. El enlazado describe este espacio tal como lo ve el enlazador. Hay dos formas de enlazado: enlace interno y enlace externo. Enlace interno signica que el espacio se pide para representar el identicador slo durante la compilacin del chero. Otros cheros pueden utilizar el mismo nombre de identicador con un enlace interno, o para una variable global, y el enlazador no encontrara conictos - se pide un espacio separado para cada identicador. El enlace interno se especica mediante la palabra reservada static en C y C++. Enlace externo signica que se pide slo un espacio para representar el identicador para todos los cheros que se estn compilando. El espacio se pide una vez, y el enlazador debe resolver todas las dems referencias a esa ubicacin. Las variables globales y los nombres de funcin tienen enlace externo. Son accesibles desde otros cheros declarndolas con la palabra reservada extern. Por defecto, las variables denidas fuera de todas las funciones (con la excepcin de const en C++) y las deniciones de las funciones implican enlace externo. Se pueden forzar especcamente a tener enlace interno utilizando static. Se puede establecer explcitamente que un identicador tiene enlace externo denindolo como extern. No es necesario denir una variable o una funcin como extern en C, pero a veces es necesario para const en C++. Las variables automticas (locales) existen slo temporalmente, en la pila, mientras se est ejecutando una funcin. El enlazador no entiende de variables automticas, de modo que no tienen enlazado.
3.6.5. Constantes
En el antiguo C (pre-Estndar), si se deseaba crear una constante, se deba utilizar el preprocesador:
#define PI 3.14159
103
Captulo 3. C en C++ En cualquier sitio en el que utilizase PI, el preprocesador lo substitua por el valor 3.14159 (an se puede utilizar este mtodo en C y C++). Cuando se utiliza el preprocesador para crear constantes, su control queda fuera del mbito del compilador. No existe ninguna comprobacin de tipo y no se puede obtener la direccin de PI (de modo que no se puede pasar un puntero o una referencia a PI). PI no puede ser una variable de un tipo denido por el usuario. El signicado de PI dura desde el punto en que es denida, hasta el nal del chero; el preprocesador no entiende de mbitos. C++ introduce el concepto de constantes con nombre que es lo mismo que variable, excepto que su valor no puede cambiar. El modicador const le indica al compilador que el nombre representa una constante. Cualquier tipo de datos predenido o denido por el usuario, puede ser denido como const. Si se dene algo como const y luego se intenta modicar, el compilador generar un error. Se debe especicar el tipo de un const, de este modo:
const int x = 10;
En C y C++ Estndar, se puede usar una constante en una lista de argumentos, incluso si el argumento que ocupa es un puntero o una referencia (p.e, se puede obtener la direccin de una constante). Las constantes tienen mbito, al igual que una variable ordinaria, de modo que se puede esconder una constante dentro de una funcin y estar seguro de que ese nombre no afectar al resto del programa. const ha sido tomado de C++ e incorporado al C Estndar pero un modo un poco distinto. En C, el compilador trata a const del mismo modo que a una variable que tuviera asociado una etiqueta que dice No me cambies. Cuando se dene un const en C, el compilador pide espacio para l, de modo que si se dene ms de un const con el mismo nombre en dos cheros distintos (o se ubica la denicin en un chero de cabeceras), el enlazador generar mensajes de error sobre del conicto. El concepto de const en C es diferente de su utilizacin en C++ (en resumen, es ms bonito en C++).
Valores constantes
En C++, una constante debe tener siempre un valor inicial (En C, eso no es cierto). Los valores de las constantes para tipos predenidos se expresan en decimal, octal, hexadecimal, o nmeros con punto otante (desgraciadamente, no se consider que los binarios fuesen importantes), o como caracteres. A falta de cualquier otra pista, el compilador assume que el valor de una constante es un nmero decimal. Los nmeros 47, 0 y 1101 se tratan como nmeros decimales. Un valor constante con un cero al principio se trata como un nmero octal (base 8). Los nmeros con base 8 pueden contener nicamente dgitos del 0 al 7; el compilador interpreta otros dgitos como un error. Un nmero octal legtimo es 017 (15 en base 10). Un valor constante con 0x al principio se trata como un nmero hexadecimal (base 16). Los nmeros con base 16 pueden contener dgitos del 0 al 9 y letras de la a a la f o A a F. Un nmero hexadecimal legtimo es 0x1fe (510 en base 10). Los nmeros en punto otante pueden contener comas decimales y potencias exponenciales (representadas mediante e, lo que signica 10 elevado a). Tanto el punto decimal como la e son opcionales. Si se asigna una constante a una variable de punto otante, el compilador tomar el valor de la constante y la convertir a un nmero en punto otante (este proceso es una forma de lo que se conoce como conversin implcita de tipo). De todos modos, es una buena idea el usar el punto decimal o una e para recordar al lector que est utilizando un nmero en punto otante; algunos compiladores incluso necesitan esta pista. Alguno valores vlidos para una constante en punto otante son: 1e4, 1.0001, 47.0, 0.0 y 1.159e-77. Se pueden aadir sujos para forzar el tipo de nmero de punto otante: f o F fuerza que sea oat, L o l fuerza que sea un long double; de lo contrario, el nmero ser un double. Las constantes de tipo char son caracteres entre comillas simples, tales como: A, o, . Fjese en que hay una gran diferencia entre el carcter o (ASCII 96) y el valor 0. Los caracteres especiales se representan con la barra invertida: \n (nueva lnea), \t (tabulacin), \\ (barra invertida), \r (retorno de carro), \" (comilla doble), \ (comilla simple), etc. Incluso se puede expresar constantes de tipo char en octal: \17 o hexadecimal: \xff. 104
3.6.6. Volatile
Mientras que el calicador const indica al compilador Esto nunca cambia (lo que permite al compilador realizar optimizaciones extra), el calicador volatile dice al compilador Nunca se sabe cuando cambiar esto, y evita que el compilador realice optimizaciones basadas en la estabilidad de esa variable. Se utiliza esta palabra reservada cuando se lee algn valor fuera del control del cdigo, algo as como un registro en un hardware de comunicacin. Una variable volatile se lee siempre que su valor es requerido, incluso si se ha ledo en la lnea anterior. Un caso especial de espacio que est fuera del control del cdigo es en un programa multi-hilo. Si est comprobando una bandera particular que puede ser modicada por otro hilo o proceso, esta bandera debera ser volatile de modo que el compilador no asuma que puede optimizar mltiples lecturas de la bandera. Fjese en que volatile puede no tener efecto cuando el compilador no est optimizando, pero puede prevenir errores crticos cuando se comienza a optimizar el cdigo (que es cuando el compilador empezar a buscar lecturas redundantes). Las palabras reservadas const y volatile se vern con ms detalle en un captulo posterior.
3.7.1. Asignacin
La asignacin se realiza mediante el operador =. Eso signica Toma el valor de la derecha (a menudo llamado rvalue) y cpialo en la variable de la izquierda (a menudo llamado lvalue). Un rvalue es cualquier constante, variable o expresin que pueda producir un valor, pero un lvalue debe ser una variable con un nombre distintivo y nico (esto quiere decir que debe haber un espacio fsico dnde guardar la informacin). De hecho, se puede asignar el valor de una constante a una variable (A = 4;), pero no se puede asignar nada a una constante - es decir, una constante no puede ser un lvalue (no se puede escribir 4 = A;).
105
Captulo 3. C en C++
// A macro to display a string and a value. #define PRINT(STR, VAR) \ cout << STR " = " << VAR << endl int main() { int i, j, k; float u, v, w; // Applies to doubles, too cout << "enter an integer: "; cin >> j; cout << "enter another integer: "; cin >> k; PRINT("j",j); PRINT("k",k); i = j + k; PRINT("j + k",i); i = j - k; PRINT("j - k",i); i = k / j; PRINT("k / j",i); i = k * j; PRINT("k * j",i); i = k % j; PRINT("k % j",i); // The following only works with integers: j %= k; PRINT("j %= k", j); cout << "Enter a floating-point number: "; cin >> v; cout << "Enter another floating-point number:"; cin >> w; PRINT("v",v); PRINT("w",w); u = v + w; PRINT("v + w", u); u = v - w; PRINT("v - w", u); u = v * w; PRINT("v * w", u); u = v / w; PRINT("v / w", u); // The following works for ints, chars, // and doubles too: PRINT("u", u); PRINT("v", v); u += v; PRINT("u += v", u); u -= v; PRINT("u -= v", u); u *= v; PRINT("u *= v", u); u /= v; PRINT("u /= v", u); }
Los rvalues de todas las asignaciones pueden ser, por supuesto, mucho mas complejos.
Se puede reemplazar la denicin de int con oat o double en el programa anterior. De todos modos, dese cuenta de que la comparacin de un nmero en punto otante con el valor cero es estricta; un nmero que es la fraccin ms pequea diferente de otro nmero an se considera distinto de. Un nmero en punto otante que es poca mayor que cero se considera verdadero.
Captulo 3. C en C++
produce un uno en cada posicin sucesiva de bit; en binario: 00000001, 00000010, etc. Si se hace and a este bit con val y el resultado es diferente de cero, signica que haba un uno en esa posicin de val. Finalmente, se utiliza la funcin en el ejemplo que muestra los operadores de manipulacin de bits:
//: C03:Bitwise.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt
108
Una vez ms, se usa una macro de preprocesador para ahorrar lneas. Imprime la cadena elegida, luego la representacin binaria de una expresin, y luego un salto de lnea. En main(), las variables son unsigned. Esto es porque, en general, no se desean signos cuando se trabaja con bytes. Se debe utilizar un int en lugar de un char para getval porque de otro modo la sentencia cin >> tratara el primer dgito como un carcter. Asignando getval a a y b, se convierte el valor a un solo byte (truncndolo). Los operadores << y >> proporcionan un comportamiento de desplazamiento de bits, pero cuando desplazan bits que estn al nal del nmero, estos bits se pierden (comnmente se dice que se caen en el mtico cubo de bits, el lugar donde acaban los bits descartados, presumiblemente para que puedan ser utilizados...). Cuando se manipulan bits tambin se pueden realizar rotaciones; es decir, que los bits que salen de uno de los extremos se pueden insertar por el otro extremo, como si estuviesen rotando en un bucle. Aunque la mayora de los procesadores de ordenadores ofrecen un comando de rotacin a nivel mquina (se puede ver en el lenguaje ensamblador de ese procesador), no hay un soporte directo para rotate en C o C++. Se supone que a los diseadores de C les pareci justicado el hecho de prescindir de rotate (en pro, como dijeron, de un lenguaje minimalista) ya que el programador se puede construir su propio comando rotate. Por ejemplo, a continuacin hay funciones para realizar rotaciones a izquierda y derecha:
//: C03:Rotation.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Perform left and right rotations unsigned char rol(unsigned char val) { int highbit;
109
Captulo 3. C en C++
if(val & 0x80) // 0x80 is the high bit only highbit = 1; else highbit = 0; // Left shift (bottom bit becomes 0): val <<= 1; // Rotate the high bit onto the bottom: val |= highbit; return val; } unsigned char ror(unsigned char val) { int lowbit; if(val & 1) // Check the low bit lowbit = 1; else lowbit = 0; val >>= 1; // Right shift by one position // Rotate the low bit onto the top: val |= (lowbit << 7); return val; }
Al intentar utilizar estas funciones en Bitwise.cpp, advierta que las deniciones (o cuando menos las declaraciones) de rol() y ror() deben ser vistas por el compilador en Bitwise.cpp antes de que se puedan utilizar. Las funciones de tratamiento de bits son por lo general extremadamente ecientes ya que traducen directamente las sentencias a lenguaje ensamblador. A veces una sentencia de C o C++ generar una nica lnea de cdigo ensamblador.
El menos unario produce el valor negativo. El ms unario ofrece simetra con el menos unario, aunque en realidad no hace nada. Los operadores de incremento y decremento (++ y --) se comentaron ya en este captulo. Son los nicos operadores, adems de los que involucran asignacin, que tienen efectos colaterales. Estos operadores incrementan o decrementan la variable en una unidad, aunque unidad puede tener diferentes signicados dependiendo del tipo de dato - esto es especialmente importante en el caso de los punteros. Los ltimos operadores unarios son direccin-de (&), indireccin (* y ->), los operadores de moldeado en C y C++, y new y delete en C++. La direccin-de y la indireccin se utilizan con los punteros, descriptos en este 110
3.7. Los operadores y su uso captulo. El moldeado se describe mas adelante en este captulo, y new y delete se introducen en el Captulo 4.
Aqu, el condicional produce el rvalue. A a se le asigna el valor de b si el resultado de decrementar b es diferente de cero. Si b se queda a cero, a y b son ambas asignadas a -99. b siempre se decrementa, pero se asigna a -99 slo si el decremento provoca que b valga 0. Se puede utilizar un sentencia similar sin el a = slo por sus efectos colaterales:
--b ? b : (b = -99);
Aqu la segunda b es superua, ya que no se utiliza el valor producido por el operador. Se requiere una expresin entre el ? y :. En este caso, la expresin puede ser simplemente una constante, lo que hara que el cdigo se ejecute un poco ms rpido.
Por supuesto, tambin se usa en listas de argumentos de funciones. De todos modos, tambin se puede utilizar como un operador para separar expresiones - en este caso produce el valor de la ltima expresin. El resto de expresiones en la lista separada por comas se evala slo por sus efectos colaterales. Este ejemplo incrementa una lista de variables y usa la ltima como el rvalue:
//: C03:CommaOperator.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main() { int a = 0, b = 1, c = 2, d = 3, e = 4; a = (b++, c++, d++, e++); cout << "a = " << a << endl; // The parentheses are critical here. Without // them, the statement will evaluate to: (a = b++), c++, d++, e++; cout << "a = " << a << endl; }
111
Captulo 3. C en C++ En general, es mejor evitar el uso de la coma para cualquier otra cosa que no sea separar, ya que la gente no est acostumbrada a verla como un operador.
La sentencia a = b siempre se va a evaluar como cierta cuando b es distinta de cero. La variable a obtiene el valor de b, y el valor de b tambin es producido por el operador =. En general, lo que se pretende es utilizar el operador de equivalencia (== dentro de una sentencia condicional, no la asignacin. Esto le ocurre a muchos programadores (de todos modos, algunos compiladores advierten del problema, lo cual es una ayuda). Un problema similar es usar los operadores and y or de bits en lugar de sus equivalentes lgicos. Los operadores and y or de bits usan uno de los caracteres (& o |), mientras que los operadores lgicos utilizan dos (&& y ||). Al igual que con = y ==, es fcil escribir simplemente un carcter en vez de dos. Una forma muy fcil de recordarlo es que los bits son mas pequeos, de modo que no necesitan tantos caracteres en sus operadores.
El moldeado es poderoso, pero puede causar dolores de cabeza porque en algunas situaciones fuerza al compi112
3.7. Los operadores y su uso lador a tratar datos como si fuesen (por ejemplo) ms largos de lo que realmente son, de modo que ocupar ms espacio en memoria; lo que puede afectar a otros datos. Esto ocurre a menudo cuando se moldean punteros, no cuando se hacen moldeos simples como los que ha visto anteriormente. C++ tiene una sintaxis adicional para moldes, que sigue a la sintaxis de llamada a funciones. Esta sintaxis pone los parntesis alrededor del argumento, como en una llamada a funcin, en lugar de a los lados del tipo:
//: C03:FunctionCallCast.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { float a = float(200); // This is equivalent to: float b = (float)200; }
Por supuesto, en el caso anterior, en realidad no se necesitara un molde; simplemente se puede decir 200.f o 200.0f (en efecto, eso es tpicamente lo que el compilador har para la expresin anterior). Los moldes normalmente se utilizan con variables, en lugar de con constantes.
reinterpret_cast
dynamic_cast
Captulo 3. C en C++ Los primeros tres moldes explcitos se describirn completamente en las siguientes secciones, mientras que los ltimos se explicarn despus de que haya aprendido ms en el Captulo 15.
static_cast
El static_cast se utiliza para todas las conversiones que estn bien denidas. Esto incluye conversiones seguras que el compilador permitira sin utilizar un molde, y conversiones menos seguras que estn sin embargo bien denidas. Los tipos de conversiones que cubre static_cast incluyen las conversiones tpicas sin molde, conversiones de estrechamiento (prdida de informacin), forzar una conversin de un void*, conversiones de tipo implcitas, y navegacin esttica de jerarquas de clases (ya que no se han visto an clases ni herencias, este ltimo apartado se postpone hasta el Captulo 15):
//: C03:static_cast.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt void func(int) {} int main() { int i = 0x7fff; // Max pos value = 32767 long l; float f; // (1) Typical castless conversions: l = i; f = i; // Also works: l = static_cast<long>(i); f = static_cast<float>(i); // (2) Narrowing conversions: i = l; // May lose digits i = f; // May lose info // Says "I know," eliminates warnings: i = static_cast<int>(l); i = static_cast<int>(f); char c = static_cast<char>(i); // (3) Forcing a conversion from void* : void* vp = &i; // Old way produces a dangerous conversion: float* fp = (float*)vp; // The new way is equally dangerous: fp = static_cast<float*>(vp); // (4) Implicit type conversions, normally // performed by the compiler: double d = 0.0; int x = d; // Automatic type conversion x = static_cast<int>(d); // More explicit func(d); // Automatic type conversion func(static_cast<int>(d)); // More explicit }
En la seccin (1), se pueden ver tipos de conversiones que eran usuales en C, con o sin un molde. Promover un int a long o oat no es un problema porque el ltimo puede albergar siempre cualquier valor que un int pueda contener. Aunque es innecesario, se puede utilizar static_cast para remarcar estas promociones. Se muestra en (2) como se convierte al revs. Aqu, se puede perder informacin porque un int no es tan ancho como un long o un oat; no aloja nmeros del mismo tamao. De cualquier modo, este tipo de conversin se llama conversin de estrechamiento. El compilador no impedir que ocurrn, pero normalmente dar una advertencia. Se puede eliminar esta advertencia e indicar que realmente se pretenda esto utilizando un molde. 114
3.7. Los operadores y su uso Tomar el valor de un void* no est permitido en C++ a menos que use un molde (al contrario de C), como se puede ver en (3). Esto es peligroso y requiere que los programadores sepan lo que estn haciendo. El static_cast, al menos, es mas fcil de localizar que los moldes antiguos cuando se trata de cazar fallos. La seccin (4) del programa muestra las conversiones de tipo implcitas que snormalmente se realizan de manera automtica por el compilador. Son automticas y no requieren molde, pero el utilizar static_cast acenta dicha accin en caso de que se quiera reejar claramente qu est ocurriendo, para poder localizarlo despus.
const_cast
Si quiere convertir de un const a un no-const o de un volatile a un no-volatile, se utiliza const_cast. Es la nica conversin permitida con const_cast; si est involucrada alguna conversin adicional se debe hacer utilizando una expresin separada o se obtendr un error en tiempo de compilacin.
//: C03:const_cast.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { const int i = 0; int* j = (int*)&i; // Deprecated form j = const_cast<int*>(&i); // Preferred // Cant do simultaneous additional casting: //! long* l = const_cast<long*>(&i); // Error volatile int k = 0; int* u = const_cast<int*>(&k); }
Si toma la direccin de un objeto const, produce un puntero a const, ste no se puede asignar a un puntero que no sea const sin un molde. El molde al estilo antiguo lo puede hacer, pero el const_cast es el ms apropiado en este caso. Lo mismo ocurre con volatile.
reinterpret_cast
Este es el menos seguro de los mecanismos de molde, y el ms susceptible de crear fallos. Un reinterpret_cast supone que un objecto es un patrn de bits que se puede tratar (para algn oscuro propsito) como si fuese de un tipo totalmente distinto. Ese es el jugueteo de bits a bajo nivel por el cual C es famoso. Prcticamente siempre necesitar hacer reinterpret_cast volver al tipo original (o de lo contrario tratar a la variable como su tipo original) antes de hacer nada ms con ella.
//: C03:reinterpret_cast.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; const int sz = 100; struct X { int a[sz]; }; void print(X* x) { for(int i = 0; i < sz; i++) cout << x->a[i] << ; cout << endl << "--------------------" << endl; } int main() { X x;
115
Captulo 3. C en C++
print(&x); int* xp = reinterpret_cast<int*>(&x); for(int* i = xp; i < xp + sz; i++) *i = 0; // Cant use xp as an X* at this point // unless you cast it back: print(reinterpret_cast<X*>(xp)); // In this example, you can also just use // the original identifier: print(&x); }
En este ejemplo, struct X contiene un array de int, pero cuando se crea uno en la pila como en X x, los valores de cada uno de los ints tienen basura (esto de muestra utilizando la funcin print() para mostrar los contenidos de struct). Para inicializarlas, la direccin del X se toma y se moldea a un puntero int, que es luego iterado a travs del array para inicializar cada int a cero. Fjese como el lmite superior de i se calcula aadiendo sz a xp; el compilador sabe que lo que usted quiere realmente son las direcciones de sz mayores que xp y l realiza el clculo aritmtico por usted. FIXME(Comprobar lo que dice este prrafo de acuerdo con el cdigo) La idea del uso de reinterpret_cast es que cuando se utiliza, lo que se obtiene es tan extrao que no se puede utilizar para los propsitos del tipo original, a menos que se vuelva a moldear. Aqu, vemos el molde otra vez a X* en la llamada a print(), pero por supuesto, dado que tiene el identicador original tambin se puede utilizar. Pero xp slo es til como un int*, lo que es verdaderamente una reinterpretacin del X original. Un reinterpret_cast a menudo indica una programacin desaconsejada y/o no portable, pero est disponible si decide que lo necesita.
Por denicin, el sizeof de cualquier tipo de char (signed, unsigned o simple) es siempre uno, sin tener en cuenta que el almacenamiento subyacente para un char es realmente un byte. Para todos los dems tipos, el resultado es el tamao en bytes. Tenga en cuenta que sizeof es un operador, no una funcin. Si lo aplica a un tipo, se debe utilizar con la forma entre parntesis mostrada anteriormente, pero si se aplica a una variable se puede utilizar sin parntesis:
//: C03:sizeofOperator.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() {
116
sizeof tambin puede informar de los tamaos de tipos denidos por el usuario. Se utilizar ms adelante en el libro.
Captulo 3. C en C++
Ahora si pone ulong, el compilador sabe que se est reriendo a unsigned long. Puede pensar que esto se puede lograr fcilmente utilizando sustitucin en el preprocesador, pero hay situaciones en las cuales el compilador debe estar advertido de que est tratando un nombre como si fuese un tipo, y por eso typedef es esencial.
int* x, y;
Esto genera en realidad un int* que es x, y un int (no un int*) que es y. Esto signica que el * aade a la derecha, no a la izquierda. Pero, si utiliza un typedef:
typedef int* IntPtr; IntPtr x, y;
Entonces ambos, x e y son del tipo int*. Se puede discutir sobre ello y decir que es ms explcito y por consiguiente mas legible evitar typedefs para los tipos primitivos, y de hecho los programas se vuelven difciles de leer cuando se utilizan demasiados typedefs. De todos modos, los typedefs se vuelven especialmente importantes en C cuando se utilizan con struct.
La declaracin de struct debe acabar con una llave. En main(), se crean dos instancias de Structure1: s1 y s2. Cada una de ellas tiene su versin propia y separada de c, I, f y d. De modo que s1 y s2 representan bloques de variables completamente independientes. Para seleccionar uno de estos elementos dentro de s1 o s2, se utiliza un ., sintaxis que se ha visto en el cpitulo previo cuando se utilizaban objetos class de C++ - ya que las clases surgan de structs, de ah proviene esta sintaxis. 118
3.8. Creacin de tipos compuestos Una cosa a tener en cuenta es la torpeza de usar Structure1 (como salta a la vista, eso slo se requiere en C, y no en C++). En C, no se puede poner Structure1 cuando se denen variables, se debe poner struct Structure1. Aqu es donde typedef se vuelve especialmente til en C:
//: C03:SimpleStruct2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Using typedef with struct typedef struct { char c; int i; float f; double d; } Structure2; int main() { Structure2 s1, s2; s1.c = a; s1.i = 1; s1.f = 3.14; s1.d = 0.00093; s2.c = a; s2.i = 1; s2.f = 3.14; s2.d = 0.00093; }
Usando typedef de este modo, se puede simular (en C; intentar eliminar el typedef para C++) que Structure2 es un tipo predenido, como int o oat, cuando dene s1 y s2 (pero se ha de tener en cuenta de que slo tiene informacin - caractersticas - y no incluye comportamiento, que es lo que se obtiene con objetos reales en C++). Observe que el struct se ha declarado al principio, porque el objetivo es crear el typedef. Sin embargo, hay veces en las que sera necesario referirse a struct durante su denicin. En esos casos, se puede repetir el nombre del struct como tal y como typedef.
//: C03:SelfReferential.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Allowing a struct to refer to itself typedef struct SelfReferential { int i; SelfReferential* sr; // Head spinning yet? } SelfReferential; int main() { SelfReferential sr1, sr2; sr1.sr = &sr2; sr2.sr = &sr1; sr1.i = 47; sr2.i = 1024; }
Si lo observa detenidamente, puede ver que sr1 y sr2 apuntan el uno al otro, guardando cada uno una parte de la informacin. 119
Captulo 3. C en C++ En realidad, el nombre struct no tiene que ser lo mismo que el nombre typedef, pero normalmente se hace de esta manera ya que tiende a simplicar las cosas.
Punteros y estructuras
En los ejemplos anteriores, todos los structs se manipulan como objetos. Sin embargo, como cualquier bloque de memoria, se puede obtener la direccin de un objeto struct (tal como se ha visto en SelfReferential. cpp). Para seleccionar los elementos de un objeto struct en particular, se utiliza un ., como se ha visto anteriormente. No obstante, si tiene un puntero a un objeto struct, debe seleccionar un elemento de dicho objeto utilizando un operador diferente: el ->. A continuacin un ejemplo:
//: C03:SimpleStruct3.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Using pointers to structs typedef struct Structure3 { char c; int i; float f; double d; } Structure3; int main() { Structure3 s1, s2; Structure3* sp = &s1; sp->c = a; sp->i = 1; sp->f = 3.14; sp->d = 0.00093; sp = &s2; // Point to a different struct object sp->c = a; sp->i = 1; sp->f = 3.14; sp->d = 0.00093; }
En main(), el puntero sp est apuntando inicialmente a s1, y los miembros de s1 se inicializan seleccionndolos con el -> (y se utiliza este mismo operador para leerlos). Pero luego sp apunta a s2, y esas variables se inicializan del mismo modo. Como puede ver, otro benecio en el uso de punteros es que pueden ser redirigidos dinmicamente para apuntar a objetos diferentes, eso proporciona ms exibilidad a sus programas, tal como ver. De momento, es todo lo que debe saber sobre struct, pero se sentir mucho ms cmodo con ellos (y especialmente con sus sucesores mas potentes, las clases) a medida que progrese en este libro.
120
shape es una variable del tipo de datos enumerado ShapeType, y su valor se compara con el valor en la enumeracin. Ya que shape es realmente un int, puede albergar cualquier valor que corresponda a int (incluyendo un nmero negativo). Tambin se puede comparar una variable int con un valor de una enumeracin. Se ha de tener en cuenta que el ejemplo anterior de intercambiar los tipos tiende a ser una manera problemtica de programar. C++ tiene un modo mucho mejor de codicar este tipo de cosas, cuya explicacin se pospondr para mucho mas adelante en este libro. Si el modo en que el compilador asigna los valores no es de su agrado, puede hacerlo manualmente, como sigue:
enum ShapeType { circle = 10, square = 20, rectangle = 50 };
Si da valores a algunos nombres y a otros no, el compilador utilizar el siguiente valor entero. Por ejemplo,
enum snap { crackle = 25, pop };
El compilador le da a pop el valor 26. Es fcil comprobar que el cdigo es ms legible cuando se utilizan tipos de datos enumerados. No obstante, en cierto grado esto sigue siendo un intento (en C) de lograr las cosas que se pueden lograr con una class en C++, y por eso ver que enum se utiliza menos en C++.
Captulo 3. C en C++ cdigo que asuma una conversin implcita a un tipo enum, el compilador alertar de que se trata de una actividad inherentemente peligrosa. Las uniones (descriptas a continuacin) tienen una comprobacin adicional de tipo similar en C++.
El compilador realiza la asignacin apropiada para el miembro de la unin seleccionado. Una vez que se realice una asignacin, al compilador le da igual lo que se haga con la unin. En el ejemplo anterior, se puede asignar un valor en coma-otante a x:
x.f = 2.222;
122
3.8.5. Arrays
Los vectores son un tipo compuesto porque permiten agrupar muchas variables, una a continuacin de la otra, bajo un identicador nico. Si dice:
int a[10];
Se crea espacio para 10 variables int colocadas una despus de la otra, pero sin identicadores nicos para cada variable. En su lugar, todas estn englobadas por el nombre a. Para acceder a cualquiera de los elementos del vector, se utiliza la misma sintaxis de corchetes que se utiliza para denir el vector:
a[5] = 47;
Sin embargo, debe recordar que aunque el tamao de a es 10, se seleccionan los elementos del vector comenzando por cero (esto se llama a veces indexado a cero4 , de modo que slo se pueden seleccionar los elementos del vector de 0 a 9, como sigue:
//: C03:Arrays.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main() { int a[10]; for(int i = 0; i < 10; i++) { a[i] = i * 10; cout << "a[" << i << "] = " << a[i] << endl; } }
Los accesos a vectores son extremadamente rpidos, Sin embargo, si se indexa ms all del nal del vector, no hay ninguna red de seguridad - se entrar en otras variables. La otra desventaja es que se debe denir el tamao del vector en tiempo de compilacin; si se quiere cambiar el tamao en tiempo de ejecucin no se puede hacer con la sintaxis anterior (C tiene una manera de crear un vector dinmicamente, pero es signicativamente ms sucia). El vector de C++ presentado en el captulo anterior, proporciona un objeto parecido al vector que se redimensiona automticamente , de modo que es una solucin mucho mejor si el tamao del vector no puede conocer en tiempo de compilacin. Se puede hacer un vector de cualquier tipo, incluso de structs:
//: C03:StructArray.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // An array of struct
4
123
Captulo 3. C en C++
typedef struct { int i, j, k; } ThreeDpoint; int main() { ThreeDpoint p[10]; for(int i = 0; i < 10; i++) { p[i].i = i + 1; p[i].j = i + 2; p[i].k = i + 3; } }
Fjese como el identicador de struct i es independiente del i del bucle for. Para comprobar que cada elemento del vector es contiguo con el siguiente, puede imprimir la direccin de la siguiente manera:
//: C03:ArrayAddresses.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main() { int a[10]; cout << "sizeof(int) = "<< sizeof(int) << endl; for(int i = 0; i < 10; i++) cout << "&a[" << i << "] = " << (long)&a[i] << endl; }
Cuando se ejecuta este programa, se ve que cada elemento est separado por el tamao de un int del anterior. Esto signica, que estn colocados uno a continuacin del otro.
Punteros y arrays
El identicador de un vector es diferente de los identicadores de las variables comunes. Un identicador de un vector no es un lvalue; no se le puede asignar nada. En realidad es FIXME:gancho dentro de la sintaxis de corchetes, y cuando se usa el nombre de un vector, sin los corchetes, lo que se obtiene es la direccin inicial del vector:
//: C03:ArrayIdentifier.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main() { int a[10]; cout << "a = " << a << endl; cout << "&a[0] =" << &a[0] << endl; }
124
3.8. Creacin de tipos compuestos Cuando se ejecuta este programa, se ve que las dos direcciones (que se imprimen en hexadecimal, ya que no se moldea a long) son las misma. De modo que una manera de ver el identicador de un vector es como un puntero de slo lectura al principio de ste. Y aunque no se pueda hacer que el identicador del vector apunte a cualquier otro sitio, se puede crear otro puntero y utilizarlo para moverse dentro del vector. De hecho, la sintaxis de corchetes tambin funciona con punteros convencionales:
//: C03:PointersAndBrackets.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { int a[10]; int* ip = a; for(int i = 0; i < 10; i++) ip[i] = i * 10; }
El hecho de que el nombre de un vector produzca su direccin de inicio resulta bastante importante cuando hay que pasar un vector a una funcin. Si declara un vector como un argumento de una funcin, lo que realmente est declarando es un puntero. De modo que en el siguiente ejemplo, fun1() y func2() tienen la misma lista de argumentos:
//: C03:ArrayArguments.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> #include <string> using namespace std; void func1(int a[], int size) { for(int i = 0; i < size; i++) a[i] = i * i - i; } void func2(int* a, int size) { for(int i = 0; i < size; i++) a[i] = i * i + i; } void print(int a[], string name, int size) { for(int i = 0; i < size; i++) cout << name << "[" << i << "] = " << a[i] << endl; } int main() { int a[5], b[5]; // Probably garbage values: print(a, "a", 5); print(b, "b", 5); // Initialize the arrays: func1(a, 5); func1(b, 5); print(a, "a", 5); print(b, "b", 5); // Notice the arrays are always modified: func2(a, 5);
125
Captulo 3. C en C++
func2(b, 5); print(a, "a", 5); print(b, "b", 5); }
A pesar de que func1() y func2() declaran sus argumentos de distinta forma, el uso es el mismo dentro de la funcin. Hay otros hechos que revela este ejemplo: los vectores no se pueden pasados por valor5 , es decir, que nunca se puede obtener automticamente una copia local del vector que se pasa a una funcin. Por eso, cuando se modica un vector, siempre se est modicando el objeto externo. Eso puede resultar un poco confuso al principio, si lo que se espera es el paso-por-valor como en los argumentos ordinarios. Fjese que print() utiliza la sintaxis de corchetes para los argumentos de tipo vector. Aunque la sintaxis de puntero y la sintaxis de corchetes efectivamente es la mismo cuando se estn pasando vectores como argumentos, la sintaxis de corchetes deja ms clara al lector que se pretende enfatizar que dicho argumento es un vector. Observe tambin que el argumento size se pasa en cada caso. La direccin no es suciente informacin al pasar un vector; siempre se debe ser posible obtener el tamao del vector dentro de la funcin, de manera que no se salga de los lmites de dicho vector. los vectores pueden ser de cualquier tipo, incluyendo vectores de punteros. De hecho, cuando se quieren pasar argumentos de tipo lnea de comandos dentro del programa, C y C++ tienen una lista de argumentos especial para main(), que tiene el siguiente aspecto:
int main(int argc, char* argv[]) { // ...
El primer argumento es el nmero de elementos en el vector, que es el segundo argumento. El segundo argumento es siempre un vector de char*, porque los argumentos se pasan desde la lnea de comandos como vectores de caracteres (y recuerde, un vector slo se puede pasar como un puntero). Cada bloque de caracteres delimitado por un espacio en blanco en la lnea de comandos se aloja en un elemento separado en el vector. El siguiente programa imprime todos los argumentos de lnea de comandos recorriendo el vector:
//: C03:CommandLineArgs.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << "argc = " << argc << endl; for(int i = 0; i < argc; i++) cout << "argv[" << i << "] = " << argv[i] << endl; }
Observe que argv[0] es la ruta y el nombre del programa en s mismo. Eso permite al programa descubrir informacin de s mismo. Tambin aade un argumento ms al vector de argumentos del programa, de modo que un error comn al recoger argumentos de lnea de comandos es tomar argv[0] como si fuera el primer argumento. No es obligatorio utilizar argc y argv como identicadores de los parmetros de main(); estos identicadores son slo convenciones (pero puede confundir al lector si no se respeta). Tambin, hay un modo alternativo de declarar argv:
5 A menos que tome la siguiente aproximacin estricta: todos los argumentos pasado en C/C++ son por valor, y el valor de un vector es el producido por su identicador: su direccin. Eso puede parecer correcto desde el punto de vista del lenguaje ensamblador, pero yo no creo que ayude cuando se trabaja con conceptos de alto nivel. La inclusin de referencias en C++ hace que el argumento todo se pasa por valor sea ms confuso, hasta el punto de que siento que es ms adecuado pensar en trminos de paso por valor vs paso por direccin.
126
Las dos formas son equivalentes, pero la versin utilizada en este libro es la ms intuitiva al leer el cdigo, ya que dice, directamente, Esto es un vector de punteros a carcter. Todo lo que se obtiene de la lnea de comandos son vectores de caracteres; si quiere tratar un argumento como algn otro tipo, ha de convertirlos dentro del programa. Para facilitar la conversin a nmeros, hay algunas funciones en la librera de C Estndar, declaradas en <cstdlib>. Las ms fciles de utilizar son atoi(), atol(), y atof() para convertir un vector de caracteres ASCII a int, long y double, respectivamente. A continuacin un ejemplo utilizando atoi() (las otras dos funciones se invocan del mismo modo):
//: C03:ArgsToInts.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Converting command-line arguments to ints #include <iostream> #include <cstdlib> using namespace std; int main(int argc, char* argv[]) { for(int i = 1; i < argc; i++) cout << atoi(argv[i]) << endl; }
En este programa, se puede poner cualquier nmero de argumentos en la lnea de comandos. Fjese que el bucle for comienza en el valor 1 para saltar el nombre del programa en argv[0]. Tambin, si se pone un nmero decimal que contenga un punto decimal en la lnea de comandos, atoi() slo toma los dgitos hasta el punto decimal. Si pone valores no numricos en la lnea de comandos, atoi() los devuelve como ceros.
127
Captulo 3. C en C++
double d = atof(argv[1]); unsigned char* cp = reinterpret_cast<unsigned char*>(&d); for(int i = sizeof(double)-1; i >= 0 ; i -= 2){ printBinary(cp[i-1]); printBinary(cp[i]); } }
Primero, el programa garantiza que se le haya pasado un argumento comprobando el valor de argc, que vale dos si hay un solo argumento (es uno si no hay argumentos, ya que el nombre del programa siempre es el primer elemento de argv). Si eso falla, imprime un mensaje e invoca la funcin exit() de la librera Estndar de C para nalizar el programa. El programa toma el argumento de la lnea de comandos y convierte los caracteres a double utilizando atof(). Luego el double se trata como un vector de bytes tomando la direccin y moldendola a un unsigned char*. Para cada uno de estos bytes se llama a printBinary() para mostrarlos. Este ejemplo se ha creado para imprimir los bytes en un orden tal que el bit de signo aparece al principio en mi mquina. En otras mquinas puede ser diferente, por lo que puede querer re-organizar el modo en que se imprimen los bytes. Tambin debera tener cuidado porque los formatos en punto-otante no son tan triviales de entender; por ejemplo, el exponente y la mantisa no se alinean generalmente entre los lmites de los bytes, en su lugar un nmero de bits se reserva para cada uno y se empaquetan en la memoria tan apretados como se pueda. Para ver lo que esta pasando, necesitara averiguar el tamao de cada parte del nmero (los bit de signo siempre son de un bit, pero los exponentes y las mantisas pueden ser de diferentes tamaos) e imprimir separados los bits de cada parte.
Aritmtica de punteros
Si todo lo que se pudiese hacer con un puntero que apunta a un vector fuese tratarlo como si fuera un alias para ese vector, los punteros a vectores no tendran mucho inters. Sin embargo, los punteros son mucho ms exibles que eso, ya que se pueden modicar para apuntar a cualquier otro sitio (pero recuerde, el identicador del vector no se puede modicar para apuntar a cualquier otro sitio). La aritmtica de punteros se reere a la aplicacin de alguno de los operadores aritmticos a los punteros. Las razn por la cual la aritmtica de punteros es un tema separado de la aritmtica ordinaria es que los punteros deben ajustarse a clusulas especiales de modo que se comporten apropiadamente. Por ejemplo, un operador comn para utilizar con punteros es ++, lo que "aade uno al puntero." Lo que de hecho signica esto es que el puntero se cambia para moverse al "siguiente valor," Lo que sea que ello signique. A continuacin un ejemplo:
//: C03:PointerIncrement.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; int main() { int i[10]; double d[10]; int* ip = i; double* dp = d; cout << "ip = " ip++; cout << "ip = " cout << "dp = " dp++; cout << "dp = " }
<< (long)ip << endl; << (long)ip << endl; << (long)dp << endl; << (long)dp << endl;
128
3.8. Creacin de tipos compuestos Para una ejecucin en mi mquina, la salida es:
ip ip dp dp = = = = 6684124 6684128 6684044 6684052
Lo interesante aqu es que aunque la operacin ++ parece la misma tanto para el int* como para el double*, se puede comprobar que el puntero de int* ha cambiado 4 bytes mientras que para el double* ha cambiado 8. No es coincidencia, que estos sean los tamaos de int y double en esta mquina. Y ese es el truco de la aritmtica de punteros: el compilador calcula la cantidad apropiada para cambiar el puntero de modo que apunte al siguiente elemento en el vector (la aritmtica de punteros slo tiene sentido dentro de los vectores). Esto funciona incluso con vectores de structs:
//: C03:PointerIncrement2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; typedef struct { char c; short s; int i; long l; float f; double d; long double ld; } Primitives; int main() { Primitives p[10]; Primitives* pp = p; cout << "sizeof(Primitives) = " << sizeof(Primitives) << endl; cout << "pp = " << (long)pp << endl; pp++; cout << "pp = " << (long)pp << endl; }
Como puede ver, el compilador tambin hace lo adecuado para punteros a structs (y con class y union). La aritmtica de punteros tambin funciona con los operadores --, + y -, pero los dos ltimos estn limitados: no se puede sumar dos punteros, y si se restan punteros el resultado es el nmero de elementos entre los dos punteros. Sin embargo, se puede sumar o restar un valor entero y un puntero. A continuacin un ejemplo demostrando el uso de la aritmtica de punteros:
//: C03:PointerArithmetic.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000
129
Captulo 3. C en C++
// Copyright notice in Copyright.txt #include <iostream> using namespace std; #define P(EX) cout << #EX << ": " << EX << endl; int main() { int a[10]; for(int i = 0; i < 10; i++) a[i] = i; // Give it index values int* ip = a; P(*ip); P(*++ip); P(*(ip + 5)); int* ip2 = ip + 5; P(*ip2); P(*(ip2 - 4)); P(*--ip2); P(ip2 - ip); // Yields number of elements }
Comienza con otra macro, pero esta utiliza una caracterstica del preprocesador llamada stringizing (implementada mediante el signo # antes de una expresin) que toma cualquier expresin y la convierte a un vector de caracteres. Esto es bastante conveniente, ya que permite imprimir la expresin seguida de dos puntos y del valor de la expresin. En main() puede ver lo til que resulta este atajo. Aunque tanto la versin prejo como sujo de ++ y -- son vlidas para los punteros, en este ejemplo slo se utilizan las versiones prejo porque se aplican antes de referenciar el puntero en las expresiones anteriores, de modo que permite ver los efectos en las operaciones. Observe que se han sumado y restado valores enteros; si se combinasen de este modo dos punteros, el compilador no lo permitira. Aqu se ve la salida del programa anterior:
*ip: 0 *++ip: 1 *(ip + 5): 6 *ip2: 6 *(ip2 - 4): 2 *--ip2: 5
En todos los casos, el resultado de la aritmtica de punteros es que el puntero se ajusta para apuntar al sitio correcto, basndose en el tamao del tipo de los elementos a los que est apuntado. Si la aritmtica de punteros le sobrepasa un poco al principio, no tiene porqu preocuparse. La mayora de las veces slo la necesitar para crear vectores e indexarlos con [], y normalmente la aritmtica de punteros ms sosticada que necesitar es ++ y -- . La aritmtica de punteros generalmente est reservada para programas ms complejos e ingeniosos, y muchos de los contenedores en la librera de Estndar C++ esconden muchos de estos inteligentes detalles, por lo que no tiene que preocuparse de ellos.
La mayora de las implementaciones de C y C++ tambin le permitirn denir y eliminar banderas (con #define y #undef) desde lnea de comandos, y de ese modo puede recompilar cdig e insertar informacin de depuracin con un nico comando (preferiblemente con un makefile, una herramienta que ser descrita en breve). Compruebe la documentacin de su entorno si necesita ms detalles.
131
Captulo 3. C en C++
cout << "Turn debugger [on/off/quit]: "; string reply; cin >> reply; if(reply == "on") debug = true; // Turn it on if(reply == "off") debug = false; // Off if(reply == "quit") break; // Out of while } }
Este programa permitiendole activar y desactivar la bandera de depuracin hasta que escriba quit para indicarle que quiere salir. Fjese que es necesario escribir palabras completas, no solo letras (puede abreviarlo a letras si lo desea). Opcionalmente, tambin se puede usar un argumento en lnea de comandos para comenzar la depuracin - este argumento puede aparecer en cualquier parte de la lnea de comando, ya que el cdigo de activacin en main() busca en todos los argumentos. La comprobacin es bastante simple como se ve en la expresin:
string(argv[i])
Esto toma la cadena argv[i] y crea un string, el cual se puede comparar fcilmente con lo que haya a la derecha de ==. El programa anterios busca la cadena completa --debug=on. Tambin puede buscar --debug= y entonces ver que hay despus, para proporcionar ms opciones. El Volumen 2 (disponible en www.BruceEckel.com) contiene un captulo dedicado a la clase string Estndar de C++. Aunque una bandera de depuracin es uno de los relativamente pocos casos en los que tiene mucho sentido usar una variable global, no hay nada que diga que debe ser as. Fjese en que la variable est escrita en minsculas para recordar al lector que no es una bandera del preprocesador.
Si se imprime la variable a invocando PR(a), tendr el mismo efecto que este cdigo:
cout << "a = " << a << "\n";
Este mismo proceso funciona con expresiones completas. El siguiente programa usa una macro para crear un atajo que imprime ls expresin cadenizadas y despus evala la expresin e imprime el resultado:
//: C03:StringizingExpressions.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; #define P(A) cout << #A << ": " << (A) << endl; int main() { int a = 1, b = 2, c = 3;
132
Puede comprobar cmo una tcnica como esta se puede convertir rpidamente en algo indispensable, especialmente si no tiene depurador (o debe usar mltiples entornos de desarrollo). Tambin puede insertar un #ifdef para conseguir que P(A) se dena como nada cuando quiera eliminar el cdigo de depuracin.
La macro original es C Estndar, as que est disponible tambin en el chero de cabecera assert.h. Cuando haya terminado la depuracin, puede eliminar el cdigo generado por la macro escribiendo la siguiente lnea:
#define NDEBUG
en el programa, antes de la inclusin de <cassert>, o deniendo NDEBUG en la lnea de comandos del compilador. NDEBUG es una bandera que se usa en <cassert> para cambiar el cdigo generado por las mcros. Ms adelante en este libro, ver algunas alternativas ms sosticadas a assert().
133
Captulo 3. C en C++
void (*funcPtr)();
Cuando se observa una dencin compleja como esta, el mejor mtodo para entenderla es empezar en el medio e ir hacia afuera. Empezar en el medio signica empezar con el nombre de la variable, que es funPtr. Ir hacia afuera signica mirar al elemento inmediatamente a la derecha (nada en este caso; el parntesis derecho marca el n de ese elemento), despus mire a la izquierda (un puntero denotado por el asterisco), despus mirar de nuevo a la derecha (una lista de argumentos vaca que indica que no funcin no toma argumentos), despus a la izquierda (void, que indica que la funcin no retorna nada). Este movimiento derecha-izquierda-derecha funciona con la mayora de las declaraciones. 6 Para repasar, empezar en el medio (funcPtr es un ..., va a la derecha (nada aqu - pare en el parntesis derecho), va a la izquierda y encontra el * (... puntero a ...), va a la derecha y encuentra la lista de argumentos vaca (... funcin que no tiene argumentos ...) va a la izquierda y encuentra el void (funcPtr es un punero a una funcin que no tiene argumentos y retorna void). Quiz se pregunte porqu *funcPtr necesita parntesis. Si no los usara, el compilador podra ver:
void *funcPtr();
Lo que corresponde a la declaracin de una funcin (que retorna un void* en lugar de denir una variable. Se podra pensar que el compilador sera capaz distinguir una declaracin de una denicin por lo que se supone que es. El compilador necesita los parntesis para tener contra qu chocar cuando vaya hacia la izquierda y encuente el *, en lugar de continuar hacia la derecha y encontrar la lista de argumentos vaca.
/* 4. */ int main() {}
Estudie cada uno y use la regla derecha-izquierda para entenderlos. El nmero 1 dice fp1 es un puntero a una funcin que toma un entero como argumento y retorna un puntero a un array de 10 punteros void. El 2 dice fp2 es un puntero a funcin que toma tres argumentos (int, int y oat) de retorna un puntero a una funcin que toma un entero como argumento y retorna un oat Si necesita crear muchas deniciones complicadas, debera usar typedef. El nmero 3 muestra cmo un typedef ahorra tener que escribir una descripcin complicada cada vez. Dice Un fp3 es un puntero a una funcin que no tiene argumentos y que retorna un puntero a un array de 10 punteros a funciones que no tienen argumentos y retornan doubles. Despus dice a es una variable de ese tipo fp3. typedef es til para construir descripciones complicadas a partir de otras simples. El 4 es una declaracin de funcin en lugar de una denicin de variable. Dice f4 es una funcin que retorna un puntero a un array de 10 punteros a funciones que retornan enteros. Es poco habitual necesitar declaraciones y deniciones tan complicadas como stas. Sin embargo, si se propone entenderlas, no le desconcertarn otras algo menos complicadas pero que si encontrar en la vida real.
6
(N. del T.) Otra forma similar de entenderlo es dibujar mentalmente una espiral que empieza en el medio (el identicador) y se va abriendo.
134
Una vez denido el puntero a funcin fp, se le asigna la direccin de una funcin func() usando fp = func (fjese que la lista de argumentos no aparece junto al nombre de la funcin). El segundo caso muestra una denicin e inicializacin simultnea.
135
Captulo 3. C en C++
void (*func_table[])() = { a, b, c, d, e, f, g }; int main() { while(1) { cout << "press a key from a to g " "or q to quit" << endl; char c, cr; cin.get(c); cin.get(cr); // second one for CR if ( c == q ) break; // ... out of while(1) if ( c < a || c > g ) continue; (*func_table[c - a])(); } }
A partir de este punto, debera ser capaz de imaginar cmo esta tcnica podra resultarle til cuando tenda que crear algn tipo de intrprete o programa para procesar listas.
En problema con este mtodo es que el compilador compilar cada chero individual tanto si el chero necesita ser recompilado como sino. Cuando un proyecto tiene muchos cheros, puede resultar prohibitivo recompilar todo cada vez que se cambia una lnea en un chero. La solucin a este problema, desarrollada en Unix pero disponible de aln modo en todos los sistemas es un programa llamado make. La utilidad make maneja todos los cheros individuales de un proyecto siguiendo las instrucciones escritas en un chero de texto llamado makefile. Cuando edite alguno de los cheros del proyecto y ejecute make, el programa make seguir las directrices del makefile para comparar las fechas de los cheros fuente con las de los cheros resultantes correspondientes, y si una chero fuente es ms reciente que su chero resultante, make recompila ese chero fuente. make slo recompila los cheros fuente que han cambiado, y cualquier otro chero que est afectado por el chero modicado. Usando make no tendr que recompilar todos los cheros de su proyecto cada vez que haga un cambio, ni tendr que comprobar si todo se construye adecuadamente. El makefile contiene todas las instrucciones para montar el proyecto. Aprender a usar make le permitir ahorrar mucho tiempo y frustraciones. Tambin descubrir que make es el mtodo tpico para instalar software nuevo en mquinas GNU o Unix 7 (aunque esos makefiles tienen a ser mucho ms complicados que los que aparecen en este libro, y a menudo podr generar automticamente un makefile para su mquina particular como parte del proceso de instalacin). Como make est disponible de algn modo para prcticamente todos los compiladores de C++ (incluso si no lo est, puede usar makes libres con cualquier compilador), ser la herramienta usada en este libro. Sin embargo, los fabricantes de compiladores crean tambin sus propias herramientas para construir proyectos. Ests herramientas preguntan qu cheros hay en el proyecto y determinan las relaciones entre ellos. Estas herramientas utilizan algo similar a un makefile, normalmente llamado chero de proyecto, pero el entorno de programacin mantiene este chero para que el programador no tenga que preocuparse de l. La conguracin y uso de los cheros de proyecto vara de un entorno de desarrollo a otro, de modo que tendr que buscar la documentacin apropiada en
7 (N. de T.) El mtodo del que habla el autor se reere normalmente a software instalado a partir de su cdigo fuente. La instalacin de paquetes binarios es mucho ms simple y automatizada en la mayora de las variantes actuales del sistema operativo GNU.
136
3.11. Make: cmo hacer compilacin separada cada caso (aunque esas herramientas proporcinadas por el fabricante normalmente son tan simples de usar que es fcil aprender a usarlas jugando un poco con ellas - mi mtodo educativo favorito). Los makefiles que acompaan a este libro deberan funcionar bien incluso si tambin usa una herramienta especca para construccin de proyectos.
Esto dice que hello.exe (el objetivo) depende de hello.cpp. Cuando hello.cpp tiene una fecha ms reciente que hello.exe, make ejecuta la regla mycompiler hello.cpp. Puede haber mltiples dependencias y mltiples reglas. Muchas implementaciones de make requieren que todas las reglas empiecen con un tabulador. Para lo dems, por norma general los espacios en blanco se ignoran de modo que se pueden usar a efectos de legibilidad. Las reglas no estn restringidas a llamadas al compilador; puede llamar a cualquier programa que quiera. Creando grupos de reglas de dependencia, puede modicar sus cheros fuentes, escribir make y estar seguro de que todos los chero afectados sern re-construidos correctamente.
Macros
Un makefile puede contener macros (tenga en cuenta que estas macros no tienen nada que ver con las del preprocesador de C/C++). La macros permiten reemplazar cadenas de texto. Los makefiles del libro usan una macro para invocar el compilador de C++. Por ejemplo,
CPP = mycompiler hello.exe: hello.cpp $(CPP) hello.cpp
El = se usa para indicar que CPP es una macro, y el $ y los parntesis expanden la macro. En este caso, la expansin signica que la llamada a la macro $(CPP) ser reemplazada con la cadena mycompiler. Con esta macro, si quiere utilizar un compilador diferente llamado cpp, slo tiene que cambiar la macro a:
CPP = cpp
Tambin puede aadir a la macro opciones del compilador, etc., o usar otras macros para aadir dichas opciones.
Reglas de sujo
Es algo tedioso tener que decir a make que invoque al compilador para cada chero cpp del proyecto, cuando se sabe que bsicamente siempre es el mismo proceso. Como make est diseado para ahorrar tiempo, tambin tiene un modo de abreviar acciones, siempre que dependan del sujo de los cheros. Estas abreviaturas se llaman reglas de sujo. Una regla de sujo es la la forma de indicar a make cmo convertir un chero con cierta extensin (.cpp por ejemplo) en un chero con otra extensin (.obj o .exe). Una vez que le haya indicado a make las reglas pra producir un tipo de chero a partir de otro, lo nico que tiene que hacer es decirle a make cuales son las dependencias respecto a otros cheros. Cuando make encuentra un chero con una fecha previa a otro chero del 137
Captulo 3. C en C++ que depende, usa la rella para crear al versin actualizada del chero objetivo. La regla de sujo le dice a make que no se necesitan reglas explcitas para construir cada cosa, en su lugar le explica cmo construir cosas en base a la extensin del chero. En este caso dice Para contruir un chero con extensin .exe a partir de uno con extensin .cpp, invocar el siguiente comando. As sera para ese ejemplo:
CPP = mycompiler .SUFFIXES: .exe .cpp .cpp.exe: $(CPP) $<
La directiva .SUFFIXES le dice a make que debe vigilar las extensiones que se indican porque tiene un signicado especial para este makefile en particular. Lo siguiente que aparece es la regla de sujo .cpp.exe, que dice cmo convertir cualquier chero con extensin .cpp a uno con extensin .exe (cuando el chero .cpp es ms reciente que el chero ..exe). Como antes, se usa la macro $(CPP), pero aqu aparece algo nuevo: $<. Como empieza con un $ es que es una macro, pero esta es una de las macros especiales predenidas por make. El $< se puede usar slo en reglas de sujom y signica cualquier prerrequisito que dispare la regla (a veces llamado dependencia), que en este caso se reere al chero .cpp que necesita ser compilado. Una ver que las reglas de sujo se han jado, puede indicar por ejemplo algo tan simple como make Union.exe y se aplicar la regla sujo, incluso aunque no se mencione Union en ninguna parte del makefile.
Objetivos predeterminados
Despus de las macros y las reglas de sujo, make busca la primero regla del chero, y la ejecuta, a menos que se especica una regla diferente. As que pare el siguiente makefile:
CPP = mycompiler .SUFFIXES: .exe .cpp .cpp.exe: $(CPP) $< target1.exe: target2.exe:
Si ejecuta simplemtente make, se construir target1.exe (usando la regla de sujo predeterminada) porque ese es el primer objetivo que make va a encontrar. Para construir target2.exe se debe indicar explcitamente diciendo make target2.exe. Esto puede resultar tedioso de modo que normalmente se crea un objetivo dummy por defecto que depende del resto de objetivos, como ste:
CPP = mycompiler .SUFFIXES: .exe .cpp .cpp.exe: $(CPP) $< all: target1.exe target2.exe
Aqu, all no existe y no hay ningn chero llamada all, as que cada vez que ejecute make, el programa ver que all es el primer objetivo de la lista (y por tanto el objetivo por defecto), entonces comprobar que all no existe y analizar sus dependencias. Comprueba target1.exe y (usando la regla de sujo) comprobar (1) que target1.exe existe y (2) que target1.cpp es ms reciente que target1.exe , y si es as ejecutar la regla (si proporciona una regla explcita para un objetivo concreto, se usar esa regla en su lugar). Despus pasa a analizar el siguiente chero de la lista de objetivos por defecto. De este modo, breando una lista de objetivos por defecto (tpicamente llamada all por convenio, aunque se puede tener cualquier nombre) puede conseguir que se construyan todos los ejecutables de su proyecto simplemente escribiendo make. Adems, puede tener otras listas de objetivos para hacer otras cosas - por ejemplo, podra hacer que escribiendo make debug se reconstruyeran todos los cheros pero incluyendo informacin de depuracin. 138
139
Captulo 3. C en C++ La macro CPP contine el nombre del compilador. Para usar un compilador diferente, puede editar el makefile o cambiar el valor de la macro desde lnea de comandos, algo como:
make CPP=cpp
Tenga en cuenta, sin embargo, que ExtractCode.cpp tiene un esquema automtico para construir makefiles para compiladores adicionales. La segunda macro OFLAG es la opcin que se usa para indicar el nombre del chero de salida. Aunque muchos compiladores asumen automticamente que el chero de salida tiene el mismo nombre base que el chero de entrada, otros no (como los compiladores GNU/Unix, que por defecto crean un chero llamado a.out). Como ve, hay dos reglas de sujo, una para cheros .cpp y otra para chero .c (en caso de que se necesite compilar algn fuente C). El objetivo por defecto es all, y cada lnea de este objetivo est continuada usando la contrabarra, hasta Guess2, que el el ltimo de la lista y por eso no tiene contrabarra. Hay muchos ms chero en este captulo, pero (por brevedad) slo se muestran algunos. Las reglas de sujo se ocupan de crear chero objeto (con extensin .o) a partir de los chero .cpp, pero en general se necesita escribir reglas explcitamente para crear el ejecutable, porque normalmente el ejecutable se crea enlazando muchos chero objeto diferente y make no puede adivinar cuales son. Tambin, en este caso (GNU/Unix) no se usan extensiones estndar para los ejecutables de modo que una regla de sujo no sirve para esas situaciones. Por eso, ver que todas las reglas para construir el ejecutable nal se indican explcitamente. Este makefile toma el camino ms seguro usando el mnimo de prestaciones de make; slo usa los conceptos bsicos de objetivos y dependencias, y tambin macros. De este modo est prcticamente asegurado que funcionar con la mayora de las implementaciones de make. Eso implica que se producen chero makefile ms grandes, pero no es algo negativo ya que se generan automticamente por ExtractCode.cpp. Hay muchsimas otras prestaciones de make que no se usan en este libro, incluyendo las versiones ms nuevas e inteligentes y las variaciones de make con atajos avanzados que permiten ahorrar mucho tiempo. La documentacin propia de cada make particular describe en ms profundidad sus caracterticas; puede aprender ms sobre make en Managing Projects with Make de Oram y Taiboot (OReilly, 1993). Tambin, si el fabricante de su compilador no proporciona un make o usa uno que no es estndar, puede encontrar GNU Make para prcticamente todas las plataformas que existen buscado en los archivos de GNU en internet (hay muchos).
3.12. Resumen
Este captulo ha sido un repaso bastante intenso a travs de todas las caractersticas fundamentales de la sintaxis de C++, la mayora heredada de C (y ello redunda la compatibilidad hacia atrs FIXME:vaunted de C++ con C). Aunque algunas caractersticas de C++ se han presentado aqu, este repaso est pensado principalmente para personas con experiencia en programacin, y simplemente necesitan una introduccin a la sintaxis bsica de C y C++. Incluso si usted ya es un programador de C, puede que haya visto una o dos cosas de C que no conoca, aparte de todo lo referente a C++ que propablemente sean nuevas. Sin embargo, si este captulo le ha sobrepasado un poco, debera leer el curso en CD ROM Thinking in C: Foundations for C++ and Java que contiene lecturas, ejercicios, y soluciones guiadas), que viene con este libro, y tambin est disponible en www.BruceEckel.com.
3.13. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree un chero de cabecera (con extensin .h). En este chero, declare un grupo de funciones variando las listas de argumentos y valores de retorno de entre los siquientes: void, char, int y float. Ahora cree un chero .cpp que incluya su chero de cabecera y haga deniciones para todas esas funciones. Cada denicin simplemente debe imprimir en nombre de la funcin, la lista de argumentos y el tipo de retorno para que se sepa que ha sido llamada. Cree un segundo chero .cpp que incluya el chero de cabecera y dena una int main(), que contenga llamadas a todas sus funciones. Compile y ejecute su programa. 2. Escriba un programa que use dos bucles for anidados y el operador mdulo (%) para detectar e imprimir nmeros enteros (nmeros enteros slo divisibles entre si mismos y entre 1). 140
3.13. Ejercicios 3. Escriba un programa que use utilice un bucle while para leer palabras de la entrada estndar (cin) y las guarde en un string. Este es un bucle while innito, que debe romper (y salir del programa) usando la sentencia break. Por cada palabra que lea, evaluela primero usando una secuencia de sentencias if para mapear un valor entero de la palabra, y despus use una sentencia switch que utilice ese valor entero como selector (esta secuencia de eventos no es un buen estilo de programacin; solamente es un supuesto para que practique con el control de ujo). Dentro de cada case, imprima algo con sentido. Debe decidir cuales son las palabras interesantes y qu signican. Tambin debe decidir qu palabra signica el n del programa. Pruebe el programa redireccionando un chero como entrada (si quiere ahorrarse tener que escribir, ese chero puede ser el propio cdigo fuente del programa). 4. Modique Menu.cpp para usar setencias switch en lugar de if. 5. Escriba un programa que evalue las dos expresiones de la seccin llamada precedencia. 6. Modique YourPets2.cpp para que use varios tipos de datos distintos (char, int, oat, double, y sus variantes). Ejecute el programa y cree un mapa del esquema de memoria resultante. Si tiene acceso a ms de un tipo de mquina, sistema operativo, o compilador, intente este experimento con tantas variaciones como pueda manejar. 7. Cree dos funciones, una que tome un string* y una que tome un string&. Cada una de estas funciones debera modicar el objeto externo a su manera. En main(), cree e inicialice un objeto string, imprmalo, despus pselo a cada una de las dos funciones, imprimiendo los resultados. 8. Escriba un programa que use todos los trgrafos para ver si su compilador los soporta. 9. Compile y ejecute Static.cpp. Elimine la palabra reservada static del cdigo, compile y ejecutelo de nuevo, y explique lo que ocurre. 10. Intente compilar y enlazar FileStatic.cpp con FileStatic2.cpp. Qu signican los mensajes de error que aparecen? 11. Modique Boolean.cpp para que funcione con valores double en lugar de int. 12. Modique Boolean.cpp y Bitwise.cpp de modo que usen los operadores explcitos (si su compilador es conforme al Estndar C++ los soportar). 13. Modique Bitwise.cpp para usar las funciones de Rotation.cpp. Asegures que muestra los resultados que deje claro qu ocurre durante las rotaciones. 14. Modique Ifthen.cpp para usar el operador if-else ternario(?:). 15. Cree una struct que contenga dos objetos string y uno int. Use un typedef para el nombre de la struct. Cree una instancia de la struct, inicialice los tres valores de la instancia, y muestrelos en pantalla. Tome la direccin de su instancia y asignela a un puntero a tipo de la struct. Usando el puntero, Cambie los tres valores de la instancia y muestrelos. 16. Cree un programa que use un enumerado de colores. Cree una variable de este tipo enum y, utilizando un bucle, muestre todos los nmeros que corresponden a los nombres de los colores. 17. Experimente con Union.cpp eliminando varios elementos de la union para ver el efecto que causa en el tamao de la union resultante.. Intente asignar un elemento (por tanto un tipo) de la union y mustrelo por medio de un elemento diferente (por tanto, un tipo diferente) para ver que ocurre. 18. Cree un programa que dena dos arrays de int, uno a continuacin del otro. Indexe el primer array ms all de su tamao para caer sobre el segundo, haga una asignacin. Muestre el segundo array para ver los cambios que eso ha causado. Ahora intente denir una variable char entre las deniciones de los arrys, y repita el experimento. Quiz quiera crear una funcin para imprimir arrays y as simplicar el cdigo. 19. Modique ArrayAddresses.cpp para que funcione con los tipos de datos char, long int, oat y double. 20. Aplique la tcnica de ArrayAddresses.cpp para mostrar el tamao de la struct y las direcciones de los elementos del array de StructArray.cpp. 21. Cree un array de objetos string y asigne una cadena a cada elemento. Muestre el array usando un bucle for. 22. Cree dos nuevos programas a partir de ArgsToInts.cpp que usen atol() y atof() respectivamente. 141
Captulo 3. C en C++ 23. Modique PointerIncrement2.cpp de modo que use una union en lugar de una struct. 24. Modique PointerArithmetic.cpp para que funcione con long y long double. 25. Dena una variable oat. Tome su direccin, moldee esa direccin a un unsigned char, y asgnela a un puntero unsigned char. Usando este puntero y [], indexe la variable oat y use la funcin printBinary() denida en este captulo para mostrar un mapa de cada oat (vaya desde 0 hasta sizeof(float)). Cambie el valor del oat y compuebe si puede averiguar que hay en el oat (el oat contiene datos codicados). 26. Dena un array de int. Tome la direccin de comienzo de ese array y utilice static_cast para convertirlo a un void*. Escriba una funcin que tome un void*, un nmero (que indica el nmero de bytes), y un valor (indicando el valor que debera ser asignado a cada byte) como argumentos. La funcin debera asignar a cada byte en el rango especivado el valor dado como argumento. Pruebe la funcin con su array de int. 27. Cree un array const de double y un array volatile de double. Indexe cada array y utilice const_cast para moldear cada elemento de no-const y no-volatile, respectivamente, y asigne un valor a cada elemento. 28. Cree una funcin que tome un puntero a un array de double y un valor indicando el tamao de ese array. La funcin debera mostrar cada valor del array. Ahora cree un array de double y inicialice cada elemento a cero, despus utilice su funcin para mostrar el array. Despus use reinterpret_cast para moldear la direccin de comienzo de su array a un unsigned char*, y ponga a 1 cada byte del array (aviso: necesitar usar sizeof para calcular el nmero de bytes que tiene un double). Ahora use su funcin de impresin de arrays para mostrar los resultados. Por qu cree los elementos no tienen el valor 1.0? 29. (Reto) Modique FloatingAsBinary.cpp para que muestra cada parte del double como un grupo separado de bits. Tendr que reemplazar las llamadas a printBinary() con su propio cdigo especco (que puede derivar de printBinary()) para hacerlo, y tambin tendr que buscar y comprender el formato de punto otante incluyendo el ordenamiento de bytes para su compilador (esta parte es el reto). 30. Cree un makefile que no slo compile YourPets1.cpp y YourPets2.cpp (para cada compilador particular) sino que tambin ejecute ambos programas como parte del comportamiento del objetivo predeterminado. Asegrese de usar las reglas de sujo. 31. Modique StringizingExpressions.cpp para que P(A) sea condicionalmente denida con #ifdef para permitir que el cdigo de depuracin sea eliminado automticamente por medio de una bandera en lnea de comandos. Necesitar consultar la documentacin de su compilador para ver cmo denir y eliminar valores del preprocesador en el compilador de lnea de comandos. 32. Dena una funcin que tome un argumento double y retorne un int. Cree e inicialice un puntero a esta funcin, e invoque la funcin por medio del puntero. 33. Declare un puntero a un funcin que toma un argumento int y retorna un puntero a una funcin que toma un argumento char y retorna un oat. 34. Modique FunctionTable.cpp para que cada funcin retorne un string (en lugar de mostrar un mensaje) de modo que este valor se imprima en main(). 35. Cree un makefile para uno de los ejercicios previos (a su eleccin) que le permita escribir make para construir una versin en produccin del programa y make debug para construir una versin del programa que incluye informacin de depuracin.
142
4: Abstraccin de Datos
C++ es una herramienta de mejora de la productividad. Por qu sino hara el esfuerzo (y es un esfuerzo, a pesar de lo fcil que intetemos hacer la transicin)
de cambiar de algn lenguaje que ya conoce y con el cual ya es productivo a un nuevo lenguaje con el que ser menos productivo durante un tiempo, hasta que se haga con l? Se debe a que est convencido de que conseguir grandes ventajas usando esta nueva herramienta. En trminos de programacin, productividad signica que menos personas, en menos tiempo, puedan realizar programas ms complejos y signicativos. Desde luego, hay otras cuestiones que nos deben importar a la hora de escoger un lenguaje de programacin. Aspectos a tener en cuenta son la eciencia (la naturaleza del lenguaje hace que nuestros programas sean lentos o demasiado grandes?), la seguridad (nos ayuda el lenguaje a asegurarnos de que nuestros programas hagan siempre lo que queremos? maneja el lenguaje los errores apropiadamente?) y el mantenimiento (el lenguaje ayuda a crear cdigo fcil de entender, modicar y extender?). Estos son, con certeza, factores importantes que se examinarn en este libro. Pero la productividad real signica que un programa que para ser escrito, antes requera de tres personas trabajando una semana, ahora le lleve slo un da o dos a una sola persona. Esto afecta a varios niveles de la esfera econmica. A usted le agrada ver que es capaz de construir algo en menos tiempo, sus clientes (o jefe) son felices porque los productos les llegan ms rpido y utilizando menos mano de obra y nalmente los compradores se alegran porque pueden obtener productos ms baratos. La nica manera de obtener incrementos masivos en productividad es apoyndose en el cdigo de otras personas; o sea, usando libreras. Una librera es simplemente un montn de cdigo que alguien ha escrito y empaquetado todo junto. Muchas veces, el paquete mnimo es tan slo un archivo con una extensin especial como lib y uno o ms archivos de cabecera que le dicen al compilador qu contiene la librera. El enlazador sabr cmo buscar el archivo de la librera y extraer el cdigo compilado correcto. Sin embargo, sta es slo una forma de entregar una librera. En plataformas que abarcan muchas arquitecturas, como GNU o Unix, el nico modo sensato de entregar una librara es con cdigo fuente para que as pueda ser recongurado y reconstruido en el nuevo objetivo. De esta forma, las libreras probablemente sean la forma ms importante de progresar en trminos de productividad y uno de los principales objetivos del diseo de C++ es hacer ms fcil el uso de libreras. Esto implica entonces, que hay algo difcil al usar libreras en C. Entender este factor le dar una primera idea sobre el diseo de C++, y por lo tanto, de cmo usarlo.
143
//: C04:CLib.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Header file for a C-like library // An array-like entity created at runtime typedef struct CStashTag { int size; // Size of each space int quantity; // Number of storage spaces int next; // Next empty space // Dynamically allocated array of bytes: unsigned char* storage; } CStash; void initialize(CStash* s, int size); void cleanup(CStash* s); int add(CStash* s, const void* element); void* fetch(CStash* s, int index); int count(CStash* s); void inflate(CStash* s, int increase);
Normalmente se utiliza un rtulo como CStashTag en aquellas estructuras que necesitan referenciarse dentro de si mismas. Ese es el caso de una lista enlazada (cada elemento de la lista contiene un puntero al siguiente elemento) se necesita un puntero a la siguiente variable estructura, o sea, una manera de identicar el tipo de ese puntero dentro del cuerpo de la propia estructura. En la declaracin de las estructuras de una librera escrita en C tambin es muy comn ver el uso de typedef como el del ejemplo anterior. Esto permite al programador tratar las estructuras como un nuevo tipo de dato y as denir nuevas variables (de esa estructura) del siguiente modo:
CStash A, B, C;
El puntero storage es un unsigned char*. Un unsigned char es la menor pieza de datos que permite un compilador C, aunque en algunas mquinas puede ser de igual tamao que la mayor. Aunque es dependiente de la implementacin, por lo general un unsigned char tiene un tamao de un byte. Dado que CStash est diseado para almacenar cualquier tipo de estructura, el lector se puede preguntar si no sera ms apropiado un puntero void *. Sin embargo, el objetivo no es tratar este puntero de almacenamiento como un bloque de datos de tipo desconocido, sino como un bloque de bytes contiguos. El archivo de cdigo fuente para la implementacin (del que no se suele disponer si fuese una librera comercial - normalmente slo dispondr que un .obj, .lib o .dll, etc) tiene este aspecto:
//: C04:CLib.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Implementation of example C-like library // Declare structure and functions: #include "CLib.h" #include <iostream> #include <cassert> using namespace std; // Quantity of elements to add // when increasing storage: const int increment = 100; void initialize(CStash* s, int sz) {
144
initialize() realiza las operaciones iniciales necesarias para la struct CStash, poniendo los valores apropiados en las variables internas. Inicialmente, el puntero storage tiene un cero dado que an no se ha almacenado nada. La funcin add() inserta un elemento en el siguiente lugar disponible de la CStash. Para lograrlo, primero verica que haya suciente espacio disponible. Si no lo hay, expande el espacio de almacenamiento (storage) usando la funcin inflate() que se describe despus. Como el compilador no conoce el tipo especco de la variable que est siendo almacenada (todo lo que obtiene la funcin es un void*), no se puede hacer una asignacin simple, que sera lo ms conveniente. En lugar de eso, la variable se copia byte a byte. La manera ms directa de hacerlo es utilizando el indexado de arrays. Lo habitual es 145
Captulo 4. Abstraccin de Datos que en storage ya haya bytes almacenados, lo cual es indicado por el valor de next. Para obtener la posicin de insercin correcta en el array, se multiplica next por el tamao de cada elemento (en bytes) lo cual produce el valor de startBytes. Luego el argumento element se moldea a unsigned char* para que se pueda direccionar y copiar byte a byte en el espacio disponible de storage. Se incrementa next de modo que indique el siguiente lugar de almacenamiento disponible y el ndice en el que ha almacenado el elemento para que el valor se puede recuperar utilizando el ndice con fetch(). fetch() verica que el ndice tenga un valor correcto y devuelve la direccin de la variable deseada, que se calcula en funcin del argumento index. Dado que index es un desplazamiento desde el principio en la CStash, se debe multiplicar por el tamao en bytes que ocupa cada elemento para obtener dicho desplazamiento en bytes. Cuando utilizamos este desplazamiento como ndice del array storage lo que obtenemos no es la direccin, sino el byte almacenado. Lo que hacemos entonces es utilizar el operador direccin-de &. count() puede parecer un poco extraa a los programadores experimentados en C. Podra parecer demasiado complicada para una tarea que probablemente sea mucho ms fcil de hacer a mano. Por ejemplo, si tenemos una CStash llamada intStash, es mucho ms directo preguntar por la cantidad de elementos utilizando intStash.next, que llamar a una funcin (que implica sobrecarga), como count(&intStash). Sin embargo, la cantidad de elementos se calcula en funcin tanto del puntero next como del tamao en bytes de cada elemento de la CStash; por eso la interfaz de la funcin count() permite la exibilidad necesaria para no tener que preocuparnos por estas cosas. Pero, ay!, la mayora de los programadores no se preocuparn por descubrir lo que para nosotros es el mejor diseo para la librera. Probablemente lo que harn es mirar dentro de la estructura y obtener el valor de next directamente. Peor an, podran incluso cambiar el valor de next sin nuestro permiso. Si hubiera alguna forma que permitiera al diseador de la librera tener un mejor control sobre este tipo de cosas! (S, esto es un presagio).
donde Tipo describe el tipo de variable para la cual se solicita memoria en el montculo. Dado que en este caso, se desea asignar memoria para un array de unsigned char de newBytes elementos, eso es lo que aparece como Tipo. Del mismo modo, se puede asignar memoria para algo ms simple como un int con la expresin:
2
146
new int;
y aunque esto se utiliza muy poco, demuestra que la sintaxis es consistente. Una expresin-new devuelve un puntero a un objeto del tipo exacto que se le pidi. De modo que con new Tipo se obtendr un puntero a un objeto de tipo Tipo, y con new int obtendr un puntero a un int. Si quiere un nuevo array de unsigned char la expresin devolver un puntero al primer elemento de dicho array. El compilador vericar que se asigne lo que devuelve la expresin-new a una variable puntero del tipo adecuado. Por supuesto, es posible que al pedir memoria, la peticin falle, por ejemplo, si no hay ms memoria libre en el sistema. Como ver ms adelante, C++ cuenta con mecanismos que entran en juego cuando la operacin de asignacin de memoria no se puede satisfacer. Una vez que se ha obtenido un nuevo espacio de almacenamiento, los datos que estaban en el antiguo se deben copiar al nuevo. Esto se hace, nuevamente, en un bucle, utilizando la notacin de ndexado de arrays, copiando un byte en cada iteracin del bucle. Una vez nalizada esta copia, ya no se necesitan los datos que estn en el espacio de almacenamiento original por lo que se pueden liberar de la memoria para que otras partes del programa puedan usarlo cuando lo necesiten. La palabra reservada delete es el complemento de new y se debe utilizar sobre todas aquellas variables a las cuales se les haya asignado memoria con new. (Si se olvida de utilizar delete esa memoria queda in-utilizable. Si estas fugas de memoria (memory leak) son demasiado abundantes, la memoria disponible se acabar.) Existe una sintaxis especial cuando se libera un array. Es como si recordara al compilador que ese puntero no apunta slo a un objeto, sino a un array de objetos; se deben poner un par de corchetes delante del puntero que se quiere liberar:
delete []myArray;
Una vez liberado el antiguo espacio de almacenamiento, se puede asignar el puntero del nuevo espacio de memoria al puntero storage, se actualiza quantity y con eso inflate() ha terminado su trabajo. En este punto es bueno notar que el administrador de memoria del montculo> es bastante primitivo. Nos facilita trozos de memoria cuando se lo pedimos con new y los libera cuando invocamos a delete. Si un programa asigna y libera memoria muchas veces, terminaremos con un montculo fragmentado, es decir un montculo en el que si bien puede haber memoria libre utilizable, los trozos de memoria estn divididos de tal modo que no exista un trozo que sea lo sucientemente grande para las necesidades concretas en un momento dado. Lamentablemente no existe una capacidad inherente del lenguaje para efectuar defragmentaciones del montculo. Un defragmentador del montculo complica las cosas dado que tiene que mover pedazos de memoria, y por lo tanto, hacer que los punteros dejen de apuntar a valores vlidos. Algunos entornos operativos vienen con este tipo de facilidades pero obligan al programador a utilizar manejadores de memoria especiales en lugar de punteros (estos manipuladores se pueden convertir temporalmente en punteros una vez bloqueada la memoria para que el defragmentador del montculo no la modique). Tambin podemos construir nosotros mismos uno de estos artilugios, aunque no es una tarea sencilla. Cuando creamos una variable en la pila en tiempo de compilacin, el mismo compilador es quien se encarga de crearla y liberar la memoria ocupada por ella automticamente. Conoce exactamente el tamao y la duracin de este tipo de variables dada por las reglas de mbito. Sin embargo, en el caso de las variables almacenadas dinmicamente, el compilador no poseer informacin ni del tamao requerido por las mismas, ni de su duracin. Esto signica que el compilador no puede encargarse de liberar automticamente la memoria ocupada por este tipo de variables y de aqu que el responsable de esta tarea sea el programador (o sea usted). Para esto se debe utilizar delete, lo cual le indica al administrador del montculo que ese espacio de memoria puede ser utilizado por prximas llamadas a new. En nuestra librera de ejemplo, el lugar lgico para esta tarea es la funcin cleanup() dado que all es dnde se deben realizar todas las labores de nalizacin de uso del objeto. Para probar la librera se crean dos Cstash, uno que almacene enteros y otro para cadenas de 80 caracteres:
//: C04:CLibTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} CLib // Test the C-like library
147
Dado que debemos respetar la sintaxis de C, todas las variables se deben declarar al comienzo de main(). Obviamente, no nos podemos olvidar de inicializar todas las variables Cstash ms adelante en el bloque main(), pero antes de usarlas, llamando a initialize(). Uno de los problemas con las libreras en C es que uno debe asegurarse de convencer al usuario de la importancia de las funciones de inicializacin y destruccin. Habr muchos problemas si estas funciones se omiten! Lamentablemente el usuario no siempre se preguntar si la inicializacin y el limpiado de los objetos son obligatorios. Ellos le darn importancia a lo que ellos quieren hacer y no nos darn tanta importancia a nosotros (el programador de la librera) cuando les digamos Hey! espera un poco! Debes hacer esto primero!. Otro problema que puede presentarse es el hecho de que algunos usuarios quieran inicializar los elementos (datos internos) de una estructura por su cuenta. En C no hay un mecanismo para prevenir este tipo de conductas (ms presagios de los tems que vendrn...). La intStash se va llenando con enteros mientras que el stringStash se va llenando con arrays de caracteres. Estos arrays de caracteres son producidos leyendo el archivo fuente CLibTest.cpp y almacenando las lneas de este archivo en el string line. Obtenemos la representacin puntero a carcter de line con el mtodo c_str(). Una vez cargados los Stash ambos se muestran en pantalla. intStash se imprime usando un bucle for en el cual se usa count() para determinar la cantidad de elementos. El stringStash se muestra utilizando un bucle while dentro del cual se va llamando a fetch(). Cuando esta funcin devuelve cero se rompe el bucle ya que esto signicar que se han sobrepasado los lmites de la estructura. El lector tambin pudo haber visto un molde adicional en la lnea:
cp = (char*)fetch(&stringStash, i++)
148
4.2. Qu tiene de malo? Esto se debe a la comprobacin estricta de tipos en C++, que no permite asignar un void * a una variable de cualquier tipo, mientras que C s lo hubiera permitido.
Captulo 4. Abstraccin de Datos nombres (name clashes). C trabaja con un nico espacio de nombres de funciones. Esto signica que, cuando el enlazador busca por el nombre de una funcin, lo hace en una nica lista de nombres maestra. Adems, cuando el compilador trabaja sobre una unidad de traduccin, un nombre de funcin slo puede hacer referencia a una nica funcin con ese nombre. Supongamos que compramos dos libreras de diferentes proveedores y que cada librera consta de una estructura que debe inicializar y destruir. Supongamos que cada proveedor ha decidido nombrar a dichas operaciones initialize() y cleanup(). Cmo se comportara el compilador si incluyramos los archivos de cabecera de ambas libreras en la misma unidad de traduccin? Afortunadamente, el compilador C dar un mensaje de error dicindonos que hay una incoherencia de tipos en las listas de argumentos de ambas declaraciones. No obstante, aunque no incluyamos los archivos de cabecera en la unidad de traduccin igual tendremos problemas con el enlazador. Un buen enlazador detectar y avisar cuando se produzca uno de estos conictos de nombres. Sin embargo, hay otros que simplemente tomarn el primer nombre de la funcin que encuentren, buscando en los archivos objeto en el orden en el que fueron pasados en la lista de enlazado. (Este comportamiento se puede considerar como una ventaja ya que permite reemplazar las funciones de las libreras ajenas con funciones propias.) En cualquiera de los dos casos, llegamos a la conclusin de que en C es imposible usar dos bibliotecas en las cuales existan funciones con nombres idnticos. Para solucionar este problema, los proveedores de libreras en C ponen un prejo nico a todas las funciones de la librera. En nuestro ejemplo, las funciones initialize() y cleanup() habra que renombrarlas como CStash_initialize() y CStash_cleanup(). Esta es una tcnica lgica: decoramos los nombres de las funciones con el nombre de la estructura sobre la cual trabajan. Este es el momento de dirigir nuestros pasos a las primeras nociones de construccin de clases en C++. Como el lector ha de saber, las variables declaradas dentro de una estructura no tienen conictos de nombres con las variables globales. Por qu, entonces, no aprovechar esta caracterstica de las variables para evitar los conictos de nombres de funciones declarndolas dentro de la estructura sobre la cual operan? O sea, por qu no hacer que las funciones sean tambin miembros de las estructuras?
La primera diferencia que puede notarse es que no se usa typedef. A diferencia de C que requiere el uso de typedef para crear nuevos tipos de datos, el compilador de C++ har que el nombre de la estructura sea un nuevo tipo de dato automticamente en el programa (tal como los nombres de tipos de datos int, char, oat y double). 150
4.3. El objeto bsico Todos los datos miembros de la estructura estn declarados igual que antes; sin embargo, ahora las funciones estn declaradas dentro del cuerpo de la struct. Ms an, fjese que el primer argumento de todas las funciones ha sido eliminado. En C++, en lugar de forzar al usuario a que pase la direccin de la estructura sobre la que trabaja una funcin como primer argumento, el compilador har este trabajo, secretamente. Ahora slo debe preocuparse por los argumentos que le dan sentido a lo que la funcin hace y no de los mecanismos internos de la funcin. Es importante darse cuenta de que el cdigo generado por estas funciones es el mismo que el de las funciones de la librera al estilo C. El nmero de argumentos es el mismo (aunque no se le pase la direccin de la estructura como primer argumento, en realidad s se hace) y sigue existiendo un nico cuerpo (denicin) de cada funcin. Esto ltimo quiere decir que, aunque declare mltiples variables
Stash A, B, C;
no existirn mltiples deniciones de, por ejemplo, la funcin add(), una para cada variable. De modo que el cdigo generado es casi idntico al que hubiese escrito para una versin en C de la librera, incluyendo la decoracin de nombres ya mencionada para evitar los conictos de nombres, nombrando a las funciones Stash_initialize(), Stash_cleanup() y dems. Cuando una funcin est dentro de una estructura, el compilador C++ hace lo mismo y por eso, una funcin llamada initialize() dentro de una estructura no estar en conicto con otra funcin initialize() dentro de otra estructura o con una funcin initialize() global. De este modo, en general no tendr que preocuparse por los conictos de nombres de funciones - use el nombre sin decoracin. Sin embargo, habr situaciones en las que desear especicar, por ejemplo, esta initialize() pertenece a la estructura Stash y no a ninguna otra. En particular, cuando dena la funcin, necesita especicar a qu estructura pertenece para lo cual, en C++ cuenta con el operador :: llamado operador de resolucin de mbito (ya que ahora un nombre puede estar en diferentes mbitos: el del mbito global o dentro del mbito de una estructura. Por ejemplo, si quiere referirse a una funcin initialize() que se encuentra dentro de la estructura Stash lo podr hacer con la expresin Stash::initialize(int size). A continuacin podr ver cmo se usa el operador de resolucin de mbito para denir funciones:
//: C04:CppLib.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // C library converted to C++ // Declare structure and functions: #include "CppLib.h" #include <iostream> #include <cassert> using namespace std; // Quantity of elements to add // when increasing storage: const int increment = 100; void Stash::initialize(int sz) { size = sz; quantity = 0; storage = 0; next = 0; } int Stash::add(const void* element) { if(next >= quantity) // Enough space left? inflate(increment); // Copy element into storage, // starting at next empty space: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Index number
151
Hay muchas otras cosas que dieres entre C y C++. Para empezar, el compilador requiere que declare las funciones en los archivos de cabecera: en C++ no podr llamar a una funcin sin haberla declarado antes y si no se cumple esta regla el compilador dar un error. Esta es una forma importante de asegurar que las llamadas a una funcin son consistentes entre el punto en que se llama y el punto en que se dene. Al forzar a declarar una funcin antes de usarla, el compilador de C++ prcticamente se asegura de que realizar esa declaracin por medio de la inclusin de un chero de cabecera. Adems, si tambin incluye el mismo chero de cabecera en el mismo lugar donde se denes las funciones, el compilador vericar que las declaraciones del archivo cabecera y las deniciones coinciden. Puede decirse entonces que, de algn modo, los cheros de cabecera se vuelven un repositorio de validacin de funciones y permiten asegurar que las funciones se usan de modo consistente en todas las unidades de traduccin del proyecto. Obviamente, las funciones globales se pueden seguir declarando a mano en aquellos lugares en las que se denen y usan (Sin embargo, esta prctica es tan tediosa que est en desuso.) De cualquier modo, las estructuras siempre se deben declarar antes de ser usadas y el mejor lugar para esto es un chero de cabecera, exceptuando aquellas que queremos esconder intencionalmente en otro chero. Se puede ver que todas las funciones miembro (mtodos) tienen casi la misma forma que sus versiones respectivas en C. Las nicas diferencias son su mbito de resolucin y el hecho de que el primer argumento ya no aparece explcito en el prototipo de la funcin. Por supuesto que sigue ah ya que la funcin debe ser capaz de trabajar sobre una variable struct en particular. Sin embargo, fjese tambin que, dentro del mtodo, la seleccin de esta estructura en particular tambin ha desaparecido! As, en lugar de decir s->size = sz; ahora dice size = sz; eliminando el tedioso s-> que en realidad no aportaba nada al signicado semntico de lo que estaba escribiendo. Aparentemente, el compilador de C++ est realizando estas tareas por el programador. De hecho, est tomando el primer argumento secreto (la direccin de la estructura que antes tena que pasar a mano) y aplicndole el selector de miembro (->) siempre que escribe el nombre de uno de los datos miembro. Eso signica que, siempre y cuando est dentro de la denicin de una mtodo de una estructura puede hacer referencia a cualquier otro miembro (incluyendo otro mtodo) simplemente dando su nombre. El compilador buscar primero en los 152
4.3. El objeto bsico nombres locales de la estructura antes de buscar en versiones ms globales de dichos nombres. El lector podr descubrir que esta caracterstica no slo agiliza la escritura del cdigo, sino que tambin hace la lectura del mismo mucho ms sencilla. Pero qu pasara si, por alguna razn, quisiera hacer referencia a la direccin de memoria de la estructura. En la versin en C de la librera sta se poda obtener fcilmente del primer argumento de cualquier funcin. En C++ la cosa es ms consistente: existe la palabra reservada this que produce la direccin de la variable struct actual. Es el equivalente a la expresin s de la versin en C de la librera. De modo que, podremos volver al estilo de C escribiendo
this->size = Size;
El cdigo generado por el compilador ser exactamente el mismo por lo que no es necesario usar this en estos casos. Ocasionalmente, podr ver por ah cdigo dnde la gente usa this en todos sitios sin agregar nada al signicado del cdigo (esta prctica es indicio de programadores inexpertos). Por lo general, this no se usa muy a menudo pero, cuando se necesite siempre estar all (en ejemplos posteriores del libro ver ms sobre su uso). Queda an un ltimo tema que tocar. En C, se puede asignar un void * a cualquier otro puntero, algo como esto:
int i = 10; void* vp = &i; // OK tanto en C como en C++ int* ip = vp; // Slo aceptable en C
y no habr ningn tipo de queja por parte de compilador. Sin embargo, en C++, lo anterior no est permitido. Por qu? Porque C no es tan estricto con los tipos de datos y permite asignar un puntero sin un tipo especco a un puntero de un tipo bien determinado. No as C++, en el cual la vericacin de tipos es crtica y el compilador se detendr quejndose en cualquier conicto de tipos. Esto siempre ha sido importante, pero es especialmente importante en C++ ya que dentro de las estructuras puede hacer mtodos. Si en C++ estuviera permitido pasar punteros a estructuras con impunidad en cuanto a conicto de tipos, podra terminar llamando a un mtodo de una estructura en la cual no existiera dicha funcin miembro! Una verdadera frmula para el desastre. As, mientras C++ s deja asignar cualquier puntero a un void * (en realidad este es el propsito original del puntero a void: que sea sucientemente largo como para apuntar a cualquier tipo) no permite asignar un void * a cualquier otro tipo de puntero. Para ello se requiere un molde que le indique tanto al lector como al compilador que realmente quiere tratarlo como el puntero destino. Y esto nos lleva a discutir un asunto interesante. Uno de los objetivos importantes de C++ es poder compilar la mayor cantidad posible de cdigo C para as, permitir una fcil transicin al nuevo lenguaje. Sin embargo, eso no signica, como se ha visto que cualquier segmento de cdigo que sea vlido en C, ser permitido automticamente en C++. Hay varias cosas que un compilador de C permite hacer que son potencialmente peligrosas y propensas a generar errores (ver ejemplos de a lo largo de libro). El compilador de C++ genera errores y avisos en este tipo de situaciones y como ver eso es ms una ventaja que un obstculo a pesar de su naturaleza restrictiva. De hecho, existen muchas situaciones en las cuales tratar de detectar sin xito un error en C y cuando recompiles el programa con un compilador de C++ ste avisa exactamente de la causa del problema!. En C, muy a menudo ocurre que para que un programa funcione correctamente, adems de compilarlo, luego debe hacer que ande. En C++, por el contrario, ver que muchas veces si un programa compila correctamente es probable que funcione bien! Esto se debe a que este ltimo lenguaje es mucho ms estricto respecto a la comprobacin de tipos. En el siguiente programa de prueba podr apreciar cosas nuevas con respecto a cmo se utiliza la nueva versin de la Stash:
//: C04:CppLibTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} CppLib // Test of C++ library #include "CppLib.h" #include "../require.h" #include <fstream>
153
Una de las cosas que el lector habr podido observar en el cdigo anterior es que las variables se denen al vuelo, o sea (como se introdujo en el captulo anterior) en cualquier parte de un bloque y no necesariamente -como en C- al comienzo de los mismos. El cdigo es bastante similar al visto en CLibTest.cpp con la diferencia de que, cuando se llama a un mtodo, se utiliza el operador de seleccin de miembro . precedido por el nombre de la variable. Esta es una sntaxis conveniente ya que imita a la seleccin o acceso de un dato miembro de una estructura. La nica diferencia es que, al ser un mtodo, su llamada implica una lista de argumentos. Tal y cmo se dijo antes, la llamada que el compilador hace genera realmente es mucho ms parecida a la llamada a la funcin de la librera en C. Considere la decoracin de nombres y el paso del puntero this: la llamada en C++ de intStash.initialize(sizeof(int), 100) se transformar en algo parecido a Stash_initialize(&intStash, sizeof(int), 100). Si el lector se pregunta qu es lo que sucede realmente debajo del envoltorio, debera recordar que el compilador original de C++ cfront de AT&T produca cdigo C como salida que luego deba ser compilada con un compilador de C para generar el ejecutable. Este mtodo permita a cfront ser rpidamente portable a cualquier mquina que soportara un compilador estndar de C y ayud a la rpida difusin de C++. Dado que los compiladores antiguos de C++ tenan que generar cdigo C, sabemos que existe una manera de representar sntaxis C++ en C (algunos compiladores de hoy en da an permiten generar cdigo C). Comparando con CLibTest.cpp observar un cambio: la introduccin del chero de cabecera require. h. He creado este chero de cabecera para realizar una comprobacin de errores ms sosticada que la que proporciona assert(). Contiene varias funciones incluyendo la llamada en este ltimo ejemplo, assure() que se usa sobre cheros. Esta funcin verica que un chero se ha abierto exitosamente y en caso contrario reporta un aviso a la salida de error estndar (por lo que tambin necesita el nombre del chero como segundo argumento) y sale del programa. Las funciones de require.h se usan a lo largo de este libro especialmente para asegurar que se ha indicado la cantidad correcta de argumentos en la lnea de comandos y para vericar que los cheros se abren correctamente. Las funciones de require.h reemplazan el cdigo de deteccin de errores repetitivo y que muchas veces es causa de distracciones y ms an, proporcionan mensajes tiles para la deteccin de posibles errores. Estas funciones se explican detalladamente ms adelante. 154
4.4. Qu es un objeto?
4.4. Qu es un objeto?
Ahora que ya se ha visto y discutido un ejemplo incial es hora de retroceder para denir la terminologa. El acto de meter funciones dentro de las estructuras es el eje central del cambio que C++ propone sobre C e introduce una nueva forma de ver las estructuras: como conceptos. En C, una estructura (struct) es tan slo una aglomeracin de datos: una manera de empaquetar datos para que puedan ser tratados como un grupo. De esta forma, cuesta hacernos la idea de que representan algo ms que slo slo una conveniencia de programacin. Las funciones que operan sobre esas estructuras estn sueltas por ah. Sin embargo, cuando vemos las funciones dentro del mismo paquete que los datos la estructura se vuelve una nueva criatura, capz de representar caractersticas (como las structs de C) y comportamientos. El concepto de un objeto, una entidad independiente y bien limitada que puede recordar y actuar, se sugiere a si mismo como denicin. En C++ un objeto es simplemente una variable, y la denicin ms pura es una regin de almacenamiento (esto ltimo es una manera ms especca de decir un objeto debe tener un nico identicador el cual, en el caso de C++, es una nica direccin de memoria). Es un lugar en el cual se puede almacenar datos y en el cual est implcita la existencia de operaciones sobre esos datos. Desafortunadamente no existe una completa consistencia entre los distintos lenguajes cuando se habla de estos trminos aunque son bastante bien aceptados. Tambin se podrn encontrar discrepancias sobre lo que es un lenguaje orientado a objetos aunque parece estarse estableciendo un concenso sobre esto hoy en da tambin. Hay lenguajes que se denominan basados en objetos los cuales, cuentan con estructuras-con-funciones como las que hemos visto aqu de C++. Sin embargo, esto tan slo es una parte de lo que denominamos lenguaje orientado a objetos y armamos que los lenguajes que solamente llegan a empaquetar las funciones dentro de las estructuras son lenguajes basados en objetos y no orientados a objetos.
155
Captulo 4. Abstraccin de Datos cdigo, y que por alguna razn accedan directamente a los bytes de la estructura en lugar de usar identicadores (conar en un tamao y distribucin particular para una estructura no es portable). El tamao de una struct es la combinacin de los tamaos de todos sus miembros. A veces cuando el compilador crea una struct, aade bytes extra para hacer que los lmites encajen limpiamente - eso puede incrementar la eciencia de la ejecucin. En el Captulo 14, ver cmo en algunos casos se aaden punteros secretos a la estructura, pero no tiene que preocuparse de eso ahora. Puede determinar el tamao de una struct usando el operador sizeof. Aqu tiene un pequeo ejemplo:
//: C04:Sizeof.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Sizes of structs #include "CLib.h" #include "CppLib.h" #include <iostream> using namespace std; struct A { int i[100]; }; struct B { void f(); }; void B::f() {} int main() { cout << "sizeof struct A = " << sizeof(A) << " bytes" << endl; cout << "sizeof struct B = " << sizeof(B) << " bytes" << endl; cout << "sizeof CStash in C = " << sizeof(CStash) << " bytes" << endl; cout << "sizeof Stash in C++ = " << sizeof(Stash) << " bytes" << endl; }
En mi mquina (sus resultado pueden variar) el primer resultado produce 200 porque cada int ocupa 2 bytes. La struct B es algo anmalo porque es una struct sin atributos. En C, eso es ilegal, pero en C++ necesitamos la posibilidad de crear una struct cuya nica tarea es ofrecer un mbito a nombres de funciones, de modo que est permitido. An asi, el segundo resultado es un sorprendente valor distinto de cero. En versiones anteriores del lenguage, el tamao era cero, pero aparecia una situacin incmoda cuando se creaban estos objetos: tenan la misma direccin que el objeto creado antes que l, y eran indistinguibles. Una de las reglas fundamentales de los objetos es que cada objeto debe tener una direccin nica, as que las estructuras sin atributos siempre tendrn tamao mnimo distinto de cero. Las dos ltimas sentencias sizeof muestran que el tamao de la estructura en C++ es el mismo que en la versin en C. C++ intenta no aadir ninguna sobrecarga innecesaria.
4.7. Conveciones para los cheros de cabecera Cuando yo aprend a programar en C, el chero de cabecera era un misterio para mi. Muchos libros de C no hacen hincapi, y el compilador no obliga la declaracin de las funciones, as que pareca algo opcional la mayor parte de las veces, excepto cuando se declaraban estrucutras. En C++ el uso de los cheros de cabecera se vuelve clara como el cristal. SOn prctimente obligatorios para el desarrollo de programas sencillos, y ponga en ellos informacin muy especca: declaraciones. El chero de cabecera informa al compilador lo que hay disponible en la librera. Puede usar la librera incluso si slo se dispone del chero de cabecera y el chero objeto o el chero de librera; no necesita disponer del cdigo fuente del chero cpp. El chero de cabecera es donde se guarda la especicacin de la interfaz. Aunque el compilador no lo obliga, el mejor modo de construir grandes proyectos en C es usar libreras; colecciones de funciones asociadas en un mismo mdulo objeto o librera, y usar un chero de cabecera para colocar todas las declaraciones de las funciones. Es de rigor en C++, Podra meter cualquier funcin en una librera C, pero el tipo abstrato de dato C++ determina las funciones que estn asociadas por medio del acceso comn a los datos de una struct. Cualquier funcin miembro debe ser declarada en la declaracin de la struct; no puede ponerse en otro lugar. El uso de libreras de funciones fue fomentado en C y institucionalizado en C++.
Estos puntos suspensivos (N. de T. ellipsis) en ingls) especican una lista de argumentos variable 4 , que dice: printf() tiene algunos argumentos, cada uno con su tipo, pero no se sabe cuales. Simplemente, coge los argumentos que veas y aceptalos. Usando este tipo de declaracin, se suspende la comprobacin de errores en los argumentos. Esta prctica puede causar problemas sutiles. Si declara funciones a mano, en un chero puede cometer un error. Dado que el compilador slo ver las declaracin hechas a mano en ese chero, se adaptar al error. El programa enlazar correctamente, pero el uso de la funcin en ese chero ser defectuoso. Se trata de un error dicil de encontrar, y que se puede evitar fcilmente usando el chero de cabecera correspondiente. Si se colocan todas las declaraciones de funciones en un chero de cabecera, y se incluye ese chero all donde se use la funcin se asegurar una declaracin consistente a travs del sistema completo. Tambin se asegurar de que la declaracin y la denicin corresponden incluyendo el chero de cabecera en el chero de denicin. Si se declara una struct en un chero de cabecera en C++, se debe incluir ese chero all donde se use una struct y tambin donde se denan los mtodos la struct. El compilador de C++ devolver un mensaje de error si intenta llamar a una funcin, o llamar o denir un mtodo, sin declararla primero. Imponiendo el uso apropiado de los cheros de cabecera, el lenguaje asegura la cosistencia de las libreras, y reduce el nmero de error forzando que se use la misma interface en todas partes. El chero de cabecera es un contrato entre el programador de la librera y el que la usa. El contrato describe las estructuras de datos, expone los argumentos y valores de retorno para las funciones. Dice, Esto es lo que hace mi librera. El usuario necesita parte de esta informacin para desarrollar la aplicacin, y el compilador necesita toda ella para generar el cdigo correcto. El usuario de la struct simplemente incluye el chero de cabecera, crea objetos (instancias) de esa struct, y enlaza con el mdulo objeto o librera (es decir, el cdigo compilado) El compilador impone el contrato obligando a declarar todas las estruturas y funciones antes que de ser usadas y, en el caso de mtodos, antes de ser denidas. De ese modo, se le obliga a poner las declaraciones en el chero de cabecera e incluirlo en el chero en el que se denen los mtodos y en los cheros en los que se usen. Como se incluye un nico chero que describe la librera para todo el sistema, el compilador puede asegurar la consistencia y prevenir errores.
4 Para escribir una denicin de funcin que toma una lista de argumentos realmente variable, debe usar varargs, aunque se debera evitar en C++. Puede encontar informacin detallada sobre el uso de varargs en un manual de C.
157
Captulo 4. Abstraccin de Datos Hay ciertos asuntos a los que debe prestar atencin para organizar su cdigo apropiadamente y escribir cheros de cabecera ecaces. La regla bsica es nicamente declaraciones, es decir, slo informacin para el compiladore pero nada que requiera alojamiento en memoria ya sea generando cdigo o creando variables. Esto es as porque el chero de cabecera normalmente es incluido en varios unidades de traduccin en un proyecto, y si el almacenamiento para un identicador se pide en ms de un sitio, el enlazador indicar un error de denicin mltiple (sta es la regla de denicin nica de C++: Se puede declarar tantas veces como se quiera, pero slo puede haber una denicin real para cada cosa). Esta regla no es completamente [FIXME:hard and fast]. Si se dene una variable que es le static (que tiene visibilidad slo en un chero) dentro de un chero de cabecera, habr mltiples instancias de ese dato a lo largo del proyecto, pero no causar un colisin en el enlazador 5 . Bsicamente, se debe evitar cualquier cosa en los cheros de cabecera que pueda causar una ambigedad en tiempo de enlazado.
o puede darle un valor (que es la manera habitual en C para denir una constante):
#define PI 3.14159
158
4.7. Conveciones para los cheros de cabecera Esto producir un resultado verdadero, y el cdigo que sigue al #ifdef ser incluido en el paquete que se enva al compilador. Esta inclusin acaba cuando el preprocesador encuentra la sentencia:
#endif
o
#endif // FLAG
Cualquier cosa despus de #endif, en la misma lnea, que no sea un comentario es ilegal, incluso aunque algunos compiladores lo acepten. Los pares #ifdef/#endif se pueden anidar. El complementario de #define es #undef (abreviacin de un-dene que har que una sentencia #ifdef que use la misma variable produzca un resultado falso. #undef tambin causar que el preprocesador deje de usar una macro. El complementario de #ifdef es #ifndef, que producir verdadero si la etiqueta no ha sido denida (ste es el que usaremos en los cheros de cabecera). Hay otras caractersticas tiles en el preprocesador de C. Consulte la documentacin de su preprocesador para ver todas ellas.
Como puede ver, la primera vez que se incluye el chero de cabecera, los contenidos del chero (incluyendo la declaracin del tipo) son incluidos por el preprocesador. Las dems veces que sea includio -en una nica unidad de programacin- la declaracin del tipo ser ignorada. El nombre HEADER_FLAG puede ser cualquier nombre nico, pero un estndar able a seguir es poner el nombre del chero de cabecera en maysculas y reemplazar los puntos por guiones bajos (sin embago, el guin bajo al comienzo est reservado para nombres del sistema). Este es un ejemplo:
//: C04:Simple.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Simple header that prevents re-definition #ifndef SIMPLE_H #define SIMPLE_H struct Simple { int i,j,k; initialize() { i = j = k = 0; } }; #endif // SIMPLE_H
Aunque el SIMPLE_H despus de #endif est comentado y es ignorado por el preprocesador, es til para documentacin. 159
Captulo 4. Abstraccin de Datos Estas sentencias del preprocesador que impiden inclusiones mltiples se denominan a menudo guardias de inclusin (include guards)
Como std es el espacio de nombres que encierra la librera Estndar C++ al completo, est directiva using en particular permite que se puedan usar los nombres de la librera Estndar C++. Sin embargo, cas nunca ver una directiva using en un chero de cabecera (al menos, no fuera de un bloque). La razn es uqe la directiva using elmina la proteccin de ese espacio de nombres en particular, y el efecto dura hasta que termina la unidad de compilacin actual. Si pone una directiva using (fuera de un bloque) e un chero de cabecera, signica que esta perdida de proteccin del espacio de nombres ocurrir con cualquier chero que incluya este chero de cabecera, lo que a menudo signica otros cheros de cabecera, es muy fcil acabar desactivando los espacios de nombres en todos sitios, y por tanto, neutralizando los efectos beneciosos de los espacios de nombres. En resumen: no ponga directivas using en cheros de cabecera.
160
La struck anidada se llama Link, y contiene un puntero al siguiente Link en la lista y un puntero al dato almacenado en el Link. Si el siguiente puntero es cero, signica que es el ltimo elemento de la lista. Fjese que el puntero head est denido a la derecha despus de la declaracin de la struct Link, es lugar de una denicin separada Link* head. Se trata de una sintaxis que viene de C, pero que hace hincapi en la importancia del punto y coma despus de la declaracin de la estructura; el punto y coma indica el n de una lista de deniciones separadas por comas de este tipo de estructura (Normalmente la lista est vaca.) La estructura anidada tiene su propia funcin initialize(), como todas las estructuras hasta el momento, para asegurar una inicializacin adecuada. Stack tiene tanto funcin initialice() como cleanup(), adems de push(), que toma un puntero a los datos que se desean almacenar (asume que ha sido alojado en el montculo), y pop(), que devuelve el puntero data de la cima de la Stack y elimina el elemento de la cima. (El que hace pop() de un elemento se convierte en responsable de la destruccin del objeto apuntado por data.) La funcin peak() tambin devuelve un puntero data a la cima de la pila, pero deja el elemento en la Stack. Aqu se muestran las deniciones de los mtodos:
//: C04:Stack.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Linked list with nesting #include "Stack.h" #include "../require.h" using namespace std; void Stack::Link::initialize(void* dat, Link* nxt) { data = dat; next = nxt; } void Stack::initialize() { head = 0; } void Stack::push(void* dat) { Link* newLink = new Link; newLink->initialize(dat, head); head = newLink; } void* Stack::peek() { require(head != 0, "Stack empty"); return head->data; } void* Stack::pop() { if(head == 0) return 0; void* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } void Stack::cleanup() { require(head == 0, "Stack not empty");
161
La primera denicin es particularmente interesante porque muestra cmo se dene un miembro de una estructura anidada. Simplemente se usa un nivel adicional de resolucin de mbito para especicar el nombre de la struct interna. Stack::Link::initialize() toma dos argumentos y los asigna a sus atributos. Stack::initialize() asgina cero a head, de modo que el objeto sabe que tiene una lista vaca. Stack::push() toma el argumento, que es un puntero a la variable a la que se quiere seguir la pista, y la apila en la Stack. Primero, usa new para pedir alojamiento para el Link que se insertar en la cima. Entonces llama a la funcin initialize() para asignar los valores apropiados a los miembres del Link. Fijese que el siguiente puntero se asigna al head actual; entonces head se asigna al nuevo puntero Link. Esto apila ecazmente el Link en la cima de la lista. Stack::pop() captura el puntero data en la cima actual de la Stack; entonces mueve el puntero head hacia abajo y borrar la anterior cima de la Stack, nalmente devuelve el puntero capturado. Cuando pop() elemina el ltimo elemento, head vuelve a ser cero, indicando que la Stack est vaca. Stack::cleanup() realmente no hace ninguna limpieza. En su lugar, establece una poltica rme que dice el programador cliente que use este objeto Stack es responsable de des-apilar todos los elementos y borrarlos. require() se usa para indicar que ha ocurrido un error de programacin si la Stack no est vaca. Por qu no puede el destructor de Stack responsabilizarse de todos los objetos que el programador ciente no des-apil? El problema es que la Stack est usando punteros void, y tal como se ver en el Captulo 13 usar delete para un void* no libera correctamente. El asunto de quin es el responsable de la memoria no siempre es sencillo, tal como veremos en prximos captulos. Un ejemplo para probar la Stack:
//: C04:StackTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Stack //{T} StackTest.cpp // Test of nested linked list #include "Stack.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; textlines.initialize(); string line; // Read file and store lines in the Stack: while(getline(in, line)) textlines.push(new string(line)); // Pop the lines from the Stack and print them: string* s; while((s = (string*)textlines.pop()) != 0) { cout << *s << endl; delete s; } textlines.cleanup(); }
162
4.9. Resumen Este es similar al ejemplo anterior, pero en este se apilan lneas de un chero (como punteros a cadena) en la Stack y despus los des-apila, lo que implica que el chero sea imprimido en orden inverso. Fjese que el pop() devuelve un void* que debe ser moldeado a string* antes de poderse usar. Para imprimir una cadena, el puntero es dereferenciado. Como textlines se llena, los contenidos de line se clona para cada push() creando un new string(line). El valor devuelto por la expresin new es un puntero al nuevo string que fue creado y al que se ha copiado la informacin de la line. Si se hubiera pasado directamente la direccin de line a push(), la Stack se llenara con direcciones idnticas, todas apuntando a line. Ms adelante en ese libro aprender ms sobre este proceso de clonacin. El nombre del chero se toma de lnea de comando. Para garantizar que hay sucientes argumentos en la lnea de comando, se usa una segunda funcin del chero de cabecera require.h: requireArgs() que compara argc con el nmero de argumentos deseado e imprime un mensaje de error y termina el programa si no hay sucientes argumentos.
Sin resolucin de mbito en S::f(), el compilador elegira por defecto las versiones miembro para f() y a.
4.9. Resumen
En este captulo, ha aprendido el [FIXME:twist] fundamental de C++: que puede poner funciones dentro de las estructuras. Este nuevo tipo de estructura se llama tipo abstracto de dato, y las variables que se crean usando esta estructura se llaman objetos, o instancias, de ese tipo. Invocar un mtodo de una objeto se denomina enviar un mensaje al objeto. La actividad principal en la progrmacin orientada a objetos es el envo de mensajes a objetos. Aunque empaquetar datos y funciones juntos es un benicio signicativo para la organizacin del cdigo y hace la librera sea ms fcil de usar porque previene conictos de nombres ocultando los nombres, hay mucho ms que se puede hacer para tener programacin ms segura en C++. En el prximo captulo, aprender cmo proteger algunos miembres de una struct para que slo el programador pueda manipularlos. Esto establece un 163
Captulo 4. Abstraccin de Datos lmite claro entre lo que es usuario de la estructura puede cambiar y lo que slo el programador puede cambiar.
4.10. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. En la librera C estndar, la funcin puts() imprime un array de caracteres a la consola(de modo que puede decir puts("Hola")). Escriba un program C que use puts() pero que no incluya <stdio.h> o de lo contrario declare la funcin. Compile ese programa con su compilador de C. (algunos compiladores de C++ no son programas distintos de sus compiladores de C, es ese caso puede que necesite averiguar que opcin de lnea de comando fuerza una compilacin C.) Ahora compilelo con el compilador C++ y preste atencin a la diferencia. 2. Cree una declaracin de struct con un nico mtodo, entonces cree una denicin para ese mtodo. Cree un objeto de su nuevo tipo de dato, e invoque el mtodo. 3. Cambie su solucin al Ejercicio 2 para que la struct sea declarada en un chero de cabecera conveniente mente guardado, con la denicin en un chero cpp y el main() en otro. 4. Cree una struct con un nico atributo de tipo entero, y dos funciones globales, cada una de las cuales acepta un puntero a ese struct. La primera funcin tiene un segundo argumento de tipo entero y asigna al entero de la struct el valor del argumento, la segunda muestra el entero de la struct. Prueba las funciones. 5. Repita el Ejercicio 4 pero mueva las funcin de modo que sean mtodos de la struct, y pruebe de nuevo. 6. Cree una clase que (de forma redundante) efectue la selecin de atributos y una llamada a mtodo usando la palabra reservada this (que indica a la direccin del objeto actual) 7. Cree una Stach que mantenga doubles. Rellnela con 25 valores double, despus mustrelos en consola. 8. Repita el Ejercicio 7 con Stack. 9. Cree un chero que contenga una funcin f() que acepte un argumento entero y lo imprima en consola usando la funcin printf() de <stdio> escribiendo: printf("%d\n", i) donde i es el entero que desea imprimir. Cree un chero separado que contenga main(), y este chero declare f() pero aceptando un argumento oat. Invoque f() desde main(). Intente compilar y enlazar el programa con el compilador C++ y vea qu ocurre. Ahora compile y enlace el programa usando el compilador C, y vea que ocurre cuando se ejecuta. Explique el comportamiento. 10. Averige cmo generar lenguaje ensamblador con su compilador C y C++. Escriba una funcin en C y una struct con un nico miembro en C++. Genere la salida en lenguaje ensamblador para cada una de ellas y encuentre los nombres de ambas funciones, de modo que pueda ver que tipo de decoracin aplica el compilador a dichos nombres. 11. Escriba un programa con cdigo condicionalmente-compilado en main(), para que cuando se dena un valor del preprocesador, se muestre un mensaje, pero cuando no se dena, se imprima otra mensaje distinto. Compile este experimentando con un #define en el programa, despus averige la forma de indicar al compilador deniciones de preprocesador en la lnea de comandos y experimente con ello. 12. Escriba un programa que use assert() con un argumento que siempre sea falso (cero) y vea que ocurre cuando lo ejecuta. Ahora compilelo con #define NDEBUG y ejecutelo de nuevo para ver la diferencia. 13. Cree un tipo abstracto de dato que represente un cinta de video en una tienda de alquiler. Considere todos los datos y operaciones que seran necesarias pra que el tipo Video funcione con el sistema de gestin de la tienda. Incluya un mtodo print() que muestre informacin sobre el Video 14. Cree un objeto Pila que almacene objetos Video del Ejercicio 13. Cree varios objetos Video, gurdelos en la Stack y entonces muestrelos usando Video::print(). 15. Escriba un programa que muestre todos los tamaos de los tipos de datos fundamentales de su computadora usando sizeof. 164
4.10. Ejercicios 16. Modique Stash para usar vector<char> como estructura de datos subyacente. 17. Cree dinmicamente espacio de almacenamiento para los siguiente tipos usando new: int, long, un array de 100 char, un array de 100 oat. Muestre sus direcciones y librelos usando delete. 18. Escriba una funcin que tome un argumento char*. Usando new, pida alojamiento dinmico para un array de char con un tamao igual al argumento pasado a la funcin. Usando indexacin de array, copie los caracteres del argumento al array dinmico (no olvide el terminador nulo) y devuelva el puntero a la copia. En su main(), pruebe la funcin pasando una cadena esttica entre comillas, despus tome el resultado y paselo de nuevo a la funcin. Muestre ambas cadenas y punteros para poder ver que tienen distinta ubicacin. Mediante delete libere todo el almacenamiento dinmico. 19. Haga un ejemplo de estructura declarada con otra estructura dentro (un estructura anidada). Declare atributos en ambas structs, y declare y dena mtodos en ambas structs. Escriba un main() que pruebe los nuevos tipos. 20. Cmo de grande es una estructura? Escriba un trozo de cdigo que muestre el tamao de varias estructuras. Cree estructuras que tengan slo atributos y otras que tengan atributos y mtodos. Despus cree una estructura que no tenga ningn miembro. Muestre los tamaos de todas ellas. Explique el motivo del tamao de la estructura que no tiene ningn miembro. 21. C++ crea automticamente el equivalente de typedef para structs, tal como ha visto en este captulo. Tambin lo hace para las enumeraciones y las uniones. Escriba un pequeo programa que lo demuestre. 22. Cree una Stack que maneje Stashes. Cada Stash mantendr cinco lneas procedentes de un chero. Cree las Stash usando new. Lea un chero en su Stack, depus mustrelo en su forma original extrayendolo de la Stack. 23. Modique el Ejercicio 22 de modo que cree una estructura que encapsule la Stack y las Stash. El usuario slo debera aadir y pedir lneas a travs de sus mtodos, pero debajo de la cubierta la estructura usa una Stack(pila) de Stashes. 24. Cree una struct que mantenga un int y un puntero a otra instancia de la misma struct. Escriba una funcin que acepte como parmetro la direccin de una de estas struct y un int indicando la longitud de la lista que se desea crear. Esta funcin crear una cadena completa de estas struct (una lista enlazada), empezando por el argumento (la cabeza de la lista), con cada una apuntando a la siguiente. Cree las nuevas struct usando new, y ponga la posicin (que nmero de objeto es) en el int. En la ltima struct de la lista, ponga un valor cero en el puntero para indicar que es el ltimo. Escriba un segunda funcin que acepte la cabeza de la lista y la recorra hasta el nal, mostrando los valores del puntero y del int para cada una. 25. Repita el ejercicio 24, pero poniendo las funciones dentro de una struct en lugar de usar struct y funciones crudas.
165
5: Ocultar la implementacin
Una librera C tpica contiene una estructura y una serie de funciones que actan sobre esa estructura. Hasta ahora hemos visto cmo C++ toma funciones conceptualmente asociadas y las asocia literalmente poniendo la declaracin de la funcin dentro del
dominio de la estructura, cambiando la forma en que se invoca a las funciones desde las estructuras, eliminando el paso de la direccin de la estructura como primer parmetro, y aadiendo un nuevo tipo al programa (de ese modo no es necesario crear un typedef para la estructura). Todo esto son mejoras - Le ayuda a organizar su cdigo hacindolo ms fcil de escribir y leer. Sin embargo, hay otros aspectos importantes a la hora de hacer las libreras ms sencillas en C++, especialmente los aspectos de seguridad y control. Este captulo se centra en el tema de la frontera de las estructuras.
167
struct A { int i; char j; float f; void func(); }; void A::func() {} struct B { public: int i; char j; float f; void func(); }; void B::func() {} int main() { A a; B b; a.i = b.i = 1; a.j = b.j = c; a.f = b.f = 3.14159; a.func(); b.func(); }
La palabra clave private, por otro lado, signica que nadie podr acceder a ese miembro excepto usted, el creador del tipo, dentro de funciones miembro de ese tipo. private es una pared entre usted y el programador cliente; si alguien intenta acceder a un miembro private, obtendr un error en tiempo de compilacin. En struct B en el ejemplo anterior, podra querer hacer partes de la representacin (esto es, los datos miembros) ocultos, accesibles solo a usted:
//: C05:Private.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Setting the boundary struct B { private: char j; float f; public: int i; void func(); }; void B::func() { i = 0; j = 0; f = 0.0; }; int main() {
168
5.3. Friends
B b; b.i = 1; // OK, public //! b.j = 1; // Illegal, private //! b.f = 1.0; // Illegal, private }
Aunque func() puede acceder a cualquier miembro de B (pues func() en un miembro de B, garantizando as automticamente el acceso), una funcin global para como main() no puede. Por supuesto tampoco miembros de otras estructuras. Solo las funciones que estn claramente declaradas en la declaracin de la estructura (el "contrato") tendrn acceso a miembros private. No hay un orden jo para los especicadores de acceso, y pueden aparecer ms de una vez. Afectan a todos los miembros declarados despus de ellos hasta el siguiente especicador.
5.2.1. protected
Es el ultimo que nos queda por ver, protected acta como private, con una excepcin de la que hablaremos ms tarde: estructuras heredadas (que no pueden acceder a lo miembros privados) si tienen acceso a los miembros protected. Todo esto se vera ms claramente en el captulo 14 cuando la herencia sea introducida. Para las consideraciones actuales considere protected como private.
5.3. Friends
Que pasa si explcitamente se quiere dar acceso a una funcin que no es miembro de la estructura? Esto se consigue declarando la funcin como friend dentro de la declaracin de la estructura. Es importante que la declaracin de una funcin friend se haga dentro de la declaracin de la estructura pues usted (y el compilador) necesita ver la declaracin de la estructura y todas las reglas sobre el tamao y comportamiento de ese tipo de dato. Y una regla muy importante en toda relacin es, "Quin puede acceder a mi parte privada?" La clase controla que cdigo tiene acceso a sus miembros. No hay ninguna manera mgica de "colarse" desde el exterior si no eres friend; no puedes declarar una nueva clase y decir, "Hola, soy friend de Bob" y esperar ver los miembros private y protected de Bob. Puede declarar una funcin global como friend, tambin puede declarar una funcin miembro de otra estructura, o incluso una estructura completa, como friend. Aqu hay un ejemplo:
//: C05:Friend.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Friend allows special access // Declaration (incomplete type specification): struct X; struct Y { void f(X*); }; struct X { // Definition private: int i; public: void initialize(); friend void g(X*, int); // Global friend friend void Y::f(X*); // Struct member friend friend struct Z; // Entire struct is a friend friend void h(); };
169
void X::initialize() { i = 0; } void g(X* x, int i) { x->i = i; } void Y::f(X* x) { x->i = 47; } struct Z { private: int j; public: void initialize(); void g(X* x); }; void Z::initialize() { j = 99; } void Z::g(X* x) { x->i += j; } void h() { X x; x.i = 100; // Direct data manipulation } int main() { X x; Z z; z.g(&x); }
struct Y tiene una funcin miembro f() que modica un objeto de tipo X. Aqu hay un poco de lo pues en C++ el compilador necesita que usted declare todo antes de poder hacer referencia a ello, asi struct Y debe estar declarado antes de que su miembro Y::f(X*) pueda ser declarado como friend en struct X. Pero para declarar Y::f(X*) , struct X debe estar declarada antes! Aqu vemos la solucin. Dese cuenta de que Y::f(X*) coge como argumento la direccin de un objeto de tipo X. Esto es fundamental pues el compilador siempre sabe como pasar una direccin, que es de un tamao jo no importa el tipo de objeto, aunque no tenga informacion del tamao real de ese objeto. Si intenta pasar el objeto completo, el compilador necesita ver la denicion completa de X, para saber el tamao de lo que pasar y como pasarlo, antes de que le permita declarar una funcin como Y::g(X). Pasando la direccin de un X, el compilador le permite hacer una identicacin de tipo incompleta de X antes de declarar Y::f(X*). Esto se consigue con la declaracin:
struct X;
Esta declaracin simplemente le dice al compilador que hay una estructura con ese nombre, as que se acepte referencias a ella siempre que no se necesite nada ms que el nombre. Ahora, en struct X, la funcin Y::f(X*) puede ser declarada como friend sin problemas. Si intentase declararla antes de que el compilador hubiese visto la especicacin completa de Y, habra dado un error. Esto es una restriccin para asegurar consistencia y eliminar errores. 170
5.3. Friends Fjese en las otras dos funciones friend. La primera declara una funcin global ordinaria g() como friend. Pero g() no ha sido antes declarada como global!. Se puede usar friend de esta forma para declarar la funcin y darle el estado de friend simultneamente. Esto se extiende a estructuras completas
friend struct Z;
171
Una vez que Pointer est declarado, se le da acceso a los miembros privados de Holder con la sentencia:
friend Pointer;
La estructuraHolder contiene un array de enteros y Pointer le permite acceder a ellos. Como Pointer esta fuertemente asociado con Holder, es comprensible que sea una estructura miembro de Holder. Pero como Pointer es una clase separada de Holder, puede crear ms de una instancia en el main() y usarlas para seleccionar diferentes partes del array. Pointer es una estructura en ved de un puntero de C, as que puede garantizar que siempre apuntara dentro de Holder. La funcin de la librera estndar de C memset() (en <cstring>) se usa en el programa por conveniencia. Hace que toda la memoria a partir de una determinada direccin (el primer argumento) se cargue con un valor particular (el segundo argumento) para n bytes pasada la direccin donde se empez (n es el tercer argumento). Por supuesto, se podra haber usado un bucle para hacer lo mismo, pero memset()esta disponible, bien probada (as que es ms factible que produzca menos errores), y probablemente es ms eciente. 172
5.5. La clase
El control de acceso se suele llamar tambin ocultacin de la implementacin. Incluir funciones dentro de las estructuras (a menudo llamado encapsulacin 1 ) produce tipos de dato con caractersticas y comportamiento, pero el control de acceso pone fronteras en esos tipos, por dos razones importantes. La primera es para establecer lo que el programador cliente puede y no puede hacer. Puede construir los mecanismos internos de la estructura sin preocuparse de que el programador cliente pueda pensar que son parte de la interfaz que debe usar Esto nos lleva directamente a la segunda razn, que es separar la interfaz de la implementacin. Si la estructura es usada en una serie de programas, y el programador cliente no puede hacer ms que mandar mensajes a la interfaz publica, usted puede cambiar cualquier cosa privada sin que se deba modicar cdigo cliente. La encapsulacin y el control de acceso, juntos, crean algo ms que una estructura de C. Estamos ahora en el mundo de la programacin orientada a objetos, donde una estructura describe una clase de objetos como describira una clase de peces o pjaros: Cualquier objeto que pertenezca a esa clase compartir esas caractersticas y comportamiento. En esto se ha convertido la declaracin de una estructura, en una descripcin de la forma en la que los objetos de este tipo sern y actuarn. En el lenguaje OOP original, Simula-67, la palabra clave class fue usada para describir un nuevo tipo de dato. Aparentemente esto inspiro a Stroustrup a elegir esa misma palabra en C++, para enfatizar que este era el punto clave de todo el lenguaje: la creacin de nuevos tipos de dato que son ms que solo estructuras de C con funciones. Esto parece suciente justicacin para una nueva palabra clave. De todas formas, el uso de class en C++ es casi innecesario. Es idntico a struct en todos los aspectos
1
173
Captulo 5. Ocultar la implementacin excepto en uno: class pone por defecto private, mientras que struct lo hace a public. Estas son dos formas de decir lo mismo:
//: C05:Class.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Similarity of struct and class struct A { private: int i, j, k; public: int f(); void g(); }; int A::f() { return i + j + k; } void A::g() { i = j = k = 0; } // Identical results are produced with: class B { int i, j, k; public: int f(); void g(); }; int B::f() { return i + j + k; } void B::g() { i = j = k = 0; } int main() { A a; B b; a.f(); a.g(); b.f(); b.g(); }
La clase (class) en un concepto OOP fundamental en C++. Es una de la palabras clave que no se pondrn en negrita en este libro - es incomodo pues se repite mucho. El cambio a clases es tan importante que sospecho que Stroustrup hubiese preferido eliminar completamente struct, pero la necesidad de compatibilidad con C no lo hubiese permitido. Mucha gente preere crear clases a la manera struct en vez de a la mnera class, pues sustituye el "pordefecto-private" de class empezando con los elementos public
class X { public: void miembro_de_interfaz(); private:
174
5.5. La clase
void miembro_privado(); int representacion_interna; };
El porqu de esto es que tiene ms sentido ver primero lo que ms interesa, el programador cliente puede ignorar todo lo que dice private. De hecho, la nica razn de que todos los miembros deban ser declarados en la clase es que el compilador sepa como de grande son los objetos y pueda colocarlos correctamente, garantizando as la consistencia. De todas formas, los ejemplos en este libro pondrn los miembros privados primero, as:
class X { void private_function(); int internal_representation; public: void interface_function(); };
Como mX esta ya oculto para Y, la m (de "miembro") es innecesaria. De todas formas, en proyectos con muchas variables globales (algo que debe evitar a toda costa, aunque a veces inevitable en proyectos existentes), es de ayuda poder distinguir dentro de una denicin de funcin miembro que datos son globales y cuales miembro.
175
La funcin inflate() ha sido hecha private porque solo es usada por la funcin add() y es por tanto parte de la implementacin interna, no de la interfaz. Esto signica que, ms tarde, puede cambiar la implementacin interna para usar un sistema de gestin de memoria diferente. Aparte del nombre del archivo include, la cabecera de antes es lo nico que ha sido cambiado para este ejemplo. El chero de implementacin y de prueba son los mismos.
Como antes, la implementacin no cambia por lo que no la repetimos aqu. El programa de prueba es tambin idntico. La nica cosa que se ha cambiado es la robustez del interfaz de la clase. El valor real del control de acceso es prevenirle de traspasar las fronteras durante el desarrollo. De hecho, el compilador es el nico que conoce los niveles de proteccin de los miembros de la clase. No hay informacin sobre el control de acceso aadida en el nombre del miembro que llega al enlazador. Todas las comprobaciones sobre proteccin son hechas por el compilador; han desaparecido al llegar a la ejecucin. Dese cuenta de que la interfaz presentada al programador cliente es ahora realmente el de una pila hacia abajo. Sucede que esta implementada como una lista enlazada, pero usted puede cambiar esto sin afectar a como los programas cliente interactan con ella, o (ms importante aun) sin afectar a una sola linea de su cdigo.
5.6. Manejo de clases cuenta lo antes posible de si hay un error. Tambin signica que su programa sera ms eciente. De todas formas, la inclusin de la implementacin privada tiene dos efectos: la implementacin es visible aunque no se pueda fcilmente acceder a ella, y puede causar innecesarias recompilaciones.
2 Este nombre se le atribuye a John Carolan, uno de los pioneros del C++, y por supuesto, Lewis Carroll. Esta tcnica se puede ver tambin como una forma del tipo de diseo "puente", descrito en el segundo volumen.
177
es una especicacin incompleta de tipo o una declaracin de clase (una denicin de clase debe incluir el cuerpo de la clase). Le dice al compilador que Chesire es el nombre de una estructura, pero no detalles sobre ella. Esta es informacin suciente para crear un puntero a la estructura; no puede crear un objeto hasta que el cuerpo de la estructura quede denido. En esta tcnica, el cuerpo de la estructura esta escondido en el chero de implementacin:
//: C05:Handle.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Handle implementation #include "Handle.h" #include "../require.h" // Define Handles implementation: struct Handle::Cheshire { int i; }; void Handle::initialize() { smile = new Cheshire; smile->i = 0; } void Handle::cleanup() { delete smile; } int Handle::read() { return smile->i; } void Handle::change(int x) { smile->i = x; }
Chesire es una estructura anidada, as que se debe denir con resolucin de dominio:
struct Handle::Cheshire {
En Handle::initialize(), se coge espacio de almacenamiento para una estructura Chesire, y en Handle::cleanup() este espacio se libera. Este espacio se usa para almacenar todos los datos que estaran normalmente en la seccin privada de la clase. Cuando compile Handle.cpp, esta denicin de la estructura estar escondida en el chero objeto donde nadie puede verla. Si cambia los elementos de Chesire, el nico archivo que debe ser recompilado es Handle.cpp pues el archivo de cabecera permanece inalterado. El uso deHandle es como el usa de cualquier clase: incluir la cabecera, crear objetos, y mandar mensajes.
//: C05:UseHandle.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Handle // Use the Handle class #include "Handle.h"
178
5.7. Resumen
La nica cosa a la que el programador cliente puede acceder es a la interfaz publica, as que mientras la implementacin sea lo nico que cambie, el chero anterior no necesitara recompilarse. As, aunque esto no es ocultacin de implementacin perfecta, es una gran mejora.
5.7. Resumen
El control de acceso en C++ da un gran control al creador de la clase. Los usuarios de la clase pueden ver claramente lo que pueden usar y que ignorar. Ms importante, aun, es la posibilidad de asegurar que ningn programador cliente depende de ninguna parte de la implementacin interna de la clase. Si sabe esto como creador de la clase, puede cambiar la implementacin subyacente con la seguridad de que ningn programador cliente se vera afectado por los cambios, pues no pueden acceder a esa parte de la clase. Cuando tiene la posibilidad de cambiar la implementacin subyacente, no solo puede mejorar su diseo ms tarde, si no que tambin tiene la libertad de cometer errores. No importa con que cuidado planee su diseo, cometer errores. Sabiendo que es relativamente seguro cometer esos errores, experimentara ms, aprender ms rpido, y acabara su proyecto antes. La interfaz publica de una clase es lo que realmente ve el programador cliente, as que es la parte de la clase ms importante de hacer bien durante el anlisis y diseo. Pero incluso esto le deja algo de libertad para el cambio. Si no consigue la interfaz buena a la primera, puede aadir ms funciones, mientras no quite ninguna que el programador cliente ya haya usado en su cdigo.
5.8. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree una clase con datos miembro y funciones miembro public, private y protected. Cree un objeto de esta clase y vea que mensajes de compilacin obtiene cuando intenta acceder a los diferentes miembros de la clase. 2. Escriba una estructura llamada Lib que contenga tres objetos string a, b y c. En main() cree un objeto Lib llamado x y asgnelo a x.a, x.b y x.c. Saque por pantalla sus valores. Ahora reemplace a, b y c con un array de cadenas s[3]. Dese cuenta de que su funcin main() deja de funcionar como resultado del cambio. Ahora cree una clase, llmela Libc con tres cadenas como datos miembro privados a, b y c, y funciones miembro seta(), geta(), setb(), getb(), setc() y getc() para establecer y recuperar los distintos valores. Escriba una funcin main() como antes. Ahora cambie las cadenas privadas a, b y c a un array de cadenas privado s[3]. Vea que ahora main() sigue funcionando. 3. Cree una clase y una funcin friend global que manipule los datos privados de la clase. 4. Escriba dos clases, cada una de ellas con una funcin miembro que reciba como argumento un puntero a un objeto de la otra clase. Cree instancias de ambos objetos en main() y llame a los funciones miembro antes mencionadas de cada clase. 5. Cree tres clases. La primera contiene miembros privados, y declara como friend a toda la segunda estructura y a una funcin miembro de la tercera. En main() demuestre que todo esto funciona correctamente. 179
Captulo 5. Ocultar la implementacin 6. Cree una clase Hen. Dentro de esta, inserte una clase Nest. Y dentro de esta una clase Egg. Cada clase debe tener una funcin miembro display(). En main(), cree una instancia de cada clase y llame a la funcin display() de cada una. 7. Modique el ejercicio 6 para que Nest y Egg contengan datos privados. De accesos mediante friend para que las clases puedan acceder a los contenidos privados de las clases que contienen. 8. Cree una clase con datos miembro diseminados por numerosas secciones public, private y protected. Aada la funcin miembro ShowMap() que saque por pantalla los nombres de cada uno de esas variables y su direccin en memoria. Si es posible, compile y ejecute este programa con ms de un compilador y/o ordenador y/o sistema operativo para ver si existen diferencias en las posiciones en memoria. 9. Copie la implementacin y cheros de prueba de Stash del captulo 4 para as poder compilar y probar el Stash.h de este captulo 10. Ponga objetos de la clase Hern denidos en el ejercicio 6 en un Stash. Apunte a ellos e imprmalos (si no lo ha hecho aun necesitara una funcin Hen::print() ) 11. Copie los cheros de implementacin y prueba de Stack del captulo 4 y compile y pruebe el Stack2.h de este captulo. 12. Ponga objetos de la clase Hen del ejercicio 6 dentro de Stack. Apunte a ellos e imprmalos (si no lo ha hecho an, necesitara aadir un Hen::print()) 13. Modique Chesire en Handle.cpp, y verique que su entorno de desarrollo recompila y reenlaza slo este chero, pero no recompila UseHandle.cpp 14. Cree una clase StackOfInt (una pila que guarda enteros) usando la tcnica "Gato de Chesire" que esconda la estructura de datos de bajo nivel que usa para guardar los elementos, en una clase llamada StackImp. Implemente dos versiones de StackImp: una que use un array de longitud ja de enteros, y otra que use un vector<int>. Tenga un tamao mximo para la pila preestablecido as no se tendr que preocupar de expandir el array en la primera versin. Fjese que la clase StackOfInt.h no tiene que cambiar con StackImp.
180
6: Inicializacin y limpieza
El capitulo 4 constituye una mejora signicativa en el uso de libreras cogiendo los diversos componentes de una librera C tpica y encapsulandolos en una estructura (un tipo abstracto de dato, llamado clase a partir de ahora).
Esto no slo permite disponer de un slo punto de entrada en un componente de librera, adems tambin oculta los nombres de las funciones con el nombre de la clase. Esto le da al diseador de la clase la posibilidad de establecer lmites claros que determinan que cosas puede hacer el programador cliente y qu queda fuera de los lmites. Eso signica que los mecanismos internos de las operaciones sobre los tipos de datos estn bajo el control y la discreccin del disedor de la clase, y deja claro a qu miembros puede y debe prestar atencin el programador cliente. Juntos, la encapsulacin y el control de acceso representan un paso signicativo para aumentar la sencillez de uso de las libreras. El concepto de "nuevo tipo de dato" que ofrecen es mejor en algunos sentidos que los tipos de datos que incorpora C. El compilador C++ ahora puede ofrecer garantas de comprobacin de tipos para esos tipos de datos y as asegura un nivel de seguridad cuando se usan esos tipos de datos. A parte de la seguridad, el compilador puede hacer mucho ms por nosotros de los que ofrece C. En este y en prximos captulos ver posibilidades adicionales que se han incluido en C++ y que hacen que los errores en sus programas casi salten del programa y le agarren, a veces antes incluso de compilar el programa, pero normalemente en forma de advertencias y errores en el proceso de compilacin. Por este motivo, pronto se acostubrar a la extraa situacin en que un programa C++ que compila, funciona a la primera. Dos de esas cuestiones de seguridad son la inicializacin y la limpiza. Gran parte de los errores de C se deben a que el programador olvida inicializar o liberar una variable. Esto sucede especialmente con las libreras C, cuando el programador cliente no sabe como inicializar una estructura, o incluso si debe hacerlo. (A menudo las libreras no incluyen una funcin de inicializacin, de modo que el programador cliente se ve forzado a inicializar la estructura a mano). La limpieza es un problema especial porque los programadores C se olvidan de las varibles una vez que han terminado, de modo que omiten cualquier limpieza que pudiera ser necesara en alguna estructura de la librera. En C++. el concepto de inicializacin y limpieza es esencial para facilitar el uno de las libreras y eliminar muchos de los errores sutiles que ocurren cuando el programador cliente olvida cumplir con sus actividades. Este captulo examina las posibilidades de C++ que ayudan a garantizar una inicializacin y limpieza apropiadas.
Captulo 6. Inicializacin y limpieza responsable de la invocacin del constructor, siempre debe saber qu funcin llamar. La solucin eligida por Stroustrup parece ser la ms sencilla y lgica: el nombre del constructor es el mismo que el de la clase. Eso hace que tenga sentido que esa funcin sea invocada automticamente en la inicializacin. Aqu se muesta un clase sencilla con un contructor:
class X { int i; public: X(); // Constructor };
Lo mismo pasa si fuese un entero: se pide alojamiento para el objeto. Pero cuando el programa llega al punto de ejecucin en el que est denida, se invoca el contructor automticamente. Es decir, el compilador inserta la llamada a X::X() para el objeto en el punto de la denicin. Como cualquier mtodo, el primer argumento (secreto) para el constructor es el puntero this - la direccin del objeto al que corresponde ese mtodo. En el caso del constructor, sin embargo, this apunta a un bloque de memoria no inicializado, y el trabajo del constructor es inicializar esa memoria de forma adecuada. Como cualquier funcin, el contructor puede argumentos que permitan especicar cmo ha de crearse el objeto, dados unos valores de inicializacin. Los argumentos del constructor son una especie de garantia de que todas las partes del objeto se inicializan con valores apropiados. Por ejemplo, si una clase Tree1 tiene un contructor que toma como argumento un nico entero que indica la altura del rbol, entonces debe crear un objeto rbol como ste:
Tree t(12) // rbol de 12 metros
Si Tree(int) es el nico contructor, el compildor no le permitir crear un objeto de otro modo. (En el prximo captulo veremos cmo crear mltiples constructores y diferentes maneras para invocarlos.) Y realmente un contructor no es ms que eso; es una funcin con un nombre especial que se invoca automticamente por el compilador para cada objeto en el momento de su creacin. A pesar de su simplicidad, tiene un valor excepcional porque evita una gran cantidad de problemas y hace que el cdigo sea ms fcil de escribir y leer. En el fragmento de cdigo anterior, por ejemplo, no hay una llamada explcita a ninguna funcin initilize() que, conceptualmente es una funcin separada de la denicin. En C++, la denicin e inicializacin con conceptor unicados - no se puede tener el uno si el otro. Constructor y destructor son tipos de funciones muy inusuales: no tienen valor de retorno. Esto es distinto de tener valor de retorno void, que indicara que la funcin no retorna nada pero teniendo la posibilidad de hacer que otra cosa. Constructores y destructores no retornan nada y no hay otra posibilidad. El acto de traer un objeto al programa, o sacarlo de l es algo especial, como el nacimiento o la muerte, y el compilador siempre hace que la funcin se llame a si misma, para asegurarse de que ocurre realmente. Si hubiera un valor de retorno, y si no elegiera uno, el compilador no tendra forma de saber qu hacer con el valor retornado, o el programador cliente tendra que disponer de una invocacin explcita del contructor o destructor, que eliminaria la seguridad.
rbol
182
6.2. Limpieza garantizada por el destructor hardware, o escribe algo en pantalla, o tiene asociado espacio en el montculo(heap). Si simplemente pasa de l, su objeto nunca lograr salir de este mundo. En C++, la limpieza es tan importante como la inicializacin y por eso est garantizada por el destructor. La sintaxis del destructor es similiar a la del constructor: se usa el nombre de la clase como nombre para la funcin. Sin embargo, el destructor se distingue del constructor porque va precedido de una tilde (~). Adems, el destructor nunca tiene argumentos porque la destruccin nunca necestia ninguna opcin. La declaracin de un destructor:
class Y { public: ~Y(); };
El destructor se invoca automticamente por el compilador cuando el objeto sale del mbito. Puede ver donde se invoca al contructor por el punto de la denicin del objeto, pero la nica evidencia de que el destructor fue invocado es la llave de cierre del mbito al que pertenece el objeto. El constructor se invoca incluso aunque utilice goto para saltar fuera del del mbito (goto sigue existiendo en C++ por compatibilidad con C.) Debera notar que un goto no-local, implementado con las funciones setjmp y longjmp() de la librera estndar de C, evitan que el destructor sea invocado. (sta es la especicacin, incluso si su compilador no lo implementa de esa manera. Conar un una caracterstica que no est en la especicacin signica que su cdigo no ser portable. A continuacin un ejemplo que demuestra las caractersticas de constructores y destructores que se han mostrado has el momento.
//: C06:Constructor1.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constructors & destructors #include <iostream> using namespace std; class Tree { int height; public: Tree(int initialHeight); ~Tree(); // Destructor void grow(int years); void printsize(); };
// Constructor
Tree::Tree(int initialHeight) { height = initialHeight; } Tree::~Tree() { cout << "inside Tree destructor" << endl; printsize(); } void Tree::grow(int years) { height += years; } void Tree::printsize() { cout << "Tree height is " << height << endl; } int main() { cout << "before opening brace" << endl; {
183
Puede ver que el destructor se llama automticamente al acabar el mbito (llave de cierre) en el que est denido el objeto.
C99, la versin actual del Estndar de C, permite denir variables en cualquier punto del bloque, como C++
184
class G { int i; public: G(int ii); }; G::G(int ii) { i = ii; } int main() { cout << "initialization value? "; int retval = 0; cin >> retval; require(retval != 0); int y = retval + 3; G g(y); }
Puede ver que se ejecuta parte del cdigo, entonces se dene >retval, que se usa para capturar datos de la consola, y entonces se denen y y g. C, al contrario, no permite denir una variable en ningn sitio que no sea el comienzo de un bloque. En general, debera denir las variables tan cerca como sea posible del punto en que se usa, e inicializarlas siempre cuando se denen. (sta es una sugerencia de estilo para tipos bsicos, en los que la inicializacin es opcional.) Es una cuestin de seguridad. Reduciendo la duracin de disponibilidad al nloque, se reduce la posibilidad de que sea usada inapropiadamente en otra parte del bloque. En resumen, la legibilidad mejora porque el lector no teiene que volver al inicio del bloque para ver el tipo de una variable.
Las sentencias anteriores son casos especiales importantes, que provocan confusin en los programadores C++ novatos. Las variables i y j estn denidas directamente dentro la expresin for (algo que no se puede hacer en C). Esas variables estn disponibles para usarlas en el bucle. Es una sintaxis muy conveniente porque el contexto disipa cualquier duda sobre el proposito de i y j, asi que no necesita utilizar nombres extraos como contador_bucle_i para quede ms claro. Sin embargo, podra resultar confuso si espera que la vida de las variables i y j continue despus del bucle algo que no ocurre3 El captulo 3 indica que las sentencias while y switch tambin permiten la denicin de objetos en sus expresiones de control, aunque ese uso es menos importante que con el bucle for. Hay que tener cuidado con las variables locales que ocultan las variables del mbito superior. En general, usar el mismo nombre para una variable anidada y una variable que es global en ese mbito es confuso y propenso a errores4 Creo que los bloques pequeos son un indicador de un buen diseo. Si una sola funcin requiere varias pginas, quiz est intentando demasiadas cosas en esa funcin. Funciones de granularidad ms na no slo son ms tiles, tambn facilitan la localizacin de errores.
3 Un reciente borrador del estndar C++ dice que la vida de la variable se extiende hasta el nal del mbito que encierra el bucle for. Algunos compiladores lo implementan, pero eso no es correcto de modo que su cdigo slo ser portable si limita el mbito al bucle for. 4 El lenguaje Java considera esto una idea tan mal que lo considera un error.
185
En el cdigo anterior, tanto el goto como el switch pueden saltar la sentencia en la que se invoca un constructor. Ese objeto corresponde al mbito incluso si no se invoca el constructor, de modo que el compilador dar un mensaje de error. Esto garantiza de nuevo que un objeto no se puede crear si no se inicializa. Todo el espacio de almacenamiento necesario se asigna en la pila, por supuesto. Ese espacio lo faciliza el compilador moviendo el puntero de pila hacia abajo (dependiendo de la mquina implica incrementar o decrementar el valor del puntero de pila). Los objetos tambin se pueden alojar en el montculo usando new, algo que se ver en el captulo 13. (FIXME:Ref C13)
De acuerdo, probablemente podra trucarlo usando punteros, pero sera muy, muy malo
186
Las nicas deniciones de mtodos que han cambiado son initialize() y cleanup(), que han sido reemplazadas con un constructor y un destructor.
//: C06:Stash2.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constructors & destructors #include "Stash2.h" #include "../require.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; Stash::Stash(int sz) { size = sz; quantity = 0; storage = 0; next = 0; } int Stash::add(void* element) { if(next >= quantity) // Enough space left? inflate(increment); // Copy element into storage, // starting at next empty space: int startBytes = next * size; unsigned char* e = (unsigned char*)element;
187
Puede ver que las funciones de require.h se usan para vigilar errores del programador, en lugar de assert(). La salida de un assert() fallido no es tan til como las funciones de require.h (que se vern ms adelante en el libro). Dado que inflate() es privado, el nico modo en que require() podra fallar sera si uno de los otros miembros pasara accidentalmente un valor incorrecto a inflate(). Si est seguro de que eso no puede pasar, debera considerar eliminar el require(), pero debera tener en mente que hasta que la clase sea estable, siempre existe la posibilidad de que el cdigo nuevo aadido a la clase podra provocar errores. El coste de require() es bajo (y podra ser eliminado automticamente por el preprocesador) mientras que la robusted del cdigo es alta. Fijese cmo en el siguiente programa de prueba la denicin de los objetos Stash aparecen justo antes de necesitarse, y cmo la inicializacin aparece como parte de la denicin, en la lista de argumentos del constructor.
//: C06:Stash2Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Stash2 // Constructors & destructors #include "Stash2.h" #include "../require.h" #include <fstream> #include <iostream>
188
Tambin observe que se ha elimiando llamada a cleanup(), pero los destructores se llaman automticamente cuando intStash y stringStash salen del mbito. Una cosa de la que debe ser consciente en los ejemplos con Stash: Tengo mucho cuidado usando slo tipos bsicos; es decir, aquellos sin destructores. Si intenta copiar objetos dentro de Stash, aparecern todo tipo de problemas y no funcionar bien. En realidad la Librera Estndar de C++ puede hacer copias correctas de objetos en sus contenedores, pero es un proceso bastante sucio y complicado. En el siguiente ejemplo de Stack, ver que se utilizan punteros para esquivar esta cuestin, y en un captulo posterior Stash tambin se convertir para que use punteros.
189
No slo hace que Stack tenga un constructor y destructir, tambin aparece la clase anidada Link.
//: C06:Stack3.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constructors/destructors #include "Stack3.h" #include "../require.h" using namespace std; Stack::Link::Link(void* dat, Link* nxt) { data = dat; next = nxt; } Stack::Link::~Link() { } Stack::Stack() { head = 0; } void Stack::push(void* dat) { head = new Link(dat,head); } void* Stack::peek() { require(head != 0, "Stack empty"); return head->data; } void* Stack::pop() { if(head == 0) return 0; void* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } Stack::~Stack() { require(head == 0, "Stack not empty"); }
El constructor Link:Link() simplemente inicializa los punteros data y next, as que en Stack::push(), la lnea:
head = new Link(dat,head);
no slo aloja un nuevo enlace (usando creacin dinmica de objetos con la sentencia new, vista en el captulo 4), tambin inicializa los punteros para ese enlace. Puede que le asombre que el destructor de Link no haga nada - en concreto, por qu no elimina el puntero data? Hay dos problemas. En el captulo 4, en el que se apareci Stack, se deca que no puede eliminar un puntero void si est apuntado a un objeto (una armacin que se demostrar en el captulo 13). Pero adems, si el destructor de Link eliminara el puntero data, pop() retornara un puntero a un objeto borrado, que denitivamente supone 190
6.6. Inicializacin de tipos agregados un error. A veces esto se llama una cuestin de propiedad: Link y por consiguiente Stack slo contienen los punteros, pero no son responsables de su limpieza. Esto signica que debe tener mucho cuidado para saber quin es el responsable. Por ejemplo, si no invoca pop() y elemina todos los punteros de Stack(), no se limipiarn automticamente por el destructor de Stack. Esto puede ser una cuestin engorrosa y llevar a fugas de memoria, de modo que saber quin es el responsable de la limpieza de un objeto puede suponer la diferencia entre un programa correcto y uno erroneo - es decir, porqu Stack::~Stack() imprime un mensaje de error si el objeto Stack no est vacio en el momento su destruccin. Dado que el alojamiento y limpieza de objetos Link estn ocultos dentro de Stack - es parte de la implementacin subyacente - no ver este suceso en el programa de prueba, aunque ser el responsable de de eliminar los punteros que devuelva pop():
//: C06:Stack3Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Stack3 //{T} Stack3Test.cpp // Constructors/destructors #include "Stack3.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; string line; // Read file and store lines in the stack: while(getline(in, line)) textlines.push(new string(line)); // Pop the lines from the stack and print them: string* s; while((s = (string*)textlines.pop()) != 0) { cout << *s << endl; delete s; } }
En este caso, todas las lneas de textlines son desapiladas y eliminadas, pero si no fuese as, obtendra un mensaje de require() que indica que hubo una fuga de memoria.
191
Captulo 6. Inicializacin y limpieza Si intenta escribir ms valores que elementos tiene el array, el compilador dar un mensaje de error. Pero, qu ocurre si escribes menos valores? Por ejemplo:
int b[6] = {0};
Aqu, el compilara usar el primer valor para el primer elemento del array, y despus usar ceros para todos los elementos para los que no se tiene un valor. Fijese en este comportamiento en la inicializacin no ocurre si dene un array sin una lista de valores de inicializacin. As que la expresin anterior es una forma resumida de inicializar a cero un array sin usar un bucle for, y sin ninguna posibilidad de un error por uno (Dependiendo del compilador, tambin puede ser ms eciente que un bucle for. Un segundo mtodo para los arrays es el conteo automtico, en el cual se permite que el compilador determine el tamao del array basndose en el nmero de valores de inicializacin.
int c[] = { 1, 2, 3, 4 };
Ahora, si decide aadir otro elemento al array, simplemente debe aadir otro valor. Si puede hacer que su cdigo necesite modicaciones en un nico stio, reducir la posibilidad de introducir errores durante la modicacin. Pero, cmo determinar el tamao del array? La expresin sizeof c / sizeof *c (el tamao del array completo dividido entre el tamao del primer elemento) es un truco que hace que no sea necesario cambiarlo si cambia el tamao del array 6 :
for(int i = 0; i < sizeof c / sizeof *c; i++) c[i]++;
Dado que las estructuras tambin son agregados, se pueden inicializar de un modo similar. Como en una estructura estilo-C todos sus miembros son pblicos, se pueden asignar directamente:
struct X { int i; float f; char c; }; X x1 = { 1, 2.2, c };
Si tiene una array de esos objetos, puede inicializarlos usando un conjunto anidado de llaves para cada elemento:
X x2[3] = { {1, 1.1, a}, {2, 2.2, b} };
Aqu, el tercer objeto se inicializ a cero. Si alguno de los atributos es privado (algo que ocurre tpicamente en el caso de clases bien diseadas en C++), o incluso si todos son pblicos pero hay un constructor, las cosas son distintas. En el ejemplo anterior, los valores se han asignado directamente a los elementos del agregado, pero los constructores son una manera de forzar que la inicializacin ocurra por medio de un interface formal. Aqu, los constructores deben ser invocados para realizar la inicializacin. De modo, que si tiene un constructor parecido a ste,
struct Y { float f; int i; Y(int a); };
6 En el segundo volumen de este libro (disponible libremente en www.BruceEckel.com), ver una forma ms corta de calcular el tamao de un array usando plantillas.
192
Debe indicar la llamada al constructor. La mejor aproximacin es una explcita como la siguiente:
Y y1[] = { Y(1), Y(2), Y(3) };
Obtendr tres objetos y tres llamadas al constructor. Siempre que tenga un constructor, si es una estructura con todos sus miembros pblicos o una clase con atributos privados, toda la inicializacin debe ocurrir a travs del constructor, incluso si est usando la inicializacin de agregados. Se muestra un segundo ejemplo con un constructor con mltiples argumentos.
//: C06:Multiarg.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Multiple constructor arguments // with aggregate initialization #include <iostream> using namespace std; class Z { int i, j; public: Z(int ii, int jj); void print(); }; Z::Z(int ii, int jj) { i = ii; j = jj; } void Z::print() { cout << "i = " << i << ", j = " << j << endl; } int main() { Z zz[] = { Z(1,2), Z(3,4), Z(5,6), Z(7,8) }; for(int i = 0; i < sizeof zz / sizeof *zz; i++) zz[i].print(); }
el compilador se quejar porque no puede encontrar un constructor por defecto. El segundo objeto del array se crear sin argumentos, y es ah donde el compilador busca un constructor por defecto. De hecho, si simplemente dene un array de objetos Y, 193
Y y3[7];
el compilador se quejar porque debe haber un constructor para inicializar cada objeto del array. El mismo problema ocurre si crea un objeto individual como ste:
Y y4;
Recuerde, si tiene un constructor, el compilador asegura que siempre ocurrir la construccin, sin tener en cuenta la situacin. El constructor por defecto es tan importante que si (y slo si) una estructura (struct o clase) no tiene constructor, el compiladore crear uno automticamente. Por ello, lo siguiente funciona:
//: C06:AutoDefaultConstructor.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Automatically-generated default constructor class V { int i; // private }; // No constructor int main() { V v, v2[10]; }
Si se han denido constructores, pero no hay constructor por defecto, las instancias anteriores de V provocarn errores durante la compilacin. Podra pensarse que el constructor sintetizado por el compilador debera hacer alguna inicializacin inteligente, como poner a cero la memoria del objeto. Pero no lo hace - aadira una sobrecarga que quedara fuera del control del programador. Si quiere que la memoria sea inicializada a cero, debera hacerlo escribiendo un constructor por defecto explcito. Aunque el compilador crear un constructor por defecto, el comportamiento de ese constructor raramente har lo que se espera. Debera considerar esta caracterstica como una red de seguridad, pero que debe usarse con moderacin. En general, debera denir sus constructores explicitamente y no permitir que el compilador lo haga en su lugar.
6.8. Resumen
Los mecanismos aparecentemente elaborados proporcionados por C++ deberan darle una idea de la importancia crtica que tiene en el lenguaje la inicializacin y limpieza. Como Stroustrup fue quien diseo C++, una de las primeras observaciones que hizo sobre la productividad de C fue que una parte importante de los problemas de programacin se deben a la inicializacin inapropiada de las variables. Estos tipo de errores son difciles de encontrar, y otro tanto se puede decir de una limpieza inapropiada. Dado que constructores y destructores le permiten garantizar una inicializacino y limpieza apropiada (el compilador no permitir que un objeto sea creado o destruido sin la invocacin del contructor y destructor correspondiente), conseguir control y seguridad. La inicializacin de agregados est incluida de un modo similar - previene de errores de inicializacin tpicos con agregados de tipos bsicos y hace que el cdigo sea ms corto. La seguridad durante la codicacin es una cuestin importante en C++. La inicializacin y la limpieza son una parte importante, pero tambin ver otras cuestiones de seguridad ms adelante en este libro. 194
6.9. Ejercicios
6.9. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Escriba una clase simple llamada Simple con un constructor que imprima algo indicando que se ha invocado. En main() crear un objeto de esa clase. 2. Aada un destructor al Ejercicio 1 que imprima un mensaje indicado que se ha llamado. 3. Modique el Ejercicio 2 de modo que la clase contenga un miembro int. Modique el constructor para que tome un argumento int que se almacene en el atributo. Tanto el contructor como el destructor debern imprimir el valor del entero como parte se su mensaje, de modo que se pueda ver cmo se crean y destruyen los objetos. 4. Demuestre se invocan incluso cuando se utiliza goto para salir de un bucle. 5. Escriba dos bucles for que impliman los valores de 0 a 10. En el primero, dena el contador del bucle antes del bucle, y en el segundo, dena el contador en la expresin de control del for. En la segunda parte del ejercicio, modique el identicador del segundo bucle para que tenga el mismo nombre del el contador del priimero y vea que hace el compilador. 6. Modique los cheros Handle.h, Handle.cpp, y UseHandle.cpp del captulo 5 para que usen constructores y destructores. 7. Use inicializacin de agregados para crear un array de double en el que se indique el tamao del array pero no se den sucientes elementos. Imprima el array usando sizeof para determinar el tamao del array. Ahora cree un array de double usando inicializacin de agregados y conteo automtico. Imprima el array. 8. Utiliza inicializacin de agregados para crear un array de objetos string. Cree una Stack para guardar esas cadenas y recorra el array, apilando cada cadena en la pila. Finalmente, extraiga las cadenas de la pila e imprima cada una de ellas. 9. Demuestre el conteo automtico e inicializacin de agregados con un array de objetos de la clase creada en el Ejercicio 3. Aada un mtodo a la clase que imprima un mensaje. Calcule el tamao del array y recorralo, llamando al nuevo mtodo. 10. Cree una clase sin ningn constructor, y demuestre que puede crear objetos con el constructor por defecto. Ahora cree un constructor explcito (que tenga un argumento) para la clase, e intente compilar de nuevo. Explique lo que ocurre.
195
Captulo 7. Sobrecarga de funciones y argumentos por defecto termine este captulo, sabr cundo utilizarlos y entender los mecanismos internos que el compilador utiliza en tiempo de compilacin y enlace.
La funcin f() dentro del mbito de la clase X no entra en conicto con la versin global de f(). El compilador resuelve los mbitos generando diferentes nombres internos tanto para la versin global de f() como para X::f(). En el Captulo 4 se sugiri que los nombres son simplemente el nombre de la clase junto con el nombre de la funcin. Un ejemplo podra ser que el compilador utilizara como nombres _f y _X_f. Sin embargo ahora se ve que la decoracin del nombre de la funcin involucra algo ms que el nombre de la clase. He aqu el porqu. Suponga que quiere sobrecargar dos funciones
void print(char); void print(float);
No importa si son globales o estn dentro de una clase. El compilador no puede generar identicadores internos nicos si slo utiliza el mbito de las funciones. Terminara con _print en ambos casos. La idea de una funcin sobrecargada es que se utilice el mismo nombre de funcin, pero diferente lista de argumentos. As pues, para que la sobrecarga funcione el compilador ha de decorar el nombre de la funcin con los nombres de los tipos de los argumentos. Las funciones planteadas ms arriba, denidas como globales, producen nombres internos que podran parecerse a algo as como _print_char y _print_oat. Ntese que como no hay ningn estndar de decoracin, podr ver resultados diferentes de un compilador a otro. (Puede ver lo que saldra dicindole al compilador que genere cdigo fuente en ensamblador.) Esto, por supuesto, causa problemas si desea comprar unas libreras compiladas por un compilador y enlazador particulares, aunque si la decoracin de nombres fuera estndar, habra otros obstculos debido a las diferencias de generacin de cdigo mquina entre compiladores. Esto es todo lo que hay para la sobrecarga de funciones: puede utilizar el mismo nombre de funcin siempre y cuando la lista de argumentos sea diferente. El compilador utiliza el nombre, el mbito y la lista de argumentos para generar un nombre interno que el enlazador pueda utilizar.
Esto funciona bien cuando el compilador puede determinar sin ambigedades a qu tipo de valor de retorno se reere, como en int x = f();. No obstante, en C se puede llamar a una funcin y hacer caso omiso del valor de retorno (esto es, puede querer llamar a la funcin debido a sus efectos laterales). Cmo puede el compilador distinguir a qu funcin se reere en este caso? Peor es la dicultad que tiene el lector del cdigo fuente para dilucidar a qu funcin se reere. La sobrecarga mediante el valor de retorno solamente es demasiado sutil, por lo que C++ no lo permite.
7.2. Ejemplo de sobrecarga mente declarada, y el compilador inere la declaracin de la funcin mediante la forma en que se llama. Algunas veces la declaracin de la funcin es correcta, pero cuando no lo es, suele resultar en un fallo difcil de encontrar. A causa de que en C++ se deben declarar todas las funciones antes de llamarlas, las probabilidades de que ocurra lo anteriormente expuesto se reducen drsticamente. El compilador de C++ rechaza declarar una funcin automticamente, as que es probable que tenga que incluir la cabecera apropiada. Sin embargo, si por alguna razn se las apaa para declarar mal una funcin, o declararla a mano o incluir una cabecera incorrecta (quiz una que sea antigua), la decoracin de nombres proporciona una seguridad que a menudo se denomina como enlace con tipos seguros. Considere el siguiente escenario. En un chero est la denicin de una funcin:
//: C07:Def.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Function definition void f(int) {}
Incluso aunque pueda ver que la funcin es realmente f(int), el compilador no lo sabe porque se le dijo, a travs de una declaracin explcita, que la funcin es f(char). As pues, la compilacin tiene xito. En C, el enlazador podra tener tambin xito, pero no en C++. Como el compilador decora los nombres, la denicin se convierte en algo as como f_int, mientras que se trata de utilizar f_char. Cuando el enlazador intenta resolver la referencia a f_char, slo puede encontrar f_int, y da un mensaje de error. ste es el enlace de tipos seguro. Aunque el problema no ocurre muy a menudo, cuando ocurre puede ser increblemente difcil de encontrar, especialmente en proyectos grandes. ste mtodo puede utilizarse para encontrar un error en C simplemente intentando compilarlo en C++.
199
El primer constructor de Stash es el mismo que antes, pero el segundo tiene un argumento Quantity que indica el nmero inicial de espacios de memoria que podrn ser asignados. En la denicin, puede observar que el valor interno de quantity se pone a cero, al igual que el puntero storage. En el segundo constructor, la llamada a inflate(initQuantity) incrementa quantity al tamao asignado:
//: C07:Stash3.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Function overloading #include "Stash3.h" #include "../require.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; Stash::Stash(int sz) { size = sz; quantity = 0; next = 0; storage = 0; } Stash::Stash(int sz, int initQuantity) { size = sz; quantity = 0; next = 0; storage = 0; inflate(initQuantity); } Stash::~Stash() { if(storage != 0) { cout << "freeing storage" << endl; delete []storage; } } int Stash::add(void* element) { if(next >= quantity) // Enough space left? inflate(increment);
200
Cuando utiliza el primer constructor no se asigna memoria alguna para storage. La asignacin ocurre la primera vez que trate de aadir (con add()) un objeto y en cualquier momento en el que el bloque de memoria actual se exceda en add(). Ambos constructores se prueban en este programa de prueba:
//: C07:Stash3Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Stash3 // Function overloading #include "Stash3.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j)
201
La llamada al constructor para la variable stringStash utiliza un segundo argumento; se presume que conoce algo especial sobre el problema especco que usted est resolviendo que le permite elegir un tamao inicial para el Stash.
7.3. Uniones
Como ya ha visto, la nica diferencia en C++ entre struct y class es que struct pone todo por defecto a public y la clase pone todo por defecto a private. Una structura tambin puede tener constructores y destructores, como caba esperar. Pero resulta que el tipo union tambin puede tener constructores, destructores, mtodos e incluso controles de acceso. Puede ver de nuevo la utilizacin y las ventajas de la sobrecarga de funciones en el siguiente ejemplo:
//: C07:UnionClass.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Unions with constructors and member functions #include<iostream> using namespace std; union U { private: // Access control too! int i; float f; public: U(int a); U(float b); ~U(); int read_int(); float read_float(); }; U::U(int a) { i = a; } U::U(float b) { f = b;} U::~U() { cout << "U::~U()\n"; } int U::read_int() { return i; } float U::read_float() { return f; } int main() { U X(12), Y(1.9F);
202
7.3. Uniones
cout << X.read_int() << endl; cout << Y.read_float() << endl; }
Podra pensar sobre el cdigo de ms arriba que la nica diferencia entre una unin y una clase es la forma en que los datos se almacenan en memoria (es decir, el int y el oat estn superpuestos). Sin embargo una unin no se puede utilizar como clase base durante la herencia, lo cual limita bastante desde el punto de vista del diseo orientado a objetos (aprender sobre la herencia en el Captulo 14). Aunque los mtodos civilizan ligeramente el tratamiento de uniones, sigue sin haber manera alguna de prevenir que el programador cliente seleccione el tipo de elemento equivocado una vez que la unin se ha inicializado. En el ejemplo de ms arriba, podra decir X.read_float() incluso aunque es inapropiado. Sin embargo, una unin "segura" se puede encapsular en una clase. En el siguiente ejemplo, vea cmo la enumeracin clarica el cdigo, y cmo la sobrecarga viene como anillo al dedo con los constructores:
//: C07:SuperVar.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // A super-variable #include <iostream> using namespace std; class SuperVar { enum { character, integer, floating_point } vartype; // Define one union { // Anonymous union char c; int i; float f; }; public: SuperVar(char ch); SuperVar(int ii); SuperVar(float ff); void print(); }; SuperVar::SuperVar(char ch) { vartype = character; c = ch; } SuperVar::SuperVar(int ii) { vartype = integer; i = ii; } SuperVar::SuperVar(float ff) { vartype = floating_point; f = ff; } void SuperVar::print() { switch (vartype) { case character: cout << "character: " << c << endl; break;
203
En el ejemplo de ms arriba la enumeracin no tiene nombre de tipo (es una enumeracin sin etiqueta). Esto es aceptable si va a denir inmediatamente un ejemplar de la enumeracin, tal como se hace aqu. No hay necesidad de referir el nombre del tipo de la enumeracin en el futuro, por lo que aqu el nombre de tipo es optativo. La unin no tiene nombre de tipo ni nombre de variable. Esto se denomina unin annima, y crea espacio para la unin pero no requiere acceder a los elementos de la unin con el nombre de la variable y el operador punto. Por ejemplo, si su unin annima es:
//: C07:AnonymousUnion.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt int main() { union { int i; float f; }; // Access members without using qualifiers: i = 12; f = 1.22; }
Note que accede a los miembros de una unin annima igual que si fueran variables normales. La nica diferencia es que ambas variables ocupan el mismo espacio de memoria. Si la unin annima est en el mbito del chero (fuera de todas las funciones y clases), entonces se ha de declarar esttica para que tenga enlace interno. Aunque SuperVar es ahora segura, su utilidad es un poco dudosa porque la razn de utilizar una unin principalmente es la de ahorrar memoria y la adicin de vartype hace que ocupe bastante espacio en la unin (relativamente), por lo que la ventaja del ahorro se elimina. Hay un par de alternativas para que este esquema funcione. Si vartype controlara ms de una unin (en el caso de que fueran del mismo tipo) entonces slo necesitara uno para el grupo y no ocupara ms memoria. Una aproximacin ms til es tener #ifdef s alrededor del cdigo de vartype, el cual puede entonces garantizar que las cosas se utilizan correctamente durante el desarrollo y las pruebas. Si el cdigo ha de entregarse, antes puede eliminar las sobrecargas de tiempo y memoria.
7.4. Argumentos por defecto ninguno en la llamada a la funcin. En el ejemplo de Stash, puede reemplazar las dos funciones:
Stash(int size); // Zero quantity Stash(int size, int initQuantity);
La denicin de Stash(int) simplemente se quita; todo lo necesario est ahora en la denicin de Stash(int, int). Ahora, las deniciones de los dos objetos
Stash A(100), B(100, 0);
producirn exactamente los mismos resultados. En ambos casos se llama al mismo constructor, aunque el compilador substituye el segundo argumento de A automticamente cuando ve que que el primer argumento es un entero y no hay un segundo argumento. El compilador ha detectado un argumento por defecto, as que sabe que todava puede llamar a la funcin si substituye este segundo argumento, lo cual es lo que usted le ha dicho que haga al no poner ese argumento. Los argumentos por defecto, al igual que la sobrecarga de funciones, son muy convenientes. Ambas caractersticas le permiten utilizar un nico nombre para una funcin en situaciones diferentes. La diferencia est en que el compilador substituye los argumentos por defecto cuando usted no los pone. El ejemplo anterior en un buen ejemplo para utilizar argumentos por defecto en vez de la sobrecarga de funciones; de otra manera se encuentra con dos o ms funciones que tienen rmas y comportamientos similares. Si las funciones tiene comportamientos muy diferentes, normalmente no tiene sentido utilizar argumentos por defecto (de hecho, podra querer preguntarse si dos funciones con comportamientos muy diferentes deberan llamarse igual). Hay dos reglas que se deben tener en cuenta cuando se utilizan argumentos por defecto. La primera es que slo los ltimos pueden ser por defecto, es decir, no puede poner un argumento por defecto seguido de otro que no lo es. La segunda es que una vez se empieza a utilizar los argumentos por defecto al realizar una llamada a una funcin, el resto de argumentos tambin sern por defecto (esto sigue a la primera regla). Los argumentos por defecto slo se colocan en la declaracin de la funcin (normalmente en el chero de cabecera). El compilador debe conocer el valor por defecto antes de utilizarlo. Hay gente que pone los valores por defecto comentados en la denicin por motivos de documentacin.
void fn(int x /* = 0 */) { // ...
En el cuerpo de la funcin, se puede hacer referencia a x y a flt, pero no al argumento de en medio puesto que no tiene nombre. A pesar de esto, las llamadas a funcin deben proporcionar un valor para este argumento de relleno : f(1) f(1,2,3,0). Esta sintaxis le permite poner el argumento como un argumento de relleno sin 205
Captulo 7. Sobrecarga de funciones y argumentos por defecto utilizarlo. La idea es que podra querer cambiar la denicin de la funcin para utilizar el argumento de relleno ms tarde, sin cambiar todo el cdigo en que ya se invoca la funcin. Por supuesto, puede obtener el mismo resultado utilizando un argumento con nombre, pero en ese caso est deniendo el argumento para el cuerpo de la funcin sin que ste lo utilice, y la mayora de los compiladores darn un mensaje de aviso, suponiendo que usted ha cometido un error. Si deja el argumento sin nombre intencionadamente, evitar el aviso. Ms importante, si empieza utilizando un argumento que ms tarde decide dejar de utilizar, puede quitarlo sin generar avisos ni fastidiar al cdigo cliente que est utilizando la versin anterior de la funcin.
El objeto Mem contiene un bloque de octetos y se asegura de que tiene suciente memoria. El constructor por defecto no reserva memoria pero el segundo constructor se asegura de que hay sz octetos de memoria en el objeto Mem. El destructor libera la memoria, msize() le dice cuntos octetos hay actualmente en Mem y pointer() retorna un puntero al principio de la memoria reservada (Mem es una herramienta a bastante bajo nivel). Hay una versin sobrecargada de pointer() que los programadores clientes pueden utilizar para obtener un puntero que apunta a un bloque de memoria con al menos el tamao minSize, y el mtodo lo asegura. El constructor y el mtodo pointer() utilizan el mtodo privado ensureMinSize() para incrementar el tamao del bloque de memoria (note que no es seguro mantener el valor de retorno de pointer() si se cambia el tamao del bloque de memoria). He aqu la implementacin de la clase:
//: C07:Mem.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "Mem.h" #include <cstring> using namespace std;
206
Puede observar que ensureMinSize() es la nica funcin responsable de reservar memoria y que la utilizan tanto el segundo constructor como la segunda versin sobrecargada de pointer(). Dentro de ensureSize() no se hace nada si el tamao es lo sucientemente grande. Si se ha de reservar ms memoria para que el bloque sea ms grande (que es el mismo caso cuando el bloque tiene tamao cero despus del constructor por defecto), la nueva porcin de ms se pone a cero utilizando la funcin de la librera estndar de C memset(), que fue presentada en el Captulo 5. La siguiente llamada es a la funcin de la librera estndar de C memcpy(), que en este caso copia los octetos existentes de mem a newmem (normalmente de una manera ecaz). Finalmente, se libera la memoria antigua y se asignan a los atributos apropiados la nueva memoria y su tamao. La clase Mem se ha diseado para su utilizacin como herramienta dentro de otras clases para simplicar su gestin de la memoria (tambin se podra utilizar para ocultar un sistema de gestin de memoria ms avanzada proporcionado, por ejemplo, por el el sistema operativo). Esta clase se comprueba aqu con una simple clase de tipo "string":
//: C07:MemTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Testing the Mem class //{L} Mem #include "Mem.h" #include <cstring> #include <iostream> using namespace std; class MyString { Mem* buf; public: MyString(); MyString(char* str); ~MyString();
207
MyString::MyString(char* str) { buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(), str); } void MyString::concat(char* str) { if(!buf) buf = new Mem; strcat((char*)buf->pointer( buf->msize() + strlen(str) + 1), str); } void MyString::print(ostream& os) { if(!buf) return; os << buf->pointer() << endl; } MyString::~MyString() { delete buf; } int main() { MyString s("My test string"); s.print(cout); s.concat(" some additional stuff"); s.print(cout); MyString s2; s2.concat("Using default constructor"); s2.print(cout); }
Todo lo que puede hacer con esta clase es crear un MyString, concatenar texto e imprimir a un ostream. La clase slo contiene un puntero a un Mem, pero note la diferencia entre el constructor por defecto, que pone el puntero a cero, y el segundo constructor, que crea un Mem y copia los datos dentro del mismo. La ventaja del constructor por defecto es que puede crear, por ejemplo, un array grande de objetos MyString vacos con pocos recursos, pues el tamao de cada objeto es slo un puntero y la nica sobrecarga en el rendimiento del constructor por defecto es el de asignarlo a cero. El coste de un MyString slo empieza a aumentar cuando concatena datos; en ese momento el objeto Mem se crea si no ha sido creado todava. Sin embargo, si utiliza el constructor por defecto y nunca concatena ningn dato, la llamada al destructor todava es segura porque cuando se llama a delete con un puntero a cero, el compilador no hace nada para no causar problemas. Si mira los dos constructores, en principio, podra parecer que son candidatos para utilizar argumentos por defecto. Sin embargo, si elimina el constructor por defecto y escribe el constructor que queda con un argumento por defecto:
MyString(char* str = "");
todo funcionar correctamente, pero perder la ecacia anterior pues siempre se crear el objeto Mem. Para volver a tener la misma ecacia de antes, ha de modicar el constructor:
MyString::MyString(char* str) { if(!*str) { // Apunta a un string vaco buf = 0; return; } buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(), str); }
208
7.6. Resumen
Esto signica, en efecto, que el valor por defecto es un caso que ha de tratarse separadamente de un valor que no lo es. Aunque parece algo inocente con un pequeo constructor como ste, en general esta prctica puede causar problemas. Si tiene que hacer por separado el valor por defecto en vez de tratarlo como un valor ordinario, debera ser una pista para que al nal se implementen dos funciones diferentes dentro de una funcin: una versin para el caso normal y otra para el caso por defecto. Podra partirlo en dos cuerpos de funcin diferentes y dejar que el compilador elija. Esto resulta en un ligero (pero normalmente invisible) incremento de la ecacia porque el argumento extra no se pasa y por tanto el cdigo extra debido a la condicin condicin no se ejecuta. Ms importante es que est manteniendo el cdigo en dos funciones separadas en vez de combinarlas en una utilizando argumentos por defecto, lo que resultar en un mantenimiento ms sencillo, sobre todo si las funciones son largas. Por otro lado, considere la clase Mem. Si mira las deniciones de los dos constructores y las dos funciones pointer(), puede ver que la utilizacin de argumentos por defecto en ambos casos no causar que los mtodos cambien. As, la clase podra ser fcilmente:
//: C07:Mem2.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef MEM2_H #define MEM2_H typedef unsigned char byte; class Mem { byte* mem; int size; void ensureMinSize(int minSize); public: Mem(int sz = 0); ~Mem(); int msize(); byte* pointer(int minSize = 0); }; #endif // MEM2_H
Note que la llamada a ensureMinSize(0) siempre ser bastante ecaz. Aunque ambos casos se basan en decisiones por motivos de ecacia, debe tener cuidado para no caer en la trampa de pensar slo en la ecacia (siempre fascinante). Lo ms importante en el diseo de una clase es la interfaz de la clase (sus miembros pblicos, que son las que el programador cliente tiene a su disposicin). Si se implementa una clase fcil de utilizar y reutilizar, entonces ha tenido xito; siempre puede realizar ajustes para mejorar la ecacia en caso necesario, pero el efecto de una clase mal diseada porque el programador est obsesionado con la ecacia puede resultar grave. Su primera preocupacin debera ser que la interfaz tuviera sentido para aqullos que la utilicen y para los que lean el cdigo. Note que en MemTest.cpp el uso de MyString no cambia independientemente de si se utiliza el constructor por defecto o si la ecacia es buena o mala.
7.6. Resumen
Como norma, no debera utilizar argumentos por defecto si hay que incluir una condicin en el cdigo. En vez de eso debera partir la funcin en dos o ms funciones sobrecargadas si puede. Un argumento por defecto debera ser un valor que normalmente pondra ah. Es el valor que es ms probable que ocurra, para que lo programadores clientes puedan hacer caso omiso de l o slo lo pongan cuando no quieran utilizar el valor por defecto. El argumento por defecto se incluye para hacer ms fciles las llamadas a funcin, especialmente cuando esas funciones tiene muchos argumentos con valores tpicos. No slo es mucho ms sencillo escribir las llamadas, sino que adems son ms sencillas de leer, especialmente si el creador de la clase ordena los argumentos de tal manera que aqullos que menos cambian se ponen al nal del todo. 209
Captulo 7. Sobrecarga de funciones y argumentos por defecto Una utilizacin especialmente importante de los argumentos por defecto es cuando empieza con una funcin con un conjunto de argumentos, y despus de utilizarla por un tiempo se da cuenta de que necesita aadir ms argumentos. Si pone los nuevos argumentos como por defecto, se asegura de que no se rompe el cdigo cliente que utiliza la interfaz anterior.
7.7. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree una clase Text que contenga un objeto string para que guarde el texto de un chero. Pngale dos constructores: un constructor por defecto y un constructor que tome un argumento de tipo string que sea el nombre del chero que se vaya a abrir. Cuando se utilice el segundo constructor, abra el chero y ponga su contenido en el atributo string. Aada un mtodo llamado contents() que retorne el string para que, por ejemplo, se pueda imprimir. En main() abra un chero utilizando Text e imprima el contenido a la pantalla. 2. Cree una clase Message con un constructor que tome un slo string con un valor por defecto. Cree un atributo privado string y asigne en el constructor el argumento string al atributo string. Cree dos mtodos sobrecargados llamados print(): uno que no tome argumentos y que imprima simplemente el mensaje guardado en el objeto, y el otro que tome un argumento string, que imprima el mensaje interno adems del argumento. Tiene sentido utilizar esta aproximacin en vez del utilizado por el constructor? 3. Descubra cmo generar cdigo ensamblador con su compilador y haga experimentos para deducir el esquema de decoracin de nombres. 4. Cree una clase que contenga cuatro mtodos con 0, 1, 2 y 3 argumentos de tipo int respectivamente. Cree un main() que haga un objeto de su clase y llame a cada mtodo. Ahora modique la clase para que tenga slo un mtodo con todos los argumentos por defecto. Esto cambia su main()? 5. Cree una funcin con dos argumentos y llmelo desde main(). Ahora haga que uno de los argumentos sea un argumento de relleno (sin identicador) y mire si el main() necesita cambios. 6. Modique Stash3.h y Stash3.cpp para que el constructor utilice argumentos por defecto. Pruebe el constructor haciendo dos versiones diferentes de un objeto Stash. 7. Cree una nueva versin de la clase Stack (del Captulo 6) que contenga el constructor por defecto al igual que antes, y un segundo constructor que tome como argumentos un array de punteros a objetos y el tamao del array. Este constructor debera recorrer el array y poner cada puntero en la pila (Stack). Pruebe su clase con un array de strings. 8. Modique SuperVar para que haya #ifdef s que engloben el cdigo de vartype tal como se describe en la seccin sobre enumeraciones. Haga vartype como una enumeracin pblica (sin ejemplares) y modique print() para que requiera un argumento del tipo vartype que le indique qu tiene que hacer. 9. Implemente Mem2.h y asegrese de que la clase modicada todava funcione con MemTest.cpp. 10. Utilice la clase Mem para implementar Stash. Note que debido a que la implementacin es privada y por tanto oculta al programador cliente, no necesita modicar el cdigo de prueba. 11. Aada un mtodo bool moved() en la clase Mem que tome el resultado de una llamada a pointer() y le diga si el puntero ha cambiado (debido a una reasignacin). Escriba una funcin main() que pruebe su mtodo moved(). Tiene ms sentido utilizar algo como moved() o simplemente llamar pointer() cada vez que necesite acceder a la memoria de Mem?
210
8: Constantes
El concepto de constante (expresin con la palabra reservada const) se cre para permitir a los programadores marcar la diferencia entre lo que puede cambiar y lo que no. Esto facilita el control y la seguridad en un proyecto de programacin.
Desde su origen, const ha sido utilizada para diferentes propositos. In the meantime it trickled back into the C language where its meaning was change puede parecer un poco confuso al principio, y en este captulo aprender cundo, por qu y cmo usar la palabra reservada const. Hacia el nal se expone una disertacin de volatile, que es familia de const (pues ambos se reeren a los cambios) y su sintaxis es idntica. El primer motivo para la creacin de const parace que fue eliminar el uso de la directiva del preprocesador #dene para sustitucin de valores. Desde entonces se usa para punteros, argumentos de funciones, tipos de retorno, objetos y funciones miembro. Todos ellos tienen pequeas diferencias pero su signicado es conceptualmente compatible. Sern tratados en diferentes secciones de este captulo.
BUFSIZE es un nombre que slo existe durante el preprocesamiento. Por tanto, no ocupa memoria y se puede colocar en un chero de cabecera para ofrecer un valor nico a todas las unidades que lo utilicen. Es muy importante para el mantenimiento del cdigo el uso de sustitucin de valores en lugar de los tambin llamados nmeros mgicos. Si usa numeros mgicos en su cdigo. no solamente impedir al lector conocer su procedencia o signicado sino que complicar inecesariamente la edicin del cdigo si necesitara cambiar dicho valor. La mayor parte del tiempo, BUFSIZE se comportar como un valor ordinario, pero no siempre. No tiene informacin de tipo. Eso puede esconder errores diciles de localizar. C++ utilizar const para eliminar estos problemas llevando la sustitucin de valores al terreno del compilador. Ahora, puede escribir:
const int bufsize = 100;
Puede colocar bufsize en cualquier lugar donde se necesite conocer el valor en tiempo de compilacin. El compilador utiliza bufsize para hacer [FIXME: constant folding], que signica que el compilador reduce una expresin constante complicada a un valor simple realizando los calculos necesarios en tiempo de compilacin. Esto es especialmente importante en las deniciones de vectores:
char buf[bufsize];
Puede usar const con todos los tipos bsicos(char, int, oat y double) y sus variantes (as como clases y todo lo que ver despus en este captulo). Debido a los problemas que introduce el preprocesador deber utilizar siempre 211
Normalmente el compilador de C++ evita la asignacin de memoria para las constantes, pero en su lugar ocupa una entrada en la tabla de smbolos. Cuando se utiliza extern con una constante, se fuerza el alojamiento en memoria (esto tambin ocurre en otros casos, como cuando se solicita la direccin de una constante). El uso de la memoria debe hacerse porque extern dice usar enlace externo, es decir, que varios mdulos deben ser capaces de hacer referencia al elemento, algo que requiere su almacenamiento en memoria. Por lo general, cuando extern no forma parte de la denicin, no se pide memoria. Cuando la constante se utiliza simplemte se incorpora en tiempo de compilacin. El objetivo de no almacenar en memoria las constantes tampoco se cumple con estructuras complicadas. Siempre que el compilador debe pedir memoria, se evita el [FIXME constant folding] (ya que el compilador no tiene forma de conocer con seguridad que valor debe almacenar; si lo conociese, no necesitara pedir memoria). Como el compilador no siempre puede impedir el almacenamiento para una constante, las deniciones de constantes utilizan enlace interno, es decir, se enlazan slo con el mdulo en que se denen. En caso contrario, los errores de enlace podran ocurrir con las expresiones constantes complicadas ya que causaran peticion de almacenamiento en diferentes mdulos. Entonces, el enlazador vera la misma denicin en multiples archivos objeto, lo que causara un error en el enlace. Con los tipos del lenguaje, que se usan en la mayor parte de las expresiones constantes, el compilador siempre realiza [FIXME: constant folding]. Ya que las constantes utilizan enlace interno, el enlazador no intenta enlazar esas deniciones a travs de los mdulos, y as no hay colisiones. Con los tipos bsicos, que son los se ven involucrados en la mayora de los casos, el compilador siempre ejecuta [FIXME constant folding].
212
Puede ver que i es una constante en tiempo de compilacin, pero j se calcula a partir de i. Sin emargo, como i es una constante, el valor calculado para j es una expresin constante y es en si mismo otra constante en tiempo de compilacin. En la siguiente lnea se necesita la direccin de j y por lo tanto el compilador se ve obligado a pedir almacenamiento para j. Ni siquiera eso impide el uso de j para determinar el tamao de buf porque el compilador sabe que j es una constante y que su valor es vlido aunque se asigne almacenamiento, ya que eso se hace para mantener el valor en algn punto en el programa. En main(), aparece un tipo diferente de constante en el identicador c, porque el valor no puede ser conocido en tiempo de compilacin. Eso signica que se requiere almacenamiento, y por eso el compilador no intenta mantener nada en la tabla de simbolos (el mismo comportamiento que en C). La inicializacin debe ocurrir, an as, en el punto de la denicin, y una vez que ocurre la inicializacin, el valor ya no puede ser cambiado. Puede ver que c2 se calcula a partir de c y adems las reglas de mbito funcionan para las constantes igual que para cualquier otro tipo, otra ventaja respecto al uso de #define. En la prctica, si piensa que una variable no debera cambiar, debera hacer que fuese una constante. Esto no slo da seguridad contra cambios inadvertidos, tambin permite al compilador generar cdigo ms eciente ahorrando espacio de almacenamiento y lecturas de memoria en la ejecucin del programa.
8.1.3. Vectores
Es posible usar constantes para los vectores, pero prcticamente est asegurado que el compilador no ser lo sucientemente sosticado para mantener un vector en la tabla de smbolos, as que le asignar espacio de almacenamiento. En estas situaciones, const signica un conjunto de datos en memoria que no pueden modicarse. En cualquier caso, sus valores no puede usarse en tiempo de compilacin porque el compilador no conoce en ese momento los contenidos de las variables que tienen espacio asignado. En el cdigo siguiente puede ver algunas declaraciones incorrectas.
//: C08:Constag.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constants and aggregates const int i[] = { 1, 2, 3, 4 }; //! float f[i[3]]; // Illegal struct S { int i, j; }; const S s[] = { { 1, 2 }, { 3, 4 } }; //! double d[s[1].j]; // Illegal int main() {}
En la denicin de un vector, el compilador debe ser capaz de generar cdigo que mueva el puntero de pila para dar cabida al vector. En las deniciones incorrectas anteriores, el compilador se queja porque no puede encontrar una expresin constante en la denicin del tamao del vector.
213
Captulo 8. Constantes
aparecer un error, aunque parezca algo razonable. Bufsize est guardado en algn sitio y el compilador no conoce su valor en tiempo de compilacin. Opcionalmente puede escribir:
const int bufsize;
en C, pero no en C++, y el compilador C lo acepta como una declaracin que indica que se almacenar en alguna parte. Como C utiliza enlace externo para las constantes, esa semntica tiene sentido. C++ utiliza normalmente enlace interno, as que, si quiere hacer lo mismo en C++, debe indicar expresamente que se use enlace externo usando extern.
extern const int bufsize; // es declaracin, no definicin
Esta declaracin tambin es vlida en C. En C++, const no implica necesariamente almacenamiento. En C, las constantes siempre necesitan almacenamiento. El hecho de que se necesite almacenamiento o no depende de como se use la constante. En general, si una constante se usa simplemente para reemplazar un nmero por un nombre (como hace #define), entonces no requiere almacenamiento. Si es as (algo que depende de la complejidad del tipo de dato y de lo sosticacin del compilador) los valores pueden expandirse en el cdigo para conseguir mayor eciencia despus de la comprobacin de los tipos, no como con #define. Si de todas formas, se necesita la direccin de una constante (an desconocida, para pasarla a una funcin como argumento por referencia) o se declara como extern, entonces se requiere asignar almacenamiento para la constante. En C++, una constante que est denida fuera de todas las funciones tiene mbito de archivo (es decir, es inaccesible fuera del archivo). Esto signica que usa enlace interno. Esto es diferente para el resto de indenticadores en C++ (y para las constantes en C) que utilizan siempre enlace externo. Por eso, si declara una constante con el mismo nombre en dos archivos diferentes y no toma sus direcciones ni los dene como extern, el compilador C++ ideal no asignar almacenamiento para la constante, simplemente la expandir en el cdigo. Como las constantes tienen implcito el mbito a su archivo, puede ponerlas en un archivo de cabecera de C++ sin que origine conictos en el enlace. Dado que las constante en C++ utilizan por defecto enlace interno, no puede denir una constante en un archivo y utilizarla desde otro. Para conseguir enlace externo para la constante y as poder usarla desde otro archivo, debe denirla explicitamente como extern, algo as:
extern const int x = 1; // definicin, no declaracin
Sealar que dado un identicador, si se dice que es extern, se fuerza el almacenamiento para la constante (aunque el compilador tenga la opcin de hacer la expansin en ese punto). La inicializacin establece que la sentencia es una denicin, no una declaracin. La declaracin:
extern const int x;
en C++ signica que la denicin existe en algn sitio (mientras que en C no tiene porque ocurrir asi). Ahora puede ver porqu C++ requiere que las deniciones de constantes incluyan la inicializacin: la inicializacin diferencia una declaracin de una denicin (en C siempre es una denicin, aunque no est inicializada). Con una declaracin const extern, el compilador no hace expansin de la constante porque no conoce su valor. La aproximacin de C a las constantes es poco til, y si quiere usar un valor simblico en una expresin constante (que deba evaluarse en tiempo de compilacin) casi est forzado a usar #define. 214
8.2. Punteros
8.2. Punteros
Los punteros pueden ser const. El compilador pondr ms esfueszo an para evitar el almacenamiento y hacer expansin de constantes cuando se trata de punteros constantes, pero estas caractersticas parecen menos tiles en este caso. Lo ms importante es que el compilador le avisar si intenta cambiar un puntero constante, lo que respresenta un buen elemento de seguridad. Cuando se usa const con punteros tiene dos opciones, se pueden aplicar a lo que apunta el puntero o a la propia direccin almacenada en el puntero, la sintaxis es un poco confusa al principio pero se vuelve cmodo con la prctica.
Empezando por el identicador, se lee u es un puntero, que apunta a un entero constante. En este caso no se requiere inicializacin porque est diciendo que u puede apuntar a cualquier cosa (es decir, no es constante), pero la cosa a la que apunta no puede cambiar. Podra pensar que hacer el puntero inalterable en si mismo, es decir, impedir cualquier cambio en la direccin que contiene u, es tan simple como mover la palabra const al otro lado de la palabra int:
int const* v;
y pensar que esto debera leerse v es un puntero constante a un entero. La forma de leerlo es v es un puntero ordinario a un entero que es constante. Es decir, la palabra const se reere de nuevo al entero y el efecto es el mismo que en la denicin previa. El hecho de que estas deniciones sean equivalentes es confuso, para evitar esta confusin por parte del lector del cdigo, debera ceirse a la primera forma.
Ahora, se lee w es un puntero constate, y apunta a un entero. Como el puntero en si es ahora una constante, el compilador requiere que se le d un valor inicial que no podr alterarse durante la vida del puntero. En cualquier caso, puede cambiar el valor de lo que apunta el puntero con algo como:
*w = 2;
Tambin puede hacer un puntero constante a un elemento constante usando una de las formas siguientes:
int d = 1; const int* const x = &d; // (1) int const* const x2 = &d; // (2)
Captulo 8. Constantes Algunos argumentan que la segunda forma ms consistente porque el const se coloca siempre a la derecha de lo que afecta. Debe decidir que forma resulta ms clara para su estilo de codicacin particular. Algunas lneas de un archivo susceptible de ser compilado.
//: C08:ConstPointers.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt const int* u; int const* v; int d = 1; int* const w = &d; const int* const x = &d; // (1) int const* const x2 = &d; // (2) int main() {}
Formato
Este libro sigue la norma de poner slo una denicin de puntero por lnea, e inicializar cada puntero en el punto de denicin siempre que sea posible. Por eso, el estilo de formato es colocar el * del lado del tipo de dato:
int* u = &i;
como si int* fuese un tipo de dato directo. Esto hace que el cdigo sea ms fcil de leer, pero desafortunadamente, esta no es la forma en que funciona. El * se reere al identicador no al tipo. Se puede colocar en cualquier sitio entre el nombre del tipo y el identicador. De modo que puede hacer esto:
int * v = &i, v = 0;
donde se crea un int* u y despus un [FIXME]int v[/FIXME]. Como a los lectores esto les puede parecer confuso, es mejor utilizar el estilo mostrado en este libro.
216
8.3. Argumentos de funciones y valores de retorno Aunque C++ ayuda a evitar errores, no le protege de si mismo si se empea en romper los mecanisnmos de seguridad.
Literales de cadena
C++ no es estricto con los literales en lo referente a constantes. Puede escribir:
char * cp = "howdy";
y el compilador lo aceptar sin objeccin. Tcnicamente esto supone un error porque el literal de cadena (howdy en este caso) se crea por el compilador como un vector de caracteres constante, y el resultado del vector de caracteres entrecomillado es la direccin de memoria del primer elemento. Si se modica uno de los caracteres del vector en tiempo de ejecucin es un error, aunque no todos los compiladores lo imponen correctamente. As que los literales de cadena son arrays de caracteres constantes. Por supuesto, el compilador le permite tratarlos como no constantes porque existe mucho cdigo C que depende de ello. De todas formas, si intenta cambiar los valores de un literal, el resultado no est denido, y probablemente funcione en muchos computadores. Si quiere poder modicar una cadena, debe ponerla en un vector:
char cp[] = "howdy";
Como los compiladores a menudo no imponen la diferecia no tiene porque recordar que debe usar la ltima forma.
pero, qu signica esto? Est impidiendo que el valor de la variable original pueda ser cambiado en la funcin f1(). De todos formas, como el argumento se pasa por valor, es sabido que inmediatamente se hace una copia de la variable original, as que dicha restriccin se cumple implicitamente sin necesidad de usar el especicador const. Dentro de la funcin, const si toma un signicado: El argumento no se puede cambiar. As que, en realidad, es una herramienta para el programador de la funcin, no para el que la usa. Para impedir la confusin del usuario de la funcin, puede hacer que el argumento sea constante dentro de la funcin en lugar de en la lista de argumentos. Podra hacerlo con un puntero, pero la sintaxis ms adecuada para lograrlo es la referencia, algo que se tratar en profuncidad en el capitulo 11[FIXME:XREF]. Brevemente, una referencia es como un puntero constante que se dereferencia automaticamente, as que es como tener un alias de la variable. Para crear una referencia, debe usar el simbolo & en la denicin. De ese modo se tiene una denicin libre de confusiones.
217
Captulo 8. Constantes
void f2(int ic) { const int &i = ic; i++; // ilegal (error de compilacin) }
De nuevo, aparece un mensaje de error, pero esta vez el especicador const no forma parte de la cabecera de la funcin, solo tiene sentido en la implementacin de la funcin y por la tanto es invisible para el cliente.
est diciendo que el valor de la variable original (en el mbito de la funcin) no se modicar. Y de nuevo, como lo est devolviendo por valor, es la copia lo que se retorna, de modo que el valor original nunca se podr modicar. En principio, esto puede hacer suponer que el especicador const tiene poco signicado. Puede ver la aparente falta de sentido de devolver constantes por valor en este ejemplo:
//: C08:Constval.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Returning consts by value // has no meaning for built-in types int f3() { return 1; } const int f4() { return 1; } int main() { const int j = f3(); // Works fine int k = f4(); // But this works fine too! }
Para los tipos bsicos, no importa si el retorno es constante, as que debera evitar la confusin para el programador cliente y no utilizar const cuando se devuelven variables de tipos bsicos por valor. Devolver por valor como constante se vuelve importante cuando se trata con tipos denidos por el programador. Si una funcin devuelve un objeto por valor como constante, el valor de retorno de la funcin no puede ser un recipiente (FIXME: NOTA DEL TRADUCTOR) (es decir, no se puede modicar ni asignarle nada). Por ejemplo:
//: C08:ConstReturnValues.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constant return by value // Result cannot be used as an lvalue class X { int i; public: X(int ii = 0); void modify();
218
f5() devuelve un objeto de clase X no constante, mientras que f6() devuelve un objeto de clase X pero constante. Solo el valor de retorno por valor no constante se puede usar como recipiente. Por eso, es importante usar const cuando se devuelve un objeto por valor si quiere impedir que se use como valor izquierdo. La razn por la que const no tiene sentido cuando se usa para devolver por valor variables de tipos del lenguaje es que el compilador impide el uso como recipiente de dichos tipos, ya que devuelve un valor, no una variable. Solo cuando se devuelven objetos por valor de tipos denidos por el programador esta funcionalidad adquiere sentido. La funcin f7() toma como argumento una referencia no constante (la referencia es una forma adicional para manejar direcciones en C++ y se trata en el [FIXME:capitulo 11]). Es parecido a tomar un puntero no constante, aunque la sintaxis es diferente. La razn por la que no compila es por la creacin de un temporario.
Temporarios
A veces, durante la evaluacin de una expresin, el compilador debe crear objetos temporales (temporarios). Son objetos como cualquier otro: requieren espacio de almacenamiento y se deben construir y destruir. La diferencia es que nunca se ven, el compilador es el responsable de decidir si se necesitan y los detalles de su existencia. Una particularidad importante de los temporarios es que siempre son constantes. Como normalmente no manejar objetos temporarios, hacer algo que cambie un temporario es casi seguro un error porque no ser capaz de usar esa informacin. Para evitar esto, el compilador crea todos los temporarios como objetos constantes, de modo que le avisar si intenta modicarlos. En el ejemplo anterior, f5() devuelve un objeto no constante. Pero en la expresin
f7(f5());
el compilador debe crear un temporario para albergar el valor de retorno de f5() para que pueda ser pasado a f7(). Esto funcionaria bien si f7() tomara su argumento por valor; entonces el temporario se copiara en f7() y eso no puede pasarle al temporario X. Sin embargo, f7() toma su argumento por referencia, lo que signica que toma la direccin del temporario X. Como f7() no toma su argumento por referencia constante, tiene permiso para modicar el objeto temporario. 219
Captulo 8. Constantes Pero el compilador sabe que el temporario desaparecer en cuanto se complete la evaluacin de la expresin, y por eso cualquier modicacin hechaen el temporario se perder. Haciendo que los objetos temporarios sean constantes automaticamente, la situacin causa un error de compilacin de modo que evitar cometer un error muy dicil de localizar. En cuaquier caso, tenga presente que las expresiones siguientes son correctas.
f5() = X(1); f5().modify();
Aunque son aceptables para el compilador, en realidad son problemticas. f5() devuelve un objeto de clase X, y para que el compilador pueda satisfacer las expresiones anteriores debe crear un temporario para albergar el valor de retorno. De modo que en ambas expresiones el objeto temporario se modica y tan pronto como la expresin es evaluada el temporario se elimina. Como resultado, las modicaciones se pierden, as que probablemente este cdigo es erroneo, aunque el compilador no diga nada al respecto. Las expresiones como estas son sucientemente simples como para detectar el problema, pero cuando las cosas son ms complejas los errores son ms diciles de localizar. La forma de preservar la constancia de los objetos se muestra ms adelante en este capitulo.
220
La funcin t() toma un puntero no-constante ordinario como argumento, y u() toma un puntero constante. En el cuerpo de u() puede ver un intento de modicar el valor de un puntero constante, algo incorrecto, pero puede copiar su valor en una variable con constante. El compilador tambin impide crear un puntero no constante y almacenar en l la direccin contenida en un puntero constante. Las funciones v() y w() prueban las semnticas de retorno de valores. v() devuelve un const char* que se crea a partir de un literal de cadena. Esta sentencia en realidad genera la direccin del literal, una vez que el compilador lo crea y almacena en rea de almacenamiento esttica. Como se ha dicho antes, tcnicamente este vector de caracteres es una constante, como bien indica el tipo de retorno de v(). El valor de retorno de w() requiere que tanto el puntero como lo que apunta sean constantes. Como en v(), el valor devuelto por w() es valido una vez terminada la funcin solo porque es esttico. Nunca debe devolver un puntero a una variable local pues se almacenan en la pila y al terminar la funcin los datos de la pila desaparecen. Lo que si puede hacer es devolver punteros que apuntan a datos almacenados en el montn (heap), pues siguen siendo validos despus de terminar la funcin. En main() se prueban las funciones con varios argumentos. Puede ver que t() aceptar como argumento un puntero ordinario, pero si intenta pasarle un puntero a una constante, no hay garantia de que no vaya a modicarse el valor de la variable apuntada; por ello el compilador lo indica con un mensaje de error. u() toma un puntero a constante, as que puede aceptar los dos tipos de argumentos. Por eso una funcin que acepta un puntero a constante es ms general que una que acepta un puntero ordinario. Como es lgico, el valor de retorno de v() slo se puede asignar a un puntero a constante. Tambin era de esperar que el compilador reuse asignar el valor devuelto por w() a un puntero ordinario, y que s acepte un const int* const, pero podra sorprender un poco que tambin acepta un const int*, que no es exactamente el tipo de retorno declarado en la funcin. De nuevo, como el valor (que es la direccin contenida en el puntero) se copia, el requisito de que la variable original permanezca inalterable se cumple automticamente. Por eso, el segundo const en la declaracin const int* const slo tiene sentido cuando lo use como recipiente, en cuyo caso el compilador lo impedira.
221
Captulo 8. Constantes
// // // // Available at http://www.BruceEckel.com (c) Bruce Eckel 2000 Copyright notice in Copyright.txt Temporaries are const
class X {}; X f() { return X(); } // Return by value void g1(X&) {} // Pass by non-const reference void g2(const X&) {} // Pass by const reference int main() { // Error: const temporary created by f(): //! g1(f()); // OK: g2 takes a const reference: g2(f()); }
Algunos autores dicen que todo en C se pasa por valor, ya que cuando se pasa un puntero se hace tambin una copia (de modo que el puntero se pasa por valor). En cualquier caso, hacer esta precisin puede, en realidad, confundir la cuestin. f() retorna un objeto de la clase X por valor. Esto signica que cuando tome el valor de retorno y lo pase inmediatamente a otra funcin como en las llamadas a g1() y g2(), se crea un temporario y los temporarios son siempre constantes. Por eso, la llamada a g1() es un error pues g1() no acepta una referencia constante, mientras que la llamada a g2() si es correcta.
8.4. Clases
Esta seccin muesta la forma en la que se puede usar el especcador const con las clases. Puede ser interesante crear una constante local a una clase para usarla en expresiones constantes que sern evaluadas en tiempo de compilacin. Sin embargo, el signicado del especicador const es diferente para las clases 1 , de modo que debe comprender la opciones adecuadas para crear miembros constantes en una clase. Tambin se puede hacer que un objeto completo sea constante (y como se ha visto, el compilador siempre hace constantes los objetos temporarios). Pero preservar la consistencia de un objeto constante es ms complicado. El compilador puede asegurar la consistencia de las variables de los tipos del lenguaje pero no puede vigilar la complejidad de una clase. Para garantizar dicha consistencia se emplean las funciones miembro constantes; que son las nicas que un objeto constante puede invocar.
222
8.4. Clases hasta algn punto posterior en el constructor, lo que signicaria que la constante no tendra valor por un momento. Y nada impedira cambiar el valor de la constante en varios sitios del constructor.
El aspecto de la lista de inicializacin del constructor mostrada arriba puede crear confucin al principio porque no es usual tratar los tipos del lenguaje como si tuvieran constructores.
223
Captulo 8. Constantes
B::B(int ii) : i(ii) {} void B::print() { cout << i << endl; } int main() { B a(1), b(2); float pi(3.14159); a.print(); b.print(); cout << pi << endl; }
Esto es especialmente crtico cuando se inicializan atributos constantes porque se deben inicializar antes de entrar en el cuerpo de la funcin. Tiene sentido extender este constructor para los tipos del lenguaje (que simplemente signican asignacin) al caso general que es por lo que la denicin oat funciona en el cdigo anterior. A menudo es til encapsular un tipo del lenguaje en una clase para garantizar la inicializacin con el constructor. Por ejemplo, aqu hay una clase entero:
//: C08:EncapsulatingTypes.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class Integer { int i; public: Integer(int ii = 0); void print(); }; Integer::Integer(int ii) : i(ii) {} void Integer::print() { cout << i << ; } int main() { Integer i[100]; for(int j = 0; j < 100; j++) i[j].print(); }
El vector de enteros declarado en main() se inicializa automaticamente a cero. Esta inicializacin no es necesariamente ms costosa que un bucle for o memset(). Muchos compiladores lo optimizan fcilmente como un proceso muy rpido.
8.4. Clases A continuacin aparece un ejemplo que muestra la creacin y uso de una static const llamada size en una clase que representa una pila de punteros a cadenas.2
//: C08:StringStack.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Using static const to create a // compile-time constant inside a class #include <string> #include <iostream> using namespace std; class StringStack { static const int size = 100; const string* stack[size]; int index; public: StringStack(); void push(const string* s); const string* pop(); }; StringStack::StringStack() : index(0) { memset(stack, 0, size * sizeof(string*)); } void StringStack::push(const string* s) { if(index < size) stack[index++] = s; } const string* StringStack::pop() { if(index > 0) { const string* rv = stack[--index]; stack[index] = 0; return rv; } return 0; } string iceCream[] = { "pralines & cream", "fudge ripple", "jamocha almond fudge", "wild mountain blackberry", "raspberry sorbet", "lemon swirl", "rocky road", "deep chocolate fudge" }; const int iCsz = sizeof iceCream / sizeof *iceCream; int main() { StringStack ss; for(int i = 0; i < iCsz; i++) ss.push(&iceCream[i]); const string* cp; while((cp = ss.pop()) != 0) cout << *cp << endl;
2
225
Captulo 8. Constantes
}
Como size se usa para determinar el tamao del vector stack, es adecuado usar una constante en tiempo de compilacin, pero que queda oculta dentro de la clase. Conste que push() toma un const string* como argumento, pop() retorna un const string* y StringStack contiene const string*. Si no fuera as, no podra usar una StringStack para contener los punteros de icecream. En cualquier caso, tambin impide hacer algo que cambie los objetos contenidos en StringStack. Por supuesto, todos los contenedores no estn diseados con esta restriccin.
Este uso de enum garantiza que no se ocupa almacenamiento en el objeto, y que todos los smbolos denidos en la enumeracin se evaluan en tiempo de compilacin. Adems se puede establecer explcitamente el valor de los smbolos:
enum { one = 1, two = 2, three };
utilizando el tipo enum, el compilador continuar contando a partir del ltimo valor, as que el smbolo three tendr un valor 3. En el ejemplo StringStack anterior, la lnea:
static const int size = 100;
Aunque es fcil ver esta tcnica en cdigo correcto, el uso de static const fue aadido al lenguaje precisamente para resolver este problema. En todo caso, no existe ninguna razn abrumadora por la que deba usar 226
8.4. Clases static const en lugar de enum, y en este libro se utiliza enum porque hay ms compiladores que le dan soporte en el momento en el momento en que se escribi este libro.
Aqu, b es un objeto constante de tipo blob, su constructor se llama con un 2 como argumento. Para que el compilador imponga que el objeto sea constante, debe asegurar que el objeto no tiene atributos que vayan a cambiar durante el tiempo de vida del objeto. Puede asegurar fcilmente que los atributos no pblicos no sean modicables, pero. Cmo puede saber que mtodos cambiarn los atributos y cuales son seguros para un objeto constante? Si declara un mtodo como constante, le est diciendo que la funcin puede ser invocada por un objeto constante. Un mtodo que no se declara constante se trata como uno que puede modicar los atributos del objeto, y el compilador no permitir que un objeto constante lo utilice. Pero la cosa no acaba ah. Slo porque una funcin arme ser const no garantiza que actuar del modo correcto, de modo que el compilador fuerza que en la denicin del mtodo se reitere el especicador const (la palabra const se convierte en parte del nombre de la funcin, as que tanto el compilador como el enlazador comprobarn que no se viole la [FIXME:constancia]). De este modo, si durante la denicin de la funcin se modica algn miembro o se llama algn mtodo no constante, el compilador emitir un mensaje de error. Por eso, est garantizado que los miembros que declare const se comportarn del modo esperado. Para comprender la sintaxis para declarar mtodos constantes, primero debe recordar que colocar const delante de la declaracin del mtodo indica que el valor de retorno es constante, as que no produce el efecto deseado. Lo que hay que hacer es colocar el especicador const despus de la lista de argumentos. Por ejemplo:
//: C08:ConstMember.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class X { int i; public: X(int ii); int f() const; }; X::X(int ii) : i(ii) {} int X::f() const { return i; } int main() { X x1(10); const X x2(20); x1.f(); x2.f(); }
La palabra const debe incluirse tando en la declaracin como en la denicin del mtodo o de otro modo el compilador asumir que es un mtodo diferente. Como f() es un mtodo constante, si intenta modicar i de alguna forma o llamar a otro mtodo que no sea constante, el compilador informar de un error. Puede ver que un miembro constante puede llamarse tanto desde objetos constantes como desde no constantes 227
Captulo 8. Constantes de forma segura. Por ello, debe saber que esa es la forma ms general para un mtodo (a causa de esto, el hecho de que los mtodos no sean const por defecto resulta desafortunado). Un mtodo que no modica ningn atributo se debera escribir como constante y as se podra usar desde objetos constantes. Aqu se muestra un ejemplo que compara mtodos const y mtodos ordinarios:
//: C08:Quoter.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Random quote selection #include <iostream> #include <cstdlib> // Random number generator #include <ctime> // To seed random generator using namespace std; class Quoter { int lastquote; public: Quoter(); int lastQuote() const; const char* quote(); }; Quoter::Quoter(){ lastquote = -1; srand(time(0)); // Seed random number generator } int Quoter::lastQuote() const { return lastquote; } const char* Quoter::quote() { static const char* quotes[] = { "Are we having fun yet?", "Doctors always know best", "Is it ... Atomic?", "Fear is obscene", "There is no scientific evidence " "to support the idea " "that life is serious", "Things that make us happy, make us wise", }; const int qsize = sizeof quotes/sizeof *quotes; int qnum = rand() % qsize; while(lastquote >= 0 && qnum == lastquote) qnum = rand() % qsize; return quotes[lastquote = qnum]; } int main() { Quoter q; const Quoter cq; cq.lastQuote(); // OK //! cq.quote(); // Not OK; non const function for(int i = 0; i < 20; i++) cout << q.quote() << endl; }
Ni los constructores ni los destructores pueden ser mtodos constantes porque prcticamente siempre realizarn alguna modicacin en el objeto durante la inicializacin o la terminacin. El miembro quote() tampoco puede 228
8.4. Clases ser constante porque modica el atributo lastquote (ver la sentencia de retorno). Por otra parte lastQuote() no hace modicaciones y por eso puede ser const y puede ser llamado de forma segura por el objeto constante cq.
Esta aproximacin funciona y puede verse en cdigo correcto, pero no es la tcnica ideal. El problema es que esta falta de constancia est oculta en la denicin de un mtodo y no hay ningn indicio en la interface de la clase que haga sospechar que ese dato se modica a menos que puede accederse al cdigo fuente (buscando el molde). Para poner todo al descubierto se debe usar la palabra mutable en la declaracin de la clase para indicar que un atributo determinado se puede cambiar an perteneciendo a un objeto constante.
//: C08:Mutable.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The "mutable" keyword
229
Captulo 8. Constantes
class Z { int i; mutable int j; public: Z(); void f() const; }; Z::Z() : i(0), j(0) {} void Z::f() const { //! i++; // Error -- const member function j++; // OK: mutable } int main() { const Z zz; zz.f(); // Actually changes it! }
De este modo el usuario de la clase puede ver en la declaracin que miembros tienen posibilidad de ser modicados por un mtodo. Si un objeto se dene como constante es un candidato para ser almacenado en memoriar de slo lectura (ROM), que a menudo es una consideracin importante en programacin de sistemas empotrados. Para conseguirlo no es suciente con que el objeto sea constante, los requisitos son mucha ms estrictos. Por supuesto, el objeto debe ser constante bitwise. Eso es fcil de comprobar si la constancia lgica se implementa mediante el uso de mutable, pero probablemente el compilador no podr detectarlo si se utiliza la tcnica del moldeado dentro de un mtodo constante. En resumen: La clase o estructura no puede tener constructores o destructor denidos por el usuario. No pueden ser clases base (tratado en el capitulo 14) u objetos miembro con constructores o destructor denidos por el usuario. El efecto de una operacin de escritura en una parte del objeto constante de un tipo ROMable no est denido. Aunque un objeto pueda ser colocado en ROM de forma conveniente, no todos lo requieren.
8.5. Volatile
La sintaxis de volatile es idntica a la de const, pero volatile signica Este dato puede cambiar sin que el compilador sea informado de ello. De algn modo, el entorno modica el dato (posiblemente mediante multitarea, multihilo o interrupciones), y volatile indica la compilador que no haga suposiciones sobre el dato, especialmente durante la optimizacin. Si el compilador dice Yo guard este dato en un registro anteriormente, y no he tocado ese registro, normalmente no necesitar leer el dato de nuevo desde memoria. Pero si es volatile, el compilador no debe hacer esa suposicin porque el dato puede haber cambiado a causa de otro proceso, y debe releer el dato en vez de optimizar el cdigo (dicha optimizacin consiste en eliminar la lectura redundante que se hace normalmente). Pueden crearse objetos volatile usando la misma sintaxis que se usa para crear objetos constantes. Tambin puede crearse objetos volatile constantes que no pueden cambiarse por el programador cliente pero se pueden modicar por una entidad exterior al programa. Aqu se muestra un ejemplo que representa una clase asociada con algn elemento fsico de comunicacin.
//: C08:Volatile.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt
230
8.6. Resumen
// The volatile keyword class Comm { const volatile unsigned char byte; volatile unsigned char flag; enum { bufsize = 100 }; unsigned char buf[bufsize]; int index; public: Comm(); void isr() volatile; char read(int index) const; }; Comm::Comm() : index(0), byte(0), flag(0) {} // Only a demo; wont actually work // as an interrupt service routine: void Comm::isr() volatile { flag = 0; buf[index++] = byte; // Wrap to beginning of buffer: if(index >= bufsize) index = 0; } char Comm::read(int index) const { if(index < 0 || index >= bufsize) return 0; return buf[index]; } int main() { volatile Comm Port; Port.isr(); // OK //! Port.read(0); // Error, read() not volatile }
Como ocurre con const, se puede usar volatile para los atributos de la clase, los mtodos y para los objetos en si mismos. Slo puede llamar a mtodos volatile desde objetos volatile. La razn por la que isr() no se puede usar como una rutina de servicio de interrupcin (ISR) es que en un mtodo, la direccin del objeto actual (this) debe pasarse secretamente, y una ISR no requiere argumentos. Para resolver este problema se puede hace que el mtodo isr() sea un mtodo de clase (static), un asunto que se trata en el [FIXME:capitulo 10]. La sintaxis de volatile es idntica a la de const, as que por eso se suelen tratar juntos. Cuando se usan combinados se conocen como cuanticador c-v (const-volatile).
8.6. Resumen
La palabra const permite la posibilidad de denir objetos, argumentos de funciones, valores de retorno y mtodos y elimina el uso de constantes simblicas para la sustitucin de valores del preprocesador sin perder sus ventajas. Todo ello ofrece una forma adicional de comprobacin de tipos y seguridad en la programacin. El uso de la llamada constancia exacta (const correctness) es decir, el uso de const en todo lugar donde sea posible, puede ser un salvavidas para muchos proyectos. Aunque ignore a const y continue usando el estilo tradicional de C, const existe para ayudarle. El [FIXME:capitulo 11] utiliza las referencias extensamente, y se ver ms sobre la importancia del uso de const con los argumentos de funciones. 231
Captulo 8. Constantes
8.7. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Crear 3 valores enteros constantes, despus sumarlos todos para producir un valor que determine el tamao en la denicin de un vector. Intentar compilar el mismo cdigo en C y ver que sucede (generalmente se puede forzar al compilador de C++ para que funcione como un compilador de C utilizando alguna opcin de linea de comandos) 2. Probar que los compiladores de C y C++ realmente tratan las constantes de modo diferente. Crear una constante global y usarla en una expresin global constante, compilar dicho cdigo en C y C++. 3. Crear deniciones constantes para todos los tipos del lenguaje y sus variantes. Usarlos en expresiones con otras constantes para hacer deniciones de constantes nuevas. Comprobar que compilan correctamente. 4. Crear una denicin de constante en un archivo de cabecera, incluir dicho archivo en dos archivos .cpp, compilarlos y enlazarlos con el compilador de C++. No deberian ocurrir errores. Ahora intentar el mismo experimento con el compilador de C. 5. Crear una constante cuyo valor se determine en tiempo de ejecucin leyendo la hora en que comienza la ejecucin del programa (se puede usar <ctime>). Despus, en el programa, intentar leer un segundo valor de hora, alamacenarlo en la constante y ver que sucede. 6. Crear un vector de caracteres constante, despus intentar cambiar uno de los caracteres. 7. Crear una declaracin de constante extern en un chero y poner un main() en el que se imprima el valor de dicha constante. Crear una denicin de constante extern en un segundo chero, compilar y enlazar los dos cheros. 8. Denir dos punteros a const long utilizando las dos formas de denicin. Apuntar con uno de ellos a un vector de long. Demostrar que se puede incrementar o decrementar el puntero, pero no se puede cambiar el valor de lo que apunta. 9. Denir un puntero constante a double, y apuntar con l a un vector de double. Demostrar que se puede cambiar lo que apunta el puntero pero no se puede incrementar ni decrementar el puntero. 10. Denir un puntero constante a objeto constante. Probar que solamente se puede leer el valor de lo que apunta el puntero, pero no se puede cambiar el puntero ni lo que apunta. 11. Eliminar el comentario de la linea erronea en PointerAssignemt.cpp para ver que mensaje de error muestra el compilador. 12. Crear un literal de cadena y un puntero que apunte al comienzo del literal. Ahora, usar el puntero para modicar los elementos del vector, Informa el compilador de algn error? Debera? Si no lo hace, Porqu piensa que puede ser? 13. Crear una funcin que tome un argumento por valor como constante, despus intentar cambiar el argumento en el cuerpo de la funcin. 14. Crear una funcin que tome un oat por valor. Dentro de la funcin vincular el argumento a un const oat& y usar dicha referencia para asegurar que el argumento no se modique. 15. Modicar ConstReturnValues.cpp eliminando los comentarios en las lneas erroneas una cada vez para ver que mensajes de error muestra el compilador. 16. Modicar ConsPointer.cpp eliminando los comentarios en las lneas erroneas para ver que mensajes de error muestra el compilador. 17. Hacer una nueva versin de ConstPointer.cpp llamada ConstReference.cpp que demuestre el funcionamiento con referencias en lugar de con punteros. (quiz necesite consultar el [FIXME:captulo 11]). 18. Modicar ConstTemporary.cpp eliminando el comentario en la lnea erronea para ver el mensaje de error que muestra el compilador. 232
8.7. Ejercicios 19. Crear una clase que contenga un oat constante y otro no constante. Inicializarlos usando la lista de inicializacin del constructor. 20. Crear una clase llamada MyString que contenga una cadena y tenga un constructor que inicialice la cadena y un mtodo print(). Modicar StringStack.cpp para que maneje objetos MyString y main() para que los imprima. 21. Crear una clase que contenga un atributo constante que se inicialice en la lista de inicializacin del constructor y una enumeracin no etiquetada que se use para determinar el tamao de un vector. 22. Eliminar el especicador const en la denicin del mtodo de ConstMember.cpp, pero dejar el de la declaracin para ver que mensaje de error muestra el compilador. 23. Crear una clase con un mtodo constante y otro ordinario. Crear un objeto constante y otro no constante de esa clase e intentar invocar ambos mtodos desde ambos objetos. 24. Crear una clase con un mtodo constante y otro ordinario. Interntar llamar al mtodo ordinario esde el mtodo constante para ver que mensaje de error muestra el compilador. 25. Eliminar el comentario de la lnea erronea en mutable.cpp para ver el mensaje de error que muestra el compilador. 26. Modicar Quoter.cpp haciendo que quote() sea un mtodo constante y lastquote sea mutable. 27. Crear una clase con un atributo volatile. Crear mtodos volatile y no volatile que modiquen el atributo volatile y ver que dice el compilador. Crear objetos volatile y no volatile de esa clase e intentar llamar a ambos mtodos para comprobar si funciona correctamente y ver que mensajes de error muestra el compilador en caso contrario. 28. Crear una clase llamada bird que pueda ejecutar fly() y una clase rock que no pueda. Crear un objeto rock, tomar su direccin y asignar a un void*. Ahora tomar el void*, asignarlo a un bird* (debe usar un molde) y llamar a fly() a travs de dicho puntero. Esto es posible porque la caracteristica de C que permite asignar a un void* (sin un molde) es un agujero del lenguaje, que no debera propagarse a C++?
233
9: Funciones inline
Una de las caractersticas ms importantes que C++ hereda de C es la eciencia. Si la eciencia de C++ fuese dramticamente menor que la de C, podra haber un contingente signicativo de programadores que no podran justicar su uso.
En C, una de las maneras de preservar la eciencia es mediante el uso de macros, lo que permite hacer lo que parece una llamada a una funcin sin la cabecera de llamada a la funcin normal. La macro est implementada con el preprocesador en vez del propio compilador, y el preprocesador reemplaza todas las llamadas a macros directamente con el cdigo de la macro, de manera que no hay que complicarse pasando argumentos, escribiendo cdigo de ensamblador para CALL, retornando argumentos ni implementando cdigo ensamblador para el RETURN. Todo el trabajo est implementado por el preprocesador, de manera que se tiene la coherencia y legibilidad de una llamada a una funcin pero sin ningn coste por ello. Hay dos problemas respecto del uso del preprocesador con macros en C++. La primera tambin existe en C: una macro parece una llamada a funcin, pero no siempre acta como tal. Esto puede acarrear dicultades para encontrar errores. El segundo problema es especco de C++: el preprocesador no tiene permisos para acceder a la informacin de los miembros de una clase. Esto signica que las macros de preprocesador no pueden usarse como mtodos de una clase. Para mantener la eciencia del uso del preprocesador con macros pero aadiendo la seguridad y la semntica de mbito de verdaderas funciones en las clases. C++ tiene las funciones inline. En este captulo, veremos los problemas del uso de las maros de preprocesador en C++, como se resuelven estos problemas con funciones inline, y las directrices e incursiones en la forma en que trabajan las funciones inline.
El problema ocurre a causa del espacio entre F y su parntesis de apertura en la denicin de la macro. Cuando el espacio es eliminado en el cdigo de la macro, de hecho puedes llamar a la funcin con el espacio incluido.
235
F (1)
El ejemplo anterior es un poco trivial y el problema es demasiado evidente. Las dicultades reales ocurren cuando se usan expresiones como argumentos en llamadas a macros. Hay dos problemas. El primero es que las expresiones se expandiran dentro de la macro de modo que la evaluacin resultante es diferente a lo que se espera. Por ejemplo:
#define FLOOR(x,b) x> b?0:1
La macro se expandira a:
if (a & 0x0f >= 0x07 ? 0 : 1)
La precedencia del & es menor que la del >=, de modo que la evaluacin de la macro te sorprender. Una vez hayas descubierto el problema, puedes solucionarlo insertando parntesis a todo lo que hay dentro de la denicin de la macro. (Este es un buen metodo a seguir cuando denimos macros de preprocesador), entonces:
#define FLOOR(x,b) ((x) >= (b) ? 0 : 1)
De cualquier manera, descubrir el problema puede ser difcil, y no lo dars con l hasta despus de haber dado por sentado el comportamiento de la macro en si misma. En la versin sin parntesis de la macro anterior, la mayora de las expresiones van a actuar de manera correcta a causa de la precedencia de >=, la cual es menor que la mayora de los operadores como +, /, --, e incluso los operadores de desplazamiento. Por lo que puedes pensar facilmente que funciona con todas las expresiones, incluyendo aquellas que empleen operadores logicos a nivel de bit. El problema anterior puede solucionarse programando cuidadosamente: poner entre parntesis todo lo que est denido dentro de una macro. De todos modos el segundo problema es ms sutil. Al contrario de una funcin normal, cada vez que usas argumentos en una macro, dicho argumento es evaluado. Mientras la macro sea llamada solo con variables corrientes, esta evaluacn es benigna, pero si la evaluacin de un argumento tiene efectos secundarios, entonces los resultados pueden ser inesperados y denitivamente no imitaran el comportamiento de una funcin. Por ejemplo, esta macro determina si un argumento entra dentro de cierto rango:
#define BAND(x) (((x)>5 && (x)>10) ? (x) : 0)
Mientras uses un argumento "ordinario", la macro trabajar de manera bastante similar a una funcin real. Pero en cuanto te relajes y comiences a creer que realmente es una funcin, comenzarn los problemas. Entonces:
//: C09:MacroSideEffects.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt
236
Observa el uso de carcteres en mayscula en el nombre de la macro. Este es un buen recurso ya que le advierte al lector que esto es una macro y no una funcin, entonces si hay algn problema, esto acta como recordatorio. A continuacin se muestra la salida producida por el programa, que no es para nada lo que se esperara de una autntica funcin:
a = 4 BAND(++a)=0 a - 5 a - 5 BAND(++a)=8 a - 8 a = 6 BAND(++a)-9 a = 9 a = / BAND(++a)=10 a = 10 a = 8 BAND(++a)-0 a = 10 a - 9 BAND(++a)=0 a = 11 a = 10 BAND(++a)=0 a = 12
Cuando a es cuatro, slo ocurre la primera parte de la condicin, de modo que la expresin es evaluada slo una vez, y el efecto resultante de la llamada a la macro es que a ser 5, que es lo que se esperara de una llamada a funcin normal en la misma situacin. De todos modos, cuando el numero est dentro del rango, se evalan ambas condiciones, lo que da como resultado un tercer incremento. Una vez que el nmero se sale del rango, ambas condiciones siguen siendo evaluadas de manera que se obtienen dos incrementos. Los efectos colaterales son distintos, dependiendo del argumento. Este no es desde luego el comportamiento que se quiere de una macro que se parece a una llamada a funcin. En este caso, la solucin obviamente es hacer una autentica funcin, lo que de hecho implica la cabecera extra y puede reducir la eciencia si se llama demasiado a esa funcin. Desafortunadamente, el problema no siempre ser tan obvio, y sin saberlo. puedes estar utilizando una librera que contiene funciones y macros juntas, de modo que un problema como este puede esconder errores difciles de encontrar. Por ejemplo, la macro putc() en cstdio puede llegar a evaluar dos veces su segundo argumento. Esto est especicado en el Estndar C. Adems, la implementacin descuidada de toupper() como una macro puede llegar a evaluar el argumento ms de una vez, lo que dar resultados inesperados con
237
toupper(*p++)
. [45]
ni nada que se le acerce. En adicin, no habr ninguna indicacin del objeto al que te estes reriendo. Simplemente no hay ninguna forma de expresar el alcance a una clase en una macro. No habiendo ninguna alternativa diferente a macros de preprocesador, los programadores se sentirn tentados de hacer alguno miembros de datos pblicos por el bien de la eciencia, as exponiendo la implementacin subyacente y previniendo cambios en esa implementacin, tanto como eliminando la proteccin que provee el tipo private.
no tiene ningun otro efecto que declarar la funcin (que puede o no tomar una denicin inline en algn momento). El mejor aproximamiento se da cuando se provee el cuerpo de la funcin inmediatamente despus de declararla:
inline int plusOne(int x) { return ++x; }
Observe que el compilador revisar (como siempre lo hace), el uso apropiado de la lista de argumentos de la funcin y del retorno de valor (haciendo cualquier conversin necesaria), algo que el preprocesador es incapaz de hacer. Adems, si intentas escribir lo anterior como una macro de preprocesador, obtendrs un efecto no deseado. Casi siempre querr poner las funciones inline en un chero de cabecera. Cuando el compilado ve una dencin como esa pone el tipo de la funcin (la rma combinada con el valor de retorno) y el cuerpo de la funcin en su tabla de smbolos. Cuando use la funcin, el compilador se asegura de que la llamada es correcta y el valor de retorno se est usando correctamente, y entonces sustituye el cuerpo de la funcin por la llamada a la funcin, y de ese modo elemina la sobrecarga. El cdigo inline ocupa espacio, pero si la funcin es pequea, realmente ocupar menos espacio que el cdigo generado para una llamada a una funcin ordinaria (colocando los argumentos en la 238
9.2. Funciones inline pila y haciendo el CALL) Una funcin inline en un chero de cabecera tiene un estado especial, dado que debe incluir el chero de cabecera que contiene la funcin y su denicin en cada chero en donde se use la funcin, pero eso no provoca un error de denicin mltiple (sin embargo, la denicin debe ser idntica en todos los sitios en los que se incluya la funcin inline).
Aqu, los dos constructores y la funcin print() son inline por defecto. Notar el hecho de usar funciones inline es transparente en main(), y as debe ser. El comportamiento lgico de una funcin debe ser idntico aunque sea inline (de otro modo su compilador no funciona). La nica diferencia visible es el rendimiento. Por supuesto, la tentacin es usar declaraciones inline en cualquier parte dentro de la case porque ahorran el paso extra de hacer una denicin de mtodo externa. Sin embargo, debe tener presente, que la idea de una inline es dar al compilador mejores oportunidades de optimizacin. Pero si declara inline una funcin grande provocar que el cdigo se duplique all donde se llame la funcin, produciendo cdigo [FIXME:bloat] que anular el benecio de velocidad obtenido (la nica manera de descubrir los efectos del uso de inline en su programa con su compilador es experimentar).
239
//: C09:Access.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Inline access functions class Access { int i; public: int read() const { return i; } void set(int ii) { i = ii; } }; int main() { Access A; A.set(100); int x = A.read(); }
Aqu, el usuario de la clase nunca tiene contacto directo con las variables de estado internas a la clase, y pueden mantenerse como privadas, bajo el control del diseador de la clase. Todo el acceso a los atributos se puede controlar a travs de los mtodos de la interfaz. Adems, el acceso es notablemente eciente. Considere read(), por ejemplo. Sin inline, el cdigo generado para la llamada a read() podra incluir colocarla en la pila y ejecutar la llamada CALL de ensamblador. En la mayora de las arquitecturas, el tamao de ese cdigo sera mayor que el cdigo creado para la variante inline, y el tiempo de ejecucin sera ms largo con toda certeza. Sin las funciones inline, un diseador de clases preocupado por la eciencia estara tentado de hacer que i fuese un atributo pblico, eliminado la sobrecarga y permitiendo al usuario acceder directamente a i. Desde el punto de vista de un diseador, eso resulta desastroso i sera parte de la interfaz pblica, lo cual signica que el diseador de la clase no podr cambiar esto en el futuro. Tendr que cargar con un entero llamado i. Esto es un problema porque despus puede que considere mejor usar un oat en lugar de un int para representar el estado, pero como i es parte de la interfaz pblica, no podr cambiarlo. O puede que necesite realizar algn clculo adicional como parte de la lectura o escritura de i, que no podr hacer si es pblico. Si, en el otro extremo, siempre usa mtodos para leer y cambiar la informacin de estado de un objeto, podr modicar la representacin subyacente del objeto [FIXME: to your hearts content] Adems, el uso de mtodos para controlar atributos le permite aadir cdigo al mtodo para detectar cuando cambia el valor, algo que puede ser muy til durante la depuracin. Si un atributo es pblico, cualquiera puede cambiarlo en cualquier momento sin que el programador lo sepa.
Accesores y mutadores
Hay gente que divide el concepto de funciones de acceso en dos accesores (para leer la informain de estado de un objeto) y mutadores (para cambiar el estado de un objeto). Adems, se puede utilizar la sobrecarga de funciones para tener mtodos accesores y mutadores con el mismo nombre; el modo en que se invoque el mtodo determina si se lee o modica la informacin de estado. As,
//: C09:Rectangle.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Accessors & mutators class Rectangle { int wide, high; public: Rectangle(int w = 0, int h = 0) : wide(w), high(h) {} int width() const { return wide; } // Read
240
El constructor usa la lista de inicializacin (brevemente introducida en el captulo 8 y ampliamente cubierta en el capitulo 14) para inicializar los valores de wide y high (usando el formato de pseudoconstructor para los tipos de datos bsicos). No puede denir mtodos que tengan el mismo nombre que los atributos, de modo que puede que se sienta tentado de distinguirlos con un guin bajo al nal. Sin embargo, los identicadores con guiones bajos nales estn reservados y el programador no debera usarlos. En su lugar, debera usar set y get para indicar que los mtodos son accesores y mutadores.
//: C09:Rectangle2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Accessors & mutators with "get" and "set" class Rectangle { int width, height; public: Rectangle(int w = 0, int h = 0) : width(w), height(h) {} int getWidth() const { return width; } void setWidth(int w) { width = w; } int getHeight() const { return height; } void setHeight(int h) { height = h; } }; int main() { Rectangle r(19, 47); // Change width & height: r.setHeight(2 * r.getWidth()); r.setWidth(2 * r.getHeight()); }
Por supuesto, los accesores y mutadores no tienen que ser simples tuberas hacias las variables internas. A veces, puedes efectuar clculos ms sofsticados. El siguiente ejemplo usa las funciones de tiempo de la librera C estndar para crear una clase Time:
//: C09:Cpptime.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // A simple time class #ifndef CPPTIME_H #define CPPTIME_H #include <ctime> #include <cstring>
241
class Time { std::time_t t; std::tm local; char asciiRep[26]; unsigned char lflag, aflag; void updateLocal() { if(!lflag) { local = *std::localtime(&t); lflag++; } } void updateAscii() { if(!aflag) { updateLocal(); std::strcpy(asciiRep,std::asctime(&local)); aflag++; } } public: Time() { mark(); } void mark() { lflag = aflag = 0; std::time(&t); } const char* ascii() { updateAscii(); return asciiRep; } // Difference in seconds: int delta(Time* dt) const { return int(std::difftime(t, dt->t)); } int daylightSavings() { updateLocal(); return local.tm_isdst; } int dayOfYear() { // Since January 1 updateLocal(); return local.tm_yday; } int dayOfWeek() { // Since Sunday updateLocal(); return local.tm_wday; } int since1900() { // Years since 1900 updateLocal(); return local.tm_year; } int month() { // Since January updateLocal(); return local.tm_mon; } int dayOfMonth() { updateLocal(); return local.tm_mday; } int hour() { // Since midnight, 24-hour clock updateLocal(); return local.tm_hour; } int minute() { updateLocal(); return local.tm_min; }
242
Las funciones de la librera C estndar tienen mltiples representaciones para el tiempo, y todas ellas son parte de la clase Time. Sin embargo, no es necesario actualizar todos ellos, as que time_t se usa para la representacin base, y tm local y la representacin ASCII asciiRep tienen banderas para indicar si han sido actualizadas para el time_t actual. Las dos funciones privadas updateLocal() y updateAscii() comprueban las banderas y condicionalmente hacen la actualizacin. El constructor llama a la funcin mark() (que el usuario puede llamar tambin para forzar al objeto a representar el tiempo actual), y esto limpia las dos banderaas para indicar que el tiempo local y la representacin ASCII son ahora invlidas. La funcin ascii() llama a updateAscii(), que copia el resultado de la funcin de la librera estndar de C asctime() en un buffer local porque asctime() usa una rea de datos esttica que se sobreescribe si la funcin se llama en otra parte. El valor de retorno de la funcin ascii() es la direccin de este buffer local. Todas las funciones que empiezan con daylightSavings() usan la funcin updateLocal(), que causa que la composcin resultante de inlines sea bastante larga. No parece que eso valga la pena, especialmente considerando que probablemente no quiera llamar mucho las funciones. Sin embargo, esto no signica que todas las funciones deban ser no-inline. Is hace otras funciones no-inline, al menos mantenga updateLocal() como inline de modo que su cdigo se duplique en las funciones no-inline, eliminando la sobrecarga extra de invocacin de funciones. Este es un pequeo programa de prueba:
//: C09:Cpptime.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Testing a simple time class #include "Cpptime.h" #include <iostream> using namespace std; int main() { Time start; for(int i = 1; i < 1000; i++) { cout << i << ; if(i%10 == 0) cout << endl; } Time end; cout << endl; cout << "start = " << start.ascii(); cout << "end = " << end.ascii(); cout << "delta = " << end.delta(&start); }
Se crea un objeto Time, se hace alguna actividad que consuma tiempo, despus se crea un segundo objeto Time para marcar el tiempo de nalizacin. Se usan para mostrar los tiempos de inicio, n y los intervalos.
243
//: C09:Stash4.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Inline functions #ifndef STASH4_H #define STASH4_H #include "../require.h" class Stash { int size; // Size of each space int quantity; // Number of storage spaces int next; // Next empty space // Dynamically allocated array of bytes: unsigned char* storage; void inflate(int increase); public: Stash(int sz) : size(sz), quantity(0), next(0), storage(0) {} Stash(int sz, int initQuantity) : size(sz), quantity(0), next(0), storage(0) { inflate(initQuantity); } Stash::~Stash() { if(storage != 0) delete []storage; } int add(void* element); void* fetch(int index) const { require(0 <= index, "Stash::fetch (-)index"); if(index >= next) return 0; // To indicate the end // Produce pointer to desired element: return &(storage[index * size]); } int count() const { return next; } }; #endif // STASH4_H
Obviamente las funciones pequeas funcionan bien como inlines, pero note que las dos funciones ms largas siguen siendo no-inline, dado que convertirlas a inline no representara ninguna ganancia de rendimiento.
//: C09:Stash4.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "Stash4.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; int Stash::add(void* element) { if(next >= quantity) // Enough space left? inflate(increment); // Copy element into storage, // starting at next empty space: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++)
244
Una vez ms, el programa de prueba que verica que todo funciona correctamente.
//: C09:Stash4Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Stash4 #include "Stash4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j) << endl; const int bufsize = 80; Stash stringStash(sizeof(char) * bufsize, 100); ifstream in("Stash4Test.cpp"); assure(in, "Stash4Test.cpp"); string line; while(getline(in, line)) stringStash.add((char*)line.c_str()); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout << "stringStash.fetch(" << k << ") = " << cp << endl; }
Este es el mismo programa de prueba que se us antes, de modo que la salida debera ser bsicamente la misma. La clase Stack incluso hace mejor uso de inlines.
//: C09:Stack4.h
245
Notar que el destructor Link, que se present pero vacio en la versin anterior de Stack, ha sido eliminado. En pop(), la expresin delete oldHead simplemente libera la memoria usada por Link (no destruye el objeto data apuntado por el Link. La mayora de las funciones inline quedan bastante bien y obviamente, especialmente para Link. Incluso pop() parece justicado, aunque siempre que haya sentencias condicionales o variables locales no est claro que las inlines sean beneciosas. Aqu, la funcin es lo sucientemente pequea as que es probable que no haga ningn dao. Si todas sus funciones son inline, usar la librera se convierte en algo bastante simple porque el enlazado es innecesario, como puede ver in el ejemplo de prueba (fjese en que no hay Stack4.cpp.
//: C09:Stack4Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{T} Stack4Test.cpp #include "Stack4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std;
246
La gente escribe veces clases con todas sus funciones inline, as que la clase completa est en el chero de cabecera (ver en este libro que yo mismo lo hago). Durante el desarrollo de un programa probablemente esto es inofensivo, aunque a veces puede hacer que las compilaciones sean ms lentas. Cuando el programa se estabiliza un poco, probablemente querr volver a hacer las funciones no-inline donde sea conveniente.
9.4.1. Limitaciones
Hay dos situaciones en que el compilador no puede efectuar la sustitucin de inline. En estos casos, simplemente convierte la funcin a la forma ordinaria tomando la dencin y pidiendo espacio para la funcin como hace con una funcin no-inline. Si debe hacerlo in varias unidades de traduccin (lo que normalmente causara un error de denicin mltiple), se informa al enlazador que ignore esas deniciones mltiples. En compilador no puede efectuar la sustitucin de inline si la funcin es demasiado complicada. Esto depende de cada compilador particular, [FIXME:pero aunque muchos compiladores lo hagan], no habr ninguna mejora de eciencia. En general, se considera cualquier tipo de bucle demasiado complicado para expandir como una inline, y si lo piensa, el bucle tomar mucho ms tiempo dentro de la funcin que el que conlleva la sobrecarga de la invocacin de la funcin. Si la funcin es simplemente una coleccin se sentencias simples, probablemente el compilador no tendr ningn problema para utilizar inline, pero si hay muchas sentencias, la sobrecarga de llamada ser mucho menor que el coste de ejecutar el cuerpo. Y recuerde, cada vez que llame a una funcin inline grande, el cuerpo completo se inserta en el lugar de la llamada, de modo que el tamao del cdigo se inar fcilmente sin que se perciba ninguna mejora de rendimiento. (Note que algunos de los ejemplos de este libro pueden exceder el 247
Captulo 9. Funciones inline tamao razonable para una inline a cambio de mejorar la esttica de los listados. El compilador tampoco efectua sustituciones inline si la direccin de la funcin se toma implcita o explcitamente. Si el compilador debe producir una direccin, entonces tendr que alojar el cdigo de la funcin y usar la direccin resultante. Sin embargo, cuando no se requiere una direccin, probablemente el compilador har la sustitucin inline. Es importante comprender que una declaracin inline es slo una sugerencia al compilador; el compilador no est forzado a hacer nada. Un buen compilador har sustituciones inline para funciones pequeas y simples mientras que ignorar las que sean demasiado complicadas. Eso le dar lo que espera - la autntica semtica de una llamada a funcin con la eciencia de una macro.
En f(), se realiza una llamada a g(), aunque g() an no ha sido declarada. Esto funciona porque la denicin del lenguaje dice que las funciones inline en una clase no sern evaluadas hasta la llave de cierre de la declaracin de clase. Por supuesto, si g() a su vez llama a f(), tendr un conjunto de llamadas recursivas, que son demasiado complicadas para el compilador pueda hacer inline. (Tambin, tendr que efectuar alguna comprobacin en f() o g() para forzar en alguna de ellas un case base, o la recursin ser innita.)
248
El constructor para Member es sucientemente simple para ser inline, dado que no hay nada especial en l - ninguna herencia u objeto miembro est provocando actividades ocultas adicionales. Pero en la clase WithMembers hay ms de lo que se ve a simple vista. Los constructores y destructores para los atributos q, r y s se llaman automticamente, y esos constructores y destructores tambin son inline, as que la diferencia es signicativa respecto a mtodos normales. Esto no signica necesariamente que los constructores y destructores deban ser no-inline; hay casos en que tiene sentido. Tambin, cuando se est haciendo un prototipo inicial de un programa escribiendo cdigo rpidamente, es conveniente a menudo usar inlines. Pero si est preocupado por la eciencia, es un sitio donde mirar.
249
Ahora si quiere comparar el efecto de la funciones inline con la versin convencional, simplemente borre la palabra inline. (Las funciones inline normalmente deberan aparecen en los cheros de cabecera, no obstante, las funciones no-inline deberan residir en un propia unidad de traduccin.) Si quiere poner las funciones en la documentacin, es tan simple como un copiar y pegar. Las funciones in situ requiere ms trabajo y tiene ms posibilidades de provocar errores. Otro argumento para esta propuesta es que siempre puede producir un estilo de formato consistente para las deniciones de funcin, algo que no siempre ocurre con las funciones in situ.
Esto imprime el valor de cualquier variable. Puede conseguir tambin una traza que imprima las sentencias tal como se ejecutan:
#define TRACE(s) cerr << #s << endl; s
El #s cadeniza la sentencia para la salida, y la segunda s hace que la sentencia se ejecute. Por supuesto, este tipo de cosas puede causar problemas, especialmente bucles for de una nica lnea.
for(int i = 0; i < 100; i++)
250
Como realmente hay dos sentencia en la macro TRACE(), el bucle for de una nica lnea ejecuta solo la primera. La solucin es reemplazar el punto y coma por una coma en la macro.
Cada llamada a la macro FIELD() crea un identicador para una cadena de caracteres y otro para la longitud de dicha cadena. No solo es fcil de leer, tambin puede eleminar errores de codicacin y facilitar el mantenimiento.
251
Los valores por defecto proporcionan mensajes razonables que se pueden cambiar si es necesario. Fjese en que en lugar de usar argumentos char* se utiliza const string&. Esto permite tanto char* y cadenas string como argumentos para estas funciones, y as es ms general (quiz quiera utilizar esta forma en su propio cdigo). En las deniciones para requireMinArgs() y requireMinArgs(), se aade uno al nmero de argumentos que necesita en la lnea de comandos porque argc siempre incluye el nombre del programa que est siendo ejecutado como argumento cero, y as siempre tiene un valor que excede en uno al nmero real de argumentos de la lnea de comandos. Fjese en el uso de declaraciones locales using namespace std con cada funcin. Esto es porque algunos compiladores en el momento de escribir este libro incluyen incorrectamente las funciones de la librera C estndar en el espacio de nombres std, as que la cualicacin explcita podra causar un error en tiempo de compilacin. Las declaraciones locales permiten que require.h funcione tanto con libreras correctas como con incorrectas sin abrir el espacio de nombres std para cualquiera que incluya este chero de cabecera. 252
Podra estar tentado a ir un paso ms all para manejar la apertura de cheros e aadir una macro a require.h.
#define IFOPEN(VAR, NAME) \ ifstream VAR(NAME); \ assure(VAR, NAME);
Lo primero, esto podra parecer atractivo porque signica que hay que escribir menos. No es terriblemente inseguro, pero es un camino que es mejor evitar. Fjese que, de nuevo, una macro parece una funcin pero se comparta diferente; realmente se est creando un objejo in cuyo alcance persiste ms all de la macro. Quiz lo entienda, pero para programadores nuevos y mantenedores de cdigo slo es una cosa ms que ellos deben resolver. C++ es sucientemente complicado sin aadir confusin, as que intente no abusar de las macros del preprocesador siempre que pueda.
9.8. Resumen
Es crtico que sea capaz de ocultar la implementacin subyacente de una clase porque puede querer cambiarla despus. Har estos cambios por eciencia, o para mejorar la legibilidad del problema, o porque hay alguna clase nueva disponible para usar en la implementacin. Cualquier cosa que haga peligrar la privacidad de la implementacin subyacente reduce la exibilidad del lenguaje. Por eso, la funcin inline es muy importante porque prcticamente elimina la necesidad de macros de preprocesador y sus problemas asociados. Con inlines, los mtodos pueden ser tan ecientes como las macros. Las funciones se puede usar con exceso en las deniciones de clase, por supuesto. El programador est tentado de hacerlo porque es fcil, as que ocurre. Sin embargo, no es un problema grave porque despus, cuando se busquen reducciones de tamao, siempre puede cambiar las inline a funciones convencionales dado que no afecta 253
Captulo 9. Funciones inline a su funcionalidad. La pauta debera ser Primero haz el trabajo, despus optimzalo.
9.9. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Escriba un programa que use la macro F() mostrada al principio del captulo y demuestre que no se expande apropiadamente, tal como describe el texto. Arregle la macho y demuestre que funciona correctamente. 2. Escriba un programa que use la macro FLOOR() mostrada al pincipio del captulo. Muestre las condiciones en que no funciona apropiadamente. 3. Modique MacroSideEffects.cpp de modo que BAND() funcione adecuadamente. 4. Cree dos funciones idnticas, f1() y f2(). Haga inline a f1() y deje f2() como no-inline. Use la funcin clock() de la librera C estndar que se encuentra en <ctime> para marcar los puntos de comienzo y n y compare las dos funciones para ver cul es ms rpida. Puede que necesite hacer un bucle de llamadas repetidas para conseguir nmeros tiles. 5. Experimente con el tamao y complejidad del cdigo de las funciones del ejercicio 4 para ver si puede encontrar el punto donde la funcin inline y la convencional tardan lo mismo. Si dispone de ellos, intntelo con compiladores distintos y fjese en las diferencias. 6. Pruebe que las funciones inline hacer enlazado interno por defecto. 7. Cree una clase que contenga un array de caracteres. Aada un constructuor inline que use la funcin memset() de la librera C estndar para inicializar el array al valor dado como argumento del constructor (por defecto ser ), y un mtodo inline llamado print() que imprima todos los caracteres del array. 8. Coja el ejemplo NestFriend.cpp del Captulo 5 y reemplace todos los mtodos con inlines. No haga mtodos inline in situ. Tambin cambie las funciones initialize() por constructores. 9. modique StringStack.cpp del Captulo 8 para usar funciones inline. 10. Cree un enumerado llamado Hue que contenga red, blue y yellow. Ahora cree una clase llamada Color que contenga un atributo de tipo Hue y un constructor que de valor al Hue con su argumento. Aada mtodos de acceso al Hue get() y set(). Haga inline todos los mtodos. 11. Modique el ejercicio 10 para usar el enfoque accesor y mutador. 12. Modique Cpptime.cpp de modo que mida el tiempo desde que comienza el programa hasta que el usuario pulsa la tecla Intro o Retorno. 13. Cree una clase con dos mtodos inline, el primero que est denido en la clasee llama al segundo, sin necesitar una declaracin adelantada. Escriba un main() que cree un objeto de esa clase y llame al primer mtodo. 14. Cree una clase A con un constructor por defecto inline que se auncie a si mismo. Ahora cree una nueva B y ponga un objeto de A como miembro de B, y dele a B un contructor inline. cree un array de objetos B y vea que sucede. 15. Cree una gran cantidad de objetos del ejercicio anterior, y use la clase Time para medir las diferencias entre los contructores inline y los no-inline. (Si tiene un perlador, intente usarlo tambin). 16. Escriba un progra que tome una cadena por lnea de comandos. Escriba un bucle for que elimine un caracter de la cadena en cada pasada, y use la macro DEGUB() de este captulo para imprimir la cadena cada vez. 17. Corrija la macro TRACE() tal como se explica en el captulo, y pruebe que funciona correctamente. 18. Modique la macro FIELD() para que tambin incluya un ndice numrico. Cree una clase cuyos miembros estn compuestos de llamadas a la macro FIELD(). Aada un mtodo que le permita buscar en un campo usando el ndice. Escriba un main() para probar la clase. 254
9.9. Ejercicios 19. Modique la macro FIELD() para que automticamente genere funciones de acceso para cada campo (data debera no obstante ser privado). Cree una clase cuyos miembros estn compuestos de llamadas a la macro FIELD(). Escriba un main() para probar la clase. 20. Escriba un programa que tome dos argumentos de lnea de comandos: el primero es un entero y el segundo es un nombre de chero. Use requiere.h para asegurar que tiene el nmero correcto de argumentos, que el entero est entre 5 y 10, y que el chero se puede abrir satisfactorimente. 21. Escriba un program que use la macro IFOPEN() para abrir un chero como un ujo de entrada. Fjese en la creacin un objeto ifstream y su alcance. 22. (Desao) Averigue cmo conseguir que su compilador genere cdigo ensamblador. Cree un chero que contenga una funcin muy pequea y un main(). Genere el cdgio ensamblador cuando la funcin es inline y cuando no lo es, y demuestre que la versin inline no tiene la sobrecarga por la llamada.
255
257
La variable static char* s mantiene su valor entre llamadas a oneChar() porque no est almacenada en el segmento de pila de la funcin, sino que est en el rea de almacenamiento esttico del programa. Cuando se llama a oneChar() con char* como argumento, s se asigna a ese argumento de forma que se devuelve el primer carcter del array. Cada llamada posterior a oneChar() sin argumentos devuelve el valor por defecto cero para charArray, que indica a la funcin que todava se estn extrayendo caracteres del previamente inicializado valor de s. La funcin continuar devolviendo caracteres hasta que alcance el valor de nal del vector, momento en el que para de incrementar el puntero evitando que este sobrepase la ltima posicin del vector. Pero qu pasa si se llama a oneChar() sin argumentos y sin haber inicializado previamente el valor de s? En la denicin para s, se poda haber utilizado la inicializacin
static char* s = 0;
pero si no se incluye un inicializador para una variable esttica de un tipo denido, el compilador garantiza que la variable se inicializar a cero (convertido al tipo adecuado) al comenzar el programa. As pues, en oneChar(), la primera vez que se llama a la funcin, s vale cero. En este caso, se cumplir la condicin if(!s). El caso anterior es muy sencillo pero la inicializacin de objetos estticos predenidos (as como la de cualquier otro objeto) puede ser una expresin arbitraria, utilizando constantes as como variables y funciones previamente declaradas. Fjese que la funcin de arriba es muy vulnerable a problemas de concurrencia. Siempre que disee funciones que contengan variables estticas, deber tener en mente este tipo de problemas.
258
Los objetos estticos de tipo X dentro de f() pueden ser inicializados tanto con la lista de argumentos del constructor como con el constructor por defecto. Esta construccin sucede nicamente la primera vez que el control llega a la denicin.
259
En Obj, char c acta como un identicador de forma que el constructor y el destructor pueden imprimir la informacin a cerca del objeto sobre el que actan. Obj a es un objeto global y por tanto su constructor siempre se llama antes de que el control pase a main(), pero el constructor para static Obj b dentro de f(), y el de static Obj c dentro de g() slo sern invocados si se llama a esas funciones. Para mostrar qu constructores y qu destructores sern llamados, slo se invoca a f(). La salida del programa ser la siguiente:
Obj::Obj() for a inside main() Obj::Obj() for b leaving main() Obj::~Obj() for b Obj::~Obj() for a
El constructor para a se invoca antes de entrar en main() y el constructor de b se invoca slo porque existe una llamada a f(). Cuando se sale de main(), se invoca a los destructores de los objetos que han sido construidos en orden inverso al de su construccin. Esto signica que si se llama a g(), el orden en el que los destructores para b y c son invocados depende de si se llam primero a f() o a g(). Ntese que el objeto out de tipo ofstream, utilizado en la gestin de cheros, tambin es un objeto esttico (puesto que est denido fuera de cualquier funcin, vive en el rea de almacenamiento esttico). Es importante remarcar que su denicin (a diferencia de una declaracin tipo extern) aparece al principio del archivo, antes de cualquier posible uso de out. De lo contrario estaramos utilizando un objeto antes de que estuviese adecuadamente inicializado. En C++, el constructor de un objeto esttico global se invoca antes de entrar en main(), de forma que ya dispone de una forma simple y portable de ejecutar cdigo antes de entrar en main(), as como de ejecutar cdigo despus de salir de main(). En C, eso siempre implicaba revolver el cdigo ensamblador de arranque del compilador utilizado.
10.1. Los elementos estticos de C Hay veces en las que conviene limitar la visibilidad de un nombre. Puede que desee tener una variable con visibilidad a nivel de archivo de forma que todas las funciones de ese archivo puedan utilizarla, pero quiz no desee que funciones externas a ese archivo tengan acceso a esa variable, o que de forma inadvertida, cause solapes de nombres con identicadores externos a ese archivo. Un objeto o nombre de funcin, con visibilidad dentro del archivo en que se encuentra, que es explcitamente declarado como static es local a su (FIXME:translation unit:unidad de traduccin) (en trminos de este libro, el cpp donde se lleva a cabo la declaracin). Este nombre tiene enlace interno. Esto signica que puede usar el mismo nombre en otras (FIXME:translation units:unidades de traduccin) sin confusin entre ellos. Una ventaja del enlace interno es que el nombre puede situarse en un archivo de cabecera sin tener que preocuparse de si habr o no un cruce de nombres en el momento del enlazado. Los nombres que aparecen usualmente en los archivos de cabecera, como deniciones const y funciones inline, tienen por defecto enlazado interno. (De todas formas, const tiene por defecto enlazado interno slo en C++; en C tiene enlazado externo). Ntese que el enlazado se reere slo a elementos que tienen direcciones en tiempo de enlazado / carga. Por tanto, las declaraciones de clases y de variables locales no tienen enlazado.
Confusin
He aqu un ejemplo de como los dos sentidos de static pueden confundirse. Todos los objetos globales tienen implcitamente clase de almacenamiento esttico, o sea que si usted dice (en rango de visibilidad a nivel de archivo)
int a = 0;
el almacenamiento para a se llevar a cabo en el rea para datos estticos del programa y la inicializacin para a slo se realizar una vez, antes de entrar en main(). Adems, la visibilidad de a es global para todas las (FIXME:translation units:unidades de traduccin). En trminos de visibilidad, lo opuesto a static (visible tan slo en su (FIXME:translation unit:unidad de traduccin)) es extern que establece explcitamente que la visibilidad del nombre se extienda a todas las (FIXME:translation units:unidades de traduccin). Es decir, la denicin de arriba equivale a
extern int a = 0;
Pero si utilizase
static int a = 0;
todo lo que habra hecho es cambiar la visibilidad, de forma que a tiene enlace interno. La clase de almacenamiento no se altera, el objeto reside en el rea de datos esttica aunque en un caso su visibilidad es static y en el otro es extern. Cuando pasamos a hablar de variables locales, static deja de alterar la visibilidad y pasa a alterar la clase de almacenamiento. Si declara lo que parece ser una variable local como extern, signica que el almacenamiento existe en alguna otra parte (y por tanto la variable realmente es global a la funcin). Por ejemplo:
//: C10:LocalExtern.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} LocalExtern2 #include <iostream> int main() { extern int i; std::cout << i; }
261
Para nombres de funciones (sin tener en cuenta las funciones miembro), static y extern slo pueden alterar la visibilidad, de forma que si encontramos
extern void f();
y si utiliza
static void f();
signica que f() es visible slo para la (FIXME:translation unit:unidad de traduccin), (esto suele llamarse (FIXME:le static:archivo esttico)).
262
se cdigo crea un nuevo espacio de nombres que contiene las declaraciones incluidas entre las llaves. De todas formas, existen diferencias signicativas entre class, struct, union y enum: * Una denicin con namespace solamente puede aparecer en un rango global de visibilidad o anidado dentro de otro namespace. * No es necesario un punto y coma tras la llave de cierre para nalizar la denicin de namespace. * Una denicin namespace puede ser "continuada" en mltiples archivos de cabecera utilizando una sintaxis que, para una clase, parecera ser la de una redenicin:
//: C10:Header1.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef HEADER1_H #define HEADER1_H namespace MyLib { extern int x; void f(); // ... } #endif // HEADER1_H
Un namespace puede sobreponerse a otro nombre de forma que no hace falta que teclee un enrevesado nombre creado por algn vendedor de libreras:
//: C10:BobsSuperDuperLibrary.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt namespace BobsSuperDuperLibrary { class Widget { /* ... */ }; class Poppit { /* ... */ }; // ... } // Too much to type! Ill alias it: namespace Bob = BobsSuperDuperLibrary; int main() {}
No puede crear una instancia de un namespace tal y como podra con una clase.
Captulo 10. Control de nombres En C++ es preferible utilizar espacios de nombres sin nombre que archivos estticos.
Amigas
Es posible aadir una declaracin tipo friend dentro de un espacio de nombres incluyndola dentro de una clase:
//: C10:FriendInjection.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt namespace Me { class Us { //... friend void you(); }; } int main() {}
Ahora la funcin you( ) es un miembro del espacio de nombres Me. Si introduce una declaracin tipo friend en una clase dentro del espacio de nombres global, dicha declaracin se inyecta globalmente.
264
Ntese que la denicin X::Y::i puede referirse tambin a una variable miembro de la clase Y anidado dentro de la clase X en lugar del espacio de nombres X.
La directiva using
Puesto que teclear toda la especicacin para un identicador en un espacio de nombres puede resultar rpidamente tedioso, la palabra clave using le permite importar un espacio denombre entero a la vez. Cuando se utiliza en conjuncin con la palabra clave namespace, se dice que utilizamos una directiva using. Las directivas using hacen que los nombres acten como si perteneciesen al mbito del espacio de nombre que les incluye ms cercano por lo que puede utilizar convenientemente los nombres sin explicitar completamente su especicacin. Considere el siguiente espacio de nombres:
//: C10:NamespaceInt.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef NAMESPACEINT_H #define NAMESPACEINT_H namespace Int { enum sign { positive, negative }; class Integer { int i; sign s; public: Integer(int ii = 0) : i(ii), s(i >= 0 ? positive : negative) {} sign getSign() const { return s; } void setSign(sign sgn) { s = sgn; } // ... }; } #endif // NAMESPACEINT_H
Un uso de las directivas using es incluir todos los nombres en Int dentro de otro espacio de nombres, dejando aquellos nombres anidados dentro del espacio de nombres
//: C10:NamespaceMath.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef NAMESPACEMATH_H #define NAMESPACEMATH_H #include "NamespaceInt.h" namespace Math { using namespace Int; Integer a, b; Integer divide(Integer, Integer); // ... } #endif // NAMESPACEMATH_H
265
Captulo 10. Control de nombres Usted tambin puede declarar todos los nombres en Int dentro de la funcin pero dejando aquellos nombres anidados dentro de la funcin:
//: C10:Arithmetic.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "NamespaceInt.h" void arithmetic() { using namespace Int; Integer x; x.setSign(positive); } int main(){}
Sin la directiva using, todos los nombres en el espacio de nombres requeriran estar completamente explicitados. Un aspecto de la directiva using podra parecer poco intuitivo al principio. La visibilidad de los nombres introducidos con una directiva using es el rango en el que la directiva se crea. Pero usted puede hacer caso omiso de los nombres denidos en la directiva using como si estos hubiesen sido declarados globalmente para ese rango!
//: C10:NamespaceOverriding1.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "NamespaceMath.h" int main() { using namespace Math; Integer a; // Hides Math::a; a.setSign(negative); // Now scope resolution is necessary // to select Math::a : Math::a.setSign(positive); }
Suponga que tiene un segundo espacio de nombres que contiene algunos nombres de namespace Math:
//: C10:NamespaceOverriding2.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef NAMESPACEOVERRIDING2_H #define NAMESPACEOVERRIDING2_H #include "NamespaceInt.h" namespace Calculation { using namespace Int; Integer divide(Integer, Integer); // ... } #endif // NAMESPACEOVERRIDING2_H
Puesto que este espacio de nombres tambin se introduce con una directiva using, existe la posibilidad de tener una colisin. De todos modos, la ambigedad aparece en el momento de utilizar el nombre, no en la directiva using: 266
//: C10:OverridingAmbiguity.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "NamespaceMath.h" #include "NamespaceOverriding2.h" void s() { using namespace Math; using namespace Calculation; // Everythings ok until: //! divide(1, 2); // Ambiguity } int main() {}
Por tanto, es posible escribir directivas using para introducir un nmero de espacios de nombre con nombres conictivos sin producir ninguna ambigedad.
La declaracin using
Usted puede inyectar nombres de uno en uno en el rango actual utilizando una declaracin using. A diferencia de la directiva using, que trata los nombres como si hubiesen sido declarados globalmente para ese rango, una declaracin using es una declaracin dentro del rango actual. Esto signica que puede sobrescribir nombres de una directiva using:
//: C10:UsingDeclaration.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef USINGDECLARATION_H #define USINGDECLARATION_H namespace U { inline void f() {} inline void g() {} } namespace V { inline void f() {} inline void g() {} } #endif // USINGDECLARATION_H
La declaracin using simplemente da completamente especicado el nombre del identicador pero no da informacin de tipo. Esto signica que si el espacio de nombres contiene un grupo de funciones sobrecargadas con el mismo nombre, la declaracin using declara todas las funciones pertenecientes al grupo sobrecargado. Es posible poner una declaracin using en cualquier sitio donde podra ponerse una declaracin normal. Una declaracin using trabaja de la misma manera que cualquier declaracin normal salvo por un aspecto: puesto que no se le da ninguna lista de argumentos, una declaracin using puede provocar la sobrecarga de una funcin con los mismos tipos de argumentos (cosa que no est permitida por el procedimiento de sobrecarga normal). De todas formas, esta ambigedad no se muestra hasta el momento de uso, no apareciendo en el instante de declaracin. Una declaracin using puede tambin aparecer dentro de un espacio de nombres y tiene el mismo efecto que en cualquier otro lugar (ese nombre se declara dentro del espacio):
//: C10:UsingDeclaration2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000
267
Una declaracin using es un alias. Le permite declarar la misma funcin en espacios de nombre diferentes. Si acaba redeclarando la misma funcin importando diferentes espacios de nombres no hay problema, no habr ambigedades o duplicados.
10.3. Miembros estticos en C++ denido. La denicin debe realizarse fuera de la clase (no se permite el uso de la sentencia inline), y slo est permitida una denicin. Es por ello que habitualmente se incluye en el archivo de implementacin para la clase. La sintaxis suele traer problemas, pero en realidad es bastante lgica. Por ejemplo, si crea un dato esttico miembro dentro de una clase de la siguiente forma:
class A { static int i; public: //... };
Deber denir el almacenamiento para ese dato esttico miembro en el archivo de denicin de la siguiente manera:
int A::i = 1;
pero aqu, el operador de resolucin de rango y el nombre de la clase se utilizar para especicar A::i. Algunas personas tienen problemas con la idea que A::i es private, y pese a ello parece haber algo que lo est manipulando abiertamente. No rompe esto el mecanismo de proteccin? Esta es una prctica completamente segura por dos razones. Primera, el nico sitio donde esta inicializacin es legal es en la denicin. Efectivamente, si el dato static fuese un objeto con un constructor, habra llamado al constructor en lugar de utilizar el operador =. Segundo, una vez se ha realizado la denicin, el usuario nal no puede hacer una segunda denicin puesto que el enlazador dara error. Y el creador de la clase est forzado a crear la denicin o el cdigo no enlazara en las pruebas. Esto asegura que la denicin slo sucede una vez y que es el creador de la clase quien la lleva a cabo. La expresin completa de inicializacin para un miembro esttico se realiza en el rango de la clase. Por ejemplo,
//: C10:Statinit.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Scope of static initializer #include <iostream> using namespace std; int x = 100; class WithStatic { static int x; static int y; public: void print() const { cout << "WithStatic::x = " << x << endl; cout << "WithStatic::y = " << y << endl; } }; int WithStatic::x = 1; int WithStatic::y = x + 1; // WithStatic::x NOT ::x
269
270
Con static consts de tipo entero puede realizar las deniciones dentro de la clase, pero para cualquier otro tipo (incluyendo listas de enteros, incluso si estos son static const) deber realizar una nica denicin externa para el miembro. Estas deniciones tienen enlazado interno, por lo que pueden incluirse en archivos de cabecera. La sintaxis para inicializar listas estticas es la misma que para cualquier agregado, incluyendo el contado automtico. Tambin puede crear objetos static const de tipos de clase y listas de dichos objetos. De todas formas, no puede inicializarlos utilizando la sintaxis tipo "inline" permitida para static consts de tipos enteros por defecto:
//: C10:StaticObjectArrays.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Static arrays of class objects class X { int i; public: X(int ii) : i(ii) {} }; class Stat { // This doesnt work, although // you might want it to: //! static const X x(100); // Both const and non-const static class // objects must be initialized externally: static X x2; static X xTable2[]; static const X x3; static const X xTable3[]; }; X Stat::x2(100); X Stat::xTable2[] = { X(1), X(2), X(3), X(4) }; const X Stat::x3(100); const X Stat::xTable3[] = { X(1), X(2), X(3), X(4) }; int main() { Stat v; }
La inicializacin de listas estticas de objetos de clase tanto const como no const debe ser realizada de la misma manera, siguiendo la tpica sintaxis de denicin esttica.
Ya puede ver el problema con miembros estticos en clases locales: Cmo describir el dato miembro en rango de archivo para poder denirlo? En la prctica, el uso de clases locales es muy poco comn.
272
10.3. Miembros estticos en C++ Cuando vea funciones miembro estticas en una clase, recuerde que el diseador pretenda que esa funcin estuviese conceptualmente asociada a la clase como un todo. Una funcin miembro static no puede acceder a los datos miembro ordinarios, slo a los datos miembro static. Slo puede llamar a otras funciones miembro static. Normalmente, la direccin del objeto actual (this) se pasa de forma encubierta cuando se llama a cualquier funcin miembro, pero un miembro static no tiene this, que es la razn por la cual no puede acceder a los miembros ordinarios. Por tanto, se obtiene el ligero incremento de velocidad proporcionado por una funcin global debido a que una funcin static miembro no lleva la carga extra de tener que pasar this. Al mismo tiempo, obtiene los benecios de tener la funcin dentro de la clase. Para datos miembro, static indica que slo existe un espacio de memoria por dato miembro para todos los objetos de la clase. Esto establece que el uso de static para denir objetos dentro de una funcin signica que slo se utiliza una copia de una variable local para todas las llamadas a esa funcin. Aqu se muestra un ejemplo mostrando datos miembro static y funciones miembro static utilizadas conjuntamente:
//: C10:StaticMemberFunctions.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class X { int i; static int j; public: X(int ii = 0) : i(ii) { // Non-static member function can access // static member function or data: j = i; } int val() const { return i; } static int incr() { //! i++; // Error: static member function // cannot access non-static member data return ++j; } static int f() { //! val(); // Error: static member function // cannot access non-static member function return incr(); // OK -- calls static } }; int X::j = 0; int main() { X x; X* xp = &x; x.f(); xp->f(); X::f(); // Only works with static members }
Puesto que no tienen el puntero this, las funciones miembro static no pueden ni acceder a datos miembro no static ni llamar a funciones miembro no static. Note el lector que en main() un miembro static puede seleccionarse utilizando la habitual sintaxis de punto o echa, asociando la funcin con el objeto, pero tambin sin objeto (ya que un miembro static est asociado con una clase, no con un objeto particular), utilizando el nombre de la clase y el operador de resolucin de rango. He aqu una interesante caracterstica: Debido a la forma en la que se inicializan los objetos miembro static, es posible poner un dato miembro static de la misma clase dento de dicha clase. He aqu un ejemplo que tan solo permite la existencia de un nico objeto de tipo Egg deniendo el constructor privado. Puede acceder a este objeto pero no puede crear ningn otro objeto tipo Egg: 273
//: C10:Singleton.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Static member of same type, ensures that // only one object of this type exists. // Also referred to as the "singleton" pattern. #include <iostream> using namespace std; class Egg { static Egg e; int i; Egg(int ii) : i(ii) {} Egg(const Egg&); // Prevent copy-construction public: static Egg* instance() { return &e; } int val() const { return i; } }; Egg Egg::e(47); int main() { //! Egg x(1); // Error -- cant create an Egg // You can access the single instance: cout << Egg::instance()->val() << endl; }
La inicializacin de E acontece una vez se completa la declaracin de la clase, por lo que el compilador tiene toda la informacin que necesita para reservar espacio y llamar al constructor. Para prevenir completamente la creacin de cualquier otro objeto, se ha aadido algo ms: un segundo constructor privado llamado copy-constructor. Llegados a este punto del libro, usted no puede saber porque es esto necesario puesto que el constructor copia no ser estudiado hasta el captulo siguiente. De todas formas, como un breve adelanto, si se propusiese retirar el constructor copia denido en el ejemplo anterior, sera posible crear objetos Egg de la siguiente forma:
Egg e = *Egg::instance(); Egg e2(*Egg::instance());
Ambos utilizan el constructor copia, por lo que para evitar esta posibilidad, se declara el constructor copia como privado (no se requiere denicin porque nunca va a ser llamado). Buena parte del siguiente captulo es una discusin sobre el constructor copia por lo que esto quedar ms claro entonces.
274
el programa puede funcionar, o puede que no. Si el entorno de programacin monta el programa de forma que el primer archivo sea inicializado despues del segundo, no habr problemas. Pero si el segundo archivo se inicializa antes que el primero, el constructor para Oof se sustenta en la existencia de out, que todava no ha sido construido, lo que causa el caos. Este problema slo ocurre con inicializadores de objetos estticos que dependen el uno del otro. Los estticos dentro de cada unidad de traduccin son inicializados antes de la primera invocacin a cualquier funcin de esa unidad, aunque puede que despues de main(). No puede estar seguro del orden de inicializacin de objetos estticos si estn en archivos diferentes. Un ejemplo sutil puede encontrarse en ARM.[47] en un archivo que aparece en el rango global:
extern int y; int x = y + 1;
Para todos los objetos estticos, el mecanismo de carga-enlazado garantiza una inicializacin esttica a cero antes de la inicializacin dinmica especicada por el programador. En el ejemplo anterior, la inicializacin a cero de la zona de memoria ocupada por el objeto fstream out no tiene especial relevancia, por lo que est realmente indenido hasta que se llama al constructor. Pese a ello, en el caso de los tipos predenidos, la inicializacin a cero s tiene importancia, y si los archivos son inicializados en el orden mostrado arriba, y empieza estticamente inicializada a cero, por lo que x se convierte en uno, e y es dinmicamente inicializada a dos. Pero si los archivos fuesen inicializados en orden opuesto, x sera estticamente inicializada a cero, y dinmicamente inicializada a uno y despues, x pasara a valer dos. Los programadores deben estar al tanto de esto porque puede darse el caso de crear un programa con dependencias de inicializacin estticas que funciones en una plataforma determinada y, de golpe y misteriosamente, compilarlo en otro entorno y que deje de funcionar. 275
10.4.1. Qu hacer
Existen tres aproximaciones para tratar con este problema: 1. No hacerlo. Evitar las dependencias de inicializacin esttica es la mejor solucin. 2. Si lo debe hacer, coloque las deniciones de objetos estticos crticos en una nica lnea, de forma que pueda controlar, de forma portable, su inicializacin colocndolos en el orden correcto. 3. Si est convencido que es inevitable dispersar objetos estticos entre (FIXME:translation units:unidades de traduccin) diferentes (como en el caso de una librera, donde no puede controlar el programa que la usa), hay dos tcnicas de programacin para solventar el problema. Tcnica uno El pionero de esta tcnica fue Jerry Schwarz mientras creaba la librera iostream (puesto que las deniciones para cin, cout y cerr son static y viven en diferentes archivos). Realmente es inferior a la segunda tcnica pero ha pululado durante mucho tiempo por lo que puede encontrarse con cdigo que la utilice; as pues, es importante que entienda como trabaja. Esta tcnica requiere una clase adicional en su archivo de cabecera. Esta clase es la responsable de la inicializacin dinmica de sus objetos estticos de librera. He aqu un ejemplo simple:
//: C10:Initializer.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Static initialization technique #ifndef INITIALIZER_H #define INITIALIZER_H #include <iostream> extern int x; // Declarations, not definitions extern int y; class Initializer { static int initCount; public: Initializer() { std::cout << "Initializer()" << std::endl; // Initialize first time only if(initCount++ == 0) { std::cout << "performing initialization" << std::endl; x = 100; y = 200; } } ~Initializer() { std::cout << "~Initializer()" << std::endl; // Clean up last time only if(--initCount == 0) { std::cout << "performing cleanup" << std::endl; // Any necessary cleanup here } } }; // The following creates one object in each // file where Initializer.h is included, but that // object is only visible within that file: static Initializer init; #endif // INITIALIZER_H
Las declaraciones para x e y anuncian tan slo que esos objetos existen, pero no reservan espacio para los objetos. No obstante, la denicin para el Initializer init reserva espacio para ese objeto en cada archivo en que se incluya el archivo de cabecera. Pero como el nombre es static (en esta ocasin controlando la visibilidad, no la 276
10.4. FIXME static initialization dependency forma en la que se almacena; el almacenamiento se produce a nivel de archivo por defecto), slo es visible en esa unidad de traduccin, por lo que el enlazador no se quejar por mltiples errores de denicin. He aqu el archivo conteniendo las deniciones para x, y e initCount:
//: C10:InitializerDefs.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Definitions for Initializer.h #include "Initializer.h" // Static initialization will force // all these values to zero: int x; int y; int Initializer::initCount;
(Por supuesto, una instancia esttica de archivo de init tambin se incluye en este archivo cuando se incluye el archivo de cabecera. Suponga que otros dos archivos son creados por la librera de usuario:
//: C10:Initializer.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Static initialization #include "Initializer.h"
y
//: C10:Initializer2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} InitializerDefs Initializer // Static initialization #include "Initializer.h" using namespace std; int main() { cout << "inside main()" << endl; cout << "leaving main()" << endl; }
Ahora no importa en qu unidad de traduccin se inicializa primero. La primera vez que una unidad de traduccin que contenga Initializer.h se inicialice, initCount ser cero por lo que la inicializacin ser llevada a cabo. (Esto depende en gran medida en el hecho que la zona de almacenamiento esttico est a cero antes de que cualquier inicializacin dinmica se lleve a cabo). Para el resto de (FIXME:translation units:unidades de traduccin), initCount no ser cero y se eludir la inicializacin. La limpieza ocurre en el orden inverso, y ~Initializer() asegura que slo ocurrir una vez. Este ejemplo utiliza tipos por defecto como objetos globales estticos. Esta tcnica tambin trabaja con clases, pero esos objetos deben ser inicializados dinmicamente por la clase Initializer. Una forma de hacer esto es creando clases sin constructores ni destructores, pero s con funciones miembro de inicializacin y limpieza utilizando nom277
Captulo 10. Control de nombres bres diferentes. Una aproximacin ms comn, de todas formas, es tener punteros a objetos y crearlos utilizando new dentro de Initializer(). Tcnica dos Bastante despues de la aparicin de la tcnica uno, alguien (no s quien) lleg con la tcnica explicada en esta seccin, que es mucho ms simple y limpia que la anterior. El hecho que tardase tanto en descubrirse es un tributo a la complejidad de C++. Esta tcnica se sustenta en el hecho que los objetos estticos dentro de funciones (slo) se inicializan la primera vez que se llama a la funcin. Mantenga en mente que el problema que estamos intentando resolver aqu no es cuando se inicializan los objetos estticos (que puede ser controlado separadamente) sino ms bien el asegurarnos que la inicializacin acontece en el orden adecuado. Esta tcnica es muy limpia y astuta. Para cualquier dependencia de inicializacin, usted coloca un objeto esttico dentro de una funcin que devuelve una referencia a ese objeto. De esta forma, la nica manera de acceder al objeto esttico es llamando a la funcin, y si ese objeto necesita acceder a otros objetos estticos de los que depende, debe llamar a sus funciones. Y la primera vez que se llama a una funcin, se fuerza a llevar a cabo la inicializacin. Est garantizado que el orden de la inicializacin esttica ser correcto debido al diseo del cdigo, no al orden que arbitrariamente decide el enlazador. Para mostrar un ejemplo, aqu tenemos dos clases que dependen la una de la otra. La primera contiene un bool que slo se inicializa por el constructor, por lo que se puede decir si se ha llamado el constructor por una instancia esttica de la clase (el rea de almacenamiento esttico se inicializa a cero al inicio del programa, lo que produce un valor false para el bool si el constructor no ha sido llamado)
//: C10:Dependency1.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef DEPENDENCY1_H #define DEPENDENCY1_H #include <iostream> class Dependency1 { bool init; public: Dependency1() : init(true) { std::cout << "Dependency1 construction" << std::endl; } void print() const { std::cout << "Dependency1 init: " << init << std::endl; } }; #endif // DEPENDENCY1_H
El constructor tambin indica cuando ha sido llamado, y usted puede print() el estado del objeto para averiguar si ha sido inicializado. La segunda clase es inicializada por un objeto de la primera clase, que es lo que causa la dependencia:
//: C10:Dependency2.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef DEPENDENCY2_H #define DEPENDENCY2_H #include "Dependency1.h" class Dependency2 { Dependency1 d1;
278
El constructor se anuncia a si mismo y imprime el estado del objeto d1 por lo que puede ver si este se ha inicializado cuando se llama al constructor. Para demostrar lo que puede ir mal, el siguiente archivo primero pone las deniciones de los objetos estticos en el orden incorrecto, tal y como sucedera si el enlazador inicializase el objeto Dependency2 antes del Dependency1. Despues se invierte el orden para mostrar como funciona correctamente si el orden resulta ser el correcto. Finalmente, se muestra la tcnica dos. Para proveer una salida ms legible, ha sido creada la funcin separator(). El truco est en que usted no puede llamar a la funcin globalmente a menos que la funcin sea utilizada para llevar a cabo la inicializacin de la variable, por lo que separator() devuelve un valor absurdo que es utilizado para inicializar un par de variables globales.
//: C10:Technique2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "Dependency2.h" using namespace std; // Returns a value so it can be called as // a global initializer: int separator() { cout << "---------------------" << endl; return 1; } // Simulate the dependency problem: extern Dependency1 dep1; Dependency2 dep2(dep1); Dependency1 dep1; int x1 = separator(); // But if it happens in this order it works OK: Dependency1 dep1b; Dependency2 dep2b(dep1b); int x2 = separator(); // Wrapping static objects in functions succeeds Dependency1& d1() { static Dependency1 dep1; return dep1; } Dependency2& d2() { static Dependency2 dep2(d1()); return dep2; } int main() { Dependency2& dep2 = d2(); }
279
Las funciones d1() y d2() contienen instancias estticas de los objetos Dependency1 y Dependency2. Ahora, la nica forma de acceder a los objetos estticos es llamando a las funciones y eso fuerza la inicializacin esttica en la primera llamada a la funcin. Esto signica que se garantiza la correcta inicializacin, cosa que ver cuando lance el programa y observe la salida. He aqu como debe organizar el cdigo para usar esta tcnica. Ordinariamente, los objetos estticos deben ser denidos en archivos diferentes (puesto que se ha visto forzado a ello por alguna razn; recuerde que denir objetos estticos en archivos diferentes es lo que causa el problema), por lo que denir las (FIXME:wrapping functions:funciones contenedoras) en archivos diferentes. Pero estas necesitan ser declaradas en los archivos de cabecera:
//: C10:Dependency1StatFun.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef DEPENDENCY1STATFUN_H #define DEPENDENCY1STATFUN_H #include "Dependency1.h" extern Dependency1& d1(); #endif // DEPENDENCY1STATFUN_H
En realidad, el "extern" es redundante para la declaracin de la funcin. Este es el segundo archivo de cabecera:
//: C10:Dependency2StatFun.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef DEPENDENCY2STATFUN_H #define DEPENDENCY2STATFUN_H #include "Dependency2.h" extern Dependency2& d2(); #endif // DEPENDENCY2STATFUN_H
Ahora, en los archivos de implementacin donde previamente habra sitado las deniciones de los objetos estticos, sitar las deniciones de las (FIXME:wrapping functions:funciones contenedoras):
//: C10:Dependency1StatFun.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "Dependency1StatFun.h" Dependency1& d1() { static Dependency1 dep1; return dep1; }
Presumiblemente, otro cdigo puede tambin componer esos archivos. He aqu otro archivo:
//: C10:Dependency2StatFun.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000
280
Ahora hay dos archivos que pueden ser enlazados en cualquier orden y si contuviesen objetos estticos ordinarios podra producirse cualquier orden de inicializacin. Pero como contienen (FIXME:wrapping functions:funciones contenedoras), no hay posibilidad de inicializacin incorrecta:
//: C10:Technique2b.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Dependency1StatFun Dependency2StatFun #include "Dependency2StatFun.h" int main() { d2(); }
Cuando ejecute este programa ver que la inicializacin del objeto esttico Dependency1 siempre se lleva a cabo antes de la inicializacin del objeto esttico Dependency2. Tambin puede ver que esta es una solucin bastante ms simple que la de la tcnica uno. Puede verse tentado a escribir d1() y d2() como funciones inline dentro de sus respectivos archivos de cabecera, pero eso es algo que, denitivamente, no debe hacer. Una funcin inline puede ser duplicada en cada archivo en el que aparezca y esa duplicacin incluye la denicin de los objetos estticos. Puesto que las funciones inline llevan asociado por defecto enlazado interno, esto provocar el tener mltiples objetos estticos entre las diversas (FIXME:translation units: unidades de traduccin), lo que ciertamente causar problemas. Es por ello que debe asegurarse que slo existe una nica denicin para cada (FIXME:wrapping functions:funciones contenedoras), y eso signica no hacer las (FIXME:wrapping functions:funciones contenedoras) inline.
el compilador de C++ adornar el nombre como algo tipo _f_int_char para permitir la sobrecarga de la funcin (y el (FIXME:type-safe linkage:enlazado asegurando los tipos)). De todas formas, el compilador de C que compil su librera C denitivamente no decor su nombre, por lo que su nombre interno ser _f. As pues, el enlazador no ser capaz de resolver sus llamadas tipo C++ a f(). La forma de escapar a esto que se propone en C++ es la especicacin de enlazado alternativo, que se produjo en el lenguaje sobrecargando la palabra clave extern. A la palabra clave extern le sigue una cadena que especica el enlazado deseado para la declaracin, seguido por la declaracin:
extern "C" float f(int a, char b);
Esto le dice al compilador que f() tiene enlazado tipo C, de forma que el compilador no decora el nombre. Las dos nicas especicaciones de enlazado soportadas por el estndar son "C" y "C++", pero algunos vendedores ofrecen compiladores que tambin soportan otros lenguajes. Si tiene un grupo de declaraciones con enlazado alternativo, pngalas entre llaves, como a continuacin: 281
extern "C" { float f(int a, char b); double d(int a, char b); }
La mayora de compiladores de C++ a la venta manejan las especicaciones de enlazado alternativo dentro de sus propios archivos de cabecera que trabajan tanto con C como con C++, por lo que no tiene que preocuparse de eso.
10.6. Resumen
La palabra clave static puede llevar a confusin porque en algunas sitaciones controla la reserva de espacio en memoria, y en otras controla la visibilidad y enlazado del nombre. Con la introduccin de los espacios de nombres de C++, dispone de una alternativa mejorada y ms exible para controlar la proliferacin de nombres en proyectos grandes. El uso de static dentro de clases es un mtodo ms para controlar los nombres de un programa. Los nombres no colisionan con nombres globales, y la visibilidad y acceso se mantiene dentro del programa, dndole un mayor control para el mantenimiento de su cdigo.
10.7. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree una funcin con una variable esttica que sea un puntero (con argumento por defecto cero). Cuando la funcin que realice la llamada provea un valor para ese argumento se usar para apuntar al principio de una tabla de int. Si se llama a la funcin con el argumento cero (utilizando el argumento por defecto), la funcin devuelve el siguiente valor de la tabla, hasta que llegue a un valor "-1" en la tabla (que actuar como seal de nal de tabla). Ejercite esta funcin en main(). 2. Cree una funcin que devuelva el siguiente valor de una serie de Fibonacci cada vez que sea llamada. Aada un argumento que sea tipo bool con valor por defecto false tal que cuando el argumento valga true "reinicie" la funcin al principio de la serie de Fibonacci. Ejercite esta funcin en main(). 3. Cree una clase que contenga una tabla de ints. Especique la dimensin del array utilizando static const int dentro de la clase. Aada una variable const int e inicialcela en la lista de inicializacin del constructor. Haga al contructor inline. Aada una variable miembro static int e inicialcela a un valor especco. Aada una funcin miembro esttica que imprima el dato static miembro. Aada una funcin miembro inline llamada print() que imprima todos los valores de la tabla y que llame a la funcin miembro esttica. Ejercite esta clase en main(). 4. Cree una clase llamada Monitor que mantenga el registro del nmero de veces que ha sido llamada su funcin miembro incident(). Aada una funcin miembro print() que muestre por pantalla el nmero de incidentes. Ahora cree una funcin global (no una funcin miembro) que contenga un objeto static Monitor. Cada vez que llame a la funcin debe llamar a incident(), despues a la funcin miembro print() para sacar por pantalla el contador de incidentes. Ejercite la funcin en main(). 5. Modique la clase Monitor del Ejercicio 4 de forma que pueda decrementar (decrement()) el contador de incidentes. Cree una clase llamada Monitor2 que tome como argumento de constructor un puntero a Monitor1, y que almacene ese puntero llame a incident() y print(). En el destructor para Monitor2, llame a decrement() 282
10.7. Ejercicios y print(). Cree ahora un objeto static de Monitor2 dentro de una funcin. Dentro de main(), experimente llamando y no llamando la funcin para ver que pasa con el destructor de Monitor2. 6. Cree un objeto global de Monitor2 y vea que sucede. 7. Cree una clase con un destructor que imprima un mensaje y despus llame a exit(). Cree un objeto global de esta clase y vea que pasa. 8. En StaticDestructors.cpp, experimente con el orden de llamada de los constructores y destructores llamando a f() y g() dentro de main() en diferentes rdenes. Su compilador inicializa los objetos de la forma correcta? 9. En StaticDestructors.cpp, pruebe el manejo de errores por defecto de su implementacin convirtiendo la denicin original de out dentro de una declaracin extern, y poniendo la denicin real despues de la denicin de a (donde el constructor de Obj manda informacin a out). Asegrese que no hay ningn otro programa importante funcionando en su mquina cuando ejecute el cdigo o que su mquina maneje las faltas robustamente. 10. Pruebe que las variables estticas de archivo en los archivos de cabecera no chocan entre s cuando son incluidas en ms de un archivo cpp. 11. Cree una nica clase que contenga un int, un constructor que inicialice el int con su argumento, una funcin miembro que cambie el valor del int con su argumento y una funcin print() que muestre por pantalla el int. Coloque su clase en un archivo de cabecera e incluya dicho archivo en dos archivos cpp. En uno de ellos cree una instancia de la clase y en la otra declare ese identicador como extern y pruebe dentro de main(). Recuerde, debe enlazar los dos archivos objeto o de lo contrario el enlazador no encontrar el objeto. 12. Cree la instancia del objeto del Ejercicio 11 como static y verique que, debido a eso, no puede ser hallada por el enlazador. 13. Declare una funcin en un archivo de cabecera. Dena la funcin en un archivo cpp y llmela dentro de main() en un segundo archivo cpp. Compile y verique que funciona. Ahora cambie la denicin de la funcin de forma que sea static y verique que el enlazador no puede encontrarla. 14. Modique Volatile.cpp del Captulo 8 para hacer que comm::isr() funcione realmente como una rutina de servicio de interrupcin. Pista: una rutina de servicio de interrupcin no toma ningn argumento. 15. Escriba y compile un nico programa que utilice las palabras clave auto y register. 16. Cree un archivo de cabecera que contenga un espacio de nombres. Dentro del espacio de nombres cree varias declaraciones de funciones. Cree ahora un segundo archivo de cabecera que incluya el primero y contine el espacio de nombres, aadiendo varias declaraciones de funciones ms. Cree ahora un archivo cpp que incluya el segundo archivo de cabecera. Cambie su espacio de nombres a otro nombre (ms corto). Dentro de una denicin de funcin, llame a una de sus funciones utilizando la resolucin de rango. Dentro de una denicin de funcin separada, escriba una directiva using para introducir su espacio de nombres dentro de ese rango de funcin, y demuestre que no necesita utilizar la resolucin de rango para llamar a las funciones desde su espacio de nombres. 17. Cree un archivo de cabecera con un (FIXME:namespace:espacio de nombres) sin nombre. Incluya la cabecera en dos archivos cpp diferentes y demuestre que un espacio sin nombre es nico para cada (FIXME:translation unit:unidad de traduccin). 18. Utilizando el archivo de cabecera del Ejercicio 17, demuestre que los nombres de un espacio de nombres sin nombre estn disponibles automticamente en una (FIXME:translation unit:unidad de traduccin) sin calicacin. 19. Modique FriendInjection.cpp para aadir una denicin para la funcin amiga y para llamar a la funcin desde dentro de main(). 20. En Arithmetic.cpp, demuestre que la directiva using no se extiende fuera de la funcin en la que fue creada. 21. Repare el problema en OverridingAmbiguity.cpp, primero con resolucin de rango y luego, con una declaracin using que fuerce al compilador a escojer uno de los idnticos nombres de funcin. 283
Captulo 10. Control de nombres 22. En dos archivos de cabecera, cree dos espacios de nombres, cada uno conteniendo una clase (con todas las deniciones inline) con idntico nombre que el del otro espacio de nombres. Cree un archivo cpp que incluya ambos archivos. Cree una funcin y, dentro de la funcin, utilice la directiva using para introducir ambos espacios de nombres. Pruebe a crear un objeto de la clase y vea que sucede. Haga las directivas using globales (fuera de la funcin) para ver si existe alguna diferencia. Repare el problema usando la resolucin de rango, y cree objetos de ambas clases. 23. Repare el problema del Ejercicio 22 con una declaracin using que fuerce al compilador a escojer uno de los idnticos nombres de clase. 24. Extraiga las declaraciones de (FIXME:namespaces:espacios de nombres) en BobsSuperDuperLibrary.cpp y UnnamedNamespaces.cpp y pngalos en archivos separados, dando un nombre al (FIXME:namespace:espacio de nombres) sin nombre en el proceso. En un tercer archivo de cabecera, cree un nuevo espacio de nombres que combine los elementos de los otros dos espacios de nombres con declaraciones using. En main(), introduzca su nuevo espacio de nombres con una directiva using y acceda a todos los elementos de su espacio de nombres. 25. Cree un archivo de cabecera que incluya <string> y <iostream> pero que no use ninguna directiva using ni ninguna declaracin using. Aada (FIXME:"include guards":"protecciones de inclusin") como ha visto en los archivos de cabecera del libro. Cree una clase con todas las funciones inline que muestre por pantalla el string. Cree un archivo cpp y ejercite su clase en main(). 26. Cree una clase que contenga un static double y long. Escriba una funcin miembro static que imprima los valores. 27. Cree una clase que contenga un int, un constructor que inicialice el int con su argumento, y una funcin print() que muestre por pantalla el int. Cree ahora una segunda clase que contenga un objeto static de la primera. Aada una funcin miembro static que llame a la funcin static print() del objeto. Ejercitu su clase en main(). 28. Cree una clase que contenga una tabla de ints esttica const y otra no const. Escriba mtodos static que impriman las tablas. Ejercite su clase en main(). 29. Cree una clase que contenga un string, con un constructor que inicialice el string a partir de su argumento, y una funcin print() que imprima el string. Cree otra clase que contenga una tabla esttica tanto const como no const de objetos de la primera clase, y mtodos static para imprimir dichas tablas. Ejercite la segunda clase en main(). 30. Cree una struct que contenga un int y un constructor por defecto que inicialice el int a cero. Haga esta struct local a una funcin. Dentro de dicha funcin, cree una tabla de objetos de su struct y demuestre que cada int de la tabla ha sido inicializado a cero automticamente. 31. Cree una clase que represente una conexin a impresora, y que slo le permita tener una impresora. 32. En un archivo de cabecera, cree una clase Mirror que contiene dos miembros dato: un puntero a un objeto Mirror y un bool. Dle dos constructores: el constructor por defecto inicializa el bool a true y el puntero a Mirror a cero. El segundo constructor toma como argumento un puntero a un objeto Mirror, que asigna al puntero interno del objeto; pone el bool a false. Aada una funcin miembro test(): si el puntero del objeto es distinto de cero, devuelve el valor de test() llamado a traves del puntero. Si el puntero es cero, devuelve el bool. Cree ahora cinco archivos cpp, cada uno incluyendo la cabecera Mirror. El primer archivo cpp dene un objeto Mirror global utilizando el constructor por defecto. El segundo archivo declara el objeto del primer archivo como extern, y dene un objeto Mirror global utilizando el segundo constructor, con un puntero al primer objeto. Siga haciendo lo mismo hasta que llegue al ltimo archivo, que tambin contendr una denicin de objeto global. En este archivo, main() debe llamar a la funcin test() y aportar el resultado. Si el resultado es true, encuentre la forma de cambiar el orden de enlazado de su enlazador y cmbielo hasta que el resultado sea false. 33. Repare el problema del Ejercicio 32 utilizando la tcnica uno mostrada en este libro. 34. Repare el problema del Ejercicio 32 utilizando la tcnica dos mostrada en este libro. 35. Sin incluir ningn archivo de cabecera, declare la funcin puts() de la Librera Estndar de C. Llame a esta funcin desde main().
284
A causa de esta "caracterstica" de C, puede utilizar cualquier tipo como si de otro se tratara sin ningn aviso por parte del compilador. C++ no permite hacer esto; el compilador da un mensaje de error, y si realmente quiere utilizar un tipo como otro diferente, debe hacerlo explcitamente, tanto para el compilador como para el lector, haciendo molde (denominado cast en ingls). (En el captulo 3 se habl sobre la sintaxis mejorada del molde "explcito".)
285
En la linea (1), el compilador asigna la cantidad necesaria de memoria, la inicializa con el valor 12, y liga la referencia a esa memoria. Lo importante es que una referencia debe estar ligada a la memoria de alguien. Cuando se accede a una referencia, se est accediendo a esa memoria. As pues, si escribe las lineas (2) y (3), se incrementar x cuando se incremente a, tal como se muestra en el main(). Lo ms fcil es pensar que una referencia es como un puntero de lujo. La ventaja de este "puntero" es que nunca hay que preguntarse si ha sido inicializado (pues el compilador lo impone) y si hay que destruirlo (pues el compilador lo hace). Hay que seguir unas determinadas reglas cuando se utilizan referencias: 1. Cuando una referencia se crea, se ha de inicializar. (Los punteros pueden inicializarse en cualquier momento.) 2. Una vez una referencia se inicializa ligndola a un objeto, no se puede ligar a otro objeto. (Los punteros se pueden apuntar a otro objeto en cualquier momento.) 3. No se pueden tener referencias con valor nulo. Siempre ha de suponer que una referencia est conectada a una trozo de memoria ya asignada.
286
La llamada a f() no tiene la ventaja ni la claridad que la utilizacin de referencias, pero est claro que se est pasando una direccin mediante un puntero. En la llamada a g(), tambin se pasa una direccin (mediante una referencia), pero no se ve.
Referencias constantes
El argumento referencia en Reference.cpp funciona solamente en caso de que el argumento no sea un objeto constante (es decir, no sea const). Si fuera un objeto constante, la funcin g() no aceptara el argumento, lo cual es positivo porque la funcin modicara el argumento que est fuera del mbito de la funcin. Si sabe que la funcin respetar el valor "constante" de un objeto, el hecho de que el argumento sea una referencia constante permitir que la funcin se pueda utilizar en cualquier situacin. Esto signica que para tipos predenidos, la funcin no modicar el argumento, y para tipos denidos por el usuario, la funcin llamar solamente a mtodos constantes, y no modicara ningn atributo pblico. La utilizacin de referencias constantes en argumentos de funciones es especialmente importante porque una funcin puede recibir un objeto temporal. ste podra haber sido creado como valor de retorno de otra funcin o explcitamente por el usuario de la funcin. Los objetos temporales son siempre constantes. As, si no utiliza una referencia constante, el compilador se quejar. Como ejemplo muy simple:
//: C11:ConstReferenceArguments.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Passing references as const void f(int&) {} void g(const int&) {} int main() { //! f(1); // Error g(1); }
La llamada f(1) provoca un error en tiempo de compilacin porque el compilador debe primero crear una referencia. Lo hace asignando memoria para un int, inicinlizndolo a uno y generando la direccin de memoria para ligarla a la referencia. La memoria debe ser constante porque no tendra sentido cambiarlo: no puede cambiarse de nuevo. Puede hacer la misma suposicin para todos los objetos temporales: son inaccesibles. Es importante que el compilador le diga cundo est intentando cambiar algo de este estilo porque podra perder informacin. 287
Referencias a puntero
En C, si desea modicar el contenido del puntero en s en vez de modicar a lo que apunta, la declaracin de la funcin sera:
void f(int**);
La sintaxis es ms clara con las referencias en C++. El argumento de la funcin pasa a ser de una referencia a un puntero, y as no ha de manejar la direccin del puntero. As,
//: C11:ReferenceToPointer.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; void increment(int*& i) { i++; } int main() { int* i = 0; cout << "i = " << i << endl; increment(i); cout << "i = " << i << endl; }
Al ejecutar este programa se observa que el puntero se incrementa en vez de incrementar a lo que apunta.
cmo sabe el compilador cmo pasar y retornar esas variables? Simplemente lo sabe! El rango de tipo con los que trata es tan pequeo (char,int, oat, double, y sus variaciones), que tal informacin ya est dentro del compilador. Si el compilador generara cdigo ensamblador y quisiera ver qu sentencias se generan por llamar a la funcin f(), tendra el equivalente a:
push b push a call f() add sp,4 mov g, register a
Este cdigo se ha simplicado para hacerlo genrico; las expresiones b y a seran diferentes dependiendo de si las variables son globales (en cuyo caso seran _b y _a) o locales (el compilador las pondra en la pila). Esto tambin es cierto para g. La sintaxis de la llamada a f() dependera de su gua de estilo, y "register a" dependera de cmo su ensamblador llama a los registros de la CPU. A pesar de la simplicacin, la lgica del cdigo sera la misma. Tanto en C como en C++, primero se ponen los argumentos en la pila de derecha a izquierda, y luego se llama a la funcin. El cdigo de llamada es responsable de retirar los argumentos de la pila (lo cual explica la sentencia add sp,4). Pero tenga en cuenta que cuando se pasan argumentos por valor, el compilador simplemente pone copias en la pila (sabe el tamao de cada uno, por lo que los puede copiar). El valor de retorno de f() se coloca en un registro. Como el compilador sabe lo que se est retornando, porque la informacin del tipo ya est en el lenguaje, puede retornarlo colocndolo en un registro. En C, con tipos primitivos, el simple hecho de copiar los bits del valor es equivalente a copiar el objeto.
289
La conversin a cdigo ensamblador es un poco ms complicada porque la mayora de los compiladores utilizan funciones "auxiliares" en vez de inline. En la funcin main(), la llamada a bigfun() empieza como debe: se coloca el contenido de B en la pila. (Aqu podra ocurrir que algunos compiladores carguen registros con la direccin y tamao de Big y luego una funcin auxiliar se encargue de colocar a Big en la pila.) En el anterior fragmento de cdigo fuente, lo nico necesario antes de llamar a la funcin es colocar los argumentos en la pila. Sin embargo, en el cdigo ensamblador de PassingBigStructures.cpp se ve una accin de ms: la direccin de B2 se coloca en la pila antes de hacer la llamada a la funcin aunque, obviamente, no sea un argumento. Para entender qu pasa, necesita entender las restricciones del compilador cuando llama a una funcin.
El cdigo generado por el resto de la funcin espera que la memoria tenga esta disposicin para que pueda utilizar los argumentos y las variables locales sin tocar la direccin al punto de retorno. Llmese a este bloque de memoria, que es todo lo que una funcin necesita cuando se le llama, mbito de la funcin. Podra creer que es razonable intentar el retorno de valores mediante la utilizacin de la pila. El compilador simplemente los colocara all y la funcin devolvera un desplazamiento que indicara cundo empieza el valor de retorno.
Re-entrada
Este problema ocurre porque las funciones en C y C++ pueden sufrir interrupciones; esto es, los lenguajes han de ser (y de hecho son) re-entrantes. Tambin permiten llamadas a funciones recursivas. Esto quiere decir que en cualquier punto de ejecucin de un programa puede sufrir una interrupcin sin que el programa se vea afectado por ello. Obviamente la persona que escribe la rutina de servicio de interrupciones (ISR) es responsable de guardar y restaurar todos los registros que se utilicen en la ISR. Pero si la ISR necesita utilizar la pila, ha de hacerlo con seguridad. (Piense que una ISR es como una funcin normal sin argumentos y con valor de retorno void que guarda y restaura el estado de la CPU. Una llamada a una ISR se provoca con un evento hardware, y no con una llamada dentro del programa de forma explcita.) Ahora imagine que pasara si una funcin normal intentara retornar valores en la pila. No puede tocar la pila por encima del la direccin del punto de retorno, as que la funcin tendra que colocar los valores de retorno debajo de la direccin del punto de retorno. Pero cuando el RETURN del ensamblador se ejecuta, el puntero de la pila debera estar apuntando a la direccin del punto de retorno (o justo debajo, depende de la mquina), as que la funcin debe subir el puntero de la pila, desechando todas las variables locales. Si intenta retornar valores con la pila por debajo de la direccin del punto de retorno, en ese momento es vulnerable a una interrupcin. La ISR escribira encima de los valores de retorno para colocar su direccin de punto de retorno y sus variables locales. Para resolver este problema, el que llama a la funcin podra ser responsable de asignar la memoria extra en la pila para los valores de retorno antes de llamar a la funcin. Sin embargo, C no se dise de esta manera y C++ ha de ser compatible. Como pronto ver, el compilador de C++ utiliza un esquema ms ecaz. Otra idea sera retornar el valor utilizando un rea de datos global, pero tampoco funcionara. La re-entrada signica que cualquier funcin puede ser una rutina de interrupcin para otra funcin, incluida la funcin en la que 290
11.3. El constructor de copia ya se est dentro. Por lo tanto, si coloca un valor de retorno en un rea global, podra retornar a la misma funcin, lo cual sobreescribira el valor de retorno. La misma lgica se aplica a la recurrencia. Los registros son el nico lugar seguro para devolver valores, as que se vuelve al problema de qu hacer cuando los registros no son lo sucientemente grandes para contener el valor de retorno. La respuesta es colocar la direccin de la ubicacin del valor de retorno en la pila como uno de los argumentos de la funcin, y dejar que la funcin copie la informacin que se devuelve directamente en la ubicacin. Esto no solo soluciona todo los problemas, si no que adems es ms ecaz. sta es la razn por la que el compilador coloca la direccin de B2 antes de llamar a bigfun en la funcin main() de PassingBigStructures.cpp. Si mirara bigfun() en el cdigo ensamblador, observara que la funcin espera este argumento escondido y copia el valor de retorno ah.
291
Captulo 11. Las referencias y el constructor de copia La clase HowMany contiene un entero esttico llamado objectCount y un mtodo esttico llamado print() para presentar el valor de objectCount, junto con argumento de mensaje optativo. El constructor incrementa objectCount cada vez que se crea un objeto, y el destructor lo disminuye. Sin embargo la salida no es lo que uno esperara:
after construction of h: objectCount = 1 x argument inside f(): objectCount = 1 ~HowMany(): objectCount = 0 after call to f(): objectCount = 0 ~HowMany(): objectCount = -1 ~HowMany(): objectCount = -2
Despus de crear h, el contador es uno, lo cual est bien. Pero despus de la llamada a f() se esperara que el contador estuviera a dos, porque h2 est ahora tambin dentro de mbito. Sin embargo, el contador es cero, lo cual indica que algo ha ido muy mal. Esto se conrma por el hecho de que los dos destructores, llamados al nal de main(), hacen que el contador se pase a negativo, algo que nunca debera ocurrir. Mire lo que ocurre dentro de f() despus de que el argumento se pase por valor. Esto quiere decir que el objeto original h existe fuera del mbito de la funcin y, por otro lado, hay un objeto de ms dentro del mbito de la funcin, el cual es la copia del objeto que se pas por valor. El argumento que se pas utiliza el primitivo concepto de copia bit a bit de C, pero la clase C++ HowMany necesita inicializarse correctamente para mantener su integridad. Por lo tanto, se demuestra que la copia bit a bit no logra el efecto deseado. Cuando el objeto local se sale de mbito al salir de la funcin f(), se llama a su destructor, lo cual disminuye objectCount, y por lo tanto el objectCount se pone a cero. La creacin de h2 se realiza tambin mediante la copia bit a bit, as que tampoco se llama al constructor, y cuando h y h2 se salen de mbito, sus destructores causan el valor negativo en objectCount.
h2, un objeto que no estaba creado anteriormente, se crea a partir del valor que retorna f(), y otra vez un nuevo objeto se crea de otro ya existente. El compilador supone que la creacin ha de hacerse con una copia bit a bit, lo que en muchos casos funciona bien, pero en HowMany no funciona porque la inicializacin va ms all de una simple copia. Otro ejemplo muy comn ocurre cuando la clase contiene punteros pues, a qu deben apuntar? debera copiar slo los punteros o debera asignar memoria y que apuntaran a ella? Afortunadamente, puede intervenir en este proceso y prevenir que el compilador haga una copia bit a bit. Se soluciona deniendo su propia funcin siempre que el compilador necesite crear un nuevo objeto de otro ya existente. Lgicamente, est creando un nuevo objeto, por lo que esta funcin es un constructor, y tambin el nico argumento del constructor tiene que ver con el objeto del que se pretende partir para crear el nuevo. Pero no puede pasar ese objeto por valor al constructor porque usted est intentando denir la funcin que maneja el paso por valor, y, por otro lado, sintcticamente no tiene sentido pasar un puntero porque, despus de todo, est creando un objeto de otro ya existente. Aqu es cuando las referencias vienen al rescate, y puede utilizar la referencia del objeto origen. Esta funcin se llama el constructor copia, que tambin se lo puede encontrar como X(X&), que es el constructor copia de una clase denominada X. Si crea un constructor copia, el compilador no realizar una copia bit a bit cuando cree un nuevo objeto de otro ya existente. El compilador siempre llamar al constructor copia. Si no crea el constructor copia, el compilador har algo sensato, pero usted tiene la opcin de tener control total del proceso. Ahora es posible solucionar el problema en HowMany.cpp: 292
//: C11:HowMany2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The copy-constructor #include <fstream> #include <string> using namespace std; ofstream out("HowMany2.out"); class HowMany2 { string name; // Object identifier static int objectCount; public: HowMany2(const string& id = "") : name(id) { ++objectCount; print("HowMany2()"); } ~HowMany2() { --objectCount; print("~HowMany2()"); } // The copy-constructor: HowMany2(const HowMany2& h) : name(h.name) { name += " copy"; ++objectCount; print("HowMany2(const HowMany2&)"); } void print(const string& msg = "") const { if(msg.size() != 0) out << msg << endl; out << \t << name << ": " << "objectCount = " << objectCount << endl; } }; int HowMany2::objectCount = 0; // Pass and return BY VALUE: HowMany2 f(HowMany2 x) { x.print("x argument inside f()"); out << "Returning from f()" << endl; return x; } int main() { HowMany2 h("h"); out << "Entering f()" << endl; HowMany2 h2 = f(h); h2.print("h2 after call to f()"); out << "Call f(), no return value" << endl; f(h); out << "After call to f()" << endl; }
Hay unas cuantas cosas nuevas para que pueda hacerse una idea mejor de lo que pasa. Primeramente, el string name hace de identicador de objeto cuando se imprima en la salida. Puede poner un identicador (normalmente el nombre del objeto) en el constructor para que se copie en name utilizando el constructor con un string como argumento. Por defecto se crea un string vaco. El constructor incrementa objectCount y el destructor lo disminuye, igual que en el ejemplo anterior. 293
Captulo 11. Las referencias y el constructor de copia Lo siguiente es el constructor copia, HowMany2(const HowMany2&). El constructor copia crea un objeto solamente desde otro ya existente, as que copia en name el identicador del objeto origen, seguido de la palabra "copy", y as puede ver de dnde procede. Si mira atentamente, ver que la llamada name(h.name) en la lista de inicializadores del constructor est llamando al constructor copia de la clase string. Dentro del constructor copia, se incrementa el contador igual que en el constructor normal. Esto quiere decir que obtendr un contador de objetos preciso cuando pase y retorne por valor. La funcin print() se ha modicado para imprimir en la salida un mensaje, el identicador del objeto y el contador de objetos. Como ahora accede al miembro name de un objeto en particular, ya no puede ser un mtodo esttico. Dentro de main() puede ver que hay una segunda llamada a f(). Sin embargo esta llamada utiliza la caracterstica de C para ningunear el valor de retorno. Pero ahora que sabe cmo se retorna el valor (esto es, cdigo dentro de la funcin que maneja el proceso de retorno poniendo el resultado en un lugar cuya direccin se pasa como un argumento escondido), podra preguntarse qu ocurre cuando se ningunea el valor de retorno. La salida del programa mostrar alguna luz sobre el asunto. Pero antes de mostrar la salida, he aqu un pequeo programa que utiliza iostreams para aadir nmeros de lnea a cualquier archivo:
//: C11:Linenum.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{T} Linenum.cpp // Add line numbers #include "../require.h" #include <vector> #include <string> #include <fstream> #include <iostream> #include <cmath> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1, "Usage: linenum file\n" "Adds line numbers to file"); ifstream in(argv[1]); assure(in, argv[1]); string line; vector<string> lines; while(getline(in, line)) // Read in entire file lines.push_back(line); if(lines.size() == 0) return 0; int num = 0; // Number of lines in file determines width: const int width = int(log10((double)lines.size())) + 1; for(int i = 0; i < lines.size(); i++) { cout.setf(ios::right, ios::adjustfield); cout.width(width); cout << ++num << ") " << lines[i] << endl; } }
El archivo se pasa a un vector(string), utilizando el mismo cdigo fuente que se ha visto anteriormente en este libro. Cuando se pone los nmeros de lnea, nos gustara que todas las lneas estuvieran alineadas, y esto necesita conocer el nmero de lneas en el archivo para que sea coherente. Se puede conocer el nmero de lneas con vector::size(), pero lo que realmente necesitamos es conocer si hay ms lneas de 10, 100, 1000, etc. Si se utiliza el logaritmo en base 10 sobre el nmero de lneas en el archivo, se trunca a un entero y se aade uno al valor resultante y eso determinar el ancho mximo en dgitos que un nmero de lnea puede tener. 294
11.3. El constructor de copia Ntese que hay un par de llamadas extraas dentro del bucle for: setf() y width(). Hay llamadas de ostream que permiten controlar, en este caso, la justicacin y anchura de la salida. Sin embargo se debe llamar cada vez que una lnea se imprime y por eso estn dentro del bucle for. El Volumen 2 de este libro tiene un captulo entero que explica los iostreams y que cuenta ms sobre estas llamadas as como otras formas de controlar los iostreams. Cuando se aplica Linenum.cpp abactor HowMany2.out, resulta:
1) HowMany2() 2) h: objectCount = 1 3) Entering f() 4) HowMany2(const HowMany2&) 5) h copy: objectCount = 2 6) x argument inside f() 7) h copy: objectCount = 2 8) Returning from f() 9) HowMany2(const HowMany2&) 10) h copy copy: objectCount 11) ~HowMany2() 12) h copy: objectCount = 2 13) h2 after call to f() 14) h copy copy: objectCount 15) Call f(), no return value 16) HowMany2(const HowMany2&) 17) h copy: objectCount = 3 18) x argument inside f() 19) h copy: objectCount = 3 20) Returning from f() 21) HowMany2(const HowMany2&) 22) h copy copy: objectCount 23) ~HowMany2() 24) h copy: objectCount = 3 25) ~HowMany2() 26) h copy copy: objectCount 27) After call to f() 28) ~HowMany2() 29) h copy copy: objectCount 30) ~HowMany2() 31) h: objectCount = 0
= 3
= 2
= 4
= 2
= 1
Como se esperaba, la primera cosa que ocurre es que para h se llama al constructor normal, el cual incrementa el contador de objetos a uno. Pero entonces, mientras se entra en f(), el compilador llama silenciosamente al constructor copia para hacer el paso por valor. Se crea un nuevo objeto, que es la copia de h (y por tanto tendr el identicador "h copy") dentro del mbito de la funcin f(). As pues, el contador de objetos se incrementa a dos, por cortesa del constructor copia. La lnea ocho indica el principio del retorno de f(). Pero antes de que se destruya la variable local "h copy" (pues sale de mbito al nal de la funcin), se debe copiar al valor de retorno, que es h2. Por tanto h2, que no estaba creado previamente, se crea de un objeto ya existente (la variable local dentro de f()) y el constructor copia vuelve a utilizarse en la lnea 9. Ahora el identicador de h2 es "h copy copy" porque copi el identicador de la variable local de f(). Cuando se devuelve el objeto, pero antes de que la funcin termine, el contador de objetos se incrementa temporalmente a tres, pero la variable local con identicador "h copy" se destruye, disminuyendo a dos. Despus de que se complete la llamada a f() en la lnea 13, slo hay dos objetos, h y h2, y puede comprobar, de hecho, que h2 termin con el identicador "h copy copy".
Objetos temporales
En la lnea 15 se empieza la llamada a f(h), y esta vez ningunea el valor de retorno. Puede ver en la lnea 16 que el constructor copia se llama, igual que antes, para pasar el argumento. Y tambin, igual que antes, en la lnea 21 se llama al constructor copia para el valor de retorno. Pero el constructor copia necesita una direccin con la que trabajar como su destino ( es decir, para trabajar con el puntero this). De dnde procede esta direccin? Esto prueba que el compilador puede crear un objeto temporal cuando lo necesita para evaluar una expresin adecuadamente. En este caso l crea uno que ni siquiera se le ve actuar como el destino para el ninguneado valor 295
Captulo 11. Las referencias y el constructor de copia que f() retorna. El tiempo de vida de este objeto temporal es tan corto como sea posible para que el programa no se llene de objetos temporales esperando a ser destruidos, lo cual provocara la utilizacin inecaz de recursos valiosos. En algunos casos, el objeto temporal podra pasarse inmediatamente a otra funcin, pero en este caso no se necesita despus de la llamada a la funcin, as que tan pronto como la funcin termina llamando al destructor del objeto local (lneas 23 y 24), el objeto temporal tambin se destruye (lneas 25 y 26). Finalmente, de la lnea 28 a la lnea 31, se destruye el objeto h2, seguido de h y el contador de objetos vuelve a cero.
296
La clase WithCC contiene un constructor copia, que simplemente anuncia que ha sido llamado, y esto muestra un asunto interesante: dentro de la clase Composite se crea un objeto tipo WithCC utilizando el constructor por defecto. Si WithCC no tuviera ningn constructor, el compilador creara uno por defecto automticamente, el cual, en este caso, no hara nada. No obstante, si aade un constructor por defecto, al compilador se le est diciendo que ha de utilizar los constructores disponibles, por lo que no crea ningn constructor por defecto y se quejar a no ser que explcitamente cree un constructor por defecto, como se hizo en WithCC. La clase WoCC no tiene constructor copia, pero su constructor mantendr un string interno imprimible por la funcin print(). La lista de inicializadores en el constructor de Composite llama explcitamente a este constructor (presentado brevemente en el Captulo 8 y tratado completamente en el Captulo 14). La razn de esto se ver posteriormente. La clase Composite tiene miembros objeto tanto de WithCC como de WoCC (note que el objeto interno wocc se inicializa en la lista de inicializadores del constructor de Composite, como debe ser), pero no estn inicializados explcitamente en el constructor copia. Sin embargo un objeto Composite se crea en main() utilizando el constructor copia:
Composite c2 = c;
El compilador ha creado un constructor copia para Composite automticamente, y la salida del programa revela la manera en que se crea:
Contents of c: Composite() Calling Composite copy-constructor WithCC(WithCC&) Contents of c2: Composite()
Para la creacin de un constructor copia para una clase que utiliza composicin (y herencia, que se trata en el Captulo 14), el compilador llama a todos los constructores copia de todos los miembros objeto y de las clases base de manera recursiva. Es decir, si el miembro objeto tambin contiene otro objeto, tambin se llama a su constructor copia. En el ejemplo, el compilador llama al constructor copia de WithCC. La salida muestra que se llama a este constructor. Como WoCC no tiene constructor copia, el compilador crea uno que realiza simplemente una copia bit a bit para que el constructor copia de Composite lo pueda llamar. La llamada a Composite::print() en main() muestra que esto ocurre, porque el contenido de c2.wocc es idntico al contenido de c.wocc. El proceso que realiza el compilador para crear un constructor copia se denomina inicializacin inteligente de miembros(memberwise initialization). Se recomienda denir constructor copia propio en vez del que hace el compilador. Esto garantiza que estar bajo control.
utilizando const
Parece que a la funcin se le pasa por valor, lo que sugiere que el argumento que se pasa no se modica. A causa de esto, es probablemente ms seguro, desde el punto de vista de mantenimiento del cdigo fuente, utilizar punteros que pasen la direccin del argumento que se desee modicar. Si siempre pasa direcciones como referencias constantes excepto cuando intenta modicar el argumento que se pasa a travs de la direccin, donde pasara un puntero no constante, entonces es ms fcil para el lector seguir el cdigo fuente. 298
Ahora suponga que tiene un puntero normal que se llama ip y que apunta a un entero. Para acceder a lo que ip est apuntando, ha de estar precedido por un *:
*ip=4;
Finalmente, se preguntar qu pasa si tiene un puntero que est apuntando a algo que est dentro de un objeto, incluso si lo que realmente representa es un desplazamiento dentro del objeto. Para acceder a lo que est apuntando, debe preceder el puntero con *. Pero como es un desplazamiento dentro de un objeto, tambin ha de referirse al objeto con el que estamos tratando. As, el * se combina con el objeto. Por tanto, la nueva sintaxis se escribe ->* para un puntero que apunta a un objeto, y .* para un objeto o referencia, tal como esto:
objectPointer->*pointerToMember = 47; object.*pointerToMember = 47;
Pero, cul es la sintaxis para denir el pointerToMember? Pues como cualquier puntero, tiene que decir el tipo al que apuntar, por lo que se utilizara el * en la denicin. La nica diferencia es que debe decir a qu clase de objetos este miembro puntero apuntar. Obviamente, esto se consigue con el nombre de la clase y el operador de resolucin de mbito. As,
int ObjectClass::*pointerToMember;
dene una variable miembro puntero llamado pointerToMember que apunta a cualquier entero dentro de ObjectClass. Tambin puede inicializar el miembro puntero cuando se dene (o en cualquier otro momento):
int ObjectClass::*pointerToMember = &ObjectClass::a;
Realmente no existe una "direccin" de ObjectClass::a porque se est reriendo a la clase y no a un objeto de esa clase. As, &ObjectClass::a se puede utilizar slo con la sintaxis de un puntero a miembro. He aqu un ejemplo que muestra cmo crear y utilizar punteros a atributos: 299
//: C11:PointerToMemberData.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class Data { public: int a, b, c; void print() const { cout << "a = " << a << ", b = " << b << ", c = " << c << endl; } }; int main() { Data d, *dp = &d; int Data::*pmInt = &Data::a; dp->*pmInt = 47; pmInt = &Data::b; d.*pmInt = 48; pmInt = &Data::c; dp->*pmInt = 49; dp->print(); }
Obviamente, son muy desagradables de utilizar en cualquier lugar excepto para caso especiales (que es exactamente para lo que crearon). Adems, los punteros a miembro son bastante limitados: pueden asignarse solamente a una ubicacin especca dentro de una clase. No podra, por ejemplo, incrementarlos o compararlos tal como puede hacer con punteros normales.
11.4.1. Funciones
Un ejercicio similar se produce con la sintaxis de puntero a miembro para mtodos. Un puntero a una funcin (presentado al nal del Captulo 3) se dene como:
int (*fp)(float);
Los parntesis que engloban a (*fb) son necesarios para que fuercen la evaluacin de la denicin apropiadamente. Sin ellos sera una funcin que devuelve un int*. Los parntesis tambin desempean un papel importante cuando denen y utilizan punteros a mtodos. Si tiene una funcin dentro de una clase, puede denir un puntero a ese mtodo insertando el nombre de la clase y el operador de resolucin de mbito en una denicin normal de puntero a funcin:
//: C11:PmemFunDefinition.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Simple2 { public: int f(float) const { return 1; } }; int (Simple2::*fp)(float) const;
300
En la denicin de fp2 puede verse que el puntero a un mtodo puede inicializarse cuando se crea, o en cualquier otro momento. A diferencia de las funciones que no son miembros, el & no es optativo para obtener la direccin de un mtodo. Sin embargo, se puede dar el identicador de la funcin sin la lista de argumentos, porque la sobrecarga se resuelve por el tipo de puntero a miembro.
Un ejemplo
Lo interesante de un puntero es que se puede cambiar el valor del mismo para apuntar a otro lugar en tiempo de ejecucin, lo cual proporciona mucha exibilidad en la programacin porque a travs de un puntero se puede cambiar el comportamiento del programa en tiempo de ejecucin. Un puntero a miembro no es diferente; le permite elegir un miembro en tiempo de ejecucin. Tpicamente, sus clases slo tendrn mtodos visibles pblicamente (los atributos normalmente se consideran parte de la implementacin que va oculta), as que el siguiente ejemplo elige mtodos en tiempo de ejecucin.
//: C11:PointerToMemberFunction.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class Widget { public: void f(int) const void g(int) const void h(int) const void i(int) const };
{ { { {
} } } }
int main() { Widget w; Widget* wp = &w; void (Widget::*pmem)(int) const = &Widget::h; (w.*pmem)(1); (wp->*pmem)(2); }
Por supuesto, no es razonable esperar que el usuario casual cree expresiones tan complejas. Si el usuario necesita manipular directamente un puntero a miembro, los typedef vienen al rescate. Para dejar an mejor las cosas, puede utilizar un puntero a funcin como parte del mecanismo interno de la implementacin. He aqu un ejemplo que utiliza un puntero a miembro dentro de la clase. Todo lo que el usuario necesita es pasar un nmero para elegir una funcin. 1
//: C11:PointerToMemberFunction2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std;
1
301
class Widget { void f(int) const { cout << "Widget::f()\n"; } void g(int) const { cout << "Widget::g()\n"; } void h(int) const { cout << "Widget::h()\n"; } void i(int) const { cout << "Widget::i()\n"; } enum { cnt = 4 }; void (Widget::*fptr[cnt])(int) const; public: Widget() { fptr[0] = &Widget::f; // Full spec required fptr[1] = &Widget::g; fptr[2] = &Widget::h; fptr[3] = &Widget::i; } void select(int i, int j) { if(i < 0 || i >= cnt) return; (this->*fptr[i])(j); } int count() { return cnt; } }; int main() { Widget w; for(int i = 0; i < w.count(); i++) w.select(i, 47); }
En la interfaz de la clase y en main(), puede observar que toda la implementacin, funciones incluidas, han sido puestas como privadas. El cdigo ha de pedir el count() de las funciones. De esta manera, el que implementa la clase puede cambiar la cantidad de funciones en la implementacin por debajo sin que afecte al cdigo que utilice la clase. La inicializacin de los punteros a miembro en el constructor para que estn sobre especicado. No debera ser capaz de poner
fptr[1] = &g;
porque el nombre g es un mtodo, la cual est en el mbito de la clase? El problema aqu es que no sera conforme a la sintaxis de puntero a miembro. As todo el mundo, incluido el compilador, puede imaginarse qu est pasando. De igual forma, cuando se accede al contenido del puntero a miembro, parece que
(this->*fptr[i])(j);
tambin est sobre especicado; this parece redundante. La sintaxis necesita que un puntero a miembro siempre est ligado a un objeto cuando se accede al contenido al que que apunta.
11.5. Resumen
Los puntero en C++ son casi idnticos a los punteros en C, lo cual es bueno. De otra manera, gran cantidad de cdigo C no compilara bajo C++. Los nicos errores en tiempo de compilacin sern aqullos que realicen asignaciones peligrosas. Esos errores pueden eliminarse con una simple (pero explcito!) molde al tipo deseado. C++ tambin aade la referencia de Algol y Pascal, que es como un puntero constante que el compilador hace que se acceda directamente al contenido al que apunta. Una referencia contiene una direccin, pero lo trata como un objeto. Las referencias son esenciales para una sintaxis clara con la sobrecarga de operadores (el tema del siguiente captulo), pero tambin proporcionan mejoras sintcticas para el paso y retorno de objetos en funciones normales. 302
11.6. Ejercicios El constructor copia coge una referencia de un objeto ya existente del mismo tipo que el argumento, y lo utiliza para la creacin de un nuevo objeto procedente del ya existente. El compilador llama automticamente al constructor copia cuando pasa o retorna un objeto por valor. Aunque el compilador crea un constructor copia automticamente, si cree que su clase necesita uno, debera denirlo para asegurar un comportamiento apropiado. Si no desea que el objeto se pase o retorne por valor, debera crear un constructor copia privado. Los punteros a miembro tienen la misma capacidad que los punteros normales: puede elegir una regin de memoria particular (atributo o mtodo) en tiempo de ejecucin. Los punteros a miembro funcionan con los miembros de una clase en vez de datos o funciones globales. Tiene la suciente exibilidad para cambiar el comportamiento en tiempo de ejecucin.
11.6. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Convierta el fragmento de cdigo "bird & rock" al principio de este captulo a un programa C (utilizando structuras para los tipos de datos), y que compile. Ahora intente compilarlo con un compilador de C++ y vea qu ocurre. 2. Coja los fragmentos de cdigo al principio de la seccin titulada "Referencias en C++" y pngalos en un main(). Aada sentencias para imprimir en la salida para que pueda demostrar usted mismo que las referencias son como punteros que acceden automticamente a lo que apuntan. 3. Escriba un programa en el cual intente (1) Crear una referencia que no est inicializada cuando se crea. (2) Cambiar una referencia para que se reera a otro objeto despus de que se haya inicializado. (3) Crear una referencia nula. 4. Escriba una funcin que tenga un puntero por argumento, modique el contenido de lo que el puntero apunta, y retorno ese mismo contenido como si de una referencia se tratara. 5. Cree una nueva clase con algunos mtodos, y haga que el objeto que sea apuntado por el argumento del Ejercicio 4. Haga que el puntero pasado por como argumento y algunos mtodos sean constantes y pruebe que slo puede llamar a los mtodos constantes dentro de su funcin. Haga que el argumento de su funcin sea una referencia en vez de un puntero. 6. Coja los fragmentos de cdigo al principio de la seccin "referencias a puntero" y convirtalos en un programa. 7. Cree una funcin que tome como argumento una referencia a un puntero que apunta a otro puntero y modique ese argumento. En main(), llame a la funcin. 8. Cree una funcin que toma un argumento del tipo char& y lo modica. En el main() imprima a la salida una variable char, llame a su funcin con esa variable e imprima la variable de nuevo para demostrar que ha sido cambiada. Cmo afecta esto a la legibilidad del programa? 9. Escriba una clase que tiene un mtodo constante y otra que no lo sea. Escriba tres funciones que toman un objeto de esa clase como argumento; la primera lo toma por valor, la segunda lo toma por referencia y la tercera lo toma mediante una referencia constante. Dentro de las funciones, intente llamar a las dos funciones de su clase y explique los resultados. 10. (Algo difcil) Escriba una funcin simple que toma un entero como argumento, incrementa el valor, y lo retorna. En el main(), llame a su funcin. Intente que el compilador genere el cdigo ensamblador e intente entender cmo los argumentos se pasan y se retornan, y cmo las variables locales se colocan en la pila. 11. Escriba una funcin que devuelva un double. Genere el cdigo ensamblador y explique cmo se retorna el valor. 12. Genere el cdigo ensamblador de PassingBigStructures.cpp. Recorra y desmitique la manera en que su compilador genera el cdigo para pasar y devolver estructuras grandes. 13. Escriba una simple funcin recursiva que disminuya su argumento y retorne cero si el argumento llega a cero, o que se vuelva a llamar. Genere el cdigo ensamblador para esta funcin y explique la forma en el el compilador implementa la recurrencia. 303
Captulo 11. Las referencias y el constructor de copia 14. Escriba cdigo para demostrar que el compilador genera un constructor copia automticamente en caso de que usted no lo implemente. Demuestre que el constructor copia generado por el compilador realiza una copia bit a bit de tipos primitivos y llama a los constructores copia de los tipos denidos por el usuario. 15. Escriba una clase que en el constructor copia se anuncia a s mismo a travs de un cout. Ahora cree una funcin que pasa un objeto de su nueva clase por valor y otro ms que crea un objeto local de su nueva clase y lo devuelve por valor. Llame a estas funciones para demostrar que el constructor copia es, en efecto, llamado cuando se pasan y retornan objetos por valor. 16. Cree un objeto que contenga un double*. Que el constructor inicialice el double* llamando a new double y asignando un valor. Entonces, que el destructor imprima el valor al que apunta, asigne ese valor a -1, llame a delete para liberar la memoria y que ponga el puntero a cero. Ahora cree una funcin que tome un objeto de su clase por valor, y llame a esta funcin en el main(). Qu ocurre? Solucione el problema implementando un constructor copia. 17. Cree una clase con un constructor que parezca un constructor copia, pero que tenga un argumento de ms con un valor por defecto. Muestre que an as se utiliza como constructor copia. 18. Cree una clase con un constructor copia que se anuncie a s mismo (es decir que imprima por la salida que ha sido llamado). Haga una segunda clase que contenga un objeto miembro de la primera clase, pero no cree un constructor copia. Muestre que el constructor copia, que el compilador genera automticamente en la segunda clase, llama al constructor copia de la primera. 19. Cree una clase muy simple, y una funcin que devuelva un objeto de esa clase por valor. Cree una segunda funcin que tome una referencia de un objeto de su clase. Llame a la segunda funcin pasndole como argumento una llamada a la primera funcin, y demuestre que la segunda funcin debe utilizar una referencia constante como argumento. 20. Cree una clase simple sin constructor copia, y una simple funcin que tome un objeto de esa clase por valor. Ahora cambie su clase aadindola una declaracin (slo declare, no dena) privada de un constructor copia. Explique lo que ocurre cuando compila la funcin. 21. Este ejercicio crea una alternativa a la utilizacin del constructor copia. Cree una clase X y declare (pero no dena) un constructor copia privado. Haga una funcin clone() pblica como un mtodo constante que devuelve una copia del objeto creado utilizando new. Ahora escriba una funcin que tome como argumento un const X& y clone una copia local que puede modicarse. El inconveniente de esto es que usted es responsable de destruir explcitamente el objeto clonado (utilizando delete) cuando ya haya terminado con l. 22. Explique qu est mal en Mem.cpp y MemTest.cpp del Captulo 7. Solucione el problema. 23. Cree una clase que contenga un double y una funcin print() que imprima el double. Cree punteros a miembro tanto para el atributo como al mtodo de su clase. Cree un objeto de su clase y un puntero a ese objeto, y manipule ambos elementos de la clase a travs de los punteros a miembro, utilizando tanto el objeto como el puntero al objeto. 24. Cree una clase que contenga un array de enteros. Puede recorrer el array mediante un puntero a miembro? 25. Modique PmemFunDenition.cpp aadiendo un mtodo f() sobrecargada (puede determinar la lista de argumentos que provoque la sobrecarga). Ahora haga un segundo puntero a miembro, asgnelo a la versin sobrecargada de f(), y llame al mtodo a travs del puntero. Cmo sucede la resolucin de funcin sobrecargada en este caso? 26. Empiece con la funcin FunctionTable.cpp del Captulo 3. Cree una clase que contenga un vector de punteros a funciones, con mtodos add() y remove() para aadir y quitar punteros a funcin. Aada una funcin denominada run() que recorra el vector y llame a todas la funciones. 27. Modique el Ejercicio 27 para que funcione con punteros a mtodos.
304
que tengan signicado. Slo una expresin que contenga tipos de datos denidos por el usuario podr tener operadores sobrecargados.
12.2. Sintaxis
Denir un operador sobrecargado es como denir una funcin, pero el nombre de esa funcin es operator@ en la que arroba representa el operador que est siendo sobrecargado. El nmero de argumentos en la lista de 305
Captulo 12. Sobrecarga de operadores argumentos del operador sobrecargado depende de dos factores: 1. Si es un operador unario (un argumento) o un operador binario (dos argumentos) 2. Si el operador es denido como una funcin global (un argumento para los unarios, dos para los binarios) o una funcin miembro (cero argumentos para los unarios y uno para los binarios. En este ltimo caso el objeto (this) se convierte en el argumento del lado izquierdo al operador). He aqu una pequea clase que muestra la sintaxis de la sobrecarga de operadores:
//: C12:OperatorOverloadingSyntax.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class Integer { int i; public: Integer(int ii) : i(ii) {} const Integer operator+(const Integer& rv) const { cout << "operator+" << endl; return Integer(i + rv.i); } Integer& operator+=(const Integer& rv) { cout << "operator+=" << endl; i += rv.i; return *this; } }; int main() { cout << "built-in types:" << endl; int i = 1, j = 2, k = 3; k += i + j; cout << "user-defined types:" << endl; Integer ii(1), jj(2), kk(3); kk += ii + jj; }
Los dos operadores sobrecargados son denidos como funciones miembros en lnea que imprimen un mensaje al ser llamados. El nico argumento de estas funciones miembro ser el que aparezca del lado derecho del operador binario. Los operadores unarios no tienen argumentos cuando son denidos como funciones miembro. La funcin miembro es llamada por el objeto de la parte izquierda del operador. Para los operadores incondicionales (los condicionales generalmente devuelven un valor booleano), generalmente se desear devolver un objeto o una referencia del mismo tipo que est operando, si los dos argumentos son del mismo tipo. (Si no son del mismo tipo, la interpretacin de lo que debera pasar es responsabilidad nuestra). De esta manera, se pueden construir expresiones tan complicadas como la siguiente:
kk += ii + jj ;
La expresin operator+ crea un nuevo objeto Integer (temporal) que se usa como el argumento rv para el operador operator+=. Este objeto temporal se destruye tan pronto como deja de necesitarse. 306
307
308
309
Captulo 12. Sobrecarga de operadores Las funciones estn agrupadas de acuerdo a la forma en que se pasan los argumentos. Ms tarde se darn unas cuantas directivas de cmo pasar y devolver argumentos. Las clases expuestas anteriormente (y las que siguen en la siguiente seccin) son las tpicas que usted usar, asi que empiece con ellas como un patrn cuando sobrecargue sus propios operadores.
Incremento y decremento
Los operadores de incremento++ y de decremento -- provocan un conicto porque querr ser capaz de llamar diferentes funciones dependiendo de si aparecen antes(prejo) o despus(posja) del objeto sobre el que actuan. La solucin es simple, pero la gente a veces lo encuentra un poco confuso inicialmente. Cuando el compilador ve, por ejemplo, ++a (un preincremento), genera una llamada al operator++(a) pero cuando ve a++, genera una llamada a operator++(a, int). Asi es como el compilador diferencia entre los dos tipos, generando llamadas a funciones sobrecargadas diferentes. En OverloadingUnaryOperators.cpp para la versin de funciones miembro, si el compilador ve ++b, genera una llamada a B::operator++()y si be b++genera una llamada a B::operator++(int). Todo lo que el usuario ve es que una funcin diferente es llamada para las versiones posja y preja. Ocultamente, sin embargo, las dos llamadas de funciones tienen diferentes rmas, asi que conectan con dos diferentes cuerpos. El compilador pasa un valor constante cticio para el argumento int(el cual nunca es proporcionado por un identicador porque el valor nunca se usa) para generar las diferentes rmas para la versin posja.
310
311
//: C12:Integer.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Implementation of overloaded operators #include "Integer.h" #include "../require.h" const Integer operator+(const Integer& left, const Integer& right) { return Integer(left.i + right.i); } const Integer operator-(const Integer& left, const Integer& right) { return Integer(left.i - right.i); } const Integer operator*(const Integer& left, const Integer& right) { return Integer(left.i * right.i); } const Integer operator/(const Integer& left, const Integer& right) { require(right.i != 0, "divide by zero"); return Integer(left.i / right.i); } const Integer operator%(const Integer& left, const Integer& right) { require(right.i != 0, "modulo by zero"); return Integer(left.i % right.i); } const Integer operator^(const Integer& left, const Integer& right) { return Integer(left.i ^ right.i); } const Integer operator&(const Integer& left, const Integer& right) { return Integer(left.i & right.i); } const Integer operator|(const Integer& left, const Integer& right) { return Integer(left.i | right.i); } const Integer operator<<(const Integer& left, const Integer& right) { return Integer(left.i << right.i); }
312
313
//: C12:IntegerTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} Integer #include "Integer.h" #include <fstream> using namespace std; ofstream out("IntegerTest.out"); void h(Integer& c1, Integer& c2) { // A complex expression: c1 += c1 * c2 + c2 % c1; #define TRY(OP) \ out << "c1 = "; c1.print(out); \ out << ", c2 = "; c2.print(out); \ out << "; c1 " #OP " c2 produces "; \ (c1 OP c2).print(out); \ out << endl; TRY(+) TRY(-) TRY(*) TRY(/) TRY(%) TRY(^) TRY(&) TRY(|) TRY(<<) TRY(>>) TRY(+=) TRY(-=) TRY(*=) TRY(/=) TRY(%=) TRY(^=) TRY(&=) TRY(|=) TRY(>>=) TRY(<<=)
314
//: C12:Byte.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Member overloaded operators #ifndef BYTE_H #define BYTE_H #include "../require.h" #include <iostream> // Member functions (implicit "this"): class Byte { unsigned char b; public: Byte(unsigned char bb = 0) : b(bb) {} // No side effects: const member function: const Byte operator+(const Byte& right) const { return Byte(b + right.b); } const Byte operator-(const Byte& right) const { return Byte(b - right.b); } const Byte operator*(const Byte& right) const { return Byte(b * right.b); } const Byte operator/(const Byte& right) const { require(right.b != 0, "divide by zero"); return Byte(b / right.b); } const Byte operator%(const Byte& right) const { require(right.b != 0, "modulo by zero"); return Byte(b % right.b); } const Byte operator^(const Byte& right) const { return Byte(b ^ right.b); } const Byte operator&(const Byte& right) const { return Byte(b & right.b);
315
316
//: C12:ByteTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "Byte.h" #include <fstream> using namespace std; ofstream out("ByteTest.out"); void k(Byte& b1, Byte& b2) { b1 = b1 * b2 + b2 % b1; #define TRY2(OP) \ out << "b1 = "; b1.print(out); \ out << ", b2 = "; b2.print(out); \ out << "; b1 " #OP " b2 produces "; \ (b1 OP b2).print(out); \ out << endl; b1 = 9; b2 = 47; TRY2(+) TRY2(-) TRY2(*) TRY2(/)
317
Puede ver como a operator= slo se le permite ser una funcin miembro. Esto se explicar mas adelante. Advierta que todos los operadores de asignacin tienen codigo para comprobar la auto asignacin; esta es una directiva general. En algunos casos esto no es necesario; por ejemplo, con operator+= a menudo usted querr decir A+=A y sumar A a s mismo. El lugar ms importante para situar las comprobaciones para la auto asignacin es operator= porque con objetos complicados pueden ocurrir resultados desastrosos. (En algunos casos es correcto, pero siempre se debera tenerlo en mente cuando escriba operator=). Todos los operadores mostrados en los dos ejemplos previos son sobrecargados para manejar un tipo simple. Tambin es posible sobrecargar operadores para manejar tipos compuestos, de manera que pueda sumar manzanas a naranjas, por ejemplo. Antes de que empiece una sobrecarga exhaustiva de operadores, no obstante, deberia mirar la seccin de conversin automtica de tipos mas adelante en este capitulo. A menudo, una conversin de tipos en el lugar adecuado puede ahorrarle un montn de operadores sobrecargados
12.3. Operadores sobrecargables Este objeto se devuelve por valor como una constante as que el resultado no puede ser modicado como un valor izquierdo. 3. Todas las operadores de asignacin modican el valor izquierdo. Para permitir al resultado de la asignacin ser usado en expresiones encadenadas, como a=b=c, se espera que devuelva una referencia al mismo valor izquierdo que acaba de ser modicado. Pero debera ser esta referencia const o no const?. Aunque lee a=b=cde izquierda a derecha, el compilador la analiza de derecha a izquierda, asi que no esta obligado a devolver una referencia no const para soportar asignaciones encadenadas. Sin embargo, la gente a veces espera ser capaz de realizar una operacin sobre el elemento de acaba de ser asignado, como (a=b).func(); para llamar a func de a despus de asignarle b. De ese modo, el valor de retorno para todos los operadores de asignacin debera ser una referencia no const para el valor izquierdo. 4. Para los operadores lgicos, todo el mundo espera obtener en el peor de los casos un tipo int, y en el mejor un tipo bool. (Las libreras desarrolladas antes de que los compiladores de C++ soportaran el tipo incorporado boolusaran un tipo int o un typedef equivalente). Los operadores de incremento y de decremento presentan un dilema a causa de las versiones posja y preja. Ambas versiones cambian el objeto y por tanto no pueden tratar el objeto como un const. La versin preja devuelve el valor del objeto despus de que sea cambiado, asi que usted espera recuperar el objeto que fue cambiado. De este modo, con la versin preja puede simplemente revolver *this como una referencia. La versin posja se supone que devolver el valor antes de que sea cambiado, luego est forzado a crear un objeto separado para representar el valor y devolverlo. As que con la version posja debe devolverlo por valor si quiere mantener el sifgnicado esperado. (Advierta que a veces usted encuentra los operadores de incremento y de decremento devolviendo un int o un bool para indicar, por ejemplo, cuando un objeto preparado para moverse a travs de una lista esta al nal de ella). Ahora la pregunta es:Debera ste ser devuelto como una referencia consto no const?. Si permite que el objeto sea modicado y alguien escribe (a++).func(), func operar en la propia a, pero con (++a).func(), funcopera en el objeto temporal devuelto por el operador posjo operator++. Los objetos temporales son automticamente const, asi que esto podra ser rechazado por el compilador, pero en favor de la consistencia tendra ms sentido hacerlos ambos const como hemos hecho aqu. O puede elegir hacer la versin preja no const y la posja const. Debido a la variedad de signicados que puede darle a los operadores de incremento y de decremento, necesitarn ser considerados en trminos del caso individual.
Esto puede parecer en principio como una funcin de llamada de a un constructor pero no lo es. La sintaxis es la de un objeto temporal;la sentencia dice crea un objeto Integer temporal y desvulvelo. A causa de esto, puede pensar que el resultado es el mismo que crear un objeto local con nombre y devolverle. Sin embargo, es algo diferente. Si quisiera decir en cambio:
Integer tmp(left. i + right. i); return tmp;
319
Captulo 12. Sobrecarga de operadores tres cosas sucedern. La primera, el objeto tmp es creado incluyendo la llamada a su constructor. La segunda, el constructor de copia duplica tmp a la localizacin del valor de retorno externo. La tercera, se llama al destructor para tmp cuando sale del mbito. En contraste, la aproximacin de devolver un objeto temporal funciona de manera bastante diferente. Cuando el compilador ve que usted hace esto, sabe que no tiene otra razn para crearlo mas que para devolverlo. El compilador aprovecha la ventaja que esto da para construir el objeto directamente en la localizacin del valor de retorno externo a la funcin. Esto necesita de una sola y ordinaria llamada al constructor(la llamada al constructor de copia no es necesaria) y no hay llamadas al destructor porque nunca se crea un objeto local. De esta manera, mientras que no cuesta nada mas que el conocimiento del programador, es signicativamente mas eciente. Esto es llamado a menudo la optimizacin del valor de retorno.
El operador coma
El operador coma es llamado cuando aparece siguiendo a un objeto del tipo para el que est denido. Sin embargo, operator, no se llama para listas de argumentos de funciones, slo para objetos fuera de ese lugar separados por comas. No parece haber un montn de usos prcticos para este operador, solo es por consistencia del lenguaje. He aqu un ejemplo que muestra como la funcin coma puede ser llamada cuando aparece antes de un objeto, as como despus:
//: C12:OverloadingOperatorComma.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class After { public: const After& operator,(const After&) const { cout << "After::operator,()" << endl; return *this; } }; class Before {}; Before& operator,(int, Before& b) { cout << "Before::operator,()" << endl; return b; } int main() { After a, b; a, b; // Operator comma called Before c; 1, c; // Operator comma called }
320
12.3. Operadores sobrecargables Las funciones globales permiten a la coma ser situada antes del objeto en cuestin. El uso mostrado es bastante oscuro y cuestionable. Aunque usted podra probablemente usar una lista separada por comas como parte de una expresin mas complicada, es demasiado renado en la mayora de las ocasiones.
El operador ->
El operador -> se usa generalmente cuando quiera hacer que un objeto aparezca como un puntero. Un objeto como este es llamado a menudo un puntero inteligente. Estos son especialmente utiles si usted quiere envolver una clase con un puntero para hacer que ese puntero sea seguro, o en la forma comn de un iterador, que es un objeto que se mueve a travs de una coleccin o contenedor de otros objetos y los selecciona de uno en uno cada vez, si proporcionar acceso directo a la implementacin del contenedor. (A menudo encontrar iteradores y contenedores en las libreras de clases, como en la Libreria Standar de C++, descrita en el volumen 2 de este libro). El operador de indireccin de punteros (*) debe ser una funcin miembro. Tiene otras obligaciones atpicas: debe devolver un objeto( o una referencia a un objeto) que tambin tenga un operador de indireccin de punteros, o debe devolver un puntero que pueda ser usado para encontrar a lo que apunta la echa del operador de indirecin de punteros. He aqu un ejemplo simple:
//: C12:SmartPointer.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> #include <vector> #include "../require.h" using namespace std; class Obj { static int i, j; public: void f() const { cout << i++ << endl; } void g() const { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } friend class SmartPointer; }; class SmartPointer { ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Return value indicates end of list: bool operator++() { // Prefix if(index >= oc.a.size()) return false; if(oc.a[++index] == 0) return false; return true; } bool operator++(int) { // Postfix return operator++(); // Use prefix version } Obj* operator->() const {
321
La clase Obj dene los objetos que son manipulados en este programa. Las funciones f() y g() simplemente escriben en pantalla los valores interesantes usando miembros de datos estticos. Los punteros a estos objetos son almacenados en el interior de los contenedores del tipo ObjContainer usando su funcin add(). ObjContanier parece un array de punteros, pero advertir que no hay forma de traer de nuevo los punteros. Sin embargo, SmartPointer se declara como una clase friend, asi que tiene permiso para mirar dentro del contenedor. La clase SmartPointer parece mucho ms un puntero inteligente-usted puede moverlo hacia adelante usando operator++(tambin puede denir un operator--, no pasar del nal del contenedor al que apunta, y genera( a travs del operador de indireccion de punteros) el valor al que apunta. Advierta que SmartPointer est hecho a medida sobre el contenedor para el que se crea;a diferencia de un puntero normal, no hay punteros inteligentes de "propsito general". Aprender mas sobre los punteros inteligentes llamados iteradores en el ultimo capitulo de este libro y en el volumen 2(descargable desde FIXME:url www. BruceEckel. com). En main(), una vez que el contenedor oc se rellena con objetos Obj un SmartPointer sp se crea. La llamada al puntero inteligente sucede en las expresiones:
sp->f(); sp->g(); // Llamada al puntero inteligente
Aqu, incluso aunque sp no tiene funciones miembro f() y g(), el operador de indireccin de punteros automticamente llama a esas funciones para Obj* que es devuelto por SmartPointer::operator->. El compilador realiza todas las comprobaciones pertinentes para asegurar que la llamada a funcin funciona de forma correcta. Aunque la mecnica subyacente de los operadores de indireccin de punteros es ms compleja que la de los otros operadores, la meta es exactamente la misma:proporcionar una sintaxis mas conveniente para los usuarios de sus clases.
Un operador anidado
Es ms comn ver un puntero inteligente o un clase iteradora anidada dentro de la clase a la que sirve. El ejemplo previo puede ser reescrito para anidar SmartPointer dentro de ObjContainer as:
//: C12:NestedSmartPointer.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> #include <vector> #include "../require.h" using namespace std;
322
class Obj { static int i, j; public: void f() { cout << i++ << endl; } void g() { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } class SmartPointer; friend class SmartPointer; class SmartPointer { ObjContainer& oc; unsigned int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Return value indicates end of list: bool operator++() { // Prefix if(index >= oc.a.size()) return false; if(oc.a[++index] == 0) return false; return true; } bool operator++(int) { // Postfix return operator++(); // Use prefix version } Obj* operator->() const { require(oc.a[index] != 0, "Zero value " "returned by SmartPointer::operator->()"); return oc.a[index]; } }; // Function to produce a smart pointer that // points to the beginning of the ObjContainer: SmartPointer begin() { return SmartPointer(*this); } }; int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Fill it up ObjContainer::SmartPointer sp = oc.begin(); do { sp->f(); // Pointer dereference operator call sp->g(); } while(++sp); }
Adems del actual anidamiento de la clase, hay solo dos diferencias aqu. La primera es la declaracin de la clase para que pueda ser friend: 323
El compilador debe saber primero que la clase existe, antes de que se le diga que es amiga. La segunda diferencia es en ObjContainerdonde la funcin miembro begin() produce el SmartPointer que apunta al principio de la secuencia del ObjContainer. Aunque realmente es slo por conveniencia, es adecuado porque sigue la manera habitual de la librera estndar de C++.
Operador ->*
El operador ->* es un operador binario que se comporta como todos los otros operadores binarios. Se proporciona para aquellas situaciones en las que quiera imitar el comportamiento producido por la sintaxis incorporada puntero a miembro, descrita en el capitulo anterior. Igual que operator->, el operador de indireccin de puntero a miembro es usado normalmente con alguna clase de objetos que representan un puntero inteligente, aunque el ejemplo mostrado aqui ser ms simple para que sea comprensible. El truco cuando se dene operator->* es que debe devolver un objeto para el que operator() pueda ser llamado con los argumentos para la funcin miembro que usted llama. La llamada a funcin operator() debe ser una funcin miembro, y es nica en que permite cualquier nmero de argumentos. Hace su objeto parecer como si fuera realmente una funcin. Aunque usted prodra denir varias funciones sobrecargadas operator() con diferentes argumentos, a menudo se usa para tipos que solo tienen una operacin simple, o al menos una especialmente destacada. Usted ver en el Volumen2 que la librera estndar de C++ usa el operador de llamada a funcin para crear objetos funcin. Para crear un operator->* debe primero crear una clase con un operator() que sea el tipo de objeto que operator->* devolver. Esta clase debe, de algn modo, capturar la informacin necesaria para que cuando operator() sea llamada( lo que sucede automticamente), el puntero a miembro sea indireccionado para el objeto. En el ejemplo siguiente, el constructor de FunctionObjectcaptura y almacena el puntero al objeto y el puntero a la funcin miembro, y entonces operator() los usa para hacer la actual llamada a "puntero a miembro":
//: C12:PointerToMemberOperator.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <iostream> using namespace std; class Dog { public: int run(int i) const { cout << "run\n"; return i; } int eat(int i) const { cout << "eat\n"; return i; } int sleep(int i) const { cout << "ZZZ\n"; return i; } typedef int (Dog::*PMF)(int) const; // operator->* must return an object // that has an operator(): class FunctionObject { Dog* ptr; PMF pmem; public:
324
Dog tiene tres funciones miembro, todas las cuales toman un argumento entero y devuelven un entero. PMC es un typedef para simplicar el denir un puntero a miembro para las funciones miembro de Dog. Una FunctionObject es creada y devuelta por operator->*. Dese cuenta que operator->* conoce el objeto para el que puntero a miembro esta siendo llamado(this) y el puntero a miembro, y los pasa al constructor FunctionObject que almacena sus valores. Cuando operator->* es llamado, el compilador inmediatamente se revuelve y llama a operator() para el valor de retorno de operator->*, pasandole los argumentos que le fueron pasados a operator->*. FunctionObject::operator() toma los argumentos y entonces indirecciona el puntero a miembro real usando los punteros a objeto y a miembro almacenados. Perctese de que lo que est haciendo aqu, justo como con operator->, es insertarse en la mitad de la llamada a operator->*. Esto permite realizar algunas operaciones adicionales si las necesitara. El mecanismo operator->* implementado aqu solo trabaja para funciones miembro que toman un argumento entero y devuelven otro entero. Esto es una limitacin, pero si intenta crear mecanismos sobrecargados para cada diferente posibilidad, ver que es una tarea prohibitiva. Afortunadamente, el mecanismo de plantillas de C++(descrito el el ultimo capitulo de este libro, y en el volumen2) esta diseado para manejar semejante problema.
Captulo 12. Sobrecarga de operadores 4. No hay operadores denidos por el usuario. Esto es, no puede crear nuevos operadores que no estn ya en el actual conjunto. Una parte del problema es como determinar la prioridad, y otra parte es la falta de necesidad a costa del problema inherente. 5. Usted no puede cambiar las reglas de prioridad. Son lo sucientemente difciles de recordad como son sin dejar a la gente jugar con ellas.
326
int main() { stringstream input("47 34 56 92 103"); IntArray I; input >> I; I[4] = -1; // Use overloaded operator[] cout << I; }
Esta clase contiene tambin un operador sobrecargado operator[] la cual devuelve una referencia a un valor a licito en el array. Dado que se devuelve una referencia, la expresin:
I[4] = -1;
No slo parece mucho ms adecuada que si se usaran punteros, tambin causa el efecto deseado. Es importante que los operadores de desplazamiento sobrecargados pasen y devuelvan por referencia, para que los cambios afecten a los objetos externos. En las deniciones de las funciones, expresiones como:
os << ia.i[j];
provocan que sean llamadas las funciones de los operadores sobrecargados(esto es, aquellas denidas en iostream). En este caso, la funcin llamada es ostream& operator<<(ostream&, int) dado que ia[i].j se resuelve en un int. Una vez que las operaciones se han realizado en istream o en ostream se devuelve para que pueda ser usado en expresiones mas complejas. En main() se usa un nuevo tipo de iostream: el stringstream(declarado en <sstream>). Esta es una clase que toma una cadena(que se puede crear de un array de char, como se ve aqu) y lo convierte en un iostream. En el ejemplo de arriba, esto signica que los operadores de desplazamiento pueden ser comprobados sin abrir un archivo o sin escribir datos en la lnea de comandos. La manera mostrada en este ejemplo para el extractor y el insertador es estndar. Si quiere crear estos operadores para su propia clase, copie el prototipo de la funcin y los tipos de retorno de arriba y siga el estilo del cuerpo.
Rob Murray, C++ Strategies & Tactics , Addison Wesley, 1993, pagina 47.
327
MyType b; MyType a = b; a = b;
En la segunda lnea, se dene el objeto a. Se crea un nuevo objeto donde no exista ninguno. Dado que por ahora conoce como de quisquilloso es el compilador de C++ respecto a la inicializacin de objetos, sabr que un constructor debe siempre ser llamado cuando se dene un objeto. Pero qu constructor?, a se crea desde un objeto existente MyType (b, en el lado derecho del signo de igualdad), asi que hay solo un eleccin: el constructor de copia. Incluso aunque el signo de igualdad est involucrado, se llama al constructor de copia. En la tercera lnea, las cosas son diferentes. En la parte izquierda del signo igual, hay un objeto previamente inicializado. Claramente, usted no llama a un constructor para un objeto que ya ha sido creado. En este caso MyType::operator= se llama para a, tomando como argumento lo que sea que aparezca en la parte derecha. (Puede tener varios funciones operator= que tomen diferentes argumentos en la parte derecha). Este comportamiento no est restringido al constructor de copia. Cada vez que inicializa un objeto usando un signo = en lugar de la forma usual de llamada al constructor, el compilador buscar un constructor que acepte lo que sea que haya en la parte derecha:
//: C12:CopyingVsInitialization.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Fi { public: Fi() {} }; class Fee { public: Fee(int) {} Fee(const Fi&) {} }; int main() { Fee fee = 1; // Fee(int) Fi fi; Fee fum = fi; // Fee(Fi) }
Cuando se trata con el signo =, es importante mantener la diferencia en mente:Si el objeto ha sido creado ya, se requiere una inicializacin;en otro caso el operador de asignacin = se usa. Es incluso mejor el evitar escribir cdigo que usa = para la inicializacin; en cambio, use siempre la manera del constructor explcito. Las dos construcciones con el signo igual se convierten en:
Fee fee(1); Fee fum(fi);
El compilador evita esta situacin obligandole a hacer una funcin miembro operator=. Cuando usted crea un operator=, debe copiar todo la informacin necesaria desde el objeto de la parte derecha al objeto actual(esto es, el objeto para el que operator= est siendo llamado) para realizar lo que sea que considere asignacin para su clase. Para objetos simples, esto es trivial:
//: C12:SimpleAssignment.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Simple operator=() #include <iostream> using namespace std; class Value { int a, b; float c; public: Value(int aa = 0, int bb = 0, float cc = 0.0) : a(aa), b(bb), c(cc) {} Value& operator=(const Value& rv) { a = rv.a; b = rv.b; c = rv.c; return *this; } friend ostream& operator<<(ostream& os, const Value& rv) { return os << "a = " << rv.a << ", b = " << rv.b << ", c = " << rv.c; } }; int main() { Value a, b(1, 2, cout << "a: " << cout << "b: " << a = b; cout << "a after }
3.3); a << endl; b << endl; assignment: " << a << endl;
Aqu, el objeto de la parte izquierda del igual copia todos los elementos del objeto de la parte derecha, y entonces devuelve una referencia a s mismo, lo que permite crear expresiones mas complejas. Este ejemplo incluye un error comn. Cuando usted est asignando dos objetos del mismo tipo, siempre debera comprobar primero la auto asignacin: Est el objeto siendo asignado a s mismo?. En algunos casos como ste, es inofensivo si realiza la operacin de asignacin, de todas formas, pero si se realizan cambios a la implementacin de la clase, puede haber diferencias y si no lo toma con una cuestin de costumbre, puede olvidarlo y provocar errores difciles de encontrar.
Punteros en clases
Qu ocurre si el objeto no es tan simple?. Por ejemplo, qu pasa si el objeto contiene punteros a otros objetos?. Simplemente copiar el puntero signica que usted terminar con dos objetos apuntando a la misma localizacin de memoria. En situaciones como sta, necesita hacer algo de contabilidad. Hay dos aproximaciones a este problema. La tcnica mas simple es copiar lo que quiera que apunta el puntero cuando realiza una asignacin o una construccin de copia. Es es directamente: 329
//: C12:CopyingWithPointers.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Solving the pointer aliasing problem by // duplicating what is pointed to during // assignment and copy-construction. #include "../require.h" #include <string> #include <iostream> using namespace std; class Dog { string nm; public: Dog(const string& name) : nm(name) { cout << "Creating Dog: " << *this << endl; } // Synthesized copy-constructor & operator= // are correct. // Create a Dog from a Dog pointer: Dog(const Dog* dp, const string& msg) : nm(dp->nm + msg) { cout << "Copied dog " << *this << " from " << *dp << endl; } ~Dog() { cout << "Deleting Dog: " << *this << endl; } void rename(const string& newName) { nm = newName; cout << "Dog renamed to: " << *this << endl; } friend ostream& operator<<(ostream& os, const Dog& d) { return os << "[" << d.nm << "]"; } }; class DogHouse { Dog* p; string houseName; public: DogHouse(Dog* dog, const string& house) : p(dog), houseName(house) {} DogHouse(const DogHouse& dh) : p(new Dog(dh.p, " copy-constructed")), houseName(dh.houseName + " copy-constructed") {} DogHouse& operator=(const DogHouse& dh) { // Check for self-assignment: if(&dh != this) { p = new Dog(dh.p, " assigned"); houseName = dh.houseName + " assigned"; } return *this; } void renameHouse(const string& newName) { houseName = newName; } Dog* getDog() const { return p; } ~DogHouse() { delete p; } friend ostream&
330
Dog es una clase simple que contiene solo una cadena con el nombre del perro. Sin embargo, generalmente sabr cuando le sucede algo al perro dado que los constructores y destructores imprimen informacin cuando son llamados. Advierta que el segundo constructor es un poco como un constructor de copia excepto que toma un puntero a Dog en vez de una referencia, y tiene un segundo argumento que es un mensaje a ser concatenado con el nombre del perro. Esto se hace asi para ayudar a rastrear el comportamiento del programa. Puede ver que cuando sea que una funcin miembro imprime informacin, no accede a esa informacin directamente sino en su lugar manda *this a cout. Este a su vez llama a ostream operator<<. Es aconsejable hacer esto as dado que si quiere reformatear la manera en la que informacin del perro es mostrada(como hice aadiendo el [ y el ]) solo necesita hacerlo en un lugar. Una DogHouse contiene un Dog* y explica las cuatro funciones que siempre necesitar denir cuando sus clases contengan punteros:todos los constructores necesarios usuales, el constructor de copia, operator= (se dene o se deshabilita) y un destructor. Operator= comprueba la auto asignacin como una cuestin de estilo, incluso aunque no es estrictamente necesario aqu. Esto virtualmente elimina la posibilidad de que olvide comprobar la auto asignacin si cambia el cdigo.
Contabilidad de referencias
En el ejemplo de arriba, el constructor de copia y el operador = realizan una copia de lo que apunta el puntero, y el destructor lo borra. Sin embargo, si su objeto requiere una gran cantidad de memoria o una gran inicializacin ja, a lo mejor puede querer evitar esta copia. Una aproximacin comn a este problema se llama contabilidad de referencias. Se le da inteligencia al objeto que esta siendo apuntado de tal manera que sabe cuantos objetos le estn apuntado. Entonces la construccin por copia o la asignacin consiste en aadir otro puntero a un objeto existente e incrementar la cuenta de referencias. La destruccin consiste en reducir esta cuenta de referencias y destruir el objeto si la cuenta llega a cero. Pero que pasa si quiere escribir el objeto(Dog en el ejemplo anterior)?. Ms de un objeto puede estar usando este Dog luego podra estar modicando el perro de alguien ms a la vez que el suyo, lo cual no parece ser muy amigable. Para resolver este problema de solapamiento se usa una tcnica adicional llamada copia para escritura. Antes de escribir un bloque de memoria, debe asegurarse que nadie ms lo est usando. Si la cuenta de referencia es superior a uno, debe realizar una copia personal del bloque antes de escribirlo, de tal manera que no moleste el espacio de otro. He aqu un ejemplo simple de contabilidad de referencias y de copia para escritura:
//: C12:ReferenceCounting.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Reference count, copy-on-write #include "../require.h" #include <string>
331
332
333
La clase Dog es el objeto apuntado por DogHouse. Contiene una cuenta de referencia y funciones para controlar y leer la cuenta de referencias. Hay un constructor de copia luego puede crear un nuevo Dog de uno existente. La funcin attach() incrementa la cuenta de referencia de un Dog para indicar que hay otro objeto usandolo. La funcin detach() decrementa la cuenta de referencia. Si llega a cero, entonces nadie ms lo esta usando, as que la funcin miembro destruye su propio objeto llamando a delete this. Antes de que haga cualquier modicacin(como renombrar un perro), debera asegurarse de que no est cambiando un Dog que algn otro objeto est usando. Hace esto llamando a DogHouse::unalias() , la cual llama a Dog::unalias(). Esta ltima funcin devolver el puntero a Dog existente si la cuenta de referencia es uno (lo que signica que nadie mas est usando ese Dog), pero duplicar Dog si esa cuenta es mayor que uno. El constructor de copia, adems de crear su propia memoria, asigna un Dog al Dog del objeto fuente. Entonces, dado que ahora hay un objeto ms usando ese bloque de memoria, incrementa la cuenta de referencia llamando a Dog::attach(). El operador = trata con un objeto que ha sido creado en la parte izquierda del =, as que debe primero debe limpiarlo llamando a detach() para ese perro, el cual destruir el viejo perro si nadie ms lo est usando. Entonces operator= repite el comportamiento del constructor de copia. Advierta que primero realiza comprobaciones para detectar cuando est asignando el objeto a s mismo. El destructor llama a detach() para destruir condicionalmente a Dog. Para implementar la copia para escritura, debe controlar todas las operaciones que escriben en su bloque de memoria. Por ejemplo, la funcin miembro renameDog() le permite cambiar valores en el bloque de memoria. Pero primero, usa unalias() para prevenir la modiciacin de un Dog solapado (un Dog con ms de un objeto DogHouse apuntndole). Y si necesita crear un puntero a Dog desde un DogHouse debe evitar el solapamiento del puntero primero. La funcin main() comprueba las numerosas funciones que deben funcionar correctamente para implementar la cuenta de referencia:el constructor, el constructor de copia, operator= y el destructor. Tambin comprueba la copia para escritura llamando a renameDog(). He aqu la salida (despus de un poco de reformateo):
Creando Dog: [Fido], rc = 1 CreadoDogHouse: [FidoHouse] contiene [Fido], rc = 1 Creando Dog: [Spot], rc = 1 CreadoDogHouse: [SpotHouse] contiene [Spot], rc = 1 Entrando en el constructor de copia Dog aadido:[Fido], rc = 2 DogHouse constructor de copia [construido por copia FidoHouse] contiene [Fido], rc = 2 Despues de la construccin por copia de Bobs fidos:[FidoHouse] contiene [Fido], rc = 2 spots:[SpotHouse] contiene [Spot], rc = 1 bobs:[construido por copia FidoHouse] contiene[Fido], rc = 2 Entrando spots = fidos Eliminando perro: [Spot], rc = 1 Borrando Perro: [Spot], rc = 0 Aadido Dog: [Fido], rc = 3
334
Estudiando la salida, rastreando el cdigo fuente y experimentando con el programa, podr ahondar en la comprensin de estas tcnicas.
Captulo 12. Sobrecarga de operadores En general, no querr dejar al compilador que haga esto por usted. Con clases de cualquier sosticacin (Especialmente si contienen punteros!)querr crear de forma explicita un operator=. Si realmente no quiere que la gente realice asignaciones, declare operator= como una funcin private. (No necesita denirla a menos que la est usando dentro de la clase).
Cuando el compilador ve f() llamada con un objeto One, mira en la declaracin de f() y nota que requiere un Two. Entonces busca si hay alguna manera de conseguir un Two de un One, y encuentra el constructor Two::Two(One) al cual llama. El objeto resultante Two es pasado a f(). En este caso, la conversin automtica de tipos le ha salvado del problema de denir dos versiones sobrecargadas de f(). Sin embargo el coste es la llamada oculta al constructor de Two lo cual puede importar si est preocupado por la eciencia de las llamadas a f(),
336
Haciendo el constructor de Two explicito, se le dice al compilador que no realice ninguna conversin automtica de tipos usando ese constructor en particular(otros constructores no explicitos en esa clase pueden todavia realizar conversiones automticas). Si el usuario quiere que ocurra esa conversin, debe escribir el codigo necesario. En el codigo de arriba, f(Two(one)) crea un objeto temporal de tipo Two desde one, justo como el compilador hizo en la versin previa.
337
Con la tcnica del constructor, la clase destino realiza la conversin, pero con los operadores, la realiza la clase origen. Lo valioso dela tcnica del constructor es que puede aadir una nueva ruta de conversin a un sistema existente mientras est creando una nueva clase. Sin embargo, creando un constructor con un nico argumento siempre dene una conversin automtica de tipos(incluso si requiere ms de un argumento si el resto de los argumentos tiene un valor por defecto), que puede no ser lo que desea(en cuyo caso puede desactivarlo usando explicit). Adems, no hay ninguna manera de usar una conversin por constructor desde un tipo denido por el usuario a un tipo incorporado;esto es posible solo con la sobrecarga de operadores.
Reexividad
Una de las razones mas normales para usar operadores sobrecargados globales en lugar de operadores miembros es que en la versin global, la conversin automtica de tipos puede aplicarse a cualquiera de los operandos, mientras que con objetos miembro, el operando de la parte izquierda debe ser del tipo apropiado. Si quiere que ambos operandos sean convertidos, la versin global puede ahorrarle un montn de cdigo. He aqu un pequeo ejemplo:
//: C12:ReflexivityInOverloading.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Number { int i; public: Number(int ii = 0) : i(ii) {} const Number operator+(const Number& n) const { return Number(i + n.i); } friend const Number operator-(const Number&, const Number&); }; const Number operator-(const Number& n1, const Number& n2) { return Number(n1.i - n2.i); } int main() { Number a(47), b(11); a + b; // OK a + 1; // 2nd arg converted to //! 1 + a; // Wrong! 1st arg not a - b; // OK a - 1; // 2nd arg converted to 1 - a; // 1st arg converted to }
La clase Number tiene tanto un miembro operator+ como un firiend operator-. Dado que hay un constructor que acepta un argumento int simple, un int puede ser convertido automticamente a un Number, pero slo bajo las condiciones adecuadas. En main(), puede ver que aadir un Number a otro Number funciona bien dado que tiene una correspondencia exacta con el operador sobrecargado. Adems, cuando el compilador ve un Number seguido de un + y de un int, puede emparejarlo a la funcin miembro Number::operator+ y convertir el argumentoint a un Number usando el constructor. Pero cuando ve un int, un + y un Number, no sabe que hacer porque todo lo que tiene es Number::operator+ el cual requiere que el operando de la izquierda sea ya un objeto Number. As que, el compilador emite un error. 338
12.6. Conversin automtica de tipos Con friend operator- las cosas son diferentes. El compilador necesita rellenar ambos argumentos como quiera que pueda; no est restringido a tener un Number como argumento de la parte izquierda. Asi que si ve:
1 - a
puede convertir el primer argumento a un Number usando el constructor. A veces querr ser capaz de restringir el uso de sus operadores hacindolos miembros. Por ejemplo, cuando multiplique una matriz por un vector, el vector debe ir en la derecha. Pero si quiere que sus operadores sean capaces de convertir cualquier argumento, haga el operador una funcin friend. Afortunadamente, el compilador coger la expresin 1-1 y convertir ambos argumentos a objetos Number y despues llamar a operator-. Eso signicara que el cdigo C existente pudiera empezar a funcionar de forma diferente. El compilador encaja la posibilidad mas simple primero, la cual es el operador incorporado para la expresin 1-1.
Aqu, slo se crea la funcin strcmp(), pero tendra que crear las correspondientes funciones para cada una de <cstring> que necesitar. Afortunadamente, puede proporcionar una conversin automtica de tipos permitiendo el acceso a todas las funciones de cstring.
//: C12:Strings2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt
339
Ahora cualquier funcin que tome un argumento char* puede tomar tambin un argumento Stringc porque el compilador sabe como crear un char* de un Stringc.
La solucin obvia a este problema es no hacerla. Simplemente proporcione una ruta nica para la conversin automtica de un tipo a otro. 340
12.6. Conversin automtica de tipos Un problema ms difcil de eliminar sucede cuando proporciona conversiones automticas a ms de un tipo. Esto se llama a veces acomodamiento:
//: C12:TypeConversionFanout.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Orange {}; class Pear {}; class Apple { public: operator Orange() const; operator Pear() const; }; // Overloaded eat(): void eat(Orange); void eat(Pear); int main() { Apple c; //! eat(c); // Error: Apple -> Orange or Apple -> Pear ??? }
La clase Apple tiene conversiones automticas a Orange y a Pear. El elemento capcioso sobre esto es que no hay problema hasta que alguien inocentemente crea dos versiones sobrecargadas de eat(). (Con slo una versin el codigo en main() funciona correctamente). De nuevo la solucin - y el lema general de la conversin automtica de tipos- es proveer solo una nica conversin automtica de un tipo a otro. Puede tener conversiones a otros tipos, slo que no deberan ser automaticas. Puede crear llamadas a funciones explicitas con nombres como makeA() y makeB().
Actividades ocultas
La conversin automtica de tipos puede producir mas actividad subyacente de la que podra esperar. Mire esta modicacin de CopyingVsInitialization.cpp como un juego de inteligencia:
//: C12:CopyingVsInitialization2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Fi {}; class Fee { public: Fee(int) {} Fee(const Fi&) {} }; class Fo { int i; public: Fo(int x = 0) : i(x) {} operator Fee() const { return Fee(i); } }; int main() { Fo fo;
341
No hay un constructor para crear Fee fee de un objeto Fo. Sin embargo, Fo tiene una conversin automtica de tipos a Fee. No hay un constructor de copia para crear un Fee de un Fee, pero esta es una de las funciones especiales que el compilador puede crear por usted. (El constructor por defecto, el constructor de copia y operator=) y el destructor puede sintetizarse automticamente por el compilador. Asi que para la relativamente inocua expresin:
Fee fee = fo;
el operador de conversin automtica es llamado, y se crea un constructor de copia. Use la conversin automtica de tipos con precaucin. Como con toda la sobrecarga de operadores, es excelente cuando reduce la tarea de codicacin signicativamente, pero no vale la pena usarla de forma gratuita.
12.7. Resumen
La completa razn para la existencia de la sobrecarga de operadores es para aquellas situaciones cuando simplica la vida. No hay nada particularmente mgico sobre ello;los operadores sobrecargados son solo funciones con nombres divertidos, y las llamadas a funcin son llamadas por el compilador para usted cuando se satisface el patrn adecuado. Pero si la sobrecarga de operadores no proporciona un benecio signicativo para usted(el creador de la clase) o para el usuario de la clase, no complique el asunto aadindolo.
12.8. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree una clase sencilla con un operador sobrecargado ++. Intente llamar a este operador en la forma preja y posja y vea que clase de advertencia del compilador obtiene. 2. Cree una clase sencilla que contenga un int y sobrecargue el operador + como una funcin miembro. Tambin cree una funcin miembro print() que tome un ostream& como un argumento y lo imprima a un ostream&. Experimente con su clase para comprobar que funciona correctamente. 3. Aada un operador binario - al ejercicio 2 como una funcin miembro. Demuestre que puede usar sus objetos in expresiones complejas como a + b -c. 4. Aada un operador ++ y otro -- al ejercicio 2, ambos con las versiones prejas y postjas, tales que devuelvan el objeto incrementado o decrementado. Asegurese que la versin posja devuelve el valor adecuado. 5. Modique los operadores de incremento y de decremento del ejercicio 4 para que la versin preja devuelva a referencia no const y la posja devuelva un objeto const. Muestre que funcionan correctamente y explique por qu esto se puede hacer en la prctica.
6. Cambie la funcin print() del ejercicio2 para que use el operador sobrecargado << como en IostreamOperatorOverload cpp. 7. Modique el ejercicio 3 para que los operadores + y - sean funciones no miembro. Demuestre que todava funcionan correctamente. 8. Aada el operador unario - al ejercicio 2 y demuestre que funciona correctamente. 9. Cree una clase que contenga un nico private char. Sobrecargue los operadores de ujos de entrada/salida << y >>(como en IostreamOperatorOverloading.cpp) y pruebelos. Puede probarlos con fstreams, stringstreams y cin y cout . 10. Determine el valor constante cticio que su compilador pasa a los operadores posjos ++ y --. 342
12.8. Ejercicios 11. Escriba una clase Number que contenga un double y aada operadores sobrecargados para +, -, *, / y la asignacin. Elija los valores de retorno para estas funciones para que las expresiones puedan ser encadenadas juntas y para que sea eciente. Escriba una conversin automtica de tipos operator int(). 12. Modique el ejercicio 11 para que use la optimizacin del valor de retorno, si todava no lo ha hecho. 13. Cree una clase que contenga un puntero, y demuestre que si permite al compilador sintetizar el operador = el resultado de usar ese operador sern punteros que estarn solapados en la misma localizacin de memoria. Ahora arregle el problema deniendo su propio operador = y demuestre que corrige el solapamiento. Asegrese que comprueba la auto asignacin y que maneja el caso apropiadamente. 14. Escriba una clase llamada Bird que contenga un miembro string y un static int. El el constructor por defecto, use el int para generar automticamente un identicador que usted construya en la string junto con el nombre de la clase(Bird #1, Bird #2, etc). Aada un operador << para ujos de salida para imprimir los objetos Bird-Escriba un operador de asignacin = y un constructor de copia. En main() verique que todo funciona correctamente. 15. Escriba una clase llamada BirdHouse que contenga un objeto, un puntero y una referencia para la clase Bird del ejercicio 14. El constructor debera tomar 3 Birds como argumentos. Aada un operador << de ujo de salida para BirdHouse. Deshabilite el operador de asignacin = y el constructor de copia. En main() verique que todo funciona correctamente. Asegrese de que puede encadenar asignaciones para objetos BirdHouse y construya expresiones que involucren a mltiples operadores. 16. Aada un miembro de datos int a Bird y a BirdHouse en el ejercicio 15. Aada operadores miembros +, -, * y / que usen el miembro int para realizar las operaciones en los respectivos miembros. Verique ques estas funcionan. 17. Repita el ejercicio 16 usando operadores no miembros. 18. Aada un operador - a SmartPointer.cpp y a NestedSmartPointer.cpp. 19. Modique CopyingVsInitialization.cpp para que todos los constructores impriman un mensaje que cuente que est pasando. Ahora verique que las dos maneras de llamar al constructor de copia(la de asignacin y la de parentesis) son equivalentes. 20. Intente crear un operador no miembro = para una clase y vea que clase de mensaje del compilador recibe. 21. Cree una clase con un operador de asignacin que tenga un segundo argumento, una string que tenga un valor por defecto que diga op = call. Cree una funcin que asigne un objeto de su clase a otro y muestre que su operador de asignacin es llamado correctamente. 22. En CopyingWithPointers.cpp elimine el operador = en DogHouse y muestre el el operador = sintetizado por el compilador copia correctamente string pero simplemente solapa el puntero a Dog. 23. En ReferenceCounting.cpp aada un static int y un int ordinario como miembros de datos a Dog y a DogHouse. En todos los constructores para ambas clases, incremente el static int y asigne el resultado al int ordinario para mantener un seguimiento del nmero de objetos que estn siendo creados. Haga las modicaciones necesarias para que todas las sentencias de impresin muestren los identicadores int de los objetos involucrados. 24. Cree una clase que contenga un string como un mimebro de datos. Inicialice el string en el constructor, pero no cree un constructor de copia o un operador =. Haga una segunda clase que tenga un objeto miembro de su primera clase;no cree un constructor de copia o un operador = para esta clase tampoco. Demuestre que el constructor de copia y el operador = son sintetizados correctamente por el compilador. 25. Combine las clases en OverloadingUnaryOperators.cpp y en Integer.cpp. 26. Modique PointerToMemmberOperator.cpp aadiendo dos nuevas funciones miembro a Dog que no tomen argumentos y devuelvan void. Cree y compruebe un operador sobrecargado ->* que funcione con sus dos nuevas funciones. 27. Aada un operador ->* a NestedSmartPointer.cpp. 343
Captulo 12. Sobrecarga de operadores 28. Cree dos clases, Apple y Orange. En Apple, cree un constructor que tome una Orange como un argumento. Cree una funcin que tome un Apple y llame a esa funcin con una una Orange para demostrar que funciona. Ahora haga explicito el constructor de Apple para demostrar que la conversin automtica de tipos es prevenida as. Modique la llamada a su funcin para que la la conversin se haga explicitamente y as funcione. 29. Aada un operador global * a ReflexivityInOverloading.cpp y demuestre que es reexivo. 30. Cree dos clases y un operador + y las funciones de conversin de tal manera que la adiccin sea reexiva para las dos clases. 31. Arregle TypeConversionFanout.cpp creando una funcin explicita para realizar la conversin de tipo, en lugar de uno de los operadoes de conversin automticos. 32. Escriba un cdigo simple que use los operadores +, -, *, / para double. Imaginese como el compilador genera el codigo ensamblador y mire el ensamblador que se genera para descubir y explicar que est ocurriendo bajo el envoltorio.
344
345
Captulo 13. Creacin dinmica de objetos El primero de estos pasos puede ocurrir de varios modos y en diferente momento: 1. Asignacin de memoria en la zona de almacenamiento esttico, que tiene lugar durante la carga del programa. El espacio de memoria asignado al objeto existe hasta que el programa termina. 2. Asignacin de memoria en la pila, cuando se alcanza algn punto determinado durante la ejecucin del programa (la llave de apertura de un bloque). La memoria asignada se vuelve a liberar de forma automtica en cuanto se alcanza el punto de ejecucin complementario (la llave de cierre de un bloque). Las operaciones de manipulacin de la pila forman parte del conjunto de instrucciones del procesador y son muy ecientes. Por otra parte, es necesario saber cuantas variables se necesitan mientras se escribe el programa de modo que el copilador pueda generar el cdigo correspondiente. 3. Asignancin dinmica, en una zona de memoria libre llamada montculo (heap o free store). Se reserva espacio para un objeto en esta zona mediante la llamada a una funcin durante la ejecucin del programa; esto signica que se puede decidir en cualquier momento que se necesita cierta cantidad de memoria. Esto conlleva la responsabilidad de determinar el momento en que ha de liberarse la memoria, lo que implica determinar el tiempo de vida de la misma que, por tanto, ya no esta bajo control de las reglas de mbito. A menudo, las tres regiones de memoria referidas se disponen en una zona contigua de la memoria fsica: rea esttica, la pila, y el montculo, en un orden determinado por el escritor del compilador. No hay reglas jas. La pila puede estar en una zona especial, y puede que las asignaciones en el montculo se obtengan mediante peticin de bloques de la memoria del sistema operativo. Estos detalles quedan normalmente ocultos al programador puesto que todo lo que se necesita conocer al respecto es que esa memoria estar disponible para cuando se necesite.
346
Se debe pasar como parmetro a malloc() el tamao del objeto. El tipo de retorno de malloc() es void*, pues es slo un puntero a un bloque de memoria, no un objeto. En C++ no se permite la asignacin directa de un void* a ningn otro tipo de puntero, de ah la necesidad de la conversin explcita de tipo (molde) Puede ocurrir que malloc() no encuentre un bloque adecuado, en cuyo caso devolver un puntero nulo, de ah la necesidad de comprobar la validez del puntero devuelto. El principal escollo est en la lnea:
obj->initialize();
El usuario deber asegurarse de inicializar el objeto antes de su uso. Obsrvese que no se ha usado el constructor debido a que ste no puede ser llamado de modo explcito 2 ; es llamado por el compilador cuando se crea un objeto. El problema es que el usuario puede olvidar inicializar el objeto antes de usarlo, introduciendo as una importante fuente de problemas. Como consecuencia, muchos programadores encuentran muy confusas y complicadas las funciones de asignacin dinmica de la memoria en C. No es muy dicil encontrar programadores que, usando mquinas con memoria virtual, usan vectores enormes en el rea de almacenamiento esttico para evitar tener que tratar con la asignacin dinmica. Dado que C++ intenta facilitar el uso de la biblioteca a los programadores ocasionales, no es aceptable la forma de abordar la asignacin dinmica en C.
se asigna espacio mediante alguna llamada equivalente a >malloc(sizeof(MyType)) --con frecuencia es as, literalmente--, y usando la direccin obtenida como puntero >this, y (1,2) como argumentos, se llama al constructor de la clase MyType. Para cuando est disponible, el valor de retorno de new es ya un puntero vlido a un objeto inicializado. Adems es del tipo correcto, lo que hace innecesaria la conversin. El operador new por defecto, comprueba el xito o fracaso de la asignacin de memoria como paso previo a la llamada al constructor, haciendo innecesaria y redundante la posterior comprobacin. Ms adelante en este captulo se ver qu sucede si se produce este fallo. En las expresiones con new se puede usar cualquiera de los constructores disponibles para una clase. Si ste no tiene argumentos, se escribe la expresin sin lista de argumentos
MyType *fp = new MyType;
2 Existe una sintaxis especial llamada placement-new que permite llamar al constructor para un bloque de memoria preasignando. Se ver ms adelante, en este mismo captulo.
347
Captulo 13. Creacin dinmica de objetos Es notable la simpleza alcanzada en la creacin dinmica de objetos: una nica expresin realiza todo el trabajo de clculo de tamao, asignacin, comprobaciones de seguridad y conversin de tipo. Esto hace que la creacin dinmica de objetos sea tan sencilla como la creacin en la pila.
Esta expresin destruye el objeto y despus libera el espacio dinmicamente asignado al objeto MyType El uso del operador delete debe reservarse slo para los objetos que hayan sido creados mediante new. Las consecuencias de aplicar el operador delete a los objetos creados con malloc(), calloc() o realloc() no estn denidas. Dado que la mayora de las implementaciones por defecto de new y delete usan malloc() y free(), el resultado ser probablemente la liberacin de la memoria sin la llamada al destructor. No ocurre nada si el puntero que se le pasa a delete es nulo. Por esa razn, a menudo se recomienda asignar cero al puntero inmediatamente despus de usar delete; se evita as que pueda ser usado de nuevo como argumento para delete. Tratar de destruir un objeto ms de una vez es un error de consecuencias imprevisibles.
Se puede probar que el constructor es invocado imprimiendo el valor de Tree. Aqu se hace sobrecargando el operator << para usarlo con un ostream y un Tree*. Notar, sin embargo, que aunque la funcin est declarada como friend, est denida como una inline!. Esto es as por conveniencia --denir una funcin amiga como inline a una clase no cambia su condicin de amiga o el hecho de que es una funcin global y no una funcin miembro. Tambin resaltar que el valor de retorno es el resultado de una expresin completa (el ostream&), y as debe ser, para satisfacer el tipo del valor de retorno de la funcin. 348
349
La clase Object contiene la variable data de tipo void* que es inicializada para apuntar a un objeto simple que no tiene destructor. En el destructor de Object se llama a delete con este puntero, sin que tenga consecuencias negativas puesto que lo nico que se necesita aqu es liberar la memoria. Ahora bien, se puede ver en main() la necesidad de que delete conozca el tipo del objeto al que apunta su argumento. Esta es la salida del programa:
Objeto a en construccin, tamao = 40 Destruccin del objeto a Objeto b en construccin, tamao = 40
Como delete sabe que a es un puntero a Object, se lleva a cabo la llamada al destructor de Object, con lo que se libera el espacio asignado a data. En cambio, cuando se manipula un objeto usando un void*, como es el caso en delete b, se libera el bloque de Object, pero no se efecta la llamada a su destructor, con lo que tampoco se liberar el espacio asignado a data, miembro de Object. Probablemente no se mostrar ningn mensaje de advertencia al compilar el programa; no hay ningn error sintctico. Como resultado obtenemos un programa con una silenciosa fuga de memoria. Cuando se tiene una fuga de memoria, se debe buscar entre todas las llamadas a delete para comprobar el tipo de puntero que se le pasa. Si es un void*, puede estar ante una de las posibles causas (Sin embargo, C++ proporciona otras muchas oportunidades para la fuga de memoria).
Los elementos de datos subyacentes no han cambiado mucho, pero ahora el almacenamiento se hace sobre un vector de punteros void, que se obtiene mediante new en lugar de malloc(). En la expresin
void** st = new void*[ quantity + increase ];
se asigna espacio para un vector de punteros a void. El destructor de la clase libera el espacio en el que se almacenan los punteros sin tratar de borrar los objetos a los que hacen referencia, ya que esto, insistimos, liberara el espacio asignado a los objetos, pero no se producira la necesaria llamada a sus destructores por la falta de informacin de tipo. El otro cambio realizado es el reemplazo de la funcin fetch() por operator [], ms signicativo sintcticamente. Su tipo de retorno es nuevamente void*, por lo que el usuario deber recordar el tipo de los objetos a que se reeren y efectuar la adecuada conversin al extraerlos del contenedor. Resolveremos este problema en captulos posteriores. Sigue la denicin de los mtodos de PStash:
//: C13:PStash.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Pointer Stash definitions #include "PStash.h" #include "../require.h" #include <iostream>
351
La funcin add() es, en efecto, la misma que antes si exceptuamos el hecho de que lo que se almacena ahora es un puntero a un objeto en lugar de una copia del objeto. El cdigo de inflate() ha sido modicado para gestionar la asignacin de memoria para un vector de void*, a diferencia del diseo previo, que slo trataba con bytes. Aqu, en lugar de usar el mtodo de copia por el ndice del vector, se pone primero a cero el vector usando la funcin memset() de la biblioteca estandar de C, aunque esto no sea estrictamente necesario ya que, presumiblemente, PStash manipular la memoria de forma adecuada, pero a veces no es muy costoso aadir un poco ms de seguridad. A continuacin, se copian al nuevo vector usando memcpy() los datos existentes en el antiguo. Con frecuencia ver que las funciones memcpy() y memset() han sido optimizadas en cuanto al tiempo de proceso, de modo que pueden ser ms rpidas que los bucles anteriormente vistos. No obstante, una funcion como inflate() no es probable que sea llamada con la frecuencia necesaria para que la direncia sea palpable. En cualquier caso, el hecho de que las llamadas a funcin sean ms concisas que los bucles, puede ayudar a prevenir errores de programacin. Para dejar denitivamente la responsabilidad de la limpieza de los objetos sobre los hombros del programador cliente, se proporcionan dos formas de acceder a los punteros en PStash: el operador [], que devuelve el puntero sin eliminarlo del contenedor, y un segundo mtodo remove() que adems de devolver el puntero lo elimina del 352
13.2. Rediseo de los ejemplos anteriores contenedor, poniendo a cero la posicin que ocupaba. Cuando se produce la llamada al destructor de PStash, se prueba si han sido previamente retirados todos los punteros, si no es as, se notica, de modo que es posible prevenir la fuga de memoria. Se vern otras soluciones mas elegantes en captulos posteriores.
Una prueba
Aqu aparece el programa de prueba de Stash, reescrito para PStash:
//: C13:PStashTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} PStash // Test of pointer Stash #include "PStash.h" #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { PStash intStash; // new works with built-in types, too. Note // the "pseudo-constructor" syntax: for(int i = 0; i < 25; i++) intStash.add(new int(i)); for(int j = 0; j < intStash.count(); j++) cout << "intStash[" << j << "] = " << *(int*)intStash[j] << endl; // Clean up: for(int k = 0; k < intStash.count(); k++) delete intStash.remove(k); ifstream in ("PStashTest.cpp"); assure(in, "PStashTest.cpp"); PStash stringStash; string line; while(getline(in, line)) stringStash.add(new string(line)); // Print out the strings: for(int u = 0; stringStash[u]; u++) cout << "stringStash[" << u << "] = " << *(string*)stringStash[u] << endl; // Clean up: for(int v = 0; v < stringStash.count(); v++) delete (string*)stringStash.remove(v); }
Igual que antes, se crean y rellenan varias Stash, pero esta vez con los punteros obtenidos con new. En el primer caso, vase la lneas:
intStash.add( new int(i) );
, con lo que que adems de crear un objeto int en el rea de memoria dinmica, le asigna el valor inicial i. 353
Captulo 13. Creacin dinmica de objetos Para imprimir, es necesario convertir al tipo adecuado el puntero obtenido de PStash::operator[]; lo mismo se repite con el resto de los objetos de PStatsh del programa. Es la consecuencia indeseable del uso de punteros void como representacin subyacente, que se corregir en captulos posteriores. En la segunda prueba, se lee lnea a lnea el propio archivo fuente. Mediante getline() se lee cada lnea de texto en una variable de cadena, de la que se crea una copia independiente. Si le hubiramos pasado cada vez la direccin de line, tendramos un montn de copias del mismo puntero, referidas a la ltima lnea leida. Obsrvese la expresin, en la recuperacin de los punteros:
*(string*)stringStash[v];
El puntero obtenido por medio de operator[] debe ser convertido a string* para tener el tipo adecuado. Despus el string* es derreferenciado y es visto por el compilador como un objeto string que se enva a cout. Antes de destruir los objetos, se han de eliminar las referencias correspondientes mediante el uso de remove(). De no hacerse as, PStash noticar que no se ha efectuado la limpieza correctamente. Vase que en el caso de los punteros a int, no es necesaria la conversin de tipo al carecer de destructor, y lo nico que se necesita es liberar la memoria:
delete intStash.remove(k);
En cambio, para los punteros a string, hace falta la conversin de tipo, so pena de crear otro discreto punto de fuga de memoria:
delete (string*) stringStash.remove(k);
Algunas de estas dicultades pueden resolverse mediante el uso de plantillas, que veremos en el captulo 16.
Esta sentencia asigna espacio suciente en el motculo para 100 objetos MyType y llama al constructor para cada uno de ellos. Lo que se ha obtenido es simplemente un MyType*, exactamente lo mismo que hubiera obtenido de esta otra forma, que crea un nico objeto:
MyType* fp2 = new MyType;
El escritor del programa sabe que fp es la direccin del primer elemento de un vector, por lo que tiene sentido selecionar elementos del mismo mediante una expresin como fp[3], pero qu pasa cuando destruimos el vector?. Las sentencias
delete fp2; // Correcta. delete fp; // Esta no tendr el efecto deseado.
parecen iguales, y sus efectos sern los mismos. Se llamar al destructor del objeto MyType al que apunta el puntero dado y despus se liberar el bloque asignado. Esto es correcto para fp2, pero no lo para fp, signica 354
13.3. new y delete para vectores que los destructores de los 99 elementos restantes del vector no se invocarn. Sin embargo, s se liberar toda la memoria asignada al vector, ya que fue obtenida como un nico gran bloque cuyo tamao qued anotado en alguna parte por las rutinas de asignacin. Esto se soluciona indicando al compilador que el puntero que pasamos es la direccin de inicio de un vector, usando la siguiente sintaxis:
delete [] fp;
Los corchetes indican al compilador la necesidad de generar el cdigo para obtener el nmero de objetos en el vector, que fue guardado en alguna parte cuando se cre, y llamar al destructor para cada uno de dichos elementos. Esta es una mejora sobre la sintaxis primitiva, que puede verse ocasionalmente en el cdigo de viejos programas:
delete [100] fp;
que forzaba al programador a incluir el nmero de objetos contenidos en el vector, introduciendo con ello una posible fuente de errores. El esfuerzo adicional que supone para el compilador tener en esto en cuenta es pequeo, y por eso se consider preferible especicar el nmero de objetos en un lugar y no en dos.
o bien
const int* q = new int[10];
pero en ambos casos el especicador const queda asociado al int, es decir, al valor al que apunta, en lugar de al puntero en si. Si se quiere conseguir el efecto deseado, en lugar de las anteriores, se debe poner:
int* const q = new int[10];
Ahora es posible modicar el valor de los elementos del vector, siendo ilegal cualquier intento posterior de modicar q, como q++ por ejemplo, al igual que ocurre con el identicador de un vector ordinario.
//: C13:NewHandler.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Changing the new-handler #include <iostream> #include <cstdlib> #include <new> using namespace std; int count = 0; void out_of_memory() { cerr << "memory exhausted after " << count << " allocations!" << endl; exit(1); } int main() { set_new_handler(out_of_memory); while(1) { count++; new int[1000]; // Exhausts memory } }
La funcin a instalar debe retornar void y no tomar argumentos. El bucle while seguir pidiendo bloques de int hasta consumir la memoria libre disponible, sin que haga nada con ellos. Justo a la siguiente llamada a new, no habr espacio para asignar y se producir la llamada al gestor de new. Este comportamiento del new-handler est asociado al operator new(), de modo que si se sobrecarga operator new()(asunto que se trata en la siguiente seccin), no se producir la llamada al gestor de new. Si se desea que se produzca dicha llamada ser necesario que lo haga en el operator new() que substituya al original. Por supuesto, es posible escribir gestores new ms sosticados, incluso alguno que intente reclamar los bloques asignados que no se usan (conocidos habitualmente como recolectores de basura). Pero este no es un trabajo adecuado para programadores noveles.
13.3. new y delete para vectores la misma cantidad de tiempo, y que no est permitida la fragmentacin ni el agotamiento en el rea dinmica. La solucin a este problema consiste en utilizar un asignador personalizado; de otro modo, los programadores evitaran usar new y delete es estos casos y desperdiciaran un recurso muy valioso de C++. A la hora de sobrecargar operator new() y operator delete() es importante tener en cuenta que lo nico que se est cambiando es la forma en que se realiza la asignacin del espacio. El compilador llamar a la nueva versin de new en lugar de al original, para asignar espacio, llamando despus al constructor que actuar sobre l. As que, aunque el compilador convierte una expresin new en cdigo para asignar el espacio y para llamar al constructor, todo lo que se puede cambiar al sobrecargar new es la parte correspondiente a la asignacin. (delete tiene una limitacin similar. Cuando se sobrecarga operator new(), se est reemplazando tambin el modo de tratar los posibles fallos en la asignacin de la memoria. Se debe decidir qu acciones va a realizar en tal caso: devolver cero, un bucle de reintento con llamada al new-handler, o lo que es ms frecuente, disparar una excepcin bad_alloc (tema que se trata en el Volumen 2). La sobrecarga de new y delete es como la de cualquier otro operador. Existe la posibilidad de elegir entre sobrecarga global y sobrecarga para una clase determinada.
357
Aqu puede verse la forma general de sobrecarga de operadores new y delete. Estos operadores sustitutivos usan las funciones malloc() y free() de la bliblioteca estndar de C, que es probablemente lo que ocurre en los operadores originales. Imprimen tambin mensajes sobre lo que estn haciendo. Ntese que no se han usado iostreams sino printf() y puts(). Esto se hace debido a que los objetos iostream como los globales cin, cout y cerr llaman a new para obtener memoria 3 . Usar printf() evita el fatal bloqueo, ya que no hace llamadas a new. En main(), se crean algunos objetos de tipos bsicos para demostrar que tambin en estos casos se llama a los operadores new y delete sobrecargados. Posteriormente, se crean un objeto simple y un vector, ambos de tipo S. En el caso del vector se puede ver, por el nmero de bytes pedidos, que se solicita algo de memoria extra para incluir informacin sobre el nmero de objetos que tendr. En todos los casos se efecta la llamada a las versiones globales sobrecargadas de new y delete.
Provocara una serie continua de llamadas a new hasta que agotada el rea de la pila y abortara el programa.
358
El espacio de almacenamiento para el montculo Framis se crea sobre el bloque obtenido al declarar un vector de tamao suciente para contener psize objetos de clase Framis. Se ha declarado tambin una variable lgica para cada uno de los psize bloques en el vector. Todas estas variables lgicas son inicializadas a false usando el truco consistente en inicializar el primer elemento para que el compilador lo haga automticamente con los restantes inicindolos a su valor por defecto, false, en el caso de variables lgicas. El operador new() local usa la misma sintaxis que el global. Lo nico que hace es buscar una posicin 359
Captulo 13. Creacin dinmica de objetos libre, es decir, un valor false en el mapa de localizacon alloc_map. Si la encuentra, cambia su valor a true para marcarla como ocupada, y devuelve la direccin del bloque correspondiente. En caso de no encontrar ningn bloque libre, enva un mensaje al chero de trazas y dispara una excepcin de tipo bad_alloc. Este es el primer ejemplo con excepcin que aparece en este libro. En el Volumen 2 se ver una discusin detallada del tratamiento de excepciones, por lo que en este ejemplo se hace un uso muy simple del mismo. El operador new hay dos expresiones relacionadas con el tratamiento de excepciones. Primero, a la lista de argumentos de funcin le sigue la expresin throw(bad_alloc), esto informa al compilador que la funcin puede disparar una excepcin del tipo indicado. En segundo lugar, si efectivamente se agota la memoria, la funcin alcanzar la sentencia throw bad_alloc() lanzando la excepcin. En el caso de que esto ocurra, la funcin deja de ejecutarse y se cede el control del programa a la rutina de tratamiento de excepcin que se ha denido en una clusula catch(bad_alloc). En main() se puede ver la clusula try-catch que es la otra parte del mecanismo. El cdigo que puede lanzar la excepcin queda dentro del bloque try; en este caso, llamadas a new para objetos Framis. Justo a continuacin de dicho bloque sigue una o varias clusulas catch, especicando en cada una la excepcin a la que se destina. En este caso, catch(bad_alloc) indica que en ese bloque se tratarn las excepciones de tipo bad_alloc. El cdigo de este bloque slo se ejecutar si se dispara la excepcin, continuando la ejecucin del programa justo despus de la ltima del grupo de clusulas catch que existan. Aqu slo hay una, pero podra haber ms. En este ejemplo, el uso de iostream es correcto ya que el operator new() global no ha sido modicado. El operator delete() asume que la direccin de Framis ha sido obtenida de nuestro almacn particular. Una asuncin justa, ya que cada vez que se crea un objeto Framis simple se llama al operator new() local; pero cuando se crea un vector de tales objetos se llama al new global. Esto causara problemas si el usuario llamara accidentalmente al operador delete sin usar la sintaxis para destruccin de vectores. Podra ser que incluso estuviera tratando de borrar un puntero a un objeto de la pila. Si se cree que estas cosas pudiedeb suceder, conviene pensar en aadir una lnea que asegurare que la direccin est en el intervalo correcto (aqu se demuestra el potencial que tiene la sobrecarga de los operadores new y delete para la localizacon de fugas de memoria). operador delete() calcula el bloque al que el puntero representa y despus pone a false la bandera correspondiente en el mapa de localizacin, para indicar que dicho bloque est libre. En la funcin main(), se crean dinmicamente sucientes objetos Framis para agotar la memoria. Con esto se prueba el comportamiento del programa en este caso. A continuacin, se libera uno de los objetos y se crea otro para mostrar la reutilizacin del bloque recin liberado. Este esquema especco de asignacin de memoria es probablemente mucho ms rpido que el esquema de propsito general que usan los operadores new y delete originales. Se debe advertir, no obstante, que este enfoque no es automticamente utilizable cuando se usa herencia, un tema que ver en el Captulo 14.
360
Si exceptuamos la informacin de rastreo que se aade aqu, las llamadas a las versiones globales de new y delete causan el mismo efecto que si estos operadores no se hubieran sobrecargado. Como se ha visto anteriormente, es posible usar cualquier esquema conveniente de asignacin de memoria en estos operadores modicados. Se puede observar que la sintaxis de new y delete para vectores es la misma que la usada para objetos simples aadindoles el operador subndice []. En ambos casos se le pasa a new como argumento el tamao del bloque de memoria solicitado. A la versin para vectores se le pasa el tamao necesario para albergar todos sus componentes. Conviene tener en cuenta que lo nico que se requiere del operator new() es que devuelva un puntero a un bloque de memoria sucintemente grande. Aunque es posible inicializar el bloque referido, eso es trabajo del constructor, que se llamar automticamente por el compilador. El constructor y el destructor smplemente imprimen mensajes para que pueda verse que han sido llamados. A continuacin se muestran dichos mensajes:
new Widget Widget::new: 40 bytes * delete Widget ~Widget::delete new Widget[25] Widget::new: 1004 bytes ************************* delete []Widget ~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]
La creacin de un nico objeto Widget requiere 40 bytes, tal y como se podra esperar para una mquina que usa 32 bits para un int. Se invoca al operator new() y luego al constructor, que se indica con la impresin del carcter *. De forma complementaria, la llamada a delete provoca primero la invocacin del destructor y slo despus, la de operator delete(). 361
Captulo 13. Creacin dinmica de objetos Cuando lo que se crea es un vector de objetos Widget, se observa el uso de la versin de operator new() para vectores, de acuerdo con lo dicho anteriormente. Se observa que el tamao del bloque solicitado en este caso es cuatro bytes mayor que el esperado. Es en estos cuatro bytes extra donde el compilador guarda la informacin sobre el tamao del vector. De ese modo, la expresin
delete []Widget;
informa al compilador que se trata de un vector, con lo cual, generar el cdigo para extraer la informacin que indica el nmero de objetos y para llamar otras tantas veces al destructor. Obsrvese que aunque se llame solo una vez a operator new() y operator delete() para el vector, se llama al constructor y al destructor una vez para cada uno de los objetos del vector.
Llamadas al constructor
Considerando que
MyType* f = new MyType;
llama a new para obtener un bloque del tamao de MyType invocando despus a su constructor, qu pasara si la asignacin de memoria falla en new?. En tal caso, no habr llamada al constructor al que se le tendra que pasar un puntero this nulo, para un objeto que no se ha creado . He aqu un ejemplo que lo demuestra:
//: C13:NoMemory.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Constructor isnt called if new fails #include <iostream> #include <new> // bad_alloc definition using namespace std; class NoMemory { public: NoMemory() { cout << "NoMemory::NoMemory()" << endl; } void* operator new(size_t sz) throw(bad_alloc){ cout << "NoMemory::operator new" << endl; throw bad_alloc(); // "Out of memory" } }; int main() { NoMemory* nm = 0; try { nm = new NoMemory; } catch(bad_alloc) { cerr << "Out of memory exception" << endl; } cout << "nm = " << nm << endl; }
Cuando se ejecuta, el programa imprime los mensajes del operator new() y del manejador de excepcin, pero no el del constructor. Como new nunca retorna, no se llama al constructor y por tanto no se imprime su mensaje. Para asegurar que no se usa indebidamente, Es importante inicializar nm a cero, debido a que new no se completa. El cdigo de manejo de excepciones debe hacer algo ms que imprimir un mensaje y continuar como si 362
13.3. new y delete para vectores el objeto hubiera sido creado con xito. Idealmente, debera hacer algo que permitiera al programa recuperarse del fallo, o al menos, provocar la salida despus de registrar un error. En las primeras versiones de C++, el comportamiento estndar consista en hacer que new retornara un puntero nulo si la asignacin de memoria fallaba. Esto poda impedir que se llamara al constructor. Si se intenta hacer esto con un compilador que sea conforme al estndar actual, le informar de que en lugar de devolver un valor nulo, debe disparar una excepcin de tipo bad_alloc.
pasar a como segundo argumento al operador operator new(). Por supuesto, slo funcionar si ha sido declarado el operator new() adecuado. He aqu un ejemplo demostrativo de cmo se usa esto para colocar un objeto en una posicin particular:
//: C13:PlacementOperatorNew.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Placement with operator new() #include <cstddef> // Size_t #include <iostream> using namespace std; class X { int i; public: X(int ii = 0) : i(ii) { cout << "this = " << this << endl; } ~X() { cout << "X::~X(): " << this << endl; } void* operator new(size_t, void* loc) { return loc; } }; int main() { int l[10]; cout << "l = " << l << endl;
363
Obsrve que lo nico que hace el operador new es retornar el puntero que se pasa. Por tanto, es posible especicar la direccin en la que se quiere construir el objeto. Aunque este ejemplo muestra slo un argumento adicional, nada impide aadir otros, si se considera conveniente para sus propsitos. Al tratar de destruir estos objetos surge un problema. Slo hay una versin del operador delete, de modo que no hay forma de decir: "Usa mi funcin de liberacin de memoria para este objeto". Se requiere llamar al destructor, pero sin utilizae el mecanismo de memoria dinmica, ya que el objeto no est alojado en el montculo. La solucin tiene una sintaxis muy especial. Se debe llamar explcitamente al destructor, tal como se muestra:
xp->X::~X(); //Llamada explcita al destructor
Hay que hacer una llamada de atencin al respecto. Algunas personas ven esto como un modo de destruir objetos en algn momento anterior al determinado por las reglas de mbito, en lugar de ajustar el mbito, o ms correctamente, en lugar de usar asignacin dinmica como medio de determinar la duracin del objeto en tiempo de ejecucin. Esto es un error, que puede provocar problemas si se trata de destruir de esta manera un objeto ordinario creado en la pila, ya que el destructor ser llamado de nuevo cuando se produzca la salida del mbito correspondiente. Si se llama de esta forma directa al destructor de un objeto creado dinmicamente, se llevar a cabo la destruccin, pero no la liberacin del bloque de memoria, lo que probablemente no es lo que se desea. La nica razn para este tipo de llamada explcita al destructor es permitir este uso especial del operador new, para emplazamiento en memoria. Existe tambin una forma de operador delete de emplazamiento que slo es llamada en caso de que el constructor dispare una excepcin, con lo que la memoria se libera automticamente durante la excepcin. El operador delete de emplazamiento usa una lista de argumentos que se corresponde con la del operador new de emplazamiento que fue llamado previamente a que el constructor lanzase la excepcin. Este asunto se tratar en el Volumen 2, en un captulo dedicado al tratamiento de excepciones.
13.4. Resumen
La creacin de objetos en la pila es ecaz y conveniente, pero para resolver el problema general de programacin es necesario poder crear y destruir objetos en cualquier momento en tiempo de ejecucin, en particular, para que pueda responder a la informacin externa al programa. Aunque C ofrece funciones de asignacin dinmica, stas no proporcionan la facilidad de uso ni la construccin garantizada de objetos que se necesita en C++. Al llevar al ncleo mismo del lenguaje gracias al uso de los operadores new y delete, la creacin dinmica de objetos se hace tan fcil como la creacin de objetos en la pila, aadiendo adems una gran exibilidad. Se puede modicar el comportamiento de new y delete si no se ajusta a los requerimientos, particularmente para mejorar la eciencia, y tambin es posible denir su comportamiento en caso de agotarse la memoria libre.
13.5. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Crear una clase Counted que contenga un int id y un static int count. El constructor por defecto debe empezar con Counted():id(count++){. Tambin deber mostrar mensajes con su id, adems de alguno que muestre que se est creando. El destructor debe mostrar que est siendo destruido y su id. Probar su funcionamiento. 2. Compruebe que new y delete llaman siempre a constructores y destructores, creando mediante el uso de new un objeto de la clase Counted del ejercicio 1, y destruyndolo despus con delete. Cree y destruya 364
13.5. Ejercicios un vector de Counted en el montculo. 3. Cree un objeto de la clase PStash, y llnelo de los objetos del ejercicio 1. Observe lo que sucede cuando el objeto PStash sale de su mbito y es llamado su destructor. 4. Cree un vector de Counted* y crguelo con punteros a objetos Counted. Recorra el vector llamando imprimiendo cada objeto, repita este paso y elimnelos uno a uno. 5. Repita el ejercicio 4 aadiendo una funcin miembro f() de Counted que muestre un mensaje. Recorra el vector llamando a f() para cada objeto del vector. 6. Repita el ejercicio 5 usando un objeto PStash. 7. Repita el ejercicio 5 usando Stack4.h del captulo 9. 8. Cree mediante asignacin dinmica un vector de objetos de clase Counted. Llame a delete con el puntero resultante como argumento, sin usar el operador subndice []. Explique el resultado. 9. Cree un objeto de clase Counted mediante new, convierta el puntero resultante a void* y luego brrelo. Explique el resultado. 10. Compile y ejecute el programa NewHandler.cpp en su ordenador. A partir del nmero resultante, calcule la cantidad de memoria libre disponible para su programa. 11. Cree una clase y dena en ella operadores de sobrecarga para new y delete, para objetos simples y para vectores de objetos. Demuestre que ambas versiones funcionan. 12. Disee un test que le permita evaluar de forma aproximada la mejora en velocidad obtenida en Framis. cpp con el uso de las versiones adaptadas de new y delete, respecto de la obtenida con las globales . 13. Modique NoMemory.cpp para que contenga un vector de enteros y realmente obtenga memoria en lugar de disparar bad_alloc. Establezca un bucle while en el cuerpo de main() similar al que existe en NewHandler.cpp para agotar la memoria. Observe lo que sucede en el caso de que su operador new no compruebe el xito de la asignacin de memoria. Aada despus esa comprobacin a su operador new y la llamada a throw bad_alloc. 14. Cree una clase y dena un operador new de emplazamiento, con un string como segundo argumento. Dena un vector de string, en el que se almacenar este segundo argumento a cada llamada a new. El operador new de emplazamiento asignar bloques de manera normal. En main(), haga llamadas a este operador new pasndole como argumentos cadenas de caracteres que describan las llamadas. Para ello, puede hacer uso de las macros __FILE__ y __LINE__ del preprocesador. 15. Modique ArrayOperatorNew.cpp deniendo un vector esttico de Widget* que aada la direccin de cada uno de los objetos Widget asignados con new, y la retire cuando sea liberada mediante delete. Puede que necesite buscar informacin sebre vectores en la documentacin de la biblioteca estndar de C++, o en el segundo volumen de este libro que est disponible en la web del autor. Cree una segunda clase a la que llamar MemoryChecker, que contenga un destructor que muestre el nmero de punteros a Widget en su vector. Disee un programa con una nica instancia global de MemoryChecker, y en main(), cree y destruya dinmicamente varios objetos y vectores de objetos Widget. Observe que MemoryCheck revela fugas de memoria.
365
367
Captulo 14. Herencia y Composicin En esta clase los miembros son privados, y entonces, es completamente seguro declarar un objeto del tipo X pblico en la nueva clase, y por ello, permitir una interfaz directa:
//: C14:Composition.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Reuse code with composition #include "Useful.h" class Y { int i; public: X x; // Embedded object Y() { i = 0; } void f(int ii) { i = ii; } int g() const { return i; } }; int main() { Y y; y.f(47); y.x.set(37); // Access the embedded object }
Para acceder a las funciones miembro alojadas en el objeto (referido como subobjeto) simplemente requiere otra seleccin del miembro. Es habitual hacer privado el objeto alojado, y por ello, formar parte de la capa de implementacin (lo que signica que es posible cambiar la implementacin si se desea). La interfaz de funciones de la nueva clase implica el uso del objeto alojado, pero no necesariamente imita a la interfaz del objeto.
//: C14:Composition2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Private embedded objects #include "Useful.h" class Y { int i; X x; // Embedded object public: Y() { i = 0; } void f(int ii) { i = ii; x.set(ii); } int g() const { return i * x.read(); } void permute() { x.permute(); } }; int main() { Y y; y.f(47); y.permute(); }
Aqu, la funcin permute() se ha aadido a la interfaz de la clase, pero el resto funciones de X son utilizadas dentro de los miembros de Y. 368
Como se puede observar, Y hereda de X, que signica que Y contendr todos los miembros de X y todas las funciones de X. De hecho, Y contiene un subobjeto X como si se hubiese creado un objeto X dentro de la clase Y en vez de heredar de X. Tanto los miembros objetos y la clase base son conocidos como subobjetos. Todos los elementos privados de X continan siendo privados en Y; esto es, aunque Y hereda de X no signica que Y pueda romper el mecanismo de proteccin. Los elementos privados de X continan existiendo, ocupando su espacio - slo que no se puede acceder a ellos directamente. En main() observamos que los datos de Y estn combinados con los datos de X porque sizeof(Y) es el doble de grande que el sizeof(X). Observar que la clase base es precedida por public. Durante la herencia, por defecto, todo es privado. Si la clase base no estuviese precedida por public, signicara que todos los miembros pblicos de la clase base seran privados en la clase derivada. Esto, en la mayora de ocasiones no es lo deseado [51]; el resultado que se desea es mantener todos los miembros pblicos de la clase base en la clase derivada. Para hacer esto, se usa la palabra clave public durante la herencia. 369
Captulo 14. Herencia y Composicin En change(), se utiliza a la funcin de la clase base permute(). La clase derivada tiene acceso directo a todas las funciones pblicas de la clase base. La funcin set() en la clase derivada redene la funcin set() de la clase base. Esto es, si llama a las funciones read() y permute() de un objeto Y, conseguir las versiones de la clase base (esto es lo que esta ocurriendo dentro de main()). Pero si llamamos a set() en un objeto Y, conseguiremos la versin redenida. Esto signica que si no deseamos un comportamiento de una funcin durante la herencia, se puede cambiar. (Tambin se pueden aadir funciones completamente nuevas como change().) Sin embargo, cuando redenimos una funcin, puede ser que desee llamar a la versin de la clase base. Si, dentro de set(), simplemente llama a set(), conseguiremos una versin local de la funcin - una funcin recursiva. Para llamar a la versin de la clase base, se debe explcitamente utilizar el nombre de la clase base y el operador de resolucin de alcance.
Esta sera la forma de un constructor de la clase MyType2, la cual hereda de Bar y contiene un miembro objeto llamado m. Fjese que mientras podemos ver el tipo de la clase base en la lista de inicializadores del constructor, slo podemos ver el miembro identicador objeto.
14.3. Lista de inicializadores de un constructor para programar. Una vez en el inicio del constructor, puede asumir que todos los subobjetos estn correctamente inicializados y centrarse en las tareas que se desean realizar en el constructor. Sin embargo, existe un contratiempo: Qu ocurre con los objetos predenidos, aquellos que no tienen constructor? Para hacer una sintaxis slida, piense en los tipos predenidos como si tuviesen un solo constructor, con un solo parmetro: una variable del mismo tipo como el que esta inicializando. Esto es
//: C14:PseudoConstructor.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class X { int i; float f; char c; char* s; public: X() : i(7), f(1.4), c(x), s("howdy") {} }; int main() { X x; int i(100); // Applied to ordinary definition int* ip = new int(47); }
El propsito de esta "pseudo-llamadas a los constructores" es una simple asignacin. Es una tcnica recomendada y un buen estilo de programacin, que usted ver usar a menudo. ncluso es posible utilizar esta sintaxis cuando se crean variables de tipos predenidos fuera de la clase:
int i(100); int* ip = new int(47);
De esta forma, los tipos predenidos actan, ms o menos, como los objetos. Sin embargo, recuerde que no son constructores reales. En particular, si usted no realiza una llamada explcita al constructor, no se ejecutar ninguna inicializacin.
371
C hereda de B y tiene un objeto miembro ("esta compuesto de") del tipo A. Puede comparar que la lista de inicializadores contiene las llamadas al constructor de la clase base y las constructores de los objetos miembros. La funcin C::f() redene B::f(), que era heredada, y tambin llama a la versin de la clase base. Adems, se llama a a.f(). Fjese que durante todo este tiempo estamos hablando acerca de la redenicin de funciones durante la herencia; con un objeto miembro que slo se puede manipular su interfaz pblica, no redenirla. Adems, al llamar a f() en un objeto de la clase C no podr llamar a a.f() si C::f() no ha sido denido, mientras que sera posible llamar a B::f().
372
Primero, se crea un objeto ofstream para enviar la salida a un archivo. Entonces, para no teclear tanto y demostrar la tcnica de las macros que ser sustituida por otra mucho ms mejorada en el captulo 16, se crea una para construir varias clases que utilizan herencia y composicin. Cada constructor y destructor escribe informacin en el archivo de salida. Fjense que los constructores no son constructores por defecto; cada uno tiene un parmetro del tipo int. Y adems, el argumento no tiene nombre; la nica razn de su existencia es forzar la llamada al constructor en la lista de inicializadores del constructor. (Eliminando el identicador evita que el compilador informe con mensajes de advertencia) La salida de este programa es
Base1 constructor Member1 constructor Member2 constructor Derived1 constructor Member3 constructor Member4 constructor Derived2 constructor Derived2 destructor Member4 destructor Member3 destructor Derived1 destructor Member2 destructor Member1 destructor Base1 destructor
373
Captulo 14. Herencia y Composicin omo puede observar, la construccin empieza desde la raz de la jerarqua de clases y en cada nivel, el constructor de la clase base se ejecuta primero, seguido por los constructores de los objetos miembro. Los destructores son llamados exactamente en el orden inverso que los constructores -esto es importante debido a los problemas de dependencias (en el constructor de la clase derivada o en el destructor, se debe asumir que el subobjeto de la clase base esta todava disponible para su uso, y ha sido construido - o no se ha destruido todava). Tambin es interesante que el orden de las llamadas al constructor para los objetos miembro no afecten para nada el orden de las llamadas en la lista de inicializadores de un constructor. El orden es determinado por el orden en que los objetos miembros son declarados en la clase. Si usted pudiera cambiar el orden del constructor en la lista de inicializadores de un constructor, usted podra tener dos secuencias diferentes de llamada en dos constructores diferentes, pero el destructor no sabra como invertir el orden para llamarse correctamente y nos encontraramos con problemas de dependencias.
374
En Base se observa una funcin sobrecargada f(), en Derived1 no se realiza ningn cambio a f() pero se redene la funcin g(). En main(), se observa que ambas funciones f() estn disponibles en Derived1. Sin embargo, Derived2 redene una versin sobrecargada de f() pero no la otra, y el resultado es que la segunda forma de sobrecarga no esta disponible. En Derived3, se ha cambiado el tipo de retorno y esconde ambas versiones de la clase base, y Derived4 muestra que al cambiar la lista de argumentos tambin se esconde las versiones de la clase base. En general, usted puede expresar cada vez que redene una funcin sobrecargada de la clase base, que todas las otras versiones son automticamente escondidas en la nueva clase. En el captulo 15, ver como aadir la palabra reservada virtual que afecta un signicativamente a la sobrecarga de una funcin. Si cambia la interfaz de la clase base modicando la signatura y/o el tipo de retorno de una funcin miembro desde la clase base, entonces esta utilizando la clase de una forma diferente en que la herencia esta pensado para realizar normalmente. Esto no quiere decir que lo que este haciendo sea incorrecto, esto es que el principal objetivo de la herencia es soportar el polimorsmo, y si usted cambia la signatura de la funcin o el tipo de retorno entonces realmente esta cambiando la interfaz de la clase base. Si esto es lo que esta intentando hacer entonces esta utilizando la herencia principalmente para la reutilizacin de cdigo, no para mantener interfaces comunes en la clase base (que es un aspecto esencial del poliformismo). En general, cuando usa la herencia de esta forma signica que esta en una clase de propsito general y la especializacin para una necesidad particular - que generalmente, pero no siempre, se considera parte de la composicin. Por ejemplo, considere la clase Stack del captulo 9. Uno de los problemas con esta clase es que se tena que realizar que convertir cada vez que se consegua un puntero desde el contenedor. Esto no es solo tedioso, tambin inseguro - se puede convertir a cualquier cosa que desee. Un procedimiento que a primera vista parece mejor es especializar la clase general Stack utilizando la herencia. Aqu esta un ejemplo que utiliza la clase del captulo 9.
//: C14:InheritStack.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Specializing the Stack class #include "../C09/Stack4.h" #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std;
375
class StringStack : public Stack { public: void push(string* str) { Stack::push(str); } string* peek() const { return (string*)Stack::peek(); } string* pop() { return (string*)Stack::pop(); } ~StringStack() { string* top = pop(); while(top) { delete top; top = pop(); } } }; int main() { ifstream in("InheritStack.cpp"); assure(in, "InheritStack.cpp"); string line; StringStack textlines; while(getline(in, line)) textlines.push(new string(line)); string* s; while((s = textlines.pop()) != 0) { // No cast! cout << *s << endl; delete s; } }
Como todas las funciones miembros en Stack4.h son inline, no es necesario ser enlazadas. StringStack especializa Stack para que push() acepte solo punteros a String. Antes, Stack acepta punteros a void, y as el usuario no tena que realizar una comprobacin de tipos para asegurarse que el punteros fuesen insertados. Adems, peek() and pop() ahora retornan punteros a String en vez de punteros a void, entonces no es necesario realizar la conversin para utilizar el puntero. orprendido este mecanismo de comprobacin de tipos seguro es gratuito, en push(), peek() y pop! Al compilador se le proporciona informacin extra acerca de los tipos y ste lo utiliza en tiempo de compilacin, pero las funciones son inline y no es necesario ningn cdigo extra. La ocultacin de nombres entra en accin en la funcin push() que tiene la signatura diferente: la lista de argumentos. Si se tuviesen dos versiones de push() en la misma clase, tendran que ser sobrecargadas, pero en este caso la sobrecarga no es lo que deseamos porque todava permitira pasar cualquier tipo de puntero a push como void *. Afortunadamente, C++ esconde la versin push (void *) en la clase base en favor de la nueva versin que es denida en la clase derivada, de este modo, solo se permite utilizar la funcin push() con punteros a String en StringStack. Ahora podemos asegurar que se conoce exactamente el tipo de objeto que esta en el contenedor, el destructor funcionar correctamente y problema esta resuelto - o al menos, un parte del procedimiento. Si utiliza push( ) con un puntero a String en StringStack, entonces (segn el signicado de StringStack) tambin se esta pasando el puntero a StringStack. Si utiliza pop(), no solo consigue puntero, sino que a la vez el propietario. Cualquier puntero que se haya dejado en StringStack ser borrado cuando el destructor sea invocado. Puesto que siempre son punteros a Strings y la declaracin delete esta funcionando con punteros a String en vez de punteros a void, la destruccin se realiza de forma adecuada y todo funciona correctamente. Esto es una desventaja: esta clase solo funciona con punteros de cadenas. Si se desea un Stack que funcione con cualquier variedad de objetos, se debe escribir una nueva versin de la clase que funcione con ese nuevo tipo de objeto. Esto puede convertirse rpidamente en una tarea tediosa, pero nalmente es resulta utilizando plantillas 376
14.5. Funciones que no heredan automticamente como se vera en el captulo 16. Existen consideraciones adicionales sobre este ejemplo: el cambio de la interfaz en Stack en el proceso de herencia. Si la interfaz es diferente, entonces StringStack no es realmente un Stack, y nunca ser posible usar de forma correcta un StringStack como Stack. Esto hace que el uso de la herencia sea cuestionable en este punto; si no se esta creando un StringStack del tipo Stack, entonces, porque hereda de l. Ms adelante, sen este mismo captulo se mostrar una versin ms adecuada.
377
Los constructores y el operator= de GameBoard y Game se describen por si solos y por ello distinguir cuando son utilizados por el compilador. Adems, el operador Other() ejecuta una conversin automtica de tipo desde un objeto Game a un objeto de la clase anidada Other. La clase Chess simplemente hereda de Game y no crea ninguna funcin (slo para ver como responde el compilador) La funcin f() coge un objeto Other para comprobar la conversin automtica del tipo. En main(), el constructor creado por defecto y el constructor copia de la clase derivada Chess son ejecutados. Las versiones de Game de estos constructores son llamados como parte de la jerarqua de llamadas a los constructores. Aun cuando esto es parecido a la herencia, los nuevos constructores son realmente creados por el compilador. Como es de esperar, ningn constructor con argumentos es ejecutado automticamente porque es demasiado trabajo para el compilador y no es capaz de intuirlo. 378
14.5. Funciones que no heredan automticamente El operator= es tambin es creado como una nueva funcin en Chess usando la asignacin (as, la versin de la clase base es ejecutada) porque esta funcin no fue explcitamente escrita en la nueva clase. Y, por supuesto el destructor es creado automticamente por el compilador. El porqu de todas estas reglas acerca de la reescritura de funciones en relacin a la creacin de un objeto pueden parecer un poco extraas en una primera impresin y como se heredan las conversiones automticas de tipo. Pero todo esto tiene sentido - si existen suciente piezas en Game para realizar un objeto Other, aquellas piezas estn todava en cualquier objeto derivado de Game y el tipo de conversin es vlido (aun cuando puede, si lo desea, redenirlo). El operator= es creado automticamente slo para asignar objeto del mismo tipo. Si desea asignar otro tipo, deber escribir el operator= usted mismo. Si mira con detenimiento Game, observar que el constructor copia y la asignacin tienen llamadas explicitas a constructor copia del objeto miembro y al operador de asignacin. En la mayora de ocasiones, deber hacer esto porque sino, en vez del constructor copia, ser llamado el constructor por defecto del objeto miembro, y en el caso del operador de asignacin, ninguna asignacin se har en los objetos miembros! Por ltimo, fjese en Checkers, dnde explcitamente se escribe un constructor por defecto, constructor copia y los operadores de asignacin. En el caso del constructor por defecto, el constructor por defecto de la clase base se llama automticamente, y esto es lo normalmente que se desea hacer. Pero, aqu existe un punto importante, tan pronto que se decide escribir nuestro propio constructor copia y operador de asignacin, el compilador asume que usted sabe lo que esta haciendo y no ejecutar automticamente las versiones de la clase base as como las funciones creadas automticamente. Si se quiere ejecutar las versiones de la clase base, debe llamarlas explcitamente. En el constructor copia de Checkers, esta llamada aparece en la lista de inicializacin del constructor:
Checkers(const Checkers& c) : Game(c) {
En el operador de asignacin de Checkers, la clase base se llama en la primera lnea del cuerpo de la funcin:
Game::operator=(c);
Estas llamadas deben seguirse de forma cannica cuando usa cualquier clase derivada.
Captulo 14. Herencia y Composicin seguro y cuando el usuario conoce que esta formando un conjunto de piezas, hace que la interfaz sea ms fcil de entender. Un buen ejemplo es la clase Car:
//: C14:Car.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Public composition class Engine { public: void start() const {} void rev() const {} void stop() const {} }; class Wheel { public: void inflate(int psi) const {} }; class Window { public: void rollup() const {} void rolldown() const {} }; class Door { public: Window window; void open() const {} void close() const {} }; class Car { public: Engine engine; Wheel wheel[4]; Door left, right; // 2-door }; int main() { Car car; car.left.window.rollup(); car.wheel[0].inflate(72); }
Como la composicin de Car es parte del anlisis del problema (y no una simple capa del diseo), hace pblicos los miembros y ayudan al programador a entender como se utiliza la clase y requiere menos complejidad de cdigo para el creador de la clase. Si piensa un poco, observar que no tiene sentido componer un Car usando un objeto "vehculo" - un coche no contiene un vehculo, es un vehculo. La relacin "es-un" es expresado con la herencia y la relacin "tiene un" es expresado con la composicin.
Subtipado
Ahora suponga que desea crear un objeto del tipo ifstream que no solo abre un chero sino que tambin guarde el nombre del chero. Puede usar la composicin e alojar un objeto ifstream y un string en la nueva clase:
//: C14:FName1.cpp
380
Sin embargo, existe un problema. Se intenta permitir el uso de un objeto FName1 en cualquier lugar dnde se utilice un objeto ifstream, incluyendo una conversin automtica del tipo desde FName1 a ifstream&. Pero en main, la lnea
file.close();
no compilar porque la conversin automtica de tipo slo ocurre cuando se llama a la funcin, no durante la seleccin del miembro. Por ello, esta manera no funcionar. Una segunda manera es aadir la denicin Close() a FName1:
void close() { file.close(); }
Esto funcionar si slo existen unas cuantas funciones a las que se desea hacer funcionar como una clase ifstream. En este caso, solo una parte de la clase y la composicin apropiada. Pero qu ocurre si se quiere que todo funcione cmo la clase deseada? A eso se le llama subtipos porque esta creando un nuevo tipo desde uno ya existente y lo que se quiere es que el nuevo tipo tenga la misma interfaz que el tipo existente (adems de otras funciones que se deseen aadir) para que se pueda utilizar en cualquier lugar donde se utilizaba el tipo existente. Aqu es dnde la herencia es esencial. Puede ver que el subtipo resuelve perfectamente el problema anterior:
381
//: C14:FName2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Subtyping solves the problem #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName2 : public ifstream { string fileName; bool named; public: FName2() : named(false) {} FName2(const string& fname) : ifstream(fname.c_str()), fileName(fname) { assure(*this, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // Dont overwrite fileName = newName; named = true; } }; int main() { FName2 file("FName2.cpp"); assure(file, "FName2.cpp"); cout << "name: " << file.name() << endl; string s; getline(file, s); // These work too! file.seekg(-200, ios::end); file.close(); }
Ahora cualquier funcin que este disponible para el objeto sfstream tambin esta disponible para el objeto FName2. Asimismo se observan funciones no miembro como getline() que esperan un objeto ifstream y que pueden funcionar con un objeto FName2. Esto es porque FName2 es un tipo de ifstream; esto no signica simplemente que lo contiene. Esto es un tema muy importante que ser explorado al nal de este capitulo y el siguiente.
Herencia privada
Puede heredar utilizando una clase base de forma privada borrando public en la lista de la clase base o explcitamente utilizando private (denitivamente la mejor poltica a tomar pues indica al usuario lo que desea hacer). Cuando se hereda de forma privada, esta "implementado en trminos de", esto es, se esta creando una nueva clase que tiene todos los datos y funcionalidad de la clase base, pero la funcionalidad esta oculta, solo una parte de capa de implementacin. La clase derivada no tiene acceso a la capa de funcionalidad y un objeto no puede ser creado como instancia de la clase base (como ocurri en FName2.cpp). Se sorprender del propsito de la herencia privada, porque la alternativa, usar la composicin para crear un objeto privado en la nueva clase parece ms apropiada. La herencia privada esta incluida para completar el lenguaje pero para reducir confusin, normalmente se usar la composicin en vez de la herencia privada. Sin embargo, existen ocasiones donde se desea el mismo interfaz como la clase base y anular tratamiento del objeto. La herencia privada proporciona esta habilidad.
382
14.6. Protected 14.5.2.2.1 Publicar los miembros heredados de forma privada Cuando se hereda de forma privada, todos los miembros pblicos de la clase base llegan como privados. Si desea que cualquiera de ellos sea visible, solo use sus nombres (sin argumentos o valores de retorno) junto con la palabra clave using en una seccin pblica de la clase derivada:
//: C14:PrivateInheritance.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt class Pet { public: char eat() const { return a; } int speak() const { return 2; } float sleep() const { return 3.0; } float sleep(int) const { return 4.0; } }; class Goldfish : Pet { // Private inheritance public: using Pet::eat; // Name publicizes member using Pet::sleep; // Both members exposed }; int main() { Goldfish bob; bob.eat(); bob.sleep(); bob.sleep(1); //! bob.speak();// Error: private member function }
As, la herencia privada es til si desea esconder parte de la funcionalidad de la clase base. Fjese que si expone el nombre de una funcin sobrecargada, expone todas las versiones sobrecargadas de la funcin en la clase base. Debe pensar detenidamente antes de utilizar la herencia privada en vez de la composicin; la herencia privada tiene complicaciones particulares cuando son combinadas con la identicacin de tipos en tiempo de ejecucin (es un tema de un captulo en el volumen 2, disponible en www.BruceEckel.com)
14.6. Protected
Ahora que ya sabe que es la herencia, la palabra reservada protected ya tiene signicado. En un mundo ideal, los miembros privados siempre serian jos-y-rpidos, pero en los proyectos reales hay ocasiones cuando se desea ocultar algo a todo el mundo y todava permitir accesos a los miembros de la clase derivada. La palabra clave protected es un movimiento al pragmatismo: este dice "Esto es privado como la clase usuario en cuestin, pero disponible para cualquiera que hereda de esta clase. La mejor manera es dejar los miembros de datos privados - siempre debe preservar su derecho de cambiar la capa de implementacin. Entonces puede permitir acceso controlado a los herederos de su clase a travs de funciones miembro protegidas:
//: C14:Protected.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The protected keyword #include <fstream> using namespace std;
383
class Base { int i; protected: int read() const void set(int ii) public: Base(int ii = 0) int value(int m) };
class Derived : public Base { int j; public: Derived(int jj = 0) : j(jj) {} void change(int x) { set(x); } }; int main() { Derived d; d.change(10); }
384
El cdigo de prueba anterior es idntico a C12:ByteTest.cpp excepto que Byte2 se usa en vez de Byte. De esta forma todos los operadores son vericados para trabajar con Byte2 a travs de la herencia. Cuando examina la clase Byte2, ver que se ha denido explcitamente el constructor y que solo se ha credo el operator= que asigna un Byte2 a Byte2; cualquier otro operador de asignacin tendr que se realizado por usted.
Captulo 14. Herencia y Composicin mente se esta de acuerdo: debe evitar intentarlo hasta que haya programado bastante y comprenda el lenguaje en profundidad. Por ahora, probablemente no le importa cuando debe absolutamente utilizar la herencia mltiple y siempre puede utilizar la herencia simple Inicialmente, la herencia mltiple parece bastante simple: se aade las clases en la lista de clases base durante la herencia separadas por comas. Sin embargo, la herencia mltiple introduce un nmero mayor de ambigedades, y por eso, un captulo del Volumen 2 hablar sobre el tema.
386
Lo interesante en este ejemplo es la funcin tune(), que acepta una referencia Instrument. Sin embargo, en main() la funcin tune() se llama utilizando una referencia a un objeto Wind. Dado que C++ es un muy peculiar sobre la comprobacin de tipos, parece extrao que una funcin que acepta solo un tipo pueda aceptar otro tipo, al menos que sepa que un objeto Instrument es tambien un objeto Instrument.
El hecho de pasar de la clase derivada a la clase base, esto es, desplazarse hacia arriba en el diagrama de la herencia, es normalmente conocido como upcasting. Upcasting es siempre seguro porque se esta desplazando de un tipo desde un tipo mas especico a otro tipo mas general. - nicamente puede ocurrir es que la interfaz de la clase pierda algunas funciones miembro, pero no ganarlas. Esto es porque el compilador permite el upcasting sin ninguna conversin explicita o notacin especial.
387
El operador de Child es interesante por la forma en que llama al operador del padre dentro de ste: convirtiendo el objeto Child a Parent& (si lo convierte a un objeto de la clase base en vez de una referencia, probablemente obtendr resultados no deseados)
return os << (Parent&)c << c.m
Dado que el compilador lo ve como Parent, ste llama al operador Parent. Puede observar que Child no tiene explcitamente denido un constructor copia. El compilador crea el constructor copia (es una de las cuatro funciones que sintetiza, junto con el constructor del defecto - si no creas a ninguna constructores - el operator= y el destructor) llamando el constructor copia de Parent y el constructor copia de Member. Esto muestra la siguiente salida
Parent(int ii) Member(int ii) Child(int ii)
388
Sin embargo, si escribe su propio constructor copia para Child puede tener un error inocente y funcionar incorrectamente:
Child(const Child& c) : i(c.i), m(c.m) {}
entonces el constructor por defecto ser llamado automticamente por la clase base por parte de Child, aqu es dnde el compilador muestra un error cuando no tienen otra (recuerde que siempre algun constructor se ejecuta para cada objeto, sin importar si es un subobjeto de otra clase). La salida ser entonces:
Parent(int ii) Member(int ii) Child(int ii) calling copy-constructor: Parent() Member(const Member&) values in c2: Parent: 0 Member: 2 Child: 2
Esto probablemente no es lo que espera, generalmente desear que la parte de la clase base sea copiada del objeto existente al nuevo objeto como parte del constructor copia. Para arreglar el problema debe recordar como funciona la llamada al constructor copia de la clase base (como el compilador lo hace) para que escriba su propio constructor copia. Este puede parecer un poco extrao a primera vista pero es otro ejemplo de upcasting.
Child(const Child& c) : Parent(c), i(c.i), m(c.m) { cout << "Child(Child&)\n"; }
La parte extraa es cmo el constructor copia es ejecutado: Parent(c). Qu signica pasar un objeto Child al constructor padre? Child hereda de Parent, entonces una referencia de Child es una referencia Parent. El constructor copia de la clase base convierte a una referencia de Child a una referencia de Parent y la utiliza en el construccin de la copia. Cuando escribe su propio constructor copia la mayora de ocasiones desear lo mismo.
389
El chero es idntico a InheritStack.cpp, excepto que un objeto Stack es alojado en StringStack y se utilizan las funciones miembros para llamarlo. No se consume tiempo o espacio porque el subobjeto tiene el mismo tamao y todas las comprobaciones de tipos han ocurrido en tiempo de compilacin. Sin embargo, esto tiende a confusin, podra tambin utilizar la herencia privada para expresar "implementado en trminos de". Esto tambin resolvera el problema de forma adecuada. Un punto importante es cuando la herencia mltiple puede ser garantizada. En este caso, si observa un diseo en que la composicin pueda utilizarse en vez de la herencia, debe eliminar la necesidad de utilizar herencia mltiple.
Como en la llamada a la funcin, ninguno de estos casos requiere una conversin explicita.
390
14.11. Resumen
el compilador puede utilizar ip solo como un puntero a Instrumento y nada mas. Esto es, ste no puede conocer que ip realmente esta apuntando a un objeto Wind. Entonces cuando llame a la funcin miembro play() diciendo
ip->play(middleC);
el compilador solo puede conocer que la llamada a play() es de un puntero a Instrument y llamara a la versin de la clase base Instrument::play() en vez de lo que debera hacer, que es llamar a Wind::play(). As, no conseguir una conducta adecuada. esto es un problema importante; es resulto en el Capitulo 15, introduccin al tercer pilar de la programacin orientada a objetos: poliformismo (implementado en C++ con funciones virtuales).
14.11. Resumen
Herencia y composicin le permiten crear nuevos tipos desde tipos existentes y ambos incluyen subobjetos de tipos existentes dentro del nuevo tipo. Sin embargo, normalmente, utilizara la composicin para reutilizar tipos existentes como parte de la capa de implementacin de un nuevo tipo y la herencia cuando desea forzar al nuevo tipo a ser del mismo tipo que la clase base (la equivalencia de tipos garantiza la equivalencia de la interfaz). Como las clases derivadas tienen el interfaz de la clase base, esta puede ser convertidas a la base, lo cual es crtico para el poliformismo como ver el Captulo 15. Aunque la reutilizacin de cdigo a travs de la composicin y la herencia es muy til para el desarrollo rpido de proyectos, generalmente deseara redisear su jerarqua de clases antes de permitir a otros programadores dependan de ella. Su objetivo es crear una jerarqua en que cada clase tenga un uso especico y sin ser demasiado grande (esforzndose ms en la funcionalidad que en la dicultad de la reutilizacin...), ni pequea, (no se podr usar por si mismo o sin aadir funcionalidad).
14.12. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Modicar Car.cpp para que herede desde una clase llamada Vehicle, colocando correctamente las funciones miembro en Vehicle (esto es, aadir algunas funciones miembro). Aada un constructor (no el de por defecto) a Vehicle, que debe ser llamado desde dentro del constructor Car 2. Crear dos clases, A y B, con constructor por defectos noticndose ellos mismos. Una nueva clase llamada C que hereda de A, y cree un objeto miembro B dentro de C, pero no cree un constructor para C. Cree un objeto de la clase C y observe los resultados. 3. Crear una jerarqua de clases de tres niveles con constructores por defecto y con destructores, ambos noticndose utilizando cout. Vericar que el objeto ms alto de la jerarqua, los tres constructores y destructores son ejecutados automticamente. Explicar el orden en que han sido realizados. 4. Modicar Combined.cpp para aadir otro nivel de herencia y un nuevo objeto miembro. Aadir el cdigo para mostrar cuando los constructores y destructores son ejecutados. 5. En Combined.cpp, crear una clase D que herede de B y que tenga un objeto miembro de la clase C. Aadir el cdigo para mostrar cuando los constructores y los destructores son ejecutados. 6. Modicar Order.cpp para aadir otro nivel de herencia Derived3 con objetos miembro de la clase Member4 y Member5. Compruebe la salida del programa. 7. En NameHidding.cpp, vericar que Derived2, Derived3 y Derived4, ninguna versin de la clase base de f() esta disponible. 391
Captulo 14. Herencia y Composicin 8. Modicar NameHiding.cpp aadiendo tres funciones sobrecargadas llamadas h() en Base y mostrar como redeniendo una de ellas en una clase derivada oculta las otras. 9. Crear una clase StringVector que herede de vector<void*> y redenir push_back y el operador [] para aceptar y producir string*. Qu ocurre si intenta utilizar push_back() un void*? 10. Escribir una clase que contenga muchos tipos y utilice una llamada a una funcin pseudo-constructor que utiliza la misma sintaxis de un constructor.Utilizarla en el constructor para inicializar los tipos. 11. Crear una clase llamada Asteroid. Utilizar la herencia para especializar la clase PStash del captulo 13 (PStash.h y PStash.cpp) para que la acepte y retorne punteros a Asteroid. Tambin modique PStashTest.cpp para comprobar sus clases. Cambiar la clase para que PStash sea un objeto miembro. 12. Repita el ejercicio 11 con un vector en vez de la clase PStash. 13. En SynthesizedFunctions.cpp, modique Chess para proporcionarle un constructor por defecto, un constructor copia y un operador de asignacin. Demostrar que han sido escritos correctamente. 14. Crear dos clases llamadas Traveler y Pager sin constructores por defecto, pero con constructores que toman un argumento del tipo string, el cual simplemente lo copia a una variable interna del tipo string. Para cada clase, escribir correctamente un constructor copia y el operador de asignacin. Entonces cree la clase BusinessTraveler que hereda de Traveler y crear ub objeto miembro Pager dentro ella. Escribir correctamente el constructor por defecto, un constructor que tome una cadena como argumento, un constructor copia y un operador de asignacin. 15. Crear una clase con dos funciones miembro estticas. Herede de estas clases y redena una de las funciones miembro. Mostrar que la otra funcin se oculta en la clase derivada. 16. Mejorar las funciones miembro de ifstream. En FName2.cpp, intentar suprimirlas del objeto le. 17. Utilice la herencia privada y protegida para crear dos nuevas clases desde la clase base. Intente convertir los objetos de las clases derivadas en la clase base. Explicar lo que ocurre. 18. En Protected.cpp, aadir una funcin miembro en Derived que llame al miembro protegido de Base read(). 19. Cambiar Protected.cpp para que Derived utilice herencia protegida. Compruebe si puede llamar a value() desde un objeto Derived. 20. Crear una clase llamada SpaceShip con un metodo y(). Crear Shuttle que hereda de SpaceShip y aadir el metodo land(). Creat un nuevo Shuttle, convertirlo por puntero o referenciaa SpaceShip e intente llamar al metodo land(). Explicar los resultados. 21. Modicar Instrument.cpp para aadir un mtodo prepare() a Instrument. Llamar a prepare () dentro de tune(). 22. Modicar Instrument.cpp para que play() muestre un mensaje con cout y que Wind redena play() para que muestra un mensaje diferente con cout. Ejecute el programa y explique porque probamenteble no deseara esta conducta. Ahora ponga la palabra reservada virtual (la cual aprender en el capitulo 15) delante de de la declaracin de play en Instrument y observe el cambio en el comportamiento. 23. En CopyConstructor.cpp, herede una nueva clase de Child y proporcionarle un miembro m. Escribir un constructor correcto, un constructor copia, operator= y operator de ostreams y comprobar la clase en main(). 24. Tomar como ejemplo CopyConstructor.cpp y modifquelo aadiendo su propio constructor copia a Child sin llamar el constructor copia de clase base y comprobar que ocurre. Arregle el problema aadiendo una llamada explicita al constructor copia de la clase base en la lista de inicializacin del constructor del constructor copia de Child. 25. Modicar InheritStack2.cpp para utilizar un vector<string> en vez de Stack. 26. Crear una clase Rock con un constructor por defecto, un constructor copia y un operador de asignacin y un destructor, todos ellos mostrndose para saber que han sido ejecutados. En main(), crear un vector<Rock> (esto es, tener objetos Rock por valor) y aadir varios Rocks. Ejecutar el programa y explicar los resultados obtenidos. Fijarse cuando los destructores son llamados desde los objetos Rock en el vector. Ahora repita el ejercicio con un vector<Rock*>. Es posible crear un vector<Rock&>? 392
14.12. Ejercicios 27. En este ejercicio cree un patrn de diseo llamado proxy. Comience con la clase base Subject y proporcinele tres funciones: f(), g() y h(). Ahora herede una clase Proxy y dos clases Implementation1 e Implementacion2 de Subject. Proxy tendra que contener un puntero a un Suboject y todos los miembros de Proxy (usualmente el constructor). En main(), crear dos objetos Proxy diferentes que usen las dos implementaciones diferentes. Modicar Proxy para que dinmicamente cambie las implementaciones. 28. Modicar ArrayOperatorNew del Captulo 13 para mostrar que si deriva de Widget, la reserva de memoria todava funciona correctamente. Explicar porque la herencia en Framis.cpp no funcionaria correctamente. 29. Modicar Framis.cpp del Captulo 13 para que herede de Framis y crear nuevas versiones de new y delete para su clase derivada. Demostrar como todo ello funciona correctamente.
393
Captulo 15. Polimorsmo y Funciones virtuales de las otras caractersticas del lenguaje. Las caractersticas de un lenguaje procedural pueden ser entendidas en un nivel algortmico, pero las funciones virtuales deben ser entendidas desde el punto de vista del diseo.
15.2. Upcasting
En el Captulo 14 se vi como un objeto puede ser usado como un objeto de su propio tipo o como un objeto de su tipo base. Adems el objeto puede ser manejado a travs de su tipo base. Tomar la direccin de un objeto (o un puntero o una referencia) y tratarlo como la direccin de su tipo base se conoce como upcasting 1 debido al camino que se genera en los rboles de herencia que se suelen pintar con la clase base en la cima. Tambin se vi surgir un problema el cul est encarnado en el siguiente cdigo:
//: C15:Instrument2.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Inheritance & upcasting #include <iostream> using namespace std; enum note { middleC, Csharp, Eflat }; // Etc. class Instrument { public: void play(note) const { cout << "Instrument::play" << endl; } }; // Wind objects are Instruments // because they have the same interface: class Wind : public Instrument { public: // Redefine interface function: void play(note) const { cout << "Wind::play" << endl; } }; void tune(Instrument& i) { // ... i.play(middleC); } int main() { Wind flute; tune(flute); // Upcasting }
La funcin tune() acepta (por referencia) un Instrument, pero tambin acepta cualquier cosa que derive de Instrument. En el main(), se puede ver este comportamiento cuando se pasa un objeto Wind a afinar() sin que se necesite ningn molde. La interfaz de Instrument tiene que existir en Wind, porque Wind hereda sus propiedades de Instrument. Moldear en sentido ascendente (Upcasting) de Wind a Instrument puede "reducir" la interfaz, pero nunca puede ser menor que la interfaz de Instrument. Los mismos argumentos son ciertos cuando trabajamos con punteros; la nica diferencia es que el usuario debe indicar la direccin de los objtos de forma explcita cuando se pasen a una funcin.
1 N del T: Por desgracia upcasting es otro de los trminos a los que no he encontrado una traduccin convincente (amoldar hacia arriba??) y tiene el agravante que deriva de una expresin ampliamente usada por los programadores de C (Quin no ha hecho nunca un cast a void* ;-) ?. Se aceptan sugerencias.
396
15.3. El problema
15.3. El problema
El problema con Instrument2.cpp puede verse al ejecutar el programa. La salida es Instrument::play. Claramente, esta no es la salida deseada, porque el objeto es actualmente un Wind y no solo un Instrument. La llamada debera producir un Wind::play. Por este motivo, cualquier objeto de una clase que derive de la clase Instrument debera usar su propia versin de play(), de acuerdo a la situacin. El comportamiento de Instrument2.cpp no es sorprendente dada la aproximacin de C a las funciones. Para entender el resultado es necesario comprender el concepto de binding (ligadura).
397
Este archivo es idntico a Instrument2.cpp excepto por la adicin de la palabra reservada virtual y, sin embargo, el comportamiento es signicativamente diferente: Ahora la salida es Wind::play.
15.4.1. Extensibilidad
Con play() denido como virtual en la clase base, se pueden aadir tantos nuevos tipos como se quiera sin cambiar la funcin play(). En un programa orientado a objetos bien diseado, la mayora de las funciones seguirn el modelo de play() y se comunicarn nicamente a travs de la interfaz de la clase base. Las funciones que usen la interfaz de la clase base no necesitarn ser cambiadas para soportar a las nuevas clases. Aqu est el ejemplo de los instrumentos con ms funciones virtuales y un mayor nmero de nuevas clases, las cuales trabajan de manera correcta con la antigua (sin modicaciones) funcin play():
//: C15:Instrument4.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Extensibility in OOP #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } virtual char* what() const { return "Instrument"; } // Assume this will modify the object: virtual void adjust(int) {} }; class Wind : public Instrument {
398
399
Se puede ver que se ha aadido otro nivel de herencia debajo de Wind, pero el mecanismo virtual funciona correctamente sin importar cuantos niveles haya. La funcin adjust() no est redenida (override) por Brass y Woodwind. Cuando esto ocurre, se usa la denicin ms "cercana" en la jerarqua de herencia - el compilador garantiza que exista alguna denicin para una funcin virtual, por lo que nunca acabar en una llamada que no est enlazada con el cuerpo de una funcin (lo cual sera desatroso). El array A[] contiene punteros a la clase base Instrument, lo que implica que durante el proceso de inicializacin del array habr upcasting. Este array y la funcin f() sern usados en posteriores discusiones. En la llamada a tune(), el upcasting se realiza en cada tipo de objeto, haciendo que se obtenga siempre el comportamiento deseado. Se puede describir como "enviar un mensaje a un objeto y dejar al objeto que se preocupe sobre qu hacer con l". La funcin virtual es la lente a usar cuando se est analizando un proyecto: Dnde deben estar las clases base y cmo se desea extender el programa? Sin embargo, incluso si no se descubre la interfaz apropiada para la clase base y las funciones virtuales durante la creacin del programa, a menudo se descubrirn ms tarde, incluso mucho ms tarde cuando se desee ampliar o se vaya a hacer funciones de mantenimiento en el programa. Esto no implica un error de anlisis o de diseo; simplemente signica que no se conoca o no se poda conocer toda la informacin al principio. Debido a la fuerte modularizacin de C++, no es mucho problema que esto suceda porque los cambios que se hagan en una parte del sistema no tienden a propagarse a otras partes como sucede en C.
400
15.5. Cmo implementa C++ la ligadura dinmica tipo est oculta. Para verlo, aqu est un ejemplo que muestra el tamao de las clases que usan funciones virtuales comparadas con aquellas que no las usan:
//: C15:Sizes.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Object sizes with/without virtual functions #include <iostream> using namespace std; class NoVirtual { int a; public: void x() const {} int i() const { return 1; } }; class OneVirtual { int a; public: virtual void x() const {} int i() const { return 1; } }; class TwoVirtuals { int a; public: virtual void x() const {} virtual int i() const { return 1; } }; int main() { cout << "int: " << sizeof(int) << endl; cout << "NoVirtual: " << sizeof(NoVirtual) << endl; cout << "void* : " << sizeof(void*) << endl; cout << "OneVirtual: " << sizeof(OneVirtual) << endl; cout << "TwoVirtuals: " << sizeof(TwoVirtuals) << endl; }
Sin funciones virtuales el tamao del objeto es exactamente el que se espera: el tamao de un nico 3 int. Con una nica funcin virtual en OneVirtual, el tamao del objeto es el tamao de NoVirtual ms el tamao de un puntero a void. Lo que implica que el compilador aade un nico puntero (el VPTR) en la estructura si se tienen una o ms funciones virtuales. No hay diferencia de tamao entre OneVirtual y TwoVirtuals. Esto es porque el VPTR apunta a una tabla con direcciones de funciones. Se necesita slo una tabla porque todas las direcciones de las funciones virtuales estn contenidas en esta tabla. Este ejemplo requiere como mnimo un miembro de datos. Si no hubiera miembros de datos, el compilador de C++ hubiera forzado a los objetos a ser de tamao no nulo porque cada objeto debe tener direcciones distintas (se imagina cmo indexar un array de objetos de tamao nulo?). Por esto se inserta en el objeto un miembro "falso" ya que de otra forma tendr un tamao nulo. Cuando se inserta la informacin de tipo gracias a la palabra reservada virtual, sta ocupa el lugar del miembro "falso". Intente comentar el int a en todas las clases del ejemplo anterior para comprobarlo.
401
El array de punteros a Instruments no tiene informacin especca de tipo; cada uno de ellos apunta a un objeto de tipo Instrument. Wind, Percussion, Stringed, y Brass encajan en esta categora porque derivan de Instrument (esto hace que tengan la misma interfaz de Instrument, y puedan responder a los mismos mensajes), lo que implica que sus direcciones pueden ser metidas en el array. Sin embargo, el compilador no sabe que sean otra cosa que objetos de tipo Instrument, por lo que normalmente llamar a las versiones de las funciones que estn en la clase base. Pero en este caso, todas las funciones han sido declaradas con la palabra reservada virtual, por lo que ocurre algo diferente. Cada vez que se crea una clase que contiene funciones virtuales, o se deriva de una clase que contiene funciones virtuales, el compilador crea para cada clase una nica VTABLE, que se puede ver a la derecha en el diagrama. En sta tabla se colocan las direcciones de todas las funciones que son declaradas virtuales en la clase o en la clase base. Si no se sobreescribe una funcin que ha sido declarada como virtual, el compilador usa la direccin de la versin que se encuentra en la clase base (esto se puede ver en la entrada adjusta de la VTABLE de Brass). Adems, se coloca el VPTR (descubierto en Sizes.cpp) en la clase. Hay un nico VPTR por cada objeto cuando se usa herencia simple como es el caso. El VPTR debe estar inicializado para que apunte a la direccin inicial de la VTABLE apropiada (esto sucede en el constructor que se ver ms tarde con mayor detalle). Una vez que el VPTR ha sido inicializado a la VTABLE apropiada, el objeto "sabe" de que tipo es. Pero este autoconocimiento no tiene valor a menos que sea usado en el momento en que se llama a la funcin virtual. Cuando se llama a una funcin virtual a travs de la clase base (la situacin que se da cuando el compilador no tiene toda la informacin necesaria para realizar la ligadura esttica), ocurre algo especial. En vez de realizarse la tpica llamada a funcin, que en lenguaje ensamblador es simplemente un CALL a una direccin en concreto, el compilador genera cdigo diferente para ejecutar la llamada a la funcin. Aqu se muestra a lo que se parece una llamada a adjust() para un objeto Brass, si se hace a travs de un puntero a Instrument (una referencia a Instrument produce el mismo efecto):
El compilador empieza con el puntero a Instrument, que apunta a la direccin inicial del objeto. Todos los objetos Instrument o los objetos derivados de Instrument tienen su VPTR en el mismo lugar (a menudo al principio del objeto), de tal forma que el compilador puede conseguir el VPTR del objeto. El VPTR apunta a la la direccin inicial de VTABLE. Todas las direcciones de funciones de las VTABLE estn dispuestas en el mismo orden, a pesar del tipo especco del objeto. play() es el primero, what() es el segundo y adjust() es el tercero. El compilador sabe que a pesar del tipo especco del objeto, la funcin adjust() se encuentra localizada en VPTR+2. Debido a esto, en vez de decir, "Llama a la funcin en la direccin absoluta Instrument::adjust() (ligadura esttica y accin equivocada), se genera cdigo que dice "Llama a la funcin que se encuentre en VPTR+2". Como la bsqueda del VPTR y la determinacin de la direccin de la funcin actual ocurre en tiempo de ejecucin, se consigue la deseada ligadura dinmica. Se enva un mensaje al objeto, y el objeto se gura que debe hacer con l.
402
15.5. Cmo implementa C++ la ligadura dinmica Los argumentos de una llamada a una funcin C++, como los de a una funcin C, son colocados en la pila de derecha a izquierda (este orden es necesario para poder soportar las listas de argumentos variables de C), por lo que el argumento 1 se pone al principio en la pila. En este punto en la funcin, el registro si (que es parte de la arquitectura del procesador Intel X86) contiene la direccin de i. Tambin se introduce en la pila porque es la direccin de comienzo del objeto de inters. Hay que recordar que la direccin del comienzo del objeto corresponde al valor de this, y this es introducido en la pila de manera oculta antes de cualquier llamada a funcin, por lo que la funcin miembro sabe sobre qu objeto en concreto est trabajando. Debido a esto se ver siempre uno ms que el nmero de argumentos introducidos en la pila antes de una llamada a una funcin miembro (excepto para las funciones miembro static, que no tienen this). Ahora se puede ejecutar la llamada a la funcin virtual. Primero hay que producir el VPTR para poder encontrar la VTABLE. Para el compilador el VPTR se inserta al principio del objeto, por lo que el contenido de this corresponde al VPTR. La lnea
mov bx, word ptr [si]
busca la direccin (word) a la que apunta si, que es el VPTR y la coloca dentro del registro bx. El VPTR contenido en bx apunta a la direccin inicial de la VTABLE, pero el puntero de la funcin a llamar no se encuentra en la posicin cero de la VTABLE, si no en la segunda posicin (debido a que es la tercera funcin en la lista). Debido al modelo de memoria cada puntero a funcin ocupa dos bytes, por lo que el compilador suma cuatro al VPTR para calcular donde est la direccin de la funcin apropiada. Hay que hacer notar que este es un valor constante establecido en tiempo de compilacin, por lo que lo nico que ocurre es que el puntero a funcin que est en la posicin dos apunta a adjust(). Afortunadamente, el compilador se encarga de todo y se asegura de que todos los punteros a funciones en todas las VTABLEs de una jerarqua particular se creen con el mismo orden, a pesar del orden en que se hayan sobreescrito las funciones en las clases derivadas. Una vez se ha calculado en la VTABLE la direccin del puntero apropiado, se llama a la funcin a la que apunta el puntero. Esto es, se busca la direccin y se llama de una sola vez con la sentencia:
call word ptr [bx+4]
Finalmente, se retrocede el puntero de la pila para limpiar los argumentos que se pusieron antes de la llamada. En cdigo ensamblador de C y de C++ se ve a menudo la instruccin para limpiar la lista de argumentos pero puede variar dependiendo del procesador o de la implementacin del compilador.
//: C15:Early.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Early binding & virtual functions #include <iostream> #include <string> using namespace std; class Pet { public: virtual string speak() const { return ""; } }; class Dog : public Pet { public: string speak() const { return "Bark!"; } }; int main() { Dog ralph; Pet* p1 = &ralph; Pet& p2 = ralph; Pet p3; // Late binding for both: cout << "p1->speak() = " << p1->speak() <<endl; cout << "p2.speak() = " << p2.speak() << endl; // Early binding (probably): cout << "p3.speak() = " << p3.speak() << endl; }
En p1->speak() y en p2.speak(), se usan direcciones, lo que signica que la informacin es incompleta: p1 y p2 pueden representar la direccin de una Pet o algo que derivee de una Pet, por lo que el debe ser usado el mecanismo virtual. Cuando se llama a p3.speak no existe ambigedad. El compilador conoce el tipo exacto del objeto, lo que hace imposible que sea un objeto derivado de Pet - es exactamente una Pet. Por esto, probablemente se use la ligadura esttica. Sin embargo, si el compilador no quiere trabajar mucho, puede usar la ligadura dinmica y el comportamiento ser el mismo.
Smalltalk, Java y Python, por ejemplo, usan esta aproximacin con gran xito. En los laboratorios Bell, donde se invent C, hay un montn de programadores de C. Hacerlos ms ecientes, aunque sea slo un poco,
404
15.7. Clases base abstractas y funciones virtuales puras Si la respuesta fuera, "Todo es magnco excepto en las llamadas a funciones donde siempre tendr un pequea sobrecarga extra", mucha gente se hubiera aguantado con C antes que hacer el cambio a C++. Adems las funciones inline no seran posibles, porque las funciones virtuales deben tener una direccin para meter en la VTABLE. Por lo tanto las funciones virtuales son opcionales, y por defecto el lenguaje no es virtual, porque es la conguracin ms eciente. Stroustrup expuso que su lnea de trabajo era, "Si no lo usa, no lo pague". Adems la palabra reservada virtual permite anar el rendimiento. Cuando se disean las clases, sin embargo, no hay que preocuparse por anarlas. Si va a usar el polimorsmo, selo en todos los sitios. Slo es necesario buscar funciones que se puedan hacer no virtuales cuando se est buscando modos de acelerar el cdigo (y normalmente hay mucho ms que ganar en otras reas - una buena idea es intentar adivinar dnde se encuentran los cuellos de botella). Como ancdota la evidencia sugiere que el tamao y la velocidad de C++ sufren un impacto del 10 por ciento con respecto a C, y a menudo estn mucho ms cerca de ser parejos. Adems otra razn es que se puede disear un programa en C++ ms rpido y ms pequeo que como sera en C.
La nica razn para establecer una interfaz comn es que despus se pueda expresar de forma diferente en cada subtipo. Se crea una forma bsica que tiene lo que est en comn con todas las clases derivadas y nada ms. Por esto, Instrument es un candidato perfecto para ser una clase abstracta. Se crea una clase abstracta slo cuando se quiere manipular un conjunto de clases a travs de una interfaz comn, pero la interfaz comn no necesita tener una implementacin (o como mucho, no necesita una implementacin completa). Si se tiene un concepto como Instrument que funciona como clase abstracta, los objetos de esa clase casi nunca tendrn sentido. Es decir, Instrument sirve solamente para expresar la interfaz, y no una implementacin particular, por lo que crear un objeto que sea nicamente un Instrument no tendr sentido, y probablemente se quiera prevenir al usuario de hacerlo. Se puede solucionar haciendo que todas las funciones virtuales en Instrument muestren mensajes de error, pero retrasa la aparicin de los errores al tiempo de ejecucin lo que obligar a un testeo exhaustivo por parte del usuario. Es mucho ms productivo cazar el problema en tiempo de compilacin. Aqu est la sintaxis usada para una funcin virtual pura:
virtual void f() = 0;
Haciendo esto, se indica al compilador que reserve un hueco para una funcin en la VTABLE, pero que no ponga una direccin en ese hueco. Incluso aunque slo una funcin en una clase sea declarada como virtual pura, la VTABLE estar incompleta. Si la VTABLE de una clase est incompleta, qu se supone que debe hacer el compilador cuando alguien intente crear un objeto de esa clase? No sera seguro crear un objeto de esa clase abstracta, por lo que se obtendra un error de parte del compilador. Dicho de otra forma, el compilador garantiza la pureza de una clase abstracta. Hacer clases abstractas asegura que el programador cliente no puede hacer mal uso de ellas.
ahorra a la compaa muchos millones.
405
Captulo 15. Polimorsmo y Funciones virtuales Aqu tenemos Instrument4.cpp modicado para usar funciones virtuales puras. Debido a que la clase no tiene otra cosa que no sea funciones virtuales, se la llama clase abstracta pura:
//: C15:Instrument5.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Pure abstract base classes #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: // Pure virtual functions: virtual void play(note) const = 0; virtual char* what() const = 0; // Assume this will modify the object: virtual void adjust(int) = 0; }; // Rest of the file is the same ... class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind"; } void adjust(int) {} }; class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; } char* what() const { return "Percussion"; } void adjust(int) {} }; class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; } char* what() const { return "Stringed"; } void adjust(int) {} }; class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; } char* what() const { return "Brass"; } }; class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind"; }
406
Las funciones virtuales puras son tiles porque hacen explcita la abstraccin de una clase e indican al usuario y al compilador cmo deben ser usadas. Hay que hacer notar que las funciones virtuales puras previenen a una clase abstracta de ser pasadas a una funcin por valor, lo que es una manera de prevenir el object slicing (que ser descrito de forma reducida). Convertir una clase en abstracta tambin permite garantizar que se use siempre un puntero o una referencia cuando se haga upcasting a esa clase. Slo porque una funcin virtual pura impida a la VTABLE estar completa no implica que no se quiera crear cuerpos de funcin para alguna de las otras funciones. A menudo se querr llamar a la versin de la funcin que est en la clase base, incluso aunque sta sea virtual. Es una buena idea poner siempre el cdigo comn tan cerca como sea posible de la raiz de la jerarqua. No slo ahorra cdigo, si no que permite fcilmente la propagacin de cambios.
407
El hueco en la VTABLE de Pet todava est vaco, pero tiene funciones a las que se puede llamar desde la clase derivada. Otra ventaja de esta caracterstica es que perimite cambiar de una funcin virtual corriente a una virtual pura sin destrozar el cdigo existente (es una forma para localizar clases que no sobreescriban a esa funcin virtual).
408
class Dog : public Pet { string name; public: Dog(const string& petName) : Pet(petName) {} // New virtual function in the Dog class: virtual string sit() const { return Pet::name() + " sits"; } string speak() const { // Override return Pet::name() + " says Bark!"; } }; int main() { Pet* p[] = {new Pet("generic"),new Dog("bob")}; cout << "p[0]->speak() = " << p[0]->speak() << endl; cout << "p[1]->speak() = " << p[1]->speak() << endl; //! cout << "p[1]->sit() = " //! << p[1]->sit() << endl; // Illegal }
La clase Pet tiene dos funciones virtuales: speak() y name(). Dog aade una tercera funcin virtual llamada sit(), y sobreescribe el signicado de speak(). Un diagrama ayuda a visualizar qu est ocurriendo. Se muestran las VTABLEs creadas por el compilador para Pet y Dog:
Hay que hacer notar, que el compilador mapea la direccin de speak() en exactamente el mismo lugar tanto en la VTABLE de Dog como en la de Pet. De igual forma, si una clase Pug heredara de Dog, su versin de sit() ocupara su lugar en la VTABLE en la misma posicin que en Dog. Esto es debido a que el compilador genera un cdigo que usa un simple desplazamiento numrico en la VTABLE para seleccionar una funcin virtual, como se vio con el ejemplo en lenguaje ensamblador. Sin importar el subtipo en concreto del objeto, su VTABLE est colocada de la misma forma por lo que llamar a una funcin virtual se har siempre del mismo modo. En este caso, sin embargo, el compilador est trabajando slo con un puntero a un objeto de la clase base. La clase base tiene nicamente las funciones speak() y name(), por lo que son a las nicas funciones a las que el compilador permitir acceder. Cmo es posible saber que se est trabajando con un objeto Dog si slo hay un puntero a un objeto de la clase base? El puntero podra apuntar a algn otro tipo, que no tenga una funcin sit(). En este punto, puede o no tener otra direccin a funcin en la VTABLE, pero en cualquiera de los casos, hacer una llamada a una funcin virtual de esa VTABLE no es lo que se desea hacer. De modo que el compilador hace su trabajo impidiendo hacer llamadas virtuales a funciones que slo existen en las clases derivadas. Hay algunos poco comunes casos en los cuales se sabe que el puntero actualmente apunta al objeto de una subclase especca. Si se quiere hacer una llamada a una funcin que slo exista en esa subclase, entonces hay que hacer un molde (cast) del puntero. Se puede quitar el mensaje de error producido por el anterior programa con:
((Dog *) p[1])->sit()
Aqu, parece saberse que p[1] apunta a un objeto Dog, pero en general no se sabe. Si el problema consiste en averiguar el tipo exacto de todos los objetos, hay que volver a pensar porque posiblemente no se estn usando las funciones virtuales de forma apropiada. Sin embargo, hay algunas situaciones en las cuales el diseo funciona mejor (o no hay otra eleccin) si se conoce el tipo exacto de todos los objetos, por ejemplo aquellos incluidos en un contenedor genrico. Este es el problema de la run time type identication o RTTI (identicacin de tipos en tiempo de ejecucin). RTTI sirve para moldear un puntero de una clase base y "bajarlo" a un puntero de una clase derivada ("arriba" y "abajo", en ingls "up" y "down" respectivamente, se reeren al tpico diagrama de clases, con la clase base arriba). Hacer el molde hacia arriba (upcast) funciona de forma automtica, sin coacciones, debido a que es completamente seguro. Hacer el molde en sentido descendente (downcast) es inseguro porque no hay informacin en tiempo de 409
Captulo 15. Polimorsmo y Funciones virtuales compilacin sobre los tipos actuales, por lo que hay que saber exactamente el tipo al que pertenece. Si se hace un molde al tipo equivocado habr problemas. RTTI se describe posteriormente en este captulo, y el Volumen 2 de este libro tiene un captulo dedicado al tema.
410
La funcin describe() recibe un objeto de tipo Pet por valor. Despus llama a la funcin virtual description() del objeto Pet. En el main(), se puede esperar que la primera llamada produzca "Este es Alfred", y que la segunda produzca "A Fluffy le gusta dormir". De hecho, ambas usan la versin description() de la clase base. En este programa estn sucediendo dos cosas. Primero, debido a que describe() acepta un objeto Pet (en vez de un puntero o una referencia), cualquier llamada a describe() crear un objeto del tamao de Pet que ser puesto en la pila y posteriormente limpiado cuando acabe la llamada. Esto signica que si se pasa a describe()un objeto de una clase heredada de Pet, el compilador lo acepta, pero copia nicamente el fragmento del objeto que corresponda a una Pet. Se deshecha el fragmento derivado del objeto:
Ahora queda la cuestin de la llamada a la funcin virtual. Dog::description() hace uso de trozos de Pet (que todava existe) y de Dog, el cual no existe porque fue truncado!. Entonces, Qu ocurre cuando se llama a la funcin virtual? El desastre es evitado porque el objeto es pasado por valor. Debido a esto, el compilador conoce el tipo exacto del objeto porque el objeto derivado ha sido forzado a transformarse en un objeto de la clase base. Cuando se pasa por valor, se usa el constructor de copia del objeto Pet, que se encarga de inicializar el VPTR a la VTABLE de Pet y copia slo las partes del objeto que correspondan a Pet. En el ejemplo no hay un constructor de copia explcito por lo que el compilador genera uno. Quitando interpretaciones, el objeto se convierte realmente en una Pet durante el truncado. El Object Slicing quita parte del objeto existente y se copia en un nuevo objeto, en vez de simplemente cambiar el signicado de una direccin cuando se usa un puntero o una referencia. Debido a esto, el upcasting a un objeto no se usa a menudo; de hecho, normalmente, es algo a controlar y prevenir. Hay que resaltar que en este ejemplo, si description() fuera una funcin virtual pura en la clase base (lo cual es bastante razonable debido a que realmente no hace nada en la clase base), entonces el compilador impedir el object slicing debido a que no se puede "crear" un objeto de la clase base (que al n y al cabo es lo que sucede cuando se hace un upcast por valor). sto podra ser el valor ms importante de las funciones virtuales puras: prevenir el object slicing generando un error en tiempo de compilacin si alguien lo intenta hacer.
411
La primera cosa a resaltar es que en Derived3, el compilador no permitir cambiar el tipo de retorno de una funcin sobreescrita (lo permitira si f() no fuera virtual). sta es una restriccin importante porque el compilador debe garantizar que se pueda llamar de forma "polimrca" a la funcin a travs de la clase base, y si la clase base est esperando que f() devuelva un int, entonces la versin de f() de la clase derivada debe mantener ese compromiso o si no algo fallar. La regla que se enseo en el captulo 14 todava funciona: si se sobreescribe una de las funciones miembro sobrecargadas de la clase base, las otras versiones sobrecargadas estarn ocultas en la clase derivada. En el main() el cdigo de Derived4 muestra lo que ocurre incluso si la nueva versin de f() no est actualmente sobreescribiendo una funcin virtual existente de la interfaz - ambas versiones de f() en la clase base estan ocultas por f(int). Sin embargo, si se hace un upcast de d4 a Base, entonces nicamente las versiones de la clase base estarn disponibles (porque es el compromiso de la clase base) y la versin de la clase derivada no est disponible (debido a que no est especicada en la clase base). 412
413
La funcin miembro Pet::eats() devuelve un puntero a PetFood. En Bird, sta funcin miembro es sobreescrita exactamente como en la clase base, incluyendo el tipo de retorno. Esto es, Bird::eats() hace un >upcast de BirdFood a PetFood en el retorno de la funcin. Pero en Cat, el tipo devuelto por eats() es un puntero a CatFood, que es un tipo derivado de PetFood. El hecho de que el tipo de retorno est heredado del tipo de retorno la funcin de la clase base es la nica razn que hace que esto compile. De esta forma el acuerdo se cumple totalmente: eats() siempre devuelve un puntero a PetFood. Si se piensa de forma polimrca lo anterior no parece necesario. Por qu no simplemente se hacen upcast de todos los tipos retornados a PetFood* como lo hace Bird::eats()? Normalmente esa es una buena solucin, pero al nal del main() se puede ver la diferencia: Cat::eats() puede devolver el tipo exacto de PetFood, mientras que al valor retornado por Bird::eats() hay que hacerle un downcast al tipo exacto. Devolver el tipo exacto es un poco ms general y adems no pierde la informacin especca de tipo debida al upcast automtico. Sin embargo, devolver un tipo de la clase base generalmente resuelve el problema por lo que esto es una caracterstica bastante especca.
15.10. funciones virtuales y constructores de la clase base puede inicializar de forma adecuada a sus propios elementos. Por lo tanto es esencial que se llame a todos los constructores; de otra forma el objeto no estar construido de forma adecuada. Esto es por lo que el compilador obliga a hacer una llamada por cada trozo en una clase derivada. Se llamar al constructor por defecto si no se hace una llamada explcita a un constructor de la clase base. Si no existe constructor por defecto, el compilador lo crear. El orden de las llamadas al constructor es importante. Cuando se hereda, se sabe todo sobre la clase base y se puede acceder a todos los miembros pblicos y protegidos (public y protected) de la clase base. sto signica que se puede asumir que todos los miembros de la clase base son vlidos cuando se est en la clase derivada. En una funcin miembro normal, la construccin ya ha ocurrido, por lo que todos los miembros de todas las partes del objeto ya han sido construidos. Dentro del constructor, sin embargo, hay que asumir que todos los miembros que se usen han sido construidos. La nica manera de garantizarlo es llamando primero al constructor de la clase base. Entonces cuando se est en el constructor de la clase derivada, todos los miembros a los que se pueda acceder en la clase base han sido inicializados. "Saber que todos los miembros son vlidos" dentro del constructor es tambin la razn por la que, dentro de lo posible, se debe inicializar todos los objetos miembros (es decir, los objetos puestos en la clase mediante composicin). Si se sigue sta prctica, se puede asumir que todos los miembros de la clase base y los miembros objetos del objeto actual han sido inicializados.
Captulo 15. Polimorsmo y Funciones virtuales otro trabajo especial: desmontar un objeto, el cual puede pertenecer a una jerarqua de clases. Para hacerlo, el compilador genera cdigo que llama a todos los destructores, pero en el orden inverso al que son llamados en los constructores. Es decir, el constructor empieza en la clase ms derivada y termina en la clase base. sta es la opcin deseable y segura debido a que el destructor siempre sabe que los miembros de la clase base estn vivos y activos. Si se necesita llamar a una funcin miembro de la clase base dentro del destructor, ser seguro hacerlo. De esta forma, el destructor puede realizar su propio limpiado, y entonces llamar al siguiente destructor, el cual har su propio limpiado, etc. Cada destructor sabe de que clase deriva, pero no cuales derivan de l. Hay que tener en cuenta que los constructores y los destructores son los nicos lugares donde tiene que funcionar sta jerarqua de llamadas (que es automticamente generada por el compilador). En el resto de las funciones, slo esa funcin, sea o no virtual, ser llamada (y no las versiones de la clase base). La nica forma para acceder a las versiones de la clase base de una funcin consiste en llamar de forma explicita a esa funciones. Normalmente, la accin del destructor es adecuada. Pero qu ocurre si se quiere manipular un objeto a travs de un puntero a su clase base (es decir, manipular al objeto a travs de su interfaz genrica)? Este tipo de actividades es uno de los objetivos de la programacin orientada a objetos. El problema viene cuando se quiere hacer un delete (eliminar) de un puntero a un objeto que ha sido creado en el montn (>heap) con new. Si el puntero apunta a la clase base, el compilador slo puede conocer la versin del destructor que se encuentre en la clase base durante el delete. Suena familiar? Al n y al cabo, es el mismo problema por las que fueron creadas las funciones virtuales en el caso general. Afortunadamente, las funciones virtuales funcionan con los destructores como lo hacen para las otras funciones excepto los constructores.
//: C15:VirtualDestructors.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Behavior of virtual vs. non-virtual destructor #include <iostream> using namespace std; class Base1 { public: ~Base1() { cout << "~Base1()\n"; } }; class Derived1 : public Base1 { public: ~Derived1() { cout << "~Derived1()\n"; } }; class Base2 { public: virtual ~Base2() { cout << "~Base2()\n"; } }; class Derived2 : public Base2 { public: ~Derived2() { cout << "~Derived2()\n"; } }; int main() { Base1* bp = new Derived1; // Upcast delete bp; Base2* b2p = new Derived2; // Upcast delete b2p; }
Cuando se ejecuta el programa, se ve que delete bp slo llama al destructor de la clase base, mientras que delete b2p llama al destructor de la clase derivada seguido por el destructor de la clase base, que es el comportamiento que deseamos. Olvidar hacer virtual a un destructor es un error peligroso porque a menudo no afecta directamente al comportamiento del programa, pero puede introducir de forma oculta agujeros de memoria. 416
15.10. funciones virtuales y constructores Adems, el hecho de que alguna destruccin est teniendo lugar puede enmascarar el problema. Es posible que el destructor sea virtual porque el objeto sabe de que tipo es (lo que no ocurre durante la construccin del objeto). Una vez que el objeto ha sido construido, su VPTR es inicializado y se pueden usar las funciones virtuales.
Normalmente, una funcin virtual pura en una clase base causar que la clase derivada sea abstracta a menos que esa (y todas las dems funciones virtuales puras) tengan una denicin. Pero aqu, no parece ser el caso. Sin embargo, hay que recordar que el compilador crea automticamente una denicin del destructor en todas las clases si no se crea una de forma explcita. Esto es lo que sucede aqu - el destructor de la clase base es sobreescrito de forma oculta, y una denicin es puesta por el compilador por lo que Derived no es abstracta. Esto nos brinda una cuestin interesante: Cul es el sentido de un destructor virtual puro? Al contrario que con las funciones virtuales puras ordinarias en las que hay que dar el cuerpo de una funcin, en una clase derivada de otra con un destructor virtual puro, no se est obligado a implementar el cuerpo de la funcin porque el compilador genera automticamente el destructor. Entonces Cul es la diferencia entre un destructor virtual normal y un destructor virtual puro? La nica diferencia ocurre cuando se tiene una clase que slo tiene una funcin virtual pura: el destructor. En este caso, el nico efecto de la pureza del destructor es prevenir la instanciacin de la clase base, pero si no existen otros destructores en las clase heredadas, el destructor virtual se ejecutar. Por esto, mientras que el aadir un destructor virtual es esencial, el hecho de que sea puro o no lo sea no es tan importante. 417
Captulo 15. Polimorsmo y Funciones virtuales Cuando se ejecuta el siguiente ejemplo, se puede ver que se llama al cuerpo de la funcin virtual pura despus de la versin que est en la clase derivada, igual que con cualquier otro destructor.
//: C15:PureVirtualDestructors.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Pure virtual destructors // require a function body #include <iostream> using namespace std; class Pet { public: virtual ~Pet() = 0; }; Pet::~Pet() { cout << "~Pet()" << endl; } class Dog : public Pet { public: ~Dog() { cout << "~Dog()" << endl; } }; int main() { Pet* p = new Dog; // Upcast delete p; // Virtual destructor call }
Como gua, cada vez que se tenga una funcin virtual en una clase, se debera aadir inmediatamente un destructor virtual (aunque no haga nada). De esta forma se evitan posteriores sorpresas.
418
Durante la llamada al destructor virtual, no se llama a Derived::f(), incluso aunque f() es virtual. A qu es debido sto? Supongamos que fuera usado el mecanismo virtual dentro del destructor. Entonces sera posible para la llamada virtual resolver una funcin que est "lejana" (ms derivada) en la jerarqua de herencia que el destructor actual. Pero los destructores son llamados de "afuera a dentro" (desde el destructor ms derivado hacia el destructor de la clase base), por lo que la llamada actual a la funcin puede intentar acceder a fragmentos de un objeto que !ya ha sido destruido! En vez de eso, el compilador resuelve la llamada en tiempo de compilacin y llama slo a la versin local de la funcin. Hay que resaltar que lo mismo es tambin verdad para el constructor (como se explic anteriormente), pero en el caso del constructor el tipo de informacin no estaba disponible, mientras que en el destructor la informacin est ah (es decir, el VPTR) pero no es accesible.
419
Para simplicar las cosas se crea todo en el chero cabecera, la denicin (requerida) del destructor virtual puro es introducida en lnea el el chero cabecera, y pop() tambin est en lnea aunque podra ser considearado como demasiado largo para ser incluido as. Los objetos Link (lista) ahora manejan punteros a Object en vez de punteros a void, y la Stack (pila) slo aceptar y devolver punteros a Object. Ahora Stack es mucho ms exible, ya que puede manejar un montn de tipos diferentes pero adems es capaz de destruir cualquier objeto dejado en la pila. La nueva limitacin (que ser nalmente eliminada cuando las plantillas se apliquen al problema en el captulo 16) es que todo lo que se ponga en la pila debe ser heredado de Object. Esto est bien si se crea una clase desde la nada, pero qu pasa si se tiene una clase como string y se quiere ser capaz de meterla en la pila? En este caso, la nueva clase debe ser al mismo tiempo un string y un Object, lo que signica que debe heredar de ambas clases. Esto se conoce como herencia mltiple y es materia para un captulo entero en el Volumen 2 de este libro (se puede bajar de www.BruceEckel.com). cuando se lea este captulo, se ver que la herencia mltiple genera un montn de complejidad, y que es una caracterstica que hay que usar con cuentagotas. Sin embargo, sta situacin es lo sucintemente simple como para no tener problemas al usar herencia mltiple:
//: C15:OStackTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com
420
Aunque es similar a la versin anterior del programa de pruebas de Stack, se puede ver que slo se han sacado 10 elementos de la pila, lo que implica que probablemente quede algn elemento. Como la pila ahora maneja Objects, el destructor puede limpiarlos de forma adecuada, como se puede ver en la salida del programa gracias a que los objetos MyString muestran un mensaje cuando son destruidos. Crear contenedores que manejen Objects es una aproximacin razonable - si se tiene una jerarqua de raiz nica (debido al lenguaje o por algn requerimiento que obligue a que todas las clases hereden de Object). En este caso, est garantizado que todo es un Object y no es muy complicado usar contenedores. Sin embargo, en C++ no se puede esperar este comportamiento de todas las clases, por lo que se est abocado a usar herencia mltiple si se quiere usar esta aproximacin. Se ver en el captulo 16 que las plantillas solucionan este problema de una forma ms simple y elegante.
421
= 0; 0; 0; 0;
// 2nd dispatch
<< endl;
<< endl;
<< endl;
// 2nd dispatch
<< endl;
<< endl;
<< endl;
class Vector : public Math { public: Math& operator*(Math& rv) { return rv.multiply(this); // 2nd dispatch }
422
15.12. Downcasting
Math& multiply(Matrix*) { cout << "Matrix * Vector" << endl; return *this; } Math& multiply(Scalar*) { cout << "Scalar * Vector" << endl; return *this; } Math& multiply(Vector*) { cout << "Vector * Vector" << endl; return *this; } }; int main() { Matrix m; Vector v; Scalar s; Math* math[] = { &m, &v, &s }; for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) { Math& m1 = *math[i]; Math& m2 = *math[j]; m1 * m2; } }
Para simplicar slo se ha sobrecargado el operator*. El objetivo es ser capaz de multiplicar dos objetos Math cualquiera y producir el resultado deseado - hay que darse cuenta que multiplicar una matriz por un vector es una operacin totalmente distinta a la de multiplicar un vector por una matriz. El problema es que, en el main(), la expresin m1 * m2 contiene dos referencias Math, y son dos objetos de tipo desconocido. Una funcin virtual es slo capaz de hacer una nica llamada - es decir, determinar el tipo de un nico objeto. Para determinar ambos tipos en este ejemplo se usa una tcnica conocida como despachado mltiple (multiple dispatching), donde lo que parece ser una nica llamada a una funcin virtual se convierte en una segunda llamada a una funcin virtual. Cuando la segunda llamada se ha ejecutado, ya se han determinado ambos tipos de objetos y se puede ejecutar la actividad de forma correcta. En un principio no es transparante, pero despus de un rato mirando el cdigo empieza a cobrar sentido. Esta materia es tratada con ms profundidad en el captulo de los patrones de diseo en el Volumen 2 que se puede bajar de >www.BruceEckel.com.
15.12. Downcasting
Como se puede adivinar, desde el momento que existe algo conocido como upcasting - mover en sentido ascendente por una jerarqua de herencia - debe existir el downcasting para mover en sentido descendente en una jerarqua. Pero el upcasting es sencillo porque al movernos en sentido ascendente en la jerarqua de clases siempre convergemos en clases ms generales. Es decir, cuando se hace un upcast siempre se est en una clase claramente derivada de un ascendente (normalmente solo uno, excepto en el caso de herencia mltiple) pero cuando se hace downcast hay normalmente varias posibilidades a las que amoldarse. Mas concretamente, un Circulo es un tipo de Figura (que sera su upcast), pero si se intenta hacer un downcast de una Figura podra ser un Circulo, un Cuadrado, un Tringulo, etc. El problema es encontrar un modo seguro de hacer downcast (aunque es incluso ms importante preguntarse por qu se est usando downcasting en vez de usar el polimorsmo para que adivine automticamente el tipo correcto. En el Volumen 2 de este libro se trata como evitar el downcasting. C++ proporciona un moldeado explcito especial (introducido en el captulo 3) llamado "moldeado dinmico" (dynamic_cast) que es una operacin segura. Cuando se usa moldeado dinmico para intentar hacer un molde a un tipo en concreto, el valor de retorno ser un puntero al tipo deseado slo si el molde es adecuado y tiene xito, de otra forma devuelve cero para indicar que no es del tipo correcto. Aqu tenemos un ejemplo mnimo:
//: C15:DynamicCast.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000
423
Cuando se use moldeado dinmico, hay que trabajar con una jerarqua polimrca real - con funciones virtuales - debido a que el modeado dinmico usa informacin almacenada en la VTABLE para determinar el tipo actual. Aqu, la clase base contiene un destructor virtual y esto es suciente. En el main(), un puntero a Cat es elevado a Pet, y despus se hace un downcast tanto a puntero Dog como a puntero a Cat. Ambos punteros son imprimidos, y se puede observar que cuando se ejecuta el programa el downcast incorrecto produce el valor cero. Por supuesto somos los responsables de comprobar que el resultado del cast no es cero cada vez que se haga un downcast. Adems no hay que asumir que el puntero ser exactamente el mismo, porque a veces se realizan ajustes de punteros durante el upcasting y el downcasting (en particular, con la herencia mltiple). Un moldeado dinmico requiere un poco de sobrecarga extra en ejecucin; no mucha, pero si se est haciendo mucho moldeado dinmico (en cuyo caso debera ser cuestionado seriamente el diseo del programa) se convierte en un lastre en el rendimiento. En algunos casos se puede tener alguna informacin especial durante el downcasting que permita conocer el tipo que se est manejando, con lo que la sobrecarga extra del modeado dinmico se vuelve innecesario, y se puede usar de manera alternativa un moldeado esttico. Aqu se muestra como funciona:
//: C15:StaticHierarchyNavigation.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Navigating class hierarchies with static_cast #include <iostream> #include <typeinfo> using namespace std; class class class class Shape { public: virtual ~Shape() {}; }; Circle : public Shape {}; Square : public Shape {}; Other {};
int main() { Circle c; Shape* s = &c; // Upcast: normal and OK // More explicit but unnecessary: s = static_cast<Shape*>(&c); // (Since upcasting is such a safe and common // operation, the cast becomes cluttering) Circle* cp = 0; Square* sp = 0; // Static Navigation of class hierarchies // requires extra type information: if(typeid(s) == typeid(cp)) // C++ RTTI cp = static_cast<Circle*>(s); if(typeid(s) == typeid(sp))
424
15.13. Resumen
sp = static_cast<Square*>(s); if(cp != 0) cout << "Its a circle!" << endl; if(sp != 0) cout << "Its a square!" << endl; // Static navigation is ONLY an efficiency hack; // dynamic_cast is always safer. However: // Other* op = static_cast<Other*>(s); // Conveniently gives an error message, while Other* op2 = (Other*)s; // does not }
En este programa, se usa una nueva caracterstica que no ser completamente descrita hasta el Volumen 2 de este libro, donde hay un captulo que cubre este tema: Informacin de tipo en tiempo de ejecucin en C++ o mecanismo RTTI (run time type information). RTTI permite descubrir informacin de tipo que ha sido perdida en el upcasting. El moldeado dinmico es actualmente una forma de RTTI. Aqu se usa la palabra reservada typeid (declarada en el chero cabecera typeinfo) para detectar el tipo de los punteros. Se puede ver que el tipo del puntero a Figura es comparado de forma sucesiva con un puntero a Circulo y con un Cuadrado para ver si existe alguna coincidencia. Hay ms RTTI que el typeid, y se puede imaginar que es fcilmente implementable un sistema de informacin de tipos usando una funcin virtual. Se crea un objeto Circulo y la direccin es elevada a un puntero a Figura; la segunda versin de la expresin muestra como se puede usar modeado esttico para ser ms explcito con el upcast. Sin embargo, desde el momento que un upcast siempre es seguro y es una cosa que se hace comunmente, considero que un cast explcito para hacer upcast ensucia el cdigo y es innecesario. Para determinar el tipo se usa RTTI, y se usa modelado esttico para realizar el downcast. Pero hay que resaltar que, efectivamente, en este diseo el proceso es el mismo que usar el moldeado dinmico, y el programador cliente debe hacer algn test para descubrir si el cast tuvo xito. Normalmente se preere una situacin ms determinista que la del ejemplo anterior para usar el modeado esttico antes que el moldeado dinmico (y hay que examinar detenidamente el diseo antes de usar moldeado dinmico). Si una jerarqua de clases no tiene funciones virtuales (que es un diseo cuestionable) o si hay otra informacin que permite hacer un downcast seguro, es un poco ms rpido hacer el downcast de forma esttica que con el moldeado dinmico. Adems, modeado esttico no permitir realizar un cast fuera de la jerarqua, como un cast tradicional permitira, por lo que es ms seguro. Sin enbargo, navegar de forma esttica por la jerarqua de clases es siempre arriesgado por lo que hay que usar moldeado dinmico a menos que sea una situacin especial.
15.13. Resumen
Polimorsmo - implementado en C++ con las funciones virtuales - signica "formas diferentes". En la programacin orientada a objetos, se tiene la misma vista (la interfaz comn en la clase base) y diferentes formas de usarla: las diferentes versiones de las funciones virtuales. Se ha visto en este captulo que es imposible entender, ni siquiera crear, un ejemplo de polimorsmo sin usar la abstraccin de datos y la herencia. El polimorsmo es una caracterstica que no puede ser vista de forma aislada (como por ejemplo las sentencias const y switch), pero sin embargo funciona nicamente de forma conjunta, como una parte de un "gran cuadro" de relaciones entre clases. La gente se vuelve a menudo confusa con otras caractersticas no orientadas a objetos de C++ como es la sobrecarga y los argumentos por defecto, los cuales son presentados a veces como orientado a objetos. No nos liemos; si no hay ligadura dinmica, no hay polimorsmo. Para usar el polimorsmo - y por lo tanto, tcnicas orientadas a objetos - en los programas hay que ampliar la visin de la programacin para incluir no solo miembros y mensajes entre clases individuales, si no tambin sus puntos en comn y las relaciones entre ellas. Aunque requiere un esfuerzo signicativo, es recompensado gracias a que se consigue mayor velocidad en el desarrollo, mejor organizacin de cdigo, programas extensibles, y mayor mantenibilidad. El polimorsmo completa las caractersticas de orientacin a objetos del lenguaje, pero hay dos caractersticas fundamentales ms en C++: plantillas (introducidas en el captulo 16 y cubiertas en mayor detalle en el segundo volumen de este libro), y manejo de excepciones (cubierto en el Volumen 2). Estas caractersticas nos proporcionan 425
Captulo 15. Polimorsmo y Funciones virtuales un incremento de poder de cada una de las caractersticas de la orientacin a objetos: tipado abstracto de datos, herencia, y polimorsmo.
15.14. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Cree una jerarqua simple "gura": una clase base llamada Figura y una clases derivadas llamadas Circulo, Cuadrado, y Triangulo. En la clase base, hay que hacer una funcin virtual llamada dibujar(), y sobreescribirla en las clases derivadas. Hacer un array de punteros a objetos Figura que se creen en el montn (heap) y que obligue a realizar upcasting de los punteros, y llamar a dibujar() a travs de la clase base para vericar el comportamiento de las funciones virtuales. Si el depurador lo soporta, intente ver el programa paso a paso. 2. Modique el Ejercicio 1 de tal forma que dibujar() sea una funcin virtual pura. Intente crear un objeto de tipo Figura. Intente llamar a la funcin virtual pura dentro del constructor y mire lo que ocurre. Dejndolo como una funcin virtual pura cree una denicin para dibujar(). 3. Aumentando el Ejercicio 2, cree una funcin que use un objeto Figura por valor e intente hacer un upcast de un objeto derivado como argumento. Vea lo que ocurre. Arregle la funcin usando una referencia a un objeto Figura. 4. Modique C14:Combined.cpp para que f() sea virtual en la clase base. Cambie el main() para que se haga un upcast y una llamada virtual. 5. Modique Instrument3.cpp aadiendo una funcin virtual preparar(). Llame a preparar() dentro de tune(). 6. Cree una jerarqua de herencia de Roedores: Raton, Gerbo, Hamster, etc. En la clase base, proporcione los mtodos que son comunes a todos los roedores, y redena aquellos en las clases derivadas para que tengan diferentes comportamientos dependiendo del tipo especco de roedor. Cree un array de punteros a Roedor, rellenelo con distintos tipos de roedores y llame a los mtodos de la clase base para ver lo que ocurre. 7. Modique el Ejercicio 6 para que use un vector<Roedor*> en vez de un array de punteros. Asegurese que se hace un limpiado correcto de la memoria. 8. Empezando con la jerarqua anterior de Roedor, herede un HamsterAzul de Hamster (si, existe algo as, tuve uno cuando era nio), sobreescriba los mtodos de la clase base y muestre que el cdigo que llama a los mtodos de clase base no necesitan cambiar para adecuarse el nuevo tipo. 9. A partir de la jerarqua Roedor anterior, aadaun destructor no virtual, cree un objeto de la Hamster usando new, haga un upcast del puntero a Roedor*, y borre el puntero con delete para ver si no se llama a los destructores en la jerarqua. Cambie el destructor a virtual y demuestre que el comportamiento es ahora correcto. 10. Modique Roedor para convertirlo en una clase base pura abstracta. 11. Cree un sistema de control areo con la clase base Avion y varios tipos derivados. Cree una clase Torre con un vector<Avion*> que envie los mensajes adecuados a los distintos aviones que estn bajo su control. 12. Cree un modelo de invernadero heredando varios tipos de Plantas y construyendo mecanismos en el invernadero que se ocupen de las plantas. 13. En Early.cpp, haga a Pet una clase base abstracta pura. 14. En AddingVirtuals.cpp, haga a todas las funciones miembro de Pet virtuales puras, pero proporcione una denicin para name(). Arregle Dog como sea necesario, usando la denicin de name() que se encuentra en la clase base. 426
15.14. Ejercicios 15. Escriba un pequeo programa para mostrar la diferencia entre llamar a una funcin virtual dentro de una funcin miembro normal y llamar a una funcin virtual dentro de un constructor. El programa de probar que las dos llamadas producen diferentes resultados. 16. Modique VirtualsInDestructors.cpp por heredando una clase de Derived y sobreescribiendo f() y el destructor. En main(), cree y haga un upcast de un objeto de su nuevo tipo, despus borrelo. 17. Use el Ejercicio 16 y aada llamadas a f() en cada destructor. Explique que ocurre. 18. Cree un clase que tenga un dato miembro y una clase derivada que aada otro dato miembro. Escriba una funcin no miembro que use un objeto de la clase base por valor e imprima el tamao del objeto usando sizeof. En el main() cree un objeto de la clase derivada, imprima su tamao, y llame a su funcin. Explique lo que ocurre. 19. Cree un ejemplo sencillo de una llamada a una funcin virtual y genere su salida en ensamblador. Localize el cdigo en ensamblador para la llamada a la funcin virtual y explique el cdigo. 20. Escriba una clase con una funcin virtual y una funcin no virtual. Herede una nueva clase, haga un objeto de esa clase, y un upcast a un puntero del tipo de la clase base. Use la funcin clock() que se encuentra en <ctime> (necesitar echar un vistazo a su librer C) para medir la diferencia entre una llamada virtual y una llamada no virtual. Ser necesario realizar multiples llamadas a cada funcin para poder ver la diferencia. 21. Modique C14:Order.cpp aadiendo una funcin virtual en la clase base de la macro CLASS (que pinte algo) y haciendo el destructor virtual. Cree objetos de las distintas subclases y hagales un upcast a la clase base. Verique que el comportamiento virtual funciona y que se realiza de forma correcta la construccin y la destruccin del objeto. 22. Escriba una clase con tres funciones virtuales sobrecargadas. Herede una nueva clase y sobreescriba una de las funciones. Cree un objeto de la clase derivada. Se puede llamar a todas las funciones de la clase base a travs del objeto derivado? Haga un upcast de la direccin del objeto a la base. Se pueden llamar a las tres funciones a travs de la base? Elimine la denicin sobreescrita en la clase derivada. Ahora Se puede llamar a todas las funciones de la clase base a travs del objeto derivado?. 23. Modique VariantReturn.cpp para que muestre que su comportamiento funciona con referencias igual que con punteros. 24. En Early.cpp, Cmo se le puede indicar al compilador que haga la llamada usando ligadura esttica o ligadura dinmica? Determine el caso para su propio compilador. 25. Cree una clase base que contenga una funcin clone() que devuelva un puntero a una copia del objeto actual. Derive dos subclases que sobreescriban clone() para devolver copias de sus tipos especcos. En el main(), cree y haga upcast de sus dos tipos derivados, y llame a clone() para cada uno y verique que las copias clonadas son de los subtipos correctos. Experimente con su funcin clone() para que se pueda ir al tipo base, y despus intente regresar al tipo exacto derivado. Se le ocurre alguna situacin en la que sea necesario esta aproximacin? 26. Modique OStackTest.cpp creando su propia clase, despus haga multiple herencia con Object para crear algo que pueda ser introducido en la pila. Pruebe su clase en el main(). 27. Aada un tipo llamado Tensor a OperartorPolymorphism.cpp. 28. (Intermedio) Cree una clase base X sin datos miembro y sin constructor, pero con una funcin virtual. Cree una Y que herede de X, pero sin un constructor explcito. Genere cdigo ensamblador y examinelo para deteriminar si se crea y se llama un constructor de X y, si eso ocurre, qu cdigo lo hace. Explique lo que haya descubierto. X no tiene constructor por defecto, entonces por qu no se queja el compilador? 29. (Intermedio) Modique el Ejercicio 28 escribiendo constructores para ambas clases de tal forma que cada constructor llame a una funcin virtual. Genere el cdigo ensamblador. Determine donde se encuentra asignado el VPTR dentro del constructor. El compilador est usando el mecanismo virtual dentro del constructor? Explique por qu se sigue usando la version local de la funcin. 30. (Avanzado) Si una funcin llama a un objeto pasado por valor si ligadura esttica, una llamada virtual accede a partes que no existen. Es posible? Escriba un cdigo para forzar una llamada virtual y vea si se produce un cuelgue de la aplicacin. Para explicar el comportamiento, observe que ocurre si se pasa un objeto por valor. 427
Captulo 15. Polimorsmo y Funciones virtuales 31. (Avanzado) Encuentre exactamente cuanto tiempo ms es necesario para una llamada a una funcin virtual buscando en la informacin del lenguaje ensamblador de su procesador o cualquier otro manual tcnico y encontrando los pulsos de reloj necesarios para una simple llamada frente al nmero necesario de las instrucciones de las funciones virtuales. 32. Determine el tamao del VPTR (usando sizeof) en su implementacin. Ahora herede de dos clases (herencia mltiple) que contengan funciones virtuales. Se tiene una o dos VPTR en la clase derivada? 33. Cree una clase con datos miembros y funciones virtuales. Escriba una funcin que mire en la memoria de un objeto de su clase y que imprima sus distintos fragmentos. Para hacer esto ser necesario experimentar y de forma iterativa descubrir donde se encuentra alojado el VPTR del objeto. 34. Imagine que las funciones virtuales no existen, y modique Instrument4.cpp para que use moldeado dinmico para hacer el equivalente de las llamadas virtuales. Esplique porque es una mala idea. 35. Modique StaicHierarchyNavigation.cpp para que en vez de usar el RTTI de C++ use su propio RTTI via una funcin virtual en la clase base llamada whatAmI() y un enum type { Circulos, Cuadrados };. 36. Comience con PointerToMemberOperator.cpp del captulo 12 y demuestre que el polimorsmo todava funciona con punteros a miembros, incluso si operator->* est sobrecargado.
428
16.1. Contenedores
Supngase que se quiere crear una pila, como se ha estado haciendo a travs de este libro. Para hacerlo sencillo, esta clase manejar enteros.
//: C16:IntStack.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Simple integer stack //{L} fibonacci #include "fibonacci.h" #include "../require.h" #include <iostream> using namespace std; class IntStack { enum { ssize = 100 }; int stack[ssize]; int top; public: IntStack() : top(0) {} void push(int i) { require(top < ssize, "Too many push()es"); stack[top++] = i; } int pop() { require(top > 0, "Too many pop()s"); return stack[--top]; } };
429
La clase IntStack es un ejemplo trivial de una pila. Para mantener la simplicidad ha sido creada con un tamao jo, pero se podra modicar para que automticamente se expanda usando la memoria del montn, como en la clase Stack que ha sido examinada a travs del libro. main() aade algunos enteros a la pila, y posteriormente los extrae. Para hacer el ejemplo ms interesante, los enteros son creados con la funcin fibonacci(), que genera los tradicionales nmeros de la reproduccin del conejo. Aqu est el archivo de cabecera que declara la funcin:
//: C16:fibonacci.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Fibonacci number generator int fibonacci(int n);
Esta es una implementacin bastante eciente, porque nunca se generan los nmeros ms de una vez. Se usa un array static de int, y se basa en el hecho de que el compilador inicializar el array esttico a cero. El primer bucle for mueve el ndice i a la primera posicin del array que sea cero, entonces un bucle while aade nmeros Fibonacci al array hasta que se alcance el elemento deseado. Hay que hacer notar que si los nmeros Fibonacci hasta el elemento n ya estn inicializados, entonces tambin se salta el bucle while. 430
Con la excepcin, en Java, de los tipos de datos primitivos, que se hicieron no Objects por eciencia.
431
Captulo 16. Introduccin a las Plantillas Debido a que la librera de clases de Smalltalk tena mucha ms experiencia e historia detrs de la que tena C++, y porque los compiladores de C++ originales no tenan libreras de clases contenedoras, pareca una buena idea duplicar la librera de Smalltalk en C++. Esto se hizo como experimento con una de las primeras implementacines de C++2 , y como representaba un signicativo ahorro de cdigo mucha gente empezo a usarlo. En el proceso de intentar usar las clases contenedoras, descubrieron un problema. El problema es que en Smalltalk (y en la mayora de los lenguajes de POO que yo conozco), todas las clases derivan automticamente de la jerarqua nica, pero esto no es cierto en C++. Se puede tener una magnica jerarqua basada en objetos con sus clases contenedoras, pero entonces se compra un conjunto de clases de guras, o de aviones de otro vendedor que no usa esa jerarqua. (Esto se debe a que usar una jerarqua supone sobrecarga, rechazada por los programadores de C). Cmo se inserta un rbol de clases independientes en nuestra jerarqua? El problema se parece a lo siguiente:
Debido a que C++ suporta mltiples jerarquas independientes, la jerarqua basada en objetos de Smalltalk no funciona tan bien. La solucin parace obvia. Si se pueden tener mltiples jerarquas de herencia, entonces hay que ser capaces de heredar de ms de una clase: La herencia mltiple resuelve el problema. Por lo que se puede hacer lo siguiente (un ejemplo similar se di al nal del Captulo 15).
Ahora OShape tiene las caractersticas y el comportamiento de Shape, pero como tambin est derivado de Object puede ser insertado en el contenedor. La herencia extra dada a OCircle, OSquare, etc. es necesaria para que esas clases puedan hacer upcast hacia OShape y puedan mantener el comportamiento correcto. Se puede ver como las cosas se estn volviendo confusas rpidamente. Los vendedores de compiladores inventaron e incluyeron sus propias jerarquas y clases contenedoras, muchas de las cuales han sido reemplazadas desde entonces por versiones de templates. Se puede argumentar que la herencia mltiple es necesaria para resolver problemas de programacin general, pero como se ver en el Volumen 2 de este libro es mejor evitar esta complejidad excepto en casos especiales.
El compilador hace el trabajo por nosotros, y se obtiene el contenedor necesario para hacer el trabajo, en vez de una jerarqua de herencia inmanejable. En C++, el template implementa el concepto de tipo parametrizado. Otro benecio de la aproximacin de las plantillas es que el programador novato que no tenga familiaridad o est incmodo con la herencia puede usar las clases contenedoras de manera adecuada (como se ha estado haciendo a lo largo del libro con el vector).
2 3 4
La librera OOPS, por Keith Gorlen, mientras estaba en el NIH. The C++ Programming Language by Bjarne Stroustrup (1 edicin, Addison-Wesley, 1986) La inspiracin de los templates parece venir de los generics de ADA
432
Se puede ver que parece una clase normal excepto por la lnea.
template<class T>
que indica que T es un parmetro de sustitucin, y que representa un nombre de un tipo. Adems, se puede ver que T es usado en todas las partes de la clase donde normalmente se vera al tipo especco que el contenedor gestiona. En Array los elementos son insertados y extraidos con la misma funcin: el operador sobrecargado operator[]. Devuelve una referencia, por lo que puede ser usado en ambos lados del signo igual (es decir, tanto como lvalue como rvalue). Hay que hacer notar que si el ndice se sale de los lmites se usa la funcin require() para mostrar un mensaje. Como operator[] es inline, se puede usar esta aproximacin para garantizar que no se producen violaciones del lmite del array para entonces eliminar el require(). En el main(), se puede ver lo fcil que es crear Arrays que manejen distintos tipos de objetos. Cuando se dice:
Array<int> ia; Array<float> fa;
433
Captulo 16. Introduccin a las Plantillas el compilador expande dos veces la plantilla del Array (que se conoce como instantiation o crear una instancia), para crear dos nuevas clases generadas, las cuales pueden ser interpretadas como Array_int y Array_float. Diferentes compiladores pueden crear los nombres de diferentes maneras. Estas clases son idnticas a las que hubieran producido de estar hechas a mano, excepto que el compilador las crea por nosotros cuando se denen los objetos ia y fa. Tambin hay que notar que las deniciones de clases duplicadas son eludidas por el compilador.
Cualquier referencia al nombre de una plantilla de clase debe estar acompaado por la lista de argumentos del template, como en Array<T>operator[]. Se puede imaginar que internamente, el nombre de la clase se rellena con los argumentos de la lista de argumentos de la plantilla para producir un nombre identicador nico de la clase for cada instanciacin de la plantilla.
Archivos cabecera
Incluso si se crean deniciones de funciones no inline, normalmente se querr poner todas las declaraciones y deniciones de un template en un archivo cabecera. Esto parece violar la regla usual de los archivos cabecera de No poner nada que asigne almacenamiento, (lo cual previene mltiples errores de denicin en tiempo de enlace), pero las deniciones de plantillas son especial. Algo precedido por template<...> signica que el compilador no asignar almacenamiento en ese momento, sino que se esperar hasta que se lo indiquen (en la instanciacin de una plantilla), y que en algn lugar del compilador y del enlazador hay un mecanismo para eliminar las mltiples deniciones de una plantilla idntica. Por lo tanto casi siempre se pondr toda la declaracin y denicin de la plantilla en el archivo cabecera por facilidad de uso. Hay veces en las que puede ser necesario poner las deniciones de la plantilla en un archivo cpp separado para satisfacer necesidades especiales (por ejemplo, forzar las instanciaciones de las plantillas para que se encuentren en un nico archivo dll de Windows). La mayora de los compiladores tienen algn mecanismo para permitir 434
16.3. Sintaxis del Template esto; hay que investigar la documentacin del compilador concreto para usarlo. Algunas personas sienten que poner el cdigo fuente de la implementacin en un archivo cabecera hace posible que se pueda robar y modicar el cdigo si se compra la librera. Esto puede ser una caracterstica, pero probablemente dependa del modo de mirar el problema: Se est comprando un producto o un servicio? Si es un producto, entonces hay que hacer todo lo posible por protegerlo, y probablemente no se quiera dar el cdigo fuente, sino slo el cdigo compilado. Pero mucha gente ve el software como un servicio, incluso ms, como un servicio por suscripcin. El cliente quiere nuestra pericia, quieren que se mantenga ese fragmento de cdigo reutilizable para no tenerlo que hacer l - para que se pueda enfocar en hacer su propio trabajo. Personalmente creo que la mayora de los clientes le tratarn como una fuente de recursos a tener en cuenta y no querrn poner en peligro su relacin con usted. Y para los pocos que quieran robar en vez de comprar o hacer el trabajo original, de todas formas probablemante tampoco se mantendran con usted.
Hay que darse cuenta que esta plantilla asume ciertas caractersticas de los objetos que est manejando. Por ejemplo, StackTemplate asume que hay alguna clase de operacin de asignacin a T dentro de la funcin push(). Se puede decir que una plantilla implica una interfaz para los tipos que es capaz de manejar. Otra forma de decir esto es que las plantillas proporcionan una clase de mecanismo de tipado dbil en C++, lo cual es tpico en un lenguaje fuertemente tipado. En vez de insistir en que un objeto sea del mismo tipo para que sea aceptable, el tipado dbil requiere nicamente que la funcin miembro a la que se quiere llamar est disponible para un objeto en particular. Es decir, el cdigo dbilmente tipado puede ser aplicado a cualquier objeto que acepte esas llamadas a funciones miembro, lo que lo hace mucho ms exible5 . Aqu tenemos el objeto revisado para comprobar la plantilla:
5 Todos los mtodos en Smalltalk y Python estn dbilmente tipados, y ese es el motivo por lo que estos lenguajes no necesitan el mecanismo de los templates. En efecto, se consiguen plantillas sin templates.
435
//: C16:StackTemplateTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Test simple stack template //{L} fibonacci #include "fibonacci.h" #include "StackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(fibonacci(i)); for(int k = 0; k < 20; k++) cout << is.pop() << endl; ifstream in("StackTemplateTest.cpp"); assure(in, "StackTemplateTest.cpp"); string line; StackTemplate<string> strings; while(getline(in, line)) strings.push(line); while(strings.size() > 0) cout << strings.pop() << endl; }
La nica diferencia est en la creacin de is. Dentro de la lista de argumentos del template hay que especicar el tipo de objeto que la pila y el iterador debern manejar. Para demostrar la genericidad de la plantilla, se crea un StackTemplate para manejar string. El ejemplo lee las lneas del archivo con el cdigo fuente.
436
{} n) {
f; } Number& x) {
template<class T, int size = 20> class Holder { Array<T, size>* np; public: Holder() : np(0) {} T& operator[](int i) { require(0 <= i && i < size); if(!np) np = new Array<T, size>; return np->operator[](i); } int length() const { return size; } ~Holder() { delete np; } }; int main() { Holder<Number> for(int i = 0; h[i] = i; for(int j = 0; cout << h[j] }
Como antes, Array es un array de objetos que previene de rebasar los lmites. La clase Holder es muy parecida a Array excepto que tiene un puntero a Array en vez de un tener incrustrado un objeto del tipo Array. Este puntero no se inicializa en el constructor; la inicializacin es retrasada hasta el primer acceso. Esto se conoce como inicializacin perezosa; se puede usar una tcnica como esta si se estn creando un montn de objetos, pero no se est accediendo a todos ellos y se quiere ahorrar almacenamiento. Hay que resaltar que nunca se almacena internamente el valor de size en la clase, pero se usa como si fuera un dato interno dentro de las funciones miembro.
Captulo 16. Introduccin a las Plantillas conocer el tipo de ese objeto. Con los templates, sin embargo, podemos escribir cdigo que no conozcan el tipo de objeto, y fcilmente instanciar una nueva versin del contenedor por cada tipo que queramos que contenga. La instancia contenedora individual conoce el tipo de objetos que maneja y puede por tanto llamar al destructor correcto (asumiendo que se haya proporcionado un destructor virtual). Para la pila es bastante sencillo debido a todas las funciones miembro pueden ser introducidas en lnea:
//: C16:TStack.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // The Stack as a template #ifndef TSTACK_H #define TSTACK_H template<class T> class Stack { struct Link { T* data; Link* next; Link(T* dat, Link* nxt): data(dat), next(nxt) {} }* head; public: Stack() : head(0) {} ~Stack(){ while(head) delete pop(); } void push(T* dat) { head = new Link(dat, head); } T* peek() const { return head ? head->data : 0; } T* pop(){ if(head == 0) return 0; T* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } }; #endif // TSTACK_H
Si se compara esto al ejemplo de OStack.h al nal del captulo 15, se ver que Stack es virtualmente idntica, excepto que Object ha sido reemplazado con T . El programa de prueba tambin es casi idntico, excepto por la necesidad de mltiple herencia de string y Object (incluso por la necesidad de Object en s mismo) que ha sido eliminada. Ahora no tenemos una clase MyString para anunciar su destruccin por lo que aadimos una pequea clase nueva para mostrar como la clase contenedora Stack limpia sus objetos:
//: C16:TStackTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{T} TStackTest.cpp #include "TStack.h" #include "../require.h" #include <fstream>
438
El destructor de X es virtual, no porque se sea necesario aqu, sino porque xx podra ser usado ms tarde para manejar objetos derivados de X. Note lo fcil que es crear diferentes clases de Stacks para string y para X. Debido a la plantilla, se consigue lo mejor de los dos mundos: la facilidad de uso de la Stack junto con un limpiado correcto.
439
440
16.4. Stack y Stash como Plantillas El tamao del incremento por defecto es muy pequeo para garantizar que se produzca la llamada a inflate(). Esto nos asegura que funcione correctamente. Para comprobar el control de propiedad de PStack en template, la siguiente clase muestra informes de creacin y destruccin de elementos, y tambin garantiza que todos los objetos que hayan sido creados sean destruidos. AutoCounter permitir crear objetos en la pila slo a los objetos de su tipo:
//: C16:AutoCounter.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef AUTOCOUNTER_H #define AUTOCOUNTER_H #include "../require.h" #include <iostream> #include <set> // Standard C++ Library container #include <string> class AutoCounter { static int count; int id; class CleanupCheck { std::set<AutoCounter*> trace; public: void add(AutoCounter* ap) { trace.insert(ap); } void remove(AutoCounter* ap) { require(trace.erase(ap) == 1, "Attempt to delete AutoCounter twice"); } ~CleanupCheck() { std::cout << "~CleanupCheck()"<< std::endl; require(trace.size() == 0, "All AutoCounter objects not cleaned up"); } }; static CleanupCheck verifier; AutoCounter() : id(count++) { verifier.add(this); // Register itself std::cout << "created[" << id << "]" << std::endl; } // Prevent assignment and copy-construction: AutoCounter(const AutoCounter&); void operator=(const AutoCounter&); public: // You can only create objects with this: static AutoCounter* create() { return new AutoCounter(); } ~AutoCounter() { std::cout << "destroying[" << id << "]" << std::endl; verifier.remove(this); } // Print both objects and pointers: friend std::ostream& operator<<( std::ostream& os, const AutoCounter& ac){ return os << "AutoCounter " << ac.id; } friend std::ostream& operator<<( std::ostream& os, const AutoCounter* ac){
441
La clase AutoCounter hace dos cosas. Primero, numera cada instancia de AutoCounter de forma secuencial: el valor de este nmero se guarda en id, y el nmero se genera usando el dato miembro count que es static. Segundo, y ms complejo, una instancia esttica (llamada verifier) de la clase CleanupCheck se mantiene al tanto de todos los objetos AutoCounter que son creados y destruidos, y nos informa si no se han limpiado todos (por ejemplo si existe un agujero en memoria). Este comportamiento se completa con el uso de la clase set de la Librera Estndar de C++, lo cual es un magnco ejemplo de cmo las plantillas bien diseadas nos pueden hacer la vida ms fcil (se podr aprender ms de los contenedores en el Volumen 2 de este libro). La clase set est instanciada para el tipo que maneja; aqu hay una instancia que maneja punteros a AutoCounter. Un set permite que se inserte slo una instancia de cada objeto; en add() se puede ver que esto sucede con la funcin set::insert(). insert() nos informa con su valor de retorno si se est intentando aadir algo que ya se haba incluido; sin embargo, desde el momento en que las direcciones a objetos se inserten podemos conar en C++ para que garantice que todos los objetos tengan direcciones nicas. En remove(), se usa set::erase() para eliminar un puntero a AutoCounter del set. El valor de retorno indica cuantas instancias del elemento se han eliminado; en nuestro caso el valor puede ser nicamente uno o cero. Si el valor es cero, sin embargo, signica que el objeto ya haba sido borrado del conjunto y que se est intentando borrar por segunda vez, lo cual es un error de programacin que debe ser mostrado mediante require(). El destructor de CleanupCheck hace una comprobacin nal asegurndose de que el tamao del set es cero - Lo que signica que todos los objetos han sido eliminados de manera adecuada. Si no es cero, se tiene un agujero de memoria, lo cual se muestra mediante el require(). El constructor y el destructor de AutoCounter se registra y desregistra con el objeto verifier. Hay que resaltar que el constructor, el constructor de copia, y el operador de asignacin son private, por lo que la nica forma de crear un objeto es con la funcin miembro static create() - esto es un ejemplo sencillo de una factory, y garantiza que todos los objetos sean creados en el montn (heap), por lo que verifier no se ver confundido con sobreasignaciones y construcciones de copia. Como todas las funciones miembro han sido denidas inline, la nica razn para el archivo de implementacin es que contenga las deniciones de los datos miembro:
//: C16:AutoCounter.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Definition of static class members #include "AutoCounter.h" AutoCounter::CleanupCheck AutoCounter::verifier; int AutoCounter::count = 0;
Con el AutoCounter en la mano, podemos comprobar las facilidades que proporciona el PStash. El siguiente ejemplo no slo muestra que el destructor de PStash limpia todos los objetos que posee, sino que tambin muestra como la clase AutoCounter detecta a los objetos que no se han limpiado.
//: C16:TPStashTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} AutoCounter
442
Cuando se eliminan los elementos AutoCounter 5 y 6 de la PStash, se vuelve responsabilidad del que los llama, pero como el cliente nunca los borra se podrn producir agujeros de memoria, que sern detectados por AutoCounter en tiempo de ejecucin. Cuando se ejecuta el programa, se ver que el mensaje de error no es tan especco como podra ser. Si se usa el esquema presentado en AutoCounter para descubrir agujeros de memoria en nuestro sistema, probablemente se quiera imprimir informacin ms detallada sobre los objetos que no se hayan limpiado. El Volumen 2 de este libro muestra algunas formas ms sosticadas de hacer esto.
Captulo 16. Introduccin a las Plantillas posicin debera saber cuando es necesario ser destruido; esto es una variante del conteo de referencias, excepto en que es el contenedor y no el objeto el que conoce el nmero de referencias a un objeto.
//: C16:OwnerStack.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Stack with runtime conrollable ownership #ifndef OWNERSTACK_H #define OWNERSTACK_H template<class T> class Stack { struct Link { T* data; Link* next; Link(T* dat, Link* nxt) : data(dat), next(nxt) {} }* head; bool own; public: Stack(bool own = true) : head(0), own(own) {} ~Stack(); void push(T* dat) { head = new Link(dat,head); } T* peek() const { return head ? head->data : 0; } T* pop(); bool owns() const { return own; } void owns(bool newownership) { own = newownership; } // Auto-type conversion: true if not empty: operator bool() const { return head != 0; } }; template<class T> T* Stack<T>::pop() { if(head == 0) return 0; T* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } template<class T> Stack<T>::~Stack() { if(!own) return; while(head) delete pop(); } #endif // OWNERSTACK_H
El comportamiento por defecto del contenedor consiste en destruir sus objetos pero se puede cambiar o modicando el argumento del constructor o usando las funciones miembro de owns(). Como con la mayora de las plantillas que se vern, la implementacin entera se encuentra en el archivo de cabecera. Aqu tenemos un pequeo test que muestra las capacidades de la propiedad:
//: C16:OwnerStackTest.cpp // From Thinking in C++, 2nd Edition
444
El objeto ac2 no posee los objetos que pusimos en l, sin embargo ac es un contenedor maestro que tiene la responsabilidad de ser el propietario de los objetos. Si en algn momento de la vida de un contenedor se quiere cambiar el que un contenedor posea a sus objetos, se puede hacer usando owns(). Tambin sera posible cambiar la granularidad de la propiedad para que estuviera en la base, es decir, objeto por objeto. Esto, sin embargo, probablemente hara a la solucin del problema del propietario ms complejo que el propio problema.
445
El constructor de copia de los objetos contenidos hacen la mayora del trabajo pasando y devolviendo objetos por valor. Dentro de push(), el almacenamiento del objeto en el array Stack viene acompaado con T::operator=. Para garantizar que funciona, una clase llamada SelfCounter mantiene una lista de las creaciones y construcciones de copia de los objetos.
//: C16:SelfCounter.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #ifndef SELFCOUNTER_H #define SELFCOUNTER_H #include "ValueStack.h" #include <iostream> class SelfCounter { static int counter; int id; public: SelfCounter() : id(counter++) { std::cout << "Created: " << id << std::endl; } SelfCounter(const SelfCounter& rv) : id(rv.id){ std::cout << "Copied: " << id << std::endl; } SelfCounter operator=(const SelfCounter& rv) { std::cout << "Assigned " << rv.id << " to " << id << std::endl; return *this; } ~SelfCounter() { std::cout << "Destroyed: "<< id << std::endl; } friend std::ostream& operator<<( std::ostream& os, const SelfCounter& sc){ return os << "SelfCounter: " << sc.id; } }; #endif // SELFCOUNTER_H
//: C16:SelfCounter.cpp {O} // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt
446
//: C16:ValueStackTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} SelfCounter #include "ValueStack.h" #include "SelfCounter.h" #include <iostream> using namespace std; int main() { Stack<SelfCounter> sc; for(int i = 0; i < 10; i++) sc.push(SelfCounter()); // OK to peek(), result is a temporary: cout << sc.peek() << endl; for(int k = 0; k < 10; k++) cout << sc.pop() << endl; }
Cuando se crea un contenedor Stack, el constructor por defecto del objeto a contener es ejecutado por cada objeto en el array. Inicialmente se vern 100 objetos SelfCounter creados sin ningn motivo aparente, pero esto es justamente la inicializacin del array. Esto puede resultar un poco caro, pero no existe ningn problema en un diseo simple como este. Incluso en situaciones ms complejas si se hace a Stack ms general permitiendo que crezca dinmicamente, porque en la implementacin mostrada anteriormente esto implicara crear un nuevo array ms grande, copiando el anterior al nuevo y destruyendo el antiguo array (de hecho, as es como lo hace la clase vector de la Librera Estndar de C++).
447
El IntStackIter ha sido creado para trabajar solo con un IntStack. Hay que resaltar que IntStackIter es un friend de IntStack, lo que lo da un acceso a todos los elementos privados de IntStack. Como un puntero, el trabajo de IntStackIter consiste en moverse a travs de un IntStack y devolver valores. En este sencillo ejemplo, el objeto IntStackIter se puede mover slo hacia adelante (usando la forma preja y suja del operador++ ). Sin embargo, no hay lmites de la forma en que se puede denir un iterador a parte de las restricciones impuestas por el contenedor con el que trabaje. Esto es totalmente aceptable (incluido los lmites del contenedor que se encuentre por debajo) para un iterador que se mueva de cualquier forma por su contenedor asociado y para que se puedan modicar los valores del contenedor. Es usual el que un iterador sea creado con un constructor que lo asocie a un nico objeto contenedor, y que ese iterador no pueda ser asociado a otro contenedor diferente durante su ciclo de vida. (Los iteradores son normalemente pequeos y baratos, por lo que se puede crear otro fcilmente). Con el iterador, se puede atravesar los elementos de la pila sin sacarlos de ella, como un puntero se mueve a travs de los elementos del array. Sin embargo, el iterador conoce la estructura interna de la pila y como atravesar 448
16.7. Introduccin a los iteradores los elementos, dando la sensacin de que se est moviendo a travs de ellos como si fuera incrementar un puntero, aunque sea ms complejo lo que pasa por debajo. Esta es la clave del iterador: Abstrae el proceso complicado de moverse de un elemento del contenedor al siguiente y lo convierte en algo parecido a un puntero. La meta de cada iterador del programa es que tengan la misma interfaz para que cualquier cdigo que use un iterador no se preocupe de a qu est apuntando - slo se sabe que todos los iteradores se tratan de la misma manera, por lo que no es importante a lo que apunte el iterador. De esta forma se puede escribir cdigo ms genrico. Todos los contenedores y algoritmos en la Librera Estndar de C++ se basan en este principio de los iteradores. Para ayudar a hacer las cosas ms genricas, sera agradable decir todas las clases contenedoras tienen una clase asociada llamada iterator, pero esto causar normalmente problemas de nombres. La solucin consite en aadir una clase anidada para cada contenedor (en este caso, iterator comienza con una letra minscula para que est conforme al estilo del C++ estndar). Aqu est el InterIntStack.cpp con un iterator anidado:
//: C16:NestedIterator.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Nesting an iterator inside the container //{L} fibonacci #include "fibonacci.h" #include "../require.h" #include <iostream> #include <string> using namespace std; class IntStack { enum { ssize = 100 }; int stack[ssize]; int top; public: IntStack() : top(0) {} void push(int i) { require(top < ssize, "Too many push()es"); stack[top++] = i; } int pop() { require(top > 0, "Too many pop()s"); return stack[--top]; } class iterator; friend class iterator; class iterator { IntStack& s; int index; public: iterator(IntStack& is) : s(is), index(0) {} // To create the "end sentinel" iterator: iterator(IntStack& is, bool) : s(is), index(s.top) {} int current() const { return s.stack[index]; } int operator++() { // Prefix require(index < s.top, "iterator moved out of range"); return s.stack[++index]; } int operator++(int) { // Postfix require(index < s.top, "iterator moved out of range"); return s.stack[index++]; } // Jump an iterator forward iterator& operator+=(int amount) {
449
Cuando se crea una clase friend anidada, hay que seguir el proceso de primero declarar el nombre de la clase, despus declararla como friend, y despus denir la clase. De otra forma, se confundir el compilador. Al iterador se le han dado algunas vueltas de tuerca ms. La funcin miembro current() produce el elemento que el iterador est seleccionando actualmente en el contenedor. Se puede saltar hacia adelante un nmero arbitrario de elementos usando el operator+=. Tambin, se pueden ver otros dos operadores sobrecargados: == y != que compararn un iterador con otro. Estos operadores pueden comparar dos IntStack::iterator, pero su intencin primordial es comprobar si el iterador est al nal de una secuencia de la misma manera que lo hacen los iteradores reales de la Librera Estndar de C++. La idea es que dos iteradores denan un rango, incluyendo el primer elemento apuntado por el primer iterador pero sin incluir el ltimo elemento apuntado por el segundo iterador. Por esto, si se quiere mover a travs del rango denido por los dos iteradores, se dir algo como lo siguiente:
while (star != end) cout << start++ << endl;
Donde start y end son los dos iteradores en el rango. Note que el iterador end, al cual se le suele referir como el end sentinel, no es desreferenciado y nos avisa que estamos al nal de la secuencia. Es decir, representa el que otro sobrepasa el nal. La mayora del tiempo se querr mover a travs de la secuencia entera de un contenedor, por lo que el contene450
16.7. Introduccin a los iteradores dor necesitar alguna forma de producir los iteradores indicando el principio y el nal de la secuencia. Aqu, como en la Standard C++ Library, estos iteradores se producen por las funciones miembro del contenedor begin() y end(). begin() usa el primer constructor de iterator que por defecto apunta al principio del contenedor (esto es el primer elemento que se introdujo en la pila). Sin embargo, un segundo constructor, usado por end(), es necesario para crear el iterador nal. Estar al nal signica apuntar a lo ms alto de la pila, porque top siempre indica el siguiente espacio de la pila que est disponible pero sin usar. Este constructor del iterator toma un segundo argumento del tipo bool, lo cual es til para distinguir los dos constructores. De nuevo se usan los nmeros Fibonacci para rellenar la IntStack en el main(), y se usan iteradores para moverse completamente a travs de la IntStack as como para moverse en un reducido rango de la secuencia. El siguiente paso, por supuesto, es hacer el cdigo general transformndolo en un template del tipo que maneje, para que en vez ser forzado a manejar enteros se pueda gestionar cualquier tipo:
//: C16:IterStackTemplate.h // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt // Simple stack template with nested iterator #ifndef ITERSTACKTEMPLATE_H #define ITERSTACKTEMPLATE_H #include "../require.h" #include <iostream> template<class T, int ssize = 100> class StackTemplate { T stack[ssize]; int top; public: StackTemplate() : top(0) {} void push(const T& i) { require(top < ssize, "Too many push()es"); stack[top++] = i; } T pop() { require(top > 0, "Too many pop()s"); return stack[--top]; } class iterator; // Declaration required friend class iterator; // Make it a friend class iterator { // Now define it StackTemplate& s; int index; public: iterator(StackTemplate& st): s(st),index(0){} // To create the "end sentinel" iterator: iterator(StackTemplate& st, bool) : s(st), index(s.top) {} T operator*() const { return s.stack[index];} T operator++() { // Prefix form require(index < s.top, "iterator moved out of range"); return s.stack[++index]; } T operator++(int) { // Postfix form require(index < s.top, "iterator moved out of range"); return s.stack[index++]; } // Jump an iterator forward iterator& operator+=(int amount) { require(index + amount < s.top, " StackTemplate::iterator::operator+=() "
451
Se puede ver que la transformacin de una clase regular en un template es razonablemente transparente. Esta aproximacin de primero crear y depurar una clase ordinaria, y despus transformarla en plantilla, est generalmente considerada como ms sencilla que crear el template desde la nada. Dese cuenta que en vez de slo decir:
friend iterator; // Hacerlo amigo
Esto es importante porque el nombre iterator ya existe en el mbito de resolucin, por culpa de un archivo incluido. En vez de la funcin miembro current(), el iterator tiene un operator* para seleccionar el elemento actual, lo que hace que el iterator se parezca ms a un puntero lo cual es una prctica comn. Aqu est el ejemplo revisado para comprobar el template.
//: C16:IterStackTemplateTest.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt //{L} fibonacci #include "fibonacci.h" #include "IterStackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(fibonacci(i)); // Traverse with an iterator: cout << "Traverse the whole StackTemplate\n";
452
El primer uso del iterador simplemente lo recorre de principio a n (y muestra que el lmite nal funciona correctamente). En el segundo uso, se puede ver como los iteradores permite fcilmente especicar un rango de elementos (los contenedores y los iteradores del Standard C++ Library usan este concepto de rangos casi en cualquier parte). El sobrecargado operator+= mueve los iteradores start y end a posiciones que estn en el medio del rango de elementos de is, y estos elementos son imprimidos. Hay que resaltar, como se ve en la salida, que el elemento nal no est incluido en el rango, o sea que una vez llegado al elemento nal (end sentinel) se sabe que se ha pasado el nal del rango - pero no hay que desreferenciar el elemento nal o si no se puede acabar desreferenciando un puntero nulo. (Yo he puesto un guardian en el StackTemplate::iterator, pero en la Librera Estndar de C++ los contenedores y los iteradores no tienen ese cdigo - por motivos de eciencia por lo que hay que prestar atencin). Por ltimo para vericar que el StackTemplate funciona con objetos clase, se instancia uno para strings y se rellena con lneas del cdigo fuente, las cuales son posteriormente imprimidas en pantalla.
453
454
Hay que hacer notar que la clase ha sido cambiada para soportar la posesin, que funciona ahora debido a que la clase conoce ahora el tipo exacto (o al menos el tipo base, que funciona asumiendo que son usados los destructores virtuales). La opcin por defecto es que el contenedor destruya sus objetos pero nosotros somos responsables de los objetos a los que se haga pop(). El iterador es simple, y fsicamente muy pequeo - el tamao de un nico puntero. Cuando se crea un iterator, se inicializa a la cabeza de la lista enlazada, y slo puede ser incrementado avanzando a travs de la lista. Si se quiere empezar desde el principio, hay que crear un nuevo iterador, y si se quiere recordar un punto de la lista, hay que crear un nuevo iterador a partir del iterador existente que est apuntando a ese elemento (usando el constructor de copia del iterador). Para llamar a funciones del objeto referenciado por el iterador, se puede usar la funcin current(), el operator*, o la desreferencia de puntero operator-> (un elemento comn en los iteradores). La ltima tiene una implementacin que parece idntica a current() debido a que devuelve un puntero al objeto actual, pero es diferente porque el operador desreferencia del puntero realiza niveles extra de desreferenciacin (ver Captulo 12). La clase iterator sigue el formato que se vio en el ejemplo anterior. class iterator est anidada dentro de la clase contenedora, contiene constructores para crear un iterador que apunta a un elemento en el contenedor y un iterador marcador de nal, y la clase contenedora tiene los mtodos begin() y end() para producir estos iteradores. (Cuando aprenda ms de la Librera Estndar de C++, ver que los nombres iterator, begin() y end() que se usan aqu tienen correspondecia en las clases contenedoras. Al nal de este captulo, se ver que esto permite manejar estas clases contenedoras como si fueran clases de la STL). La implementacin completa se encuentra en el archivo cabecera, por lo que no existe un archivo cpp separado. Aqu tenemos un pequeo test que usa el iterador.
//: C16:TStack2Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "TStack2.h" #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { ifstream file("TStack2Test.cpp"); assure(file, "TStack2Test.cpp"); Stack<string> textlines; // Read file and store lines in the Stack: string line; while(getline(file, line)) textlines.push(new string(line)); int i = 0; // Use iterator to print lines from the list: Stack<string>::iterator it = textlines.begin(); Stack<string>::iterator* it2 = 0; while(it != textlines.end()) { cout << it->c_str() << endl; it++; if(++i == 10) // Remember 10th line it2 = new Stack<string>::iterator(it); } cout << (*it2)->c_str() << endl; delete it2; }
455
Una pila Stack es instanciada para gestionar objetos string y se rellena con lneas de un chero. Entonces se crea un iterador y se usa para moverse a travs de la secuencia. La dcima lnea es recordada mediante un segundo iterador creado con el constructor de copia del primero; posteriormente esta lnea es imprimida y el iterador - crado dinmicamente - es destruido. Aqu la creacin dinmica de objetos es usada para controlar la vida del objeto.
456
457
La mayora de este archivo es un traduccin prcticamente directa del anterior PStash y el iterador anidado dentro de un template. Esta vez, sin embargo, el operador devuelve referencias al iterador actual, la cual es una aproximacin ms tpica y exible. El destructor llama a delete para todos los punteros que contiene, y como el tipo es obtenido de la plantilla, se ejecutar la destruccin adecuada. Hay que estar precavido que si el contenedor controla punteros al tipo de la clase base, este tipo debe tener un destructor virtual para asegurar un limpiado adecuado de los objetos derivados que hayan usado un upcast cuando se los aloj en el contenedor. El PStash::iterator mantiene el modelo de engancharse a un nico objeto contenedor durante su ciclo de vida. Adems, el constructor de copia permite crear un nuevo iterador que apunte a la misma posicin del iterador desde el que se le creo, creando de esta manera un marcador dentro del contenedor. Las funciones miembro operator+= y el operator-= permiten mover un iterador un nmero de posiciones, mientras se respeten los lmites del contenedor. Los operadores sobrecargados de incremento y decremento mueven el iterador una posicin. El operator+ produce un nuevo iterador que se mueve adelante la cantidad aadida. Como en el ejemplo anterior, los operadores de desreferencia de punteros son usados para manejar el elemento al que el iterador est referenciando, y remove() destruye el objeto actual llamando al remove() del contenedor. 458
16.7. Introduccin a los iteradores Se usa la misma clase de cdigo de antes para crear el marcador nal: un segundo constructor, la funcin miembro del contenedor end(), y el operator== y operator!= para comparaciones. El siguiente ejemplo crea y comprueba dos diferentes clases de objetos Stash, uno para una nueva clase llamada Int que anuncia su construccin y destruccin y otra que gestiona objetos string de la librera Estndar.
//: C16:TPStash2Test.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include "TPStash2.h" #include "../require.h" #include <iostream> #include <vector> #include <string> using namespace std; class Int { int i; public: Int(int ii = 0) : i(ii) { cout << ">" << i << ; } ~Int() { cout << "~" << i << ; } operator int() const { return i; } friend ostream& operator<<(ostream& os, const Int& x) { return os << "Int: " << x.i; } friend ostream& operator<<(ostream& os, const Int* x) { return os << "Int: " << x->i; } }; int main() { { // To force destructor call PStash<Int> ints; for(int i = 0; i < 30; i++) ints.add(new Int(i)); cout << endl; PStash<Int>::iterator it = ints.begin(); it += 5; PStash<Int>::iterator it2 = it + 10; for(; it != it2; it++) delete it.remove(); // Default removal cout << endl; for(it = ints.begin();it != ints.end();it++) if(*it) // Remove() causes "holes" cout << *it << endl; } // "ints" destructor called here cout << "\n-------------------\n"; ifstream in("TPStash2Test.cpp"); assure(in, "TPStash2Test.cpp"); // Instantiate for String: PStash<string> strings; string line; while(getline(in, line)) strings.add(new string(line)); PStash<string>::iterator sit = strings.begin(); for(; sit != strings.end(); sit++) cout << **sit << endl; sit = strings.begin(); int n = 26;
459
Por conveniencia Int tiene asociado un ostream operator<< para Int& y Int*. El primer bloque de cdigo en main() est rodeado de llaves para forzar la destruccin de PStash<Int> que produce un limpiado automtico por este destructor. Unos cuantos elementos son sacados y borrados a mano para mostrar que PStash limpia el resto. Para ambas instancias de PStash, se crea un iterador y se usa para moverse a travs del contenedor. Note la elegancia generada por el uso de estos constructores; no hay que preocuparse por los detalles de implementacin de usar un array. Se le dice al contenedor y al iterador qu hacer y no cmo hacerlo. Esto produce una solucin ms sencilla de conceptualizar, construir y modicar.
460
Se usa la estructura clsica de las funciones virtuales en la clase base que son sobreescritas en la clase derivada. Hay que resaltar que la clase Shape incluye un destructor virtual, algo que se debera aadir automticamente a cualquier clase con funciones virtuales. Si un contenedor maneja punteros o referencias a objetos Shape, entonces cuando los destructores virtuales sean llamados para estos objetos todo ser correctamente limpiado. Cada tipo diferente de dibujo en el siguiente ejemplo hace uso de una plantilla de clase contenedora diferente: el PStash y el Stack que han sido denido en este captulo, y la clase vector de la Librera Estndar de C++. El uso de los contenedores es extremadamente simple, y en general la herencia no es la mejor aproximacin (composicin puede tener ms sentido), pero en este caso la herencia es una aproximacin ms simple.
//: C16:Drawing.cpp // From Thinking in C++, 2nd Edition // Available at http://www.BruceEckel.com // (c) Bruce Eckel 2000 // Copyright notice in Copyright.txt #include <vector> // Uses Standard vector too! #include "TPStash2.h" #include "TStack2.h" #include "Shape.h" using namespace std; // A Drawing is primarily a container of Shapes: class Drawing : public PStash<Shape> { public: ~Drawing() { cout << "~Drawing" << endl; } }; // A Plan is a different container of Shapes: class Plan : public Stack<Shape> { public: ~Plan() { cout << "~Plan" << endl; } }; // A Schematic is a different container of Shapes: class Schematic : public vector<Shape*> { public: ~Schematic() { cout << "~Schematic" << endl; } }; // A function template: template<class Iter> void drawAll(Iter start, Iter end) { while(start != end) { (*start)->draw(); start++; } } int main() { // Each type of container has // a different interface:
461
Los distintos tipos de contenedores manejan punteros a Shape y punteros a objetos de clases derivadas de Shape. Sin embargo, debido al polimorsmo, cuando se llama a las funcione virtuales ocurre el comportamiento adecuado. Note que sarray, el array de Shape*, puede ser recorrido como un contenedor.
16.9. Resumen principio y al nal del array sarray. Esta habilidad para tratar arrays como contenedores est integrada en el diseo de la Librera Estndar de C++, cuyos algoritmos se parecen mucho a drawAll(). Debido a que las plantillas de clases contenedoras estn raramente sujetas a la herencia y al upcast se ven como clases ordinarias, casi nunca se vern funciones virtuales en clases contenedoras. La reutilizacin de las clases contenedoras est implementado mediante plantillas, no mediante herencia.
16.9. Resumen
Las clases contenedoras son una parte esencial de la programacin orientada a objetos. Son otro modo de simplicar y ocultar los detalles de un programa y de acelerar el proceso de desarrollo del programa. Adems, proporcionan un gran nivel de seguridad y exibilidad reemplazando los anticuados arrays y las relativamente toscas tcnicas de estructuras que se pueden encontrar en C. Como el programador cliente necesita contenedores, es esencial que sean fciles de usar. Aqu es donde entran los templates. Con las plantillas la sintaxis para el reciclaje del cdigo fuente (al contrario del reciclaje del cdigo objeto que proporciona la herencia y la composicin) se vuelve lo sucientemente trivial para el usuario novel. De hecho, la reutilizacin de cdigo con plantillas es notablemente ms fcil que la herencia y el polimorsmo. Aunque se ha aprendido cmo crear contenedores y clases iteradoras en este libro, en la prctica es mucho ms til aprender los contenedores e iteradores que contiene la Librera Estndar de C++, ya que se puede esperar encontrarlas en cualquier compilador. Como se ver en el Volumen 2 de este libro (que se puede bajar de www.BruceEckel.com, los contenedores y algoritmos de la STL colmarn virtualmente sus necesidades por lo que no tendr que crear otras nuevas. Las caractersticas que implica el diseo con clases contenedoras han sido introducidas a lo largo de todo el captulo, pero hay que resaltar que van mucho ms all. Una librera de clases contenedoras ms complicada debera cubrir todo tipo de caractersticas adicionales, como la multitarea, la persistencia y la recoleccin de basura.
16.10. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Implemente la jerarqua de herencia del diagrama de OShape de este captulo. 2. Modique el resultado del Ejercicio 1 del captulo 15 para usar la Stack y el iterator en TStack2. h en vez de un array de punteros a Shape. Aada destructores a la jerarqua de clases para que se pueda ver que los objetos Shape han sido destruidos cuando la Stack se sale del mbito. 3. Modique TPStash.h para que el valor de incremento usado por inflate() pueda ser cambiado durante la vida de un objeto contenedor particular. 4. Modique TPStash.h para que el valor de incremento usado por inflate() automticamente cambie de tamao para que reduzca el nmero de veces que debe ser llamado. Por ejemplo, cada vez que se llama podra doblar el valor de incremento para su uso en la siguiente llamada. Demuestre la funcionalidad mostrando cada vez que se llama a inflate(), y escriba cdigo de prueba en main(). 5. Convierta en plantilla la funcin de fibonacci() con los tipos que puede producir (puede generar long, oat, etc. en vez de slo int). 6. Usar el vector de la STL como implementacin subyacente, para crear una platilla Set que acepte solo uno de cada tipo de objeto que se aloje en l. Cree un iterador anidado que soporte el concepto de "marcador nal" de este captulo. Escriba cdigo de prueba para el Set en el main(), y entonces sustituyalo por la plantilla set de la STL para comprobar que el comportamiento es correcto. 7. Modique AutoCounter.h para que pueda ser usado como un objeto miembro dentro de cualquier clase cuya creacin y destruccin quiera comprobar. Aada un miembro string para que contenga el nombre de la clase. Compruebe esta herramienta dentro una clase suya. 463
Captulo 16. Introduccin a las Plantillas 8. Cree una versin de OwnerStack.h que use un vector de la Librera Estndar de C++ como su implementacin subyacente. Ser necesario conocer algunas de las funciones miembro de vector para poder hacerlo (slo hay que mirar en el archivo cabecera <vector>). 9. Modique ValueStack.h para que pueda expandirse dinmicamente segn se introduzcan ms objetos y se quede sin espacio. Cambie ValueStackTest.cpp para comprobar su nueva funcionalidad. 10. Repita el ejercicio 9 pero use el vector de la STL como la implementacin interna de ValueStack. Note lo sencillo que es. 11. Modique ValueStackTest.cpp para que use un vector de la STL en vez de un Stack en el main(). Dse cuenta del comportamiento en tiempo de ejecucin: Se genera un grupo de objetos por defecto cuando se crea el vector? 12. Modique TStack2.h para que use un vector de la STL. Asegurese de que no cambia la interfaz, para que TStack2Test.cpp funcione sin cambiarse. 13. Repita el Ejercicio 12 usando una stack de la Librera Estndar de C++ en vez de un vector. 14. Modique TPStash2.h para que use un vector de la STL como su implementacin interna. Asegurese que no cambia la interfaz, por lo que TPStash2Test.cpp funciona sin modicarse. 15. En IterIntStack.cpp, modique IntStackIter para darle un constructor de marcador nal, y aada el operator== y el operator!=. En el main(), use un iterador para moverse a travs de los elementos del contenedor hasta que se encuentre el marcador. 16. Use TStack2.h, TPSTash2.h, y Shape.h, instancie los contenedores PStash y Stack para que contenga Shape*, rellene cada uno con punteros a Shape, entonces use iteradores para moverse a travs de cada contenedor y llame a draw() para cada objeto. 17. Cree una plantilla en la clase Int para que pueda alojar cualquier tipo de objetos (Sintase libre de cambiar el nombre de la clase a algo ms apropiado). 18. Cree una plantilla de la clase IntArray en IostreamOperatorOverloading.cpp del captulo 12, introduzca en plantilla ambos tipos de objetos que estn contenidos y el tamao del array interno 19. Convierta ObjContainer en NestedSmartPointer.cpp del Captulo 12 en una plantilla. Compruebelo con dos clases diferentes. 20. Modique C15:OStack.h y C15:OStackTest.cpp consiguiendo que class Stack pueda tener mltiple herencia automticamente de la clase contenida y de Object. La Stack contenida debe aceptar y producir slo punteros del tipo contenido. 21. Repita el ejercicio 20 usando vector en vez de Stack. 22. Herede una clase StringVector de vector<void> y redena las funciones miembro push_back() y el operator[] para que acepten y produzcan nicamente string* (y realizen el moldeado adecuado). Ahora creee una plantilla que haga automticamente lo mismo a una clase contenedora para punteros de cualquier tipo. Esta tcnica es a menudo usada para reducir el cdigo producido por muchas instanciaciones de templates. 23. En TPStash2.h, aada y compruebe un operator- para PStash::iterator, siguiendo la lgica de operator+. 24. En Drawing.cpp, aada y compruebe una plantilla de funcin que llame a funciones miembro erase(). 25. (Avanzado) Modique la clase Stack en TStack2.h para permitir una granularidad de la propiedad: Aada una bandera para cada enlace indicando si el enlace posee el objeto al que apunta, y de soporte a esta informacin la funcin push() y en el destructor. Aada funciones miembro para leer y cambiar la propiedad de cada enlace. 26. (Avanzado) Modique PointerToMemberOperator.cpp del Captulo 12 para que la FunctionObject y el operator->* sean convertidos en plantillas para que funcionen con cualquier tipo de retorno (para operator->*, tendr que usar plantillas miembro descritas en el Volumen 2). Aada soporte y compruebe para cero, uno y dos argumentos en las funciones miembro Dog.
464
A: Estilo de codicacin
Este apndice no trata sobre indentacin o colocacin de parntesis y llaves, aunque s que se menciona. Trata sobre las directrices generales que se usan en este libro para la organizacin de los listados de cdigo.
Aunque muchas de estas cuestiones se han tratado a lo largo del libro, este apndice aparece al nal de manera que se puede asumir que cada tema es FIXME:juego limpio, y si no entiende algo puede buscar en la seccin correspondiente. Todas las decisiones sobre estilo de codicacin en este libro han sido consideradas y ejectuadas deliberadamente, a veces a lo largo de perodos de aos. Por supuesto, cada uno tiene sus razones para organizar el cdigo en el modo en que lo hace, y yo simplemente intento explicarle cmo llegu a tomar mi postura y las restricciones y factores del entorno que me llevaron a tomar esas decisiones.
A.1. General
En el texto de este libro, los identicadores (funciones, variables, y nombres de clases) aparecen en negrita. Muchas palabras reservadas tambin son negritas, exceptuando aquellas que se usan tan a menudo que escribirlas en negrita puede resultar tedioso, como class o virtual. Utilizo un estilo de codicacin particular para los ejemplos de este libro. Se desarroll a lo largo de varios aos, y se inspir parcialmente en el estilo de Bjarne Stroustrup en el The C++ Programming Language 1 original. El asunto del estilo de codicacin es ideal para horas de acalorado debate, as que slo dir que no trato de dictar el estilo correcto a travs de mis ejemplos; tengo mis propios motivos para usar el estilo que uso. Como C++ es un lenguaje de FIXME:forma libre, cada uno puede continuar usando aquel estilo en que se encuentre ms cmodo. Dicho esto, s har hincapi en que es importante tener un estilo consistente dentro de un proyecto. Si busca en Internet, encontrar un buen nmero de herramientas que se pueden utilizar para reformatear todo el cdigo de un proyecto para conseguir esa valiosa consistencia. Los programas de este libro son cheros extrados automticamentente del texto del libro, lo que permite que se puedan probar para asegurar que funcionan correctamente 2 . De ese modo, el cdigo mostrado en el libro debera funcionar sin errores cuando se compile con una implementacin conforme al Estndar C++ (no todos los compiladores soportan todas las caractersticas del lenguaje). Las sentencias que deberan causar errores de compilacin estn comentadas con //! de modo que se pueden descubrir y probar fcilmente de modo automtico. Los errores descubiertos por el autor aparecern primero en la versin electrnica del libro (www.BruceEckel.com) y despus en las actualizaciones del libro. Uno de los estndares de este libro es que todos los programas compilarn y enlazarn sin errores (aunque a veces causarn advertencias). Algunos de los programas, que demuestran slo un ejemplo de codicacin y no representan programas completos, tendrn funciones main() vacas, como sta:
int main() {}
FIXME:Ibid. (N. de T.) Se reere al libro original. En esta traduccin, los programas son cheros externos incluidos en el texto.
465
Apndice A. Estilo de codicacin El estndar para main() es retornar un int, pero C++ Estndar estipula que si no hay una sentencia return en main(), el compilador generar automticamente cdigo para return 0. Esta opcin (no poner un return en main()) se usa en el libro (algunos compiladores producen advertencias sobre ello, pero es porque no son conformes con C++ Estndar).
466
A.4. Parntesis, llaves e indentacin de ms puede provocar este efecto. Todo el mundo parece estar de acuerdo en que el cdigo que se pone dentro de llaves debe estar indentado. En lo que la gente no est de acuerdo - y es el sitio donde ms inconsistencia tienen los estilos - es: Dnde debe ir la llave de apertura? Esta nica cuestin, creo yo, es la que causa la mayora de las variaciones en los estilos de codicacin (Si quiere ver una enumeracin de estilos de codicacin vea C++ Programming Guidelines, de [FIXME:autores] Tom Plum y Dan Saks, Plum Hall 1991), Intentar convencerle de que muchos de los estilos de codicacin actuales provienen de la restricciones previas al C Estndar (antes de los prototipos de funcin) de manera que no son apropiadas actualmente. Lo primero, mi respuesta a esa pregunta clave: la llave de apertura debera ir siempre en la misma lnea que el precursor (es decir cualquier cosa de la que sea cuerpo: una clase, funcin, denicin de objeto, sentencia if, etc. Es una regla nica y consistente que aplico a todo el cdigo que escribo, y hace que el formateo de cdigo sea mucho ms sencillo. Hace ms sencilla la escaneabilidad - cuando se lee esta lnea:
int func(int a);
Se sabe, por el punto y coma al nal de la lnea, que esto es una declaracin y no hay nada ms, pero al leer la lnea:
int func(int a) {
inmediatamente se sabe que se trata de una denicin porque la lnea termina con una llave de apertura, y no un punto y coma. Usando este enfoque, no hay diferencia a la hora de colocar el parntesis de apertura en una denicin de mltiples lneas.
int func(int a) { int b = a + 1; return b * 2; }
y para una denicin de una sola lnea que a menudo se usa para inlines:
int func(int a) { return (a + 1) * 2; }
es una denicin de clase. En todos los casos, se puede saber mirando una sola lnea si se trata de una declaracin o una denicin. Y por supuesto, escribir la llave de apertura en la misma lnea, en lugar de una lnea propia, permite ahorrar espacio en la pgina. As que por qu tenemos tantos otros estilos? En concreto, ver que mucha gente crea clases siguiente el estilo anterior (que Stroustrup usa en todas las ediciones de su libro The C++ Programming Language de AddisonWesley) pero crean deniciones de funciones poniendo la llave de apertura en una lnea aparte (lo que da lugar a muchos estilos de indentacin diferentes). Stroustrup lo hace excepto para funciones inline cortas. Con el enfoque que yo describo aqu, todo es consistente - se nombra lo que sea (class, functin, enum, etc) y en la misma lnea se pone la llave de apertura para indicar que el cuerpo de esa cosa est debajo. Y tambin, la llave de apertura se pone en el mismo sitio para funciones inline que para deniciones de funciones ordinarias. Creo que el estilo de denicin de funciones que utiliza mucha gente viene de el antiguo prototipado de funciones de C, en el que no se declaraban los argumentos entre los parntesis, si no entre el parntesis de cierre y la 467
Apndice A. Estilo de codicacin llave de apertura (esto demuestra que las races de C son el lenguaje ensamblador):
void bar() int x; float y; { /* body here */ }
Aqu, quedara bastante mal poner la llave de apertura en la misma lnea, as que nadie lo haca. Sin embargo, haba distintas opiniones sobre si las llaves deban indentarse con el cuerpo del cdigo o deban dejarse a nivel con el precursor. De modo que tenemos muchos estilos diferentes. Hay otros argumentos para poner la llave en la lnea siguiente a la declaracin (de una clase, struct, funcin, etc). Lo siguiente proviene de un lector, y lo presento aqu para que sepa a qu se reere. Los usuarios experimentado de vi (vim) saben que pulsar la tecla ] dos veces lleva el cursor a la siguiente ocurrencia de { (o L) en la columna 0. Esta caracterstica es extremadamente til para moverse por el cdigo (saltando a la siguiente decin de funcin o clase). [Mi comentario: cuando yo trabajaba en Unix, GNU Emacs acababa de aparecer y yo me convert en un fan suyo. Como resultado, vi nunca ha tenido sentido para m, y por eso yo no pienso en trminos de situacin de columna 0. Sin embargo, hay una buena cantidad de usuarios de vi ah fuera, a los que les afecta esta caracterstica.] Poniendo la { en la siguiente lnea se eliminan algunas confusiones en sentencias condicionales complejas, ayudando a la escaneabilidad.
if (cond1 && cond2 && cond3) { statement; }
separa el if del cuerpo, mejorando la legibilidad. [Sus opiniones sobre si eso es cierto variarn dependiendo para qu lo haya usado.] Finalmente, es mucho ms fcil visualizar llaves emparejadas si estn alineadas en la misma columna. Visualmente destacan mucho ms. [Fin del comentario del lector] El tema de dnde poner la llave de apertura es probablemente el asunto en el que hay menos acuerdo. He aprendido a leer ambas formas, y al nal cada uno utiliza la que le resulta ms cmoda. Sin embargo, he visto que el estndar ocial de codicacin de Java (que se puede encontar en la pgina de Java de Sun) efectivamente es el mismo que yo he presentado aqu - dado que ms personas estn empezando a programar en ambos lenguajes, la consistencia entre estilos puede ser til. Mi enfoque elimina todas las excepciones y casos especiales, y lgicamente produce un nico estilo de indentacin, Incluso con un cuerpo de funcin, la consistencia se mantiene, como en:
for(int i = 0; i < 100; i++) { cout << i << endl; cout << x * i << endl; }
468
A.5. Nombres para identicadores El estilo es fcil de ensear y recordar - use una regla simple y consistente para todo sus formatos, no una para clases, dos para funciones (funciones inline de una lnea vs. multi-lnea), y posiblemente otras para bucles, sentencias if, etc. La consistencia por si sola merece ser tenida en cuenta. Sobre todo, C++ es un lenguaje ms nuevo que C, y aunque debemos hacer muchas concesiones a C, no deberamos acarrear demasiados FIXME:artifacts que nos causen problemas en el futuro. Problemas pequeos multiplicados por muchas lneas de cdigo se convierten en grandes problemas. Para un examen minucioso del asunto, aunque trata de C, vea C Style: Standards and Guidelines, de David Straker (Prentice-Hall 1992). La otra restriccin bajo la que debo trabajar es la longitud de la lnea, dado que el libro tiene una limitacin de 50 caracteres. Qu ocurre si algo es demasiado largo para caber en una lnea? Bien, otra vez me esfuerzo en tener una poltica consistente para las lneas partidas, de modo que sean fcilmente visibles. Siempre que sean parte de una nica denicin, lista de argumentos, etc., las lneas de continuacin deberan indentarse un nivel respecto al comienzo de la denicin, lista de argumentos, etc.
y una funcin:
void eatIceCreamCone();
(tanto para un mtodo como para un funcin normal). La nica excepcin son las constantes en tiempo de compilacin (const y #define), en las que todas las letras del identicador son maysculas. El valor del estilo es que el uso de maysculas tiene signicado - viendo la primera letra se puede saber si es una clase o un objeto/mtodo. Esto es especialmente til cuando se invocan miembros estticos.
Apndice A. Estilo de codicacin chero de cabecera no es coherente por si mismo, lo descubrir antes y prevendr disgustos en el futuro.
El identicador de la ltima lnea se incluye nicamente por claridad. Algunos preprocesadores ignoran cualquier carcter que aparezca despus de un #endif, pero no es el comportamiento estndar y por eso el identicador aparece comentado.
470
B: Directrices de Programacin
Este apndice es una coleccin de sugerencias para programacin con C++. Se han reunido a lo largo de mi experiencia en como docente y programador y
tambin de las aportaciones de amigos incluyendo a Dan Saks (co-autor junto a Tom Plum de C++ Programming Guidelines, Plum Hall, 1991), Scott Meyers (autor de Effective C++, 2 edicin, Addison-Wesley, 1998), and Rob Murray (autor de C++ Strategies & Tactics, Addison-Wesley, 1993). Tambin, muchos de los consejos estn resumidos a partir del contenido de Thinking in C++. 1. Primero haga que funcione, despus hgalo rpido. Esto es cierto incluso si se est seguro de que una trozo de cdigo es realmente importante y se sabe que ser un cuello de botella es el sistema. No lo haga. Primero, consiga que el sistema tenga un diseo lo ms simple posible. Entonces, si no es sucientemente rpido, optimcelo. Casi siempre descubrir que su cuello de botella no es el problema. Guarde su tiempo para lo verdaderamente importante. 2. La elegancia siempre vale la pena. No es un pasatiempo frvolo. No slo permite que un programa sea ms fcil de construir y depurar, tambin es ms fcil de comprender y mantener, y ah es donde radica su valor econmico. Esta cuestin puede requerir de alguna experiencia para creerselo, porque puede parecer que mientras se est haciendo un trozo de cdigo elegante, no se es productivo. La productividad aparece cuando el cdigo se integra sin problemas en el sistema, e incluso cuando se modica el cdigo o el sistema. 3. Recuerde el principio divide y vencers. Si el problema al que se enfrenta es desmasiado confuso, intente imaginar la operacin bsica del programa se puede hacer, debido a la existencia de una pieza mgica que hace el trabajo difcil. Esta pieza es un objeto - escriba el cdigo que usa el objeto, despus implemente ese objeto encapsulando las partes difciles en otros objetos, etc. 4. No reescriba automticamente todo su cdigo C a C++ a menos que necesite un cambiar signicativamente su funcionalidad (es decir, no lo arregle si no est roto). Recompilar C en C++ es un positivo porque puede revelar errores ocultos. Sim embargo, tomar cdigo C que funciona bien y reescribirlo en C++ no es la mejor forma de invertir el tiempo, a menos que la versin C++ le ofrezca ms oportunidad de reutilizarlo como una clase. 5. Si tiene un gran trozo de cdigo C que necesite cambios, primero aisle las partes del cdigo que no se modicar, posiblemente envolviendo esas funciones en una clase API como mtodos estticos. Despus ponga atncin al cdigo que va a cambiar, recolocandolo dentro de clases para facilitar las modicaciones en el proceso de mantenimiento. 6. Separe al creador de la clase del usuario de la clase (el programador cliente). El usuario de la clase es el consumidor y no necesita o no quiere conocer que hay dentro de la clase. El creador de la clase debe ser un experto en diseo de clases y escribir la clase para que pueda ser usada por el programador ms inexperto posible, y an as funcionar de forma robusta en la aplicacin. El uso de la librera ser sencillo slo is es transparente. 7. Cuando cree una clase, utilice nombres tan claros como sea posible. Eo objetivo debera ser que la interface del programador cliente sea conceptualmente simple. Intente utilizar nombres tan claros que los comentarios sean innecesarios. Luego, use sobrecarga de funciones y argumentos por defecto para crear un interface intuitiva y fcil de usar. 8. El control de acceso permite (al creador de la clase) cambiar tanto como sea posible en el futuro sin afectar al cdigo del cliente en el que se usa la clase. FIXME:Is this light, mantenga todo tan privado como sea posible, y haga pblica solamente la interfaz de la clase, usando siempre mtodos en lugar de atributos. 471
Apndice B. Directrices de Programacin Ponga atributos pblicos slo cuando se vea obligado. Si una parte de su clase debe quedar expuesta a clases derivadas como protegida, proporcione una interface con funciones en lugar de exponer los datos reales. De este modo, los cambios en la implementacin tendrn un impacto mnimo en las clases derivadas. 9. FIXME No caiga en FIXME:analysis paralysis. Hay algunas cosas que no aprender hasta que empiece a codicar y consiga algn tipo de sistema. C++ tiene mecanimos de seguridad de fbrica, dejelos trabajar por usted. Sus errores en una clase o conjunto de clases no destruir la integridad del sistema completo. 10. El anlisis y diseo debe producir, como mnimo, las clases del sistema, sus interfaces pblicas, y las relaciones con otras clases, especialmente las clases base. Si su metodologa de diseo produce ms que eso, preguntese a si mismo si todas las piezas producidas por la metodologa tiene valor respecto al tiempo de vide del programa. Si no lo tienen, no mantenga nada que no contribuya a su productividad, este es un FIXME:fact of life] que muchos mtodos de diseo no tienen en cuenta. 11. Escriba primero el cdigo de las pruebas (antes de escribir la clase), y guardelo junto a la clase. Automatice la ejecucin de las pruebas con un makefile o herramienta similar. De este modo, cualquier cambio se puede vericar automticamente ejecutando el cdigo de prueba, lo que permite descubrir los errores inmediatamante. Como sabe que cuenta con esa red de seguridad, puede arriesgar haciendo cambios ms grandes cuando descubra la necesidad. Recuerde que las mejoras ms importantes en los lenguajes provienen de las pruebas que hace el compilador: chequeo de tipos, gestin de excepciones, etc., pero estas caractersticas no puede ir muy lejos. Debe hacer el resto del camino creando un sistema robusto rellenando las pruebas que verican las caractersticas especcas de la clase o programa concreto. 12. Escriba primero el cdigo de las pruebas (antes de escribir la clase) para vericar que el diseo de la clase est completo. Si no puede escribir el cdigo de pruebas, signica que no sabe que aspecto tiene la clases. En resumen, el echo de escribir las pruebas a menudo desvela caractersticas adicionales o restricciones que necesita la clase - esas caractersticas o restricciones no siempre aparecen durante el anlisis y diseo. 13. Recuerde una regla fundamental de la ingeniera del software 1 : Todos los problemas del diseo de software se puede simplicar introduciendo una nivel ms de indireccin conceptual. Esta nica idea es la pase de la abstraccin, la principal cualidad de la programacin orientada a objetos. 14. Haga clases tan atmicas como sea posible: Es decir, d a cada clase un propsito nico y claro. Si el diseo de su clase o de su sistema crece hasta ser demasiado complicado, divida las clases complejas en otras ms simples. El indicador ms obvio es tamao total: si una clase es grande, FIXME: chances are its doing demasiado y debera dividirse. 15. Vigile las deniciones de mtodos largos. Una funcin demasiado larga y complicada es dicil y cara de mantener, y es problema que est intentado hacer demasiado trabajo por ella misma. Si ve una funcin as, indica que, al menos, debera dividirse en mltiples funciones. Tambin puede sugerir la creacin de una nueva clase. 16. Vigile las listas de argumentos largas. Las llamadas a funcin se vuelven difciles de escribir, leer y mantener. En su lugar, intente mover el mtodo a una clase donde sea ms apropiado, y/o pasele objetos como argumentos. 17. No se repita. Si un trozo de cdigo se repite en muchos mtodos de las clases derivadas, ponga el cdigo en un mtodo de la clase base e invquelo desde las clases derivadas. No slo ahorrar cdigo, tambin facilita la propagacin de los cambios. Puede usar una funcin inline si necesita eciencia. A veces el descubrimiento de este cdigo comn aadir funcionalidad valiosa a su interface. 18. Vigile las sentencias switch o cadenas de if-else. Son indicadores tpicos de cdigo dependiente del tipo, lo que signica que est decidiendo qu cdigo ejecutar basndose en alguna informacin de tipo (el tipo exacto puede no ser obvio en principio). Normalemente puede reemplazar este tipo de cdigo por herencia y polimorsmo; una llamada a una funcin polimrca efectuar la comprobacin de tipo por usted, y har que el cdigo sea ms able y sencillo de extender. 19. Desde el punto de vista del diseo, busque y distinga cosas que cambian y cosas que no cambian. Es decir, busque elementos en un sistema que podran cambiar sin forzar un rediseo, despus encapsule esos elementos en clases. Puede aprender mucho ms sobre este concepto en el captulo Dessign Patterns del Volumen 2 de este libro, disponible en www.BruceEckel.com 2
1 2
Que me explic Andrew Koening. (N. de T.) Est prevista la traduccin del Volumen 2 por parte del mismo equipo que ha traducido este volumen. Visite FIXME
472
20. Tenga cuidado con las FIXME discrepancia. Dos objetos semnticamente diferentes puede tener acciones idnticas, o responsabilidades, y hay una tendencia natural a intentar hacer que una sea subclase de la otra slo como benecio de la herencia. Ese se llama discrepancia, pero no hay una justicacin real para forzar una relacin superclase/subclase donde no existe. Un solucin mejor es crear una clase base general que produce una herencia para las dos como clases derivadas - eso require un poco ms de espacio, pero sigue beneciandose de la herencia y probablemente har un importante descubrimiento sobre el diseo. 21. Tenga cuidado con la FIXME: limitacin de la herencia. Los diseos ms lmpios aaden nuevas capacidades a las heredadas. Un diseo sospechoso elimina capacidades durante la herencia sin aadir otras nuevas. Pero las reglas estn hechas para romperse, y si est trabajando con una librera antigua, puede ser ms eciente restringir una clase existente en sus subclases que restructurar la jerarqua de modo que la nueva clase encaje donde debera, sobre la clase antigua. 22. No extienda funcionalidad fundamental por medio de subclases. Si un elemento de la interfaz es esecial para una clase debera estr en la clase base, no aadido en una clase derivada. Si est aadiendo mtodos por herencia, quiz debera repensar el diseo. 23. Menos es ms. Empiece con una interfaz mnima a una clase, tan pequea y simple como necesite para resolver el problema que est tratando, pero no intente anticipar todas las formas en las que se podra usar la clase. Cuando use la clase, descubrir formas de usarla y deber expandir la interface. Sin embargo, una vez que que la clase est siendo usada, no podr reducir la interfaz sin causar problemas al cdigo cliente. Si necesita aadir ms funciones, est bien; eso no molesta, nicamente obliga a recompilar. Pero incluso si los nuevos mtodos reemplazan las funcionalidad de los antiguos, deje tranquila la interfaz existente (puede combinar la funcionalidad de la implementacin subyacente si lo desea. Si necesita expandir la interfaz de un mtodo existente aadiendo ms argumentos, deje los argumentos existentes en el orden actual, y ponga valores por defecto a todos los argumentos nuevos; de este modo no perturbar ninguna de las llamadas antiguas a esa funcin. 24. Lea sus clases en voz alta para estar seguro que que suenan lgicas, reriendose a las relacin entre una clase base y una clase derivada com es-un y a los objetos miembro como tiene-un. 25. Cuando tenga que decidir entre herencia y composicin, pregunte si necesita hacer upcast al tipo base. Si la respuesta es no, elija composicin (objetos miembro) en lugar de herencia. Esto puede eliminar la necesidad de herencia mltiple. Si hereda, los usuarios pensarn FIXME:they are supposed to upcast. 26. A veces, se necesita heredar para acceder a miembros protegidos de una clase base. Esto puede conducir a una necesidad de herencia mltiple. Si no necesita hacer upcast, primero derive una nueva clase para efectuar el acceso protegido. Entonces haga que la nueva clase sea un objeto miembro dentro de cualquier clase que necesite usarla, el lugar de heredar. 27. Tpicamente, una clase base se usar principalmente para crear una interface a las clases que hereden de ella. De ese modo, cuando cree una clase base, haga que por defecto los mtodos sean virtuales puros. El destructor puede ser tambin virtual puro (para forzar que los derivadas tengan que anularlo explicitamente), pero recuerde poner al destructor un cuerpo, porque todos destructores de la jerarqua se ejecutan siempre. 28. Cuando pone un mtodo virtual puro en una clase, haga que todos los mtodos de la clase sean tambin viruales, y ponga un constructor virtual. Esta propuesta evita sorpresas en el comportamiento de la interfaz. Empiece a quitar la palabra virtual slo cuando est intentando optimizar y su perlador haya apuntado en esta direccin. 29. Use atributos para variaciones en los valores y mtodos virtuales para variaciones en el comportamiento. Es decir, si encuentra una clase que usa atributos estticos con mtodos que cambian de comportamiento basandose en esos atributos, probablemente deberia redisearla para expresar las diferencias de comportamiento con subclases y mtodos virtuales anulados. 30. If debe hacer algo no portable, cree una abstraccin para el servicio y pngalo en una clase. Este nivel extra de indireccin facilita la portabilidad mejor que si se distribuyera por todo el programa. 31. Evite la herencia mltiple. Estar a salvo de malas situaciones, especialmente cuando repare las interfaces de clases que estn fuera de su control (vea el Volumen 2). Debera ser un programador experimentado antes de poder disear con herencia mltiple. 32. No use herencia privada. Aunque, est en el lenguaje y parece que tiene una funcionalidad ocasional, ello implica ambigedades importantes cuando se combina con comprobacin dinmica de tipo. Cree un objeto miembro privado en lugar de usar herencia privada. 473
Apndice B. Directrices de Programacin 33. Si dos clases estn asociadas entre si de algn modo (como los contenedores y los iteradores). intente hacer que una de ellas sea una clase amiga anidada de la otro, tal como la Librera Estndar C++ hace con los interadores dentro de los contenedores (En la ltima parte del Captulo 16 se muestran ejemplos de esto). No solo pone de maniesto la asociacin entre las clases, tambin permite que el nombre de la clase se pueda reutilizar anidndola en otra clase. La Librera Estndar C++ lo hace deniendo un clase iterador anidada dentro de cada clase contenedor, de ese modo los contenedores tienen una interface comn. La otra razn por la que querr anidar una clase es como parte de la implementacin privada. En ese caso, el anidamiento es benecioso para ocultar la implementacin ms por la asociacin de clases y la prevencin de la contaminacin del espacio de nombres citada arriba. 34. La sobrecarga de operadores en slo azucar sintctico: una manera diferente de hacer una llamada a funcin. Is sobrecarga un operador no est haciendo que la interfaz de la clase sea ms clara o fcil de usar, no lo haga. Cree slo un operador de conversin automtica de tipo. En general, seguir las directrices y estilo indicados en el Captulo 12 cuando sobrecargue operadores. 35. No sea una vctima de la optimizacin prematura. Ese camino lleva a la locura. In particular, no se preocupe de escribir (o evitar) funciones inline, hacer algunas funciones no virtuales, anar el cdigo para hacerlo ms eciente cuando est en las primer fase de contruccin del sistema. El objetivo principal debera ser probar el diseo, a menos que el propio diseo requiera cierta eciencia. 36. Normalmente, no deje que el compilador cree los constructores, destructores o el operator= por usted. Los diseadores de clases siempre deberan decir qu debe hacer la clase exactamente y mantenerla enteramente bajo su control. Si no quiere costructor de copia u operator=, declarelos como privados. Recuerde que si crea algn constructor, el compilador un sintetizar un constructor por defecto. 37. Si su clase contiene punteros, debe crear el constructor de copia, el operator= y el destructor de la clase para que funcione adecuadamente. 38. Cuando escriba un constructor de copia para una clase derivada, recuerde llamar explcitamente al constructor de copia de la clase base (tambin cuando se usan objetos miembro). (Vea el Captulo 14.) Si no lo hace, el constructor por defecto ser invocado desde la case base (o el objeto miembro) y con mucha probabilidad no har lo que usted espera. Para invocar el constructor de copia de la clase base, psele el objeto derivado desde el que est copiando:
Derived(const Derived& d) : Base(d) { // ...
39. Cuando escriba un operador de asignacin para una clase derivada, recuerde llamar explcitamente al operador de asignacin de la clase base. (Vea el Captulo 14.) SI no lo hace, no ocurrir nada (lo mismo es aplicable a los objetos miembro). Para invocar el operador de asignacin de la clase base, use el nombre de la clase base y el operador de resolucin de mbito:
Derived& operator=(const Derived& d) { Base::operator=(d);
40. Si necesita minimizar las recompilaciones durante el desarrollo de un proyecto largo, use FIXME: demostrada en el Captulo 5, y eliminela solo si la eciencia en tiempo de ejecucin es un problema. 41. Evite el preprocesador. Use siempre const para substitucin de valores e inlines para las machos. 42. Mantenga los mbitos tan pequeos como sea posible de modo que la visibilidad y el tiempo de vidad de los objetos sea lo ms pequeo posible. Esto reduce el peligro de usar un objeto en el contexto equivocado y ello supone un bug dicil de encontrar. Por ejemplo, suponga que tiene un contenedor y un trozo de cdigo que itera sobre l. Si copia el cdigo para usarlo otro contenedor, puede que accidentalmente acabe usando el tamao del primer contenedor como el lmite superior del nuevo. Pero, si el primer contendor estuviese fuera del mbito, podra detectar el error en tiempo de compilacin. 43. Evite las variables globales. Esfuercese en pones los datos dentro de clases. En ms probable que aparezcan funciones globales de forma natural que variables globales, aunque puede que despus descubra que una funcin global puede encajar como mtodo esttico de una clase. 474
44. Si necesita declara una clase o funcin de una librera, hgalo siempre incluyendo su chero de cabecera. Por ejemplo, si quiere crear una funcin para escribir en un ostream, no declare nunca el ostream por usted mismo, usando una especicacin de tipo incompleta como esta:
class ostream;
Este enfoque hace que su cdigo sea vulnerabla a cambios en la representacin. (Por ejmplo, ostream podras ser en realidad un typedef.) En lugar de lo anterior, use siempre el cheor de cabecera:
#include <iostream>
Cuando cree sus propias clases, si una librera es grande, proporciones a sus usuarios una versin abreviada del chero de cabecera con especicaciones de tipo incompletas (es decir, declaraciones de los nombres de las clases) para los casos en que ellos puedan necesitar usar nicamente punteros. (eso puede acelerar las compilaciones.) 45. Cuando elija el tipo de retorno de una operador sobrecargado, considere que ocurrir if se encadenan expresiones. Retorne una copia o referencia al valor (return *this) de modo que se pueda usar e una expresin encadenada (A = B = C). Cuando dena el operator=, recuerde que x=x. 46. Cuando escriba una funcin, pase los argumentos por referencia constante como primera eleccin. Siempre que no necesite modicar el objeto que est pasando, esta es la mejor prctica porque es tan simple como si lo parasa por valor pero sin pagar el alto precio de construir y destruir un objeto local, que es lo que ocurre cuando se pasa por valor. Normalmente no se querr preocupar demasiado de las cuestiones de eciencia cuando est diseando y contruyendo su sistema, pero este hbito es una ganancia segura. 47. Tenga cuidado con los temporarios. Cuando est optimizando, busque creaciones de temporarios, especialmente con sobrecarga de operadores. Si sus constructores y destructores son complicados, el coste de la creaci y destruccin de temporarios puede ser muy alto. Cuando devuelva un valor en una funcin, intente siempre contruir el objeto en el sitio (in place) con una llamada al constructor en la sentencia de retorno:
return MyType(i, j);
mejor que
MyType x(i, j); return x;
La primera sentencia return (tambin llamada optimizacin de valor de retorno) evita una llamada al constructor de copia y al destructor. 48. Cuando escriba constructores, considere las excepciones. En el mejor caso, el constructor no har nada que eleve un excepcin. En ese escenario, la clas ser compuesta y heredar solo de clases robustas, de modo que ellas se limpiarn automticamente si se eleva una excepcin. Si requiere punteros, usted es responsable de capturar sus propias excepciones y de liberar los recursos antes de elevar una excepcin en su constructor. Si un contructor tiene que fallar, la accin apropiada es elevar una excepcin. 49. En los constructores, haga lo mnimo necesario. No solo producir una sobrecarga menor al crear objetos (muchas de las cuales pueden quedar fuera del control del programador), adems la probabilidad de que eleven excepciones o causen problemas ser menor. 50. La responsabilidad del destructor es la de liberar los recursos solicitados durante la vida del objeto, no slo durante la construccin. 51. Utilice jerarquas de excepciones, preferiblemente derivadas de la jerarqua de excepcin estndar de C++ y anidelas como clases pblicas con la clase que eleva la excepcin. La persona que captue las excepcines puede capturar los tipos especcos de excepciones, seguida del tipo base. Si aade una nueva excepcin derivada, el cdigo de cliente anterior seguir capturando la excepcin por medio del tipo base. 475
Apndice B. Directrices de Programacin 52. Eleve las excepciones por valor y capturelas por referencia. Deje que el mecanismo de gestin de excepciones haga la gestin de memoria. Si eleva punteros como objetos en la excepcin que han sido creados en el montculo, el que capture la excepcin debe saber como liberar la excepcin, lo cual implica un acoplamiento perjudicial. Si captura las excepciones por valor, causar que se creen temporarios; peor, las partes derivadas de sus objetos-excepcin se pueden partir al hacer upcasting por valor. 53. No escriba sus propias clases plantilla a menos que debe. Mire primero en la Librera Estndar de C++, despus en libreras de propsito especco. Adquiera habilidad en su uso y conseguir incrementar mucho su productividad. 54. Cuando cree plantillas, escriba cdigo que no dependa del tipo y ponga ese cdigo en una clase base noplantilla para evitar que el cdigo aumente de tamao sin necesidad. Por medio de herencia o composicin, puede crear plantillas en las que el volumen de cdigo que contienen es dependiente del tipo y por tanto esencial. 55. No use las funciones de <stdio>, como por ejemplo printf(). Aprenda a usar iostreams en su lugar; son FIXME:type-safe y type-extensible, y mucho ms potentes. El esfuerzo se ver recompensado con regularidad. En general, use siempre libreras C++ antes que libreras C. 56. Evite los tipos predenidos de C. El soporte de C++ es por compatibilidad con C, pero son tipos mucho menos robustos que las clases C++, de modo que pueden complicar la depuracin. 57. Siempre que use tipos predenidos para variables globales o automticas, no los dena hasta que pueda inicializarlos. Dena una variable por lnea. Cuando dena punteros, ponga el * al lado del nombre del tipo. Puede hacerlo de forma segura si dene una variable por lnea. Este estilo suele resultar menos confuso para el lector. 58. Garantize que tiene lugar la inicializacin en todos los aspectos de su programa. Inicialice todos los atributos en la lista de inicializacin del constructor, incluso para los tipo predenidos (usando los pseudoconstructores). Usar la lista de inicializacin del constructor es normalmente ms eciente cuando se inicializan subobjetos; si no se hace se invocar el constructor por defecto, y acabar llamando a otros mtodos (probablemnte el operator=) para conseguir la inicializacin que desea. 59. No use la forma MyType a = b; para denir un objeto. Esta es una de la mayores fuentes de confusin porque llama a un contructor en lugar de al operator=. Por motivos de claridad, sea especco y use mejor la forma MyType a(b);. Los resultados son idnticos, pero el lector no se podr confundir. 60. Use los moldes explcitos descritos en el Captulo 3. Un molde reemplaza el sistema normal de tipado y es un punto de error. Como los moldes explcitos separan los un-molde-lo hace-todo de C en clases de moldes bien-marcados, cualquiera que depure o mantenga el cdigo podr encontrar fcilmente todo los sitios en los que es ms probable que sucedan errores lgicos. 61. Para que un programa sea robusto, cada componente debe ser robusto. Use todas las herramientas que proporciona C++: control de acceso, excepciones, constantes, comprobacin de tipos, etc en cada clase que cree. De ese modo podr pasar de una forma segura al siguiente nivel de abstraccin cuando construya su sistema. 62. Use las constantes con correccin. Esto permite que el compilador advierta de errores que de otro modo seran sutiles y difciles de encontrar. Esta prctica requiere de cierta disciplina y se debe usar de modo consistente en todas sus clases, pero merece la pena. 63. Use la comprobacin de tipos del compilador en su benecio. Haga todas las compilaciones con todos los avisos habilitados y arregle el cdigo para eliminar todas las advertencias. Escriba cdigo que utilice los errores y advertencias de compilacin (por ejemplo, no use listas de argumentos variables, que eliminar todas los comprobaciones de tipos). Use assert() para depurar, pero use excepciones para los errores de ejecucin. 64. Son preferibles los errores de compilacin que los de ejecucin. Intente manejar un error tan cerca del punto donde ocurre como sea posible. Es mejor tratar el error en ese punto que elevar una excepcin. Capture cualqueir excepcin en el manejador ms cercano que tenga suciente informacin para tratarla. Haga lo que pueda con la excepcin en el nivel actual; si no puede resolver el problema, relance la excepcin. (Vea el Volumen 2 si necesita ms detalles.) 476
65. Si est usando las especicaciones de excepcin (vea el Volumen 2 de este libro, disponible en www.BruceEckel.com, para aprender sobre manejo de excepciones), instale su propia funcin unexpected() usando set_unexpected(). Su unexpected() debera registrar el error y relanzar la excepcin actual. De ese modo, si una funcin existente es reemplazada y eleva excepciones, dispondr de un registro de FIXME:culprint y podr modicar el cdigo que la invoca para manejar la excepcin. 66. Cree un terminate() denida por el usuario (indicando un error del programador) para registrar el error que caus la excepcin, despus libere los recursos del sistema, y termine el programa. 67. Si un destructor llama a cualquier funcin, esas funciones podran elevar excepciones. Un destructor no puede elevar una excepcin (eso podra ocasionar una llamada a terminate(), lo que indica un error de programacin), as que cualquier destructor que llame a otras funciones debe capturar y tratar sus propias excepciones. 68. No decore los nombres de sus atributos privados (poniendo guiones bajos, notacin hngara, etc.), a menos que tenga un montn de valores globales ya existentes; en cualquier otro caso, deje que las clases y los espacios de nombres denan el mbito de los nombres por usted. 69. Ponga atencin a la sobrecarga. Una funcin no debera ejecutar cdigo condicionalmente basandose en el valor de un argumento, sea por defecto o no. En su lugar, debera crear dos o ms mtodos sobrecargados. 70. Oculte sus punteros dentro de clases contenedor. Dejelos fuera slo cuando vaya a realizar operaciones con ellos. Los punteros ha sido siempre la mayor fuente de errores. Cuando use new, intente colocar el puntero resultante en un contenedor. Es preferible que un contenedor posea sus punteros y sea responsable de la limpieza. Incluso mejor, envuelva un puntero dentro de una clase; si an as quiere que parezca un puntero, sobrecargue operator-> y operator*. Si necesita tener un puntero normal, inicialicelo siempre, preferiblemente con la direccin de un objeto, o cero si es necesario. Asignele un cero cuando le libere para evitar liberaciones mltiples. 71. No sobrecargue los new y delete globales. Hgalo siempre en cada clase. Sobrecargar las versiones globales affecta la proyecto completo, algo que slo los creadores del proyecto debera controlar. Cuando sobrecargue new y delete en las clases, no asume que conoce el tamao del objeto; alguien puede heredar de esa clase. Use el argumento proporcionado. Si hace algo especial, considere el efecto que podra tener en las clases derivadas. 72. Evite el troceado de objetos. Prcticamente nunca tiene sentido hacer upcast de un objeto por valor. Para evitar el upcast por valor, use mtodos virtuales puros en su clase base. 73. A veces la agregacin simple resuelve el problema. Un FIXME:sistema conforme al pasajero en una lnea area consta en elementos desconectados: asiento, aire acondicionado, video, etc., y todava necesita crear muchos ms en un avin. Debe crear miembros privados y construir una nueva interfaz completa? No - en este caso, los componentes tambin son parte de la interfaz pblica, as que deberan ser objetos miembros pblicos. Esos objetos tienen sus propias implementaciones privadas, que continan seguras. Sea consciente de que la agregacin simple no es una solucin usan a menudo, pero que puede ocurrir.
477
C: Lecturas recomendadas
C.1. Sobre C
Thinking in C: Foundations for Java & C++, por Chuck Allison (un seminario en CDROM de MindView, Inc. , 2000, incluido al nal de este libro y disponible tambin en www.BruceEckel.com). Se trata de un curso que incluye lecciones y transparencias sobre los conceptos bsicos del lenguaje C para preparar al lector a aprender Java o C++. No es un curso exhaustivo sobre C; slo contiene lo necesario para cambiarse a esos otros lenguajes. Unas secciones adicionales sobre esos lenguajes concretos introducen al aspirante a programador en C++ o en Java, a sus caractersticas. Requisitos previos: alguna experiencia con un lenguaje de alto nivel, como Pascal, BASIC, Fortran, o LISP (sera posible avanzar por el CD sin ese bagaje, pero el curso no est pensado para servir de introduccin bsica a la programacin).
Apndice C. Lecturas recomendadas llamado a sustituirlo. Puede saber ms acerca de ese libro y conseguir el cdigo fuente en www.BruceEckel.com. Thinking in C++, 1 edition (Prentice-Hall 1995). Black Belt C++, the Masters Collection, Bruce Eckel, editor (M&T Books 1994).Agotado. Est constituido por una serie de captulos escritos por personas de prestigio sobre la base de sus presentaciones en el coloquio sobre C++ durante la Conferencia sobre Desarrollo de Software que yo presid. La portada del libro me llev a ejercer desde entonces ms control sobre el diseo de las portadas. Thinking in Java, 2 edicin (Prentice-Hall, 2000). La primera edicin de ese libro gan el Premio a la Productividad del Software Development Magazine y tambin el Premio del Editor 1999 del Java Developer_s Journal. Se puede descargar desde www.BruceEckel.com.
480
C.4. Sobre Anlisis y Diseo es bueno" (independientemente de su nivel de experiencia real con l) as que podra conseguir que lo adopten. Pienso que ese libro debera ser el buque insignia del UML, y es el que se debe de leer despus del UML Distilled de Fowler en cuanto se desee tener ms nivel de detalle. Antes de elegir mtodo alguno, es til enriquecer su perspectiva travs de los que no estn intentando vender ninguno. Es fcil adoptar un mtodo sin entender realmente lo que se desea conseguir con l o lo que puede hacer por uno. otras personas lo estn usando, lo cual parece una buena razn. Sin embargo, los humanos tienen un extrao perl psicolgico: si quieren creer que algo va a solucionar sus problemas, lo van a probar. (Eso se llama experimentacin, que es una cosa buena) Pero si eso no les resuelve nada, redoblarn sus esfuerzos y empezarn a anunciar por todo lo alto su fabuloso descubrimiento. (Eso es negacin de la realidad, que no es bueno) La idea parece consistir en que si usted consigue meter a ms gente en el mismo barco, no se sentir solo, incluso si no va a ninguna parte (o se hunde). No estoy insinuando que todas las metodologas no llevan a ningn lado, pero hay que estar armado hasta los dientes con herramientas mentales que ayuden a seguir en el modo de experimentacin (Esto no funciona, vamos a probar otra cosa) y no en el de negacin (No, no es problema. Todo va maravillosamente, no necesitamos cambiar). Creo que los libros siguientes, ledos antes de elegir un mtodo, le proporcionarn esas herramientas. Software Creativity, por Robert Glass (Prentice-Hall, 1995).Ese es el mejor libro que he ledo que describa una visin de conjunto sobre el debate de las metodologas. Consta de una serie de ensayos cortos y artculos que Glass ha escrito o comprado (P.J. Plauger es uno de los que contribuyen al libro), que reejan sus numerosos aos dedicados a pensar y estudiar el tema. Son amenos y de la longitud justa para decir lo necesario; no divaga ni aburre al lector. Pero tampoco vende simplemente aire; hay centenares de referencias a otros artculos y estudios. Todos los programadores y jefes de proyecto deberan leer ese libro antes de caer en el espejismo de las metodologas. Software Runaways: Monumental Software Disasters, por Robert Glass (Prentice-Hall 1997).Lo realmente bueno de ese libro es que expone a la luz lo que nunca contamos: la cantidad de proyectos que no solo fracasan, sino que lo hacen espectacularmente. Veo que la mayora de nosotros an piensa Eso no me va a pasar a m (o Eso no volver a pasarme) y creo que eso nos desfavorece. Al tener siempre en mente que las cosas pueden salir mal, se est en mejor posicin para hacerlas ir bien. Object Lessons por Tom Love (SIGS Books, 1993). otro buen libro para tener perspectiva. Peopleware , por Tom Demarco y Timothy Lister (Dorset House, 2 edicin 1999).A pesar de que tiene elementos de desarrollo de software, ese libro trata de proyectos y equipos de trabajo en general. Pero el nfasis est puesto en las personas y sus necesidades, y no en las tecnologas. Se habla de crear un entorno en el que la gente est feliz y productiva, en lugar de decidir las reglas que deben seguir para convertirse perfectos engranajes de una mquina. Esta ltima actitud, pienso yo, es lo que ms contribuye a que los programadores sonran y digan s con la cabeza cuando un mtodo es adoptado y sigan tranquilamente haciendo lo mismo que siempre. Complexity, by M. Mitchell Waldrop (Simon & Schuster, 1992). Relata el encuentro entre un grupo de cientcos de diferentes disciplinas en Santa Fe, Nuevo Mjico, para discutir sobre problemas reales que como especialistas no podan resolver aisladamente (el mercado burstil en economa, la formacin inicial de la vida en biologa, por qu la gente se comporta de cierta manera en sociologa, etc.). Al reunir la fsica, la economa, la qumica, las matemticas, la informtica, la sociologa, y otras ciencias, se est desarrollando un enfoque multidisciplinar a esos problemas. Pero ms importante aun, una nueva forma de pensar en esos problemas extremadamente complejos est apareciendo: alejndose del determinismo matemtico y de la ilusin de poder escribir una frmula que prediga todos los comportamientos, hacia la necesidad de observar primero y buscar un patrn para despus intentar emularlo por todos los medios posibles. (El libro cuenta, por ejemplo, la aparicin de los algoritmos genticos). Ese tipo de pensamiento, creo yo, es til a medida que investigamos formas de gestionar proyectos de software cada vez ms complejos.
481
Bruce Eckel
Puede encontrar la versin actualizada de este libro e informacin adicional sobre el proyecto de traduccin en http://arco.inf-cr.uclm.es/~david.villa/pensarC++.html
ndice de contenido
1 Introduccin 1.1 1.2 1.3 Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Captulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 1.4 1.5 1.6 1.7 1.8 1.9 Soluciones de los ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 7 7 7 7 7 7 7 7
Cdigo fuente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Compiladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Estndares del lenguaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Seminarios, CD-ROMs y consultora . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sobre la portada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.10 Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
I
2
9
11 11 12 13 13 13 14 14 15 15 15 16 17 18 19 20 21 21 3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 2.4.2 2.4.3 Capturar cualquier excepcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Relanzar una excepcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Excepciones no capturadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5
Limpieza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 2.5.2 2.5.3 2.5.4 Gestin de recursos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . auto_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bloques try a nivel de funcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6 2.7
ndice de contenido 2.7.1 2.7.2 2.7.3 2.8 2.9 Mejores especicaciones de excepciones? . . . . . . . . . . . . . . . . . . . . . . . . . Especicacin de excepciones y herencia . . . . . . . . . . . . . . . . . . . . . . . . . . Cundo no usar especicaciones de excepcin . . . . . . . . . . . . . . . . . . . . . . . 23 23 24 24 26 26 26 26 26 26
Seguridad de la excepcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programar con excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.9.1 2.9.2 Cundo evitar las excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Usos tpicos de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
II
3
27
29 29 30 31 32 35 36 40 41 43 44 48 52 56 56
Buscar en cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 Busqueda inversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encontrar el primero/ltimo de un conjunto de caracteres . . . . . . . . . . . . . . . . . . Borrar caracteres de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Comparar cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cadenas y rasgos de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
III
4
Temas especiales
Herencia mltiple 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 Perspectiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia de interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herencia de implementacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Subobjetos duplicados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clases base virtuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cuestin sobre bsqueda de nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evitar la MI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Extender una interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
61 61 61 63 66 68 72 74 74 77 77
4.10 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
ndice de contenido 5 Patrones de Diseo 5.1 El Concepto de Patrn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 5.2 La composicin es preferible a la herencia . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 80 80 80 81 81 82 83 84 87 88 90 90 91 92 94 95 96 97 98
5.3
5.4
5.5
5.6
Desacoplado de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.1 5.6.2 Proxy: FIXME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . State: cambiar el comportamiento del objeto . . . . . . . . . . . . . . . . . . . . . . . .
5.10 Cadena de Responsabilidad: intentar una secuencia de estrategias . . . . . . . . . . . . . . . . . . 5.11 Factoras: encapsular la creacin de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.11.1 Factoras polimrcas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.11.2 Factoras abstractas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 5.11.3 Constructores virtuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 5.12 Builder: creacin de objetos complejos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 5.13 Observador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.13.1 El ejemplo de observador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 5.14 Despachado mltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 5.14.1 Despachado mltiple con Visitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 5.15 Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 5.16 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 6 Concurrencia 6.1 6.2 117
Motivacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Concurrencia en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 6.2.1 6.2.2 Instalacin de ZThreads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 Denicin de tareas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
6.3
Utilizacin de los hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Simplicacin con Ejecutores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 Prioridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.4
ndice de contenido 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.5 Aseguramiento de la existencia de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . 127 Acceso no apropiado a recursos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Control de acceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 Cdigo simplicado mediante guardas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 Almacenamiento local al hilo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Finalizacin de tareas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 6.5.1 6.5.2 6.5.3 6.5.4 Prevencin de coliciones en iostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 El jardn ornamental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 FIXME:Teminacin en el bloqueo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 Interrupcin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
6.6
Cooperacin entre hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 6.6.1 6.6.2 6.6.3 6.6.4 Wait y signal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 Relacin de productor/consumidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 Resolucin de problemas de hilos mediante colas . . . . . . . . . . . . . . . . . . . . . . 143 Broadcast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
1: Introduccin
1.1. Objetivos 1.2. Captulos
Parte 1: Construccin de sistemas estables Parte 2: La librera estndar de C++ Parte 3: Temas especiales
1.3. Ejercicios
1.3.1. Soluciones de los ejercicios
1.4. Cdigo fuente 1.5. Compiladores 1.6. Estndares del lenguaje 1.7. Seminarios, CD-ROMs y consultora 1.8. Errores 1.9. Sobre la portada 1.10. Agradecimientos
Part I
2: Tratamiento de excepciones
Mejorar la recuperacin de errores es una de las maneras ms potentes de incrementar la robustez de su cdigo. Una de las principales caractersticas de C++ es el tratamiento o manejo de excepciones, el cual es una manera mejor de pensar acerca de los errores y su tratamiento. Con el tratamiento de excepciones: 1. El cdigo de manejo de errores no resulta tan tedioso de escribir y no se entremezcla con su cdigo normal. Usted escribe el cdigo que desea que se ejecute, y ms tarde, en una seccin aparte, el cdigo que se encarga de los problemas. Si realiza varias llamadas a la misma funcin, el manejo de errores de esa funcin se har una sola vez, en un solo lugar. 2. Los errores no pueden ser ignorados. Si una funcin necesita enviar un mensaje de error al invocador de esa funcin, sta lanza un objeto que representa a ese error fuera de la funcin. Si el invocador no captura el error y lo trata, ste pasa al siguiente mbito abarcador, y as hasta que el error es capturado o el programa termina al no existir un manejador adecuado para ese tipo de excepcin.
11
El problema con C++ es que longjmp( ) no respeta los objetos; en particular no llama a los destructores cuando salta fuera de un mbito.[1] Puesto que las llamadas a los destructores son esenciales, esta propuesta no es vlida para C++. De hecho, el estndar de C++ aclara que saltar a un mbito con goto (pasando por alto las llamadas a los constructores), o saltar fuera de un mbito con longjmp( ) donde un objeto en la pila posee un destructor, constituye un comportamiento indenido.
12
int main() { // As youll see shortly, well want a "try block" here: f(); }
MyError es una clase normal, que en este caso acepta un char* como argumento del constructor. Usted puede usar cualquier tipo para lanzar (incluyendo los tipos predenidos), pero normalmente crear clases especial para lanzar excepciones. La palabra clave throw hace que suceda una serie de cosas relativamente mgicas. En primer lugar se crea una copia del objeto que se est lanzando y se devuelve desde la funcin que contiene la expresin throw, aun cuando ese tipo de objeto no es lo que normalmente la funcin est diseada para devolver. Un modo simplicado de pensar acerca del tratamiento de excepciones es como un mecanismo alternativo de retorno (aunque llegar a tener problemas si lleva esta analoga demasiado lejos). Tambin es posible salir de mbitos normales lanzando una excepcin. En cualquier caso se devuelve un valor y se sale de la funcin o mbito. Adems es posible lanzar tantos tipos de objetos diferentes como se quiera. Tpicamente, para cada categora de error se lanzar un tipo diferente. La idea es almacenar la informacin en el objeto y en el nombre de la clase con el n de quien est en el contexto invocador pueda averiguar lo que hacer con esa excepcin.
//: C01:Nonlocal2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Illustrates exceptions. #include <iostream> using namespace std; class Rainbow { public:
13
2.3.3.
2.4.
//: C01:Autoexcp.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // No matching conversions. #include <iostream> using namespace std; class Except1 {}; class Except2 { public: Except2(const Except1&) {} }; void f() { throw Except1(); } int main() { try { f(); } catch(Except2&) cout << "inside } catch(Except1&) cout << "inside } }
//: C01:Basexcpt.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net.
14
2.4.
// Exception hierarchies. #include <iostream> using namespace std; class X { public: class Trouble {}; class Small : public Trouble {}; class Big : public Trouble {}; void f() { throw Big(); } }; int main() { X x; try { x.f(); } catch(X::Trouble&) { cout << "caught Trouble" << endl; // Hidden by previous handler: } catch(X::Small&) { cout << "caught Small Trouble" << endl; } catch(X::Big&) { cout << "caught Big Trouble" << endl; } }
15
2.5. Limpieza
//: C01:Cleanup.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Exceptions clean up complete objects only. #include <iostream> using namespace std; class Trace { static int counter; int objid; public: Trace() { objid = counter++; cout << "constructing Trace #" << objid << endl; if(objid == 3) throw 3; } ~Trace() { cout << "destructing Trace #" << objid << endl; } }; int Trace::counter = 0; int main() { try { Trace n1; // Throws exception: Trace array[5]; Trace n2; // Wont get here. } catch(int i) { cout << "caught " << i << endl; }
16
2.5. Limpieza
}
constructing Trace #0 constructing Trace #1 constructing Trace #2 constructing Trace #3 destructing Trace #2 destructing Trace #1 destructing Trace #0 caught 3
17
2.5.2.
//: C01:Wrapped.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Safe, atomic pointers. #include <iostream> #include <cstddef> using namespace std; // Simplified. Yours may have other arguments. template<class T, int sz = 1> class PWrap { T* ptr; public: class RangeError {}; // Exception class PWrap() { ptr = new T[sz]; cout << "PWrap constructor" << endl; } ~PWrap() { delete[] ptr; cout << "PWrap destructor" << endl; } T& operator[](int i) throw(RangeError) { if(i >= 0 && i < sz) return ptr[i]; throw RangeError(); } }; class Cat { public: Cat() { cout << "Cat()" << endl; } ~Cat() { cout << "~Cat()" << endl; } void g() {} }; class Dog { public: void* operator new[](size_t) { cout << "Allocating a Dog" << endl; throw 47; } void operator delete[](void* p) { cout << "Deallocating a Dog" << endl;
18
2.5. Limpieza
::operator delete[](p); } }; class UseResources { PWrap<Cat, 3> cats; PWrap<Dog> dog; public: UseResources() { cout << "UseResources()" << endl; } ~UseResources() { cout << "~UseResources()" << endl; } void f() { cats[1].g(); } }; int main() { try { UseResources ur; } catch(int) { cout << "inside handler" << endl; } catch(...) { cout << "inside catch(...)" << endl; } }
Cat() Cat() Cat() PWrap constructor allocating a Dog ~Cat() ~Cat() ~Cat() PWrap destructor inside handler
2.5.3. auto_ptr
//: C01:Auto_ptr.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Illustrates the RAII nature of auto_ptr. #include <memory> #include <iostream> #include <cstddef> using namespace std; class TraceHeap { int i; public: static void* operator new(size_t siz) { void* p = ::operator new(siz); cout << "Allocating TraceHeap object on the heap " << "at address " << p << endl; return p; } static void operator delete(void* p) { cout << "Deleting TraceHeap object at address " << p << endl;
19
//: C01:FunctionTryBlock.cpp {-bor} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net.
20
void f();
//: C01:Unexpected.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net.
21
//: C01:BadException.cpp {-bor} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <exception> // For std::bad_exception #include <iostream> #include <cstdio> using namespace std; // Exception classes: class A {}; class B {}; // terminate() handler void my_thandler() { cout << "terminate called" << endl; exit(0); } // unexpected() handlers void my_uhandler1() { throw A(); } void my_uhandler2() { throw; }
22
23
template<class T> T stack<T>::pop() { if(count == 0) throw logic_error("stack underflow"); else return data[--count]; }
//: C01:SafeAssign.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // An Exception-safe operator=. #include <iostream> #include <new> // For std::bad_alloc #include <cstring> #include <cstddef> using namespace std; // A class that has two pointer members using the heap class HasPointers { // A Handle class to hold the data struct MyData { const char* theString; const int* theInts; size_t numInts; MyData(const char* pString, const int* pInts, size_t nInts) : theString(pString), theInts(pInts), numInts(nInts) {} } *theData; // The handle // Clone and cleanup functions: static MyData* clone(const char* otherString, const int* otherInts, size_t nInts) {
24
25
2.10. Sobrecarga
//: C01:HasDestructor.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. class HasDestructor { public: ~HasDestructor() {} }; void g(); // For all we know, g may throw. void f() { HasDestructor h; g(); }
26
Part II
27
3.1. Qu es un string?
En C, una cadena es simplemente un arreglo de caracteres que siempre incluye un 0 binario (frecuentemente llamado terminador nulo) como elemento nal del arreglo. Existen diferencias signicativas entre los string de C++ y sus progenitoras en C. Primero, y ms importante, los string de C++ esconden la implementacin fsica de la secuencia de caracteres que contiene. No debe preocuparse de las dimensiones del arreglo o del terminador nulo. Un string tambin contiene cierta informacin para uso interno sobre el tamao y la localizacin en memoria de los datos. Especcamente, un objeto string de C++ conoce su localizacin en memoria, su contenido, su longitud en caracteres, y la cantidad de caracteres que puede crecer antes de que el objeto string deba redimensionar su buffer interno de datos. Las string de C++, por tanto, reducen enormemente las probabilidades de cometer uno de los tres errores de programacin en C ms comunes y destructivos: sobrescribir los lmites del arreglo, intentar acceder a un arreglo no inicializado o con valores de puntero incorrectos, y dejar punteros colgando despus de que el arreglo deje de ocupar el espacio que estaba ocupando. La implementacin exacta del esquema en memoria para una clase string no esta denida en el estndar C++. Esta arquitectura esta pensada para ser sucientemente exible para permitir diferentes implementaciones de los
1
Algunos materiales de este captulo fueron creados originalmente por Nancy Nicolaisen
29
Captulo 3. Las cadenas a fondo fabricantes de compiladores, garantizando igualmente un comportamiento predecible por los usuarios. En particular, las condiciones exactas de cmo situar el almacenamiento para alojar los datos para un objeto string no estn denidas. FIXME: Las reglas de alojamiento de un string fueron formuladas para permitir, pero no requerir, una implementacin con referencias mltiples, pero dependiendo de la implementacin usar referencias mltiples sin variar la semntica. Por decirlo de otra manera, en C, todos los arreglos de char ocupan una nica regin fsica de memoria. En C++, los objetos string individuales pueden o no ocupar regiones fsicas nicas de memoria, pero si su conjunto de referencias evita almacenar copias duplicadas de datos, los objetos individuales deben parecer y actuar como si tuvieran sus propias regiones nicas de almacenamiento.
//: C03:StringStorage.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef STRINGSTORAGE_H #define STRINGSTORAGE_H #include <iostream> #include <string> #include "../TestSuite/Test.h" using std::cout; using std::endl; using std::string; class StringStorageTest : public TestSuite::Test { public: void run() { string s1("12345"); // This may copy the first to the second or // use reference counting to simulate a copy: string s2 = s1; test_(s1 == s2); // Either way, this statement must ONLY modify s1: s1[0] = 6; cout << "s1 = " << s1 << endl; // 62345 cout << "s2 = " << s2 << endl; // 12345 test_(s1 != s2); } }; #endif // STRINGSTORAGE_H
Decimos que cuando una implementacin solo hace una sola copia al modicar el string usa una estrategia de copiar al escribir. Esta aproximacin ahorra tiempo y espacio cuando usamos string como parmetros por valor o en otras situaciones de solo lectura. El uso de referencias mltiples en la implementacin de una librera debera ser transparente al usuario de la clase string. Desgraciadamente, esto no es siempre el caso. En programas multihilo, es prcticamente imposible usar implementaciones con mltiples referencias de forma segura[32].2
30
3.2. Operaciones con cadenas La segunda trampa inherente a las herramientas para cadenas del estndar de C es que todas ellas explcitamente confan en la asuncin de que cada cadena incluye un terminador nulo. Si por confusin o error el terminador nulo es omitido o sobrescrito, poco se puede hacer para impedir que las funciones de cadena de C manipulen la memoria ms all de los lmites del espacio de alojamiento, a veces con resultados desastrosos. C++ aporta una vasta mejora en cuanto a conveniencia y seguridad de los objetos string. Para los propsitos de las actuales operaciones de manipulacin, existe el mismo nmero de funciones que la librera de C, pero gracias a la sobrecarga, la funcionalidad es mucho mayor. Adems, con una nomenclatura ms sensata y un acertado uso de los argumentos por defecto, estas caractersticas se combinan para hacer de la clase string mucho ms fcil de usar que la biblioteca de funciones de cadena de C.
31
Este ejemplo demuestra que aunque puede ignorar con seguridad muchas de las responsabilidades de reserva y gestin de la memoria que tus string ocupan, C++ provee a los string con varias herramientas para monitorizar y gestionar su tamao. Ntese la facilidad con la que hemos cambiado el tamao de la memoria reservada para los string. La funcin size() retorna el numero de caracteres actualmente almacenados en el string y es idntico a la funcin miembro lenght(). La funcin capacity() retorna el tamao de la memoria subyacente actual, es decir, el nmero de caracteres que el string puede almacenar sin tener que reservar ms memoria. La funcin reserve() es una optimizacin del mecanismo que indica su intencin de especicar cierta cantidad de memoria para un futuro uso; capacity() siempre retorna un valor al menos tan largo como la ultima llamada a reserve(). La funcin resize() aade espacio si el nuevo tamao es mayor que el tamao actual del string; sino trunca el string. (Una sobrecarga de resize() puede especicar una adicin diferente de caracteres). La manera exacta en que las funciones miembro de string reservan espacio para sus datos depende de la implementacin de la librera. Cuando testeamos una implementacin con el ejemplo anterior, parece que se hacia una reserva de una palabra de memoria (esto es, un entero) dejando un byte en blanco entre cada una de ellas. Los arquitectos de la clase string se esforzaron para poder mezclar el uso de las cadenas de caracteres de C y los objetos string, por lo que es probable por lo que se puede observar en StrSize.cpp, en esta implementacin en particular, el byte est aadido para acomodar fcilmente la insercin de un terminador nulo.
32
3.2. Operaciones con cadenas Tag es insertada en s (notese que la insercin ocurre antes de que el valor indicando el punto de insercin y de que el espacio extra haya sido aadido despues de Tag), y entonces es encontrada y reemplazada. Debera cerciorarse de que ha encontrado algo antes de realizar el replace(). En los ejemplos anteriores se reemplaza con un char*, pero existe una versin sobrecargada que reemplaza con un string. Aqui hay un ejempl ms completo de demostracin de replace():
//: C03:Replace.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cassert> #include <cstddef> // For size_t #include <string> using namespace std; void replaceChars(string& modifyMe, const string& findMe, const string& newChars) { // Look in modifyMe for the "find string" // starting at position 0: size_t i = modifyMe.find(findMe, 0); // Did we find the string to replace? if(i != string::npos) // Replace the find string with newChars: modifyMe.replace(i, findMe.size(), newChars); } int main() { string bigNews = "I thought I saw Elvis in a UFO. " "I have been working too hard."; string replacement("wig"); string findMe("UFO"); // Find "UFO" in bigNews and overwrite it: replaceChars(bigNews, findMe, replacement); assert(bigNews == "I thought I saw Elvis in a " "wig. I have been working too hard."); }
Si replace() no encuentra la cadena buscada, retorna un string::npos. El dato miembro npos es una constante estatica de la clase string que representa una posicin de carcter que no existe[33]. 3 A diferencia de insert(), replace() no aumentar el espacio de alamcenamiento de string si copia nuevos caracteres en el medio de una serie de elementos de array existentes. Sin embargo, s que cerecer su espacio si es necesario, por ejemplo, cuando hace un "reemplazamiento" que pueda expandir el string ms all del nal de la memoria reservada actual. Aqu un ejemplo:
//: C03:ReplaceAndGrow.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cassert> #include <string> using namespace std; int main() { string bigNews("I have been working the grave."); string replacement("yard shift.");
3 Es una abrviacin de "no position", y su valor ms alto puede ser representado por el ubicador de string size_type (std::size_t por defecto).
33
La llamada a replace() empieza "reemplazando" ms all del nal del array existente, que es equivalente a la operacin append(). Ntese que en este ejemplo replace() expande el array coherentemente. Puede que haya estado buscando a travs del captulo; intentando hacer algo relativamente fcil como reemplazar todas las ocurrencias de un carcter con diferentes caracteres. Al buscar el material previo sobre reemplazar, puede que haya encontrado la respuesta, pero entonces ha empezaro viendo grupos de caracteres y contadores y otras cosas que parecen un poco demasiado complejas. No tiene string una manera para reemplazar un carcter con otro simplemente? Puede escribir fcilmente cada funcin usando las funciones miembro find() y replace() como se muestra acontinuacion.
//: C03:ReplaceAll.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef REPLACEALL_H #define REPLACEALL_H #include <string> std::string& replaceAll(std::string& context, const std::string& from, const std::string& to); #endif // REPLACEALL_H
//: C03:ReplaceAll.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cstddef> #include "ReplaceAll.h" using namespace std; string& replaceAll(string& context, const string& from, const string& to) { size_t lookHere = 0; size_t foundHere; while((foundHere = context.find(from, lookHere)) != string::npos) { context.replace(foundHere, from.size(), to); lookHere = foundHere + to.size(); } return context; }
La versin de find() usada aqu toma como segundo argumento la posicin donde empezar a buscar y retorna string::npos si no lo encuentra. Es importante avanzar en la posicin contenida por la variable lookHere pasada como subcadena, en caso de que from es una subcadena de to. El siguiente programa comprueba la funcion replaceAll(): 34
//: C03:ReplaceAllTest.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ReplaceAll #include <cassert> #include <iostream> #include <string> #include "ReplaceAll.h" using namespace std; int main() { string text = "a man, a plan, a canal, Panama"; replaceAll(text, "an", "XXX"); assert(text == "a mXXX, a plXXX, a cXXXal, PXXXama"); }
Como puede comprobar, la clase string por ella sola no resuelve todos los posibles problemas. Muchas soluciones se han dejado en los algoritmos de la librera estndar4 por que la clase string puede parece justamente como una secuencia STL(gracias a los iteradores descritos antes). Todos los algoritmos genricos funcionan en un "rango" de elementos dentro de un contenedor. Generalmente este rango es justamente desde el principio del contenedor hasta el nal. Un objeto string se parece a un contenedor de caracteres: para obtener el principio de este rango use string::begin(), y para obtener el nal del rango use string::end(). El siguiente ejemplomuestra el uso del algoritmo replace() para reemplazar todas las instancias de un determinado carcter "X" con "Y"
//: C03:StringCharReplace.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <algorithm> #include <cassert> #include <string> using namespace std; int main() { string s("aaaXaaaXXaaXXXaXXXXaaa"); replace(s.begin(), s.end(), X, Y); assert(s == "aaaYaaaYYaaYYYaYYYYaaa"); }
Ntese que esta funcin replace() no es llamada como funcin miembro de string. Adems, a diferencia de la funcin string::replace(), que solo realiza un reemplazo, el algoritmo replace() reemplaza todas las instancias de un carcter con otro. El algoritmo replace() solo funciona con objetos individuales (en este caso, objetos char) y no reemplazar arreglos constantes o objetos string. Desde que un string se copmporta como una secuencia STL, un conjunto de algoritmos pueden serle aplicados, que resolvern otros problemas que las funciones miembro de string no resuelven.
35
Captulo 3. Las cadenas a fondo y operator+=. Estos operadores hacen combinaciones de cadenas sintacticamente parecidas a la suma de datos numricos:
//: C03:AddStrings.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <string> #include <cassert> using namespace std; int main() { string s1("This "); string s2("That "); string s3("The other "); // operator+ concatenates strings s1 = s1 + s2; assert(s1 == "This That "); // Another way to concatenates strings s1 += s3; assert(s1 == "This That The other "); // You can index the string on the right s1 += s3 + s3[4] + "ooh lala"; assert(s1 == "This That The other The other oooh lala"); }
Usar los operadores operator+ y operator+= es una manera exible y conveniente de combinar los datos de las cadenas. En la parte derecha de la sentencia, puede usar casi cualquier tipo que evale a un grupo de uno o ms caracteres.
3.3. Buscar en cadenas Busca en un string, desde el nal hasta el origen, un carcter o grupo de caracteres y retorna la posicin inicial de la ocurrencia si se ha encontrado alguna. Si no encuentra ocurrencias, retorna npos. El uso ms simple de find(), busca uno o ms caracteres en un string. La versin sobrecargada de find() toma un parmetro que especica el/los carcter(es) que buscar y opcionalmente un parmetro que dice donde empezar a buscar en el string la primera ocurrencia. (Por defecto la posicin de incio es 0). Insertando la llamada a la funcin find() dentro de un bucle puede buscar fcilmente todas las ocurrencias de un carcter dado o un grupo de caracteres dentro de un string. El siguiente programa usa el mtodo del Tamiz de Eratostenes para hallar los nmeros primos menores de 50. Este mtodo empieza con el nmero 2, marca todos los subsecuentes mltiplos de 2 ya que no son primos, y repite el proceso para el siguiente candidato a primo. El constructor de sieveTest inicializa sieveChars poniendo el tamao inicial del arreglo de carcter y escribiendo el valor P para cada miembro.
//: C03:Sieve.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef SIEVE_H #define SIEVE_H #include <cmath> #include <cstddef> #include <string> #include "../TestSuite/Test.h" using std::size_t; using std::sqrt; using std::string; class SieveTest : public TestSuite::Test { string sieveChars; public: // Create a 50 char string and set each // element to P for Prime: SieveTest() : sieveChars(50, P) {} void run() { findPrimes(); testPrimes(); } bool isPrime(int p) { if(p == 0 || p == 1) return false; int root = int(sqrt(double(p))); for(int i = 2; i <= root; ++i) if(p % i == 0) return false; return true; } void findPrimes() { // By definition neither 0 nor 1 is prime. // Change these elements to "N" for Not Prime: sieveChars.replace(0, 2, "NN"); // Walk through the array: size_t sieveSize = sieveChars.size(); int root = int(sqrt(double(sieveSize))); for(int i = 2; i <= root; ++i) // Find all the multiples: for(size_t factor = 2; factor * i < sieveSize; ++factor) sieveChars[factor * i] = N; } void testPrimes() { size_t i = sieveChars.find(P); while(i != string::npos) { test_(isPrime(i++)); i = sieveChars.find(P, i);
37
//: C03:Sieve.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ../TestSuite/Test #include "Sieve.h" int main() { SieveTest t; t.run(); return t.report(); }
La funcin find() puede recorrer el string, detectando mltiples ocurrencias de un carcter o un grupo de caracteres, y find_first_not_of() encuentra otros caracteres o subcadenas. No existen funciones en la clase string para cambiar entre maysculas/minsculas en una cadena, pero puede crear esa funcin fcilmente usando la funcin de la libreria estndar de C toupper() y tolower(), que cambian los caracteres entre maysculas/minsculas de uno en uno. El ejemplo siguiente ilustra una bsqueda sensible a maysculas/minsculas.
//: C03:Find.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef FIND_H #define FIND_H #include <cctype> #include <cstddef> #include <string> #include "../TestSuite/Test.h" using std::size_t; using std::string; using std::tolower; using std::toupper; // Make an uppercase copy of s inline string upperCase(const string& s) { string upper(s); for(size_t i = 0; i < s.length(); ++i) upper[i] = toupper(upper[i]); return upper; } // Make a lowercase copy of s inline string lowerCase(const string& s) { string lower(s); for(size_t i = 0; i < s.length(); ++i)
38
//: C03:Find.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ../TestSuite/Test #include "Find.h" #include "../TestSuite/Test.h" int main() { FindTest t; t.run(); return t.report(); }
39
Tanto las funciones upperCase() como lowerCase() siguen la misma forma: hacen una copia de la cadena argumento y cambian entre maysculas/minsculas. El programa Find.cpp no es la mejor solucin para el problema para las maysculas/minsculas, por lo que lo revisitaremos cuando examinemos la comparacin entre cadenas.
40
//: C03:Rparse.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ../TestSuite/Test #include "Rparse.h" int main() { RparseTest t; t.run(); return t.report(); }
La funcin miembro de string rfind() vuelve por la cadena buscando elementos y reporta el indice del arreglo de las coincidencias de caracteres o string::npos si no tiene xito.
41
La primera prueba checkea si el string esta vaco; en ese caso, ya no se realizan ms test, y se retorna una copia. Ntese que una vez los puntos del nal son encontrados, el constructor de string construye un nuevo string desde el viejo, dndole el contador incial y la longitud. Las pruebas de una herramienta tan general den ser cuidadosas
//: C03:TrimTest.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef TRIMTEST_H #define TRIMTEST_H #include "Trim.h" #include "../TestSuite/Test.h" class TrimTest : public TestSuite::Test { enum {NTESTS = 11}; static std::string s[NTESTS]; public: void testTrim() { test_(trim(s[0]) == "abcdefghijklmnop"); test_(trim(s[1]) == "abcdefghijklmnop"); test_(trim(s[2]) == "abcdefghijklmnop"); test_(trim(s[3]) == "a"); test_(trim(s[4]) == "ab"); test_(trim(s[5]) == "abc"); test_(trim(s[6]) == "a b c"); test_(trim(s[7]) == "a b c"); test_(trim(s[8]) == "a \t b \t c"); test_(trim(s[9]) == ""); test_(trim(s[10]) == ""); } void run() { testTrim(); } }; #endif // TRIMTEST_H
//: C03:TrimTest.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "TrimTest.h" // Initialize static data std::string TrimTest::s[TrimTest::NTESTS] = { " \t abcdefghijklmnop \t ", "abcdefghijklmnop \t ", " \t abcdefghijklmnop", "a", "ab", "abc", "a b c", " \t a b c \t ", " \t a \t b \t c \t ", "\t \n \r \v \f", "" // Must also test the empty string };
42
//: C03:TrimTestMain.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ../TestSuite/Test TrimTest #include "TrimTest.h" int main() { TrimTest t; t.run(); return t.report(); }
En el arrglo de string, puede ver que los arreglos de carcter son automticamente convertidos a objetos string. Este arreglo provee casos para checkear el borrado de espacios en blanco y tabuladores en los extremos, adems de asegurar que los espacios y tabuladores no son borrados de la mitad de un string.
43
Este ejemplo borrar incluso las etiquetas HTML que se extienden a lo largo de varias lneas.5 Esto se cumple gracias a la bandera esttica inTag, que evala a cierto si el principio de una etiqueta es encontrada, pero la etiqueta de nalizacin correspondiente no es encontrada en la misma lnea. Todas la formas de erase() aparecen en la funcin stripHTMLFlags().6 La versin de getline() que usamos aqu es una funcin (global) declarada en la cabecera de string y es til porque guarda una lnea arbitrariamente larga en su argumento string. No necesita preocuparse de las dimensiones de un arreglo cuando trabaja con istream::getline(). Ntese que este programa usa la funcin replaceAll() vista antes en este captulo. En el pximo capitulo, usaremos los ujos de cadena para crear una solucin ms elegante.
44
3.3. Buscar en cadenas rango del 32 al 127 decimal. En la codicacin ASCII, el primer "carcter" en la lista es el espacio, seguido de diversas marcas de puntuacin comn, y despus las letras maysculas y minsculas. Respecto al alfabeto, esto signica que las letras cercanas al principio tienen un valor ASCII menor a aquellos ms cercanos al nal. Con estos detalles en mente, se vuelve ms fcil recordar que cuando una comparaci lxica reporta que s1 es "mayor que" s2, simplemente signica que cuando fueron comparados, el primer carcter diferente en s1 estaba atrs en el alfabeto que el carcter en la misma posicin en s2. C++ provee varias maneras de comparar cadenas, y cada una tiene ventajas. La ms simple de usar son las funciones no-miembro sobrecargadas de operador: operator==, operator!= operator>, operator<, operator>= y operator<=.
//: C03:CompStr.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef COMPSTR_H #define COMPSTR_H #include <string> #include "../TestSuite/Test.h" using std::string; class CompStrTest : public TestSuite::Test { public: void run() { // Strings to compare string s1("This"); string s2("That"); test_(s1 == s1); test_(s1 != s2); test_(s1 > s2); test_(s1 >= s2); test_(s1 >= s1); test_(s2 < s1); test_(s2 <= s1); test_(s1 <= s1); } }; #endif // COMPSTR_H
//: C03:CompStr.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ../TestSuite/Test #include "CompStr.h" int main() { CompStrTest t; t.run(); return t.report(); }
Los operadores de comaracin sobrecargados son tiles para comparar dos cadenas completas y elementos individuales de una cadena de caracteres. Ntese en el siguiente ejemplo la exibilidad de los tipos de argumento ambos lados de los operadores de comparacin. Por eciencia, la clase string provee operadores sobrecargados para la comparacin directa de objetos string, literales de cadena, y punteros a cadenas estilo C sin tener que crear objetos string temporales. 45
//: C03:Equivalence.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> #include <string> using namespace std; int main() { string s2("That"), s1("This"); // The lvalue is a quoted literal // and the rvalue is a string: if("That" == s2) cout << "A match" << endl; // The left operand is a string and the right is // a pointer to a C-style null terminated string: if(s1 != s2.c_str()) cout << "No match" << endl; }
La funcin c_str() retorna un const char* que apunta a una cadena estilo C terminada en nulo, equivalente en contenidos al objeto string. Esto se vuelve muy til cuando se quiere pasar un strin a una funcin C, como atoi() o cualquiera de las funciones denidas en la cabecera cstring. Es un error usar el valor retornado por c_str() como un argumento constante en cualquier funcin. No encontrar el operador not (!) o los operadores de comparacin lgicos (&& y ||) entre los operadore para string. (No encontrar ninguna versin sobrecargada de los operadores de bits de C: &, |, , o ~.) Los operadores de conversin no miembros sobrecargados para la clases string estn limitados a un subconjunto que tiene una aplicacin clara y no ambigua para caracteres individuales o grupos de caracteres. La funcin miembro compare() le ofrece un gran modo de comparacin ms sosticado y preciso que el conjunto de operadores nomiembro. Provee versiones sobrecargadas para comparar: Dos string completos Parte de un string con un string completo Partes de dos string
//: C03:Compare.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Demonstrates compare() and swap(). #include <cassert> #include <string> using namespace std; int main() { string first("This"); string second("That"); assert(first.compare(first) == 0); assert(second.compare(second) == 0); // Which is lexically greater? assert(first.compare(second) > 0); assert(second.compare(first) < 0); first.swap(second); assert(first.compare(second) < 0); assert(second.compare(first) > 0);
46
La funcin swap() en este ejemplo hace lo que su nombre implica: cambia el contenido del objeto por el del parmetro. Para comparar un subconjunto de caracteres en un o ambos string, aada argumentos que denen donde empezar y cuantos caracteres considerar. Por ejemplo, puede usar las siguientes versiones sobrecargadas de compare(): s1.compare(s1StartPos, s1NumberChars, s2, s2StartPos, s2NumberChars); Aqui un ejemplo:
//: C03:Compare2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Illustrate overloaded compare(). #include <cassert> #include <string> using namespace std; int main() { string first("This is a day that will live in infamy"); string second("I dont believe that this is what " "I signed up for"); // Compare "his is" in both strings: assert(first.compare(1, 7, second, 22, 7) == 0); // Compare "his is a" to "his is w": assert(first.compare(1, 9, second, 22, 9) < 0); }
Hasta ahora, en los ejemplos, hemos usado la sintaxis de indexacin de arrays estilo C para referirnos a un carcter individual en un string. C++ provee de una alternativa a la notacin s[n]: el miembro at(). Estos dos mecanismos de indexacin producen los mismos resultados si todo va bien:
//: C03:StringIndexing.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cassert> #include <string> using namespace std; int main() { string s("1234"); assert(s[1] == 2); assert(s.at(1) == 2); }
Sin embargo, existe una importante diferencia entre [ ] y at() . Cuando usted intenta referenciar el elemento de un arreglo que esta fuera de sus lmites, at() tiene la delicadeza de lanzar una excepcin, mientras que ordinariamente [ ] le dejar a su suerte.
//: C03:BadStringIndexing.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt,
47
Los programadores responsables no usarn ndices errticos, pero puede que quiera beneciarse de la comprobacin automtica de indices, usandoat() en el lugar de [ ] le da la oportunidad de recuperar diligentemente de las referencias a elementos de un arreglo que no existen. La ejecucin de sobre uno de nuestros compiladores le da la siguiente salida: "invalid string position" La funcin miembro at() lanza un objeto de clase out_of_class, que deriva nalmente de std::exception. Capturando este objeto en un manejador de excepciones, puede tomar las medidas adecuadas como recalcular el ndice incorrecto o hacer crecer el arreglo. Usar string::operator[]( ) no proporciona ningn tipo de proteccin y es tan peligroso como el procesado de arreglos de caracteres en C.[37] 7
En el Captulo 5, examinamos las plantillas con gran detalle (mucho ms que en el Captulo 16 del volmen 1). Por ahora ntese que el tipo string es creada cuando instanciamos la plantilla basic_string con char. Dentro de la declaracin plantilla basic_string< > la lnea: class traits = char_traits<charT<, nos dice que el comportamiento de la clase hecha a partir de basic_string< > es deneida por una clase basada en la plantilla char_traits< >. As, la plantilla basic_string< > produce clases orientadas a string que manipulan otros tipos que char (caracteres anchos, por ejemplo). Para hacer esto, la plantilla char_traits< > controla el contenido y el comportamiento de la ordenacin de una variedad de conjuntos
7 Por las razones de seguridad mencionadas, el C++ Standards Committee est considerando una propuesta de redenicin del string::operator[] para comportarse de manera idntica al string::at() para C++0x.
48
3.3. Buscar en cadenas de caracteres usando las funciones de comparacin eq() (equal), ne() (not equal), y lt() (less than). Las funciones de comparacin de basic_string< > conan en esto. Es por esto por lo que la clase string no incluye funciones miembro sensibles a maysculas/minsculas: eso no esta en la descripcin de su trabajo. Para cambiar la forma en que la clase string trata la comparacin de caracteres, tiene que suministrar una plantilla char_traits< > diferente ya que dene el comportamiento individual de las funciones miembro de comparacin carcteres. Puede usar esta informacin para hacer un nuevo tipo de string que ignora las maysculas/minsculas. Primero, deniremos una nueva plantilla no sensible a maysculas/minsculas de char_traits< > que hereda de una plantilla existente. Luego, sobrescribiremos slo los miembros que necesitamos cambiar para hacer la comparacin carcter por carcter. (Adems de los tres miembros de comparacin lxica mencionados antes, daremos una nueva implementacin para laspara las funciones de char_traits find() y compare()). Finalmente, haremos un typedef de una nueva clase basada en basic_string, pero usando nuestra plantilla insensible a maysculas/minsculas, ichar_traits, como segundo argumento:
//: C03:ichar_traits.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Creating your own character traits. #ifndef ICHAR_TRAITS_H #define ICHAR_TRAITS_H #include <cassert> #include <cctype> #include <cmath> #include <cstddef> #include <ostream> #include <string> using std::allocator; using std::basic_string; using std::char_traits; using std::ostream; using std::size_t; using std::string; using std::toupper; using std::tolower; struct ichar_traits : char_traits<char> { // Well only change character-by// character comparison functions static bool eq(char c1st, char c2nd) { return toupper(c1st) == toupper(c2nd); } static bool ne(char c1st, char c2nd) { return !eq(c1st, c2nd); } static bool lt(char c1st, char c2nd) { return toupper(c1st) < toupper(c2nd); } static int compare(const char* str1, const char* str2, size_t n) { for(size_t i = 0; i < n; ++i) { if(str1 == 0) return -1; else if(str2 == 0) return 1; else if(tolower(*str1) < tolower(*str2)) return -1; else if(tolower(*str1) > tolower(*str2)) return 1; assert(tolower(*str1) == tolower(*str2)); ++str1; ++str2; // Compare the other chars
49
Proporcionamos un typedef llamado istring ya que nuestra clase actuar como un string ordinario en todas sus formas, excepto que realizar todas las comparaciones sin respetar las maysculas/minsculas. Por conveniencia, damos un operador sobrecargado operator<<() para que pueda imprimir los istring. Aque hay un ejemplo:
//: C03:ICompare.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cassert> #include <iostream> #include "ichar_traits.h" using namespace std; int main() { // The same letters except for case: istring first = "tHis"; istring second = "ThIS"; cout << first << endl; cout << second << endl; assert(first.compare(second) == 0); assert(first.find(h) == 1); assert(first.find(I) == 2); assert(first.find(x) == string::npos); }
Este es solo un ejemplo de prueba. Para hacer istring completamente equivalente a un string, deberiamos haber creado las otras funciones necesarias para soportar el nuevo tipo istring. La cabecera <string> provee de un string ancho 8 gracias al siguiente typedef: typedef basic_string<wchar_t> wstring; El soporte para string ancho se revela tambien en los streams anchos (wostream en lugar de ostream, tambien denido en <iostream>) y en la especializacin de wchar_t de los char_traits en la libreria estndar le da la posibilidad de hacer una version de carcter ancho de ichar_traits
8 (N.del T.) Se reere a string amplio puesto que esta formado por caracteres anchos wchar_t que deben soportar la codicacin mas grande que soporte el compilador. Casi siempre esta codicacin es Unicode, por lo que casi siempre el ancho de wchar_t es 2 bytes
50
//: C03:iwchar_traits.h {-g++} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Creating your own wide-character traits. #ifndef IWCHAR_TRAITS_H #define IWCHAR_TRAITS_H #include <cassert> #include <cmath> #include <cstddef> #include <cwctype> #include <ostream> #include <string> using using using using using using using using std::allocator; std::basic_string; std::char_traits; std::size_t; std::towlower; std::towupper; std::wostream; std::wstring;
struct iwchar_traits : char_traits<wchar_t> { // Well only change character-by// character comparison functions static bool eq(wchar_t c1st, wchar_t c2nd) { return towupper(c1st) == towupper(c2nd); } static bool ne(wchar_t c1st, wchar_t c2nd) { return towupper(c1st) != towupper(c2nd); } static bool lt(wchar_t c1st, wchar_t c2nd) { return towupper(c1st) < towupper(c2nd); } static int compare( const wchar_t* str1, const wchar_t* str2, size_t n) { for(size_t i = 0; i < n; i++) { if(str1 == 0) return -1; else if(str2 == 0) return 1; else if(towlower(*str1) < towlower(*str2)) return -1; else if(towlower(*str1) > towlower(*str2)) return 1; assert(towlower(*str1) == towlower(*str2)); ++str1; ++str2; // Compare the other wchar_ts } return 0; } static const wchar_t* find(const wchar_t* s1, size_t n, wchar_t c) { while(n-- > 0) if(towupper(*s1) == towupper(c)) return s1; else ++s1; return 0; } }; typedef basic_string<wchar_t, iwchar_traits> iwstring;
51
inline wostream& operator<<(wostream& os, const iwstring& s) { return os << wstring(s.c_str(), s.length()); } #endif // IWCHAR_TRAITS_H
Como puede ver, esto es principalmente un ejercicio de poner w en el lugar adecuado del cdigo fuente. El programa de prueba podria ser asi:
//: C03:IWCompare.cpp {-g++} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <cassert> #include <iostream> #include "iwchar_traits.h" using namespace std; int main() { // The same letters except for case: iwstring wfirst = L"tHis"; iwstring wsecond = L"ThIS"; wcout << wfirst << endl; wcout << wsecond << endl; assert(wfirst.compare(wsecond) == 0); assert(wfirst.find(h) == 1); assert(wfirst.find(I) == 2); assert(wfirst.find(x) == wstring::npos); }
Desgraciadamente, todavia algunos compiladores siguen sin ofrecer un soporte robusto para caracteres anchos.
3.4. Una aplicacin con cadenas chero de texto9 (llammosle TICV2.txt)y ejecutando algo como la siguiente lnea de comandos: C:> extractCode TICV2.txt /TheCode Este comando lee el chero de texto TICV2.txt y escribe todos los archivos de cdigo fuente en subdirectorios bajo el denido /TheCode. El arbol de directorios se mostrar como sigue: TheCode/ C0B/ C01/ C02/ C03/ C04/ C05/ C06/ C07/ C08/ C09/ C10/ C11/ TestSuite/ Los cheros de cdigo fuente que contienen los ejemplos de cada captulo estarn en el correspondiente directorio. Aqu est el programa:
//: C03:ExtractCode.cpp {-edg} {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Extracts code from text. #include <cassert> #include <cstddef> #include <cstdio> #include <cstdlib> #include <fstream> #include <iostream> #include <string> using namespace std; // Legacy non-standard C header for mkdir() #if defined(__GNUC__) || defined(__MWERKS__) #include <sys/stat.h> #elif defined(__BORLANDC__) || defined(_MSC_VER) \ || defined(__DMC__) #include <direct.h> #else #error Compiler not supported #endif // Check to see if directory exists // by attempting to open a new file // for output within it. bool exists(string fname) { size_t len = fname.length(); if(fname[len-1] != / && fname[len-1] != \\) fname.append("/"); fname.append("000.tmp"); ofstream outf(fname.c_str()); bool existFlag = outf; if(outf) { outf.close(); remove(fname.c_str()); } return existFlag; } int main(int argc, char* argv[]) { // See if input file name provided if(argc == 1) { cerr << "usage: extractCode file [dir]" << endl; exit(EXIT_FAILURE); }
9 Est alerta porque algunas versiones de Microsoft Word que substituyen erroneamente los caracteres con comilla simple con un carcter ASCII cuando salva el documento como texto, causan un error de compilacin. No tenemos idea de porqu pasa esto. Simplemente reemplace el carcter manualmente con un apstrofe.
53
54
Primero observar algunas directivas de compilacin condicionales. La funcin mkdir(), que crea un directorio en el sistema de cheros, se dene por el estndar POSIX10 en la cabecera (<direct.h>). La respectiva signatura de mkdir() tambin diere: POSIX especica dos argumentos, las viejas versiones slo uno. Por esta razn, existe ms de una directiva de compilacin condicional despus en el programa para elegir la llamada correcta a mkdir(). Normalmente no usamos compilaciones condicionales en los ejemplos de este libro, pero en este programa en particular es demasiado til para no poner un poco de trabajo extra dentro, ya que puede usarse para extraer todo el cdigo con l. La funcin exists() en ExtractCode.cpp prueba que un directorio existe abriendo un echero temporal en l. Si la obertura falla, el directorio no existe. Borre el chero enviando su nombre como unchar* a std::remove(). El programa principal valida los argumentos de la lnea de comandos y despus lee el chero de entrada lnea por lnea, mirando por los delimitadores especiales de cdigo fuente. La bandera booleana inCode indica que el programa esta en el medio de un chero fuente, as que las lineas deben ser extradas. La bandera printDelims ser verdadero si el elemento de obertura no est seguido de un signo de exclamancin; si no la primera y la ltima lnea no son escritas. Es importante comprobar el ltimo delimitador primero, por que el elemnto inicial es un subconjuntom y buscando por el elemento inicial debera retornar cierto en ambos casos. Si encontramos el elemento nal, vericamos que estamos en el medio del procesamiento de un chero fuente; sino, algo va mal con la manera en que los delimitadores han sido colocados en el chero de texto. Si inCode es verdadero, todo est bien, y escribiremos (opcionalmente) la ltima linea y cerraremos el chero. Cuando el elemento de obertura se encuentra, procesamos el directorio y el nombre del chero y abrimos el chero. Las siguientes funciones relacionadas con string fueron usadas en este ejemplo: length( ), append( ), getline( ), find( ) (dos versiones), find_first_not_of( ), substr( ),find_first_of( ), c_str( ), y, por supuesto, operator<<( )
10 POSIX, un estndar IEEE, es un "Portable Operating System Interface" (Intercie de Sistema Operativo Portable) y es una generalizacin de muchas de las llamadas a sistema de bajo nivel encontradas en los sistemas UNIX.
55
3.5. Resumen
Los objetos string proporcionan a los desarrolladores un gran nmero de ventajas sobre sus contrapartidas en C. La mayoria de veces, la clase string hacen a las cadenas con punteros a caracteres innecesarios. Esto elimina por completo una clase de defectos de software que radican en el uso de punteros no inicializados o con valores incorrectos. FIXME: Los string de C++, de manera transparente y dinmica, hacen crecer el espacio de alamcenamiento para acomodar los cambios de tamao de los datos de la cadena. Cuando los datos en n string crece por encima de los lmites de la memoria asignada inicialmente para ello, el objeto string har las llamadas para la gestin de la memoria para obtener el espacio y retornar el espacio al montn. La gestin consistente de la memoria previente lagunas de memoria y tiene el potencial de ser mucho ms eciente que un "hgalo usted mismo". Las funciones de la clase string proporcionan un sencillo y comprensivo conjunto de herramientas para crear, modicar y buscar en cadenas. Las comparaciones entre string siempre son sensibles a maysculas/minsculas, pero usted puede solucionar el problema copiando los datos a una cadena estilo C acabada en nulo y usando funciones no sensibles a maysculas/minsculas, convirtiendo temporalmente los datos contenidos a maysculas o minsculas, o creando una clase string sensible que sobreescribe los rasgos de carcter usados para crear un objeto basic_string
3.6. Ejercicios
Las soluciones a los ejercicios se pueden encontrar en el documento electrnico titulado The Thinking in C++ Annotated Solution Guide, disponible por poco dinero en www.BruceEckel.com. 1. Escriba y pruebe una funcin que invierta el orden de los caracteres en una cadena. 2. 2. Un palindromo es una palabra o grupo de palabras que tanto hacia delante hacia atrs se leen igual. Por ejemplo "madam" o "wow". Escriba un programa que tome un string como argumento desde la lnea de comandos y, usando la funcin del ejercicio anterior, escriba si el string es un palndromo o no. 3. 3. Haga que el programa del Ejercicio 2 retorne verdadero incluso si las letras simetricas dieren en maysculas/minsculas. Por ejemplo, "Civic" debera retornar verdadero aunque la primera letra sea mayscula. 4. 4. Cambie el programa del Ejercicio 3 para ignorar la puntuacin y los espacios tambin. Por ejemplo "Able was I, ere I saw Elba." debera retornar verdadero. 5. 5. Usando las siguientes declaraciones de string y solo char (no literales de cadena o nmeros mgicos): string one("I walked down the canyon with the moving mountain bikers."); string two("The bikers passed by me too close for comfort."); string three("I went hiking instead."); produzca la siguiente frase: I moved down the canyon with the mountain bikers. The mountain bikers passed by me too close for comfort. So I went hiking instead. 6. 6. Escriba un programa llamado "reemplazo" que tome tres argumentos de la lnea de comandos representando un chero de texto de entrada, una frase para reemplazar (llmela from), y una cadena de reemplazo (llmela to). El programa debera escribir un nuevo chero en la salida estandar con todas las ocurrencias de from reemplazadas por to. 7. 7. Repetir el ejercicio anterior pero reemplazando todas las instancias pero ignorando las maysculas/minsculas. 8. 8. Haga su programa a partir del Ejercicio 3 tomando un nombre de chero de la linea de comandos, y despues mostrando todas las palabras que son palndromos (ignorando las maysculas/minsculas) en el chero. No intente buscar palabras para palndromos que son mas largos que una palabra (a diferencia del ejercicio 4). 9. 9. Modique HTMLStripper.cpp para que cuando encuentre una etiqueta, muestre el nombre de la etiqueta, entonces muestre el contenido del chero entre la etiqueta y la etiqueta de nalizacin de chero. Asuma que no existen etiquetas anidadas, y que todas las etiquetas tienen etiquetas de nalizacion (denotadas con </TAGNAME>). 56
3.6. Ejercicios 10. 10. Escriba un programa que tome tres argumentos de la lnea de comandos (un nombre de chero y dos cadenas) y muestre en la consola todas la lneas en el chero que tengan las dos cadenas en la lnea, alguna cadena, solo una cadena o ninguna de ellas, basndose en la entreada de un usuario al principio del programa (el usuario elegir que modo de bsqueda usar). Para todo excepto para la opcin "ninguna cadena", destaque la cadena(s) de entrada colocando un asterisco (*) al principio y al nal de cada cadena que coincida cuando sea mostrada. 11. 11. Escriba un programa que tome dos argumentos de la linea de comandos (un nombre de chero y una cadena) y cuente el numero de veces que la cadena esta en el chero, incluso si es una subcadena (pero ignorando los solapamientos). Por ejemplo, una cadena de entrada de "ba" debera coincidir dos veces en la palabra "basquetball", pero la cadena de entrada "ana" solo deberia coincidir una vez en "banana". Muestre por la consola el nmero de veces que la cadena coincide en el chero, igual que la longitud media de las palabras donde la cadena coincide. (Si la cadena coincide ms de una vez en una palabra, cuente solamente la palabra una vez en el clculo de la media). 12. 12. Escriba un programa que tome un nombre de chero de la lnea de comandos y perle el uso del carcter, incluyendo la puntuacin y los espacios (todos los valores de caracteres desde el 0x21 [33] hasta el 0x7E [126], adems del carcter de espacio). Esto es, cuente el numero de ocurrencias para cada carcter en el chero, despus muestre los resultados ordenados secuencialmente (espacio, despues !, ", #, etc.) o por frecuencia descendente o ascendente basado en una entrada de usuario al principio del programa. Para el espacio, muestre la palabra "espacio" en vez del carcter . Una ejecucin de ejemplo debe mostrarse como esto: Formato secuencial, ascendente o descendente (S/A/D): D t: 526 r: 490 etc. 13. 13. Usando find() y rfind(), escriba un programa que tome dos argumentos de lnea de comandos (un nombre de chero y una cadena) y muestre la primera y la ltima palapra (y sus indices) que no coinciden con la cadena, asi como los indice de la primera y la ltima instancia de la cadena. Muestre "No Encontrado" si alguna de las busquedas fallan. 14. 14. Usando la familia de fuciones find_first_of (pero no exclusivamente), escriba un programa que borrar todos los caracteres no alfanumricos excepto los espacios y los puntos de un chero. Despues convierta a maysculas la primera letra que siga a un punto. 15. 15. Otra vez, usando la familia de funciones find_first_of, escriba un programa que acepte un nombre de chero como argumentod elinea de comandos y despus formatee todos los nmeros en un chero de moneda. Ignore los puntos decimales despus del primero despues de un carcter no mumrico, e redondee al 16. 16. Escriba un programa que acepte dos argumentos por lnea de comandos (un nombre de chero y un numero) y mezcle cada paralabra en el chero cambiando aleatoriamente dos de sus letras el nmero de veces especicado en el segundo parametro. (Esto es, si le pasamos 0 a su programa desde la lnea de comandos, las palabras no sern mezcladas; si le pasamos un 1, un par de letras aleatoriamente elegidas deben ser cambiadas, para una entrada de 2, dos parejas aleatorias deben ser intercambiadas, etc.). 17. 17. Escriba un programa que acepte un nombre de chero desde la lnea de comandos y muestre el numero de frases (denido como el numero de puntos en el chero), el nmero medio de caracteres por frase, y el nmero total de caracteres en el chero.
57
Part III
Temas especiales
59
4: Herencia mltiple
4.1. Perspectiva 4.2. Herencia de interfaces
//: C09:Interfaces.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Multiple interface inheritance. #include <iostream> #include <sstream> #include <string> using namespace std; class Printable { public: virtual ~Printable() {} virtual void print(ostream&) const = 0; }; class Intable { public: virtual ~Intable() {} virtual int toInt() const = 0; }; class Stringable { public: virtual ~Stringable() {} virtual string toString() const = 0; }; class Able : public Printable, public Intable, public Stringable { int myData; public: Able(int x) { myData = x; } void print(ostream& os) const { os << myData; } int toInt() const { return myData; } string toString() const { ostringstream os; os << myData; return os.str(); } }; void testPrintable(const Printable& p) { p.print(cout); cout << endl;
61
//: C09:Interfaces2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Implicit interface inheritance via templates. #include <iostream> #include <sstream> #include <string> using namespace std; class Able { int myData; public: Able(int x) { myData = x; } void print(ostream& os) const { os << myData; } int toInt() const { return myData; } string toString() const { ostringstream os; os << myData; return os.str(); } }; template<class Printable> void testPrintable(const Printable& p) { p.print(cout); cout << endl; } template<class Intable> void testIntable(const Intable& n) { cout << n.toInt() + 1 << endl; } template<class Stringable> void testStringable(const Stringable& s) { cout << s.toString() + "th" << endl; } int main() { Able a(7); testPrintable(a); testIntable(a); testStringable(a);
62
//: C09:UseDatabase.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "Database.h" int main() { Database db("MyDatabase"); db.open(); // Use other db functions... db.close(); } /* Output: connected to MyDatabase MyDatabase closed */
63
//: C09:DBConnection.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Uses a "mixin" class. #ifndef DBCONNECTION_H #define DBCONNECTION_H #include <cassert> #include <string> #include "Countable.h" #include "Database.h" using std::string; class DBConnection : public Database, public Countable { DBConnection(const DBConnection&); // Disallow copy DBConnection& operator=(const DBConnection&); protected: DBConnection(const string& dbStr) throw(DatabaseError) : Database(dbStr) { open(); } ~DBConnection() { close(); } public: static DBConnection* create(const string& dbStr) throw(DatabaseError) { DBConnection* con = new DBConnection(dbStr); con->attach(); assert(con->refCount() == 1); return con; } // Other added functionality as desired... }; #endif // DBCONNECTION_H
//: C09:UseDatabase2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison.
64
//: C09:DBConnection2.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // A parameterized mixin. #ifndef DBCONNECTION2_H #define DBCONNECTION2_H #include <cassert> #include <string> #include "Database.h" using std::string; template<class Counter> class DBConnection : public Database, public Counter { DBConnection(const DBConnection&); // Disallow copy DBConnection& operator=(const DBConnection&); protected: DBConnection(const string& dbStr) throw(DatabaseError) : Database(dbStr) { open(); } ~DBConnection() { close(); } public: static DBConnection* create(const string& dbStr) throw(DatabaseError) { DBConnection* con = new DBConnection(dbStr); con->attach(); assert(con->refCount() == 1); return con; } // Other added functionality as desired... }; #endif // DBCONNECTION2_H
65
//: C09:UseDatabase3.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Tests a parameterized "mixin" class. #include <cassert> #include "Countable.h" #include "DBConnection2.h" class DBClient { DBConnection<Countable>* db; public: DBClient(DBConnection<Countable>* dbCon) { db = dbCon; db->attach(); } ~DBClient() { db->detach(); } }; int main() { DBConnection<Countable>* db = DBConnection<Countable>::create("MyDatabase"); assert(db->refCount() == 1); DBClient c1(db); assert(db->refCount() == 2); DBClient c2(db); assert(db->refCount() == 3); db->detach(); assert(db->refCount() == 2); }
66
//: C09:Duplicate.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Shows duplicate subobjects. #include <iostream> using namespace std; class Top { int x; public: Top(int n) { x = n; } }; class Left : public Top { int y; public: Left(int m, int n) : Top(m) { y = n; } }; class Right : public Top { int z; public: Right(int m, int n) : Top(m) { z = n; } }; class Bottom : public Left, public Right { int w; public: Bottom(int i, int j, int k, int m) : Left(i, k), Right(j, k) { w = m; } }; int main() {
67
68
//: C09:VirtualBase2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // How NOT to implement operator<<. #include <iostream> using namespace std; class Top { int x; public: Top(int n) { x = n; } virtual ~Top() {} friend ostream& operator<<(ostream& os, const Top& t) { return os << t.x; } }; class Left : virtual public Top { int y; public: Left(int m, int n) : Top(m) { y = n; } friend ostream& operator<<(ostream& os, const Left& l) { return os << static_cast<const Top&>(l) << , << l.y; } }; class Right : virtual public Top { int z; public: Right(int m, int n) : Top(m) { z = n; } friend ostream& operator<<(ostream& os, const Right& r) { return os << static_cast<const Top&>(r) << , << r.z; } }; class Bottom : public Left, public Right { int w; public: Bottom(int i, int j, int k, int m) : Top(i), Left(0, j), Right(0, k) { w = m; } friend ostream& operator<<(ostream& os, const Bottom& b){ return os << static_cast<const Left&>(b) << , << static_cast<const Right&>(b) << , << b.w; } };
69
//: C09:VirtualBase3.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // A correct stream inserter. #include <iostream> using namespace std; class Top { int x; public: Top(int n) { x = n; } virtual ~Top() {} friend ostream& operator<<(ostream& os, const Top& t) { return os << t.x; } }; class Left : virtual public Top { int y; protected: void specialPrint(ostream& os) const { // Only print Lefts part os << ,<< y; } public: Left(int m, int n) : Top(m) { y = n; } friend ostream& operator<<(ostream& os, const Left& l) { return os << static_cast<const Top&>(l) << , << l.y; } }; class Right : virtual public Top { int z; protected: void specialPrint(ostream& os) const { // Only print Rights part os << ,<< z; } public: Right(int m, int n) : Top(m) { z = n; } friend ostream& operator<<(ostream& os, const Right& r) { return os << static_cast<const Top&>(r) << , << r.z; } }; class Bottom : public Left, public Right { int w; public: Bottom(int i, int j, int k, int m) : Top(i), Left(0, j), Right(0, k) { w = m; } friend ostream& operator<<(ostream& os, const Bottom& b){ os << static_cast<const Top&>(b); b.Left::specialPrint(os); b.Right::specialPrint(os); return os << , << b.w;
70
//: C09:VirtInit.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Illustrates initialization order with virtual bases. #include <iostream> #include <string> using namespace std; class M { public: M(const string& s) { cout << "M " << s << endl; } }; class A { M m; public: A(const string& s) : m("in A") { cout << "A " << s << endl; } virtual ~A() {} }; class B { M m; public: B(const string& s) : m("in B") cout << "B " << s << endl; } virtual ~B() {} }; class C { M m; public: C(const string& s) : m("in C") cout << "C " << s << endl; } virtual ~C() {} }; class D { M m; public: D(const string& s) : m("in D") { cout << "D " << s << endl; } virtual ~D() {} }; class E : public A, virtual public B, virtual public C { M m; public:
71
M B M C M A M E M D M F M G
G G E G F G main
72
//: C09:BreakTie.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. class Top { public: virtual ~Top() {} }; class Left : virtual public Top { public: void f() {} }; class Right : virtual public Top { public: void f() {} }; class Bottom : public Left, public Right { public: using Left::f; }; int main() { Bottom b; b.f(); // Calls Left::f() }
//: C09:Dominance.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. class Top { public: virtual ~Top() {} virtual void f() {} }; class Left : virtual public Top {
73
//: C09:Dominance2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> using namespace std; class A { public: virtual ~A() {} virtual void f() { cout << "A::f\n"; } }; class B : virtual public A { public: void f() { cout << "B::f\n"; } }; class C : public B {}; class D : public C, virtual public A {}; int main() { B* p = new D; p->f(); // Calls B::f() delete p; }
74
//: C09:Vendor.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Assume this is compiled and unavailable to you. #include "Vendor.h" #include <iostream> using namespace std; void Vendor::v() const { cout << "Vendor::v()" << endl; } void Vendor::f() const { cout << "Vendor::f()" << endl; } Vendor::~Vendor() { cout << "~Vendor()" << endl; } void Vendor1::v() const { cout << "Vendor1::v()" << endl; } void Vendor1::f() const { cout << "Vendor1::f()" << endl; } Vendor1::~Vendor1() { cout << "~Vendor1()" << endl; } void A(const Vendor& v) { // ... v.v(); v.f(); // ... } void B(const Vendor& v) { // ... v.v(); v.f(); // ... }
//: C09:Paste.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} Vendor
75
calling f() Paste1::f() Vendor1::f() calling g() Paste1::g() calling A(p1p) Paste1::v() Vendor1::v() Vendor::f() calling B(p1p) Paste1::v() Vendor1::v() Vendor::f() delete mp
76
4.9. Resumen
~Paste1() ~Vendor1() ~Vendor() ~MyBase()
77
5: Patrones de Diseo
"...describa un problema que sucede una y otra vez en nuestro entorno, y luego describa el ncleo de la solucin a ese problema, de tal forma que pueda utilizar esa solucin un milln de veces ms, sin siquiera hacerlo dos veces de la misma manera." - Christopher Alexander Este captulo presenta el importante y an no tradicional enfoque de los patrones para el diseo de programas. El avance reciente ms importante en el diseo orientado a objetos es probablemente el movimiento de los patrones de diseo, inicialmente narrado en "Design Patterns", por Gamma, Helm, Johnson y Vlissides (Addison Wesley, 1995), que suele llamarse el libro de la "Banda de los Cuatro" (en ingls, GoF: Gang of Four). El GoF muestra 23 soluciones para clases de problemas muy particulares. En este captulo se discuten los conceptos bsicos de los patrones de diseo y se ofrecen ejemplos de cdigo que ilustran los patrones escogidos. Esto debera abrirle el apetito para leer ms acerca de los patrones de diseo, una fuente de lo que se ha convertido en vocabulario esencial, casi obligatorio, para la programacin orientada a objetos.
Captulo 5. Patrones de Diseo Tambin ha visto ya otro patrn que aparece en el GoF: el iterador. Esta es la herramienta fundamental usada en el diseo del STL, descrito en captulos anteriores. El iterador esconde la implementacin concreta del contenedor a medida que se avanza y se seleccionan los elementos uno a uno. Los iteradores le ayudan a escribir cdigo genrico que realiza una operacin en todos los elementos de un rango sin tener en cuenta el contenedor que contiene el rango. Por lo tanto, cualquier contenedor que pueda producir iteradores puede utilizar su cdigo genrico.
5.3. Simplicacin de modismos Aunque a veces, esta idea se clasica errneamente como un patrn. Esto no es bueno, porque contamina la idea del patrn de diseo, y sugiere que cualquier cosa que le sorprenda la primera vez que la ve debera convertirse en un patrn de diseo. El lenguaje Java da otro ejemplo equivocado: Los diseadores de la especicacin de JavaBeans decidieron referirse a la notacin get/set como un patrn de diseo (por ejemplo, getInfo() devuelve una propiedad Info y setInfo() la modica). Esto es nicamente una convencin de nombrado, y de ninguna manera constituye un patrn de diseo.
5.3.1. Mensajero
El ms trivial es el Mensajero (Messenger), [136] que empaqueta informacin en un objeto que se envia, en lugar de ir enviando todas las piezas independientemente. Ntese que sin el Mensajero, el cdigo para la funcin translate() sera mucho ms confuso:
//: C10:MessengerDemo.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> #include <string> using namespace std; class Point { // A messenger public: int x, y, z; // Since its just a carrier Point(int xi, int yi, int zi) : x(xi), y(yi), z(zi) {} Point(const Point& p) : x(p.x), y(p.y), z(p.z) {} Point& operator=(const Point& rhs) { x = rhs.x; y = rhs.y; z = rhs.z; return *this; } friend ostream& operator<<(ostream& os, const Point& p) { return os << "x=" << p.x << " y=" << p.y << " z=" << p.z; } }; class Vector { // Mathematical vector public: int magnitude, direction; Vector(int m, int d) : magnitude(m), direction(d) {} }; class Space { public: static Point translate(Point p, Vector v) { // Copy-constructor prevents modifying the original. // A dummy calculation: p.x += v.magnitude + v.direction; p.y += v.magnitude + v.direction; p.z += v.magnitude + v.direction;
81
El cdigo ha sido simplicado para evitar distracciones. Como el objetivo del Mensajero es simplemente llevar datos, dichos datos se hacen pblicos para facilitar el acceso. Sin embargo, podra tener razones para hacer estos campos privados.
82
5.4. Singleton El parmetro de recoleccin debe tener alguna forma de establecer o insertar valores. Ntese que por esta denicin, un Mensajero podra usarse como parmetro de recoleccin. La clave reside en que el parmetro de recoleccin se pasa y es modicado por la funcin que lo recibe.
5.4. Singleton
Posiblemente, el patrn de diseo ms simple del GoF es el Singleton, que es una forma de asegurar una nica instancia de una clase. El siguiente programa muestra cmo implementar un Singleton en C++:
//: C10:SingletonPattern.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> using namespace std; class Singleton { static Singleton s; int i; Singleton(int x) : i(x) { } Singleton& operator=(Singleton&); // Disallowed Singleton(const Singleton&); // Disallowed public: static Singleton& instance() { return s; } int getValue() { return i; } void setValue(int x) { i = x; } }; Singleton Singleton::s(47); int main() { Singleton& s = Singleton::instance(); cout << s.getValue() << endl; Singleton& s2 = Singleton::instance(); s2.setValue(9); cout << s.getValue() << endl; }
La clave para crear un Singleton es evitar que el programador cliente tenga control sobre el ciclo de vida del objeto. Para lograrlo, declare todos los constructores privados, y evite que el compilador genere implcitamente cualquier constructor. Fjese que el FIXME: constructor de copia? y el operador de asignacin (que intencionadamente carecen de implementacin alguna, ya que nunca van a ser llamados) estn declarados como privados, para evitar que se haga cualquier tipo de copia. Tambin debe decidir cmo va a crear el objeto. Aqu, se crea de forma esttica, pero tambin puede esperar a que el programador cliente pida uno y crearlo bajo demanda. Esto se llama "inicializacin vaga", y slo tiene sentido si resulta caro crear el objeto y no siempre se necesita. Si devuelve un puntero en lugar de una referencia, el usuario podra borrar el puntero sin darse cuenta, por lo que la implementacin citada anteriormente es ms segura (el destructor tambin podra declararse privado o protegido para solventar el problema). En cualquier caso, el objeto debera almacenarse de forma privada. Usted da acceso a travs de FIXME (funciones de miembros) pblicas. Aqu, instance() genera una referencia al objeto Singleton. El resto de la interfaz (getValue() y setValue()) es la interfaz regular de la clase. Fjese en que no est restringido a crear un nico objeto. Esta tcnica tambin soporta la creacion de un pool limitado de objetos. En este caso, sin embargo, puede enfrentarse al problema de compartir objetos del pool. Si esto supone un problema, puede crear una solucin que incluya un check-out y un check-in de los objetos compartidos. 83
La implementacin no debe FIXME: hacerse en la misma lnea, porque eso signicara que la funcin entera, includa la denicin del objeto esttico que contiene, podra ser duplicada en cualquier unidad de traduccin donde se incluya, lo que viola la regla de nica denicin de C++. [137] Con toda seguridad, esto frustrara cualquier intento de controlar el orden de inicializacin (pero potencialmente de una forma sutil y difcil de detectar). De forma que la implementacin debe separarse:
//: C10:LogFile.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "LogFile.h" std::ofstream& logfile() { static std::ofstream log("Logfile.log"); return log; }
Ahora el objeto log no se inicializar hasta la primera vez que se llame a logfile(). As que, si crea una funcin:
//: C10:UseLog1.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef USELOG1_H #define USELOG1_H void f(); #endif // USELOG1_H
84
5.4. Singleton
//: C10:UseLog1.cpp {O} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "UseLog1.h" #include "LogFile.h" void f() { logfile() << __FILE__ << std::endl; }
el objecto log no se crea hasta la primera llamada a f(). Puede combinar fcilmente la creacin de objetos estticos dentro de una funcin miembro con la clase Singleton. SingletonPattern.cpp puede modicarse para usar esta aproximacin:[138]
//: C10:SingletonPattern2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Meyers Singleton. #include <iostream> using namespace std; class Singleton { int i; Singleton(int x) : i(x) { } void operator=(Singleton&); Singleton(const Singleton&); public: static Singleton& instance() { static Singleton s(47); return s; } int getValue() { return i; } void setValue(int x) { i = x; } }; int main() { Singleton& s = Singleton::instance();
85
Se da un caso especialmente interesante cuando dos Singletons dependen mutuamente el uno del otro, de esta forma:
//: C10:FunctionStaticSingleton.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. class Singleton1 { Singleton1() {} public: static Singleton1& ref() { static Singleton1 single; return single; } }; class Singleton2 { Singleton1& s1; Singleton2(Singleton1& s) : s1(s) {} public: static Singleton2& ref() { static Singleton2 single(Singleton1::ref()); return single; } Singleton1& f() { return s1; } }; int main() { Singleton1& s1 = Singleton2::ref().f(); }
Cuando se llama a Singleton2::ref(), hace que se cree su nico objeto Singleton2. En el proceso de esta creacin, se llama a Singleton1::ref(), y esto hace que se cree su objeto nico Singleton1. Como esta tcnica no se basa en el orden de linkado ni el de carga, el programador tiene mucho mayor control sobre la inicializacin, lo que redunda en menos problemas. Otra variacin del Singleton separa la unicidad de un objeto de su implementacin. Esto se logra usando el "Patrn Plantilla Curiosamente Recursivo" mencionado en el Captulo 5:
//: C10:CuriousSingleton.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Separates a class from its Singleton-ness (almost). #include <iostream> using namespace std; template<class T> class Singleton { Singleton(const Singleton&); Singleton& operator=(const Singleton&); protected:
86
MyClass se convierte en Singleton: 1. Haciendo que su constructor sea private o protected. 2. Hacindose amigo de Singleton<MyClass>. 3. Derivando MyClass desde Singleton<MyClass>. La auto-referencia del paso 3 podra sonar inversmil, pero tal como se explic en el Captulo 5, funciona porque slo hay una dependencia esttica sobre el argumento plantilla de la plantilla Singleton. En otras palabras, el cdigo de la clase Singleton<MyClass> puede ser instanciado por el compilador porque no depende del tamao de MyClass. Es despus, cuando se a Singleton<MyClass>::instance(), cuando se necesita el tamao de MyClass, y para entonces MyClass ya se ha compilado y su tamao se conoce.[139] Es interesante lo intrincado que un patrn tan simple como el Singleton puede llegar a ser, y ni siquiera se han tratado todava asuntos de seguridad de hilos. Por ltimo, el patrn Singleton debera usarse lo justo y necesario. Los verdaderos objetos Singleton rara vez aparecen, y la ltima cosa para la que debe usarse un Singleton es para remplazar a una variable global. [140]
87
//: C10:CommandPattern.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> #include <vector> using namespace std; class Command { public: virtual void execute() = 0; }; class Hello : public Command { public: void execute() { cout << "Hello "; } }; class World : public Command { public: void execute() { cout << "World! "; } }; class IAm : public Command { public: void execute() { cout << "Im the command pattern!"; } }; // An object that holds commands: class Macro { vector<Command*> commands; public: void add(Command* c) { commands.push_back(c); } void run() { vector<Command*>::iterator it = commands.begin(); while(it != commands.end()) (*it++)->execute(); } }; int main() { Macro macro; macro.add(new Hello); macro.add(new World); macro.add(new IAm); macro.run(); }
El punto principal del Comando es permitirle dar una accin deseada a una funcin u objeto. En el ejemplo anterior, esto provee una manera de encolar un conjunto de acciones que se deben ejecutar colectivamente. Aqu, puede crear dinmicamente nuevos comportamientos, algo que puede hacer normalmente escribiendo nuevo cdigo, pero en el ejemplo anterior podra hacerse interpretando un script (vea el patrn Intrprete si lo que necesita hacer se vuelve demasiado complicado).
88
89
90
class Proxy : public ProxyBase { ProxyBase* implementation; public: Proxy() { implementation = new Implementation(); } ~Proxy() { delete implementation; } // Forward calls to the implementation: void f() { implementation->f(); } void g() { implementation->g(); } void h() { implementation->h(); } }; int main() Proxy p; p.f(); p.g(); p.h(); } {
91
//: C10:KissingPrincess2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // The State pattern. #include <iostream> #include <string> using namespace std; class Creature { class State { public: virtual string response() = 0; }; class Frog : public State { public: string response() { return "Ribbet!"; } }; class Prince : public State { public: string response() { return "Darling!"; } }; State* state; public: Creature() : state(new Frog()) {} void greet() { cout << state->response() << endl; } void kiss() { delete state; state = new Prince(); } }; int main() { Creature creature; creature.greet(); creature.kiss(); creature.greet(); }
5.7. Adaptador
//: C10:FibonacciGenerator.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #ifndef FIBONACCIGENERATOR_H #define FIBONACCIGENERATOR_H
92
5.7. Adaptador
class FibonacciGenerator { int n; int val[2]; public: FibonacciGenerator() : n(0) { val[0] = val[1] = 0; } int operator()() { int result = n > 2 ? val[0] + val[1] : n > 0 ? 1 : 0; ++n; val[0] = val[1]; val[1] = result; return result; } int count() { return n; } }; #endif // FIBONACCIGENERATOR_H
//: C10:FibonacciGeneratorTest.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> #include "FibonacciGenerator.h" using namespace std; int main() { FibonacciGenerator f; for(int i =0; i < 20; i++) cout << f.count() << ": " << f() << endl; }
//: C10:FibonacciAdapter.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Adapting an interface to something you already have. #include <iostream> #include <numeric> #include "FibonacciGenerator.h" #include "../C06/PrintSequence.h" using namespace std; class FibonacciAdapter { // Produce an iterator FibonacciGenerator f; int length; public: FibonacciAdapter(int size) : length(size) {} class iterator; friend class iterator; class iterator : public std::iterator< std::input_iterator_tag, FibonacciAdapter, ptrdiff_t> { FibonacciAdapter& ap; public: typedef int value_type; iterator(FibonacciAdapter& a) : ap(a) {} bool operator==(const iterator&) const { return ap.f.count() == ap.length; }
93
94
95
96
} Answer canIHave() { vector<GimmeStrategy*>::iterator it = chain.begin(); while(it != chain.end()) if((*it++)->canIHave() == YES) return YES; // Reached end without success... cout << "Whiiiiinnne!" << endl; return NO; } ~Gimme() { purge(chain); } }; int main() { Gimme chain; chain.canIHave(); }
97
98
99
100
//: C10:VirtualConstructor.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include <iostream> #include <string> #include <stdexcept> #include <stdexcept> #include <cstddef> #include <vector> #include "../purge.h" using namespace std; class Shape { Shape* s; // Prevent copy-construction & operator= Shape(Shape&); Shape operator=(Shape&); protected: Shape() { s = 0; } public: virtual void draw() { s->draw(); } virtual void erase() { s->erase(); } virtual void test() { s->test(); } virtual ~Shape() { cout << "~Shape" << endl; if(s) { cout << "Making virtual call: "; s->erase(); // Virtual call } cout << "delete s: "; delete s; // The polymorphic deletion // (delete 0 is legal; it produces a no-op) } class BadShapeCreation : public logic_error { public: BadShapeCreation(string type) : logic_error("Cannot create type " + type) {} }; Shape(string type) throw(BadShapeCreation); }; class Circle : public Shape { Circle(Circle&); Circle operator=(Circle&); Circle() {} // Private constructor friend class Shape; public: void draw() { cout << "Circle::draw" << endl; } void erase() { cout << "Circle::erase" << endl; } void test() { draw(); } ~Circle() { cout << "Circle::~Circle" << endl; } }; class Square : public Shape { Square(Square&); Square operator=(Square&); Square() {} friend class Shape; public: void draw() { cout << "Square::draw" << endl; } void erase() { cout << "Square::erase" << endl; } void test() { draw(); }
102
103
104
//: C10:Bicycle.cpp {O} {-mwcc} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "Bicycle.h" #include <cassert> #include <cstddef> using namespace std; std::string BicyclePart::names[NPARTS] = { "Frame", "Wheel", "Seat", "Derailleur", "Handlebar", "Sprocket", "Rack", "Shock" }; // MountainBikeBuilder implementation void MountainBikeBuilder::buildFrame() { product->addPart(new BicyclePart(BicyclePart::FRAME)); } void MountainBikeBuilder::buildWheel() { product->addPart(new BicyclePart(BicyclePart::WHEEL)); } void MountainBikeBuilder::buildSeat() { product->addPart(new BicyclePart(BicyclePart::SEAT)); } void MountainBikeBuilder::buildDerailleur() { product->addPart( new BicyclePart(BicyclePart::DERAILLEUR)); } void MountainBikeBuilder::buildHandlebar() { product->addPart( new BicyclePart(BicyclePart::HANDLEBAR)); }
105
106
//: C10:BuildBicycles.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} Bicycle // The Builder design pattern. #include <cstddef> #include <iostream> #include <map> #include <vector> #include "Bicycle.h" #include "../purge.h" using namespace std; // Constructs a bike via a concrete builder Bicycle* buildMeABike( BicycleTechnician& t, BicycleBuilder* builder) { t.setBuilder(builder); t.construct(); Bicycle* b = builder->getProduct(); cout << "Built a " << builder->getBikeName() << endl; return b; } int main() { // Create an order for some bicycles map <string, size_t> order; order["mountain"] = 2; order["touring"] = 1; order["racing"] = 3; // Build bikes vector<Bicycle*> bikes; BicycleBuilder* m = new MountainBikeBuilder; BicycleBuilder* t = new TouringBikeBuilder; BicycleBuilder* r = new RacingBikeBuilder; BicycleTechnician tech; map<string, size_t>::iterator it = order.begin(); while(it != order.end()) { BicycleBuilder* builder; if(it->first == "mountain") builder = m; else if(it->first == "touring") builder = t; else if(it->first == "racing") builder = r; for(size_t i = 0; i < it->second; ++i) bikes.push_back(buildMeABike(tech, builder)); ++it; } delete m; delete t; delete r; // Display inventory for(size_t i = 0; i < bikes.size(); ++i) cout << "Bicycle: " << *bikes[i] << endl;
107
5.13. Observador
//: C10:Observer.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // The Observer interface. #ifndef OBSERVER_H #define OBSERVER_H class Observable; class Argument {}; class Observer { public: // Called by the observed object, whenever // the observed object is changed: virtual void update(Observable* o, Argument* arg) = 0; virtual ~Observer() {} }; #endif // OBSERVER_H
//: C10:Observable.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // The Observable class. #ifndef OBSERVABLE_H #define OBSERVABLE_H #include <set> #include "Observer.h" class Observable { bool changed;
108
5.13. Observador
std::set<Observer*> observers; protected: virtual void setChanged() { changed = true; } virtual void clearChanged() { changed = false; } public: virtual void addObserver(Observer& o) { observers.insert(&o); } virtual void deleteObserver(Observer& o) { observers.erase(&o); } virtual void deleteObservers() { observers.clear(); } virtual int countObservers() { return observers.size(); } virtual bool hasChanged() { return changed; } // If this object has changed, notify all // of its observers: virtual void notifyObservers(Argument* arg = 0) { if(!hasChanged()) return; clearChanged(); // Not "changed" anymore std::set<Observer*>::iterator it; for(it = observers.begin();it != observers.end(); it++) (*it)->update(this, arg); } virtual ~Observable() {} }; #endif // OBSERVABLE_H
//: C10:InnerClassIdiom.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Example of the "inner class" idiom. #include <iostream> #include <string> using namespace std; class Poingable { public: virtual void poing() = 0; }; void callPoing(Poingable& p) { p.poing(); } class Bingable { public: virtual void bing() = 0; }; void callBing(Bingable& b) { b.bing(); } class Outer { string name; // Define one inner class:
109
110
5.13. Observador
isOpen = true; openNotifier.notifyObservers(); closeNotifier.open(); } void close() { // Closes its petals isOpen = false; closeNotifier.notifyObservers(); openNotifier.close(); } // Using the "inner class" idiom: class OpenNotifier; friend class Flower::OpenNotifier; class OpenNotifier : public Observable { Flower* parent; bool alreadyOpen; public: OpenNotifier(Flower* f) : parent(f), alreadyOpen(false) {} void notifyObservers(Argument* arg = 0) { if(parent->isOpen && !alreadyOpen) { setChanged(); Observable::notifyObservers(); alreadyOpen = true; } } void close() { alreadyOpen = false; } } openNotifier; class CloseNotifier; friend class Flower::CloseNotifier; class CloseNotifier : public Observable { Flower* parent; bool alreadyClosed; public: CloseNotifier(Flower* f) : parent(f), alreadyClosed(false) {} void notifyObservers(Argument* arg = 0) { if(!parent->isOpen && !alreadyClosed) { setChanged(); Observable::notifyObservers(); alreadyClosed = true; } } void open() { alreadyClosed = false; } } closeNotifier; }; class Bee { string name; // An "inner class" for observing openings: class OpenObserver; friend class Bee::OpenObserver; class OpenObserver : public Observer { Bee* parent; public: OpenObserver(Bee* b) : parent(b) {} void update(Observable*, Argument *) { cout << "Bee " << parent->name << "s breakfast time!" << endl; } } openObsrv; // Another "inner class" for closings: class CloseObserver; friend class Bee::CloseObserver; class CloseObserver : public Observer { Bee* parent;
111
112
113
114
115
// Add the ability to do "Bee" activities: class Bee : public Visitor { public: virtual void visit(Gladiolus*) { cout << "Bee and Gladiolus" << endl; } virtual void visit(Renuculus*) { cout << "Bee and Renuculus" << endl; } virtual void visit(Chrysanthemum*) { cout << "Bee and Chrysanthemum" << endl; } }; struct FlowerGen { Flower* operator()() { switch(rand() % 3) { default: case 0: return new Gladiolus; case 1: return new Renuculus; case 2: return new Chrysanthemum; } } }; int main() { srand(time(0)); // Seed the random number generator vector<Flower*> v(10); generate(v.begin(), v.end(), FlowerGen()); vector<Flower*>::iterator it; // Its almost as if I added a virtual function // to produce a Flower string representation: StringVal sval; for(it = v.begin(); it != v.end(); it++) { (*it)->accept(sval); cout << string(sval) << endl; } // Perform "Bee" operation on all Flowers: Bee bee; for(it = v.begin(); it != v.end(); it++) (*it)->accept(bee); purge(v); }
116
6: Concurrencia
Los objetos ofrecen una forma de dividir un programa en diferentes secciones. A menudo, tambin es necesario dividir un programa, independientemente de las subtareas en ejecucin. Utilizando multihilado, un hilo de ejecucin dirige cada una esas subtareas independientes, y puedes programar como si cada hilo tuviera su propia CPU. Un mecanismo interno reparte el tiempo de CPU por ti, pero en general, no necesitas pensar acerca de eso, lo que ayuda a simplicar la programacin con mltiples hilos. Un proceso es un programa autocontenido en ejecucin con su propio espacio de direcciones. Un sistema operativo multitarea puede ejecutar ms de un proceso (programa) en el mismo tiempo, mientras ....., por medio de cambios peridicos de CPU de una tarea a otra. Un hilo es una simple ujo de control secuencial con un proceso. Un proceso puede tener de este modo mltiples hilos en ejecucin concurrentes. Puesto que los hilos se ejecutan con un proceso simple, pueden compartir memoria y otros recursos. La dicultad fundamental de escribir programas multihilados est en coordinar el uso de esos recursos entre los diferentes hilos. Hay muchas aplicaciones posibles para el multihilado, pero lo ms usual es querer usarlo cuando tienes alguna parte de tu programa vinculada a un evento o recurso particular. Para evitar bloquear el resto de tu programa, creas un hilo asociado a ese evento o recurso y le permites ejecutarse independientemente del programa principal. La programacin concurrente se como caminar en un mundo completamente nuevo y aprender un nuevo lenguaje de programacin, o por lo menos un nuevo conjunto de conceptos del lenguaje. Con la aparicin del soporte para los hilos en la mayora de los sistemas operativos para microcomputadores, han aparecido tambin en los lenguajes de programacin o libreras extensiones para los hilos. En cualquier caso, programacin hilada: 1. Parece misteriosa y requiere un esfuerzo en la forma de pensar acerca de la programacin. 2. En otros lenguajes el soporte a los hilos es similar. Cuando entiendas los hilos, comprenders una jerga comn. Comprender la programacin concurrente est al mismo nivel de dicultad que comprender el polimorsmo. Si pones un poco de esfuerzo, podrs entender el mecanismo bsico, pero generalmente necesitar de un entendimiento y estudio profundo para desarrollar una comprensin autntica sobre el tema. La meta de este captulo es darte una base slida en los principios de concurrencia para que puedas entender los conceptos y escribir programas multihilados razonables. S consciente de que puedes conarte fcilmente. Si vas a escribir algo complejo, necesitars estudiar libros especcos sobre el tema.
6.1. Motivacin
Una de las razones ms convincentes para usar concurrencia es crear una interfaz sensible al usuario. Considera un programa que realiza una operacin de CPU intensiva y, de esta forma, termina ignorando la entrada del usuario y comienza a no responder. El programa necesita continuar controlandor sus operaciones, y al mismo tiempo necesita devolver el control al botn de la interfaz de usuario para que el programa pueda responder al usuario. Si tienes un botn de "Salir", no querrs estar forzado a sondearlo en todas las partes de cdigo que escribas en tu programa. (Esto acoplara tu botn de salir a lo largo del programa y sera un quebradero de cabeza a la hora de mantenerlo). ..... Una funcin convencional no puede continuar realizando sus operaciones y al mismo tiempo devolver el control al resto del programa. De hecho, suena a imposible, como si la CPU estuviera en dos lugares a la vez, pero esto es precisamente la "ilusin" que la concurrencia permite (en el caso de un sistema multiprocesador, debe haber ms de una "ilusin"). Tambin puedes usar concurrencia para optimizar la carga de trabajo. Por ejemplo, podras necesitar hacer algo importante mientras ests estancado esperando la llegada de una entrada del puerto I/O. Sin hilos, la nica 117
Captulo 6. Concurrencia solucin razonable es sondear los puertos I/O, que es costoso y puede ser difcil. Si tienes una mquina multiprocesador, los mltiples hilos pueden ser distribudos a lo largo de los mltiples procesadores, pudiendo mejorar considerablemente la carga de trabajo. Este es el tpico caso de los potentes servidores web multiprocesdor, que pueden distribuir un gran nmero de peticiones de usuario por todas las CPUs en un programa que asigna un hilo por peticin. Un programa que usa hilos en una mquina monoprocesador har una cosa en un tiempo dado, por lo que es tericamente posible escribir el mismo programa sin el uso de hilos. Sin embargo, el multihilado proporciona un benecio de optimizacin importante: El diseo de un programa puede ser maravillosamente simple. Algunos tipos de problemas, como la simulacin - un video juego, por ejemplo - son difciles de resolver sin el soporte de la concurrencia. El modelo hilado es una comodidad de la programacin para simplicar el manejo de muchas operaciones al mismo tiempo con un simple programa: La CPU desapilar y dar a cada hilo algo de su tiempo. Cada hilo tiene consciencia de que tiene un tiempo constante de uso de CPU, pero el tiempo de CPU est actualmente repartido entre todo los hilos. La excepcin es un programa que se ejecuta sobre mltiples CPUs. Pero una de las cosas fabulosas que tiene el hilado es que te abstrae de esta capa, por lo que tu cdigo no necesita saber si est ejecutndose sobre una sla CPU o sobre varias.[149] De este modo, usar hilos es una manera de crear programas escalables de forma transparente - si un programa se est ejecutando demasiado despacio, puedes acelerarlo fcilmente aadiendo CPUs a tu ordenador. La multitarea y el multihilado tienden a ser las mejores opciones a utilizar en un sistema multiprocesador. El uso de hilos puede reducir la eciencia computacional un poco, pero el aumento neto en el diseo del programa, balanceo de recursos, y la comodidad del usuario a menudo es ms valorado. En general, los hilos te permiten crear diseor ms desacoplados; de lo contrario, las partes de tu cdigo estara obligadas a prestar atencin a tareas que podras manejarlas con hilos normalmente.
6.2. Concurrencia en C++ El mtodo habitual para compilar la librera ZThreads para los distintos sabores de UNIX (Linux, SunOS, Cygwin, etc) es usar un script de conguracin. Despus de desempaquetar los archivos (usando tar), simplemente ejecuta: ./congure && make install en el directorio principal de ZThreads para compilar e instalar una copia de la librera en directorio /usr/local. Puedes personalizar algunas opciones cuando uses el script, includa la localizacin de los cheros. Para ms detalles, utiliza este comando: ./congure ?help El cdigo de ZThreads est estructurado para simplicar la compilacin para otras plataformas (como Borland, Microsoft y Metrowerks). Para hacer esto, crea un nuevo proyecto y aade todos los archivos .cxx en el directorio src de ZThreads a la lista de archivos a compilar. Adems, asegrate de incluir el directorio includo del archivo en la ruta de bsqueda de la cabecera para tu proyecto???. Los detalles exactos variarn de compilador en compilador, por lo que necesitars estar algo familiarizado con tu conjunto de herramientas para ser capaz de utilizar esta opcin. Una vez la compilacin ha nalizado con xito, el siguiente paso es crear un proyecto que use la nueva librera compilada. Primero, permite al compilador saber donde estn localizadas las cabeceras, por lo que tu instruccin #include funcionar correctamente. Habitualmente, necesitars en tu proyecto una opcin como se muestra:
-I/path/to/installation/include -->
Si utilizaste el script de conguracin, la ruta de instalacin ser el prejo de la que deniste (por defecto, /usr/local). Si utilizaste uno de los archivos de proyecto en la creacin del directorio, la ruta instalacin debera ser simplemente la ruta al directorio principal del archivo ZThreads. Despus, necesitars aadir una opcin a tu proyecto que permitir al enlazador saber donde est la librera. Si usaste el script de conguracin, se parecer a lo siguiente:
-L/path/to/installation/lib ?lZThread
De nuevo, si usaste el script de conguracin, la ruta de instalacin s ser el prejo de la que denistes. Si su utilizaste un archivo del proyecto, la ruta ser la misma que la del directorio principal de ZThreads. Nota que si ests utilizando Linux, o Cygwin (www.cygwin.com) bajo Windows, no deberas necesitar modicar la ruta de include o de la librera; el proceso por defecto de instalacin tendr cuidado para hacerlo por ti, normalmente. En GNU/Linux, es posible que necesites aadir lo siguiente a tu .bashrc para que el sistema pueda encontrar la el archivo de la librera compartida LibZThread-x.x.so.0 cuando ejecute programas de este captulo.
export LD_LIBRARY_PATH=/usr/local/lib:${LD_LIBRARY_PATH}
(Asumiendo que utilizas el proceso de instalacin por defecto y la librera compartida acaba en /user/local/lib/; en otro caso, cambia la ruta por tu localizacin.
119
Captulo 6. Concurrencia
virtual void run() = 0; virtual ~Runnable() {} };
Al hacerla una clase base abstracta, Runnable es fcilmente combinable con una clase bsica u otras clases. Para denir una tarea, simplemente hereda de la clase Runnable y sobreescribe run( ) para que la tarea haga lo que quieres. Por ejecomplo, la tarea LiftOff siguiente muestra la cuenta atrs antes de despegar:
//: C11:LiftOff.h // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Demonstration of the Runnable interface. #ifndef LIFTOFF_H #define LIFTOFF_H #include <iostream> #include "zthread/Runnable.h" class LiftOff : public ZThread::Runnable { int countDown; int id; public: LiftOff(int count, int ident = 0) : countDown(count), id(ident) {} ~LiftOff() { std::cout << id << " completed" << std::endl; } void run() { while(countDown--) std::cout << id << ":" << countDown << std::endl; std::cout << "Liftoff!" << std::endl; } }; #endif // LIFTOFF_H
El identicador id sirve como distincin entre multiples instancias de la tarea. Si slo quieres hacer una instancia, debes utilizar el valor por defecto para identicarla. El destructor te permitir ver que tarea est destruda correctamente. En el siguiente ejemplo, las tareas de run( ) no estn dirigidas por hilos separados; directamente es una simple llamada en main( ):
//: C11:NoThread.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. #include "LiftOff.h" int main() { LiftOff launch(10); launch.run(); }
Cuando una clase deriva de Runnable, debe tene una funcin run( ), pero no tiene nada de especial - no produce ninguna habibilidad innata en el hilo. 120
6.3. Utilizacin de los hilos Para llevar a cabo el funcionamiento de los hilos, debes utilizas la clase Thread.
Synchronization_Exception forma parte de la librera ZThread y la clase base para todas las excepciones de ZThread. Se lanzar si hay un error al crear o usar un hilo. Un constructor de Thread slo necesita un puntero a un objeto Runnable. Al crear un objeto Thread se efectuar la incializacin necesaria del hilo y despus se llamar a Runnable::run()
//: C11:MoreBasicThreads.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Adding more threads. //{L} ZThread #include <iostream> #include "LiftOff.h" #include "zthread/Thread.h" using namespace ZThread; using namespace std; int main() { const int SZ = 5; try { for(int i = 0; i < SZ; i++) Thread t(new LiftOff(10, i)); cout << "Waiting for LiftOff" << endl; } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
121
Captulo 6. Concurrencia
6.3.1.
//: C11:UnresponsiveUI.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Lack of threading produces an unresponsive UI. //{L} ZThread #include <iostream> #include <fstream> #include <string> #include "zthread/Thread.h" using namespace std; using namespace ZThread; int main() { cout << "Press <Enter> to quit:" << endl; ifstream file("UnresponsiveUI.cpp"); string line; while(getline(file, line)) { cout << line << endl; Thread::sleep(1000); // Time in milliseconds } // Read input from the console cin.get(); cout << "Shutting down..." << endl; }
//: C11:ResponsiveUI.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Threading for a responsive user interface. //{L} ZThread #include <iostream> #include <fstream> #include <string> #include "zthread/Thread.h" using namespace ZThread; using namespace std; class DisplayTask : public Runnable { ifstream in; string line; bool quitFlag; public: DisplayTask(const string& file) : quitFlag(false) { in.open(file.c_str()); } ~DisplayTask() { in.close(); } void run() { while(getline(in, line) && !quitFlag) { cout << line << endl; Thread::sleep(1000); }
122
//: C11:PoolExecutor.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ZThread #include <iostream> #include "zthread/PoolExecutor.h" #include "LiftOff.h" using namespace ZThread; using namespace std; int main() { try { // Constructor argument is minimum number of threads: PoolExecutor executor(5); for(int i = 0; i < 5; i++)
123
Captulo 6. Concurrencia
executor.execute(new LiftOff(10, i)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:ConcurrentExecutor.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ZThread #include <iostream> #include "zthread/ConcurrentExecutor.h" #include "LiftOff.h" using namespace ZThread; using namespace std; int main() { try { ConcurrentExecutor executor; for(int i = 0; i < 5; i++) executor.execute(new LiftOff(10, i)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:SynchronousExecutor.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ZThread #include <iostream> #include "zthread/SynchronousExecutor.h" #include "LiftOff.h" using namespace ZThread; using namespace std; int main() { try { SynchronousExecutor executor; for(int i = 0; i < 5; i++) executor.execute(new LiftOff(10, i)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
6.3.3.
//: C11:YieldingTask.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved.
124
6.3.4.
//: C11:SleepingTask.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Calling sleep() to pause for awhile. //{L} ZThread #include <iostream> #include "zthread/Thread.h" #include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; class SleepingTask : public Runnable { int countDown; int id; public: SleepingTask(int ident = 0) : countDown(5), id(ident) {}
125
Captulo 6. Concurrencia
~SleepingTask() { cout << id << " completed" << endl; } friend ostream& operator<<(ostream& os, const SleepingTask& st) { return os << "#" << st.id << ": " << st.countDown; } void run() { while(true) { try { cout << *this << endl; if(--countDown == 0) return; Thread::sleep(100); } catch(Interrupted_Exception& e) { cerr << e.what() << endl; } } } }; int main() { try { ThreadedExecutor executor; for(int i = 0; i < 5; i++) executor.execute(new SleepingTask(i)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
6.3.5. Prioridad
//: C11:SimplePriorities.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Shows the use of thread priorities. //{L} ZThread #include <iostream> #include "zthread/Thread.h" using namespace ZThread; using namespace std; const double pi = 3.14159265358979323846; const double e = 2.7182818284590452354; class SimplePriorities : public Runnable { int countDown; volatile double d; // No optimization int id; public: SimplePriorities(int ident=0): countDown(5), id(ident) {} ~SimplePriorities() { cout << id << " completed" << endl; } friend ostream& operator<<(ostream& os, const SimplePriorities& sp) { return os << "#" << sp.id << " priority: " << Thread().getPriority() << " count: "<< sp.countDown;
126
127
Captulo 6. Concurrencia
count->increment(); } } }; int main() { cout << "This will cause a segmentation fault!" << endl; Count count; try { Thread t0(new Incrementer(&count)); Thread t1(new Incrementer(&count)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:ReferenceCounting.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // A CountedPtr prevents too-early destruction. //{L} ZThread #include <iostream> #include "zthread/Thread.h" #include "zthread/CountedPtr.h" using namespace ZThread; using namespace std; class Count { enum { SZ = 100 }; int n[SZ]; public: void increment() { for(int i = 0; i < SZ; i++) n[i]++; } }; class Incrementer : public Runnable { CountedPtr<Count> count; public: Incrementer(const CountedPtr<Count>& c ) : count(c) {} void run() { for(int n = 100; n > 0; n--) { Thread::sleep(250); count->increment(); } } }; int main() { CountedPtr<Count> count(new Count); try { Thread t0(new Incrementer(count)); Thread t1(new Incrementer(count)); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
128
//: C11:EvenGenerator.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt,
129
Captulo 6. Concurrencia
// distributed with the code package available at www.MindView.net. // When threads collide. //{L} ZThread #include <iostream> #include "EvenChecker.h" #include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; class EvenGenerator : public Generator { unsigned int currentEvenValue; // Unsigned cant overflow public: EvenGenerator() { currentEvenValue = 0; } ~EvenGenerator() { cout << "~EvenGenerator" << endl; } int nextValue() { ++currentEvenValue; // Danger point here! ++currentEvenValue; return currentEvenValue; } }; int main() { EvenChecker::test<EvenGenerator>(); }
130
//: C11:TemporaryUnlocking.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Temporarily unlocking another guard. //{L} ZThread #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" using namespace ZThread; class TemporaryUnlocking { Mutex lock; public: void f() { Guard<Mutex> g(lock);
131
Captulo 6. Concurrencia
// lock is acquired // ... { Guard<Mutex, UnlockedScope> h(g); // lock is released // ... // lock is acquired } // ... // lock is released } }; int main() { TemporaryUnlocking t; t.f(); }
//: C11:TimedLocking.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Limited time locking. //{L} ZThread #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" using namespace ZThread; class TimedLocking { Mutex lock; public: void f() { Guard<Mutex, TimedLockedScope<500> > g(lock); // ... } }; int main() { TimedLocking t; t.f(); }
//: C11:SynchronizedClass.cpp {-dmc} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ZThread #include "zthread/GuardedClass.h" using namespace ZThread; class MyClass { public: void func1() {} void func2() {} };
132
133
Captulo 6. Concurrencia
} friend ostream& operator<<(ostream& os, Accessor& a) { return os << "#" << a.id << ": " << a.tlv->get(); } }; int main() { cout << "Press <Enter> to quit" << endl; try { CountedPtr<ThreadLocalVariables> tlv(new ThreadLocalVariables); const int SZ = 5; ThreadedExecutor executor; for(int i = 0; i < SZ; i++) executor.execute(new Accessor(tlv, i)); cin.get(); tlv->cancel(); // All Accessors will quit } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
134
135
Captulo 6. Concurrencia
ostringstream os; os << *this << " Total: " << count->increment() << endl; display->output(os); } Thread::sleep(100); } waitingForCancel = true; while(!count->isCanceled()) // Hold here... Thread::sleep(100); ostringstream os; os << "Terminating " << *this << endl; display->output(os); } int getValue() { while(count->isPaused() && !waitingForCancel) Thread::sleep(100); return number; } friend ostream& operator<<(ostream& os, const Entrance& e) { return os << "Entrance " << e.id << ": " << e.number; } }; int main() { srand(time(0)); // Seed the random number generator cout << "Press <ENTER> to quit" << endl; CountedPtr<Count> count(new Count); vector<Entrance*> v; CountedPtr<Display> display(new Display); const int SZ = 5; try { ThreadedExecutor executor; for(int i = 0; i < SZ; i++) { Entrance* task = new Entrance(count, display, i); executor.execute(task); // Save the pointer to the task: v.push_back(task); } cin.get(); // Wait for user to press <Enter> count->pause(); // Causes tasks to stop counting int sum = 0; vector<Entrance*>::iterator it = v.begin(); while(it != v.end()) { sum += (*it)->getValue(); ++it; } ostringstream os; os << "Total: " << count->value() << endl << "Sum of Entrances: " << sum << endl; display->output(os); count->cancel(); // Causes threads to quit } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:Interrupting.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Interrupting a blocked thread. //{L} ZThread #include <iostream> #include "zthread/Thread.h" using namespace ZThread; using namespace std; class Blocked : public Runnable { public: void run() { try { Thread::sleep(1000); cout << "Waiting for get() in run():"; cin.get(); } catch(Interrupted_Exception&) { cout << "Caught Interrupted_Exception" << endl; // Exit the task } } }; int main(int argc, char* argv[]) { try { Thread t(new Blocked); if(argc > 1) Thread::sleep(1100); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:Interrupting2.cpp // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Interrupting a thread blocked // with a synchronization guard. //{L} ZThread #include <iostream> #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" using namespace ZThread; using namespace std; class BlockedMutex { Mutex lock; public: BlockedMutex() { lock.acquire(); } void f() { Guard<Mutex> g(lock); // This will never be available
137
Captulo 6. Concurrencia
} }; class Blocked2 : public Runnable { BlockedMutex blocked; public: void run() { try { cout << "Waiting for f() in BlockedMutex" << endl; blocked.f(); } catch(Interrupted_Exception& e) { cerr << e.what() << endl; // Exit the task } } }; int main(int argc, char* argv[]) { try { Thread t(new Blocked2); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:Interrupting3.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // General idiom for interrupting a task. //{L} ZThread #include <iostream> #include "zthread/Thread.h" using namespace ZThread; using namespace std; const double PI = 3.14159265358979323846; const double E = 2.7182818284590452354; class NeedsCleanup { int id; public: NeedsCleanup(int ident) : id(ident) { cout << "NeedsCleanup " << id << endl; } ~NeedsCleanup() { cout << "~NeedsCleanup " << id << endl; } }; class Blocked3 : public Runnable { volatile double d; public: Blocked3() : d(0.0) {} void run() { try { while(!Thread::interrupted()) { point1: NeedsCleanup n1(1); cout << "Sleeping" << endl;
138
139
Captulo 6. Concurrencia
waxOn = true; // Ready to buff condition.signal(); } void buffed() { Guard<Mutex> g(lock); waxOn = false; // Ready for another coat of wax condition.signal(); } void waitForWaxing() { Guard<Mutex> g(lock); while(waxOn == false) condition.wait(); } void waitForBuffing() { Guard<Mutex> g(lock); while(waxOn == true) condition.wait(); } }; class WaxOn : public Runnable { CountedPtr<Car> car; public: WaxOn(CountedPtr<Car>& c) : car(c) {} void run() { try { while(!Thread::interrupted()) { cout << "Wax On!" << endl; Thread::sleep(200); car->waxed(); car->waitForBuffing(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Ending Wax On process" << endl; } }; class WaxOff : public Runnable { CountedPtr<Car> car; public: WaxOff(CountedPtr<Car>& c) : car(c) {} void run() { try { while(!Thread::interrupted()) { car->waitForWaxing(); cout << "Wax Off!" << endl; Thread::sleep(200); car->buffed(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Ending Wax Off process" << endl; } }; int main() { cout << "Press <Enter> to quit" << endl; try { CountedPtr<Car> car(new Car); ThreadedExecutor executor; executor.execute(new WaxOff(car)); executor.execute(new WaxOn(car)); cin.get(); executor.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl;
140
141
Captulo 6. Concurrencia
Mutex lock; Condition toastReady; CountedPtr<Jammer> jammer; bool gotToast; int buttered; public: Butterer(CountedPtr<Jammer>& j) : toastReady(lock), jammer(j) { gotToast = false; buttered = 0; } void moreToastReady() { Guard<Mutex> g(lock); gotToast = true; toastReady.signal(); } void run() { try { while(!Thread::interrupted()) { { Guard<Mutex> g(lock); while(!gotToast) toastReady.wait(); ++buttered; } cout << "Buttering toast " << buttered << endl; jammer->moreButteredToastReady(); { Guard<Mutex> g(lock); gotToast = false; } } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Butterer off" << endl; } }; class Toaster : public Runnable { CountedPtr<Butterer> butterer; int toasted; public: Toaster(CountedPtr<Butterer>& b) : butterer(b) { toasted = 0; } void run() { try { while(!Thread::interrupted()) { Thread::sleep(rand()/(RAND_MAX/5)*100); // ... // Create new toast // ... cout << "New toast " << ++toasted << endl; butterer->moreToastReady(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Toaster off" << endl; } }; int main() { srand(time(0)); // Seed the random number generator try { cout << "Press <Return> to quit" << endl; CountedPtr<Jammer> jammer(new Jammer); CountedPtr<Butterer> butterer(new Butterer(jammer));
142
//: C11:TestTQueue.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. //{L} ZThread #include <string> #include <iostream> #include "TQueue.h" #include "zthread/Thread.h"
143
Captulo 6. Concurrencia
#include "LiftOff.h" using namespace ZThread; using namespace std; class LiftOffRunner : public Runnable { TQueue<LiftOff*> rockets; public: void add(LiftOff* lo) { rockets.put(lo); } void run() { try { while(!Thread::interrupted()) { LiftOff* rocket = rockets.get(); rocket->run(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Exiting LiftOffRunner" << endl; } }; int main() { try { LiftOffRunner* lor = new LiftOffRunner; Thread t(lor); for(int i = 0; i < 5; i++) lor->add(new LiftOff(10, i)); cin.get(); lor->add(new LiftOff(10, 99)); cin.get(); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:ToastOMaticMarkII.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Solving the problems using TQueues. //{L} ZThread #include <iostream> #include <string> #include <cstdlib> #include <ctime> #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" #include "zthread/Condition.h" #include "zthread/ThreadedExecutor.h" #include "TQueue.h" using namespace ZThread; using namespace std; class Toast { enum Status { DRY, BUTTERED, JAMMED }; Status status; int id; public: Toast(int idn) : status(DRY), id(idn) {} #ifdef __DMC__ // Incorrectly requires default Toast() { assert(0); } // Should never be called
144
145
Captulo 6. Concurrencia
: butteredQueue(buttered), finishedQueue(finished) {} void run() { try { while(!Thread::interrupted()) { // Blocks until next piece of toast is available: Toast t = butteredQueue->get(); t.jam(); cout << t << endl; finishedQueue->put(t); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Jammer off" << endl; } }; // Consume the toast: class Eater : public Runnable { ToastQueue finishedQueue; int counter; public: Eater(ToastQueue& finished) : finishedQueue(finished), counter(0) {} void run() { try { while(!Thread::interrupted()) { // Blocks until next piece of toast is available: Toast t = finishedQueue->get(); // Verify that the toast is coming in order, // and that all pieces are getting jammed: if(t.getId() != counter++ || t.getStatus() != "jammed") { cout << ">>>> Error: " << t << endl; exit(1); } else cout << "Chomp! " << t << endl; } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Eater off" << endl; } }; int main() { srand(time(0)); // Seed the random number generator try { ToastQueue dryQueue(new TQueue<Toast>), butteredQueue(new TQueue<Toast>), finishedQueue(new TQueue<Toast>); cout << "Press <Return> to quit" << endl; ThreadedExecutor executor; executor.execute(new Toaster(dryQueue)); executor.execute(new Butterer(dryQueue,butteredQueue)); executor.execute( new Jammer(butteredQueue, finishedQueue)); executor.execute(new Eater(finishedQueue)); cin.get(); executor.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
146
6.6.4. Broadcast
//: C11:CarBuilder.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // How broadcast() works. //{L} ZThread #include <iostream> #include <string> #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" #include "zthread/Condition.h" #include "zthread/ThreadedExecutor.h" #include "TQueue.h" using namespace ZThread; using namespace std; class Car { int id; bool engine, driveTrain, wheels; public: Car(int idn) : id(idn), engine(false), driveTrain(false), wheels(false) {} // Empty Car object: Car() : id(-1), engine(false), driveTrain(false), wheels(false) {} // Unsynchronized -- assumes atomic bool operations: int getId() { return id; } void addEngine() { engine = true; } bool engineInstalled() { return engine; } void addDriveTrain() { driveTrain = true; } bool driveTrainInstalled() { return driveTrain; } void addWheels() { wheels = true; } bool wheelsInstalled() { return wheels; } friend ostream& operator<<(ostream& os, const Car& c) { return os << "Car " << c.id << " [" << " engine: " << c.engine << " driveTrain: " << c.driveTrain << " wheels: " << c.wheels << " ]"; } }; typedef CountedPtr< TQueue<Car> > CarQueue; class ChassisBuilder : public Runnable { CarQueue carQueue; int counter; public: ChassisBuilder(CarQueue& cq) : carQueue(cq),counter(0) {} void run() { try { while(!Thread::interrupted()) { Thread::sleep(1000); // Make chassis: Car c(counter++); cout << c << endl; // Insert into queue carQueue->put(c); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "ChassisBuilder off" << endl;
147
Captulo 6. Concurrencia
} }; class Cradle { Car c; // Holds current car being worked on bool occupied; Mutex workLock, readyLock; Condition workCondition, readyCondition; bool engineBotHired, wheelBotHired, driveTrainBotHired; public: Cradle() : workCondition(workLock), readyCondition(readyLock) { occupied = false; engineBotHired = true; wheelBotHired = true; driveTrainBotHired = true; } void insertCar(Car chassis) { c = chassis; occupied = true; } Car getCar() { // Can only extract car once if(!occupied) { cerr << "No Car in Cradle for getCar()" << endl; return Car(); // "Null" Car object } occupied = false; return c; } // Access car while in cradle: Car* operator->() { return &c; } // Allow robots to offer services to this cradle: void offerEngineBotServices() { Guard<Mutex> g(workLock); while(engineBotHired) workCondition.wait(); engineBotHired = true; // Accept the job } void offerWheelBotServices() { Guard<Mutex> g(workLock); while(wheelBotHired) workCondition.wait(); wheelBotHired = true; // Accept the job } void offerDriveTrainBotServices() { Guard<Mutex> g(workLock); while(driveTrainBotHired) workCondition.wait(); driveTrainBotHired = true; // Accept the job } // Tell waiting robots that work is ready: void startWork() { Guard<Mutex> g(workLock); engineBotHired = false; wheelBotHired = false; driveTrainBotHired = false; workCondition.broadcast(); } // Each robot reports when their job is done: void taskFinished() { Guard<Mutex> g(readyLock); readyCondition.signal(); } // Director waits until all jobs are done: void waitUntilWorkFinished() {
148
149
Captulo 6. Concurrencia
} }; class WheelRobot : public Runnable { CradlePtr cradle; public: WheelRobot(CradlePtr cr) : cradle(cr) {} void run() { try { while(!Thread::interrupted()) { // Blocks until job is offered/accepted: cradle->offerWheelBotServices(); cout << "Installing Wheels" << endl; (*cradle)->addWheels(); cradle->taskFinished(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "WheelRobot off" << endl; } }; class Reporter : public Runnable { CarQueue carQueue; public: Reporter(CarQueue& cq) : carQueue(cq) {} void run() { try { while(!Thread::interrupted()) { cout << carQueue->get() << endl; } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Reporter off" << endl; } }; int main() { cout << "Press <Enter> to quit" << endl; try { CarQueue chassisQueue(new TQueue<Car>), finishingQueue(new TQueue<Car>); CradlePtr cradle(new Cradle); ThreadedExecutor assemblyLine; assemblyLine.execute(new EngineRobot(cradle)); assemblyLine.execute(new DriveTrainRobot(cradle)); assemblyLine.execute(new WheelRobot(cradle)); assemblyLine.execute( new Director(chassisQueue, finishingQueue, cradle)); assemblyLine.execute(new Reporter(finishingQueue)); // Start everything running by producing chassis: assemblyLine.execute(new ChassisBuilder(chassisQueue)); cin.get(); assemblyLine.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
150
151
Captulo 6. Concurrencia
output("eating"); ZThread::Thread::sleep(randSleepTime()); right.drop(); left.drop(); } } catch(ZThread::Synchronization_Exception& e) { output(e.what()); } } friend std::ostream& operator<<(std::ostream& os, const Philosopher& p) { return os << "Philosopher " << p.id; } }; #endif // DININGPHILOSOPHERS_H
//: C11:DeadlockingDiningPhilosophers.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Dining Philosophers with Deadlock. //{L} ZThread #include <ctime> #include "DiningPhilosophers.h" #include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // Seed the random number generator int ponder = argc > 1 ? atoi(argv[1]) : 5; cout << "Press <ENTER> to quit" << endl; enum { SZ = 5 }; try { CountedPtr<Display> d(new Display); ThreadedExecutor executor; Chopstick c[SZ]; for(int i = 0; i < SZ; i++) { executor.execute( new Philosopher(c[i], c[(i+1) % SZ], d, i,ponder)); } cin.get(); executor.interrupt(); executor.wait(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
//: C11:FixedDiningPhilosophers.cpp {RunByHand} // From "Thinking in C++, Volume 2", by Bruce Eckel & Chuck Allison. // (c) 1995-2004 MindView, Inc. All Rights Reserved. // See source code use permissions stated in the file License.txt, // distributed with the code package available at www.MindView.net. // Dining Philosophers without Deadlock. //{L} ZThread #include <ctime> #include "DiningPhilosophers.h"
152
6.8. Resumen
#include "zthread/ThreadedExecutor.h" using namespace ZThread; using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // Seed the random number generator int ponder = argc > 1 ? atoi(argv[1]) : 5; cout << "Press <ENTER> to quit" << endl; enum { SZ = 5 }; try { CountedPtr<Display> d(new Display); ThreadedExecutor executor; Chopstick c[SZ]; for(int i = 0; i < SZ; i++) { if(i < (SZ-1)) executor.execute( new Philosopher(c[i], c[i + 1], d, i, ponder)); else executor.execute( new Philosopher(c[0], c[i], d, i, ponder)); } cin.get(); executor.interrupt(); executor.wait(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } }
153