1 1 A Máquina Analítica de Charles Babage
Transcrição
1 1 A Máquina Analítica de Charles Babage
NOTAS DE AULA DE INTRODUÇÃO À PROGRAMAÇÃO PROFESSOR: MATEUS CONRAD B. DA COSTA1 1 2 3 4 5 6 7 1 A Máquina Analítica de Charles Babage ....................................................................... 3 1.1 Estrutura Lógica ..................................................................................................... 3 Máquinas Programáveis ................................................................................................. 4 Programa (Software)....................................................................................................... 4 3.1 Solução de Problemas............................................................................................. 5 3.1.1 Solução descritiva do problema...................................................................... 7 3.1.2 Algoritmos ...................................................................................................... 8 3.1.3 Refinamentos Sucessivos ............................................................................... 9 3.1.4 Desvios Condicionais ................................................................................... 10 3.1.5 Comando de repetição .................................................................................. 11 3.1.6 Algoritmo Completo..................................................................................... 11 3.1.7 Comando de atribuição................................................................................. 12 3.1.8 Exercícios de Fixação {Níveis de Conhecimento e Compreensão}............ 13 3.2 Variáveis............................................................................................................... 14 3.2.1 Memória ....................................................................................................... 14 3.2.2 Variáveis são apelidos para endereços ......................................................... 15 3.2.3 Nomes de variáveis (identificadores) ........................................................... 15 3.2.4 Tipos de uma variável .................................................................................. 16 3.2.5 Declaração de Variáveis ............................................................................... 17 3.2.6 Exemplos ...................................................................................................... 17 3.2.7 Expressões Aritméticas ................................................................................ 20 3.2.8 Expressões Lógicas ...................................................................................... 21 3.2.9 Exemplo........................................................................................................ 23 3.2.10 Exercícios de Fixação................................................................................... 25 Construção de Programas em Linguagem C ................................................................ 28 4.1 Estratégias para resolução de Problemas usando C.............................................. 28 4.2 Introdução a Linguagem C ................................................................................... 29 4.3 Guia Rápido.......................................................................................................... 29 4.4 Exemplos Introdutórios ........................................................................................ 33 4.5 Exercícios de Fixação........................................................................................... 40 Comandos de Repetição ............................................................................................... 41 5.1 Motivação ............................................................................................................. 41 5.2 Estrutura básica de um laço .................................................................................. 41 5.3 Sintaxe de comandos de repetição........................................................................ 42 5.3.1 Comando Enquanto ..................................................................................... 42 5.3.2 Exemplos ...................................................................................................... 43 5.3.3 Exercícios ..................................................................................................... 47 5.3.4 Comando Faça .. .Enquanto.......................................................................... 48 5.3.5 Exercícios ..................................................................................................... 48 Vetores e Matrizes ........................................................................................................ 49 6.1 Vetores.................................................................................................................. 49 Registros - Structs........................................................................................................ 56 Obs: Esta cópia não é revisada, podendo conter erros de ortografia. 1 7.1 Definição .............................................................................................................. 56 7.2 Declaração de registros......................................................................................... 56 7.3 Manipulando Registros......................................................................................... 57 7.4 Vetores de Registros............................................................................................. 58 7.5 Exercícios ............................................................................................................. 59 7.6 Registro em C ....................................................................................................... 59 8 Arquivos em C.............................................................................................................. 60 8.1 Definições............................................................................................................. 61 8.2 Tipos de arquivos ................................................................................................. 61 8.3 Ferramentas para manipulação de arquivos.......................................................... 62 8.3.1 Estrutura de controle FILE – ........................................................................ 62 8.3.2 Abrindo um arquivo ..................................................................................... 62 8.3.3 Escrevendo e lendo em um arquivo texto.................................................... 64 8.4 Outras Funções de manipulação de arquivos ....................................................... 67 8.5 Lendo e escrevendo em Arquivos Binários - fread() e fwrite()........................... 68 8.5.1 Operador que determina o tamanho de tipos de dados - sizeof() ............... 68 8.5.2 Função fwrite() ............................................................................................. 69 8.5.3 Função fread()............................................................................................... 69 8.5.4 Exemplo1 - Escrevendo número inteiros em um arquivo ........................... 70 8.5.5 Operador de endereço &............................................................................... 70 8.5.6 Exemplo 2 – Programa que lê um arquivo binário de números inteiros e imprime na tela ............................................................................................................. 71 8.5.7 Exemplo 3 - Escrevendo um vetor de inteiros em um arquivo binário ...... 71 8.5.8 Nomes de vetores são ponteiros ................................................................... 72 9 Apontadores.................................................................................................................. 72 9.1 Apontadores e Endereços ..................................................................................... 73 9.2 Declaração de Apontadores .................................................................................. 74 9.3 Qual o valor inicial de um apontador? ................................................................. 78 9.4 Apontadores e Vetores (Arranjos)........................................................................ 78 9.5 O nome do vetor é um apontador constante ......................................................... 80 9.6 Exercícios ............................................................................................................. 82 9.6.1 Mais exemplos.............................................................................................. 89 10 Construção de Funções............................................................................................. 92 10.1 Chamada de Função ............................................................................................. 92 10.2 Fluxo de Execução ............................................................................................... 93 10.3 Passagem de parâmetros....................................................................................... 93 10.4 Tipo de retorno da função..................................................................................... 93 10.5 Construção de Funções......................................................................................... 93 2 1 A Máquina Analítica de Charles Babage 1.1 Estrutura Lógica Armazena mento Seção de Entrada Engenho Seção De Saída Seção de Entrada - Dispositivo que interpreta Cartões perfurados contendo valores e instruções, e conduzia os valores para o armazenamento e instruções para o engenho. Valores – Informações numéricas a serem trabalhadas (Dados) Instruções – “A forma” como os valores devem ser trabalhados. Exemplo: Valores: - valor1 = 7 - Valor2 = 3.14 Instruções: - Elevar valor1 ao quadrado - Guardar o resultado da operação anterior em valor3 - Multiplicar valor2 por valor3 - Enviar o resultado da operação para a seção de saída Armazenamento – Dispositivo capaz de guardar valores vindos da seção de entrada e do engenho. Os valores podem ser recuperados (buscados) pelo engenho através de um sistema de numeração das células de armazenamento (sistema de endereços) 3 Engenho – A partir de instruções vindas da seção de entrada, (isto é, seguindo o que é determinado pelas instruções) realiza os seguintes tipos de operações: - busca de valores no armazenamento - realiza operações aritméticas sobre os valores - envia valores para o armazenamento - envia valores para a seção de saída Seção de saída - Produz cartões perfurados representando os valores enviados pelo engenho em símbolos que podem ser interpretados pelo operador 2 Máquinas Programáveis Essa estrutura da máquina analítica tornava a mesma programável. Definição: “Uma máquina programável é aquela que pode alterar seu comportamento mediante uma seqüência de instruções novas (software) sem a necessidade de alterar a sua estrutura física (Hardware).” Exemplos de máquinas não programáveis - Linha de produção tradicional - Um aparelho liqüidificador - Calculadoras (não programáveis) Exemplos de máquinas programáveis - Computador - Robôs - Calculadoras programáveis 3 Programa (Software) Programa e software são sinônimos. Um definição de programa é dada a seguir: Programa “Um programa é um conjunto de instruções em seqüência (ordem cronológica para a sua execução) que pode ser interpretada e executada por uma máquina programável de forma não ambígua.” Nível Abstração “Abstrair significa deixar de lado certas partes de um todo em função de prestar atenção a apenas alguns detalhes desse todo. É focar seu ponto de atenção em um detalhe que, por hora, é o que interessa... é concentrar-se naquilo que, por hora, interessa.” Essa idéia de abstração é usada em computação. Níveis de abstração altos indicam uma proximidade com os mecanismos humanos de resolver problemas, deixando-se de lado características físicas de computadores. Níveis de abstração 4 baixos indicam a preocupação com a implementação da solução em uma forma executável em um computador. Linguagens de Programação Os elementos (códigos, símbolos, nomes, etc.) usados para se escrever um programa formam uma linguagem de programação2. Existem linguagens que não podem ser entendidas pelo computador e são mais próximas do homem. Essas linguagens possuem um alto nível de abstração. São exemplos dessas linguagens: Basic, Pascal, Modula, C, C++, Java, Python, etc. A linguagem entendida pelo computador é de difícil compreensão para o homem e possui um baixo nível de abstração. Criaram-se então mecanismos para que os programadores escrevam programas em linguagens de alto nível que possam ser traduzidos em programas de baixo nível. Esses programas são chamados de Tradutores e Interpretadores. Um tradutor tem a função de, a partir de um programa escrito em uma determinada linguagem, traduzi-lo completamente para uma outra, similarmente à tradução de um texto de um idioma para outro. Um interpretador funciona como um intérprete, traduzindo o programa instrução por instrução e passando essa instrução interpretada individualmente para o seu receptor. No caso, o computador. Tradutores que transformam programas escritos em linguagem de alto nível para programas em linguagem de baixo nível executáveis em um computador (linguagem de máquina) são chamados de Compiladores. Ou seja, um compilador é um tipo especial de tradutor. 3.1 Solução de Problemas Programas são criados para se resolver um determinado problema. Dada uma máquina e um problema, a solução desse problema é dada por um programa. No entanto, a construção de um programa passa por várias etapas até estar em linguagem de máquina A figura a seguir mostra os vários níveis de abstração de um programa desde a proposição do problema. 2 Na realidade, esse elementos formam a gramática da linguagem e o conjunto de todos os programas que podem ser escritos com essa gramática formam a linguagem. Por motivos de simplificação consideramos o conjunto dos elementos citados como sendo a linguagem. 5 SER HUMANO PROBLEMA homem SOLUÇÃO DESCRITIVA homem ALGORITMO homem homem PROGRAMA FONTE EM LINGUAGEM DE ALTO NÍVEL Compilador/ Interpretador PROGRAMA EM LINGUAGEM DE MONTAGEM OU PSEUDO CÓDIGO Montador (Assembler) PROGRAMA EM LINGUAGEM DE MÁQUINA (EXECUTÁVEL) Execução do programa COMPUTADOR 6 3.1.1 Solução descritiva do problema A construção de programas se inicia com a definição do problema. Esta, por sua vez, parte do estabelecimento de um resultado a ser alcançado a partir de um determinado conjunto de dados de entrada (domínio dos dados). Matematicamente um programa pode ser considerado uma função (conjunto de instruções) que atua sobre um conjunto de variáveis (dados). F(variáveis de entrada) = Programa As implicações do conceito matemático de função podem ajudar ao programador a não cometer erros. Vejamos por exemplo o problema do cálculo do fatorial de um número. Nesse problema, o programador terá como variável de entrada um único número inteiro e deverá formular um algoritmo, ou seja, uma seqüência ordenada de instruções, para obter o fatorial desse número. Além disso, o programador deverá conhecer as peculiaridades do cálculo de fatorial para definir restrições quanto ao domínio da função fatorial. Sabemos que o fatorial só existe para números inteiros positivos incluindo o zero. Portanto, o programa para o calculo do fatorial não poderá aceitar como dados de entrada números inteiros negativos e tampouco números fracionários. As restrições de entrada de dados devem ser precisamente estabelecidas pois a corretude de algoritmo tem por base um determinado conjunto de dados de entrada e, situações imprevistas para esses dados podem levar o algoritmo a uma condição de erro. Não só o domínio dos dados mas a observação de todas as características que determinam a natureza do problema é de suma importância para o programador. O primeiro passo do programador é entender o problema em seus mínimos detalhes. Voltando ao problema do fatorial, para construção da solução, o programador deve obviamente conhecer a função fatorial. O fatorial de um número x é dado pelo produto de todos os números inteiros que vão de x até 1, para valores de x maiores que 0 e é igual a 1 para x igual a zero. Se o programador não conhecer essa informação, torna-se impossível o mesmo construir um algoritmo que forneça o resultado desejado. Assim, com base nas restrições de entrada e nas características do problema, podemos montar uma primeira solução do mesmo, a solução descritiva do problema: Para calcular o fatorial de um número devemos obter como entrada de dados apenas números inteiros positivos incluindo o zero. Em posse do número que se deseja calcular o fatorial verificamos duas alternativas: Se o número for maior que 0, o fatorial é dado pelo produto de todos os números inteiros menores ou iguais ao número e maiores que zero. Se o número for igual a 0, então o fatorial é igual a 1 Nem sempre a solução descritiva de um problema está pronta na matemática como no caso do fatorial. Na maioria das vezes a solução descritiva do problema resulta de 7 elaborações de longas seqüências lógicas encadeadas, obtendo-se assim vários resultados intermediários até que se chegue no resultado final esperado. 3.1.2 Algoritmos A solução descritiva de um problema possui um alto nível de abstração e é baseada em mecanismos humanos de resolução de problemas. Quando Apresentamos na solução do problema do fatorial a sentença: Se o número for maior que 0, o fatorial é dado pelo produto de todos os números inteiros menores ou iguais ao número e maiores que zero, estamos usando um mecanismo humano para resolver o problema. Ora, sabemos como fazer um produto de um conjunto de números: Multiplicamos o primeiro pelo segundo e o resultado disso pelo terceiro e assim por diante até terminarmos com os números. Resta-nos saber ser conseguiremos transpor para uma linguagem de programação a solução proposta na sentença. Iniciar a tradução de uma solução descritiva de um problema para um programa em uma linguagem de computador pode não ser uma abordagem eficiente. Lembre-se: Existe um nível de abstração adequado para cada momento. Além disso, alguns mecanismos da solução descritiva podem mesmo nem ter uma forma de implementação adequada em uma linguagem de programação. Por esses motivos, o correto é a construção de um algoritmo que detalhe a solução descritiva em termos de construções lógicas similares as que serão encontradas nas linguagens de programação em que se pretende implementar a solução. Construindo um algoritmo, o programador deverá atingir os seguintes objetivos: 1. Validar solução encontrada como possível de ser implementada; 2. encontrar uma solução computacional eficiente para o problema; 3. impor um nível de detalhamento da solução tal que no trabalho de transposição posterior para um código na linguagem de programação alvo, o programador não se tenha nenhuma necessidade de alterar ou aumentar a solução do ponto de vista lógico, preocupando-se nesse momento com os detalhes de implementação (nível de abstração mais baixo). 8 Como exemplo, tomemos novamente o problema do fatorial. Um possível algoritmo para a solução dada seria o seguinte: Algoritmo Fatorial 1. obter o número que se deseja calcular o armazenar na variável número 2. Se número for maior ou igual a 0 então 3. Calcular e informar o fatorial Senão 4. informar que o fatorial não existe Fim Se Fim Alg. fatorial e 3.1.3 Refinamentos Sucessivos Os itens numerados no algoritmo acima são instruções que ainda encontram-se em um nível de abstração alto e podem ser detalhadas. A técnica de refinamentos sucessivos é amplamente utilizada e visa o aumento gradativo do nível de detalhamento de um algoritmo. Os programadores utilizam essa técnica para evitarem erros e encontrarem o caminho mais curto para a solução. Nessa técnica, o detalhamento de cada sentença numerada chama-se refinamento. Vejamos, por exemplo, o refinamento 3 do algoritmo fatorial: Ref. 3 { Calcular e informar o fatorial } Se o número = 0 então 1.informar que o fatorial é igual a 1 Senão 2.calcular o fatorial para valores maiores que 0 Fim Se Fim Ref. 3 Para tornar os refinamentos claros de serem lidos, coloca-se a palavra Ref. seguida do número do refinamento e o título do refinamento entre chaves. Em seguida o refinamento passa a ser construído. No final coloca-se a indicação do fim do refinamento. Os refinamentos podem continuar sucessivamente até que o algoritmos esteja detalhado no nível que possa ser fielmente traduzido para a linguagem de programação. No refinamento 3 teremos ainda dois refinamentos para serem escritos com comandos da linguagem algorítmica utilizada: Ref. 3.1 {informar que o fatorial é 1} Imprima (“ O fatorial é ”, 1) Fim ref. 3.1 9 Ref. 3.2 { Calcular o fatorial para valores maiores que 0 } Fatorial recebe 1 Repita enquanto número > 0 Fatorial recebe Fatorial * Numero Número recebe Número - 1 Fim Repita Imprima(“O fatorial é ”, Fatorial) Fim Ref. 3.2 Restam ainda os refinamentos 1,2 e 4. O refinamento 1 trata-se de uma entrada de dados. Qualquer dado que for introduzido em um algoritmo deverá ser feito através da “leitura” de uma variável. O comando algorítmico usado para a leitura da variável será Leia. Desse modo teremos: Ref. 1 { obter o número que se deseja calcular o fatorial e armazenar na variável número} Leia(número) Fim Ref. 1. O refinamento 4 é uma operação de saída. Para realizarmos qualquer operação de saída de dados, usaremos o comando Imprima, que já foi usado no ref. 3. Ref. 4 { informar que o fatorial não existe} Imprima(“O fatorial de números negativos não existe”); Fim Ref. 4 3.1.4 Desvios Condicionais O refinamento 2 é um comando condicional se .... então .... senão .... fim se. Este comando indica que as instruções posteriores ao então e anteriores ao senão só serão executadas se as condições colocadas após o se forem verdadeiras. Caso contrário essas instruções serão ignoradas e ocorrerá um salto para a instrução imediatamente posterior ao senão. Nesse caso as instruções após o senão e antes do fim se é que serão executadas. No ref. 2 as condições podem ser detalhadas da seguinte forma: Ref. 2 {Se número for maior ou igual a 0 então} Se (número >=0) então Fim Ref. 2 10 O comando formado pelas partes se ... então .... senão ... fim se é chamado de desvio condicional composto pois possui a cláusula senão. O desvio condicional simples é formado apenas pela construção: Se .... então .... fim se. No desvio condicional simples, se a condição após o Se for verdadeira, as instruções após o então serão executadas. Caso contrário, ocorrerá um desvio (salto) para o primeiro comando. 3.1.5 Comando de repetição Para calcular o fatorial de números maiores que zero, foi lançado mão de uma construção algorítmica chamada de comando de repetição. Esse comando é composto da forma Repita enquanto <Condição> Instrução 1 . ... ... Instrução k Fim repita Nesse comando, as instruções após a condição e antes do fim repita serão executadas ordenadamente. Quando a última instrução for executada (instrução k) haverá um retorno até o comando repita, onde novamente a condição é testada. Se a condição continuar verdadeira, o bloco de instruções será novamente executado. Quando a condição for para um estado de Falsa, haverá um desvio para o comando imediatamente após o Fim repita. Outras aplicações e derivações do comando de repetição serão estudadas mais adiante em maiores detalhes. Nessa introdução nos ateremos a soluções de problemas que não necessitam de comandos de repetição. 3.1.6 Algoritmo Completo Concluídos todos os refinamentos, basta agora que os mesmos sejam reunidos em um único código para termos um algoritmo completo da solução do problema. Esse procedimento deve ser automático e não envolve nenhum raciocínio. Basta substituir os títulos dos refinamentos no algoritmo principal pelos próprios refinamentos. O Algoritmo completo e pronto para ser traduzido para uma linguagem de programação é dado a seguir. Para ficar claro foram inseridos comentários no algoritmos. São os textos inseridos após os // que estão indicando o refinamento correspondente dos comandos seguintes. Comentários não tem função operacional alguma em algoritmos e programas. Sua única finalidade é tornar claro o algoritmo. Segue então o algoritmo: 11 Algoritmo Fatorial Leia(número) // ref. 1 Se (número >=0) então // ref. 2 Se número = 0 então // refinamento 3 Imprima (“ O fatorial é ”, 1) // ref. 3.1 Senão // ref. 3.2 Fatorial recebe 1 Repita enquanto número > 0 Fatorial recebe Fatorial * Numero Número recebe Número - 1 Fim Repita Imprima(“O fatorial é ”, Fatorial) Fim Se Senão Imprima(“O fatorial de números negativos não existe”); Fim Se Fim Algoritmo 3.1.7 Comando de atribuição O comando Fatorial recebe 1 Está descrito claramente e indica que será armazenado na variável Fatorial o valor 1. Em linguagens de programação comando assim são chamados de comando de atribuição e representados por um sinal de atribuição. Nos algoritmos iremos usar o sinal ← para representar uma atribuição. Assim, o comando anterior fica mais simples de ser escrito, da seguinte forma: Fatorial ← 1 O comando Fatorial recebe Fatorial * Numero, por sua vez, indica que o valor contido em fatorial anteriormente deve ser multiplicado pelo valor contido em número e posteriormente armazenado na variável fatorial, (apagando o valor que existia anteriormente em Fatorial). Com o comando de atribuição ficaria da seguinte forma: Fatorial ← Fatorial * Número Analogamente, o comando Número recebe Número - 1 ficaria: Número ← Número -1 Resumindo, Sempre que quisermos escrever que um variável recebe um valor usaremos o sinal ← no lugar da palavra recebe. O algoritmo completo ficaria então: Algoritmo Fatorial Leia(número) // ref. 1 12 Se (número >=0) então // ref. 2 Se número = 0 então // refinamento 3 Imprima (“ O fatorial é ”, 1) // ref. 3.1 Senão // ref. 3.2 Fatorial ←1 Repita enquanto número > 0 Fatorial ←Fatorial * Numero Número ←Número - 1 Fim Repita Imprima(“O fatorial é ”, Fatorial) Fim Se Senão Imprima(“O fatorial de números negativos não existe”); Fim Se Fim Algoritmo Chegamos ao final dessa seção. Na seção seguinte veremos detalhes relacionados a variáveis. No capítulo seguinte estudaremos com mais detalhes as construções envolvendo desvios condicionais e expressões lógicas. É fundamental a resolução dos exercícios propostos para a continuação dos estudos. 3.1.8 Exercícios de Fixação {Níveis de Conhecimento e Compreensão} 1. Cite os quatro componentes da máquina analítica de Babage e suas respectivas funções. 2. Esta máquina poderia funcionar sem o armazenamento? Por que? 3. Como Funcionava o armazenamento? Como o engenho conseguia obter valores lá armazenados? 4. Podemos afirmar que a máquina analítica era programável? Por que? 5. Explique a relação entre máquina programável, hardware e software. 6. Defina Programa. Enfatize três características que um programa deve possuir. 7. O que é linguagem de programação? 8. O que são tradutores e interpretadores? 9. O que caracteriza um compilador? 10. Geralmente, quando construímos um programa, partimos de uma solução mais abstrata. Por que? 11. Quais aspectos de um problema devemos considerar para conseguirmos uma solução descritiva do mesmo? Quais objetivos devem ser alcançados na construção de um algoritmo? 12. Construa soluções descritivas para os problemas a seguir: i. Dados como entrada os valores de horas, minutos e segundos, informar o total de segundos equivalente ao montante de horas, minutos e segundos informados. ii. Calcular a raiz quadrada de um número. iii. Dados como entrada três números, verificar se os mesmos podem ser os lados de um triângulo iv. Dados três números de entrada (x, y e z), informar qual deles é o maior e qual deles é o menor. 13 v. Dados os coeficientes a, b e c de uma equação do segundo grau, verificar se as raízes existem e em caso afirmativo, calcular e informar as mesmas. vi. Dados quatro números de entrada (x, y, z e w), informar qual deles é o maior e qual deles é o segundo maior. 13. Construir algoritmos utilizando a técnica de refinamentos sucessivos para os itens ii, iv, v e vi do exercício 12. 3.2 Variáveis 3.2.1 Memória Assim como na máquina analítica de Babage, em computadores digitais a programação só se torna possível pela fato destes terem em sua estrutura dispositivos de armazenamento de dados. A armazenagem de dados é fundamental em um programa pois é ela que permite “guardar” os resultados de um passo de programação (instrução) para serem utilizados nos passos seguintes (próximas instruções). Ou seja, é por meio das variáveis que se registram as operações realizadas pelas instruções. O esquema de armazenamento de computadores modernos é chamado memória. A memória de um computador pode conter milhões ou bilhões de células numeradas onde cada célula pode armazenar um valor (dado) individualmente. O número de cada célula é chamado de endereço de memória da célula. Por maior que esta memória seja, cada célula de armazenamento terá um endereço exclusivo, único. É através desses endereços que os dados podem ser recuperados na memória e levados para o processador. Em uma célula de memória, não pode existir mais de um valor ao mesmo tempo, embora este valor possa ser alterado no tempo dada a execução de um programa. Por exemplo, uma célula pode, no inicio de um programa estar valendo 0 e através da execução de uma instrução de programa vir a valer 5. O esquema a seguir é um modelo simplificado da memória de um computador. Os números binários da coluna endereço são os endereços de cada célula e os números binários na coluna Célula são os conteúdos das posições de memória identificados pelos respectivos endereços. Por exemplo, o conteúdo do endereço de memória 1 (1 em binário, 001 na representação da figura) é 128 (binário 1000 0000). Já o conteúdo do endereço de memória 5 (binário 101) é 2 ( 10 em binário, 0000 0010 na figura). Endereço 000 001 010 011 100 101 110 111 Célula 00101001 10000000 11001000 10101010 11000000 00000010 10101010 01010101 Representação da memória de um computador digital 14 3.2.2 Variáveis são apelidos para endereços Os endereços são seqüências de 0 e 1 e geralmente estão submetidos a esquemas de endereçamento complexos. Por esse e outros motivos, o uso de números para informar endereço de memória em um programa em linguagem de alto nível seria uma tarefa trabalhosa e inconveniente para o programador. Felizmente, há uma maneira mais simples de lidar com esses endereços de memória chamando-os de variáveis. Uma variável nada mais é do que uma abstração, um apelido para um endereço de memória. Tal abstração é possível porque quando estamos construindo um programa não necessitamos saber onde exatamente os dados serão armazenados. Necessitamos apenas que sejam armazenados em lugares determináveis. Dessa forma, quando quisermos armazenar um valor em um determinado lugar, basta denominar esse lugar com um nome único e, sempre que se quiser realizar uma operação envolvendo o valor armazenado, utilizamos o nome dado. Este nome é chamado de identificador da variável ou simplesmente nome da variável. O nome da variável é o seu atributo mais importante. No algoritmo para o cálculo do fatorial, foram utilizadas duas variáveis: A variável Número e a variável Fatorial. Ambas as variáveis se destinam, no algoritmo, a armazenarem valores numéricos. Poderíamos entretanto, termos variáveis destinadas a armazenarem outros tipos de dados como por exemplo palavras e valores lógicos (verdadeiro e falso). O tipo da variável é o outro atributo que por hora irá nos importar. Uma variável portanto possui dois atributos fundamentais que iremos ver em detalhes: - nome - tipo 3.2.3 Nomes de variáveis (identificadores) Nomes de variáveis em algoritmos devem obedecer a regras rigorosas quanto a sua formação, semelhantes as regras impostas em linguagens de programação. Essas regras são chamadas de regras para formação de identificadores e serão descritas a seguir: 1. Um nome de variável deve ser único no módulo do algoritmo onde for declarada. Inicialmente consideraremos apenas que um nome de variável deve identificar unicamente uma variável. Ou seja, NUNCA daremos um mesmo nome para duas variáveis em um mesmo bloco de declaração. 2. Um nome de variável pode ser formado por uma letra de (A .. Z) e de (a .. z), sendo que em linguagem algorítmica não faremos distinção entre maiúsculas e minúsculas (embora na linguagem C maiúsculas e minúsculas sejam distinguidas). São exemplos de nomes válidos de variáveis: X, y h M Z Q W e k. 3. Um nomes de variável pode ser formado por palavras ou seqüências de caracteres que obedeçam as seguintes regras: - O primeiro caracter deve obrigatoriamente ser uma letra; - Os demais caracteres podem ser números, letras ou o símbolo _ (underscore). São exemplos de variáveis: Numero, Nota, Teste, x1,X2, Yw1, HK3, Nota_maxima 15 4. Uma variável nunca poderá receber como identificador, um nome de comando ou tipo de variável da linguagem definida. Ex: se, senão, então, repita, etc. 5. Um nome de variável deve ser formado exclusivamente obedecendo as regras acima. Vale lembrar que não podem existir nomes de variáveis com espaços em branco, por exemplo: valor máximo. Ao invés disso, utilize o símbolo _ para unir as duas partes do nome: valor_máximo. Qualquer outro símbolo não é admitido em nomes de variáveis. Isso inclui os símbolos: ( ) { } [ ] ‘ “ : ; > < , . / ? | \ - + = * & ^ % $ # @ ! ~ `. 6. Por último, devemos lembrar que em algoritmos admitiremos nomes de variáveis com acentos, embora em quase todas as linguagens de programação não se admitem acentos ( A linguagem Logo permite acentos em variáveis). 3.2.4 Tipos de uma variável Assim como em nossas vidas classificamos as coisas como pertencentes a determinada classe ( eletrodomésticos, veículos, roupas, cães, pessoas, etc.) as variáveis também deverão possuir uma classificação. Essa classificação diz respeito primordialmente à natureza dos dados que desejamos armazenar nas variáveis. Em algoritmos, definiremos três tipos de variáveis que determinam os três tipos básicos logicamente diferenciáveis em linguagens de programação. São eles os tipos: - Numérico - Literal - Lógico Tipo Numérico – Quando definimos uma variável do tipo numérico, é por que nela desejamos armazenar valores numéricos, ou seja, qualquer valor pertencente ao conjunto dos números Reais, fracionários, inteiros, positivos ou negativos, evidentemente. Assim se X e Y forem variáveis numéricas podemos realizar as seguintes atribuições são válidas: X ← 20 Y←3.14 Y ← (X*X)*Y Tipo Literal - O Tipo literal se reserva a armazenar valores que são seqüências de caracteres. Uma seqüência de caracteres é formada por mais de um caracter (letras, número ou símbolo) inserido entre “aspas” . Variáveis do tipo literal são destinadas a guardar informação alfanumérica. Por exemplo, se as variáveis Nome e Turma forem literais. Os comandos a seguir são válidos. Nome ← “Adriana da Silva” Turma ← “2° período” Tipo Lógico – Verdadeiro ou falso, sim ou não, aberto ou fechado zero ou um, branco ou preto, cheio ou vazio, etc. Muitas são as circunstâncias em que temos apenas duas alternativas possíveis para um dado. Nesses casos, o tipo de variável que usaremos para armazenar o dado é o tipo lógico, que só poderá armazenar dois valores: Verdadeiro e Falso. São atribuições válidas para variáveis lógicas: Teste ← Verdadeira 16 Resultado ← (10<4) Neste caso, a variável receberá o valor resultante da expressão lógica. No caso Falso) 3.2.5 Declaração de Variáveis A maior parte das linguagens de programação exige que o programador defina explicitamente as variáveis utilizadas no programa. Esse definição das variáveis se chama declaração de variáveis e tem os seguintes objetivos: - Definir o identificador da variável - Definir o tipo da variável - Definir o escopo da variável O escopo de uma variável é a sua área de abrangência dentro de um programa. Falar em escopo só faz sentido quando falamos de modularização e definição de funções. Portanto, essa discussão será feita em etapas seguintes. Sintaxe da declaração A declaração de variáveis em nossos algoritmos será feita através do comando Declare que define o início da declaração de variáveis. Para declararmos uma variável devemos definir um identificador, de acordo com as regras estabelecidas e o tipo da seguinte forma: declare X: numérico Variáveis do mesmo tipo podem ser declaradas em seqüência, separadas por vírgulas: declare X,nota, media: numérico Variáveis de tipos diferente são definidas em seqüências independentes: declare X,nota, media: numérico teste: lógico Nome,curso: literal; 3.2.6 Exemplos Como exemplo vamos construir o algoritmo para o problema iii do exercício de fixação 12 da seção anterior. Dados três números verificar e informar se os mesmos podem ser os lados de um triângulo. 17 Passo 1- Solução descritiva Para solucionar esse problema precisamos saber que em um triângulo, um lado nunca é maior ou igual a soma dos outros. Sabendo disso basta obter os valores e comparar cada valor com a soma dos outros verificando se a condição do problema é atendida. Passo2 – Algoritmo No algoritmo iremos inserir um refinamento inicial chamado declarar variáveis. Esse refinamento sempre será o último a ser feito pois só com o algoritmo finalizado é que sabemos com certeza quais as variáveis que serão usadas. Algoritmo triângulo 1. Declarar variáveis 2. Obter os três valores (a,b e c) candidatos a lados do triângulo 3. Verificar se os mesmos podem formar um triângulo 4. Informar o resultado Fim algoritmo Ref. 2 { Obter os três valores (a,b,c), candidatos a lados de um triângulo} Leia (a,b,c) Fim Ref. 2 Ref. 3 { Verificar se os mesmos podem formar um triângulo} Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Senão E_TRIANGULO ← falso Fim se Senão E_TRIANGULO ← falso Fim se Senão E_TRIANGULO ← falso Fim se Fim ref. 3 Ref. 4 {informar o resultado} Se E_TRIANGULO= verdadeiro então Imprima(“Os valores fornecidos formam um triângulo”) Senão Imprima(“Os valores fornecidos não formam um triângulo”) 18 Fim se Fim ref. 4 Nesse refinamento, usamos a variável lógica E_TRIANGULO para fazer o teste de verificação. Como a variável é lógica,, ou seja já possui um valor verdadeiro ou falso, o teste pode ser feito sem a comparação com o valor verdadeiro. Da seguinte forma: Ref. 4 {informar o resultado} Se E_TRIANGULO então Imprima(“Os valores fornecidos formam um triângulo”) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim ref. 4 Ref. 1 {declarar variáveis} Declare a,b,c:numérico E_TRIANGULO: lógico Fim ref. 1 O algoritmo completo fica: Algoritmo triangulo Declare a,b,c:numérico E_TRIANGULO: lógico Leia (a,b,c) Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Senão E_TRIANGULO ← falso Fim se Senão E_TRIANGULO ← falso Fim se Senão E_TRIANGULO ← falso Fim se Se E_TRIANGULO então Imprima(“Os valores fornecidos formam um triângulo”) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim algoritmo 19 Uma outra alternativa para o algoritmo do triângulo seria inicializar a variável E_TRIANGULO com o valor FALSO, antes das verificações. Neste caso os testes precisariam apenas alterar o valor da variável E_TRIANGULO quando os valores puderem formar um triângulo. Caso contrário, a variável já estaria com o valor FALSO. O algoritmo ficaria assim: O algoritmo completo ficaria assim: Algoritmo triangulo Declare a,b,c:numérico E_TRIANGULO: lógico Leia (a,b,c) E_TRIANGULO ← FALSO {inicialização da variável} Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Fim se Fim se Fim se Se E_TRIANGULO então Imprima(“Os valores fornecidos formam um triângulo”) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim algoritmo 3.2.7 Expressões Aritméticas Expressões aritméticas podem ser formadas por operadores aritméticas (*, /, + e -) e valores numéricos. Os valores numéricos podem ser valores absolutos e variáveis numéricas. Da mesma forma que na matemática, existe uma ordem de precedência entre os operadores. Ou seja, em uma expressão os operadores ‘*’ e ‘/’ são executados primeiro, seguindo-se os operadores + e -. Para mudar essa regra é necessário usar os parênteses. Ex: 2 +3*3 = 11 (2+3)*3 = 15 (( 2+3)*4)+1 Veja que podemos usar parênteses dentro de parênteses 20 3.2.8 Expressões Lógicas Assim como podemos formar expressões aritméticas como valores numéricos e operadores aritméticos (+, - , *, /), podemos formar expressões lógicas com valores lógicos e operadores lógicos. As expressões lógicas são usadas em programas em qualquer comando que exija um teste de uma condição, como no caso do Se condição então... senão .... Fim se. A condição sempre será uma expressão lógica. Outros comandos que utilizam testes de condição, como o repita enquanto condição ... fim repita, também exigem que a condição seja uma expressão lógica. Os operadores lógicos são os três operadores básicos definidos na álgebra booleana: - Operador OU Operador E Operador Não Os operadores OU e E são chamados operadores binários, pois envolvem dois operandos. O operador Não é unário, envolvendo apenas um operando. Tabelas verdade de operadores Cada operador lógico possui a sua tabela verdade, que representa todo seu universo de possibilidades dados os possíveis valores dos operandos. A tabela verdade define a lógica de funcionamento dos operadores. Tabela verdade do operador OU Sejam A e B valores lógicos. Temos os possíveis valores para A OU B A B A OU B FALSO FALSO VERDADEIRO VERDADEIRO FALSO VERDADEIRO FALSO VERDADEIRO FALSO VERDADEIRO VERDADEIRO VERDADEIRO Tabela verdade do operador E. Sejam A e B valores lógicos. Temos os possíveis valores para A E B A B AEB FALSO FALSO VERDADEIRO VERDADEIRO FALSO VERDADEIRO FALSO VERDADEIRO FALSO FALSO FALSO VERDADEIRO Tabela verdade do operador E. Sejam A um valor lógico. Temos os possíveis valores para NÂO( A ) 21 A NÂO(A) FALSO VERDADEIRO VERDADEIRO FALSO Os valores lógicos podem ser variáveis lógicas, valores lógicos absolutos (VERDADEIRO ou FALSO) e expressões cujo resultado seja um valor lógico. Essas expressões são formadas a partir dos operadores matemáticos de comparação: > maior que < menor que = igual a <> diferente de >= maior ou igual a <= menor ou igual a Ex: idade > 20 Nome = “Maria” Media >= (nota1+nota2)/2 As expressões lógicas podem ser compostas de combinações dessas expressões unidas por operadores lógicos. Ex. (idade > 20) E (nome = “Maria” ) E NÂO(Media >= (nota1+nota2)/2) Regras de precedência Como pode ser visto, podemos formar expressões envolvendo operadores aritméticos, de comparação e lógicos. Portanto é necessário definirmos regras de precedência para os possíveis operadores. As regras de precedência definem que executa primeiro. Operador Primeiro lugar Segundo lugar Terceiro lugar Quarto lugar Precedência Operador lógico Não Operadores aritméticos *,/ e operador lógico E Operadores aritméticos + -, operador lógico OU Operadores de comparação Portanto para escrevermos expressões lógicas corretas é necessário utilizar parênteses para mudarmos a prioridade da execução. Por exemplo, sejam X<Y Variáveis numéricas e TESTE uma variável lógica: Errada NÃO x>y+1 NÂO(Y + 2=7 E X >3) X + Y < 10 OU X<10 E TESTE Possibilidade correta NÃO (x>y+1) NÂO((Y + 2 = 7 ) E (X >3)) ((X + Y < 10) OU (X<10) ) E TESTE 22 3.2.9 Exemplo Construir um algoritmo para o problema abaixo: Dados três valores (a,b,c) verificar se os mesmos podem formar um triângulo. Em caso afirmativo, verificar se o triângulo será equilátero, isósceles ou escaleno. Passo 1- Solução descritiva No exemplo anterior a solução descritiva foi: Para solucionar esse problema precisamos saber que em um triângulo, um lado nunca é maior ou igual a soma dos outros. Sabendo disso basta obter os valores e comparar cada valor com a soma dos outros verificando se a condição do problema é atendida. Neste, além disso temos que determinar o tipo do triângulo. Sabemos que um triângulo é: - Equilátero se os seus lados são todos iguais, - Escaleno se todos os lados são diferentes - Isósceles se pelo menos dois de seus lados são iguais Portanto, depois de verificada a condição de existência do triângulo, devemos comparar seus lados verificando qual das condições acima é verdadeira, determinando assim o tipo do triângulo. Passo2 – Algoritmo Algoritmo triângulo 1. Declarar variáveis 2. Obter os três valores (a,b e c) candidatos a lados do triângulo 3. Verificar se os mesmos podem formar um triângulo 4. Verificar o tipo do triângulo 5. Informar o resultado Fim algoritmo Ref. 2 { Obter os três valores (a,b,c), candidatos a lados de um triângulo} Leia (a,b,c) Fim Ref. 2 Ref. 3 { Verificar se os mesmos podem formar um triângulo} E_TRIANGULO←falso Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Fim se Fim se Fim se Fim ref. 3 23 Ref. 4 {verificar o tipo do triângulo} Se E_TRIANGULO então Se ((a = b) E (a=c)) então Tipo ← “Equilátero” Senão Se ((a <> b) E (a<>c) E (b <> c)) então Tipo ← “Escaleno” Senão Tipo ← “Isósceles” Fim se Fim Se Fim Se Fim Ref. 4 Ref. 5 {informar o resultado} Se E_TRIANGULO= verdadeiro então Imprima(“Os valores fornecidos formam um triângulo”) Imprima(“O triângulo é ”, tipo) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim ref. 5 Ref. 1 {declarar variáveis} Declare a,b,c:numérico E_TRIANGULO: lógico Tipo: literal Fim ref. 1 Algoritmo Completo Algoritmo triângulo Declare a,b,c: numérico E_TRIANGULO: lógico Tipo: literal Leia (a,b,c) E_TRIANGULO←falso Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Fim se Fim se 24 Fim se Se E_TRIANGULO então Se ((a = b) E (a=c)) então Tipo ← “Equilátero” Senão Se ((a <> b) E (a<>c) E (b <> c)) então Tipo ← “Escaleno” Senão Tipo ← “Isósceles” Fim se Fim Se Fim Se Se E_TRIANGULO então Imprima(“Os valores fornecidos formam um triângulo”) Imprima(“O triângulo é ”, tipo) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim algoritmo 3.2.10 Exercícios de Fixação Para os problemas abaixo construa algoritmos usando a técnica de refinamentos sucessivos e depois monte o algoritmo completo. Se possível tente encontrar mais de uma solução para cada problema. 1. Ler 4 números verificar se existem dois números iguais entre os quatro. 2. Ler 3 números (A,B,C) trocar os valores colocando o conteúdo de B em A, de C em B e de A em C. 3. Ler 4 números, calcular a média aritmética dos mesmos e imprimir dentre os 4 , os números acima da média calculada. 4. Ler 3 números (A,B,C) e calcular o valor da raiz quadrada do produto entre a média de A e B e o quadrado de C. 5. Ler um número complexo na forma retangular e converter para forma polar. 6. Ler 4 nomes e imprimir os mesmos em ordem alfabética 7. Ler o valor de um empréstimo e imprimir o valor corrigido com juros de 7%. 8. Ler quatro dígitos binários (números que poderão ser 0 ou 1) e imprimir o valor decimal correspondente ao número binários formado pelos 4 dígitos. 9. Ler um número inteiro de 4 dígitos e escrever a sua representação de acordo com o exemplo abaixo: Para o número 2341 deverá ser impresso 2 milhares, 3 centenas, 4 dezenas e 1 unidade. 10. Ler um valor total em segundos e transformar em horas, minutos e segundos. Ex. 11030 seg. = 3h 3min 50 seg. 11. Em uma central de PABX de uma empresa, os ramais são formados por números de 3 dígitos, sendo que quando o dígito das centenas é igual a: 25 - 7 : o PABX irá transferir para o atendimento automático. 8: o PABX irá transferir para o PABX de uma Filial da empresa 9: o PABX irá estabelecer a conexão direta com o ramal Quando um número iniciando em 9 é discado, Se o dígito das dezenas for: - 1 : o PABX irá transferir para a central da Diretoria - 2 : o PABX irá transferir para a central de Marketing - 3: o PABX irá transferir para a central de Atendimento ao Cliente - 4: o PABX irá transferir para a central da Produção Construa um algoritmo que simule o funcionamento desse sistema de PABX. O Algoritmo deverá ler um número inteiro de três dígitos e imprimir mensagens informando destino da ligação correspondente ao número discado, conforme descrito acima. Por exemplo, quando a ligação for para o atendimento automático, imprimir a mensagem: LIGAÇÃO PARA O ATENDIMENTO AUTOMÁTICO. Quando a ligação for para a Produção, imprimir a mensagem: ENCAMINHADO PARA A CENTRAL DE PRODUÇÃO. 12. Construa um algoritmo que leia 4 números e imprima os 4 em ordem decrescente. 13. Construa um algoritmo que leia 3 números inteiros, descubra e informe se a soma do menor com o maior é um número par. 14. Construa um algoritmo que leia 2 números e um literal que deverá ser um dos símbolos das operações aritméticas: “+” , “-“, “*” ou “/” e: - Verifique se o símbolo lido está entre os símbolos possíveis. Em caso afirmativo informar a operação que será realizada e o resultado. Caso contrário imprimir a mensagem: “Erro. Operação inválida” Exemplos: a) Valores lidos 3 4 e “+” Saída do algoritmo: Operação: Soma Resultado: 7 b) Valores lidos 18 9 e “s” Saída do algoritmo: Erro Operação inválida 15. Em um jogo, uma bola principal é atirada do ponto de saída e para a uma distância de X metros da saída. Três competidores lançam suas bolas a exatamente A, B e C metros da saída na mesma direção da bola principal. Ganhará a partida o competidor cuja a bola estiver mais próxima da bola principal. Em caso de empate e as bolas empatadas estiverem a distâncias diferentes da saída, ganha a bola que estiver mais distante da saída. Veja o exemplo abaixo: C= 8 metros X=10 metros Bola principal A=12 metros B=14 metros Saída Ganhador: Competidor A 26 Nesse caso, ganhou o competidor A, embora as distâncias de A e C para a bola principal sejam iguais. Construa um algoritmo que leia as distâncias X,A,B E C , determine e informe o vencedor. 16. Construa um algoritmo que leia o nome e a cidade de 3 pessoas e imprima o nome e a cidade em ordem alfabética tomando por base a cidade. Ex. Valores lidos: Nome1 = Maria Cidade1 = Vitória Nome2 = Fernanda Cidade2 = Aracruz Nome3 = Pedro da Silva Cidade3 = São Paulo Saída: Fernanda Aracruz Pedro da Silva São Paulo Maria Vitória 17. Sabendo que o dia 1 de Janeiro foi um Segunda feira e que os dias da semana se repetem de 7 em sete dias, podemos encontrar o dia da semana de qualquer outro dia de janeiro. Como podemos calcular? Usando o operador resto (%). Construa então um algoritmo que leia um dia do mês de janeiro de 2001 e informe qual o dia da semana. 18. A empresa de telefonia TELECRUZ possui o seguinte sistema de tarifação com relação aos horários de início das ligações: - De 0h00 até as 5h00: 0,1 o segundo - De 5h00 até as 8h00: 0,2 o segundo - Demais horários : 0,8 o segundo Construa um algoritmo que leia o total de minutos de uma ligação, a sua hora de início e determine o valor total da ligação. 27 4 Construção de Programas em Linguagem C Este capítulo tem os seguintes objetivos: - fazer uma introdução a linguagem de Programação C cobrindo as estruturas de programação já estudadas. - Desenvolver maiores habilidades com relação a estratégias para resolução de Problemas através de programas computacionais. A construção de algoritmos vista no capítulo anterior, dependeu exclusivamente do uso dos seguintes elementos s e estruturas: - Estrutura do algoritmo Valores numéricos, literais e lógicos Operadores aritméticos Operadores de comparação Operadores lógicos Declaração de Variáveis numéricas, literais e lógicas Comando de atribuição Comando de entrada de dados Comando de saídas de dados Comando Condicional Simples Comando Condicional composto Expressões aritméticas Expressões lógicas Assim como outras linguagens de programação, a linguagem C possui estruturas e elementos equivalentes aos já estudados em algoritmos, de modo que a implementação de programas nessa linguagem pode ser feita através da transposição dos elementos e estruturas já conhecidas para as formas da linguagem obedecendo a sua simbologia, sintaxe e semântica. 4.1 Estratégias para resolução de Problemas usando C Em se tratando de programação, a expressão “Se você não souber, jamais irá aprender.” é uma verdade absoluta. É muito comum ouvirmos queixas do tipo “nunca conseguirei escrever um programa”, ou “só sendo louco ou gênio para descobrir a solução”. Estas expressões geralmente são ditas por estudantes que desconhecem o fato de que cada elemento da linguagem usada para programar (comandos, funções) não existe sozinho, mas somente combinados a outros elementos. Desta forma a orientação maior para a compreensão geral do programa, deve vir antes da análise detalhada de cada comando ou função. De fato, para construirmos nossos primeiros algoritmos, necessitamos, até o momento, de alguns comandos fundamentais e outros elementos que devem ser de conhecimento do estudante. Mas este, não pode cometer o erro de tentar enxergar os algoritmos de forma fragmentada, pois os elementos de um programa só fazem sentido se vistos como um todo. 28 Falar em estratégias de resolução de problemas sem formalismos matemáticos não é uma tarefa simples e não há nada além do uso da lógica, do raciocínio abstrato e de estratégias como dividir para conquistar e abordagem top-down para facilitar o nosso trabalho. Acredita-se que a facilidade para resolução pode vir da experiência em resolver problemas e do desenvolvimento do senso crítico com relação aos mesmos. Sendo assim, nossa abordagem nesse capítulo será a apresentação dessas estratégias através do desenvolvimento de exemplos e a vinculação do raciocínio lógico e abstrato com o dia-adia do estudante. Todos os elementos utilizados até o momento foram construídos em linguagem algorítmica. De agora em diante nossos exemplos serão construídos em linguagem algorítmica e posteriormente transformados para C. Para tanto, na sessão 2 serão introduzidos os elementos da linguagem necessários. 4.2 Introdução a Linguagem C C foi Desenvolvida nos laboratórios Bell na década de 70, a partir da Linguagem B, por Brian Kernighan e Dennis M. Ritchie. Com a linguagem C podemos construir programas organizados e concisos (como o Pascal), ocupando pouco espaço de memória e com alta velocidade de execução (como o Assembler). Todavia, devido a grande flexibilidade da linguagem, também poderemos escrever programas desorganizados e difíceis de serem compreendidos. Uma análise superficial dos programas escritos em C e Clipper, nos permite perceber que a linguagem C supera em muito em dificuldade o programa análogo em Clipper. Ora, então porque não desenvolvermos programas somente em Clipper? Há inúmeras razões para a escolha da linguagem C como a predileta para os desenvolvedores “profissionais”. Algumas delas estão descritas em suas características: - Portabilidade entre máquinas e sistemas operacionais. - Dados compostos em forma estruturada. - Programas Estruturados. - Total interação com o Sistema Operacional. - Código compacto e rápido, quando comparado ao código de outras linguagem de complexidade análoga. Como nosso objetivo é construir programas, não nos ateremos em comparações, fatos históricos e detalhes não importantes no momento. 4.3 Guia Rápido A seguir temos uma tabela que apresenta de forma sucinta construções algorítmicas utilizadas até o momento. a tradução para C das 29 Estrutura #include <stdio.h> #include <conio.h> #include <stdlib.h> #include <math.h> #define max 100 Linguagem C - Guia Rápido de Consulta - Primeira parte Algoritmo Algoritmo XXXX ..... Fim Algoritmo void main() { ....} Funcionamento/Descrição Todo programa em C tem como corpo principal a função main(). Os comando devem ser colocados entre as chaves { ... } A diretiva #include permite a inclusão de funções das bibliotecas referenciadas. No caso stdio.h e conio.h. Em nossos programas iremos sempre inserir essas bibliotecas e poderemos necessitar de outras. A diretiva #define permite a definição de constantes ou macros. Declaração de variáveis. As listas de identificadores são os nomes das Declare tipo_1 lista de lista de identificadores 1: variáveis que serão declaradas. Cada lista será do tipo especificado anteriormente identificadores 1; tipo1; tipo_2 lista de lista de identificadores 2: Ex: int x,y,z; int é o tipo inteiro identificadores 2; tipo2; ... ... tipo_n lista de lista de identificadores n: tipo n; identificadores n; Inicio... fim Delimitam respectivamente o início e o fim de um bloco de comandos. { ... } Variável = Expressão; Comando de Atribuição. O valor calculado na expressão é armazenado Variável ←Expressão; na variável. A variável e a expressão devem ser do mesmo tipo. if (expressão lógica) Se expressão lógica então Estrutura de decisão simples: O comando1 só será executado caso o Comando1; Comando1 valor da expressão lógica seja verdadeiro. O ";" só vem depois do comando1. Os parêntesis não podem ser esquecidos. if (expressão lógica) { Se expressão lógica então Estrutura de decisão simples. Nesse caso, todo o bloco de comandos Comando1; Comando1 delimitado pelo { ... } só será executado se a expressão lógica for Comando2; Comando2 verdadeira. ... ... Comando n; Comando n } Fim se if (expressão lógica) Se expressão lógica então Estrutura de decisão composta. Nesse caso o comando1 será executado Comando1; Comando1 se a expressão lógica for verdadeira e o comando 2 só será executado se a expressão lógica for falsa. Repare que o último comando antes do else else Senão Comando2; Comando2; não possui ";" if (expressão lógica) { Comando1; Comando2; ... Comando n; } else Comando n+1; Se expressão lógica então Comando1; Comando2; ... Comando n; Fim se Senão Comando n+1; - - Int Float Char Estrutura condicional composta. Se o valor da expressão lógica for verdadeiro, o bloco de comandos entre o { e } será executado. Caso a expressão lógica seja falsa, apenas o comando n+1 será executado. Inteiro São os tipos de dados simples. Variáveis do tipo: - int: podem armazenar valores inteiros com ou sem sinal. Real Literal (caracter ou - float: podem armazenar valores reais, com parte inteira e parte fracionária, separadas por um "." cadeia de caracteres) - char: pode armazenar um único caracter ou uma cadeia de caracteres existentes na tabela ASCII. 30 pow(x,y), sqrt(x) xy e raiz quadrada de (x) printf(“string de Imprima(..) formatação”, “valores”) scanf(“String de Leia(..) formatação”, “endereço das variáveis de armazenamento”) getch() Função usada para obter apenas um caracter da entrada Não ! E && OU || = == <> != <, >, >= e <= <, >, >= e <= /* .... */ ou // .... As cadeias de caracteres devem vir sempre representadas entre apóstrofes. Ex. 'CASA', 'maria', ' X11', 'TEMPO', etc. Para se declarar uma vairiável char para conter uma cadeia de caracter, devemos determinar o tamanho da mesma. Ex: char palavra[20]; declara uma variável literal que pode conter até 20 caracteres. Calcula respectivamente x elevado y e raiz quadrada de x. Devemos incluir a biblioteca math.h : #include<math.h> função usada para saída de dados ( impressão) Função usada para entrada de dados (leitura) Muitas vezes usamos essa função para que o programa pare e possamos ver o resultado na tela Operador lógico de negação. Conectivo lógico E Conectivo lógico OU Operador de comparação igual Operador de comparação diferente (Não igual) Respectivamente: menor que, maior que, maior ou igual que, menor ou igual que Comentários: Quando se que colocar um comentário dentro de um programa em se usam-se os delimitadores /* no início do comentário e */ no final do comentário. Uma outra alternativa é comentar uma linha usando as barras duplas: // Ex: /* Primeiro programa em C */ Ou // Primeiro programa em C % +.*,-,/ while (expressão lógica) { Comando1; Comando2; ... Comando n; } % +.*,-,/ enquanto (expressão lógica) Comando1; Comando2 ... fim enquanto do{ Comando1; Comando2; ... Comando n; } while (expressão lógica); Faça Comando1; Comando2 ... enquanto (expressão lógica) Operador Resto Operadores aritméticos Comando para implementar um laço de repetição. Enquanto o valor da expressão lógica for verdadeiro o conjunto de comandos entre as chaves será repetido sucessivamente. C1, C2..Cn C1C2 .. Cn ... Observação: é importante que quando a execução atingir o while as variáveis utilizadas na expressão lógica tenha valores válidos. Em geral, a lista de comandos dentro do bloco deve garantir que em algum momento a expressão lógica fique falsa para que o programa não entre em loop infinito. Comando para implementar um laço de repetição. Enquanto o valor da expressão lógica for verdadeiro o conjunto de comandos entre as chaves será repetido sucessivamente. C1, C2..Cn C1C2 .. Cn ... Neste caso os comando do bloco são executados pelo menos uma vez. Somente após o final da execução quando o while é executado é que a expressão lógica é avaliada. 31 for(C1; C2; C2){ Comando1; Comando2; ... Comando n; para i Å x ate y faça Comando1; Comando2; ... Comando n; Fim para } break continue Switch (variável avaliada) { case v1: Comando 1; Comando 2; ... Comando k; break; case v2: Comando k+1 Comando k+2 .. Comando m; break; case v3: Comando m+1; Comando m+2 .. Comando n; break; ... default: ... } Caso(variável avaliada) valor 1: Comando 1; Comando 2; ... Comando k; valor 2: Comando k+1 Comando k+2 .. Comando m; Valor 3: Comando m+1; Comando m+2 .. Comando n; ... Fim caso O Comando for é um comando de repetição que contém dentro dos parentes todas os comandos para que seja iniciado e interrompido. Ou seja, o for não depende dos comando de dentro do bloco para ser interrompido. O for contem 3 partes separadas por ponto e vírgula conforme descrito: C1: é um comando executado uma única vez, antes do laço ser iniciado. Geralmente é usado para inicializar uma variável de controle. C2: é a expressão lógica avaliada para continuar ou interromper o laço. Se C2 é verdadeira, o a repetição continua. Se C2 é falsa a repetição é interrompida. Ou seja, o for termina quando C2 é falsa. C3: é um comando executado toda vez que uma repetição é iniciada. Com este comando, a variável utilizada na expressão lógica pode ser atualizada. Um comando break permite a saída antecipada de um laço while, do, for e de um comando swtich. Um comando break faz com que um laço (while,do, for) ou um swtich mais interno seja terminado antecipadamente. Inicia a próxima interação do laço mais interno. Comando de seleção ou chaveamento. O Switch permite que, dependendo do valor de uma variável, um bloco específico de comandos sejam executados. A variável a ser avalida aparece entre os parentes depois da palavra chave switch. Esta variável deve ser um escalar. Ou seja, uma variável que guarde valores contáveis, como um inteiro ou um caractere (um único caracter). Strings ou números reais não podem ser usados. Na implementação do switch em C quando o valor contido na variável avaliada é encontrado junto a um case, todos os comandos existentes após este case até o final do switch serão executados. Por isso, sempre que se queira que apenas o bloco específico do case seja executado, deve-se colocar um break antes do próximo case. Os comandos da opção default são executados caso nenhum case seja satisfeito. A função scanf scanf() Uma das mais importantes e poderosas instruções, servirá basicamente para promover leitura de dados via teclado. Sua forma geral será: scanf(“string de controle”, lista de argumentos); Posteriormente ao vermos sua sintaxe completa, abordaremos os recursos mais poderosos da <string de controle>, no momento bastará saber que: %c - leitura de caracter (char) %d - leitura de números inteiros (int) %f - leitura de números reais (float) 32 %s - leitura de cadeia de caracteres (char) A lista de argumentos deve conter exatamente o mesmo número de argumentos quantos forem os códigos de formatação na <string de controle>. Se este não for o caso, diversos problemas poderão ocorrer - incluindo até mesmo a queda do sistema. A função printf() printf() printf() servirá basicamente para a apresentação de dados no monitor. Sua forma geral será: printf(“string de controle”, lista de argumentos); Necessariamente você precisará ter tantos argumentos quantos forem os comandos de formatação na “string de controle”. Se isto não ocorrer, a tela poderá exibir sujeira ou não exibirá qualquer dado. Os caracteres a serem utilizados pelo printf() em sua <string de controle>, no momento serão os mesmos de scanf(). 4.4 Exemplos Introdutórios Exemplo 1 – Imprimindo uma constante literal Como nosso primeiro exemplo, iremos construir um programa para imprimir a mensagem Olá Pessoal. Seguem algoritmo e o programa equivalente: O programa em C equivalente será: Algoritmo ola Imprima(“Olá pessoal”) #include #include #inlcude #include <stdio.h> <conio.h> <stdlib.h> <math.h> Fim Algoritmo void main() { printf(“Olá Pessoal”); } Para se tornar uma linguagem portável, C possui poucas instruções próprias, sendo que nenhuma operação de entrada e saída (que dependem do Sistema Operacional) é feita através de comandos próprios da linguagem, mas através de funções. As bibliotecas de programação são necessárias para permitir o acesso funções. Nesse primeiro exemplo temos que usar a função printf() que está na biblioteca stdio. Assim foi preciso adicionar a diretiva #include <stdio.h> que inclui o arquivo header (cabeçalho) da biblioteca stdio no nosso programa. 33 Asa outras inclusões que foram feitas não são absolutamente necessárias nesse programa. No entanto, para iniciantes, incluir sempre as bibliotecas stdio, conio, stdlib e math é uma boa prática. Todo e qualquer programa em C possui a função main(). Esta função é onde o processamento irá iniciar. Dentro da função, ou seja, entre as chaves ({ e } ), poderemos declarar variáveis e inserir os comandos que necessitamos para resolver o problema em questão. O abre chaves, indica o início da função main(). O fecha chaves indica o final da função main() (nesse exemplo!) O nome void antes de main(), indica que essa função não deverá retornar nenhum valor. Alguns aspectos da linguagem, como esse, serão melhor entendidos mais a frente, no decorrer do aprendizado e não cabem no momento. Como pode ser observado, a função printf() é a substituta do comando Imprima(). A constante literal Olá pessoal!, está entre “ ”, e é assim que tem de ser sempre que queremos imprimir literais. Palavras reservadas e Regras básicas de Sintaxe C diferencia letras maiúsculas de minúsculas. Todos os comandos e funções e palavras reservadas de C devem ser escritos com letras minúsculas. As palavras reservadas são o conjunto de palavras utilizadas pela linguagem e que não podem ser usadas para outros fins, como nome de variáveis, por exemplo. Quadro de palavras reservadas do C Padrão. auto break case char continue default do Double Else Entry Extern Float For Goto If Int Long Register Return sizeof Short static struct switch typedef union unsigned while Um outro detalhe de C é que todo fim de comando deve ser marcado com um ponto-evírgula (;). Para o estudante não cometer erros com relação a esta regra, o mesmo deve entender o que significa fim de comando. Em um algoritmo por exemplo o comando (a) X←10 termina após o 10. Já o comando (b) Se (X>Y) então (c) X←5 (d) Fim se Não termina após o então pois a ação do comando se irá agir sobre o próximo comando. Dessa forma o comando em C para o exemplo (a), leva o ; 34 X=10; Já o comando (b) não leva o ponto e vírgula pois só terminará após o comando que está sob a condição do if if (X>Y) X=5; O término do if é no ponto e vírgula. Exemplo 2 – Imprimindo números Seja o seguinte algoritmo: O programa em C equivalente será: Algoritmo inteiros Imprima (10) Imprima (10.75) Imprima(10/3) Fim Algoritmo #include #include #inlcude #include <stdio.h> <conio.h> <stdlib.h> <math.h> void main() { printf(“%d\n”,10); printf(“%f\n”,10.75); printf(“%f”,10.0/3.0); } Conforme citado, a função printf necessita da string de controle para determinar o tipo e a posição dos argumentos a serem impressos. Assim, para imprimir um inteiro usamos o %d na string de controle. Para imprimir um número real, usamos o %f. O \n usado na string de controle para pular uma linha. Se não tivesse sido colocado, os números iriam ser impressos todos ligados. Por motivos de espaço, a partir de agora iremos omitir as diretivas #include nos programas, mas lembre-se que elas são quase sempre necessárias. Exemplo 3 – Declarando variáveis Algoritmo variaveis Declare x,yz, S,r,t: numérico A,c: literal x←10 y←5 z←x+y r←5.5 t←10 S←(r+t)/2 A←”z é maior que S” c←”!” Imprima(“Valor de S: ”,S) Imprima(“Valor de z: ”,z) Imprima(A,c) Fim algoritmo A saída desse algoritmo será exatamente: Valor de S: 7.75 Valor de z: 15 z é maior que S! O programa equivalente em C ficará: void main(){ int x,y,z,t; float r,S; char A[20],c; x=10; y=5; z=x+y; 35 r=5.5; t=10; S=(r+t)/2.0; strcpy(A, ”z é maior que S”); c=’!’; printf(“Valor de S: %f\n”,S) printf(“Valor de z: %d\n ”,z) printf(“%s%c”,A,c) } Detalhes importantes no Exemplo 3 - O tipo numérico pode ser de dois tipos: Inteiros ou reais. No caso de inteiro, a variável deve ser declarada como int. No caso de reais, a variável deve ser declarada como float. O tipo da variável vai depender do uso que o programador pretende dar a ela. No exemplo, as variáveis x, y, z e t foram usadas para armazenarem valores inteiros. Sendo assim foi usado o tipo int para declara-las. Já as variáveis r e S foram usadas para armazenar números contendo uma parte decimal, reais e não inteiros. Assim o tipo das variáveis deve ser float. - Os tipos int e float podem ser misturados em expressões aritméticas, sendo que o resultado da expressão será um valor do tipo float. - O separador de casas decimais em C é o ponto (.) - A variável S declarada em maiúsculo, deve ser sempre referenciada no programa em maiúsculo. - A variável A, foi declarada como char de 20 posições. Dessa forma essa variável pode conter valores literais (palavras, por exemplo) de até 20 caracteres. - O número de posições é definido em função do uso que se dará para a variável. Se uma variável char for usada para armazenar palavras cujo tamanho varia entre 10 e 150 letras, a mesma deve ser declarada com tamanho 150.: char Nomes[150]; - A variável c irá armazenar apenas um caracter. - A atribuição de valores a variáveis do tipo char com apenas 1 caracter é feita utilizando-se as aspas simples para especificar o valor: c=’a’; No caso de cadeias de caracteres, como a variável A do ex. 3, a atribuição de valores deve ser feita através do uso de uma função chamada strcpy que deve Ter dois argumentos: a cadeia origem e a cadeia destino. Suponha o exemplo: strcpy(s1,s2), nesse caso, s1 é a cadeia destino e s2 a cadeia origem. Isso significa que o conteúdo de s2 (origem) será copiado para a cadeia s2 (destino). No ex. 3, a cadeia ”z é maior que S” foi copiada para a “cadeia” variável A: strcpy(A,”z é maior que S”); Observe que o contrário (copiar A para ”z é maior que S”) não seria possível, pois só podemos ter como cadeia destino, uma variável e nunca uma constante. - Na função printf, para imprimir um float, devemos usar %f, para imprimir uma cadeia de caracteres devemos usar o %s e para imprimir um caracter usamos o %c. Já o \n ca cadeia de controle é usado para saltar uma linha. 36 - Quando realizamos operações aritméticas envolvendo apenas operandos do tipo inteiro, o resultado da operação será também um tipo inteiro. Por exemplo, na chamada da função: printf(“%f”,10/3), a expressão 10/3 resultará na divisão inteira entre 10 e 3 cujo resultado é 3. Para obtermos a divisão real entre os números 10 e três temos que usar pelo menos 1 operando real. Assim a função printf poderá ser escrita da seguinte forma: printf(“%f”, 10.0/3) ou printf(“%f”, 10/3.0) ou printf(“%f”, 10.0/3.0), ou seja, basta que tenhamos pelo menos 1 operando real para que o resultado da expressão seja um float. Exemplo 4 – Definido constantes, Lendo variáveis, construindo expressões lógicas e testando condições. Nesse exemplo vamos verificar mais alguns pontos importantes da linguagem C. Para isso utilizaremos o exemplo triângulo da sessão 3.2.6. O algoritmo é o seguinte: Algoritmo triangulo Declare a,b,c:numérico E_TRIANGULO: lógico Leia (a,b,c) E_triangulo←falso Se a < b+c então Se b< a+c então Se c < b+a então E_TRIANGULO ← verdadeiro Se E_TRIANGULO então Imprima(“Os valores fornecidos formam um triângulo”) Senão Imprima(“Os valores fornecidos não formam um triângulo”) Fim se Fim algoritmo O programa em C equivalente será: //Definição das constantes #define mensagem1 "Os valores fornecidos formam um triangulo\n" #define mensagem2 "Os valores fornecidos não formam um triangulo\n" #define TRUE 1 #define FALSE 0 void main(){ int a,b,c; int eh_triangulo; printf("Entre com os lados triangulo:"); scanf("%d%d%d",&a,&b,&c); eh_triangulo=FALSE; if ( a<b+c) if (b<a+c) if (c<a+b) eh_triangulo=TRUE; if (eh_triangulo) printf(mensagem1); else printf(mensagem2); getch(); } do Definindo Constantes em C 37 Em C podemos usar a diretiva #define para definir constantes e macros. Uma constante nada mais é que um apelido para um valor. A sintaxe é a seguinte: #define nome_da_constante valor Antes do programa ser compilado, todas as constantes são substituídas na íntegra pelos seus valores. Constantes são usadas para facilitar a programação. Suponha por exemplo que em um programa qualquer seja usado um valor fixo em centenas de expressões. Ou seja, um mesmo valor é repetido centenas de vezes no programa. Caso esse valor sofra alguma alteração, o programador vai ter de alterar o programa em centenas de linhas diferentes. Se, ao invés disso o programador tivesse definido uma constantes para o referido valor, bastaria que ele alterasse o valor na definição da constante, reduzindo assim o esforços de manutenção do programa. A diretiva define pode definir constantes de qualquer tipo. Valores lógicos em C C não possui um tipo lógico especial. Em C, o valor 0 é tido como falso em um teste lógico e os demais valores são tidos como verdadeiro. Por exemplo, Se a é um valor inteiro, posso realizar teste da seguinte maneira: if (a) printf(“A é diferente zero”); else printf(“A vale zero”); de ou, if (!a) printf(“A vale else printf(“A é zero”); zero”); diferente de Para facilitar a escrita e leitura de programas, Podemos definir constantes que representem os valores lógicos: TRUE para o 1 e FALSE para o 0. Essas definições funcionam perfeitamente, pois: − Quando uma expressão lógica é avaliada como Falsa, essa retorna o valor 0. − Uma expressão avaliada como verdadeira, retorna o valor 1. − Finalmente, quando um valor Falso (0) é negado, este é convertido para 1, e quando um valor Verdadeiro é negado, este é convertido para 0. Lendo Variáveis Conforme visto no exemplo, a função scanf foi usada para leitura de variáveis em C. Essa função pertence a biblioteca padrão stdio (standard input output library) e, semelhantemente a função printf, possui uma string de controle para determinar o número e os tipos dos parâmetros que serão lidos. Assim, para cada variável lida, devemos ter 1 “%” seguido da letra que determina o tipo da variável na string de controle. Além disso, em C variáveis simples, devem vir antecedidas do operador de endereço &. Isso ocorre porque a função scanf necessita do endereço da variável para armazenar o valor lido dentro dela. Já variáveis compostas, como cadeias de caracteres (char A[10], por exemplo), não necessitam do operador & para serem lidas. Isso ocorre porque o seu nome já é um endereço de memória. 38 Expressões Lógicas Expressões lógicas em C podem ser construídas de modo semelhante as expressões em algoritmos. Conforme visto anteriormente os operadores lógicos em C são: && - Operador E || - Operador OU ! - Operador NÃO Os operadores de comparação em C são os seguintes: == (dois iguais) - Igual < - Menor que > - Maior que <= - Menor ou igual >= - Maior ou igual != - Diferente Condicionais A sintaxe do if seguinte: simples em C é a if (condição) Comando; Ou if (condição) { Seqüência de comandos } Ou if (condição) { Seqüência de comandos } else { Seqüência de Comandos } Ou O uso de parênteses fechando a condição por completo é sempre necessário. if (condição) Comando; else { Seqüência de Comandos } A sintaxe do if seguinte: Ou composto em C é a if (condição) Comando; else Comando; if (condição) { Seqüência de comandos } else Comando; Um outro detalhe é a necessidade do { } quando o if vai definir a execução de uma seqüência de comandos. Quando apenas uma comando é submetido ao if, não há necessidade do {}. No Exemplo temos: if(a<b+c) //1 if (b<a+c) //2 if (c<a+b) //3 39 eh_triangulo=TRUE; O terceiro if está submetido ao segundo, que por sua vez está submetido a condição do primeiro. Portanto, o comando he_triangulo=TRUE; só será executado caso as três condições sejam verdadeiras. Essa estrutura pode também ser construída utilizando o conectivo lógico &&: if ( (a<b+c) && (b<a+c) && (c<a+b) ) eh_triangulo=TRUE; 4.5 Exercícios de Fixação 1. Construa os programas em C equivalentes a todos os problemas da sessão 3.2.10 2. Sabendo-se que primeiro de janeiro de 2001 foi uma Segunda-feira, Construa um programa que leia uma data qualquer de 2001 (Mês e Dia), determine e imprima o dia da semana equivalente. 3. Faça um programa leia valores de temperatura em Celsius e transforme para Fahrenheit e vice-versa. O programa deve consultar o usuário sobre qual a opção de conversão desejada. Nota. C = (5/9)(F-32). 4. Construa um programa que leia um binário de 8 dígitos e imprima o seu equivalente em Decimais 5. Construa um programa que leia um número de 4 dígitos hexadecimais e imprima seu equivalente em binário, octal e decimal. 6. Construa um programa que leia um número de no máximo 4 dígitos expresso em algarismos romanos e imprima o seu equivalente em números arábicos (sistema decimal). 40 5 Comandos de Repetição Até o momento nossos programas e algoritmos caracterizam-se por serem uma seqüência de instrução que sempre avançam para o fim do programa. Nessa sessão iremos introduzir construções que permitem a construção de “laços de execução” utilizando comandos de repetição, que como o próprio nome sugere, nos permitirá realizar tarefas repetitivas com mais facilidade. 5.1 Motivação Inúmeros são os casos em que necessitamos de inserir laços em programas. Na verdade, é muito difícil encontrarmos problemas cuja solução não dependa de um laço no programa. Isso se deve ao fato que o computador e usada para automatizar processos e automatizar processos em muitos casos significa realizar tarefas repetitivas, geralmente implementadas em programas através de comandos de repetição ou laços. 5.2 Estrutura básica de um laço Um laço em um programa, assim como um condicional é uma estrutura que determina fluxos de controle em programas. A figura a seguir representa um laço genérico: Início do laço Comando 1; Comando 2; Comando 3; Comando 4; ... Comando n Fim do Laço Quando a execução do programa atinge os comandos do interior do laço, esta procede normalmente até chegar no último comando (comando n, no exemplo). Quando chega no último comando, o programa pode voltar para o comando1 e repetir a seqüência de comandos novamente. 41 Quando um programa entra em um laço e permanece neste indefinidamente, sempre repetindo a mesma seqüência de comandos dizemos que o mesmo entrou em um Looping infinito. Exemplo de aplicação: Suponha por exemplo que queiramos calcular a soma dos números pares menores ou iguais a 10 e maiores que 0. A solução do problema deve partir de uma situação inicial, processar os dados de forma a obter os valores necessários para obtermos a soma, Situação inicial: Numero inicial = 10 1. Processamento: Verificar se o número é par acumular o valor em uma variável 2. Obter o próximo número 3. verificar se o número é maior 0. Se for Voltar ao passo 1 5. Apresentar o conteúdo acumulado Exercícios Construa soluções semelhantes a anterior para encontrar: - A soma dos número maiores que 3 e menores 31 que são divisíveis por 3 - A soma do 100 primeiros números inteiro maiores que 0 que são divisíveis por 8 Para que um laço não entre em um looping infinito, este deve Ter uma condição de parada. Muitos erros em programa ocorrem pelo fato do programador não Ter definido uma condição de parada correta. A condição de parada é uma expressão lógica associada ao comando de repetição. 5.3 Sintaxe de comandos de repetição 5.3.1 Comando Enquanto Este comando Possui a seguinte sintaxe: Enquanto (expressão lógica) Comando1 Comando2 Comando3 ... Comando n Fim enquanto Funcionamento Quando um programa atinge o comando enquanto, ele verifica a validade da expressão lógica. Se for válida (verdadeira) os comandos de dentro do enquanto passam a ser executados. Quando o programa atinge o Fim Enquanto, ele volta ao enquanto e testa 42 novamente a validade da expressão. Esse processo se repete até que a expressão lógica torne-se falsa. Expressão Lógica – As expressões lógicas do comando enquanto devem obedecer as condições de construção de expressões lógicas já vistas no capítulo anterior. A seguir veremos alguns exemplos de algoritmos utilizando o comando Enquanto: 5.3.2 Exemplos Exemplo 1 Construir um algoritmo que imprime os números inteiros de 1 a 10 Esse exercício pede para que seja impressa a série de números que inicia em 1 e termina em 10. A alternativa viável é usar um comando de repetição para gerar essa série. A media que os números são gerados, eles devem ser impressos. Ou seja a geração dos números e a impressão ficam dentro do comando de repteição. Usaremos uma variável numérica para gerarmos a série: Algoritmo exemplo1 Declare i:numérico i←1 enquanto (i<=10) // repete enquanto i<=10 imprima(i) // imprime os números i←i+1 // gera o próximo número fim enquanto fim algoritmo Exemplo 2 - Imprimir a soma dos números pares de 1 a 10 Nesse exemplo, temos que: Gerar os números de 1 a 10 e, dentro do repita, acumular os alores gerados em outra variável de modo a obter a soma final. A impressão da soma tem de ser feita fora do comando de repetição: Algoritmo exemplo2 Declare i,soma:numérico soma←0 //inicializo o valor da soma com 0 i←1 //inicializo i com 1 enquanto (i<=10) // repete enquanto i<=10 soma←soma+i // acumula os valores gerados na // variável soma i←i+1 // gera o próximo número fim enquanto imprima(soma) fim algoritmo Exemplo 3 - Algoritmo que imprime os divisores de 100 maiores que 0 Esse exemplo pede para que seja impresso a série de números que são divisores de 100, iniciando de 1. Solução: Utilizando um comando de repetição podemos gerar todos os número de 1 a 100 e verificar se esses números são divisores de 100. Para verificar se são divisores de 100 basta verificar se o resto da divisão entre 100 e o número gerado é 0: Algoritmo exemplo3 Declare i: numérico i←1 //inicializo i com 1 43 enquanto (i<=100) // repete enquanto i<=10 se resto(100,i)=0 então imprima(i) fim se i←i+1 // gera o próximo número fim enquanto fim algoritmo Exemplo 4 - Algoritmo que lê 10 números e imprime o quadrado desses números Qualquer comando pode ser inserido dentro de uma repetição, inclusive comandos de leitura. Nesse exemplo, quero ler 10 números e imprimir o quadrado dos mesmos e só. Sendo assim, não necessitamos de guardar os números em variáveis disitintas, podendo usar apenas uma. Assim lemos um valor e imprimimos o quadrado, depois lemos outro e imprimimos o quadrado, assim por diante até completarmos 10 números. Para que o programa pare de ler quando completarmos 10 números, devemos Ter um contador auxiliar (i): Algoritmo exemplo4 Declare i,n: numérico i←1 //inicializo i com 1 enquanto (i<=10) // repete enquanto i<=10 leia(n) // lê os números imprima(n2) // imprime o quadrado de n i←i+1 // incrementa o contador fim enquanto fim algoritmo Exemplo 5- Algoritmo que lê 10 pares de números e imprime o maior. Nesse caso temos que ler 10 pares de números e imprimir apenas o maior de cada par: Algoritmo exemplo5 Declare i,a,b: numérico i←1 //inicializo i com 1 enquanto (i<=10) // repete enquanto i<=10 leia(a,b) // lê os números se (a >b) então imprima(a) // se a for maior imprime a senão imprima(b) // se b for maior imprime b fim se i←i+1 // incrementa o valor de i fim enquanto fim algoritmo Exemplo 6- Escrever um algoritmo que leia a nota de 30 alunos de uma turma, calcule e imprima a média da turma. Para calcular a média, basta lermos as 30 notas e acumularmos em uma variável. Depois, dividimos o total acumulado por 30: Algoritmo exemplo6 Declare i,nota,soma,media: numérico 44 i←1 //inicializo i com 1 soma←0 // inicializo a soma com zero enquanto (i<=30) // repete enquanto i<=30 leia(nota) // lê as notas soma←soma+nota // acumula os valores das notas i←i+1 // incrementa o valor de i fim enquanto media←soma/30 imprima(media) fim algoritmo Note que não é possível ler e acumular em uma única variável. Precisamos ler em uma variável (nota) e acumular o valor em outra (soma) Exemplo 7 - Calcular e imprimir os 30 primeiros termos da série: 1 3 7 15 31.. Para gerar elementos da série temos que descobrir primeiramente a lei de formação da série. Ou seja, como os termos da série são produzidos matematicamente, em função do termo inicial Neste caso temos que o termo inicial é 1 e os demais são gerados multiplicando o termo anterior por 2 e somando mai 1: Termo = (termo_anterior* 2 )+1 Assim teremos: Algoritmo exemplo7 Declare i,termo: numérico i←1 // uso um contador auxiliar i começando com 1 termo←1 // termo recebe o valor inicial da série enquanto (i<=30) // repete enquanto i<=30 imprime(termo) // imprimo os termos termo←2*termo +1 // atualizo o valor do termo // incrementa o valor de i i←i+1 fim enquanto fim algoritmo Exemplo 8 - Calcular e imprimir os 50 primeiros termos da série: S = 1/1 - 2/3 +3/7 - 4/15 + 5/31 - ... Quando temos uma série com numerador e denominador nos termos é interessante repartir o termo em duas variáveis, uma para o numerador e outra para o denominador. Nesse exemplo, temos ainda a inversão de sinais entre os termos consecutivos. Para inverter, uma das possibilidades é usarmos uma variável sinal, que nos termos impares será positiva (+1) e nos termos pares será negativa (-1). Assim, teremos: Algoritmo exemplo8 Declare a,b,termo,sinal: numérico termo←1 // termo recebe o valor inicial da série a←1 b←1 sinal←1 enquanto (a<=30) // utilizo o numerador na condição do enquanto 45 imprime(termo) // imprimo os termos a←a+1 // atualizo o valor do numerador b←(2*b)+1 // atualizo o valor do denominador sinal←sinal*(-1) // inverto o sinal termo←a/b*sinal // calculo o próximo termo fim enquanto fim algoritmo Exemplo 9 – calcular e imprimir os 20 primeiros termos da série: 1 2 3 5 9 17 33 65 .... Para gerarmos essa série, temos que observar que: - o primeiro termo é 1 - os demais termos são sempre 2 elevado a uma potência adicionado de 1. Temos então que: 1º termo: 1 2º termo: 2 - 20 +1 3º termo: 3 - 21 +1 4º termo: 5 - 22 +1 5º termo: 9 - 23 +1 ..... 20º termo: 262145 - 218 +1 Sendo assim, se usarmos uma variável para contar os 20 termos e iniciarmos a variável com 0: Algoritmo exemplo9 Declare termo,i: numérico termo←1 // termo recebe o valor inicial da série i←0 enquanto (i<20) // o i será incrementado até 20 quando a repetição //termina imprime(termo) // imprimo o termo termo←2i+1 // Gero o próximo termo i←i+1 // incremento o i só depois de gerar o próximo termo fim enquanto fim algoritmo Exemplo 10 - Calcular o fatorial de todos os números de 1 a 10 Para calcular o fatorial de um número lido temos os seguinte algoritmo: Algoritmo fatorial declare n, i, fat: numerico leia(n); se (n<0) imprima ("Não existe fatorial de número negativo"); senão i←n; fat←1; enquanto (i>0) fat←fat*i 46 i=i-1 fim enquanto imprima(fat) fim algoritmo A parte sombreada do algoritmo e a que realmente faz o cálculo do fatorial e o imprime. No exemplo estamos querendo realizar esse processo para uma série de números de 1 a 10. Sendo assim basta construirmos um comando de repetição para gerarmos a série e dentro da repetição iremos calcular o fatorial da forma apresentada: Algoritmo exemplo10 Algoritmo fatorial declare n, i, fat: numerico n←1 enquanto (n<=10) i←n; fat←1; enquanto (i>0) fat←fat*i i=i-1 fim enquanto imprima(fat) n←n+1 fim enquanto fim algoritmo Note que as variáveis i e fat devem ser reiniciadas sempre que a fatorial do próximo número da série for ser calculado. Ou seja, essas inicializações tem de ficar no laço mais externo. Esse exercício mostra como podemos utilizar um laço dentro de outro. A mesma abordagem deve ser usada para a resolução do problema do número de Euler dado a seguir. 5.3.3 Exercícios 1. Escrever um algoritmo que leia um número e verifique se ele é um número perfeito ou não. Um número perfeito é aquele cuja soma de seus divisores, exceto ele mesmo, é igual a ele. Ex: 6 é um número perfeito. Pois 6 = 1 + 2 + 3 2. O número de euler, e (e=2.7182818), pode ser calculado através da série abaixo: e = 1 + 1/(1!) + 1/(2!) +1/(3!) + ... Construa um algoritmo que calcule o número de Euler A somatória deverá parar quando o termo a ser adicionado for menor que 0.00001 3. Ler trinta notas e imprimir a maior e a menor 4. Calcular e imprimir os 50 primeiros termos da série de Fibonacci 5. Construir um algoritmo para encontrar o Máximo divisor comum entre dois números 6. Imprimir os 20 primeiros termos da série 1 3 5 11 29 83 ... 7. Imprimir a soma dos 15 primeiros termos da série 1 3 6 18 66 258 ... 8. Calcular e imprimir o seguinte somatório: S = 1/225 - 2/196 + 4/169 - 8/144 + ...+ 16384/1 9. Calcular e imprimir o seguinte somatório: S = 37*38/1 + 36*37/2 + 35*36/3 + ... + 1*2/37 47 5.3.4 Comando Faça .. .Enquanto Sintaxe Faça Comando1 Comando2 Comando3 ... Comando n Enquanto (expressão lógica) Nessa estrutura a expressão lógica é testada apenas no final do comando. Isso significa que os comandos dentro da estrutura de repetição serão executado sempre, pelo menos uma vez. 5.3.5 Exercícios 1. Refazer os exercícios e exemplos dados para o comando enquanto utilizando o comando faça. 2. Estudem toda a matéria , sem entender a teoria é impossível o aprendizado pleno. Prestem atenção aos detalhes. Concentrem-se e abstraiam: “Nada mais prático que uma boa teoria” 48 6 6.1 Vetores e Matrizes Vetores Seja a declaração string: char s[20]; Esta declaração define uma cadeia de caracteres de 20 posições. Cada posição pode armazenar um elemento de seu tipo básico, no caso o char. Um string pode também ser chamado de um Vetor de caracteres: Um vetor é um conjunto de variáveis de um mesmo tipo básico agrupadas. Cada variável do conjunto pode ser referenciada, manipulada ou lida de maneira isolada. Para termos acesso a cada variável isoladamente vetores mantém índices de cada posição em separado. Uma string é um vetor tratado de maneira especial em linguagens de programação porque o seu uso para manipulação de dados alfanuméricos é muito corriqueiro. No entanto, podemos ter vetores de qualquer tipo básico ou não. Vetor numéricos Declaração: Algoritmo Declare Nome_vetor: vetor[Tamanho] numerico; Exemplo: Declare Vet: vetor[10] numérico; Esta declaração define um vetor de nome Vet com 10 posições Representação gráfica: Vet 0 1 2 3 4 5 6 7 8 9 Cada posições de um vetor numérico pode guardar um valor numérico. 49 Assim se fizermos vet[0]←5 vet[1]←10 vet[2]←15 vet[3]←20 vet[4]←25 vet[5]←30 vet[6]←35 vet[7]←40 vet[8]←45 vet[9]←50 O vetor ficará com os conteúdos: 0 5 1 10 2 15 3 20 4 25 5 30 6 35 7 40 8 45 9 50 A grande vantagem de usarmos vetores está na possibilidade de usarmos variáveis como índices: O resultado acima pode ser obtido da seguinte maneira: Declare i: numérico; i←0 ; enquanto (i<10) vet[i]←5 * (i+1); i←i+1 fim enquanto Usando o comando para, o programa fica ainda menor: Para i←0 até 9 faça vet[i]←5 * (i+1); Leitura e impressão de vetores usando índices Exemplo: ler 10 números e imprimir na ordem que foi lida e na ordem inversa: 50 Algoritmo le_vetor; Declare vet: vetor[10] numérico; i: numérico Para i←0 até 9 faça Leia(vet[i]) Fim para Para i←0 até 9 faça Imprima(vet[i]) Fim para Para i←0 até 9 faça Imprima(vet[9-i]) Fim para Exercícios: 1. Construa um algoritmo que leia 20 números, Calcule a média e imprima os números que ficaram acima da média calculada 2. Construa um algoritmo que leia 50 números e imprima separadamente os número pares e os números ímpares lidos. Neste exercício, use um vetor para ler os números, outro para guardar os números pares e outro para guardar os números ímpares. 3. Construa um algoritmo que leia 50 números e armazene-os em um vetor em ordem crescente. Imprima o vetor lido. 4. Construa um algoritmo que leia 50 números. Estes números poderão ser 0 e 1s. Informe o tamanho da maior sequência de 0 e da maior sequência de 1’s que existir no vetor. 5. Construa um algoritmo que leia dois vetores X e Y de 30 posições. Os vetores x e y representam um conjunto de posições de uma figura da janela de texto 51 Exemplo1 Construa um algoritmo que leia 10 números e armazene-os em um vetor em ordem crescente. Imprima o vetor lido. Solução: A medida que os números são lidos, manter o vetor ordenado: Seja a seguinte simulação: Vetor V- inicial 0 1 2 3 4 5 6 7 8 9 Valores lidos (aux) I 0 1 2 3 4 5 6 7 8 9 Aux 3 2 5 0 4 7 3 8 9 7 Simulação: I= 0 aux =3 V[i]=aux 0 3 1 2 3 4 5 6 7 8 9 1 3 2 3 4 5 6 7 8 9 I=1 aux=2 V[0]=au 0 2 52 V[1]=v[0] Algoritmo 1. Declarar variáveis 2. Ler e Ordenar o vetor em ordem crescente 3. Imprimir o vetor ordenado Fim Algoritmo Ref. 2 {ler e ordenar o vetor} Para i ← 0 até 9 faça leia (aux) Se ( i = 0 ) então 1. insiro o 1° valor lido na primeira posição do vetor Senão 2. Pesquiso o vetor a partir da posição 0 até i para tentar encontrar um valor ja inserido maior que aux 3. Com o índice da posição onde o valor é maior que aux, (j) percorro o vetor da última posição inserida (i-1) até j deslocando os elementos já inseridos uma posição adiantes no vetor 4. Insiro o valor de aux na posição que ficará disponível no vetor Fim se Fim para Fim ref. 2 Ref. 2.1 { insiro o 1° valor lido na primeira posição do vetor} V[i]=aux Fim Ref. 2.1 Ref. 2.2 {Pesquiso o vetor a partir da posição 0 até i para tentar encontrar um valor ja inserido maior que aux j←0 Enquanto (( j<i) E (v[j]< aux)) j←j+1 fim Enquanto Fim ref. 2.2 Ref 2.3 { Com o índice da posição onde o valor é maior que aux, (j) percorro o vetor da última posição inserida (i-1) até j deslocando os elementos já inseridos uma posição adiantes no vetor} k ← i-1 Enquanto (k>=j) 53 V[k+1]←v[k] k←k-1 Fim Enquanto Fim Ref. 2.3 Ref. 2.4 { Insiro o valor de aux na posição que ficará disponível no vetor} V[j]←aux; Fim ref. 2.4 Ref. 3 {imprmir o vetor ordenado} Para i←0 até 9 Imprima(v[i] Fim para Ref. 1 {declarar variaveis} Declare v: vetor[10] numércio Declare i,j,k,aux: numerico Exemplo 2 - Construa um algoritmo que leia um vetor de 20 números. Estes números poderão ser 0 e 1s. Informe o tamanho da maior sequência de 0 e da maior sequência de 1’s que existir no vetor. Solução: Ler o vetor , identificar e contar o tamanho das seqüências de 1 e 0. Guardar sempre o tamanho das maiores seqüências de 1’s e 0’s identificadas. Algoritmo 1. Declaração de Variaveis 2. Leitura e inicialização de variáveis 3. Identificar e contar as seqüências de 1 e 0 e guardar a maior sequencia até o termino do vetor 4. Imprimir resultados Ref. 2 Para i←0 até 19 faça Leia(v[i]) Fim para Maior1←0 Maior0←0 54 Fim ref 2 Ref 3. I=0; Enquanto(i<20) C0←0 Enquanto(( v[i]=0) e (i<20) C0←c0+1 i←i+1 fim enquanto se (c0> maior0) então maior0←c0 fim se c1←0 Enquanto(( v[i]=1) e (i<20) C1←c0+1 i←i+1 fim enquanto se (c1> maior1) então maior1←c1 fim se Fim enquanto Fim ref 3 Ref. 4 {imprmir} Imprima(“ a maior seqüência de 0’s é:”, maior0) Imprima(“ a maior seqüência de 1’s é:”, maior1) Fim ref 4 55 7 7.1 Registros - Structs Definição Assim como um vetor ou uma matriz, um registro agrupa um conjunto de variáveis sob único nome. No entanto, um registro é uma estrutura de dados heterogênea ao passo que um vetor ou matriz é uma estrutura de dados homogênea. Qual a diferença então? No caso dos vetores ou matrizes, o tipo de dados básico das variáveis agrupadas é o mesmo. Ou seja, quando declaramos uma vetor de 100 inteiros, todas as 100 variáveis serão do tipo inteiro. Um vetor pode ser visualmente representado por uma longa lista indexada: 0 1 2 3 4 5 6 7 ... Já um registro se assemelha visualmente a uma ficha de cadastro onde cada campo de preenchimento possui um nome especifico. Por exemplo, suponha que uma escola mantém um arquivo de fichas de alunos contendo os seguintes dados: Nome, Endereço, Telefone, Identidade, CPF e data de Nascimento. Registro Aluno Nome Endereço Telefone Identidade CPF Data de Nascimento Um registro então é uma forma de estruturar dados de acordo com o esquema já utilizado em arquivos de registros tradicionais. Um registro é chamado de estrutura de dados heterogênea porque as variáveis que o compõe podem ser de diferentes tipos básicos. 7.2 Declaração de registros Um registro possui um: - Identificador que é seu nome e obedece as leis de formação de nomes de variáveis 56 - Campos que são os elementos ou variáveis que o compõe. No exemplo acima o nome do registro é Aluno e este possui os seguintes campos: - Nome - Endereço - Telefone - Identidade - CPF - Data de Nascimento Os nomes dos campos deve obedecer as leis de formação de nomes de variáveis, não podendo Ter acentos nem espaços em branco não caracteres especiais nem iniciar com números. Sintaxe da Declaração Declare Identificador_registro: registro < Seqüência de identificadores de campo 1>: Tipo1 < Seqüência de identificadores de campo 2>: Tipo2 .... fim do registro Para declarar o exemplo acima, teremos: Declare Aluno: Registro Nome, endereco: literal Telefone,identidade: numerico CPF,datanascimento: literal Fim do registro 7.3 Manipulando Registros O acesso aos campos de um registro deve ser feito individualmente. Ou seja para lermos, escrevermos, e utilizarmos os campos de um registro, temos uma forma de acessar cada campo individualmente. Cada campo do registro e acessado individualmente através do nome do registro e o nome do campo separados por um ponto “.”. Assim, suponha que queiramos ler o registro Aluno: Leia (Aluno.nome) Leia (Aluno.endereco) Leia (Aluno.telefone) Leia (Aluno.identidade) Leia (Aluno.CPF) Leia (Aluno.datanascimento) Ou em um único comando leia: Leia (Aluno.nome, Aluno.endereco, aluno.datanascimento) Aluno.telefone, Aluno.identidade, Aluno.CPF, 57 Atribuições de valores, escritas e outras operações são feitas da mesma maneira: Aluno.nome ← “Daniela” Aluno.datanascimento ← “20/03/1970” Aluno.CPF←127654844 Atribuições de estruturas de mesmo tipo Algumas linguagens de programação, dentre elas o C permite a atribuição de registros de mesma estrutura diretamente, sem precisar atribuir campo a campo. Exemplo Declare circulo1,circulo2: registro X,Y,Raio: numérico Fim registro circulo1.X←10 circulo1.Y←5 circulo1.Raio←30 circulo2←circulo1 7.4 Vetores de Registros O uso mais comum de registros e associado a vetores. O objetivo é termos uma lista de registros de forma que possamos guardar vários registros de um tipo simultaneamente. Suponha no caso de uma escola. Obviamente esta possuirá mais de um aluno e um único registro não seria suficiente para armazenar as informações de todos os alunos ao mesmo tempo. No entanto, se definirmos um vetor cujo tipo básico é um registro teremos então um conjunto de N registros de alunos, onde N é o tamanho do vetor. Declaração de Vetores de registros: Tomemos como exemplo a declaração do vetor Turma: Declare Turma: Vetor [100] de Registro Nome, endereco: literal Telefone,identidade: numerico CPF,datanascimento: literal Fim do registro /* Com essa declaração, teremos um conjunto de 100 registros contendo cada registro os campos nome, endereco, telefone identidade, CPF e datanascimento. Para ler os dados da turma teriamos o seguinte código: */ Declare i: numérico Imprima(“Digite o numero de alunos:”) Leia (N) 58 Se N <=100 então Para i← 0 até N-1 faça Leia (Turma[i].nome, Turma[i].endereco, Turma[i].datanascimento) Fim para Turma[i].telefone, Turma[i].identidade, Turma[i].CPF, Senão Escreva(“ O número de alunos deve ser menor que 100”) Fim se 7.5 Exercícios 1. Declarar o registro que represente os dados informativos de uma banda de rock. O registro deve conter os dados: Nome, número de integrantes, data de criação, estilo musical, número de Cds gravados. 2. Declarar um vetor de 200 registros que representem os dados informativos de fazendas. O registro deve conter os dados: Nome da Fazenda, endereço, Principal atividade, Tamanho, nome do Proprietário, CPF do proprietário. 3. Construir um algoritmo que leia um conjunto de fazendas de e armazene no vetor de registro declarado no exercício 2 Posteriormente ler um nome de uma fazenda e verificar se este nome está na lista de fazendas lida anteriormente. Em caso afirmativo, imprimir os demais dados da fazenda. 7.6 Registro em C Um registro em C é chamado de struct (estrutura) Sua declaração é feita da seguinte forma: struct { Tipo1 Tipo2 < Seqüência de identificadores de campo 1> < Seqüência de identificadores de campo 2> ..... } Nome_da_struct Suponho o registro aluno declarado anteriormente. Em C teríamos a seguinte declaração. Struct Aluno { char nome[50],endereco[100]; long int Telefone,identidade; char CPF[20],datanascimento[20]; } Al; Para declararmos um vetor ou matriz de structs em C, podemos usar o nome da estrutura predefinida. Assim, para declarar o vetor turma teríamos: struct Al turma[100]; 59 A referência aos nomes dos campos e feita usando o ponto “.”: int register i; strcpy(turma[0].nome, “Maria de Lourdes”); for (i=0;i<100;i++) scanf(“%s %d”,turma[i].endereco,&turma[i].telefone); Notas: - Modificador de tipo register – Aloca a variável em um registrador de memória tornando seu acesso mais eficiente. Modificador de tipo long – aumenta a capacidade de representação do tipo int para um número de 4 bytes. O uso do operador de endereço continua sendo necessário quando se trata de variáveis diferentes de cadeias de caracteres Trabalho 1 – Construir um programa que implemente uma agenda telefônica contendo as seguintes operações Tipo: Individual Data de entrega: 16/08/01 Apresentação: 16 e 17/08 Cadastro de pessoas Pesquisa por nome Eliminação Atualização A agenda deve guardar os seguintes dados: Nome, telefone fixo, telefone celular, e_mail, endereço Os dados devem ser guardados em um vetor de registros. Dica, Cada registro deve conter um campo que indique se o mesmo está ou não ocupado. De modo que quando um registro é eliminado a posição dele no vetor possa ser novamente ocupada. Exercicios: 1. Declare um novo tipo de registro chamado Conta, que deve Ter os campos: código, codigo do Correntista, número, saldo. 2. Declare um novo tipo de registro chamado correntista, contendo os campos Codigo, Nome, CPF, Telefone, e_mail. 3. Declare as variáveis Contas e Correntistas. Contas será um vetor de 500 posições do tipo Conta. Correntistas será um vetor de 500 posições do tipo Correntista. 8 Arquivos em C O objetivo deste capítulo e permitir a construção de programas que armazenem dados de forma permanente em dispositivos de armazenamento secundário, tais como disquetes e discos rígidos. 60 8.1 Definições Sistema de arquivos em C Æ é o conjunto de funções da linguagem que podem ler, escrever e manipular dados em dispositivos periféricos. Stream e Arquivos Æ Stream é uma abstração do dispositivo periférico. O dispositivo real é o arquivo. Em C o termo Stream identifica logicamente um dispositivo periférico qualquer que permita a leitura e/ou gravação de dados. No entanto neste texto usaremos o termo arquivo para nos referirmos aos arquivos gravados em memória secundária Arquivos de dados armazenados em memória Secundária Æ permitem o armazenamento permanentemente dos dados. A linguagem C provê um conjunto completo de funções para manipulação de arquivos em disco. 8.2 Tipos de arquivos Existem dois tipos básicos de arquivos, nos quais se baseiam programas que manipulam arquivos. Em C esses tipos de arquivos são chamados de arquivos texto e arquivos binários. Arquivo texto - É um arquivo cujo conteúdo é baseado em uma seqüência de caracteres que formam linhas determinadas por um caracter de nova linha ( “ \ n ”). Dentro destes arquivos podem ser gravados apenas dados em forma de texto. Ou seja, não poderemos gravar, por exemplo, um valor numérico nestes arquivos, a não ser que este valor seja transformado em seqüências de caracteres. Esta forma de organização de arquivos e a mais simples. Funções em C Funções são subprogramas que quando chamados Arquivo texto Arquivos binários – Arquivos binários tem o seu conteúdo baseado em uma estrutura ou dado que respeita um determinado tipo de dado. Este tipo de dado pode ser um tipo simples (int, float, char) ou um tipo estruturado como registro (struct). Portanto esses arquivos são construídos como uma seqüência de bytes respeitando uma determinada estrutura. Esess arquivos podem ser visualizados como um tabela onde cada linha possui um determinado conjuntos de campos (colunas). Cada linha dessa tabela possuirá também um número que identificará sua posição no arquivo. Por essas características, tais arquivos tem a vantagem de permitir o acesso aleatório a posições. Nome Telefone idade Maria 45545 21 João 87878 30 Paula 78867 22 José 565675 20 ... ... ... Visualização abstrata de um arquivo binário organizado a partir de uma struct contendo os campos nome 61 8.3 Ferramentas para manipulação de arquivos 8.3.1 Estrutura de controle FILE – A linguagem C utiliza esta estrutura para criarmos um ponteiro para o arquivo. Este ponteiro irá conter o endereço da estrutura FILE criada que manterá várias informações sobre o arquivo, como o seu nome, posição atual, etc. Estas informações são usadas pelas funções do sistema de arquivos em C. Para ler ou escrever arquivos, qualquer programa precisa usar ponteiros de arquivos. Declaração de um ponteiro de arquivo FILE *fp; FILE – é o nome da estrutura (tipo de dado) O * define que a variável fp é uma variável do tipo ponteiro para FILE. 8.3.2 Abrindo um arquivo abrir um arquivo significa criar uma associação entre o arquivo físico (na memória secundária) e o programa, através do ponteiro de arquivo. Para tanto usa-se a função fopen cujo protótipo é mostrado a seguir. O protótipo de uma função indica como a função foi definida, qual o tipo de dado que ela retorna e quais os parâmetros da função. Os parâmetros são indicados entre os parênteses da função. Por exemplo no lugar do parâmetro nomearq da função fopen, deve ser informado o nome do arquivo a ser aberto, através de uma variável do tipo ponteiro para char. Ainda não falamos de ponteiros para char. No entanto você já está acustumado a usá-los. Trata-se de uma string, de um variável do tipo cadeia de caracteres (ex. char str[30]) ou de uma constante do tipo string (ex. “arquivox.txt”). O valor de retorno de uma função deve ser atribuído a uma variável do mesmo tipo da função. No caso, fopen() é definida como um FILE *, ou seja, um ponteiro para FILE. 62 Assim sendo, quando fopen é chamada deve ser feita com a atribuição do seu valor de retorna a uma variável do tipo FILE *. Ex: FILE *arq; arq = fopen(“arquivox.txt”, “w); Usar o comando de atribuição entre uma variável e uma função indica que o valor de retorno da função está sendo atribuído a variável. Parâmetro indicando o nome físico do arquivo FILE *fopen ( const char * nomearq, const char *modo) Significa que fopen retorna um ponteiro de arquivo parêmtro do tipo string que determina o modo como o arquivo será aberto nomearq é um ponteiro (nome de string) para uma cadeia de caracteres que indica um nome válido de um arquivo, podendo indicar o seu caminho. modo é um ponteiro para uma cadeia de caracteres que indica o modo como o arquivo será aberto. Esta string é construída utilizando-se os caracteres da tabela abaixo: MODO r w a rb wb ab r+ w+ a+ r+b w+b a+b SIGNIFICADO Abre um arquivo texto para leitura Cria um arquivo texto para escrita ou sobrescreve caso o arquivo exista Anexa a um arquivo texto: abre para escrita no final do arquivo texto ou caia um novo arquivo caso o mesmo não exista Abre um arquivo binário para leitura Cria um arquivo binário para escrita Abre um arquivo binário para escrita no final do arquivo ou cria um novo caso o mesmo não exista Abre um arquivo texto para leitura e escrita Cria um arquivo texto p/ leitura e escrita ou sobrescreve caso o arquivo já exista Abre um arquivo e cria um novo para leitura e escrita no final do arquivo Abre um arquivo binário para leitura/escrita Cria um arquivo binário para leitura/escrita Abre um arquivo binário para leitura e escrita no final do mesmo 63 Exemplo: Suponha o arquivo teste.txt. O programa abaixo pode ser usado para abrí-lo para escrita no modo texto main ( ) { FILE *fp; fp = fopen (“teste.txt”, “w”) } Embora esteja correto, normalmente o código anterior é escrito da seguinte maneira: main ( ) { FILE *fp; if ((fp = fopen (“teste.txt”, “w”) = = null) { printf (“o arquivo não pode ser aberto \ n”); return 0; } } NULL é um valor nulo – inválido. Desta forma o programa irá saber se o arquivo foi corretamente aberto, detectando qualquer tipo de erro que possa ocorrer. Para evitar erros, sempre devemos conferir o sucesso da chamada de fopen. Obs. Sempre que abrirmos um arquivo para escrita, se o arquivo existir o conteúdo do mesmo será apagado. Fechando um arquivo todo arquivo aberto deve ser fechado quando não mais necessário, evitando perda de dados e outros problemas. Para tanto a função fclose ( ) deve ser usada. Seu protótipo é o seguinte: Ponteiro do arquivo int close ( FILE *fp ) fclose Retorna EOF se houver algum erro e 0 caso tenha sucesso.. 8.3.3 Escrevendo e lendo em um arquivo texto Escrevendo caracteres Função: putc ( ou fputc ( ) ). 64 Protótipo: int putc (int ch, FILE*fp); Esta função pode ser usada para escrevermos caracteres em arquivos que foram previamente abertos por fopen (). fp é um ponteiro de arquivo retornado por fopen. ch é definido como int (2 bytes) mas apenas o byte menos significativo é levado em conta. Se putc ( ) falha ela devolve o caracter de fim de arquivo EOF Lendo caracteres Função getc ( ) (ou fgetc()) Protótipo: int getc (FILE,*fp); Lê um caracter de um arquivo previamente aberto por fopen() usando o ponteiro fp. Da mesma forma que putc, getc devolve um inteiro, mas apenas o byte menos significativo é usado. Quando chega ao final do arquivo getc ( ) devolve o caracter EOF. Ex. No código seguinte um arquivo é lido até o seu final do { ch = getc (fp); }while (ch!=EOF); Quando ocorre um erro, getc também retorna EOF. Lendo e escrevendo strings Funções: fputs e fgets As funções: fputs () e fgets() – efetuam operações de leitura e escrita de e para arquivos de forma semelhante a getc e putc. Contudo, elas lêem e escrevem strings, ao invés de caracteres. Protótipos: int fputs ( const char * str, FILE *fp); char * fgets ( char *str, int lenght, FILE *fp); fgets retorna um ponteiro para STR 65 fputs escreve a string str no arquivo especificado. Devolve EOF se um erro ocorre. fgets lê uma string do arquivo especificado até que um caracter de nova linha seja encontrado ou que length-1 caracteres tenham sido lidos, se o caracter de nova linha for lido ele será inserido na string. A string resultante será terminada por um ‘\0’. Retorna NULL se ocorre um erro ou fim de arquivo. Exemplo1: Lê a primeira linha do arquivo “agenda.cpp” int main(int argc, char **argv) { FILE *fp; char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.cpp","r"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } fgets(buffer,100,fp); puts(buffer); getch(); return 0; } 8.3.3.1.1 Exemplo 2 – Escrevendo strings em um arquivo int main(int argc, char **argv) { FILE *fp; char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.txt","a"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } do { printf ("Digite uma string (enter para sair:\n"); gets(buffer); // gets não guarda o ‘\n‘. temos que usar o strcat para // colocar o ‘\n‘ no final da string se quisermos saltar uma linha no // arquivo. strcat (buffer,"\n"); // função de concatenação de strings fputs (buffer, fp); }while (*buffer!='\n'); fclose(fp); } exemplo 3 – Lendo um arquivo e apresentando na tela com fgets() int main(int argc, char **argv) { FILE *fp; 66 char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.cpp","r"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } fgets(buffer,100,fp); while (fgets(buffer,100,fp)!=NULL) { printf("%s",buffer); } fclose(fp); getch(); } Exercícios: 1. Construir um programa que leia uma string e escreva essa string em um arquivo chamado ex1.txt caracter a caracter usando a função putc. 2. Construir um programa que leia o arquivo ex1.txt e guarde os caracteres lidos em uma string. Posteriormente imprima a string na tela. 3. Construir um programa que leia palavras do teclado para um vetor de 20 strings. Posteriormente, grave as strings guardadas no vetor no arquivo ex2.txt usando a função fputs(). 4. Construir um programa que leia o arquivo ex2.txt. usando a função fgets e apresente as strings lidas na tela. 5. Criar um programa que permita a cópia de arquivos texto. O programa deve pedir o nome do arquivo que se quer copiar e o nome do arquivo que será a cópia. 6. Criar um programa que conte o número de palavras de um arquiv. Dica: Uma palavra pode ser terminada por um espaço (‘ ‘), um caracter de tabulação (‘\t’) ou um caracter de nova linha (‘\n’). 7. Construir um programa que leia uma palavra do teclado e conte o número de ocorrências da palavra lida em um arquivo texto. 8. Criar um programa que leia um arquivo e crie uma cópia “criptografada” do arquivo lido somando-se o valor 1 a cada caracter lido no arquivo original. Criar um programa para decriptografar o arquivo subtraindo 1 de cada caracter lido. Sugestão: Não criptografe o caracter de nova linha (‘\n’). 9. Construa um programa que conte o número de ocorrências de uma seqüência de caracteres dentro de um arquivo texto. A seqüência de caracteres deve ser lida do teclado e não deve conter nem espaços, nem tabs, nem caracteres de nova linha. Neste caso a sequência pode estar dentro de um outra seqüência. Por exemplo, se procurarmos a seqüência “ana” em um arquivo que contém o texto: “Mariana é uma garota de Americana muito bacana”. Neste caso, a seqüência ana ocorre três vezes: Mariana é uma garota de Americana muito bacana. 8.4 Outras Funções de manipulação de arquivos feof() – Determina a ocorrência de fim de arquivo quando este é aberto em modo binário. Protótipo: 67 int feof(FILE *fp) Comentário: No caso de arquivos binários, é possível que um dos dados lidos seja o valor EOF. Portanto, não é confiável utilizar este caracter para determinar o fim de arquivo. Assim, devemos usar a função feof() no lugar do teste de fim de arquivo. rewind() – Reposiciona o indicador de posição do arquivo em seu início. Protótipo: void rewind (FILE * fp) A medida que avançamos na leitura ou escrita de um arquivo, o seu indicador de posição avança junto. Assim, se quisermos voltar ao início do arquivo sem fechá-lo, devemos usar a função rewind(). ferror() – determina se uma operação com um arquivo produziu um erro. Protótipo: int ferror(FILE *fp); Ferror informa retornando verdadeiro se ocorreu um erro na última operação realizada no arquivo. Ou seja, ferreor deve ser testada logo após as operações que se deseja verificar. remove() - apaga um arquivo Protótipo: int remove(const char *nomearq) Esta função apaga o arquivo indicado com pelo nome. Esta função deve ser usada cuidadosamente. É sempre bom utilizar questionar o usuário se este deseja realmente apagar o arquivo em questão. 8.5 Lendo e escrevendo em Arquivos Binários - fread() e fwrite() Até agora, usamos o sistema de arquivos de C para lermos arquivos texto. No entanto, C também permite que criemos arquivos do tipo binário. Um arquivo binário é uma forma de organização que permite o armazenamento sequencial de estruturas maiores que um byte (char). Finalidade – a finaldade deste tipo de organização é podermos Ter em armazenamento secundário uma forma de organização mais eficiente. Um aplicação imediata é no progama da agenda. Neste caso, os dados são armazenados em um vetor de registros. Da mesma forma, podemos armazenar os registros em um arquivo binário usando fread e fwrite. 8.5.1 Operador que determina o tamanho de tipos de dados - sizeof() O operador sizeof será bastante utilizado na manipulação de arquivos binários. Isso porque, para usarmos fread ou fwrite precisamos determinar o número de bytes que 68 desejamos ler ou gravar no arquivo binário. Como saber exatamente o tamanho de uma variável? Simples: basta usar sizeof tomando como parâmetro o seu tipo. Por exemplo, o programa abaixo imprime os tamanho em bytes dos tipos char, int, float, double e long double: int main() int a,b,c,d,e; a=sizeof(char); b=sizeof(int); c=sizeof(float); d=sizeof(double); e=sizeof(long double); printf (" char: %d \n int: %d \n float: %d \n double: %d \n long double: %d \n",a,b,c,d,e); getch(); } sizeof pode ser usado para determinar o tamanho de structs. Exercício: Construa um struct para conter os dados de uma pessoa: Nome, telefone, endereco, e-mail, data de nascimento e CPF. Use sizeof para imprimir o tamanho da struct que você declarou. 8.5.2 Função fwrite() Protótipo: Size_t fwrite(const void *buffer, size_t numbytes, size_t count, FILE *fp); Parâmetros: - buffer – ponteiro para uma região da memória que contém os dados que serão escritos no arquivo - numbytes – determina o número de bytes de cada item (elemento individual) a ser escrito no arquivo. - Count determina o número de itens a serem escritos no arquivo. Os bytes serão pegos na memória a partir do endereço especificado por buffer e escritos no arquivo. - fp é um ponteiro para um arquivo previamente aberto por fopen no modo binário. 8.5.3 Função fread() Protótipo: Size_t fread(void *buffer,size_t numbytes, size_t count, FILE *fp); Parâmetros: - buffer – ponteiro para uma região da memória que receber os dados que serão lidos do arquivo irá 69 - numbytes – determina o número de bytes de cada item (elemento individual) a ser lido do arquivo. - Count determina o número de itens a serem lidos do arquivo. Os bytes serão lidos do arquivo e armazenados na memória a partir do endereço especificado por buffer. - fp é um ponteiro para um arquivo previamente aberto por fopen no modo binário. 8.5.4 Exemplo1 - Escrevendo número inteiros em um arquivo // Programa que lê n números inteiros do teclado e armazena os números em um arquivo binário int main() { FILE *fp; int numero,n,i; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); scanf("%s",nomearq); //abre o arquivo para escrita em modo binário fp=fopen(nomearq,"wb"); if (fp==NULL){ puts("erro"); getch(); return 0; } printf("Digite o número de elementos scanf("%d",&n); a serem lidos:"); for(i=0;i<n;i++){ printf("Digite o elemento %d:",i+1); scanf("%d",&numero); // Le um número // escreve o numero no arquivo fwrite(&numero,sizeof(int),1,fp); } if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } 8.5.5 Operador de endereço & Neste exemplo n números inteiros são lidos do teclado para a memória na variável numero, e posteriormente gravados no arquivo. Observe que na função fwrite o parâmetro buffer e substituído por &numero. O operador & usado é chamado de operador de endereço. Ele 70 é usado porque, conforme visto anteriormente, buffer deve ser um ponteiro (endereço) para a região da memória onde se encontra o dado a ser gravado por fwrite. Ou seja, a função do & junto a uma variável é obter o endereço desta variável. Este é o mesmo motivo pelo qual utilizamos o operador & nas variáveis a serem lidas por scanf. 8.5.6 Exemplo 2 – Programa que lê um arquivo binário de números inteiros e imprime na tela int main() { FILE *fp; int numero,n,i; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); scanf("%s",nomearq); //abre o arquivo para leitura em modo binário fp=fopen(nomearq,"rb"); if (fp==NULL){ puts("erro"); getch(); return 0; } fread(&numero,sizeof(int),1,fp); // tenta ler o primeiro elemento do //arquivo while(!feof(fp)){ // feof testa se não é fim de arquivo printf("%d\n",numero); // imprime o número fread(&numero,sizeof(int),1,fp); // le o próximo número // escreve o numero no arquivo } getch(); if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } Neste exemplo, o procedimento de leitura do arquivo é semelhante ao de leitura de uma arquivo texto. Lê-se primeiro elemento fora do while. No entanto, ao invés do teste com o caracter EOF, a função feof() deve ser utilizada. 8.5.7 Exemplo 3 - Escrevendo um vetor de inteiros em um arquivo binário int main() { FILE *fp; int i, n, vetint[100]; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); 71 scanf("%s",nomearq); //abre o arquivo para escrita em modo binário fp=fopen(nomearq,"wb"); if (fp==NULL){ puts("erro"); getch(); return 0; } // le o numero de elementos a ser lido para o vetor printf("Digite o número de elementos do vetor:"); scanf("%d",&n); // le os números preenchendo o vetor for (i=0;i<n;i++) { printf("Digite o elemento %d",i+1); scanf("%d",&vetint[i]); } // escreve o conteudo do vetor de uma única vez fwrite(vetint,sizeof(int),n,fp); //fecha o arquivo if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } Neste exemplo, o vetor foi previamente lido elemento a elemento do teclado. Posteriormente o vetor foi escrito de uma única vez através de fwrite. Veja que o parâmetro count de fwrite foi substituído por n que determina o número de elementos lidos para o vetor. Isso significa que o número de bytes a ser gravado no arquivo nesse caso será igual a sizeof(int)*count a partir do endereço vetint. 8.5.8 Nomes de vetores são ponteiros Observe que o no último exemplo, o parâmetro buffer de fwrite foi substituído apenas por vetint e não por &vetint. Isso ocorre porque na realidade, um nome de vetor em C já é um endereço de memória. Ou seja, o nome de um vetor possui um endereço que aponta para a região da memória onde se encontram as posições daquele vetor. Mais especificamente, um nome de vetor possui o endereço (aponta) de memória da primeira posição do vetor. Por esse motivo, o operador & é desnecessário quando usamos vetores. Por esse mesmo motivo, na função scanf, quando vamos ler uma string (vetor de char), não precisamos usar o operador de endereço. 9 Apontadores Defnição: Um apontador ou ponteiro é uma uma variável que pode conter o endereço de outra variável. Sua utilização é vasta em programas por dois motivos: 72 1. Algumas computações só são possíveis com a utilização de ponteiros 2. A utilização de ponteiros em geral simplifica a programação e aumenta a eficiência do código executável, tornando-o mais rápido. 9.1 Apontadores e Endereços Deve ficar claro que o apontador não é um endereço de memória. É uma variável que pode conter um endereço. Por exemplo Seja x uma variável inteira e px um ponteiro. Como sabemos, o operador & fornece o endereço de uma variável. Assim a atribuição abaixo é válida: px=&x Observação: O operador & só pode ser aplicado sobre variáveis e elementos de arranjos. Não é possível por exemplo obter o endereço de expressões como &(x+1). Suponha a seguinte sequencia de código: x=7; px= &x; A figura abaixo mostra esquematicamente a situação em uma memória hipotética de 256 posições endereçadas de 0 (00000000) a 255 (11111111). Suponha que a variável x tenha sido alocada no endereço 3 da memória e px na posição 5 da memória. Assim, após a execução das linhas acima teremos a seguinte situação: Memória Endereço 0000000 0 0000000 1 0000001 0 0000001 0 0 1 0000010 0 0000010 0 0 1 0000011 0 0000011 1 .... Dado 0 0 0 1 1 1 0 0 0 0 1 1 73 O operador * trata seu operando como um endereço permitindo o acesso a esse endereço para manipulação do conteúdo desse enreço. Por exemplo, a atribuição px=&x *px= 6; coloca o valor 6 no endereço apontado por px, ou seja em x. Seja y uma variável inteira. Então a seqüência: x=20; px=&x; y= *px; Surte o mesmo efeito de x=20; y=x; Quando temos uma variável do tipo apontador (px) com um endereço para uma outra variável (x), dizemos que px aponta para x. 9.2 Declaração de Apontadores Assim como qualquer outra variável, um apontador necessita ser declarado: int x,y; int *px; Como sabemos, int x,y declaram x e y como duas variáveis inteiras. A declaração int *px é a declaração do ponteiro px. Esta declaração é uma menemônico que representa a seguinte idéia: Como sabemos uma variável do tipo int x, indica que x é um inteiro. O mesmo raciocínio pode ser aplicado com relação a *px. Ou seja *px é um inteiro também. Este raciocínio deve ser sempre empregado identificarmos e entenderemos claramente a declaração de variáveis e ponteiros. É importante perceber que a sintaxe da declaração das variáveis tem forma similar a sintaxe das expressões onde a variável pode aparecer. Ou fator importante é que um apontador em geral restringe-se a apontar para um único tipo de objeto, explicitado na declaração. A declaração do apontador px do tipo inteiro significa que a forma *px pode ser utilizada em qualquer situação em que uma variável iteira pode poderia. Por exemplo, a sequência abaixo: Int *px, x , y; x=20; px=&x; y=*px +10; 74 Faz com que y receba o conteúdo de x somado com 10. Outras expressões válidas são: px=&x; Atribui o valor 10 no endereço apontado por px: *px=10; Incrementa o o conteúdo de px em uma unidade: *px+=1; ou (*px)++; Na última construção, o uso dos parênteses é impressindível. Sem eles, estaríamos incrementando o ponteiro e não o seu conteúdo, dado que operadores unários ( * e ++ por exemplo) são avaliados da direita para a esquerda. Não podemos esquecer que apontadores são variáveis. Assim sendo serão também válidas espressões envolvendo variáveis apontadores. Por exemplo, se py é uma variável do tipo ponteiro, então a expressão py=px; copia o conteúdo de px (não o conteúdo do endereço apontado por px) para dentro de py fazendo com que py passe a apontar para o mesmo local para onde px aponta. Por exemplo, na seqüência abaixo, o conteúdo de x e atribuído indiretamente a y através dos ponteiros: int x, y, *px, *py; x=10; px=&x; py=px; y=*py; Note que py aponta para x e não para y. Vejamos esquematicamente. Suponha que os nomes representam os endereços das variáveis na memória. x=10; X y px py 10 px=&x; 75 X y 10 px py x py=px; X y 10 px x py x y=*py; X 10 y 10 px x py x Exercícios Analise as seguintes sequências de código. Verifique se estão corretas ou incorretas. Caso estejam incorretas, apontem os prováveis erros. Para tanto, considere a declaração das variáveis abaixo: int a,b, *c, *d; a) c=a; b) c= &b; d=c; *d=10; c) c=*a; d=10+ *c; d) d=c; c=&a; a=10; b=*d; printf(“%d”,b); e) c=&a; scanf(“%d”,&c); d=c; b=*d; f) b=10; c=&b; printf(“%d, c); 76 g) *c=20; h) b=2; c=&b; *c+=2; i) b=1; c=*b; *c=a+ *c; j) a=1; c=&a; for(b=0;b<10;b++) *c+=1; k) c=&b; b=10; printf(“%d”,*c); l) scanf(“%d”, c); m) a=100; c=&a; while (a>0){ *c=*c-1; printf(“%d\n”,a); } n) *c=10; d=c; *d=*d +10; printf(“%d”,*d); 77 9.3 Qual o valor inicial de um apontador? Observe o seguinte programa: Main() { int *px, int x; *pt=10; x=*pt; } O que há de errado neste programa? Basta construir o esquema de quadros e observar: *pt = 10; X Px ??????? Logo na primeira instrução percebemos que não há coerência na atribuição. Isso porque: se px não aponta para ninguém, como poderemos atribuir um conteúdo para dentro deste ninguém? Devemos Ter em mente que inicialmente em um apontador não há endereço nenhum. Se há um endereço inicial em um ponteiro, este aponta para uma posição desconhecida da memória. Isso significa que se usarmos um ponteiro sem a sua prévia inicialização tornamos a manipulação desse ponteiro uma provável fonte de erros graves no programa. Situações como essas podem facilmente derrubar o programa ou mesmo o sistema operacional . Assim, para manipularmos apontadores, estes devem apontar sempre para um endereço de memória previamente alocada pelo programa (através da declaração de variáveis ou dinamicamente como veremos no mais adiante). Caso isso não aconteça estabelece-se uma situação a usar o conteúdo de uma variável sem antes atribuir qualquer valor a esta. Por exemplo: int x,y; y= x+10; 9.4 Apontadores e Vetores (Arranjos) Na sessão anterior, para incrementarmos o conteúdo de um ponteiro em uma unidade usando o operador ++ tivemos que usar os parênteses: 78 (*px)++; A pergunta agora é: qual o efeito da instrução *px++; A resposta desta pergunta pode nos ajudar a compreender a relação entre apontadores e vetores. Mais adiante a pergunta será respondida. Na linguagem C, a relação entre apontadores e vetores é muito próxima. Qualquer operação realizada com índices de vetores pode ser realizada através de apontadores. Em geral a solução com apontadores será mais rápida. A definição int a[10] define um vetor de tamanho 10. Ou seja, um bloco de 10 objetos consecutivos na memória chamados respectivamente de a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8] e a[9]. A notação a[i] refere-se ao i-ésimo elemento do vetor a partir de seu início. Considere p um apontador para inteiro e a um vetor de 10 inteiros definidos como segue: int *p; int a[10]; Nesse caso, p=&a[0]; fará com que p receba o endereço da primeira posição do vetor a. Ou seja, p irá apontar para a posição 0 do vetor a. Pela definição da linguagem, se p aponta para um posição de um vetor, p+1 apontará para a próxima posição do vetor e p-1 apontará para a posição anterior do vetor. Genericamente, se p aponta para uma posição do vetor, p- i apontará para a i-ésima posição anterior a p e p+i irá apontar para a i-ésima posição posterior. Sendo assim, A seqüência p=&a[0]; p=p+9; *p=20; irá armazenar o valor 20 na posição a[9] do vetor. Após a atribuição p=&a[0]; Teriamos as seguintes correspondências: Acesso ao conteúdo a[0] p *p a[1] p+1 *(p+1) 79 a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] p+2 p+3 p+4 p+5 p+6 p+7 p+8 p+9 *(p+2) *(p+3) *(p+4) *(p+5) *(p+6) *(p+7) *(p+8) *(p+9) 9.5 O nome do vetor é um apontador constante Quando um vetor é criado, seu nome é convertido para um apontador para a primeira posição do vetor. Assim se temos a declaração int v[10]; *v fará referência a primeira posição do vetor *(v+i) fará referência a i-ésima posição do vetor São então válidas expressões com o nome do vetor sendo um apontador. Por exemplo, O fragmento abaixo, for(i=0; i<10 i++) v[i]=0; Pode ser escrito assim: for(i=0; i<10 i++) *(v+i)=0; Note que o comando *(v+i) =0; NÃO ALTERA O VALOR DO APONTADOR v. Não poderia ser diferente pois, o apontador v, por ser nome de vetor, é um apontador constante. Ou seja, o valor de v não pode ser alterado. De fato, não faria sentido alterar o valor de um endereço de um vetor. Dessa forma que tentem alterar o valor de um apontador constante não são válidos: Por exemplo, v++; v=v+5; v--; v=v+i; são comandos não aplicáveis, quando v é um vetor. 80 Todos os aspectos discutidos aqui se aplicam a vetores de outros tipos de dados. Vejamos um exemplo com vetores de caracteres: char str[30]; int i; // inicialização da string str com \0 *str=’\0’; // coloco o carcater a ná posição 10 da string *(str+10)=’a’; // coloco o caracter ‘b’ em 29 posições da string e o \0 na última posição for (i=0; i<29;i++) { *(str+i)=’b’; } *(str+i)=’\0’; // leio 29 caracteres e guardo na string, colocando o \0 na última posição: for(i=0;i<29;i++) { *(str+i)=getchar(); } *(str+i)=’\0’; 81 9.6 Exercícios Verificar a corretude e determinar o que será feito nos fragmentos de código abaixo. Leve em consideração as seguintes declarações: int v[5], *pi,*pa, x,i; EXEMPLO pi=v; *pi=30; *pi+1=50; Está incorreto pois na linha 3 a referência *pi+1=50 não faz sentido pois uma referência a uma variável ou posição de vetor é necessária do lado esquerdo da expressão. Uma possivel correção seria: pi=v; *pi=30; *(pi+1)=50; De forma que a posição v1 seria coupada pelo valor 30: Esquema: Situação inicial: v pi v0 v0 v1 V2 v3 v4 v0 v1 V2 v3 v4 v0 30 v1 V2 v3 v4 v1 50 V2 v3 v4 pi=v; v pi v0 v0 *pi=30; v pi v0 v0 *(pi+1)=50; v pi v0 v0 v0 30 *pa+=π a) pi=v; *pi++=200; pi=pi+2; *pi=100; c) *v=10; v++; *v=20; b) v[0]=10; v[4]=30; pi=&v[4]; pa=&v[0]; d) scanf(“%d”,v); if (v[0]>10){ pi=v+1; *pi=2*(*v); } 82 else { *(pi+1)=v[0]; } 83 e) scanf(“%d”,v+1); v++; x=*v; if (x <0) { pa=&v; for (i=0;i<5;i++) *pa+1=i; } else { pa=&v[4]; *pa= x+5; } f) pi=*v; for(i=0;i<5;i++) scanf(“%d”,&(pi+i)); if (*pi > *(pi+1)) { x=pi; *pi=*(pi+1); *(pi+1)=x; } g) v[0]=70; v[1]=40; pi=v; pa=v+1; if (*pi>*pa){ pi=pa pa=v; printf(“%d %d”, *pi,*pa); } h) pi=&v[4]; v=pi-1; *(p1-1)=10; printf(“%d”,*v); i) pi=&x; pa=v; scanf(“%d”,&x); *pa=*pi; *pa+1=*pi-1; *pa+2=*pi-2; *pa+3=*pi-3; Considere agora as seguintes declarações: char *ps, *pr, s1[10],s2[10]; int i; 84 j) strcpy(s1,”Aracruz”); ps=s1; ps++; printf(“%c”,*ps); l) strcpy(s1,”Aracruz”); ps=s1; while(*pr=*ps)!=’\0’); Solução de problemas Usando apontadores - Exemplos Comentados Considere o seguinte problema: Um programa tem um espaço para armazenamento de palavras de tamanho = 100 caracteres, ou seja um vetor de 100 caracteres. As palavras armazenadas no vetor devem ser terminadas com um \0. Construa um programa que armazene o máximo de palavras nesse vetor. A palavras serão lidas com a função scanf. Assim, o programa deve fornecer um apontador para a primeira posição livre do vetor. /*exemplo 1 : Usando um string para guardar várias palavras O objetivo desse programa é utilizar uma única string para guardar várias palavras. O tamanho total da string é 20 Cada palavra ocupa o espaço correspondente ao número de caracteres + 1 do \0 As palavras são lidas e guardadas a partir do início da string. A leitura será feita usando um apontador para a primeira posição livre da string Algoritmo 1. Declaração de variáveis 2. Inicialização de Variáveis Faça Imprima("Escolha uma das opções abaixo:"); Imprima("Ler uma palavra - 1"); Imprima("Imprimir as palavras armazenadas - 2"); Imprima("Procurar uma palavra- 3"); Imprima("Sair - 0:"); leia(op); se op=1 então 3. ler uma palavra senão se op=2 então 4. Imprimir as palavras armazenadas senão se op=3 então 5. Procurar uma palavra fim se fim se fim se enquanto (op<> 0) 85 Fim algoritmo 86 Implementação dos refinamentos Ref. 1 {declaração de variáveis} char buffer[20], *p, *paux; int op; Ref. 2 { inicialização de variáveis} p=buffer; fim ref. Ref. 3 { ler uma palavra } Quando uma palavra poderá ser inserida no vetor? Quando houver espaço Como testar se ha espaço? Comparando os ponteiros p e buffer+19 O ponteiro p ser menor que o endereço da última posição da string significa que há espaco ainda no buffer. Então podemos testar: se (p< buffer+19) le a palavra senão imprima("Não há mais espaço para outras palavras"); fim se Além disso, podemos imprimir o número de espaços vagos no vetor. Quando subtraimos um ponteiro de outro relacionado a um mesmo vetor, obtemos o número de espaços que existem entre eles Assim, se pegamos o endereço da ultima posição (buffer+19) e subtraímos de p (primeira posição livre) teremos: buffer+19-p reescrevendo o código acima teremos: se (p< buffer+19) imprima("O número de espaços vagos é , buffer+19-p) le a palavra senão imprima("Não há mais espaço para outras palavras"); fim se O ponteiro p deve ser alterado para que ele fique sempre apontando para a próxima posição livre do vetor. O endereço da próxima posição livre é dado por p+tamanho(palavra lida)+1 Dessa forma, p deve ser atualizado por p+tamanho(palavra lida) reescrevendo o código acima teremos: se (p< buffer+19) imprima("O número de espaços vagos é , buffer+19-p) le a palavra p<--p+ tamanho(palavra lida) +1 senão 87 imprima("Não há mais espaço para outras palavras"); fim se Ref. 4{ Imprimir as palavras armazenadas} Usando um ponteiro auxiliar (paux) podemos percorrer o vetor imprimindo as palavras. Inicialmente paux aponta para a primeira posição: paux<-- buffer Posteriormente, o ponteiro paux deve se atualizado de forma que aponte sempre para o início das palavras Quando paux > = p não háverá mais palavras para serem impressas, pois p aponta sempre para o primeiro lugar vazio de buffer Assim podemos fazer: paux<--buffer Faça se (p>paux) então imprima(paux); paux=paux+tamanho(paux)+1; } }enquanto(p>paux); A implementação completa e dada a seguir. Como execício, verifiqueo funcionamento da implementação da opção de pesquisa de palavras. */ int main(int argc, char **argv) { char *p,*paux, buffer[20],str[20]; int op; p=buffer; do{ printf("Ler uma palavra: 1\n"); printf("Imprimir as palavras: 2\n"); printf("Pesquisar uma palavra: 3\n"); printf("Sair: 0\n"); scanf("%d",&op); if( op==1) { if (p<buffer+19){ printf("O espaco maximo para a palavra e: %d\n Digite a palavra:" , buffer+19-p); scanf("%s",p); p = p+strlen(p)+1; } else { printf("Nao ha espaco para mais palavras\n"); } } else if(op==2){ paux=buffer; do{ if(p>paux) { printf("%s\n",paux); paux=paux+strlen(paux)+1; } 88 }while(p>paux); } else if(op==3) { printf("Digite a palavra a ser pesquisada: "); scanf("%s",str); paux=buffer; do{ if(p>paux) { if(!strcmp(paux,str)){ printf("Palavra encontrada: %s\n",paux); break; } else { paux=paux+strlen(paux)+1; } } }while(p>paux); if (p<=paux) printf("A palavra não existe no buffer:\n"); } }while(op!=0); return 0; } 9.6.1 Mais exemplos Copia de cadeias. // Versão com indices void main() { char s1[30],s2[30]; int i; // versão com indices scanf("%s",s1); i=0; while(( s2[i]=s1[i])!='\0') i++; printf("%s",s2); getch(); } // versão com apontadores void main() { char s1[30],s2[30], *p1,*p2; scanf("%s",s1); p1=s1; p2=s2; while(( *p2=*p1)!='\0'){ p1++; p2++; 89 } printf("%s",s2); getch(); } // versão com apontadores atribuindo e incrementando // dentro da condição do while void main() { char s1[30],s2[30], *p1,*p2; scanf("%s",s1); p1=s1; p2=s2; while(( *p2++=*p1++)!='\0') ; printf("%s",s2); getch(); } Notemos que o \0 é falso para C então a comparação (*p2++=*p1++)!='\0' é redundante. Assim o código em sua versão final pode ser escrito da seguinte maneira (mínima): // versão com apontadores atribuindo e incrementando dentro da // condição do while void main() { char s1[30],s2[30], *p1,*p2; scanf("%s",s1); p1=s1; p2=s2; while(*p2++=*p1++) ; printf("%s",s2); getch(); } Comparação de strings // versão com índices void main() { char s1[30],s2[30], *p1,*p2; int i,retorno; scanf("%s",s1); scanf("%s",s2); i=0; retorno=-1; while(s1[i]==s2[i]) if (s1[i++]=='\0') { 90 retorno=0; break; } if (retorno!=0) retorno = s1[i]-s2[i]; if (retorno == 0 ) printf("São iguais"); else if (retorno<0) printf(" %s menor que %s", s1,s2); else printf(" %s maior que %s", s1,s2); getch(); } // versão com apontadores void main() { char s1[30],s2[30], *p1,*p2; int retorno; scanf("%s",s1); scanf("%s",s2); p1=s1; p2=s2; retorno=-1; for(; *p1==*p2;p1++,p2++) if (*p1=='\0'){ retorno=0; break; } if (retorno!=0) retorno = *p1-*p2; if (retorno == 0 ) printf("São iguais"); else if (retorno<0) printf(" %s menor que %s", s1,s2); else printf(" %s maior que %s", s1,s2); getch(); } 91 10 Construção de Funções Até o momento, os programas que temos construído são compostos de um bloco único de declarações de variáveis e comandos: a função main. Como no programa abaixo: int main() { int x,y,z; scanf("%d%d",&x,&y); z=x+y; printf("%d",z); getch(); return 0; } A função main (principal) é a função central de qualquer programa em C. Todo programa em C tem de possuir uma função main e começa a execução a partir desta. Assim, no programa acima, a primeira instrução a ser executada é scanf("%d%d",&x,&y); Note que scanf não é um comando da linguagem C, mas sim uma função da biblioteca stdio (Standard Input Output – biblioteca padrão de entrada e saída). Da mesma forma printf é uma função de stdio e getch é um função da biblioteca conio (console input output- biblioteca de entrada e saída de console ). Assim como estas, existem dezenas de outras funções úteis na biblioteca C. 10.1 Chamada de Função O que acontece exatamente quando scanf é executada? Quando o computador encontra a função scanf("%d%d",&x,&y); dizemos que houve uma chamada da função scanf. Uma chamada de função faz com que o computador execute um desvio na execução direcionando-se para o código da função chamada. Uma representação do que acontece pode ser visualizada na figura abaixo: Desvio para a função e transferência de dados int main() { .... scanf("%d%d",&x,&y); z=x+y; ... } int scanf(const char *format[, address, ...]) { /* implementação de scanf */ Retorno da função scanf para main na execução do comando return transferência do valor de retorno return ...; } 92 10.2 Fluxo de Execução É importante notar que no estilo de programação seqüencial que estamos considerando, quando ocorre a chamada de uma função, a função que chamou fica interrompida até o término da função chamada . No exemplo do início da seção, o comando z = x+y; só executará após o término da execução da função scanf. 10.3 Passagem de parâmetros Na hora da chamada da função ocorre também a transferência de dados da função que chamou (main) para a função chamada (scanf). Essa transferência de dados é chamada de passagem de parâmetros. Através da passagem de parâmetros, funções conseguem executar suas tarefas sobre dados especificados na hora da chamada. Tomemos como exemplo a função scanf. Através de seus parâmetros ela consegue identificar para que endereços de memória ela deve ler os dados: (scanf("%d%d",&x,&y);. Assim, conseguimos usar scanf para ler dados para qualquer endereço de variável definida no programa, bastando alterar os parâmetros na hora da chamada da função. 10.4 Tipo de retorno da função Quando uma função termina, esta pode transferir um valor para a função que a chamou através do comando return. Este valor deve é determinado na implementação da função e deve ser de um tipo definido este tipo é definido na declaração da função e é denominado o tipo de retorno da função. No caso programa anterior, main é do tipo int por causa da sua declaração: int main ( ) { ... } O valor de retorno de uma função pode ser capturado pela função chamadora através de um comando de atribuição. Por exemplo: Ch=getch(); O comando acima chama a função getch cuja finalidade é ler um caracter do teclado. Quando a mesma é executada, o caracter lido é atribuído a variável Ch. Um outra forma de utilizar o valor de retorno é em alguma comparação. Por exemplo: if (getch() == ‘s’) Neste caso o valor de retorno foi usado apenas para o teste e não pode mais ser recuperado 10.5 Construção de Funções Se main é uma função e scanf também, podemos deduzir que scanf ou qualquer outra função deve possuir características construtivas semelhante a main. Ou seja scanf, printf e getch, por serem funções devem ser construída em um esquema parecido com a função main. Daí, concluímos que para construirmos funções novas seguiremos esquemas 93 similares a implementação de main, tendo um nome, um tipo de retorno, parâmetros, variáveis e uma seqüência de comandos. Na realidade, funções podem ser vistas como sub-programas: são construídas com as mesmas características de um programa, tem um finalidade definida, são executadas seqüencialmente, iniciam e terminam. Como exemplo, vamos verificar a implementação da função copia (similar a strcpy) e sua utilização int main() { char s1[30],s2[30]; scanf("%s",s1); copia(s2,s1); printf("%s",s2); getch(); } int copia( char *s, char *t){ while(*s++=*t++) ; return 1; } Da mesma forma que main, copia também possui um tipo e um nome (int copia). A função main, nesse caso não possui parâmetros, por isso os parênteses vazios (main()). copia possui dois parâmetros: dois apontadores para char (char *s, char *t). Na função main são declaradas variáveis (char s1[30],s2[30];). copia não possui nenhuma variável interna mas poderia ter. copia tem a função de copiar o conteúdo de uma string para a outra. Os ponteiros para as strings que ela deve manipular devem ser passadas para ela através dos parâmetros na hora da chamada da função: (copia(s2,s1);) na função main. Na passagem de parâmetros em C os dados transferidos para a função são como “cópias” dos originais. Ou seja, quando s1 e s2 são colocados na lista de parâmetros de copia, o que é transferido é o valor de dentro de s1 e s2, nesse caso os endereços iniciais dessas duas strings. Em posse dos endereços das duas strings, a função copia pode manipular as variáveis cujos endereços foram transferidos. Discutiremos a passagem de parâmetros mais adiante. Um programa em C pode possuir várias funções e não só a função main. Na verdade, a maioria dos programas em C é formado por um grande conjunto de pequenas funções e não por funções grandes. Isso porque quando repartimos o nosso programa em pequenas funções tornamos o código mais compacto, legível e mais fácil de se depurar. Todas essas são vantagens da utilização de funções mas podemos dizer que a maior vantagem é reutilização de funções, ou seja, desde que uma função esteja implementada, podemos utilizá-la quantas vezes quisermos somente fazendo uma chamada da mesma. 94 A divisão de um programa em funções acompanha a definição do algoritmo e da construção de refinamentos. Em geral uma função é a implementação de um refinamento. Vejamos o exemplo a seguir: Construir um programa que realize as 4 operações aritméticas entre dois números: Algoritmo operacoes Declare a,b,c:numérico op: literal Leia(a,b); Leia(op) Se (op=’+’) então 1 Soma a e b e guarda em c Fim se Se op=’-‘ então 2 Subtrair b de a e guarda em c Fim se Se op=’*’ então 3 Multiplica a e b e guarda em c Fim se se (op=’/’ ) então 4 divide a por b e guarda em c fim se imprime c fim do algoritmo Cada um dos refinamentos pode ser construído em uma função. A implementação ficaria assim: float soma(float x,float y){ float r; r=(x+y); return r; } float subtrai(float x,float y){ return (x-y); } float multiplica(float x,float y){ return (x*y); } float divide(float x,float y){ return (x/y); } int main(int argc, char **argv) { float a,b,c; char op; printf("Digite os números:"); scanf("%f%f",&a,&b); 95 printf("Digite a operação:"); op=getch(); if (op=='+') c = soma(a,b); if (op=='-') c = subtrai(a,b); if (op=='*') c = multiplica(a,b); if (op=='/') c = divide(a,b); printf("%f", c); getch(); } O programador poderia pensar em construir uma função genérica para as operações. Nesse caso ele deverá informar para a função (através dos parâmetros) qual a operação que deve ser feita: float opera(float x,float y, char opcao){ switch(opcao){ case '+' : return(x+y); case '-' : return (x-y); case '*' : return (x*y); case '/': return (x/y); default: return 0.0; } } int main(int argc, char **argv) { float a,b,c; char op; printf("Digite os números:"); scanf("%f%f",&a,&b); printf("Digite a operação:"); op=getch(); c= opera(a,b,op); printf("%f", c); getch(); } Exercícios 1. determine para as funções de biblioteca da linguagem C abaixo em qual biblioteca se encontram, qual a sua finalidade, quais são os seus parâmetros e seus tipos, e qual o tipo de retorno da função: - strcmp - atoi - sqrt - atof - fread - ceil - fgets - floor - getc - strcat - getchar - pow 96 - sin cos fcvt - itoa tan 2. Quais parâmetros seriam necessários e qual o valor de retorno para a implementação das seguintes funções: a) Função que verifica se um número é menor que 10 b) Função que verifica se um caractere está entre os dígitos ‘0’ e ‘9’ c) Função que conta quantas vogais existem em uma string d) Função que calcula a média de um vetor contendo n números e) Função que calcula o fatorial de um número 3. Construa uma função maior que identifique e retorne o maior entre 3 números. Use a função em um programa que leia 3 números e imprima o maior dentre os números lidos. 4. Construa um programa que leia um número e verifique se o número é par ou impar. A função deve retornar 1 se o número for para e 0 se for impar. Use a função para construir um programa que leia 10 números e imprima apenas os números pares 5. Construa uma função que calcule o fatorial de um número. Use a função para calcular a soma dos 100 primeiros termos da série: 1/1! +1/2! +1/3!... 6. 4. Construa uma função para calcular a média final dos alunos. A média é dada a partir de 3 notas: n1,n2,n3, de acordo com as seguinte fórmula: 7. n1*0.3+n2*0.4+n3*0.3 Use a função acima em um programa que leia 20 conjuntos de 3 notas e calcule a média de cada conjunto. 97