Vous êtes sur la page 1sur 670

Programao Orientada para Objectos

Manuel Menezes de Sequeira 18 de Fevereiro de 2004

Who are the learned? They who practice what they know. Muhammad, The Sayings of Muhammad, Allama Sir Abdullah e Al-Mamun Al-Suhrawardy, editores, 10 (1949)

Contedo
Prefcio 0.1 1 xi Exerccios sobre classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii 1 1 2 3 4 8

Introduo Programao 1.1 1.2 1.3 Computadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Algoritmos: resolvendo problemas . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 1.3.2 1.4 1.5 Regras do jogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desenvolvimento e demonstrao de correco . . . . . . . . . . . . . . .

Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Resumo: resoluo de problemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 19

Conceitos bsicos de programao 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.2.2 2.2.3 2.3 2.3.1 2.3.2 2.3.3

Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Consola e canais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Denio de variveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Controlo de uxo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Memria e inicializao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Identicadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Inicializao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Tipos aritmticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 Booleanos ou lgicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 i

Variveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

Tipos bsicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

ii 2.4 2.5 2.6 2.7

CONTEDO
Valores literais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 Instncias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Expresses e operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 Operadores aritmticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Operadores relacionais e de igualdade . . . . . . . . . . . . . . . . . . . . . 49 Operadores lgicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Operadores bit-a-bit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Operadores de atribuio . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Operadores de incrementao e decrementao . . . . . . . . . . . . . . . 54 Precedncia e associatividade . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Efeitos laterais e mau comportamento . . . . . . . . . . . . . . . . . . . . . 55 Ordem de clculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 59

Modularizao: rotinas 3.1 3.2

Introduo modularizao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Funes e procedimentos: rotinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 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 Abordagens descendente e ascendente . . . . . . . . . . . . . . . . . . . . 63 Denio de rotinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 Sintaxe das denies de funes . . . . . . . . . . . . . . . . . . . . . . . . 73 Contrato e documentao de uma rotina . . . . . . . . . . . . . . . . . . . 74 Integrao da funo no programa . . . . . . . . . . . . . . . . . . . . . . . 76 Sintaxe e semntica da invocao ou chamada . . . . . . . . . . . . . . . . 77 Parmetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Retorno e devoluo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

3.2.10 Signicado de void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.2.11 Passagem de argumentos por valor e por referncia . . . . . . . . . . . . . 81 3.2.12 Variveis locais e globais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 3.2.13 Blocos de instrues ou instrues compostas . . . . . . . . . . . . . . . . 89 3.2.14 mbito ou visibilidade de variveis . . . . . . . . . . . . . . . . . . . . . . 90 3.2.15 Durao ou permanncia de variveis . . . . . . . . . . . . . . . . . . . . . 93 3.2.16 Nomes de rotinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94

CONTEDO

iii

3.2.17 Declarao vs. denio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 3.2.18 Parmetros constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 3.2.19 Instrues de assero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 3.2.20 Melhorando mdulos j produzidos . . . . . . . . . . . . . . . . . . . . . . 112 3.3 3.4 3.5 3.6 4 Rotinas recursivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 Mecanismo de invocao de rotinas . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Sobrecarga de nomes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 Parmetros com argumentos por omisso . . . . . . . . . . . . . . . . . . . . . . . 126 129

Controlo do uxo dos programas 4.1 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4 4.4.1 4.4.2 4.5

Instrues de seleco . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 As instrues if e if else . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 Instrues de seleco encadeadas . . . . . . . . . . . . . . . . . . . . . . . 133 Problemas comuns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 Deduo de asseres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 Predicados mais fortes e mais fracos . . . . . . . . . . . . . . . . . . . . . . 139 Deduo da pr-condio mais fraca de uma atribuio . . . . . . . . . . . 139 Asseres em instrues de seleco . . . . . . . . . . . . . . . . . . . . . . 141 Escolha das instrues alternativas . . . . . . . . . . . . . . . . . . . . . . . 147 Determinao das pr-condies mais fracas . . . . . . . . . . . . . . . . . 148 Determinao das guardas . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 Vericao das guardas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 Escolha das condies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 Alterando a soluo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 Discusso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Outro exemplo de desenvolvimento . . . . . . . . . . . . . . . . . . . . . . 156 O operador ? : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 A instruo switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160

Asseres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

Desenvolvimento de instrues de seleco . . . . . . . . . . . . . . . . . . . . . . 146

Variantes das instrues de seleco . . . . . . . . . . . . . . . . . . . . . . . . . . 159

Instrues de iterao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164

iv 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.7 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 5

CONTEDO
A instruo de iterao while . . . . . . . . . . . . . . . . . . . . . . . . . 165 Variantes do ciclo while: for e do while . . . . . . . . . . . . . . . . . . 166 Exemplo simples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 return, break, continue, e goto em ciclos . . . . . . . . . . . . . . . . 175 Problemas comuns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 Somas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 Produtos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 Conjunes e o quanticador universal . . . . . . . . . . . . . . . . . . . . 186 Disjunes e o quanticador existencial . . . . . . . . . . . . . . . . . . . . 187 Contagens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 O resto da diviso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 Noo de invariante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 Correco de ciclos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 Melhorando a funo potncia() . . . . . . . . . . . . . . . . . . . . . . 202 Metodologia de Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 Um exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Outro exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 235

Asseres com quanticadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183

Desenvolvimento de ciclos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189

Matrizes, vectores e outros agregados 5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.2 5.2.1 5.2.2 5.2.3

Matrizes clssicas do C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 Denio de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 Indexao de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 Inicializao de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 Matrizes multidimensionais . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 Matrizes constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242 Matrizes como parmetros de rotinas . . . . . . . . . . . . . . . . . . . . . 242 Restries na utilizao de matrizes . . . . . . . . . . . . . . . . . . . . . . 247 Denio de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Indexao de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Inicializao de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250

Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248

CONTEDO
5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9

v Operaes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 Acesso aos itens de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 Alterao da dimenso de um vector . . . . . . . . . . . . . . . . . . . . . 252 Insero e remoo de itens . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 Vectores multidimensionais? . . . . . . . . . . . . . . . . . . . . . . . . . . 254 Vectores constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 . . . . . . . . . . . . . . . . . . . . . 255

5.2.10 Vectores como parmetros de rotinas

5.2.11 Passagem de argumentos por referncia constante . . . . . . . . . . . . . . 257 5.2.12 Outras operaes com vectores . . . . . . . . . . . . . . . . . . . . . . . . . 261 5.3 Algoritmos com matrizes e vectores . . . . . . . . . . . . . . . . . . . . . . . . . . 262 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8 5.4 5.4.1 5.4.2 6 Soma dos elementos de uma matriz . . . . . . . . . . . . . . . . . . . . . . 262 Soma dos itens de um vector . . . . . . . . . . . . . . . . . . . . . . . . . . 266 ndice do maior elemento de uma matriz . . . . . . . . . . . . . . . . . . . 266 ndice do maior item de um vector . . . . . . . . . . . . . . . . . . . . . . . 272 Elementos de uma matriz num intervalo . . . . . . . . . . . . . . . . . . . 272 Itens de um vector num intervalo . . . . . . . . . . . . . . . . . . . . . . . 277 Segundo elemento de uma matriz com um dado valor . . . . . . . . . . . 278 Segundo item de um vector com um dado valor . . . . . . . . . . . . . . . 286 Cadeias de caracteres clssicas . . . . . . . . . . . . . . . . . . . . . . . . . 289 A classe string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 297

Cadeias de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288

Tipos enumerados 6.1

Sobrecarga de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 301 . . . . . . . . . . . . . . . . . . . . . . . 307

Tipos abstractos de dados e classes C++ 7.1 7.2 Tipos Abstractos de Dados e classes C++ 7.2.1 7.2.2 7.2.3 7.2.4 7.3

De novo a soma de fraces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 Denio de TAD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308 Acesso aos membros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 Alguma nomenclatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 Operaes suportadas pelas classes C++ . . . . . . . . . . . . . . . . . . . 312

Representao de racionais por fraces . . . . . . . . . . . . . . . . . . . . . . . . 312

vi 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.6 7.7

CONTEDO
Operaes aritmticas elementares . . . . . . . . . . . . . . . . . . . . . . . 313 Canonicidade do resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . 313 Aplicao soma de fraces . . . . . . . . . . . . . . . . . . . . . . . . . . 314 Encapsulamento e categorias de acesso . . . . . . . . . . . . . . . . . . . . 318 Rotinas membro: operaes e mtodos . . . . . . . . . . . . . . . . . . . . 319 Construtores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326 Construtores por cpia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332 Condio invariante de classe . . . . . . . . . . . . . . . . . . . . . . . . . . 332 Porqu o formato cannico das fraces? . . . . . . . . . . . . . . . . . . . 334 Explicitao da condio invariante de classe . . . . . . . . . . . . . . . . . 335

Classes C++ como mdulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325

Sobrecarga de operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343 Testes de unidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 Devoluo por referncia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 7.7.1 7.7.2 7.7.3 Mais sobre referncias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 Operadores ++ e -- prexo . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 Operadores ++ e -- suxo . . . . . . . . . . . . . . . . . . . . . . . . . . . 361 Operadores de atribuio especiais . . . . . . . . . . . . . . . . . . . . . . . 364 Operadores aritmticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 Valores literais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370 Converses implcitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 Sobrecarga de operadores: operaes ou rotinas? . . . . . . . . . . . . . . 372

7.8

Mais operadores para o TAD Racional . . . . . . . . . . . . . . . . . . . . . . . . 364 7.8.1 7.8.2

7.9

Construtores: converses implcitas e valores literais . . . . . . . . . . . . . . . . . 370 7.9.1 7.9.2 7.9.3

7.10 Operadores igualdade, diferena e relacionais . . . . . . . . . . . . . . . . . . . . . 372 7.10.1 Inspectores e interrogaes . . . . . . . . . . . . . . . . . . . . . . . . . . . 373 7.10.2 Operadores de igualdade e diferena . . . . . . . . . . . . . . . . . . . . . 375 7.10.3 Operadores relacionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376 7.11 Constncia: vericando erros durante a compilao . . . . . . . . . . . . . . . . . 377 7.11.1 Passagem de argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378 7.11.2 Constantes implcitas: operaes constantes . . . . . . . . . . . . . . . . . 383 7.11.3 Devoluo por valor constante . . . . . . . . . . . . . . . . . . . . . . . . . 387

CONTEDO

vii

7.11.4 Devoluo por referncia constante . . . . . . . . . . . . . . . . . . . . . . 390 7.12 Reduzindo o nmero de invocaes com inline . . . . . . . . . . . . . . . . . . 391 7.13 Optimizao dos clculos com racionais . . . . . . . . . . . . . . . . . . . . . . . . 395 7.13.1 Adio e subtraco . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396 7.13.2 Multiplicao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399 7.13.3 Diviso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400 7.13.4 Simtrico e identidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401 7.13.5 Operaes de igualdade e relacionais . . . . . . . . . . . . . . . . . . . . . 401 7.13.6 Operadores especiais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402 7.14 Operadores de insero e extraco . . . . . . . . . . . . . . . . . . . . . . . . . . . 403 7.14.1 Sobrecarga do operador < < . . . . . . . . . . . . . . . . . . . . . . . . . . . 404 7.14.2 Sobrecarga do operador > > . . . . . . . . . . . . . . . . . . . . . . . . . . . 407 7.14.3 Lidando com erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408 7.14.4 Coerncia entre os operadores < < e > > . . . . . . . . . . . . . . . . . . . . 411 7.14.5 Leitura e escrita de cheiros . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 7.15 Amizades e promiscuidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 7.15.1 Rotinas amigas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 7.15.2 Classes amigas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 7.15.3 Promiscuidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 7.16 Cdigo completo do TAD Racional . . . . . . . . . . . . . . . . . . . . . . . . . . 420 7.17 Outros assuntos acerca de classes C++ . . . . . . . . . . . . . . . . . . . . . . . . . 434 7.17.1 Constantes membro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434 7.17.2 Membros de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435 7.17.3 Destrutores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437 7.17.4 De novo os membros de classe . . . . . . . . . . . . . . . . . . . . . . . . . 439 7.17.5 Construtores por omisso . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440 7.17.6 Matrizes de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443 7.17.7 Converses para outros tipos . . . . . . . . . . . . . . . . . . . . . . . . . . 443 7.17.8 Uma aplicao mais til das converses . . . . . . . . . . . . . . . . . . . . 446 8 Programao baseada em objectos 8.1 449

Desenho de classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451

viii 9 Modularizao de alto nvel 9.1 9.2 9.1.1 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.4.6 9.4.7 9.4.8 9.4.9

CONTEDO
453

Modularizao fsica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455 Constituio de um mdulo . . . . . . . . . . . . . . . . . . . . . . . . . . . 456 . . . . . . . . . . . . . . . . . . . . . . 456 Pr-processamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457 Compilao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464 Fuso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466 Arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 Vantagens de restringir a ligao dos nomes . . . . . . . . . . . . . . . . . 474 Espaos nominativos sem nome . . . . . . . . . . . . . . . . . . . . . . . . 477 Ligao de rotinas, variveis e constantes . . . . . . . . . . . . . . . . . . . 479 Ligao de classes C++ e tipos enumerados . . . . . . . . . . . . . . . . . . 486 Relao entre interface e implementao . . . . . . . . . . . . . . . . . . . 491 Fases da construo do cheiro executvel

Ligao dos nomes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473

Contedo dos cheiros de interface e implementao . . . . . . . . . . . . . . . . 490 Ferramentas de utilidade interna ao mdulo . . . . . . . . . . . . . . . . . 491 Rotinas no-membro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492 Variveis globais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492 Constantes globais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492 Tipos enumerados no-membro . . . . . . . . . . . . . . . . . . . . . . . . 493 Classes C++ no-membro . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494 Operaes (rotinas membro) . . . . . . . . . . . . . . . . . . . . . . . . . . 494 Atributos de instncia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495

9.4.10 Variveis membro de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . 495 9.4.11 Constantes membro de classe . . . . . . . . . . . . . . . . . . . . . . . . . . 496 9.4.12 Classes C++ membro (embutidas) . . . . . . . . . . . . . . . . . . . . . . . 497 9.4.13 Enumerados membro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497 9.4.14 Evitando erros devido a incluses mltiplas . . . . . . . . . . . . . . . . . 498 9.4.15 Ficheiro auxiliar de implementao . . . . . . . . . . . . . . . . . . . . . . 500 9.5 9.6 Construo automtica do cheiro executvel . . . . . . . . . . . . . . . . . . . . . 500 Modularizao em pacotes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 507 9.6.1 Coliso de nomes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 507

CONTEDO
9.6.2 9.6.3 9.6.4 9.6.5 9.6.6 9.6.7 9.6.8 9.7

ix Espaos nominativos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509 Directivas de utilizao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511 Declaraes de utilizao . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512 Espaos nominativos e modularizao fsica . . . . . . . . . . . . . . . . . 513 Ficheiros de interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515 Ficheiros de implementao e cheiros auxiliares de implementao . . . 515 Pacotes e espaos nominativos . . . . . . . . . . . . . . . . . . . . . . . . . 516

Exemplo nal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517 523

10 Listas e iteradores

10.1 Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523 10.1.1 Operaes com listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524 10.2 Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527 10.2.1 Operaes com iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528 10.2.2 Operaes se podem realizar com iteradores e listas . . . . . . . . . . . . . 529 10.2.3 Itens ctcios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530 10.2.4 Operaes que invalidam os iteradores . . . . . . . . . . . . . . . . . . . . 532 10.2.5 Concluso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533 10.3 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535 10.3.1 Interface de ListaDeInt . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535 10.3.2 Interface de ListaDeInt::Iterador . . . . . . . . . . . . . . . . . . . . 540 10.3.3 Usando a interface das novas classes . . . . . . . . . . . . . . . . . . . . . . 543 10.3.4 Teste dos mdulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545 10.4 Implementao simplista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547 10.4.1 Implementao de ListaDeInt . . . . . . . . . . . . . . . . . . . . . . . . 547 10.4.2 Implementao de ListaDeInt::Iterador . . . . . . . . . . . . . . . . 550 10.4.3 Implementao dos mtodos pblicos de ListaDeInt . . . . . . . . . . . 552 10.4.4 Implementao dos mtodos pblicos de ListaDeInt::Iterador . . . 556 10.5 Uma implementao mais eciente . . . . . . . . . . . . . . . . . . . . . . . . . . . 558 10.5.1 Cadeias ligadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 559 11 Ponteiros e variveis dinmicas 12 Herana e polimorsmo 563 565

x 13 Programao genrica 14 Excepes e tratamento de erros A Notao e smbolos

CONTEDO
567 569 587

A.1 Notao e smbolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587 A.2 Abreviaturas e acrnimos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 590 B Um pouco de lgica C Curiosidades e fenmenos estranhos C.1.1 591 593

C.1 Inicializao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 593 Inicializao de membros . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594 C.2 Rotinas locais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594 C.3 Membros acessveis s para leitura . . . . . . . . . . . . . . . . . . . . . . . . . . 595 C.4 Variveis virtuais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597 C.5 Persistncia simplicada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 603 D Nomes e seu formato: recomendaes E Palavras-chave do C++ F Precedncia e associatividade no C++ G Tabelas de codicao ISO-8859-1 (Latin-1) e ISO-8859-15 (Latin-9) H Listas e iteradores: listagens 615 617 621 625 633

H.1 Verso simplista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633 H.1.1 Ficheiro de interface: lista_int.H . . . . . . . . . . . . . . . . . . . . . . 633 H.1.2 Ficheiro de implementao auxiliar: lista_int_impl.H . . . . . . . . . 638 H.1.3 Ficheiro de implementao: lista_int.C . . . . . . . . . . . . . . . . . . 643 I Listas e iteradores seguros 651

Prefcio
Colocar aqui ideias sobre evoluo das folhas: Notas marginais. Notas de p de pgina. Minipages para Saber mais. Minipages para Curiosidades. Minipages para Exerccios de auto-avaliao. !!Vou em 3.7.9! 2003/12/4 !!Ateno! Digo que comum no usar const por valor nas declaraes. Mas a verdade que, sem o usar, na condio objectivo no h forma de garantir que no mudaram de valor! !!Retirar referncias a Arquitectura de Computadores e Sistemas Operativos e CA e LP. !!Rever os captulos posteriores ao das classes! CII devia ser CIC, etc. !!Rever todo o texto de modo a usar instncia no sentido de varivel ou constante! !!Avisar para os exemplos de sintaxe: no os copiar. Pr algures agradecimentos aos colegas e alunos. !!Terminar ndice! !!Nas asseres (controlo de uxo), distinguir claramente entre demonstrao directa e inversa (ver resumo das aulas prticas em HTML). !!Substituir idntico por igual excepto onde se pretender dizer a mesma entidade. !!Deixar claro que PBO e POO o nfase no so os algoritmos: a modelao. O que existe e que propriedades tem. E isso tambm resolver problemas. Neste texto far-se- uma (discutvel) abordagem histrica da programao: primeiro o nfase sero os algoritmos (programao procedimental estruturada), depois os tipos de dados (programao baseada em objectos) e nalmente as entidades existentes no problema e suas relaes e comportamento (programao orientada para objectos). !!Eliminar todos os !! !!Vericar ortograa. xi

xii !!Nas classes usar CII logo desde o incio. !!Nas classes introduzir noo de inspector cedo.

PREFCIO

!!Herana: usar tipo esttico vs. tipo dinmico. um, mais rigor: Funciona como um, mais rigor: Usvel como um. !!Herana privada: s se for necessrio fazer sobreposio de operao polimrca ou se for necessrio acesso protegido. !!Tem um pode signicar implementada custa de. !!Herana (Exceptional C++): mtodos no exigem mais, nem prometem menos. !!Em ADT deve-se denir sempre um swap externo. !!Usar PilhaDeInt e PilhaDe<T>. !!Ser sistemtico na devoluo de constantes em rotinas ou mtodos que devolvam instncias de classes sem ser por referncia (ou em modelos, quanto o tipo devolvido for parmetro do modelo, pois pode vir a ser uma classe). !!Polticas de gesto de objectos dinmicos: quem constri destri (bom para composio, mau para agregao). Usar auto_ptr sem transferncia de posse, por exemplo. quem tem destri (bom para agregao, mas no permite referncias mltiplas). Usar auto_ptr com transferncia de posse. ltimo fecha a porta (agregao com posse partilhada). Ponteiros espertos com contagem de referncias. !!Vericar se material sobre canais existe nas folhas o suciente. Captulo ou espalhado pelos captulos? !!Fazer breve demonstrao da preservao do invariante nos mtodos desenvolvidos para a classe Racional? til? !!No captulo de sobre modularizao fsica, fazer guras UML? Seria til, particularmente para o exemplo nal. Captulo 2: !! Explicar algures int() e o que so variveis temporrias (sem nome!). UML: objecto sem nome! Captulo 3: !!Ateno! Usar nomes de funes como absolutoDe(). Corrigir tudo. !!Usar a expresso parmetros com argumentos por omisso. Colocar na modularizao! !!Na modularizao referir que normalmente se pretendem mdulos com PC fraca e CO forte. !! Aqui deveriam vir melhores recomendaes! Ver erros mais frequentes do Problema 1. !! Ateno s guras! Os valores devolvidos devem ser objectos UML sem nome!!!! Variveis temporrias!

0.1. EXERCCIOS SOBRE CLASSES


Captulo 7: !! Discutir comentrios de documentao: ver nos erros mais frequentes. Captulo 9: !! Usar como exemplo a classe dos racionais dividida em cheiros! !! Dizer que modularizao fsica acelera o desenvolvimento.

xiii

!! using namespace X nunca em .H ou _impl.H. Nunca em .C se o pacote pertencer ao espao X! !! Em X.H de namespace Y usar #ifndef Y_X_H, e por X.H em Y/X.H.

0.1 Exerccios sobre classes


3. O clculo da raiz quadrada de um valor v pode ser feito, de uma forma algo ineciente, procurando a soluo da equao f(x) = v, onde f(x) := x2 (em que se usou a notao := com o signicado denida por), usando o mtodo de Newton. Este mtodo advoga que se deve construir uma sequncia r0, r1, ... de razes denida, de uma forma recorrente, por rn+1 = rn - (rn2 - v) / (2rn). Esta sequncia converge para a raiz quadrada (positiva) de v quando n tende para innito, desde que o valor inicial r0 seja positivo. Escreva uma funo Racional raizQuadrada(Racional v) que calcule um valor racional que aproxime a raiz quadrada do racional v. Considere a aproximao razovel quando a diferena entre termos sucessivos da sequncia for inferior a 1/100 (ou seja, quando |rn+1 - rn| = |(rn2 - v) / (2rn)| < 1/100). Se procurar a raiz quadrada de 2 usando o mtodo sugerido, chegar surpreendente fraco 577/408, que uma excelente aproximao (577/408 = 1,41421568..., cujo quadrado 2,000006007...). 4. [difcil] Os inteiros so limitados, pelo que, em mquinas em que os int so representados em complemento para 2 e tm 32 bits, o valor racional mais pequeno (em mdulo) representvel pela classe Racional tal como desenvolvida 2-32 (aproximadamente 10-10) e o maior (em mdulo) 232 (aproximadamente 1010). Se se pretendesse tornar a classe dos racionais verdadeiramente til, seria necessrio estender a gama dos int consideravelmente. No sendo isso possvel sem mudar de linguagem, compilador, sistema operativo e/ou computador, a soluo pode passar por construir uma nova classe Inteiro, custa da qual a classe Racional pode ser construda, e que represente inteiros de preciso (virtualmente) arbitrria. Construa essa classe, usando sequncias de int para representar os inteiros de preciso arbitrria. Pode ter de usar memria dinmica, a ensinar posteriormente.

xiv

PREFCIO

Captulo 1

Introduo Programao
It has often been said that a person does not really understand something until after teaching it to someone else. Actually, a person does not really understand something until after teaching it to a computer (...) Donald E. Knuth, Selected Papers in Computer Science, 10 (1996)

1.1 Computadores
O que um computador? As respostas que surgem com mais frequncia so que um computador um conjunto de circuitos integrados, uma ferramenta ou uma mquina programvel. Todas estas respostas so verdadeiras, at certo ponto. No entanto, um rdio tambm um conjunto de circuitos integrados, um martelo uma ferramenta, e uma mquina de lavar roupa uma mquina programvel... Algo mais ter de se usar para distinguir um computador de um rdio, de um martelo, ou de uma mquina de lavar roupa. A distino principal est em que um computador pode ser programado para resolver virtualmente qualquer problema ou realizar praticamente qualquer tarefa, ao contrrio da mquina de lavar roupa em que existe um conjunto muito pequeno de possveis programas escolha e que esto pr-denidos. I.e., um computador tipicamente no tem nenhuma aplicao especca: uma mquina genrica. Claro que um computador usado para meras tarefas de secretariado (onde se utilizam os ditos conhecimentos de informtica do ponto de vista do utilizador) no muito diferente de uma mquina de lavar roupa ou de uma mquina de escrever electrnica. Um computador portanto uma mquina programvel de aplicao genrica. Serve para resolver problemas de muitos tipos diferentes, desde o problema de secretaria mais simples, passando pelo apoio gesto de empresas, at ao controlo de autmatos e construo de sistemas inteligentes. O objectivo desta disciplina ensinar os princpios da resoluo de problemas atravs de um computador. Os computadores tm uma arquitectura que, nos seus traos gerais, no parece muito complicada. No essencial, os computadores consistem num processador (o crebro) que, para alm de dispor de um conjunto de registos (memria de curta durao), tem acesso a uma 1

CAPTULO 1. INTRODUO PROGRAMAO

memria (memria de mdia durao) e a um conjunto de dispositivos de entrada e sada de dados, entre os quais tipicamente discos rgidos (memria de longa durao), um teclado e um ecr. Esta uma descrio simplista de um computador. Na disciplina de Arquitectura de Computadores os vrios componentes de um computador sero estudados com mais profundidade. Na disciplina de Sistemas Operativos, por outro lado, estudar-se-o os programas que normalmente equipam os computadores de modo a simplicar a sua utilizao.

1.2 Programao
Como se programa um computador? Os computadores, como se ver na disciplina de Arquitectura de Computadores, entendem uma linguagem prpria, usualmente conhecida por linguagem mquina. Esta linguagem caracteriza-se por no ter qualquer tipo de ambiguidades: a interpretao das instrues nica. Por outro lado, esta linguagem tambm se caracteriza por consistir num conjunto de comandos ou instrues que so executados cegamente pelo computador: uma linguagem imperativa. Os vrios tipos de linguagens (imperativas, declarativas, etc.), as vrias formas de classicar as linguagens e as vantagens e desvantagens de cada tipo de linguagem sero estudados na disciplina de Linguagens de Programao. A linguagem mquina caracteriza-se tambm por ser penosa de utilizar para os humanos: todas as instrues correspondem a cdigos numricos, difceis de memorizar, e as instrues so muito elementares. Uma soluo parcial passa por atribuir a cada instruo uma dada mnemnica, ou seja, substituir os nmeros que representam cada operao por nomes mais fceis de decorar. s linguagens assim denidas chama-se linguagens assembly e aos tradutores de assembly para linguagem mquina assemblers ou assembladores. Uma soluo prefervel passa por equipar os computadores com compiladores, isto com programas que so capazes de traduzir para linguagem mquina programas escritos em linguagens mais fceis de usar pelo programador e mais poderosas que o assembly. Infelizmente, no existem ainda compiladores para as linguagens naturais, que o nome que se d s linguagens humanas (e.g., portugus, ingls, etc.). Mas existem as chamadas linguagens de programao de alto nvel (alto nvel entre as linguagens de programao, bem entendido), que se aproximam um pouco mais das linguagens naturais na sua facilidade de utilizao pelos humanos, sem no entanto introduzirem as imprecises, ambiguidades e dependncias de contextos externos que so caractersticas das linguagens naturais. Nesta disciplina usar-se- o C++ como linguagem de programao1 . Tal como o conhecimento profundo do portugus no chega para fazer um bom escritor, o conhecimento profundo de uma linguagem de programao no chega para fazer um bom programador, longe disso. O conhecimento da linguagem necessrio, mas no de todo suciente. Programar no o simples acto de escrever ideias de outrem: ter essas ideias, ser criativo e engenhoso. Resolver problemas exige conhecimentos da linguagem, conhecimento das tcnicas conhecidas de ataque aos problemas, inteligncia para fazer analogias com outros
Pode-se pensar num computador equipado com um compilador de uma dada linguagem de programao como um novo computador, mais poderoso. de toda a convenincia usar este tipo de abstraco, que de resto ser muito til para perceber sistemas operativos, onde se vo acrescentando camada aps camada de software para acrescentar inteligncia aos computadores.
1

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS

problemas, mesmo em reas totalmente desconexas, criatividade, intuio, engenho, persistncia, etc. Programar no um acto mecnico. Assim, aprender a programar consegue-se atravs do estudo e, fundamentalmente, do treino.

1.3 Algoritmos: resolvendo problemas


Dado um problema que necessrio resolver, como desenvolver uma soluo? Como expressla? ltima pergunta responde-se muito facilmente: usando uma linguagem. A primeira mais difcil. Se a soluo desenvolvida corresponder a um conjunto de instrues bem denidas e sem qualquer ambiguidade, podemos dizer que temos um algoritmo que resolve um problema, i.e., que a partir de um conjunto de entradas produz determinadas sadas. A noo de algoritmo no simples de perceber, pois uma abstraco. Algoritmos so mtodos de resolver problemas. Mas concretizao de um algoritmo numa dada linguagem j no se chama algoritmo: chama-se programa. Sob esse ponto de vista, todas as verses escritas de um algoritmo so programas, mesmo que expressos numa linguagem natural (desde que no faam uso da sua caracterstica ambiguidade). Abusando um pouco das denies, no entanto, chamaremos algoritmo a um mtodo de resoluo de um dado problema expresso em linguagem natural, e programa concretizao de um algoritmo numa dada linguagem de programao. A denio de algoritmo um pouco mais completa do que a apresentada. De acordo com Knuth [10] os algoritmos tm cinco caractersticas importantes: Finitude Um algoritmo tem de terminar sempre ao m de um nmero nito de passos. De nada nos serve um algoritmo se existirem casos em que no termina. Denitude2 Cada passo do algoritmo tem de ser denido com preciso; as aces a executar tm de ser especicadas rigorosamente e sem ambiguidade. No fundo isto signica que um algoritmo tem de ser to bem especicado que at um computador possa seguir as suas instrues. Entrada Um algoritmo pode ter zero ou mais entradas, i.e., entidades que lhe so dadas inicialmente, antes do algoritmo comear. Essas entidades pertencem a um conjunto bem denido (e.g., o conjunto dos nmeros inteiros). Existem algoritmos interessantes que no tm qualquer entrada, embora sejam raros. Sada Um algoritmo tem uma ou mais sadas, i.e., entidades que tm uma relao bem denida com as entradas (o problema resolvido pelo algoritmo o de calcular as sadas correspondentes s entradas). Eccia Todas as operaes executadas no algoritmo tm de ser sucientemente bsicas para, em princpio, poderem ser feitas com exactido e em tempo nito por uma pessoa usando um papel e um lpis. Para alm destas caractersticas dos algoritmos, pode-se tambm falar da sua ecincia, isto , do tempo que demoram a ser executados para dadas entradas. Em geral, portanto, pretendese que os algoritmos sejam no s nitos mas tambm sucientemente rpidos [10]. Ou seja,

CAPTULO 1. INTRODUO PROGRAMAO

devem resolver o problema em tempo til (e.g., enquanto ainda somos vivos para estarmos interessados na sua resoluo) mesmo para a mais desfavorvel combinao possvel das entradas. O estudo dos algoritmos, a algoritmia (algorithmics), um campo muito importante da cincia da computao, que envolve por exemplo o estudo da sua correco (vericao se de facto resolvem o problema), da sua nitude (ser de facto um algoritmo, ou no passa de um mtodo computacional [10] intil na prtica?) e da sua ecincia (anlise de algoritmos). Estes temas sero inevitavelmente abordados nesta disciplina, embora informalmente, e sero fundamentados teoricamente na disciplina de Computao e Algoritmia. A disciplina de Introduo Programao serve portanto de ponte entre as disciplinas de Arquitectura de Computadores e Computao e Algoritmia, fornecendo os conhecimentos necessrios para o trabalho subsequente em outras disciplinas da rea da informtica.

1.3.1 Regras do jogo


No jogo de resoluo de problemas que o desenvolvimento de algoritmos, h duas regras simples: 1. as variveis so os nicos objectos manipulados pelos algoritmos e 2. os algoritmos s podem memorizar valores em variveis. As variveis so os objectos sobre os quais as instrues dos algoritmos actuam. Uma varivel corresponde a um local onde se podem guardar valores. Uma varivel tem trs caractersticas: 1. Um nome, que xo durante a vida da varivel, e pelo qual a varivel conhecida. importante distinguir as variveis sobre as quais os algoritmos actuam das variveis matemticas usuais. Os nomes das variveis de algoritmo ou programa sero grafados com um tipo de largura xa, enquanto as variveis matemticas sero grafadas em itlico. Por exemplo, k e nmero_de_alunos so variveis de algoritmo ou programa enquanto k uma varivel matemtica. 2. Um tipo, que tambm xo durante a vida da varivel, e que determina o conjunto de valores que nela podem ser guardados e, sobretudo, as operaes que se podem realizar com os valores guardados nas variveis desse tipo. Para j, considerar-se- que todas as variveis tm tipo inteiro, i.e., que guardam valores inteiros suportando as operaes usuais com nmeros inteiros. Nesse caso dir-se- que o tipo das variveis inteiro ou Z. 3. Um e um s valor em cada instante de tempo. No entanto, o valor pode variar no tempo, medida que o algoritmo decorre. Costuma-se fazer uma analogia entre algoritmo e receita de cozinha. Esta analogia atractiva, mas falha em alguns pontos. Tanto uma receita como um algoritmo consistem num conjunto de instrues. Porm, no caso das receitas, as instrues podem ser muito vagas. Por exemplo,

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS

ponha ao lume e v mexendo at alourar. Num algoritmo as instrues tem de ser precisas, sem margem para interpretaes diversas. O pior ponto da analogia diz respeito s variveis. Pode-se dizer que as variveis de um algoritmo correspondem aos recipientes usados para realizar uma receita. O problema que um recipiente pode (a) estar vazio e (b) conter qualquer tipo de ingrediente, enquanto uma varivel contm sempre valores do mesmo tipo e, alm disso, uma varivel contm sempre um qualquer valor: as variveis no podem estar vazias! absolutamente fundamental entranhar esta caracterstica pouco intuitiva das variveis. muito conveniente ter uma notao para representar gracamente uma varivel. A Figura 1.1 mostra uma varivel inteira de nome idade com valor 24. comum, na linguagem corrente, nome idade: inteiro 24 valor Figura 1.1: Notao para uma varivel. A azul explicaes sobre a notao grca usada. no distinguir entre a varivel, o seu nome e o valor que guarda. Esta prtica abusiva, mas simplica a linguagem. Por exemplo, acerca da varivel na Figura 1.1, costume dizerse que a idade 24. Em rigor dever-se-ia dizer que a varivel de nome idade guarda actualmente o valor 24. Suponha-se um problema simples. So dadas duas variveis n e soma, ambas de tipo inteiro. Pretende-se que a execuo do algoritmo coloque na varivel soma a soma dos primeiros n inteiros no-negativos. Admite-se portanto que a varivel soma tem inicialmente um valor arbitrrio enquanto a varivel n contm inicialmente o nmero de termos da soma a realizar. Durante a execuo de um algoritmo, as variveis existentes vo mudando de valor. Aos valores das variveis num determinado instante da execuo do algoritmo chama-se estado. H dois estados particularmente importantes durante a execuo de um algoritmo: o estado inicial e o estado nal. O estado inicial importante porque os algoritmos s esto preparados para resolver os problemas se determinadas condies mnimas se vericarem no incio da sua execuo. Para o problema dado no faz qualquer sentido que a varivel n possua inicialmente o valor -3, por exemplo. Que poderia signicar a soma dos primeiros -3 inteiros no-negativos? Um algoritmo uma receita para resolver um problema, mas apenas se as variveis vericaram inicialmente determinadas condies mnimas, expressas na chamada pr-condio ou P C. Neste caso a pr-condio diz simplesmente que a varivel n no pode ser negativa, i.e., P C 0 n. O estado nal ainda mais importante que o estado inicial. O estado das variveis no nal de um algoritmo deve ser o necessrio para que o problema que o algoritmo suposto resolver esteja de facto resolvido. Para especicar quais os estados aceitveis para as variveis no nal do algoritmo usa-se a chamada condio-objectivo ou CO. Neste caso a condio objectivo CO soma = n1 j, ou seja, a varivel soma deve conter a soma dos inteiros entre 0 e n 1 j=0 tipo

6 inclusive3 .

CAPTULO 1. INTRODUO PROGRAMAO

O par de condies pr-condio e condio objectivo representa de forma compacta o problema que um algoritmo suposto resolver. Neste caso, porm, este par de condies est incompleto: uma vez que o algoritmo pode alterar o valor das variveis, pode perfeitamente alterar o valor da varivel n, pelo que um algoritmo perverso poderia simplesmente colocar o valor zero quer em n quer em soma e declarar resolver o problema! Em rigor, portanto, deverse-ia indicar claramente que n no pode mudar de valor ao longo do algoritmo, i.e., que n uma constante, ou, alternativamente, indicar claramente na condio objectivo que o valor de n usado o valor de n no incio do algoritmo. Em vez disso admitir-se- simplesmente que o valor de n no alterado pelo algoritmo. O problema proposto pode ser resolvido de uma forma simples. Considere-se uma varivel adicional i que conter os inteiros a somar (um de cada vez...). Comece-se por colocar o valor zero quer na varivel soma quer na varivel i. Enquanto o valor da varivel i no atingir o valor da varivel n, deve-se somar o valor da varivel i ao valor da varivel soma e guardar o resultado dessa soma na prpria varivel soma (que vai servindo para acumular o resultado), e em seguida deve-se aumentar o valor de i de uma unidade. Quando o valor de i atingir n, o algoritmo termina. Um pouco mais formalmente 4 :
{P C 0 n.} i0 soma 0 enquanto i = n faa-se: soma soma + i ii+1 {CO soma = n1 j.} j=0

O smbolo deve ser lido ca com o valor de e chama-se a atribuio. Tudo o que se coloca entre chavetas so comentrios, no fazendo parte do algoritmo propriamente dito. Neste caso usaram-se comentrios para indicar as condies que se devem vericar no incio e no nal do algoritmo. Um algoritmo lido e executado pela ordem normal de leitura em portugus: de cima para baixo e da esquerda para a direita, excepto quando surgem construes como um enquanto, que implicam voltar atrs para repetir um conjunto de instrues. muito importante perceber a evoluo dos valores das variveis ao longo da execuo do algoritmo. Para isso necessrio arbitrar os valores iniciais da variveis. Suponha-se que n tem inicialmente o valor 4. O estado inicial, i.e., imediatamente antes de comear a executar o algoritmo, o indicado na Figura 1.2. Dois aspectos so de notar. Primeiro que este estado verica a pr-condio indicada. Segundo que os valores iniciais das variveis i e soma so irrelevantes, o que indicado atravs do smbolo ?. Neste caso evidente que o estado nal, i.e., imediatamente aps a execuo do algoritmo, o indicado na Figura 1.3. Mas em geral pode no ser to evidente, pelo que necessrio
Mais tarde usar-se- uma notao diferente para o somatrio: CO soma = (S j : 0 j < n : j). Para o leitor mais atento dever ser claro que uma forma mais simples de resolver o problema simplesmente colocar na varivel soma o resultado de n(n1) ... 2
4 3

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS

n: inteiro 4 soma: inteiro ? i: inteiro ?

Figura 1.2: Estado inicial do algoritmo.

n: inteiro 4 soma: inteiro 6 i: inteiro 4

Figura 1.3: Estado nal do algoritmo.

CAPTULO 1. INTRODUO PROGRAMAO

fazer o traado da execuo do algoritmo, i.e., a vericao do estado ao longo da execuo do algoritmo. O traado da execuo de um algoritmo implica, pois, registar o valor de todas as variveis antes e depois de todas as instrues executadas. Para que isso se torne claro, conveniente numerar as transies entre as instrues:
{P C 0 n.} i0 soma 0 enquanto i = n faa-se: 4 5 6 7 soma soma + i ii+1 m do enquanto. {CO soma = n1
j=0

1 2 3

j.}

Introduziu-se explicitamente o nal do enquanto de modo a separar claramente os intervalos 6 e 7. O intervalo 6 est aps o aumento de um da varivel i e antes de se vericar se o seu valor atingiu j o valor de n. O intervalo 7, pelo contrrio, est depois do enquanto, quando i atingiu j o valor de n, no nal do algoritmo. Estes intervalos cam mais claros se se recorrer a um diagrama de actividade 5 para representar o algoritmo, como se pode ver na Figura 1.4. fcil agora seguir o uxo de execuo e analisar os valores das variveis em cada transio entre instrues. O resultado dessa anlise pode ser visto na Tabela 1.1. A realizao do traado de um algoritmo, como se ver, no demonstra a sua correco. Para o fazer h que usar tcnicas um pouco mais formais, abordadas brevemente na prxima seco.

1.3.2 Desenvolvimento e demonstrao de correco


Seja o problema de, dados quaisquer dois inteiros positivos, calcular os seu mximo divisor comum6 . O algoritmo que resolve este problema tem de comear por instrues dizendo para os dois inteiros serem pedidos a algum (i.e., lidos) e guardados em duas variveis, a que se daro
Como estes diagramas mostram claramente o uxo de execuo do algoritmo, tambm so conhecidos por diagramas de uxo ou uxogramas. 6 Este exemplo faz uso de alguma simbologia a que pode no estar habituado. Recomenda-se a leitura do Apndice A.
5

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS

incio de actividade (algoritmo) [estado de] aco (instruo) i0 2 soma 0 entroncamento 3 comentrio 1

ramicao (seleco)

[i = n] [i = n] soma soma + i 5 i i+1 6 7 4

m de actividade (algoritmo)

Figura 1.4: Diagrama de actividade do algoritmo. As setas indicam a sequncia de execuo das aces (instrues). Os losangos tm signicados especiais: servem para unir uxos de execuo alternativos (entroncamentos) ou para os comear (ramicaes). Em cada um dos ramos sados de uma ramicao indicam-se as condies que tm de se vericar para que seja escolhido esse ramo. A azul encontram-se explicaes sobre a notao grca usada. Os comentrios contm a numerao das transies usadas no algoritmo, para que se possa fazer uma mais fcil correspondncia entre este e o diagrama.

10

CAPTULO 1. INTRODUO PROGRAMAO


Tabela 1.1: Traado do algoritmo.

Transio 1 2 3 4 5 6 4 5 6 4 5 6 4 5 6 7

n 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4

i ? 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4

soma ? ? 0 0 0 0 0 1 1 1 3 3 3 6 6 6

Comentrios Verica-se P C 0 n.

Verica-se CO soma =

n1 j = j=0

3 j=0 j

= 0 + 1 + 2 + 3 = 6.

os nomes m e n, e terminar por uma instruo dizendo para o mximo divisor comum ser comunicado a algum (i.e., escrito). Para que o resultado possa ser comunicado a algum, necessrio que esteja guardado em algum lado. Para isso necessria uma varivel adicional, a que se dar o nome de k. Os passos intermdios do algoritmo, entre a leitura dos dois inteiros e a escrita do resultado, indicam a forma de clculo do mximo divisor comum, e so os passos que nos interessam neste momento. Assim, considerar-se- inicialmente o problema mais simples de, dados dois valores inteiros positivos arbitrrios colocados nas duas variveis m e n, colocar na varivel k o seu mximo divisor comum (mdc). Um exemplo representado na Figura 1.5. A abordagem deste problema passa em primeiro lugar pela identicao da chamada prcondio (P C), i.e., pelas condies que se sabe que se vericam (ou que pelo menos se admite que se vericam) a priori. Neste caso a pr-condio P C m e n so inteiros positivos. a Depois, deve-se formalizar a condio objectivo (CO). Neste caso a condio objectivo CO k = mdc(m, n), onde se assume que m e n no mudam de valor ao longo do algoritmo. O objectivo do problema , pois, encontrar um valor para k que verique a condio objectivo CO. Para que o problema faa sentido, necessrio que, quaisquer que sejam m e n tais que a P C verdadeira, exista pelo menos um inteiro positivo k tal que a CO verdadeira. No

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS


m: inteiro 12 n: inteiro 8 k: inteiro ?
(a) Antes de executar o algoritmo

11 m: inteiro 12 n: inteiro 8 k: inteiro 4

(b) Depois de executar o algoritmo

Figura 1.5: Exemplo de estado das variveis antes 1.5(a) e depois 1.5(b) de executado o algoritmo. problema em causa no h quaisquer dvidas: todos os pares de inteiros positivos tm um correspondente mximo divisor comum. Para que a soluo do problema no seja ambgua, necessrio garantir mais. No basta que exista soluo: necessrio que seja nica, ou seja, que quaisquer que sejam m e n tais que a P C verdadeira, existe um e um s inteiro positivo k tal que a CO verdadeira. Quando isto se verica, pode-se dizer que o problema est bem colocado (a demonstrao de que um problema est bem colocado muitas vezes se faz desenvolvendo um algoritmo: so as chamadas demonstraes construtivas). fcil vericar que, neste caso, o problema est bem colocado: existe apenas um mdc de cada par de inteiros positivos. Depois de se vericar que de facto o problema est bem colocado dadas a pr-condio e a condio objectivo, de todo o interesse identicar propriedades interessantes do mdc. Duas propriedades simples so: a) O mdc sempre superior ou igual a 1, i.e., quaisquer que sejam m e n inteiros positivos, 1 mdc(m, n). b) O mdc no excede nunca o menor dos dois valores, i.e., quaisquer que sejam m e n inteiros positivos, mdc(m, n) min(m, n), em que min(m, n) o menor dos dois valores m e n. A veracidade destas duas propriedades evidente, pelo que no se demonstra aqui. Seja m k a notao para o resto da diviso inteira de m por k. Ou seja, dados dois inteiros 0 m e 0 < k, e sendo m = qk + r com 0 r < k (onde q e r so respectivamente o quociente e o resto da diviso de m por k), ento r = m k. Nesse caso, dizer que 0 < k divisor de 0 m o mesmo que dizer que m k = 0, i.e., que a diviso inteira de m por k tem resto zero. Duas outras propriedades que se vericar serem importantes so:

12

CAPTULO 1. INTRODUO PROGRAMAO


c) Quaisquer que sejam k, m e n inteiros positivos, mdc(m, n) k (m k = 0 n k = 0) k = mdc(m, n). Ou seja, se k superior ou igual ao mximo divisor comum de m e n e se k divisor comum de m e n, ento k o mximo divisor comum de m e n. A demonstrao pode ser feita por absurdo. Suponha-se que k no o mximo divisor comum de m e n. Como k divisor comum, ento conclui-se que k < mdc(m, n), isto , h divisores comuns maiores que k. Mas ento mdc(m, n) k < mdc(m, n), que uma impossibilidade. Logo, k = mdc(m, n). d) Quaisquer que sejam k, m e n inteiros positivos, mdc(m, n) k (m k = 0 n k = 0) mdc(m, n) < k. Ou seja, se k superior ou igual ao mximo divisor comum de m e n e se k no divisor comum de m e n, ento k maior que o mximo divisor comum de m e n. Neste caso a demonstrao trivial.

Em que que estas propriedades nos ajudam? As duas primeiras propriedades restringem a gama de possveis divisores, i.e., o intervalo dos inteiros onde o mdc deve ser procurado, o que obviamente til. As duas ltimas propriedades sero teis mais tarde. No h nenhum mtodo mgico de resolver problemas. Mais tarde ver-se- que existem metodologias que simplicam a abordagem ao desenvolvimento de algoritmos, nomeadamente aqueles que envolvem iteraes (repeties). Mesmo essas metodologias no so substituto para o engenho e a inteligncia. Para j, usar-se- apenas a intuio. Onde se deve procurar o mdc? A intuio diz que a procura deve ser feita a partir de min(m, n), pois de outra forma, comeando por 1, fcil descobrir divisores comuns, mas no imediato se eles so ou no o mximo divisor comum: necessrio testar todos os outros potenciais divisores at min(m, n). Deve-se portanto ir procurando divisores comuns partindo de min(m, n) para baixo at se encontrar algum, que ser forosamente o mdc. Como transformar estas ideias num algoritmo e, simultaneamente, demonstrar a sua correco? A varivel k, de acordo com a CO, tem de terminar o algoritmo com o valor do mximo divisor comum. Mas pode ser usada entretanto, durante o algoritmo, para conter inteiros que so candidatos a mximo divisor comum. Assim, se se quiser comear pelo maior valor possvel, deve-se atribuir a k (ou seja, registar na caixa k) o menor dos valores m e n:
se m < n ento: km seno: kn

Depois destas instrues, evidente que se pode armar que mdc(m, n) k, dada a propriedade b). Em seguida, deve-se vericar se k divide m e n e, no caso contrrio, passar ao prximo candidato, que o valor inteiro imediatamente abaixo:

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS


enquanto m k = 0 n k = 0 faa-se: kk1

13

A condio que controla o ciclo (chama-se ciclo porque uma instruo que implica a repetio cclica de outra, neste caso k k 1) chama-se guarda (G) do ciclo. Neste caso a guarda G m k = 0 n k = 0, que verdadeira se k no for divisor comum a m e n. A instruo k k1 chama-se o progresso (prog) do ciclo pois, como se ver, ela que garante que o ciclo progride em direco ao seu m. Quando este ciclo terminar, k o mdc. Como demonstr-lo? Repare-se que, dadas as propriedades c) e d), quer a inicializao de k, quer a guarda e o progresso, garantem que h uma condio que se mantm sempre verdadeira, desde o incio ao m do ciclo, e que por isso recebe o nome de condio invariante (CI) do ciclo: CI mdc(m, n) k. Esta condio diz simplesmente que k sempre superior ou igual ao mximo divisor comum que se procura. Dada a inicializao do ciclo
se m < n ento: km seno: kn

e a pr-condio, tem-se que k = min(m, n), o que, conjugado com a propriedade b), garante que mdc(m, n) k. Ou seja, a inicializao leva veracidade da condio invariante.

Durante o ciclo, que acontece se a G for verdadeira? Diminui-se k de uma unidade, isto progride-se7. Mas, admitindo que a condio invariante verdadeira e sendo a guarda tambm verdadeira, conclui-se pela propriedade d) que mdc(m, n) < k logo aps a vericao da guarda e imediatamente antes do progresso.

Como o valor guardado em k inteiro, isso o mesmo que dizer que mdc(m, n) k 1. Donde, depois do progresso k k 1, a veracidade da condio invariante recuperada, i.e., mdc(m, n) k. Ou seja, demonstrou-se que se a condio invariante for verdadeira antes do progresso, tambm o ser depois. A demonstrao da veracidade da condio invariante do ciclo aps a inicializao do ciclo e da preservao da sua veracidade durante a execuo do ciclo correspondem, no fundo, demonstrao por induo de que a condio invariante se verica durante todo o ciclo, inclusive quando este termina. I.e., demonstrou-se que a condio invariante , de facto, invariante.
uma progresso porque se ca mais prximo do nal do ciclo, ou seja, ca-se mais prximo do real valor do mdc(m, n).
7

14

CAPTULO 1. INTRODUO PROGRAMAO

Que acontece se, durante o ciclo, a guarda for falsa? Nesse caso o ciclo termina, concluindo-se que h duas condies verdadeiras: a condio invariante CI e tambm a negao da guarda, i.e., G. A conjuno destas condies, pela propriedade c), implica que a condio objectivo verdadeira, i.e., que k termina de facto com o mximo divisor comum de m e n. A demonstrao da correco (parcial) do algoritmo passa, pois, por demonstrar que a condio invariante verdadeira do incio ao m do ciclo (i.e., que de facto invariante) e que, quando o ciclo termina (i.e., quando a guarda falsa), se tem forosamente que a condio objectivo verdadeira, i.e., CI G CO. Assim, o algoritmo completo, decorado com comentrios onde se indica claramente quais as condies verdadeiras em cada transio entre intrues, :
{P C 0 < m 0 < n.} se m < n ento: km seno: kn {Aqui sabe-se que mdc(m, n) min(m, n) = k, ou seja, que a CI verdadeira..} enquanto m k = 0 n k = 0 faa-se: {Aqui a G verdadeira. Logo, pela propriedade d), a CI mantm-se verdadeira depois do seguinte progresso:} kk1 {Aqui a G falsa. Logo, pela propriedade c), k = mdc(m, n), ou seja a CO verdadeira..}

Uma questo importante, que cou por demonstrar, se garantido que o ciclo termina sempre. Se no se garantir a terminao, no se pode chamar a esta sequncia de instrues um algoritmo, como se viu. Esta demonstrao essencial para garantir a correco total do algoritmo. A demonstrao simples. Em primeiro lugar, a inicializao garante que o valor inicial de k superior ou igual a 1 (pois m e n so inteiros positivos). O progresso, por outro lado, faz o valor de k diminuir a cada iterao do ciclo. Como 1 divisor comum de qualquer par de inteiros positivos, o ciclo, na pior das hipteses, termina com k = 1, i.e., na pior das hipteses o ciclo termina ao m de min(m, n) 1 iteraes. Assim, o algoritmo de facto um algoritmo, pois verica a condio de nitude: est totalmente correcto. Resumindo, o conjunto de instrues que se apresentou um algoritmo porque: 1. nito, terminando sempre ao m de no mximo min(m, n) 1 iteraes do ciclo; 2. est bem denido, pois cada passo do algoritmo est denido com preciso e sem ambiguidades; 3. tem duas entradas, que so os valores colocados inicialmente em m e n, pertencentes ao conjunto dos inteiros positivos;

1.3. ALGORITMOS: RESOLVENDO PROBLEMAS

15

4. tem uma sada que o valor que se encontra em k no nal do algoritmo e que verica k = mdc(m, n); e 5. ecaz, pois todas as operaes do algoritmo podem ser feitas com exactido e em tempo nito por uma pessoa usando um papel e um lpis (e alguma pacincia). Quanto sua ecincia, pode-se desde j armar que sucientemente rpido para se poder usar em muitos casos, embora existam algoritmos consideravelmente mais ecientes, que sero abordados posteriormente. Mais uma vez se poderia fazer o traado deste algoritmo admitindo, por exemplo, o estado inicial indicado na Figura 1.5(a). Esse traado ca como exerccio para o leitor, que pode recorrer ao diagrama de actividade correspondente ao algoritmo mostrado na Figura 1.6.

[m < n]

[m n]

km

kn

[m k = 0 n k = 0] [m k = 0 n k = 0] kk1

Figura 1.6: Diagrama de actividade correspondente ao algoritmo para clculo do mdc.

16 Observaes

CAPTULO 1. INTRODUO PROGRAMAO

O desenvolvimento de algoritmos em simultneo com a demonstrao da sua correco um exerccio extremamente til, como se ver no Captulo 4. Mas pode-se desde j adiantar a uma das razes: no possvel demonstrar que um algoritmo funciona atravs de testes, excepto se se testarem rigorosamente todas as possveis combinaes de entradas vlidas (i.e., entradas que vericam pr-condio do algoritmo). A razo simples: o facto de um algoritmo funcionar correctamente para n diferentes combinaes da entrada, por maior que n seja, no garante que no d um resultado errado para uma outra combinao ainda no testada. Claro est que o oposto verdadeiro: basta que haja uma combinao de entradas vlidas para as quais o algoritmo produz resultados errados para se poder armar que ele est errado! No caso do algoritmo do mdc evidente que jamais se poder mostrar a sua correco atravs de testes: h innitas possveis combinaes das entradas. Ainda que se limitasse a demonstrao a entradas inferiores ou iguais a 1 000 000, ter-se-iam 1 000 000 000 000 testes para realizar. Admitindo que se conseguiam realizar 1 000 000 de testes por segundo, a demonstrao demoraria 1 000 000 de segundos, cerca de 12 dias. Bastaria aumentar os valores das entradas at 10 000 000 para se passar para um total de 1157 dias, cerca de trs anos. Pouco prtico, portanto... O ciclo usado no algoritmo no foi obtido pela aplicao directa da metodologia de Dijkstra que ser ensinada na Seco 4.7, mas poderia ter sido. Pede-se ao leitor que regresse mais tarde a esta seco para vericar que o ciclo pode ser obtido usando a factorizao da condio objectivo
G CI

CO m k = 0 n k = 0 0 < k (Q i : 0 i < k : m i = 0 n i = 0), onde Q signica qualquer que seja. Finalmente, para o leitor mais interessado, ca a informao de que algoritmos mais ecientes para o clculo do mdc podem ser obtidos pela aplicao das seguintes propriedades do mdc: Quaisquer que sejam a e b inteiros positivos mdc(a, b) = mdc(b a, a). Se a = 0 0 < b, ento mdc(a, b) = b. Estas propriedades permitem, em particular, desenvolver o chamado algoritmo de Euclides para obteno do mximo divisor comum.

1.4 Programas
Como se viu, um programa a concretizao, numa dada linguagem de programao, de um determinado algoritmo (que resolve um problema concreto). Nesta disciplina ir-se- utilizar uma linguagem de alto nvel chamada C++. Esta linguagem tem o seu prprio lxico, a sua sintaxe, a sua gramtica e a sua semntica (embora simples). Todos estes aspectos da linguagem sero tratados ao longo deste texto. Para j, no entanto, apresenta-se sem mais explicaes

1.4. PROGRAMAS

17

a traduo do algoritmo do mdc para C++, embora agora j com instrues explcitas para leitura das entradas a partir de um teclado e de escrita da sada no ecr. O programa resultante sucientemente simples para que possa ser entendido quase na sua totalidade. No entanto, normal que o leitor que com dvidas: sero esclarecidas no prximo captulo.
// Todo o texto aps // so comentrios, no fazendo parte do programa. // Os programas comeam tipicamente por um conjunto de incluses. // Neste caso incluem-se ferramentas de entradas e sadas (Input/Output) a partir // de canais (stream): #include <iostream> // Para evitar ter de escrever std::cout, std::cin, etc. using namespace std; /// Este programa calcula o mximo divisor comum de dois nmeros. int main() { // Instrues de insero de informao no canal de sada cout, ligado ao ecr. // Mostra ao utilizador informao sobre o programa e pede-lhe para inserir os // dois inteiros dos quais quer obter o mximo divisor comum: cout < < "Mximo divisor comum de dois nmeros." < < endl; cout < < "Introduza dois inteiros positivos: "; // Denio de duas variveis inteiras: int m, n; // Instruo de extraco de informao do canal de entrada, ligado ao teclado. // Serve para obter do utilizador os valores dos quais ele pretende saber o mdc: cin > > m > > n; // Assume-se que m e n so positivos! // Como um divisor sempre menor ou igual a um nmero, escolhe-se // o mnimo dos dois! int k; if(m < n) k = m; else k = n; // Neste momento sabe-se que mdc(m, n) k. while(m % k != 0 or n % k != 0) { // Se o ciclo no parou, ento k no divide m e n, logo, // k = mdc(m, n) mdc(m, n) k. Ou seja, mdc(m, n) < k. --k; // Decrementa k. o progresso do ciclo. // Neste momento, mdc(m, n) k outra vez! o invariante do ciclo!

18
}

CAPTULO 1. INTRODUO PROGRAMAO

// Como mdc(m, n) k (invariante do ciclo) e k divide m e n // (o ciclo terminou, no foi?), conclui-se que k = mdc(m, n)! // Insere no canal de sada uma mensagem para o utilizador dizendo-lhe qual o // resultado: cout < < "O mximo divisor comum de " < < m < < " e " < < n < < " " < < k < < . < < endl; }

1.5 Resumo: resoluo de problemas


Resumindo, a resoluo de problemas usando linguagens de programao de alto nvel tem vrios passos: 1. Especicao do problema, feita por humanos. 2. Desenvolvimento de um algoritmo que resolve o problema, feito por humanos. neste passo que se faz mais uso da inteligncia e criatividade. 3. Concretizao do algoritmo na linguagem de programao: desenvolvimento do programa, feito por humanos. O resultado deste passo um programa, consistindo numa sequncia de instrues, qual tambm se chama cdigo. Este passo mecnico e relativamente pouco interessante. 4. Traduo do programa para linguagem mquina, feita pelo computador, ou melhor, por uma programa chamado compilador. 5. Execuo do programa para resolver um problema particular (e.g., clculo de mdc(131, 47)), feita pelo computador. Podem ocorrer erros em todos estes passos. No primeiro, se o problema estiver mal especicado. No segundo ocorrem os chamados erros lgicos: o algoritmo desenvolvido na realidade no resolve o problema tal como especicado. aqui que os erros so mais graves, pelo que extremamente importante assegurar a correco dos algoritmos desenvolvidos. No terceiro passo, os erros mais benvolos so os que conduzem a erros sintcticos ou gramaticais, pois o compilador, que o programa que traduz o programa da linguagem de programao em que est escrito para a linguagem mquina, assinala-os. Neste passo os piores erros so gralhas que alteram a semntica do programa sem o invalidar sintacticamente. Por exemplo, escrever l (letra l) em vez de 1 pode ter consequncias muito graves se l for o nome de uma varivel. Estes erros so normalmente muito difceis de detectar 8 . Finalmente, a ocorrncia de erros nos dois ltimos passos to improvvel que mais vale assumir que no podem ocorrer de todo.
Estes erros assemelham-se ao no introduzido (acidentalmente?) pelo revisor de provas em Histria do cerco de Lisboa, de Jos Saramago.
8

Captulo 2

Conceitos bsicos de programao


Qu sera cada uno de nosotros sin su memria? Es una memria que en buena parte est hecha del ruido pero que es esencial. (...) se es el problema que nunca podremos resolver: el problema de la identidad cambiante. (...) Porque si hablamos de cambio de algo, no decimos que algo sea reemplazado por otra cosa. Jorge Luis Borges, Borges Oral, 98-99 (1998)

Um programa, como se viu no captulo anterior, consiste na concretizao prtica de um algoritmo numa dada linguagem de programao. Um programa numa linguagem de programao imperativa consiste numa sequncia de instrues 1 , sem qualquer tipo de ambiguidade, que so executadas ordenadamente. As instrues: 1. alteram o valor de variveis, i.e., alteram o estado do programa e consequentemente da memria usada pelo programa; 2. modicam o uxo de controlo, i.e., alteram a execuo sequencial normal dos programas permitindo executar instrues repetida, condicional ou alternativamente; ou 3. denem (ou declaram) entidades (variveis, constantes, funes, procedimentos, etc.). Neste captulo comea por se analisar informalmente o programa apresentado no captulo anterior e depois discutem-se em maior pormenor os conceitos bsicos de programao, nomeadamente variveis, constantes, tipos, expresses e operadores.

2.1 Introduo
Um programa em C++ tem normalmente uma estrutura semelhante seguinte (todas as linhas precedidas de // e todo o texto entre /* e */ so comentrios, sendo portanto ignorados pelo compilador e servindo simplesmente para documentar os programas, i.e., explicar ou claricar as intenes do programador):
1

E no s, como se ver.

19

20

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


/* As duas linhas seguintes so necessrias para permitir a apresentao de variveis no ecr e a insero de valores atravs do teclado (usando os canais [ou streams] cin e cout): */ #include <iostream> using namespace std; /* A linha seguinte indica o ponto onde o programa vai comear a ser executado. Indica tambm que o programa no usa argumentos e que o valor por ele devolvido ao sistema operativo um inteiro (estes assuntos sero vistos em pormenor mais tarde): */ int main() { // esta chaveta assinala o incio do programa. // Aqui aparecem as instrues que compem o programa. } // esta chaveta assinala o m do programa.

A primeira linha,
#include <iostream>

serve para obter as declaraes do canal de leitura de dados do teclado (cin) e do canal de escrita de dados no ecr (cout). A linha seguinte,
using namespace std;

uma directiva de utilizao do espao nominativo std, e serve para se poder escrever simplesmente cout em vez de std::cout. Nem sempre o seu uso recomendvel [12, pg.171], sendo usado aqui apenas para simplicar a apresentao dos programas sem os tornar invlidos (por razes de espao, neste texto estas duas linhas iniciais sero muitas vezes omitidas, devendo o leitor coloc-las se decidir experimentar os exemplos apresentados). Os espaos nominativos sero abordados no Captulo 9. Quanto a main(), uma funo que tem a particularidade de ser a primeira a ser invocada no programa. As funes sero vistas em pormenor no Captulo 3. Esta estrutura pode ser vista claramente no programa de clculo do mdc introduzido no captulo anterior:
// Todo o texto aps // so comentrios, no fazendo parte do programa. // Os programas comeam tipicamente por um conjunto de incluses. // Neste caso incluem-se ferramentas de entradas e sadas (Input/Output) a partir

2.1. INTRODUO
// de canais (stream): #include <iostream> // Para evitar ter de escrever std::cout, std::cin, etc. using namespace std; /// Este programa calcula o mximo divisor comum de dois nmeros. int main() { // Instrues de insero de informao no canal de sada cout, ligado ao ecr. // Mostra ao utilizador informao sobre o programa e pede-lhe para inserir os // dois inteiros dos quais quer obter o mximo divisor comum: cout < < "Mximo divisor comum de dois nmeros." < < endl; cout < < "Introduza dois inteiros positivos: "; // Denio de duas variveis inteiras: int m, n; // Instruo de extraco de informao do canal de entrada, ligado ao teclado. // Serve para obter do utilizador os valores dos quais ele pretende saber o mdc: cin > > m > > n; // Assume-se que m e n so positivos! // Como um divisor sempre menor ou igual a um nmero, escolhe-se // o mnimo dos dois! int k; if(m < n) k = m; else k = n; // Neste momento sabe-se que mdc(m, n) k. while(m % k != 0 or n % k != 0) { // Se o ciclo no parou, ento k no divide m e n, logo, // k = mdc(m, n) mdc(m, n) k. Ou seja, mdc(m, n) < k. --k; // Decrementa k. o progresso do ciclo. // Neste momento, mdc(m, n) k outra vez! o invariante do ciclo! } // Como mdc(m, n) k (invariante do ciclo) e k divide m e n // (o ciclo terminou, no foi?), conclui-se que k = mdc(m, n)! // Insere no canal de sada uma mensagem para o utilizador dizendo-lhe qual o // resultado: cout < < "O mximo divisor comum de " < < m < < " e " < < n < < " " < < k < < . < < endl;

21

22
}

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Este programa foi apresentado como concretizao em C++ do algoritmo de clculo do mdc tambm desenvolvido no captulo anterior. Em rigor isto no verdade. O programa est dividido em trs partes, das quais s a segunda parte a concretizao directa do algoritmo desenvolvido:
// Como um divisor sempre menor ou igual a um nmero, escolhe-se // o mnimo dos dois! int k; if(m < n) k = m; else k = n; // Neste momento sabe-se que mdc(m, n) k. while(m % k != 0 or n % k != 0) { // Se o ciclo no parou, ento k no divide m e n, logo, // k = mdc(m, n) mdc(m, n) k. Ou seja, mdc(m, n) < k. --k; // Decrementa k. o progresso do ciclo. // Neste momento, mdc(m, n) k outra vez! o invariante do ciclo! } // Como mdc(m, n) k (invariante do ciclo) e k divide m e n // (o ciclo terminou, no foi?), conclui-se que k = mdc(m, n)!

As duas partes restantes resolvem dois problemas que foram subtilmente ignorados no captulo anterior: 1. De onde vem as entradas do programa? 2. Para onde vo as sadas do programa? Claramente as entradas do programa tm de vir de uma entidade exterior ao programa. Que entidade exactamente depende da aplicao que se pretende dar ao programa. Por exemplo, as entradas poderiam ser originadas: por um humano utilizador do programa, por um outro programa qualquer ou a partir de um cheiro no disco rgido do computador. Tambm bvio que as sadas (neste caso a sada) do programa tm de ser enviadas para alguma entidade externa ao programa, pois de outra forma no mereceriam o seu nome. No caso do programa apresentado assume-se que existe um utilizador humano, que digita as entradas num teclado e v as sadas num ecr.

2.1. INTRODUO

23

2.1.1 Consola e canais


tpico os programas actuais usarem interfaces mais ou menos sosticadas com o seu utilizador. Ao longo deste texto, no entanto, adoptar-se- um modelo muito simplicado (e primitivo) de interaco com o utilizador: admite-se que os programas desenvolvidos so executados numa consola de comandos, sendo as entradas do programa lidas de um teclado no qual um utilizador humano as digita (ou lidas de um cheiro de texto guardado no disco rgido do computador) e sendo as sadas do programa escritas na consola de comandos onde o utilizador humano as pode ler (ou escritas num cheiro de texto guardado no disco rgido do computador). A consola de comandos tem um modelo muito simples. uma grelha rectangular de clulas, com uma determinada altura e largura, em que cada clula pode conter um qualquer dos caracteres (smbolos grcos) disponveis na tabela de codicao usada na mquina em causa (ver explicao mais abaixo). Quando um programa executado 2 , a grelha de clulas ca sua disposio. As sadas do programa fazem-se escrevendo caracteres nessa grelha, o que se consegue inserindo-os no chamado canal 3 de sada cout. Pelo contrrio as entradas fazemse lendo caracteres do teclado, o que se consegue extraindo-os do chamado canal de entrada cin. No modelo de consola usado, os caracteres correspondentes s teclas premidas pelo utilizador do programa no so simplesmente postos disposio do programa em execuo: so tambm mostrados na grelha de clulas da consola de comandos, que assim mostrar uma mistura de informao inserida pelo utilizador do programa e de informao gerada pelo prprio programa. O texto surge na consola de comandos de cima para baixo e da esquerda para a direita. Quando uma linha de clulas est preenchida, o texto continua a ser escrito na linha seguinte. Se a consola estiver cheia, a primeira linha descartada e o contedo de cada uma das linhas restantes deslocado para a linha imediatamente acima, deixando uma nova linha disponvel na base da grelha. Num programa em C++ o canal de sada para o ecr 4 designado por cout. O canal de entrada de dados pelo teclado designado por cin. Para efectuar uma operao de escrita no ecr usa-se o operador de insero < <. Para efectuar uma operao de leitura de dados do teclado usa-se o operador de extraco > >. No programa do mdc o resultado apresentado pela seguinte instruo:
cout < < "O mximo divisor comum de " < < m < < " e " < < n < < " " < < k < < . < < endl;

Admitindo, por exemplo, que as variveis do programa tm os valores indicados na Figura 1.5(b), o resultado ser aparecer escrito na consola:
O mximo divisor comum de 12 e 8 4.
O que se consegue em Linux escrevendo na consola o seu nome (precedido de ./) depois do pronto (prompt). Optou-se por traduzir stream por canal em vez de uxo, com se faz noutros textos, por parecer uma abstraco mais apropriada. 4 Para simplicar chamar-se- muitas vezes ecr grelha de clulas que constitui a consola.
3 2

24

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Por outro lado, as entradas so lidas do teclado pelas seguintes instrues:


cout < < "Introduza dois inteiros positivos: "; int m, n; cin > > m > > n; // Assume-se que m e n so positivos!

A primeira instruo limita-se a escrever no ecr uma mensagem pedindo ao utilizador para introduzir dois nmeros inteiros positivos, a segunda serve para denir as variveis para onde os valores dos dois nmeros sero lidos e a terceira procede leitura, ou extrao, propriamente dita. Aps uma operao de extraco de valores do canal de entrada, a varivel para onde a extraco foi efectuada toma o valor que foi inserido no teclado pelo utilizador do programa, desde que esse valor possa ser tomado por esse tipo de varivel 5 . Ao ser executada uma operao de leitura como acima, o computador interrompe a execuo do programa at que seja introduzido algum valor no teclado. Todos os espaos em branco so ignorados. Consideram-se espaos em branco os espaos propriamente ditos ( ), os tabuladores (que so os caracteres que se insere quando se carrega na tecla tab ou e os ns-de-linha |) (que so caracteres especiais que assinalam o m de uma linha de texto e que se insere quando se carrega na tecla return ou ). Para tornar evidente a presena de um espao quando se mostra um sequncia de caracteres usar-se- o smbolo . Compare-se
Este texto tem um tabulador aqui e um espao no fim

com
Este texto tem um tabulador aqui e um espao no fim |

O manipulador endl serve para mudar a impresso para a prxima linha da grelha de clulas sem que a linha corrente esteja preenchida. Por exemplo, as instrues
cout cout cout cout << << << << "****"; "***"; "**"; "*";

resultam em
**********

Mas se se usar o manipulador endl


Mais tarde se ver como vericar se o valor introduzido estava correcto, ou seja, como vericar se a operao de extraco teve sucesso.
5

2.1. INTRODUO
cout cout cout cout << << << << "****" < < endl; "***" < < endl; "**" < < endl; "*" < < endl;

25

o resultado
**** *** ** *

Pode-se usar uma nica instruo de insero para produzir o mesmo resultado, embora seja recomendvel dividir essa instruo em vrias linhas, por forma a que seja mais evidente o aspecto grco com que o ecr car depois de executada a instruo:
cout < < << << << "****" < < endl "***" < < endl "**" < < endl "*" < < endl;

O mesmo efeito do manipulador endl pode ser obtido incluindo no texto a escrever no ecr a sequncia de escape \n (ver explicao mais abaixo). Ou seja;
cout < < << << << "****\n" "***\n" "**\n" "*\n";

A utilizao de canais de entrada e sada, associados no apenas ao teclado e ao ecr mas tambm a cheiros arbitrrios no disco, ser vista mais tarde.

2.1.2 Denio de variveis


Na maior parte das linguagens de programao de alto nvel as variveis tem de ser denidas 6 antes de utilizadas. A denio das variveis serve para indicar claramente quais so as suas duas caractersticas estticas, nome e tipo, e qual o seu valor inicial, se for possvel indicar um que tenha algum signicado no contexto em causa. No programa em anlise podem ser encontradas trs denies de variveis em duas instrues de denio:
int m, n; int k;
6

Ou pelo menos declaradas.

26

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Estas instrues denem as variveis m e n, que contero os valores inteiros dos quais se pretende saber o mdc, e a varivel k que conter o desejado valor do mdc. Ver-se- mais tarde que de toda a convenincia inicializar as variveis, i.e., indicar o seu valor inicial durante a sua denio. Por exemplo, para inicializar k com o valor 121 poder-se-ia ter usado:
int k = 121;

No entanto, no exemplo dado, nenhuma das variveis pode ser inicializada com um valor que tenha algum signicado. As variveis m e n tm de ser lidas por operaes de extraco, pelo que a sua inicializao acabaria por no ter qualquer efeito prtico, uma vez que os valores iniciais seriam imediatamente substitudos pelos valores extrados do canal de entrada. varivel k, por outro lado, s se pode atribuir o valor desejado depois de saber qual a menor das outras duas variveis, pelo que a sua inicializao tambm seria intil 7 . A denio de variveis e os seus possveis tipos sero vistos em pormenor nas Seces 2.2 e 2.3.

2.1.3 Controlo de uxo


A parte mais interessante do programa apresentado a segunda, correspondente ao algoritmo desenvolvido no captulo anterior. A encontram-se as instrues do programa que permitem alterar o uxo normal de execuo, que ocorre normalmente de cima para baixo e da esquerda para a direita ao longo das instrues dos programas. So as chamadas instrues de seleco (neste caso um if else) e as instrues iterativas (neste caso um while). O objectivo de uma instruo de seleco permitir a seleco de duas instrues alternativas de acordo com o valor lgico de uma determinada condio. No programa existe uma instruo de seleco:
if(m < n)
Na realidade seria possvel proceder inicializao recorrendo funo (ver Captulo 3) min(), desde que se acrescentasse a incluso do cheiro de interface apropriado (algorithm): #include <iostream> #include <algorithm> using namespace std; int main() {
...
7

int k = min(m, n);


...

2.1. INTRODUO
k = m; else k = n;

27

Esta instruo de seleco coloca em k o menor dos valores das variveis m e n. Para isso executa alternativamente duas instrues de atribuio, que colocam na varivel k o valor de m se m < n, ou o valor de n no caso contrrio, i.e., se n m. As instrues de seleco if tm sempre um formato semelhante ao indicado, com as palavras-chave if e else a preceder as instrues alternativas, e com a condio entre parnteses logo aps a palavra-chave if. Uma instruo de seleco pode controlar a execuo de sequncias de instrues, bastando para isso coloc-las entre chavetas:
if(x < y) { int aux = x; x = y; y = aux; }

Tal como no exemplo anterior, possvel omitir a segunda instruo alternativa, aps a palavrachave else, embora nesse caso a instruo if se passe a chamar instruo condicional e j no instruo de seleco. importante notar que a atribuio se representa, em C++, pelo smbolo =. Assim, a instruo
k = m;

no deve ser lida como k igual a m, mas sim, k ca com o valor de m. O objectivo de uma instruo iterativa repetir uma instruo controlada, i.e., construir um ciclo. No caso de uma instruo while, o objectivo , enquanto uma condio for verdadeira, repetir a instruo controlada. No programa existe uma instruo iterativa while:
while(m % k != 0 or n % k != 0) { --k; }

O objectivo desta instruo , enquanto k no for divisor comum de m e n, ir diminuindo o valor de k. Tal como no caso das instrues de seleco, tambm nas instrues iterativas pode haver apenas uma instruo controlada ou uma sequncia delas, desde que envoltas por chavetas. No exemplo acima as chavetas so redundantes, pelo que se pode escrever simplesmente
while(m % k != 0 or n % k != 0) --k;

28

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Nesta instruo iterativa h duas expresses importantes. A primeira a expresso --k na instruo controlada pelo while, consistindo simplesmente na aplicao do operador de decrementao prexo, que neste caso se limita a diminuir de um o valor guardado em k. A segunda a expresso usada como condio do ciclo, ou seja, como guarda. Na expresso usada como guarda encontram-se trs operadores: 1. O operador or que calcula a disjuno dos valores lgicos de duas expresses condicionais. Ou seja, um operador lgico correspondente ao ou da linguagem natural e ao smbolo em notao matemtica (embora com particularidades que sero vistas mais tarde). 2. O operador !=, que resulta no valor V (verdadeiro) se os seus operandos forem diferentes e no valor F (falso) se eles forem iguais. A notao usada para a diferena deve-se inexistncia do smbolo = na generalidade dos teclados (e mesmo das tabelas de codicao de caracteres). 3. O operador %, que resulta no resto da diviso inteira do primeiro operando pelo segundo. O smbolo usado (percentagem) deve-se inexistncia do smbolo na generalidade do teclados. As instrues de seleco e de iterao e o seu desenvolvimento sero estudados no Captulo 4. Os operadores e as expresses com eles construdas sero pormenorizado na Seco 2.7.

2.2 Variveis
2.2.1 Memria e inicializao
Os computadores tm memria, sendo atravs da sua manipulao que os programas eventualmente acabam por chegar aos resultados pretendidos (as sadas). As linguagens de alto nvel, como o C++, escondem a memria por trs do conceito de varivel, que so uma forma estruturada de lhe aceder. As variveis so, na realidade, pedaos de memria a que se atribui um nome, que tm um determinado contedo ou valor, e cujo contedo interpretado de acordo com o tipo da varivel. Todas as variveis tm um dado tipo. Uma varivel pode ser, por exemplo, do tipo int em C++. Se assim for, essa varivel ter sempre um valor inteiro dentro de uma gama de valores admissvel. Os tipos das variveis no passam, na realidade, de uma abstraco. Todas as variveis, independentemente do seu tipo, so representadas na memria do computador por padres de bits8 , os famosos zeros e uns, colocados na zona de memria atribuda a essa varivel. O tipo de uma varivel indica simplesmente como o padro de bits associado varivel deve ser interpretado.
8

Dgitos binrios, do ingls binary digit.

2.2. VARIVEIS

29

Cada varivel tem duas caractersticas estticas, o nome e o tipo, e uma caracterstica dinmica, o seu valor9 , tal como nos algoritmos. Antes de usar uma varivel necessrio indicar ao compilador qual o seu nome e tipo, de modo a que a varivel possa ser construda, i.e., car associada a uma zona de memria, e de modo a que a forma de interpretar os padres de bits nessa zona de memria que estabelecida. Uma instruo onde se contri uma varivel com um dado nome, de um determinado tipo, e com um determinado valor inicial, denomina-se denio. A instruo
int a = 10;

a denio de uma varivel chamada a que pode guardar valores do tipo int (inteiros) e cujo valor inicial o inteiro 10. A sintaxe das denies pode ser algo complicada, mas em geral tem a forma acima, isto , o nome do tipo, seguido do nome da varivel, e seguido de uma inicializao. Uma forma intuitiva de ver uma varivel imagin-la como uma folha de papel com um nome associado e onde se decidiu escrever apenas nmeros inteiros, por exemplo. No entanto, essa folha de papel pode conter apenas um valor em cada instante, pelo que a escrita de um novo valor implica o apagamento do anterior, e tem de conter sempre um valor, mesmo inicialmente ( como se a folha viesse j preenchida de fbrica). A notao usada para representar gracamente uma varivel a introduzida no captulo anterior. A Figura 2.1 mostra a representao grca da varivel a denida acima. a: int 10 Figura 2.1: Notao grca para uma varivel denida por int a = 10;. O nome e o tipo da varivel esto num compartimento parte, no topo, e tm de ser sublinhados. Quando uma varivel denida, o computador reserva para ela uma zona da memria com o nmero de bits necessrio para guardar um valor do tipo indicado (essas reservas so feitas em mltiplos de uma unidade bsica de memria, tipicamente com oito bits, ou seja, um octeto ou byte10 ). Se a varivel no for explicitamente inicializada, essa posio de memria conter inicialmente um padro de bits arbitrrio, desconhecido, a que se d usualmente o nome de lixo. Por exemplo, se se tivesse usado a denio
int a;

a varivel a conteria um padro de bits arbitrrio e portanto o seu valor seria arbitrrio. Diz-seia que a continha lixo. Para evitar esta arbitrariedade, que pode ter consequncias nefastas no
Mais tarde se aprender que existe um outro gnero de variveis que no tm nome. Estas variveis, que se dizem annimas, podem ser variveis temporrias, que existem apenas durante o clculo de expresses, ou as variveis dinmicas introduzidas no Captulo 11. De igual forma se ver que existem constantes, que partilham todas as caractersticas das variveis, embora o seu valor tambm seja esttico. 10 Em rigor a dimenso dos bytes pode variar de mquina para mquina, no tendo de ter forosamente oito bits.
9

30

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

comportamento do programa se no se tiver cuidado, ao denir uma varivel deve-se, sempre que possvel e razovel, inicializ-la com um dado valor inicial, tal como indicado na primeira denio. A inicializao de uma varivel pode ser feita de duas formas alternativas 11 :
int a = 10; // como originalmente. int a(10); // forma alternativa.

A sintaxe das denies de variveis tambm permite que se dena mais do que uma varivel numa s instruo. Por exemplo,
int a = 0, b = 1, c = 2;

dene trs variveis, todas do tipo int.

2.2.2 Identicadores
Os nomes de variveis, e em geral de todas as entidades em C++, i.e., os identicadores, podem ser constitudos por letras12 (sendo as minsculas distinguidas das maisculas), dgitos decimais, e tambm pelo caractere _ (sublinhado ou underscore), que se usa normalmente para aumentar a legibilidade do nome. Um identicador no pode conter espaos nem pode comear por um dgito. Durante a traduo do programa, o compilador, antes de fazer uma anlise sintctica do programa, durante a qual verica a gramtica do programa, faz uma anlise lexical, durante a qual identica os smbolos (ou tokens) que constituem o texto. Esta anlise lexical gulosa: os smbolos detectados (palavras, sinais de pontuao, etc.) so to grandes quanto possvel, pelo que lValor sempre interpretado como um nico identicador, e no como o identicador l seguido do identicador Valor. Os identicadores devem ser to auto-explicativos e claros quanto possvel. Se uma varivel guarda, por exemplo, o nmero de alunos numa turma, deve chamar-se nmero_de_alunos_na_turma ou ento nmero_de_alunos
Na realidade as duas formas no so rigorosamente equivalentes (ver Seco C.1). A norma do C++ especica que de facto se pode usar qualquer letra. Infelizmente a maior parte dos compiladores existentes recusa-se a aceitar letras acentuadas ou letras especiais, tais como ou . Este texto foi escrito ignorando esta restrio prtica por duas razes:
12 11

1. Mais tarde ou mais cedo os compiladores sero corrigidos e o autor no precisar de mais do que eliminar esta nota :-). 2. O autor claramente prefere escrever com acentos. Por exemplo, Cgado...

2.3. TIPOS BSICOS

31

se o complemento na turma for evidente dado o contexto no programa. claro que o nome usado para uma varivel no tem qualquer importncia para o compilador, mas uma escolha apropriada dos nomes pode aumentar grandemente a legibilidade dos programas (pelos humanos...).

2.2.3 Inicializao
As zonas de memria correspondentes s variveis contm sempre um padro de bits: no h bits vazios. Assim, as variveis tm sempre um valor qualquer. Depois de uma denio, se no for feita uma inicializao explcita conforme sugerido atrs, a varivel denida contm um valor arbitrrio13 . Uma fonte frequente de erros o esquecimento de inicializar as variveis denidas. Por isso, recomendvel a inicializao de todas as variveis to cedo quanto possvel, desde que essa inicializao tenha algum signicado para o resto do programa (e.g., no exemplo da Seco 1.4 essa inicializao no era possvel 14 ). Da mesma forma, as variveis devem-se denir to tarde quanto possvel, i.e., to prximo quanto for possvel da sua primeira utilizao, por forma a facilitar a sua inicializao com um valor que faa sentido e a evitar a utilizao dessa varivel por engano em instrues intermdias. Existem algumas circunstncias nas quais as variveis de tipos bsicos do C++ so inicializadas implicitamente com zero (ver Seco 3.2.15). Deve-se evitar fazer uso desta caracterstica do C++ e inicializar explicitamente todas as variveis.

2.3 Tipos bsicos


O tipo das variveis est relacionado directamente com o conjunto de possveis valores tomados pelas variveis desse tipo. Para poder representar e manipular as variveis na memria do computador, o compilador associa a cada tipo no s o nmero de bits necessrio para a representao de um valor desse tipo na memria do computador, mas tambm a forma como os padres de bits guardados em variveis desse tipo devem ser interpretados. A linguagem C++ tem denidos a priori alguns tipos: os chamados tipos bsicos ou primitivos do C++. O paradigma de programao que usa na parte inicial destas folhas o paradigma de programao procedimental. S depois se estudaro dois paradigmas de programao alternativos que passam por acrescentar linguagem novos tipos mais apropriados para os problemas em causa: a programao baseada em objectos (Captulo 7 e, sobretudo, Captulo 8) e a programao orientada para objectos (Captulo 12). At l os nicos tipos disponveis so os tipos bsicos. Nas tabelas seguintes so apresentados os tipos bsicos existentes no C++ e a gama de valores que podem representar em Linux sobre Intel 15 . muito importante notar que os computadores
Desde que seja de um tipo bsico do C++ e em alguns outros casos. A armao no verdadeira para classes em geral (ver Captulo 7). 14 Ou melhor, a inicializao possvel desde que se use o operador (meio extico) ?: (ver Seco 4.4.1): int k = m < n ? m : n; Ambas as tabelas se referem ao compilador de C++ g++ da GNU Compiler Collection (GCC 3.2) num sistema Linux, distribuio Caixa Mgica, ncleo 2.4.14, correndo sobre a arquitectura Intel, localizado para a Europa Ocidental.
15 13

32

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

so mquinas nitas: tudo limitado, desde a memria ao tamanho da representao dos tipos em memria. Assim, a gama de diferentes valores possveis de guardar em variveis de qualquer tipo limitada. A gama de valores representvel para cada tipo de dados pode variar com o processador e o sistema operativo. Por exemplo, no sistema operativo Linux em processadores Alpha, os long int (segunda tabela) tm 64 bits. Em geral s se pode armar que a gama dos long sempre suciente para abarcar qualquer int e a gama dos int sempre suciente para abarcar qualquer short, o mesmo acontecendo com os long double relativamente aos double e com os double relativamente aos float. Tabela 2.1: Tipos bsicos elementares. Tipo bool int float char Descrio valor booleano ou lgico nmero inteiro nmero racional (representao IEEE 754 [6]) caractere (cdigo Latin-1) Gama V eF 231 a 231 1 (-2147483648 a 2147483647) 1,17549435 1038 a 3,40282347 1038 (e negativos) A maior parte dos caracteres grcos em uso. Exemplos: a, A, 1, !, *, etc. Bits 8 32 32 8

Tabela 2.2: Outros tipos bsicos. Tipo short [int] unsigned short [int] unsigned [int] long [int] unsigned long [int] double Descrio nmero inteiro nmero inteiro positivo nmero inteiro positivo nmero inteiro nmero inteiro positivo nmero racional (representao IEEE 754 [6]) nmero racional (representao IEEE 754 [6]) Gama 215 a 2151 (-32768 a 32767) 0 a 216 1 (0 a 65535) 0 a 2 32 1 (0 a 4294967295) a mesma que int a mesma que unsigned int 2,2250738585072014 10308 a 1,7976931348623157 10308 (e negativos) 3,36210314311209350626 104932 a 1,18973149535723176502 104932 (e negativos) Bits 16 16 32 32 32 64 96

long double

Alguns dos tipos derivados de int podem ser escritos de uma forma abreviada: os parnteses rectos na Tabela 2.2 indicam a parte opcional na especicao do tipo. O qualicador signed tambm pode ser usado, mas signed int e int so sinnimos. O nico caso em que este qualicador faz diferena na construo signed char, mas apenas em mquinas onde os char no tm sinal.

2.3. TIPOS BSICOS

33

A representao interna dos vrios tipos pode ser ou no relevante para os programas. A maior parte das vezes no relevante, excepto quanto ao facto de que se deve sempre estar ciente das limitaes de qualquer tipo. Por vezes, no entanto, a representao muito relevante, nomeadamente quando se programa ao nvel do sistema operativo e se tem de aceder a bits individuais. Assim, apresentam-se em seguida algumas noes sobre a representao usual dos tipos bsicos do C++. Esta matria ser pormenorizada na disciplina de Arquitectura de Computadores.

2.3.1 Tipos aritmticos


Os tipos aritmticos so todos os tipos que permitem representar nmeros (int, float e seus derivados). Variveis (e valores literais, ver Seco 2.4) destes tipos podem ser usadas para realizar operaes aritmticas e relacionais, que sero vistas nas prximas seces. Os tipos derivados de int chamam-se tipos inteiros. Os tipos derivados de float chamam-se tipos de vrgula utuante. Os char, em rigor, tambm so tipos aritmticos e inteiros, mas sero tratados parte. Representao de inteiros Admita-se, para simplicar, que num computador hipottico as variveis do tipo unsigned int tm 4 bits (normalmente tm 32 bits). Pode-se representar esquematicamente uma dada varivel do tipo unsigned int como b3 b2 b1 b0

em que os bi com i = 0, , 3 so bits, tomando portanto os valores 0 ou 1. fcil vericar que existem apenas 24 = 16 padres diferentes de bits possveis de colocar numa destas variveis. Como associar valores inteiros a cada um desses padres? A resposta mais bvia a que usada na prtica: considera-se que o valor representado (b 3 b2 b1 b0 )2 , i.e., que os bits so dgitos de um nmero expresso na base binria (ou seja, na base 2). Por exemplo: 1 0 0 1

o padro de bits correspondente ao valor (1001) 2 , ou seja 1 23 + 0 22 + 0 21 + 1 20 = 9, em decimal16 . Suponha-se agora que a varivel contm
Os nmeros inteiros podem-se representar de muitas formas. A representao mais evidente, corresponde a desenhar um trao por cada unidade: ||||||||||||| representa o inteiro treze (treze por sua vez outra representao, por extenso). A representao romana do mesmo inteiro XIII. A representao rabe posicional e a mais prtica. Nela usam-se sempre os mesmos 10 dgitos, que vo aumentando de peso da direita para a esquerda (o peso multiplicado sucessivamente por 10), e onde o dgito zero fundamental. A representao rabe do mesmo inteiro 13, que signica 1 10 + 3. A representao rabe usa 10 dgitos e por isso diz-se que usa a base 10, ou
16

34

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


1 1 1 1

e que se soma 1 ao seu contedo: o resultado (1111 + 1) 2 = (10000) 2 . Mas este valor no representvel num unsigned int de quatro bits! Um dos bits tem de ser descartado. Normalmente escolhe-se guardar apenas os 4 bits menos signicativos do resultado, pelo que, no computador hipottico em que os inteiros tm 4 bits, (1111 + 1) 2 = (0000)2 . Ou seja, nesta aritmtica binria com um nmero limitado de dgitos, tudo funciona como se os valores possveis estivessem organizados em torno de um relgio, neste caso em torno de um relgio com 16 horas, ver Figura 2.2, onde aps as (1111) 2 = 15 horas fossem de novo (0000) 2 = 0 horas17 .
1111 1110 1101 0000 0001

15 14

1 2 +1

0010

13

3 0011 4 0100 5 0101

1100 12 1011 11 1010

10 9
1001

6 8
1000

7
0111

0110

Figura 2.2: Representao de inteiros sem sinal com quatro bits. A extenso destas ideias para, por exemplo, 32 bits trivial: nesse caso o relgio seria enorme, pois teria 232 horas, de 0 a 232 1, que , de facto, a gama dos unsigned int no Linux com a congurao apresentada. extremamente importante recordar as limitaes dos tipos. Por exemplo, como os valores das variveis do tipo int no podem crescer indenidamente, ao se atingir o topo do relgio
que a representao decimal. Pode-se usar a mesma representao posicional com qualquer base, desde que se forneam os respectivos dgitos. Em geral a notao posicional tem a forma (dn1 dn2 d1 d0 )B onde B a base da representao, n o nmero de dgitos usado neste nmero em particular, e d i com i = 0, , n 1 so os sucessivos dgitos, sendo que cada um deles pertence a um conjunto com B possveis dgitos possuindo valores de 0 a B 1. O nmero representado por (dn1 dn2 d1 d0 )B pode ser calculado como
n1

di B i = dn1 B n1 + dn2 B n2 + + d1 B 1 + d0 B 0 .
i=0

Para B = 2 (numerao binria) o conjunto de possveis dgitos, por ordem crescente de valor, {0, 1}, para B = 8 (numerao octal) {0, 1, 2, 3, 4, 5, 6, 7}, para B = 10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, e para B = 16 (numerao hexadecimal) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F}, onde falta de smbolos se recorreu s letras de A a F. Quando se omite a indicao explcita da base, assume-se que esta 10. 17 Tipicamente no topo do relgio estaria 16, e no zero, mas optar-se- pela numerao a comear em zero, que quase sempre mais vantajosa (ver Nota 4 na pgina 239).

2.3. TIPOS BSICOS


volta-se a zero!

35

Falta vericar como representar inteiros com sinal, i.e., incluindo no s valores positivos mas tambm valores negativos. Suponha-se que os int tm, de novo num computador hipottico, apenas 4 bits. Admita-se uma representao semelhante dos unsigned int, mas diga-se que, no lugar das 15 horas (padro de bits 1111), mesmo antes de chegar de novo ao padro 0000, o valor representado x, ver Figura 2.3. Pelo que se disse anteriormente, somar 1 a x corresponde a rodar o ponteiro do padro 1111 para o padro 0000. Admitindo que o padro 0000 representa de facto o valor 0, tem-se x + 1 = 0. Conclui, por isso, que o padro 1111 um bom candidato para representar o valor x = 1! Estendendo o argumento anterior, pode-se dizer que as 14 horas correspondem representao de -2, e assim sucessivamente, como se indica na Figura 2.4.
0000

1111 1110 1101 1100 ? 1011 ? 1010

x ?

0001

1 2 +1

0010

0011

4 0100 5 0101 ? ?
1001

6 8
1000

7
0111

0110

Figura 2.3: Como representar os inteiros com sinal? O salto nos valores no relgio deixa de ocorrer do padro 1111 para o padro 0000 (15 para 0 horas, se interpretados como inteiros sem sinal), para passar a ocorrer na passagem do padro 0111 para o padro 1000 (das 7 para as -8 horas, se interpretados como inteiros com sinal). A escolha deste local para a transio no foi arbitrria. Em primeiro lugar permite representar um nmero semelhante de valores positivos e negativos: 7 positivos e 8 negativos. Deslocando a transio de uma hora no sentido dos ponteiros do relgio, poder-se-ia alternativamente representar 8 positivos e 7 negativos. A razo para a escolha apresentada na gura acima prende-se com o facto de, dessa forma, a distino entre no-negativos (positivos ou zero) e negativos se poder fazer olhando apenas para o bit mais signicativo (o bit mais esquerda), que quando 1 indica que o valor representado negativo. A esse bit chama-se, por isso, bit de sinal. Esta representao chama-se representao em complemento para 2. Em Arquitectura de Computadores caro mais claras as vantagens desta representao na simplicao do hardware do computador encarregue de realizar operaes com valores inteiros. Como saber, olhando para um padro de bits, qual o valor representado, no caso de se estarem a usar inteiros com sinal? Primeiro olha-se para o bit de sinal. Se for 0, interpreta-se o padro de bits como um nmero binrio: esse o valor representado. Se o bit de sinal for 1, ento o valor

36

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


0000

1111 1110 1101

0001

1 2 +1

0010

3 0011 4 0100 5 0101 6

1100 4 1011 5

6
1010

7
1001

8
1000

7
0111

0110

Figura 2.4: Representao de inteiros com sinal com quatro bits. representado igual ao valor binrio correspondente ao padro de bits subtrado de 16. Por exemplo, observando o padro 1011, conclui-se que representa o valor negativo (1011) 2 16 = 11 16 = 5, como se pode conrmar na Figura 2.4.

A extenso destas ideias para o caso dos int com 32 bits muito simples. Nesse caso os valores representados variam de 2 31 a 231 1 e a interpretao dos valores representados faz-se como indicado no pargrafo acima, s que subtraindo 2 32 , em vez de 16, no caso dos valores negativos. Por exemplo, os padres 00000000000000000000010000000001 e 10000000000000000000000000000111 representam os valores 1025 e -2147483641, como se pode vericar facilmente com uma mquina de calcular. Representao de valores de vrgula utuante Os tipos de vrgula utuante destinam-se a representar valores decimais, i.e., uma gama limitada dos nmeros racionais. Porventura a forma mais simples de representar valores racionais passa por usar exactamente a mesma representao que para os inteiros com sinal (complemento para dois), mas admitindo que todos os valores devem ser divididos por uma potncia xa de 2. Por exemplo, admitindo que se usam 8 bits na representao e que a diviso feita por 16 = 24 = (10000)2 , tem-se que o padro 01001100 representa o valor (01001100) 2 = (0100, 1100) 2 = 4, 75. (10000)
2

Esta representao corresponde a admitir que os bits representam um valor binrio em que a vrgula se encontra quatro posies para a esquerda

2.3. TIPOS BSICOS


b3 b2 b1 b0 , b1 b2 b3 b4

37

do que acontecia na representao dos inteiros, onde a vrgula est logo aps o bit menos signicativo (o bit mais direita) b7 b6 b5 b4 b3 b2 b1 b0

O valor representado por um determinado padro de bits b 3 b2 b1 b0 b1 b2 b3 b4 dado por


3

bi 2i .
i=4 1 O menor valor positivo representvel nesta forma 16 = 0, 0625. Por outro lado, s se conseguem representar valores de -8 a -0,0625, o valor 0, e valores de 0,0625 a 7,9375. O nmero de dgitos decimais de preciso est entre dois e trs (um antes da vrgula e entre um e dois depois da vrgula). Caso se utilize 32 bits e se desloque a vrgula 16 posies para a esquerda, o menor valor positivo representvel 21 = 0, 00001526, e so representveis valores de -32768 16 a -0,00001526, o valor 0, e valores de 0,00001526 a 32767,99998, aproximadamente, correspondendo a entre nove e 10 dgitos decimais de preciso, cinco antes da vrgula e entre quatro e cinco depois depois da vrgula.

A este tipo de representao chama-se vrgula xa, por se colocar a vrgula numa posio xa e pr-determinada. A representao em vrgula xa impe limitaes considerveis na gama de valores representveis: no caso dos 32 bits com vrgula 16 posies para a esquerda, representam-se apenas valores entre aproximadamente 1, 5 10 5 e 3, 8 105 , em mdulo. Este problema resolve-se usando uma representao em que a vrgula no est xa, mas varia consoante as necessidades: a vrgula passa a utuar. Uma vez que especicar a posio da vrgula o mesmo que especicar uma potncia de dois pela qual o valor deve ser multiplicado, a representao de vrgula utuante corresponde a especicar um nmero da forma m 2 e , em que a m se chama mantissa e a e se chama expoente. Na prtica esta representao no usa a representao em complemento para dois em nenhum dos seus termos, pelo que inclui um termo de sinal s m 2e , em que m sempre no-negativo e s o termo de sinal, valendo -1 ou 1. A representao na memria do computador de valores de vrgula utuante passa, pois, por dividir o padro de bits disponvel em trs zonas com representaes diferentes: o sinal, a mantissa e o expoente. Como parte dos bits tm de ser usados para o expoente, que especica a localizao da vrgula utuante, o que se ganha na gama de valores representveis perdese na preciso desses mesmo valores. Na maior parte dos processadores utilizados hoje em dia usa-se uma das representaes especicadas na norma IEEE 754 [6] que, no caso de valores representados em 32 bits (chamados de preciso simples e correspondentes ao tipo oat do C++),

38

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


1. atribui um bit (s0 ) ao sinal (em que 0 signica positivo e 1 negativo), 2. atribui 23 bits (m1 a m23 ) mantissa, que so interpretados como um valor de vrgula xa, sem sinal, com vrgula imediatamente antes do bit mais signicativo, e 3. atribui oito bits (e7 a e0 ) ao expoente, que so interpretados como um inteiro entre 0 e 255 a que se subtrai 127, pelo que o expoente varia entre -126 e 127 (os valores 0 e 255 antes da subtraco so especiais, pelo que o expoente no pode tomar os valores -127 nem 128);

ou seja, s0 1, m1 m23 e7 e0

Os valores representados desta forma so calculados como se indica na Tabela 2.3. Tabela 2.3: Valor representado em formato de vrgula utuante de preciso simples de acordo com IEEE 754 [6]. O sinal dado por s0 . Mantissa m1 m23 = 0 0 m1 m23 = 0 0 m1 m23 m1 m23 = 0 0 m1 m23 = 0 0 Expoente e 7 e0 = 0 0 e 7 e0 = 0 0 e7 e 0 e7 e 0 e 7 e0 e 7 e0 = 00 = 11 = 11 = 11 Valor representado 0 (0, m1 m23 )2 2126 (valores no-normalizados18 ) (1, m1 m23 )2 2(e7 e0 )2 127 (valores normalizados18 ) (valores especiais)

Quando os bits do expoente no so todos 0 nem todos 1 (i.e., (e 7 e0 )2 no 0 nem 255), a mantissa tem um bit adicional esquerda, implcito, com valor sempre 1. Nesse caso diz-se que os valores esto normalizados18 . fcil vericar que o menor valor positivo normalizado representvel + (1,0 0)2 2(00000001) 2 127 = 2126 = 1,17549435 1038 e o maior + (1,1 1)2 2(11111110)2 127 = 224 1 2127 = 3,40282347 1038 223

18 Os valores normalizados do formato IEEE 754 [6] tm sempre, portanto, o bit mais signicativo, que implcito, a 1. Os valores no-normalizados tm, naturalmente, menor preciso (dgitos signicativos) que os normalizados, pois tm o bit mais signicativo, que implcito, com valor 0.

2.3. TIPOS BSICOS

39

Comparem-se estes valores com os indicados para o tipo float na Tabela 2.1. Com esta representao conseguem-se cerca de seis dgitos decimais de preciso, menos quatro que a representao de vrgula xa apresentada em primeiro lugar, mas uma gama bastante maior de valores representveis. Os tipos double tm uma representao semelhante apresentada, embora com maior preciso e gama, pois a sua representao feita usando respectivamente 64 e 96 bits no total. No que diz respeito programao, a utilizao de valores de vrgula utuante deve ser evitada sempre que possvel: se for possvel usar inteiros nos clculos prefervel us-los a recorrer aos tipos de vrgula utuante, que, apesar da sua aparncia inocente, reservam muitas surpresas. Em particular, importante recordar que os valores so representados com preciso nita, o que implica arredondamentos e acumulaes de erros. Outra fonte comum de erros prende-se com a converso de valores na base decimal para os formatos de vrgula utuante em base binria: valores inocentes como 0,2 no so representveis na base 2 usando um nmero nito de dgitos, pois so dzimos innitos peridicos! Ao estudo da forma de lidar com estes tipos de erros sem surpresas desagradveis d-se o nome de anlise numrica [3].

2.3.2 Booleanos ou lgicos


Existe um tipo bool cujas variveis guardam valores lgicos. A forma de representao usual tem oito bits e reserva o padro 00000000 para representar o valor falso (F), representando todos os outros padres o valor verdadeiro (V). Os valores verdadeiro e falso so representados em C++ pelos identicadores reservados (ou palavras-chave) true e false (que so tambm valores literais, ver Seco 2.4). Os valores booleanos podem ser convertidos em inteiros. Nesse caso o valor falso convertido em 0 e o valor verdadeiro convertido em 1. Por exemplo,
int i = int(true);

inicializa a varivel i com o valor inteiro 1.

2.3.3 Caracteres
Caracteres so smbolos representando letras, dgitos decimais, sinais de pontuao, etc. Cada caractere tem variadas representaes grcas, dependendo do seu tipo tipogrco. Por exemplo, a primeira letra do alfabeto em maiscula tem as seguintes representaes grcas (entre muitas outras): A

possvel denir variveis que guardam caracteres. O tipo char usado em C++ para esse efeito. Em cada varivel do tipo char armazenado o cdigo de um caractere. Esse cdigo consiste num padro de bits, correspondendo cada padro de bits a um determinado caractere. Cada padro de bits pode tambm ser interpretado como um nmero inteiro em binrio, das

40

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

formas que se viram atrs. Assim cada caractere representado por um determinado valor inteiro, o seu cdigo, correspondente a um determinado padro de bits. Em Linux sobre uma arquitectura Intel, as variveis do tipo char so representadas em memria usando 8 bits, pelo que existem 2 8 = 256 diferentes caracteres representveis. Existem vrias tabelas de codicao de caracteres diferentes, que a cada padro de bits fazem corresponder um caractere diferente. De longe a mais usada neste canto da Europa a tabela dos cdigos Latin-9 (ou melhor, ISO-8859-15, ver Apndice G), que uma extenso da tabela ASCII (American Standard Code for Information Interchange) incluindo os caracteres acentuados em uso na Europa Ocidental19 . Existem muitas destas extenses, que podem ser usadas de acordo com os caracteres que se pretendem representar, i.e., de acordo com o alfabeto da lngua mais utilizada localmente. Essas extenses so possveis porque o cdigo ASCII fazia uso apenas dos 7 bits menos signicativos de um caractere (i.e., apenas 128 das possveis combinaes de zeros e uns)20 . No necessrio, conhecer os cdigos dos caracteres de cor: para indicar o cdigo do caractere correspondente letra b usa-se o valor literal b. Claro est que o que ca guardado numa varivel no b, um padro de bits correspondente ao inteiro 98, pelo menos se se usar a tabela de codicao Latin-9. Esta traduo de uma representao habitual, b, para um cdigo especco, 98, feita pelo compilador e permite escrever programas sem quaisquer preocupaes relativamente tabela de codicao em uso. Decorre naturalmente do que se disse que, em C++, possvel tratar um char como um pequeno nmero inteiro. Por exemplo, se se executar o conjunto de instrues seguinte:
char caractere = i; caractere = caractere + 1; cout < < "O caractere seguinte : " < < caractere < < endl;

aparece no ecr
O caractere seguinte : j

O que sucedeu foi que se adicionou 1 ao cdigo do caractere i, de modo que a varivel caractere passou a conter o cdigo do caractere j. Este pedao de cdigo s produz o efeito apresentado se, na tabela de codicao que est a ser usada, as letras sucessivas do alfabeto possurem cdigos sucessivos. Isso pode nem sempre acontecer. Por exemplo, na tabela EBCDIC (Extended Binary Coded Decimal Interchange Code), j pouco usada, o caractere i tem
A tabela de codicao Latin-1, ou ISO-8859-1, esteve em vigor na Europa Ocidental at criao do euro. O suporte para o novo smbolo obrigou mudana para a nova tabela ISO-8859-15. 20 Existe um outro tipo de codicao, o Unicode, que suporta todos os caracteres de todas as expresses escritas vivas ou mortas em simultneo. Mas exige uma codicao diferente, com maior nmero de bits (o C++ possui um outro tipo, wchar_t, para representar caracteres com estas caractersticas). No unicode existem duas formas de codicao. O UTF-16 prope representaes com 16 bits, extensveis a 32 bits. O UTF-8 prope representaes onde os caracteres so representados por sequncias de um, dois, trs ou quatro bytes, sendo os caracteres da tabela ASCII representados com apenas oito bits e usando exactamente os mesmos padres de bits do ASCII. espectvel que a prazo o Unicode seja o nico tipo de codicao em uso.
19

2.3. TIPOS BSICOS

41

cdigo 137 e o caractere j tem cdigo 145! Num sistema que usasse esse cdigo, as instrues acima certamente no escreveriam a letra j no ecr. Os char so interpretados como inteiros em C++. Estranhamente, se esses inteiros tm ou no sinal no especicado pela linguagem. Quer em Linux sobre arquitecturas Intel quer no Windows NT, os char so interpretados como inteiros com sinal (ou seja, charsigned char), com uma gama de valores que varia entre -128 e 127, devendo-se usar o tipo unsigned char se se pretender forar uma interpretao dos caracteres como inteiros sem sinal, i.e., com uma gama de valores variando entre 0 e 255. O programa seguinte imprime no ecr todos os caracteres da tabela ASCII (que s especica os caracteres correspondentes aos cdigos de 0 a 127, isto , todos os valores positivos dos char em Linux e a primeira metade da tabela Latin-9) 21 :
#include <iostream> using namespace std; int main() { for(int i = 0; i != 128; ++i) cout < < "" < < char(i) < < " (" < < i < < ")" < < endl; }

Quando realizada uma operao de extraco de um canal para uma varivel do tipo char, lido apenas um caractere, mesmo que no teclado seja inserido um cdigo (ou mais do que um caractere), e so descartados os espaos em branco. I.e., o resultado das instrues
cout < < "Insira um caractere: "; char caractere; cin > > caractere; cout < < "O caractere inserido foi: " < < caractere < < "" < < endl;

seria
48 Insira um caractere: O caractere inserido foi: 4

caso o utilizador inserisse 48 (aps uns quantos espaos). O dgito oito foi ignorado visto que se leu apenas um caractere, e no o seu cdigo. Para ler qualquer caractere, incluindo os espaos em branco, deve-se usar a operao get() com o canal cin. Por exemplo, o resultado das instrues
Alguns dos caracteres escritos so especiais, representando mudanas de linha, etc. Por isso, o resultado de uma impresso no ecr de todos os caracteres da tabela de codicao ASCII pode ter alguns efeitos estranhos.
21

42

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


cout < < "Insira um caractere: "; char caractere; cin.get(caractere); cout < < "O caractere inserido foi: " < < caractere < < "" < < endl;

seria
Insira um caractere: 48 O caractere inserido foi:

admitindo que o utilizador digitava a mesma sequncia de caracteres que anteriormente.

2.4 Valores literais


Os valores literais permitem indicar explicitamente num programa valores dos tipos bsicos do C++. Alguns exemplos de valores literais (repare-se bem na utilizao dos suxos U, L e F) so:
false a 100 100U 100L 100.0 100.0F 100.0L 1.1e230 // // // // // // // // // do tipo bool, representa o valor lgico F. do tipo char, representa o cdigo do caractere a. do tipo int, valor 100 (em decimal). do tipo unsigned. do tipo long. do tipo double. do tipo float (e no double). do tipo long double. do tipo double, valor 1, 1 10230 , usa a chamada notao cientca.

Os valores inteiros podem ainda ser especicados em octal (base 8) ou hexadecimal (base 16). Inteiros precedidos de 0x so considerados como representados na base hexadecimal e portanto podem incluir, para alm dos 10 dgitos usuais, as letras entre A a F (com valores de 10 a 15), em maisculas ou em minsculas. Inteiros precedidos de 0 simplesmente so considerados como representados na base octal e portanto podem incluir apenas dgitos entre 0 e 7. Por exemplo22 :
0x1U // o mesmo que 1U. 0x10FU // o mesmo que 271U, ou seja (00000000000000000000000100001111)2 . 077U // o mesmo que 63U, ou seja (00000000000000000000000000111111)2 .

Em alguns casos a utilizao de valores literais nas instrues de um programa so um mau sinal. Isso ocorre quando esses valores literais representam valores importantes do ponto de
22

Os exemplos assumem que os int tm 32 bits.

2.5. CONSTANTES

43

vista do problema a resolver, que tm uma semntica prpria, que ocorrem em vrias instrues do programa, e que, por isso, seriam melhor representados recorrendo a constantes (ver Seco 2.5). Para alm dos valores literais para os tipos bsicos do C++ apresentados acima, h um tipo adicional de valores literais: as cadeias de caracteres. Correspondem a sequncias de caracteres colocados entre "". Para j no se dir qual o tipo destes valores literais, bastando saber-se que se podem usar para escrever texto no ecr (ver Seco 2.1.1 para mais informao sobre canais e Seco 5.4 para informao acerca de cadeias de caracteres). H alguns caracteres que so especiais. O seu objectivo no serem representados por um determinado smbolo, como se passa com as letras e dgitos, por exemplo, mas sim controlar a forma como o texto est organizado. Normalmente considera-se um texto como uma sequncia de caracteres, dos quais fazem parte caracteres especiais que indicam o nal das linhas, os tabuladores, ou mesmo se deve soar uma campainha quando o texto for mostrado. Os caracteres de controlo no podem ser especicados directamente como valores literais, uma vez que no tm representao grca. Para os representar usam-se as chamadas sequncias de escape, que correspondem a sequncias de caracteres com signicado especial e que so iniciadas por um caractere chamado de escape. Quando esse caractere ocorre num valor literal do tipo char (ou numa cadeia de caracteres), indica que se deve escapar da interpretao normal destes valores literais e passar a uma interpretao especial. Esse caractere a barra para trs (\) e serve para construir as sequncias de escape indicadas na Tabela 2.4.

2.5 Constantes
Nos programas em C++ tambm se pode denir constantes, i.e., variveis que no mudam de valor durante toda a sua existncia. Nas constantes o valor uma caracterstica esttica e no dinmica, como no caso das variveis. A denio de constantes faz-se colocando a palavra-chave23 const aps o tipo pretendido para a constante 24 . As constantes, justamente por o serem, tm obrigatoriamente de ser inicializadas no acto da denio. No caso das constantes de tipos bsicos, isso signica que o seu valor esttico tem de ser indicado na prpria denio. Por exemplo:
int const primeiro_primo = 2; char const primeira_letra_do_alfabeto_latino = a;

A constante primeiro_primo representada pela notao apresentada na Figura 2.5. As constantes devem ser usadas como alternativa aos valores literais quando estes tiverem uma semntica (um signicado) particular. O nome dado constante deve reectir exactamente esse signicado. Por exemplo, em vez de
Palavras-chave so palavras cujo signicado est pr-determinado pela prpria linguagem e portanto que no podem ser usadas para outros ns. Ver Apndice E. 24 A palavra-chave const tambm pode ser colocada antes do tipo. Porm, embora essa prtica seja corrente, no recomendvel dadas as confuses a que induz quando aplicada a ponteiros. Ver !!.
23

44

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Tabela 2.4: Sequncias de escape. A negrito as sequncias mais importantes. Sequncia \n \t \v \b \r \f \a \\ \? \ \" \ooo \xhhh Nome m-de-linha tabulador tabulador vertical espao para trs retorno do carreto nova pgina campainha caractere \ caractere ? caractere caractere " caractere com cdigo ooo em octal. caractere com cdigo hhh em hexadecimal. Signicado Assinala o m de uma linha. Salta para a prxima posio de tabulao.

O caractere \ serve de escape, num valor literal tem de se escrever \\. Evita a sequncia ?? que tem um signicado especial. Valor literal correspondente ao caractere plica \. Dentro duma cadeia de caracteres o caractere " escreve-se "\"". Pouco recomendvel: o cdigo dos caracteres muda com a tabela de codicao. Pouco recomendvel: o cdigo dos caracteres muda com a tabela de codicao.

primeiro_primo: int 2

Figura 2.5: Notao usada para as constantes.

2.5. CONSTANTES
double raio = 3.14; cout < < "O permetro " < < 2.0 * 3.14 * raio < < endl; cout < < "A rea " < < 3.14 * raio * raio < < endl;

45

prefervel25
double const pi = 3.14; double raio = 3.14; cout < < "O permetro " < < 2 * pi * raio < < endl; cout < < "A rea " < < pi * raio * raio < < endl;

H vrias razes para ser prefervel a utilizao de constantes no cdigo acima: 1. A constante tem um nome apropriado (com um signicado claro), o que torna o cdigo C++ mais legvel. 2. Se se pretender aumentar a preciso do valor usado para , basta alterar a inicializao da constante:
double const pi = 3.1415927; double raio = 3.14; cout < < "O permetro " < < 2 * pi * raio < < endl; cout < < "A rea " < < pi * raio * raio < < endl;

3. Se, no cdigo original, se pretendesse alterar o valor inicial do raio para 10,5, por exemplo, seria natural a tentao de recorrer facilidade de substituio de texto do editor. Uma pequena distraco seria suciente para substituir por 10,5 tambm a aproximao de , cujo valor original era, por coincidncia, igual ao do raio. O resultado seria desastroso:
double raio = 10.5; // Erro! Substituio desastrosa: cout < < "O permetro " < < 2 * 10.5 * raio < < endl; // Erro! Substituio desastrosa: cout < < "A rea " < < 10.5 * raio * raio < < endl;

Aos valores literais usados directamente no cdigo, sem recurso a constantes que lhes atribuam uma semntica clara, chama-se muitas vezes numeros mgicos. A programao simultaneamente uma arte e uma cincia, pelo que o recurso magia deve-se pr de parte: nunca se devem usar nmeros mgicos num programa!
A subtileza neste caso que o valor literal 3.14 tem dois signicados completamente diferentes! o valor do raio mas tambm uma aproximao de !
25

46

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

2.6 Instncias
A uma varivel ou constante de um dado tipo usual chamar-se, no jargo da programao orientada para os objectos, uma instncia desse tipo. Esta palavra, j existindo em portugus, foi importada do ingls recentemente com o novo signicado de exemplo concreto ou exemplar (cf. a expresso for instance, com o signicado de por exemplo). Assim, uma varivel ou constante de um tipo uma instncia ou exemplar dessa classe, tal como uma mulher ou um homem so instncias dos humanos. Instncia , por isso, um nome que se pode dar quer a variveis quer a constantes.

2.7 Expresses e operadores


O tipo de instruo mais simples, logo aps a instruo de denio de variveis, consiste numa expresso terminada por ponto-e-vrgula. Assim, as prximas so instrues vlidas, se bem que inteis:
; // expresso nula (instruo nula). 2; 1 + 2 * 3;

Para que os programas tenham algum interesse, necessrio que algo mude medida que so executados, i.e., que o estado da memria (ou de dispositivos associados ao computador, como o ecr, uma impressora, etc.) seja alterado. Isso consegue-se, por exemplo, alterando os valores das variveis atravs do operador de atribuio. As prximas instrues so potencialmente mais teis, pois agem sobre o valor de uma varivel:
int i = 0; // Atribuies: i = 2; i = 1 + 2 * 3;

Uma expresso composta por valores (valores literais, valores de instncias, etc) e operaes. Muitas vezes numa expresso existe um operador de atribuio = ou suas variantes 26 , ver Seco 2.7.5. A expresso
a = b + 3;

signica atribua-se varivel a o resultado da soma do valor da instncia b com o valor literal inteiro (do tipo int) 3. A expresso na ltima instruo de
Excepto quando a expresso for argumento de alguma funo ou quando controlar alguma instruo de seleco ou iterao, como se ver mais tarde.
26

2.7. EXPRESSES E OPERADORES


int i, j; bool so_iguais; ... so_iguais = i == j;

47

signica atribua-se varivel so_iguais o valor lgico (booleano, do tipo bool) da armao os valores das instncias i e j so iguais. Depois desta operao o valor de so_iguais verdadeiro se i for igual a j e falso no caso contrrio. Os operadores usuais em C++ so de um de trs tipos: unrios, se tiverem apenas um operando, binrios, se tiverem dois operandos, e ternrios, se tiverem trs operandos. Existem ainda outros tipos de operadores com uma aridade (nmero de operandos) maior, como o caso da invocao de rotinas, como se ver mais tarde.

2.7.1 Operadores aritmticos


Os operadores aritmticos so + adio (binrio), e.g., 2.3 + 6.7; + identidade (unrio), e.g., +5.5; - subtraco (binrio), e.g., 10 - 4; - simtrico (unrio), e.g., -3; * multiplicao (binrio), e.g., 4 * 5; / diviso (binrio), e.g., 10.5 / 3.0; e % resto da diviso inteira (binrio), e.g., 7 % 3. As operaes aritmticas preservam os tipos dos operandos, i.e., a soma de dois float resulta num valor do tipo float, etc. O signicado do operador diviso depende do tipo dos operandos usados. Por exemplo, o resultado de 10 / 20 0 (zero), e no 0,5. I.e., se os operandos da diviso forem de algum dos tipos aritmticos inteiros, ento a diviso usada a diviso inteira, que no usa casas decimais. Por outro lado, o operador % s pode ser usado com operandos de um tipo inteiro, pois a diviso inteira s faz sentido nesse caso. Os operadores de diviso e resto da diviso inteira esto especicados de tal forma que, quaisquer que sejam dois valores inteiros a e b, se verique a condio a == (a / b) * b + a % b. possvel, embora no recomendvel, que os operandos de um operador sejam de tipos aritmticos diferentes. Nesse caso feita a converso automtica do operando com tipo menos abrangente para o tipo do operando mais abrangente. Por exemplo, o cdigo
double const pi = 3.1415927; double x = 1 + pi;

48

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

leva o compilador a converter automaticamente o valor literal 1 do tipo int para o tipo double. Esta uma das chamadas converses aritmticas usuais, que se processam como se segue: Se algum operando for do tipo long double, o outro operando convertido para long double; caso contrrio, se algum operando for do tipo double, o outro operando convertido para double; caso contrrio, se algum operando for do tipo float, o outro operando convertido para float; caso contrrio, todos os operandos do tipo char, signed char, unsigned char, short int ou unsigned short int so convertidos para int, se um int puder representar todos os possveis valores do tipo de origem, ou para um unsigned int no caso contrrio (a estas converses chama-se promoes inteiras); depois, se algum operando for do tipo unsigned long int, o outro operando convertido para unsigned long int; caso contrrio, se um dos operandos for do tipo long int e o outro unsigned int, ento se um long int puder representar todos os possveis valores de um unsigned int, o unsigned int convertido para long int, caso contrrio ambos os operandos so convertidos para unsigned long int; caso contrrio, se algum dos operandos for do tipo long int, o outro operando convertido para long int; e caso contrrio, se algum dos operandos for do tipo unsigned int, o outro operando convertido para unsigned int. Estas regras, apesar de se ter apresentado apenas uma verso resumida, so complexas e pouco intuitivas. Alm disso, algumas destas converses podem resultar em perda de preciso (e.g., converter um long int num float pode resultar em erros de arredondamento, se o long int for sucientemente grande). prefervel, portanto, evitar as converses tanto quanto possvel. Assim, o cdigo acima deveria ser reescrito como
double const pi = 3.1415927; double x = 1.0 + pi;

de modo a que o valor literal fosse do mesmo tipo que a constante pi. Se no se tratar de um valor literal mas sim de uma instncia, ento prefervel converter explicitamente um ou ambos os operandos para compatibilizar os seus tipos double const pi = 3.1415927; int i = 1; double x = double(i) + pi; // as converses tm o formato // tipo(expresso).

2.7. EXPRESSES E OPERADORES

49

pois ca muito claro que o programador est consciente da converso e, presume-se, das suas consequncias. Em qualquer dos casos sempre boa ideia repensar o cdigo para perceber se as converses so mesmo necessrias, pois h algumas converses que podem introduzir erros de aproximao indesejveis e, em alguns casos, mesmo desastrosos.

2.7.2 Operadores relacionais e de igualdade


Os operadores relacionais (todos binrios) so > maior, < menor, >= maior ou igual, e <= menor ou igual, com os signicados bvios. Para comparar a igualdade ou diferena de dois operandos usam-se os operadores de igualdade == igual a, e != diferente de. Quer os operadores relacionais, quer os de igualdade, tm como resultado no um valor aritmtico mas sim um valor lgico, do tipo bool, sendo usadas comummente para controlar instrues de seleco e iterativas, que sero estudadas em pormenor mais tarde. Por exemplo:
if(m < n) k = m; else k = n;

ou
while(n % k != 0 or m % k != 0) --k;

Os tipos dos operandos destes operadores so compatibilizados usando as converses aritmticas usuais apresentadas atrs. muito importante no confundir o operador de igualdade com o operador de atribuio. Em C++ a atribuio representada por = e a vericao de igualdade por ==.

50

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

2.7.3 Operadores lgicos


Os operadores lgicos (ou booleanos) aplicam-se a operandos lgicos e so 27 and conjuno ou e (binrio), tambm se pode escrever &&; or disjuno ou ou (binrio), tambm se pode escrever ||; e not negao ou no (unrio), tambm se pode escrever !. Por exemplo:
a > not a < a < 5 (a > 5) 5 and b <= 7 5 or b <= 7 // // // // verdadeira se verdadeira se verdadeira se verdadeira se a a a a for maior que 5. no for maior que 5. for menor que 5 e b for menor ou igual a 7. for menor que 5 ou b for menor ou igual a 7.

Estes operadores podem operar sobre operandos booleanos, mas tambm sobre operandos aritmticos. Neste ltimo caso, os operandos aritmticos so convertidos para valores lgicos, correspondendo o valor zero a F e qualquer outro valor diferente de zero a V. sempre m ideia recorrer a estas interpretaes de valores aritmticos como se de valores booleanos se tratasse. Esta caracterstica da linguagem C++, como tantas outras, apresentada aqui no por ser uma boa caracterstica, a explorar, mas exactamente por ser uma m caracterstica, que ocasionalmente d azo a erros de difcil deteco. 28
27 Fez-se todo o esforo neste texto para usar a verso mais intuitiva destes operadores, uma introduo relativamente recente na linguagem C++. No entanto, o hbito de anos prega rasteiras, pelo que o leitor poder encontrar ocasionalmente um && ou outro... Nem todos os compiladores aceitam as verses mais recentes dos operadores lgicos. Quando isso no acontecer, e para o programador no ser forado a usar os velhos e desagradveis smbolos, recomenda-se que coloque no incio dos seus programas as directivas de pr-processamento seguintes (Seco 9.2.1):

#define #define #define #define #define #define #define

and && or || not ! bitand & bitor | xor ^ compl ~

28 Ou seja, no se deve nunca usar o valor de uma expresso aritmtica como se de um valor booleano se tratasse. infelizmente comum encontrar-se cdigo como

int i = ...; ... if(i) ... onde se poderia usar o cdigo int i = ...;

2.7. EXPRESSES E OPERADORES

51

A ordem de clculo dos operandos de um operador no , em geral, especicada. Os dois operadores binrios and e or so uma das excepes. Os seus operandos (que podem ser sub-expresses) so calculados da esquerda para a direita, sendo o clculo atalhado logo que o resultado seja conhecido. Se o primeiro operando de um and F, o resultado F, se o primeiro operando de um or V, o resultado V, e em ambos os casos o segundo operando no chega a ser calculado. Esta caracterstica ser de grande utilidade no controlo de uxo dos programas (Captulo 4). Para os mais esquecidos apresentam-se as tabelas de verdade das operaes lgicas na Figura 2.6. F F F V V F V V = = = = F F F V F F F V V F V V = = = = F V F V V V F F F V V F V V = = = = F V V F

F V

= V = F

falso verdadeiro conjuno disjuno ou exclusivo negao

Figura 2.6: Tabelas de verdade das operaes lgicas elementares.

2.7.4 Operadores bit-a-bit


H alguns operadores do C++ que permitem fazer manipulaes de valores de muito baixo nvel, ao nvel do bit. So chamadas operaes bit-a-bit e apenas admitem operandos de tipos bsicos inteiros. Muito embora estejam denidos para tipos inteiros com sinal, alguns tm resultados no especicados quando os operandos so negativos. Assim, assume-se aqui que os operandos so de tipos inteiros sem sinal, ou pelo menos que so garantidamente positivos. Estes operadores pressupem naturalmente que se conhecem as representaes dos tipos bsicos na memria. Isso normalmente contraditrio com a programao de alto nvel. Por isso, estes operadores devem ser usados apenas onde for estritamente necessrio. Os operadores so bitand conjuno bit-a-bit (binrio), tambm se pode escrever &;
... if(i != 0) ... que muito mais claro e directo.

52

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

bitor disjuno bit-a-bit (binrio), tambm se pode escrever |; xor disjuno exclusiva bit-a-bit (binrio), tambm se pode escrever ^; compl negao bit-a-bit (unrio), tambm se pode escrever ~; < < deslocamento para a esquerda (binrio); e > > deslocamento para a direita (binrio). Estes operadores actuam sobre os bits individualmente. Admitindo, para simplicar, que o tipo unsigned int representado com apenas 16 bits,
123U bitand 0xFU == 11U e 123U bitand 0xFU == 0xBU,

pois sendo 123 = (0000000001111011) 2 e (F )16 = (0000000000001111) 2 , a conjuno calculada para pares de bits correspondentes na representao de 123U e 0xFU resulta em 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 1 0 1 1 1 1 1 0 1 1 1 1

ou seja, (0000000000001011) 2 = 11 = (B)16 . As operaes de deslocamento deslocam o padro de bits correspondente ao valor do primeiro operando de tantas posies quanto o valor do segundo operando, inserindo zeros (0) direita, quando o deslocamento para a esquerda, e esquerda, quando o deslocamento para a direita, e eliminando os bits do extremo oposto. Por exemplo, admitindo de novo representaes de 16 bits:
1U < < 4U == 16U 52U > > 4U == 3U

Pois 1 = (0000000000000001) 2 deslocado para a esquerda de quatro dgitos binrios resulta em (0000000000010000) 2 = 16 e 52 = (0000000000110100) 2 deslocado para a direita de quatro posies resulta em (0000000000000011) 2 = 3. O deslocamento para a esquerda de n bits corresponde multiplicao do inteiro por 2 n e o deslocamento de n bits para a direita corresponde diviso inteira por 2n . Os operadores < < e > > tm signicados muito diferentes quando o seu primeiro operando um canal, como se viu na Seco 2.1.1.

2.7. EXPRESSES E OPERADORES

53

2.7.5 Operadores de atribuio


A operao de atribuio, indicada pelo smbolo =, faz com que a varivel que est esquerda do operador (o primeiro operando) passe a tomar o valor do segundo operando. Uma consequncia importante que, ao contrrio do que acontece quando os operadores que se viu at aqui so calculados, o estado do programa afectado pelo clculo de uma operao de atribuio: h uma varivel que muda de valor. Por isso se diz que a atribuio uma operao com efeitos laterais. Por exemplo29 :
a = 3 + 5; // a toma o valor 8.

A atribuio uma operao, e no uma instruo, ao contrrio do que se passa noutras linguagens (como o Pascal). Isto signica que a operao tem um resultado, que o valor que cou guardado no seu operando esquerdo. Este facto, conjugado com a associatividade direita deste tipo de operadores (ver Seco 2.7.7), permite escrever a = b = c = 1; para, numa nica instruo, atribuir 1 s trs variveis a, b e c. Ao contrrio do que acontece, por exemplo, com os operadores aritmticos e relacionais, quando os operandos de uma atribuio so de tipos diferentes, sempre o segundo operando que convertido para se adaptar ao tipo do primeiro operando. Assim, o resultado de
int i; float f; f = i = 1.9f;

que 1. a varivel i ca com o valor 1, pois a converso de float para int elimina a parte fraccionria (i.e., no arredonda, trunca), e 2. a varivel f ca com o valor 1,0, pois o resultado da converso do valor 1 para float, sendo 1 o resultado da atribuio a i 30 . Por outro lado, do lado esquerdo de uma atribuio, i.e., como primeiro operando, tem de estar uma varivel ou, mais formalmente, um chamado lvalue (de left value), ou seja, algo a que se possa mudar o valor. Deve ser claro que no faz qualquer sentido colocar uma constante ou um valor literal do lado esquerdo de uma atribuio:
Mais uma vez so de notar os signicados distintos dos operadores = (atribuio) e == (comparao, igualdade). frequente o programador confundir estes dois operadores, levando a erros de programao muito difceis de detectar. 30 Recorda-se que o resultado de uma atribuio o valor que ca na varivel a que se atribui o valor.
29

54

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO


double const pi = 3.1415927; pi = 4.5; // Absurdo! No faz sentido alterar o que constante! 4.6 = 1.5; // Pior ainda! Que signicado poderia ter semelhante instruo?

Existem vrios outros operadores de atribuio em C++ que so formas abreviadas de escrever expresses comuns. Assim, i += 4 tem (quase) o mesmo signicado que i = i + 4. Todos eles tm o mesmo formato: op= em que op uma das operaes binrias j vistas (mas nem todas tm o correspondente operador de atribuio, ver Apndice F). Por exemplo:
i i i i i = = = = = i i i i i + * % / n; n; n; n; n; // // // // // ou ou ou ou ou i i i i i += -= *= %= /= n; n; n; n; n;

2.7.6 Operadores de incrementao e decrementao


As expresses da forma i += 1 e i -= 1, por serem to frequentes, merecem tambm uma forma especial de abreviao em C++: os operadores de incrementao e decrementao ++ e --. Estes dois operadores tm, por sua vez, duas verses: a verso prexo e a verso suxo. Quando o objectivo simplesmente incrementar ou decrementar uma varivel, as duas verses podem ser consideradas equivalentes, embora a verso prexo deva em geral ser preferida 31 :
i += 1; // ou ++i; (prefervel) // ou i++;

Porm, se o resultado da operao for usado numa expresso envolvente, as verses prexo e suxo tm resultados muito diferentes: o valor da expresso i++ o valor de i antes de incrementado, ou seja, i incrementado depois de o seu valor ser extrado como resultado da operao, enquanto o valor da expresso ++i o valor de i depois de incrementado, ou seja, i incrementado antes de o seu valor ser extrado como resultado da operao. Assim,
int i = 0; int j = i++; cout < < i < < < < j < < endl;

escreve no ecr os valores 1 e 0, enquanto


int i = 0; int j = ++i; cout < < i < < < < j < < endl;

escreve no ecr os valores 1 e 1. As mesmas observaes aplicam-se s duas verses do operador de decrementao.
31

As razes para esta preferncia caro claras quando na Seco 7.7.1.

2.7. EXPRESSES E OPERADORES

55

2.7.7 Precedncia e associatividade


Qual o resultado da expresso 4 * 3 + 2? 14 ou 20? Qual o resultado da expresso 8 / 4 / 2? 1 ou 4? Para que estas expresses no sejam ambguas, o C++ estabelece um conjunto de regras de precedncia e associatividade para os vrios operadores possveis. A Tabela 2.5 lista os operadores j vistos por ordem decrescente de precedncia (ver uma tabela completa no Apndice F). Quanto associatividade, apenas os operadores unrios (com um nico operando) e os operadores de atribuio se associam direita: todos os outros associam-se esquerda, como habitual. Para alterar a ordem de clculo dos operadores numa expresso podem-se usar parnteses. Os parnteses tanto servem para evitar a precedncia normal dos operadores, e.g., x = (y + z) * w, como para evitar a associatividade normal de operaes com a mesma precedncia, e.g., x * (y / z). A propsito do ltimo exemplo, os resultados de 4 * 5 / 6 e 4 * (5 / 6) so diferentes! O primeiro 3 e o segundo 0! O mesmo se passa com valores de vrgula utuante, devido aos erros de arredondamento. Por exemplo, o seguinte troo de programa
// Para os resultados serem mostrados com 20 dgitos (usar #include <iomanip>): cout < < setprecision(20); cout < < 0.3f * 0.7f / 0.001f < < < < 0.3f * (0.7f / 0.001f) < < endl;

escreve no ecr (em mquinas usando o formato IEEE 754 para os float) 210 209.9999847412109375 onde se pode ver claramente que os arredondamentos afectam de forma diferente duas expresses que, do ponto de vista matemtico, deveriam ter o mesmo valor.

2.7.8 Efeitos laterais e mau comportamento


Chamam-se expresses sem efeitos laterais as expresses cujo clculo no afecta o valor de nenhuma varivel. O C++, ao classicar as vrias formas de fazer atribuies como meros operadores, diculta a distino entre instrues com efeitos laterais e instrues sem efeitos laterais. Para simplicar, classicar-se-o como instrues de atribuio as instrues que consistam numa operao de atribuio, numa instruo de atribuio compacta (op=) ou numa simples incrementao ou decrementao. Se a expresso do lado direito da atribuio no tiver efeitos laterais, a instruo de atribuio no tem efeitos laterais e vice-versa. Pressupe-se, naturalmente, que uma instruo de atribuio tem como efeito principal (no lateral) atribuir o valor da expresso do lado direito entidade (normalmente uma varivel) do lado esquerdo. Uma instruo diz-se mal comportada se o seu resultado no estiver denido. As instrues sem efeitos laterais so sempre bem comportadas. As instrues com efeitos laterais so bem comportadas se puderem ser decompostas numa sequncia de instrues sem efeitos laterais. Assim:

56

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Tabela 2.5: Precedncia e associatividade de alguns dos operadores do C++. Operadores colocados na mesma clula da tabela tm a mesma precedncia. As clulas so apresentadas por ordem decrescente de precedncia. Apenas os operadores unrios (com um nico operando) e os operadores de atribuio se associam direita: todos os outros associam-se esquerda.

Descrio construo de valor incrementao suxa decrementao suxa incrementao prexa decrementao prexa negao bit-a-bit ou complemento para um negao simtrico identidade endereo de contedo de multiplicao diviso resto da diviso inteira adio subtraco deslocamento para a esquerda deslocamento para a direita menor menor ou igual maior maior ou igual igual diferente conjuno bit-a-bit disjuno exclusiva bit-a-bit disjuno bit-a-bit conjuno disjuno atribuio simples multiplicao e atribuio diviso e atribuio resto e atribuio adio e atribuio subtraco e atribuio deslocamento para a esquerda e atribuio deslocamento para a direita e atribuio conjuno bit-a-bit e atribuio disjuno bit-a-bit e atribuio disjuno exclusiva bit-a-bit e atribuio

Sintaxe (itlico: partes variveis da sintaxe) tipo ( lista_expresses ) lvalue ++ lvalue -++ lvalue -- lvalue compl expresso (ou ) not expresso - expresso + expresso & lvalue * expresso expresso * expresso expresso / expresso expresso % expresso expresso + expresso expresso - expresso expresso < < expresso expresso > > expresso expresso < expresso expresso <= expresso expresso > expresso expresso >= expresso expresso == expresso expresso != expresso (ou not_eq) expresso bitand expresso (ou &) expresso xor expresso (ou ^) expresso bitor expresso (ou |) expresso and expresso (ou &&) expresso or expresso (ou ||) lvalue = expresso lvalue *= expresso lvalue /= expresso lvalue %= expresso lvalue += expresso lvalue -= expresso lvalue < <= expresso lvalue > >= expresso lvalue &= expresso (ou and_eq) lvalue |= expresso (ou or_eq) lvalue ^= expresso (ou xor_eq)

2.7. EXPRESSES E OPERADORES

57

x = y = z = 1; Instruo de atribuio com efeitos laterais, pois a expresso y = z = 1 tem efeitos laterais (altera y e z), mas bem comportada, pois pode ser transformada numa sequncia de instrues sem efeitos laterais:
z = 1; y = z; x = y;

x = y + (y = z = 1); Instruo com efeitos laterais e mal comportada. Esta instruo no tem remisso. Est simplesmente errada. Ver mais abaixo discusso de caso semelhante. ++x; Instruo sem efeitos laterais, equivalente a x = x + 1. if(x == 0) ... Sem efeitos laterais, pois a expresso no altera qualquer varivel. if(x++ == 0) ... Com efeitos laterais, pois x muda de valor, mas bem comportada. Se x for uma varivel inteira, pode ser transformada numa sequncia de instrues sem efeitos laterais:
x = x + 1; if(x == 1)

... while(cin > > x) ... Com efeitos laterais mas bem comportada. Pode ser decomposta em:
cin > > x; while(cin.good()) { ... cin > > x; }

2.7.9 Ordem de clculo


A ordem de clculo dos operandos indenida para a maior parte dos operadores. As excepes so as seguintes: and O operando esquerdo calculado primeiro. Se for F, o resultado F e o segundo operando no chega a ser calculado. or O operando esquerdo calculado primeiro. Se for V , o resultado V e o segundo operando no chega a ser calculado. , O primeiro operando sempre calculado primeiro. ? : O primeiro operando sempre calculado primeiro. O seu valor determina qual dos dois restantes operandos ser tambm calculado, cando sempre um deles por calcular.

58

CAPTULO 2. CONCEITOS BSICOS DE PROGRAMAO

Para os restantes operadores a ordem de clculo dos operandos de um operador indenida. Assim, na expresso:
y = sin(x) + cos(x) + sqrt(x)

no se garante que sin(x) (seno de x) seja calculada em primeiro lugar e sqrt(x) (raiz quadrada de x) em ltimo32 . Se uma expresso no envolver operaes com efeitos laterais, esta indenio no afecta o programador. Quando a expresso tem efeitos laterais que afectam variveis usadas noutros locais da mesma expresso, esta pode deixar de ter resultados bem denidos devido indenio quanto ordem de clculo. Nesse caso a expresso mal comportada. Por exemplo, depois das instrues
int i = 0; int j = i + i++;

o valor de j pode ser 0 ou 1, consoante o operando i seja calculado antes ou depois do operando i++. No fundo o problema que impossvel saber a priori se a segunda instruo pode ser decomposta em int j = i; i++; j = j + i; ou em int j = i; j = j + i; i++; Este tipo de comportamento deve-se a que, como se viu, no C++ as atribuies e respectivas abreviaes so operadores que tm um resultado e que portanto podem ser usados dentro de expresses envolventes. Se fossem instrues especiais, como em Pascal, era mais difcil escrever instrues mal comportadas33 . Este tipo de operaes, no entanto, fazem parte do estilo usual de programao em C++, pelo que devem ser bem percebidas as suas consequncias: uma expresso com efeitos laterais no pode ser interpretada como uma simples expresso matemtica, pois h variveis que mudam de valor durante o clculo! Expresses com efeitos laterais so pois de evitar, salvo nas expresses idiomticas da linguagem C++. Como se ver, esta tambm uma boa razo para se fazer uma distino clara entre funes (sem efeitos laterais) e procedimentos (com efeitos laterais mas cujas invocaes no podem constar numa expresso) quando se introduzir a modularizao no prximo captulo.
Repare-se que a ordem de clculo dos operandos que indenida. A ordem de clculo das operaes neste caso bem denida. Como a adio tem associatividade esquerda, a expresso equivalente a y = (sin(x) + cos(x)) + sqrt(x), ou seja, a primeira adio forosamente calculada antes da segunda. 33 Mas no impossvel, pois uma funo com argumentos passados por referncia (& em C++ e var em Pascal) pode alterar argumentos envolvidos na mesma expresso que invoca a funo.
32

Captulo 3

Modularizao: rotinas
Methods are more important than facts. Donald E. Knuth, Selected Papers in Computer Science, 176 (1996)

A modularizao um conceito extremamente importante em programao. Neste captulo abordar-se- o nvel atmico de modularizao: as funes e os procedimentos. Este tipo de modularizao fundamental em programao procedimental. Quando se comear a abordar a programao baseada em objectos, no Captulo 7, falar-se- de um outro nvel de modularizao: as classes. Finalmente a modularizao regressar a um nvel ainda mais alto no Captulo 9.

3.1 Introduo modularizao


Exemplos de modularizao, i.e., exemplos de sistemas constitudos por mdulos, so bem conhecidos. A maior parte dos bons sistemas de alta delidade so compostos por mdulos: o amplicador, o equalizador, o leitor de DVD, o sintonizador, as colunas, etc. Para o produtor de um sistema deste tipo a modularizao tem vrias vantagens: 1. reduz a complexidade do sistema, pois cada mdulo mais simples que o sistema na globalidade e pode ser desenvolvido por um equipa especializada; 2. permite alterar um mdulo independentemente dos outros, por exemplo porque se desenvolveu um novo circuito para o amplicador, melhorando assim o comportamento do sistema na totalidade; 3. facilita a assistncia tcnica, pois fcil vericar qual o mdulo responsvel pela avaria e consert-lo isoladamente; e 4. permite fabricar os mdulos em quantidades diferentes de modo produo se adequar melhor procura (e.g., hoje em dia os leitores de DVD vendem-se mais que os CD, que esto a car obsoletos). 59

60

CAPTULO 3. MODULARIZAO: ROTINAS

Tambm para o consumidor nal do sistema a modularizao traz vantagens: 1. permite a substituio de um nico mdulo do sistema, quer por avaria quer por se pretender uma maior delidade usando, por exemplo, um melhor amplicador; 2. em caso de avaria apenas o mdulo avariado ca indisponvel, podendo-se continuar a usar todos os outros (excepto, claro, se o mdulo tiver um papel fundamental no sistema); 3. permite a evoluo do sistema por acrescento de novos mdulos com novas funes (e.g., possvel acrescentar um leitor de DVD a um sistema antigo ligando-o ao amplicador); 4. evita a redundncia, pois os mdulos so reutilizados com facilidade (e.g., o amplicador amplica os sinais do sintonizador, leitor de DVD, etc.). Estas vantagens no so exclusivas dos sistemas de alta delidade: so gerais. Qualquer sistema pode beneciar de pelo menos algumas destas vantagens se for modularizado. A arte da modularizao est em identicar claramente que mdulos devem existir no sistema. Uma boa modularizao atribui uma nica funo bem denida a cada mdulo, minimiza as ligaes entre os mdulos e maximiza a coeso interna de cada mdulo. No caso de um bom sistema de alta delidade, tal corresponde a minimizar a complexidade dos cabos entre os mdulos e a garantir que os mdulos contm apenas os circuitos que contribuem para a funo do mdulo. A coeso tem portanto a ver com as ligaes internas a um mdulo, que idealmente devem ser maximizadas. Normalmente, um mdulo s pode ser coeso se tiver uma nica funo, bem denida. H algumas restries adicionais a impor a uma boa modularizao. No basta que um mdulo tenha uma funo bem denida: tem de ter tambm uma interface bem denida. Por interface entende-se aquela parte de um mdulo que est acessvel do exterior e que permite a sua utilizao. claro, por exemplo, que um dono de uma alta delidade no pode substituir o seu amplicador por um novo modelo se este tiver ligaes e cabos que no sejam compatveis com o modelo mais antigo. A interface de um mdulo a parte que est acessvel ao consumidor. Tudo o resto faz parte da sua implementao, ou mecanismo, e tpico que esteja encerrado numa caixa fora da vista, ou pelo menos fora do alcance do consumidor. Num sistema bem desenhado, cada mdulo mostra a sua interface e esconde a complexidade da sua implementao: cada mdulo est encapsulado numa caixa, a que se costuma chamar uma caixa preta. Por exemplo, num relgio v-se o mostrador, os ponteiros e o manpulo para acertar as horas, mas o mecanismo est escondido numa caixa. Num automvel toda a mecnica est escondida sob o capot. Para o consumidor, o interior (a implementao) de um mdulo irrelevante: o audilo s se importa com a constituio interna de um mdulo na medida em que ela determina o seu comportamento externo. O consumidor de um mdulo s precisa de conhecer a sua funo e a sua interface. A sua viso de um mdulo permite-lhe, abstraindo-se do seu funcionamento interno, preocupar-se apenas com aquilo que lhe interessa: ouvir som de alta delidade. A modularizao, o encapsulamento e a abstraco so conceitos fundamentais em engenharia da programao para o desenvolvimento de programas de grande escala. Mesmo para pequenos programas estes conceitos so teis, quando mais no seja pelo treino que proporciona a

3.1. INTRODUO MODULARIZAO

61

sua utilizao e que permite ao programador mais tarde lidar melhor com projectos de maior escala. Estes conceitos sero estudados com mais profundidade em disciplinas posteriores, como Concepo e Desenvolvimento de Sistemas de Informao e Engenharia da Programao. Neste captulo far-se- uma primeira abordagem aos conceitos de modularizao e de abstraco em programao. Os mesmos conceitos sero revisitados ao longo dos captulos subsequentes. As vantagens da modularizao para a programao so pelo menos as seguintes [2]: 1. facilita a deteco de erros, pois em princpio simples identicar o mdulo responsvel pelo erro, reduzindo-se assim o tempo gasto na identicao de erros; 2. permite testar os mdulos individualmente, em vez de se testar apenas o programa completo, o que reduz a complexidade do teste e permite comear a testar antes de se ter completado o programa; 3. permite fazer a manuteno do programa (correco de erros, melhoramentos, etc.) mdulo a mdulo e no no programa globalmente, o que reduz a probabilidade de essa manuteno ter consequncias imprevistas noutras partes do programa; 4. permite o desenvolvimento independente dos mdulos, o que simplica o trabalho em equipa, pois cada elemento ou cada sub-equipa tem a seu cargo apenas alguns mdulos do programa; e 5. permite a reutilizao do cdigo 1 desenvolvido, que porventura a mais evidente vantagem da modularizao em programas de pequena escala. Um programador assume, ao longo do desenvolvimento de um programa, dois papeis distintos: por um lado produtor, pois sua responsabilidade desenvolver mdulos; por outro consumidor, pois far com certeza uso de outros mdulos, desenvolvidos por outrem ou por ele prprio no passado. Esta uma noo muito importante. de toda a convenincia que um programador possa ser um mero consumidor dos mdulos j desenvolvidos, sem se preocupar com o seu funcionamento interno: basta-lhe, como consumidor, sabe qual a funo mdulo e qual a sua interface. utilizao de um sistema em que se olha para ele apenas do ponto de vista do seu funcionamento externo chama-se abstraco. A capacidade de abstraco das qualidades mais importantes que um programador pode ter (ou desenvolver), pois permite-lhe reduzir substancialmente a complexidade da informao que tem de ter presente na memria, conduzindo por isso a substanciais ganhos de produtividade e a uma menor taxa de erros. A capacidade de abstraco to fundamental na programao como no dia-a-dia. Ningum conduz o automvel com a preocupao de saber se a vela do primeiro cilindro produzir a fasca no momento certo para a prxima exploso! Um automvel para o condutor normal um objecto que lhe permite deslocar-se e que possui um interface simples: a ignio para ligar o automvel, o volante para ajustar a direco, o acelerador para ganhar velocidade, etc. O encapsulamento dos mdulos, ao esconder do consumidor o seu mecanismo, facilita-lhe esta viso externa dos mdulos e portanto facilita a sua capacidade de abstraco.
1

D-se o nome de cdigo a qualquer pedao de programa numa dada linguagem de programao.

62

CAPTULO 3. MODULARIZAO: ROTINAS

3.2 Funes e procedimentos: rotinas


A modularizao , na realidade, um processo hierrquico: muito provavelmente cada mdulo de um sistema de alta delidade composto por sub-mdulos razoavelmente independentes, embora invisveis para o consumidor. O mesmo se passa na programao. Para j, no entanto, abordar-se-o apenas as unidades atmicas de modularizao em programao: funes e procedimentos2 . Funo Conjunto de instrues, com interface bem denida, que efectua um dado clculo. Procedimento Conjunto de instrues, com interface bem denida, que faz qualquer coisa. Por uma questo de simplicidade daqui em diante chamar-se- rotina quer a funes quer a procedimentos. Ou seja, a unidade atmica de modularizao so as rotinas, que se podem ser ou funes e ou procedimentos. As rotinas permitem isolar pedaos de cdigo com objectivos bem denidos e torn-los reutilizveis onde quer que seja necessrio. O fabrico de uma rotina corresponde em C++ quilo que se designa por denio. Uma vez denida fabricada, uma rotina pode ser utilizada sem que se precise de conhecer o seu funcionamento interno, da mesma forma que o audilo no est muito interessado nos circuitos dentro do amplicador, mas simplesmente nas suas caractersticas vistas do exterior. Rotinas so pois como caixas pretas: uma vez denidas (e correctas), devem ser usadas sem preocupaes quanto ao seu funcionamento interno. Qualquer linguagem de programao, e o C++ em particular, fornece um conjunto de tipos bsicos e de operaes que se podem realizar com variveis, constantes e valores desses tipos. Uma maneira de ver as rotinas como extenses a essas operaes disponveis na linguagem no artilhada. Por exemplo, o C++ no fornece qualquer operao para calcular o mdc (mximo divisor comum), mas no Captulo 1 viu-se uma forma de o calcular. O pedao de programa que calcula o mdc pode ser colocado numa caixa preta, com uma interface apropriada, de modo a que possa ser reutilizado sempre que necessrio. Isso corresponde a denir uma funo chamada mdc que pode mais tarde ser utilizada onde for necessrio calcular o mximo divisor comum de dois inteiros:
cout < < "Introduza dois inteiros: "; int m, n; cin > > m > > n; cout < < "mdc(" < < m < < ", " < < n < < ") = " < < mdc(m, n) < < endl;

Assim, ao se produzirem rotinas, est-se como que a construir uma verso mais potente da linguagem de programao utilizada. interessante que muitas tarefas em programao podem ser interpretadas exactamente desta forma. Em particular, ver-se- mais tarde que possvel aplicar a mesma ideia aos tipos de dados disponveis: o programador pode no apenas artilhar a linguagem com novas operaes sobre tipos bsicos, como tambm com novos tipos!
A linguagem C++ no distingue entre funes e procedimentos: ambos so conhecidos simplesmente por funes nas referncias tcnicas sobre a linguagem.
2

3.2. FUNES E PROCEDIMENTOS: ROTINAS

63

A este ltimo tipo de programao chama-se programao centrada nos dados, e a base da programao baseada em objectos e, consequentemente, da programao orientada para objectos.

3.2.1 Abordagens descendente e ascendente


Nos captulos anteriores introduziram-se vrios conceitos, como os de algoritmos, dados e programas. Explicaram-se tambm algumas das ferramentas das linguagens de programao, tais como variveis, constantes, tipos, valores literais, expresses, operaes, etc. Mas, como usar todos estes conceitos para resolver um problema em particular? Existem muitas possveis abordagens resoluo de problemas em programao, quase todas com um paralelo perfeito com as abordagens que se usam no dia-a-dia. Porventura uma das abordagens mais clssicas em programao a abordagem descendente (ou top-down). Abordar um problema de cima para baixo corresponde a olhar para ele na globalidade e identicar o mais pequeno nmero de sub-problemas independentes possvel. Depois, sendo esses sub-problemas independentes, podem-se resolver independentemente usando a mesma abordagem: cada sub-problema dividido num conjunto de sub-sub-problemas mais simples. Esta abordagem tem a vantagem de limitar a quantidade de informao a processar pelo programador em cada passo e de, por diviso sucessiva, ir reduzindo a complexidade dos problema at trivialidade. Quando os problemas identicados se tornam triviais pode-se escrever a sua soluo na forma do passo de um algoritmo ou instruo de um programa. Cada problema ou sub-problema identicado corresponde normalmente a uma rotina no programa nal. Esta abordagem no est isenta de problemas. Um deles o de no facilitar o reaproveitamento de cdigo. que dois sub-problemas podem ser iguais sem que o programador d por isso, o que resulta em duas rotinas iguais ou, pelo menos, muito parecidas. Assim, muitas vezes conveniente alternar a abordagem descendente com a abordagem ascendente. Na abordagem ascendente, comea por se tentar perceber que ferramentas fazem falta para resolver o problema mas no esto ainda disponveis. Depois desenvolvem-se essas ferramentas, que correspondem tipicamente a rotinas, e repete-se o processo, indo sempre acrescentando camadas de ferramentas sucessivamente mais sosticadas linguagem. A desvantagem deste mtodo que dicilmente se pode saber que ferramentas fazem falta sem um mnimo de abordagem descendente. Da a vantagem de alternar as abordagens. Suponha-se que se pretende escrever um programa que some duas fraces positivas introduzidas do teclado e mostre o resultado na forma de uma fraco reduzida (ou em termos mnimos). Recorda-se que uma fraco n est em termos mnimos se no existir qualquer did visor comum ao numerador e ao denominador com excepo de 1, ou seja, se mdc(m, n) = 1. Para simplicar admite-se que as fraces introduzidas so representadas, cada uma, por um par de valores inteiros positivos: numerador e denominador. Pode-se comear por escrever o esqueleto do programa:
#include <iostream>

64
using namespace std;

CAPTULO 3. MODULARIZAO: ROTINAS

/** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { ... }

Olhando o problema na globalidade, verica-se que pode ser dividido em trs sub-problemas: ler as fraces de entrada, obter a fraco soma em termos mnimos e escrever o resultado. Traduzindo para C++:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: ... // Clculo da fraco soma em termos mnimos: ... // Escrita do resultado: ... }

Pode-se agora abordar cada sub-problema independentemente. Comeando pela leitura das fraces, identicam-se dois sub-sub-problemas: pedir ao utilizador para introduzir as fraces e ler as fraces. Estes problemas so to simples de resolver que se passa directamente ao cdigo:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado:

3.2. FUNES E PROCEDIMENTOS: ROTINAS


cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; // Clculo da fraco soma em termos mnimos: ... // Escrita do resultado: ... }

65

Usou-se um nica instruo para denir quatro variveis do tipo int que guardaro os numeradores e denominadores das duas fraces lidas: o C++ permite denir vrias variveis na mesma instruo. De seguida pode-se passar ao sub-problema nal da escrita do resultado. Suponha-se que, sendo as fraces de entrada 6 e 7 , se pretendia que surgisse no ecr: 9 3
A soma de 6/9 com 7/3 3/1.

Ento o problema pode ser resolvido como se segue:


#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; // Clculo da fraco soma em termos mnimos: ... // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(?, ?); cout < < . < < endl; }

66

CAPTULO 3. MODULARIZAO: ROTINAS

Neste caso adiou-se um problema: admitiu-se que est disponvel algures um procedimento chamado escreveFraco() que escreve uma fraco no ecr. evidente que mais tarde ser preciso denir esse procedimento, que, usando um pouco de abordagem ascendente, se percebeu vir a ser utilizado em trs locais diferentes. Podia-se ter levado a abordagem ascendente mais longe: se se vai lidar com fraces, no seria til um procedimento para ler uma fraco do teclado? E uma outra para reduzir uma fraco a termos mnimos? O resultado obtido como uma tal abordagem, dada a pequenez do problema, seria semelhante ao que se obter prosseguindo a abordagem descendente. Sobrou outro problema: como escrever a fraco resultado sem saber onde se encontram o seu numerador e o seu denominador? Claramente necessrio, para a resoluo do sub-problema do clculo da soma, denir duas variveis adicionais onde esses valores sero guardados:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; // Clculo da fraco soma em termos mnimos: int n; int d; ... // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

necessrio agora resolver o sub-problema do clculo da fraco soma em termos mnimos. Dadas duas fraces, a sua soma simples se desde que tenham o mesmo denominador. A forma mais simples de reduzir duas fraces diferentes ao mesmo denominador consiste em multiplicar ambos os termos da primeira fraco pelo denominador da segunda e vice versa. Ou seja, ad bc ad + bc a c + = + = , b d bd bd bd

3.2. FUNES E PROCEDIMENTOS: ROTINAS


pelo que o programa ca:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; ... // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

67

Usando o mesmo exemplo que anteriormente, se as fraces de entrada forem grama tal como est escreve
A soma de 6/9 com 7/3 81/27.

6 9

7 e 3 , o pro-

Ou seja, a fraco resultado no est reduzida. Para a reduzir necessrio dividir o numerador e o denominador da fraco pelo seu mdc:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main()

68
{

CAPTULO 3. MODULARIZAO: ROTINAS

// Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; int k = mdc(n, d); n /= k; // o mesmo que n = n / k; d /= k; // o mesmo que d = d / k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

Neste caso adiou-se mais um problema: admitiu-se que est disponvel algures uma funo chamada mdc() que calcula o mdc de dois inteiros. Isto signica que mais tarde ser preciso denir esse procedimento. Recorda-se, no entanto, que o algoritmo para o clculo do mdc foi visto no Captulo 1 e revisto no Captulo 2. A soluo encontrada ainda precisa de ser renada. Suponha-se que o programa compilado e executado num ambiente onde valores do tipo int so representados com apenas 6 bits. Nesse caso, de acordo com a discusso do captulo anterior, essas variveis podem conter 7 valores entre -32 e 31. Que acontece quando, sendo as fraces de entrada 6 e 3 , se inicializa 9 a varivel n? O valor da expresso n1 * d2 + n2 * d1 81, que excede em muito a gama dos int com 6 bits! O resultado desastroso. No possvel evitar totalmente este problema, mas possvel minimiz-lo se se reduzir a termos mnimos as fraces de entrada logo aps a 6 2 sua leitura. Se isso acontecesse, como 9 em termos mnimos 3 , a expresso n1 * d2 + n2 * d1 teria o valor 27, dentro da gama de valores hipottica dos int. Nos ambientes tpicos os valores do tipo int so representados por 32 bits, pelo que o problema acima s se pe para numeradores e denominadores muito maiores. Mas no pelo facto de ser um problema mais raro que deixa de ser problema, pelo que convm alterar o programa para:
#include <iostream> using namespace std;

3.2. FUNES E PROCEDIMENTOS: ROTINAS


/** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; int k = mdc(n1, d1); n1 /= k; d1 /= k; int k = mdc(n2, d2); n2 /= k; d2 /= k; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; int k = mdc(n, d); n /= k; d /= k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

69

O programa acima precisa ainda de ser corrigido. Como se ver mais frente, no se podem denir mltiplas variveis com o mesmo nome no mesmo contexto. Assim, a varivel k deve ser denida uma nica vez e reutilizada quando necessrio:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: ";

70

CAPTULO 3. MODULARIZAO: ROTINAS


int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; int k = mdc(n1, d1); n1 /= k; d1 /= k; k = mdc(n2, d2); n2 /= k; d2 /= k; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; k = mdc(n, d); n /= k; d /= k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

3.2.2 Denio de rotinas


Antes de se poder utilizar uma funo ou um procedimento, necessrio deni-lo (antes de usar a aparelhagem h que fabric-la). Falta, portanto, denir a funo mdc() e o procedimento escreveFraco(). Um possvel algoritmo para o clculo do mdc de dois inteiros positivos foi visto no primeiro captulo. A parte relevante do correspondente programa :
int m; int n; // Como inicializar m e n? int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k;

3.2. FUNES E PROCEDIMENTOS: ROTINAS

71

Este troo de programa calcula o mdc dos valores de m e n e coloca o resultado na varivel k. necessrio colocar este cdigo numa funo, ou seja, num mdulo com uma interface e uma implementao escondida numa caixa. Comea-se por colocar o cdigo numa caixa, i.e., entre {}:
{ int m; int n; // Como inicializar m e n? int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; }

Tudo o que ca dentro da caixa est inacessvel do exterior. necessrio agora atribuir um nome ao mdulo, tal como se atribui o nome Amplicador ao mdulo de uma aparelhagem que amplica os sinais udio vindos de outros mdulos. Neste caso o mdulo chama-se mdc:
mdc { int m; int n; // Como inicializar m e n? int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; }

Qual a interface deste mdulo, ou melhor, desta funo? Uma caixa sem interface intil. A funo deve calcular o mdc de dois nmeros. Mas de onde vm eles? O resultado da funo ca guardado na varivel k, que est dentro da caixa. Como comunicar esse valor para o exterior? No programa da soma de fraces a funo mdc() utilizada, ou melhor, invocada (ou ainda chamada), em trs locais diferentes. Em cada um deles escreveu-se o nome da funo seguida de uma lista de duas expresses. A estas expresses chama-se argumentos passados funo.

72

CAPTULO 3. MODULARIZAO: ROTINAS

Quando calculadas, essas expresses tm os valores que se pretende que sejam usados para inicializar as variveis m e n denidas na funo mdc(). Para o conseguir, as variveis m e n no devem ser variveis normais denidas dentro da caixa: devem ser parmetros da funo. Os parmetros da funo so denidos entre parnteses logo aps o nome da funo, e no dentro da caixa ou corpo da funo, e servem como entradas da funo, fazendo parte da sua interface:
mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; }

A funo mdc(), que um mdulo, j tem entradas, que correspondem aos dois parmetros denidos. Quando a funo invocada os valores dos argumentos so usados para inicializar os parmetros respectivos. Claro que isso implica que o nmero de argumentos numa invocao de uma funo tem de ser rigorosamente igual ao nmero de parmetros da funo, salvo em alguns casos que se vero mais tarde. E os tipos dos argumentos tambm tm de ser compatveis com os tipos dos parmetros. muito importante distinguir entre a denio de uma rotina (neste caso um funo) e a sua invocao ou chamada. A denio de uma rotina nica, e indica a sua interface (i.e., como a rotina se utiliza e que nome tem) e a sua implementao (i.e., como funciona). Uma invocao de uma rotina feita onde quer que seja necessrio recorrer aos seus servios para calcular algo (no caso de uma funo) ou para fazer alguma coisa (no caso de um procedimento). Finalmente falte denir como se fazem as sadas da funo. Uma funo em C++ s pode ter uma sada. Neste caso a sada o valor guardado em k no nal da funo, que o maior divisor comum dos dois parmetros m e n. Para que o valor de k seja usado como sada da funo, usa-se uma instruo de retorno:
mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k;

3.2. FUNES E PROCEDIMENTOS: ROTINAS

73

return k; }

A denio da funo tem de indicar claramente que a funo tem uma sada de um dado tipo. Neste caso a sada um valor do tipo int, pelo que a denio da funo ca:
int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; }

3.2.3 Sintaxe das denies de funes


A denio de uma rotina constituda por um cabealho seguido de um corpo, que consiste no conjunto de instrues entre {}. No cabealho so indicados o tipo do valor calculado ou devolvido por essa rotina, o nome da rotina e a sua lista de parmetros (cada parmetro representado por um par tipo nome, sendo os pares separados por vrgulas). Isto :
tipo_de_devoluo nome(lista_de_parmetros)

O cabealho de uma rotina corresponde sua interface, tal como as tomadas para cabos nas traseiras de um amplicador e os botes de controlo no seu painel frontal constituem a sua interface. Quando a rotina uma funo porque calcula um valor de um determinado tipo. Esse tipo indicado em primeiro lugar no cabealho. No caso de um procedimento, que no calcula nada, necessrio colocar a palavra-chave void no lugar desse tipo, como se ver mais frente. Logo a seguir indica-se o nome da rotina, e nalmente uma lista de parmetros, que consiste simplesmente numa lista de denies de variveis com uma sintaxe semelhante (embora no idntica) que se viu no Captulo 2. No exemplo anterior deniu-se uma funo que tem dois parmetros (ambos do tipo int) e que devolve um valor inteiro. O seu cabealho :
int mdc(int m, int n)

A sintaxe de especicao dos parmetros diferente da sintaxe de denio de variveis normais, pois no se podem denir vrios parmetros do mesmo tipo separando os seus nomes por vrgulas: o cabealho

74
int mdc(int m, n)

CAPTULO 3. MODULARIZAO: ROTINAS

invlido, pois falta-lhe a especicao do tipo do parmetro n. O corpo desta funo, i.e., a sua implementao, corresponde s instrues entre {}:
{ int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; }

Idealmente o corpo das rotinas deve ser pequeno, contendo entre uma e dez instrues. Muito raramente haver boas razes para ultrapassar as 60 linhas. A razo para isso prende-se com a diculdade dos humanos (sim, os programadores so humanos) em abarcar demasiados assuntos de uma s vez: quanto mais curto for o corpo de uma rotina, mais fcil foi de desenvolver e mais fcil de corrigir ou melhorar. Por outro lado, quanto maior for uma rotina, mais difcil reutilizar o seu cdigo.

3.2.4 Contrato e documentao de uma rotina


A denio de uma rotina (ou melhor, a sua declarao, como se ver na Seco 3.2.17) s ca realmente completa quando incluir um comentrio indicando exactamente aquilo que calcula (se for uma funo) ou aquilo que faz (se for um procedimento). I.e., a denio de uma rotina s ca completa se contiver a sua especicao:
/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n;

3.2. FUNES E PROCEDIMENTOS: ROTINAS


while(m % k != 0 or n % k != 0) --k; return k; }

75

Todo o texto colocado entre /* e */ ignorado pelo compilador: um comentrio de bloco (os comentrios comeados por // so comentrios de linha). Este comentrio contm: uma descrio do que a funo calcula ou do que o procedimento faz, em portugus vernculo; a pr-condio ou P C da rotina, ou seja, a condio que as entradas (i.e., os valores iniciais dos parmetros) tm de vericar de modo a assegurar o bom funcionamento da rotina; e a condio objectivo ou CO, mais importante ainda que a P C, que indica a condio que deve ser vlida quando a rotina termina numa instruo de retorno. No caso de uma funo, o seu nome pode ser usado na condio objectivo para indicar o valor devolvido no seu nal. Na denio acima, colocou-se o comentrio junto ao cabealho da rotina por ser fundamental para se perceber o que a rotina faz (ou calcula). O cabealho de uma rotina, por si s, no diz o que ela faz, simplesmente como se utiliza. Por outro lado, o corpo de uma rotina diz como funciona. Uma rotina uma caixa preta: no seu interior ca o mecanismo (o corpo da rotina), no exterior a interface (o cabealho da rotina) e pode-se ainda saber para que serve e como se utiliza lendo o seu manual de utilizao (os comentrios contendo a descrio em portugus e a pr-condio e a condio objectivo). Estes comentrios so parte da documentao do programa. Os comentrios de bloco comeados por /** e os de linha comeados por /// so considerados comentrios de documentao por alguns sistemas automticos que extraem a documentao de um programa a partir deste tipo especial de comentrio3 . Da mesma forma, as construes @pre e @post servem para identicar ao sistema automtico de documentao a pr-condio e a condio objectivo, que tambm conhecida por ps-condio. As condies P C e CO funcionam como um contrato que o programador produtor da rotina estabelece com o seu programador consumidor: Se o programador consumidor desta rotina garantir que as variveis do programa respeitam a pr-condio P C imediatamente antes de a invocar, o programador produtor desta rotina garante que a condio objectivo CO ser verdadeira imediatamente depois de esta terminar.
Em particular existe um sistema de documentao disponvel em Linux chamado doxygen que de grande utilidade.
3

76

CAPTULO 3. MODULARIZAO: ROTINAS

Esta viso legalista da programao est por trs de uma metodologia relativamente recente de desenvolvimento de programas a que se chama desenho por contracto. Em algumas linguagens de programao, como o Eiffel, as pr-condies e as condies objectivo fazem parte da prpria linguagem, o que permite fazer a sua vericao automtica. Na linguagem C++ um efeito semelhante, embora mais limitado, pode ser obtido usando as chamadas instrues de assero, discutidas mais abaixo.

3.2.5 Integrao da funo no programa


Para que a funo mdc() possa ser utilizada no programa desenvolvido, necessrio que a sua denio se encontre antes da primeira utilizao:
#include <iostream> using namespace std; /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; } /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; int k = mdc(n1, d1); n1 /= k; d1 /= k;

3.2. FUNES E PROCEDIMENTOS: ROTINAS


k = mdc(n2, d2); n2 /= k; d2 /= k; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; k = mdc(n, d); n /= k; d /= k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

77

3.2.6 Sintaxe e semntica da invocao ou chamada


Depois de denidas, as rotinas podem ser utilizadas noutros locais de um programa. A utilizao tpica corresponde a invocar ou chamar a rotina para que seja executada com um determinado conjunto de entradas. A invocao da funo mdc() denida acima pode ser feita como se segue:
int x = 5; // int divisor; // divisor = mdc(x + 3, 6); // cout < < divisor < < endl; // 1 2 3 4

A sintaxe da invocao de rotinas consiste simplesmente em colocar o seu nome seguido de uma lista de expresses (separadas por vrgulas) em nmero igual aos dos seus parmetros. A estas expresses chama-se argumentos. Os valores recebidos pelos parmetros de uma rotina e o valor devolvido por uma funo podem ser de qualquer tipo bsico do C++ ou de tipos de dados denidos pelo programador (comear-se- a falar destes tipos no Captulo 5). O tipo de um argumento tem de ser compatvel com o tipo do parmetro respectivo 4 . Como evidente, uma funo pode devolver um nico valor do tipo indicado no seu cabealho. Uma invocao de uma funo pode ser usada em expresses mais complexas, tal como qualquer operador disponvel na linguagem, uma vez que as funes devolvem um valor calcu4

No esquecer que todas as expresses em C++ so de um determinado tipo.

78

CAPTULO 3. MODULARIZAO: ROTINAS

lado. No exemplo acima, a chamada funo mdc() usada como segundo operando de uma operao de atribuio (instruo 3). Que acontece quando o cdigo apresentado executado? Instruo 1: construda uma varivel x inteira com valor inicial 5. Instruo 2: construda uma varivel divisor, tambm inteira, mas sem que seja inicializada, pelo que contm lixo (j se viu que no boa ideia no inicializar, isto apenas um exemplo!). Instruo 3: Esta instruo implica vrias ocorrncias sequenciais, pelo que se separa em duas partes:
divisor = mdc(x + 3, 6) // 3A ; // 3B

Instruo 3A: invocada a funo mdc(): 1. So construdos os parmetros m e n, que funcionam como quaisquer outras variveis, excepto quanto inicializao. 2. Cada um dos parmetros m e n inicializado com o valor do argumento respectivo na lista de argumentos colocados entre parnteses na chamada da funo. Neste caso o parmetro m inicializado com o valor 8 e o parmetro n inicializado com o valor 6. 3. A execuo do programa passa para a primeira instruo do corpo da funo. 4. O corpo da funo executado. A primeira instruo executada constri uma nova varivel k com lixo. A funo termina quando se atinge a chaveta nal do seu corpo ou quando ocorre uma instruo de retorno, que consiste na palavra-chave return seguida de uma expresso (apenas no caso das funes, como se ver). O valor dessa expresso o valor calculado e devolvido pela funo. Neste caso o seu valor 2 (valor de k depois da procura do mdc). 5. Ao ser atingida a instruo de retorno a funo termina. 6. So destrudas as variveis k, m e n. 7. A execuo do programa passa para a instruo seguinte de invocao da funo (neste caso 3B). Instruo 3B: atribudo varivel divisor o valor calculado pela funo (neste caso 2). Diz-se que a funo devolveu o valor calculado. Instruo 4: O valor de divisor escrito no ecr.

3.2.7 Parmetros
Parmetros so as variveis listadas entre parnteses no cabealho da denio de uma rotina. So variveis locais (ver Seco 3.2.12), embora com uma particularidade: so automaticamente inicializadas com o valor dos argumentos respectivos em cada invocao da rotina.

3.2. FUNES E PROCEDIMENTOS: ROTINAS

79

3.2.8 Argumentos
Argumentos so as expresses listadas entre parnteses numa invocao ou chamada de uma rotina. O seu valor utilizado para inicializar os parmetros da rotina invocada.

3.2.9 Retorno e devoluo


Em ingls a palavra return tem dois signicados distintos: retornar (ou regressar) e devolver. O portugus neste caso mais rico, pelo que se usaro palavras distintas: dir-se- que uma rotina retorna quando termina a sua execuo e o uxo de execuo regressa ao ponto de invocao, e dir-se- que uma funo, ao retornar, devolve um valor que pode ser usado na expresso em que a funo foi invocada. No exemplo do mdc acima o valor inteiro devolvido usado numa expresso envolvendo o operador de atribuio. Uma funo termina quando o uxo de execuo atinge uma instruo de retorno. As instrues de retorno consistem na palavra-chave return seguida de uma expresso e de um ;. A expresso tem de ser de um tipo compatvel com o tipo de devoluo da funo. O resultado da expresso, depois de convertido no tipo de devoluo, o valor devolvido ou calculado pela funo. No caso da funo mdc() o retorno e a devoluo fazem-se com a instruo
return k;

O valor devolvido neste caso o valor contido na varivel k, que o mdc dos valores iniciais de m e n.

3.2.10 Signicado de void


Um procedimento tem a mesma sintaxe de uma funo, mas no devolve qualquer valor. Esse facto indicado usando a palavra-chave void como tipo do valor de devoluo. Um procedimento termina quando se atinge a chaveta nal do seu corpo ou quando se atinge uma instruo de retorno simples, sem qualquer expresso, i.e., return;. Os procedimentos tm tipicamente efeitos laterais, e.g., afectam valores de variveis que lhes so exteriores (e.g., usando passagem de argumentos por referncia, descrita na prxima seco). Assim sendo, para evitar maus comportamentos, no se devem usar procedimentos em expresses (ver Seco 2.7.8). A utilizao do tipo de devoluo void impede a chamada de procedimentos em expresses, pelo que o seu uso recomendado 5 . Resumindo: de toda a convenincia que os procedimentos tenham tipo de devoluo void e que as funes se limitem a devolver um valor calculado e no tenham qualquer efeito lateral. O respeito por esta regra simples pode poupar muitas dores de cabea ao programador. No programa da soma de fraces cou em falta a denio do procedimento escreveFraco(). A sua denio muito simples:
Isto uma simplicao. Na realidade podem haver expresses envolvendo operandos do tipo void. Mas a sua utilidade muito restrita.
5

80

CAPTULO 3. MODULARIZAO: ROTINAS


/** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d) { cout < < n < < / < < d; }

No necessria qualquer instruo de retorno, pois o procedimento retorna quando a execuo atinge a chaveta nal. O programa completo ento:
#include <iostream> using namespace std; /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; } /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d) { cout < < n < < / < < d; }

3.2. FUNES E PROCEDIMENTOS: ROTINAS

81

/** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; int k = mdc(n1, d1); n1 /= k; d1 /= k; k = mdc(n2, d2); n2 /= k; d2 /= k; // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; k = mdc(n, d); n /= k; d /= k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

3.2.11 Passagem de argumentos por valor e por referncia


Observe o seguinte exemplo de procedimento. O seu programador pretendia que o procedimento trocasse os valores de duas variveis passadas como argumentos:
// Ateno! Este procedimento no funciona! void troca(int x, int y) { int const auxiliar = x; x = y; y = auxiliar; /* No h instruo de retorno explcita, pois trata-se de um

82

CAPTULO 3. MODULARIZAO: ROTINAS


procedimento que no devolve qualquer valor. Alternativamente poder-se-ia usar return;. */ }

O que acontece ao se invocar este procedimento como indicado na segunda linha do seguinte cdigo?
int a = 1, b = 2; troca(a, b); /* A invocao no ocorre dentro de qualquer expresso, dado que o procedimento no devolve qualquer valor. */ cout < < a < < < < b < < endl;

1. So construdas as variveis x e y. 2. Sendo parmetros do procedimento, a varivel x inicializada com o valor 1 (valor de a) e a varivel y inicializada com o valor 2 (valor de b). Assim, os parmetros so cpias dos argumentos. 3. Durante a execuo do procedimento os valores guardados em x e y so trocados, ver Figura 3.1. 4. Antes de o procedimento terminar, as variveis x e y tm valores 2 e 1 respectivamente. 5. Quando termina a execuo do procedimento, as variveis x e y so destrudas (ver explicao mais frente) Ou seja, no h qualquer efeito sobre os valores das variveis a e b! Os parmetros mudaram de valor dentro do procedimento mas as variveis a e b no mudaram de valor: a continua a conter 1 e b a conter 2. Este tipo de comportamento ocorre quando numa funo ou procedimento se usa a chamada passagem de argumentos por valor. Normalmente, este um comportamento desejvel. S em alguns casos, como neste exemplo, esta uma caracterstica indesejvel. Para resolver este tipo de problemas, onde de interesse que o valor das variveis que so usadas como argumentos seja alterado dentro de um procedimento, existe o conceito de passagem de argumentos por referncia. A passagem de um argumento por referncia indicada no cabealho do procedimento colocando o smbolo & depois do tipo do parmetro pretendido, como se pode ver abaixo:
void troca(int& x, int& y) { int const auxiliar = x; x = y; y = auxiliar; }

Ao invocar como anteriormente, ver Figura 3.2:

3.2. FUNES E PROCEDIMENTOS: ROTINAS

83

x: int 1 1: int const auxiliar = x; x: int 1 2: x = y; x: int 2 3: y = auxiliar; x: int 2

y: int 2 indica que o valor xo: uma constante

y: int 2

auxiliar: int{frozen} 1

y: int 2

auxiliar: int{frozen} 1

y: int 1 ou x: int 2 1 3

auxiliar: int{frozen} 1

y: int

auxiliar: int

Figura 3.1: Algoritmo usual de troca de valores entre duas variveis x e y atravs de uma constante auxiliar.

84

CAPTULO 3. MODULARIZAO: ROTINAS

int a = 1, b = 2; a: int 1 invocao de troca() x: int& a: int 1 int const auxiliar = x; x: int& a: int 1 x = y; x: int& a: int 2 y = auxiliar; x: int& a: int 2 m de troca() a: int 2 b: int 1 y: int& b: int 1 auxiliar: int{frozen} 1 y: int& b: int 2 auxiliar: int{frozen} 1 y: int& b: int 2 auxiliar: int{frozen} 1 y: int& b: int 2 b: int 2

Figura 3.2: Diagramas com evoluo do estado do programa que invoca o procedimento troca() entre cada instruo.

3.2. FUNES E PROCEDIMENTOS: ROTINAS

85

1. Os parmetros x e y tornam-se sinnimos (referncias) das variveis a e b. Aqui no feita a cpia dos valores de a e b para x e y. O que acontece que os parmetros x e y passam a referir-se s mesmas posies de memria onde esto guardadas as variveis a e b. Ao processo de equiparao de um parmetro ao argumento respectivo passado por referncia chama-se tambm inicializao. 2. No corpo do procedimento o valor que est guardado em x trocado com o valor guardado em y. Dado que x se refere mesma posio de memria que a e y mesma posio de memria que b, uma vez que so sinnimos, ao fazer esta operao est-se efectivamente a trocar os valores das variveis a e b. 3. Quando termina a execuo da funo so destrudos os sinnimos x e y das variveis a e b (que permanecem intactas), cando os valores destas trocados. Como s podem existir sinnimos/referncias de entidades que, tal como as variveis, tm posies de memria associadas, a chamada
troca(20, a + b);

no faz qualquer sentido e conduz a dois erros de compilao. de notar que a utilizao de passagens por referncia deve ser evitada a todo o custo em funes, pois levariam ocorrncia de efeitos laterais nas expresses onde essas funes fossem chamadas. Isto evita situaes como
int incrementa(int& valor) { return valor = valor + 1; } int main() { int i = 0; cout < < i + incrementa(i) < < endl; }

em que o resultado nal tanto pode ser aparecer 1 como aparecer 2 no ecr, dependendo da ordem de clculo dos operandos da adio. Ou seja, funes com parmetros que so referncias so meio caminho andado para instrues mal comportadas, que se discutiram na Seco 2.7.8. Assim, as passagens por referncia s se devem usar em procedimentos e mesmo a com parcimnia. Mais tarde ver-se- que existe o conceito de passagem por referncia constante que permite aliviar um pouco esta recomendao (ver Seco 5.2.11). Uma observao atenta do programa para clculo das fraces desenvolvido mostra que este contm instrues repetidas, que mereciam ser encapsuladas num procedimento: so as instrues de reduo das fraces aos termos mnimos, identicadas abaixo em negrito:

86
#include <iostream> using namespace std;

CAPTULO 3. MODULARIZAO: ROTINAS

/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; } /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d) { cout < < n < < / < < d; } /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; int k = mdc(n1, d1); n1 /= k; d1 /= k; k = mdc(n2, d2); n2 /= k; d2 /= k;

3.2. FUNES E PROCEDIMENTOS: ROTINAS

87

// Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; k = mdc(n, d); n /= k; d /= k; // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

necessrio, portanto, denir um procedimento que reduza uma fraco passada como argumento na forma de dois inteiros: numerador e denominador. No possvel escrever uma funo para este efeito, pois seriam necessrias duas sadas, ou dois valores de devoluo, o que as funes em C++ no permitem. Assim sendo, usa-se um procedimento que tem de ser capaz de afectar os valores dos argumentos. Ou seja, usa-se passagem de argumentos por referncia. O procedimento ento:
/** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d) { int const k = mdc(n, d); n /= k; d /= k; }

Note-se que se usaram as variveis matemticas n e d para representar os valores iniciais das variveis do programa n e d. O programa completo ento:
#include <iostream> using namespace std; /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos.

88

CAPTULO 3. MODULARIZAO: ROTINAS


@pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; } /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d) { int const k = mdc(n, d); n /= k; d /= k; } /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d) { cout < < n < < / < < d; } /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; reduzFraco(n1, d1); reduzFraco(n2, d2);

3.2. FUNES E PROCEDIMENTOS: ROTINAS

89

// Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d); // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

3.2.12 Variveis locais e globais


Uma observao cuidadosa dos exemplos anteriores revela que anal main() uma funo. Mas uma funo especial: no seu incio que comea a execuo do programa. Assim sendo, verica-se tambm que at agora s se deniram variveis dentro de rotinas. s variveis que se denem no corpo de rotinas chama-se variveis locais. As variveis locais podem ser denidas em qualquer ponto de uma rotina onde possa estar uma instruo. s variveis que se denem fora de qualquer rotina chama-se variveis globais. Os mesmos nomes se aplicam no caso das constantes: h constantes locais e constantes globais. Os parmetros de uma rotina so variveis locais como quaisquer outras, excepto quanto sua forma de inicializao: os parmetros so inicializados implicitamente com o valor dos argumentos respectivos em cada invocao da rotina.

3.2.13 Blocos de instrues ou instrues compostas


Por vezes conveniente agrupar um conjunto de instrues e trat-las como uma nica instruo. Para isso envolvem-se as instrues entre {}. Por exemplo, no cdigo
double raio1, raio2; ... if(raio1 < raio2) { double const aux = raio1; raio1 = raio2; raio2 = aux; }

as trs instrues

90
double const aux = raio1; raio1 = raio2; raio2 = aux;

CAPTULO 3. MODULARIZAO: ROTINAS

esto agrupadas num nico bloco de instrues, ou numa nica instruo composta, com execuo dependente da veracidade de uma condio. Um outro exemplo simples de um bloco de instrues o corpo de uma rotina. Os blocos de instrues podem estar embutidos (ou aninhados) dentro de outros blocos de instrues. Por exemplo, no programa
int main() { int n; cin > > n; if(n < 0) { cout < < "Valor negativo! Usando o mdulo!; n = -n; } cout < < n < < endl; }

existem dois blocos de instrues: o primeiro corresponde ao corpo da funo main() e o segundo sequncia de instrues executada condicionalmente de acordo com o valor de n. O segundo bloco de instrues encontra-se embutido no primeiro. Cada varivel tem um contexto de denio. As variveis globais so denidas no contexto do programa6 e as variveis locais no contexto de um bloco de instrues. Para todos os efeitos, os parmetros de uma rotina pertencem ao contexto do bloco de instrues correspondente ao corpo da rotina.

3.2.14 mbito ou visibilidade de variveis


Cada varivel tem um mbito de visibilidade, determinado pelo contexto no qual foi denida. As variveis globais so visveis (isto , utilizveis em expresses) desde a sua declarao at ao nal do cheiro (ver-se- no Captulo 9 que um programa pode consistir de vrios cheiros). As variveis locais, por outro lado, so visveis desde o ponto de denio at chaveta de fecho do bloco de instrues onde foram denidas. Por exemplo:
#include <iostream>
6 Note-se que as variveis globais tambm podem ser denidas no contexto do cheiro, bastando para isso preceder a sua denio do qualicador static. Este assunto ser claricado quando se discutir a diviso de um programa em cheiros no Captulo 9.

3.2. FUNES E PROCEDIMENTOS: ROTINAS


using namespace std; double const pi = 3.1416; /** Devolve o permetro de uma circunferncia de raio r. @pre P C 0 raio. @post CO permetro = 2 raio. */ double permetro(double raio) { return 2.0 * pi * raio; } int main() { cout < < "Introduza dois raios: "; double raio1, raio2; cin > > raio1 > > raio2; // Ordenao dos raios (por ordem crescente): if(raio1 < raio2) { double const aux = raio1; raio1 = raio2; raio2 = aux; } // Escrita do resultado: cout < < "raio = " < < raio1 < < ", permetro = " < < permetro(raio1) < < endl < < "raio = " < < raio2 < < ", permetro = " < < permetro(raio2) < < endl; }

91

Neste cdigo: 1. A constante pi visvel desde a sua denio at ao nal do corpo da funo main(). 2. O parmetro raio (que uma varivel local a permetro()), visvel em todo o corpo da funo permetro(). 3. As variveis raio1 e raio2 so visveis desde o ponto de denio (antes da operao de extraco) at ao nal do corpo da funo main(). 4. A constante aux visvel desde o ponto de denio at ao m da instruo composta controlada pelo if. Quanto mais estreito for o mbito de visibilidade de uma varivel, menores os danos causados por possveis utilizaes errneas. Assim, as variveis locais devem denir-se tanto quanto possvel imediatamente antes da primeira utilizao.

92

CAPTULO 3. MODULARIZAO: ROTINAS

Em cada contexto s pode ser denida uma varivel com o mesmo nome. Por exemplo:
{ int j; int k; ... int j; // erro! j denida pela segunda vez! float k; // erro! k denida pela segunda vez! }

Por outro lado, o mesmo nome pode ser reutilizado em contextos diferentes. Por exemplo, no programa da soma de fraces utiliza-se o nome n em contextos diferentes:
int mdc(int m, int n) { ... } int main() { ... int n = n1 * d2 + n2 * d1; ... }

Em cada contexto n uma varivel diferente. Quando um contexto se encontra embutido (ou aninhado) dentro de outro, as variveis visveis no contexto exterior so visveis no contexto mais interior, excepto se o contexto interior denir uma varivel com o mesmo nome. Neste ltimo caso diz-se que a denio interior oculta a denio mais exterior. Por exemplo, no programa
double f = 1.0; int main() { if(...) { f = 2.0; } else { double f = 3.0; cout < < f < < endl; } cout < < f < < endl; }

3.2. FUNES E PROCEDIMENTOS: ROTINAS

93

a varivel global f visvel desde a sua denio at ao nal do programa, incluindo o bloco de instrues aps o if (que est embutido no corpo de main()), mas excluindo o bloco de instrues aps o else (tambm embutido em main()), no qual uma outra varivel como mesmo nome denida e portanto visvel. Mesmo quando existe ocultao de uma varivel global possvel utiliz-la. Para isso basta qualicar o nome da varivel global com o operador de resoluo de mbito :: aplicado ao espao nominativo global (os espaos nominativos sero estudados na Seco 9.6.2). Por exemplo, no programa
double f = 1.0; int main() { if(...) { f = 2.0; } else { double f = ::f; f += 10.0; cout < < f < < endl; } cout < < f < < endl; }

a varivel f denida no bloco aps o else inicializada com o valor da varivel f global. Um dos principais problemas com a utilizao de variveis globais tem a ver com o facto de estabelecerem ligaes entre os mdulos (rotinas) que no so explcitas na sua interface, i.e., na informao presente no cabealho. Dois procedimentos podem usar a mesma varivel global, cando ligados no sentido em que a alterao do valor dessa varivel por um procedimento tem efeito sobre o outro procedimento que a usa. As variveis globais so assim uma fonte de erros, que ademais so difceis de corrigir. O uso de variveis globais , por isso, fortemente desaconselhado, para no dizer proibido... J o mesmo no se pode dizer de constantes globais, cuja utilizao fortemente aconselhada. Outro tipo de prtica pouco recomendvel o de ocultar nomes de contextos exteriores atravs de denies locais com o mesmo nome. Esta prtica d origem a erros de muito difcil correco.

3.2.15 Durao ou permanncia de variveis


Quando que as variveis existem, i.e., tm espao de memria reservado para elas? As variveis globais existem sempre desde o incio ao m do programa, e por isso dizem-se estticas. So construdas no incio do programa e destrudas no seu nal. As variveis locais (incluindo parmetros de rotinas) existem em memria apenas enquanto o bloco de instrues em que esto inseridas est a ser executado, sendo assim potencialmente construdas e destrudas muitas vezes ao longo de um programa. Variveis com estas caractersticas dizem-se automticas.

94

CAPTULO 3. MODULARIZAO: ROTINAS

As variveis locais tambm podem ser estticas, desde que se preceda a sua denio do qualicador static. Nesse caso so construdas no momento em que a execuo passa pela primeira vez pela sua denio e so destrudas (deixam de existir) no nal do programa. Este tipo de variveis usa-se comummente como forma de denir variveis locais que preservam o seu valor entre invocaes da rotina em que esto denidas. Inicializao As variveis de tipos bsicos podem no ser inicializadas explicitamente. Quando isso acontece, as variveis estticas so inicializadas implicitamente com um valor nulo, enquanto as variveis automticas (por uma questo de ecincia) no so inicializadas de todo, passando portanto a conter lixo. Recorde-se que os parmetros, sendo variveis locais automticas, so sempre inicializados com o valor dos argumentos respectivos. Sempre que possvel deve-se inicializar explicitamente as variveis com valores apropriados. Mas nunca se deve inicializar com qualquer coisa s para o compilador no chatear.

3.2.16 Nomes de rotinas


Tal como no caso das variveis, o nome das funes e dos procedimentos dever reectir claramente aquilo que calculado ou aquilo que feito, respectivamente. Assim, as funes tm tipicamente o nome da entidade calculada (e.g., seno(), co_seno(), comprimento()) enquanto os procedimentos tm normalmente como nome a terceira pessoa do singular do imperativo do verbo indicador da aco que realizam, possivelmente seguido de complementos (e.g., acrescenta(), copiaSemDuplicaes()). S se devem usar abreviaturas quando forem bem conhecidas, tal como o caso em mdc(). Uma excepo a estas regras d-se para funes cujo resultado um valor lgico ou booleano. Nesse caso o nome da funo deve ser um predicado, sendo o sujeito um dos argumentos da funo7 , de modo que a frase completa seja uma proposio verdadeira ou falsa. Por exemplo, estVazia(fila). Os nomes utilizados para variveis, funes e procedimentos (e em geral para qualquer outro identicador criado pelo programador), devem ser tais que a leitura do cdigo se faa da forma mais simples possvel, quase como se de portugus se tratasse. Idealmente os procedimentos tm um nico objectivo, sendo por isso descritveis usando apenas um verbo. Quando a descrio rigorosa de um procedimento obrigar utilizao de dois ou mais verbos, isso indicia que o procedimento tem mais do que um objectivo, sendo por isso um fortssimo candidato a ser dividido em dois ou mais procedimentos. A linguagem C++ imperativa, i.e., os programas consistem em sequncias de instrues. Assim, o corpo de uma funo diz como se calcula qualquer coisa, mas no diz o que se calcula. de toda a convenincia que, usando as regras indicadas, esse o que que o mais possvel explcito no nome da funo, passando-se o mesmo quanto aos procedimentos. Isto, claro est,
No caso de mtodos de classes, ver Captulo 7, o sujeito o objecto em causa, ou seja, o objecto para o qual o mtodo foi invocado.
7

3.2. FUNES E PROCEDIMENTOS: ROTINAS

95

no excluindo a necessidade de comentar funes e procedimentos com as respectivas P C e CO. Finalmente, de toda a convenincia que se use um estilo de programao uniforme. Tal facilita a compreenso do cdigo escrito por outros programadores ou pelo prprio programador depois de passados uns meses sobre a escrita do cdigo. Assim, sugerem-se as seguintes regras adicionais: Os nomes de variveis e constantes devem ser escritos em minsculas usando-se o sublinhado _ para separar as palavras. Por exemplo:
int const mximo_de_alunos_por_turma = 50; int alunos_na_turma = 10;

Os nomes de funes ou procedimentos devem ser escritos em minsculas usando-se letras maisculas iniciais em todas as palavras excepto a primeira:
int nmeroDeAlunos();

Recomendaes mais gerais sobre nomes e formato de nomes em C++ podem ser encontradas no Apndice D.

3.2.17 Declarao vs. denio


Antes de se poder invocar uma rotina necessrio que esta seja declarada, i.e., que o compilador saiba que ela existe e qual a sua interface. Declarar uma rotina consiste pois em dizer qual o seu nome, qual o tipo do valor devolvido, quantos parmetros tem e de que tipo so esses parmetros. Ou seja, declarar consiste em especicar o cabealho da rotina. Por exemplo,
void imprimeValorLgico(bool b);

ou simplesmente, visto que o nome dos parmetros irrelevante numa declarao,


void imprimeValorLgico(bool);

so possveis declaraes da funo imprimeValorLgico() que se dene abaixo:


/** Imprime verdadeiro ou falso consoante o valor lgico do argumento. @pre P C V. @post CO surge escrito no ecr verdadeiro ou falso conforme o valor de b. */ void imprimeValorLgico(bool b) { if(b) cout < < "verdadeiro"; else cout < < "falso"; }

96

CAPTULO 3. MODULARIZAO: ROTINAS

Como se viu, na declarao no necessrio indicar os nomes dos parmetros. Na prtica conveniente faz-lo para mostrar claramente ao leitor o signicado das entradas da funo. A sintaxe das declaraes em sentido estrito simples: o cabealho da rotina (como na denio) mas seguido de ; em vez do corpo. O facto de uma rotina estar declarada no a dispensa de ter de ser denida mais cedo ou mais tarde: todas as rotinas tm de estar denidas em algum lado. Por outro lado, uma denio, um vez que contm o cabealho da rotina, tambm serve de declarao. Assim, aps uma denio, as declaraes em sentido estrito so desnecessrias. Note-se que, mesmo que j se tenha procedido declarao prvia de uma rotina, necessrio voltar a incluir o cabealho na denio. O facto de uma denio ser tambm uma declarao foi usado no programa da soma de fraces para, colocando as denies das rotinas antes da funo main(), permitir que elas fossem usadas no corpo da funo main(). possvel inverter a ordem das denies atravs de declaraes prvias:
#include <iostream> using namespace std; /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Declarao dos procedimentos necessrios. Estas declaraes so visveis // apenas dentro da funo main(). void reduzFraco(int& n, int& d); void escreveFraco(int n, int d); // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d); // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2);

3.2. FUNES E PROCEDIMENTOS: ROTINAS


cout < < " "; escreveFraco(n, d); cout < < . < < endl; } /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d) { // Declarao da funo necessria. Esta declarao visvel apenas // dentro desta funo. int mdc(int m, int n); int const k = mdc(n, d); n /= k; d /= k; } /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d) { cout < < n < < / < < d; } /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; }

97

98

CAPTULO 3. MODULARIZAO: ROTINAS

A vantagem desta disposio que aparecem primeiro as rotinas mais globais e s mais tarde os pormenores, o que facilita a leitura do cdigo. Poder-se-ia alternativamente ter declarado as rotinas fora das funes e procedimentos em que so necessrios. Nesse caso o programa seria:
#include <iostream> using namespace std; /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d); /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. P C V (ou seja, nenhuma pr-condio). CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d); /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). Assume-se que m e n no mudam de valor. */ int mdc(int m, int n); /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d); // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1);

3.2. FUNES E PROCEDIMENTOS: ROTINAS


cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; } void reduzFraco(int& n, int& d) { int const k = mdc(n, d); n /= k; d /= k; } void escreveFraco(int n, int d) { cout < < n < < / < < d; } int mdc(int m, int n) { int k; if(m < n) k = m; else k = n; while(m % k != 0 or n % k != 0) --k; return k; }

99

Esta disposio mais usual que a primeira. Repare-se que neste caso a documentao das rotinas aparece junto com a sua declarao. que essa documentao fundamental para saber o que a rotina faz, e portanto faz parte da interface da rotina. Como uma declarao a especicao completa da interface, natural que seja a declarao a ser documentada, e no a denio.

3.2.18 Parmetros constantes


comum que os parmetros de uma rotina no mudem de valor durante a sua execuo. Nesse caso bom hbito explicit-lo, tornando os parmetros constantes. Dessa forma, enganos do programador sero assinalados prontamente: se alguma instruo da rotina tentar alterar o valor de um parmetro constante, o compilador assinalar o erro. O mesmo argumento pode

100

CAPTULO 3. MODULARIZAO: ROTINAS

ser aplicado no s a parmetros mas a qualquer varivel: se no suposto que o seu valor mude depois de inicializada, ento deveria ser uma constante, e no uma varivel. No entanto, do ponto de vista do consumidor de uma rotina, a constncia de um parmetro correspondente a uma passagem de argumentos por valor perfeitamente irrelevante: para o consumidor da rotina suciente saber que ela no alterar o argumento. A alterao ou no do parmetro um pormenor de implementao, e por isso importante apenas para o produtor da rotina. Assim, comum indicar-se a constncia de parmetros associados a passagens de argumentos por valor apenas na denio das rotinas e no na sua declarao. Estas ideias aplicadas ao programa da soma de fraces conduzem a
#include <iostream> using namespace std; /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d); /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d); /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int m, int n); /** Devolve o menor de dois inteiros passados como argumentos. @pre P C V (ou seja, nenhuma pr-condio). @post CO (mnimo = a a b) (mnimo = b b a). */ int mnimo(int a, int b); /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2;

3.2. FUNES E PROCEDIMENTOS: ROTINAS


reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d); // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; } void reduzFraco(int& n, int& d) { int const k = mdc(n, d); n /= k; d /= k; } void escreveFraco(int const n, int const d) { cout < < n < < / < < d; } int mdc(int const m, int const n) { int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; return k; } int mnimo(int const a, int const b) { if(a < b) return a; else return b;

101

102
}

CAPTULO 3. MODULARIZAO: ROTINAS

Repare-se que se aproveitou para melhorar a modularizao do programa acrescentando uma funo para calcular o mnimo de dois valores.

3.2.19 Instrues de assero


Que deve suceder quando o contrato de uma rotina violado? A violao de um contrato decorre sempre, sem excepo, de um erro de programao. muito importante perceber-se que assim . Em primeiro lugar, tem de se distinguir claramente os papis dos vrios intervenientes no processo de escrita e execuo de um programa: [programador] produtor aquele que escreveu uma ferramenta, tipicamente um mdulo (e.g., uma rotina). [programador] consumidor aquele que usa uma ferramenta, tipicamente um mdulo (e.g., uma rotina), com determinado objectivo. utilizador do programa aquele que faz uso do programa. A responsabilidade pela violao de um contrato nunca do utilizador do programa. A responsabilidade sempre de um programador. Se a pr-condio de uma rotina for violada, a responsabilidade do programador consumidor da rotina. Se a condio objectivo de uma rotina for violada, e admitindo que a respectiva pr-condio no o foi, a responsabilidade do programador fabricante dessa rotina. Se uma pr-condio de uma rotina for violada, o contrato assinado entre produtor e consumidor no vlido, e portanto o produtor livre de escolher o que a rotina faz nessas circunstncias. Pode fazer o que entender, desde devolver lixo (no caso de uma funo), a apagar o disco rgido e escrever uma mensagem perversa no ecr: tudo vlido. Claro est que isso no desejvel. O ideal seria que, se uma pr-condio ou uma condio objectivo falhassem, esse erro fosse assinalado claramente. No Captulo 14 ver-se- que o mecanismo das excepes o mais adequado para lidar com este tipo de situaes. Para j, falta de melhor, optar-se- por abortar imediatamente a execuo do programa escrevendo-se uma mensagem de erro apropriada no ecr. Considere-se de novo a funo para clculo do mximo divisor comum:
/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int const m, int const n); {

3.2. FUNES E PROCEDIMENTOS: ROTINAS


int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; return k; }

103

Que sucede se a pr-condio for violada? Suponha-se que a funo invocada com argumentos 10 e -6. Ento a varivel k ser inicializada com o valor -6. A guarda do ciclo inicialmente verdadeira, pelo que o valor de k passa para -7. Para este valor de k a guarda ser tambm verdadeira. E s-lo- tambm para -8, etc. Tem-se portanto um ciclo innito? No exactamente. Como se viu no Captulo 2, as variveis de tipos inteiros guardam uma gama de valores limitados. Quando se atingir o limite inferior dessa gama, o valor de k passar para o maior inteiro representvel. Da descer, lentamente, inteiro por inteiro, at ao valor 2, que levar nalmente falsidade da guarda, consequente terminao do ciclo, e devoluo do valor correcto! Isso ocorrer muito, mesmo muito tempo depois de chamada a funo, visto que exigir exactamente 4294967288 iteraes do ciclo numa mquina onde os inteiros tenham 32 bits... certamente curioso este resultado. Mesmo que a pr-condio seja violada, o algoritmo, em algumas circunstncias, devolve o resultado correcto. H duas lies a tirar deste facto: 1. A funo, tal como denida, demasiado restritiva. Uma vez que faz sentido calcular o mximo divisor comum de quaisquer inteiros, mesmo negativos, desde que no sejam ambos nulos, a funo deveria t-lo previsto desde o incio e a pr-condio deveria ter sido consideravelmente relaxada. Isso ser feito mais abaixo. 2. Se o contrato violado qualquer coisa pode acontecer, incluindo uma funo devolver o resultado correcto... Quando isso acontece h normalmente uma outra caracterstica desejvel que no se verica. Neste caso a ecincia. Claro est que nem sempre a violao de um contrato leva devoluo do valor correcto. Alis, isso raramente acontece. Repare-se no que acontece quando se invoca a funo mdc() com argumentos 0 e 10, por exemplo. Nesse caso o valor inicial de k 0, o que leva a que a condio da instruo iterativa while tente calcular uma diviso por zero. Logo que isso sucede o programa aborta com uma mensagem de erro pouco simptica e semelhante seguinte:
Floating exception (core dumped)

Existe uma forma padronizada de explicitar as condies que devem ser verdadeiras nos vrios locais de um programa, obrigando o computador a vericar a sua validade durante a execuo do programa: as chamadas instrues de assero (armao). Para se poder usar instrues de assero tem de ser incluir o cheiro de interface apropriado:
#include <cassert>

104 As instrues de assero tm o formato


assert(condio);

CAPTULO 3. MODULARIZAO: ROTINAS

onde condio uma condio que deve ser verdadeira no local onde a instruo se encontra para que o programa se possa considerar correcto. Se a condio for verdadeira quando a instruo de assero executada, nada acontece: a instruo no tem qualquer efeito. Mas se a condio for falsa o programa aborta e mostrada uma mensagem de erro com o seguinte aspecto:

ficheiro_executvel: ficheiro_fonte:linha: cabealho: Assertion condio failed.

onde: ficheiro_executvel Nome do cheiro executvel onde ocorreu o erro. ficheiro_fonte Nome do cheiro em linguagem C++ onde ocorreu o erro. linha Nmero da linha do cheiro C++ onde ocorreu o erro. cabealho Cabealho da funo ou procedimento onde ocorreu o erro (s em alguns compiladores). condio Condio que deveria ser verdadeira mas no o era. Deve-se usar uma instruo de assero para vericar a veracidade da pr-condio da funo mdc():
/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int const m, int const n); { assert(0 < m and 0 < n); int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; return k; }

Suponha-se o seguinte programa usando a funo mdc():

3.2. FUNES E PROCEDIMENTOS: ROTINAS


#include <iostream> #include <cassert> using namespace std; /** Devolve o menor de dois inteiros passados como argumentos. @pre P C V (ou seja, nenhuma pr-condio). @post CO (mnimo = a a b) (mnimo = b b a). */ int mnimo(int const a, int const b) { if(a < b) return a; else return b; } /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int const m, int const n) { assert(0 < m and 0 < n); int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; return k; } int main() { cout < < mdc(0, 10) < < endl; }

105

A execuo deste programa leva seguinte mensagem de erro:

teste: teste.C:23: int mdc(int, int): Assertion 0 < m and 0 < n failed. Abort (core dumped)

claro que este tipo de mensagens muito mais til para o programador que o simples abortar do programa ou, pior, a produo pelo programa de resultados errados.

106

CAPTULO 3. MODULARIZAO: ROTINAS

Suponha-se agora o programa original, para o clculo da soma de fraces, mas j equipado com instrues de assero vericando as pr-condies dos vrios mdulos que o compem (exceptuando os que no impem qualquer pr-condio):
#include <iostream> using namespace std; /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d @post CO n = n mdc(n, d) = 1 */ d d void reduzFraco(int& n, int& d); /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d); /** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int m, int n); /** Devolve o menor de dois inteiros passados como argumentos. @pre P C V (ou seja, nenhuma pr-condio). @post CO (mnimo = a a b) (mnimo = b b a). */ int mnimo(int a, int b); /** Calcula e escreve a soma em termos mnimos de duas fraces (positivas) lidas do teclado: */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d);

3.2. FUNES E PROCEDIMENTOS: ROTINAS

107

// Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; } void reduzFraco(int& n, int& d) { assert(0 < n and 0 < d); int const k = mdc(n, d); n /= k; d /= k; } void escreveFraco(int const n, int const d) { cout < < n < < / < < d; } int mdc(int const m, int const n) { assert(0 < m and 0 < n); int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; return k; } int mnimo(int const a, int const b) { if(a < b) return a; else return b; }

Que h de errado com este programa? Considere-se o que acontece se o seu utilizador intro-

108 duzir fraces negativas, por exemplo:


-6 7 15 7

CAPTULO 3. MODULARIZAO: ROTINAS

Neste caso o programa aborta com uma mensagem de erro porque a pr-condio do procedimento reduzFraco() foi violada. Um programa no deve abortar nunca. Nunca mesmo. De quem a responsabilidade disso acontecer neste caso? Do utilizador do programa, que desobedeceu introduzindo fraces negativas apesar de instrudo para no o fazer, ou do programador produtor da funo main() e consumidor do procedimento reduzFraco()? A resposta correcta a segunda: a culpa nunca do utilizador, do programador. Isto tem de car absolutamente claro: o utilizador tem sempre razo. Como resolver o problema? H duas solues. A primeira diz que deve ser o programador consumidor a garantir que os valores passados nos argumentos de reduzFraco() tm de ser positivos, conforme se estabeleceu na sua pr-condio. Em geral este o caminho certo, embora neste caso se possa olhar para o problema com um pouco mais de cuidado e reconhecer que a reduo de fraces s deveria ser proibida se o denominador fosse nulo. Isso implica, naturalmente, refazer o procedimento reduzFraco(), relaxando a sua pr-condio. O que ressalta daqui que quanto mais leonina (forte) uma pr-condio, menos trabalho tem o programador produtor do mdulo e mais trabalho tem o programador consumidor. Pelo contrrio, se a pr-condio for fraca, isso implica mais trabalho para o produtor e menos para o consumidor. Em qualquer dos casos, continua a haver circunstncias nas quais as pr-condies do procedimento podem ser violadas. sempre responsabilidade do programador consumidor garantir que isso no acontece. A soluo mais simples seria usar um ciclo para pedir de novo ao utilizador para introduzir as fraces enquanto estas no vericassem as condies pretendidas. Tal soluo no ser ensaiada aqui, por uma questo de simplicidade. Adoptar-se- a soluo algo simplista de terminar o programa sem efectuar os clculos no caso de haver problemas com os valores das fraces:
int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; if(n1 < 0 or d1 < 0 or n2 < 0 or d2 < 0) cout < < "Termos negativos. Nada feito." < < endl; else { reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2;

3.2. FUNES E PROCEDIMENTOS: ROTINAS


reduzFraco(n, d); // Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl;

109

}
}

Neste caso evidente que no haver violao de nenhuma pr-condio. Muitos tero a tentao de perguntar para que servem as asseres neste momento, e se no seria apropriado elimin-las. H vrias razes para as asseres continuarem a ser indispensveis: 1. O programador, enquanto produtor, no deve assumir nada acerca dos consumidores (muito embora em muitos casos produtor e consumidor sejam uma e a mesma pessoa). O melhor mesmo colocar a assero: o diabo tece-as. 2. O produtor deve escrever uma rotina pensando em possveis reutilizaes futuras. Pode haver erros nas futuras utilizaes, pelo que o mais seguro mesmo manter a assero. 3. Se algum zer alteraes no programa pode introduzir erros. A assero nesse caso permitir a sua rpida deteco e correco. Parece ter faltado algo em toda esta discusso: a condio objectivo. Tal como se deve usar asseres para vericar as pr-condies, tambm se deve usar asseres para vericar as condies objectivo. A falsidade de uma condio objectivo, sabendo que a respectiva pr-condio verdadeira, tambm devida a um erro de programao, s que desta vez o responsvel pelo erro o programador produtor. Assim, as asseres usadas para vericar as pr-condies servem para o produtor de uma rotina facilitar a deteco de erros do programador consumidor, enquanto as asseres usadas para vericar as condies objectivo servem para o produtor de uma rotina se proteger dos seus prprios erros. Transformar as condies objectivo em asseres , em geral, uma tarefa mais difcil que no caso das pr-condies, que tendem a ser mais simples. As maiores diculdades surgem especialmente se a condio objectivo contiver quanticadores (somatrios, produtos, quaisquer que seja, existe uns, etc.), se estiverem envolvidas inseres ou extraces de canais (entradas e sadas) ou se a condio objectivo zer meno aos valores originais das variveis, i.e., ao valor que as variveis possuiam no incio da rotina em causa. Considere-se cada uma das rotinas do programa. O caso da funo mnimo() fcil de resolver, pois a condio objectivo traduz-se facilmente para C++. necessrio, no entanto, abandonar os retornos imediatos e guardar o valor a

110 devolver numa varivel a usar na assero 8 :

CAPTULO 3. MODULARIZAO: ROTINAS

/** Devolve o menor de dois inteiros passados como argumentos. @pre P C V (ou seja, nenhuma pr-condio). @post CO (mnimo = a a b) (mnimo = b b a). */ int mnimo(int const a, int const b) { int mnimo; if(a < b) mnimo = a; else mnimo = b; assert((mnimo == a and a <= b) or (mnimo == b and b <= a)); return mnimo; }

Quanto ao procedimento reduzFraco(), fcil vericar o segundo termo da condio objectivo.


/** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d 0 < n 0 < d n @post CO d = n mdc(n, d) = 1 */ d void reduzFraco(int& n, int& d) { assert(0 < n and 0 < d); int const k = mdc(n, d); n /= k; d /= k; assert(mdc(n, d) == 1); }

Mas, e o primeiro termo, que se refere aos valores originais de n e d? H linguagens, como Eiffel [11], nas quais as asseres correspondentes s condies objectivo podem fazer recurso aos valores das variveis no incio da respectiva rotina, usando-se para isso uma notao especial. Em C++ no possvel faz-lo, infelizmente. Por isso o primeiro termo da condio objectivo car por vericar. O procedimento escreveFraco() tem um problema: o seu objectivo escrever no ecr. No fcil formalizar uma condio objectivo que envolve alteraes do ecr, como se pode
comum usar-se o truque de guardar o valor a devolver por uma funo numa varivel com o mesmo nome da funo, pois nesse caso a assero tem exactamente o mesmo aspecto que a condio-objectivo.
8

3.2. FUNES E PROCEDIMENTOS: ROTINAS

111

ver pela utilizao de portugus vernculo na condio objectivo. ainda menos fcil escrever uma instruo de assero nessas circunstncias. Este procedimento ca, pois, sem qualquer instruo de assero. Finalmente, falta a funo mdc(). Neste caso a condio objectivo faz uso de uma funo matemtica mdc. No faz qualquer sentido escrever a instruo de assero como se segue:
/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int const m, int const n) { assert(0 < n and 0 < d); int k = mnimo(m, n); while(m % k != 0 or n % k != 0) --k; assert(k == mdc(m, n)); // absurdo! return k; }

Porqu? Simplesmente porque isso implica uma invocao recursiva interminvel funo (ver Seco 3.3). Isso signica que se dever exprimir a condio objectivo numa forma menos compacta: CO m mdc = 0 n mdc = 0 (Q j : mdc < j : m j = 0 n j = 0) . Os dois primeiros termos da condio objectivo tm traduo directa para C++. Mas o segundo recorre a um quanticador que signica qualquer que seja j maior que o valor devolvido pela funo, esse j no pode ser divisor comum de m e n (os quanticadores sero abordados mais tarde). No h nenhuma forma simples de escrever uma instruo de assero recorrendo a quanticadores, excepto invocando uma funo que use um ciclo para vericar o valor do quanticador. Mas essa soluo, alm de complicada, obriga implementao de uma funo adicional, cuja condio objectivo recorre de novo ao quanticador. Ou seja, no soluo... Assim, o melhor que se pode fazer reter os dois primeiros termos da condio objectivo reescrita:
/** Calcula e devolve o mximo divisor comum de dois inteiros positivos passados como argumentos. @pre P C 0 < m 0 < n. @post CO mdc = mdc(m, n). */ int mdc(int const m, int const n)

112
{ assert(0 < n and 0 < d); int k = mnimo(m, n);

CAPTULO 3. MODULARIZAO: ROTINAS

while(m % k != 0 or n % k != 0) --k; assert(m % k == 0 and n % k == 0); return k; }

Estas diculdades no devem levar ao abandono pura e simples do esforo de expressar prcondies e condies objectivo na forma de instrues de assero. A vantagem das instrues de assero por si s enorme, alm de que o esforo de as escrever exige uma completa compreenso do problema, o que leva naturalmente a menos erros na implementao da respectiva resoluo. A colocao criteriosa de instrues de assero , pois, um mecanismo extremamente til para a depurao de programas. Mas tem uma desvantagem aparente: as vericaes da asseres consomem tempo. Para qu, ento, continuar a fazer essas vericaes quando o programa j estiver liberto de erros? O mecanismo das instrues de assero interessante porque permite evitar esta desvantagem com elegncia: basta denir uma macro de nome NDEBUG (no debug) para que as asseres deixem de ser vericadas e portanto deixem de consumir tempo, no sendo necessrio apag-las do cdigo. As macros sero explicadas no Captulo 9, sendo suciente para j saber que a maior parte dos compiladores para Unix (ou Linux) permitem a denio dessa macro de uma forma muito simples: basta passar a opo -DNDEBUG ao compilador.

3.2.20 Melhorando mdulos j produzidos


Uma das vantagens da modularizao, como se viu, que se pode melhorar a implementao de qualquer mdulo sem com isso comprometer o funcionamento do sistema e sem obrigar a qualquer outra alterao. Na verso do programa da soma de fraces que se segue utiliza-se uma funo de clculo do mdc com um algoritmo diferente, mais eciente. o algoritmo de Euclides, que decorre naturalmente das seguintes propriedades do mdc (lembra-se das sugestes no nal do Captulo 1?): 1. mdc(m, n) = mdc(n m, m) se 0 < m 0 n. 2. mdc(m, n) = n se m = 0 0 < n. O algoritmo usado deixa de ser uma busca exaustiva do mdc para passar a ser uma reduo sucessiva do problema at trivialidade: a aplicao sucessiva da propriedade1 vai reduzindo os valores at um deles ser zero. A demonstrao da sua correco faz-se exactamente da

3.2. FUNES E PROCEDIMENTOS: ROTINAS

113

mesma forma que no caso da busca exaustiva, e ca como exerccio para o leitor. Regressese a este algoritmo depois de ter lido sobre metodologias de desenvolvimentos de ciclos no Captulo 4. Aproveitou-se ainda para relaxar as pr-condies da funo, uma vez que o algoritmo utilizado permite calcular o mdc de dois inteiros m e n qualquer que seja m desde que n seja positivo. Este relaxar das pr-condies permite que o programa some convenientemente fraces negativas, para o que foi apenas necessrio alterar o procedimento reduzFraco() de modo a garantir que o denominador sempre positivo.
#include <iostream> using namespace std; /** Reduz a fraco passada com argumento na forma de dois inteiros positivos. @pre P C n = n d = d d = 0 @post CO n = n 0 < d mdc(n, d) = 1 */ d d void reduzFraco(int& n, int& d); /** Escreve no ecr uma fraco, no formato usual, que lhe passada na forma de dois argumentos inteiros positivos. @pre P C V (ou seja, nenhuma pr-condio). @post CO o ecr contm n/d em que n e d so os valores de n e d representados em base decimal. */ void escreveFraco(int n, int d); /** Calcula e devolve o mximo divisor comum de dois inteiros passados como argumentos (o segundo inteiro tem de ser positivo). @pre P C m = m n = n 0 < n. @post CO mdc = mdc(|m|, n). */ int mdc(int m, int n); /** Calcula e escreve a soma em termos mnimos de duas fraces lidas do teclado (os denominadores no podem ser nulos!): */ int main() { // Leitura das fraces do teclado: cout < < "Introduza duas fraces: "; int n1, d1, n2, d2; cin > > n1 > > d1 > > n2 > > d2; reduzFraco(n1, d1); reduzFraco(n2, d2); // Clculo da fraco soma em termos mnimos: int n = n1 * d2 + n2 * d1; int d = d1 * d2; reduzFraco(n, d);

114

CAPTULO 3. MODULARIZAO: ROTINAS

// Escrita do resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; } void reduzFraco(int& n, int& d) { assert(d != 0); if(d < 0) { n = -n; d = -d; } int const k = mdc(n, d); n /= k; d /= k; assert(0 < d and mdc(n, d) == 1); } void escreveFraco(int const n, int const d) { cout < < n < < / < < d; } int mdc(int m, int n) { assert(0 < n); if(m < 0) m = -m; while(m != 0) { int const auxiliar = n % m; n = m; m = auxiliar; } return n; }

3.3. ROTINAS RECURSIVAS

115

3.3 Rotinas recursivas


O C++, como a maior parte das linguagens de programao imperativas, permite a denio daquilo a que se chama rotinas recursivas. Diz-se que uma rotina recursiva se o seu corpo incluir chamadas prpria rotina9 . Por exemplo
/** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { if(n == 0 or n == 1) return 1; return n * factorial(n - 1); }

n
i=1

i). */

uma funo recursiva que calcula o factorial e que foi obtida de uma forma imediata a partir da denio recorrente do factorial n! = n(n 1)! se 0 < n , 1 se n = 0

usando-se adicionalmente o facto de que 1! tambm 1. Este tipo de rotinas pode ser muito til na resoluo de alguns problemas, mas deve ser usado com cautela. A chamada de uma rotina recursivamente implica que as variveis locais (parmetros includos) so construdas tantas vezes quantas a rotina chamada e s so destrudas quando as correspondentes chamadas retornam. Como as chamadas recursivas se aninham umas dentro das outras, se ocorrerem muitas chamadas recursivas no s pode ser necessria muita memria para as vrias verses das variveis locais (uma verso por cada chamada aninhada), como tambm a execuo pode tornar-se bastante lenta, pois a chamada de funes implica alguma perda de tempo nas tarefas de arrumao da casa do processador. A funo factorial(), em particular, pode ser implementada usando um ciclo, que resulta em cdigo muito mais eciente e claro:
/** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { int factorial = 1; for(int i = 0; i != n; ++i) factorial *= i + 1; return factorial; }
9

n i). */ i=1

possvel ainda que a recursividade seja entre duas ou mais rotinas, que se chamam mutuamente.

116

CAPTULO 3. MODULARIZAO: ROTINAS

Muito importante tambm a garantia de que uma rotina recursiva termina sempre. A funo factorial recursiva acima tem problemas graves quando invocada com um argumento negativo. que vai sendo chamada recursivamente a mesma funo com valores do argumento cada vez menores (mais negativos), sem m vista. Podem acontecer duas coisas. Como cada chamada da funo implica a construo de uma varivel local (o parmetro n) num espao de memria reservado para a chamada pilha (stack), como se ver na prxima seco, esse espao pode-se esgotar, o que leva o programa a abortar. Ou ento, se por acaso houver muito, mas mesmo muito espao disponvel na pilha, as chamadas recursivas continuaro at se atingir o limite inferior dos inteiros nos argumentos. Nessa altura a chamada seguinte feita com o menor dos inteiros menos uma unidade, que como se viu no Captulo 2 o maior dos inteiros. A partir da os argumentos das chamadas recursivas comeam a diminuir e, ao m de muito, mas mesmo muito tempo, atingiro o valor 0, que terminar a sequncia de chamadas recursivas, sendo devolvido um valor errado. Os problemas no surgem apenas com argumentos negativos, na realidade. que os valores do factorial crescem depressa, pelo que a funo no pode ser invocada com argumentos demasiado grandes. No caso de os inteiros terem 32 bits, o limite a impor aos argumentos que tm de ser inferiores a 13! A funo deve sofrer uma actualizao na pr-condio e, j agora, ser equipada com as instrues de assero apropriadas:
/** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n 12. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { assert(0 <= n and n <= 12); if(n == 0 or n == 1) return 1; return n * factorial(n - 1); }

n i). */ i=1

o mesmo acontecendo com a verso no-recursiva:


/** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n 12. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { assert(0 <= n and n <= 12); int factorial = 1; for(int i = 0; i != n; ++i) factorial *= i + 1; return factorial; }

n i). */ i=1

3.4. MECANISMO DE INVOCAO DE ROTINAS

117

Para se compreender profundamente o funcionamento das rotinas recursivas tem de se compreender o mecanismo de chamada ou invocao de rotinas, que se explica na prxima seco.

3.4 Mecanismo de invocao de rotinas


Quando nas seces anteriores se descreveu a chamada da funo mdc(), referiu-se que os seus parmetros eram construdos no incio da chamada e destrudos no seu nal, e que a funo, ao terminar, retornava para a instruo imediatamente aps a instruo de invocao. Como que o processo de invocao funciona na prtica? Apesar de ser matria para a disciplina de Arquitectura de Computadores, far-se- aqui uma descrio breve e simplicada do mecanismo de invocao de rotinas que ser til (embora no fundamental) para se compreender o funcionamento das rotinas recursivas. O mecanismo de invocao de rotinas utiliza uma parte da memria do computador como se de uma pilha se tratasse, i.e., como um local onde se pode ir acumulando informao de tal forma que a ltima informao a ser colocada na pilha seja a primeira a ser retirada, um pouco como acontece com as pilhas de processos das reparties pblicas, em os processos dos incautos podem ir envelhecendo ao longo de anos na base de uma pilha... A pilha utilizada para colocar todas as variveis locais automticas quando estas so construdas. O topo da pilha varia quando executada uma instruo de denio de um varivel automtica. As variveis denidas so colocadas (construdas) no topo da pilha e apenas so retiradas (destrudas) quando se abandona o bloco onde foram denidas. tambm na pilha que se guarda o endereo da instruo para onde o uxo de execuo do programa deve retornar uma vez terminada a execuo de uma rotina. No fundo, a pilha serve para o computador saber a quantas anda. Exemplo no-recursivo Suponha-se o seguinte programa, incluindo a funo mdc(),
#include <iostream> using namespace std; /** Calcula e devolve o mximo divisor comum de dois inteiros passados como argumentos (o segundo inteiro tem de ser positivo). @pre P C m = m n = n 0 < n. @post CO mdc = mdc(|m|, n). */ int mdc(int const m, int const n) { assert(0 < n); if(m < 0) m = -m;

118
while(m int n = m = }

CAPTULO 3. MODULARIZAO: ROTINAS


!= 0) { const auxiliar = n % m; m; auxiliar;

assert(m % k == 0 and n % k == 0); return n; } int main() { int m = 5;

// 1 mdc(m + 3, 6) // 2A int divisor = ; // 2B cout < < divisor < < endl; // 3

em que se dividiu a instruo 2 em duas sub-instrues 2A e 2B. Considera-se, para simplicar, que pilha est vazia quando se comea a executar a funo main(): topo da pilha

De seguida executada a instruo 1, que constri a varivel m. Como essa varivel automtica, construda na pilha, que ca

m: int 5

Que acontece quando a instruo 2A executada? A chamada da funo mdc() na instruo 2A comea por guardar na pilha o endereo da instruo a executar quando a funo retornar, i.e., 2B

3.4. MECANISMO DE INVOCAO DE ROTINAS

119

retorno a 2B m: int 5

Em seguida so construdos na pilha os parmetros da funo. Cada parmetro inicializado com o valor do argumento respectivo:

n: int 6 m: int 8 retorno a 2B m: int


Neste momento existem na pilha duas variveis de nome m: uma pertencente funo main() e outra funo mdc(). fcil saber em cada instante quais so as variveis automtica visveis: so todas as variveis desde o topo da pilha at ao prximo endereo de retorno. Isto , neste momento so visveis apenas as variveis m e n de mdc(). A execuo passa ento para o corpo da funo, onde durante o ciclo a constante local auxiliar construda e destruda no topo da pilha vrias vezes 10 , at que o ciclo termina com valor desejado na varivel n, i.e., 2. Assim, imediatamente antes da execuo da instruo de retorno, a pilha contm:
10 Qualquer compilador minimamente inteligente evita este processo de construo e destruio repetitivas construindo a varivel auxiliar na pilha logo no incio da invocao da funo.

120

CAPTULO 3. MODULARIZAO: ROTINAS

n: int 2 m: int 0 retorno a 2B m: int


A instruo de retorno comea por calcular o valor a devolver (neste caso o valor de n, i.e., 2), retira da pilha (destri) todas as variveis desde o topo at ao prximo endereo de retorno

e em seguida retira da pilha a instruo para onde o uxo de execuo deve ser retomado (i.e., 2B) colocando na pilha (mas para l do seu topo, em situao periclitante...) o valor a devolver 2

m: int 5

Em seguida a execuo continua em 2B, que constri a varivel divisor, inicializando-a com o valor devolvido, colocado aps o topo da pilha, que depois se deixa levar por uma corrente de ar:

retorno a 2B m: int 5

3.4. MECANISMO DE INVOCAO DE ROTINAS

121

d: int 2 m: int

O valor de d depois escrito no ecr na instruo 3. Finalmente atinge-se o nal da funo main(), o que leva retirada da pilha (destruio) de todas as variveis at base (uma vez que no h nenhum endereo de retorno na pilha):

No nal, a pilha ca exactamente como no incio: vazia. Exemplo recursivo Suponha-se agora o seguinte exemplo, que envolve a chamada funo recursiva factorial():
#include <iostream> using namespace std; /** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n 12. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { assert(0 <= n and n <= 12); if(n == 0 or n == 1) return 1; return n * factorial(n - 1); // 1 // 2 // 3

} int main() { cout < < factorial(3) < < endl; // 4 }

Mais uma vez conveniente dividir a instruo 4 em duas sub-instrues

n
i=1

i). */

122
factorial(3) cout < <

CAPTULO 3. MODULARIZAO: ROTINAS


// 4A < < endl; // 4B

uma vez que a funo invocada antes da escrita do resultado no ecr. Da mesma forma, a instruo 3 pode ser dividida em duas sub-instrues
factorial(n - 1) // 3A ; // 3B

return n *

Ou seja,
#include <iostream> using namespace std; /** Devolve o factorial do inteiro passado como argumento. @pre P C 0 n 12. @post CO factorial = n! (ou ainda factorial = int factorial(int const n) { assert(0 <= n and n <= 12); if(n == 0 or n == 1) // 1 return 1; // 2 factorial(n - 1) // 3A return n * ; // 3B } int main() { factorial(3) cout < < } // 4A < < endl; // 4B

n
i=1

i). */

Que acontece ao ser executada a instruo 4A? Essa instruo contm uma chamada funo factorial(). Assim, tal como se viu antes, as variveis locais da funo (neste caso apenas o parmetro constante n) so colocadas na pilha logo aps o endereo da instruo a executar quando funo retornar. Quando a execuo passa para a instruo 1, j a pilha est j como indicado em (b) na Figura 3.3. Em seguida, como a constante n contm 3 e portanto diferente de 0 e de 1, executada a instruo aps o if, que a instruo 3A (se tiver dvidas acerca do funcionamento do if, consulte a Seco 4.1.1). Mas a instruo 3A consiste numa nova chamada funo, pelo que os passos acima se repetem, mas sendo agora o parmetro inicializado com o valor do argumento, i.e., 2, e sendo o endereo de retorno 3B, resultando na pilha indicada em (c) na Figura 3.3.

3.4. MECANISMO DE INVOCAO DE ROTINAS

123

topo da pilha

n: int 1 retorno a 3B n: int 2 retorno a 3B n: int 3 retorno a 4B


valor devolvido : int 1 o mesmo que {frozen} (constante) n: int 2 retorno a 3B n: int 3 retorno a 4B

n: int 2 retorno a 3B n: int 3 retorno a 4B


: int 2

n: int 3 retorno a 4B

n: int 3 retorno a 4B

: int 6

(a)

(b)

(c)

(d)

(e)

(f)

(g)

(h)

Figura 3.3: Evoluo da pilha durante invocaes recursivas da funo factorial(). Admite-se que a pilha inicialmente est vazia, como indicado em (a).

124

CAPTULO 3. MODULARIZAO: ROTINAS

Neste momento existem duas verses da constante n na pilha, uma por cada chamada funo que ainda no terminou (h uma chamada principal e outra aninhada). esta repetio das variveis e constantes locais que permite s funes recursivas funcionarem sem problemas. A execuo passa ento para o incio da funo (instruo 1). De novo, como constante n da chamada em execuo correntemente 2, e portanto diferente de 0 e de 1, executada a instruo aps o if, que a instruo 3A. Mas a instruo 3A consiste numa nova chamada funo, pelo que os passos acima se repetem, mas sendo agora o parmetro inicializado com o valor do argumento, i.e., 1, e sendo o endereo de retorno 3B, resultando na pilha indicada em (d) na Figura 3.3. A execuo passa ento para o incio da funo (instruo 1). Agora, como a constante n da chamada em execuo correntemente 1, executada a instruo condicionada pelo if, que a instruo 2. Mas a instruo 2 consiste numa instruo de retorno com devoluo do valor 1. Assim, as variveis e constantes locais so retiradas da pilha, o endereo de retorno (3B) retirado da pilha, o valor de devoluo 1 colocado aps o topo da pilha, e a execuo continua na instruo 3B, cando a pilha indicada em (e) na Figura 3.3. A instruo 3B consiste numa instruo de retorno com devoluo do valor n * 1(ou seja 2), em que n tem o valor 2 e o 1 o valor de devoluo da chamada anterior, que cou aps o topo da pilha. Assim, as variveis e constantes locais so retiradas da pilha, o endereo de retorno (3B) retirado da pilha, o valor de devoluo 2 colocado aps o topo da pilha, e a execuo continua na instruo 3B, cando a pilha indicada em (f) na Figura 3.3. A instruo 3B consiste numa instruo de retorno com devoluo do valor n * 2 (ou seja 6), em que n tem o valor 3 e o 2 o valor de devoluo da chamada anterior, que cou aps o topo da pilha. Assim, as variveis locais e constantes locais so retiradas da pilha, o endereo de retorno (4B) retirado da pilha, o valor de devoluo 6 colocado aps o topo da pilha, e a execuo continua na instruo 4B, cando a pilha indicada em (g) na Figura 3.3. A instruo 4B corresponde simplesmente a escrever no ecr o valor devolvido pela chamada funo, ou seja, 6 (que 3!, o factorial de 3). de notar que, terminadas todas as chamadas funo, a pilha voltou sua situao original (que se sups ser vazia)indicada em (h) na Figura 3.3. A razo pela qual as chamadas recursivas funcionam como espectvel que, em cada chamada aninhada, so criadas novas verses das variveis e constantes locais e dos parmetros (convenientemente inicializados) da rotina. Embora os exemplos acima se tenham baseado em funes, evidente que o mesmo mecanismo usado para os procedimentos, embora simplicado pois estes no devolvem qualquer valor.

3.5 Sobrecarga de nomes


Em certos casos importante ter rotinas que fazem conceptualmente a mesma operao ou o mesmo clculo, mas que operam com tipos diferentes de dados. Seria pois de todo o interesse que fosse permitida a denio de rotinas com nomes idnticos, distintos apenas no tipo dos seus parmetros. De facto, a linguagem C++ apenas probe a denio no mesmo contexto de

3.5. SOBRECARGA DE NOMES

125

funes ou procedimentos com a mesma assinatura, i.e., no apenas com o mesmo nome, mas tambm com a mesma lista dos tipos dos parmetros 11 . Assim, de permitida a denio de mltiplas rotinas com o mesmo nome, desde que diram no nmero ou tipo de parmetros. As rotinas com o mesmo nome dizem-se sobrecarregadas. A invocao de rotinas sobrecarregadas faz-se como habitualmente, sendo a rotina que de facto invocada determinada a partir do nmero e tipo dos argumentos usados na invocao. Por exemplo, suponha-se que esto denidas as funes:
int soma(int const a, int const b) { return a + b; } int soma(int const a, int const b, int const c) { return a + b + c; } float soma(float const a, float const b) { return a + b; } double soma(double const a, double const b) { return a + b; }

Ao executar as instrues
int i1, i2; float f1, f2; double d1, d2; i2 = soma(i1, 4); i2 = soma(i1, 3, i2); f2 = soma(5.6f, f1); d2 = soma(d1, 10.0);

// // // //

invoca invoca invoca invoca

int soma(int, int). int soma(int, int, int). float soma(float, float). double soma(double, double).

so chamadas as funes apropriadas para cada tipo de argumentos usados. Este tipo de comportamento emula para as funes denidas pelo programador o comportamento normal dos operadores do C++. A operao +, por exemplo, signica soma de int se os operandos forem int, signica soma de float se os operandos forem float, etc. O exemplo mais claro talvez seja o do operador diviso (/). As instrues
A noo de assinatura usual um pouco mais completa que na linguagem C++, pois inclui o tipo de devoluo. Na linguagem C++ o tipo de devoluo no faz parte da assinatura.
11

126
cout < < 1 / 2 < < endl; cout < < 1.0 / 2.0 < < endl;

CAPTULO 3. MODULARIZAO: ROTINAS

tm como resultado no ecr


0 0.5

porque no primeiro caso, sendo os operandos inteiros, a diviso usada a diviso inteira. Assim, cada operador bsico corresponde na realidade a vrios operadores com o mesmo nome, i.e., sobrecarregados, cada um para determinado tipo dos operandos. A assinatura de uma rotina corresponde sequncia composta pelo seu nome, pelo nmero de parmetros, e pelos tipos dos parmetros. Por exemplo, as funes soma() acima tm as seguintes assinaturas12 : soma, int, int soma, int, int, int soma, float, float soma, double, double O tipo de devoluo de uma rotina no faz parte da sua assinatura, no servindo portanto para distinguir entre funes ou procedimentos sobrecarregados. Num captulo posterior se ver que possvel sobrecarregar os signicados dos operadores bsicos (como o operador +) quando aplicados a tipos denidos pelo programador, o que transforma o C++ numa linguagem que se artilha de uma forma muito elegante e potente.

3.6 Parmetros com argumentos por omisso


O C++ permite a denio de rotinas em que alguns parmetros tm argumentos por omisso. I.e., se no forem colocados os argumentos respectivos numa invocao da rotina, os parmetros sero inicializados com os valores dos argumentos por omisso. Mas com uma restrio: os parmetros com argumentos por omisso tm de ser os ltimos da rotina. Por exemplo, a denio
int soma(int const a = 0, int const b = 0, int const c = 0, int const d = 0) { return a + b + c + d; }
A constncia de um parmetro (desde que no seja um referncia) no afecta a assinatura, pois esta reecte a interface da rotina, e no a sua implementao.
12

3.6. PARMETROS COM ARGUMENTOS POR OMISSO


permite a invocao da funo soma() com qualquer nmero de argumentos at 4: cout cout cout cout cout << << << << << soma() < < endl soma(1) < < endl soma(1, 2) < < endl soma(1, 2, 3) < < endl soma(1, 2, 3, 4) < < endl; // // // // // Surge 0. Surge 1. Surge 3. Surge 6. Surge 10.

127

Normalmente os argumentos por omisso indicam-se apenas na declarao de um rotina. Assim, se a declarao da funo soma() fosse feita separadamente da respectiva denio, o cdigo deveria passar a ser:
int soma(int const a = 0, int const b = 0, int const c = 0, int const d = 0); ... int soma(int const a, int const b, int const c, int const d) { return a + b + c + d; }

128

CAPTULO 3. MODULARIZAO: ROTINAS

Captulo 4

Controlo do uxo dos programas


Se temos...! Diz ela mas o problema no s de aprender saber a partir da que fazer Srgio Godinho, 2o andar direito, Pano Cru.

Quase todas as resolues de problemas envolvem tomadas de decises e repeties. Dicilmente se consegue encontrar um algoritmo interessante que no envolva, quando lido em portugus, as palavras se e enquanto, correspondendo aos conceitos de seleco e de iterao. Neste captulo estudar-se-o em pormenor os mecanismos da programao imperativa que suportam esses conceitos e discutir-se-o metodologias de resoluo de problemas usando esses mecanismos.

4.1 Instrues de seleco


A resoluo de um problema implica quase sempre a tomada de decises ou pelo menos a seleco de alternativas. Suponha-se que se pretendia desenvolver uma funo para calcular o valor absoluto de um inteiro. O esqueleto da funo pode ser o que se segue
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0 absoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { ... }

129

130

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Este esqueleto inclui, como habitualmente, documentao claricando o que a funo faz, o cabealho que indica como a funo se utiliza, e um corpo, onde se colocaro as instrues que resolvem o problema, i.e., que explicitam como a funo funciona. A resoluo deste problema requer que sejam tomadas aces diferentes consoante o valor de x seja positivo ou negativo (ou nulo). So portanto fundamentais as chamadas instrues de seleco ou alternativa.

4.1.1 As instrues if e if else


As instrues if e if else so das mais importantes instrues de controlo do uxo de um programa, i.e., instrues que alteram a sequncia normal de execuo das instrues de um programa. Estas instrues permitem executar uma instruo caso uma condio seja verdadeira e, no caso da instruo if else, uma outra instruo caso a condio seja falsa. Por exemplo, no troo de programa
if(x < 0) x = 0; else x = 1; ... // // // // // 1 2 3 4 5

as linhas 1 a 4 correspondem a uma nica instruo de seleco. Esta instruo de seleco composta de uma condio (na linha 1) e duas instrues alternativas (linhas 2 e 4). Se x for menor do que zero quando a instruo if executada, ento a prxima instruo a executar a instruo na linha 2, passando o valor de x a ser zero. No caso contrrio, se x for inicialmente maior ou igual a zero, a prxima instruo a executar a instruo na linha 4, passando a varivel x a ter o valor 1. Em qualquer dos casos, a execuo continua na linha 5 1 . Como a instruo na linha 2 s executada se x < 0, diz-se que x < 0 a sua guarda, normalmente representada por G. De igual modo, a guarda da instruo na linha 4 0 x. A guarda da instruo alternativa aps o else est implcita, podendo ser obtida por negao da guarda do if: (x < 0) equivalente a 0 x. Assim, numa instruo de seleco pode-se sempre inverter a ordem das instrues alternativas desde que se negue a condio. Assim,
if(x < 0) // G1 x < 0 x = 0; else // G2 0 x x = 1;

equivalente a
Se existirem instrues return, break, continue ou goto nas instrues controladas por um if, o uxo normal de execuo pode ser alterado de tal modo que a execuo no continua na instruo imediatamente aps esse if.
1

4.1. INSTRUES DE SELECO


if(0 <= x) // G2 0 x x = 1; else // G1 x < 0 x = 0;

131

O uxo de execuo de uma instruo de seleco genrica


if(C) // G1 C instruo1 else // G2 C instruo2

representado no diagrama de actividade da Figura 4.1. condio que garantidamente verdadeira {G1 } com G1 C instruo1 [C] [C] {G2 } com G2 C instruo2

Figura 4.1: Diagrama de actividade de uma instruo de seleco genrica. Uma condio que tem de ser sempre verdadeira coloca-se num comentrio entre chavetas. Em certos casos pretende-se apenas executar uma dada instruo se determinada condio se vericar, no se desejando fazer nada caso a condio seja falsa. Nestes casos pode-se omitir o else e a instruo que se lhe segue. Por exemplo, no troo de programa
if(x < 0) // 1 x = 0; // 2 ... // 3

132

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

se x for inicialmente menor do que zero, ento executada a instruo condicionada na linha 2, passando o valor de x a ser zero, sendo em seguida executada a instruo na linha 3. No caso contrrio a execuo passa directamente para a linha 3. A este tipo restrito de instruo de seleco tambm se chama instruo condicional. Uma instruo condicional sempre equivalente a uma instruo de seleco em que a instruo aps o else uma instruo nula (a instruo nula corresponde em C++ a um simples terminador ;). Ou seja, o exemplo anterior equivalente a:
if(x < 0) x = 0; else ; ... // // // // // 1 2a 2b 2c 3

O uxo de execuo de uma instruo de condicional genrica


if(C) instruo

representado no diagrama de actividade da Figura 4.2.

[C] [C] instruo

Figura 4.2: Diagrama de actividade de uma instruo condicional genrica. Em muitos casos necessrio executar condicional ou alternativamente no uma instruo simples mas sim uma sequncia de instrues. Para o conseguir, agrupam-se essas instrues num bloco de instrues ou instruo composta, colocando-as entre chavetas {}. Por exemplo, o cdigo que se segue ordena os valores guardados nas variveis x e y de tal modo que x termine com um valor menor ou igual ao de y:

4.1. INSTRUES DE SELECO


int x, y; ... if(y < x) { int const auxiliar = x; x = y; y = auxiliar; }

133

4.1.2 Instrues de seleco encadeadas


Caso seja necessrio, podem-se encadear instrues de seleco umas nas outras. Isso acontece quando se pretende seleccionar uma entre mais do que duas instrues alternativas. Por exemplo, para vericar qual a posio do valor de uma varivel a relativamente a um intervalo [mnimo mximo], pode-se usar
int a, mnimo, mximo; cin > > mnimo > > mximo > > a; if(a < mnimo) cout < < a < < " menor que mnimo." < < endl; else { if(a <= mximo) cout < < a < < " entre mnimo e mximo." < < endl; else cout < < a < < " maior que mximo." < < endl; }

Sendo as instrues de seleco instrues por si s, o cdigo acima pode-se escrever sem recurso s chavetas, ou seja,
int a, mnimo, mximo; cin > > mnimo > > mximo > > a; if(a < mnimo) cout < < a < < " menor que mnimo." < < endl; else if(a <= mximo) cout < < a < < " entre mnimo e mximo." < < endl; else cout < < a < < " maior que mximo." < < endl;

Em casos como este, em que se encadeiam if else sucessivos, comum usar uma indentao que deixa mais claro que existem mais do que duas alternativas,

134

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


int a, mnimo, mximo; cin > > mnimo > > mximo > > a; if(a < mnimo) cout < < a < < " menor que mnimo." < < endl; else if(a <= mximo) cout < < a < < " entre mnimo e mximo." < < endl; else cout < < a < < " maior que mximo." < < endl;

Guardas em instrues alternativas encadeadas Podem-se colocar como comentrios no exemplo anterior as guardas de cada instruo alternativa. Estas guardas reectem as circunstncias nas quais as instrues alternativas respectivas so executadas
int a, mnimo, mximo; cin > > mnimo > > mximo > > a; if(a < mnimo) // G1 a < mnimo. cout < < a < < " menor que mnimo." < < endl; else if(a <= mximo) // G2 mnimo a mximo. cout < < a < < " entre mnimo e mximo." < < endl; else // G3 mnimo a mximo < a. cout < < a < < " maior que mximo." < < endl;

Fica claro que as guardas no correspondem directamente condio indicada no if respectivo, com excepo da primeira. As n guardas de uma instruo de seleco, construda com n 1 instrues if encadeadas, podem ser obtidas a partir das condies de cada um dos if como se segue:
if(C1 ) // G1 C1 . ... else if(C2 ) // G2 G1 C2 (ou, C1 C2 ). ... else if(C3 ) // G3 G1 G2 C3 (ou, C1 C2 C3 ). ... ...

4.1. INSTRUES DE SELECO


else if(Cn1 ) // Gn1 G1 Gn2 Cn1 (ou, C1 Cn2 Cn1 ). ... else // Gn G1 Gn1 (ou, C1 Cn1 ). ...

135

Ou seja, as guardas das instrues alternativas so obtidas por conjuno da condio do if respectivo com a negao das guardas das instrues alternativas (ou das condies de todos os if) anteriores na sequncia, como seria de esperar. Uma instruo de seleco encadeada pois equivalente a uma instruo de seleco com mais do que duas instrues alternativas, como se mostra na Figura 4.3.

[G1 ] instruo1

[G2 ] instruo2 ...

[Gn1 ] instruon1

[Gn ] instruon

Figura 4.3: Diagrama de actividade de uma instruo de mltipla (sem correspondente directo na linguagem C++) equivalente a uma instruo de seleco encadeada. As guardas presumem-se mutuamente exclusivas.

Inuncia de pr-condies A ltima guarda do exemplo dado anteriormente parece ser redundante: se a maior que mximo no forosamente maior que mnimo? Ento porque no a guarda simplesmente mximo < a? Acontece que nada garante que mnimo mximo! Se se introduzir essa condio como pr-condio das instrues de seleco encadeadas, ento essa condio verica-se imediatamente antes de cada uma das instrues alternativas, pelo que as guardas podem ser simplicadas e tomar a sua forma mais intuitiva
// P C mnimo mximo if(a < mnimo)

136

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


// G1 a < mnimo. cout < < a < < " menor que mnimo." < < endl; else if(a <= mximo) // G2 mnimo a mximo. cout < < a < < " entre mnimo e mximo." < < endl; else // G3 mximo < a. cout < < a < < " maior que mximo." < < endl;

4.1.3 Problemas comuns


A sintaxe das instrues if e if else do C++ presta-se a ambiguidades. No cdigo
if(m == 0) if(n == 0) cout < < "m e n so zero." < < endl; else cout < < "m no zero." < < endl;

o else no diz respeito ao primeiro if. Ao contrrio do que a indentao do cdigo sugere, o else diz respeito ao segundo if. Em caso de dvida, um else pertence ao if mais prximo (e acima...) dentro mesmo bloco de instrues e que no tenha j o respectivo else. Para corrigir o exemplo anterior necessrio construir uma instruo composta, que neste caso consiste de uma nica instruo de seleco
if(m == 0) { if(n == 0) cout < < "m e n so zero." < < endl; } else cout < < "m no zero." < < endl;

conveniente usar blocos de instrues de uma forma liberal, pois construes como a que se apresentou podem dar origem a erros muito difceis de detectar e corrigir. Os compiladores de boa qualidade, no entanto, avisam o programador da presena de semelhantes (aparentes) ambiguidades. Um outro erro frequente corresponde a colocar um terminador ; logo aps a condio do if ou logo aps o else. Por exemplo, a inteno do programador do troo de cdigo
if(x < 0); x = 0;

era provavelmente que se mantivesse o valor de x excepto quando este fosse negativo. Mas a interpretao feita pelo compilador (e a correcta dada a sintaxe da linguagem C++)

4.2. ASSERES
if(x < 0) ; // instruo nula: no faz nada. x = 0;

137

ou seja, x terminar sempre com o valor zero! Este tipo de erro, no sendo muito comum, ainda mais difcil de detectar do que o da (suposta) ambiguidade da pertena de um else: os olhos do programador, habituados que esto presena de ; no nal de cada linha, recusam-se a detectar o erro.

4.2 Asseres
Antes de se passar ao desenvolvimento de instrues de seleco, importante fazer uma pequena digresso para introduzir um pouco mais formalmente o conceito de assero. Chama-se assero a um predicado (ver Seco A.1) escrito normalmente na forma de comentrio antes ou depois de uma instruo de um programa. As asseres correspondem a armaes acerca das variveis do programa que se sabe serem verdadeiras antes da instruo seguinte assero e depois da instruo anterior assero. Uma assero pode sempre ser vista como pr-condio P C da instruo seguinte e condio objectivo CO da instruo anterior. As asseres podem tambm incluir armaes acerca de variveis matemticas, que no pertencem ao programa. Nas asseres cada varivel pertence a um determinado conjunto. Para as variveis C++, esse conjunto determinado pelo tipo da varivel indicado na sua denio (e.g., int x; signica que x pertence ao conjunto dos inteiros entre 2 n1 e 2n1 1, se os int tiverem n bits). Para as variveis matemticas, esse conjunto deveria, em rigor, ser indicado explicitamente. Neste texto, no entanto, admite-se que as variveis matemticas pertencem ao conjunto dos inteiros, salvo onde for explicitamente indicado outro conjunto ou onde o conjunto seja fcil de inferir pelo contexto da assero. Nas asseres tambm normal assumir que as varivel C++ no tm limitaes (e.g., admite-se que uma varivel int pode guardar qualquer inteiro). Embora isso no seja rigoroso, permite resolver com maior facilidade um grande nmero de problemas sem que seja necessrio introduzir demasiados pormenores nas demonstraes. Em cada ponto de um programa existe um determinado conjunto de variveis, cada uma das quais pode tomar determinados valores, consoante o seu tipo. Ao conjunto de todos os possveis valores de todas as variveis existentes num dado ponto de um programa chama-se o espao de estados do programa nesse ponto. Ao conjunto dos valores das variveis existentes num determinado ponto do programa num determinado instante de tempo chama-se o estado de um programa. Assim, o estado de um programa um elemento do espao de estados. As asseres fazem armaes acerca do estado do programa num determinado ponto. Podem ser mais fortes, por exemplo se armarem que uma dada varivel toma o valor 1, ou mais fracas, por exemplo se armarem que uma dada varivel toma um valor positivo.

4.2.1 Deduo de asseres


Suponha-se o seguinte troo de programa

138
++n;

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

onde se admite que a varivel n tem inicialmente um valor no-negativo. Como adicionar asseres a este troo de programa? Em primeiro lugar escreve-se a assero que corresponde assuno de que n guarda inicialmente um valor no-negativo:
// 0 n ++n;

Como se pode vericar, as asseres so colocadas no cdigo na forma de comentrios. A assero que segue uma instruo, a sua condio objectivo, tipicamente obtida pela semntica da instruo e pela respectiva pr-condio. Ou seja, dada a P C, a instruo implica a veracidade da respectiva CO. No caso acima bvio que
// 0 n. ++n; // 1 n.

ou seja, se n era maior ou igual a zero antes da incrementao, depois da incrementao ser forosamente maior ou igual a um. A demonstrao informal da correco de um pedao de cdigo pode, portanto, ser feita recorrendo a asseres. Suponha-se que se pretende demonstrar que o cdigo
int const t = x; x = y; y = t;

troca os valores de duas variveis x e y do tipo int. Comea-se por escrever as duas principais asseres: a pr-condio e a condio objectivo da sequncia completa de instrues. Neste caso a escrita destas asseres complicada pelo facto de a CO ter de ser referir aos valores das variveis x e y antes das instrues. Para resolver este problema, considere-se que as variveis matemticas x e y representam os valores iniciais das variveis C++ x e y (repare-se bem na diferena de tipo de letra usado para variveis matemticas e para variveis do programa C++). Ento a CO pode ser escrita simplesmente como x=yy=x e a P C como x=xy=y donde o cdigo com as asseres iniciais e nais

4.2. ASSERES
// x = x y = y. int const t = x; x = y; y = t; // x = y y = x.

139

A demonstrao de correco pode ser feita deduzindo as asseres intermdias:


// x = x y = y. int const t = x; // x = x y = y t = x. x = y; // x = y y = y t = x. y = t; // x = y y = x t = x x = y y = x.

A demonstrao de correco pode ser feita tambm partindo dos objectivos. Para cada instruo, comeando na ltima, determina-se quais as condies mnimas a impor s variveis antes da instruo, i.e., determina-se qual a P C mais fraca a impor instruo em causa, de modo a que, depois dessa instruo, a sua CO seja verdadeira. Antes do o fazer, porm, necessrio introduzir mais alguns conceitos.

4.2.2 Predicados mais fortes e mais fracos


Diz-se que um predicado P mais fraco do que outro Q se Q implicar P , ou seja, se o conjunto dos valores que tornam o predicado P verdadeiro contm o conjunto dos valores que tornam o predicado Q verdadeiro. Por exemplo, se P 0 < x e Q x = 1, ento P mais fraco do que Q, pois o conjunto {1} est contido no conjunto dos positivos {x : 0 < x}. O mais fraco de todos os possveis predicados aquele que sempre verdadeiro, pois o conjunto dos valores que o vericam o conjunto universal. Logo, o mais fraco de todos os possveis predicados V. Por razes bvias, o mais forte de todos os possveis predicados F, sendo vazio o conjunto dos valores que o vericam.

4.2.3 Deduo da pr-condio mais fraca de uma atribuio


possvel estabelecer uma relao entre as asseres que possvel escrever antes e depois de uma instruo de atribuio. Ou seja, possvel relacionar duma forma algbrica P C e CO em
// P C x = expresso; // CO

A relao mais de estabelecer partindo da condio objectivo CO. que a P C mais fraca que, depois da atribuio, conduz CO, pode ser obtida substituindo por expresso todas

140

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

as ocorrncias da varivel x em CO. Esta deduo da P C mais fraca s pode ser feita se a expresso cujo valor se atribui a x no tiver efeitos laterais, i.e., se no implicar a alterao de nenhuma varivel do programa (ver Seco 2.7.8). Se a expresso tiver efeitos laterais mas apesar de tudo for bem comportada, possvel decomp-la numa sequncia de instrues e aplicar a deduo a cada uma delas. Por exemplo, suponha-se que se pretendia saber qual a P C mais fraca para a qual a instruo de atribuio x = -x; conduz CO 0 x. Aplicando o mtodo descrito conclui-se que
// 0 x, ou seja, x 0. x = -x; // 0 x.

Ou seja, para que a inverso do sinal de x conduza a um valor no-negativo, o menos que tem de se exigir que o valor de x seja inicialmente no-positivo. Voltando ao exemplo da troca de valores de duas variveis
int const t = x; x = y; y = t; // x = y y = x.

e aplicando a tcnica proposta obtm-se sucessivamente


int const t = x; x = y; // x = y t = x. y = t; // x = y y = x. int const t = x; // y = y t = x. x = y; // x = y t = x. y = t; // x = y y = x. // y = y x = x. int const t = x; // y = y t = x. x = y; // x = y t = x. y = t; // x = y y = x.

4.2. ASSERES
tendo-se recuperado a P C inicial.

141

Em geral este mtodo no conduz exactamente P C escrita inicialmente. Suponha-se que se pretendia demonstrar que
// x < 0. x = -x; // 0 x.

Usando o mtodo anterior conclui-se que:


// x < 0. // 0 x, ou seja, x 0. x = -x; // 0 x.

Mas como x < 0 implica que x 0, conclui-se que o cdigo est correcto.

Em geral, portanto, quando se escrevem duas asseres em sequncia, a primeira insero implica a segunda, ou seja,
// A1 . // A2 .

s pode acontecer se A1 A2 . Se as duas asseres surgirem separadas por uma instruo, ento se a primeira assero se vericar antes da instruo a segunda assero vericar-se- depois da instruo ser executada.

4.2.4 Asseres em instrues de seleco


Suponha-se de novo o troo de cdigo que pretende ordenar os valores das variveis x e y (que se presume serem do tipo int) de modo que x y,
if(y < x) { int const t = x; x = y; y = t; }

Qual a CO e qual a P C? Considerem-se x e y os valores das variveis x e y antes da instruo de seleco. Ento a P C e a CO podem ser escritas

142

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


// P C x = x y = y. if(y < x) { int const t = x; x = y; y = t; } // CO x y ((x = x y = y) (x = y y = x)).

Ou seja, o problema ca resolvido quando as variveis x e y mantm ou trocam os seus valores iniciais e x termina com um valor no-superior ao de y. Determinar uma CO no fcil. A P C e a CO, em conjunto, constituem a especicao formal do problema. A sua escrita obriga compreenso profunda do problema a resolver, e da a diculdade. Ser que a instruo condicional conduz forosamente da P C CO? necessrio demonstrlo. Partindo da pr-condio conveniente comear por converter a instruo condicional na instruo de seleco equivalente e, simultaneamente, explicitar as guardas das instrues alternativas:
// P C x = x y = y. if(y < x) { // G1 y < x. int const t = x; x = y; y = t; } else // G2 x y. ; // instruo nula!

A P C, sendo verdadeira antes da instruo de seleco, tambm o ser imediatamente antes de qualquer das instrues alternativas, pelo que se pode escrever
// P C x = x y = y. if(y < x) { // y < x x = x y = y. int const t = x; x = y; y = t; } else // x y x = x y = y. ; // instruo nula!

4.2. ASSERES
Pode-se agora deduzir as asseres vlidas aps cada instruo alternativa:
// P C x = x y = y. if(y < x) { // y < x x = x y = y. int const t = x; // y < x x = x y = y y < t t = x. x = y; // y = y y < t t = x x = y x < t. y = t; // t = x x = y x < t y = x x < y, que implica // x y x = y y = x. } else // x y x = x y = y. ; // instruo nula! // x y x = x y = y, j que a instruo nula no afecta asseres.

143

Conclui-se que, depois da troca de valores entre x e y na primeira das instrues alternativas, x < y, o que implica que x y. Eliminando as asseres intermdias, teis apenas durante a demonstrao,
// P C x = x y = y. if(y < x) { int const t = x; x = y; y = t; // x y x = y y = x. } else ; // instruo nula! // x y x = x y = y, j que a instruo nula no afecta asseres. // Que assero vlida aqui?

Falta agora deduzir a assero vlida depois da instruo de seleco completa. Esse ponto pode ser atingido depois de se ter passado por qualquer uma das instrues alternativas, pelo que uma assero que vlida certamente a disjuno das asseres deduzidas para cada uma das instrues alternativas:
// P C x = x y = y. if(y < x) { int const t = x; x = y; y = t; // x y x = y y = x. } else ; // instruo nula!

144

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


// x y x = x y = y, j que a instruo nula no afecta asseres. // (x y x = x y = y) (x y x = y y = x), ou seja // x y ((x = x y = y) (x = y y = x)).

que exactamente a CO que se pretendia demonstrar vlida. Partindo da condio objectivo Neste caso comea por se observar que a CO tem de ser vlida no nal de qualquer das instrues alternativas para que o possa ser no nal da instruo de seleco:
if(y < x) { int const t = x; x = y; y = t; // x y ((x = x y = y) (x = y y = x)). } else ; // instruo nula! // x y ((x = x y = y) (x = y y = x)). // CO x y ((x = x y = y) (x = y y = x)).

Depois vo-se determinando sucessivamente as pr-condies mais fracas de cada instruo (do m para o incio) usando as regras descritas acima para as atribuies:
if(y < x) { // y x ((y = x x = y) (y = y x = x)). int const t = x; // y t ((y = x t = y) (y = y t = x)). x = y; // x t ((x = x t = y) (x = y t = x)). y = t; // x y ((x = x y = y) (x = y y = x)). } else // x y ((x = x y = y) (x = y y = x)). ; // instruo nula! // x y ((x = x y = y) (x = y y = x)). // CO x y ((x = x y = y) (x = y y = x)).

Eliminando as asseres intermdias obtm-se:


if(y < x) { // y x ((y = x x = y) (y = y x = x)). int const t = x; x = y;

4.2. ASSERES
y = t; } else // x y ((x = x y = y) (x = y y = x)). ; // instruo nula! // CO x y ((x = x y = y) (x = y y = x)).

145

Basta agora vericar se a P C em conjuno com cada uma das guardas implica a respectiva assero deduzida. Ou seja, sabendo o que se sabe desde o incio, a pr-condio P C, e sabendo que a primeira instruo alternativa ser executada, e portanto a guarda G 1 , ser que a pr-condio mais fraca dessa instruo alternativa se verica? E o mesmo para a segunda instruo alternativa? Resumindo, tem de se vericar se: 1. P C G1 y x ((y = x x = y) (y = y x = x)) e 2. P C G2 x y ((x = x y = y) (x = y y = x)) so implicaes verdadeiras. fcil vericar que o so de facto. Resumo Em geral, para demonstrar a correco de uma instruo de seleco com n alternativas, ou seja, com n instrues alternativas
// P C if(C1 ) // G1 C1 . instruo1 else if(C2 ) // G2 G1 C2 (ou, C1 C2 ). instruo2 else if(C3 ) // G3 G1 G2 C3 (ou, C1 C2 C3 ). instruo3 ... else if(Cn1 ) // Gn1 G1 Gn2 Cn1 (ou, C1 Cn2 Cn1 ). instruon1 else // Gn G1 Gn1 (ou, C1 Cn1 ). instruon // CO

seguem-se os seguintes passos: Demonstrao directa Partindo da P C:

146

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


1. Para cada instruo alternativa instruoi (com i = 1 . . . n), deduz-se a respectiva COi admitindo que a pr-condio P C da instruo de seleco e a guarda G i da instruo alternativa so ambas verdadeiras. Ou seja, deduz-se CO i tal que:
// P C Gi instruoi // COi

2. Demonstra-se que (CO1 CO2 COn ) CO. Ou, o que o mesmo, que (CO1 CO) (CO2 CO) (COn CO). Demonstrao inversa Partindo da CO: 1. Para cada instruo alternativa instruoi (com i = 1 . . . n), determina-se a prcondio mais fraca P Ci que leva forosamente CO da instruo de seleco. Ou seja, determina-se a P Ci mais fraca tal que:
// P Ci instruoi // CO

2. Demonstra-se que P C Gi P Ci para i = 1 . . . n. No necessrio vericar se pelo menos uma guarda sempre verdadeira, porque, por construo da instruo de seleco, esta termina sempre com um else. Se isso no acontecer, necessrio fazer a demonstrao para a instruo de seleco equivalente em que todos os if tm o respectivo else, o que pode implicar introduzir uma instruo alternativa nula.

4.3 Desenvolvimento de instrues de seleco


As seces anteriores apresentaram a noo de assero e sua relao com as instrues de seleco. Falta agora ver como esse formalismo pode ser usado para desenvolver programas. Regresse-se ao problema inicial, da escrita de uma funo para calcular o valor absoluto de um valor inteiro. O objectivo , portanto, preencher o corpo da funo
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0 absoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { ... }

onde se usou o predicado V (verdadeiro) para indicar que a funo no tem pr-condio.

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO

147

Para simplicar o desenvolvimento, pode-se comear o corpo pela denio de uma varivel local para guardar o resultado e terminar com a devoluo do seu valor, o que permite escrever as asseres principais da funo em termos do valor desta varivel 2 :
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { // P C V (ou seja, sem restries). int r; ... // CO r = |x|, ou seja, 0 r (r = x r = x). assert(0 <= r and (r == -x or r == x)); return r; }

Antes de comear o desenvolvimento, necessrio perceber se para a resoluo do problema necessrio recorrer a uma instruo de seleco. Neste caso bvio que sim, pois tem de se discriminar entre valores negativos e positivos (e talvez nulos) de x. O desenvolvimento usado ser baseado nos objectivos. Este um princpio importante da programao [8] a programao deve ser orientada pelos objectivos. certo que a pr-condio afecta a soluo de qualquer problema, mas os problemas so essencialmente determinados pela condio objectivo. De resto, com se pode vericar depois de alguma prtica, mais inspirao para a resoluo de um problema pode ser obtida por anlise da condio objectivo do que por anlise da pr-condio. Por exemplo, comum no haver qualquer pr-condio na especicao de um problema, donde nesses casos s a condio objectivo poder ser usada como fonte de inspirao.

4.3.1 Escolha das instrues alternativas


O primeiro passo do desenvolvimento corresponde a identicar possveis instrues alternativas que paream poder levar veracidade da CO. fcil vericar que h duas possveis
2 A semntica de uma instruo de retorno muito semelhante de uma instruo de atribuio. A pr-condio mais fraca pode ser obtida por substituio, na CO da funo,do nome da funo pela expresso usada na instruo de retorno. Assim, a pr-condio mais fraca da instruo return r; que leva condio objectivo

CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). CO r = |x|, ou seja, 0 r (r = x r = x). No entanto, a instruo de retorno difere da instruo de atribuio pelo numa coisa: a instruo de retorno termina a execuo da funo.

148

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

instrues nessas condies: r = -x; e r = x;. Estas possveis instrues podem ser obtidas por simples observao da CO.

4.3.2 Determinao das pr-condies mais fracas


O segundo passo corresponde a vericar em que circunstncias estas instrues alternativas levam veracidade da CO. Ou seja, quais as pr-condies P C i mais fracas que garantem que, depois da respectiva instruo i, a condio CO verdadeira. Comece-se pela primeira instruo:
r = -x; // CO 0 r (r = x r = x).

Usando a regra da substituio discutida na Seco 4.2.3, chega-se a


// CO 0 x (x = x x = x), ou seja, x 0 (V x = 0), ou seja, x 0. r = -x; // CO 0 r (r = x r = x).

Logo, a primeira instruo, r = -x;, s conduz aos resultados pretendidos desde que x tenha inicialmente um valor no-positivo, i.e., P C 1 x 0. A mesma vericao pode ser feita para a segunda instruo

// CO 0 x (x = x x = x), ou seja, 0 x (x = 0 V), ou seja, 0 x. r = x; // CO 0 r (r = x r = x).

Logo, a segunda instruo, r = x;, s conduz aos resultados pretendidos desde que x tenha inicialmente um valor no-negativo, i.e., P C 2 0 x. O corpo da funo pode-se agora escrever

/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { // P C V (ou seja, sem restries). int r; if(C1 ) // G1 // P C1 x 0. r = -x;

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO


else // G2 // P C2 0 x. r = x; // CO r = |x|, ou seja, 0 r (r = x r = x). assert(0 <= r and (r == -x or r == x)); return r; }

149

4.3.3 Determinao das guardas


O terceiro passo corresponde a determinar as guardas G i de cada uma das instrues alternativas. De acordo com o que se viu anteriormente, para que a instruo de seleco resolva o problema, necessrio que P C Gi P Ci . S assim se garante que, sendo a guarda G i verdadeira, a instruo alternativa i conduz condio objectivo desejada. Neste caso P C sempre V, pelo que dizer que P C Gi P Ci o mesmo que dizer que Gi P Ci , e portanto a forma mais simples de escolher as guardas fazer simplesmente G i = P Ci . Ou seja,
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { // P C V (ou seja, sem restries). int r; if(C1 ) // G1 x 0. r = -x; else // G2 0eqx. r = x; // CO r = |x|, ou seja, 0eqr (r = x r = x). assert(0 <= r and (r == -x or r == x)); return r; }

4.3.4 Vericao das guardas


O quarto passo corresponde a vericar se a pr-condio P C da instruo de seleco implica a veracidade de pelo menos uma das guardas G i das instrues alternativas. Se isso no acontecer, signica que pode haver casos para os quais nenhuma das guardas seja verdadeira. Se

150

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

isso acontecer o problema ainda no est resolvido, sendo necessrio determinar instrues alternativas adicionais e respectivas guardas at todos os possveis casos estarem cobertos. Neste caso a P C no impe qualquer restrio, ou seja P C V. Logo, tem de se vericar se V (G1 G2 ). Neste caso G1 G2 x 0 0 x, ou seja, V. Como V V, o problema est quase resolvido.

4.3.5 Escolha das condies


No quinto e ltimo passo determinam-se as condies das instrues de seleco encadeadas de modo a obter as guardas entretanto determinadas. De acordo com o que se viu na Seco 4.2.4, G1 = C1 , pelo que a funo ca
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { // P C V (ou seja, sem restries). int r; if(x <= 0) // G1 x 0. r = -x; else // G2 0 < x. r = x; // CO r = |x|, ou seja, 0eqr (r = x r = x). assert(0 <= r and (r == -x or r == x)); return r; }

A segunda guarda foi alterada, pois de acordo com a Seco 4.2.4 G2 = G1 = 0 < x. Esta guarda mais forte que a guarda originalmente determinada, pelo que a alterao no traz qualquer problema. Na realidade o que aconteceu foi que a semntica da instruo if do C++ forou escolha de qual das instrues alternativas lida com o caso x = 0. Finalmente podem-se eliminar as asseres intermdias:
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x)

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO


{ int r; if(x <= 0) // G1 x 0. r = -x; else // G2 0 < x. r = x; assert(0 <= r and (r == -x or r == x)); return r; }

151

4.3.6 Alterando a soluo


A soluo obtida pode ser simplicada se se observar que, depois de terminada a instruo de seleco, a funo se limita a devolver o valor guardado em r por uma das duas instrues alternativas: possvel eliminar essa varivel e devolver imediatamente o valor apropriado:
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { if(x <= 0) // G1 x 0. return -x; else // G2 0 < x. return x; }

Por outro lado, se a primeira instruo alternativa (imediatamente abaixo do if) termina com uma instruo return, ento no necessria uma instruo de seleco, bastando uma instruo condicional:
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { if(x <= 0)

152
return -x; return x; }

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Este ltimo formato no forosamente mais claro ou prefervel ao anterior, mas muito comum encontr-lo em programas escritos em C++. uma expresso idiomtica do C++. Uma desvantagem destes formatos que no permitem usar instrues de assero para vericar a validade da condio objectivo.

4.3.7 Metodologia
Existem (pelo menos) dois mtodos semi-formais para o desenvolvimento de instrues de seleco. O primeiro foi usado para o desenvolvimento na seco anterior, e parte dos objectivos: 1. Determina-se n instrues instruoi , com i = 1 . . . n, que paream poder levar veracidade da CO. Tipicamente estas instrues so obtidas por anlise da CO. 2. Determina-se as pr-condies mais fracas P C i de cada uma dessas instrues de modo que conduzam CO. 3. Determina-se uma guarda Gi para cada alternativa i de modo que P C G i P Ci . Se o problema no tiver pr-condio (ou melhor, se P C V), ento pode-se fazer G i = P Ci . Note-se que uma guarda Gi = F resolve sempre o problema, embora seja to forte que nunca leve execuo da instruo respectiva! Por isso deve-se sempre escolher as guardas o mais fracas possvel3 . 4. Verica-se se, em todas as circunstncias, pelo menos uma das guardas encontradas verdadeira. Isto , verica-se se P C (G 1 Gn ). Se isso no acontecer, necessrio acrescentar mais instrues alternativas s encontradas no ponto 1. Para o fazer, identica-se que casos caram por cobrir analisando as guardas j encontradas e a P C. No segundo mtodo tenta-se primeiro determinar as guardas e s depois se desenvolve as respectivas instrues alternativas: 1. Determina-se os n casos possveis interessantes, restritos queles que vericam a P C, que cobrem todas as hipteses. Cada caso possvel corresponde a uma guarda G i . A vericao da P C tem de implicar a vericao de pelo menos uma das guardas, ou seja, P C (G1 Gn ). 2. Para cada alternativa i, encontra-se uma instruo instruoi tal que
// P C Gi instruoi // COi // CO
Excepto, naturalmente, quando se reconhece que as guardas se sobrepem. Nesse caso pode ser vantajoso fortalecer as guardas de modo a minimizar as sobreposies, desde que isso no conduza a guardas mais complicadas: a simplicidade uma enorme vantagem.
3

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO

153

ou seja, tal que, se a pr-condio P C e a guarda G i forem verdadeiras, ento a instruo leve forosamente veracidade da condio objectivo CO. Em qualquer dos casos, depois de encontradas as guardas e as respectivas instrues alternativas, necessrio escrever a instruo de seleco com o nmero de instrues if apropriado (para n instrues alternativas so necessrias n 1 instrues if encadeadas) e escolher as condies Ci apropriadas de modo a obter as guardas pretendidas. Pode-se comear por fazer Ci = Gi , com i = 1 n 1, e depois identicar sobreposies e simplicar as condies C i . Muitas vezes as guardas encontrada contm sobreposies (e.g., 0 x e x 0 sobrepem-se no valor 0) que podem ser eliminadas ao escolher as condies de cada if.

4.3.8 Discusso
As vantagens das metodologias informais apresentadas face a abordagens mais ou menos adhoc do desenvolvimento so pelo menos: 1. Foram especicao rigorosa do problema, e portanto sua compreenso profunda. 2. O desenvolvimento acompanhado da demonstrao de correco, o que reduz consideravelmente a probabilidade de erros. 3. No so descurados aparentes pormenores que, na realidade, podem dar origem a erros graves e difceis de corrigir. claro que a experincia do programador e a sua maior ou menor inventiva muitas vezes levem a desenvolvimentos ao sabor da pena. No forosamente errado, desde que feito em conscincia. Recomenda-se, nesses casos, que se tente fazer a posteriori uma demonstrao de correco (e no um teste!) para garantir que a inspirao funcionou... Para se vericar na prtica a importncia da especicao cuidada do problema, tente-se resolver o seguinte problema: Escreva uma funo que, dados dois argumentos do tipo int, devolva o booleano verdadeiro se o primeiro for mltiplo do segundo e falso no caso contrrio. Neste ponto o leitor deve parar de ler e tentar resolver o problema. . . . . . . A abordagem tpica do programador considerar que um inteiro n mltiplo de outro inteiro m se o resto da diviso de n por m for zero, e passar directo ao cdigo:
bool Mltiplo(int const m, int const n) { if(m % n == 0)

154
return true; else return false; }

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Implementada a funo, o programador fornece-a sua equipa para utilizao. Depois de algumas semanas de utilizao, o programa em desenvolvimento, na fase de testes, aborta com uma mensagem (ambiente Linux):
Floating exception (core dumped)

Depois de umas horas no depurador, conclui-se que, se o segundo argumento da funo for zero, a funo tenta uma diviso por zero, com a consequente paragem do programa. Neste ponto o leitor deve parar de ler e tentar resolver o problema. . . . . . . Chamado o programador original da funo, este observa que no h mltiplos de zero, e portanto tipicamente corrige a funo para
bool Mltiplo(int const m, int const n) { if(n != 0) if(m % n == 0) return true; else return false; else return false; }

Corrigida a funo e integrada de novo no programa em desenvolvimento, e repetidos os testes, no se encontra qualquer erro e o desenvolvimento continua. Finalmente o programa fornecido ao cliente. O cliente utiliza o programa durante meses at que detecta um problema estranho, incompreensvel. Como tem um contrato de manuteno com a empresa que desenvolveu o programa, comunica-lhes o problema. A equipa de manuteno, da qual o programador original no faz parte, depois de algumas horas de execuo do programa em modo depurao e de tentar reproduzir as condies em que o erro ocorre (que lhe foram fornecidas de uma forma parcelar pelo cliente), acaba por detectar o problema: a funo devolve false quando lhe so passados dois argumentos zero! Mas zero mltiplo de zero! Rogando pragas ao programador original da funo, esta corrigida para:

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO


bool Mltiplo(int const m, int const n) { if(n != 0) if(m % n == 0) return true; else return false; else if(m == 0) return true; else return false; }

155

O cdigo testado e verica-se que os erros esto corrigidos. Depois, o programador olha para o cdigo e acha-o demasiado complicado: para qu as instrues de seleco se uma simples instruo de retorno basta? Neste ponto o leitor deve parar de ler e tentar resolver o problema com uma nica instruo de retorno. . . . . . . Basta devolver o resultado de uma expresso booleana que reicta os casos em que m mltiplo de n:
bool Mltiplo(int const m, int const n) { return (m % n == 0 and n != 0) or (m == 0 and n == 0); }

Como a alterao meramente cosmtica, o programador no volta a testar e o programa corrigido fornecido ao cliente. Poucas horas depois de ser posto ao servio o programa aborta. O cliente recorre de novo aos servios de manuteno, desta vez furioso. A equipa de manuteno verica rapidamente que a execuo da funo leva a uma diviso por zero como originalmente. Desta vez a correco do problema simples: basta inverter os operandos da primeira conjuno:
bool Mltiplo(int const m, int const n) { return (n != 0 and m % n == 0) or (m == 0 and n == 0); }

156

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Esta simples troca corrige o problema porque nos operadores lgicos o clculo atalhado, i.e., o operando esquerdo calculado em primeiro lugar e, se o resultado car imediatamente determinado (ou seja, se o primeiro operando numa conjuno for falso ou se o primeiro operando numa disjuno for verdadeiro) o operando direito no chega a ser calculado (ver Seco 2.7.3). A moral desta estria que quanto mais depressa, mais devagar... O programador deve evitar as solues rpidas, pouco pensadas e menos vericadas. Ah! E faltou dizer que h uma soluo ainda mais simples:
bool Mltiplo(int const m, int const n) { return (n != 0 and m % n == 0) or m == 0; }

Talvez tivesse sido boa ideia ter comeado por especicar a funo. Se tal tivesse acontecido ter-se-ia evitado os erros e ter-se-ia chagado imediatamente soluo mais simples...

4.3.9 Outro exemplo de desenvolvimento


Apresenta-se brevemente o desenvolvimento de uma outra instruo alternativa, um pouco mais complexa. Neste caso pretende-se escrever um procedimento de interface
void limita(int& x, int const mn, int const mx)

que limite o valor de x ao intervalo [mn, mx], onde se assume que mn mx. I.e., se o valor de x for inferior a mn, ento deve ser alterado para mn, se for superior a mx, deve ser alterado para mx, e se pertencer ao intervalo, deve ser deixado com o valor original. Como habitualmente, comea por se escrever a especicao do procedimento, onde x uma varivel matemtica usada para representar o valor inicial da varivel de programa x:
/** Limita x ao intervalo [mn, mx]. @pre P C mn mx x = x. @post CO (x = mn x mn) (x = mx mx x) (x = x mn x mx). */ void limita(int& x, int const mn, int const mx) { }

A observao da condio objectivo conduz imediatamente s seguintes possveis instrues alternativas: 1. ; // para manter x com o valor inicial x. 2. x = mn;

4.3. DESENVOLVIMENTO DE INSTRUES DE SELECO


3. x = mx;

157

As pr-condies mais fracas para que se verique a condio objectivo do procedimento depois de cada uma das instrues so P C1 (x = x mn x mx) (x = mn x mn) (x = mx mx x) (mn = x mn x mx) x mn (mn = mx mx x) P C2 (mn = x mn x mx) (mn = mn x mn) (mn = mx mx x) P C3 (mx = x mn x mx) (mx = mn x mn) (mx = mx mx x) (mx = x mn x mx) (mx = mn x mn) mx x A determinao das guardas faz-se de modo a que P C G i P Ci .

No primeiro caso tem-se da pr-condio P C que x = x, pelo que a guarda mais fraca G1 (mn x x mx) x = mn x = mx mn x x mx

pois os casos x = mn e x = mx so cobertos pela primeira conjuno dado que a pr-condio P C garante que mn mx. No segundo caso, pelas mesmas razes, tem-se que

G2 (mn = x mn x mx) x mn (mn = mx mx x) mn = x x mn (mn = mx mx x) x mn (mn = mx mx x)

pois da pr-condio P C sabe-se que mn mx, logo se x = mn tambm ser x mx. Da mesma forma se obtm G3 (mx = mn x mn) mx x evidente que h sempre pelo menos uma destas guardas vlidas quaisquer que sejam os valores dos argumentos vericando a P C. Logo, no so necessrias quaisquer outras instrues alternativas. Ou seja, a estrutura da instruo de seleco (trocando a ordem das instrues de modo a que a instruo nula que em ltimo lugar):
if(C1 ) // G2 x mn (mn = mx mx x). x = mn; else if(C2 ) // G3 (mx = mn x mn) mx x. x = mx; else // G1 mn x x mx. ;

158

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

A observao das guardas demonstra que elas so redundantes para algumas combinaes de valores. Por exemplo, se mn = mx qualquer das guardas G 2 e G3 se verica. fcil vericar, portanto, que as guardas podem ser reforadas para:
if(C1 ) // G2 x mn. x = mn; else if(C2 ) // G3 mx x. x = mx; else // G1 mn x x mx. ;

de modo a que, caso mn = mx, seja vlida apenas uma das guardas (excepto, claro, quando x = mn = mx, em que se vericam de novo as duas guardas). De igual modo se podem eliminar as redundncias no caso de x ter como valor um dos extremos do intervalo, pois nesse caso a guarda G1 d conta do recado:
if(C1 ) // G2 x < mn. x = mn; else if(C2 ) // G3 mx < x. x = mx; else // G1 mn x x mx. ;

As condies das instrues if so:


if(x < mn) // G2 x < mn. x = mn; else if(mx < x) // G3 mx < x. x = mx; else // G1 mn x x mx. ;

Finalmente, pode-se eliminar o ltimo else, pelo que o procedimento ca, j equipado com as instrues de assero:

4.4. VARIANTES DAS INSTRUES DE SELECO


/** Limita x ao intervalo [mn, mx]. @pre P C mn mx x = x. @post CO (x = mn x mn) (x = mx mx x) (x = x mn x mx). */ void limita(int& x, int const mn, int const mx) { assert(mn <= mx); if(x < mn) x = mn; else if(mx < x) x = mx; assert(mn <= x and x <= mx); }

159

4.4 Variantes das instrues de seleco


4.4.1 O operador ? :
Seja a funo que calcula o valor absoluto de um nmero desenvolvida nas seces anteriores:
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) { if(x <= 0) return -x; return x; }

Ser possvel simplic-la mais? Sim. A linguagem C++ fornece um operador ternrio (com trs operandos), que permite escrever a soluo como 4
/** Devolve o valor absoluto do argumento. @pre P C V (ou seja, sem restries). @post CO absoluto = |x|, ou seja, 0eqabsoluto (absoluto = x absoluto = x). */ int absoluto(int const x) {
O leitor mais atento notou que se alterou a instruo que lida com o caso em que x = 0. que trocar o sinal de zero uma simples perda de tempo...
4

160

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


return x < 0 ? -x : x; }

Este operador ? : , tambm conhecido por se aritmtico, tem a seguinte sintaxe:


condio ? expresso1 : expresso2

O resultado do operador o resultado da expresso expresso1, se condio for verdadeira (nesse caso expresso2 no chega a ser calculada), ou o resultado da expresso expresso2, se condio for falsa (nesse caso expresso1 no chega a ser calculada).

4.4.2 A instruo switch


Suponha-se que se pretende escrever um procedimento que, dado um inteiro entre 0 e 9, o escreve no ecr por extenso e em portugus (escrevendo erro se o inteiro for invlido). Os nomes dos dgitos decimais em portugus, como em qualquer outra lngua natural, no obedecem a qualquer regra lgica. Assim, o procedimento ter de lidar com cada um dos 10 casos separadamente. Usando a instruo de seleco encadeada:
/** Escreve no ecr, por extenso, um dgito inteiro entre 0 e 9. @pre P C 0 dgito < 10. @post CO ecr contm, para alm do que continha originalmente, o dgito dado pelo inteiro dgito. */ void escreveDgitoPorExtenso(int const dgito) { assert(0 <= dgito and dgito < 10); if(dgito == 0) cout < < "zero"; else if(dgito == 1) cout < < "um"; else if(dgito == 2) cout < < "dois"; else if(dgito == 3) cout < < "trs"; else if(dgito == 4) cout < < "quatro"; else if(dgito == 5) cout < < "cinco"; else if(dgito == 6) cout < < "seis"; else if(dgito == 7) cout < < "sete"; else if(dgito == 8) cout < < "oito";

4.4. VARIANTES DAS INSTRUES DE SELECO


else cout < < "nove"; }

161

Existe uma soluo mais usual para este problema e que faz uso da instruo de seleco switch. Quando necessrio comparar uma varivel com um nmero discreto de diferentes valores, e executar uma aco diferente em cada um dos casos, deve-se usar esta instruo. Esta instruo permite claricar a soluo do problema apresentado:
/** Escreve no ecr, por extenso, um dgito inteiro entre 0 e 9. @pre P C 0 dgito < 10. @post CO ecr contm, para alm do que continha originalmente, o dgito dado pelo inteiro dgito. */ void escreveDgitoPorExtenso(int const dgito) { assert(0 <= dgito and dgito < 10); switch(dgito) { case 0: cout < < "zero"; break; case 1: cout < < "um"; break; case 2: cout < < "dois"; break; case 3: cout < < "trs"; break; case 4: cout < < "quatro"; break; case 5: cout < < "cinco"; break; case 6: cout < < "seis"; break;

162

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


case 7: cout < < "sete"; break; case 8: cout < < "oito"; break; case 9: cout < < "nove"; }

Esta instruo no permite a especicao de gamas de valores nem de desigualdades: construes como case 1..10: ou case < 10: so invlidas. Assim, possvel usar como expresso de controlo do switch (i.e., a expresso que se coloca entre parnteses aps a palavrachave switch) apenas expresses de um dos tipos inteiros ou de um tipo enumerado (ver Captulo 6), devendo as constantes colocadas nos casos a diferenciar ser do mesmo tipo. possvel agrupar vrios casos ou alternativas:
switch(valor) { case 1: case 2: case 3: cout < < "1, 2 ou 3"; break; ... }

Isto acontece porque a construo case n: apenas indica qual o ponto de entrada nas instrues que compem o switch quando a sua expresso de controlo tem valor n. A execuo do corpo do switch (o bloco de instrues entre {}) s termina quando for atingida a chaveta nal ou quando for executada uma instruo de break. Terminado o switch, a execuo continua sequencialmente aps a chaveta nal. A consequncia deste agulhamento do uxo de instruo que, se no exemplo anterior se eliminarem os break
/** Escreve no ecr, por extenso, um dgito inteiro entre 0 e 9. @pre P C 0 dgito < 10. @post CO ecr contm, para alm do que continha originalmente, o dgito dado pelo inteiro dgito. */ void escreveDgitoPorExtenso(int const dgito) { // Cdigo errado! switch(dgito) { case 0: cout < < "zero";

4.4. VARIANTES DAS INSTRUES DE SELECO

163

case 1: cout < < "um"; case 2: cout < < "dois"; case 3: cout < < "trs"; case 4: cout < < "quatro"; case 5: cout < < "cinco"; case 6: cout < < "seis"; case 7: cout < < "sete"; case 8: cout < < "oito"; case 9: cout < < "nove"; }

uma chamada escreveDgitoPorExtenso(7) resulta em:


seteoitonove

que no de todo o que se pretendia! Pelas razes que se indicaram atrs, no possvel usar a instruo switch (pelo menos de uma forma elegante) como alternativa s instrues de seleco em:
/** Escreve no ecr a ordem de grandeza de um valor. @pre P C 0 v < 10000. @post CO ecr contm, para alm do que continha originalmente, a ordem de grandeza de v. */ void escreveGrandeza(double v) { assert(0 <= v and v < 10000);

164
if(v < 10) cout < < else if(v < cout < < else if(v < cout < < else cout < < }

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

"unidades"; 100) "dezenas"; 1000) "centenas"; "milhares";

A ordem pela qual se faz as comparaes em instrues de seleco encadeadas pode ser muito relevante em termos do tempo de execuo do programa, embora seja irrelevante no que diz respeito aos resultados. Suponha-se que os valores passados como argumento a escreveGrandeza() so equiprovveis, i.e., to provvel ser passado um nmero como qualquer outro. Nesse caso consegue-se demonstrar que, em mdia, so necessrias 2,989 comparaes ao executar o procedimento e que, invertendo a ordem das instrues alternativas para
/** Escreve no ecr a ordem de grandeza de um valor. @pre P C 0 v < 10000. @post CO ecr contm, para alm do que continha originalmente, a ordem de grandeza de v. */ void escreveGrandeza(double v) { assert(0 <= v and v < 10000);

if(1000 <= x) cout < < "milhares"; else if(100 <= x) cout < < "centenas"; else if(10 <= x) cout < < "dezenas"; else cout < < "unidades";
}

so necessrias 1,11 comparaes5 ! No caso da instruo de seleco switch, desde que todos os conjuntos de instrues tenham o respectivo break, a ordem dos casos irrelevante.

4.5 Instrues de iterao


Na maioria dos programas h conjuntos de operaes que necessrio repetir vrias vezes. Para controlar o uxo do programa de modo a que um conjunto de instrues sejam repetidos
5

Demonstre-o!

4.5. INSTRUES DE ITERAO

165

condicionalmente (em ciclos) usam-se as instrues de iterao ou repetio, que sero estudadas at ao nal deste captulo. O conhecimento da sintaxe e da semntica das instrues de iterao do C++ no suciente para o desenvolvimento de bom cdigo que as utilize. Por um lado, o desenvolvimento de bom cdigo obriga demonstrao formal ou informal do seu correcto funcionamento. Por outro lado, o desenvolvimento de ciclos no uma tarefa fcil, sobretudo para o principiante. Assim, nas prximas seces apresenta-se um mtodo de demonstrao da correco de ciclos e faz-se uma pequena introduo metodologia de Dijkstra, que fundamenta e disciplina a construo de ciclos.

4.5.1 A instruo de iterao while


O while a mais importante das instrues de iterao. A sua sintaxe simples:
while(expresso_booleana) instruo_controlada

A execuo da instruo while consiste na execuo repetida da instruo instruo_controlada enquanto expresso_booleana tiver o valor lgico verdadeiro. A expresso_booleana sempre calculada antes da instruo instruo_controlada. Assim, se a expresso for inicialmente falsa, a instruo instruo_controlada no chega a ser executada, passando o uxo de execuo para a instruo subsequente ao while. A instruo instruo_controlada pode consistir em qualquer instruo do C++, e.g., um bloco de instrues ou um outro ciclo. expresso booleana de controlo do while chama-se a guarda do ciclo (representada muitas vezes por G), enquanto instruo controlada se chama passo. Assim, o passo repetido enquanto a guarda for verdadeira. Ou seja
while(G) passo

A execuo da instruo de iterao while pode ser representada pelo diagrama de actividade na Figura 4.4. Exemplo O procedimento que se segue escreve no ecr uma linha com todos os nmeros inteiros de zero a n (exclusive), sendo n o seu nico parmetro (no se colocaram instrues de assero para simplicar):
/** Escreve inteiros de 0 a n (exclusive) no ecr. @pre P C 0 n. @post CO ecr contm, para alm do que continha originalmente, os inteiros entre 0 e n exclusive, em representao decimal. */

166

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

[G] [G] passo

Figura 4.4: Diagrama de actividade da instruo de iterao while.


void escreveInteiros(int const n) // 1 { // 2 int i = 0; // 3 while(i != n) { // 4 cout < < i < < ; // 5 ++i; // 6 } // 7 cout < < endl; // 8 } // 9

O diagrama de actividade correspondente encontra-se na Figura 4.5. Seguindo o diagrama, comea por se construir a varivel i com valor inicial 0 (linha 3) e em seguida executa-se o ciclo while que: 1. Verica se a guarda i = n verdadeira (linha 4). 2. Se a guarda for verdadeira, executa as instrues nas linhas 5 e 6, voltando depois a vericar a guarda (volta a 1.). 3. Se a guarda for falsa, o ciclo termina. Depois de terminado o ciclo, escreve-se um m-de-linha (linha 8) e o procedimento termina.

4.5.2 Variantes do ciclo while: for e do while


A maior parte dos ciclos usando a instruo while tm a forma

4.5. INSTRUES DE ITERAO

167

int i = 0;

[i = n] [i = n] cout << i << ;

++i

cout << endl;

Figura 4.5: Diagrama de actividade do procedimento escreveInteiros().

168
inic while(G) { aco prog }

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

onde inic so as instrues de inicializao que preparam as variveis envolvidas no ciclo, prog so instrues correspondentes ao chamado progresso do ciclo que garantem que o ciclo termina em tempo nito, e aco so instrues correspondentes chamada aco do ciclo. Nestes ciclos, portanto, o passo subdivide-se em aco e progresso, ver Figura 4.6.

inic

[G] [G] aco

prog

Figura 4.6: Diagrama de actividade de um ciclo tpico. Existe uma outra instruo de iterao, a instruo for, que permite abreviar este tipo de ciclos:
for(inic; G; prog) aco

4.5. INSTRUES DE ITERAO

169

em que inic e prog tm de ser expresses, embora inic possa denir tambm uma varivel locais. O procedimento escreveInteiros() j desenvolvido pode ser reescrito como
/** Escreve inteiros de 0 a n (exclusive) no ecr. @pre P C 0 n. @post CO ecr contm, para alm do que continha originalmente, os inteiros entre 0 e n exclusive, em representao decimal. */ void escreveInteiros(int const n) // 1 { // 2 for(int i = 0; i != n; ++i) // 3 cout < < i < < ; // 4 cout < < endl; // 5 } // 6

Ao converter o while num for aproveitou-se para restringir ainda mais a visibilidade da varivel i. Esta varivel, estando denida no cabealho do for, s visvel nessa instruo (linhas 3 e 4). Relembra-se que de toda a convenincia que a visibilidade das variveis seja sempre o mais restrita possvel zona onde de facto so necessrias. Qualquer das expresses no cabealho de uma instruo for (inic, G e prog) pode ser omitida. A omisso da guarda G equivalente utilizao de uma guarda sempre verdadeira, gerando por isso um ciclo innito, ou lao (loop). Ou seja, escrever
for(inic; ; prog) aco

o mesmo que escrever


for(inic; true; prog) aco

ou mesmo
inic; while(true) { aco prog }

cujo diagrama de actividade se v na Figura 4.7. No entanto, os ciclos innitos s so verdadeiramente teis se o seu passo contiver alguma instruo return ou break (ver Seco 4.5.4) que obrigue o ciclo a terminar alguma vez (nesse caso, naturalmente, deixam de ser innitos...). Um outro tipo de ciclo corresponde instruo do while. A sintaxe desta instruo :

170

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

inic

aco

prog

Figura 4.7: Diagrama de actividade de um lao.


do instruo_controlada while(expresso_booleana);

A execuo da instruo do while consiste na execuo repetida de instruo_controlada enquanto expresso_booleana tiver o valor verdadeiro. Mas, ao contrrio do que se passa no caso da instruo while, expresso_booleana sempre calculada e vericada depois da instruo instruo_controlada. Quando o resultado falso o uxo de execuo passa para a instruo subsequente ao do while. Note-se que instruo_controlada pode consistir em qualquer instruo do C++, e.g., um bloco de instrues. Tal como no caso da instruo while, instruo controlada pela instruo do while chamase passo e condio que a controla chama-se guarda. A execuo da instruo de iterao do while pode ser representada pelo diagrama de actividade na Figura 4.8. Ao contrrio do que se passa com a instruo while, durante a execuo de uma instruo do while o passo executado sempre pelo menos uma vez. Exemplo com for O ciclo for usado frequentemente para repeties ou contagens simples. Por exemplo, se se pretender escrever no ecr os inteiros de 0 a 9 (um por linha), pode-se usar o seguinte cdigo:
for(int i = 0; i != 10; ++i) cout < < i < < endl;

4.5. INSTRUES DE ITERAO

171

passo

[G] [G]

Figura 4.8: Diagrama de actividade da instruo de iterao do while. Se se pretender escrever no ecr uma linha com 10 asteriscos, pode-se usar o seguinte cdigo: for(int i = 0; i != 10; ++i) cout < < *; cout < < endl; // para terminar a linha. importante observar que, em C++, tpico comear as contagens em zero e usar como guarda o total de repeties a efectuar. Nos ciclos acima a varivel i toma todos os valores entre 0 e 10 inclusive, embora para i = 10 a aco do ciclo no chegue a ser executada. Alterando o primeiro ciclo de modo a que a denio da varivel i seja feita fora do ciclo e o seu valor no nal do ciclo mostrado no ecr
int i = 0; for(; i != 10; ++i) cout < < i < < endl; cout < < "O valor final " < < i < < . < < endl;

ento a execuo deste troo de cdigo resultaria em


0 1 2 3 4 5

172
6 7 8 9 O valor final 10.

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Exemplo com do while muito comum usar-se o ciclo do while quando se pretende validar uma entrada de dados por um utilizador do programa. Suponha-se que se pretende que o utilizador introduza um nmero inteiro entre 0 e 100 inclusive). Se se pretender obrig-lo repetio da entrada de dados at que introduza um valor nas condies indicadas, pode-se usar o seguinte cdigo:
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { cout < < "Introduza valor (0 a 100): "; cin > > n; while(n < 0 or 100 < n) { cout < < "Valor incorrecto! < < endl; cout < < "Introduza valor (0 a 100): "; cin > > n; } assert(0 <= n and n <= 100); }

A instruo cin > > n e o pedido de um valor aparecem duas vezes, o que no parece uma soluo muito elegante. Isto deve-se a que, antes de entrar no ciclo para a primeira iterao, tem de fazer sentido vericar se n est ou no dentro dos limites impostos, ou seja, a varivel n tem de ter um valor que no seja arbitrrio. A alternativa usando o ciclo do while seria:
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { do { cout < < "Introduza valor (0 a 100): "; cin > > n; if(n < 0 or 100 < n)

4.5. INSTRUES DE ITERAO


cout < < "Valor incorrecto!" < < endl; } while(n < 0 or 100 < n); assert(0 <= n and n <= 100); }

173

Este ciclo executa o bloco de instrues controlado pelo menos uma vez, dado que a guarda s avaliada depois da primeira execuo. Assim, s necessria uma instruo cin > > n e um pedido de valor. A contrapartida a necessidade da instruo alternativa if dentro do ciclo, com a consequente repetio da guarda... Mais tarde se vero formas alternativas de escrever este ciclo. Em geral os ciclos while e for so sucientes, sendo muito raras as ocasies em que a utilizao do ciclo do while resulta em cdigo realmente mais claro. No entanto, m ideia resolver o problema atribuindo um valor inicial varivel n que garanta que a guarda inicialmente verdadeira, de modo a conseguir utilizar o ciclo while:
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { // Truque sujo: inicializao para a guarda ser inicialmente verdadeira. M ideia! n = -1; while(n < 0 or 100 < n) { cout < < "Introduza valor (0 a 100): "; cin > > n; if(n < 0 or 100 < n) cout < < "Valor incorrecto!" < < endl; } assert(0 <= n and n <= 100); }

Quando se tiver de inicializar uma varivel de modo a que o passo de um ciclo seja executado pelo menos uma vez, melhor recorrer a um ciclo do while. Equivalncias entre instrues de iterao sempre possvel converter um ciclo de modo a usar qualquer das instrues de iterao, como indicado na Tabela 4.1. No entanto, a maior parte dos problemas resolvem-se de um modo mais bvio e mais legvel com uma destas instrues do que com as outras.

174

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Tabela 4.1: Equivalncias entre ciclos. Estas equivalncias no so verdadeiras se as instrues controladas inclurem as instrues return, break, continue, ou goto (ver Seco 4.5.4). H diferenas tambm quanto ao mbito das variveis denidas nestas instrues. { equivalente a inic while(G) { aco prog } } { equivalente a inic while(G) passo equivalente a inic while(G) passo equivalente a for(inic; G;) passo // M ideia... inic if(G) do passo while(G); { passo while(G) passo for(inic; G; prog) aco

do passo while(G)

4.5. INSTRUES DE ITERAO

175

4.5.3 Exemplo simples


A ttulo de exemplo de utilizao simultnea de instrues de iterao e de seleco e de instrues de iterao encadeadas, segue-se um programa que escreve no ecr um tringulo rectngulo de asteriscos com o interior vazio:
#include <iostream> using namespace std; /** Escreve um tringulo oco de asteriscos com a altura passada como argumento. @pre P C 0 altura. @post CO ecr contm, para alm do que continha originalmente, altura linhas adicionais representando um tringulo rectngulo oco de asteriscos. */ void escreveTringuloOco(int const altura) { assert(0 <= altura); for(int i = 0; i != altura; ++i) { for(int j = 0; j != i + 1; ++j) if(j == 0 or j == i or i == altura - 1) cout < < *; else cout < < ; cout < < endl; } } int main() { cout < < "Introduza a altura do tringulo: "; int altura; cin > > altura; escreveTringuloOco(altura); }

Sugere-se que o leitor faa o traado deste programa e que o compile e execute em modo de depurao para compreender bem os dois ciclos encadeados.

4.5.4 return, break, continue, e goto em ciclos


Se um ciclo estiver dentro de uma rotina e se pretender retornar (sair da rotina) a meio do ciclo, ento pode-se usar a instruo de retorno. Por exemplo, se se pretender escrever uma funo que devolva verdadeiro caso o seu parmetro (inteiro) seja primo e falso no caso contrrio, ver-se- mais tarde que uma possibilidade (ver Seco 4.7.5):

176

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); if(n <= 1) return false; for(int i = 2; i != n; ++i) if(n % i == 0) // Se se encontrou um divisor 2 e < n, ento n no primo e pode-se // retornar imediatamente: return false; return true; }

Este tipo de terminao abrupta de ciclos pode ser muito til, mas tambm pode contribuir para tornar difcil a compreenso dos programas. Deve portanto ser usado com precauo e parcimnia e apenas em rotinas muito curtas. Noutros casos torna-se prefervel usar tcnicas de reforo das guardas do ciclo (ver tambm Seco 4.7.5). As instrues de break, continue, e goto oferecem outras formas de alterar o funcionamento normal dos ciclos. A sintaxe da ltima encontra-se em qualquer livro sobre o C++ (e.g., [12]), e no ser explicada aqui, desaconselhando-se vivamente a sua utilizao. A instruo break serve para terminar abruptamente a instruo while, for, do while, ou switch mais interior dentro da qual se encontre. Ou seja, se existirem duas dessas instrues encadeadas, uma instruo break termina apenas a instruo interior. A execuo continua na instruo subsequente instruo interrompida. A instruo continue semelhante instruo break, mas serve para comear antecipadamente a prxima iterao do ciclo (apenas no caso das instrues while, for e do while). Desaconselha-se vivamente a utilizao desta instruo. instruo break aplica-se a recomendao feita quanto utilizao da instruo return dentro de ciclos: usar pouco e com cuidado. No entanto, ver-se- no prximo exemplo que uma utilizao adequada das instrues return e break pode conduzir cdigo simples e elegante. Exemplo Considerem-se de novo os dois ciclos alternativos para validar uma entrada de dados por um utilizador:
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V

4.5. INSTRUES DE ITERAO


@post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { cout < < "Introduza valor (0 a 100): "; cin > > n; while(n < 0 or 100 < n) { cout < < "Valor incorrecto!" < < endl; cout < < "Introduza valor (0 a 100): "; cin > > n; } assert(0 <= n and n <= 100); }

177

e
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { do { cout < < "Introduza valor (0 a 100): "; cin > > n; if(n < 0 or 100 < n) cout < < "Valor incorrecto!" < < endl; } while(n < 0 or 100 < n); assert(0 <= n and n <= 100); }

Nenhuma completamente satisfatria. A primeira porque obriga repetio da instruo de leitura do valor, que portanto aparece antes da instruo while e no seu passo. A segunda porque obriga a uma instruo de seleco cuja guarda idntica guarda do ciclo. O problema est em que teste da guarda deveria ser feito no antes do passo (como na instruo while), nem depois do passo (como na instruo do while), mas dentro do passo! Ou seja, negando a guarda da instruo de seleco e usando uma instruo break,
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { do { cout < < "Introduza valor (0 a 100): ";

178

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


cin > > n; if(0 <= n and n <= 100) break; cout < < "Valor incorrecto!" < < endl; } while(n < 0 or 100 < n); assert(0 <= n and n <= 100); }

que, como a guarda do ciclo sempre verdadeira quando vericada (quando n vlido o ciclo termina na instruo break, sem se chegar a testar a guarda), equivalente a
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { do { cout < < "Introduza valor (0 a 100): "; cin > > n; if(0 <= n and n <= 100) break; cout < < "Valor incorrecto!" < < endl; } while(true); assert(0 <= n and n <= 100); }

que mais comum escrever como


/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { while(true) { cout < < "Introduza valor (0 a 100): "; cin > > n; if(0 <= n and n <= 100) break; cout < < "Valor incorrecto!" < < endl; } assert(0 <= n and n <= 100); return n; }

4.5. INSTRUES DE ITERAO


A Figura 4.9 mostra o diagrama de actividade da funo.

179

cout << "Introduza valor (0 a 100):

";

cin n;

[0 n 100] [n < 0 100 < n] cout << "Valor incorrecto!< endl;

Figura 4.9: Diagrama de actividade da funo inteiroPedidoDe0a100(). Os ciclos do while(true), for(;;), e while(true) so ciclos innitos ou laos, que s terminam com recurso a uma instruo break ou return. No h nada de errado em ciclos desta forma, desde que recorram a uma e uma s instruo de terminao do ciclo. Respeitando esta restrio, um lao pode ser analisado e a sua correco demonstrada recorrendo noo de invariante, como usual. Uma ltima verso do ciclo poderia ser escrita recorrendo a uma instruo de retorno, desde que se transformasse o procedimento numa funo:
/** Devolve um inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 inteiroPedidoDe0a100 100. */ int inteiroPedidoDe0a100() { while(true) { cout < < "Introduza valor (0 a 100): "; int n;

180

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


cin > > n; if(0 <= n and n <= 100) { assert(0 <= n and n <= 100); return n; } cout < < "Valor incorrecto!" < < endl; } }

Note-se que se passou a denio da varivel n para o mais perto possvel da sua primeira utilizao6 . Esta ltima verso, no entanto, no muito recomendvel, pois a funo tem efeitos laterais: o canal cin sofre alteraes durante a sua execuo. Em geral m ideia desenvolver mistos de funes e procedimentos, i.e., funes com efeitos laterais ou, o que o mesmo, procedimentos que devolvem valores. Nesta altura conveniente fazer uma pequena digresso e explicar como se pode no procedimento lInteiroPedidoDe0a100() lidar no apenas com erros do utilizador quanto ao valor do inteiro introduzido, mas tambm com erros mais graves, como a introduo de uma letra em vez de uma sequncia de dgitos. Uma verso prova de bala Que acontece no procedimento lInteiroPedidoDe0a100() quando o utilizador introduz dados errados, i.e., dados que no podem ser interpretados como um valor inteiro? Infelizmente, o ciclo torna-se innito, repetindo a mensagem
Noutras linguagens existe uma instruo loop para este efeito, que pode ser simulada em C++ recorrendo ao pr-processador (ver Seco 9.2.1):
6

#define loop while(true)


Depois desta denio o ciclo pode-se escrever:

/** Devolve um inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 inteiroPedidoDe0a100 100. */ int inteiroPedidoDe0a100() { loop { cout < < "Introduza valor (0 a 100): "; int n; cin > > n; if(0 <= n and n <= 100) { assert(0 <= n and n <= 100); return n; } cout < < "Valor incorrecto!" < < endl; } }

4.5. INSTRUES DE ITERAO


Introduza valor (0 a 100): Valor incorrecto!

181

eternamente. Isso deve-se ao facto de o canal de entrada cin de onde se faz a extraco do valor inteiro, car em estado de erro. Uma caracterstica interessante de um canal em estado de erro que qualquer tentativa de extraco subsequente falhar! Assim, para resolver o problema necessrio limpar explicitamente essa condio de erro usando a instruo
cin.clear();

que corresponde invocao de uma operao clear() do tipo istream, ao qual cin pertence (o signicado de operao neste contexto ser visto no Captulo 7). Assim, o cdigo original pode ser modicado para
/** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { while(true) { cout < < "Introduza valor (0 a 100): "; cin > > n; if(not cin) { cout < < "Isso no um inteiro!" < < endl; cin.clear(); } else if(n < 0 or 100 < n) cout < < "Valor incorrecto!" < < endl; else break; } assert(0 <= n and n <= 100); return n; }

onde se tirou partido do facto de um canal poder ser interpretado como um valor booleano, correspondendo o valor falso a um canal em estado de erro. O problema do cdigo acima que... ca tudo na mesma... Isto acontece porque o caractere errneo detectado pela operao de extraco mantm-se no canal cin apesar de se ter limpo a condio de erro, logo a prxima extraco tem de forosamente falhar, tal como a primeira, e assim sucessivamente. A soluo passa por remover os caracteres errneos do canal cin sempre que se detectar um erro. A melhor forma de o fazer eliminar toda a linha introduzida pelo utilizador: mais simples e mais intuitivo para o utilizador do programa.

182

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Para eliminar toda a linha, basta extrair do canal caracteres at se encontrar o caractere que representa o m-de-linha, i.e., \n. Para que a extraco seja feita para cada caractere individualmente, coisa que no acontece com o operador > >, que por omisso ignora todos os espaos em branco que encontra7 , usa-se a operao get() da classe istream:
/** Extrai todos os caracteres do canal cin at encontrar o m-de-linha. @pre P C V. @post CO Todos os caracteres no canal cin at ao primeiro m-de-linha inclusiva foram extrados. */ void ignoraLinha() { char caractere; do cin.get(caractere); while(cin and caractere != \n) } /** L inteiro entre 0 e 100 pedido ao utilizador. @pre P C V @post CO 0 n 100. */ void lInteiroPedidoDe0a100(int& n) { while(true) { cout < < "Introduza valor (0 a 100): "; cin > > n; if(not cin) { cout < < "Isso no um inteiro!" < < endl; cin.clear(); ignoraLinha(); } else if(n < 0 or 100 < n) cout < < "Valor incorrecto!" < < endl; else break; } assert(0 <= n and n <= 100); return n; }

4.5.5 Problemas comuns


De longe o problema mais comum ao escrever ciclos o falhano por um (off by one). Por exemplo, quando se desenvolve um ciclo for para escrever 10 asteriscos no ecr, comum
7

Espaos em branco so os caracteres espao, tabulador, tabulador vertical e m-de-linha.

4.6. ASSERES COM QUANTIFICADORES


errar a guarda do ciclo e escrever
for(int i = 0; i <= 10; ++i) cout < < *;

183

que escreve um asterisco a mais. necessrio ter cuidado com a guarda dos ciclos, pois estes erros so comuns e muito difceis de detectar. Na Seco 4.7 estudar-se-o metodologias de desenvolvimento de ciclos que minimizam grandemente a probabilidade de estes erros ocorrerem. Outro problema comum corresponde a colocar um ; aps o cabealho de um ciclo. Por exemplo:
int i = 0; while(i != 10); // Isto uma instruo nula! { cout < < *; ++i; }

Neste caso o ciclo nunca termina, pois o passo do while a instruo nula, que naturalmente no afecta o valor de i. Um caso pior ocorre quando se usa um ciclo for:
for(int i = 0; i != 10; ++i); cout < < *;

Este caso mais grave porque o ciclo termina 8 . Mas, ao contrrio do que o programador pretendia, este pedao de cdigo escreve apenas um asterisco, e no 10!

4.6 Asseres com quanticadores


A especicao de de um problema sem quaisquer ambiguidades , como se viu, o primeiro passo a realizar para a sua soluo. A especicao de um problema faz-se tipicamente indicando a pr-condio (P C) e a condio objectivo (CO). A pr-condio um predicado acerca das variveis do problema que se assume ser verdadeiro no incio. A condio objectivo um predicado que se pretende que seja verdadeiro depois de resolvido o problema, i.e., depois de executado o troo de cdigo que o resolve. A pr-condio e a condio objectivo no passam, como se viu antes, de asseres ou armaes feitas acerca das variveis de um programa, i.e., acerca do seu estado, sendo o estado de um programa dado pelo valor das suas variveis em determinado instante de tempo. Assim, a pr-condio estabelece limites ao estado do programa imediatamente antes de comear a sua resoluo e a condio objectivo estabelece limites ao estado do programa imediatamente depois de terminar a sua resoluo.
8

Se no terminar mais fcil perceber que alguma coisa est errada no cdigo!

184

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

A escrita de asseres para problemas um pouco mais complicados que os vistos at aqui requer a utilizao de quanticadores. Quanticadores so formas matemticas abreviadas de escrever expresses que envolvem a repetio de uma dada operao. Exemplos so os quanticadores aritmticos somatrio e produto, e os quanticadores lgicos universal (qualquer que seja) e existencial (existe pelo menos um). Apresentam-se aqui algumas notas sobre os quanticadores que so mais teis para a construo de asseres acerca do estado dos programas. A notao utilizada encontra-se resumida no Apndice A. Os quanticadores tero sempre a forma (X v : predicado(v) : expresso(v)) a onde X indica a operao realizada: S para a soma (somatrio), P para o produto (produtrio ou piatrio), Q para a conjuno (qualquer que seja), e E para a disjuno (existe pelo menos um). v uma varivel muda, que tem signicado apenas dentro do quanticador, e que se assume normalmente pertencer ao conjunto dos inteiros. Se pertencer a outro conjunto tal pode ser indicado explicitamente. Por exemplo: Q x R : 2 |x| : 0 x2 2 . Um quanticador pode possuir mais do que uma varivel muda. Por exemplo: (S i, j : 0 i < m 0 j < n : f (i, j)) predicado(v) um predicado envolvendo a varivel muda v e que dene implicitamente o conjunto de valores de v para os quais deve ser realizada a operao. expresso(v) uma expresso envolvendo a varivel muda v e que deve ter um resultado a aritmtico no caso dos quanticadores aritmticos e lgico no caso dos quanticadores lgicos. I.e., no caso dos quanticadores lgicos qualquer que seja e existe pelo menos um, essa expresso deve ser tambm um predicado.

4.6.1 Somas
O quanticador soma corresponde ao usual somatrio. Na notao utilizada, (S j : m j < n : expresso(j)) a tem exactamente o mesmo signicado que o somatrio de expresso(j) com j variando entre a m e n 1 inclusive. Numa notao mais clssica escrever-se-ia
n1

expresso(j). a
j=m

4.6. ASSERES COM QUANTIFICADORES

185

Por exemplo, um facto conhecido que o somatrio dos primeiros n inteiros no-negativos n(n1) , ou seja, 2 n(n 1) se 0 < n. (S j : 0 j < n : j) = 2 Que acontece se n 0? Neste caso evidente que a varivel muda j no pode tomar quaisquer valores, e portanto o resultado a soma de zero termos. A soma de zero termos zero, por ser 0 o elemento neutro da soma. Logo, (S j : 0 j < n : j) = 0 se n 0. Em geral pode dizer que (S j : m j < n : expresso(j)) = 0 se n m. a Se se pretender desenvolver uma funo que calcule a soma dos n primeiros nmeros mpares positivos (sendo n um parmetro da funo), pode-se comear por escrever o seu cabealho bem como a pr-condio e a condio objectivo, como habitual:
/** Devolve a soma dos primeiros n mpares positivos. @pre P C 0 n. @post CO somampares = (S j : 0 j < n : 2j + 1) */ int somampares(int const n) { ... }

Mais uma vez fez-se o parmetro da funo constante de modo a deixar claro que a funo no lhe altera o valor.

4.6.2 Produtos
O quanticador produto corresponde ao produto de factores por vezes conhecido como produtrio ou mesmo piatrio. Na notao utilizada, (P j : 0 j < n : expresso(j)) a tem exactamente o mesmo signicado que o usual produto de expresso(j) com j variando a entre m e n 1 inclusive. Numa notao mais clssica escrever-se-ia
n1

expresso(j). a
j=m

Por exemplo, a denio de factorial n! = (P j : 1 j < n + 1 : j) .

186

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

O produto de zero termos um, por ser 1 o elemento neutro da multiplicao. Ou seja, (P j : m j < n : expresso(j)) = 1 se n m. a Se se pretender desenvolver uma funo que calcule o factorial de n (sendo n um parmetro da funo), pode-se comear por escrever o seu cabealho bem como a pr-condio e a condio objectivo:
/** Devolve o factorial de n. @pre P C 0 n. @post CO factorial = n! = (P j : 1 j < n + 1 : j). */ int factorial(int const n) { ... }

4.6.3 Conjunes e o quanticador universal


O quanticador universal corresponde conjuno (e) de vrios predicados usualmente conhecida por qualquer que seja9 . Na notao utilizada, (Q j : m j < n : predicado(j)) tem exactamente o mesmo signicado que a conjuno dos predicados predicado(j) com j variando entre m e n 1 inclusive. Numa notao mais clssica escrever-se-ia
n1

predicado(j)
j=m

ou ainda m j < n : predicado(j). A conjuno de zero predicados tem valor verdadeiro, por ser V o elemento neutro da conjuno. Ou seja10 , (Q j : m j < n : predicado(j)) = V se n m. Por exemplo, a denio do predicado primo(n) que tem valor V se n primo e F no caso contrrio, (Q j : 2 j < n : n j = 0) se 2 n, e primo(n) = F se 0 n < 2,
Se no claro para si que o quanticador universal corresponde a uma sequncia de conjunes, pense no signicado de escrever todos os humanos tm cabea. A traduo para linguagem mais matemtica seria qualquer que seja h pertencente ao conjunto dos humanos, h tem cabea. Como o conjunto dos humanos nito, pode-se escrever por extenso, listando todos os possveis humanos: o Antnio tem cabea e o Sampaio tem cabea e .... Isto , a conjuno de todas as armaes. 10 A armao todos os marcianos tm cabea verdadeira, pois no existem marcianos. Esta propriedade menos intuitiva que no caso dos quanticadores soma e produto, mas importante.
9

4.6. ASSERES COM QUANTIFICADORES


ou seja, primo(n) = ((Q j : 2 j < n : n j = 0) 2 n) para 0 n, sendo a operao resto da diviso inteira 11 .

187

Se se pretender desenvolver uma funo que devolva o valor lgico verdadeiro quando n (sendo n um parmetro da funo) um nmero primo, e falso no caso contrrio, pode-se comear por escrever o seu cabealho bem como a pr-condio e a condio objectivo:
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { ... }

4.6.4 Disjunes e o quanticador existencial


O quanticador existencial corresponde disjuno (ou) de vrios predicados usualmente conhecida por existe pelo menos um12 . Na notao utilizada, (E j : m j < n : predicado(j)) tem exactamente o mesmo signicado que a disjuno dos predicados predicado(j) com j variando entre m e n 1 inclusive e pode-se ler como existe um valor j entre m e n exclusive tal que predicado(i) verdadeiro. Numa notao mais clssica escrever-se-ia
n1

predicado(j)
j=m

ou ainda m j < n : predicado(j). A disjuno de zero predicados tem valor falso, por ser F o elemento neutro da disjuno. Ou seja13 , (E j : m j < n : predicado(j)) = F se n m. Este quanticador est estreitamente relacionado com o quanticador universal. sempre verdade que (Q j : m j < n : predicado(j)) = (E j : m j < n : predicado(j)) ,

Como o operador % em C++. Se no claro para si que o quanticador existencial corresponde a uma sequncia de disjunes, pense no signicado de escrever existe pelo menos um humano com cabea. A traduo para linguagem mais matemtica seria existe pelo menos um h pertencente ao conjunto dos humanos tal que h tem cabea. Como o conjunto dos humanos nito, pode-se escrever por extenso, listando todos os possveis humanos: o Z tem cabea ou o Sampaio tem cabea ou .... Isto , a disjuno de todas as armaes. 13 A armao existe pelo menos um marciano com cabea falsa, pois no existem marcianos.
12

11

188

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

ou seja, se no verdade que para qualquer j o predicado predicado(j) verdadeiro, ento existe pelo menos um j para o qual o predicado predicado(j) falso. Aplicado denio de nmero primo acima, tem-se primo(n) = ((Q j : 2 j < n : n j = 0) 2 n) = (Q j : 2 j < n : n j = 0) n < 2 = (E j : 2 j < n : n j = 0) n < 2

para 0 n. I.e., um inteiro no-negativo no primo se for inferior a dois ou se for divisvel por algum inteiro superior a 1 e menor que ele prprio. Se se pretender desenvolver uma funo que devolva o valor lgico verdadeiro quando existir um nmero primo entre m e n exclusive (sendo m e n parmetros da funo, com m nonegativo), e falso no caso contrrio, pode-se comear por escrever o seu cabealho bem como a pr-condio e a condio objectivo:
/** Devolve verdadeiro se s se existir um nmero primo no intervalo [m, n[. @pre P C 0 m. @post CO existePrimoNoIntervalo = (E j : m j < n : primo(j)). */ bool existePrimoNoIntervalo(int const m, int const n) { ... }

4.6.5 Contagens
O quanticador de contagem (N j : m j < n : predicado(j)) tem como valor (inteiro) o nmero de predicados predicados(j) verdadeiros para j variando entre m e n 1 inclusive. Por exemplo, (N j : 1 j < 10 : primo(j)) = 4, ou seja, existem quatro primos entre 1 e 10 exclusive, uma armao verdadeira. Este quanticador extremamente til quando necessrio especicar condies em que o nmero de ordem fundamental. Se se pretender desenvolver uma funo que devolva o n-simo nmero primo (sendo n parmetro da funo), pode-se comear por escrever o seu cabealho bem como a pr-condio e a condio objectivo:
/** Devolve o n-simo nmero primo. @pre P C 1 n. @post CO primo(primo) (N j : 2 j < primo : primo(j)) = n 1. int primo(int const n) { ... }

4.7. DESENVOLVIMENTO DE CICLOS

189

A condio objectivo arma que o valor devolvido um primo e que existem exactamente n 1 primos de valor inferior.

4.6.6 O resto da diviso


Apresentou-se atrs o operador resto da denio inteira sem que se tenha denido formalmente. A denio pode ser feita custa de alguns dos quanticadores apresentados: m n, com 0 m e 0 < n14 , o nico elemento do conjunto {0 r < n : (E q : 0 q : m = qn + r)}. Claro que a denio est incompleta enquanto no se demonstrar que de facto o conjunto tem um nico elemento, ou seja que, quando 0 m e 0 < n, se tem (N r : 0 r < n : (E q : 0 q : m = qn + r)) = 1. Deixa-se a demonstrao como exerccio para o leitor.

4.7 Desenvolvimento de ciclos


O desenvolvimento de programas usando ciclos simultaneamente uma arte [10] e uma cincia [8]. Embora a intuio seja muito importante, muitas vezes importante usar metodologias mais ou menos formais de desenvolvimento de ciclos que permitam garantir simultaneamente a sua correco. Embora esta matria seja formalizada na disciplina de Computao e Algoritmia, apresentam-se aqui os conceitos bsicos da metodologia de Dijkstra para o desenvolvimento de ciclos. Para uma apresentao mais completa consultar [8]. Suponha-se que se pretende desenvolver uma funo para calcular a potncia n de x, isto , uma funo que, sendo x e n os seus dois parmetros, devolva x n . A sua estrutura bsica
double potncia(double const x, int const n) { ... }

claro que xn = x x x, ou seja, xn pode ser obtido por multiplicao repetida de x. Assim, uma soluo passa por usar um ciclo que no seu passo faa cada uma dessas multiplicaes:
A denio de resto pode ser generalizada para englobar valores negativos de m e n. Em qualquer dos casos tem de existir um quociente q tal que m = qn + r. Mas o intervalo onde r se encontra varia: 0r<n 0 r < n n < r 0 n<r0 se se se se 0 m 0 < n (neste caso 0 q) 0 m n < 0(neste caso q 0) m 0 0 < n(neste caso q 0) m 0 n < 0(neste caso 0 q)
14

190

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


double potncia(double const x, int const n) { int i = 1; // usada para contar o nmero de x j includos no produto. double r = x; // usada para acumular os produtos de x. while(i <= n) { r *= x; // o mesmo que r = r * x; ++i; // o mesmo que i = i + 1; } return r; }

Ser que o ciclo est correcto? Fazendo um traado da funo admitindo que chamada com os argumentos 5,0 e 2, verica-se facilmente que devolve 125,0 e no 25,0! Isso signica que feita uma multiplicao a mais. Observando com ateno a guarda da instruo de iterao, conclui-se que esta no deveria deixar o contador i atingir o valor de n. Ou seja, a guarda deveria ser i < n e no i n. Corrigindo a funo:
double potncia(double const x, int const n) { int i = 1; double r = x; while(i < n) { r *= x; ++i; } return r; }

Estar o ciclo denitivamente correcto? Fazendo traados da funo admitindo que chamada com argumentos 5 e 2 e com 5 e 3, facilmente se verica que devolve respectivamente 25 e 125, pelo que aparenta estar correcta. Mas estar correcta para todos os valores? E se os argumentos forem 5 e 0? Nesse caso a funo devolve 5 em vez do valor correcto, que 5 0 = 1! Observando com ateno a inicializao do contador e do acumulador dos produtos, conclui-se que estes deveriam ser inicializados com 0 e 1, respectivamente. Corrigindo a funo:
double potncia(double const x, int const n) { int i = 0; double r = 1.0; while(i < n) { r *= x; ++i; } return r; }

4.7. DESENVOLVIMENTO DE CICLOS

191

Neste momento a funo parece estar correcta. Mas que acontece se o expoente for negativo? Fazendo o traado da funo admitindo que chamada com os argumentos 5 e -1, facilmente se verica que devolve 1 em vez de 51 = 0,2! Os vrios problemas que surgiram ao longo desde desenvolvimento atribulado deveram-se a que: 1. O problema no foi bem especicado atravs da escrita da pr-condio e da condio objectivo. Em particular a pr-condio deveria estabelecer claramente se a funo sabe lidar com expoentes negativos ou no. 2. O desenvolvimento foi feito ao sabor da pena, de uma forma pouco disciplinada. Retrocedendo um pouco na resoluo do problema de escrever a funo potncia(), fundamental, pelo que se viu, comear por especicar o problema sem ambiguidades. Para isso formalizam-se a pr-condio e a condio objectivo da funo. Para simplicar, suponha-se que a funo s deve garantir bom funcionamento para expoentes no-negativos:
/** Devolve a potncia n de x. @pre P C 0 n. @post CO potncia = xn . */ double potncia(double const x, int const n) { int i = 0; double r = 1.0; while(i < n) { r *= x; ++i; } return r; }

importante vericar agora o que acontece se o programador consumidor da funo se enganar e, violando o contrato expresso pela pr-condio e pela condio objectivo, a invocar com um expoente negativo. Como se viu, a funo simplesmente devolve um valor errado: 1. Isso acontece porque, sendo n negativo e i inicializado com 0, a guarda inicialmente falsa, no sendo o passo do ciclo executado nenhuma vez. possvel enfraquecer a guarda, de modo a que seja falsa em menos circunstncias e de tal forma que o contrato da funo no se modique: basta alterar a guarda de i < n para i = n. bvio que para valores do expoente no-negativos as duas guardas so equivalentes. Mas para expoentes negativos a nova guarda leva a um ciclo innito! O contador i vai crescendo a partir de zero, afastando-se irremediavelmente do valor negativo de n. Qual das guardas ser prefervel? A primeira, que em caso de engano por parte do programador consumidor da funo devolve um valor errado, ou a segunda, que nesse caso entra num

192

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

ciclo innito, no chegando a terminar? A verdade que prefervel a segunda, pois o programador consumidor mais facilmente se apercebe do erro e o corrige em tempo til 15 . Com a primeira guarda o problema pode s ser detectado demasiado tarde, quando os resultados errados j causaram danos irremediveis. Assim, a nova verso da funo
/** Devolve a potncia n de x. @pre P C 0 n. @post CO potncia = xn . */ double potncia(double const x, int const n) { int i = 0; double r = 1.0; while(i != n) { r *= x; ++i; } return r; }

onde a guarda consideravelmente mais fraca do que anteriormente. Claro est que o ideal explicitar tambm a vericao da validade da pr-condio:
/** Devolve a potncia n de x. @pre P C 0 n. @post CO potncia = xn . */ double potncia(double const x, int const n) { assert(0 <= n); int i = 0; double r = 1.0; while(i != n) { r *= x; ++i; } return r; }
Note-se que na realidade o ciclo no innito, pois os int so limitados e incrementaes sucessivas levaro o valor do contador ao limite superior dos int. O que acontece depois depende do compilador, sistema operativo e mquina em que o programa foi compilado e executado. Normalmente o que acontece que uma incrementao feita ao contador quando o seu valor j atingiu o limite superior dos int leva o valor a dar a volta aos inteiros, passando para o limite inferior dos int (ver Seco 2.3). Incrementaes posteriores levaro o contador at zero, pelo que em rigor o ciclo no innito... Mas demora muito tempo a executar, pelo menos, o que mantm a validade do argumento usado para justicar a fraqueza das guardas.
15

4.7. DESENVOLVIMENTO DE CICLOS

193

Isto resolve o primeiro problema, pois agora o problema est bem especicado atravs de uma pr-condio e uma condio objectivo. E o segundo problema, do desenvolvimento indisciplinado? possvel certamente desenvolver cdigo ao sabor da pena, mas importante que seja desenvolvido correctamente. Signica isto que, para ciclos mais simples ou conhecidos, o programador desenvolve-os rapidamente, sem grandes preocupaes. Mas para ciclos mais complicados o programador deve ter especial ateno sua correco. Das duas uma, ou os desenvolve primeiro e depois demonstra a sua correco, ou usa uma metodologia de desenvolvimento que garanta a sua correco 16 . As prximas seces lidam com estes dois problemas: o da demonstrao de correco de ciclos e o de metodologias de desenvolvimento de ciclos. Antes de avanar, porm, fundamental apresentar a noo de invariante um ciclo.

4.7.1 Noo de invariante


Considerando de novo a funo desenvolvida, assinalem-se com nmeros todos as transies entre instrues da funo:
/** Devolve a potncia n de x. @pre P C 0 n. @post CO potncia = xn . */ double potncia(double const x, int const n) { assert(0 <= n); int i = 0; double r = 1.0;

1: Depois da inicializao do ciclo, i.e., depois da inicializao das variveis nele envolvidas.
while(i != n) {

2: Depois de se vericar que a guarda verdadeira.


r *= x;

3: Depois de acumular mais uma multiplicao de x em r.


++i;

4: Depois de incrementar o contador do nmero de x j includos no produto r.


}
Em alguns casos a utilizao desta metodologia no prtica, uma vez que requer um arsenal considervel de modelos: o caso de ciclos que envolvam leituras do teclado e/ou escritas no ecr. Nesses casos a metodologia continua aplicvel, como bvio, embora seja vulgar que as asseres sejam escritas com menos formalidade.
16

194

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

5: Depois de se vericar que a guarda falsa, imediatamente antes do retorno.


return r; }

Suponha-se que a funo invocada com os argumentos 3 e 4. Isto , suponha-se que quando a funo comea a ser executada as constantes x e n tm os valores 3 e 4. Faa-se um traado da execuo da funo anotando o valor das suas variveis em cada uma das transies assinaladas. Obtm-se a seguinte tabela:

Transio 1 2 3 4 2 3 4 2 3 4 2 3 4 5

i 0 0 0 1 1 1 2 2 2 3 3 3 4 4

r 1 1 3 3 3 9 9 9 27 27 27 81 81 81

Comentrios Como 0 = 4, o passo do ciclo ser executado, passando-se transio 2.

Como 1 = 4, o passo do ciclo ser executado, passando-se transio 2.

Como 2 = 4, o passo do ciclo ser executado, passando-se transio 2.

Como 3 = 4, o passo do ciclo ser executado, passando-se transio 2.

Como 4 = 4, o ciclo termina, passando-se transio 5.

fcil vericar que h uma condio relacionando x, r, n e i que se verica em todos as transies do ciclo com excepo da transio 3, i.e., que se verica depois da inicializao, antes do passo, depois do passo, e no nal do ciclo. A relao r = x i 0 i n. Esta relao diz algo razoavelmente bvio: em cada instante (excepto no Ponto 3) o acumulador possui a potncia i do valor em x, tendo i um valor entre 0 e n. Esta condio, por ser verdadeira ao longo de todo o ciclo (excepto a meio do passo, no Ponto 3), diz-se uma invariante do ciclo. Representando o ciclo na forma de um diagrama de actividade e colocando as asseres que se sabe serem verdadeiras em cada transio do diagrama, mais fcil compreender a noo de invariante, como se v na Figura 4.10. As condies invariantes so centrais na demonstrao da correco de ciclos e durante o desenvolvimento disciplinado de ciclos, como se ver nas prximas seces. Em geral, para um ciclo da forma
inic while(G) passo

4.7. DESENVOLVIMENTO DE CICLOS

195

{0 n} int i = 0; double r = 1.0; {0 n i = 0 r = 1.0}

[i = n] [i = n] {0 n 0 i < n r = xi }

r *= x; {0 n 0i = n r = xn } ++i; {0 n 0 < i n r = xi }

Figura 4.10: Diagrama de actividade do ciclo para clculo da potncia mostrando as asseres nas transies entre instrues (actividades).

196

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

uma condio diz-se invariante (CI) se 1. for verdadeira logo aps a inicializao do ciclo (inic), 2. for verdadeira imediatamente antes do passo (passo), 3. for verdadeira imediatamente aps o passo e 4. for verdadeira depois de terminado o ciclo. O diagrama de actividade de um ciclo genrico pode-se ver na Figura 4.11.

{P C} inic {CI}

[G] [G] {CI G} {CI G}

passo {CI} {CO}

Figura 4.11: Diagrama de actividade de um ciclo genrico. S as instrues podem alterar o estado do programa e portanto validar ou invalidar asseres. Incluram-se algumas asseres adicionais no diagrama. Antes da inicializao assume-se que a pr-condio (P C) do ciclo se verica, e no nal do ciclo pretende-se que se verique a sua condio objectivo (CO). Alm disso, depois da deciso do ciclo, em que se verica a veracidade da guarda, sabe-se que a guarda (G) verdadeira antes de executar o passo e falsa (ou seja, verdadeira a sua negao G) depois de terminado o ciclo. Ou seja, antes do passo sabe-se que CI G uma assero verdadeira e no nal do ciclo sabe-se que CI G tambm uma assero verdadeira. No caso da funo potncia(), a condio objectivo do ciclo diferente da condio objectivo da funo, pois refere-se varivel r, que ser posteriormente devolvida pela funo. I.e.:

4.7. DESENVOLVIMENTO DE CICLOS


/** Devolve a potncia n de x. @pre 0 n. @post potncia = xn . */ double potncia(double const x, int const n) { // P C 0 n. assert(0 <= n); int i = 0; double r = 1.0; // CI r = xi 0 i n. while(i != n) { r *= x; ++i; } // CO r = xn . return r; }

197

onde se aproveitou para documentar a condio invariante do ciclo. Demonstrao de invarincia Um passo fundamental na demonstrao da correco de um ciclo a demonstrao de invarincia de uma dada assero CI. Para que a assero CI possa ser invariante tem de ser verdadeira depois da inicializao (ver Figura 4.11). Assumindo que a pr-condio do ciclo se verica, ento a inicializao inic tem de conduzir forosamente veracidade da assero CI. necessrio portanto demonstrar que:
// P C inic // CI

Para o exemplo do clculo da potncia isso uma tarefa simples. Nesse caso tem-se:
// P C 0 n. int i = 0; double r = 1.0; // CI r = xi 0 i n.

A demonstrao pode ser feita substituindo em CI os valores iniciais de r e i: CI r = xi 0 i n 1 = x0 0 0 n V 0n 0 n,

198

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

que garantidamente verdadeira dada a P C, ou seja CI V Neste momento demonstrou-se a veracidade da assero CI depois da inicializao. Falta demonstrar que verdadeira antes do passo, depois do passo, e no m do ciclo. A demonstrao pode ser feita por induo. Supe-se que a assero CI verdadeira antes do passo numa qualquer iterao do ciclo e demonstra-se que tambm verdadeira depois do passo nessa mesma iterao. Como antes do passo a guarda forosamente verdadeira, pois de outra forma o ciclo teria terminado, necessrio demonstrar que:
// CI G passo // CI

Se esta demonstrao for possvel, ento por induo a assero CI ser verdadeira ao longo de todo o ciclo, i.e., antes e depois do passo em qualquer iterao do ciclo e no nal do ciclo. Isso pode ser visto claramente observando o diagrama na Figura 4.11. Como se demonstrou que a assero CI verdadeira depois da inicializao, ento tambm ser verdadeira depois de vericada a guarda pela primeira vez, visto que a vericao da guarda no altera qualquer varivel do programa17 . Ou seja, se a guarda for verdadeira, a assero CI ser verdadeira antes do passo na primeira iterao do ciclo e, se a guarda for falsa, a assero CI ser verdadeira no nal do ciclo. Se a guarda for verdadeira, como se demonstrou que a veracidade de CI antes do passo numa qualquer iterao implica a sua veracidade depois do passo na mesma iterao, conclui-se que, quando se for vericar de novo a guarda do ciclo (ver diagrama) a assero CI verdadeira. Pode-se repetir o argumento para a segunda iterao do ciclo e assim sucessivamente. Para o caso dado, necessrio demonstrar que:
// CI G r = xi 0 i n i = n r = xi 0 i < n. r *= x; ++i; // CI r = xi 0 i n.

A demonstrao pode ser feita vericando qual a pr-condio mais fraca de cada uma das atribuies, do m para o princpio (ver Seco 4.2.3). Obtm-se:
// r = xi+1 0 i + 1 n, ou seja, // r = xi x 1 i n 1, ou seja, // r = xi x 1 i < n. ++i; // o mesmo que i = i + 1; // CI r = xi 0 i n.
17

Admite-se aqui que a guarda uma expresso booleana sem efeitos laterais.

4.7. DESENVOLVIMENTO DE CICLOS

199

antes da incrementao. Aplicando a mesma tcnica atribuio anterior tem-se que:


// r x = xi x 1 i < n. r *= x; // o mesmo que r = r * x; // r = xi x 1 i < n.

ou seja, para que a assero CI se verique depois da incrementao de i necessrio que se verique a assero r = xi x 1 i < n

Falta portanto demonstrar que


// CI G r = xi 0 i < n implica // r x = xi x 1 i < n.

mas isso verica-se por mera observao, pois 0 i 1 i e r = x i r x = xi x Em resumo, para provar a invarincia de uma assero CI necessrio:

1. Mostrar que, admitindo a veracidade da pr-condio antes da inicializao, a assero CI verdadeira depois da inicializao inic. Ou seja:
// P C inic // CI

2. Mostrar que, se CI for verdadeira no incio do passo numa qualquer iterao do ciclo, ento tambm o ser depois do passo nessa mesma iterao. Ou seja:
// CI G passo // CI

sendo a demonstrao feita, comummente, do m para o princpio, deduzindo as prcondies mais fracas de cada instruo do passo.

4.7.2 Correco de ciclos


Viu-se que a demonstrao da correco de ciclos muito importante. Mas como faz-la? H que demonstrar dois factos: que o ciclo quando termina garante que a condio objectivo se verica e que o ciclo termina sempre ao m de um nmero nito de iteraes. Diz-se que se demonstrou a correco parcial de um ciclo se se demonstrou que a condio objectivo se verica quando o ciclo termina, assumindo que a pr-condio se verica no seu incio. Diz-se que se demonstrou a correco total de um ciclo se, para alm disso, se demonstrou que o ciclo termina sempre ao m de um nmero nito de iteraes.

200 Correco parcial

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

A determinao de uma condio invariante CI apropriada para a demonstrao muito importante. que, como a CI verdadeira no nal do ciclo e a guarda G falsa, para demonstrar a correco parcial do ciclo basta mostrar que CI G CO. No caso da funo potncia() essa demonstrao simples: CI G r = xi 0 i n i = n r = xi i = n

r = xn CO

Em resumo, para provar a correco parcial de um ciclo necessrio 1. encontrar uma assero CI apropriada (a assero V trivialmente invariante de qualquer ciclo e no ajuda nada na demonstrao de correco parcial: necessrio que a CI seja rica em informao), 2. demonstrar que essa assero CI de facto uma invariante do ciclo (ver seco anterior) e 3. demonstrar que CI G CO. Correco total A correco parcial insuciente. Suponha-se a seguinte verso da funo potncia():
/** Devolve a potncia n de x. @pre 0 n. @post potncia = xn . */ double potncia(double const x, int const n) { // P C 0 n. assert(0 <= n); int i = 0; double r = 1.0; // CI r = xi 0 i n. while(i != n) ; // Instruo nula! // CO r = xn . return r; }

4.7. DESENVOLVIMENTO DE CICLOS

201

fcil demonstrar a correco parcial deste ciclo 18 ! Mas este ciclo no termina nunca excepto quando n 0. De acordo com a denio dada na Seco 1.3, este ciclo no implementa um algoritmo, pois no verica a propriedade da nitude. A demonstrao formal de terminao de um ciclo ao m de um nmero nito de iteraes faz-se usando o conceito de funo de limitao (bound function) [8], que no ser abordado neste texto. Neste contexto ser suciente a demonstrao informal desse facto. Por exemplo, na verso original da funo potncia()
/** Devolve a potncia n de x. @pre 0 n. @post potncia = xn . */ double potncia(double const x, int const n) { // P C 0 n. assert(0 <= n); int i = 0; double r = 1.0; // CI r = xi 0 i n. while(i != n) { r *= x; ++i; } // CO r = xn . return r; }

evidente que, como a varivel i comea com o valor zero e incrementada de uma unidade ao m de cada passo, fatalmente tem de atingir o valor de n (que, pela pr-condio, nonegativo). Em particular fcil vericar que o nmero exacto de iteraes necessrio para isso acontecer exactamente n. O passo , tipicamente, dividido em duas partes: a aco e o progresso. Os ciclos tm tipicamente a seguinte forma:
inic while(G) { aco prog }

onde o progresso prog corresponde ao conjunto de instrues que garante a terminao do ciclo e a aco aco corresponde ao conjunto de instrues que garante a invarincia de CI apesar de se ter realizado o progresso. No caso do ciclo da funo potncia() a aco e o progresso so:
18

Porqu?

202
r *= x; // aco ++i; // progresso

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Resumo Para demonstrar a correco total de um ciclo:


// P C inic while(G) { aco prog } // CO

necessrio: 1. encontrar uma assero CI apropriada; 2. demonstrar que essa assero CI de facto uma invariante do ciclo: (a) mostrar que, admitindo a veracidade de P C antes da inicializao, a assero CI verdadeira depois da inicializao inic, ou seja:
// P C inic // CI

(b) mostrar que, se CI for verdadeira no incio do passo numa qualquer iterao do ciclo, ento tambm o ser depois do passo nessa mesma iterao, ou seja:
// CI G passo // CI

3. demonstrar que CI G CO; e 4. demonstrar que o ciclo termina sempre ao m de um nmero nito de iteraes, para o que se raciocina normalmente em termos da inicializao inic, da guarda G e do progresso prog.

4.7.3 Melhorando a funo potncia()


Ao se especicar sem ambiguidades o problema que a funo potncia() deveria resolver fez-se uma simplicao: admitiu-se que se deveriam apenas considerar expoentes nonegativos. Como relaxar esta exigncia? Acontece que, se o expoente tomar valores negativos, ento a base da potncia no pode ser nula, sob pena de o resultado ser innitamente grande. Ento, a nova especicao ser

4.7. DESENVOLVIMENTO DE CICLOS


/** Devolve a potncia n de x. @pre 0 n x = 0. @post potncia = xn . */ double potncia(double const x, int const n) { ... }

203

fcil vericar que a resoluo do problema exige o tratamento de dois casos distintos, resolveis atravs de uma instruo de seleco, ou, mais simplesmente, atravs do operador ? ::
/** Devolve a potncia n de x. @pre 0 n x = 0. @post potncia = xn . */ double potncia(double const x, int const n) { assert(0 <= n or x != 0.0); int const exp = n < 0 ? -n : n; // P C 0 exp. int i = 0; double r = 1.0; // CI r = xi 0 i exp. while(i != exp) { r *= x; ++i; } // CO r = xexp . return n < 0 ? 1.0 / r : r; }

ou ainda, convertendo para a instruo de iterao for e eliminando os comentrios,


/** Devolve a potncia n de x. @pre P C 0 n x = 0. @post CO potncia = xn . */ double potncia(double const x, int const n) { assert(0 <= n or x != 0.0); int const exp = n < 0 ? -n : n; double r = 1.0; for(int i = 0; i != exp; ++i) r *= x; return n < 0 ? 1.0 / r : r; }

204

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


1 xn ,

Um outra alternativa , sabendo que x n =

usar recursividade:

/** Devolve a potncia n de x. @pre P C 0 n x = 0. @post CO potncia = xn . */ double potncia(double const x, int const n) { assert(0 <= n or x != 0.0); if(n < 0) return 1.0 / potncia(x, -n); double r = 1.0; for(int i = 0; i != n; ++i) r *= x; return r; }

4.7.4 Metodologia de Dijkstra


A programao deve ser uma actividade orientada pelos objectivos. A metodologia de desenvolvimento de ciclos de Dijkstra [8][5] tenta integrar o desenvolvimento do cdigo com a demonstrao da sua correco, comeando naturalmente por prescrever olhar com ateno para a condio objectivo do ciclo. O primeiro passo do desenvolvimento de um ciclo a determinao de possveis condies invariantes por observao da condio objectivo, seguindo-se-lhe a determinao da guarda, da inicializao e do passo, normalmente decomposto na aco e no progresso: 1. Especicar o problema sem margem para ambiguidades: denir a pr-condio P C e a condio objectivo CO. 2. Tentar perceber se um ciclo , de facto, necessrio. Este passo muitas vezes descurado, com consequncias infelizes, como se ver. 3. Olhando para a condio objectivo, determinar uma condio invariante CI interessante para o ciclo. A condio invariante escolhida uma verso enfraquecida da condio objectivo CO. 4. Escolher uma guarda G tal que CI G CO. 5. Escolher uma inicializao inic de modo a garantir a veracidade da condio invariante logo no incio do ciclo (assumindo, claro est, que a sua pr-condio P C se verica). 6. Escolher o passo do ciclo de modo a que a condio invariante se mantenha verdadeira (i.e., de modo a garantir que de facto invariante). fundamental que a escolha do passo garanta a terminao do ciclo. Para isso o passo usualmente dividido num progresso prog e numa aco aco, sendo o progresso que garante a terminao do ciclo e a aco que garante que, apesar do progresso, a condio invariante CI de facto invariante.

4.7. DESENVOLVIMENTO DE CICLOS

205

As prximas seces detalham cada um destes passos para dois exemplos de funes a desenvolver. Especicao do problema A pr-condio P C e a condio objectivo CO devem indicar de um modo rigoroso e sem ambiguidade (tanto quanto possvel) quais os possveis estados do programa no incio de um pedao de cdigo e quais os possveis estados no seu nal. Podem-se usar pr-condies e condies objectivo para qualquer pedao de cdigo, desde uma simples instruo, at um ciclo ou mesmo uma rotina. Pretende-se aqui exemplicar o desenvolvimento de ciclos atravs da escrita de duas funes. Estas funes tm cada uma uma pr-condio e um condio objectivo, que no so em geral iguais pr-condio e condio objectivo do ou dos ciclos que, presumivelmente, elas contm, embora estejam naturalmente relacionadas. Funo somaDosQuadrados() Pretende-se escrever uma funo int somaDosQuadrados(int const n) que devolva a soma dos quadrados dos primeiros n inteiros no-negativos. A sua estrutura pode ser:
/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = ...; ... // CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . return soma_dos_quadrados; }

Repare-se que existem duas condies objectivo diferentes. A primeira faz parte do contracto da funo (ver Seco 3.2.4) e indica que valor a funo deve devolver. A segunda, que a que vai ser usada no desenvolvimento de ora em diante, indica que valor deve ter a varivel soma_dos_quadrados antes da instruo de retorno. Funo raizInteira() Pretende-se escrever uma funo int raizInteira(int x) que, assumindo que x no-negativo, devolva o maior inteiro menor ou igual raiz quadrada de x. Por exemplo, se x for 4 devolve 2, que a raiz exacta de 4, se for 3 devolve 1, pois o maior inteiro menor ou igual a 3 1,73 e se for 5 devolve 2, pois o maior inteiro menor ou igual a 5 2,24. A estrutura da funo pode ser:
/** Devolve a melhor aproximao inteira por defeito da raiz quadrada de x.

206

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


@pre P C 0eqx. @post 0 raizInteira x < raizInteira + 1, ou seja, 0 raizInteira 0 raizInteira2 x < (raizInteira + 1)2 . */ int raizInteira(int const x) { assert(0 <= x); int r = ...; ... // CO 0 r r2 x < (r + 1)2 . assert(0 <= r and r * r <= x and x < (r + 1) * (r + 1)); return r; }

Mais uma vez existem duas condies objectivo diferentes. A primeira faz parte do contracto da funo e a segunda, que a que vai ser usada no desenvolvimento de ora em diante, indica que valor deve ter a varivel r antes da instruo de retorno. a relao x < (r + 1) 2 que garante que o valor devolvido o maior inteiro menor ou igual a x. Determinando se um ciclo necessrio A discusso deste passo ser feita mais tarde, por motivos que caro claros a seu tempo. Para j admite-se que sim, que ambos os problemas devem ser resolvidos usando ciclos. Determinao da condio invariante e da guarda A escolha da condio invariante CI do ciclo faz-se sempre olhando para a sua condio objectivo CO. Esta escolha a fase mais difcil no desenvolvimento de um ciclo. No existem panaceias para esta diculdade: necessrio usar de intuio, arte, engenho, experincia, analogias com casos semelhantes, etc. Mas existe uma metodologia, desenvolvida por Edsger Dijkstra [5], que funciona bem para um grande nmero de casos. A ideia subjacente metodologia que a condio invariante se deve obter por enfraquecimento da condio objectivo, ou seja, CO CI. A ideia que, depois de terminado o ciclo, a condio invariante reforada pela negao da guarda G (o ciclo termina forosamente com a guarda falsa) garante que foi atingida a condio objectivo, ou seja, CI G CO.

Por enfraquecimento da condio objectivo CO entende-se a obteno de uma condio invariante CI tal que, de entre todas as combinaes de valores das variveis que a tornam verdadeira, contenha todas as combinaes de valores de variveis que tornam verdadeira a CO. Ou seja, o conjunto dos estados do programa que vericam CI deve conter o conjunto dos estados do programa que vericam a CO, que o mesmo que dizer CO CI. Apresentar-se-o aqui apenas dois dos vrios possveis mtodos para obter a condio invariante por enfraquecimento da condio objectivo:

4.7. DESENVOLVIMENTO DE CICLOS

207

1. Substituir uma das constantes presentes em CO por uma varivel de programa com limites apropriados, obtendo-se assim a CI. A maior parte das vezes substitui-se uma constante que seja limite de um quanticador. A negao da guarda G depois escolhida de modo a que CI G CO, o que normalmente conseguido escolhendo para G o predicado que arma que a nova varivel tem exactamente o valor da constante que substituiu. 2. Se CO corresponder conjuno de vrios termos, escolher parte deles para constiturem a condio invariante CI e outra parte para constiturem a negao da guarda G. Por exemplo, se CO C1 Cm , ento pode-se escolher por exemplo CI C 1 Cm1 e G Cm . Esta seleco conduz, por um lado, a uma condio invariante que obviamente uma verso enfraquecida da condio objectivo, e por outro lado vericao trivial da implicao CI G CO, pois neste caso CI G = CO. A este mtodo chama-se factorizao da condio objectivo. Muitas vezes s ao se desenvolver o passo do ciclo se verica que a condio invariante escolhida no apropriada. Nesse caso deve-se voltar atrs e procurar uma nova condio invariante mais apropriada. Substituio de uma constante por uma varivel Muitas vezes possvel obter uma condio invariante para o ciclo substituindo uma constante presente na condio objectivo por uma varivel de programa introduzida para o efeito. A constante pode corresponder a uma varivel que no seja suposto ser alterada pelo ciclo, i.e., a uma varivel que seja constante apenas do ponto de vista lgico. Voltando funo somaDosQuadrados(), evidente que o corpo da funo pode consistir num ciclo cuja condio objectivo j foi apresentada: CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . A condio invariante do ciclo pode ser obtida substituindo a constante n por uma nova varivel de programa i, com limites apropriados, cando portanto: CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. Como se escolheram os limites da nova varivel? Simples: um dos limites (normalmente o inferior) o primeiro valor de i para o qual o quanticador no tem nenhum termo (no h qualquer valor de j que verique 0 j < 0) e o outro limite (normalmente o superior) a constante substituda. Esta condio invariante tem um signicado claro: a varivel soma_dos_quadrados contm desde o incio ao m do ciclo a soma dos primeiros i inteiros no-negativos e a varivel i varia entre 0 e n. Claro est que a nova varivel tem de ser denida na funo:

208

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = ...; int i = ...; ... // CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . return soma_dos_quadrados; }

Em rigor, a condio invariante escolhida no corresponde simplesmente a uma verso enfraquecida da condio objectivo. Na verdade, a introduo de uma nova varivel feita em duas etapas, que normalmente se omitem. A primeira etapa obriga a uma reformulao da condio objectivo de modo a reectir a existncia da nova varivel, que aumenta a dimenso do espao de estados do programa: substituise a constante pela nova varivel, mas fora-se tambm a nova varivel a tomar o valor da constante que substituiu, acrescentando para tal uma conjuno condio objectivo CO soma_ dos_ quadrados = S j : 0 j < i : j 2 i = n A segunda etapa corresponde obteno da condio invariante por enfraquecimento de CO : o termo que xa o valor da nova varivel relaxado. A condio invariante obtida de facto mais fraca que a condio objectivo reformulada CO , pois aceita mais possveis valores para a varivel soma_dos_quadrados do que CO , que s aceita o resultado nal pretendido 19. Os valores aceites para essa varivel pela condio invariante correspondem a valores intermdios durante a execuo do ciclo. Neste caso correspondem a todas as possveis somas de quadrados de inteiros positivos desde a soma com zero termos at soma com os n termos pretendidos. A escolha da guarda simples: para negao da guarda G escolhe-se a conjuno que se acrescentou condio objectivo para se obter CO G i = n, de modo a garantir que CO CO.

Na realidade o enfraquecimento pode ser feito usando a factorizao! Para isso basta um pequeno truque na reformulao da condio objectivo de modo a incluir um termo extra: CO soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n i = n. Este novo termo no faz qualquer diferena efectiva em CO , mas permite aplicar facilmente a factorizao da condio objectivo:
CI G

19

CO soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n i = n .

4.7. DESENVOLVIMENTO DE CICLOS


ou seja, G i = n.

209

Dessa forma tem-se forosamente que (CI G) = CO CO, como pretendido. A funo neste momento
/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = ...; int i = ...; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. while(i != n) { passo } // CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . return soma_dos_quadrados; }

A escolha da guarda pode tambm ser feita observando que no nal do ciclo a guarda ser forosamente falsa e a condio invariante verdadeira (i.e., que CI G), e que se pretende, nessa altura, que a condio objectivo do ciclo seja verdadeira. Ou seja, sabe-se que no nal do ciclo soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n G e pretende-se que CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . A escolha mais simples da guarda que garante a vericao da condio objectivo G i = n, ou seja, G i = n. Ou seja, esta guarda quando for falsa garante que i = n, pelo que sendo a condio invariante verdadeira, tambm a condio objectivo o ser. Mais uma vez, s se pode conrmar se a escolha da condio invariante foi apropriada depois de completado o desenvolvimento do ciclo. Se o no tiver sido, h que voltar a este passo e tentar de novo. Isso no ser necessrio neste caso, como se ver.

importante que, pelas razes que se viram mais atrs, a guarda escolhida seja o mais fraca possvel. Neste caso pode-se-ia reforar a guarda relaxando a sua negao para G n i, que corresponde guarda G i < n muito mais forte e infelizmente to comum...

210

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Factorizao da condio objectivo Quando a condio objectivo composta pela conjuno de vrias condies, pode-se muitas vezes utilizar parte delas como negao da guarda G e a parte restante como condio invariante CI. Voltando funo raizInteira(), evidente que o corpo da funo pode consistir num ciclo cuja condio objectivo j foi apresentada: CO 0 r r2 x < (r + 1)2 0 r r2 x x < (r + 1)2

Escolhendo para negao da guarda o termo x < (r + 1) 2 , ou seja, escolhendo G (r + 1)2 x obtm-se para a condio invariante os termos restantes CI 0 r r2 x. que o mesmo que CI 0 r r x.

Esta escolha faz com que CI G seja igual condio objectivo CO, pelo que se o ciclo terminar termina com o valor correcto. A condio invariante escolhida tem um signicado claro: desde o incio ao m do ciclo que a varivel r no excede a raiz quadrada de x. Alm disso, a condio invariante , mais uma vez, uma verso enfraquecida da condio objectivo, visto que admite um maior nmero de possveis valores para a varivel r do que a condio objectivo, que s admite o resultado nal pretendido. A funo neste momento
/** Devolve a melhor aproximao inteira por defeito da raiz quadrada de x. @pre P C 0eqx. @post 0 raizInteira x < raizInteira + 1, ou seja, 0 raizInteira 0 raizInteira2 x < (raizInteira + 1)2 . */ int raizInteira(int const x) { assert(0 <= x); int r = ...; // CI 0 r r2 x. while((r + 1) * (r + 1) <= x) { passo } // CO 0 r r2 x < (r + 1)2 . assert(0 <= r and r * r <= x and x < (r + 1) * (r + 1)); return r; }

4.7. DESENVOLVIMENTO DE CICLOS


Escolha da inicializao

211

A inicializao de um ciclo feita normalmente de modo a, assumindo a veracidade da prcondio P C, garantir a vericao da condio invariante CI da forma mais simples possvel. Funo somaDosQuadrados() A condio invariante j determinada CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. pelo que a forma mais simples de se inicializar o ciclo com as instrues
int soma_dos_quadrados = 0; int i = 0;

pois nesse caso, por simples substituio, CI 0 = S j : 0 j < 0 : j 2 0 0 n 0=00n 0n

que verdadeira desde que a pr-condio P C 0 n o seja. A funo neste momento


/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = 0; int i = 0; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. while(i != n) { passo } // CO soma_ dos_ quadrados = S j : 0 j < n : j 2 . return soma_dos_quadrados; }

Funo raizInteira() A condio invariante j determinada CI 0 r r2 x. pelo que a forma mais simples de se inicializar o ciclo com a instruo

212
int r = 0;

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

pois nesse caso, por simples substituio, CI 0 0 0 x 0x que verdadeira desde que a pr-condio P C 0eqx o seja. A funo neste momento
/** Devolve a melhor aproximao inteira por defeito da raiz quadrada de x. @pre P C 0eqx. @post 0 raizInteira x < raizInteira + 1, ou seja, 0 raizInteira 0 raizInteira2 x < (raizInteira + 1)2 . */ int raizInteira(int const x) { assert(0 <= n); int r = 0; // CI 0 r r2 x. while((r + 1) * (r + 1) <= x) { passo } // CO 0 r r2 x < (r + 1)2 . assert(0 <= r and r * r <= x and x < (r + 1) * (r + 1)); return r; }

Determinao do passo: progresso e aco A construo de um ciclo ca completa depois de determinado o passo, normalmente constitudo pela aco aco e pelo progresso prog. O progresso escolhido de modo a garantir que o ciclo termina, i.e., de modo a garantir que ao m de um nmero nito de repeties do passo a guarda G se torna falsa. A aco escolhida de modo a garantir a invarincia de CI apesar do progresso. Funo somaDosQuadrados() A guarda j determinada G i=n pelo que o progresso mais simples a simples incrementao de i. Este progresso garante que o ciclo termina (i.e., que a guarda se torna falsa) ao m de no mximo n iteraes, uma vez que i foi inicializada com 0. Ou seja, o passo do ciclo

4.7. DESENVOLVIMENTO DE CICLOS


// CI G soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n i = n, ou seja, // soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i < n aco ++i; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n.

213

onde se assume a condio invariante como verdadeira antes da aco, se sabe que a guarda verdadeira nesse mesmo lugar (de outra forma o ciclo teria terminado) e se pretende escolher uma aco de modo a que a condio invariante seja verdadeira tambm depois do passo, ou melhor, apesar do progresso. Usando a semntica da operao de atribuio, pode-se deduzir a condio mais fraca antes do progresso de modo a que condio invariante seja verdadeira no nal do passo:
// CI G soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n i = n, ou seja, // soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i < n aco // soma_ dos_ quadrados = S j : 0 j < i + 1 : j 2 0 i + 1 n, ou seja, // soma_ dos_ quadrados = S j : 0 j < i + 1 : j 2 1 i < n. ++i; // o mesmo que i = i + 1; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n.

Falta pois determinar uma aco tal que


// soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i < n aco // soma_ dos_ quadrados = S j : 0 j < i + 1 : j 2 1 i < n.

Para simplicar a determinao da aco, pode-se fortalecer um pouco a sua condio objectivo forando i a ser maior do que zero, o que permite extrair o ltimo termo do somatrio
// soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i < n aco // soma_ dos_ quadrados = S j : 0 j < i : j 2 + i2 0 i < n // soma_ dos_ quadrados = S j : 0 j < i + 1 : j 2 1 i < n

A aco mais simples limita-se a acrescentar i2 ao valor da varivel soma_dos_quadrados:


soma_dos_quadrados += i * i;

pelo que o o ciclo e a funo completos so

214

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post CO somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = 0; int i = 0; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. while(i != n) { soma_dos_quadrados += i * i; ++i; } return soma_dos_quadrados; }

ou, substituindo pelo ciclo com a instruo for equivalente,


/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post CO somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n); int soma_dos_quadrados = 0; // CI soma_ dos_ quadrados = S j : 0 j < i : j 2 0 i n. for(int i = 0; i != n; ++i) soma_dos_quadrados += i * i; return soma_dos_quadrados; }

Nas verses completas da funo incluiu-se um comentrio com a condio invariante CI. Este um comentrio normal, e no de documentao. Os comentrios de documentao, aps /// ou entre /** */, servem para documentar a interface da funo ou procedimento (ou outras entidades), i.e., para dizer claramente o que essas entidades fazem, para que servem ou como se comportam, sempre de um ponto de vista externo. Os comentrios normais, pelo contrrio, servem para que o leitor do saiba como o cdigo, neste caso a funo, funciona. Sendo a condio invariante a mais importante das asseres associadas a um ciclo, naturalmente candidata a gurar num comentrio na verso nal das funes ou procedimentos. Funo raizInteira() A guarda j determinada G (r + 1)2 x

4.7. DESENVOLVIMENTO DE CICLOS

215

pelo que o progresso mais simples a simples incrementao de r. Este progresso garante que o ciclo termina (i.e., que a guarda se torna falsa) ao m de um nmero nito de iteraes (quantas?), uma vez que r foi inicializada com 0. Ou seja, o passo do ciclo
// CI G 0 r r2 x (r + 1)2 x, ou seja, // 0 r (r + 1)2 x, porque r2 (r + 1)2 sempre que 0 r. aco ++r; // CI 0 r r2 x.

onde se assume a condio invariante como verdadeira antes da aco, se sabe que a guarda verdadeira nesse mesmo lugar (de outra forma o ciclo teria terminado) e se pretende escolher uma aco de modo a que a condio invariante seja verdadeira tambm depois do passo, ou melhor, apesar do progresso. Usando a semntica da operao de atribuio, pode-se deduzir a condio mais fraca antes do progresso de modo a que condio invariante seja verdadeira no nal do passo:
// 0 r (r + 1)2 x. aco // 0 r + 1 (r + 1)2 x, ou seja, 1 r (r + 1)2 x. ++r; // o mesmo que r = r + 1; // CI 0 r r2 x.

Como 0 r 1 r, ento evidente que neste caso no necessria qualquer aco, pelo que o ciclo e a funo completos so
/** Devolve a melhor aproximao inteira por defeito da raiz quadrada de x. @pre P C 0eqx. @post CO 0 raizInteira x < raizInteira + 1, ou seja, 0 raizInteira 0 raizInteira2 x < (raizInteira + 1)2 . */ int raizInteira(int const x) { assert(0 <= x); int r = 0; // CI 0 r r2 x. while((r + 1) * (r + 1) <= x) ++r; assert(0 <= r and r * r <= x and x < (r + 1) * (r + 1)); return r; }

A guarda do ciclo pode ser simplicada se se adiantar a varivel r de uma unidade

216

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


/** Devolve a melhor aproximao inteira por defeito da raiz quadrada de x. @pre P C 0eqx. @post CO 0 raizInteira x < raizInteira + 1, ou seja, 0 raizInteira 0 raizInteira2 x < (raizInteira + 1)2 . */ int raizInteira(int const x) { assert(0 <= x); int r = 1; // CI 1 r (r 1)2 x. while(r * r <= x) ++r; assert(1 <= r and (r - 1) * (r - 1) <= x and x < r * r); return r - 1; }

Determinando se um ciclo necessrio Deixou-se para o m a discusso deste passo, que em rigor deveria ter tido lugar logo aps a especicao do problema. que essa a tendncia natural do programador principiante... Considere-se de novo a funo int somaDosQuadrados(int const n). Ser mesmo necessrio um ciclo? Acontece que a soma dos quadrados dos primeiros n inteiros no-negativos, pode ser expressa simplesmente por 20 n(n 1)(2n 1) . 6 No necessrio qualquer ciclo na funo, que pode ser implementada simplesmente como 21 :
/** Devolve a soma dos quadrados dos primeiros n inteiros no-negativos. @pre P C 0 n. @post CO somaDosQuadrados = S j : 0 j < n : j 2 . */ int somaDosQuadrados(int const n) { assert(0 <= n);
20

A demonstrao faz-se utilizando a propriedade telescpica dos somatrios


n1

(f (j) f (j 1)) = f (n 1) f (1)


j=0

com f (j) = j 3 . 21 Porqu devolver n * (n - 1) / 2 * (2 * n - 1) / 3 e no n * (n - 1) * (2 * n - 1) / 6? Lembre-se das limitaes dos inteiros em C++.

4.7. DESENVOLVIMENTO DE CICLOS

217

return n * (n - 1) / 2 * (2 * n - 1) / 3; }

4.7.5 Um exemplo
Pretende-se desenvolver uma funo que devolva verdadeiro se o valor do seu argumento inteiro no-negativo n for primo e falso no caso contrrio. Um nmero inteiro no-negativo primo se for apenas divisvel por 1 e por si mesmo. O inteiro 1 classicado como no-primo, o mesmo acontecendo com o inteiro 0. O primeiro passo da resoluo do problema a sua especicao, ou seja, a escrita da estrutura da funo, incluindo a pr-condio e a condio objectivo:
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); ... }

Como abordar este problema? Em primeiro lugar, conveniente vericar que os valores 0 e 1 so casos particulares. O primeiro porque divisvel por todos os inteiros positivos e portanto no pode ser primo. O segundo porque, apesar de s ser divisvel por 1 e por si prprio, no considerado, por conveno, um nmero primo. Estes casos particulares podem ser tratados com recurso a uma simples instruo condicional, pelo que se pode reescrever a funo como
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre 0 n. @post Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); if(n == 0 || n == 1) return false; // P C 2 n. bool _primo = ...; ... // CO _ primo = (Q j : 2 j < n : n j = 0). return _primo; }

218

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

uma vez que, se a guarda da instruo condicional for falsa, e admitindo que a pr-condio da funo 0 n verdadeira, ento forosamente 2 n depois da instruo condicional. Por outro lado, a condio objectivo antes da instruo de retorno no nal da funo pode ser simplicada dada a nova pr-condio, mais forte que a da funo. Aproveitou-se para introduzir uma varivel booleana que, no nal da funo, dever conter o valor lgico apropriado a devolver pela funo. Assim, o problema resume-se a escrever o cdigo que garante que CO se verica sempre que P C se vericar. Um ciclo parece ser apropriado para resolver este problema, pois para vericar se um nmero n primo pode-se ir vericando se divisvel por algum inteiro entre 2 e n 1. A condio invariante do ciclo pode ser obtida substituindo o limite superior da conjuno (a constante n) por uma varivel i criada para o efeito, e com limites apropriados. Obtm-se a seguinte estrutura para o ciclo
// P C 2 n. bool _primo = ...; int i = ...; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. while(G) { passo } // CO _ primo = (Q j : 2 j < n : n j = 0).

O que signica a condio invariante? Simplesmente que a varivel _primo tem valor lgico verdadeiro se e s se no existirem divisores de n superiores a 1 e inferiores a i, variando i entre 2 e n. Ou melhor, signica que, num dado passo do ciclo, j se testaram todos os potenciais divisores de n entre 2 e i exclusive. A escolha da guarda muito simples: quando o ciclo terminar CI G deve implicar CO. Isso consegue-se simplesmente fazendo G i = n, ou seja, G i = n. Quanto inicializao, tambm simples, pois basta atribuir 2 a i para que a conjuno no tenha qualquer termo e portanto tenha valor lgico verdadeiro. Assim, a inicializao :
bool _primo = true; int i = 2;

pelo que o ciclo ca


// P C 2 n. bool _primo = true; int i = 2;

4.7. DESENVOLVIMENTO DE CICLOS


// CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. while(i != n) { passo } // CO _ primo = (Q j : 2 j < n : n j = 0).

219

Que progresso utilizar? A forma mais simples de garantir a terminao do ciclo simplesmente incrementar i. Dessa forma, como i comea com valor 2 e 2 npela pr-condio, a guarda torna-se falsa, e o ciclo termina, ao m de exactamente n 2 iteraes do ciclo. Assim, o ciclo ca
// P C 2 n. bool _primo = true; int i = 2; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. while(i != n) { aco ++i; } // CO _ primo = (Q j : 2 j < n : n j = 0).

Finalmente, falta determinar a aco a executar para garantir a veracidade da condio invariante apesar do progresso realizado. No incio do passo admite-se que a condio verdadeira e sabe-se que a guarda o tambm: ou seja, CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i n i = n, CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i < n. Por outro lado, no nal do passo deseja-se que a condio invariante seja verdadeira. Logo, o passo com as respectivas asseres :
// Aqui admite-se que CIG _ primo = (Q j : 2 j < i : n j = 0)2 i < n. aco ++i; // Aqui pretende-se que CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n.

Antes de determinar a aco, conveniente vericar qual a pr-condio mais fraca do progresso que necessrio impor para que, depois dele, se verique a condio invariante do ciclo. Usando a transformao de variveis correspondente atribuio i = i + 1 (equivalente a ++i), chega-se a:
// CI aco // _ // _ ++i; // CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i < n. primo = (Q j : 2 j < i + 1 : n j = 0) 2 i + 1 n, ou seja, primo = (Q j : 2 j < i + 1 : n j = 0) 1 i < n. _ primo = (Q j : 2 j < i : n j = 0) 2 i n.

220

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Por outro lado, se 2 i, pode-se extrair o ltimo termo da conjuno. Assim, reforando a pr-condio do progresso,
// CI aco // _ // _ ++i; // CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i < n. primo = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. primo = (Q j : 2 j < i + 1 : n j = 0) 1 i < n. _ primo = (Q j : 2 j < i : n j = 0) 2 i n.

Assim sendo, a aco deve ser tal que


// _ primo = (Q j : 2 j < i : n j = 0) 2 i < n. aco // _ primo = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n.

evidente, ento, que a aco pode ser simplesmente:


_primo = (_primo and n % i != 0);

onde os parnteses so dispensveis dadas as regras de precedncia dos operadores em C++ (ver Seco 2.7.7). A correco da aco determinada pode ser vericada facilmente calculando a respectiva prcondio mais fraca
// (_ primo n i = 0) = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. _primo = (_primo and n % i != 0); // _ primo = (Q j : 2 j < i : n j = 0) n i = 0 2 i < n.

e observando que CI G leva forosamente sua vericao: _ primo = (Q j : 2 j < i : n j = 0) 2 i < n

(_ primo n i = 0) = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. Escrevendo agora o ciclo completo tem-se:


// P C 2 n. bool _primo = true; int i = 2; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. while(i != n) { _primo = _primo and n % i != 0; ++i; } // CO _ primo = (Q j : 2 j < n : n j = 0).

4.7. DESENVOLVIMENTO DE CICLOS


Reforo da guarda

221

A observao atenta deste ciclo revela que a varivel _primo, se alguma vez se tornar falsa, nunca mais deixar de o ser. Tal deve-se a que F o elemento neutro da conjuno. Assim, evidente que o ciclo poderia terminar antecipadamente, logo que essa varivel tomasse o valor F: o ciclo s deveria continuar enquanto G _ primo i = n. Ser que esta nova guarda, reforada com uma conjuno, mesmo apropriada? Em primeiro lugar necessrio demonstrar que, quando o ciclo termina, se atinge a condio objectivo. Ou seja, ser que CI G CO? Neste caso tem-se CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i n (_ primo i = n). Considere-se o ltimo termo da conjuno, ou seja, a disjuno _ primo i = n. Quando o ciclo termina pelo menos um dos termos da disjuno verdadeiro. Suponha-se que _ primo = V. Ento, como _ primo = (Q j : 2 j < i : n j = 0) tambm verdadeira no nal do ciclo, tem-se que (Q j : 2 j < i : n j = 0) falsa. Mas isso implica que (Q j : 2 j < n : n j = 0), pois se no verdade que no h divisores de n entre 2 e i exclusive, ento tambm no verdade que no os haja entre 2 e n exclusive, visto que i n. Ou seja, a condio objectivo CO _ primo = (Q j : 2 j < n : n j = 0) CO F = F CO V

verdadeira! O outro caso, se i = n, idntico ao caso sem reforo da guarda. Logo, a condio objectivo de facto atingida em qualquer dos casos 22 . Quanto ao passo (aco e progresso), evidente que o reforo da guarda no altera a sua validade. No entanto, instrutivo determinar de novo a aco do passo. A aco deve garantir a veracidade da condio invariante apesar do progresso. No incio do passo admite-se que a condio verdadeira e sabe-se que a guarda o tambm: CI G _ primo = (Q j : 2 j < i : n j = 0) 2 i n _ primo i = n (Q j : 2 j < i : n j = 0) 2 i < n _ primo Por outro lado, no nal do passo deseja-se que a condio invariante seja verdadeira. Logo, o passo com as respectivas asseres :
// Aqui admite-se que CI G (Q j : 2 j < i : n j = 0) 2 i < n _ primo. aco ++i; // Aqui pretende-se que CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n.
Se no lhe pareceu claro lembre-se que (A B) C o mesmo que A C B C e que a (B C) o mesmo que (A B) (A C).
22

222

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Antes de determinar a aco, conveniente vericar qual a pr-condio mais fraca do progresso que necessrio impor para que, depois dele, se verique a condio invariante do ciclo. Usando a transformao de variveis correspondente atribuio i = i + 1 (equivalente a ++i), chega-se a:
// CI G (Q j : 2 j < i : n j = 0) 2 i < n _ primo. aco // _ primo = (Q j : 2 j < i + 1 : n j = 0) 1 i < n. ++i; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n.

Por outro lado, se 2 i, pode-se extrair o ltimo termo da conjuno. Assim, reforando a pr-condio do progresso,
// CI aco // _ // _ ++i; // CI G (Q j : 2 j < i : n j = 0) 2 i < n _ primo. primo = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. primo = (Q j : 2 j < i + 1 : n j = 0) 1 i < n. _ primo = (Q j : 2 j < i : n j = 0) 2 i n.

Assim sendo, a aco deve ser tal que


// (Q j : 2 j < i : n j = 0) 2 i < n _ primo. aco // _ primo = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n.

evidente, ento, que a aco pode ser simplesmente:


_primo = n % i != 0;

A aco simplicou-se relativamente aco no ciclo com a guarda original. A alterao da aco pode ser percebida observando que, sendo a guarda sempre verdadeira no incio do passo do ciclo, a varivel _primo tem a sempre o valor verdadeiro, pelo que a aco antiga tinha uma conjuno com a veracidade. Como V P o mesmo que P , pois V o elemento neutro da conjuno, a conjuno pode ser eliminada. A correco da aco determinada pode ser vericada facilmente calculando a respectiva prcondio mais fraca:
// (n i = 0) = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. _primo = n % i != 0; // _ primo = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n.

4.7. DESENVOLVIMENTO DE CICLOS


e observando que CI G leva forosamente sua vericao: (Q j : 2 j < i : n j = 0) 2 i < n _ primo

223

(n i = 0) = ((Q j : 2 j < i : n j = 0) n i = 0) 2 i < n. Escrevendo agora o ciclo completo tem-se:


// P C 2 n. bool _primo = true; int i = 2; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. while(_primo and i != n) { _primo = n % i != 0; ++i; } // CO _ primo = (Q j : 2 j < n : n j = 0).

Verso nal Convertendo para a instruo for e inserindo o ciclo na funo tem-se:
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); if(n == 0 || n == 1) return false; bool _primo = true; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. for(int i = 2; _primo and i != n; ++i) _primo = n % i != 0; return _primo; }

A funo inclui desde o incio uma instruo de assero que verica a veracidade da prcondio. E quanto condio objectivo? A condio objectivo envolve um quanticador, pelo que no possvel exprimi-la na forma de uma expresso booleana em C++, excepto recorrendo a funes auxiliares. Como resolver o problema? Uma hiptese passa por reectir na condio objectivo apenas os termos da condio objectivo que no envolvem quanticadores:

224

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


assert(not _primo or 2 <= n);

Recorde-se que A B o mesmo que A B. A expresso da assero verica portanto se _ primo 2 n.

Esta assero pouco til, pois deixa passar como primos ou no primos todos os nmeros no primos superiores a 2... Pode-se fazer melhor. Todos os inteiros positivos podem ser escritos na forma 6k l, com l = 0, . . . , 5 e k inteiro positivo. imediato que os inteiros das formas 6k, 6k 2, 6k 3 e 6k 4 no podem ser primos (com excepo de 2 e 3). Ou seja, os nmeros primos ou so o 2 ou o 3, ou so sempre de uma das formas 6k 1 ou 6k 5. Assim, pode-se reforar um pouco a assero nal para:
assert(((n != 2 and n != 3) or _primo) and (not _primo or n == 2 or n == 3 or (2 <= n and ((n + 1) % 6 == 0 or (n + 5) % 6 == 0))));

que, expressa em notao matemtica ca (_ primo (n = 2n = 3 (2 n ((n + 1) 6 = 0 (n + 5) 6 = 0)))) A funo ca ento:


/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); if(n == 0 || n == 1) return false; bool _primo = true; // CI _ primo = (Q j : 2 j < i : n j = 0) 2 i n. for(int i = 2; _primo and i != n; ++i) _primo = n % i != 0; assert(n == 2 or n == 3 or (5 <= n and ((n - 1) % 6 == 0 or (n - 5) % 6 == 0))); assert(((n != 2 and n != 3) or _primo) and (not _primo or n == 2 or n == 3 or (2 <= n and ((n + 1) % 6 == 0 or (n + 5) % 6 == 0)))); return _primo; }

((n = 2 n = 3) _ primo)

4.7. DESENVOLVIMENTO DE CICLOS

225

Desta forma continuam a no se detectar muitos possveis erros, mas passaram a detectar-se bastante mais do que inicialmente. Finalmente, uma observao atenta da funo revela que ainda pode ser simplicada (do ponto de vista da sua escrita) para
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre P C 0 n. @post CO Primo = ((Q j : 2 j < n : n j = 0) 2 n). */ bool Primo(int const n) { assert(0 <= n); if(n == 0 || n == 1) return false; // CI (Q j : 2 j < i : n j = 0) 2 i n. for(int i = 2; i != n; ++i) if(n % i != 0) return false; return true; }

Este ltimo formato muito comum em programas escritos em C ou C++. A demonstrao da correco de ciclos incluindo sadas antecipadas no seu passo um pouco mais complicada e ser abordada mais tarde. Outra abordagem Como se viu, o valor 2 n primo se nenhum inteiro entre 2 e n exclusive o dividir. Mas pode-se pensar neste problema de outra forma mais interessante. Considere-se o conjunto D de todos os possveis divisores de n. Claramente o prprio n sempre membro deste conjunto, n D. Se n for primo, o conjunto tem apenas como elemento o prprio n, i.e., C = {n}. Se n no for primo existiro outros divisores no conjunto, forosamente inferiores ao prprio n. Considere-se o mnimo desse conjunto. Se n for primo, o mnimo o prprio n. Se n no for primo, o mnimo forosamente diferente de n. Ou seja, a armao n primo tem o mesmo valor lgico que o mais pequeno dos divisores no-unitrios de n o n. Regresse-se estrutura da funo:
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre 0 n. @post Primo = (min {2 j n : n j = 0} = n 2 n). */ bool Primo(int const n) {

226
assert(0 <= n);

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

if(n == 0 || n == 1) return false; // P C 2 n. ... return ...; // CO Primo = min {2 j n : n j = 0} = n.

Introduza-se uma varivel i que se assume conter o mais pequeno dos divisores no-unitrios de n no nal da funo. Nesse caso a funo dever devolver o valor lgico de i = n. Ento pode-se reescrever a funo:
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre 0 n. @post Primo = (min {2 j n : n j = 0} = n 2 n). */ bool Primo(int const n) { assert(0 <= n);

if(n == 0 || n == 1) return false; // P C 2 n. int i = ...; ... // CO i = min {2 j n : n j = 0}. return i == n; // Primo = (min {2 j n : n j = 0} = n).

O problema reduz-se pois a escrever um ciclo que, dada a pr-condio P C, garanta que no seu nal se verique a nova CO. A condio objectivo pode ser reescrita numa forma mais simptica. Se i for o menor dos divisores de n entre 2 e n inclusive, ento 1. i est entre 2 e n inclusive. 2. i tem de ser divisor de n, i.e., n i = 0, e

3. nenhum outro inteiro inferior a i e superior ou igual a 2 divisor de n, i.e., (Q j : 2 j < i : n j = 0). Traduzindo para notao matemtica: CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n.

4.7. DESENVOLVIMENTO DE CICLOS

227

Como a nova condio objectivo uma conjuno, pode-se tentar obter a condio invariante e a negao da guarda do ciclo por factorizao. A escolha mais evidente faz do primeiro termo a guarda e do segundo a condio invariante
G CI

CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n, conduzindo ao seguinte ciclo


// P C 2 n. int i = ...; // CI (Q j : 2 j < i : n j = 0) 2 i n. while(n % i != 0) { passo } // CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n.

A escolha da inicializao simples. Como a conjuno de zero termos verdadeira, basta fazer
int i = 2;

pelo que o ciclo ca


// P C 2 n. int i = 2; // CI (Q j : 2 j < i : n j = 0) 2 i n. while(n % i != 0) { passo } // CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n.

Mais uma vez o progresso mais simples a incrementao de i


// P C 2 n. int i = 2; // CI (Q j : 2 j < i : n j = 0) 2 i n. while(n % i != 0) { aco ++i; } // CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n.

Com este progresso o ciclo termina na pior das hipteses com i = n. Que aco usar? Antes da aco sabe-se que a guarda verdadeira e admite-se que a condio invariante o tambm. Depois do progresso pretende-se que a condio invariante seja verdadeira:

228

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


// CI G (Q j : 2 j < i : n j = 0) 2 i n n i = 0. aco ++i; // CI (Q j : 2 j < i : n j = 0) 2 i n.

Como habitualmente, comea por se determinar a pr-condio mais fraca do progresso que garante a vericao da condio invariante no nal do passo:
// CI G (Q j : 2 j < i : n j = 0) 2 i n n i = 0, // que, como n i = 0 implica que i = n, o mesmo que // (Q j : 2 j < i : n j = 0) n i = 0 2 i < n. aco // (Q j : 2 j < i + 1 : n j = 0) 2 i + 1 n, ou seja, // (Q j : 2 j < i + 1 : n j = 0) 1 i < n. ++i; // CI (Q j : 2 j < i : n j = 0) 2 i n.

Fortalecendo a pr-condio do progresso de modo garantir que 2 i, pode-se extrair o ltimo termo da conjuno:
// (Q j : 2 j < i : n j = 0) n i = 0 2 i < n. aco // (Q j : 2 j < i : n j = 0) n i = 0 2 i < n, ou seja, // (Q j : 2 j < i + 1 : n j = 0) 1 i < n. ++i; // CI (Q j : 2 j < i : n j = 0) 2 i n.

pelo que a aco pode ser a instruo nula! O ciclo ca pois


// P C 2 n. int i = 2; // CI (Q j : 2 j < i : n j = 0) 2 i n. while(n % i != 0) ++i; // CO n i = 0 (Q j : 2 j < i : n j = 0) 2 i n.

e a funo completa
/** Devolve V se n for um nmero primo e F no caso contrrio. @pre 0 n. @post Primo = (min {2 j n : n j = 0} = n 2 n). */ bool Primo(int const n) {

4.7. DESENVOLVIMENTO DE CICLOS


assert(0 <= n); if(n == 0 || n == 1) return false; int i = 2; // CI (Q j : 2 j < i : n j = 0) 2 i n. while(n % i != 0) ++i; assert(((n != 2 and n != 3) or (i == n)) and (i != n or n == 2 or n == 3 or (2 <= n and ((n + 1) % 6 == 0 or (n + 5) % 6 == 0)))); return i == n; }

229

A instruo de assero para vericao da condio objectivo foi obtida por simples adaptao da obtida na seco anterior. Discusso H inmeras solues para cada problema. Neste caso comeou-se por uma soluo simples mas ineciente, aumentou-se a ecincia recorrendo ao reforo da guarda e nalmente, usando uma forma diferente de expressar a condio objectivo, obteve-se um ciclo mais eciente que os iniciais, visto que durante o ciclo necessrio fazer apenas uma comparao e uma incrementao. Mas h muitas outras solues para este mesmo problema, e mais ecientes. Recomenda-se que o leitor tente resolver este problema depois de aprender sobre matrizes, no Captulo 5. Experimente procurar informao sobre um velho algoritmo chamado a peneira de Eratstenes.

4.7.6 Outro exemplo


Suponha-se que se pretende desenvolver um procedimento que, dados dois inteiros como argumento, um o dividendo e outro o divisor, sendo o dividendo no-negativo e o divisor positivo, calcule o quociente e o resto da diviso inteira do dividendo pelo divisor e os guarde em variveis externas ao procedimento (usando passagem de argumentos por referncia). Para que o problema tenha algum interesse, no se pode recorrer aos operadores *, / e % do C++, nem to pouco aos operadores de deslocamento bit-a-bit. A estrutura do procedimento (ver Seco 4.6.6)
/** Coloca em q e r respectivamente o quociente e o resto da diviso inteira de dividendo por divisor.

230

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS


@pre P C 0 dividendo 0 < divisor. @post CO 0 r < divisor dividendo = q divisor + r. void divide(int const dividendo, int const divisor, int& q, int& r) { assert(0 <= dividendo and 0 < divisor); ... assert(0 <= r and r < divisor and dividendo = q * divisor + r); }

A condio objectivo pode ser vista como uma denio da diviso inteira. No s o quociente q multiplicado pelo divisor e somado do resto r tem de resultar no dividendo, como o resto tem de ser no-negativo e menor que o divisor (seno no estaria completa a diviso!), existindo apenas uma soluo nestas circunstncias. Como evidente a diviso tem de ser conseguida atravs de um ciclo. Qual ser a sua condio invariante? Neste caso, como a condio objectivo a conjuno de trs termos CO 0 r r < divisor dividendo = q divisor + r, a soluo passa por obter a condio invariante e a negao da guarda por factorizao da condio objectivo. Mas quais das proposies usar para G e quais usar para CI? Um pouco de experimentao e alguns falhanos levam a que se perceba que a negao da guarda deve corresponder ao segundo termo da conjuno, ou seja, reordenando os termos da conjuno,
G CI

CO r < divisor dividendo = q divisor + r 0 r, Dada esta escolha, a forma mais simples de inicializar o ciclo fazendo
r = dividendo; q = 0;

pois substituindo os valores na condio invariante obtm-se CI dividendo = 0 divisor + dividendo 0 dividendo,

que verdadeira dado que a pr-condio garante que dividendo no-negativo. O ciclo ter portanto a seguinte forma:

4.7. DESENVOLVIMENTO DE CICLOS


r = dividendo; q = 0; // CI dividendo = q divisor + r 0 r. while(divisor <= r) { passo }

231

O progresso deve ser tal que a guarda se torne falsa ao m de um nmero nito de iteraes do ciclo. Neste caso claro que basta ir reduzindo sempre o valor do resto para que isso acontea. A forma mais simples de o fazer decrement-lo:
--r;

Para que CI seja de facto invariante, h que escolher uma aco tal que:
// Aqui admite-se que: // CI G dividendo = q divisor + r 0 r divisor r, ou seja, // dividendo = q divisor + r divisor r. aco --r; // Aqui pretende-se que CI dividendo = q divisor + r 0 r.

Antes de determinar a aco, verica-se qual a pr-condio mais fraca do progresso que leva veracidade da condio invariante no nal do passo:
// dividendo = q divisor + r 1 0 r 1. --r; // o mesmo que r = r - 1; // CI dividendo = q divisor + r 0 r.

A aco deve portanto ser tal que


// dividendo = q divisor + r divisor r. aco // dividendo = q divisor + r 1 1 r.

As nicas variveis livres no procedimento so r e q, pelo que se o progresso fez evoluir o valor de r, ento a aco dever actuar sobre q. Dever pois ter o formato:
q = expresso;

Calculando a pr-condio mais fraca da aco:


// dividendo = expresso divisor + r 1 1 r. q = expresso; // dividendo = q divisor + r 1 1 r.

232 a expresso deve ser tal que

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

// dividendo = q divisor + r divisor r. // dividendo = expresso divisor + r 1 1 r.

evidente que a primeira das asseres s implica a segunda se divisor = 1. Como se pretende que o ciclo funcione para qualquer divisor positivo, houve algo que falhou. Uma observao mais cuidada do passo leva a compreender que o progresso no pode fazer o resto decrescer de 1 em 1, mas de divisor em divisor! Se assim for, o passo
// CI G dividendo = q divisor + r divisor r. aco r -= divisor; // CI dividendo = q divisor + r 0 r.

e calculando de novo a pr-condio mais fraca do progresso


// dividendo = q divisor + r divisor 0 r divisor, ou seja, // dividendo = (q 1) divisor + r divisor r. r -= divisor; // o mesmo que r = r - divisor; // CI dividendo = q divisor + r 0 r.

A aco deve portanto ser tal que


// dividendo = q divisor + r divisor r. aco // dividendo = (q 1) divisor + r divisor r.

evidente ento que a aco pode ser simplesmente:


++q;

Tal pode ser conrmado determinando a pr-condio mais fraca da aco


// dividendo = (q + 1 1) divisor + r divisor r, ou seja, // dividendo = q divisor + r divisor r. ++q; // o mesmo que q = q + 1; // dividendo = (q 1) divisor + r divisor r.

e observando que CI G implica essa pr-condio. A implicao trivial neste caso, pois as duas asseres so idnticas! Transformando o ciclo num ciclo for e colocando no procedimento:

4.7. DESENVOLVIMENTO DE CICLOS


/** Coloca em q e r respectivamente o quociente e o resto da diviso inteira de dividendo por divisor. @pre P C 0 dividendo 0 < divisor. @post CO 0 r < divisor dividendo = q divisor + r. void divide(int const dividendo, int const divisor, int& q, int& r) { assert(0 <= dividendo and 0 < divisor); r = dividendo; q = 0; // CI dividendo = q divisor + r 0 r. while(divisor <= r) { ++q; r -= divisor; } assert(0 <= r and r < divisor and dividendo = q * divisor + r); }

233

que tambm comum ver escrito em C++ como


/** Coloca em q e r respectivamente o quociente e o resto da diviso inteira de dividendo por divisor. @pre P C 0 dividendo 0 < divisor. @post CO 0 r < divisor dividendo = q divisor + r. void divide(int const dividendo, int const divisor, int& q, int& r) { // CI dividendo = q divisor + r 0 r. for(r = dividendo, q = 0; divisor <= r; ++q, r -= divisor) ; assert(0 <= r and r < divisor and dividendo = q * divisor + r); }

onde o progresso da instruo for contm o passo completo. Esta uma expresso idiomtica do C++ pouco clara, e por isso pouco recomendvel, mas que ilustra a utilizao de um novo operador: a vrgula usada para separar o progresso e a aco do ciclo o operador de sequenciamento do C++. Esse operador garante que o primeiro operando calculado antes do segundo operando, e tem como resultado o valor do segundo operando. Por exemplo, as instrues
int n = 0; n = (1, 2, 3, 4);

234 colocam o valor 4 na varivel n.

CAPTULO 4. CONTROLO DO FLUXO DOS PROGRAMAS

Podem-se desenvolver algoritmos mais ecientes para a diviso inteira se se considerarem progressos que faam diminuir o valor do resto mais depressa. Uma ideia interessante subtrair ao resto no apenas o divisor, mas o divisor multiplicado por uma potncia to grande quanto possvel de 2.

Captulo 5

Matrizes, vectores e outros agregados


muitas vezes conveniente para a resoluo de problemas ter uma forma de guardar agregados de valores de um determinado tipo. Neste captulo apresentam-se duas alternativas para a representao de agregados em C++: as matrizes e os vectores. As primeiras fazem parte da linguagem propriamente dita e so bastante primitivas e pouco exveis. No entanto, h muitos problemas para os quais so a soluo mais indicada, alm de que existe muito cdigo C++ escrito que as utiliza, pelo que a sua aprendizagem importante. Os segundos no fazem parte da linguagem. So fornecidos pela biblioteca padro do C++, que fornece um conjunto vasto de ferramentas que, em conjunto com a linguagem propriamente dita, resultam num potente ambiente de programao. Os vectores so consideravelmente mais exveis e fceis de usar que as matrizes, pelo que estas sero apresentadas primeiro. No entanto, as seces sobre matrizes de vectores tm a mesma estrutura, pelo que o leitor pode optar por ler as seces pela ordem inversa da apresentao. Considere-se o problema de calcular a mdia de trs valores de vrgula utuante dados e mostrar cada um desses valores dividido pela mdia calculada. Um possvel programa para resolver este problema :
#include <iostream> using namespace std; int main() { // Leitura: cout < < "Introduza trs valores: "; double a, b, c; cin > > a > > b > > c; // Clculo da mdia: double const mdia = (a + b + c) / 3.0; // Diviso pela mdia:

235

236
a /= mdia; b /= mdia; c /= mdia;

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

// Escrita do resultado: cout < < a < < < < b < < < < c < < endl; }

O programa simples e resolve bem o problema em causa. Mas ser facilmente generalizvel? E se se pretendesse dividir, j no trs, mas 100 ou mesmo 1000 valores pela sua mdia? A abordagem seguida no programa acima seria no mnimo pouco elegante, j que seriam necessrias tantas variveis quantos os valores a ler do teclado, e estas variveis teriam de ser todas denidas explicitamente. Convm usar um agregado de variveis.

5.1 Matrizes clssicas do C++


A generalizao de este tipo de problemas faz-se recorrendo a uma das possveis formas de agregado que so as matrizes clssicas do C++ 1 . Uma matriz um agregado de elementos do mesmo tipo que podem ser indexados (identicados) por nmeros inteiros e que podem ser interpretados e usados como outra varivel qualquer. A converso do programa acima para ler 1000 em vez de trs valores pode ser feita facilmente recorrendo a este novo conceito:
#include <iostream> using namespace std; int main() { int const nmero_de_valores = 1000; // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; // Denio da matriz com nmero_de_valores elementos do tipo double: double valores[nmero_de_valores]; for(int i = 0; i != nmero_de_valores; ++i) cin > > valores[i]; // l o i-simo valor do teclado. // Clculo da mdia:
Traduziu-se o ingls array por matriz, falta de melhor alternativa. Isso pode criar algumas confuses se se usar uma biblioteca que dena o conceito matemtico de matriz. Por isso a estas matrizes bsicas se chama matrizes clssicas do C++.
1

5.1. MATRIZES CLSSICAS DO C++


double soma = 0.0; for(int i = 0; i != nmero_de_valores; ++i) soma += valores[i]; // acrescenta o i-simo valor soma. double const mdia = soma / nmero_de_valores; // Diviso pela mdia: for(int i = 0; i != nmero_de_valores; ++i) valores[i] /= mdia; // divide o i-simo valor pela mdia. // Escrita do resultado: for(int i = 0; i != nmero_de_valores; ++i) cout < < valores[i] < < ; // escreve o i-simo valor. cout < < endl; }

237

Utilizou-se uma constante para representar o nmero de valores a processar. Dessa forma, alterar o programa para processar no 1000 mas 10000 valores trivial: basta alterar o valor da constante. A alternativa utilizao da constante seria usar o valor literal 1000 explicitamente onde quer que fosse necessrio. Isso implicaria que, ao adaptar o programa para fazer a leitura de 1000 para 10000 valores, o programador recorreria provavelmente a uma substituio de 1000 por 10000 ao longo de todo o texto do programa, usando as funcionalidade do editor de texto. Essa substituio poderia ter consequncias desastrosas se o valor literal 1000 fosse utilizado em algum local do programa num contexto diferente. Recorda-se que o nome de uma constante atribui um signicado ao valor por ela representado. Por exemplo, denir num programa de gesto de uma disciplina as constantes:
int const nmero_mximo_de_alunos_por_turma = 50; int const peso_do_trabalho_na_nota_final = 50;

permite escrever o cdigo sem qualquer utilizao explcita do valor 50. A utilizao explcita do valor 50 obrigaria a inferir o seu signicado exacto (nmero mximo de alunos ou peso do trabalho na nota nal) a partir do contexto, tarefa que nem sempre fcil e que se torna mais difcil medida que os programas se vo tornando mais extensos.

5.1.1 Denio de matrizes


Para utilizar uma matriz de variveis necessrio deni-la 2 . A sintaxe da denio de matrizes simples:
tipo nome[nmero_de_elementos];
Na realidade, para se usar uma varivel, necessrio que ela esteja declarada, podendo por vezes a sua denio estar noutro local, um pouco como no caso das funes e procedimentos. No Captulo 9 se ver como e quando se podem declarar variveis ou constantes sem as denir.
2

238

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

em que tipo o tipo de cada elemento da matriz, nome o nome da matriz, e nmero_de_elementos o nmero de elementos ou dimenso da nova matriz. Por exemplo:
int mi[10]; char mc[80]; float mf[20]; double md[3]; // // // // matriz com 10 int. matriz com 80 char. matriz com 20 float. matriz com 3 double.

O nmero de elementos de uma matriz tem de ser um valor conhecido durante a compilao do programa (i.e., um valor literal ou uma constante). No possvel criar uma matriz usando um nmero de elementos varivel3 . Mas possvel, e em geral aconselhvel, usar constantes em vez de valores literais para indicar a sua dimenso:
int int int int int n = 50; const m = 100; matriz_de_inteiros[m]; // ok, m uma constante. matriz_de_inteiros[300]; // ok, 300 um valor literal. matriz_de_inteiros[n]; // errado, n no uma constante.

Nem todas as constantes tm um valor conhecido durante a compilao: a palavra-chave const limita-se a indicar ao compilador que o objecto a que diz respeito no poder ser alterado. Por exemplo:
// Valor conhecido durante a compilao: int const nmero_mximo_de_alunos = 10000; int alunos[nmero_mximo_de_alunos]; // ok. int nmero_de_alunos_lido; cin > > nmero_de_alunos_lido; // Valor desconhecido durante a compilao: int const nmero_de_alunos = nmero_de_alunos_lido; int alunos[nmero_de_alunos]; // erro!

5.1.2 Indexao de matrizes


Seja a denio
int m[10];

Para atribuir o valor 5 ao stimo elemento da matriz pode-se escrever:


Na realidade podem-se denir matrizes com um dimenso varivel, desde que seja uma matriz dinmica. Alternativamente pode-se usar o tipo genrico vector da biblioteca padro do C++. Ambos os assuntos sero vistos mais tarde.
3

5.1. MATRIZES CLSSICAS DO C++


m[6] = 5;

239

Ao valor 6, colocado entre [], chama-se ndice. Ao escrever numa expresso o nome de uma matriz seguido de [] com um determinado ndice est-se a efectuar uma operao de indexao, i.e., a aceder a um dado elemento da matriz indicando o seu ndice. Os ndices das matrizes so sempre nmeros inteiros e comeam sempre em zero. Assim, o primeiro elemento da matriz m m[0], o segundo m[1], e assim sucessivamente. Embora esta conveno parea algo estranha a incio, ao m de algum tempo torna-se muito natural. Recorde-se os problemas de contagem de anos que decorreram de chamar ano 1 ao primeiro ano de vida de Cristo e a consequente polmica acerca de quando ocorre de facto a mudana de milnio4 ... Como bvio, os ndices usados para aceder s matrizes no necessitam de ser constantes: podem-se usar variveis, como alis se pode vericar no exemplo da leitura de 1000 valores apresentado atrs. Assim, o exemplo anterior pode-se escrever:
int a = 6; m[a] = 5; // atribui o inteiro 5 ao 7o elemento da matriz.

Usando de novo a analogia das folhas de papel, uma matriz como um bloco de notas em que as folhas so numeradas a partir de 0 (zero). Ao se escrever m[6] = 5, est-se a dizer algo como substitua-se o valor que est escrito na folha 6 (a stima) do bloco m pelo valor 5. Na Figura 5.1 mostra-se um diagrama com a representao grca da matriz m depois desta atribuio (por ? representa-se um valor arbitrrio, tambm conhecido por lixo). m: int[10] m[0]: m[1]: m[2]: m[3]: m[4]: m[5]: m[6]: m[7]: m[8]: m[9]: ? ? ? ? ? ? 5 ? ? ?

Figura 5.1: Matriz m denida por int m[10]; depois das instrues int a = 6; e m[a] = 5;. Porventura uma das maiores decincias da linguagem C++ est no tratamento das matrizes, particularmente na indexao. Por exemplo, o cdigo seguinte no resulta em qualquer erro de compilao nem to pouco na terminao do programa com uma mensagem de erro, isto apesar de tentar atribuir valores a posies inexistentes da matriz (as posies com ndices -1 e 4):
4 Note-se que o primeiro ano antes do nascimento de Cristo o ano -1: no h ano zero! O problema mais frequente do que parece. Em Portugal, por exemplo, a numerao dos andares comea em R/C, ou zero, enquanto nos EUA comea em um (e que nmero ter o andar imediatamente abaixo?). Ainda no que diz respeito a datas e horas, o primeiro dia de cada ms 1, mas a primeira hora do dia (e o primeiro minuto de cada hora) 0, apesar de nos relgios analgicos a numerao comear em 1! E, j agora, porque se usam em portugus as expresses absurdas "de hoje a oito dias" e "de hoje a quinze dias" em vez de "daqui a sete dias" ou "daqui a 14 dias"? Ser que "amanh" sinnimo de "de hoje a dois dias" e "hoje" o mesmo que "de hoje a um dia"?

240
int a = 0; int m[4]; int b = 0;

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

m[-1] = 1; // erro! s se pode indexar de 0 a 3! m[4] = 2; // erro! s se pode indexar de 0 a 3! cout < < a < < < < b < < endl;

O que aparece provavelmente escrito no ecr , dependendo do ambiente em que o programa foi compilado e executado,
2 1

ou
1 2

dependendo da arrumao que dada s variveis a, m e b na memria. O que acontece que as variveis so arrumadas na memria (neste caso na pilha ou stack) do computador por ordem de denio, pelo que as folhas de papel correspondentes a a e b seguem ou precedem (conforme a direco de crescimento da pilha) as folhas do bloco de notas correspondente a m. Assim, a atribuio m[-1] = 1; coloca o valor 1 na folha que precede a folha 0 de m na memria, que normalmente a folha de b (a pilha normalmente cresce para baixo, como se ver na disciplina de Arquitectura de Computadores). Esta uma fonte muito frequente de erros no C++, pelo que se recomenda extremo cuidado na utilizao de matrizes. Uma vez que muitos ciclos usam matrizes, este mais um argumento a favor do desenvolvimento disciplinado de ciclos (Seco 4.7) e da utilizao de asseres (Seco 3.2.19).

5.1.3 Inicializao de matrizes


Tal como para as variveis simples, tambm as matrizes s so inicializadas implicitamente quando so estticas5 . Matrizes automticas, i.e., locais a alguma rotina (e sem o qualicador static) no so inicializadas: os seus elementos contm inicialmente valores arbitrrios (lixo). Mas possvel inicializar explicitamente as matrizes. Para isso, colocam-se os valores com que se pretende inicializar os elementos da matriz entre {}, por ordem e separados por vrgulas. Por exemplo:
int m[4] = {10, 20, 0, 0};
Na realidade as variveis de tipos denidos pelo programador ou os elementos de matrizes com elementos de um tipo denido pelo programador (com excepo dos tipos enumerados) so sempre inicializadas, ainda que implicitamente (nesse caso so inicializadas com o construtor por omisso, ver Seco 7.17.5). S variveis automticas ou elementos de matrizes automticas de tipos bsicos do C++ ou de tipos enumerados no so inicializadas implicitamente, contendo por isso lixo se no forem inicializadas explicitamente.
5

5.1. MATRIZES CLSSICAS DO C++

241

Podem-se especicar menos valores de inicializao do que o nmero de elementos da matriz. Nesse caso, os elementos por inicializar explicitamente so inicializados implicitamente com 0 (zero), ou melhor, com o valor por omisso correspondente ao seu tipo (o valor por omisso dos tipos aritmticos zero, o dos booleanos falso, o dos enumerados, ver Captulo 6, zero, mesmo que este valor no seja tomado por nenhum dos seus valores literais enumerados, e o das classes, ver Seco 7.17.5, o valor construdo pelo construtor por omisso). Ou seja,
int m[4] = {10, 20};

tem o mesmo efeito que a primeira inicializao. Pode-se aproveitar este comportamento algo obscuro para forar a inicializao de uma matriz inteira com zeros:
int grande[100] = {}; // inicializa toda a matriz com 0 (zero).

Mas, como este comportamento tudo menos bvio, recomenda-se que se comentem bem tais utilizaes, tal como foi feito aqui. Quando a inicializao explcita, pode-se omitir a dimenso da matriz, sendo esta inferida a partir do nmero de inicializadores. Por exemplo,
int m[] = {10, 20, 0, 0};

tem o mesmo efeito que as denies anteriores.

5.1.4 Matrizes multidimensionais


Em C++ no existe o conceito de matrizes multidimensionais. Mas a sintaxe de denio de matrizes permite a denio de matrizes cujos elementos so outras matrizes, o que acaba por ter quase o mesmo efeito prtico. Assim, a denio:
int m[2][3];

interpretada como signicando int (m[2])[3], o que signica m uma matriz com dois elementos, cada um dos quais uma matriz com trs elementos inteiros. Ou seja, gracamente: m: int[2][3] m[0]: int[3] ? ? ? ? m[1]: int[3] ? ? m[0][0]: m[0][1]: m[0][2]: m[1][0]: m[1][1]: m[1][2]:

Embora sejam na realidade matrizes de matrizes, usual interpretarem-se como matrizes multidimensionais (de resto, ser esse o termo usado daqui em diante). Por exemplo, a matriz m acima interpretada como: A indexao destas matrizes faz-se usando tantos ndices quantas as matrizes dentro de matrizes (incluindo a exterior), ou seja, tantos ndices quantas as dimenses da matriz. Para m conforme denida acima:

242

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


m[1][2] = 4; // atribui 4 ao elemento na linha 1, coluna 2 da matriz. int i = 0, j = 0; m[i][j] = m[1][2]; // atribui 4 posio (0,0) da matriz.

A inicializao destas matrizes pode ser feita como indicado, tomando em conta, no entanto, que cada elemento da matriz por sua vez uma matriz e por isso necessita de ser inicializado da mesma forma. Por exemplo, a inicializao:
int m[2][3] = { {1, 2, 3}, {4} };

leva a que o troo de programa6


for(int i = 0; i != 2; ++i) { for(int j = 0; j != 3; ++j) cout < < setw(2) < < m[i][j]; cout < < endl; }

escreva no ecr
1 2 3 4 0 0

5.1.5 Matrizes constantes


No existe em C++ a noo de matriz constante. Um efeito equivalente pode no entanto ser obtido indicando que os elementos da matriz so constantes. Por exemplo:
int const dias_no_ms_em_ano_normal[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int const dias_no_ms_em_ano_bissexto[] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

5.1.6 Matrizes como parmetros de rotinas


possvel usar matrizes como parmetros de rotinas. O programa original pode ser modularizado do seguinte modo:
O manipulador setw() serve para indicar quantos caracteres devem ser usados para escrever o prximo valor inserido no canal. Por omisso so acrescentados espaos esquerda para perfazer o nmero de caracteres indicado. Para usar este manipulador necessrio fazer #include <iomanip>.
6

5.1. MATRIZES CLSSICAS DO C++


#include <iostream> using namespace std; int const nmero_de_valores = 1000; /** Preenche a matriz com valores lidos do teclado. @pre P C o canal de entrada (cin) contm nmero_de_valores nmeros decimais. @post CO a matriz m contm os valores decimais que estavam em cin. */ void l(double m[nmero_de_valores]) { for(int i = 0; i != nmero_de_valores; ++i) cin > > m[i]; // l o i-simo elemento do teclado. } /** Devolve a mdia dos valores na matriz. @pre P C V. (S j : 0j<nmero_ de_ valores : m[j]) . */ @post CO mdia = nmero_ de_ valores double mdia(double const m[nmero_de_valores]) { double soma = 0.0; for(int i = 0; i != nmero_de_valores; ++i) soma += m[i]; // acrescenta o i-simo elemento soma. return soma / nmero_de_valores; } /** Divide todos os elementos da matriz por divisor. @pre P C divisor = 0 m = m.

243

m[j] @post CO Q j : 0 j < nmero_ de_ valores : m[j] = divisor . */ void normaliza(double m[nmero_de_valores], double const divisor) { assert(divisor != 0); // ver nota7 abaixo.

for(int i = 0; i != nmero_de_valores; ++i) m[i] /= divisor; // divide o i-simo elemento. } /** Escreve todos os valores no ecr. @pre P C V. @post CO o canal cout contm representaes dos valores em m,
Na pr-condio h um termo, m = m, em que ocorre uma varivel matemtica: m. Como esta varivel no faz parte do programa C++, no possvel usar esse termo na instruo de assero. De resto, essa varivel usada simplesmente para na condio objectivo signicar o valor original da varivel de programa m. Como se disse mais atrs, o mecanismo de instrues de assero do C++ algo primitivo, no havendo nenhuma forma de incluir referncias ao valor original de uma varivel.
7

244

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


por ordem. */ void escreve(double const m[nmero_de_valores]) { for(int i = 0; i != nmero_de_valores; ++i) cout < < m[i] < < ; // escreve o i-simo elemento. } int main() { // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; double valores[nmero_de_valores]; l(valores); // Diviso pela mdia: normaliza(valores, mdia(valores)); // Escrita do resultado: escreve(valores); cout < < endl; }

Passagens por referncia Para o leitor mais atento o programa acima tem, aparentemente, um erro. que, nos procedimentos onde se alteram os valores da matriz (nomeadamente l() e normaliza()), a matriz parece ser passada por valor e no por referncia, visto que no existe na declarao dos parmetros qualquer & (ver Seco 3.2.11). Acontece que as matrizes so sempre passadas por referncia8 e o & , portanto, redundante9. Assim, os dois procedimentos referidos alteram de facto a matriz que lhes passada como argumento. Esta uma caracterstica desagradvel das matrizes, pois introduz uma excepo semntica das chamadas de rotinas. Esta caracterstica mantm-se na linguagem apenas por razes de compatibilidade com cdigo escrito na linguagem C. Dimenso das matrizes parmetro H uma outra caracterstica das matrizes que pode causar alguma perplexidade. Se um parmetro de uma rotina for uma matriz, ento a sua dimenso simplesmente ignorada pelo compilador. Assim, o compilador no verica a dimenso das matrizes passadas como argumento, limitando-se a vericar o tipo dos seus elementos. Esta caracterstica est relacionada com o
A verdadeira explicao no esta. A verdadeira explicao ser apresentada quando se introduzir a noo de ponteiro no Captulo 11. 9 Poder-se-ia ter explicitado a referncia escrevendo void l(double (&m)[nmero_de_valores]) (os () so fundamentais), muito embora isso no seja recomendado, pois sugere que na ausncia do & a passagem se faz por valor, o que no verdade.
8

5.1. MATRIZES CLSSICAS DO C++

245

facto de no se vericar a validade dos ndices em operaes de indexao 10 , e com o facto de estarem proibidas a maior parte das operaes sobre matrizes tratadas como um todo (ver Seco 5.1.7). Assim, lMatriz(double m[nmero_de_valores]) e lMatriz(double m[]) tm exactamente o mesmo signicado 11 . Este facto pode ser usado a favor do programador (se ele tiver cuidado), pois permite-lhe escrever rotinas que operam sobre matrizes de dimenso arbitrria, desde que a dimenso das matrizes seja tambm passada como argumento 12 . Assim, na verso do programa que se segue todas as rotinas so razoavelmente genricas, pois podem trabalhar com matrizes de dimenso arbitrria:
#include <iostream> using namespace std; /** Preenche os primeiros n elementos da matriz com n valores lidos do teclado. @pre P C 0 n n dim(m)o canal de entrada (cin) contm n nmeros decimais13 . @post CO a matriz m contm os n valores decimais que estavam em cin. */ void l(double m[], int const n) { assert(0 <= n); // ver nota14 abaixo. for(int i = 0; i != n; ++i)
Na realidade as matrizes so sempre passadas na forma de um ponteiro para o primeiro elemento, como se ver no !!. 11 Esta equivalncia deve-se ao compilador ignorar a dimenso de uma matriz indicada como parmetro de uma funo ou procedimento. Isto no acontece se a passagem por referncia for explicitada conforme indicado na nota Nota 9 na pgina 244: nesse caso a dimenso no ignorada e o compilador verica se o argumento respectivo tem tipo e dimenso compatveis. 12 Na realidade o valor passado como argumento no precisa de ser a dimenso real da matriz: basta que seja menor ou igual ao dimenso real da matriz. Isto permite processar apenas parte de uma matriz. 13 Em expresses matemticas, dim(m) signica a dimenso de uma matriz m. A mesma notao tambm se pode usar no caso dos vectores, que se estudaro em seces posteriores. 14 Nesta instruo de assero no possvel fazer melhor do que isto. Acontece que impossvel vericar se existem n valores decimais disponveis para leitura no canal de entrada antes de os tentar extrair e, por outro lado, caracterstica desagradvel da linguagem C++ no permitir saber a dimenso de matrizes usadas como parmetro de uma funo ou procedimento. O primeiro problema poderia ser resolvido de duas formas. A primeira, mais correcta, passava por exigir ao utilizador a introduo dos nmeros decimais pretendidos, insistindo com ele at a insero ter sucesso. Neste caso a pr-condio seria simplicada. Alternativamente poder-se-ia usar uma assero para saber se cada extraco teve sucesso. Como um canal, interpretado como um booleano, tem valor verdadeiro se e s se no houve qualquer erro de extraco, o ciclo poderia ser reescrito como: for(int i = 0; i != n; ++i) { cin > > m[i]; // l o i-simo elemento do teclado. assert(cin); } Um canal de entrada, depois de um erro de leitura, ca em estado de erro, falhando todas as extraces que se tentarem realizar. Um canal em estado de erro tem sempre o valor falso quando interpretado como um booleano.
10

246

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


cin > > m[i]; // l o i-simo elemento do teclado. } /** Devolve a mdia dos n primeiros elementos da matriz. @pre P C 1 n n dim(m). @post CO mdia = (S j : 0j<n : m[j]) . */ n double mdia(double const m[], int const n) { assert(1 <= n); double soma for(int i = soma += return soma } /** Divide os primeiros n elementos da matriz por divisor. @pre P C 0 n n dim(m) divisor = 0 m = m. = 0.0; 0; i != n; ++i) m[i]; // acrescenta o i-simo elemento soma. / n;

m[j] @post CO Q j : 0 j < n : m[j] = divisor . */ void normaliza(double m[], int const n, double const divisor) { assert(0 <= n and divisor != 0);

for(int i = 0; i != n; ++i) m[i] /= divisor; // divide o i-simo elemento. } /** Escreve todos os valores no ecr. @pre P C 0 n n dim(m). @post CO o canal cout contm representaes dos primeiros n elementos de m, por ordem. */ void escreve(double const m[], int const n) { assert(0 <= n); for(int i = 0; i != n; ++i) cout < < m[i] < < ; // escreve o i-simo elemento. } int main() { int const nmero_de_valores = 1000; // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; double valores[nmero_de_valores];

5.1. MATRIZES CLSSICAS DO C++


l(valores, nmero_de_valores); // Diviso pela mdia: normaliza(valores, nmero_de_valores, mdia(valores, nmero_de_valores)); // Escrita do resultado: escreve(valores, nmero_de_valores); cout < < endl; }

247

O caso das matrizes multidimensionais mais complicado: apenas ignorada a primeira dimenso das matrizes multidimensionais denidas como parmetros de rotinas. Assim, impossvel escrever
double determinante(double const m[][], int const linhas, int const colunas) { ... }

com a esperana de denir uma funo para calcular o determinante de uma matriz bidimensional de dimenses arbitrrias... obrigatrio indicar as dimenses de todas as matrizes excepto a mais exterior:
double determinante(double const m[][10], int const linhas) { ... }

Ou seja, a exibilidade neste caso resume-se a que a funo pode trabalhar com um nmero arbitrrio de linhas15 ...

5.1.7 Restries na utilizao de matrizes


Para alm das particularidades j referidas relativamente utilizao de matrizes em C++, existem duas restries que importante conhecer: Devoluo No possvel devolver matrizes (directamente) em funes. Esta restrio desagradvel obriga utilizao de procedimentos para processamento de matrizes. Operaes No so permitidas as atribuies ou comparaes entre matrizes. Estas operaes tm de ser realizadas atravs da aplicao sucessiva da operao em causa a cada um dos elementos da matriz. Pouco prtico, em suma.
15

Na realidade nem isso, porque o determinante de uma matriz s est denido para matrizes quadradas...

248

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

5.2 Vectores
Viu-se que as matrizes tm um conjunto de particularidades que as tornam pouco simpticas de utilizar na prtica. No entanto, a linguagem C++ traz associada a chamada biblioteca padro (por estar disponvel em qualquer ambiente de desenvolvimento que se preze) que disponibiliza um conjunto muito vasto de ferramentas (rotinas, variveis e constantes, tipos, etc.), entre os quais um tipo genrico chamado vector. Este tipo genrico uma excelente alternativa s matrizes e apresentado brevemente nesta seco. Pode-se resolver o problema de generalizar o programa apresentado no incio deste captulo recorrendo a outra das possveis formas de agregado: os vectores. Ao contrrio das matrizes, os vectores no fazem parte da linguagem C++. Os vectores so um tipo genrico, ou melhor uma classe C++ genrica, denido na biblioteca padro do C++ e a que se acede colocando
#include <vector>

no incio do programa. As noes de classe C++ e classe C++ genrica sero abordadas no Captulo 7 e no Captulo 13, respectivamente. Um vector um contentor de itens do mesmo tipo que podem ser indexados (identicados) por nmeros inteiros e que podem ser interpretados e usados como outra varivel qualquer. Ao contrrio das matrizes, os vectores podem ser redimensionados sempre que necessrio. A converso do programa inicial para ler 1000 em vez de trs valores pode ser feita facilmente recorrendo ao tipo genrico vector:
#include <iostream> #include <vector> using namespace std; int main() { int const nmero_de_valores = 1000; // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; // Denio de um vector com nmero_de_valores itens do tipo double: vector<double> valores(nmero_de_valores); for(int i = 0; i != nmero_de_valores; ++i) cin > > valores[i]; // l o i-simo valor do teclado. // Clculo da mdia: double soma = 0.0; for(int i = 0; i != nmero_de_valores; ++i)

5.2. VECTORES
soma += valores[i]; // acrescenta o i-simo valor soma. double const mdia = soma / nmero_de_valores; // Diviso pela mdia: for(int i = 0; i != nmero_de_valores; ++i) valores[i] /= mdia; // divide o i-simo valor pela mdia. // Escrita do resultado: for(int i = 0; i != nmero_de_valores; ++i) cout < < valores[i] < < ; // escreve o i-simo valor. cout < < endl; }

249

5.2.1 Denio de vectores


Para utilizar um vector necessrio deni-lo. A sintaxe da denio de vectores simples:
vector<tipo> nome(nmero_de_itens);

em que tipo o tipo de cada item do vector, nome o nome do vector, e nmero_de_itens o nmero de itens ou dimenso inicial do novo vector. possvel omitir a dimenso inicial do vector: nesse caso o vector inicialmente no ter qualquer elemento:
vector<tipo> nome;

Por exemplo:
vector<int> vector<char> vector<float> vector<double> vi(10); vc(80); vf(20); vd; // // // // vector com 10 int. vector com 80 char. vector com 20 float. vector com zero double.

Um vector uma varivel como outra qualquer. Depois das denies acima pode-se dizer que vd uma varivel do tipo vector<double> (vector de double) que contm zero itens.

5.2.2 Indexao de vectores


Seja a denio
vector<int> v(10);

Para atribuir o valor 5 ao stimo item do vector pode-se escrever:

250
v[6] = 5;

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

Ao valor 6, colocado entre [], chama-se ndice. Ao escrever numa expresso o nome de um vector seguido de [] com um determinado ndice est-se a efectuar uma operao de indexao, i.e., a aceder a um dado item do vector indicando o seu ndice. Os ndices dos vectores, como os das matrizes, so sempre nmeros inteiros e comeam sempre em zero. Assim, o primeiro item do vector v v[0], o segundo v[1], e assim sucessivamente. Os ndices usados para aceder aos vectores no necessitam de ser constantes: podem-se usar variveis, como alis se pode vericar no exemplo da leitura de 1000 valores apresentado atrs. Assim, o exemplo anterior pode-se escrever:
int a = 6; v[a] = 5; // atribui o inteiro 5 ao 7o item do vector.

Da mesma forma que acontece com as matrizes, tambm com os vectores as indexaes no so vericadas. Por exemplo, o cdigo seguinte no resulta em qualquer erro de compilao e provavelmente tambm no resulta na terminao do programa com uma mensagem de erro, isto apesar de tentar atribuir valores a posies inexistentes do vector:
vector<int> v(4); v[-1] = 1; // erro! s se pode indexar de 0 a 3! v[4] = 2; // erro! s se pode indexar de 0 a 3!

Esta uma fonte muito frequente de erros no C++, pelo que se recomenda extremo cuidado na indexao de vectores.

5.2.3 Inicializao de vectores


Os itens de um vector, ao contrrio do que acontece com as matrizes, so sempre inicializados. A inicializao usada a chamada inicializao por omisso, que no caso dos itens serem dos tipos bsicos (int, char, float, etc.), a inicializao com o valor zero. Por exemplo, depois da denio
vector<int> v(4);

todos os itens do vector v tm o valor zero. No possvel inicializar os itens de um vector como se inicializam os elementos de uma matriz. Mas pode-se especicar um valor com o qual se pretendem inicializar todos os itens do vector. Por exemplo, depois da denio
vector<int> v(4, 13);

todos os itens do vector v tm o valor 13.

5.2. VECTORES

251

5.2.4 Operaes
Os vectores so variveis como qualquer outras. A diferena principal est em suportarem a invocao das chamadas operaes, um conceito que ser visto em pormenor no Captulo 7. Simplicando grosseiramente, as operaes so rotinas associadas a um tipo (classe C++) que se invocam para uma determinada varivel desse tipo. A forma de invocao um pouco diferente da usada para as rotinas normais. As operaes invocam-se usando o operador . de seleco de membro (no Captulo 7 se ver o que exactamente um membro). Este operador tem dois operandos. O primeiro a varivel qual a operao aplicado. O segundo a operao a aplicar, seguida dos respectivos argumentos, se existirem. Uma operao extremamente til do tipo genrico vector chama-se size() e permite saber a dimenso actual de um vector. Por exemplo:
vector<double> distncias(10); cout < < "A dimenso " < < distncias.size() < < . < < endl;

O valor devolvido pela operao size() pertence a um dos tipos inteiros do C++, mas a norma da linguagem no especica qual... No entanto, fornecido um sinnimo desse tipo para cada tipo de vector. Esse sinnimo chama-se size_type e pode ser acedido usando o operador :: de resoluo de mbito em que o operando esquerdo o tipo do vector:
vector<double>::size_type

Um ciclo para percorrer e mostrar todos os itens de um vector pode ser escrito fazendo uso deste tipo e da operao size():
for(vector<double>::size_type i = 0; i != distncias.size(); ++i) cout < < distncias[i] < < endl;

Uma outra operao pode ser usada se se pretender apenas saber se um vector est ou no vazio, i.e., se se pretender saber se um vector tem dimenso zero. A operao chama-se empty() e devolve verdadeiro se o vector estiver vazio e falso no caso contrrio.

5.2.5 Acesso aos itens de vectores


Viu-se que a indexao de vectores insegura. O tipo genrico vector fornece uma operao chamada at() que permite aceder a um item dado o seu ndice, tal como a operao de indexao, mas que garantidamente resulta num erro 16 no caso de o ndice ser invlido. Esta operao tem como argumento o ndice do item ao qual se pretende aceder e devolve esse mesmo item. Voltando ao exemplo original,
16

Ou melhor, resulta no lanamento de uma excepo, ver !!.

252
vector<int> v(4);

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

v.at(-1) = 1; // erro! s se pode indexar de 0 a 3! v.at(4) = 2; // erro! s se pode indexar de 0 a 3!

este troo de cdigo levaria terminao abrupta 17 do programa em que ocorresse logo ao ser executada a primeira indexao errada. Existem ainda duas operaes que permitem aceder aos dois itens nas posies extremas do vector. A operao front() devolve o primeiro item do vector. A operao back() devolve o ltimo item do vector. Por exemplo, o seguinte troo de cdigo
vector<double> distncias(10); for(vector<double>::size_type i = 0; i != distncias.size(); ++i) distncias[i] = double(i); // converte i num double. cout < < "Primeiro: " < < distncias.front() < < . < < endl; cout < < "ltimo: " < < distncias.back() < < . < < endl;

escreve no ecr
Primeiro: 0. ltimo: 9.

5.2.6 Alterao da dimenso de um vector


Ao contrrio do que acontece com as matrizes, os vectores podem ser redimensionados sempre que necessrio. Para isso recorre-se operao resize(). Esta operao recebe como primeiro argumento a nova dimenso pretendida para o vector e, opcionalmente, recebe como segundo argumento o valor com o qual so inicializados os possveis novos itens criados pelo redimensionamento. Caso o segundo argumento no seja especicado, os possveis novos itens so inicializados usando a inicializao por omisso. Por exemplo, o resultado de
vector<double> distncias(3, 5.0); distncias.resize(6, 10.0); distncias.resize(9, 15.0); distncias.resize(8); for(vector<double>::size_type i = 0; i != distncias.size(); ++i) cout < < distncias[i] < < endl;

surgir no ecr
17

Isto, claro est, se ningum capturar a excepo lanada, como se ver no !!.

5.2. VECTORES
5 5 5 10 10 10 15 15

253

Se a inteno for eliminar todos os itens do vector, reduzindo a sua dimenso a zero, pode-se usar a operao clear() como se segue:
distncias.clear();

Os vectores no podem ter uma dimenso arbitrria. Para saber a dimenso mxima possvel para um vector usar a operao max_size():
cout < < "Dimenso mxima do vector de distncias " < < distncias.max_size() < < " itens." < < endl;

Finalmente, como as operaes que alteram a dimenso de um vector podem ser lentas, j que envolvem pedidos de memria ao sistema operativo, o tipo genrico fornece uma operao chamada reserve() para pr-reservar espao para itens que se prev venham a ser necessrios no futuro. Esta operao recebe como argumento o nmero de itens que se prev virem a ser necessrios na pior das hipteses e reserva espao para eles. Enquanto a dimenso do vector no ultrapassar a reserva feita todas as operaes tm ecincia garantida. A operao capacity() permite saber qual o nmero de itens para os quais h espao reservado em cada momento. A seco seguinte demonstra a utilizao desta operao.

5.2.7 Insero e remoo de itens


As operaes mais simples para insero e remoo de itens referem-se ao extremo nal dos vectores. Se se pretender acrescentar um item no nal de um vector pode-se usar a operao push_back(), que recebe como argumento o valor com o qual inicializar o novo item. Se se pretender remover o ltimo item de um vector pode-se usar a operao pop_back(). Por exemplo, em vez de escrever
vector<double> distncias(10); for(vector<double>::size_type i = 0; i != distncias.size(); ++i) distncias[i] = double(i); // converte i num double.

possvel escrever

254

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


vector<double> distncias; for(vector<double>::size_type i = 0; i != 10; ++i) distncias.push_back(double(i));

A segunda verso, com push_back(), particularmente vantajosa quando o nmero de itens a colocar no vector no conhecido partida. Se a dimenso mxima do vector for conhecida partida, pode ser til comear por reservar espao suciente para os itens a colocar:
vector<double> distncias; distncias.reserve(10); /* Neste local o vector est vazio, mas tem espao para crescer sem precisar de recorrer ao sistema operativo para requerer memria. */ for(vector<double>::size_type i = 0; i != 10; ++i) distncias.push_back(double(i));

A operao pop_back() pode ser conjugada com a operao empty() para mostrar os itens de um vector pela ordem inversa e, simultaneamente, esvazi-lo:
while(not distncias.empty()) { cout < < distncias.back() < < endl; distncias.pop_back(); }

5.2.8 Vectores multidimensionais?


O tipo genrico vector no foi pensado para representar matrizes multidimensionais. No entanto, possvel denir vectores de vectores usando a seguinte sintaxe, apresentada sem mais explicaes18 :
vector<vector<tipo> > nome(linhas, vector<tipo>(colunas));

onde linhas o nmero de linhas pretendido para a matriz e colunas o nmero de colunas. O processo no elegante, mas funciona e pode ser estendido para mais dimenses:
vector<vector<vector<tipo> > > nome(planos, vector<vector<tipo> >(linhas, vector<tipo>(colunas)));
18

Tem de se deixar um espao entre smbolos > sucessivos para que no se confundam com o operador > >.

5.2. VECTORES

255

No entanto, estas variveis no so verdadeiramente representaes de matrizes, pois, por exemplo no primeiro caso, possvel alterar a dimenso das linhas da matriz independentemente umas das outras. possvel simplicar as denies acima se o objectivo no for emular as matrizes mas simplesmente denir um vector de vectores. Por exemplo, o seguinte troo de programa
/* Denio de um vector de vectores com quatro itens inicialmente, cada um tendo inicialmente dimenso nula. */ vector<vector<char> > v(4); v[0].resize(1, v[1].resize(2, v[2].resize(3, v[3].resize(4, a); b); c); d);

for(vector<vector<char> >::size_type i = 0; i != v.size(); ++i) { for(vector<char>::size_type j = 0; j != v[i].size(); ++j) cout < < v[i][j]; cout < < endl; }

quando executado resulta em


a bb ccc dddd

5.2.9 Vectores constantes


Como para qualquer outro tipo, pode-se denir vectores constantes. A grande diculdade est em inicializar um vector constante, visto que a inicializao ao estilo das matrizes no permitida:
vector<char> const muitos_aa(10, a);

5.2.10 Vectores como parmetros de rotinas


A passagem de vectores como argumento no difere em nada de outros tipos. Podem-se passar vectores por valor ou por referncia. O programa original pode ser modularizado de modo a usar rotinas do seguinte modo:

256

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


#include <iostream> #include <vector> using namespace std; /** Preenche o vector com valores lidos do teclado. @pre P C o canal de entrada (cin) contm v.size() nmeros decimais. @post CO os itens do vector v contm os v.size() valores decimais que estavam em cin. */ void l(vector<double>& v) { for(vector<double>::size_type i = 0; i != v.size(); ++i) cin > > v[i]; // l o i-simo elemento do teclado. } /** Devolve a mdia dos itens do vector. @pre P C 1 v.size(). (S j : 0j<v.size() : v[j]) . */ @post CO mdia = v.size() double mdia(vector<double> const v) { assert(1 <= v.size()); double soma = 0.0; for(vector<double>::size_type i = 0; i != v.size(); ++i) soma += v[i]; // acrescenta o i-simo elemento soma. return soma / v.size(); } /** Divide os itens do vector por divisor. @pre P C divisor = 0 v = v.

v[j] @post CO Q j : 0 j < v.size() : v[j] = divisor . */ void normaliza(vector<double>& v, double const divisor) { assert(divisor != 0);

for(vector<double>::size_type i = 0; i != v.size(); ++i) v[i] /= divisor; // divide o i-simo elemento. } /** Escreve todos os valores do vector no ecr. @pre P C V. @post CO o canal cout contm representaes dos itens de v, por ordem. */ void escreve(vector<double> const v) { for(vector<double>::size_type i = 0; i != v.size(); ++i) cout < < v[i] < < ; // escreve o i-simo elemento.

5.2. VECTORES
} int main() { int const nmero_de_valores = 1000; // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; vector<double> valores(nmero_de_valores); l(valores); // Diviso pela mdia: normaliza(valores, mdia(valores)); // Escrita do resultado: escreve(valores); cout < < endl; }

257

No entanto, importante perceber que a passagem de um vector por valor implica a cpia do vector inteiro, o que pode ser extremamente ineciente. O mesmo no acontece se a passagem se zer por referncia, mas seria desagradvel passar a mensagem errada ao compilador e ao leitor do cdigo, pois uma passagem por referncia implica que a rotina em causa pode alterar o argumento. Para resolver o problema usa-se uma passagem de argumentos por referncia constante.

5.2.11 Passagem de argumentos por referncia constante


Considere-se de novo funo para somar a mdia de todos os itens de um vector de inteiros:
double mdia(vector<double> const v) { assert(1 <= v.size()); double soma = 0.0; for(vector<double>::size_type i = 0; i != v.size(); ++i) soma += v[i]; // acrescenta o i-simo elemento soma. return soma / v.size(); }

De acordo com o mecanismo de chamada de funes (descrito na Seco 3.2.11), e uma vez que o vector passado por valor, evidente que a chamada da funo mdia() implica a construo de uma nova varivel do tipo vector<int>, o parmetro v, que inicializada a partir do vector passado como argumento e que , portanto, uma cpia desse mesmo argumento. Se o vector passado como argumento tiver muitos itens, como o caso no programa acima,

258

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

evidente que esta cpia pode ser muito demorada, podendo mesmo em alguns casos tornar a utilizao da funo impossvel na prtica. Como resolver o problema? Se a passagem do vector fosse feita por referncia, e no por valor, essa cpia no seria necessria. Assim, poder-se-ia aumentar a ecincia da chamada da funo denindo-a como
double mdia(vector<double>& v) // m ideia! { assert(1 <= v.size()); double soma = 0.0; for(vector<double>::size_type i = 0; i != v.size(); ++i) soma += v[i]; // acrescenta o i-simo elemento soma. return soma / v.size(); }

Esta nova verso da funo tem uma desvantagem: na primeira verso, o consumidor da funo e o compilador sabiam que o vector passado como argumento no poderia ser alterado pela funo, j que esta trabalhava com uma cpia. Na nova verso essa garantia no feita. O problema pode ser resolvido se se disser de algum modo que, apesar de o vector ser passado por referncia, a funo no est autorizada a alter-lo. Isso consegue-se recorrendo de novo ao qualicador const:
double mdia(vector<double> const& v) // boa ideia! { assert(1 <= v.size()); double soma = 0.0; for(vector<double>::size_type i = 0; i != v.size(); ++i) soma += v[i]; // acrescenta o i-simo elemento soma. return soma / v.size(); }

Pode-se agora converter o programa de modo a usar passagens por referncia constante em todas as rotinas onde til faz-lo:
#include <iostream> #include <vector> using namespace std; /** Preenche o vector com valores lidos do teclado. @pre P C o canal de entrada (cin) contm v.size() nmeros decimais. @post CO os itens do vector v contm os v.size() valores decimais que estavam em cin. */ void l(vector<double>& v)

5.2. VECTORES
{ for(vector<double>::size_type i = 0; i != v.size(); ++i) cin > > v[i]; // l o i-simo elemento do teclado. } /** Devolve a mdia dos itens do vector. @pre P C 1 v.size(). (S j : 0j<v.size() : v[j]) @post CO mdia = . */ v.size() double mdia(vector<double> const& v) { assert(1 <= v.size()); double soma = 0.0; for(vector<double>::size_type i = 0; i != v.size(); ++i) soma += v[i]; // acrescenta o i-simo elemento soma. return soma / v.size(); } /** Divide os itens do vector por divisor. @pre P C divisor = 0 v = v.

259

v[j] @post CO Q j : 0 j < v.size() : v[j] = divisor . */ void normaliza(vector<double>& v, double const divisor) { assert(divisor != 0);

for(vector<double>::size_type i = 0; i != v.size(); ++i) v[i] /= divisor; // divide o i-simo elemento. } /** Escreve todos os valores do vector no ecr. @pre P C V. @post CO o canal cout contm representaes dos itens de v, por ordem. */ void escreve(vector<double> const& v) { for(vector<double>::size_type i = 0; i != v.size(); ++i) cout < < v[i] < < ; // escreve o i-simo elemento. } int main() { int const nmero_de_valores = 1000; // Leitura: cout < < "Introduza " < < nmero_de_valores < < " valores: "; vector<double> valores(nmero_de_valores); l(valores);

260

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

// Diviso pela mdia: normaliza(valores, mdia(valores)); // Escrita do resultado: escreve(valores); cout < < endl; }

Compare-se esta verso do programa com a verso usando matrizes apresentada mais atrs. As passagens por referncia constante tm uma caracterstica adicional que as distingue das passagens por referncia simples: permitem passar qualquer constante (e.g., um valor literal) como argumento, o que no era possvel no caso das passagens por referncia simples. Por exemplo:
// Mau cdigo. Bom para exemplos apenas... int soma1(int& a, int& b) { return a + b; } /* No grande ideia usar referncias constantes para os tipos bsicos do C++ (no se poupa nada): */ int soma2(int const& a, int const& b) { return a + b; } int main() { int i = 1, j = 2, res; res = soma1(i, j); // vlido. res = soma2(i, j); // vlido. res = soma1(10, 20); // erro! res = soma2(10, 20); // vlido! os parmetros a e b // tornam-se sinnimos de duas // constantes temporrias inicializadas // com 10 e 20. }

A devoluo de vectores em funes possvel, ao contrrio do que acontece com as matrizes, embora no seja recomendvel. A devoluo de um vector implica tambm uma cpia: o valor devolvido uma cpia do vector colocado na expresso de retorno. O problema pode, por vezes, ser resolvido tambm por intermdio de referncias, como se ver na Seco 7.7.1.

5.2. VECTORES

261

5.2.12 Outras operaes com vectores


Ao contrrio do que acontece com as matrizes, possvel atribuir vectores e mesmo comparlos usando os operadores de igualdade e os operadores relacionais. A nica particularidade merecedora de referncia o que se entende por um vector ser menor do que outro. A comparao entre vectores com os operadores relacionais feita usando a chamada ordenao lexicogrca. Esta ordenao a mesma que usada para colocar as palavras nos vulgares dicionrios19 , e usa os seguintes critrios: 1. A comparao feita da esquerda para a direita, comeando portanto nos primeiros itens dos dois vectores em comparao, e prosseguindo sempre a par ao longo dos vectores. 2. A comparao termina assim que ocorrer uma de trs condies: (a) os itens em comparao so diferentes: nesse caso o vector com o item mais pequeno considerado menor que o outro. (b) um dos vectores no tem mais itens, sendo por isso mais curto que o outro: nesse caso o vector mais curto considerado menor que o outro. (c) nenhum dos vectores tem mais itens: nesse caso os dois vectores tm o mesmo comprimento e os mesmos valores dos itens e so por isso considerados iguais. Representando os vectores por tuplos: () = (). () < (10). (1, 10, 20) < (2). (1, 2, 3, 4) < (1, 3, 2, 4). (1, 2) < (1, 2, 3). O troo de cdigo abaixo
vector<int> v1; v1.push_back(1); v1.push_back(2); vector<int> v2;
De acordo com [4]: lexicogrco (cs). [De lexicograa + -ico2 .] Adj. Respeitante lexicograa. lexicograa (cs). [De lexico- + -graa.] S.f. A cincia do lexicgrafo. lexicgrafo (cs). [Do gr. lexikogrphos.] S.m. Autor de dicionrio ou de trabalho a respeito de palavras duma lngua; dicionarista; lexiclogo. [Cf. lexicografo, do v. lexicografar.]
19

262

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

v2.push_back(1); v2.push_back(2); v2.push_back(3); if(v1 < v2) cout < < "v1 mesmo menor que v2." < < endl;

produz no ecr
v1 mesmo menor que v2.

como se pode vericar observando o ltimo exemplo acima.

5.3 Algoritmos com matrizes e vectores


Esta seco apresenta o desenvolvimento pormenorizado de quatro funes usando ciclos e operando com matrizes e vectores e serve, por isso, de complemento aos exemplos de desenvolvimento de ciclos apresentados no captulo anterior.

5.3.1 Soma dos elementos de uma matriz


O objectivo da funo calcular a soma dos valores dos primeiros n elementos de uma matriz m. O primeiro passo da resoluo do problema a sua especicao, ou seja, a escrita da estrutura da funo, incluindo a pr-condio e a condio objectivo:
/** Devolve a soma dos primeiros n elementos da matriz m. @pre P C 0 n n dim(m). @post CO soma = (S j : 0 j < n : m[j]). */ int soma(int const m[], int const n) { assert(0 <= n); int soma = ...; ... return soma; }

Como necessrio no nal devolver a soma dos elementos, dene-se imediatamente uma varivel soma para guardar esse valor. Usa-se uma varivel com o mesmo nome da funo para que a condio objectivo do ciclo e a condio objectivo da funo sejam idnticas 20 .
O nome de uma varivel local pode ser igual ao da funo em que est denida. Nesse caso ocorre uma ocultao do nome da funo (ver Seco 3.2.14). A nica consequncia desta ocultao que para invocar recursivamente a funo, se isso for necessrio, necessrio usar o operador de resoluo de mbito ::.
20

5.3. ALGORITMOS COM MATRIZES E VECTORES

263

O passo seguinte consiste em, tendo-se percebido que a soluo pode passar pela utilizao de um ciclo, determinar uma condio invariante apropriada, o que se consegue usualmente por enfraquecimento da condio objectivo. Neste caso pode-se simplesmente substituir o limite superior do somatrio (a constante n) por uma varivel i inteira, limitada a um intervalo apropriado de valores. Assim, a estrutura do ciclo
// P C 0 n n dim(m). int soma = ...; int i = ...; // CI soma = (S j : 0 j < i : m[j]) 0 i n. while(G) { passo } // CO soma = (S j : 0 j < n : m[j]).

Identicada a condio invariante, necessrio escolher uma guarda tal que seja possvel demonstrar que CI G CO. Ou seja, escolher uma guarda que, quando o ciclo terminar, conduza naturalmente condio objectivo 21 . Neste caso evidente que a escolha correcta para G i = n, ou seja, G i = n:
// P C 0 n n dim(m). int soma = ...; int i = ...; // CI soma = (S j : 0 j < i : m[j]) 0 i n. while(i != n) { passo } // CO soma = (S j : 0 j < n : m[j]).

De seguida deve-se garantir, por escolha apropriada das instrues das inicializaes, que a condio invariante verdadeira no incio do ciclo. Como sempre, devem-se escolher as instrues mais simples que conduzem veracidade da condio invariante. Neste caso fcil vericar que se deve inicializar i com zero e soma tambm com zero (recorde-se de que a soma de zero elementos , por denio, zero):
// P C 0 n n dim(m). int soma = 0; int i = 0; // CI soma = (S j : 0 j < i : m[j]) 0 i n. while(i != n) { passo } // CO soma = (S j : 0 j < n : m[j]).
A condio invariante verdadeira, por construo, no incio, durante, e no nal do ciclo: por isso se chama condio invariante. Quando o ciclo termina, tem de se ter forosamente que a guarda falsa, ou seja, que G verdadeira.
21

264

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

Como se pretende que o algoritmo termine, deve-se agora escolher um progresso que o garanta (o passo normalmente dividido em duas partes, o progresso prog e a aco aco). Sendo i inicialmente zero, e sendo 0 n (pela pr-condio), evidente que uma simples incrementao da varivel i conduzir falsidade da guarda, e portanto terminao do ciclo, ao m de exactamente n iteraes do ciclo:
// P C 0 n n dim(m). int soma = 0; int i = 0; // CI soma = (S j : 0 j < i : m[j]) 0 i n. while(i != n) { aco ++i; } // CO soma = (S j : 0 j < n : m[j]).

Finalmente, necessrio construir uma aco que garanta a veracidade da condio invariante depois do passo e apesar do progresso entretanto realizado. Sabe-se que a condio invariante e a guarda so verdadeiras antes do passo, logo, necessrio encontrar uma aco tal que:
// CI G soma = (S j : 0 j < i : m[j]) 0 i n i = n, ou seja, // soma = (S j : 0 j < i : m[j]) 0 i < n. aco ++i; // CI soma = (S j : 0 j < i : m[j]) 0 i n.

Pode-se comear por vericar qual a pr-condio mais fraca do progresso que garante que a condio invariante recuperada:
// soma = (S j : 0 j < i + 1 : m[j]) 0 i + 1 n, ou seja, // soma = (S j : 0 j < i + 1 : m[j]) 1 i < n. ++i; // CI soma = (S j : 0 j < i : m[j]) 0 i n.

Se se admitir que 0 i, ento o ltimo termo do somatrio pode ser extrado:


// soma = (S j : 0 j < i : m[j]) + m[i] 0 i < n. // soma = (S j : 0 j < i + 1 : m[j]) 1 i < n.

Conclui-se que a aco dever ser escolhida de modo a que:


// soma = (S j : 0 j < i : m[j]) 0 i < n. aco // soma = (S j : 0 j < i : m[j]) + m[i] 0 i < n.

5.3. ALGORITMOS COM MATRIZES E VECTORES


o que se consegue facilmente com a aco:
soma += m[i];

265

A funo completa ento


/** Devolve a soma dos primeiros n elementos da matriz m. @pre P C 0 n n dim(m). @post CO soma = (S j : 0 j < n : m[j]). */ int soma(int const m[], int const n) { assert(0 <= n); int soma = 0; int i = 0; // CI soma = (S j : 0 j < i : m[j]) 0 i n. while(i != n) { soma += m[i]; ++i; } return soma; }

Pode-se ainda converter o ciclo de modo a usar a instruo for:


/** Devolve a soma dos primeiros n elementos da matriz m. @pre P C 0 n n dim(m). @post CO soma = (S j : 0 j < n : m[j]). */ int soma(int const m[], int const n) { assert(0 <= n); int soma = 0; // CI soma = (S j : 0 j < i : m[j]) 0 i n. for(int i = 0; i != n; ++i) soma += m[i]; return soma; }

Manteve-se a condio invariante como um comentrio antes do ciclo, pois muito importante para a sua compreenso. A condio invariante no fundo reecte como o ciclo (e a funo) funcionam, ao contrrio da pr-condio e da condio objectivo, que se referem quilo que o ciclo (ou a funo) faz. A pr-condio e a condio objectivo so teis para o programador consumidor da funo, enquanto a condio invariante til para o programador produtor e para o programador assistncia tcnica, que pode precisar de vericar a correcta implementao do ciclo. O ciclo desenvolvido corresponde a parte da funo mdia() usada em exemplos anteriores.

266

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

5.3.2 Soma dos itens de um vector


O desenvolvimento no caso dos vectores semelhante ao usado para as matrizes. A funo resultante desse desenvolvimento
/** Devolve a soma dos itens do vector v. @pre P C V. @post CO soma = (S j : 0 j < v.size() : v[j]). */ int soma(vector<int> const& v) { int soma = 0; // CI soma = (S j : 0 j < i : v[j]) 0 i v.size(). for(vector<int>::size_type i = 0; i != v.size(); ++i) soma += v[i]; return soma; }

5.3.3 ndice do maior elemento de uma matriz


O objectivo construir uma funo que devolva um dos ndices do mximo valor contido nos primeiros n elementos de uma matriz m. Como no se especica qual dos ndices devolver caso existam vrios elementos com o valor mximo, arbitra-se que a funo deve devolver o primeiro desses ndices. Assim, a estrutura da funo :
/** Devolve o ndice do primeiro elemento com o mximo valor entre os primeiros n elementos da matriz m. @pre P C 1 n n dim(m). @post CO 0 ndiceDoPrimeiroMximoDe < n (Q j : 0 j < n : m[j] m[ndiceDoPrimeiroMximoDe]) (Q j : 0 j < ndiceDoPrimeiroMximoDe : m[j] < m[ndiceDoPrimeiroMximoDe]). int ndiceDoPrimeiroMximoDe(int const m[], int const n) { assert(1 <= n); int i = ...; ... // Sem ciclos no se pode fazer muito melhor: assert(0 <= i < n and m[0] <= m[i]); return i; }

A condio objectivo indica que o ndice devolvido tem de pertencer gama de ndices vlidos para a matriz, que o valor do elemento de m no ndice devolvido tem de ser maior ou igual aos

5.3. ALGORITMOS COM MATRIZES E VECTORES

267

valores de todos os elementos da matriz (estas condies garantem que o ndice devolvido um dos ndices do valor mximo na matriz) e que os valores dos elementos com ndice menor do que o ndice devolvido tm de ser estritamente menores que o valor da matriz no ndice devolvido (ou seja, o ndice devolvido o primeiro dos ndices dos elementos com valor mximo na matriz). A pr-condio, neste caso, impe que n no pode ser zero, pois no tem sentido falar do mximo de um conjunto vazio, alm de obrigar n a ser inferior ou igual dimenso da matriz. evidente que a procura do primeiro mximo de uma matriz pode recorrer a um ciclo. A estrutura do ciclo pois:
// P C 1 n n dim(m). int i = ...; while(G) { passo } // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

onde a condio objectivo do ciclo se refere varivel i, a ser devolvida pela funo no seu nal. Os dois primeiros passos da construo de um ciclo, obteno da condio invariante e da guarda, so, neste caso, idnticos aos do exemplo anterior: substituio da constante n por uma nova varivel k:
// P C 1 n n dim(m). int i = ...; int k = ...; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n. while(k != n) { passo } // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

A condio invariante indica que a varivel i contm sempre o ndice do primeiro elemento cujo valor o mximo dos valores contidos nos k primeiros elementos da matriz. A inicializao a usar tambm simples, embora desta vez no se possa inicializar k com 0, pois no existe mximo de um conjunto vazio (no se poderia atribuir qualquer valor a i)! Assim, a soluo inicializar k com 1 e i com 0:
// P C 1 n n dim(m). int i = 0; int k = 1; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n.

268

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


while(k != n) { passo } // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

Analisando os termos da condio invariante um a um, verica-se que: 1. 0 i < k ca 0 0 < 1 que verdadeiro; 2. (Q j : 0 j < k : m[j] m[i]) ca (Q j : 0 j < 1 : m[j] m[0]), que o mesmo que m[0] m[0], que verdadeiro; 3. (Q j : 0 j < i : m[j] < m[i]) ca (Q j : 0 j < 0 : m[j] < m[0]) que, como existem zero termos no quanticador, tem valor verdadeiro por denio; e 4. 0 k n ca 0 1 n, que verdadeira desde que a pr-condio o seja, o que se admite acontecer; isto , a inicializao leva veracidade da condio invariante, como se pretendia. O passo seguinte a determinao do progresso. Mais uma vez usa-se a simples incrementao de k em cada passo, que conduz forosamente terminao do ciclo:
// P C 1 n n dim(m). int i = 0; int k = 1; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n. while(k != n) { aco ++k; } // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

A parte mais interessante deste exemplo a determinao da aco a utilizar para manter a condio invariante verdadeira apesar do progresso. A aco tem de ser tal que
// CIG 0 i < k(Q j : 0 j < k : m[j] m[i])(Q j : 0 j < i : m[j] < m[i]) // 0 k n k = n, ou seja, // 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k < n. aco ++k; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n.

Mais uma vez comea-se por encontrar a pr-condio mais fraca do progresso:

5.3. ALGORITMOS COM MATRIZES E VECTORES


// 0 i < k + 1 (Q j : 0 j < k + 1 : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k + 1 n, ou seja, // 0 i k (Q j : 0 j < k + 1 : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 1 k < n. ++k; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n.

269

Se se admitir que 0 k, ento o ltimo termo do primeiro quanticador universal pode ser extrado:
// 0 i k (Q j : 0 j < k : m[j] m[i]) m[k] m[i] // (Q j : 0 j < i : m[j] < m[i]) 0 k < n. // 0 i k (Q j : 0 j < k + 1 : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 1 k < n.

Conclui-se que a aco dever ser escolhida de modo a que:


// 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k < n. aco // 0 i k (Q j : 0 j < k : m[j] m[i]) m[k] m[i] // (Q j : 0 j < i : m[j] < m[i]) 0 k < n.

claro que a aco dever afectar apenas a varivel i, pois a varivel k afectada pelo progresso. Mas como? Haver alguma circunstncia em que no seja necessria qualquer alterao da varivel i, ou seja, em que a aco possa ser a instruo nula? Comparando termo a termo as asseres antes e depois da aco, conclui-se que isso s acontece se m[k] m[i]. Ento a aco deve consistir numa instruo de seleco:
// 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k < n. if(m[k] <= m[i]) // G1 m[k] m[i]. ; // instruo nula! else // G2 m[i] < m[k]. instruo2 // 0 i k (Q j : 0 j < k : m[j] m[i]) m[k] m[i] // (Q j : 0 j < i : m[j] < m[i]) 0 k < n.

Resta saber que instruo deve ser usada para resolver o problema no caso em que m[i] < m[k]. Falta, pois, falta determinar uma instruo2 tal que:

270

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


// 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k < n m[i] < m[k]. instruo2 // 0 i k (Q j : 0 j < k : m[j] m[i]) m[k] m[i] // (Q j : 0 j < i : m[j] < m[i]) 0 k < n.

Antes da instruo, i contm o ndice do primeiro elemento contendo o maior dos valores dos elementos com ndices entre 0 e k exclusive. Por outro lado, o elemento de ndice k contm um valor superior ao valor do elemento de ndice i. Logo, h um elemento entre 0 e k inclusive com um valor superior a todos os outros: o elemento de ndice k. Assim, a varivel i dever tomar o valor k, de modo a continuar a ser o ndice do elemento com maior valor entre os valores inspeccionados. A instruo a usar portanto:
i = k;

A ideia que, quando se atinge um elemento com valor maior do que aquele que se julgava at ento ser o mximo, deve-se actualizar o ndice do mximo. Para vericar que assim , calcule-se a pr-condio mais fraca que conduz assero nal pretendida:
// 0 k k (Q j : 0 j < k : m[j] m[k]) m[k] m[k] // (Q j : 0 j < k : m[j] < m[k]) 0 k < n, ou seja, // (Q j : 0 j < k : m[j] < m[k]) 0 k < n. i = k; // 0 i k (Q j : 0 j < k : m[j] m[i]) m[k] m[i] // (Q j : 0 j < i : m[j] < m[i]) 0 k < n.

Falta pois vericar se


// 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k < n m[i] < m[k] // (Q j : 0 j < k : m[j] < m[k]) 0 k < n.

Eliminando os termos que no so necessrios para vericar a implicao,


// (Q j : 0 j < k : m[j] m[i]) 0 k < n m[i] < m[k] // (Q j : 0 j < k : m[j] < m[k]) 0 k < n.

evidente que a implicao verdadeira e, portanto, a atribuio i = k; resolve o problema. Assim, a aco do ciclo a instruo de seleco
if(m[k] <= m[i]) // G1 m[k] m[i]. ; // instruo nula! else // G2 m[i] < m[k]. i = k;

5.3. ALGORITMOS COM MATRIZES E VECTORES


que pode ser simplicada para uma instruo condicional mais simples
if(m[i] < m[k]) i = k;

271

O ciclo completo ca
// P C 1 n n dim(m). int i = 0; int k = 1; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n. while(k != n) { if(m[i] < m[k]) i = k; ++k; } // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

que pode ser convertido para um ciclo for:


// P C 1 n n dim(m). int i = 0; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]) // 0 k n. for(int k = 1; k != n; ++k) if(m[i] < m[k]) i = k; // CO 0 i < n (Q j : 0 j < n : m[j] m[i]) (Q j : 0 j < i : m[j] < m[i]).

A funo completa :
/** Devolve o ndice do primeiro elemento com o mximo valor entre os primeiros n elementos da matriz m. @pre P C 1 n n dim(m). @post CO 0 ndiceDoPrimeiroMximoDe < n (Q j : 0 j < n : m[j] m[ndiceDoPrimeiroMximoDe]) (Q j : 0 j < ndiceDoPrimeiroMximoDe : m[j] < m[ndiceDoPrimeiroMximoDe]). int ndiceDoPrimeiroMximoDe(int const m[], int const n) { assert(1 <= n); int i = 0; // CI 0 i < k (Q j : 0 j < k : m[j] m[i]) // (Q j : 0 j < i : m[j] < m[i]) 0 k n.

272

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


for(int k = 1; k != n; ++k) if(m[i] < m[k]) i = k; // Sem ciclos no se pode fazer muito melhor (amostragem em trs locais): assert(0 <= i < n and m[0] <= m[i] and m[n / 2] <= m[i] and m[n - 1] <= m[i]); return i; }

5.3.4 ndice do maior item de um vector


O desenvolvimento no caso dos vectores semelhante ao usado para as matrizes. A funo resultante desse desenvolvimento
/** Devolve o ndice do primeiro item com o mximo valor do vector v. @pre P C 1 v.size(). @post CO 0 ndiceDoPrimeiroMximoDe < v.size() (Q j : 0 j < v.size() : v[j] v[ndiceDoPrimeiroMximoDe]) (Q j : 0 j < ndiceDoPrimeiroMximoDe : v[j] < v[ndiceDoPrimeiroMximoDe]). int ndiceDoPrimeiroMximo(vector<int> const& v) { assert(1 <= v.size()); int i = 0; // CI 0 i < k (Q j : 0 j < k : v[j] v[i]) // (Q j : 0 j < i : v[j] < v[i]) 0 k v.size(). for(vector<int>::size_type k = 1; k != v.size(); ++k) if(v[i] < v[k]) i = k; // Sem ciclos no se pode fazer muito melhor (amostragem em trs locais): assert(0 <= i < v.size() and m[0] <= m[i] and m[v.size() / 2] <= m[i] and m[v.size() - 1] <= m[i]); return i; }

5.3.5 Elementos de uma matriz num intervalo


Pretende-se escrever uma funo que devolva o valor lgico verdadeiro se e s se os valores dos n primeiros elementos de uma matriz m estiverem entre mnimo e mximo (inclusive).

5.3. ALGORITMOS COM MATRIZES E VECTORES

273

Neste caso a estrutura da funo e a sua especicao (i.e., a sua pr-condio e a sua condio objectivo) so mais fceis de escrever:
/** Devolve verdadeiro se os primeiros n elementos da matriz m tm valores entre mnimo e mximo. @pre P C 0 n n dim(m). @post CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo). */ bool estEntre(int const m[], int const n, int const mnimo, int const mximo) { assert(0 <= n); ... }

Uma vez que esta funo devolve um valor booleano, que apenas pode ser V ou F, vale a pena vericar em que circunstncias cada uma das instrues de retorno
return false; return true;

resolve o problema. Comea por se vericar a pr-condio mais fraca da primeira destas instrues:
// CO F = (Q j : 0 j < n : mnimo m[j] mximo), ou seja, // (E j : 0 j < n : m[j] < mnimo mximo < m[j]). return false; // CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo).

Consequentemente, deve-se devolver falso se existir um elemento da matriz fora da gama pretendida. Depois verica-se a pr-condio mais fraca da segunda das instrues de retorno:
// CO V = (Q j : 0 j < n : mnimo m[j] mximo), ou seja, // (Q j : 0 j < n : mnimo m[j] mximo). return true; // CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo).

Logo, deve-se devolver verdadeiro se todos os elementos da matriz estiverem na gama pretendida. Que ciclo resolve o problema? Onde colocar, se que possvel, estas instrues de retorno? Seja a condio invariante do ciclo: CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n,

274

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

onde i uma varivel introduzida para o efeito. Esta condio invariante arma que todos os elementos inspeccionados at ao momento (com ndices inferiores a i) esto na gama pretendida. Esta condio invariante foi obtida intuitivamente, e no atravs da metodologia de Dijkstra. Em particular, esta condio invariante obriga o ciclo a terminar de uma forma pouco usual se se encontrar um elemento fora da gama pretendida, como se ver mais abaixo. Se a guarda for ento no nal do ciclo tem-se CI G, ou seja, que implica (Q j : 0 j < n : mnimo m[j] mximo) . Logo, a instruo
return true;

G i = n,

CI G (Q j : 0 j < i : mnimo m[j] mximo) 0 i n i = n,

deve terminar a funo. Que inicializao usar? A forma mais simples de tornar verdadeira a condio invariante inicializar i com 0, pois o quanticador qualquer que seja sem qualquer termo tem valor lgico verdadeiro. Pode-se agora acrescentar funo o ciclo parcialmente desenvolvido:
/** Devolve verdadeiro se os primeiros n elementos da matriz m tm valores entre mnimo e mximo. @pre P C 0 n n dim(m). @post CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo). */ bool estEntre(int const m[], int const n, int const mnimo, int const mximo) { assert(0 <= n); int i = 0; // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n. while(i != n) { passo } // Aqui recorreu-se a amostragem, mais uma vez: assert(n == 0 or // se n for zero a resposta correcta true. (mnimo <= m[0] <= mximo and (mnimo <= m[n / 2] <= mximo and (mnimo <= m[n - 1] <= mximo)); return true; }

5.3. ALGORITMOS COM MATRIZES E VECTORES


Que progresso usar? Pelas razes habituais, o progresso mais simples a usar :
++i;

275

Resta determinar a aco de modo a que a condio invariante seja de facto invariante. Ou seja, necessrio garantir que
// CI G (Q j : 0 j < i : mnimo m[j] mximo) 0 i n i = n, ou seja, // (Q j : 0 j < i : mnimo m[j] mximo) 0 i < n. aco ++i; // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n.

Vericando qual a pr-condio mais fraca que, depois do progresso, conduz veracidade da condio invariante, conclui-se:
// (Q j : 0 j < i + 1 : mnimo m[j] mximo) 0 i + 1 n, ou seja, // (Q j : 0 j < i + 1 : mnimo m[j] mximo) 1 i < n. ++i; // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n.

Se se admitir que 0 i, ento o ltimo termo do quanticador universal pode ser extrado:
// (Q j : 0 j < i : mnimo m[j] mximo)mnimo m[i] mximo0 i < n. // (Q j : 0 j < i + 1 : mnimo m[j] mximo) 1 i < n.

Conclui-se facilmente que, se mnimo m[i] mximo, ento no necessria qualquer aco para que a condio invariante se verique depois do progresso. Isso signica que a aco consiste numa instruo de seleco:
// (Q j : 0 j < i : mnimo m[j] mximo) 0 i < n. if(mnimo <= m[i] and m[i] <= mximo) // G1 mnimo m[i] mximo. ; // instruo nula! else // G2 m[i] < mnimo mximo < m[i]. instruo2 ++i; // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n.

Resta pois vericar que instruo deve ser usada na alternativa da instruo de seleco. Para isso comea por se vericar que, antes dessa instruo, se vericam simultaneamente a condio invariante a guarda e a segunda guarda da instruo de seleco, ou seja, CI G G2 (Q j : 0 j < i : mnimo m[j] mximo) 0 i < n (m[i] < mnimo mximo < m[i]) ,

276

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

Traduzindo para portugus vernculo: os primeiros i elementos da matriz esto dentro da gama pretendida mas o i + 1-simo (de ndice i) no est. claro portanto que CI G G2 (E j : 0 j < i + 1 : m[j] < mnimo mximo < m[j]) 0 i < n (E j : 0 j < n : m[j] < mnimo mximo < m[j]) Ou seja, existe pelo menos um elemento com ndice entre 0 e i inclusive que no est na gama pretendida e portanto o mesmo se passa para os elementos com ndices entre 0 e n exclusive. Conclui-se que a instruo alternativa da instruo de seleco deve ser
return false;

pois termina imediatamente a funo (e portanto o ciclo), devolvendo o valor apropriado (ver pr-condio mais fraca desta instruo mais atrs). A aco foi escolhida de tal forma que, ou termina o ciclo devolvendo o valor apropriado (falso), ou garante a validade da condio invariante apesar do progresso. A funo completa :
/** Devolve verdadeiro se os primeiros n elementos da matriz m tm valores entre mnimo e mximo. @pre P C 0 n n dim(m). @post CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo). */ bool estEntre(int const m[], int const n, int const mnimo, int const mximo) { assert(0 <= n); int i = 0; // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n. while(i != n) { if(mnimo <= m[i] and m[i] <= mximo) ; // instruo nula! else return false; ++i; } // Aqui recorreu-se a amostragem, mais uma vez: assert(n == 0 or // se n for zero a resposta correcta true. (mnimo <= m[0] <= mximo and (mnimo <= m[n / 2] <= mximo and (mnimo <= m[n - 1] <= mximo)); return true; }

5.3. ALGORITMOS COM MATRIZES E VECTORES

277

Trocando as instrues alternativas da instruo de seleco (e convertendo-a numa instruo condicional) e convertendo o ciclo while num ciclo for obtm-se a verso nal da funo:
/** Devolve verdadeiro se os primeiros n elementos da matriz m tm valores entre mnimo e mximo. @pre P C 0 n n dim(m). @post CO estEntre = (Q j : 0 j < n : mnimo m[j] mximo). */ bool estEntre(int const m[], int const n, int const mnimo, int const mximo) { assert(0 <= n); // CI (Q j : 0 j < i : mnimo m[j] mximo) 0 i n. for(int i = 0; i != n; ++i) if(m[i] < mnimo or mximo < m[i]) return false; // Aqui recorreu-se a amostragem, mais uma vez: assert(n == 0 or // se n for zero a resposta correcta true. (mnimo <= m[0] <= mximo and (mnimo <= m[n / 2] <= mximo and (mnimo <= m[n - 1] <= mximo)); return true; }

5.3.6 Itens de um vector num intervalo


O desenvolvimento no caso dos vectores semelhante ao usado para as matrizes. A funo resultante desse desenvolvimento
/** Devolve verdadeiro se os itens do vector v tm valores entre mnimo e mximo. @pre P C V. @post CO estEntre = (Q j : 0 j < v.size() : mnimo v[j] mximo). */ bool estEntre(vector<int> const& v, int const mnimo, int const mximo) { // CI (Q j : 0 j < i : mnimo v[j] mximo) 0 i v.size(). for(vector<int>::size_type i = 0; i != v.size(); ++i) if(v[i] < mnimo or mximo < v[i]) return false; // Aqui recorreu-se a amostragem, mais uma vez: assert(n == 0 or // se n for zero a resposta correcta true.

278

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


(mnimo <= m[0] <= mximo and (mnimo <= m[n / 2] <= mximo and (mnimo <= m[n - 1] <= mximo)); return true; }

5.3.7 Segundo elemento de uma matriz com um dado valor


O objectivo agora encontrar o ndice do segundo elemento com valor k nos primeiros n elementos de uma matriz m. Neste caso a pr-condio um pouco mais complicada do que para os exemplos anteriores, pois tem de se garantir que existem pelo menos dois elementos com o valor pretendido, o que, por si s, implica que a matriz tem de ter pelo menos dois elementos. A condio objectivo mais simples. Arma que o ndice a devolver deve corresponder a um elemento com valor k e que, no conjunto dos elementos com ndice menor, existe apenas um elemento com valor k (diz ainda que o ndice deve ser vlido, neste caso maior do que 0, porque tm de existir pelo menos dois elementos com valores iguais at ao ndice). Assim, a estrutura da funo :
/** Devolve o ndice do segundo elemento com valor k nos primeiros n elementos da matriz m. @pre P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). @post CO (N j : 0 j < ndiceDoSegundo : m[j] = k) = 1 1 ndiceDoSegundo < n m[ndiceDoSegundo] = k. */ int ndiceDoSegundo(int const m[], int const n, int const k) { int i = ...; ... return i; }

Para resolver este problema necessrio um ciclo, que pode ter a seguinte estrutura:
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; while(G) { passo } // CO (N j : 0 j < i : m[j] = k) = 1 1 i < n m[i] = k.

Neste caso no existe na condio objectivo do ciclo um quanticador onde se possa substituir (com facilidade) uma constante por uma varivel. Assim, a determinao da condio objectivo pode ser tentada factorizando a condio objectivo, que uma conjuno, em CI e G.

5.3. ALGORITMOS COM MATRIZES E VECTORES


Uma observao atenta das condies revela que a escolha apropriada
CI G

279

CO (N j : 0 j < i : m[j] = k) = 1 1 i < n m[i] = k Que signica esta condio invariante? Simplesmente que, durante todo o ciclo, tem de se garantir que h um nico elementos da matriz com valor k e com ndice entre 0 e i exclusive. O ciclo neste momento
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n. while(m[i] != k) { passo } // CO (N j : 0 j < i : m[j] = k) = 1 1 i < n m[i] = k.

Um problema com a escolha que se fez para a condio invariante que, aparentemente, no fcil fazer a inicializao: como escolher um valor para i tal que existe um elemento de valor k com ndice inferior a i? Em vez de atacar imediatamente esse problema, adia-se o problema e assume-se que a inicializao est feita. O passo seguinte, portanto, determinar o passo do ciclo. Antes do passo sabe-se que CI G, ou seja: CI G (N j : 0 j < i : m[j] = k) = 1 1 i < n m[i] = k. Mas
o mesmo que

(N j : 0 j < i : m[j] = k) = 1 m[i] = k (N j : 0 j < i + 1 : m[j] = k) = 1,

pois, sendo m[i] = k, pode-se estender a gama de valores tomados por j sem afectar a contagem de armaes verdadeiras: CI G (N j : 0 j < i + 1 : m[j] = k) = 1 1 i < n. Atente-se bem na expresso acima. Ser que pode ser verdadeira quando i atinge o seu maior valor possvel de acordo com o segundo termo da conjuno, i.e., quando i = n 1? Sendo i = n 1, o primeiro termo da conjuno ca (N j : 0 j < n : m[j] = k) = 1, o que no pode acontecer, dada a pr-condio! Logo i = n 1, e portanto CI G (N j : 0 j < i + 1 : m[j] = k) = 1 1 i < n 1. O passo tem de ser escolhido de modo a garantir a invarincia da condio invariante, ou seja, de modo a garantir que

280

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


// CI G (N j : 0 j < i + 1 : m[j] = k) = 1 1 i < n 1. passo // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

Comea por se escolher um progresso apropriado. Qual a forma mais simples de garantir que a guarda se torna falsa ao m de um nmero nito de passos? Simplesmente incrementando i. Se i atingisse alguma vez o valor n (ndice para alm do m da matriz) sem que a guarda se tivesse alguma vez tornado falsa, isso signicaria que a matriz no possua pelo menos dois elementos com o valor k, o que violaria a pr-condio. Logo, o ciclo tem de terminar antes de i atingir n, ao m de um nmero nito de passos, portanto. O passo do ciclo pode ento ser escrito como:
// CI G (N j : 0 j < i + 1 : m[j] = k) = 1 1 i < n 1. aco ++i; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

Determinando a pr-condio mais fraca do progresso que conduz vericao da condio invariante no seu nal,
// (N j : 0 j < i + 1 : m[j] = k) = 1 1 i + 1 < n, ou seja, // (N j : 0 j < i + 1 : m[j] = k) = 1 0 i < n 1. ++i; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

Assim sendo, a aco ter de ser escolhida de modo a garantir que


// CI G (N j : 0 j < i + 1 : m[j] = k) = 1 1 i < n 1. aco // (N j : 0 j < i + 1 : m[j] = k) = 1 0 i < n 1.

Mas isso consegue-se sem necessidade de qualquer aco, pois 1 i<n1 0 i<n1 O ciclo completo
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n. while(m[i] != k) ++i // CO (N j : 0 j < i : m[j] = k) = 1 1 i < n m[i] = k.

5.3. ALGORITMOS COM MATRIZES E VECTORES


E a inicializao?

281

A inicializao do ciclo anterior um problema por si s, com as mesmas pr-condies, mas com uma outra condio objectivo, igual condio invariante do ciclo j desenvolvido. Isto , o problema a resolver :
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; inic // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

Pretende-se que i seja maior do que o ndice da primeira ocorrncia de k na matriz. A soluo para este problema mais simples se se reforar a sua condio objectivo (que a condio invariante do ciclo anterior) um pouco mais. Pode-se impor que i seja o ndice imediatamente aps a primeira ocorrncia de k:
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; inic // (N j : 0 j < i : m[j] = k) = 1 m[i 1] = k 1 i < n. // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

Pode-se simplicar ainda mais o problema se se terminar a inicializao com uma incrementao de i e se calcular a pr-condio mais fraca dessa incrementao:
// P C int i = inic // (N j : // (N j : ++i; // (N j : 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). ...; 0 j < i + 1 : m[j] = k) = 1 m[i] = k 1 i + 1 < n, ou seja, 0 j < i + 1 : m[j] = k) = 1 m[i] = k 0 i < n 1. 0 j < i : m[j] = k) = 1 m[i 1] = k 1 i < n.

Sendo 0 i < n 1, ento (N j : 0 j < i + 1 : m[j] = k) = 1 m[i] = k 0 i < n 1 o mesmo que (N j : 0 j < i : m[j] = k) = 0 m[i] = k 0 i < n 1 ou ainda (ver Apndice A) (Q j : 0 j < i : m[j] = k) m[i] = k 0 i < n 1 pelo que o cdigo de inicializao se pode escrever:

282

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = ...; inic // CO (Q j : 0 j < i : m[j] = k) m[i] = k 0 i < n 1. ++i; // (N j : 0 j < i : m[j] = k) = 1 m[i 1] = k 1 i < n.

onde CO representa a condio objectivo do cdigo de inicializao e no a condio objectivo do ciclo j desenvolvido. A inicializao reduz-se portanto ao problema de encontrar o ndice do primeiro elemento com valor k. Este ndice forosamente inferior a n 1, pois a matriz, pela pr-condio, possui dois elementos com valor k. A soluo deste problema passa pela construo de um outro ciclo e relativamente simples, pelo que se apresenta a soluo sem mais comentrios (dica: factorize-se a condio objectivo):
// P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). int i = 0; // CI (Q j : 0 j < i : m[j] = k) 0 i < n 1. while(m[i] != k) ++i; // CO (Q j : 0 j < i : m[j] = k) m[i] = k 0 i < n 1. ++i; // (N j : 0 j < i : m[j] = k) = 1 m[i 1] = k 1 i < n. // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n.

A funo completa :
/** Devolve o ndice do segundo elemento com valor k nos primeiros n elementos da matriz m. @pre P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). @post CO (N j : 0 j < ndiceDoSegundo : m[j] = k) = 1 1 ndiceDoSegundo < n m[ndiceDoSegundo] = k. */ int ndiceDoSegundo(int const m[], int const n, int const k) { int i = 0; // CI (Q j : 0 j < i : m[j] = k) 0 i < n 1. while(m[i] != k) ++i; // CO (Q j : 0 j < i : m[j] = k) m[i] = k 0 i < n 1. ++i; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n. while(m[i] != k)

5.3. ALGORITMOS COM MATRIZES E VECTORES


++i return i; }

283

Pode-se obter cdigo mais fcil de perceber se se comear por desenvolver uma funo que devolva o primeiro elemento com valor k da matriz. Essa funo usa um ciclo que , na realidade o ciclo de inicializao usado acima:
/** Devolve o ndice do primeiro elemento com valor k nos primeiros n elementos da matriz m. @pre P C 1 n n dim(m) 1 (N j : 0 j < n : m[j] = k). @post CO (Q j : 0 j < ndiceDoPrimeiro : m[j] = k) 0 ndiceDoPrimeiro < n m[ndiceDoPrimeiro] = k. */ int ndiceDoPrimeiro(int const m[], int const n, int const k) { int i = 0; // CI (Q j : 0 j < i : m[j] = k) 0 i < n. while(m[i] != k) ++i; return i; } /** Devolve o ndice do segundo elemento com valor k nos primeiros n elementos da matriz m. @pre P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k). @post CO (N j : 0 j < ndiceDoSegundo : m[j] = k) = 1 1 ndiceDoSegundo < n m[ndiceDoSegundo] = k. */ int ndiceDoSegundo(int const m[], int const n, int const k) { int i = ndiceDoPrimeiro(m, n, k) + 1; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n. while(m[i] != k) ++i return i; }

Deixou-se propositadamente para o m a escrita das instrues de assero para vericao da pr-condio e da condio objectivo. No caso destas funes, quer a pr-condio quer a condio objectivo envolvem quanticadores. Ser que, por isso, as instrues de assero tm

284

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

de ser mais fracas do que deveriam, vericando apenas parte do que deveriam? Na realidade no. possvel escrever-se uma funo para contar o nmero de ocorrncias de um valor nos primeiros elementos de uma matriz, e us-la para substituir o quanticador de contagem:
/** Devolve o nmero de ocorrncias do valor k nos primeiros n elementos da matriz m. @pre P C 0 n n dim(m). @post CO ocorrnciasDe = (N j : 0 j < n : m[j] = k). */ int ocorrnciasDe(int const m[], int const n, int const k) { assert(0 <= n); int ocorrncias = 0; // CI ocorrnciasDe = (N j : 0 j < i : m[j] = k) 0 i n. for(int i = 0; i != n; ++i) if(m[i] == k) ++ocorrncias; return ocorrncias; } /** Devolve o ndice do primeiro elemento com valor k nos primeiros n elementos da matriz m. @pre P C 1 n n dim(m) 1 (N j : 0 j < n : m[j] = k). @post CO (Q j : 0 j < ndiceDoPrimeiro : m[j] = k) 0 ndiceDoPrimeiro < n m[ndiceDoPrimeiro] = k. */ int ndiceDoPrimeiro(int const m[], int const n, int const k) { assert(1 <= n and 1 <= ocorrnciasDe(m, n, k)); int i = 0; // CI (Q j : 0 j < i : m[j] = k) 0 i < n. while(m[i] != k) ++i; assert(ocorrnciasDe(m, i, k) == 0 and 0 <= i < n and m[i] = k); return i; } /** Devolve o ndice do segundo elemento com valor k nos primeiros n elementos da matriz m. @pre P C 2 n n dim(m) 2 (N j : 0 j < n : m[j] = k).

5.3. ALGORITMOS COM MATRIZES E VECTORES


@post CO (N j : 0 j < ndiceDoSegundo : m[j] = k) = 1 1 ndiceDoSegundo < n m[ndiceDoSegundo] = k. */ int ndiceDoSegundo(int const m[], int const n, int const k) { assert(2 <= n and 2 <= ocorrnciasDe(m, n, k)); int i = ndiceDoPrimeiro(m, n, k) + 1; // CI (N j : 0 j < i : m[j] = k) = 1 1 i < n. while(m[i] != k) ++i assert(ocorrnciasDe(m, i, k) == 1 and 1 <= i < n and m[i] = k); return i; }

285

Ao se especicar as funes acima, poder-se-ia ter decidido que, caso o nmero de ocorrncias do valor k na matriz fosse inferior ao desejado, estas deveriam devolver o valor n, pois sempre um ndice invlido (os ndices dos primeiros n elementos da matriz variam entre 0 e n 1) sendo por isso um valor apropriado para indicar uma condio de erro. Nesse caso as funes poderiam ser escritas como 22 :
/** Devolve o ndice do primeiro elemento com valor k nos primeiros n elementos da matriz m ou n se no existir. @pre P C 0 n n dim(m). @post CO ((Q j : 0 j < ndiceDoPrimeiro : m[j] = k) 0 ndiceDoPrimeiro < n m[ndiceDoPrimeiro] = k) ((Q j : 0 j < n : m[j] = k) ndiceDoPrimeiro = n). */ int ndiceDoPrimeiro(int const m[], int const n, int const k) { assert(0 <= n); int i = 0;
22

As condies objectivo das duas funes no so, em rigor, correctas. O problema que em 0 ndiceDoPrimeiro < n m[ndiceDoPrimeiro] = k

o segundo termo tem valor indenido para ndiceDoPrimeiro = n. Na realidade dever-se-ia usar uma conjuno especial, que tivesse valor falso desde que o primeiro termo tivesse valor falso independentemente do segundo termo estar ou no denido. Pode-se usar um smbolo especial para uma conjuno com estas caractersticas, por exemplo . De igual forma pode-se denir uma disjuno especial com valor verdadeiro se o primeiro termo for verdadeiro independentemente de o segundo termo estar ou no denido, por exemplo . Em [8] usam-se os nomes cand e cor com o mesmo objectivo. Em [11] chama-se-lhes and if e or else. Estes operadores binrios no so comutativos, ao contrrio do que acontece com a disjuno e a conjuno usuais. Na linguagem C++, curiosamente, s existem as verses no-comutativas destes operadores.

286

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


// CI (Q j : 0 j < i : m[j] = k) 0 i n. while(i != n and m[i] != k) ++i; assert((ocorrnciasDe(m, i, k) == 0 and 0 <= i < n and m[i] = k) or (ocorrnciasDe(m, n, k) == 0 and i == n)); return i; } /** Devolve o ndice do segundo elemento com valor k nos primeiros n elementos da matriz m ou n se no existir. @pre P C 0 n n dim(m). @post CO ((N j : 0 j < ndiceDoSegundo : m[j] = k) = 1 1 ndiceDoSegundo < n m[ndiceDoSegundo] = k) ((N j : 0 j < n : m[j] = k) < 2 ndiceDoSegundo = n). */ int ndiceDoSegundo(int const m[], int const n, int const k) { assert(0 <= n); int i = ndiceDoPrimeiro(m, n, k); if(i == n) return n; ++i; // CI (N j : 0 j < i : m[j] = k) = 1 1 i n. while(i != n and m[i] != k) ++i assert((ocorrnciasDe(m, i, k) == 1 and 1 <= i < n and m[i] = k) or (ocorrnciasDe(m, n, k) < 2 and i == n)); return i; }

5.3.8 Segundo item de um vector com um dado valor


O desenvolvimento no caso dos vectores semelhante ao usado para as matrizes. As funes resultantes desse desenvolvimento so
/** Devolve o nmero de ocorrncias do valor k nos primeiros n elementos

5.3. ALGORITMOS COM MATRIZES E VECTORES


do vector v. @pre P C 0 n n v.size(). @post CO ocorrnciasDe = (N j : 0 j < v.size() : v[j] = k). */ vector<int>::size_type ocorrnciasDe(vector<int> const& v, int const n, int const k) { assert(0 <= n and n <= v.size()); vector<int>::size_type ocorrncias = 0; // CI ocorrnciasDe = (N j : 0 j < i : v[j] = k) 0 i v.size(). for(vector<int>::size_type i = 0; i != v.size(); ++i) if(v[i] == k) ++ocorrncias; return ocorrncias; }

287

/** Devolve o ndice do primeiro item com valor k do vector v ou v.size() se no existir. @pre P C V. @post CO ((Q j : 0 j < ndiceDoPrimeiro : v[j] = k) 0 ndiceDoPrimeiro < v.size() v[ndiceDoPrimeiro] = k) ((Q j : 0 j < v.size() : v[j] = k) ndiceDoPrimeiro = v.size()). */ vector<int>::size_type ndiceDoPrimeiro(vector<int> const& v, int const k) { vector<int>::size_type i = 0; // CI (Q j : 0 j < i : v[j] = k) 0 i v.size(). while(i != v.size() and v[i] != k) ++i; assert((ocorrnciasDe(v, i, k) == 0 and 0 <= i < v.size() and v[i] = k) or (ocorrnciasDe(v, v.size(), k) == 0 and i == v.size())); return i; } /** Devolve o ndice do segundo elemento com valor k do vector v ou v.size() se no existir. @pre P C V. @post CO ((N j : 0 j < ndiceDoSegundo : v[j] = k) = 1 1 ndiceDoSegundo < v.size() v[ndiceDoSegundo] = k)

288

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


((N j : 0 j < v.size() : m[j] = k) < 2 ndiceDoSegundo = v.size()). */ vector<int>::size_type ndiceDoSegundo(vector<int> const& v, int const k) { int i = ndiceDoPrimeiro(m, k); if(i == v.size() return v.size(); ++i; // CI (N j : 0 j < i : v[j] = k) = 1 1 i v.size(). while(i != v.size() and v[i] != k) ++i assert((ocorrnciasDe(v, i, k) == 1 and 1 <= i < v.size() and v[i] = k) or (ocorrnciasDe(v, v.size(), k) < 2 and i == v.size())); return i; }

5.4 Cadeias de caracteres


A maior parte da comunicao entre humanos faz-se usando a palavra, falada ou escrita. natural, portanto, que o processamento de palavras escritas seja tambm parte importante da maior parte dos programas: a comunicao entre computador e humano suporta-se ainda fortemente na palavra escrita. Dos tipos bsicos do C++ faz parte o tipo char, que a base para todo este processamento. Falta, no entanto, forma de representar cadeias ou sequncias de caracteres. O C++ herdou da linguagem C uma forma de representao de cadeias de caracteres peculiar. So as chamadas cadeias de caracteres clssicas, representadas por matrizes de caracteres. As cadeias de caracteres clssicas so matrizes de caracteres em que o m da cadeia assinalado usando um caractere especial: o chamado caractere nulo, de cdigo 0, que no representa nenhum smbolo em nenhuma das possveis tabelas de codicao de caracteres (ver exemplo no Apndice G). As cadeias de caracteres clssicas so talvez ecientes, mas so tambm seguramente inexveis e desagradveis de utilizar, uma vez que sofrem de todos os inconvenientes das matrizes clssicas. claro que uma possibilidade de representao alternativa para as cadeias de caracteres seria recorrendo ao tipo genrico vector: a classe vector<char> permite de facto representar sequncias de caracteres arbitrrias. No entanto, a biblioteca padro do C++ fornece uma classe, de nome string, que tem todas as capacidades de vector<char> mas acrescenta uma quantidade considervel de operaes especializadas para lidar com cadeias de caracteres.

5.4. CADEIAS DE CARACTERES

289

Como se fez mais atrs neste captulo acerca das matrizes e dos vectores, comear-se- por apresentar brevemente a representao mais primitiva de cadeias de caracteres, passando-se depois a uma descrio mais ou menos exaustiva da classe string.

5.4.1 Cadeias de caracteres clssicas


A representao mais primitiva de cadeias de caracteres em C++ usa matrizes de caracteres em que o nal da cadeia marcado atravs de um caractere especial, de cdigo zero, e que pode ser explicitado atravs da sequncia de escape \0. Por exemplo,
char nome[] = {Z, a, c, a, r, i, a, s, \0};

dene uma cadeia de caracteres representando o nome Zacarias. importante perceber que uma matriz de caracteres s uma cadeia de caracteres clssica se possuir o terminador \0. Assim,
char nome_matriz[] = {Z, a, c, a, r, i, a, s};

uma matriz de caracteres mas no uma cadeia de caracteres clssica. Para simplicar a inicializao de cadeias de caracteres, a linguagem permite colocar a sequncia de caracteres do inicializador entre aspas e omitir o terminador, que colocado automaticamente. Por exemplo:
char dia[] = "Sbado";

dene uma cadeia com seis caracteres contendo Sbado e representada por uma matriz de dimenso sete: os seis caracteres da palavra Sbado e o caractere terminador. Uma cadeia de caracteres no necessita de ocupar toda a matriz que lhe serve de suporte. Por exemplo,
char const janeiro[12] = "Janeiro";

dene uma matriz de 12 caracteres constantes contendo uma cadeia de caracteres de comprimento sete, que ocupa exactamente oito elementos da matriz: os primeiros sete com os caracteres da cadeia e o oitavo com o terminador. As cadeias de caracteres podem ser inseridas em canais, o que provoca a insero de cada um dos seus caracteres (com excepo do terminador). Assim, dadas as denies acima, o cdigo
cout < < "O nome " < < nome < < . < < endl; cout < < "Hoje " < < dia < < . < < endl;

faz surgir no ecr

290

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


O nome Zacarias. Hoje Sbado.

Se se olhar atentamente o cdigo acima, vericar-se- que existe uma forma alternativa escrever cadeias de caracteres num programa: as chamadas cadeias de caracteres literais, que correspondem a uma sequncia de caracteres envolvida em aspas, por exemplo "O nome ". As cadeias de caracteres literais so uma peculiaridade da linguagem. Uma cadeia de caracteres literal: 1. uma matriz de caracteres, como qualquer cadeia de caracteres clssica. 2. Os seus caracteres so constantes. O tipo de uma cadeia de caracteres literal , por isso, char const [dimenso], onde dimenso o nmero de caracteres somado de um (para haver espao para o terminador). 3. No tem nome, ao contrrio das variveis usuais. 4. O seu mbito est limitado expresso em que usada. 5. Tem permanncia esttica, existindo durante todo o programa, como se fosse global. Para o demonstrar comece-se por um exemplo simples. possvel percorrer uma cadeia de caracteres usando um ciclo. Por exemplo:
char nome[] = "Zacarias"; for(int i = 0; nome[i] != \0; ++i) cout < < nome[i]; cout < < endl;

Este troo de cdigo tem exactamente o mesmo resultado que


char nome[] = "Zacarias"; cout < < nome < < endl;

A sua particularidade a guarda usada para o ciclo. que, como a dimenso da cadeia no conhecida partida, uma vez que pode ocupar apenas uma parte da matriz, a forma mais segura de a percorrer usar o terminador para vericar quando termina. As mesmas ideias podem ser usadas para percorrer uma cadeia de caracteres literal, o que demonstra claramente que estas so tambm matrizes:
int comprimento = 0; while("Isto um teste."[comprimento] != \0) ++comprimento; cout < < "O comprimento " < < comprimento < < . < < endl;

5.4. CADEIAS DE CARACTERES

291

Este troo de programa escreve no ecr o comprimento da cadeia Isto um teste., i.e., escreve 16. Uma cadeia de caracteres literal no pode ser dividida em vrias linhas. Por exemplo, o cdigo seguinte ilegal:
cout < < "Isto uma frase comprida que levou necessidade de a dividir em trs linhas. S que, infelizmente, de uma forma ilegal." < < endl;

No entanto, como cadeias de caracteres adjacentes no cdigo so consideradas como uma e uma s cadeia de caracteres literal, pode-se resolver o problema acima de uma forma perfeitamente legal e legvel:
cout < < "Isto uma frase comprida que levou necessidade de a " "dividir em trs linhas. Desta vez de uma " "forma legal." < < endl;

5.4.2 A classe string


As cadeias de caracteres clssicas devem ser utilizadas apenas onde indispensvel: para especicar cadeias de caracteres literais, e.g., para compor frases a inserir no canal de sada cout, como tem vindo a ser feito at aqui. Para representar cadeias de caracteres deve-se usar a classe string, denida no cheiro de interface com o mesmo nome. Ou seja, para usar esta classe deve-se colocar no topo dos programas a directiva
#include <string>

De este ponto em diante usar-se-o as expresses cadeia de caracteres e cadeia para classicar qualquer varivel ou constante do tipo string. Como todas as descries de ferramentas da biblioteca padro feitas neste texto, a descrio da classe string que se segue no pretende de modo algum ser exaustiva. Para tirar partido de todas as possibilidades das ferramentas da biblioteca padro indispensvel recorrer, por exemplo, a [12]. As cadeias de caracteres suportam todas as operaes dos vectores descritas na Seco 5.2, com excepo das operaes push_back(), pop_back(), front() e back(), pelo que sero apresentadas apenas as operaes especcas das cadeias de caracteres. Denio (construo) e inicializao Para denir e inicializar uma cadeia de caracteres pode-se usar qualquer das formas seguintes:

292

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS


string vazia; string nome = "Zacarias Zebedeu Zagalo"; string mesmo_nome = nome; string apelidos(nome, 9); string nome_do_meio(nome, 9, 7); string vinte_aa(20, a); // // // // // // // // // cadeia vazia, sem caracteres. a partir de cadeia clssica (literal, neste caso). cpia a partir de cadeia. cpia a partir da posio 9. cpia de 7 caracteres a partir da posio 9. dimenso inicial 20, tudo com a.

Atribuio Podem-se fazer atribuies entre cadeias de caracteres. Tambm se pode atribuir uma cadeia de caracteres clssica ou mesmo um simples caractere a uma cadeia de caracteres:
string nome1 = "Xisto Ximenes"; string nome2; string nome3; nome2 = nome1; // atribuio entre cadeias. nome1 = "Ana Anes"; // atribuio de cadeia clssica. nome3 = X; // atribuio de um s caractere (literal, neste caso).

Podem-se fazer atribuies mais complexas usando a operao assign():


string string string string nome = "Zacarias Zebedeu Zagalo"; apelidos; nome_do_meio; vinte_aa;

apelidos.assign(nome, 9, string::npos); // s nal de cadeia. nome_do_meio.assign(nome, 9, 7); // s parte de cadeia. vinte_aa.assign(20, a); // caractere a repetido 20 // vezes.

A constante string::npos maior do que o comprimento de qualquer cadeia possvel. Quando usada num local onde se deveria indicar um nmero de caracteres signica todos os restantes. Da que a varivel apelidos passe a conter Zebedeu Zagalo. Dimenso e capacidade As cadeias de caracteres suportam todas as operaes relativas a dimenses e capacidade dos vectores, adicionadas das operaes length() e erase():

5.4. CADEIAS DE CARACTERES


size() Devolve a dimenso actual da cadeia.

293

length() Sinnimo de size(), porque mais usual falar-se em comprimento que em dimenso de uma cadeia. max_size() Devolve a maior dimenso possvel de uma cadeia. resize(n, v) Altera a dimenso da cadeia para n. Tal como no caso dos vectores, o segundo argumento, o valor dos possveis novos caracteres, opcional (se no for especicado os novos caracteres sero o caractere nulo). reserve(n) Reserva espao para n caracteres na cadeia, de modo a evitar a inecincia associada a aumentos sucessivos. capacity() Devolve a capacidade actual da cadeia, que a dimenso at qual a cadeia pode crescer sem ter de requerer memria ao sistema operativo. clear() Esvazia a cadeia. erase() Sinnimo de clear(), porque mais usual dizer-se apagar do que limpar uma cadeia. empty() Indica se a cadeia est vazia. Indexao Os modos de indexao so equivalentes aos dos vectores. A indexao usando o operador de indexao [] insegura, no sentido em que a validade dos ndices no vericada. Para realizar uma indexao segura utilizar a operao at(). Acrescento Existem vrias formas elementares de acrescentar cadeias de caracteres:
string nome = "Zacarias"; string nome_do_meio = "Zebedeu"; nome += ; // um s caractere. nome += nome_do_meio; // uma cadeia. nome += " Zagalo"; // uma cadeia clssica (literal, neste caso).

Para acrescentos mais complexos existe a operao append():


string vinte_aa(10, a); // s 10... string contagem = "um dois "; string dito = "no h duas sem trs"; vinte_aa.append(10, a); // caracteres repetidos (10 novos a). contagem.append(dito, 17, 4); // quatro caracteres da posio 17 de dito.

294 Insero

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

De igual forma existem vrias verses da operao insert() para inserir material em lugares arbitrrios de uma cadeia:
string contagem = "E vo , e !"; string dito = "no h uma sem duas"; string duas = "duas"; contagem.insert(12, "trs"); // cadeia clssica na posio 12. contagem.insert(9, duas); // cadeia na posio 9. contagem.insert(7, dito, 7, 3); // parte da cadeia dito na posio 7. contagem.insert(6, 3, .); // caracteres repetidos na posio 6. // contagem ca com E vo ... uma, duas e trs!.

O material inserido antes da posio indicada. Substituio possvel, recorrendo s vrias operaes replace(), substituir pedaos de uma cadeia por outras sequncias de caractere. Os argumentos so os mesmos que para as operaes insert(), mas, para alm de se indicar onde comea a zona a substituir, indica-se tambm quantos caracteres substituir. Por exemplo:
string texto = "O nome $nome."; texto.replace(9, 5, "Zacarias"); // substitui $nome por Zacarias.

Procuras H muitas formas de procurar texto dentro de uma cadeia de caracteres. A primeira forma procura uma sequncia de caracteres do incio para o m (operao find()) ou do m para o incio (operao rfind()) da cadeia. Estas operaes tm como argumentos, em primeiro lugar, a sequncia a procurar e opcionalmente, em segundo lugar, a posio a partir da qual iniciar a procura (se no se especicar este argumento, a procura comea no incio ou no m da cadeia, consoante a direco de procura). A sequncia a procurar pode ser uma cadeia de caracteres, uma cadeia de caracteres clssica ou um simples caractere. O valor devolvido a posio onde a sequncia foi encontrada ou, caso no o tenha sido, o valor string::npos. Por exemplo:
string texto = "O nome $nome."; // Podia tambm ser texto.find($): string::size_type posio = texto.find("$nome");

5.4. CADEIAS DE CARACTERES

295

if(posio != string::npos) // Substitui $nome por Zacarias: texto.replace(posio, 5, "Zacarias");

A segunda forma serve para procurar caracteres de uma dado conjunto dentro de uma cadeia de caracteres. A procura feita do incio para o m (operao find_first_of()) ou do m para o incio (operao find_last_of()) da cadeia. O conjunto de caracteres pode ser dado na forma de uma cadeia de caracteres, de uma cadeia de caracteres clssica ou de um simples caractere. Os argumentos so mais uma vez o conjunto de caracteres a procurar e a posio inicial de procura, que opcional. O valor devolvido mais uma vez a posio encontrada ou string::npos se nada foi encontrado. Por exemplo:
string texto = "O nome $nome."; // Podia tambm ser texto.find_first_of($): string::size_type posio = texto.find_first_of("$%#", 3); // Comea-se em 3 s para claricar onde se especica a posio inicial de procura. if(posio != string::npos) // Substitui $nome por Zacarias: texto.replace(posio, 5, "Zacarias");

Existem ainda as operaes find_first_not_of() e find_last_not_of() se se quiser procurar caracteres que no pertenam a um dado conjunto de caracteres. Concatenao possvel concatenar cadeias usando o operador +. Por exemplo:
string um = "um"; string contagem = um + " dois" + + t + "rs";

Comparao A comparao entre cadeias feita usando os operadores de igualdade e relacionais. A ordem considerada para as cadeias lexicogrca (ver Seco 5.2.12) e depende da ordenao dos caracteres na tabela de codicao usada, que por sua vez baseada no respectivo cdigo. Assim, consultando o Apndice G, que contm a tabela de codicao Latin-1, usada por enquanto em Portugal, conclui-se que A < a a < Z < a z < a

296

CAPTULO 5. MATRIZES, VECTORES E OUTROS AGREGADOS

onde infelizmente a ordenao relativa de maisculas e minsculas e, sobretudo, dos caracteres acentuados, no respeita as regras habituais do portugus. Aqui entra-se pela terceira vez no problema das variaes regionais de critrios 23 . Estes problemas resolvem-se recorrendo ao conceito de locales, que saem fora do mbito deste texto. Assim, representando as cadeias por sequncias de caracteres entre aspas: leva < levada levada < levadia rfo > orfeo (infelizmente...) fama < fome etc. Formas mais complexas de comparao recorrem operao compare(), que devolve um valor positivo se a cadeia for maior que a passada como argumento, zero se for igual, e um valor negativo se for menor. Por exemplo:
string cadeia1 = "fama"; string cadeia2 = "fome"; int resultado = cadeia1.compare(cadeia2); if(resultado == 0) cout < < "So iguais!" < < endl; else if(resultado < 0) cout < < "A primeira a menor!" < < endl; else cout < < "A segunda a menor!" < < endl;

Este cdigo, para as cadeias indicadas, escreve:


A primeira a menor!

23 As duas primeiras no foram referidas explicitamente e dizem respeito ao formato dos valores decimais e de valores booleanos em operaes de insero e extraco. Seria desejvel, por exemplo, que os nmeros decimais aparecessem ou fossem lidos com vrgula em vez de ponto, e os valores booleanos por extenso na forma verdadeiro e falso, em vez de true e false.

Captulo 6

Tipos enumerados
Alm dos tipos de dados pr-denidos no C++, os chamados tipos bsicos, podem-se criar tipos de dados adicionais. Essa , alis, uma das tarefas fundamentais da programao centrada nos dados. Para j, abordar-se-o extenses mais simples aos tipos bsicos: os tipos enumerados. No prximo captulo falar-se- de tipos de primeira categoria, usando classes C++ Uma varivel de um tipo enumerado pode conter um nmero limitado de valores, que se enumeram na denio do tipo1 . Por exemplo,
enum DiaDaSemana { segunda_feira, tera_feira, quarta_feira, quinta_feira, sexta_feira, sbado, domingo };

dene um tipo enumerado com sete valores possveis, um para cada dia da semana. Convencionalmente no nome dos novos tipos todas as palavras comeam por uma letra maiscula e no se usa qualquer caractere para as separar. O novo tipo utiliza-se como habitualmente. Pode-se, por exemplo, denir variveis do novo tipo2 :
DiaDaSemana dia = quarta_feira;

Pode-se atribuir atribuir nova varivel dia qualquer dos valores listados na denio do tipo enumerado DiaDaSemana:
1 Esta armao uma pequena mentira piedosa. Na realidade os enumerados podem conter valores que no correspondem aos especicados na sua denio [12, pgina 77]. 2 Ao contrrio do que se passa com as classes, os tipos enumerados sofrem do mesmo problema que os tipos bsicos: variveis automticas de tipos enumerados sem inicializao explcita contm lixo!

297

298
dia = tera_feira;

CAPTULO 6. TIPOS ENUMERADOS

Cada um dos valores associados ao tipo DiaDaSemana (viz. segunda_feira, ..., domingo) utilizado como se fosse um valor literal para esse tipo, tal como 10 um valor literal do tipo int ou a um valor literal do tipo char. Convencionalmente nestes valores literais as palavras esto em minsculas e usa-se um sublinhado (_) para as separar 3 . Como se trata de um tipo denido pelo programador, no possvel, sem mais esforo, ler valores desse tipo do teclado ou escrev-los no ecr usando os mtodos habituais (viz. os operadores de extraco e insero em canais: > > e < <). Mais tarde ver-se- como se pode ensinar o computador a extrair e inserir valores de tipos denidos pelo utilizador de, e em, canais. Na maioria dos casos os tipos enumerados so usados para tornar mais claro o signicado dos valores atribudos a uma varivel. Por exemplo, segunda_feira tem claramente mais signicado que 0. Na realidade, os valores de tipos enumerados so representados como inteiros atribudos sucessivamente a partir de zero. Assim, segunda_feira tem representao interna 0, tera_feira tem representao 1, etc. De facto, se se tentar imprimir segunda_feira o resultado ser surgir 0 no ecr, que a sua representao na forma de um inteiro. possvel associar inteiros arbitrrios a cada um dos valores de uma enumerao, pelo que podem existir representaes idnticas para valores com nome diferentes:
enum DiaDaSemana { // agora com nomes alternativos... primeiro = 0, // inicializao redundante: o primeiro valor sempre 0. segunda = primeiro, segunda_feira = segunda, tera, tera_feira = tera, quarta, quarta_feira = quarta, quinta, quinta_feira = quinta, sexta, sexta_feira = sexta, sbado, domingo, ltimo = domingo, };

Se um operando de um tipo enumerado ocorrer numa expresso, ser geralmente convertido num inteiro. Essa converso tambm se pode explicitar, escrevendo int(segunda_feira), por exemplo. As converses opostas tambm so possveis, usando-se DiaDaSemana(2), por exemplo, para obter quarta_feira. Na prxima seco ver-se- como redenir os operadores existentes na linguagem de modo a operarem sobre tipos enumerados sem surpresas
3 Existe uma conveno alternativa em que os valores literais de tipos enumerados e os nomes das constantes se escrevem usando apenas maisculas com as palavras separadas por um sublinhado (e.g., SEGUNDA_FEIRA). Desaconselha-se o uso dessa conveno, pois confunde-se com a conveno de dar esse tipo de nomes a macros, que sero vistas no Captulo 9.

6.1. SOBRECARGA DE OPERADORES

299

desagradveis para o programador (pense-se no que deve acontecer quando se incrementa uma varivel do tipo DiaDaSemana que contm o valor domingo).

6.1 Sobrecarga de operadores


Da mesma forma que se podem sobrecarregar nomes de funes, i.e., dar o mesmo nome a funes que, tendo semanticamente o mesmo signicado, operam com argumentos de tipos diferentes (ou em diferente nmero), tambm possvel sobrecarregar o signicado dos operadores usuais do C++ de modo a que tenham um signicado especial quando aplicados a tipos denidos pelo programador. Se se pretender, por exemplo, sobrecarregar o operador ++ prexo (incrementao prexa) para funcionar com o tipo DiaDaSemana denido acima, pode-se denir uma rotina4 com uma sintaxe especial:
DiaDaSemana operator ++ (DiaDaSemana& dia) { if(dia == ltimo) dia = primeiro; else dia = DiaDaSemana(int(dia) + 1); return dia; }

ou simplesmente
DiaDaSemana operator ++ (DiaDaSemana& dia) { if(dia == ltimo) return dia = primeiro; else return dia = DiaDaSemana(int(dia) + 1); }

A nica diferena relativamente sintaxe habitual da denio de funes e procedimentos que se substitui o habitual nome do procedimento pela palavra-chave operator seguida do operador a sobrecarregar5 . No possvel sobrecarregar todos os operadores, ver Tabela 6.1. O operador foi construdo de modo a que a incrementao de uma varivel do tipo DiaDaSemana conduza sempre ao dia da semana subsequente. Utilizou-se primeiro e ltimo e no segunda_feira
Este operador no , em rigor, nem um procedimento nem uma funo. No um procedimento porque no se limita a incrementar: devolve o valor do argumento depois de incrementado. No uma funo porque no se limita a devolver um valor: altera, incrementando, o seu argumento. por esta razo que o operador ++ tem efeitos laterais, podendo a sua utilizao descuidada conduzir a expresses mal comportadas, com os perigos que da advm (Seco 2.7.8). 5 Em todo o rigor o operador de incrementao prexa deveria devolver uma referncia para um DiaDaSemana. Veja-se a Seco 7.7.1.
4

300

CAPTULO 6. TIPOS ENUMERADOS


Tabela 6.1: Operadores que possvel sobrecarregar. + bitor -= << >= -> compl *= >> and [] * not /= < <= or () / = %= > >= ++ new % < ^= == -new[] xor > &= != ->* delete bitand += |= <= , delete[]

e domingo, pois dessa forma pode-se mais tarde decidir que a semana comea ao Domingo sem ter de alterar o procedimento acima, alterando apenas a denio da enumerao. Este tipo de sobrecarga, como bvio, s pode ser feito para novos tipos denidos pelo programador. Esta restrio evita redenies abusivas do signicado do operador + quando aplicado a tipos bsicos como o int, por exemplo, que poderiam ter resultados trgicos.

Captulo 7

Tipos abstractos de dados e classes C++


In this connection it might be worthwhile to point out that the purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise. Edsger W Dijkstra, The Humble Programmer, Communications of the ACM, 15(10), 1972

Quando se fala de uma linguagem de programao, no se fala apenas da linguagem em si, com o seu lxico, sintaxe, gramtica e semntica. Fala-se tambm de um conjunto de ferramentas acessveis ao programador que, no fazendo parte da linguagem propriamente dita, esto acessveis em qualquer ambiente de desenvolvimento de programas. Ao conjunto dessas ferramentas adicionais que se encontra em todos os ambientes de desenvolvimento chama-se biblioteca padro (standard library). Da biblioteca padro do C++ fazem parte, por exemplo, os canais cin e cout, que permitem leituras do teclado e escritas para o ecr, o tipo string e o tipo genrico vector. Em rigor, portanto, o programador tem sua disposio no a linguagem em si, mas a linguagem equipada com a biblioteca padro. Para o programador, no entanto, tudo funciona como se a linguagem em si inclusse essas ferramentas. Isto , para o programador em C++ o que est acessvel no o C++, mas um C++ ++ de que fazem parte todas as ferramentas da biblioteca padro. A tarefa de um programador resolver problemas usando (pelo menos) um computador. F-lo atravs da escrita de programas numa linguagem de programao dada. Depois de especicado o problema com exactido, o programador inteligente comea por procurar, na linguagem bsica, na biblioteca padro e noutras quaisquer bibliotecas disponveis, ferramentas que resolvam o problema na totalidade ou pelo menos parcialmente: esta procura evita as perdas de tempo associadas ao reinventar da roda infelizmente ainda to em voga 1 . Se no existirem
Por outro lado, importante notar que se pede muitas vezes ao estudante que reinvente a roda. Faz-lo parte fundamental do treino na resoluo de problemas concretos. Convm, portanto, que o estudante se disponha a essa tarefa que fora do contexto da aprendizagem intil. Mas convm tambm que no se deixe viciar na resoluo por si prprio de todos os pequenos problemas que j foram resolvidos milhares de vezes. importante saber fazer um equilbrio entre a curiosidade intelectual de resolver esses problemas e o pragmatismo de procurar um soluo j pronta. Durante a vida acadmica, a balana deve pender fortemente no sentido da curiosidade intelectual. Finda a vida acadmica, o equilbrio deve pender mais para o pragmatismo.
1

301

302

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

ferramentas disponveis, ento h que constru-las. Ao faz-lo, o programador est a expandir mais uma vez a linguagem disponvel, que passa a dispor de ferramentas adicionais (digamos que incrementa de novo a linguagem para C++ ++ ++). ferramentas do programador biblioteca padro linguagem C++

Figura 7.1: A bibloteca padro do C++ e as ferramentas do programador como extenses funcionalidade bsica da linguagem C++. H essencialmente duas formas distintas de construir ferramentas adicionais para uma linguagem. A primeira passa por equipar a linguagem com operaes adicionais, na forma de rotinas, mas usando os tipos existentes (int, char, bool, double, matrizes, etc.): a chamada programao procedimental. A segunda passa por adicionar tipos linguagem e engloba a programao centrada nos dados (ou programao baseada em objectos). Para que os novos tipos criados tenham algum interesse, fundamental que tenham operaes prprias, que tm de ser concretizadas pelo programador. Assim, a segunda forma de expandir a linguagem passa necessariamente pela primeira. Neste captulo ver-se- a forma por excelncia de acrescentar tipos, e respectivas operaes, linguagem. No captulo anterior abordaram-se as simples e limitadas enumeraes, neste ver-se-o os tipos abstractos de dados, pea fundamental da programao centrada nos dados. A partir deste ponto, portanto, o nfase ser posto na construo de novos tipos. Neste captulo construir-se-o novos tipos relativamente simples e independentes uns dos outros. Quando se iniciar o estudo da programao orientada por objectos, em captulos posteriores, ver-se- como se podem desenhar classes e hierarquias de classes e quais as suas aplicaes na resoluo de problemas de maior escala.

7.1 De novo a soma de fraces


Na Seco 3.2.20 desenvolveu-se um pequeno programa para ler duas fraces do teclado e mostrar a sua soma. Neste captulo desenvolver-se- esse programa at construir uma pequena calculadora. Durante esse processo aproveitar-se- para introduzir uma quantidade considervel de conceitos novos. O programa apresentado na Seco 3.2.20 pode ser melhorado. Assim, apresenta-se abaixo uma verso melhorada nos seguintes aspectos:

7.1. DE NOVO A SOMA DE FRACES

303

A noo de mximo divisor comum facilmente generalizvel a inteiros negativos ou nulos. O nico caso complicado o de mdc(0, 0). Como bvio, todos os inteiros positivos so divisores comuns de zero, pelo que no existe este mximo divisor comum. No entanto, de toda a convenincia estender a denio do mximo divisor comum, arbitrando o valor 1 como resultado de mdc(0, 0). Ou seja, por denio mdc(0, 0) = 1. Assim, a funo mdc() foi exibilizada, tendo-se enfraquecido a respectiva pr-condio de modo a ser aceitar argumentos arbitrrios. A utilidade da cobertura do caso mdc(0, 0) ser vista mais tarde. O enfraquecimento da pr-condio da funo mdc permitiu enfraquecer tambm todas as restantes pr-condies, tornando o programa capaz de lidar com fraces com termos negativos. O ciclo usado na funo mdc() foi optimizado, passando a usar-se um ciclo pouco ortodoxo, com duas possveis sadas. Fica como exerccio para o leitor demonstrar o seu correcto funcionamento e vericar a sua ecincia. Foram acrescentadas rotinas para a leitura e clculo da soma de duas fraces. Nas rotinas lidando com fraces alterou-se o nome das variveis para explicitar melhor aquilo que representam (e.g., numerador em vez de n). Para evitar cdigo demasiado extenso para uma verso impressa deste texto, cada rotina denida antes das rotinas que dela fazem uso, no se fazendo uma distino clara entre declarao e denio. Mais tarde ser ver que esta no forosamente uma boa soluo. Uma vez que a pr-condio e a condio objectivo so facilmente identicveis pela sua localizao na documentao das rotinas, aps @pre e @post respectivamente, abandonou-se o hbito de nomear essas condies P C e CO. Protegeu-se de erros a leitura das fraces (ver Seco 7.14).
#include <iostream> #include <cassert> using namespace std; /** Devolve o mximo divisor comum dos inteiros passados como argumento. @pre m = m n = n. mdc(m, n) m = 0 n = 0 @post mdc = . */ 1 m=0n=0 int mdc(int m, int n) { if(m == 0 and n == 0) return 1; if(m < 0) m = -m; if(n < 0)

304
n = -n;

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

while(true) { if(m == 0) return n; n = n % m; if(n == 0) return m; m = m % n; } } /** Reduz a fraco recebida como argumento. @pre denominador = 0 denominador = d numerador = n. @post denominador = 0 mdc(numerador, denominador) = numerador 1 denominador = n . */ d void reduzFraco(int& numerador, int& denominador) { assert(denominador != 0); int mximo_divisor_comum = mdc(numerador, denominador); numerador /= mximo_divisor_comum; denominador /= mximo_divisor_comum; assert(denominador != 0 and mdc(numerador, denominador) == 1); } /** L do teclado uma fraco, na forma de dois inteiros sucessivos. @pre numerador = n denominador = d. @post Se cin e cin tem dois inteiros n e d disponveis para leitura, com d = 0, ento 0 < denominador mdc(numerador, denominador) = 1 n numerador denominador = d cin, seno numerador = n denominador = n cin. */ void lFraco(int& numerador, int& denominador) { int n, d; if(cin > > n > > d) if(d == 0) cin.setstate(ios_base::failbit); else { numerador = d < 0 ? -n : n; denominador = d < 0 ? -d : d;

7.1. DE NOVO A SOMA DE FRACES

305

reduzFraco(numerador, denominador); assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador * d == n * denominador and cin); return; } assert(not cin); } /** Soma duas fraces. @pre denominador1 = 0 denominador2 = 0. numerador numerador1 numerador2 @post denominador = denominador1 + denominador2 denominador = 0 mdc(numerador, denominador) = 1. */ void somaFraco(int& numerador, int& denominador, int const numerador1, int const denominador1, int const numerador2, int const denominador2) { assert(denominador1 != 0 and denominador2 != 0); numerador = numerador1 * denominador2 + numerador2 * denominador1; denominador = denominador1 * denominador2; reduzFraco(numerador, denominador); assert(denominador != 0 and mdc(numerador, denominador) == 1); } /** Escreve uma fraco no ecr no formato usual. @pre V. @post cout cout contm n/d (ou simplesmente n se d = 1) sendo n e d os valores de numerador e denominador. */ void escreveFraco(int const numerador, int const denominador) { cout < < numerador; if(denominador != 1) cout < < / < < denominador; } int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; int n1, d1, n2, d2;

306

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


lFraco(n1, d1); lFraco(n2, d2); if(not cin) { cerr < < "Opps! return 1; }

A leitura das fraces falhou!" < < endl;

// Calcular fraco soma reduzida: int n, d; somaFraco(n, d, n1, d1, n2, d2); // Escrever resultado: cout < < "A soma de "; escreveFraco(n1, d1); cout < < " com "; escreveFraco(n2, d2); cout < < " "; escreveFraco(n, d); cout < < . < < endl; }

A utilizao de duas variveis inteiras independentes para representar cada fraco no permite a denio de uma funo para proceder soma, visto que as funes em C++ podem devolver um nico valor. De facto, a utilizao de mltiplas variveis independentes para representar um nico valor torna o cdigo complexo e difcil de perceber. O ideal seria poder reescrever o cdigo da mesma forma que se escreveria se o seu objectivo fosse ler e somar inteiros, e no fraces. Sendo as fraces representaes dos nmeros racionais, pretende-se escrever o programa como se segue:
... int main() { cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; cin > > r1 > > r2; if(not cin) { cerr < < "Opps! return 1; }

A leitura dos racionais falhou!" < < endl;

Racional r = r1 + r2; cout < < "A soma de " < < r1 < < " com " < < r2 < < " "

7.2. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


< < r < < . < < endl; }

307

Este objectivo ir ser atingido ainda neste captulo.

7.2 Tipos Abstractos de Dados e classes C++


Como representar cada nmero racional com uma varivel apenas? necessrio denir um novo tipo que se comporte como qualquer outro tipo existente em C++. necessrio um TAD (Tipo Abstracto de Dados) ou tipo de primeira categoria 2 . Um TAD ou tipo de primeira categoria um tipo denido pelo programador que se comporta como os tipos bsicos, servindo para denir instncias, i.e., variveis ou constantes, que guardam valores sobre os quais se pode operar. A linguagem C++ proporciona uma ferramenta, as classes C++, que permite concretizar tipos de primeira categoria. importante notar aqui que o termo classe tem vrios signicados. Em captulos posteriores falar-se- de classes propriamente ditas, que servem para denir as caractersticas comuns de objectos dessa classe, e que se concretizam tambm usando as classes C++. Este captulo, por outro lado, debrua-se sobre os TAD, que tambm se concretizam custa de classes C++. Se se acrescentar que a fronteira entre TAD, cujo objectivo denir instncias, e as classes propriamente ditas, cujo objectivo denir as caractersticas comuns de objectos independentes, percebe-se que inevitvel alguma confuso de nomenclatura. Assim, sempre que se falar simplesmente de classe, ser na acepo de classe propriamente dita, enquanto que sempre que se falar do mecanismo da linguagem C++ que permite concretizar quer TAD quer classes propriamente ditas, usar-se- sempre a expresso classe C++ 3 . Assim: TAD Tipo denido pelo utilizador que se comporta como qualquer tipo bsico da linguagem. O seu objectivo permitir a denio de instncias que armazenam valores. O que distingue umas instncias das outras fundamentalmente o seu valor. Nos TAD o nfase pe-se na igualdade, pelo que as cpias so comuns. Classe propriamente dita Conceito mais complexo a estudar em captulos posteriores. Representam as caractersticas comuns de objectos independentes. O seu objectivo poder construir objectos independentes de cuja interaco e colaborao resulte o comportamento adequado do programa. O nfase pe-se na identidade, e no na igualdade, pelo que as cpias so infrequentes, merecendo o nome de clonagens. Classe C++ Ferramenta da linguagem que permite implementar quer TAD, quer classes propriamente ditas.
Na realidade os tipos de primeira categoria so concretizaes numa linguagem de programao de TAD, que so uma abstraco matemtica. Como os TAD na sua acepo matemtica esto fora (por enquanto) do mbito deste texto, os dois termos usam-se aqui como sinnimos. 3 Apesar do cuidado posto na redaco deste texto provvel que aqui e acol ocorram violaes a esta conveno. Espera-se que no sejam factor de distraco para o leitor.
2

308

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.2.1 Denio de TAD


possvel denir um novo tipo (um TAD) para representar nmeros racionais (na forma de uma fraco), como se segue:
/** Representa nmeros racionais. */ class Racional { public: // Isto magia (por enquanto). int numerador; int denominador; };

A sintaxe de denio de um TAD custa de uma classe C++ , portanto,


class nome_do_tipo { declarao_de_membros };

sendo importante notar que este um dos poucos locais onde a linguagem exige um terminador (;) depois de uma chaveta nal4 . A notao usada para representar a classe C++ Racional pode ser vista na Figura 7.2. nome da classe Racional numerador: int denominador: int tipo nome Figura 7.2: Notao usada para representar a classe C++ Racional. A denio de uma classe C++ consiste na declarao dos seus membros. A denio da classe estabelece um modelo segundo o qual sero construdas as respectivas variveis. No caso apresentado, as variveis do tipo Racional, quando forem construdas, consistiro em dois membros: um numerador e um denominador do tipo int. Neste caso os membros so simples
Ao contrrio do que acontece na denio de rotinas e nos blocos de instrues em geral, o terminador aqui imprescindvel, pois a linguagem C++ permite a denio simultnea de um novo tipo de de variveis desse tipo. Por exemplo: class Racional { ... } r; // Dene o TAD Racional e uma varivel r numa nica instruo. M ideia, mas possvel. Note-se que esta possibilidade deve ser evitada na prtica.
4

atributos

7.2. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

309

variveis, mas poderiam ser tambm constantes. s variveis e constantes membro de uma classe d-se o nome de atributos. Tal como as matrizes, as classes permitem guardar agregados de informao (ou seja, agregados de variveis ou constantes, chamados elementos no caso das matrizes e membros no caso das classes), com a diferena de que, no caso das classes, essa informao pode ser de tipos diferentes. As variveis de um TAD denem-se como qualquer varivel do C++:
TAD nome [= expresso];

ou
TAD nome[(expresso, ...)];

Por exemplo:
Racional r1, r2;

dene duas variveis r1 e r2 no inicializadas, i.e., contendo lixo (mais tarde se ver como se podem evitar construes sem inicializao em TAD). Para classes C++ que representem meros agregados de informao possvel inicializar cada membro da mesma forma como se inicializam os elementos de uma matriz clssica do C++:
Racional r1 = {6, 9}; Racional r2 = {7, 3};

Note-se, no entanto, que esta forma de inicializao deixar de ser possvel (e desejvel) quando se equipar a classe C++ com um construtor, como se ver mais frente. As instrues apresentadas constroem duas novas variveis do tipo Racional, r1 e r2, cada uma das quais com verses prprias dos atributos numerador e denominador. s variveis de um TAD tambm comum chamar-se objectos e instncias, embora em rigor o termo objecto deva ser reservado para as classes propriamente ditas, a estudar em captulos posteriores. Para todos os efeitos, os atributos da classe Racional funcionam como variveis guardadas quer dentro da varivel r1, quer dentro da varivel r2. A notao usada para representar instncias de uma classe a que se pode ver na Figura 7.3, onde ca claro que os atributos so parte das instncias da classe. Deve-se comparar a Figura 7.2 com a Figura 7.3, pois na primeira representa-se a classe Racional e na segunda as variveis r1 e r2 dessa classe.

310

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

r1: Racional numerador = 6 denominador = 9 valor atributo


(a) Notao usual.

r2: Racional numerador = 7 denominador = 3

r1: Racional numerador: int 6 denominador: int 9


(b) Com sub-instncias.

r2: Racional numerador: int 7 denominador: int 3

r1: Racional
2 3

r2: Racional
7 3

(c) Como TAD com valor lgico representado.

Figura 7.3: Notaes usadas para representar instncias da classe C++ Racional.

7.2. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

311

7.2.2 Acesso aos membros


O acesso aos membros de uma instncia de uma classe C++ faz-se usando o operador de seleco de membro, colocando como primeiro operando a instncia a cujo membro se pretende aceder, depois o smbolo . e nalmente o nome do membro pretendido:
instncia.membro

Por exemplo,
Racional r1, r2; r1.numerador = 6; r1.denominador = 9; r2.numerador = 7; r2.denominador = 3;

constri duas novas variveis do tipo Racional e atribui valores aos respectivos atributos. Os nomes dos membros de uma classe s tm visibilidade dentro dessa classe, pelo que poderia existir uma varivel de nome numerador sem que isso causasse qualquer problema:
Racional r = {6, 9}; int numerador = 1000;

7.2.3 Alguma nomenclatura


s instncias, i.e., variveis ou constantes, de uma classe C++ comum chamar-se objectos, sendo essa a razo para as expresses programao baseada em objectos e programao orientada para os objectos. No entanto, reservar-se- o termo objecto para classes C++ que sejam concretizaes de classes propriamente ditas, e no para classes C++ que sejam concretizaes de TAD. s variveis e constantes membro de uma classe C++ tambm se chama atributos. Podem tambm existir rotinas membro de uma classe C++. A essas funes ou procedimentos chama-se operaes. No contexto das classes propriamente ditas, em vez de se dizer invocar uma operao para uma instncia (de uma classe) diz-se por vezes enviar uma mensagem a um objecto. Como se ver mais tarde, quer os atributos, quer as operaes podem ser de instncia ou de classe, consoante cada instncia da classe C++ possua conceptualmente a sua prpria cpia do membro em causa ou exista apenas uma cpia desse membro partilhada entre todas as instncias da classe.

312

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Todas as rotinas tm uma interface e uma implementao, e as rotinas membro no so excepo. Normalmente o termo operao usado para designar a rotina membro do ponto de vista da sua interface, enquanto o termo mtodo usado para designar a implementao da rotina membro. Para j, a cada operao corresponde um e um s mtodo, mas mais tarde se ver que possvel associar vrios mtodos mesma operao. Ao conjunto dos atributos e das operaes de uma classe C++ chama-se caractersticas, embora, como se ver, o que caracteriza um TAD seja apenas a sua interface, que normalmente no inclui quaisquer atributos.

7.2.4 Operaes suportadas pelas classes C++


Ao contrrio do que se passa com as matrizes, as variveis de uma classe C++ podem-se atribuir livremente entre si. O efeito de uma atribuio o de copiar todos os atributos (de instncia) entre as variveis em causa. Da mesma forma, possvel construir uma instncia de uma classe a partir de outra instncia da mesma classe, cando a primeira igual segunda. Por exemplo:
Racional r1 = {6, 9}; Racional r2 = r1; // r2 construda igual a r1. Racional r3; r3 = r1; // o valor de r1 atribudo a r3, cando as variveis iguais.

Da mesma forma, esto bem denidas as devolues e a passagem de argumentos por valor para valores de uma classe C++: as instncias de um TAD concretizado por intermdio de uma classe C++ podem ser usadas exactamente da mesma forma que as instncias dos tipos bsicos. possvel, por isso, usar uma funo, e no um procedimento, para calcular a soma de dois racionais no programa em desenvolvimento. Antes de o fazer, no entanto, far-se- uma digresso sobre as formas de representao de nmero racionais.

7.3 Representao de racionais por fraces


Qualquer nmero racional pode ser representado por uma fraco, que um par ordenado de nmeros inteiros (n, d), em que n e d so os termos da fraco 5 . Ao segundo termo d-se o nome de denominador ( o que d o nome fraco) e ao primeiro numerador (diz a quantas fraces nos referimos). Por exemplo, (3, 4) signica trs quartos. Normalmente os racionais representam-se gracamente usando uma notao diferente da anterior: n/d ou n . d Uma fraco n s representa um nmero racional se d = 0. Por outro lado, importante sad ber se fraces diferentes podem representar o mesmo racional ou se, pelo contrrio, fraces diferentes representam sempre racionais diferentes. A resposta questo inversa evidente:
5

H representaes alternativas para as fraces, ver [1][7].

7.3. REPRESENTAO DE RACIONAIS POR FRACES

313

2 racionais diferentes tm forosamente representaes diferentes. Mas 4 , 1 e 2 so fraces 2 1 que correspondem a um nico racional, e que, por acaso, tambm um inteiro. Para se obter uma representao em fraces que seja nica para cada racional, necessrio introduzir algumas restries adicionais.

Em primeiro lugar, necessrio usar apenas o numerador ou o denominador para conter o sinal do nmero racional. Como j se imps uma restrio ao denominador, viz. d = 0, natural impor uma restrio adicional: d deve ser no-negativo. Assim, 0 < d. Mas necessria uma restrio adicional. Para que a representao seja nica, tambm necessrio que n e d no tenham qualquer divisor comum diferente de 1, i.e., que mdc(n, d) = 1. Uma fraco nestas condies diz-se em termos mnimos e dos seus termos diz-se que so mutuamente primos. 2 Dos trs exemplos acima ( 4 , 1 e 2 ), apenas a ltima fraco verica todas as condies 2 1 enunciadas, ou seja, tem denominador positivo e numerador e denominador so mutuamente primos. Uma fraco n que verique estas condies, i.e., 0 < d mdc(n, d) = 1, diz-se no d formato cannico.

7.3.1 Operaes aritmticas elementares


As operaes aritmticas elementares (adio, subtraco, multiplicao, diviso, simtrico e identidade) esto bem denidas para os racionais (com excepo da diviso por 0, ou melhor, 0 por 1 ). Assim, em termos da representao dos racionais como fraces, o resultado das operaes aritmticas elementares pode ser expresso como n1 n2 + d1 d2 n1 n2 d1 d2 n1 n2 d1 d2 n1 n2 / d1 d2 n d n + d = = = = = = n1 d 2 + n 2 d 1 , d1 d 2 n1 d 2 n 2 d 1 , d1 d 2 n1 n 2 , d1 d 2 n1 n1 d 2 d1 se n2 = 0, n2 = d1 n 2 d2 n , e d n . d

7.3.2 Canonicidade do resultado


Tal como denidas, algumas destas operaes sobre fraces no garantem que o resultado esteja no formato cannico, mesmo que as fraces que servem de operandos o estejam. Este problema fcil de resolver, no entanto, pois dada uma fraco n que no esteja forosamente d no formato cannico, pode-se dividir ambos os termos pelo seu mximo divisor comum para mdc(n,d) obter uma fraco equivalente em termos mnimos, n/ mdc(n,d) , e, se o denominador for negad/ tivo, pode-se multiplicar ambos os termos por -1 para obter uma fraco equivalente com o numerador positivo, n . d

314

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.3.3 Aplicao soma de fraces


Voltando classe C++ denida,
/** Representa nmeros racionais. */ class Racional { public: // Isto magia (por enquanto). int numerador; int denominador; };

muito importante estar ciente das diferenas entre a concretizao do conceito de racional e o conceito em si: os valores representveis num int so limitados, o que signica que no possvel representar qualquer racional numa varivel do tipo Racional, tal como no era possvel representar qualquer inteiro numa varivel do tipo int. Os problemas causados por esta diferena sero ignorados durante a maior parte deste captulo, embora na Seco 7.13 sejam, seno resolvidos, pelo menos mitigados. Um mero agregado de dois inteiros, mesmo com um nome sugestivo, no s tem pouco interesse, como poderia representar muitas coisas diferentes. Para que esse agregado possa ser considerado a concretizao de um TAD, necessrio denir tambm as operaes que o novo tipo suporta. Uma das operaes a implementar a soma. Pode-se implementar a soma actualizando o procedimento do programa original para a seguinte funo:
/** Devolve a soma de dois racionais. @pre r1.denominador = 0 r2.denominador = 0. @post somaDe = r1 + r2 somaDe.denominador = 0 mdc(somaDe.numerador, somaDe.denominador) = 1. */ Racional somaDe(Racional const r1, Racional const r2) { assert(r1.denominador1 != 0 and r2.denominador2 != 0); Racional r; r.numerador = r1.numerador * r2.denominador + r2.numerador * r1.denominador; r.denominador = r1.denominador * r2.denominador; reduz(r); assert(r.denominador != 0 and mdc(r.numerador, r.denominador) == 1); return r; }

7.3. REPRESENTAO DE RACIONAIS POR FRACES

315

onde reduz() um procedimento para reduzir a fraco que representa o racional, i.e., uma adaptao do procedimento reduzFraco(). O programa pode agora ser reescrito ser na ntegra para usar a nova classe C++, devendo-se ter o cuidado de colocar a denio da classe C++ Racional antes da sua primeira utilizao no programa. Pode-se aproveitar para alterar os nomes das rotinas, onde o suxo Fraco se torna desnecessrio, dado o tipo dos respectivos parmetros:
#include <iostream> #include <cassert> using namespace std; /** Devolve o mximo divisor comum dos inteiros passados como argumento. @pre m = m n = n. mdc(m, n) m = 0 n = 0 @post mdc = . */ 1 m=0n=0 int mdc(int m, int n) { ... } /** Representa nmeros racionais. */ class Racional { public: // Isto magia (por enquanto). int numerador; int denominador; }; /** Reduz a fraco que representa o racional recebido como argumento. @pre r.denominador = 0 r = r. @post r.denominador = 0mdc(r.numerador, r.denominador) = 1r = r. */ void reduz(Racional& r) { assert(r.denominador != 0); int k = mdc(r.numerador, r.denominador); r.numerador /= k; r.denominador /= k; assert(r.denominador != 0 and mdc(r.numerador, r.denominador) == 1); } /** L do teclado um racional, na forma de dois inteiros sucessivos. @pre r = r. @post Se cin e cin tem dois inteiros n e d disponveis para leitura, com d = 0, ento

316

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


0 < r.denominador mdc(r.numerador, r.denominador) = 1 r = n cin, d seno r = r cin. */ void l(Racional& r) { int n, d; if(cin > > n > > d) if(d == 0) cin.setstate(ios_base::failbit); else { r.numerador = d < 0 ? -n : n; r.denominador = d < 0 ? -d : d; reduz(r); assert(0 < r.denominador and mdc(r.numerador, r. denominador) == 1 and r.numerador * d == n * r.denominador and cin); return; } assert(not cin); } /** Devolve a soma de dois racionais. @pre r1.denominador = 0 r2.denominador = 0. @post somaDe = r1 + r2 somaDe.denominador = 0 mdc(somaDe.numerador, somaDe.denominador) = 1. */ Racional somaDe(Racional const r1, Racional const r2) { assert(r1.denominador != 0 and r2.denominador != 0); Racional r; r.numerador = r1.numerador * r2.denominador + r2.numerador * r1.denominador; r.denominador = r1.denominador * r2.denominador; reduz(r); assert(r.denominador != 0 and mdc(r.numerador, r.denominador) == 1); return r;

7.3. REPRESENTAO DE RACIONAIS POR FRACES


} /** Escreve um racional no ecr no formato de uma fraco. @pre V. @post cout cout contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional r. */ d void escreve(Racional const r) { cout < < r.numerador; if(r.denominador != 1) cout < < / < < r.denominador; } int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; l(r1); l(r2); if(not cin) { cerr < < "Opps! return 1; }

317

A leitura das fraces dos racionais falhou!" < < endl;

// Calcular racional soma: Racional r = somaDe(r1, r2); // Escrever resultado: cout < < "A soma de "; escreve(r1); cout < < " com "; escreve(r2); cout < < " "; escreve(r); cout < < . < < endl; }

Ao escrever este pedao de cdigo o programador assumiu dois papeis: produtor e consumidor. Quando deniu a classe C++ Racional e a funo somaDe(), que opera sobre variveis dessa classe C++, fez o papel de produtor. Quando escreveu a funo main(), assumiu o papel de consumidor.

318

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.3.4 Encapsulamento e categorias de acesso


O leitor mais atento ter reparado que o cdigo acima tem pelo menos um problema: a classe Racional no tem qualquer mecanismo que impea o programador de colocar 0 (zero) no denominador de uma fraco:
Racional r1; r.numerador = 6; r.denominador = 0;

ou
Racional r1 = {6, 0};

Isto claramente indesejvel, e tem como origem o facto do produtor ter tornado pblicos os membros numerador e denominador da classe: esse o signicado do especicador de acesso public. De facto, os membros de uma classe podem pertencer a uma de trs categorias de acesso: pblico, protegido e privado. Para j apenas se descrevero a primeira e a ltima. Membros pblicos, introduzidos pelo especicador de acesso public:, so acessveis sem qualquer restrio. Membros privados, introduzidos pelo especicador de acesso private:, so acessveis apenas por membros da mesma classe (ou, alternativamente, por funes amigas da classe, que sero vistas mais tarde). Fazendo uma analogia de uma classe com um clube, dir-se-ia que h certas partes de um clube que esto abertas ao pblico e outras que esto disposio apenas dos seus membros. O consumidor de um relgio ou de um micro-ondas assume que no precisa de conhecer o funcionamento interno desses aparelhos, podendo recorrer apenas a uma interface. Assim, o produtor desses aparelhos normalmente esconde o seu mecanismo numa caixa, deixando no exterior apenas a interface necessria para o consumidor. Tambm o produtor da classe C++ Racional deveria ter escondido os pormenores de implementao da classe C++ do consumidor nal. Podem-se resumir estas ideias num princpio bsico da programao: Princpio do encapsulamento: O produtor deve esconder do consumidor nal tudo o que puder ser escondido. I.e., os pormenores de implementao devem ser escondidos, devendose fornecer interfaces limpas e simples para a manipulao das entidades fabricadas (aparelhos de cozinha, relgios, rotinas C++, classes C++, etc.). Isso consegue-se, no caso das classes C++, usando o especicador de acesso private: para esconder os membros da classe:
/** Representa nmeros racionais. */ class Racional { private: int numerador; int denominador; };

7.3. REPRESENTAO DE RACIONAIS POR FRACES

319

Ao se classicar os membros numerador e denominador como privados no se impede o programador consumidor de, usando mecanismos mais ou menos obscuros e perversos, aceder ao seu valor. O facto de um membro ser privado no coloca barreiras muito fortes quanto ao seu acesso. Pode-se dizer que funciona como um aviso, esse sim forte, de que o programador consumidor no deve aceder a eles, para seu prprio bem (o produtor poderia, por exemplo, decidir alterar os nomes dos membros para n e d, com isso invalidando cdigo que zesse uso directo dos membros da classe). O compilador encarrega-se de gerar erros de compilao por cada acesso ilegal a membros privados de uma classe. Assim, claro que os membros privados de uma classe C++ fazem parte da sua implementao, enquanto os membros pblicos fazem parte da sua interface. Tornados os atributos da classe privados, torna-se impossvel no procedimento l() atribuir valores directamente aos seus membros. Da mesma forma, todas as outras rotinas deixam de poder aceder aos atributos da classe. A inicializao tpica dos agregados, por exemplo
Racional r1 = {6, 9};

tambm deixa de ser possvel. Que fazer?

7.3.5 Rotinas membro: operaes e mtodos


Uma vez que a membros privados tm acesso quaisquer outros membros da classe, a soluo passa por tornar as rotinas existentes membros da classe C++ Racional. Comear-se- por tornar o procedimento escreve() membro da classe, i.e., por transform-lo de simples rotina em operao do TAD em concretizao:
... /** Representa nmeros racionais. */ class Racional { public: /** Escreve o racional no ecr no formato de uma fraco. @pre *this = r. @post *this = r(cout cout contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this). */ d void escreve(); // Declarao da rotina membro: operao. private: int numerador; int denominador; }; // Denio da rotina membro: mtodo. void Racional::escreve()

320
{

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

cout < < numerador; if(denominador != 1) cout < < / < < denominador; } ...

So de notar quatro pontos importantes: 1. Para o consumidor da classe C++ poder invocar a nova operao, necessrio que esta seja pblica. Da o especicador de acesso public:, que coloca a nova operao escreve() na interface da classe C++. 2. Qualquer operao ou rotina membro de uma classe C++ tem de ser declarada dentro da denio dessa classe e denida fora ou, alternativamente, denida (e portanto tambm declarada) dentro da denio da classe. Recorda-se que implementao de uma operao se chama mtodo, e que por isso todos os mtodos fazem parte da implementao de uma classe C++6 . 3. A operao escreve() foi declarada sem qualquer parmetro. 4. H um pormenor na denio do mtodo escreve() que novo: o nome do mtodo precedido de Racional::. Esta notao serve para indicar que escreve() um mtodo correspondente a uma operao da classe Racional, e no uma rotina vulgar. Onde ir a operao Racional::escreve() buscar o racional a imprimir? De onde vem as variveis numerador e denominador usadas no corpo do mtodo Racional::escreve()? Em primeiro lugar, recorde-se que o acesso aos membros de uma classe se faz usando o operador de seleco de membro. Ou seja,
instncia.nome_do_membro

em que instncia uma qualquer instncia da classe em causa. Esta notao to vlida para atributos como para operaes, pelo que a instruo para escrever a varivel r no ecr, no programa em desenvolvimento, deve passar a ser:
r.escreve();

O que acontece que instncia atravs da qual a operao Racional::escreve() invocada est explcita na prpria invocao, mas est implcita durante a execuo do respectivo mtodo! Mais, essa instncia que est implcita durante a execuo pode ser modicada pelo
Em captulos posteriores se ver que as classes propriamente ditas podem ter mais do que um mtodo associado a cada operao.
6

7.3. REPRESENTAO DE RACIONAIS POR FRACES

321

mtodo, pelo menos se for uma varivel. Tudo funciona como se a instncia usada para invocar a operao fosse passada automaticamente por referncia. Durante a execuo do mtodo Racional::escreve(), numerador e denominador referemse aos atributos da instncia atravs da qual a respectiva operao foi invocada. Assim, quando se adaptar o nal do programa em desenvolvimento para
int main() { ... // Escrever resultado: ... r1.escreve(); ... r2.escreve(); ... r.escreve(); ... }

durante a execuo do mtodo Racional::escreve() as variveis numerador e denominador referir-se-o sucessivamente aos correspondentes atributos de r1, r2, e r. instncia que est implcita durante a execuo de um mtodo chama-se naturalmente instncia implcita (ou varivel implcita se for uma varivel, ou constante implcita se for uma constante), pelo que no exemplo anterior a instncia implcita durante a execuo do mtodo comea por ser r1, depois r2 e nalmente r. possvel explicitar a instncia implcita durante a execuo de um mtodo da classe, ou seja, a instncia atravs da qual a respectiva operao foi invocada. Para isso usa-se a construo *this7. Esta construo usou-se na documentao da operao escreve(), nomeadamente no seu contrato, para deixar claro que a invocao da operao no afecta a instncia implcita. Mais tarde se ver uma forma mais elegante de garantir a constncia da instncia implcita durante a execuo de um mtodo, i.e, uma forma de garantir que a instncia implcita tratada como uma constante implcita, mesmo que na realidade seja uma varivel. Resolvemos o problema do acesso aos atributos privados para o procedimento escreve(), transformando-o em procedimento membro da classe C++. necessrio fazer o mesmo para todas as outras rotinas que acedem directamente aos atributos:
#include <iostream> #include <cassert> using namespace std; /** Devolve o mximo divisor comum dos inteiros passados como argumento.
7

O signicado do operador * car claro em captulos posteriores.

322

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


@pre m = m n = n. mdc(m, n) m = 0 n = 0 . */ @post mdc = 1 m=0n=0 int mdc(int m, int n) { ... } /** Representa nmeros racionais. */ class Racional { public: /** Escreve o racional no ecr no formato de uma fraco. @pre *this = r. @post *this = r(cout cout contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this). */ d void escreve(); /** Devolve a soma com o racional recebido como argumento. @pre denominador = 0 r2.denominador = 0 *this = r. @post *this = r somaCom = *this + r2 denominador = 0 somaCom.denominador = 0 mdc(somaCom.numerador, somaCom.denominador) = 1. */ Racional somaCom(Racional const r2); /** L do teclado um novo valor para o racional, na forma de dois inteiros sucessivos. @pre *this = r. @post Se cin e cin tem dois inteiros n e d disponveis para leitura, com d = 0, ento 0 < denominador mdc(numerador, denominador) = 1 *this = n cin, d seno *this = r cin. */ void l(); private: int numerador; int denominador; /** Reduz a fraco que representa o racional. @pre denominador = 0 *this = r. @post denominador = 0 mdc(numerador, denominador) = 1 *this = r. */ void reduz(); }; void Racional::escreve() {

7.3. REPRESENTAO DE RACIONAIS POR FRACES


cout < < numerador; if(denominador != 1) cout < < / < < denominador; } Racional Racional::somaCom(Racional const r2) { assert(denominador != 0 and r2.denominador != 0); Racional r; r.numerador = numerador * r2.denominador + r2.numerador * denominador; r.denominador = denominador * r2.denominador; r.reduz();

323

assert(denominador != 0 and r.denominador != 0 and mdc(r.numerador, r.denominador) == 1); return r; } void Racional::l() { int n, d; if(cin > > n > > d) if(d == 0) cin.setstate(ios_base::failbit); else { numerador = d < 0 ? -n : n; denominador = d < 0 ? -d : d; reduz(); assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador * d == n * denominador and cin); return; } assert(not cin); } void Racional::reduz() {

324

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


assert(denominador != 0); int k = mdc(numerador, denominador); numerador /= k; denominador /= k; assert(denominador != 0 and mdc(numerador, denominador) == 1); } int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; r1.l(); r2.l(); if(not cin) { cerr < < "Opps! return 1; }

A leitura dos racionais falhou!" < < endl;

// Calcular racional soma: Racional r = r1.somaCom(r2); // Escrever resultado: cout < < "A soma de "; r1.escreve(); cout < < " com "; r2.escreve(); cout < < " "; r.escreve(); cout < < . < < endl; }

Na operao Racional::somaCom(), soma-se a instncia implcita com o argumento passado operao. No programa acima, por exemplo, a varivel r1 da funo main() funciona como instncia implcita durante a execuo do mtodo correspondente operao Racional::somaCom() e r2 funciona como argumento. O procedimento reduz() foi transformado em operao privada da classe C++ que representa o TAD em desenvolvimento. Tomou-se tal deciso por no haver qualquer necessidade de o consumidor do TAD se preocupar directamente com a representao em fraco dos racionais. O consumidor do TAD limita-se a preocupar-se com o comportamento exterior do tipo. Pelo contrrio, para o produtor da classe C++ a representao dos racionais fundamental, pois ele que tem de garantir que todas as operaes cumprem o respectivo contrato.

7.4. CLASSES C++ COMO MDULOS

325

A invocao da operao Racional::reduz() no mtodo Racional::l() feita sem necessidade de usar a sintaxe usual para a invocao de operaes, i.e., sem indicar explicitamente a instncia atravs da qual (e para a qual) essa invocao feita. Isso deve-se ao facto de se pretender fazer a invocao para a instncia implcita. Seria possvel explicitar essa instncia,
(*this).reduz();

tal como de resto poderia ter sido feito para os atributos,


(*this).numerador = n;

mas isso conduziria apenas a cdigo mais denso. Note-se que os parnteses em volta de *this so fundamentais, pois o operador de seleco de membro tem maior precedncia que o operador unrio * (ou seja, o operador contedo de, a estudar mais tarde). tambm importante perceber-se que no existe qualquer vantagem em tornar a funo mdc() membro na nova classe C++. Em primeiro lugar, pode haver necessidade de calcular o mximo divisor comum de outros inteiros que no o numerador e o denominador. Alis, tal necessidade surgir ainda durante este captulo. Em segundo lugar porque o clculo do mximo divisor comum poder ser necessrio em contextos que nada tenham a ver com nmeros racionais. Finalmente, a notao usada para calcular a soma
Racional r = r1.somaCom(r2);

horrenda, sem dvida alguma. Numa seco posterior se ver como sobrecarregar o operador + de modo a permitir escrever
Racional r = r1 + r2;

7.4 Classes C++ como mdulos


Das discusses anteriores, nomeadamente sobre o princpio do encapsulamento e as categorias de acesso dos membros de uma classe, torna-se claro que as classes C++ so uma unidade de modularizao. De facto, assim . Alis, as classes so a unidade de modularizao por excelncia na linguagem C++ e na programao baseada em (e orientada para) objectos. Como qualquer mdulo que se preze, as classes C++ distinguem claramente interface e implementao. A interface de uma classe C++ corresponde aos seus membros pblicos. Usualmente a interface de uma classe C++ consiste num conjunto de operaes e tipos pblicos. A implementao de uma classe C++ consiste, pelo contrrio, nos membros privados e na denio das respectivas operaes, i.e., nos mtodos da classe. Normalmente a implementao de uma classe C++ contm os atributos da classe, particularmente as variveis membro, e operaes utilitrias, necessrias apenas para o programador produtor da classe.

326

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

de toda a convenincia que os atributos de uma classe C++ (e em especial as suas variveis membro) sejam privados. S dessa forma se garante que um consumidor da classe no pode, perversa ou acidentalmente, alterar os valores dos atributos de tal forma que um instncia da classe C++ deixe de estar num estado vlido. Este assunto ser retomado com maior pormenor mais abaixo, quando se falar da chamada CIC (Condio Invariante de Classe). As classes C++ possuem tambm um manual de utilizao, correspondente ao contrato entre o seu produtor e os seus consumidores. Esse contrato normalmente expresso atravs de um comentrio de documentao para a classe em si e dos comentrios de documentao de todas os seus membros pblicos.

7.4.1 Construtores
Suponha-se o cdigo Racional a; a.numerador = 1; a.denominador = 3; ou Racional a = {1, 3}; A partir do momento em que os atributos da classe passaram a ser privados ambas as formas de inicializao8 deixaram de ser possveis. Como resolver este problema? Para os tipos bsicos da linguagem, a inicializao faz-se usando uma de duas possveis sintaxes:
int a = 10;

ou
int a(10);

Se realmente se pretende que a nova classe C++ Racional represente um tipo de primeira categoria, importante fornecer uma forma de os racionais poderem se inicializados de uma forma semelhante. Por exemplo,
Racional r(1, 3); // Pretende-se que inicialize r com o racional 1 . 3
Na realidade no primeiro troo de cdigo no se faz uma inicializao. As operaes de atribuio alteram os valores dos atributos j inicializados (ou melhor, a atributos deixados por inicializar pelas regras absurdas importadas da linguagem C, e por isso contendo lixo).
8

7.4. CLASSES C++ COMO MDULOS


ou mesmo
2 Racional r = 2; // Pretende-se que inicialize r com o racional 1 . 3 Racional r(3); // Pretende-se que inicialize r com o racional 1 .

327

Por outro lado, deveria ser possvel evitar o comportamento dos tipos bsicos do C++ e eliminar completamente as instncias por inicializar, fazendo com que falta de uma inicializao explcita, os novos racionais fossem inicializados com o valor zero, (0, representado pela fraco 0 ). Ou seja, 1
Racional r; Racional r(0); Racional r(0, 1);

deveriam ser instrues equivalentes. Finalmente, deveria haver alguma forma de evitar a inicializao de racionais com valores impossveis, nomeadamente com denominador nulo. I.e., a instruo
Racional r(3, 0);

deveria de alguma forma resultar num erro. Quando se constri uma instncia de uma classe C++, chamado um procedimento especial que se chama construtor da classe C++. Esse construtor fornecido implicitamente pela linguagem e um construtor por omisso, i.e., um construtor que se pode invocar sem lhe passar quaisquer argumento9 . O construtor por omisso fornecido implicitamente constri cada um dos atributos da classe invocando o respectivo construtor por omisso. Neste caso, como os atributos so de tipos bsicos da linguagem, no so inicializados durante a sua construo, ao contrrio do que seria desejvel, contendo por isso lixo. Para evitar o problema, deve ser o programador produtor a declarar explicitamente um ou mais construtores (e, j agora, denilos com o comportamento pretendido), pois nesse caso o construtor por omisso deixa de ser fornecido implicitamente pela linguagem. Uma vez que se pretende que os racionais sejam inicializados por omisso com zero, tem de se fornecer um construtor por omisso explicitamente que tenha esse efeito:
/** Representa nmeros racionais. */ class Racional { public: /** Constri racional com valor zero. Construtor por omisso. @pre V. @post *this = 0 0 < denominador mdc(numerador, denominador) = 1. */
Nem sempre a linguagem fornece um construtor por omisso implicitamente. Isso acontece quando a classe tem atributos que so constantes, referncias, ou que no tm construtores por omisso, entre outros casos.
9

328
Racional(); ... private: ... };

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Racional::Racional() : numerador(0), denominador(1) { assert(0 < denominador and mdc(numerador, denominador) == 1); }

Os construtores so operaes de uma classe C++, mas so muito especiais, quer por por razes semnticas, quer por razes sintcticas. Do ponto de vista semntico, o que os distingue dos outros operadores o facto de no serem invocados atravs de variveis da classe preexistentes. Pelo contrrio, os construtores so invocados justamente para construir uma nova varivel. Do ponto de vista sintctico os construtores tm algumas particularidades. A primeira que tm o mesmo nome que a prpria classe. Os construtores so como que funes membro, pois tm como resultado uma nova varivel da classe a que pertencem. No entanto, no s no se pode indicar qualquer tipo de devoluo no seu cabealho, como no seu corpo no permitido devolver qualquer valor, pois este age sobre uma instncia implcita em construo. Quando uma instncia de uma classe construda, por exemplo devido denio de uma varivel dessa classe, invocado o construtor da classe compatvel com os argumentos usados na inicializao. I.e., possvel que uma classe tenha vrios construtores sobrecarregados, facto de que se tirar partido em breve. Os argumentos so passados aos construtores colocando-os entre parnteses na denio das instncias. Por exemplo, as instrues
Racional r; Racional r(0); Racional r(0, 1);

deveriam todas construir uma nova varivel racional com o valor zero, muito embora para j s a primeira instruo seja vlida, pois a classe ainda no possui construtores com argumentos. Note-se que as instrues
Racional r; Racional r();

no so equivalentes! Esta irregularidade sintctica do C++ deve-se ao facto de a segunda instruo ter uma interpretao alternativa: a de declarar uma funo r que no tem parmetros

7.4. CLASSES C++ COMO MDULOS

329

e devolve um valor Racional. Face a esta ambiguidade de interpretao, a linguagem optou por dar preferncia declarao de uma funo... Aquando da construo de uma instncia de uma classe C++, um dos seus construtores invocado. Antes mesmo de o seu corpo ser executado, no entanto, todos os atributos da classe so construdos. Se se pretender passar argumentos aos construtores dos atributos, ento obrigatria a utilizao de listas de utilizadores, que se colocam na denio do construtor, entre o cabealho e o corpo, aps o smbolo dois-pontos (:). Esta lista consiste no nome dos atributos pela mesma ordem pela qual esto denidos na classe C++, seguido cada um dos argumentos a passar ao respectivo construtor colocados entre parnteses. No caso da classe C++ Racional, pretende-se inicializar os atributos numerador e denominador respectivamente com os valores 0 e 1, pelo que a lista de inicializadores
Racional::Racional() : numerador(0), denominador(1) { ... }

Uma vez que se pretendem mais duas formas de inicializao dos racionais, necessrio fornecer dois construtores adicionais. O primeiro constri um racional a partir de um nico inteiro, o que quase to simples como construir um racional com o valor zero. O segundo um pouco mais complicado, pois, construindo um racional a partir do numerador e denominador de uma fraco, precisa de receber garantidamente um denominador no-nulo e tem de ter o cuidado de garantir que os seus atributos, numerador e denominador, esto no formato cannico das fraces:
/** Representa nmeros racionais. */ class Racional { public: /** Constri racional com valor zero. Construtor por omisso. @pre V. @post *this = 0 0 < denominador mdc(numerador, denominador) = 1. */ Racional(); /** Constri racional com valor inteiro. @pre V. @post *this = n 0 < denominador mdc(numerador, denominador) = 1. */ Racional(int const n); /** Constri racional correspondente a n/d. @pre d = 0. @post *this = n d 0 < denominador mdc(numerador, denominador) = 1. */ Racional(int const n, int const d);

330

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

... private: ... }; Racional::Racional() : numerador(0), denominador(1) { assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador == 0); } Racional::Racional(int const n) : numerador(n), denominador(1) { assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador == n * denominador); } Racional::Racional(int const n, int const d) : numerador(d < 0 ? -n : n), denominador(d < 0 ? -d : d) { assert(d != 0); reduz(); assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador * d == n * denominador); } ...

Uma observao atenta dos trs construtores revela que os dois primeiros so quase iguais, enquanto o terceiro mais complexo, pois necessita vericar o sinal do denominador recebido no parmetro d e, alm disso, tem de se preocupar com a reduo dos termos da fraco. Assim, surge naturalmente a ideia de condensar os dois primeiros construtores num nico, no se fazendo o mesmo relativamente ao ltimo construtor ( custa do qual poderiam ser obtidos os dois primeiros), por razes de ecincia. A condensao dos dois primeiros construtores num nico faz-se recorrendo aos parmetros com argumentos por omisso, vistos na Seco 3.6:

7.4. CLASSES C++ COMO MDULOS


/** Representa nmeros racionais. */ class Racional { public: /** Constri racional com valor inteiro. Construtor por omisso. @pre V. @post *this = n 0 < denominador mdc(numerador, denominador) = 1. */ Racional(int const n = 0); /** Constri racional correspondente a n/d. @pre d = 0. @post *this = n d 0 < denominador mdc(numerador, denominador) = 1. */ Racional(int const n, int const d); ... private: ... }; Racional::Racional(int const n) : numerador(n), denominador(1) { assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador == n * denominador); } Racional::Racional(int const n, int const d) : numerador(d < 0 ? -n : n), denominador(d < 0 ? -d : d) { assert(d != 0); reduz(); assert(0 < denominador and mdc(numerador, denominador) == 1 and numerador * d == n * denominador); } ...

331

A Figura 7.4 mostra a notao usada para representar a classe C++ Racional desenvolvida at aqui.

332

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


Racional atributo privado operao pblica construtores -numerador: int -denominador: int constructor +Racional(in n: int = 0) +Racional(in n: int, in d: int) query +somaCom(in r2: Racional): Racional +escreve() update +le() -reduz() operaes atributos

inspectores

modicadores operao privada

Figura 7.4: A classe C++ Racional agora tambm com operaes. Note-se a utilizao de + e - para indicar a caractersticas pblicas e privadas da classe C++, respectivamente, e o termo in para indicar que o argumento da operao Racional::somaCom() passado por valor, ou seja, apenas para dentro da operao.

7.4.2 Construtores por cpia


Viu-se que a linguagem fornece implicitamente um construtor por omisso para as classes, excepto quando estas declaram algum construtor explicitamente. Algo de semelhante se passa relativamente aos chamados construtores por cpia. Estes construtores so usados para construir uma instncia de uma classe custa de outra instncia da mesma classe. A linguagem fornece tambm implicitamente um construtor por cpia, desde que tal seja possvel, para todas as classes C++ que no declarem explicitamente um construtor por cpia. O construtor por cpia fornecido implicitamente limita-se a invocar os construtores por cpia para construir os atributos da instncia em construo custa dos mesmos atributos na instncia original, sendo a invocao realizada por ordem de denio dos atributos na denio da classe. possvel, e muitas vezes desejvel, declarar ou mesmo denir explicitamente um construtor por cpia para as classes. Este assunto ser tratado com pormenor num captulo posterior.

7.4.3 Condio invariante de classe


Na maior parte das classes C++ que concretizam um TAD, os atributos s esto num estado aceitvel se vericarem um conjunto de restries, expressos normalmente na forma de uma condio a que se d o nome de condio invariante de classe ou CIC. A classe dos racionais possui uma condio invariante de classe que passa por exigir que os atributos numerador e denominador sejam o numerador e o denominador da fraco cannica representativa do

7.4. CLASSES C++ COMO MDULOS


racional correspondente, i.e., CIC 0 < denominador mdc(numerador, denominador).

333

A vantagem da denio de uma condio invariante de classe que todos os mtodos correspondentes a operaes pblicas bem como todas as rotinas amigas da classe C++ (que fazem parte da interface da classe com o consumidor, Seco 7.15) poderem admitir que os atributos das variveis da classe C++ com que trabalham vericam inicialmente a condio, o que normalmente os simplica bastante. I.e., a condio invariante de classe pode ser vista como parte da pr-condio quer de mtodos correspondentes a operaes pblicas, quer de rotinas amigas da classe C++. Claro que, para serem bem comportadas, as rotinas, membro e no membro, tambm devem garantir que a condio se verica para todas as variveis da classe C++ criadas ou alteradas por essas rotinas. Ou seja, a condio invariante de classe para cada instncia da classe criada ou alterada pelas mesmas rotinas pode ser vista tambm como parte da sua condio objectivo. Tal como sucedia nos ciclos, em que durante a execuo do passo a condio invariante muitas vezes no se vericava, embora se vericasse garantidamente antes e aps o passo, tambm a condio invariante de classe pode no se vericar durante a execuo dos mtodos pblicos ou das rotinas amigas da classe C++ em causa, embora se verique garantidamente no seu incio e no seu nal. Durante os perodos em que a condio invariante de classe no verdadeira, pode ser conveniente invocar alguma rotina auxiliar, que portanto ter de lidar com instncias que no vericam a condio invariante de classe e que poder tambm no garantir que a mesma condio se verica para as instncias por si criadas ou alteradas. Essas rotinas mal comportadas devem ser privadas, de modo a evitar utilizaes errneas por parte do consumidor nal da classe C++ que coloquem alguma instncia num estado invlido. A denio de uma condio invariante de classe e a sua imposio entrada e sada dos mtodos pblicos e de rotinas amigas de uma classe C++ no passa de um esforo intil se as suas variveis membro forem pblicas, i.e., se o seu estado for altervel do exterior. Se o forem, o consumidor da classe C++ pode alterar o estado de uma varivel da classe, por engano ou maliciosamente, invalidando a condio invariante de classe, com consequncias potencialmente dramticas no comportamento da classe C++ e no programa no seu todo. Essas consequncias so normalmente graves, porque as rotinas que lidam com as variveis membro da classe assumem que estas vericam a condio invariante de classe, no fazendo quaisquer garantias acerca do seu funcionamento quando ela no se verica. De todas as operaes de uma classe C++, as mais importantes so porventura as operaes construtoras10 . So estas que garantem que as instncias so criadas vericando imediatamente a condio invariante de classe. A sua importncia pode ser vista na classe Racional, em que os construtores garantem, desde que as respectivas pr-condies sejam respeitadas, que a condio invariante da classe se verica para as instncias construdas. Finalmente, de notar que algumas classes C++ no tm condio invariante de classe. Tais classes C++ no so normalmente concretizaes de nenhum TAD, sendo meros agregados de
10 Note-se que construtor e operao construtora no signicam forosamente a mesma coisa. A noo de operao construtora mais geral, e refere-se a qualquer operao que construa novas variveis da classe C++. claro que os construtores so operaes construtoras, mas uma funo membro pblica que devolva um valor da classe C++ em causa tambm o .

334

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

informao. o caso, por exemplo, de um agregado que guarde nome e morada de utentes de um servio qualquer. Essas classes C++ tm normalmente todas as suas variveis membro pblicas, e por isso usam normalmente a palavra-chave struct em vez de class. Note-se que estas palavras chave so quase equivalentes, pelo que a escolha de class ou struct meramente convencional, escolhendo-se class para classes C++ que sejam concretizaes de TAD ou classes propriamente ditas, e struct para classes C++ que sejam meros agregados de informao. A nica diferena entre as palavras chave struct e class que, com a primeira, todos os membros so pblicos por omisso, enquanto com a segunda todos os membros so privados por omisso.

7.4.4 Porqu o formato cannico das fraces?


Qual a vantagem de manter todas as fraces que representam os racionais no seu formato cannico? I.e., qual a vantagem de impor 0 < denominador mdc(numerador, denominador)

como condio invariante de classe C++?

A verdade que esta condio poderia ser consideravelmente relaxada: para o programador consumidor, a representao interna dos racionais irrelevante, muito embora ele espere que a operao escreve() resulte numa representao cannica dos racionais. Logo, o problema poderia ser resolvido alterando apenas o mtodo escreve(), de modo a reduzir a fraco, deixando o restante cdigo de se preocupar com a questo. Ou seja, poder-se-ia relaxar a condio invariante de classe para denominador = 0. No entanto, a escolha de uma condio invariante de classe mais forte trar algumas vantagens. A primeira vantagem tem a ver com a unicidade de representao garantida pela condio invariante de classe escolhida: a cada racional corresponde uma e uma s representao na forma de uma fraco cannica. Dessa forma muito fcil comparar dois racionais: dois racionais so iguais se e s se as correspondentes fraces cannicas tiverem o mesmo numerador e o mesmo denominador. A segunda vantagem tem a ver com as limitaes dos tipos bsicos do C++. Sendo os valores do tipo int limitados em C++, como se viu no Captulo 2, a utilizao de uma representao em fraces no-cannicas pe alguns problemas graves de implementao. O primeiro tem a ver com a facilidade com que permite realizar algumas operaes. Por exemplo, muito fcil vericar a igualdade de dois racionais comparando simplesmente os seus numeradores e denominadores, coisa que s possvel fazer directamente se se garantir que as fraces que os representam esto no formato cannico. O segundo problema tem a ver com as limitaes dos inteiros. Suponha-se o seguinte cdigo:

7.4. CLASSES C++ COMO MDULOS


int main() { Racional x(50000, 50000), y(1, 50000); Racional z = x.soma(y); z.escreve(); cout < < endl; }

335

No ecr deveria aparecer


1/50000

No se usando uma representao em fraces cannicas, ao se calcular o denominador do resultado, i.e., ao se multiplicar os dois denominadores, obtm-se 50000 50000 = 2500000000. Em mquinas em que os int tm 32 bits, esse valor no representvel, pelo que se obtm um valor errado (em Linux i386 obtm-se -1794967296), apesar de a fraco resultado ser perfeitamente representvel! Este problema pode ser mitigado se se trabalhar sempre com fraces no formato cannico. Mesmo assim, o problema no totalmente resolvido. Suponha-se o seguinte cdigo:
int main() { Racional x(1, 50000), y(1, 50000); Racional z = x.soma(y); z.escreve(); cout < < endl; }

No ecr deveria aparecer


1/25000

mas ocorre exactamente o mesmo problema que anteriormente. pois desejvel no s usar uma representao cannica para os racionais, como tambm tentar garantir que os resultados de clculos intermdios so to pequenos quanto possvel. Este assunto ser retomado mais tarde (Seco 7.13).

7.4.5 Explicitao da condio invariante de classe


A condio invariante de classe til no apenas como uma ferramenta formal que permite vericar o correcto funcionamento de, por exemplo, um mtodo. til como ferramenta de deteco de erros. Da mesma forma que conveniente explicitar pr-condies e condies

336

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

objectivo das rotinas atravs de instrues de assero, tambm o no caso da condio invariante de classe. A inteno detectar as violaes dessa condio durante a execuo do programa e abort-lo se alguma violao for detectada 11 . A condio invariante de classe claramente uma noo de implementao: refere-se sempre aos atributos (que se presume serem privados) de uma classe. Uma das vantagens de se estabelecer esta distino clara entre interface e implementao est em permitir alteraes substanciais na implementao sem que a interface mude. De facto, perfeitamente possvel que o programador produtor mude substancialmente a implementao de uma classe C++ sem que isso traga qualquer problema para o programador consumidor, que se limita a usar a interface da classe C++. A mudana da implementao de uma classe implica normalmente uma alterao da condio invariante de classe, mas no do comportamento externo da classe. por isso muito importante que pr-condio e condio objectivo de cada operao/mtodo sejam claramente factorizadas em condies que dizem respeito apenas implementao, e que devem corresponder condio invariante de classe, e condies que digam respeito apenas ao comportamento externo da operao. Dito por outras palavras, apesar de do ponto de vista da implementao a condio invariante de classe fazer parte da pr-condio e da condio objectivo de todas as operaes/mtodos, como se disse na seco anterior, prefervel p-la em evidncia, documentando-a claramente parte das operaes e mtodos, e excluindo-a da documentao/contrato de cada operao. Ou seja, a condio invariante de classe far parte do contrato de cada mtodo (ponto de vista da implementao), mas no far parte do contrato da correspondente operao (ponto de vista externo, da interface). Quando a condio invariante de classe violada, de quem a culpa? Nesta altura j no devero subsistir dvidas: a culpa do programador produtor da classe: 1. Violao da condio invariante de classe: culpa do programador produtor da classe. 2. Violao da pr-condio de uma operao: culpa do programador consumidor da classe. 3. Violao da condio objectivo de uma operao: culpa do programador produtor do respectivo mtodo. Como explicitar a condio invariante de classe? apenas uma questo de usar instrues de assero e comentrios de documentao apropriados. Para simplicar, conveniente denir uma operao privada da classe, chamada convencionalmente cumpreInvariante(), que devolve o valor lgico V se a condio invariante de classe se cumprir e falso no caso contrrio.
/** Descrio da classe Classe. @invariant CIC. class Classe { public: ...
Mais tarde se ver que, dependendo da aplicao em desenvolvimento, abortar o programa em caso de erro de programao pode ou no ser apropriado.
11

7.4. CLASSES C++ COMO MDULOS


/** Descrio da operao operao(). @pre P C. @post CO. */ tipo operao(parmetros); private: ... /** Descrio da operao operao_privada(). @pre P C. @post CO. */ tipo operao_privada(parmetros);

337

/** Indica se a condio invariante de classe (CIC) se verica. @pre *this = v. @post cumpreInvariante = CIC *this = v. */ bool cumpreInvariante(); }; ... // Implementao da operao operao(): mtodo. tipo Classe::operao(parmetros) { assert(cumpreInvariante() [and v.cumpreInvariante()]...); assert(P C); ... // Implementao. assert(cumpreInvariante() [and v.cumpreInvariante()]...); assert(CO); return ...; } ... bool Classe::cumpreInvariante() { return CIC. } // Implementao da operao operao(): mtodo. tipo Classe::operao_privada(parmetros)

338
{ assert(P C);

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

... // Implementao. assert(CO); return ...; }

So de notar os seguintes pontos importantes: A condio invariante de classe foi includa na documentao da classe, que parte da sua interface, apesar de antes se ter dito que esta condio era essencialmente uma questo de implementao. de facto infeliz que assim seja, mas os programas que extraem automaticamente a documentao de uma classe (e.g., Doxygen) requerem este posicionamento12 . A documentao das operaes no inclui a condio invariante de classe, visto que esta foi posta em evidncia, cando na documentao da classe. A implementao das operaes, i.e., o respectivo mtodo, inclui instrues de assero para vericar a condio invariante de classe para todas as instncias da classe em jogo (que incluem a instncia implcita 13 , parmetros, instncias locais ao mtodo, etc.), quer no incio do mtodo, quer no seu nal. As instrues de assero para vericar a veracidade da condio invariante de classe so anteriores quer instruo de assero para vericar a pr-condio da operao, quer instruo de assero para vericar a condio objectivo da operao. Esse posicionamento importante, pois as vericaes da pr-condio e da condio objectivo podem obrigar invocao de outras operaes pblicas da classe, que por sua vez vericam a condio invariante de classe: se a ordem fosse outra, o erro surgiria durante a execuo dessas outras operaes. Separaram-se as instrues de assero relativas a pr-condies, condies objectivo e condies invariantes de classe, de modo a ser mais bvia a razo do erro no caso de o programa abortar. A funo membro privada cumpreInvariante() no tem qualquer instruo de assero. Isso deve-se ao facto de ter sempre pr-condio V e de poder operar sobre variveis implcitas que no vericam a condio invariante de classe (como bvio, pois serve justamente para indicar se essa condio se verica).
12 Parece haver aqui uma contradio. No ser toda a documentao parte da interface? A resposta simplesmente no. Para uma classe, podem-se gerar trs tipos de documentao. A primeira diz respeito de facto interface, e inclui todos os membros pblicos: a documentao necessria ao programador consumidor. A segunda diz respeito categoria de acesso protected e deixar-se- para mais tarde. A terceira diz respeito implementao, e inclui os membros de todas as categorias de acesso: a documentao necessria ao programador produtor ou, pelo menos, assistncia tcnica, i.e., aos programadores que faro a manuteno do cdigo existente. Assim, a condio invariante de classe deveria ser parte apenas da documentao de implementao. 13 Excepto para operaes de classe.

7.4. CLASSES C++ COMO MDULOS

339

Os mtodos privados no tm instrues de assero para a condio invariante de classe, pois podem ser invocados por outros mtodos em instantes de tempo durante os quais as instncias da classe (instncia implcita, parmetros, etc.) no veriquem essa condio. Aplicando estas ideias classe Racional em desenvolvimento obtm-se:
#include <iostream> #include <cassert> using namespace std; /** Devolve o mximo divisor comum dos inteiros passados como argumento. @pre m = m n = n. mdc(m, n) m = 0 n = 0 @post mdc = . */ 1 m=0n=0 int mdc(int m, int n) { ... } /** Representa nmeros racionais. @invariant 0 < denominador mdc(numerador, denominador) = 1. */ class Racional { public: /** Constri racional com valor inteiro. Construtor por omisso. @pre V. @post *this = n. */ Racional(int const n = 0); /** Constri racional correspondente a n/d. @pre d = 0. @post *this = n . */ d Racional(int const n, int const d); /** Escreve o racional no ecr no formato de uma fraco. @pre *this = r. @post *this = r(cout cout contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this). */ d void escreve(); // Declarao da rotina membro: operao. /** Devolve a soma com o racional recebido como argumento. @pre *this = r. @post *this = r somaCom = *this + r2. */ Racional somaCom(Racional const r2); /** L do teclado um novo valor para o racional, na forma de dois inteiros sucessivos.

340

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


@pre *this = r. @post Se cin e cin tem dois inteiros n e d disponveis para leitura, com d = 0, ento *this = n cin, d seno *this = r cin. */ void l(); private: int numerador; int denominador; /** Reduz a fraco que representa o racional. @pre denominador = 0 *this = r. @post denominador = 0 mdc(numerador, denominador) = 1 *this = r. */ void reduz(); /** Indica se a condio invariante de classe se verica. @pre *this = r. @post *this = r cumpreInvariante = (0 < denominador mdc(numerador, denominador) = 1). */ bool cumpreInvariante(); }; Racional::Racional(int const n) : numerador(n), denominador(1) { assert(cumpreInvariante()); assert(numerador == n * denominador); } Racional::Racional(int const n, int const d) : numerador(d < 0 ? -n : n), denominador(d < 0 ? -d : d) { assert(d != 0); reduz(); assert(cumpreInvariante()); assert(numerador * d == n * denominador); } void Racional::escreve() {

7.4. CLASSES C++ COMO MDULOS


assert(cumpreInvariante()); cout < < numerador; if(denominador != 1) cout < < / < < denominador; assert(cumpreInvariante()); } Racional Racional::somaCom(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); Racional r; r.numerador = numerador * r2.denominador + r2.numerador * denominador; r.denominador = denominador * r2.denominador; r.reduz(); assert(cumpreInvariante() and r.cumpreInvariante()); return r; } void Racional::l() { assert(cumpreInvariante()); int n, d; if(cin > > n > > d) if(d == 0) cin.setstate(ios_base::failbit); else { numerador = d < 0 ? -n : n; denominador = d < 0 ? -d : d; reduz(); assert(cumpreInvariante()); assert(numerador * d == n * denominador and cin); return; }

341

342

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


assert(cumpreInvariante()); assert(not cin); } void Racional::reduz() { assert(denominador != 0); int k = mdc(numerador, denominador); numerador /= k; denominador /= k; assert(denominador != 0 and mdc(numerador, denominador) == 1); } bool Racional::cumpreInvariante() { return 0 < denominador and mdc(numerador, denominador) == 1; } int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; r1.l(); r2.l(); if(not cin) { cerr < < "Opps! return 1; }

A leitura dos racionais falhou!" < < endl;

// Calcular racional soma: Racional r = r1.somaCom(r2); // Escrever resultado: cout < < "A soma de "; r1.escreve(); cout < < " com "; r2.escreve(); cout < < " "; r.escreve(); cout < < . < < endl; }

7.5. SOBRECARGA DE OPERADORES

343

Note-se que o compilador se encarrega de garantir que algumas instncias no mudam de valor durante a execuo de um mtodo. o caso das constantes. evidente, pois, que se essas constantes cumprem inicialmente a condio invariante de classe, tambm a cumpriro no nal no mtodo, pelo que se pode omitir a vericao explcita atravs de uma instruo de assero, tal como se fez para o mtodo somaCom(). A Figura 7.5 mostra a notao usada para representar a condio invariante da classe C++ Racional, bem como a pr-condio e a condio objectivo da operao Racional::somaCom(). invariant {0 denominador mdc(numerador, denominador) = 1}

Racional -numerador: int -denominador: int constructor +Racional(in n: int = 0) +Racional(in n: int, in d: int) query +somaCom(in r2: Racional): Racional +escreve() -cumpreInvariante(): bool update +le() -reduz()

precondition {*this = r} postcondition {*this = r somaCom = r + r2}

Figura 7.5: A classe C++ Racional agora tambm com condio invariante de instncia, prcondio e condio objectivo indicadas para a operao Racional::somaCom().

7.5 Sobrecarga de operadores


Tal como denida, a classe C++ Racional obriga o consumidor a usar uma notao desagradvel e pouco intuitiva para fazer operaes com racionais. Como se viu, seria desejvel que a funo main(), no programa em desenvolvimento, se pudesse escrever simplesmente como:
int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; cin > > r1 > > r2; if(not cin) {

344

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


cerr < < "Opps! return 1; } // Calcular racional soma: Racional r = r1 + r2; // Escrever resultado: cout < < "A soma de " < < r1 < < " com " < < r2 < < " " < < r < < . < < endl; } A leitura dos racionais falhou!" < < endl;

Se se pudesse escrever o programa como acima, claramente a classe Racional, uma vez equipada com os restantes operadores dos tipos aritmticos bsicos, passaria a funcionar para o consumidor como qualquer outro tipo bsico do C++: seria verdadeiramente um tipo de primeira categoria. O C++ possibilita a sobrecarga dos operadores (+, -, *, /, ==, etc.) de modo a poderem ser utilizados com TAD concretizados pelo programador na forma de classes C++. A soluo para o problema passa ento pela sobrecarga dos operadores do C++ de modo a terem novos signicados quando aplicados ao novo tipo Racional, da mesma forma que se tinha visto antes relativamente aos tipos enumerados (ver Seco 6.1). Mas, ao contrrio do que se fez ento, agora as funes de sobrecarga tm de ser membros da classe Racional, de modo a poderem aceder aos seus membros privados (alternativamente poder-se-iam usar funes membro amigas da classe, Seco 7.15). Ou seja, a soluo simplesmente alterar o nome da operao Racional::somaCom() de somaCom para operator+:
... /** Representa nmeros racionais. @invariant 0 < denominador mdc(numerador, denominador) = 1. */ class Racional { public: ... /** Devolve a soma com o racional recebido como argumento. @pre *this = r. @post *this = r operator+ = *this + r2. */ Racional operator+(Racional const r2); ... private: ...

7.5. SOBRECARGA DE OPERADORES


}; ... Racional Racional::operator+(Racional const r2) { ... } ...

345

Tal como acontecia com a expresso r1.somaCom(r2), a expresso r1.operator+(r1) invoca a operao operator+() da classe C++ Racional usando r1 como instncia (varivel) implcita. S que agora possvel escrever a mesma expresso de uma forma muito mais clara e intuitiva:
r1 + r2

De facto, sempre que se sobrecarregam operadores usando operaes, o primeiro operando (que pode ser o nico no caso de operadores unrios, i.e., s com um operando) sempre a instncia implcita durante a execuo do respectivo mtodo, sendo os restantes operandos passados como argumento operao. Se @ for um operador binrio (e.g., +, -, *, etc.), ento a sobrecarga do operador @ pode ser feita:

Para uma classe C++ Classe, denindo uma operao tipo_de_devoluo Classe::operator@(t Numa invocao deste operador, o primeiro operando, obrigatoriamente do tipo Classe, usado como instncia implcita e o segundo operando passado como argumento.

Atravs de uma rotina no-membro tipo_de_devoluo operator@(tipo_do_primeiro_operan tipo_do_segundo_operando). Numa invocao deste operador, ambos os operandos so passados como argumentos. A expresso a @ b pode portanto ser interpretada como
a.operator@(b)

ou
operator@(a, b)

consoante o operador esteja denido como membro da classe a que a pertence ou esteja denido como rotina normal, no-membro. Se @ for um operador unrio (e.g., +, -, ++ prexo, etc.), ento a sobrecarga do operador @ pode ser feita:

346

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Para uma classe C++ Classe, denindo uma operao tipo_de_devoluo Classe::operator@() Numa invocao deste operador, o seu nico operando, obrigatoriamente do tipo Classe, usado como instncia implcita. Atravs de uma rotina no membro tipo_de_devoluo operator@(tipo_do_operando). A expresso @a (ou a@ se @ for suxo) pode portanto ser interpretada como
a.operator@()

ou
operator@(a)

consoante o operador esteja denido como membro da classe a que a pertence ou esteja denido como rotina normal, no-membro. importante notar que: 1. Quando a sobrecarga de um operador se faz por intermdio de uma operao (rotina membro) de uma classe C++, o primeiro operando (e nico no caso de uma operao unria) numa expresso que envolva esse operador no sofre nunca converses implcitas de tipo. Em todos os outros casos as converses implcitas so possveis. 2. Nunca se deve alterar a semntica dos operadores. Imagine-se os problemas que traria sobrecarregar o operador + para a classe C++ Racional como signicando o produto! 3. Nem todos os operadores podem ser sobrecarregados por intermdio rotinas no-membro. Os operadores = (atribuio), [] (indexao), () (invocao) e -> (seleco), s podem ser sobrecarregados por meio de operaes (rotinas membro). Para todas as classes que no os redenam, os operadores = (atribuio), & (unrio, endereo de) e , (sequenciamento) so denidos implicitamente: por isso possvel atribuir instncias de classes C++, como a classe Racional, sem para isso ter de sobrecarregar o operador de atribuio =). Falta agora a tarefa algo penosa de sobrecarregar todos os operadores aplicveis a racionais. Porqu? Porque, apesar de o programa da soma das fraces no necessitar seno dos operadores > > e < <, de extraco e insero em canais, instrutivo preparar a classe para utilizaes futuras, ainda difceis de antecipar. Pretende-se, pois, equipar o TAD Racional com todos os operadores usuais para os tipos bsicos do C++: +, -, * e / Operadores aritmticos (binrios): adio, subtraco, produto e diviso. No tm efeitos laterais, i.e., no alteram os operandos. + e - Operadores aritmticos (unrios): identidade e simtrico. No tm efeitos laterais.

7.6. TESTES DE UNIDADE

347

<, <=, >, >= Operadores relacionais (binrios): menor, menor ou igual, maior e maior ou igual. No tm efeitos laterais. == e != Operadores de igualdade e diferena (binrios). No tm efeitos laterais. ++ e Operadores de incrementao e decrementao prexo (unrios). Tm efeitos laterais, pois alteram o operando. Alis, so eles a sua principal razo de ser. ++ e Operadores de incrementao e decrementao suxo (unrios). Tm efeitos laterais. +=, -=, *= e /= Operadores especiais de atribuio: adio e atribuio, subtraco e atribuio, produto e atribuio e diviso e atribuio (binrios). Tm efeitos laterais, pois alteram o primeiro operando. > > e < < Operadores de extraco e insero de um canal (binrios). Ambos alteram o operando esquerdo (que um canal), mas apenas o primeiro altera o operando direito. Tm efeitos laterais.

7.6 Testes de unidade


Na prtica, no fcil a deciso de antecipar ou no utilizaes futuras. Durante o desenvolvimento de uma classe deve-se tentar suportar utilizaes futuras, difceis de antecipar, ou deve-se restringir o desenvolvimento quilo que necessrio em cada momento? Se o objectivo preparar uma biblioteca de ferramentas utilizveis por qualquer programador, ento claramente devem-se tentar prever as utilizaes futuras. Mas, se a classe est a ser desenvolvida para ser utilizada num projecto em particular, a resposta cai algures no meio destas duas opes. m ideia, de facto, gastar esforo de desenvolvimento 14 a desenvolver ferramentas de utilizao futura mais do que dbia. Mas tambm m ideia congelar o desenvolvimento de tal forma que aumentar as funcionalidades de uma classe C++, logo que tal se revele necessrio, seja difcil. O ideal, pois, est em no desenvolver prevendo utilizaes futuras, mas em deixar a porta aberta para futuros desenvolvimentos. A recomendao anterior no se afasta muito do preconizado pela metodologia de desenvolvimento eXtreme Programming [1]. Uma excelente recomendao dessa metodologia tambm o desenvolvimento dos chamados testes de unidade. Se se olhar com ateno para a denio da classe C++ Racional denida at agora, conclui-se facilmente que a maior parte das condies objectivo das operaes no so testadas usando instrues de assero. O problema que a condio objectivo das operaes est escrita em termos da noo matemtica de nmero racional, e no fcil fazer a ponte entre uma noo matemtica e o cdigo C++... Por exemplo, como explicitar em cdigo a condio objectivo do operador + para racionais? Uma primeira tentativa poderia ser a traduo directa:
/** Devolve a soma com o racional recebido como argumento. @pre *this = r. @post *this = r operator+ = *this + r2. */
14

A no ser para efeitos de estudo e desenvolvimento pessoal, claro.

348

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


Racional Racional::operator+(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); Racional r; r.numerador = numerador * r2.denominador + r2.numerador * denominador; r.denominador = denominador * r2.denominador; r.reduz(); assert(cumpreInvariante() and r.cumpreInvariante()); assert(r == *this + r2); return r; }

H dois problemas neste cdigo. O primeiro que o operador == ainda no est denido. Este problema resolver-se- facilmente mais frente neste captulo. O segundo muito mais importante: a assero, tal como est escrita, recorre recursivamente ao prprio operador +! Claramente, o caminho certo no passa por aqui. Os testes de unidade proporcionam uma alternativa interessante s instrues de assero para as condies objectivo das operaes. A ideia que se deve escrever um conjunto exaustivo de testes para as vrias operaes da unidade e mant-los durante toda a vida do cdigo. Por unidade entende-se aqui uma unidade de modularizao, tipicamente uma classe C++ e rotinas associadas que concretizam um TAD ou uma classe propriamente dita. Os testes de unidade s muito parcialmente substituem as instrues de assero para as condies objectivo das operaes da classe: 1. As instrues de asseres esto sempre activas e vericam a validade da condio objectivo sempre que o operao invocada. Por outro lado, os testes de unidade apenas so executados de tempos e tempos, e de uma forma independente do programa, ou dos programas, no qual a unidade testada est integrada. 2. As instrues de assero vericam a validade da condio objectivo para todos os casos para os quais o programa, ou os programas, invocam a respectiva operao da classe C++. No caso dos testes de unidade, no entanto, impensvel testar exaustivamente as operaes em causa. 3. As instrues de assero esto activas durante o desenvolvimento e durante a explorao do programa desenvolvido15 , enquanto os testes de unidade so executados de tempos a tempos, durante o desenvolvimento ou manuteno do programa.
Uma das caractersticas das instrues de assero que pode ser desactivadas facilmente, bastando para isso denir a macro NDEBUG. No entanto, no muito boa ideia desactivar as instrues de assero. Ver discusso sobre o assunto no !! citar captulo sobre tratamento de erros.
15

7.6. TESTES DE UNIDADE

349

Justicados que foram os testes de unidade, pode-se agora criar o teste de unidade para o TAD Racional:
#ifdef TESTE #include <fstream> /** Programa de teste do TAD Racional e da funo mdc(). */ int main() { assert(mdc(0, 0) == 1); assert(mdc(10, 0) == 10); assert(mdc(0, 10) == 10); assert(mdc(10, 10) == 10); assert(mdc(3, 7) == 1); assert(mdc(8, 6) == 2); assert(mdc(-8, 6) == 2); assert(mdc(8, -6) == 2); assert(mdc(-8, -6) == 2); Racional r1(2, -6); assert(r1.numerador() == -1 and r1.denominador() == 3); Racional r2(3); assert(r2.numerador() == 3 and r2.denominador() == 1); Racional r3; assert(r3.numerador() == 0 and r2.denominador() == 1); assert(r2 == 3); assert(3 == r2); assert(r3 == 0); assert(0 == r3); assert(r1 assert(r2 assert(r1 assert(r2 assert(r1 assert(r2 < r2); > r1); <= r2); >= r1); <= r1); >= r2);

assert(r2 == +r2); assert(-r1 == Racional(1, 3));

350

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

assert(++r1 == Racional(2, 3)); assert(r1 == Racional(2, 3)); assert(r1++ == Racional(2, 3)); assert(r1 == Racional(5, 3)); assert((r1 *= Racional(7, 20)) == Racional(7, 12)); assert((r1 /= Racional(3, 4)) == Racional(7, 9)); assert((r1 += Racional(11, 6)) == Racional(47, 18)); assert((r1 -= Racional(2, 18)) == Racional(5, 2)); assert(r1 + r2 == Racional(11, 2)); assert(r1 - Racional(5, 7) == Racional(25, 14)); assert(r1 * 40 == 100); assert(30 / r1 == 12); ofstream sada("teste"); sada < < r1 < < < < r2; sada.close(); ifstream entrada("teste"); Racional r4, r5; entrada > > r4 > > r5; assert(r1 == r4); assert(r2 == r5); } #endif // TESTE

So de notar os seguintes pontos: Alguma da sintaxe utilizada neste teste s ser introduzida mais tarde. O leitor deve regressar a este teste quando o TAD Racional for totalmente desenvolvido. Cada teste consiste essencialmente numa instruo de assero. H melhores formas de escrever os testes de unidade, sem recorrer a asseres, nomeadamente recorrendo a bibliotecas de teste. Mas tais bibliotecas esto fora do mbito deste texto. O teste consiste numa funo main(). De modo a no entrar em conito com a funo main() do programa propriamente dito, envolveu-se a funo main() de teste entre duas directivas de pr-compilao, #ifdef TESTE e #endif // TESTE. Isso faz com que toda a funo s seja levada em conta pelo compilador quando estiver denida a macro TESTE (coisa que num compilador em Linux se consegue tipicamente com a opo de compilao -DTESTE). Este assunto ser visto com rigor no Captulo 9, onde se ver tambm como se pode preparar um TAD como o tipo Racional para ser utilizado em qualquer programa onde seja necessrio trabalhar com racionais.

7.7. DEVOLUO POR REFERNCIA

351

7.7 Devoluo por referncia


Comear-se- o desenvolvimento dos operadores para o TAD Racional pelo operador de incrementao prexo. Uma questo nesse desenvolvimento saber o que que devolve esse operador e, de uma forma mais geral, todos os operadores de incrementao e decrementao prexos e especiais de atribuio.

7.7.1 Mais sobre referncias


Na Seco 3.2.11 viu-se que se pode passar um argumento por referncia a um procedimento se este tiver denido o parmetro respectivo como uma referncia. Por exemplo,
void troca(int& a, int& b) { int auxiliar = a; a = b; b = auxiliar; }

um procedimento que troca os valores das duas variveis passadas como argumento. Este procedimento pode ser usado no seguinte troo de programa
int x = 1, y = 2; troca(x, y); cout < < x < < < < y < < endl;

que mostra
2 1

no ecr. O conceito de referncia pode ser usado de formas diferentes. Por exemplo,
int i = 1; int& j = i; // a partir daqui j sinnimo de i! j = 3; cout < < i < < endl;

mostra
3

no ecr, pois alterar a varivel j o mesmo que alterar a varivel i, j que j sinnimo de i. As variveis que so referncias, caso de j no exemplo anterior e dos parmetros a e b do procedimento troca(), tm de ser inicializadas com a varivel de que viro a ser sinnimos. Essa inicializao feita explicitamente no caso de j, e implicitamente no caso das variveis a e b, neste caso atravs da passagem de x e y como argumento na chamada de troca().

352

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Necessidade da devoluo por referncia Suponha-se o cdigo:


int i = 0; ++(++i); cout < < i < < endl;

(Note-se que este cdigo muito pouco recomendvel! S que, como a sobrecarga dos operadores deve manter a mesma semntica que esses mesmos operadores possuem para os tipos bsicos, necessrio conhecer bem os cantos casa, mesmo os mais obscuros, infelizmente.) Este cdigo resulta na dupla incrementao da varivel i, como seria de esperar. Mas para isso acontecer, o operador ++, para alm de incrementar a varivel i, tem de devolver a prpria varivel i, e no uma sua cpia, pois de outra forma a segunda aplicao do operador ++ levaria incrementao da cpia, e no do original. Para que este assunto que mais claro, comear-se- por escrever um procedimento incrementa() com o mesmo objectivo do operador de incrementao. Como este procedimento deve afectar a varivel passada como argumento, neste caso i, deve receber o argumento por referncia:
/** Incrementa o inteiro recebido como argumento e devolve-o. @pre i = i. @post i = i + 1. */ void incrementa(int& v) { v = v + 1; } ... int i = 0; incrementa(incrementa(i)); cout < < i < < endl;

Infelizmente este cdigo no compila, pois a invocao mais exterior do procedimento recebe como argumento o resultado da primeira invocao, que void. Logo, necessrio devolver um inteiro nesta rotina:
/** Incrementa o inteiro recebido como argumento e devolve-o. @pre i = i. @post incrementa = i i = i + 1. */ int incrementa(int& v) { /* 1 */ v = v + 1; /* 2 */ return v; } ...

7.7. DEVOLUO POR REFERNCIA


int i = 0; /* 3 */ incrementa(i) /* 4 */ incrementa( ); /* 5 */ cout < < i < < endl;

353

Este cdigo tem trs problemas. O primeiro problema que, dada a denio actual da linguagem, no compila, pois o valor temporrio devolvido pela primeira invocao da rotina no pode ser passado por referncia (no-constante) para a segunda invocao. A linguagem C++ probe a passagem por referncia (no-constante) de valores, ou melhor, de variveis temporrias16 . O segundo problema que, ao contrrio do que se recomendou no captulo sobre modularizao, esta rotina no um procedimento, pois devolve alguma coisa, nem uma funo, pois afecta um dos seus argumentos. Note-se que continua a ser indesejvel escrever este tipo de cdigo. Mas a emulao do funcionamento do operador ++, que um operador com efeitos laterais, obriga utilizao de uma funo com efeitos laterais... O terceiro problema, mais grave, que, mesmo que fosse possvel a passagem de uma varivel temporria por referncia, o cdigo acima ainda no faria o desejado, pois nesse caso a segunda invocao da rotina incrementa() acabaria por alterar apenas essa varivel temporria, e no a varivel i, como se pode ver na Figura 7.6. Para resolver este problema, a rotina dever devolver no uma cpia de i, mas a prpria varivel i, que como quem diz, um sinnimo da varivel i. Ou seja, a rotina dever devolver i por referncia.
/** Incrementa o inteiro recebido como argumento e devolve-o. @pre i = i. @post incrementa i i = i + 1. */ int& incrementa(int& v) { v = v + 1; return v; // ou simplesmente return v = v + 1; } ... int i = 0; incrementa(i) incrementa( ); cout < < i < < endl;

/* 1 */ /* 2 */

/* 3 */ /* 4 */ /* 5 */

Para se compreender bem a diferena entre a devoluo por valor e devoluo por referncia, comparem-se as duas rotinas abaixo:
16

Esta verso da rotina incrementa() j leva ao resultado pretendido, usando para isso uma devoluo por referncia. Repare-se na condio objectivo desta rotina e compare-se com a usada para a verso anterior da mesma rotina(), em que se devolvia por valor: neste caso necessrio dizer que o que se devolve no apenas igual a i, mas tambm idntico a i, ou seja, o prprio i, como se pode ver na Figura 7.717 . Para isso usou-se o smbolo em vez do usual =.

Esta restrio razoavelmente arbitrria, e est em discusso a sua possvel eliminao numa prxima verso da linguagem C++. 17 necessrio claricar a diferena entre igualdade e identidade. Pode-se dizer que dois gmeos so iguais, mas no que so idnticos, pois so indivduos diferentes. Por outro lado, pode-se dizer que Fernando Pessoa e Alberto Caeiro no so apenas iguais, mas tambm idnticos, pois so nomes que se referem mesma pessoa.

354

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

v: int& retorno a5 v: int& retorno a4 i: int


v: int& retorno a5 : int 2 i: int


: int 2

v: int& retorno a4 i: int


: int 1 i: int

: int 1 i: int

i: int

i: int

(a)

(b)

(c)

(d)

(e)

(f)

(g)

Figura 7.6: Estado da pilha durante a execuo. (a) Imediatamente antes das invocaes nas linhas 3 e 4; (b) imediatamente antes da instruo 1, depois de invocada a rotina pela primeira vez, j com o parmetro v na pilha; (c) entre as instrues 1 e 2, j depois de incrementado v e portanto i (pois v sinnimo de i); (d) imediatamente aps a primeira invocao da rotina e imediatamente antes da sua segunda invocao, j com os parmetros retirados da pilha, sendo de notar que o valor devolvido est guardado numa varivel temporria, sem nome, no topo da pilha (a varivel est abaixo do pisa-papeis que representa o topo da pilha, e no acima, como habitual com os valores devolvidos, por ir ser construda uma referncia para alea, que obriga a que seja preservada mais tempo); (e) imediatamente antes da instruo 1, depois de invocada a rotina pela segunda vez; (f) entre as instrues 1 e 2, j depois de incrementado v e portanto j depois de incrementada a varivel temporria, de que v sinnimo; e (g) imediatamente antes da instruo 5, depois de a rotina retornar. V-se claramente que a varivel incrementada da segunda vez no foi a varivel i, como se pretendia, mas uma varivel temporria, entretanto destruda.

7.7. DEVOLUO POR REFERNCIA

355

v: int& retorno a4 i: int 0


v: int& retorno a4 i: int 1


v: int& v: int& retorno a5 i: int 1

v: int& retorno a5 i: int 2


v: int&

i: int 0

i: int 1

i: int 2

(a)

(b)

(c)

(d)

(e)

Figura 7.7: Estado da pilha durante a execuo. (a) Imediatamente antes das invocaes nas linhas 3 e 4; (b) imediatamente antes da instruo 1, depois de invocada a rotina pela primeira vez, j com o parmetro v na pilha; (c) entre as instrues 1 e 2, j depois de incrementado v e portanto i (pois v sinnimo de i); (d) imediatamente aps a primeira invocao da rotina e imediatamente antes da sua segunda invocao, j com os parmetros retirados da pilha, sendo de notar que a referncia devolvida se encontra no topo da pilha; (e) imediatamente antes da instruo 1, depois de invocada a rotina pela segunda vez, tendo a referncia v sido inicializada custa da referncia no topo da pilha, e portanto sendo v sinnimo de i; (f) entre as instrues 1 e 2, j depois de incrementado v e portanto j depois de incrementada a varivel i pela segunda vez; e (g) imediatamente antes da instruo 5, depois de a rotina retornar. Vse claramente que a varivel incrementada da segunda vez foi exactamente a varivel i, como se pretendia.

(f)

(g)

356
int cpia(int v) { return v; }

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

int& mesmo(int& v) { return v; }

A primeira rotina devolve uma cpia do parmetro v, que por sua vez j uma cpia do argumento passado rotina. Ou seja, devolve uma cpia do argumento, coisa que aconteceria mesmo que o argumento fosse passado por referncia. A segunda rotina, pelo contrrio, recebe o seu argumento por referncia e devolve o seu parmetro tambm por referncia. Ou seja, o que devolvido um sinnimo do prprio parmetro v, que por sua vez um sinnimo do argumento passado rotina. Ou seja, a rotina devolve um sinnimo do argumento. Uma questo losca que esse sinnimo ... no tem nome! Ou melhor, a prpria expresso de invocao da rotina que funciona como sinnimo do argumento. Isso deve ser claro no seguinte cdigo:
int main() { int valor = 0; cpia(valor) = 10; // erro! mesmo(valor) = 10; }

A instruo envolvendo a rotina cpia() est errada, pois a rotina devolve um valor temporrio, que no pode surgir do lado esquerdo de uma atribuio. Na terminologia da linguagem C++ diz-se que cpia(valor) no um valor esquerdo (lvalue ou left value). Pelo contrrio a expresso envolvendo a rotina mesmo() est perfeitamente correcta, sendo absolutamente equivalente a escrever:
valor = 10;

Na realidade, ao se devolver por referncia numa rotina, est-se a dar a possibilidade ao consumidor desse procedimento de colocar a sua invocao do lado esquerdo da atribuio. Por exemplo, denido a rotina incrementa() como acima, possvel escrever
int a = 11; incrementa(a) = 0; // possvel (mas absurdo), incrementa e depois atribui zero a a. incrementa(a) /= 2; // possvel (mas m ideia), incrementa e depois divide a por dois.

7.7. DEVOLUO POR REFERNCIA

357

Note-se que a devoluo de referncias implica alguns cuidados adicionais. Por exemplo, a rotina int& mesmoFalhado(int v) { return v; } contm um erro grave: devolve uma referncia (ou sinnimo) para uma varivel local, visto que o parmetro v no uma referncia, mas sim uma varivel local cujo valor uma cpia do argumento respectivo. Como essa varivel local destruda exactamente aquando do retorno da rotina, a referncia devolvida ca a referir-se a ... coisa nenhuma! Uma digresso pelo operador [] Uma vez que o operador de indexao [], usado normalmente para as matrizes e vectores, pode ser sobrecarregado por tipos denidos pelo programador, a devoluo de referncias permite, por exemplo, denir a classe VectorDeInt abaixo, que se comporta aproximadamente como a classe vector<int> descrita na Seco 5.2, embora com vericao de erros de indexao:
#include <iostream> #include <vector> #include <cstdlib> using namespace std; class VectorDeInt { public: VectorDeInt(vector<int>::size_type const dimenso_inicial = 0, int const valor_inicial_dos_itens = 0); ... int& operator[](vector<int>::size_type const ndice); ... private: vector<int> itens; }; VectorDeInt::VectorDeInt(vector<int>::size_type const dimenso_inicial, int const valor_inicial_dos_itens) : v(dimenso_inicial, valor_inicial_dos_itens)

358
{ } ...

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

int& VectorDeInt::operator[](vector<int>::size_type const ndice) { assert(0 <= ndice and ndice < itens.size()); return itens[ndice]; } int main() { VectorDeInt v(10); v[0] = 1; v[10] = 3; // ndice errado! aborta com assero falhada. }

7.7.2 Operadores ++ e -- prexo


O operador ++ prexo necessita de alterar o seu nico operando. Assim, conveniente sobrecarreglo na forma de uma operao da classe C++ Racional. Uma vez que tem um nico operando, este ser usado como instncia, neste caso varivel, implcita durante a execuo do respectivo mtodo, pelo que a operao no tem qualquer parmetro. importante perceber que a incrementao de um racional pode ser feita de uma forma muito mais simples do que recorrendo soma de racionais em geral: a adio de um a um racional representado por uma fraco cannica n d n+d , d que tambm uma fraco no formato cannico 18 , pelo que o cdigo simplesmente
Seja n o racional guardado numa instncia da classe C++ Racional, e que portanto verica a condio invad riante dessa classe, ou seja, 0 < d mdc(n, d) = 1. bvio que n+d n +1 = . d d Mas ser que a fraco n+d d verica a condio invariante de classe? Claramente o denominador d positivo, pelo que resta vericar se mdc(n+ d, d) = 1. Suponha-se que existe um divisor 1 < k comum ao numerador e ao denominador. Nesse caso existem n e d tais que kn = n + d e kd = d, de onde se conclui facilmente que kn = n + kd , ou seja, n = k(n d ). S que isso signicaria que 1 < k mdc(n, d), o que contraditrio com a hiptese de partida de que mdc(n, d) = 1. Logo, no existe divisor comum ao numerador e denominador superior unidade, ou seja, mdc(n + d, d) = 1 como se queria demonstrar.
18

7.7. DEVOLUO POR REFERNCIA


/** Representa nmeros racionais. @invariant 0 < denominador mdc(numerador, denominador) = 1. */ class Racional { public: ... /** Incrementa e devolve o racional. @pre *this = r. @post operador++ *this *this = r + 1. */ Racional& operator++(); ... }; ... Racional& Racional::operator++() { assert(cumpreInvariante()); numerador += denominador; assert(cumpreInvariante()); return ?; } ...

359

no se necessitando de reduzir a fraco depois desta operao. Falta resolver um problema, que o que devolver no nal do mtodo. Depois da discusso anterior com a rotina incrementa(), deve j ser claro que se deseja devolver o prprio operando do operador, que neste caso corresponde varivel implcita. Como se viu atrs, possvel explicitar a varivel implcita usando a construo *this, pelo que o cdigo ca simplesmente:
Racional& Racional::operator++() { assert(cumpreInvariante()); numerador += denominador; assert(cumpreInvariante());

360

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

return *this; }

A necessidade de devolver a prpria varivel implcita car porventura mais clara se se observar um exemplo semelhante ao que se usou mais atrs, mas usando racionais em vez de inteiros:
Racional r(1, 2); ++ ++r; // o mesmo que ++(++r); r.escreve(); cout < < endl;

Este cdigo absolutamente equivalente ao seguinte, que usa a notao usual de invocao de operaes de uma classe C++:
Racional r(1, 2); (r.operator++()).operator++(); r.escreve(); cout < < endl;

Aqui torna-se perfeitamente clara a necessidade de devolver a prpria varivel implcita, para que esta possa ser usada par invocar pela segunda vez o mesmo operador. Quanto ao operador -- prexo, a sua denio igualmente simples:
/** Representa nmeros racionais. @invariant 0 < denominador mdc(numerador, denominador) = 1. */ class Racional { public: ... /** Decrementa e devolve o racional. @pre *this = r. @post operador- *this *this = r 1. */ Racional& operator--(); ... }; ... inline Racional& Racional::operator--()

7.7. DEVOLUO POR REFERNCIA


{ assert(cumpreInvariante()); numerador -= denominador; assert(cumpreInvariante()); return *this; } ...

361

7.7.3 Operadores ++ e -- suxo


Qual a diferena entre os operadores de incrementao e decrementao prexo e suxo? Como j foi referido no Captulo 2, a diferena est no que devolvem. As verses prexo devolvem o prprio operando, j incrementado, e as verses suxo devolvem uma cpia do valor do operando antes de incrementado. Para que tal comportamento que claro, convm comparar cuidadosamente os seguintes troos de cdigo:
int i = 0; int j = ++i;

e
int i = 0; int j = i++;

Enquanto o primeiro troo de cdigo inicializa a varivel j como valor de i j incrementado, i.e., com 1, o segundo troo de cdigo inicializa a varivel j com o valor de i antes de incrementado, ou seja, com 0. Em ambos os casos a varivel i incrementada, cando com o valor 1. Claricada esta diferena, h agora que implementar os operadores de incrementao e decrementao suxo para a classe C++ Racional. A primeira questo, fundamental, sintctica: sendo os operadores prexo e suxo ambos unrios, como distingui-los na denio dos operadores? Se forem denidos como operaes da classe C++ Racional, ento ambos tero o mesmo nome e ambos no tero nenhum parmetro, distinguindo-se apenas no tipo de devoluo, visto que as verses prexo devolvem por referncia e as verses suxo devolvem por valor. O mesmo se passa se os operadores forem denidos como rotinas normais, no-membro. O problema que o tipo de devoluo no faz parte da assinatura das rotinas, membro ou no, pelo que o compilador se queixar de uma dupla denio do mesmo operador... Face a esta diculdade, os autores da linguagem C++ tomaram uma das decises mais arbitrrias que poderiam ter tomado. Arbitraram que para as assinaturas entre os operadores de

362

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

incrementao e decrementao prexo serem diferentes das respectivas verses suxo, estas ltimas teriam como que um operando adicional, inteiro, implcito, e cujo valor deve ser ignorado. um pouco como se os operadores suxo fossem binrios... Por razes que caro claras mais frente, denir-se-o os operadores de incrementao e decrementao suxo como rotinas normais, no-membro. Comece-se pelo operador de incrementao suxo. Sendo suxo, a sua denio assume que o operador binrio, tendo como primeiro operando o racional a incrementar e como segundo operando um inteiro cujo valor deve ser ignorado. Como o operador ser sobrecarregado atravs de uma rotina normal, ambos os operandos correspondem a parmetros da rotina, sendo o primeiro, corresponde ao racional a incrementar, passado por referncia:
/** Incrementa o racional recebido como argumento, devolvendo o seu valor antes de incrementado. @pre *this = r. @post operator++ = r *this = r + 1. */ Racional operator++(Racional& r, int valor_a_ignorar) { Racional const cpia = r; ++r; return cpia; }

Como a parmetro valor_a_ignorar arbitrrio, servindo apenas para compilador perceber que se est a sobrecarregar o operador suxo, e no o prexo, no necessrio sequer dar-lhe um nome, pelo que a denio pode ser simplica para
/** Incrementa o racional recebido como argumento, devolvendo o seu valor antes de incrementado. @pre *this = r. @post operator++ = r *this = r + 1. */ Racional operator++(Racional& r, int) { Racional const cpia = r; ++r; return cpia; }

interessante notar como se recorre ao operador de incrementao prexo, que j foi denido, na implementao do operador suxo. Ao contrrio do que pode parecer, tal no ocorre simplesmente porque se est a sobrecarregar o operador suxo como uma rotina no-membro da classe Racional. De facto, mesmo que o operador fosse denido como membro da classe

7.7. DEVOLUO POR REFERNCIA


/* A sobrecarga tambm se poderia fazer custa de uma operao da classe! */ Racional Racional::operator++(int) { Racional const cpia = *this; ++*this; return cpia; }

363

continuaria a ser vantajoso faz-lo: que o cdigo de incrementao propriamente dito ca concentrado numa nica rotina, pelo que, se for necessrio mudar a representao dos racionais, apenas ser necessrio alterar a implementao do operador prexo. Repare-se como, em qualquer dos casos, necessrio fazer uma cpia do racional antes de incrementado e devolver essa cpia por valor, o que implica realizar ainda outra cpia. Finalmente compreende-se a insistncia, desde o incio deste texto, em usar a incrementao prexo em detrimento da verso suxo, mesmo onde teoricamente ambas produzem o mesmo resultado, tal como em incrementaes ou decrementaes isoladas (por exemplo no progresso de um ciclo): que a incrementao ou decrementao suxo quase sempre menos eciente do que a respectiva verso prexo. O operador de decrementao suxo dene-se exactamente de mesma forma:
/** Decrementa o racional recebido como argumento, devolvendo o seu valor antes de decrementado. @pre *this = r. @post operator- = r *this = r 1. */ Racional operator--(Racional& r, int) { Racional const cpia = r; --r; return cpia; }

Como bvio, tendo-se devolvido por valor em vez de por referncia, no possvel escrever
Racional r; r++ ++; // erro!

que de resto j era uma construo invlida para os tipos bsicos do C++.

364

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.8 Mais operadores para o TAD Racional


Falta ainda sobrecarregar muitos operadores para o TAD Racional. Um facto curioso, como se vericar em breve, que os operadores aritmticos sem efeitos laterais se implementam facilmente custa dos operadores aritmticos com efeitos laterais, e que a verso alternativa, em que se implementam os operadores com efeitos laterais custa dos que no os tm, conduz normalmente a menores ecincias, pois estes ltimos operadores implicam frequentemente a realizao de cpias. Assim, tendo-se j sobrecarregado os operadores de incrementao e decrementao, o prximo passo ser o de sobrecarregar os operadores de atribuio especiais. Depois denir-se-o os operadores aritmticos normais. Alis, no caso do operador + ser uma re-implementao.

7.8.1 Operadores de atribuio especiais


Comear-se- pelo operador *=, de implementao muito simples. Tal como os operadores de incrementao e decrementao, tambm os operadores de atribuio especiais so mal comportados. So denidos custas de rotinas que so mistos de funo e procedimento, ou funes com efeitos laterais. O operador *= no excepo. Ir ser sobrecarregado custa de uma operao da classe C++ Racional, pois necessita de alterar os atributos da classe. Como o operador *= tem dois operandos, o primeiro ser usado com instncia (alis, varivel) implcita, e o segundo ser passado como argumento. A operao ter, pois um nico parmetro. Todos os operadores de atribuio especiais devolvem uma referncia para o primeiro operando, tal como os operadores de incrementao e decrementao prexo. isso que permite escrever o seguinte pedao de cdigo, muito pouco recomendvel, mas idntico ao que se poderia tambm escrever para variveis dos tipos bsicos do C++:
Racional a(4), b(1, 2); (a *= b) *= b;

Deve ser claro que este cdigo multiplica a por

1 2

duas vezes, cando a com o valor 1.

A implementao do operador produto e atribuio simples::


... class Racional { public: ... /** Multiplica por um racional. @pre *this = r. @post operator*= *this *this = r r2. */ Racional& operator*=(Racional r2);

7.8. MAIS OPERADORES PARA O TAD RACIONAL


... }; ... Racional& Racional::operator*=(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); numerador *= r2.numerador; denominador *= r2.denominador; reduz(); assert(cumpreInvariante()); return *this; } ...

365

O corpo do mtodo denido limita-se a efectuar o produto da forma usual para as fraces, i.e., o numerador do produto o produto dos numeradores e o denominador do produto o produto dos denominadores. Como os denominadores so ambos positivos, o seu produto tambm o ser. Para que o resultado cumpra a condio invariante de classe falta apenas garantir que no nal do mtodo mdc(n, d) = 1. Como isso no garantido (pense-se, por exemplo, o produto de 1 por 2), necessrio reduzir a fraco resultado. Tal como no caso dos 2 operadores de incrementao e decrementao prexo, tambm aqui se termina devolvendo a varivel implcita, i.e., o primeiro operando. O operador /= sobrecarrega-se da mesma forma, embora tenha de haver o cuidado de garantir que o segundo operando no zero:
... class Racional { public: ... /** Divide por um racional. @pre *this = r r2 = 0. @post operator/= *this *this = r/r2. */ Racional& operator/=(Racional r2); ...

366

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

}; ... Racional& Racional::operator/=(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); assert(r2 != 0); if(r2.numerador < 0) { numerador *= -r2.denominador; denominador *= -r2.numerador; } else { numerador *= r2.denominador; denominador *= r2.numerador; } reduz(); assert(cumpreInvariante()); return *this; } ...

H neste cdigo algumas particularidades que preciso estudar. A diviso por zero impossvel, pelo que a pr-condio obriga r2 a ser diferente de zero. A instruo de assero reecte isso mesmo, embora contenha um erro: por ora no possvel comparar dois racionais atravs do operador !=, quanto mais um racional e um inteiro (0 um do tipo int). Pede-se ao leitor que seja paciente, pois dentro em breve este problema ser resolvido sem ser necessrio alterar em nada este mtodo! O clculo da diviso muito simples: o numerador da diviso o numerador do primeiro operando multiplicado pelo denominador do segundo operando, e vice-versa. Uma verso simplista do clculo da diviso seria:
numerador *= r2.denominador; denominador *= r2.numerador;

Este cdigo, no entanto, no s no garante que o resultado esteja reduzido, e da a invocao de reduz() no cdigo mais acima. (tal como acontecia para o operador *=) como tambm no garante que o denominador resultante seja positivo, visto que o numerador de r2 pode perfeitamente ser negativo. Prevendo esse caso o cdigo ca

7.8. MAIS OPERADORES PARA O TAD RACIONAL


if(r2.numerador < 0) { numerador *= -r2.denominador; denominador *= -r2.numerador; } else { numerador *= r2.denominador; denominador *= r2.numerador; }

367

tal como se pode encontrar no no mtodo acima. Relativamente ao operador += possvel resolver o problema de duas formas. A mais simples neste momento implementar o operador += custa do operador +, pois este j est denido. Nesse caso a soluo :
Racional& Racional::operator+=(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); *this = *this + r2 assert(cumpreInvariante()); return *this; }

Esta soluo, no entanto, tem o inconveniente de obrigar realizao de vrias cpias entre racionais, alm de exigir a construo de um racional temporrio para guardar o resultado da adio antes de este ser atribudo varivel implcita. Como se ver, a melhor soluo desenvolver o operador += de raiz e implementar o operador + sua custa. Os operadores += e -= sobrecarregam-se de forma muito semelhante:
... class Racional { public: ... /** Adiciona de um racional. @pre *this = r. @post operator+= *this *this = r + r2. */ Racional& operator+=(Racional r2); /** Subtrai de um racional. @pre *this = r.

368

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


@post operator-= *this *this = r r2. */ Racional& operator-=(Racional r2); ... }; ... Racional& Racional::operator+=(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); numerador = numerador * r2.denominador + r2.numerador * denominador; denominador *= r2.denominador; reduz(); assert(cumpreInvariante()); return *this; } Racional& Racional::operator-=(Racional const r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); numerador = numerador * r2.denominador r2.numerador * denominador; denominador *= r2.denominador; reduz(); assert(cumpreInvariante()); return *this; } ...

7.8.2 Operadores aritmticos


Os operadores aritmticos usuais podem ser facilmente implementados custa dos operadores especiais de atribuio. Implementar-se-o aqui como rotinas normais, no-membro, por razes que sero claricadas em breve. Comear-se- pelo operador *. A ideia criar uma

7.8. MAIS OPERADORES PARA O TAD RACIONAL

369

varivel local temporria cujo valor inicial seja uma cpia do primeiro operando, e em seguida usar o operador *= para proceder soma:
/** Produto de dois racionais. @pre V. @post operator* = r1 r2. */ Racional operator*(Racional const r1, Racional const r2) { Racional auxiliar = r1; auxiliar *= r2; return auxiliar; }

Observando cuidadosamente este cdigo, conclui-se facilmente que o parmetro r1, desde que deixe de ser constante, pode fazer o papel da varivel auxiliar, visto que a passagem se faz por valor:
/** Produto de dois racionais. @pre r1 = r1 . @post operator* = r1 r2. */ Racional operator*(Racional r1, Racional const r2) { r1 *= r2; return r1; }

Finalmente, dado que o operador *= devolve o primeiro operando, podem-se condensar as duas instrues do mtodo numa nica instruo idiomtica:
/** Produto de dois racionais. @pre r1 = r1 . @post operator* = r1 r2. */ Racional operator*(Racional r1, Racional const r2) { return r1 *= r2; }

A implementao dos restantes operadores aritmticos faz-se exactamente da mesma forma:


/** Diviso de dois racionais. @pre r1 = r1 r2 = 0. @post operator/ = r1 /r2. */ Racional operator/(Racional r1, Racional const r2) { assert(r2 != 0);

370

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

return r1 /= r2; } /** Adio de dois racionais. @pre r1 = r1 . @post operator+ = r1 + r2. */ Racional operator+(Racional r1, Racional const r2) { return r1 += r2; } /** Subtraco de dois racionais. @pre r1 = r1 . @post operator- = r1 r2. */ Racional operator-(Racional r1, Racional const r2) { return r1 -= r2; }

Para alm da vantagem j discutida de implementar um operador custa de outro, agora deve j ser clara a vantagem de ter implementado o operador * custa do operador *= e no o contrrio: a operao *= tornou-se muito mais eciente, pois no obriga a copiar ou construir qualquer racional, enquanto a operao * continua a precisar a sua dose de cpias e construes... Mas, porqu denir estes operadores como rotinas normais, no-membro? H uma razo de peso, que tem a ver com as converses implcitas.

7.9 Construtores: converses implcitas e valores literais


7.9.1 Valores literais
J se viu que a denio de classes C++ concretizando TAD permite a acrescentar linguagem C++ novos tipos que funcionam praticamente como os seus tipos bsicos. Mas haver equivalente aos valores literais? Recorde-se que, num programa em C++, 10 e 100.0 so valores literais dos tipos int e double, respectivamente. Ser possvel especicar uma forma para, por exemplo, escrever valores literais do novo tipo Racional? Infelizmente isso impossvel em C++. Por exemplo, o cdigo
Racional r; r = 1/3;

redunda num programa aparentemente funcional mas com um comportamento inesperado. Acontece que a expresso 1/3 interpretada como a diviso inteira, que neste caso tem resultado zero. Esse valor inteiro depois convertido implicitamente para o tipo Racional e atribuda varivel r. Logo, r, depois da atribuio, conter o racional zero!

7.9. CONSTRUTORES: CONVERSES IMPLCITAS E VALORES LITERAIS

371

Existe uma alternativa elegante aos inexistentes valores literais para os racionais. proporcionada pelos construtores da classe, e funciona quase como se de valores literais se tratasse: os construtores podem ser chamados explicitamente para criar um novo valor dessa classe. Assim, o cdigo anterior deveria ser corrigido para:
Racional r; r = Racional(1, 3);

7.9.2 Converses implcitas


Se uma classe A possuir um construtor que possa ser invocado passando um nico argumento do tipo B como argumento, ento est disponvel uma converso implcita do tipo B para a classe A. Por exemplo, o primeiro construtor da classe Racional (ver Seco 7.4.1) pode ser chamado com apenas um argumento do tipo int, o que signica que, sempre que o compilador esperar um Racional e encontrar um int, converte o int implicitamente para um valor Racional. Por exemplo, estando denido um operator + com operandos do tipo Racional, o seguinte pedao de cdigo
Racional r1(1, 3); Racional r2 = r1 + 1;

perfeitamente legal, tendo o mesmo signicado que


Racional r1(1, 3); Racional r2 = r1 + Racional(1);
4 e colocando em r2 o racional 3 .

Em casos em que esta converso implcita de tipos indesejvel, pode-se preceder o respectivo construtor da palavra-chave explicit. Assim, se a classe Racional estivesse denida como
... class Racional { public: /** Constri racional com valor inteiro. Construtor por omisso. @pre V. @post *this = n 0 < denominador mdc(numerador, denominador) = 1. */ explicit Racional(int const n = 0); ... }; ...

372

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

o compilador assinalaria erro ao encontrar a expresso r1 + 1. Neste caso, no entanto, a converso implcita de int para Racional realmente til, pelo que o qualicador explicit desnecessrio.

7.9.3 Sobrecarga de operadores: operaes ou rotinas?


Suponha-se por um instante que o operador + para a classe C++ Racional sobrecarregado atravs de uma operao. Isto , regresse-se verso do operador + apresentada na Seco 7.5. Nesse caso o seguinte cdigo
Racional r(1, 3); Racional s = r + 3;

vlido, pois o valor inteiro 3 convertido implicitamente para Racional e seguidamente invocado o operador + denido. Ou seja, o cdigo acima equivalente a
Racional r(1, 3); Racional s = r + Racional(3);

Porm, o cdigo
Racional r(1, 3); Racional s = 3 + r;

invlido, pois a linguagem C++ probe converses na instncia atravs da qual se invoca um mtodo. Se o operador tivesse sido sobrecarregado custa de uma normal rotina nomembro, todos os seus argumentos poderiam sofrer converses implcitas, o que resolveria o problema. Mas foi exactamente isso que se fez nas seces anteriores! Logo, o cdigo acima perfeitamente legal e equivalente a
Racional r(1, 3); Racional s = Racional(3) + r;

Este facto ser utilizado para implementar alguns dos operadores em falta para a classe C++ Racional.

7.10 Operadores igualdade, diferena e relacionais


Os operadores de igualdade, diferena e relacionais sero desenvolvidos usando algumas das tcnicas j apresentadas. Estes operadores sero sobrecarregados usando rotinas no-membro, de modo a se tirar partido das converses implcitas de int para Racional, e tentar-se-

7.10. OPERADORES IGUALDADE, DIFERENA E RELACIONAIS

373

que sejam implementados custa de outros mdulos preexistentes, por forma a minimizar o impacte de possveis alteraes na representao (interna) dos nmeros racionais. Os primeiros operadores a sobrecarregar sero o operador igualdade, ==, e o operador diferena, !=. Viu-se na Seco 7.4.4 que o facto de as instncias da classe C++ Racional cumnumerador prirem a condio invariante de classe, i.e., de denominador ser uma fraco no formato cannico, permitia simplicar muito a comparao entre racionais. De facto, assim , pois dois racionais so iguais se e s se tiverem representaes em fraces cannicas iguais. Assim, uma primeira tentativa de denir o operador == poderia ser:
/** Indica se dois racionais so iguais. @pre V. @post operator== = (r1 = r2). */ bool operator==(Racional const r1, Racional const r2) { return r1.numerador == r2.numerador and r1.denominador == r2.denominador; }

O problema deste cdigo que, sendo o operador uma rotina no-membro, no tem acesso aos membros privados da classe C++. Por outro lado, se o operador fosse uma operao da classe C++, embora o problema do acesso aos membros se resolvesse, deixariam de ser possveis converses implcitas do primeiro operando do operador. Como resolver o problema? H duas solues para este dilema. A primeira passa por tornar a rotina que sobrecarrega o operador == amigo da classe C++ Racional (ver Seco 7.15). Esta soluo desaconselhvel, pois h uma alternativa simples que no passa por amizades (e, por isso, no est sujeita a introduzir quaisquer promiscuidades): deve-se explorar o facto de a rotina precisar de saber os valores do numerador e denominador da fraco cannica correspondente ao racional, mas no precisar de alterar o seu valor.

7.10.1 Inspectores e interrogaes


Se se pensar cuidadosamente nas possveis utilizaes do TAD Racional, conclui-se facilmente que o programador consumidor pode necessitar de conhecer a fraco cannica correspondente ao racional. Se assim for, convm equipar a classe C++ com duas funes membro que se limitam a devolver o valor do numerador e denominador dessa fraco cannica. Como a representao de um Racional justamente feita custa de uma fraco cannica, concluise que as duas funes membro so muito fceis de implementar 19 :
19 A condio objectivo da operao denominador() algo complexa, pois evita colocar na interface da classe referncias sua implementao, como seria o caso se se referisse ao atributo denominador_. Assim, usa-se a denio de denominador de fraco cannica. O valor devolvido denominador tem de ser tal que exista um numerador n tal que n 1. a fraco denominador igual instncia implcita e n 2. a fraco denominador est no formato cannico,

374
... class Racional { public: ...

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

/** Devolve numerador da fraco cannica correspondente ao racional. @pre *this = r. numerador @post *this = r denominador() = *this. */ int numerador(); /** Devolve denominador da fraco cannica correspondente ao racional. @pre *this = r. @post *this = r

n E n : V : denominador = *this 0 < denominador mdc(n, denominador) = 1 . */ int denominador();

... private: int numerador_; int denominador_; ... }; ... int Racional::numerador() { assert(cumpreInvariante()); assert(cumpreInvariante()); return numerador_; } int Racional:: denominador() { assert(cumpreInvariante());
ou seja, o valor devolvido o denominador da fraco cannica correspondente instncia implcita. A condio objectivo da operao numerador() mais simples, pois recorre denio da operao denominador() para dizer que se o valor devolvido for o numerador de uma fraco cujo denominador o valor devolvido pela operao denominador(), ento essa fraco igual instncia implcita. Como o denominador usado o denominador da fraco cannica igual instncia implcita, conclui-se que o valor devolvido de facto o numerador dessa mesma fraco cannica.

7.10. OPERADORES IGUALDADE, DIFERENA E RELACIONAIS

375

assert(cumpreInvariante()); return denominador_; } ...

s operaes de uma classe C++ que se limitam a devolver propriedades das suas instncias chama-se inspectores. Invoc-las tambm se diz interrogar a instncia. Os inspectores permitem obter os valores de propriedades de um instncia sem que se exponham as suas partes privadas manipulao pelo pblico em geral. de notar que a introduo destes novos operadores trouxe um problema prtico. As novas operaes tm naturalmente o nome que antes tinham os atributos da classe. Repare-se que no se decidiu dar outros nomes s operaes para evitar os conitos: que produz uma classe deve estar preparado para, em nome do fornecimento de uma interface to clara e intuitiva quanto possvel, fazer alguns sacrifcios. Neste caso o sacrifcio o de alterar o nome dos atributos, aos quais comum acrescentar um sublinhado (_) para os distinguir de operaes com o mesmo nome, e, sobretudo, alterar os nomes desses atributos em todas as operaes entretanto denidas (e note-se que j so algumas...).

7.10.2 Operadores de igualdade e diferena


Os inspectores denidos na seco anterior so providenciais, pois permitem resolver facilmente o problema do acesso aos atributos. Basta recorrer a eles para comparar os dois racionais:
/** Indica se dois racionais so iguais. @pre V. @post operator== = (r1 = r2). */ bool operator==(Racional const r1, Racional const r2) { return r1.numerador() == r2.numerador() and r1.denominador() == r2.denominador(); }

O operador != sobrecarrega-se de uma forma ainda mais simples: negando o resultado de uma invocao ao operador == denido acima:
/** Indica se dois racionais so diferentes. @pre V. @post operator!= = (r1 = r2). */ bool operator!=(Racional const r1, Racional const r2) { return not (r1 == r2); }

376

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.10.3 Operadores relacionais


O operador < pode ser facilmente implementado para a classe C++ Racional, bastando recorrer ao mesmo operador para os inteiros. Suponha-se que se pretende saber se n2 n1 < , d1 d2
2 1 em que n1 e n2 so fraces no formato cannico. Como 0 < d 1 e 0 < d2 , a desigualdade acima d d equivalente a

n1 d2 < n 2 d1 . Logo, a sobrecarga do operador < pode ser feita como se segue:
/** Indica se o primeiro racional menor que o segundo. @pre V. @post operator< = (r1 < r2). */ bool operator<(Racional const r1, Racional const r2) { return r1.numerador() * r2.denominador() < r2.numerador() * r1.denominador(); }

Os restantes operadores relacionais podem ser denidos todos custa do operador <. instrutivo ver como, sobretudo no caso desconcertantemente simples do operador >:
/** Indica se o primeiro racional maior que o segundo. @pre V. @post operator> = (r1 > r2). */ bool operator>(Racional const r1, Racional const r2) { return r2 < r1; } /** Indica se o primeiro racional menor ou igual ao segundo. @pre V. @post operator<= = (r1 r2). */ bool operator<=(Racional const r1, Racional const r2) { return not (r2 < r1); } /** Indica se o primeiro racional maior ou igual ao segundo. @pre V. @post operator>= = (r1 r2). */

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO


bool operator>=(Racional const r1, Racional const r2) { return not (r1 < r2); }

377

Curiosamente (ou no), tambm os operadores == e != se podem implementar custa apenas do operador <. Faz-lo ca como exerccio para o leitor.

7.11 Constncia: vericando erros durante a compilao


Uma boa linguagem de programao permite ao programador escrever programas com um mnimo de erros. Um bom programador que tira partido das ferramentas que a linguagem possui para reduzir ao mnimo os seus prprios erros. H trs formas importantes de erros: 1. Erros lgicos. So erros devidos a um raciocnio errado do programador: a sua resoluo do problema, incluindo algoritmos e estruturas de dados, ainda que correctamente implementados, no leva ao resultado pretendido, ou seja, na realidade no resolve o problema. Este tipo de erro o mais difcil de corrigir. A facilidade ou diculdade da sua deteco varia bastante conforme os casos, mas comum que ocorram erros lgicos de difcil deteco. 2. Erros de implementao. Ao implementar a resoluo do problema idealizada, foram cometidos erros no-sintcticos (ver abaixo), i.e., o programa no uma implementao do algoritmo idealizado. Erros deste tipo so fceis de corrigir, desde que sejam detectados. A deteco dos erros tanto pode ser fcil como muito difcil, por exemplo, quando os erros ocorrem em casos fronteira que raramente ocorrem na prtica, ou quando o programa produz resultados que, sendo errados, parecem plausveis. 3. Erros sintcticos. So gralhas. O prprio compilador se encarrega de os detectar. So fceis de corrigir. Antes de um programa ser disponibilizado ao utilizador nal, testado. Antes de ser testado, compilado. Antes de ser compilado, desenvolvido. Com excepo dos erros durante o desenvolvimento, claro que quanto mais cedo no processo ocorrerem os erros, mais fceis sero de detectar e corrigir, e menor o seu impacte. Assim, uma boa linguagem aquela que permite que os (inevitveis) erros sejam sobretudo de compilao, detectados facilmente pelo compilador, e no de implementao ou lgicos, detectados com diculdade pelo programador ou pelos utilizadores do programa. Para evitar os erros lgicos, uma linguagem deve possuir uma boa biblioteca, que liberte o programador da tarefa ingrata, e sujeita a erros, de desenvolver algoritmos e estruturas de dados bem conhecidos. Mas como evitar os erros de implementao? H muitos casos em que a linguagem pode ajudar. o caso da possibilidade de usar constantes em vez de variveis, que permite ao compilador detectar facilmente tentativas de alterar o seu valor, enquanto

378

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

que a utilizao de uma varivel para o mesmo efeito impediria do compilador de detectar o erro, deixando esse trabalho nas mos do programador. Outro caso o do encapsulamento. A categorizao de membros de uma classe C++ como privados permite ao compilador detectar tentativas errneas de acesso a esses membros, coisa que seria impossvel se os membros fossem pblicos, recaindo sobre os ombros do programador consumidor da classe a responsabilidade de no aceder a determinados membros, de acordo com as especicaes do programador produtor. Ainda outro caso a denio das variveis to perto quando possvel da sua primeira utilizao, que permite evitar utilizaes errneas dessa varivel antes do local onde realmente necessria, e onde, se a varivel for de um tipo bsico, toma um valor arbitrrio (lixo). Assim, conveniente usar os mecanismos da linguagem de programao que permitem exprimir no prprio cdigo determinadas opes de implementao e condies de utilizao, e que permitem que seja o prprio compilador a vericar do seu cumprimento, tirando esse peso dos ombros do programador, que pode por isso dedicar mais ateno a outros assuntos mais importantes. o caso da classicao de determinadas instncias como constantes, estudada nesta seco no mbito da classe Racional.

7.11.1 Passagem de argumentos


At agora viram-se duas formas de passagem de argumentos: por valor e por referncia. Com a utilizao da palavra-chave const as possibilidades passam a quatro, ou melhor, a trs e meia... A forma mais simples de passagem de argumentos por valor. Neste caso os parmetros so variveis locais rotina, inicializadas custa dos argumentos respectivos. Ou seja, os parmetros so cpias dos argumentos:
// Declarao: TipoDeDevoluo rotina(TipoDoParmetro parmetro); // Denio: TipoDeDevoluo rotina(TipoDoParmetro parmetro) { ... }

tambm possvel que os parmetros sejam constantes:


// Declarao: TipoDeDevoluo rotina(TipoDoParmetro const parmetro); // Denio: TipoDeDevoluo rotina(TipoDoParmetro const parmetro) { ... }

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO

379

No entanto, a diferena entre um parmetro varivel ou constante, no caso da passagem de argumentos por valor, no tem qualquer espcie de impacte sobre o cdigo que invoca a rotina. Ou seja, para o programador consumidor da rotina irrelevante se os parmetros so variveis ou constantes: o que lhe interessa que sero cpias dos argumentos, que por isso no sero afectados pelas alteraes que os parmetros possam ou no sofrer 20 : a interface da rotina no afectada, e as declaraes
TipoDeDevoluo rotina(TipoDoParmetro parmetro);

e
TipoDeDevoluo rotina(TipoDoParmetro const parmetro);

so idnticas, pelo que se si usar apenas a primeira forma, excepto quando for importante deixar clara a constncia do parmetro devido ao facto de ele ocorrer na condio objectivo da rotina, i.e., quando se quiser dizer que o parmetro usado na condio objectivo tem o valor original, entrada da rotina. J do ponto de vista do programador programador, ou seja, durante a denio da rotina, faz toda a diferena que o parmetro seja constante: se o for, o compilador detectar tentativas de o alterar no corpo da rotina, protegendo o programador dos seus prprios erros no caso de a alterao do valor do parmetro ser de facto indesejvel. Finalmente, note-se que a palavra-chave const, no caso da passagem de argumentos por valor, eliminada automaticamente da assinatura da rotina, pelo que perfeitamente possvel que surja apenas na sua denio (implementao), sendo eliminada da declarao (interface):
// Declarao: TipoDeDevoluo rotina(TipoDoParmetro parmetro); // Denio: TipoDeDevoluo rotina(TipoDoParmetro const parmetro) { ... // Alterao de parmetro proibida! }

No caso da passagem por referncia a palavra-chave const faz toda a diferena em qualquer caso, quer do ponto de vista da interface, quer do ponto de vista da implementao. Na passagem de argumentos por referncia,
Curiosamente possvel criar classes cujo construtor por cpia (ver Seco 7.4.2) altere o original! normalmente muito m ideia faz-lo, pois perverte a semntica usual da cpia, mas em alguns casos poder ser uma prtica justicada. o caso do tipo genrico auto_ptr, da biblioteca padro do C++. Mas mesmo no caso de uma classe C++ ter um construtor por cpia que altere o original, tal alterao ocorre durante a passagem de um argumento dessa classe C++ por valor, seja ou no o parmetro respectivo constante, o que s vem reforar a irrelevncia para a interface de uma rotina de se usar a palavras chave const para qualicar parmetros que no sejam referncias.
20

380

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


// Declarao: TipoDeDevoluo rotina(TipoDoParmetro& parmetro); // Denio: TipoDeDevoluo rotina(TipoDoParmetro& parmetro) { ... }

os parmetros funcionam como sinnimos dos argumentos (ou referncias [variveis] para os argumentos). Assim, qualquer alterao de um parmetro repercute-se sobre o argumento respectivo. Como neste tipo de passagem de argumentos no realizada qualquer cpia, ela tende a ser mais eciente que a passagem por valor, pelo menos para tipos em que as cpias so onerosas computacionalmente, o que no o caso dos tipos bsicos da linguagem. Por outro lado, este tipo de passagem de argumentos probe a passagem como argumento de constantes, como natural, mas tambm de variveis temporrias, tais como resultados de expresses que no sejam lvalues (ver Seco 7.7.1). Este facto impede a passagem de argumentos de tipos diferentes dos parmetros mas para os quais exista uma converso implcita. Quando a passagem de argumentos se faz por referncia constante,
// Declarao: TipoDeDevoluo rotina(TipoDoParmetro const& parmetro); // Denio: TipoDeDevoluo rotina(TipoDoParmetro const& parmetro) { ... }

os parmetros funcionam como sinnimos constantes dos argumentos (ou referncias constantes para os parmetros). Sendo constantes, as alteraes aos parmetros so proibidas. Por um lado, a passagem de argumentos por referncia constante semelhante passagem por valor, pois no s impossibilita alteraes aos argumentos como permite que sejam passados valores constantes e temporrios como argumentos e que estes sofram converses implcitas: uma referncia constante pode ser sinnimo tanto de uma varivel como de uma constante, pois uma varivel pode sempre ser tratada como uma constante (e no o contrrio), e pode mesmo ser sinnimo de uma varivel ou constante temporria. Por outro lado, este tipo de passagem de argumentos semelhante passagem de argumentos por referncia simples, pois no obriga realizao de cpias. Ou seja, a passagem de argumentos por referncia constante tem a vantagem das passagens por referncia, ou seja, a sua maior ecincia na passagem de tipos no bsicos, e a vantagens da passagem por valor, ou seja, a impossibilidade de alterao do argumento atravs do respectivo parmetro e a possibilidade de passar instncias (variveis ou constantes) temporrias ou no. Assim, como regra geral, sempre recomendvel a passagem de argumentos por referncia constante, em detrimento da passagem por valor, quando estiverem em causa tipos no

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO

381

bsicos e quando no houver necessidade por alguma razo de alterar o valor do parmetro durante a execuo da rotina em causa. Esta regra deve ser aplicada de forma sistemtica s rotinas membro e no-membro desenvolvidas, no caso deste captulo s rotinas associadas ao TAD Racional em desenvolvimento. A ttulo de exemplo mostra-se a sua utilizao na sobrecarga dos operadores +=, /= e + para a classe C++ Racional:
... class Racional { public: ... /** Adiciona de um racional. @pre *this = r. @post operator+= *this *this = r + r2. */ Racional& operator+=(Racional const& r2); /** Divide por um racional. @pre *this = r r2 = 0. @post operator/= *this *this = r/r2. */ Racional& operator/=(Racional const& r2); ... }; ... Racional& Racional::operator+=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); numerador = numerador * r2.denominador + r2.numerador * denominador; denominador *= r2.denominador; reduz(); assert(cumpreInvariante()); return *this; } Racional& Racional::operator/=(Racional const& r2) {

382

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


assert(cumpreInvariante() and r2.cumpreInvariante()); assert(r2 != 0); int numerador2 = r2.numerador_; if(r2.numerador_ < 0) { numerador_ *= -r2.denominador_; denominador_ *= -numerador2; } else { numerador_ *= r2.denominador_; denominador_ *= numerador2; } reduz(); assert(cumpreInvariante()); return *this; }

...
/** Adio de dois racionais. @pre r1 = r1 . @post operator+ = r1 + r2. */ Racional operator+(Racional r1, Racional const& r2) { return r1 += r2; } ...

Preservou-se a passagem por valor do primeiro argumento do operador + por ser desejvel que nesse caso o parmetro seja uma cpia do argumento, de modo a sobre ele se poder utilizar o operador +=. de notar uma alterao importante denio da sobrecarga do operador /=: passou a ser feita um cpia do numerador do segundo operando, representado pelo parmetro r2. fundamental faz-lo para que o cdigo tenha o comportamento desejvel no caso de se invocar o operador da seguinte forma:
r /= r;

Fica como exerccio para o leitor vericar que o resultado estaria longe do desejado se esta alterao no tivesse sido feita (dica: a varivel implcita e a varivel da qual r2 sinnimo so a mesma varivel).

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO

383

7.11.2 Constantes implcitas: operaes constantes


possvel denir constantes de um TAD concretizado custa de uma classe C++. Por exemplo, para a classe C++ Racional possvel escrever o cdigo
Racional const um_tero(1, 3);

que dene uma constante um_tero. O problema est em que, tal como a classe C++ Racional est denida, esta constante praticamente no se pode usar. Por exemplo, o cdigo
cout < < "O denominador " < < um_tero.denominador() < < endl;

resulta num erro de compilao. A razo para o erro simples: o compilador assume que as operaes da classe C++ Racional alteram a instncia implcita, ou seja, assume que as operaes tm sempre uma varivel e no uma constante implcita. Assim, como o compilador assume que h a possibilidade de a constante um_tero ser alterada, o que um contra-senso, simplesmente probe a invocao da operao inspectora Racional::denominador(). Note-se que o mesmo problema j existia no cdigo desenvolvido: repare-se na rotina que sobrecarrega o operador ==, por exemplo:
/** Indica se dois racionais so iguais. @pre V. @post operator== = (r1 = r2). */ bool operator==(Racional const& r1, Racional const& r2) { return r1.numerador() == r2.numerador() and r1.denominador() == r2.denominador(); }

Os parmetros r1 e r2 desta rotina funcionam como sinnimos constantes dos respectivos argumentos (que podem ser constantes ou no). Logo, o compilador assinala um erro no corpo desta rotina ao se tentar invocar os inspectores Racional::numerador() e Racional::denominador() atravs das duas constantes: o compilador no adivinha que uma operao no altera a instncia implcita. Alis, nem o poderia fazer, pois muitas vezes no cdigo que invoca a operao o compilador no tem acesso ao respectivo mtodo, como se ver no 9, pelo que no pode vericar se de facto assim . Logo, necessrio indicar explicitamente ao compilador quais as operaes que no alteram a instncia implcita, ou seja, quais as operaes que tratam a instncia implcita como uma constante implcita. Isso consegue-se acrescentando a palavra-chave const ao cabealho das operaes em causa e respectivos mtodos, pois esta palavra-chave passar a fazer parte da respectiva assinatura (o que permite sobrecarregar uma operao com o mesmo nome e lista de parmetros onde a nica coisa que varia a constncia da instncia implcita). Por exemplo, o inspector Racional::numerador() deve ser qualicado como no alterando a instncia implcita:

384
...

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

class Racional { public: ...


/** Devolve numerador da fraco cannica correspondente ao racional. @pre V. numerador @post denominador() = *this. */ int numerador() const;

... }; ...
int Racional::numerador() const { assert(cumpreInvariante()); assert(cumpreInvariante()); return numerador_; }

... importante perceber que o compilador verica se no mtodo correspondente a uma operao constante, que o nome que se d a uma operao que garante a constncia da instncia implcita, se executa alguma instruo que possa alterar a constante implcita. Isso signica que o compilador probe a invocao de operaes no-constantes atravs da constante implcita e tambm que probe a alterao dos atributos, pois os atributos de uma constante assumem-se tambm constantes! o facto de a constncia da instncia implcita ser agora claramente indicada atravs do qualicador const e garantida pelo compilador que permitiu deixar de explicitar essa constncia atravs de um termo extra na pr-condio e na condio objectivo: a constncia da instncia implcita continua a estar expressa no contrato destas operaes, mas agora no na prcondio e na condio objectivo mas na prpria sintaxe do cabealho das operaes 21 . Todas as operaes inspectoras so naturalmente operaes constantes. Embora tambm seja comum dizer-se que as operaes constantes so inspectoras, neste texto reserva-se o nome inspector para as operaes que devolvam propriedades da instncia para a qual so invocados. Pelo contrrio, s operaes que alteram a instncia implcita, ou que a permitem alterar
Exactamente da mesma forma que as pr-condies no se referem normalmente ao tipo dos parmetros (e.g., o primeiro parmetro tem de ser um int), pois esse facto expresso na prpria linguagem C++ e garantido pelo compilador (bem, quase sempre, como se ver quando se distinguir tipo esttico de tipo dinmico...).
21

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO

385

indirectamente, chama-se normalmente operaes modicadoras, embora tambm seja possvel distinguir entre vrias categorias de operaes no-constantes. Resta, pois, qualicar como constantes todas as operaes e respectivos mtodos que garantem a constncia da instncia implcita:
...

class Racional { public: ...


/** Devolve numerador da fraco cannica correspondente ao racional. @pre V. numerador @post denominador() = *this. */ int numerador() const; /** Devolve denominador da fraco cannica correspondente ao racional. @pre V.

n @post E n : V : denominador = *this 0 < denominador mdc(n, denominador) = 1 . */ int denominador() const;

/** Escreve o racional no ecr no formato de uma fraco. @pre V. @post cout cout contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this. */ d void escreve() const;

... private: ...


/** Indica se a condio invariante de classe se verica. @pre V. @post cumpreInvariante = (0 < denominador_ mdc(numerador_, denominador_) = 1). */ bool cumpreInvariante() const;

... }; ...

386

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


int Racional::numerador() const { assert(cumpreInvariante()); return numerador_; } int Racional:: denominador() const { assert(cumpreInvariante()); return denominador_; } void Racional::escreve() const { assert(cumpreInvariante()); cout < < numerador_; if(denominador_ != 1) cout < < / < < denominador_; }

...
bool Racional::cumpreInvariante() const { return 0 < denominador_ and mdc(numerador_, denominador_) == 1; } ...

Note-se que nas operaes que garantem a constncia da instncia implcita, tendo-se vericado a veracidade da condio invariante de classe no seu incio, no necessrio voltar a veric-la no seu nal. Note-se tambm que, pela sua natureza, a operao que indica se a condio invariante de instncia se verica, tipicamente chamada cumpreInvariante(), uma operao constante. interessante vericar que uma classe C++ tem duas interface distintas. A primeira, mais pequena, a interface disponvel para utilizao com constantes dessa classe, e consiste no conjunto das operaes que garantem a constncia da instncia implcita. A segunda, que engloba a primeira, a interface disponvel para utilizao com variveis da classe. Finalmente, muito importante pensar logo nas operaes de uma classe como sendo ou no constantes, ou melhor, como garantindo ou no a constncia da instncia implcita, e no fazlo posteriori, como neste captulo! O desenvolvimento do TAD Racional feito neste captulo no feito pela ordem mais apropriada na prtica (para isso ver o prximo captulo), mas

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO

387

sim pela ordem que se julgou mais conveniente pedagogicamente para introduzir os muitos conceitos associados a classes C++ que o leitor tem de dominar para as desenhar com procincia.

7.11.3 Devoluo por valor constante


Outro assunto relacionado com a constncia a devoluo de constantes. A ideia de devolver constantes pode parecer estranha primeira vista, mas repare-se no seguinte cdigo:
Racional r1(1, 2), r2(3, 2); ++(r1 + r2);

Que faz ele? Dene duas variveis r1 e r2, soma-as, e nalmente incrementa a varivel temporria devolvida pelo operador +. Tal cdigo mais provavelmente fruto de erro do programador do que algo desejado. Alm disso, semelhante cdigo seria proibido se em vez de racionais as variveis fossem do tipo int. Como se pretende que o TAD Racional possa ser usado como qualquer tipo bsico da linguagem, desejvel encontrar uma forma de proibir a invocao de operaes modicadoras atravs de instncias temporrias. luz da discusso na seco anterior, fcil perceber que o problema se resolve se as funes que devolvem instncias temporrias de classes C++, i.e., as funes que devolvem instncias de classes C++ por valor, forem alteradas de modo a devolverem constantes temporrias, e no variveis. No caso do TAD em desenvolvimento, so apenas as rotinas que sobrecarregam os operadores aritmticos usuais e os operadores de incrementao e decrementao suxo que precisam de ser alteradas:
/** Adio de dois racionais. @pre r1 = r1 . @post operator+ = r1 + r2. */ Racional const operator+(Racional r1, Racional const& r2) { return r1 += r2; } /** Subtraco de dois racionais. @pre r1 = r1 . @post operator- = r1 r2. */ Racional const operator-(Racional r1, Racional const& r2) { return r1 -= r2; } /** Produto de dois racionais. @pre r1 = r1 . @post operator* = r1 r2. */

388

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


Racional const operator*(Racional r1, Racional const& r2) { return r1 *= r2; } /** Diviso de dois racionais. @pre r1 = r1 r2 = 0. @post operator/ = r1 /r2. */ Racional const operator/(Racional r1, Racional const& r2) { assert(r2 != 0); return r1 /= r2; } /** Incrementa o racional recebido como argumento, devolvendo o seu valor antes de incrementado. @pre *this = r. @post operator++ = r *this = r + 1. */ Racional const operator++(Racional& r, int valor_a_ignorar) { Racional const cpia = r; ++r; return cpia; } /** Decrementa o racional recebido como argumento, devolvendo o seu valor antes de decrementado. @pre *this = r. @post operator- = r *this = r 1. */ Racional const operator--(Racional& r, int) { Racional const cpia = r; --r; return cpia; } ...

Ficaram a faltar ao TAD Racional os operadores + e - unrios. Comear-se- pelo segundo. O operador - unrio pode ser sobrecarregado quer atravs de uma operao da classe C++ Racional

7.11. CONSTNCIA: VERIFICANDO ERROS DURANTE A COMPILAO


...

389

class Racional { public: ...


/** Devolve simtrico do racional. @pre V. @post operator- = *this. */ Racional const operator-() const; ... }; ... Racional const Racional::operator-() const { assert(cumpreInvariante()); Racional r; r.numerador_ = -numerador_; r.denominador_ = denominador_; assert(r.cumpreInvariante()); return r; } ...

quer atravs de uma funo normal


Racional const operator-(Racional const& r) { return Racional(-r.numerador(), r.denominador()); }

Embora a segunda verso seja muito mais simples, ela implica a invocao do construtor mais complicado da classe C++, que verica o sinal do denominador e reduz a fraco correspondente ao numerador e denominador passados como argumento. Neste caso essas vericaes so inteis, pois o denominador no varia, mantendo-se positivo, e mudar o sinal do numerador mantm numerador e denominador mutuamente primos. Assim, prefervel a primeira verso, onde se constri um racional usando o construtor por omisso, que muito eciente, e em seguida se alteram directamente e sem mais vericaes os valores do numerador e denominador. Em qualquer dos casos devolvido um racional por valor e, por isso, constante.

390

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.11.4 Devoluo por referncia constante


Em alguns casos tambm possvel utilizar devoluo por referncia constante. Esta tm a vantagem de ser mais eciente do que a devoluo por valor, podendo ser utilizada quando o valor a devolver no for uma varivel local funo, nem uma instncia temporria construda dentro da funo, pois tal redundaria na devoluo de um sinnimo constante de uma instncia entretanto destruda... o caso do operador + unrio que, por uma questo de simetria, se sobrecarrega por intermdio de uma operao da classe C++ Racional:
...

class Racional { public: ...


/** Devolve verso constante do racional. @pre V. @post operator+ *this. */ Racional const& operator+() const; ... }; ... Racional const& Racional::operator+() const { assert(cumpreInvariante()); return *this; } ...

Como contra exemplo, suponha-se que a rotina que sobrecarrega o operador ++ suxo devolvia por referncia constante:
/** Incrementa o racional recebido como argumento, devolvendo o seu valor antes de incrementado. @pre *this = r. @post operator++ = r *this = r + 1. */ Racional const& operator++(Racional& r, int) { Racional const cpia = r;

7.12. REDUZINDO O NMERO DE INVOCAES COM INLINE


++r; return cpia; // Erro! Devoluo de referncia para varivel local! }

391

Seria claramente um erro faz-lo, pois seria devolvida uma referncia para uma instncia local, que destruda logo que a funo retorna.

7.12 Reduzindo o nmero de invocaes com inline


O mecanismo de invocao de rotinas (membro ou no) implica tarefas de arrumao da casa algo morosas, como se viu na Seco 3.4: necessrio colocar na pilha o endereo de retorno e os respectivo argumentos, executar as instrues do corpo da rotina, depois retirar os argumentos da pilha, e retornar, eventualmente devolvendo o resultado no seu topo. Logo, a invocao de rotinas pode ser, em alguns casos, um factor limitador da ecincia dos programas. Suponha-se as instrues:
Racional r(1, 3); Racional s = r + 2;

Quantas invocaes de rotinas so feitas neste cdigo? A resposta surpreendente, mesmo ignorando as instrues de assero (que alis podem ser facilmente desligadas): 1. O construtor para construir r, que invoca 2. a operao Racional::reduz(), cujo mtodo invoca 3. a funo mdc(). 4. O construtor para converter implicitamente o valor literal 2 num racional. 5. O construtor por cpia (ver Seco 7.4.2) para copiar o argumento r para o parmetro r1 durante a invocao d 6. a funo operator+, que invoca 7. a operao Racional::operator+=, cujo mtodo invoca 8. a operao Racional::reduz(), cujo mtodo invoca 9. a funo mdc(). 10. O construtor por cpia para devolver r1 por valor na funo operator+. 11. O construtor por cpia para construir a varivel s custa da constante temporria devolvida pela funo operator+.

392

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Mesmo tendo em conta que o compilador pode eventualmente optimizar algumas destas invocaes, 11 invocaes para duas inocentes linhas de cdigo parece demais. No ser lento? Como evit-lo? A linguagem C++ fornece uma forma simples de reduzir o peso da arrumao da casa aquando da invocao de uma rotina: rotinas muito simples, tipicamente no fazendo uso de ciclos e consistindo em apenas duas ou trs linhas (excluindo instrues de assero), podem qualicadas como em-linha ou inline. A palavra-chave inline pode ser usada para este efeito, qualicando-se com ela as denies das rotinas que se deseja que sejam em-linha. Mas o que signica a denio de uma rotina ser em-linha? Que o compilador, se lhe parecer apropriado (e o compilador pode-se recusar a faz-lo) em vez de traduzir o cdigo da rotina em linguagem mquina, coloc-lo num nico local do programa executvel e cham-lo quando necessrio, coloca o cdigo da rotina em linguagem mquina directamente nos locais onde ela deveria ser invocada. Por exemplo, natural que o cdigo mquina produzido por
inline int soma(int const& a, int const& b) { return a + b; } int main() { int x1 = 10; int x2 = 20; int x3 = 30; int r = 0; r = soma(x1, x2); r = soma(r, x3); }

seja idntico ao produzido por


int main() { int x1 = 10; int x2 = 20; int x3 = 30; int r = 0; r = x1 + x2; r = r + x3; }

Para melhor compreender o que foi dito, boa ideia fazer uma digresso pela linguagem assembly, alis a nica nestas folhas. Para isso recorrer-se- mquina MAC-1, desenvolvida

7.12. REDUZINDO O NMERO DE INVOCAES COM INLINE

393

por Andrew Tanenbaum para ns pedaggicos e apresentada em [13, Seco 4.3] (ver tambm MAC-1 asm http://www.daimi.aau.dk/~bentor/html/useful/asm.html). A traduo para o assembly do MAC-1 do programa original : Se no levasse em conta o qualicador inline, um compilador de C++ para assembly MAC-1 poderia gerar:
jump main # Variveis: x1 = 10 x2 = 20 x3 = 30 r = 0 main: # Programa principal: lodd x1 # Carrega varivel x1 no acumulador. push # Coloca acumulador no topo da pilha. lodd x2 # Carrega varivel x2 no acumulador. push # Coloca acumulador no topo da pilha. # Aqui a pilha tem os dois argumentos x1 e x2: call soma # Invoca a funo soma(). insp 2 # Repe a pilha. # Aqui o acumulador tem o valor devolvido. stod r # Guarda o acumulador na varivel r. lodd r push lodd x3 push # Aqui a pilha tem os dois argumentos r e x3: call soma insp 2 # Aqui o acumulador tem o valor devolvido. stod r halt soma: # Funo soma(): lodl 1 # Carrega no acumulador o segundo parmetro. addl 2 # Adiciona primeiro parmetro ao acumulador. retn # Retorna, devolvendo resultado no acumulador.

Se levasse em conta o qualicador inline, um compilador de C++ para assembly MAC-1 provavelmente geraria:
jump main

394

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

# Variveis: x1 = 10 x2 = 20 x3 = 30 r = 0 main: # Programa principal: lodd x1 # Carrega varivel x1 no acumulador. addd x2 # Adiciona varivel x2 ao acumulador. stod r # Guarda o acumulador na varivel r. lodd r addd x3 stod r halt

A diferena entre os dois programas em assembly notvel. O segundo claramente mais rpido, pois evita todo o mecanismo de invocao de funes. Mas tambm mais curto, ou seja, ocupa menos espao na memria do computador! Embora normalmente haja sempre um ganho em termos do nmero de instrues a efectuar, se o cdigo a colocar em-linha for demasiado extenso, o programa pode-se tornar mais longo, o que pode inclusivamente levar ao esgotamento da memria fsica, levando utilizao da memria virtual do sistema operativo, que tem a lamentvel caracterstica de ser ordens de grandeza mais lenta. Assim, necessrio usar o qualicador inline com conta, peso e medida. Para denir uma operao como em-linha, pode-se fazer uma de duas coisas: 1. Ao denir a classe C++, denir logo o mtodo (em vez de a declarar apenas a respectiva operao). 2. Ao denir o mtodo correspondente operao declarada na denio da classe, preceder o seu cabealho do qualicador inline. Em geral a segunda alternativa prefervel primeira, pois torna mais evidente a separao entre a interface e a implementao da classe, separando claramente operaes de mtodos. A denio de uma rotina, membro ou no-membro, como em-linha no altera a semntica da sua invocao, tendo apenas consequncias em termos da traduo do programa para cdigo mquina. No caso do cdigo em desenvolvimento neste captulo, relativo ao TAD Racional, todas as rotinas so sucientemente simples para que se justique a utilizao do qualicador inline, com excepo apenas da funo mdc(), por envolver um ciclo, e da operao Racional::l(), por ser demasiado extensa. Exemplicando apenas para o primeiro construtor da classe:

7.13. OPTIMIZAO DOS CLCULOS COM RACIONAIS


... class Racional { public: /** Constri racional com valor inteiro. Construtor por omisso. @pre V. @post *this = n. */ Racional(int const n = 0); ... }; inline Racional::Racional(int const n) : numerador_(n), denominador_(1) { assert(cumpreInvariante()); assert(numerador_ == n * denominador_); } ...

395

7.13 Optimizao dos clculos com racionais


Um dos problemas com a representao escolhida para a classe C++ Racional o facto de os atributos numerador_ e denominador_ serem do tipo int, que tem limitaes devidas sua representao na memria do computador. Essa foi parte da razo pela qual se insistiu em que os racionais fossem sempre representados pelo numerador e denominador de uma fraco no formato cannico, i.e., com denominador positivo e formando uma fraco reduzida. No entanto, esta escolha no suciente. Basta olhar para a denio da funo que sobrecarrega o operador < para a classe C++ Racional
/** Indica se o primeiro racional menor que o segundo. @pre V. @post operator< = (r1 < r2). */ inline bool operator<(Racional const& r1, Racional const& r2) { return r1.numerador() * r2.denominador() < r2.numerador() * r1.denominador(); }

para se perceber imediatamente que, mesmo que os racionais sejam representveis, durante clculos intermdios que os envolvam podem ocorrer transbordamentos. No entanto, embora

396

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

seja impossvel eliminar totalmente a possibilidade de transbordamentos (excepto eventualmente abandonando o tipo int e usando um TAD representando nmeros inteiros de dimenso arbitrria), possvel minorar o seu impacte. Por exemplo, no caso do operador < possvel encontrar divisores comuns aos numeradores e aos denominadores dos racionais a comparar e us-los para reduzir ao mximo a magnitude dos inteiros a comparar:
/** Indica se o primeiro racional menor que o segundo. @pre V. @post operator< = (r1 < r2). */ inline bool operator<(Racional const& r1, Racional const& r2) { int dn = mdc(r1.numerador(), r2.numerador()); int dd = mdc(r1.denominador(), r2.denominador()); return (r1.numerador() / dn) * (r2.denominador() / dd) < (r2.numerador() / dn) * (r1.denominador() / dd); }

As mesmas ideias podem ser aplicadas a outras operaes, pelo que se discutem nas seces 1 2 seguintes. Durante estas seces admite-se que as fraces originais ( n , n1 e n2 ) esto no d d d formato cannico. Recorda-se tambm que se admite uma extenso da funo mdc de tal forma que mdc(0, 0) = 1.

7.13.1 Adio e subtraco


O resultado da soma de fraces dado por n1 n2 n1 d 2 + n 2 d 1 + = , d1 d2 d1 d 2 embora tenha forosamente o denominador positivo, pode no estar no formato cannico. Se k = mdc(d1 , d2 ) e l = mdc(n1 , n2 ), ento, dividindo ambos os termos da fraco resultado por k e pondo l em evidncia, l (n1 d2 + n2 d1 ) n1 n2 + = , d1 d2 k d 1 d2 onde d1 = d1 /k e d2 = d2 /k so mutuamente primos, i.e., mdc(d 1 , d2 ) = 1, e n1 = n1 /l e n2 = n2 /l so mutuamente primos, i.e., mdc(n 1 , n2 ) = 1. Este novo resultado, apesar da diviso por k de ambos os termos da fraco, pode ainda no estar no formato cannico, pois pode haver divisores no-unitrios comuns ao numerador e ao denominador. Repare-se no exemplo 1 1 + , 10 15

7.13. OPTIMIZAO DOS CLCULOS COM RACIONAIS


em que k = mdc(10, 15) = 5. Aplicando a equao acima obtm-se 1 1 13+12 5 + = = . 10 15 523 30

397

Neste caso, para reduzir a fraco aos termos mnimos necessrio dividir ambos os termos da fraco por 5. Em vez de tentar reduzir a fraco resultado tomando quer o numerador quer o denominador como um todo, prefervel vericar primeiro se possvel haver divisores comuns entre os respectivos factores. Considerar-se-o dois factores para o numerador (l e n 1 d2 + n2 d1 ) e dois factores para o denominador (k e d 1 d2 ), num total de quatro combinaes onde possvel haver divisores comuns. Ser que podem haver divisores no-unitrios comuns a l e a k? Suponha-se que existe um divisor 1 < i comum a l e a k. Nesse caso, dado que d 1 = d1 k e n1 = n1 l, ter-se-ia de concluir que i mdc(n1 , d1 ), ou seja, i 1, o que uma contradio. Logo, l e a k no tm divisores comuns no-unitrios. Ser que pode haver divisores no-unitrios comuns a l e a d 1 d2 ? Suponha-se que existe um divisor 1 < i comum a l e a d1 d2 . Nesse caso, existe forosamente um divisor 1 < j comum a l e a d1 ou a d2 . Se j for divisor comum a l e a d1 , ento j tambm divisor comum a n1 e a d1 , ou seja, j mdc(n1 , d1 ), donde se conclui que j 1, o que uma contradio. O mesmo argumento se aplica se j for divisor comum a l e a d 2 . Logo, l e d1 d2 no tm divisores comuns no-unitrios. Ser que podem haver divisores no-unitrios comuns a n 1 d2 +n2 d1 e a d1 d2 ? Suponha-se que existe um divisor 1 < h comum n1 d2 +n2 d1 e de d1 d2 . Nesse caso, existe forosamente um divisor 1 < i comum a n1 d2 + n2 d1 e a d1 ou a d2 . Seja ento 1 < i um divisor comum a n1 d2 + n2 d1 e a d1 . Nesse caso tem de existir um divisor 1 < j comum a d 1 e a n1 ou a d2 . Isso implicaria que j mdc(n1 , d1 ) ou que j mdc(d1 , d2 ) = 1. Em qualquer dos casos conclui-se que j 1, o que uma contradio. O mesmo argumento se aplica se 1 < i for divisor comum a n1 d2 + n2 d1 e a d2 . Logo, n1 d2 + n2 d1 e d1 d2 no tm divisores comuns no-unitrios. Assim, a existirem divisores no-unitrios comuns ao denominador e numerador da fraco l (n1 d2 + n2 d1 ) , k d 1 d2

eles devem-se existncia de divisores no-unitrios comuns a n 1 d2 + n2 d1 e a k. Assim, sendo m = mdc(n1 d2 + n2 d1 , k), a fraco l ((n1 d2 + n2 d1 ) /m) n1 n2 + = , d1 d2 (k/m) d1 d2

est no formato cannico.

398

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Qual foi a vantagem de factorizar l e k e proceder aos restantes clculos face alternativa, mais simples, de calcular a fraco como n1 n2 (n1 d2 + n2 d1 ) /h + = , d1 d2 (d1 d2 ) /h com h = mdc(n1 d2 + n2 d1 , d1 d2 )? A vantagem meramente computacional. Apesar de os clculos propostos exigirem mais operaes, os valores intermdios dos clculos so em geral mais pequenos, o que minimiza a possibilidade de existirem valores intermdios que no sejam representveis em valores do tipo int, evitando-se assim transbordamentos.

A fraco cannica correspondente adio pode ser portanto calculada pela equao acima. A fraco cannica correspondente subtraco pode ser calculada por uma equao semelhante l ((n1 d2 n2 d1 ) /m) n1 n2 . = d1 d2 (k/m) d1 d2 Pode-se agora actualizar a denio dos mtodos Racional::operator+= e Racional::operator-= para:
Racional& Racional::operator+=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); // Devido a r += r: int d2 = r2.denominador_; numerador_ /= dn; denominador_ /= dd; numerador_ = numerador_ * (d2 / dd) + r2.numerador_ / dn * denominador_; dd = mdc(numerador_, dd); numerador_ = dn * (numerador_ / dd); denominador_ *= d2 / dd; assert(cumpreInvariante()); return *this; }

7.13. OPTIMIZAO DOS CLCULOS COM RACIONAIS


Racional& Racional::operator-=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); // Devido a r -= r: int d2 = r2.denominador_; numerador_ /= dn; denominador_ /= dd; numerador_ = numerador_ * (d2 / dd) r2.numerador_ / dn * denominador_; dd = mdc(numerador_, dd); numerador_ = dn * (numerador_ / dd); denominador_ *= d2 / dd; assert(cumpreInvariante()); return *this; }

399

Uma vez que ambos os mtodos caram bastante extensos, decidiu-se retirar-lhes o qualicador inline.

7.13.2 Multiplicao
Relativamente multiplicao de fraces, n1 n 2 n1 n2 = , d1 d2 d1 d 2 apesar de o denominador ser forosamente positivo, possvel que o resultado no esteja no formato cannico, bastando para isso que existam divisores no-unitrios comuns a n 1 e d2 ou a d1 e n2 . fcil vericar que, sendo k = mdc(n 1 , d2 ) e l = mdc(n2 , d1 ), a fraco n1 n2 (n1 /k) (n2 /l) = d1 d2 (d1 /l) (d2 /k) est, de facto, no formato cannico. Pode-se agora actualizar a denio do mtodo Racional::operator*= para:

400

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


inline Racional& Racional::operator*=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); int n1d2 = mdc(numerador_, r2.denominador_); int n2d1 = mdc(r2.numerador_, denominador_); numerador_ = (numerador_ / n1d2) * (r2.numerador_ / n2d1); denominador_ = (denominador_ / n2d1) * (r2.denominador_ / n1d2); assert(cumpreInvariante()); return *this; }

7.13.3 Diviso
O caso da diviso de fraces, n1 n2 n1 d 2 / = se n2 = 0, d1 d2 d1 n 2 muito semelhante ao da multiplicao, sendo mesmo possvel usar os mtodos acima para a calcular. Em primeiro lugar necessrio garantir que n 2 = 0. Se n2 = 0 a diviso no est denida. Admitindo que n2 = 0, ento a diviso equivalente a uma multiplicao: n1 d2 n1 n2 / = . d1 d2 d1 n2 No entanto, necessrio vericar se n 2 positivo, pois de outra forma o resultado da multiplicao no estar no formato cannico, uma vez que ter denominador negativo. Se 0 < n 2 , a d 1 diviso calculada multiplicando as fraces cannicas n1 e n2 . Se n2 < 0, multiplicam-se as d 2 d 1 fraces cannicas n1 e n2 . Para garantir que o resultado est no formato cannico usa-se a d 2 mesma tcnica que para a multiplicao. Pode-se agora actualizar a denio do mtodo Racional::operator/= para:
inline Racional& Racional::operator/=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); assert(r2 != 0); int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); if(r2.numerador_ < 0) {

7.13. OPTIMIZAO DOS CLCULOS COM RACIONAIS


numerador_ = denominador_ } else { numerador_ = denominador_ } (numerador_ / dn) * (-r2.denominador_ / dd); = (denominador_ / dd) * (-r2.numerador_ / dn); (numerador_ / dn) * (r2.denominador_ / dd); = (denominador_ / dd) * (r2.numerador_ / dn);

401

assert(cumpreInvariante()); return *this; }

7.13.4 Simtrico e identidade


O caso das operaes de clculo do simtrico e da identidade, n d n + d = = n e d n , d

no pe qualquer problema, pois os resultados esto sempre no formato cannico.

7.13.5 Operaes de igualdade e relacionais


Sendo dois racionais r1 e r2 e as respectivas representaes na forma de fraces cannicas 2 1 r1 = n1 e r2 = n2 , evidente que r1 = r2 se e s se n1 = n2 d1 = d2 . Da mesma forma, r1 = r2 d d se e s se n1 = n2 d1 = d2 .

1 2 Relativamente aos mesmos dois racionais, a expresso r 1 < r2 equivalente a n1 < n2 ou d d ainda a n1 d2 < n2 d1 , pois ambos os denominadores so positivos. Assim, possvel comparar dois racionais usando apenas comparaes entre inteiros. Os inteiros a comparar podem ser reduzidos calculando k = mdc(d1 , d2 ) e l = mdc(n1 , n2 ) e dividindo os termos apropriados da expresso: (n1 /l) (d2 /k) < (n2 /l) (d1 /k) .

Da mesma forma podem-se reduzir todas as comparaes entre racionais (com <, >, ou ) s correspondentes comparaes entre inteiros. Pode-se agora actualizar a denio da rotina operator<:
/** Indica se o primeiro racional menor que o segundo. @pre V. @post operator< = (r1 < r2). */ inline bool operator<(Racional const& r1, Racional const& r2) { int dn = mdc(r1.numerador(), r2.numerador());

402

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


int dd = mdc(r1.denominador(), r2.denominador()); return (r1.numerador() / dn) * (r2.denominador() / dd) < (r2.numerador() / dn) * (r1.denominador() / dd); }

7.13.6 Operadores especiais


O TAD Racional tal como concretizado at agora, suporta operaes simultneas entre racionais e inteiros, sendo para isso fundamental a converso implcita entre valores do tipo int e o tipo Racional fornecida pelo primeiro construtor da respectiva classe C++. No entanto, instrutivo seguir a ordem dos acontecimentos quando se calcula, por exemplo, a soma de um racional com um inteiro:
Racional r(1, 2); cout < < r + 1 < < endl;

A soma implica as seguintes invocaes: 1. Construtor da classe C++ Racional para converter o inteiro 1 no correspondente racional. 2. Rotina operator+(). 3. Operao Racional::operator+=(). Ser possvel evitar a converso do inteiro em racional e, sobretudo, evitar calcular a soma de um racional com um inteiro recorrendo complicada maquinaria necessria para somar dois racionais? Certamente. Basta fornecer verses especializadas para operandos inteiros das sobrecargas dos operadores em causa:
...

class Racional { public: ...


/** Adiciona de um inteiro. @pre *this = r. @post operator+= *this *this = r + n. */ Racional& operator+=(int const n); ...

7.14. OPERADORES DE INSERO E EXTRACO


}; ... Racional& Racional::operator+=(int const i) { assert(cumpreInvariante()); numerador_ += i * denominador_; assert(cumpreInvariante()); return *this; } /** Adio de um racional e um inteiro. @pre r = r. @post operator+ = r + i. */ inline Racional const operator+(Racional r, int const i) { return r += i; } /** Adio de um inteiro e um racional. @pre r = r. @post operator+ = i + r. */ inline Racional const operator+(int const i, Racional r) { return r += i; }

403

Fica como exerccio para o leitor desenvolver as sobrecargas de operadores especializadas em todos os outros casos em que se possam fazer operaes conjuntas entre inteiros e racionais.

7.14 Operadores de insero e extraco


possvel e desejvel sobrecarregar o operador < < de insero num canal e o operador > > de extraco de um canal. Alis, estas sobrecargas so, digamos, a cereja sobre o bolo. So o toque nal que permite escrever o programa da soma de racionais exactamente da mesma forma como se faria se se pretendesse somar inteiros:
int main() { // Ler fraces:

404

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; cin > > r1 > > r2; if(not cin) { cerr < < "Opps! return 1; }

A leitura dos racionais falhou!" < < endl;

// Calcular racional soma: Racional r = r1 + r2; // Escrever resultado: cout < < "A soma de " < < r1 < < " com " < < r2 < < " " < < r < < . < < endl; }

7.14.1 Sobrecarga do operador < <


Comear-se- pelo operador de insero, por ser mais simples. O operador < < binrio, tendo por isso dois operandos. Por exemplo, se se pretender escrever um valor inteiro no ecr podese usar a instruo
cout < < 10;

onde o primeiro operando, cout, um canal de sada, ligado normalmente ao ecr, e 10 um valor literal inteiro. O efeito da operao fazer surgir o valor 10 no ecr. O segundo operando claramente do tipo int, mas, qual o tipo do primeiro operando? Alis, o que cout? A resposta a estas perguntas muito importante para a sobrecarga do operador < < desejada: o primeiro operando, cout uma varivel global do tipo ostream, ou seja, canal de sada. Ambos, varivel cout e tipo ostream, esto declarados no cheiro de interface 22 iostream. Por outro lado, o operador < < um operador binrio como qualquer outro, e por isso tem associatividade esquerda. Isso quer dizer que a instruo
cout < < 10 < < ;

interpretada como
(cout < < 10) < < ;

A primeira operao com o operador < < faz-se, por isso, com o primeiro operando do tipo ostream e o segundo do tipo char. A segunda operao com o operador < < faz-se claramente com o segundo operando do tipo char. De que tipo ser o primeiro operando nesse
Um cheiro de interface usado na modularizao fsica para permitir a um mdulo fsico (cheiro) usar ferramentas denidas noutro mdulo fsico. Estes assuntos so matria do Captulo 9.
22

7.14. OPERADORES DE INSERO E EXTRACO

405

caso? E que valor possui? As respostas so evidentes se se lembrar que a instruo acima equivalente a
cout < < 10; cout < < ;

claro que o primeiro operando da segunda operao com o operador < < tem de ser no apenas do tipo ostream, para que o segundo operando seja inserido num canal, mas deve ser exactamente o canal cout, para que a insero se faa no local correcto. A sobrecarga do operador < < no se pode fazer custa de uma operao da classe Racional, pois o primeiro operando do operador deve ser do tipo ostream. Sendo este tipo uma classe, quando muito o operador < < poderia ser sobrecarregado por uma operao dessa classe. Mas como a classe est pr-denida na biblioteca padro do C++, no possvel faz-lo. Assim, a sobrecarga ser feita usando uma rotina normal, e por isso com dois parmetros. O primeiro parmetro corresponder ao canal onde se deve realizar a operao de insero, e o segundo ao racional a inserir. Como o canal certamente modicado pela operao de insero, ter de ser passado por referncia:

tipo_de_devoluo operator< <(ostream& sada, Racional const& r); O tipo de devoluo, para que o canal seja passado sucessivamente em instrues de insero encadeadas tais como Racional r1, r2; ... cout < < r1 < < < < r2 < < endl; ter naturalmente de ser ostream&, ou seja, o canal ter de ser devolvido por referncia. Alis, a justicao para o fazer idntica que se usou para os operadores de incrementao e decrementao prexo e para os operadores especiais de atribuio. Ou seja, a denio da rotina operator< < dever tomar a forma:
/** Insere o racional no canal de sada no formato de uma fraco. @pre V. @post sada sada contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional r. */ d ostream& operator< <(ostream& sada, Racional const& r) { ... return sada; }

406

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Dada a existncia de operaes de inspeco que permitem obter o numerador e o denominador da fraco cannica correspondente a um racional, seria perfeitamente possvel eliminar a operao Racional::escreve() da classe C++ Racional e usar o seu corpo, com adaptaes, como corpo da rotina operator< <. No entanto, adoptar-se- uma soluo diferente. Manter-se- a operao Racional::escreve(), embora com um nome mais apropriado, e implementar-se- a rotina operator< < sua custa. Para isso fundamental tornar a operao mais genrica, de modo a inserir o racional num canal arbitrrio:
... class Racional { public: ... /** Insere o racional no canal no formato de uma fraco. @pre V. @post sada sada contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this. */ d void insereEm(ostream& sada) const; ... }; ... ... inline void Racional::insereEm(ostream& sada) const { assert(cumpreInvariante()); sada < < numerador_; if(denominador_ != 1) sada < < / < < denominador_; } ... /** Insere o racional no canal de sada no formato de uma fraco. @pre V. @post sada sada contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional r. */ d ostream& operator< <(ostream& sada, Racional const& r) { r.insereEm(sada); return sada;

7.14. OPERADORES DE INSERO E EXTRACO


} ...

407

7.14.2 Sobrecarga do operador > >


O caso do operador > > muito semelhante, embora neste caso o primeiro operando do operador seja do tipo istream, ou seja, canal de entrada, e se deva passar o racional por referncia, para permitir a sua alterao:
... class Racional { public: ... /** Extrai do canal um novo valor para o racional, na forma de dois inteiros sucessivos. @pre *this = r. @post Se entrada e entrada tem dois inteiros n e d disponveis para leitura, com d = 0, ento *this = n entrada, d seno *this = r entrada. */ void extraiDe(istream& entrada); ... }; ... ... void Racional::extraiDe(istream& entrada) { assert(cumpreInvariante()); int n, d; if(entrada > > n > > d) if(d == 0) entrada.setstate(ios_base::failbit); else { numerador_ = d < 0 ? -n : n; denominador_ = d < 0 ? -d : d;

408

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


reduz(); assert(cumpreInvariante()); assert(numerador_ * d == n * denominador_ and cin); return; } assert(cumpreInvariante()); assert(not entrada); } ... /** Extrai do canal um novo valor para o racional, na forma de dois inteiros sucessivos. @pre r = r. @post Se entrada e entrada tem dois inteiros n e d disponveis para leitura, com d = 0, ento r = n entrada, d seno r = r entrada. */ istream& operator> >(istream& entrada, Racional& r) { r.extraiDe(entrada); return entrada; } ...

Neste caso a vantagem de implementar a rotina operator> > custa de uma operao correspondente na classe C++ ca mais clara. Como a rotina operator> > no pode ser membro da classe e, no entanto, necessita de alterar os atributos da classe, a soluo foi delegar a tarefa da extraco ou leitura para uma operao da classe, que no tem quaisquer restries de acesso.

7.14.3 Lidando com erros


No incio deste captulo apresentou-se a primeira verso da rotina de leitura de racionais, ento vistos simplesmente como fraces, sem mais explicaes. Chegou agora a altura de explicar o cdigo dessa rotina, entretanto convertida na operao Racional::extraiDe(), apresentada abaixo sem instrues de assero, i.e., reduzida ao essencial:
void Racional::extraiDe(istream& entrada) { int n, d;

7.14. OPERADORES DE INSERO E EXTRACO

409

if(entrada > > n > > d) if(d == 0) entrada.setstate(ios_base::failbit); else { numerador_ = d < 0 ? -n : n; denominador_ = d < 0 ? -d : d; reduz(); } }

Em primeiro lugar, note-se que a extraco do numerador e do denominador se faz, no directamente para os respectivos atributos, mas para duas variveis criadas para o efeito. De outra forma, se a extraco do numerador tivesse sucesso, mas a extraco do denominador falhasse, a extraco do racional como um todo teria falhado e este teria mudado de valor, o que, para alm de ser m ideia, violaria o estabelecido no contrato da operao. Usa-se o idioma do C++
if(entrada > > n > > d) ...

para fazer simultaneamente a extraco dos dois valores inteiros canal de entrada e vericar se essa leitura teve sucesso. Esta instruo poderia (e talvez devesse...) ser decomposta em duas:
entrada > > n > > d; if(entrada) ...

A primeira destas instrues serve para fazer as duas extraces. Tal como se viu para o operador < <, a primeira instruo interpretada como
(entrada > > n) > > d;

devido associatividade esquerda do operador > >. Que acontece quando a primeira extraco falha, por exemplo quando no canal no se encontra uma sequncia de dgitos interpretvel como um nmero inteiro, mas sim, por exemplo, caracteres alfabticos? Nesse caso a primeira extraco no tem qualquer efeito e o canal entrada ca em estado de erro. Nesse caso, a segunda falhar tambm, pois todas as operaes de insero e extraco realizadas sobre um canal em estado de erro esto condenadas ao fracasso. Ou seja, se a primeira operao falhar, a segunda no tem qualquer efeito, o que equivalente a no ser realizada. Como o valor de um canal interpretado como um valor booleano verdadeiro se o canal no estiver em estado de erro e falso se estiver, as instrues acima so equivalentes a

410

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


if(not entrada.fail()) { entrada > > n; if(not entrada.fail()) { entrada > > d; if(not entrada.fail()) ... } }

onde istream::fail() uma operao da classe istream que indica se o canal est em estado de erro. Uma vez realizadas as duas extraces com sucesso, necessrio vericar ainda assim se os valores lidos so aceitveis. Neste caso isso corresponde a vericar se o denominador nulo. Se for, a leitura deve falhar. I.e., o racional deve manter o valor original e o canal deve car em estado de erro, de modo a que a falha de extraco possa ser detectada mais tarde. S dessa forma ser possvel, por exemplo, escrever o seguinte cdigo:
Racional r; r.extraiDe(cin); if(not cin){ cerr < < "Opps! ... }

A leitura falhou!" < < endl;

ou mesmo
Racional r; cin > > r; if(not cin){ cerr < < "Opps! ... }

A leitura falhou!" < < endl;

utilizando o operador > > j sobrecarregado para os racionais. Pretende-se de novo que o comportamento de um programa usando racionais seja to semelhante quanto possvel do ponto de vista semntico com o mesmo programa usando inteiros. Assim, no caso de se extrair um denominador nulo, deve-se colocar o canal entrada em estado de erro. Para o fazer usa-se a instruo 23
23

Tambm se pode limpar o estado de erro de um canal, usando-se para isso a operao istream::clear(): entrada.clear();

7.14. OPERADORES DE INSERO E EXTRACO


entrada.setstate(ios_base::failbit);

411

A parte restante do cdigo auto-explicativa.

7.14.4 Coerncia entre os operadores < < e > >


Tal como sobrecarregados, os operadores < < e > > para racionais, ou melhor, as operaes Racional::insereEm() e Racional::extraiDe() no so coerentes: a extraco no suporta o formato usado pela insero, nomeadamente no espera o smbolo / entre os termos da fraco, nem admite a possibilidade de o racional no ter denominador explcito, nomeadamente quando tambm um valor inteiro. excelente ideia que o operador de extraco consiga extrair racionais em qualquer dos formatos produzidos pelo operador de insero. Para o conseguir necessrio complicar um pouco o mtodo Racional::extraiDe(), e indicar claramente o novo formato admissvel no contrato das rotinas envolvidas:
... class Racional { public: ... /** Extrai do canal um novo valor para o racional, na forma de uma fraco. @pre *this = r. @post Se entrada e entrada contm n /d (ou apenas n , assumindose d = 1), em que n e d so inteiros com d = 0, ento *this = n entrada, d seno *this = r entrada. */ void extraiDe(istream& entrada); ... }; ... ... void Racional::extraiDe(istream& entrada) { assert(cumpreInvariante()); int n; int d = 1; if(entrada > > n) {

412

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


if(entrada.peek() != /) { numerador_ = n; denominador_ = 1; } else { if(entrada.get() and isdigit(entrada.peek()) and entrada > > d and d != 0) { numerador_ = d < 0 ? -n : n; denominador_ = d < 0 ? -d : d; reduz(); } else if(entrada) entrada.setstate(ios_base::failbit); } } assert(cumpreInvariante()); assert(numerador_ * d == n * denominador_ or not entrada.good()); } ... /** Extrai do canal um novo valor para o racional, na forma de uma fraco. @pre r = r. @post Se entrada e entrada contm n /d (ou apenas n , assumindose d = 1), em que n e d so inteiros com d = 0, ento r = n entrada, d seno r = r entrada. */ istream& operator> >(istream& entrada, Racional& r) { r.extraiDe(entrada); return entrada; } ...

So de notar os seguintes pontos: A operao istream::peek() devolve o valor do prximo caractere no canal sem o extrair! A operao istream::get() extrai o prximo caractere do canal (e devolve-o).

7.14. OPERADORES DE INSERO E EXTRACO

413

A funo isdigit()24 indica se o caractere passado como argumento um dos dgitos decimais. Fica como exerccio para o leitor o estudo pormenorizado do mtodo Racional::extraiDe().

7.14.5 Leitura e escrita de cheiros


Uma vez sobrecarregados os operadores < < e > > para a classe C++ Racional, possvel uslos para fazer extraces e inseres no apenas do teclado e para o ecr, mas de e para onde quer que um canal esteja estabelecido. possvel estabelecer canais de entrada e sada para cheiros, por exemplo. Para isso usam-se as classes ifstream e ofstream, compatveis com istream e ostream, respectivamente, e que cam disponveis se se incluir a seguinte linha no incio do programa:
#include <fstream>

Escrita em cheiros Para se poder escrever num cheiro necessrio estabelecer um canal de escrita ligado a esse cheiro e inserir nele os dados a escrever no cheiro. O estabelecimento de um canal de escrita para um cheiro feito de uma forma muito simples. A instruo
ofstream sada("nome do ficheiro");

constri um novo canal chamado sada que est ligado ao cheiro da nome nome do ficheiro. Note-se que sada uma varivel como outra qualquer, s que representa um canal de escrita para um dado cheiro. A instruo acima possvel porque a classe ofstream possui um construtor que recebe uma cadeia de caracteres clssica do C++. Isso signica que possvel usar cadeias de caracteres para especicar o nome do cheiro ao qual o canal deve estar ligado, desde que se faa uso da operao string::c_str(), que devolve a cadeia de caracteres clssica corresponde a uma dada cadeia de caracteres:
cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ofstream sada(nome_do_ficheiro.c_str());
24

Acrescentar #include <cctype>

no incio do programa para usar esta funo.

414

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

O estabelecimento de um canal de sada para um dado cheiro uma operao destrutora: se o cheiro j existir, esvaziado antes. Dessa forma a escrita comea sempre do nada. Claro est que o estabelecimento de um canal de escrita pode falhar. Por exemplo, o disco rgido pode estar cheiro, pode no haver permisses para escrever no directrio em causa, ou pode existir j um cheiro com o mesmo nome protegido para escrita. Se o estabelecimento do canal falhar durante a sua construo, este ca em estado de erro, sendo muito fcil vericar essa situao tratando o canal como se de um booleano se tratasse:
cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ofstream sada(nome_do_ficheiro.c_str()); if(not sada) { cerr < < "Opps... No consegui criar ficheiro \"" < < nome_do_ficheiro < < "\"!" < < endl; ... }

ainda possvel estabelecer e encerrar um canal usando as operaes ofstream::open(), que recebe o nome do cheiro como argumento, e ofstream::close(), que no tem argumentos. S se garante que todos os dados inseridos num canal ligado a um cheiro j nele foram escritos se: 1. o canal tiver sido explicitamente encerrado atravs da operao ostream::close(); 2. o canal tiver sido destrudo da forma usual (e.g., no nal do bloco onde a varivel que o representa est denida); 3. tiver sido invocada a operao ostream::flush(); 4. tiver sido inserido no canal o manipulador flush (i.e., sada < < flush); ou 5. tiver sido inserido no canal o manipulador endl (i.e., sada < < endl), que coloca um m-de-linha no canal e invoca automaticamente a operao ostream::flush(). Ou seja, tudo funciona como se o canal tivesse um certa capacidade para dados, tal como uma mangueira tem a capacidade de armazenar um pouco de gua. Tal como numa mangueira s se pode garantir que toda a gua que nela entrou saiu pela outra ponta tomando algumas diligncias, tambm num canal de sada s se pode garantir que os dados chegaram ao seu destino, e no esto ainda em circulao no canal se se diligenciar como indicado acima. A partir do momento em que um canal de sada est estabelecido, pode-se us-lo da mesma forma que ao canal cout. Por exemplo:

7.14. OPERADORES DE INSERO E EXTRACO


cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ofstream sada(nome_do_ficheiro.c_str()); if(not sada) { cerr < < "Opps... No consegui criar ficheiro \"" < < nome_do_ficheiro < < "\"!" < < endl; ... } Racional r(1, 3); sada < < r < < endl;

415

Leitura de cheiros Para se poder ler de um cheiro necessrio estabelecer um canal de leitura ligado a esse cheiro e extrair dele os dados a ler do cheiro. O estabelecimento de um canal de leitura para um cheiro feito de uma forma muito simples. A instruo
ifstream entrada("nome do ficheiro");

constri um novo canal chamado entrada que est ligado ao cheiro da nome nome do ficheiro. Mais uma vez entrada uma varivel como outra qualquer, s que representa um canal de leitura para um dado cheiro. tambm possvel usar cadeias de caracteres para especicar o nome do cheiro ao qual o canal deve estar ligado:
cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ifstream entrada(nome_do_ficheiro.c_str());

O estabelecimento de um canal de entrada pode falhar. Por exemplo, se o cheiro no existir, ou se estiver protegido para leitura. Se o estabelecimento do canal falhar durante a sua construo, este ca em estado de erro, sendo de novo muito fcil vericar essa situao:
cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ifstream entrada(nome_do_ficheiro.c_str());

416

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

if(not entrada) { cerr < < "Opps... No consegui ligar a ficheiro \"" < < nome_do_ficheiro < < "\"!" < < endl; ... }

Tal como para os canais de sada, tambm possvel estabelecer e encerrar um canal usando as operaes ofstream::open(), que recebe o nome do cheiro como argumento, e ofstream::close(), que no tem argumentos. A partir do momento em que um canal de entrada est estabelecido, pode-se us-lo da mesma forma que ao canal cin. Por exemplo:
cout < < "Diga o nome do ficheiro: "; string nome_do_ficheiro; cin > > nome_do_ficheiro; ofstream entrada(nome_do_ficheiro.c_str()); if(not entrada) { cerr < < "Opps... No consegui ligar a ficheiro \"" < < nome_do_ficheiro < < "\"!" < < endl; ... } Racional r; entrada > > r;

Se num mesmo programa se escreve e l de um mesmo cheiro, possvel usar canais que permitem simultaneamente inseres e extraces. Porm, a forma mais simples alternar escritas e leituras no mesmo cheiro usando canais diferentes, desde que se garanta que todos os dados inseridos no respectivo canal de sada foram j escritos no respectivo cheiro antes de os tentar extrair a partir de um canal de entrada ligado ao mesmo cheiro. Repare-se nas linhas nais do teste de unidade do TAD Racional:
... Racional r1(2, -6); ... Racional r2(3); ...

7.15. AMIZADES E PROMISCUIDADES


ofstream sada("teste"); sada < < r1 < < < < r2; sada.close(); // S o fecho explcito garante que a extraco do canal entrada tem sucesso. ifstream entrada("teste"); Racional r4, r5; entrada > > r4 > > r5; ...

417

7.15 Amizades e promiscuidades


7.15.1 Rotinas amigas
H casos em que pode ser conveniente denir rotinas normais (i.e., no-membro) que tenham acesso aos membros privados de uma classe C++. Por exemplo, suponha-se que se pretendia denir a rotina operator> > para a classe C++ Racional, como se fez mais atrs, mas sem delegar o trabalho na operao Racional::extraiDe(). Nesse caso podia-se tentar denir a rotina como:
void Racional::extraiDe(istream& entrada) { } ... /** Extrai do canal um novo valor para o racional, na forma de uma fraco. @pre r = r. @post Se entrada e entrada contm n /d (ou apenas n , assumindo-se d = 1), em que n e d so inteiros com d = 0, ento r = n entrada, d seno r = r entrada. */ istream& operator> >(istream& entrada, Racional& r) { assert(r.cumpreInvariante()); int n; int d = 1; if(entrada > > n) { if(entrada.peek() != /) {

418

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


r.numerador_ = n; r.denominador_ = 1; } else { if(entrada.get() and isdigit(entrada.peek()) and entrada > > d and d != 0) { r.numerador_ = d < 0 ? -n : n; r.denominador_ = d < 0 ? -d : d; reduz(); } else if(entrada) entrada.setstate(ios_base::failbit); } } assert(r.cumpreInvariante()); assert(r.numerador_ * d == n * r.denominador_ or not canal); return entrada; }

Como a rotina denida no membro da class C++ Racional, no tem acesso aos seus membros privados. Para resolver este problema pode-se usar o conceito de amizade: se a classe C++ Racional declarar que amiga, e por isso cona, nesta rotina, ela passa a ter acesso total s suas partes ntimas. Para o conseguir basta colocar o cabealho da rotina em qualquer ponto da denio da classe C++25 , precedendo-a do qualicador friend:
... class Racional { public: ... private: ... friend istream& operator> >(istream& entrada, Racional& r); }; ...
Para o compilador irrelevante se a declarao de amizade feita na parte pblica ou na parte privada da classe. No entanto, sendo as amizades uma questo de implementao, desejvel colocar a declarao na parte privada da classe, tipicamente no seu nal.
25

7.15. AMIZADES E PROMISCUIDADES

419

Qualquer rotina no-membro que faa uso de uma classe C++ conceptualmente parte das operaes que concretizam o comportamento desejado para o correspondente TAD. No entanto, se uma rotina no-membro no for amiga da classe C++, embora seja parte da implementao do TAD, no parte da implementao da correspondente classe C++. Isso deve-se ao facto de no ter acesso s suas partes privadas, e por isso no ter nenhuma forma directa de afectar o estado das suas instncias. Porm, a partir do momento em que uma rotina se torna amiga de uma classe C++, passa a fazer parte da implementao dessa classe. por isso que a rotina operator> > denida acima se preocupa com o cumprimento da condio invariante de classe: se pode afectar directamente o estado das instncias da classe bom que assim seja. que o produtor de uma rotina no-membro amiga de uma classe deve ser visto como produtor da classe C++ respectiva, enquanto o produtor de uma rotina no-membro e no-amiga que faa uso de uma classe no pode ser visto como produtor dessa classe C++, mas sim como seu consumidor.

7.15.2 Classes amigas


Da mesma forma que no caso das rotinas, tambm se podem declarar classes inteiras como amigas de uma classe C++. Nesse caso todos os mtodos da classe declarada como amiga tm acesso s partes privadas da classe C++ que declarou a amizade. Este tipo de amizade pode ser muito til em algumas circunstncias, como se ver no Captulo 10, embora em geral sejam de evitar. A sintaxe da declarao de amizade de classes semelhante usada para as rotinas. Por exemplo,
class B { ... }; class A { public: ... private: ... // Declarao da classe C++ B como amiga da classe A. friend class B; };

7.15.3 Promiscuidades
Em que casos devem ser usadas rotinas ou classes amigas de uma classe C++? A regra geral o mnimo possvel. Se se puder evitar usar rotinas e classes amigas tanto melhor, pois evita-se introduzir uma excepo regra de que s os membros de uma classe C++ tm acesso

420

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

suas partes privadas e regra de que a implementao de uma classe corresponde s suas partes privadas e aos respectivos mtodos. Todas as excepes so potencialmente geradoras de erros. Como mnemnica do perigo das amizades em C++, ca a frase amizades normalmente trazem promiscuidades. Assim, e respeitando a regra geral enunciada, manter-se- a implementao da rotina operator> > custa da operao Racional::extraiDe().

7.16 Cdigo completo do TAD Racional


O TAD Racional foi concretizado na forma de uma classe C++ com o mesmo nome e de rotinas (no-membro) associadas. Conceptualmente o TAD denido pelas respectivas operaes, pelo que a classe C++ no suciente para o concretizar: faltam as rotinas associadas, que no so membro da classe. Ou seja, as rotinas que operam com racionais fazem logicamente parte do TAD, mesmo no pertencendo classe C++ que o concretiza. Note-se que se concentraram as declaraes das rotinas no incio do cdigo, pois fazem logicamente parte da interface do TAD, tendo-se colocado a respectiva denio no nal do cdigo, junto com a restante implementao do TAD. Esta organizao ser til aquando da organizao do cdigo em mdulos fsicos, ver 9, de modo a permitir a fcil utilizao do TAD desenvolvido em qualquer programa.
#include <iostream> #include <cctype> #include <cassert> using namespace std; /** Devolve o mximo divisor comum dos inteiros passados como argumento. @pre V. mdc(m, n) m = 0 n = 0 . */ @post mdc = 1 m=0n=0 int mdc(int const m, int const n); /** Representa nmeros racionais. @invariant 0 < denominador_ mdc(numerador_, denominador_) = 1. */ class Racional { public: /** Constri racional com valor inteiro. Construtor por omisso. @pre V. @post *this = n. */ Racional(int const n = 0); /** Constri racional correspondente a n/d. @pre d = 0. @post *this = n . */ d Racional(int const n, int const d);

7.16. CDIGO COMPLETO DO TAD RACIONAL

421

/** Devolve numerador da fraco cannica correspondente ao racional. @pre V. numerador @post denominador() = *this. */ int numerador() const; /** Devolve denominador da fraco cannica correspondente ao racional. @pre V.

n @post E n : V : denominador = *this 0 < denominador mdc(n, denominador) = 1 . */ int denominador() const;

/** Devolve verso constante do racional. @pre V. @post operator+ *this. */ Racional const& operator+() const; /** Devolve simtrico do racional. @pre V. @post operator- = *this. */ Racional const operator-() const; /** Insere o racional no canal no formato de uma fraco. @pre V. @post sada sada contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional *this. */ d void insereEm(ostream& sada) const; /** Extrai do canal um novo valor para o racional, na forma de uma fraco. @pre *this = r. @post Se entrada e entrada contm n /d (ou apenas n , assumindose d = 1), em que n e d so inteiros com d = 0, ento *this = n entrada, d seno *this = r entrada. */ void extraiDe(istream& entrada); /** Adiciona de um racional. @pre *this = r. @post operator+= *this *this = r + r2. */ Racional& operator+=(Racional const& r2); /** Subtrai de um racional. @pre *this = r. @post operator-= *this *this = r r2. */ Racional& operator-=(Racional const& r2);

422

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

/** Multiplica por um racional. @pre *this = r. @post operator*= *this *this = r r2. */ Racional& operator*=(Racional const& r2); /** Divide por um racional. @pre *this = r r2 = 0. @post operator/= *this *this = r/r2. */ Racional& operator/=(Racional const& r2); /** Incrementa e devolve o racional. @pre *this = r. @post operador++ *this *this = r + 1. */ Racional& operator++(); /** Decrementa e devolve o racional. @pre *this = r. @post operador- *this *this = r 1. */ Racional& operator--(); private: int numerador_; int denominador_; /** Reduz a fraco que representa o racional. @pre denominador_ = 0 *this = r. @post denominador_ = 0 mdc(numerador_, denominador_) = 1 *this = r. */ void reduz(); /** Indica se a condio invariante de classe se verica. @pre V. @post cumpreInvariante = (0 < denominador_ mdc(numerador_, denominador_) = 1). */ bool cumpreInvariante() const; }; /** Adio de dois racionais. @pre V. @post operator+ = r1 + r2. */ Racional const operator+(Racional const r1, Racional const& r2); /** Subtraco de dois racionais. @pre V. @post operator- = r1 r2. */ Racional const operator-(Racional const r1, Racional const& r2);

7.16. CDIGO COMPLETO DO TAD RACIONAL

423

/** Produto de dois racionais. @pre V. @post operator* = r1 r2. */ Racional const operator*(Racional const r1, Racional const& r2); /** Diviso de dois racionais. @pre r2 = 0. @post operator/ = r1/r2. */ Racional const operator/(Racional const r1, Racional const& r2); /** Incrementa o racional recebido como argumento, devolvendo o seu valor antes de incrementado. @pre *this = r. @post operator++ = r *this = r + 1. */ Racional const operator++(Racional& r, int); /** Decrementa o racional recebido como argumento, devolvendo o seu valor antes de decrementado. @pre *this = r. @post operator- = r *this = r 1. */ Racional const operator--(Racional& r, int); /** Indica se dois racionais so iguais. @pre V. @post operator== = (r1 = r2). */ bool operator==(Racional const& r1, Racional const& r2); /** Indica se dois racionais so diferentes. @pre V. @post operator== = (r1 = r2). */ bool operator!=(Racional const& r1, Racional const& r2); /** Indica se o primeiro racional menor que o segundo. @pre V. @post operator< = (r1 < r2). */ bool operator<(Racional const& r1, Racional const& r2); /** Indica se o primeiro racional maior que o segundo. @pre V. @post operator> = (r1 > r2). */ bool operator>(Racional const& r1, Racional const& r2); /** Indica se o primeiro racional menor ou igual ao segundo. @pre V. @post operator<= = (r1 r2). */

424

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


bool operator<=(Racional const& r1, Racional const& r2); /** Indica se o primeiro racional maior ou igual ao segundo. @pre V. @post operator>= = (r1 r2). */ bool operator>=(Racional const& r1, Racional const& r2); /** Insere o racional no canal de sada no formato de uma fraco. @pre V. @post sada sada contm n/d (ou simplesmente n se d = 1) sendo n a fraco cannica correspondente ao racional r. */ d

ostream& operator< <(ostream& sada, Racional const& r);


/** Extrai do canal um novo valor para o racional, na forma de uma fraco. @pre r = r. @post Se entrada e entrada contm n /d (ou apenas n , assumindose d = 1), em que n e d so inteiros com d = 0, ento r = n entrada, d seno r = r entrada. */

istream& operator> >(istream& entrada, Racional& r);

int mdc(int m, int n) { if(m == 0 and n == 0) return 1; if(m < 0) m = -m; if(n < 0) n = -n; while(true) { if(m == 0) return n; n = n % m; if(n == 0) return m; m = m % n; } } inline Racional::Racional(int const n) : numerador_(n), denominador_(1) {

7.16. CDIGO COMPLETO DO TAD RACIONAL

425

assert(cumpreInvariante()); assert(numerador_ == n * denominador_); } inline Racional::Racional(int const n, int const d) : numerador_(d < 0 ? -n : n), denominador_(d < 0 ? -d : d) { assert(d != 0); reduz(); assert(cumpreInvariante()); assert(numerador_ * d == n * denominador_); } inline int Racional::numerador() const { assert(cumpreInvariante()); return numerador_; } inline int Racional::denominador() const { assert(cumpreInvariante()); return denominador_; } inline Racional const& Racional::operator+() const { assert(cumpreInvariante()); return *this; } inline Racional const Racional::operator-() const { assert(cumpreInvariante()); Racional r; r.numerador_ = -numerador_; r.denominador_ = denominador_;

426

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


assert(r.cumpreInvariante()); return r; } inline void Racional::insereEm(ostream& sada) const { assert(cumpreInvariante()); sada < < numerador_; if(denominador_ != 1) sada < < / < < denominador_; } void Racional::extraiDe(istream& entrada) { assert(cumpreInvariante()); int n; int d = 1; if(entrada > > n) { if(entrada.peek() != /) { numerador_ = n; denominador_ = 1; } else { if(entrada.get() and isdigit(entrada.peek()) and entrada > > d and d != 0) { numerador_ = d < 0 ? -n : n; denominador_ = d < 0 ? -d : d; reduz(); } else if(entrada) entrada.setstate(ios_base::failbit); } } assert(cumpreInvariante()); assert(numerador_ * d == n * denominador_ or not entrada); } Racional& Racional::operator+=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante());

7.16. CDIGO COMPLETO DO TAD RACIONAL

427

int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); // Devido a r += r: int n2 = r2.numerador_; int d2 = r2.denominador_; numerador_ /= dn; denominador_ /= dd; numerador_ = numerador_ * (d2 / dd) + n2 / dn * denominador_; dd = mdc(numerador_, dd); numerador_ = dn * (numerador_ / dd); denominador_ *= d2 / dd; assert(cumpreInvariante()); return *this; } Racional& Racional::operator-=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); // Devido a r += r: int n2 = r2.numerador_; int d2 = r2.denominador_; numerador_ /= dn; denominador_ /= dd; numerador_ = numerador_ * (d2 / dd) - n2 / dn * denominador_; dd = mdc(numerador_, dd); numerador_ = dn * (numerador_ / dd); denominador_ *= d2 / dd; assert(cumpreInvariante());

428

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


return *this; } inline Racional& Racional::operator*=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); int n1d2 = mdc(numerador_, r2.denominador_); int n2d1 = mdc(r2.numerador_, denominador_); numerador_ = (numerador_ / n1d2) * (r2.numerador_ / n2d1); denominador_ = (denominador_ / n2d1) * (r2.denominador_ / n1d2); assert(cumpreInvariante()); return *this; } inline Racional& Racional::operator/=(Racional const& r2) { assert(cumpreInvariante() and r2.cumpreInvariante()); assert(r2 != 0); int dn = mdc(numerador_, r2.numerador_); int dd = mdc(denominador_, r2.denominador_); if(r2.numerador_ numerador_ = denominador_ } else { numerador_ = denominador_ } < 0) { (numerador_ / dn) * (-r2.denominador_ / dd); = (denominador_ / dd) * (-r2.numerador_ / dn); (numerador_ / dn) * (r2.denominador_ / dd); = (denominador_ / dd) * (r2.numerador_ / dn);

assert(cumpreInvariante()); return *this; } inline Racional& Racional::operator++() { assert(cumpreInvariante()); numerador_ += denominador_;

7.16. CDIGO COMPLETO DO TAD RACIONAL


assert(cumpreInvariante()); return *this; } inline Racional& Racional::operator--() { assert(cumpreInvariante()); numerador_ -= denominador_; assert(cumpreInvariante()); return *this; } inline void Racional::reduz() { assert(denominador_ != 0); int k = mdc(numerador_, denominador_); numerador_ /= k; denominador_ /= k;

429

assert(denominador_ != 0 and mdc(numerador_, denominador_) == 1); } inline bool Racional::cumpreInvariante() const { return 0 < denominador_ and mdc(numerador_, denominador_) == 1; } inline Racional const operator+(Racional r1, Racional const& r2) { return r1 += r2; } inline Racional const operator-(Racional r1, Racional const& r2) { return r1 -= r2; } inline Racional const operator*(Racional r1, Racional const& r2) { return r1 *= r2;

430
}

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

inline Racional const operator/(Racional r1, Racional const& r2) { assert(r2 != 0); return r1 /= r2; } inline Racional const operator++(Racional& r, int) { Racional const cpia = r; ++r; return cpia; } inline Racional const operator--(Racional& r, int) { Racional const cpia = r; --r; return cpia; } inline bool operator==(Racional const& r1, Racional const& r2) { return r1.numerador() == r2.numerador() and r1.denominador() == r2.denominador(); } inline bool operator!=(Racional const& r1, Racional const& r2) { return not (r1 == r2); } inline bool operator<(Racional const& r1, Racional const& r2) { int dn = mdc(r1.numerador(), r2.numerador()); int dd = mdc(r1.denominador(), r2.denominador()); return (r1.numerador() / dn) * (r2.denominador() / dd) < (r2.numerador() / dn) * (r1.denominador() / dd); }

7.16. CDIGO COMPLETO DO TAD RACIONAL

431

inline bool operator>(Racional const& r1, Racional const& r2) { return r2 < r1; } inline bool operator<=(Racional const& r1, Racional const& r2) { return not (r2 < r1); } inline bool operator>=(Racional const& r1, Racional const& r2) { return not (r1 < r2); }

ostream& operator< <(ostream& sada, Racional const& r) { r.insereEm(sada); return sada; } istream& operator> >(istream& entrada, Racional& r) { r.extraiDe(entrada); return entrada; }
#ifdef TESTE #include <fstream> /** Programa de teste do TAD Racional e da funo mdc(). */ int main() { assert(mdc(0, 0) == 1); assert(mdc(10, 0) == 10); assert(mdc(0, 10) == 10); assert(mdc(10, 10) == 10); assert(mdc(3, 7) == 1); assert(mdc(8, 6) == 2); assert(mdc(-8, 6) == 2); assert(mdc(8, -6) == 2); assert(mdc(-8, -6) == 2);

432

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Racional r1(2, -6); assert(r1.numerador() == -1 and r1.denominador() == 3); Racional r2(3); assert(r2.numerador() == 3 and r2.denominador() == 1); Racional r3; assert(r3.numerador() == 0 and r2.denominador() == 1); assert(r2 == 3); assert(3 == r2); assert(r3 == 0); assert(0 == r3); assert(r1 assert(r2 assert(r1 assert(r2 assert(r1 assert(r2 < r2); > r1); <= r2); >= r1); <= r1); >= r2);

assert(r2 == +r2); assert(-r1 == Racional(1, 3)); assert(++r1 == Racional(2, 3)); assert(r1 == Racional(2, 3)); assert(r1++ == Racional(2, 3)); assert(r1 == Racional(5, 3)); assert((r1 *= Racional(7, 20)) == Racional(7, 12)); assert((r1 /= Racional(3, 4)) == Racional(7, 9)); assert((r1 += Racional(11, 6)) == Racional(47, 18)); assert((r1 -= Racional(2, 18)) == Racional(5, 2)); assert(r1 + r2 == Racional(11, 2)); assert(r1 - Racional(5, 7) == Racional(25, 14)); assert(r1 * 40 == 100); assert(30 / r1 == 12); ofstream sada("teste"); sada < < r1 < < < < r2; sada.close();

7.16. CDIGO COMPLETO DO TAD RACIONAL


ifstream entrada("teste"); Racional r4, r5; entrada > > r4 > > r5; assert(r1 == r4); assert(r2 == r5); } #else // TESTE int main() { // Ler fraces: cout < < "Introduza duas fraces (numerador denominador): "; Racional r1, r2; cin > > r1 > > r2; if(not cin) { cerr < < "Opps! return 1; }

433

A leitura dos racionais falhou!" < < endl;

// Calcular racional soma: Racional r = r1 + r2; // Escrever resultado: cout < < "A soma de " < < r1 < < " com " < < r2 < < " " < < r < < . < < endl; } #endif // TESTE

O cdigo acima representa o resultado nal de um longo priplo, comeado no incio deste captulo. Ser que valeu a pena o esforo posto no desenvolvimento do TAD Racional? Depende. Se o objectivo era simplesmente somar duas fraces, certamente que no. Se o objectivo, por outro lado, era permitir a escrita simples de programas usando nmeros racionais, ento valeu a pena. Em geral, quanto mais esforo for dispendido pelo programador produtor no desenvolvimento de uma mdulo, menos esforo ser exigido do programador consumidor. No entanto, no se deve nunca perder o pragmatismo de vista e desenhar um mdulo tentando prever todas as suas possveis utilizaes. Em primeiro lugar porque essas previses provavelmente falharo, conduzindo a cdigo intil, e em segundo porque todo esse cdigo intil ir servir apenas como fonte de complexidade e erros desnecessrios. Neste caso, no entanto, o negcio foi bom: o desenvolvimento serviu no apenas para fazer surgir naturalmente muitas particularidades associadas ao desenvolvimento de TAD em C++, servindo assim como ferramenta pedaggica, como tambm redundou numa classe que realmente til 26 , ou que
26

de tal forma assim que se est a estudar a incluso de uma classe genrica semelhante na biblioteca padro

434

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

pelo menos est no bom caminho para o vir a ser.

7.17 Outros assuntos acerca de classes C++


Ao longo deste captulo, e a pretexto do desenvolvimento de um TAD Racional, introduziramse muitos conceitos importantes relativos s classes C++. Nenhum exemplo prtico esgota todos os assuntos, pelo que se reservou esta seco nal para introduzir alguns conceitos adicionais sobre classes C++.

7.17.1 Constantes membro


Suponha que se pretende denir uma classe C++ para representar vectores num espao de dimenso xa e nita. De modo a ser fcil alterar a dimenso do espao onde se encontra o vector sem grande esforo, tentador denir um atributo constante para representar a dimenso desse espao. Depois, as coordenadas do vector poderiam ser guardadas numa matriz clssica do C++, membro da classe, com a dimenso dada. Ou seja:
class Vector { public: int const dimenso = 3; // erro! ... private: double coordenadas[dimenso]; ... };

Infelizmente, no possvel denir e inicializar uma constante dentro de uma classe. A razo para a proibio simples. Sendo dimenso uma constante membro (de instncia), cada instncia da classe C++ possuir a sua prpria cpia dessa constante. Mas isso signica que, para ser verdadeiramente til, essa constante dever poder tomar valores diferentes para cada instncia da classe C++, i.e., para cada varivel dessa classe C++ que seja construda. Da que no se possam inicializar atributos constantes (de instncia) na sua prpria denio. Seria possvel, por exemplo, inicializar a constante nos construtores da classe:
class Vector { public: int const dimenso; Vector(); ...
do C++ (ver http://www.boost.org).

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++

435

private: double coordenadas[dimenso]; ... }; Vector::Vector() : dimenso(3), ... { ... }

As listas de inicializadores so extremamente teis, sendo utilizadas para inicializar no s atributos constantes, como tambm atributos que sejam referncias e atributos que sejam de uma classe sem construtor por omisso, i.e., que exijam uma inicializao explcita. Mas neste caso a soluo a que chegmos no a mais adequada. Se todos os vectores devem ter a mesma dimenso, porqu fornecer cada instncia da classe C++ com a sua prpria constante dimenso? O ideal seria partilhar essa constante entre todas as instncias da classe.

7.17.2 Membros de classe


At agora viu-se apenas como denir membros de instncia, i.e., membros dos quais cada instncia da classe C++ possui uma verso prpria. Como fazer para denir um atributo do qual exista apenas uma verso, comum a todas as instncias da classe C++? A soluo simples: basta declarar o atributo com sendo um atributo de classe e no de instncia. Isso consegue-se precedendo a sua declarao do qualicador static:
class Vector { public: static int const dimenso; Vector(); ... private: double coordenadas[dimenso]; // Opps... No funciona... ... };

Um atributo de classe no pode ser inicializado nas listas de inicializadores dos construtores. Nem faria qualquer sentido, pois um atributo de classe tm apenas uma instncia, partilhada entre todas as instncias da classe de que membro, no fazendo sentido ser inicializada seno uma vez, antes do programa comear. Assim, necessrio denir esse atributo fora da classe, sendo durante essa denio que se procede respectiva inicializao:

436

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++


int const Vector::dimenso = 3;

Infelizmente esta soluo no compila correctamente. que em C++ s se pode especicar a dimenso de uma matriz clssica usando um constante com valor conhecido pelo compilador. Como um atributo de classe s inicializado na sua denio, que est fora da classe, o atributo dimenso inicializado tarde demais: o compilador engasga-se na denio da matriz membro, dizendo que no sabe o valor da constante. Este problema no , em geral, resolvel, excepto quando o atributo de classe em denio for de um tipo bsico inteiro do C++. Nesse caso possvel fazer a denio (como inicializao) dentro da prpria classe:
class Vector { public: static int const dimenso = 3; Vector(); ... private: double coordenadas[dimenso]; // Ah! J funciona... ... };

onde a denio externa classe deixa de ser necessria. Da mesma forma, possvel declarar operaes da classe C++ que no precisam de ser invocadas atravs de nenhuma instncia: so as chamadas operaes de classe. Suponha-se, por absurdo, que se queria que a classe C++ Vector mantivesse uma contagem do nmero de instncias de si prpria existente em cada instante. Uma possibilidade seria:
class Vector { public: static int const dimenso = 3; Vector(); static int nmeroDeInstncias(); ... private: double coordenadas[dimenso]; // Ah! J funciona...

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++


static int nmero_de_instncias; ... }; Vector::Vector() : ... { ... ++nmero_de_instncias; } inline int Vector::nmeroDeInstncias() { return nmero_de_instncias; } int Vector::nmero_de_instncias = 0;

437

Os membros de classe (e no de instncia) so precedidos do qualicador static aquando da sua declarao dentro da denio da classe. O atributo de classe nmero_de_instncias declarado durante a denio da classe e s denido (i.e., construda de facto com o valor inicial 0) depois da classe C++, tal como acontece com as rotinas membro. O contador de instncias acima tem dois problemas. O primeiro que a contabilizao falha se algum vector for construdo por cpia a partir de outro vector: Vector v1; // Ok, incrementa contador. Vector v2(v1); // Erro! No incrementa contador. Para j ignorar-se- este problema, at porque s se falar do fornecimento explcito de construtores por cpia, que substituem o construtor por cpia fornecido implicitamente pela linguagem, num captulo posterior. O segundo problema que a contabilizao falha quando se destri alguma instncia da classe.

7.17.3 Destrutores
Da mesma forma que os construtores de uma classe C++ so usados para inicializar as suas instncias quando estas so construdas, podem-se usar destrutores, i.e., cdigo que deve executado quando a instncia destruda, para arrumar a casa no nal do tempo de vida das instncias de uma classe C++. Os destrutores so extremamente teis, particularmente quando se utilizam variveis dinmicas ou, em geral, quando os construtores da classe C++ reservam

438

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

algum recurso externo para uso exclusivo da instncia construda. Utilizaes mais interessantes do conceito ver-se-o mais tarde, bastando para j apresentar um exemplo ingnuo da sua utilizao no mbito da classe C++ Vector. Os destrutores declaram-se e denem-se como os construtores, excepto que se coloca o smbolo ~ antes do seu nome (que tambm o nome da classe):
class Vector { public: static int const dimenso = 3; Vector(); ~Vector(); static int nmeroDeInstncias(); ... private: double coordenadas[dimenso]; // Ah! J funciona... static int nmero_de_instncias; ... }; Vector::Vector() : ... { ... ++nmero_de_instncias; } Vector::~Vector() { --nmero_de_instncias; ... } inline int Vector::nmeroDeInstncias() { return nmero_de_instncias; }

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++


int Vector::nmero_de_instncias = 0;

439

Note-se que a linguagem fornece implicitamente um destrutor para as classes denidas sempre que este no seja denido explicitamente pelo programador fabricante.

7.17.4 De novo os membros de classe


Suponha-se o seguinte cdigo usando a classe desenvolvida:
Vector a; int main() { { Vector b; for(int i = 0; i != 3; ++i) { Vector c; cout < < "Existem " < < Vector::nmeroDeInstncias() < < " instncias." < < endl; static Vector d; } cout < < "Existem " < < Vector::nmeroDeInstncias() < < " instncias." < < endl; } cout < < "Existem " < < C::nmeroDeInstncias() < < " instncias." < < endl; }

de notar que a invocao da operao de classe C++ Vector::nmeroDeInstncias() faz-se no atravs do operador de seleco de membro ., o que implicaria a invocao da operao atravs de uma qualquer instncia da classe C++, mas atravs do operador de resoluo de mbito ::. No entanto, tambm possvel, se bem que intil, invocar operaes de classe atravs do operador de seleco de membro. As mesmas observaes se podem fazer no que diz respeito aos atributos de classe. A execuo do programa acima teria como resultado:
Existem Existem Existem Existem Existem 3 4 4 3 2 instncias. instncias. instncias. instncias. instncias.

440

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

De aqui em diante utilizar-se- a expresso membro como signicando membro de instncia, i.e., membros dos quais cada instncia da classe C++ a que pertencem possui uma cpia prpria, usando-se sempre a expresso membro de classe para os membros partilhados entre todas as instncias da classe C++. Para que o resultado do programa acima seja claro, necessrio recordar que: instncias automticas (i.e., variveis e constantes locais sem o qualicador static) so construdas quando a instruo da sua denio executada e destrudas quando o bloco de instrues na qual foram denidas termina; instncias estticas globais so construdas antes de o programa comear e destrudas depois do seu nal (depois de terminada a funo main()); e instncias estticas locais so construdas quando a instruo da sua denio executada pela primeira vez e destrudas depois do nal do programa (depois de terminada a funo main()).

7.17.5 Construtores por omisso


Todas as classes C++ tm construtores. A um construtor que possa ser invocado sem qualquer argumento (porque no tem qualquer parmetro ou porque todos os parmetros tm valores por omisso) chama-se construtor por omisso. Se o programador no declarar qualquer construtor, fornecido implicitamente, sempre que possvel, um construtor por omisso. Por exemplo, no cdigo
class C { private: Racional r1; Racional r2; int i; int j; }; C c; // nova instncia, construtor por omisso invocado.

fornecido implicitamente pelo compilador um construtor por omisso para a classe C++ C. Este construtor invoca os construtores por omisso de todos os atributos, com excepo dos pertencentes a tipos bsicos do C++ que, infelizmente, no so inicializados implicitamente. Neste caso, portanto, o construtor por omisso da classe C constri os atributos r1 r r2 com o valor racional zero, deixando os tributos i e j por inicializar. O construtor por omisso fornecido implicitamente pode ser invocado explicitamente,
C c = C(); // ou C c(C());

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++

441

embora neste caso seja tambm invocado o construtor por cpia, tambm fornecido implicitamente, que constri a varivel c custa da instncia temporria construda pelo construtor por omisso. Para que esse facto que claro, repare-se no cdigo
cout < < Racional() < < endl;

que mostra no ecr o valor da instncia temporria da classe C++ Racional construda pelo respectivo construtor por omisso, i.e., o valor zero. Neste caso o construtor por cpia, da classe C++ Racional, no invocado. Se o programador declarar algum construtor explicitamente, ento o construtor por omisso deixa de ser fornecido implicitamente. Por exemplo, se a classe C++ C fosse
class C { public: C(int i, int j); private: Racional r1; Racional r2; int i; int j; };

o cdigo
C c; // ops... falta o construtor por omisso!

resultaria num erro de compilao. No exemplo seguinte, o construtor por omisso faz exactamente o mesmo papel que o construtor por omisso fornecido implicitamente para a classe C++ C no exemplo original:
class C { public: C(); private: Racional r1; Racional r2; int i; int j; }; C::C() : r1(), r2() { }

442

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Sempre no sejam colocados na lista de inicializao de um construtor, os atributos que no sejam de tipos bsicos do C++ so inicializados implicitamente atravs do respectivo construtor por omisso (se ele existir, bem entendido). Assim, o construtor no exemplo acima pode-se simplicar para:
C::C() { }

Antes de ser executado o corpo do construtor envolvido numa construo, todos os atributos da classe C++ so construdos pela ordem da sua denio na classe, sendo passados aos construtores os argumentos indicados na lista de inicializadores entre parnteses aps o nome do atributo, se existirem, ou os construtores por omisso na falta do nome do atributo na lista, com excepo dos atributos de tipos bsicos do C++, que tm de ser inicializados explicitamente, caso contrrio cam por inicializar. Por exemplo, no cdigo
class C { public: C(int n, int d, int i); private: Racional r1; Racional r2; int i; int j; }; C::C(int const n, int const d, int const i) : r1(n, d), i(i), j() { r2 = 3; } C c(2, 10, 1);

ao ser construda a varivel c invocado o seu construtor, o que resultar nas seguintes operaes: 1. Construo de r1 por invocao do construtor da classe C++ Racional com argumen1 tos n e d (que neste caso inicializa r1 com 5 ). 2. Construo de r2 por invocao implcita do construtor da classe C++ Racional que pode ser invocado sem argumentos (que inicializa r2 com o racional zero ou 0 ). 1 3. Construo do atributo i a partir do parmetro i (que neste caso ca com 1). usado o construtor por cpia dos int.

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++

443

4. Construo do atributo j atravs do construtor por omisso dos int, que necessrio invocar explicitamente (e que inicializa j com o valor zero). 5. Execuo do corpo do construtor da classe C++ C: (a) Converso do valor literal 3 de int para Racional. (b) Atribuio do racional
3 1

a r2.

de notar que a varivel membro r2 construda e inicializada com o valor zero e s depois lhe atribudo o racional 3 , o que uma perda de tempo. Seria prefervel incluir r2 na lista de 1 inicializadores.

7.17.6 Matrizes de classe


possvel denir matrizes clssicas do C++ tendo como elementos valores de TAD, por exemplo da classe C++ Racional:
Racional m1[10]; Racional m2[10] = {1, 2, Racional(3), Racional(), Racional(1, 3)};

Os 10 elementos de m1 e os ltimos cinco elementos de m2 so construdos usando o construtor por omisso da classe Racional (que os inicializa com o racional zero). Os dois primeiros elementos da matriz m2 so inicializados a partir de inteiros implicitamente convertidos para racionais usando o construtor com um nico argumento da classe C++ Racional (i.e., o segundo construtor, usando-se um segundo argumento tendo valor por omisso um). Essa converso explicitada no caso do terceiro elemento de m2. J para o quarto e o quinto elementos, eles so construdos por cpia a partir de um racional construdo usando o construtor por omisso, no primeiro caso, e o construtor completo, com dois argumentos, no segundo caso. Note-se que se a classe C++ em causa no possuir construtores por omisso, obrigatrio inicializar todos os elementos da matriz explicitamente.

7.17.7 Converses para outros tipos


Viu-se j que ao se denir numa classe C++ um construtor passvel de ser invocado com um nico argumento, se fornece uma forma de converso implcita de tipo (ver Seco 7.9.2). Por exemplo, a classe C++ Racional fornece um construtor que pode ser invocado com um nico argumento inteiro, o que possibilita a converso implcita de tipo entre valores do tipo int e valores da classe Racional,
Racional r(1, 3); ... if(r < 1) // 1 convertido implicitamente de int para Racional.

444

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

sem haver necessidade de explicitar essa converso:


Racional r(1, 3); ... if(r < Racional(1)) // no necessrio...

E se se pretendesse equipar a classe C++ Racional com uma converso implcita desse tipo para double, i.e., se se pretendesse tornar o seguinte cdigo vlido?
Racional r(1, 2); double x = r; cout < < r < < < < x < < endl; // mostra: 1/2 0.5 cout < < double(r) < < endl; // mostra: 0.5

A soluo poderia passar por alterar a classe C++ double, de modo a ter um construtor que aceitasse um racional. O problema que nem o tipo double uma classe, nem, se por absurdo o fosse, estaria nas mos do programador alter-la. A soluo passa por alterar a classe C++ Racional indicando que se pode converter implicitamente um racional num double. De facto, a linguagem C++ permite faz-lo. possvel numa classe C++ denir uma converso implcita de um tipo para essa classe, mas tambm o contrrio: denir uma converso implcita da classe para um outro tipo. Isso consegue-se sobrecarregando um operador de converso para o tipo pretendido, neste caso double. Esse operador chama-se operator double. Este tipo de operadores, de converso de tipo, tm algumas particularidades: 1. S podem ser sobrecarregados atravs de rotinas membro da classe C++ a converter. 2. No incluem tipo de devoluo no seu cabealho, pois este indicado pelo prprio nome do operador. Ou seja, no caso em apreo
... class Racional { public: ... /** Devolve o racional visto como um double. @pre V. @post O valor devolvido uma aproximao to boa quanto possvel do racional representado por *this. */

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++


operator double() const; ... }; ...

445

Racional::operator double() const { return double(numerador()) / double(denominador()); } A diviso do numerador pelo denominador feita depois de ambos serem convertidos para double. De outra forma seria realizada a diviso inteira, cujo resultado no bem aquilo que se pretendia... O problema deste tipo de operadores de converso de tipo, que devem ser usados com moderao, que levam frequentemente a ambiguidades. Por exemplo, denido o operador de converso para double de valores da classe C++ Racional, como deve ser interpretado o seguinte cdigo?
Racional r(1,3); ... if(r == 1) ...

O compilador pode interpretar a guarda da instruo de seleco ou condicional como


double(r) == double(1)

ou como
r == Racional(1)

mas no sabe qual escolher. As regras do C++ para resolver este tipo de ambiguidades so algo complicadas (ver [12, Seco 7.4]) e no resolvem todos os casos. No exemplo dado o programa de facto ambguo e portanto resulta num erro de compilao. Como muito mais natural e frequente a converso implcita de um int num Racional do que a converso implcita de um Racional num double, a melhor soluo simplesmente no sobrecarregar o operador de converso para double.

446

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

7.17.8 Uma aplicao mais til das converses


H casos em que os operadores de converso podem ser muito teis. Suponha-se que se pretende denir um tipo Palavra que se comporta como um int em quase tudo, mas fornece algumas possibilidades adicionais, tais como acesso individualizado aos seus bits (ver Seco 2.7.4). O novo tipo poderia ser concretizado custa de uma classe C++:
class Palavra { public: Palavra(int const valor = 0); operator int() const; bool bit(int const n) const; private: int valor; }; inline Palavra::Palavra(int const valor) : valor(valor) { } inline Palavra::operator int() const { return valor; } inline bool Palavra::bit(int const n) const { return (valor & (1 < < n)) != 0; }

Esta denio tornaria possvel escrever


Palavra p = 99996; p = p + 4; std::cout < < "Valor : " < < p < < std::endl; std::cout < < "Em binrio: "; for(int i = 32; i != 0; --i) std::cout < < p.bit(i - 1); std::cout < < std::endl;

7.17. OUTROS ASSUNTOS ACERCA DE CLASSES C++


que resultaria em
Valor : 100000 Em binrio: 00000000000000011000011010100000

447

Esta classe C++, para ser verdadeiramente til, deveria proporcionar outras operaes, que cam como exerccio para o leitor.

448

CAPTULO 7. TIPOS ABSTRACTOS DE DADOS E CLASSES C++

Captulo 8

Programao baseada em objectos


!!!!!!Ateno! Colocar diagrama UML completo das classes nesse captulo! !!Falar de programao centrada nos dados. Preocupa-nos queobjectos existem no domnio do problema, que tipos de objectos so (ou a que classe pertencem), que operaes se podem realizar com eles e que relaes existem entre eles. !! Num captulo parte: Exemplo de desenho e utilizao de TAD. Aula 14. Exemplo da calculadora simples. Depois mudar de double para Racional. Acrescentar parnteses ou, alternativamente, deix-los como exerccio para o leitor (pode-se dar umas dicas). !!Fazer captulo sobre desenvolvimento. Aqui que deve vir para a parte sobre tipos abstractos de dados. Falar acerca do desenvolvimento. Identicao de classes, relaes entre classes, desenvolvimento incremental, teste antecipado, guardar testes! !! Inspectores/interrogaes: sempre funes e const. No h funes sem ser const? H, mas nesse caso no alteram directamente: devolvem referncia atravs da qual se pode alterar a varivel implcita (ver frente). Modicadores: procedimentos no const. Actuadores: procedimentos const (afectam globais ou parmetros por referncia). Modicadores/actuadores: e.g., l, afecta implcita e canal. Manpulos: funes no const que no mudam a varivel implcita mas devolvem referncia ou classe referncia que a permite alterar. Const: query ou interrogaes. !!Falar de nomes: classes = substantivos. Objectos reais ou conceptuais. Hmm... distinguir TAD de Classes? !!Colocar aqui exemplo dos mapas (calculadora mais sosticada?). Aproveitar para introduzir o conceito de mutable. Distinguir entre estado e estado observvel (estado: produtor, estado observvel: consumidor). !!!!!!! Incluir parnteses e vericao de erros? S parnteses? Erros transmitidos como? Como excepes? !!!!!!!!! Hoje vamos falar um pouco mais sobre desenho de TAD. Normalmente os TAD no surgem do nada. Surgem por serem teis para resolver um determinado problema. Vamos ento resolver um problema. Suponhamos que queremos implementar uma calculadora simples. Esta 449

450

CAPTULO 8. PROGRAMAO BASEADA EM OBJECTOS

calculadora deve suportar as operaes de adio, subtraco, multiplicao, diviso e potenciao sobre valores de vrgula utuante (usaremos double). Para simplicar vamos supor que os parnteses so proibidos. As expresses a calcular devem ser lidas do canal de entrada cin, ligado ao teclado, e os resultados devem ser escritos no canal de sada cout, ligado ao ecr. A calculadora pode calcular vrias expresses, pelo que se arbitra que estas so separadas umas das outras pelo smbolo ; (ponto-e-vrgula). A calculadora termina o clculo de expresses quando encontra o smbolo . (ponto). Por exemplo, se a entrada for: 2 + 2 ; 2 * 2.5 . a sada deve ser 4 5 Para percebermos melhor como resolver o problema, vamos tentar perceber como ns prprios efectuaramos os clculos caso tivssemos acesso a um smbolo de cada vez. Por smbolo quero dizer ou valores numricos ou os operadores permitidos. Seja ento a expresso 1 + 4 * 3 / 2 ^ 3 - 2 . Qualquer um de ns calcula rapidamente o resultado que ... Exacto: 0,5. E se s virmos um smbolo de cada vez. Que fazer? Discutir. Concluir que o melhor ir guardando os valores e os operadores! Mas onde? O.k., vamos passo a passo. Primeiro recebemos 1 Que fazer? Discutir. Depois recebemos + Que fazer? Discutir. Deixar a dica que talvez seja boa ideia usar um TAD para representar os operadores. Depois recebemos 4 Pode-se j calcular? Discutir. No! Porqu? Porque a seguir pode vir outro operador com maior precedncia! E se agora fosse + ? Discutir. Nesse caso podia-se logo calcular a primeira operao! O mesmo se a expresso at aqui fosse: 1 * 4 + ! Mas no ... Temos de ler mais um smbolo, que : * Calma l. Quantos valores j lemos? E quantos operadores? E onde os guardamos? Discutir, mesmo sem concluir. Depois vem 3 Pouco ajuda... E depois vem / E agora? Discutir! Qual a regra? Podem-se calcular todos os operadores em espera que tenham precedncia superior ou igual ao que acabmos de ler! Mas por que ordem? Discutir. Concluir que os clculos prosseguem pela ordem inversa da leitura! Ou seja, podemos empilhar as operaes! E os valores? Discutir. Uma pilha tambm serve para os valores! Exemplicar desde o incio ao m da expresso, desenhando a evoluo das pilhas. Explicar o que acontece no . (ponto). Ento j temos uma ideia da forma de clculo da nossa calculadora. Falta denirmos os TAD necessrios. Em primeiro lugar, vai-nos dar muito jeito um TAD para representar os operadores! Apresentar acetato! Explicar cuidadosamente a classe. Explicar o operator()(). Explicar o operador <: um operador menor que outro se tiver menos precedncia. Explicar membro de classe cuidadosamente! Dar exemplos de invocao! Em segundo lugar vai-nos ser til uma classe para representar a calculadora! Apresentar acetato. Explicar detalhadamente o seu funcionamento. Deixar claro que no se vericam nenhuns erros do utilizador! A inteno simplicar a resoluo. Fica como exerccio... Fazer um traado admitindo a mesma entrada. Depois de eles se terem convencido da bondade da soluo, avanar. Disse-vos que ia falar do desenho de TAD e na volta apresentei as classes C++ correspondentes j prontas e acabadas... Batotice... Bom, no inteiramente. Faltam as pilhas! Explicar que ainda no temos os tipos PilhaDeDouble nem PilhaDeOperador, mas que j temos um programa escrito que faz uso da classe! Esta uma boa forma de sabermos que operaes deve um TAD suportar: comear a resolver o problema assumindo que a classe existe! Vamos comear pela classe PilhaDeDouble. A classe PilhaDeOperador ser praticamente igual. Que operaes podemos realizar sobre uma pilha? Discutir. Concluir: Criar pilha vazia. Pr item na pilha. Altera a pilha. Obter o item do topo. No altera a pilha! Tirar o item do topo. Altera a pilha. Saber altura da pilha. No altera a pilha! Vericar se est vazia. No altera a pilha! Antes de avanarmos, no entanto, conveniente, alis, fundamental, fazermos aquilo que se chama um teste de unidade. Um teste de unidade, ou teste de mdulo, um pequeno programa que nos permite testar todas as operaes de um TAD, de modo a carmos seguros do seu bom

8.1. DESENHO DE CLASSES

451

funcionamento. ..... #ifdef TESTE int main() { PilhaDeDouble pilha; assert(pilha.estaVazia()); assert(pilha.altura() == 0); for(int i = 0; i != 10; ++i) { pilha.poe(i); assert(pilha.itemNoTopo() == i); } assert(not pilha.estaVazia()); assert(pilha.altura() == 10); for(int i = 10; i != -1; i) { assert(pilha.itemNoTopo() == i); pilha.tiraItem(); } assert(pilha.estaVazia()); assert(pilha.altura() == 0); } #endif Explicar bem as directivas de pr-processador, embora sem entrar em pormenores. Dizer que a funo main() s chega a ser compilada se o programa for compilado com um comando da forma: c++ -DTESTE ... Agora s ir denindo a interface da classe C++, incluindo documentao, mas deixando a implementao de lado. Discutir efeito de cada operao e circunstncias em que falha. /** Representa pilhas de double. @invariant Dizer que no se conhece por ser questo de implementao. */ class PilhaDeDouble { public: typedef double Item; /** Constri pilha vazia. @pre V. @post estaVazia(). */ PilhaDeDouble(); /** Devolve o item que est no topo da pilha. @pre estaVazia(). @post itemNoTopo idntico ao item no topo de *this. */ Item const& itemNoTopo() const; /** Indica se a pilha est vazia. @pre V. @post estaVazia = *this est vazia. */ bool estaVazia() const; /** Devolve altura da pilha. @pre V. @post altura = altura de *this. */ int altura() const; /** Pe um novo item na pilha (no topo). @pre V. @post *this contm um item adicional no topo igual a novo_item. */ void poe(Item const& novo_item); /** Tira o item que est no topo da pilha. @pre estaVazia(). @post *this contm os itens originais menos o do topo. */ void tiraItem(); private: ... }; ... Discutir cuidadosamente o typedef e sua utilidade! Com Item fcil alterar o tipo dos itens! O que fazer para denir a classe PilhaDeOperador? Discutir. O que que falta? A implementao! o menos importante! Faam-na vocs: ca como exerccio. Concluses: Os TAD surgem para auxiliar na resoluo de um problema. A resoluo deve ser usada para sugerir as operaes necessrias para o novo TAD. Deve-se comear por escrever o respectivo teste de unidade. S ento se desenha a interface da classe C++ que implementa o TAD. Finalmente passa-se implementao. O teste de unidade e a interface da classe podem ser desenvolvidos em paralelo, incrementalmente. Mas a implementao deve ser deixada para o m! Falar de modularizao: departamento de marketing + designers denem a interface (o que faz, como se opera, aspecto exterior), departamento de engenharia implementa mecanismo. Programao baseada em objectos: primeiro pensa-se que classes existem (substantivos no vocabulrio do problema). Depois pensa-se nos algoritmos. E no o contrrio: programao procedimental, em que primeiro se pensa nos algoritmos e s depois nos dados. Por vezes misturam-se os dois tipos de abordagem. Se houver tempo discutir um pouco a implementao. Dizer que as pilhas... J existem... #include <stack>... push(), pop(), size(), top(), empty(). Referir que no segundo semestre vamos aprender a escrever o cdigo da pilha uma vez apenas de modo a que seja vlido com qualquer tipo: classes modelo e programao genrica.

8.1 Desenho de classes


Como se desenha uma classe? Quando e porqu o fazer? No h respostas taxativas para estas questes, mas tentar-se- apresentar nesta seco um exemplo de resoluo de um problema que sirva para as claricar um pouco. Suponha-se que se pretende escrever um programa que leia 10 valores inteiros do teclado e os escreva pela ordem inversa no ecr. Um pouco de reexo sobre o problema revela que ele pode ser resolvido com matrizes C++. Mas, antes de aceitar a soluo como evidente, convm

452 pensar um pouco no problema.

CAPTULO 8. PROGRAMAO BASEADA EM OBJECTOS

Ponha-se no lugar do computador e admita que lhe so passadas 10 folhas de papel, com nmero escritos, e que as deve devolver pela ordem inversa da entrega. Como organizaria as folhas de papel? A resposta usual a esta pergunta que os papeis deveriam ser organizados numa pilha (com os nmeros voltados para cima) porque, numa pilha de papeis, o ltimo papel a entrar o primeiro a sair. No h equivalente a uma pilha de papeis na linguagem C++ (embora exista na biblioteca padro...). Surge ento a ideia de construir uma classe para acrescentar esse conceito ao C++. Mas resta uma dvida: sendo o problema to simples e facilmente resolvel usando matrizes directamente, porqu denir uma classe nova? Mais uma vez no existem respostas taxativas a esta questo. A questo deve ser analisada sob diversos pontos de vista e uma deciso tomada com base nos resultados dessa anlise. Por exemplo: 1.O desenvolvimento da nova classe exige substancialmente mais esforo que uma soluo ad hoc? 2.A nova classe pode vir a ser til no futuro, no mesmo ou noutros programas? 3.A modularizao inerente construo de uma classe vai trazer vantagens para o desenvolvimento e correco do programa? Neste caso, evidente que uma classe representando o conceito de pilha de inteiros pode vir a ser til no futuro, sobretudo se for facilmente adaptvel para representar pilhas de entidades de outro tipo tais como pilhas de char ou pilhas de Racional (mais tarde se ver como se podem construir classes genricas, ou classes modelo).

Captulo 9

Modularizao de alto nvel


A modularizao um assunto recorrente em programao. De acordo com [4]: mdulo1 . [Do lat. modulu.] S. m. [...] 4. Unidade (de mobilirio, de material de construo, etc.) planejada segundo determinadas propores e destinada a reunirse ou ajustar-se a outras unidades anlogas, de vrias maneiras, formando um todo homogneo e funcional. [...] Em programao os mdulos so tambm unidades que se podem reunir ou ajustar umas s outras de modo a formar um todo homogneo e funcional. Mas normalmente exige-se que os mdulos tenham algumas caractersticas adicionais: 1. Devem ter um nico objectivo bem denido. 2. Devem ser coesos, i.e., devem existir muitas ligaes dentro dos mdulos. 3. Devem ser fracamente ligados entre si, i.e., devem existir poucas ligaes entre os diferentes mdulos. 4. Devem separar claramente a sua interface da sua implementao, i.e., devem distinguir claramente entre o que deve estar visvel para o consumidor do mdulo e que deve estar escondido no seu interior. Ou seja, os mdulos devem respeitar o princpio do encapsulamento. No Captulo 3 introduziu-se pela primeira vez a noo de modularizao. As unidades bsicas de modularizao so as rotinas, apresentadas nesse captulo: 1. Tm um nico objectivo bem denido (que no caso das funes devolver um qualquer valor e no caso dos procedimentos fazer qualquer coisa). 2. So coesas, pois tipicamente, como tm um objectivo bem denido, podem ser expressos em poucas linhas de cdigo, todas interdependentes. comum, e a maior parte das vezes desejvel, que um mdulo utilize outros mdulos disponveis, ou seja, que uma rotina utilize outras rotinas para realizar parte do seu trabalho. 453

454

CAPTULO 9. MODULARIZAO DE ALTO NVEL

3. So fracamente ligadas. comum uma rotina usar outra rotina, o que estabelece uma ligao entre as duas, mas f-lo normalmente recorrendo apenas sua interface, invocandoa, pelo que as ligaes se reduzem ligao entre argumentos e parmetros, que desejavelmente so em pequeno nmero, e ao valor devolvido, no caso de uma funo. 4. Separam claramente interface de implementao. O cabealho de uma rotina contm (quase1 ) tudo aquilo que quem o utiliza precisa de saber, indicando o nome da rotina, o nmero e tipo dos parmetros e o tipo de devoluo, i.e., a sua interface. O corpo de uma rotina corresponde sua implementao, indicando como funciona. Existem vrios nveis de modularizao adicionais, que podem ser organizados hierarquicamente. Depois das rotinas, o nvel seguinte de modularizao corresponde s classes: 1. Tm um nico objectivo bem denido que o de representar um dado conceito, fsico ou abstracto. Por exemplo, uma classe Racional serve para representar o conceito de nmero racional, com as respectivas operaes, enquanto uma classe CircuitoLgico pode servir para representar o conceito de circuito lgico e respectivas operaes. 2. So coesas, pois servem para representar uma nica entidade, e portanto os seus mtodos esto totalmente interligados uns aos outros atravs dos atributos da classe e, muitas vezes, atravs de utilizaes mtuas. 3. Separam claramente interface de implementao. A interface corresponde aos membros pblicos da classe, que tipicamente no incluem atributos variveis. Das operaes pblicas, apenas fazem parte da interface os respectivos cabealhos. A implementao das operaes, ou seja, os mtodos, est inacessvel ao consumidor da classe 2 . Fazem parte da implementao de uma classe todos os membros privados (incluindo normalmente todos os atributos variveis) e todos os mtodos da classe. O nvel seguinte de modularizao o nvel fsico. porventura o nvel com pior suporte pela linguagem e que mais facilmente mal utilizado pelo programador inexperiente. A modularizao fsica corresponde diviso dos programas em cheiros e tem duas utilidades principais: fazer uma diviso lgica das ferramentas de um programa a um nvel superior ao das classes e, uma vez que um programa j no precisa de estar concentrado num nico cheiro, mas pode ser dividido por vrios cheiros, facilitar a reutilizao de cdigo em programas diferentes. Finalmente, a linguagem C++ fornece ainda o conceito de espao nominativo, que se pode fazer corresponder aproximadamente noo de pacote. Este o nvel mais alto de modularizao, e permite fazer uma diviso lgica das ferramentas de um programa a um nvel superior ao da modularizao fsica e, simultaneamente, evitar a coliso de nomes denidos
1 O cabealho de uma rotina diz como ela se utiliza, embora no diga o que faz ou calcula. Para isso, e mesmo que o nome da rotina seja auto-explicativo, acrescenta-se ao cabealho um comentrio de documentao que indica o que a rotina faz ou calcula e que inclui a sua pr-condio e a sua condio objectivo. 2 A implementao de um mdulo em C++ est muitas vezes visvel ao programador consumidor. O programador consumidor de uma funo pode, muitas vezes, ver o seu corpo ou implementao. Mas no pode, atravs do seu programa, afectar directamente essa implementao: o programador consumidor usa a implementao de um mdulo sempre indirectamente atravs da sua interface. um pouco como se os mdulos em C++ fossem transparentes: pode-se ver o mecanismo, mas no se tem acesso directo a ele.

9.1. MODULARIZAO FSICA

455

em cada parte de um programa complexo. Assim, um espao nominativo (ou melhor, um pacote) abarca tipicamente vrios mdulos fsicos. Em resumo, existem os seguintes nveis de modularizao: 1. Modularizao procedimental: os mdulos a este nvel chamam-se rotinas (funes e procedimentos). 2. Modularizao de dados: os mdulos a este nvel chamam-se classes. 3. Modularizao fsica: os mdulos a este nvel chamam-se normalmente mdulos fsicos (ou simplesmente mdulos) e correspondem a cheiros (normalmente um par ou um trio de cheiros, como se ver). 4. Modularizao em pacotes: os mdulos a este nvel chamam-se pacotes e correspondem, no C++, a espaos nominativos. Nas prximas seces sero discutidos em algum pormenor estes dois novos nveis de modularizao: cheiros (modularizao fsica) e pacotes (espaos nominativos).

9.1 Modularizao fsica


Nos captulos anteriores viu-se que uma forma natural de reaproveitar cdigo no mesmo programa atravs da escrita de rotinas. Viu-se tambm como se criavam novos tipos de dados (i.e., TAD concretizados custa de classes C++), como se equipavam esses novos tipos das respectivas operaes, e como se utilizavam esses novos tipos num dado programa. Mas como reutilizar uma ferramenta, e.g., uma rotina ou uma classe C++, em programas diferentes? Claro est que uma soluo seria usar as opes Copiar/Colar do editor de texto. Mas essa no , decididamente, a melhor soluo. H vrias razes para isso, mas a principal talvez seja que dessa forma sempre que se altera a ferramenta copiada, presumivelmente para lhe introduzir correces ou melhorias, necessrio repetir essas alteraes em todas as suas cpias, que ao m de algum tempo se podem encontrar espalhadas por vrios programas. Para obviar a este problema, a linguagem C++ fornece um mecanismo de reutilizao mais conveniente: a compilao separada de mdulos fsicos correspondentes a diferentes cheiros. Mesmo que o objectivo no seja a reutilizao de cdigo, a modularizao fsica atravs da colocao de diferentes ferramentas (rotinas, classes, etc.) em cheiros separados muito til, pois permite separar claramente ferramentas com aplicaes diversas, agrupando simultaneamente ferramentas com relaes fortes entre si. Isto tem a vantagem no apenas de aumentar a estruturao do programa, mas tambm de facilitar a participao de vrias equipas no desenvolvimento de um programa: cada equipa desenvolve um conjunto de mdulos fsicos diferentes, e consequentemente cheiros diferentes. Alm disso, a compilao separada acelera o processo de construo do cheiro executvel pois, como se ver, uma alterao num mdulo de um programa no exige normalmente seno a reconstruo de uma pequena parte do programa, por vezes apenas do mdulo afectado.

456

CAPTULO 9. MODULARIZAO DE ALTO NVEL

9.1.1 Constituio de um mdulo


Um mdulo fsico de um programa constitudo normalmente por dois cheiros fonte 3 : o cheiro de interface (header le) com extenso .H (alternativamente .hpp, .hh ou mesmo .h++) e o cheiro de implementao com extenso .C (alternativamente .cpp, .cc ou mesmo .c++)4 . Por vezes um dos cheiros no existe. tipicamente o caso do mdulo que contm a funo main() do programa, que no muito til para reutilizaes noutros programas e que s contm o cheiro de implementao. Tambm ocorrem casos em que o mdulo contm apenas o cheiro de interface. Normalmente cada mdulo corresponde a uma unidade de traduo (translation unit). Uma unidade de traduo corresponde ao cheiro de implementao de um mdulo j depois de pr-processado, i.e., incluindo todos os cheiros especicados em directivas #include (ver mais abaixo). O termo mdulo por si s refere-se normalmente a um mdulo fsico, i.e., a uma unidade de modularizao fsica constituda normalmente por um cheiro de interface e pelo respectivo cheiro de implementao. Assim, de hora em diante a palavra mdulo ser usada neste sentido mais restrito, sendo utilizaes mais latas do termo indicadas explicitamente ou, espera-se, claras pelo contexto. Os nomes escolhidos para cada um dos cheiros de um mdulo reectem a sua utilizao usual. Normalmente o cheiro de interface serve para indicar as interfaces de todas as ferramentas disponibilizadas pelo mdulo e o cheiro de implementao serve para implementar essas mesmas ferramentas. Vistos a um nvel de abstraco superior, o cheiro de interface corresponde interface do mdulo e o cheiro de implementao sua implementao. Assim, o cheiro de interface contm normalmente a declarao de rotinas e a denio das classes C++ disponibilizadas pelo mdulo. Cada denio de um classe C++ contm a declarao das respectivas operaes, a denio dos atributos de instncia, a declarao dos atributos de classe, e a declarao das classes C++ e rotinas amigas da classe C++ em causa. Por outro lado, o cheiro de implementao contm normalmente as denies de todas as ferramentas usadas dentro do mdulo mas que no fazem parte da sua interface, ou seja, ferramentas de utilidade interna ao mdulo, e as denies de todas as ferramentas que fazem parte da interface mas que foram apenas declaradas no cheiro de interface. Mais frente se apresentaro um conjunto de regras mais explcito acerca do contedo tpico destes dois cheiros.

9.2 Fases da construo do cheiro executvel


A construo de um cheiro executvel a partir de um conjunto de diferentes mdulos fsicos tem trs fases: o pr-processamento, a compilao propriamente dita e a fuso. Inicialmente, um programa chamado pr-processador age individualmente sobre o cheiro de implementao (.C) de cada mdulo, incluindo nele todos os cheiros de interface (.H) es3 Chamam-se cheiros fonte pois so os cheiros que contm o cdigo C++ escrito pelo(s) programador(es) e que daro origem ao cheiro executvel do programa atravs de um processo de construo que produz uma srie de cheiros intermdios. 4 boa ideia reservar as extenses .h e .c para cheiros de interface escritos na linguagem C.

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL

457

pecicados por directivas #include e processando todas as directivas de pr-processamento (que correspondem a linhas comeadas por um #). O resultado do pr-processamento um cheiro de extenso .ii (em Linux) que contm linguagem C++ e a que se chama uma unidade de traduo. Depois, um programa chamado compilador traduz cada unidade de traduo (.ii) de C++ para cdigo relocalizvel em linguagem mquina, na forma de um cheiro objecto de extenso .o (em Linux). Os cheiros objecto, para alm de conterem o cdigo em linguagem mquina, contm tambm uma lista dos smbolos denidos ou usados nesse ou por esse cdigo e que ser usada na fase de fuso. A compilao age sobre cada unidade de traduo independentemente de todas as outras, da que seja usado o termo compilao separada para referir o processo de construo de um executvel a partir dos correspondentes cheiros fonte. O termo compilao pode ser usado no sentido lato de construo de um executvel a partir de cheiros fonte ou no sentido estrito de traduo de uma unidade de traduo para o correspondente cheiro objecto. Em rigor s o sentido estrito est correcto, mas comum (e fazemo-lo neste texto) usar tambm o sentido lato onde isso for claro pelo contexto. A ltima grande fase da construo a fuso(linking). Nesta fase, que a nica que actua sobre todos os mdulos em simultneo (com excepo dos que no tm unidade de traduo), os cheiros objecto do programa so fundidos (linked) num executvel. Ao programa que funde os cheiros objecto chama-se fusor (linker) 5 . Note-se que em Linux o mesmo programa (c++ ou g++) que se encarrega das trs fases, podendo-as realizar todas em sequncia ou apenas uma delas consoante os casos. As vrias fases da construo de um executvel so descritas abaixo com um pouco mais de pormenor.

9.2.1 Pr-processamento
O pr-processamento uma herana da linguagem C. No fazendo parte propriamente da linguagem C++, no entanto fundamental para desenvolver programas em mdulos fsicos separados. uma forma muito primitiva de garantir a compatibilidade e a correco do cdigo escrito em cada mdulo. A verdade, porm, que infelizmente o C++ no disponibiliza nenhum outro mtodo para atingir os mesmos objectivos, ao contrrio de outras linguagens como o Java, onde a modularizao fsica suportada directamente pela linguagem. O pr-processador age sobre um cheiro de implementao (.C), gerando uma unidade de traduo (.ii). Esta unidade de traduo contm cdigo C++, tal como o cheiro de implementao original, mas o pr-processador faz-lhe algumas alteraes, i.e., processa-o. O comando para pr-processar um cheiro de implementao em Linux 6 :
c++ -E nome.C -o nome.ii
5 Neste texto optou-se pelos termos fundir, fusor e fuso. Porventura teria sido prefervel a traduo literal de to link por ligar, linker por ligador e linkage por ligao. 6 O comando tambm pode ser g++.

458

CAPTULO 9. MODULARIZAO DE ALTO NVEL

onde nome o nome do mdulo a pr-processar, -E uma opo que leva o programa c++ a limitar-se a proceder pr-compilao do cheiro e -o uma opo que serve para indicar em que cheiro deve ser colocado o resultado do pr-processamento. O pr-processador copia o cdigo C++ do cheiro de implementao para a unidade de traduo7 , mas sempre que encontra uma linha comeada por um cardinal (#) interpreta-a. Estas linhas so as chamadas directivas de pr-processamento, que, salvo algumas excepes pouco importantes neste contexto, no existem no cheiro pr-processado, ou seja, na unidade de traduo gerada. Directivas de pr-processamento Existem variadssimas directivas de pr-processamento. No entanto, s algumas so relevantes neste contexto: #include #define #ifdef (#else) e #endif #ifndef (#else) e #endif A directiva #include tem dois formatos: 1. #include <nome> 2. #include "nome" Em ambos os casos o resultado da directiva a substituio dessa directiva, na unidade de traduo (cheiro pr-processado), por todo o contedo pr-processado do cheiro indicado por nome (e que pode incluir um caminho 8 ). A diferena entre os dois formatos prende-se com o local onde os cheiros a incluir so procurados. Os cheiros includos por estas directivas so tambm pr-processados, pelo que podem possuir outras directivas de incluso e assim sucessivamente. Quando o primeiro formato usado, o cheiro procurado nos locais ociais do sistema onde se trabalha. Por exemplo, para incluir os cheiros de interface que declaram as ferramentas de entradas e sadas a partir de canais da biblioteca padro faz-se
#include <iostream>
7 Na realidade o pr-processador faz mais do que isso. Por exemplo, elimina todos os comentrios do cdigo substituindo-os por um espao. A maior parte das operaes levadas a cabo pelo pr-processador, no entanto, no so muito relevantes para esta discusso simplicada. 8 Optou-se por traduzir por caminho o ingls path.

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL

459

pois este um cheiro de interface ocial. Neste caso um cheiro que faz parte da norma do C++. Isto signica que existe algures no sistema um cheiro chamado iostream (procure-o, em Linux, usando o comando locate iostream e veja o seu contedo). possvel acrescentar cheiros de interface aos locais ociais, desde que se tenha permisses para isso, ou ento acrescentar directrios lista dos locais ociais de modo a ocializar um conjunto de ferramentas por ns desenvolvido. Nos sistemas operativos baseados no Unix h vrios directrios com cheiros de interface ociais, sendo o mais usual o directrio /usr/include. Quando o segundo formato da directiva de incluso usado, o cheiro procurado primeiro a partir do directrio onde se encontra o cheiro fonte contendo a directiva em causa e, caso no seja encontrado, procurado nos locais ociais. Os cheiros includos so cheiros de interface com declaraes de ferramentas teis ao mdulo em processamento. A directiva #define tem como objectivo a denio de macros, que so como que variveis do pr-processador. As macros podem ser usadas de formas muito sosticadas e perigosas, sendo um mecanismo externo linguagem C++ propriamente dita e que com ela interferem muitas vezes de formas insuspeitas. Por isso recomenda-se cautela na sua utilizao. Neste texto ver-se- apenas a utilizao mais simples: #define NOME Depois desta directiva, o pr-processador tem denida uma macro de nome NOME com contedo nulo. Convencionalmente o nome das macros escreve-se usando apenas maisculas para as distinguir claramente dos nomes usados no programa C++. possvel colocar na unidade de traduo cdigo alternativo ou condicional, consoante uma dada macro esteja denida ou no durante o pr-processamento do cdigo fonte. A isso chama-se compilao condicional ou alternativa e consegue-se usando a directiva condicional #ifdef (que signica if dened) #ifdef NOME texto... #endif ou a directiva de seleco correspondente #ifdef NOME texto1... #else texto2... #endif ou ainda a directiva negada correspondente, comeada por #ifndef (que signica if not dened).

460

CAPTULO 9. MODULARIZAO DE ALTO NVEL

Quando a directiva condicional interpretada pelo pr-processador, o texto texto... s prprocessado e includo na unidade de traduo se a macro NOME estiver denida. Na directiva de seleco, se a macro estiver denida, ento apenas o texto texto1... pr-processado e includo na unidade de traduo, caso contrrio apenas o texto texto2... pr-processado e includo na unidade de traduo. Depois do pr-processamento A unidade de traduo que resulta do pr-processamento de um cheiro de implementao possui, tipicamente, algumas directivas residuais que so colocadas pelo prprio prprocessador. o que se passa com o pr-processador da GCC (Gnu Compiler Colection), usado em Linux. A discusso abaixo descreve esse ambiente particular de desenvolvimento. Para que um programa possa ser executado em modo de depurao deve ser compilado com a opo -g. Acontece que, para que o depurador saiba a que linha nos cheiros fonte corresponde cada instruo (para a poder mostrar no editor, por exemplo o XEmacs), a informao acerca da linha e do cheiro fonte a que corresponde cada instruo em cdigo mquina tem de car guardada nos cheiros objecto e, depois da fuso, no cheiro executvel. Suponha-se o seguinte cheiro de implementao chamado ol.C:
#include <iostream> using namespace std; int main() { cout < < "Ol mundo!" < < endl; }

Podem-se usar as seguintes instrues para construir o executvel:


c++ -E ol.C -o ol.ii c++ -c ol.ii c++ -o ol ol.o

Note-se que se compilou o cheiro pr-processado, ou seja, a unidade de traduo. Como pode o compilador saber de onde vieram as linhas do cheiro ol.ii, necessrias durante a depurao? S se o cheiro pr-processado contiver essa informao! Para isso servem as directivas introduzidas pelo pr-processador (que, como usualmente, comeam por #). No apenas para a depurao que essa informao til. Se essa informao no estivesse no cheiro pr-processado, os erros de compilao seriam assinalados no cheiro ol.ii, o que no ajudaria muito o programador, que editou o cheiro ol.C e no quer saber do cheiro ol.ii para nada. Logo, a informao acerca da origem das linhas da unidade de traduo tambm til para o compilador produzir mensagens de erro (experimente-se executar os

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL

461

comandos acima, mas acrescentando a opo -P ao pr-processar, e veja-se o que o compilador diz quando encontra um erro...). As nicas directivas presentes nas unidades de traduo (.ii) tm o seguinte formato: # linha nome [x...] onde: linha o nmero da linha no cheiro fonte de onde veio a prxima linha da unidade de traduo. nome o nome do cheiro fonte de onde veio a prxima linha na unidade de traduo. x so um conjunto de nmeros com os seguintes possveis valores: 1. Indica o comeo de um novo cheiro fonte. 2. Indica que se regressou a um cheiro fonte (depois de terminar a incluso de outro). 3. Indica que as linhas que se seguem vm de um cheiro de interface ocial (serve para desactivar alguns avisos do compilador). 4. Indica que as prximas linhas contm linguagem C. Exemplo Suponha-se que se escreveu uma funo mximoDe() para calcular o mximo dos primeiros valores contidos num vector de double:
/** Devolve o maior dos valores dos itens do vector v. @pre P C 0 < v.size(). @post CO (Q j : 0 j < v.size() : v[j] mximoDe) (E j : 0 j < v.size() : v[j] = mximoDe). */ double mximoDe(vector<double> const& v) { double mximo = v[0]; for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; return mximo; }

Durante o desenvolvimento do programa onde a funo acima usada, conveniente vericar a pr-condio da funo. Desse modo, se o programador consumidor da funo se enganar, o programa abortar imediatamente com uma mensagem de erro apropriada, o que antecipar a deteco do erro e consequente correco. Assim, durante o desenvolvimento, a funo deveria fazer a vericao explcita da pr-condio 9 :
O procedimento exit() termina abruptamente a execuo de um programa. No se recomenda a sua utilizao em nenhuma circunstncia. Por favor leia um pouco mais e encontrar melhores alternativas...
9

462

CAPTULO 9. MODULARIZAO DE ALTO NVEL


/** Devolve o maior dos valores dos itens do vector v. @pre P C 0 < v.size(). @post CO (Q j : 0 j < v.size() : v[j] mximoDe) (E j : 0 j < v.size() : v[j] = mximoDe). */ double mximoDe(vector<double> const& v) { if(v.size() == 0) { cerr < < "Erro em mximo()! Vector com dimenso nula!" < < endl; exit(1); // Ateno! No se advoga o uso de exit()! // Leia um pouco mais, por favor... } double mximo = v[0]; for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; return mximo; }

O problema desta soluo que, quando o programa est j desenvolvido, testado e distribudo, a instruo condicional acrescentada funo deixa de ter utilidade, pois presume-se que o programador consumidor j garantiu que todas as suas chamadas desta funo vericam a pr-condio, servindo a sua vericao explcita apenas para tornar o programa mais lento. Da que fosse conveniente que essa instruo fosse retirada depois de depurado o programa e que se voltasse a colocar apenas quando o programa fosse actualizado. Ou seja, o ideal seria que as instrues de vericao produzissem efeito em modo de depurao ou desenvolvimento e no produzissem qualquer efeito em modo de distribuio 10 . Mas pr e tirar instrues desta forma muito m ideia, mesmo se para o efeito se usarem comentrios: que pode haver muitas, mas mesmo muitas instrues deste tipo num programa, o que levar a erros e esquecimentos. O problema resolve-se recorrendo compilao condicional:
/** Devolve o maior dos valores dos itens do vector v. @pre P C 0 < v.size(). @post CO (Q j : 0 j < v.size() : v[j] mximoDe) (E j : 0 j < v.size() : v[j] = mximoDe). */ double mximoDe(vector<double> const& v) { #ifndef NDEBUG if(v.size() == 0) { cerr < < "Erro em mximo()! Vector com dimenso nula!"
10 O modo de distribuio (release) o modo do programa tal como ele distribudo aos seus utilizadores nais. O modo de depurao (debug) ou desenvolvimento o modo do programa enquanto est em desenvolvimento e teste.

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL


< < endl; exit(1); } #endif double mximo = v[0]; for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; return mximo; }

463

Se a macro NDEBUG (que signica not debug) no estiver denida, i.e., se se estiver em fase de depurao, a vericao da pr-condio feita. Caso contrrio, i.e., se no se estiver em fase de depurao mas sim em fase de distribuio e portanto a macro NDEBUG estiver denida, ento o cdigo de vericao da pr-condio eliminado da unidade de traduo e nem chega a ser compilado! As instrues de assero descritas na Seco 3.2.19 usam a macro NDEBUG exactamente da mesma forma que no cdigo acima. De resto, as instrues de assero permitem escrever o cdigo de uma forma mais clara (aproveitou-se para colocar uma vericao parcial da condio objectivo)11 :
/** Devolve o maior dos valores dos itens do vector v. @pre P C 0 < v.size(). @post CO (Q j : 0 j < v.size() : v[j] mximoDe) (E j : 0 j < v.size() : v[j] = mximoDe). */ double mximo(vector<double> const& v) { assert(0 < v.size()); double mximo = v[0]; for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; // Sem ciclos no se pode fazer muito melhor (amostragem em trs locais): assert(v[0] <= mximo and v[v.size() / 2] <= mximo and v[v.size() - 1] <= mximo); return mximo; }

Todas as asseres de um mdulo podem ser desligadas denindo a macro NDEBUG. Para o fazer no sequer necessrio alterar nenhum cheiro do mdulo! Basta dar a opo de
11

Note-se que assert() , na realidade, uma macro com argumentos (noo que no se descreve neste texto).

464

CAPTULO 9. MODULARIZAO DE ALTO NVEL

pr-processamento -DNDEBUG. A opo -DMACRO dene automaticamente a macro MACRO em todos os cheiros pr-processados. Logo, para pr-processar um cheiro em modo de distribuio pode-se usar o comando
c++ -DNDEBUG -E nome.C -o nome.ii

ou, se se pretender tambm compilar o cheiro, para alm de o pr-processar:


c++ -DNDEBUG -c nome.C

9.2.2 Compilao
O compilador age sobre uma unidade de traduo (cheiro pr-processado) e tradu-lo para linguagem mquina. O cheiro gerado chama-se cheiro objecto e, apesar de conter linguagem mquina, no um cheiro executvel, pois contm informao adicional acerca desse mesmo cdigo mquina, como se ver. Os cheiros objecto tm extenso .o (em Linux) e contm cdigo mquina utilizvel (sem necessidade de compilao) por outros programas. Em Linux, o comando para compilar a unidade de traduo de um mdulo de nome nome
c++ -Wall -ansi -pedantic -g -c nome.ii

ou, se se quiser pr-processar e compilar usando um s comando:


c++ -Wall -ansi -pedantic -g -c nome.C

em que a opo -c indica que se deve proceder apenas compilao (e, se necessrio, tambm ao pr-processamento), a opo -Wall pede ao compilador para avisar de todos (all) os potenciais erros (Warnings), as opes -ansi e -pedantic dizem para o compilador seguir to perto quanto possvel a norma da linguagem, e a opo -g indica que o cheiro objecto gerado deve conter informao de depurao. A compilao de uma unidade de traduo consiste pois na sua traduo para linguagem mquina e feita em vrios passos: 1. Anlise lexical. Separa o texto do programa em smbolos, vericando a sua correco. Equivale identicao de palavras e sinais de pontuao que os humanos fazem quando lem um texto, que na realidade consiste numa sequncia de caracteres. 2. Anlise sintctica. Verica erros gramaticais no cdigo, por exemplo conjuntos de smbolos que no fazem sentido como a / / b. Equivale vericao inconsciente da correco gramatical de um texto que os humanos fazem. Ao contrrio dos humanos, que so capazes de lidar com um texto contendo muitos erros gramaticais (e.g., eu leste texto este e erro nenhum encontrmos), o compilador no lida nada bem com erros sintcticos. Embora os compiladores tentem recuperar de erros passados e continuar a analisar o cdigo de modo a produzir um conjunto to relevante quanto possvel de erros e avisos, muitas vezes enganam-se redondamente, assinalando erros que no esto presentes no cdigo. Por isso, apenas o primeiro erro assinalado pelos compiladores verdadeiramente vel (mesmo que possa ser difcil de perceber).

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL

465

3. Anlise semntica. Verica se o texto, apesar de sintacticamente correcto, tem signicado. Por exemplo, verica a adequao dos tipos de dados aos operadores, assinalando erros em expresses como 1.1 % 3.4. Equivale vericao inconsciente do sentido de frases gramaticalmente correctas. Por exemplo, o leitor humano reconhece como no fazendo sentido os versos12 O ganso, gostou da dupla e fez tambm qem, qem Olhou pro cisne e disse assim vem, vem Que um quarteto car bem, muito bom, muito bem. 4. Optimizao. Durante esta fase o compilador elimina cdigo redundante, simplica expresses, elimina variveis desnecessrias, etc., de modo a poder gerar cdigo mquina to eciente quanto possvel. 5. Gerao de cdigo mquina (pode ter uma fase intermdia numa linguagem de mais baixo nvel). O resultado da compilao um cheiro objecto, como se viu. Este cheiro no contm apenas cdigo mquina. Simplicando algo grosseiramente a realidade, cada cheiro objecto tem guardadas duas tabelas: uma a tabela das disponibilidades e outra a tabela das necessidades. A primeira tabela lista os nomes (ou melhor, as assinaturas) das ferramentas denidas pelo respectivo mdulo, e portanto disponveis para utilizao em outros mdulos. A segunda tabela lista os nomes das ferramentas que so usadas pelo respectivo mdulo, mas no so denidas por ele, e que portanto devem ser denidas por outro mdulo. Por exemplo, suponham-se seguintes mdulos A e B: A.H
void meu(); // declarao do procedimento meu() que est denido em A.C.

A.C
#include "A.H" #include "B.H" // Denio do procedimento meu(): void meu() { outro(); }

B.H
void outro(); // declarao do procedimento outro() que // est denido em B.C.
O Pato, de Joo Gilberto. No entanto, estes versos no esto errados, pois usam a gura da personicao. Os compiladores no tm esta nossa capacidade de entender guras de estilo, bem entendido...
12

466 B.C
#include "B.H"

CAPTULO 9. MODULARIZAO DE ALTO NVEL

// Denio do procedimento outro(): void outro() { }

A compilao separada das unidades de traduo destes dois mdulos resulta em dois cheiros objecto A.o e B.o contendo: A.o 1. Cdigo mquina. 2. Disponibilidades: meu() 3. Necessidades: outro() B.o 1. Cdigo mquina. 2. Disponibilidades: outro() 3. Necessidades: Cada cheiro objecto, por si s, no executvel. Por exemplo, aos cheiros A.o e B.o falta a funo main() e ao cheiro A.o falta o procedimento outro() para poderem ser executveis. No entanto, mesmo que um cheiro objecto contenha a denio da funo main() e de todas as ferramentas usadas, no executvel. O executvel sempre gerado pelo fusor, que utiliza a informao acerca do que cada cheiro objecto disponibiliza e necessita para fazer o seu trabalho.

9.2.3 Fuso
Quer o pr-processamento quer a compilao, j descritas, agem sobre um nico mdulo. O pr-processador age sobre o cheiro de implementao do mdulo (.C), honrando todas as directivas de #include que encontrar, pelo que na realidade o pr-processamento pode envolver vrios cheiros: um de implementao e os outros de interface. O resultado do prprocessamento um nico cheiro, chamado unidade de traduo. O compilador age sobre uma unidade de traduo de cada vez e independentemente de todas as outras. Por isso se chama muitas vezes modularizao fsica compilao separada. Mas um programa, mesmo que o seu cdigo esteja espalhado por vrios mdulos, tem normalmente um nico cheiro executvel. Logo, tem de ser a ltima fase da construo de um executvel a lidar com todos os mdulos em simultneo: a fuso. O fusor o programa que funde todos os cheiros objecto do programa e gera o cheiro executvel. Em Linux, o comando para fundir os cheiros objecto num executvel :

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL


c++ -o programa mdulo1.o mdulo2.o ...

467

onde mdulo1.o etc., so os cheiros objecto a fundir e programa o nome que se pretende dar ao cheiro executvel. Se se quiser pr-processar, compilar e fundir usando um s comando, o que pouco recomendvel, pode-se usar o comando
c++ -Wall -ansi -pedantic -g -o programa mdulo1.C mdulo2.C ...

que comea por pr-processar o cheiros de implementao, depois compila as respectivas unidades de traduo, e nalmente funde os cheiros objecto resultantes. O fusor basicamente concatena o cdigo mquina de cada um dos cheiros objecto especicados13 . Mas durante esse processo: 1. Verica se h repeties. No pode haver dois cheiros objecto a denir a mesma ferramenta. Se houver repeties, o fusor escreve uma mensagem de erro (com um aspecto um pouco diferente das mensagens do compilador). 2. Para cada ferramenta listada como necessria na tabela de necessidades de cada cheiro objecto, o fusor verica se ela est listada na tabela de disponibilidades de algum cheiro objecto. Se faltar alguma ferramenta, o fusor assinala o erro. 3. Verica se existe uma funo main(). De outra forma no se poderia criar o executvel, cuja execuo comea sempre nessa funo 14 . de notar que o fusor no coloca no cheiro executvel todo o cdigo mquina de todos os cheiros objecto. O fusor tenta fazer uma seleco inteligente de quais as ferramentas que so realmente usadas pelo programa. por isso que os cheiros executveis so normalmente bastante mais pequenos que a soma das dimenses das partes fundidas. Ficheiros fundidos automaticamente O fusor acrescenta lista de cheiros a fundir o cheiro de arquivo (de extenso .a, ver Seco 9.2.4) contendo os cheiros objecto da biblioteca padro do C++. por isso que possvel usar canais de entrada ou sada (cin e cout), cadeias de caracteres (string), etc., sem grandes preocupaes: os respectivos cheiros objecto so fundidos automaticamente ao construir o executvel. Por exemplo, o cheiro executvel do famoso primeiro programa em C++: ol.C
Na realidade tem de fazer bastante mais, mas isso est fora do mbito deste texto. Na realidade este ltimo passo no existe, pois o fusor funde sempre automaticamente os cheiros objecto dados pelo utilizador junto com outros cheiros fornecidos por si e que contm a funo main() na tabela das necessidades.
14 13

468
#include <iostream> using namespace std;

CAPTULO 9. MODULARIZAO DE ALTO NVEL

int main() { cout < < "Ol mundo!" < < endl; }

pode ser construdo por


c++ -Wall -ansi -pedantic -g -c ol.C c++ -Wall -g -o ol ol.o

em que o primeiro comando pr-processa e compila o cheiro de implementao ol.C e o segundo constri o cheiro executvel ol. Estes comandos parecem indicar que o programa depende apenas do cheiro ol.C. Nada mais falso. A incluso de iostream e utilizao de cout, endl e do operador < < obrigam fuso com, entre outros, o cheiro objecto do mdulo iostream existente no cheiro de arquivo da biblioteca padro da linguagem C++. O arquivo em causa chama-se libstdc++.a e encontra-se algures no sistema (procure-o com o comando locate libstdc++.a e use o comando ar t caminho/libstdc++.a para ver o seu contedo, onde encontrar o cheiro ios.o). Mesmo o programa mais simples obriga fuso com um conjunto avulso de outros cheiros objecto e cheiros objecto includos em arquivos. O comando c++ especica esses cheiros automaticamente, simplicando com isso a vida do programador. Experimente-se passar a opo -nostdlib ao comando c++ ao construir o programa ol a partir do cheiro objecto ol.o:
c++ -nostdlib -o ol ol.o

Como esta opo inibe a fuso automtica do cheiro ol.o com o conjunto de cheiros objecto e arquivos de cheiros objecto necessrios construo do executvel, o comando anterior produz erros semelhantes ao seguinte 15 :
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 08048074 ol.o: In function main: .../ol.C:7: undefined reference to endl(ostream &) .../ol.C:7: undefined reference to cout .../ol.C:7: undefined reference to ostream::operator< <(char const *) .../ol.C:7: undefined reference to ostream::operator< <(ostream &(*)(ostream &)) collect2: ld returned 1 exit status
15 Estes erros foram obtidos num sistema Linux, distribuio Red Hat 7.0, com o GCC verso 2.96, pelo que em sistemas com outra congurao as mensagens podem ser diferentes.

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL

469

O primeiro dos erros apresentados o menos evidente. Ele indica que o programa no tem stio para comear. que os programas em C++, como comeam todos na funo main(), tm de ser fundidos com cheiros objecto que invocam essa funo logo no incio do programa. Alguns desses cheiros objecto esto includos no cheiro de arquivo da biblioteca padro da linguagem C, que se chama libc.a. Se se usar o comando:

c++ -nostdlib -o ol /usr/lib/crt1.o /usr/lib/crti.o ol.o /usr/lib/libc.a /usr/lib/gcc-lib/i386-redhat-

a primeira mensagem de erro desaparece, restando apenas:


ol.o: In function main: .../ol.C:7: undefined reference to endl(ostream &) .../ol.C:7: undefined reference to cout .../ol.C:7: undefined reference to ostream::operator< <(char const *) .../ol.C:7: undefined reference to ostream::operator< <(ostream &(*)(ostream &)) collect2: ld returned 1 exit status

Para conseguir construir o executvel com sucesso, no entanto, necessrio especicar todos os cheiros objecto e arquivos de cheiros objecto necessrios (e que incluem, entre outros, o arquivo da biblioteca padro do C++ libstdc++.a):

c++ -nostdlib -o ol /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o

No se preocupe se no sabe para que serve cada um dos cheiros! Basta retirar a opo -nostdlib e tudo isto feito automaticamente... Fuso dinmica Quase todos os sistemas operativos suportam fuso dinmica. A ideia que a fuso com os cheiros de arquivo das bibliotecas usadas pelo programa pode ser adiada, sendo realizada dinamicamente apenas quando o programa executado. Esta soluo tem como vantagens levar a cheiros executveis bastante mais pequenos e permitir a actualizao dos cheiros de arquivo das bibliotecas sem obrigar a reconstruir o executvel (note-se que o executvel e as bibliotecas, que neste caso se dizem dinmicas ou partilhadas, podem ser fornecidos por entidades independentes). Os cheiros de arquivo de bibliotecas dinmicas ou partilhadas tm extenso .a, em Linux, e .dll em Windows. Para mais pormenores ver [9, Capitulo 7].

9.2.4 Arquivos
possvel colocar um conjunto de cheiros objecto relacionados num cheiro de arquivo. Estes cheiros tm prexo lib e extenso .a (de archive). tpico que os cheiros objecto de uma biblioteca de ferramentas sejam colocados num nico cheiro de arquivo, para simplicar a sua fuso com os cheiros objecto de diferentes programas. Por exemplo, os cheiros

470

CAPTULO 9. MODULARIZAO DE ALTO NVEL

objecto da biblioteca padro da linguagem C++ (de nome stdc++) esto arquivados no cheiro libstdc++.a (que se encontra algures num sub-directrio do directrio /usr/lib). Para arquivar cheiros objecto no se usa o fusor: usa-se o chamado arquivador. O programa arquivador invoca-se normalmente como se segue:
ar ru libbibliteca.a mdulo1.o mdulo2.o ...

Onde ru so opes passadas ao arquivador (r signica replace e u signica update), biblioteca o nome da biblioteca cujo arquivo se pretende criar, e mdulo1 etc., so os nomes dos mdulos que constituem a biblioteca e cujos cheiros objecto se pretende arquivar. Como cada mdulo includo numa biblioteca tem um cheiro de interface e produz geralmente um cheiro objecto que guardado no arquivo dessa biblioteca, pode-se dizer que as bibliotecas so representadas por: 1. Ficheiro de arquivo dos cheiros objecto (um cheiro objecto por mdulo com cheiro de implementao) da biblioteca. 2. Ficheiros de interface de cada mdulo (que podem incluir outros cheiros de interface). o que se passa com a biblioteca padro da linguagem C++, representada em Linux pelo cheiro de arquivo libstdc++.a e pelos vrios cheiros de interface que a compem (iostream, string, cmath, etc.). Exemplo Suponha-se uma biblioteca de nome saudaes constituda (para j) apenas pelo seguinte mdulo: ol.H
void ol();

ol.C
#include "ol.H" #include <iostream> using namespace std; void ol() { cout < < "Ol mundo!" < < endl; }

9.2. FASES DA CONSTRUO DO FICHEIRO EXECUTVEL


Para construir o cheiro de arquivo da biblioteca pode-se usar os seguintes comandos:
c++ -Wall -ansi -pedantic -g -c ol.C ar ru libsaudaes.a ol.o

471

em que o primeiro comando pr-processa e compila o cheiro de implementao do mdulo ol e o segundo arquiva o cheiro objecto resultante no arquivo da biblioteca saudaes, de nome libsaudaes.a. Seja agora um programa de nome sada que pretende usar o procedimento ol(). Para isso deve comear por incluir o cheiro de interface ol.H, que se pretende seja tomado por um cheiro ocial (incluso com <> e no ""): sada.C
#include <ol.H> int main() { ol(); }

O pr-processamento e compilao do cheiro de implementao sada.C com o comando


c++ -Wall -ansi -pedantic -g -c sada.C

resulta na mensagem de erro


sada.C:1: ol.H: No such file or directory

O problema que, para incluir ol.H como um cheiro de interface ocial, uma de trs coisas tem de ocorrer: 1. O cheiro ol.H est num local ocial (e.g., /usr/include em Linux). 2. D-se a opo -I. ao comando c++ para que ele acrescente o directrio corrente (.) lista de directrios onde os cheiros de interface ociais so procurados:
c++ -Wall -ansi -pedantic -g -I. -c ol.C

3. Coloca-se o cheiro ol.H num directrio criado para o efeito (e.g., $HOME/include, onde $HOME o directrio casa do utilizador corrente em Linux) e coloca-se esse directrio na varivel de ambiente (environment variable) CPLUS_INCLUDE_PATH, que contm uma lista de directrios que, para alm dos usuais, devem ser considerados como contendo cheiros de interface ociais:

472

CAPTULO 9. MODULARIZAO DE ALTO NVEL


mkdir $HOME/include setenv CPLUS_INCLUDE_PATH $HOME/include mv ol.H $HOME/include c++ -Wall -ansi -pedantic -g -c sada.C

onde o primeiro comando (criar directrio) s tm de ser dado uma vez, e o segundo tem de ser dado sempre que se entra no Linux (e s funciona com o interpretador de comandos /bin/tcsh), pelo que um bom candidato a fazer parte do cheiro .tcshrc. Falta agora fundir o cheiro sada.o com os cheiros objecto do cheiro arquivo libsaudaes.a (que por acaso s um: ol.o). Para isso podem-se usar um de trs procedimentos: 1. Fuso directa com o cheiro de arquivo:
c++ -o sada sada.o libsaudaes.a

2. Fuso usando a opo -l (link with):


c++ -o sada sada.o -L. -lsaudaes

necessria a opo -L. para que o comando c++ acrescente o directrio corrente (.) lista de directrios onde os cheiros de arquivo ociais so procurados. 3. Fuso usando a opo -l, mas convencendo o comando c++ de que o arquivo em causa ocial. Para isso: (a) coloca-se o cheiro libsaudaes.a num local ocial (e.g., /usr/lib em Linux); ou (b) coloca-se o cheiro libsaudaes.a num directrio criado para o efeito, e.g., $HOME/lib, e coloca-se esse directrio na varivel de ambiente LIBRARY_PATH, que contm uma lista de directrios que, para alm dos usuais, devem ser considerados como contendo os arquivos ociais:
mkdir $HOME/lib setenv LIBRARY_PATH $HOME/lib mv libsaudaes.a $HOME/lib c++ -o sada sada.o -lsaudaes

onde o primeiro comando (criar directrio) s tm de ser dado uma vez, e o segundo tem de ser dado sempre que se entra no Linux (e s funciona com a shell /bin/tcsh), pelo que um bom candidato a fazer parte do cheiro .tcshrc. Arquivos vs. cheiros objecto A fuso com um arquivo de cheiros objecto no , em rigor, equivalente fuso directa com os cheiros objecto arquivados. Enquanto numa fuso so vericadas denies repetidas em todos os cheiros objecto existentes, essa vericao no feita no caso dos cheiro de arquivo.

9.3. LIGAO DOS NOMES

473

Estes so percorridos por ordem de especicao na linha de comandos e a pesquisa pra logo que se encontra um cheiro objecto arquivado que contenha a ferramenta procurada. Assim, o comando
c++ -o sada sada.o ol.o ol.o

produziria uma mensagem de erro indicando a denio mltipla do procedimento ol(), enquanto o comando
c++ -o sada sada.o libsaudaes.a libsaudaes.a

no produziria qualquer mensagem de erro, pois o fusor pararia ao encontrar o procedimento no primeiro arquivo especicado, ignorando o segundo.

9.3 Ligao dos nomes


Os exemplos das seces anteriores tinham uma particularidade interessante: rotinas denidas num mdulo podiam ser utilizadas noutros mdulos, desde que apropriadamente declaradas. Ser que sempre assim? E ser assim tambm para variveis e constantes globais? E o caso das classes? Nesta seco abordar-se- a questo da ligao dos nomes em C++. A ligao de um nome tem a ver com a possibilidade de uma dada entidade (e.g., uma funo, uma constante, uma classe, etc.), com um dado nome, poder ser utilizada fora do mdulo (ou melhor, fora da unidade de traduo) onde foi denida. Em C++ existem trs tipos de ligao possvel para os nomes: nomes sem ligao, com ligao interna, e com ligao externa. Quando um nome no tem ligao, a entidade correspondente no pode ser usada seno no seu mbito de visibilidade. Uma varivel local, por exemplo, no tem ligao, pois pode ser usada apenas no bloco onde foi denida. Uma entidade cujo nome tenha ligao interna pode ser usada em variados mbitos dentro da unidade de traduo onde foi denida, desde que o seu nome tenha sido previamente declarado nesses mbitos, mas no pode ser usada em outras unidades de traduo do mesmo programa. Normalmente h uma unidade de traduo por mdulo (que o seu cheiro de implementao pr-processado). Assim, pode-se dizer que uma entidade cujo nome tem ligao interna apenas utilizvel dentro do mdulo que a dene. Uma entidade cujo nome tenha ligao externa pode ser usada em qualquer mbito de qualquer unidade de traduo do programa, desde que o seu nome tenha sido previamente declarado nesses mbitos. Assim, pode-se dizer que uma entidade cujo nome tem ligao externa utilizvel em qualquer mdulo do programa. Em geral as entidades denidas dentro de blocos de instrues, que so entidades locais, no tm ligao. Assim, esta discusso centrar-se- essencialmente nas entidades com ligao, sendo ela interna ou externa, e que geralmente so as entidades globais, i.e., denidas fora de qualquer funo ou procedimento.

474

CAPTULO 9. MODULARIZAO DE ALTO NVEL

A linguagem C++ impe a chamada regra da denio nica. Esta regra diz que, no mesmo contexto, no podem ser denidas mais do que uma entidade com o mesmo nome. Assim, resumidamente: 1. Um programa no pode denir mais do que uma rotina ou mtodo que no seja em-linha e com ligao externa se estes tiverem a mesma assinatura (podem ter o mesmo nome, desde que variem na lista de parmetros). 2. Uma unidade de traduo no pode denir mais do que uma rotina no-membro que no seja em-linha e com ligao interna se estas tiverem a mesma assinatura. 3. Uma varivel ou constante global com ligao externa s pode ser denida uma vez no programa. 4. Uma varivel ou constante global com ligao interna s pode ser denida uma vez na sua unidade de traduo. 5. Uma classe ou um tipo enumerado no-local s pode ser denido uma vez em cada unidade de traduo e, se for denido em diferentes unidades de traduo, a sua denio tem de ser idntica.

9.3.1 Vantagens de restringir a ligao dos nomes


muito importante perceber que h vantagens em restringir a utilizao de determinadas ferramentas a um dado mdulo: esta restrio corresponde a distinguir entre ferramentas que fazem apenas parte da implementao do mdulo e ferramentas que so disponibilizadas na sua interface. a possibilidade de distinguir entre o que est restrito e o que no est que faz com que os mdulos fsicos meream o nome que tm... Suponha-se, por exemplo, um mdulo vectores com funes e procedimentos especializados em operaes sobre vectores de inteiros. Esse mdulo poderia consistir nos seguintes cheiros de interface e implementao: matrizes.H
/** Devolve o maior dos valores dos itens do vector v. @pre P C 0 < v.size(). @post CO (Q j : 0 j < v.size() : m[j] mximoDe) (E j : 0 j < v.size() : m[j] = mximoDe). */ double mximoDe(vector<double> const& v); /** Devolve o ndice do primeiro item com o mximo valor do vector v. @pre P C 1 v.size(). @post CO 0 ndiceDoPrimeiroMximoDe < v.size() (Q j : 0 j < v.size() : v[j] v[ndiceDoPrimeiroMximoDe]) (Q j : 0 j < ndiceDoPrimeiroMximoDe : v[j] < v[ndiceDoPrimeiroMximoDe]). */ int ndiceDoPrimeiroMximoDe(vector<int> const& v);

9.3. LIGAO DOS NOMES

475

/** Ordena os valores do vector v. @pre P C v = v. @post CO perm(v, v) (Q i, j : 0 i j < v.size() : v[i] v[j]). void ordenaPorOrdemNoDecrescente(vector<int>& v);

matrizes.C
#include <cassert> using namespace std; /** Verica se o valor de mximo o mximo dos valores num vector v. @pre P C 0 < v.size(). @post CO MAaximoDe = ((Q j : 0 j < v.size() : m[j] mximo) (E j : 0 j < v.size() : m[j] = mximo)). */ bool MximoDe(int const mximo, vector<int> const& v) { assert(0 < v.size()); bool existe = false; // CI existe = (E j : 0 j < i : m[j] = mximo) // (Q j : 0 j < i : m[j] mximo). for(vector<int>::size_type i = 0; i != v.size(); ++i) { if(mximo < v[i]) return false; existe = existe or mximo == v[i]; } return existe; } /** Verica se os valores do vector v esto por ordem no decrescente. @pre P C V. @post CO NoDecrescente = (Q j, k : 0 j k < v.size() : v[j] v[k]). */ bool NoDecrescente(vector<int> const& v) { if(v.size() <= 1) return true; // CI (Q j, k : 0 j k < i : v[j] v[k]). for(vector<int>::size_type i = 1; i != v.size(); ++i) if(v[i - 1] > v[i]) return false; return true; }

476

CAPTULO 9. MODULARIZAO DE ALTO NVEL


double mximoDe(vector<double> const& v); { assert(0 < v.size()); double mximo = v[0]; // CI (Q j : 0 j < i : v[j] mximo) // (E j : 0 j < i : m[j] = mximo). for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; assert(MximoDe(mximo, v)); return mximo; } int ndiceDoPrimeiroMximoDe(vector<int> const& v) { assert(1 <= v.size()); int i = 0; // CI 0 i < k (Q j : 0 j < k : v[j] v[i]) // (Q j : 0 j < i : v[j] < v[i]) 0 k v.size(). for(vector<int>::size_type k = 1; k != v.size(); ++k) if(v[i] < v[k]) i = k; assert(0 <= i and i < v.size() and MximoDe(v[i], v)); return i; } void ordenaPorOrdemNoDecrescente(vector<int>& v) { ... // um algoritmo de ordenao eciente... assert(NoDecrescente(v)); }

No cheiro de implementao deste mdulo existem duas funes de utilidade restrita ao mdulo. A primeira, MximoDe(), serve para vericar se um dado valor o mximo dos valores contidos num vector. A segunda, NoDecrescente(), serve para vericar se os itens de um vector esto por ordem crescente (ou melhor, se formam uma sucesso montona no-decrescente). Ambas so usadas em instrues de assero para vericar se (pelo menos parte) das condies objectivo das outras rotinas do mdulo so cumpridas. Que estas duas funes no tm qualquer utilidade directa para o programador consumidor

9.3. LIGAO DOS NOMES

477

desde mdulo evidente. Saber se um valor o maior dos valores dos itens de um vector no muito til em geral. O que til saber qual o maior dos valores dos itens de um vector. De igual forma no muito til em geral saber se um vector est ordenado. muito mais til ter um procedimento para ordenar esse vector. Haver alguma desvantagem em deixar que essas funes sejam utilizadas fora deste mdulo? Em geral sim. Se so ferramentas teis apenas dentro do mdulo, ento o programador do mdulo pode decidir alterar o seu nome ou mesmo elimin-las sem dar cavaco a ningum. Se isso acontecesse, o incauto programador consumidor dessas funes caria numa situao incmoda... Alm disso, corre-se o risco de o nome dessas funes colidir com o nome de funes existentes noutros mdulos (ver Seco 9.6.1 mais abaixo). Assim, seria desejvel que as duas funes de vericao tivessem ligao interna.

9.3.2 Espaos nominativos sem nome


A melhor forma de restringir a utilizao de uma entidade ao mdulo em que est denida coloc-la dentro de um espao nominativo sem nome (ver Seco 9.6.2). Assim, a soluo para o problema apresentado na seco anterior passa por colocar as duas funes de vericao num espao nominativo sem nome: matrizes.C
#include <cassert> using namespace std; namespace { // Aqui denem-se as ferramentas de utilidade restrita a este mdulo: /** Verica se o valor de mximo o mximo dos valores num vector v. @pre P C 0 < v.size(). @post CO MAaximoDe = ((Q j : 0 j < v.size() : m[j] mximo) (E j : 0 j < v.size() : m[j] = mximo)). */ bool MximoDe(int const mximo, vector<int> const& v) { assert(0 < v.size()); bool existe = false; // CI existe = (E j : 0 j < i : m[j] = mximo) // (Q j : 0 j < i : m[j] mximo). for(vector<int>::size_type i = 0; i != v.size(); ++i) { if(mximo < v[i]) return false; existe = existe or mximo == v[i]; } return existe; }

478

CAPTULO 9. MODULARIZAO DE ALTO NVEL

/** Verica se os valores do vector v esto por ordem no decrescente. @pre P C V. @post CO NoDecrescente = (Q j, k : 0 j k < v.size() : v[j] v[k]). */ bool NoDecrescente(vector<int> const& v) { if(v.size() <= 1) return true; // CI (Q j, k : 0 j k < i : v[j] v[k]). for(vector<int>::size_type i = 1; i != v.size(); ++i) if(v[i - 1] > v[i]) return false; return true; } } double mximoDe(vector<double> const& v); { assert(0 < v.size()); double mximo = v[0]; // CI (Q j : 0 j < i : v[j] mximo) // (E j : 0 j < i : m[j] = mximo). for(vector<double>::size_type i = 1; i != v.size(); ++i) if(mximo < v[i]) mximo = v[i]; assert(MximoDe(mximo, v)); return mximo; } int ndiceDoPrimeiroMximoDe(vector<int> const& v) { assert(1 <= v.size()); int i = 0; // CI 0 i < k (Q j : 0 j < k : v[j] v[i]) // (Q j : 0 j < i : v[j] < v[i]) 0 k v.size(). for(vector<int>::size_type k = 1; k != v.size(); ++k) if(v[i] < v[k]) i = k; assert(0 <= i and i < n and MximoDe(v[i], v));

9.3. LIGAO DOS NOMES

479

return i; } void ordenaPorOrdemNoDecrescente(int m[], int n) { assert(0 <= n); ... // um algoritmo de ordenao eciente... assert(NoDecrescente(m, n)); }

Em termos tcnicos a denio de entidades dentro de um espao nominativo sem nome no lhes d obrigatoriamente ligao interna. O que acontece na realidade que o compilador atribui um nome nico ao espao nominativo. Em termos prticos o efeito o mesmo que se as entidades tivessem ligao interna.

9.3.3 Ligao de rotinas, variveis e constantes


importante voltar a distinguir entre os conceitos de denio e declarao. Uma denio cria uma entidade. Uma declarao indica a existncia de uma entidade, indicando o seu nome e cararactersticas. Uma denio sempre tambm uma declarao. Mas uma declarao no seu sentido estrito no uma denio, pois indicar a existncia de uma entidade e o seu nome no o mesmo que criar essa entidade: essa entidade foi ou ser criada num outro local. Nas seces seguintes apresentam-se exemplos que permitem distinguir entre declaraes e denies de vrios tipos de entidades e saber qual a ligao dos seus nomes. Nas tabelas, admite-se sempre que existem dois mdulos no programa: o mdulo do lado esquerdo (define) o mdulo que dene a maior parte das entidades enquanto o mdulo do lado direito (usa) o mdulo que as usa. Rotinas A declarao (em sentido estrito) de uma rotina faz-se colocando o seu cabealho seguido de ;. Por exemplo:
/** Devolve a potncia n de x. @pre P C 0 n x = 0. @post CO potncia = xn . */ double potncia(double const x, int const n); // funo. /** Troca os valores dos dois argumentos. @pre P C a = a b = b. @post CO a = b b = a. */ void troca(int& a, int& b);

// procedimento.

480

CAPTULO 9. MODULARIZAO DE ALTO NVEL

A denio de uma rotina faz-se indicando o seu corpo imediatamente aps o cabealho. Por exemplo:
double potncia(double const x, int const n) { assert(0 <= n or x != 0.0); if(n < 0) return 1.0 / potncia(x, -n); double r = 1.0; // CI r = xi 0 i n. for(int i = 0; i != n; ++i) r *= x; return r; } void troca(int& a, int& b) { int const auxiliar = a; a = b; b = auxiliar; }

Os mtodos de uma classe tm ligao externa se a respectiva classe tiver ligao externa. Caso contrrio no tm ligao, uma vez que as classes tambm ou no tm ligao ou tm ligao externa (ver mais abaixo). Em C++ no possvel denir rotinas locais (mas ver C.2). Sobram, pois, as funes ou procedimentos globais (ou denidos num espao nominativo), que tm sempre ligao. Por omisso as funes e procedimentos globais tm ligao externa. Para que uma funo ou procedimento tenha ligao interna, necessrio preceder a sua declarao do especicador de armazenamento static. um erro declarar uma funo ou procedimento como tendo ligao externa e, posteriormente, declar-lo com tendo ligao interna. Sejam os dois mdulos representados na Figura 9.1. Ao fundir os dois mdulos da gura (e admitindo que se eliminaram os erros de compilao assinalados) o fusor gerar erros como os seguintes:
.../usa.C:35: undefined reference to f3(void) .../usa.C:37: undefined reference to f5(void)

No primeiro caso, o erro ocorre porque o nico procedimento f3() que existe tem ligao interna ao mdulo define (a sua denio precedida do especicador static). No segundo caso, o procedimento f5() foi declarado e usado em usa.C mas nenhum mdulo o dene.

9.3. LIGAO DOS NOMES

481

define.C
void f4(); // externo void f1() // externo {...} void f2() // externo {...} static void f3() // interno {...} void f4() // externo { f3(); }

usa.C
void f1(); // externo void f3(); // externo extern void f2(); // externo void f5(); // externo void f6(); // externo /* Erro de compilao! pois denido interno: */ static void f6() {...} Declarado externo e de-

/* Erro de compilao! No est denido dentro do mdulo: */ static void f7(); // interno. static void f8(); // interno. extern void f8(); // interno. void f8() // interno. {...} int main() { f1(); f2(); f3(); compilao, falta declarao: f4(); f5(); f8(); }

// Erro de

Figura 9.1: Exemplo de ligao para rotinas. Declaraes em itlico e fontes de erros de compilao a vermelho.

482

CAPTULO 9. MODULARIZAO DE ALTO NVEL

O especicador extern, embora til no caso das variveis e constantes, como se ver mais abaixo, intil no caso das rotinas 16 . O que determina a ligao o especicador static. Se uma funo ou procedimento for declarado static, ter ligao interna. Assim, o seguinte cdigo declara e dene um procedimento com ligao interna:
static void f7(); extern void f7() { ... }

A utilizao de da palavra-chave static est deprecada 17 . prefervel usar espaos nominativos sem nome, ou annimos, para restringir a ligao de uma rotina ou instncia a uma unidade de traduo. Variveis As variveis denidas dentro de um bloco, i.e., as variveis locais, no tm ligao. As variveis membro de instncia de uma classe tm um esquema de acesso prprio, descrito no Captulo 7. As variveis membro de classe (ou estticas, ver Seco 7.17.2) tm ligao externa se a respectiva classe tiver ligao externa. Caso contrrio no tero ligao, uma vez que as classes ou no tm ligao ou tm ligao externa. Os casos mais interessantes de ligao de variveis dizem respeito a variveis globais (ou denidas num espao nominativo), que tm sempre ligao interna ou externa. Antes de discutir a ligao de variveis globais, porm, conveniente rearmar o que vem sendo dito: m ideia usar variveis globais! At este momento neste texto nunca se declararam variveis globais. H dois factores que permitem distinguir uma declarao de uma denio no caso das variveis. Para que uma declarao de uma varivel seja uma declarao no sentido estrito, i.e., para que no seja tambm uma denio, tm de acontecer duas coisas: 1. A declarao tem de ser precedida do especicador extern. 2. A declarao no pode inicializar a varivel. Se uma declarao possuir uma inicializao explcita forosamente uma denio, mesmo que possua o especicador extern. Se uma denio de uma varivel global no possuir uma inicializao explcita, ser inicializada implicitamente com o valor por omisso do tipo respectivo, mesmo que o seu tipo seja um dos tipos bsicos do C++. Ou seja, ao contrrio do
16 Na realidade o especicador extern no totalmente intil em conjunto com rotinas. Para se poder invocar, a partir de cdigo C++, uma rotina num mdulo em linguagem C, necessrio preceder a declarao dessa funo ou procedimento de extern "C", como se segue:

extern "C" int atoi(char char[]); Usa-se a palavra deprecar no seu sentido em ingls: deprecar algo mais forte que no recomendar esse algo: recomendar no utilizar/fazer esse algo.
17

9.3. LIGAO DOS NOMES

483

que acontece com as variveis locais dos tipos bsicos, as variveis globais dos tipos bsicos so inicializadas implicitamente com valor por omisso do respectivo tipo (que sempre um valor nulo). Se o tipo da varivel for uma classe sem construtor por omisso, ento uma denio sem inicializao um erro. Por exemplo:
int i = 10; // int j; // extern int k = 0; // extern int j; // class A { public: A(int i) : i(i) { } private: int i; }; A a(10); // denio e inicializao com 10. A b; // tentativa de denio: falha porque A no tem construtor por omisso. denio e inicializao explcita com 10. denio e inicializao implcita com 0. denio e inicializao com 0. declarao.

Uma varivel global tem, por omisso, ligao externa. Para que uma varivel global tenha ligao interna necessrio preceder a sua declarao do especicador static. Na realidade, como os especicadores static e extern no se podem aplicar em simultneo, a presena do especicador static numa declarao obriga-a a ser tambm uma denio. Sejam os dois mdulos representados na Figura 9.2. Ao fundir os dois mdulos acima (e admitindo que se eliminaram os erros de compilao assinalados) o fusor gerar erros como os seguintes:
.../usa.C:35: undefined reference to v3 .../usa.C:37: undefined reference to v5

No primeiro caso, o erro ocorre porque a nica varivel v3 que existe tem ligao interna ao mdulo define, uma vez que a sua denio precedida do especicador static. No segundo caso, a varivel v5 foi declarada em usa.C mas nenhum mdulo a dene. Constantes As constantes denidas dentro de um bloco, ou seja, constantes locais, no tm ligao. As constantes membro de instncia de uma classe tm um esquema de acesso prprio, descrito no Captulo 7. As constantes membro de classe (ou estticas, ver Seco 7.17.2) tm ligao

484

CAPTULO 9. MODULARIZAO DE ALTO NVEL

define.C
extern int v4; // externa int v1; // externa int v2 = 1; // externa static int v3 = 3; // interna int v4; // externa

usa.C
extern int v1; // externa extern int v2; // externa extern int v3; // externa extern int v5; // externa extern int v6; // externa /* Erro (ou aviso) de compilao! terna e depois denida interna: */ static int v6 = 12; static int v7; // interna extern int v7; // interna int main() { int i = v1 + v2 + v3 + // Erro de compilao, falta declarao: v4 + v5 + v7; } Declarada ex-

Figura 9.2: Exemplo de ligao para variveis. Declaraes em itlico e fontes de erros de compilao a vermelho.

9.3. LIGAO DOS NOMES

485

externa se a respectiva classe tiver ligao externa. Caso contrrio no tero ligao, uma vez que as classes tambm ou no tm ligao ou tm ligao externa. Os casos mais interessantes de ligao de constantes dizem respeito a constantes globais (ou denidas num espao nominativo), que tm sempre ligao interna ou externa. Para que uma declarao de uma constante seja uma declarao no sentido estrito, i.e., para que no seja tambm uma denio, tm de acontecer duas coisas: 1. A declarao tem de ser precedida do especicador extern. 2. A declarao no pode inicializar a constante. Se uma declarao possuir uma inicializao explcita, ento forosamente uma denio, mesmo que possua o especicador extern. Se uma denio de uma constante global no possuir uma inicializao explcita, ser inicializada implicitamente com o valor por omisso do tipo respectivo, excepto se o seu tipo seja um dos tipos bsicos do C++ ou, em geral, um POD (plain old datatype). Se o tipo da constante for uma classe sem construtor por omisso, ento uma denio sem inicializao um erro. As regras que denem o que exactamente um POD so complexas. No contexto deste texto bastar dizer que um agregado, entendido como uma classe s com variveis membro pblicas de tipos bsicos ou de outros agregados ou matrizes de tipos bsicos e agregados, um POD. Por exemplo, a classe
struct Ponto { double x; double y; };

um agregado, e portanto um POD, logo:


Ponto const p;

um erro, pois falta uma inicializao explcita. Outros exemplos:


int const i = 10; int const j; que j de um tipo bsico. extern int const k = 0; extern int const i; class A { public: A(int i) : i(i) { } // denio e inicializao explcita com 10. // tentativa de denio: falha por// denio e inicializao com 0. // declarao.

486
private: int i; };

CAPTULO 9. MODULARIZAO DE ALTO NVEL

A const a(10); // denio e inicializao com 10. A const b; // tentativa de denio: falha porque A // no tem construtor por omisso.

Uma constante global tem, por omisso, ligao interna, i.e., o oposto da ligao por omisso das variveis. Para que uma constante global tenha ligao externa necessrio preceder a sua declarao do especicador extern. Assim sendo, a utilizao do especicador static na declarao de constantes globais redundante. Sejam os dois mdulos representados na Figura 9.3. Ao fundir os dois mdulos acima (e admitindo que se eliminaram os erros de compilao assinalados a vermelho) o fusor gerar os seguintes erros:
.../usa.C:35: undefined reference to c3 .../usa.C:37: undefined reference to c5

No primeiro caso, o erro ocorre porque a nica constante c3 que existe tem ligao interna ao mdulo define (por omisso as constantes globais tm ligao interna). No segundo caso, a constante c5 foi declarada em usa.C mas nenhum mdulo a dene.

9.3.4 Ligao de classes C++ e tipos enumerados


No possvel simplesmente declarar um tipo enumerado: sempre necessrio tambm denilo. A distino entre a declarao e a denio de uma classe C++ simples e semelhante sintacticamente distino entre declarao e denio no caso das rotinas. A declarao no sentido estrito de uma classe C++ termina em ; logo aps o seu nome. Se em vez de ; surgir um bloco com a declarao os membros da classe, a declarao tambm uma denio. Por exemplo:
class Racional; // declarao. class Racional { // denio. public: ... private: ... };

9.3. LIGAO DOS NOMES

487

define.C extern int const c4; // externa extern int const c1 =1;// externa extern int const c2 =1;// externa int const c3 = 3; // interna int const c4 = 33; // externa

usa.C extern int const c1; // externa extern int const c2; // externa extern int const c3; // externa extern int const c5; // externa extern int const c6; // externa /* Erro (ou aviso) de compilao! Declarada externa e depois denida interna: */ static int const c6 = 12; int const c7 = 11; // interna extern int const c7; // interna int main() { int i = c1 + c2 + c3 + // Erro de compilao, falta declarao: c4 + c5 + c7; }

Figura 9.3: Exemplo de ligao para constantes. Declaraes em itlico e fontes de erros de compilao a vermelho.

488

CAPTULO 9. MODULARIZAO DE ALTO NVEL

A declarao em sentido estrito de uma classe C++ til em poucas situaes. Como s a denio da classe C++ inclui a denio dos seus atributos (variveis e constantes membro) de instncia, s com a denio o compilador pode calcular o espao de memria ocupado pelas instncias dessa classe C++. Assim, no possvel denir variveis de uma classe C++ sem que a classe tenha sido denida antes, embora se possam denir referncias e ponteiros (a ver no Captulo 11) para essa classe C++. A declarao em sentido estrito usa-se principalmente: 1. Quando se esto a denir duas classes C++ que se usam mutuamente, mas em que nenhuma pode ser considerada mais importante que a outra (e portanto nenhuma embutida). Note-se que pelo menos uma das classes C++ no pode ter atributos de instncia da outra classe C++. Por exemplo:
class B; class A { public: A() { } A(const B&); // Declarao de B essencial! ... }; class B { public: B() { } ... private: A m[10]; // Denio de A essencial! };

2. Quando se est a denir uma classe C++ com outra embutida e se pretendem separar as duas denies por uma questo de clareza. Por exemplo:
class ListaDeInt { public: ... class Iterador; class IteradorConstante; ... }; class ListaDeInt::Iterador { ... };

9.3. LIGAO DOS NOMES


class ListaDeInt::IteradorConstante { ... };

489

importante tambm perceber que a denio de uma classe C++ no inclui a denio das suas operaes, i.e., no inclui os respectivos mtodos, excepto se esta denio for feita internamente classe C++. Assim, pode-se distinguir entre a denio de uma classe e a denio completa de uma classe C++, que inclui a denio de todos os seus membros. Podem-se denir tipo enumerados ou mesmo classes C++ dentro de um bloco de instrues, embora com algumas restries. Por exemplo, o seguinte cdigo
#include <iostream> using namespace std; int main() { class Mdia { public: Mdia() : soma_dos_valores(0.0), nmero_de_valores(0) { } Mdia& operator < < (double const novo_valor) { soma_dos_valores += novo_valor; ++nmero_de_valores; return *this; } operator double () const { assert(nmero_de_valores != 0); return soma_dos_valores / nmero_de_valores; } private: double soma_dos_valores; int nmero_de_valores; }; Mdia m; m < < 3 < < 5 < < 1; cout < < m < < endl; }

490

CAPTULO 9. MODULARIZAO DE ALTO NVEL

dene uma classe Mdia18 localmente funo main()19. Note-se que no se pretende aqui defender este tipo de prtica: so muito raros os casos em que se justica denir uma classe localmente e este no certamente um deles! Uma classe ou tipo enumerado local no tem ligao. A sua utilizao restringe-se ao bloco em que foi denido. Assim, a classe Mdia s pode ser usada no programa acima dentro da funo main(). Classes denidas dentro de outras classes, as chamadas classes embutidas, ou tipos enumerados denidos dentro de classes, tm a mesma ligao que a classe que as envolve. Finalmente, classes ou tipos enumerados denidos no contexto global tm sempre ligao externa. Note-se que, ao contrrio do que sucede com as funes e procedimentos, a mesma classe ou tipo enumerado pode ser denido em mltiplas unidades de traduo de um mesmo programa: o fusor s verica as faltas ou duplicaes de denies de rotinas globais e de variveis ou constantes globais. Isto acontece porque a informao presente na denio de uma classe necessria na sua totalidade durante a compilao, com excepo das denies de operaes que no em-linha, que o compilador no precisa de conhecer. Alis, a informao presente na denio de uma classe crucial para o compilador poder fazer o seu papel, ao contrrio do que acontece, por exemplo, com as rotinas, que para poderem ser usadas tm apenas de ser declaradas. por esta razo que as classes no so apenas declaradas nos cheiros de interface: so tambm denidas. Desse modo, atravs da directiva #include, a denio de uma classe ser includa em todas as unidades de traduo de dela necessitam. A regra de denio nica tem, por isso, uma verso especial para as classes e enumerados: uma classe ou tipo enumerado com ligao externa pode ser denido em diferentes unidades de traduo desde que essas denies sejam rigorosamente equivalentes. justamente para garantir essa equivalncia que as denies de classes e enumerados so colocadas em cheiros de interface.

9.4 Contedo dos cheiros de interface e implementao


Que ferramentas se devem colocar em cada mdulo? algo difcil responder de uma forma taxativa a esta pergunta. Mas pode-se dizer que cada mdulo deve corresponder a um conjunto muito coeso de rotinas, classes, etc. vulgar um mdulo conter apenas uma classe e algumas rotinas associadas intimamente a essa classe. Por vezes h mais do que uma classe, quando essas classes esto muito interligadas, quando cada uma delas no faz sentido sem a outra. Ou ento vrias pequenas classes independentes mas relacionadas conceptualmente. Outras vezes vezes um mdulo no denir classe nenhuma, como poderia suceder, por exemplo, com um mdulo de funes matemticas.
A classe Mdia dene um operador de converso implcita para double. Na denio destes operadores de converso no necessrio (nem permitido) colocar o tipo de devoluo, pois este igual ao tipo indicado aps a palavra-chave operator. este conversor que permite inserir uma instncia da classe Mdia num canal de sada sem qualquer problema: essa instncia convertida para um double, que se pode inserir no canal, sendo para isso calculada a mdia dos valores inseridos atravs do operador < <. 19 Esta curiosa possibilidade de denir classes locais permite simular a denio de rotinas locais, que o C++ no suporta. Se est preparado para alguns pontaps na gramtica, veja a Seco C.2.
18

9.4. CONTEDO DOS FICHEIROS DE INTERFACE E IMPLEMENTAO

491

Depois de escolhidas e desenvolvidas as ferramentas que constituem cada mdulo fsico de um programa, h que decidir para cada mdulo o que deve car no seu cheiro de interface (.H) e o que deve car no seu cheiro de implementao (.C). A ideia geral que o cheiro de interface deve conter o que for estritamente necessrio para que as ferramentas denidas pelo mdulo possam ser usadas em qualquer outro mdulo por simples incluso do correspondente cheiro de interface. Ou seja, tipicamente o cheiro de interface declara o que se dene no cheiro de implementao. Algumas das ferramentas denidas num mdulo so de utilidade apenas dentro desse mdulo. Essas ferramentas devem car inacessveis do exterior, no dando-lhes ligao interna, conforme explicado nas seces anteriores, mas usando espaos nominativos sem nome, ou annimos, como sugerido na Seco 9.3.2. Todas as outras ferramentas (com excepo de alguns tipos de constantes) devero ter ligao externa. As prximas seces apresentam algumas regras gerais acerca do que deve ou no constar em cada um dos cheiros fonte de um mdulo.

9.4.1 Relao entre interface e implementao


Para que o compilador possa vericar se o contedo do cheiro de interface corresponde ao contedo do respectivo cheiro de implementao, o cheiro de implementao comea sempre por incluir o cheiro de interface: mdulo.C
#include "mdulo.H" // ou <mdulo.H> ...

Esta incluso deve ser feita antes de qualquer outra incluso necessria na cheiro de implementao. Dessa forma car claro se faltar alguma incluso no prprio cheiro de interface: se isso acontecer, o compilador queixar-se- quando encontrar no cheiro de interface referncias a ferramentas que desconhece.

9.4.2 Ferramentas de utilidade interna ao mdulo


Denem-se no cheiro de implementao (.C) dentro de um espao nominativo sem nome: mdulo.C
namespace { // Denio de ferramentas internas ao mdulo, invisveis do exterior: ... }

492

CAPTULO 9. MODULARIZAO DE ALTO NVEL

9.4.3 Rotinas no-membro


As rotinas no-membro que no sejam em-linha devem ser declaradas no cheiro de interface (.H) e denidas no cheiro de implementao (.C): mdulo.H
// Declarao de rotinas no-membro e que no sejam em-linha: tipo nome(parmetros...);

mdulo.C
// Denio de rotinas no-membro e que no sejam em-linha: tipo nome(parmetros...) { ... // corpo. }

As rotinas no-membro em-linha devem ser denidas apenas no cheiro de interface (.H): mdulo.H
// Denio de rotinas no-membro e em-linha: inline tipo nome(parmetros...) { ... // corpo. }

Alternativamente, ou melhor, desejavelmente, as rotinas no-membro em-linha podem ser denidas num terceiro cheiro do mdulo, como descrito na Seco 9.4.15.

9.4.4 Variveis globais


As variveis globais no devem ser usadas.

9.4.5 Constantes globais


As constantes sero tratadas de forma diferente consoante o seu tamanho. Se o tipo da constante for um tipo bsico, uma matriz ou uma classe muito simples, ento dever ser denida com ligao interna no cheiro de interface (.H). Desse modo, sero denidas tantas constantes todas iguais quantos os mdulos em que esse cheiro de interface for includo. Compreendese assim que a constante deva ser pequena, para evitar programas ocupando demasiada memria, e rpida de construir, para evitar inecincias. mdulo.H

9.4. CONTEDO DOS FICHEIROS DE INTERFACE E IMPLEMENTAO


// Denio de constantes pequenas e rpidas: tipo const nome1; // valor por omisso. tipo const nome2 = expresso; tipo const nome3(argumentos);

493

Se o tipo da constante no vericar estas condies, ento a constante dever ser denida com ligao externa no cheiro de implementao (.C) e declarada no cheiro de interface (.H). mdulo.H
// Declarao de constantes grandes ou lentas: extern tipo const nome1; extern tipo const nome2; extern tipo const nome3;

mdulo.C
#include "mdulo.H" ... // Denio de constantes grandes ou lentas: tipo const nome1; // valor por omisso. tipo const nome2 = expresso; tipo const nome3(argumentos);

Note-se que no necessrio o especicador extern no cheiro de implementao porque este inclui sempre o respectivo cheiro de interface, que possui a declarao das constantes como externas.

9.4.6 Tipos enumerados no-membro


Os tipos enumerados tm de ser denidos no cheiro de interface (.H). mdulo.H
// Denies de enumerados: enum Nome { ... };

494

CAPTULO 9. MODULARIZAO DE ALTO NVEL

9.4.7 Classes C++ no-membro


As classes C++, por razes que j se viram atrs, devem ser denidas apenas no cheiro de interface (.H). mdulo.H
// Denio de classes: class Nome { ... // aqui tanto quanto possvel s declaraes. };

9.4.8 Operaes (rotinas membro)


Os mtodos so implementao das operaes de uma classe C++. Podem ser denidos dentro da denio da classe, o que os torna automaticamente em-linha. Mas no recomendvel faz-lo, pois isso leva a uma mistura de implementao com interface que no facilita a leitura do cdigo. Assim, as operaes, quer de classe quer de instncia, so sempre declaradas dentro da classe e portanto no cheiro de interface (.H). Os correspondentes mtodos sero denidos fora da classe. No caso dos mtodos que no so em-linha, a denio far-se- no cheiro de implementao (.C). No caso dos mtodos em-linha, a denio far-se- no cheiro de interface. mdulo.H
class Nome { ... // Declaraes de mtodos: tipo nome1(parmetros...); // membro de instncia. tipo nome2(parmetros...); // membro de instncia. static tipo nome3(parmetros...); // membro de classe. static tipo nome4(parmetros...); // membro de classe. ... }; // Denies de mtodos em-linha: inline tipo Nome::nome2(parmetros...) { // membro de instncia. ... // corpo. }

9.4. CONTEDO DOS FICHEIROS DE INTERFACE E IMPLEMENTAO

495

inline tipo Nome::nome4(parmetros...) { // membro de classe. ... // corpo. }

mdulo.C
// Denies de mtodos que no so em-linha: tipo Nome::nome1(parmetros...) // membro de instncia. ... // corpo. } tipo Nome::nome3(parmetros...); // membro de classe. ... // corpo. }

Alternativamente, os mtodos em-linha podem ser denidos num terceiro cheiro do mdulo, como descrito na Seco 9.4.15.

9.4.9 Atributos de instncia


Os atributos (variveis e constantes) de instncia so denidos dentro da prpria classe C++ e inicializados pelos seus construtores, pelo que no requerem qualquer tratamento especial.

9.4.10 Variveis membro de classe


As variveis membro de classe declaram-se dentro da classe C++, e portanto no cheiro de interface (.H), e denem-se no cheiro de implementao (.C). mdulo.H
class Nome { ... // Declaraes de variveis membro de classe: static tipo nome; ... };

mdulo.C
// Denies de variveis membro de classe (alternativas): tipo Nome::nome; // valor por omisso.

496

CAPTULO 9. MODULARIZAO DE ALTO NVEL

tipo Nome::nome = expresso; tipo Nome::nome(argumentos);

9.4.11 Constantes membro de classe


As constantes membro de classe tratam-se exactamente como as variveis membro de classe: declaram-se na classe C++, e portanto no cheiro de interface (.H), e denem-se no cheiro de implementao (.C). mdulo.H
class Nome { ... // Declaraes de constantes membro de classe: static tipo const nome; ... };

mdulo.C
// Denies de constantes membro de classe: tipo const Nome::nome; // valor por omisso. tipo const Nome::nome = expresso; tipo const Nome::nome(argumentos);

Uma excepo a esta regra so as constantes membro de classe de tipos aritmticos inteiros. Estas podem ser denidas dentro da classe C++ e usadas, por isso, para indicar a dimenso de matrizes membro. Por exemplo: mdulo.H
class Nome { ... // Declaraes de constantes membro de classe de tipos aritmticos inteiros: static int const dimenso = 100; ... int matriz[dimenso]; ... };

9.4. CONTEDO DOS FICHEIROS DE INTERFACE E IMPLEMENTAO

497

9.4.12 Classes C++ membro (embutidas)


As classes C++ membro de outras classes, ou seja, embutidas, podem ser denidas dentro da classe C++ que as envolve. Mas muitas vezes prefervel, embora nem sempre possvel, deni-las parte. Em qualquer dos casos a denio ser feita dentro do mesmo cheiro de interface (.H) da classe C++ envolvente. Aos membros de classes C++ embutidas aplicam-se os mesmos comentrios que aos membros da classe C++ envolvente. Forma recomendada: mdulo.H
class Nome { ... // Declaraes de classes embutidas: class OutroNome; ... }; ... class Nome::OutroNome { ... };

Alternativa: mdulo.H
class Nome { ... // Denies de classes embutidas: class OutroNome { ... }; ... };

9.4.13 Enumerados membro


Os tipos enumerados membro tm de ser denidos dentro da classe C++ e portanto no cheiro de interface (.H). mdulo.H

498

CAPTULO 9. MODULARIZAO DE ALTO NVEL


class Data { ... // Denies de enumerados embutidos: enum DiaDaSemana { primeiro, segunda-feira = primeiro, tera_feira, ... domingo, ultimo = domingo }; ... };

9.4.14 Evitando erros devido a incluses mltiplas


A implementao da modularizao fsica em C++, sendo bastante primitiva, pe alguns problemas. Suponha-se que se est a desenvolver um programa dividido em trs mdulos. Dois deles, A e B, disponibilizam ferramentas. O terceiro, C, contm o programa principal e faz uso de ferramentas de A e B, sendo constitudo apenas por um cheiro de implementao (C.C) que inclui os cheiros de interface dos outros mdulos (A.H e B.H): C.C
#include "A.H" #include "B.H" ... Restante contedo de C.C.

Suponha-se ainda que o cheiro de interface do mdulo B necessita de algumas declaraes do mdulo A. Nesse caso: B.H
#include "A.H" ... Restante contedo de B.H.

Suponha-se nalmente que o cheiro A.H dene uma classe (o argumento seria o mesmo com um tipo enumerado, uma rotina em-linha ou uma constante): A.H

9.4. CONTEDO DOS FICHEIROS DE INTERFACE E IMPLEMENTAO


class A { ... };

499

Que acontece quando se pr-processa o cheiro C.C? O resultado a unidade de traduo C.ii: C.ii
# 1 "C.C" # 1 "A.H" 1 class A { ... }; # 2 "C.C" 2 # 2 "B.H" 1 # 1 "A.H" 1 class A { ... }; # 2 "B.H" 2 ... Restante contedo de B.H. # 3 "C.C" 2 ... Restante contedo de C.C.

Este cheiro contm duas denies da classe A, o que viola a regra da denio nica, pelo que no se poder compilar com sucesso. O problema deve-se incluso mltipla do contedo do cheiro A.H na unidade de traduo C.ii. Como resolver o problema? O pr-processador fornece uma forma ecaz de evitar este problema recorrendo a compilao condicional. Para isso basta envolver todo o cdigo dentro dos cheiros de interface da seguinte forma: mdulo.H
#ifndef MDULO_H #define MDULO_H ... // Contedo do cheiro de interface. #endif // MDULO_H

Desta forma, na segunda incluso do cheiro j estar denida a macro MDULO_H, e portanto o cdigo no ser colocado segunda vez na unidade de traduo. A macro deve ter um nome

500

CAPTULO 9. MODULARIZAO DE ALTO NVEL

nico entre todos os mdulos, de modo a que este mecanismo no entre em conito com o mesmo mecanismo noutros mdulos tambm usados. No fcil garantir que o nome seja nico, o que revela quo primitivo o modelo de modularizao fsica do C++. Uma das formas de tentar garantir unicidade no nome das macros anexar-lhes os nomes dos pacotes a que o mdulo pertence, como se ver mais frente.

9.4.15 Ficheiro auxiliar de implementao


Os cheiros de interface, como se viu, contm mais informao do que aquela que, em rigor, corresponde interface das ferramentas disponibilizadas pelo mdulo correspondente. Em particular as rotinas e mtodos em-linha so totalmente denidos no cheiro de interface. Para o programador consumidor de um mdulo, a denio exacta destas rotinas irrelevante. A sua presena no cheiro de interface acaba at por ser um factor de distraco. Uma soluo comum para este problema passa por utilizar um terceiro tipo de cheiro em cada mdulo: o cheiro auxiliar de implementao. Sugere-se, pois, que os mdulos sejam constitudos por (no mximo) trs cheiros fonte: 1. Ficheiro de interface (.H) Com o contedo sugerido nas seces anteriores, mas sem denies de rotinas e mtodos em-linha e contendo as declaraes de todas as rotinas nomembro, independentemente de serem ou no em-linha. Este cheiro deve terminar com a incluso do respectivo cheiro auxiliar de implementao (terminado em _impl.H). 2. Ficheiro auxiliar de implementao (_impl.H) Com a denio todas as rotinas e mtodos em-linha. 3. Ficheiro de implementao (.C) - com o contedo sugerido anteriormente. Desta forma a complexidade do cheiro de interface reduz-se consideravelmente, o que facilita a sua leitura. A mesma soluo pode ser usada para resolver o problema da denio de rotinas e mtodos modelo quando se usa compiladores que no suportam a palavra-chave export. Ver Captulo 13 para mais pormenores.

9.5 Construo automtica do cheiro executvel


A construo do cheiro executvel de um programa constitudo por vrios mdulos exige uma sequncia de comandos (pelo menos se se quiser tirar partido da compilao separada, bem entendido). Em ambientes Unix (como o Linux) possvel automatizar a construo usando o construtor, que um programa chamado make, e, se necessrio, um cheiro de construo. O construtor tem um conjunto de regras implcitas que lhe permitem construir automaticamente pequenos programas. Infelizmente, est especializado para a linguagem C, pelo que so necessrios alguns passos de congurao para usar a linguagem C++. Em particular,

9.5. CONSTRUO AUTOMTICA DO FICHEIRO EXECUTVEL

501

necessrio indicar claramente quais so os programas e respectivas opes que devem ser usados para pr-processar e compilar e para fundir. Para isso necessrio atribuir valores variveis de ambiente CXX, que guarda o nome do programa usado para pr-processar e compilar, CC, que guarda o nome do programa usado para fundir, CXXFLAGS, que guarda as opes de compilao desejadas, e LDLIBS, que indica as bibliotecas a usar nas fuses. Se se estiver a usar o interpretador de comandos /bin/tcsh, tal pode ser conseguido atravs dos comandos:
setenv setenv setenv setenv CC c++ CXX c++ CXXFLAGS "-g -Wall -ansi -pedantic" LDLIBS ""

Estes comandos tambm podem ser colocados no cheiro de congurao do interpretador de comandos: ~/.tcshrc20. Depois das conguraes anteriores, fcil construir pequenos programas constitudos apenas por um cheiro de implementao: ol.C
#include <iostream> using namespace std; int main() { cout < < "Ol mundo!" < < endl; }

Para construir o cheiro executvel basta dar o comando:


make ol

Em casos mais complicados, quando existem vrios mdulos num programa, necessrio criar um cheiro de construo (makele), de nome Makefile. Este cheiro, cujo contedo pode ser muito complicado, consiste nas suas verses mais simples num conjunto de regras de dependncia. Estas dependncias tm o seguinte formato:
alvo: dependncia dependncia...
Essas alteraes s tm efeito depois de se voltar a entrar no interpretador ou depois de se dar o comando source ~/.tcshrc na consola em causa.
20

502

CAPTULO 9. MODULARIZAO DE ALTO NVEL

Estas regras servem para indicar ao construtor que dependncias existem entre os cheiros do programa, dependncias essas que o construtor no sabe adivinhar. A melhor forma de perceber os cheiros de construo estudar um exemplo. Suponha-se que um projecto deve construir dois cheiros executveis, um para calcular mdias de alunos e outro para saber a nota de um dado aluno, e que esse projecto constitudo por trs mdulos, respectivamente aluno, mdia e procura. Para simplicar o exemplo, no se usar o cheiro auxiliar de implementao sugerido na seco anterior, pelo que cada mdulo ter no mximo dois cheiros fonte. O primeiro mdulo, aluno, dene ferramentas para lidar com alunos e respectivas notas, e possui dois cheiros fonte: aluno.C e aluno.H. Os outros mdulos, mdia e procura, denem os programas para clculo de mdias e pesquisa de alunos, e portanto consistem apenas num cheiro de implementao cada um (mdia.C e procura.C). Os cheiros fonte so21 : aluno.H
#ifndef ALUNO_H #define ALUNO_H #include <string> #include <iostream> bool Positiva(int nota); const int nota_mnima_aprovao = 10; const int nota_mxima = 20; class Aluno { public: Aluno(std::string const& nome, int nmero, int nota = 20); std::string const& nome() const; int nmero() const; int nota() const; private: std::string nome_; int nmero_; int nota_; };
Optou-se por no documentar o cdigo para no o tornar exageradamente longo. Optou-se tambm por eliminar a vericao de erros do utilizador, pela mesma razo. Finalmente, distinguiu-se algo articialmente entre mtodos e rotinas em linha e no-em linha. A razo foi garantir que o cheiro de implementao aluno.C no casse vazio.
21

9.5. CONSTRUO AUTOMTICA DO FICHEIRO EXECUTVEL


inline Aluno::Aluno(std::string const& nome, int const nmero, int const nota) : nome_(nome), nmero_(nmero), nota_(nota) { assert(0 <= nota and nota <= nota_mxima); } std::ostream& operator < < (std::ostream& sada, Aluno const& aluno); #endif // ALUNO_H

503

aluno.C
#include "aluno.H" using namespace std; bool Positiva(int const nota) { return nota >= nota_mnima_aprovao; } string const& Aluno::nome() const { return nome_; } int Aluno::nmero() const { return nmero_; } int Aluno::nota() const { return nota_; } ostream& operator < < (ostream& sada, Aluno const& aluno) { return sada < < aluno.nome() < < < < aluno.nmero() < < < < aluno.nota(); }

mdia.C
#include <iostream> #include <string>

504
#include <vector> using namespace std; #include "aluno.H"

CAPTULO 9. MODULARIZAO DE ALTO NVEL

int main() { cout < < "Introduza nmero de alunos: "; int nmero_de_alunos; cin > > nmero_de_alunos; cout < < "Introduza os alunos (nome nmero nota):" < < endl; vector<Aluno> alunos; for(int i = 0; i != nmero_de_alunos; ++i) { string nome; int nmero; int nota; cin > > nome > > nmero > > nota; alunos.push_back(Aluno(nome, nmero, nota)); } double soma = 0.0; for(int i = 0; i != nmero_de_alunos; ++i) soma += alunos[i].nota(); cout < < "A mdia : " < < soma / nmero_de_alunos < < " valores." < < endl; }

procura.C
#include <iostream> #include <string> #include <vector> using namespace std; #include "aluno.H" int main() { vector<Aluno> alunos; alunos.push_back(Aluno("Z", 1234, 15)); alunos.push_back(Aluno("Zacarias", 666, 20));

9.5. CONSTRUO AUTOMTICA DO FICHEIRO EXECUTVEL


alunos.push_back(Aluno("Marta", 5465, 18)); alunos.push_back(Aluno("Maria", 1111, 14)); cout < < "Pauta:" < < endl; for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i) cout < < alunos[i] < < endl; cout < < "De que aluno deseja saber a nota? "; string nome; cin > > nome; for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i) if(alunos[i].nome() == nome) { cout < < "O aluno " < < nome < < " teve " < < alunos[i].nota() < < endl; if(not Positiva(alunos[i].nota())) cout < < "Este aluno reprovou." < < endl; } }

505

Observando estes cheiros fonte, fcil vericar que: 1. Sempre que um dos cheiros fonte do mdulo aluno for alterado, o respectivo cheiro objecto deve ser produzido de novo, pr-processando e compilando o respectivo cheiro de implementao. Como o construtor sabe que os cheiros objecto dependem dos cheiros de implementao respectivos, s necessrio indicar explicitamente que o cheiro objecto depende do cheiro de interface. Ou seja, deve-se colocar no cheiro de construo a seguinte linha:
aluno.o: aluno.H

2. O cheiro de implementao do mdulo mdia inclui o cheiro de interface aluno.H. Assim, necessrio indicar explicitamente que o cheiro objecto mdia.o depende do cheiro de interface aluno.H:
mdia.o: aluno.H

3. O mesmo se passa para o mdulo procura:


procura.o: aluno.H

4. Finalmente, necessrio indicar que os cheiros executveis so obtidos, e portanto dependem, de determinados cheiros objecto:
mdia: mdia.o aluno.o procura: procura.o aluno.o

506 Assim, o cheiro de construo car: Makefile

CAPTULO 9. MODULARIZAO DE ALTO NVEL

mdia: mdia.o aluno.o procura: procura.o aluno.o aluno.o: aluno.H mdia.o: aluno.H procura.o: aluno.H

As trs ltimas regras podem ser geradas automaticamente pelo compilador se se usar a opo -MM. Sugere-se que o cheiro de construo seja inicializado da seguinte forma:
c++ -MM aluno.C mdia.C procura.C > Makefile

O resultado ser o cheiro: Makefile


aluno.o: aluno.C aluno.H mdia.o: mdia.C aluno.H procura.o: procura.C aluno.H

onde faltam as regras de construo dos cheiros executveis, que tm de ser acrescentadas mo. As regras obtidas automaticamente pelo comando c++ -MM indicam explicitamente que os cheiros objecto dependem dos cheiros de implementao, o que redundante, pois o construtor sabe dessa dependncia. Com o cheiro de construo sugerido, construir o programa mdia, por exemplo, fcil. Basta dar o comando
make mdia

que o construtor make se encarregar de pr-processar, compilar e fundir apenas aquilo que for necessrio para construir o cheiro executvel mdia. O construtor tenta sempre construir os alvos que lhe forem passados como argumento. Se se invocar o construtor sem quaisquer argumentos, ento ele tentar construir o primeiro alvo indicado no cheiro de construo. Assim, pode-se acrescentar uma regra inicial ao cheiro de construo de modo a que o construtor tente construir os dois executveis quando no lhe forem passados argumentos: Makefile

9.6. MODULARIZAO EM PACOTES


all: mdia procura mdia: mdia.o aluno.o procura: procura.o aluno.o aluno.o: aluno.H mdia.o: aluno.H procura.o: aluno.H

507

Neste caso para construir os dois executveis basta dar o comando:


make

9.6 Modularizao em pacotes


J se viu atrs que o nvel de modularizao acima da modularizao fsica o nvel de pacote. Um pacote constitudo normalmente pelas ferramentas de vrios mdulos fsicos. Em C++ no existe suporte directo para a noo de pacote. A aproximao utilizada consiste na colocao dos vrios mdulos fsicos de um pacote num espao nominativo comum ao pacote. A noo de espao nominativo descrita brevemente nas prximas seces.

9.6.1 Coliso de nomes


Um problema comum em grandes projectos a existncia de entidades (classes C++, instncias, rotinas, etc.) com o mesmo nome, embora desenvolvidos por pessoas, equipas ou empresas diferentes. Quando isto acontece diz-se que ocorreu uma coliso de nomes. Por exemplo, suponha-se que se est a desenvolver uma aplicao de gesto. Pode-se decidir comprar duas bibliotecas de ferramentas, uma com ferramentas de contabilidade, outra de logstica. Esta uma opo acertada a maior parte das vezes: provavelmente mais rpido e barato comprar as bibliotecas a terceiros do que desenvolver as respectivas ferramentas de raiz. Suponha-se que os fornecedores dessas bibliotecas so duas empresas diferentes e que estas foram fornecidas no formato de cheiros de interface e cheiros de arquivo contendo os cheiro objecto de cada biblioteca. Suponha-se ainda que ambas as bibliotecas denem um procedimento com a mesma assinatura22 :
void consolida();

Simplicando, os cheiros fornecidos por cada empresa so:


22

Fica a cargo do leitor imaginar o que cada um desses dois procedimentos faz.

508 Biblioteca de logstica

CAPTULO 9. MODULARIZAO DE ALTO NVEL

liblogstica.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto produtos.o, que dene (entre outros) o procedimento consolida(). produtos.H
#ifndef PRODUTOS_H #define PRODUTOS_H ... void consolida(); ... #endif // PRODUTOS_H

outros cheiros de interface Biblioteca de contabilidade libcontabilidade.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto balano.o, que dene (entre outros) o procedimento consolida(). balano.H
#ifndef BALANO_H #define BALANO_H ... void consolida(); ... #endif // BALANO_H

outros cheiros de interface Suponha-se que a aplicao em desenvolvimento contm um mdulo usa.C com apenas a funo main() e contendo uma invocao do procedimento consolida() da biblioteca de contabilidade: usa.C
#include <balano.H> int main() { consolida(); }

9.6. MODULARIZAO EM PACOTES

509

A construo do programa consiste simplesmente em pr-processar e compilar os cheiros de implementao (entre os quais usa.C) e em seguida fundir os cheiros objecto resultantes com os cheiros de arquivo das duas bibliotecas (admite-se que algum outro mdulo faz uso de ferramentas da biblioteca de logstica):
c++ -Wall -ansi -pedantic -g -c *.C c++ -o usa *.o -llogstica -lcontabilidade

Que sucede? O inesperado: o executvel gerado sem problemas e quando se executa vericase que o procedimento consolida() executado o da biblioteca de logstica! Porqu? Porque o fusor no se importa com duplicaes nos cheiros de arquivo. Os cheiros de arquivo so pesquisados pela ordem pela qual so indicados no comando de fuso e a pesquisa pra quando a ferramenta procurada encontrada. Como se colocou -llogstica antes de -lcontabilidade, o procedimento encontrado o da biblioteca de logstica... Mas h um problema mais grave. E se se quiser invocar ambos os procedimentos na funo main()? impossvel distingui-los. Como resolver este problema de coliso de nomes? H vrias possibilidades, todas ms: 1. Pedir a um dos fornecedores para alterar o nome do procedimento. uma m soluo. Sobretudo porque provavelmente a empresa fornecedora tem outros clientes e no se pode dar ao luxo de ter uma verso diferente da biblioteca para cada cliente. Provavelmente protegeu-se de semelhantes problemas no contrato de fornecimento e recusar o pedido... 2. Esquecer, para cada nome em coliso, a verso da biblioteca contabilidade, e desenvolver essas ferramentas atribuindo-lhes outros nomes. reinventar a roda, e isso custa tempo e dinheiro.

9.6.2 Espaos nominativos


A soluo escolher fornecedores que garantam um mximo de proteco a priori contra a possibilidade de colises de nomes. Cada uma das empresas, se fosse competente, deveria ter colocado o seu cdigo dentro de um espao nominativo apropriado. Neste caso as escolhas acertadas seriam os espaos nominativos Logstica e Contabilidade (os nomes dos espaos nominativos comeam tipicamente por maiscula, como os das classes). Nesse caso as bibliotecas fornecidas consistiriam em: Biblioteca de logstica liblogstica.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto produtos.o, que dene (entre outros) o procedimento Logstica::consolida(). produtos.H

510
#ifndef PRODUTOS_H #define PRODUTOS_H

CAPTULO 9. MODULARIZAO DE ALTO NVEL

namespace Logstica { ... void consolida(); ... } #endif // PRODUTOS_H

outros cheiros de interface Biblioteca de contabilidade

libcontabilidade.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto balano.o, que dene (entre outros) o procedimento Contabilidade::consoli balano.H
#ifndef BALANO_H #define BALANO_H namespace Contabilidade { ... void consolida(); ... } #endif // BALANO_H

outros cheiros de interface O efeito prtico da construo


namespace Nome { ... }

o de fazer com que todos os nomes declarados dentro das chavetas passem a ter o prexo Nome::. Assim, com estas novas verses das bibliotecas, passaram a existir dois procedimentos diferentes com diferentes nomes: Logstica::consolida() e Contabilidade::consolida(). Agora a funo main() ter de indicar o nome completo do procedimento: usa.C

9.6. MODULARIZAO EM PACOTES


#include <balano.H> int main() { Contabilidade::consolida(); }

511

9.6.3 Directivas de utilizao


Para evitar ter de preceder o nome das ferramentas da biblioteca de contabilidade de Contabilidade::, pode-se usar uma directiva de utilizao: usa.C
#include <balano.H> using namespace Contabilidade; int main() { consolida(); }

Uma directiva de utilizao injecta todos os nomes do espao nominativo indicado no espao nominativo corrente. Note-se que, quando uma entidade no est explicitamente dentro de algum espao nominativo, ento est dentro do chamado espao nominativo global, sendo o seu prexo simplesmente ::, que pode ser geralmente omitido. Assim, no cdigo acima a directiva de utilizao injecta o nome consolida() no espao nominativo global, pelo que este pode ser usado directamente. Pode-se simultaneamente usar o procedimento consolida() da biblioteca de logstica, bastando para isso indicar o seu nome completo: usa.C
#include <balano.H> #include <produtos.H> using namespace Contabilidade; int main() { consolida(); Logstica::consolida(); }

512

CAPTULO 9. MODULARIZAO DE ALTO NVEL

mesmo possvel injectar todos os nomes de ambos os espaos nominativos no espao nominativo global. Nesse caso os nomes duplicados funcionam como se estivessem sobrecarregados, se forem nomes de rotinas. I.e., se existirem duas rotinas com o mesmo nome, ento tero de ter assinaturas (nmero e tipo dos parmetros) diferentes. Se tiverem a mesma assinatura, ento uma tentativa de usar o nome sem o prexo apropriado gera um erro de compilao, uma vez que o compilador no sabe que verso invocar. Por exemplo: usa.C
#include <balano.H> #include <produtos.H> using namespace Contabilidade; using namespace Logstica; int main() { consolida(); // ambguo! Logstica::consolida(); }

Um possvel soluo usa.C


#include <balano.H> #include <produtos.H> using namespace Contabilidade; using namespace Logstica; int main() { Contabilidade::consolida(); Logstica::consolida(); }

onde se usaram nomes completos para discriminar entre as entidades com nomes e assinaturas iguais em ambos os espaos nominativos.

9.6.4 Declaraes de utilizao


A utilizao de directivas de utilizao tem a consequncia nefasta de injectar no espao nominativo corrente todos os nomes declarados no espao nominativo indicado. Por isso, deve-se

9.6. MODULARIZAO EM PACOTES

513

usar directivas de utilizao com conta peso e medida nos cheiros de implementao e nunca nos cheiros de interface. Uma alternativa menos drstica que as directivas so as declaraes de utilizao. Se se pretender injectar no espao nominativo corrente apenas um nome, pode-se usar uma declarao de utilizao: usa.C
#include <balano.H> #include <produtos.H> int main() { using Contabilidade::consolida; consolida(); Logstica::consolida(); }

Nas declaraes de utilizao apenas se indica o nome da entidade: no caso de uma rotina, por exemplo, um erro indicar o cabealho completo.

9.6.5 Espaos nominativos e modularizao fsica


Podem-se colocar vrios mdulos num mesmo espao nominativo 23 . tpico dividir uma biblioteca, por exemplo, num conjunto de pacotes com funes diversas, atribuindo a cada pacote um espao nominativo, e consistindo cada pacote num conjunto de mdulos fsicos. No exemplo apresentado o problema da coliso de nomes no foi totalmente afastado. Foi simplesmente minimizado. que as duas bibliotecas poderiam, com azar, usar o mesmo nome para um dos seus espaos nominativos. Para reduzir ainda mais essa possibilidade, conveniente que todos os pacotes de uma empresa pertenam a um super-pacote com o nome da empresa. Suponha-se que a biblioteca de contabilidade foi fornecido pela empresa Vero Software, Lda. e que a biblioteca de logstica foi fornecido pela empresa Inverno Software, Lda. Outra fonte de possveis colises so os nomes das macros usadas para evitar os erros associados a incluses mltiplas. Como at agora se convencionou que estas macros seriam baseadas apenas no nome do respectivo mdulo, podem surgir problemas graves se existirem dois mdulos com o mesmo nome nas duas bibliotecas. O problema pode ser resolvido convencionando que as macros incluem tambm os nomes dos pacotes a que os mdulos pertencem. Se ambas as empresas levando em conta os problemas acima, os cheiros fornecidos poderiam ser: Biblioteca de logstica
23

E vice-versa, mas no to til...

514

CAPTULO 9. MODULARIZAO DE ALTO NVEL

liblogstica.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto produtos.o, que dene (entre outros) o procedimento InvernoSoftware::Logstica produtos.H
#ifndef INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H #define INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H namespace InvernoSoftware { namespace Logstica { ... void consolida(); ... } } #endif // INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H

outros cheiros de interface Biblioteca de contabilidade

libcontabilidade.a Ficheiro de arquivo da biblioteca. Contm (entre outros) o cheiro objecto balano.o, que dene (entre outros) o procedimento VeroSoftware::Contabi balano.H
#ifndef VERAOSOFTWARE_CONTABILIDADE_BALANO_H #define VERAOSOFTWARE_CONTABILIDADE_BALANO_H namespace VeroSoftware { namespace Contabilidade { ... void consolida(); ... } } #endif // VERAOSOFTWARE_CONTABILIDADE_BALANO_H

outros cheiros de interface Os espaos nominativos podem, portanto, ser organizados hierarquicamente, pelo que um pacote pode conter outros pacotes para alm das suas ferramentas. Neste caso a utilizao poderia ter o seguinte aspecto: usa.C
#include <balano.H>

9.6. MODULARIZAO EM PACOTES


#include <produtos.H> int main() { using VeroSoftware::Contabilidade::consolida; consolida(); InvernoSoftware::Logstica::consolida(); }

515

Apresentam-se abaixo algumas regras gerais sobre o contedo dos cheiros fonte de cada mdulo quando se usam espaos nominativos.

9.6.6 Ficheiros de interface


Os cheiros de interface devem ter o contedo descrito anteriormente (ver Seco 9.4), excepto que todas as declaraes e denies sero envolvidas nos espaos nominativos necessrios, como se viu nos exemplos acima. No se devem fazer incluses dentro das chavetas de um espao nominativo, nem mesmo do cheiro auxiliar de implementao! Por outro lado, a macro usada para evitar erros associados a incluses mltiplas deve reectir no apenas o nome do mdulo, mas tambm o nome dos pacotes a que o mdulo pertena.

9.6.7 Ficheiros de implementao e cheiros auxiliares de implementao


Os cheiros de implementao e auxiliar de implementao, que contm denies de entidades apenas declaradas no cheiro de interface, no devem envolver todas as denies nos espaos nominativos a que pertencem. Para evitar erros, os nomes usados na denio devem incluir como prexo os espaos nominativos a que pertencem, para que o compilador possa garantir que esses nomes foram previamente declarados dentro desses espaos nominativos no cheiro de interface do mdulo. Por exemplo, os cheiros de implementao dos mdulos produtos (da empresa Inverno Software) e balano (da empresa Vero Software), poderiam ter o seguinte aspecto: produtos.C
#include "produtos.H" ... // outros #include. ... void InvernoSoftware::Logstica::consolida() { ... } ...

516 balano.C
#include "balano.H" ... // outros #include.

CAPTULO 9. MODULARIZAO DE ALTO NVEL

... void VeroSoftware::Contabilidade::consolida() { ... }

Note-se que a soluo anterior s funciona se as empresas pertencerem ao mesmo espao nacional: nesse caso o registo de nomes de empresas garante que no h duplicao de nomes. A nvel internacional o problema complica-se. Nesse caso pode ser uma ideia que as empresas acrescentem um espao nominativo exterior a todos os outros e que identica o pas de origem usando o cdigo ISO. Aplicando ao exemplo j apresentado, o cheiro de interface produtos.H caria
#ifndef PT_INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H #define PT_INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H namespace pt { namespace InvernoSoftware { namespace Logstica { ... void consolida(); ... } } } #endif // PT_INVERNOSOFTWARE_LOGISTICA_PRODUTOS_H

9.6.8 Pacotes e espaos nominativos


Um pacote uma unidade de modularizao. No entanto, a sua implementao custa de espaos nominativos tem um problema: no separa claramente interface de implementao. A nica separao que existe a que decorre do facto de os pacotes serem constitudos por mdulos fsicos e estes por classes, funes e procedimentos, que so nveis de modularizao em que esta separao existe. Sendo os pacotes constitudos por mdulos fsicos, seria til poder classicar os mdulos fsicos em mdulos fsicos pblicos (que fazem parte da interface do pacote) e mdulos fsicos privados (que fazem parte da implementao do pacote). Isso pode-se conseguir de uma forma indirecta colocando os mdulos fsicos privados num espao nominativo especial, com um nome pouco evidente, e no disponibilizando para os consumidores do pacote seno os cheiros de interface dos mdulos fsicos pblicos.

9.7. EXEMPLO FINAL

517

Esta soluo de fcil utilizao quando o pacote fornecido integrado numa biblioteca, visto que as bibliotecas so tipicamente fornecidas como um conjunto de cheiros de interface (e s se fornecem os que dizem respeito a mdulos fsicos pblicos) e um cheiro de arquivo (de que fazem parte os cheiros objecto de todos os mdulos, pblicos ou privados, mas nos quais as ferramentas dos mdulos privados so de mais difcil acesso, pois encontram-se inseridas num espao nominativo de nome desconhecido para o consumidor).

9.7 Exemplo nal


Apresenta-se aqui o exemplo da Seco 9.5, mas melhorado de modo a usar cheiros auxiliares de implementao e espaos nominativos e a fazer um uso realista de rotinas e mtodos emlinha. O exemplo contm um pacote de ferramentas de gesto de uma escola de nome Escola. Este pacote, implementado usando um espao nominativo, contm dois mdulos fsicos descritos abaixo: nota e aluno. Mdulos: nota Um mdulo fsico criado para conter ferramentas relacionadas com notas. Pertence ao pacote Escola. Colocaram-se todas as ferramentas dentro de um espao nominativo Nota para evitar nomes demasiado complexos para as ferramentas. Pode-se considerar, portanto, que o pacote Escola constitudo por um mdulo fsico aluno e por um pacote Nota, que por sua vez constitudo por um nico mdulo fsico nota. Os cheiros fonte do mdulo nota so (no h cheiro de implementao, pois todas as rotinas so em-linha): nota.H
#ifndef ESCOLA_NOTA_NOTA_H #define ESCOLA_NOTA_NOTA_H /// Pacote que contm todas as ferramentas de gesto da escola. namespace Escola { /// Pacote que contm ferramentas relacionadas com notas. namespace Nota { /** Devolve verdadeiro se a nota for considerada positiva. @pre 0 <= nota e nota <= mxima. @post Positiva = (mnima_de_aprovao <= nota). */ bool Positiva(int nota); /// A nota mnima para obter aprovao. int const mnima_de_aprovao = 10; /// A nota mxima.

518

CAPTULO 9. MODULARIZAO DE ALTO NVEL


int const mxima = 20; } } #include "nota_impl.H" #endif // ESCOLA_NOTA_NOTA_H

nota_impl.H
inline bool Escola::Nota::Positiva(int const nota) { return mnima_de_aprovao <= nota; }

aluno Um mdulo fsico criado para conter ferramentas relacionadas com alunos. Pertence ao pacote Escola. Os cheiros fonte do mdulo aluno so (no h cheiro de implementao, pois todas as funes e procedimentos so em-linha): aluno.H
#ifndef ESCOLA_ALUNO_H #define ESCOLA_ALUNO_H #include <string> #include <iostream> #include "nota.H" namespace Escola { /// Representa o conceito de aluno de uma escola. class Aluno { public: /// Constri um aluno dado o nome, o nmero e, opcionalmente, a nota. Aluno(string const& nome, int nmero, int nota = Nota::maxima); /// Devolve o nome do aluno. string const& nome() const; /// Devolve o nmero do aluno. int nmero() const; /// Devolve a nota do aluno. int nota() const; private: string nome_;

9.7. EXEMPLO FINAL


int nmero_; int nota_; }; /// Operador de insero de um aluno num canal. ostream& operator < < (ostream& sada, Aluno const& aluno); } #include "aluno_impl.H" #endif // ESCOLA_ALUNO_H

519

aluno_impl.H
inline Escola::Aluno::Aluno(string const& nome, int const nmero, int const nota) : nome_(nome), nmero_(nmero), nota_(nota) { assert(0 <= nota and nota <= Nota::mxima); } inline string const& Escola::Aluno::nome() const { return nome_; } inline int Escola::Aluno::nmero() const { return nmero_; } inline int Escola::Aluno::nota() const { return nota_; } inline ostream& Escola::operator < < (ostream& sada, Aluno const& aluno) { return sada < < aluno.nome() < < < < aluno.nmero() < < < < aluno.nota(); }

mdia e procura So os mdulos fsicos com as funes main() dos programas a construir. No tm cheiro de interface nem de implementao auxiliar: mdia.C
#include <iostream> #include <string> #include <vector>

520

CAPTULO 9. MODULARIZAO DE ALTO NVEL

using namespace std; #include "aluno.H" using namespace Escola; /** Programa que l informao sobre um conjunto de alunos do teclado e depois mostra a mdia das suas notas. */ int main() { cout < < "Introduza nmero de alunos: "; int nmero_de_alunos; cin > > nmero_de_alunos; cout < < "Introduza os alunos " < < "(nome nmero nota):" < < endl; vector<Aluno> alunos; for(int i = 0; i != nmero_de_alunos; ++i) { string nome; int nmero; int nota; cin > > nome > > nmero > > nota; alunos.push_back(Aluno(nome, nmero, nota)); } double soma = 0.0; for(int i = 0; i != nmero_de_alunos; ++i) soma += alunos[i].nota(); cout < < "A mdia : " < < soma / nmero_de_alunos < < " valores." < < endl; }

procura.C
#include <iostream> #include <string> #include <vector> using namespace std; #include "nota.H" #include "aluno.H" using namespace Escola;

9.7. EXEMPLO FINAL


/** Programa que mostra informao acerca de um aluno escolha do utilizador. */ int main() { vector<Aluno> alunos; alunos.push_back(Aluno("Z", 1234, 15)); alunos.push_back(Aluno("Zacarias", 666, 20)); alunos.push_back(Aluno("Marta", 5465, 18)); alunos.push_back(Aluno("Maria", 1111, 14)); cout < < "Pauta:" < < endl; for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i) cout < < alunos[i] < < endl; cout < < "De que aluno deseja saber a nota? "; string nome; cin > > nome; for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i) if(alunos[i].nome() == nome) { cout < < "O aluno " < < nome < < " teve " < < alunos[i].nota() < < endl; if(not Nota::Positiva(alunos[i].nota())) cout < < "Este aluno reprovou." < < endl; } }

521

Makele
all: mdia procura procura.o: procura.C nota.H nota_impl.H aluno.H aluno_impl.H mdia.o: mdia.C nota.H nota_impl.H aluno.H aluno_impl.H

522

CAPTULO 9. MODULARIZAO DE ALTO NVEL

Captulo 10

Listas e iteradores
!!Falar de promiscuidade e intimidade! Antes de se avanar para as noes de ponteiro e variveis dinmicas, estudadas no prximo captulo, far-se- uma digresso pelos conceitos de lista e iterador. Estes dois conceitos permitem uma considervel diversidade de implementaes, todas com a mesma interface mas diferentes ecincias. Neste captulo ir-se- to longe quanto possvel no sentido de uma implementao apropriada dos conceitos de lista e iterador. No prximo captulo continuar-se- este exerccio, mas j usando ponteiros e variveis dinmicas. Assim, a implementao eciente de listas e iteradores surgir como justicao parcial para essas noes. O estudo das listas e dos iteradores associados tem algumas vantagens adicionais, tais como abordar ao de leve questes relacionadas com as estruturas de dados e a ecincia de algoritmos, bem como habituar o leitor compreenso de classes com um grau j aprecivel de complexidade.

10.1 Listas
Todos ns conhecemos o signicado da palavra lista, pelo menos intuitivamente. As listas ocorrem na nossa vida prtica em muitas circunstncias: lista de compras, de afazeres, de pessoas... O Novo Aurlio [4] dene lista da seguinte forma: lista. [Do fr. liste < it. lista < germ. *lista (al. mod. Leiste).] S. f. 1. Relao de nomes de pessoas ou de coisas: relao, rol, listagem. [...] Escusado ser dizer que as denies de relao, rol e listagem do Novo Aurlio remetem de volta noo de lista1 ! Tentar-se- aqui dar uma denio do conceito de lista um pouco menos genrica. Suponha-se uma lista de tarefas a realizar por um determinado aluno mais ou menos aplicado e com alguma ideia das prioridades:
uma fatalidade que tal acontea num dicionrio. Mas esperamos sempre que os ciclos de denies no sejam to curtos...
1

523

524 Comprar CD dos Daft Punk. Combinar ida ao concerto Come Together. Fazer trabalho de Sistemas Operativos. Estudar Programao Orientada para Objectos.

CAPTULO 10. LISTAS E ITERADORES

Uma lista parece portanto corresponder simplesmente a um conjunto de itens. No entanto, haver alguma razo para, tal como acontece nos conjuntos, no existirem repeties de itens? Na realidade no. Note-se na seguinte lista de afazeres, bastante mais razovel que a anterior: Estudar Programao Orientada para Objectos. Comprar CD dos Daft Punk. Estudar Programao Orientada para Objectos. Combinar ida ao concerto Come Together. Estudar Programao Orientada para Objectos. Fazer trabalho de Sistemas Operativos. Estudar Programao Orientada para Objectos. Assim, melhor seria dizer que uma lista uma coleco de itens. Porm, a noo de coleco no suciente, pois ignora um factor fundamental: os itens de uma lista tm uma determinada ordem, que no tem forosamente a ver com os valores dos itens (como evidente no ltimo exemplo)2 . Assim, como denio tentativa, pode-se dizer que uma lista uma sequncia de itens com ordem arbitrria mas relevante. Naturalmente que uma denio apropriada de lista ter de incluir as operaes que se podem realizar com listas. Alis, a noo de lista (e de iterador) podem ser totalmente denidas custa das respectivas operaes.

10.1.1 Operaes com listas


A operao fundamental a realizar com qualquer tipo abstracto de dados naturalmente a construo de instncias desse tipo. Depois de construda uma lista, deve ser possvel pr novos itens na lista e tirar itens existentes da lista, aceder aos itens, e obter informao geral acerca do estado da lista. Grosso modo, as operaes a realizar so: Construir nova lista.

A ordem normalmente determinada pela forma como os seus itens foram colocados na lista.

10.1. LISTAS

525

Pr novo item algures na lista. A lista no pode estar cheia, se tiver limite pr-estabelecido. Tirar algum item existente da lista. A lista no pode estar vazia. Aceder a um qualquer item da lista. A lista no pode estar vazia. Saber comprimento da lista. Saber se a lista est vazia. Saber se est cheia, se tiver limite pr-estabelecido. Esta listagem de operaes deixa um problema por resolver: pr itens, tirar itens e aceder a itens implica ser capaz de lhes especicar a posio na lista. Naturalmente que uma possibilidade seria indicar as posies atravs de um nmero de ordem, por exemplo numerando os itens a partir de zero desde o incio da lista, e usar depois ndices para especicar posies, como se faz usualmente nas matrizes e vectores. Esta soluo tem, no entanto, um problema. Esse problema s se tornar verdadeiramente claro quando se zer uma implementao eciente do conceito de lista em seces posteriores e tem a ver com a ecincia dos seus mtodos: quando se melhorar a implementao inicial das listas (ainda por fazer...) de modo a permitir inseres ecientes de novos itens em qualquer posio, concluir-se- que a utilizao de ndices se tornar extremamente ineciente... Porqu esta preocupao com a ecincia quando se est ainda a denir a interface das listas, ou seja, as suas operaes e respectivos efeitos? Acontece que se pode e deve considerar que a ecincia faz parte do contrato das rotinas e mtodos. Embora a questo da ecincia de algoritmos esteja fora do contexto desta disciplina, fundamental apresentar aqui pelo menos uma ideia vaga destas ideias. Suponha-se que se pretende desenvolver um procedimento para ordenar os itens de um vector por ordem crescente. A interface do procedimento inclui, como se viu em captulos anteriores, o seu cabealho, que indica como o procedimento se utiliza, e um comentrio de documentao onde se indicam a pr-condio e a condio objectivo do procedimento, ou seja o contrato estabelecido entre o programador que o produziu e os programadores que o consumiro (ver Seco 9.3.1):
/** Ordena os valores do vector v. @pre P C v = v. @post CO perm(v, v) (Q i, j : 0 i j < v.size() : v[i] v[j]). void ordenaPorOrdemNoDecrescente(vector<int>& v);

No entanto, esta interface no diz tudo. Se se mandar ordenar um vector com 1000 itens, quanto tempo demora o procedimento? E se forem o dobro, 2000 itens? Como cresce o tempo de execuo com o nmero de itens? E quanta memria adicional usar o procedimento? Estas questes so muitas vezes relevantes, embora nem sempre. Podem fazer a diferena entre um programa que demora dias ou meses (ou anos, sculos ou milnios...) a executar e outro programa que demora segundos. Ou entre um programa que pode ser executado em

526

CAPTULO 10. LISTAS E ITERADORES

computadores com memria limitada e outro com um apetite voraz pela memria que o torna intil na prtica. As ecincias em termos de tempo de execuo e em termos de espao de memria consumido so muito importantes e podem e devem ser considerados parte do contrato de uma rotina ou mtodo de uma classe. No caso do procedimento de ordenao a interface, em rigor, deveria ser melhor especicada:
/** Ordena os valores do vector v. O tempo de execuo cresce com nmero n de itens ao mesmo ritmo que a funo n ln n. Ou seja, a ecincia temporal O(n ln(n)). A ordenao feita directamente sobre a matriz, recorrendo-se apenas a um pequeno nmero de variveis auxiliares. @pre P C v = v. @post CO perm(v, v) (Q i, j : 0 i j < v.size() : v[i] v[j]). void ordenaPorOrdemNoDecrescente(vector<int>& v);

importante perceber-se que do contracto no fazem parte tempos de execuo ao segundo. H duas razes para isso. A primeira prtica: os tempos exactos de execuo no so fceis de calcular e, sobretudo, variam de computador para computador e de mquina para mquina. A segunda razo prende-se com o facto de ser muito mais importante saber que o tempo de execuo ou a memria requerida por um algoritmo crescem de uma forma bem comportada do que saber o tempo exacto de execuo 3 . Esta digresso veio como justicao para a utilizao de critrios de ecincia na excluso dos ndices para indicar localizaes de itens em listas. que se pretende que as listas tenham mtodos de insero de novos itens em locais arbitrrios (e respectiva remoo) to ecientes quanto a sua insero em (ou remoo de) locais cannicos, tais como os seus extremos. Isso, como se ver, levar a uma arrumao dos itens tal que a sua indexao ser inevitavelmente muito ineciente. Claro que uma possibilidade de resoluo deste problema seria fornecer a possibilidade de usar ndices para referenciar posies de itens na lista mas assinalar claramente como inecientes as respectivas operaes. No entanto, tal no ser feito aqui, at porque se ir desenvolver uma forma mais apropriada de referenciar os itens de uma lista. H que distinguir entre as operaes realizadas em locais cannicos das listas, que so os seus extremos, e aquelas que se realizam em locais arbitrrios, que tero de fazer uso de uma generalizao do conceito de indexao, a introduzir na prxima seco. As operaes de insero, remoo e acesso em locais cannicos so: Tirar o item da frente da lista. A lista no pode estar vazia.
3 A ttulo de exemplo, suponham-se dois algoritmos possveis para a implementao do procedimento de ordenao acima. Suponha-se que o primeiro, mais simples, caracterizado por o tempo de execuo crescer com o quadrado do nmero de itens a ordenar e que o segundo, um pouco mais complicado, tem tempos de execuo que crescem com o produto do nmero de itens a ordenar pelo seu logaritmo. A notao da algoritmia para a ecincia temporal destas algoritmos O(n2 ) e O(n log n), respectivamente, em que n o nmero de itens a ordenar. Clculos muito simples permitem vericar que, para um nmero sucientemente grande de itens, o algoritmo mais complicado acaba sempre por ter uma ecincia superior.

10.2. ITERADORES
Tirar o item de trs da lista. A lista no pode estar vazia.

527

Pr um novo item na frente da lista. A lista no pode estar cheia, se tiver limite prestabelecido. Pr um novo item na traseira da lista. A lista no pode estar cheia, se tiver limite prestabelecido. Aceder ao item na frente da lista (para modicao ou no). A lista no pode estar vazia. Aceder ao item na traseira da lista (para modicao ou no). A lista no pode estar vazia. possvel imaginar muitas mais operaes com listas. Algumas delas so realmente teis, mas s sero introduzidas mais tarde, de modo a manter uma complexidade limitada na primeira abordagem. Outras sero porventura menos teis em geral e podero ser dispensadas. No fcil, como bvio, distinguir operaes essenciais de operaes acessrias. Algumas so essenciais para que se possa dar o nome de lista ao tipo em anlise. As operaes escolhidas neste e no prximo captulo incluem as fundamentais para que se possa de facto falar em listas (como as operaes de insero e remoo de itens em locais cannicos) e tambm as mais comummente utilizadas na prtica. Por outro lado, correspondem tambm a algumas das operaes existentes na interface do tipo genrico list da biblioteca padro do C++ 4 . o caso, por exemplo da seguinte operao, que far parte da interface das listas: Esvaziar a lista, tirando todos os seus itens.

10.2 Iteradores
Como fazer para indicar um local arbitrrio de insero, remoo ou simples acesso a itens da lista? conveniente aqui observar como um humano resolve o problema. Como o nico humano disponvel neste momento o leitor, peo-lhe que entre no jogo e colabore. Considere a seguinte lista de inteiros

(1 2 3 11 20 0 354 2 3 45 12 34 30 4 4 23 3 77 4 1 20 46). Considere um qualquer item da lista e suponha que pretende indic-lo a um amigo. Suponha que esse amigo est ao seu lado e pode ver a lista. Indique-lhe o item que escolheu. . . . . . .
4

Ou seja, reinventa-se de novo a roda... Ver Nota 1 na pgina 301.

528

CAPTULO 10. LISTAS E ITERADORES

Provavelmente o leitor indicou-o da forma mais bvia: apontando-o com o dedo indicador... Pelo menos assim que a maioria das pessoas faz... O objectivo agora , pois, conseguir representar o conceito de dedo indicador em C++... E se o leitor quiser indicar o local para inserir um novo item na lista? Provavelmente f-lo- indicando um intervalo entre dois itens (excepto se pretender inserir num dos extremos da lista). Aqui, no entanto, usar-se- uma abordagem diferente: sempre possvel indicar um local de insero indicando um item existente e dizendo para inserir o novo item imediatamente antes ou depois do item indicado. Isto, claro est, se a lista no estiver vazia! Mas mesmo nesse caso se encontrar uma soluo interessante. Para alm da lista em si, tem-se agora um outro conceito para representar. Bons nomes para o novo conceito poderiam ser indicador (sugestivo dada a analogia do dedo) ou cursor. Porm, o termo aqui usado o usual em programao: iterador.

10.2.1 Operaes com iteradores


Que operaes se podem realizar com os iteradores? A primeira operao , naturalmente, construir um iterador. razovel impor que um iterador tenha de estar sempre associado a uma determinada lista. Assim, a construo de um iterador associa-o a uma lista e pe-no a referenciar um determinado item de essa lista, por exemplo o da sua frente (se existir...). Imaginem-se agora as operaes que se podem realizar com um dedo sobre uma lista. Identicamse imediatamente as operaes mais elementares: avanar e recuar 5 o iterador e aceder ao item referenciado pelo iterador. Por razes que se ver mais tarde, no ser eciente colocar um iterador numa posio arbitrria de uma lista (se exceptuarmos as posies cannicas) nem avanar ou recuar um iterador mais do que um item de cada vez. Estas restries tm menos a ver com o conceito de iterador em si do que com o tipo de contentor de itens a que est associado6 . Ou seja, as operaes a realizar com iteradores so: Construir iterador associado a lista e referenciando o item na sua frente. Avanar o iterador. O iterador no pode referenciar o item de trs da lista. Recuar o iterador. O iterador no pode referenciar o item na frente da lista. Aceder ao item referenciado pelo iterador (para modicao ou no).
Usa-se o termo avanar para deslocamentos da frente para trs e o termo recuar para deslocamentos de trs para a frente. Isto pode parecer estranho, mas prtica habitual e far um pouco mais de sentido quando associado aos termos incio e m. A ideia que avanar deslocar do incio para o m, tal como o leitor desloca o olhar, avanando, do incio para o m deste texto... 6 Um contentor um tipo abstracto de dados capaz de conter itens de um outro tipo. Existem variadssimos contentores, que incluem listas, las, pilhas, vectores, matrizes, coleces, conjuntos, mapas, etc. Cada um destes tipos de contentores (com excepo das pilhas e das las) pode ser associado a um tipo especco de iterador. Os iteradores podem ser categorizados em conceitos, de acordo com as caractersticas especcas do respectivo contentor !!!!citarAustern. Por exemplo, vectores tm associados iteradores ditos de acesso aleatrio, muito menos restrivos que os das listas, ditos bidireccionais.
5

10.2. ITERADORES

529

Quantos iteradores se podem associar a uma lista? A soluo mais restritiva permitir apenas um iterador por lista. Esta soluo, que no ser seguida aqui, tem a vantagem de no exigir a denio de classes auxiliares e de resolver bem o problema da validade dos iteradores depois de alteraes lista que lhe est associada. A soluo aqui seguida ser a mais genrica: podero existir um nmero arbitrrio de iteradores associados mesma lista. A existncia de vrios iteradores associados mesma lista torna necessrios os operadores de igualdade e diferena entre iteradores: Vericao de igualdade entre iteradores. Os iteradores tm de estar associados mesma lista. Vericao da diferena entre iteradores. Os iteradores tm de estar associados mesma lista.

10.2.2 Operaes se podem realizar com iteradores e listas


Para alm das operaes realizadas sobre os iteradores, h algumas operaes que so realizadas sobre uma lista mas usando um iterador como indicador de uma posio arbitrria. Estas operaes so as de insero e remoo de itens em e de locais arbitrrios mencionadas mais atrs. Por outro lado, era conveniente que a lista possusse mtodos construtores de iteradores para as suas posies cannicas. Relativamente s operaes de insero e remoo, h que tomar algumas decises. Em primeiro lugar tem de se decidir se se insere antes ou depois do item referenciado por um iterador. Isto deve-se a que os iteradores referenciam itens, e no intervalos entre itens. Poder-se-ia escolher fornecer duas operaes, uma inserindo antes e outra depois. Na prtica considera-se sucente fornecer uma verso que insira antes de um iterador, pois durante inseres ordenadas tpico o iterador parar logo que se encontra o primeiro item direita do qual o novo item no pode ser colocado (ver mais abaixo). Em segundo lugar tem de se decidir o que fazer a um iterador depois da remoo do item por ele referenciado. usual escolher-se avanar o iterador para o item imediatamente a seguir. Ambas as opes, i.e., inserir antes e avanar ao remover, so tpicas, embora razoavelmente arbitrrias. Relativamente s posies cannicas, chamar-se- primeiro a um iterador referenciando o item na frente da lista e ltimo a um iterador referenciando o item na traseira da lista. Assim, as operaes so: Inserir novo item antes do item referenciado por um iterador. Remover item referenciado por um iterador (e avan-lo). Construir primeiro iterador, i.e., um iterador referenciando o item na frente da lista. Construir ltimo iterador, i.e., um iterador referenciando o item na traseira da lista.

530

CAPTULO 10. LISTAS E ITERADORES

10.2.3 Itens ctcios


Para terminar a discusso acerca das operaes de listas e iteradores h que resolver ainda alguns problemas. Nas operaes apresentadas at agora teve-se sempre o cuidado de apresentar as respectivas pr-condies. Mas houve alguns esquecimentos: Qual o primeiro iterador de uma lista vazia? E o ltimo? Que item referencia um iterador depois de, atravs dele, se ter removido o item da traseira da lista? Outras perguntas igualmente interessantes podem ser feitas: Que item referencia um iterador associado a uma lista vazia? Como se insere na traseira da lista, atravs de um iterador, se existe apenas uma operao para inserir antes da posio referenciada por um iterador? Como se percorre uma lista do princpio ao m com um iterador? Para situar um pouco melhor estas perguntas e justicar uma possvel resposta, suponha-se o problema de inserir ordenadamente um novo item numa lista de inteiros lista = (1 2 6 9). Suponha-se que o item a inserir 5. Que fazer? Que algoritmo se pode usar que resolva o problema para qualquer lista ordenada e qualquer novo item? Uma possibilidade fazer uma pesquisa sequencial desde o item na frente da lista at encontrar o primeiro item maior ou igual ao novo item e inseri-lo imediatamente antes do item encontrado. Ou seja,
{ Algoritmo para inserir ordenadamente o novo item novo_item na lista lista. } { Construir um iterador referenciando o primeiro item da lista: } i primeiro de lista enquanto item referenciado por i < novo_item faa-se: avanar i inserir novo_item na lista lista imediatamente antes do item referenciado por i

E se o novo item for 10? claro que o algoritmo anterior no est correcto. A sua guarda tem de permitir a paragem mesmo que todos os itens da lista sejam inferiores ao novo item a inserir:

10.2. ITERADORES
{ Algoritmo para inserir ordenadamente o novo item novo_item na lista lista. } { Construir um iterador referenciando o primeiro item da lista: } i primeiro de lista enquanto i no atingiu o m da lista lista item referenciado por i < novo_item faa-se: avanar i inserir novo_item na lista lista imediatamente antes do item referenciado por i

531

O problema o signicado da frase i no atingiu o m da lista l. Que signica o m da lista? No decerto o iterador referenciando o item na traseira. Porqu? Porque nesse caso o algoritmo que segue, que era suposto mostrar todos os itens da lista, pura e simplesmente no funciona: deixa o item de trs da lista de fora...
{ Algoritmo para mostrar todos os itens da lista lista. } { Construir um iterador referenciando o primeiro item da lista: } i primeiro de lista enquanto i no atingiu o m da lista lista faa-se: mostrar item referenciado por i avanar i

Conclui-se facilmente que, a haver um iterador referenciando o m da lista, ele est para alm do ltimo! Mas nesse caso, que item referencia? A esse item, que na realidade no existe na lista, dar-se- o nome de item ctcio. Assim, pode-se considerar que as listas, para alm dos seus itens, possuem dois itens ctcios nos seus extremos. Estes itens so particulares por vrias razes: 1. Existem sempre, mesmo com a lista vazia. 2. No correspondem a itens reais, pelo que no tm valor. 3. So teis apenas enquanto ncoras para os iteradores, que se lhes podem referir. Chamar-se- aos iteradores antes do primeiro e depois do ltimo respectivamente incio e m. Veja-se a situao retratada na Figura 10.1. Na realidade a noo de item ctcio foi usada j extensamente quando se desenvolveram ciclos para percorrer matrizes. Repare-se no seguinte troo de programa:
double md[4] = {1.1, 2.2, 3.3, 4.4}; for(int i = 0; i != 4; ++i) cout < < md[i] < < endl;

Neste ciclo o ndice toma todos os valores entre 0 e 4, sendo 4 a dimenso da matriz e, portanto, o ndice de um elemento ctcio nal da matriz. Da mesma forma se pode considerar que uma matriz tem um item ctcio inicial:

532 frente

CAPTULO 10. LISTAS E ITERADORES


trs

( !

2 6

! )

i1

i2

i3

i4

inicial primeiro ltimo

nal

Figura 10.1: Denio de iteradores e itens cannicos das listas. Representam-se por um ponto de exclamao os itens ctcios da lista. Os iteradores primeiro (i2) e ltimo (i3) referenciam respectivamente os itens na frente e na traseira da lista. Os iteradores incio (i1) e m (i4) so denidos como sendo respectivamente anterior ao primeiro e posterior ao ltimo.
double md[4] = {1.1, 2.2, 3.3, 4.4}; for(int i = 3; i != -1; --i) cout < < md[i] < < endl;

Voltando s listas, e dados os dois novos iteradores incio e m, so teis duas novas operaes de construo para os obter: Construir iterador incio, i.e., o iterador antes do primeiro. Construir iterador m, i.e., o iterador depois do ltimo. Com estes novos iteradores, torna-se claro que os algoritmos propostos esto correctos. No caso da insero ordenada, mesmo que o novo item seja superior a todos os itens da lista, o ciclo termina com o iterador nal, pelo que inserir o novo item antes da posio referenciada pelo iterador leva-o a car na traseira da lista, como apropriado! tambm interessante compreender que o posicionamento dos iteradores no caso de uma lista vazia, tal como indicado na Figura 10.2, apesar de contra-intuitivo, leva tambm ao correcto funcionamento dos algoritmos nesse caso extremo.

10.2.4 Operaes que invalidam os iteradores


Que acontece a um iterador referenciando um item no-ctcio de uma dada lista se essa lista for esvaziada? evidente que esse iterador no pode continuar vlido. Assim, importante reconhecer que os iteradores podem estar em estados invlidos, nos quais a sua utilizao um erro. A questo mais importante associada validade dos iteradores a de saber que

10.2. ITERADORES
( ! ! )

533

i1

i2

inicial primeiro ltimo

nal

Figura 10.2: Denio de iteradores e itens cannicos de uma lista vazia. Note-se que os iteradores primeiro e ltimo trocam as suas posies relativas usuais. Decorre daqui que so estes iteradores que se denem custa dos iteradores incio e m, e no o contrrio, como se sugeriu atrs. operaes invalidam que iteradores associados a uma lista. Esta questo e as pr-condies das operaes discutidas apresentam-se na seco seguinte.

10.2.5 Concluso
Operaes apenas das listas: Construtoras: Construir nova lista vazia. Inspectoras: Saber comprimento da lista, i.e., quantos itens contm. Saber se a lista est vazia. Saber se est cheia, se tiver limite pr-estabelecido. Saber valor do item na traseira da lista. P C lista no pode estar vazia. Modicadoras: Aceder ao item na frente da lista (para possvel modicao). P C a lista no pode estar vazia. No invalida iteradores. Aceder ao item na traseira da lista (para possvel modicao). P C a lista no pode estar vazia. No invalida iteradores. Pr um novo item na frente da lista. P C a lista no pode estar cheia, se tiver limite pr-estabelecido. No invalida iteradores. Pr um novo item na traseira da lista. P C a lista no pode estar cheia, se tiver limite pr-estabelecido. No invalida iteradores. Saber valor do item na frente da lista. P C a lista no pode estar vazia.

534

CAPTULO 10. LISTAS E ITERADORES


Tirar o item da frente da lista. P C a lista no pode estar vazia. Invalida todos os iteradores que referenciem o item da frente da lista. Tirar o item de trs da lista. P C a lista no pode estar vazia.Invalida todos os iteradores que referenciem o item de trs da lista. Esvaziar a lista, tirando todos os seus itens. Invalida todos os iteradores associado lista, com excepo dos que referenciam os seus itens ctcios.

Em todas as operaes relacionadas com iteradores (exceptuando as construtoras de iteradores) considera-se como pr-condio que os iteradores tm de ser vlidos. Esta condio no se apresenta explicitamente abaixo. Operaes dos iteradores: Construir iterador associado a lista (iterador o primeiro, i.e., se a lista estiver vazia o seu m). Avanar o iterador. P C o iterador no pode ser o m da lista. Recuar o iterador. P C o iterador no pode ser o incio da lista. Aceder ao item referenciado pelo iterador (para possvel modicao). P C o iterador no pode ser nem o incio nem o m da lista. Vericao de igualdade entre iteradores. P C os iteradores tm de estar associados mesma lista. Vericao da diferena entre iteradores. P C os iteradores tm de estar associados mesma lista. Operaes das listas relacionadas com iteradores: Inserir novo item antes do item referenciado por um iterador. P C o iterador no pode ser o incio da lista. No invalida iteradores. Remover item referenciado por um iterador (e avan-lo).P C o iterador no pode ser nem o incio nem o m da lista. Invalida todos os iteradores que referenciem o item removido (com excepo do iterador que foi usado para a remoo, que avana). Construir primeiro iterador, i.e., o iterador depois do incio da lista. Construir ltimo iterador, i.e., o iterador antes do m da lista. Construir iterador incio. Construir iterador m.

10.3. INTERFACE

535

10.3 Interface
Os conceitos de lista e iterador, para serem verdadeiramente teis, tm de ser concretizados na forma de classes. As operaes estudadas nas seces anteriores correspondem sua interface. Para simplicar o desenvolvimento supor-se- que as listas guardam inteiros. Assim, chamarse- ListaDeInt classe que concretiza o conceito de lista de inteiros e Iterador classe que concretiza o conceito de iterador de uma lista de inteiros. Nas seces abaixo vai-se denindo a interface da classe passo por passo. Os contractos das operaes declaradas so paresentados informalmente. No nal apresentada uma veroa completa, com os contratos das operaes na forma de comentrios de documentao.

10.3.1 Interface de ListaDeInt


Esta classe a concretizao do conceito de lista de inteiros:
class ListaDeInt { public:

Tipos H dois tipos denidos pela classe. O primeiro, Item, no verdadeiramente um novo tipo, pois Item sinnimo de int:
typedef int Item;

Este sinnimo tem a vantagem de simplicar consideravelmente a tarefa de criar listas com itens de outros tipos. !!!!Dar maior nfase a isto! O segundo tipo a classe Iterador:
class Iterador;

!!!!Reescrever De modo a que o identicador Iterador no que gasto, uma vez que se podem denir iteradores para muitos outros tipos de contentores para alm das listas, conveniente que a classe Iterador seja embutida dentro da classe ListaDeInt. Mtodos construtores Declara-se apenas um mtodo construtor que constri uma lista vazia:
ListaDeInt();

536 Mtodos inspectores

CAPTULO 10. LISTAS E ITERADORES

Declaram-se trs mtodos inspectores. Este inspectores podem ser usados em qualquer circunstncia para indagar do estado da lista. O primeiro devolve o nmero de itens da lista, ou seja, o seu comprimento:
int comprimento() const;

O segundo serve para vericar se a lista est vazia:


bool estVazia() const;

O terceiro serve para vericar se a lista est cheia:


bool estCheia() const;

Declaram-se adicionalmente dois mtodos inspectores que apenas podem ser invocados se a lista no estiver vazia e que servem para aceder (sem poder modicar) aos itens nas posies cannicas da lista. Ambos tm como pr-condio: P C estVazia(). O primeiro devolve uma referncia constante para o item na frente da lista:
Item const& frente() const;

O segundo devolve uma referncia constante para o item na traseira da lista:


Item const& trs() const;

Mtodos modicadores Os dois primeiros mtodos modicadores declarados so semelhantes aos inspectores dos itens nas posies cannicas da lista. Tal como eles, apenas podem ser invocados se a lista no estiver vazia P C estVazia().

Ao contrrio deles, no entanto, devolvem referncias para os itens, o que permite que estes sejam modicados. O primeiro devolve um referncia para o item na frente da lista:
Item& frente();

O segundo devolve uma referncia para o item na traseira da lista:

10.3. INTERFACE
Item& trs();

537

Os dois mtodos modicadores seguintes permitem pr um novo item nas posies cannicas da lista. Ambos requerem que a lista no esteja cheia, i.e.,!!! P C estCheia(). !!!!!Vou aqui O primeiro dos mtodos pe o novo item na frente da lista:
void peNaFrente(Item const& novo_item);

O segundo pe o novo item na traseira da lista:


void peAtrs(Item const& novo_item);

Declaram-se tambm dois mtodos modicadores que permitem tirar um item das posies cannicas da lista. Ambos requerem que a lista no esteja vazia, i.e., P C estVazia(). Ambos invalidam qualquer iterador que esteja associado lista. O primeiro dos mtodos tira o item da frente da lista:
void tiraDaFrente();

O segundo tira o item da traseira da lista:


void tiraDeTrs();

Finalmente, declara-se um mtodo modicador de que no se falou ainda. Este mtodo serve para esvaziar a lista, i.e., para descartar todos os seus itens:
void esvazia();

Este mtodo invalida qualquer iterador que esteja associado lista.

538 Mtodos modicadores envolvendo iteradores

CAPTULO 10. LISTAS E ITERADORES

Declaram-se dois mtodos modicadores que envolvem a especicao de posies atravs de iteradores. O primeiro insere um novo item imediatamente antes da posio indicada por um iterador iterador, exigindo-se para isso que a lista no esteja cheia, que o iterador seja vlido e esteja associado lista em causa e que o iterador no referencie o iterador incio 7 P C iterador vlidoiterador associado ` listaestCheia()iterador = incio(). e a a Este mtodo garante que o iterador continua a referenciar o mesmo item que antes da insero8 :
void insereAntes(Iterador& iterador, Item const& novo_item);

Qualquer outro iterador associado lista invalidado por este mtodo. O segundo mtodo remove o item indicado pelo iterador iterador, exigindo-se que este seja vlido e esteja associado lista em causa e que referencie um dos itens vlido da lista, pois no faz sentido remover os itens ctcios 9 P C iterador vlidoiterador associado ` listaiterador = incio()iterador = fim(). e a a Este mtodo garante que o iterador ca a referenciar o item logo aps o item removido. Por isso o iterador tem de ser passado por referncia no-constante.
void remove(Iterador& iterador);

Qualquer outro iterador associado lista invalidado por este mtodo.


Ver declarao do mtodo incio() mais frente. Repare-se que no se incluiu na pr-condio nenhum termo que garante que o iterador est de facto associado lista em causa. Isso ser feito mais tarde, quando se falar de ponteiros. 8 H aqui uma incoerncia que o leitor mais atento detectar. Se o iterador se mantm referenciando o mesmo item que antes da insero, no deveria ser passado por valor ou referncia constante? verdade que sim. Acontece que a primeira implementao simplista destas classes, que ser feita na Seco 10.4, implicar uma alterao fsica ao iterador para ele conceptualmente se possa manter constante... Este tipo de problemas resolve-se normalmente custa do qualicador mutable, que aplicado a uma varivel membro de uma classe, permite que ela seja alterada mesmo que a instncia que a contm seja constante. No entanto essa soluo levaria a uma maior complexidade da classe e, alm disso, o problema resolver-se- naturalmente quando se melhorar a implementao da classe na Seco 10.5, pelo que mais tarde a declarao do mtodo ser mudada paulatinamente de modo ao iterador ser passado por referncia constante... 9 Note-se que no necessrio incluir na pr-condio a garantia de que a lista no est vazia, visto que se o iterador no se refere a nenhum dos itens ctcios, ento certamente que a lista no est vazia.
7

10.3. INTERFACE
Mtodos construtores de iteradores

539

Estes mtodos consideram-se modicadores porque a lista pode ser modicada atravs dos iteradores devolvidos. Existem quatro mtodos construtores de iteradores, correspondentes aos quatro iteradores cannicos denidos na Figura 10.1. Os primeiros mtodos devolvem iteradores referenciando os itens ctcios da lista:
Iterador incio(); Iterador fim();

Os dois seguintes devolvem iteradores imediamente depois do incio e antes do m, i.e., nas posies conhecidas por primeiro e ltimo:
Iterador primeiro(); Iterador ltimo();

Com evidente os iteradores construdos por qualquer destes mtodos so vlidos. Estes dois mtodos completam a interface da classe ListaDeInt. Mais tarde, quando se passar implementao, discutir-se-o os membros privados da classe:
private: ... // a completar mais tarde, pois parte da implementao da classe. };

Interface completa em C++ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!Que fazer ao contrato? Convinha apresent-lo! !!!!Ir buscar tudo de novo aos guies. No esquecer de acrescentar invalidao de iteradores nos contratos das operaes!
class ListaDeInt { public: typedef int Item; class Iterador; ListaDeInt(); int comprimento() const; bool estVazia() const; bool estCheia() const;

540
Item const& frente() const; Item const& trs() const; Item& frente(); Item& trs();

CAPTULO 10. LISTAS E ITERADORES

void peNaFrente(Item const& novo_item); void peAtrs(Item const& novo_item); void tiraDaFrente(); void tiraDeTrs(); void esvazia(); void insereAntes(Iterador& iterador, Item const& novo_item); void remove(Iterador& iterador); Iterador incio(); Iterador fim(); Iterador primeiro(); Iterador ltimo(); private: ... // a completar mais tarde, pois parte da implementao da classe. };

10.3.2 Interface de ListaDeInt::Iterador


Esta classe, ListaDeInt::Iterador, a concretizao do conceito de iterador de uma lista de inteiros:
class ListaDeInt::Iterador { public:

Mtodos construtores Esta classe tem apenas um construtor. Como um iterador tem de estar sempre associado a uma qualquer lista, natural que o construtor receba a lista como argumento. O iterador construdo ca vlido e associado lista passada como argumento e referenciando o item na sua frente. I.e., o iterador construdo o primeiro da lista. Como atravs do iterador se podem alterar os itens da lista, a lista passada por referncia no-constante:
explicit Iterador(ListaDeInt& lista_a_associar);

10.3. INTERFACE

541

Note-se a utilizao da palavra-chave explicit. A sua inteno impedir que o construtor, podendo ser invocado com um nico argumento, introduza uma converso implcita de lista para iterador. Uma instruo como
ListaDeInt lista; ... ListaDeInt i(lista); ... if(i == lista) // comparao errnea se no houver converso implcita. ...

resulta portanto num erro de compilao, ao contrrio do que sucederia se a converso implcita existisse10. Mtodos inspectores Declara-se um nico mtodo inspector, que serve para aceder ao item referenciado pelo iterador. Este mtodo devolve uma referncia para o item referenciado pelo iterador, de modo a que seja possvel alter-lo. importante perceber-se que este mtodo constante, apesar de permitir alteraes ao item referenciado: a constncia de um iterador refere-se ao iterador em si, e no lista associada ou aos itens referenciados, que no pertencem ao iterador, mas sim lista associada11 . Um iterador constante (const) no pode ser alterado (e.g., avanar ou recuar), mas permite alterar o item por ele referenciado na lista associada. O mtodo requer que o iterador seja vlido e no seja nem o incio nem o m da lista, pois no faz sentido aceder aos itens ctcios P C vlido no iterador inicial da lista no iterador nal da lista. e a a e a e A declarao do mtodo :
Item& item() const;

Operadores inspectores Declaram-se dois operadores inspectores, i.e., dois operadores que no modicam a instncia implcita, que sempre o seu primeiro operando. So os operadores de igualdade (==) e diferena (!=). Estes operadores so membros da classe porque exigem acesso aos atributos privados da classe12 .
Na realidade essa converso poderia contribuir para aproximar o modelo das listas e iteradores do modelo de matrizes e ponteiros do C++. Ver Captulo 11. 11 Este facto, de resto, ir obrigar mais tarde denio de uma classe adicional de iterador, menos permissiva, que garanta a constncia da lista associada e dos itens referenciados. 12 Uma alternativa possvel, embora indesejvel, seria denir os operadores como rotinas normais (no-membro) e torn-los amigos da classe ListaInt::Iterador. Isso s seria justicvel se fosse importante que ocorressem converses implcitas em qualquer dos seus operandos, como sucedia no exemplo dos nmeros racionais (ver Captulo 13). Neste caso essas converses no so necessrias. Alm disso, como se ver mais tarde (ver !!), a tranformao destas classes em classes modelo (genricas) exige que os operadores de uma classe embutida sejam denidos como membros.
10

542

CAPTULO 10. LISTAS E ITERADORES

O primeiro operador verica se a instncia implcita (primeiro operando) igual ao iterador passado como argumento (segundo operando). Dois iteradores so iguais se se referirem ao mesmo item. O segundo verica se so diferentes:
bool operator == (Iterador const& outro_iterador) const; bool operator != (Iterador const& outro_iterador) const;

S faz sentido invocar estes operadores para iteradores vlidos e associados mesma lista P C vlido outro_ iterador vlido associados ` mesma lista. e a e a a Mtodos modicadores Os nicos mtodos modicadores de iteradores so os que permitem avanar e recuar um iterador de um (e um s) item na lista 13 . Em vez de se declararem mtodos modicadores com nomes como avana() e recua(), optou-se por declarar os operadores de incrementao (++) e decrementao (--), quer prexos quer suxos, de modo a que a utilizao de iteradores fosse o mais parecida possvel quer com a utilizao de ndices inteiros, quer com a utilizao de ponteiros, a estudar no prximo captulo. Os operadores de incrementao prexa e suxa
Iterador& operator ++ (); Iterador operator ++ (int);

exigem ambos que o iterador seja vlido e no seja o m da lista P C vlido no iterador nal da lista. e a a e Os operadores de decrementao prexa e suxa
Iterador& operator -- (); Iterador operator -- (int);

exigem ambos que o iterador no seja o incio da lista P C vlido no iterador inicial da lista. e a a e Estes dois mtodos completam a interface da classe ListaDeInt::Iterador. Mais tarde, quando se passar implementao, discutir-se-o os membros privados da classe:
private: ... // a completar mais tarde, pois parte da implementao da classe. };
Esta limitao deve-se s mesmas razes pelas quais no se usam ndices para localizar itens em listas: implementaes adequadas das listas tornariam essas operaes onerosas.
13

10.3. INTERFACE
Interface completa em C++ !!!O mesmo que para as listas!
class ListaDeInt::Iterador { public: explicit Iterador(ListaDeInt& lista_a_associar); Item& item() const; bool operator == (Iterador const& outro_iterador) const; bool operator != (Iterador const& outro_iterador) const; Iterador& operator ++ (); Iterador operator ++ (int); Iterador& operator -- (); Iterador operator -- (int); private: ... // a completar mais tarde, pois parte da implementao da classe. };

543

10.3.3 Usando a interface das novas classes


Uma vez denidos os conceitos de classe e iterador e denidas as respectivas interfaces em C++, pode-se passar directamente ao desenvolvimento de programas que os usem. claro que no se pode ainda gerar programas executveis, pois falta a implementao das classes. Como todas as interfaces so contratos, no apenas no caso das rotinas mas tambm no das classes, o que se possui para j relativamente s classes ListaDeInt e Iterador um contrato de promessa. O programador produtor compromete-se a, num determinado prazo, fornecer ao programador consumidor uma implementao para as classes sem qualquer alterao interface acordada. I.e., compromete-se a, a partir de determinada data, garantir o correcto funcionamento das classes com a interface acordada desde que o programador consumidor garanta, por seu lado, que invoca todos os mtodos e rotinas com argumentos que vericam as respectivas pr-condies. Este contrato permite ao programador consumidor ir desenvolvendo programas usando as classes ainda antes de estas estarem completas. extremamente importante conseguir faz-lo no apenas porque permite alguns ganhos em produtividade no seio das equipas, mas sobretudo, numa fase de formao, por faz-lo estimula a capacidade de abstrao: se a implementao das classes ainda no existe no possvel que nos distraia ao construirmos cdigo que as usa! Comea-se por um exemplo simples. Suponha-se uma dada lista lista, por exemplo

lista = (! 1 2 3 11 20 0 354 2 3 45 12 34 30 4 4 23 3 77 4 1 20 46).

544

CAPTULO 10. LISTAS E ITERADORES

Como mostrar os itens desta lista no ecr? Claramente necessrio percorr-la, o que s pode ser realizado usando iteradores. O algoritmo o seguinte:
{ Algoritmo para mostrar todos os itens da lista lista. } { Construir um iterador referenciando o primeiro item da lista: } i primeiro de lista enquanto i no atingiu o m da lista lista faa-se: mostrar item referenciado por i avanar i

A traduo deste algoritmo para C++ pode ser j feita recorrendo s classes cuja interface se deniu nas seces anteriores:
ListaDeInt lista; ... // Aqui preenche-se a lista... ListaDeInt::Iterador i = lista.primeiro(); while(i != lista.fim()) { cout < < i.item() < < endl; ++i; }

ou, usando um ciclo for,


ListaDeInt lista; ... // Aqui preenche-se a lista... for(ListaDeInt::Iterador i = lista.primeiro(); i != lista.fim(); ++i) cout < < i.item() < < endl;

Um problema mais complexo o de inserir um item novo_item por ordem numa lista lista. O algoritmo j foi visto atrs:
{ Algoritmo para inserir ordenadamente o novo item novo_item na lista lista. } { Construir um iterador referenciando o primeiro item da lista: } i primeiro de lista enquanto i no atingiu o m da lista lista item referenciado por i < novo_item faa-se: avanar i inserir novo_item na lista lista imediatamente antes do item referenciado por i

10.3. INTERFACE
A sua traduo para C++ mais uma vez imediata:
ListaDeInt::Iterador i = lista.primeiro(); while(i != lista.fim() and i.item() < novo_item) ++i; lista.insereAntes(i, novo_item);

545

Assim, possvel escrever cdigo usando classes das quais se conhece apenas a interface. Alis, esta a tarefa do programador consumidor, que mesmo que conhea uma implementao, deve-se abstrair dela. necessrio agora implementar estas classes. A responsabilidade de o fazer do programador produtor. Mas como pode o programador produtor garantir que cumpre a sua parte do contrato? Como pode ter alguma segurana acerca da qualidade dos seus produtos? Para alm do cuidado posto no desenvolvimento disciplinado do cdigo, o programador produtor tem de testar o cdigo produzido.

10.3.4 Teste dos mdulos


Como pode o programador produtor testar os mdulos desenvolvidos, neste caso as classes ListaDeInt e ListaDeInt::Iterador? Escrevendo cdigo com o objectivo simples de usar os mdulos num conjunto de casos considerados interessantes, quer pela sua frequncia na prtica, quer pela sua natureza extrema. A ideia levar os mdulos desenvolvidos ao limite e vericar se resultam naquilo que se espera. Tal como o programador consumidor pode comear a escrever cdigo quando ainda s tem disponvel a interface de um mdulo, tambm o programador produtor o pode fazer: os testes de um mdulo podem ser escritos antes mesmo da sua implementao. Alis, a palavra podem demasiado fraca: os testes devem ser escritos antes da sua implementao. So o ponto de partida para a implementao. As razes so vrias: Ao escrever os testes detectam-se erros, inconsistncias, faltas e excessos na interface do mdulo respectivo. Uma vez prontos, os testes podem ser usados, mesmo com implementaes incompletas (nesse caso o programador produtor espera e antecipa a ocorrncia de erros, naturalmente). Note-se que a execuo dos testes com uma implementao incompleta implica que todas as rotinas e mtodos do mdulo estejam denidos em esqueleto pelo menos 14 (i.e., tem de ser possvel construir um executvel...). Se os testes completos existirem desde o incio todas as alteraes implementao so feitas com maior segurana, uma vez que o teste pode ser realizado sem qualquer trabalho e, dessa forma, pode-se conrmar se as alteraes tiveram sucesso.
14

O nome usual para estes esqueletos que compilam embora no faam (ainda) o que suposto fazerem stubs.

546

CAPTULO 10. LISTAS E ITERADORES

As caractersticas desejveis de um bom teste so pelo menos as seguintes: O nvel mais baixo ao qual devem existir testes o de mdulo fsico. Como usual que cada mdulo fsico dena uma nica classe (ou pelo menos vrias classes totalmente interdependentes), os testes dos mdulos fsicos normalmente confundem-se com os testes das classes. Este requisito tem apenas a ver com a facilidade com que se podem executar automaticamente todos os testes de um projecto. Por vezes desejvel fugir a esta regra e fazer testes individualizados para algum ou todos os mdulos denidos por um mdulo fsico, mas nesse caso o teste (global) do mdulo fsico deve-os invocar um por um. Os testes devem estar tanto quanto possvel embebidos no prprio mdulo fsico. usual que em C++ tenham a forma de uma funo main() colocada dentro do cheiro de implementao (.C) e protegida por uma directiva de compilao condicional denio de uma macro de nome TESTE
#ifdef TESTE ... // Prembulo do teste (#include, etc.) int main() { ... // Conjunto de testes... } #endif

Isto permite facilmente executar os testes sempre que se faz alguma alterao ao mdulo. Os testes no devem gerar seno mensagens muito simples, informando que se iniciou o teste de um dado mdulo fsico e de cada um dos seus mdulos. No nal devem terminar com uma mensagem assinalando o m dos testes do mdulo fsico. S em caso de erro devem ser escritas mais mensagens, que devero se claras e explicativas. As mensagens de erro devem incluir o nome do cheiro e o nmero da linha onde o erro foi detectado. Caso ocorra algum erro a funo main() deve devolver 1. Dessa forma podem ser facilmente desenvolvidos programas que geram e executam os testes de todos os mdulos fsicos de um projecto e geram um relatrio global. !!!!!Valer a pena manter o apndice? Usando as ideias acima desenvolveu-se o programa de teste que se pode encontrar na Seco H.1.3. As listagens completas do mdulo fsico lista_int, que dene as classes ListaDeInt e ListaDeInt::Iterador, encontram-se na Seco H.1.

10.4. IMPLEMENTAO SIMPLISTA

547

10.4 Implementao simplista


!!!!!Dizer algures que no se cumpre parte dos contratos! Nomeadamente no que diz respeito invalidao dos iteradores. Finalmente chega a altura de se pensar numa implementao para as classes ListaDeInt e ListaDeInt::Iterador. Mais vale tarde que nunca, dir-se-. Mas sempre conveniente adiar a implementao de um mdulo at depois de denida a sua interface e de implementada a respectiva funo de teste, pelo que esta a altura certa para atacar o problema. Nesta seco optar-se- por uma implementao simplista, e consequentemente ineciente, para as listas e respectivos iteradores. Discutir-se-o apenas os aspectos fundamentais dessa implementao, pelo que se recomenda que o leitor complemente esta leitura com o estudo da Seco H.1, onde se encontra listada a implementao completa do mdulo.

10.4.1 Implementao de ListaDeInt


natural comear a implementao pelas listas, uma vez que a implementao dos iteradores depender da forma como os itens das listas forem organizados. A questo mais importante onde guardar os itens. Esta questo pode ser respondida de uma forma simples se se determinar que as listas so limitadas e se impuser que o nmero mximo de itens comum a qualquer lista e exactamente igual a, por exemplo, 100 itens. Com esta restrio, evidente que os itens podem ser guardados numa matriz clssica 15 . Para isso til comear por denir uma constante com o nmero mximo de itens das listas:
class ListaDeInt { public: ... private: static int const nmero_mximo_de_itens = 100;

Esta constante partilhada por todas as instncias da classe lista. Por isso se utilizou a palavrachave static, que aplicada a um atributo torna-o um atributo de classe e no um atributo de instncia (ver Seco 7.17.2). O facto de ser um atributo constante de classe e de um tipo aritmtico inteiro bsico autoriza a sua denio dentro da prpria classe. Como o compilador conhece o valor da constante, esta pode ser usada para denir a matriz que guardar os itens:
Item itens[nmero_mximo_de_itens];

Como organizar os itens na matriz? A soluo simplista aqui adoptada passa por guardlos nos primeiros elementos da matriz pela mesma ordem que tm na lista. Dessa forma os
15 Poderiam ser guardados alternativamente num vector, mas as matrizes tm duas vantagens. A primeira que demonstram que possvel implementar as classes em causa sem recorrer a outras classes do C++, e portanto de raiz. A segunda que permitir uma evoluo clara e elegante para as verses mais ecientes da implementao e, nalmente, para o uso de ponteiros e variveis dinmicas.

548

CAPTULO 10. LISTAS E ITERADORES

elementos da matriz estaro divididos em duas zonas: os de menor ndice esto ocupados com itens da lista, enquanto os de maior ndice esto disponveis para futura utilizao. Esta organizao torna necessria uma varivel que guarde o nmero de itens da lista em cada instante, i.e., que permita saber a dimenso relativa dessas duas zonas da matriz:
int nmero_de_itens;

A Figura 10.3 ilustra o estado interno de uma lista contendo

lista = ( ! 10 5 3 20 ! ).

lista: ListaInt itens: Item[100] itens[0]: 10 itens[1]: 5 itens[2]: 3 itens[3]: 20 itens[4]: ? ... itens[99]: ?

nmero_de_itens: int 4 desocupados

Figura 10.3: Estado interno da lista lista = ( ! 10 5 3 20 ! ).

Com a implementao escolhida os itens ctcios no tm existncia fsica. Pode-se arbitrar, no entanto, que o item ctcio antes da frente o elemento (inexistente) da matriz com ndice -1 e que o item ctcio depois da traseira o elemento (inexistente) com ndice nmero_de_itens. Escolheu-se como item ctcio nal o elemento com ndice nmero_de_itens e no nmero_mximo_de_it de modo a que a passagem entre itens se possa fazer sempre por simples incrementao ou decrementao de ndices. A classe ListaDeInt::Iterador, que se implentar na prxima seco, necessita de acesso aos atributos privados da classe ListaDeInt. De outra forma no se poderia escrever o mtodo ListaDeInt::Iterador::item(), como se ver. Assim, necessrio permitir acesso irrestrito dos iteradores s listas, o que se consegue introduzindo uma amizade:
friend class Iterador;

};

10.4. IMPLEMENTAO SIMPLISTA

549

A utilizao de amizades , em geral, m ideia. Neste caso, porm, existe um casamento perfeito16 entre as duas classes: o conceito de lista s se completa com o conceito de iterador e o conceito de iterador s se pode concretizar para um tipo especco de contentor, neste caso as listas. natural que, havendo um casamento perfeito entre duas classes, estas tenham acesso aos membros privados (s partes ntimas, digamos) mutuamente. Sendo o casamento legtimo, esta amizade no promscua... Condio invariante de instncia Qualquer classe que merea esse nome tem uma condio invariante de instncia (CIC). Esta condio indica as relaes entre os atributos de instncia da classe que se devem vericar em cada instante. No caso presente, a condio invariante de instncia simplesmente CIC 0 nmero_ de_ itens nmero_ mximo_ de_ itens. Isto , para que uma instncia da classe ListaDeInt represente de facto uma lista, necessrio que a varivel nmero_de_itens contenha um valor no-negativo e inferior ou igual ao mximo estipulado. No necessrio impor qualquer condio adicional. A condio invariante de instncia tem de se vericar para todas as instncias em jogo quer no incio dos mtodos pblicos da classe quer no seu nal 17 . Para segurana do programador produtor, conveniente colocar asseres no incio e no m de todos esses mtodos que veriquem explicitamente esta condio. Para simplicar a escrita dessas asseres, pode-se acrescentar classe um mtodo privado cumpreInvariante() para vericar se o invariante ou no cumprido:
class ListaDeInt { public: ... private: static int const nmero_mximo_de_itens = 100; Item itens[nmero_mximo_de_itens]; int nmero_de_itens; bool cumpreInvariante() const; };

Este mtodo privado porque o programador consumidor no precisa de vericar nunca se uma lista cumpre o invariante. Limita-se a assumir que sim. De resto, o invariante de uma classe irrelevante para o programador consumidor, pois reecte uma implementao particular e nada diz acerca da interface. A denio do mtodo simplesmente
I.e., por amor. O caso dos construtores e dos destrutores especial. A condio invariante de instncia deve ser vlida apenas no nal do construtor e no incio do destrutor.
17 16

550

CAPTULO 10. LISTAS E ITERADORES


inline bool ListaDeInt::cumpreInvariante() const { return 0 <= nmero_de_itens and nmero_de_itens <= nmero_mximo_de_itens; }

10.4.2 Implementao de ListaDeInt::Iterador


Dada a implementao da classe ListaDeInt, a implementao da classe ListaDeInt::Iterador quase imediata. Em primeiro lugar, cada iterador est associado a uma determinada lista. Assim, necessrio guardar uma referncia para a lista associada dentro de cada iterador, pois podem existir variadas lista num programa, cada uma com vrios iteradores associados. Em segundo lugar um iterador precisa de saber onde se encontra o item referenciado. Como os itens so guardados na matriz itens da classe ListaDeInt, basta guardar o ndice nessa matriz do item referenciado:
class ListaDeInt::Iterador { public: ... private: ListaDeInt& lista_associada; int ndice_do_item_referenciado; };

A Figura 10.5 contm a representao interna da situao retratada na Figura 10.4, em que um iterador i referencia o terceiro item de uma lista

lista = (10 5 3 20). lista = ( ! 10 5 3 20 ! )

i Figura 10.4: Representao simplicada de uma lista lista = ( ! 10 5 3 20 ! ) e de um iterador i referenciando o seu terceiro item.

Condio invariante de classe Tambm os iteradores tm uma condio invariante de classe. Para esta implementao essa condio muito simples: CIC 1 ndice_ do_ item_ referenciado lista_ associada.nmero_ de_ itens.

10.4. IMPLEMENTAO SIMPLISTA

551

lista: ListaInt itens: Item[100] itens[0]: 10 itens[1]: 5 itens[2]: 3 itens[3]: 20 itens[4]: ? ... itens[99]: ?

nmero_de_itens: int 4

i: ListaInt::Iterador lista_associada: ListaInt& ndice_do_item_referenciado: int

Figura 10.5: Estado interno da lista lista = ( ! 10 5 3 20 ! ) e de um iterador i referenciando o seu terceiro item.

552

CAPTULO 10. LISTAS E ITERADORES

e indica simplesmente que o ndice do item referenciado tem de estar entre -1 (se o iterador for o incio da lista) e lista_associada.nmero_de_itens (se o iterador for o incio da lista). O problema que os iteradores, tal como pensados at aqui, tm uma caracterstica infeliz: podem estar em estados invlidos. Por exemplo, se uma lista for esvaziada, todos os iteradores a ela associada cam invlidos. Essa condio de iterador invlido deveria ser prevista pelo prprio cdigo de tal forma que a condio invariante de instncia fosse verdadeira mesmo para iteradores invlidos: CIC vlido 1 ndice_ do_ item_ referenciado e a

lista_ associada.nmero_ de_ itens.

Na prtica poder-se-ia guardar informao acerca do estado de validade de um iterador num novo atributo, booleano. Mas este pequeno acrescento exigiria muitas outras alteraes, nomeadamente na implementao das listas, o que levaria um aumento considervel da complexidade deste par de classes. Em particular seria necessrio prever o estado de iterador invlido em todas as operaes que os envolvessem e, na implementao das listas, garantir que os iteradores invalidados fossem assinalados como tal. Faz-lo um exerccio interessante e til, mas fora do mbito deste captulo e do prximo 18 . Assim, com as implementaes que sero feitas neste texto, dever ser o programador consumidor a garantir que no faz uso de um iterador depois de este ter sido invalidado por alguma operao realizada sobre a lista associada: o cdigo, atravs do mtodo privado cumpreInvariante() que tambm ser denido nesta classe, no o vericar:
class ListaDeInt::Iterador { public: ... private: ListaDeInt& lista_associada; int ndice_do_item_referenciado; bool cumpreInvariante() const; }; inline bool ListaDeInt::Iterador::cumpreInvariante() const { return -1 <= ndice_do_item_referenciado and ndice_do_item_referenciado <= lista_associada.nmero_de_itens; }

10.4.3 Implementao dos mtodos pblicos de ListaDeInt


Neste seco denir-se-o alguns dos mtodos da classe ListaDeInt. Os restantes cam como exerccio para o leitor, que poder depois conferir com a implementao total do mdulo lista_int que se encontra na Seco H.1.
O Apndice I conter no futuro uma verso mais segura e comentada das classes ListaInt e Iterador que verica e mantm informao acerca da validade dos iteradores. !!!referir STLport
18

10.4. IMPLEMENTAO SIMPLISTA

553

O primeiro mtodo a denir o construtor da lista, que se pode limitar a inicializar o nmero de itens com o valor zero, para que a lista que vazia:
inline ListaDeInt::ListaDeInt() : nmero_de_itens(0) {

termina-se vericando se a nova lista cumpre o seu invariante:


assert(cumpreInvariante()); }

O mtodo ListaDeInt::peAtrs() tambm muito simples. Basta colocar o novo item aps todos os itens existentes (ver Figura 10.3) e incrementar o nmero de itens:
inline void ListaDeInt::peAtrs(Item const& novo_item) {

Comea-se por vericar se a lista cumpre a sua condio invariante de instncia,


assert(cumpreInvariante());

depois verica-se a pr-condio do mtodo,


assert(not estCheia());

faz-se a insero do novo item,


itens[nmero_de_itens++] = novo_item;

e termina-se vericando se no nal do mtodo a lista continua a cumprir a sua condio invariante de instncia.
assert(cumpreInvariante()); }

O mtodo ListaDeInt::peNaFrente() um pouco mais complicado. O primeiro item est na posio 0 da matriz, pelo que necessrio deslocar todos os itens existentes de modo a deixar espao para o novo item. Esta a fonte principal de inecincia desta implementao e a justicao para a necessidade de se mudar a implementao das listas, como se far mais tarde:
void ListaDeInt::peNaFrente(Item const& novo_item) {

554

CAPTULO 10. LISTAS E ITERADORES

Comea-se por vericar a condio invariante de instncia e a pr-condio do mtodo,


assert(cumpreInvariante()); assert(not estCheia());

Rearranjam-se os elementos da matriz de modo a deixar um espao vago na posio 0,


for(int i = nmero_de_itens; i != 0; --i) itens[i] = itens[i - 1];

depois insere-se o novo item na posio livre,


itens[0] = novo_item;

incrementa-se o contador de itens,


++nmero_de_itens;

e termina-se vericando se no nal do mtodo a lista continua a cumprir a sua condio invariante de instncia.
assert(cumpreInvariante()); }

O mtodo ListaDeInt::insereAntes() muito semelhante, embora o elemento da matriz a vagar seja o que contm o item referenciado pelo iterador, pelo que j no necessrio, em geral, rearranjar todos os elementos da matriz:
void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(cumpreInvariante()); assert(not estCheia()); assert(iterador vlido); assert(iterador != lista_associada.incio()); for(int i = nmero_de_itens; i != iterador.ndice_do_item_referenciado; --i) itens[i] = itens[i - 1];

Tem de se incrementar o ndice do item referenciado pelo iterador, para que ele passe a referenciar o item imediatamente depois do que se pretende remover.

10.4. IMPLEMENTAO SIMPLISTA


itens[iterador.ndice_do_item_referenciado++] = novo_item; assert(iterador.cumpreInvariante()); ++nmero_de_itens; assert(cumpreInvariante()); }

555

Os mtodos ListaDeInt::remove() e ListaDeInt::tiraDaFrente() tm de fazer a operao inversa. Dene-se aqui o mais complicado dos dois:
void ListaDeInt::remove(Iterador& iterador) { assert(cumpreInvariante()); assert(iterador vlido); assert(iterador != incio() and iterador != fim()); --nmero_de_itens;

A decrementao do nmero de itens fundamental para o bom funcionamento do ciclo!


for(int i = iterador.ndice_do_item_referenciado; i != nmero_de_itens; ++i) itens[i] = itens[i + 1];

O iterador ca automaticamente no local apropriado.


assert(cumpreInvariante()); }

Para terminar denem-se dois dos mtodos construtores de iteradores:


inline ListaDeInt::Iterador ListaDeInt::primeiro() { assert(cumpreInvariante());

Constri-se um novo iterador para esta lista, que referencia inicialmente o item na frente da lista (ver construtor da classe ListaDeInt::Iterador), e devolve-se imediatamente o iterador criado.
return Iterador(*this); } inline ListaDeInt::Iterador ListaDeInt::ltimo() { assert(cumpreInvariante()); Iterador iterador(*this);

556

CAPTULO 10. LISTAS E ITERADORES

Aqui, depois de construdo um novo iterador, altera-se o seu ndice, de modo a que referencie o item desejado (o da traseira).
iterador.ndice_do_item_referenciado = nmero_de_itens - 1;

No boa ideia que seja a classe ListaDeInt a invocar directamente o mtodo de vericao do invariante da classe ListaDeInt::Iterador: Neste caso est-se mesmo a introduzir um excesso de intimidade... Mais tarde se ver uma melhor soluo para este problema.
assert(iterador.cumpreInvariante()); return iterador; }

10.4.4 Implementao dos mtodos pblicos de ListaDeInt::Iterador


Neste seco denir-se-o alguns dos mtodos da classe ListaDeInt::Iterador. Os restantes cam como exerccio para o leitor, que poder depois conferir com a implementao total do mdulo lista_int que se encontra na Seco H.1. O primeiro mtodo a denir o construtor da classe. Este mtodo inicializa um novo ponteiro e modo a que referencie o primeiro item de uma lista dada:

inline ListaDeInt::Iterador::Iterador(ListaDeInt& lista_a_associar)

O atributo lista_associada uma referncia, pelo que neste caso no apenas conveniente ou recomendvel usar uma lista de inicializadores: obrigatrio.
: lista_associada(lista_a_associar), ndice_do_item_referenciado(0) {

Mais uma vez se termina vericando a condio invariante de instncia.


assert(cumpreInvariante()); }

O operador de decrementao serve para recuar o item referenciado na lista:

inline ListaDeInt::Iterador& ListaDeInt::Iterador::operator -- () { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.incio());

10.4. IMPLEMENTAO SIMPLISTA


Basta decrementar o ndice do item referenciado.
--ndice_do_item_referenciado; assert(cumpreInvariante()); return *this; }

557

O operador de igualdade entre iteradores muito simples. Dois uteradores vlidos e associados mesma lista so iguais se referenciarem o mesmo item, ou seja, se tiverem o mesmo valor no atributo ndice_do_item_referenciado:
inline bool ListaDeInt::Iterador:: operator == (Iterador const& outro_iterador) const { assert(cumpreInvariante() and outro_iterador.cumpreInvariante()); // assert( vlido and outro_iterador vlido); // assert(iteradores associados mesma lista...);

importante garantir que os iteradores esto associados mesma lista. O problema resolverse- no prximo captulo, quando se introduzirem os conceitos e ponteiro e endereo.
return ndice_do_item_referenciado == outro_iterador.ndice_do_item_referenciado; }

Finalmente, dene-se o mtodo inspector que devolve uma referncia para o item referenciado pelo iterador:
inline ListaDeInt::Item& ListaDeInt::Iterador::item() const { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.incio() and *this != lista_associada.fim());

Esta linha de cdigo, em que a classe ListaDeInt::Iterador acede a um atributo da classe ListaDeInt uma das razes pelas quais se declarou a primeira classe amiga da segunda.
return lista_associada.itens[ndice_do_item_referenciado]; }

558

CAPTULO 10. LISTAS E ITERADORES

10.5 Uma implementao mais eciente


!!!!!!!!!!Imprimir apndice com listagens e reproduzir em cdigo! Depois ir buscar verso em cdigo antiga da implementao com cadeias e actualizar de modo a ser o mais parecida possvel com a que obtive. !!!!Aqui o fundamental so os bonecos. Fazer gura sideways! Comear por escrever as classes ListaDeInt e ListaDeInt::Iterador no quadro (verso simples com matrizes). Pelo menos as partes principais. Explic-las brevemente. Vamos ver uma pequena utilizao de listas. Suponham que se pretendia ler informao (nome e nmero) acerca de um conjunto de 100 alunos e depois escrev-la por ordem alfabtica do nome. Podia-se comear por fazer uma classe para representar um aluno: class Aluno { public: Aluno(string const& nome = "", int nmero = 0); string const& nome() const { return nome_; } int nmero() const { return nmero_; } private: string nome_; int nmero_; }; Agora, s alterar a classe ListaDeInt para guardar alunos. O que preciso fazer? Discutir alteraes. Concluir que suciente alterar o typedef e o nome da lista... Falta agora o programa para ler os 100 alunos e escrev-los por ordem alfabtica. int main() { ListaAluno lista; for(int i = 0; i != 100; ++i) { string nome; // uma s palavra! int nmero; cin > > nome > > nmero; Aluno aluno(nome, nmero); // Inserir por ordem... } for(ListaAluno::Iterador i = lista.primeiro(); i != lista.m(); ++i) cout < < i.item().nome() < < < < i.item().numero() < < endl; } Como inserir por ordem? Discutir brevemente o algoritmo. Concluir que tem de se procurar o primeiro nome maior e inserir antes. Se no se encontrar, insere-se antes do m. int main() { ListaAluno lista; for(int i = 0; i != 100; ++i) { string nome; // uma s palavra! int nmero; cin > > nome > > nmero; Aluno aluno(nome, nmero); Iterador i = lista.primeiro(); while(i != lista.m && i.item().nome() <= nome) ++i; lista.insereAntes(i, aluno); } for(ListaAluno::Iterador i = lista.primeiro(); i != lista.m(); ++i) cout < < i.item().nome() < < < < i.item().numero() < < endl; } Perfeito! Vimos que as listas, como esto, so muito simpticas. Mas... Quando se insere sistematicamente a meio da lista (que o caso do programa que zemos) a implementao que usmos para as lista muito ineciente. J viram o que acontece se um 100 o aluno for uma Ana? Fica a primeira da lista... e isso implica empurrar todos os alunos que constam na lista... E reparem que um problema semelhante ocorre quanto se removem itens que no estejam no m da lista: preciso puxar os que j l esto para trs... Que se pode fazer para melhorar a situao? Suponham que havia apenas trs alunos na lista: Ana Carlos Duarte

10.5. UMA IMPLEMENTAO MAIS EFICIENTE

559

e que se pretende inserir o Berto antes do Carlos. Porque que temos de empurrar o Carlos e o Duarte? Algum sabe? Discutir. Soluo: porque assumimos que a ordem dos itens na lista era a mesma que a ordem dos itens na matriz... E se a ordem dos elementos na matriz pudesse ser diferente da ordem dos itens na lista? A poda-se pr o Berto no m, sem deslocar ningum... Colocar. Mas como saber ento que o Berto est a seguir da Ana e antes do Carlos? Discutir. Concluir que cada elemento da matriz precisa de saber onde est item seguinte da lista.

10.5.1 Cadeias ligadas


Como representar essa informao? Discutir possibilidades. Concluir que se pode usar um ndice em cada elemento da matriz! Mas ento, cada elemento da matriz guarda... Um item e um ndice... Logo, j no basta uma matriz de ndices... Concluir que necessria uma estrutura. Chamar-lhe Elo (da cadeia). Introduzir a noo de CADEIA LIGADA. Introduzir a noo de CADEIA SIMPLESMENTE LIGADA. Construir a classe e us-la na lista: class ListaDeInt { public: ... private: struct Elo { Item item; int seguinte; }; Elo elos[nmero_mximo_de_itens]; ... }; Representar as ligaes por setas no quadro! E agora? Como implementar o mtodo insereAntes()? inline void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(not estCheia()); ++nmero_de_itens; ??????? } Discutir algoritmo. Admitir que h um mtodo reserva() que procura um elo livre na e devolve o seu ndice. Concluir onde se guarda o item e que fazer ao ndice do seguinte. inline void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(not estCheia()); ++nmero_de_itens; int const i = reserva(); elos[i].item = item; elos[i].seguinte = iterador.ndice; ?????? } Tudo bem? No! Porqu? O elo anterior no est ligado ao que acabmos de inserir! Temos de pr o seu seguinte com o valor i! Mas onde est o elo anterior? Discutir. Concluir que no forosamente o elemento da matriz com o ndice anterior. Que fazer ento? Sugestes?

560

CAPTULO 10. LISTAS E ITERADORES

Discutir possibilidades. Concluir que a mais simples fazer a cadeia DUPLAMENTE LIGADA. Ento preciso alterar a classe Elo. Alterar. struct Elo { Item item; int anterior; int seguinte; }; E agora o mtodo insereAntes() ca... Discutir calmamente... inline void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(not estCheia()); ++nmero_de_itens;

int const i = reserva(); elos[i].item = item; elos[i].seguinte = iterador.ndice; int const anterior = elos[iterador.ndice].anterior; elos[anterior].seguinte = i; // ou simplesmente: // elos[elos[iterador.ndice].ant = i; ????? } Falta alguma coisa? Representar as ligaes para trs no quadro... Concluir que falta actualizar o ndice do anterior na posio do iterador e do novo elo! inline void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(not estCheia()); ++nmero_de_itens; int const i = reserva(); elos[i].item = item; elos[i].anterior = elos[iterador.ndice].anterior; elos[i].seguinte = iterador.ndice; elos[elos[iterador.ndice].anterior].seguinte = i; elos[iterador.ndice].anterior = i; } Perfeito! Perfeito? Bolas... E se o iterador se referir ao primeiro item da lista? Que instrues que no se podem fazer? Pior, como que se sabe onde a cadeia de itens comea, i.e., onde est o primeiro item da lista? No forosamente o primeiro elemento da matriz! Discutir assunto. Dizer para se lembrarem dos itens ctcios da lista. Os itens ctcios da lista tm um caracterstica importante: "existem" (entre aspas) sempre, mesmo com a lista vazia. Logo o problema resolve-se... Discutir. Concluir que d jeito dar existncia real aos itens ctcios! Sugerir coloc-los no nal da matriz, que passa a 102 elementos. Mas ento o que deve fazer o construtor da lista? Discutir. Concluir que uma possibilidade : class ListaDeInt { ... private: ... static int const inicial = numero_mximo_de_itens; static int const nal = numero_mximo_de_itens + 1; }; ListaDouble::ListaDouble() : nmero_de_itens(0) { elos[inicial].seguinte = nal; elos[nal].anterior = inicial; ?????? // falta aqui qualquer coisa... } O anterior do inicial e o seguinte do nal podem car com lixo... Nunca so precisos! E agora, o procedimento insere como funciona?

10.5. UMA IMPLEMENTAO MAIS EFICIENTE

561

Concluir que funciona sempre sem problemas desde que... o iterador no se rera ao item (ctcio) inicial... inline void ListaDeInt::insereAntes(Iterador& iterador, Item const& novo_item) { assert(not estCheia()); assert(iterador.ndice != inicial); ++nmero_de_itens; int const i = reserva(); elos[i].item = item; elos[i].anterior = elos[iterador.ndice].anterior; elos[i].seguinte = iterador.ndice; elos[elos[iterador.ndice].anterior].seguinte = i; elos[iterador.ndice].anterior = i; } E o mtodo remove()? Discutir. Concluir: inline void ListaDouble::remove(Iterador& iterador) { assert(iterador.ndice != inicial and iterador.ndice != nal); nmero_de_itens; int const i = iterador.ndice; elos[elos[i].anterior].seguinte = elos[i].seguinte; elos[elos[i].seguinte].anterior = elos[i].anterior; // Avanar itereador! iterador.ndice = elos[i].seguinte; liberta(i); } Discutir mtodo liberta(). Adimos os problemas de reservar uma posio livre da matriz, quando se est a inserir um item, ou de libertar uma posio ocupada, quando se est a remover um item. Discutir forma de saber que elementos da matriz esto livres. Concluir que uma boa forma usar um ndice para primeiro livre e em cada elemento livre usar o ndice para seguinte e fazer cadeia simplesmente ligada sem guardas de elos livres. Notar que o construtor tem de ser alterado de modo a colocar todos os elementos da matriz (excepto as guardas) como elos livres. Deixar para as aulas prticas a discusso pormenorizada. Discutir interface vs. implementao. Concluir que se melhorou a classe sem alterar a sua interface. Os programas que a usavam no precisam de ser alterados. Deixar a implementao da parte restante da classe para as aulas prticas. !!!!!!!!!!!!!Ver cdigo das tericas, verso 2 das listas. Listagens completas?

562

CAPTULO 10. LISTAS E ITERADORES

Captulo 11

Ponteiros e variveis dinmicas


!!!No esquecer de criar mtodo listaAssociada() nos iteradores para poder melhorar a PC de insereAntes e remove! !!!!!!!!!!!!Colocar resumos das aulas 4 e 5. 1. Ponteiros resumo ou guio da aula 4 2. Listas e iteradores usando ponteiros mudana simples das listas e iteradores do captulo anterior 3. Variveis dinmicas resumo ou guio da aula 5 4. Listas e ponteiros usando variveis dinmicas 5. Classes com variveis dinmicas espalhar resumo ou guio da aula 6 Construtores e destrutores Cpias: introduo s noes de igualdade e identidade. 6. Introduo s excepes bad_alloc. Capturando. Problemas. !! Falar de construtor por cpia: voltar ao contador de instncias do captulo sobre TAD.

563

564

CAPTULO 11. PONTEIROS E VARIVEIS DINMICAS

Captulo 12

Herana e polimorsmo
!!!!!O nome do captulo deve reectir a utilidade das coisas

565

566

CAPTULO 12. HERANA E POLIMORFISMO

Captulo 13

Programao genrica
!!Expandir resumo da aula 11.

567

568

CAPTULO 13. PROGRAMAO GENRICA

Captulo 14

Excepes e tratamento de erros


!!Colocar aqui resumo de aula do ano passado. Adaptar de modo a deixar claro que excepes so geralmente mais apropriadas que asseres para lidar com erros do programador (a ideia que o programa aborte na mesma, mas pode-se capturar a excepo e tentar fazer o programa morrer honradamente, salvando o que puder ser salvo). Guio da 12a aula terica Sumrio 1.Tratamento bsico de erros ocorridos durante a execuo de programas no crticos: a.Origens dos erros: lgicos, utilizador, recursos externos. b.Tratamento de erros: instrues usuais (condicionais, de seleco, de iterao), asseres e excepes. c.Origens humanas imediatas (programador e utilizador) versus recursos externos (e.g., formato de cheiros, existncia de cheiros, limitaes de memria, etc.). d.Papeis do humano: programador fabricante, programador utilizador e utilizador nal. e.Proteco contra erros lgicos (do programador): Utilizao de asseres. Os erros devem ser detectados. So corrigidos pelo programador e no pelo prprio programa. Vantagens da utilizao de assert. Pr-condies: o programador fabricante simplica a vida do programador utilizador. Condies objectivo e invariantes (de ciclos ou de classe): o programador fabricante simplica a sua prpria vida. Utilizao de assert durante o teste do programa. Desligando o efeito de assert sem retirar as instrues do cdigo. Efeitos de uma assero falhada: cheiro core e sua utilidade. f.Proteco contra erros do utilizador: Princpio: o utilizador enganar-se- muitas vezes e quando os efeitos forem mais nefastos. O utilizador humano: repetio como soluo tpica. Os erros so detectados e corrigidos localmente. g.Proteco contra erros com origem em recursos externos: Filosoa: os recursos esto disponveis e sem problemas excepto em casos excepcionais. Utilizao de excepes. Os erros so detectados e so lanadas excepes. S captura a excepo quem sabe lidar com o erro. A captura das excepes pode ser feita "muito longe" do cdigo que as origina. Efeito de uma excepo no capturada: de novo o cheiro core. Classes para excepes. Hierarquias de erros e polimorsmo. Guio !!!!!!!!!!!!!Isto tranformou-se num captulo... Tornar captulo mesmo e refazer guio... Nesta aula vamos falar de tratamento bsico de erros. Note-se que este um assunto complicado em geral. Vamos assumir que estamos a desenvolver uma aplicao "normal". I.e., vamos 569

570

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

admitir que uma falha do programa muito inconveniente mas no dramtica (nem fatal). o caso, por exemplo, de um processador de texto. Se fosse um programa de controlo de trfego areo, de clculo de trajectrias de msseis, de controlo do arrefecimento de uma central nuclear, teramos de usar estratgias muito diferentes e nada triviais. Comecemos por identicar as possveis fontes de erros num programa: Discutir com eles! Concluir em: 1.Utilizador humano do programa. 2.Erros do programador (quer como programador utilizador quer como programador fabricante). 3.Recursos externos inadequados (errados, indisponveis ou insucientes). H um princpio bsico na programao: o utilizador errar. H outro princpio bsico na programao: o utilizador falhar na pior ocasio (a nica no prevista) Como deve o programa lidar com erros do utilizador? Discutir garantido que os utilizadores humanos do programa se vo enganar. inevitvel. Com estes erros devemos lidar explicitamente no programa vericando e lidando com os erros onde quer que eles possam ocorrer. H outro princpio na programao: o programador errar. H ainda outro princpio na programao: alguns os erros do programador resistiro a todos os teste e revelar-se-o apenas depois de o programa ter sido distribuido ao seu utilizador nal... Como se deve lidar com estes erros? Discutir. No preciso concluir nada. Deixar para depois. E os recursos externos? Estes so recursos que esto fora do controlo de programador e do programa propriamente ditos. Exemplos: Falta de memria Falta de espao em disco Falta de cheiro Formato de cheiro errado Por outro lado, no razovel pedir ao computador para corrigir estes erros, tal como se faria com um humano! Como se deve lidar com estes erros? Discutir. No preciso concluir nada. Deixar para depois. Note-se que os erros tm muitas vezes origem humana. O utilizador do programa tipicamente um humano. O programador, quer como programador utilizador quer como programador fabricante, tambm um humano. Muitas vezes o mesmo humano que desempenha alternada ou simultaneamente papis diferentes! Um programador fabrica cdigo usando cdigo j escrito, compila e testa o programa, assumindo neste processo os trs papis. O que temos de descobrir como se deve lidar em geral com os vrios tipos de erros. Como ferramentas temos as instrues de seleco, condicionais ou de iterao da prpria linguagem, as asseres e as excepes. Quando adequado usar cada uma destas ferramentas? Vamos ver alguns programas de exemplo e estudar os possveis erros que podem ocorrer no seu contexto.

571 Por exemplo, suponham um programa simples para clculo duma raiz quadrada pelo mtodo de Newton: Explicar vagamente o mtodo e dizer para eles deduzirem a expresso de progresso enquanto passo o programa no quadro. Deixar claro que no a forma mais eciente de calcular a raiz quadrada... #include <iostream> #include <limits> using namespace std; // PC: v >= 0 // CO: v (1 - e) < raiz raiz < v (1 + e), onde e (epsilon) a diferena entre o // menor double maior que 1 e 1, representando portanto de alguma forma o limite mnimo // de erro alcanvel. double raiz(double v) { if(v == 0.0) return 0.0; double r = v; double const e = numeric_limits<double>::epsilon(); while(true) { double nr = 0.5 * (r + v / r); if(nr * nr - v < e * v) return nr; r = nr; } } int main() { cout < < "Introduza um valor: "; double valor; cin > > valor; cout < < "raiz = " < < raiz(valor) < < endl; } Que acontece se o valor dado pelo utilizador for negativo? E se o utilizador pressionar a em vez de um nmero? Se o valor for negativo a funo entra em ciclo innito! De quem o problema? Do programador, obviamente! Ele deve garantir que as pr-condies da funo se vericam. Mas qual programador? Quem programou a funo? Ou quem programou o main? Bom, os dois, mas de formas totalmente diferentes. Quem programa a funo (o programador fabricante) no pode assumir muito acerca de futuras utilizaes. Responsabiliza-se pelo valor devolvido pela funo apenas se as pr-condies se vericarem. esse o contrato que estabelece com o programador utilizador da funo. Mas pode ser simptico e garantir que, enquanto o programa estiver em teste, a passagem de argumentos que no cumpram a PC levem terminao do programa com uma mensagem apropriada. Para isso usam-se asseres. Note-se que o programador da funo est a prepar-la para detectar erros de outros programadores, os programadores utilizadores (que podem ser a mesma pessoa)! A funo no sabe nada acerca do humano que usa o programa! Assim: #include <iostream> #include <limits> #include <cassert> using namespace std; // PC: v >= 0 // CO: v (1 - e) < raiz raiz < v (1 + e), onde e (epsilon) a diferena entre o // menor double maior que 1 e 1, representando portanto de alguma forma o limite mnimo // de erro alcanvel. double raiz(double v) { assert(v >= 0.0); if(v == 0.0) return 0.0; double r = v; double const e = numeric_limits<double>::epsilon(); while(true) { double nr = 0.5 * (r + v / r); if(nr * nr - v < e * v) return nr; r = nr; } } int main() { cout < < "Introduza um valor: "; double valor; cin > > valor; cout < < "raiz = " < < raiz(valor) < < endl; } Desse modo, o cdigo fornecedor da funo contm uma proteco contra erros no cdigo cliente.

572

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

importante perceber que a colocao de uma assero para vericao das pr-condies da funo serve para proteger o programador utilizador dessa funo dos seus prprios erros. Por outro lado, possvel tambm o programador fabricante proteger-se dos seus prprios erros colocando uma assero para vericao das condies objectivo da funo. Neste caso a CO razoavelmente complicada, pelo que no se entra em pormenores: #include <iostream> #include <limits> #include <cassert> using namespace std; // PC: v >= 0 // CO: v (1 - e) < raiz raiz < v (1 + e), onde e (epsilon) a diferena entre o // menor double maior que 1 e 1, representando portanto de alguma forma o limite mnimo // de erro alcanvel. double raiz(double v) { assert(v >= 0.0); if(v == 0.0) return 0.0; double r = v; double const e = numeric_limits<double>::epsilon(); while(true) { double nr = 0.5 * (r + v / r); if(nr * nr - v < e * v) { assert(v - nr * nr < e * v && nr * nr - v < e * v); return nr; } r = nr; } } int main() { cout < < "Introduza um valor: "; double valor; cin > > valor; cout < < "raiz = " < < raiz(valor) < < endl; } Desta forma se o programador fabricante se enganar ao escrever a funo, a assero falhar e assinalar que a aproximao no sucientemente boa. As asseres tm uma vantagem adicional: se falharem o programa termina no apenas com uma mensagem de erros mais ou menos inteligvel mas tambm gerando um cheiro de "core". Este cheiro contm uma imagem completa do processo no momento em que a assero falhou. Esta imagem pode ser usada para determinar ou pelo menos para facilitar a determinao das causas para a falha. Para isso basta executar o depurador e dizer-lhe para carregar a imagem do processo. Se o programa se chamasse "raiz", por exemplo, bastava fazer: gdb raiz core core A partir deste instante possvel vericar o valor de todas as variveis no momento da falha. Para isso pode ser til usar o comando up, que sobe um nvel na hierarquia de chamadas de funes e procedimentos, at que a funo ou procedimento mostrado seja um dos que esto em desenvolvimento. Agora, se o utilizador do programa introduzir um valor negativo o programa aborta com uma mensagem antiptica. Isso til durante o teste do programa. Mas muito indesejvel para o seu utilizador! De quem a culpa? Do programador fabricante de main() e utilizador de raiz()! Ele deveria vericar se o valor dado pelo utilizador aceitvel! A assero tem como grande utilidade mostrar claramente ao programador de main() que h algo de errado com o seu cdigo. Ele vai vericar este erro durante os testes do programa. Presumivelmente antes de entregar o programa ao utilizador nal. Note-se que h aqui trs papeis diferentes: o programador fabricante de raiz(), o programador utilizador de raiz() e o utilizador do programa! Durante o desenvolvimento do programa estes podem ser papeis tomados alternadamente pela mesma pessoa. A soluo passa pois por reconhecer que o utilizador nal se pode enganar e escrever o programa prova desses erros! #include <iostream> #include <limits> #include <cassert>

573 using namespace std; // PC: v >= 0 // CO: v (1 - e) < raiz raiz < v (1 + e), onde e (epsilon) a diferena entre o // menor double maior que 1 e 1, representando portanto de alguma forma o limite mnimo // de erro alcanvel. double raiz(double v) { assert(v >= 0.0); if(v == 0.0) return 0.0; double r = v; double const e = numeric_limits<double>::epsilon(); while(true) { double nr = 0.5 * (r + v / r); if(nr * nr - v < e * v) { assert(v - nr * nr < e * v && nr * nr - v < e * v); return nr; } r = nr; } } double lePositivo() { while(true) { double valor; cout < < "Introduza um valor: "; cin > > valor; if(valor >= 0.0) return valor; cout < < "O valor tem de ser >= 0!" < < endl; } } int main() { double valor = lePositivo(); cout < < "raiz = " < < raiz(valor) < < endl; } Fazer uma nota acerca dos estranhos ciclos! Explicar que estam entre um while e um do while. No uma heresia... Feito isto e testado o programa, cou garantido que a funo raiz ser chamada com um valor no negativo. Nesse caso a vericao das asseres j no so necessrias! Logo, pode-se retir-la do cdigo. Hmmm..... Ser boa ideia? No! O programa ainda pode precisar de acrescentos, melhorias, correces, e portanto de ser testado de novo! Se isso acontecer d muito jeito que a assero l continue! Como resolver o problema ento? ineciente estar l quando o programa est testado mas conveniente que l continue porque podemos precisar de testar o programa de novo! Soluo: desligar as asseres. Como? Compilando com a opo -DNDEBUG (no debug). Para acelerar o teste de uma aplicao comum distribuir verses no nais, as chamadas verses alfa ou beta, a utilizadores seleccionados. Estas verses devem ser distribuidas sem desligar as asseres. que se pode pedir aos utilizadores para, em caso de erro, enviarem uma mensagem de correio electrnico com: 1. Mensagem de erro gerada (identica a assero falhada). 2. Ficheiro imagem: permite inspeco do estado do programa. 3. Descrio breve dos passos que geraram o erro: facilita perceber como se atingiu o estado de erro. Problema resolvido... De certeza? E se o utilizador escrever uma letra? A a leitura falha! E pior, o canal ca num estado de erro em que todas as tentativas de leituras posteriores falham tambm... Fica em ciclo innito! Explicar. Soluo: #include <iostream> #include <limits> #include <cassert> using namespace std; // PC: v >= 0 // CO: v (1 - epsilon) < raiz * raiz < v (1 + epsilon), onde epsilon a // diferena entre o menor double maior que 1 e 1, representando // portanto de alguma forma o limite mnimo de erro alcanvel. double raiz(double v) { assert(v >= 0.0); if(v == 0.0) return 0.0; double r = v; double const e = numeric_limits<double>::epsilon(); while(true) { double nr = 0.5 * (r + v / r); if(nr * nr - v < e * v) { assert(v - nr * nr < e * v && nr * nr - v < e * v); return nr; } r = nr; } } // Ver Slang++ para uma soluo mais genrica e elegante... void ignoraLinha() { cin.clear(); char c; while(cin.get(c) && c != \n) ; }

574

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

double lePositivo() { while(true) { double valor; cout < < "Introduza um valor: "; if(!(cin > > valor)) { ignoraLinha(); cout < < "Tem de ser um nmero real!" < < endl; } else if(valor >= 0.0) return valor; else cout < < "O valor tem de ser >= 0!" < < endl; } } int main() { double valor = lePositivo(); cout < < "raiz = " < < raiz(valor) < < endl; } Mas, e a terceira fonte de erros? Erros devido a recursos externos ao programa? Exemplos? Falta de memria, falta de espao em disco, cheiro inexistente, cheiro com formato errado... H aqui algo que me cheira a trabalho nal... !!!!!!!!!Este exemplo baseia-se no trabalho nal de 1999/2000. Pode precisar de ser refeito em anos posteriores... !!!!!!!!!!!Ao passar este exemplo para folhas tericas conveniente basear-me na hierarquia de formas desenvolvida nos captulos anteriores. Talvez essa hierarquia de formas deva passar a usar o Slang++.... Suponhamos que queramos escrever uma classe Polgono usando o Slang++ e que essa classe deveria estar integrada numa hierarquia de conceitos encimada pela classe abstracta Forma: !!!!!!!!!Nas folhas tericas pr Slang::. class Forma /* simplicada... */ { public: // Com origem em origem: Forma(Posicao const& origem); // Tem de haver sempre no topo das hierarquias polimrcas: virtual ~Forma() {} // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const = 0; // Todas as formas podem ser desenhadas no ecr: virtual desenha(bool seleccionada = false) = 0; // Devolve origem da forma: Posicao const& origem() const; // Move a forma estabelecendo uma nova origem: void movePara(Posicao const& nova_origem); // Move a forma deslocando a sua origem: void moveDe(Posicao const& deslocamento); private: Posicao origem_; }; class Polgono : public Forma { public: // Sem vrtices com origem em origem: Poligono(Posicao const& origem); // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const; // O vertice v tem coordenadas absolutas... void novoVrtice(Posicao const& v); virtual void desenha(bool seleccionado = false); ... private: list<Posicao> vertices; }; A ideia que um polgono representado pela sequncia dos seus vrtices, admitindo-se que cada vrtice se liga por uma aresta aos vrtices adjacentes na sequncia, sendo que o primeiro e o ltimo vrtice se consideram adjacentes. Note-se que a posio dos vrtices relativa origem do polgono, que uma posio convencional. Mas pretendamos mais. Deveria ser possvel carregar e guardar polgonos de e em cheiros... Para isso acrescentaramos:

575 1.Um construtor para construir um polgono a partir de informao lida de um canal. 2.Um procedimento carrega() para carregar informao de um canal, descartando a informao anteriormente guardada no polgono. 3.Um procedimento guarda() para guardar a informao escrevendo num canal. 4.E faramos o mesmo a todas as classes da hierarquia... Ou seja: class Forma { public: // Com origem em origem: Forma(Posicao const& origem); // Lida de canal: Forma(istream& entrada); // Tem de haver sempre no topo das hierarquias polimrcas: virtual ~Forma() {} // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const = 0; // Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; // Todas as formas podem ser desenhadas no ecr: virtual desenha(bool seleccionada = false) = 0; // Devolve origem da forma: Posicao const& origem() const; // Move a forma estabelecendo uma nova origem: void movePara(Posicao const& nova_origem); // Move a forma deslocando a sua origem: void moveDe(Posicao const& deslocamento); private: Posicao origem_; }; class Polgono : public Forma { public: // Sem vrtices com origem em origem: Poligono(Posicao const& origem); // Lido de canal: Poligono(istream& entrada); // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const; // Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; virtual void desenha(bool seleccionado = false) const; // O vertice v tem coordenadas absolutas... void novoVrtice(Posicao const& v); private: list<Posicao> vertices; }; Discutir implementao. inline Forma::Forma(Posicao const& origem) : origem_(origem) { } inline Forma::Forma(istream& entrada) : origem_(entrada) { // Que acontece se o construtor de Posicao falhar? } inline void Forma::carrega(istream& entrada) { origem_.carrega(entrada); // Que acontece se o procedimento Posicao::carrega() falhar? } inline void Forma::guarda(ostream& saida) { origem_.guarda(saida); // Que acontece se o procedimento Posicao::guarda() falhar? } inline Posicao const& Forma::origem() const { return origem_; }

576

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

inline void Forma::movePara(Posicao const& nova_origem) { origem_ = nova_origem; } inline void Forma::moveDe(Posicao const& deslocamento) { origem += deslocamento; } inline Poligono::Poligono(Posicao const& origem) : Forma(origem) { } Poligono::Poligono(istream& entrada) : Forma(entrada) // Que acontece se o construtor de Forma falhar? { int numero_de_vertices; if(!(entrada > > numero_de_vertices)) /* Que fazer se a extraco do nmero de vrtices falhar? */; while(numero_de_vertices != 0) vertices.push_back(Posicao(entrada)); // Que acontece se o construtor de Posicao falhar? } inline string const Poligono::nomeDoTipo() const { return "Poligono"; } void Poligono::carrega(istream& entrada) { Forma::carrega(entrada); // Que acontece se o procedimento Forma::carrega() falhar? vertices.clear(); int numero_de_vertices; if(!(entrada > > numero_de_vertices)) /* Que fazer se a extraco do nmero de vrtices falhar? */; while(numero_de_vertices != 0) vertices.push_back(Posicao(entrada)); // Que acontece se o construtor de Posicao falhar? } void Poligono::guarda(ostream& saida) const { Forma::guarda(saida); // Que acontece se o procedimento Forma::guarda() falhar? if(!(saida < < vertices.size())) /* Que fazer se a insero do nmero de vrtices falhar? */; for(list<Posicao>::const_iterator i = vertices.begin(); i != vertices.end(); ++i) i->guarda(saida)); // Que acontece se o procedimento Posicao::guarda() falhar? } inline void Poligono::novoVertices(Posicao const& v) { // O vertice v tem coordenadas absolutas, pelo que h que lhe retirar a origem do polgono: vertices.push_back(v - origem()); } void Poligono::desenha(bool seleccionado) const { ... } Para evitar repeties de cdigo pode-se factorizar o cdigo repetido num procedimento privado auxiliar da classe Poligono: class Forma { public: // Com origem em origem: Forma(Posicao const& origem); // Lida de canal: Forma(istream& entrada); // Tem de haver sempre no topo das hierarquias polimrcas: virtual ~Forma() {} // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const = 0; // Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; // Todas as formas podem ser desenhadas no ecr: virtual desenha(bool seleccionada = false) = 0; // Devolve origem da forma: Posicao const& origem() const; // Move a forma estabelecendo uma nova origem: void movePara(Posicao const& nova_origem); // Move a forma deslocando a sua origem: void moveDe(Posicao const& deslocamento);

577 private: Posicao origem_; }; class Polgono : public Forma { public: // Sem vrtices com origem em origem: Poligono(Posicao const& origem); // Lido de canal: Poligono(istream& entrada); // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const; // Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; virtual void desenha(bool seleccionado = false) const; // O vertice v tem coordenadas absolutas... void novoVrtice(Posicao const& v); private: list<Posicao> vertices; void carregaEspecico(istream& entrada); }; inline Forma::Forma(Posicao const& origem) : origem_(origem) { } inline Forma::Forma(istream& entrada) : origem_(entrada) { // Que acontece se o construtor de Posicao falhar? } inline void Forma::carrega(istream& entrada) { origem_.carrega(entrada); // Que acontece se o procedimento Posicao::carrega() falhar? } inline void Forma::guarda(ostream& guarda) { origem_.guarda(saida); // Que acontece se o procedimento Posicao::guarda() falhar? } inline Posicao const& Forma::origem() const { return origem_; } inline void Forma::movePara(Posicao const& nova_origem) { origem_ = nova_origem; } inline void Forma::moveDe(Posicao const& deslocamento) { origem += deslocamento; } inline Poligono::Poligono(Posicao const& origem) : Forma(origem) { } inline Poligono::Poligono(istream& entrada) : Forma(entrada) { // Que acontece se o construtor de Forma falhar? carregaEspecico(entrada); // Que acontece se o procedimento carregaEspecico() falhar? } inline string const Poligono::nomeDoTipo() const { return "Poligono"; } void Poligono::carrega(istream& entrada) { Forma::carrega(entrada); // Que acontece se o procedimento Forma::carrega() falhar? vertices.clear(); carregaEspecico(entrada); } void Poligono::guarda(ostream& saida) const { Forma::guarda(saida); // Que acontece se o procedimento Forma::guarda() falhar? if(!(saida < < vertices.size())) /* Que fazer se a insero do nmero de vrtices falhar? */; for(list<Posicao>::const_iterator i = vertices.begin(); i != vertices.end(); ++i) i->guarda(saida)); // Que acontece se o procedimento Posicao::guarda() falhar? }

578

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

void Poligono::carregaEspecico(istream& entrada) { int numero_de_vertices; if(!(entrada > > numero_de_vertices)) /* Que fazer se a extraco do nmero de vrtices falhar? */; while(numero_de_vertices != 0) vertices.push_back(Posicao(entrada)); // Que acontece se o construtor de Posicao falhar? } inline void Poligono::novoVertices(Posicao const& v) { // O vertice v tem coordenadas absolutas, pelo que h que lhe retirar a origem do polgono: vertices.push_back(v - origem()); } void Poligono::desenha(bool seleccionado) const { ... } Problemas: que fazer quando a leitura ou a escrita falham? Em primeiro lugar h que reconhecer que as causas para uma falha so normalmente externas: os recursos usados para leitura e escrita so tipicamente cheiros. Estes recursos externos no esto totalmente sob o controlo do programador... Por um lado claro que se uma leitura de um canal falhar no se pode pedir de novo os dados, pelo simples facto de no se poder assumir que existe um ente inteligente do outro lado do canal: a maior parte das vezes o canal estar ligado a um cheiro que, se no tiver o formato correcto, dicilmente se auto-corrigir... Por outro lado tambm no aceitvel que o programa simplesmente aborte quando alguma leitura falhar! Se voc fosse utilizador de um programa to temperamental certamente rogaria pragas ao seu programador. Logo, necessrio que as funes e os procedimentos assinalem os possveis erros para que o cdigo onde so invocados possa recuperar desses erros. Uma soluo clssica mas problemtica fazer os procedimentos devolver um valor booleano verdadeiro se tudo tiver corrido bem e falso no caso contrrio... Por exemplo: inline bool Forma::carrega(istream& entrada) { return origem_.carrega(entrada); // Que acontece se o procedimento Posicao::carrega() falhar? Devolve-se false. } bool Poligono::carrega(istream& entrada) { if(!Forma::carrega(entrada)) return false; // Que acontece se o procedimento Forma::carrega() falhar? Devolve-se false. vertices.clear(); return carregaEspecico(entrada); } bool Poligono::carregaEspecico(istream& entrada) { int numero_de_vertices; if(!(entrada > > numero_de_vertices)) return false; while(numero_de_vertices != 0) vertices.push_back(Posicao(entrada)); // Opps... Construtores no podem devolver valores... // Que acontece se o construtor de Posicao falhar? return true; } Esta soluo tem dois graves problemas: 1.No aplicvel no caso dos construtores, pois no devolvem qualquer valor. (Tambm se pode argumentar que devolvem o objecto construdo, mas nesse caso claro que no podem devolver um booleano...) 2.Obriga o programador utilizador do cdigo a vericar sistematicamente o valor devolvido pelos procedimentos. Como isso muito aborrecido, o programador tende a "distrair-se", e os erros cam por tratar... 3.Todas as funes e procedimentos tem de lidar com os erros explicitamente: ou recuperando desses erros ou limitando-se a assinar a sua

579 ocorrncia devolvendo um valor falso. Toda a sequncia de funes e procedimentos invocados desde o local onde ele pode ser corrigido (recuperao) at sua origem tm de lidar desta forma com os erros. Note-se que o local onde se pode corrigir um erro (recuperar) e o local onde se detecta o erro podem estar muito distantes... Isto mais uma vez muito maador e portanto leva a esquecimentos fatais pelos programadores. Discutir solues! A soluo ideal a utilizao de excepes. As vantagens so: 1.Podem-se lanar excepes inclusive nos construtores. 2.O programador utilizador no obrigado a lidar com as excepes. Se ningum capturar uma excepo lanada o programa aborta gerando um cheiro de imagem core, que pode ser analisado como indicado acima para vericar as razes que levaram ao lanamento da excepo. 3.S lida com as excepes quem sabe o que fazer com elas! Assim, cdigo que no sabe que fazer caso seja lanada uma excepo limita-se a no se preocupar: ou algum cdigo mais exterior se encarregar de recuperar dessa excepo ou o programa abortar. S obrigado a lidar com uma excepo o cdigo que pretende recuperar do erro que a originou. O Slang++ j utiliza excepes. Os procedimentos para guardar informao em canais lanam a excepo ErroAoGuardar em caso de erro e os procedimentos para carregar lanam a excepo ErroAoCarregar. Essas classes fazem parte de uma pequena hierarquia de excepes com o seguinte aspecto: // Esta classe serve de base a uma pequena hierarquia de classes representando excepes. class Erro { public: // Construtor da classe. mensagem uma mensagem explicando a origem da excepo. Erro(std::string const& mensagem) : mensagem(mensagem) { } // Destrutor virtual para poder sofrer derivaes... virtual ~Erro() { } /* Inspector da mensagem explicando a origem da excepo na forma de uma converso implcita para std::string. */ virtual operator std::string () const { return mensagem; } private: // A mensagem explicando a origem da excepo. std::string mensagem; }; // Esta classe serve para representar excepes de carregamento de objectos a partir de canais. class ErroAoCarregar : public Erro { public: // Construtor da classe. classe o nome da classe que originou a excepo. ErroAoCarregar(std::string const& classe) : Erro(std::string("Erro ao carregar ") + classe + "") { } }; // Esta classe serve para representar excepes ao guardar objectos usando canais. class ErroAoGuardar : public Erro { public: // Construtor da classe. classe o nome da classe que originou a excepo. ErroAoGuardar(std::string const& classe) : Erro(std::string("Erro ao guardar ") + classe + "") { } }; Assim, deve-se acrescentar cdigo para lanar excepes apropriadas em caso de erro: class Forma { public: // Com origem em origem: Forma(Posicao const& origem); // Lida de canal: Forma(istream& entrada); // Tem de haver sempre no topo das hierarquias polimrcas: virtual ~Forma() {} // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const = 0;

580

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

// Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; // Todas as formas podem ser desenhadas no ecr: virtual desenha(bool seleccionada = false) = 0; // Devolve origem da forma: Posicao const& origem() const; // Move a forma estabelecendo uma nova origem: void movePara(Posicao const& nova_origem); // Move a forma deslocando a sua origem: void moveDe(Posicao const& deslocamento); private: Posicao origem_; }; class Polgono : public Forma { public: // Sem vrtices com origem em origem: Poligono(Posicao const& origem); // Lido de canal: Poligono(istream& entrada); // Todos os tipos concretos de formas tm um nome: virtual string const nomeDoTipo() const; // Procedimentos para guardar e carregar: virtual void carrega(istream& entrada); virtual void guarda(ostream& saida) const; virtual void desenha(bool seleccionado = false) const; // O vertice v tem coordenadas absolutas... void novoVrtice(Posicao const& v); private: list<Posicao> vertices; void carregaEspecico(istream& entrada); }; Forma::Forma(Posicao const& origem) : origem_(origem) { } Forma::Forma(istream& entrada) : origem_(entrada) { // E se for lanada uma excepo no construtor de Posicao? Deixa-se passar... } void Forma::carrega(istream& entrada) { origem_.carrega(entrada); // E se for lanada uma excepo no procedimento Posicao::carrega()? Deixa-se passar... } void Forma::guarda(ostream& saida) { origem_.guarda(saida); // E se for lanada uma excepo no procedimento Posicao::guarda()? Deixa-se passar... } Posicao const& Forma::origem() const { return origem_; } void Forma::movePara(Posicao const& nova_origem) { origem_ = nova_origem; } void Forma::moveDe(Posicao const& deslocamento) { origem += deslocamento; } inline Poligono::Poligono(Posicao const& origem) : Forma(origem) { } Poligono::Poligono(istream& entrada) : Forma(entrada) // E se for lanada uma excepo no construtor de Forma? Deixa-se passar... { carregaEspecico(entrada); // E se for lanada uma excepo no procedimento carregaEspecico()? Deixa-se passar... } inline string const Poligono::nomeDoTipo() const { return "Poligono"; } void Poligono::carrega(istream& entrada) { Forma::carrega(entrada); // E se for lanada uma excepo no procedimento Forma::carrega()? Deixa-se passar... vertices.clear();

581 carregaEspecico(entrada); // E se for lanada uma excepo no procedimento carregaEspecico()? Deixa-se passar... } void Poligono::guarda(ostream& saida) const { Forma::guarda(saida); // E se for lanada uma excepo no procedimento Forma::guarda()? Deixa-se passar... // Em caso de erro na insero lana-se explicitamente uma excepo! Note-se que as // inseres (operador < <) normalmente no lanam excepes, assinalando erro ao colocar o // canal em estado de erro, que tem de ser vericado explicitamente. if(!(saida < < vertices.size())) throw ErroAoGuardar("Poligono"); for(list<Posicao>::const_iterator i = vertices.begin(); i != vertices.end(); ++i) i->guarda(saida)); // E se for lanada uma excepo no procedimento Posicao::guarda()? Deixa-se passar... } void Poligono::carregaEspecico(istream& entrada) { // Em caso de erro na extraco lanase explicitamente uma excepo! Note-se que as // extraces (operador > >) normalmente no lanam excepes, assinalando erro ao colocar o // canal em estado de erro, que tem de ser vericado explicitamente. int numero_de_vertices; if(!(entrada > > numero_de_vertices)) throw ErroAoCarregar("Poligono"); while(numero_de_vertices != 0) vertices.push_back(Posicao(entrada)); // E se for lanada uma excepo no construtor de Posicao? Deixa-se passar... } inline void Poligono::novoVertices(Posicao const& v) { // O vertice v tem coordenadas absolutas, pelo que h que lhe retirar a origem do polgono: vertices.push_back(v - origem()); } void Poligono::desenha(bool seleccionado) const { ... } Note-se: 1.Se o construtor da Forma falhar ser lanada uma excepo! O construtor de Forma limita-se a construir uma posio. Essa construo que lana a excepo. O programador da classe Forma no tem de se preocupar com o assunto! 2.Idem se houverem erros nos procedimentos Forma::carrega() e Forma::guarda(). 3.Os procedimentos lanam excepes explicitamente caso ocorra algum erro que no resulte no lanamento de uma uma excepo. o caso dos procedimentos Poligono::guarda() e Poligono::carregaEspecico(). 4.Os procedimentos no recuperam de erros. Se detectarem erros, lanam excepes. Se os erros forem em funes ou procedimentos por eles invocados, limitam-se a ignorar as excepes por eles lanados. Quem responsvel por recuperar dos erros? Apenas que entender dever faz-lo. Suponha-se uma classe Figura que representa o conceito de Figura como uma agregao de Formas: class Figura { public: Figura(istream& entrada); void carrega(istream& entrada); void guarda(ostream& saida); ... void desenha() const; private: list<Forma*> formas; }; inline Figura::Figura(istream& entrada) { carrega(entrada); }

582

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

void Figura::carrega(istream& entrada) { // Em caso de erro na extraco lana-se explicitamente uma excepo! Note-se que as // extraces (operador > >) normalmente no lanam excepes, assinalando erro ao colocar o // canal em estado de erro, que tem de ser vericado explicitamente. int numero_de_formas; if(!(entrada > > numero_de_formas)) throw ErroAoCarregar("Figura"); while(numero_de_formas != 0) { string tipo_de_forma; if(!(entrada > > tipo_de_forma)) throw ErroAoCarregar("Figura"); if(tipo_de_forma == "Poligono") formas.push_back(new Poligono(entrada)); else if(tipo_de_forma == ...) ... else // Tipo de gura desconhecido. Talvez merecesse uma excepo mais especca... throw ErroAoCarregar("Figura"); } // E se for lanada uma excepo no construtor de Poligono? Deixa-se passar... } void Figura::guarda(ostream& saida) const { // Em caso de erro na insero lana-se explicitamente uma excepo! Note-se que as // inseres (operador < <) normalmente no lanam excepes, assinalando erro ao colocar o // canal em estado de erro, que tem de ser vericado explicitamente. if(!(saida < < formas.size())) throw ErroAoGuardar("Figura"); for(list<Forma*>::const_iterator i = formas.begin(); i != formas.end(); ++i) { if(!(saida < < (*i)>nomeDoTipo())) throw ErroAoGuardar("Figura"); (*i)->guarda(saida)); // E se for lanada uma excepo no procedimento Forma::guarda()? Deixa-se passar... } } Repare-se como se resolveu o problema de carregar e guardar os objectos usando o nome do seu tipo ( o problema da persistncia dos objectos). Suponha-se ainda uma classe Editor, com a responsabilidade de criar e editar guras: class Editor { public: ... void carrega(); void guarda() const; ... private: bool gura_alterada; Figura* gura; }; void Editor::carrega() { string nome = CaixaDeTexto("Ficheiro a carregar:").executa(); if(ifstream entrada(nome.c_str())) { try { Figura* nova_gura = new Figura(entrada); if(gura_alterada && MenuSimNao("Quer guardar a gura corrente?").executa()) guarda(); delete gura; gura = nova_gura; } catch(Erro& e) { Aviso(e).executa(); } catch(bad_alloc) { Aviso("Memria insuciente!").executa(); } catch(...) { Aviso("Erro desconhecido ao carregar gura!").executa(); } } else Aviso("No consegui abrir!").executa(); } claro que a classe Figura no quer lidar com possveis erros de carregamento, por exemplo. Que poderia ela fazer? Por outro lado a classe Editor tem obrigao de fazer alguma coisa. Essa classe responsvel, presume-se, pela interao com o utilizador do programa e por isso tem a responsabilidade de capturar excepes durante o carregamento de guras, se elas ocorrerem, e delicadamente avisar o utilizador do programa e dar-lhe a chance de prosseguir o seu trabalho. Note-se a estratgia seguida pelo editor: a gura anterior s descartada quando a nova foi carregada com sucesso. Assim, em caso de falha, o utilizador avisado e continua a editar a gura anterior. importante perceber-se que as excepes podem, e muitas vezes devem, ser capturadas bastante longe do local onde so lanadas. Repare-se no que sucede se ocorrer um erro na leitura da origem de um polgono da nova gura. A excepo lanada pelo construtor da classe

583 Posicao (do Slang++), invocado no construtor de Forma, invocado no construtor de Poligono, invocado no construtor de Figura, invocado no procedimento carrega() da classe Editor, que nalmente a captura! Note-se tambm que a herana permite a denio de hierarquias de excepes que podem ser usadas depois para capturas mais especcas ou mais genricas. Neste caso fez-se a captura de excepes da classe genrica Erro, que se presume representarem qualquer possvel tipo de erro do Slang++ (note-se a declarao da excepo capturada como referncia de modo a poder fazer uso do polimorsmo). Como podem ser lanadas excepes de outros tipos, nomeadamente bad_alloc se no houver memria ao criar alguma varivel dinmica, fazemse capturas em sequncia, chegando-se ao extremo de capturar qualquer excepo que no seja capturada pelos catch anteriores. !!!!!!!!!!!!!!!Daqui para baixo so conceitos avanados! O que se disse at agora s parte da verdade... que se uma excepo for lanada a meio de uma operao complexa (e.g., um carregamento) pode haver objectos que quem num estado invlido! Isso muito perigoso! Esta questo tem de ser resolvida pelos programadores fabricantes. Estes no se podem limitar a lanar excepes em caso de erro e ignorar as excepes com que no queiram lidar... Podem faz-lo mas apenas se garantirem que todos os objectos cam num estado aceitvel. A isto chama-se "segurana face a excepes" [stroustrup]. Segundo Stroustrup [??] h trs nveis de segurana face a excepes que podem ser garantidos pelos programadores fabricantes de uma dada funo ou procedimento membro ou no membro: 1.Nvel bsico: todos os objectos cam num estado vlido (ou seja, vericando a CII da sua classe). 2.Nvel forte: ou h total sucesso ou lanada uma excepo e a funo ou procedimento no tem quaisquer efeitos, deixando os objectos exactamente no mesmo estado que tinham antes do seu incio. 3.Sem lanamento: a funo ou procedimento no lanar nenhuma excepo (directa ou indirectamente), tendo sempre sucesso. Em nenhum dos casos so aceitveis fugas de memria, como bvio. claro que se deve lutar sempre por dar as garantias mais fortes! Mas isso no deve ser feito custa de uma complexidade exagerada dos programas ou de grandes perdas de ecincia. Para ajudar neste processo, a linguagem C++ faz algumas garantias. A mais importante delas diz que ao se abandonar uma funo ou procedimento devido a uma excepo todos os objectos locais construdo so automaticamente destrudos. Claro est que esta garantia signica que se for lanada uma excepo num construtor, o objecto em construo no ser deixado semi-construdo: todos os atributos (variveis e constantes membros) entretanto construdos, assim como as classes base, sero automaticamente destrudos. Estas garantias so simpticas, mas no resolvem todos os problemas. Variveis dinmicas no so destrudas automaticamente, por exemplo. Em geral, qualquer recurso externo reservado numa funo ou procedimento no qual seja lanada uma excepo (explicita ou implicitamente) devem ser libertados sob risco de haver fugas... A concluso a que se chega, portanto, que a interface de uma funo ou procedimento no consiste apenas no seu cabealho (que indicam como se usa) e das pr-condio e condio objecto (que indicam o que se compromete a fazer e em que circunstncias): inclui tambm informao acerca das garantias de segurana face a excepes (que indicam o que sucede aos objectos envolvidos em casos excepcionais).

584

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

Analisemos as garantias que so dadas no cdigo apresentado: Classe Forma Se, num construtor da classe Forma, for lanada uma excepo na construo do atributo origem_, a construo no tem sucesso. O bom comportamento da classe Forma face a erros durante a construo do atributo origem_ depende por isso do bom comportamento dos construtors da classe Posicao. E durante o procedimento carrega() da classe Forma? Como a classe Forma se limita a invocar o procedimento carrega() da classe Posicao, tudo depende do bom comportamento desse procedimento. Acontece que quer os construtores da classe Posicao quer o seu procedimento carrega() oferecem o nvel mais forte de segurana face ao lanamento de excepes (esta garantia faz parte da interface da classe). Assim, tambm os construtores da classe Forma e o seu procedimento carrega() oferecem o o nvel mais forte de segurana face a excepes. E o procedimento guarda()? Aqui o problema diferente. que guardar no altera seno o canal onde se escreve: o objecto a guardar nunca alterado. Como no possvel voltar atrs no que se escreveu num canal de sada genrico, a ideia que se um procedimento guarda() falhar, deve-se considerar como lixo tudo o que foi escrito no canal... No fundo isto signica que, do ponto de vista do objecto forma, o procedimento oference o nvel forte de segurana, mas do ponto de vista do canal no, limitando-se a oferecer o nvel bsico de segurana. Calma... E o canal de entrada no caso do construtor a partir de um canal e do procedimento carrega(), ambos da classe Forma? No se pode simplesmente "des-ler" aquilo que j foi lido (por acaso at pode, mas com limitaes). Por isso, tambm essas operaes oferecem apenas o nvel bsico de segurana relativamente ao canal de entrada... Cientes das nuances face aos canais de entrada e sada apontadas, daqui em diante, para simplicar a discusso, o estado destes canais deixar de ser tido em conta. Classe Poligono Este caso mais interessante. O construtor de um polgono vazio ao qual se passa apenas a origem oferece naturalmente a segurana de no lanar qualquer excepo. Por outro lado o construtor a partir de um canal comea por invocar o construtor da classe Forma. Como este oferece o nvel forte de segurana, no h qualquer problema. Em seguida invocado implicitamente o construtor por omisso da lista de vrtices. Tambm este construtor oferece o nvel forte de segurana face a excepes. Finalmente, j no corpo do construtor, invocado o procedimento carregaEspecco(). Se durante a sua execuo for lanada uma excepo toda a construo do Poligono falha, sendo invocados os destrutores da lista de vrtices e da classe base. Perfeito. O procedimento carrega() da classe Poligono mais complicado. Comea por invocar o procedimento carrega() da classe Forma, que oferece o nvel mximo de segurana face a excepes. O problema que depois invoca o procedimento carregaEspecico(), que pode falhar... Vamos admitir que o procedimento carregaEspecco() se comporta "decentemente", i.e., ou consegue carregar aquilo que especco de um polgono (a lista dos vrtices), ou mantm a lista de vrtices como estava. Neste caso temos um problema complicado. que se o carregaEspecico() falhar, s parte do polgono foi carregado, nomeadamente a sua origem, atraves do

585 procedimento Forma::carrega(), cando a lista de vrtices como estava... Para que seja oferecido o nvel forte de segurana h que garantir que se consegue voltar atrs no carregamento. A soluo tpica : void Poligono::carrega(istream& entrada) { Poligono novo(entrada); *this = novo; } Ou seja, a soluo passou por recorrer no carrega() ao construtor da classe. Como o construtor oferece o nvel forte de segurana, uma de duas coisas podem ocorrer: 1.A construo tem sucesso. Nesse caso o novo polgono copiado para a instncia implcita e depois destrudo (implitamente). 2.A construo falha. Nesse caso no s o novo polgono no chega a ser construdo como a execuo no chega atribuio devido ao lanamento de uma excepo, cando o polgono intacto. Claro est que esta soluo tem uma desvantagem: ineciente. O que acontece de facto ao longo da sua execuo? 1. construdo um novo polgono lido de um canal. 2.O novo polgono atribudo instncia implcita. Ou seja: a. esvaziada a lista de vrtices da instncia implcita. b.So copiados os vrtices da nova lista para a lista da instncia implcita. 3.Finalmente invocado o destrutor do novo polgono, levando destruio da respectiva lista de vrtices. O problema est na feitura de uma cpia. Como evit-la? Outro problema da soluo acima que a atribuio entre polgonos pode lanar excepes... Porqu? Porque implica fazer uma cpia dos vrtices, e pode no haver memria para isso! Os dois problemas podem-se resolver de uma forma simples. As listas da biblioteca padro do C++ disponibilizam uma forma rpida de troca de itens: o procedimento membro swap. Pode-se ento reescrever: void Poligono::carrega(istream& entrada) { Poligono novo(entrada); // Copia o que genrico (nunca lana excepo): Forma::operator = (novo); // Troca o que especco (nunca lana excepo): vertices.swap(novo.vertices); } Como a atribuio entre duas Forma no lana excepes e o mesmo se passa com o procedimento swap(), esse problema est resolvido. Tambm ca resolvido o problema da cpia. Agora as listas so trocadas (sem cpia), o que faz com que a lista dos vrtices antigos v parar ao novo polgono, sendo destruda quando esta varivel for destruda, i.e., ao se atingir a chaveta nal do procedimento. Classe Figura A discusso para as guras semelhante discusso para os polgonos, pelo que no se trata aqui. Assuntos por resolver Uma questo que no se abordou aqui, mas deveria ter sido abordada, a dos construtor e operador de atribuio por cpia das classes que recorrem a variveis dinmicas: Editor e Figura. O caso da gura particularmente interessante. O construtor por cpia limita-se a criar uma nova gura contendo ponteiros para as mesmas formas da gura original. Isso ou no desejvel? Duas guras so iguais se possuirem as mesmas formas ou se possuirem formas iguais? O que nos interessa semntica de valor ou de cpia?

586

CAPTULO 14. EXCEPES E TRATAMENTO DE ERROS

Em qualquer dos casos pem-se problemas muito interessantes, que sero abordados mais tarde. ???? !!!!!!!!!Semntica de referncia: guras no podem destruir as suas formas! Que o faz, ento? Soluo: ponteiros espertos com contadores de referncias? Ou contadores de referncias dentro da classe? !!!!!!!!!!!!Semntica de valor: como construir cpias de objectos heterogneos? Quando se tem um ponteiro polimrco e se constri uma nova varivel dinmica igual ao objecto apontado por esse ponteiro diz-se que se fez uma clonagem. A soluo passa por usar uma funo membro virtual chamada clona(), que devolve um clone, e que implementada apropriadamente por todas as classes da hierarquia: class Base { // Abstracta public: Base(Base const&); // construtor por cpia explcito ou implcito. Base* clona() const = 0; // a clonagem puramente virtual nas classes abstractas e virtual nas concretas. }; classe Derivada : public Base { // Concreta public: Derivada(Derivada const&); // idem. virtual Derivada* clona() const { return new Derivada(*this); } }; A estas funes de clonagem tambm se chama construtores virtuais, por razes bvias. Curiosamente no padro Compsito (Composto?), o construtor por cpia ter de invocar os clona das formas! !!!!!!!!!Ateno ao assunto da persistncia! Convinha trat-lo algures! !!!!!!!!!!!!!!!!!!!Especicaes de excepes: incluir?

Apndice A

Notao e smbolos
A.1 Notao e smbolos
1. Todos os pedaos de cdigo (i.e., pedaos de texto numa linguagem de programao) e construes especcas do C++ aparecem em tipo (de letra) de largura constante (como Courier). Por exemplo: main() e i++;. Em todo o restante texto usa-se um tipo proporcional. Nos comentrios em C++ usa-se tambm um tipo proporcional. 1 2. As variveis matemticas aparecem sempre em itlico. Por exemplo: n = qm + r. 3. N o conjunto dos nmeros naturais. 4. Z o conjunto dos nmeros inteiros. 5. Q o conjunto dos nmeros racionais. 6. R o conjunto dos nmeros reais. 7. Os conjuntos podem ser indicados em extenso colocando os seus elementos entre {}. 8. {} e so representaes alternativas para o conjunto vazio. 9. o operador cardinal, que faz corresponder a cada conjunto o respectivo nmero de elementos.

10. Os produtos matemticos nem sempre so escritos explicitamente: pq signica o mesmo que p q, ou seja, o produto de p por q. 11. O valor absoluto de um nmero x representa-se por |x|. 12. Os smbolos usados para as igualdades e desigualdades quando inseridos em expresses matemticas (e no expresses C++, onde a notao do C++ usada) so os seguintes:
Um tipo diz-se proporcional se a largura dos caracteres varia: um i e muito mais estreito que um m. Pelo contrario, num tipo de largura xa todos os caracteres tem a mesma largura.
1

587

588 = signica igual a ou equivalente a.

APNDICE A. NOTAO E SMBOLOS

signica idntico a ou o mesmo que 2 . = signica diferente de. > signica maior que. < signica menor que. signica maior ou igual a.

signica menor ou igual a. 13. A conjuno e a disjuno representam-se pelos smbolos e ( signica ou exclusivo). 14. A pertena a um conjunto indica-se pelo smbolo . 15. A implicao e a equivalncia representam-se pelos smbolos e . 16. Os valores lgicos representam-se por: V signica verdadeiro.

F signica falso.

17. A operao de negao representa-se pelo smbolo . 18. A operao de obteno do resto da diviso inteira representa-se pelo smbolo . 19. O smbolo usado para aproximadamente igual . 20. Predicado uma expresso matemtica envolvendo variveis que, de acordo com o valor dessas variveis, pode ter valor lgico verdadeiro ou falso. Por exemplo, x > y um predicado. Se x = 1 e y = 0 o predicado tem valor lgico V (verdadeiro). 21. Os quanticadores representam-se como se segue (em todos os casos varivel i chamase varivel [muda] de quanticao): 3 Soma: (S i : m i < n : f (i)) ou mi<n f (i) a soma (o somatrio) dos valores que a funo f () toma para todos os valores do inteiro i vericando m i < n. I.e., o mesmo que f (m) + f (m + 1) + + f (n 1) . Produto: (P i : m i < n : f (i)) ou mi<n f (i) o produto dos valores que a funo f () toma para todos os valores do inteiro i vericando m i < n. I.e., o mesmo que f (m)f (m + 1) f (n 1) .

Qualquer que seja: (Q i : m i < n : f (i)) ou mi<n f (i) ou ainda m i < n : f (i) a conjuno dos valores (lgicos) que o predicado f () toma para todos os valores do inteiro i vericando m i < n. I.e., o mesmo que f (m)f (m+1) f (n1), ou seja, o mesmo que armar que qualquer que seja i com m i < n, f (i) verdadeira, da que seja conhecido como o quanticador universal.

2 necessrio claricar a diferena entre igualdade e identidade. Pode-se dizer que dois gmeos so iguais, mas no que so idnticos, pois so indivduos diferentes. Por outro lado, pode-se dizer que Fernando Pessoa e Alberto Caeiro no so apenas iguais, mas tambm idnticos, pois so nomes que se referem mesma pessoa. 3 Notao retirada de [8].

A.1. NOTAO E SMBOLOS

589

Existe um: (E i : m i < n : f (i)) ou mi<n f (i) ou ainda m i < n : f (i) a disjuno dos valores (lgicos) que o predicado f () toma para todos os valores do inteiro i vericando m i < n. I.e., o mesmo que f (m) f (m + 1) f (n 1), ou seja, o mesmo que armar que existe pelo menos um i com m i < n tal que f (i) verdadeira, da que seja conhecido como o quanticador existencial. Contagem: (N i : m i < n : f (i)) o nmero dos valores (lgicos) verdadeiros que o predicado f () toma para todos os valores do inteiro i vericando m i < n. I.e., o nmero de valores lgicos verdadeiros na sequncia f (m), f (m + 1), f (n 1). 22. Note-se que a notao x < y < z (onde em vez de < pode surgir ) uma forma abreviada de escrever x < y y < z. 23. dim(x) a dimenso de x, que tanto pode ser uma matriz como um vector (ver Captulo 5). 24. Sejam v1 e v2 duas sequncias de dimenses dim(v 1 ) e dim(v2 ) tomando valores num conjunto A. Diz-se que v1 uma permutao de v2 e vice-versa se dim(v1 ) = dim(v2 ) (Q x : : (N j : 0 j < dim(v1 ) : v1 [j] = x) = (N j : 0 j < dim(v2 ) : v2 [j] = x)), ou seja, se as sequncias tiverem a mesma dimenso e contiverem exactamente os mesmo valores com o mesmo nmero de repeties. Dene-se um predicado perm para vericar se duas sequncias so permutaes uma da outra da seguinte forma:

perm(v1 , v2 ) dim(v1 ) = dim(v2 )(Q x : : (N j : 0 j < dim(v1 ) : v1 [j] = x) = (N j : 0 j < dim(v2 A notao para os quanticadores divide-se em trs partes: (a : b : c). A primeira parte, a, indica o tipo de quanticador e as respectivas variveis mudas bem como o respectivo conjunto base. Por exemplo, (P i, j N : : ) indica um produto para todos os i e j inteiros. A segunda parte, b, consiste num predicado que restringe os valores possveis das variveis mudas. Esse predicado pode ser omitido se for sempre V. Na realidade, portanto, as variveis mudas tomam apenas valores que tornam o predicado verdadeiro. Por exemplo, (P i N : i 2 = 0 : ) indica um produto para todos os inteiros mpares. O mesmo efeito poderia ser obtido escrevendo (P i {j N : i 2 = 0} : V : ), onde o predicado sempre verdadeiro e se restringe partida o conjunto base do quanticador. A parte c indica os termos do quanticador. O conjunto de valores que a varivel muda de um quanticador pode tomar pode ser indicado implicitamente ou explicitamente. Em qualquer dos casos a indicao feita atravs de um predicado que ser verdadeiro para todos os valores que se pretende que a varivel muda tome e falso no caso contrrio. O conjunto base da varivel muda pode ser indicado na primeira parte do quanticador quando a indicao for implcita. Quando o conjunto base no for indicado assume-se que Z (conjunto dos nmeros inteiros). Exemplos: 1. (Q i : i N : ), (Q i N : V : ), (Q i Z : i > 0 : ) ou (Q i : i > 0 : ): para todos os naturais. 2. (Q i N : i 2 = 0 : ) ou, menos formalmente, (Q i N : i par : ): para todos os naturais pares.

590

APNDICE A. NOTAO E SMBOLOS

3. (Q i : m i < n : ): para todos os inteiros entre m (inclusive) e n (exclusive). 4. (Q i {1, 4, 5} : : ): para todos os elementos do conjunto {1, 4, 5}. Os quanticadores tm, por denio, os seguintes valores quando o conjunto de valores possveis para a varivel de quanticao vazio: (S i : F : ) = 0, a soma de zero termos nula. (P i : F : ) = 1, o produto de zero termos 1. (Q i : F : ) = V, a conjuno de zero predicados verdadeira. (E i : F : ) = F, a disjuno de zero predicados falsa. (N i : F : ) = 0, a contagem de zero predicados nula. Em geral , quando o conjunto de variao da varivel muda vazio, o valor destes quanticadores o elemento neutro da operao utilizada no quanticador. Assim, para a soma 0, para o produto 1, para a conjuno V e para a disjuno F. Existem (pelo menos) as seguintes equivalncias entre quanticadores: (Q i A : p(i) : f (i)) = (E i A : p(i) : f (i)). (E i A : p(i) : f (i)) = (Q i A : p(i) : f (i)). (Q i A : p(i) : f (i)) = (E i A : p(i) : f (i)) o mesmo que (N i A : p(i) : f (i)) = 0. (Q i A : p(i) : f (i)) = (E i A : p(i) : f (i)) o mesmo que (N i A : p(i) : f (i)) = {i A : p(i)}.

A.2 Abreviaturas e acrnimos


e.g. (do latim exempli gratia) por exemplo (tambm se pode usar v.g., de verbi gratia). i.e. (do latim id est) isto . vs. (do latim versus) versus, contra. cf. (do latim confer) conferir, confrontar, comparar. viz. (do latim videlicet) nomeadamente.

Apndice B

Um pouco de lgica
As seguintes implicaes so teis durantes as demonstraes: !!!!!!!!!!!!!Colocar aqui tabelas de verdade. Explicao da implicao. Regras de deduo. So todas Tautologias!. A => A ou C, A e B => A, A e B = B e A. (A=A)=V, Distributividade. ((A ou B)=>C) =(A=>B e B=>C). ((A e B)=>C)=(A=>B ou B=>C), (A=>(B e C)) = (A=>B e A=>C),(A=>(B ou C)) = (A=>B ou A=>C), (A=>B e C=>D)=>((A e C)=>(B e D). Explicar por escrito: = l-se equivale ou o mesmo ou ou seja. => l-se o que que implica, logo, por consequncia. Ver outras regras no Gries. Quando estiver pronto, fazer referncia ao apndice na introduo das demonstraes com asseres.

591

592

APNDICE B. UM POUCO DE LGICA

Apndice C

Curiosidades e fenmenos estranhos


Apresentam-se aqui algumas curiosidades acerca da linguagem C++ e da programao em geral. Note-se que este apndice contm cdigo que vai do completamente intil at ao interessante ou mesmo, imagine-se, potencialmente til. No o leia se no se sentir perfeitamente vontade com a matria que consta nos captulos que a este se referem. A ordem das curiosidades razoavelmente arbitrria.

C.1

Inicializao

As duas formas de inicializao do C++ diferem em alguns aspectos. Compare-se o programa


#include <iostream> using namespace std; int main() { int i = 1; { int i = i; cout < < i < < endl; } }

com este outro


#include <iostream> using namespace std; int main()

593

594
{

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

int i = 1; { int i(i); cout < < i < < endl; } }

O segundo escreve 1 no ecr. O primeiro escreve lixo (s por azar, ou sorte, ser 1). Porqu? Porque no primeiro a varivel mais interior j est denida (embora contendo lixo) quando se escreve = i, pelo que a varivel inicializada com o seu prprio valor (tinha lixo... com lixo ca). No segundo caso a varivel mais interior no est ainda denida quando se escreve (i), pois estes parnteses fazem parte da denio. Assim, a varivel interior inicializada com o valor da varivel mais exterior...

C.1.1 Inicializao de membros


Uma consequncia interessante do que se disse atrs que tambm se aplica nas listas de construtores das classes. Por exemplo:
class A { public: A(int i, int j, int k) : i(k), // i membro inicializada com k parmetro! j(j), // j membro inicializada com j parmetro! k(i) // k membro inicializada com i parmetro! { } private: int i; int j; int k; };

C.2

Rotinas locais

A linguagem C++ probe a denio de rotinas locais: todas as funes e procedimentos so globais. Esta armao verdadeira, mas apenas para funes e procedimentos no membro. Uma vez que existe o conceito de classe local, podem-se usar mtodos de classe em classes locais para simular rotinas locais. O mtodo simples:
// Dene-se esta macro para tornar a sintaxe de denio mais simptica: #define local_definitions struct _

C.3. MEMBROS ACESSVEIS S PARA LEITURA

595

// Dene-se esta macro para tornar a sintaxe de invocao mais simptica: #define local _:: int main() { // As rotinas locais denem-se sempre dentro deste falso ambiente: local_definitions { // As rotinas locais so sempre mtodos de classe (static): static int soma(int a, int b) { return a + b; } }; // A invocao de rotinas locais faz-se atravs da falsa palavra-chave local: int n = local soma(10, 45); }

C.3

Membros acessveis s para leitura

A linguagem C++ proporciona trs diferentes categorias de acesso para os membros de uma classe: h membros pblicos, protegidos e privados. O valor de varivel membro pblica (a evitar porque viola o princpio do encapsulamento) pode ser usado ou alterado directamente sem quaisquer restries. Uma varivel membro privada no acessvel de todo do exterior. No era simptico que existissem variveis membro cujo valor estivesse disponvel para ser usado directamente mas no pudesse ser alterado excepto pela prpria classe? Note-se que uma constante membro pblica no resolve o problema, pois nem a classe a pode alterar! A soluo passa por usar uma referncia constante pblica para aceder a uma varivel membro privada! Por exemplo, se a varivel for do tipo int e se chamar valor:
class A { public: // O construtor faz valor referenciar valor_: A(int v) : valor(valor_), valor_(v) { } // Quando se usa este truque fundamental fornecer um construtor por cpia // apropriado. Porqu? Porque seno a referncia da cpia refere-se varivel // do original! A(A const& a) : valor(valor_), valor_(a.valor_) { }

596

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

// // // // A&

Da mesma forma tem de se fornecer o operador de atribuio por cpia. Caso contrrio no se poderiam fazer atribuies entre instncias de classes com o truque. que o C++ no pode fornecer o operador de atribuio por cpia automaticamente a classes com membros que sejam referncias! operator = (A const& a) { valor_ = a.valor_; return *this;

} // A referncia valor para a varivel valor_ constante para evitar alteraes // por entidades externas classe: int const& valor; // Um procedimento para atribuir um novo valor varivel. S para efeitos de teste: void poeValor(int v) { valor_ = v; } private: // A varivel propriamente dita privada... int valor_; };

Com esta classe possvel escrever o seguinte cdigo:


#include <iostream> using namespace std; int main() { A a(10); cout < < a.valor a.valor = 20; a.poeValor(30); cout < < a.valor A b = a; cout < < b.valor A c(40); c = a; cout < < c.valor }

< < endl; // mostra 10. // d erro! < < endl; // mostra 30. < < endl; // mostra 30.

< < endl; // mostra 30.

Quem disse que no havia a categoria s para leitura em C++?

C.4. VARIVEIS VIRTUAIS

597

C.4

Variveis virtuais

O princpio do encapsulamento obriga a esconder as variveis membro de uma classe (faz-las privadas) e fornecer funes de inspeco para o seu valor se necessrio. Isto fundamental por um conjunto de razes: 1. A implementao da classe pode precisar de mudar. As variveis membro fazem naturalmente parte da implementao de uma classe. Uma varivel membro pblica faz tambm parte da interface da classe, pelo que uma alterao na implementao pode ter impactos srios em cdigo que use a classe. 2. Se o valor contido pela varivel tiver de ser calculado de forma diferente em classes derivadas, s com funes de inspeco virtuais se pode especializar essa forma de clculo. Suponha-se, por exemplo, uma hierarquia de classes representando contas. A classe base da hierarquia representa uma abstraco de conta. Para simplicar considera-se que uma conta permite apenas saber o seu saldo (e possui um destrutor virtual, como todas as classes polimrcas):
class Conta { public: virtual ~Conta() {} virtual double saldo() const = 0; };

Denem-se agora duas concretizaes do conceito. A primeira uma conta simples. Uma conta simples tem um saldo como atributo. Possui um construtor que inicializa o saldo e sobrepe uma funo de inspeco especializada que se limita a devolver esse saldo (ou seja, fornece um mtodo para a operao abstracta saldo()). A segunda um portfolio de contas, i.e., uma conta de contas. O portfolio de contas tem um procedimento acrescentaConta() que permite acrescentar uma conta ao portfolio, um destrutor para destruir as suas contas e fornece tambm um mtodo para a operao virtual saldo():
#include <list> class ContaSimples : public Conta { public: ContaSimples(double saldo) : saldo_(saldo) { } virtual double saldo() const { return saldo_; } private: double saldo_;

598
};

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

class Portfolio : public Conta { public: ~Portfolio() { // Destri contas do portfolio: for(std::list<Conta*>::iterator i = contas.begin(); i != contas.end(); ++i) delete *i; } void acrescentaConta(Conta* nova_conta) { contas.push_back(nova_conta); } virtual double saldo() const { // a soma dos saldos das contas no portfolio: double saldo = 0.0; for(std::list<Conta*>::const_iterator i = contas.begin(); i != contas.end(); ++i) saldo += (*i)->saldo(); return saldo; } private: std::list<Conta*> contas; };

Estas classes podem ser usadas como se segue:


#include <iostream> using namespace std; int main() { // Constri duas contas simples (ponteiros para Conta!): Conta* c1 = new ContaSimples(10); Conta* c2 = new ContaSimples(30); // Mostra saldos das contas simples: cout < < c1->saldo() < < endl; // mostra 10. cout < < c2->saldo() < < endl; // mostra 30.

// Constri uma conta portfolio e acrescenta-lhe as duas contas: Portfolio* p = new Portfolio; p->acrescentaConta(c1);

C.4. VARIVEIS VIRTUAIS


p->acrescentaConta(c2); // Guarda endereo da conta portfolio no ponteiro c para Conta: Conta* c = p; // Mostra saldo da conta portfolio: cout < < c->saldo() < < endl; // mostra 40. }

599

Note-se que o polimorsmo fundamental para que a invocao da operao saldo() leve execuo do mtodo saldo() apropriado classe do objecto apontado e no classe do ponteiro! Isto seria muito difcil de reproduzir se se tivesse colocado uma varivel membro para guardar o saldo na classe base, mesmo que fosse privada. Porqu? Porque nesse caso a operao saldo() no seria abstracta, estando denida na classe base como devolvendo a valor dessa varivel membro. Por isso o valor dessa varivel teria de ser mantido coerente com o saldo da conta. Isso fcil de garantir para uma conta simples mas muito mais difcil num portfolio, pois sempre que uma classe num portfolio tiver uma mudana de saldo ter de avisar a conta portfolio desse facto para que esta tenha a oportunidade de actualizar o valor da varivel. Se essa varivel fosse pblica ter-se-ia a desvantagem adicional de futuras alteraes na implementao terem impacto na interface da classe, o que indesejvel. Logo, no se pode usar uma varivel membro pblica neste caso para guardar o saldo. Porqu este discurso todo no apndice de curiosidades e fenmenos estranhos do C++ e numa seco chamada Variveis virtuais? Porque... possvel ter variveis membro pblicas, sem nenhum dos problemas apontados, e ainda apenas com permisso para leitura (restrio fcil de eliminar e que ca como exerccio para o leitor). O truque passa por denir essa varivel membro como sendo de uma classe extra DoubleVirtual (amiga da classe Conta) e que possui: 1. Um construtor que recebe a instncia da conta. 2. Um operador de converso implcita para double que invoca o operador virtual de clculo do saldo. Adicionalmente, pode-se colocar o operador de clculo do saldo na parte privada das classes, uma vez que deixa de ser til directamente para o consumidor das classes. Finalmente, para evitar conitos de nomes, este operador passa a ter um nome diferente:
class Conta { public: class DoubleVirtual { public: DoubleVirtual(Conta& base) : base(base) {

600

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS


} operator double () const { return base.calculaSaldo(); } private: // Guarda-se uma referncia para a conta a que o saldo diz respeito: Conta& base; }; friend DoubleVirtual; Conta() : saldo(*this) { } Conta(Const const&) : saldo(*this) { } Conta& operator = (Conta const&) { return *this; } virtual ~Conta() {} DoubleVirtual saldo; private: virtual double calculaSaldo() const = 0; }; #include <list> class ContaSimples : public Conta { public: ContaSimples(double saldo) : saldo_(saldo) { } private: double saldo_; virtual double calculaSaldo() const { return saldo_; } }; class Portfolio : public Conta { public: ~Portfolio() { for(std::list<Conta*>::iterator i = contas.begin(); i != contas.end(); ++i) delete *i; } void acrescentaConta(Conta* nova_conta) {

C.4. VARIVEIS VIRTUAIS


contas.push_back(nova_conta); } private: std::list<Conta*> contas; virtual double calculaSaldo() const { double saldo = 0.0; for(std::list<Conta*>::const_iterator i = contas.begin(); i != contas.end(); ++i) saldo += (*i)->saldo; return saldo; } };

601

O programa de teste agora mais simples, pois aparenta aceder directamente a um atributo para obter o saldo das contas:
#include <iostream> using namespace std; int main() { // Constri duas contas simples (ponteiros para Conta!): Conta* c1 = new ContaSimples(10); Conta* c2 = new ContaSimples(30); // Mostra saldos das contas simples: cout < < c1->saldo < < endl; cout < < c2->saldo < < endl; // Constri uma conta portfolio e acrescenta-lhe as duas contas: Portfolio* p = new Portfolio; p->acrescentaConta(c1); p->acrescentaConta(c2); // Guarda endereo da conta portfolio no ponteiro c para Conta: Conta* c = p; // Mostra saldo da conta portfolio: cout < < c->saldo < < endl; }

Este truque pode ser renado denindo uma classe C++ genrica TipoVirtual que simplique a sua utilizao mais genrica:
template <typename B, typename T>

602

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS


class TipoVirtual { public: TipoVirtual(B& base, T (B::*calcula)() const) : base(base), calcula(calcula) { } operator T () const { return (base.*calcula)(); } private: // Guarda-se uma referncia para a classe a que varivel diz respeito: B& base; // Guarda-se um ponteiro para a funo membro que devolve o valor da varivel: T (B::*calcula)() const; }; class Conta { public: Conta() : saldo(*this, &calculaSaldo) { } Conta(Const const&) : saldo(*this, &calculaSaldo) { } Conta& operator = (Conta const&) { return *this; } virtual ~Conta() {} friend TipoVirtual<Conta, double>; TipoVirtual<Conta, double> saldo; private: virtual double calculaSaldo() const = 0; }; // O resto tudo igual.

Com esta classe C++ genrica denida, a utilizao de variveis virtuais exige apenas pequenas alteraes na hierarquia de classes. Note-se que: 1. Nada se perdeu em encapsulamento. A varivel saldo parte da interface mas no parte da implementao! 2. Nada se perdeu em polimorsmo. Continua a existir uma funo para devoluo do saldo. Esta funo pode (e deve) ser especializada em classes derivadas. 3. Mas que sucede quando se copiam instncias de classes com variveis virtuais? Classes com variveis virtuais tm de denir o construtor por cpia! Tm tambm de denir o operator de atribuio por cpia!

C.5. PERSISTNCIA SIMPLIFICADA

603

4. possvel construir outra classe C++ genrica para permitir acesso completo, incluindo escritas. Essa classe C++ genrica ca como exerccio!

C.5

Persistncia simplicada

!!Texto a completar! Vericar se ainda existe verso com registo automtico.


/** Este mdulo contm uma nica classe C++ genrica muito simples, Serializador, que serve para simplicar a serializao polimrca de hierarquias de classes. Esta tcnica foi desenvolvida por mim independentemente, embora tenha beniciado com algumas ideias de: Jim Hyslop e Herb Sutter, "Conversations: Abstract Factory, Template Style", CUJ Experts, Junho de 2001. Jim Hyslop and Herb Sutter, "Conversations: How to Persist an Object", CUJ Experts, Julho de 2001. @see Serializador. Copyright c Manuel Menezes de Sequeira, ISCTE, 2001. */ #ifndef SERIALIZADOR_H #define SERIALIZADOR_H #include <map> #include <string> #include <typeinfo> /** Uma classe C++ genrica que serializa polimorcamente objectos referenciados por ponteiros. Os nicos requisitos feitos hierarquia de classes so que dois mtodos tm de estar presentes: Construtor por desserializao: Classe(istream& entrada); Operao polimrca de serializao: virtual void serializa(istream& saida) const; Conceitos (novo): Serializvel? */ template <class Base> class Serializador { public:

604

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

/** Usado para serializar polimorcamente uma instncia da hierarquia encimada pela classe Base. Um identicador da classe colocado no canal antes dos dados da classe propriamente ditos. */ static void serializa(std::ostream& saida, Base const* a); /** Usado para construir uma nova instncia da hierarquia de classes atravs da sua desserializao a partir de um canal de entrada. Assume-se que o canal contm um identicador de classe, tal como inserido pelo mtodo serializa() acima. */ static Base* constroi(std::istream& entrada); /** Uma classe usada para simplicar o registo de classes na hierarquia. */ template <class Derivada> class Registador { public: Registador() { Serializador<Base>:: regista_(typeid(Derivada).name(), &Serializador<Base>::template constroi_<Derivada>); } }; private: typedef Base* Criador(std::istream&); static Serializador& instancia(); Serializador() {} Serializador(Serializador const&); Serializador& operator = (Serializador const&); std::map<std::string, Criador*> registo; template <class Derivada> static Base* constroi_(std::istream& entrada); public: // Devia ser privado e Registador devia ser amiga de Serializador, // mas o GCC estoirou... static void regista_(std::string const& nome_da_classe, Criador* criador);

C.5. PERSISTNCIA SIMPLIFICADA


}; template <class Base> template <class Derivada> inline Base* Serializador<Base>::constroi_(std::istream& entrada) { return new Derivada(entrada); } template <class Base> inline void Serializador<Base>:: regista_(std::string const& nome_da_classe, Criador* criador) { // Uma assero apropriada aqui, uma vez que o registo de classes deve // ocorrer antes de a funo main() comear... assert(instancia().registo.count(nome_da_classe) == 0); instancia().registo[nome_da_classe] = criador; } template <class Base> Base* Serializador<Base>::constroi(std::istream& entrada) { // Isto deveria ser uma assero lanadora de excepes (pr-condio): if(not entrada) throw string("canal de entrada errado em " "Serializador<Base>::constroi"); std::string nome_da_classe; getline(entrada, nome_da_classe); // Isto deveria ser uma excepo para erros extrando de um canal de entrada: if(not entrada) throw string("erro extrando identificador de classe"); // Isto deveria ser uma excepo para classe por registar: if(instancia().registo.count(nome_da_classe) == 0) throw std::string("Classe ") + nome_da_classe + " por registar!"; return instancia().registo[nome_da_classe](entrada); } template <class Base> inline void Serializador<Base>::serializa(std::ostream& saida, Base const* a)

605

606
{

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

// Isto deveria ser uma assero lanadora de excepes (pr-condio): if(not saida) throw string("canal de sada errado em " "Serializador<Base>::serializa"); // Desnecessrio em rigor, mas til (traz alguma simetria)... // Isto deveria ser uma excepo para classe por registar: if(instancia().registo.count(typeid(*a).name()) == 0) throw std::string("Classe ") + typeid(*a).name() + " por registar!"; saida < < typeid(*a).name() < < std::endl; // Isto deveria ser uma excepo para erros inserindo num canal de sada: if(not saida) throw string("erro inserindo identificador de classe"); a->serializa(saida); } template <class Base> Serializador<Base>& Serializador<Base>::instancia() { static Serializador instancia; return instancia; } #endif // SERIALIZADOR_H

!!Seguem-se os cheiros de teste: a.H, a.C, b.H, b.C, c.H, c.C, d.H, d.C, teste.C a.H
#ifndef A_H #define A_H #include <iostream> #include <string> class A { public: A(); A(std::istream& entrada); virtual void serializa(std::ostream& saida) const;

C.5. PERSISTNCIA SIMPLIFICADA


}; inline A::A() { std::clog < < "A criado" < < std::endl; }

607

inline A::A(std::istream& entrada) { if(not entrada) throw string("canal de entrada errado em A::A(std::istream&)"); std::string texto; std::getline(entrada, texto); if(not entrada or texto != "A") throw string("erro desserializando A"); std::clog < < "A criado de canal" < < std::endl; } inline void A::serializa(std::ostream& saida) const { if(not saida) throw string("canal de sada errado em A::serializa"); saida < < "A" < < std::endl; if(not saida) throw string("erro serializando A"); std::clog < < "A serializado" < < std::endl; } #endif // A_H

a.C
#include "a.H" #include "serializador.H" namespace { Serializador<A>::Registador<A> faz_registo; }

b.H

608
#ifndef B_H #define B_H

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

#include <iostream> #include <string> #include "a.H" class B : public A { public: B(); B(std::istream& entrada); virtual void serializa(std::ostream& saida) const; }; inline B::B() { std::clog < < "B criado" < < endl; } inline B::B(std::istream& entrada) : A(entrada) { std::string texto; std::getline(entrada, texto); if(not entrada or texto != "B") throw string("erro desserializando B"); std::clog < < "B criado de canal" < < std::endl; } inline void B::serializa(ostream& saida) const { if(not saida) throw string("canal de sada errado em B::serializa"); A::serializa(saida); saida < < "B" < < std::endl; if(not saida) throw string("erro serializando B"); std::clog < < "B serializado" < < std::endl;

C.5. PERSISTNCIA SIMPLIFICADA


} #endif // B_H

609

b.C
#include "b.H" #include "serializador.H" namespace { Serializador<A>::Registador<B> faz_registo; }

c.H
#ifndef C_H #define C_H #include <iostream> #include <string> #include "a.H" class C : public A { public: C(); C(std::istream& entrada); virtual void serializa(std::ostream& saida) const; }; inline C::C() { std::clog < < "C criado" < < endl; } inline C::C(std::istream& entrada) : A(entrada) { std::string texto; std::getline(entrada, texto); if(not entrada or texto != "C") throw string("erro desserializando C"); std::clog < < "C criado de canal" < < std::endl; }

610

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

inline void C::serializa(ostream& saida) const { if(not saida) throw string("canal de sada errado em C::serializa"); A::serializa(saida); saida < < "C" < < std::endl; if(not saida) throw string("erro serializando C"); std::clog < < "C serializado" < < std::endl; } #endif // C_H

c.C
#include "c.H" #include "serializador.H" namespace { Serializador<A>::Registador<C> faz_registo1; Serializador<C>::Registador<C> faz_registo2; }

d.H
#ifndef D_H #define D_H #include <iostream> #include <string> #include "c.H" class D : public C { public: D(); D(std::istream& entrada); virtual void serializa(std::ostream& saida) const; };

C.5. PERSISTNCIA SIMPLIFICADA


inline D::D() { std::clog < < "D criado" < < endl; } inline D::D(std::istream& entrada) : C(entrada) { std::string texto; std::getline(entrada, texto); if(not entrada or texto != "D") throw string("erro desserializando D"); std::clog < < "D criado de canal" < < std::endl; } inline void D::serializa(ostream& saida) const { if(not saida) throw string("canal de sada errado em D::serializa"); C::serializa(saida); saida < < "D" < < std::endl; if(not saida) throw string("erro serializando D"); std::clog < < "D serializado" < < std::endl; } #endif // D_H

611

d.C
#include "d.H" #include "serializador.H" namespace { Serializador<A>::Registador<D> faz_registo1; Serializador<C>::Registador<D> faz_registo2; /* Mensagens de erro no so difcieis de entender:

612

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS


class X { public: X(std::istream&) {} }; Serializador<C>::Registador<X> faz_registo3; Serializador<D>::Registador<C> faz_registo4; */ }

teste.C
#include <iostream> #include <fstream> using namespace std; #include #include #include #include "a.H" "b.H" "c.H" "d.H"

#include "serializador.H" class E : public D { public: E() {} E(istream&) {} }; int main() { A* a = A* b = A* c = A* d =

new new new new

A; B; C; D;

try { ofstream saida("teste1.txt"); Serializador<A>::serializa(saida, Serializador<A>::serializa(saida, Serializador<A>::serializa(saida, Serializador<A>::serializa(saida, saida.flush(); a); b); c); d);

C.5. PERSISTNCIA SIMPLIFICADA


ifstream entrada("teste1.txt"); A* a = Serializador<A>::constroi(entrada); A* b = Serializador<A>::constroi(entrada); A* c = Serializador<A>::constroi(entrada); A* d = Serializador<A>::constroi(entrada); } catch(string excepcao) { cerr < < "Algo de errado... " < < excepcao < < endl; } C* x = new C; C* y = new D; try { ofstream saida("teste2.txt"); Serializador<C>::serializa(saida, x); Serializador<C>::serializa(saida, y); saida.flush(); ifstream entrada("teste2.txt"); C* x = Serializador<C>::constroi(entrada); C* y = Serializador<C>::constroi(entrada); } catch(string excepcao) { cerr < < "Algo de errado... " < < excepcao < < endl; } // Falha... try { A* outro = new E; Serializador<A>::serializa(cout, outro); } catch(string excepcao) { cerr < < "Algo de errado... " < < excepcao < < endl; } }

613

614

APNDICE C. CURIOSIDADES E FENMENOS ESTRANHOS

Apndice D

Nomes e seu formato: recomendaes


!!!!!!!!!!!!! Completar isto!

Entidade funo funo devolvendo bool procedimento funes membro

Nome valor calculado predicado aco [+ complementos] valor calculado

Formato minsculas, maisculas para separao minsculas, maisculas para separao minsculas, maisculas para separao minsculas, maisculas para separao

Exemplo int mximoDivi bool NmeroPr

void troca(int int Racional::

Recomendaes sobre nomes de entidades nos programas em C++. Parte das entidades s sero abordadas em captulos posteriores. Volte a consultar esta tabela Entre [] partes opcionais.

615

616

APNDICE D. NOMES E SEU FORMATO: RECOMENDAES

Apndice E

Palavras-chave do C++
!Colocar explicaes com referncias para o resto do texto.!!!!!!!!!!!!! !!!!!!!!!!!!!! !!!!!!!!!!!! !!!! !!!!!!!!!!! !!!!!!!!!! !!!!!!!!!!!!! !!!!!!!!!!!!!

Palavra-chave and and_eq asm auto bitand bitor bool break case catch char class compl const

Utilizao Operador conjuno, o mesmo que &&. Ver Seco 2.7.3. Operador conjuno bit-a-bit e atribuio, o mesmo que &=. Ver Seco 2.7.4. Serve para introduzir cdigo assembly. S para programao de muito baixo nvel! Indica explicitamente que uma varivel automtica. A sua utilizao redundante. Ver Seco 3.2.12. Operador conjuno bit-a-bit, o mesmo que &. Ver Seco 2.7.4. Operador disjuno bit-a-bit, o mesmo que |. Ver Seco 2.7.4. Um dos tipos bsicos do C++, usado para guardar valores booleanos (i.e., V ou F). Ver Seco 2.3. Permite terminar prematuramente as instrues switch (ver Seco 4.4.2) e while, for e do while (ver Seco 4.5.4). Assinala um ponto de entrada numa instruo switch. Ver Seco 4.4.2. Tipo bsico usado para representar caracteres. Ver Seco 2.3. Operador negao bit-a-bit e ou complemento para um, o mesmo que . Ver Seco 2.7.4. Qualicador que indica que uma varivel no pode ser alterada depois de inicializada, i.e., que uma constante, ou que uma funo membro de instncia no altera a instncia implcita.. Ver Ver !!.

const_cast continue default 617

618

APNDICE E. PALAVRAS-CHAVE DO C++

delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new not operator or or_eq private protected public register reinterpret_cast return short signed sizeof static static_cast struct switch template this throw true

Operador negao, o mesmo que !. Ver Seco 2.7.3. Operador disjuno, o mesmo que ||. Ver Seco 2.7.3. Operador disjuno bit-a-bit e atribuio, o mesmo que |=. Ver Seco 2.7.4.

619

try typedef typeid typename union unsigned using virtual void volatile wchat_t while xor xor_eq

Operador disjuno exclusiva bit-a-bit, o mesmo que ^. Ver Seco 2.7.4. Operador disjuno exclusiva bit-a-bit e atribuio, o mesmo que ^=. Ver Seco 2.7.4.

620

APNDICE E. PALAVRAS-CHAVE DO C++

Apndice F

Precedncia e associatividade no C++


A tabela seguinte sumariza as regras de precedncia e associatividade dos operadores do C++. Operadores colocados na mesma seco tm a mesma precedncia. As seces so separadas por e apresentadas por ordem decrescente de precedncia. Quanto associatividade, apenas os operadores unrios (com um nico operando) e os operadores de atribuio se associam direita: todos os outros associam-se esquerda, como habitual.

Descrio resoluo de mbito resoluo de mbito global global seleco de membro seleco de membro indexao invocao de funo construo de valor incrementao suxa decrementao suxa identicao de tipo identicao de tipo durante a execuo converso vericada durante a execuo converso vericada durante a compilao converso no vericada (evitar) converso constante (evitar) tamanho de objecto tamanho de tipo

Sintaxe (itlico: partes variveis da sintaxe) nome_de_classe :: membro nome_de_espao_nominativo :: membro :: nome :: nome_qualificado objecto . membro ponteiro -> membro ponteiro [ expresso_inteira ] expresso ( lista_expresses ) tipo ( lista_expresses ) lvalor ++ lvalor -typeid ( tipo ) typeid ( expresso ) dynamic_cast < tipo > ( expresso ) static_cast < tipo > ( expresso ) reinterpret_cast < tipo > ( expresso ) const_cast < tipo > ( expresso ) sizeof expresso sizeof ( tipo ) 621

622

APNDICE F. PRECEDNCIA E ASSOCIATIVIDADE NO C++


++ lvalue -- lvalue compl expresso (ou ) not expresso - expresso + expresso & lvalue * expresso new tipo new tipo ( lista_expresses ) new ( lista_expresses ) tipo new ( lista_expresses ) tipo lista_expresses ) delete ponteiro delete[] ponteiro ( tipo ) expresso objecto .* ponteiro_para_membro ponteiro ->* ponteiro_para_membro expresso * expresso expresso / expresso expresso % expresso expresso + expresso expresso - expresso expresso < < expresso expresso > > expresso expresso < expresso expresso <= expresso expresso > expresso expresso >= expresso expresso == expresso expresso != expresso (ou not_eq) expresso bitand expresso (ou &) expresso xor expresso (ou ^) expresso bitor expresso (ou |) (

incrementao prexa decrementao prexa negao bit-a-bit ou complemento para um negao simtrico identidade endereo de contedo de construode varivel dinmica construode varivel dinmica com inicializadores construo localizada de varivel dinmica construo localizada de varivel dinmica com inicializadores destruio de varivel dinmica destruio de matriz dinmica converso de tipo de baixo nvel (crime!) seleco de membro seleco de membro multiplicao diviso resto da diviso inteira adio subtraco deslocamento para a esquerda deslocamento para a direita menor menor ou igual maior maior ou igual igual diferente conjuno bit-a-bit disjuno exclusiva bit-a-bit disjuno bit-a-bit

623 expresso and expresso (ou &&) expresso or expresso (ou ||) expresso ? expresso : expresso lvalue = expresso lvalue *= expresso lvalue /= expresso lvalue %= expresso lvalue += expresso lvalue -= expresso lvalue < <= expresso lvalue > >= expresso lvalue &= expresso (ou and_eq) lvalue |= expresso (ou or_eq) lvalue ^= expresso (ou xor_eq) throw expresso expresso , expresso

conjuno disjuno operador condicional atribuio simples multiplicao e atribuio diviso e atribuio resto e atribuio adio e atribuio subtraco e atribuio deslocamento para a esquerda e atribuio deslocamento para a direita e atribuio conjuno bit-a-bit e atribuio disjuno bit-a-bit e atribuio disjuno exclusiva bit-a-bit e atribuio lanamento de excepo sequenciamento

624

APNDICE F. PRECEDNCIA E ASSOCIATIVIDADE NO C++

Apndice G

Tabelas de codicao ISO-8859-1 (Latin-1) e ISO-8859-15 (Latin-9)


Apresenta-se exaustivamente a tabela de codicao ISO-8859-1, tambm conhecida por Latin1, incluindo os nomes dos smbolos. Esta tabela foi usada durante bastante tempo nos pases da Europa ocidental (e uns quantos outros) e uma extenso tabela ASCII. No entanto, esta tabela no contm o smbolo para o euro, alm de lhe faltarem alguns smbolos para as lnguas francesa e nlandesa. Por isso, o ISO (International Standards Organization) decidiu criar uma nova tabela que dever passar a ser usada na Europa ocidental: o ISO-8859-15 ou Latin-9. Na tabela so indicados os oito caracteres que variam da tabela Latin-1 para a tabela Latin-9.

Descrio: Caracteres de controlo: nulo comeo de cabealho comeo de texto m de texto m de transmisso inquirio conrmao de recepo campanha espao para trs tabulador horizontal nova linha tabulador vertical nova pgina retorno do carreto shift out shift in escape de ligao de dados 625

Cdigo: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Smbolo:

626APNDICE G. TABELAS DE CODIFICAO ISO-8859-1 (LATIN-1) E ISO-8859-15 (LATIN-9) controlo de dispositivo 1 controlo de dispositivo 2 controlo de dispositivo 3 controlo de dispositivo 4 conrmao de recepo negativa inaco sncrona m de transmisso de bloco cancelamento m de meio substituto escape separador de cheiro separador de grupo separador de registo separador de unidade Caracteres especiais e dgitos: espao ponto de exclamao aspa cardinal dlar percentagem e comercial apstrofe parnteses esquerdo parnteses direito asterisco sinal de adio vrgula hfen ponto barra dgito 0 dgito 1 dgito 2 dgito 3 dgito 4 dgito 5 dgito 6 dgito 7 dgito 8 dgito 9 dois pontos 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

! " # $ % & ( ) * + , . / 0 1 2 3 4 5 6 7 8 9 :

627 ponto e vrgula menor igual maior ponto de interrogao Letras maisculas: arroba A maisculo B maisculo C maisculo D maisculo E maisculo F maisculo G maisculo H maisculo I maisculo J maisculo K maisculo L maisculo M maisculo N maisculo O maisculo P maisculo Q maisculo R maisculo S maisculo T maisculo U maisculo V maisculo W maisculo X maisculo Y maisculo Z maisculo parnteses recto esquerdo barra reversa parnteses recto direito acento circunexo sublinhado Letras minsculas: acento grave a minsculo b minsculo c minsculo 96 97 98 99 ` a b c 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ 59 60 61 62 63 ; < = > ?

628APNDICE G. TABELAS DE CODIFICAO ISO-8859-1 (LATIN-1) E ISO-8859-15 (LATIN-9) d minsculo e minsculo f minsculo g minsculo h minsculo i minsculo j minsculo k minsculo l minsculo m minsculo n minsculo o minsculo p minsculo q minsculo r minsculo s minsculo t minsculo u minsculo v minsculo w minsculo x minsculo y minsculo z minsculo chaveta esquerda barra vertical chaveta direita til apagar Caracteres de controlo estendidos: 128 a 159 Caracteres especiais: espao inquebrvel ponto de esclamao invertido cntimo libra esterlina moeda geral (Latin-9: euro) iene barra vertical interrompida (Latin-9: S maisculo caron ) seco trema (Latin-9: s minsculo caron ) direitos reservados ordinal feminino aspa angular esquerda 160 161 162 163 164 165 166 167 168 169 170 171 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 d e f g h i j k l m n o p q r s t u v w x y z { | }

(\euro) () () c
a

629 negao hfen curto marca registada vogal longa grau mais ou menos 2 elevado 3 elevado acento agudo (Latin-9: Z maisculo caron ) micro pargrafo ponto intermdio cedilha (Latin-9: z minsculo caron ) 1 elevado ordinal masculino aspa angular direita fraco 1/4 (Latin-9: OE ligado maisculo ) fraco 1/2 (Latin-9: oe ligado minsculo ) fraco 3/4 (Latin-9: Y maisculo trema ) ponto de interrogao invertido Letras maisculas Latin-1: A maisculo grave A maisculo agudo A maisculo circunexo A maisculo til A maisculo trema A maisculo crculo AE ligado maisculo C maisculo cedilha E maisculo grave E maisculo agudo E maisculo circunexo E maisculo trema I maisculo grave I maisculo agudo I maisculo circunexo I maisculo trema Eth maisculo N maisculo til O maisculo grave O maisculo agudo O maisculo circunexo O maisculo til 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

2 3

() ()
1 o

1 4 1 2 3 4

() () ()

630APNDICE G. TABELAS DE CODIFICAO ISO-8859-1 (LATIN-1) E ISO-8859-15 (LATIN-9) O maisculo trema sinal de multiplicao O maisculo cortado U maisculo grave U maisculo agudo U maisculo circunexo U maisculo trema Y maisculo agudo Thorn maisculo SS ligado minsculo Letras minsculas Latin-1: a minsculo grave a minsculo agudo a minsculo circunexo a minsculo til a minsculo trema a minsculo crculo ae ligado minsculo c minsculo cedilha e minsculo grave e minsculo agudo e minsculo circunexo e minsculo trema i minsculo grave i minsculo agudo i minsculo circunexo i minsculo trema eth minsculo n minsculo til o minsculo grave o minsculo agudo o minsculo circunexo o minsculo til o minsculo trema sinal de multiplicao o minsculo cortado u minsculo grave u minsculo agudo u minsculo circunexo u minsculo trema y minsculo agudo thorn minsculo y minsculo trema 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 214 215 216 217 218 219 220 221 222 223

631

632APNDICE G. TABELAS DE CODIFICAO ISO-8859-1 (LATIN-1) E ISO-8859-15 (LATIN-9)

Apndice H

Listas e iteradores: listagens


Este apndice contm as listagens completas das vrias verses do mdulo fsico lista_int desenvolvidas ao longo do Captulo 10 e do Captulo 11.

H.1 Verso simplista


Corresponde verso desenvolvida no incio do Captulo 10, que no usa ponteiros nem variveis dinmicas, e na qual os itens so guardados numa matriz pela mesma ordem pela qual ocorrem na lista.

H.1.1 Ficheiro de interface: lista_int.H


Este cheiro contm a interface do mdulo lista_int. Contm tambm a declarao dos membros privados das classes, o que corresponde a uma parte da implementao:
#ifndef LISTA_INT_H #define LISTA_INT_H #include <iostream> /** Representa listas de itens do tipo Item. Item actualmente um sinnimo de int, mas que pode ser alterado facilmente para o tipo que se entender. Por conveno, chama-se "frente" e "trs" ao primeiro e ltimo item na lista. Os nomes "primeiro", "ltimo", "incio" e "m" so reservados para iteradores. */ class ListaDeInt { public: /** Sinnimo de int. Usa-se para simplicar a tarefa de criar listas com itens de outros tipos. */ typedef int Item;

633

634

APNDICE H. LISTAS E ITERADORES: LISTAGENS

/* Declarao de uma classe embutida que serve para percorrer e manipular listas: */ class Iterador;

// Construtores: /// Construtor da classe, cria uma lista vazia. ListaDeInt();

// Inspectores: /// Devolve o comprimento da lista, ou seja, o seu nmero de itens. int comprimento() const; /// Indica se a lista est vazia. bool estVazia() const; /// Indica se a lista est cheia. bool estCheia() const; /** Devolve referncia constante para o item na frente da lista. @pre P C estVazia(). */ Item const& frente() const; /** Devolve referncia constante para o item na traseira da lista. @pre P C estVazia(). */ Item const& trs() const;

// Modicadores: /** Devolve referncia para o item na frente da lista. No modica directamente a lista, mas permite modicaes atravs da referncia devolvida. @pre P C estVazia(). */ Item& frente(); /** Devolve referncia para o item na traseira da lista. @pre P C estVazia(). */ Item& trs(); /** Pe novo item na frente da lista. Invalida qualquer iterador associado lista.

H.1. VERSO SIMPLISTA


@pre P C estCheia(). */ void peNaFrente(Item const& novo_item); /** Pe novo item na traseira da lista. Invalida qualquer iterador associado lista. @pre P C estCheia(). */ void peAtrs(Item const& novo_item); /** Tira o item da frente da lista. Invalida qualquer iterador associado lista. @pre P C estVazia(). */ void tiraDaFrente(); /** Tira o item da traseira da lista. Invalida qualquer iterador associado lista. @pre P C estVazia(). */ void tiraDeTrs(); /** Esvazia a lista. Invalida qualquer iterador associado lista. */ void esvazia(); /** Insere novo item imediatamente antes da posio indicada pelo iterador i. Faz com que o iterador continue a referenciar o mesmo item que antes da insero. Invalida qualquer outro iterador associado lista. @pre P C estCheia iterador = fim() iterador vlido. */ void insereAntes(Iterador& iterador, Item const& novo_item); /* Ateno! Em todo o rigor o iterador deveria ser constante, pois o mantm-se referenciando o mesmo item! */ /** Remove o item referenciado pelo iterador i. O iterador ca a referenciar o item logo aps o item removido. Invalida qualquer outro iterador associado lista. @pre P C iterador = incio() iterador = fim() iterador vlido. */ void remove(Iterador& i);

635

/* Funes construtoras de iteradores. Consideram-se modicadoras porque a lista pode ser modicada atravs dos iteradores. */ /** Devolve um novo iterador inicial, i.e., um iterador referenciando o item ctcio imediatamente antes do item na frente da lista. */ Iterador incio();

636

APNDICE H. LISTAS E ITERADORES: LISTAGENS


/** Devolve um novo iterador nal, i.e., um iterador referenciando o item ctcio imediatamente aps o item na traseira da lista. */ Iterador fim(); /** Devolve um novo primeiro iterador, i.e., um iterador referenciando o item na frente da lista. Note-se que se a lista estiver vazia o primeiro iterador igual ao iterador nal. */ Iterador primeiro(); /** Devolve um novo ltimo iterador, i.e., um iterador referenciando o item na traseira da lista. Note-se que se a lista estiver vazia o ltimo iterador igual ao iterador inicial. */ Iterador ltimo(); private: // O nmero mximo de itens na lista: static int const nmero_mximo_de_itens = 100; // Matriz que guarda os itens da lista: Item itens[nmero_mximo_de_itens]; // Contador do nmero de itens na lista: int nmero_de_itens; /* Funo auxiliar que indica se a condio invariante de instncia da classe se verica: */ bool cumpreInvariante() const; // A classe de iterao tem acesso irrestrito s listas: friend class Iterador; }; /** Representa iteradores para itens de listas do tipo ListaDeInt. Os iteradores tm uma caracterstica infeliz: podem estar em estados invlidos. Por exemplo, se uma lista for esvaziada, todos os iteradores a ela associada cam invlidos. possvel resolver este problema, mas custa de um aumento considervel da complexidade deste par de classes. */ class ListaDeInt::Iterador { public: // Construtores: /** Construtor da classe. Associa o iterador com a lista passada como

H.1. VERSO SIMPLISTA


argumento e pe-no a referenciar o item na sua frente. */ explicit Iterador(ListaDeInt& lista_a_associar);

637

// Inspectores: /** Devolve uma referncia para o item referenciado pelo iterador. Note-se que a referncia devolvida no constante. que um iterador const no pode ser alterado (avanar ou recuar), mas permite alterar o item por ele referenciado na lista associada. @pre P C O item referenciado no pode ser nenhum dos itens ctcios da lista (i.e., nem o item antes da frente da lista, nem o item aps a sua traseira) e tem de ser vlido. */ Item& item() const; /** Indica se dois iteradores so iguais. Ou melhor, se a instncia implcita igual ao iterador passado como argumento. Dois iteradores so iguais se se referirem ao mesmo item da mesma lista (mesmo que sejam itens ctcios). @pre P C os iteradores tm de estar associados mesma lista e ser vlidos. */ bool operator == (Iterador const& outro_iterador) const; /** Operador de diferena entre iteradores. @pre P C os iteradores tm de estar associados mesma lista e ser vlidos. */ bool operator != (Iterador const& outro_iterador) const;

// Modicadores: /** Avana iterador para o prximo item da lista. Devolve o prprio iterador. @pre P C O iterador no pode ser o m da lista associada e tem de ser vlido. */ Iterador& operator ++ (); /** Avana iterador para o prximo item da lista. Devolve um novo iterador com o valor do prprio iterador antes de avanado. @pre P C O iterador no pode ser o m da lista associada e tem de ser vlido. */ Iterador operator ++ (int); /** Recua iterador para o item anterior da lista. Devolve o prprio iterador. @pre P C O iterador no pode ser o incio da lista associada e tem de ser vlido. */ Iterador& operator -- ();

638

APNDICE H. LISTAS E ITERADORES: LISTAGENS

/** Recua iterador para o item anterior da lista. Devolve um novo iterador com o valor do prprio iterador antes de recuado. @pre P C O iterador no pode ser o incio da lista associada e tem de ser vlido. */ Iterador operator -- (int); private: // Referncia para a lista a que o iterador est associado: ListaDeInt& lista_associada; // ndice do item da lista referenciado pelo iterador: int ndice_do_item_referenciado; // Funo auxiliar que indica se a condio invariante de instncia da classe se verica: bool cumpreInvariante() const; /* A classe ListaDeInt tem acesso irrestrito a todos os membros da classe Iterador. importante perceber que as duas classes, ListaDeInt e ListaDeInt::Iterador esto completamente interligadas. No h qualquer promiscuidade nesta relao. So partes do mesmo todo. */ friend ListaDeInt; }; #include "lista_int_impl.H" #endif // LISTA_INT_H

H.1.2 Ficheiro de implementao auxiliar: lista_int_impl.H


Este cheiro contem a denio de todas as rotinas e mtodos em-linha do mdulo lista_int:
#include <cassert> inline ListaDeInt::ListaDeInt() : nmero_de_itens(0) { assert(cumpreInvariante()); } inline int ListaDeInt::comprimento() const { assert(cumpreInvariante()); return nmero_de_itens;

H.1. VERSO SIMPLISTA


} inline bool ListaDeInt::estVazia() const { assert(cumpreInvariante()); return comprimento() == 0; } inline bool ListaDeInt::estCheia() const { assert(cumpreInvariante()); return comprimento() == nmero_mximo_de_itens; } inline ListaDeInt::Item const& ListaDeInt::frente() const { assert(cumpreInvariante()); assert(not estVazia()); return itens[0]; } inline ListaDeInt::Item const& ListaDeInt::trs() const { assert(cumpreInvariante()); assert(not estVazia()); return itens[nmero_de_itens - 1]; } inline ListaDeInt::Item& ListaDeInt::frente() { assert(cumpreInvariante()); assert(not estVazia()); return itens[0]; } inline ListaDeInt::Item& ListaDeInt::trs() { assert(cumpreInvariante()); assert(not estVazia()); return itens[nmero_de_itens - 1]; } inline void ListaDeInt::peAtrs(Item const& novo_item) { assert(cumpreInvariante()); assert(not estCheia());

639

640

APNDICE H. LISTAS E ITERADORES: LISTAGENS


itens[nmero_de_itens++] = novo_item; assert(cumpreInvariante()); } inline void ListaDeInt::tiraDeTrs() { assert(cumpreInvariante()); assert(not estVazia()); --nmero_de_itens; assert(cumpreInvariante()); } inline void ListaDeInt::esvazia() { assert(cumpreInvariante()); nmero_de_itens = 0; assert(cumpreInvariante()); } inline ListaDeInt::Iterador ListaDeInt::incio() { assert(cumpreInvariante()); // Cria-se um iterador para esta lista: Iterador iterador(*this); iterador.ndice_do_item_referenciado = -1; /* Em bom rigor no boa ideia que seja um mtodo da lista a vericar se a codio invariante de instncia do iterador verdadeira... Mais tarde se ver melhor forma de resolver o problema. */ assert(iterador.cumpreInvariante()); return iterador; } inline ListaDeInt::Iterador ListaDeInt::fim() { assert(cumpreInvariante()); Iterador iterador(*this); iterador.ndice_do_item_referenciado = nmero_de_itens; assert(iterador.cumpreInvariante());

H.1. VERSO SIMPLISTA

641

return iterador; } inline ListaDeInt::Iterador ListaDeInt::primeiro() { assert(cumpreInvariante()); /* Cria-se um iterador para esta lista, que referencia inicialmente o item na frente da lista (ver construtor de ListaDeInt::Iterador), e devolve-se imediatamente o iterador criado: */ return Iterador(*this); } inline ListaDeInt::Iterador ListaDeInt::ltimo() { assert(cumpreInvariante()); Iterador iterador(*this); iterador.ndice_do_item_referenciado = nmero_de_itens - 1; assert(iterador.cumpreInvariante()); return iterador; } inline bool ListaDeInt::cumpreInvariante() const { return 0 <= nmero_de_itens and nmero_de_itens <= nmero_mximo_de_itens; } inline ListaDeInt::Iterador::Iterador(ListaDeInt& lista_a_associar) : lista_associada(lista_a_associar), ndice_do_item_referenciado(0) { assert(cumpreInvariante()); } inline ListaDeInt::Item& ListaDeInt::Iterador::item() const { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.incio() and *this != lista_associada.fim()); return lista_associada.itens[ndice_do_item_referenciado]; } inline bool ListaDeInt::Iterador::

642

APNDICE H. LISTAS E ITERADORES: LISTAGENS


operator == (Iterador const& outro_iterador) const { assert(cumpreInvariante() and outro_iterador.cumpreInvariante()); // assert( vlido and outro_iterador vlido); // assert(iteradores associados mesma lista...); return ndice_do_item_referenciado == outro_iterador.ndice_do_item_referenciado; } inline bool ListaDeInt::Iterador:: operator != (Iterador const& outro_iterador) const { assert(cumpreInvariante() and outro_iterador.cumpreInvariante()); // assert( vlido and outro_iterador vlido); // assert(iteradores associados mesma lista...); return not (*this == outro_iterador); } inline ListaDeInt::Iterador& ListaDeInt::Iterador::operator ++ () { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.fim()); ++ndice_do_item_referenciado; assert(cumpreInvariante()); return *this; } inline ListaDeInt::Iterador ListaDeInt::Iterador::operator ++ (int) { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.fim()); ListaDeInt::Iterador resultado = *this; operator ++ (); return resultado; } inline ListaDeInt::Iterador& ListaDeInt::Iterador::operator -- () { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.incio());

H.1. VERSO SIMPLISTA


--ndice_do_item_referenciado; assert(cumpreInvariante()); return *this; }

643

inline ListaDeInt::Iterador ListaDeInt::Iterador::operator -- (int) { assert(cumpreInvariante()); // assert( vlido); assert(*this != lista_associada.incio()); ListaDeInt::Iterador resultado = *this; operator -- (); return resultado; } inline bool ListaDeInt::Iterador::cumpreInvariante() const { return -1 <= ndice_do_item_referenciado and ndice_do_item_referenciado <= lista_associada.nmero_de_itens; }

H.1.3 Ficheiro de implementao: lista_int.C


Este cheiro contm a funo main() de teste do mdulo lista_int. Contem tambm a denio de todas as rotinas e mtodos que no so em-linha do mdulo.
#include "lista_int.H" void ListaDeInt::peNaFrente(Item const& novo_item) { assert(cumpreInvariante()); assert(not estCheia()); for(int i = nmero_de_itens; i != 0; --i) itens[i] = itens[i - 1]; itens[0] = novo_item; ++nmero_de_itens; assert(cumpreInvariante()); } void ListaDeInt::insereAntes(Iterador& iterador,

644

APNDICE H. LISTAS E ITERADORES: LISTAGENS


Item const& novo_item) { assert(cumpreInvariante()); assert(not estCheia()); assert(iterador vlido); assert(iterador != incio()); // H que rearranjar todos os itens a partir do referenciado pelo iterador: for(int i = nmero_de_itens; i != iterador.ndice_do_item_referenciado; --i) itens[i] = itens[i - 1]; // Agora j h espao (no esquecer de revalidar o iterador!): itens[iterador.ndice_do_item_referenciado++] = novo_item; assert(iterador.cumpreInvariante()); // Mais um... ++nmero_de_itens; assert(cumpreInvariante()); } void ListaDeInt::tiraDaFrente() { assert(cumpreInvariante()); assert(not estVazia()); --nmero_de_itens; for(int i = 0; i != nmero_de_itens; ++i) itens[i] = itens[i + 1]; assert(cumpreInvariante()); } void ListaDeInt::remove(Iterador& iterador) { assert(cumpreInvariante()); assert(iterador vlido); assert(iterador != incio() and iterador != fim()); --nmero_de_itens; for(int i = iterador.ndice_do_item_referenciado; i != nmero_de_itens; ++i) itens[i] = itens[i + 1];

H.1. VERSO SIMPLISTA


assert(cumpreInvariante()); } #ifdef TESTE // Macro denida para encurtar a escrita dos testes: #define erro(mensagem) \ { \ cout < < __FILE__ < < ":" < < __LINE__ < < ": " \ < < (mensagem) < < endl; \ ocorreram_erros = true; \ } int main() { bool ocorreram_erros = false; cout < < "Testando mdulo fsico lista_int..." < < endl; cout < < "Testando classes ListaDeInt e ListaDeInt::Iterador..." < < endl; // Denem-se itens cannicos para usar nos testes para que seja // fcil adaptar para tipos de itens diferentes: ListaDeInt::Item zero = 0; ListaDeInt::Item um = 1; ListaDeInt::Item dois = 2; ListaDeInt::Item tres = 3; ListaDeInt::Item quatro = 4; ListaDeInt::Item cinco = 5; ListaDeInt::Item seis = 6; ListaDeInt::Item sete = 7; ListaDeInt::Item oito = 8; ListaDeInt::Item nove = 9; ListaDeInt::Item dez = 10; int const nmero_de_vrios = 11; ListaDeInt::Item vrios[nmero_de_vrios] = { zero, um, dois, tres, quatro, cinco, seis, sete, oito, nove, dez }; ListaDeInt l; if(l.comprimento() != 0) erro("l.comprimento() dvia ser 0.");

645

646
l.peAtrs(tres); l.peNaFrente(dois); l.peAtrs(quatro); l.peNaFrente(um);

APNDICE H. LISTAS E ITERADORES: LISTAGENS

if(l.comprimento() != 4) erro("l.comprimento() devia ser 4."); if(l.frente() != um) erro("l.frente() devia ser um."); if(l.trs() != quatro) erro("l.trs() devia ser um."); ListaDeInt::Iterador i = l.incio(); ++i; if(i != l.primeiro()) erro("i devia ser l.primeiro()."); if(i.item() != um) erro("i.item() devia ser um."); ++i; if(i.item() != dois) erro("i.item() devia ser dois."); ++i; if(i.item() != tres) erro("i.item() devia ser tres."); ++i; if(i.item() != quatro) erro("i.item() devia ser quatro."); ++i; if(i != l.fim()) erro("i devia ser l.fim()."); --i;

H.1. VERSO SIMPLISTA


if(i != l.ltimo()) erro("i devia ser l.ltimo()."); if(i.item() != quatro) erro("i.item() devia ser quatro."); --i; if(i.item() != tres) erro("i.item() devia ser tres."); --i; if(i.item() != dois) erro("i.item() devia ser dois."); --i; if(i.item() != um) erro("i.item() devia ser um."); if(i != l.primeiro()) erro("i devia ser l.primeiro()."); --i; if(i++ != l.incio()) erro("i devia ser l.incio()."); ++i; l.insereAntes(i, cinco); if(i.item() != dois) erro("i.item() devia ser dois."); --i; if(i.item() != cinco) erro("i.item() devia ser cinco."); i--; l.remove(i); if(i.item() != cinco)

647

648

APNDICE H. LISTAS E ITERADORES: LISTAGENS


erro("i.item() devia ser cinco."); if(--i != l.incio()) erro("i devia ser l.incio()."); ++i; i++; ++i; i++; l.remove(i); if(i != l.fim()) erro("i devia ser l.fim()."); if(l.frente() != cinco) erro("l.frente() devia ser cinco."); if(l.trs() != tres) erro("l.trs() devia ser tres."); l.tiraDaFrente(); if(l.frente() != dois) erro("l.frente() devia ser dois."); l.tiraDeTrs(); if(l.trs() != dois) erro("l.frente() devia ser dois."); l.tiraDeTrs(); if(l.comprimento() != 0) erro("l.comprimento() devia ser 0."); int nmero_de_colocados = 0; while(nmero_de_colocados != nmero_de_vrios and not l.estCheia()) l.peAtrs(vrios[nmero_de_colocados++]); if(l.comprimento() != nmero_de_colocados) erro("l.comprimento() devia ser nmero_de_colocados."); ListaDeInt::Iterador j = l.ltimo(); while(not l.estVazia()) {

H.1. VERSO SIMPLISTA


--nmero_de_colocados; if(l.trs() != vrios[nmero_de_colocados]) erro("l.trs() devia ser " "vrios[nmero_de_colocados]."); if(j.item() != vrios[nmero_de_colocados]) erro("i.item() devia ser " "vrios[nmero_de_colocados]."); --j; l.tiraDeTrs(); } if(nmero_de_colocados != 0) erro("nmero_de_colocados devia ser 0."); if(j != l.incio()) erro("i devia ser l.incio()."); ++j; l.insereAntes(j, seis); --j; l.insereAntes(j, sete); l.insereAntes(j, oito); --j; l.insereAntes(j, nove); l.insereAntes(j, dez); l.remove(j); l.remove(j); --j; --j; --j; l.remove(j); l.remove(j);

649

650

APNDICE H. LISTAS E ITERADORES: LISTAGENS

if(j.item() != dez) erro("j.item() devia ser dez."); if(l.trs() != dez) erro("l.trs() devia ser dez."); if(l.frente() != dez) erro("l.frente() devia ser dez."); if(l.comprimento() != 1) erro("l.comprimento() devia ser 1."); l.insereAntes(j, l.insereAntes(j, l.insereAntes(j, l.insereAntes(j, l.insereAntes(j, um); dois); tres); quatro); cinco);

ListaDeInt const lc = l; l.esvazia(); if(not l.estVazia()) erro("l.estVazia() devia ser true."); if(lc.frente() != um) erro("lc.frente() devia ser um."); if(lc.trs() != dez) erro("lc.trs() devia ser dez."); cout < < "Testes terminados." < < endl; return ocorreram_erros? 1 : 0; } #endif // TESTE

Apndice I

Listas e iteradores seguros


!!!!!Repensar listas e iteradores de modo a se saber sempre a validade dos iteradores.

651

652

APNDICE I. LISTAS E ITERADORES SEGUROS

Bibliograa
[1] Kent Beck. Extreme Programming Explained: Embrace Change. Adison-Wesley, Boston, 1999. 347 [2] Doug Bell, Ian Morrey, e John Pugh. Software Engineering: A Programming Approach. Prentice Hall, Nova Iorque, 2a edio, 1992. 61 [3] Samuel D. Conte e Carl de Boor. Elementary Numerical Analysis. McGraw-Hill International, Auckland, 1983. 39 [4] Aurlio Buarque de Holanda Ferreira. Novo Aurlio Sculo XXI: O Dicionrio da Lingua Portuguesa. Editora Nova Fronteira, Rio de Janeiro, 3 a edio, 1999. 261, 453, 523 [5] Edsger Dijkstra. A Discipline of Programming. Prentice Hall, 1976. 204, 206 [6] Irv Englander. The Architecture of Computer Hardware and Systems Software: An Information Technology Approach. John Wiley & Sons, Inc., Nova Iorque, 1996. 32, 37, 38 [7] Ronald L. Graham, Donald E. Knuth, e Oren Patashnik. Concrete Mathematics: A Foundation for Computer Science. Addison-Wesley, 2 a edio, 1994. 312 [8] David Gries. The Science of Programming. Texts and Monographs in Computer Science. Springer-Verlag, Nova Iorque, 1981 [1989]. 147, 189, 201, 204, 285, 588 [9] Michael K Johnson e Erik W. Troan. Linux Application Development. Addison-Wesley, Reading, Massachusetts, 1998. 469 [10] Donald E. Knuth. Fundamental algorithms, volume 1 de The Art of Computer Programming. Addison-Wesley Publishing Company, Reading, Massachusetts, 2 a edio, 1973. 3, 4, 189 [11] Bertrand Meyer. Object-Oriented Software Construction. Prentice Hall PTR, Upper Saddle River, New Jersey, 2a edio, 1997. 110, 285 [12] Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley, Reading, Massachusetts, 3a edio, 1998. 20, 176, 291, 297, 445 [13] Andrew S. Tanenbaum. Structured Computer Organization. Prentice-Hall, 3 a edio, 1989. 393

653

ndice
algoritmia, 4 algoritmo, 3 anlise, 4 ecincia, 3 assembladores, 2 assemblers, 2 compiladores, 2 computador, 1 Denitude, 3 discos, 2 Eccia, 3 Entrada, 3 existe um, ver quanticador existencial Finitude, 3 linguagem alto nvel, 2 assembly, 2 declarativa, 2 imperativa, 2 mquina, 2 natural, 2 programao, 2 mtodo computational, 4 memria, 2 mnemnica, 2 pr-condio, 10 processador, 1 programa, 3 qualquer que seja, ver quanticador universal quanticador, 588 contagem, 589 654 existencial, 589 produto, 588 soma, 588 universal, 588 registos, 1 Sada, 3 somatrio, ver quanticador soma

Vous aimerez peut-être aussi