Prévia do material em texto
Autor: Prof. Olavo Tomohisa Ito
Colaboradoras: Profa. Vanessa Santos Lessa
Profa. Larissa Rodrigues Damiani
Estruturas de Dados
Professor conteudista: Olavo Tomohisa Ito
Mestre em Engenharia de Produção pela UNIP, bacharel em Física pelo Instituto de Física da USP,
licenciado em Ciências pela Faculdade de Educação da USP, com o curso completo de Bacharelado
em Língua e Literatura Japonesa da FFLCH‑USP. Professor dos cursos de graduação tradicional
de Sistemas de Informação e Ciência da Computação, bem como do curso superior tecnológico de
Análise e Desenvolvimento de Sistemas. Escreveu alguns livros didáticos como: Linguagem e Técnicas
de Programação, Programação Orientada a Objetos e Programação para Dispositivos Móveis.
© Todos os direitos reservados. Nenhuma parte desta obra pode ser reproduzida ou transmitida por qualquer forma e/ou
quaisquer meios (eletrônico, incluindo fotocópia e gravação) ou arquivada em qualquer sistema ou banco de dados sem
permissão escrita da Universidade Paulista.
Dados Internacionais de Catalogação na Publicação (CIP)
I89e Ito, Olavo Tomohisa.
Estruturas de Dados / Olavo Tomohisa Ito. – São Paulo: Editora
Sol, 2022.
348 p., il.
Nota: este volume está publicado nos Cadernos de Estudos e
Pesquisas da UNIP, Série Didática, ISSN 1517‑9230.
1. Linguagem C. 2. Percurso. 3. Grafo. I. Título.
CDU 681.3.01
U515.97 – 22
Prof. Dr. João Carlos Di Genio
Reitor
Profa. Sandra Miessa
Reitora em Exercício
Profa. Dra. Marilia Ancona Lopez
Vice-Reitora de Graduação
Profa. Dra. Marina Ancona Lopez Soligo
Vice-Reitora de Pós-Graduação e Pesquisa
Profa. Dra. Claudia Meucci Andreatini
Vice-Reitora de Administração
Prof. Dr. Paschoal Laercio Armonia
Vice-Reitor de Extensão
Prof. Fábio Romeu de Carvalho
Vice-Reitor de Planejamento e Finanças
Profa. Melânia Dalla Torre
Vice-Reitora de Unidades do Interior
Unip Interativa
Profa. Elisabete Brihy
Prof. Marcelo Vannini
Prof. Dr. Luiz Felipe Scabar
Prof. Ivan Daliberto Frugoli
Material Didático
Comissão editorial:
Profa. Dra. Christiane Mazur Doi
Profa. Dra. Angélica L. Carlini
Profa. Dra. Ronilda Ribeiro
Apoio:
Profa. Cláudia Regina Baptista
Profa. Deise Alcantara Carreiro
Projeto gráfico:
Prof. Alexandre Ponzetto
Revisão:
Kleber Souza
Willians Calazans
Sumário
Estruturas de Dados
APRESENTAÇÃO ......................................................................................................................................................9
INTRODUÇÃO ...........................................................................................................................................................9
Unidade I
1 INTRODUÇÃO À LINGUAGEM C ................................................................................................................. 11
1.1 Histórico ................................................................................................................................................... 12
1.2 IDE C/C++ ................................................................................................................................................ 12
1.3 Regras da linguagem .......................................................................................................................... 15
1.4 Estrutura de um programa em C ................................................................................................... 17
1.5 Funções e procedimentos C ............................................................................................................. 18
1.6 Tipos primitivos ..................................................................................................................................... 19
1.6.1 Operadores ................................................................................................................................................. 20
1.6.2 Entrada e saída ........................................................................................................................................ 21
1.6.3 Comandos da linguagem ..................................................................................................................... 22
1.6.4 Cast ............................................................................................................................................................... 24
2 ALGORITMOS E SOLUÇÃO DE PROBLEMAS .......................................................................................... 25
2.1 Introdução ao nivelamento de algoritmos ................................................................................ 25
2.2 Conceito de análise de algoritmos ................................................................................................ 26
3 REVISÃO DE ARRANJOS ................................................................................................................................ 31
3.1 Representação linear de matrizes ................................................................................................. 35
3.2 Operações com cadeias ...................................................................................................................... 35
3.3 Caracteres em C .................................................................................................................................... 37
3.4 Manipulação de strings ..................................................................................................................... 39
3.5 Modularização ....................................................................................................................................... 40
3.5.1 Procedimentos e funções .................................................................................................................... 44
3.6 Tipos abstratos de dados ................................................................................................................... 44
3.6.1 Tipo estrutura ........................................................................................................................................... 44
3.6.2 Definição de “novos” tipos .................................................................................................................. 45
3.6.3 Conceitos de TAD cadeias .................................................................................................................... 49
4 ALOCAÇÃO DINÂMICA DE MEMÓRIA: PONTEIROS ........................................................................... 52
4.1 O uso da memória ................................................................................................................................ 52
4.2 Variáveis ................................................................................................................................................... 56
4.3 Ponteiro de variáveis ........................................................................................................................... 57
4.4 Passagem de parâmetros por valor e por referência ............................................................. 69
4.5 Passagem de vetores para funções ............................................................................................... 85
4.6 Funções da biblioteca padrão.......................................................................................................... 86
Unidade II
5 ESTRUTURAS UNIDIMENSIONAIS ............................................................................................................. 98
5.1 Lista linear: definição e representação ........................................................................................ 98
5.2 Aplicações: lista sequencial .............................................................................................................. 99
5.2.1 Lista sequencial: inicialização ..........................................................................................................1005.2.2 Lista sequencial: inserção ..................................................................................................................100
5.2.3 Lista sequencial: impressão ..............................................................................................................104
5.2.4 Lista sequencial: busca .......................................................................................................................104
5.2.5 Lista sequencial: remoção .................................................................................................................105
6 LISTA ENCADEADA ........................................................................................................................................107
6.1 Lista encadeada: inicialização .......................................................................................................110
6.1.1 Lista encadeada: inserção ..................................................................................................................111
6.1.2 Lista encadeada: impressão, busca e liberação de memória .............................................. 120
6.1.3 Lista encadeada: remoção ................................................................................................................ 123
6.2 Listas com descritores ......................................................................................................................130
6.2.1 Listas duplamente encadeadas ...................................................................................................... 130
6.3 Listas lineares com disciplina de acesso: pilhas .....................................................................131
6.3.1 Implementação de pilha com lista: representação encadeada ......................................... 132
6.3.2 Criação da pilha com lista ................................................................................................................ 133
6.4 Função de impressão e função de liberação de memória .................................................143
6.5 Listas lineares com disciplina de acesso: filas.........................................................................146
6.5.1 Implementação de fila com lista: representação encadeada ............................................ 147
6.5.2 Criação da fila com lista ................................................................................................................... 148
6.5.3 Inserção na fila com lista .................................................................................................................. 149
6.5.4 Remoção de nó da fila com lista ................................................................................................... 156
6.5.5 Função de impressão e função de liberação de memória com lista ............................... 159
6.6 Implementação de fila com vetor: representação linear ...................................................162
6.6.1 Fila circular ............................................................................................................................................. 168
6.6.2 Filas especiais: deque ......................................................................................................................... 174
Unidade III
7 RECURSIVIDADE E ÁRVORE ......................................................................................................................184
7.1 Recursividade .......................................................................................................................................184
7.2 Árvores ....................................................................................................................................................196
7.2.1 Árvores: definições e representações básicas ........................................................................... 197
7.2.2 Árvores binárias .................................................................................................................................... 198
7.2.3 Percurso em árvores binárias .......................................................................................................... 199
7.2.4 Percurso em pré‑ordem ou prefixo (busca em profundidade) .......................................... 200
7.2.5 Percurso em ordem ou infixo (ordem simétrica)..................................................................... 209
7.2.6 Percurso em pós‑ordem ou pós‑fixo ............................................................................................213
7.3 Estrutura de árvores binárias em C .............................................................................................217
7.3.1 Árvores binárias de busca ................................................................................................................. 224
7.4 Pesquisa de dados: sequencial e binária ...................................................................................237
7.4.1 Busca sequencial .................................................................................................................................. 237
7.4.2 Busca binária ......................................................................................................................................... 238
7.5 Ordenação de dados ..........................................................................................................................242
7.5.1 Ordenação por troca BubbleSort (método da bolha) ............................................................ 243
7.5.2 QuickSort (método da troca e partição) ..................................................................................... 246
7.5.3 Ordenação por inserção InsertionSort (método da inserção direta) .............................. 247
7.5.4 BinaryInsertionSort (método da inserção direta binária) .................................................... 250
7.5.5 Ordenação por seleção SelectionSort (método da seleção direta) .................................. 252
7.5.6 HeapSort (método da seleção em árvore) ................................................................................. 255
7.6 Outros métodos: MergeSort (método da intercalação) e BucketSort
(método da distribuição de chave) .....................................................................................................277
7.6.1 MergeSort (método da partição e mesclagem) ....................................................................... 277
7.6.2 BucketSort (método do balde) ....................................................................................................... 283
8 GRAFOS: CONCEITOS, REPRESENTAÇÃO E APLICAÇÕES ...............................................................302
8.1 Tipos de grafos ....................................................................................................................................303
8.1.1 Grafo dirigido ........................................................................................................................................ 303
8.1.2 Grafo não dirigido ............................................................................................................................... 304
8.2 Adjacência .............................................................................................................................................304
8.3 Grau .........................................................................................................................................................305
8.4 Caminho .................................................................................................................................................305
8.5 Comprimento de um caminho......................................................................................................306
8.6 Ciclo .........................................................................................................................................................3068.7 Conexão ..................................................................................................................................................306
8.8 Ponderação ...........................................................................................................................................308
8.9 Representação computacional .....................................................................................................308
8.9.1 Matriz de adjacências ........................................................................................................................ 308
8.9.2 Lista de adjacências .............................................................................................................................310
8.10 Tabela hash .........................................................................................................................................331
8.10.1 Inserção e busca .................................................................................................................................331
8.10.2 Tratamento de colisão ..................................................................................................................... 335
9
APRESENTAÇÃO
Caro aluno, a presente disciplina é um passo adiante para aqueles que já estudaram LPA ou disciplinas
ligadas a algoritmos. Aqui abrangeremos algoritmos de estruturas e técnicas consagradas que o aluno
poderia até desenvolver por si, mas analisaremos os melhores caminhos para chegarmos à construção
de uma solução. O paradigma de programação será a estruturada, uma vez que veremos algoritmos
modulares e técnicas que se tornam fundamentos do paradigma de orientação a objetos. Para tanto,
utilizaremos a linguagem C++, pois por ser de médio nível nos permitirá o acesso a endereços de memória.
Veremos através das estruturas como as informações estão organizadas no computador, nas pastas
e nos arquivos. Os dados computacionais, de algum modo, deverão estar dispostos para que seja
possível realizar a escrita e a sua posterior recuperação de forma íntegra e funcionalmente prática.
Assim, as estruturas de dados são formas otimizadas de armazenamento e tratamento das informações
eletronicamente, representando a maturidade na lógica de programação.
INTRODUÇÃO
Na unidade I, veremos uma pequena introdução à linguagem C++. Todavia, este não é um curso de
tal linguagem, será apenas um exemplo. A estrutura lógica é a mesma das linguagens de programação
já estudadas no curso. Faremos uma revisão sobre algoritmos, a sua qualidade, e a modularização.
A conceituação de tipo abstrato de dados (TAD), bem como o estudo técnico da alocação dinâmica de
memória e os ponteiros como uma maneira dinâmica de manipular valores na memória. Passaremos os
conceitos iniciais e não os algoritmos em si. Conceitos como tipo abstrato de dados (TAD) e técnicas de
manipulação de memória através de ponteiros serão fundamentais.
Na unidade II, apresentaremos a primeira estrutura importante, a lista ligada, ou lista encadeada.
Ela é conhecida por possuir encadeamentos unidirecionais, ou seja, listas organizadas com os dados
organizados um após o outro. Veremos variantes das listas utilizando a disciplina de acesso, a fila e
a pilha. A compreensão do conceito de ligação entre as informações servirá como ferramenta para
diversas técnicas de programação, tornando praticamente ilimitado o armazenamento de informações.
Por fim, haverá uma técnica muito especial não estudada anteriormente, a recursividade. A partir
dela poderemos estudar estruturas como as árvores, pesquisas, ordenações. O foco da unidade III será
buscar informação em um conjunto de dados. Para fazê‑lo, utilizaremos algumas técnicas como a
sequencial, a binária, o uso de grafos e a tabela hash.
11
ESTRUTURAS DE DADOS
Unidade I
Iniciaremos o módulo com um breve resumo da linguagem C. A aplicação dos conceitos será
através da linguagem C padrão de forma estrutural. Devemos deixar claro que este não é um curso de
linguagem C ou C++, portanto apenas os comandos e as técnicas aplicáveis à presente disciplina serão
explicados. Pensando em termos de outras linguagens mais modernas, como Java, PHP ou mesmo o
Interface Definition Language (IDL) do Common Object Request Broker Architecture (CORBA), usado
em sistemas distribuídos, têm como base a linguagem C++. Como o aluno neste ponto do curso já deve
ter certa familiaridade com alguma linguagem de programação, esse início é somente um pequeno
guia para implementar as estruturas, comandos extras serão explicados durante o transcorrer do curso.
1 INTRODUÇÃO À LINGUAGEM C
A linguagem C pertence a uma família de linguagens cujas características são: portabilidade,
modularidade, compilação separada, recursos de baixo nível, geração de código eficiente, confiabilidade,
regularidade, simplicidade e facilidade de uso. Ela é considerada de médio nível. Não é uma linguagem
de máquina que apenas os sistemas operacionais reconhecem, os chamados de baixo nível, como
o Assembler, nem de alto nível, em que a própria linguagem fornece todos os recursos para o
desenvolvimento do programa, como o Visual Basic. Ela faz parte da classe dos programas compilados,
que são escritos em texto e passam por traduções para adequar‑se ao sistema operacional.
A linguagem C é adequada para a programação estruturada, tendo aplicação, principalmente, no
desenvolvimento de sistemas operacionais, planilhas eletrônicas e gerenciadores de bancos de dados
(HICKSON, 2005). Assim, o programa usado para processar este livro‑texto foi escrito em linguagem C.
Segundo Cocian (2004, p. 32), os pontos que tornaram a linguagem C popular foram:
• A portabilidade do compilador, pois existem compiladores para todos os sistemas operacionais.
• O conceito de bibliotecas padronizadas.
• A quantidade e a variedade de operadores poderosos.
• A sintaxe elegante.
• O fácil acesso ao hardware, quando necessário.
• A facilidade com que as aplicações podem ser otimizadas, tanto na codificação quanto na
depuração, pelo uso de rotinas isoladas e encapsuladas.
12
Unidade I
1.1 Histórico
Em 1966, uma linguagem chamada Basic Combined Programming Language (BCPL) – Linguagem
de Programação Básica Combinada, desenvolvida por Martin Richards, da Universidade de Cambridge
(POLLONI; PERES; FEDELI, 2009), serviu de base para a linguagem B, elaborada por Ken Thompson para o
computador PDP‑7 e que funcionava no sistema operacional Unix. A linguagem B, em razão de problemas
de velocidade e processamento, viria a ser corrigida na linguagem C (COCIAN, 2004), desenvolvida na
Bell Laboratories por Dennis Ritchie, em 1972, e liberada para as universidades.
O fato de ser liberada para as universidades tornou‑a popular, havia diversos compiladores para
os vários sistemas operacionais e computadores. Esse fato mostrou a necessidade de padronização
da linguagem, o que aconteceu em 1983, quando a American National Standards Institute (ANSI)
– correspondente à ABNT no Brasil – estabeleceu esse padrão. Com o avanço do paradigma de
programação orientada a objeto, foi desenvolvido o C++.
Como a linguagem C++ engloba totalmente a linguagem C e os recursos para esse curso são bem
simples, a validade da referência à linguagem C será válida para a linguagem C++.
Saiba mais
Caso haja a necessidade de consultar a normatização da linguagem C,
basta consultar o link a seguir. Apesar de extremamente técnico, poderá
ajudar na implantação de uma linguagem de programação em novos
dispositivos físicos.
ISO/IEC. JTC1/SC22/WG21: The C++ Standards Committee – ISOCPP.
[s.d.]. Disponível em: https://bit.ly/3w2dHEL. Acesso em: 10 maio 2022.
1.2 IDE C/C++
Os programas apresentados neste livro‑texto foram desenvolvidos utilizando o Microsoft Visual
Studio 2008 (MICROSOFT, s.d.) e a sua versão online. Trata‑se da versão mais leve e com menos problemas
de incompatibilidade na interface. Uma alternativa para ela é o Code::Blocks(CODE BLOCKS, s.d.), que
tem instalação simples e debug eficiente. Outra alternativa é usar o Colab do Google, que não tem uma
compilação automática, fato que permite uma experiência muito boa no processo de passagem do texto
de programação para o executável no sistema operacional (Linux).
Observação
Após o processo de compilação algumas mensagens são apresentadas.
Em caso de erros de sintaxe, nenhum executável é gerado, sendo exibido o
número da linha da ocorrência. Outra mensagem é o Warning (aviso), neste
caso o executável é gerado normalmente, ela é apenas uma observação dos
programas desenvolvidos neste livro‑texto.
13
ESTRUTURAS DE DADOS
A Microsoft oferece outras opções, como o Microsoft Visual Studio, que tem as versões Community
(gratuita) e a Code. No Visual Studio, nas versões a partir de 2016, é necessária a inclusão na primeira
linha da seguinte instrução:
Figura 1
O uso do Visual Studio Code exige algumas personalizações manuais para evitar erros, portanto é
recomendável apenas para usuários mais experientes.
Existem versões online como a recomendada pelo Code::Block, o OnlineGDB (s.d.). O importante
nesses casos é selecionar o compilador da linguagem para C ou Turbo C++, conforme a figura a seguir.
Figura 2 – Selecionando o compilador C no OnlineGDB
Adaptada de: OnlineGDB (s.d.).
Saiba mais
Como visto, o Google tem um ambiente de programação, o Colab, que
foi projetado para python, mas o seu ambiente já possui instalado um
compilador C. Seu endereço é o seguinte:
GOOGLE. Conheça o Colab. [s.d.]. Disponível em: https://bit.ly/3lWSgPD.
Acesso em: 10 maio 2022.
Para fazer a programação, abra um ambiente de trabalho que é tratado, como notebook já
existente ou novo.
A fim de programar as instruções faça o seguinte procedimento:
• Abra uma janela de código e digite a instrução %%writefile e o nome do programa.
• Digite o seu programa.
• Clique em executar para salvar o arquivo.
14
Unidade I
Figura 3 – Janela de codificação do Colab
Para compilar:
• Abra uma nova janela e digite %%shell, que passará a trabalhar com o ambiente Linux.
• Digite a execução do compilador C (gcc): gcc nome_arquivo_fonte ‑w ‑o nomeexecutável
Em seguida, execute:
• /nomeexecutável (figura a seguir).
• Clique na seta de execução. Em caso de erro, basta corrigir na janela do writefile para salvar o
código corrigido e executar novamente.
Figura 4 – Compilando e executando um programa
Saiba mais
A fim de visualizar o notepad mostrado anteriormente, acesse:
COLAB RESEARCH. Linguagem C.ipynb. [s.d.]. Disponível em:
https://bit.ly/3tdd0XI. Acesso em: 10 maio 2022.
15
ESTRUTURAS DE DADOS
1.3 Regras da linguagem
Como toda linguagem de programação, a linguagem C é muito rígida na sua sintaxe. Sintaxe são
regras detalhadas para que um programa possa ser executado.
Elas estão relacionadas com os tópicos a seguir:
• Tipos: definem as propriedades dos dados manipulados em um programa.
• Declarações: estabelecem o identificador, para alocar memória, definir conteúdo inicial e
determinar funções.
• Expressões: fórmulas que atribuem e alteram valores, bem como fazem chamada de funções de
entrada e saída.
• Funções: subprogramas em C, que, por sua vez, são chamados por outras funções executando
tarefas específicas. Há funções básicas que estão definidas nas bibliotecas padrão da linguagem,
e outras que são desenvolvidas por terceiros, com rotinas mais específicas (ITO, 2015). As funções
printf() e scanf(), por exemplo, que veremos posteriormente, as quais permitem, respectivamente,
escrever na tela e ler os dados a partir do teclado, fazem parte da biblioteca básica. O programador
também pode escrever as suas próprias funções.
A primeira função que o programa executa é o main(), portanto é obrigatória a sua declaração no
programa principal.
Outras regras importantíssimas:
• A linguagem é case sensitive; isso quer dizer que existe diferenciação entre as letras maiúsculas e
minúsculas, na identificação de comandos, variáveis e funções.
Abacaxi ≠ abacaxi
• Os comandos são separados por ponto e vírgula (“;”), que deve ser usado com muito cuidado,
principalmente, antes de blocos de comandos.
Observação
O compilador ignora as quebras de linha, os comandos escritos
em linhas diferentes, mas separados apenas por esse recurso, para ele
estão na mesma linha.
16
Unidade I
abcdefgh
ijk
lmn
abc
defgh;ijk;
lmn;
Figura 5 – Para o compilador, a separação dos comandos é feita por “;” e não pela linha
• A linguagem também permite o uso de comentários, que são colocados no texto entre “/*” e “*/”,
quando queremos escrever mais de uma linha. O uso do “//” ainda serve como comentário, e, neste
caso, o compilador ignorará tudo o que estiver escrito a partir dele até o fim da linha.
abcdefgh
mn
abc
defgh;/*ijk;
l*/mn; //opqrs
Figura 6 – O compilador ignora os comentários
• Pela definição padronizada pela ANSI (HICKSON, 2005), existem 32 palavras‑chave (ou palavras
reservadas) que formam a linguagem de programação C. Conforme o compilador, poderão existir
mais palavras‑chave, todas escritas em letras minúsculas. Nenhuma delas deverá ser utilizada
para dar nomes a variáveis e funções, pois isso gerará erro ao compilar.
Quadro 1 – Palavras reservadas pelo padrão ANSI
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
Fonte: Hickson (2005, p. 28).
17
ESTRUTURAS DE DADOS
• Os identificadores são nomes usados para fazer referência a variáveis, funções, rótulos e vários
outros objetos definidos pelo programador. Como norma, conforme vimos no pseudocódigo, o
primeiro caractere deve ser uma letra ou um sublinhado, e o restante podem ser números, letras
ou sublinhados; apenas os 32 primeiros caracteres de um identificador são significativos. Como já
dito, a linguagem C é case sensitive, ou seja, as letras maiúsculas diferem das minúsculas.
1.4 Estrutura de um programa em C
Para programar em C partindo do zero, ou seja, de um texto totalmente vazio, é necessário apenas
o bloco da função main(). Como há a necessidade de interações, devemos colocar as bibliotecas no
programa, o que é feito em primeiro lugar.
A fim de termos uma ideia mais clara da estrutura, elaboraremos o primeiro programa e
compreenderemos cada uma das linhas.
Figura 7
Ao executar o programa, teremos uma mensagem no console do DOS dizendo “Meu primeiro
programa em C”. Apesar de ser um programa muito simples, existe a estrutura mínima de um programa
na linguagem. Vamos analisar cada parte dela a seguir:
#include <stdio.h>
O comando include informa ao compilador que ele deve incluir o arquivo‑cabeçalho stdio.h. Nele,
há um série de funções de entrada e saída úteis, std‑i‑o standard input‑output (biblioteca padrão de
entrada/saída). Sempre que quisermos usar funções relativas à entrada e saída, devemos verificar se o
arquivo stdio.h as contém. A linguagem C fornece dezenas de arquivos‑cabeçalhos com milhares de
funções úteis. Podemos chamar os arquivos‑cabeçalhos de bibliotecas.
Observação
O compilador ao encontrar o comando #include, antes de iniciar o
processo de compilação, adiciona a listagem da biblioteca à listagem
escrita pelo programador, formando apenas uma listagem. Somente depois
o programa é compilado.
18
Unidade I
/*
Meu primeiro programa em C
*/
No código a seguir, temos uma estrutura básica que possui o conjunto:
Figura 8
1.5 Funções e procedimentos C
Os programas em C são montados com a reunião de várias pequenas funções, em vez de poucas de
maior tamanho. Na linguagem C, tudo é feito por meio de funções e de suas bibliotecas. Já havíamos
utilizado algumas, como as da biblioteca stdlib.h, para entrada e saída de dados.
Sintaxe da função em C:
<tipo de retorno> nome( <tipo1> var1, <tipo2> var2){
Corpo da função
return retorno;
}
19
ESTRUTURAS DE DADOS
O <tipode retorno> pode ser qualquer um, até os criados por meio do struct. Um caso particular
é o tipo void (nulo). Isso significa que a função não retornará nenhum valor, ou seja, trata‑se de
um procedimento.
Durante a programação, a execução sempre começa no main(). Na verdade, o próprio main() é
uma função. Aqui, usualmente utilizamos no main() o tipo void, e o seu parâmetro é vazio. Sendo uma
função main(), ele pode ser definido como qualquer tipo e podemos vê‑lo como int, mas deverá retornar
um valor inteiro que poderá ser utilizado pelo sistema operacional. Os parâmetros da função main são
aqueles passados quando um programa é executado na linha de comando e alguns valores são digitados
após o nome. Dessa maneira, é feita a interação do ambiente operacional e do programa.
1.6 Tipos primitivos
Na linguagem C, além dos tipos primitivos int, float, char e double, temos as suas modificações, e a
faixa de números utilizáveis é determinada pelo número de bits de cada tipo modificado. Podemos ver
essa precisão e a faixa numérica a seguir.
Quadro 2 – Tipos primitivos com os modificadores
Tipo N. de bits
Intervalo
Valor inicial Valor final
Char 8 ‑128 127
Unsigned char 8 0 255
Signed char 8 ‑128 127
Int 16 ‑32,768 32,767
Unsigned int 16 0 65,535
Signed int 16 ‑32,768 32,767
Short int 16 ‑32,768 32,767
Unsigned short int 16 0 65,535
Signed short int 16 ‑32,768 32,767
Long int 32 ‑2.147.483.648 2.147.483.647
Signed long int 32 ‑2.147.483.648 2.147.483.647
Unsigned long int 32 0 4.294.967.295
Float 32 3,4E‑38 3,4E+38
Double 64 1.7 E‑308 1.7 E308
Long Double 80 3.4E‑4932 3.4E+4932
Fonte: Hickson (2005, p. 30).
Conforme vimos, os tipos lógicos e as cadeias de caracteres não constam no quadro. Para esses tipos,
a linguagem C tem um tratamento específico.
20
Unidade I
• Tipo lógico: a linguagem C trata como verdadeiro (true) o valor inteiro 1 e como falso (false) o
valor inteiro 0; assim, a ausência do tipo lógico é compensada pelo correspondente numérico. Na
versão C++, a linguagem passa a incorporar o tipo bool.
1.6.1 Operadores
Conforme veremos a seguir, existem os operadores unários e binários. No quadro, temos
os operadores de referência que observaremos posteriormente em Alocação dinâmica de
memória: ponteiros.
Quadro 3 – Relação de operadores
Tipo Operador Propósito Exemplo
Aritméticos
+ Adição a = 4 + 1; // 5
‑ Subtração a = 4 – 1; // 3
* Multiplicação a = 2 * 4; // 8
/ Divisão a = 8 / 2; // 4
% Módulo (resto da divisão) a = 5 % 2; // 1
Atribuição = Atribuição simples a = 50;
Lógicos
&& “e” lógico (a > 1) && (b < 1)
|| “ou” lógico (a > 1) || (b < 1)
! não (inversão) !(a > 2)
Relacionais (Comparação)
== igual a (a == 0)
!= diferente de (a != 0)
< menor que (a < 0)
> maior que (a > 0)
<= menor ou igual a (a <= 0)
>= maior ou igual a (a >= 0)
Incremento e Decremento
++ Incremento a++;
‑‑ Decremento a‑‑;
Referência (Apontadores)
Operadores utilizados antes
do nome de variáveis. Será
visto com detalhes em
Alocação dinâmica de
memória: ponteiros.
& Retorna o “endereço de” int a; //variável inteira
int *p; //declaração de ponteiro p = &a;
//atribui o endereço de a
*p = 2; //atribui ao conteúdo
//apontado por p o valor 2;
//como p aponta para o endereço
//de a, então a recebe 2.
* Retorna o “conteúdo de”
21
ESTRUTURAS DE DADOS
1.6.2 Entrada e saída
No item Estrutura de um programa em C, vimos que, no código, devemos incluir o arquivo‑cabeçalho
stdio.h, que contém uma série de funções de entrada e saída úteis. Nos nossos programas, utilizaremos
duas funções dessa biblioteca: o printf e o scanf.
• printf(formato, argumentos): função para saída de valores segundo um determinado formato.
As máscaras de formatação podem ser vistas no quadro 4 – Especificadores de formato.
Exemplo:
Figura 9
Figura 10
• scanf(formato, lista de endereços): função para capturar e armazenar valores fornecidos
via teclado.
No comando scanf, a lista de endereços pode ser escrita com o nome da variável em que se deseja
atribuir um valor precedido pelo caractere &.
Exemplo:
Figura 11
Figura 12
22
Unidade I
Quadro 4 – Especificadores de formato
%c char
%d int
%u unsigned int
%f double ou float
%e double ou float (científico)
%s cadeia de caracteres
\n quebra de linha
\t tabulação
\” caractere ”
\\ caractere \
1.6.3 Comandos da linguagem
No padrão da linguagem C, como vimos, existem 32 palavras‑chave, e entre elas temos os comandos
para que todas as bibliotecas possam ser montadas.
Quadro 5 – Relação dos comandos da linguagem C
Comando Propósito Sintaxe
Declaração de variável Declaração de variável tipo nome_variavel = valor_inicial;
Declaração de constante Declaração de constante #define NOME_CONSTANTE valor
Bloco Marcar um bloco de cód. { } //Abre e fecha chaves “{}“
if Comando condicional
if (a > b) {
printf(“%s”, “A é maior que B”);
} else {
printf(“%s”, “A é igual ou menor que B”);
}
switch Comando condicional
switch (i) {
case 0 :
printf(“%s”, “ZERO”);
break;
case 1:
printf(“%s”, “UM”);
break;
case 2:
printf(“%s”, “DOIS”);
break;
}
23
ESTRUTURAS DE DADOS
Comando Propósito Sintaxe
while Laço com pré‑validação
int i = 1;
while (i <= 10) {
printf(“%d”, i++);
}
do Laço com pós‑validação
int i = 1;
do {
printf(“%d”, i++);
} while (i <= 10);
for Laço simplificado
for (i=1;i<=10;i++){
printf(“\n%d”, i);
}
break Saída de bloco break;
continue Reinício de bloco continue;
Sub‑rotinas
Funções
float area(float altura, float base) {
return altura * base;
}
Procedimentos
void area(float altura, float base) {
{ printf(“A area é: %f”, altura * base);
}
Vetores Variáveis unidimensionais
int v[10]; //Vetor de inteiros
//v[0] é o primeiro elemento e v[9] o último
Matrizes Variáveis multidimensionais
float mat[4][3]; //Tabela com 4 linhas
//e 3 colunas
struct Tipos compostos de dados
struct ponto {
int x;
int y;
}
struct ponto p;
p.x = 10;
p.y = 20;
typedef Definição de novos tipos de dados
typedef struct ponto {
int x;
int y;
} Ponto;
Ponto p;
p.x = 10;
24
Unidade I
1.6.4 Cast
A conversão de um tipo de dados em outro é conhecida como cast de tipo, um molde, ou coerção, para
fazer conversão. Por exemplo, para armazenar um valor long em um inteiro simples (int), é necessário
fazer do long para int. É possível converter os valores de um tipo para outro explicitamente usando o
operador de conversão da seguinte maneira:
Figura 13
Figura 14
Observamos que o resultado é 3, mas o resultado da divisão da linha 8 deveria ser 3,4. Mesmo a
variável média sendo do tipo double o cálculo resulta em um número inteiro, assim sendo as casas após
a vírgula são desprezadas.
Para corrigir o problema, a divisão será formatada com um cast para double.
Figura 15
Figura 16
25
ESTRUTURAS DE DADOS
2 ALGORITMOS E SOLUÇÃO DE PROBLEMAS
Todas as ações no nosso dia a dia seguem um algoritmo, seja um ato intencional, seja um ato
instintivo. Com o decorrer da vida, conforme vamos crescendo e aprendendo, novas instruções se
incorporam ao nosso repertório. A experiência humana é um repertório de algoritmos que foram
se acumulando durante a vida. O algoritmo é um conjunto finito de instruções, de comandos, de ações
que tem como objetivo a resolução de uma tarefa, ou a resolução de um problema.
Constam na sequência outras definições sobre algoritmos:
Para Forbellone (1993, p. 3),
Algoritmo é uma sequência finita de instruções ou operações cuja execução,
em tempo finito, resolve um problema computacional, qualquer que seja
sua instância. [...] Algoritmo é uma sequência de passos que visam atingir
um objetivo bem‑definido.
Já Manzano (1996, p. 6) considera que “algoritmo são regras formais para a obtenção de um resultado
ou da solução de um problema, englobando fórmulas de expressões aritméticas”. Temos na sequência a
descrição dada por Ascencio e Campos (2003, p. 1): “algoritmo é a descrição de uma sequênciade passos
que deve ser seguida para a realização de uma tarefa”.
Por fim, consta a definição de Farrer et al. (1999, p. 14):
Ação é um acontecimento que, a partir de um estado inicial, após um período
de tempo finito, produz um estado final previsível e bem‑definido. Portanto
um algoritmo é a descrição de um conjunto de comandos que, obedecidos,
resultam numa sucessão finita de ações.
2.1 Introdução ao nivelamento de algoritmos
Diferentes algoritmos podem realizar a mesma tarefa usando um conjunto distinto de instruções em
mais ou menos tempo, espaço ou esforço do que outros. Tal diferença pode ser reflexo da complexidade
computacional aplicada, que depende de estruturas de dados adequadas ao algoritmo. A existência de
um algoritmo para resolver um problema não implica necessariamente que ele possa de fato fazê‑lo na
prática. Há restrições de tempo e espaço de memória a serem consideradas.
Se até agora os algoritmos foram vistos como um conjunto de instruções computacionais para
resolver problemas gerais, a partir de agora serão entendidos como uma sequência de técnicas lógicas
para a solução de problemas fundamentais de tratamento a dados como ordenações de busca, acesso
a informações.
26
Unidade I
Sendo assim, nivelamento é desenvolver uma métrica, ou um modelo de avaliar para analisar a
eficiência de algoritmos com a mesma finalidade, com lógicas de programação diversas, sem levar em
consideração todos os detalhes do dispositivo no qual o algoritmo está executando.
2.2 Conceito de análise de algoritmos
Nesta disciplina veremos apenas os conceitos da análise de algoritmos, não faremos uma abordagem
mais profunda.
Conforme Cormen (2012, p. 14),
Algoritmos diferentes criados para resolver o mesmo problema muitas
vezes são muito diferentes em termos de eficiência. Essas diferenças
podem ser muito mais significativas que as diferenças relativas a
hardware e software.
Analisar um algoritmo significa prever os recursos de que ele necessita. Verificando‑se algoritmos
candidatos para a solução de um problema, pode‑se identificar um que seja mais eficiente, ou então
apontar também um descartável devido à qualidade inferior no processo.
Na análise do desempenho de um algoritmo, precisam ser observados os seguintes parâmetros:
• Tempo de execução: quanto tempo um código leva para ser executado.
• Uso de memória volátil: quantidade de espaço ocupada na memória principal do computador.
O tempo de execução de um algoritmo será representado por uma função de custo T, onde T(n) é a
medida do tempo total necessário para executar um algoritmo para um problema de tamanho n. Esse
custo em tempo de execução é o quanto um programa demora para encerrar a sua execução. Define‑se
então que T é chamada de função complexidade de tempo do algoritmo. Se T(n) é a medida de memória
necessária para a execução do algoritmo de tamanho n, então T é chamada função de complexidade de
espaço. Observa‑se que, como há a dependência do hardware, T(n) não representa diretamente o tempo
de execução, mas o número de vezes que certa operação relevante é executada. Conforme Borin (2020),
a função custo T(n) de um algoritmo qualquer pode ser dada como:
T(n) = Ttempo+Tespaço
Sendo T tempo o custo em tempo de execução e Tespaço o custo de ocupação de memória.
Para entender o custo ao executar um programa simples, limitando‑se a um processador único,
considerando‑se o processamento simples e cada operação com o peso, temos o exemplo simples. Em
um programa que executa o laço com a quantidade de iterações determinada pelo valor atribuído na
variável n, o tempo será maior quanto maior a grandeza do número.
27
ESTRUTURAS DE DADOS
Figura 17
Para analisar executando passo a passo, podemos contar a quantidade de instruções realizadas no
processamento do programa.
Quadro 6
Passo a passo
Linha Instruções Operações Quantidade
5 n=5 n=5 1
6 for (i=0;i<=n;i++){ i=0 i<=0 2
7 //Laço 0
6 for (i=0;i<=n;i++){ i++ i<=0 2
n vezes
7 //Laço
6 for (i=0;i<=n;i++){ i++ i<=0 2
7 //Laço
6 for (i=0;i<=n;i++){ i++ i<=0 2
7 //Laço
6 for (i=0;i<=n;i++){ i++ i<=0 2
7 //Laço
6 for (i=0;i<=n;i++){ i++ i<=0 2
Ao processar a linha 5, a atribuição na variável n, bem como a inicialização da variável i e a primeira
verificação do laço na linha 6 acontecem apenas uma vez.
Quadro 7
Linha Instruções Operações Quantidade
5 n=5 n=5 1
6 for (i=0;i<=n;i++){ i=0 i<=0 2
6 for (i=0;i<=n;i++){ i++ i<=0 2n
28
Unidade I
O laço da linha 6 é executado n vezes, assim sendo o custo do tempo de execução do programa é
dado por:
T(n) = 2n + 3
O primeiro exemplo é simples apenas para entender o funcionamento do custo de tempo. Ao inserir
condicionais no programa, conforme os dados de entrada, os custos podem ter resultados distintos, pois
os caminhos tomados no processamento conseguem executar quantidades diferentes de instruções.
Ao montar um programa para apresentar o maior valor contido em um vetor, temos o programa
a seguir:
Figura 18
Neste caso, o custo é calculado da seguinte forma:
Quadro 8
Passo a passo
Linha Instruções Operações Quantidade
5 maior = v[0]; maior = v[0] 1
6 i=0; i=0 1
7 while (i<4){ (i<4) 1
8 if (v[i]>=maior) (v[i]>=maior) 1
n vezes
9 maior=v[i]; maior=v[i] 1
10 i++; i++ 1
7 while (i<4){ (i<4) Laço 1
Desta forma, o custo do programa para a entrada v={1,2,3,4} é:
T(n) = 4n + 3
29
ESTRUTURAS DE DADOS
O vetor v está em ordem crescente e considera‑se que em todos os casos a linha 9 será executada,
ou seja, é o pior caso. Alterando a entrada em ordem decrescente, fica da seguinte forma:
Figura 19
O custo para a entrada está em ordem decrescente v={4,3,2,1}, ao ser processado o seu resultado é:
Quadro 9
Passo a passo
Linha Instruções Operações Quantidade
5 maior = v[0]; maior = v[0] 1
6 i=0; i=0 1
7 while (i<4){ (i<4) 1
8 if (v[i]>=maior) (v[i]>=maior) 1
n vezes10 i++; i++ 1
7 while (i<4){ (i<4) Laço 1
O custo do programa para a entrada v={4,3,2,1} é:
T(n) = 3n + 3
Logo, o resultado do custo de tempo é menor. De acordo com a figura a seguir, observa‑se que
conforme a quantidade de dados n aumenta, a diferença entre o melhor e o pior caso também o faz.
Ainda é importante observar que independentemente dos dados de entrada no programa apresentado,
o resultado estará entre o melhor e o pior caso.
30
Unidade I
100
200
300
400
500
600
700
800
900
1000
Custo de tempo
100
0
20 30 40 50
n
pior caso melhor caso
60 70 80 90 100
Figura 20 – Gráfico do custo de tempo em função da quantidade de dados
No caso, o programa é bastante simples, mas a grande maioria deles é mais complexa e seria muito
trabalhosa uma contagem linha a linha. Para contornar essa dificuldade, é possível outra abordagem, a
análise assintótica.
A análise assintótica é feita tentando extrapolar o conjunto de dados de entrada, tendendo‑os
ao infinito e tomando a liberdade de desprezar os outros. Desta forma, se considerarmos o melhor
caso, temos:
T(n) = 4n + 3 → T(n) = n
Na hipótese de laços encadeados, um laço dentro do outro, teremos n x n, ou seja, n2. Por exemplo:
31
ESTRUTURAS DE DADOS
Figura 21
O importante são os 3 laços intercalados (linhas 6, 7, 8), o que resultará pela análise assintótica um
custo com o comportamento n3. Desta forma, conforme laços são colocados dentro de outros, o custo
do tempo aumenta exponencialmente. O objetivo ideal é construir um algoritmo em que o custo seja
menor com uma grande quantidade de dados.
Na literatura temos notações utilizadas para comparar algoritmos. De acordo com Ascencio (2002),
essas notações determinam a complexidade assintótica de um código para diferentes situações e
dependentes de um conjunto de dados.
• Grande-O (Big-O): define o comportamento assintótico superior, possui mais instruções sendo
executadas e é o pior caso de um algoritmo.
• Grande-ômega (Big-Ω): define o comportamento assintótico inferior, possui menos instruçõessendo executadas e é o melhor caso do algoritmo (caso ótimo).
• Grande-teta (BIG-Θ): define o comportamento assintótico firme, trata‑se do comportamento
considerado na grande maioria dos casos, é o caso médio de um algoritmo.
3 REVISÃO DE ARRANJOS
Uma estrutura de dados que utiliza somente um tipo de dado é conhecida como dados homogêneos.
Variáveis compostas homogêneas são aquelas em que as posições de memória são identificadas por um
mesmo nome e individualizadas por índices, possuindo todos os conteúdos compostos do mesmo tipo. Assim,
como visto anteriormente, os vetores (também conhecidos como estruturas de dados unidimensionais) e
as matrizes (estruturas de dados bidimensionais) seguem a estrutura dos dados homogêneos.
32
Unidade I
A forma mais simples de estruturar um conjunto de dados é por meio de vetores. Os vetores são utilizados
quando há muitas variáveis do mesmo tipo em um programa. É possível imaginá‑los como uma “fila” de
variáveis do mesmo tipo e com mesmo nome que é identificada por um número sequencial. Portanto, um
vetor pode ser definido como um conjunto de variáveis exatamente do mesmo tipo que armazena, cada
um associado a um número, que se refere à posição de armazenamento e é conhecido como índice.
Cada posição sua é acessada individualmente de forma direta, podendo ser lida ou escrita diretamente,
como uma variável, conforme vimos, e sem obedecer a nenhuma regra ou ordem preestabelecida. Logo,
os vetores podem ter acesso aleatório.
Segundo Laureano (2008, p. 2),
O vetor é uma estrutura de dados linear que necessita de somente um
índice para que seus elementos sejam endereçados. É utilizado para
armazenar uma lista de valores do mesmo tipo, ou seja, o tipo vetor
permite armazenar mais de um valor em uma mesma variável. Um dado
vetor é definido como tendo um número fixo de células idênticas (seu
conteúdo é dividido em posições). Cada célula armazena um e somente um
dos valores de dados do vetor. Cada uma das células de um vetor possui seu
próprio endereço, ou índice, através do qual pode ser referenciada. Nessa
estrutura todos os elementos são do mesmo tipo, e cada um pode receber
um valor diferente. Algumas características do tipo vetor:
• Alocação estática (devem‑se conhecer as dimensões da estrutura no
momento da declaração);
• Estrutura homogênea;
• Alocação sequencial (bytes contíguos);
• Inserção/exclusão;
• Realocação dos elementos;
• Posição de memória não liberada.
6,0
1
7,0
2
2,5
3
10,0
4
9,5
5
8,0
6
elementos
índices
5,5
0
Figura 22 – Vetor contendo notas
Essa figura ilustra um vetor de sete posições, preenchido com notas. Assim, por exemplo, no elemento
de índice 3, o conteúdo corresponde à nota 2,5; em outro caso, no elemento de índice 5, temos a nota 9,5.
Logo, na linguagem C, a declaração do vetor é:
float v[7];
33
ESTRUTURAS DE DADOS
Observação
Devemos tomar cuidado, pois o índice do vetor na linguagem C sempre
se inicia em zero. Portanto, na declaração anterior, os elementos vão
de v[0] até v[6].
Uma característica na linguagem C é que os vetores também podem ser inicializados na declaração:
float v[7]={ 5.5, 6.0, 7.0, 2.5, 10.0, 9.5, 8.0};
Ou simplesmente:
float v[ ]={ 5.5, 6.0, 7.0, 2.5, 10.0, 9.5, 8.0};
O programa aloca, automaticamente, sete espaços de inteiros na memória.
A atribuição de um elemento do vetor a uma variável é feita utilizando a seguinte sintaxe:
variável=v[índice];
Assim, na operação notaPedro<‑notas[2], a variável NotaPedro passará a armazenar o valor 7,0.
float notaPedro=v[2];
A atribuição de um valor a um elemento do vetor é feita utilizando a seguinte sintaxe:
v[índice]=valor;
Para a atribuição do valor 8,0 na posição 2, o comando será:
v[3]=7.0;
6,0
1
7,0
2
8,0
3
10,0
4
9,5
5
8,0
6
elementos
índices
5,5
0
Vetor: v
Figura 23 – Nova situação do vetor
Quando os vetores são multidimensionais eles recebem o nome de matrizes; os mais comuns são os
bidimensionais. Assim, a matriz bidimensional é uma estrutura de dados que necessita de um índice para
referenciar a linha e outro para referenciar a coluna e localizar o elemento da informação.
34
Unidade I
7,0
1
1
8,5
2
2
10
3
elementos
índices
8,5
0
0
4,0 6,0 5,53,0
7,5 6,0 5,07,0
Figura 24 – Representação de uma matriz bidimensional
Na linguagem C podem ser criadas matrizes bidimensionais. Portanto, para declararmos uma matriz
de valores reais com três linhas e quatro colunas, fazemos:
float classe[3][4];
Ao ser declarada a matriz, o C reserva um espaço de memória para armazenar os 12 elementos de
maneira contínua, organizada linha a linha. Então, esse espaço é preenchido com a seguinte declaração:
float classe[3][4] = {{ 8.5, 7.0, 8.5, 10.0},{ 3.0, 4.0, 6.0, 5.5},{ 7.0, 7.5,
6.0, 5.0}};
Como a linguagem C ignora os caracteres de quebra de linha, para ficar mais fácil de visualizar
durante a programação, é possível declarar a matriz da seguinte forma:
float classe[3][4] = {{ 8.5, 7.0, 8.5, 10.0}
,{ 3.0, 4.0, 6.0, 5.5}
,{ 7.0, 7.5, 6.0, 5.0}};
Lembrete
Como as matrizes são um caso particular de vetores, os índices também
iniciam em zero.
Saiba mais
O uso típico de matrizes é o jogo da batalha naval. Consta a seguir
exemplo simples de jogo em uma matriz 5 x 5.
C PROGRESSIVO. Batalha naval em C. [s.d.]. Disponível em:
https://bit.ly/39HP0oq. Acesso em: 10 maio 2022.
35
ESTRUTURAS DE DADOS
3.1 Representação linear de matrizes
Como vimos, ao inicializar um vetor com valores, é desnecessário declarar a sua dimensão. Trata‑se de
uma característica muito importante para começar a entender como a linguagem C manipula a memória.
A fim de ilustrar o funcionamento, consta a linguagem ao executar a linha a seguir:
float v[ ]={ 5.5, 6.0, 7.0, 2.5, 10.0, 9.5, 8.0};
A execução ao encontrar float v[ ] entende que receberá a informação { 5.5, 6.0, 7.0, 2.5, 10.0, 9.5,
8.0}; ou seja, sete números do tipo float declarados à esquerda, irá procurar na memória RAM um
espaço livre contíguo que o vetor caiba. Após encontrá‑lo, uma tabela das variáveis é preenchida com
o nome, o endereço e o tipo da variável. No exemplo, supondo que o tipo float ocupe cada 4 bytes, o
sistema reservará 28 bytes.
As matrizes trabalham da mesma forma, alocando os dados na memória de modo contíguo. Assim,
uma matriz pode ser declarada de várias maneiras.
Utilizando o exemplo já visto:
float classe[3][4] = {{ 8.5, 7.0, 8.5, 10.0}
,{ 3.0, 4.0, 6.0, 5.5}
,{ 7.0, 7.5, 6.0, 5.0}};
Outra maneira que a linguagem C permite é declararmos sequencialmente:
float classe[3][4] = { 8.5, 7.0, 8.5, 10.0, 3.0, 4.0, 6.0, 5.5, 7.0, 7.5, 6.0,
5.0};
Em razão da particularidade da administração da memória também podemos escrever da seguinte
forma:
float classe[ ][4] = { 8.5, 7.0, 8.5, 10.0, 3.0, 4.0, 6.0, 5.5, 7.0, 7.5, 6.0,
5.0};
O administrador da memória que possui apenas o segundo elemento é capaz de montar as linhas da
matriz. Sempre é necessário tomar cuidado com a relação linha/coluna, haverá erro se a declaração for:
float classe[3][4] = {{ 8.5, 7.0, 8.5},
{10.0, 3.0, 4.0},
{ 6.0, 5.5, 7.0},
{ 7.5, 6.0, 5.0}};
Não há conformidade entre linhas e colunas e a matriz inicializada.
3.2 Operações com cadeias
As cadeias de caracteres em C (Strings) são representadas por vetores do tipo char terminadas,
obrigatoriamente, pelo caractere nulo (‘\0’). Sempre que ocorre o armazenamento em cadeia é necessário
36
Unidade I
reservar um elemento adicional para o caractere de fim da cadeia. Todas as funções que manipulam
Strings (dos quais alguns serão estudados) recebem como parâmetro um vetor de char e processam
caractere por caractere, até encontrarem o caractere nulo, que sinaliza o final da cadeia.
Para imprimir uma cadeia utilizando a função printf, é necessário o especificador de formato %s.
A função, na verdade, recebe um vetor de char e imprime elemento por elemento, até encontrar o
caractere nulo (‘\0’).
O exemplo a seguir ilustra a representaçãode uma string. Como queremos representar a palavra
UNIP, composta de quatro caracteres, declaramos um vetor com dimensão 5 (um elemento adicional
para armazenarmos o caractere nulo no final da cadeia). O código preenche os elementos do vetor,
incluindo o caractere ‘\0’, e imprime a palavra na tela.
Figura 25
Ao executarmos, obtemos o seguinte resultado:
Figura 26 – Saída do programa com uma string montada em um vetor
Se o ‘\0’ não fosse colocado, a função printf não encontraria o fim da cadeia, e o programa mostraria
caracteres que eventualmente restam na memória até encontrar um ‘\0’.
Figura 27 – A função de impressão não reconhece o fim de caractere
37
ESTRUTURAS DE DADOS
3.3 Caracteres em C
A linguagem C não oferece um tipo de caractere em especial. Os caracteres são representados
por números inteiros convertidos no momento da exibição. O tipo char armazena valores inteiros do
tamanho de 1 byte, 8 bits, podendo então representar valores que variam de ‑128 a 127.
Observação
A correspondência entre os caracteres e seus códigos numéricos é
feita por uma tabela de códigos, a tabela ASCII, os códigos de 0 até 31 são
de uso do sistema, ou layout. Alguns de interesse são 0 que corresponde
ao NULL ou \0, 9 que corresponde ao TAB ou \t e a combinação de 10
(pulo de linha) com 13 (Volta ao início da linha) corresponde ao \n. Fora
desta faixa outros importantes são o 32 que é o espaço e o 127 que
aciona o botão delete.
A tabela completa pode ser obtida executando o programa:
int main (){
for (int i=0;i<32;i++){
printf(“%5d %5x %5c “,32+i,32+i,32+i);
printf(“| %5d %5x %5c “,64+i,64+i,64+i);
printf(“| %5d %5x %5c \n”,96+i,96+i,96+i);
}
}
Executando a saída é:
32 20 64 40 @ 96 60 `
33 21 ! 65 41 A 97 61 a
34 22 “ 66 42 B 98 62 b
35 23 # 67 43 C 99 63 c
36 24 $ 68 44 D 100 64 d
37 25 % 69 45 E 101 65 e
38 26 & 70 46 F 102 66 f
39 27 ‘ 71 47 G 103 67 g
40 28 ( 72 48 H 104 68 h
41 29 ) 73 49 I 105 69 i
42 2a * 74 4a J 106 6a j
43 2b + 75 4b K 107 6b k
44 2c , 76 4c L 108 6c l
45 2d ‑ 77 4d M 109 6d m
38
Unidade I
46 2e . 78 4e N 110 6e n
47 2f / 79 4f O 111 6f o
48 30 0 80 50 P 112 70 p
49 31 1 81 51 Q 113 71 q
50 32 2 82 52 R 114 72 r
51 33 3 83 53 S 115 73 s
52 34 4 84 54 T 116 74 t
53 35 5 85 55 U 117 75 u
54 36 6 86 56 V 118 76 v
55 37 7 87 57 W 119 77 w
56 38 8 88 58 X 120 78 x
57 39 9 89 59 Y 121 79 y
58 3a : 90 5a Z 122 7a z
59 3b ; 91 5b [ 123 7b {
60 3c < 92 5c \ 124 7c |
61 3d = 93 5d ] 125 7d }
62 3e > 94 5e ^ 126 7e ~
63 3f ? 95 5f _ 127 7f
A diferença entre caracteres e inteiros em C acontece apenas nos tratamentos deles. Assim, a
impressão de um mesmo valor pode ser feita de duas formas desde que os formatos sejam distintos.
Por exemplo:
Figura 28
Resulta em:
Figura 29 – Diferença entre %c e %d na saída
Como o número 97 corresponde ao caractere a, o printf ao mostrar na tela a variável c no formato %d,
exibe o conteúdo numérico, pois o formato é de número inteiro, e para a letra a, quando encontra o
formato %c, mostra o caractere correspondente ao número 97.
39
ESTRUTURAS DE DADOS
3.4 Manipulação de strings
A linguagem C, para manipular strings e caracteres, fornece algumas bibliotecas. Essas bibliotecas
dão algumas funções extras, além do scanf e do printf, para entrada e saída de caracteres e cadeias.
• Função putchar(): a função putchar() (put character) da biblioteca stdio.h imprime um caractere
na saída‑padrão (em geral, o monitor de vídeo).
• Função getchar(): a função getchar() (get character) da biblioteca stdio.h lê um caractere do
teclado, ou arquivo. Se ocorrer um erro ou uma condição de “fim de arquivo” durante a leitura,
a função retornará o valor da constante simbólica end of file (EOF) definida na biblioteca stdio.h,
permitindo facilmente a detecção de finais de arquivos. Essa função não retorna valores até
que o caractere de controle enter (\n) seja lido. Exemplo: consta a seguir o uso das funções
getchar() e putchar() em um programa que lê caracteres do teclado e os reimprime convertidos
para maiúsculos.
Figura 30
Figura 31 – Uso do getchar e do putchar
• Função puts(): a função puts() (put string) da biblioteca stdio.h mostra uma string na tela
acrescentando um enter (\n) ao final.
• Função gets(): a função gets() (get string) da biblioteca stdio.h lê uma string do teclado até o
operador digitar o enter. Essa instrução é útil e foi substituída pela fgets (variável, tamanho, stdin).
40
Unidade I
Exemplo:
Figura 32
Testando com o exemplo Joaquim Santos, temos:
Figura 33 – Saída usando fgets e puts
Veremos posteriormente mais funções de manipulações de cadeias em Conceitos de TAD cadeias.
3.5 Modularização
Quando um programa é muito grande, por diversas vezes, torna‑se difícil acompanhar a lógica
ali empregada. Com o uso das funções, pode‑se dividir tarefas volumosas de computação em tarefas
menores. A criação de funções evita a repetição de código. Quando um trecho do programa é bastante
repetido, deve ser transformado em uma função, que será chamada diversas vezes daqueles lugares
onde aconteciam a repetição do código. Um programa bem‑estruturado deve ser pensado utilizando
funções externas de seu corpo principal. Tal processo é denominado modularização.
Os programas em C usualmente são montados com uma coleção de várias pequenas funções em
vez de utilizar poucas de maior tamanho. Na linguagem C, tudo é feito por meio de funções e de suas
bibliotecas. Algumas já foram utilizadas, como as da biblioteca stdlib.h para entrada e saída de dados.
Sintaxe da função em C:
<tipo de retorno> nome( <tipo1> var1, <tipo2> var2){
Corpo da função
return retorno;
}
O <tipo de retorno> pode ser qualquer um, até os criados por meio do struct. Um caso particular é
o tipo void (nulo). Isso significa que a função não retornará nenhum valor, ou seja, é um procedimento.
41
ESTRUTURAS DE DADOS
Vejamos na sequência um exemplo de declaração da função fatorial:
Figura 34
Exemplo de aplicação
Vamos fazer um programa para calcular a quantidade de combinações simples. A fórmula matemática
da combinação é:
Ca,b= b!(a‑b)!
a!
Assim, tomando a combinação de três elementos dois a dois, temos:
C3,2= = 32!(3‑2)!
3!
Se considerarmos o conjunto de combinações de três letras (a, b, c) tomando‑as duas a duas,
independentemente da ordem, teremos (a, b), (a, c) e (b, c), ou seja, três.
Para montarmos o programa, como já vimos em laços, podemos montar os três fatoriais a!, b! e (a‑b)!
e fazer as operações entre eles.
O programa montado da forma tradicional ficará:
Figura 35
42
Unidade I
Figura 36 – Saída do programa de combinação simples com o programa sem modularização
Para modularizar, analisaremos o programa procurando por repetições de código.
Figura 37
Podemos observar que o seguinte trecho se repete durante o programa:
Figura 38
É interessante, portanto, transformá‑lo em uma função.
Como o return será fat, um número inteiro, e onde está a interrogação, um número que será o
fatorial a ser calculado, podemos montar a seguinte função:
43
ESTRUTURAS DE DADOS
Figura 39
Assim sendo, remontamos o programa agora modularizando a função.
Figura 40
Figura 41 – Saída do programa de combinação simples com o programa modularizado
Lembrete
Com o uso de diversas funções em um único programa, muitas vezes entre
os programadores inexperientes, pode acontecer de programar duas vezes a
função main(), causando um erro na compilação. Devemos tomar cuidado.
44
Unidade I
3.5.1 Procedimentos e funções
A diferença entre uma função e um procedimento é o fato de a função retornar um valor, mas
o procedimento não. Formalmente não existem distinções estruturais entre ambos na linguagem C.
A diferença é que o procedimento é um tipo particular de função cujo retorno é void (nulo), sendo assim
não é necessário o return no bloco da função.
Durante a programação, a execução sempre começa no main(). Na verdade, ele próprioé uma função.
O tipo, até agora, é void, e o parâmetro é vazio. Os parâmetros da função main são aqueles passados
quando um programa é executado na linha de comando e alguns valores são digitados após o nome.
Dessa maneira, é feita a interação entre o ambiente operacional e o programa.
Observaremos a seguir um programa com uma função e dois procedimentos, lembrando que o main
no caso, pela declaração void, é um procedimento.
Figura 42
3.6 Tipos abstratos de dados
Antes de entrarmos em tipos abstratos de dados, devemos estudar como fazer tipos de dados próprios.
3.6.1 Tipo estrutura
Em C, é possível definir um tipo de dado cujos campos são compostos de vários valores de tipos mais
simples. Uma estrutura, em C, serve basicamente para agrupar diversas variáveis em um único contexto.
A sintaxe para a definição de uma estrutura será mostrada na sequência:
struct ponto {
float x;
float y;
};
45
ESTRUTURAS DE DADOS
Desta forma, a estrutura ponto passa a ser um tipo e então é possível declarar suas variáveis, como
em qualquer tipo normal visto até agora.
struct ponto p;
A linha de código declara p como sendo uma variável do tipo struct ponto. Os elementos de uma
estrutura podem ser acessados através do operador de acesso “ponto” (.). Assim, é válido escrever:
p.x=10.0;
p.y=5.0;
A variável p, definida dentro de main, é local como outra qualquer. Quando a declaração é encontrada,
aloca‑se, na pilha de execução, um espaço para seu armazenamento, isto é, um local suficiente para
armazenar todos os campos da estrutura (no caso, dois números reais).
3.6.2 Definição de “novos” tipos
A linguagem C permite criar nomes de tipos. Em geral, definimos nomes de tipos para as estruturas
com as quais nossos programas trabalham. Por exemplo, podemos escrever:
struct ponto {
float x;
float y;
};
typedef struct ponto Ponto;
Neste caso, Ponto passa a representar nossa estrutura de ponto, podendo ser utilizado como um tipo
conforme o programa a seguir:
Figura 43
46
Unidade I
Por fim, vale salientarmos que é possível definir a estrutura e associar mnemônicos para ela em um
mesmo comando:
typedef struct ponto {
float x;
float y;
};Ponto;
Voltando ao tipo abstrato de dados (TAD), trata‑se da especificação matemática de um conjunto de
dados e das operações que podem ser executadas sobre eles. Lembrando que:
• O conceito de TAD é dissociado do hardware.
• TAD define o que cada operação faz, mas não como faz.
Assim, um mesmo TAD pode ser concretizado (ou implementado) de diversas formas.
TADs são materializados pelas seguintes estruturas de dados:
• Lista encadeada: implementação com referências.
• Lista com alocação estática: implementação usando array.
Um programa geralmente agrupa muitos tipos e funções com funcionalidades relacionadas, com
uma finalidade bem definida em um mesmo propósito. Por exemplo, se analisarmos o stdio.h, ele tem
um conjunto de funções para a manipulação de entradas e saídas. Nos casos em que um módulo
define um novo tipo de dado e o conjunto de operações para manipular dados desse tipo, dizemos que
o módulo representa um tipo abstrato de dados (TAD). Nesse contexto, abstrato significa “abstraída a
forma de implementação”, ou seja, um TAD é descrito pela finalidade do tipo e de suas operações, e não
pela forma como está implementado.
Por exemplo, será criado um TAD para manipular uma conta bancária definida pela seguinte estrutura:
Figura 44
Onde o número é o número de uma conta bancária, e o seu saldo atual. Para manipular essa
estrutura, inicializando, depositando, sacando e mostrando o saldo, são criadas funções, bem como o
47
ESTRUTURAS DE DADOS
conjunto estrutura e as funções que formam o TAD. Os asteriscos (*), seta (‑>) e o “e” comercial (&) serão
explicados no item Alocação dinâmica de memória: ponteiros.
Figura 45
Desta forma, uma função main pode manipular as operações utilizando o TAD:
Figura 46
Uma boa técnica de programação é implementar os TADs em arquivos separados do programa
principal. Para isso, geralmente separa‑se a declaração e a implementação em dois arquivos:
• NomeDoTAD.h: com a declaração.
• NomeDoTAD.c: com a implementação.
O programa ou outros TADs que utilizam o seu TAD devem dar um #include no arquivo.h. Assim
sendo, se quisermos podemos separar o programa anterior nos seguintes arquivos:
48
Unidade I
Figura 47 – Arquivo ContaBancaria.h na pasta \include
Figura 48 – Arquivo ContaBancaria.c na pasta \include
Figura 49 – Arquivo <NomedoArquivoPrincipal>.c na pasta do projeto
49
ESTRUTURAS DE DADOS
Consta a seguir o código completo:
#include <stdio.h>
typedef struct {
int numero;
double saldo;
}contabancaria;
void inicia(contabancaria* conta, int numero, double saldo) {
conta‑>numero = numero;
conta‑>saldo = saldo;
}
void deposito(contabancaria* conta, double valor) {
conta‑>saldo += valor;
}
void saque(contabancaria* conta, double valor) {
conta‑>saldo ‑= valor;
}
void imprime(contabancaria conta) {
printf(“Numero: %d\n”, conta.numero);
printf(“Saldo: %f\n”, conta.saldo);
}
int main() {
contabancaria conta1;
inicia(&conta1, 1, 0.00);
printf(“\n Antes da movimentação: \n”);
imprime(conta1);
deposito(&conta1, 50.00);
saque(&conta1, 70.00);
printf(“\n Depois da movimentação: \n”);
imprime(conta1);
}
3.6.3 Conceitos de TAD cadeias
A linguagem C possui uma biblioteca reunindo funções que trabalham com a estrutura de cadeias
(Strings). A seguir serão apresentadas as mais usuais delas.
• Função strlen(): a função strlen() (string length), da biblioteca string.h, retorna o tamanho de
uma cadeia, conta a quantidade de caracteres até encontrar um nulo (\0) e não o conta. Exemplo:
o programa retorna o tamanho da cadeia digitada.
50
Unidade I
Figura 50
A biblioteca usada é a string.h. O resultado é:
Figura 51 – Contando a quantidade de caracteres
• Função strcat(): a função strcat() (string concatenate), da biblioteca string.h, tem duas cadeias
concatenadas (unidas sequencialmente). Ela recebe duas strings como argumento e copia a
segunda delas no final da primeira. Exemplo: fazer um programa que junte duas cadeias.
Figura 52
Figura 53 – Fazendo a concatenação de caracteres
• Função strcpy(): o strcpy (string copy) é uma função da biblioteca string.h que copia uma string
em uma variável. Ao contrário da função strcat, que faz a concatenação, esta sobrescreve a cadeia
caso ela já tenha um valor armazenado. Exemplo:
51
ESTRUTURAS DE DADOS
Figura 54
Neste programa, a saída é dada por duas variáveis, sendo que nenhuma delas é aquela em que foi
dada a entrada. O resultado do programa é:
Figura 55 – Saída da função strcpy
• Função strcmp(): na função strcmp() (string compare), da biblioteca string.h, duas cadeias são
comparadas. A comparação é feita caractere a caractere, até encontrar a primeira diferença entre
eles; conforme a diferença, a função devolve um valor distinto, usando o seguinte critério:
— < 0, se cadeia1 < cadeia2;
— = 0, se cadeia1 = cadeia2;
— > 0, se cadeia1 > cadeia2.
Executando o programa a seguir:
Figura 56
52
Unidade I
Cujo resultado será:
Figura 57 – Comparações entre caracteres
Saiba mais
Para obter acesso a outras funções da biblioteca strings, consulte o
seguinte livro:
MANZANO, J. A. Linguagem C: acompanhada de uma xícara de café. São
Paulo: Érica/Saraiva, 2015.
4 ALOCAÇÃO DINÂMICA DE MEMÓRIA: PONTEIROS
Para declarar o vetor de um certo tamanho, é necessário saber de antemão quantos elementos
iriam compô‑lo. Devemos prever o número de elementos no vetor durante o processo da codificação.
Seu pré‑dimensionamento é um fator que limita a programação. Uma solução seria superdimensionar
o vetor para tentar contornar essa limitação, acarretando um desperdício de memória. Esse desperdício
é inaceitável em diversas situações, como em aplicativos portáteis os quais estão sempre sujeitos a ter
falta de memória.
A linguagem C oferece meios de utilizar e racionalizar o uso da memóriadurante a execução do
programa, ou seja, podemos alocar memória dinamicamente. Esse recurso permite ao programa reservar
elementos ou registros de modo dinâmico, sem desperdício de memória.
4.1 O uso da memória
Basicamente existem três maneiras de reservarmos espaço de memória para o armazenamento de
informações. São eles:
• Por meio de variáveis globais (e estáticas): o espaço reservado para uma variável existirá
enquanto o programa estiver sendo executado.
• Por meio de variáveis locais: o espaço na memória existe apenas no período em que a função
que declarou a variável está sendo executada, sendo liberado assim que a execução terminar.
Como consequência, a função pai, a que chama, não pode fazer referência ao espaço local da
função chamada (filho). As variáveis globais ou as locais podem ser simples ou vetores. Para
53
ESTRUTURAS DE DADOS
os vetores, é necessário informar o número de elementos que ele comporta, caso contrário o
compilador não terá a informação sobre o tamanho do espaço a ser reservado.
• Por meio de solicitação ao programa que aloque dinamicamente um espaço na memória
durante sua execução: o espaço alocado permanece reservado até que seja liberado explicitamente
pelo programa, através de comando específico.
Para o entendimento do processo de uso da memória, executaremos a simulação da execução de
um programa. Inicialmente observa‑se a memória RAM de um computador sem nenhum programa
sendo executado.
RAM
Figura 58
Ao executar um programa, o sistema operacional carrega o código compilado na memória, o
programa é carregado na memória RAM a partir de um HD, ou de um servidor de rede, ou de um SSD e
outros dispositivos de armazenamento de memória não volátil, byte a byte.
54
Unidade I
RAM
Figura 59
Uma vez carregado com o código na memória do computador, o sistema passa a ser executado pelos
comandos carregados na memória.
RAM
Programa
Figura 60
Na linguagem C, e em outras linguagens fortemente tipadas, a primeira etapa é reservar o
espaço para as constantes e variáveis globais, pois essas são estáticas, ou seja, não serão criadas
constantes nem variáveis globais durante a execução do resto do programa, e terão endereço fixo
facilitando o acesso.
55
ESTRUTURAS DE DADOS
RAM
Variáveis globais e
constantes
Programa
Figura 61
Cada vez que uma determinada função é chamada, o sistema reserva o espaço necessário para
as variáveis locais dela. Simultaneamente, a pilha do controle da chamada das funções armazena a
sequência de chamada e o estado geral das memórias operacionais. Conforme elas são chamadas,
a memória livre cresce, e a pilha é atualizada. Quando as funções são encerradas, o sistema consulta
na pilha o ponto em que a função foi chamada, retornando o programa nele e liberando o espaço das
variáveis e da pilha criados dinamicamente. O sistema administra de tal forma que as variáveis locais
sejam criadas e destruídas de modo dinâmico no espaço livre de memória, enquanto a pilha de controle
utiliza a memória RAM do final para o início.
RAM
Variáveis globais e constantes
Variáveis locais
Memória livre
Pilha
Programa
Figura 62 – As variáveis locais são alocadas e desalocadas no início da
memória livre, enquanto a pilha de controle das funções ocorre no fim
56
Unidade I
4.2 Variáveis
Antes de falarmos de ponteiro de variáveis, é necessário entendermos o processo envolvido
quando uma variável é declarada. Para tanto, faremos um ambiente virtual simulando as operações.
Nele, possuímos a memória RAM, uma vez que cada byte tem um endereço numérico, conforme
representação a seguir.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Figura 63 – Simulação da memória RAM
Os dados ocuparão uma quantidade de bytes conforme o tipo da variável armazenada. Para o
sistema saber onde uma variável está, é feita uma tabela associando o nome ao endereço e o tipo que
será encontrado naquele lugar.
Tabela das variáveis
nome endereço tipo
Figura 64 – Tabela de variáveis que relaciona o nome ao endereço
Ao fazer a declaração:
int idade;
O administrador de memória localiza um espaço em que é possível ocupar a quantidade de bytes
necessária para armazenar um número inteiro.
Supondo que uma variável do tipo inteiro ocupe quatro bytes. Na memória RAM a seguir, o espaço
é localizado no endereço 125.
57
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
espaço livre para ocupar 4 bytes
Figura 65 – Memória RAM já com informações que
estão ocupando os bytes pintados em preto
Uma vez localizado um espaço livre, a tabela é preenchida com as informações associando o nome,
o endereço e o tipo da variável.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome idade
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
idade 125 int
Figura 66 – Situação após a declaração da variável
Assim, sempre que necessário o programa manipular uma variável, o administrador de memória
consulta a tabela de variáveis para localizar o conteúdo.
4.3 Ponteiro de variáveis
Para entendermos ponteiros, utilizaremos um exemplo no mundo real.
Na cidade de São Paulo existe um edifício famoso, diante do qual acontecem as comemorações
dos campeonatos conquistados pelos times paulistas, o prédio da Gazeta. Supondo que você não seja
paulistano, mas deseje comemorar o campeonato do seu time, e precise chegar ao referido local a partir
58
Unidade I
de um terminal de ônibus, de uma estação de trem ou de um aeroporto, indo de táxi ou Uber, pode pedir
para que o levem ao prédio da Gazeta, ou então para a Avenida Paulista, n. 900.
Nas diversas disciplinas sempre foi dito em princípio que cada vez que uma variável é declarada, um
espaço é alocado na memória para armazenar valores. Tecnicamente, é feita mais do que uma simples
alocação de espaço. Como vimos, o sistema tem uma tabela, em que armazena o nome da variável, o
endereço e o tipo de valor que será armazenado.
Em muitas ocasiões, seria interessante ter a possibilidade de acessar o conteúdo de um local da
memória de outras maneiras que não apenas pelo nome, mas pelo endereço de memória, como no exemplo
do prédio da Gazeta em que é possível ser localizado tanto pelo seu endereço quanto pelo seu nome.
A linguagem C tem uma maneira especial de uma variável armazenar endereços. Ela se chama
variável ponteiro ou simplesmente ponteiro. A declaração de uma variável é feita da seguinte forma:
<tipo> *nome;
Por exemplo:
int *p;
O programa reserva um espaço na memória para uma variável chamada p, que irá guardar um
endereço, e esse endereço armazenado conterá uma informação do tipo inteiro. O símbolo * identifica
que uma variável é do tipo ponteiro. A variável ponteiro sempre irá armazenar um endereço de memória,
independentemente do seu tipo.
Para acessar os endereços de memória, a linguagem oferece dois operadores unários:
• & (“endereço de”): aplicado a variáveis, resulta no endereço da posição da memória reservada
para a variável.
• * (“conteúdo de”): aplicado a variáveis do tipo ponteiro, acessa o conteúdo do endereço de
memória armazenado pela variável ponteiro.
Vamos analisar o seguinte trechode programa:
59
ESTRUTURAS DE DADOS
Figura 67
Analisando linha a linha, temos:
Ao executar a linha 4, a variável a, como vimos, é criada em um espaço disponível para o tamanho
de um número inteiro na memória RAM. No exemplo, encontra‑se no endereço 117 e está registrado na
tabela de variáveis.
Figura 68
60
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a 117 int
a
Figura 69 – Criação da variável a na memória RAM
Executando a linha seguinte é criado o ponteiro p, considerando que particularmente neste exemplo
uma variável que armazena um endereço de memória ocupe três bytes, o administrador de memória
encontra um espaço no endereço 125. Ele é alocado e a tabela atualizada.
Figura 70
61
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
117
125
int
int
p
a
Figura 71 – Criando a variável ponteiro p
Na linha 6, para armazenar o valor 5 na variável a, o sistema recorre à tabela e localiza o endereço
da variável para gravar o valor na memória.
Figura 72
62
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
117
125
int
int
p
a
Figura 73 – O valor 5 é armazenado na variável a, localizado no endereço 117
Na variável ponteiro p é armazenado o endereço da variável a. O endereço da variável está na
própria tabela na coluna endereço. Desta forma, o conteúdo é armazenado no local reservado para o
ponteiro p na memória RAM.
Figura 74
63
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &117
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
117
125
int
int
p
a
Figura 75 – Armazenando o endereço de a no ponteiro p
Na linha 8 observa‑se a principal característica do uso de ponteiro. A variável p serve para indicar
o local a ser alterado. No caso, temos o valor 6 atribuído no local apontado por p, ou seja, no endereço
que é o conteúdo da variável p.
Figura 76
64
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &117
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
117
125
int
int
p
a
Figura 77 – O valor é armazenado no endereço contido na variável ponteiro p
O valor contido na variável a será mostrado já com o conteúdo alterado pela operação *p.
Figura 78
65
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &117
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
117
125
int
int
p
a
Figura 79 – Localizando o conteúdo da variável a
Na linha 10 é criada uma nova variável inteira. O administrador de memória procura um espaço
livre (&136), reserva a quantidade de bytes necessária para armazenar um número inteiro (no exemplo,
4 bytes) e atualiza a tabela.
Figura 80
66
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &117
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
b
117
125
136
int
int
int
p
a
b
Figura 81 – Criando uma nova variável inteira b
Agora o endereço de b é armazenado na variável ponteiro p.
Figura 82
67
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &117 &136
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
b
117
125
136
int
int
int
p
a
b
Figura 83 – Armazenando o endereço da variável b na memória RAM no ponteiro p
Na linha 12 o valor que está contido na variável a é atribuído no endereço apontado pelo ponteiro
p (&136), ou seja, o mesmo local da variável b.
Figura 84
68
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &136 6
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
b
117
125
136
int
int
int
p
a
b
Figura 85 – Conteúdo da variável a no endereço %136 apontado por p
Como a atribuição aconteceu no endereço da variável b, podemos sempre utilizar diretamente o
nome da variável.
Figura 86
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 6
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo &136 6
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
*p
b
117
125
136
int
int
int
p
a
b
Figura 87 – Mostrando diretamente a variável b
69
ESTRUTURAS DE DADOS
A flexibilidade do uso do ponteiro pôde ser observada, vimos que dá para manipular várias variáveis
utilizando apenas um ponteiro, dispensando o nome dela e muitas vezes precisamos editar e recompilar
o programa. Assim sendo, existem duas maneiras através das quais conseguimos trabalhar com memória
em C: por meio do nome da variável e pelo endereço dela.
4.4 Passagem de parâmetros por valor e por referência
Ao estudarmos funções, vimos que quando passamos informações para dentro delas, fazemos isso
por meio de parâmetros. Também aprendemos que a função devolve apenas um valor. O uso de ponteiros
amplia a possibilidade de utilização dos parâmetros e a solução de problemas de retornos múltiplos.
• Quando há passagem de parâmetros por valor, já que conteúdos (e não endereços) são passados
para as funções.
No exemplo a seguir, os valores de a e b são passados para a função troca.
Figura 88 – Saída mostrando a passagem de parâmetros por valor
O resultado de a é 7 e o de b é 5, pois efetivamente as variáveis a e b são locais da função main().
• Quando os parâmetros passados são endereços, dizemos que foram transmitidos por referência,como no exemplo a seguir.
70
Unidade I
Figura 89 – Saída do programa passando parâmetros por referência
Apesar de os programas serem praticamente idênticos, o resultado é bem diferente, pois o mecanismo
de trabalho foi bem diverso.
Exemplo de aplicação
Para entender o que acontece em cada um dos casos, faremos o teste de mesa nos próprios códigos.
Passagem por valor
Figura 90
71
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
Figura 91 – Início do programa
O programa começa com a memória vazia, executando a função main().
Figura 92
72
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Função: main
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
a b
Figura 93 – Criação das variáveis a e b do programa
As variáveis a e b da função são criadas e inicializadas. Nesse momento, o programa reserva os
espaços na memória RAM e atualiza a tabela das variáveis da função main().
Ao encontrar a chamada para a função troca, o programa continua reservando espaço da memória
para as variáveis que forem aparecendo e cria uma nova tabela para as suas variáveis locais, armazenando
os parâmetros de entrada. Em seguida, passa a executar a função troca.
Figura 94
73
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 7 5
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
109
113
int
int
Função: main Função: troca
a ab b
Figura 95 – Reserva de memória para as próximas variáveis e criação de tabela para as variáveis locais
Na execução das figuras 97, 99 e 103 é efetuado swap de valores entre as variáveis a e b.
Figura 96
74
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 7 5
nome a b a b temp
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
113
117
int
int
int
Função: main Função: troca
Figura 97 – O programa cria a variável temp do programa
Figura 98
75
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 7 5 7
nome a b a b temp
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
113
117
int
int
int
Função: main Função: troca
Figura 99 – Temp recebe o conteúdo da variável a da função troca
Figura 100
76
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 5 5 7
nome a b a b temp
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
113
117
int
int
int
Função: main Função: troca
Figura 101 – A variável a recebe o conteúdo da variável b da função troca
Figura 102
77
ESTRUTURAS DE DADOS
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 5 7 7
nome a b a b temp
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
113
117
int
int
int
Função: main Função: troca
Figura 103 – A variável b recebe conteúdo de temp troca
A variável b recebe o conteúdo da variável temp da função troca, e o programa retorna para o ponto
de chamada da função main().
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 5 5 7
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
113
117
int
int
int
Função: main Função: troca
a b
Figura 104 – Exibição dos conteúdos de a e b da função main() troca
78
Unidade I
A função main() exibe os conteúdos das variáveis a e b da própria função main(), os valores que
estão nos endereços da tabela de variáveis: main.
Figura 105
Passagem por referência
Figura 106 – Saída do programa passando parâmetros por referência
Apesar de os programas serem praticamente idênticos, o resultado é diferente, pois o mecanismo de
trabalho ocorre de modo diverso.
O programa começa com a memória vazia, executando a função main().
79
ESTRUTURAS DE DADOS
Figura 107
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Função: main
a b
Figura 108 – Inicialização, criação e inicialização das variáveis a e b
As variáveis a e b da função são criadas e inicializadas. Nesse momento, o programa reserva os
espaços na memória RAM e atualiza a tabela das variáveis da função main(). Até esse ponto, os dois
programas são iguais.
80
Unidade I
Figura 109
Na chamada da função começam as diferenças. A função troca recebe dois ponteiros do tipo
inteiro, lembrando sempre que ponteiros armazenam endereços e não valores absolutos. Na passagem
de parâmetro, são endereços de memória, em virtude do operador unário & aplicado em a e em b.
Assim, os parâmetros *a e *b da função troca são os endereços das variáveis a e b do programa pai.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 &101 &105
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
109111
int
int
Função: main Função: troca
a *ab *b
Figura 110 – Passagem da referência como parâmetro
Na função troca, a variável temporária temp é criada, depois ela receberá o valor contido no
endereço apontado pelo parâmetro *a. Já se observa a diferença em relação à passagem por valor.
81
ESTRUTURAS DE DADOS
Figura 111
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 7 5 &101 &105 7
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
111
113
int
int
int
Função: main Função: troca
a b *b temp*a
1
2
Figura 112 – *a aponta para o endereço &105 e o seu conteúdo é atribuído à variável temp
Na linha 4, o conteúdo do endereço apontado por *b da função troca é armazenado no local
apontado por *a, também da função troca, ou seja, no endereço &101, e receberá o valor que está no
endereço &105.
82
Unidade I
Figura 113
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5 5 &101 &105 7
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
111
113
int
int
int
Função: main Função: troca
a b *b temp*a
Figura 114 – O conteúdo do endereço apontado por *b é armazenado no endereço apontado por *a
A seguir, o conteúdo da variável auxiliar temp é atribuído no local apontado pela variável *b da
função troca. O programa retorna para o ponto em que foi chamado.
83
ESTRUTURAS DE DADOS
Figura 115
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5 7 &101 &105 7
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
111
113
int
int
int
Função: main Função: troca
a b *b temp*a
Figura 116 – O conteúdo de temp é copiado para o endereço apontado por *b
No retorno da função as variáveis estão trocadas, pois as operações são realizadas entre os locais
referenciados pelos parâmetros passados para a função.
84
Unidade I
Figura 117
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo 5 7 &101 &105 7
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
a
b
101
105
int
int
Tabela das variáveis
nome endereço tipo
a
b
temp
109
111
113
int
int
int
Função: main Função: troca
a b *b temp*a
Figura 118 – As variáveis estão com os valores trocados, pois foram manipuladas por referência
Observamos que a passagem de valor por referência permite que variáveis criadas em uma função
possam ser alteradas fora dela. Essa técnica se torna importante, principalmente, pelo fato de a
linguagem C não passar vetores e matrizes como parâmetros de uma função.
85
ESTRUTURAS DE DADOS
4.5 Passagem de vetores para funções
Como dito, a linguagem C não passa vetores como parâmetros de função; assim, a maneira para fazê‑lo
é passando o endereço da primeira posição do vetor. Dessa forma, para receber um vetor, é necessário
colocar uma declaração do mesmo tipo do vetor com a indicação de que é um ponteiro (usando o *).
Exemplo de aplicação
Dado o programa a seguir:
Figura 119
Na função main() é criado o vetor inteiro a[], que será passado como parâmetro para a função
incrvetor(). Assim, ela recebe o endereço do vetor a pela declaração int * v. Nesse caso, o vetor é chamado
de a na função main() e, dentro da função incrvetor(), é tratado como v. Como ambos têm o mesmo
endereço, eles se referem ao mesmo espaço de memória.
O resultado desse programa será mostrado na sequência:
Figura 120 – Saída do programa com passagem de vetor na função
O programa passa como parâmetros um número e um vetor; ele tem como elementos os valores
{1, 3, 5}. A função recebe o endereço do vetor, refere‑se a ele como v e acrescenta 1 a cada elemento;
assim, o vetor é alterado para {2, 4, 6}.
Lembrete
As bibliotecas de funções são anexadas ao programa principal pela
diretiva de pré‑compilação #include. Vimos até agora as bibliotecas
stdio.h e string.h.
86
Unidade I
4.6 Funções da biblioteca padrão
Muitas vezes o uso de vetores e matrizes fica limitado pelo fato de sabermos antecipadamente
a quantidade de elementos que serão necessários. O interessante seria ter condições de criar e
destruir elementos sem a limitação (apenas da máquina, e não da lógica), conforme forem surgindo
as necessidades.
A biblioteca stdlib.h possui algumas funções que permitem criar e trabalhar dinamicamente, ou seja,
durante a execução de um certo trecho do programa. Para isso, é preciso entender certas funções e
utilizar os conhecimentos do uso da memória.
A melhor forma de usar todas as funções necessárias é mostrando‑as manualmente, como no
seguinte trecho:
int *v;
v = (int*) malloc(10*sizeof(int));
Que é equivalente a simplesmente declararmos:
int v[10];
Para estudarmos esse trecho com detalhes, analisaremos minuciosamente como é escrita a
segunda linha.
Começaremos com a memória fictícia vazia, iniciada pelo endereço &101 e, logo na sequência,
veremos a tabela de variáveis sem nenhuma declaração.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
Figura 121 – Memória fictícia vazia, começando do endereço &101
87
ESTRUTURAS DE DADOS
Para termos uma referência, a variável ponteiro é criada. Ela apontará o vetor originado.
int *v;
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
*v 101 int
Figura 122 – O ponteiro *v alocado na posição &101
A primeira função é malloc(int), memory allocation ou alocação de memória. Ela reserva a quantidade
de bytes que é passada como parâmetro e retorna o endereço em que esse espaço de memória foi
reservado. Assim, para reservar o espaço de dez elementos inteiros a um vetor, supondo que cada valor
inteiro use 2 bytes (esse é um número puramente fictício, pois cada compilador pode ter um tamanho
diferente), será necessário reservar 20 bytes de memória.
malloc(20)
Considerando que 20 é 10* 2, dez elementos de 2 bytes. Ao utilizar a função, o administrador de
memória reserva a quantidade de bytes livres contíguos passados pelo parâmetro. No exemplo, malloc
encontra um endereço livre no endereço &103, a função devolverá o valor correspondente ao local em
que o espaço foi reservado.
Ao tentar alocar o valor retornado pela função em uma variável ponteiro, temos:
V=malloc(20);
O programa irá acusar erro, pois existe um conflitode informações: a variável v é um ponteiro
apontando para inteiro, e o espaço de memória não corresponde à informação de um valor inteiro.
Na prática, a função malloc retorna um número inteiro (int) correspondente ao endereço, enquanto a
variável v foi declarada com int *.
Voltando ao número 20, não é sabido ao certo se a quantidade de bytes que uma variável do tipo
inteiro ocupa é realmente 2 bytes. Para descobrir quantos bytes um tipo ocupa, a biblioteca nos fornece
a função sizeof(tipo), tamanho de.
88
Unidade I
sizeof(<tipo>)
Essa função retorna um número inteiro que um tipo ocupa, seja ele preexistente, seja criado por nós
com o typedef. Assim, por exemplo, ao fazer:
sizeof(int)
A função devolverá o número de bytes utilizados por uma variável do tipo int. Conforme o compilador,
o sistema operacional de um mesmo tipo pode ter tamanhos diferentes. Portanto, para montar um vetor
de 10 elementos inteiros, bastará fazer:
10 * sizeof(int)
Assim, para reservar o espaço, utilizamos a expressão como parâmetro da função malloc().
malloc(10 * sizeof(int))
Obter automaticamente o tamanho correto para alocarmos 10 números inteiros independe
do compilador.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
*v 101 int
Figura 123 – Alocado espaço para 10 números inteiros
O espaço alocado está de acordo com o espaço independente do compilador (no exemplo, 2 bytes
cada), mas voltando ao problema de armazenarmos o endereço do local reservado, temos:
v=malloc(10 * sizeof(int))
O erro persiste, pois v é um ponteiro inteiro, e o endereço devolvido pelo malloc é um valor absoluto.
89
ESTRUTURAS DE DADOS
Anteriormente, vimos a conversão de tipos, o chamado cast. Ela irá compatibilizar o valor absoluto
retornado pelo malloc no tipo correspondente do ponteiro. Nesse caso, é possível afirmar que é feita a
formatação ou a adequação do espaço alocado. Como o tipo de v é um ponteiro inteiro, aplicaremos
a conversão do tipo int *.
Assim, a expressão fica:
int *v;
v=(int *)malloc(10*sizeof(int));
A informação devolvida pelo malloc fica completa: com um endereço e o tipo correspondente. Ao
aplicarmos o cast, a memória ficará assim:
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo &103
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
*v 101 int
Figura 124 – Cast aplicado dividindo o espaço reservado pelo malloc em inteiros
O programa a seguir monta manualmente o vetor v. Nele, é criado um vetor sem a necessidade de
declaração int v[10].
Figura 125
90
Unidade I
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo &103 13 23
nome
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
*v 101 int
*v v[0] v[1] v[2] v[3] v[4]
v[8]v[6] v[7] v[9]v[5]
Figura 126 – Situação após a execução do programa, criando o vetor manualmente
Utilizando a criação manual, o valor 10 no exemplo pode ser substituído por uma variável. Conforme
o valor dela, é possível um vetor em um mesmo código fonte ter tamanhos diferentes.
Para liberar um espaço de memória alocado dinamicamente, utiliza‑se a função free da biblioteca
stdlib.h. Ela recebe como parâmetro o ponteiro da memória a ser liberada e o espaço alocado é liberado
para outros usos futuros. Assim, a fim de liberar o vetor v, é colocado no programa:
free(v);
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo &103
nome *v
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo
nome
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo
nome
Tabela das variáveis
nome endereço tipo
*v 101 int
Figura 127 – Espaço liberado pelo free()
91
ESTRUTURAS DE DADOS
Uma vez definida a estrutura do vetor e seu espaço na memória RAM, os valores são armazenados
nos espaços múltiplos a partir do endereço inicial. Observe que supondo o tamanho do tipo float como
4 bytes (sizeof(float)), se o endereço inicial for 121 (121 + 0 × 4), o próximo será 125 (121 + 1 × 4), o
terceiro elemento 129 (121 + 2 × 4), e assim por diante.
Memória RAM
endereço 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
conteúdo &121
nome *v
endereço 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
conteúdo 5.5 6.0 7.0 2.5 10.0
nome v+0 x sizeof(float) v+1 x sizeof(float) v+2 x sizeof(float) v+3 x sizeof(float) v+4 x sizeof(float)
endereço 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
conteúdo 9.5 8.0
nome v+5 x sizeof(float) v+6 x sizeof(float)
Tabela das variáveis
nome endereço tipo
*v 103 int
δr u Ц միꝘ Щ ملسو هيلع هللا ىلص
Figura 128 – Valores armazenados no vetor
Pode‑se notar que o valor multiplicando o sizeof() corresponde ao índice de um vetor. A partir desta
operação, podemos chegar ao conceito de aritmética de ponteiros.
Observação
O ponteiro também tem uma aritmética própria. Ao fazermos a soma
de um número inteiro a um ponteiro, ele apontará para o endereço
com o avanço de múltiplos correspondentes ao tamanho do tipo
definido para ele.
Assim, no exemplo anterior podemos ter as seguintes equivalências:
Quadro 10 – Relação entre o ponteiro e o índice de um vetor
Expressão Ao fazer a expressão Equivale ao fazer a expressão
int *x = v+0 printf(“%d”,*x) printf(“%d”,v[0])
int *x = v+1 printf(“%d”,*x) printf(“%d”,v[1])
int *x = v+2 printf(“%d”,*x) printf(“%d”,v[2])
int *x = v+3 printf(“%d”,*x) printf(“%d”,v[3])
int *x = v+4 printf(“%d”,*x) printf(“%d”,v[4])
92
Unidade I
Lembrete
O compilador ignora os comentários.
Resumo
Nesta unidade tivemos como objetivo uma breve introdução à
linguagem C, e por meio dela estudamos os conceitos fundamentais tanto
na programação quanto nos dados. Vimos alguns conceitos da análise dos
algoritmos e as técnicas para o uso da memória RAM.
Na análise de um algoritmo, focamos no cálculo do custo de execução,
revimos certos conceitos de modularização. Da modularização evoluímos
para o uso de várias funções que manipularão um conjunto de dados,
conhecido como tipo abstrato de dados (TAD).
No item TAD também foi estudada a criação para agrupar e modelar
novos tipos de dados por meio de estruturas.
Além das estruturas, em relação aos dados, foi feita a revisão das
matrizes e vetores, bem como o seu uso como cadeia de caracteres. Uma
nova técnica de armazenamento de dados foi apresentada, o ponteiro de
variáveis, que vai muito além da declaração de variáveis. Para entendê‑la,
foi vista a administração dinâmica da memória RAM. O ponteiro é um tipo
especial de variável que armazena o endereço de memória. Trata‑se de
uma ferramenta que permite a manipulação de variáveis e que servirá para
novas técnicas, ampliando o horizonte da manipulação de dados.
93
ESTRUTURAS DE DADOS
Exercícios
Questão 1. Leia o texto a seguir, a respeito da linguagem de programação C.
A linguagem C é uma das mais bem‑sucedidas linguagens de alto nível já criadas, sendo considerada
uma das mais utilizadas de todos os tempos. Define‑se como linguagem de alto nível aquela que
apresenta um nível deabstração relativamente elevado, que está mais próximo da linguagem humana
do que do código de máquina. Ela foi criada em 1972, nos laboratórios Bell, por Dennis Ritchie, sendo
revisada e padronizada pelo ANSI (American National Standards Institute), em 1989.
Trata‑se de uma linguagem estruturalmente simples e de grande portabilidade. Poucas são as
arquiteturas de computadores para as quais não exista um compilador C. Além disso, o compilador da
linguagem gera códigos mais enxutos e velozes do que muitas outras linguagens.
Trata‑se de uma linguagem procedural, ou seja, permite que um problema complexo seja facilmente
decomposto em módulos, sendo cada módulo um problema mais simples. Além disso, ela fornece
acesso de baixo nível à memória, o que possibilita o acesso e a programação direta do microprocessador.
Ela também permite a implantação de programas utilizando instruções em Assembly, o que viabiliza
programar problemas em que a dependência do tempo é crítica.
Por fim, a linguagem C foi criada para incentivar a programação multiplataforma, ou seja, programas
escritos em C podem ser compilados para uma grande variedade de plataformas e sistemas operacionais
com apenas pequenas alterações no seu código‑fonte.
Todo programa escrito em linguagem C que vier a ser desenvolvido deve ter o esqueleto mostrado
no código‑fonte da figura a seguir.
Figura 129
Adaptada de: BACKES, A. Linguagem C: completa e descomplicada. Rio de Janeiro: LTC, 2021.
94
Unidade I
Com base na leitura e em seus conhecimentos, avalie as afirmativas a seguir.
I – O código de um programa em C precisa, necessariamente, conter um bloco da função main().
II – O tipo de dado char é um tipo primitivo da linguagem C.
III – A scanf() é uma função, declarada no arquivo de cabeçalho stdio.h, que serve para imprimir
valores em tela.
É correto o que se afirma em:
A) I, apenas.
B) II, apenas.
C) I e II, apenas.
D) II e III, apenas.
E) I, II e III.
Resposta correta: alternativa C.
Análise das afirmativas
I – Afirmativa correta.
Justificativa: ao programarmos em C, precisamos inserir, de forma obrigatória, uma função main().
Outras funções podem ser inseridas no código, mas a main() é a única obrigatória. Considerando um
código com diversas funções, a main() servirá como o ponto de partida para a execução do programa.
Em geral, ela coordena a execução, direcionando as chamadas para outras funções ao longo da execução
das instruções do programa.
II – Afirmativa correta.
Justificativa: a linguagem C traz definidos os seguintes tipos de dados primitivos: char, int, float e
double. No código, eles podem vir acompanhados dos modificadores signed, unsigned, short e long.
III – Afirmativa incorreta.
Justificativa: a função scanf() é encontrada no arquivo de cabeçalho stdio.h, mas atua como função
de entrada de dados. Ela captura e armazena valores fornecidos via teclado. A função descrita no texto
da afirmativa é a printf(), que é uma função de saída de dados.
95
ESTRUTURAS DE DADOS
Questão 2. Leia o texto a seguir, que demonstra a importância de uma estrutura de dados,
implementada em linguagem C.
Imagine o seguinte problema: leia as notas de uma turma de cinco estudantes e depois imprima
aquelas que forem maiores do que a média da turma. Um programa simples para resolver esse problema
poderia ser o apresentado na figura a seguir.
Figura 130
O programa anterior representa, de fato, uma solução possível para o problema. Porém, os
inconvenientes dessa solução são a grande quantidade de variáveis para gerenciar e o uso repetitivo de
comandos praticamente idênticos, como é o caso dos comandos if().
Expandir o programa anterior para trabalhar com uma turma de 100 alunos significaria, basicamente,
aumentar o número de variáveis para guardar as notas de cada aluno e repetir, ainda mais, o conjunto
de comandos praticamente idênticos. Surge, então, a necessidade de usar uma estrutura de dados.
Pensando no exemplo anterior, poderíamos usar uma estrutura contendo 100 elementos para
guardar as notas dos 100 alunos. Ela seria declarada conforme mostrado a seguir.
float notas[100];
Adaptado de: BACKES, A. Linguagem C: completa e descomplicada. Rio de Janeiro: LTC, 2021.
A respeito da estrutura de dados apresentada no texto como solução para o problema, avalie as
afirmativas a seguir.
96
Unidade I
I – A estrutura é um array, traduzido para o português como vetor.
II – O comando apresentado ao final do texto define um array de nome notas, contendo
100 elementos adjacentes na memória. Cada elemento do array é do tipo float.
III – O vetor é uma estrutura de dados linear que necessita de somente um índice para que seus
elementos sejam endereçados.
É correto o que se afirma em:
A) I, apenas.
B) II, apenas.
C) I e II, apenas.
D) II e III, apenas.
E) I, II e III.
Resposta correta: alternativa E.
Análise das afirmativas
I – Afirmativa correta.
Justificativa: trata‑se de um array (ou vetor), uma estrutura que cria um conjunto de variáveis do
mesmo tipo utilizando apenas um nome. No exemplo do código da figura, as variáveis que guardam as
notas dos alunos são todas do mesmo tipo. Usar um array permitiria utilizar apenas um nome (notas, por
exemplo) de variável para representar todas as notas dos alunos, em vez de um nome para cada variável.
Em linguagem C, a declaração de um array possui a seguinte forma geral:
tipo_dado nome_array[tamanho];
Este comando define um array de nome nome_array contendo tamanho e elementos adjacentes na
memória. Cada elemento do array é do modelo tipo_dado.
II – Afirmativa correta.
Justificativa: o comando apresentado ao final do texto é replicado a seguir:
float notas[100];
97
ESTRUTURAS DE DADOS
Ao compararmos o comando do texto com a forma geral, apresentada na justificativa da afirmativa I,
temos uma troca de tipo_dado por float, nome_array por notas e [tamanho] por [100]. Isso significa que
foi declarado um vetor de nome notas, que abriga 100 elementos do tipo float.
III – Afirmativa correta.
Justificativa: um array de n elementos endereça cada um de seus elementos por um índice i, que
começa em 0 e termina em n-1. Desse modo, um vetor de 5 elementos terá endereços que vão de 0 a 4,
cada um deles apontando para um item.