Você está na página 1de 107

Linguagens de Programação C e C++: Uma

Introdução

Luı́s Fernando de Oliveira

26 de maio de 2011
2
Sumário

1 Programação em C e C++ 7
1.1 Aspectos Básicos do Código-Fonte em C . . . . . . . . . . . . 7
1.2 Compiladores C e C++ . . . . . . . . . . . . . . . . . . . . . 9
1.3 Compilação dos Códigos-fonte . . . . . . . . . . . . . . . . . . 10

2 Tipos de Dados 13
2.1 Representação de Dados na Memória . . . . . . . . . . . . . . 14
2.1.1 Representação de Números Inteiros . . . . . . . . . . . 15
2.1.2 Representação de Números Reais . . . . . . . . . . . . 16
2.1.3 Tipos de Dados Representados . . . . . . . . . . . . . . 17
2.1.4 Organização dos Dados na Memória . . . . . . . . . . . 18
2.2 Tipos Definidos de Dado . . . . . . . . . . . . . . . . . . . . . 19
2.2.1 Tipo Literal: char . . . . . . . . . . . . . . . . . . . . 19
2.2.2 Tipo Inteiro: char e int . . . . . . . . . . . . . . . . . 21
2.2.3 Tipo Real: float e double . . . . . . . . . . . . . . . . . 22
2.2.4 Tipo Indefinido: void . . . . . . . . . . . . . . . . . . . 23
2.2.5 Tipo Lógico . . . . . . . . . . . . . . . . . . . . . . . . 23
2.2.6 Modificadores de Tipo signed e unsigned . . . . . . . . 24
2.2.7 Modificadores de Tipo short e long . . . . . . . . . . . 25
2.2.8 Type Casting . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.9 Resumo dos Tipos intrı́nsecos . . . . . . . . . . . . . . 27
2.3 Vetores, Matrizes e Strings . . . . . . . . . . . . . . . . . . . . 27
2.3.1 Declaração de Vetores e Matrizes . . . . . . . . . . . . 27
2.3.2 Cadeia de Caracteres (Strings) . . . . . . . . . . . . . 28
2.4 Tipos Abstratos de Dado . . . . . . . . . . . . . . . . . . . . . 29
2.4.1 Estruturas de Dados: struct . . . . . . . . . . . . . . . 29
2.4.2 Enumerações: enum . . . . . . . . . . . . . . . . . . . 32
2.4.3 Uniões: union . . . . . . . . . . . . . . . . . . . . . . . 33

3
4

2.4.4 Campo de Bits . . . . . . . . . . . . . . . . . . . . . . 34


2.4.5 Declaração typedef . . . . . . . . . . . . . . . . . . . . 36
2.5 Ponteiros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.5.1 Ponteiros e Endereço de Memória . . . . . . . . . . . . 36
2.5.2 Declaração de Ponteiros . . . . . . . . . . . . . . . . . 38
2.5.3 Operador de Endereçamento de Dado(&) . . . . . . . . 39
2.5.4 Operador de Referenciamento de Dado (*) . . . . . . . 39
2.5.5 Operador de Referenciamento de Campo de Estrutura
(->) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.5.6 Aritmética de Ponteiros . . . . . . . . . . . . . . . . . 41
2.5.7 Um Cuidado Mais Especial . . . . . . . . . . . . . . . 41
2.5.8 Relação entre Ponteiros, Vetores e Matrizes . . . . . . 42
2.5.9 Alocação de Memória para Ponteiros . . . . . . . . . . 45

3 Operadores Matemáticos, Lógicos e Binários 47


3.1 Conversão de Tipo e Operador de Atribuição . . . . . . . . . . 47
3.2 Operadores Aritméticos . . . . . . . . . . . . . . . . . . . . . 50
3.3 Operadores Relacionais . . . . . . . . . . . . . . . . . . . . . . 52
3.3.1 Operadores de Igualdade (==) e de Diferença (!=) . . 52
3.3.2 Operadores Maior (>) e Maior que (>=) . . . . . . . . 52
3.3.3 Operadores Menor (<) e Menor que (<=) . . . . . . . 52
3.4 Operadores Lógicos . . . . . . . . . . . . . . . . . . . . . . . . 52
3.4.1 Operador de Conjunção: E Lógico (&&) . . . . . . . . 52
3.4.2 Operador de Disjunção: OU Lógico (||) . . . . . . . . . 52
3.4.3 Operador de Negação (!) . . . . . . . . . . . . . . . . . 52
3.5 Operadores Binários . . . . . . . . . . . . . . . . . . . . . . . 52
3.5.1 Operador E Binário (&) . . . . . . . . . . . . . . . . . 52
3.5.2 Operador OU Binário (|) . . . . . . . . . . . . . . . . . 53
3.5.3 Operador OU EXCLUSIVO Binário (ˆ) . . . . . . . . . 53
3.5.4 Operador de Negação Binário (∼) . . . . . . . . . . . . 53
3.5.5 Operadores de Deslocamento Binário (<< e >>) . . . 53
3.6 Operadores de Atribuição Concatenados . . . . . . . . . . . . 53
3.6.1 Operador de Adição (+=) . . . . . . . . . . . . . . . . 53
3.6.2 Operador de Subtração (-=) . . . . . . . . . . . . . . . 53
3.6.3 Operador de Multiplicação (*=) . . . . . . . . . . . . . 53
3.6.4 Operador de Divisão (/=) . . . . . . . . . . . . . . . . 53
3.6.5 Operador Lógico E Binário (&=) . . . . . . . . . . . . 53
3.6.6 Operador Lógico OU Binário (|=) . . . . . . . . . . . . 53
3.6.7 Operador Lógico OU EXCLUSIVO Binário (ˆ=) . . . . 54
3.6.8 Operadores de Deslocamento Binário (<<= e >>=) . 54
5

4 Estruturas de Controle de Execução 55


4.1 Estruturas de Condição . . . . . . . . . . . . . . . . . . . . . . 55
4.1.1 Estrutura do Se . . . . . . . . . . . . . . . . . . . . . . 55
4.1.2 Estrutura do Se Ternário . . . . . . . . . . . . . . . . . 57
4.1.3 Estrutura de Seleção de Caso . . . . . . . . . . . . . . 58
4.2 Estruturas de Repetição . . . . . . . . . . . . . . . . . . . . . 58
4.2.1 Estrutura de Laço Definido . . . . . . . . . . . . . . . 58
4.2.2 Estrutura de Laço Condicional . . . . . . . . . . . . . . 60

5 Funções 63
5.1 Argumentos por Valor e por Referência . . . . . . . . . . . . . 64
5.2 Protótipo de uma Função . . . . . . . . . . . . . . . . . . . . 66
5.3 Relação das Funções Intrı́nsecas . . . . . . . . . . . . . . . . . 67
5.4 Funções de Entrada e Saı́da Padrões . . . . . . . . . . . . . . 74

6 Diretivas de Compilação 75
6.1 Diretiva #include . . . . . . . . . . . . . . . . . . . . . . . . . 75
6.2 Diretivas #define|#undef . . . . . . . . . . . . . . . . . . . . 77
6.3 Diretivas #if –#elif –#else–#endif . . . . . . . . . . . . . . . 79
6.4 Diretivas #ifdef |#ifndef –#endif . . . . . . . . . . . . . . . . 80

7 Organização da Programação em C 85

8 Classes e Objetos 87
8.1 Encapsulamento de Atributos e Métodos . . . . . . . . . . . . 88
8.2 Visibilidade de Implementação . . . . . . . . . . . . . . . . . . 89
8.3 Hierarquia de Classes e Hereditariedade . . . . . . . . . . . . 91
8.4 Polimorfismo de Métodos . . . . . . . . . . . . . . . . . . . . . 93
8.5 Objetos das Classes . . . . . . . . . . . . . . . . . . . . . . . . 94
8.6 Métodos Construtores e Destruidores . . . . . . . . . . . . . . 95
8.7 Sobrecarga de Operadores . . . . . . . . . . . . . . . . . . . . 98
8.8 Virtualização de Métodos . . . . . . . . . . . . . . . . . . . . 104
6
Programação em C e C++
1
1.1 Aspectos Básicos do Código-Fonte em C
Para começar, vejamos um exemplo de código-fonte simples em C para,
depois, apresentarmos cada elemento componente dele.

Código 1.1: exemplo1.c


# include < stdlib .h >
# include < stdio .h >
# include < string .h >
# include < math .h >

int main ( void ) {


int a ;
float b , c ;

printf ( " digite um valor inteiro : \ n " );

scanf ( " % d " ,& a );

b = 0.5;
c = a+b;

printf ( " a soma deste valor com 0.5 eh : % f \ n " ,c );

return 0;
}

Um código-fonte em C é composto por instruções de compilação e instruções


de programação.
As instruções de compilação (ou diretivas de compilação) iniciam
com o sı́mbolo “#” e são direcionadas ao compilador. Elas não geram código

7
8 CAPÍTULO 1. PROGRAMAÇÃO EM C E C++

executável e somente têm efeito durante o processo de compilação. Indicam


