Você está na página 1de 130

LINGUAGEM DE

PROGRAMAÇÃO
AULA 1

Prof. Sandro de Araujo


CONVERSA INICIAL

Esta aula tem como base o livro Treinamento em Linguagem C, do autor


Victorine Viviane Mizrahi, livro que pode ser acessado em nossa Biblioteca
Virtual Pearson. Não deixe de revisá-lo e ou consultá-lo em caso de dúvidas.
A aula apresenta a seguinte estrutura de conteúdo:

1. Compilador;
2. Estrutura de um programa em C;
3. Função main;
4. Principais características de uma função;
5. Pré-processador e diretivas.

O objetivo desta aula é introduzir os principais conceitos e temas das


abordagens sobre compiladores; a estrutura de um programa em linguagem de
programação C; a função main; as principais características de uma função; e o
pré-processador e diretivas a serem utilizadas nesta disciplina.

TEMA 1 – COMPILADOR

Criado na década de 1950, o nome compilador se refere ao processo de


tradução da linguagem de programação para linguagem de máquina (de uma
linguagem fonte para uma linguagem que o computador entenda). No entanto, a
tradução de uma linguagem fonte não é a única função do compilado; ele
também reporta ao seu usuário a presença de erros no programa origem
(Mizrahi, 2008).
Definido em Aho et al. (1995), um compilador consiste em um programa
que lê outro programa escrito em uma linguagem – a linguagem de origem – e a
traduz em um programa equivalente em outra linguagem – a linguagem de
destino. Sendo importante no processo de tradução, o compilador reporta ao seu
usuário a presença de erros no programa origem.
Ao longo dos anos 1950, os compiladores foram considerados programas
notoriamente difíceis de escrever. O primeiro compilador Fortran, por exemplo,
consumiu 18 homens/ano para ser implementado (Backus, 1957). Desde então,
foram descobertas técnicas sistemáticas para o tratamento de muitas das mais
importantes tarefas desenvolvidas por um compilador.

2
A variedade de compiladores nos dias de hoje é muito grande. Existem
inúmeras linguagens fontes, que poderiam ser citadas em várias páginas deste
trabalho. Isso se deve, principalmente, ao fato de que, com o aumento do uso
dos computadores, aumentaram também as necessidades de cada indivíduo,
sendo essas específicas, exigindo, por sua vez, linguagens de programação
diferentes.
Este processo, com a evolução da tecnologia de desenvolvimento de
compiladores, levou à criação de várias técnicas para a construção de um
compilador, ou seja, passaram a existir diferentes maneiras de se implementar
um compilador.
No entanto, a despeito dessa aparente complexidade, as tarefas básicas
que qualquer compilador precisa realizar são essencialmente as mesmas. A
grande maioria dos compiladores de hoje faz uso da técnica chamada tradução
dirigida pela sintaxe. Nessa técnica, as regras de construção do programa fonte
são utilizadas para guiar todo o processo de compilação. Algumas das técnicas
mais antigas utilizadas na construção dos primeiros compiladores (da linguagem
Fortran) podem ser obtidas em Rosen (1967).
Existem duas tarefas principais executadas por um compilador no
processo de tradução:

1. Análise – momento em que o texto de entrada (código escrito na


linguagem de programação) é examinado, verificado e compreendido;
2. Síntese (ou geração de código) – momento em que o texto de saída (texto
objeto) é gerado.

A análise cria uma representação intermediária desse texto, e a síntese


constrói o texto objeto (programa objeto) por meio da representação
intermediária criada pela análise, conforme mostrado na Figura 1.

Figura 1 – Etapas do processo de tradução

3
A análise retorna como uma representação intermediária do código fonte.
Sendo assim, deve conter as informações necessárias para a geração do código
objeto que o corresponda. Quase sempre, essa representação tem como
complemento tabelas com informações adicionais sobre o programa fonte.
Existem casos em que a representação intermediária toma a forma de um
programa em uma linguagem intermediária, tornando mais fácil a tradução para
a linguagem objeto desejada (Mizrahi, 2008).
Uma das formas mais comuns de tabela utilizada na representação
intermediária é a tabela de símbolos (tokens). A função dessa tabela é guardar
cada token usado no programa na informação correspondente.

1.1 Componentes de um compilador

Geralmente, um compilador é dividido em quatro fases. São elas: 1)


analisador léxico; 2) analisador semântico; 3) analisador sintático; 4)
geração/otimização do código. Cada uma dessas partes tem uma função
específica, conforme mostrado abaixo:

 Analisador léxico – separa cada símbolo do código fonte que tenha


algum significado para a linguagem, ou avisa quando encontra um
símbolo que não faz parte da linguagem.
 Analisador semântico – verifica se os aspectos semânticos estão
corretos, ou seja, se não existem incoerências quanto ao significado das
construções utilizadas pelo programador. A análise semântica depende
de uma tabela de símbolos em que são armazenadas as informações de
variáveis declaradas, funções, entre outros.
 Analisador sintático – é responsável por verificar se a sequência de
símbolos existentes no programa fonte forma um programa válido ou não.
É construído sobre uma gramática composta de uma série de regras que
descrevem as construções válidas da linguagem.
 Geração e otimização de código – após a verificação da não existência
de erros sintáticos ou semânticos, o compilador realizará a tarefa de criar
o código objeto correspondente, mediante instruções de baixo nível.
Nessa etapa, também pode realizar otimizações no código, em que serão
aplicadas diversas técnicas para otimizar algumas características do
código objeto, como tamanho ou velocidade.

4
Essas fases são conceituais e especificam atividades que todos os
compiladores executam, embora frequentemente as atividades de várias fases
possam ser combinadas e executadas simultaneamente. Essas fases estão
resumidamente ilustradas na Figura 2.

Figura 2 – Fases das atividades de um compilador

1.2 Os quatro estágios da compilação de um programa em C

Saber como funciona a compilação pode ser muito útil na hora de escrever
um código e ao depurá-lo. Compilar um programa C é um processo que pode
ser dividido em quatro estágios separados: pré-processamento, compilação,
montagem e linkagem.
Para melhor entendimento, vamos percorrer cada um dos quatro estágios
de compilação do seguinte programa em C, mostrado na Figura 3.

Figura 3 – Arquivo helloWorld.c

5
1.2.1 Pré-processamento

O primeiro estágio da compilação é chamado de pré-processamento.


Nele, as linhas que começam com um #caractere são interpretadas pelo pré-
processador como comandos. Esses comandos formam uma linguagem macro
simples com sua própria sintaxe e semântica (Mizrahi, 2008).
Essa linguagem é usada para reduzir a repetição no código-fonte,
fornecendo funcionalidade para arquivos embutidos, definindo macros e
omitindo o código condicionalmente.
A primeira fase da tradução de código fonte para código de máquina inclui:

 Remoção de comentários;
 Expansão de macros;
 Expansão dos arquivos incluídos.

Antes de interpretar os comandos, o pré-processador retira os


comentários e produz o conteúdo da biblioteca stdio.h e stdlib.h, que está no
cabeçalho do nosso código, que será associado ao conteúdo do helloWorld.c.
Os comentários fornecem uma forma de incluir um texto descritivo nos
códigos, facilitando a compreensão do programa por outras pessoas que fizeram
parte de seu desenvolvimento.
Vantagens de se comentar um programa:

 Facilidade na organização e escrita do algoritmo.


 Auxilia a detecção e correção de erros.

Os comentários operam, na prática, como uma documentação interna dos


sistemas. Sua utilização é considerada de grande importância.
Um comentário em C consiste em uma sequência de caracteres que
começa com uma barra e um asterisco (/*) e termina com um asterisco e uma
barra (*/). Os comentários são tratados pelo compilador como um espaço em
branco, isto é, são ignorados. Um comentário pode incluir qualquer grupo de
caracteres, incluindo mudanças de linha, exceto o indicador de fim de
comentário, ou seja /*. Em virtude deste fato, os comentários descritos não
podem ser encadeados (um comentário dentro de outro comentário) (Intprogc,
2018).

6
Outra forma de formar comentários em C consiste em escrever duas
barras (//) na coluna em que queremos iniciar o comentário. Todos os caracteres,
até ao final dessa linha, serão considerados comentários (Intprogc, 2018).
Os comentários podem aparecer em qualquer ponto do programa em que
seja permitida a utilização de um espaço em branco.
Os exemplos seguintes ilustram algumas das formas possíveis de um
comentário.
// Isto é um exemplo de um comentário uma única linha.
/*
Isto é outro exemplo
de um comentário
que ocupa mais de
uma linha
*/

1.2.2 Compilação

Nesse estágio, o código pré-processado é traduzido para instruções de


montagem específicas da arquitetura do processador de destino. Alguns
compiladores também suportam o uso de um montador integrado, no qual o
estágio de compilação gera diretamente o código de máquina, evitando a
sobrecarga de gerar as instruções de montagem intermediárias e invocando o
montador (Mizrahi, 2008).
Nessa fase, é gerado um arquivo de saída intermediário, o helloWorld.s,
arquivo já preparado com as instruções no nível da montagem.

1.2.3 Montagem

Nessa fase, o helloWorld.s é tomado como entrada e transformado em


helloWorld.o pelo montador, já com instruções no nível da máquina (Mizrahi,
2008). Somente o código existente é convertido em linguagem de máquina, as
chamadas de função, como printf (), não são resolvidas nesse momento, e sim
na próxima fase, na linkagem ou vinculação.

7
1.2.4 Linkagem

Essa é a fase final, em que todas as chamadas de função e suas


definições são resolvidas (Mizrahi, 2008). O Linker sabe onde todas essas
funções são implementadas e, caso tenha algum código extra, também será
adicionado ao nosso programa.
Para produzir um programa executável, as peças existentes precisam ser
reorganizadas; as que faltam, são preenchidas. No caso do programa "Hello,
World!", O vinculador adicionará o código do objeto para a função printf ().

TEMA 2 – ESTRUTURA DE UM PROGRAMA EM C

Todos os computadores suportam alguma linguagem nativa que


especifica um conjunto de instruções para serem executadas diretamente.
Traduzida para representação binária, escrever o algoritmo necessário para
resolver o problema computacional, nesta linguagem, por um programador
humano, é um tanto fastidiosa e sujeita a erros (Mizrahi, 2008).
Desde o desenvolvimento dos primeiros computadores, foram feitas
tentativas de tornar o processo de programação mais simples, por meio da
redução do conhecimento das características internas do computador,
necessárias para a escrita de programas. Daí a necessidade de criar sistemas
em uma linguagem mais compreensível para o humano (Mizrahi, 2008).
As linguagens de programação de alto nível realizadas pelos humanos,
em suas línguas nativas, possibilitam a especificação de soluções em conceitos
mais próximos dos empregados. Essas linguagens foram criadas para tornar a
criação de algoritmos mais simples, sem ter a necessidade de os programadores
conhecerem a estrutura e o funcionamento interno um máquina.
A linguagem de Programação C é de alto nível e apresenta sintaxe
estruturada e flexível. Com a linguagem de Programação C, são criados
programas compilados, gerando programas executáveis.
A linguagem de Programação C tem estrutura simples e gera códigos
mais enxutos e velozes se comparada a outras linguagens, pois permite a
inclusão de uma farta quantidade de rotinas.
A linguagem C foi criada por Dennis Ritchie, nos laboratórios Bell, em
1972. Hoje, é uma das linguagens mais utilizada no mundo (Mizrahi, 2008).

8
Um código fonte em C pode ser formado por um ou mais arquivos fonte.
Em um arquivo fonte, encontramos declarações e definições de funções e
identificadores. Tais declarações e definições podem estar contidas em arquivos
fonte, arquivos cabeçalho, bibliotecas e outros arquivos necessários ao
programa. Para se obter um arquivo executável, é necessário compilar cada um
dos arquivos do projeto e gerar arquivos objeto destes.
Um programa em C é constituído de:

 Um cabeçalho – que contém inclusão de bibliotecas, as diretivas de


compilador onde se define o valor de constantes simbólicas, a declaração
de variáveis, a declaração de funções, entre outros.
 Um bloco principal de instruções e outros blocos de rotinas.
 Documentação do programa em forma de comentários.

Os comentários podem ser escritos em qualquer parte do algoritmo. Para


que seja identificado como tal, ele deve ter um /* antes e um */ depois.
A seguir, veremos, na Figura 4, um programa em C que vai mostrar no
monitor a frase “Olá Alunos!”:

Figura 4 – Código C olaAlunos.c

A primeira linha do programa #include <stdio.h> informa ao compilador a


biblioteca que deve incluir à biblioteca stdio (standard input/output) no programa
(Intprogc, 2018).
Na segunda linha, foi declarada a única função, a função main, e nessa
existe apenas uma única instrução, que é a função printf() na linha 4 (disponível
na biblioteca stdio.h da linguagem C) para escrever uma mensagem no monitor.
As palavras include, int, pertencem ao léxico do C. São palavras com
significado especial na linguagem C.
Algumas palavras apresentam um significado especial para o compilador
e, portanto, não podem ser utilizadas para descrever entidades definidas pelo
programador. A linguagem C utiliza um conjunto extenso de palavras-chave em
9
relação à linguagem C (Intprogc, 2018). No entanto, as seguintes palavras-
chave, um subconjunto das palavras chave utilizadas pela linguagem C, são as
únicas que utilizaremos, conforme mostrado na Tabela 1.

Tabela 1 – Palavras reservadas da linguagem C

