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
Download

Introdução à Orientação a Objetos em C++ Gonzalo Travieso