Universidade de São Paulo Instituto de Física de São Carlos Introdução à Orientação a Objetos em C++ Gonzalo Travieso 2007 ii U NIVERSIDADE DE S ÃO PAULO Sumário 1 2 3 4 Elementos básicos 1.1 Tipos básicos de dados . . . . . . . . . . 1.2 Variáveis . . . . . . . . . . . . . . . . . 1.3 Constantes literais . . . . . . . . . . . . . 1.3.1 Constantes booleanas . . . . . . . 1.3.2 Constantes inteiras . . . . . . . . 1.3.3 Constantes tipo caracter . . . . . 1.3.4 Constantes de ponto flutuante . . 1.3.5 Constantes de cadeia de caracteres 1.4 Operadores . . . . . . . . . . . . . . . . 1.5 Conversões-padrão de tipos . . . . . . . . 1.6 Conversão explícita . . . . . . . . . . . . 1.7 Inicialização de variáveis . . . . . . . . . 1.8 Constantes . . . . . . . . . . . . . . . . . 1.9 Arrays . . . . . . . . . . . . . . . . . . . 1.10 Entrada e saída básica . . . . . . . . . . . 1.11 Estrutura de um programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 3 3 3 3 5 5 5 9 10 10 11 11 12 12 Estruturas de controle 2.1 Condicional . . . . . . . . . 2.2 Seleção . . . . . . . . . . . 2.3 Repetição . . . . . . . . . . 2.4 Desvios de fluxo de execução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 17 18 19 Funções 3.1 Definição de funções . . . . . . 3.2 Arrays como parâmetros . . . . 3.3 Recursão . . . . . . . . . . . . 3.4 Funções in-line . . . . . . . . . 3.5 Passagem por referência . . . . 3.6 Protótipos . . . . . . . . . . . . 3.7 Argumentos assumidos . . . . . 3.8 Parâmetros constantes . . . . . . 3.9 Sobrecarga de nomes de funções 3.10 Variáveis locais e globais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 25 26 26 27 28 29 29 30 31 Ponteiros 4.1 Definição de ponteiros . . . . . . . . . . . . . 4.2 Operadores de ponteiros . . . . . . . . . . . . 4.3 Ponteiros e arrays . . . . . . . . . . . . . . . . 4.4 Aritmética de ponteiros . . . . . . . . . . . . . 4.5 Cadeias de caracteres . . . . . . . . . . . . . . 4.6 Alocação dinâmica de memória . . . . . . . . . 4.7 Ponteiros como parâmetros . . . . . . . . . . . 4.8 Ponteiros constantes e ponteiros para constantes 4.9 Ponteiros para funções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 35 35 36 36 37 38 39 40 41 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS . . . . iv 5 SUMÁRIO Estruturas de dados 5.1 Estruturas . . . . 5.2 Tipos enumerados 5.3 Uniões . . . . . . 5.4 Campos de bits . 5.5 Definição de tipos . . . . . 45 45 47 47 50 50 6 Compilação separada 6.1 O processo de compilação separada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Arquivos de cabeçalho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Variáveis externas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 51 53 7 Classes 7.1 Classes . . . . . . . . . . . . . . . . . . . . . 7.2 Controle de acesso . . . . . . . . . . . . . . . 7.3 Construtores e destruidores . . . . . . . . . . . 7.4 Inicialização de membros . . . . . . . . . . . . 7.5 Métodos in-line . . . . . . . . . . . . . . . . . 7.6 Conversões . . . . . . . . . . . . . . . . . . . 7.6.1 Conversões de outros tipos para a classe 7.6.2 Conversões da classe para outros tipos . 7.7 Objetos constantes . . . . . . . . . . . . . . . 7.8 Composição de classes . . . . . . . . . . . . . 7.9 Funções e classes amigas . . . . . . . . . . . . 7.10 O ponteiro this . . . . . . . . . . . . . . . . . 7.11 Membros estáticos . . . . . . . . . . . . . . . 7.12 Sobrecarga de operadores . . . . . . . . . . . . 7.13 Atribuição de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 55 61 62 64 66 66 66 68 69 70 71 72 72 73 76 8 Escopo 8.1 Espaços de nome . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 82 9 Herança 9.1 Herança como ferramenta de projeto . 9.2 Definição de classes derivadas . . . . 9.3 Membros protegidos . . . . . . . . . 9.4 Tipos de herança . . . . . . . . . . . 9.5 Ponteiros para classe base e derivadas 9.6 Herança e composição . . . . . . . . 9.7 Herança múltipla . . . . . . . . . . . 9.8 Classes base virtuais . . . . . . . . . 9.9 Construtores, destruidores e herança . . . . . . . . . . . . . . . . . . . . . 10 Polimorfismo 10.1 Funções virtuais . . . . . 10.2 Classes base abstratas . . 10.3 Destruidores virtuais . . 10.4 Ponteiros para membros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 85 85 90 90 93 93 93 94 95 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 97 98 99 99 11 Templates 11.1 Templates de funções . . . . . . . 11.2 Templates de classe . . . . . . . . 11.3 Templates e compilação separada 11.4 Amigos de templates . . . . . . . 11.5 Especialização de templates . . . 11.6 Templates e herança . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 103 104 106 106 108 108 . . . . . . . . . . . . . . . . U NIVERSIDADE DE S ÃO PAULO SUMÁRIO v 12 Exceções 111 12.1 Tratamento de erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 12.2 Exceções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 13 Entradas e saídas 13.1 Streams . . . . . . . . . . . . . . . . . . . . . . 13.2 Streams de saída . . . . . . . . . . . . . . . . . 13.3 Streams de entrada . . . . . . . . . . . . . . . . 13.4 Entrada e saída não formatadas . . . . . . . . . . 13.5 Manipuladores de stream . . . . . . . . . . . . . 13.6 Estados de formatação de stream . . . . . . . . . 13.7 Estados de erro de stream . . . . . . . . . . . . . 13.8 Sobrecarga dos operadores de inserção e extração 13.9 Amarrando streams de saída e entrada . . . . . . 13.10Entradas e saídas em arquivos . . . . . . . . . . 13.11Acesso aleatório em arquivos . . . . . . . . . . . 13.12Streams associadas com cadeias de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 117 118 118 119 119 120 121 122 122 123 123 124 14 Biblioteca padrão de templates 14.1 Containers e iteradores . . . . . . . . . . . . . . . 14.2 Tipos de containers . . . . . . . . . . . . . . . . . 14.3 Tipos auxiliares . . . . . . . . . . . . . . . . . . . 14.4 Operações sobre containers . . . . . . . . . . . . . 14.4.1 vector . . . . . . . . . . . . . . . . . . . . 14.4.2 list . . . . . . . . . . . . . . . . . . . . . 14.4.3 deque . . . . . . . . . . . . . . . . . . . . 14.4.4 stack . . . . . . . . . . . . . . . . . . . . 14.4.5 queue . . . . . . . . . . . . . . . . . . . . 14.4.6 priority_queue . . . . . . . . . . . . . . . 14.4.7 map . . . . . . . . . . . . . . . . . . . . . 14.4.8 multimap . . . . . . . . . . . . . . . . . . 14.4.9 set . . . . . . . . . . . . . . . . . . . . . 14.4.10 multiset . . . . . . . . . . . . . . . . . . 14.4.11 bitset . . . . . . . . . . . . . . . . . . . . 14.4.12 string . . . . . . . . . . . . . . . . . . . . 14.4.13 valarray . . . . . . . . . . . . . . . . . . 14.5 Algoritmos . . . . . . . . . . . . . . . . . . . . . 14.5.1 Predicados e operações . . . . . . . . . . . 14.5.2 Execução de operação . . . . . . . . . . . 14.5.3 Algoritmos de busca . . . . . . . . . . . . 14.5.4 Modificação e geração de novas seqüências 14.5.5 Algoritmos para seqüências ordenadas . . . 14.5.6 Operações em heaps . . . . . . . . . . . . 14.5.7 Comparações . . . . . . . . . . . . . . . . 14.5.8 Permutações . . . . . . . . . . . . . . . . 14.5.9 Algoritmos numéricos . . . . . . . . . . . 14.6 Ponteiros gerenciados automaticamente . . . . . . 14.7 Números complexos . . . . . . . . . . . . . . . . 14.8 Exceções padrão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 127 127 129 129 130 134 136 136 136 137 137 138 138 139 139 140 143 146 146 147 147 149 151 153 153 153 154 154 155 155 15 Pré-processador 16 Tópicos Adicionais 16.1 Argumentos de linha de comando 16.2 Variáveis voláteis . . . . . . . . . 16.3 Especificações de ligação . . . . . 16.4 Lidando com exaustão de memória 16.5 Número variável de argumentos . 16.6 Asserções . . . . . . . . . . . . . I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 157 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 163 163 163 164 164 165 vi SUMÁRIO U NIVERSIDADE DE S ÃO PAULO Prefácio O texto seguinte consiste em notas de aula para a disciplina FFI-312, Programação Orientada a Objetos, ministrada aos alunos de Física Computacional do Instituto de Física de São Carlos. Devido à sua característica de “notas de aula”, o texto não pretende ser completo, mas apresentar um caráter complementar às aulas. Em especial, elementos conceituais de orientação a objetos, discutidos com maior ênfase durante as aulas, são apenas ligeiramente citados aqui; por outro lado, detalhes da linguagem, que tomariam muito tempo se discutidos nas aulas, são apresentados aqui. Portanto, o texto não deve ser encarado como material de auto-estudo. Além disso, como o próprio nome escolhido para o texto indica, aqui é apresentada apenas uma introdução à orientação a objetos e à linguagem C++. Esses temas são extensos e não podem ser cobertos em profundidade através de uma disciplina de apenas um semestre. Para aqueles interessados em um aprofundamento nos assuntos, sugiro os livro The C++ Programming Language, Third Edition de Bjarne Stroustrup (Addison-Wesley), Object-Oriented Software Construction de Bertrand Meyer, Component Oriented Software de Clemens Szyperski (ACM Press) e Design Patterns de Gamma, Helm, Johnson e Vlissides (Addison-Wesley). Uma dificuldade na elaboração do texto foi determinar o nível de conhecimentos prévios assumidos. Na prática, as aulas são elaboradas de tal forma que um conhecimento prévio da linguagem C é necessário. No entanto, a experiência tem demonstrado que o conhecimento que os alunos trazem da linguagem para o curso é falho em diversos detalhes, o que prejudica a compreensão de certos elementos adicionais da linguagem C++. Por esta razão, optamos por incluir uma cobertura de certos elementos da linguagem C++ que são diretamente “herdados” da linguagem C (tipos, variáveis, operadores, regras de precedência, operações com ponteiros, etc). Desta forma, o texto pode servir como uma referência rápida quando algum problema na formação do leitor nesses campos for detectado. Como comentário final, gostaria de sublinhar que o texto ainda está em desenvolvimento, podendo portanto conter erros. Ficaria grato por quaisquer indicações de erros ou incompatibilidades com o padrão da linguagem. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS viii Prefácio U NIVERSIDADE DE S ÃO PAULO Capítulo 1 Elementos básicos Descrevemos aqui os elementos básicos da linguagem C++: tipos de dados, variáveis, operadores e expressões. Apresentamos também a estrutura de um programa simples e o modo de realização de entradas e saídas. 1.1 Tipos básicos de dados Tipos de dados são responsáveis pela determinação da forma de armazenamento de dados no computador, da interpretação que deve ser dada aos valores armazenados e da enumeração das operações válidas sobre esses dados. Existem duas espécies de tipos de dados: tipos pré-definidos (ou básicos) e tipos derivados ou tipos definidos pelo usuário. Estes últimos serão tratados nos capítulos posteriores. Aqui lidaremos apenas com os tipos básicos da linguagem. Os tipos básicos se dividem em quatro categorias: booleano, tipos de caracter, tipos inteiros e tipos de ponto flutuante. Os tipos booleano, de caracter e inteiros são conjuntamente denominados tipos integrais. A tabela 1.1 lista os tipos básicos de C++. Além desses tipos, o “tipo” void é também definido. Seu uso será apresentado ao tratarmos de funções (capítulo 3) e ponteiros (capítulo 4). Algumas considerações sobre a tabela: 1. Os tipos não possuem, tanto em C++ quanto em C, uma definição explícita do número de bytes utilizados para sua representação. A definição é deixada a cargo do compilador, o que representa um ponto fraco na portabilidade (além de enfraquecer a precisão semântica) da linguagem. Entretanto algumas regras devem ser obedecidas: • a ordem de inclusão char, short, int, long deve ser respeitada, o que significa que, por exemplo, todos os valores representáveis em short devem poder ser representados também em int e long, enquanto que nem todos os valores int precisam ser representáveis em short ou char; • char deve ser capaz de armazenar o código de um caracter de acordo com a representação de caracteres do sistema onde o programa será executado; • wchar_t deve ser capaz de armazenar o maior conjunto de caracteres suportado pelo sistema para o qual o programa está sendo compilado, por exemplo, Unicode de 16 bits. • short, int e long representam inteiros com sinal (representação de complemento de dois, normalmente); as versões unsigned respectivas utilizam a mesma quantidade de bytes para representar apenas número não negativos; • char não é definido explicitamente com ou sem sinal, sendo a implementação (do compilador) livre para escolher o que for mais adequado (em geral levando em conta fatores de eficiência do código gerado); • int deve ser representado utilizando o número de bytes que for naturalmente mais adequado para a máquina onde o programa será executado, por exemplo 4 bytes numa máquina de comprimento de palavra de 32 bits; • operações com inteiros sem sinal (unsigned) não envolvem estouro (isto é, quando uma operação resulta em um valor superior ao que pode ser representado, os bits mais significativos excedentes são simplesmente ignorados, o que corresponde a operações módulo 2n , onde n é o número de bits utilizado na representação); • short e int devem ser capazes de representar números inteiros incluindo pelo menos todos os números na faixa −32767 . . . 32767; unsigned short e unsigned int devem ser capazes de armazenar números na faixa 0 . . . 65525; long deve ser capaz de armazenar números entre −2147483647 . . . 2147483647, unsigned long números na faixa de 0 . . . 4294967295; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 2 Elementos básicos Tabela 1.1: Tipos básicos de C++ Tipo Descrição bool char unsigned char signed char wchar_t short unsigned short int unsigned int long unsigned long float double long double valores lógicos caracteres (8 bits) 8 bits sem sinal 8 bits com sinal caracteres 16 bits inteiro pequeno com sinal inteiro pequeno sem sinal inteiro com sinal inteiro sem sinal inteiro grande com sinal inteiro grande sem sinal ponto flutuante precisão simples ponto flutuante precisão dupla ponto flutuante precisão extendida • float deve poder representar números com 6 dígitos decimais de precisão, e expoente de −37 a 37, apresentando um 1 máximo de 10−5 ; double deve apresentar até 10 dígitos decimais de precisão, expoentes de −37 a 37, e máximo de 10−9 ; long double tem especificações mínimas semelhantes a double; 2. os valores apresentados aqui como mínimos ou máximos são apenas limites, sendo que normalmente as implementações fornecem melhores valores; por exemplo, é comum representações de double atingirem expoentes de −138 a 138. 3. Como pode ser depreendido do exposto acima, o desenvolvimento de programas que tomam em conta tamanhos específicos dos tipos de dados é errôneo, a menos que o programa seja desenvolvido para execução em apenas uma plataforma. Exemplos de erros comuns são programas que consideram implicitamente char como sendo signed ou unsigned, e programas que atribuem a variáveis do tipo int valores maiores do que 32767. 4. Em alguns casos, a falta de portabilidade é inevitável para um programa. Por exemplo, expoentes de −37 a 37 são insuficientes para certas aplicações numéricas, o que faz com que a linguagem seja inutilizável se não se desenvolvem programas que consideram características não padronizadas, como os expoentes de −138 a 138 utilizados em muitas implementações. 1.2 Variáveis Uma variável representa um local de armazenamento de dados. Em C++ a toda variável deve ser associado um tipo dos dados que ela armazenará, o que é feito na declaração da variável. Variáveis não declaradas não são permitidas em C++. Além de declarada,uma variável deve ser também definida.Na definição de uma variável, o espaço de memória necessário para a mesma é reservado. Normalmente declarações e definições de variáveis são efetuadas simultaneamente, como será explicado nesta seção. Casos em que declarações sem definições são necessárias serão estudados mais adiante. A cada variável está associado um valor, que deve ser do tipo especificado para a variável, e um endereço, que indica a posição de memória onde a variável está armazenada. As variáveis são identificadas em C++ através de nomes, ou identificadores. Os identificadores são formados por uma seqüencia arbitrariamente longa de letras ou dígitos, sendo que o primeiro caracter deve ser uma letra, o caracter ‘_’ é considerado uma letra, e letras maiúsculas e minúsculas são consideradas distintas. O conjunto de identificadores na tabela 1.2 é reservado para a linguagem, não sendo permitido seu uso para denominar identificadores definidos pelo usuário. Estes identificadores são denominados palavras reservadas. A definição de variáveis se dá através da especificação de um tipo, seguido de uma lista com um ou mais identificadores separados por vírgula e terminada com um ponto-e-vírgula, como nos exemplos abaixo. 1 2 int i ; char c , d ; 1 é o menor número representável que, somado a 1.0 dá um valor diferente de 1.0 na representação em questão. U NIVERSIDADE DE S ÃO PAULO 1.3 Constantes literais 3 Tabela 1.2: Palavras reservadas em C++ and bitor char continue dynamic_cast extern goto mutable operator public signed switch try unsigned wchar_t 3 4 5 and_eq bool class default else false if namespace or register sizeof template typedef using while asm break compl delete enum float inline new or_eq reinterpret_cast static this typeid virtual xor auto case const do explicit for int not private return static_cast throw typename void xor_eq bitand catch const_cast double export friend long not_eq protected short struct true union volatile unsigned i n t primeiro , segundo ; f l o a t a2 , b3c ; long double a_quadrado ; Este trecho de código define as variáveis i como do tipo int, c e d como do tipo char, primeiro e segundo como do tipo unsigned int, a2 e b3c como do tipo float e a_quadrado como do tipo long double. Note que definições de variáveis são em C++ comandos executáveis, e podem portanto aparecer em qualquer lugar onde um comando executável é permitido, e não apenas no começo de blocos, como em C. Veremos exemplos adiante. 1.3 Constantes literais Constantes literais são utilizadas para representar valores fixos literais dos diversos tipos. Veremos agora a forma de representação de constantes literais para os tipos apresentados. 1.3.1 Constantes booleanas Existem duas constantes booleanas (do tipo bool): false e true. Essas constantes são compatíveis com valores numéricos e ponteiros, conforme será discutido na seção 1.5 1.3.2 Constantes inteiras Constantes inteiras são representadas por uma seqüência de dígitos. Se essa seqüencia não começa com o dígito 0, então ela é interpretada como representando um número decimal. Se começa com 0, é considerada como representando um número octal, e os dígitos 8 e 9 não podem estar incluídos. Uma seqüência de dígitos prefixada com ‘0x’ ou ‘0X’ é interpretada como representando um número hexadecimal, e neste caso as letras a até f (ou A até F) são usadas para representar os dígitos correspondentes ao valores decimais de 10 a 15. Como exemplos, o número decimal 25 pode ser representado em C++ pelos literais 25, 031 e 0x19; o número 126 pelos literais 126, 0176 e 0x7E. O tipo associado ao literal é o primeiro na lista int, long, unsigned long que puder acomodar o seu valor. Se o literal for octal ou hexadecimal, então o tipo unsigned int é tentado antes de long. Esta associação padrão pode ser alterada por meio do uso dos sufixos u (ou U) ou l (ou L). u indica que o literal representa um unsigned, enquanto l indica que ele representa um long; u e l podem ser utilizados conjuntamente para indicar um unsigned long. Como exemplo, a tabela 1.3 apresenta alguns literais e os tipos associados (supondo que int usa 2 bytes e long 4 bytes). 1.3.3 Constantes tipo caracter Constantes literais do tipo caracter são representadas pelo respectivo caracter entre apóstrofes. Por exemplo ’a’, ’X’, ’ ( ’, ’2’ e ’&’ são literais do tipo caracter. Alguns caracteres especiais ou não gráficos necessitam de representação especial, conforme tabela 1.4. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 4 Elementos básicos Tabela 1.3: Literais e seus tipos Literal 1 1L 1u 1ul 50000 0xD000 Tipo (veja texto) int long unsigned int unsigned long long unsigned int Tabela 1.4: Caracteres especiais Representação Caracter ’ \n’ ’\t’ ’ \v’ ’ \b’ ’\r’ ’\f’ ’ \a’ ’ \\ ’ ’ \? ’ ’ \’ ’ ’\~’ ’ \ooo’ ’ \xhh’ nova linha tabulação tabulação vertical retrocesso retorno de cursor avanço de formulário alerta \ ? ’ ~ caracter com representação octal ooo caracter com representação hexadecimal hh U NIVERSIDADE DE S ÃO PAULO 1.4 Operadores 5 Constantes literais de wchar_t são representadas de forma similar, mas com um L na frente, por exemplo L’ã’. Os caracteres também podem ser representado por seu código hexadecimal utilizando um \U ou \u: L’\U00001234’; se os primiros 4 dígitos hexadecimais são zero, podemos usar o \u no lugar: L’\u1234’. Isto permite a represetação de um conjunto de caracteres mais amplo, como o Unicode, em um sistema que entende apenas um conjunto mais restrito. 1.3.4 Constantes de ponto flutuante Uma constante literal de ponto flutuante consiste nas seguintes partes: parte inteira, ponto decimal, parte fracionária, um e ou E, expoente inteiro (com ou sem sinal) e sufixo de tipo. Os elementos obrigatórios são: a parte inteira ou a parte fracionária, e o ponto decimal ou a letra e (ou E). O tipo da constante é sempre double, a não ser que um sufixo de tipo, f ou F para float ou l ou L paralong double, seja especificado. Exemplos: 2.0, 2., .1, 2.3e2, 1.1E−3 são constantes double, 2.0F é uma constante float e 2.0L é uma constante long double. 1.3.5 Constantes de cadeia de caracteres Existe ainda um tipo de literal constante que não corresponde a nenhum tipo básico, mas sim a um tipo derivado (ponteiro para caracter, como será visto adiante). São os literais de cadeias de caracteres. Uma cadeia de caracteres é representada por uma seqüência de caracteres entre aspas. Qualquer caracter válido para constantes do tipo caracter pode ser utilizado para constantes do tipo cadeia de caracteres. Exemplos de cadeias de caracteres são: "Maria Silva", "Adeus!\n" e " \nOi!\a\n". Para cadeias de caracteres wchar_t, basta colocar um L na frente: L"Sørensen". 1.4 Operadores Operadores são utilizados para a construção de expressões, que determinarão as computações a serem executadas no programa. Essencial para a compreensão da construção de expressões através dos operadores é a compreensão da semântica de cada operador, das regras de precedencia entre os operadores, e da sua associatividade. Conhecidos esses fatores e os valores dos operandos envolvidos (operandos são os elementos sobre os quais os operadores atuam) pode-se determinar o valor da expressão, que é o resultado de todas as operações efetuadas na ordem adequada sobre os valores dados. Toda expressão possui um valor. A semântica é determinada pelo operador utilizado e pelo tipo de dados sobre o qual está operando. Por exemplo, a semântica é distinta entre operadores de soma e de subtração, mas também é distinta entre operadores de soma sobre número inteiros e sobre números de ponto flutuante (isto é, a soma de inteiros exige operações distintas daquela de números de ponto flutuante). A precedência é associada a grupos de operadores, que formam os denominados níveis de precedência. Através da precedência se determina a ordem em que as diversas operações em uma expressão serão executadas. Quando operadores de diferentes níveis de precedência ocorrem em uma expressão serão executados em primeiro lugar os operadores de maior precedência. Por exemplo, de acordo com as regras matemáticas usuais, é associada uma precedência maior ao produto do que à soma, de forma que uma expressão do tipo a+b∗c será interpretada como devendo-se realizar primeiro o produto de b com c, e em seguida a soma de a com o resultado desse produto. Quando uma expressão envolve operadores de mesmo nível de precedência, a ordem de execução é determinada pela associatividade. A associatividade pode ser da esquerda para a direita (caso mais comum) ou da direita para a esquerda. Por exemplo, os operadores de soma e subtração têm a mesma precedência, e associatividade da esquerda para a direita; assim, uma expressão do tipo a+b−c será interpretada como devendo-se executar inicialmente a soma de a com b, e do resultado se subtrai c. Note-se que, no sentido matemático estrito, os operadores aritméticos do computador não são associativos. Por exemplo, as expressões (a+b)−c e a+(b−c) não são equivalentes. Para entender isto considere o caso em que a, b e c são valores do tipo int, positivos e próximos do valor máximo que pode ser expresso por um int. A execução de (a+b) provocará um estouro, e resultará num valor errôneo; por outro lado (b−c) resultará em um valor pequeno, seja positivo ou negativo, que não necessariamente provocará um estouro quando somado a a; desta forma, a execução de a+(b−c) pode ser completada corretamente, e as duas expressões não são então equivalentes. Tente avaliar as duas expressões, por exemplo, considerando que um int pode representar inteiros até 32767, que a vale 30000, b vale 20000 e c vale 18000. Quando se lida com números de ponto flutuante é ainda possível que ambas as expressões possam gerar um resultado, mas o valor final das duas seja diferente (devido à precisão finita da representação). Deve-se portanto ter sempre presente a diferença entre o conceito matemático de associatividade, que envolve a liberdade na escolha da ordem de execução de certas operações, e o uso do termo associatividade em linguagens de programação, que indica justamente uma ordem específica em que as operações devem ser executadas. Os operadores podem ser classificados em operadores binários e unários. Operadores binários operam sobre dois operandos, um à esquerda e um à direita; operadores unários operam sobre apenas um operando, podendo este localizarse à direita ou à esquerda, de acordo com o operador. A tabela 1.5 apresenta os diversos operadores de C++ organizados I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 6 Elementos básicos Tabela 1.5: Precedência dos operadores Operador :: (unário), :: (binário) a++, a−−, () , [] , −>, . new, delete , ++a, −−a, + (unário), − (unário), !, ~, ( tipo ), ∗ (unário), &, sizeof .∗, −>∗ ∗ (binário), /, % + (binário), − (binário) <<, >> <, <=, >, >= ==, != & ^ | && || ? : =, +=, −=, ∗=, /=, %=, &=, ^=, |=, <<=, >>= , por nível de precedência. Operadores listados mais acima na tabela possuem precedência maior. A associatividade dos operadores unários e de atribuição é da direita para a esquerda, a dos demais operadores binários é da esquerda para a direita. Uma breve descrição dos operadores segue. :: Este é o denominado operador de escopo. Em sua forma unária, recebe um operando à direita, onde este operando deve ser um identificador. Em sua forma binária, o operando da esquerda deve ser o nome de um escopo e o da direita um identificador ou uma outra expressão do tipo a :: b (isto para lidar com escopos aninhados). Veja capítulo 8. () Operador de chamada de função, recebe um único operando, que deve ficar à sua esquerda. Veja capítulo 3. [] Operador de acesso a array. Recebe dois operandos, um que deve ficar a sua esquerda e deve corresponder a um array ou a uma classe que faz sobrecarga deste operador (ver capítulo 7), e um outro operando que deve ser colocado entre os colchetes, como em v[ i ] (v é o array, e i é o índice). Arrays serão discutidos a seguir (seção 1.9). . Operador de acesso a membro. Recebe um operando à esquerda que deve ser de um tipo de dados estruturado e um operando à direita que deve ser um nome de membro desse tipo de dados (ver capítulo 5). −> Operador de acesso a membro através de ponteiros. Semelhante ao operador anterior, mas o operando da esquerda é um ponteiro para um elemento estruturado (ver capítulos 4 e 5). ++ Operador de auto-incremento. Este operador é unário, podendo seu operando localizar-se à esquerda ou à direita, e sendo a semântica em cada caso ligeiramente distinta. O operando deste operador deve ser uma variável. Após a realização da operação, o valor da variável será incrementado de um de acordo com a regra de incremento do tipo da variável. O valor da expressão correspondente depende do posicionamento do operando. Quando o operando fica à direita do operador (++a), a operação é denominada pré-incremento e o valor da expressão será o valor final da variável após o incremento; quando o operando fica à esquerda do operador (a++) a operação é denominada pós-incremento, e o valor da expressão é o valor da variável antes do incremento. −− Operador de auto-decremento. É similar ao de auto-incremento, porém o valor da variável será decrementado de um U NIVERSIDADE DE S ÃO PAULO 1.4 Operadores 7 após a execução do operador. Existe também como pré-decremento ou pós-decremento. + (unário) Este operador nunca é necessário, estando presente apenas por questão de simetria com o operador - unário. − (unário) Operador de inversão de sinal. Seu operando deve ser colocado à direita, e o valor da expressão resultante tem o mesmo valor absoluto do operando, mas sinal oposto. ! Operador de negação. Seu operando deve ser colocado à direita. O valor da expressão resultante é false se o valor do operando for diferente true e true se o valor do operando for false . Este operador costuma ser usado no cálculo de condições lógicas (ver capítulo 2). ~ Operador de reversão de bits ou negação binária. Seu operando deve ser colocado à direita. Dada a representação em bits do operando, o valor resultante terá todos os bits invertidos (0 onde no operando existe um 1, 1 onde no operando existe um 0). ( tipo ) Operador de conversão de tipo. tipo deve ser o nome de um tipo (básico ou derivado). O operando deve ser colocado à direita. O valor da expressão é o valor resultante da conversão do tipo do operando original para o tipo especificado, de acordo com a regra de conversão correspondente (pré-definida ou definida pelo usuário, ver seção 1.5 e capítulo 7). Por exemplo, a expressão ( int )x resultará no valor correspondente ao valor de x convertido para int, de acordo com a regra de conversão de valores do tipo da variável x para int. Uma expressão ( tipo ) é também chamada cast. Outra forma de expressar a conversão é através da notação de função: int (x), ao invés de ( int )x. Veja também a seção 1.6. ∗ (unário) Este é o operador de acesso indireto, utilizado quando se lida com ponteiros (ver capítulo 4). Seu operando deve ser um ponteiro, e colocado à direita do operador. O valor da expressão resultante é o valor da variável apontada pelo ponteiro. & (unário ) Este é o operador de avaliação de endereço, utilizado quando se lida com ponteiros (capítulo 4). Seu operando deve ser uma variável, e colocado à direita do operador. O valor da expressão é um ponteiro para essa variável (correspondendo ao seu endereço na memória). sizeof Operador de tamanho. Seu operando deve ser colocado à direita, e pode ser ou um nome de tipo ou uma expressão. Se o operando for um nome de tipo, o valor da expressão resultante será o tamanho de variáveis desse tipo. Se o operando for uma expressão, esta expressão não será avaliada, apenas seu tipo será computado, e o tamanho necessário para seu armazenamento dará o valor da expressão resultante. Os tamanhos retornados têm como base o tamanho de um char, isto é, sizeof (char) é sempre 1. .∗ Operador ponteiro para membro . Similar ao operador de acesso a membro, mas avalia um ponteiro para o membro especificados, ao invés do seu valor. −>∗ Operador ponteiro para membro através de ponteiro. Similar ao operador de acesso a membro por ponteiro, mas retorna um ponteiro para o membro especificado. ∗ (binário) Operador de produto. O valor da expressão resultante é o produto do valor dos operandos à esquerda e à direita, de acordo com as regras de produto definidas para o tipo dos operandos. Se os operandos possuem tipos distintos, deve haver conversão (veja regras de conversão seções 1.5 e 7.6) ou a definição de um produto misto com a ordem especificada. / Operador de divisão. O valor da expressão é o quociente da divisão do operando à esquerda pelo operando à direita, I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 8 Elementos básicos de acordo com as regras de divisão do tipo, e considerando as conversões necessárias. % Operador de resto. Os operandos devem ser de tipo integral. O resultado é o resto da divisão do operando à esquerda pelo operando à direita. + Operador de soma. O resultado é a soma dos dois operandos. − Operador de subtração. O resultado é o valor do operando da esquerda diminuído do valor do operando da direita. << Operador de deslocamento de bits à esquerda. O valor da expressão corresponde ao valor do operando da esquerda deslocado de tantos bits à esquerda quanto o valor do operando à direita. Por exemplo x<<3 terá como resultado o valor de x deslocado 3 bits à esquerda. Utilizado em C++ também como operador de inserção, conforme será apresentado na seção 1.10 e no capítulo 13. >> Operador de deslocamento à direita. Semelhante ao operador de deslocamento à esquerda. Utilizado em C++ também como operador de extração, conforme será apresentado mais adiante. < <= > >= == != Operadores de comparação. O valor da expressão será true se a comparação for verdadeira e false se a comparação for falsa. & (binário) Operador e bit-a-bit. O valor da expressão é o que resulta da realização da operação e lógica sobre cada um dos bits correspondentes dos operandos à esquerda e à direita. | Operador ou bit-a-bit. Semelhante ao operador e bit-a-bit, mas executando a operação ou lógica. ^ Operador ou exclusivo bit-a-bit. Semelhante ao operador e bit-a-bit, mas executando a operação ou-exclusivo lógica. && Operador e lógico. O valor da expressão resultante será true se ambos os operandos tiverem valor true. O segundo operando somente será avaliado se o primeiro operando for true. || Operador ou lógico. O valor da expressão resultante será true se pelo menos um dos operandos for true. O segundo operando somente será avaliado se o primeiro operando for false . ?: Operador condicional. Implementa um tipo de execução condicional que pode ser incluído numa expressão. Uma expressão do tipo c ? v : f tem o seguinte significado: a expressão c é avaliada; se o resultado for diferente de 0, então v é avaliada e fornece o valor da expressão total, se o resultado da avaliação de c for 0, f é avaliada e fornece o valor da expressão total. = += −= ∗= /= %= &= ^= |= <<= >>= Operadores de atribuição. O operador = recebe à esquerda uma variável, que terá seu valor alterado para o valor do operando à direita. O valor da expressão resultante será o valor que foi atribuído. Uma expressão do tipo x+=y é equivalente a x=x+y; e similarmente para os demais operadores de listados. , Operador de composição seqüencial de expressões. As expressões à esquerda e à direita da vírgula são avaliadas. O resultado da expressão determinada pelo operador é o resultado da expressão à direita. Alguns comentários sobre os operadores e as regras apresentadas: U NIVERSIDADE DE S ÃO PAULO 1.5 Conversões-padrão de tipos 9 1. Quando as regras de precedência não são adequadas a uma expressão, a ordem de execução pode ser explicitamente determinada por meio do uso de parênteses. Assim, devemos escrever (a+b)∗c quando a soma deve ser efetuada antes do produto. Em alguns casos pode ser útil o uso de parênteses para facilitar a leitura, mesmo quando as regras de precedência são adequadas. 2. As regras de precedência foram definidas de tal forma a permitir que as operações mais comuns possam ser escritas sem o uso de muitos parênteses e para seguir as regras matemáticas tradicionais. Assim, por exemplo, a expressão a∗b+c/d é interpretada como (a∗b)+(c/d), seguindo a regra matemática tradicional, e a expressão condicional freqüente x>=0 && x<10 é interpretada como (x>=0) && (x<10). 3. A associatividade da direita para a esquerda dos operadores de atribuição, juntamente com o fato de eles representarem também uma expressão, permite o uso de expressões do tipo a=b=c=d=0, resultando em que as variáveis a, b, c e d têm seu valor alterado para 0, pois corresponde a (a=(b=(c=(d =0)))) . Note também que essas regras permitem que uma atribuição surja em qualquer ponto onde uma expressão pode ser utilizada, por exemplo b=(a=3)∗c implica que à variável a será atribuído o valor 3, e à variável b o valor resultante do produto desse novo valor de a com o valor de c. 4. Quando um mesmo símbolo é utilizado tanto para um operador binário como para um operador unário, a decisão de qual dos operadores está sendo representado é realizada pelo compilador com base no contexto. Assim, por exemplo, em a=b∗c o operador é interpretado como sendo o operador de produto, enquanto em d=∗e+f ele é interpretado como sendo o operador de acesso indireto. Isto ocorre porque, no primeiro caso, se o operador fosse interpretado como o de acesso indireto, teríamos a=b(∗c), o que não é sintaticamente correto (falta um operador entre b e (∗c)); no segundo caso, se o operador ∗ fosse considerado como sendo o de multiplicação, novamente haveria problemas sintáticos, pois faltaria o valor que deve ser multiplicado ao valor de e. 5. Além desses operadores existem os operadores new e delete , utilizados para lidar com alocação dinâmica de memória, que serão estudados adiante (capítulo 4). 1.5 Conversões-padrão de tipos Freqüentemente durante a avaliação de expressões surgem problemas relacionados com os tipos, como: • Mistura de tipos para um operador. Por exemplo, se x foi declarada como float e i como int, então a expressão i∗x apresenta mistura de tipos. • Uma expressão tem como resultado um tipo diferente daquele esperado no contexto onde a expressão surge. Por exemplo, sendo x do tipo float e i do tipo int, na expressão x=2∗i, a sub-expressão 2∗i resulta em um int, enquanto é esperado um float (para ser armazenado em x). Para esses casos a linguagem C++ dispõe de regras de conversão automática. Para os tipos básicos existem regras de conversão pré-definidas, ou padrão. Para alguns tipos derivados (como ponteiros) também existem regras pré-definidas. Para tipos definidos pelo usuário, as regras de conversão devem ser especificadas pelo usuário, como será visto no capítulo 7. Quando uma expressão resulta em um tipo diferente do esperado, o valor da expressão é convertido implicitamente para o tipo adequado. Por exemplo, sendo x do tipo float e i do tipo int, x=2∗i é equivalente a x=( float )(2∗ i ). Quando um operador atua sobre operandos de tipos distintos, as conversões seguem as seguintes regras: • Qualquer valor numérico ou ponteiro pode ser convertido para bool de acordo com a seguinte regra: um valor igual a 0 é convertido para false e um valor diferente de 0 é convertido para true. Ao converter de bool para int, false corresponde ao valor 0 e true ao valor 1. • Se um dos operandos é de um dos tipos float , double ou long double, o outro operando será convertido também para esse tipo, sendo que caso os dois operandos sejam de dois desses tipos, o que for do tipo menos abrangente será convertido ao tipo mais abrangente. Por exemplo, numa operação entre int e double, o valor int será convertido para double; numa operação entre float e double, o valor float será convertido para double. • Se ambos os operandos forem de tipo integral menos abrangente que int (char, short, com ou sem sinal), então será realizada a chamada promoção integral: ambos os operandos serão convertidos para int (uma exceção ocorre se o tipo não puder ser convertido para int, como pode ocorrer no caso de unsigned short quando int e short ocupam o mesmo número de bytes; neste caso, há promoção para unsigned int). I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 10 Elementos básicos • Caso nenhuma das regras acima se aplique, se um dos operandos for do tipo unsigned long, o outro será também convertido para unsigned long. • Se um operando for long o outro será convertido para long, a menos que o outro seja um unsigned int e este não possa ser contido em um long, em cujo caso ambos serão convertidos para unsigned long. • Caso nenhuma das regras anteriores se aplique e um dos operandos seja unsigned int, então o outro será convertido para unsigned int. 1.6 Conversão explícita Em algumas situações é desejável, ou mesmo necessário, que a conversão seja efetuada de forma explícita, indicando o tipo ao qual desejamos realizar a conversão. Uma forma de fazer isso é através do operador de conversão (veja pag. 7). Isto também pode ser realizado através de static_cast e reinterpret_cast . O static_cast deve ser usado quando queremos transformar um valor em um valor relacionado, por exemplo de double para int, de int para algum enum ou entre ponteiros compatíveis (veja capítulo 4 e seção 9.5). 1 2 3 4 double a = 1 . 2 5 5 ∗ 3 5 ; int b = ( int ) a ; / / Converte a para i n t int c = int ( a ); / / O mesmo . . . i n t d = s t a t i c _ c a s t < i n t > ( a ) / / De novo , o mesmo . . . 5 6 7 8 9 / / A l i n h a a b a i x o f a z com que o p o n t e i r o r e t o r n a d o p e l a f u n ç ã o / / de a l o c a ç ã o de memória do C ( d e s n e c e s s á r i a em C++) s e j a / / i n t e r p r e t a d o como p o n t e i r o p a r a d o u b l e . d o u b l e ∗v = s t a t i c _ c a s t < d o u b l e ∗ >( m a l l o c ( 1 0 0 0 ∗ s i z e o f ( d o u b l e ) ) ) ; Já o reinterpret_cast é usado raramente, apenas em situações em que queremos interpretar um certo valor em memória como se fosse de outro tipo. Por exemplo, se em nosso computador temos um dispositivo de leitura de temperatura que fornece a temperatura como um inteiro ligado na posição de memória 0 xffffdc00 , podemos ler o valor da temperatura com um código da seguinte forma: 1 2 c o n s t i n t ∗ e n d _ t e r m o m e t r o = r e i n t e r p r e t _ c a s t < i n t ∗ >(0 x f f f f d c 0 0 ) ; s t d : : c o u t << " T e m p e r a t u r a a t u a l : " << ∗ e n d _ t e r m o m e t r o << s t d : : e n d l ; Ele também se presta a alguns jogos sujos, como por exemplo ver a seqüencia de bytes usados para representar um inteiro: 1 2 3 4 5 i n t n = 123456789; char ∗p = r e i n t e r p r e t _ c a s t < char ∗>(&n ) ; f o r ( i n t i = 0 ; i < s i z e o f ( i n t ) ; i ++) s t d : : c o u t << s t a t i c _ c a s t < i n t > ( p [ i ] ) << " " ; s t d : : c o u t << s t d : : e n d l ; Outro operador de conversão é o dynamic_cast , que é similar ao static_cast , mas usado quando há herança (ver capítulos 9 e 10), verificando em tempo de execução se o objeto a ser convertido é realmente compatível e realizando a conversão adequada também durante a execução. Um outro operador de conversão é o const_cast, que serve para retirar os atributos const (seção 1.8) e volatile (seção 16.2) de uma variável ou parâmetro. Este operador deve ser usado com muita cautela, em geral apenas para lidar com rotinas antigas, que não conhecem o atributo const e não podem ser reescritas em C++. 1.7 Inicialização de variáveis No ato de definição de uma variável, pode-se associar à mesma um valor inicial. Isto é feito através do uso do operador =, apesar de não se tratar de uma atribuição. Após o operador, deve vir o valor a ser utilizado para a inicialização. É sempre possível inicializar uma variável com outra do mesmo tipo, ou uma de um tipo para o qual exista conversão. Para inicialização com valores constantes, a sintaxe utilizada dependerá do tipo, sendo para os tipos básicos conforme apresentada acima (seção 1.3). Veja os exemplos abaixo: 1 2 3 4 int N = 100; i n t m = N; f l o a t x = 1 , y = 2 . 3 e −5; i n t a , b =3 , c ; U NIVERSIDADE DE S ÃO PAULO 1.8 Constantes 11 Note que as conversões-padrão definidas acima são aplicada durante a inicialização. Por exemplo, x foi inicializada com o valor 1 (originalmente um int) convertido para float , e y com o valor 2.3e−5 (originalmente um double) convertido para float . Também deve-se ter em mente que uma definição com inicialização, como a de N acima, é diferente de uma seqüencia definição-atribuição, como abaixo. 1 2 i n t N; N = 100; Neste segundo caso, primeiro a variável é definida, não sendo especificado então nenhum valor inicial, e posteriormente o seu valor é alterado pela atribuição. Apesar desta distinção ser sem importância para tipos básicos, ela é importante para constantes (ver abaixo), referências (ver seção 3.5) e para certos tipos definidos pelo usuário, conforme veremos (capítulo 7). 1.8 Constantes A definição de constantes em C++ é similar à definição de variáveis com inicialização, mas acrescentando-se o qualificativo const antes do tipo da variável. Por exemplo: 1 2 const int N = 100; c o n s t d o u b l e GOLDEN = 1 . 6 1 8 0 3 3 9 8 9 ; define uma constante int chamada N de valor 100 e uma constante double chamada GOLDEM de valor 1.618033989. A diferença entre constantes e variáveis com inicialização é que constantes podem ser apenas inicializadas, e não utilizadas em atribuições ou qualquer outra operação que possa resultar em mudança de seu valor. 1.9 Arrays Chamamos de array a um conjunto de elementos do mesmo tipo, sendo que cada elemento pode ser acessado individualmente através do uso de um índice. Variáveis do tipo array podem ser definidas em C++ acrescentando-se uma especificação do número de elementos que o array poderá conter, como nos exemplos abaixo. 1 2 int a [10]; float b [1000]; Neste caso, a é definido como um array com 10 elementos int, e b como um array com 1000 elementos float . Os índices utilizados para acessar os elementos do array são contados a partir de 0; para um array com n elementos, os índices válidos serão de 0 a n − 1. Note que nenhum teste de validade de acesso a elementos de array é introduzido pelo compilador. É responsabilidade do programador garantir que apenas índices válidos sejam acessados. Pode-se formar arrays multidimensionais através de arrays de arrays. Veja os exemplos abaixo. 1 2 d o u b l e m1 [ 1 0 ] [ 1 5 ] , m2 [ 3 ] [ 1 0 ] [ 4 0 ] ; char t [ 5 ] [ 2 5 6 ] ; Estas definições introduzem um array bidimensional 10×15 de elementos double, um array tridimensional 3×10×40 de elementos double e um array bidimensional 5 × 256 de elementos char. Um elemento acessado como m1[3][5] será do tipo double, enquanto que um elemento m1[5] será um array de 15 elementos do tipo double. Os arrays definidos como apresentado aqui devem ter seu tamanho conhecido em tempo de compilação (arrays estáticos). O modo de definição de arrays dinâmicos, isto é, com tamanho definido em tempo de execução, será apresentado no capítulo 4, quando tratarmos de ponteiros. Assim como os tipos básicos, os arrays podem ser inicializados na definição. Isto é feito através da especificação dos valores de cada um dos elementos, separados por vírgulas, e entre chaves: 1 i n t a [ 5 ] = { 1 0 , 2 0 , 3 0 , 4 0 , 50 } ; inicializa a[0] em 10, a[1] em 20, a[2] em 30, a[3] em 40 e a[4] em 50. Se existirem menos números do que elementos do array, os elementos finais restantes do array serão inicializados com zero. Arrays multidimensionais podem ser inicializados de forma similar: 1 2 3 i n t b [ 3 ] [ 4 ] = { { 1 , 2 , 3 , 4} , { 5 , 6 , 7 , 8} , { 9 , 10 , 11 , 12}}; ou simplesmente listando todos os elemento um após o outro por linhas: 1 int b [3][4] = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12}; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 12 Elementos básicos 1.10 Entrada e saída básica Para podermos desenvolver alguns programas simples, apresentaremos aqui a sintaxe utilizada mais freqüentemente para entrada e saída de dados em C++. Neste ponto, esta sintaxe deve apenas ser aceita como um fato, pois sua compreensão envolve elementos que serão apresentados apenas bem mais adiante. Se quisermos escrever o valor de uma variável x, utilizaremos um código como: 1 s t d : : c o u t << x ; Podemos escrever uma seqüência arbitrariamente grande de valores de qualquer expressão através do uso de diversos operadores de inserção <<: 1 2 s t d : : c o u t << " x v a l e " << x << " , y v a l e " << y << " , x∗y v a l e " << x∗y << s t d : : e n d l ; Para realização de entrada de dados, utilizamos o operador de extração com a sintaxe: 1 s t d : : c i n >> x ; Ou, também como para saída, uma seqüencia de entradas: 1 2 s t d : : c o u t << " D i g i t e v a l o r e s de x e y \ n " ; s t d : : c i n >> x >> y ; std :: cin e std :: cout não são, na verdade, elementos da linguagem, mas sim objetos definidos em uma das bibliotecas comumente distribuídas com os compiladores C++. 1.11 Estrutura de um programa Para que possamos começar a desenvolver pequenos programas de teste, apresentaremos aqui os elementos essenciais da estrutura de um programa C++. A execução de um programa C++ começa pela função denominada main. A definição de funções será melhor tratada no capítulo 3. Outras características mais avançadas da função main serão apresentadas no capítulo 16. Por enquanto apresentaremos apenas a estrutura sintática básica, que deve ser seguida como uma “receita”. O programa deve conter: 1 2 3 4 i n t main ( ) { / / Codigo a s e r e x e c u t a d o } O código a ser executado é colocado, como indicado, entre os símbolos { e }, que demarcam o início e o final (respectivamente) da função main. Comentários podem ser introduzidos no texto de duas formas. Primeiro através do método utilizado em C, isto é, através dos delimitadores /∗ e ∗/ para início e final de comentário, respectivamente. A desvantagem deste método é que comentários não podem ser aninhados, o que dificulta o uso da técnica tradicional de comentar-se regiões inteiras de programa durante o processo de desenvolvimento. Assim, por exemplo, suponha que no código 1 2 3 i n t s = 0 ; / ∗ s eh o acumulador , i n i c i a l i z a d o em 0 ∗ / int i , j ; i n t m, n ; queira-se comentar as linhas que definem s, i e j. Uma tentativa seria: 1 2 3 4 5 / ∗ I n í c i o do t r e c h o c o m e n t a d o i n t s = 0 ; / ∗ s é o acumulador , i n i c i a l i z a d o em 0 ∗ / int i , j ; Fim do t r e c h o c o m e n t a d o ∗ / i n t m, n ; No entanto aqui o comentário será iniciado na primeira linha, com o /∗ e terminado na segunda linha, com o ∗/ que originalmente fechava o comentário da definição de s. O resultado é que a linha que define i e j não é comentada, e que um erro sintático será apresentado na linha seguinte, onde existe uma frase de comentário. Para minimizar este tipo de problema, o C++ introduz uma nova forma de comentário, que é iniciado pelos caracteres // e terminado pelo fim da linha. A recomendação é utilizar sempre, para comentários sobre o programa, os comentários do tipo // e deixar os comentários com /∗ e ∗/ para os casos em que trechos do código devem ser excluídos durante testes ou manutenção do programa. Seguindo estas linhas, o programa original ficaria: U NIVERSIDADE DE S ÃO PAULO 1.11 Estrutura de um programa 1 2 3 13 i n t s = 0 ; / / s eh o acumulador , i n i c i a l i z a d o em 0 int i , j ; i n t m, n ; e para comentar a região indicada poderíamos utilizar: 1 2 3 4 5 / ∗ I n í c i o do t r e c h o c o m e n t a d o i n t s = 0 ; / / s eh o acumulador , i n i c i a l i z a d o em 0 int i , j ; Fim do t r e c h o c o m e n t a d o ∗ / i n t m, n ; agora sem problema algum, pois o comentário será terminado pelo ∗/ da linha 4. Como um comentário final nesta introdução sobre a estrutura de um programa C++, acrescentamos que antes da definição da função main devem ser incluídas as declarações de todos os identificadores utilizados na mesma. Por exemplo, se forem ser realizadas entradas e saídas como as descritas acima, devem ser incluídas as declarações dos objetos std :: cin e std :: cout (além de outros identificadores úteis). Isto pode ser feito comodamente pela inclusão de uma linha como no código abaixo: 1 # include <iostream > 2 3 4 5 6 i n t main ( ) { / / Codigo a s e r e x e c u t a d o } O identificador #include é uma das chamadas “diretivas de pré-processamento”, que serão melhor estudadas no capítulo 15. O que aparece após o #include e entre os delimitadores < e > é o nome de um arquivo de cabeçalho, conforme será visto no capítulo 6. Como primeiro exemplo de um programa completo apresentamos o código abaixo, que lê dois valores de ponto flutuante e imprime sua soma. 1 # include <iostream > 2 3 4 5 6 7 8 9 10 i n t main ( ) { double x , y ; s t d : : c o u t << " D i g i t e d o i s numeros r e a i s : " << s t d : : e n d l ; s t d : : c i n >> x >> y ; s t d : : c o u t << "A soma v a l e : " << x+y << s t d : : e n d l ; return 0; } O código “return 0” indica o valor de retorno da função main (que é definida como retornando um int). Por convenção, 0 é o valor a ser retornado quando não houve erro, e qualquer valor diferente de 0 quando houve erro. Quando um return é atingido, a execução da função main termina. A notação std :: antes dos nomes cin, cout e endl indica que estes nomes fazem parte do espaço de nomes (ver cap. 8) da biblioteca padrão do C++. Num programa que se utilizará freqüentemente de elementos definidos na biblioteca padrão, o uso de std :: antes de seus nomes pode ser eliminado como na versão abaixo do programa anterior: 1 # include <iostream > 2 3 u s i n g namespace s t d ; 4 5 6 7 8 9 10 11 12 i n t main ( ) { double x , y ; c o u t << " D i g i t e d o i s numeros r e a i s : " << e n d l ; c i n >> x >> y ; c o u t << "A soma v a l e : " << x+y << e n d l ; return 0; } Esta última versão reduz a digitação, mas pode causar problemas de conflito de nomes entre os inúmeros elementos da biblioteca padrão e os do usuário. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 14 Elementos básicos Exercícios 1. O que são variáveis? O que são tipos? Qual a relação entre variáveis e tipos? 2. O que são constantes? O que são constantes literais? 3. De que forma são determinados os tipos de constantes literais? 4. Indique o tipo de cada uma das constantes literais abaixo. Considere char de 1 byte, short de 2 bytes, int de 4 bytes, long de 4 bytes. (a) 2 (b) −10 (c) 2.1 (d) 3e−5 (e) 12L (f) 234u (g) 167ul (h) 0273 (i) 0x124 (j) 0xf12a4be6 (k) ’a’ (l) ’=’ (m) ’ \ t ’ (n) ’ \0 ’ (o) ’\100’ (p) ’ \x20’ (q) 2.3 f (r) 4.0L (s) "Erro" 5. O que são operadores? Qual a função das regras de precedência e associatividade? 6. O que ocorre quando tipos de dados distintos são misturados numa mesma expressão? 7. Nas expressões abaixo, coloque os parênteses de forma a indicar explicitamente a ordem em que as operações serão executadas, como no exemplo. (a) [Exemplo] a+x/n−d∗e/f: (( a+(x/n))−((d∗e)/ f )) (b) a/2+b∗c−5+2∗d/f/g (c) x+4∗n++/g−2.1∗−−k (d) a+b > 5 && c < 3 ? std :: cout << "Caso um": std :: cout << "Caso dois"; (e) b = a >= 0 && a < 10 (f) x = y = 5, z = 3 (g) a = b+c^5−k (h) b = c < 0 || a >= 0 && a < 10 8. Qual a diferença entre inicialização e atribuição? 9. O que são arrays estáticos? U NIVERSIDADE DE S ÃO PAULO Capítulo 2 Estruturas de controle Até agora vimos como definir variáveis e como formar expressões que definem operações a serem executadas com os valores dessas variáveis. A execução de todos os códigos apresentados até o momento é puramente seqüencial, não sendo possível nenhum tipo de controle do fluxo de execução dos comandos. Para possibilitar esse controle existem as estruturas de controle, que serão apresentadas neste capítulo. 2.1 Condicional Dizemos que ocorre execução condicional se um conjunto de operações é executado apenas quando uma certa condição é satisfeita. Em C++, um valor do tipo bool correspondente à condição é calculado; se esse valor for diferente true, então o conjunto de operações espeficado é executado; se o valor calculado for false , então as operações não são executadas. A sintaxe para isso é: 1 i f ( cond ) S ; onde cond é a condição a ser verificada e S é o comando a ser executado caso a condição seja true. Os parênteses são obrigatórios. Se mais do que uma operação deve ser executada, então todas as operações devem ser reunidas em um bloco, com o uso dos delimitadores { e }, como no exemplo abaixo: 1 i f ( cond ) { S1 ; S2 ; S3 ; } Note que aqui o ponto-e-vírgula final não é necessário, e seria considerado como um comando nulo seguindo-se ao condicional. Um bloco pode sempre ser usado onde um comando é esperado. Uma situação bastante comum é que uma certa ação deva ser tomada quando uma condição é verdadeira, e uma ação diferente quando a condição é falsa. Para isto, a construção utilizada é indicada abaixo: 1 2 3 4 i f ( cond ) S1 ; else S2 ; S1 será executado se cond for true, e S2 se cond for false . Especial atenção deve ser tomada aqui quando se utilizam blocos. A sintaxe correta neste caso é: 1 2 3 4 i f ( cond ) { S1 ; S2 ; S3 ; } else { S4 ; S5 ; } Se o comando fosse escrito como abaixo 1 2 3 4 i f ( cond ) { S1 ; S2 ; S3 ; } ; / / E r r o ! i n s e r i d o comando n u l o a p ó s i f else / / r e s u l t a em e r r o de s i n t a x e no e l s e { S4 ; S5 ; } existe então um erro de sintaxe, pois o ponto-e-vírgula após a chave não é parte do condicional, e portanto considerado como um comando nulo (como já dito anteriormente), com o resultado de que o else não será associado ao if , pois o if já foi considerado encerrado antes do comando nulo. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 16 Estruturas de controle Outro uso comum de execução condicional ocorre quando diversas condições diferentes devem ser testadas, e para cada um dos casos um diferente conjunto de operações deve ser executado. Para isto não existe uma construção própria da linguagem, mas o efeito pode ser conseguido pelo uso aninhado de construções if - else . 1 2 3 4 5 6 7 8 9 10 i f ( cond1 ) S1 ; else i f ( cond2 ) S2 ; else i f ( cond3 ) S3 ; else S4 ; Na verdade esse padrão é tão comum que uma notação mais adequada é normalmente utilizada, como abaixo: 1 2 3 4 5 6 7 8 i f ( cond1 ) S1 ; e l s e i f ( cond2 ) S2 ; e l s e i f ( cond3 ) S3 ; else S4 ; Note que existe uma diferença conceitual importante entre um if sem else e um if com else (ou um conjunto múltiplo de if - else como acima). O if - else determina a escolha de um entre dois (ou múltiplos) possíveis caminhos de execução, de acordo com o valor da condição, enquanto que o if sem else serve como proteção de um conjunto de operações, que somente deve ser executado se uma certa condição for satisfeita. A condição utilizada no if sem else é, por essa razão, muitas vezes chamada de guarda. Alguns exemplos de execução condicional seguem. O código abaixo garante que o valor de x seja não-negativo após a sua execução: 1 2 i f ( x < 0) x = −x ; O próximo código, dado os valores de m e n, subtrai o menor do maior: 1 2 3 4 i f (m < n ) n −= m; else m −= n ; O código abaixo coloca em s o sinal de x, isto é, se x < 0, s vale -1, se x = 0; s vale 0, se x > 0, s vale 1: 1 2 3 4 5 6 i f ( x < 0) s = −1; e l s e i f ( x > 0) s = 1; else s = 0; Um exemplo semelhante, mas um pouco mais complexo, é o seguinte: 1 2 3 4 5 6 7 8 i f ( x < −5) y = 1; e l s e i f ( x < 0) y = 2; e l s e i f ( x < 5) y = 3; else y = 4; O resultado da execução desse código será que y receberá o valor 1 se x < −5, 2 se −5 ≤ x < 0, 3 se 0 ≤ x < 5 e 4 se x ≥ 5. Note que o segundo if testa apenas x < 0, pois a condição −5 ≤ x é automaticamente decorrente do fato da condição do primeiro if ter falhado (ou do contrário este segundo if não seria executado). Algo similar ocorre com o teste seguinte e o ramo else final. U NIVERSIDADE DE S ÃO PAULO 2.2 Seleção 2.2 17 Seleção Seleção é um tipo especial de execução condicional, onde uma operação entre várias é selecionada de acordo com o valor de uma expressão. A sintaxe para seleção em C++ é como indicada abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 switch ( expr ) { case val1 : S1 ; break ; case val2 : S2 ; break ; /∗ . . . ∗/ c a s e valN : SN ; break ; default : Sd ; } expr é a expressão a ser avaliada, e cujo valor determinará a operação a ser executada. val1, val2, ..., valN são possíveis valores de expr. Se o valor avaliado da expressão for diferente de todos os valores listados, então as operações que se seguem ao rótulo default serão executadas. As operações presentes entre o valor correspondente à avaliação da expressão e o próximo break, ou o final do bloco switch (marcado pelo correspondente caracter }) serão escolhidas para execução. Assim, por exemplo, as operações que seguem ao rótulo default não precisam ser terminadas por um break. Também é possível fazer com que o mesmo conjunto de operação sejam executadas para um conjunto de valores da expressão, simplesmente colocando os diversos valores (seguidos de dois-pontos) uns após os outros. O comentário /∗ ... ∗/ acima indica que trechos do código foram omitidos; esta notação será utilizada freqüentemente neste texto. No exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 switch ( expr ) { case val1 : case val2 : S1 ; S2 ; S3 ; break ; case val3 : S4 ; S5 ; break ; case val4 : S6 ; case val5 : S7 ; break ; default : S8 ; } S1, S2 e S3 serão executados tanto se o resultado da avaliação de expr for val1 como se for val2; se expr para val3, S4 e S5 serão executados; se avaliar para val4, devido à inexistência de um break após S6 serão executados S6 e S7; se o resultado for val5 será executado S7 e se for um valor diferente dos anteriores será executado S8. A expressão utilizada deve ser de tipo integral. Abaixo dois exemplos de uso de seleção. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /∗ . . . ∗/ s w i t c h ( diaDaSemana ) { c a s e 1 : s t d : : c o u t << " Domingo " ; break ; c a s e 2 : s t d : : c o u t << " Segunda " ; break ; c a s e 3 : s t d : : c o u t << " T e r c a " ; break ; c a s e 4 : s t d : : c o u t << " Q u a r t a " ; break ; c a s e 5 : s t d : : c o u t << " Q u i n t a " ; break ; c a s e 6 : s t d : : c o u t << " S e x t a " ; break ; c a s e 7 : s t d : : c o u t << " Sabado " ; break ; d e f a u l t : s t d : : c e r r << " Dia de semana i n v a l i d o ! " ; } /∗ . . . ∗/ s t d : : c o u t << " D e s e j a c o n t i n u a r ( s / n ) ? " ; char c ; bool cont ; s t d : : c i n >> c ; switch ( c ) { I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 18 18 19 20 21 Estruturas de controle c a s e ’ s ’ : c a s e ’ S ’ : c o n t = t r u e ; break ; c a s e ’ n ’ : c a s e ’N ’ : c o n t = f a l s e ; break ; d e f a u l t : s t d : : c e r r << " E s c o l h a s ou n . " ; } Numa seleção, o default é optativo, mas boas técnicas de programação recomendam que ele esteja sempre presente, a menos que os valores explicitamente apresentados cubram todos os valores possíveis para a expressão (o que é possível quando a expressão tem um tipo enumerado, como será visto no capítulo 5), ou que o programador possa provar (pela análise do contexto em que a seleção é executada) que a expressão somente poderá assumir um dos valores especificados. 2.3 Repetição Uma repetição permite a execução repetitiva de um conjunto de instruções. Em C++ existem três tipos de repetições: while, do-while e for. As repetições while e do-while repetem uma operação enquanto uma certa condição for verdadeira, sendo que a diferença entre elas é que a repetição while testa a condição antes de executar a operação, enquanto que a repetição do-while testa a condição após executar a operação. A sintaxe da repetição while é: 1 2 w h i l e ( cond ) S; Isto significa que cond será avaliada; se verdadeira (diferente de zero), então S será executada e cond voltará a ser avaliada; sendo novamente verdadeira S voltará a ser executada, e o processo será repetido até que cond deixe de ser verdadeira (assuma o valor 0). A sintaxe do do-while é como abaixo: 1 2 3 do S w h i l e ( cond ) ; Neste caso primeiro S será executada, e então cond será avaliada; se diferente de 0, S voltará a ser executada, e o processo será repetido até que cond seja falsa. Note que o código acima é equivalente a: 1 2 3 S; w h i l e ( cond ) S; Abaixo exemplos desses tipos de repetição. 1 # include <iostream > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 i n t main ( ) { int n , d ; s t d : : c o u t << " Q u o c i e n t e e r e s t o de d i v i s a o ( numero p o s i t i v o s ) \ n " ; s t d : : c o u t << " Numerador : " ; / / E n t r a d a de v a l o r e s s t d : : c i n >> n ; s t d : : c o u t << " Denominador : " ; s t d : : c i n >> d ; i f ( n >= 0 && d > 0 ) { / / C o n d i c a o n e c e s s a r i a de f u n c i o n a m e n t o int q = 0 , r = n ; w h i l e ( r >= d ) { r −= d ; q ++; } s t d : : c o u t << " Q u o c i e n t e : " << q << " , r e s t o : " << r << " \ n " ; } return 0; } s t d : : c o u t << " D e s e j a c o n t i n u a r ( s / n ) ? " ; char c ; bool cont , c e r t o ; U NIVERSIDADE DE S ÃO PAULO 2.4 Desvios de fluxo de execução 4 5 6 7 8 9 10 11 12 13 14 19 do { s t d : : c i n >> c ; switch ( c ) { c a s e ’ s ’ : c a s e ’ S ’ : c o n t = t r u e ; c e r t o = t r u e ; break ; c a s e ’ n ’ : c a s e ’N ’ : c o n t = f a l s e ; c e r t o = t r u e ; break ; default : s t d : : c o u t << " E s c o l h a i n v a l i d a , t e n t e n o v a m e n t e . " ; certo = false ; } } while ( ! c e r t o ) ; / / c o n t == t r u e s e f o r p a r a c o n t i n u a r . A repetição for apresenta, além da expressão para condição, a possibilidade de especificação de uma operação de inicialização, a ser executada antes da primeira execução da operação a ser repetida, e de uma operação a ser executada após cada execução da operação a ser repetida. A sintaxe é: 1 2 f o r ( I ; cond ; A) S; onde I é a operação de inicialização, cond a condição a ser testada e A a operação a ser executada após cada execução de S. O código acima é equivalente a: 1 { I; w h i l e ( cond ) { S; A; } 2 3 4 5 6 7 } A repetição for é freqüentemente utilizada para implementar iterações, como no exemplo abaixo: 1 2 3 int v [100]; f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) v [ i ] = 0; onde o for é utilizado para inicializar todos os elementos de v com o valor 0. Para se familiarizar com a operação do for, verifique o funcionamento do exemplo acima. Quando uma nova variável é definida para o índice do for, como neste caso a variável i, ela tem validade apenas durante a execução do corpo da repetição. Uma variante do código acima seria: 1 2 3 int v [100] , i ; f o r ( i = 0 ; i < 1 0 0 ; i ++) v [ i ] = 0; sendo que neste caso a variável i continua existindo após o término da repetição (neste caso, com o valor 100). Note que os elementos I, cond e A são opcionais, e portanto uma repetição while 1 2 w h i l e ( cond ) S; pode ser expressa como 1 2 f o r ( ; cond ; ) S; Em geral reserva-se entretanto o uso do for para casos em que pelo menos um de seus elementos adicionais (I ou A) seja necessário. 2.4 Desvios de fluxo de execução Para completar as estruturas de controle presentes em C++, resta apresentar os comando break, continue e goto. break Um uso de break já foi abordado quando tratamos de seleção. Ele é também bastante útil dentro de repetições, onde a execução de um break determina o término da repetição, mesmo que a condição de término ainda não tenha sido atingida. Por exemplo, o código abaixo: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 20 1 2 3 4 Estruturas de controle f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) { i f ( v [ i ] < 0 ) break ; v [ i ] ∗= 2 ; } irá multiplicar por 2 os elementos de índices 0 a 99 do array v. No entanto, se um dos elementos for negativo, apenas os elementos anteriores a ele terão sido multiplicados por 2. Isto é equivalente ao código abaixo que não usa break: 1 2 f o r ( i n t i = 0 ; i < 100 && v [ i ] >= 0 ; i ++) v [ i ] ∗= 2 ; Em muitos casos, no entanto, a re-escrita do código para evitar o break pode ser complexa. continue O comando continue pode ser utilizado quando queremos desviar a execução para a próxima iteração da repetição, sem executar os comando seguintes ao continue. Por exemplo, o código abaixo: 1 2 3 4 f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) { i f ( v [ i ] < 0) continue ; v [ i ] ∗= 2 ; } irá multiplicar por dois todos os elementos de v com índices de 0 a 99 e que tenham valores não-negativos. Os elementos de valores negativos permanecerão inalterados. Outra forma de escrever esse código é como segue: 1 2 3 f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) i f ( v [ i ] >= 0 ) v [ i ] ∗=2; Novamente, em alguns casos a re-escrita do código para evitar o uso de continue é complexa. goto O comando goto implementa em C++ um desvio incondicional. Associado a cada goto deve existir um rótulo. Um rótulo é definido como um identificador seguido de dois-pontos. Um rótulo pode ser colocado antes de qualquer comando executável. Após o goto colocamos o identificador correspondente ao rótulo, e quando a execução chegar no ponto correspondente ao goto, ela será desviada para o comando seguinte ao rótulo. A sintaxe é: 1 2 3 4 5 S1 ; rot1 : S2 ; /∗ . . . ∗/ goto r o t 1 ; O exemplo abaixo implementa um while com uso de if e goto: 1 2 3 4 5 6 7 8 9 /∗ . . . ∗/ / / int q = 0 , begW : if ( r < d) r −= d ; q ++; g o t o begW ; endW : /∗ . . . ∗/ / / Codigo i n i c i a l r = n; g o t o endW ; Codigo p o s t e r i o r O uso de goto é muito problemático do ponto de vista de engenharia de software, e raramente justificável. Deve ser evitado. Exercícios 1. Quais são as estruturas de controle de execução de C++? Indique em que situações cada uma delas deve ser utilizada. 2. Os comandos break e continue representam uma quebra das regras de programação estruturada? 3. Indique erros existentes nos códigos abaixo: U NIVERSIDADE DE S ÃO PAULO 2.4 Desvios de fluxo de execução (a)1 i n t x =1 , t o t a l ; 2 3 4 5 w h i l e ( x <= 1 0 ) { t o t a l += x ; ++x ; } (b)1 y = 1 0 ; 2 3 4 5 while ( y > 0) { s t d : : c o u t << y << s t d : : e n d l ; ++y ; } (c)1 i n t a [ 1 0 ] , i ; 2 3 f o r ( i = 1 ; i <= 1 0 ; i ++) a [ i ] = 2∗ i ; (d)1 i f ( a > 1 0 ) { 2 3 4 5 6 s t d : : c o u t << " Muito g r a n d e " << s t d : : e n d l ; }; else { s t d : : c o u t << " A c e i t a v e l " << s t d : : e n d l ; }; 4. Qual o resultado da execução dos programas abaixo? (a)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 11 12 13 i n t main ( ) { i n t y , x =1 , t o t a l = 0 ; w h i l e ( x <= 1 0 ) { y = x∗x ; s t d : : c o u t << y << s t d : : e n d l ; t o t a l += y ; x ++; } s t d : : c o u t << " T o t a l : " << t o t a l << s t d : : e n d l ; return 0; } (b)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 i n t main ( ) { int count = 1; w h i l e ( c o u n t <= 1 0 ) { s t d : : c o u t << ( c o u n t % 2 ? " ∗∗∗∗ " : " ++++++++ " ) << s t d : : e n d l ; ++ c o u n t ; } return 0; } (c)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 11 12 i n t main ( ) { i n t a [ 1 0 ] = { 3 , 4 , 5 , 7 , 8 , −1, 5 , 9 , −3, 4 } ; f o r ( i n t i = 0 ; i < 1 0 ; i ++) { a [ i ] += 1 ; i f ( a [ i ] < 0 ) break ; i f ( a [ i ]%2 ! = 0 ) c o n t i n u e ; a [ i ] /= 2; } f o r ( i n t i = 0 ; i < 1 0 ; i ++) s t d : : c o u t << a [ i ] << " " ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 21 22 Estruturas de controle s t d : : c o u t << s t d : : e n d l ; return 0; 13 14 15 } 5. Escreva um programa que pede ao usuário que forneça dois números inteiros. Em seguida, o programa deve imprimir, se os números forem diferentes, o maior número, seguido do texto “é maior que ”, seguido do menor número. Se ambos os números forem iguais, o texto “Os números fornecidos são iguais.” deve ser impresso. 6. Escreva um programa que leia 5 números do terminal e imprima a soma, o produto, a média, o maior e o menor desses números. 7. Escreva um programa que, dados 10 números inteiros lidos do terminal, imprima o valor dos dois maiores e dos dois menores números. 8. Escreva um programa para encontrar o máximo divisor comum de dois números inteiros dados. 9. Escreva um programa que calcule todos os números primos menores que um dado número utilizando o algoritmo do crivo de Eratóstenes. 10. Escreva um programa que leia valores de dia, mês e ano (como números inteiros), e imprima a data no formato: 24 de setembro de 1998. U NIVERSIDADE DE S ÃO PAULO Capítulo 3 Funções No desenvolvimento de programas complexos é freqüente ocorrerem situações como as descritas abaixo: • Um mesmo trecho de código se repete, de forma idêntica ou com variações apenas em nomes de variáveis e constantes, em diversas partes do programa. Por exemplo, um programa que precisa ordenar duas diferentes listas de nomes. • O programa como um todo pode ser considerado como a junção de um conjunto de sub-programas, sendo que cada sub-programa pode ser desenvolvido de forma relativamente independente do outro. Por exemplo, um programa para cadastramento de clientes em uma base de dados poderia ser separado em uma parte para realizar a leitura dos dados do cliente e outra parte para armazenar esses dados na base. • O programa realiza, durante sua execução, operações que são também úteis em outros programas. Por exemplo, diversos programas podem necessitar calcular as raízes de um polinômio. Todos esses problemas podem ser atacados de forma eficiente através do uso de funções, que além de ajudarem nesses problemas apresentam outras vantagens adicionais, como por exemplo permitir o uso de recursão. Desenvolver uma função consiste em dar um nome para um conjunto de instruções, sendo que a função pode ainda ser parametrizada, isto é, parte do código pode ser adaptado a diferentes variáveis e constantes, variando de uma execução da função para outra. As funções ajudam a resolver os problemas apontados anteriormente como descrito a seguir. Para um código que ocorre repetidamente, cria-se uma função. Nos locais onde este código surgiria no programa é feita apenas uma referência à função correspondente (uma chamada de função). Duas grandes vantagens deste método são: o código para a execução das operações somente precisa ser gerado uma única vez, o que implica em código executável menor e compilação mais rápida; se uma alteração é necessária neste código, ela precisa ser realizada apenas em um lugar, ao invés de em vários lugares. Para a decomposição de programas, criamos uma função para cada um dos sub-programas, e o programa principal será constituída de chamadas a essas funções. Este processo pode ser continuado, dividindo-se possivelmente cada função em um conjunto de funções mais simples. A grande vantagem deste método é que a complexidade do problema inicial vai sendo tratada por partes, até que cada uma das partes tenha complexidade suficientemente pequena para que possa ser tratada individualmente. Este método é chamado desenvolvimento top-down, ou “dividir para conquistar”. Quando o mesmo conjunto de operações pode ser útil para diversos programas, o programador pode criar uma função para executar essas operações, e então colocá-la à disposição de qualquer programa através do desenvolvimento de uma biblioteca de funções (isto será tratado em maior profundidade no capítulo 6). Vejamos agora como lidar com funções em C++. 3.1 Definição de funções A definição de uma função especifica os seguintes elementos: o nome da função, o conjunto de instruções associado a esse nome, os parâmetros que serão variados de uma chamada a outra da função e o tipo do valor de retorno. Este último é necessário pois uma função, além de realizar um conjunto de operações, pode necessitar retornar um valor para o ponto do programa onde foi chamada. A sintaxe é como apresentada abaixo: 1 2 3 4 t i p o nome ( p a r a m e t r o s ) { / / c o n j u n t o de i n s t r u ç õ e s } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 24 Funções tipo deve ser um tipo de dados. São permitidos tanto tipos básicos como tipos derivados, incluindo tipos definidos pelo usuário. nome deve ser um identificador. O conjunto de instruções pode incluir as operações existente na linguagem, definições e declarações de variáveis, mas não definições de outras funções (isto é, não são permitidas definições de funções aninhadas). parametros deve ser uma lista de variáveis com especificação de tipo, separada por vírgulas. Os parênteses e as chaves são obrigatórios. Quando a função não precisa retornar nenhum valor, o tipo de retorno deve ser especificado pelo pseudo-tipo void. Quando a função retorna um valor, ela deve ser terminada pelo comando return; este deve ser seguido por uma expressão, cujo valor será o valor retornado. O tipo da expressão deve coincidir com o tipo especificado para a função, ou uma conversão do tipo da expressão para o tipo da função deve ser conhecida. Quando a função não precisa de parâmetros, a lista de parâmetros pode permanecer vazia, ou então ser substituída pela palavra reservada void. A definição da função abaixo é válida, apesar da função não ter nenhum efeito: 1 2 3 void nada ( void ) { } Uma chamada para essa função poderá ocorrer dentro de um código através do uso do nome da função seguido de abre e fecha parênteses, como indicado abaixo: 1 2 3 /∗ . . . ∗/ / / codigo a n t e r i o r n a d a ( ) ; / / chamada da f u n c a o /∗ . . . ∗/ / / codigo p o s t e r i o r A função seguinte faz com que a variável y tenha seu valor alterado para o quadrado da variável x: 1 2 3 4 void sq1 ( ) { y = x∗x ; } Uma chamada a sq1 será feita como na chamada a nada acima, mas agora terá como efeito alterar o valor de y, como se o código da função fosse colocado no lugar de sua chamada. Isto é, uma chamada do tipo: 1 2 3 /∗ . . . ∗/ / / codigo a n t e r i o r sq1 ( ) ; /∗ . . . ∗/ / / codigo p o s t e r i o r é equivalente (do ponto de vista semântico, e não de código gerado) a 1 2 3 /∗ . . . ∗/ / / codigo a n t e r i o r y = x∗x ; /∗ . . . ∗/ / / codigo p o s t e r i o r (a não ser por questões de resolução de nomes) de variáveis, como será discutido no capítulo 8). Como escrita a função permite associar a y apenas o quadrado de x. Podemos tornar a função mais versátil através do uso de um parâmetro: 1 2 3 4 void sq2 ( double x ) { y = x∗x ; } Na definição de sq1, o x referenciado é o nome de uma variável externa à função (deve ser uma variável global, como veremos adiante), enquanto que o x de sq2 é um parâmetro, cujo valor será substituído pelo valor da variável que for passada como argumento na chamada da função. Agora necessitamos, ao realizar a chamada da função, passar um valor para o parâmetro x da função, como no código abaixo: 1 2 3 /∗ . . . ∗/ / / codigo a n t e r i o r sq2 ( z ) ; /∗ . . . ∗/ / / codigo p o s t e r i o r cujo efeito será equivalente a: 1 2 3 /∗ . . . ∗/ / / codigo a n t e r i o r y = z∗z ; /∗ . . . ∗/ / / codigo p o s t e r i o r (a menos por resolução de nomes com relação à variável y). Note que nada impede que a variável utilizada como argumento na chamada da função tenha o mesmo nome do parâmetro dessa função, como abaixo: U NIVERSIDADE DE S ÃO PAULO 3.2 Arrays como parâmetros 1 2 3 25 /∗ . . . ∗/ / / codigo a n t e r i o r sq2 ( x ) ; /∗ . . . ∗/ / / codigo p o s t e r i o r deve-se apenas ter presente que neste caso as duas variáveis x são distintas: uma é a variável x do ponto de chamada da função e outra é o parâmetro de sq2. Ainda permanece nesta função a limitação de que ela sempre altera o valor da variável y. Se quisermos torná-la mais flexível, podemos utilizar-nos da possibilidade de retorno de valor por parte da função, como abaixo: 1 2 3 4 5 6 double sq3 ( double x ) { double y ; y = x∗x ; return y ; } ou ainda, equivalentemente: 1 2 3 4 double sq4 ( double x ) { r e t u r n x∗x ; } Como essas funções retornam um valor, elas podem ser utilizadas em qualquer lugar onde uma expressão possa ser utilizada: 1 2 y = sq3 ( x ) ; z = sq3 ( 2 ) + sq4 ( y ) ; 3.2 Arrays como parâmetros Arrays podem ser utilizados como parâmetros para funções, mas a especifição do tamanho do array deve ser omitida. A sintaxe é como no exemplo da função seguinte, que computa o produto escalar de dois vetores: 1 2 3 4 5 6 7 d o u b l e P r o d u t o E s c a l a r ( d o u b l e v1 [ ] , d o u b l e v2 [ ] , i n t N) { double s = 0 ; f o r ( i n t i = 0 ; i < N; i ++) s += v1 [ i ] ∗ v2 [ i ] ; return s ; } Note que v1 e v2 são especificados como arrays simplesmente através do uso de colchetes. Como o número de elementos seria de outro modo desconhecido pela função, este é passado como parâmetro adicional (parâmetro N no exemplo). Na chamada da função, devem ser passados apenas o nome dos arrays e o número de elementos, como no exemplo abaixo: 1 2 3 4 5 /∗ . . . ∗/ double a [ 1 0 0 ] , b [ 1 0 0 ] , p ; /∗ . . . ∗/ p = ProdutoEscalar (a , b ,100); /∗ . . . ∗/ No caso de arrays multidimensionais, apenas a primeira dimensão fica aberta (isto é, é especificada sem indicação de número de elementos). Todas as outras dimensões devem ter o número de elementos especificados, como na seguinte função de impressão de matrizes: 1 2 3 4 5 6 7 8 v o i d I m p r i m e M a t r i z ( d o u b l e m[ ] [ 1 0 ] , i n t N) { f o r ( i n t i = 0 ; i < N; i ++) { f o r ( i n t j = 0 ; j < 1 0 ; j ++) s t d : : c o u t << m[ i ] [ j ] << " " ; s t d : : c o u t << s t d : : e n d l ; } } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 26 Funções Deve-se notar que esta característica limita muito a utilidade de funções desenvolvidas para arrays multidimensionais estáticos. Por exemplo, a função anterior só funcionará com matrizes com exatamente 10 colunas. Para desenvolver rotinas mais versáteis devem ser utilizados arrays dinâmicos ou outros truques baseados em ponteiros, como será explicado no capítulo 4. 3.3 Recursão Chama-se de recursão ao fato de, durante a avaliação de uma função, uma chamada para essa mesma função ser realizada. No caso em que a própria função realiza a chamada, dizemos que existe recursividade simples. No caso em que uma outra função chamada pela função original executa por sua vez uma chamada à função original (direta ou indiretamente) dizemos que existe recursividade múltipla. Recursão é uma ferramenta extremamente poderosa para lidar com diversos tipos de algoritmos. O programador deve tomar cuidado para que o término da recursão seja garantido, isto é, evitar que a função continue se chamando recursivamente indefinidamente. A função abaixo apresenta um exemplo típico de recursão: o cálculo do fatorial. 1 2 3 4 5 6 7 8 9 10 11 12 int Fatorial ( int n) { i f ( n >= 0 ) { i f ( n == 0 | | n == 1 ) return 1; else r e t u r n n∗ F a t o r i a l ( n −1); } else { s t d : : c e r r << " E r r o : f a t o r i a l de numero n e g a t i v o ! " << s t d : : e n d l ; r e t u r n −1; } } 3.4 Funções in-line Como indicado, o uso de funções para substituir códigos repetitivos tem as vantagens de reduzir o código executável e de facilitar a manutenção do código, por ele estar localizado em apenas um ponto no código fonte. Quando a função é extremamente simples, entretanto, (como na função sq4 acima, pag. 25), a vantagem de redução do código executável é mínima, e surge a desvantagem de que o tempo de execução envolvido com uma chamada de função fica significativo, tornando o código mais lento, principalmente se a função for chamada com muita freqüência, ou dentro de uma repetição com muitas iterações. Nestes casos, podemos ainda conservar a vantagem de manter o código fonte em apenas um lugar, mas eliminar a desvantagem do código de chamada e retorno de função pela definição da função como inline . Por exemplo, sq4 poderia ser redefinida: 1 2 3 4 i n l i n e double sq4 ( double x ) { r e t u r n x∗x ; } o que resultaria em que, em cada chamada da função sq4, o seu código seria expandido de acordo com as operações especificadas, isto é, uma chamada do tipo: 1 y = sq4 ( z ) ; seria compilada como: 1 y = z∗z ; ao invés de como uma chamada da função sq4 como anteriormente. Na verdade a definição de funções in-line é muito delicada, pois as vantagens da declaração de uma função como inline são dependentes das características do processador utilizado. Esta análise é melhor deixada a cargo do compilador, quando esse tipo de otimização é disponível. As funções in-line devem ser definidas no mesmo arquivo em que são utilizadas e não podem ser compiladas separadamente (sobre compilação separada veja capítulo 6). U NIVERSIDADE DE S ÃO PAULO 3.5 Passagem por referência 3.5 27 Passagem por referência Nas funções que apresentamos até aqui, lidamos sempre com o que é denominado passagem de parâmetros por valor. Isto significa que o parâmetro da função é considerado uma nova variável, à qual será atribuído o valor do argumento utilizado na chamada da função. Um efeito disso é que alterações realizadas pela função no valor de seu parâmetro não implicam numa alteração do argumento. Por exemplo, no código abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int f ( int a ) { a ++; return a ; } /∗ . . . ∗/ i n t main ( ) { i n t m, n ; /∗ . . . ∗/ n = 5; m = f (n ); /∗ . . . ∗/ } / / A q u i m == 6 e n == 5 o valor de n (isto é, 5) é passado para o parâmetro a da função f. O valor da variável a é então incrementado de um, sem que no entanto isto afete o valor da variável n. Na verdade, na chamada de f, qualquer expressão poderia ter sido utilizada, ao invés da variável n, pois apenas o valor da expressão é necessário, valor esse que será utilizado para inicialização do parâmetro a. A situação é diferente no caso de arrays. Se um array é utilizado como argumento para uma função, então a alteração de elementos do array dentro da função se reflete no array utilizado como argumento. Por exemplo no código abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 v o i d Z e r o ( i n t v [ ] , i n t N) { f o r ( i n t i = 0 ; i < N; i ++) v [ i ] = 0; } /∗ . . . ∗/ i n t main ( ) { int a [10]; /∗ . . . ∗/ Zero ( a , 1 0 ) ; /∗ . . . ∗/ / / Todos o s e l e m e n t o s de a s a o a g o r a 0 } Em certos casos, desejamos que as alterações realizadas em um parâmetro dentro de uma função sejam refletidas na variável que foi utilizada como argumento na chamada da função. Isto é conseguido através da passagem de parâmetro por referência. A sintaxe para isso utiliza-se do operador &, e é demonstrada no exemplo seguinte: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 i n t g ( i n t &a ) { a ++; return a ; } /∗ . . . ∗/ i n t main ( ) { i n t m, n ; /∗ . . . ∗/ n = 5; m = g(n ); /∗ . . . ∗/ } / / A q u i m == 6 e n == 6 Neste caso, durante a execução da função g, o parâmetro a é apenas uma referência (isto é, um outro nome) para a variável n da função main, e o incremento executado em a é portanto refletido na variável n. Quando o parâmetro de uma função é do tipo referência, então o correspondente argumento na chamada de função deve sempre ser uma variável, isto I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 28 Funções é, expressões não são aceitas como argumentos, a menos que o parâmetro tenha sido definido como constante (ver seção 3.8), caso em que o compilador irá gerar uma variável temporária, que armazenará o valor dessa expressão, e o parâmetro será uma referência a essa variável temporária. Além de serem utilizadas como parâmetros de funções, referências podem também ser utilizadas em outras circunstâncias. Por exemplo, podemos definir uma variável como uma referência para uma outra variável existente do mesmo tipo: 1 2 3 4 5 6 7 /∗ . . . ∗/ int a ; i n t &b = a ; / / define b /∗ . . . ∗/ a = 1; / / a g o r a a == a ++; / / a g o r a a == b += 3 ; / / a g o r a a == como uma r e f e r ê n c i a p a r a a 1 e b == 1 2 e b == 2 5 e b == 5 Este tipo de uso no entanto pode gerar códigos difíceis de entender, e deve ser evitado. Um outro uso mais freqüente de referências é para valores de retorno de funções. Um exemplo é a função abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 i n t &Elem ( i n t v [ ] , i n t i ) { return v [ i ] ; } /∗ . . . ∗/ i n t main ( ) { int a [10] , x ; /∗ . . . ∗/ x = Elem ( a , 2 ) ; / / equivale a x = a [2] Elem ( a , 4 ) = −5; / / e q u i v a l e a a [ 4 ] = −5 } Como o valor retornado pela função Elem é definido como uma referência para uma variável do tipo int , a chamada Elem(a,2) retornará uma referência ao elemento de índice 2 do vetor a. O retorno da função pode ser usado do lado esquerdo de uma atribuição, e por isso é um lvalue, podendo ser utilizado em qualquer lugar onde uma variável possa ser utilizada. Outro uso de referências é como membros de classes (capítulo 7). Referências podem apenas ser inicializadas, e após inicialização não podem ser alteradas (isto é, não podemos fazer com que elas se refiram a outras variáveis). 3.6 Protótipos Funções podem ser declaradas sem ser definidas através do uso de protótipos. Um protótipo especifica o nome da função, seus parâmetros e o valor de retorno, mas não apresenta o código a ser executado. Por exemplo, um protótipo para a função Elem definida acima seria: 1 i n t &Elem ( i n t v [ ] , i n t i ) ; Os nomes dos parâmetros em um protótipo não têm nenhum significado, e podem ser omitidos, como abaixo: 1 i n t &Elem ( i n t [ ] , i n t ) ; no entanto eles são geralmente utilizados, pois servem como comentário para indicar o uso esperado de cada parâmetro, como no exemplo abaixo: 1 i n t &Elem ( i n t v e t o r [ ] , i n t i n d i c e ) ; O nome da função, tipo dos parâmetros e tipo de retorno devem ser idênticos no protótipo e na definição da função. Os nomes dos parâmetros utilizados no protótipo podem diferir dos nomes usados na definição. A vantagem do uso de protótipos é que, uma vez estabelecida a interface de uma função (chamamos de interface de uma função o seu nome, tipos dos parâmetros e tipo de retorno), outras funções que dependem desta função podem ser desenvolvidas independentemente do código desta. Isto facilita o desenvolvimento independente de funções por diversos programadores. Além disso, o uso de protótipos é obrigatório quando utilizamos funções de uma biblioteca (veja capítulo 6), pois neste caso o código das funções não é conhecido. Alguns compiladores inferem o protótipo de acordo com o contexto no local onde a função é chamada e apresentam apenas um advertência (warning), mas a inferência pode se mostrar errada. U NIVERSIDADE DE S ÃO PAULO 3.7 Argumentos assumidos 3.7 29 Argumentos assumidos Para aumentar a flexibilidade de funções, C++ permite a especificação de valores a serem assumidos para certos parâmetros de uma função. Na ausência de um argumento para esse parâmetro, o valor assumido é utilizado. A função abaixo atribui novos valores aos elementos de um vetor: 1 2 3 4 5 v o i d I n i A r r a y ( i n t v [ ] , i n t N, i n t v a l o r = 0 ) { f o r ( i n t i = 0 ; i < N; i ++) v[ i ] = valor ; } O código ‘= 0’ presente na declaração do parâmetro valor indica que este tem um valor assumido de 0. Desta forma, se em uma chamada à função IniArray não houver um argumento correspondente ao parâmetro valor , este assumirá o valor 0. 1 2 3 4 5 /∗ . . . ∗/ int a [10] , b [20] , c [30]; IniArray (a ,10 ,0); IniArray ( b ,20 , −1); IniArray (c ,30); / / equivale a IniArray (c ,30 ,0) Mais do que um parâmetro podem ter valores assumidos. Por exemplo, a função abaixo computa no parâmetro c um combinação linear dos parâmetros a e b: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 v o i d CombLin ( i n t c [ ] , i n t a [ ] , i n t b [ ] , i n t N, i n t c a = 1 , i n t cb = 1 ) { f o r ( i n t i = 0 ; i < N; i ++) c [ i ] = c a ∗ a [ i ] + cb ∗b [ i ] ; } /∗ . . . ∗/ i n t main ( ) { /∗ . . . ∗/ int a [10] , b [10] , c [10] , d [10] , e [10] , f [10] , g [10] , h [10] , i [10]; /∗ . . . ∗/ CombLin ( a , b , c , 1 0 , 2 , 3 ) ; / / a = 2∗ b+3∗ c CombLin ( d , e , f , 1 0 ) ; / / d = e+ f CombLin ( g , h , i , 1 0 , 3 ) ; / / g = 3∗ h+ i /∗ . . . ∗/ } Algumas regras com relação a argumentos assumidos: 1. Os valores assumidos podem ser especificados tanto no protótipo quanto na definição da função, mas não nos dois simultaneamente. Isto facilita a manutenção do código, pois os valores assumidos precisam ser alterados em apenas um lugar, e não existem possibilidades de definições conflitantes. Como regra geral, especificamos os valores assumidos nos protótipos. 2. Todos os parâmetros com valores assumidos devem ser agrupados no final da lista de parâmetros. 3. Na chamada de uma função com parâmetros com valores assumidos, se o argumento para um parâmetro for omitido, os argumentos para todos os parâmetros seguintes devem ser omitidos. Isto significa, por exemplo, que na função CombLin acima não é possível omitir o argumento para o parâmetro ca sem simultaneamente omitir o argumento para o parâmetro cb. Devido a isto a ordem dos parâmetros na lista de parâmetros deve ser cuidadosamente escolhida. 3.8 Parâmetros constantes Os parâmetros têm, dentro da função, a mesma funcionalidade de variáveis. Eles podem ser utilizados em qualquer situação onde uma variável possa ser utilizada. Em alguns casos, queremos que os parâmetros tenham, dentro da função, a funcionalidade de constantes, isto é, que seu valor não possa ser alterado. Isto pode ser conseguido com o uso do especificador const. A seguinte variante da função CombLin utiliza-se desta possibilidade: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 30 1 2 3 4 5 6 Funções v o i d CombLin ( i n t c [ ] , c o n s t i n t a [ ] , c o n s t i n t b [ ] , c o n s t i n t N, c o n s t i n t c a = 1 , c o n s t i n t cb = 1 ) { f o r ( i n t i = 0 ; i < N; i ++) c [ i ] = c a ∗ a [ i ] + cb ∗b [ i ] ; } Um caso especial interessante é o de parâmetros do tipo referência constantes. Em geral, utiliza-se passagem por referência quando desejamos que a alteração do valor do parâmetro seja comunicada à variável utilizada como argumento na chamada da função. Por outro lado, const é utilizado quando o parâmetro não será alterado pela função. É claro que neste caso const e referência são contraditórios. No entanto, em alguns casos a passagem por referência é utilizada não para retornar alterações realizadas pela função, mas para evitar a cópia do valor da variável original no parâmetro. Isto é especialmente útil quando o tipo da variável é estruturado e de tamanho muito grande (ver capítulo 5), ou quando estamos definindo a forma de cópia de objetos do tipo, isto é, no construtor de cópia, como veremos no capítulo 7. É aconselhável a declaração de parâmetros formais como constantes sempre que, de acordo com o significado do parâmetro, considerando-se a funcionalidade esperada para a função, fique claro que ele é uma constante para a função, como no caso dos parâmetros constantes da função acima. Em alguns casos, o uso de const pode até ser imprescindível, como quando desejamos permitir a de passagem de objetos constantes como argumentos (veja capítulo 7). Valores de retorno de funções também podem ser declarados constantes, caso em que terão as mesmas restrições de acesso de uma constante. Isto é especialmente útil quando queremos retornar uma referência a um elemento da classe, para evitar realizar uma cópia desse elemento, mas ao mesmo tempo impedir que esse elemento possa ser alterado externamente aos métodos da classe (manter a encapsulação 7). 3.9 Sobrecarga de nomes de funções Em C++, as funções são distinguidas não apenas através de seus nomes, mas também por meio do número e dos tipos de seus parâmetros. Isto é denominado sobrecarga de nomes de funções. O trecho de código seguinte define três diferentes funções denominadas sqr: 1 2 3 4 5 6 7 8 9 10 11 12 int sqr ( const int i ) { return i ∗ i ; } f l o a t sqr ( const f l o a t x ) { r e t u r n x∗x ; } double s q r ( const double x ) { r e t u r n x∗x ; } (nota: estas funções poderiam ser mais facilmente definidas com o uso de templates, capítulo 11). Ao ser feita uma chamada para a função, o compilador decide qual das funções chamar com base nos tipos dos argumentos utilizados: 1 2 3 4 5 6 7 8 9 10 11 12 i n t main ( ) { int i , j ; float x , y; double d ; /∗ . . . ∗/ i = sqr ( j ) ; x = sqr (y ) ; d = sqr ( 2 . 0 ) ; y = sqr ( 1 . 0 ) ; x = sqr ( 3 ) ; } // // // // // chama chama chama chama chama i n t sqr ( const i n t i ) f l o a t sqr ( const f l o a t x ) double sqr ( const double x ) double sqr ( const double x ) i n t sqr ( const i n t i ) As regras exatas utilizadas pelo compilador para resolução de conflitos na sobrecarga de funções são bastantes complexas. Alguns pontos importantes são apresentados abaixo, e no geral problemas podem ser evitados considerando esses fatores e evitando uma programação muito arriscada. De qualquer forma, a compreensão (e portanto também a manutenção e o desenvolvimento) de programas que dependam de detalhes mais complexos das regras da linguagem é difícil e portanto o desenvolvimento de programas que dependem desses detalhes deve ser evitado. U NIVERSIDADE DE S ÃO PAULO 3.10 Variáveis locais e globais 31 Os tipos de retorno não são utilizados na resolução. Isto pode ser verificado por exemplo no programa acima, onde na atribuição y=sqr (1.0) foi feita uma chamada à função que opera com double (o tipo da constante 1.0), apesar de y ser float . Quando não existe uma função com parâmetros com o tipo exato dos argumentos, então conversões padrão (seção 1.5) ou definidas pelo usuário (seção 7.6) são aplicadas. A função a ser escolhida será aquela que requerer o menor número de conversões ou as conversões mais simples. Para efeito de resolução, uma função com valores assumidos para parâmetros é considerada como múltiplas funções (seção 3.7) com diversos números de parâmetros. 3.10 Variáveis locais e globais Quando uma variável é definida no corpo de uma função, dizemos que essa variável é local à função, pois seu identificador é válido apenas no corpo da própria função. Estas variáveis são normalmente automáticas, pois são geradas novamente cada vez que a função for chamada, sendo seu valor na chamada anterior perdido. No caso de uma função recursiva, cada uma das instâncias recursivas da função tem uma variável diferente para essas variáveis automáticas. 1 2 3 4 5 6 7 8 9 i n t SomaQuadrados ( c o n s t i n t v [ ] , c o n s t i n t N) { i f (N == 0 ) return 0; else { i n t s = v [N−1]∗ v [N−1]; r e t u r n SomaQuadrados ( v , N−1)+ s ; } } Aqui cada uma das instâncias recursivas de SomaQuadrados terá sua própria variável s, que é uma variável local automática. Um outro tipo de variável local é a variável local estática. Esta difere das variáveis automáticas pelo fato de reter o valor entre uma chamada e outra da função. Para definir uma variável como estática devemos acrescentar o especificador static antes do tipo. Por exemplo, a função abaixo retorna o número de vezes que foi chamada: 1 2 3 4 5 i n t Conta ( ) { s t a t i c int n = 0; r e t u r n ++n ; } Note que sem o static a variável n seria automática, e portanto gerada e inicializada com 0 toda vez que a função fosse chamada, o que resultaria na função retornando sempre o valor 1. Como ela é estática, o valor assumido numa chamada é guardado para a próxima, e incrementado novamente, gerando uma seqüência crescente de números. A inicialização de variáveis estáticas é executada ao se começar a execução do programa, enquanto que a inicialização de variáveis automáticas é executada quando o programa atinge o ponto de definição da variável. Além de variáveis locais, que somente são acessíveis dentro de uma função, podemos definir também variáveis globais, que podem ser acessadas por qualquer função em um programa (em princípio; veja capítulo 6 para maiores detalhes). Para definir uma variável como global, basta defini-la fora do corpo de qualquer função. Variáveis globais devem, como regra geral, ser evitadas, mas constantes globais são muito úteis. Variáveis globais, como variáveis locais estáticas, são inicializadas ao começar a execução do programa, antes de iniciar a exceção da função main. Exercícios 1. Em quais situações o uso de funções é vantajoso? Quais são essas vantagens? 2. Quando uma função deve possuir parâmetros? O que deve ser parâmetro de uma função? 3. Um erro típico na definição de funções recursivas é sua não-terminação. Apresente condições gerais para que uma função recursiva termine. 4. O que são funções in-line? Quais as vantagens e desvantagens? 5. Normalmente, parâmetros são passados por valor em C++. Em que situações deve ser utilizada passagem por referência? Qual a vantagem do uso de referências ao invés de ponteiros (como em C) nesses casos? I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 32 Funções 6. O que são protótipos de funções? Para que são eles utilizados? 7. O que é um argumento assumido em uma função? Em quais situações é útil o uso de argumentos assumidos? 8. Qual é o efeito de definir um parâmetro como constante? Quando um parâmetro deve ser definido como constante? 9. O que se entende por sobrecarga de nome de função? Como o compilador decide qual das diversas funções sobrecarregadas deve ser chamada em cada caso? Você consegue enxergar um perigo no uso de sobrecarga com relação à engenharia de software (em especial à manutenção)? 10. O que são variáveis globais? Por que o uso de variáveis globais deve ser evitado como regra geral? 11. Qual a diferença entre uma variável estática e uma variável automática? Qual a semelhança entre elas? 12. Qual a diferença entre uma variável global e uma variável estática? Qual a semelhança entre elas? 13. Indique erros existentes nos códigos abaixo: (a)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 v o i d i n c ( i n t n ) { n ++; } i n t main ( ) { for ( int i = 0; i < 10; inc ( i ) ) { s t d : : c o u t << i << s t d : : e n d l ; } } (b)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 v o i d d e c ( i n t &n ) { n−−; } i n t main ( ) { const int N = 10; f o r ( i n t i = N; i >= 0 ; i −−) { s t d : : c o u t << N << s t d : : e n d l ; d e c (N ) ; } } (c)1 i n t soma ( i n t v [ ] , c o n s t i n t n ) 2 { i n t s =0; f o r ( i n t i = 0 ; i < n ; i ++) s +=v [ i ] ; 3 4 5 6 } (d)1 v o i d i m p r i m e ( c o n s t i n t i ) 2 { s t d : : c o u t << i << " " ; imprime ( i + i ) ; 3 4 5 } (e)1 v o i d P r o d ( i n t v [ ] , c o n s t i n t N=10 , c o n s t i n t f a c ) 2 { f o r ( i n t i = 0 ; i < N; i ++) v [ i ] ∗= f a c ; 3 4 5 } (f)1 i n t f ( i n t a , i n t b = 0 ) { r e t u r n a+b ; } 2 i n t f ( i n t a ) { r e t u r n 2∗ a ; } 14. Qual o resultado da execução dos programas abaixo? U NIVERSIDADE DE S ÃO PAULO 3.10 Variáveis locais e globais 33 (a)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int m i s t e r i o ( const int , const int ) ; i n t main ( ) { int x , y ; s t d : : c o u t << " De d o i s v a l o r e s : " ; s t d : : c i n >> x >> y ; s t d : : c o u t << " R e s u l t a d o : " << m i s t e r i o ( x , y ) << s t d : : e n d l ; return 0; } int m i s t e r i o ( const int a , const int b ) { i f ( b <= 0 ) r e t u r n 0 e l s e i f ( b == 1 ) r e t u r n a ; e l s e r e t u r n a+ m i s t e r i o ( a , b −1); } (b)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void f ( const i n t [ ] , const i n t ) ; i n t main ( ) { const int N = 10; i n t a [ ] = {2 , 4 , 7 , 12 , 13 , 12 , 6 , 21 , 8 , 12}; s t d : : c o u t << " V a l o r e s no a r r a y : " << s t d : : e n d l ; f ( a ,N) ; s t d : : c o u t << s t d : : e n d l ; return 0; } void f ( const i n t b [ ] , const i n t n ) { i f ( n > 0) { f (&b [ 1 ] , n −1); s t d : : c o u t << b [ 0 ] << " " ; } } (c)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 int f ( const int a , const int b = 2 , const int c = 3) { r e t u r n a+b∗ c ; } i n t main ( ) { s t d : : c o u t << " A l g u n s v a l o r e s : " << f ( 3 , 4 , 5 ) << " " << f ( 7 , 9 ) << " " << f ( 2 ) << s t d : : e n d l ; } 15. Escreva um programa que leia números inteiros de cinco dígitos e imprima esses dígitos separados por espaços (dois espaços entre cada dígito). Se a entrada dada pelo usuário não corresponde a um número inteiro de cinco dígitos, uma mensagem de erro deve ser enviada. Use funções para organizar o programa. 16. Escreva um programa para avaliar o valor da exponencial de x (ex ) através de aproximação por série de Taylor. Pense num bom critério de aproximação para truncar a série. Use funções para organizar o programa. 17. Escreva uma função para retornar o máximo divisor comum de dois números. 18. Escreva uma função para simular o lançamento de uma moeda. A função deve retornar cara ou coroa, aleatoriamente. Use uma função de geração de números aleatórios da biblioteca padrão. 19. Usando uma função de geração de números aleatórios da biblioteca padrão, escreva uma função que, dado um número de ponto flutuante, retorne um número aleatório entre zero e o valor dado. 20. Escreva um programa para avaliar experimentalmente as probabilidades da soma dos valores de dois dados lançados. O programa deve simular o lançamento de dois dados, sendo os valores de 1 a 6 equiprováveis em ambos os dados. Um array deve ser então utilizado para contar o número de vezes que todas as possíveis somas foram sorteadas. No I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 34 Funções começo, o programa lê o número de experimentos que devem ser efetuados, e no final imprime as probabilidades calculadas para cada valor de soma possível. 21. Escreva uma função para ordenação pelo método quicksort de um vetor de números inteiros. U NIVERSIDADE DE S ÃO PAULO Capítulo 4 Ponteiros Ponteiros são tipos de dados derivados, que possibilitam lidar com estruturas dinâmicas de dados. Um ponteiro tem como valor um endereço de memória, que corresponde ao endereço de uma variável. Veremos neste capítulo como tratar com ponteiros em C++. 4.1 Definição de ponteiros Para lidarmos com ponteiros, devemos definir variáveis capazes de armazenar ponteiros, isto é, variáveis cujos valores serão ponteiros. Isto é feito acrescentando um ∗ antes do nome da variável em sua definição. Um ponteiro deve “apontar” para variáveis de apenas um tipo, e este tipo é especificado na definição do ponteiro, como abaixo: 1 2 3 char ∗ s , ∗ t ; int ∗ pi ; d o u b l e ∗ pd ; Neste código, s e t são definidos como ponteiros para char, pi como ponteiro para int e pd como ponteiro para double. Em alguns casos especiais, queremos um ponteiro que possa apontar para qualquer tipo de variável. Isto pode ser feito com o uso do pseudo-tipo void: 1 v o i d ∗ pv ; Qualquer tipo de ponteiro pode receber o valor especial 0. Este valor, chamado ponteiro nulo indica que o ponteiro não está apontando para nenhuma variável válida. 1 2 s = t = 0; pd = 0 ; 4.2 Operadores de ponteiros Como o interesse em programas é em geral lidar com valores dos diversos tipos, e não com ponteiros para esses valores, necessitamos de operadores para associar um ponteiro a uma variável e para ler o valor de uma variável apontada por um ponteiro. Estes são os operadores & e ∗, respectivamente. & Dada uma variável v, to tipo t, &v retornará um ponteiro para a variável v (o endereço de v), e será do tipo “ponteiro para t”. Veja os exemplos abaixo (considerando as definições de variáveis tipo ponteiro anteriores): 1 2 3 4 5 6 7 8 9 char c ; int a ; double x ; /∗ . . . ∗/ s = &c ; p i = &a ; pd = &x ; pv = &x ; / / pd = &a s e r i a i n v a l i d o I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 36 Ponteiros Nestas definições, s recebe o valor do endereço de c (dizemos que s aponta para c), pi recebe o valor do endereço de a, pd recebe o endereço de x e pv recebe também o endereço de x. Em todos os casos anteriores, menos o último, a variável é do tipo esperado pelo ponteiro. No caso de pv, este é um ponteiro para void, e portanto aceita apontar para qualquer tipo de variável. Outros tipos de ponteiros não aceitam endereços de variáveis de tipo distinto (como na linha comentada acima). ∗ Quando temos um ponteiro para uma variável de um certo tipo, podemos conseguir o valor armazenado por essa variável através do operador de acesso indireto, como nos exemplos abaixo (ainda tomando em conta as definições anteriores): 1 2 3 4 5 6 char b ; int i ; double z ; b = ∗s ; i = ∗ pi +1; z = 2 ∗ ( ∗ pd ) ; O mesmo não é possível através de um ponteiro para void: um ponteiro para void pode ser utilizado apenas como ponteiro, pois o compilador não saberia como interpretar o valor apontado. 4.3 Ponteiros e arrays Ponteiros e arrays possuem em C++ uma afinidade especial. Quando o identificador de um array aparece isoladamente (sem a presença do operador de indexação), então ele é considerado como representando um ponteiro para o primeiro elemento do array. Assim, o seguinte código é válido: 1 2 3 4 int a [10] , b [3][5]; i n t ∗p1 , ∗ p2 ; p1 = a ; p2 = b [ 2 ] ; Para associar-se um ponteiro a um elemento do array que não o primeiro, devemos utilizar o operador de endereçamento: 1 2 i n t ∗ p3 ; p3 = &a [ 9 ] ; Como complemento dessa similaridade, o operador de indexação pode ser aplicado a um ponteiro: 1 2 int x ; x = p1 [ 3 ] ; A interpretação do significado do uso do operador de indexação com ponteiros ficará clara quando discutirmos aritmética de ponteiros (ver abaixo), mas o resultado da operação acima será o esperado da equivalência entre ponteiros e arrays. 4.4 Aritmética de ponteiros Algumas operações aritméticas com ponteiros são permitidas. O significado dessas operações está vinculado à relação entre ponteiros e arrays discutida acima, sempre se considerando que o ponteiro está apontando para um array de elementos do tipo correspondente. Uma das operações é a de somar (ou subtrair) um inteiro a (ou de) um ponteiro. O efeito é fazer o ponteiro avançar (ou recuar) o número de elementos especificado pelo inteiro. Veja os exemplos abaixo: 1 2 3 4 5 6 7 8 9 10 int a [100]; int ∗ pi ; pi = a ; // p i += 3 ; / / p i −−; // char b [ 1 0 ] ; char ∗ pc ; pc = b ; // pc += 9 ; / / pc −= 6 ; / / p i aponta para a [0] p i passa a apontar para a [3] p i passa a apontar para a [2] pc a p o n t a p a r a b [ 0 ] pc a p o n t a p a r a b [ 9 ] pc a p o n t a p a r a b [ 3 ] U NIVERSIDADE DE S ÃO PAULO 4.5 Cadeias de caracteres 37 Note que a aritmética funciona de forma correta, não importa o número de bytes necessário para o armazenamento dos elementos. A outra operação aritmética permitida é a subtração de ponteiros, que pode ser realizada desde que ambos os ponteiros apontem para o mesmo tipo. O resultado é um inteiro, que determina o número de elementos no array entre os dois ponteiros: int a [10]; i n t ∗p1 , ∗p2 , 3 i n t m, n , o ; 4 p1 = a ; 5 p2 = &a [ 9 ] ; 6 p3 = &a [ 4 ] ; 7 m = p2 − p1 ; 8 n = p3 − p1 ; 9 o = p3 − p2 ; 1 2 ∗ p3 ; / / m vale 9 / / n vale 4 / / o v a l e −5 Agora podemos compreender o significado da aplicação do operador de indexação a um ponteiro. Sendo p um ponteiro e i uma expressão que retorna um inteiro, p[ i ] é equivalente a ∗(p+i). Nenhuma operação aritmética é válida sobre um ponteiro para void, pois o compilador não sabe a que tipo de elemento o ponteiro aponta. A aritmética sobre ponteiros é muito sujeita a problemas, e cautela deve ser exercida em seu uso (cautela que também se estende à indexação de arrays, pois como vimos indexação de arrays é equivalente a aritmética de ponteiros): • Só faz sentido utilizar aritmética com um ponteiro que realmente aponte para um array. Para ponteiros que apontem para variáveis isoladas, qualquer aritmética com valores diferentes de 0 tem como resultado um valor sem sentido (um ponteiro para um elemento não determinado). • Deve-se ter cuidado para que a aritmética não resulte em acesso a elementos inexistentes no array, por exemplo tentativa de acesso a elementos com índices negativos ou maiores que o número de elementos do array menos um. • A subtração de dois ponteiros só faz sentido se ambos apontam para elementos do mesmo array. Se esse não for o caso, o resultado é indeterminado. Nenhum erro de aritmética com ponteiros (como os indicados acima) é testado pelo compilador ou durante a execução do código. Isso significa que o efeito de um erro desses será a produção de resultados errôneos por parte do programa, sem nenhum aviso ao usuário. 4.5 Cadeias de caracteres Vimos que arrays podem ser considerados como ponteiros. O mesmo se aplica a cadeias de caracteres. Uma cadeia de caracteres (cuja constante literal é indicada entre aspas, como vimos, subseção 1.3.5) é representada em C++ como um array de char, sendo os caracteres consecutivos colocados em elementos com índices consecutivos do array, e tendo também, após o último caracter da cadeia, um caracter que corresponde ao caracter com código numérico zero, e que pode ser representado por ’ \0 ’ em C++. Isto significa que cadeias de caracteres podem ser utilizadas quando um array de char é esperado, ou um array de char (terminado por ’ \0 ’) pode ser usado onde uma cadeia de caracteres é esperada. Obviamente, devido à equivalência entre arrays e ponteiros, ponteiros para char são também intercambiáveis com cadeias de caracteres. Na verdade, cadeias de caracteres são internamente representadas por ponteiros para char. A função seguinte, apresentada como exemplo, realiza a copia de cadeias de caracteres (tomando em consideração que cadeias de caracteres são sempre terminadas por um caracter zero): 1 2 3 4 v o i d s c o p y ( char ∗d , char ∗ s ) { w h i l e ( ∗ d++ = ∗ s + + ) ; } Aqui foi utilizado, propositalmente, o estilo “criptico” de alguns programas C antigos. Para entender o funcionamento desta função devemos considerar os seguintes pontos: • O operador ++ tem maior precedência do que o operador ∗. Desta forma, são incrementados os ponteiros, e não os caracteres apontados. • O auto-incremento é um pós-incremento, e portanto o valor inicial do ponteiro é usado pelo operador de acesso indireto, e em seguida o ponteiro é incrementado. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 38 Ponteiros • O operador de atribuição retorna um valor, que é o valor que foi atribuído. Este é o valor do caracter anteriormente apontado pelo ponteiro s. Este valor é utilizado como condição para o while, e será falso (igual a zero) apenas quando o caracter ’ \0 ’ final da cadeia for atingido. • Como tanto o trabalho de cópia de um caracter da cadeia fonte para a cadeia destino como o trabalho de atualização dos dois ponteiros foi efetuado no cálculo da condição, nenhum trabalho adicional é necessário no corpo do while, que então possui apenas um comando nulo (apenas um ponto-e-vírgula). • Como os ponteiros para as cadeias fonte e destino foram passados por valor, as alterações no valor dos ponteiros realizadas dentro da função não serão transmitidas novamente de volta à função que realizou a chamada. Considerando que os parâmetros são ponteiros para char, tanto arrays de char como cadeias de caracteres podem ser utilizados como argumentos: 1 2 char b [ 1 0 ] ; s c o p y ( b , "Bom d i a ! " ) ; 4.6 Alocação dinâmica de memória Ponteiros são também utilizados para alocação dinâmica de memória. Alocação dinâmica permite que espaço para armazenamento de dados seja determinado em tempo de execução, e não durante a compilação do programa. Isto tem duas vantagens: primeiro permite que o número de elementos de arrays seja decidido apenas em tempo de execução, e que portanto o espaço ocupado por eles seja adequado à tarefa a executar, e não precise ser decidido com com base numa análise de máximo necessário; segundo, é possível desta forma lidar com estruturas de dados dinâmicas, como listas e árvores binárias (veja capítulo 5 sobre estruturas de dados). O gerenciamento dinâmico de memória em C++ ocorre por meio dos operadores new e delete , o primeiro reserva espaço para uma estrutura de dados em uma região de memória disponível, e o segundo libera um espaço anteriormente reservado. new O operador new recebe como operando um tipo de dados, que indica o tipo do dado a ser alocado. O valor resultante é um ponteiro para a variável alocada. Veja exemplo: 1 2 3 4 5 int ∗ pi ; p i = new i n t ; ∗ pi = 3; char ∗ pc = new char ; ∗ pc = ’ s ’ ; Arrays de elementos de um certo tipo também podem ser alocados com new, colocando-se, após o nome do tipo, o número de elementos entre colchetes: 1 2 3 i n t ∗v = new i n t [ 1 0 0 ] ; f o r ( i n t i = 0 ; i < 1 0 0 ; i ++) v [ i ] = 10∗ i ; Esta é a forma utilizada em C++ de lidar com arrays com o número de elementos determinado em tempo de execução. Nada impede que arrays de ponteiros sejam alocados: 1 2 d o u b l e ∗∗m; m = new ( d o u b l e ∗ ) [ 1 0 ] ; Este exemplo coloca em m um ponteiro para o primeiro elemento de um array de 10 ponteiros para double. Cada ponteiro desse array pode por sua vez apontar para arrays alocados dinamicamente: 1 2 f o r ( i n t i = 0 ; i < 1 0 ; i ++) m[ i ] = new d o u b l e [ 3 0 ] ; Cada um dos elementos do array apontado por m aponta por sua vez agora para um array de 30 elementos double. Como m[i] é um ponteiro, o operador de indexação pode ser utilizado com ele: 1 2 3 f o r ( i = 0 ; i < 1 0 ; i ++) f o r ( j = 0 ; j < 3 0 ; j ++) m[ i ] [ j ] = 0 ; U NIVERSIDADE DE S ÃO PAULO 4.7 Ponteiros como parâmetros 39 Esta é a forma de trabalho com arrays multidimensionais de tamanhos decididos em tempo de execução em C++. Note que arrays multidimensionais dinâmicos e estáticos são essencialmente diferentes, apesar de serem indexados de forma sintaticamente similar. Considerando-se um array bidimensional definido como: 1 d o u b l e m2 [ 1 0 ] [ 3 0 ] ; podemos fazer as seguintes comparações com o array dinâmico m definido acima: • tanto m[i][ j ] como m2[i][ j ] são do tipo double; • tanto m[i] como m2[i] são compatíveis com o tipo (double ∗); • m é do tipo (double ∗∗), enquanto que m2 é um ponteiro para arrays de 30 elementos double (indicado como double (∗)[30] ). Como resultado desta diferença, um incremento em m provoca um aumento no seu valor correspondente ao tamanho de um ponteiro para double, enquanto um incremento em m2 provoca um aumento de valor correspondente ao tamanho de 30 elementos double. delete Quando um espaço de memória alocado dinamicamente não é mais necessário, ele deve ser liberado, para que posteriores alocações de memória possam se valer desse espaço adicional. Em muitas linguagem orientadas a objeto, este serviço é executado automaticamente por um sistema chamado coletor de lixo. Infelizmente C++ não dispõe de um coletor de lixo, e portanto a liberação deve ser realizada explicitamente pelo usuário. Em certos casos, é extremamente complexo decidir quando a liberação pode ser executada, e nestes casos a falta de um coletor de lixo é uma deficiência grave da linguagem. O operador delete recebe como operando um ponteiro, que deve apontar para uma região de memória previamente alocada pelo operador new. Se o ponteiro aponta para um array, então entre o operador delete e seu operando devem ser colocados os caracteres abre e fecha colchetes ou, de outra forma, o delete não teria como saber que o ponteiro aponta para um array. O número de elementos do array não precisa ser especificado. Veja exemplos abaixo, que consideram as alocações realizadas nos exemplos anteriores: 1 2 3 4 5 6 delete pi ; d e l e t e pc ; delete [] v ; f o r ( i = 0 ; i < 1 0 ; i ++) d e l e t e [ ] m[ i ] ; d e l e t e [ ] m; Note que ao liberar um array bidimensional dinâmico precisamos primeiro liberar os espaços de cada um dos arrays unidimensionais, para então liberar o array de ponteiros, ou não teríamos, após liberar o array de ponteiros, mais acesso aos ponteiros para arrays unidimensionais, e estes permaneceriam alocados, mas sem possibilidade de uso. Isto se generaliza para arrays dinâmicos de dimensões maiores. Se o delete recebe um ponteiro nulo como operando ele simplesmente não faz nada. Assim é perfeitamente correto fazer delete sobre um ponteiro nulo. 4.7 Ponteiros como parâmetros Ponteiros, como qualquer outro tipo de dados, podem ser utilizados como parâmetros para funções. No entanto, devido às suas características peculiares, eles têm alguns usos especiais. Já vimos como ponteiros para char podem ser utilizados para escrever funções que lidam com cadeias de caracteres. Devido à compatibilidade entre arrays e ponteiros, um parâmetro do tipo ponteiro pode receber um argumento do tipo array. Na verdade arrays são internamente representados por ponteiros, e isso explica por que razão a alteração de elementos de um array dentro de uma função é refletida na função que realizou a chamada. Veja o código abaixo: 1 2 3 4 5 6 7 8 9 v o i d ZeroUm ( i n t ∗v , i n t i ) { v [ i ] = 0; } /∗ . . . ∗/ i n t main ( ) { /∗ . . . ∗/ int a [10]; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 40 ZeroUm ( a , 5 ) ; /∗ . . . ∗/ 10 11 12 Ponteiros / / a [5] recebe 0 } O parâmetro v recebe o valor de um ponteiro para o primeiro elemento de a. A expressão v[ i ], que corresponde a ∗(v+i) vai então apontar para o elemento de índice i (5 neste caso) do próprio array a, e este terá seu valor alterado. Isso demonstra como ponteiros podem ser utilizados para simular passagem por referência, visto que o que é feito normalmente com elementos de um array pode também ser feito com variáveis separadas: 1 2 3 4 5 6 7 8 9 10 v o i d i n c ( i n t ∗n ) { (∗ n )++; } /∗ . . . ∗/ i n t main ( ) { int i = 0; i n c (& i ) ; / / i passa a v a l e r 1 } Neste caso, o endereço da variável i é passado como o valor do parâmetro n. Como n aponta para a variável i de main, o valor desta variável será incrementado de um por inc, resultando num efeito similar ao de passagem por referência. Em C, linguagem que foi utilizada como base para o desenvolvimento de C++, não existem referências, e apenas o método de passagem de ponteiros pode ser utilizado. Como C++ se utiliza de diversas funções padrão originalmente desenvolvidas para C, é importante conhecer este método. Ponteiros também podem ser passados por referência, de forma a permitir que a alteração de seu valor dentro da função ocasione alteração no argumento utilizado. A sintaxe para isto é demonstrada no exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 v o i d A l o c a ( i n t ∗&v , c o n s t i n t N) { v = new i n t [N ] ; } /∗ . . . ∗/ { /∗ . . . ∗/ i n t ∗a ; Aloca ( a , 1 0 0 ) ; /∗ . . . ∗/ } Aqui se o parâmetro v não fosse referência, a mudança do seu valor para o valor retornado pelo operador new não se refletiria no ponteiro a, resultando em que este permaneceria não inicializado. 4.8 Ponteiros constantes e ponteiros para constantes Ponteiros, como outras variáveis, também podem ser declarados constantes. Mas no caso de ponteiros, estamos lidando com duas entidades: a variável que armazena o ponteiro e a variável que está sendo apontada. Podemos querer declarar como constante tanto uma como outra (ou ambas), e a linguagem permite isto. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 c o n s t i n t c i = 1 0 , ∗ pc = &c i , ∗ c o n s t c p c = pc ; i n t i , ∗p , ∗ c o n s t cp = &i ; / / Operacoes v a l i d a s ( exemplos ) : i = ci ; ∗ cp = c i ; pc ++; pc = c p c ; pc = p ; / / Operacoes i n v a l i d a s ( exemplos ) : / / ci = i ; / / c i ++; / / ∗ pc = 2 ; / / cp = &c i ; / / c p c ++; / / p = pc ; U NIVERSIDADE DE S ÃO PAULO 4.9 Ponteiros para funções 41 Nas definições acima: ci é uma constante int, pc é um ponteiro para uma constante int (que foi inicializado para apontar para ci), cpc é um ponteiro constante para uma constante int; i é um int, p é um ponteiro para int, cp é um ponteiro constante para int, inicializado para apontar para i. Através de um ponteiro para uma constante, não é possível alterar o valor da variável apontada, mas o ponteiro pode ser alterado para apontar para outras variáveis. Através de um ponteiro constante podemos alterar o valor da variável apontada, mas o ponteiro irá sempre apontar para a mesma variável. Quando temos um ponteiro constante para constante, nem o valor da variável apontada nem o ponteiro podem ser alterados. Isto explica a validade ou não das operações acima. • i = ci é válida pois atribui o valor de uma constante a uma variável comum. • ∗cp = ci é válida pois atribui o valor de uma constante à variável apontada por cp. Apesar do ponteiro haver sido declarado constante, a variável apontada pode ser alterada. • pc++ é válida (apesar de aqui não fazer sentido) pois incrementa o valor do ponteiro pc. O ponteiro aponta para constante, mas ele em si pode ser alterado. • pc = cpc é válida pois altera o valor do ponteiro pc, fazendo-o apontar para a mesma variável que cpc apontava. O ponteiro pc não foi declarado constante. • pc = p é válida pois faz pc apontar para a mesma variável que p aponta. A variável poderá então ser alterada por intermédio de p, mas não por intermédio de pc. • ci = i é inválida pois tenta alterar o valor de uma constante. • ci++ é inválida pois tenta alterar o valor de uma constante. • ∗pc = 2 é inválida pois tenta alterar o valor apontado por um ponteiro para constante. Como pc foi declarado como ponteiro para constante, o valor da variável apontada não pode ser alterado por seu intermédio. • cp = &ci é inválida pois tenta alterar o valor de um ponteiro declarado como constante. • cpc++ é inválida pois tenta alterar o valor de um ponteiro declarado como constante. • p = pc é inválida pois seria possível posteriormente alterar o valor da variável apontada por pc através do ponteiro p, que não foi marcado como apontando para constante. 4.9 Ponteiros para funções Além de apontar para variáveis, um ponteiro pode também apontar para funções. Como no caso dos outros ponteiros, os ponteiros para funções devem ter seu tipo completamente especificado. A especificação de um tipo de função é feita através da determinação de seu tipo de retorno e do número e tipo dos parâmetros. Veja o código abaixo para um exemplo. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 v o i d V e z e s D o i s ( i n t &x ) { x ∗= 2 ; } v o i d MaisUm ( i n t &y ) { y += 1 ; } v o i d A p l i c a ( v o i d ( ∗ f ) ( i n t &) , i n t ∗v , c o n s t i n t N) { f o r ( i n t i = 0 ; i < N; i ++) f (v[ i ]); } i n t main ( ) { int a [10] , b [20]; f o r ( i n t i = 0 ; i < 1 0 ; i ++) a [ i ] = i ; f o r ( i n t i = 0 ; i < 2 0 ; i ++) b [ i ] = 2∗ i ; A p l i c a ( VezesDois , a , 1 0 ) ; A p l i c a ( MaisUm , b , 2 0 ) ; return 0; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 42 Ponteiros Aqui são definidas duas funções (VezesDois e MaisUm) que recebem uma referência para um int e não retornam nada. A função Aplica é definida como tendo três parâmetros: f, v e N. v é um ponteiro para int, e N um int; f por seu lado é definido como um ponteiro para uma função que recebe uma referência para um int e não retorna nada. Como o tipo das funções VezesDois e MaisUm combinam com o tipo do primeiro parâmetro formal de Aplica, elas podem ser utilizadas como argumento para essa função, como feito na função main. Além de parâmetros de funções, ponteiros para funções podem também definir variáveis normais. Por exemplo, dadas as definições acima, o código abaixo é válido: 1 2 3 4 5 6 int m = 3 , n = 6; void (∗ g ) ( i n t &); g = VezesDois ; g (m ) ; / / m v a l e agora 6 g = MaisUm ; g(n ); / / n v a l e agora 7 Exercícios 1. Qual a relação entre variáveis, ponteiros e endereços? 2. Em que sentido ponteiros e arrays são semelhantes? 3. Qual a relação entre aritmética de ponteiros e indexação de arrays? 4. De que forma o tipo do ponteiro influencia as operações aritméticas efetuadas sobre ele? Relacione isso com as duas perguntas anteriores. 5. Existem regras para conversão automática de ponteiros em C++ similares às regras de conversão automática de tipos? Explique a razão. 6. A linguagem não permite a realização de aritmética sobre ponteiros void∗. Explique a razão. 7. Como é representada uma cadeia em C++? 8. Qual a diferença entre uma cadeia e um array de caracteres? 9. O que se entende por alocação dinâmica de memória? Quais são suas vantagens? 10. O que é um array dinâmico? Como ele pode ser definido e inicializado. 11. Como se deve efetuar a inicialização e liberação de arrays dinâmicos multidimensionais? 12. Compare alocação dinâmica de memória em C++ (usando os operadores new e delete ) com a alocação dinâmica em C (usando funções malloc e free ). 13. O que são ponteiros constantes? E ponteiros para constantes? 14. O que são ponteiros para funções? Qual sua utilidade? 15. Indique erros existentes no código abaixo: (a)1 char c , ∗ pc = &c ; 2 3 4 5 6 int ∗ pi ; v o i d ∗ pv ; p i = pc ; pv = pc ; pv ++; 16. Qual o resultado da execução do programa abaixo? U NIVERSIDADE DE S ÃO PAULO 4.9 Ponteiros para funções 43 (a)1 # i n c l u d e < i o s t r e a m > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 i n t maximo ; int f ( const int v [ ] , const int n ) { s t a t i c int a [100] , j = 0; i n t s =0; i f ( n > 1) { a [ j ++] = v [ n −1]; i n t n1 = n / 2 ; i n t n2 = n%2 == 0 ? n /2 −1 : n / 2 ; s = f (&v [ 0 ] , n1 ) + f (&v [ n1 ] , n2 ) + v [ n −1]; } e l s e i f ( n == 1 ) { a [ j ++] = v [ 0 ] ; s = v[0]; } else return 0; i f ( n == maximo ) f o r ( i n t i = 0 ; i < n ; i ++) s t d : : c o u t << a [ i ] << " " ; return s ; } i n t main ( ) { i n t a [10] = {9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0}; maximo = 1 0 ; s t d : : c o u t << " T o t a l : " << f ( a , 1 0 ) << s t d : : e n d l ; return 0; } 17. Escreva uma função para realizar produto escalar de dois vetores. 18. Escreva funções para realizar soma, subtração e produto de matrizes dinâmicas. 19. Escreva uma versão de quicksort para ordenação de um array de cadeias de caracteres. 20. Escreva um programa que leia o tamanho de duas matrizes, reserve espaço para elas, sorteie valores aleatórios entre -10 e 10 para seus elementos e chame então a função de soma de matrizes definida acima. Finalmente, os valores das matrizes sorteadas e da matriz resultado devem ser escritos na tela. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 44 Ponteiros U NIVERSIDADE DE S ÃO PAULO Capítulo 5 Estruturas de dados Tão importante para o bom desenvolvimento de um sistema de computação quanto a estruturação do controle da execução (conseguida através do uso de estruturas de controle adequadas, capítulo 2, e da decomposição em funções, capítulo 3) é a estruturação adequada dos dados que serão processados. Isto é conseguido através do uso de estruturas de dados a serem discutidas neste capítulo (que engloba as estruturas tradicionais) e no capítulo 7 (que trata de orientação a objetos). 5.1 Estruturas No desenvolvimento de programas são de ocorrência comum situações nas quais diversos elementos, possivelmente de tipos distintos, sejam logicamente interligados. Por exemplo, ao representar uma data precisamos de um número para o dia, um para o mês e outro para o ano. Ao representarmos dados sobre um indivíduo, precisamos de uma cadeia de caracteres para o nome, outra para o endereço, e possivelmente outros campos, como campos para a data de nascimento, ou um número de identificação. Se esses diversos elementos fazem logicamente parte de um único elemento mais complexo, eles devem então ser representados por um único elemento para que essa relação entre eles esteja explicitamente incluída no código. C++ dispõe do conceito de estruturas, que podem ser utilizadas para esse fim. O exemplo abaixo define uma estrutura para lidar com datas: 1 2 3 4 s t r u c t Data { i n t d i a , mes , ano ; }; D a t a d1 , d2 , d3 ; A palavra reservada struct é usada para indicar que uma estrutura de dados será definida; em seguida temos o nome da estrutura, neste caso Data, seguido pelos nomes dos membros da estrutura entre chaves, neste caso dia, mes e ano. Os membros indicam os tipos de dados que compõe a estrutura. Neste caso, os três membros são int. Note que, apesar de similar à definição de variáveis, as declarações entre chaves apenas declaram os nomes e tipos dos membros da estrutura. A definição de variáveis do tipo criado é feita como para d1, d2 e d3 acima. Para utilizar as variáveis, devemos saber como acessar o valor dos membros da estrutura. Isto é feito pelo uso do operador de acesso a membro, seguido do nome do membro a ser acessado: 1 2 3 4 5 6 7 8 9 /∗ . . . ∗/ d1 . d i a = d1 . mes = d1 . ano = d2 . d i a = d2 . mes = d2 . ano = d3 = d1 ; 12; 7; 1978; d1 . d i a + 1 ; d1 . mes ; d1 . ano ; / / o mesmo que / / d3 . d i a=d1 . d i a ; d3 . mes=d1 . mes ; d3 . ano=d1 . ano Um membro de uma estrutura pode ser utilizado em qualquer lugar onde uma variável de seu tipo poderia ser utilizada. Atribuições de variáveis estruturadas (variáveis de um tipo declarado com struct ) são também válidas, e têm o mesmo efeito que se os membros fossem atribuídos um a um. Qualquer tipo de dados pode ser utilizado como membro de uma estrutura, inclusive outras estruturas: 1 2 3 struct DadosPessoais { char ∗nome ; char ∗ e n d e r e c o ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 46 4 5 6 7 8 9 10 11 12 13 14 15 Estruturas de dados Data nascimento ; }; i n t main ( ) { DadosPessoais h ; h . nome = " J o a o " ; h . e n d e r e c o = " Rua d a s A c a c i a s , 254 " ; h . nascimento . dia = 1; h . n a s c i m e n t o . mes = 1 ; h . n a s c i m e n t o . ano = 1 9 0 1 ; return 0; } Uma característica importante é a possibilidade de definir estruturas de dados recursivas, isto é, estruturas que se referem a si mesmas. Isto é possível através do uso de ponteiros para uma estrutura dentro da própria estrutura: 1 2 3 4 struct ListaLigada { int valor ; L i s t a L i g a d a ∗ proximo ; }; Quando estamos mexendo com ponteiros para estruturas, é muito comum necessitar-se acessar um membro através de um ponteiro, o que pode ser feito da forma: (∗p ). m (p é um ponteiro para uma estrutura e m é um membro da estrutura). Esta operação pode ser mais brevemente expressa como p−>m. Com a estrutura ListaLigada definida acima podemos implementar uma pilha, definindo funções para manipulação da lista como uma pilha: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 / / I n i c i a l i z a uma p i l h a v a z i a v o i d I n i ( L i s t a L i g a d a ∗& p i l h a ) { pilha = 0; } / / I n s e r e e l e m e n t o de v a l o r v a l na p i l h a v o i d Poe ( i n t v a l , L i s t a L i g a d a ∗& p i l h a ) { L i s t a L i g a d a ∗ novo = new L i s t a L i g a d a ; novo−> v a l o r = v a l ; novo−>p r o x i m o = p i l h a ; p i l h a = novo ; } / / E x t r a i e l e m e n t o do t o p o da p i l h a i n t R e t i r a ( L i s t a L i g a d a ∗& p i l h a ) { int val ; i f ( p i l h a == 0 ) { s t d : : c e r r << " T e n t a t i v a de r e t i r a d a de p i l h a v a z i a ! " << s t d : : e n d l ; exit (1); } v a l = p i l h a −> v a l o r ; L i s t a L i g a d a ∗ tmp = p i l h a ; p i l h a = p i l h a −>p r o x i m o ; d e l e t e tmp ; return val ; } / / V e r i f i c a se a pilha esta vazia bool Vazia ( L i s t a L i g a d a ∗ p i l h a ) { r e t u r n ( p i l h a == 0 ) ; } A função exit que é chamada na função Retira é uma função pré-definida na linguagem, e cujo protótipo se encontra no arquivo cstdlib , que deve ser incluído quando desejarmos utilizar a função. O efeito da chamada da função exit é terminar a execução do programa, e retornar ao sistema operacional o número dado como parâmetro (como no caso de um return executado na função main). U NIVERSIDADE DE S ÃO PAULO 5.2 Tipos enumerados 47 A definição de uma estrutura e de variáveis do correspondente tipo pode ser realizada simultaneamente. Também, se a estrutura será necessária apenas para a definição de certas variáveis, ela não precisa receber um nome. Assim, são válidas todas as definições abaixo: 1 2 3 4 s t r u c t S1 { i n t a ; char b ; f l o a t c ; } ; S1 x , y ; s t r u c t S2 { d o u b l e p ; d o u b l e q ; } z , t ; s t r u c t { i n t n ; i n t ∗v ; } v1 , v2 ; Estruturas podem ser inicializadas de forma similar a arrays (seção 1.9), colocando-se o valor desejado para cada um dos membros entre chave e separados por vírgulas: 1 2 3 4 5 s t r u c t Endereco { char ∗ r u a ; i n t numero ; }; E n d e r e c o EndJoao = { " Rua d a s C a m e l i a s " , 1 3 3 } ; 5.2 Tipos enumerados Outra espécie de tipos de dados definidos pelo usuário são as enumerações. Neste caso uma variável do tipo definido pode assumir apenas um dos valores especificados (enumerados) na definição do tipo. O código abaixo apresenta um exemplo de uso: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 enum DiaDaSemana { Domingo , Segunda , T e r c a , Q u a r t a , Q u i n t a , S e x t a , Sabado } ; i n t main ( ) { DiaDaSemana d ; /∗ . . . ∗/ switch ( d ) { c a s e Domingo : / ∗ . . . ∗ / break ; c a s e Segunda : / ∗ . . . ∗ / break ; case Terca : / ∗ . . . ∗ / break ; c a s e Q u a r t a : / ∗ . . . ∗ / break ; c a s e Q u i n t a : / ∗ . . . ∗ / break ; case Sexta : / ∗ . . . ∗ / break ; c a s e Sabado : / ∗ . . . ∗ / break ; } /∗ . . . ∗/ return 0; } Os tipos enumerados são considerados integrais (daí seu uso possível num switch). Na verdade números inteiros começando em 0 são associados a cada um dos valores sucessivos do enum. No exemplo acima, Domingo == 0, Segunda == 1, etc. Podemos mudar os valores associados especificando o inteiro correspondente algum valor do enum: 1 2 enum Ordem { p r i m e i r o = 1 , segundo , t e r c e i r o } ; enum S u p e r s t i c a o { d e z = 1 0 , onze , doze , c a t o r z e = 1 4 , q u i n z e } ; Nestes exemplos, primeiro == 1, segundo == 2, terceiro == 3; dez == 10, onze == 11, doze = 12, quatorze == 14 e quinze == 15. Também no caso de tipos enumerados, a definição do tipo e a declaração de variáveis podem ser feitas simultaneamente, como para struct . 5.3 Uniões Em algumas situações especiais ocorre desejarmos que uma certa variável ou um certo membro de uma estrutura de dados, tenha o seu tipo dependente de algumas condições no programa. Isto pode ser realizado com o uso de union. A sintaxe é similar à de struct como abaixo: 1 2 union I n t O u P f l o a t { int n ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 48 3 4 5 Estruturas de dados f l o a t ∗v ; }; IntOuPfloat a ; Aqui a variável a poderá armazenar um int ou um ponteiro para float , mas não os dois simultaneamente: 1 2 3 4 a . n = 1 0 ; / / a . n a g o r a tem o v a l o r i n t 10 / / a . v nao pode s e r a c e s s a d o a . v = new f l o a t [ 1 0 ] ; / / a . v a p o n t a p a r a a r r a y de f l o a t / / a . n nao pode s e r a c e s s a d o Ao desenvolver um tipo de dados para armazenar dados pessoais, os dados armazenados podem diferir se a pessoa em questão é brasileira ou estrangeira. Sendo brasileira queremos armazenar o RG e o número de reservista, e sendo estrangeira o número do passaporte e a nacionalidade. Isto poderia ser conseguido tendo-se um campo para cada elemento possível, e apenas acessando os campos adequados, como no exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct DadosPessoais { char ∗nome ; char ∗ e n d e r e c o ; bool e s t r a n g e i r o ; l o n g RG; char ∗ r e s e r v i s t a ; char ∗ p a s s a p o r t e ; char ∗ n a c i o n a l i d a d e ; / ∗ . . . ∗ / / / o u t r o s membros }; /∗ . . . ∗/ { DadosPessoais p ; /∗ . . . ∗/ if (p. estrangeiro ) { p . passaporte = /∗ . . . ∗/ ; p . nacionalidade = /∗ . . . ∗/ ; } else { p . RG = / ∗ . . . ∗ / ; p . r e s e r v i s t a = /∗ . . . ∗/ ; } /∗ . . . ∗/ } Neste caso, sempre ao processar uma variável do tipo DadosPessoais, deveríamos verificar o campo estrangeiro . Se for true, então consideramos os campos passaporte e nacionalidade , e desconsideramos os campos RG e reservista . Se for false , procedemos ao contrário. Esta solução no entanto não é satisfatória, visto que uma variável do tipo DadosPessoais deveria ter espaço para guardar os dois pares de campos, quando apenas um par de cada vez é utilizado. O uso de union permite eliminar este problema: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct IdBrasileiro { l o n g RG; char ∗ r e s e r v i s t a ; }; struct IdEstrangeiro { char ∗ p a s s a p o r t e ; char ∗ n a c i o n a l i d a d e ; }; union I d { I d B r a s i l e i r o bra ; IdEstrangeiro est ; }; struct DadosPessoais { char ∗nome ; char ∗ e n d e r e c o ; bool e s t r a n g e i r o ; Id id ; /∗ . . . ∗/ }; /∗ . . . ∗/ U NIVERSIDADE DE S ÃO PAULO 5.3 Uniões 21 { DadosPessoais p ; /∗ . . . ∗/ if (p. estrangeiro ) { p . id . est . passaporte = /∗ . . . ∗/ ; p . id . est . nacionalidade = /∗ . . . ∗/ ; } else { p . i d . b r a . RG = / ∗ . . . ∗ / ; p . id . bra . r e s e r v i s t a = /∗ . . . ∗/ ; } /∗ . . . ∗/ 22 23 24 25 26 27 28 29 30 31 32 49 } Devido ao uso de union, será ocupado aqui apenas o espaço necessário para o armazenamento da maior das estruturas IdBrasileiro ou IdEstrangeiro . O código acima pode ser um pouco simplificado se nos utilizarmos de uniões anônimas. A linguagem C++ permite que, dentro de uma struct , uma union seja definida sem o correspondente nome de membro. Os membros da union serão então diretamente acessados como se fossem membros da struct : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 struct IdBrasileiro { l o n g RG; char ∗ r e s e r v i s t a ; }; struct IdEstrangeiro { char ∗ p a s s a p o r t e ; char ∗ n a c i o n a l i d a d e ; }; struct DadosPessoais { char ∗nome ; char ∗ e n d e r e c o ; bool e s t r a n g e i r o ; union { struct I d B r a s i l e i r o bra ; struct IdEstrangeiro est ; }; /∗ . . . ∗/ }; /∗ . . . ∗/ { DadosPessoais p ; /∗ . . . ∗/ if (p. estrangeiro ) { p . est . passaporte = /∗ . . . ∗/ ; p . est . nacionalidade = /∗ . . . ∗/ ; } else { p . b r a . RG = / ∗ . . . ∗ / ; p . bra . r e s e r v i s t a = /∗ . . . ∗/ ; } /∗ . . . ∗/ } Na verdade o código acima foi apresentado apenas como exemplo. Veremos nos capítulos seguintes que a melhor forma de lidar com o problema específico do exemplo acima é por meio do uso de herança (capítulo 9). Uma união pode ser inicializada de forma similar a uma struct , mas apenas é possível especificar o valor do primeiro membro alternativo: 1 2 3 4 5 6 union e s c o l h a { int n ; char ∗ s ; }; /∗ . . . ∗/ e s c o l h a x = { 12 } ; / / I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS i n i c i a l i z a x . n = 12 50 Estruturas de dados 5.4 Campos de bits Campos de bits permitem definir variáveis (em geral membros de uma estrutura) que necessitam para sua representação um número específico de bits. A definição é como segue: 1 t i p o _ i n t e g r a l nome : e x p r e s s a o _ c o n s t a n t e ; O tipo integral especificado indica os valores que serão atribuídos ao campo (em geral utiliza-se int ou unsigned int); a expressão constante fornece o valor que indica o número de bits necessários para representar os valores. Campos de bits são em geral utilizados para compactar diversos números dentro de uma palavra (ou uma seqüência de palavras) de forma a economizar espaço ou a se adaptar a algum formato externo. Em muitas implementações cada campo de bits deve poder ser contido em múltiplos do tamanho de palavra, e não é permitido começar um campo de bits em uma palavra e terminar em outra; devido a isso, sob certas circunstâncias não haverá economia de espaço com o uso de campos de bits. 1 2 3 4 5 struct pixel { unsigned i n t vermelho : 8 ; unsigned i n t verde : 8; unsigned i n t a z u l : 8; }; 5.5 Definição de tipos É possível também definir-se novos tipos como um outro nome para tipos já existentes. Para isso se utiliza a palavra reservada typedef: 1 typedef t i p o _ a n t i g o tipo_novo ; Alguns exemplos: 1 2 3 4 typedef typedef typedef typedef i n t Codigo ; double coord ; char ∗ S t r i n g ; double ∗ Vetor ; // // // // Codigo e q u i v a l e a i n t coord e q u i v a l e a double S t r i n g e q u i v a l e a p o n t e i r o para char Vetor e q u i v a l e a p o n t e i r o para double Exercícios 1. Em que situações devem ser utilizadas estruturas ( struct ), enumerações (enum) e uniões (union)? 2. De que forma o uso de tipos de dados estruturados influencia as características de engenharia de software de um programa? 3. O que são campos de bits? Em que situações o uso de campos de bits resulta em real economia de espaço? Em que situações isso não ocorre? 4. Escreva um programa que leia diversas cadeias de caracteres e as insira em ordem alfabética em uma lista ligada. As cadeias serão de no máximo 100 caracteres, mas o número total de cadeias não é conhecido inicialmente. Após ler todas as cadeias, o programa deve mostrar a lista ordenada na tela. 5. Escreva um programa que, dado um número de ponto flutuante na entrada, interpretado como double, imprima a representação binária interna desse número. U NIVERSIDADE DE S ÃO PAULO Capítulo 6 Compilação separada No desenvolvimento de um programa complexo é freqüente surgirem as seguintes situações: • É necessário aproveitar-se funções previamente desenvolvidas para outros programas (ver discussão pág. 23). • A complexidade é tal que pode ser melhor atacada se o problema for dividido em sub-tarefas, cada uma cuidando de um dos aspectos do problema, e consistindo em um ou mais tipos de dados, variáveis e funções. • O programa deve ser desenvolvido por mais de um programador. Alguns destes pontos já foram tocados em exposição anterior. Aqui queremos apenas salientar que estes três pontos indicam a necessidade de compilação separada. Denominamos compilação separada ao fato de partes de um programa serem compiladas independentemente de outras. Neste capítulo trataremos de aspectos de linguagem relativos à compilação separada. 6.1 O processo de compilação separada A compilação separada é possível através da separação do processo de geração do código executável à partir do programa fonte em duas partes. Na primeira parte, chamada compilação, o código na linguagem de alto nível é traduzido para código de máquina, e o resultado armazenado num arquivo não executável chamado arquivo objeto. Após isto, diversos arquivos objetos, isolados ou retirados de coleções de arquivos objetos denominadas bibliotecas, são ligados entre si no processo de ligação. A ligação tem a função de ligar um elemento referenciado em um arquivo objeto (por exemplo, uma função chamada) com a sua definição em outro arquivo objeto. Assim, todos os arquivos do programa são compilados separadamente gerando arquivos objeto. Estes diversos arquivos são então ligados entre si, e possivelmente também com algumas bibliotecas de função de utilidade mais ampla (como funções matemáticas, de acesso a arquivos, de manipulação de cadeias de caracteres, etc) para gerar o código executável final. 6.2 Arquivos de cabeçalho Conforme dito anteriormente, nenhum identificador pode ser utilizado em C++ se não houver sido previamente declarado. Quando lidamos com compilação separada, temos o problema de que identificadores (por exemplo, funções) serão definidos em um arquivo e utilizados em um arquivo diferente. No arquivo que vai fazer uso desse identificador, ele deve ser declarado, e essa declaração deve sempre ser compatível com a definição no outro arquivo. Isto pode ser conseguido através do uso de um arquivo de cabeçalho. Para cada arquivo que define identificadores que serão úteis em outros arquivos, devemos ter um arquivo de cabeçalho correspondente, que fará a declaração dos mesmos identificadores. Por exemplo, um arquivo inclui definição de funções. Um arquivo de cabeçalho correspondente irá conter os protótipos dessas funções. Novos tipos de dados são definidos no cabeçalho, de forma que sua definição fica acessível aos outros arquivos. Este arquivo de cabeçalho será então incluído (com o uso de #include, capítulo 15) em todos os arquivos que fizerem uso dos identificadores, e também no arquivo que define estes identificadores. A inclusão no próprio arquivo de definição, além de necessária quando existem definições de novos tipos de dados, garante a consistência entre os protótipos e as definições das funções, pois se uma função for definida de forma incompatível com um protótipo o compilador acusará erro. Vejamos um exemplo. Suponhamos que como parte de um programa foi desenvolvido um tipo de dados de pilha com funções para seu acesso. Escreveremos então dois arquivos, um arquivo de cabeçalho ( pilha . h) e um arquivo de I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 52 Compilação separada implementação ( pilha . cc). No arquivo de cabeçalho incluímos a definição do tipo e os protótipos das funções de acesso. No arquivo de implementação incluímos as definições das funções de acesso. Por exemplo, teríamos em pilha . h: 1 2 # i f n d e f _PILHA_H_ # d e f i n e _PILHA_H_ 3 4 5 6 7 struct ListaLigada { int valor ; L i s t a L i g a d a ∗ proximo ; }; 8 9 typedef ListaLigada ∗ Pilha ; 10 11 12 13 14 void void int bool Ini Poe Retira Vazia ( P i l h a &p ) ; ( i n t v a l , P i l h a &p ) ; ( P i l h a &p ) ; ( Pilha p ); 15 16 # e n d i f _PILHA_H_ e o arquivo de implementação pilha . cc: 1 2 3 # include < cstdlib > # include <iostream > # include " pilha . h" 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 / / I n i c i a l i z a uma p i l h a v a z i a v o i d I n i ( P i l h a &p i l h a ) { pilha = 0; } / / I n s e r e e l e m e n t o de v a l o r v a l na p i l h a v o i d Poe ( i n t v a l , P i l h a &p i l h a ) { L i s t a L i g a d a ∗ novo = new L i s t a L i g a d a ; novo−> v a l o r = v a l ; novo−>p r o x i m o = p i l h a ; p i l h a = novo ; } / / E x t r a i e l e m e n t o do t o p o da p i l h a i n t R e t i r a ( P i l h a &p i l h a ) { int val ; i f ( p i l h a == 0 ) { s t d : : c o u t << " T e n t a t i v a de r e t i r a d a de p i l h a v a z i a ! " << s t d : : e n d l ; exit (1); } v a l = p i l h a −> v a l o r ; P i l h a tmp = p i l h a ; p i l h a = p i l h a −>p r o x i m o ; d e l e t e tmp ; return val ; } / / V e r i f i c a se a pilha esta vazia bool Vazia ( P i l h a p i l h a ) { r e t u r n p i l h a == 0 ; } O efeito das diretivas de pré-processador #ifndef, #define e #endif (ver capítulo 15) em pilha . h é o de impedir que as declarações sejam incluídas mais do que uma vez em um mesmo arquivo. Isto pode ocorrer em programas com múltiplos arquivos, onde as interdependências entre os diversos arquivos são complexa. Como a múltipla definição (de tipos por exemplo) não é permitida em C++, a inclusão múltipla de pilha . h geraria um erro de compilação. Um exemplo de inclusão múltipla não intencional seria: um arquivo usa pilhas para implementar alguma nova funcionalidade (digamos U NIVERSIDADE DE S ÃO PAULO 6.3 Variáveis externas 53 armazenamento de identificações de clientes a serem atendidos); o cabeçalho desse arquivo deve então incluir pilha . h. Se este arquivo for incluído por um outro, que também faz uso de pilhas, então haverá multipla inclusão. No caso de múltipla inclusão, as diretivas acima asseguram que não ocorre múltipla definição. O #ifndef somente permite que sejam processadas as linhas entre ele e o correspondente #endif se o símbolo dado a seguir não houver ainda sido definido. Da primeira vez que pilha . h é incluído, o símbolo ainda não é definido, mas então a próxima linha (o #define) faz com que ele passe a ser definido, e todas as outras inclusões irão ignorar todas as linhas até o #endif. 6.3 Variáveis externas Algumas vezes necessitamos de variáveis globais, que deverão ser acessadas por funções em diferentes arquivos (funções que serão compiladas separadamente). Em um desses arquivos, a variável pode ser definida normalmente como uma variável global. Nos outros arquivos, ela não pode ser definida novamente, pois haveria múltipla definição, proibida pela linguagem, mas também não pode ser utilizada sem haver sido declarada. A solução é declarar a variável, ao mesmo tempo indicando ao compilador que ela foi definida em algum outro arquivo, isto é, que ela é uma variável externa. A ligação entre os usos dessa variável e seu espaço de armazenamento é decidida no momento da ligação dos diversos arquivos. A sintaxe para declarar uma variável como externa é similar à declaração de uma variável, mas colocando-se a palavra reservada extern antes do tipo: 1 2 extern i n t N; / / N e do t i p o i n t , d e f i n i d a e x t e r n a m e n t e e x t e r n char ∗ nomes ; / / nomes e p o n t e i r o p a r a char , e x t e r n a Exercícios 1. O que se entende por compilação separada? Quais as vantagens de sua utilização? 2. O que são variáveis externas. Quando elas são necessárias? 3. Refaça o exercício de manipulação de cadeias do capítulo 5 de forma a que a leitura e a impressão de cadeias fiquem em um arquivo, enquanto a parte de inserção das cadeias numa lista ordenada fique em outro arquivo. 4. Refaça o programa de manipulação de matrizes do capítulo 4 de forma que seja composto de três arquivos: um com a rotina de soma de matrizes, outro com a rotina de inicialização de valores para os elementos das matrizes, e outra com o programa principal e rotinas de entrada e saída de dados. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 54 Compilação separada U NIVERSIDADE DE S ÃO PAULO Capítulo 7 Classes Vimos como organizar de forma estruturada o controle do fluxo de execução do programa através do uso de decomposição em funções e das diversas estruturas de controle (capítulos 2 e 3). Vimos também como estruturar os dados necessários ao processamento (capítulo 5). Por fim, vimos como esses elementos podem ser organizados em um conjunto de arquivos de cabeçalho e implementação (capítulo 6). Do ponto de vista de engenharia de software, no entanto, os elementos apresentados não são ainda suficientes para uma resolução a contento do problema de implementar novos tipos de dados e as operações sobre eles. A limitação do programa a definir estruturas e de funções para lidar com essas estruturas, técnicas apresentadas até o momento, apresenta diversos inconvenientes: 1. Não existe uma conexão sintática entre os tipos de dados definidos e as funções que os manipulam. Esta conexão deve ser apresentada externamente ao código do programa (por exemplo, por comentários) ou deve ser inferida de uma análise do significado e objetivos do código. 2. A forma como o novo tipo de dados é internamente representado é externamente acessível, isto é, acessível às funções que usam esse tipo de dados (que chamaremos de clientes desse tipo). Isto pode levar a dependências dos clientes em relação aos detalhes da implementação, o que dificulta o desenvolvimento de novas versões do tipo, pois códigos desenvolvidos para a versão anterior podem facilmente deixar de ser válidos. Certamente o implementador do tipo não pode garantir que essas dependências não existam. 3. Nada impede que os códigos clientes façam alteração nas variáveis do tipo sem se utilizarem das funções de acesso fornecidas. Isto faz com que a depuração do uso do tipo fique extremamente complexa, pois se uma inconsistência dentro de um elemento do tipo for encontrada, essa inconsistência pode tanto haver sido incluída por uma das funções de acesso como pelo próprio código cliente. 4. Não existe forma de especificar o que pode ser acessível aos clientes e o que deve ser apenas utilizado pelas funções próprias do tipo, o que faz com que simplesmente todos os membros e funções de acesso do tipo devam ser considerados como fazendo parte da interface1 do tipo. 5. As variáveis de um novo tipo definido desta forma têm necessariamente um tratamento diferente das variáveis de tipos pré-definidos pela linguagem. Por exemplo, não existem conversões para outros tipos e não é permitido o uso de operadores da linguagem sobre variáveis desse tipo. Esses problemas podem ser tratados através da utilização de conceitos de tipos abstratos de dados, que em C++ são implementados pelo uso de classes. Tipos abstratos de dados definem um novo tipo de dados pela especificação não só da sua estrutura de dados como também das operações que serão efetuadas sobre esses dados. Eles permitem também especificar quais elementos serão acessíveis ou não aos clientes. 7.1 Classes Uma classe especifica um novo tipo de dados, sua estrutura, as operações que podem ser efetuadas sobre ele, e controla o acesso aos diversos elementos do tipo. A estrutura do novo tipo é definida, como no caso de estruturas de dados simples, através da especificação dos membros componentes e seus tipos. As operações associadas ao tipo são indicadas através 1 Denominamos de interface os elementos de um componente de software que são acessíveis externamente (acessíveis aos clientes), em contraposição à implementação que indica a estruturação interna desses componentes. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 56 Classes de protótipos apresentados juntamente com a declaração do tipo. O controle de acesso é feito através da especificação dos membros como públicos (acessíveis aos clientes) ou privados (não acessíveis aos clientes). Suponha que desejamos definir um tipo para lidar com números complexos. (Na verdade a linguagem já dispõe em sua biblioteca básica de um template para lidar com números complexos.) Devemos então definir a forma como números complexos serão armazenados e como o acesso a eles poderá ser realizado. Um exemplo possível: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 c l a s s Complexo { d o u b l e r e , im ; public : void v a l e ( c o n s t d o u b l e r e = 0 , c o n s t d o u b l e im = 0 ) ; void v a l e ( c o n s t Complexo &c ) ; double real (); double imag ( ) ; double mod ( ) ; double fase ( ) ; Complexo m a i s ( c o n s t Complexo a ) ; Complexo menos ( c o n s t Complexo a ) ; Complexo v e z e s ( c o n s t Complexo a ) ; Complexo s o b r e ( c o n s t Complexo a ) ; }; Este código define um novo tipo, denominado Complexo. O tipo é caracterizado pelos membros privados re e im (que serão utilizados para armazenar as partes real e imaginária) e pelas funções vale (sobrecarregada), real , imag, mod, fase, mais, menos, vezes e sobre. As funções definidas como fazendo parte da classe são denominadas funções-membro ou métodos da classe. Os membros que aparecem na classe são considerados privados. Os membros públicos são apenas aqueles indicados após o rótulo public :, no exemplo apenas os métodos. Isto significa que variáveis do tipo Complexo somente poderão ser acessadas através dos métodos da classe. Este é o esquema tradicional de definição de classes: os membros tipo dados são declarados privados, e os métodos são declarados públicos. Os membros da classe que são declarados públicos constituem a chamada interface da classe. A definição da interface de uma classe é um fator importante de projeto, e deve ser cuidadosamente estudada. Os elementos a serem levados em consideração no projeto da interface de uma classe serão apresentados no decorrer da exposição. Os diversos métodos públicos da classe Complexo têm os seguintes usos: os métodos vale alteram o valor de um complexo, a partir de outro complexo ou dos valores das partes real e imaginária; os métodos real e imag retornam parte real e imaginária; mod e fase retornam módulo e fase; mais, menos, vezes e sobre retornam o complexo resultante da soma, subtração, multiplicação ou divisão, respectivamente, do complexo com um outro complexo passado como parâmetro para os métodos. Seu uso seria como abaixo: 1 2 3 4 5 6 Complexo a , b , c , d ; a . vale ( ) ; // a = 0 b . vale (3 ,2); / / b = 3+2 i c . v a l e ( a . menos ( b ) ) ; / / c = a − b ; d . vale ( c . sobre ( b ) ) ; // d = c/b; / / a . re = 0; I s t o e um e r r o ! Neste ponto, a tentativa de acesso de um membro privado, neste caso re ou im, seria considerada um erro pois esses membros são privados, daí a última linha acima estar comentada. Variáveis declaradas como do tipo de uma classe são chamadas de objetos da classe, ou instâncias da classe. A chamada de um método é denominada uma mensagem. Assim por exemplo a . vale () é descrito como o envio da mensagem vale (double,double) ao objeto a. A declaração da classe apresenta apenas os protótipos dos métodos. A implementação desses métodos deve ser fornecida para que eles possam ser utilizados. Como classes distintas podem necessitar implementar métodos de mesmo nome e mesmo tipo de parâmetros, é necessário, ao fornecer a implementação de um método, indicar a que classe ele pertence. Isto é feito como no exemplo abaixo para a classe Complexo (obs: as implementações abaixo não são adequadas para uma biblioteca de números complexos a ser usada na resolução de problemas numéricos, são apenas implementações simples para efeito de demonstração dos conceitos): 1 # i n c l u d e <cmath > 2 3 4 5 6 7 8 9 void { re } void { re Complexo : : v a l e ( c o n s t d o u b l e r , c o n s t d o u b l e i ) = r ; im = i ; Complexo : : v a l e ( c o n s t Complexo &c ) = c . r e ; im = c . im ; U NIVERSIDADE DE S ÃO PAULO 7.1 Classes 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 } d o u b l e Complexo : : r e a l ( ) { return re ; } d o u b l e Complexo : : imag ( ) { r e t u r n im ; } d o u b l e Complexo : : mod ( ) { r e t u r n s q r t ( r e ∗ r e +im∗im ) ; } d o u b l e Complexo : : f a s e ( ) { r e t u r n a t a n 2 ( im , r e ) ; } Complexo Complexo : : m a i s ( c o n s t Complexo a ) { Complexo r ; r . re = re + a . re ; r . im = im + a . im ; return r ; } Complexo Complexo : : menos ( c o n s t Complexo a ) { Complexo r ; r . re = re − a . re ; r . im = im − a . im ; return r ; } Complexo Complexo : : v e z e s ( c o n s t Complexo a ) { Complexo r ; r . r e = r e ∗ a . r e − im∗ a . im ; r . im = im∗ a . r e + r e ∗ a . im ; return r ; } Complexo Complexo : : s o b r e ( c o n s t Complexo a ) { / / Cuidado : p o s s i v e i s p r o b l e m a s n u m e r i c o s ! d o u b l e s = a . r e ∗ a . r e +a . im∗ a . im ; Complexo r ; r . r e = ( r e ∗ a . r e +im∗ a . im ) / s ; r . im = ( im∗ a . r e −r e ∗ a . im ) / s ; return r ; } Com relação a esta implementação gostaríamos de apontar para os seguintes fatos: • Note como antes do nome dos métodos é acrescentado o nome da classe, separado pelo operador de escopo :: . Isto caracteriza a função que vai ser definida como um método da classe. • Os métodos, por pertencerem à classe, podem acessar os membros privados re e im, tanto do próprio objeto como dos objetos da mesma classe passados como parâmetros. • Na expressão b. vale (3,2) , b será o objeto sob o qual o método vale (double,double) será chamado. Este objeto é considerado implícito durante a execução do método, e qualquer referência a um membro da classe sem indicação de objeto será interpretada como uma referência ao correspondente membro do objeto implícito. Por exemplo a expressão re = r do método vale (double,double) atribuirá, quando executada para a chamada b. vale (3,2) , o valor 3 ao membro re de b. Desta forma os membros privados de uma classe são acessados por intermédio dos métodos públicos. Normalmente a definição de uma classe é incluída num arquivo cabeçalho, enquanto que a implementação dos métodos é feita num arquivo a ser compilado separadamente (o arquivo de implementação). I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 58 Classes A vantagem do uso de tipos abstratos de dados pode ser demonstrada no caso da classe Complexo de forma simples. Suponhamos que, por qualquer razão, nos decidamos a alterar a representação de números complexos, passando da representação real-imaginário para a representação módulo-fase. Neste caso, como a representação está codificada nos membros re e im, que são privados e somente podem ser acessados pelos métodos da classe, a alteração não terá nenhum efeito negativo sobre os códigos que usam a classe Complexo, desde que a interface da classe (seus membros públicos) seja mantida: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 c l a s s Complexo { d o u b l e m, p ; public : void v a l e ( c o n s t d o u b l e r e = 0 , c o n s t d o u b l e im = 0 ) ; void v a l e ( c o n s t Complexo &c ) ; double real (); double imag ( ) ; double mod ( ) ; double fase ( ) ; Complexo m a i s ( c o n s t Complexo a ) ; Complexo menos ( c o n s t Complexo a ) ; Complexo v e z e s ( c o n s t Complexo a ) ; Complexo s o b r e ( c o n s t Complexo a ) ; }; 15 16 # i n c l u d e <cmath > 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 v o i d Complexo : : v a l e ( c o n s t d o u b l e r , c o n s t d o u b l e i ) { m = sqrt ( r∗r+i ∗ i ); i f ( r != 0 | | i != 0) p = a t a n 2 ( i , r ) ; else p = 0; } v o i d Complexo : : v a l e ( c o n s t Complexo &c ) { m = c .m; p = c . p ; } d o u b l e Complexo : : r e a l ( ) { r e t u r n m∗ c o s ( p ) ; } d o u b l e Complexo : : imag ( ) { r e t u r n m∗ s i n ( p ) ; } d o u b l e Complexo : : mod ( ) { r e t u r n m; } d o u b l e Complexo : : f a s e ( ) { return p ; } Complexo Complexo : : m a i s ( c o n s t Complexo a ) { Complexo r ; r .m = s q r t (m∗m+a .m∗ a .m+2∗m∗ a .m∗ c o s ( p−a . p ) ) ; r . p = a t a n 2 (m∗ s i n ( p ) + a .m∗ s i n ( a . p ) ,m∗ c o s ( p ) + a .m∗ c o s ( a . p ) ) ; return r ; } Complexo Complexo : : menos ( c o n s t Complexo a ) { Complexo r ; r .m = s q r t (m∗m+a .m∗ a . m−2∗m∗ a .m∗ c o s ( p−a . p ) ) ; r . p = a t a n 2 (m∗ s i n ( p)−a .m∗ s i n ( a . p ) ,m∗ c o s ( p)−a .m∗ c o s ( a . p ) ) ; return r ; } U NIVERSIDADE DE S ÃO PAULO 7.1 Classes 58 59 60 61 62 63 64 65 66 67 68 69 70 71 59 Complexo Complexo : : v e z e s ( c o n s t Complexo a ) { Complexo r ; r .m = m∗ a .m; r . p = p+a . p ; return r ; } Complexo Complexo : : s o b r e ( c o n s t Complexo a ) { Complexo r ; r .m = m/ a .m; r . p = p−a . p ; return r ; } Como todos os programas que faziam uso da classe Complexo somente podiam acessar os métodos públicos, cuja interface não foi alterada, eles continuarão funcionando com a nova versão da classe. O mesmo não poderia ser dito se os membro re e im fossem públicos, pois então algum programa poderia ser dependente de sua existência, o que não mais ocorre na nova versão da classe. Dizemos que os membros privados e outros detalhes de implementação da classe estão encapsulados pelos membros públicos. Atingir a encapsulação da implementação por uma interface bem projetada é um dos objetivos principais de tipos abstratos de dados. Abaixo apresentamos como um outro exemplo de classe uma classe para lidar com cadeias de caracteres. (A linguagem já possui em sua biblioteca padrão uma classe para lidar com cadeias de caracter, chamada string .) 1 # include <cstring > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 c l a s s Cadeia { char ∗ s ; public : v o i d v a l e ( c o n s t char ∗ t = 0 ) ; void a l o c a ( const i n t n ) ; C a d e i a c o p i a ( c o n s t i n t n = −1); i n t tamanho ( ) ; char ∗ c o n v e r t e ( c o n s t i n t n = −1); v o i d muda ( c o n s t i n t i n i , c o n s t i n t fim , c o n s t char ∗ t ) ; l o n g b u s c a ( c o n s t char c ) ; C a d e i a j u n t a ( c o n s t C a d e i a &c ) ; Cadeia p a r t e ( const i n t i n i , const i n t fim ) ; i n t compara ( c o n s t C a d e i a &c , c o n s t i n t n = −1); void l i b e r a ( ) ; }; 18 19 20 21 22 23 24 25 26 27 v o i d C a d e i a : : v a l e ( c o n s t char ∗ t ) { delete [] s ; i f ( t != 0) { s = new char [ s t r l e n ( t ) + 1 ] ; strcpy (s , t ); } e l s e i f ( s != 0) s = 0; } 28 29 30 31 32 33 34 void Cadeia : : a l o c a ( const i n t n ) { delete [] s ; i f ( n > 0 ) s = new char [ n + 1 ] ; else s = 0; } 35 36 37 38 39 Cadeia Cadeia : : copia ( const i n t n ) { C a d e i a nova ; i f ( s != 0) { I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 60 i n t l e n = ( n >= 0 ? n : s t r l e n ( s ) ) ; nova . s = new char [ l e n + 1 ] ; s t r n c p y ( nova . s , s , l e n ) ; nova . s [ l e n ] = ’ \ 0 ’ ; 40 41 42 43 } e l s e nova . s = 0 ; r e t u r n nova ; 44 45 46 47 Classes } 48 49 50 51 52 53 i n t C a d e i a : : tamanho ( ) { i f ( s != 0) return s t r l e n ( s ) ; e l s e return 0; } 54 55 56 57 58 59 60 61 62 63 64 65 char ∗ C a d e i a : : c o n v e r t e ( c o n s t i n t n ) { i f ( s != 0) { int len = s t r l e n ( s ) ; i f ( n > 0 && n < l e n ) l e n = n ; char ∗ t = new char [ l e n + 1 ] ; strncpy ( t , s , len ) ; t [ len ] = ’ \0 ’ ; return t ; } e l s e return 0; } 66 67 68 69 70 71 72 73 74 75 76 77 v o i d C a d e i a : : muda ( c o n s t i n t i n i , c o n s t i n t fim , c o n s t char ∗ t ) { i n t n = fim−i n i +1 , s l e n , t l e n ; i f ( s != 0) s l e n = s t r l e n ( s ) ; e l s e s l e n = 0 ; i f ( t != 0) t l e n = s t r l e n ( t ) ; e l s e t l e n = 0 ; i f ( n > 0) { if (n > slen ) n = slen ; if (n > tlen ) n = tlen ; f o r ( i n t i = 0 ; i < n ; i ++) s [ i n i + i ] = t [ i ] ; } } 78 79 80 81 82 83 84 85 86 87 l o n g C a d e i a : : b u s c a ( c o n s t char c ) { i f ( s != 0) { char ∗ t = s ; w h i l e ( ∗ t ! = ’ \ 0 ’ && ∗ t ! = c ) t ++; i f ( ∗ t == c ) r e t u r n ( t −s ) ; } r e t u r n −1; } 88 89 90 91 92 93 94 95 96 97 98 99 100 C a d e i a C a d e i a : : j u n t a ( c o n s t C a d e i a &c ) { C a d e i a nova ; int len = 0; i f ( s ! = 0 ) l e n += s t r l e n ( s ) ; i f ( c . s ! = 0 ) l e n += s t r l e n ( c . s ) ; nova . s = new char [ l e n + 1 ] ; s t r c p y ( nova . s , " " ) ; i f ( s ! = 0 ) s t r c a t ( nova . s , s ) ; i f ( c . s ! = 0 ) s t r c a t ( nova . s , c . s ) ; r e t u r n nova ; } 101 102 Cadeia Cadeia : : p a r t e ( const i n t i n i , const i n t fim ) U NIVERSIDADE DE S ÃO PAULO 7.2 Controle de acesso 103 { C a d e i a nova ; i n t l e n = fim−i n i + 1 ; i f ( l e n >= 0 ) { nova . s = new char [ l e n + 1 ] ; s t r n c p y ( nova . s , s , l e n ) ; nova . s [ l e n ] = ’ \ 0 ’ ; } e l s e nova . s = 0 ; r e t u r n nova ; 104 105 106 107 108 109 110 111 112 113 61 } 114 115 116 117 118 119 i n t C a d e i a : : compara ( c o n s t C a d e i a &c , c o n s t i n t n ) { i f ( n >= 0 ) r e t u r n s t r n c m p ( s , c . s , n ) ; e l s e return strcmp ( s , c . s ) ; } 120 121 122 123 124 125 void Cadeia : : l i b e r a ( ) { delete [] s ; s = 0; } Ambos os exemplos de classes apresentados aqui serão mais elaborados adiante, conforme forem sendo apresentados outros elementos da linguagem ligados a classes. 7.2 Controle de acesso Conforme discutido acima, o uso de tipos abstratos de dados tem dois objetivos. Primeiro, permitir que as estruturas de dados e as funções que as manipulam sejam sintaticamente relacionadas. Isto melhora o desenvolvimento e a manutenção do código, por permitir uma clara identificação de todos os elementos relacionados com a implementação de um tipo. Segundo, permitir que os elementos que fazem parte da interface do tipo sejam cuidadosamente escolhidos. Este último ponto é conseguido através do controle de acesso, que garante a encapsulação. Controle de acesso é realizado através do uso dos rótulos public : e private :. Os membros declarados após um rótulo private são considerados internos da classe, e somente serão acessíveis a outros membros da classe. Os membros declarados após um public, são considerados parte da interface da classe, e poderão ser acessado por qualquer código que faça uso de objetos da classe. Quando não especificado, os membros de uma class são considerados privados, e os membros de uma struct são considerados públicos. Esta é a única diferença existente entre classes e estruturas em C++. Por exemplo, a definição: 1 2 3 4 5 6 class A { int a ; public : void f ( i n t x ) ; int g ( ) ; }; é equivalente a: 1 2 3 4 5 6 7 class A { private : int a ; public : void f ( i n t x ) ; int g ( ) ; }; ou a: 1 2 3 4 class A { public : void f ( i n t x ) ; private : I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 62 5 6 7 8 Classes int a ; public : int g ( ) ; }; ou a: 1 2 3 4 5 6 7 class A { public : void f ( i n t x ) ; int g ( ) ; private : int a ; }; ou a: 1 2 3 4 5 6 7 struct A { public : void f ( i n t x ) ; int g ( ) ; private : int a ; }; ou, por fim, a: 1 2 3 4 5 6 struct A { void f ( i n t x ) ; int g ( ) ; private : int a ; }; Em geral se reserva struct para o uso em tipos de dados não abstratos, e class para tipos abstratos, pois em tipos não abstratos não existem métodos, e portanto os membros devem ser públicos, enquanto que em tipos abstratos os membros tipo dados normalmente devem ser privados e acessados apenas através de métodos públicos. 7.3 Construtores e destruidores Ao usarmos um tipo abstrato de dados onde os elementos de implementação estão encapsulados e somente acessíveis via métodos públicos, podemos garantir que os objetos desse tipo mantenham sua consistência interna se garantirmos que todos os métodos públicos ao atuarem sobre um objetos inicialmente consistente o deixem ao final num estado consistente. Por exemplo, suponha que implementamos uma classe para lidar com uma lista de nomes: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # include <cstring > c o n s t i n t max = 1 0 0 ; c l a s s ListaDeNomes { int n ; char ∗nome [ max ] ; public : int quantos ( ) ; v o i d i n s e r e ( c o n s t char ∗ novo ) ; char ∗ l e ( c o n s t i n t i ) ; }; i n t ListaDeNomes : : q u a n t o s ( ) { return n ; } v o i d ListaDeNomes : : i n s e r e ( c o n s t char ∗ novo ) { i f ( novo ! = 0 && n < max ) { nome [ n ] = new char [ s t r l e n ( novo ) + 1 ] ; s t r c p y ( nome [ n ] , novo ) ; n ++; } U NIVERSIDADE DE S ÃO PAULO 7.3 Construtores e destruidores 22 23 24 25 26 27 28 29 30 63 } char ∗ ListaDeNomes : : l e ( c o n s t i n t i ) { i f ( i >= 0 && i < n ) { char ∗ s = new char [ s t r l e n ( nome [ i ] ) + 1 ] ; s t r c p y ( s , nome [ i ] ) ; return s ; } e l s e return 0; } Sempre que um novo nome é inserido na lista, o número de nomes na lista é incrementado. Isto garante que, uma vez estando a variável que armazena o número de nomes consistente com o número de nomes armazenados na lista, a consistência será sempre mantida. No entanto, não existe nenhuma garantia de que o valor inicial seja correto ao se criar um objeto desse tipo. Isto poderia ser contornado com a introdução de um método de inicialização, a ser chamado pelo usuário antes de utilizar cada lista de nomes. No entanto, uma solução ainda melhor é fornecida na linguagem C++, que permite a especificação de construtores. Construtores são métodos chamados sempre que um objeto da classe é gerado. A função do construtor é justamente inicializar os membros do novo objeto, de forma a que o objeto comece sua existência dentro de um estado internamente consistente. A classe acima pode ser então alterada para incluir um construtor: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # include <cstring > c o n s t i n t max = 1 0 0 ; c l a s s ListaDeNomes { int n ; char ∗nome [ max ] ; public : ListaDeNomes ( ) ; int quantos ( ) ; v o i d i n s e r e ( c o n s t char ∗ novo ) ; char ∗ l e ( c o n s t i n t i ) ; }; ListaDeNomes : : ListaDeNomes ( ) { n = 0; } / ∗ . . . ∗ / / / Os o u t r o s m e t o d o s s a o i d e n t i c o s O construtor é um método com o mesmo nome da classe e sem especificação de tipo de retorno. O construtor pode aceitar qualquer número e tipo de parâmetros; esses parâmetros serão utilizados para a inicialização do objeto que está sendo construído. Também é possível, por meio de sobrecarga de nome de função (seção 3.9), ter-se mais de um construtor para uma mesma classe, cada qual aceitando parâmetros diferentes. Se um construtor é declarado sem parâmetros, ele é dito o construtor assumido ou default e é utilizado em diversas situações onde, ao ser criado um objeto, não é possível passar parâmetros para o mesmo (por exemplo, ao criar arrays de objetos). Assim como temos construtores, que se encarregam de inicializar um objeto da classe em um estado consistente, temos também destruidores, que têm a finalidade de realizar quaisquer operações necessárias em um objeto antes que ele seja abandonado. Um uso típico de destruidores é para liberar recursos, como por exemplo memória dinâmica, utilizados pelos membros da classe. Podemos por exemplo definir um destruidor para a classe ListaDeNomes: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # include <cstring > c o n s t i n t max = 1 0 0 ; c l a s s ListaDeNomes { int n ; char ∗nome [ max ] ; public : ListaDeNomes ( ) ; int quantos ( ) ; v o i d i n s e r e ( c o n s t char ∗ novo ) ; char ∗ l e ( c o n s t i n t i ) ; ~ ListaDeNomes ( ) ; }; / ∗ . . . ∗ / / / Os o u t r o s m e t o d o s s a o i d e n t i c o s ListaDeNomes : : ~ ListaDeNomes ( ) { f o r ( i n t i = 0 ; i < n ; i ++) d e l e t e [ ] nome [ i ] ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 64 18 Classes } Note que o destruidor tem o mesmo nome da classe, precedido por um ~, não tem especificação de tipo de retorno e não deve aceitar nenhum parâmetro. Ocorre a chamada de um construtor : • No ponto do programa onde um objeto da classe é declarado. • Onde exista a necessidade de um objeto temporário do tipo da classe. Por exemplo, para armazenar um valor retornado por uma função e que será passado para outra. • Onde um objeto temporário da classe seja necessário para conversão (ver seção 7.6). • Ao se executado um new para gerar objeto da classe. Um destruidor é chamado: • Onde um objeto da classe sair do escopo (ver capítulo 8). • Quando um objeto temporário da classe não for mais necessário. • Ao ser executado um delete sobre um objeto da classe. Um tipo especial de construtor é o chamado construtor de cópia. Este construtor é chamado sempre que uma nova cópia de um objeto da classe é necessária, por exemplo, quando um objeto da classe é passado por valor para uma função. A sintaxe do construtor de cópia para uma classe A é A(A&), isto é, ele recebe como parâmetro uma referência a um objeto da classe. O fato de receber uma referência ao objeto é essencial, visto que se o objeto a ser copiado fosse passado por valor uma cópia desse objeto seria necessária. Como o parâmetro será utilizado apenas para copiar seus dados, nenhuma alteração no objeto passado como argumento será realizada em geral, e o parâmetro pode ser declarado constante, o que habilita o uso do construtor de cópia para copiar objetos constantes. Por exemplo, a classe Complexo poderia dispor de um construtor que recebe os valores de parte imaginária e parte real e um construtor de cópia: 1 2 3 4 5 6 7 c l a s s Complexo { / ∗ . . . ∗ / / / Demais d e c l a r a c o e s public : Complexo ( c o n s t d o u b l e r = 0 , c o n s t d o u b l e i = 0 ) ; Complexo ( c o n s t Complexo &c ) ; / ∗ . . . ∗ / / / Demais d e c l a r a c o e s }; Quando uma variável é criada pelo método normal utilizado para os tipos pré-definidos: 1 Complexo c ; então um construtor sem parâmetros, ou um construtor para o qual todos os parâmetros tenham valores assumidos é chamado para a inicialização do novo objeto, caso eles existam. Se desejamos que algum outro construtor seja chamado ao inicializar um objeto, podemos passar os parâmetros adequados para o construtor como no exemplo abaixo: 1 2 3 Complexo z ( 2 , 3 ) ; / / c r i a z = 2+3∗ i , Complexo ( d o u b l e , d o u b l e ) Complexo t ( z ) ; / / c r i a t c o p i a de z , Complexo ( Complexo &) Complexo ∗ c = new Complexo ( 1 , 4 ) / / ∗ c e o c o m p l e x o 1+4 i Se criamos um array de objetos, então não é possível passar parâmetros para o construtor de cada objeto, e eles serão inicializados com o construtor sem parâmetros ou com todos os parâmetros assumidos. 7.4 Inicialização de membros Construtores permitem o estabelecimento de valores iniciais para os membros de um objeto. No entanto, com o apresentado até o momento, não é possível realizar isto para todos os tipos de membros. Como já explicado, tanto constantes como referências não podem ser atribuídas, mas apenas inicializadas. Isto significa que se um membro for constante ou referência ele somente pode ter seu valor ajustado se houver algum tipo de sintaxe que possibilite sua inicialização. Por exemplo, o código abaixo é errôneo: U NIVERSIDADE DE S ÃO PAULO 7.4 Inicialização de membros 65 class A { const int x ; 3 float y; 4 public : 5 A( c o n s t i n t a , c o n s t f l o a t b ) ; 6 }; 7 A : : A( c o n s t int a , const f l o a t b ) 8 { 9 x = a ; / / E r r a d o ! Uma c o n s t a n t e nao a c e i t a a t r i b u i c a o ! 10 y = b; 11 } 1 2 Membros podem ser inicializados através da chamada lista de inicialização de membros. Uma lista de inicialização de membros é apresentada juntamente com a definição de um construtor, entre o caracter ) que termina a lista de parâmetros do construtor e o caracter { que abre o corpo do construtor, separada da lista de parâmetro por um caracter : e consistindo nos nomes dos membros e seus valores entre parênteses, sendo os inicializadores de cada membro separados por vírgulas, como nos exemplos abaixo: class A { const int x ; 3 float y; 4 public : 5 A( c o n s t i n t a , c o n s t f l o a t b ) ; 6 }; 7 A : : A( c o n s t i n t a , c o n s t f l o a t b ) : x ( a ) / / x e i n i c i a l i z a d o com a 8 { 9 y = b; 10 } 1 2 Inicializadores de membros são geralmente utilizados para constantes e referências, onde as semânticas de inicialização e de atribuição são diferentes, mas nada impede que eles sejam utilizados para outros tipos de membros: class A { const int x ; 3 float y; 4 public : 5 A( c o n s t i n t a , c o n s t f l o a t b ) ; 6 }; 7 A : : A( c o n s t i n t a , c o n s t f l o a t b ) : x ( a ) , y ( b ) {} 1 2 Como um exemplo prático, citamos a classe ListaDeNomes definida acima (pág. 63). Na versão ali apresentada, a constante max é definida exteriormente à classe. No entanto, tecnicamente falando esta constante é parte da classe, e deveria ser definida internamente à mesma. Anteriormente não havíamos realizado isso por não havermos apresentado a sintaxe de inicialização. Agora podemos redefinir a classe da seguinte forma: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # include <cstring > c l a s s ListaDeNomes { c o n s t i n t max ; int n ; char ∗∗nome ; public : ListaDeNomes ( i n t m = 1 0 0 ) ; int quantos ( ) ; v o i d i n s e r e ( c o n s t char ∗ novo ) ; char ∗ l e ( c o n s t i n t i ) ; ~ ListaDeNomes ( ) ; }; ListaDeNomes : : ListaDeNomes ( i n t m) : max (m) { n = 0; nome = new char ∗ [ max ] ; } i n t ListaDeNomes : : q u a n t o s ( ) { return n ; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 66 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 Classes v o i d ListaDeNomes : : i n s e r e ( c o n s t char ∗ novo ) { i f ( novo ! = 0 && n < max ) { nome [ n ] = new char [ s t r l e n ( novo ) + 1 ] ; s t r c p y ( nome [ n ] , novo ) ; n ++; } } char ∗ ListaDeNomes : : l e ( c o n s t i n t i ) { i f ( i >= 0 && i < n ) { char ∗ s = new char [ s t r l e n ( nome [ i ] ) + 1 ] ; s t r c p y ( s , nome [ i ] ) ; return s ; } e l s e return 0; } ListaDeNomes : : ~ ListaDeNomes ( ) { f o r ( i n t i = 0 ; i < n ; i ++) d e l e t e [ ] nome [ i ] ; } Uma vantagem adicional desta definição é que agora ao definirmos um objeto da classe podemos escolher o número máximo de nomes que a lista pode conter, ao invés de usar como antes o número fixo em 100. Note como o uso de um valor assumido de 100 para o tamanho do array no construtor garante que códigos que usavam a versão anterior continuem funcionando sem alteração. 7.5 Métodos in-line Vimos na seção 3.4 que funções podem ser declaradas in-line. O mesmo pode ser feito com funções membro (métodos). Para declarar um método in-line, devemos apresentar sua definição juntamente com a declaração da classe, ao invés de apresentar apenas um protótipo. Por exemplo o construtor e o método quantos da classe ListaDeNomes podem ser tornados in-line alterando a declaração da classe da seguinte forma: 1 2 3 4 5 6 7 8 9 10 11 c l a s s ListaDeNomes { c o n s t i n t max ; int n ; char ∗nome [ max ] ; public : ListaDeNomes ( i n t m = 0 ) : max (m) { n = 0 ; nome = new char ∗ [ max ] ; } i n t quantos ( ) { return n ; } v o i d i n s e r e ( c o n s t char ∗ novo ) ; char ∗ l e ( c o n s t i n t i ) ; ~ ListaDeNomes ( ) ; }; Obviamente a declaração dos métodos in-line já é uma definição, e eles não devem ser posteriormente redefinidos. 7.6 Conversões Vimos que os tipos básicos permitem conversões entre si. Para os tipos definidos pelo usuário, é também possível definir conversões. As conversões são tanto de objetos de outros tipos para objetos da classe como de objetos da classe para outros tipos. 7.6.1 Conversões de outros tipos para a classe Um construtor constrói um objeto da classe a partir de informações provenientes de objetos possivelmente de outra classe, passados como parâmetros do construtor. Ele pode portanto ser encarado como um operador de conversão dos objetos de outros tipos para o objeto da classe. Assim, para definir uma conversão de um certo tipo para a classe, basta definir um construtor que aceita como parâmetro um objeto do tipo a ser convertido. Por exemplo, para nossa classe Cadeia, U NIVERSIDADE DE S ÃO PAULO 7.6 Conversões 67 queremos definir uma forma de conversão de char∗ para Cadeia. Para isto, definimos um construtor que aceita um char∗ como parâmetro: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 c l a s s Cadeia { char ∗ s ; public : /∗ . . . ∗/ / / Outros metodos C a d e i a ( c o n s t char ∗ t = 0 ) ; /∗ . . . ∗/ / / Outros metodos }; /∗ . . . ∗/ / / Outras d e f i n i c o e s C a d e i a : : C a d e i a ( c o n s t char ∗ t ) { i f ( t != 0) { s = new char [ s t r l e n ( t ) + 1 ] ; strcpy (s , t ); } else { s = 0; } } /∗ . . . ∗/ / / Outras d e f i n i c o e s Note que o código é similar ao método Cadeia :: vale (const char ∗) (pág. 59), com a diferença de que não é necessário testar se a cadeia já havia sido anteriormente inicializada, visto que isto é um construtor, que somente é chamado para objetos novos. Para realizar a conversão, podemos utilizar o construtor como se fosse uma chamada de função: 1 2 3 4 Cadeia a , b ; char ∗ s = " O u t r a c o i s a " ; a = C a d e i a ( " Alguma c o i s a " ) ; b = Cadeia ( s ) ; Outra forma é utilizar a sintaxe padrão de conversão de tipos (cast): 1 2 3 Cadeia a , b ; a = ( C a d e i a ) " Alguma c o i s a " ; b = ( Cadeia ) s ; Uma vez definida uma conversão, o compilador irá se valer dela para realizar conversões automáticas, sempre que necessário. Por exemplo, no código abaixo: 1 C a d e i a c = " Mais a l g o " ; tenta-se inicializar o objeto c tipo Cadeia com uma cadeia de caracteres. Uma vez que existe uma conversão definida de char∗ para Cadeia, o código será aceito pelo compilador, e resultará na execução do construtor Cadeia(const char ∗). Note que, dependendo da implementação do compilador, ele poderá inicialmente gerar um objeto tipo Cadeia temporário, e depois utilizar o construtor de cópia da classe Cadeia para construir o objeto c em função desse temporário. Algo similar ao exposto acima para a inicialização ocorrerá em todos os pontos do programa onde um objeto tipo Cadeia for esperado e um char∗ for encontrado. Qualquer construtor pode ser encarado como uma conversão. Assim, são também possíveis conversões de diversos tipos simultaneamente para um novo tipo. Por exemplo, podemos definir uma conversão de dois double para um Complexo: 1 2 3 4 5 6 7 c l a s s Complexo { d o u b l e r e , im ; public : /∗ . . . ∗/ / / Outros metodos Complexo ( d o u b l e r = 0 , d o u b l e i = 0 ) : r e ( r ) , im ( i ) {} /∗ . . . ∗/ / / Outros metodos }; Este construtor pode agora ser utilizado para conversão de dois valores double em um Complexo: 1 Complexo i = Complexo ( 0 , 1 ) ; Por possuir um valor assumido para o segundo parâmetro, este construtor será também utilizado para conversão de um único double no correspondente Complexo: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 68 1 Classes Complexo um = 1 ; Por razões sintáticas, um construtor com mais de um parâmetro não pode ser utilizado na sintaxe tradicional de conversão ( tipo ) (pág. 7), mas apenas no estilo de chamada de função. Um problema com o uso de construtores para conversão implícita é que, em certas situações, declaramos um contrutor que aceita certos parâmetros, mas o seu uso para conversão seria um erro. Por exemplo, suponhamos uma classe Vetor usada para armazenamento de um vetor de números e que defina um construtor para indicar o número de elementos do vetor: 1 2 3 4 5 6 c l a s s Vetor { /∗ . . . ∗/ public : Vetor ( i n t N) ; /∗ . . . ∗/ }; / / C r i a v e t o r com N e l e m e n t o s Com essa declaração, o código abaixo: 1 2 3 V e t o r v ( 1 0 ) ; / / C r i a v e t o r de 10 e l e m e n t o s // ... v = 2 ; / / C r i a um v e t o r de d o i s e l e m e n t o s e c o l o c a em v tem um comportamento que não se pode considerar esperado. Como indicado no comentário, a atribuição de 2 a v ativa o construtor de Vetor como conversor de int para Vetor, criando um novo vetor que é atribuído a v. Para evitar esse tipo de semântica não-intuitiva, e os possíveis erros de programação decorrentes, precisamos indicar que o construtor Vetor :: Vetor ( int ) não deve ser utilizado para conversão. Isso é feito através do uso da palavra-chave explicit , conforme exemplo abaixo: 1 2 3 4 5 6 c l a s s Vetor { /∗ . . . ∗/ public : e x p l i c i t Vetor ( i n t N) ; /∗ . . . ∗/ }; / / C r i a v e t o r com N e l e m e n t o s Sempre que se declara um construtor, é necessário verificar se esse construtor pode ou não ser legitimamente utilizado para conversão, e caso negativo deve ser utilizada a palavra-chave explicit , para evitar erros sutis e de difícil localização. 7.6.2 Conversões da classe para outros tipos Para especificar a conversão de uma classe para um outro tipo de dados utilizamos operadores de conversão. Eles são métodos da classe a ser convertida com o nome constituído pela palavra reservada operator seguida do nome do tipo que resultará da converão. Esses métodos não aceitam nenhum parâmetro. Por exemplo, se quisermos especificar um operador para converter da classe Cadeia para char∗: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 c l a s s Cadeia { char ∗ s ; public : /∗ . . . ∗/ / / Outros metodos o p e r a t o r char ∗ ( ) ; /∗ . . . ∗/ / / Outros metodos }; C a d e i a : : o p e r a t o r char ∗ ( ) { i f ( s != 0) { char ∗ t = new char [ s t r l e n ( s ) + 1 ] ; strcpy ( t , s ); return t ; } e l s e return 0; } /∗ . . . ∗/ Cadeia c ( " Europa " ) ; char ∗ s ; s = c ; / / C a d e i a : : o p e r a t o r c h a r ∗ chamado U NIVERSIDADE DE S ÃO PAULO 7.7 Objetos constantes 69 Operadores de conversão permitem converter de objetos tipo classe para tipos básicos, o que não é possível realizar com construtores. Permitem também converter de uma classe para outra. Neste último caso, construtores também poderiam ser utilizados. Suponha que temos duas classes A e B. Para especificar uma conversão da classe B para a classe A, podemos fazê-lo por meio de um construtor de conversão na classe A: 1 2 3 4 5 6 7 8 9 class B { /∗ . . . ∗/ }; class A { /∗ . . . ∗/ public : A ( B& ) ; /∗ . . . ∗/ }; ou então por meio de um operador de conversão na classe B: 1 2 3 4 5 6 7 8 9 class A { /∗ . . . ∗/ }; class B { /∗ . . . ∗/ public : operator A( ) ; /∗ . . . ∗/ }; Os dois métodos não podem ser especificados simultaneamente, pois existiria ambigüidade. Por exemplo, ao ser encontrado um código do tipo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 c la s s B; / / d e c l a r a B como uma c l a s s e a s e r d e f i n i d a class A { /∗ . . . ∗/ public : A( B& ) ; /∗ . . . ∗/ }; class B { /∗ . . . ∗/ public : operator A( ) ; /∗ . . . ∗/ }; /∗ . . . ∗/ { A a; B b; /∗ . . . ∗/ a = b ; / / Ambiguo ! ! } o compilador não teria como determinar qual das conversões utilizar para converter o valor de b. 7.7 Objetos constantes Assim como outras variáveis, objetos de uma classe podem ser declarados constantes. Como nos casos das outras variáveis, os objetos constantes somente poderão ser inicializados, mas não podem ter seu valor alterado. A inicialização de um objeto é realizada por um dos construtores da classe. Um problema aqui surge com os métodos. Como qualquer método pode, em princípio, realizar alterações no estado do objeto, a chamada de qualquer método é desabilitada pelo compilador, a menos que este método garantidamente não altere o estado do objeto. Isto é conseguido declarando o método como constante. Apenas métodos declarados constantes podem ser chamados para objetos constantes, e o compilador se assegura de que um método constante não altera o estado do objeto. Para declarar um método como constante basta acrescentar a palavra reservada const após o parênteses que fecha a lista de parâmetros, tanto no protótipo quanto na definição do método. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 70 1 Classes # include <cstring > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 c l a s s Cadeia { char ∗ s ; public : C a d e i a ( c o n s t char ∗ t = 0 ) ; C a d e i a ( c o n s t C a d e i a &c ) ; Cadeia ( const i n t n ) ; i n t tamanho ( ) c o n s t ; v o i d v a l e ( c o n s t char ∗ t ) ; o p e r a t o r char ∗ ( ) c o n s t ; v o i d muda ( c o n s t i n t i n i , c o n s t i n t fim , c o n s t char ∗ t ) ; l o n g b u s c a ( c o n s t char c ) c o n s t ; C a d e i a j u n t a ( c o n s t C a d e i a &c ) c o n s t ; Cadeia p a r t e ( const i n t i n i , const i n t fim ) const ; i n t compara ( c o n s t C a d e i a &c , c o n s t i n t n = −1) c o n s t ; ~Cadeia ( ) ; }; 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 C a d e i a : : C a d e i a ( c o n s t char ∗ t ) C a d e i a ( c o n s t C a d e i a &c ) Cadeia ( const i n t n ) i n t C a d e i a : : tamanho ( ) c o n s t v o i d C a d e i a : : v a l e ( c o n s t char ∗ t ) C a d e i a : : o p e r a t o r char ∗ ( ) c o n s t v o i d C a d e i a : : muda ( c o n s t i n t i n i , c o n s t i n t fim , c o n s t char ∗ t ) l o n g C a d e i a : : b u s c a ( c o n s t char c ) c o n s t C a d e i a Cadeoa : : j u n t a ( c o n s t C a d e i a &c ) c o n s t Cadeia Cadeia : : p a r t e ( const i n t i n i , const i n t fim ) const i n t compara ( c o n s t C a d e i a &c , c o n s t i n t n = −1) c o n s t ~Cadeia ( ) /∗ . . . ∗/ { C a d e i a c1 , c2 , c3 ; c o n s t C a d e i a c c = " Alguma c o i s a " ; / / OK: i n i c i a l i z a c a o /∗ . . . ∗/ c2 = c1 . s u b ( 0 , 1 0 ) ; / / OK, c1 pode s e r a l t e r a d o c1 . v a l e ( "Uma f r a s e " ) ; / / OK, c1 pode s e r a l t e r a d o c3 = c c . p a r t e ( 0 , 3 ) ; / / OK, s u b e c o n s t c c . v a l e ( " O u t r a f r a s e " ) ; / / ERRO : v a l e nao e c o n s t /∗ . . . ∗/ } 7.8 { { { { { { { { { { { { /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ ... ... ... ... ... ... ... ... ... ... ... ... ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ } } } } } } } } } } } } Composição de classes Suponha que desejamos lidar com dados pessoais, entre os quais nome, endereço e data de nascimento. Podemos ter uma classe para lidar com dados pessoais. Como endereço e data de nascimento são elementos compostos, eles podem também por sua vez ser representados por intermédio de classes: 1 2 3 4 5 6 7 8 9 10 11 12 13 c l a s s Data { i n t d i a , mes , ano ; public : /∗ . . . ∗/ }; c l a s s Endereco { char ∗ r u a ; i n t numero ; char ∗CEP ; char ∗ b a i r r o ; char ∗ c i d a d e ; char ∗ e s t a d o ; public : U NIVERSIDADE DE S ÃO PAULO 7.9 Funções e classes amigas 14 15 16 17 18 19 20 21 22 23 71 /∗ . . . ∗/ }; class DadosPessoais { char ∗nome ; Endereco endereco ; Data nascimento ; /∗ . . . ∗/ public : /∗ . . . ∗/ }; Este tipo de organização de classes é denominado composição. Acima, DadosPessoais é composto utilizando Endereco e Data. A relação entre DadosPessoais e as classes de que é composto é também chamada de tem-um. DadosPessoais tem-um Endereco e tem-uma Data. Um outro tipo de relação, a ser estudada no capítulo 9, é a relação é-um. Objetos membros de uma classe podem ser inicializados na lista de inicialização de membros da classe por meio dos construtores da classe membro. No exemplo acima, em um construtor da classe DadosPessoais podemos inicializar o membro nascimento por meio de um construtor da classe Data: 1 2 3 4 5 6 7 8 9 10 11 c l a s s Data { /∗ . . . ∗/ public : D a t a ( i n t d , i n t m, i n t a ) ; /∗ . . . ∗/ }; class DadosPessoais { /∗ . . . ∗/ public : DadosPessoais ( ) : nascimento (25 ,12 ,1) { /∗ . . . ∗/ } }; 7.9 Funções e classes amigas Como vimos, os membro privados de uma classe podem ser acessados apenas pelos membros da classe. Este é um dos pontos fundamentais para garantir encapsulação e redução da interface do tipo, conforme discutido. Entretanto, em alguns casos é útil permitir um relaxamento dessa proteção, liberando o acesso dos membros privados para alguma função especifica. Isto pode ser realizado declarando uma função como amiga da classe, através do uso da palavra reservada friend. 1 2 3 4 5 6 7 c l a s s Complexo { d o u b l e r e , im ; public : /∗ . . . ∗/ f r i e n d Complexo soma ( c o n s t Complexo &, c o n s t Complexo & ) ; /∗ . . . ∗/ }; 8 9 10 11 12 13 14 15 Complexo soma ( c o n s t Complexo &a , c o n s t Complexo &b ) { Complexo c ; c . r e = a . r e +b . r e ; c . im = a . im+b . im : return c ; } Aqui a função soma(Complexo&,Complexo&) é declarada com amiga da classe Complexo. Uma função amiga não é um membro da classe, e portanto a definição de soma não inclui o operado de escopo (Complexo::). No entanto, mesmo não sendo membro da classe, a função pode acessar seus membros privados re e im, conforme se vê no código. Note que é a classe que especifica quais funções são suas amigas. Desta forma não é possível introduzir amigas arbitrariamente, quebrando a proteção dos membros da classe. Assim como uma função pode ser declarada como amiga, também uma classe o pode. O resultado é que todos os métodos da classe são considerados amigos. 1 c l a s s No { I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 72 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Classes int valor ; No ∗ p r o x i m o ; friend class Fila ; }; class Fila { No ∗ p r i m e i r o ; No ∗ u l t i m o ; public : /∗ . . . ∗/ void i n s e r e ( i n t v ) ; /∗ . . . ∗/ }; void F i l a : : i n s e r e ( i n t v ) { No ∗n = new No ; n−> v a l o r = v ; / / A c e s s o a membro p r i v a d o de No n−>p r o x i m o = 0 ; / / idem i f ( p r i m e i r o == 0 ) primeiro = ultimo = n ; else { u l t i m o −>p r o x i m o = n ; ultimo = n ; } } /∗ . . . ∗/ O método Fila :: insere , por ser membro da classe Fila , é considerado amigo da classe No, e pode acessar seus membros privados. 7.10 O ponteiro this Vimos que durante a execução de um método existe sempre um objeto implícito, que corresponde àquele para o qual o método foi chamado, e que será o objeto utilizado quando uma referência a um membro da classe sem indicação de objeto ocorrer. Em alguns casos, necessitamos nos referir explicitamente a esse objeto, e isso pode ser conseguido através do ponteiro this . Sempre durante a execução de um método, o ponteiro this é definido como apontando para o objeto sobre o qual o método foi chamado. Assim por exemplo o código: 1 2 v o i d Complexo : : v a l e ( d o u b l e r , d o u b l e i ) { r e = r ; im = i ; } é equivalente ao seguinte código: 1 2 v o i d Complexo : : v a l e ( d o u b l e r , d o u b l e i ) { t h i s −> r e = r ; t h i s −>im = i ; } Certamente neste caso o uso do ponteiro this não é necessário. Sua necessidade surge, por exemplo, quando precisamos passar o objeto atual para uma função que não é membro. Neste caso ele precisa ser explicitamente passado, e o ponteiro this é necessário. Ao lidar com sobrecarga de operadores (seção 7.12) veremos alguns exemplos práticos de uso. 7.11 Membros estáticos Os membros de dados utilizados até aqui são tais que cada objeto da classe possui uma instância diferente desses membros. Por exemplo, cada membro da classe Complexo possui diferentes valores para os membros re e im. Este é o caso normal, pois em geral desejamos que os estados dos diversos objetos sejam distintos. Entretanto, é também possível em C++ definir membros que possuem o mesmo valor para todos os objetos da classe. Estes são os chamados membros estáticos, e podem também ser considerados como membros da classe, ao invés de membros dos objetos da classe. U NIVERSIDADE DE S ÃO PAULO 7.12 Sobrecarga de operadores 73 Para especificar membros como estáticos, utilizamos a palavra reservada static antes do tipo do membro. 1 2 3 4 5 6 7 c l a s s Conta { static int n ; public : C o n t a ( ) { n ++; } s t a t i c i n t quantos ( ) { return n ; } ~ C o n t a ( ) { n−−; } }; Aqui o membro n é estático, e portanto terá um valor único para todos os objetos da classe. Como o construtor da classe incrementa n e o destruidor o decrementa, n vai possuir como valor o número de objetos da classe Conta existentes em cada instante (desde que convenientemente inicializado, veja a seguir), e o método quantos retorna esse valor. Como no caso de membros não-estáticos, a definição da classe não implica na definição dos membros estáticos. No caso de membro não-estáticos a definição (e a inicialização) ocorre simultaneamente com a definição de um objeto da classe. No caso de membros estáticos, isto não pode ser feito assim, pois eles devem ser definidos apenas uma vez para serem depois utilizados por todos os membros da classe. A sintaxe para isso é apresentada no exemplo abaixo: 1 i n t Conta : : n = 0 ; Este código define o membro estático n da classe Conta e o inicializa com 0. A definição de membros estáticos é normalemente incluída no arquivo de implementação da classe. Com isto a definição da classe Conta está pronta, e pode ser utilizada: 1 2 3 4 5 6 7 8 9 10 Conta a ; int i = a . quantos ( ) ; Conta b , c ; int j = a . quantos ( ) ; C o n t a ∗p = new C o n t a ; int k = c . quantos ( ) ; Conta d ; i = p−>q u a n t o s ( ) ; / / delete p ; j = b . quantos ( ) ; // / / i recebe 1 / / j recebe 3 / / k recebe 4 i recebe 5 j recebe 4 Como os membros estáticos não estão ligados a nenhum objeto específico, mas sim à classe como um todo, eles podem também ser acessados diretamente com o uso do operador de escopo: 1 s t d : : c o u t << C o n t a : : q u a n t o s ( ) ; << s t d : : e n d l ; Esta chamada através do nome da classe foi possível pois o método quantos () foi declarado static . Métodos static podem fazer acesso apenas a outros membros (campos ou métodos) também static (pois outros tipos de membros exigem a presença de um objeto que recebe a mensagem, o que pode não existir no caso de uma chamada a métodos estáticos como acima). 7.12 Sobrecarga de operadores A implementação de números complexos apresentada acima tem uma grande desvantagem com relação ao uso: a sintaxe para realização de operações com números complexos é muito artificial e complicada. 1 2 3 Complexo a , b , c ; /∗ . . . ∗/ a . v a l e ( b . mais ( c ) ) ; // a = b + c Felizmente a linguagem C++ apresenta um solução muito melhor, que permite operações como as indicadas no comentário acima. Isto é conseguido com a chamada sobrecarga de operadores. Da mesma forma que nomes de funções podem ser sobrecarregados, operadores pré-definidos da linguagem podem ser sobrecarregados para aceitar objetos de classe. Para definir a sobrecarga de um operador para determinada classe devemos definir uma função com um nome consistindo da palavra reservada operator seguida do símbolo do operador. Esta função pode tanto ser um membro da classe quanto uma função amiga. As diferenças entre esses dois casos serão discutidas adiante. Por exemplo, podemos sobrecarregar os operadores aritméticos para a classe Complexo: 1 2 3 4 c l a s s Complexo { d o u b l e r e , im ; public : Complexo ( c o n s t d o u b l e r = 0 , c o n s t d o u b l e i = 0 ) : I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 74 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Classes r e ( r ) , im ( i ) {} Complexo ( c o n s t Complexo &c ) : r e ( c . r e ) , im ( c . im ) {} double r e a l ( ) const { return r e ; } d o u b l e imag ( ) c o n s t { r e t u r n im ; } d o u b l e mod ( ) c o n s t ; double f a s e ( ) const ; f r i e n d Complexo o p e r a t o r + ( c o n s t Complexo &, c o n s t Complexo & ) ; f r i e n d Complexo o p e r a t o r −( c o n s t Complexo &, c o n s t Complexo & ) ; f r i e n d Complexo o p e r a t o r ∗ ( c o n s t Complexo &, c o n s t Complexo & ) ; f r i e n d Complexo o p e r a t o r / ( c o n s t Complexo &, c o n s t Complexo & ) ; f r i e n d Complexo o p e r a t o r −( c o n s t Complexo & ) ; }; /∗ . . . ∗/ Complexo o p e r a t o r + ( c o n s t Complexo &a , c o n s t Complexo &b ) { Complexo c ; c . r e = a . r e +b . r e ; c . im = a . im+b . im ; return c ; } / ∗ . . . ∗ / / / idem p a r a d e m a i s o p e r a d o r e s Note que os operadores devem ser membros ou amigos, de forma a poderem acessar os membros privados da classe. Dois operadores ‘−’ foram definidos: um é operador de subtração (operador binário) e outro o de mudança de sinal (operador unário). Uma vez definidos os operadores como acima, eles podem ser utilizados em cálculos: 1 2 3 4 5 Complexo a , b , c , d ; /∗ . . . ∗/ a = b+c ; d = b/a; c = −(( a+b ) ∗ ( c−d ) ) ; O código acima é equivalente ao seguinte: 1 2 3 4 5 Complex a , b , c , d ; /∗ . . . ∗/ a = operator +( b , c ) ; d = operator / ( b , a ) ; c = o p e r a t o r −( o p e r a t o r ∗ ( o p e r a t o r + ( a , b ) , o p e r a t o r −(c , d ) ) ) ; Note que os operandos à esquerda são passados como primeiro parâmetro para os operadores binários, enquanto os operandos à direita são passados como segundo parâmetro. Uma outra forma de definir os operadores seria como membros da classe: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 c l a s s Complexo { d o u b l e r e , im ; public : Complexo ( c o n s t d o u b l e r = 0 , c o n s t d o u b l e i = 0 ) : r e ( r ) , im ( i ) {} Complexo ( c o n s t Complexo &c ) : r e ( c . r e ) , im ( c . im ) {} double r e a l ( ) const { return r e ; } d o u b l e imag ( ) c o n s t { r e t u r n im ; } d o u b l e mod ( ) c o n s t ; double f a s e ( ) const ; Complexo o p e r a t o r + ( c o n s t Complexo & ) ; Complexo o p e r a t o r −( c o n s t Complexo & ) ; Complexo o p e r a t o r ∗ ( c o n s t Complexo & ) ; Complexo o p e r a t o r / ( c o n s t Complexo & ) ; Complexo o p e r a t o r − ( ) ; }; /∗ . . . ∗/ Complexo Complexo : : o p e r a t o r + ( c o n s t Complexo &a ) { Complexo c ; c . r e = r e +a . r e ; c . im = im+a . im ; U NIVERSIDADE DE S ÃO PAULO 7.12 Sobrecarga de operadores 23 24 25 75 return c ; } / ∗ . . . ∗ / / / idem p a r a d e m a i s o p e r a d o r e s Quando um operador é membro, um dos operandos é sempre o objeto sobre o qual o operador será chamado. O código: 1 2 3 4 5 Complexo a , b , c , d ; /∗ . . . ∗/ a = b+c ; d = b/a; c = −(( a+b ) ∗ ( c−d ) ) ; será agora equivalente a: 1 2 3 4 5 6 7 8 Complexo a , b , c , d ; /∗ . . . ∗/ a = b . operator +( c ) ; d = b . operator / ( a ) ; Complexo tmp1 = a . o p e r a t o r + ( b ) ; Complexo tmp2 = c . o p e r a t o r −(d ) ; Complexo tmp3 = tmp1 . o p e r a t o r ∗ ( tmp2 ) ; c = tmp3 . o p e r a t o r − ( ) ; Note que o operando à esquerda será o objeto sobre o qual o método será chamado, e eventuais segundos operandos serão passados como parâmetro. Isto já indica a desvantagem da definição de operadores como membros: o operando da esquerda precisa sempre ser um objeto da classe. Esta limitação não existe quando o operador é amigo. Por exemplo, a classe Complexo tem um construtor capaz de converter double para Complexo. Isto significa que, se for utilizado um operador de produto amigo, a operação: 1 a = 5+b ; corresponderá a: 1 a = operator +(5 , b ) ; e como o compilador sabe converter automaticamente int para double, o valor 5 será convertido para double, e então chamado o conversor de double para Complexo, resultando no código: 1 a = o p e r a t o r + ( Complexo ( ( d o u b l e ) 5 ) , b ) ; e a operação será válida. Já no caso de ser utilizado um operador membro para a soma, o compilador precisaria gerar o código: 1 a = 5 . operator +( b ) ; o que não faz sentido, pois o tipo int não é uma classe, e não possui nenhum operador de soma com Complexo. Portanto o código seria recusado pelo compilador. Por isso são em geral utilizados operadores amigos. Algumas considerações com relação à sobrecarga: • Os operadores seguintes não podem ser sobrecarregados: . .∗ :: ?: sizeof • Os outros operadores, incluindo new, delete , () (chamada de função) e [] (indexação) podem ser sobrecarregados. • Não é possível alterar a precedência, a associatividade ou o número de operandos dos operadores pré-definidos. • Não é possível criar novos operadores. Apenas operadores pré-definidos podem ser sobrecarregados. • Um operador sobrecarregado não pode ter argumentos assumidos. • Quando se definem operadores ++ e −− com um parâmetro (caso amigos, ou sem parâmetro caso membros), então eles definem os operadores de pré-incremento e pré-decremento respectivamente. Para definir os operadores de pós-incremento e pós-decremento deve ser utilizado um argumento adicional, de tipo int, e que receberá o valor 0 na chamada. Veja exemplo: 1 2 3 4 5 class A { int x ; public : A( i n t i = 0 ) { x = i ; } A& o p e r a t o r + + ( ) { ++x ; r e t u r n ∗ t h i s ; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 76 Classes 6 7 8 9 10 11 12 13 A o p e r a t o r ++( i n t i ) { r e t u r n A( x + + ) ; } }; /∗ . . . ∗/ { A a; ++ a ; / / c o r r e s p o n d e a a . o p e r a t o r + + ( ) ; a ++; / / c o r r e s p o n d e a a . o p e r a t o r + + ( 0 ) ; } • Os operadores () , [] , −> e = devem sempre ser declarados como membros (não podem ser amigos). Para concluir, vejamos um exemplo com a sobrecarga do operador de indexação. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 c l a s s MeuVetor { size_t n; d o u b l e ∗v ; public : MeuVetor ( s i z e _ t m, d o u b l e v a l = 0 ) { i f (m <= 0 ) a b o r t ( ) ; n = m; v = new d o u b l e [ n ] ; f o r ( s i z e _ t i = 0 ; i < n ; i ++) v [ i ] = v a l ; } d o u b l e& o p e r a t o r [ ] ( s i z e _ t i ) { i f ( i >= 0 && i < n ) r e t u r n v [ i ] ; e l s e a b o r t ( ) ; } c o n s t d o u b l e& o p e r a t o r [ ] ( s i z e _ t i ) c o n s t { i f ( i >= 0 && i < n ) r e t u r n v [ i ] ; e l s e a b o r t ( ) ; } }; Note que o operador de indexação deve retornar uma referência para o elemento indicado pelo índice, para que a indexação possa ser utilizada como na indexação de vetores da linguagem. A segunda variante, que retorna uma referência constante, é usada para indexar vetores constantes Esta classe pode ser utilizada como um vetor, mas com teste da indexação: 1 2 3 4 5 6 MeuVetor a ( 1 0 ) ; c o n s t MeuVetor b ( 2 0 , 1 . 5 ) ; f o r ( s i z e _ t i = 0 ; i < 1 0 ; i ++) { a [ i ] = 2∗ i + b [ i ] ; / / a c e s s o b [ i ] s e r i a p r o i b i d o s e v a r i a n t e c o n s t de [ ] } / / a [ 1 2 ] i n t e r r o m p e a e x e c u ç ã o do programa 7.13 Atribuição de objetos Vimos acima que o operador de atribuição = pode ser sobrecarregado. Até aqui não fizemos isto, e no entanto nos utilizamos diversas vezes de operadores de atribuição para objetos de classe, por exemplo para objetos da classe Complexo (pág. 74). Isto pode ser feito porque a linguagem dispõe do que é chamado operador de atribuição assumido. Sempre que nenhum operador de atribuição especial é designado para uma classe, o operador de atribuição assumido é utilizado para as atribuições. O operador de atribuição assumido realiza simplesmente uma cópia membro a membro. Isto é, todos os membros de dados do objeto que terá seu valor atribuído são copiados dos membros do objeto com o valor original. Esta operação é suficiente para muitos casos, como no caso da classe Complexo, e nesses casos a redefinição não é necessária. Em outros casos, entretanto, isto não é suficiente. Suponha por exemplo uma classe Cadeia com construtor e destruidor: 1 2 3 4 5 6 7 8 9 10 c l a s s Cadeia { char ∗ s ; public : C a d e i a ( c o n s t char ∗ t = 0 ) ; /∗ . . . ∗/ / / o u t r o s metodos ~Cadeia ( ) ; }; C a d e i a : : C a d e i a ( c o n s t char ∗ t ) { i f ( t != 0) { U NIVERSIDADE DE S ÃO PAULO 7.13 Atribuição de objetos 11 12 13 14 15 16 17 18 19 77 s = new char [ s t r l e n ( t ) + 1 ] ; strcpy (s , t ); } else s = 0; } /∗ . . . ∗/ / / o u t r o s metodos Cadeia : : ~ Cadeia ( ) { delete [] s ; } Esta classe terá um problema com o operador de atribuição padrão, como demonstrado no código abaixo: 1 2 3 4 C a d e i a a = "Uma c a d e i a " ; C a d e i a ∗b = new C a d e i a ; ∗b = a ; delete b ; Primeiro o objeto a é inicializado com a cadeia de caracteres dada. Em seguida um novo objeto do tipo Cadeia é alocado e um ponteiro colocado em b. O valor de a é copiado no objeto apontado por b, o que resulta em que b−>s aponte para o mesmo lugar que a . s. Quando o objeto apontado por b é liberado por meio do operador delete , o destruidor da classe Cadeia é chamado, ocasionando a liberação do espaço apontado por b−>s. Como esse era o mesmo espaço apontado por a . s, este último fica apontando para uma região de memória já liberada, e seus dados são portanto inválidos. Este problema pode ser resolvido através da definição de um operador de atribuição para a classe Cadeia que se incumba de realizar uma cópia da cadeia original, ao invés de apenas copiar o ponteiro. É fácil ver que o mesmo problema ocorre em inicializações, se um construtor de cópia adequado não for definido. Este problema é tão freqüente que enunciamos a seguinte regra prática: Sempre que uma classe lida com ponteiros para elementos alocados dinâmicamente ela deve definir um operador de atribuição e um construtor de cópia próprios. Para permitir que o operador de atribuição seja utilizado para classes como ele é utilizado para tipos básicos, é necessário que ele retorne um valor, que será utilizado como valor resultante da correspondente expressão, permitindo o uso de atribuições múltiplas em um mesmo comando. Também é importante considerar possíveis valores armazenados anteriormente no objeto, e que devam ser liberados antes da atribuição de novos valores (por exemplo, realizar delete para ponteiros do objeto que tenham recebido um new). Como exemplo, vejamos um operador de atribuição para a classe Cadeia: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 c l a s s Cadeia { char ∗ s ; public : /∗ . . . ∗/ / / Outros metodos c o n s t C a d e i a &o p e r a t o r = ( c o n s t C a d e i a &c ) ; /∗ . . . ∗/ / / Outros metodos }; /∗ . . . ∗/ / / Implementacoes c o n s t C a d e i a &C a d e i a : : o p e r a t o r = ( c o n s t C a d e i a &c ) { i f (& c ! = t h i s ) { delete [] s ; i f ( c . s != 0) { s = new char [ s t r l e n ( c . s ) + 1 ] ; strcpy (s , c . s ); } else s = 0; } return ∗ t h i s ; } Repare no uso do ponteiro this . Primeiro ele é utilizado para verificar se o objeto a ser copiado é o próprio objeto (auto-cópia). Se este for o caso, nada precisa ser feito. Aliás, se não houvesse a proteção desse teste haveria problemas com atribuições do tipo a = a (pense na razão!). O teste é feito verificando se o endereço do objeto a ser copiado é igual ao valor do ponteiro this . Por fim, o ponteiro this é usado novamente para fornecer o retorno da função. Uma referência ao objeto apontado por this (portanto ao objeto que recebeu a chamada do método) é retornada. Outro fato a ressaltar é que definimos a referência retornada pela função como sendo constante. Isto é compatível com o uso do operador de atribuição pré-definido. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 78 Classes Exercícios 1. O que caracteriza um tipo abstrato de dados? Quais as vantagens em sua utilização? 2. Defina os conceitos de interface e implementação. Por que a implementação deve ser ocultada dos clientes? 3. O que se entende por encapsulação? 4. De que forma a ocultação de implementação é conseguido em C++? 5. Qual a relação entre classes e objetos? 6. O que é um membro de uma classe? 7. O que são métodos e mensagens? 8. Por que a interface de uma classe deve permanecer inalterada em novas versões da classe? O mesmo é válido para a implementação? 9. Indique as similaridades e diferenças entre struct e class em C++. 10. O que são construtores e destruidores? Para que são utilizados? Quando eles são executados? 11. O que se entende por inicialização de membros? Como ela é efetuada? Em que situações é obrigatório o uso de inicialização de membros? 12. De que forma podemos definir um método in-line? 13. Como se definem conversões entre classes? Quais são os dois tipos de conversão que podem ser definidos em uma classe? 14. O que são objetos constantes? Qual a limitação no envio de mensagens para objetos constantes? 15. O que se entende por composição de classes? De que forma a composição de classes pode ser utilizada para melhorar as características de engenharia de software de um programa? 16. O que são funções amigas? Quando é útil declarar uma função como amiga? Comente sobre o uso de funções amigas em relação à encapsulação. 17. Qual é o significado de declarar uma classe como amiga? 18. Qual é o significado da palavra reservada this quando usada dentro de um método de uma dada classe? 19. O que são membros estáticos? Qual é sua utilidade? 20. Por que razão um membro de dados estático precisa ser explicitamente definido, enquanto que um membro de dados comum não precisa? 21. O que se entende por sobrecarga de operadores? 22. O que são operadores friend e operadores membros? Eles são totalmente equivalentes? 23. O que é um operador de atribuição assumido? Quando ele é utilizado? Qual seu comportamento? Em que situações o seu uso deve ser evitado, e como isso é feito? 24. No exemplo apresentado de operador de atribuição para a classe Cadeia, é feita uma verificação se o objeto a ser copiado é o próprio objeto a ser atribuído, e é dito que se esse teste não fosse realizado haveria problema com atribuições da forma a = a. Qual é a razão? 25. Indique erros existentes nos códigos abaixo: U NIVERSIDADE DE S ÃO PAULO 7.13 Atribuição de objetos (a)1 c l a s s D a t a { 2 3 4 5 6 7 8 9 10 i n t d i a , mes , ano ; public : Data ( int , int , i n t ) ; v o i d a l t e r a ( i n t d , i n t m, i n t a ) ; }; void p r i n t ( Data d ) { s t d : : c o u t << d . d i a << " / " << d . mes << " / " << d . ano ; } (b)1 c l a s s V e t o r { 2 3 4 5 6 7 c o n s t i n t N; i n t ∗v ; public : V e t o r ( i n t n ) { N = n ; v = new i n t [ n ] ; } /∗ . . . ∗/ }; (c)1 c l a s s A { 2 3 4 5 6 7 8 9 10 11 12 13 int x ; public : void s e t ( i n t i ) { x = i ; } i n t get ( ) { return x ; } }; i n t main ( ) { const A a ; a . set (2); s t d : : c o u t << a . g e t ( ) << s t d : : e n d l ; return 0; } (d)1 c l a s s B { 2 3 4 5 6 7 8 double r ; public : B( double a ) { r = a ; } f r i e n d B o p e r a t o r + (B , B ) ; f r i e n d B o p e r a t o r −(B , B ) ; f r i e n d B o p e r a t o r = (B ) ; }; 26. Qual o resultado da execução do programa abaixo? (a)1 # i n c l u d e < i o s t r e a m > class A { int n ; 4 public : 5 A( i n t i = 3 ) ; 6 A(A &a ) ; 7 void s e t ( i n t i ) ; 8 int get ( ) ; 9 }; 10 A : : A( i n t i) 11 { 12 s t d : : c o u t << " C o n s t r u t o r A( i n t ) chamado " << s t d : : e n d l ; 13 n = i; 14 } 15 A : : A(A &a ) 16 { 17 s t d : : c o u t << " C o n s t r u t o r A(A&) chamado " << s t d : : e n d l ; 18 n = a.n; 2 3 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 79 80 Classes 19 20 21 22 23 24 25 26 27 28 29 30 } void A : : s e t ( i n t i ) { n = i ; } i n t A : : get ( ) { return n ; } v o i d f (A a ) { s t d : : c o u t << a . g e t ( ) << s t d : : e n d l ; } v o i d g (A &a ) { s t d : : c o u t << a . g e t ( ) << s t d : : e n d l ; } i n t main ( ) { A a , b(2); f (a ); a . set (5); f (a ); g(b ); return 0; } 27. Desenvolva uma classe para lidar com números racionais. A classe deve incluir operações aritméticas básicas e permitir a impressão de números racionais tanto no formatos 12/17 como através do valor de ponto flutuante correspondente. 28. Projete e implemente uma classe para lidar com datas. Projete cuidadosamente a interface da classe, e mantenha a implementação encapsulada. 29. A classe Cadeia apresentada no texto é limitada em suas capacidades. Desenvolva uma nova classe Cadeia, que permita que uma Cadeia tenha funcionalidade similar a um char∗. Queremos que a classe tenha métodos que sejam correspondentes a todas as funções de manipulação de cadeias fornecidas por string . h. Procure definir a interface da classe de tal forma que o seu uso seja mais simples do que o das funções de biblioteca correspondentes. 30. Uma firma deseja montar uma base de dados sobre seus funcionários. Sobre cada funcionário devem ser guardados dados pessoais, como nome, endereço, identificação, data de nascimento, além de dados próprios úteis para a firma, como cargo, data de admissão e número de dias de férias já utilizadas durante o ano. Os dados de todos os funcionários devem ser guardados em um arquivo. Quando o programa é rodado, esses dados são lidos em memória, e o usuário do programa pode realizar alterações na base de dados, como exclusão de funcionário, inclusão de novo funcionário e alteração de dados de algum funcionário atual. Ao sair do programa, os novos dados devem ser guardados de volta no arquivo. Se ao iniciar o programa um arquivo de dados não existe, então um novo deve ser criado. Resolva este problema utilizando orientação a objetos. U NIVERSIDADE DE S ÃO PAULO Capítulo 8 Escopo Escopo é o nome dado à região de validade de identificadores. Cada identificador é válido dentro de um certo escopo. Em escopos diferentes o mesmo identificador pode ter significados diferentes. C++ tem os seguintes tipos de escopo: Escopo local: é também chamado escopo de bloco. Uma variável declarada dentro de um bloco tem seu escopo apenas nesse bloco (a partir do ponto de definição) e nos blocos interiores a ele. Parâmetros de uma função são considerados com escopo no bloco mais externo da função. Escopo de função: é válido para rótulos utilizados dentro de uma função. Os rótulos têm escopo em toda a função onde sejam declarados. Apenas rótulos têm escopo de função. Escopo de arquivo: Identificadores declarados exteriormente a todos os blocos e classes têm escopo de arquivo e podem ser utilizados em qualquer ponto desse arquivo após o ponto da declaração. Identificadores com escopo de arquivo são chamados identificadores globais. Escopo de classe: Identificadores que declaram membros de classe são locais a essa classe e somente podem ser utilizados em métodos da classe ou através de um objeto da classe com uso dos operadores de acesso a membro . ou −> ou com uso do operador de resolução de escopo :: . Escopo de namespace: Um conjunto de identificadores pode ser declarado em um espaço de escopo explicitamente distinto, utilizando a construção conhecida como namespace, a ser discutida a seguir. Os vários escopos locais não podem ser individualmente especificados, porém o escopo global e os vários escopos de classe e de namespace podem ser acessados explicitamente com o uso do operador de escopo :: . O operador de escopo unário, isto é, aquele sem um operando à esquerda, especifica o escopo global. O operando deve ser um identificador global. Este operador é útil para acessar variáveis globais que foram escondidas por definições locais. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int x ; void f ( ) { int y ; y = x ; / / acessa x } void g ( ) { int x , y ; y = x; / / acessa y = : : x ; / / acessa } void h ( i n t x ) { int y ; y = x; / / acessa y = : : x ; / / acessa } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS global x local x global parametro x x global 82 Escopo O operador de escopo binário recebe um nome de classe ou namespace à esquerda, que será a classe ou espaço de nomes em cujo escopo o identificador à direita será buscado. Já vimos seu uso na definição de métodos da classe e de membros estáticos. Um outro uso deste operador é relacionado com a definição aninhada de tipos. Por exemplo, o código abaixo define uma enumeração para dias da semana dentro da classe Data: 1 2 3 4 5 6 7 8 c l a s s Data { i n t d i a , mes , ano ; public : / ∗ . . . ∗ / / / o u t r o s membros enum DiaDaSemana { domingo , s e g u n d a , t e r c a , q u a r t a , quinta , sexta , sabado }; / ∗ . . . ∗ / / / o u t r o s membros }; Como está definida dentro da classe Data, esta enumeração tem escopo de classe, e somente poderá ser utilizada com a ajuda do operador de escopo, assim como todos os valores associados: 1 2 D a t a : : DiaDaSemana d s ; ds = Data : : segunda ; 8.1 Espaços de nome Espaços de nome (namespaces) permitem a declaração de identificadores em um escopo exclusivo, de forma que seus nomes não colidirão com identificadores de mesmo nome em escopos locais, globais, de classe ou de outros namespaces. Isto facilita o desenvolvimento modular de software, pois durante o desenvolvimento de uma parte do programa não é necessário temer colisão de identificadores com outras partes. Cada namespace deve englobar definições e declarações correlacionadas logicamente, isto é, deve-se evitar utilizar um namespace apenas para eliminar colisão de nomes. A sintaxe é exemplificada no trecho de código abaixo: 1 2 3 4 5 namespace P r i m o s { bool t e s t a ( i n t n ) ; v o i d decompoe ( i n t n , i n t &f a t o r e s [ ] , i n t &p o t e n c i a s [ ] , i n t &m ) ; v o i d p r i m o s _ a t e ( i n t n , i n t &p r i m o s [ ] , i n t &m ) ; } 6 7 8 9 bool Primos : : t e s t a ( i n t n ) { /∗ . . . ∗/ } 10 11 12 13 v o i d P r i m o s : : decompoe ( i n t n , i n t &f a t o r e s [ ] , i n t &p o t e n c i a s [ ] , i n t &m) { /∗ . . . ∗/ } 14 15 16 17 v o i d P r i m o s : : p r i m o s _ a t e ( i n t n , i n t &p r i m o s [ ] , i n t &m) { /∗ . . . ∗/ } Os identificadores definidos e declarados dentro de um namespace somente são válidos nesse namespace, e portanto todos os usos devem ser qualificados com o operador de escopo. 1 2 3 4 5 6 7 8 int f ( int x) { /∗ . . . ∗/ i f ( ! Primos : : t e s t a ( n ) ) { P r i m o s : : decompoe ( n , f a t , p o t , q u a n t o s ) ; /∗ . . . ∗/ } } Se um trecho de código vai usar muito freqüentemente algumas funções de um namespace, pode ser usada a declaração using. Por exemplo, o trecho acima pode ser reescrito: 1 2 3 int f ( int x) { using Primos : : t e s t a ; U NIVERSIDADE DE S ÃO PAULO 8.1 Espaços de nome u s i n g P r i m o s : : decompoe ; /∗ . . . ∗/ if ( ! testa (n) ) { decompoe ( n , f a t , p o t , q u a n t o s ) ; /∗ . . . ∗/ } 4 5 6 7 8 9 10 83 } Caso um trecho de código faça uso extensivo de diversas declarações de um namespace, então pode-se utilizar a directiva using namespace, como na variante abaixo: 1 2 3 4 5 6 7 8 9 int f ( int x) { u s i n g namespace P r i m o s ; /∗ . . . ∗/ if ( ! testa (n) ) { decompoe ( n , f a t , p o t , q u a n t o s ) ; /∗ . . . ∗/ } } As declarações e diretivas using são também úteis no processo de transição de um código que não usa namespaces para um que os usa, pois permitem que o código anterior usuário das declarações não precise ser alterado além da inclusão de uma diretiva using. Os namespaces são bastante úteis para fornecer variantes distintas de um mesmo conjunto de classes ou rotinas. Por exemplo, uma empresa pode distribuir duas versões de rotinas de álgebra linear, uma que procura executar no menor tempo possível e uma que procura conseguir trabalhar com matrizes as maiores possíveis. Esses dois conjuntos de rotinas podem ser distribuídos em dois namespaces distintos, por exemplo FastLinAlg e BigLinAlg. O usuário pode então fazer a sua escolha ao desenvolver seu programa: 1 2 3 4 F a s t L i n A l g : : M a t r i x m1 , m2 ; / / I n i c i a l i z a m1 e m2 m1 += m2 ; / / u s a o p e r a d o r += de F a s t L i n A l g ( c l a s s e de m1 ) F a s t L i n A l g : : d i a g o n a l i z e ( m1 , a u t o v a l ) ; O código desenvolvido como no exemplo acima tem o problema de que, se o usuário começar a trabalhar com matrizes muito grandes e quiser mudar para BigLinAlg ele terá que procurar todas as entradas FastLinAlg e trocá-las por BigLinAlg em seu código. O problema não existiria se o usuário utilizasse um using namespace no início de seu código, mas isso implicaria no risco de colisão de nomes. A solução mais versátil é utilizar um sinônimo de namespace : 1 namespace L i n A l g = F a s t L i n A l g ; 2 3 4 5 6 L i n A l g : : M a t r i x m1 , m2 ; / / I n i c i a l i z a m1 e m2 m1 += m2 ; / / u s a o p e r a d o r += de F a s t L i n A l g ( c l a s s e de m1 ) L i n A l g : : d i a g o n a l i z e ( m1 , a u t o v a l ) ; Para alterar o programa para usar BigLinAlg basta trocar o nome na linha de sinônimo. Um uso bastante interessante dessa técnica é apresentar duas variantes de uma biblioteca: uma final, para execução normal, e uma de depuração, que envia durante a execução várias mensagens de depuração. O uso de um sinônimo permite então escolher entre as duas versões. Na verdade, pode-se escolher individualmente no programa rotina por rotina ou classe por classe se desejamos a versão com depuração ou não: 1 2 using DebugContainers : : Stack ; using Containers : : L i s t ; / / p i l h a s com d e p u r a c a o / / L i s t a s sem d e p u r a c a o 3 4 5 S t a c k a , b ; / / a e b t e r ã o i n f o r m a c a o de d e p u r a c a o List c ; / / c sem m e n s a g e n s de d e p u r a c a o Uma outra possibilidade é criar um novo namespace através da composição de dois ou mais namespaces. Um exemplo é dado abaixo: 1 2 3 4 5 namespace F i g u r a s { class Circulo { /∗ . . . ∗/ }; c l a s s Quadrado { / ∗ . . . ∗ / } ; // ... } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 84 6 7 8 9 10 Escopo namespace I m a g e n s { c l a s s JPG { / ∗ . . . ∗ / } ; c l a s s PNG { / ∗ . . . ∗ / } ; // ... } 11 12 13 14 15 16 namespace G r a f i c o s { u s i n g namespace F i g u r a s ; u s i n g namespace I m a g e n s ; class I n t e r f a c e { /∗ . . . ∗/ }; } Neste exemplo, o espaço de nomes Grafico inclui todas as classes dos namespace Figuras e Imagens, além da classe Interface . Exercícios 1. Quais são os tipos de escopo existentes em C++? 2. Qual a diferença entre os operadores de escopo binário e unário? 3. Qual a utilidade de se declarar tipos dentro de uma classe? Como esses tipos podem posteriormente ser acessados? 4. Quais as diferenças e semelhanças entre espaços de nome e classes? 5. De que forma o uso de espaço de nomes auxilia o desenvolvimento de softwares complexos por uma equipe grande de programadores? U NIVERSIDADE DE S ÃO PAULO Capítulo 9 Herança Três pontos fundamentais que caracterizam a programação orientada a objetos são encapsulação, herança e polimorfismo. Encapsulação é conseguida através do uso de tipos abstratos de dados, conforme discutido no capítulo 7. Polimorfismo será estudado no capítulo 10. Neste capítulo estudaremos o conceito de herança. 9.1 Herança como ferramenta de projeto Vimos como tipos de dados abstratos (classes) podem ser implementados. Quando classes são utilizadas para modelar objetos do campo de aplicação do programa, é comum ocorrer que as diversas classes não sejam totalmente independentes entre si. Já vimos como a composição (seção 7.8) pode ser utilizada para modelar um tipo de relação entre classes, a relação que chamamos de tem-um. Muitas vezes, entretanto, a relação entre as classes é uma que podemos chamar de relação de subconjunto, ou de especialização. Com isto queremos indicar que o conjunto dos objetos de uma das classes é um subconjunto do conjunto dos objetos de outra classe; ou dito de outra forma, uma das classes é uma especialização de outra, isto é, um objeto de uma das classes pode ser considerado como um objeto da outra classe com algumas características especiais. Isto é modelado através do uso de herança, sendo a classe mais geral chamada classe base ou superclasse e a classe mais restrita chamada classe derivada ou subclasse. Existem diversas formas pelas quais uma classe pode vir a ser subclasse de outra. Dizemos que B é subclasse de A por restrição se um objeto do tipo B pode ser considerado como um objeto do tipo A com algumas das características fixas. A classe C é chamada uma subclasse de A por expansão se um objeto da classe C pode ser considerado como um objeto da classe A adicionado de algumas características. Por fim, D é chamada uma subclasse de A por alteração se um objeto de D pode ser considerado como um objeto de A com algumas características alteradas1 . As diversas formas podem ocorrer simultaneamente. Vejamos alguns exemplos. Quadrado é uma subclasse de retângulo por restrição, pois um quadrado é um tipo de retângulo onde os lados são sempre iguais, e portanto os lados somente podem variar de forma restrita. Funcionário é uma subclasse de pessoa por expansão, pois além de todos os atributos normais em pessoas tem também um atributo para designar o cargo que ocupa. Automóvel pifado é uma subclasse de automóvel por alteração, pois o comportamento de um automóvel pifado é uma alteração do comportamento de um automóvel em relação às suas respostas aos comandos do motorista. É importante notar que, seja a subclasse derivada por restrição, expansão ou alteração, sempre a subclasse é um subconjunto da classe base, no sentido de que objetos da classe derivada podem ser contados entre os objetos da classe base, apesar de que terão talvez comportamento distintos em algumas situações. Se B é subclasse de A, nada impede que B também tenha suas subclasses. Sendo C uma subclasse de B, ela é também uma subclasse de A, pois, todo C é um B, e todo B é um A. Assim, a organização em classes pode se ramificar em árvores que apresentam toda uma hierarquia nas suas relações. 9.2 Definição de classes derivadas Vejamos agora como definir classes derivadas em C++. Consideremos que desejamos desenvolver um programa que precisará lidar com dados sobre alunos de graduação e de pós-graduação, professores, técnicos e secretárias. Todas essas classes têm muita coisa em comum, por exemplo nome, endereço e data de nascimento. No entanto, enquanto professores, técnicos e secretárias são funcionários, alunos de graduação e pós-graduação são alunos. Além do mais, os dois tipos de 1 Note que neste último caso, a relação ideal de subconjunto não é totalmente pertinente, e da mesma forma a herança não pode ser aplicada sem problemas. Este tipo de herança é freqüentemente usado para aproveitamento de código, mas pode causar problemas de manutenção no programa. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 86 Herança Figura 9.1: Exemplo de hierarquia de classes alunos diferem entre si, assim como os três tipos de funcionários. Ficamos então com a hierarquia de classes mostrada na figura 9.1. A classe Pessoa terá todas as informações comuns a todos os outros tipos. Aluno trará aquilo que é comum a alunos de graduação e alunos de pós-graduação e Funcionario aquilo que é comum a professores, técnicos e secretárias. Para especificar essa hierarquia de classes em C++ utilizamos um código como o que segue: 1 2 3 4 5 6 7 8 class class class class class class class class Pessoa Aluno AlunoGraduacao AlunoPos Funcionario Professor Tecnico Secretaria : : : : : : : public public public public public public public Pessoa Aluno Aluno Pessoa Funcionario Funcionario Funcionario { { { { { { { { /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ ... ... ... ... ... ... ... ... ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ ∗/ }; }; }; }; }; }; }; }; Como vemos, para declarar uma classe derivada, utilizamos a notação de colocar o nome da classe base após doispontos. O significado da palavra reservada public será discutido mais adiante (seção 9.4). O efeito de fazer a classe Aluno como derivada da classe Pessoa é tornar todos os membros (membros tipo dados e métodos) da classe Pessoa disponíveis na classe Aluno (esse membros são herdados). A classe derivada pode então acrescentar novos membros, alterar membros já existentes na classe base (através de uma nova definição com o mesmo nome) ou ocultar membros da classe base. Desta forma o código desenvolvido para a classe base pode ser reaproveitado ao máximo na classe derivada, sendo alterado apenas nos pontos necessários.Vejamos isto através do exemplo acima: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 c l a s s Cadeia { char ∗ s ; public : C a d e i a ( c o n s t char ∗ t = 0 ) ; C a d e i a ( c o n s t C a d e i a &c ) ; v o i d i m p r ( ) c o n s t { c o u t << s ; } /∗ . . . ∗/ / / o u t r o s metodos }; c l a s s Data { i n t d i a , i n t mes , i n t ano ; public : D a t a ( i n t d , i n t m, i n t a ) : { i f ( ( d >=1 && d <=31) && (m>=1 && m<=12) && a ! = 0 ) { d i a =d ; mes=m; ano =a ; } } D a t a ( c o n s t D a t a &d ) { d i a =d . d i a ; mes=d . mes ; ano =d . ano ; } void impr ( ) const ; /∗ . . . ∗/ }; c l a s s Endereco { Cadeia rua ; U NIVERSIDADE DE S ÃO PAULO 9.2 Definição de classes derivadas 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 87 i n t numero ; C a d e i a complemento ; Cadeia b a i r r o ; Cadeia cidade ; C a d e i a CEP ; Cadeia estado ; Cadeia p a i s ; public : E n d e r e c o ( C a d e i a r , i n t n , C a d e i a b , C a d e i a c , C a d e i a cep , C a d e i a e = " SP " , C a d e i a cmp = " " Cadeia p = " B r a s i l " ) : r u a ( r ) , numero ( n ) , complemento ( cmp ) , b a i r r o ( b ) , c i d a d e ( c ) , CEP ( c e p ) , e s t a d o ( e ) , p a i s ( p ) {} void impr ( ) const ; /∗ . . . ∗/ }; c l a s s Pessoa { C a d e i a nome ; Endereco endereco ; Data nascimento ; public : P e s s o a ( C a d e i a n , E n d e r e c o e , D a t a dn ) : nome ( n ) , e n d e r e c o ( e ) , n a s c i m e n t o ( dn ) {} void impr ( ) const ; /∗ . . . ∗/ }; C a d e i a : : C a d e i a ( c o n s t C a d e i a &c ) { i f ( c . s != 0) { s = new char [ s t r l e n ( c . s ) + 1 ] ; s t r c p y ( s , c . s ) ; } else s = 0; } void Data : : impr ( ) const { s t a t i c char ∗nomeDoMes [ ] = { " j a n e i r o " , " f e v e r e i r o " , " marco " , " a b r i l " , " maio " , " j u n h o " , " julho " , " agosto " , " setembro " , " o u t u b r o " , " novembro " , " dezembro " } ; s t d : : c o u t << d i a << " de " << nomeDoMes [ mes ] << " de " << ano << s t d : : e n d l ; } void Endereco : : impr ( ) const { s t d : : c o u t << r u a << " , " << numero << " " << complemento << s t d : : e n d l << CEP << " " << b a i r r o << " , " << c i d a d e << " , " << e s t a d o << s t d : : e n d l << p a i s << s t d : : e n d l ; } void Pessoa : : impr ( ) const { nome . i m p r ( ) ; en der eco . impr ( ) ; s t d : : c o u t << " D a t a de n a s c i m e n t o : " ; nascimento . impr ( ) ; s t d : : c o u t << e n d l ; } No capítulo 13 veremos uma forma melhor de lidar com entrada e saída de objetos. Podemos agora definir as classes derivadas: 1 2 3 4 c l a s s Aluno : p u b l i c P e s s o a { D a t a i n g r e s s o ; / / Data de i n g r e s s o l o n g NUSP ; / / numero USP public : I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 88 5 6 7 8 9 10 11 12 13 14 Herança Aluno ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a d i , l o n g nu ) : P e s s o a ( n , e , dn ) , i n g r e s s o ( d i ) , NUSP ( nu ) {} void impr ( ) const ; }; v o i d Aluno : : i m p r ( ) c o n s t { Pessoa : : impr ( ) ; s t d : : c o u t << " D a t a de i n g r e s s o : " ; i n g r e s s o . i m p r ( ) ; s t d : : c o u t << s t d : : e n d l ; s t d : : c o u t << "NUSP : " << NUSP << s t d : : e n d l ; } A classe Aluno possui todos os membros de dados e métodos definidos pela classe Pessoa, por ser uma classe derivada da mesma. Além disso, a classe Aluno possui os membros ingresso e NUSP não presentes na classe base, e define uma variante do método impr, alterando a definição apresentada na classe base. Repare como o construtor da classe Aluno faz uso, na lista de inicialização de membros, do construtor da classe base Pessoa, para inicializar os membros herdados. Repare também como a nova definição do método impr faz uso do método impr definido na classe base para lidar com a impressão dos membros herdados, através da sintaxe Pessoa :: impr(). Esta é a técnica tradicional para uso de herança: os métodos desenvolvidos na classe base são reutilizados nas classes derivadas, mesmo quando uma redefinição é necessária. Também é possível a uma classe derivada acessar diretamente os membros da classe base, se isto for necessário ou útil. Um código como: 1 2 3 Aluno a ( / ∗ p a r a m e t r o s do c o n s t r u t o r ∗ / ) ; /∗ . . . ∗/ a . impr ( ) ; irá resultar na chamada do método impr da classe Aluno. Se desejarmos por alguma razão chamar o método impr definido para a classe Pessoa, isto é possível, pois um Aluno é também uma Pessoa. Para isto utilizamos o operador de escopo: 1 2 3 Aluno a ; /∗ . . . ∗/ a . Pessoa : : impr ( ) ; Se um método impr não fosse explicitamente implementado para a classe Aluno, então o método impr da classe Pessoa seria utilizado automaticamente, pois ele é herdado. De forma similar podemos implementar as subclasses de Aluno: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 c l a s s A l u n o G r a d u a c a o : p u b l i c Aluno { Cadeia curso ; public : A l u n o G r a d u a c a o ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a d i , l o n g nu , C a d e i a c ) : Aluno ( n , e , dn , d i , nu ) , c u r s o ( c ) {} void impr ( ) ; }; void AlunoGraduacao : : impr ( ) { Aluno : : i m p r ( ) ; s t d : : c o u t << " C u r s o : " ; c u r s o . i m p r ( ) ; s t d : : c o u t << s t d : : e n d l ; } c l a s s AlunoPos : p u b l i c Aluno { Cadeia o r i e n t a d o r ; public : AlunoPos ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a d i , l o n g nu , C a d e i a o ) : Aluno ( n , e , dn , d i , nu ) , o r i e n t a d o r ( o ) {} void impr ( ) ; }; v o i d AlunoPos : : i m p r ( ) { Aluno : : i m p r ( ) ; s t d : : c o u t << " O r i e n t a d o r : " ; o r i e n t a d o r . i m p r ( ) ; s t d : : c o u t << e n d l ; } Também para a classe Funcionario e suas derivadas: 1 c l a s s Funcionario : public Pessoa { U NIVERSIDADE DE S ÃO PAULO 9.2 Definição de classes derivadas 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 Data c o n t r a t a c a o ; long nFuncional ; float salario ; public : F u n c i o n a r i o ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a dc , l o n g nf , f l o a t s ) : P e s s o a ( n , e , dn ) , c o n t r a t a c a o ( dc ) , n F u n c i o n a l ( n f ) , s a l a r i o ( s ) {} void impr ( ) ; }; void F u n c i o n a r i o : : impr ( ) { Pessoa : : impr ( ) ; s t d : : c o u t << " D a t a de c o n t r a t a c a o : " ; c o n t r a t a c a o . i m p r ( ) ; s t d : : c o u t << e n d l ; s t d : : c o u t << " Numero f u n c i o n a l : " << n F u n c i o n a l << s t d : : e n d l ; s t d : : c o u t << " S a l a r i o : " << s a l a r i o << s t d : : e n d l ; } class Professor : public Funcionario { int referencia ; public : P r o f e s s o r ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a dc , l o n g nf , f l o a t s , i n t r ) : F u n c i o n a r i o ( n , e , dn , dc , nf , s ) , r e f e r e n c i a ( r ) {} void impr ( ) ; }; void P r o f e s s o r : : impr ( ) { F u n c i o n a r i o : : impr ( ) ; s t d : : c o u t << " R e f e r e n c i a : MS−" << r e f e r e n c i a << s t d : : e n d l ; } c l a s s Tecnico : public Funcionario { Cadeia c a t e g o r i a ; Cadeia e s p e c i a l i d a d e ; public : T e c n i c o ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a dc , l o n g nf , f l o a t s , C a d e i a c t , C a d e i a e s ) : F u n c i o n a r i o ( n , e , dn , dc , nf , s ) , c a t e g o r i a ( c t ) , e s p e c i a l i d a d e ( e s ) {} void impr ( ) ; }; void Tecnico : : impr ( ) { F u n c i o n a r i o : : impr ( ) ; s t d : : c o u t << " C a t e g o r i a : " ; c a t e g o r i a . i m p r ( ) ; s t d : : c o u t << s t d : : e n d l ; s t d : : c o u t << " E s p e c i a l i d a d e : " ; e s p e c i a l i d a d e . i m p r ( ) ; s t d : : c o u t << s t d : : e n d l ; } class S e c r e t a r i a : public Funcionario { Cadeia grupo ; public : S e c r e t a r i a ( C a d e i a n , E n d e r e c o e , D a t a dn , D a t a dc , l o n g nf , f l o a t s , C a d e i a g ) : F u n c i o n a r i o ( n , e , dn , dc , nf , s ) , g r u p o ( g ) {} void impr ( ) ; }; void S e c r e t a r i a : : impr ( ) { F u n c i o n a r i o : : impr ( ) ; s t d : : c o u t << " Grupo : " ; g r u p o . i m p r ( ) ; s t d : : c o u t << e n d l ; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 89 90 Herança 9.3 Membros protegidos Devido à encapsulação, os membros privados da classe base não podem ser acessados diretamente pelas classes derivadas. No exemplo acima, o membro nome da classe Pessoa somente pode ser acessado pelos métodos da própria classe Pessoa, e não por métodos de suas classes derivadas. Em alguns casos é desejável permitir a classes derivadas o acesso a alguns membros que não serão acessíveis de forma pública. Isto é possível através do uso do modo de proteção protected. Este é um modo de proteção que se coloca entre o modo private e o modo public. Membros private podem ser acessados apenas por métodos da própria classe. Membros public podem ser acessados de qualquer função. Membros protected podem ser acessados por métodos da própria classe ou de qualquer classe derivada. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class A { int i ; protected : int j ; public : int k ; void f ( ) ; }; void A : : f ( ) { i = 0 ; / / OK j = 1 ; / / OK k = 2 ; / / OK } class B : public A { public : void f ( ) ; }; void B : : f ( ) { i = 0 ; / / E r r o ! i e p r i v a d o da c l a s s e A j = 1 ; / / OK: j e p r o t e c t e d e B e s u b c l a s s e de A k = 2 ; / / OK: k e p u b l i c o } void f ( ) { A a; a . i = 3 ; / / Erro ! a . j = 4 ; / / Erro ! a . k = 5 ; / / OK! } Visto que o programador que desenvolve uma classe não tem controle sobre as classes derivadas, o uso de protected representa um enfraquecimento da encapsulação, e por essa razão deve ser evitado dentro do possível. 9.4 Tipos de herança O tipo de herança que vimos nos exemplos anteriores é chamado de herança pública, e é caracterizado pela presença da palavra reservada public antes do nome da classe base. Além deste, existem outros dois tipos de herança: herança protegida e herança privada, caracterizados respectivamente pelas palavras reservadas protected e private antes do nome da classe base. A diferença entre esses tipos de herança consiste no tipo de proteção assumido pelos membros protegidos e públicos herdados. • Na herança pública, os membros herdados que eram protected na classe base são considerados protected na classe derivada, e os membros public da classe base são considerados public na classe derivada. • Na herança protegida, tanto os membros protected como os membros public herdados da classe base são considerados protected na classe derivada. • Na herança privada, os membros protected e public herdados da classe base são considerados private na classe derivada. U NIVERSIDADE DE S ÃO PAULO 9.4 Tipos de herança 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class A { int i ; protected : int j ; public : int k ; void f ( ) ; }; void A : : f ( ) { i = 0 ; / / OK j = 1 ; / / OK k = 2 ; / / OK } class B : public A { public : void f ( ) ; }; void B : : f ( ) { i = 1 ; / / Erro ! j = 2 ; / / OK k = 3 ; / / OK } class C : public B { public : void f ( ) ; }; void C : : f ( ) { i = 2 ; / / Erro ! j = 3 ; / / OK k = 4 ; / / OK } class D : protected A { public : void f ( ) ; }; void D : : f ( ) { i = 1 ; / / Erro ! j = 2 ; / / OK k = 3 ; / / OK } class E : public D { public : void f ( ) ; }; void E : : f ( ) { i = 2 ; / / Erro ! j = 3 ; / / OK k = 4 ; / / OK } class F : private A { public : void f ( ) ; }; void F : : f ( ) { i = 1 ; / / Erro ! j = 2 ; / / OK! k = 3 ; / / OK I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 91 92 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 Herança } class G : public F { public : void f ( ) ; }; void G : : f ( ) { i = 2 ; / / Erro ! j = 3 ; / / Erro ! k = 4 ; / / Erro ! } void f ( ) { A a; B b; C c; D d; E e; F f ; G g; // Inacessiveis a. i , a. j , b. i , b , j , c , i , c , j // Acessiveis : a . k , b . k , c . k // Inacessiveis : d. i , d. j , d.k , e . i , e . j , e . k // Inacessiveis : f . i , f . j , f .k , g. i , g. j , g. k } Acessibilidade de membros pode também ser ajustada membro a membro, como no exemplo: 1 2 3 4 5 6 7 8 9 10 11 12 class A { int i ; protected : int j ; public : int k ; void f ( ) ; }; class B : private A { public : A : : k ; / / t o r n a membro k p u b l i c em B }; Isto é útil quando se usa herança protegida ou privada. O ajuste de acessibilidade de membro está restrito pela regra de que não é permitido restringir o acesso de um membro que seja permitido pela classe básica nem pode ser permitido o acesso a um membro que não seja acessível na classe básica. Sem esta segunda restrição, seria possível violar o controle de acesso de classes através do uso de herança. Sem a primeira restrição, o uso de herança pública não garantiria a relação de que um objeto da classe derivada poder ser considerado como um objeto da classe base (pois então o membro que não mais é acessível não estaria visível na classe derivada, e um objeto desta classe não poderia então ser utilizado como um objeto da classe base). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { int i ; protected : int j ; public : int k ; }; class B : private A { public : A : : k ; / / OK A : : i ; / / E r r o : nao pode t o r n a r i a c e s s i v e l }; class C : protected A { public : A : : j ; / / E r r o : nao pode t o r n a r j a c e s s i v e l }; O tipo comum de herança é a herança pública, que deve ser preferida. Heranças protegidas e privadas são raramente necessárias (usadas unicamente para reaproveitamento de código). Quando a classe B é derivada por herança protegida ou privada da classe A, então não se diz que todo B é também um objeto da classe A, pois os membros públicos da classe A não estão acessíveis para objetos da classe B, e portanto estes não se comportam como objetos da classe A, isto é, não respondem à interface especificada para objetos da classe A. U NIVERSIDADE DE S ÃO PAULO 9.5 Ponteiros para classe base e derivadas 9.5 93 Ponteiros para classe base e derivadas Normalmente, ponteiros para tipos distintos não são compatíveis, isto é, um ponteiro para um tipo não pode ser utilizado quando um ponteiro para outro tipo é esperado. Como um objeto de uma classe derivada é também um tipo de objeto da classe base, existe a seguinte regra de compatibilidade de ponteiros. Um ponteiro para classe derivada é compatível com um ponteiro para classe base, no sentido de que um ponteiro para classe derivada pode ser utilizado quando um ponteiro para classe base é esperado, desde que a herança seja pública. Por exemplo, considerando a estrutura de classes apresentada anteriormente, as seguintes atribuições são válidas: 1 2 3 4 5 6 7 P e s s o a ∗p ; Aluno ∗ a ; A l u n o G r a d u a c a o ∗g ; AlunoPos ∗o ; Funcionario ∗ f ; P r o f e s s o r ∗ r ; Tecnico ∗ t ; S e c r e t a r i a ∗s ; /∗ . . . ∗/ p = a; p = g; p = o; p = f ; p = r ; p = t ; p = s; a = g; a = o; f = r; f = t; f = s; A idéia aqui é que, como um objeto da classe derivada pode ser considerado como um objeto da classe base (é-um), se ele for acessado por meio de um ponteiro para a classe base ele pode simplesmente se comportar de acordo com o esperado para elementos da classe base. Por exemplo: 1 2 3 4 5 6 P e s s o a ∗p ; Professor ∗r ; /∗ . . . ∗/ r −>i m p r ( ) ; / / S e r a chamado P r o f e s s o r : : i m p r ( ) p = r; p−>i m p r ( ) ; / / S e r a chamado P e s s o a : : i m p r ( ) O uso de um ponteiro para a classe base onde um ponteiro para classe derivada é esperado não é permitido, assim como a mistura de ponteiros para classes derivadas distintas. 9.6 Herança e composição Herança, assim como composição (seção 7.8), é uma relação entre classes. É importante distinguir claramente os dois tipos de relação. A relação de composição, também chamada relação tem-um, ocorre quando um objeto de uma classe tem como um de seus componentes um objeto de outra classe. É o caso da classe Pessoa, definida acima, que tem-um Endereco. Dizemos então que um objeto do tipo Endereco é utilizado para compor (juntamente com outros) um objeto da classe Pessoa. A relação de herança (pública) é também chamada de relação é-um, pois um objeto da classe derivada é também um objeto da classe base. No exemplo anterior, um Funcionario é-uma Pessoa. Uma outra forma de relação entre classes ocorre quando um objeto precisa ser referir a um objeto de outra classe, mas sem que este objeto seja parte dele. Por exemplo, um objeto para armazenar dados de alunos de pós-graduação pode querer indicar o orientador através de uma relação com o objeto que representa o professor. Neste caso, o objeto que representa o aluno pode ter uma referência ou um ponteiro ao objeto do tipo Professor . A diferença entre os dois métodos é que a referência pode ser ajustada apenas no momento da criação do objeto (através da lista de inicialização de membros, seção 7.4), e depois deve permanecer fixa, enquanto que um ponteiro pode ser alterado a qualquer momento. Durante o projeto da estrutura de classes de um programa, estas relações devem ser avaliadas cuidadosamente. 9.7 Herança múltipla Uma classe pode herdar de mais do que uma classe base. Isto é chamado herança múltipla. O uso de herança múltipla é bastante controvertido, sendo muitas vezes argumentado que ela não é necessária. De fato, encontrar exemplos simples que demonstrem a utilidade de herança múltipla não é trivial. Em muitos textos são utilizados exemplos que na verdade deveriam ser resolvidos com o uso de composição. Aqui nos limitaremos a exemplos artificiais, que demonstrarão a sintaxe de herança múltipla na linguagem. Para implementar uma classe com herança múltipla, simplesmente listamos as diversas classes base separadas por vírgulas, formando a lista de classes base: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 94 1 2 3 Herança c l a s s C : p u b l i c A, p u b l i c B { /∗ . . . ∗/ }; Aqui a classe C é derivada simultaneamente das classes A e B. A relação entre a classe C e as classes A e B será exatamente aquela descrita acima entre classe derivada e classe base. Não é difícil de imaginar que a herança múltipla pode introduzir diversos tipos de problemas. Um deles é o da múltipla definição de membros, e que ocorre quando mais do que uma das classes base possuem membros com o mesmo nome. Quando isto ocorre, os membros herdados de nome duplicado somente podem ser referenciados com a ajuda do operador de escopo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class A { public : int x ; }; class B { public : int x ; }; class C : }; /∗ . . . ∗/ { C c; c.x; // c .A : : x ; c .B: : x ; } 9.8 p u b l i c A, p u b l i c B { E r r o : r e f e r e n c i a ambigua a membro x / / OK: u s a x h e r d a d o da c l a s s e A / / OK: u s a x h e r d a d o da c l a s s e B Classes base virtuais Enquanto se usa apenas herança simples, a hierarquia de classes assume a forma de uma árvore. Devido à presença de herança múltipla, a organização em forma de árvore é quebrada, e grafos mais complexos podem ocorrer. Veja o exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { public : int a ; }; c la s s B: public A { public : int b ; }; c la s s C: public A { public : int c ; }; c l a s s D: p u b l i c B, p u b l i c C { public : int d ; }; Objetos da classe D possuem um membro b proveniente da classe B, um membro c proveniente da classe C, mas dois membros a provenientes da classe A, pois esta classe é herdada duplamente através de B e de C. Não só isto introduz uma ambigüidade, mas também uma ambigüidade que não pode ser resolvida com o operador de escopo na forma A::a, pois na verdade ambos os membro a provém da classe A. Neste caso a ambigüidade deveria ser resolvida utilizando o operador de escopo para especificar qual das classes base imediata é desejada: B::a ou C::a. Além disto, ao se tentar utilizar um objeto do tipo D como um objeto do tipo A, o que deveria ser válido visto que a herança é pública, não se sabe a quais dos membros da classe A fazer referência: àqueles herdados através de B ou àqueles herdados através de C? Para resolver este problema se utilizam as chamadas classes base virtuais. 1 2 class A { public : U NIVERSIDADE DE S ÃO PAULO 9.9 Construtores, destruidores e herança 3 4 5 6 7 8 9 10 11 12 13 14 15 16 95 int a ; }; c la s s B: virtual public A { public : int b ; }; c la s s C: virtual public A { public : int c ; }; c l a s s D: p u b l i c B, p u b l i c C { public : int d ; }; Quando uma classe base é especificada como virtual, conforme acima, então apenas uma cópia de seus membros existirá mesmo no caso de herança múltipla. Agora uma referência ao membro a da classe D será sem ambigüidade, pois A é especificada como classe base virtual nas classes base de D. 9.9 Construtores, destruidores e herança Quando um objeto de uma classe derivada é criado, um construtor correspondente deve ser chamado. Como um objeto da classe derivada é também um objeto da classe base, o construtor da classe base também deve ser chamado. Já vimos que podemos passar parâmetros para o construtor da classe base através da lista de inicialização de membros do construtor da classe derivada (pág. 88). Veremos aqui a ordem de chamada dos construtores e destruidores. O primeiro ponto a notar é que a ordem em que os construtores são especificados na lista de inicialização de membros, tanto para classes base como para objetos membro, é imaterial, isto é, não tem nenhum efeito. As regras são as seguintes: • A ordem em que os construtores de membros serão chamados é determinada pela ordem em que os membros foram declarados na declaração da classe. • Os construtores de classes base são chamados antes dos construtores de classes derivadas. • Quando há herança múltipla, os construtores das classes base são chamados na ordem em que as classes aparecem na lista de classes base. Os destruidores são chamados exatamente na ordem inversa dos construtores. Exercícios 1. Qual a vantagem de se utilizar herança no projeto de um programa orientado a objetos? 2. Em que sentido se diz que uma classe derivada é um subconjunto da classe base? 3. Em que sentido se diz que uma classe derivada é uma especialização da classe base? 4. O que se entende por um hierarquia de classes? 5. Projete uma hierarquia de classes envolvendo: quadriláteros, trapézios, paralelogramos, retângulos e quadrados. 6. Explique de que forma o uso de herança ajuda a reaproveitar código. 7. Uma classe derivada pode se comportar em relação a métodos herdados da classe base de diversas formas: (a) utilizar o método herdado sem alteração; (b) fazer uma nova implementação para o método, completamente distinta da herdada; (c) alterar o método, fazendo com que ele execute operações adicionais às executadas pelo método da classe base. Comente como cada um desses comportamentos é implementado em C++. 8. Como se faz a inicialização dos membros herdados da classe base na classe derivada? I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 96 Herança 9. Quando uma classe derivada redefine um método da classe base, existe algum modo pelo qual o método herdado seja chamado sobre certo objeto da classe derivada, ao invés do novo método? 10. A regra básica de C++ é de que os membros privados da classe base não podem ser acessados diretamente pela classe derivada. Qual a razão desta regra? 11. Qual o significado da declaração de um membro como protected? 12. Quando a declaração de um membro como protected é indicada? 13. Qual é a desvantagem de declarar membros protected em uma classe? (Do ponto de vista de engenharia de software.) 14. Qual a diferença entre os tipos de heranças pública, protegida e privada? 15. Por que razão se diz que apenas a herança pública implica numa relação de é-um? 16. A acessibilidade de membros herdados da classe base pode ser ajustada membro-a-membro. De que forma isso pode ser feito em C++? Quais são as regras que limitam esse tipo de ajuste? Explique por que razão essas regras são necessárias. 17. Por que um ponteiro para classe derivada pode ser atribuído a um ponteiro para classe base pública? Por que o contrário não é permitido? Por que, para a atribuição ser permitida, a classe base deve ser pública? 18. Qual a diferença entre herança e composição? Explique qual a importância da consideração de relações de herança e composição durante o projeto de um programa orientado a objetos. 19. O que se entende por herança múltipla? 20. O que acontece quando membros herdados de duas ou mais classes base distintas têm o mesmo nome? Como eles podem então ser distinguidos? 21. O que se entende por classe base virtual? Que tipo de problema ela resolve? 22. Durante a construção de um objeto de uma classe derivada, o construtor da classe base é executado antes do construtor da classe derivada. Você pode indicar uma razão para que isso seja assim? U NIVERSIDADE DE S ÃO PAULO Capítulo 10 Polimorfismo Polimorfismo é o terceiro ponto fundamental na programação orientada a objetos, complementando encapsulação e herança. O termo polimorfismo significa que uma mesma entidade pode assumir diversas formas. Em orientação a objetos o termo é utilizado para indicar que uma mesma mensagem pode resultar na chamada de distintos métodos, de acordo com a classe do objeto à qual a mensagem foi enviada, sendo a decisão sobre qual método é chamado deixada para o momento da execução. Com as técnicas vistas até o momento isto não é possível, pois o método a ser chamado é sempre decidido em tempo de compilação de acordo com a classe da variável utilizada. Para possibilitar polimorfismo, devemos nos utilizar de funções virtuais (métodos virtuais). 10.1 Funções virtuais Vimos no capítulo 9 uma hierarquia de classes para lidar com dados sobre pessoas em uma universidade. Todas as classes da hierarquia possuem um método impr próprio, responsável pela impressão dos dados correspondentes. Suponha que desejamos montar uma lista com dados sobre pessoas de qualquer dos tipos que satisfaçam um determinado critério, digamos que sejam aniversariantes num mês específico. A lista não pode se constituir de objetos de uma das classes, pois os objetos das diversas classes derivadas não são compatíveis entre si. No entanto, devido à compatibilidade de ponteiros de classes derivadas e classe base, a lista geral pode ser construída como uma lista de ponteiros para a classe base geral Pessoa. Ponteiros para objetos de quaisquer das classes derivadas podem então ser armazenados nessa lista. Após montada a lista, queremos então imprimir os dados de todos os seus elementos, como no trecho de código abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 struct ListaPessoa { Pessoa ∗ valor ; L i s t a P e s s o a ∗ prox ; }; / ∗ . . . ∗ / / / Cria l i s t a e i n s e r e os e l e m e n t o s ListaPessoa ∗ corrente , ∗ cabeca ; /∗ . . . ∗/ c o r r e n t e = cabeca ; while ( c o r r e n t e != 0) { c o r r e n t e −>v a l o r −>i m p r ( ) ; c o r r e n t e = c o r r e n t e −>p r o x ; } Aqui surge um problema. Como todos os elementos da lista são ponteiros para Pessoa, e como a resolução de qual o método a ser chamado é tomada em tempo de compilação, será sempre chamado o método Pessoa :: impr, independente da verdadeira classe do objeto apontado. Suponha que o primeiro elemento da lista seja um AlunoGraduacao, o segundo um Tecnico e o terceiro um Professor . Para os três serão sempre impressos os campos de Pessoa, e nunca os campos próprios das classes. Este problema pode ser resolvido declarando o método impr como virtual. Isto é feito pelo uso da palavra reservada virtual na declaração do método na classe base. Assim, alteramos a declaração da classe Pessoa como segue: 1 2 3 4 5 c l a s s Pessoa { /∗ . . . ∗/ public : /∗ . . . ∗/ v i r t u a l void impr ( ) const ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 98 /∗ . . . ∗/ 6 7 Polimorfismo }; Isto indica ao compilador que a escolha do método a ser chamado para uma dada mensagem deve ser realizada em tempo de execução. Assim, no trecho de código que percorre a lista, e considerando o exemplo de objetos na lista anterior, serão chamados os métodos AlunoGraduacao::impr, Tecnico :: Impr e Professor :: impr respectivamente. Isto é o chamado polimorfismo, pois a mesma mensagem no código da linha 1 c o r r e n t e −>v a l o r −>i m p r ( ) ; resulta em chamadas para diferentes métodos, de acordo com a classe do objeto efetivamente apontado pelo ponteiro corrente −>valor. A grande utilidade de polimorfismo consiste justamente em permitir que um trecho de código se adapte em tempo de execução às diversas classes dos objetos existentes, desde que todas as classes sejam derivadas de uma classe comum. Assim podem ser criadas estruturas contendo objetos de diferentes classes derivadas de uma mesma classe base, e as operações executadas sobre os elementos dessa estrutura se adaptarão automaticamente às classes específicas dos objetos armazenados. Note que novas classes, derivadas da classe base geral, podem a qualquer momento ser incluídas, inclusive após o código que lida com a estrutura estar completo, e este não necessitará alterações (não necessitará nem ao menos ser recompilado). Algumas observações sobre métodos virtuais: • O atributo de virtual declarado numa classe base se propaga por todas as classes derivadas. Assim, no exemplo anterior foi suficiente declarar impr virtual na classe Pessoa, e o atributo já se propagou para as classes Aluno e Funcionario e suas subclasses. • Se uma classe derivada não redefine um método virtual, o método da classe base imediata é utilizado, como no caso de métodos não virtuais. • Os métodos virtuais redefinidos devem possuir sempre o mesmo número e os mesmos tipos de parâmetro, e mesmo tipo de retorno. Uma diferença aqui é considerada um erro sintático. O comportamento é diferente em relação a métodos não virtuais, que podem redefinir a interface ou valor de retorno em relação aos métodos das classes base. • Polimorfismo em C++ é sempre associado com ponteiros ou referências. Quando se usam objetos, a escolha do método é sempre realizada em tempo de compilação. Quando se usam ponteiros ou referências para objetos a escolha é realizada em tempo de execução se o método for virtual. • Outros nomes usados para polimorfismo são ligação dinâmica, ou ligação tardia, implicando que a ligação entre mensagem e método é executada dinamicamente (de acordo com as condições do programa sendo executado), ou é executada mais tarde em relação à compilação). 10.2 Classes base abstratas Considere novamente a hierarquia de classes que estamos utilizando como exemplo. Nota-se facilmente que as classes Pessoa, Aluno e Funcionario são utilizadas apenas como classes base, sem que se espere que algum objeto dessas classes realmente venha a existir no sistema. Existirão instâncias apenas das classes derivadas. Da forma como essas classes foram definidas nada impede que geremos alguma instância delas. Em alguns sistemas, pode ser a intenção do programador que a criação de instâncias de alguma classe base seja proibida. Isto é conseguido pela declaração de classes base abstratas. Em C++ uma classe base é abstrata se ela contém pelo menos um método puramente virtual. Um método puramente virtual é um método declarado como virtual, mas para o qual nenhuma implementação é fornecida. Isto é possível através da sintaxe abaixo: 1 2 3 4 5 6 7 8 c l a s s Pessoa { /∗ . . . ∗/ public : /∗ . . . ∗/ v i r t u a l void impr ( ) const = 0 ; /∗ . . . ∗/ }; / ∗ . . . ∗ / / / Nenhuma i m p l e m e n t a c a o p a r a P e s s o a : : i m p r Este trecho de código declara o método impr da classe Pessoa como puramente virtual, o que resulta em que a classe Pessoa é considerada uma classe base abstrata, e o método impr é considerado virtual nas classes derivadas. Obviamente para o nosso exemplo da classe Pessoa isto implicaria em refazer a implementação de todos os métodos impr das classes U NIVERSIDADE DE S ÃO PAULO 10.3 Destruidores virtuais 99 derivadas. Na verdade, já que as classes Pessoa, Aluno e Funcionario têm no código a função de classes abstratas, esta reescrita é recomendada por boas técnicas de programação. 10.3 Destruidores virtuais Quando lidamos com estruturas com ponteiros para classe base, que irão apontar para objetos de classes derivadas, para permitir polimorfismo, temos um problema na chamada do destruidor. Como o ponteiro é para a classe base, uma chamada para o destruidor implicaria em uma chamada para o destruidor da classe base, e não o da classe à qual o objeto realmente pertence. Por exemplo: 1 2 3 4 5 6 7 8 Pessoa ∗ alguns [ 3 ] ; a l g u n s [ 0 ] = new A l u n o G r a d u a c a o ( / ∗ . . . ∗ / ) ; a l g u n s [ 1 ] = new T e c n i c o ( / ∗ . . . ∗ / ) ; a l g u n s [ 2 ] = new P r o f e s s o r ( / ∗ . . . ∗ / ) ; /∗ . . . ∗/ d e l e t e a l g u n s [ 0 ] ; / / ~ P e s s o a ( ) chamado d e l e t e a l g u n s [ 1 ] ; / / ~ P e s s o a ( ) chamado d e l e t e a l g u n s [ 2 ] ; / / ~ P e s s o a ( ) chamado Para solucionar o problema, devemos declarar o destruidor da classe base como virtual: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 c l a s s Pessoa { /∗ . . . ∗/ public : /∗ . . . ∗/ v i r t u a l ~Pessoa ( ) ; }; /∗ . . . ∗/ Pessoa ∗ alguns [ 3 ] ; a l g u n s [ 0 ] = new A l u n o G r a d u a c a o ( / ∗ . . . ∗ / ) ; a l g u n s [ 1 ] = new T e c n i c o ( / ∗ . . . ∗ / ) ; a l g u n s [ 2 ] = new P r o f e s s o r ( / ∗ . . . ∗ / ) ; /∗ . . . ∗/ d e l e t e a l g u n s [ 0 ] ; / / ~ AlunoGraduacao ( ) chamado d e l e t e a l g u n s [ 1 ] ; / / ~ T e c n i c o ( ) chamado d e l e t e a l g u n s [ 2 ] ; / / ~ P r o f e s s o r ( ) chamado Os destruidores das classes derivadas ficam então automaticamente virtuais, apesar de possuírem nomes distintos. Com relação a destruidores virtuais é útil se valer da seguinte regra: Sempre que uma classe dispuser de um método virtual, forneça um destruidor virtual para a classe, mesmo que esta não necessite um destruidor. 10.4 Ponteiros para membros Vimos na seção 4.9 que é possível declarar ponteiros para funções, que podem ser guardados em variáveis do tipo correto e manuseados como outros valores (por exemplo, passando-os para outras funções como parâmetros). Métodos são também funções e pode ser útil lidar com métodos dessa forma. No entanto, métodos apresentam algumas complicações adicionais: 1. Um método é definido apenas no escopo de uma classe. 2. Um método deve ser chamado sobre um objeto. 3. O método desejado pode ser polimórfico de forma que o método a ser chamado depende da classe exata do objeto que receberá a mensagem. Para lidar com esses problema, C++ define a noção de ponteiro para membro. Suponha uma classe Tocador, com a seguinte interface: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 100 1 2 3 4 5 6 7 8 9 10 Polimorfismo c l a s s Tocador { public : v i r t u a l void l i g a ( ) ; v i r t u a l void d e s l i g a ( ) ; v i r t u a l void pausa ( ) ; v i r t u a l void recomeca ( ) ; v i r t u a l void proxima ( ) ; v i r t u a l void a n t e r i o r ( ) ; v i r t u a l void p r i m e i r a ( ) ; }; // // // // // // // Liga o aparelho desliga suspende a execucao recomeca s u s p e n s o proxima t r i l h a trilha anterior primeira t r i l h a Pode-se lidar com os métodos de classe através de ponteiros como no trecho de código abaixo: 1 typedef void ( Tocador : : ∗ Operacao ) ( ) ; 2 3 4 5 6 7 8 void { // // op } i n t e r f a c e _ u s u a r i o ( O p e r a c a o &op ) v e r i f i c a com u s u a r i o o que f a z e r por exemplo : pausa : = &T o c a d o r : : p a u s a ; 9 10 11 12 13 i n t main ( ) { T o c a d o r t ; O p e r a c a o op ; / / muitas coisas . . . 14 i n t e r f a c e _ u s u a r i o ( op ) ; ( t . ∗ op ) ( ) ; / / E x e c u t a a o p e r a c a o r e q u i s i t a d a 15 16 17 / / muitas outras coisas . . . return 0; 18 19 20 } Se temos um ponteiro para o objeto ao qual desejamos enviar a mensagem podemos usar a sintaxe ( pt−>∗op)(). Suponhamos agora que temos uma classe derivada de Tocador: 1 2 3 4 5 6 7 8 9 10 c l a s s Tocador_CD : public : void l i g a ( ) ; void d e s l i g a ( ) ; void pausa ( ) ; void recomeca ( ) ; void proxima ( ) ; void a n t e r i o r ( ) ; void p r i m e i r a ( ) ; }; public Tocador { / / a d a p t a d o s p a r a t o c a d o r de CD // // // // // // Podemos reutilizar o código acima com objetos dessa classe: 1 2 Tocador_CD ∗ c d p l a y e r ; c d p l a y e r = new Tocador_CD ( ) ; / / p o n t e i r o , so para v a r i a r . . . 3 4 5 i n t e r f a c e _ u s u a r i o ( op ) ; ( c d p l a y e r −>∗op ) ( ) ; / / e x e c u t a a o p e r a c a o Nesse código, o método pausa chamado será o método da classe Tocador_CD, isto é, o compilador gera código que leva em consideração o polimorfismo. Exercícios 1. O que se entende por polimorfismo em orientação a objetos? 2. Qual a relação entre funções virtuais e polimorfismo? 3. Em que circunstâncias o polimorfismo pode facilitar a inclusão de novas características em um software já existente? U NIVERSIDADE DE S ÃO PAULO 10.4 Ponteiros para membros 101 4. Por que razão métodos virtuais redefinidos numa classe derivada devem possuir sempre o mesmo número e tipo de parâmetros do que os métodos originais? 5. O que é uma classe base abstrata? O que é um método puramente virtual? 6. O que são destruidores virtuais? Que tipo de problema eles resolvem? Quando se deve declarar um destruidor como virtual? 7. Desenvolva um programa para lidar com formas, bi e tridimensionais. As formas devem ser organizadas em uma hierarquia de classes, tendo Figura como classe base, Figura2D e Figura3D como classes derivadas de Figura. Essas classes devem ser classes base abstratas. Classes concretas seriam então Retangulo, Quadradro, Triangulo, Circulo, Cubo, Tetraedro , Isocaedro, etc. As classes devem dispor de métodos para mover, rodar, ampliar, reduzir e desenhar a figura (o método para desenhar precisa apenas imprimir o tipo, tamanho e posição da figura na tela). O programa deve permitir ao usuário formar um desenho incluindo diversas dessas figuras, sendo que o programa pede ao usuário para escolher o tipo da figura a inserir, seu tamanho, posição, etc. O usuário também deve poder alterar as características das figuras (tamanho, posição, etc.), ou mandar desenhar tudo o que já foi inserido. Use polimorfismo para simplificar o desenvolvimento do programa. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 102 Polimorfismo U NIVERSIDADE DE S ÃO PAULO Capítulo 11 Templates Polimorfismo possibilita uma certa forma de generalidade de código. Estruturas de dados que armazenam elementos de uma classe base ou funções que tenham como parâmetros ponteiros ou referências para uma classe base podem ser utilizadas, através de polimorfismo, para objetos de quaisquer classes derivadas dessa classe base. A generalização no entanto permanece restrita a classes derivadas de uma mesma classe base. Podemos conseguir uma generalização que seja útil para tipos não relacionados através do uso de templates. Neste sentido templates têm uma certa similaridade com polimorfismo. Entretanto eles diferem radicalmente em seus usos, pois polimorfismo implica em decidir qual método chamar em tempo de execução, enquanto que todas as decisões relativas a templates são estáticas, isto é, realizadas pelo compilador. Assim, os campos de uso dos dois tipos de generalização são claramente distintos. 11.1 Templates de funções É extremamente comum que um código desenvolvido para uma função que lida com certo tipo de dados seja exatamente idêntico ao código utilizado para um outro tipo de dados, sendo a diferença exatamente apenas os tipos envolvidos. Por exemplo, para encontrar o máximo de dois números podemos definir a função abaixo: 1 2 3 4 5 d o u b l e max ( d o u b l e a , d o u b l e b ) { i f ( a >= b ) r e t u r n a ; e l s e return b ; } Devido à conversão automática de tipos, esta função pode também ser utilizada para outros tipos, como int, long e float . No entanto, as operações com double são geralmente mais lentas, e se a função for ser chamada muitas vezes, as diversas conversões envolvidas, juntamente com as operações mais lentas em double, podem resultar pouco eficientes. Certamente podemos nos utilizar de sobrecarga de nomes de funções: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 d o u b l e max ( d o u b l e a , d o u b l e b ) { i f ( a >= b ) r e t u r n a ; e l s e return b ; } f l o a t max ( f l o a t a , f l o a t b ) { i f ( a >= b ) r e t u r n a ; e l s e return b ; } i n t max ( i n t a , i n t b ) { i f ( a >= b ) r e t u r n a ; e l s e return b ; } No entanto, o código de todas as funções é exatamente igual, divergindo apenas pelos tipos utilizados. O fato de necessitar duplicar os códigos não é bom, pois significa que uma mudança necessária no código (bastante provável para funções mais complexas), deveria ser realizada em diversos pontos, dificultando a manutenção. Este problema pode ser resolvido definindo um template de função: I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 104 1 2 3 4 5 Templates t e m p l a t e < c l a s s t i p o > t i p o max ( t i p o a , t i p o b ) { i f ( a >= b ) r e t u r n a ; e l s e return b ; } Este código define um template de função parametrizada por tipo . O tipo da função será decidido por sobrecarga: 1 2 3 4 5 6 7 8 9 int i , j ; float a , b; char c , d ; double e , f ; /∗ . . . ∗/ i = max ( i , j ) ; a = max ( a , b ) ; d = max ( c , d ) ; e = max ( e , f ) ; // // // // chama chama chama chama f u n c a o i n t max ( i n t , i n t ) f l o a t max ( f l o a t , f l o a t ) c h a r max ( char , c h a r ) d o u b l e max ( d o u b l e , d o u b l e ) Templates de função são também chamados funções parametrizadas, ou funções genéricas. Alguns pontos importantes a notar com relação a templates de funções são: • O compilador gera uma função diferente para cada combinação de tipos necessária. Não existe reaproveitamento de código executável, mas apenas de código fonte. • A resolução de tipos é determinada pelas regras de sobrecarga (seção 3.9). Isto implica que o tipo de retorno não é utilizado para a determinação da função a ser chamada. • Tipos definidos pelo usuário podem também ser utilizados, desde que esses tipos possuam sobrecarga para todas as funções e operadores usados no template. No nosso exemplo, o template max pode ser utilizado para qualquer tipo definido pelo usuário que sobrecarregue o operador >=. Com a definição apresentada acima, a chamada seguinte: 1 2 3 4 int k ; double x , y ; /∗ . . . ∗/ y = max ( x , k ) ; / / Problemas ! não é válida, pois não foi definida uma função max capaz de aceitar um double e um int. Para resolver esse problema, deve ser realizada a conversão explícita de um dos parâmetros ou então deve ser especificado explícitamente o tipo do template: 1 2 y = max ( x , d o u b l e ( i ) ) ; y = max< double > ( x , i ) ; / / agora os d o i s parâmetros são double / / max ( d o u b l e , d o u b l e ) usada , i c o n v e r t i d o i m p l i c i t a m e n t e . 11.2 Templates de classe Da mesma forma que temos funções parametrizadas, podemos também ter tipos parametrizados. Isto é conseguido em C++ através da definição de templates de classes. Os templates de classes permitem a definição de classes genéricas, que poderão possuir variantes que lidam com diversos tipos de membros. Um exemplo seria uma classe de pilhas. Podemos querer implementar pilhas que contenham diversos tipos de dados. Nesta caso, devemos definir um template de pilha, algo como: 1 2 3 4 5 6 7 8 9 10 11 12 t e m p l a t e < c l a s s T> c l a s s P i l h a { T ∗valores ; i n t max , t o p o ; public : Pilha ( int m = 100); v o i d p u s h ( c o n s t T &v ) ; T pop ( ) ; bool v a z i a ( ) const ; bool l o t a d a ( ) const ; ~Pilha ( ) ; }; t e m p l a t e < c l a s s T> P i l h a <T > : : P i l h a ( i n t m) U NIVERSIDADE DE S ÃO PAULO 11.2 Templates de classe 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 105 { max = m > 0 ? m : 1 ; v a l o r e s = new T [ max ] ; t o p o = −1; } t e m p l a t e < c l a s s T> v o i d P i l h a <T > : : p u s h ( c o n s t T &v ) { if (! lotada ()) v a l o r e s [++ t o p o ] = v ; } t e m p l a t e < c l a s s T> T P i l h a <T > : : pop ( ) { i f (! vazia ( ) ) r e t u r n v a l o r e s [ t o p o −−]; } t e m p l a t e < c l a s s T> b o o l P i l h a <T > : : v a z i a ( ) c o n s t { r e t u r n ( t o p o == −1); } t e m p l a t e < c l a s s T> b o o l P i l h a <T > : : l o t a d a ( ) c o n s t { r e t u r n ( t o p o == max −1); } Note que como a classe é parametrizada, todos os métodos da classe são parametrizados. As regras de sobrecarga de nomes de funções não podem ser aplicadas a nomes de classe, e portanto uma forma diferente de determinar o tipo a ser efetivamente utilizado por uma classe deve existir. Isto é feito como no código abaixo: 1 2 3 Pilha <int > pi ( 3 ) ; / / uma p i l h a de i n t P i l h a < double > pd ( 1 0 ) ; / / uma p i l h a de d o u b l e P i l h a < AlunoPos > pa ; / / uma p i l h a de A l u n o P o s Além de parametrizadas por tipos, as classes podem também ser parametrizadas por valores. Por exemplo, podemos passar o número máximo de elementos a serem armazenados na pilha através de um parâmetro para o template, ao invés de um parâmetro para o construtor, como acima: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 t e m p l a t e < c l a s s T , i n t max> c l a s s P i l h a { T v a l o r e s [ max ] ; int topo ; public : P i l h a ( ) { t o p o = −1; } ; /∗ . . . ∗/ }; /∗ . . . ∗/ t e m p l a t e < c l a s s T , i n t max> i n t P i l h a <T , max > : : l o t a d a ( ) { r e t u r n ( t o p o == max −1); } /∗ . . . ∗/ P i l h a < i n t ,3 > p i ; P i l h a < double ,10 > pd ; P i l h a < AlunoPos ,100 > pa ; /∗ . . . ∗/ Templates também podem ter valores assumidos para seus parâmetros. Se o parâmetro é do tipo class , como no primeiro parâmetro do template Pilha acima, o valor assumido fornecido deve indicar o tipo a ser escolhido caso não seja especificado um tipo pelo usuário; se o parâmetro for um valor, como no caso do segundo parâmetro de Pilha, o valor a ser assumido deve ser indicado. 1 2 3 t e m p l a t e < c l a s s T = i n t , i n t max = 100 > c l a s s P i l h a { / / idem ao c o d i g o a n t e r i o r }; 4 5 6 7 /∗ . . . ∗/ P i l h a <> p i ; / / p i g u a r d a a t e 100 e l e m e n t o s i n t P i l h a < double ,10 > pd ; / / pd g u a r d a a t e 10 e l e m e n t o s d o u b l e I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 106 8 Templates P i l h a < AlunoPos > pa ; / / pa g u a r d a a t e 100 e l e m e n t o s A l u n o P o s Um método de um template pode por sua vez ser também um template. Por exemplo, podemos declarar um construtor que cria uma pilha de um novo tipo a partir de uma pilha dada, desde que haja conversão definida entre o tipo da pilha antiga e o tipo da pilha nova da seguinte forma: 1 2 3 4 5 6 7 t e m p l a t e < c l a s s T = i n t , i n t max = 100 > c l a s s P i l h a { / / idem ao c o d i g o a n t e r i o r public : / / idem ao c o d i g o a n t e r i o r template < c l a s s , int > f r i e n d c l a s s P i l h a ; t e m p l a t e < c l a s s OT , i n t omax> P i l h a ( c o n s t P i l h a <OT , omax> &op ) ; }; 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 t e m p l a t e < c l a s s T , i n t max> t e m p l a t e < c l a s s OT , i n t omax> P i l h a <T , max > : : P i l h a ( c o n s t P i l h a <OT , omax> &op ) { a s s e r t ( op . t o p o <= max ) ; t o p o = op . t o p o ; f o r ( i n t i = 0 ; i < t o p o ; i ++) v a l o r e s [ i ] = s t a t i c _ c a s t <T> ( op . v a l o r e s [ i ] ) ; } i n t main ( ) { // ... P i l h a < i n t , 10> x ; P i l h a < double , 20> y = x ; / / C o n v e r t e P i l h a < i n t ,10 > p a r a P i l h a <d o u b l e ,20 > // ... } A principal alteração é a introdução do construtor (declarado na linha 6) que será usado para a conversão de Pilha<T1,m1> para Pilha<T2,m2>. Note que esse construtor é duplamente um template: por ser membro de uma classe template e por ser ele mesmo um template. Isso se torna aparente na definição do método, que necessita duas declarações de template<>. Em princípio, cada classe declarada pela especificação dos tipos de um template é uma classe distinta. Assim, Pilha<int,10> e Pilha<double,20> são duas classes completamente independentes. Deste modo, elas estão sujeitas às regras de acesso a membros e uma delas não pode acessar os membros privados da outra. A declaração da linha 5 é usada para indicar que todas as classes do tipo Pilha<T,m> são amigas entre si. 11.3 Templates e compilação separada Quando um template é adaptado para um novo tipo (ou conjunto de tipos), uma nova função ou classe deve ser criada. Esta é uma operação que precisa ser realizada pelo compilador. Desta forma, o código do template deve estar disponível durante a compilação do código do cliente que usa o template. Por esta razão, os códigos de implementação dos templates são incluídos nos arquivos de cabeçalho que os definem, isto é, não se usa compilação separada dos códigos de implementação dos templates. A padrão que define a linguagem C++ especifica também a possibilidade de compilação separada, caso em que todos os métodos implementados num arquivo a ser compilado separadamente devem ser precedidos pela palavra reservada export. Infelizmente, isto não é implementado em muitos compiladores. 11.4 Amigos de templates Quando declaramos um template de classe, todos os métodos dessa classe são automaticamente entendidos como um template. Isto é necessário pois esses métodos devem ser adaptados ao tipo especificado no parâmetro do template. Por exemplo, no código inicial do template de pilha temos: 1 2 3 4 5 6 7 t e m p l a t e < c l a s s T> c l a s s P i l h a { // ... public : // ... v o i d p u s h ( c o n s t T &v ) ; // ... }; U NIVERSIDADE DE S ÃO PAULO 11.4 Amigos de templates 8 9 10 11 12 13 107 // ... t e m p l a t e < c l a s s T> v o i d P i l h a <T > : : p u s h ( c o n s t T &v ) { if (! lotada ()) v a l o r e s [++ t o p o ] = v ; } Apesar do método push não ser declarado explicitamente como template, ele é definido como template. A declaração de Pilha como template já declara implicitamente todos os seu métodos como templates. Se desejamos declarar uma função ou operador amigo de um template de classe, então essa função ou esse operador possivelmente deverão também ser um template, pois necessitarão se adaptar ao tipo do parâmetro da classe. No entanto, como os amigos não são membros da classe (Seção 7.9), eles não são assumidos implicitamente como templates. Para indicar que amigos serão templates nos mesmos parâmetros que a classe, deve ser colocado <> após o nome da função ou operador. Por exemplo, se desejamos implementar um operador que permita comparar duas pilhas com a definição de que duas pilhas são consideradas iguais se elas têm exatamente os mesmos elementos na mesma ordem, então podemos definir: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 t e m p l a t e < c l a s s T> c l a s s P i l h a ; t e m p l a t e < c l a s s T> b o o l o p e r a t o r ==( c o n s t P i l h a <T>&, c o n s t P i l h a <T>&); t e m p l a t e < c l a s s T> c l a s s P i l h a { // ... public : // ... f r i e n d b o o l o p e r a t o r ==<> ( c o n s t P i l h a &a , c o n s t P i l h a &b ) ; // ... }; t e m p l a t e < c l a s s T> b o o l o p e r a t o r == ( c o n s t P i l h a <T> &a , c o n s t P i l h a <T> &b ) { i f ( a . topo != b . topo ) return f a l s e ; f o r ( s i z e _ t i = 0 ; i < a . t o p o ; i ++) i f ( a . v a l o r e s [ i ] != b . v a l o r e s [ i ] ) return f a l s e ; return true ; } As linhas 10 a 15 definem um operador template operator==<T> que compara valores Pilha<T>. Na linha 7 esse operador é declarado friend da classe Pilha<T>; a notação <> após o nome do operador (ou função) indica que eles são um template. A linguagem C++ exige que um template que vai ser declarado friend de um template de classe tenha sido previamente declarado. Isso é realizado na linha 2. Como a declaração de operator==<T> na linha 2 usa o fato de que existe um template de classe Pilha<T>, esse fato deve ser informado ao compilador, o que é feito através da declaração prévia na linha 1. Para compreender melhor as interações entre templates e funções amigas, considere o exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 t e m p l a t e < c l a s s T> c l a s s A; t e m p l a t e < c l a s s T> v o i d g ( c o n s t A<T> & ) ; t e m p l a t e < c l a s s T> c l a s s A { // ... public : // ... friend void f ( ) ; f r i e n d v o i d g < >( c o n s t A<T> &a ) ; t e m p l a t e < c l a s s C> b o o l h ( c o n s t C &c ) ; }; void f ( ) { /∗ . . . ∗/ } t e m p l a t e < c l a s s T> v o i d g ( c o n s t A<T> &a ) { / ∗ . . . ∗ / } t e m p l a t e < c l a s s C> b o o l h ( c o n s t C &c ) { / ∗ . . . ∗ / } Neste exemplo, a função f () é amiga de todas as classes especializadas a partir do template dado, isto é, de A<T> para todos os T; o template g é amigo de A mas apenas quando seus parâmetros forem idênticos, isto é, g<T>() é amiga de A<T>, mas não de A<T2> para T2 diferente de T; no caso de h, todas as funções h<T1>() são amigas de todas as classes A<T2>, não importa os valores de T1 e T2. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 108 11.5 Templates Especialização de templates Ocorre freqüentemente que dispomos de um algoritmo genérico que funciona para diversos tipos de dados, e portanto pode ser implementado como um template, mas ocorre que ele não faz sentido para alguns tipos de dado específicos. Considere por exemplo o seguinte template para retornar o máximo de dois valores: 1 2 3 4 t e m p l a t e < c l a s s T> T max ( T a , T b ) { i f ( a > b ) return a ; e l s e return b ; } Este código funciona bem para todos os tipos básicos e as classes que façam sobrecarga do operador >. No entanto, suponha que ele seja usado da seguinte forma: 1 2 c o n s t char ∗ a1 = " Ana P a u l a " , ∗ a2 = " Ana M a r i a " ; c o n s t char ∗m = max ( a1 , a2 ) ; O compilador irá criar uma função max(const char∗,const char∗) que infelizmente não realizará o esperado, pois fará apenas uma comparação dos valores dos ponteiros (e não das cadeias apontadas). Certamente para const char∗ precisamos uma implementação especial, o que é chamado uma especialização do template: 1 2 3 4 5 6 7 8 9 t e m p l a t e <> c o n s t char ∗ max< c o n s t char ∗ >( c o n s t char ∗ a , c o n s t char ∗ b ) { char ∗ r e t ; c o n s t char ∗ t h e o n e ; i f ( strcmp ( a , b ) >0) theone = a ; e l s e theone = b ; r e t = new char [ s t r l e n ( t h e o n e ) + 1 ] ; strcpy ( ret , theone ) ; return r e t ; } A sintaxe template<> indica que a função que está sendo declarada é a especialização de um template pré definido (as especializações devem sempre ser definidas após os casos gerais). O tipo da especialização pode, neste caso, ser deduzido a partir do tipo dos parâmetros da função. Assim, a sintaxe da linha 1 acima pode ser simplificada para: 1 t e m p l a t e <> c o n s t char ∗ max ( c o n s t char ∗ a , c o n s t char ∗ b ) Também é possível declarar especializações para templates de classes, da mesma forma que para templates de funções. Outra possibilidade é definir uma função com o mesmo nome de um template, mas com tipos específicos. O compilador sempre procura a declaração que seja mais específica e que se ajuste às necessidades do local de uso. Por exemplo, na chamada max(a1,a2) do exemplo de cadeias anterior, o compilador usará a especialização max<const char ∗>(), pois ela se adapta às necessidades e é mais específica do que a definição max<T>(). 11.6 Templates e herança Templates podem ser usado conjuntamente com herança. É possível derivar uma classe de um template, derivar um template de uma classe normal ou derivar um template de outro. 1 2 3 # include <iostream > # include < vector > # include < s t r i n g > 4 5 6 t e m p l a t e < c l a s s C> i n l i n e v o i d d e s t r o y (C c o n t , s i z e _ t i ) { c o n t [ i ] = ∗ ( c o n t . end ( ) − 1 ) ; c o n t . p o p _ b a c k ( ) ; } 7 8 9 10 11 12 13 c l a s s Pessoa { s t d : : s t r i n g _nome ; public : P e s s o a ( c o n s t s t d : : s t r i n g &nome ) : _nome ( nome ) {} s t d : : s t r i n g nome ( ) { r e t u r n _nome ; } }; 14 15 16 17 t e m p l a t e < c l a s s T> c l a s s C o l e c i o n a d o r : p u b l i c P e s s o a { s t d : : v e c t o r <T> _ c o l ; public : U NIVERSIDADE DE S ÃO PAULO 11.6 Templates e herança C o l e c i o n a d o r ( c o n s t s t d : : s t r i n g &nome ) : P e s s o a ( nome ) {} s i z e _ t quantos ( ) { return _col . s i z e ( ) ; } v o i d novo ( c o n s t T &n ) { _ c o l . p u s h _ b a c k ( n ) ; } T &o p e r a t o r [ ] ( s i z e _ t i ) { r e t u r n _ c o l [ i ] ; } c o n s t T &o p e r a t o r [ ] ( s i z e _ t i ) c o n s t { r e t u r n _ c o l [ i ] ; } void perde ( s i z e _ t i ) { d e s t r o y ( _col , i ) ; } 18 19 20 21 22 23 24 109 }; 25 26 c l a s s Selo { /∗ . . . ∗/ }; 27 28 29 30 31 32 33 34 35 36 37 c l a s s F i l a t e l i s t a : publ ic Colecionador <Selo > { std : : vector <int > _paixao ; public : F i l a t e l i s t a ( c o n s t s t d : : s t r i n g &nome ) : C o l e c i o n a d o r < S e l o > ( nome ) {} v o i d novo ( c o n s t S e l o &s , i n t p = 1 ) { C o l e c i o n a d o r < S e l o > : : novo ( s ) ; _ p a i x a o . p u s h _ b a c k ( p ) ; } void perde ( s i z e _ t i ) { Colecionador <Selo > : : perde ( i ) ; d e s t r o y ( _paixao , i ) ; } i n t paixao ( s i z e _ t i ) { return _paixao [ i ] ; } }; 38 39 40 41 42 43 44 45 46 47 48 49 50 t e m p l a t e < c l a s s T> c l a s s I n t e r m e d i a r i o : p u b l i c C o l e c i o n a d o r <T> { s t d : : v e c t o r < double > _ p r e c o ; C o l e c i o n a d o r <T > : : novo ; public : I n t e r m e d i a r i o ( c o n s t s t d : : s t r i n g &nome ) : C o l e c i o n a d o r <T> ( nome ) {} v o i d compra ( c o n s t T &n , d o u b l e p ) { novo ( n ) ; _ p r e c o . p u s h _ b a c k ( p ) ; } double v a l o r ( s i z e _ t i ) { return _preco [ i ] ; } void perde ( s i z e _ t i ) { C o l e c i o n a d o r <T > : : p e r d e ( i ) ; d e s t r o y ( _ p r e c o , i ) ; } double vende ( s i z e _ t i ) { double p = _preco [ i ] ; perde ( i ) ; d e s t r o y ( _preco , i ) ; return p ; } }; 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 i n t main ( ) { Pessoa j ( " Joao " ) ; s t d : : c o u t << " Nao s e i o que " << j . nome ( ) << " f a z . " << s t d : : e n d l ; C o l e c i o n a d o r < double > a ( " Angelo " ) ; a . novo ( 1 . 0 ) , a . novo ( 8 ) ; a . novo ( − 3 . 1 4 ) ; s t d : : c o u t << a . nome ( ) << " tem " << a . q u a n t o s ( ) << " numeros . " ; s t d : : c o u t << " O s e g u n d o d e l e s v a l e " << a [ 1 ] << " . " << s t d : : e n d l ; F i l a t e l i s t a p ( " Pedro " ) ; p . novo ( ∗ ( new S e l o ) , 1 0 ) ; p . novo ( ∗ ( new S e l o ) , 5 ) ; s t d : : c o u t << p . nome ( ) << " tem " << p . q u a n t o s ( ) << " s e l o s . " ; s t d : : c o u t << " O p r i m e i r o e l e g o s t a " << p . p a i x a o ( 0 ) << " . " << s t d : : e n d l ; I n t e r m e d i a r i o <Selo > c ( " C a r l o s " ) ; c . compra ( ∗ ( new S e l o ) , 2 0 ) ; c . compra ( ∗ ( new S e l o ) , 3 7 . 5 ) ; s t d : : c o u t << c . nome ( ) << " tem " << c . q u a n t o s ( ) << " s e l o s , " ; s t d : : c o u t << " que valem no t o t a l " ; double s = 0 ; f o r ( s i z e _ t i = 0 ; i < c . q u a n t o s ( ) ; i ++) s += c . v a l o r ( i ) ; s t d : : c o u t << s << " r e a l o s . " << s t d : : e n d l ; return 0; } Neste código, o template Colecionador<T> é derivado da classe simples Pessoa, acrescentando um membro que guarda a coleção e métodos para seu acesso; a classe simples Filatelista é derivada do template Colecionador especializado para selos acrescentando novos membros e métodos para indicar o quanto gosta de cada selo da coleção; o template Intermediario é derivado de um Colecionador do mesmo tipo ( Intermediario <T> é derivado de Colecionador<T>), acrescentando membros para lidar com o preço dos objetos e proibindo o acesso ao membro herdado Colecionador<T>::novo, para impedir que um novo elemento seja adicionado sem o preço correspondente. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 110 Templates Exercícios 1. Quais são as similaridades e diferenças entre polimorfismo e templates? Quando se deve usar templates? 2. Comente quais seriam as vantagens de se declarar uma classe Fila como um template. 3. Escreva um template de classe de lista ligada. 4. Escreva um template de classe de vetores unidimensionais. 5. Escreva um template de classe de matrizes bidimensionais. 6. Escreva um template de função quicksort que possa funcionar com qualquer tipo para o qual o operador < esteja definido. U NIVERSIDADE DE S ÃO PAULO Capítulo 12 Exceções Um problema que precisa sempre ser considerado com seriedade ao se desenvolver um programa diz respeito ao tratamento de erros. Qual deve ser a ação a ser tomada quando um erro é detectado? Existem diversas formas de realizar isto, desde ignorar o erro até realizar tentativas de recuperação, e é extremamente importante que o tipo de tratamento de erros a ser realizado pelo programa seja decido de antemão (antes da implementação), pois a alteração posterior do programa para cuidar de erros é em geral difícil. Veremos neste capítulo uma técnica de tratamento de erros presente em algumas linguagens mais modernas, e recentemente incorporada à linguagem C++. Esta técnica é chamada tratamento de exceções. 12.1 Tratamento de erros Inicialmente vejamos alguns modos normalmente utilizados para lidar com erros em programas. 1. Ignorar o erro e continuar processando com os dados errôneos. Este método é completamente inadequado, apesar de que infelizmente muito comum. Sob nenhuma hipótese deve ser permitido a um programa continuar seu processamento utilizando dados provenientes de uma computação errada. Em linguagens como C e C++ é comum encontrarem-se programas com este tipo de comportamento quando ponteiros são incorretamente utilizados (ponteiro não inicializados, acesso a ponteiros para posições de memória já desalocadas ou acesso a array com índices inválidos). 2. Terminação do programa quando um erro é detectado. Esta já é uma solução mais adequada, pois impede que o programa gere saídas errôneas. No entanto a simples terminação do programa não fornece ao usuário ou ao programador informações sobre o que ocorreu de errado. 3. Envio de uma mensagem indicando o tipo e localização do erro no programa, e então término do programa. Esta é uma solução bem mais adequada, desde que as informações sobre a condição de erro e sua localização sejam precisas. 4. Se o erro ocorre durante interação com o usuário, pode ser enviada uma mensagem indicando o erro e ser feita nova tentativa. 5. Quando o erro ocorre durante a execução de uma função, um código para o erro pode ser colocado numa variável global, que depois pode ser testada para verificar se ocorreram erros. A desvantagem aqui é que é muito fácil o programador se esquecer de verificar se um erro ocorreu, e portanto deixar o processamento prosseguir de forma errônea. 6. Quando o erro ocorre durante a execução de uma função, esta pode retornar um código de erro para a função que a chamou. 7. Alguns tipos de erros podem ser tratados de forma especial, por exemplo a falta de memória ao realizar um new ocasiona a chamada de uma função apontada pelo ponteiro para função new_handler. Veja capítulo 16. Todas essas técnicas podem ser aplicadas sem nenhuma necessidade de novos dispositivos na linguagem. Linguagens mais recentes, entretanto, têm introduzido técnicas de tratamento de exceções, que corresponde a construções sintáticas que permitem lidar com situações fora do fluxo de controle normal do programa. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 112 12.2 Exceções Exceções Consideremos um erro como exemplo, uma tentativa de escrita num arquivo quando o disco está cheio. Podemos lidar com esse problema da seguinte forma: a função que cuida da escrita de dados em disco retorna um valor, que quando diferente de zero (por exemplo), indica que um erro ocorreu, e se o valor retornado for um certo valor especificado então indica que o disco estava cheio. Em cada ponto do programa onde uma escrita em disco é necessária, a função é chamada, e o valor de retorno testado para verificar se um erro de disco cheio ocorreu, caso em que uma mensagem é enviada e a execução terminada. O problema com esta solução é que, no caso do código apresentar diversos pontos onde uma escrita a disco ocorre, o tratamento desse erro estará distribuído por diversos pontos do programa, e misturado com o código de processamento normal do programa. O uso de exceções permite eliminar esse problema. A cada possível condição de erro associamos uma exceção, e a cada exceção associamos um tratador de exceções. Sempre que (dentro de um certo código) um erro ocorre, o correspondente tratador de exceções será ativado. Desta forma o tratamento de cada tipo de erro será mantido em apenas um lugar (para um dado trecho de código). Veremos a seguir como isto é realizado em C++. O programa abaixo lê os dados de um arquivo de entrada (input . txt ) e os copia em dois arquivos de saída (output1 . txt e output2 . txt ). 1 # include <cstdio > 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 i n t main ( ) { FILE ∗ i n , ∗ o u t 1 , ∗ o u t 2 ; in = fopen ( " i n p u t . t x t " , " r " ) ; o u t 1 = f o p e n ( " o u t p u t 1 . t x t " , "w" ) ; o u t 2 = f o p e n ( " o u t p u t 2 . t x t " , "w" ) ; int c ; w h i l e ( ( c = f g e t c ( i n ) ) ! = EOF ) { fputc ( c , out1 ) ; fputc ( c , out2 ) ; } fclose ( in ) ; f c l o s e ( out1 ) ; f c l o s e ( out2 ) ; return 0; } Note que nenhum código para verificação de erros é apresentado. Qualquer das operações com arquivo podem gerar erros, como ausência do arquivo de leitura, falta de espaço para escrita dos arquivos de saída entre outras. Na verdade este programa é muito pequeno para justificar o uso de exceções, no entanto será utilizado para mostrar a sintaxe usada para a programação de tratamento de exceções em C++. Abaixo mostramos o código alterado, e em seguida comentaremos sobre os diversos elementos. 1 2 # include <iostream > # include <cstdio > 3 4 5 6 7 8 / / C l a s s e s de e x c e c a o class FileError { public : F i l e E r r o r ( ) {} }; 9 10 11 12 13 14 15 16 class OpenInputError : public F i l e E r r o r { c o n s t char ∗nome ; public : O p e n I n p u t E r r o r ( c o n s t char ∗n= " << D e s c o n h e c i d o >> " ) : nome ( n ) {} v o i d nomeia ( ) { s t d : : c o u t << "Nome do a r q u i v o : " << nome << s t d : : e n d l ; } }; 17 18 19 20 21 22 c l a s s OpenOutputError : public F i l e E r r o r { c o n s t char ∗nome ; public : O p e n O u t p u t E r r o r ( c o n s t char ∗n= " << D e s c o n h e c i d o >> " ) : nome ( n ) {} v o i d nomeia ( ) U NIVERSIDADE DE S ÃO PAULO 12.2 Exceções { s t d : : c o u t << "Nome do a r q u i v o : " << nome << s t d : : e n d l ; } 23 24 }; 25 26 27 28 29 class GetError : public F i l e E r r o r { public : G e t E r r o r ( ) {} }; 30 31 32 33 34 class PutError : public F i l e E r r o r { public : P u t E r r o r ( ) {} }; 35 36 37 38 39 class CloseError : public F i l e E r r o r { public : C l o s e E r r o r ( ) {} }; 40 41 42 43 44 45 46 47 / / Funcoes A u x i l i a r e s FILE ∗ O p e n I n p u t ( c o n s t char ∗nome ) { FILE ∗ a r q = f o p e n ( nome , " r " ) ; i f ( a r q == 0 ) throw O p e n I n p u t E r r o r ( nome ) ; return arq ; } 48 49 50 51 52 53 54 FILE ∗ OpenOutput ( c o n s t char ∗nome ) { FILE ∗ a r q = f o p e n ( nome , "w" ) ; i f ( a r q == 0 ) throw O p e n O u t p u t E r r o r ( nome ) ; return arq ; } 55 56 57 58 59 60 61 i n t G e t C h a r ( FILE ∗ a r q ) { int c = fgetc ( arq ) ; i f ( f e r r o r ( a r q ) ) throw G e t E r r o r ( ) ; return c ; } 62 63 64 65 66 67 v o i d P u t C h a r ( i n t c , FILE ∗ a r q ) { int r = fputc ( c , arq ) ; i f ( r == EOF ) throw P u t E r r o r ( ) ; } 68 69 70 71 72 v o i d C l o s e ( FILE ∗ a r q ) { i f ( f c l o s e ( a r q ) == EOF ) throw C l o s e E r r o r ( ) ; } 73 74 75 76 77 78 79 80 81 82 83 84 85 i n t main ( ) { try { FILE ∗ i n , ∗ o u t 1 , ∗ o u t 2 ; in = OpenInput ( " i n p u t . t x t " ) ; o u t 1 = OpenOutput ( " o u t p u t 1 . t x t " ) ; o u t 2 = OpenOutput ( " o u t p u t 2 . t x t " ) ; int c ; w h i l e ( ( c = G e t C h a r ( i n ) ) ! = EOF ) { PutChar ( c , out1 ) ; PutChar ( c , out2 ) ; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 113 114 Close ( in ) ; Close ( out1 ) ; Close ( out2 ) ; return 0; 86 87 88 89 } catch ( OpenInputError e ) { s t d : : c e r r << " E r r o ao a b r i r a r q u i v o de e n t r a d a " << s t d : : e n d l ; e . nomeia ( ) ; } catch ( OpenOutputError e ) { s t d : : c e r r << " E r r o ao a b r i r a r q u i v o de s a i d a " << s t d : : e n d l ; e . nomeia ( ) ; } catch ( GetError e ) { s t d : : c e r r << " E r r o ao l e r a r q u i v o " << s t d : : e n d l ; } catch ( PutError e ) { s t d : : c e r r << " E r r o ao e s c r e v e r a r q u i v o " << s t d : : e n d l ; } catch ( CloseError e ) { s t d : : c e r r << " E r r o ao f e c h a r a r q u i v o " << s t d : : e n d l ; } catch ( . . . ) { s t d : : c e r r << " E r r o d e s c o n h e c i d o " << s t d : : e n d l ; } return 1; 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 Exceções } Note como, com exceção do fato de que as funções originais foram substituídas por funções protegidas por exceções, o código acima é, na sua parte inicial (dentro do bloco try), praticamente idêntico ao código anterior sem tratamento de erros. O uso de exceções pode ser explicado resumidamente como segue: • Um código com exceções consiste dos seguintes elementos: alguns tipos de exceções, o código a ser executado e que poderá gerar exceções, e códigos para o tratamento de cada um dos tipos de exceções. • Um tipo de exceção é representado por um tipo de dados. Qualquer tipo de dados, incluindo tipos básicos, ponteiros e classes, pode ser utilizado como um tipo de exceção. • O código principal a ser executado e que pode gerar exceções é envolvido por um bloco try. Um bloco try é um bloco delimitado por { e } e precedido pela palavra reservada try. • Cada um dos tipos de exceção é tratado por um diferente tratador de exceção. • Um tratador de exceção é indicado por um bloco catch. Um bloco catch é um bloco delimitado por { e } e precedido pela palavra reservada catch seguida de um especificador de tipo de exceção, opcionalmente com um nome para o objeto de exceção, usando a mesma sintaxe que para declaração de parâmetro de funções (ver exemplos acima). • O bloco try deve ser seguido por todos os blocos catch associados. • Durante a execução de um bloco try pode ser indicada a ocorrência de uma exceção por meio da execução de um throw. • O throw aceita um operando, que será a exceção a ser gerada. O tipo do operando determina o tipo de exceção a ser gerada. Se uma exceção do tipo classe for gerada, um objeto de exceção temporário pode ser utilizado especificando-se como operando do throw o construtor da classe de exceção indicada. • Ao ser executado um throw, a execução do bloco try é interrompida, e o controle de execução é desviado para o bloco catch que combine com o tipo de exceção (regras de combinação serão descritas a seguir). Após a execução do bloco catch, o controle não mais retorna para o bloco try, mas ao invés disso é desviado para a primeira instrução após o último bloco catch associado ao bloco try que gerou a exceção. • Blocos try podem ocorrer aninhados, tanto dentro de uma mesma função como em diferentes funções que chamam umas às outras. U NIVERSIDADE DE S ÃO PAULO 12.2 Exceções 115 • Quando uma exceção ocorre, é procurado um tratador associado ao bloco try imediato que combine com a exceção. Se nenhum é encontrado, um tratador adequado associado ao bloco try imediatamente acima é procurado, e assim sucessivamente. • Se nenhum tratador adequado é encontrado para uma exceção, então a função terminate é chamada, que por sua vez termina a execução chamando abort. É possível redefinir terminate por meio de uma chamada à função set_terminate , passando como parâmetro o novo tratador de terminação a ser utilizado, que deve ser uma função sem parâmetros e sem valor de retorno. • Informações sobre a exceção estão presentes em dois pontos: no tipo do objeto de exceção, que determina qual tratador de exceção será chamado, e no valor do objeto, que pode ser utilizado para transmitir informação adicional (veja a comunicação do nome do arquivo no exemplo acima). • O tratador adequado para uma exceção é procurado pelos diversos blocos catch na ordem em que eles aparecem após o bloco try. Isto significa que a ordem dos blocos catch é importante. • Um tratador de exceção é considerado adequado para uma exceção se um dos seguintes critérios for válido: 1. o parâmetro do tratador for do mesmo tipo do que a exceção; 2. o tipo do parâmetro for uma classe base pública do tipo da exceção; 3. o tipo do parâmetro for um ponteiro, e o tipo da exceção for um ponteiro compatível, isto é, um ponteiro que pode ser atribuído a ponteiros do tipo do parâmetro do tratador; 4. o catch é da forma catch (...) , indicando que ele aceita todos os tipos de exceções. • A construção catch (...) indica um tratador para todos os tipos de exceções. Deve-se tomar extremo cuidado com o uso desses tratadores, pois dificilmente o mesmo código pode ser apropriado para todos os tipos possíveis de exceções (lembre-se de que exceções podem ser geradas por rotinas chamadas dentro do bloco try, e que foram desenvolvidas por outro programador). De qualquer forma, estes tratadores devem aparecer por último na lista de tratadores de qualquer bloco try, ou os tratadores listados a seguir nunca serão utilizados. • Tratadores gerais de exceção podem ser definidos como tratadores para uma classe base. Esses tratadores podem então ser utilizados para exceções de todos os tipos de classes derivadas de forma pública dessa classe base. • Um tratador de exceção pode decidir passar a exceção para níveis mais altos, isto é, blocos try mais externos. Isto pode ser feito pelo uso da palavra reservada throw sem operandos no correspondente bloco catch. Isto é útil em funções onde certas exceções podem ser melhor tratadas com maior conhecimento do contexto sob o qual a função está executando. Por exemplo, na implementação de rotinas de escrita em arquivo, é preferível passar exceções para a função que chamou estas rotinas, para que ela possa tomar as ações que julgar necessárias. • Quando uma função é definida, é possível especificar todos os tipos de exceções que a função irá possivelmente gerar. Isto serve como uma forma de documentação da função. A sintaxe para isto é: 1 i n t f ( f l o a t h ) throw (A, B , C ) ; onde A, B e C são os tipos de exceções que a função f poderá gerar. Se nenhuma especificação de exceções é apresentada para uma função, isto significa que a função pode em princípio gerar qualquer exceção. Se queremos indicar que uma função não gera nenhum tipo de exceção, podemos utilizar uma lista de tipos nula, por exemplo: 1 i n t g ( d o u b l e x ) throw ( ) ; indica que a função g não irá gerar nenhuma exceção. Se alguma função gera uma exceção que não está de acordo com sua lista de exceções, então a função unexpected é chamada. Normalmente unexpected chama terminate , mas seu comportamento pode ser redefinido por meio de set_unexpected, que funciona similarmente a set_terminate descrita anteriormente. • Como um throw aborta a execução de um contexto de funções ele também cuida para que todos os objetos automáticos associados ao contexto que será interrompido sejam corretamente destruídos por meio da chamada dos destruidores correspondentes. Se uma exceção ocorrer durante a execução de um desses destruidores, terminate será chamada. • Recursos, como por exemplo memória alocada dinamicamente ou arquivos abertos, não são automaticamente liberados quando uma exceção ocorre. O programador tem que codificar essa liberação explicitamente no correspondente bloco catch. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 116 Exceções Exercícios 1. Por que razão ignorar a ocorrência de erros é perigoso? 2. Quais são os elementos de um código com tratamento de exceções? 3. Que tipos de dados podem ser utilizados como tipos de exceção? 4. O que é um tratador de exceção? 5. Como se indica a ocorrência de uma exceção? 6. O que ocorre ao ser gerada uma exceção durante a execução? 7. O que ocorre se um tratador de exceções adequado associado ao bloco try imediato não é encontrado? 8. Quais informações existem sobre uma exceção gerada? 9. Quais são as regras para se encontrar um tratador de exceção adequado para a exceção gerada? 10. Por que razão devemos tomar cuidado com o uso do tratador catch (...) ? Por que ele deve sempre aparecer com último na lista de tratadores? 11. Quando é vantajoso definir um tratador para a classe base de diversos tipos de exceção? 12. É possível a um tratador de exceção delegar o tratamento da exceção a tratadores de níveis mais externos? Como? Qual seria a vantagem de uma ação dessas? 13. Reescrever o template de classe vetor dos exercícios do capítulo 11, para que todas as situações excepcionais sejam tratadas por meio de exceções. U NIVERSIDADE DE S ÃO PAULO Capítulo 13 Entradas e saídas O tópico deste capítulo não faz parte estritamente falando de uma discussão da linguagem em si. Apenas serão apresentadas aqui algumas formas como os recursos da linguagem C++ foram utilizados para implementar uma biblioteca de classes e objetos para lidar com entradas e saídas de dados. Esta biblioteca é padronizada, e acessível nos mais diversos sistemas que apresentam compiladores C++, de forma que ela pode ser utilizada quando se pretende desenvolver programas portáteis. Apesar de entradas e saídas poderem ser efetuadas em programas C++ utilizando as mesmas funções presentes na linguagem C, isto não é recomendado, pois as versões C++ apresentam as vantagens próprias de C++ em relação a C: um tratamento uniforme dos diversos tipos, operações seguras com relação a tipos (não permite que um tipo seja passado erroneamente para uma função que espera outro tipo) e extensibilidade. Neste capítulo, como são diversas vezes relacionados nomes de elementos da biblioteca padrão, será sempre omitido o indicador de escopo std :: . 13.1 Streams Um stream é uma seqüencia de bytes, da qual podem ser lidos dados, numa operação de entrada, ou escritos dados, numa operação de saída. Entrada consiste em ler dados de um stream e colocá-los em uma variável do programa. Numa operação de saída dados de uma variável são escritos no stream. Leituras e escritas podem tanto ser formatadas quanto não formatadas. Ser não-formatada implica em que os bytes que representam o valor da variável na memória são armazenados no stream. No caso de saída formatada, uma representação dos valores utilizando códigos como o ASCII é armazenada na stream, e a conversão entre formato interno e formato de representação na stream é realizada durante o processo de leitura ou escrita. A vantagem de entrada e saída não formatada é que ela ocupa menos espaço. A desvantagem é que os dados não são portáteis de uma máquina para outra, e também não são acessíveis para leitura pelo usuário. A biblioteca iostream contém a definição de diversas classes e objetos para lidar com entrada e saída de dados. istream é a classe para streams de entrada, ostream para streams de saída, e iostream para streams que serão utilizados tanto para entrada como para saída. As classes istream e ostream são derivadas da classe base ios, enquanto que iostream é derivada por herança múltipla tanto de istream como de ostream. Quatro objetos definidos nessa biblioteca são cin, cout, cerr e clog. cin é um objeto da classe istream , e é associado ao dispositivo de entrada padrão. cout é da classe ostream, e associado ao dispositivo de saída padrão. cerr e clog são também da classe ostream, mas associados ao dispositivo de erros padrão. A diferença entre cerr e clog é que as mensagens para cerr são imediatamente enviadas para o dispositivo de erro, sem bufferização, enquanto que as mensagens enviadas para clog são bufferizadas. A classe istream sobrecarrega o operador >>, chamado operador de extração, para representar a leitura (extração) de dados do stream (operando da esquerda) para uma variável (operando da direita). A classe ostream sobrecarrega similarmente o operador <<, chamado operador de inserção, para significar a escrita (inserção) de dados de uma variável no stream. Os dois operadores são sobrecarregados para os tipos básicos e mais char∗ na biblioteca padrão, de forma que qualquer expressão desses tipos pode ser utilizada. Deve-se tomar cuidado com a precedência dos operadores (seção 1.4). Os operadores são sobrecarregados de tal forma que uma referência para seu operando à esquerda é retornada. Isto torna possível utilizar os operadores para inserções ou extrações consecutivas, sem necessidade de repetir a cada vez o nome do objeto stream, como no exemplo abaixo: 1 c o u t << "A e x p r e s s a o v a l e " << x << e n d l ; Esse código, devido à associatividade da esquerda para a direita do operador << é equivalente a: 1 ( ( c o u t << "A e x p r e s s a o v a l e " ) << x ) << e n d l ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 118 Entradas e saídas 13.2 Streams de saída Ao se realizar inserção em um objeto do tipo ostream, os dados inseridos são normalmente bufferizados (uma exceção é o objeto cerr , como visto). Normalmente os dados são enviados do buffer ao stream apenas em situações pré-definidas, por exemplo quando o buffer está cheio. Se desejamos que os dados do buffer sejam esvaziados no stream em certo ponto do programa, podemos utilizar o manipulador flush , como abaixo: 1 c o u t << f l u s h ; O buffer de saída também é esvaziado quando um caracter de mudança de linha é enviado, como por exemplo através do uso do manipulador endl: 1 c o u t << e n d l ; endl indica que uma nova linha deve ser começada, mas também implica no esvaziamento do buffer. Além do uso do operador de inserção, podemos realizar também saída de dados com outros métodos da classe ostream, como o método put, que escreve um caracter. O código: 1 cout . put ( ’x ’ ) ; envia o caracter ’x’ para cout. O método put também retorna uma referência para o objeto, e então códigos como o seguinte são possíveis: 1 cout . put ( ’x ’ ) . put ( ’ \ n ’ ) ; 13.3 Streams de entrada O operador de extração normalmente ignora espaços em branco (brancos, tabulações e mudanças de linha). O valor retornado pelo operador de extração é uma referência para o operando da esquerda se a entrada ocorreu corretamente, e 0 se houve um erro ou o fim do stream foi encontrado. Além disso, a classe base de istream , ios, provê um conversor de stream para void∗, o que permite usos como no código abaixo: 1 2 3 w h i l e ( c i n >>x ) { /∗ . . . ∗/ } O while será executado até um fim de arquivo ser encontrado na entrada padrão, ou um erro de leitura ocorrer (por exemplo, dados de tipo incompatível com a variável x), casos em que será retornado 0 (ponteiro nulo). Além do operador de extração, diversos métodos são definidos na classe istream : eof () Este método retorna true se o fim de arquivo foi encontrado no stream de entrada, false caso contrário. get () O método get sem parâmetros retorna o próximo caracter do stream de entrada, incluindo caracteres em branco. Se o fim do arquivo for encontrado, retorna EOF. get (char &c) Quando recebe uma variável char como parâmetro, o método get armazena o caracter lido no argumento. O valor de retorno é uma referência ao istream utilizado, ou 0 caso o fim de arquivo seja encontrado. get (char ∗, int , char = ’ \n’) Esta terceira versão lê uma cadeia de caracteres no buffer apontado pelo primeiro argumento, até encontrar o caracter delimitador designado pelo terceiro argumento (valor assumido: ’ \n’), mas lendo no máximo um a menos que o número de caracteres especificado pelo segundo argumento. Um caracter nulo é inserido no final da cadeia lida. O delimitador não é inserido na cadeia, mas permanece no stream de entrada. getline (char ∗, int , char = ’ \n’) O funcionamento de getline é similar ao da versão de get com três parâmetros, sendo a única diferença que o delimitador é retirado do stream de entrada. ignore ( int = 1, int = EOF) Ignora o número de caracteres especificado pelo primeiro argumento (valor assumido 1), ou até encontrar o delimitador especificado (valor assumido EOF), o que ocorrer primeiro. U NIVERSIDADE DE S ÃO PAULO 13.4 Entrada e saída não formatadas 119 putback(char) Coloca de volta no istream (para posterior nova leitura) o último caracter lido por get (que deve ser passado como parâmetro). peek() Retorna o próximo caracter do stream, mas sem retirá-lo de lá. Serve para verificar qual o próximo caracter que será lido. 13.4 Entrada e saída não formatadas Os métodos e operadores anteriores lidam com entrada e saída formatadas. Veremos agora os métodos para lidar com entrada e saída não formatadas. read(char ∗, int ) Este método lê do istream no buffer especificado pelo primeiro argumento tantos bytes quanto especificados pelo segundo argumento. Se o número de caracteres especificados não puder ser lido, failbit será marcado (veja seção 13.7 sobre failbit ). O valor retornado é uma referência para o istream . write (const char ∗, int ) O número de bytes especificado pelo segundo argumento é escrito do buffer especificado pelo primeiro argumento no ostream. O valor retornado é uma referência para o ostream. gcount () Chamado após o métodos read, get ou getline , retorna o número de caracteres efetivamente lidos do istream . 13.5 Manipuladores de stream Para a execução de diversas funções especiais as classes istream e ostream dispõe de diversos manipuladores, que são objetos que devem ser inseridos no stream e têm o efeito de alterar o modo de operação dos métodos e operadores. O uso de qualquer manipulador exige a inclusão de iomanip. dec, oct, hex, setbase ( int ) Estes manipuladores permitem determinar qual a base numérica a ser utilizada para impressão dos valores. Normalmente a base é 10. Será alterada para base 8 se o manipulador oct ou setbase (8) for utilizado, para 16 se o manipulador hex ou setbase (16) for utilizado, e retornará para 10 se dec ou setbase (10) for utilizado. O valor alterado de base permanecerá até ser novamente explicitamente alterado. Em streams de saída, a base usada para escritas é decimal, a menos que seja especificamente ajustada outra base. Em streams de entrada, a base usada é escolhida de acordo com a entrada, seguindo as regras de constantes literais inteiras (seção 1.3.2). Esta forma de tratamento pode ser restaurada após alguma mudança através de um setbase (0). setprecision ( int ) Este manipulador especifica o número de dígitos após a vírgula decimal a serem utilizados na escrita de valores de ponto flutuante. O mesmo efeito pode ser conseguido com o uso do método precison ( int ). A precisão ficará ajustada para todas as operações de saída no stream até ser explicitamente alterada. Se o método precision é chamado sem argumentos, ele retorna a precisão atual. Se o valor especificado de precisão é 0, então o valor normal (6 casas depois da vírgula) é restaurado. setw( int ) Este manipulador especifica o número de caracteres a serem reservados para a impressão do próximo valor. Se o espaço necessário para a impressão do valor é menor do que o especificado, então caracteres adicionais de preenchimento são inseridos. Se mais caracteres são necessários do que o especificado, então todo o espaço necessário será utilizado (o dado não será truncado, mas o campo separado para a impressão será excedido). Após a impressão do valor que vem em seguida ao setw, o valor da largura será automaticamente ajustado para 0. O mesmo efeito pode ser conseguido com o uso do método width( int ). Quando o manipulador é utilizado para streams de entrada, o número especificado menos um de caracteres é lido, deixando então espaço para o caracter nulo a ser inserido no fim da cadeia. setfill ( int ) I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 120 Entradas e saídas Tabela 13.1: Flags de formatação Flag Significado ios :: skipws ios :: right ios :: left ios :: internal brancos devem ser pulados saída ajustada à direita saída ajustada à esquerda sinal ou base ajustados à esquerda, número ajustado à direita, espaço interno restante preenchido base decimal (10) base octal (8) base hexadecimal (16) força o indicador de base de um número inteiro a ser impresso força número de ponto flutuante a ser impresso com ponto decimal e todos os zeros finais especificados pela precisão atual força todas as letras usadas para impressão de números (como X e E) a serem maiúsculas mostra sinal também para números positivos força a saída de um número de ponto flutuante em formato científico força a saída a utilizar um número específico de casas decimais após a vírgula streams são esvaziados após toda inserção (semelhante a cerr ) streams de saída e erro padrão são esvaziados após toda inserção ios :: dec ios :: oct ios :: hex ios :: showbase ios :: showpoint ios :: uppercase ios :: showpos ios :: scientific ios :: fixed ios :: unitbuf ios :: stdio Este manipulador permite especificar o caracter de preenchimento a ser utilizado quando a largura do campo especificada é maior do que o necessário para a representação do valor. O mesmo pode ser feito com o método fill (char). ws Quando utilizado num istream , extrai todos os caracteres de espaço em branco (caracteres brancos, tabulações e fins de linha) até o próximo caracter não branco. Útil para ignorar explicitamente os espaços em branco iniciais antes de chamar um dos métodos que não ignoram espaços em branco. endl Insere uma mudança de linha e esvazia o buffer do ostream. flush Esvazia o buffer do ostream. 13.6 Estados de formatação de stream Um conjunto de flags permite a especificação de diversas características do formato. Para controlar esses flags existem os métodos setf , unsetf e flags . Muitos dos flags podem ser controlados também por meio de manipuladores. O método setf é utilizado para ligar um ou vários flags; unsetf é utilizado para desligar um ou vários flags; flags pode ser utilizado para ajustar o todos os flags simultaneamente para os valores desejados. Os flags existentes são definidos por uma enumeração na classe ios, e listados na tabela 13.1. Alguns desses flags são combinados, e sempre que isto ocorre devemos fornecer, como segundo argumento aos métodos setf e unsetf , um membro estático correspondente, de forma a que o método tenha condição de ajustar apenas o valor do flag indicado, sem alterar os flags associados. A tabela 13.2 indica as associações de membros estáticos com flags. Um exemplo seria: 1 cout . s e t f ( ios : : l e f t , ios : : a d j u s t f i e l d ) ; U NIVERSIDADE DE S ÃO PAULO 13.7 Estados de erro de stream 121 Tabela 13.2: Membros estáticos usados com flags Membro estático Flags ios :: adjstfield ios :: basefield ios :: floatfield ios :: left , ios :: right , ios :: internal ios :: oct, ios :: dec, ios :: hex ios :: scientific , ios :: fixed Tabela 13.3: Flags para indicação de erros Flag Significado ios :: eofbit ios :: failbit fim de arquivo foi encontrado no stream houve erro de formatação, mas nenhum caracter foi perdido houve erro com perda de dados nenhuma das condições anteriores ocorreu ios :: badbit ios :: goodbit Diversos dos flags podem ser ajustados simultaneamente usando-se o operador de ou binário, como em 1 c o u t . s e t f ( i o s : : s h o w p o i n t | i o s : : showpos ) ; É interessante notar que quando um dos flag ios :: fixed ou ios :: scientific está ligado a interpretação da precisão (ajustada pelo manipulador setprecision ou pelo método precision ) é alterada para indicar o número de casas após a vírgula, ao invés do número de dígitos decimais significativos. Também, um setprecision (0) indicará que nenhuma casa após a virgula deverá ser utilizada, ao invés de restaurar a precisão padrão. Ao invés dos métodos setf e unsetf , podem ser utilizados os manipuladores setiosflags e resetiosflags respectivamente. Esses manipuladores recebem como parâmetro um ou binário dos flags a serem ligados ou desligados, como em 1 c o u t << s e t i o s f l a g s ( i o s : : s h o w p o i n t | i o s : : showpos ) ; O método flags ajusta simultaneamente todos os flags. Os flags especificados no argumento serão ligados, enquanto que os restantes serão desligados. Os métodos flags , setf e unsetf retornam o valor anterior dos flags armazenados internamente, que poderá então ser utilizado para restaurar a situação original após uma mudança. O valor retornado é do tipo long. 13.7 Estados de erro de stream Assim como existem flags de estado associados à formatação e métodos para lidar com eles, também existem flags de estado associados com condições de erro que ocorreram em operações no stream e métodos associados. Os flags são listados na tabela 13.3. O método eof () retorna verdadeiro se o flag ios :: eofbit está ligado; fail () retorna verdadeiro se o flag ios :: failbit está ligado; bad() retorna veradeiro se ios :: badbit está ligado; good() retorna verdadeiro se ios :: goodbit está ligado. O método rdstate () retorna um valor que representa o estado de erro total do stream. O método clear () é utilizado para desligar um dos bits. Quando usado sem argumentos, limpa todos os flags de erro e fim de arquivo, e liga ios :: goodbit. Quando um dos flags de erro é passado, desliga esse flag de erro. 1 2 cin . clear ( ) ; cin . clear ( ios : : f a i l b i t ) ; / / d e s l i g a f l a g s de e r r o , l i g a g o o d b i t / / desliga ios :: f a i l b i t Além disso, a classe ios sobrecarrega o operator! para retornar verdadeiro se ios :: failbit ou ios :: badbit estiverem ligados. Também na classe ios existe um conversor para void∗ que retorna nulo se um desses bits estiver ligado. Estes métodos permitem que a própria operação de entrada e saída seja utilizada como condição de teste numa repetição (ver exemplo na seção 13.3). I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 122 13.8 Entradas e saídas Sobrecarga dos operadores de inserção e extração Os operadores de inserção e extração são sobrecarregados para todos os tipos básico, bem como para char∗, conforme visto. O programador de uma classe pode especificar a forma de sobrecarga desses operadores para suas classes, como exemplificado abaixo: 1 2 # include <iostream > # i n c l u d e <iomanip > 3 4 5 6 7 8 9 10 11 12 c l a s s Data { i n t d i a , mes , ano ; public : D a t a ( i n t d =1 , i n t m=1 , i n t a = 19 01 ) { d i a = d ; mes = m; ano = a ; } D a t a ( c o n s t D a t a &c ) { d i a = c . d i a ; mes = c . mes ; ano = c . ano ; } f r i e n d s t d : : i s t r e a m &o p e r a t o r >> ( s t d : : i s t r e a m &i s , D a t a &d t ) ; f r i e n d s t d : : o s t r e a m &o p e r a t o r << ( s t d : : o s t r e a m &os , c o n s t D a t a &d t ) ; }; 13 14 15 16 17 18 19 20 21 22 s t d : : i s t r e a m &o p e r a t o r >> ( s t d : : i s t r e a m &i s , D a t a &d t ) { i s >> d t . d i a ; is . ignore ( ) ; i s >> d t . mes ; is . ignore ( ) ; i s >> d t . ano ; return i s ; } 23 24 25 26 27 28 29 30 s t d : : o s t r e a m &o p e r a t o r << ( s t d : : o s t r e a m &os , c o n s t D a t a &d t ) { o s << d t . d i a << " / " << s e t w ( 2 ) << s e t f i l l ( ’ 0 ’ ) << d t . mes << s e t f i l l ( ’ ’ ) << " / " << d t . ano ; return os ; } 31 32 33 34 35 36 37 38 39 40 i n t main ( ) { Data d ; s t d : : c o u t << " D a t a : " << d << s t d : : e n d l ; s t d : : c o u t << " De uma nova d a t a ( dd /mm/ a a a a ou dd−mm−a a a a ) : " ; s t d : : c i n >> d ; s t d : : c o u t << " V a l o r da d a t a l i d a : " << d << s t d : : e n d l ; return 0; } Note que, para manter a funcionalidade dos operadores de inserção e extração, eles devem ser definidos como retornando uma referência para o operador da esquerda (o stream). Esta característica garante a extensibilidade da biblioteca de entrada e saída de C++. 13.9 Amarrando streams de saída e entrada Quando temos uma aplicação interativa, esta normalmente realiza saída via um ostream e entrada via um istream . Como a saída é bufferizada, poderia ocorrer que a execução do programa chegasse num ponto onde uma entrada é necessária, para a qual uma mensagem foi anteriormente enviada, com o intuito de informar ao usuário sobre qual a entrada requerida. Essa saída pode não haver sido enviada para o stream de saída, apesar da inserção já haver sido efetuada e os dados se encontrarem no buffer. O resultado seria que o programa interromperia a execução esperando a entrada sem que o usuário fosse informado de que dados de entrada são necessários. Para evitar este tipo de comportamento anômalo é possível amarrar um ostream a um istream , de forma que o buffer do ostream seja sempre esvaziado antes de uma extração do istream ocorrer. Isto já é feito automaticamente para o caso de cout e cin. Para o caso de outros streams, podemos utilizar o método tie , da classe istream , como abaixo: U NIVERSIDADE DE S ÃO PAULO 13.10 Entradas e saídas em arquivos 123 Tabela 13.4: Modos de abertura de arquivos 1 Modo Descrição ios :: in ios :: out ios :: app ios :: ate ios :: trunc abre para leitura abre para escrita escreve apenas no final do arquivo arquivo posicionando no final se arquivo existe, apaga e abre um novo i s . t i e (& o s ) ; onde is é uma istream e os é uma ostream. Para desamarrar uma istream de qualquer ostream, utilizamos o método tie com argumento nulo, por exemplo: 1 is . tie (0); desfaz a ligação feita anteriormente estabelecida. 13.10 Entradas e saídas em arquivos Entradas e saídas em arquivos podem ser realizadas de acordo com o exposto acima, declarando-se streams de arquivos. Um arquivo de entrada é associado a um ifstream , um arquivo de saída a um ofstream e um arquivo a ser utilizado para entrada e saída a uma fstream. ifstream é derivada de istream , ofstream é derivada de ostream e fstream é derivada de iostream. Estas classes são acessíveis através do arquivo de cabeçalho fstream . h. Antes de um arquivo ser acessado ele deve ser aberto e associado a um stream adequado. A associação de arquivo para stream pode ser realizada na inicialização da variável tipo stream, através do construtor da classe, como em: 1 2 ofstream sai ( " saida . txt " ) ; ifstream entra ( " entrada . txt " ); ou então posteriormente através do método open: 1 2 3 4 ofstream sai ; ifstream entra ; s a i . open ( " s a i d a . t x t " ) ; e n t r a . open ( " e n t r a d a . t x t " ) ; Tanto o construtor como o método open apresentam ainda dois outros parâmetros, o segundo é o modo de abertura e o terceiro o modo de proteção. O modo de abertura pode ser um dos listados na tab. 13.4. ios :: in é o modo assumido para ifstream e fstream, ios :: out é o modo assumido para ofstream. Uma combinação de modos através do operador ou binário pode ser também utilizada: 1 if s t r e a m arq ( " arquivo . dat " , ios : : in | ios : : ate ) ; Quando um modo é fornecido para ofstream ou ifstream explicitamente, então ele deve incluir os correspondentes ios :: out ou ios :: in, pois então o default do método não estaria sendo utilizado. Quando o arquivo não é mais necessário, o correspondente stream deve ser fechado utilizando-se o método close: 1 arq . close ( ) ; No entanto, isto somente é necessário se desejamos reutilizar o mesmo stream para outro arquivo. Caso contrário, podemos deixar o arquivo ser automaticamente fechado quando o destruidor do stream for ativado. 13.11 Acesso aleatório em arquivos Uma característica única de streams de arquivo em relação a streams simples é o fato de que podemos realizar acesso aleatório às mesmas, isto é, ler ou escrever dados em posições específicas do arquivo, ao invés de apenas seqüencialmente. Neste caso, antes de realizar uma leitura ou escrita, por um dos métodos indicados acima, podemos posicionar o ponteiro de leitura ou o ponteiro de escrita no ponto adequado do arquivo. Cada arquivo possui de fato um indicador de ponto de leitura e um indicador de ponto de escrita. Estes podem ser acessados e manipulados pelos métodos seguintes. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 124 Entradas e saídas Tabela 13.5: Pontos de referência para seekg e seekp Referência ios :: beg ios :: cur ios :: end Descrição valor em relação ao início do arquivo valor em relação ao ponto corrente valor em relação ao final do arquivo seekg(long) seekg(long, seek_dir ) Este método permite posicionar o indicador de ponto de leitura (get). A primeira variante posiciona de acordo com uma posição absoluta, como retornada por tellg (ver abaixo). A segunda variante permite posicionar especificando-se um offset em relação a uma posição específica (ver tabela 13.5). O método retorna uma referência para o stream. seekp(long) seekp(long, seek_dir ) Similar a seekg, mas posiciona o ponteiro de escrita (put). tellg () Retorna a posição de leitura atual absoluta. tellp () Retorna a posição de escrita atual absoluta. Valores negativos de offset são permitidos e úteis por exemplo para calcular a posição a partir do final do arquivo. 13.12 Streams associadas com cadeias de caracteres Uma outra possibilidade de streams é associá-las com cadeias de caracteres, de forma semelhante ao que é feito para arquivos. Para isto existem as classes istringstream , e ostingrstream , acessíveis através do arquivo de cabeçalho sstream. Uma cadeia que possui os dados a serem lidos é associada a uma istringstream , da qual os dados podem ser extraídos pelo operador de extração; o fim da cadeia é equivalente ao fim de arquivo. Um uso interessante dessas classes é para entrada de dados: uma linha inteira pode ser lida e associada com uma cadeia de caracteres. Se associamos então essa cadeia com um istringstream , podemos ler os diversos elementos da linha a partir dessa cadeia, inclusive tentando-se leitura em vários formatos, ou ainda pode ser realizada a validação dos dados, antes que eles sejam lidos. 1 2 3 # include <iostream > # include < s t r i n g > # include <sstream > 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 i n t main ( ) { char b u f f e r [ 2 5 6 ] ; s t d : : c o u t << " D o b r a d u r a s \ n E s c o l h a s e u s numeros . \ n \ n " ; while ( true ) { s t d : : c o u t << " : " ; std : : cin . getline ( buffer ,256); std : : istringstream is ( buffer ); double v a l ; i s >> v a l ; if ( is . fail ()) { std : : istringstream is ( buffer ); s t d : : s t r i n g comando ; i s >> comando ; i f ( comando == " s a i r " ) break ; e l s e s t d : : c o u t << " \ nO que ? \ n " << s t d : : e n d l ; } else s t d : : c o u t << 2∗ v a l << s t d : : e n d l ; } return 0; U NIVERSIDADE DE S ÃO PAULO 13.12 Streams associadas com cadeias de caracteres 26 125 } A classe ostringstream possui um método especial, chamado str , que retorna ums string com a cadeia atualmente contida no ostringstream : 1 2 3 # include <iostream > # include <sstream > # include < s t r i n g > 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 i n t main ( ) { s t d : : s t r i n g nome ; std : : ostringstream output ; s t d : : c o u t << "Quem e s t a a i ? " << s t d : : e n d l ; s t d : : c i n >> nome ; s t d : : c o u t << s t d : : e n d l ; o u t p u t << " Nao s u p o r t o a p r e s e n c a de " << nome ; std : : string saida = output . s t r ( ) ; s t d : : c o u t << s a i d a << " ! " << s t d : : e n d l ; s t d : : c o u t << s t d : : e n d l << " Eu d i s s e : \ " " << s a i d a << " \ " , o u v i u ? " << s t d : : e n d l << s t d : : e n d l ; s t d : : c o u t << "Como eh , nao v a i q u e b r a r o m o n i t o r ? " << s t d : : e n d l ; return 0; } Um ostrstream é dinâmico, pois o espaço que ele usa será alocado conforme a necessidade. Podemos também especificar uma cadeia inicial, isto é, colocar os dados inseridos após uma cadei pré-fixada, passando essa cadeia como parâmetro para o construtor do ostringstream : 1 2 3 s t d : : o s t r i n g s t r e a m m i n h a s a i d a ( "O r e s u l t a d o e : \ n " ) ; m i n h a s a i d a << " Maior a l t u r a : " << maxh << s t d : : e n d l ; m i n h a s a i d a << " Maior l a r g u r a : " << maxl << s t d : : e n d l ; Isso permite ir acrescentanto paulatinamento informações na cadeia e usar outros métodos de processamento de cadeias conjuntamente com os métodos de inserção. Exercícios 1. Qual a diferença entre operações de entrada e saída formatadas e não formatadas? 2. O que significa dizer que os dados de um stream de saída são bufferizados? 3. Que caracteres são considerados espaços em branco num stream de entrada? 4. Qual a diferença entre os métodos get (com 3 parâmetros) e getline da classe istream ? 5. O que são manipuladores de streams? 6. Cite as formas pelas quais a formatação de entrada e saída pode ser controlada. 7. O que significa amarrar streams de entrada e saída? Qual a utilidade disso? 8. Qual a diferença entre acesso aleatório e acesso seqüencial em arquivos? 9. Desenvolva um programa que, dada uma lista de nomes de mercadorias e preços, imprima um relatório como o seguinte: ========================================================== |Qt.| Mercadoria Preco | |--------------------------------------------------------| | 1| Caixa de disquetes (10 disquetes).............7.50 | | 10| Caneta esferografica.........................18.00 | | 1| Kit multimidia..............................230.00 | |--------------------------------------------------------| | Total 265.50 | ========================================================== I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 126 Entradas e saídas U NIVERSIDADE DE S ÃO PAULO Capítulo 14 Biblioteca padrão de templates Uma linguagem fornece os elementos necessários para o desenvolvimento de programas. Em geral, entretanto, não desenvolvemos todo o programa a partir do zero, mas nos baseamos em rotinas pré-desenvolvidas que apenas reaproveitamos. Para facilitar a utilização de um programa em sistemas distintos, é importante que as tarefas mais comuns tenham uma interface comum ao programador. Essas interfaces comuns são especificadas como complementação à especificação da linguagem. Uma parte dessas tarefas, referentes a operações de entradas e saídas, já foi descrita no capítulo 13. Neste capítulo trataremos do que é conhecido como Standard Template Library (STL), que especifica uma biblioteca com diversas funcionalidades que devem ser fornecidas pelo implementador de um compilador C++. Os conceitos principais dessa biblioteca são os receptáculos (containers), os iteradores e os algoritmos gerais (baseados em receptáculos e iteradores). A biblioteca faz uso extensivo de templates, para viabilizar generalidade. 14.1 Containers e iteradores Containers são objetos utilizados para armazenar uma coleção de objetos. Em STL, os objetos inseridos no receptáculo devem ser todos do mesmo tipo. Pode-se pensar num container como uma generalização de um array: da mesma forma que um array de inteiros, um container de inteiros permite armazenar e acessar diversos valores inteiros. Nessa mesma linha de comparação iteradores podem ser pensados como generalizações de ponteiros: da mesma forma que um ponteiro permite apontar para qualquer elemento de um vetor de inteiros, um iterador permite indicar um elemento específico armazenado em um container. De fato, os operadores associados com iteradores imitam os operadores de ponteiros, como veremos abaixo. Containers são definidos como templates, sendo que o tipo do objeto a ser inserido no container é apresentado como parâmetro do template. Por exemplo, existe um container vector que funciona como um vetor de elementos idênticos. Se queremos um vetor de números de ponto flutuante de precisão dupla e outro de números inteiros, ambos com 1000 elementos, podemos declarar: 1 2 v e c t o r < double > h e i g h t ( 1 0 0 0 ) ; v e c t o r < int > age ( 1 0 0 0 ) ; Um iterador é um tipo definido pelo próprio container. Para o exemplo acima, podemos escrever o seguinte código: 1 2 f o r ( v e c t o r < double > : : i t e r a t o r p = h e i g h t . b e g i n ( ) ; p ! = h e i g h t . end ( ) ; p ++) ∗p = 1 . 7 0 ; Esse código inicializa todos os elementos do vetor height com o valor 1.7. Note como tanto o auto-incremento como o operador de acesso indireto são usados como para ponteiros. Os métodos begin e end do template vector serão discutidos a seguir. 14.2 Tipos de containers Os tipos de receptáculos existentes são classificados em receptáculos de seqüência , receptáculos associativos , e quasereceptáculos. Um receptáculo de seqüência guarda uma coleção de elementos com uma ordem entre eles. Os containers de seqüência são vector , list e deque. Além desses, existem os chamados adaptadores de seqüência, stack, queue e priority_queue , que são implementados usando os containers de seqüência. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 128 Biblioteca padrão de templates Um receptáculo associativo guarda uma coleção de pares (chave, valor). Eles atuam como se fossem arrays que podem ser acessados por valores não-numéricos (as “chaves”). Os receptáculos associativos fornecidos são: map, multimap, set e multiset . Quase-containers é a denominação dada a um conjunto de tipos que, apesar de poder guardar coleções de objetos, não têm toda a flexibilidade dos containers indicados acima. Os quase-containers são: string , valarray , bitset e o array tradicional. A seguir descrevemos brevemente esses containers. vector O container vector fornece a possibilidade de armazenar compactamente objetos que precisam ser acessados em ordem aleatória, através de operadores de indexação. Acessível pelo cabeçalho <vector>. list Um list é otimizado para inserção e remoção de elementos, por outro lado não permite o acesso aleatório por meio de subscritos. Acessível pelo cabeçalho < list >. deque Um deque é uma fila de duas entradas, isto é, ela permite operações otimizadas de inserção e remoção pelas duas pontas. Por outro lado, inserção e remoção no meio podem apresentar custos altos. Acessível pelo cabeçalho <deque>. stack Um stack implementa uma pilha, com inserção e remoção por apenas uma das pontas. Acessível pelo cabeçalho <stack>. queue Um queue implementa uma fila com inserção no final e remoção do começo. Acessível pelo cabeçalho <queue>. priority_queue Um priority_queue possui associada uma ordem (que pode ser especificada pelo usuário) que decide a ordem em que os elementos serão removidos. Acessível pelo cabeçalho <queue>. map Um map guarda uma seqüencia (chave, valor) e permite ler o valor fornecida a chave. Para cada chave existe apenas um valor; se um novo valor for associado a uma chave existente, o valor anterior será descartado. Acessível pelo cabeçalho <map>. multimap O multimap é similar a map, mas permite múltiplos valores para cada chave. Por causa disso, não é possível indexar por valor de chave e outras operações de acesso são fornecidas. Acessível pelo cabeçalho <map>. set Um set é como um map, mas sem valor associado para cada chave. O set apenas guarda a informação se uma dada chave já foi inserida ou não. Acessível pelo cabeçalho <set>. multiset Um multiset é similar a set , mas permite múltiplas inserções para cada chave. Acessível pelo cabeçalho <set>. string Uma string guarda uma cadeia de “caracteres”. O template original é denominado basic_string , sendo que um tipo string é pré-definido para uma cadeia de caracteres tipo char. Acessível pelo cabeçalho < string >. valarray Um valarray é um vetor otimizado para operações numéricas. Acessível pelo cabeçalho < valarray >. bitset Um bitset permite a operação em bits individuais, numa generalização do que os operadores de bit &, | , ^, << e >> fazem para valores inteiros, permitindo que as operações sejam realizadas sobre conjuntos de número arbitrário de bits (desde que o número seja conhecido em tempo de execução). Acessível pelo cabeçalho < bitset >. U NIVERSIDADE DE S ÃO PAULO 14.3 Tipos auxiliares 14.3 129 Tipos auxiliares Cada container fornece um conjunto de tipos auxiliares, que facilitam seu uso e principalmente o desenvolvimento de código independente do container escolhido. Os tipos fornecidos são descritos abaixo. value_type Indica o tipo dos elementos armazenados no container. allocator_type Indica o tipo usado como gerente de alocação para o container. A cada container podemos associar, na sua criação, uma classe que cuidará do gerenciamento da memória para o mesmo; allocator_type retorna este tipo. size_type É o tipo dos subscritos, número de elementos, etc., no container. 1 2 3 4 / / I n i c i a l i z a v e t o r de q u a d r a d o s . vector <int > quadrados ( 1 0 0 ) ; f o r ( v e c t o r < i n t > : : s i z e _ t y p e i = 0 ; i < q u a d r a d o s . s i z e ( ) ; i ++) quadrados [ i ] = i ∗ i ; difference_type Indica o tipo da diferença entre iteradores. iterator É o tipo dos iteradores para elementos do container. const_iterator Idêntico ao iterator , mas indica que o elemento não será alterado por meio deste iterador (equivalente a um ponteiro para constante). reverse_iterator Similar a iterator, mas permiter ver o container de trás para a frente, isto é, quando se avança um reverse_iterator ele vai para trás no container. Útil em conjunto com os algoritmos genéricos a serem descritos abaixo (seção 14.5). const_reverse_iterator Idêntico a reverse_iterator , mas indica que o objeto ao qual o iterador se refere não será alterado por meio do iterador. reference Tipo associado com uma referência para um elemento do container. const_reference Tipo associado com uma referência para constante para um elemento do container. key_type Válido apenas para containers associativos. Tipo da chave. mapped_type Válido apenas para containers associativos. Tipo do valor associado às chaves. key_compare Válido apenas para containers associativos. Tipo do critério de comparação. 14.4 Operações sobre containers Os containers de STL procuram, na medida do possível sem muito prejuízo de desempenho, fornecer uma interface homogênea, isto é, apresentar o mesmo conjunto de métodos para seus objetos e apresentar e interpretar parâmetros de maneira uniforme. Abaixo descreveremos em mais detalhes a interface para o template vector ; os outros templates serão discutidos apenas no que forem diferentes de vector . I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 130 14.4.1 Biblioteca padrão de templates vector A definição do template vector pode ser apresentada como abaixo: 1 2 3 4 5 namespace s t d { t e m p l a t e < c l a s s T , c l a s s A = a l l o c a t o r <T> > v e c t o r { / / etc . . . }; } O tipo T é o tipo dos objetos que serão inseridos no vetor. O tipo A indica um alocador define a interação entre os objetos inserido e os sistemas de gerenciamento de memória. Como indicado acima, a biblioteca fornece uma classe assumida, por sua vez um template definido na biblioteca, que é adequado para a grande maioria dos propósitos. Aqui não entraremos em maiores detalhes sobre a função e operação do alocador. Construtores vector possui um construtor assumido que cria um container vazio e um construtor de cópia que faz a cópia de um outro vetor fornecido. Além desses, existe um construtor que inicializa o vetor com um tamanho especificado e com um valor especificado em cada elemento; se um valor não é fornecido para a inicialização dos elementos, o construtor assumido da classe dos elementos é usado. 1 2 3 4 5 v e c t o r < i n t > v i ; / / v e t o r de i n t e i r o s v a z i o ( sem e l e m e n t o s ) v e c t o r < double > vd ( 1 0 ) ; / / v e t o r de 10 d o u b l e s i n i c i a l i z a d o s com 0 v e c t o r < R a c i o n a l > v r ( 1 0 0 ) ; / / v e t o r de 100 o b j e t o s da c l a s s e R a c i o n a l , / / i n i c i a l i z a d o s a t r a v é s do c o n s t r u t o r d e f a u l t de R a c i o n a l v e c t o r < double > u n s ( 1 0 0 0 , 1 . 0 ) / / v e t o r com 1000 e l e m e n t o s , t o d o s com v a l o r 1 Nada impede que o tipo usado como elemento dos vetores seja mais complexo. Por exemplo, podemos criar uma matriz como vetor de vetores. 1 v e c t o r < v e c t o r < i n t > > m( 1 0 , v e c t o r < i n t > ( 5 ) ) ; Neste código, m é uma matriz de 10 elementos, cada um dos quais do tipo vector <int>; cada um desses elementos, por sua vez, é inicializado pelo construtor que o inicializa com 5 elementos do tipo int. Resumindo, criamos uma matriz de 10 × 5. É importante tomar cuidado para não escrever o código acima como 1 v e c t o r < v e c t o r < i n t >> m( 1 0 , v e c t o r < i n t > ( 5 ) ) ; / / ERRO de s i n t a x e ! pois o compilador irá confundir os dois >> após o int com o operador de extração. Um outro construtor permite criar um novo vetor a partir de elementos de um container. Os elementos a usar são especificados através dos iteradores inicial e final. Iteradores Os métodos begin () e end() devolvem iteradores (ou iteradores constantes) para o primeiro elemento e um elemento após o último, respectivamente. A especificação de uma faixa de elementos em STL é sempre realizada dessa forma: com um iterador para o primeiro elemento da faixa e outro para um elemento após o último. Assim, podemos inicializar todos os elementos de um vetor através de um código como: 1 2 3 v e c t o r < double > v ( 1 0 0 ) ; f o r ( v e c t o r < double > : : i t e r a t o r p = v . b e g i n ( ) ; p ! = v . end ( ) ; p ++) ∗p = 0 ; Note como paramos assim que p foi incrementado para ficar igual a v.end(), pois este já não é um elemento do vetor. Se desejamos percorrer o vetor na direção oposta, podemos usar os iteradores reversos retornados por rbegin () e rend () , que iteradores que apontam para o último elemento e um antes do primeiro, respectivamente: 1 2 3 4 v e c t o r < double > v ( 1 0 0 ) ; int i = 0; f o r ( v e c t o r < double > : : r e v e r s e _ i t e r a t o r p = v . r b e g i n ( ) ; p ! = v . r e n d ( ) ; p ++) ∗p = i ++; Repare que ao incrementar (p++) um iterador reverso estamos na verdade indo para trás no vetor. Assim, no código acima o último elemento do vetor recebe 0, o penúltimo recebe 1 e assim por diante. Se p é um iterador reverso, então p.base () fornece um iterador normal que aponta para o elemento seguinte (na ordem normal) ao elemento apontado por p. Por exemplo, v. rbegin (). base () é o mesmo que v.end() e v.rend (). base () é o mesmo que v.begin () . U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 131 Acesso a elementos O acesso a elementos de um vector pode ser feito, além de por meio dos iteradores, como indicado acima, por meio de quatro outros métodos: o operador de indexação [] , o método de indexação verificada at () e os métodos front () e back(). O operador de indexação é sobrecarregado para um vector de modo a funcionar como para um array normal. Nenhum teste é feito para verificar se o índice fornecido é válido. O método at () permite o acesso com teste de validade do índice: se um índice inválido for fornecido, a exceção out_of_range é lançada. 1 2 3 4 5 6 7 8 9 10 v e c t o r < i n t > num ( 1 0 ) ; f o r ( v e c t o r < i n t > : : s i z e _ t y p e i = 0 ; i < 2 0 ; i ++) num [ i ] = 2 ; / / Oops ! BUG: b u f f e r o v e r f l o w try { f o r ( v e t o r < i n t > : : s i z e _ t y p e i = 0 ; i < 2 0 ; i ++) num . a t ( i ) = 2 ; } catch ( out_of_range ) { c e r r << " P a r e c e que v o c e andou f a z e n d o bobagem ! " << e n d l ; } O primeiro for do código gera problemas em tempo de execução, com comportamento imprevisível. O segundo for gera uma exceção out_of_range, que é capturada pelo tratador indicado. Os métodos front () e back() permitem o acesso direto ao primeiro e ao último elemento do vetor, respectivamente. Não são normalmente muito usados em vetores, a não ser quando estes estão sendo usados para simular outras estruturas de dados. Atribuição É possível realizar atribuição de vetores inteiros, através de um operador de atribuição definido no template. Também é possível fazer atribuição de uma parte de outro container especificada pelos iteradores inicial e final, usando o método assign. 1 2 vector <int > a ( 1 0 0 ) ; vector <int > b , c ; / / I n i c i a l m e n t e vazios . 3 4 5 6 7 8 9 f o r ( v e c t o r < i n t > : : s i z e _ t y p e i = 0 ; i < a . s i z e ( ) ; i ++) a [ i ] = i ; b = a ; / / b v i r a uma c ó p i a de a vector <int > : : c o n s t _ i t e r a t o r ini , f i n ; i n i = a . b e g i n ( ) ; i n i += 1 0 ; f i n = a . end ( ) ; f i n −= 1 0 ; c . a s s i g n ( i n i , f i n ) ; / / c r e c e b e uma c ó p i a d o s e l e m e n t o s 10 a 89 de a Há também um método assign para fazer um dado número de cópias de um valor especificado: 1 2 v e c t o r < double > e ; e . assign (20 , 1 . 3 ) ; / / e inicialmente vazio / / e a g o r a c o n t é m 20 c ó p i a s do v a l o r 1 . 3 Uma atribuição envolve a cópia de todos os elementos do vetor. Isso será muito lento para vetores grandes e pode ser indesejado. Como os vetores usam internamente ponteiros para a área de armazenamento de memória, um método swap é fornecido que permite a troca dos conteúdos de dois vetores. 1 2 3 4 5 6 7 v o i d l a p l a c i a n o ( v e c t o r < double > &s i n a l ) { v e c t o r < double > tmp ( s i n a l . s i z e ( ) ) ; / / tmp tem o mesmo tamanho de s i n a l f o r ( v e c t o r < double > : : s i z e _ t y p e i = 1 ; i < s i n a l . s i z e ( ) − 1 ; i ++) tmp [ i ] = ( s i n a l [ i −1]+2∗ s i n a l [ i ] + s i n a l [ i + 1 ] ) / 4 ; s i n a l . swap ( tmp ) ; } A rotina laplaciano do código acima aplica um laplaciano sobre o sinal unidimensional passado no vetor do parâmetro. O laplaciano consiste em transformar o sinal de acordo com a regra: Sinal0i ← Sinali−1 + 2Sinali + Sinali+1 4 (14.1) No código, os pontos extremos não são considerados por não terem um dos vizinhos necessários e o cálculo deve ser feito sobre um vetor temporário pois o valor original de sinal [ i ] é necessário para o processamento de sinal [ i+1]. Após I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 132 Biblioteca padrão de templates o processamento, os valores de sinal e tmp são trocados pelo método swap, sendo que logo em seguida o vetor tmp sai de escopo e libera seu espaço. Obviamente existem soluções mais eficazes para vetores grandes neste problema, mas o código mostra um uso importante de swap. Operações de pilha Um vetor pode ser operado como uma pilha, através de métodos que inserem e retiram valores no seu final. O método push_back recebe um valor e o insere no final do vetor, aumentando o tamanho do vetor correspondentemente. O método pop_back descarta o último valor do vetor, reduzindo correspondentemente o tamanho do vetor. Nenhum valor é retornado por pop_back; se o valor é desejado, deve ser lido (usando o método back) antes de realizar o pop_back. Essas operações são bastante úteis quando não se sabe de antemão o tamanho que o vetor terá, mas isso apenas é descoberto no processo de inserção dos valores. 1 2 3 # include <iostream > # include <iomanip > # include < vector > 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 i n t main ( ) { s t d : : vector < int > dados ; s t d : : c o u t << " D i g i t e o s v a l o r e s , t e r m i n a d o s p o r um número n e g a t i v o . " << s t d : : e n d l ; while ( true ) { int n ; s t d : : c i n >> n ; i f ( n < 0 ) break ; dados . push_back ( n ) ; } s t d : : c o u t << " Voce d i g i t o u : " << s t d : : e n d l ; f o r ( s t d : : v e c t o r < i n t > : : s i z e _ t y p e i = 0 ; i < d a d o s . s i z e ( ) ; i ++) s t d : : c o u t << s t d : : s e t w ( 8 ) << d a d o s [ i ] ; s t d : : c o u t << s t d : : e n d l ; return 0; } O código abaixo implementa uma pilha de inteiros primitiva usando vector (onde agora a operação pop retorna o valor retirado). 1 2 3 4 5 6 7 8 9 class PilhaInt { std : : vector <int > p ; public : class pilha_vazia {}; void push ( const i n t n ) { p . push_back ( n ) ; } i n t pop ( ) { i f ( v a z i a ( ) ) throw p i l h a _ v a z i a ( ) ; i n t n = p . back ( ) ; p . pop_back ( ) ; return n ; } b o o l v a z i a ( ) c o n s t { r e t u r n ( p . s i z e ( ) == 0 ) ; } }; Operações de lista Também é possível realizar operações de inserção e retirada de elementos mais gerais do que push_back e pop_back. Essas operações são realizadas pelos métodos insert e erase. Existem variantes para inserção e retirada de um elemento e de múltiplos elementos. O local de inserção ou retirada é especificado por iteradores. Se desejamos retirar apenas um elemento especificamos um iterador que aponta para esse elemento; se desejamos retirar múltiplos elementos, especificamos um iterador para o primeiro elemento a retirar e outro para um depois do último a retirar. 1 2 3 4 5 6 7 vector <int > a ( 1 0 ) ; f o r ( i n t i = 0 ; i < 1 0 ; i ++) a [ i ] = i ; / / a = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9} v e c t o r < int > : : i t e r a t o r pi , pf ; p i = a . b e g i n ( ) ; p i += 4 ; a . erase ( pi ) ; / / r e t i r a elemento a [4] / / a = { 0 , 1 , 2 , 3 , 5 , 6 , 7 , 8 , 9} U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 8 9 10 11 133 p i = a . b e g i n ( ) ; p i += 4 ; p f = a . end ( ) ; p f −= 2 ; a . e r a s e ( p i , p f ) ; / / r e t i r a o s e l e m e n t o s de í n d i c e s 4 a 6 do novo v e t o r / / a = { 0 , 1 , 2 , 3 , 8 , 9} Note como o valor de pi precisou ser recalculado após o primeiro erase, apesar de se referir ao mesmo índice do vetor nos dois casos. Isto é obrigatório pois as operações de inserção e retirada de elementos podem provocar deslocamento do vetor, invalidando os iteradores existentes. Na inserção, um iterador especifica o elemento antes do qual os novos elementos serão inseridos. Os valores a inserir podem ser um certo número de cópias de um valor dado ou os valores de elementos de um container especificados por um iterador inicial e um iterador final. 1 2 3 4 5 6 7 8 vector <int > a (10 ,1) , b ( 1 0 , 2 ) ; vector <int > c ( 1 0 , 0 ) ; c . i n s e r t ( c . b e g i n ( ) , 5 , −1); / / i n s e r e 5 v a l o r e s −1 no comeco de c c . i n s e r t ( c . b e g i n ( ) + 1 0 , a . b e g i n ( ) , a . end ( ) ) ; / / i n s e r e uma c o p i a de a d e p o i s d o s / / 10 p r i m e i r o s e l e m e n t o s de c c . i n s e r t ( c . b e g i n ( ) + 2 0 , b . b e g i n ( ) , b . end ( ) ) ; / / i n s e r e uma c o p i a de b d e p o i s d o s / / e l e m e n t o s i n s e r i d o s de a / / Agora c p o s s u i 35 e l e m e n t o s Tamanho e capacidade Como vimos, um vector pode crescer conforme o necessário. Dessa forma, seu tamanho não é fixo de uma vez por todas na criação. Com relação ao espaço ocupado, um vector possui duas características: seu tamanho e sua capacidade. O tamanho corresponde ao número de elementos inseridos no container; a capacidade indica quanto de memória o container tem disponível, possibilitando que ele cresça sem deslocamento para outra posição. Esta última característica existe para evitar que um vetor precise continuamente ser deslocado ao inserir-se elementos aos poucos; o vetor reserva um pouco de espaço adicional, além do necessário para seus elemento, de modo que quando novos elementos são inseridos não é necessário alocar uma nova região de memória para seu armazenamento. O tamanho do vetor, em número de elementos, é retornado pelo método size ; a capacidade do vetor, também em número de elemento, é retornado pelo método capacity . É possível especificar o espaço que desejamos reservar para um vetor através do método reserve . Este método recebe o número de elementos que queremos deixar reservado para o crescimento do vetor. Uma chamada a reserve não afeta o tamanho do vetor, nem inclui novos elementos. Se desejamos mudar o tamanho do vetor (o número de seus elementos) podemos usar o método resize , que recebe o novo tamanho desejado para o vetor. Se o vetor for crescer devido a um resize , os novos elementos serão inicializados com um valor passado como segundo parâmetro; esse segundo parâmetro tem um valor assumido igual ao gerado pelo construtor assumido do tipo mantido pelo vetor. 1 2 3 4 5 6 7 8 9 10 v e c t o r < i n t > a ; / / i n i c i a l m e n t e v a z i o : a . s i z e ( ) == 0 a . r e s e r v e ( 1 0 ) ; / / r e s e r v a e s p a c o p a r a 10 e l e m e n t o s a . p u s h _ b a c k ( 1 ) ; / / a . s i z e ( ) == 1 a . p u s h _ b a c k ( 3 ) ; / / a . s i z e ( ) == 2 a . p u s h _ b a c k ( 7 ) ; / / a . s i z e ( ) == 3 / / t e n t a t i v a de a c e s s o a a [ i ] , i >2 e um e r r o ! v e c t o r < i n t > b ; / / i n i c i a l m e n t e v a z i o : b . s i z e ( ) == 0 ; b. resize (10); / / i n s e r e 10 v a l o r e s 0 no v e t o r b : b . s i z e ( ) == 10 b . resize (20 ,1); / / a g o r a b . s i z e ( ) == 20 e o s u l t i m o s 10 e l e m e n t o s v a l e m 1 b . p u s h _ b a c k ( 1 5 ) ; / / a g o r a b . s i z e ( ) == 21 Outra operação relacionada é a operação empty, que retorna true se o container não tiver nenhum elemento e false caso contrário. Comparações O template vector possui sobrecarga para os operadores de comparação. O operador == retorna true se os dois vetores forem estritamente iguais, de acordo com o operador de comparação dos elementos. O operador < retorna true de acordo com a chamada ordem lexicográfica, segundo a qual dadas duas seqüências a e b, dizemos que a < b se e somente se existe n para o qual ai = bi para i < n e ou o número de elementos de a é menor que n ou an < bn . Os demais operadores de comparação são construídos a partir desses dois. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 134 Biblioteca padrão de templates Vetor de booleanos Um booleano tem apenas dois valores e portanto carrega informação de apenas um bit. Na representação bool é normalmente utilizado um byte inteiro, por questões de eficiência de acesso. Se desejamos armazenar um vetor de booleanos, o uso de um byte para cada um se torna um desperdício de memória e uma solução mais inteligente se faz necessária. Por causa disso STL possui uma implementação especial para vector <bool>, que se utiliza de apenas um bit para cada booleano, mantendo a mesma interface do template geral vector . 1 2 3 4 v e c t o r < bool > s i n a l i z a c a o ( 1 0 0 0 0 0 0 ) ; / / 1 m i l h a o de b o o l e a n o s , u s a n d o 125000 b y t e s s i n a l i z a c a o [ 1 0 ] = t r u e ; / / l i g a o b i t de i n d i c e 10 s i n a l i z a c a o [ 1 0 0 0 ] = t r u e ; / / l i g a o b i t i n d i c e 1000 s i n a l i z a c a o [ 1 0 ] = f a l s e ; / / d e s l i g a o b i t 10 Note como os diversos bits podem ser acessados individualmente, como se fossem booleanos normais. 14.4.2 list Listas são otimizadas para inserção e remoção de elementos. Por outro lado, acesso aleatório através de subscritos seria muito custoso e portanto não é fornecido. Da mesma forma, um posicionamento aleatório através de iteradores seria custoso e não é possível; quer dizer, os iteradores de lista não permitem aritmética (não é possível somar um inteiro a um iterador, como fizemos acima com os iteradores de vetores) além das operações de incremento e decremento. Se queremos andar quatro elementos para a frente na lista, precisamos incrementar o iterador quatro vezes. Os iteradores são nesse caso chamados iteradores bidirecionais ao invés de iteradores de acesso aleatório, como o de vetores. Também não existe o conceito de capacidade para listas e portanto os métodos capacity e reserve não são implementados. Todos os outros métodos de vector são implementados e têm funcionamento semelhante para list . Além disso, list implementa alguns métodos adicionais, que serão discutidos a seguir. Reorganização de listas O container list oferece algumas operações que provocam mudanças na estrutura da lista e que seriam caras de implementar em vetores. A operação splice permite mover elementos, sem realizar cópias, de uma lista para outra. Podemos mover apenas um elemento, uma seqüência de elementos ou uma lista inteira. Os elementos a serem movidos são especificados através de sua lista original e iteradores para indicar a faixa a mover (caso não queiramos mover a lista inteira). 1 2 3 4 5 6 7 8 9 10 11 12 13 l i s t <int > a , b ; a . push_back ( 1 ) ; a . push_back ( 3 ) ; a . push_back ( 5 ) ; b . push_back ( 2 ) ; b . push_back ( 4 ) ; b . push_back ( 6 ) ; b . push_back ( 7 ) ; b . push_back ( 8 ) ; b . push_back ( 9 ) ; l i s t <int > : : i t e r a t o r p = a . begin ( ) ; p ++; a . s p l i c e ( p , b , b . b e g i n ( ) ) ; / / t i r a o 2 da l i s t a b p = a . b e g i n ( ) ; p ++; p ++; p ++; a . s p l i c e ( p , b , b . b e g i n ( ) ) ; / / t i r a o 4 da l i s t a b p = b . end ( ) ; p−−; p−−; a . s p l i c e ( a . end ( ) , b , b . b e g i n ( ) , p ) ; / / t i r a o 6 a . s p l i c e ( a . end ( ) , b ) ; / / c o l o c a t o d o o r e s t o de / / a g o r a a tem 1 2 3 4 5 6 7 8 9 e b e s t a v a z i a e c o l o c a e n t r e 1 e 3 da a e c o l o c a e n t r e 3 e 5 na a e o 7 de b e c o l o c a no f i n a l de a b no f i n a l de a A operação merge realiza a fusão de duas listas ordenadas, gerando uma lista ordenada. Para que a operação funcione, as duas fornecidas deve estar originalmente ordenadas; se alguma das listas não estiver ordenada, o resultado ainda vai ser uma lista com todos os elementos das duas listas fornecidas, mas não existem garantias quanto à ordem na lista resultante. 1 2 3 4 5 6 l i s t <int > a , b ; a . push_back ( 1 ) ; a . push_back ( 3 ) ; a . push_back ( 5 ) ; b . push_back ( 2 ) ; b . push_back ( 4 ) ; b . push_back ( 6 ) ; b . push_back ( 7 ) ; b . push_back ( 8 ) ; b . push_back ( 9 ) ; a . merge ( b ) ; / / a g o r a a tem 1 2 3 4 5 6 7 8 9 e b e s t a v a z i a Outra operação desse tipo é a operação sort , que retorna a lista ordenada de acordo com os operadores de comparação dos elementos. 1 2 l i s t <int > val ; c o u t << " D i g i t e v a l o r e s i n t e i r o s t e r m i n a d o s p o r um número n e g a t i v o : " << e n d l ; U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 3 4 5 6 7 8 9 10 11 12 13 135 while ( true ) { int n ; c i n >> n ; i f ( n < 0 ) break ; val . push_back ( n ) ; } val . sort ( ) ; c o u t << " S e u s v a l o r e s em ordem : " << e n d l ; f o r ( l i s t < i n t > : : i t e r a t o r p = v a l . b e g i n ( ) ; p ! = v a l . end ( ) ; p ++) c o u t << ∗p << " " ; c o u t << e n d l ; Tanto sort como merge podem receber como parâmetro opcional um objeto funcional, isto é, um objeto que será usado como uma função, que fornece um critério de comparação. Templates de objetos funcionais para as comparações mais comuns de elementos são definidos em < functional >. Por exemplo, para ordenar em ordem decrescente, podemos fazer: 1 2 3 4 l i s t <int > a ; /∗ . . . ∗/ a . s o r t ( greater <int > ( ) ) ; / / a g o r a o o p e r a d o r > e u s a d o ao i n v e s de < p a r a a s c o m p a r a c o e s As comparações fornecidas por < functional >, todas templates para serem adaptadas aos diversos tipos, são equal_to, not_equal_to, greater , less , greater_equal , less_equal , logical_and , logical_or e logical_not , com significados óbvios pelos nomes. O usuário pode também gerar seus próprios objetos funcionais, simplesmente declarando um classe que sobrecarrega o operador de chamada de função () e retorna um booleano com o valor adequado à comparação desejada. Existe também um algoritmo sort , que será discutido na seção 14.5; a diferença deste método é que o algoritmo faz a reordenação copiando os valores, enquanto este método apenas troca os elementos de lugar na lista, o que pode ser vantajoso no caso de elementos grandes. Operações no início Um vetor pode crescer no final, mas não no começo. Isso porque a inserção de um elemento no começo implicaria o deslocamento de todos os elemento existentes. Numa lista, geralmente implementada com ponteiros, esse problema não existe. Por isso, o container list fornece operações no início da lista similares às fornecidas no final, chamadas push_front e pop_front, além da operação front , já existente em vector . 1 2 3 4 5 l i s t <int > a ; f o r ( i n t i = 0 ; i < 5 ; i ++) { a . push_back ( i ) ; a . p u s h _ f r o n t ( i ) ; } / / a a g o r a tem 4 3 2 1 0 0 1 2 3 4 Outras operações de remoção Dada a eficiência com que elementos podem ser removidos de uma lista, algumas operações adicionais de remoção são fornecidas. A operação remove apaga todos os elementos da lista que tenham um valor especificado; a operação unique remove todos os elementos duplicados consecutivos da lista, deixando apenas uma cópia de cada valor. Se quisermos ficar com apenas um elemento de cada valor, podemos ordenar a lista antes do unique. 1 2 3 4 5 6 7 8 9 10 11 12 l i s t <int > a ; f o r ( i n t i = 0 ; i < 5 ; i ++) { a . p u s h _ b a c k ( i ) ; a . p u s h _ b a c k ( i ) ; } / / a == 0 0 1 1 2 2 3 3 4 4 a . remove ( 2 ) ; / / a == 0 0 1 1 3 3 4 4 a . push_front (4); a . push_front (3); / / a == 3 4 0 0 1 1 3 3 4 4 a . unique ( ) ; / / a == 3 4 0 1 3 4 a . sort (); a . unique ( ) ; / / a == 0 1 3 4 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 136 Biblioteca padrão de templates O método unique aceita como parâmetro opcional um objeto funcional que fornece o critério de comparação, em substituição ao critério assumido ==, por exemplo para comparar cadeias de caracter sem considerar diferenças entre maiúsculas e minúsculas. Outro método de remoção é o remove_if, que aceita um objeto funcional que especifica um critério; para cada objeto, se ele satisfaz o critério ele é removido. 1 2 3 4 5 6 7 8 9 10 c l a s s impar { public : b o o l o p e r a t o r ( ) ( i n t a ) { r e t u r n ( a%2 ! = 0 ) ; } }; /∗ . . . ∗/ l i s t <int > a ; f o r ( i n t i = 0 ; i < 5 ; i ++) { a . p u s h _ b a c k ( i ) ; a . p u s h _ b a c k ( i ) ; } / / a == 0 0 1 1 2 2 3 3 4 4 a . remove_if ( impar ( ) ) ; / / a == 0 0 2 2 4 4 14.4.3 deque Um deque é uma fila de duas entradas, que permite inserção em ambos os extremos. Ela possui todas as operações de vetor, exceto capacity e resize adicionadas das operações push_front e pop_front de lista. Operações de inserção e retirada no meio têm eficiência (ruim) similar à de vector , mas permitem operações de indexação (com eficiência similar à de vector ), ao contrário de list . 14.4.4 stack Um stack é, conforme dito acima, apenas um adaptador de container, no sentido de que ele é implementado usando outro container. Se nenhum container básico específico é escolhido, então deque é usado como base de implementação. 1 2 s t a c k < double > a ; / / d e c l a r a s t a c k de d o u b l e com d e q u e como i m p l e m e n t a c a o s t a c k < double , v e c t o r < double > > b ; / / e s t a s t a c k tem um v e c t o r como i m p l e m e n t a ç ã o O método top retorna o valor no topo da pilha; os métodos push e pop fazem as operações de inserção e retirada de elementos. 1 2 3 4 5 6 7 8 stack <int > s ; for ( int i = 10; i < w h i l e ( ! s . empty ( ) ) c o u t << s . t o p ( ) << s . pop ( ) ; } c o u t << e n d l ; / / i m p r i m e : 19 18 17 14.4.5 2 0 ; i ++) s . p u s h ( i ) ; { " "; 16 15 14 13 12 11 10 queue O container queue é um adaptador que permite a inserção no final e a retirada no começo. As operações de inserção e retirada são chamada também push e pop respectivamente, não devendo ser confundidas com as de stack, pois o pop na verdade retira um elemento do começo, ao invés do final. Para ler os elementos podemos usar front ou back, já discutidos. 1 2 3 4 5 6 7 8 queue < i n t > q ; f o r ( i n t i = 1 0 ; i < 2 0 ; i ++) q . p u s h ( i ) ; w h i l e ( ! q . empty ( ) ) { c o u t << q . f r o n t ( ) << " " ; q . pop ( ) ; } c o u t << e n d l ; / / i m p r i m e : 10 11 12 13 14 15 16 17 18 19 U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 14.4.6 137 priority_queue Uma priority_queue é uma fila em que os elementos não são retirados na ordem de entrada, mas de acordo com uma prioridade estabelecida por um objeto funcional de comparação. Se nenhum objeto de comparação é especificado, o operador < é usado, isto é, ao retirar um elemento da fila, retiramos o maior de todos. O elemento de maior prioridade é retornado pelo método top; os elementos são inseridos e retirados pelos métodos push e pop, respectivamente. 1 2 3 4 5 6 7 8 9 10 11 12 13 p r i o r i t y _ q u e u e < i n t > q ; / / f i l a de p r i o r i d a d e com o p e r a d o r < while ( true ) { int n ; c i n >> n ; i f ( n < 0 ) break ; q . push ( n ) ; } w h i l e ( ! q . empty ( ) ) { c o u t << q . t o p ( ) << " " ; q . pop ( ) ; } c o u t << e n d l ; / / d a d o s f o r n e c i d o s f o r a m i m p r i m i d o s em ordem d e c r e s c e n t e 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 / / f i l a de p r i o r i d a d e com o p e r a d o r > priority_queue < int , vector <int > , greater <int > > rq ; while ( true ) { int n ; c i n >> n ; i f ( n < 0 ) break ; rq . push ( n ) ; } w h i l e ( ! r q . empty ( ) ) { c o u t << r q . t o p ( ) << " " ; r q . pop ( ) ; } c o u t << e n d l ; / / d a d o s f o r n e c i d o s f o r a m i m p r i m i d o s em ordem c r e s c e n t e 14.4.7 map Os iteradores de um map são bidirecionais (não de acesso aleatório). Os elementos do tipo usado como chave devem fornecer um operador <. O critério de comparação das chaves pode opcionalmente ser fornecido na declaração do map. Os pares (chave, valor) são armazenados como elementos do template pair . Um pair possui dois campos, chamados first e second, que podem ser de tipos diferentes. 1 2 3 pair <int , s t r i n g > id ; id . f i r s t = 23456789; id . second = " T r a i r a " ; Um iterador de map aponta para um pair , cujo membro first é do tipo da chave, enquanto second é do tipo do valor. O tipo value_type associado com o map é equivalente ao par de chave e valor. Indexação A característica especial de map é que sua indexação é pelo valor da chave, quer dizer, o índice não precisa ser inteiro, mas pode ser de um tipo arbitrário. Quando um map é indexado por um valor de chave, uma busca é feita por um valor com a chave associada; se nenhum valor é encontrado para a chave, um elemento com a chave o default do tipo do valor é inserido automaticamente no container. 1 2 3 4 5 6 7 map< s t r i n g , i n t > i d a d e ; i d a d e [ " Ze " ] = 1 9 ; i d a d e [ " Maria " ] = 18; idade [ " Joao " ] = 21; / / i d a d e tem a g o r a t r e s c o u t << i d a d e [ " M a r i a " ] ; c o u t << i d a d e [ " P e d r o " ] ; I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS / / mapa v a z i o de i d a d e s p a r e s : ( Ze , 1 9 ) , ( Maria , 1 8 ) e ( Joao , 2 1 ) / / i m p r i m e 18 / / i m p r i m e 0 , e i n s e r e Pedro no mapa 138 8 Biblioteca padrão de templates / / i d a d e tem a g o r a ( Ze , 1 9 ) , ( Maria , 1 8 ) , ( Joao , 2 1 ) , ( Pedro , 0 ) O método find pode ser usado para verificar se existe um valor associado com uma chave especificada, sem recorrer na inserção de uma chave com valor zero, como no caso do operador de indexação. Se um valor associado à chave for encontrado, o método retorna um iterador para o elemento. Caso nenhum valor seja encontrado, é retornado um iterador equivalente ao retornado por pelo método end. 1 2 3 4 5 6 map< s t r i n g , i n t > i d a d e ; /∗ . . . ∗/ i f ( a . f i n d ( " M a r c e l o " ) ! = a . end ( ) ) c o u t << "A i d a d e do M a r c e l o e " << a [ " M a r c e l o " ] << " a n o s . " << e n d l ; else c o u t << " D e s c u l p e , nao s e i a i d a d e do M a r c e l o ! " << e n d l ; Operações de lista As operações de inserção e remoção de elementos podem ser utilizadas em um map. Somente deve-se considerar que cada elemento inserido consiste em um par com chave e valor. A remoção pode ser efetuada especificando apenas a chave. Ao especificar iteradores para a remoção, podemos considerar que as entradas no map estão ordenadas em ordem crescente de acordo com o operador de comparação da classe da chave, ou então com uma classe funcional de comparação fornecida na criação do map. 1 2 3 4 map< s t r i n g , i n t > t e l ; / / mapa o r d e n a d o p e l a ordem da c l a s s e s t r i n g ( l e x i c o g r á f i c a ) map< s t r i n g , i n t , SemCaso > b ; / / mapa o r d e n a d o de a c o r d o com a c l a s s e SemCaso / ∗ . . . P r e e n c h e o mapa t e l e f o n e s . . . ∗ / t e l . e r a s e ( t e l . f i n d ( " Maria " ) , t e l . f i n d ( " Maria Z u l e i c a " ) + + ) ; / / e l i m i n a as Marias . 14.4.8 multimap A diferença entre multimap e map é, conforme já dito, que um multimap permite acrescentar mais do que um valor para cada chave, por exemplo, mais do que um telefone para cada pessoa. Como temos possivelmente mais do que um valor para cada chave, o operador de indexação não é mais adequado. Ao invés, devemos usar as operações equal_range ou lower_bound e upper_bound para consultar valores e insert para inseri-los. 1 2 3 4 5 6 7 8 9 multimap < s t r i n g , i n t > t e l ; t e l . i n s e r t ( m a k e _ p a i r ( s t r i n g ( " Ana " ) , 2 7 5 1 1 1 1 ) ) ; t e l . i n s e r t ( m a k e _ p a i r ( s t r i n g ( " Ana " ) , 9 1 1 1 2 2 2 2 ) ) ; t e l . i n s e r t ( make_pair ( s t r i n g ( " Beto " ) , 3 3 7 3 3 3 3 3 ) ) ; t e l . i n s e r t ( make_pair ( s t r i n g ( " Cesar " ) , 2 7 7 2 1 2 1 ) ) ; / / B r i g a com a Ana t y p e d e f multimap < s t r i n g , i n t > : : c o n s t _ i t e r a t o r TI ; p a i r <TI , TI > a = t e l . e q u a l _ r a n g e ( " Ana " ) ; t e l . e r a s e ( a . f i r s t , a . second ) ; A função make_pair constrói um par do tipo apropriado com os valores fornecidos. O uso de um typedef para simplificar a declaração quando temos tipos de nomes complexos é uma técnica muito utilizada. A extração dos limites no mapa podem ser requisitados separadamente pelas métodos lower_range e upper_range, ao invés de por equal_range; no entanto, os pedidos separados normalmente envolvem um custo maior. A busca e remoção das entradas correspondentes a Ana no código acima poderia ser feita como abaixo: 1 2 3 TI i n i , f i n ; i n i = t e l . l o w e r _ r a n g e ( " Ana " ) ; f i n = t e l . u p p e r _ r a n g e ( " Ana " ) ; t e l . erase ( ini , fin ) ; Outra operação útil para multimap é a operação count, que retorna o número de valores para uma chave especificada. Por exemplo, antes de remover as entradas para Ana do multimap tel , uma chamada tel . count("Ana") retornaria 2. 14.4.9 set Um set pode ser entendido como um map que não associa um valor à chave, mas apenas indica se a chave foi inserida ou não. A operação é similar à de map, mas sem operador de indexação. U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 1 2 3 4 5 6 139 s e t < s t r i n g > amigos ; a m i g o s . i n s e r t ( " Ana " ) ; amigos . i n s e r t ( " Beto " ) ; amigos . i n s e r t ( " Cesar " ) ; / / B r i g a com Ana a m i g o s . e r a s e ( " Ana " ) ; 14.4.10 multiset O container multiset está para multimap assim como set está para map. 1 2 3 4 5 6 7 multiset <string > carros ; carros . i n s e r t ( " Porsche " ) ; carros . insert ( " Ferrari " ); carros . insert ( " Ferrari " ); carros . insert ( " Ferrari " ); carros . i n s e r t ( " Civic " ) ; / / c o u t << " Tenho " << c a r r o s 14.4.11 / / Para mim / / Para mim / / Para a e s p o s a / / Para o f i l h o Para a f a x i n e i r a . c o u n t ( " F e r r a r i " ) << " F e r r a r i s . " << e n d l ; bitset Um bitset <N> é um array de N bits. Ao contrário de vector <bool>, o tamanho é fixo; ao contrário de set , a indexação é por inteiros e não associativa (portanto mais eficiente); também distingue-se tanto de vector <bool> como de set pelo fato de fornecer diversas operações de manipulação de bits. Um bitset não fornece iteradores. O operador de indexação é fornecido e, ao contrário de vector , é verificado, isto é, se um índice ilegal for acessado uma exceção out_of_range será disparada. Construtores Um construtor assumido inicializa o bitset com todos os bits em zero ( false ). Também existe um construtor que aceita um unsigned long int; neste caso, os bits menos significativos do bitset são inicializados com os correspondentes bits do valor fornecido. Outra possibilidade é inicializar com uma cadeia de caracteres; isto faz com que os bits menos significativos do bitset sejam inicializados com os valores fornecidos na cadeia. Se a cadeia possuir algum caracter diferente de ’0’ ou ’1’, uma exceção invalid_argument será lançada. 1 2 3 4 bitset bitset bitset bitset <10> <10> <10> <10> a; b (0 xff ) ; c ( s t r i n g ( " 110110 " ) ) ; d ( s t r i n g ( " 124 " ) ) ; // // // // 0000000000 0011111111 0000110110 e q u i v a l e n t e a throw i n v a l i d _ a r g u m e n t ( ) ; Manipulação de bits A manipulação dos bits no bitset é possível através da sobrecarga dos operadores de atribuição com operações de bit: &=, |=, ^=, <<=, >>=. 1 2 3 4 5 6 b i t s e t <12> a ( s t r i n g ( " 001100110011 " ) ) ; a &= 0 x f f c ; / / a == 0 0 1 1 0 0 1 1 0 0 0 0 ; 0 x f f c f o i t r a n s f o r m a d o em b i t s e t p e l o c o n s t r u t o r a | = b i t s e t <12 >( s t r i n g ( " 110000000000 " ) ) ; / / a == 111100110000 a ^= b i t s e t <12 >( s t r i n g ( " 111111111111 " ) ) ; / / a == 000011001111 a <<= 2 ; / / a == 001100111100 a >>= 2 ; / / a == 000011001111 Note que os deslocamentos são lógicos, isto é, os bits mais significativos vão sendo descartados e nos menos significativos vão sendo inseridos zeros. Os operadores binários &, | e ^ também são implementados. 1 2 3 4 5 6 b i t s e t <5> a ( s t r i n g ( " 01011 " ) ) ; b i t s e t <5> b ( s t r i n g ( " 11001 " ) ) ; b i t s e t <5> c , b , e ; c = a & b ; / / c == 01001 d = a | b ; / / d == 11011 e = a ^ b ; / / e == 10010 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 140 Biblioteca padrão de templates Outras operações de manipulação de bits são: set sem parâmetros coloca todos os bits em um (true); com parâmetros recebe um índice que indica o bit a mudar e opcionalmente um valor, que indica se o bit dever ser colocado em 0 ou 1 (o valor assumido é um); reset sem parâmetros coloca todos os bits em zero; com um parâmetro coloca o bit na posição indicada em zero; flip sem parâmetros muda o valor de todos os bits; com um parâmetro muda o valor do bit indicado. 1 2 3 4 5 6 7 8 9 b i t s e t <12> a ( s t r i n g ( " 001100110011 " ) ) ; a [0] = 0; / / a == 0 0 1 1 0 0 1 1 0 0 1 0 ; o s i n d i c e s s a o a . set (2); / / a == 0 0 1 1 0 0 1 1 0 1 1 0 ; mesmo que a [ 2 ] a . s e t ( 4 , 0 ) ; / / a == 0 0 1 1 0 0 1 0 0 1 1 0 ; mesmo que a [ 4 ] a . r e s e t ( 5 ) ; / / a == 0 0 1 1 0 0 0 0 0 1 1 0 ; mesmo que a [ 5 ] a . f l i p ( 1 1 ) ; / / a == 101100000110 a. flip (); / / a == 010011111001 a . set (); / / a == 111111111111 a . reset (); / / a == 000000000000 de t r a s p a r a f r e n t e = 1 = 0 = 0 Outras operações Um bitset dispõe dos operadores de comparação == e !=. O número de bits é retornado por size ; o número de bits com valor 1 é retornado por count; se a chamada test ( i ) retorna true, o bit i está ligado; se a chamada any() retorna true, então existe pelo menos um bit ligado; se a chamada none() retorna true então nenhum bit está ligado. Os métodos to_ulong e to_string fazem o trabalho oposto do construtor: retornam um unsigned long ou string correspondentes ao valor do bitset . Se o bitset não cabe em um unsigned long, a exceção overflow_error é lançada. Também são sobrecarregados os operadores de inserção e extração em stream, possibilitando a escrita e leitura de bitset s. 14.4.12 string O template para cadeias de caracteres é basic_string . Cadeias de caracteres char e wchar_t são definidas por STL como 1 2 t y p e d e f b a s i c _ s t r i n g <char > s t r i n g ; t y p e d e f b a s i c _ s t r i n g <wchar_t > w s t r i n g ; Para possibilitar uma implementação mais eficiente de cadeias, uma basic_string não tem toda a flexibilidade de um container. Em especial, não é permitido gerar uma basic_string com objetos que tenham construtor de cópia, destruidor ou atribuição definidos pelo usuário. O template basic_string fornece os mesmos tipos (value_type, etc.) e os mesmos iteradores ( iterator , etc.) fornecidos por vector . Também o operador de indexação [] e o método at são fornecidos, mas não os métodos front e back. Um sinônimo para size é fornecido no método length : ambos retornam o número de caracteres na cadeia (não existe necessariamente um zero final, como numa cadeia representada por um char∗). Construtores Um basic_string tem um construtor assumido que inicializa com uma cadeia vazia. Ele também pode ser inicializado por meio de um cadeia C padrão (char∗), outra basic_string , um pedaço de uma cadeia C, um pedaço de uma basic_string , uma seqüência de caracteres delimitada por iteradores ou uma repetição de um caracter especificado. Os pedaços de cadeias são indicados por um índice inicial e o número de elementos a copiar. 1 2 3 4 5 6 7 8 9 10 string string string string string string string char t string string s1 ; / / cadeia v a z i a s 2 = " " ; / / tambem c a d e i a v a z i a s 3 ( " P a p a i Noel " ) ; / / c a d e i a n a t a l i n a s4 = s3 ; / / o u t r a cadeia n a t a l i n a s 5 ( s3 , 5 ) ; / / c a d e i a f a m i l i a r s 6 ( s5 , 2 , 3 ) ; / / um pouco m a i s f o r m a l s 7 ( " P a p a i " , 2 , 3 ) ; / / o mesmo que s 6 [5] = { ’ t ’ , ’ i ’ , ’ t ’ , ’ i ’ , ’a ’ }; s 8 (& t [ 0 ] , &t [ 5 ] ) ; / / m a i s uma c a d e i a f a m i l i a r s9 ( 1 0 , " 0 " ) ; / / dez z e r o s Erros Sempre que um índice incorreto é fornecido, uma exceção out_of_range é lançada. Já na especificação de tamanhos (por exemplo, número de caracteres a copia na inicialização), um tamanho muito grande simplesmente indica que a operação U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 141 será efetuada até o fim da cadeia. É até fornecida uma constante npos, que corresponde ao maior tamanho possível de uma cadeia, para ser usada na especificação de tamanhos quando queremos nos referir a operações até o final da cadeia. 1 2 3 4 string string string string a ( " Zico " ) ; b ( a , 0 , 1 0 0 ) ; / / Z i c o nao tem 100 c a r a c t e r e s , e n t a o c o p i a t u d o c ( a , 1 , s t r i n g : : n p o s ) ; / / c == i c o d ( a , 10 , 3 ) ; / / throw out_of_range ( ) ! Como tanto os índices como os tamanhos são representados por números sem sinal, o fornecimento de um número negativo é equivalente a fornecer um número positivo muito grande. Atribuição São fornecidos operadores de atribuição que aceitam, à direita, tanto outras basic_string correspondentes como ponteiros para os caracteres ou mesmo caracteres individuais. Atribuição de caracteres individuais é especialmente útil em +=, que permite então adicional um caracter ao final de uma cadeia. O método assign permite atribuir a uma cadeia trechos de outras cadeias ou seqüências de caracter, usando a mesma sintaxe que os construtores. 1 2 3 4 5 6 7 8 9 s t r i n g lu ; lu = " Lucia " ; s t r i n g lu2 ; lu2 = lu ; lu2 [2] = ’z ’ ; / / Luzia string a; a = ’a ’ ; s t r i n g dia ; d i a . a s s i g n ( l u 2 , 0 , 3 ) ; / / v i u a l u z do d i a Conversão para cadeias C Em muitas situações é importante converter uma string para uma cadeia tradicional C (char∗), por exemplo para passar essa cadeia a uma rotina que espera um char∗. Para realizar isso temos três métodos: c_str , data e copy. c_str retorna um ponteiro para uma cadeia de caracteres terminada por zero; data não coloca o zero final. Esses dois métodos mantém propriedade sobre o array que guarda os dados; portanto não se deve fazer um delete ou free no ponteiro retornado. Se os dados vão ser manipulados externamente, é necessário fazer uma cópia usando o método copy. Esse método recebe um ponteiro para o array onde a cópia deve ser realizada e um valor indicando o número máximo de caracteres que podem ser copiados no array. Opcionalmente, pode-se fornecer um índice inicial para a cópia. Note que neste caso a especificação de índice inicial e tamanho fica ao contrário da dos construtores e métodos de atribuição. 1 2 3 4 s t r i n g p r e s i d e n t e ( " Deodoro da F o n s e c a " ) ; p r i n t f ( "O p r i m e i r o p r e s i d e n t e : %s \ n " , p r e s i d e n t e . c _ s t r ( ) ) ; / / quem u s a p r i n t f ? char ∗ p r e = new char [ p r e s i d e n t e . l e n g t h ( ) + 1 ] ; p r e s i d e n t e . copy ( p r e , s t r i n g : : n p o s ) ; / / v a i c a b e r t u d o , n e s t e c a s o Comparação As cadeias podem ser comparadas com outras cadeias do mesmo tipo ou com vetores de caracteres do mesmo tipo dos da cadeia. Além dos operadores usuais de comparação, ==, !=, <, >, <= e >=, é fornecido também o método compare. Ao contrário dos operadores de comparação, que retornam um booleano, uma chamada s . compare(s2) ou similar retorna 0 se s e s2 são iguais, −1 se s é anterior a s2 e 1 se s é posterior a s2. Todas as comparações são lexicográficas. O método também permite comparação de trechos das cadeias, fornecendo a posição e tamanho do trecho desejado. 1 2 3 4 5 6 7 8 9 10 11 s t r i n g a l ( " Ana L u c i a " ) ; s t r i n g a j ( " Ana J u l i a " ) ; bool a , b , c , d , e , f ; int g , h , i ; a = a l == a j ; / / f a l s e b = a l != a j ; / / t r u e c = al < aj ; / / false d = al > aj ; / / true e = a l <= a j ; / / f a l s e f = a l >= a j ; / / t r u e g = a l . compare ( a j ) ; / / 1 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 142 12 13 Biblioteca padrão de templates h = a l . compare ( 0 , 3 , a j ) ; / / −1 i = a l . compare ( 0 , 3 , a j , 0 , 3 ) ; / / 0 Busca Existe uma grande variedade de métodos para busca de caracteres ou subcadeias em uma cadeia. A subcadeias a comparar podem ser especificadas por uma basic_string ou um ponteiro para o tipo de caracter básico. Opcionalmente, pode-se especificar o ponto inicial para a comparação. No caso de comparar com uma cadeia fornecida por ponteiro, pode-se especifica o número de elementos da cadeia fornecida a usar na comparação. O método find realiza a busca de uma subcadeia começando no começo da cadeia. O método rfind procura a subcadeia de trás para frente. O método find_first_of interpreta a cadeia fornecida como um conjunto de caracteres e procura na cadeia a primeira ocorrência de um deles. O método find_first_not_of usa a mesma interpretação, mas busca a primeira ocorrência de um caracter que não seja um dos especificados. Os métodos find_last_of e find_last_not_of são equivalentes, mas procuram de trás para frente. O valor retornado por esses métodos é o índice do caracter encontrado ou do começo da subcadeia encontrada. 1 2 3 4 5 6 7 8 9 10 11 12 13 s t r i n g nome = " J a n a i n a " ; string : : size_type a , b , c , d , e , f ; a = nome . f i n d ( " na " ) ; / / a == 2 b = nome . r f i n d ( " na " ) ; / / b == 5 c = nome . f i n d _ f i r s t _ o f ( " na " ) ; / / a == 1 d = nome . f i n d _ l a s t _ o f ( " na " ) ; / / d == 6 e = nome . f i n d _ f i r s t _ n o t _ o f ( " na " ) ; / / e = 0 f = nome . f i n d _ l a s t _ n o t _ o f ( " na " ) ; / / f == 4 g = nome . f i n d _ f i s t _ o f ( ’ i ’ ) ; / / g == 4 h = nome . f i n d ( " na " , 3 ) ; / / h == 5 ( comecou b u s c a em nome [ 3 ] ) i = nome . r f i n d ( " na " , 3 ) ; / / i == 2 ( comecou b u s c a em nome [ 3 ] ) j = nome . f i n d ( " n a d a " , 0 , 2 ) ; / / j == 2 ( comecou em nome [ 0 ] , u s a a p e n a s 2 c a r a c t e r e s de " nada " ) Operações de alteração Várias operações permitem a modificação de uma cadeia de caracteres. Uma delas é o operador +=, que permite adicionar um cadeia (especificada através de uma basic_string ou de um ponteiro para o tipo de caracter básico) ou um ou caracter no final. Isso pode ser feito também pelo método append, que permite maior flexibilidade especificando o número de caracteres da cadeia fornecida a serem inserido ou o número de cópias do caracter fornecido. 1 2 3 4 5 6 7 8 9 10 11 12 13 s t r i n g b a g u n c a = " Dada " ; s t r i n g dado = " cub " ; b a g u n c a += " ismo " ; / / " Dadaismo " dado += " i s t a " ; / / " c u b i s t a " b a g u n c a += ’ ’ ; b a g u n c a += dado ; / / " Dadaismo c u b i s t a " b a g u n c a . a p p e n d ( " de v a n g u a r d a " ) ; / / " Dadaismo c u b i s t a de v a n g u a r d a " bagunca . append ( " m o d e r n i s t a e c h a t o " , 1 1 ) ; / / " Dadaismo c u b i s t a de v a n g u a r d a m o d e r n i s t a " bagunca . append ( 5 , ’ ! ’ ) ; / / " Dadaismo c u b i s t a de v a n g u a r d a m o d e r n i s t a ! ! ! ! ! " b a g u n c a . a p p e n d ( " E l e d i s s e : Ufa , a c a b o u ! " , 1 0 , 1 3 ) ; / / p o s i c a o i n i c i a l e tamanho / / " Dadaismo c u b i s t a de v a n g u a r d a m o d e r n i s t a ! ! ! ! ! Ufa , acabou ! " Também é possível inserir uma cadeia numa posição arbitrária, usando o método insert , que recebe como primeiro parâmetro o índice do elemento antes do qual os novos caracteres serão inseridos. De resto as variantes são como para append. 1 2 3 4 string parent string parent parent = " () " ; . i n s e r t ( 1 , " a p e n a s um c o m e n t a r i o " ) ; / / " ( a p e n a s um c o m e n t a r i o ) " a l t o = " mais f o r t e " ; . i n s e r t ( 7 , a l t o , 0 , 5 ) ; / / " ( a p e n a s m a i s um c o m e n t a r i o ) " Outra opção de inserção é usar um iterador como indicador do ponto de inserção, ao invés de um índice; neste caso ou inserimos um caracter, ou um número de cópias de um caracter, ou uma faixa de uma cadeia também especificada por dois iteradores. U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 1 2 3 4 5 string string string string string 143 a ( " aa " ) ; b ( " bbbb " ) ; a . i n s e r t ( a . b e g i n ( ) + 1 , b . b e g i n ( ) , b . end ( ) ) ; / / " abbbba " a . i n s e r t ( a . begin ( ) + 3 , 2 , ’ c ’ ) ; / / " abbccbba " a . i n s e r t ( a . end ( ) , ’ d ’ ) ; / / " a b b c c b b a d " Para concatenar duas cadeias pode-se usar o operador +, que permite a concatenação tanto de basic_string s entre si como com caracteres isolado ou cadeias representadas por ponteiros. 1 2 3 4 s t r i n g nome_todo ( c o n s t s t r i n g &p r i m e i r o , c o n s t s t r i n g &s o b r e n o m e ) { return p r i m e i r o + ’ ’ + sobrenome ; } Substituições de subcadeias de uma cadeia também são possíveis através do método replace . A subcadeia a ser substituída deve ser especificada por meio de um índice inicial e número de elementos ou por dois iteradores. A nova subcadeia tem opções semelhantes de especificação de append. Não é necessário que a nova subcadeia seja do mesmo tamanho que a original: a basic_string irá re-adaptar seu tamanho de acordo com o necessário. 1 2 3 4 5 6 s t r i n g d i t o = "O b a r r a c o do Ze P e c h i n c h a f i c a na f a v e l a . " ; / / Ganhou na l o t e r i a d i t o . r e p l a c e ( 0 , 1 , "A" ) ; d i t o . r e p l a c e ( d i t o . f i n d ( " b a r r a c o " ) , 7 , " mansao " ) ; d i t o . r e p l a c e ( d i t o . f i n d ( " Ze P e c h i n c h a " ) , 1 2 , " S r . J o s e P e r e i r a " ) ; d i t o . r e p l a c e ( d i t o . end () −10 , d i t o . end () −1 , "em A l p h a v i l l e " ) ; Extração de subcadeias O método substr retorna uma nova basic_string que é uma subcadeia definida pelo índice inicial e pelo número de elementos. 1 2 s t r i n g m1 = " M a r i a n a " ; s t r i n g m2 = m1 . s u b s t r ( 0 , 5 ) ; Operações de entrada e saída Pode-se realizar operações de entrada e saída de uma basic_string usando os operadores de inserção e extração << e >>. Também é possível usar o método getline das istream , sendo que neste caso não precisamos especifica o número máximo de caracteres que podem ser lidos, pois a basic_string irá expandir conforme o necessário. Como no caso de leitura de char∗, um caracter pode ser fornecido como parâmetro opcional para especificar o caracter de terminação da cadeia. 1 2 3 4 5 s t r i n g nome , s e x o ; c o u t << " Qual s e u s e x o ? " ; c i n >> s e x o ; c o u t << " Qual s e u nome ? " ; c i n . g e t l i n e ( nome ) ; / / Para p e r m i t i r e s p a c o s em b r a n c o 14.4.13 valarray Em programas de alto desempenho, que dependem de uma implementação bastante eficiente de operações em vetores, as operações sobre vector não são muito apropriadas, pois a generalidade desse container limita as possibilidades de implementação eficiente. Para auxiliar nesses casos, STL possui o vetor valarray , não são tão gerais nem trabalham tão bem com os outros containers e algoritmos, mas por outro lado permitem uma implementação mais eficiente. Uma das características que permite essa eficiência é a flexibilidade na indexação de elementos. Slices, máscaras e acesso indireto Slices são usados para especificar conjuntos regulares de índices que estarão envolvidos numa operação. Um slice é especificado por três valores: um índice inicial, um tamanho (número de índices no slice) e um stride (espaçamento entre índices). Por exemplo, um slice com índice inicial 2, tamanho 4 e stride 5 terá índices 2, 7, 12 e 17. Para acesso a slices temos um construtor que recebe os valores na ordem descrita e métodos que retornam esses três valores. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 144 1 2 3 4 5 6 7 Biblioteca padrão de templates slice slice slice slice slice slice slice s1 ( 0 , s2 ( 1 , s3 ( 2 , s4 ( 3 , s5 ( 0 , s5 ( 4 , s5 ( 8 , 3, 3, 3, 3, 4, 4, 4, 4); 4); 4); 4); 1); 1); 1); // // // // // // // 0, 1, 2, 3, 0, 4, 8, 4, 5, 6, 7, 1, 5, 9, 8 9 10 11 2, 3 6, 7 1 0 , 11 Como a biblioteca somente fornece vetores unidimensionais através de valarray , uma das utilidades de slices é justamente permitir lidar destes como se fossem vetores multidimensionais. Considere, por exemplo, a construção de uma matriz de duas dimensões. Essa matriz será armazenada em em elementos consecutivos na memória. Existem dois modos comuns de se fazer isso: ou colocando os elemento de uma mesma linha consecutivos na memória (o método usado em C e C++ para os arrays), ou colocando os elementos de uma mesma coluna consecutivos na memória (o método usado por Fortran). Em processamento de alto desempenho é desejado utilizar-se rotinas desenvolvidas em Fortran (como BLAS e Lapack) de um programa C++. Assim, devemos ser capazes de encarar um conjunto de elementos consecutivos como sendo uma matriz da mesma forma que Fortran. Suponha uma matriz de 4 linhas e 3 colunas, com elementos a0,0 , a0,1 , a0,2 , a0,3 , a1,0 , a1,1 , a1,2 , a1,3 , a2,0 , a2,1 , a2,2 e a2,3 . Se esses elementos forem colocados na memória exatamente nessa ordem, temos o layout de C++; se eles forem colocados na ordem a0,0 , a1,0 , a2,0 , a3,0 , a0,1 , a1,1 , a2,1 , a3,1 , a0,2 , a1,2 , a2,2 e a3,2 temos o layout de Fortran. Em qualquer dos casos, precisamos de 12 elementos para armazenar a matriz e portanto ela pode ser representada por um valarray de 12 elementos. Se queremos usar Lapack, que exige o layout de Fortran, então podemos acessar linhas e colunas da matriz pelos seguintes slices (compare também com o exemplo acima). 1 2 3 4 5 6 7 slice slice slice slice slice slice slice lin0 (0 , lin1 (1 , lin2 (2 , lin3 (3 , col0 (0 , col1 (4 , col2 (8 , 3, 3, 3, 3, 4, 4, 4, 4); 4); 4); 4); 1); 1); 1); // // // // // // // linha 0 linha 1 linha 2 linha 3 coluna 0 coluna 1 coluna 2 Em geral, se temos uma matriz de M linhas e N colunas em layout Fortran, teremos: 1 2 s l i c e f l i n i ( i , N, M) ; / / i −e s i m a l i n h a s l i c e f c o l j ( j ∗M, M, 1 ) ; / / j −e s i m a c o l u n a Mas se estivermos usando layout de C e C++ teremos: 1 2 s l i c e c l i n i ( i ∗N, N, 1 ) ; / / i −e s i m a l i n h a s l i c e c c o l j ( j , M, N ) ; / / j −e s i m a c o l u n a Em algumas situações desejamos acessar um conjunto mais complexo de índices, como uma faixa ou um bloco de elementos da matriz. Isso pode ser conseguido com o uso de um slice generalizado, ou gslice . Um gslice recebe um vetor ( valarray ) de tamanhos e um vetor de strides que indicam o número de linhas e colunas a colocar no bloco. Por exemplo, se desejamos, da matriz Fortran descrita acima, acessar os elementos das três primeiras linhas e duas primeiras colunas (total de 6 elementos), podemos gerar o gslice como abaixo: 1 2 3 4 5 s i z e _ t tam [ ] = { 2 , 3 } ; / / uma l i n h a tem 2 c o l u n a s , uma c o l u n a tem 3 l i n h a s s i z e _ t s t r [ ] = {4 , 1 } ; / / s t r i d e 4 nas l i n h a s , 1 nas c o l u n a s v a l a r r a y < s i z e _ t > t a m a n h o s ( tam , 2 ) ; / / o c o n s t r u t o r q u e r v a l a r r a y s valarray <size_t > s t r i d e s ( str , 2 ) ; g s l i c e ( 0 , tamanhos , s t r i d e s ) ; / / b l o c o que comeca no e l e m e n t o ( 0 , 0 ) Uma outra forma de acessar alguns dos elementos é através das chamadas máscaras. Estas consistem em um valarray <bool>; se um elemento desse array for true isso indica que o índice correspondente será usado na operação; se for false então o índice não é utilizado. Veremos exemplo mais adiante. Por fim, uma outra forma de indexação é o indireto. Construímos simplesmente um array com os índices que desejamos. Construtores Um valarray pode ser construído vazio, através do construtor assumido; com um número indicado de elementos inicializados com o construtor assumido do elemento; com um número indicado de elementos iguais com valor dado; com um número indicado de elementos copiados de um array de elementos, copiando um outro valarray pelo construtor de cópia ou gerando um novo valarray através dos métodos de indexação discutidos acima. U NIVERSIDADE DE S ÃO PAULO 14.4 Operações sobre containers 1 2 3 4 5 6 145 v a l a r r a y < double > v1 ; / / v a z i o v a l a r r a y < double > v2 ( 1 0 0 ) ; / / 100 e l e m e n t o s i g u a i s a 0 . 0 v a l a r r a y < double > v3 ( 1 . 0 , 1 0 0 ) ; / / 100 e l e m e n t o s i g u a i s a 1 . 0 double a [ ] = { 1 . , 2 . , 3 . , 4 . , 5 . , 6 . , 7 . , 8 . , 9 . , 1 0 . } ; v a l a r r a y < double > v4 ( a , 5 ) ; / / o s 5 p r i m e i r o e l e m e n t o s de a v a l a r r a y < double > v5 = v4 ; / / i g u a l a v4 Operações A indexação pode ser feita pelo operador [] , que não realiza verificação de validade do índice (por considerações de eficiência). O operador de indexação aceita tanto um índice simples como um slice , um gslice , um vetor de máscara ( valarray <bool>) ou um vetor valarray < size_t > para acesso indireto. O operador de atribuição por sua vez aceita tanto outro valarray como um valor escalar; neste último caso, todos os elementos do valarray receberam o valor escalar especificado. Quando indexamos um valarray por um dos índices múltiplos acima, o resultado é um tipos correspondente slice_array , gslice_array , mask_array ou indirect_array que é compatível com valarray e pode ser usado em atribuições. v a l a r r a y < i n t > m( 1 2 ) ; / / 12 e l e m e n t o s 0 s l i c e l i n 1 ( 1 , 3 , 4 ) ; / / l i n h a 1 no l a y o u t F o r t r a n 3 slice c o l 1 ( 4 , 4 , 1 ) ; / / c o l u n a 1 no l a y o u t F o r t r a n 4 m[ l i n 1 ] = 1 0 ; / / e l e m e n t o s da l i n h a 1 r e c e b e m 10 5 m[ c o l 1 ] = −10; / / e l e m e n t o s da c o l u n a 1 r e c e b e m −10 6 // Por e n q u a n t o v1 == 0 10 0 0 −10 −10 −10 −10 0 10 0 0 7 // que c o r r e s p o n d e a ( l a y o u t F o r t r a n ) : 8 // 0 −10 0 9 // 10 −10 10 10 / / 0 −10 0 11 / / 0 −10 0 12 b o o l b [ ] = { t r u e , f a l s e , f a l s e , f a l s e , f a l s e , true , 13 f a l s e , f a l s e , f a l s e , f a l s e , true , f a l s e } ; 14 v a l a r r a y < bool > m a s c a r a ( b , 1 2 ) ; 15 m[ m a s c a r a ] = 2 ; 16 / / Por e n q u a n t o v1 == 2 10 0 0 −10 2 −10 −10 0 10 2 0 17 / / que c o r r e s p o n d e a ( l a y o u t F o r t r a n ) : 18 / / 2 −10 0 19 / / 10 2 10 20 / / 0 −10 2 21 / / 0 −10 0 22 s i z e _ t ind [ ] = {3 , 6 , 9}; 23 v a l a r r a y < s i z e _ t > i n d i c e s ( i n d , 3); 24 m[ i n d i c e s ] = 3 ; 25 / / Por e n q u a n t o v1 == 2 10 0 3 −10 2 3 −10 0 3 2 0 26 / / que c o r r e s p o n d e a ( l a y o u t F o r t r a n ) : 27 / / 2 −10 0 28 / / 10 2 3 29 / / 0 3 2 30 / / 3 −10 0 1 2 São definidas diversas operações sobre valarray incluindo: todos os operadores de atribuição com um escalar à direita, que operam o escalar com todos os elementos do vetor; operadores unários de mudança de sinal, troca de bits ou negação lógica, também atuando sobre cada elemento do vetor; método sum que retorna a soma de todos os elementos; método shift que desloca logicamente todo os elementos um certo número de posições à esquerda (à direita se fornecermos um número de posições negativo); cshift que faz o mesmo mas com deslocamento circular (os elementos que saem por um lado são inseridos novamente pelo outro) e o método aply que gera um novo valarray que é o resultado da aplicação de uma função fornecida sobre todos os elementos. 1 2 3 4 5 6 7 i n t muda ( i n t a ) { r e t u r n −2∗a ; } i n t i n i c i a i s [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; valarray <int > v ( i n i c i a i s , 10); v ∗= 2 ; / / 2 4 6 8 10 12 14 16 18 20 v = −v ; / / −2 −4 −6 −8 −10 −12 −14 −16 −18 −20 i n t s = v . sum ( ) ; / / s == −110 v . s h i f t ( 2 ) ; / / −6 −8 −10 −12 −14 −16 −18 −20 0 0 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 146 8 9 Biblioteca padrão de templates v . c s h i f t ( − 3 ) ; / / −20 0 0 −6 −8 −10 −12 −14 −16 −18 v a l a r r a y < i n t > v2 = v . a p l y ( muda ) ; / / v2 == 40 0 0 12 16 20 24 28 32 36 Também os operadores aritméticos são sobrecarregados, fazendo que que eles executem suas operações elemento a elemento no vetor ou vetores fornecidos. Também incluídas estão as funções matemáticas como sen, cos, log, etc. 1 2 3 4 5 6 7 v a l a r r a y < double > a ( 1 . 0 , 1 0 ) ; v a l a r r a y < double > b ( 2 . 0 , 1 0 ) ; v a l a r r a y < double > c ; c = a + b ; / / c tem 10 v a l o r e s 3 . 0 v a l a r r a y < double > d ; d = b ∗ c ; / / d tem 10 v a l o r e s 6 . 0 ( não é p r o d u t o v e t o r i a l nem e s c a l a r ! ) c = c o s ( c ) ; / / c tem 10 v a l o r e s c o s ( 3 . 0 ) ; 14.5 Algoritmos Uma das vantagens dos containers fornecerem uma interface padronizada (dentro de certos limites) é que isso facilita o desenvolvimento de algoritmos que podem operar sobre qualquer container. A biblioteca padrão fornece diversos algoritmos, que passamos a descrever brevemente abaixo. Os algoritmos são definidos no header <algorithm>. 14.5.1 Predicados e operações A utilidade de muitos desses algoritmos é ampliada pelo fato deles receberem como parâmetro adicional uma função ou objeto funcional que descrever a forma de tomar decisões (predicados) ou de realizar comparações. Os predicados e comparações mais comuns são definidos no header < functional >. Além dos predicados, já descritos à pag. 135, são definidos templates de alguns objetos funcionais aritméticos, que são plus, minus, multiplies , divides , modulus e negate. modulus corresponde ao resto da divisão, negate é o operador de troca de sinal; os outros templates têm o significado esperado. Muitas vezes, a função que desejamos usar como predicado ou operação já existe como uma função simples ou como um método, ou então é uma pequena variação de uma função existente, por exemplo fixando um dos argumentos. Para facilitar o uso dessas funções, a biblioteca fornece, em < functional >, um conjunto de transformadores que podem ser utilizados. bind2nd Fixa o segundo argumento na chamada de uma função binária em um valor especificado. Por exemplo, para gerar um operador que compara um valor dado para verificar se ele é menor que 2 podemos usar bind2nd( less <int >(),2) . bind1st Fixa o primeiro argumento. bind1st ( less <int >(), 2) é equivalente a 2 < x. mem_fun Permite utilizar um método como predicado ou operador. Gera uma função que usa o primeiro argumento para fazer uma chamada de método. Por exemplo, se a classe C possui um método m sem parâmetros ou unário, então mem_fun(&C::m) pode ser usado como operação ou predicado para containers que mantém ponteiros e fará com que, ao chamar a operação, sejam realizadas chamadas do tipoc−>m() ou c−>m(x), de acordo com se o algoritmo quer uma operação unária ou binária. mem_fun_ref Similar a mem_fun, mas para quando o container guarda objetos, e não ponteiros. not1 Gera a negação de um predicado unário. not2 Gera a negação de um predicado binário. ptr_fun Necessário quando queremos transformar uma função normal por meio de um bind2nd, bind1st , not1 ou not2; faz com que o ponteiro para função passado seja adaptado ao tipo necessário por esses transformadores. U NIVERSIDADE DE S ÃO PAULO 14.5 Algoritmos 147 unary_function binary_function Para que predicados definidos pelo usuário funcionem adequadamente em conjunto com a biblioteca, é importante que esses predicados sejam declarados de forma a serem derivados das classes unary_function ou binary_function , de acordo com o adequado. Essas classes são templates que têm como parâmetros os tipos dos argumentos e o tipo do valor de retorno. Exemplos de uso serão dados abaixo. 14.5.2 Execução de operação De certa forma, o algoritmos mais simples possível é executar uma operação sobre todos os elementos do containers. Isso pode ser realizado pelo algoritmo for_each. Op for_each( Iter ini , Iter fin , Op f) Ini e Fin são os iteradores que determinam a faixa de elementos do container sobre os quais realizar a operação; f é uma função ou objeto funcional que determina a operação a executar (deve aceitar como parâmetro um elemento do tipo de elementos do container). 1 2 3 # include <iostream > # include <algorithm > # include < vector > 4 5 u s i n g namespace s t d ; 6 7 8 9 10 11 12 class Pega_pares { v e c t o r < i n t > &p a r e s ; public : P e g a _ p a r e s ( v e c t o r < i n t > &p ) : p a r e s ( p ) {} v o i d o p e r a t o r ( ) ( i n t i ) { i f ( i %2 == 0 ) p a r e s . p u s h _ b a c k ( i ) ; } }; 13 14 15 16 17 18 t e m p l a t e < c l a s s T> c l a s s Imprime { public : v o i d o p e r a t o r ( ) ( T v a l ) { c o u t << " " << v a l ; } }; 19 20 21 22 23 24 25 26 27 28 29 30 i n t main ( ) { vector <int > a ( 1 0 ) ; f o r ( v e c t o r < i n t > : : s i z e _ t y p e i = 0 ; i < a . s i z e ( ) ; i ++) a [ i ] = i + 1 ; vector <int > pares ; f o r _ e a c h ( a . b e g i n ( ) , a . end ( ) , P e g a _ p a r e s ( p a r e s ) ) ; c o u t << " Os p a r e s do v e t o r s ã o " << e n d l ; f o r _ e a c h ( p a r e s . b e g i n ( ) , p a r e s . end ( ) , Imprime < i n t > ( ) ) ; c o u t << e n d l ; return 0; } 14.5.3 Algoritmos de busca Existem diversos algoritmos que podem ser utilizados para busca de informações no container. Iter find ( Iter ini , Iter fin , const T& val) Busca um elemento entre ini e fin que tenha valor igual a val. O valor de retorno é um iterador para o elemento, se encontrado; se nenhum elemento é encontrado então o valor final fin é retornado. Iter find_if ( Iter ini , Iter fin , Pred p) Busca um elemento que satisfaça o predicado p. 1 2 i n t a [ ] = {0 , 2 , 4 , 6 , 8}; i n t b [ ] = {0 , 3 , 6 , 9}; 3 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 148 4 5 6 Biblioteca padrão de templates i n t ∗p = f i n d ( a , a +5 , 4 ) ; / / ∗p == 4 ; i n t ∗ s e r v e de i t e r a d o r p a r a a r r a y de i n t i n t ∗q = f i n d _ i f ( a , a +5 , b i n d 2 n d ( g r e a t e r < i n t > ( ) , 1 0 ) ) ; / / p r o c u r a > 1 0 ; q == a+5 i n t ∗ r = f i n d _ i f ( b , b +4 , b i n d 1 s t ( l e s s < i n t > ( ) , 7 ) ) ; / / r == b +3; meio c o n f u s o . . . Iter find_first_of ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 ) Iter find_first_of ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 , BinPred p) Retornam o primeiro elemento da seqüência definida por ini1 e fin1 que se encontra na segunda seqüência, definida por ini2 e fin2 . A primeira variante usa o operador == para determinar igualdade; a segunda variante permite especificar um predicado distinto para comparação. 1 2 s t r i n g nome = " B r j z i n s k i " ; s t r i n g v o g a i s = " AEIOUaeiou " ; 3 4 5 6 s t r i n g : : i t e r a t o r p r i m v o g = f i n d _ f i r s t _ o f ( nome . b e g i n ( ) , nome . end ( ) , v o g a i s . b e g i n ( ) , v o g a i s . end ( ) ) ; / / p r i m v o g a p o n t a p a r a a p r i m e i r a v o g a l de nome Iter adjacent_find ( Iter ini1 , Iter fin1 ) Iter adjacent_find ( Iter ini1 , Iter fin1 , BinPred p) Procuram um par de valores adjacentes iguais e retornam um iterador para o primeiro elemento desse par. A segunda variante permite especifica um predicado diferente de == para a comparação. 1 2 3 4 5 6 7 c l a s s T o l e r a n c i a : p u b l i c b i n a r y _ f u n c t i o n < double , double , bool > { double t o l ; public : T o l e r a n c i a ( d o u b l e t ) : t o l ( t ) {} b o o l o p e r a t o r ( ) ( d o u b l e x , d o u b l e y ) { r e t u r n f a b s ( x−y ) < t o l ; } }; /∗ . . . ∗/ 8 9 10 double v [ ] = { 1 . 8 , 2 . , 2 . 1 , 2 . 1 1 , 2 . 1 7 , 2 . 2 } ; d o u b l e ∗ d u p l a = a d j a c e n t _ f i n d ( v , v +6 , T o l e r a n c i a ( 0 . 0 5 ) ) ; / / acha o p a r 2 . 1 2 . 1 1 Note como o predicado Tolerancia é definido como derivado de uma binary_function que aceita dois argumentos double e retorna um bool. count( Iter ini1 , Iter fin1 , const T& val) count_if ( Iter ini1 , Iter fin1 , Pred p) Retornam o número de vezes que o valor val foi encontrado na seqüência ou o número de vezes que o predicado foi satisfeito. 1 2 3 i n t v [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; p t r _ d i f f _ t n3 = c o u n t ( v , v +10 , 3 ) ; / / n3 == 1 p t r _ d i f f _ t m3 = c o u n t _ i f ( v , v +10 , b i n d 2 n d ( g r e a t e r < i n t > , 3 ) ) ; / / m3 == 7 bool equal ( Iter ini1 , Iter fin1 , Iter2 ini2 ) bool equal ( Iter ini1 , Iter fin1 , Iter2 ini2 , BinPred p) Este algoritmo compara duas seqüências e retorna true se elas forem iguais. A segunda forma permite escolher o predicado de comparação de igualdade. mismatch( Iter ini1 , Iter fin1 , Iter2 ini2 ) mismatch( Iter ini1 , Iter fin1 , Iter2 ini2 , BinPred p) Este algoritmo compara duas seqüência e retorna um par ( pair ) com um iterador para a primeira seqüência e um iterador para a segunda que indicam os pontos onde as duas seqüências diferem pela primeira vez. A segunda versão permite escolher o predicado de comparação. 1 2 3 4 5 i n t v1 [ ] = { 1 , 2 , 3 , 4 , 5 } ; i n t v2 [ ] = { 1 , 2 , 4 , 8 , 1 6 } ; b o o l t u d o = e q u a l ( v1 , v1 +5 , v2 ) ; / / f a l s e b o o l p a r t e = e q u a l ( v1 , v1 +2 , v2 ) ; / / t r u e p a i r < i n t ∗ , i n t ∗> d i f = m i s m a t c h ( v1 , v1 +5 , v2 ) ; / / p a i r <&v1 [ 2 ] , &v2 [2] > U NIVERSIDADE DE S ÃO PAULO 14.5 Algoritmos 149 search ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 ) search ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 , BinPred p) Busca a seqüência definida por ini2 e fin2 como subseqüência da definida por ini1 e fin1 , retornando um iterador para o primeiro elemento da subseqüência dentro da primeira seqüência. 1 2 3 i n t v [ ] = {1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3}; i n t s [ ] = { 1 , 2} i n t ∗p = s e a r c h ( v , v +9 , s , s + 2 ) ; / / p == &v [ 1 ] find_end ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 ) find_end ( Iter ini1 , Iter fin1 , Iter2 ini2 , Iter2 fin2 , BinPred p) Similar a search, mas busca de trás para frente. 1 2 3 i n t v [ ] = {1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3}; i n t s [ ] = { 1 , 2} i n t ∗p = f i n d _ e n d ( v , v +9 , s , s + 2 ) ; / / p == &v [ 6 ] search_n( Iter ini1 , Iter fin1 , Size n, const T& val) find_end ( Iter ini1 , Iter fin1 , Size n, const T& val, BinPred p) Procura n cópias consecutivas do valor val na seqüência; retorna um iterador para o começo dos valores repetidos. 1 2 i n t v [ ] = {1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3}; i n t ∗p = s e a r c h _ n ( v , v +9 , 2 , 2 ) ; / / p == &v [ 2 ] 14.5.4 Modificação e geração de novas seqüências Alguns algoritmos permitem alterar uma seqüência ou gerar uma nova seqüência a partir de uma seqüência existente. Como os algoritmos não têm conhecimento sobre a constituição interna do container ao qual a seqüência pertence, eles não podem de fato alterar a estrutura, por exemplo, não podem apagar ou inserir elementos. Os algoritmos que alteram uma seqüência se contentam então com colocar os valores selecionados no começo da seqüência e deixar os valores não utilizados no final, retornam um iterador para o primeiro valor inútil. copy( Iter ini , Iter fin , Iter2 res ) copy_backward(Iter ini , Iter fin , Iter2 res ) Copiam uma seqüência numa segunda seqüência. copy_backward copia de trás para frente. Esses algoritmos supões que a seqüência destino possui elementos suficiente para copiar todos os elementos entre ini e fin . Quando queremos copiar num container novo ou adicionar ao final de um container existente, podemos usar back_inserter , que é um adaptador que faz com que os elementos sejam adicionados ao final. 1 2 3 4 5 6 7 vector <int > a ( 1 0 ) ; vector <int > b ( 2 0 , 1 ) ; vector <int > c ( 1 0 , 2 ) ; copy ( b . b e g i n ( ) , b . end ( ) copy ( c . b e g i n ( ) , c . end ( ) copy ( c . b e g i n ( ) , c . end ( ) copy ( b . b e g i n ( ) , b . end ( ) , , , , a . b e g i n ( ) ) ; / / ERRO : a nao tem 20 e l e m e n t o s a . b e g i n ( ) ) ; / / OK a . end ( ) ) ; / / ERRO : nao ha e l e m e n t o s d e p o i s de a . end ( ) b a c k _ i n s e r t e r ( a ) ) ; / / OK: i n s e r e no f i n a l transform ( Iter ini , Iter fin , Iter2 res , Op op) transform ( Iter ini , Iter fin , Iter2 ini2 , Iter3 res , BinOp op) Estes algoritmos geram uma nova seqüência que corresponde à seqüência dada depois de aplicado a operação op sobre cada elemento. Quando passamos duas seqüência, a operação binária op é aplicada sobre os pares correspondentes das duas seqüências e o resultado é escrito na seqüência gerada. 1 2 3 4 5 6 i n t d o b r a ( i n t x ) { r e t u r n 2∗ x ; } vector <int > a ( 1 0 , 1 ) ; vector <int > b ( 1 0 , 3 ) ; vector <int > c , d ( 1 0 ) ; t r a n s f o r m ( a . b e g i n ( ) , a . end ( ) , b a c k _ i n s e r t e r ( c ) , d o b r a ) ; / / c = 2∗ a t r a n s f o r m ( a . b e g i n ( ) , a . end ( ) , b . b e g i n ( ) , d . b e g i n ( ) , p l u s < i n t > ( ) ) ; / / d = a+b Iter unique( Iter ini , Iter fin ) Iter unique( Iter ini , Iter fin , BinPred p) I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 150 Biblioteca padrão de templates Iter2 unique_copy( Iter ini , Iter fin , Iter2 res ) Iter2 unique_copy( Iter ini , Iter fin , Iter2 res , BinPred p) Estes algoritmos eliminam valores idênticos sucessivos numa seqüência, deixando apenas uma cópia de cada valor. A variante unique_copy copia a seqüência limpa para uma outra seqüência, enquanto que a variante unique rearranja os valores, de forma a que os elementos iniciais da seqüência dada sejam únicos. Ambas as variantes retornam um iterador para a um elemento após a seqüência gerada. 1 2 3 4 5 6 i n t v1 [ ] = { 1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3 } ; i n t ∗ f v = u n i q u e ( v1 , v1 + 9 ) ; / / de v1 a p−1 t e m o s : 1 2 5 6 1 2 3 v e c t o r < i n t > v2 ; v e c t o r < i n t > : : i t e r a t o r f 2 = u n i q u e _ c o p y ( v1 , v1 +9 , b a c k _ i n s e r t e r ( v2 ) ) ; v e c t o r < i n t > v3 ( v1 , f v ) ; b o o l i g = e q u a l ( v2 , v3 ) ; / / t r u e replace ( Iter ini , Iter fin , const T& val, const T& newval) Este algoritmo procura na seqüência fornecida elementos com o valor val e se os encontra, substitui seus valores por newval. replace_copy( Iter ini , Iter fin , Iter2 res , const T& val, const T& newval) Similar a replace , mas copia os valores (alterados ou não) em outra seqüência. replace_if ( Iter ini , Iter fin , Pred p, const T& newval) replace_copy_if ( Iter ini , Iter fin , Iter2 res , Pred p, const T& newval) Similares aos anteriores, mas o novo valor é copiado se o valor anterior satisfaz o predicado p. 1 2 3 4 5 6 7 i n t v1 [ ] = { 1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3 } ; v e c t o r < i n t > v2 , v3 ; r e p l a c e _ c o p y ( v1 , v1 +9 , b a c k _ i n s e r t e r ( v2 ) , 2 , 4 ) ; / / v2 == 1 1 4 4 5 6 1 4 3 r e p l a c e _ c o p y _ i f ( v2 . b e g i n ( ) , v2 . end ( ) , b a c k _ i n s e r t e r ( v3 ) , −+b i n d 2 n d ( l e s s < i n t > ( ) , 3 ) , 3 ) ; / / v3 == 3 3 4 4 5 6 3 4 3 Iter remove( Iter ini , Iter fin , const T& val) Iter2 remove_copy(Iter ini , Iter fin , Iter2 res , const T& val) Iter remove_if( Iter ini , Iter fin , Pred p) Iter2 remove_copy_if( Iter ini , Iter fin , Iter2 res , Pred p) Os algoritmos remove são parecidos com os replace correpondentes, mas removem o elemento ao invés de mudar seu valor. Os algoritmos retornam um iterador para um elemento além do último elemento válido na seqüência gerada. 1 2 3 4 5 6 i n t v1 [ ] = { 1 , 1 , 2 , 2 , 5 , 6 , 1 , 2 , 3 } ; v e c t o r < i n t > v2 , v3 ( 9 ) ; remove_copy ( v1 , v1 +9 , b a c k _ i n s e r t e r ( v2 ) , 2 ) ; / / v2 == 1 1 5 6 1 3 vector <int > : : i t e r a t o r p = r e m o v e _ c o p y _ i f ( v1 , v1 +8 , v3 . b e g i n ( ) , b i n d 2 n d ( l e s s < i n t > , 3 ) ) ; / / e n t r e v3 . b e g i n ( ) e p t e m o s : 5 6 3 fill ( Iter ini , Iter fin , const T& val) fill_n ( Iter ini , Size n, const T& val) Os algoritmos fill servem para preencher uma seqüência com um valor especificado. A variante fill_n copia n valores val na seqüência iniciada por ini . generate ( Iter ini , Iter fin , Gen g) generate_n ( Iter ini , Size n, Gen g) Os generate são semelhantes aos fill , mas os valores a colocar são encontrados por chamadas repetidas do objeto funcional g. 1 2 3 4 5 class Pra_frente { i n t proximo ; public : P r a _ f r e n t e ( i n t i n i = 0 ) : p r o x i m o ( i n i ) {} o p e r a t o r ( ) ( ) { r e t u r n p r o x i m o ++; } U NIVERSIDADE DE S ÃO PAULO 14.5 Algoritmos 6 7 8 9 10 11 12 13 14 151 }; /∗ . . . ∗/ v e c t o r < i n t > v1 ( 1 0 ) , v2 ( 1 0 ) , v3 ( 1 0 ) , v4 ( 1 0 ) ; f i l l ( v1 . b e g i n ( ) , v1 . end ( ) , 1 2 ) ; f i l l _ n ( v2 . b e g i n ( ) , 1 0 , 1 2 ) ; / / v2 f i c o u i g u a l a v1 g e n e r a t e ( v3 . b e g i n ( ) , v3 . end ( ) , P r a _ f r e n t e ( ) ) ; / / v3 == 0 1 2 3 4 5 6 7 8 9 g e n e r a t e _ n ( v4 . b e g i n , 1 0 , P r a _ f r e n t e ( 1 0 ) ) ; / / v4 == 10 11 12 13 14 15 16 17 18 19 reverse ( Iter ini , Iter fin ) Iter2 reverse_copy ( Iter ini , Iter fin , Iter2 ini ) Estes algoritmos revertem a ordem de uma seqüência, sendo que reverse coloca os resultados na própria seqüência enquanto reverse_copy coloca o resultado em outra seqüência e retorna um iterador para um elemento após o último elemento inserido. rotate ( Iter ini , Iter meio, Iter fin ) Iter2 rotate_copy ( Iter ini , Iter meio, Iter fin , Iter2 ini ) Permitem realizar o deslocamento cíclico dos elementos na seqüência, de tal modo que o elemento apontado inicialmente por meio vá parar na posição inicialmente apontada por ini . A variante rotate_copy coloca o resultado numa cópia. 1 2 3 4 i n t seq [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; r e v e r s e ( seq , s e q + 1 0 ) ; / / 10 9 8 7 6 5 4 3 2 1 r o t a t e ( seq , s e q +4 , s e q + 1 0 ) ; / / 6 5 4 3 2 1 10 9 8 7 r o t a t e ( s e q +2 , s e q +5 , s e q + 8 ) ; / / 6 5 1 10 9 4 3 2 8 7 random_shuffle( Iter ini , Iter meio, Iter fin ) random_shuffle( Iter ini , Iter meio, Iter fin , Gen &g) Este algoritmo permite embaralhar aleatoriamente uma seqüência. A primeira variante garante que cada possivel embaralhamento tenha a mesma probabilidade. Na segunda variante podemos fornecer um gerador de número aleatórios, que deve ser um objeto funcional que recebendo um inteiro como parâmetro retorne um inteiro aleatório entre zero e o valor fornecido menos 1. iter_swap ( Iter i1 , Iter2 i2 ) Iter2 swap_ranges( Iter ini , Iter fin , Iter2 ini2 ) Estes algoritmos trocam as informações entre duas seqüências. iter_swap simplesmente troca os valores dos elementos apontados por dois iteradores. swap_ranges troca todos os valores de duas seqüências. 14.5.5 Algoritmos para seqüências ordenadas sort ( Iter ini , Iter fin ) sort ( Iter ini , Iter fin , Cmp cmp) stable_sort ( Iter ini , Iter fin ) stable_sort ( Iter ini , Iter fin , Cmp cmp) Estes algoritmos realizam a ordenação das seqüências apresentadas em ordem crescente; sort usa para comparação o operador < do elemento ou um objeto de comparação fornecido. stable_sort difere de sort pelo fato de garantir que objetos que comparam como iguais mantenham a mesma ordem original. 1 2 3 i n t v [ ] = {3 , 4 , 2 , 7 , 8 , 2 , 1 , 9 , 0}; sort (v , v +9); / / 0 1 2 2 3 4 7 8 9 s o r t ( v , v +9 , g r e a t e r < i n t > ( ) ) ; / / 9 8 7 4 3 2 2 1 0 partial_sort ( Iter ini , Iter meio, Iter fin ) sort ( Iter ini , Iter meio, Iter fin , Cmp cmp) partial_sort_copy ( Iter ini , Iter fin , Iter2 ini2 , Iter2 fin2 ) partial_sort_copy ( Iter ini , Iter fin , Iter2 ini2 , Iter2 fin2 , Cmp cmp) Em alguns casos, desejamos apenas os primeiros elementos de uma lista ordenada. Pode ser proveitoso então não ordenar a lista toda, mas apenas até o ponto em que conhecemos os elementos desejados. Isto é chamado ordenação I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 152 Biblioteca padrão de templates parcial e realizado pelos algoritmos acima. partial_sort ordena apenas até o ponto de os menores elementos estarem entre ini e meio; partial_sort_copy ordena tantos elementos entre ini e fin quantos cabem entre ini2 e fin2 . 1 2 3 4 i n t v [ ] = {3 , 4 , 2 , 7 , 8 , 2 , 1 , 9 , 0}; int maiores [ 3 ] ; p a r t i a l _ s o r t _ c o p y ( v , v +9 , m a i o r e s , m a i o r e s +3 , g r e a t e r < i n t > ( ) ) ; / / m a i o r e s == 9 8 7 nth_element( Iter ini , Iter nesimo, Iter fin ) nth_element( Iter ini , Iter nesimo, Iter fin , Cmp cmp) Estes algoritmos ordenam apenas até o ponto em que o n-ésimo elemento, indicado por nesimo, esteja no lugar certo, quer dizer, depois de nesimo não há nenhum elemento menor do que ele e antes não há nenhum elemento maior. binary_search ( Iter ini , Iter fin , const T& val) binary_search ( Iter ini , Iter fin , const T& val, Cmp cmp) Realizam a busca binária do valor val na seqüência, supondo que ela já está ordenada. Iter lower_bound( Iter ini , Iter fin , const T& val) Iter lower_bound( Iter ini , Iter fin , const T& val, Cmp cmp) Iter upper_bound( Iter ini , Iter fin , const T& val) Iter upper_bound( Iter ini , Iter fin , const T& val, Cmp cmp) pair < Iter , Iter > equal_range( Iter ini , Iter fin , const T& val) pair < Iter , Iter > equal_range( Iter ini , Iter fin , const T& val, Cmp cmp) Dada uma seqüência ordenada, possivelmente com várias cópias de cada valor, estes algoritmos retornam os iteradores para o começo (lower_bound) ou final (upper_bound) ou os começo e final (equal_range) de uma subseqüência com todos os valores iguais a val. 1 2 3 4 5 6 7 i n t v [ ] = {1 , 2 , 2 , 3 , 3 , 3 , 5 , 8 , 10}; b o o l t 3 = b i n a r y _ s e a r c h ( v , v +9 , 3 ) ; / / t r u e b o o l t 4 = b i n a r y _ s e a r c h ( v , v +9 , 4 ) ; / / f a l s e i n t ∗ p i = l o w e r _ b o u n d ( v , v +9 , 3 ) ; / / p i == &v [ 3 ] i n t ∗ p f = u p p e r _ b o u n d ( v , v +9 , 3 ) ; / / p f == &v [ 6 ] p a i r < i n t ∗ , i n t ∗> l i m 3 = e q u a l _ r a n g e s ( v , v +9 , 3 ) ; / / m a i s e f i c i e n t e / / l i m 3 . f i r s t == &v [ 3 ] , l i m 2 . s e c o n d == &v [ 6 ] Iter3 merge( Iter1 ini1 , Iter1 fin1 , Iter2 ini2 , Iter2 fin2 , Iter3 res ) Iter3 merge( Iter1 ini1 , Iter1 fin1 , Iter2 ini2 , Iter2 fin2 , Iter3 res , Cmp cmp) inplace_merge( Iter ini , Iter meio, Iter fin ) inplace_merge( Iter ini , Iter meio, Iter fin , Cmp cmp) Dadas duas listas ordenadas ini1 , fin1 e ini2 , fin2 , um merge produz em res uma lista ordenada com os valores das duas lidas originais. inplace_merge é usado quando temos dois pedações de uma lista, cada um ordenado e queremos que a lista inteira fique ordenada. Iter partition ( Iter ini , Iter fin , Pred p) Iter stable_partition ( Iter ini , Iter fin , Pred p) O algortimos partition faz com que todos os elementos da seqüência que satisfazem o predicado p sejam colocados antes dos que não o satisfazem. stable_partition é similar mas garante que a ordem original é preservada entre elemento de um mesmo grupo. 1 2 3 4 5 6 7 8 c l a s s P a r : p u b l i c u n a r y _ f u n c t i o n < i n t , bool > { public : o p e r a t o r ( ) ( i n t n ) { r e t u r n ( n%2 == 0 ) ; } }; /∗ . . . ∗/ i n t v [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; s t a b l e _ p a r t i t i o n ( v , v +10 , P a r ( ) ) ; / / v == 2 4 6 8 10 1 3 5 7 9 bool includes ( It1 i1 , It1 f1 , It2 i2 , It2 f2) bool includes ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , Cmp cmp) U NIVERSIDADE DE S ÃO PAULO 14.5 Algoritmos 153 It3 It3 It3 It3 It3 It3 It3 It3 set_union ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res ) set_union ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res , Cmp cmp) set_intersection ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res ) set_intersection ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res , Cmp cmp) set_difference ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res ) set_difference ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res , Cmp cmp) set_symmetric_difference ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res ) set_symmetric_difference ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , It3 res , Cmp cmp) Esta são operações de teoria dos conjuntos. Elas somente devem ser realizadas sobre seqüências ordenadas; seu comportamento quando alguma seqüência não está ordenada não é definido. include verifica se os elementos da seqüência i2, f2 são também elementos da seqüência i1,f1. Os outros algoritmos realizam as correspondentes operações de conjuntos colocando os resultados na seqüência res e retornando um iterador para um elemento além do último elemento inserido. 14.5.6 Operações em heaps make_heap(Iter ini , Iter fin ) make_heap(Iter ini , Iter fin , Cmp cmp) push_heap( Iter ini , Iter fin , Cmp cmp) pop_heap( Iter ini , Iter fin ) pop_heap( Iter ini , Iter fin , Cmp cmp) sort_heap ( Iter ini , Iter fin ) sort_heap ( Iter ini , Iter fin , Cmp cmp) Estas operações permitem lidar com qualquer container como se fosse uma heap. make_heap constrói uma heap a partir da seqüência especificada. Uma vez tendo uma heap pronto, podemos adicionar um elemento simplesmente inserindo um elemento no final e chamando push_heap. Para retirar o próximo elemento da heap fazemos pop_heap e então extraimos o último elemento. Uma chamada para sort_heap desfaz a heap e a substitui por uma seqüência ordenada. Muitas das situações para as quais se usa heaps podem ser resolvidas por meio de uma priority_queue . 14.5.7 Comparações Iter max_element(Iter ini , Iter fin ) Iter max_element(Iter ini , Iter fin , Cmp cmp) Iter min_element( Iter ini , Iter fin ) Iter min_element( Iter ini , Iter fin , Cmp cmp) Retornam um iterador para o elemento de maior e menor valor da seqüência, respectivamente. bool lexicographical_compare ( It1 i1 , It1 f1 , It2 i2 , It2 f2) bool lexicographical_compare ( It1 i1 , It1 f1 , It2 i2 , It2 f2 , Cmp cmp) Retorna true se a primeira seqüência é lexicograficamente anterior à segunda. 14.5.8 Permutações bool next_permutation ( Iter ini , Iter fin ) bool next_permutation ( Iter ini , Iter fin , Cmp cmp) bool prev_permutation ( Iter ini , Iter fin ) bool prev_permutation ( Iter ini , Iter fin , Cmp cmp) Estes algoritmos geram todas as permutações de uma seqüência. next_permutation gera a próxima permutação em ordem lexicográfica, enquanto prev_permutation gera a permutação anterior na mesma ordem. Ambos retornam false se não há nova permutação a gerar. O final da geração é determinado pelo fato de que a próxima permutação teria todos os elementos em ordem crescente. Assim, se queremos realmente percorrer todas as permutações devemos partir dos elementos nessa ordem. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 154 14.5.9 Biblioteca padrão de templates Algoritmos numéricos Alguns algoritmos numéricos genéricos são fornecidos através do cabeçalho <numeric>. Esses algoritmos executam as operações de acumulação, produto escalar, soma parcial e diferença entre adjacentes. T accumulate( Iter ini , Iter fin , T ini ) T accumulate( Iter ini , Iter fin , T ini , BinOp op) Este algoritmo retorna a acumulação dos elementos da seqüência de acordo com uma operação dada. O valor inicial é especificado por ini ; o tipo do valor de ini determina o valor de retorno. Se nenhuma operação é fornecida, o operador + é usado, resultando na somatória dos elementos. 1 2 3 i n t v [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; i n t soma = a c c u m u l a t e ( v , v +10 , 0 ) ; / / soma == 55 d o u b l e p r o d = a c c u m u l a t e ( v , v +10 , 1 . 0 , m u l t i p l i e s < i n t > ( ) ) ; / / p r o d == 1 0 . 0 T inner_product ( Iter ini , Iter fin , Iter2 ini2 , T inival ) T inner_product ( Iter ini , Iter fin , Iter2 ini2 , T inival , BinOp op, BinOp2 op2) Realiza o produto escalar das listas iniciadas por ini e ini2 . Quando nenhum operador é fornecido, realizada o produto escalar tradicional, usando multiplies <T> para o produto e plus<T> para a soma. Se operadores são fornecidos, então op é usado como operador de soma e op2 como operador de produto. O valor inicial é dado por inival . 1 2 3 4 5 i n t v1 [ ] = { 1 , 1 , 2 , 2 , 3 , 3 } ; i n t v2 [ ] = { 3 , 1 , 2 , 1 , 1 , 1 } ; i n t e s c = i n n e r _ p r o d u c t ( v1 , v1 +6 , v2 , 0 ) ; / / e s c == ( v1 . v2 ) == 14 i n t x = i n n e r _ p r o d u c t ( v1 , v1 +6 , v2 , 1 , m u l t i p l i e s < i n t > ( ) , p l u s < i n t > ( ) ) ; / / x == ( 1 + 3 ) ∗ ( 1 + 1 ) ∗ ( 2 + 2 ) ∗ ( 2 + 1 ) ∗ ( 3 + 1 ) ∗ ( 3 + 1 ) == 1536 Iter2 partial_sum ( Iter ini , Iter fin , Iter2 ini2 ) Iter2 partial_sum ( Iter ini , Iter fin , Iter2 ini2 , BinOp op) Na soma parcial, o i-ésimo elemento da saída (gerada a partir de ini2 é a soma de todos os elementos da seqüência da entrada até o i-ésimo. Por exemplo, dada uma seqüência a, b, c, d, . . ., a seqüência gerada será a, a + b, a + b + c, a + b + c + d, . . .. O operador opcional permite especificar uma outra operação ao invés do operador +. Iter2 adjacent_difference ( Iter ini , Iter fin , Iter2 ini2 ) Iter2 adjacent_difference ( Iter ini , Iter fin , Iter2 ini2 , BinOp op) O algoritmo adjacent_difference computa a diference entre um valor e o seu anterior na seqüência. Por exemplo, dada uma seqüência a, b, c, d, . . ., a seqüência gerada será a, b−a, c−b, d−c, . . .. O operador opcional permite especificar uma operação diferente do operador −. 1 2 3 4 5 i n t v [ ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}; int s [10] , d [10] , r [10]; p a r t i a l _ s u m ( v , v +10 , s ) ; / / s == 1 3 6 10 15 21 28 36 45 55 a d j a c e n t _ d i f f e r e n c e ( v , v +10 , d ) ; / / d == 1 1 1 1 1 1 1 1 1 1 p a r t i a l _ s u m ( d , d +10 , r ) ; / / r == 1 2 3 4 5 6 7 8 9 10 14.6 Ponteiros gerenciados automaticamente Quando lidamos com ponteiros e alocação dinâmica de memória, devemos nos lembrar sempre de apagar os objetos quando eles não são mais necessários, para evitar vazamento de memória, isto é, evitar que a memória fique ocupada com objetos inúteis. Isto já é normalmente difícil, mas pode ficar impossível no caso de ocorrer uma exceção, quando então o fluxo de execução é desviado para um ponto imprevisível do programa. Vimos, no capítulo 12, que para objetos automáticos existe chamada do destrutor quando uma exceção determina o término do escopo onde eles foram declarados. A biblioteca padrão permite que se tenha uma sintaxe similar para ponteiros com o uso do template auto_ptr . Um auto_ptr é inicializado com um ponteiro e, ao sair de escopo, seu contrutor garante a chamada do delete para o ponteiro. Para evitar que mútliplos delete s sejam realizados no mesmo ponteiro, ao copiarmos um auto_ptr em outro lugar seu ponteiro passa a ser nulo, quer dizer, podemos transferir o ponteiro de um auto_ptr para outro, mas não ficar com dois auto_ptr apontando para a mesma posição de memória. O template é definido em <memory>. 1 2 3 v o i d o u t r o ( v e c t o r < F i g u r a ∗> &f i g s ) { a u t o _ p t r < F i g u r a > n r ( new R e t a n g u l o ( 4 , 5 ) ) ; U NIVERSIDADE DE S ÃO PAULO 14.7 Números complexos nr −>d e s e n h a ( ) ; / / Sem p r o b l e m a s , mesmo que d e s e n h a j o g u e e x c e c a o / / s e d e s e n h a ( ) j o g a e x c e c a o , o novo r e t a n g u l o e ’ apagado f i g s . p u s h _ b a c k ( n r . r e l e a s e ( ) ) ; / / a q u i o c o n t r o l e e e n t r e g u e ao v e t o r f i g s 4 5 6 7 155 } 14.7 Números complexos A biblioteca define, em <complex> um template para números complexos, que permite a operação com números complexos formados por diversos tipo básicos. complex<T> define um complexo cujo tipo básico é T; complex é a mesma coisa que complex<double>. O template tem sobrecarga para todos os operadores artiméticos, as funções matemáticas comuns ( sqrt , pow, etc.) e as funções real (retorna a parte real), imag (retorna a parte imaginária) conj (retorna o conjugado), polar para construir um complexo dado por coordenadas polares (distância e ângulo, nessa ordem), abs (retorna o módulo) arg (retorna o ângulo) e norm (retorna o quadrado do módulo). 1 2 3 4 5 6 complex I ( 0 , 1 ) ; / / i com p a r t e r e a l 0 e i m a g i n a r i a 1 complex a = 5 ; // 5 complex b ( 1 , 2 ) ; / / 1 + 2∗ i complex c ; c = a ∗ i + b ; / / c == 1 + 7∗ i d o u b l e r = a b s ( c ) ; / / r == s q r t ( 5 0 ) 14.8 Exceções padrão Durante a execução do programa, diversas exceções podem ser lançadas, além das exceções definidas pelo usuário. A exceção bad_alloc, definida em <new> pode ser lançada pelo operador new. As exceções bad_cast e bad_typeid, definidas em <typeinfo> podem ser lançadas pelos operadores dynamic_cast e typeid respectivamente, quando existe problema entre o tipo do objeto e os tipos esperado nessas operações. A exceção bad_exception, definida em <exception> indica que uma exceção não permitida em um contexto foi lançada. O cabeçalho <stdexcept> define as seguintes exceções: out_of_range, lançada pelos métodos at e outros métodos que verificam validade de índices; invalid_argument , lançada pelo construtor de bitset ; overflow_error , lançada pelo método de bitset que o converte para um long sem sinal. A exceção ios_base :: failure pode ser lançada pelo método clear , quando não é possível limpar o estado do stream. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 156 Biblioteca padrão de templates U NIVERSIDADE DE S ÃO PAULO Capítulo 15 Pré-processador Uma característica de C++ herdada da linguagem C é o uso de um pré-processador. O pré-processador permite, através de diretivas inseridas no código fonte do programa, controlar o texto que será apresentado ao compilador. Consiste portanto de uma fase anterior à compilação, e permite apenas operações simples, que não dependam de análise estrutural do programa. Vemos a seguir as principais diretivas, juntamente com uma breve descrição e indicação de seus usos normais. Todas as diretivas começam com o caracter #, que deve ser o primeiro caracter da linha. #include Esta diretiva aceita o nome de um arquivo como parâmetro, e inclui o seu conteúdo no ponto do texto sob processamento onde a diretiva se encontra. O nome do arquivo a ser incluído pode ser fornecido delimitado por aspas ou por < e >. Quando delimitado por < e > o arquivo é buscado nos diretórios de inclusão do sistema; quando delimitado por aspas, o arquivo a ser incluído é primeiro procurado no mesmo diretório do arquivo que está sendo processado; se não for encontrado, então será buscado nos diretórios de inclusão do sistema. Os diretórios de sistema onde os arquivos serão buscados são determinados pelo sistema operacional e pelo compilador. 1 2 # include <iostream > # include " Pilha . h" #define Esta diretiva permite definir abreviações ou nomes para trechos de programa. Existe em duas versões, uma sem parâmetros e outra com parâmetros. Na versão sem parâmetros, simplesmente um nome é definido para uma seqüência de caracteres que será utilizada no programa: 1 2 3 4 5 6 7 8 9 # include <iostream > # d e f i n e UM 1 # d e f i n e PI 3.1416 # d e f i n e SAUDACAO " Alo , mundo " i n t main ( ) { s t d : : c o u t << SAUDACAO << " " << UM+ P I << s t d : : e n d l ; return 0; } Normalmente usam-se identificadores com todas as letras maiúsculas para elementos definidos por meio de #define. O mesmo programa acima poderia ter sido mais elegantemente escrito por meio do uso de constantes: 1 2 3 4 5 6 7 8 9 # include <iostream > c o n s t i n t um = 1 ; const double p i = 3 . 1 4 1 6 ; c o n s t char ∗ c o n s t s a u d a c a o = " Alo , mundo " ; i n t main ( ) { s t d : : c o u t << s a u d a c a o << " " << um+ p i << s t d : : e n d l ; return 0; } I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 158 Pré-processador Esta última definição é mais clara, mais segura (pois as constantes têm tipos que são verificados pelo compilador), e ajuda durante o processo de desenvolvimento e depuração do programa. É importante enfatizar que as definições não são limitadas a elementos simples como exemplificado acima. Veja por exemplo o estranho código abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # include <iostream > # d e f i n e IF i f ( # d e f i n e THEN ) { # d e f i n e ELSE } e l s e { # d e f i n e ENDIF } # d e f i n e HALT r e t u r n ( 0 ) i n t main ( ) { int i = 2 , j = 3; I F i %2 == 0 THEN s t d : : c o u t << " i e p a r " << s t d : : e n d l ; ENDIF I F i > j THEN s t d : : c o u t << " i m a i o r do que j " << s t d : : e n d l ; ELSE s t d : : c o u t << " j m a i o r ou i g u a l a i " << s t d : : e n d l ; ENDIF HALT ; } A definição é terminada pelo fim da linha. Se for necessário utilizar mais do que uma linha, deve-se utilizar o caracter de continuação de linha: um \ seguido de um caracter de fim de linha. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // / / CUIDADO! CÓDIGO HORROSO! // # include <iostream > # d e f i n e IMPRCABECALHO s t d : : c o u t << " C o n t r o l e de e s t o q u e " << e n d l \ << " S i l v a & S i l v a L t d a . " << e n d l \ << " V e r s a o 3 . 2 " << e n d l /∗ . . . ∗/ i n t main ( ) { IMPRCABECALHO ; /∗ . . . ∗/ return 0; } Na versão com parâmetros, também chamada de macro, a substituição é mais complexa, pois alguns elementos da substituição podem ser variados de acordo com certo número de parâmetros. Veja o exemplo abaixo: 1 2 3 4 5 6 7 # include <iostream > # d e f i n e max ( a , b ) ( ( a ) > ( b ) ? ( a ) : ( b ) ) i n t main ( ) { s t d : : c o u t << max ( 5 , 3 ) << s t d : : e n d l ; return 0; } Note que uma macro não é uma função. A substituição é puramente textual. No exemplo anterior, o compilador irá ver um código como: 1 2 3 4 5 6 /∗ . . . ∗/ i n t main ( ) { s t d : : c o u t << ( ( 5 ) > ( 3 ) ? ( 5 ) : ( 3 ) ) << s t d : : e n d l ; return 0; } Quando a macro é utilizada em situações mais complexas, como: 1 c = max ( s q r t ( a ) , b −5); resultará no código: U NIVERSIDADE DE S ÃO PAULO 159 1 c = ( ( s q r t ( a )) >( b −5):( s q r t ( a ) ) : ( b −5)); o que resultará na avaliação de sqrt (a) ou b−5 duas vezes. Macros desse tipo podem ser com vantagens substituídas por funções in-line: 1 2 3 4 5 t e m p l a t e < c l a s s T> i n l i n e T max ( T a , T b ) { return ( a > b ? a : b ) ; } com as vantagens de maior clareza e segurança de tipos, além de facilitar o processo de desenvolvimento e depuração. Também a avaliação dupla de parâmetros será evitada. #undef Esta diretiva aceita o nome de um identificador previamente definido e elimina a definição, isto é, retira o identificador da lista de definidos. É em geral útil quando queremos modificar a definição de um identificador anteriormente definido, visto que um simples #define não funciona neste caso, pois o compilador não aceita múltipla definição. 1 2 3 4 5 6 7 8 # i f d e f TRUE # u n d e f TRUE # endif # d e f i n e TRUE 1 # i f d e f FALSE # u n d e f FALSE # endif # d e f i n e FALSE 0 # if #else # elif #endif # ifdef #ifndef Estas diretivas permitem a compilação condicional. Isto quer dizer que um trecho de código será enviado para compilação apenas se certas condições (verificáveis em tempo de compilação) forem satisfeitas. A estrutura geral é: 1 2 3 4 5 # i f / ∗ . . . ∗ / / / Condicao a s e r t e s t a d a /∗ . . . ∗/ / / Trecho a s e r i n c l u i d o se condicao v e r d a d e i r a #else /∗ . . . ∗/ / / Trecho a s e r i n c l u i d o se condicao f a l s a # endif Também é possível omitir a parte #else, ou testar mais do que uma condição usando # elif : 1 2 3 4 5 6 7 8 9 #if /∗ . . . ∗/ / / prmeira condicao /∗ . . . ∗/ / / codigo se primeira condicao verdadeira # e l i f /∗ . . . ∗/ / / segunda condicao /∗ . . . ∗/ / / codigo se segunda condicao v e r d a d e i r a # e l i f /∗ . . . ∗/ / / t e r c e i r a condicao /∗ . . . ∗/ / / codigo se t e r c e i r a condicao verdadeira #else /∗ . . . ∗/ / / c o d i g o s e nenhuma c o n d i c a o v e r d a d e i r a # endif Uma condição muito utilizada é verificar se um certo identificador já foi definido ou não. Esta verificação pode ser feita com o uso de defined () , como em: 1 2 3 # i f ! d e f i n e d (NULL) # d e f i n e NULL 0 # endif Devido à grande utilidade deste tipo de código, existe abreviação através de # ifdef , que corresponde a # if defined () e #ifndef, que corresponde a # if ! defined () . O exemplo anterior é normalmente escrito: 1 2 3 # i f n d e f NULL # d e f i n e NULL 0 # endif I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 160 Pré-processador As expressões do condicional devem ter como resultado um valor de tipo integral, e devem envolver apenas valores que podem ser avaliados em tempo de compilação, excluindo expressões de transformação de tipo, expressões com sizeof ou constantes de enumeração. Um uso bastante comum de compilação condicional é para incluir mensagens de depuração no código. As mensagens de depuração fornecem informações sobre o andamento do programa, mas devem estar presentes apenas durante a depuração do programa, e não no código final. Podemos simplesmente incluir as mensagens de depuração em diretivas de compilação condicional, de forma que elas sejam incluídas apenas se um certo identificador for definido. Durante a depuração, definimos o identificador, e quando o código for ser entregue ao usuário, apenas retiramos a definição do identificador e todas as mensagens são automaticamente retiradas: 1 2 3 4 5 6 7 8 9 # d e f i n e DEBUG /∗ . . . ∗/ # i f d e f DEBUG s t d : : c o u t << " I n i c a l i z a n d o m a t r i z do g r a f o " << s t d : : e n d l ; # endif / ∗ . . . ∗ / / / Codigo de i n i c i a l i z a c a o da m a t r i z # i f d e f DEBUG s t d : : c o u t << " M a t r i z do g r a f o i n i c i a l i z a d a " << s t d : : e n d l ; # endif Note que o identificador DEBUG é definido sem nenhum valor. Isto ocorre porque iremos testar apenas se o identificar foi definido ou não, e não estamos interessados em nenhum tipo de valor. Esta técnica pode ser expandida para depuração em vários níveis: 1 2 3 4 5 6 7 8 9 # d e f i n e DEBUG 2 /∗ . . . ∗/ # i f DEBUG >= 1 s t d : : c o u t << " I n i c i a l i z a n d o m a t r i z do g r a f o " << s t d : : e n d l ; # endif # i f DEBUG >= 2 s t d : : c o u t << " V a l o r e s i n i c i a i s : " << s t d : : e n d l ; / ∗ . . . ∗ / / / Codigo de i m p r e s s a o d o s v a l o r e s i n i c i a s # endif Veja como neste caso a mensagem geral de inicialização é incluída sempre que o nível de depuração (especificado na definição de DEBUG) for maior ou igual a 1, enquanto que a mensagem mais detalhada incluindo valores sobre o que será inicializado é incluída apenas se o nível de depuração for maior ou igual a 2. # Este não é uma diretiva, mas um operador de pré-processamento. O operador # precedendo um parâmetro da macro irá resultar na expansão do argumento passado para a macro envolvido por aspas. Por exemplo, o código: 1 2 3 4 5 6 7 # include <iostream > # d e f i n e ALO( quem ) s t d : : c o u t << " Alo , " #quem << s t d : : e n d l i n t main ( ) { ALO( mundo ) ; return 0; } é equivalente a: 1 2 3 4 5 6 # include <iostream > i n t main ( ) { s t d : : c o u t << " Alo , " " mundo " << s t d : : e n d l ; return 0; } que por sua vez, devido à regra de que cadeias de caracteres literais separadas apenas por espaços em branco são concatenadas pelo pré-processador, é equivalente a: 1 2 3 4 # include <iostream > i n t main ( ) { s t d : : c o u t << " Alo , mundo " << s t d : : e n d l ; U NIVERSIDADE DE S ÃO PAULO 161 return 0; 5 6 } ## Este é outro operador de pré-processamento. Seu efeito é concatenar dois elementos em um único. Por exemplo, o seguinte código: 1 2 3 4 5 6 7 8 9 10 # include <iostream > # d e f i n e JUNTA ( a , b ) a ## b i n t main ( ) { const int a = 2 , b = 1; JUNTA ( i , f ) ( a > b ) { s t d : : c o u t << " a m a i o r que b " << s t d : : e n d l ; } return 0; } é equivalente a: 1 2 3 4 5 6 7 8 9 # include <iostream > i n t main ( ) { const int a = 2 , b = 1; if (a > b) { s t d : : c o u t << " a m a i o r que b " << s t d : : e n d l ; } return 0; } Além dessas diretivas, existem ainda outras, como #error e #pragma, cuja interpretação é dependente da implementação (isto é, do compilador), e #line que é mais útil para quem está desenvolvendo compiladores para outras linguagens que geram código C++. Estas diretivas não serão tratadas aqui. I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 162 Pré-processador U NIVERSIDADE DE S ÃO PAULO Capítulo 16 Tópicos Adicionais Neste capítulo trataremos de diversos tópicos da linguagem que ainda não foram tratados nos capítulos anteriores. 16.1 Argumentos de linha de comando Um programa em C++ pode receber do sistema operacional uma série de argumentos. Estes argumentos são passados ao programa por meio de parâmetros para a função main. O primeiro parâmetro, tradicionalmente denominado argc, é um contador de argumentos; o segundo parâmetro, tradicionalmente denominado argv, é um array de ponteiros para char com um dos argumentos por entrada. O primeiro argumento em argv, argv [0], é sempre o nome sob o qual o programa foi executado. 1 2 3 4 5 6 7 8 9 10 11 # include <iostream > i n t main ( i n t a r g c , char ∗ a r g v [ ] ) { s t d : : c o u t << " P r o g r a m a " << a r g v [ 0 ] << s t d : : e n d l ; i f ( argc > 1) { s t d : : c o u t << " Argumentos : " << s t d : : e n d l ; f o r ( i n t i = 1 ; i < a r g c ; i ++) s t d : : c o u t << a r g v [ i ] << s t d : : e n d l ; } return 0; } O modo de passar os parâmetros para o programa é dependente do sistema operacional, mas em geral se especificam os argumentos ao dar o nome do programa a ser executado. 16.2 Variáveis voláteis Em alguns casos especiais, uma variável em um programa pode ter seu valor alterado por elementos externos ao programa. Por exemplo, a variável pode estar associada a um dispositivo de leitura de dados, e uma leitura do valor dessa variável implica a leitura de um valor do dispositivo. Neste caso, devemos indicar este fato ao compilador, para evitar que ele efetue otimizações com o valor dessa variável. Por exemplo, o compilador poderia decidir que o valor da variável já se encontra em certo registrador, pois lá foi colocado por uma operação anterior, e então não efetuar nova leitura, o que faria com que o valor anterior da variável fosse utilizado, gerando um erro. Para impedir este tipo de problema, declaramos a variável como volátil: 1 volatile int relogio ; 16.3 Especificações de ligação Devido à sobrecarga de nomes de funções, os compiladores C++ se utilizam de um esquema de produção de nomes internos, utilizados no processo de ligação (ver capítulo 6), bastante elaborados, e que envolvem uma codificação dos parâmetros da função, além do seu nome. Isto pode gerar problemas ao se tentar utilizar rotinas compiladas por outras linguagens, pois o compilador tentaria alterar o nome dessas rotinas de acordo com seus parâmetros, resultando num nome diferente do utilizado na linguagem original. Para contornar este problema, é possível especificar a linguagem na I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 164 Tópicos Adicionais Tabela 16.1: Macros usadas com argumentos variáveis Identificador Descrição va_list Um tipo que guarda as informações necessárias para as outras macros abaixo. Macro que deve ser chamada antes que os argumentos variáveis da função possam ser acessados. Macro que, especificado um tipo, pega o próximo argumento variável, que deve ser do tipo especificado. Macro que deve ser chamada após ler todos os argumentos variáveis, e antes de retornar da função. va_start va_arg va_end qual uma certa função externa foi compilada, o que faz com que o compilador C++ então gere chamadas para uma função com o nome esperado pelas convenções dessa outra linguagem. No momento, pode-se especificar apenas C como uma linguagem externa: 1 e x t e r n "C" { / ∗ p r o t o t i p o s de f u n c o e s C ∗ / } Por exemplo, para especificar que chamadas para a função sqrt () devem utilizar o padrão de ligação próprio de C declaramos: 1 e x t e r n "C" { d o u b l e s q r t ( d o u b l e ) ; } Na verdade isto já é feito dentro do cabeçalho cmath, que deve ser incluído quando se pretende utilizar as funções matemáticas. De forma similar os cabeçalhos que definem os protótipos de outras funções padrão de C declaram-nas como de ligação C. 16.4 Lidando com exaustão de memória Quando o programa tenta executar um new para o qual não existe espaço disponível em memória, o comportamento padrão é abortar o programa, com uma mensagem de que a memória disponível se exauriu. Este comportamento padrão pode ser alterado pela alteração do ponteiro para função new_handler, o que pode ser feito por uma chamada a set_new_handler () , passando como argumento o nome de uma função que será chamada sempre que new não encontrar espaço suficiente. Esta função poderá então tentar liberar espaço do programa, e retornar. Se a função retorna, o new será novamente tentado, e caso falhe a função será novamente chamada. O programador tem que levar isso em conta para evitar que o programa fique preso nesse ponto, alternando chamadas de new e a função de gerenciamento. Caso seja feita a chamada set_new_handler(0), então um new que não dispõe de espaço retornará o ponteiro nulo. 16.5 Número variável de argumentos Em C++ como em C é possível definir uma função que aceita um número variável de argumentos. Estas funções devem sempre possuir ao menos um parâmetro fixo, a partir do qual será possível determinar, em tempo de execução, o número e tipo de argumentos usados em cada chamada específica. Um exemplo desse tipo de função é a função printf . O primeiro parâmetro é uma cadeia de caracteres, dentro da qual existem seqüências de caracteres que indicam o tipo e número de parâmetros adicionais. O protótipo da função printf é: 1 i n t p r i n t f ( char ∗ , . . . ) ; Os três pontos (elipses) no final indicam que o número de parâmetros é variável. Só o primeiro parâmetro deve ser um ponteiro para char. Para lidar com funções com um número variável de argumentos existe um conjunto de identificadores especiais, incluídos em stdarg . h, e mostrados na tabela 16.1. Veja o exemplo da função abaixo, que realiza a média de um certo número de valores dados como argumentos: 1 2 3 4 5 # include <iostream > # include <cstdarg > d o u b l e Media ( i n t n , i n t main ( ) { ...); U NIVERSIDADE DE S ÃO PAULO 16.6 Asserções 6 7 8 9 10 11 12 13 14 15 16 17 18 19 165 s t d : : c o u t << " P r i m e i r a : " << Media ( 3 , 1 . 0 , 2 . 3 , 4 . 5 ) << s t d : : e n d l ; s t d : : c o u t << " Segunda : " << Media ( 5 , 0 . 6 , 0 . 4 , 0 . 7 , 0 . 3 , 0 . 8 ) << s t d : : e n d l ; return 0; } d o u b l e Media ( i n t n , . . . ) { i n t i ; double s = 0 ; v a _ l i s t va ; v a _ s t a r t ( va , n ) ; f o r ( i n t i = 0 ; i < n ; i ++) s += v a _ a r g ( va , d o u b l e ) ; v a _ e n d ( va ) ; return ( s / n ) ; } Alguma observações: • As elipses (três pontos) devem sempre aparecer no final da lista de argumentos. • As passagem de valores correspondentes a elipses envolvem uma conversão de tipos no estilo da linguagem C tradicional: valores tipo char e short são convertidos para int, valores float são convertidos para double. • Uma variável do tipo va_list deve sempre ser declarada, e será utilizada pelas macros. • A macro va_start recebe a variável tipo va_list e o identificador do último parâmetro fixo antes das elipses. • A macro va_arg recebe a variável tipo va_list e o tipo da variável a ser lida, que neste momento deve ser conhecido. • A macro va_end recebe a variável do tipo va_list . • A variável va_list utilizada deve ser a mesma para todas as macros dentro de uma função. 16.6 Asserções Muitos autores gostam de enfatizar a necessidade de garantir que um certo trecho de programa (por exemplo uma função), somente seja executado se um certo conjunto de condições necessárias para seu bom funcionamento forem cumpridas. As condições podem então ser testadas por meio de uma asserção. Se a asserção se mostrar verdadeira, a execução continua, se ela se mostrar falsa, isso indica que um erro de programação ocorreu, e o programa deve ser abortado. Asserções são especialmente úteis no início de funções de biblioteca, feitas com o intuito de virem a ser utilizadas por diversos programadores. As asserções garantem neste caso que as funções não sejam chamadas sob condições para as quais não foram projetadas. Como exemplo de uma asserção, um trecho de código que realiza divisão por uma certa variável somente deve ser executado se o valor dessa variável for diferente de zero. Condições podem ser testadas em C++ utilizando a função assert , que recebe como argumento uma condição. Se a condição resultar em true a execução continua, senão a execução é interrompida com uma mensagem que indica o arquivo e a linha do código fonte onde o erro foi encontrado. Suponhamos que desenvolvemos uma função para cálculo do quociente e resto da divisão de dois número inteiros positivos, como abaixo: 1 2 3 4 5 6 7 8 v o i d ( i n t n , i n d d , i n t &q , i n t &r ) { r = n ; q = 0; w h i l e ( r >= d ) { r −= d ; q ++; } } O algoritmo utilizado nessa função somente é válido se n for maior ou igual a zero e d for maior do que zero. Estas condições podem ser testadas numa asserção, para garantir que a função não fique presa no ciclo (o que ocorre quando d é zero) e não gere resultados errôneos (o que ocorre quando n ou d são negativos): I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 166 1 2 3 4 5 6 7 8 9 Tópicos Adicionais v o i d ( i n t n , i n d d , i n t &q , i n t &r ) { a s s e r t ( n >= 0 && d > 0 ) ; r = n ; q = 0; w h i l e ( r >= d ) { r −= d ; q ++; } } Efeito similar poderia ter sido conseguido com um condicional, mas a função assert fornece informações de que um condicional não dispõe: o nome do arquivo fonte e a linha nesse arquivo da asserção que falhou. O protótipo da função assert é definido no arquivo de cabeçalho cassert . U NIVERSIDADE DE S ÃO PAULO Índice Remissivo abort, 115 accumulate, 154 acesso a membro, 61–62 ilegal, 11 indireto, 7, 35 operador, 36 adjacent_difference , 154 adjacent_find , 148 allocator_type , 129 alocador, 129, 130 amiga classe, veja classe, amiga função, veja função, amiga aninhamento condicional, 16 função, 24 any, 140 aply, 145 append, 142 argumento, 24, 27 assumido, 29 e sobrecarga, 31 argumentos número variável, 164–165 arquivo acesso, 123–124 aleatório, 123–124 cabeçalho, 13, 51–53, 57 modos, 123 objeto, 51 ponteiro de escrita, 123 de leitura, 123 array, 144 array, 11 dinâmico, 11, 26 alocação, 38 liberação, 39 e construtor, 64 e container, veja container, e array e ponteiro, 36 estático, 11 multidimensional, 25 dinâmico×estático, 39 operador, 6 parâmetro, 25–27, 39 array indireto, 144 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS asserção, 165–166 assert , 165 assign, 131, 141 associatividade, veja operador, associatividade at, 131 atribuição, 45 de objetos, 76–77 operador, 8, 38, 76 assumido, 76 auto-decremento operador, 6 auto-incremento e iteradores, 127 operador, 6 back, 131, 136 back_inserter , 149 bad, 121 bad_alloc, 155 bad_cast, 155 bad_type_id, 155 base, 119 base, 130 basic_string , 140 begin, 130 biblioteca, 23, 51 binary_function , 147 binary_search , 152 bind1st , 146 bind2nd, 146 bitset , 128, 139 bool, 3 booleano, 1 literal, 3 branco espaço, 118, 120 break, 19 em repetição, 19 em seleção, 19 cadeia de caracteres e array, 37 e ponteiro, 37 literal, 5 terminador, 37 campos de bits, 50 capacity , 133 caracter, 1 e sinal, 1 168 literal, 3 cast, 7, 67 catch, 114 cerr , 117 char, 1 cin, 12, 117 classe, 55–61 amiga, 71–72 base, 85 abstrata, 98–99 virtual, 94–95 derivada, 85–89 incialização, 63 clear , 121 cliente, 55, 56 clog, 117 coletor de lixo, 39 comentário, 12 comparação operadores, 8 compilação, 11, 51–53, 97 condicional, 159 complemento de dois, 1 composição, 70–71, 85 e herança, 93 condicional, 15–16 operador, 8 const, 11 constante, 11, 29 literal, veja literal parâmetro, veja parâmetro, constante const_iterator , 129 const_reference , 129 const_reverse_iterator , 129 construtor, 62–64, 71, 123 assumido, 63, 64 cópia, 30, 64, 77 de containers, 130 default, 63 e arrays, 63 e conversão, 66 e herança, 95 sem parâmetros, 64 container, 127 de seqüencia, 127 de seqüencia, 127 e array, 127 continuação de linha, 158 continue, 20 controle de execução, 15–20 conversão, 7, 10, 24, 55, 118, 165 automática, 9, 67 cast, veja cast como chamada de função, 7, 67 definida pelo usuário, 31, 66–69 explícita, 10 ÍNDICE REMISSIVO operador, 68 padrão, 9–10, 31 cópia, 30 copy, 141, 149 copy_backward, 149 count, 138, 140, 148 count_if , 148 cout, 12, 117 cshift , 145 c_str , 141 dados entrada de, 12, 117–124 estruturados, 45–47 inicialização, 47 recursivos, 46 formatados, 117, 120–121 não formatados, 117, 119 saída de, 12, 117–124 bufferizada, 117, 118, 122–123 tipo de, veja tipo data, 141 dec, 119 decimal, 3 declaração, 51 função, veja protótipo variável, veja variável, declaração #define, 157 definição, 2, 51 é executável, 3 função, veja função, definição múltipla, 94, 159 variável, veja variável, definição definição múltipla, 53 delete , 38, 39 deque, 128, 136 deslocamento à direita operador, 8 à esquerda operador, 8 destruidor, 62–64, 77, 123 e exceção, 115 e herança, 95 virtual, 99 desvio, 19–20 incondicional, veja goto difference_type , 129 divides , 146 divisão operador, 7 resto operador, 8 do, veja repetição, do double, 2, 5 dynamic_cast, 10 e binário, 8 U NIVERSIDADE DE S ÃO PAULO ÍNDICE REMISSIVO e lógico, 8 é-um, veja herança # elif , 159 elipses, 164 #else, 159 empty, 133 encapsulação, 59, 71 end, 130 endereço, 2, 36 operador, 7, 35 #endif, 159 endl, 118, 120 enumeração, 47, 160 eof, 118, 121 , 2 equal, 148 equal_range, 138 equal_to, 135 erase, 132 erro tratamento, 111 #error, 161 escopo, 24, 81–84 de arquivo, 81 de bloco, veja escopo, local de classe, 81 de função, 81 local, 81 namespace, 81–84 operador, 6, 57, 81–82, 94 binário, 82 unário, 81 estouro, 1, 5 exceção, 111–115 tipo, 114 tratador, 112, 114 tratamento, 111 execução, 98 extern, 53 extração operador, 8, 12, 117, 122 fail , 121 failbit , 119 false , 3 fill , 120, 150 fill_n , 150 find , 138, 142, 147 find_end, 149 find_first_not_of , 142 find_first_of , 142, 148 find_if , 147 find_last_not_of , 142 find_last_of , 142 first , 137 flags, 120 flags , 120 flip , 140 float , 2, 5 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 169 flush , 118, 120 for, veja repetição, for for_each, 147 front , 131, 135, 136 fstream, 123 função, 23–31 amiga, 71, 73 chamada de, 23 declaração, veja protótipo definição, 23–25 genérica, 104 in-line, 26, 159 membro, veja método parametrizada, 104 sobrecarga, veja sobrecarga, função virtual, 97 função operador, 6 funcional objeto, 135 gcount, 119 generalidade, 103 generate , 150 generate_n, 150 get, 118, 119 getline , 118, 119, 143 global, veja identificador, global good, 121 goto, 20 greater , 135 greater_equal , 135 guarda, 16 herança, 71, 85–95 e templates, 108 e união, 49 múltipla, 93–95, 117 privada, 90 protegida, 90 pública, 90 hex, 119 hexadecimal, 3 hierarquia, 85 identificador, 2, 24, 81 global, 24, 81 reservado, 2 # if , 159 # ifdef , 159 #ifndef, 159 ifstream , 123 ignore, 118 implementação, 55, 98 if , veja condicional #include, 157 includes , 152 indexação em maps, 137 170 operador, 131 índice, 11 indireto acesso, 7 e iteradores, 127 inline , veja função, in-line inner_product , 154 inplace_merge, 152 inserção operador, 8, 12, 117, 122 insert , 132, 142 instância, veja objeto int, 1 integral, veja tipo, integral promoção, 9 inteiro, 1 literal, 3 interface, 28, 55, 56 invalid_argument , 139, 155 flags , 121 ios, 117, 120, 121, 123, 124 ios :: badbit , 121 ios :: eofbit , 121 ios :: failbit , 121 ios :: fixed , 121 ios :: goodbit, 121 ios :: scientific , 121 iostream, 117, 123 istream , 117, 122, 123 istringstream , 124 iterador, 127, 130 acesso aleatório, 134 bidirecional, 134 e ponteiro, 127 iterator , 129 iter_swap, 151 key_compare, 129 key_type, 129 less , 135 less_equal , 135 lexicographical_compare , 153 ligação, 51 dinâmica, 98 outras linguagens, 163–164 tardia, 98 #line, 161 linha de comando, 163 list , 128, 134 literal, 3–5 booleano, 3 cadeia de caracteres, 5, 160 caracter, 3 inteiro, 3 ponto flutuante, 5 local, veja variável, local logical_and , 135 logical_not , 135 ÍNDICE REMISSIVO logical_or , 135 long, 1 long double, 5 lower_bound, 138, 152 lvalue, 28 macro, 158, 160 main, 12, 163 make_heap, 153 manipulador, 118–120 map, 128, 137 mapped_type, 129 máscara, 144 max_element, 153 membro, 45 acesso ajuste, 92 operador, 6, 45 por ponteiro, 46 estático, 72–73, 120 inicialização, 73 herdado, 86 inicialização, 64–66 lista, 65, 71 operador, 74 ponteiro para, veja ponteiro, membro para operador, 7 privado, 56, 57, 71, 90 protegido, 90 público, 56 membros template, 106 mem_fun, 146 mem_fun_ref, 146 memória alocação, 38–39, 63 dinâmica e atribuição, 77 exaustão, 164 posição de, 2 mensagem, 56 merge, 134, 152 método, 56, 69 constante, 69 herdado, 88 in-line, 66 virtual, veja polimorfismo puro, 98 min_element, 153 minus, 146 mismatch, 148 modulus, 146 multimap, 128, 138 multiplies , 146 multiset , 128, 139 namespace, veja escopo, namespace composição, 83 sinônimo, 83 U NIVERSIDADE DE S ÃO PAULO ÍNDICE REMISSIVO negação binária, 7 lógica, 7 negate, 146 new, 38, 111, 164 new_handler, 111, 164 next_permutation, 153 nome, veja identificador espaço de, veja escopo, namespace resolução, veja escopo none, 140 not1, 146 not2, 146 not_equal_to, 135 nth_element, 152 nulo comando, 15, 38 ponteiro, veja ponteiro, nulo objeto, 12, 56 constante, 69–70 oct, 119 octal, 3 ofstream, 123 operador, 5–9, 55 associatividade, 5, 6 binário, 5 precedência, 5, 117 unário, 5 operando, 5 operator, 68, 73 ostream, 117, 122, 123 ostringstream , 124 ou binário, 8 ou exclusivo, 8 ou lógico, 8 out_of_range, 131, 139, 155 overflow_error , 140 overflowerror overflow_error , 155 pair , 137 parênteses, 9 parâmetro, 23, 24, 29 array, veja array, parâmetro constante, 29–30 ponteiro, veja ponteiro, parâmetro por referência, 27 constante, 30 por valor, 27 partial_sort , 151 partial_sort_copy , 151 partial_sum , 154 partition , 152 peek, 119 plus, 146 polimorfismo, 97–99, 103 e ponteiros, 98 e referências, 98 ponteiro, 7, 35–42, 46 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 171 aritmética, 36–37 e array, 11 e constantes, 40–41 e herança, 93 e indexação, 36, 37 e iterador, veja iterador, e ponteiro e polimorfismo, 98 nulo, 35, 39 para função, 41–42 para membro, 99–100 parâmetro, 39–40 referência para, 40 ponto flutuante, 1 expoente, 5 literal, 5 parte fracionária, 5 parte inteira, 5 pop, 136 pop_back, 132 pop_front, 135 pop_heap, 153 portabilidade, 1, 2 pós-decremento, 7, 75 pós-incremento, 6, 37, 75 #pragma, 161 pré-decremento, 7, 75 pré-incremento, 6, 75 pré-processador, 13, 157–161 precedência, veja operador, precedência precisão, 119, 121 precision , 119, 121 preenchimento, 119 prev_permutation, 153 printf , 164 priority_queue , 128, 137 private, 61, 90 produto operador, 7 protected, 90 protótipo, 28–29, 51, 56 ptr_fun , 146 public, 56, 61, 86, 90 push, 136 push_back, 132 push_front, 135 push_heap, 153 put, 118 putback, 119 quase-container, 127 queue, 128, 136 random_shuffle, 151 rbegin, 130 rdstate , 121 read, 119 reaproveitamento, 86, 92 receptáculo, 127 recursão, 23, 26, 31 172 múltipla, 26 simples, 26 reference , 129 referência, 11, 122 e polimorfismo, 98 parâmetro, veja parâmetro, por referência e ponteiros, 40 reinterpret_cast , 10 remove, 135, 150 remove_copy, 150 remove_copy_if, 150 remove_if, 150 rend, 130 repetição, 18–19 for, 19 do, 18 for, 18 e while, 19 while, 18 replace , 143, 150 replace_copy, 150 replace_copy_if , 150 replace_if , 150 reservado identificador, 2 reserve , 133 reset , 140 resetiosflags , 121 retorno de main, 13 e sobrecarga, 31 return, 24 reverse , 151 reverse_copy, 151 reverse_iterator , 129 rfind , 142 rotate , 151 rotate_copy , 151 rotina, veja função rótulo, 20, 81 search, 149 second, 137 seekg, 124 seekp, 124 seleção, 17–18 set , 128, 138, 140 setbase , 119 set_difference , 153 setf , 120 setfill , 119 set_intersection , 153 setiosflags , 121 set_new_handler, 164 setprecision , 119, 121 set_symmetric_difference , 153 set_terminate , 115 set_unexpected, 115 set_union, 153 ÍNDICE REMISSIVO setw, 119 shift , 145 short, 1 sinal e caracter, 1 inversão, 7 size , 133, 140 sizeof , 7 size_type , 129 slice, 143 sobrecarga, 76 e templates, 104 função, 30–31, 63 operador, 73–76, 117 soma operador, 8 sort , 151 em lista, 134 sort_heap, 153 splice , 134 stable_partition , 152 stable_sort , 151 stack, 128, 136 static , 31, 73 static_cast , 10 STL, 127 str , 125 stream, 117–119 de arquivo, 123 e cadeias de caracteres, 124 entrada, 118–119 erros, 121 saída, 118 stride, 143 string , 128, 140 struct , 45 subclasse, 85 substr , 143 subtração operador, 8 sum, 145 superclasse, 85 swap, 131 swap_ranges, 151 switch, veja seleção tamanho, 7 tellg , 124 tellp , 124 tem-um, veja composição template, 103–109 compilação separada, 106 de classe, 104–106 de função, 103–104 e herança, 108 friend, 106 membros, 106 terminate , 115 test , 140 U NIVERSIDADE DE S ÃO PAULO ÍNDICE REMISSIVO this , 72, 77 throw, 114 sem operando, 115 tie , 122 tipo, 1, 2, 5, 24, 55 abstrato, 55, 85 básico, veja tipo, pré-definido, 69 definição, 50 definido pelo usuário, 1, 104 integral, 1, 9, 17, 47, 50, 160 parametrizado, 104 pré-definido, 1 top, 136 top-down, 23 to_string , 140 to_ulong, 140 transform, 149 true, 3 try, 114 typedef, 50, 138 unary_function, 147 #undef, 159 unexpected, 115 união, 47–49 anônima, 49 inicialização, 49 Unicode, 5 union, 47 unique, 135, 149 unique_copy, 150 unsetf , 120 unsigned, 1 upper_bound, 138, 152 using, 82 namespace, 83 va_arg, 164, 165 va_end, 164, 165 valarray , 128, 143 va_list , 164, 165 valor, 2 value_type, 129 variável, 2 automática, 31, 115 inicialização, 31 declaração, 2 definição, 2, 10 estática, 31 inicialização, 31 externa, 53 global, 31, 53, 111 inicialização, 31 inicialização, 10 local, 31 temporária, 28 volátil, 163 va_start , 164, 165 vector , 128, 130 I NSTITUTO DE F ÍSICA DE S ÃO C ARLOS 173 vírgula, 8 void, 1, 24, 35 e acesso, 36 volatile , 163 wchar_t, 5, 140 while, veja repetição, while width, 119 write, 119 ws, 120