auto break case char


continue default do double
else enum extern false
float for goto if
int struct long register
return short signed sizeof
static struct switch typedef
union unsigned struct void
volatile while
Fonte: Intprogc, 2018.

TEMA 3 – A FUNÇÃO MAIN

A função main serve como o ponto de partida para a execução do


programa. Em geral, ela controla a execução, direcionando as chamadas para
outras funções no programa. Normalmente, um programa para de ser executado
no final de main, embora possa ser encerrado precocemente em outros pontos
por diversos motivos.
Portanto, a função main é a primeira função de um programa C a ser
executada e a única que não precisa ser explicitamente chamada.
A seguir, os formatos usados na função main:

 int main()
 int main(void)
 int main(int argc, char * argv[ ])
 int main(int argc, char * const argv[ ], char * const envp[ ])

Os formatos int main( ) e int main(void) são usados quando nenhum


argumento é passado ao programa. Por sua vez, os formatos int main(int argc,
char * argv[ ]) e int main(int argc, char * const argv[ ], char * const envp[ ])
fornecem, respectivamente, dois e três argumentos. Os argumentos, quando
declarados, são sempre os seguintes:

10
 argc – é o contador de argumentos. Ele informa quantos argumentos
foram passados juntos com o nome do programa. Se o valor é 1, então
argumentos não foram fornecidos com o nome do programa. Observe as
figuras 5 e 6.

Figura 5 – Algoritmo que imprime o conteúdo de argc

Figura 6 – Resultado da execução do algoritmo da figura 5

 argv – o nome do programa é armazenado em argv[0]. Observe as figuras


7 e 8.

Figura 7 – Algoritmo que imprime o conteúdo de argv

11
Figura 8 – Resultado da execução do algoritmo da figura 7

A Figura 8 traz como resultado o endereço onde foi armazenado o arquivo


executável teste4.exe após a compilação do algoritmo. Nesse exemplo, o
endereço apresentado foi:

C:\Dev-Cpp\teste4.exe

 envp – apresenta informações sobre o ambiente do processo. Observe


as figuras 9 e 10.

Figura 9 – Algoritmo que imprime o conteúdo de envp em diferentes posições no


vetor

Figura 10 – Resultado da execução do algoritmo da figura 9

12
O ambiente do processo é substituído pelo parâmetro envp, sendo
possível guardar vários processos em diferentes posições no vetor.
Por padrão, os nomes argc, argv[ ] e envp[ ] são sempre usados.
Entretanto, não há problema em você usar outros nomes (ex: contador,
argumentos[ ] e ambiente[ ]), desde que você não altere os tipos de dados.
A função main pode retornar um inteiro para o sistema operacional. Para
isso, utiliza-se o int main(), em conjunto com o comando return, para encerrar a
função e passar um inteiro para o sistema operacional, conforme mostrado no
exemplo da Figura 11.

Figura 11 – Exemplo função main

O retorno zero indica ao Sistema Operacional que o programa foi bem-


sucedido. Cada outro número retornado indica o código de uma condição de erro
(Mizrahi, 2008). Portanto, lembre-se sempre de colocar o retorno. Isso é uma
boa prática de programação.

TEMA 4 – PRINCIPAIS CARACTERÍSTICAS DE UMA FUNÇÃO

Toda função é declarada com uma identificação e parênteses após seu


nome. No exemplo main(), a função é identificada com o nome “main” e
acompanhada de “(” e “)”. Essas características que permitem que o compilador
saiba que se trata de uma função. Sem os parênteses, o compilador pode tratar
o nome como se fosse uma variável, ocasionando um erro, uma vez que a
palavra main é reservada pelo compilador (Mizrahi, 2008).
Após a chamada da função, vem o bloco de código. Toda função em
linguagem de programação C delimita o bloco com chaves. Começar com uma

13
chave de abertura de bloco ({) e terminar com uma chave de fechamento de
bloco (}). Essas chaves determinam o corpo da função.
Em uma função, é permitido inserir espaços em branco, tabulações e pular
linhas (esses caracteres são ignorados pelo compilador). É permitido também
escrever várias instruções em uma única linha ou escrever uma instrução em
várias linhas. Não há um estilo obrigatório para a escrita de programas em C. No
entanto, é sempre desejável seguir as boas práticas da programação com
indentações sem exagero (Mizrahi, 2008).
Quando desenvolvemos um projeto de desenvolvimento de software, é
importante que todo o código seja bem indentado, ou seja alinhando de forma
correta.
Em um grande projeto, é importante a definição de um padrão de
indentação, que deve ser documentado e disponibilizado aos demais
programadores.
A palavra indentação é um neologismo e não existe na língua
portuguesa. Esse termo foi “abrasileirado” do termo indentation, usado na língua
inglesa, que significa recuo.
O objetivo da indentação é o de tornar os códigos mais legíveis. A
utilização de regras de indentação permite, por meio de uma simples inspeção
visual, evidenciar facilmente a estrutura global do programa, tornando
claramente visíveis todas as instruções de um ou mais blocos de código.
Nos exemplos das figuras 12, 13, 14, 15, 16 e 17, a seguir, são ilustrados
os dois estilos de indentação mais utilizados. O número de espaços utilizado
pode variar entre 2 e 8. Escolha o estilo que mais lhe agrada e utilize de forma
uniforme em todos os seus programas.

Figura 12 – Indentação IF

14
Figura 13 – Indentação IF, ELSE IF, ELSE

Figura 14 – Indentação FOR

Figura 15 – Indentação WHILE

Figura 16 – Indentação DO WHILE

15
Figura 17 – Indentação SWITCH CASE

TEMA 5 – PRÉ-PROCESSADOR E DIRETIVAS

As primeiras linhas de um programa não são instruções da linguagem C