ações que o compilador deve executar ou modificam um comportamento es-
pecı́fico do compilador.
As instruções de programação (tudo que não começa com “#”) podem
ser classificadas como comandos de declaração e comandos de execução.
Os comandos de declaração são usados para definir variáveis, tipos de variáveis,
estruturas de dados e funções. Os comandos de execução são as instruções que
serão efetivamente executadas pelo processador. Durante o processo de com-
pilação, estes comandos são traduzidos em uma linguagem intermediária, cha-
mada de linguagem objeto (que não tem nada a ver com orientação a objeto)
para depois, durante o processo de link -edição, ser convertida em linguagem
de máquina.
Neste exemplo inicial, você pode ver todos estes tipos de instrução. As
quatro primeiras linhas de código (quatro diretivas #include) informam ao
compilador que ele deve incluir, no processo de compilação, quatro arquivos
com extensão “.h” (chamados de arquivos de cabeçalho, do inglês header files).
Estes arquivos contêm os protótipos de funções intrı́nsecas da linguagem C e
são usadas basicamente, durante o processo de compilação, para verificar se
as funções intrı́nsecas usadas no programa estão declaradas corretamente (sin-
taxe da linguagem). Os arquivos de cabeçalho não contém o código-fonte das
funções, mas apenas as declarações (protótipos). As funções em si estão dis-
ponı́veis na forma de bibliotecas pré-compiladas que acompanham a instalação
do compilador. Os arquivos de cabeçalho, por assim dizer, funcionam como
uma lista condensada com o nome das funções e seus respectivos argumentos.

• O arquivo stdlib.h contém os protótipos das funções intrı́nsecas básicas.


• O arquivo stdio.h contém os protótipos das funções básicas de entrada
e saı́da de dados.
• O arquivo string.h contém os protótipos das funções básicas de mani-
pulação de strings (cadeia de caracteres).

• O arquivo math.h contém os protótipos das funções básicas de ma-


temática.

Depois das diretivas de compilação, encontramos as instruções de pro-


gramação. A primeira instrução é uma declaração de função: a função int
main(void). Esta é a função principal das linguagens C e C++. Ela desem-
penha um papel similar ao comando program do Fortran e do Pascal, isto é,
define o ponto de entrada do programa executável. Pode faltar quase tudo
num código-fonte em C e C++, mas não pode faltar a função main().
Dentro da função main(), encontramos as declarações de variáveis e os
comandos de execução. As variáveis são declaradas sempre no inı́cio da co-
dificação de um bloco de instruções (delimitado pelos sı́mbolos “{” e “}”).
1.2. COMPILADORES C E C++ 9

Por que? Porque, sendo a linguagem C uma linguagem de programação es-


truturada, as variáveis devem ser declaradas antes de serem usadas. Durante
o processo de compilação, as declarações de variáveis são usadas para montar
uma tabela de identificadores que deverão armazenar dados. Se uma variável
é declarada duas vezes, o compilador tem como detectar o erro, pois o pro-
cesso de compilação tentará criar dois identificadores com os mesmos nomes.
Se isso fosse feito depois de processar os comandos de execução, o compilador
perderia o controle na identificação de que variável está sendo referenciada
numa instrução especı́fica. Então, declaração de variável vem sempre antes
dos comandos de execução, iniciando os blocos de instruções.
Depois das declarações de variáveis, vemos comandos de atribuição, opera-
ção aritmética, chamadas de funções e de retorno de dado.
Uma informação muito importante que deve ser levantada aqui, no inı́cio
do material sobre as linguagens C e C++, é que, em C e C++, não existe a
declaração formal de sub-rotina ou procedimento. Tudo em C e C++ é função.
O que irá definir se a função se comportará como uma função tradicional
(que retorna um dado) ou como uma sub-rotina (que não possui retorno de
informação através de operação de atribuição) é o tipo da função. Como será
visto no capı́tulo 2 sobre tipos de dados, existe um tipo chamado de void
que indica a ausência de tipo definido. Veja bem, não é “tipo indefinido”, é
“ausência de tipo definido”. Então, uma função declarada como sendo do tipo
void não retorna dado, ou seja, comporta-se como uma sub-rotina. Outro uso
do tipo void está na declaração de funções que não precisam de argumentos
(no nosso exemplo, a função main() é declarada com a palavra reservada void
no lugar dos argumento. Isto significa que a função main não recebe qualquer
informação de fora do programa. Poderia ser diferente? Sim, poderia. Se o
programador precisar passar dados para dentro da função main, ele declara
a lista de argumentos da forma tradicional: tipo do argumento e nome do
argumento.
Agora que você já teve um primeiro contato com a linguagem, vamos falar
um pouco do compilador e da compilação.

1.2 Compiladores C e C++


Existem diversos compiladores C e C++. Pensando em compiladores de
acesso livre, os mais usados são os compilares GNU gcc para a linguagem C e
GNU g++ para a linguagem C++. Ambos os compiladores possuem versões
para Windows, MacOS e Linux. No ambiente Linux, estes compiladores são
padrão e já vem com a instalação da distribuição do sistema operacional. Caso
não estejam instalados, é só acessar o repositório de programas da distribuição
e instalá-los. Para Windows, existem as distribuições MinGW e CygWin,
ambos gratuitos.
10 CAPÍTULO 1. PROGRAMAÇÃO EM C E C++

Estes compiladores não possuem interfaces de desenvolvimento (as famosas


IDE’s, do inglês Integrated Development Environment), mas aceitam que sejam
instaladas a parte. As IDE’s para Windows mais famosas são o VisualC da
Microsoft e o C++Buider da CodeGear, ambos não gratuitos. No campo das
IDE’s de código aberto, tanto para Windows como para Linux, existem alguns
programas bons. Destacaria o Code::Blocks que tem versões para Windows e
Linux. Outras duas IDE’s são: Eclipse e NetBeans, que originalmente foram
concebidas para a linguagem Java, mas possuem pacotes para as linguagens C
e C++.
Para todos os efeitos, este material não tem a intenção de forçar nenhuma
das IDE’s mencionadas, apenas determinar que os exemplos apresentados serão
todos testados nos compiladores gcc e g++. Fica ao cargo de cada um decidir
se instala ou não uma IDE.

1.3 Compilação dos Códigos-fonte


As linhas de comando dos compiladores são exatamente iguais às usadas
no Fortran (com o f95 ou g95 ou gfortran):

• gcc:

– por etapas:
gcc -c <lista códigos fontes>
gcc -o <programa executável><lista códigos objeto>

– forma resumida:
gcc -o <programa executável><lista códigos fontes>

• g++:

– por etapas:
g++ -c <lista códigos fontes>
g++ -o <programa executável><lista códigos objeto>

– forma resumida:
g++ -o <programa executável><lista códigos fontes>

Os arquivos de código fonte em C usam a extensão “.c” e os arquivos


de código fonte em C++ usam a extensão “.cpp” ou “.C”. No processo de
compilação (gcc -c ou g++ -c), os códigos fontes dão origem aos arquivos
objeto cujas extensões são “.o”. No processo de link -edição (gcc -o ou g++
-o), todos os arquivos objeto gerados na compilação são processados para gerar
o programa executável.
1.3. COMPILAÇÃO DOS CÓDIGOS-FONTE 11

Existe a forma reduzida de comando onde a compilação e a link -edição são


executadas sequencialmente. O resultado final é o mesmo em qualquer uma
das duas opções. A vantagem de se compilar por etapas se destaca quando o
projeto que está sendo desenvolvido contém muitos arquivos de código. É sem-
pre interessante compilar os arquivos separadamente para se ter um controle
melhor sobre os erros. Quando se compila vários arquivos simultaneamente, a
lista de erros pode ser tão grande que a manutenção e correção dos erros fica
prejudicada, além de desestimular.
Então, como a linguagem está montada sobre esta estrutura, vale olharmos
para cada uma delas separadamente e com mais detalhes. Por isso, o material
ficará organizado da seguinte forma:

• Capı́tulo 2: comandos de declaração de variáveis, os tipos pré-definidos


das linguagens, ponteiros, vetores e matrizes, tipos abstratos de dados.

• Capı́tulo 3: comandos de execução referentes aos operadores algébricos,


relacionais, lógicos e binários.

• Capı́tulo 4: comandos de execução condicionais se-senão, se ternário e


seleção de caso e de comandos de repetição para-de-até, repita-até e
enquanto.

• Capı́tulo 5: comandos de declaração de funções, passagem de argumentos


e protótipos de função.

• Capı́tulo 6: diretivas de compilação.

• Capı́tulo 8: comandos de declaração de classes de objetos e objetos.

Outro detalhe é a codificação dos exemplos. Vai acontecer dos exemplos


conterem elementos (instruções) que não tenham sido apresentados formal-
mente ainda, em especial as diretivas de compilação. Mas, a intenção é que os
exemplos possam ser copiados e testados. No tempo certo, as dúvidas serão
sanadas. Portanto, mãos à obra.
12 CAPÍTULO 1. PROGRAMAÇÃO EM C E C++
Tipos de Dados
2
Como em toda linguagem formal, a linguagem C possui tipos pré-definidos
de dados (tipos intrı́nsecos) que podem ser classificados como:

• tipo literal (que armazena caracteres alfanuméricos),


• tipo numérico inteiro (para dados numéricos dentro do conjunto dos
números naturais positivos e negativos),
• tipo numérico real (próprio para os números racionais) e
• tipo lógico (para o “falso” e “verdadeiro”).

E para que o programador tenha uma liberdade de criação, a linguagem C


permite também a definição de novos tipos de dados – são os chamados tipos
abstratos de dados. Dentre eles, encontram-se:

• os vetores e matrizes (tanto numéricas como literais, as strings),


• as estruturas de dados (na forma de registros e campos),
• as enumerações (como listas ordenadas de constantes) e
• as uniões (que permitem a superposição de estruturas).

Um tópico particular da linguagem C versa sobre os ponteiros, elemento


este que distingui o C de todas as outras linguagens. Os ponteiros são uma
ferramenta muito poderosa que permite ao programador trabalhar diretamente
na memória do computador e descer a um nı́vel mais baixo de programação (o
que nem sempre é necessário). Para tanto, se faz necessário relembrar algumas
caracterı́sticas de representação binária e organização de dados na memória
do computador. Isso facilitará a compreensão de recursos de programação em
C tais como passagem de parâmetros por referência, declaração de vetores e
matrizes e alocação dinâmica de memória.

13
14 CAPÍTULO 2. TIPOS DE DADOS

2.1 Representação de Dados na Memória


Todo dado deve estar armazenado em algum lugar no computador. O
local mais provável é a memória RAM (do inglês random access memory). A
arquitetura dos computadores não permite que os dados sejam armazenados
usando a mesma representação gráfica que nós, humanos, usamos. Como todos
devem recordar, os computadores só reconhecem dois tipos de informação:
“ligado” e “desligado”. Para maior conforto nosso, as informações “ligado”
e “desligado” podem ser representados como “falso” e “verdadeiro” ou 1 e
0, respectivamente. Ainda assim, representar uma informação complexa na
forma de 0’s e 1’s não se traduz numa forma completamente confortável de
leitura, pois não estamos acostumados a ver as coisas desta forma. Mas é muito
importante que sejamos capazes de entender como um dado é armazenado na
memória, pois isso interfere diretamente naquilo que estamos tentando fazer,
ou seja, na programação.
Então, de inı́cio, vamos lembrar dos termos mais usuais, quais sejam: os
bits, os bytes e seus prefixos de grandeza (quilo, mega, giga, tera, etc.).

bit (b): O bit é a menor informação representada no computador. Pode as-


sumir dois valores distintos: 0 e 1. Eletronicamente corresponde às si-
tuações de: tem corrente, não tem corrente, ou tensão diferente de zero,
tensão igual a zero. Daı́ a noção de “ligado” e “desligado”.

byte (B): O byte é o agrupamento de 8 bits. Forma a menor “palavra” em


computadores. Sua decodificação, ou seja, sua interpretação é obtida
usando a base matemática binária. Os bytes podem ser concatenados
formando palavras de 2 bytes (16 bits), 4 bytes (32 bits), 8 bytes (64
bits) e assim por diante. Repare que os agrupamentos são sempre em
potência de 2 (uma vez que estamos usando notação binária).

kilobyte (kB): um kilobyte é o agrupamento de 1024 bytes. Este valor não é


mágico; corresponde ao número 210 ; é a potência de 2 mais próxima do
valor decimal 1000 ou 103 .

megabyte (MB): um megabyte é o agrupamento de 10242 bytes. Segue o


raciocı́nio de potência de 2 mais próximo a 106 .

gigabyte (GB): um gigabyte é o agrupamento de 10243 bytes.

terabyte (TB): um terabyte é o agrupamento de 10244 bytes.

petabyte (PB): um pentabyte é o agrupamento de 10245 bytes.

exabyte (EB): um exabyte é o agrupamento de 10246 bytes.

e por aı́ vai...


2.1. REPRESENTAÇÃO DE DADOS NA MEMÓRIA 15

2.1.1 Representação de Números Inteiros


O conjunto de números naturais incorporam números positivos, negativos e
nulo. Em binário, também se faz necessário representar este mesmo conjunto.
Até aqui, o byte é apenas um agrupamento de 8 bits, sendo cada dı́gito, 0 ou
1, valores compreendidos como positivos. Então, como representar um valor
negativo?
Uma forma muito natural seria admitir o sinal “+” e “−” prefixando os
números binários. Mas o computador só entende 0 e 1. Então, o jeito foi criar
um padrão (internacional e aceito pela maioria das indústrias de componen-
tes eletrônicos) que definisse a representação de números positivos e negati-
vos em binário. Uma da agências internacionais que recomendam padrões é
a IEEE (do inglês, Institute of Electrical and Electronic Engineering). Nas
recomendações de representação de números binárias, consta que o bit mais
significativo (abreviado em inglês para msb) do agrupamento de bytes, o bit
mais a esquerda, pode ser interpretado como o bit de sinal, seguindo a seguinte
codificação:

• se o msb é 0, o número representado é positivo;

• se o msb é 1, o número representado é negativo;

Neste padrão, pensando em um número com 1 byte, o bit mais a esquerda,


o msb, se torna o indicador de sinal. Sobram 7 bits então para representar os
números propriamente ditos.

b7 b6 b5 b4 b3 b2 b1 b 0

msb

Usando aritmética binária, 27 vale 128. Se pusermos o bit de sinal na frente,


terı́amos 128 números positivos (desde o +0 até o +127) e 128 negativos (dede
o −0 até o −127). Surge um problema: temos dois zeros, o +0 e o −0. Não
faz sentido representarmos duas vezes o mesmo valor, até porque estarı́amos
desperdiçando capacidade computacional. Então, para superar esta dificul-
dade, propôs-se um mecanismo de cálculo de números negativos chamado de
cálculo por complemento a 2. Este mecanismo funciona assim: pegue a
representação binário do número positivo; retire o bit de sinal; inverta cada
bit da representação, isto é, troque os 0’s por 1’s e vice-versa (isto se chama
cálculo de complemento a 1); adicione 1 ao resultado das inversões; acres-
cente o digito 1 como msb, o bit de sinal; este novo resultado é a representação
negativa do número positivo inicial.
Um exemplo: o número +9 em binário é 0|0001001(b). Colocamos o sufixo
“(b)” para lembrarmos que o número está em binário e o carácter “|” para
16 CAPÍTULO 2. TIPOS DE DADOS

separar o bit de sinal – isso ajuda na visualização da representação binária. A


representação binária do número −9 será 1|1110111(b). O cálculo do número
−9 é apresentado a seguir:

|0001001(b) → |1110110(b) /* complemento a 1 */


+ 1(b)
|1110111(b) /* complemento a 2 */

Então, o número −9 é 1|1110111(b). Este mecanismo pode ser aplicado a


qualquer agrupamento de bytes, sempre lembrando que o bit de sinal é o msb,
o bit mais a esquerda do agrupamento.
Outros exemplos: o número binário 0|0000000(b) é o 0 decimal, 1|1111111(b)
é o −1 e 1|0000000(b) é o número −128.

2.1.2 Representação de Números Reais


Um número real se difere do número inteiro por conta da parte fracionária.
Os computadores, que só trabalham com 0’s e 1’s, precisam de alguma ou-
tra forma de padronização de representação para números reais. Daı́ que,
novamente, a IEEE gerou uma outra recomendação. Os números reais são
organizados na forma de mantissa e expoente. A mantissa do número real
deve estar no intervalo 0,0 (fechado) e 1,0 (aberto). O expoente se refere a
base 2. Assim, um número real, em binário, deve ser organizado na forma:
mantissa×2expoente . A IEEE sugere uma representação mı́nima para números
reais com 4 bytes de comprimento. Este é o chamado “número real de precisão
simples”. Dos 4 bytes, o byte mais significativo é o expoente e os demais são
a mantissa. O expoente e a mantissa possuem, cada um, um bit de sinal.
O formato binário de um número real usa o ponto decimal e cada bit após o
ponto corresponde à uma potência de 2 com expoente negativo. Por exemplo:
o número binário .1(b) corresponde ao número 1 × 2−1 , isto é, 0, 5 reais. O
número 1|.11(b) vale −(1 × 2−1 + 1 × 2−2 ), ou seja, −0, 5 − 0, 25 = −0, 75 reais.
O número

0|0000001 0|.1100000 00000000 00000000

vale 21 × (1 × 2−1 + 1 × 2−2 ) = 2 × 0, 75 = 1, 5, ou

0|0000000 0|1.100000 00000000 00000000

O dı́gito 1 antes do ponto é uma unidade e o dı́gito 1 após o ponto corresponde


a 0, 5. Logo, este numerão também vale 1, 5 reais e o expoente corresponde,
na prática, ao deslocamento de todos os bits para a direita ou esquerda, de-
pendendo o sinal do expoente. Neste caso, como o expoente é positivo, o
deslocamento é para a esquerda.
2.1. REPRESENTAÇÃO DE DADOS NA MEMÓRIA 17

Por fim, a IEEE também recomenda um formato de dupla precisão para


os números reais. Nele, o expoente tem 2 bytes e a mantissa tem 6 bytes. E
se pode chegar até precisão quádrupla com 4 bytes para o expoente e 12 bytes
para a mantissa. Na prática, o maior formato para números reais usa 4 bytes
para o expoente e 6 bytes para a mantissa.

2.1.3 Tipos de Dados Representados


Como foi visto, os números inteiros podem ser representados agrupando um
ou mais bytes. Cada agrupamento pode conter números com limites distintos.
Um número inteiro representado por 1 byte pode armazenar valores entre 0 e
28 − 1 (= 255) se for sem sinal e entre −27 (= −128) e 27 − 1 (= 127) se for
com sinal. Se a representação do número inteiro utilizar 2 bytes, as faixas de
valores aumentam: entre 0 e 216 − 1 (= 65.535) para números sem sinal e entre
−215 (= −32.768) e 215 − 1 (= 32.767) para números com sinal.
Admitindo diferentes comprimentos em bytes para representação de núme-
ros inteiros, cada linguagem de programação nomeia seus tipos de dados. Na
linguagem C, os números inteiros são representados com tipos de dados que
usam 1, 2 e 4 bytes de comprimento. Cada agrupamento recebe um nome
diferente (um tipo diferente de inteiro). Além disso, existe a possibilidade
de se “tipar” explicitamente, representações de números com e sem sinal. A
linguagem C também permite essa situação.
Quanto aos números reais, as linguagem “tipificam” a precisão segundo a
quantidade de dı́gitos possı́veis para a parte fracionária em decimal. Existe
a precisão simples, dupla e quádrupla. A linguagem C implementa estes três
tipos de números reais.
Sobram, no rol dos tipos de dados que poderiam ser representados em
computador por uma linguagem de programação, os dados lógicos e os literais.
Algumas linguagens implementam os valores lógicos falso e verdadeiro. Não
é o caso da linguagem C. Em C, “falso” é tudo que é igual a nulo e “verdadeiro”
é tudo que for diferente de nulo. Como a base dos computadores é binária, 0
é falso e 1 é verdadeiro; 00000000(b) é falso e qualquer coisa diferente disso é
verdadeiro.
E os dados literais (caracteres), estes são codificados segundo um padrão
internacional chamado ASCII (que é a abreviatura de uma organização de
padronização americana). Cada carácter é indexado na tabela ASCII. Logo,
quando a linguagem C (e provavelmente muitas outras) precisa armazenar um
carácter, ela armazena o ı́ndice do carácter. Como a tabela ASCII tem 256
caracteres, uma palavra de 8 bits é suficiente para acessá-la completamente.
Então, os caracteres em C são representados por um tipo de dado com 1 byte
de comprimento.
18 CAPÍTULO 2. TIPOS DE DADOS

2.1.4 Organização dos Dados na Memória

Por fim, e não menos importante, quando um agrupamento de bytes repre-


sentando um número inteiro ou real é armazenado na memória do computador,
o mesmo precisa reservar uma sequência contı́nua de bytes para esta tarefa.
A ordem com que os bytes componentes destes agrupamento são arranjados
também precisa ser definido de alguma forma.
Durante muito tempo, esta ordem era definida pelo fabricante do dispo-
sitivo (fosse ele um computador, um videogame, uma calculadora, um relógio
digital ou qualquer outra coisa que usasse dados digitais). Com a popularização
dos computadores, sua miniaturização, seu barateamento e o aumento da com-
plexidade das redes de computadores, fez-se necessário uma padronização (que
ainda não é admitida por todos). O padrão mais comum é armazenar dados a
partir do byte menos significativo (abreviado em inglês como lsb) para o mais
significativo (msb). Cada byte de um agrupamento (de um tipo de dado) é
armazenado em um endereço de memória e o endereço do byte menos significa-
tivo é o que se chama de endereço base. A partir do endereço base, os demais
bytes do dado são arranjados.
Numa linguagem de alto nı́vel, quando o programador declara uma variável,
ele está na verdade solicitando ao computador que localize um espaço de
memória com um determinado comprimento em bytes (suficiente para arma-
zenar o dado) e que associe o nome da variável ao endereço de memória onde
o dado será armazenado. Quando o programador atribui um dado à variável,
ele está na verdade solicitando ao computador para copiar o dado no endereço
referente à variável declarada. O computador pega o dado, vê o nome da
variável (que é um identificador), recupera o endereço associado à variável e
transfere o dado para este endereço.
Na linguagem C, existe um “tipo” de dado chamado ponteiro que o diferen-
cia de praticamente todas as outras linguagens (pelo menos as mais antigas).
O ponteiro nada mais é do que o endereço de memória onde o dado está ou o
endereço de memória referente a uma variável. Como endereço é um número
inteiro e precisa ficar armazenado em algum lugar, a variável que armazena
endereços é dita ser do tipo “ponteiro”. Por que? Porque ele aponta para um
endereço especı́fico da memória. Simples assim.
Serão apresentados, nas seções seguintes, os nomes formais dos tipos de
dados definidos pela linguagem C. Você deve reparar que cada tipo de dado tem
um comprimento diferente em bytes. Serão apresentados os tipos intrı́nsecos,
ou seja, pré-definidos, os ponteiros, os vetores e matrizes e os tipos de dados
que o usuário pode elaborar. A linguagem C é muito versátil, permitindo
ao programador uma liberdade de trabalho muito grande como poderá ser
constatado.
2.2. TIPOS DEFINIDOS DE DADO 19

2.2 Tipos Definidos de Dado

2.2.1 Tipo Literal: char

O tipo de variável em C usado para armazenar caracteres se chama char .


Este tipo tem comprimento de 1 byte e é equivalente ao tipo character do
Fortran. Os caracteres válidos que podem ser associados as variáveis char são
os caracteres da tabela ASCII.
O fragmento de código abaixo mostra a declaração de duas variáveis char
chamadas ch e letra. Estas variáveis serão preenchidas com as constantes
literais ‘a’ e ‘+’.

Código 2.1: exemplo2.c

void main ( void ) {


char ch ;
char letra ;

ch = ’a ’;
letra = ’+ ’;
}

Como no Fortran, as variáveis podem ser inicializadas diretamente na de-


claração de variável como a seguir:

Código 2.2: exemplo3.c

void main ( void ) {


char ch = ’a ’;
char letra = ’+ ’;
}

Reparar que a constante literal é formada por um único carácter e que este
é digitado entre apóstrofos.
A linguagem C reserva algumas constantes literais especiais para controle
de edição e exibição de caracteres tais como os exibidos na tabela a seguir.
20 CAPÍTULO 2. TIPOS DE DADOS

constante ASCII
literal hexadecimal carácter significado
\a 0x07 BEL beep
\b 0x08 BS backspace
\f 0x0c FF alimentador de folha
\n 0x0a LF alimentador de linha
\r 0x0d CR retorno de carrilhão
\t 0x09 HT tabulação horizontal
\v 0x0b VT tabulação vertical
\\ 0x5c \ backslash
\’ 0x27 ’ apóstrofo
\” 0x22 ” aspas
\? 0x3f ? interrogação

Fora desta tabela, existe um outro carácter especial chamado NULL que
é ‘\0’. Como seu nome diz, ele é um carácter nulo que corresponde ao valor
zero. Ele será apresentado no tópico sobre ponteiros.
Para imprimir na tela o conteúdo de variáveis do tipo char quando o
conteúdo é uma constante literal, usamos o seguinte comando:

printf("%c",/*variavel*/);

onde o código de formatação “%c” indica que um carácter deverá ser impresso.
E para que a função printf() funcione corretamente, é necessário incluir a
diretiva “#include <stdio.h>” no inı́cio do código. Repetindo o exemplo2:
Código 2.3: exemplo4a.c
# include < stdio .h >

void main ( void ) {


char ch = ’a ’;
char letra = ’+ ’;

printf ( " % c " , ch );


printf ( " % c " , letra );
}

Para “quebrar” a linha após a impressão do conteúdo, pode-se incluir a


constante literal “\n” na string de formatação:
Código 2.4: exemplo4b.c
# include < stdio .h >

void main ( void ) {


char ch = ’a ’;
2.2. TIPOS DEFINIDOS DE DADO 21

char letra = ’+ ’;

printf ( " % c % c \ n " ,ch , letra );


}

2.2.2 Tipo Inteiro: char e int


Em C, variáveis que armazenam valores inteiros são do tipo char e int.
O tipo char é o mesmo usado para armazenar caracteres e, neste caso, o
tipo character do Fortran não possui correspondência. Quando usado para
armazenar valores inteiros, o tipo char pode assumir valores entre -128 e 127,
que são os valores possı́veis para um número inteiro de 8 bits, sendo um deles
o bit de sinal.
Código 2.5: exemplo5.c
# include < stdio .h >

void main ( void ) {


char x = -100;
char i = 54;

printf ( " % d % d \ n " ,x , i );


}

O código de controle de formatação para impressão de um número inteiro


é “%d”. Repare que mesmo a variável sendo do tipo char , o que será impresso
na tela é o número inteiro armazenado na variável, mesmo que a variável tenha
sido inicializada com uma constante literal:
Código 2.6: exemplo5a.c
# include < stdio .h >

void main ( void ) {


char x = ’a ’;
char i = ’1 ’;

printf ( " % d % d \ n " ,x , i );


}

Neste exemplo, o compilador substitui as constantes literais pelos seus respec-


tivos ı́ndices na tabela ASCII: o ı́ndice do carácter ‘a’ é 97 e do carácter ‘1’ é
49.
O tipo int tem comprimento de 32 bits (4 bytes) e pode armazenar números
entre -2.147.483.648 e 2.147.483.647, ou seja, 31 bits para representar o número
e 1 bit de sinal. Este tipo equivale ao tipo integer do Fortran.
22 CAPÍTULO 2. TIPOS DE DADOS

Código 2.7: exemplo6.c


# include < stdio .h >

void main ( void ) {


int k = 10000 , a , h ;

a = -1234567;
h = 85748403;

printf ( " % d \ t % d \ t % d \ n " ,k ,a , h );


}

A constante literal “\t” é responsável pela tabulação da impressão. O


padrão de tabulação em C são 8 caracteres.

2.2.3 Tipo Real: float e double


No Fortran, um número real de precisão simples recebe o nome de real. Em
C, o número real de precisão simples é o float, que possui precisão de 7 dı́gitos
e 4 bytes de comprimento. Os valores limites do tipo float são ±3, 4 × 10±38 .

Código 2.8: exemplo7.c


# include < stdio .h >

void main ( void ) {


float f = 0.03453;
float x = 1.2 e -10;
float y = -0.4356 e23 ;
float z = 10;

printf ( " %f , %f , %f , % f \ n " ,f ,x ,y , z );


}

O código de controle de formatação para o tipo float é “%f”. O número


real é impresso na forma decimal. A questão é que nem sempre este estilo
de formatação é adequado à magnitude do número. O resultado do exemplo
anterior é prova disto. Então, uma alternativa é imprimir o número real no
formato de notação cientı́fica. O código de formatação é “%e”. Experimente
trocar a formatação no exemplo anterior para ver o efeito.
Além do tipo float, a linguagem C possui outro tipo de variável para ar-
mazenar um número real com um número maior de dı́gitos que é o double.
Sua precisão é de 15 dı́gitos e possui 64 bits (ou 8 bytes) de comprimento.
Seus valores limites são ±1, 7 × 10±308 . O double não possui equivalente direto
no Fortran. É necessário modificar o tipo de precisão através da instrução
select kind precision.
2.2. TIPOS DEFINIDOS DE DADO 23

Código 2.9: exemplo8.c


# include < stdio .h >

void main ( void ) {


double G , k , g , Na ;

G = 6.672 e -11; /* constante gravitacional */


k = 1.3807 e -23; /* constante de Boltzmann */
g = 9.80665; /* gravidade padrao */
Na = 6.0220 e23 ; /* numero de Avogadro */

printf ( " numero de Avogadro = % lf \ n "


" constante de Boltzmann = % lf \ n "
" constante gravitacional = % lf \ n "
" gravidade padrao = % lf \ n " ,Na ,k ,G , g );
}

O código de controle de formatação para o tipo double é “%lf” (long float).


Novamente, a formatação pode não ser apropriada para a impressão do número
em função de sua magnitude. Experimente trocar “%lf” para “%g” que ajusta
a formatação automaticamente.
Repare que existem duas variáveis que usam a letra gê, uma delas maiúscula
G e a outra minúscula g. A linguagem C diferencia estes nomes de variáveis
(identificadores), pois ela é case sensitive, ou seja, “sensı́vel à caixa”. Portanto,
as variáveis G e g são distintas.

2.2.4 Tipo Indefinido: void


O tipo void é algo que só existe na linguagem C. Ele representa a ausência
de tipo pré-definido. Possui um comprimento de 4 bytes e pode ser usado
para armazenar endereços de memória se associado a um ponteiro (que será
apresentado mais adiante). Sua aplicação mais intensa se refere à definição de
sub-rotinas que, em C, são funções que não retornam valores.

2.2.5 Tipo Lógico


Um dado do tipo lógico deve, por definição, assumir dois valores possı́veis:
falso e verdadeiro. Em C, não existe um tipo lógico pré-definido como em
Fortran (logical ). Para reproduzir as caracterı́sticas de um tipo lógico, a lin-
guagem C usa a seguinte regra: qualquer dado igual a zero é interpretado como
o valor “falso” e, por oposição, qualquer coisa diferente de zero é considerado
“verdadeiro”.
O tipo lógico é muito útil no caso de tomada de decisão. Dependendo do
conteúdo de uma variável ou do resultado de uma expressão lógica, o algoritmo
24 CAPÍTULO 2. TIPOS DE DADOS

que está sendo executado pode ser desviado para uma posição especı́fica dentro
do código. O teste lógico também está presente nas instruções de repetição. O
teste de parada pode usar uma variável lógica ou o resultado de uma expressão
lógica para determinar se a iteração prossegue ou para.
Tome os seguintes exemplos:
Código 2.10: exemplo9.c
void main ( void ) {
char a = 0 , b = 1;
char c = ’ \0 ’ , d = ’z ’;
int g = 0 , h = -100;
float f = 0 , r = 0.1 e -10;
double x = 0 , y = 1e -30;
}

Se os testes lógicos fossem realizados com as variáveis declaradas acima,


os resultados dos testes para as variáveis a, c, g, f e x seriam “falso”. As
demais variáveis retornariam “verdadeiro”, pois seus conteúdos são diferentes
de zero (nulo). Isso ficará mais claro quando for tratado o tema sobre comandos
condicionais.

2.2.6 Modificadores de Tipo signed e unsigned


Duas palavras reservadas em C são usadas para controlar o uso ou não do
bit de sinal em um número inteiro. Estas palavras são signed e unsigned . Toda
variável inteira é, a princı́pio, uma variável inteira com sinal (usando-se ou não
a palavra signed ). Se há a necessidade de se declarar uma variável inteira sem
sinal, deve-se usar a palavra reservada unsigned antes da declaração do tipo
(somente os tipos char e int aceitam o prefixo signed e unsigned ). Em Fortran,
não há equivalência para este mecanismo.
Uma variável declarada como unsigned char aceita valores entre 0 e 255 e
uma variável do tipo unsigned int assume valores entre 0 e 4.294.967.295. Se
uma variável for declarada como unsigned (sem a declaração int), o compilador
entende que a variável declarada é do tipo unsigned int. Exemplo:
Código 2.11: exemplo10.c
# include < stdio .h >

void main ( void ) {


char r ; /* 1 byte com sinal */
unsigned char t ; /* 1 byte sem sinal */
int i ; /* 4 bytes com sinal */
unsigned j ; /* 4 bytes sem sinal */

printf ( " sizeof ( char )=% d \ nsizeof ( unsigned char )=% d \ n "
2.2. TIPOS DEFINIDOS DE DADO 25

" sizeof ( int )=% d \ nsizeof ( unsigned )=% d \ n " ,


sizeof ( char ) , sizeof ( unsigned char ) ,
sizeof ( int ) , sizeof ( unsigned ));
}

A função sizeof(tipo) retorna o comprimento em bytes do tipo passado


como argumento.
Caberia uma pergunta aqui que seria a seguinte: o que acontece quando se
declara uma variável char , por exemplo, e associa-se um valor maior que 127
a ela?
Código 2.12: exemplo11.c
# include < stdio .h >

void main ( void ) {


char ch = 129; /* ? */

printf ( " % d \ n " , ch );


}

Para responder isto, é necessário analisar o byte que representa a variável


ch. O número 129(d) em binário é 10000001(b). Mas o bit mais significativo
(msb: most significant bit), que é o bit mais a esquerda, é o bit de sinal. Por-
tanto, o processador entende que este número é um número negativo. Para
descobrir que número negativo é este, é necessário calcular-se o complemento
a dois dele.

|0000001(b) → |1111110(b) /* complemento a 1 */


+ 1(b)
|1111111(b) /* complemento a 2 */

O número binário |1111111(b) é o decimal 127(d). Portanto, o computador


entenderá internamente que “char ch = 129;” é, na verdade, “char ch =
-127;”.

2.2.7 Modificadores de Tipo short e long


Outras duas palavra que interferem na declaração de variáveis são as pala-
vras short e long. Elas indicam ao compilador que o tipo que está sendo usado
deve ter um comprimento em bytes menor ou maior respectivamente. O tipo
short int possui 2 bytes e, como tem sinal, pode assumir valores desde -32.768
até 32.767. Se o tipo da variável for declarado como unsigned short int, então
a variável possuirá os mesmos 2 bytes de comprimento, mas aceitará valores
entre 0 e 65.535. Uma variável do tipo long int mantém o mesmo número
26 CAPÍTULO 2. TIPOS DE DADOS

de bytes de comprimento que o tipo int, isto é, 4 bytes. Portanto, pode ar-
mazenar valores entre -2.147.483.648 e 2.147.483.647. A linguagem C aceita a
declaração de variáveis usando-se somente as palavras reservadas short e long.
Ela entende que as variáveis serão do tipo short int e long int respectivamente,
mas esta não é uma boa regra de programação.
O tipo double também admite o modificador long. Uma variável do tipo
long double pode assumir valores com precisão ou amplitudes muito grandes
(±3, 37 × 10±4932 ). Ela possui um comprimento de 80 bits (ou 10 bytes) com
18 dı́gitos de precisão.
Os compiladores Fortran possuem um mecanismo que permite definir o
comprimento de uma variável inteira, mas este mecanismo não é padronizado.
Alguns compiladores aceitam a declaração integer *8 como um inteiro de 8
bytes, outros declaram integer 8, e há aqueles que definem integer (8). Substi-
tuindo o número 8 por 4 ou 2, seria possı́vel declarar-se variáveis inteiras com
4 ou 2 bytes respectivamente.

2.2.8 Type Casting


Type casting é um mecanismo de conversão de tipos que nada mais é que
colocar um dos tipos predefinidos (char , int, float, double, short, long, un-
signed e assim por diante) entre parênteses na frente da variável a ter o tipo
convertido. Veja que esta operação não muda o tipo da variável, mas somente
o seu conteúdo no momento de uma associação. Veja o seguinte exemplo:
Código 2.13: exemplo12.c
# include < stdio .h >

void main ( void ) {


char c ;
int i ;
float f =200.0;

i = ( int ) f ; /* Converter 200.0 para inteiro


significa truncar a parte decimal . */

c = ( char ) f ; /* Aqui , alem do numero 200.0 ser


truncado , pois o tipo char so aceita
números inteiros , o numero 200
ultrapassa o limite de representacao
do char . Logo , o número 200 sera
interpretado como -56. */

printf ( " % f \ t % d \ t % d \ n " ,f ,i , c );


}
2.3. VETORES, MATRIZES E STRINGS 27

2.2.9 Resumo dos Tipos intrı́nsecos


Tipo bits Faixa de valores
unsigned char 8 0 : 255
char 8 −128 : 127
short int 16 −32.768 : 32.767
unsigned short int 16 0 : 65.535
unsigned int 32 0 : 4.294.967.295
int 32 −2.147.483.648 : 2.147.483.647
unsigned long int 32 0 : 4.294.967.295
long int 32 −2.147.483.648 : 2.147.483.647
float 32 ±3, 4 × 10±38
double 64 ±1, 7 × 10±308
long double 80 ±3, 4 × 10±4932

2.3 Vetores, Matrizes e Strings


2.3.1 Declaração de Vetores e Matrizes
Os vetores e matrizes são sequências contı́nuas de um mesmo tipo de
variável cujos elementos individuais podem ser acessados através de ı́ndices.
Em C, um vetor é declarado definindo-se o seu tipo de variável e o nome do
vetor seguido de sua dimensão:

Código 2.14: exemplo13.c


void main ( void ) {
unsigned v_int [100]; /* Tipo : unsigned int
Nome : v_int
Dimens~
a o : 100 elementos . */
}

Uma matriz é declarada da mesma forma: tipo, nome da variável e suas


dimensões. Mas cada dimensão é apresentada individualmente entre colchetes:

Código 2.15: exemplo14.c


void main ( void ) {
double dmat [10][10]; /* Tipo : double
Nome : dmat
Dimens~
a o : 10 x10 . */
}

Como mencionado, elementos individuais nos vetores e matrizes são aces-


sados através de ı́ndices. É importante destacar que os vetores e matrizes em
C começam com o ı́ndice zero. Portanto, v int[0] é o primeiro elemento do
vetor v int e dmat[0][0] é o elemento inicial da matriz. O ı́ndice final do
28 CAPÍTULO 2. TIPOS DE DADOS

vetor e da matriz é sua dimensão menos 1. Para os exemplos apresentados,


os elementos v int[99] e dmat[9][9] são os elementos finais do vetor e da
matriz, respectivamente.
Teoricamente, não há limite no dimensionamento das matrizes. Isto signi-
fica que se poderia criar matrizes N dimensionais com N tendendo a infinito.
Lógico que isso é um exagero, mas fica a ideia de poder-se criar matrizes com
dimensões muito grandes. O maior limitante é a quantidade de memória dis-
ponı́vel. Para se calcular a quantidade de memória ocupada por um vetor
ou matriz, basta multiplicar as dimensões da estrutura (vetor ou matriz) pelo
total de bytes correspondente ao tipo de dado que define a estrutura. Então,
o vetor v int com 100 elementos ocupa 100 × 4 bytes ou 400 bytes. A matriz
dmat ocupa 10 × 10 × 8 bytes ou 800 bytes de memória.

2.3.2 Cadeia de Caracteres (Strings)


As cadeias de caracteres (strings) em Fortran são declaradas usando-se o
tipo character e a palavra reservada len. O len define o comprimento total de
caracteres que a cadeia pode assumir. Em C, o tipo char é usado para declarar
a cadeira de caracteres, que não é mais que um vetor de caracteres. E como
vetor, sua declaração em C é igual à usada em qualquer outra situação:

Código 2.16: exemplo15.c


void main ( void ) {
/* duas strings : frase e palavra . */
char frase [100] , palavra [30];
}

A string frase pode conter até 100 caracteres e palavra, 30; O que diferencia
o C do Fortran é a utilização do carácter NULL como terminador do vetor.
Em Fortran, se uma string chamada “palavra” for declarada com comprimento
30 e contiver a palavra “paralelepipedo” (14 caracteres), os 16 caracteres res-
tantes continuam fazendo parte da string. Caso ela seja impressa na tela do
computador, através do comando “write(unit=*,fmt=“(a)”) palavra”, os 30
caracteres serão impressos. Para eliminar os 6 caracteres restantes, é preciso
usar a função “trim(palavra)”.
Em C, o carácter que indica o fim da string é o NULL. No exemplo acima,
se o vetor palavra[30] contiver a palavra “paralelepipedo”, o décimo quinto
carácter, ou seja, palavra[14], será o carácter NULL.
Quando o programador usar a função “printf(“%s”,palavra);” que imprime
na tela (idêntico ao comando “write” do Fortran), serão impressos somente os
14 caracteres da palavra “paralelepipedo”. Se não houver o terminador NULL,
será impresso a palavra “paralelepipedo” e um monte de outros caracteres. A
sequência de “lixos” só irá parar quando o computador encontrar um carácter
NULL perdido na memória.
2.4. TIPOS ABSTRATOS DE DADO 29

Veja que o nome da string (que é um vetor) é um ponteiro. Portanto,


a função “printf( )” recebe um ponteiro contendo o inı́cio da string e começa
imprimindo na tela a sequência de caracteres até encontrar o NULL. Enquanto
a função não encontrar o NULL, ela continuará imprimindo. Mas o importante
aqui não é a função “printf()”, mas a importância do carácter terminador
NULL e que o programador deve sempre prever que um dos caracteres de
sua string será ele, ou seja, ele terá que somar 1 no comprimento da string.
Sempre. Para armazenar a palavra “paralelepipedo”, ele deve usar no mı́nimo
15 caracteres (14 letras mais o NULL).
Se o programador quiser concatenar duas palavras, a string que receberá
a união das duas deverá ter, no mı́nimo, a soma dos comprimentos delas mais
1. Uma string nula, isto é, sem caracteres (“”), deve ter pelo menos um byte
de comprimento, para acomodar o carácter NULL.
Duas funções em C que são muito úteis na manipulação de strings são:
“strcpy( )” e “strlen()”. A função “strcpy( )” usa dois argumentos: um pon-
teiro que aponta para a área de memória que contém a string e outro que
aponta para o endereço de destino. A função “strlen()” retorna o total de
caracteres que compõem uma string, passada como argumento, exclusive o
terminador NULL. O exemplo a seguir mostra como copiar a string “parale-
lepipedo” para o vetor palavra:

strcpy(palavra,‘‘paralelepipedo’’);

2.4 Tipos Abstratos de Dado


2.4.1 Estruturas de Dados: struct
O Fortran e o C estabelecem um mecanismo de construção de tipos mais
complexos que os intrı́nsecos através de agrupamentos (campos) em estruturas
de dados. Uma situação tı́pica de aplicação de estrutura de dados é a criação
de bancos de dados. Normalmente, deseja-se cadastrar pessoas agrupando,
de alguma forma, seus dados de identificação, tais como: nome completo,
identidade, endereço, profissão, etc.
A palavra reservada em C que define uma estrutura é struct. Por exemplo,
se o programador deseja criar uma estrutura chamada “tDadosPessoais” e
que contenha os campos nome, identidade e endereço, ele deveria escrever o
seguinte fragmento de código:

struct tDadosPessoais {
char nome[256];
char endereco[256];
int identidade;
};
30 CAPÍTULO 2. TIPOS DE DADOS

Repare que a declaração struct termina com o ponto e vı́rgula. Para se de-
finir uma variável deste novo tipo de dado, o procedimento é similar a definição
de variáveis de qualquer outro tipo:

struct tDadosPessoais Usuario;


/* variavel: Usuário
tipo : struct tDadosPessoais */
struct tDadosPessoais Funcionario;
/* variavel: Funcionario
tipo : struct tDadosPessoais */
struct tDadosPessoais Biblioteca;
/* variavel: Biblioteca
tipo : struct tDadosPessoais */

Para preencher qualquer um dos campos de uma estrutura em C, é ne-


cessário utilizar o operador ‘.’ (ponto). Este operador indica o acesso a um
determinado campo da estrutura. Por exemplo: um determinado funcionário
tem identidade 9871234. O código completo seria:
Código 2.17: exemplo16.c
void main ( void ) {
struct tDadosPessoais {
char nome [256];
char endereco [256];
int identidade ;
};

struct tDadosPessoais Funcionario ;


/* tipo : struct tDadosPessoais
variavel : Funcionario */

Funcionario . identidade = 9871234;


/* le - se : campo identidade da variavel Funcionario */
}

Pode-se criar um vetor de estruturas simplesmente adicionando-se a di-


mensão do vetor após o nome da variável estrutura:
Código 2.18: exemplo17.c
void main ( void ) {
/* declaracao da estrutura */
struct tDadosPessoais {
char nome [256];
char endereco [256];
int identidade ;
};
2.4. TIPOS ABSTRATOS DE DADO 31

/* declaracao do vetor de estrutura


Usuario com 1000 entradas */
struct tDadosPessoais Usuario [1000];

/* usuario de ı́ n d i c e 0 */
strcpy ( Usuario [0]. nome , " Joao das Neves " );
strcpy ( Usuario [0]. endereco , " Av . Atlantida , 100/101 " );
Usuario [0]. identidade = 1234;

/* usuario de indice 100 */


strcpy ( Usuario [100]. nome , " Patricia Araujo " );
strcpy ( Usuario [100]. endereco , " R . Xavier , 312/708 " );
Usuario [100]. identidade = 2345;

/* usuario de indice 12 */
strcpy ( Usuario [12]. nome , " Carlos Parreira " );
strcpy ( Usuario [12]. endereco , " R . Da Cruz , casa 100 " );
Usuario [12]. identidade = 6343;

/* usuario de indice 999 */


/* ultima entrada de um vetor de 1000 posicoes */
strcpy ( Usuario [999]. nome , " Raquel de Queiroz " );
strcpy ( Usuario [999]. endereco , " R . Paiva , 398/1102 " );
Usuario [999]. identidade = 4444;
}

A linguagem C permite algumas simplificações muito úteis para o progra-


mador no que se refere a declaração de variáveis de estrutura. A principal
é a declaração da estrutura propriamente dita combinada à declaração das
variáveis. Por exemplo:

Código 2.19: exemplo18.c


void main ( void ) {
/* declaracao da estrutura combinada
a declaracao das variaveis */
struct tDadosPessoais {
char nome [256];
char endereco [256];
int identidade ;
} Usuario [1000] , Funcionario ;

strcpy ( Usuario [0]. nome , " Joao das Neves " );


strcpy ( Usuario [0]. endereco , " Av . Atlantida , 100/101 " );
Usuario [0]. identidade = 1234;
32 CAPÍTULO 2. TIPOS DE DADOS

Funcionario . identidade = 9871234;


strcpy ( Funcionario . endereco , " R . S . Francisco "
" Xavier , 524 " );
strcpy ( Funcionario . nome , " Piquet Carneiro Jr . " );
}

A declaração das variáveis segue a declaração dos campos da estrutura.

2.4.2 Enumerações: enum


As enumerações são agrupamentos de “constantes” associadas à números
inteiros. Por exemplo:

enum Posicao { PARA_CIMA, PARA_BAIXO,


PARA_ESQUERDA, PARA_DIREITA };

Nesta declaração de enumeração, a constante PARA CIMA é vista pelo com-


pilador como o número 0. As constantes PARA BAIXO, PARA ES-QUERDA e
PARA DIREITA são interpretadas como os números 1, 2 e 3 respectivamente.
O papel principal das enumerações é facilitar a rotulação de determinados
números que tenham um significado especial. E a enumeração impede que
uma variável do tipo enumeração assuma outros valores que não tenham sido
declarados na enumeração.
O mecanismo para declarar uma variável do tipo enum Posicao é similar
ao de uma estrutura:

enum Posicao posicao;

A variável posicao pode assumir qualquer um dos valores pré-definidos


para o enum Posicao. Veja o trecho de código a seguir:
Código 2.20: exemplo19.c
void main ( void ) {
/* declaracao da enumeracao */
enum Posicao { PARA_CIMA , PARA_BAIXO ,
PARA_ESQUERDA , PARA_DIREITA };

/* declaracao das variaveis do tipo enumeracao */


enum Posicao posicao , situacao , comando ;

posicao = PARA_CIMA ;
situacao = PARA_DIREITA ;
comando = PARA_BAIXO ;
}
2.4. TIPOS ABSTRATOS DE DADO 33

2.4.3 Uniões: union


As uniões (unions) são estruturas onde os campos compartilham o mesmo
espaço da memória, isto é, os campos que compõem a união estão “superpos-
tos”. Por exemplo, uma união que define dois campos: uma variável do tipo
int e um vetor de 4 elementos do tipo unsigned char .

union char4int {
unsigned char c[4];
int i;
};

A declaração de variáveis do tipo união segue o mesmo modelo das de-


clarações de variáveis de estruturas. No exemplo a seguir, a variável Byte4
está sendo declarada como sendo do tipo union char4int. Para preencher o
campo i de variável Byte4, usa-se o operador ‘.’.

Código 2.21: exemplo20.c


# include < stdio .h >

void main ( void ) {


/* declaracao da uniao */
union char4int {
unsigned char c [4];
int i ;
};

/* declaracao da variavel */
union char4int Byte4 ;

/* acessando o campo ’i ’ da uniao */


Byte4 . i = 0 x01200803 ; /* hexadecimal */

/* impressao do conteudo do vetor ’c ’


como numeros hexadecimais */
printf ( " % x \ t % x \ t % x \ t % x \ n " ,
Byte4 . c [0] , Byte4 . c [1] , Byte4 . c [2] , Byte4 . c [3]);
}

O código de formatação “%x” imprime um número inteiro na forma de um


número hexadecimal. Desta forma, fica mais fácil conferir o conteúdo de cada
byte de dado da estrutura.
O vetor c e a variável i ocupam o mesmo espaço na memória. O esquema
representando a memória ajudará a visualizar o que se passa no programa.
34 CAPÍTULO 2. TIPOS DE DADOS

Endereço Memória Variável



0x1a3c20 0x03

c[0]


0x08

c[1]
i
0x20 c[2]

0x01

c[3]

0x1a3c24

Na memória, o byte menos significativo é o primeiro a ser escrito: 0x03. O


próximo byte é 0x08, o terceiro, 0x20 e o quarto, o mais significativo, 0x01.
Repare que c[0] coincide com o byte menos significativo. c[1] coincide com
o segundo, c[2] com o terceiro e c[3] com o quarto.

2.4.4 Campo de Bits


O campo de bits é um recurso provavelmente exclusivo da linguagem C.
Tem por sintaxe a forma de uma estrutura, mas cada campo declarado dentro
dela refere-se a uma sequência de bits que pode variar de 1 até o limite de 32
bits. O tipo de cada entrada na estrutura de campo de bits deve ser do tipo
unsigned , pois o elemento bit não tem sinal. Por exemplo:

struct CampoBits {
/* bit identificado por b0 tem 1 bit de comprimento. */
unsigned b0:1;
/* o mesmo vale para o bit declarado como b1. */
unsigned b1:1;
/* o campo b2_3 tem comprimento de 2 bits. */
unsigned b2_3:2;
/* e o campo b4_7 tem comprimento de 4 bits. */
unsigned b4_7:4;
};

No exemplo acima, a estrutura CampoBits declara quatro agrupamentos


de bits: dois com 1 bit de comprimento (b0 e b1), um com dois bits (b2 3) e
um com quatro (b4 7). Repare que o total de bits da estrutura é 8 que equivale
a uma variável char . O acesso a cada bit é tratado de forma natural como de
qualquer outra estrutura:
Código 2.22: exemplo21.c
void main ( void ) {
struct CampoBits {
unsigned b0 :1; /* 0 ,1 */
unsigned b1 :1; /* 0 ,1 */
2.4. TIPOS ABSTRATOS DE DADO 35

unsigned b2_3 :2; /* 0..3 */


unsigned b4_7 :4; /* 0..15 */
};

struct CampoBits bits ;

bits . b0 = 0;
bits . b1 = 1;
bits . b2_3 = 2; /* 2 decimal em binario é 10. */
bits . b4_7 = 6; /* 6 decimal em binario é 0110. */

Se o campo de bits for utilizado dentro de uma união, cria-se a possibilidade


de se converter números declarados “binariamente” em decimais e vice-versa.
É interessante perceber que o campo de bits é muito apropriado para geração
de “máscaras” (muito utilizado quando se precisa acessar o hardware e tes-
tar/acionar bits individualmente).

Código 2.23: exemplo22.c


void main ( void ) {
struct CampoBits {
unsigned b0 :1;
unsigned b1 :1;
unsigned b2_3 :2;
unsigned b4_7 :4;
};

union StatusMouse {
/* o campo de bits e a variavel unsigned char */
/* compartilham a mesma area da memoria . */
struct CampoBits bits ;
unsigned char ch ;
};

union StatusMouse sm ;

sm . ch = 12;
/* 12 em binario é 00001100. O bit b0 eh o */
/* mais a direita e os bits de b4_7 , os mais */
/* a esquerda . Portanto , b0 é 0 , b1 eh 0 , */
/* b2_3 eh 3 (11 em binario eh 3 decimal ) , e */
/* b4_7 eh 0 (0000 binario ). */
}

Por exemplo: se o bit b0 corresponde ao botão esquerdo do mouse e o bit b1


é o botão direito, para testar se o usuário está pressionando o botão esquerdo,
bastaria verificar se o bit b0 é 1; para testar o botão direito, é só verificar o
36 CAPÍTULO 2. TIPOS DE DADOS

bit b1 (o mecanismo que liga o bit b0 e o bit b1 ao status do mouse não está
mostrado; assuma que exista um mecanismo que faça isso).

2.4.5 Declaração typedef


typedef é uma palavra reservada da linguagem C que simplifica a declaração
de estruturas, uniões, enumerações e campos de bits. Através do typedef,
declara-se formalmente o nome de novos tipos. A sintaxe do typedef é simples:

Código 2.24: exemplo23.c


/* cria - se a estrutura MeuCadastro . */
struct MeuCadastro {
char nome [256];
char endereco [256];
unsigned telefone ;
};

/* defini - se o novo tipo para struct MeuCadastro


como sendo simplesmente Cadastro . */
typedef struct MeuCadastro Cadastro ;

void main ( void ) {


/* vetor de 100 elementos do tipo Cadastro
( que é , na verdade , struct MeuCadastro ). */
Cadastro cad [100];
}

Sem o typedef, a linha struct MeuCadastro Cadastro estaria criando


uma variável Cadastro do tipo struct MeuCadastro. Com o typedef, o
compilador entende que Cadastro é o novo nome de struct MeuCadastro.
Cadastro é muito mais compacto que struct MeuCadastro.

2.5 Ponteiros
Dada a importância e a frequência com que os ponteiros são utilizados em
C, este “tipo” único de dado, que é tı́pico do C e de umas poucas outras lin-
guagens, será apresentado de forma cuidadosa nesta seção. Antecipando uma
informação crucial, os ponteiros estão intimamente relacionados aos vetores e
matrizes.

2.5.1 Ponteiros e Endereço de Memória


Resgatando o que já foi apresentado na subseção 2.1.4, quando o progra-
mador declara uma variável, compila o código-fonte e o programa é executado,
2.5. PONTEIROS 37

ele sabe que a sua variável será alocada em algum endereço na memória do
computador. O programador não precisa, a princı́pio, saber o endereço da
variável para fazer sua lógica funcionar ou armazenar um dado; o computador
é que faz o papel de relacionar o nome da variável com o endereço no qual ela
foi alocada, e “copiar para” ou “ler de” lá os dados.

Para facilitar a visualização do mecanismo de funcionamento dos ponteiros,


imagine a memória do computador como uma grande pilha de caixas onde cada
uma possui um endereço especı́fico e um byte de comprimento. Quando o pro-
gramador declara uma variável e executa o programa (depois da compilação),
o computador associa uma dessas caixas com o como da variável; é como se o
nome da variável e o endereço na memória fossem sinônimos. Quando o pro-
gramador acessa uma variável, é o endereço dela que o computador enxerga.
Quando o programador lê ou escreve um dado na variável, o computador lê
ou escreve este dado na caixa correspondente à variável. A figura abaixo irá
ajudar.

Endereço Memória Variável

char ch1, ch2; 0x1100 10 ch1


0x1101 20 ch2
ch1 = 10;
ch2 = 2*ch1;

Pela figura, a variável ch1 foi alocada na memória no endereço 0x1100 e a


variável ch2 no endereço 0x1101. Quando a linha de instrução “ch1 = 10;” é
executada, o computador copia o número 10 no endereço da variável ch1, ou
seja, no endereço 0x1100.

A linha de instrução seguinte é “ch2 = 2*ch1;”. O computador irá ler o


dado no endereço da variável ch1, irá multiplicar este dado por 2 e, depois,
escreverá o resultado desta multiplicação no endereço da variável ch2. Note
que, em nenhum momento, o programador precisou saber o endereço das suas
variáveis.
38 CAPÍTULO 2. TIPOS DE DADOS

Endereço Memória Variável

0x1100 i1

int i1, i2;


0x1104 i2

0x1107
0x1108

Vejamos um outro exemplo agora usando variáveis do tipo int que tem
4 bytes de comprimento. Quando o programador declara uma variável do
tipo int, ele está solicitando ao computador que reserve 4 bytes contı́guos
na memória para serem usados no armazenamento de números inteiros. O
computador irá, novamente, associar um endereço de memória ao nome da
variável. O endereço associado é o endereço do primeiro byte dos quatro que
formam o número inteiro, o endereço base (veja a figura anterior).
Se o programador declara duas variáveis int, i1 e i2, o computador reserva
4 bytes para cada uma delas. A variável i1 é alocada no endereço 0x1100. A
variável i2 só poderá ser alocada 4 bytes depois. Isto significa que seu endereço
de memória será 0x1104. Além disso, qualquer outra variável que tiver de ser
alocada na memória, só poderá estar a partir do endereço 0x1108, uma vez
que o byte do endereço 0x1107 ainda faz parte da variável i2.
A relação entre uma variável e seu endereço é biunı́voca, de um para um:
toda variável possui um endereço especı́fico, assim como todo endereço corres-
ponde a uma variável.

2.5.2 Declaração de Ponteiros


A variável ponteiro é declarada a partir de um dos tipos válidos em C,
isto é, é válido declarar ponteiros para: char , unsigned char , short, unsigned
short, int, unsigned , long, unsigned long, float, double e long double. O
compilador reconhece como definição de ponteiro a declaração de uma variável
de qualquer um destes tipos válidos precedido de um asterisco ‘*’. Por exemplo,
o fragmento de código abaixo declara variáveis ponteiro para cada um dos tipos
válidos (os nomes das variáveis foram escolhidos arbitrariamente).

char *ch;
unsigned char *uch;
short *si; /* mesmo que ‘short int’ */
2.5. PONTEIROS 39

unsigned short *usi; /* mesmo que ‘unsigned short int’ */


int *i;
unsigned *ui; /* mesmo que ‘unsigned int’ */
long *li; /* mesmo que ‘long int’ */
unsigned long *uli; /* mesmo que ‘unsigned long int’ */
float *flt;
double *dbl;
long double *ldbl;

Duas variáveis ponteiro do mesmo tipo podem ser declaradas na mesma


linha:

unsigned char *ch1, *ch2;

As variáveis ponteiros ocupam 4 bytes de memória, independente do tipo de


dado apontado. O conteúdo da variável ponteiro é um endereço de memória.
Qualquer endereço de memória é um número inteiro, positivo e sem sinal. Se
o computador é de 32 bits, a variável ponteiro tem 32 bits de comprimento (4
bytes). Caso o computador seja de 64 bits, as variáveis ponteiros ocuparão 8
bytes cada. Nos exemplos apresentados nesta seção, será assumido um com-
putador de 32 bits.

2.5.3 Operador de Endereçamento de Dado(&)


Um ponteiro pode receber um endereço de memória explicitamente (digi-
tado pelo programador ou declarado como constante numérica) ou receber o
endereço de uma variável através do operador ‘&’. Este operador é usado na
frente da variável que se deseja extrair o endereço. Por exemplo:

int *iptr, i; /* iptr é um ponteiro para int e


i é uma variável do tipo int. */

iptr = &i; /* iptr recebe o endereço


da variável i. */

É importante manter a coerência entre tipos de ponteiros e tipos de variáveis


que estão retornando endereço. Ponteiros do tipo int recebem endereços de
variáveis do tipo int; ponteiros do tipo double recebem endereços de variáveis
do tipo double e assim por diante.

2.5.4 Operador de Referenciamento de Dado (*)


Mostrou-se até agora o mecanismo de extração de endereço de uma variável
e subsequente armazenamento em um ponteiro. Para acessar o dado apontado
40 CAPÍTULO 2. TIPOS DE DADOS

pela variável ponteiro, é necessário usar-se o operador ‘*’ antes do ponteiro.


O fragmento de código a seguir mostra o procedimento e a figura auxilia na
visualização da memória:
Endereço Memória Variável

char i1, *iptr, i2; 0x1a3c22 20 i1


0x1a3c23 0x22 iptr
i1 = 20; 0x3c
iptr = &i1; 0x1a
i2 = *iptr; 0x0
0x1a3c27 20 i2

Quando o código é executado, o computador encontra inicialmente a de-


claração de três variáveis: i1, iptr e i2. Ele aloca as variáveis, na ordem de
declaração, em espaços da memória. Suponha que a variável i1 seja alocada
no endereço 0x1a3c22, a variável ponteiro iptr fique no endereço 0x1a3c23 e
a variável i2 fique em 0x1a3c27. As variáveis i1 e i2 ocupam 1 byte cada. Já
a variável iptr, por ser um ponteiro, ocupa 4 bytes da memória.
Uma vez criadas as variáveis, a primeira instrução executada é copiar o
número 20 na variável i1. Depois, copiar o endereço de i1 em iptr. iptr
recebe então o endereço 0x1a3c22. Por fim, copiar o dado apontado pelo
endereço contido em iptr para a variável i2, ou seja, o número 20.

2.5.5 Operador de Referenciamento de Campo de Es-


trutura (->)
Quando uma estrutura é associada a um ponteiro, seus campos são aces-
sados substituindo os operadores ‘.’ pelos operadores ‘->’. Por exemplo:

struct DadosPessoais {
char nome[100];
char matricula;
};

struct DadosPessoais Cadastro[1000], *ficha;

/* endereço do centésimo elemento. */


ficha = &(Cadastro[99]);
/* equivalente a: Cadastro[99].matricula = 5234542; */
ficha->matricula = 5234542;
/* idem: Cadastro[99].nome, ... */
strcpy(ficha->nome, "Sergio Buarque de Holanda");
2.5. PONTEIROS 41

A aplicação de ponteiros com estruturas é mais intensa quando o programa


a ser implementado envolve banco de dados e as estruturas apontam para
outras estruturas. É normal que o espaço ocupado por uma estrutura seja
grande comparado com o espaço de um ponteiro. Nestas situações, é mais
econômico e mais eficiente fazer-se referência ao endereço a estrutura que uma
cópia desta.

2.5.6 Aritmética de Ponteiros


Os endereços de memória são números inteiros sem sinal. Uma vez ar-
mazenados em ponteiros, estes endereços podem ser incrementados ou decre-
mentados. Mas o incremento de uma unidade no endereço de memória pode
não corresponder ao endereço do próximo byte. O número de bytes corres-
pondente ao incremento (ou decremento) de uma unidade depende do tipo do
ponteiro. Assim, se o ponteiro é do tipo char ou unsigned char , somar ou
subtrair uma unidade de um endereço significa somar ou subtrair 1 byte. Se
o ponteiro é um short int ou um unsigned short int, a unidade corresponde
a 2 bytes. Se o ponteiro é do tipo int, unsigned int ou long int e o endereço
de memória é incrementado (ou decrementado) de uma unidade, esta unidade
corresponderá a 4 bytes. Portanto, a unidade somada ou subtraı́da de um en-
dereço de memória corresponde ao comprimento do tipo, em bytes, associado
ao ponteiro. A tabela abaixo resume a correspondência entre o tipo associado
ao ponteiro e o número de bytes acrescido ao endereço de memória quando se
soma uma unidade a este.

tipo apontado bytes


unsigned char 1
char 1
short int 2
unsigned int 4
int 4
unsigned long 4
long 4
float 4
double 8
long double 10

2.5.7 Um Cuidado Mais Especial


É preciso ter um cuidado especial com os ponteiros, porque o uso de um
endereço inválido pode acarretar em desastres. Quando um programa é iniciali-
zado, as variáveis normalmente são preenchidas com valores aleatórios. Alguns
compiladores possuem um dispositivo para “zerar” as variáveis, mas isso não
é padrão. Portanto, entre escrever um programa que controla a inicialização
42 CAPÍTULO 2. TIPOS DE DADOS

dos ponteiros e outro que não, o melhor é optar por perder um pouco mais de
tempo de desenvolvimento e controlar de forma mais rı́gida os ponteiros.
Em termos práticos, cuidar da inicialização dos ponteiros significa iniciá-los
com algum endereço “inofensivo” ou pré-estabelecido. Dentre os milhões de
endereços válidos, o endereço 0x0 (em hexadecimal) é o melhor, pois a maioria
dos sistemas operacionais “sabe” que escrever no endereço 0 é errado. Con-
sequentemente, o sistema operacional gera uma mensagem de erro abortando
o programa. Em C, este endereço 0 é representado pelo carácter NULL, que
já foi mencionado no tópico sobre variáveis do tipo char . A inicialização do
ponteiro com NULL é simples e direto:

char *cptr = NULL;


double *dptr = NULL;
unsigned *uiptr = NULL;

Se o programador compilar seu programa e depois executá-lo, e, em algum


instante, receber uma mensagem de erro por referência indevida a um endereço
de memória, ele deve desconfiar que algum ponteiro em seu programa ainda
esteja com o endereço NULL.
O perigo real é que as ações do sistema operacional (ejetar CD, desligar o
computador, formatar o HD, etc) são funções que estão na memória e tem um
endereço de inı́cio. Se, por um azar do destino, um ponteiro não inicializado
contiver o endereço de entrada da função que formata o HD e este ponteiro
for usado, o sistema operacional pode “entender” que o programador esteja
querendo formatar seu HD. E aı́ ... No melhor das hipóteses, o computador
irá travar ou reiniciar. Mas se o programa estiver rodando em um supercom-
putador junto de dezenas de outros programas, reiniciar o computador pode
não ser uma boa.

2.5.8 Relação entre Ponteiros, Vetores e Matrizes


Existe uma relação muito estreita entre os vetores, matrizes e ponteiros.
O nome de um vetor e de uma matriz (sem o ı́ndice de um elemento) é um
ponteiro. Nos exemplos apresentados acima, v int e dmat são ponteiros do tipo
unsigned e double, respectivamente, e armazenam os endereços onde começam
o vetor e a matriz. Logo, para recuperar estes endereços, não é necessário usar o
operador ‘&’. Só se usa este operador com vetores e matrizes se o programador
deseja descobrir o endereço de um elemento individualmente (referenciado por
seus ı́ndices). Por exemplo:

char cvet[3], *cptr1, *cptr2;

cptr1 = cvet; /* o endereço inicial


do vetor cvet é
2.5. PONTEIROS 43

copiado para cptr1. */


cptr2 = &(cvet[2]); /* o endereço do terceiro
elemento do vetor cvet
é copiado para cptr2. */

Os endereços iniciais do vetor e da matriz correspondem aos endereços


dos primeiros elementos do vetor e da matriz, respectivamente. No exemplo
apresentado, o endereço contido em cvet é o mesmo obtido com o comando
“&(cvet[0])”. Na figura da memória, assumindo o vetor cvet iniciando no
endereço 0x1a3c26, seus elementos e demais variáveis ficam dispostos da se-
guinte forma:

Endereço Memória Variável

0x1a3c22 0x26 cvet


0x3c
Endereço Memória Variável
0x1a
0x0
0x1a3c2d 0x28 cptr2
0x1a3c26 -1 cvet[0]
0x3c
0x1a3c27 2 cvet[1]
0x1a
0x1a3c28 100 cvet[2]
0x0
0x1a3c29 0x26 cptr1
0x3c
0x1a
0x1a3c2c 0x0
0x1a3c2d cptr2

Para acessar um elemento do vetor ou da matriz, basta referenciá-lo pelo


ı́ndice:

cvet[0] = 5; /* copiando o número 5


para o primeiro
elemento de cvet. */

Voltando à relação entre vetores, matrizes e ponteiros, o acesso a um ele-


mento especı́fico em um vetor é idêntico a soma do endereço de entrada do
vetor (endereço base) ao ı́ndice do elemento buscado. Por exemplo:
44 CAPÍTULO 2. TIPOS DE DADOS

char c[4], ch; Endereço Memória Variável

/* acesso direto ao terceiro 0x1a3c22 0x26 c


elemento do vetor c. */ 0x3c
0x1a
ch = c[2]; 0x0
0x1a3c26 6 c[0]
/* acesso indireto ao terceiro 0x1a3c27 20 c[1]
elemento do vetor c através 0x1a3c28 -47 c[2]
de aritmética de ponteiros. */ 0x1a3c29 -55 c[3]
0x1a3c2a -47 ch
ch = *(c+2);

Assumindo que os elementos do vetor c começam no endereço 0x1a3c26, o


resultado da soma do ı́ndice 2 com este endereço gera o endereço 0x1a3c28 que
é o endereço do elemento c[2]. Por isso, o dado apontado por c+2, ou seja,
*(c+2), e o elemento c[2] são os mesmos OBRIGATORIAMENTE! Veja que
isto funciona para todos os elementos do vetor!

c[0] <-> *(c+0) = *c


c[1] <-> *(c+1)
c[2] <-> *(c+2)
c[3] <-> *(c+3)

A organização de uma matriz é mais sofisticada e exige mais tempo para


ser compreendida.
A matriz é um vetor de ponteiros que contêm os endereços dos dados.
Acompanhe o seguinte exemplo: uma matriz m do tipo char com dimensão
2 × 2. A declaração da matriz m pode ser vista no fragmento de código a
seguir:

char m[2][2];

m[0][0] = 6;
m[1][1] = -55;
m[1][0] = -47;
m[0][1] = 20;

Comparando as declarações de uma matriz e de um vetor, entende-se que


o vetor é um ponteiro seguido de uma definição de dimensão. Na declaração
da matriz m, o código “char m[2][2];” parece ser a declaração de um vetor
chamado “m[2]” com dois elementos
2.5. PONTEIROS 45

char m[2]
| {z }[2]
vetor

Endereço Memória Variável Endereço Memória Variável

0x1a3c20 0x24 m 0x1a3c28 0x2e m[1]


0x3c 0x3c
0x1a 0x1a
0x0 0x0
0x1a3c24 0x2c m[0] 0x1a3c2c 6 m[0][0]
0x3c 0x1a3c2d 20 m[0][1]
0x1a 0x1a3c2e -47 m[1][0]
0x0 0x1a3c2f -55 m[1][1]

e a declaração “m[2]” parece ser a declaração de um vetor chamado “m”


também com 2 elementos. E isso está correto. “m[2]” é um vetor de en-
dereços com dois elementos. Ele aponta para as duas linhas que compõem a
matriz uma vez que a matriz não é mais que vetores linha empilhados. Os en-
dereços contidos em “m[2]” são os endereços dos vetores linha da matriz. Cada
vetor linha está alocado na memória a partir de um endereço. Este endereço
é armazenado em “m[2]”. Então, no elemento de ı́ndice 0 de m (m[0] <->
*(m+0)) está armazenado o endereço da primeira linha da matriz. O segundo
elemento de m (m[1] <-> *(m+1)) aponta para a segunda linha da matriz. Se
m[0] é o endereço da primeira linha, o primeiro elemento da primeira linha
pode ser acessado como m[0][0] (= *(*(m+0)+0)). O segundo elemento da
primeira linha é m[0][1] (= *(*(m+0)+1)). Os elementos da segunda linha
são: m[1][0] (= *(*(m+1)+0)) e m[1][1] (= *(*(m+1)+1)).

2.5.9 Alocação de Memória para Ponteiros


Algum texto.
46 CAPÍTULO 2. TIPOS DE DADOS
Operadores Matemáticos, Lógicos e Binários
3
As operações aritméticas, lógicas e binárias são realizadas por operadores
(sı́mbolos) que, dentro da linguagem C, funcionam como se fossem funções.
Esta caracterı́stica ficará mais clara ao final deste capı́tulo. Mas antes de se
apresentar os operadores aritméticos, lógicos e binários, faz-se necessário falar
de um tópico preliminar: a conversão entre tipos de dados.

3.1 Conversão de Tipo e Operador de Atri-


buição
Cada tipo de dado intrı́nseco possui uma representação binária própria
como foi apresentado no capı́tulo anterior. As constantes numéricas também
são armazenadas em formatos binários adequados definidos durante o processo
de compilação do código fonte. Durante a execução do código (após a com-
pilação), toda variável e toda constante (numérica ou literal) está armazenada
na memória do computador em um endereço especı́fico (noção de ponteiro). A
partir de cada endereço, as variáveis e as constantes são alocadas agrupando o
número de bytes correto para cada tipo de dado respeitando a representação
binária dos mesmos.
Assim, quando um dado (constante ou conteúdo de uma variável) é copiado
para uma variável através do operador de atribuição ‘=’, o programa precisa,
antes de tudo, verificar as representações binárias do dado e da variável que está
recebendo o dado. Se as representações forem idênticas, não há a necessidade
de conversão de tipo. No caso de representações binárias diferentes, isto é,
tipos diferentes entre dado e variável que recebe o dado, o programa terá que
adequar o processo.
Por exemplo, uma constante do tipo char possui 1 byte de comprimento e o
bit mais significativo representa o sinal da constante. Se este dado for atribuı́do
a uma variável do tipo char , as representações binárias serão coincidentes e

47
48CAPÍTULO 3. OPERADORES MATEMÁTICOS, LÓGICOS E BINÁRIOS

não haverá conversão de tipo. Qualquer outro tipo de variável que receba este
mesmo dado exigirá adequação, ou seja, conversão de tipo.
Veja este caso:

char a = 10; /* 1 byte com sinal */


char b = -10; /* 1 byte com sinal */

As constantes numéricas 10 e −10 são armazenadas na memória do com-


putador, durante a execução do programa, na forma de constantes do tipo
char , pois 10 e −10 podem ser representados binariamente com exatidão como
0|0001010(b) e 1|1110110(b), respectivamente. Como as variáveis a e b são do
mesmo tipo (char ), não há conversão.
Outro exemplo: imagine duas variáveis inteiras, sem sinal, com 1 byte de
comprimento (tipo unsigned char ), c e d, e duas constantes numéricas (10 e
200).

unsigned char c = 10; /* 1 byte sem sinal */


unsigned char d = 200; /* 1 byte sem sinal */

Na execução do programa que contém este trecho de código fonte, as cons-


tantes serão armazenadas na memória do computador como constantes do
tipo unsigned char , pois ambas as constantes podem ser representadas com
exatidão neste formato. Isso evita conversões desnecessárias.
Veja que a constante numérica 10 foi usada nos dois exemplos e, em cada
um, ela foi alocada com uma representação diferente. Este papel de adequação
de tipo é realizada preliminarmente pelo compilador e posto em prática na
execução do código compilado.
Alguns exemplos onde ocorre a conversão de tipo:

char i = 200;
char j = 10.0;

Na primeira declaração de variável, a variável i é declarada do tipo char


e a constante 200 está sendo atribuı́da a ela como valor de inicialização. A
constante 200 é representada em binário como:

• uma constante inteira de 1 byte sem sinal: 11001000(b)

• uma constante inteira de 2 byte sem sinal: 00000000 11001000(b)

• uma constante inteira de 2 byte com sinal: 0|0000000 11001000(b)

• uma constante inteira de 4 byte sem sinal: 00000000 00000000 00000000


11001000(b)
3.1. CONVERSÃO DE TIPO E OPERADOR DE ATRIBUIÇÃO 49

• uma constante inteira de 4 byte com sinal: 0|0000000 00000000 00000000


11001000(b)

Mas a variável i é representada com 1 byte com sinal. Não “casa” nenhuma
das opções de representação binária da constante numérica 200. Logo, alguma
conversão de tipo deverá acontecer. A regra do compilador é tentar usar sempre
a menor representação binária em termos de comprimento do formato. Desta
forma, o compilador converterá a representação binária 11001000(b) (constante
inteira de 1 byte sem sinal) em 1|1001000(b) (constante inteira de 1 byte com
sinal). A constante 1|1001000(b) é o número −56. Se o conteúdo da variável
i fosse impressa na tela, observar-se-ı́a a resposta “−56”.
Na segunda declaração, a variável j está recebendo a constante real 10.0.
É óbvio que uma variável do tipo inteiro não pode armazenar um dado real.
Logo, haverá uma conversão de tipo com truncamento da parte decimal. A
constante real 10.0 é um valor com sinal. Quando a parte decimal é truncada,
a constante assume a representação numérica de uma constante inteira com
sinal (valor 10). A constante 10 pode ser atribuı́da a uma variável do tipo
char sem problemas. Então, o efeito final da conversão foi apenas o trunca-
mento. Se a constante real fosse 200.0, provavelmente o processo de conversão
aplicaria um truncamento da parte decimal e depois uma conversão do valor
200 em −56. Bem, o resultado em si não importa aqui. O importante é que
o resultado final desta atribuição não será o que o programador estará espe-
rando: o programador tentou inicializar a variável j com o valor 200.0 e, no
final, ele terá qualquer outro resultado que não o 200.0. Esse é um erro de
lógica comum de acontecer com os principiantes em programação. Portanto,
CUIDADO!

unsigned k = -10;
short l = 600000;

Na declaração da variável k, a constante negativa −10 será convertida


em um valor inteiro de 1 byte sem sinal. A declaração seguinte, variável l,
tenta atribuir uma constante inteira com mais de 2 bytes de representação
binária a uma variável que suporta somente representações de 2 bytes. Logo,
haverá truncamento de byte. Neste caso, os dois bytes mais significativos serão
eliminados, resultando numa constante de 2 bytes.

float m = 1;
double n = ‘x’;

Com os tipos reais não é diferente. A variável m foi declarada como float
e está recebendo uma constante inteira. O tipo da constante será trocada de
inteira para real e uma parte decimal será acrescentada à constante. Como
efeito final, a variável m armazenará o valor 1.0. Esta conversão não oferece
50CAPÍTULO 3. OPERADORES MATEMÁTICOS, LÓGICOS E BINÁRIOS

risco de lógica, mas é sempre prudente declarar as constantes já no formato da


variável que irá recebê-la, ou seja, “float m = 1.0;”. E na última declaração
de variável, a variável n é do tipo double e está recebendo a constante literal
‘x’. A constante literal é representada binariamente com o valor do ı́ndice
da tabela ASCII correspondente. A constante ‘x’ corresponde ao valor 120.
Logo, a variável n apresentará o valor 120.0.
Tudo que está sendo dito em relação a atribuição de constantes à variáveis
vale para o caso de cópia de conteúdo de variáveis para outras variáveis. Isto
significa que se as variáveis forem de tipos diferentes, haverá conversão de tipo.
Isso também acontece com o resultado de funções (intrı́nsecas ou não). Sempre
que um dado estiver sendo copiado ou atribuı́do, o programa processará as
regras de conversão de tipo.
Agora, serão apresentados os operadores aritméticos, lógicos e binários.

3.2 Operadores Aritméticos


Os operadores aritméticos para soma, subtração, multiplicação e divisão
são, respectivamente: ‘+’, ‘−’, ‘∗’ e ‘/’. A ordem de precedência na execução
das operações aritméticas é: multiplicação e divisão primeiro, seguidos da
soma e subtração. Os operadores de precedência são os parênteses ‘(’ e ‘)’
e são capazes de modificar a ordem de precedência padrão. Em C, não há
operador de potenciação (que no Fortran corresponde ao duplo asterisco ‘**’).
A potenciação ab é obtida através de uma função intrı́nseca chamada pow(a,b).
Uma observação importante: a expressão ‘p’ + ‘i’ não gera a string
‘‘pi’’. O operador de soma não funciona como operador de concatenação.
Somar dois caracteres é o mesmo que somar dois números inteiros. Lembre-
se que, em C, as constantes caracteres são substituı́das pelos seus respectivos
ı́ndices na tabela ASCII. A concatenação é realizada por uma função intrı́nseca
chamada strcat(a,b). O resultado desta função é o conteúdo da variável a
concatenada ao conteúdo da variável b.
Outra observação importante: como todas as operações aritméticas estão
sujeitas às regras de conversões de tipo, as operações com números inteiros
devem gerar resultados inteiros. A soma, a subtração e a multiplicação de
dois números inteiros não exigem grandes cuidados, a não ser a extrapolação
dos valores limites de representação numérica dos resultados. Mas a divisão
de dois números inteiro, assim como no Fortran, gera outro número inteiro.
Logo, a divisão do número inteiro ‘1’ pelo número inteiro ‘2’ não gera ‘0.5’ e
sim ‘0’. Por isso, cuidado na hora de definir os tipos das variáveis caso elas
estejam envolvidas em operações do tipo divisão.
Além dos tipos tradicionais de operadores aritméticos, a linguagem C ofe-
rece mais três: um operador de incremento unitário, um outro de decremento
unitário e um operador para resto de divisão de dois números inteiros.
3.2. OPERADORES ARITMÉTICOS 51

Os operadores de incremento e decremento unitário são, respectivamente,


‘++’ e ‘−−’. Eles são unários, isto é, operam com um único operando e
adicionam uma unidade à variável associada ao operador. Por exemplo:

char i = 20;
unsigned k = 10;
float m = 1e-3;

i++;
k--;

Na instrução i++, a variável i será acrescida de uma unidade. Como ela


foi inicializada com o valor 20, a instrução i++ atualizará o valor de i para 21.
A variável k, inicializada com o valor 10, será decrementada de uma unidade.
Logo, após a instrução, o valor de k será 9.
Uma particularidade dos operadores de incremento e decremento é a ordem
em que eles aparecem em uma expressão aritmética. Como operadores unários,
eles podem preceder a variável ou não. Veja as situações seguintes:

char i = 20, j, k;

j = i++;
k = ++j;

Neste primeiro exemplo, a variável i é inicializada com o valor 20. A


primeira instrução é “j = i++;”. Como o operador de incremento vem depois
da variável i, seu conteúdo será, primeiro, copiado para a variável j e, depois,
ela será incrementada. Então, na sequência: j recebe o valor de i, ou seja, “j
= 20”; i é incrementado, ou seja, “i = 21”.
Na segunda instrução, o operador de incremento vem antes da variável j.
Logo, primeiro, j será incrementado e, depois, o resultado será copiado para
a variável k. De novo, na sequência: j é incrementado, ou seja, “j = 21”;
depois, k recebe o resultado do incremento, ou seja, “k = 21”.
Mais um exemplo, agora usando o operador de decremento:

unsigned l = 10, m, n;

m = --l;
n = m--;

A variável l é inicializada com o valor 10 e na primeira instrução, ela é


decrementada. Este resultado é então copiado para a variável m. l começou
com o valor 10, foi decrementada passando para 9 e seu novo valor foi copiado
para m, ou seja, o valor 9.
52CAPÍTULO 3. OPERADORES MATEMÁTICOS, LÓGICOS E BINÁRIOS

Na segunda instrução, a variável n recebe o conteúdo de m (o valor 9) antes


de m ser decrementado. Então, m é decrementado assumindo o valor 8.
Outro operador especial é o operador de resto de divisão de inteiros. O
sı́mbolo do operador é ‘%’ e ele age de forma similar à função modulo(a, b) do
Fortran quando a e b são variáveis inteiras. A versão da função modulo(a, b)
para a e b reais não possui correspondente em linguagem C.

3.3 Operadores Relacionais


3.3.1 Operadores de Igualdade (==) e de Diferença (!=)
Algum texto.

3.3.2 Operadores Maior (>) e Maior que (>=)


Algum texto

3.3.3 Operadores Menor (<) e Menor que (<=)


Algum texto

3.4 Operadores Lógicos


3.4.1 Operador de Conjunção: E Lógico (&&)
Algum texto.

3.4.2 Operador de Disjunção: OU Lógico (||)


Algum texto.

3.4.3 Operador de Negação (!)


Algum texto.

3.5 Operadores Binários


3.5.1 Operador E Binário (&)
Algum texto.
3.6. OPERADORES DE ATRIBUIÇÃO CONCATENADOS 53

3.5.2 Operador OU Binário (|)


Algum texto.

3.5.3 Operador OU EXCLUSIVO Binário (ˆ)


Algum texto.

3.5.4 Operador de Negação Binário (∼)


Algum texto.

3.5.5 Operadores de Deslocamento Binário (<< e >>)


Algum texto.

3.6 Operadores de Atribuição Concatenados


3.6.1 Operador de Adição (+=)
Algum texto.

3.6.2 Operador de Subtração (-=)


Algum texto.

3.6.3 Operador de Multiplicação (*=)


Algum texto.

3.6.4 Operador de Divisão (/=)


Algum texto.

3.6.5 Operador Lógico E Binário (&=)


Algum texto.

3.6.6 Operador Lógico OU Binário (|=)


Algum texto.
54CAPÍTULO 3. OPERADORES MATEMÁTICOS, LÓGICOS E BINÁRIOS

3.6.7 Operador Lógico OU EXCLUSIVO Binário (ˆ=)


Algum texto.

3.6.8 Operadores de Deslocamento Binário (<<= e >>=)


Algum texto.
Estruturas de Controle de Execução
4
4.1 Estruturas de Condição
4.1.1 Estrutura do Se
Toda linguagem de programação estruturada possui um instrumento de
controle do tipo Se-Senão (no inglês if-then) e no C não poderia ser diferente.
As estruturas do comando if são as seguintes:

if (condiç~
ao for verdadeira) {
/*
bloco de comandos para serem
executados caso a condiç~
ao seja
verdadeira
*/
}

if (condiç~
ao for verdadeira) {
/*
bloco de comandos para serem
executados caso a condiç~
ao seja
verdadeira
*/
} else {
/*
bloco de comandos para serem
executados caso a condiç~
ao seja
falsa
*/
}

55
56 CAPÍTULO 4. ESTRUTURAS DE CONTROLE DE EXECUÇÃO

Primeira observação: repare que a limitação dos blocos que serão executados
nos casos de testes verdadeiros ou falsos é definida pelos caracteres “{” e “}”.
Na linguagem C, estes caracteres são chamados de “limitadores de escopo”.
Como situação particular, caso o bloco de instruções do if ou do else for com-
posto por uma única instrução, os limitadores de escopo podem ser omitidos.
Mas isso gera uma péssima prática de programação, pois, se o programador,
durante uma manutenção de seu código, achar que são necessárias novas ins-
truções dentro do bloco do if (ou else) e ele não colocar os limitadores de
escopo, o código não fara nada do que ele acredita estar fazendo. Os resulta-
dos serão imprevisı́veis, além de incontroláveis. Portanto, como sugestão, use
sempre os limitadores de escopo, mesmo que o if e/ou o else encerrem uma
única instrução em seus blocos.

Segunda observação: o comando if do C não usa a partı́cula then. Ela está


implı́cita no comando.

Não há limitação quanto ao aninhamento de comandos if ’s dentro de outros


if ’s ou if ’s dentro dos blocos dos else’s. A organização destes depende exclu-
sivamente da lógica do programador.

Outra observação diz respeito ao comando if do Fortran. Lá, existe uma


variante do comando if que otimiza a escrita de if ’s encadeados; é o if-then-
elseif. Este comando não possui equivalente no C. Mas, isso não representa
qualquer prejuı́zo para a linguagem, uma vez que a estrutura presente no
Fortran não é mais que uma simplificação de escrita e não um novo código.
Então, se o programador precisar usar uma lógica que envolva algo do tipo if
encadeado, ele usa o encadeamento explicito, como no exemplo a seguir:
if (condiç~
ao1 for verdadeira) {
/*
bloco de comandos para serem
executados caso a condiç~
ao1 seja
verdadeira
*/
} else if (condiç~
ao2 for verdadeira) {
/*
bloco de comandos para serem
executados caso a condiç~
ao2 seja
verdadeira
*/
} else if (condiç~
ao3 for verdadeira) {
/*
bloco de comandos para serem
executados caso a condiç~
ao3 seja
4.1. ESTRUTURAS DE CONDIÇÃO 57

verdadeira
*/
} else {
/*
bloco de comandos para serem
executados caso a condiç~
ao3 seja
falsa
*/
}

Podem existir tantos “if (cond1) { } else if (cond2) { } · · · ” quantos o


programador desejar.

4.1.2 Estrutura do Se Ternário


Apesar do C não possuir a instrução if-elseif do Fortran, o C possui uma
variante do comando if que é muito reduzida. É o comando chamado “Se
Ternário”. Este comando, sim, é muito otimizado, usando, inclusive, instruções
de baixo nı́vel diferentes dos tradicionais. É um tipo de comando if muito
rápido. Seu formato é o seguinte:

condiç~
ao ? resultado1 : resultado2 ;

Parece estranho? Veja o exemplo:

float temp, limite;


int on_off;

limite = 60.0;
temp = sensor_de_temperatura();
on_off = (temp > limite) ? 1 : 0;
ligar_alarme_de_incendio(on_off);

Sem grande preocupações, suponha que a função sensor de temperatura()


retorne um valor real que corresponda à temperatura de um sensor e que a
função ligar alarme de in- cendio(on off) seja responsável por acionar um
alarme caso a temperatura ultrapasse um determinado limite de temperatura.
O que o “Se Ternário” faz é comparar a temperatura temp com o valor limite
estabelecido. Se a temperatura for maior que o limite, então, o “Se Ternário”
retorna o valor 1 que será atribuı́do à variável on off. Caso a temperatura não
seja maior que o limite, então o “Se Ternário” retorna o valor 0 e a variável
on off receberá este valor.
58 CAPÍTULO 4. ESTRUTURAS DE CONTROLE DE EXECUÇÃO

4.1.3 Estrutura de Seleção de Caso


A linguagem C também oferece uma estrutura de controle do tipo “Seleção
de Caso”. O comando é o switch(valor ). Ele avalia o conteúdo da variável
valor e desvia a execução do código para o “caso” correspondente. O formato
do comando switch é:

switch(valor) {
case n1:
/* bloco corresponde ao caso n1 */
break;
case n2:
/* bloco corresponde ao caso n2 */
break;
case n3:
/* bloco corresponde ao caso n3 */
break;
/* outros ‘case’s testando outros
conteudos de ‘‘valor’’ */
default:
/* bloco corresponde ao caso default */
}

Cada case é encerrado por um comando break que tem o papel de inter-
romper a execução do bloco de instruções. Assim, se o conteúdo da variável
valor for igual a número representado por n1, a execução do código é desviado
para o bloco do “case n1” e terminará no primeiro comando break que for
encontrado. Se “case n1” não tiver o comando break, a execução do código
invadirá o bloco de “case n2” e parará no comando break. Este comporta-
mento da estrutura do switch é útil quando vários case’s são tratados por um
mesmo bloco de instruções. Veja o seguinte exemplo:

falta o exemplo

4.2 Estruturas de Repetição


4.2.1 Estrutura de Laço Definido
A estrutura de repetição clássica é o laço com inı́cio e fim, ou seja, a
estrutura de repetição tem um ponto de partida, normalmente um contador
inicializado com o valor 0, e um teste de fim de repetição, além do passo de
incremento. No C, esta estrutura é realizada pelo comando for . Sua sintaxe
é: “for ( contador =valor ; contador ¡limite; incremento ) { }”.
4.2. ESTRUTURAS DE REPETIÇÃO 59

O primeiro termo do for diz respeito a inicialização do contador de re-


petição. O contador, em si, pode ser de qualquer um do tipos numéricos
válidos. O segundo termo é o teste de continuidade da repetição. Enquanto
o teste resultar “verdadeiro”, a repetição continua. O terceiro termo é a ins-
trução de incremento do contador. Se o contador é do tipo inteiro, o incremento
pode ser uma simples operação de incremento unário. Também é válido de-
crementar o contador. Se o contador é do tipo real, o incremento deve ser
declarado de forma explı́cita. Veja o exemplo:

int cont, soma;

soma = 0;
for ( cont=-10; cont<10; cont++ ) {
/* somatório de uma série aritmética */
soma = soma + 1;
}

A variável contadora cont é inicializada com o valor −10. O laço de re-


petição prosseguirá até que o conteúdo de cont não seja mais inferior a 10,
valor limite estabelecido neste exemplo. O incremento está implementado pelo
operador de incremento unário. Isto significa que o contador evoluirá de uma
em uma unidade, desde −10 até 9. Quando o contador cont for igual a 10, o
teste de repetição falhará, pois 10 não é menor que 10. O resultado, portanto,
é falso e o laço para.
A execução do comando for segue a seguinte dinâmica:

1. o contador é inicializado;

2. o contador é confrontado com o limite de repetição;

3. se o resultado for verdadeiro, então

4. o programa segue rodando as instruções no bloco de repetição;

5. após a última instrução do laço de repetição, o contador é incrementado;

6. repete o passo 2);

7. se o resultado for falso, o laço de repetição é interrompido e o programa


segue normalmente.

No passo-a-passo do exemplo, o contador é iniciado com o valor −10 e


testado contra o limite, que é −10. Como o resultado é verdadeiro, o bloco
de repetição é executado, isto é, “soma = soma + 1;”. Como não há outra
instrução, o contador é incrementado passando para o valor −9 e é confrontado
com o limite de repetição. Novamente, o resultado é verdadeiro e o processo de
60 CAPÍTULO 4. ESTRUTURAS DE CONTROLE DE EXECUÇÃO

repetição continua até que o contador assume o valor 9 depois de 19 iterações.


Então, o contador é comparado com o limite resultado em verdadeiro. A ins-
trução é executada pela vigésima vez e o contador é incrementado. Neste
momento, o contador assume o valor 10. Quando comparado ao limite de re-
petição, o resultado é falso e o laço de repetição é interrompido. A inicialização
do contador acontece uma única vez, no inı́cio do processo de repetição.
Existem algumas situações onde o laço precisa ser interrompido antes de
chegar ao fim. Para isso, usa-se o comando break, o mesmo usado na estrutura
de seleção de caso. O papel do break é encerrar a execução da estrutura na
qual está inserida, mas não encerrar o programa.

4.2.2 Estrutura de Laço Condicional


Existem duas estruturas de repetição condicional: o while(condição){ } e o
do { } while(condição). Em ambas, a repetição continua até que a condição re-
torne “falso”, ou seja, enquanto a condição for verdadeira. Para interromper
a repetição, pode-se usar o comando break como no for e no switch.
Um exemplo usando while(condição){ }:

int cont, soma;

soma = 0;
cont = -10;
while ( cont<10 ) {
/* somatório de uma série aritmética */
soma = soma + 1;
cont++;
}

Exemplo usando o do { } while(condição):

int cont, soma;

soma = 0;
cont = -10;
do {
/* somatório de uma série aritmética */
soma = soma + 1;
cont++;
} while ( cont<10 );

A diferença entre estas duas estruturas é o momento em que a condição


é avaliada. No while(condição){ }, a condição é testada logo no inı́cio. Isto
significa que, se a condição não for “satisfeita de cara”, nenhuma iteração será
4.2. ESTRUTURAS DE REPETIÇÃO 61

realizada. Já no caso do do { } while(condição), como a condição só é avaliada


no final do bloco, pelo menos uma iteração será realizada, mesmo se a condição
for falsa.
62 CAPÍTULO 4. ESTRUTURAS DE CONTROLE DE EXECUÇÃO
Funções
5
Em C, não existe diferença entre a declaração de uma função e de uma
sub-rotina. Tudo é função. A princı́pio, a linguagem C sempre espera que
alguma coisa seja retornada como resultado da função. Então, como criar algo
que tenha um comportamento de sub-rotina? O jeito é declarar uma função
que “retorna” um valor void . Lembre-se que void não representa um tipo
especı́fico de variável. Desta forma, o compilador entenderá que a “função”
é, na verdade, uma sub-rotina. Vamos começar primeiro mostrando como se
declara uma função.
A declaração de função precisa definir o tipo de valor que será retornado,
onde todos os tipos intrı́nsecos, os tipos abstratos definidos pelo usuário e
os criados por typedef são válidos. Depois, vem o nome da função e, entre
parênteses, os argumentos da função (que é o jeito que o Fortran usa).

double quadrado(double x) {
double y;
y = x*x;
return y;
}

O exemplo acima define uma função chamada quadrado que recebe como
argumento uma variável do tipo double chamada x. A função quadrado retorna
um valor do tipo double (variável y). O corpo da função é composto pela
declaração da variável y, pela linha que diz que y é igual a x vezes x e pelo
“return y;”. A palavra reservada return indica o que é que deve ser retornado
pela função.
Uma função pode ter mais de um return, dependendo da lógica da função.
Por exemplo:

int MaiorQue(double a, double b) {


/* se a eh maior que b, entao...*/

63
64 CAPÍTULO 5. FUNÇÕES

if (a>b)
/* retorna "verdadeiro" e a funcao termina; */
return 1;
/* senao */
else
/* retorna "falso" e a funcao termina. */
return 0;
}

Vejamos agora uma sub-rotina. A função no papel de sub-rotina deve


retornar “nada”, ou seja, deve ser do tipo void .

void ImprimeValor(int a) {
printf("O valor da variavel eh: %d\n",a);
}

A função ImprimeValor recebe um argumento do inteiro e imprime na tela


o conteúdo deste argumento. A função printf escreve na tela do computador
“O valor da variavel eh:” e substitui os caracteres de controle “%d” pelo valor
da variável ‘a’ (a letra ‘d’ indica um número inteiro com ou sem sinal).
A passagem de parâmetros não é obrigatória. Quando a função não ne-
cessita de argumentos, deve-se usar o tipo void no lugar da passagem de
parâmetros. Por exemplo:

void MensagemPadrao(void) {
printf("Estou vivo.\n");
}

A função MensagemPadrao não retorna nem recebe valor algum, apenas


imprime a frase “Estou vivo.”

5.1 Argumentos por Valor e por Referência


Os argumentos podem assumir papéis importantes na execução do pro-
grama. Existem duas formas de se transmitir dados através dos argumentos:
passagem de argumentos por valor e passagem de argumentos por referência.
obs.: será adotado, daqui em diante, o uso somente do termo “função”.
Se, em um exemplo, a função for declarada como void , deve-se entender, de
imediato, que ela desempenha um papel de sub-rotina.
Quando se passa um dado como sendo “um argumento por valor”, está-se
apenas copiando o dado para dentro da função. Aquilo que acontece dentro da
função, não afeta o valor original do dado (que está declarado fora da função).
Por exemplo, seja vezes2 uma função que multiplica o dado de entrada por
dois e armazena o resultado nela mesmo:
5.1. ARGUMENTOS POR VALOR E POR REFERÊNCIA 65

double vezes2(double a) {
a = a*2;
return a;
}

Imagine agora o programa principal que chama a função vezes2:


Código 5.1: exemplo24.c
# include < stdio .h >

double vezes2 ( double a ) {


a = a *2;
return a ;
}

int main ( void ) {


double x , y ;

x = 29.3;
y = vezes2 ( x );

/* % lf diz respeito a um valor double . */


printf ( " O valor de x eh : % lf \ n " ,x );
printf ( " O valor de y eh : % lf \ n " ,y );
}

Da forma como a função vezes2 foi declarada, o valor de ‘x’ (no programa
principal) será COPIADO para dentro da função. Quem “captura” a cópia do
valor de ‘x’ dentro da função é a variável ‘a’. Modificar o conteúdo de ‘a’ não
afetará o valor de ‘x’ (que está fora da função), porque ‘a’ recebeu uma cópia
do valor de ‘x’. Pensando em termo de endereço de memória, as variáveis ‘x’
e ‘a’ estão localizadas em lugares DISTINTOS na memória do computador.
Logo, modificar ‘a’ não perturbará ‘x’. Isto é passagem de argumento por
valor. O mais correto seria dizer passagem de argumento por cópia de
valor.
Para que o dado seja passado para dentro de uma função e as alterações
que forem feitas sejam percebidas fora da função, é necessário que o dado
não seja simplesmente copiado. É necessário fazer-se uma referência ao lo-
cal na memória onde o dado original se encontra. E endereço de memória é
PONTEIRO!!! Portanto, para que um dado seja passado para dentro de uma
função e que a manipulação desse dado seja refletida fora da função, deve-se
passar como argumento o endereço do dado na memória. Isto é passagem de
argumento por referência. Exemplo:
66 CAPÍTULO 5. FUNÇÕES

void dobro(double *a) {


/* o asterisco indica que ‘a’ é um ponteiro. */
(*a) = (*a)*2;
/* aqui, o asterisco indica ‘o dado apontado por a’. */
}

A função dobro recebe então, como argumento, o endereço do dado original


que é armazenado em ‘a’. Qualquer alteração no dado apontado por ‘a’ será
percebido fora da função porque a operação manipula diretamente o dado
original através de seu endereço. Atualizando o exemplo anterior:

Código 5.2: exemplo25.c


# include < stdio .h >

void dobro ( double * a ) {


(* a ) = (* a )*2;
}

int main ( void ) {


double x ;

x = 29.3;
dobro ( x );

/* % lf diz respeito a um valor double . */


printf ( " O valor de x eh : % lf \ n " ,x );
}

5.2 Protótipo de uma Função


A linguagem C é uma linguagem “prototipada”. Isto significa que o compi-
lador espera encontrar os protótipos das funções antes de encontrar as funções
propriamente ditas. A prototipagem é um mecanismo de gerenciamento do
compilador que previne erros com relação à passagem de parâmetros e valores
de retorno.
O protótipo de uma função não é mais que a cópia de sua declaração sem
a codificação. Inclusive, o protótipo não precisa declarar o nome das variáveis
que serão passadas como argumento, basta o tipo delas. O exemplo abaixo
mostra isso:

/* prototipos das funcoes vezes2 e dobro. */


double vezes2(double);
void dobro(double *);
5.3. RELAÇÃO DAS FUNÇÕES INTRÍNSECAS 67

/* declaracao da funcao vezes2*/


double vezes2(double a) {
a = a*2;
return a;
}
/* declaracao da funcao dobro*/
void dobro(double *a) {
(*a) = (*a)*2;
}

Como se pode ver, o protótipo não é mais que a cópia da declaração da


função sem as variáveis e sem o código. O efeito prático da prototipagem é a
regulação de erros pelo compilador. Quando o compilador converte o código-
fonte em código-objeto (etapa anterior a “link-edição”), ele compara todas as
chamadas de função com os modelos de declaração pré-existentes, ou seja, as
funções das bibliotecas padrões. Mas, uma função criada pelo programador
não faz parte de nenhuma biblioteca padrão. Neste caso, o compilador fatal-
mente gerará uma mensagem de erro registrando o encontro de uma função
desconhecida. O problema é facilmente superado pela inclusão do protótipo
da função criada pelo programador no inı́cio do código-fonte, a exemplo do que
acontece com o Fortran onde a declaração das bibliotecas que serão usadas no
código-fonte aparece no inı́cio do programa. Isso evita que o compilador For-
tran gere erros por encontrar funções desconhecidas. Além disso, se existem
os protótipos das funções pessoais do programador, o compilador é capaz de
detectar erros ao longo do código-fonte.
Alguém poderia perguntar: as funções que fazem parte das bibliotecas
do C têm protótipo? A resposta é sim. Os protótipos de todas as funções
das bibliotecas padrões estão em arquivos chamados de arquivos de cabeçalho
(header files). Estes arquivos serão apresentados mais adiante na seção sobre
organização da linguagem C.
O mais importante agora é lembrar ao programador (você) que, sempre que
for(em) escrito(s) função(ões) particular(es), deve-se colocar o(s) protótipo(s)
de sua(s) função(ões) no inı́cio do código-fonte.

5.3 Relação das Funções Intrı́nsecas


As funções intrı́nsecas da linguagem C e C++ estão organizadas em uma
biblioteca chamada “libstdc” e “libstdc++” respectivamente. Para facilitar
a vida do programador, o consórcio internacional que regulamenta e provê
suporta às linguagens C e C++ (sigla ISO, do inglês International Standardi-
zation Organization) organiza os protótipos das funções intrı́nsecas em grupos
de arquivos de cabeçalho distintos. Cada arquivo de cabeçalho atende a um
68 CAPÍTULO 5. FUNÇÕES

grupo de propósitos. A tabela a seguir mostra como as funções estão agrupa-


das.

Caracterı́stica Principal Arquivo de


Cabeçalho
C Diagnostics Library assert.h
Character handling functions ctype.h
C Errors errno.h
Characteristics of floating-point types float.h
Sizes of integral types limits.h
C localization library locale.h
C numerics library math.h
Non local jumps setjmp.h
C library to handle signals signal.h
Variable arguments handling stdarg.h
C Standard definitions stddef.h
C library to perform Input/Output operations stdio.h
C Standard General Utilities Library stdlib.h
C Strings string.h
C Time Library time.h

assert.h assert Evaluate assertion (macro)


type.h isalnum Check if character is alphanumeric (function)
isalpha Check if character is alphabetic (function)
iscntrl Check if character is a control character (function)
isdigit Check if character is decimal digit (function)
isgraph Check if character has graphical representation
using locale (function template)
islower Check if character is lowercase letter (function)
isprint Check if character is printable (function)
ispunct Check if character is a punctuation character (function)
isspace Check if character is a white-space (function)
isupper Check if character is uppercase letter (function)
isxdigit Check if character is hexadecimal digit (function)
tolower Convert uppercase letter to lowercase (function)
toupper Convert lowercase letter to uppercase (function)
errno.h errno Last error number (macro)
float.h only defined values
limits.h only constants
locale.h setlocale Set or retrieve locale (function)
localeconv Get locale formatting parameters for quantities (function)
lconv Formatting info for numeric values (type)
5.3. RELAÇÃO DAS FUNÇÕES INTRÍNSECAS 69

math.h cos Compute cosine (function)


sin Compute sine (function)
tan Compute tangent (function)
acos Compute arc cosine (function)
asin Compute arc sine (function)
atan Compute arc tangent (function)
atan2 Compute arc tangent with two parameters (function)
cosh Compute hyperbolic cosine (function)
sinh Compute hyperbolic sine (function)
tanh Compute hyperbolic tangent (function)
exp Compute exponential function (function)
frexp Get significand and exponent (function)
ldexp Generate number from significand and exponent (function)
log Compute natural logarithm (function)
log10 Compute common logarithm (function)
modf Break into fractional and integral parts (function)
pow Raise to power (function)
sqrt Compute square root (function)
ceil Round up value (function)
fabs Compute absolute value (function)
floor Round down value (function)
fmod Compute remainder of division (function)
setjmp.h longjmp Long jump (function)
setjmp Save calling environment for long jump (macro)
jmp buf Type to hold information to restore calling environment (type)
signal.h signal Set function to handle signal (function)
raise Generates a signal (function)
sig atomic t Integral type (type)
stdarg.h va list Type to hold information about variable arguments (type)
va start Initialize a variable argument list (macro)
va arg Retrieve next argument (macro)
va end End using variable argument list (macro)
stddef.h ptrdiff t Result of pointer subtraction (type)
size t Unsigned integral type (type)
offsetof Return member offset (macro)
NULL Null pointer (macro)
70 CAPÍTULO 5. FUNÇÕES

stdio.h clearerr Clear error indicators (function)


EOF End-of-File (constant)
FILE Object containing information to control a stream (type)
FILENAME MAX Maximum length of file names (constant)
fclose Close file (function)
feof Check End-of-File indicator (function)
ferror Check error indicator (function)
fflush Flush stream (function)
fgetc Get character from stream (function)
fgetpos Get current position in stream (function)
fgets Get string from stream (function)
fopen Open file (function)
fpos t Object containing information to specify a position
within a file (type)
fprintf Write formatted output to stream (function)
fputc Write character to stream (function)
fputs Write string to stream (function)
fread Read block of data from stream (function)
freopen Reopen stream with different file or mode (function)
fscanf Read formatted data from stream (function)
fseek Reposition stream position indicator (function)
fsetpos Set position indicator of stream (function)
ftell Get current position in stream (function)
fwrite Write block of data to stream (function)
getc Get character from stream (function)
getchar Get character from stdin (function)
gets Get string from stdin (function)
NULL Null pointer (constant)
perror Print error message (function)
printf Print formatted data to stdout (function)
putc Write character to stream (function)
putchar Write character to stdout (function)
puts Write string to stdout (function)
remove Remove file (function)
rename Rename file (function)
rewind Set position indicator to the beginning (function)
scanf Read formatted data from stdin (function)
setbuf Set stream buffer (function)
setvbuf Change stream buffering (function)
size t Unsigned integral type (type)
sprintf Write formatted data to string (function)
sscanf Read formatted data from string (function)
5.3. RELAÇÃO DAS FUNÇÕES INTRÍNSECAS 71

TMP MAX Number of temporary files (constant)


tmpfile Open a temporary file (function)
tmpnam Generate temporary filename (function)
ungetc Unget character from stream (function)
vfprintf Write formatted variable argument list to stream (function)
vprintf Print formatted variable argument list to stdout (function)
vsprintf Print formatted variable argument list to string (function)
72 CAPÍTULO 5. FUNÇÕES

stdlib.h atof Convert string to double (function)


atoi Convert string to integer (function)
atol Convert string to long integer (function)
strtod Convert string to double (function)
strtol Convert string to long integer (function)
strtoul Convert string to unsigned long integer (function)
rand Generate random number (function)
srand Initialize random number generator (functions)
calloc Allocate space for array in memory (function)
free Deallocate space in memory (function)
malloc Allocate memory block (function)
realloc Reallocate memory block (function)
abort Abort current process (function)
atexit Set function to be executed on exit (function)
exit Terminate calling process (function)
getenv Get environment string (function)
system Execute system command (function)
bsearch Binary search in array (function)
qsort Sort elements of array (function)
abs Absolute value (function)
div Integral division (function)
labs Absolute value (function)
ldiv Integral division (function)
mblen Get length of multibyte character (function)
mbtowc Convert multibyte character to wide character (function)
wctomb Convert wide character to multibyte character (function)
mbstowcs Convert multibyte string to wide-character string (function)
wcstombs Convert wide-character string to multibyte string (function)
EXIT FAILURE Failure termination code (macro)
EXIT SUCCESS Success termination code (macro)
MB CUR MAX Maximum size of multibyte characters (macro)
NULL Null pointer (macro)
RAND MAX Maximum value returned by rand (macro)
div t Structure returned by div (type)
ldiv t Structure returned by div and ldiv (type)
size t Unsigned integral type (type)
5.3. RELAÇÃO DAS FUNÇÕES INTRÍNSECAS 73

string.h memchr Locate character in block of memory (function)


memcmp Compare two blocks of memory (function)
memcpy Copy block of memory (function)
memmove Move block of memory (function)
memset Fill block of memory (function)
strcat Concatenate strings (function)
strchr Locate first occurrence of character in string (function)
strcmp Compare two strings (function)
strcoll Compare two strings using locale (function)
strcpy Copy string (function)
strcspn Get span until character in string (function)
strerror Get pointer to error message string (function)
strlen Get string length (function)
strncpy Copy characters from string (function)
strncat Append characters from string (function)
strncmp Compare characters of two strings (function)
strpbrk Locate character in string (function)
strrchr Locate last occurrence of character in string (function)
strspn Get span of character set in string (function)
strstr Locate substring (function)
strtok Split string into tokens (function)
strxfrm Transform string using locale (function)
NULL Null pointer (macro)
size t Unsigned integral type (type)
time.h asctime Convert tm structure to string (function)
clock Clock program (function)
clock t Clock type (type)
CLOCKS PER SEC Clock ticks per second (macro)
ctime Convert time t value to string (function)
difftime Return difference between two times (function)
gmtime Convert time t to tm as UTC time (function)
localtime Convert time t to tm as local time (function)
mktime Convert tm structure to time t (function)
NULL Null pointer (macro)
size t Unsigned integral type (type)
strftime Format time to string (function)
struct tm Time structure (type)
time Get current time (function)
time t Time type (type)
74 CAPÍTULO 5. FUNÇÕES

5.4 Funções de Entrada e Saı́da Padrões


Quando o programador deseja imprimir uma saı́da formatada no Fortran,
ele deve usar caracteres de formatação: para imprimir o conteúdo de variáveis
character, usa-se “(nAx)”; para imprimir as variáveis inteiras, “(nIx.w)”; para
imprimir variáveis reais, “(nFx.w)”; e para imprimir variáveis lógicas, “(nL)”.
O significado dos n, x e w depende do tipo de variável que se deseja imprimir.
Em C, é a mesma coisa. Para imprimir o conteúdo de uma variável, é
necessário usar um carácter de formatação para indicar o tipo de dado. A lista
abaixo relaciona os tipos de variáveis e seus caracteres de formatação. Estes
caracteres serão usados nas funções de entrada e saı́da (tela, teclado, arquivo,
impressora, etc.).

tipo de código de
variável formatação
unsigned char %ud
char %c, %d
short int %d
int %d
long int %ld
unsigned short int %ud
unsigned int %ud
unsigned long int %uld
float %f
double %lf
long double %lf

Repare que o tipo char tem dois códigos de formatação: ‘%c’ e ‘%d’. ‘%c’ é
usado quando a variável char possui uma constante literal e ‘%d’, quando o
conteúdo for um número inteiro de 8 bits.
Diretivas de Compilação
6
As diretivas de compilação, como já foi dito, são comandos para o compi-
lador e não compõem o conjunto de instruções de comando de declaração e de
execução. Mas são muito úteis quando o código-fonte precisa se adaptar às
condições do sistema operacional ou do próprio computador. De uma forma
geral, as diretivas auxiliam na portabilidade dos códigos-fonte.

6.1 Diretiva #include


O primeiro conjunto de diretivas, #include, se refere à inclusão de arqui-
vos de cabeçalho da linguagem. Os arquivos de cabeçalho são usados como
apoio à organização da programação. O conteúdo mais comum dos arquivos
de cabeçalho são os protótipos das funções intrı́nsecas da linguagem que se
organizam, mais ou menos, de acordo com o propósito de cada uma.
Então, os protótipos das funções intrı́nsecas relacionadas com entrada e
saı́da de dados, sejam pelo teclado, arquivo de dados, tela, rede, impressora,
estão agrupadas num arquivo de cabeçalho chamado stdio.h. Os protótipos das
funções de manipulação de cadeia de caracteres (as strings) estão no arquivo
string.h. Os protótipos das funções de manipulação de memória e de controle
de tempo estão, respectivamente, nos arquivos mem.h e time.h. Os protótipos
das funções matemáticas estão em math.h. Alguns protótipos de funções muito
utilizadas estão repetidas no arquivo de cabeçalho stdlib.h.
Não há problema em se repetir protótipos de funções em vários arquivos
de cabeçalho, mas se deve tomar cuidado. O compilador, ao se deparar com
múltiplos protótipos de uma mesma função, gera uma série de avisos alertando
para a multiplicidade de declarações. Para evitar isso, os avisos, pode-se lançar
mão de outras diretivas, o #define, o #ifdef e o #ifndef , que serão explicados
nas próximas seções.
Mas antes de avançarmos da outras diretivas, vale chamar a atenção para
a sintaxe da diretiva #include. Quando o arquivo de cabeçalho é do sistema,

75
76 CAPÍTULO 6. DIRETIVAS DE COMPILAÇÃO

isto é, está pré-instalado e não foi confeccionado pelo programador, a diretiva
#include usa os sı́mbolos ‘<’ e ‘>’ envolvendo o nome do arquivo de cabeçalho.
Você já deve ter visto isso em vários exemplos:
exemplo3b.c
# include < stdio .h >

void main ( void ) {


char ch = ’a ’;
char letra = ’+ ’;

printf ( " % c % c \ n " ,ch , letra );


}

O compilador, ao processar o código e detectar a diretiva #include procura


os sı́mbolos que envolvem o nome do arquivo de cabeçalho. Encontrando os
sı́mbolos ‘<’ e ‘>’, ele saberá que este arquivo deve estar na estrutura de
instalação da linguagem, isto é, faz parte do sistema.
E quando o programador gera suas próprias funções? É permitido organizar
os seus protótipos em arquivos de cabeçalho próprios? A resposta é sim.
Não é recomendado ao programador modificar os arquivos de cabeçalho do
sistema. Logo, resta ao programador criar seus próprios arquivos de cabeçalho.
Normalmente, os arquivos de cabeçalho particulares ficam no mesmo diretório
dos códigos-fonte que estão sendo criados. Neste caso, para indicar ao compila-
dor que ele deve usar estes arquivos de cabeçalho que estão no mesmo diretório
dos códigos-fonte, a diretiva #include usa aspas. Por exemplo, considere que o
programador criou um arquivo de cabeçalho chamado “minhas funcoes” e que
este arquivo esteja no mesmo diretório dos códigos-fonte. Para incluı́-lo nos
arquivos de código, o programador deve usar a diretiva #include da seguinte
forma:

#include "minhas_funcoes.h"

E se os arquivos de cabeçalho do programador estão em um diretório especı́fico,


ele pode combinar o caminho (path), relativo ou absoluto, com o nome do
arquivo de cabeçalho. Digamos que o arquivo de cabeçalho “minhas funcoes”
esteja no diretório “/home/usuario/programa-cao”. O #include ficará

#include "/home/usuario/programador/minhas_funcoes.h"

No caso dos arquivos de sistema, o próprio sistema sabe onde eles estão ar-
mazenados. Então o compilador pode usar esta informação e não exige do
programador a inclusão do caminho dos arquivos de sistema. Mas os arquivos
do programador não estão em locais pré-definidos e nem faz sentido que es-
tejam. Então, o compilador usa o caminho associado ao nome do arquivo de
6.2. DIRETIVAS #DEFINE|#UNDEF 77

cabeçalho do programador para buscá-lo corretamente. E se não tiver caminho


associado ao nome do arquivo de cabeçalho, o compilador procura no mesmo
diretório onde estão os códigos-fonte.

6.2 Diretivas #define|#undef


A diretiva #define é usada para criar “macros” que são trechos de código
referenciados por um identificador. As macros parecem funções, podendo rece-
ber argumentos, mas não geram código compilado e os argumentos funcionam
somente no sentido “de fora da macro para dentro da macro”. As macros fun-
cionam na prática como carimbos; onde o compilador encontrar o identificador
de uma macro, ele substituirá pelo conjunto de instruções que a definem.
Quando o compilador encontra uma chamada de função, ele verifica o en-
dereço de entrada da função e desvia a execução do programa para lá. O
programa percorre o corpo da função e, ao deixá-la, retorna para a instrução
seguinte ao endereço de desvio. Quantas vezes o programa tiver chamadas da
mesma função, tantas vezes o programa desviará para o ponto de entrada da
função e retornará para a instrução seguinte ao desvio.
Já na macro, não. A macro não ocupa um endereço de memória; ela só
existe durante o processo de compilação e o compilador substitui a referência
da macro pelo conjunto de instruções que a definem. Vamos a um exemplo.
Imagine implementar um código que gera o quadrado e um número. Se for
feita uma função para isso, ela seria, provavelmente, assim:

double quadrado(double a) {
return a*a;
}

Independente do tipo de dado de entrada, passado como argumento, a função


double quadrado(double a) sempre retornará um dado do tipo double.
Uma macro que realize o mesmo cálculo poderia ser assim:

#define quadrado(a) a*a

Quando o compilador encontrar o identificador quadrado, ele substituirá pelo


corpo da macro atualizando o conteúdo do rótulo “a”. O rótulo da macro
funciona como se fosse uma variável. Por exemplo:
Código 6.1: exemplo26.c
# include < stdio .h >
# define quadrado ( a ) a * a

void main ( void ) {


int x = 10 , y ;
78 CAPÍTULO 6. DIRETIVAS DE COMPILAÇÃO

y = quadrado ( x );

printf ( " O quadrado de % d eh % d \ n " ,x , y );


}

Comparando a definição da macro quadrado(a) e sua ocorrência na instrução


“y = quadrado(x);”, percebe-se que a variável “x” coincide com o rótulo
“a”. O compilador, ao tratar esta linha de instrução, irá, primeiro, localizar a
definição da macro (já que não existe nenhuma função definida com esse nome).
Depois, irá substituir a identificação da macro por sua definição, atualizando
os rótulos, ou seja, no lugar de “quadrado(x)”, o compilador colocará “x*x”
e a instrução completa será “y = x*x;”. É essa instrução que será compilada
e gerará código de máquina.
Duas observações: primeiro, as macros não definem tipo de variável; logo,
elas podem ser usadas com qualquer tipo de argumento. O resultado dependerá
somente das conversões de tipo envolvidas no corpo da macro; segundo, como
a macro é substituı́da por sua codificação, alguns cuidados devem ser tomados
na hora de definir a macro. Por exemplo:
Código 6.2: exemplo27.c
# include < stdio .h >
# define quadrado ( a ) a * a

void main ( void ) {


int x = 10 , y ;

y = quadrado ( x +10);

printf ( " O quadrado de % d eh % d \ n " ,x , y );


}

Se esperaria, neste código é que a variável “y” recebesse o quadrado de “x+10”,


ou seja, 400, mas não é isso que vai acontecer. Se a macro funciona como
um carimbo, então onde tem “a” na macro deve ser substituı́do por “x+10”
e a instrução ficará “y = x+10*x+10;”. O resultado disso é 120 porque a
multiplicação tem precedência sobre a adição.
O que falta para que a macro funcione corretamente? Parênteses. Se a
macro for escrita da seguinte forma:

#define quadrado(a) ((a)*(a))


6.3. DIRETIVAS #IF–#ELIF–#ELSE–#ENDIF 79

o resultado será correto, pois a instrução será (depois da substituição da macro)


“y = ((x+10)*(x+10));”.
Outra aplicação de macros é a legibilidade do código-fonte. Um exemplo
muito simples é a definição de rótulos. Os rótulos são apelidos para constan-
tes definidas no código-fonte. Pode-se usar o #define para criar os rótulos
“FALSO” e “VERDADEIRO” para as constantes 0 e 1, respectivamente. As-
sim, numa lógica onde falso e verdadeiro são 0 e 1, usar os rótulos acima facili-
taria a leitura do código. As declarações do #define para os rótulos “FALSO”
e “VERDADEIRO” são
#define FALSO 0
#define VERDADEIRO 1
Com estas declarações, o compilador saberá que, ao encontrar a macro “FALSO”,
ele deverá substituir por 0 e a macro “VERDADEIRO” por 1.
As macros não precisam estar associadas à rótulos ou instruções, isto é,
nome da macro seguido de conteúdo. A linguagem C aceita a definição de
macros como simples definições de sı́mbolo. Isto será visto a seguir com a
apresentação das diretivas de condição.
Para terminar, a diretiva #undef é responsável por anular uma definição
de macro declarada com #define

6.3 Diretivas #if –#elif –#else–#endif


As diretivas condicionais são utilizadas para definir que trechos de código-
fonte serão compilados em função de testes lógicos. Estes testes ocorrem du-
rante a compilação e não se tornam instruções executáveis. Por exemplo:
Código 6.3: exemplo28.c
# include < stdio .h >
# if 1
# define quadrado ( a ) a * a
# else
double quadrado ( double a ) {
return a * a ;
}
# endif

void main ( void ) {


int x = 10 , y ;

y = quadrado ( x +10);

printf ( " O quadrado de % d eh % d \ n " ,x , y );


}
80 CAPÍTULO 6. DIRETIVAS DE COMPILAÇÃO

Neste exemplo, a diretiva #if está sendo usada para definir que bloco de
código será compilado. Como qualquer coisa diferente de 0 é considerado ver-
dadeiro, a declaração “#if 1” retorna verdadeiro para o compilador e ele
usará o trecho de código entre o #if e o #else no processo de compilação
que é o comando “#define quadrado(a) a*a”, ou seja, o compilador usará a
definição da macro quadrado(a). Se o programador trocar a constante 1 para
0, a declaração “#if 0” retornará falso e o trecho de código compilado será a
da função “double quadrado(double a) { ... }”. Em termos de legibili-
dade, o código-fonte não foi afetado pelo teste da diretiva #if . Mas em termos
de performance, usar macros gera programas mas rápidos porque a chamada
de uma função envolve várias instruções em código de máquina. É claro que
neste exemplo não é possı́vel avaliar-se esta diferença de desempenho, pois o
código é muito simples. Mas no caso de um laço com muitas iterações, o uso
de funções ou macros pode fazer diferença na avaliação final de desempenho.
Mas toda vantagem cobra um preço. Neste caso, o aumento de desempenho é
acompanhado do aumento do tamanho do código executável. O uso de funções
gera programas mais enxutos. O uso de macros gera programas mais rápidos.
Use isso com sabedoria!
O programador pode aninhar e encadear diretivas condicionais tal qual as
instruções if ( ) { } else { }. A diretiva #elif é a combinação do #else com o
#if .
A diretiva #endif encerra a atuação das diretivas condicionais.

6.4 Diretivas #ifdef |#ifndef –#endif


Se você é curioso, certamente você já abriu um arquivo de cabeçalho para
ver o que tem dentro dele. De cara, você viu vários testes condicionais avali-
ando o ambiente de programação.
Quando um arquivo de cabeçalho é criado, é comum definir-se um sı́mbolo
que o identifique. O sı́mbolo pode ser qualquer string válida em C. De praxe,
o sı́mbolo usado é o nome do arquivo de cabeçalho em letras maiúsculas com
o carácter sublinha (“ ”) no lugar do ponto da extensão. Por exemplo, se o
arquivo de cabeçalho se chama “MeuCabecalho.h”, o candidato para sı́mbolo
seria “#define MEUCABECALHO H ”.
Como foi apresentado nas seções sobre funções e diretiva #include, o ar-
quivo de cabeçalho contém protótipos de funções, estruturas de dados, de-
finições de tipo com typedef ’s, definições de macros e algumas coisas mais. É
comum estruturar-se a programação em vários arquivos, tanto de código-fonte
como de cabeçalho, segundo uma lógica definida pelo programador. É provável
também que um ou mais arquivos de cabeçalho contenham definições básicas
6.4. DIRETIVAS #IFDEF|#IFNDEF–#ENDIF 81

que outros arquivos de cabeçalho e de código-fonte utilizam. Neste cenário,


um problema corriqueiro é a duplicidade de definições de macros, protótipos e
outras informações, principalmente no caso de arquivos de cabeçalho incluı́rem
outros arquivos de cabeçalho através da diretiva #include.
Para clarear a situação, imagine que três arquivos de cabeçalho chamados
“base.h”, “modulo.h” e “fase.h” tenham os seguintes trechos de código:
Código 6.4: base.h
# define quadrado ( a ) (( a )*( a ))

typedef struct {
double x , y ;
} upla ;

Código 6.5: modulo.h


# include " base . h "
/* prototipo da funcao modulo () */
double modulo ( upla a );

Código 6.6: fase.h


# include " base . h "
/* prototipo da funcao fase () */
double fase ( upla a );

e que “modulo.h” e “fase.h” sejam chamados pelo código-fonte “exemploX.c”:


Código 6.7: exemplo29.c
# include < stdio .h >
# include < math .h >
# include " base . h " /* por causa do tipo upla
e da macro quadrado ( a ) */
# include " modulo . h " /* por causa do prototipo */
# include " fase . h " /* por causa do prototipo */

double modulo ( upla a ) {


return sqrt ( quadrado ( a . x )+ quadrado ( a . y ));
}

double fase ( upla a ) {


return atan2 ( a .y , a . x );
}

int main ( void ) {


82 CAPÍTULO 6. DIRETIVAS DE COMPILAÇÃO

upla c ;

c . x = 1;
c . y = 1;

printf ( " modulo = % lf \ tfase = % lf \ n " ,


modulo ( c ) , fase ( c ));

return 0;
}

O compilador, ao processar o arquivo de código-fonte “exemploX.c”, começará


detectando e incluindo o arquivo de cabeçalho “math.h”. Depois, incluirá
“base.h” que define o tipo “upla”. A próxima inclusão é o arquivo de cabeçalho
“modulo.h” que contém, também, a inclusão do arquivo de cabeçalho “base.h”,
que já foi processado pelo compilador. Quando o compilador abrir pela segunda
vez o arquivo “base.h”, ele encontrará a declaração do tipo “upla”, que já foi
definido na primeira leitura do arquivo “base.h”. Como medida defensiva, o
compilador gera um erro e aborta o processo de compilação.
Para evitar duplicidades de declarações durante o processo de compilação,
cria-se sı́mbolos para cada arquivo de cabeçalho: no arquivo “base.h”, cria-
se o sı́mbolo “#define BASE H”; no arquivo “modulo.h”, o sı́mbolo “#define
MODULO H”; e no arquivo “fase.h”, o sı́mbolo “#define FASE H”. Além disso,
introduz-se as diretivas #ifndef para controlar a o reprocessamento dos con-
teúdos dos arquivos. Os arquivos ficam da seguinte forma:
Código 6.8: base.h
# ifndef BASE_H
# define BASE_H

# define quadrado ( a ) (( a )*( a ))

typedef struct {
double x , y ;
} upla ;

# endif

Código 6.9: modulo.h


# ifndef MODULO_H
# define MODULO_H

# include " base . h " /* por causa de " upla " */


6.4. DIRETIVAS #IFDEF|#IFNDEF–#ENDIF 83

double modulo ( upla a ); /* prototipo da funcao modulo () */

# endif

Código 6.10: fase.h


# ifndef FASE_H
# define FASE_H

# include " base . h " /* por causa de upla */

double fase ( upla a ); /* prototipo da funcao fase () */

# endif

Então, quando “exemploX.c” é compilado, o compilador carrega o conteúdo


do arquivo “base.h”. A primeira pergunta que o compilador faz é: “O sı́mbolo
BASE H já foi definido? ”. Como é a primeira vez que o arquivo está sendo
acessado, a resposta é não. Se o sı́mbolo “BASE H ” não está definido, então
o compilador entra no bloco e cria o sı́mbolo “BASE H ” através do comando
“#define BASE H”.
Ao terminar o processamento de “base.h”, o compilador passa para o
próximo #include (“#include "modulo.h"”). Ao abrir o arquivo “modulo.h”,
o compilador encontra o #include do arquivo “base.h”. O compilador, então,
carrega o arquivo “base.h” e faz novamente a pergunta: “O sı́mbolo BASE H
já foi definido? ”. Nesta caso, já. Então, a diretiva #ifndef falha e o conteúdo
de “base.h” não é mais carregado, pois já está na memória do computador.
O uso do #ifndef é muito prático, pois, se a ordem do arquivos de cabeçalho
forem alterados, o conteúdo de “base.h” também não será recarregado. Uma
vez lido, as próximas solicitações não são mais atendidas.
84 CAPÍTULO 6. DIRETIVAS DE COMPILAÇÃO
7
Organização da Programação em C

Algum texto

85
86 CAPÍTULO 7. ORGANIZAÇÃO DA PROGRAMAÇÃO EM C
Classes e Objetos
8
A linguagem C segue o modelo (paradigma) da programação estruturada.
Ela se caracteriza principalmente na organização do código fonte na forma
de funções. Por isso ela é classificada como uma linguagem procedural (que
usam funções e procedimentos). A organização baseada em funções e proce-
dimentos permite que o usuário reutilize seus recursos já programados sempre
que precisar, bastando, para isso, chamar a função ou sub-rotina necessária.
Na programação estruturada, a solução dos problemas é obtida através da
execução da sequência de instruções implementadas no código. Portanto, as
linguagens estruturadas são ditas serem orientadas a fluxo de execução.
Mas nem todos os problemas são modelados facilmente através do mo-
delo de programação estruturada. A natureza de alguns problemas exige uma
abordagem diferente. Um exemplo simples está na aplicação da computação
para reprodução de sistemas, ou seja, simulação. Neste tipo de problema,
várias partes do sistema real estão operando simultaneamente e cada parte do
sistema possui caracterı́sticas próprias. A colaboração entre as partes é que
“materializam” a solução do problema.
Imagine uma célula. Cada estrutura da célula faz uma coisa diferente, mas
tire uma delas para você ver o que acontece... Então, cada parte, cada elemento
é essencial para o funcionamento do todo, mas a parte, o elemento, não é o
todo. O importante neste paradigma é a colaboração entre as partes, cada um
fazendo o que sabe e colaborando com as demais. E a colaboração se dá através
de trocas de mensagens. No caso da célula, as mensagens são as substâncias
quı́micas liberadas por cada estrutura. Essas substâncias provocam alterações
no funcionamento das outras estruturas produzindo ações diferentes.
Se fôssemos simular uma célula, partirı́amos do projeto dos elementos cons-
tituintes dela, pois cada uma possui caracterı́sticas e habilidades particulares.
Podemos olhar para cada elemento projetado como um objeto, algo que possui
propriedades e capacidades. Depois, colocarı́amos estes objetos juntos numa
caixa e a “ligarı́amos”. Imaginando que cada objeto começasse a interagir com

87
88 CAPÍTULO 8. CLASSES E OBJETOS

as demais, a nossa célula simulada começaria a funcionar.


A moral que espero que seja observado aqui, neste exemplo, é que, se os
objetos forem programados e se eles cooperarem entre si, o nosso problema
está resolvido. Esta é a essência do paradigma da programação orientada
a objeto. Agora, entre a abstração e a especificação, a distância é grande.
Partindo-se da linguagem estruturada C, propôs-se, no final da década de 70
e inı́cio dos anos 80, uma complementação das capacidades da linguagem de
forma que ela suportasse o modelo de programação orientada a objeto. Esta
complementação, que no jargão da ciência da computação se chama superset,
deu origem à linguagem de programação C++ que, na verdade, é a mesma
linguagem estruturada C acrescida de novas capacidades. Por isso, tem muita
gente que usa o compilador de C++ e acha que está fazendo programação
orientada a objeto, mas está na verdade programando de forma estruturada,
usando somente os recursos da linguagem C. Não é pelo fato de usar instruções
próprias do C++ que transforma a programação em orientada a objeto. É
necessário que a lógica, o projeto, sejam baseados no modelo de programação
orientado a objeto. Então, não se iludam. Querem programar orientado a
objeto? Aprendam primeiro o que é programação orientada a objeto.
Como este material não tem a intenção de ensinar o que é programação
orientada a objeto, mas, simplesmente, introduzir os elementos e os recursos
de programação da linguagem C++, fica o compromisso de cada leitor de
correr atrás do conhecimento necessário para aprender a programar usando
orientação a objeto. O que será apresentado aqui é a conceituação de classes
e objetos, suas propriedades e a sintaxe da linguagem C++ (que é tudo que
já foi visto até aqui mais um pouquinho).
Então, vamos lá.

8.1 Encapsulamento de Atributos e Métodos


As classes são modelos de objetos. Funcionam como projetos de algo que
se materializará em um futuro próximo, os objetos propriamente ditos. Em
uma classe, podem existir atributos e métodos, onde os atributos são as ca-
racterı́sticas estáticas que diferenciarão um objeto de outro e os métodos são
as habilidades que cada objeto poderá exercer.
Um exemplo simples: quando pensamos num veı́culo, imaginamos um ob-
jeto com portas, rodas, motor, cor, marca, categoria, etc. Todo veı́culo tem
estas coisas, estes atributos, mas, ainda assim, somos capazes de distinguir
os veı́culos. Uns tem duas portas, outros quatro. Uns são vermelhos, outros
pretos. Tem veı́culo de passeio e tem veı́culo de carga e transporte. Quando
atribuı́mos valores, conteúdos, aos atributos de uma classe, geramos um objeto
desta classe. Todo objeto é particular e toda classe pode gerar vários objetos.
Um Pegeout 206 e um Renout Clio são da classe veı́culo e são objetos distintos.
8.2. VISIBILIDADE DE IMPLEMENTAÇÃO 89

A outra caracterı́stica das classes são os métodos. Como foi dito, método
é toda habilidade que a classe possui. É sua capacidade de fazer coisas. A
implementação dos métodos de uma classe é idêntica à implementação de uma
função. Um método pode receber dados e devolver dados. Estes dados que
“vão e vem” através dos métodos são as mensagens. É através destas trocas
de mensagens que um programa orientado a objeto funciona.
Em C++, uma classe é implementada da mesma forma que uma estrutura
do tipo struct. Ela possui campos (os atributos da classe) e incorpora funções
(os métodos da classe), tudo na mesma estrutura. Esta propriedade se chama
encapsulamento. A declaração de uma classe usa a palavra reservada class:

class Nome_da_Classe {
/* declaraç~
ao dos atributos */
char dado1;
unsigned dado2;
struct estrutura1 {
} dado3;

/* declaraç~
ao dos métodos */
void medodo1(void);
unsigned metodo2(void);
void metodo3(float arg1);
};

Os atributos são dado1, dado2 e dado3 dos tipos char , unsigned e struct es-
trutura1, respectivamente, e os métodos são metodo1(void), metodo2(void)
e metodo3(float arg1).
Os tipos intrı́nsecos são classes.

8.2 Visibilidade de Implementação


As classes, enquanto modelo de objetos, podem ter partes “visı́veis” e “in-
visı́veis” a outras classes. Isto cria um mecanismo de “reserva” de dados.
Significa que nem tudo que está implementado em uma classe pode ser “vista
de fora” dela. Isso se chama visibilidade de implementação. Na prática,
a classe pode declarar atributos e/ou métodos como private ou public organi-
zados na forma de seções:

class Nome_da_Classe {
private:
/* declaraç~
ao dos atributos privados */
/* declaraç~
ao dos métodos privados */
90 CAPÍTULO 8. CLASSES E OBJETOS

public:
/* declaraç~
ao dos atributos públicos */
/* declaraç~
ao dos métodos públicos */
};

Como efeito prático, os atributos e métodos declarados como privados fun-


cionam como se fossem variáveis e métodos locais que só existem no escopo da
classe que as define. Vamos ao exemplo:

class ContaBancaria {
private:
unsigned senha;
char autenticado;

char AutenticarSenha(unsigned digitada);

public:
void CadastrarSenha(unsigned minha_senha);
unsigned SolicitarSenha(void);
char AcessarConta(long conta);
void VerificarSaldo(void);
};

A classe ContaBancaria declara dois atributos e um método como privados


e três métodos como públicos. O atributo senha deve armazenar a senha
cadastrada pelo usuário. Logo, ela não deve ser pública, por questões de
segurança. O atributo autenticado serve para armazenar o status do usuário
da conta durante o acesso. O método AutenticarSenha(void) também é
privada e, portanto, é somente acessada de dentro da classe, ou seja, o usuário
não tem acesso ao método, também por questões de segurança. Poderı́amos
imaginar o seguinte procedimento de acesso à conta bancária:

1. o usuário solicita acesso a sua conta (método AcessarConta(long con-


ta)) fornecendo o número de sua conta bancária;
2. na implementação do método AcessarConta(long conta) (que não está
aqui), imaginemos que método verifique se o usuário já tem senha ca-
dastrada ou não;
3. se não tem, o método pede para que o usuário cadastre sua senha;
4. o usuário, então, acessa o método CadastrarSenha(unsigned minha se-
nha) informando a senha desejada;
5. o método armazena a senha fornecida no atributo senha e o acesso se
encerra;
8.3. HIERARQUIA DE CLASSES E HEREDITARIEDADE 91

6. se o usuário já tem senha cadastrada, o método AcessarConta(long


conta) chama o método SolicitarSenha(void) que disponibiliza al-
guma forma de entrada de dados para que a senha digitada permaneça
inacessı́vel a outros;

7. então, o método SolicitarSenha(void) chama o método Autenticar-


Senha(unsigned digitada) que compara a senha digitada com a senha
cadastrada;

8. se a autenticação for positiva, o atributo autenticado é atualizado para


“verdadeiro” e o usuário é liberado para verificar seu saldo, por exemplo,
através do método VerificarSaldo(void).

9. se a autenticação falhar, o atributo autenticado recebe “falso” e o


usuário não tem acesso a mais nada.

Este é um exemplo bastante simplificado, mas serve para se ter uma noção
de como usar as propriedades de atributos e métodos privados e públicos. Dá
para fazer muito melhor que isso, mas não é o caso neste momento. Não faz
sentido, por exemplo, o usuário ter acesso a senha cadastrada. Este papel é
do sistema de acesso. Portanto, a senha deve ser privada. O método de au-
tenticação também não deve ser público. Quem deve acessá-lo é o método de
acesso a conta. Daria para imaginar, inclusive, um método para criptografia
dos dados, dificultando ainda mais o acesso aos dados. Este método de cripto-
grafia deveria ser privado para que a chave criptográfica não seja descoberta.
A execução deste procedimento é realizado por um objeto da classe Con-
taBancaria e não pela classe em si. Lembrando, a classe é um modelo. O
objeto da classe é que executa efetivamente as tarefas. Mas, imaginando o que
o objeto deveria fazer, é possı́vel se projetar a classe. Normalmente, esse é o
processo de design de uma programação orientada a objeto: faz-se um estudo
de caso, registra-se os requisitos, anota-se os procedimentos e projeta-se as
classes. Este processo deve ser realimentado, permitindo uma otimização da
programação.

8.3 Hierarquia de Classes e Hereditariedade


O paradigma da programação orientada a objeto reserva alguns mecanis-
mos incomuns para quem está acostumado com a programação estruturada.
Um desses mecanismos é a hereditariedade das classes. Assim como numa
famı́lia, onde filhos herdam de seus pais caracterı́sticas particulares como es-
trutura corporal, personalidade e manias, as classes são concebidas com a ca-
pacidade de herdar atributos e métodos de classes ancestrais. Esta estrutura
“genealógica” corresponde a uma hierarquia de classes, onde classes primitivas
estão próximas da abstração e as classes filiais são mais especializadas.
92 CAPÍTULO 8. CLASSES E OBJETOS

A capacidade de herança é muito prática no desenvolvimento da pro-


gramação orientada a objeto, pois evita que atributos e métodos sejam de-
finidos de forma redundante entre classes ancestrais e descendentes. Vale,
portanto, a regra mais simples: tudo que tiver na classe mãe, a classe filha
herdará. Agora, o que for redefinido na classe filha, sobrescreverá o que for
transmitido pela classe mãe.
E, como no exemplo da classe ContaBancaria, se a classe mãe tiver métodos
e/ou atributos que nem mesmo os descendentes podem herdar? Como fa-
zer? Para isso, existe um acréscimo na propriedade de visibilidade de im-
plementação. Além do encapsulamento poder ser público e privado, existe
a possibilidade dele ser protegido (protected ). Assim, o que for público, é
visı́vel por todos os elementos da programação; o que for privado é reservado
para a classe que contém o código; o que for protegido fica oculto a todos
os elementos de programação menos para a classe que define o código e seus
descendentes.
Vamos aos exemplos.
class ClasseBase {
private:
unsigned dado1;
void metodo1(void);

protected:
float dado2;
int metodo2(char a) {
metodo1();
dado2 += a;
return dado2;
}
};

ClasseBase::metodo1(void) {
dado1 = (unsigned)dado2;
}

class ClasseDerivada : ClasseBase {


public:
int dado3;
double metodo3(double a, double b) {
dado2 = (float)a;
dado3 = metodo2((char)b);
return dado2+dado3;
}
};
8.4. POLIMORFISMO DE MÉTODOS 93

A classe ClasseBase declara como privados o atributo dado1 e o método


metodo1(void). Portanto, somente objetos da classe ClasseBase podem
acessá-los, além de não serem visı́veis ao restante da programação. Já o atri-
buto dado2 e o método metodo2(char a) são declarados como protegidos.
Logo, eles são acessados tanto pelos objetos da classe ClasseBase quanto pe-
los seus descendentes, neste caso, ClasseDerivada. Mas dado2 e metodo2
continuam inacessı́veis ao restante do código, podendo ser acessados somente
dentro das classes já mencionadas.
A declaração da classe ClasseDerivada encapsula os atributos públicos
(dado3 e metodo3(double a, double b)) e herda de ClasseBase o atributo
dado2 e o método metodo2(char a). A sintaxe para declaração de des-
cendência é: “class nome da classe descendente : nome da classe ancestral {
};”.
Para a classe descendente, é como se os atributos e métodos tivessem
sido declarados ali. O método metodo1(void), declarado como privado em
ClasseBase, não pode ser acessado diretamente pela classe ClasseDerivada.
Mas é válido o acesso ao metodo1(void) através do método metodo2(char
a) porque tanto metodo1 quanto metodo2 são declarados na mesma classe.
Vale uma observação importante aqui, os métodos tem as mesmas for-
mulações que as funções do C. A codificação dos métodos podem aparecer tanto
na própria elaboração das classes como pode vir separadamente. Neste exem-
plo, metodo2(char a) e metodo3(double a, double b) são declarados e co-
dificados dentro das classes. Esta forma de programação se chama codificação
de método inline. O método metodo1(void) está declarado fora da estru-
tura da classe ClasseBase. Esta forma se chama codificação de método
outline. Sua sintaxe é: “nome da classe :: nome do método(argumentos) {
}”.

8.4 Polimorfismo de Métodos


A propriedade de polimorfismo representa um mecanismo de programação
muito flexı́vel na orientação a objeto. Ela permite que o programador adapte
seus métodos aos argumentos que estão sendo passados. Na prática, o progra-
mador pode declarar métodos com o mesmo nome dentro da mesma classe e
diferenciá-los apenas com os argumentos. Por exemplo:

class ClasseUm {
protected:
char var1;

public:
void met1(void);
char met1(char a);
94 CAPÍTULO 8. CLASSES E OBJETOS

char met1(char a, char b);


void met1(unsigned i);
};

Os métodos void met1(void), char met1(char a), char met1(char a,


char b) e void met1(unsigned i), apesar de terem o mesmo nome “met1”,
são implementados diferentemente. O compilador identifica qual método es-
tará sendo requisitado através da identificação dos argumentos passados ao
método.

8.5 Objetos das Classes


Objeto é a instância de uma classe, isto é, uma materialização, um ele-
mento funcional. A classe é um modelo, não tem a capacidade de executar
nada. A classe modela o que será o objeto da classe, dotando-lhe de atribu-
tos e métodos. O objeto é como uma variável de estrutura de dado. Seus
atributos podem assumir valores, mas como instância de classe e classe tem
método, um objeto pode executar tarefas. Com já foi colocado anteriormente,
os métodos correspondem às habilidades, capacidades do objeto, modelado
através da classe.
Por exemplo, imagine uma classe que define um ponto em um sistema de
coordenadas retangulares:

class Ponto {
protected:
double x, y, z;

public:
void X(double ix) { x = ix; }
void Y(double iy) { y = iy; }
void Z(double iz) { z = iz; }

double X(void) { return x; }


double Y(void) { return y; }
double Z(void) { return z; }
};

Seus atributos são “double x, y, z;” e os métodos são tais que cada or-
denada pode ser manipulada individualmente, tanto para leitura como para
preenchimento.
8.6. MÉTODOS CONSTRUTORES E DESTRUIDORES 95

8.6 Métodos Construtores e Destruidores


A propriedade de encapsulamento de métodos permite a implementação
de um mecanismo de inicialização de objetos através de métodos chamados
construtores e finalização de objetos através dos destrutores. Toda vez que
uma classe é instanciada, isto é, um objeto é declarado, o método construtor da
classe é executado automaticamente da mesma forma que o método destrutor
é executado quando o objeto é destruı́do.
Um método construtor é declarado com o mesmo nome da classe e o des-
trutor também usa o nome da classe, porém se diferencia do construtor pela
presença de um til (∼) na frente do nome do método.
Lembre-se que em C, as variáveis não são inicializadas automaticamente
como no Fortran ou no Pascal. Por isso, a existência dos construtores e des-
truidores é muito importante e útil.
Uma classe pode implementar vários métodos construtores através do poli-
morfismo, sendo que cada implementação atende uma circunstância diferente
de instanciamento. Voltemos ao exemplo da classe Ponto. Um construtor po-
deria ser implementado para inicializar o objeto com as coordenadas nulas. A
implementação a seguir mostra como fazer isso:

class Ponto {
protected:
double x, y, z;

public:
/* construtor da classe Ponto */
Ponto(void) { x = y = z = 0; }

void X(double ix) { x = ix; }


void Y(double iy) { y = iy; }
void Z(double iz) { z = iz; }

double X(void) { return x; }


double Y(void) { return y; }
double Z(void) { return z; }
};

Você deve perceber que o construtor tem o mesmo nome da classe, é declarado
como um método – logo, pode ou não ter argumentos – e não tem um tipo
definido – significa que o construtor não retorna nada. No exemplo, classe
Ponto implementa um construtor sem argumentos cujo papel é simplesmente
inicializar os atributos com o valor zero. Poderı́amos criar um outro construtor
que permitisse a inicialização dos atributos com outros valores que não zero:
96 CAPÍTULO 8. CLASSES E OBJETOS

class Ponto {
protected:
double x, y, z;

public:
/* construtores da classe Ponto */
Ponto(void) { x = y = z = 0; }
Ponto(double ix, double iy, double iz) {
x = ix; y = iy; z = iz;
}

void X(double ix) { x = ix; }


void Y(double iy) { y = iy; }
void Z(double iz) { z = iz; }

double X(void) { return x; }


double Y(void) { return y; }
double Z(void) { return z; }
};

Vejamos agora um programa completo com instanciamentos da classe Ponto:


Código 8.1: exemplo30.cpp
# include < stdio .h >

class Ponto {
protected :
double x , y , z ;

public :
/* construtores da classe Ponto */
Ponto ( void ) { x = y = z = 0; }
Ponto ( double ix , double iy , double iz ) {
x = ix ; y = iy ; z = iz ;
}

void X ( double ix ) { x = ix ; }
void Y ( double iy ) { y = iy ; }
void Z ( double iz ) { z = iz ; }

double X ( void ) { return x ; }


double Y ( void ) { return y ; }
double Z ( void ) { return z ; }
};
8.6. MÉTODOS CONSTRUTORES E DESTRUIDORES 97

int main ( void ) {


/* inst^ a ncias */
Ponto p ;
Ponto q (1 , -2 ,2);

printf ( " p = (% lf ,% lf ,% lf )\ n " ,p . X () , p . Y () , p . Z ());


printf ( " q = (% lf ,% lf ,% lf )\ n " ,q . X () , q . Y () , q . Z ());

return 0;
}

Quando o objeto p é criado, o construtor Ponto(void) é executado, associando


o valor zero aos seus atributos. Já o objeto q é criado e inicializado através
do construtor Ponto(double ix, double iy, double iz). Os atributos de
q são inicializados com os valores de ix, iy e iz passados como argumento
do construtor. Essa prática simplifica a leitura e contribuı́ para a clareza do
código-fonte.
Um outro método útil na atribuição de dados aos objetos ao longo da
codificação é o uso de construtores dentro de uma instrução. Por exemplo, o
usuário poderia modificar o conteúdo do objeto q no exemplo anterior usando
a instrução:

...
q = Ponto(1,-2,2);
...

Esta instrução cria um objeto temporário, sem nome, da classe Ponto e


copia seu conteúdo para o objeto q. O operador de atribuição “=” funciona
corretamente neste caso, assim como no caso de cópia de estruturas de dados,
pois opera sobre cada atributo da classe, copiando-os um a um. Mas isto só
funciona para o processo de cópia entre objetos (e estruturas) idênticos. Então,
o corpo da função principal do código anterior poderia ser:

int main(void) {
/* inst^
ancias */
Ponto p; /* construtor Ponto(void) */
Ponto q; /* construtor Ponto(void) */

/* invocando construtor */
/* Ponto(double,double,double) */
q = Ponto(1,-2,2);

printf("p = (%lf,%lf,%lf)\n",p.X(),p.Y(),p.Z());
98 CAPÍTULO 8. CLASSES E OBJETOS

printf("q = (%lf,%lf,%lf)\n",q.X(),q.Y(),q.Z());

return 0;
}

e o efeito final seria o mesmo.


Os métodos destrutores são executados quando o programa se encerra ou
o objeto é destruı́do ao longo do programa. Uma situação muito comum é
a declaração de objetos locais dentro de funções ou métodos. As variáveis e
objetos locais só existem enquanto a função ou o método estão sendo execu-
tados. Quando estes terminam, as variáveis e objetos locais são destruı́dos.
Neste momento, se a classe dos objetos implementa um destrutor, este será
executado.
Um destrutor também é muito útil na desalocação de memória. Imagine um
vetor de objetos sendo alocado dinamicamente em um programa. Quando este
vetor não for mais necessário, ele poderá ser desalocado, liberando memória.
Ao destruir o vetor, se o vetor é de objetos, o destrutor de cada objeto será exe-
cutado. Por sinal, na alocação do vetor de objetos, cada elemento instanciado
do vetor executa seu construtor.

8.7 Sobrecarga de Operadores


Uma outra forma de polimorfismo intrı́nseco às classes é realizado através
da sobrecarga de alguns operadores, isto é, a “função” do operador pode ser
alterado para atender uma nova situação. O mais comum é a modificação da
operação reservada ao operador para atuar sobre novos tipos de dados ou clas-
ses. Por exemplo, ao se criar uma classe vetor, se esperaria que fosse possı́vel
realizar as operações de soma de vetores, subtração de vetores, escala de veto-
res, produto escalar, produto vetorial, tudo realizado através da aplicação de
operadores tais como: “+”, “−”, “∗” e “|”. Os operadores “+”, “−”, “∗” são
operadores numéricos e estão inicialmente configurados para trabalhar sobre
dados numéricos (números reais e inteiros, com ou sem sinal) e o operador “|”
é um operador lógico binário. A capacitação destes operadores para operarem
sobre novos tipos de dados e classes vem por meio da sobrecarga utilizando a
palavra chave operator. O mecanismo da linguagem C++ é trabalhar com a
sobrecarga dos operadores como se fosse um método da classe. Então, vamos
ao exemplo. O trecho de código a seguir implementa uma classe chamada
vetor e sobrecarrega alguns operadores:

class vetor {
protected:
double x, y, z;
8.7. SOBRECARGA DE OPERADORES 99

public:
// construtor da classe vetor
vetor(void) { x = y = z = 0; }

// sobrecarga do operador +
// adiç~
ao de vetores
vetor operator +(vetor v) {
return vetor(x + v.x,y + v.y,z + v.z);
}

// sobrecarga do operador -
// subtraç~
ao de vetores
vetor operator -(vetor v) {
return vetor(x - v.x,y - v.y,z - v.z);
}

// sobrecarga do operador *
// produto escalar de vetores
double operator *(vetor v) {
return x*v.x + y*v.y + z*v.z;
}

// sobrecarga do operador *
// escala de vetores
vetor operator *(double f) {
return vetor(f*x,f*y,f*z);
}

// sobrecarga do operador *
// produto vetorial de vetores
vetor operator |(vetor v) {
return vetor(y*v.z - z*v.y, z*v.x - x*v.z, x*v.y - y*v.x);
}
};

Repare o uso dos construtores nas instruções return evitando declarações


desnecessárias de instruções e variáveis.
Um código completo é apresentado a seguir:
Código 8.2: exemplo31.cpp
# include < stdlib .h >
# include < stdio .h >
# include < string .h >
# include < math .h >
100 CAPÍTULO 8. CLASSES E OBJETOS

class vetor {
protected :
double x , y , z ;

public :
vetor ( void ) { x = y = z = 0; }
vetor ( double ix , double iy , double iz ) {
x = ix ; y = iy ; z = iz ;
}

double X ( void ) { return x ; }


double Y ( void ) { return y ; }
double Z ( void ) { return z ; }

vetor operator +( vetor v ) {


return vetor ( x + v .x , y + v .y , z + v . z );
}
vetor operator -( vetor v ) {
return vetor ( x - v .x , y - v .y , z - v . z );
}
double operator *( vetor v ) {
return x * v . x + y * v . y + z * v . z ;
}
vetor operator *( double f ) {
return vetor ( f *x , f *y , f * z );
}
vetor operator |( vetor v ) {
return vetor ( y * v . z - z * v .y ,
z * v . x - x * v .z ,
x * v . y - y * v . x );
}
};

int main ( int argc , char * argv []) {


vetor a , b , c ;
double d ;

a = vetor (1 ,0 ,0);
b = vetor (0 ,1 ,0);
printf ( " a = (% lf ,% lf ,% lf )\ n " ,a . X () , a . Y () , a . Z ());
printf ( " b = (% lf ,% lf ,% lf )\ n " ,b . X () , b . Y () , b . Z ());

c = a+b;
printf ( " a + b = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());
c = a|b;
8.7. SOBRECARGA DE OPERADORES 101

printf ( " a | b = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());


c = a *0.5;
printf ( " a *0.5 = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());
d = a*b;
printf ( " a * b = % lf \ n " ,d );

return 0;
}

A ação de um operador está sempre ligado ao objeto da esquerda e o objeto


da direita é processado como se fosse o argumento. Logo, na operação c=a+b;,
o operador “+” do objeto a recebe o objeto b como argumento e a operação é
realizada atributo a atributo, somando o atributo x de a com o x de b (passado
como objeto v na lista de argumentos do operador “+”). A operação “c=a+b;”
poderia ser lida como:

c = a.+(b);

Em especial, repare na operação de escala usando “∗”. Não há problema em


termos várias sobrescrições do mesmo operador dentro da mesma classe, desde
de que cada sobrescrição tenha uma lista de argumentos diferente, indicando
que as operações (as ações) são distintas. No caso, o operador “∗” é usado
para duas ações diferentes: uma é a escala do vetor e a outra é o cálculo do
produto escalar. Como as listas de argumentos são diferentes, o compilador
tem a capacidade de identificar no código quando é uma ação ou outra.
Voltando à escala do vetor, repare também que o fator de escala (a cons-
tante numérica) vem após o objeto, pois o operador “∗” recebe como argumento
a constante numérica, ou seja:

c = a*0.5; // => c = a.*(0.5);

Isto significa dizer que não podemos escrever (por enquanto) a seguinte ins-
trução:

c = 0.5*a; // => c = 0.5.*(a); NAO FUNCIONA!!!

pois o compilador tentará usar uma definição do operador “∗” que deveria
estar declarada na classe float.1 Mas como a classe float não declara um
operador “∗” que receba a classe vetor como argumento, esta instrução “c =
0.5*a;” não é valida.
1
Sim, é isso mesmo. Os tipos intrı́nsecos da linguagem C são “encarados” como classes
no C++.
102 CAPÍTULO 8. CLASSES E OBJETOS

Agora, convenhamos, esta operação, escrita como “c = 0.5*a;”, é clara


e muito mais natural e não teria porque não ser decodificada corretamente
pelo compilador. Para isso, ou por isso, existe um mecanismo que permite
que situações como esta sejam tratadas corretamente. Existe o modificador
de agregação chamado friend que pode ser usado na declaração de métodos,
permitindo que os métodos encapsulados operem como se “pertencessem” a ou-
tras classes, tendo como consequência, “acesso” aos atributos remotos. Então,
o operador “∗” para escala de vetor da classe vetor poderia ser sobrescrito
usando o modificador friend da seguinte forma:

friend vetor operator *(double f, vetor v) {


return vetor(f*v.x,f*v.y,f*v.z);
}

Obs.:esta sobrescrição do operador “∗” não substitui as declarações anteriores,


isto é, ela deve ser adicionada as declarações dos demais métodos e sobres-
crições de operadores já existentes.
Rescrevendo o exemplo completo:

Código 8.3: exemplo31.cpp


# include < stdlib .h >
# include < stdio .h >
# include < string .h >
# include < math .h >

class vetor {
protected :
double x , y , z ;

public :
// construtores da classe vetor
vetor ( void ) { x = y = z = 0; }
vetor ( double ix , double iy , double iz ) {
x = ix ; y = iy ; z = iz ;
}

double X ( void ) { return x ; }


double Y ( void ) { return y ; }
double Z ( void ) { return z ; }

// sobrecarga do operador +
// adiç~
a o de vetores
vetor operator +( vetor v ) {
return vetor ( x + v .x , y + v .y , z + v . z );
}
8.7. SOBRECARGA DE OPERADORES 103

// sobrecarga do operador -
// subtraç~
a o de vetores
vetor operator -( vetor v ) {
return vetor ( x - v .x , y - v .y , z - v . z );
}

// sobrecarga do operador *
// produto escalar de vetores
double operator *( vetor v ) {
return x * v . x + y * v . y + z * v . z ;
}

// sobrecarga do operador *
// escala de vetores
vetor operator *( double f ) {
return vetor ( f *x , f *y , f * z );
}

// sobrecarga do operador *
// escala de vetores
friend vetor operator *( double f , vetor v ) {
return vetor ( f * v .x , f * v .y , f * v . z );
}

// sobrecarga do operador |
// produto vetorial de vetores
vetor operator |( vetor v ) {
return vetor ( y * v . z - z * v .y ,
z * v . x - x * v .z ,
x * v . y - y * v . x );
}
};

int main ( int argc , char * argv []) {


vetor a , b , c ;
double d ;

a = vetor (1 ,0 ,0);
b = vetor (0 ,1 ,0);
printf ( " a = (% lf ,% lf ,% lf )\ n " ,a . X () , a . Y () , a . Z ());
printf ( " b = (% lf ,% lf ,% lf )\ n " ,b . X () , b . Y () , b . Z ());

c = a+b;
printf ( " a + b = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());
c = a|b;
104 CAPÍTULO 8. CLASSES E OBJETOS

printf ( " a | b = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());


c = a *0.5;
printf ( " a *0.5 = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());
c = 0.5* a ;
printf ( " 0.5* a = (% lf ,% lf ,% lf )\ n " ,c . X () , c . Y () , c . Z ());
d = a*b;
printf ( " a * b = % lf \ n " ,d );

return 0;
}

8.8 Virtualização de Métodos


A virtualização de métodos é outro mecanismo de polimorfismo. Esta ca-
racterı́stica é muito poderosa quando associada a propriedade de hierarquia
(herança). Uma classe ancestral e sua descendência podem definir métodos
com mesmo nome. Os métodos nas classes descendentes sobrescrevem o método
da classe ancestral. Até aqui, nada estranho. Agora, imagine a situação onde
um programa deva ser genérico o suficiente para que, com um único código,
seja capaz de trabalhar com qualquer objeto de classes descendentes de uma
classe primitiva.

Código 8.4: exemplo32.cpp


# include < stdio .h >
# include < string .h >

class figura {
protected :
char nome [30];
double dim1 , dim2 ;

public :
// metodo Nome da classe figura
char * Nome ( void ) { return nome ; }
// metodo virtual Area da classe figura
virtual double Area ( void ) { }

};

class quadrado : public figura {


public :
// construtor da classe quadrado
quadrado ( char * str , double d1 ) {
strcpy ( nome , str ); dim1 = d1 ;
8.8. VIRTUALIZAÇÃO DE MÉTODOS 105

// metodo Area da classe quadrado


double Area ( void ) { return dim1 * dim1 ; }
};

class triangulo : public figura {


public :
// construtor da classe triangulo
triangulo ( char * str , double d1 , double d2 ) {
strcpy ( nome , str ); dim1 = d1 ; dim2 = d2 ;
}

// metodo Area da classe triangulo


double Area ( void ) { return 0.5* dim1 * dim2 ; }
};

void imprimir_area ( figura & f ) {


printf ( " Area da figura % s : % lf \ n " ,f . Nome () , f . Area ());
}

int main ( void ) {


quadrado q ( " quadrado " ,2);
triangulo t ( " triangulo " ,2 ,2);

imprimir_area ( q );
imprimir_area ( t );

return 0;
}
106 CAPÍTULO 8. CLASSES E OBJETOS
Referências Bibliográficas

107

Você também pode gostar