Você está na página 1de 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 <