(observe que não há ponto-e-vírgula ao seu final), mas sim diretivas do pré-
processador.
Um pré-processador executa um conjunto de processos preliminares
sobre os arquivos fonte antes de estes serem fornecidos para o compilador
(Mizrahi, 2008).
O pré-processador é um programa que examina o programa fonte em C
e executa certas modificações com base em instruções, as quais chamamos de
diretivas. Toda diretiva é iniciada pelo símbolo (#), código especial, e o seu texto
deve ser escrito em uma única linha. Como descrito no capítulo 1.2, as diretivas
do pré-processador não fazem parte da linguagem C, elas servem para auxiliar
no desenvolvimento do código fonte.
Uma diretiva ensina o pré-processador no sentido de efetuar determinada
ação sobre o texto do programa antes da compilação.
Na linguagem de programação C, existem comandos processados
durante a compilação do programa, conhecidos como diretivas de compilação.
Esses comandos informam ao compilador as constantes simbólicas usadas na
linguagem de programação e as bibliotecas que devem ser anexadas ao
programa compilado (Mizrahi, 2008). Observe os exemplos mostrados abaixo:

 A diretiva #include diz ao compilador para incluir na compilação do


programa outros arquivos. Geralmente estes arquivos contém bibliotecas
de funções ou rotinas do usuário.
 A diretiva #define diz ao compilador quais são as constantes simbólicas
que serão usadas no programa.

O código da Figura 18 mostra o uso das diretivas #include e #define.


16
Figura 18 – Código C com diretivas

No código da Figura 18, a constante QUANTIDADE_MAXIMA será


executada antes da compilação do texto escrito pelo programador. A diretiva
#include provoca a inclusão das bibliotecas stdio.h e stdlib.h em nosso programa
fonte, não interferindo no tempo de execução da programação, mas impactando
no tempo de compilação.
Na verdade, o compilador substitui a linha que contém essa diretiva pelo
conteúdo do arquivo indicado. Essa substituição também é executada antes de
o programa ser compilado. Assim, o efeito obtido é a apresentação de um texto,
como se tivéssemos digitado todo o conteúdo de arquivo stdio.h e do arquivo
stdlib.h na posição em que escrevemos as linhas:

Primeiro o #include <stdio.h> seguido do #include <stdio.h>.

O arquivo stdlib.h contém as definições e declarações necessárias para o


uso da função printf(), já o stdlib.h contém as declarações necessárias para o
uso da função system().
A diretiva #include aceita uma segunda sintaxe:

#include "meuarq.h".

Quando usamos os sinais < e >, o arquivo é procurado somente na pasta


include, criada na instalação do seu compilador. Quando usamos " e " (aspas
duplas), o arquivo é procurado primeiramente na pasta atual e, depois, se não
for encontrado, na pasta include.

17
Embora a linguagem C disponibilize um grande conjunto de diretivas, que
estão enunciadas na tabela seguinte, no âmbito deste curso, utilizaremos
fundamentalmente a diretiva #include (Intprogc, 2018).

Tabela 2 – Diretivas em C

#define #endif #ifdef #line


#elif #error #ifndef #pragma
#else #if #include #undef
Fonte: Intprogc, 2018.

5.1 Códigos especiais

Além do comando #, existem vários outros caracteres usados para auxiliar


o programador em seu código. A barra invertida (\) é um comando usado no
momento em que o programador precisa digitar algo que não pode ser digitados
diretamente do nosso teclado. Exemplo: [ENTER], uma quebra de linha, a
tabulação, entre outros. Esses caracteres são codificados em C por meio da
combinação do sinal \ (barra invertida) com outros caracteres. Observe a Tabela
3.

Tabela 3 – Códigos especiais

CÓDIGOS
SIGNIFICADO
ESPECIAIS
\n Nova linha
\t Tabulação
\b Retrocesso (usado para impressora)
\f Salto de página de formulário
\a Beep – Toque do alto-falante
\r CR – Retorno do cursor p/ o início da linha
\\ \ – Barra invertida
\0 Zero
\’ Aspas simples (apóstrofo)
\” Aspas dupla
\xdd Representação hexadecimal
\ddd Representação octal
Fonte: Adaptado de Mizrahi, 2008.

O exemplo da Figura 19 traz um algoritmo com os códigos especiais \t e


o \n.

18
Figura 19 – Algoritmo com códigos especiais \t e \n

Figura 20 – Resultado da execução do algoritmo da figura 19

O exemplo da Figura 21 traz o mesmo algoritmo da Figura 19 sem os


códigos especiais \t e o \n. Analise os dois resultados e veja as diferenças.

Figura 21 – Algoritmo sem códigos especiais \t e \n

Figura 22 – Resultado da execução do algoritmo da figura 21

19
Portanto, na linguagem de programação C, sempre que aparecer, em um
conjunto de caracteres (texto literal no código), uma barra invertida ('\') ou um
porcento ('%'), será um comando. Vejamos na Tabela 4 alguns comandos mais
usados na função printf().

Tabela 4 – Códigos para impressão formatada com printf().

Códigos de
impressão
SIGNIFICADO
formatada com
printf().
%c Caractere simples
%d Inteiro decimal com sinal
%i Inteiro decimal com sinal
%e Notação científica (e minúsculo)
%E Notação científica (e maiúsculo)
%f Ponto flutuante em decimal
Fonte: Adaptado de Mizrahi, 2008.

FINALIZANDO

Nesta aula, aprendemos o conceito de compiladores, a estrutura de um


programa em linguagem de programação C, a função main, as principais
características de uma função e o pré-processador e diretivas estudadas nesta
aula.

20
REFERÊNCIAS

AHO, A. V.; SETHI, R.; ULLMAN, J. D. Compilers, Principles,


Techniques and Tools. Company Reading, Massachusetts, 1995.

BACKUS, J. W. The FORTRAN Automatic Coding System. Western joint


computer conference: techniques for reliability. Los Angeles, 1957.

INTPROGC. Programação em C. Disponível em:


<http://intprogc.pbworks.com/w/page/11211411/FrontPage>. Acesso em: 5 out.
2018.

MIZRAHI, V. V. Treinamento em Linguagem C. 2. ed. São Paulo: Pearson,


2008.

RANGEL, J. L. Compiladores. Rio de Janeiro: PUC-Rio, 1999.

ROSEN, S. Programming Systems and Languages. USA, 1999.

21
LINGUAGEM DE
PROGRAMAÇÃO
AULA 2

Prof. Sandro de Araujo


CONVERSA INICIAL

Esta aula tem como base os livros Fundamentos da Programação de


Computadores: Algoritmos, Pascal, C/C++ e Treinamento em Linguagem C. Em
caso de dúvidas ou para aprofundamento, consulte-os em nossa Biblioteca
Virtual Pearson.
A aula apresenta a seguinte estrutura de conteúdo:

1. Endereços de memória;
2. Ponteiros;
3. Ponteiros – tamanho e endereçamento;
4. Ponteiros e vetores;
5. Passagem de parâmetros por referência.

O objetivo desta aula é conhecer o conceito de ponteiro e sua aplicação


em algoritmos computacionais; entender como um dado é acessado na
memória; e sua relação com vetores e funções.

TEMA 1 – ENDEREÇOS DE MEMÓRIA

A memória de um computador é dividida em bytes, numerados de zero


até o limite de memória da máquina. Esses números são chamados endereços
de bytes, usados como referências, pelo computador, para localizar as variáveis
(Mizrahi, 2008).
Toda variável tem uma localização na memória, e o endereço de
identificação desta variável é o primeiro byte ocupado por ela, conforme
explicado anteriormente.
Se o programa contém a informação somente do endereço do primeiro
byte da variável, como ele sabe quais endereços deve ler? Bom, para obter esta
resposta, o programa deve saber que toda variável está armazenada em bytes
sequenciais e, identificando o tamanho desta variável pelo seu tipo, infere até
onde a leitura dos endereços deve ir.
Por exemplo, uma variável do tipo int em C com tamanho 4 bytes, ou seja,
sabendo o endereço inicial, sabe que, a partir, dele temos mais três endereços
sequenciais que correspondem à variável desejada.
Quando o programa é carregado na memória, ocupa certa quantidade de
bytes, e toda variável e função desse programa terão seu espaço e endereço

2
particular. Para conhecer o endereço em que uma variável está alocada, usa-se
o operador de endereços &. Observe a Figura 1, que mostra um algoritmo que
vai imprimir os endereços de três variáveis usando a função printf() nas linhas
9, 10 e 11.

Figura 1 – Algoritmo que imprime o endereço de três variáveis

A Figura 2 mostra a saída do algoritmo acima após a sua execução.

Figura 2 – Saída do algoritmo

Quando se define uma variável como ponteiro, dizemos que o endereço


de uma variável simples está guardado em um ponteiro que pode ser utilizado
como parâmetro para uma função. Para que isso ocorra, basta colocar o

3
operador “*” antes da variável e o operador “&”1 na chamada do parâmetro
(Mizrahi, 2008).
O resultado do operador &, "endereço de", sempre será o endereço de
memória do elemento em questão, normalmente é o local onde uma variável está
alocada na memória. Isto é, esse operador gera um ponteiro.
A função scanf() espera que o usuário digite algum dado de entrada e o
operador ‘&’, acompanhado da variável, serve para especificar o lugar certo onde
esse dado vai ficar posicionado na memória. Portanto, o uso do operador de
endereço para essa função se faz necessário (Ascencio, 2012).
O operador oposto é o * (asterisco), que pega o valor apontado pelo
endereço.
No exemplo do algoritmo da Figura 1, foram declaradas três variáveis
inteiras. Obtivemos como saída a impressão dos seus endereços em
hexadecimal com o uso do operador &. O endereço alocado depende de vários
fatores, dentre eles, o tamanho da palavra2, se há ou não outros programas
usando a memória, entre outros.
Por essas razões, podemos encontrar endereços diferentes na passagem
de parâmetros e execução do algoritmo (Mizrahi, 2008) para cada nova
execução de um problema. Faça o teste você mesmo. Implemente o exemplo
anterior no seu compilador e mande-o executar diversas vezes. Cada nova
execução gerará valores diferentes de endereços alocados.
Mizrahi (2008) descreve memória como uma unidade organizada
logicamente em palavras. Uma palavra é uma unidade lógica de informação
constituída por um número de bits de único endereço, consequentemente, um
conjunto de palavras armazenadas na memória é um programa, e pode ser
dividido em duas categorias:

 Instruções – operações (programa propriamente dito) realizadas pela


máquina;
 Dados – variáveis, ou valores, processadas nessas operações.

Cada palavra é identificada por meio de um endereço de memória sem


ambiguidade. Observe a Tabela 1.

1 O operador unário & retorna o endereço na memória de seu operando (Mizrahi, 2008).
2 Unidade de informação para cada tipo de computador (Mizrahi, 2008).
4
Tabela 1 – Exemplo de endereços de palavras

Ordem na Endereço na
Palavras
memória memória
0 000 Palavra 0
1 001 Palavra 1
2 010 Palavra 2
3 011 Palavra 3
4 100 Palavra 4
5 101 Palavra 5
6 110 Palavra 6
7 111 Palavra 7
A capacidade, ou tamanho, de uma memória vai depender do número de
palavras que ela pode suportar. A posição de uma palavra dentro da memória é
tida como o seu endereço. A primeira palavra da memória tem o endereço 000,
a próxima, 001, e assim por diante (Mizrahi, 2008).

TEMA 2 – PONTEIROS

O ponteiro é uma ferramenta poderosa oferecida em linguagens de


programação e considerada, pela maioria dos programadores, um dos tópicos
mais difíceis (Mizrahi, 2008; Ascencio, 2012).
Apontadores, ou ponteiros, são variáveis que armazenam o endereço de
outras variáveis na memória. Ou seja, em vez de termos um valor numérico ou
caracteres, por exemplo, armazenado na variável, temos um endereço. Dizemos
que um ponteiro “aponta” para uma variável na memória quando este
contém o endereço daquela variável.
O uso de ponteiros é muito útil quando um dado deve ser acessado na
memória em diferentes partes de um programa. Assim, podem existir vários
ponteiros espalhados, indicando a localidade da variável que contém o dado
desejado. Caso este dado seja atualizado, todas as partes que apontam para a
variável serão atualizados simultaneamente (Ascencio, 2012).
De acordo com Mizrahi (2008), estas são algumas razões para o uso de
ponteiros:

1. Fornecem maneiras com as quais as funções podem realmente modificar


os argumentos que recebem (passagem de parâmetros por referência);
2. Criar estruturas de dados complexas, como listas encadeadas e árvores
binárias, em que um item deve conter referências a outro;
3. Alocar e desalocar memória dinamicamente do sistema;
4. Passar para uma função o endereço de outra função.
5
A sintaxe de declaração de um ponteiro é:
tipo *nome_ponteiro;
Em que temos:

 tipo – refere-se ao tipo de dado da variável armazenada que é apontada


pelo endereço do ponteiro;
 *nome_ponteiro – o nome da variável ponteiro;
 O uso do asterisco * serve para determinar que a variável usada será um
ponteiro.

Um ponteiro, como qualquer variável, deve ser tipificado, que é a


identificação do tipo da variável para a qual ele aponta. Para declarar um
ponteiro, especifica-se o tipo da variável para a qual ele aponta com o nome
precedido por asterisco. Exemplo:
int ponteiro; // declaração de uma variável comum do tipo inteiro
int *ponteiro; // declaração de um ponteiro para um inteiro
É importante prestar bastante atenção na hora de declarar vários
ponteiros em uma linha, visto que o asterisco deve vir antes de cada nome de
variável. Exemplos:
int x, y, z; // Essa instrução declara três variáveis comuns.
int *x, y, z; // Essa instrução declara somente x como ponteiro.
int *x, *y, *z; // Essa instrução declara três ponteiros.
Um ponteiro é uma variável que armazena um endereço de memória, a
localização de outra variável. Dizemos que uma variável aponta para outra
quando a primeira contém o endereço da segunda. Observe os exemplos
mostrados nas figuras 3 e 4.

Figura 3 – Exemplo de ponteiros

6
A Figura 4 mostra a saída do algoritmo acima após a sua execução.

Figura 4 – Saída do algoritmo

Figura 5 – Exemplo de ponteiros

A Figura 6 mostra a saída do algoritmo acima após a sua execução.

Figura 6 – Saída do algoritmo

7
Figura 7 – Exemplo de ponteiros

A Figura 8 mostra a saída do algoritmo acima após a sua execução.

Figura 8 – Saída do algoritmo

TEMA 3 – PONTEIROS: TAMANHO E ENDEREÇAMENTO

Um ponteiro também é uma variável e também ocupa espaço na memória.


Normalmente, o tamanho de um ponteiro independe do tipo de dados da variável
da qual está apontando e ocupa o espaço de um inteiro (Mizrahi, 2008).
Para obter o tamanho de um tipo de variável na linguagem de
programação C, utiliza-se a função sizeof. A Figura 9 mostra um algoritmo que
imprime o tamanho das variáveis mais usadas na escrita de um programa em
linguagem de programação C (Mizrahi, 2008; Ascencio, 2012).

8
Figura 9 – Tamanho de variáveis mais usadas em C

A Figura 10 mostra a saída do algoritmo acima após a sua execução.

Figura 10 – Saída do algoritmo

A saída gerada, na Figura 9, após a execução da função sizeof, tem como


retorno o tamanho dos tipos de variáveis em bytes.
Como exemplo, vamos considerar a declaração de duas destas variáveis
listadas no algoritmo, em uma memória endereçada byte a byte, uma variável do
tipo float que ocupará 4 bytes e outra variável do tipo double, que ocupará 8
bytes. A Figura 11 ilustra o endereço base dessas duas variáveis.

Figura 11 – Exemplo de duas variáveis na memória

No cenário da Figura 11, considera-se como endereço o menor valor na


região ocupada. Sendo assim, a variável a terá como endereço o numeral 3, e a
variável b, o numeral 7.

9
Trabalha-se com ponteiros quando existe a necessidade de ter os valores
das variáveis alterados diretamente na memória mostra um. A Figura 12 mostra
um algoritmo que evidencia esse conceito.

Figura 12 – Manipulação de dados com ponteiros

A Figura 13 mostra a saída do algoritmo acima após a sua execução.

Figura 13 – Saída do algoritmo

No momento em que declarou as variáveis c e d, também se fixaram os


valores 5 e 3, respectivamente. Após a execução das instruções, conforme
mostrado na Figura 6, seus valores foram alterados para 0 e 8 com o uso de
ponteiros.

TEMA 4 – PONTEIROS E VETORES

Vetores unidimensionais, ou arrays, consistem em um conjunto de dados


de mesmo tipo armazenados em posições sequenciais na memória,
caracterizando uma estrutura de dados homogênea. O nome do vetor é um

10
ponteiro que aponta para o primeiro elemento do vetor. Observe o algoritmo da
Figura 14.

Figura 14 – Ponteiros e vetores

A Figura 15 mostra a saída do algoritmo acima após a sua execução.

Figura 15 – Saída do algoritmo

As instruções acima são usadas para criar um ponteiro que vai apontar
para o primeiro elemento do vetor x[ ]. A expressão “pont = x;” faz com que o
ponteiro “pont” atribua o endereço do primeiro elemento do vetor x[ ]. A Figura
16 ilustra a atribuição do endereço ao ponteiro “pont”.

Figura 16 – Representação de um ponteiro com o endereço de um vetor

11
Para obter o endereço do primeiro elemento, basta escrever:
1. int x[ ] = {2, 16, 15, 3, 10};
2. int *pont;
3.
4. pont = x.
ou
1. int x[ ] = {2, 16, 15, 3, 10};
2. int *pont;
3.
4. pont = &x[0].
As instruções anteriores produzem resultados equivalentes. Logo, as
instruções abaixo serão usadas de forma análoga para obter o endereço do
quinto elemento:
1. int x[] = {2, 16, 15, 3, 10};
2. int *pont;
3.
4. pont = &x[4].
Para obter o endereço de outro índice, é necessário utilizar o operador ‘&’.
Observe o código acima e ilustrado na Figura 17.

Figura 17 – Ponteiro com o endereço do quinto elemento do vetor

12
Figura 18 – Exemplo de ponteiro e vetor

A Figura 19 mostra a saída do algoritmo acima após a sua execução.

Figura 19 – Saída do algoritmo

13
Figura 20 – Exemplo de ponteiro e vetor

A Figura 21 mostra a saída do algoritmo acima após a sua execução.

Figura 21 – Saída do algoritmo

14
A Figura 23 mostra a saída do algoritmo acima após a sua execução.

Figura 23 – Saída do algoritmo

4.1 Vetor de ponteiros

Os ponteiros também podem ser declarados na forma de uma estrutura


de dados homogênea (Mizrahi, 2008). Para evidenciar esse conceito, temos o
algoritmo na Figura 24, que define um vetor de ponteiros com 4 elementos, e
mais quatros vetores de 3 elementos.

Figura 24 – Vetor de ponteiros

15
A Figura 25 mostra a saída do algoritmo da Figura 24 após a sua
execução.

Figura 25 – Saída do algoritmo

Para acessar os elementos de pont[0], pont[1], pont[2] e pont[3], basta


manipular os ponteiros utilizando o operador ‘*’ e indicar o índice desejado.
Conforme mostrado abaixo:
 *pont[0] – é o valor 1, o conteúdo do endereço 116, ou seja, x[0] e o
mesmo valor pode ser obtido com a instrução
 *pont[1] – é o valor 4, o conteúdo do endereço 128, ou seja, y[0];
 *pont[2] – é o valor 7, o conteúdo do endereço 140, ou seja, z[0];
 *pont[3] – é o valor 10, o conteúdo do endereço 152, ou seja, w[0].

Esse exemplo é ilustrado na Figura 26.

Figura 26 – Vetor de ponteiros do tipo inteiro

16
Figura 27 – Exemplo de vetor de ponteiros

A Figura 28 mostra a saída do algoritmo da Figura 27 após a sua


execução.

Figura 28 – Saída do algoritmo

TEMA 5 – PASSAGEM DE PARÂMETROS POR REFERÊNCIA

Uma das vantagens obtidas com ponteiros é a possibilidade de alterar o


valor de variáveis que estão lugares diferentes do programa. O asterisco é
utilizado para indicar que as variáveis são ponteiros e guardam o endereço de
outras variáveis simples na memória.
Portanto, o conteúdo destas variáveis simples também pode ser
modificado diretamente na memória quando passados seus endereços por meio

17
dos ponteiros para uma função, ou seja, as alterações dos dados sofridas dentro
da função também serão sentidas fora dela (Mizrahi, 2008).
O código na Figura 29 mostra a implementação da passagem de
parâmetros por referência.

Figura 29 – Passagem de parâmetros por referência

A Figura 30 mostra a saída do algoritmo acima após a sua execução.

Figura 30 – Saída do algoritmo

No algoritmo da Figura 29, temos os seguintes passos:

1. Na linha 8, declara-se a variável ‘a’ com o valor 8;


2. Na linha 11, a função printf() imprime o valor da variável ‘a’ antes do seu
endereço ser passado para a função;

18
3. Na linha 13, a instrução soma_mais_1(&a) recebe o endereço da variável
‘a’;
4. Na linha 22, a função soma_mais_1(*num) altera diretamente o dado na
memória.
5. Na linha 15, a função printf() imprime o valor da variável ‘a’ depois que a
função soma_mais_1(*num) foi executada.

Esses efeitos não ocorrem quando os parâmetros são passados por valor
(sem o uso do asterisco ‘*’ e o operador ‘&’), em que uma cópia do dado é
passada como parâmetro para a função e a variável origem não sofre qualquer
alteração.

Figura 31 – Exemplo de passagem de parâmetros por referência

A Figura 32 mostra a saída do algoritmo acima após a sua execução.

19
Figura 32 – Saída do algoritmo

FINALIZANDO

Nesta aula, aprendemos os principais conceitos e temas das abordagens


sobre ponteiro e sua aplicação em algoritmos computacionais; vimos como um
dado é acessado na memória; bem como sua relação com vetores e funções.

20
REFERÊNCIAS

ASCENCIO, A. F. G. Fundamentos da programação de computadores:


Algoritmos, Pascal, C/C++ (padrão ANSI) JAVA. 3. ed. São Paulo: Pearson,
2012.

MIZRAHI, V. V. Treinamento em linguagem C. 2. ed. São Paulo: Pearson,


2008.

21
LINGUAGEM DE
PROGRAMAÇÃO
AULA 3

Prof. Sandro de Araujo


CONVERSA INICIAL

Essa aula teve como base o livro Treinamento em Linguagem C, de


Viviane Victorine Mizrahi. Em caso de dúvidas ou aprofundamento consulte-o.
A aula apresenta a seguinte estrutura de conteúdo:

1. Struct;
2. Union;
3. Enum;
4. Typedef;
5. Typedef e struct.

O objetivo dessa aula é conhecer os principais conceitos de struct,


union, enum e typedef na linguagem de programação C, e representá-los
facilmente em diversos algoritmos para resolver problemas computacionais.

TEMA 1 – STRUCT

Uma struct pode ser compreendida como um conjunto de variáveis


referenciadas pelo mesmo nome, sendo que cada uma delas pode ter um
mesmo tipo de dado, ou então vários.
A ideia básica por trás da uma struct é criar uma variável que contenha
várias outras variáveis, ou seja, estamos criando uma variável que contém
dentro de si outras variáveis.
Na linguagem de programação C podemos declarar tipos de variáveis
como:

 Tipos básicos: char, int, float, double;


 Exemplo: int x; float y;
 Tipos compostos homogêneos: array;
 Exemplo: int x[5]; char nome[25].

Além dos tipos de dados mencionados acima a linguagem de


programação C permite a criação dos nossos próprios tipos de variáveis e um
desses tipos é a estrutura ou struct.

2
A struct segue a seguinte sintaxe:

struct <nome_da_struct>
{
<tipo 1> e <variável 1>;
<tipo 2> e <variável 2>;
<tipo 3> e <variável 3>;
...
<tipo n> e <variável n>;

}
struct < nome_da_struct > <nome_variavel>;

Exemplo de declaração de uma struct:

Figura 1 – Declaração de uma struct

A Figura 2 mostra o algoritmo acima de forma detalhada.

Figura 2 – Declaração de uma struct

No exemplo acima temos o nome da struct (cadastroDeAluno), os


membros que compõem a estrutura (char nome[40], char disciplina[20], float
nota1 e float nota2) e a variável que vai usar a struct (aluno). Dizemos que a
variável aluno é do tipo cadastroDeAluno (struct cadastroDeAluno aluno). A
Figura 3 traz um algoritmo com um exemplo de uma struct.

3
Figura 3 – Algoritmo com struct

A Figura 4 mostra a saída do algoritmo acima após a sua execução:

4
Figura 4 – Saída do algoritmo

TEMA 2 – UNION

Com a union pode-se criar variáveis capazes de suportar dados


diferentes, alocados no mesmo espaço de memória, em momentos diferentes.
Isto é, a union permite que um conjunto de variáveis compartilhem o mesmo
espaço na memória
Declara-se uma union de forma muito semelhante à uma struct; estas só
se diferem no aspecto da struct ser alocada com espaço suficiente para todos
os objetos, e o union só aloca espaço para o maior objeto que o compõe. Esse
espaço alocado é suficiente para armazenar o maior dos seus membros.
Portanto, uma union define um conjunto de membros que serão
armazenados numa porção compartilhada da memória, isto é, apenas um
membro será armazenado de cada vez.
A sintaxe da union é similar à da struct , conforme mostrado a seguir:

union <nome_da_union>
{
<tipo 1> e <variável 1>;
<tipo 2> e <variável 2>;
<tipo 3> e <variável 3>;
...
<tipo n> e <variável n>;
};

A declaração começa com a palavra union seguida do identificador da


união. A primeira linha da declaração indica ao compilador que este é um novo

5
tipo de dados para ser usado em outras partes do programa. Após as chaves,
declara-se as variáveis que vão compor a union.
No exemplo da Figura 5 vamos trabalhar com números, porém não
sabemos se os dados serão do tipo int ou float. Portanto, declara-se um novo
tipo de dados, que iremos chamar de numeroFlex, capaz de armazenar valores
de um dos dois tipos:

Figura 5 – Declaração de uma union

Ao acessar uma variável de tipo union, precisamos também indicar qual


a variável que desejamos acessar. A sintaxe é a mesma que aquela para o
acesso de membros em uma struct. Conforme exemplo na Figura 6 que vai
declarar e atribuir um valor inteiro à variável num:

Figura 6 – Declaração e atribuição de uma union

Para atribuir um valor real à variável num usou-se a seguinte instrução:

num.num2 = 4.0;

A particularidade da union está no fato de que as variáveis num.num1 e


num.num2 vão compartilhar a mesma posição na memória, e a atribuição de
um valor fracionário sobrescreve o valor inteiro e vice-versa. Portanto, não há
uma forma de “consultar” uma union e sobre qual tipo ela está armazenando
em um dado momento. Tampouco existe uma forma para o programa decidir
automaticamente a que variável da união ele deve atribuir um valor. Este
controle fica por conta do programador. A Figura 7 ilustra o compartilhamento
de espaço na memória.

6
Figura 7 – Compartilhamento da memória entre variáveis

A Figura 7 representa que se aloca a quantidade de armazenamento


ocupada pelo maior membro da union. Com isso, a memória economiza
espaço de armazenamento.
No próximo exemplo, na Figura 8, vamos trabalhar com um inteiro e um
caractere. Para esse algoritmo declara-se um novo tipo de dado que iremos
chamar de totalFlex, capaz de armazenar valores de um dos dois tipos (inteiro
e caractere).

Figura 8 – Union com um inteiro e um caractere

Uma union torna o código fonte um pouco confuso e, portanto, deve ser
utilizada em momentos em que as variáveis não sejam executadas no mesmo
momento.
Para um melhor entendimento vejamos o algoritmo em que ele imprime
o tamanho de uma union e struct, ambas com os mesmos tipos de variáveis
(Figura 9).

7
Figura 9 – Compartilhamento da memória entre variáveis

A Figura 10 mostra a saída do algoritmo acima após a sua execução:

Figura 10 – Compartilhamento da memória entre variáveis

No exemplo da Figura 10 podemos verificar que a union ocupou um


terço do espaço ocupado pela struct com o método de compartilhamento de
espaço.

8
TEMA 3 – ENUM

Enum ou enumeração, é um tipo de dado definido pelo usuário, com o


uso de uma lista de identificadores. Os identificadores podem ser vistos como
uma lista de constantes, onde cada constante tem um nome significativo.
A enum segue a seguinte sintaxe:

enum <nome_da_enum>{lista_de_identificadores};

Assim como em um vetor, uma estrutura enum também começa com o


valor zero (0). Para adequar uma enum em um conjunto de constantes que
referencie os meses do ano instanciamos o valor um (1) ao mês de janeiro,
conforme o exemplo mostrado na Figura 11.

Figura 11 – Declaração e atribuição de uma enum

A Figura 12 apresenta um exemplo com os dias da semana. Para esse


exemplo não inicializamos nenhum membro da enum.

Figura 12 – Compartilhamento da memória entre variáveis

A Figura 13 mostra a saída do algoritmo acima após a sua execução:

9
Figura 13 – Compartilhamento da memória entre variáveis

O identificador quarta apresentou o valor três (3). Para resolver esse


problema devemos definir no enum semana uma enumeração em que é
atribuído um identificador para a primeira constante com valor um (1); a enum,
portanto, vai incrementar com o valor um (1) de forma sequencial os demais
identificadores até o fim da lista. Com isso, a lista terá os valores de um a sete
para os dias da semana.
Para melhor entendimento vejamos um algoritmo, na Figura 14, que vai
executar o mesmo exemplo com dois identificadores instanciados.

Figura 14 – Operações com identificadores

10
A Figura 15 mostra a saída do algoritmo acima após a sua execução.

Figura 15 – Operações com identificadores

O exemplo da Figura 9 – o identificador quinta – também foi instanciado.


Repare ainda que na Figura 10 o identificador sexta seguiu a sequência,
apresentando o valor nove (9).
Podemos também atribuir valores da tabela ASCII para enumeração,
conforme mostrado no algoritmo da Figura 16.

Figura 16 – Escapes com identificadores

A Figura 17 mostra a saída do algoritmo acima após a sua execução:

11
Figura 17 – Escapes com identificadores

A Figura 18 traz outro exemplo com estrutura enum.

Figura 18 – Operações com identificadores

A Figura 19 mostra a saída do algoritmo acima após a sua execução:

12
Figura 19 – Operações com identificadores

TEMA 4 – TYPEDEF

O comando typedef é usado para criar “sinônimo” ou um “alias” para


tipos de dados existentes. Na prática podemos dizer que estamos renomeando
um tipo de dados. Essa renomeação de tipos facilita a organização e o
entendimento do código. Sintaxe:

typedef <nome do tipo de dado> <novo nome>;

É importante ressaltar que o comando typedef não cria um novo tipo. Ele
apenas permite que um tipo existente seja denominado de uma forma
diferente, de acordo com a especificação desejada pelo programador,
conforme mostrado no algoritmo da Figura 20:

Figura 20 – Renomeação de tipo de variável com typedef

13
A Figura 21 mostra a saída do algoritmo acima após a sua execução:

Figura 21 – Renomeação de tipo de variável com typedef

TEMA 5 – TYPEDEF E STRUCT

É muito frequente o uso de typedef para criar apelidos a fim de tornar os


nomes mais curtos, desta forma podemos representar uma estrutura usando
apenas seu sinônimo (Figura 22).

Figura 22 – Typedef e struct

14
A Figura 23 mostra a saída do algoritmo acima após a sua execução:

Figura 23 – Typedef e struct

A Figura 24 traz outro exemplo de typedef com struct.

Figura 24 – Typedef e struct

A Figura 25 mostra a saída do algoritmo acima após a sua execução:

Figura 25 – Typedef e struct

15
FINALIZANDO

Nesta aula aprendemos os principais conceitos referente a struct, union,


enum e typedef na linguagem de programação C, e também como representá-
los facilmente em algoritmos nas diferentes estruturas para resolver problemas
computacionais.
Aproveite a disciplina e bons estudos!

16
REFERÊNCIAS

MIZRAHI, V. V. Treinamento em Linguagem C. [S.l.]: Edição da autora, 2008.

17
LINGUAGEM DE
PROGRAMAÇÃO
AULA 4

Prof. Sandro de Araújo


CONVERSA INICIAL

Esta aula teve como base os livros: Lógica de programação e estruturas


de dados, Lógica de programação algorítmica e Treinamento em Linguagem C.
Em caso de dúvidas ou se desejar aprofundamento, consulte-os em nossa
Biblioteca Virtual Pearson.
O objetivo desta aula é conhecer os principais conceitos e aplicações de
ponteiros em struct, struct de ponteiros, struct com funções e alocação dinâmica
de memória com as funções calloc(), free(), malloc() e realloc(), na linguagem C,
para resolver problemas computacionais.

TEMA 1 – PONTEIRO: STRUCT

Na linguagem C, uma struct é uma coleção de variáveis referenciada pelo


mesmo nome, conhecida também como tipo de dado agregado. Uma vez que
variáveis do tipo estrutura são tratadas exatamente da mesma forma que
variáveis de tipos básicos, é possível definir variáveis do tipo ponteiro para
estruturas (Mizrahi, 2008), conforme a seguinte sintaxe:

1. struct <nome_da_struct> *<nome_do_ponteiro>;

Um componente ou membro de uma estrutura é uma variável, e pode ser


usado para guardar o endereço de outra variável. Um exemplo disso são as listas
ligadas, que têm em cada nó um ponteiro para o próximo nó.
Para passar um endereço de uma variável, basta colocar o símbolo ‘*’
antes da definição do seu ponteiro e o operador ‘&’ – operador unário que retorna
o endereço na memória de seu operando, na chamada da struct. O uso do
asterisco indica o componente da struct pode ser modificado diretamente na
memória (Mizrahi, 2008).
Uma vez que variáveis do tipo struct são tratadas exatamente da mesma
forma que variáveis simples, é possível definir variáveis do tipo ponteiro para
struct, conforme mostrado no exemplo abaixo:

1. struct calendario{

2. int dia;
3. int mes;
4. int ano;

2
5. }; struct calendario agora, *depois; // declara o ponteiro ‘depois’
6. depois = &agora; // Coloca o endereço no ponteiro ‘depois’

Os componentes individuais de uma struct podem ser acessados como


qualquer variável, desde que se use o operador ‘.’ ponto, entre o nome da
estrutura (instância) e o nome do componente. Portanto, para acessar os
componentes da struct calendario, basta usar o ponteiro *depois, entre
parênteses, junto com a variável de referência da struct, separado por um ‘.’
(Mizrahi, 2008):

1. (*depois).dia = 28;

2. (*depois).mes = 09;
3. (*depois).ano = 2018;

Os componentes da struct podem ser impressos com a mesma sintaxe,


conforme o exemplo:

1. printf("%i/%i/%i\n\n", (*depois).dia, (*depois).mes, (*depois).ano);

A linguagem de programação C, também usa uma notação simplificada


para substituir a forma do exemplo acima, substituímos (*depois).dia por
depois->dia. O operador ‘->’ (seta) é usado para substituir o operador ‘.’ (ponto),
eliminando também a necessidade de usar o ponteiro entre os parênteses. A
Figura 1 apresenta um algoritmo com a junção dos exemplos acima
apresentados.

3
Figura 1 – Ponteiro para uma Struct

O operador seta ‘->’ facilita, resume e evita erros na compilação no acesso


aos componentes da struct. Pois, para essa lógica, a expressão “*depois.dia”,
sem o uso de parênteses, apresenta um erro sintático, porque o compilador dá
prioridade de execução ao operador ‘.’. O compilador primeiro executa o
operador ‘.’ e depois o operador ‘*’. O algoritmo da Figura 1 apresenta os
seguintes passos:

1. Entre as linhas seis e dez, criamos uma struct “calendário” com três
componentes do tipo inteiro “dia”, ”mês” e “ano”;

2. Na linha seis, criamos a variável “agora”, juntamente com o ponteiro


“*depois”;
3. Na linha 12, é atribuído o endereço de endereço de “agora” para o
ponteiro “depois”;
4. Nas linhas 19, 20 e 21, os componentes da struct são instanciados com o
uso de ponteiros;

4
5. Na linha 25, os componentes são impressos na tela do usuário e os
acessos aos dados se dá por meio dos ponteiros.

A Figura 2 mostra a saída do algoritmo do algoritmo mostrado na Figura


1, após a sua execução.

Figura 2 – Saída do algoritmo

TEMA 2 – STRUCT DE PONTEIROS

Ponteiros também podem ser definidos como componentes de estruturas.


Basta colocar o operador ‘*’ asterisco antes dos componentes de uma struct
(Mizrahi, 2008):

1. struct calendario{ //Struct “calendário”

2. int *dia; //Ponteiro “dia”


3. int *mes; //Ponteiro “mês”
4. int *ano; //Ponteiro “ano”
5. }; struct calendario atual; //Variável de referência “atual”

As instruções acimam declaram uma struct com o nome calendario que


apresenta três componentes ponteiros. Além disso, também foi declarada uma
variável com o nome atual para referenciar a struct calendario.
As inicializações dos ponteiros podem ser declaradas conforme as
instruções mostradas abaixo:

1. atual.dia = &diaSetembro;

2. atual.mes = &mesSetembro;
5
3. atual.ano = &anoSetembro;

Para imprimir as variáveis, usando os componentes da struct, que por sua


vez são os ponteiros que as referenciam, procede-se conforme o exemplo:

1. printf("Dia = %i\n", *atual.dia);

A Figura 3 mostra um algoritmo com a junção dos exemplos acima


apresentados.

Figura 3 – Struct de ponteiros

O algoritmo da Figura 3 apresenta os seguintes passos:

1. Entre as linhas seis e dez. criamos uma struct “calendário” com três
ponteiros “*dia”, “*mês” e “*ano”;

2. Nas linhas 12, 13 e 14, criamos três variáveis com suas instancias;
3. Nas linhas 16, 17 e 18, atribui-se os endereços das variáveis para os
ponteiros:

6
4. Nas linhas 20, 21, 22, 23, 24 e 25, são impressos os endereços das
variáveis e o apontamento dos ponteiros;
5. Na linha 27, são impressos os conteúdos das variáveis usando os
ponteiros da struct.

A Figura 4 mostra a saída do algoritmo apresentado na Figura 3, após a


sua execução:

Figura 4 – Saída do algoritmo

TEMA 3 – STRUCT: FUNÇÃO

Assim como uma variável, uma struct também pode ser passada como
parâmetro para uma função; essa passagem é feita de duas formas: por valor e
por referência (Mizrahi, 2008).

3.1 Passagem de uma struct por valor

Uma struct é tratada com uma variável comum, e a passagem por valor é
feita por meio da passagem de uma cópia do seu componente para uma função.
Na passagem por valor, uma cópia do componente da struct é usada e alterado
dentro da função sem afetar a variável da estrutura, na memória da qual ela foi
gerada (Mizrahi, 2008).

7
Figura 5 – Passagem de struct por valor

O algoritmo da Figura 5 apresenta os seguintes passos:

1. Entre as linhas 4 e 6, criamos uma struct “p_valor” com dois componentes


‘a’ e ‘b’;

2. Na linha 12, os componentes da struct são instanciados;


3. Nas linhas 15 e 16, são impressos os valores dos componentes antes da
execução da função “imprimir_valor()”;
4. Nas linhas 19 e 20, são feitas duas chamadas para a função
“imprimir_valor”, que traz como resultados a impressão dos valores da
struct processados;
5. Nas linhas 23 e 24, são impressos os valores dos componentes depois
da execução da função “imprimir_valor()”;
6. Entre as linhas 30 e 34, é criado a função “imprimir_valor()”.

A Figura 6 mostra a saída do algoritmo da Figura 5 após a sua execução.

8
Figura 6 – Saída do algoritmo

No exemplo acima, os componentes da struct foram instanciados na linha


12 com a instrução “struct p_valor pont1 = {201,302};”, alterados com a função
“void imprimir_valor(int num)”. Após a execução da função, os valores antes
instanciados continuaram os mesmos, sem alterações na memória.

3.2 Passagem de uma struct por referência

Para que a passagem de um parâmetro seja por referência, basta colocar


o símbolo “*” antes da sua definição dos parâmetros formais e o operador “&”1,
na chamada do parâmetro (Mizrahi, 2008). O uso do asterisco indica que esses
parâmetros podem ser modificados dentro da função, ou seja, as alterações dos
parâmetros sofridas dentro da função também serão sentidas fora dela. Esses
efeitos não ocorrem quando os parâmetros são passados por valor (sem o uso
do asterisco (*)). Veja o exemplo na Figura 7, com a passagem por referência
dos valores de uma struct para uma função.

1 O operador unário & retorna o endereço na memória de seu operando (MIZRAHI,


2008).
9
Figura 7 – Passagem de struct por referência

O algoritmo da Figura 7 apresenta os seguintes passos:

1. Entre as linhas 7 e 9, criamos uma struct “p_valor” com dois componentes


‘a’ e ‘b’;

2. Na linha 9, criamos a variável “x”, que vai referenciar a struct “p_valor”,


juntamente com o ponteiro “*px”;
3. Na linha 11, o ponteiro “px” recebe o endereço de ‘x’;
4. Nas linhas 13 e 14, os componentes da struct são instanciados;
5. Nas linhas 17 e 18, são impressos os valores dos componentes antes da
execução da função “imprimir_soma_valor()”;
6. Nas linhas 20 e 22, são feitas duas chamadas para a função
“imprimir_soma_valor”; como parâmetros, são passados os endereços
dos componentes da struct, que traz como resultado a impressão dos
valores processados;

10
7. Nas linhas 25 e 26, são impressos os valores dos componentes depois da
execução da função “imprimir_soma_valor()”;
8. Entre as linhas 33 e 36, é criada a função “imprimir_soma_valor()”.

A figura 8 mostra a saída do algoritmo após execução.

Figura 8 – Saída do algoritmo

Uma das vantagens de criar um ponteiro para uma struct é a possibilidade


de passar o seu endereço como um parâmetro para uma função. Por manipular
apenas o endereço do componente, a passagem por referência promove um
ganho de desempenho considerável no acesso à memória.

3.3 Ponteiro do tipo void

Um ponteiro também pode ser declarado como void, ou seja, um ponteiro


sem tipo definido. Um ponteiro void tem como objetivo armazenar endereço de
memória (Mizrahi, 2008). Um ponteiro do tipo void tem a seguinte sintaxe:

1. void *nome_do_ponteiro;

Se em determinado momento existir a necessidade de apenas guardar


um endereço, utiliza-se um ponteiro void, conforme mostra o algoritmo da Figura
9.

11
Figura 9 – Ponteiro do tipo void

O algoritmo da Figura 9 apresenta os seguintes passos:

1. Na linha 6, declaramos a variável ‘x’ com a instância 3;

2. Na linha 7, criamos um ponteiro “y” do tipo void que vai receber o endereço
de ‘x’;
3. Na linha 10, ‘z’ recebe o apontamento de ‘y’;
4. Na linha 11, z altera a instância de x;
5. Na linha 13, imprimimos a atual instância de ‘x’;
6. E nas linhas 14, 15 e 16, os endereços da variável e apontamentos,
respectivamente.

A Figura 10 mostra a saída do algoritmo após execução:

Figura 10 – Saída do algoritmo

12
TEMA 4 – ALOCAÇÃO DINÂMICA: CALLOC() E FREE()

A alocação dinâmica de memória é um mecanismo que reserva uma


quantidade de memória, em região conhecida como durante heap, durante a
execução de um programa (Mizrahi, 2008). Na biblioteca stdlib da linguagem C,
temos quatro funções para trabalhar com alocação dinâmica de memória:

1. calloc;

2. free;

3. malloc;

4. realloc.

A função calloc( ) tem como objetivo criar um vetor com tamanho


dinâmico. Essa função serve para alocar memória durante a execução do
programa. Ela faz o pedido de memória ao computador e retorna um ponteiro
com o endereço do início do espaço alocado. Um fato interessante dessa função
é que, após alocar a memória, ela preenche com zero todos os bits (Mizrahi,
2008). Para declarar uma função calloc( ), usa-se a seguinte sintaxe:

1. void* calloc(unsigned int nElementos, unsigned int tElemento)

Nela, nElementos é a quantidade de posições que queremos no vetor,


tElemento é o tamanho de cada posição de memória do nosso vetor, e unsigned
int é números inteiros sem sinais (só números inteiros positivos). Na alocação da
memória, devemos considerar o tamanho do tipo alocado. Vejamos um exemplo
na Figura 11 de um vetor de tamanho 40.

13
Figura 11 – Exemplo com a função calloc()

A principal dificuldade no exemplo da Figura 11 está em decorar o


tamanho de cada tipo enquanto estamos criando um algoritmo. Para isso,
podemos usar o operador sizeof, conforme o exemplo da Figura 12.

Figura 12 – Exemplo com a função calloc()

Caso a memória não tenha espaço suficiente para a alocação de espaço


retornará NULL, vejamos um exemplo na Figura 13 com mais detalhes do uso
da função calloc().

14
Figura 13 – Implementando a função calloc()

A função free( ) libera o espaço de memória que foi previamente alocado.


Essa função recebe um ponteiro, que foi usado para receber o endereço do bloco
de memória alocada, e não retorna nada (Mizrahi, 2008). Para declarar uma
função free( ), usa-se a sintaxe mostrada abaixo:

1. void free(void *nomePonteiro);

Se um projeto for mais simples, vai precisar liberar ao final de sua


execução. É uma boa prática de programação liberar o que foi alocado antes da
aplicação terminar sua execução, conforme mostra a Figura 14.

15
Figura 14 – Função free()

Na Figura 15 temos esse mesmo exemplo, explicando cada etapa do


processo no algoritmo.

16
Figura 15 – Função free()

TEMA 5 – ALOCAÇÃO DINÂMICA: MALLOC() E REALLOC ()

A função malloc() é bem parecida com a função calloc(). Ela também aloca
um espaço de memória e retorna um ponteiro do tipo void para o início do espaço
de memória alocado. Mas há uma grande diferença: essa função não coloca zero
nos bits do espaço alocado (Mizrahi, 2008). Para declarar uma função malloc( ),
usa-se a sintaxe mostrada abaixo:

1. void* malloc(unsigned int nElementos);

A função malloc() recebe por parâmetro a quantidade de bytes que será


alocada na memória, tendo também o valor NULL, caso apresente algum erro,
ou o ponteiro para a primeira posição do vetor, caso tenha sucesso na locação.
Vejamos um exemplo na Figura 15.

17
Figura 16 – Função malloc().

Do mesmo modo, fica complicado toda vez ter que ficar contando Bites
para cada tipo e para resolver esse problema implementamos o operador sizeof.
Vejamos o exemplo da Figura 17.

Figura 17 – Função malloc() com sizeof

Agora vejamos um exemplo, na Figura 17, tratando o retorno NULL da


função malloc().

18
Figura 18 – Função malloc() com tratamento do retorno NULL

A função realloc( ) aloca e realoca um espaço na memória durante a


execução do programa. Essa função realiza um pedido de memória e retorna um
ponteiro com o endereço do início do espaço de memória alocado (Mizrahi,
2008).
Para declarar uma função realloc( ), usa-se a sintaxe mostrada abaixo:

1. void* realloc(void* nomePonteiro, unsigned int nElementos);

Essa função recebe por parâmetro um ponteiro para um bloco de memória


já alocada, na nossa sintaxe o *nomeponteiro, e a quantidade de Bytes a ser
alocada, nElementos e retorna NULL em caso de erros ou um ponteiro para a
primeira posição do vetor em caso de sucesso. Vejamos um exemplo na Figura
19 com mais detalhes do uso da função realloc().

19
Figura 19 – Função realloc()

A realocação da memória também leva em conta o tamanho do tipo.


Assim como nas funções anteriores, usamos o operador sizeof para calcular o
tamanho no momento da execução da função (Mizrahi, 2008), conforme
mostrado no exemplo da Figura 20.

Figura 20 – Função realloc() com sizeof

FINALIZANDO

Nesta aula, aprendemos os principais conceitos e aplicações de ponteiros


em struct, struct de ponteiros, struct com funções e alocação dinâmica de
memória com as funções calloc(), free(), malloc() e realloc(), na linguagem C,
para resolver problemas computacionais. Aproveite a disciplina e bons estudos!

20
REFERÊNCIAS

MIZRAHI, V. V. Treinamento em Linguagem C. 2. ed. São Paulo: Pearson,


2008.

21
LINGUAGEM DE
PROGRAMAÇÃO
AULA 5

Prof. Sandro de Araújo


CONVERSA INICIAL

Esta aula teve como base os livros: Lógica de programação e estruturas


de dados, Lógica de programação algorítmica e Treinamento em Linguagem C.
Em caso de dúvidas ou se desejar aprofundamento, consulte-os em nossa
Biblioteca Virtual Pearson.
O objetivo dessa aula é conhecer os principais conceitos e aplicações de
recursividade, iteração, função recursiva, função recursiva com vetor e função
macro com a diretiva #define. Vamos também utilizar linguagem C para a criação
de algoritmos.

TEMA 1 – RECURSIVIDADE

Recursividade ou recursão é quando uma função chama ela mesma para


resolver um problema. E como funciona a recursividade?
De um modo geral, a recursividade é considerada como um processo
repetitivo de uma rotina (procedimento ou função), que faz uma chamada para
ela mesma. Por conseguinte, se essa função realiza essa chamada inúmeras
vezes, é necessário tomar muito cuidado com a quantidade de repetições no
processo. Quando não controlada, será executada de forma infinita, que é o que
conhecemos como loop eterno. Um loop eterno ou infinito ocorre quando um
bloco do código repete a instrução descontroladamente, sobrecarregando a
memória e ocasionando o travamento de todo o sistema.
Para evitar que um loop seja executado de uma infinitamente, é
necessário definir uma condição que vai parar o processo. Para definir a
condição de terminação, são necessárias análise e avaliação detalhadas do
problema que será resolvido de forma recursiva, entendendo como a rotina
deverá terminar.
Uma rotina recursiva pode ser escrita de duas formas:

 Recursão Direta – É uma rotina composta por um conjunto de instruções,


e uma dessas instruções faz a chamada para a rotina. A rotina X chama
a própria rotina X;
 Recursão Indireta – É uma rotina que contém uma chamada a outra rotina
que tem uma chamada a outra rotina e assim sucessivamente. A rotina X
chama uma rotina Y, que por sua vez chama X.
Uma rotina recursiva é chamada para resolver um problema. Ela sabe
como resolver somente a "parte" mais simples, o "caso" mais trivial. Portanto, a
solução para um problema recursivo normalmente pode ser dividida em uma
solução que é trivial, e outra solução mais geral.
Dessa forma, a recursão aplica uma técnica chamada dividir para
conquistar. Isto é, dividir uma ou mais versões menores do mesmo problema.
Exemplo: se o problema é muito grande, mesmo dividindo-o em dois, ainda
continuará grande. Podemos dividir as partes em dois novamente; até chegar
em algo mais simples e fácil de resolver.
No caso da recursão, o processo vai sendo dividido em partes, até chegar
em uma fração que ela vai saber resolver. Dessa forma, temos:

 Caso Trivial ou Condição de Parada – O problema é facilmente resolvível


e retorna um resultado; normalmente é o caso mais simples, ou básico,
do problema, inclusive nesse caso não haverá a necessidade de aplicar
recursão.
 Caso Geral – O problema é, em sua essência, igual ao problema original,
porém deve ser uma versão mais genérica. Como esse novo problema é
parecido com o original, a rotina chama uma nova cópia de si mesma para
trabalhar no problema menor.
Enquanto for necessário dividir o problema em problemas menores, a
rotina recursiva continuará chamando a si mesma, para continuar dividindo, até
chegar no caso mais básico. Quando isso ocorre, a função para de dividir e
começa a gerar os resultados. Todos os dados de todas as variáveis envolvidas
na função recursiva devem ser guardados a cada chamada. Isso significa que
uma pilha de chamadas da função deve ser criada.
Exemplos de recursividade em nossa vida:

 Caracóis;
 Girassóis;
 As folhas de algumas árvores;
 Dois espelhos quando apontados um para o outro.
O algoritmo passo a passo abaixo ilustra o processo recursivo descrito até
aqui:

 Terminou?

 Se sim:

 Retorne o resultado.

 Se não:

3
 Divida o problema;
 Resolva o(s) problema(s);
 Monde os resultados na solução do problema original;
 Retorne à solução.

TEMA 2 – RECURSÃO X ITERAÇÃO

A grande dúvida está em saber quando devemos usar recursão ou


iteração em nosso algoritmo. A regra é: se dá pra resolver o problema com
iteração, é possível resolver o mesmo problema com recursão. Porém, é
necessário, antes de qualquer coisa, compreender bem a diferença entre
recursão e iteração, para depois decidir qual delas utilizar em seu código.
A repetição é uma característica tanto da recursão quanto da iteração. A
iteração utiliza a repetição em forma de laços ou estruturas de repetição (for,
while, do-while), enquanto a recursão utiliza a repetição na forma de chamadas
para ela mesma. As duas formas precisam de uma condição para terminar o
ciclo repetitivo – um teste de terminação. A iteração se encerra quando a
condição de teste falha e a recursão se encerra quando se alcança o caso trivial.
As duas formas podem ingressar em loop infinito; no contexto da iteração, se o
teste jamais se tornar falso, o laço vai se repetir eternamente, enquanto no
contexto da recursão, se o problema não for reduzido de forma que se converta
para o caso trivial, e não tenha a condição de parada definida, o laço vai se
repetir até sobrecarregar a memória.
As funções recursivas são muito utilizadas em robótica, automação e
inteligência artificial. Estruturas de Dados Dinâmicas, como Árvores, Filas, Pilhas
e Listas, também fazem uso da recursão.
A partir do momento em que fique clara a diferença e o emprego dessas
duas formas de resolver problemas algorítmicos, teremos mais clareza na hora
de decidir qual melhor se enquadra na lógica que está sendo desenvolvida.
Portanto, analise o tempo computacional que será gasto com uma função
recursiva e veja se vale ou não a pena – se o gasto for muito alto, implemente a
mesma função de forma iterativa.

4
TEMA 3 – FUNÇÃO RECURSIVA

As funções recursivas geralmente tornam o programa mais legível. Elas


são definidas como na Matemática para evitar a retribuição de valores a
variáveis. Essas funções podem substituir trechos de código que envolvem laços
de repetição, como os laços de repetições while e for.
Dizemos que uma função é recursiva quando, dentro do corpo de uma
função, se faz uma chamada para a própria função, conforme mostra a Figura 1.

Figura 1 – Exemplo de função recursiva

Internamente, quando qualquer chamada de função é feita dentro de um


programa, é criado um Registro de Ativação na Pilha de Execução do programa.
Esse registro de ativação armazena os parâmetros e as variáveis locais da
função, bem como o “ponto de retorno” no programa ou subprograma que
chamou essa função. Ao final da execução da função, o registro é desempilhado
e a execução volta ao subprograma que chamou a função, conforme a Figura 2.

Figura 2 – Armazenamento da função recursiva em uma pilha de função


recursiva

Quando a rotina começa a retornar os resultados das chamadas, começa


o desempilhamento das chamadas da função na pilha, que retornarão ao
5
solicitante o agrupamento das camadas, até que se forme o resultado final. À
medida que são desempilhados, libera-se os valores da memória.
Quando optamos por usar uma função recursiva para resolver um
problema, precisamos saber como fazê-la parar e evitar o loop infinito que
estudamos na aula anterior.
Para um melhor entendimento, vejamos alguns exemplos de algoritmos
sem recursividade, e na sequência o mesmo algoritmo com recursividade.
Vejamos o Exemplo 1, sem recursividade. O algoritmo apresentado na
Figura 3 vai imprimir os números de 1 a 20 usando uma função iterativa com o
laço de repetição for.

Figura 3 – Algoritmo sem função recursiva

A Figura 4 mostra a saída do algoritmo mostrado na Figura 3, após a sua


execução.

6
Figura 4 – Saída do algoritmo

O algoritmo mostrado na Figura 5 vai imprimir os números de 1 a 20


usando o laço de repetição for e a função recursiva imprimeN().

Figura 5 – Algoritmo com função recursiva

A Figura 6 mostra a saída do algoritmo mostrado na Figura 5, após a sua


execução.
7
Figura 6 – Saída do algoritmo

Um algoritmo com recursividade torna a escrita mais simples e elegante,


tornando-o fácil de entender e de manter. Porém, se o loop recursivo for muito
grande, o consumo de recursos será proporcional, podendo sobrecarregar a
memória, pois cada chamada recursiva aloca espaço na memória para as
variáveis e parâmetros. Na maioria dos casos, com uma solução iterativa
(usando laço de repetição) gasta-se menos memória, tornando o algoritmo mais
eficiente, na performance do código, do que usar recursividade.

TEMA 4 – FUNÇÃO RECURSIVA COM VETOR

Na recursividade com vetor, usamos uma chamada de uma função,


passando um elemento de um array como parâmetro; depois, dentro dessa
função, fazemos uma nova chamada para ela mesma. Isso é, funciona do
mesmo modo que uma função recursiva simples, só que, em vez do parâmetro
ser uma variável, agora será um vetor. A Figura 7 traz um algoritmo sem função
recursiva usando vetor e a Figura 9 traz o mesmo exemplo com função recursiva
também usando vetor.

8
Figura 7 – Algoritmo sem função recursiva

A Figura 8 mostra a saída do algoritmo mostrado na Figura 7, após a sua


execução.

Figura 8 – Saída do algoritmo

O algoritmo mostrado na Figura 9 traz a função recursiva exibir().

9
Figura 9 – Algoritmo com função recursiva

A Figura 10 mostra a saída do algoritmo mostrado na Figura 9, após a sua


execução.

Figura 10 – Saída do algoritmo

10
Passamos agora ao Exemplo 2, sem recursividade.

O algoritmo mostrado na Figura 11 vai imprimir a soma dos elementos do


vetor usando o laço de repetição for.

Figura 11 – Algoritmo sem função recursiva

A Figura 12 mostra a saída do algoritmo mostrado na Figura 11, após a


sua execução.

Figura 12 – Saída do algoritmo

O algoritmo mostrado na Figura 13 vai imprimir a soma dos elementos do


vetor usando o laço de repetição for e usando a função recursiva somaVetor().

11
Figura 13 – Algoritmo com função recursiva

A Figura 14 mostra a saída do algoritmo mostrado na Figura 13, após a


sua execução.

Figura 14 – Saída do algoritmo

12
TEMA 5 – FUNÇÕES MACRO – DIRETIVA #DEFINE

Neste capítulo, estudaremos os diversos usos da diretiva #define.


Basicamente, uma diretiva avisa para o compilador que ele deve procurar todos
os eventos de determinada expressão e substituir por outra na compilação do
programa. Isso permite criar o que chamamos de funções macro.
Uma função macro é um tipo de declaração de função em que são
informados o nome e os parâmetros da função como sendo o nome da macro e
o trecho de código semelhante a ser aplicado na substituição.

5.1 Diretiva #define

A diretiva #define associa um identificador a uma cadeia de caracteres de


token. Após a definição da macro, o compilador pode substituir a cadeia de
caracteres de token em cada ocorrência do identificador no arquivo de origem.
A diretiva #define permite três sintaxes.
A primeira sintaxe #define nome_da_macro. Nela, define-se um nome
que que será ser usado em alguma estrutura no código. Exemplo: nome para
ser testado em estruturas condicionais.
Para associar a diretiva #define com uma estrutura condicional, usamos
outra diretiva chamada #ifdef. Nos próximos exemplos, Figura 15 e Figura 17,
os algoritmos vão checar se o a definição status realmente existe. Caso exista
a definição status os algoritmos executa a instrução “printf("O Status
existeeeee!!!! Uhuuuuu!!!\n\n");”, caso contrário, ele executará a instrução
“printf("Status NAO definido. O #define FOI DECLARADO?\n\n");”. Vejamos
exemplos apresentados na Figura 15 e Figura 17, sem e com #define
respectivamente.

13
Figura 15 – Algoritmo sem diretiva #define

A Figura 16 mostra a saída do algoritmo mostrado na Figura 15, após a


sua execução.

Figura 16 – Saída do algoritmo

Figura 17 – Algoritmo com diretiva #define

A Figura 18 mostra a saída do algoritmo mostrado na Figura 17, após a


sua execução.
14
Figura 18 – Saída do algoritmo

A segunda sintaxe #define nomeConstante valorConstante. Nela,


define-se um valor para uma constante que será usada ao longo do
desenvolvimento do algoritmo.
Essa sintaxe informa ao compilador que ele deve procurar todas as
ocorrências de “nomeConstante” e substituir por “valorConstante” no momento
da compilação do programa, conforme exemplo apresentado na Figura 19.

Figura 19 – Algoritmo com diretiva #define

A Figura 20 mostra a saída do algoritmo mostrado na Figura 19, após a


sua execução.

15
Figura 20 – Saída do algoritmo

A terceira sintaxe #define nome_da_macro(PARÂMETROS)


expressão. Ela é uma função macro, ou seja, um pedaço de código pelo qual
foi atribuído a um nome. Vejamos Figura 21.

Figura 21 – Algoritmo #define com função com expressão

A Figura 22 mostra a saída do algoritmo mostrado na Figura 21, após a


sua execução.

Figura 22 – Saída do algoritmo

16
5.2 Boas práticas da diretiva #define

A ideia é não apenas escrever um código funcional, mas sim usar as boas
práticas para ter um código limpo e de fácil entendimento. Um código mal escrito
dificulta a sua alteração por outros programadores, e é suscetível a erros graves
de compilação. Um código malfeito provoca um retrabalho desnecessário e
frustação para quem vai ter que reprogramá-lo.
Qualquer tecnologia pode ser usada para ajudar ou piorar de vez a
situação. Essa afirmação também se aplica quando usamos macro. Se bem
utilizada, pode facilitar muito a lógica do nosso algoritmo, melhorar a leitura do
código e otimizá-lo. Só que, se for mal utilizada, pode nos causar muita dor de
cabeça, na busca interminável pelos erros do código.
Cada recurso da linguagem de programação tem sua particularidade. As
boas práticas existem para que todos tomem os cuidados necessários, evitando-
se certos erros no desenvolvimento do algoritmo. Quando usamos macro, é
aconselhável sempre colocar, na sequência de substituição, os parâmetros da
macro entre parênteses. Isso serve para preservar a “precedência dos
operadores”, conforme exemplo apresentado na Figura 23.

Figura 23 – Exemplo de presidência na expressão

17
A Figura 24 mostra a saída do algoritmo mostrado na Figura 23, após a
sua execução.

Figura 24 – Saída do algoritmo

Deve-se sempre usar letras maiúsculas para declarar sua macro, seja ela
uma constante ou uma função. Esse padrão é conhecido pelos programadores
experientes e facilita a leitura e a manutenção do código. Vejamos o exemplo
apresentado na Figura 25.

Figura 25 – Exemplo de diretiva com letra maiúscula

18
A Figura 26 mostra a saída do algoritmo mostrado na Figura 25, após a
sua execução.

Figura 26 – Saída do algoritmo

Preste muita atenção quando for usar operadores unários dentro de


macros. Chamar a macros os operadores de incremento/decremento podem ser
um problema. Observe o exemplo na Figura 27.

Figura 27 – Exemplo de diretiva com operadores unitários

O compilador irá executar a macro conforme mostrado na Figura 28.

Figura 28 – Exemplo de diretiva com operadores unitários.

Se o “a” for maior que o “b”, o incrementado será executado mais de uma
vez.
É preciso atentar também à expansão de linhas em macro dentro de um
laço. A barra “\” em macro representa a quebra de linha e indica para o
compilador que a macro continuará na linha abaixo. O exemplo na Figura 29
representa um erro nesse contexto.

19
Figura 29 – Exemplo de um algoritmo sem as chaves no if

O código apresentado na Figura 29 não vai compilar porque a macro


possui mais de uma linha; quando executada no “if”, o compilador apresentará
erro. Se você colocar o bloco do “if” entre chaves “{ }”, resolverá o problema,
conforme mostrado na Figura 30.

Figura 30 – Exemplo de um algoritmo com as chaves no if

A Figura 31 mostra a saída do algoritmo mostrado na Figura 29, após a


sua execução.

20
Figura 31 – Saída do algoritmo

5.3 Diretiva #undef

É utilizada sempre que desejamos apagar a definição de uma macro da


tabela interna que as guarda. Em outras palavras, ela remove a definição de uma
macro para que ela possa ser redefinida.
A diretiva #undef segue a seguinte sintaxe: #undef nome_da_macro.
Vejamos exemplo mostrado na Figura 32.

Figura 32 – Exemplo de diretiva #undef

A Figura 33 mostra a saída do algoritmo mostrado na Figura 32, após a


sua execução.

21
Figura 33 – Saída do algoritmo

FINALIZANDO

Nesta aula, aprendemos os principais conceitos que envolvem as


aplicações de recursividade, iteração, função recursiva, função recursiva com
vetor e função macro com a diretiva #define. Trabalhamos também com a
linguagem C para a criação de algoritmos. Bons estudos!

22
REFERÊNCIAS

ASCENCIO, A. F. G. Fundamentos da Programação de Computadores:


Algoritmos, Pascal, C/C++ (padrão ANSI) JAVA. 3. ed. São Paulo: Pearson,
2012.

MIZRAHI, V. V. Treinamento em Linguagem C. 2. ed. São Paulo: Pearson,


2008.

PUGA, S.; RISSETTI, G. Lógica de programação e estruturas de dados. São


Paulo: Pearson, 2016.

23
LINGUAGEM DE
PROGRAMAÇÃO
AULA 6

Prof. Sandro de Araújo


CONVERSA INICIAL

Esta aula tem como base os livros: Lógica de programação e estruturas


de dados, Fundamentos da Programação de Computadores e Treinamento em
Linguagem C. Em caso de dúvidas, ou para aprofundamento dos temas,
consulte-os na biblioteca virtual.
A aula apresenta a seguinte estrutura de conteúdo:

1. Operações em memória - parte 1

2. Operações em memória - parte 2


3. Arquivos em C
4. Modos de abertura: read (r), write (w) e append (a)
5. Gravação e leitura de arquivos

O objetivo desta aula é apresentar os principais conceitos e aplicações de


operações em memória, arquivos em C, modos de leituras de arquivos e
gravação e leitura de arquivos para resolver problemas computacionais.

TEMA 1 – OPERAÇÕES EM MEMÓRIA: PARTE 1

As sub-rotinas de memória operam diretamente em áreas de memória, e


a linguagem de programação C possui algumas funções para manipulação
dessa sub-rotinas. Essas funções pertencem à biblioteca string.h. São elas:

 memset – usada para preenchimento de memória.

 memcpy – faz cópia de memória.


 memmove – também faz cópia de memória, só que de uma forma mais
segura.
 memcmp – faz comparação de memória.

A seguir, veremos as funções memset e memcpy.

1.1 Função memset()

A função memset() preenche (inicializa) uma quantidade de memória


(variável, constante, vetor, estrutura, entre outros) com um determinado valor de
byte.
A memeset() tem a seguinte sintaxe:
void * memset ( void * nPonteiro , int nValor , size_t nBytes );
2
Na qual:

 nPonteiro – refere-se a um ponteiro criado para a região de memória que


será preenchida.

 nValor – refere-se ao valor usado para preencher a região de memória.


O valor será convertido automaticamente para unsigner char (8 bits). O
unsigner char não permite armazenar valores negativos e pode
representar números em um intervalo que vai de 0 até 255.
 nBytes – refere-se ao número de bytes que serão preenchidos. Não é
necessariamente o tamanho do array.

Retorno:

 Retorna uma cópia do ponteiro nPonteiro.

 Retorna NULL em caso de erro.

A Figura 1 apresenta um exemplo com a função memset():

Figura 1 – Exemplo de um algoritmo com memset()

Fonte: elaborado pelo autor.

A Figura 2 mostra a saída do algoritmo da Figura 1 após a sua execução:

3
Figura 2 – Saída do algoritmo

Fonte: elaborado pelo auto.

No próximo exemplo, apresentado na Figura 3, veremos outro algoritmo


com a função memset usada para substituir números inteiros de um vetor com
seis posições.

Figura 3 – Exemplo de um algoritmo com memset() e vetor

4
Fonte: Elaborado pelo autor.

A Figura 4 mostra a saída do algoritmo da Figura 3 após a sua execução:

Figura 4 – Saída do algoritmo

Fonte: Elaborado pelo autor.

5
Para o próximo algoritmo vamos preencher os espaços com o número
271, só que binário. Esse número é equivalente a:
0 0 0 0 0 0 0 1 0 0 0 0 1 1 1 1.
Conforme a conversão realizada na Tabela 1:

Tabela 1 – Conversão de decimal para binário

512 256 128 64 32 16 8 4 2 1


0 1 0 0 0 0 1 1 1 1
Fonte: Elaborado pelo autor.

Nesse caso a função memset vai usar apenas os 8 bits, representados


em verde na Tabela 2, descartando todo o resto em azul.

Tabela 2 – Byte usado pela função memset()

0 1 0 0 0 0 1 1 1 1
Fonte: Elaborado pelo autor.

Então:
271 = 0 0 0 0 0 0 0 1 0 0 0 0 1 1 1 1
e
15 = 0 0 0 0 1 1 1 1
O memset() vai usar só 8 bits, que estão representados na Tabela 1. A
cor verde, em binário, é equivalente a decimal 15, conforme apresentado no
algoritmo da Figura 5:

6
Figura 5 – Exemplo de um algoritmo com memset() e vetor

Fonte: Elaborado pelo autor.

A Figura 6 mostra a saída do algoritmo da Figura 5 após a sua execução:

Figura 6 – Saída do algoritmo

Fonte: Elaborado pelo autor.

7
Repare que, na Figura 6, tivemos como saída o número 3855. Isso
aconteceu porque o byte 0 0 0 0 1 1 1 1 preencheu duas posições.

3855 = 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1

No próximo exemplo, na Figura 7, vamos preencher três posições do


vetor. Analise o código e veja o resultado na Figura 8.

Figura 7 – Exemplo de um algoritmo com memset() e vetor

Fonte: Elaborado pelo autor.

A Figura 8 mostra a saída do algoritmo da Figura 7 após a sua execução:

8
Figura 8 – Saída do algoritmo

Fonte: Elaborado pelo autor.

1.2 Função memcpy()

Essa função copia uma quantidade de bytes de uma área de memória


para outra. Ambas as regiões de memória são tratadas com unsigned char.
Sintaxe da função memcpy():

void* memcpy(void* pDestino, void* pOrigem, size_t num);

Na qual temos:

 pDestino – refere-se ao ponteiro para a região de memória que receberá


os dados copiados.

 pOrigem – refere-se ao ponteiro para a região de memória de onde os


dados serão copiados.
 num – refere-se ao número de bytes que serão copiados. Não é
necessariamente o tamanho de um vetor.

Retorno:

 Retorna uma cópia do ponteiro pDestino;

 Retorna NULL em caso de erro.

A função memcpy() é a função mais rápida entre regiões de memória. Mas


o endereço de memória não deve sobrepor-se; caso se sobreponha, então o
memcpy() é indefinido – isto é, o memcpy() desconsidera sobreposição. Desse
modo, essa função pode copiar do começo ao fim (ou o inverso), em blocos de
vários bytes etc.
A Figura 9 apresenta um exemplo com a função memcpyt():

9
Figura 9 – Exemplo de um algoritmo com memcpy()

Fonte: Elaborado pelo autor.

A Figura 10 mostra a saída do algoritmo da Figura 9 após a sua execução:

Figura 10 – Saída do algoritmo

Fonte: Elaborado pelo autor.

A Figura 11 apresenta um exemplo com a função memcpyt() e struct:

10
Figura 11 – Exemplo de um algoritmo com memcpy() e struct

Fonte: Elaborado pelo autor.

A Figura 12 mostra a saída do algoritmo da Figura 11 após a sua


execução:

Figura 12 – Saída do algoritmo

Fonte: Elaborado pelo autor.

TEMA 2 – OPERAÇÕES EM MEMÓRIA: PARTE 2

As funções para manipular a memória estão agrupadas nas categorias


definidas na biblioteca string.h. Neste momento, veremos como fazer uma cópia

11
de memória de uma forma mais segura com a função memmove() e como fazer
comparação de memória com a função memcmp().

2.1 Função memmove()

Essa função copia uma quantidade de bytes de uma área de memória


para outra. Ambas as regiões de memória são tratadas com unsigned char.
Sintaxe memmove():

Void* memmove(void* pDestino, void* pOrigem, size_t num);

Na qual temos:

 pDestino – refere-se ao ponteiro para região de memória que receberá


os dados copiados.

 pOrigem – refere-se ao ponteiro para a região de memória de onde os


dados serão copiados.
 num – refere-se ao número de bytes que serão copiados, não
necessariamente o tamanho de um vetor.

Retorno:

 Retorna uma cópia do ponteiro pDestino.

 Retorna NULL em caso de erro.

O funcionamento da função memmove() é igual ao da função memcpy(),


só que mais lenta. Porém, a função memmove() é mais segura no caso da
existência de duas regiões sobrepostas na memória, ou seja, duas áreas em
comum na memória.
A memcpy() usa um vetor auxiliar para fazer a cópia.
Exemplo:

1. char nome[20];
2. memcpy(&nome[0], &nome[4], 10); //Comportamento inesperado no caso
de sobreposição.
3. Memmove(&nome[0], &nome[4], 10); //Usa um vetor para tratar o
problema de sobreposição.

A Figura 13 apresenta um exemplo com a função memmove():

12
Figura 13 – Exemplo de um algoritmo com memmove()

Fonte: Elaborado pelo autor.

A Figura 14 mostra a saída do algoritmo da Figura 13 após a sua


execução:

Figura 14 – Saída do algoritmo

Fonte: Elaborado pelo autor.

2.2 Função memcmp()

A função memcmp() é usada para saber se uma string é maior, menor ou


igual a outra. Essa função compara as n primeiras posições de duas strings, ou
seja, de 0 até n-1.
Essas strings são, na verdade, números inteiros, e sua representação
está na tabela ASCII. Essa comparação é feita caractere por caractere.
Exemplo:

13
 a=c

 a<c
 z>c

Essa função compara diretamente os caracteres das strings. Se, em


algum momento da comparação, algum caractere da string 1 for menor que o da
string 2, a função para e retorna -1. Ademais, caso algum caractere da string 1
for maior que o da string 2, a função para e retorna 1. Caso o processo não
retorne nem 1 ou -1, caracteriza strings idênticas. Ambas as regiões da memória
são tratadas como unsigned char, e a comparação é feita na ordem lexicográfica,
ou seja, ordem do dicionário ou ordem alfabética.
Sintaxe memcmp():

Int memcmp(void* pRegiao1, void* pRegiao2, size_t N);

Na qual temos:

 pRegiao1 – refere-se ao ponteiro para uma região de memória.

 pRegiao2 – refere-se ao ponteiro para uma região de memória.


 N – refere-se ao número de bytes que serão comparados, não
necessariamente o tamanho de um vetor.

Retorno:

 Se o valor de retorno < 0, então pRegiao1 menor que pRegiao2.

 Se o valor de retorno == 0, então blocos de memória são iguais.


 Se o valor de retorno > 0, então pRegiao1 maior que pRegiao2.

A Figura 15 apresenta um exemplo com a função memcmp():

14
Figura 15 – Exemplo de um algoritmo com memcmp()

Fonte: Elaborado pelo autor.

A Figura 16 mostra a saída do algoritmo da Figura 15 após a sua


execução:

Figura 16 – Saída do algoritmo

15
Fonte: Elaborado pelo autor.

A Figura 17 apresenta um exemplo com a função memcmpt() e struct:

Figura 17 – Exemplo de um algoritmo com memcmp() e struct

Fonte: Elaborado pelo autor.

16
A Figura 18 mostra a saída do algoritmo da Figura 14 após a sua
execução:

Figura 18 – Saída do algoritmo

Fonte: Elaborado pelo autor.

TEMA 3 – ARQUIVOS EM C

Arquivo em linguagem de programação C é uma coleção de bytes


armazenados em um dispositivo secundário.
Exemplos:

 disco rígido;

 pen drive;
 cartão SSD;
 entre outros.

Quais as vantagens de se usar arquivos?

 Armazenamento durável;

 permitem armazenar uma grande quantidade de informação;


 acesso concorrente aos dados.

É importante ressaltar que a extensão do arquivo não define o seu tipo. O


que define um arquivo é a maneira como os dados estão organizados por um
programa para processar (ler e escrever) esse arquivo.
Para manipular arquivos na linguagem C usa-se a biblioteca stdio.h e um
tipo especial de ponteiro. A função desse ponteiro é apontar a localização de um
registro e controlar o fluxo de leitura e escrita dentro de um arquivo. A declaração
de um ponteiro para arquivo em C segue a sintaxe a seguir:

FILE *nomePonteiro;

17
Lembrando que FILE deve ser escrito em letras maiúsculas.
A linguagem C trabalha com apenas dois tipos de arquivos:

 Arquivos texto – podem ser editados no bloco de notas.

 Arquivos binários – não podem ser editados no bloco de notas.

Um arquivo texto tem seus dados gravados exatamente como seriam


impressos na tela. Esses dados são gravados como caracteres de 8 bits
utilizando a tabela ASCII. Para que isso ocorra, existe uma etapa de conversão
dos dados. Se um dado inteiro tem 4 bits, quando convertido ele terá 8 bits, que
é o padrão da tabela ASCII para um caractere. Consequentemente, os arquivos
são maiores, tendo leitura e escrita mais lentos.
Um arquivo binário tem seus dados gravados exatamente como estão
organizados na memória do computador. Não existe etapa de conversão dos
dados para esse tipo de arquivo, e o conteúdo da memória será copiado
diretamente para o arquivo. Consequentemente, os arquivos são menores, tendo
leitura e escritas mais rápidas.
Em C, o arquivo é manipulado por meio de um ponteiro especial para o
arquivo.

3.1 Abertura e fechamento de arquivos

A manipulação de um arquivo na linguagem C se dá em três etapas:

1. abrir o arquivo;

2. ler e/ou gravar os dados;


3. fechar o arquivo.

Para trabalhar com um arquivo, a primeira operação necessária é abrir tal


arquivo. Para se realizar essa operação, usamos a seguinte sintaxe de abertura
de arquivo:

FILE *fopen(char *nome_do_arquivo, char *modo);

A função fopen recebe como parâmetros o nome do arquivo a ser aberto


e o tipo de abertura a ser realizado. Vejamos um exemplo, em que a função
fopen vai abrir um arquivo txt em modo de escrita:

1. FILE *arq;

18
2. arq = fopen(“arquivo.txt”,”w”);

Há duas formas para especificar o caminho do arquivo:

1. Caminho absoluto – o endereço completo é escrito. Exemplo:

f = fopen(“C:\\Users\\Casa\\Documents\\uninter.txt","w");
Para representar a barra "\" em uma string, usamos duas barras: \\
2. Caminho relativo – relativo ao diretório do programa. Exemplo:
f = fopen(“uninter.txt","w");
f = fopen(“..\\Novo\\uninter2.txt","w");

A Figura 19 apresenta um exemplo com a função fopen():

Figura 19 – Exemplo de um algoritmo com fopen()

Fonte: Elaborado pelo autor.

A Figura 20 mostra a saída do algoritmo da Figura 19 após a sua


execução:

19
Figura 20 – Saída do algoritmo

Fonte: Elaborado pelo autor.

Caso o arquivo não exista ou não tenha permissão de acesso, a função


fopen também irá retornar NULL para o ponteiro. Quando esse erro ocorre, o
ponteiro irá apontar para NULL, sendo essa prática (checar se o ponteiro aponta
para NULL) muito importante para o tratamento de erros na abertura de arquivos
em C.
Para saber o final do arquivo, a linguagem C procura um sinal — uma
constante conhecida por EOF —, que sinaliza o fim do arquivo.
Se o byte lido pelo algoritmo representa o EOF, a função fclose() "fecha"
a abertura do arquivo. Ou seja, libera a memória associado ao ponteiro do FILE*.
Assim como em ponteiros, quando usamos a função free() para liberar
memória alocada, fechar os arquivos que não estão mais sendo usados é uma
boa prática de programação.

TEMA 4 – MODOS DE ABERTURA: READ (R), WRITE (W) E APPEND (A)

O modo de acesso é uma string que contém uma sequência de caracteres


que informam se o arquivo será aberto para escrita ou leitura. Depois que abrir
o arquivo, podemos executar os tipos de ação previstos pelo modo de acesso.
Assim, não será possível ler de um arquivo que foi aberto somente para escrita.
Os modos de acesso usados na linguagem C são descritos a seguir.

 read(r) – Leitura de arquivo

1. r – para ler um arquivo, usamos o modo read.


Exemplo: FILE *arquivo = fopen("uninter.txt", "r").
O arquivo será aberto unicamente para leitura.

20
2. r+ – na adição do sinal "+" no "r", iremos abrir o arquivo tanto para
leitura como para escrita; caso ele não exista, o arquivo será criado. Se
o arquivo existir, terá o seu conteúdo apagado e substituído pelo novo.
Exemplo: FILE *arquivo = fopen("uninter.txt", "r+").
3. rb – abre o arquivo em modo binário para leitura.
Exemplo: FILE *arquivo = fopen("uninter.txt", "rb").

 write(w) – Escrita em arquivo

1. w – para abrir um arquivo no modo de escrita, usamos a letra w. Esse


modo automaticamente cria o arquivo ou substitui seu conteúdo
anterior.
Exemplo: FILE *arquivo = fopen("uninter.txt", "w").
2. w+ – para abrir um arquivo tanto para leitura quanto para escrita. Se o
arquivo já existir, terá seu conteúdo substituído.
Exemplo: FILE *arquivo = fopen("uninter.txt", "w+").
3. wb – usado para escrita em arquivos no modo binário.
Exemplo: FILE *arquivo = fopen("uninter.txt", "wb").
 append(a) – escrevendo ao final do arquivo (anexando)
1. a – usamos o modo de abertura "a" para ANEXAR informações ao
arquivo. Exemplo: FILE *arquivo = fopen("uninter.txt", "a").
2. a+ – para abrir um arquivo no modo de leitura ou no modo de escrita
ao final do arquivo (anexar), usamos o símbolo "+" depois da letra "a".
Exemplo: FILE *arquivo = fopen("arquivo.txt", "a+").
3. ab – do mesmo modo que a leitura binária "rb" e a escrita binária "wb",
podemos anexar informações ao final do arquivo de maneira binária
usando o "ab".
Exemplo: FILE *arquivo = fopen("uninter.txt", "ab").

Sempre que se terminar de usar um arquivo é preciso fechá-lo. Para


realizar essa tarefa, usa-se a função fclose() com a seguinte sintaxe:

Int fclose(FILE *ponteiro);

A função fclose() retorna ZERO (0) caso o algoritmo tenha sucesso no


fechamento do arquivo.
A Figura 21 apresenta um exemplo com a função fclose():

21
Figura 21 – Exemplo de um algoritmo com fclose()

Fonte: Elaborado pelo autor.

TEMA 5 – GRAVAÇÃO E LEITURA DE ARQUIVOS

Existem várias funções em C para a operação de gravação e leitura de


dados em arquivos. Agora iremos trabalhar com duas funções que gravam e
leem um arquivo txt caractere por caractere. A primeira função é o fputc(), que é
usada para escrever um caractere de cada vez em um determinado arquivo. A
segunda função é o fgetc(), que é usada para obter entrada de um caractere de
arquivo por vez, contido na biblioteca stdio.h.

5.1 Função fputc()

Serve para gravar um caractere em um arquivo. Essa função grava o


caractere fornecido na posição indicada pelo ponteiro do arquivo e, em seguida,
avança o ponteiro do arquivo.
Para realizar essa tarefa, usa-se a função fputc(), que tem a seguinte
sintaxe:
int fputc(char c, FILE *arquivo);
Retorno:

 Se houver erro, a função retorna a constante EOF.

 Se o algoritmo tiver sucesso, retornará o próprio caractere.

22
A Figura 22 apresenta um exemplo com a função fputc():

Figura 22 – Exemplo de um algoritmo com fputc()

Fonte: Elaborado pelo autor.

A Figura 23 mostra a execução do algoritmo da Figura 22:

Figura 23 – Execução do algoritmo

Fonte: Elaborado pelo autor.

23
A Figura 24 mostra a saída do algoritmo, após sua execução na Figura
23:

Figura 24 – Saída do algoritmo

Fonte: Elaborado pelo autor.

5.2 Função fgetc()

Essa função lê o caractere presente na posição indicada pelo ponteiro do


arquivo e automaticamente já se posiciona no próximo campo, e assim segue
lendo até encontrar a constante EOF.
A sintaxe da função fgetc é:

int fgetc(FILE *arq)

E, como comentamos, uma coisa importante que ocorre “por debaixo dos
panos” é que, após retornar um caractere, essa função já passa a apontar para
o próximo caractere automaticamente, até encontrar -1 (EOF).
Retorno:

 Se houver erro a função, retorna à constante EOF.

 Se o algoritmo tiver sucesso, retornará à leitura do arquivo.

A Figura 25 apresenta um exemplo com a função fgetc():

24
Figura 25 – Exemplo de um algoritmo com fgetc()

Fonte: Elaborado pelo autor.

A Figura 26 mostra a execução do algoritmo da Figura 24:

Figura 26 – Saída do algoritmo

Fonte: Elaborado pelo autor.

25
A linguagem de programação C traz uma série funções usadas para
manipular arquivos. Na Tabela 3 estão listadas outras funções relacionadas à
manipulação de arquivos.

Tabela 3 – Principais funções de manipulação de arquivos da biblioteca stdio.h

FUNÇÃO Usada para


fscanf() Ler um arquivo
fprintf() Escrever textos (strings) em arquivos
fgets() Ler uma string num arquivo
fputs() Inserir uma string no arquivo
fread() Ler um bloco de dados do arquivo
fwrite() Escrever um bloco de dados no arquivo
fseek() Reposicionar o ponteiro
rewind() Reposicionar o ponteiro para o início do arquivo
ftell() Retornar a posição do ponteiro.
Fonte: Mizrahi, 2008.

FINALIZANDO

Nesta aula aprendemos os principais conceitos e aplicações de


operações em memória, arquivos em C, modos de leituras de arquivos, e
gravação e leitura de arquivos. Aproveite a disciplina e bons estudos!

26
REFERÊNCIAS

ASCENCIO, A. F. G. Fundamentos da programação de computadores:


algoritmos, Pascal, C/C++ (padrão ANSI), JAVA. 3. ed. São Paulo: Pearson,
2012.

MIZRAHI, V. V. Treinamento em Linguagem C. 2. ed. São Paulo: Pearson,


2008.

27

Você também pode gostar