Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas

Programação linguagem C: introdução, Manuais, Projetos, Pesquisas de Teoria dos Jogos

Aprendendo a programar: programando a linguagem C

Tipologia: Manuais, Projetos, Pesquisas

Antes de 2010
Em oferta
30 Pontos
Discount

Oferta por tempo limitado


Compartilhado em 14/06/2009

emerson-damasceno-de-oliveira-8
emerson-damasceno-de-oliveira-8 🇧🇷

5

(1)

18 documentos

1 / 139

Documentos relacionados


Pré-visualização parcial do texto

Baixe Programação linguagem C: introdução e outras Manuais, Projetos, Pesquisas em PDF para Teoria dos Jogos, somente na Docsity! Aprendendo a Programar Programando na Linguagem C Para Iniciantes Jaime Evaristo Terceira Edição Revisada/Ampliada Edição Digital (cópias autorizadas) Aprendendo a Programar Programando na Linguagem C Jaime Evaristo Professor Adjunto Instituto de Computação Universidade Federal de Alagoas 5.5 Passagem de parâmetros por referência no Turbo C 2.01....................................................... 73 5.6 Uma urna eletrônica.................................................................................................................73 5.7 Recursividade...........................................................................................................................75 5.8 Usando funções de outros arquivos......................................................................................... 79 5.9 "Tipos" de variáveis.................................................................................................................80 5.10 Uma aplicação à História da Matemática.............................................................................. 82 5.11 Exercícios propostos.............................................................................................................. 83 6 Vetores.............................................................................................................................................84 6.1 O que são vetores.....................................................................................................................84 6.2 Declaração de um vetor unidimensional..................................................................................84 6.3 Vetores e ponteiros.................................................................................................................. 85 6.4 Lendo e escrevendo um vetor.................................................................................................. 85 6.5 Exemplos Parte IV................................................................................................................... 86 6.6 Vetores multidimensionais...................................................................................................... 90 6.7 Exemplos Parte V.................................................................................................................... 92 6.8 Uma aplicação esportiva..........................................................................................................94 6.9 Exercícios propostos................................................................................................................ 95 7 Pesquisa e ordenação....................................................................................................................... 99 7.1 Introdução................................................................................................................................ 99 7.2 Pesquisa sequencial..................................................................................................................99 7.3 Pesquisa binária....................................................................................................................... 99 7.4 Ordenação.............................................................................................................................. 101 7.5 Exercícios propostos.............................................................................................................. 103 8. Cadeias de caracteres (strings)..................................................................................................... 104 8.1 Introdução.............................................................................................................................. 104 8.2 Funções de biblioteca para manipulação de cadeias de caracteres........................................105 8.3 Exemplos Parte VI................................................................................................................. 107 8.4 Exercícios propostos.............................................................................................................. 111 9 Estruturas e Arquivos.................................................................................................................... 113 9.1 O que são estruturas...............................................................................................................113 9.2 Exemplos Parte VII................................................................................................................114 9.3 O que são arquivos.................................................................................................................116 9.4 Arquivos de registros (Arquivos binários).............................................................................117 9.5 Arquivo texto......................................................................................................................... 126 9.6 Exercícios propostos.............................................................................................................. 130 10 Noções básicas de alocação dinâmica de memória .................................................................... 132 10.1 O que é alocação dinâmica.................................................................................................. 132 10.2 Armazenando dinamicamente um polinômio...................................................................... 133 10.3 Listas....................................................................................................................................134 10.4 Exercícios propostos............................................................................................................ 136 Bibliografia.......................................................................................................................................137 Índice remissivo................................................................................................................................138 1 Introdução à Programação 1.1 Organização básica de um computador Um computador é constituído de quatro unidades básicas: unidade de entrada, unidade de saída, unidade de processamento central e memória. Como indica sua denominação, uma unidade de entrada é um dispositivo que permite que o usuário interaja com o computador, fornecendo-lhe dados e informações que serão processadas, sendo o teclado o seu exemplo mais trivial. Uma unidade de saída, por seu turno, serve para que sejam fornecidos ao usuário do computador os resultados do processamento realizado. O monitor de vídeo e uma impressora são exemplos de unidades de saída. A unidade central de processamento é responsável por todo o processamento requerido, sendo muito conhecida por cpu, acrossemia de central processing unit. Já a memória armazena dados e informações que serão utilizados no processamento, armazenamento temporário, pois quando o computador é desligado tudo que está nela armazenado deixa de sê-lo (dizemos que toda a memória é "apagada"). 1.2 Linguagem de máquina Linguagens de comunicação Evidentemente, há a necessidade de que as unidades que compõem um computador se comuniquem umas com as outra. Por exemplo, um dado fornecido pelo teclado deve ser armazenado na memória; para a cpu realizar uma operação aritmética, ela vai “buscar” valores que estão armazenados na memória, e assim por diante. Para que haja comunicação entre as unidades do computador é necessário que se estabeleça uma linguagem. Os seres humanos se comunicam basicamente através de duas linguagens: a linguagem escrita e a fala. Uma comunicação através de uma linguagem escrita é constituída de parágrafos, os quais contêm períodos, que contêm frases, que são constituídas de palavras, sendo cada uma das palavras formadas por letras e esta sequência termina aí. Assim, uma letra é um ente indivisível da linguagem escrita e, em função disto, é chamada símbolo básico desta linguagem. Este exemplo foi apresentado para que se justifique a afirmação de que toda linguagem requer a existência de símbolos básicos, como - e para mais um exemplo - os fonemas para a linguagem falada. A linguagem de comunicação entre as unidades Como a comunicação entre as unidades do computador teria que ser obtida através de fenômenos físicos, os cientistas que conceberam os computadores atuais estabeleceram dois símbolos básicos para a linguagem. Esta quantidade de símbolos foi escolhida pelo fato de que através de fenômenos físicos é muito fácil obter dois estados distintos e não confundíveis, como passar corrente elétrica/não passar corrente elétrica, estar magnetizado/não estar magnetizado, etc., podendo cada um destes estados ser um dos símbolos. Assim a linguagem utilizada para comunicação interna num computador, chamada linguagem de máquina, possui apenas dois símbolos. Cada um destes símbolos é denominado bit (binary digit) e eles são representados por 0 (zero) e 1 (um). Esta forma de representar os bit's justifica a sua denominação: binary digit, que significa dígito binário (além disto, bit em inglês significa fragmento). Portanto, as palavras da linguagem de máquina são sequências de bits, ou seja, sequências de dígitos zero e um. O código ASCII Para que haja a possibilidade da comunicação do homem com o computador, é necessário que as palavras da linguagem escrita sejam traduzidas para a linguagem de máquina e vice-versa. Para que isto seja possível, é necessário que se estabeleça qual a sequência de bit's que corresponde a cada caractere usado na linguagem escrita. Ou seja, é necessário que se estabeleça uma codificação em sequência de bit's para cada um dos caracteres. Uma codificação muito utilizada é o código ASCII (American Standard Code for Information Interchange ou Código Padrão Americano para Intercâmbio de Informações), estabelecido pelo ANSI (American National Standards Institute). Nesta codificação, cada caractere é representado por uma sequência de oito bits (normalmente, um conjunto de oito bit's é chamado byte). Só para exemplificar (será visto ao longo do livro que, em geral, não há necessidade de que se conheça os códigos dos caracteres), apresentamos a tabela abaixo com os códigos ASCII de alguns caracteres. Tabela 1 Códigos ASCII de alguns caracteres Caractere Código ASCII Espaço em branco 00100000 ! 00100001 " 00100010 . . . . . . 0 00110000 1 00110001 . . . . . . A 01000001 B 01000010 . . . . . . Z 01011010 . . . . . . a 01100001 . . . . .. Observe a necessidade de se haver codificado o espaço em branco (este "caractere" é utilizado para separar nossas palavras) e de se haver codificado diferentemente as letras maiusculas e minúsculas, para que se possa considerá-las como coisas distintas. Levando em conta que cada sequência de zeros e uns pode ser vista como a representação de um número inteiro no sistema binário de numeração [Evaristo, J 2002], podemos, até para facilitar a sua manipulação, associar a cada código ASCII o inteiro correspondente, obtendo assim o que se costuma chamar de código ASCII decimal. Por exemplo, como 1000001 é a representação do número (decimal) 65 no sistema binário de numeração, dizemos que o código ASCII decimal de A é 65. 1.3 Programas de computadores Para que um computador tenha alguma utilidade, ele deve executar um programa que tenha uma finalidade específica. Games são programas que têm como objetivo propiciar entretenimento aos seus usuários. Processadores de texto são programas que permitem que textos sejam digitados, impressos e armazenados para futuras modificações ou impressões. Planilhas eletrônicas são programas que oferecem recursos para manipulação de tabelas de valores numéricos. Navegadores permitem acessos a páginas da internet, a rede mundial de computadores. Estes programas destinam-se a usuários finais, aquelas pessoas que vão utilizar o computador com um determinado objetivo específico, usando para tal um programa que ela aprendeu a usar, não tendo nenhuma preocupação relativa ao funcionamento interno do sistema computador/programa. Por exemplo, um usuário de um processador de texto deve aprender o que fazer para que o processador destaque em negrito alguma parte do texto ou localize uma palavra, não havendo necessidade de saber como o programa realiza estas ações. Na verdade, para que um processador de texto propicie ao usuário a possibilidade de que textos sejam digitados, corrigidos, gravados, inseridos em outros textos e de que palavras sejam localizadas dentro de um 5. (A, 1), (B, 0). 6. (A, 0), (B, 1). 7. (A, 3), (B, 1). 8. (A, 0), (B, 4). Outras questões que podem ser levantadas são: há outras soluções? Existe alguma solução que realize a mesma tarefa com uma quantidade menor de instruções? Para responder a estas questões talvez seja interessante lembrar que 4 = 5 – 1. Significa que, se conseguirmos tirar 1 litro do recipiente de 5 litros quando ele estiver cheio, resolveremos a questão. Para conseguir isto, basta que o recipiente de 3 litros contenha 2 litros. E para se obter 2 litros? Aí basta ver que 2 = 5 – 3. Podemos então resolver a questão com o seguinte algoritmo, constituído de apenas seis instruções: 1. Encha o recipiente de 5 litros. 2. Com o conteúdo do recipiente de 5 litros, encha o de 3 litros. 3. Esvazie o recipiente de 3 litros. 4. Transfira o conteúdo do recipiente de 5 litros para o recipiente de 3 litros. 5. Encha o recipiente de 5 litros. 6. Com o conteúdo do recipiente de 5 litros, complete o recipiente de 3 litros. Após a execução de cada uma das instruções teremos: 1. (A, 0), (B, 5). 2. (A, 3), (B, 2). 3. (A, 0), (B, 2). 4. (A, 2), (B, 0). 5. (A, 2), (B, 5). 6. (A, 3), (B, 4). Uma outra técnica bastante utilizada é se tentar raciocinar a partir de uma solução conhecida de uma outra questão. Para compreender isto considere as duas seguintes questões: imagine uma relação de n números, os quais podem ser referenciados por ai com i = 1, 2, ..., n e queiramos somá-los com a restrição de que só sabemos efetuar somas de duas parcelas. Para resolver esta questão, podemos pensar em casos particulares: se n = 2, basta somar os dois números; se n = 3, basta somar os dois primeiros e somar esta soma com o terceiro. Naturalmente este raciocínio pode ser reproduzido para n > 3. A questão é que a soma dos dois primeiros deve estar "guardada" para que se possa somá-la com o terceiro, obtendo-se a soma dos três primeiros; esta soma deve ser "guardada" para que seja somada com o quarto e assim sucessivamente. Para isto podemos estabelecer uma referência à soma "atual", a qual será alterada quando a soma com o elemento seguinte for efetuada. Até para somar os dois primeiros, pode-se pensar em somar "a soma do primeiro" com o segundo. Temos então o seguinte algoritmo: 1. Faça i = 1. 2. Faça Soma = a1. 3. Repita n – 1 vezes as instruções 3.1 e 3.2. 3.1. Substitua i por i + 1. 3.2. Substitua Soma por Soma + ai. Por exemplo: se n = 5 e a1 = 8, a2 = 4, a3 = 9, a4 = 13 e a5 = 7, a execução do algoritmo resultaria nas seguintes ações: 1. i = 1. 2. Soma = 8. 3.1.1. i = 2. 3.2.1. Soma = 8 + 4 = 12 3.1.2. i = 3. 3.2.2. Soma = 12 + 9 = 21. 3.1.3. i = 4. 3.2.3. Soma = 21 + 13 = 34. 3.1.4. i = 5. 3.2.4. Soma = 34 + 7 = 41. Naturalmente, na execução acima estamos indicando por 3.1.x e 3.2.x a execução de ordem x das instruções 3.1 e 3.2. Como veremos ao longo do livro, este algoritmo é bastante utilizado em programação, sendo mais comum até o primeiro termo da relação ser "somado" dentro da repetição. Neste caso, para que o primeiro seja somado, é necessário que Soma seja inicializado com 0 (zero), ficando assim o algoritmo: 1. Faça i = 0. 2. Faça Soma = 0. 3. Repita n vezes as instruções 3.1 e 3.2. 3.1. Substitua i por i + 1. 3.2. Substitua Soma por Soma + ai. Conhecendo este algoritmo, é fácil então resolver a questão de se calcular o produto de n números nas mesmas condições, e aí vemos como utilizar uma solução conhecida para resolver um problema. Deve-se inicializar uma referência Produto com 1 e, numa repetição, multiplicar os números como foi feito no caso da soma: 1. Faça i = 0. 2. Faça Produto = 1. 3. Repita n vezes as instruções 3.1 e 3.2. 3.1. Substitua i por i + 1. 3.2. Substitua Produto por Produto x ai. 1.6 Processador de um algoritmo Obviamente, um algoritmo deve ser executado por algum agente. Este agente pode ser uma pessoa munida de certos equipamentos e utensílios ou por máquinas projetadas para executar automaticamente algumas instruções básicas. O algoritmo para a travessia do senhor gordo com as galinhas, sua raposa e seu saco de milho seria executado pelo tal senhor, que estava para tal munido do barco e de remos. O algoritmo para obtenção de quatro litros de água a partir de recipientes de conteúdos cinco litros e três litros poderia ser executado por uma pessoa que dispusesse dos dois recipientes e de água em abundância. Neste último caso, quem sabe, a pessoa poderia ser substituída por um robô. O agente que executa um algoritmo é chamado processador e é evidente que para que o algoritmo seja executado é necessário que o processador seja capaz de executar cada uma das suas instruções. Se o senhor gordo não souber remar ele não será capaz de atravessar o rio. Uma pessoa que não seja capaz de esvaziar um recipiente que pese cinco quilos não será capaz de executar o algoritmo dos quatro litros de água. Alguns autores de livros com objetivos idênticos a este - facilitar a aprendizagem da programação de computadores - iniciam seus textos discorrendo exclusivamente sobre resolução de problemas, encarando o processador como uma "caixa preta" que recebe as instruções formuladas pelo algoritmo e fornece a solução do problema, não levando em conta o processador quando da formulação do tal algoritmo. Entendemos que esta não é a melhor abordagem, visto que o conhecimento do que o processador pode executar pode ser definidor na elaboração do algoritmo. Por exemplo: imagine que queiramos elaborar um algoritmo para extrair o algarismo da casa das unidades de um inteiro dado (apresentaremos posteriormente uma questão bastante prática cuja solução depende deste algoritmo). Evidentemente, o algoritmo para resolver esta “grande” questão depende do processador que vai executá-lo. Se o processador for um ser humano que saiba o que é número inteiro, algarismo e casa das unidades, o algoritmo teria uma única instrução: 1. Forneça o algarismo das unidades do inteiro dado. Porém, se o processador for um ser humano que saiba o que é número inteiro e algarismo, mas não saiba o que é casa das unidades, o algoritmo não poderia ser mais esse. Neste caso, para resolver a questão, o processador deveria conhecer mais alguma coisa, como, por exemplo, ter a noção de "mais à direita", ficando o algoritmo agora como: 1. Forneça o algarismo "mais à direita" do número dado. E se o processador é uma máquina e não sabe o que é algarismo, casa das unidades, "mais à direita", etc.? Nesta hipótese, quem está elaborando o algoritmo deveria conhecer que instruções o processador é capaz de executar para poder escrever o seu algoritmo. Por exemplo, se a máquina é capaz de determinar o resto de uma divisão inteira, o algoritmo poderia ser: 1. Chame de n o inteiro dado; 2. Calcule o resto da divisão de n por 10; 3. Forneça este resto como o algarismo pedido. Algumas das questões anteriores são importantes para se desenvolver o raciocínio, mas não é este tipo de questão que se pretende discutir ao longo deste livro. Estamos interessados em algoritmos para: 1. Resolver problemas matemáticos, como algoritmos para determinar a média aritmética de vários números dados, determinar as raízes de uma equação do segundo grau, encontrar o máximo divisor comum de dois números dados, totalizar as colunas de uma tabela, etc. 2. Resolver questões genéricas, como algoritmos para colocar em ordem alfabética uma relação de nomes de pessoas, atualizar o saldo de uma conta bancária na qual se fez um depósito, corrigir provas de um teste de múltipla escolha, cadastrar um novo usuário de uma locadora, etc.. Na linguagem coloquial, o algoritmo para o cálculo da média pode ser escrito de forma muito simples: 1. Determine a quantidade de números; 2. Some os números dados; 3. Divida esta soma pela quantidade de números. Qualquer pessoa que saiba contar, somar e dividir números é capaz de executar este algoritmo dispondo apenas de lápis e papel. A questão que se põe é: e se a relação contiver 13.426 números? A tal pessoa é capaz de executar, porém, quanto tempo levará para fazê-lo? Um outro aspecto a ser observado é que nem sempre a linguagem coloquial é eficiente para se escreverem as instruções. Nessa linguagem o algoritmo para determinação das raízes de uma equação do segundo grau teria uma instrução difícil de escrever e difícil de compreender como: n. Subtraia do quadrado do segundo coeficiente o produto do número quatro pelo produto dos dois outros coeficientes. Isto pode ser parcialmente resolvido utilizando-se uma linguagem próxima da linguagem matemática que já foi utilizada em exemplos da seção anterior. No caso da equação do segundo grau teríamos o seguinte algoritmo, que nos é ensinado nas últimas séries do ensino fundamental: 1. Chame de a, b e c os coeficientes da equação. 2. Calcule d = b² - 4ac. 3. Se d < 0 forneça como resposta a mensagem: A equação não possui raízes reais. 4. Se d ≥ 0 4.1 Calcule x1 = (-b + raiz(d))/2a e x2 = (-b - raiz(d))/2a. 4.2 Forneça x1 e x2 como raízes da equação. De maneira mais ou menos evidente, raiz(d) está representando a raiz quadrada de d e a execução deste algoritmo requer que o processador seja capaz de determinar valores de expressões aritméticas, calcular raízes quadradas, efetuar comparações e que conheça a linguagem matemática. Algoritmos para problemas genéricos são mais complicados e as linguagens utilizadas anteriormente não são adequadas (para o caso da ordenação de uma relação de nomes, foram desenvolvidos vários algoritmos e teremos oportunidade de discutir alguns deles ao longo deste livro). 1.7 Exemplos de algoritmos matemáticos Para uma primeira discussão em termos de aprendizagem de desenvolvimento de algoritmos e utilizando a linguagem usada no exemplo da equação do segundo grau, apresentamos a seguir alguns exemplos de algoritmos que objetivam a solução de questões da matemática. Para eles supomos que o processador seja capaz de efetuar somas, subtrações e divisões decimais, de realizar comparações, de repetir a execução de um conjunto de instruções um número determinado de vezes ou enquanto uma condição seja atendida. 1. No exemplo do algoritmo para obtenção do algarismo da casa das unidades de um inteiro dado supomos que o processador seria capaz de calcular o resto de uma divisão inteira. Observando que não está suposto que o nosso processador seja capaz de determinar restos de divisões inteiras, vamos discutir um aluno do ensino médio e a relação contiver poucos números, uma simples olhada na relação permitirá se identificar o maior número. Mas, e se o processador for um aluno das classes iniciais do ensino fundamental? E se a relação contiver 10 000 números? E se os números estiverem escritos em forma de fração ordinária? Uma solução possível é supor que o maior número é o primeiro da relação e comparar este suposto maior com os demais números, alterando-o quando for encontrado um número na relação maior do que aquele que até aquele momento era o maior. 1. Chame de A o primeiro número dado. 2. Faça M = A. 3. Repita 9 999 vezes as instruções 3.1 e 3.2. 3.1 Chame de A o próximo número dado. 3.2 Se A > M substitua o valor de M por A. 4. Forneça M para o valor do maior número. Para exemplificar, suponha que a entrada fosse o conjunto {5, 3, 8, 11, 10...}. Até a quinta execução das instruções 3.1 e 3.2 teríamos a seguinte tabela: A M 5 5 3 8 8 11 11 10 1.8 Linguagens de alto nível Computadores digitais foram concebidos para executarem instruções escritas em linguagem de máquina. Isto significa que um computador é capaz de executar um algoritmo escrito nesta linguagem. Um algoritmo escrito em linguagem de máquina é normalmente chamado de programa objeto. Nos primórdios da computação, os algoritmos que se pretendiam que fossem executados por um computador eram escritos em linguagem de máquina, o que tornava a tarefa de desenvolvimento de algoritmos muito trabalhosa, devido ao fato de que era necessário que se conhecesse qual sequência de bits correspondia à instrução pretendida. Naturalmente, esta dificuldade acontecia pelo fato de que o ser humano não está habituado a uma linguagem com apenas dois símbolos básicos. Um grande avanço ocorreu na computação quando se conseguiu desenvolver programas que traduzissem instruções escritas originariamente numa linguagem dos seres humanos para a linguagem de máquina. O surgimento de programas para esta finalidade permitiu o desenvolvimento de algoritmos em linguagens que utilizam caracteres, palavras e expressões de um idioma, ou seja, uma linguagem cujos símbolos básicos e cujas palavras estão no cotidiano do ser humano. Uma linguagem com esta característica é chamada linguagem de alto nível, onde alto nível aí não se refere à qualidade e sim ao fato de que ela está mais próxima da linguagem do ser humano do que da linguagem da máquina (quando alguma coisa está mais próxima da máquina do que do ser humano dizemos que ela é de baixo nível). Como exemplos de linguagens de alto nível temos Pascal, C, Delphi, Visual Basic, Java e C++. Um algoritmo escrito numa linguagem de alto nível é chamado programa fonte ou simplesmente programa Como foi dito acima, um programa fonte deve ser traduzido para a linguagem de máquina. Há dois tipos de programas que fazem isto: os interpretadores que traduzem os comandos para a linguagem de máquina um a um e os compiladores que traduzem todo o programa para a linguagem de máquina. Um compilador ao receber como entrada um programa fonte fornece como saída um programa escrito em linguagem de máquina, chamado programa objeto. A compilação do programa, portanto, gera um programa que pode então ser executado. É comum nos referirmos à execução do programa fonte quando se está executando o programa objeto. Já um interpretador traduz para a linguagem de máquina os comandos do programa um a um, executando-os em seguida. Assim a interpretação de um programa não gera um programa objeto. 1.9 Sintaxe e semântica de uma instrução O que é sintaxe Dissemos que um programa escrito em linguagem de alto nível é traduzido para a linguagem de máquina por um compilador ou cada instrução é traduzida por um interpretador. É natural se admitir que, para que o compilador consiga traduzir uma instrução escrita com caracteres de algum idioma para instruções escritas como sequências de zeros e uns, é necessário que cada instrução seja escrita de acordo com regras preestabelecidas. Estas regras são chamadas sintaxe da instrução e quando não são obedecidas dizemos que existe erro de sintaxe. Se o programa fonte contém algum erro de sintaxe, o compilador não o traduz para a linguagem de máquina (isto é, o compilador não compila o programa) e indica qual o tipo de erro cometido e a instrução onde este erro aconteceu. Se o programa fonte for interpretado, ele é executado até a instrução que contém o erro, quando então é interrompida a sua execução e o tal erro é indicado. O que é semântica Naturalmente, cada instrução tem uma finalidade específica. Ou seja, a execução de um instrução resulta na realização de alguma ação, digamos parcial, e é a sequência das ações parciais que redunda na realização da tarefa para a qual o programa foi escrito. A ação resultante da execução de uma instrução é chamada semântica da instrução. Infelizmente, um programa pode não conter erros de sintaxe (e, portanto, pode ser executado), mas a sua execução não fornecer como saída o resultado esperado para alguma entrada. Neste caso, dizemos que o programa contém erros de lógica que, ao contrário dos erros de sintaxe que são detectados pelo compilador ou pelo interpretador, são, às vezes, de difícil detecção. No nosso entendimento, para aprender a programar numa determinada linguagem é necessário que se aprenda as instruções daquela linguagem (para que se conheça o que o processador é capaz de fazer), a sintaxe de cada um destes instruções e as suas semânticas. Aliado a isto, deve-se ter um bom desenvolvimento de lógica programação para que se escolha as instruções necessárias e a sequência segundo a qual estas instruções devem ser escritas, para que o programa, ao ser executado, execute a tarefa pretendida. Felizmente ou infelizmente, para cada tarefa que se pretende não existe apenas uma sequência de instruções que a realize. Ou seja, dado um problema não existe apenas um programa que o resolva. Devemos procurar o melhor programa, entendendo-se como melhor programa um programa que tenha boa legibilidade, cuja execução demande o menor tempo possível e que necessite, para sua execução, a utilização mínima da memória. Existe um conjunto de instruções que é comum a todas as linguagens de alto nível e cujas semânticas permitem executar a maioria das tarefas. A aprendizagem das semânticas destas instruções e das suas sintaxes em alguma linguagem de programação (aliado ao desenvolvimento da lógica de programação) permite que se aprenda com facilidade outra linguagem do mesmo paradigma. 1.10 Sistemas de computação Como foi dito anteriormente, a cpu de um computador é capaz de executar instruções (escritas em linguagem de máquina, permitam a repetição). Ou seja, um computador é capaz de executar programas e só para isto é que ele serve. Se um computador não estiver executando um programa ele para nada está servindo. Como foram concebidos os computadores atuais, um programa para ser executado deve estar armazenado na sua memória. O armazenamento dos programas (e todo o gerenciamento das interações entre as diversas unidades do computador) é feito por um programa chamado sistema operacional. Um dos primeiros sistemas operacionais para gerenciamento de microcomputadores foi o DOS (Disk Operating System). Quando um computador é ligado, de imediato o sistema operacional é armazenado na memória e só a partir daí o computador está apto a executar outros programas. Estes programas podem ser um game, que transforma o "computador" num poderoso veículo de entretenimento; podem ser um processador de texto, que transforma o "computador" num poderoso veículo de edição de textos; podem ser uma planilha eletrônica, que transforma o "computador" num poderoso veículo para manipulação de tabelas numéricas, podem ser programas para gerenciar, por exemplo, o dia a dia comercial de uma farmácia e podem ser ambientes que permitam o desenvolvimento de games ou de programas para gerenciar o dia a dia comercial de uma farmácia. Talvez com exceção de um game, os programas citados acima são, na verdade, conjuntos de programas que podem ser executados de forma integrada. Um conjunto de programas que podem ser executados de forma integrada é chamado software. Por seu turno, as unidades do computador, associadas a outros equipamentos chamados periféricos, como uma impressora, constituem o hardware. O que nos é útil é um conjunto software + hardware. Um conjunto deste tipo é chamado de um sistema de computação. De agora em diante, os nossos processadores serão sistemas de computação. Isto é, queremos escrever programas que sejam executado por um sistema de computação. Como foi dito acima, o desenvolvimento de um programa que gerencie o dia a dia comercial de uma farmácia requer um compilador (ou um interpretador) que o traduza para a linguagem de máquina. Antigamente, as empresas que desenvolviam compiladores desenvolviam apenas estes programas, de tal sorte que o programador necessitava utilizar um processador de texto à parte para edição do programa fonte. Atualmente, os compiladores são integrados num sistema de computação que contém, entre outros: 1. Processador de texto, para a digitação dos programas fontes; 2. Depurador, que permite que o programa seja executado comando a comando, o que facilita a descoberta de erros de lógica; 3. Help, que descreve as sintaxes e as semânticas de todas as instruções da linguagem; 4. Linker, que permite que um programa utilize outros programas. Rigorosamente falando, um sistema constituído de um compilador e os softwares listados acima deveria ser chamado de ambiente de programação; é mais comum, entretanto, chamá-lo, simplesmente, de compilador. Os ambientes de programação que utilizamos para desenvolver os programas deste livro foram o compilador Turbo C, versão 2.01, e Turbo C++, versão 3.0, ambos desenvolvidos pela Borland International, Inc., o primeiro em 1988 e o segundo em 1992. Como se pode ver, são sistemas desenvolvidos há bastante tempo (as coisas em computação andam muito mais rápido), já estando disponíveis gratuitamente na internet. Estaremos, portanto, utilizando um compilador “puro C” e um compilador C++, que é up grade da linguagem C para a programação orientada a objeto, paradigma que não está no escopo deste livro. 1.11 Exercícios propostos 1. Três índios, conduzindo três brancos, precisam atravessar um rio dispondo para tal de um barco cuja capacidade é de apenas duas pessoas. Por questões de segurança, os índios não querem ficar em minoria, em nenhum momento e em nenhuma das margens. Escreva um algoritmo que oriente os índios para realizarem a travessia nas condições fixadas. (Cabe observar que, usualmente, este exercício é enunciado envolvendo três jesuítas e três canibais. A alteração feita é uma modesta contribuição para o resgate da verdadeira história dos índios). 2. O jogo conhecido como Torre de Hanói consiste de três torres chamadas origem, destino e auxiliar e um conjunto de n discos de diâmetros diferentes, colocados na torre origem na ordem decrescente dos seus diâmetros. O objetivo do jogo é, movendo um único disco de cada vez e não podendo colocar um disco sobre outro de diâmetro menor, transportar todos os discos para torre destino, podendo usar a torre auxiliar como passagem intermediária dos discos. Escreva algoritmos para este jogo nos casos n = 2 e n = 3. 3. Imagine que se disponha de três esferas numeradas 1, 2 e 3 iguais na forma, duas delas com pesos iguais e diferentes do peso da outra. Escreva um algoritmo que, com duas pesagens numa balança de dois pratos, determine a esfera de peso diferente e a relação entre seu peso e o peso das esferas de pesos iguais. 4. A média geométrica de n números positivos é a raiz n-ésima do produto destes números. Supondo que o processador é capaz de calcular raízes n-ésimas, escreva um algoritmo para determinar a média geométrica de n números dados. 5. Sabendo que o dia 01/01/1900 foi uma segunda-feira, escreva um algoritmo que determine o dia da semana correspondente a uma data, posterior a 01/01/1900, dada. Por exemplo, se a data dada for 23/01/1900, o algoritmo deve fornecer como resposta terça-feira. O tipo de dado O tipo de dado associado a uma variável é o conjunto dos valores que podem ser nela armazenados. A linguagem C dispõe dos tipos de dados discriminados na tabela a seguir. Tabela 3 Tipos de dados da Linguagem C Denominação Número de Bytes Conjunto de valores char 1 caracteres codificados no código ASCII int 2 números inteiros de –32768 a 32767 long ou long int 4 números inteiros de –65536 a 65535 float 4 números reais de –3,4x1038 a –3,4x10-38 e 3,4x10-38 a 3,4x1038 double 8 números reais de –1,7x10308 a -1,7x10-308 e 1,7x10-308 a 1,7x10308 void 0 conjunto vazio A utilização void será melhor explicada no capítulo 5, quando estudarmos funções. Uma observação importante é que os tipos float e double, rigorosamente falando, não armazenam números reais e sim números de um sistema de ponto flutuante, que não contém todos os reais entre dois números reais dados. O estudo de sistemas de ponto flutuante foge ao escopo deste livro e é feito, normalmente, em disciplinas do tipo Organização e Arquitetura de Computadores e Cálculo Numérico. Vale lembrar que, de um modo geral, um byte contém oito bit's e cabe ressaltar que, em algumas situações, é importante se conhecer a quantidade necessária de bytes para uma variável de um determinado tipo. Declaração de variáveis Para que o sistema de computação possa reservar as posições de memória que serão utilizadas pelo programa, associar identificadores aos endereços destas posições de memória e definir a quantidade de bytes de cada posição de acordo com o tipo de dado pretendido, um programa escrito em C deve conter a declaração de variáveis, feita através da seguinte sintaxe: Tipo de dado Lista de identificadores; Por exemplo, um programa para determinar a média de uma relação de números dados pode ter a seguinte declaração: int Quant; float Num, Soma, Media; A ideia é que Quant seja utilizada para armazenar a quantidade de números; Num para armazenar os números (um de cada vez); Soma para armazenar a soma dos números; e Media para armazenar a média procurada. Nas seções 2.7 e 2.9 veremos as instruções em C para o armazenamento em variáveis de dados de entrada e de dados gerados pela execução do algoritmo. Um valor armazenado em uma variável é comumente referido como sendo o conteúdo da variável ou o valor da variável. Também é comum se referir ao identificador da variável como sendo a própria variável. 2.2 Constantes Como uma variável, uma constante também é uma posição de memória à qual devem ser associados um identificador e um tipo de dado. O que caracteriza uma constante (e daí sua denominação, emprestada da matemática) é o fato de que o conteúdo de uma constante não pode ser modificado durante a execução do programa. Este conteúdo é fixado quando da declaração da constante o que deve ser feito de acordo com a seguinte sintaxe: const Tipo de Dado Identificador = Valor; Por exemplo, um programa para processar cálculos químicos poderia ter uma declaração do tipo const float NumAvogadro = 6.023E+23; onde a expressão 6.023E+23 é a forma que os compiladores C utilizam para representar os valores do tipo float na notação científica, ou seja 6.023E+23 = 6.023 x 1023. Um programa para cálculos de áreas de figuras planas, perímetros de polígonos inscritos em circunferências, etc., poderia ter uma declaração do tipo const float Pi = 3.1416; Esta declaração é desnecessária quando o sistema utilizado é o Turbo C++ 3.0, pois esse sistema disponibiliza uma constante pré-definida, identificada por M_PI, que pode ser utilizada em qualquer parte do programa e cujo valor é uma aproximação do número irracional π. 2.3 Expressões aritméticas Como era de se esperar, os compiladores da linguagem C são capazes de avaliar expressões aritméticas que envolvam as operações binárias de multiplicação, divisão, soma e subtração e a operação unária de troca de sinal. Para isto são usados os seguintes operadores aritméticos binários: Tabela 4 Operadores aritméticos Operador Operação + adição - subtração * multiplicação / divisão e o operador aritmético unário (-) para a troca de sinal. Esses operadores atuam com operandos do tipo int ou do tipo float. Se um dos operandos for do tipo float o resultado da operação será do tipo float; se os dois operandos forem do tipo int o resultado é também do tipo int. No caso do operador de divisão /, se os dois operandos forem do tipo int o resultado da operação é do tipo int e igual ao quociente da divisão do primeiro operando pelo segundo. Por exemplo, o resultado de 30/4 é 7. Se quisermos a divisão decimal teremos de escrever 30.0 / 7.0 ou 30.0 / 7 ou 30 / 7.0. Ou seja cada uma destas divisões é igual a 7.5 e este valor, tal como ele é, pode ser armazenado numa variável do tipo float. O que acontece é que no armazenamento de um valor do tipo float numa variável do tipo int a parte decimal do valor é desprezada, só sendo armazenada a parte inteira do número. Uma expressão que envolva diversas operações é avaliada de acordo com as regras de prioridade da matemática: em primeiro lugar é realizada a operação troca de sinal, em seguida são realizadas as multiplicações e divisões e, finalmente, as somas e subtrações. Por exemplo, a expressão 8 + 2*-3 é avaliada como 8 + (-6) = 2. Naturalmente, a prioridade pode ser alterada com a utilização de parênteses: a expressão (8 + 2)*-3 resulta em 10*(-3) = -30. Embora, o sistema não exija, vamos sempre utilizar parênteses para separar o operador unário para troca de sinal de algum operador binário. Assim, 8 + 2*-3 será indicada por 8 + 2*(-3). Uma expressão não parentesada contendo operadores de mesma prioridade é avaliada da esquerda para direita. Por exemplo, 10/2*3 é igual a (10/2)*3 = 5*3 = 15. Operandos podem ser conteúdos de variáveis. Neste caso, o operando é indicado pelo identificador da variável (é para isto que serve o identificador, para se fazer referência aos valores que na variável estão armazenados). Além dos operadores aritméticos usuais, os compiladores C disponibilizam o operador módulo, indicado por %, que calcula o resto da divisão do primeiro operando pelo segundo. Por exemplo, 30 % 4 = 2 e 5 % 7 = 5. Este operador atua apenas em operandos do tipo int, resultando um valor deste mesmo tipo. Por exemplo, se S é uma variável do tipo float, a expressão S % 5 gerará um erro de compilação. Uma expressão do tipo 30.0 % 7 também gerará erro de compilação, pelo fato de que um dos operandos não é inteiro. Este erro é indicado pelo sistema pela mensagem Illegal use of floating point in function ... (Uso ilegal de tipo float na função ...), onde as reticências estão substituindo o identificador da função, como será discutido posteriormente. 2.4 Relações Os ambientes que implementam a linguagem C efetuam comparações entre valores numéricos, realizadas no sentido usual da matemática. Essas comparações são chamadas relações e são obtidas através dos operadores relacionais > (maior do que), >= (maior do que ou igual a), < (menor do que), <= (menor do que ou igual a), == (igual) e != (diferente). O resultado da avaliação de uma relação é 1 (um), se a relação for matematicamente verdadeira, ou 0 (zero), se a relação for matematicamente falsa. Assim, 3 > 5 resulta no valor 0 (zero), enquanto que 7 <= 7 resulta no valor 1 (um). Sendo um valor 1 (um) ou 0 (zero), o resultado da avaliação de uma relação pode ser armazenado numa variável do tipo int. Os operandos de uma relação podem ser expressões aritméticas. Nestes casos, as expressões aritméticas são avaliadas em primeiro lugar para, em seguida, ser avaliada a relação. Por exemplo, a relação 3*4 - 5 < 2*3 - 4 resulta no valor 0 (zero), pois 3*4 - 5 = 7 e 2*3 - 4 = 2. Isto significa que os operadores relacionais têm prioridade mais baixa que os aritméticos. 2.5 Expressões lógicas Os compiladores C também avaliam expressões lógicas obtidas através da aplicação dos operadores lógicos binários &&, || e ^ a duas relações ou da aplicação do operador lógico unário ! a uma relação. Se R1 e R2 são duas relações, a avaliação da aplicação dos operadores lógicos binários, de acordo com os valores de R1 e R2, são dados na tabela abaixo. Tabela 5 Avaliação de expressões lógicas R1 R2 (R1)&&(R2) (R1)||(R2) (R1) ^ (R2) 1 1 1 1 0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 0 Ou seja, uma expressão lógica do tipo (R1)&&(R2) só recebe o valor 1 (um) se os valores de R1 e de R2 forem iguais a 1 (um); uma expressão lógica do tipo (R1)||(R2) só recebe o valor 0 (zero) se os valores de R1 e de R2 forem iguais a 0 (zero); uma expressão lógica do tipo (R1) ^ (R2) só recebe o valor 1 (um) se apenas um dos valores de R1 e R2 for igual a 1. O leitor já deve ter percebido que o operador && age como o conectivo e da nossa linguagem; o operador || atua como o nosso e/ou e o operador ^ como o conectivo ou. A aplicação do operador unário ! simplesmente inverte o valor original da relação: Tabela 6 Operador unário ! R1 !R1 1 0 0 1 Considerando que os operadores &&, || e ^ possuem o mesmo grau de prioridade, se uma expressão não parentesada possuir mais de uma relação, ela será avaliada da esquerda para direita. O operador unário ! tem prioridade em relação aos operadores binários. Assim, ! (5 > 3) || (5 < 3) tem valor 0 (zero), pois ! (5 > 3) é uma relação falsa e 5 < 3 também é. Considerando que os operadores lógicos têm prioridade mais baixa que os operadores relacionais, os parênteses nas expressões acima são desnecessários; porém entendemos que a colocação deles facilita a leitura da expressão. Os sistemas C 2.01 e C++ 3.0 também disponibilizam os operadores lógicos & e | cujas aplicações são idênticas às aplicações de && e ||, respectivamente. A diferença entre & e &&, por exemplo, é a seguinte: se em (R1) && (R2) o valor de R1 for 0 (zero) o valor R2 não é mais avaliado, enquanto que em (R1) & (R2) o valor de R2 é avaliado, independentemente do valor de R1. onde as digitações do caractere que se pretende armazenar na variável c e do inteiro a ser armazenado em i devem ser separadas pelo acionamento da tecla <enter> ou da <barra de espaço>. É necessário notar que a digitação de um valor de um tipo diferente do tipo da variável não provoca erro de execução, mas, evidentemente, pode provocar erro de lógica do programa. Por exemplo, se na execução do comando scanf("%c %d", &c, &i); digitarmos w<enter>5.9<enter>, o caractere w é armazenado na variável c e o inteiro 5 é armazenado na variável i. Portanto, se o dado de entrada fosse realmente 5.9, o resultado do processamento poderia ser fornecido com erros. Se o dado de entrada poderia ser 5.9, a variável para seu armazenamento deveria ter sido definida com float. Os códigos de conversão e a instrução #include <stdio.h> Os códigos de conversão de acordo com o tipo de dado da variável onde os valores digitados serão armazenados são apresentados na tabela a seguir. Tabela 7 Códigos de conversão da função scanf() Código Elemento armazenado %c um único caractere %d ou %i um inteiro do sistema decimal %o um inteiro do sistema octal %x um inteiro do sistema hexadecimal %ld um valor do tipo long %e um número na notação científica %f um número em ponto flutuante %s uma cadeia de caracteres A instrução #include <stdio.h> que precede a função main() é necessária pelos seguintes fatos. Como dissemos acima, para se definir uma função é necessário fixar o tipo de dado que ela retorna, o identificador da função e a lista de parâmetros, com seus identificadores e seus tipos de dados; este conjunto de elementos é chamado protótipo da função. Para que a função main() ative uma outra função (seja uma função definida pelo usuário ou uma função de biblioteca), o seu protótipo deve ser definido antes ou no interior da função main(). Os protótipos das funções do sistema encontram-se reunidos, de acordo com objetivos semelhantes, em arquivos chamados arquivos de cabeçalhos (header files) (o cabeçalho de uma função inclui o seu protótipo, as variáveis declaradas dentro da função e outras declarações e definições que não são instruções propriamente ditas). A instrução #include <stdio.h> "anexa" à função main() os protótipos das funções de biblioteca que executam ações padrões de entrada e de saída (stdio vem de standard input output, entrada e saída padrão e h é a extensão padrão dos arquivos de cabeçalhos). A não inclusão de um include provoca erro de compilação no sistema C++ 3.01. Isto não acontece no C 2.01, porém, há casos em que esta não inclusão gera erros de lógica (a entrada de dados não é feita do modo que se esperava). 2.8 Saída de dados A função printf() A exibição dos resultados do processamento e de mensagens é feita através da função pré-definida printf(), cujo protótipo está contido também no arquivo stdio.h. Sua sintaxe é a seguinte: printf(Expressão de controle, Lista de argumentos); onde Expressão de controle contém mensagens que se pretende que sejam exibidas, códigos de formatação (idênticos aos códigos de conversão da função scanf()) que indicam como o conteúdo de uma variável deve ser exibido e códigos especiais para a exibição de alguns caracteres especiais e realização de ações que permitam formatar a saída do programa. A Lista de argumentos pode conter identificadores de variáveis, expressões aritméticas ou lógicas e valores constantes. No primeiro caso, o conteúdo da variável é exibido; no segundo caso, a expressão é avaliada e o seu resultado é exibido; no terceiro caso o valor constante é exibido. A ordem de exibição dos conteúdos de variáveis, dos resultados das expressões e dos valores constantes relacionados na lista de argumentos é dada pela ordem em que estes elementos estão listados; a posição dentro da mensagem contida na expressão de controle é fixada pela posição do código de formatação respectivo. Quando, na expressão de controle, um código de formatação é encontrado o conteúdo da variável, o resultado da expressão ou o valor constante respectivo (no sentido da ordem da colocação da variável na lista e da colocação do código de formatação na expressão de controle) é exibido. Por exemplo, a função printf() no programa abaixo contém uma expressão de controle que não possui códigos de formatação. Isto significa que apenas a mensagem será exibida. Assim, o programa #include <stdio.h> main() { printf("Estou aprendendo a programar em C"); } é um programa em C que faz com que seja exibida na tela a mensagem Estou aprendendo a programar em C. Já o programa abaixo, contém uma função printf() que possui quatro caracteres de controle #include <stdio.h> main() { float a, b, c; scanf("%f %f %f", &a, &b, &c); printf("%f , %f e %f %f", a, b , c, (a + b + c)/3); } Quando da execução deste programa, o sistema, para execução da função scanf(), aguarda que sejam digitados três valores numéricos. Quando isto é feito, o sistema armazena estes três valores nas variáveis a, b e c, respectivamente. Na execução do último comando, o sistema exibe os valores armazenados nas variáveis a, b e c, em seguida avalia a expressão (a + b + c)/3 e exibe o seu valor na tela. Assim, o programa fornece a média aritmética de três números dados. Como um outro exemplo e considerando que o resultado de uma expressão lógica é um inteiro, o programa #include <stdio.h> main() { printf("%d", 5 > 3); } exibe na tela o valor 1, pois a relação 5 > 3 é verdadeira. Nos dois exemplos anteriores, utilizamos expressões, uma aritmética e uma lógica, como argumentos de uma função printf(). No nosso entendimento, não é uma boa prática de programação se utilizar expressões como argumentos de uma função printf(). Se o valor de uma expressão é útil para alguma coisa, ele deve ser armazenado em alguma variável (veremos isto na próxima seção) e esta deve ser utilizada para o fornecimento de resultados. Facilitando a execução de um programa A possibilidade de que mensagens possam ser exibidas permite que o próprio programa facilite a sua execução e que torne compreensíveis os resultados fornecidos. Da forma em que está escrito acima, a execução do programa que fornece a média de três números dados é dificultada pelo fato de que a execução da função scanf() faz com que o sistema aguarde a digitação dos números pretendidos (o cursor fica simplesmente piscando na tela do usuário) e o usuário pode não saber o que está se passando. Além disto, a execução da função printf() exibe apenas o resultado da expressão, sem indicação a que aquele valor se refere. Sem dúvida, o programa referido ficaria muito melhor da seguinte forma. #include <stdio.h> main() { float a, b, c; printf("Digite três números"); scanf("%f %f %f", &a, &b, &c); printf("A media dos numeros %f , %f e %f é igual a %f", a, b, c, (a + b + c)/3); } A exibição de uma mensagem pode ser também obtida através da função puts(), cujo protótipo está no arquivo stdio.h. Por exemplo, o comando printf(“Digite três números”) pode ser substituído pelo comando puts(“Digite três números”). Fixando o número de casas decimais O padrão utilizado pela maioria dos compiladores C é exibir os números de ponto flutuante com seis casas decimais. O número de casas decimais com as quais os números de ponto flutuante serão exibidos pode ser alterado pelo programa. Para isso deve-se acrescentar .n ao código de formatação da saída, sendo n o número de casas decimais pretendido. Por exemplo, se o programa que determina a média de três números fosse executado para a entrada 6.2, 8.45 e 7 seria exibido na tela o seguinte resultado A media dos numeros 6.200000, 8.550000 e 7.000000 é igual a 7.250000 Se o comando de saída do programa fosse printf("A media dos numeros %.2f , %.2f e %.2f é igual a %.1f", a, b, c, (a + b + c)/3); a saída seria A media dos numeros 6.20, 8.55 e 7.00 é igual a 7.3 Observe que a média dos números dados, de fato, é igual a 7.26. Como o código da formatação da saída da média foi %.1f, ela foi exibida com uma casa decimal e o sistema efetua os arredondamentos necessários. Observe também a utilização do ponto (e não da vírgula) como separador das partes inteiras e fracionárias. Isto é sempre necessário quando o ambiente de programação que se está utilizando foi desenvolvido nos Estados Unidos, o que é o mais frequente. Alinhando a saída O programa pode fixar a coluna da tela a partir da qual o conteúdo de uma variável, ou o valor de uma constante ou o valor de uma expressão será exibido. Isto é obtido acrescentado-se um inteiro m ao código de formatação. Neste caso, m indicará o número de colunas que serão utilizadas para exibição do conteúdo da variável ou do valor da constante. Por exemplo, levando-se em conta que a frase "Estou aprendendo a programar" contém vinte e oito caracteres, o programa abaixo #include <stdio.h> main() { printf("%38s", "Estou aprendendo a programar"); } exibe na tela a frase referida a partir da décima coluna. Observe que este programa também exemplifica a utilização de uma constante (no caso, uma cadeia de caracteres) como um argumento da função printf(). Observe também que referências a constantes do tipo cadeia de caracteres devem ser feitas com a cadeia escrita entre aspas. As aspas distinguem para o sistema 103.45 5.37 45.00 2.9 Comando de atribuição Armazenando dados gerados pelo programa A seção 2.7 apresentou o comando que permite que se armazene em variáveis a entrada do programa. Agora veremos como armazenar dados gerados durante a execução de um programa. Considere um programa para o cálculo da média de uma relação de números. Naturalmente, a quantidade de números da relação (se não foi fornecida a priori) deve ser de alguma forma determinada e armazenada em alguma variável para que possa ser utilizada no cálculo final da média pretendida. O armazenamento de dados gerados pelo próprio programa, alterações no conteúdo de variáveis e determinações de resultados finais de um processamento são realizados através do comando de atribuição, que deve ser escrito com a seguinte sintaxe. Identificador de variável = expressão; A expressão do segundo membro pode se resumir a um valor constante pertencente ao tipo de dado da variável do primeiro membro, caso em que o valor é armazenado naquela variável. Se não for este o caso, a expressão é avaliada e, se for do mesmo tipo da variável do primeiro membro, o resultado é armazenado na variável. A expressão do segundo membro pode envolver a própria variável do primeiro membro. Neste caso, o conteúdo anterior da variável será utilizado para a avaliação da expressão e será substituído pelo valor desta expressão. Por exemplo, se i é uma variável do tipo int ou do tipo float o comando i = i + 1; faz com que o seu conteúdo seja incrementado de uma unidade. Veremos ao longo do livro que comandos do tipo i = i + 1; aparecem com muita frequência. A linguagem C oferece uma forma simplificada de escrever este comando: i++;. Esta sintaxe se tornou tão característica da linguagem C que sua "ampliação" para incorporar recursos de programação orientada a objetos foi denominada C++ (de forma semelhante, o comando i = i – 1 pode ser escrito i--;). O incremento de uma variável de uma unidade também pode ser obtido através do comando ++i e estas expressões podem figurar em expressões aritméticas. A diferença entre i++ e ++i pode ser entendida no seguinte exemplo. A sequência de comandos i = 2; j = i++; k = ++i; realiza as seguintes ações: i = 2, armazena em i o valor 2; j = i++, armazena em j o valor 2 e armazena em i o valor 3 (incrementa o valor de i); k = ++i, armazena em i o valor 4 (incrementa o valor de i) e armazena o valor 4 na variável j. Um exemplo simples: determinando a parte fracionária de um número Como dissemos na seção 2.3, o armazenamento de um valor de ponto flutuante numa variável do tipo int faz com que seja armazenada na variável a parte inteira do valor de ponto flutuante. Isto permite que se extraia facilmente a parte fracionária de um número. Por exemplo, o programa a seguir fornece a parte fracionária de um número dado, calculada como a diferença entre ele e a sua parte inteira. /* Programa que fornece a parte fracionária de um número dado */ #include <stdio.h> main() { float Num, Frac; int Inteiro; printf("Digite um numero "); scanf("%f", &Num); Inteiro = Num; Frac = Num - Inteiro; printf("A parte fracionaria de %f e' %f ", Num, Frac); } Há que se ter cuidado com números fracionários. Já foi dito que o sistema (e qualquer ambiente para programação) não armazena exatamente todos os números reais, armazenando, de fato, aproximações da maioria deles. Por exemplo, se modificássemos o comando de saída do programa anterior para printf("A parte fracionaria de %f e' %.9f ", Num, Frac); e o executássemos para a entrada 2.41381 teríamos como saída a frase A parte fracionaria de 2.41381 e' 0.413810015! O ponto de exclamação (que não faz parte da saída do programa) foi posto pelo fato de que a saída esperada para esta entrada seria 0.41381. Combinando comandos de atribuição com operadores aritméticos O comando de atribuição pode ser combinado com operadores aritméticos para substituir atribuições cuja expressão do segundo membro contenha a variável do primeiro membro. Se x for o identificador da variável e $ for um operador aritmético, a atribuição x = x $ (expressão); pode ser indicada, simplesmente, por x $= expressão; Por exemplo, x *= 4; equivale a x = x*4; x += 5; equivale a x = x + 5; x %= y + 1; equivale a x = x % (y + 1); x -= 5; equivale a x = x – 5; x /= 2; equivale a x = x/2;. De acordo com o objetivo do livro, evitaremos a utilização destas opções oferecidas pela linguagem C, por entendermos que elas podem dificultar a legibilidade do comando. No nosso entendimento, só programadores mais experientes devem usar estes recursos. Lendo caracteres Alem da possibilidade de se dar entrada em caracteres através da função scanf() com código de conversão "%c", pode-se dar entrada em caracteres utilizando-se as funções getch() e getche() cujos cabeçalhos encontram-se no arquivo conio.h. Para a execução destas funções é necessário que se acione uma tecla; quando isto é feito, o caractere correspondente é retornado pela função e pode então ser armazenado numa variável do tipo char através de um comando de atribuição. A diferença entre estas funções é que na primeira o caractere digitado não aparece na tela de trabalho, o que acontece com a segunda função. Por exemplo, a execução do programa #include <stdio.h> #include <conio.h> main() { char c; c = getch(); printf("Voce digitou a letra %c \n", c); } digitando-se a letra A deixa a tela de trabalho da seguinte forma Voce digitou a letra A enquanto que a execução do programa #include <stdio.h> #include <conio.h> main() { char c; c = getche(); printf("Voce digitou a letra %c \n", c); } deixa a tela de trabalho da seguinte forma: A Você digitou a letra A 2.10 Exemplos Parte I 1. Voltando ao programa do cálculo da média de três números dados, observe que a média foi calculada e exibida, mas não foi armazenada. Se este programa fizesse parte de um programa maior (e isto normalmente acontece! Não se usa computação para uma questão tão simples!) e esta média fosse necessária em outra parte do programa, aquele trecho teria que ser rescrito. É uma boa prática, portanto, que resultados finais de processamento sejam armazenados em variáveis, sendo então os conteúdos destas variáveis exibidos através da função printf(). Assim, o programa referido ficaria melhor escrito da seguinte forma. /* Programa que determina a média de três números dados */ #include <stdio.h> main() { float a, b, c, Media; puts("Digite três números"); scanf("%f %f %f", &a, &b, &c); Media = (a + b + c)/3; printf("A media dos números %f , %f e %f é igual a %f ", a, b, c, Media); } 2. Agora apresentaremos um programa que recebendo um número inteiro como entrada fornece o algarismo da casa das unidades deste número, questão discutida no capítulo 1. Como vimos naquele capítulo, o algarismo procurado é o resto da divisão do número dado por 10. Temos então o seguinte programa (no capítulo 6 veremos um programa que necessita da solução desta questão). /* Programa que determina o algarismo da casa das unidades de um inteiro dado */ #include <stdio.h> main() { x = y; y = Aux; printf("Saida x = %0.2f, y = %0.2f \n", x, y); } Cabe observar que a permuta dos conteúdos pode ser obtida sem a utilização da variável Aux. Isto é deixado para que o leitor descubra a solução, sendo apresentado como exercício proposto. 2.11 Funções de biblioteca Como dissemos na seção 2.5, os compiladores C oferecem diversas funções com objetivos pré- determinados e que podem ser executadas durante a execução de um programa. Para isto a execução da função deve ser solicitada no programa como uma instrução, como operando de uma expressão ou como argumento de outra função (a solicitação da execução de uma função é normalmente chamada de ativação ou chamada da função). Para que o programador possa colocar no seu programa uma instrução que ative uma função é necessário que ele conheça o identificador da função, quantos e de que tipo são os argumentos com que elas devem ser ativadas e o tipo de valor que ela retorna ao programa quando termina sua execução (como já foi dito, este conjunto constitui o protótipo da função). A definição de uma função pré-definida se faz através da seguinte sintaxe. Identificador da função(Lista de argumentos) sendo que a lista de argumentos pode ser vazia. A tabela a seguir apresenta algumas das funções pré- definidas dos compiladores C, indicando o tipo dos seus argumentos e comentando o seu valor de retorno. Tabela 12 Algumas funções de biblioteca Identificador Argumentos O que retorna fabs(x) double Valor absoluto do argumento x acos(x) double Arco cujo valor do co-seno é o argumento x asin(x) double Arco cujo valor do seno é o argumento x atan(x) double Arco cujo valor da tangente é o argumento x cos(x) double Co-seno do argumento x log(x) double Logaritmo natural do argumento x log10(x) double Logaritmo decimal do argumento x pow(x, y) double, double Argumento x elevado ao argumento y pow10(x) int 10 elevado ao argumento x random(x) int Um número aleatório entre 0 e x - 1 sin(x) double Seno do argumento x sqrt(x) double Raiz quadrada do argumento x tan(x) doublé Tangente do argumento x tolower(x) char Converte o caractere x para minúsculo toupper(x) char Converte o caractere x para maiusculo O protótipo da função random() se encontra no arquivo stdlib.h e os protótipos das funções tolower() e toupper() estão no arquivo ctype.h. Os protótipos das outras funções estão no arquivo math.h que, como seu nome indica, contém os protótipos das funções matemáticas. Para que a função random() seja ativada é necessário que sua ativação seja precedida pela ativação da função randomize() que ativa o gerador de número aleatório. Por exemplo, o programa abaixo exibirá um número aleatório entre 0 e 99. /* programa que exibe, aleatoriamente, um número entre 0 e 99 */ #include <stdio.h> #include <stdlib.h> main() { int x; randomize(); x = random(100); printf("%d \n", x); } O exemplo a seguir, além de pretender motivar o próximo capítulo, ressalta uma observação já feita anteriormente: um programador só é capaz de escrever um programa que resolva um determinado problema se ele souber resolver o tal problema "na mão", ou seja, com a utilização apenas de lápis e papel. Trata-se de um programa que calcule a área de um triângulo, dados os comprimentos dos seus lados. Naturalmente, só é capaz de escrever este programa aquele que conhecer a fórmula abaixo, que dá a área do triângulo cujos lados têm comprimentos a, b e c: )(.)(.)(. cpbpappS −−−= onde 2 cbap ++= é o semiperímetro do triângulo. Com isto, temos o seguinte programa. /*Programa que determina a área de um triângulo de lados de comprimentos dados*/ #include <stdio.h> #include <math.h> main() { float x, y, z, Area, SemiPer; printf("Digite os comprimentos dos lados do triangulo"); scanf("%f %f %f", &x, &y, &z); SemiPer = (x + y + z)/2; Area = sqrt(SemiPer * (SemiPer - x) * (SemiPer - y) * (SemiPer - z)); printf("A area do triangulo de lados %f , %f e %f e' igual a %f \n", x, y, z, Area); } Se este programa for executado com entrada 3, 4 e 5 temos SemiPer = 6 e 36)56(.)46(.)36(.6 =−−−=S e, como era de se esperar, a área do triângulo cujos lados têm comprimento 3, 4 e 5 unidades de comprimento é igual a 6 unidades de área. Agora, se este programa fosse executado para entrada 1, 2 e 5 teríamos SemiPer = 4, 24)54(.)24(.)14(.4 −=−−−=S e ocorreria erro de execução pois o sistema (como era de se esperar) não calcula raiz quadrada de número negativo. O que acontece é que nem sempre três números podem ser comprimentos dos lados de um triângulo (a matemática prova que isto só acontece se cada um deles for menor do que a soma dos outros dois). Assim, o comando que calcula a Area só deveria ser executado se os valores digitados para x, y, e z pudessem ser comprimentos dos lados de um triângulo. 2.12 Exercícios propostos 1. Avalie cada uma das expressões abaixo. a) (-(-9) + sqrt((-9)*(-9) - 4*3*6))/(2*3). b) ((pow(3, 2) == 9) && (acos(0) == 0)) || (4 % 8 == 3). 2. Escreva programas para a) Converter uma temperatura dada em graus Fahrenheit para graus Celsius. b) Gerar o invertido de um número com três algarismos (exemplo: o invertido de 498 é 894). c) Somar duas frações ordinárias, fornecendo o resultado em forma de fração. d) Determinar o maior múltiplo de um inteiro dado menor do que ou igual a um outro inteiro dado 24)54(.)24(.)14(.4 −=−−−=S (exemplo: o maior múltiplo de 7 menor que 50 é 49). e) Determinar o perímetro de um polígono regular inscrito numa circunferência, dados o número de lados do polígono e o raio da circunferência. 3. Escreva um programa que permute o conteúdo de duas variáveis sem utilizar uma variável auxiliar (ver exemplo 5 da seção 2.9). 4. Uma loja vende seus produtos no sistema entrada mais duas prestações, sendo a entrada maior do que ou igual às duas prestações; estas devem ser iguais, inteiras e as maiores possíveis. Por exemplo, se o valor da mercadoria for R$ 270,00, a entrada e as duas prestações são iguais a R$ 90,00; se o valor da mercadoria for R$ 302,75, a entrada é de R$ 102,75 e as duas prestações são a iguais a R$ 100,00. Escreva um programa que receba o valor da mercadoria e forneça o valor da entrada e das duas prestações, de acordo com as regras acima. Observe que uma justificativa para a adoção desta regra é que ela facilita a confecção e o consequente pagamento dos boletos das duas prestações. 5. Um intervalo de tempo pode ser dado em dias, horas, minutos, segundos ou sequências "decrescentes" destas unidades (em dias e horas; em horas e minutos; em horas, minutos e segundos), de acordo com o interesse de quem o está manipulando. Escreva um programa que converta um intervalo de tempo dado em segundos para horas, minutos e segundos. Por exemplo, se o tempo dado for 3 850 segundos, o programa deve fornecer 1 h 4 min 10 s. 6. Escreva um programa que converta um intervalo de tempo dado em minutos para horas, minutos e segundos. Por exemplo, se o tempo dado for 145.87 min, o programa deve fornecer 2 h 25 min 52.2 s (vale lembrar que o ponto é o separador da parte inteira). 7. Um programa para gerenciar os saques de um caixa eletrônico deve possuir algum mecanismo para decidir o número de notas de cada valor que deve ser disponibilizado para o cliente que realizou o saque. Um possível critério seria o da "distribuição ótima" no sentido de que as notas de menor valor disponíveis fossem distribuídas em número mínimo possível. Por exemplo, se a máquina só dispõe de notas de R$ 50, de R$ 10, de R$ 5 e de R4 1, para uma quantia solicitada de R$ 87, o programa deveria indicar uma nota de R$ 50, três notas de R$ 10, uma nota de R$ 5 e duas notas de R$ 1. Escreva um programa que receba o valor da quantia solicitada e retorne a distribuição das notas de acordo com o critério da distribuição ótima. 8. De acordo com a Matemática Financeira, o cálculo das prestações para amortização de um financiamento de valor F em n prestações e a uma taxa de juros i é dada pela fórmula P = F/an¬i, onde an¬i = ((1 + i)n – 1)/(i . (1 + i)n). Escreva um programa que determine o valor das prestações para amortização de um financiamento, dados o valor do financiamento, o número de prestações para amortização e a taxa de juros. Observação Propostas de soluções dos exercícios propostos podem ser solicitadas através de mensagem eletrônica para jaime@ccen.ufal.br com assunto RESPOSTAS LIVRO C, anexando o formulário abaixo devidamente preenchido. Nome Categoria1 Instituição2 Curso2 Cidade/Estado 1Categoria: docente, estudante, autodidata 2Se docente ou estudante int x, y; printf("Digite o numero"); scanf("%d", &x); if (x % 2 == 0) printf("%d e' par \n", x); else printf("%d e' impar \n", x); } Mesmo considerando que os compiladores da linguagem C não consideram espaços nem mudanças de linha, observe que estamos procurando escrever cada instrução em uma linha e a sequência vinculada à estrutura de decisão com uma tabulação diferente da tabulação em que estão postos o if e o else. Esta forma de se editar um programa, chamada indentação, deve ser praticada por todo programador pois ela facilita sobremaneira a legibilidade dos programas. Se o programa acima fosse digitado da forma seguinte /* Programa para verificar se um número é par*/ #include <stdio.h> main(){ int x, y; printf("Digite o numero"); scanf("%d", &x); if (x % 2 == 0) printf("%d e' par \n", x); else printf("%d e' impar \n", x); } ele seria executado da mesma forma, porém a sua legibilidade estaria prejudicada. 3.4 O operador condicional ternário Quando as duas opções de um comando if else contêm apenas uma atribuição a uma mesma variável, pode-se utilizar o operador condicional ternário que possui a seguinte sintaxe: Variável = Expressão lógica ? Expressão 1 : Expressão 2; Na execução deste comando a Expressão lógica é avaliada e se for diferente de zero o valor da Expressão 1 é atribuído à Variável; caso contrário, o valor da Expressão 2 é atribuído. Por exemplo, se x, y e Maior são três variáveis do tipo float o armazenamento do maior dos conteúdos de x e de y na variável Maior poderia ser obtido com a seguinte atribuição: Maior = (x > y) ? x : y; Como um outro exemplo, para se armazenar na variável AbsNum o valor absoluto do conteúdo de uma variável Num (sem utilizar a função fabs()) bastaria o comando: AbsNum = (Num >= 0) ? Num : -Num; 3.5 Exemplos Parte II 0. De um modo geral, as ligações telefônicas são cobradas pelas suas durações. O sistema registra os instantes em que a ligação foi iniciada e concluída e é acionado um programa que determina o intervalo de tempo decorrido entre aqueles dois instantes dados. O programa abaixo recebe dois instantes dados em horas e minutos e determina o intervalo de tempo (em horas e minutos) decorrido entre eles. /*Programa que determina o intervalo de tempo decorrido entre dois instantes*/ include <stdio.h> main() { int h1, min1, h2, min2, h, min; puts("Digite o instante inicial (horas e minutos)"); scanf("%d %d", &h1, &min1); puts("Digite o instante final"); scanf("%d %d", &h2, &min2); h = h2 - h1; min = min2 - min1; if ((h < 0) || ((h == 0) && (min < 0))) puts("\aDados invalidos! O segundo instante é anterior ao primeiro"); else { if (min < 0) { h = h - 1; min = min + 60; } printf( "Entre os instantes %dh %dmin e %dh %dmin passaram-se %dh %dmin", h1, min1, h2, min2, h, min); } } 1. No último exemplo do capítulo 2, apresentamos um programa que calculava a área de um triângulo, dados os comprimentos dos seus lados. No final dele, mostramos que o mesmo não fornecia respostas satisfatórias para todas as entradas e comentamos que o cálculo da área deveria ser precedido da verificação de que os dados de entrada são de fato comprimentos dos lados de um triângulo. O programa referido, escrito agora de forma completa e correta, seria o seguinte. /* Programa para calcular a área de um triângulo*/ #include <stdio.h> #include <math.h> main() { float x, y, z, Area, SemiP; printf("Digite os comprimentos dos lados do triangulo"); scanf("%f %f %f", &x, &y, &z); if ((x < y + z) && (y < x + z) && (z < x + y)) { SemiP = (x + y + z)/2; Area = sqrt(SemiP * (SemiP - x) * (SemiP - y) * (SemiP - z)); printf("A area do triangulo de lados %f , %f e %f e' igual a %f \n", x, y, z, Area); } else printf("Os números %f, %f %f não podem ser comprimentos dos lados de um triângulo\n", x, y, z); } 2. Programas que manipulam datas (por exemplo, um programa que determine o número de dias entre duas datas dadas) contêm trechos que verificam se um ano dado é bissexto. Sabendo que um ano é bissexto se ele é múltiplo de quatro, teríamos o seguinte programa. /*Programa que verifica se um dado ano é bissexto */ #include <stdio.h> main() { int Ano; printf("Digite o ano"); scanf("%d", &Ano); if (Ano % 4 == 0) printf("%d e' bissexto %d \n", Ano); else printf("%d não e' bissexto %d \n", Ano); } Rigorosamente falando, há anos múltiplos de quatro que não são bissextos. São aqueles múltiplos de 100 que não são múltiplos de 400. Por exemplo, o ano 2000 foi um ano bissexto, mas o ano de 2100 não será. Para que o programa detecte estas exceções, a expressão lógica que controla o comando if deve ser ampliada e talvez seja mais fácil considerar a condição para que um ano não seja bissexto: não deve ser múltiplo de quatro ou se for múltiplo de 100 não deve ser múltiplo de 400. Observe que agora optamos por uma expressão lógica que garantisse o fato de que o ano dado não é bissexto. /* Programa que verifica se um dado ano é bissexto */ #include <stdio.h> main() { int Ano; printf("Digite o ano"); scanf("%d", &Ano); if ((Ano % 4 != 0) || ((Ano % 100 == 0) && (Ano % 400 != 0))) printf("%d nao e' bissexto \n", Ano); else printf("%d e' bissexto \n", Ano); } 3. O programa para ordenar os conteúdos de duas variáveis, visto na seção 3.2, é um caso muito particular da questão mais geral da ordenação de uma relação de números ou de nomes, problema que tem vasta aplicação na vida prática, principalmente na ordenação de uma lista de nomes (este problema também é conhecido como classificação). Para a solução geral existem diversos algoritmos com este objetivo. No capítulo 7 teremos oportunidade de discutir programas baseados em alguns destes algoritmos. Por enquanto, vejamos um programa que ordene três números dados. Além de exemplificar o comando if, o programa abaixo mostra como se pode (e se deve) utilizar raciocínios anteriores para se escrever programas. Seja então um programa que receba três números inteiros, armazene-os nas variáveis x, y e z e que ao final da sua execução deixe os conteúdos de x, de y e de z na ordem crescente. Uma ideia bem interessante é armazenar na variável x o menor dos números e em seguida ordenar os conteúdos de y e de z, que é exatamente o problema de ordenar os conteúdos de duas variáveis, que foi referido acima. Obviamente, para se executar a primeira ação pretendida (armazenar na variável x o menor dos números) só é necessário se fazer alguma coisa se o valor de x já não for o menor dos números dados, ou seja, se x > y ou x > z. Nesta hipótese, o menor deles é y ou z e este menor deve ser permutado com x. Temos então o seguinte programa. /* Programa para ordenar três números dados*/ #include <stdio.h> main() { float x, y, z, Aux; printf("Digite os tres numeros"); scanf("%f %f %f", &x, &y, &z); printf("Numeros dados: %f , %f , %f \n", x, y, z); if ((x > y) || (x > z)) /* verifica se x não é o menor */ if (y < z) /* neste caso y é o menor */ { Aux = x; /* troca os conteúdos de x e de y */ x = y; y = Aux; } else /* neste caso z é o menor */ { Aux = x; /* troca os conteúdos de x e de z */ x = z; z = Aux; } if (y > z) /* verifica se z e y ainda não estão ordenados */ { Aux = y; /* troca o conteúdo de y e de z */ #include <stdio.h> main() { float SAtual, SNovo, Indice; printf("Digite o salário atual"); scanf("%f", &SAtual); if (SAtual <= 200) Indice = 1.13; else if (SAtual <= 400) Indice = 1.11; else if (SAtual <= 800) Indice = 1.09; else Indice = 1.07; SNovo = SAtual*Indice; printf("Atual = %.2f \n Novo = %.2f \n" , SAtual, SNovo); } Observe que a sequência associada à opção else é iniciada com um outro comando if. Alguns autores preferem destacar um fato como este definindo um "novo comando" denominando-o else if. 7. Um outro exemplo que utiliza comandos de seleção aninhados e em que a escolha da expressão lógica que controlará o comando if é importante é um programa que determine o número de dias de um mês (um programa como este seria parte integrante de um programa que manipulasse datas). Como os meses de trinta dias são quatro e os de trinta e um dias são sete, usamos os primeiros para o controle do comando de seleção. /* Programa que determina o número de dias de um mês dado */ #include <stdio.h> main() { int Mes, Ano, NumDias; printf("Digite o mes"); scanf("%d", &Mes); if ((Mes == 4 ) || (Mes == 6) || (Mes == 9) || (Mes == 11)) NumDias = 30; else if (Mes == 2) { printf("Digite o ano"); canf("%d", &Ano); if (Ano % 4 != 0) NumDias = 28; else NumDias = 29; } else NumDias = 31; printf("O mes %d tem %d dias", Mes, NumDias); } No capítulo 6 veremos que o programa acima pode ser bastante simplificado. 3.6 O comando switch Muitos programas são desenvolvidos de modo que eles podem realizar várias tarefas, de forma independente. Por exemplo, um programa que gerencie um caixa eletrônico de um banco deve oferecer ao usuário algumas opções em relação à ação que ele pretende realizar na sua conta como a emissão do saldo atual, a emissão de um extrato, a realização de um saque e a realização de um depósito. É comum que um programa que permita a realização de várias tarefas inicie apresentando ao usuário um menu de opções com a indicação das diversas tarefas que o programa pode executar e a permissão de que o usuário escolha a tarefa pretendida. Como, em geral, são várias as opções disponíveis (cada uma delas com uma sequência específica de comandos) e só uma das opções será a escolhida, é necessária uma estrutura que decide entre várias sequências de comandos qual vai ser executada ou quais vão ser executadas. O comando switch tem este objetivo e deve ser escrito com a seguinte sintaxe: switch(Expressão) { case constante1 : Sequência de instruções 1 case constante2 : Sequência de instruções 2 . . . case constante n : Sequência de instruções n default : Sequência de comando x } Aí, a Expressão argumento do comando deve resultar num valor do tipo int ou num valor do tipo char e, opcionalmente, a ultima instrução de cada uma das sequências Sequência de instruções i é break. A semântica deste comando é bem simples: a Expressão é avaliada e as sequências de instruções situadas entre o valor da expressão apresentado nos cases e um comando break ou o delimitador do comando são executadas. Se o valor da Expressão for diferente de todas as opções dadas pelas constantes associadas aos cases, a sequência de instruções vinculada ao default será executada. Por exemplo, o programa #include <stdio.h> main() { int x; printf("Digite um número inteiro entre 1 e 5 \n"); scanf("%d", &x); switch (x) { case 1 : printf("Valor de x: %d \n", x); case 2 : printf("Valor do dobro de %d: %d \n", x, 2*x); case 3 : printf("Valor do triplo de %d: %d \n", x, 3*x); case 4 : printf("Valor do quadruplo de %d: %d \n", x, 4*x); default : printf("Valor digitado: %d \n", x); } } executado para x = 1 executa todas as sequências vinculadas aos cases fornecendo a seguinte saída: Valor de x: 1 Valor do dobro de 1: 2 Valor do triplo de 1: 3 Valor do quadruplo de 1: 4 Valor digitado: 1 Se for executado para x = 3, só as sequências a partir do case 3 serão executadas e a saída será: Valor do triplo de 3: 9 Valor do quadruplo de 3: 12 Valor digitado: 3 e se for executado x = 10 apenas a sequência vinculada à condição default será a executada e a saída será: Valor digitado : 10 Três observações: 1. A sequência de instruções vinculada a uma opção case pode ser vazia, caso em que, evidentemente, nada é executado; 2. Se apenas uma sequência de comandos deve ser executada, deve-se encerrá-la com um break; 3. A opção default é opcional: se ela não aparece no comando e o valor da Expressão for diferente de todos os valores disponíveis, nada é executado e a instrução logo após o comando switch passa a ser executada. 3.7 Exemplos Parte III 1. O programa para determinar o número de dias de um mês (exemplo 7 da seção anterior) poderia utilizar o comando switch: /* Programa para determinar o numero de dias de um mes*/ #include <stdio.h> main() { int Mes, Ano, NumDias; printf("Digite o mes \n"); scanf("%d", &Mes); switch (Mes) { case 2 : printf("Digite o ano"); scanf("%d", &Ano); if (Ano % 4 != 0) NumDias = 28; else NumDias = 29; break; case 4 : case 6 : case 9 : case 11 : NumDias = 30; break; default : NumDias = 31; } printf("O mes de numero %d tem %d dias \n", Mes, NumDias); } Observe que se o mês de entrada for 2, o programa pede o ano para determinar se ele é bissexto. Aí, determina o número de dias e a instrução break encerra o comando switch. Se a entrada for 4, com a sequência de comandos vinculada ao case 4 é vazia (e, portanto, não contém break) as sequências vinculadas aos cases seguintes são executadas até o break do case 11 (para os meses 4, 6, 9 e 11 o número de dias é igual a 30!). Se a entrada não for 2, 4, 6, 9 e 11 a opção default será executada e, portanto, o mês terá 31 dias. Evidentemente, fica faltando discutir a possibilidade de uma entrada inválida como, por exemplo, 13. Isto será discutido no próximo capítulo. 2. Vejamos um exemplo onde a expressão do comando switch retorna um valor do tipo char. Trata-se da geração de uma calculadora para as quatro operações aritméticas básicas. /*Calculadora eletrônica*/ #include <stdio.h> exibindo, nos casos afirmativos, sua hipotenusa e seus catetos. 7. Escreva um programa para determinar as raízes reais ou complexas de uma equação do segundo grau, dados os seus coeficientes. 8. Escreva um programa para determinar a idade de uma pessoa, em anos meses e dias, dadas a data (dia, mês e ano) do seu nascimento e a data (dia, mês e ano) atual. 9. Escreva um programa que, recebendo as duas notas bimestrais de um aluno da escola referida no exemplo 5 da seção 3.5, forneça a nota mínima que ele deve obter na prova final para que ele seja aprovado. Observação Propostas de soluções dos exercícios propostos podem ser solicitadas através de mensagem eletrônica para jaime@ccen.ufal.br com assunto RESPOSTAS LIVRO C, anexando o formulário abaixo devidamente preenchido. Nome Categoria1 Instituição2 Curso2 Cidade/Estado 1Categoria: docente, estudante, autodidata 2Se docente ou estudante 4. Estruturas de repetição 4.1 Para que servem as estruturas de repetição Um locutor brasileiro ao narrar um jogo de futebol americano nos Estados Unidos recebe a informação do placar eletrônico sobre a temperatura do estádio medida em graus Fahrenheit. Naturalmente, ele deve fornecer aos telespectadores brasileiros a temperatura em graus Celsius. Para isto, o locutor, de posse de um computador, poderia utilizar o programa abaixo, que foi solicitado no primeiro item do segundo exercício da seção 2.12. /*Programa que converte uma temperatura dada em graus Fahrenheit para graus Celsius*/ #include <stdio.h> main() { float Fahrenheit, Celsius; printf("Digite a temperatura em Fahrenheit"); scanf("%f", &Fahrenheit); Celsius = 5 * (Fahrenheit - 32)/9; printf("A temperatura de %.2f Fahrenheit corresponde a %.2f Celsius ", Fahrenheit, Celsius); } Se o placar eletrônico indicasse uma temperatura de 60o F, o narrador executaria o programa com a entrada 60 e receberia a saída A temperatura de 60 graus Fahrenheit corresponde a 15.55 graus Celsius Certamente, seria mais prático a produção da transmissão do evento disponibilizar para o locutor uma tabela contendo as temperaturas possíveis em graus Fahrenheit e as correspondentes em graus Celsius. A confecção desta tabela poderia ser feita através de um programa que contivesse vários comandos que calculassem para cada temperatura em graus Fahrenheit pretendida a correspondente temperatura em graus Celsius e exibissem estas temperaturas. Neste caso, não haveria necessidade de comando de entrada; porém, para cada temperatura em graus Fahrenheit pretendida, haveria, pelo menos, um comando de atribuição e a chamada da função printf(). Se a faixa de temperatura em graus Fahrenheit a ser coberta pela tabela fosse de vinte a oitenta graus, teríamos um programa como o programa abaixo. /*Programa (muito ruim) que gera uma tabela de conversão de temperaturas em graus Fahrenheit para graus Celsius */ #include <stdio.h> main() { int Fahrenheit; printf("Tabela de conversao graus Fahrenheit/graus Celsius \n"); printf("-------------------------------------------------\n"); printf("\t Fahrenheit \t | \t Celsius\n"); printf("-------------------------------------------------\n"); Fahrenheit = 10; printf("\t %f \t | \t %f \n", Fahrenheit, 5.0*(Fahrenheit - 32)/9); Fahrenheit = 11; printf("\t %f \t | \t %f \n", Fahrenheit, 5.0*(Fahrenheit - 32)/9); . . . /*Mais "uma porção" de comandos! */ Fahrenheit = 80; printf("\t %f \t | \t %f \n", Fahrenheit, 5.0*(Fahrenheit - 32)/9); } Isto seria contornado se pudéssemos repetir a execução dos comandos que gerariam as temperaturas em graus Fahrenheit e as correspondentes em graus Celsius. A linguagem C possui os comandos for; while e do while, chamados estruturas de repetição ou laços, cujas execuções redundam em repetições da execução de uma determinada sequência de comandos. 4.2 O comando for O comando for é uma estrutura de repetição que repete a execução de uma dada sequência de comandos um número de vezes que pode ser determinado pelo próprio programa, devendo ser escrito com a seguinte sintaxe: for (inicializações; condições de manutenção da repetição; incrementos) { sequência de comandos } Como os nomes indicam, em inicializações, são atribuídos valores iniciais a variáveis; em condições de manutenção da repetição, estabelecem-se, através de uma expressão, as condições nas quais a execução da sequência de comandos será repetida; em incrementos, incrementam-se variáveis. Quando um comando for é executado, a sequência de comandos da área das inicializações é executada. Em seguida, a expressão que fixa as condições de manutenção da repetição é avaliada. Se o valor desta expressão não for nulo, a sequência de comandos é executada, sendo em seguida executada a sequência de comandos da área dos incrementos. Novamente a expressão das condições de manutenção da repetição é avaliada e tudo se repete até que o seu valor seja igual a zero. Por exemplo, o programa #include <stdio.h> main() { int i; for (i = 1; i <= 10; i = i + 1) printf("%d ", i); } exibe na tela os números 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. Por seu turno, o programa #include <stdio.h> main() { int i; for (i = 10; i >= 0; i = i - 2) printf("%d ", i); } exibe na tela os números 10, 8, 6, 4, 2, 0. Já o programa #include <stdio.h> main() { int i; for (i = 1; i <= 10; i = i + 20) printf("%d ", i); } exibe, apenas, o número 1. A semântica do comando for implica que a sequência de comandos pode não ser executada nem uma única vez. Basta que na "primeira" execução do comando for a expressão que controla a repetição assuma o valor zero. Por exemplo, o programa abaixo não exibe nenhum valor na tela. #include <stdio.h> main() encontrado, o que, evidentemente, vai prejudicar a performance do programa. Isto pode ser contornado pois os compiladores C permitem que uma variável de controle de um comando for tenha o seu conteúdo alterado dentro do próprio comando. Com isto, o programa acima ficaria da seguinte forma. #include <stdio.h> main() { int Num, i, Divisor; printf("Digite um número inteiro: "); scanf("%d", &Num); Divisor = 0; for (i = 2; i < Num; i = i + 1) if (Num % i == 0) { Divisor = i; i = Num; } if (Divisor != 0) printf("%d e' divisor próprio de %d \n", Divisor, Num); else printf("%d não tem divisores próprios \n", Num); } Nesta versão, quando o primeiro divisor próprio é encontrado, o comando i = Num; faz com que a execução do comando for seja interrompida. A prática de encerrar um comando for através da alteração do conteúdo da variável de controle não será aqui incentivada pelo fato de que isto desestrutura o programa, dificultando sua legibilidade. Além disso, há situações em que não se pode conhecer o número máximo de repetições de uma estrutura de repetição. Na verdade, a questão central é que o comando for deve ser utilizado quando o número de repetições de execução de uma sequência de comandos é conhecido a priori. Quando isto não acontece (que é o caso do exemplo anterior: não se sabe a priori se e quando um divisor próprio vai ser encontrado), deve-se usar o comando while, que possui a seguinte sintaxe: while (Expressão) { Sequência de comandos } sendo os delimitadores opcionais se a sequência possui um só comando (como acontece nas outras estruturas de repetição). A semântica deste comando é óbvia: a sequência de comandos é executada enquanto o valor da Expressão for diferente de zero. Naturalmente, pode ocorrer que a sequência de comandos não seja executada nenhuma vez, isto ocorrendo se o valor da Expressão for igual a zero quando da "primeira" execução do comando (o teste é feito antes da execução da sequência de comandos). Por outro lado, é necessário que um dos comandos da sequência de comandos altere conteúdos de variáveis que aparecem na Expressão de modo que em algum instante ela se torne igual a zero. Do contrário, a sequência de comandos terá sua execução repetida indefinidamente, o programa nunca termina e, evidentemente, não executa a tarefa para a qual foi desenvolvido. Quando isto acontece é comum se dizer que o programa está em looping. Com o comando while as questões levantadas acima sobre o programa para determinar um divisor próprio de um inteiro dado são resolvidas e temos o seguinte programa: /*Programa que determina o menor divisor próprio de um inteiro */ #include <stdio.h> #include <conio.h> main() { int Num, d, Met; printf("Digite o numero: "); scanf("%d", &Num); Met = Num/2; d = 2; while (Num % d != 0 && d < Met) d++; if (d <= Met) printf("%d é divisor de %d \n", d, Num); else printf("%d não tem divisores próprios", Num); getch(); } Observe que, ao contrário dos exemplos anteriores, a estrutura também seria interrompida quando a variável com a qual se procura um divisor atingisse a "metade" do inteiro; isto se explica pelo fato de que se um inteiro não possui um divisor próprio menor do que sua "metade", então ele é primo. Esta versão ainda pode ser melhorada utilizando-se o fato discutido em [Evaristo, J 2002] de que se um inteiro não possui um divisor próprio menor do que ou igual a sua raiz quadrada, ele não tem divisores próprios. Levando isso em conta, teríamos o seguinte programa. /*Programa que determina o menor divisor próprio de um inteiro*/ #include <stdio.h> #include <conio.h> #include <math.h> main() { int Num, d; float r; printf("Digite o numero: "); scanf("%d", &Num); r = sqrt(Num); d = 2; while (Num % d != 0 && d <= r) d++; if (d <= r) printf("%d é divisor de %d \n", d, Num); else printf("%d não tem divisores próprios", Num); getch(); } Como já foi dito, um número inteiro que não tem divisores próprios é chamado número primo. Assim, o comando de saída vinculado à opção else poderia ser printf("%d é primo", Num); Vale observar que o comando d = 2; dos programas acima atribuiu um valor inicial à variável d. Este valor é incrementado de uma unidade enquanto um divisor não foi encontrado. Um comando de atribuição de um valor inicial a uma variável é chamado inicialização da variável e os compiladores da linguagem C permitem que inicializações de variáveis sejam feitas no instante em que elas são declaradas. Assim, as declarações de variáveis dos programas acima poderiam ter sido feitas da seguinte forma: int Num, i, d = 2; Neste livro, na maioria das vezes vamos optar por inicializar as variáveis imediatamente antes da necessidade. A razão desta opção é que há situações, como mostraremos no próximo exemplo, em que não se pode simplesmente inicializar uma variável quando da sua declaração. Observe que o último comando dos últimos dois programas foi uma chamada da função getch(). Como já foi dito, a execução desta função requer a digitação de alguma tecla. Isto faz com a janela do usuário (que exibe o resultado do processamento) permaneça ativa até que uma tecla seja acionada. Repetindo a execução de um programa Uma outra aplicação importante do comando while diz respeito a aplicações sucessivas de um programa. O leitor deve ter observado que os programas anteriores são executados apenas para uma entrada. Se quisermos a sua execução para outra entrada precisamos executar o programa de novo. Pode-se repetir a execução de um programa quantas vezes se queira, colocando-o numa estrutura definida por um comando while, controlada pelo valor de algum dado de entrada. Neste caso, o valor que encerra a execução pode ser informado dentro da mensagem que indica a necessidade da digitação da entrada. O programa anterior poderia ser então escrito da seguinte forma. /*Programa que determina o menor divisor próprio de um inteiro */ #include <stdio.h> #include <conio.h> #include <math.h> main() { int Num, d; float r; printf("Digite o numero (zero para encerrar): "); Num = 1; while (Num != 0) { scanf("%d", &Num); r = sqrt(Num); d = 2; while (Num % d != 0 && d <= r) d++; if (d <= r) printf("%d é divisor de %d \n", d, Num); else printf("%d é primo", Num); } } Observe que, neste caso, a variável d não pode ser inicializada quando da sua declaração. Observe também que não há necessidade da função getch(), pois a própria repetição da execução deixa a janela do usuário aberta. Alguns programadores preferem que a repetição da execução de um programa seja determinada por uma pergunta ao usuário do tipo “Deseja continuar (S/N)?”. Neste caso, há necessidade de uma variável do tipo char para receber a resposta e controlar a repetição da execução do programa. #include <stdio.h> #include <ctype.h> #include <conio.h> #include <math.h> main() { int Num, d; float r; char c; c = 'S'; while (toupper(c) == 'S') { printf("Digite o numero: "); scanf("%d", &Num); r = sqrt(Num); d = 2; 4.6 Exemplos Parte IV 1. Consideremos um programa para determinar a soma dos n primeiros números ímpares, n dado. Por exemplo, se for fornecido para n o valor 6, o programa deve retornar 36, pois 1 + 3 + 5 + 7 + 9 + 11 = 36. Naturalmente, o sistema pode gerar os números impares que se pretende somar, através do comando Impar = 1 e da repetição do comando Impar = Impar + 2. Naturalmente, também, para que o sistema gere o próximo ímpar, o anterior já deve ter sido somado. Isto pode ser feito através do comando Soma = 0 e da repetição do comando Soma = Soma + Impar. Temos então o seguinte programa. /*Programa que soma os n primeiros números ímpar, n dado*/ #include <stdio.h> main() { int Soma, Impar, n, i; printf("Digite o valor de n: "); scanf("%d", &n); Impar = 1; Soma = 0; for (i = 1; i <= n; i = i + 1) { Soma = Soma + Impar; Impar = Impar + 2; } printf("Soma dos %d primeiros números impares: %d \n", n, Soma); } Observe que os comandos Impar = 1 e Soma = 0 atribuem um valor inicial às variáveis para que estes valores iniciais possam ser utilizados nas primeiras execuções dos comandos Soma = Soma + Impar e Impar = Impar + 2. Como já dissemos, nos referimos a comandos que atribuem valores iniciais a variáveis para que estes valores possam ser utilizados na primeira execução de um comando que terá sua execução repetida como inicialização da variável. Uma outra observação interessante é que, como existe uma fórmula que dá o i-ésimo número ímpar (ai = 2i - 1), o programa acima poderia ser escrito de uma forma mais elegante, prescindindo, inclusive, da variável Impar. /*Programa que soma os n primeiros números impar, n dado*/ #include <stdio.h> main() { int Soma, n, i; printf("Digite o valor de n: "); scanf("%d", &n); Soma = 0; for (i = 1; i <= n; i = i + 1) Soma = Soma + 2*i - 1; printf("Soma dos %d primeiros números impares: %d \n", n, Soma); } Optamos por apresentar a primeira versão pelo fato de que nem sempre a fórmula para gerar os termos da sequência que se pretende somar é tão simples ou é muito conhecida. Por exemplo, o exercício número 2 da seção 4.7 pede para somar os quadrados dos n primeiros números naturais e, neste caso, embora a fórmula exista, ela não é tão conhecida. 2. Um dos exemplos da seção anterior apresentava um programa que determinava, se existisse, um divisor próprio de um inteiro dado. Imaginemos agora que queiramos um programa que apresente a lista de todos os divisores de um inteiro n dado. Neste caso, o programa pode percorrer todos os inteiros desde um até a metade de n verificando se cada um deles é um seu divisor. Temos então o seguinte programa. #include <stdio.h> main() { int Num, i; printf("Digite o numero: "); scanf("%d", &Num); printf("Divisores próprios de %d: \n", Num); for (i = 2; i <= Num/2; i = i + 1) if (Num % i == 0) printf("%d \n", i); } Vale observar que, ao contrário do que foi dito na seção 2.9, os valores de saída deste programa não estão sendo armazenados. O que acontece é que ainda não temos condições de armazenar uma quantidade indefinida de elementos. Este problema será resolvido no capítulo 6. 3. Na seção 1.5 discutimos um algoritmo que determinava o quociente e o resto da divisão entre dois inteiros positivos dados. Embora os compiladores de C possuam o operador % que calcula o resto de uma divisão inteira entre dois inteiros positivos, vamos apresentar, por ser interessante, a implementação do algoritmo referido. /*Programa que determina o quociente e o resto da divisão entre dois inteiros positivos*/ #include <stdio.h> main() { int Dividendo, Divisor, Quoc, Resto; printf("Digite o dividendo e o divisor (diferente de zero!): "); scanf("%d %d", &Dividendo, &Divisor); Quoc = 1; while (Quoc * Divisor <= Dividendo) Quoc = Quoc + 1; Quoc = Quoc - 1; Resto = Dividendo - Quoc * Divisor; printf("Quociente e resto da divisão de %d por %d: %d e %d\n", Dividendo, Divisor, Quoc, Resto); } 4. Em muitos casos há necessidade de que um dos comandos da sequência que terá sua execução repetida através de uma estrutura de repetição seja uma outra estrutura de repetição (num caso deste dizemos que as estruturas estão aninhadas). Para um exemplo, sejam A = {1, 2, 3, ..., n} e um programa que pretenda exibir o produto cartesiano AxA. Observe que para cada valor da primeira componente o programa deve gerar todas as segundas componentes. Devemos ter, portanto, uma estrutura de repetição para gerar as primeiras componentes e uma outra, vinculada a cada valor da primeira componente, para gerar as segundas componentes. /*Programa para gerar um produto cartesiano*/ #include <stdio.h> main() { int n, i, j; printf("Digite o numero de elementos do conjunto: "); scanf("%d", &n); printf("{"); for (i = 1; i <= n; i = i + 1) for (j = 1; j <= n; j = j + 1) printf("(%d, %d), ", i, j); printf("}"); } 5. É interessante observar que a variável de controle da estrutura interna pode depender da variável de controle da estrutura externa. Por exemplo, se ao invés dos pares ordenados, quiséssemos os subconjuntos do conjunto A com dois elementos, o programa não deveria exibir o subconjunto {1, 1}, que possui um só elemento, e deveria exibir apenas um dos subconjuntos {1, 2} e {2, 1} já que eles são iguais. Isto pode ser obtido inicializando j com uma unidade maior do que o valor de i. /*Programa para gerar um conjunto de subconjuntos de um conjunto*/ #include <stdio.h> main() { int n, i, j; printf("Digite o numero de elementos do conjunto: "); scanf("%d", &n); printf("{"); for (i = 1; i <= n; i = i + 1) for (j = i + 1; j <= n; j = j + 1) printf("{%d, %d}, ", i, j); printf("}"); } 6. Seja um programa para o cálculo da média de uma dada quantidade de números. Na seção 1.5 discutimos um algoritmo para determinar a média de 10.000 números dados. Na ocasião discutimos que utilizaríamos uma única variável para receber os números sendo que um valor subsequente só seria solicitado depois que o anterior fosse "processado". A diferença agora é que a quantidade de números será um dado de entrada, o que torna o programa de aplicação mais variada. Como a quantidade de números será dada, pode- se utilizar uma estrutura for para receber e somar os números. /*Programa para calcular a media de n numeros, n dado*/ #include <stdio.h> main() { int n, i; float Num, Soma, Media; Soma = 0; printf("Digite o numero de elementos: "); scanf("%d", &n); printf("\n Digite os elementos:"); for (i = 1; i <= n; i = i + 1) { scanf("%f", &Num); Soma = Soma + Num; } Media = Soma/n; printf("Media = %f", Media); } 7. O exemplo acima tem o inconveniente de que sua execução exige que se saiba anteriormente a quantidade de números e isto não ocorre na maioria dos casos. Vejamos então um programa para determinar a média de uma relação de números dados, sem que se conheça previamente a quantidade deles. Neste caso, não devemos utilizar o comando for, pois não sabemos o número de repetições! Assim, o comando while deve ser utilizado; porém, uma pergunta deve ser formulada: qual a expressão lógica que controlará a estrutura? A solução é "acrescentar" à relação um valor sabidamente diferente dos valores da relação e utilizar este valor para controlar a repetição. Este valor é conhecido como flag. Como dito logo acima, deve- se ter certeza que o flag não consta da relação. Isto não é complicado, pois ao se escrever um programa se tem conhecimento de que valores o programa vai manipular e a escolha do flag fica facilitada. Por exemplo, se o programa vai manipular números positivos pode-se usar -1 para o flag. Além do flag, o programa necessita de uma variável (no caso Cont de contador) que determine a quantidade de números da relação, pois este valor será utilizado no cálculo da média. /*Programa para calcular a media de uma relacao de numeros*/ #include <stdio.h> main() { int Cont; que o anterior. A ideia é a seguinte: x, 2x, 3x, etc. são múltiplos de x. Para se obter o mínimo múltiplo comum basta que se tome o primeiro destes números que seja múltiplo também de y. /*Programa para determinar o mínimo múltiplo comum de dois números positivos*/ #include <stdio.h> main() { int x, y, i, Mmc; printf("Digite os dois numeros \n"); scanf("%d %d", &x, &y); Mmc = x; while (Mmc % y != 0) Mmc = Mmc + x; printf("mmc(%d, %d) = %d \n", x, y, Mmc); } 4.7 Exercícios propostos 1. Mostre a configuração da tela após a execução do programa #include <stdio.h> main() { int i, a, q, Termo; for (i = 5; i > 0; i = i - 1) { a = i; q = 3; Termo = a; while (Termo <= 9 * a) { printf("%d \n", Termo); Termo = Termo * q; } } } 2. Escreva um programa que determine a soma dos quadrados dos n primeiros números naturais, n dado. 3. Escreva um programa para calcular a soma dos n primeiros termos das sequências abaixo, n dado. a) 1 2 3 5 5 8 , , , ...   b) 1 1 2 1 3 1 4 , , , , ...− −   4. O exemplo 10 da seção anterior apresentava uma solução para a questão do mínimo múltiplo comum de simples compreensão. Um problema que esta solução possui é que se o primeiro valor digitado fosse muito menor do que o segundo, o número de repetições necessárias para se chegar ao mmc seria muito grande. Refaça o exemplo, tomando o maior dos números dados como base do raciocínio ali utilizado. 5. Um número inteiro é dito perfeito se o dobro dele é igual à soma de todos os seus divisores. Por exemplo, como os divisores de 6 são 1, 2, 3 e 6 e 1 + 2 + 3 + 6 = 12, 6 é perfeito. A matemática ainda não sabe se a quantidade de números perfeitos é ou não finita. Escreva um programa que liste todos os números perfeitos menores que um inteiro n dado. 6. O número 3.025 possui a seguinte característica: 30 + 25 = 55 e 552 = 3 025. Escreva um programa que escreva todos os números com quatro algarismos que possuem a citada característica. 7. Escreva um programa que escreva todos os pares de números de dois algarismos que apresentam a seguinte propriedade: o produto dos números não se altera se os dígitos são invertidos. Por exemplo, 93x13 = 39x31 = 1.209. 8. Escreva um programa para determinar o número de algarismos de um número inteiro positivo dado. 9. Um número inteiro positivo é dito semiprimo se ele é igual ao produto de dois números primos. Por exemplo, 15 é semiprimo pois 15 = 3 x 5; 9 é semiprimo pois 9 = 3 x 3; 20 não é semiprimo pois 20 = 2 x 10 e 10 não é primo. Os números semiprimos são fundamentais para o sistema de criptografia RSA {Evaristo, J, 2002]. Escreva um programa que verifique se um inteiro dado é semiprimo. 10. Quando um número não é semiprimo, a Matemática prova que ele pode ser escrito de maneira única como um produto de potências de números primos distintos. Este produto é chamado de decomposição em fatores primos do número e os expoentes são chamados de multiplicidade do primo respectivo. Por exemplo, 360 = 23x32x5. Escreva um programa que obtenha a decomposição em fatores primos de um inteiro dado. 11. Escreva um programa que transforme o computador numa urna eletrônica para eleição, em segundo turno, para presidente de um certo país, às quais concorrem os candidatos 83-Alibabá e 93- Alcapone. Cada voto deve ser dado pelo número do candidato, permitindo-se ainda o voto 00 para voto em branco. Qualquer voto diferente dos já citados é considerado nulo; em qualquer situação, o eleitor deve ser consultado quanto à confirmação do seu voto. No final da eleição o programa deve emitir um relatório contendo a votação de cada candidato, a quantidade votos em branco, a quantidade de votos nulos e o candidato eleito. 12. A sequência de Fibbonaci é a sequência (1, 1, 2, 3, 5, 8, 13, ...) definida por a se n ou n a a se nn n n = = = + >    − − 1 1 2 21 2 , , Escreva um programa que determine o n-ésimo termo desta sequência, n dado. 13. A série harmônica S n = + + + + +1 1 2 1 3 1 ... ... é divergente. Isto significa que dado qualquer real k existe n0 tal que 1 1 2 1 3 1 0 + + + + >... n k . Escreva um programa que dado um real k determine o menor inteiro n0 tal que S > k. Por exemplo se k = 2, o programa deve fornecer n0 = 4, pois 1 1 2 1 3 1 4 2 083+ + + = , .... e 1 1 2 1 3 1 8333+ + = , .... 14. Dois números inteiros são ditos amigos se a soma dos divisores de cada um deles (menores que eles) é igual ao outro. Por exemplo, os divisores de 220 são 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 e 110 e 1 + 2 + 4 + 5 + 10 + 11 + 20 + 22 + 44 + 55 + 110 = 284 e os divisores de 284 são 1, 2, 4, 71 e 142 e 1 + 2 + 4 + 71 + 142 = 220. Escreva um programa que determine todos os pares de inteiros amigos menores que um inteiro dado. 15. Escreva um programa que escreva todos os subconjuntos com três elementos do conjunto {1, 2, 3, ..., n}, n dado. 16. Um inteiro positivo x é dito uma potência prima se existem dois inteiros positivos p e k, com p primo, tais que x = pk. Escreva uma função que receba um inteiro e verifique se ele é uma potência prima. 17. Um inteiro positivo x é dito uma potência perfeita de base z e expoente y se existem dois inteiros positivos z e y tais que x = zy. Escreva uma função que receba um inteiro e verifique se ele é uma potência perfeita. Observação Propostas de soluções dos exercícios propostos podem ser solicitadas através de mensagem eletrônica para jaime@ccen.ufal.br com assunto RESPOSTAS LIVRO C, anexando o formulário abaixo devidamente preenchido. Nome Categoria1 Instituição2 Curso2 Cidade/Estado 1Categoria: docente, estudante, autodidata 2Se docente ou estudante 5. Funções e ponteiros 5.1 O que são funções Como dissemos no capítulo 2, um programa em C pode (e deve) ser escrito como um conjunto de funções que são executadas a partir da execução de uma função denominada main(). Cada função pode conter declarações de variáveis, instruções, ativações de funções do sistema e de outras funções definidas pelo programador. Naturalmente, o objetivo de uma função deve ser a realização de alguma "sub-tarefa" específica da tarefa que o programa pretende realizar. Assim, pode-se escrever funções para a leitura dos dados de entrada, para a saída do programa, para a determinação da média de vários elementos, para a troca dos conteúdos de uma variável, para o cálculo do máximo divisor comum de dois números dados, etc. Normalmente, a realização da "sub-tarefa" para a qual a função foi escrita é chamada de retorno da função. Este retorno pode ser a realização de uma ação genérica, como a leitura dos dados de entrada, ou um valor específico, como o cálculo do máximo divisor comum de dois números dados. Como foi dito na seção citada, uma função deve ser definida com a seguinte sintaxe: Tipo de Dado Identificador da função(Lista de parâmetros) { Declaração de variáveis Sequência de instruções } onde, como também já foi dito, o conjunto Tipo de Dado Identificador da função(Lista de parâmetros) é chamado protótipo da função. Aí, Tipo de dado é int, char, float, ou um dos seus "múltiplos", quando a função deve retornar um valor específico e void se a função deve realizar uma ação genérica sem retornar um valor definido. Por seu turno, Lista de parâmetros é um conjunto de variáveis utilizadas para a função receber os valores para os quais a função deve ser executada; estes valores são chamados argumentos, que, comparando uma função com um programa, constituem os dados de entrada da função. Na declaração de variáveis são declaradas as variáveis que as instruções da função vão manipular internamente. Estas variáveis (e os parâmetros da função) só são acessáveis pelas instruções da função e, por esta razão, são chamadas variáveis locais (na seção 5.5 são apresentados maiores detalhes). Se a função deve retornar um valor, uma de suas instruções deve ter a seguinte sintaxe: return (expressão); sendo os parênteses facultativos. A semântica desta instrução é evidente: a expressão é avaliada e o seu valor é retornado à função que a ativou. Além disso (e isto agora não é evidente), a execução desta instrução interrompe a execução da função e o processamento retorna à função que ativou a função em discussão. A ativação (ou chamada) da função por outra função se faz com a referência ao identificador da função seguido dos argumentos em relação aos quais se pretende executar a função. Por exemplo, o cálculo do máximo divisor comum de dois números dados, discutido no capítulo anterior, poderia ser reescrito da seguinte forma: /*Função que retorna o máximo divisor comum de dois números positivos dados*/ int MaxDivCom(int x, int y) { int Resto; Resto = x % y; while (Resto != 0) { x = y; y = Resto; Resto = x % y; } return (y); } /* Programa para ordenar tres numeros dados*/ #include <stdio.h> main() { float x, y, z, Aux; printf("Digite os tres numeros"); scanf("%f %f %f", &x, &y, &z); printf("Numeros dados: %f , %f , %f \n", x, y, z); if ((x > y) || (x > z)) /* verifica se x não é o menor */ if (y < z) /* neste caso y é o menor */ { Aux = x; /* troca os conteúdos de x e de y */ x = y; y = Aux; } else /* neste caso z é o menor */ { Aux = x; /* troca os conteúdos de x e de z */ x = z; z = Aux; } if (y > z) /* verifica se z e y ainda não estão ordenados */ { Aux = y; /* troca o conteúdo de y e de z */ y = z; z = Aux; } printf("Numeros ordenados: %f , %f , %f \n", x, y, z); } Observe que uma sequência de comandos com o mesmo objetivo (trocar os conteúdos de duas variáveis) se repete. Num caso como este poderíamos escrever uma função que realizasse aquela ação pretendida e, então, esta função seria utilizada para substituir a sequência referida. Por enquanto temos o seguinte problema. Se definirmos a função void Troca(float x, float y) { float Aux; Aux = x; x = y; y = Aux; } e a executarmos passando as variáveis a e b, apenas os conteúdos de a e de b serão passados para x e para y e a troca realizada pela função só afeta os conteúdos de x e de y, não modificando os conteúdos de a e de b que é o que se pretendia. Ou seja, a função Troca recebe apenas os "valores" de a e de b e as ações realizadas pela função interfere apenas nos parâmetros x e y, não alterando nada em relação aos argumentos a e b. Neste caso, dizemos que os parâmetros foram passados por valor. O sistema Turbo C++ 3.0 oferece a possibilidade de que a execução de uma função altere conteúdos de variáveis “não locais”. Para isto, no protótipo da função os parâmetros devem ser precedidos de &. Neste caso, os argumentos para ativação da função têm que ser variáveis e qualquer alteração no conteúdo do parâmetro se reflete no conteúdo da variável argumento. Diz-se então que a passagem dos parâmetros é feita por referência. Com este tipo de passagem de parâmetro, o programa acima poderia ser escrito da seguinte forma: /* Programa para ordenar tres numeros dados*/ #include <stdio.h> void Troca(float &a, float &b) { float Aux; Aux = a; a = b; b = Aux; } main() { float x, y, z, Aux; printf("Digite os tres numeros"); scanf("%f %f %f", &x, &y, &z); printf("Numeros dados: %f , %f , %f \n", x, y, z); if ((x > y) || (x > z)) if (y < z) Troca(x, y); else Troca(x, z); if (y > z) Troca(y, z); printf("Numeros ordenados: %f , %f , %f \n", x, y, z); } A passagem de parâmetro por referência permite que entrada de dados seja feita através de uma função. Isto pode ser útil, por exemplo, em programas multitarefas em que o número de entradas pode variar de acordo com a tarefa pretendida. Para exemplificar apresentaremos um programa para tratar números complexos. Naturalmente, um programa com este objetivo deve estar apto a somar e multiplicar complexos, casos em que a entrada será dois números complexos, e a determinar o módulo e a forma polar de um complexo, quando entrada será apenas de um número. Além de exemplificar a entrada de dados através de uma função, o programa abaixo exemplifica um programa multitarefa “completo”. /*Programa para álgebra dos números complexos*/ #include <stdio.h> #include <math.h> void LeComplexo(float &a, float &b) { puts("Digite a parte real <enter> parte imaginaria"); scanf("%f %f", &a, &b); } float Modulo(float a, float b) { return sqrt(a*a + b*b); } void Polar(float a, float b, float &c, float &d) { c = Modulo(a, b); d = asin(b/c); } void Soma(float a, float b, float c, float d, float &e, float &f) { e = a + c; f = b + d; } void Produto(float a, float b, float c, float d, float &e, float &f) { e = a*c - b*d; f = a*d + b*c; } void Menu() { puts("1-Modulo \n 2- Forma polar \n 3-Soma \n 4-Produto \n 5-Encerra \n Digite sua opcao: "); } main() { float x, y, z, w, t, u; int Opc; Menu(); scanf("%d", &Opc); switch (Opc) { case 1: LeComplexo(x, y); z = Modulo(x, y); printf("|%.2f + %.2fi| = %.2f", x, y, z); break; case 2: LeComplexo(x, y); Polar(x, y, z, w); printf("%.2f + %.2fi = %.2f(cos%.2f + isen%.2f)", x, y, z, w, w); break; case 3: LeComplexo(x, y); LeComplexo(z, w); Soma(x, y, z, w, t, u); printf("(%.2f + %.2fi) + (%.2f + %.2fi) = %.2f + %.2fi", x, y, z, w, t, u); break; case 4: LeComplexo(x, y); LeComplexo(z, w); Produto(x, y, z, w, t, u); printf("(%.2f + %.2fi) + (%.2f + %.2fi) = %.2f + %.2fi", x, y, z, w, t, u); break; } } O exemplo a seguir melhora sobremaneira a legibilidade do programa (parte dele) que determina o dia da semana de uma data posterior ao ano de 1600 dada apresentado no capítulo 3. Lá precisávamos determinar o número de dias decorridos entre 01/01/1600 e a data dada. Vimos que precisávamos determinar, entre outras coisas: o número de dias já decorridos no ano da data dada (para isto precisávamos determinar se tal ano era bissexto e o número de dias 31 já ocorridos) e a quantidade de anos bissextos entre 1600 e o ano da data dada. A boa técnica de programação sugere que cada ação parcial do programa seja executada por uma função. Temos então a seguinte proposta para um programa que determine o número de dias dias decorridos entre duas datas dadas (este programa é utilizado em aposentadorias: pela legislação atual (novembro de 2008) um trabalhador de uma empresa privada adquire o direito de se aposentar quando completa 35 anos de serviço, sendo este cálculo a partir da soma do número de dias trabalhados nas (possivelmente) várias empresas nas quais o interessado trabalhou). /*Programa para determinar o número de dias entre duas datas dadas*/ #include <conio.h> #include <stdio.h> #include <conio.h> Temos então um programa - certamente um pouco sofisticado - para permutar os conteúdos de duas variáveis. Qualquer que seja o tipo de variável apontada por um ponteiro, pode-se "atribuir-lhe" a constante 0 (zero) e pode-se comparar um ponteiro com esta constante. O sistema oferece uma constante simbólica NULL que pode (e normalmente o é) ser utilizado no lugar do zero para, mnemonicamente, indicar mais claramente que este é um valor especial para um ponteiro. Como veremos daqui por diante, este valor especial de ponteiro será utilizado para inicializações de ponteiros, para valores de escape de estruturas de repetição e para retorno de funções quando alguma ação pretendida não é conseguida. 5.5 Passagem de parâmetros por referência no Turbo C 2.01 A utilização de ponteiros como parâmetros de funções permite a passagem de parâmetros por referência no Turbo C 2.01. Considere um parâmetro p do tipo ponteiro. Como p armazenará um endereço, se for passado para p o endereço de uma variável que possa ser apontada por ele, qualquer ação realizada no ponteiro afetará o conteúdo da variável. O caso da função Troca(), comentada na seção anterior, poderia ser definida da seguinte forma: void troca(float *a, float *b) { float Aux; Aux = *a; *a = *b; *b = Aux; } e suas ativações deveriam ser feitas através de Troca(&x, &y). 5.6 Uma urna eletrônica A passagem de parâmetros por referência também é muito útil quando se pretende que uma função retorne mais de um valor. Um destes valores pode ser retornado pelo comando return() e os demais podem ser retornados para variáveis que foram passadas por referência para parâmetros da função. O exemplo abaixo, uma melhor resposta de um exercício proposto no capítulo anterior, transforma um computador numa urna eletrônica para a eleição, em segundo turno, para a presidência de um certo país, às quais concorrem dois candidatos Alibabá, de número 89, e Alcapone, de número 93, sendo permitido ainda o voto em branco (número 99) e considerando como voto nulo qualquer voto diferente dos anteriores. A função Confirma() deve retornar dois valores: o primeiro para, no caso de confirmação do voto, permitir sua contabilização e o segundo para, ainda no caso de confirmação do voto, interromper a estrutura do while, o que permitirá a recepção do voto seguinte. Observe também a passagem por referência do parâmetro da função ComputaVoto(). Há necessidade de que seja desta forma, pelo fato de que esta função alterará conteúdos de variáveis diferentes. #include <stdio.h> #include <ctype.h> #include <dos.h> #include <conio.h> /*Função para confirmação do voto*/ int Confirma(char *s, char *p) { int r; char Conf; printf("Voce votou em %s! Confirma seu voto (SN)? ", s); fflush(stdin); scanf("%c", &Conf); if (toupper(Conf) == 'S') { *p = 's'; r = 1; } else { *p = 'n'; printf("\a Vote de novo: "); sound(1000); delay(80000); nosound(); r = 0; } return r; } /*Função para computar cada voto confirmado para o candidato*/ void ComputaVoto(int *p) { *p = *p + 1; } /*Função principal*/ main() { int Alibaba, Alcapone, Nulos, Brancos, Eleitores, Voto; char Sim, Cont; clrscr(); Alibaba = Alcapone = Nulos = Brancos = 0; do { do { printf(" 89 - Alibaba \n 93 - Alcapone \n 99 - Branco \n"); printf("Digite seu voto: "); scanf("%d", &Voto); switch (Voto) { case 89: if (Confirma("Alibaba", &Sim) == 1) ComputaVoto(&Alibaba); break; case 93: if (Confirma("Alcapone", &Sim) == 1) ComputaVoto(&Alcapone); break; case 99: if (Confirma("Brancos", &Sim) == 1) ComputaVoto(&Brancos); break; default: if (Confirma("Nulo", &Sim) == 1) ComputaVoto(&Nulos); break; } clrscr(); } while (Sim != 's'); printf("Outro eleitor (S/N)? "); fflush(stdin); scanf("%c", &Cont); } while (toupper(Cont) == 'S'); Eleitores = Alibaba + Alcapone + Brancos + Nulos; printf("Total de eleitores %d \n Alibaba %d \n Alcapone %d \n Brancos %d \n Nulos %d", Eleitores, Alibaba, Alcapone, Brancos, Nulos); } O arquivo de cabeçalhos dos.h contém as funções sound(n), nosound() e delay(n). A primeira emite um som de frequência n hertz; a segunda interrompe a emissão de som e a terceira suspende a execução do programa por n milissegundos. A razão da chamada da função fflush() é a seguinte. Em alguns sistemas, quando algum dado de entrada é digitado para execução da função scanf(), os compiladores C não o armazena diretamente na posição de memória respectiva, armazenando-o inicialmente numa região chamada buffer para, ao final da execução da função de leitura transferir o conteúdo do buffer para a memória. Se quando da execução de uma função de leitura o conteúdo do buffer não estiver vazio, é este conteúdo (naturalmente, indesejado) que será armazenado na variável. A ativação de fflush(stdin) "descarrega" todo o buffer dos dados digitados no teclado e assim a função de leitura aguardará que o dado realmente pretendido seja digitado. É prudente, portanto, preceder leituras de caracteres e de cadeias de caracteres pela chamada de fflush(stdin). Observe que um ponteiro do tipo char é capaz de “armazenar” uma cadeia de caracteres (mais detalhes no capítulo 8). Observe também que utilizamos inicializações sucessivas no comando Alibaba = Alcapone = Nulos = Brancos = 0. Esta forma de inicializar variáveis também é válida, mas não será muito utilizada neste livro. Para concluir (por enquanto) o estudo de ponteiros, vale ressalvar que, sendo variáveis capazes de receber endereços (portanto, valores do tipo int) pode-se somar e subtrair ponteiros. No capítulo 7, veremos um exemplo onde isto será útil. 5.7 Recursividade Algumas funções matemáticas podem ser estabelecidas de tal forma que as suas definições utilizem, de modo recorrente, a própria função que se está definindo. Um exemplo trivial (no bom sentido) de um caso como este é a função fatorial. No ensino médio aprendemos que o fatorial de um número natural n é o produto de todos os números naturais de 1 até o referido n, ou seja, n! = 1 . 2 . 3 . ... . n. Como mostra o exemplo abaixo, é muito simples se escrever uma função (função iterativa) que calcule o fatorial de n: basta se inicializar um variável com 1 e, numa estrutura de repetição, calcular os produtos 1 x 2 = 2, 2 x 3 = 6; 6 x 4 = 24; 24 x 5 = 120; ...; etc., até multiplicar todos os naturais até n. long int Fatorial(int n) { long int Fat; int i; Fat = 1; for (i = 2; i <= n; i = i + 1) Fat = Fat * i; return (Fat); } Embora o conceito anterior seja de simples compreensão, é matematicamente mais elegante definir o fatorial de um natural n por    >− == = 1,)!1(. 10,1 ! nsenn nounse n 7. origem → destino Observe que os três movimentos iniciais transferem dois discos da torre origem para a torre auxiliar, utilizando a torre destino como auxiliar; o quarto movimento transfere o maior dos discos da origem para destino e os últimos movimentos transferem os dois discos que estão na auxiliar para destino utilizando origem como torre auxiliar. Assim, a operação Move(3, origem, auxiliar, destino) - move três discos da origem para destino usando auxiliar como torre auxiliar - pode ser decomposta em três etapas: 1) Move(2, origem, destino, auxiliar) - move dois discos de origem para auxiliar usando destino como auxiliar; 2) Move um disco de origem para destino 3) Move(2, auxiliar, origem, destino) - move dois discos de auxiliar para destino usando origem como auxiliar. O interessante é que é fácil mostrar que este raciocínio se generaliza para n discos, de modo que a operação Move(n, a, b, c) pode ser obtida com as seguintes operações: 1) Move(n-1, a, c, b) 2) Move um disco de a para c 3) Move(n-1, b, a, c) O mais interessante ainda é que isto pode ser implementado em C, através do seguinte programa: /* Programa que implementa o jogo Torre de Hanoi*/ #include <stdio.h> void MoveDisco(char t1[10], char t2[10]) { printf("%s --> %s \n", t1, t2); } void Hanoi(int x, char o[10], char a[10], char d[10]) { if (x > 0) { Hanoi(x - 1, o, d, a); MoveDisco(o, d); Hanoi(x - 1, a, o, d); } } main() { int n; printf("Digite o numero de discos \n"); scanf("%d", &n); Hanoi(n, "origem", "auxiliar", "destino"); } 5.8 Usando funções de outros arquivos Os compiladores C permitem que um programa utilize funções definidas em outros programas. Basta que o referido programa seja incluído na instrução #include "NomeArquivo". Por exemplo, imagine que a declaração de variáveis e a função abaixo #include <stdio.h> int x, y; int MaxDivCom(int a, int b) { int Resto; Resto = a % b; while (Resto != 0) { a = b; b = Resto; Resto = a % b; } return b; } estejam num arquivo mdc.c. Como a matemática prova que o produto de dois números inteiros é igual ao produto do máximo divisor comum dos números pelo mínimo múltiplo comum, a função MaxDivCom() poderia ser utilizada para se escrever um programa para o cálculo do mínimo múltiplo comum de dois números dados. Este programa poderia utilizar as variáveis x, y e Mdc declaradas no arquivo mdc.c. Teríamos então o seguinte programa: /*Programa que calcula o minimo multiplo comum de dois numeros utilizando uma função definida em outro arquivo*/ #include <stdio.h> #include "mdc.c" main() { int m, Mmc; printf("Digite os numeros "); scanf("%d %d", &x, &y); m = MaxDivCom(x, y); Mmc = (x * y)/m; printf("Mmc(%d, %d) = %d \n", x, y, Mmc); } A referência ao arquivo que vai ser “incluído” no programa pode ser feita com o caminho do arquivo escrito entre aspas ou com o nome do arquivo entre < >, se ele está gravado na pasta padrão dos arquivos de cabeçalho .h. 5.9 "Tipos" de variáveis Variáveis locais Como foi dito na seção 5.1, as variáveis declaradas no interior de uma função (variáveis locais, para lembrar) só são acessáveis por instruções desta função. Na realidade, elas só existem durante a execução da função: são "criadas" quando a função é ativada e são "destruídas" quando termina a execução da função. Por esta última razão, variáveis locais também são chamadas variáveis automáticas e variáveis dinâmicas. Também são variáveis locais os parâmetros da função. Isto explica a necessidade de declará-los, definindo-se seus identificadores e seus tipos de dados. Variáveis globais e o modificador extern Se uma variável deve ser acessada por mais de uma função ela deve ser declarada fora de qualquer função, sendo chamada, neste caso, de variável global. Uma variável global pode ser referenciada em qualquer função do programa e, embora isto não seja aconselhável, pode-se identificar uma variável local com o mesmo identificador de uma variável global. Neste caso, referências ao identificador comum dentro da função na qual a variável local foi definida refere-se a esta variável local. Isto, naturalmente, impede a função de acessar a variável global. O modificador de variável extern permite que um programa utilize variáveis definidas e inicializadas em funções de um outro arquivo que tenha sido "incluído" através da instrução #include "NomeArquivo", como visto na seção 5.8. No capítulo seguinte apresentaremos um exemplo bastante esclarecedor do uso do modificador extern. Variáveis estáticas Como uma variável local deixa de existir quando se encerra a execução da função, o último valor nela armazenado é perdido. Pode ocorrer que o último valor armazenado numa variável local seja necessário para uma chamada subsequente da função. Após a execução de uma função, o último valor armazenado numa variável local pode ser preservado para ser utilizado numa chamada posterior da função através do modificador de variável static. No Turbo C 2.01 e no Turbo C++ 3.0, uma variável local static deve ser inicializada por um valor constante ou o endereço de uma variável global, quando da sua declaração. Neste caso, considerando que receberá endereços, uma variável static deve ser definida como um ponteiro. Por exemplo, o programa abaixo utiliza uma variável estática para guardar o valor de Termo em cada ativação da função GeraPA() para, com este valor, obter o valor do termo seguinte. #include <stdio.h> #include <conio.h> int a1; int GeraPA(int r) { static int *Termo = &a1; *Termo = *Termo + r; return (*Termo); } main() { int i, Razao; clrscr(); printf("Digite o primeiro termo e a razÆo: "); scanf("%d %d", &a1, &Razao); printf("Progressao Aritmetica de primeiro termo %d e razao %d: \n%d ", a1, Razao, a1); for (i = 1; i <= 9; i = i + 1) printf("%d ", GeraPA(Razao)); } Naturalmente, concordo com o leitor que está pensando que existem programas para geração de progressões aritméticas bem mais simples. Uma outra aplicação de variáveis estáticas pode ser obtida numa função que determine a decomposição em fatores primos de um inteiro dado, exercício proposto no capítulo anterior. Um algoritmo para determinar a decomposição em fatores primos de um inteiro é efetuar divisões sucessivas pelos primos divisores, sendo o número de divisões pelo primo realizadas a sua multiplicidade. Por exemplo, para se obter a decomposição de 1.400 teríamos 1400 2 700 2 350 2 175 5 35 5 7 7 1 1400 = 23x52x7 As funções abaixo, a primeira iterativa e a segunda recursiva, retornam a decomposição de um inteiro passado para o parâmetro n. void DecompFatores(int n) { int d, m; d = 2; Nome Categoria1 Instituição2 Curso2 Cidade/Estado 1Categoria: docente, estudante, autodidata 2Se docente ou estudante 6 Vetores 6.1 O que são vetores No exemplo 6 da seção 4.6 discutimos uma função para a determinação da média de uma relação de números dados. Para tal, utilizamos uma variável simples para receber os números, sendo que cada vez que um número, a partir do segundo, era recebido o anterior era "perdido". Ou seja, a relação de números não era armazenada. Imagine que a relação fosse uma relação de notas escolares e além da média se quisesse também saber a quantidade de alunos que obtiveram nota acima da média ou uma outra medida estatística (desvio médio, por exemplo) que dependesse da média. Neste caso, haveria a necessidade de que a relação fosse redigitada o que, além da duplicidade do trabalho, facilitaria os erros de digitação. É importante então que exista uma "variável" capaz de armazenar vários valores simultaneamente de tal forma que se possa acessar cada um deles independentemente de se acessar os demais. Um outro exemplo é o caso do exemplo 2 da seção 4.6. Lá queríamos a relação dos divisores de um inteiro dado e estes divisores eram apenas exibidos, não sendo armazenados, como recomendado na seção 2.9. Até aquele momento, a dificuldade de se armazenar os divisores residia no fato de que não se sabe a priori o número de divisores de um inteiro dado e, portanto, não saberíamos quantas variáveis deveríamos declarar. Um vetor é um conjunto de variáveis de um mesmo tipo de dado as quais são acessadas e referenciadas através da aposição de índices ao identificador do vetor. 6.2 Declaração de um vetor unidimensional Um vetor unidimensional (ou simplesmente vetor) é declarado através da seguinte sintaxe: Tipo de dado Identificador[n]; onde Tipo de dado fixará o tipo de dado das variáveis componentes do vetor e n indicará o número das tais componentes. Por exemplo, a declaração int Vetor[10] definirá um conjunto de dez variáveis do tipo int, enquanto que a declaração char Cadeia[100] definirá um conjunto de cem variáveis do tipo char. Como cada variável do tipo int utiliza dois bytes de memória e cada variável do tipo char utiliza apenas um byte, a variável Vetor ocupará vinte bytes de memória enquanto que a variável Cadeia ocupará cem bytes. Os compiladores C possuem uma função de biblioteca, sizeof(), que retorna o número de bytes ocupado por uma variável ou por um vetor. Por exemplo, o programa #include <stdio.h> main() { float x; int v[30]; printf("Numero de bytes de x = %d \nNumero de bytes de v = %d \n", sizeof(x), sizeof(v)); } exibirá na tela a seguinte saída: Numero de bytes de x = 4 Numero de bytes de v = 60 Cada componente de um vetor pode ser acessada e referenciada através de índices associados ao identificador do vetor, sendo o índice da primeira componente igual a zero. Assim, as componentes do vetor Cadeia do exemplo acima serão identificadas por Cadeia[0], Cadeia[1], ..., Cadeia[99]. O índice de uma componente pode ser referido através de uma expressão que resulte num valor inteiro. Por exemplo, a sequência de comandos int i, Quadrados[100]; for (i = 1; i <= 100; i = i + 1) Quadrados[i - 1] = i * i; armazena no vetor Quadrados os quadrados dos cem primeiros inteiros positivos. Uma coisa em que o programador em C deve se preocupar é o fato de que os compiladores C não verificam se os valores atribuídos a um índice estão dentro dos limites definidos na declaração do vetor. Se os limites não forem obedecidos, podem ocorrer erros de lógica na execução programa ou conflitos com o sistema operacional (provocando, até mesmo, travamento no sistema). 6.3 Vetores e ponteiros É muito importante observar que o identificador de um vetor em C é, na verdade, um ponteiro que aponta para o primeiro elemento do vetor. Quando se declara int v[10]; está se reservando um conjunto de dez posições de memória contíguas, cada uma delas com dois bytes, como mostra a figura abaixo: v v[0] v[1] v[9] A partir daí, qualquer referência ao identificador v é uma referência ao endereço da componente v[0], de tal forma que, se tivermos a declaração int *p; os comandos p = &v[0]; p = v; executam a mesma ação: armazenam no ponteiro p o endereço de v[0]. Sendo um ponteiro que aponta para sua primeira componente, um vetor pode ser um parâmetro de uma função, podendo receber endereços. Dessa forma, qualquer ação realizada no vetor afetará o conteúdo do vetor passado como argumento; ou seja, a passagem de parâmetros do tipo vetor é sempre feita por referência. Esta observação também justifica a possibilidade de que parâmetros do tipo vetor sejam declarados como um ponteiro: void funcao(int *v). 6.4 Lendo e escrevendo um vetor Como foi dito na seção 6.1, vetores servem para armazenar uma relação de dados do mesmo tipo. Uma função para fazer este armazenamento depende do conhecimento ou não da quantidade de elementos da relação. Na hipótese do número de elementos da relação ser conhecido, basta usar uma função, do tipo void, com dois parâmetros: um para receber o vetor que vai armazenar a relação e outro para receber a quantidade de elementos da relação. Dentro da função, pode-se utilizar um comando for. #include <stdio.h> void ArmazenaRelacaoN(int *v, int t) { int i; printf("Digite os elementos da relacao \n"); for (i = 0; i < t; i++) scanf("%d", &v[i]); } Se o número de elementos da relação não é conhecido a priori, deve-se utilizar um flag para encerrar a entrada dos dados, de acordo com o que foi comentado na seção 4.6. É importante que a função, para utilizações posteriores, determine o número de elementos da relação. Este valor pode ser retornado através valor da posição onde foi encontrada uma componente maior do que Maior. float MaiorElemento(float *v, int t, int *p) { int i; float Maior; Maior = v[0]; *p = 0; for (i = 1; i < t; i = i + 1) if (v[i] > Maior) { Maior = v[i]; *p = i; } return(Maior); } Uma chamada desta função vai requerer, além do vetor p onde está armazenada a relação, duas variáveis, digamos Maior e Pos; nestas condições a ativação da função será feita através do comando Maior = MaiorElemento(p, Quant, &Pos); onde p é o vetor onde está armazenada a relação, Quant é a quantidade de elementos da relação e &Pos é o endereço da variável que irá armazenar a posição da ocorrência do maior elemento procurado. Vale ressaltar que, na função acima, adotamos a passagem por referência "formato Turbo C 2.01" que também funciona no Turbo C++ 3.0. 4. O exemplo a seguir tem o objetivo de mostrar que o índice de acesso às componentes de um vetor pode ser dado através de expressões, como já foi dito anteriormente. O exemplo mostra uma função que, recebendo dois vetores com a mesma quantidade de elementos, gera um vetor intercalando as componentes dos vetores dados. Assim se v1 = {4, 8, 1, 9} e v2 = {2, 5, 7, 3} a função deve gerar o vetor v = {4, 2, 8, 5, 1, 7, 9, 3}. Observe que as componentes ímpares de v são os elementos de v1 e as componentes pares são os elementos de v2. void IntercalaVetor(float *v1, float *v2, float *v, int t) { int i; for (i = 0; i < 2 * t; i = i + 1) if (i % 2 == 1) v[i] = v2[(i - 1)/2]; else v[i] = v1[i/2]; } 5. Agora apresentaremos um exemplo que mostra um vetor cujas componentes são cadeias de caracteres, além de mostrar como um vetor de caracteres pode ser inicializado. Trata-se de uma função que retorna o nome do mês correspondente a um número dado, que poderia ser usada num programa que escrevesse por extenso uma data da no formato dd/mm/aaaa. char *NomeMes(int n) { char *Nome[13] = {"Mes ilegal", "Janeiro", "Fevereiro", "Marco", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"}; if ((n > 0) && (n < 13)) return(Nome[n]); else return(Nome[0]); } Observe que a inicialização do vetor se faz quando da sua declaração, com a enumeração dos valores das componentes entre chaves, como a matemática faz com conjuntos. Observe também que a função NomeMes() retorna um ponteiro para uma variável do tipo char, o que significa que retornará uma cadeia de caracteres (uma string), pois, como veremos no próximo capítulo, uma string é um vetor de caracteres. Este fato também é indicado no vetor Nome que é um vetor de ponteiros para variáveis do tipo char. 6. Este exemplo mostra uma situação em que há a necessidade de vários vetores. Trata-se de um programa para administrar os pedidos de uma lanchonete, cujo cardápio é o seguinte: Codigo Produto Preço 101 Refrigerante 1.20 102 Suco 1.00 103 Sanduíche 2.50 104 Salgado 1.00 105 Torta 2.00 Uma possível solução é considerar três vetores globais, inicializados como no exemplo anterior, de tal forma que seja possível para cada pedido gerar vetores com os códigos dos produtos solicitados e, a partir daí, se possa armazenar as quantidades de cada item do pedido. #include <stdio.h> #include <conio.h> int Cod[5] = {101, 102, 103, 104, 105}; char *Prod[5] = {"Refrigerante", "Suco", "Sanduiche", "Salgado", "Torta"}; float Precos[5] = {1.20, 1.00, 2.50, 1.00, 2.00}; int Pedido(int *c, int *q, float *v) { int i = 0; do { do { puts("Código (100 para encerrar o pedido):"); scanf("%d", &c[i]); if (c[i] < 100 || c[i] > 105) printf("\a Codigo invalido"); } while (c[i] < 100 || c[i] > 105); if (c[i] != 100) { puts("Quantidade"); scanf("%d", &q[i]); v[i] = q[i]*Precos[c[i] - 101]; i++; } } while (c[i] != 100); return i; } void ExibePedido(int *c, int *q, float *v, int t) { int i; float Total = 0.0; clrscr(); puts("Código Discriminação Quantidade Valor \n"); for (i = 0; i < t; i++) { printf("%d %s %10.d %10.2f\n", Cod[c[i] - 101], Prod[c[i] - 101], q[i], v[i]); Total = Total + v[i]; } printf("\nValor total do pedido: %.2f\n\n", Total); } main() { int NumItens, *Itens, *Quant; float *Valor; char S = 'n'; do { clrscr(); NumItens = Pedido(Itens, Quant, Valor); if (NumItens != 0) ExibePedido(Itens, Quant, Valor, NumItens); puts("Outro pedido (S/N)?"); fflush(stdin); scanf("%c", &S); } while (toupper(S) != 'N'); } 7. Mostraremos agora um exemplo cujo objetivo é desenvolver o raciocínio recursivo utilizando vetores. Trata-se de uma função recursiva para a determinação da soma das componentes de um vetor, coisa já feita iterativamente no interior da função Media() desenvolvida no exemplo 1. Ora, como uma função que trata vetores recebe sempre o vetor e o seu tamanho, podemos recursivamente “diminuir” o tamanho do vetor através do decremento do parâmetro respectivo até que o vetor “tenha apenas uma componente”, quando então a soma das componentes se reduzirá a esta “única” componente. /*Função recursiva que retorna a soma das componentes de um vetor*/ int SomaCompRec(float *v, int t) { if (t == 1) return v[i – 1]; else return v[t - 1] + SomaComRec(v, t – 1); } 6.6 Vetores multidimensionais Na seção 6.1 foi dito que um vetor é um conjunto de variáveis de mesmo tipo, chamadas componentes do vetor. A linguagem C permite que as componentes de um vetor sejam também vetores, admitindo que se armazene uma matriz da matemática, uma tabela de dupla entrada que, por exemplo, enumere as distâncias entre as capitais brasileiras ou um livro considerando a página, a linha e a coluna em que cada caractere se localiza. A declaração de um vetor multidimensional é uma extensão natural da declaração de um vetor unidimensional: Tipo de dado Identificador[n1][n2] ... [nk]; onde k indica a dimensão e n1, n2, ..., nk indicam o número de componentes em cada dimensão. Por exemplo, a declaração int Mat[10][8]; define um vetor de dez componentes, cada uma delas sendo um vetor de oito componentes. Ou seja, Mat é um conjunto de 10 x 8 = 80 variáveis do tipo int. Para um outro exemplo, a declaração char Livro[72][30][30]; define uma variável capaz de armazenar os caracteres de um livro com até setenta duas páginas, cada página possuindo trinta linhas e cada linha possuindo trinta colunas. A inicialização de um vetor multidimensional também segue o padrão da inicialização de um vetor unidimensional, com a ressalva de que as componentes, que agora são vetores, devem estar entre chaves. Por exemplo, se quiséssemos um vetor bidimensional para armazenar os números de dias dos meses do ano, fazendo a distinção entre anos bissextos e não bissextos poderíamos declarar e inicializar um vetor I3 1 0 0 0 1 0 0 0 1 =           void GeraMatrizUnidade(int Mat[10][10], int m) { int i, j; for (i = 0; i < m; i = i + 1) for (j = 0; j < m; j = j + 1) if (i == j) Mat[i][j] = 1; else Mat[i][j] = 0; } 3. Quando o número de linhas de uma matriz é igual ao número de colunas a matriz é dita matriz quadrada. Neste caso, os elementos de índices iguais constituem a diagonal principal. A soma dos elementos da diagonal principal de uma matriz quadrada é o traço da matriz. Como mais um exemplo de programas que manipulam matrizes, a função abaixo determina o traço de uma matriz quadrada dada. Observe que para percorrer a diagonal principal não há necessidade de um duplo for. float Traco(float Mat[10][10], int m, int n) { int i; float Tr; Tr = 0; if (m == n) { for (i = 0; i < m; i = i + 1) Tr = Tr + Mat[i][i]; return(Tr); } else printf("A matriz nao e quadrada"); } 4. Uma tabela que enumere as distâncias entre várias cidades é uma matriz simétrica: os termos simétricos em relação à diagonal principal são iguais, ou seja Mat[i][j] = Mat[j][i]. Obviamente, a digitação de uma matriz com esta propriedade pode ser simplificada, devendo-se digitar apenas os termos que estão acima da diagonal principal. void ArmazenaMatrizSimetrica(float Mat[10][10], int m) { int i, j; printf("Digite, por linha, os elementos da matriz, a partir da diagonal"); for (i = 0; i < m; i = i + 1) for (j = i; j < m; j = j + 1) { scanf("%f", &Mat[i][j]); Mat[j][i] = Mat[i][j]; } } Observe que a inicialização de j no segundo comando for foi com o valor de cada i do primeiro. A razão disto é que só serão digitados os termos acima da diagonal principal, termos em que j ≥ i. 5. Nos exemplos anteriores, sempre "percorremos a matriz pelos elementos de suas linhas". O próximo exemplo mostra um caso em que é necessário percorrer as colunas. Trata-se de uma questão muito comum da totalização das colunas de uma tabela. void TotalizaColunas(float Mat[10][10], int m, int n) { int i, j; for (j = 0; j < n; j = j + 1) { Mat[m][j] = 0; for (i = 0; i < m; i = i + 1) Mat[m][j] = Mat[m][j] + Mat[i][j]; } } 6.8 Uma aplicação esportiva Nesta seção, apresentaremos um programa para administrar o placar de um set de um jogo de vôlei de praia. De acordo com as regras em vigoravam nas Olimpíadas de Pequim (2008), para uma equipe vencer um set de uma partida ela deveria obter um mínimo de 21 pontos para os sets “normais” ou de 15 pontos para um set de desempate, desde que a diferença entre sua pontuação e a do adversário fosse superior ou igual a dois. #include <stdio.h> #include <conio.h> #include <math.h> void MostraPlacar(char *Time1, char *Time2, int Pontos1, int Pontos2) { printf("%20s %2d x %2d %-20s\n", Time1, Pontos1, Pontos2, Time2); } void VerificaMudanca(int Pontos1, int Pontos2, int mud) { if ( (Pontos1+Pontos2)%mud == 0) { puts("Atencao! mudanca de quadra! Digite uma tecla para continuar" ); getch(); } } void FimdeSet(char *Time1, char*Time2, int Pontos1, int Pontos2) { puts("FIM DE SET!"); if(Pontos1>Pontos2) printf("%s",Time1); else printf("%s" ,Time2); puts(" ganhou o set!"); puts("Placar final: "); MostraPlacar(Time1,Time2,Pontos1,Pontos2); } void main() { char *Nome[2]; int Equipe1[200], Equipe2[200]; int Set, Mudanca, Saque, Ponto, Dif; clrscr(); puts("Digite os nomes dos paises:"); gets(Nome[0]); flushall(); gets(Nome[1]); puts("Digite a quantidade de pontos do set (15/21):"); scanf("%d",&Set); Mudanca = Set/3; Equipe1[0] = 0; Equipe2[0] = 0; Saque = 0; clrscr(); do { /* Exibe o placar atual */ puts("Placar atual:"); MostraPlacar(Nome[0], Nome[1], Equipe1[Saque], Equipe2[Saque]); if (Saque != 0) VerificaMudanca(Equipe1[Saque], Equipe2[Saque], Mudanca); Saque++; puts("Digite a equipe que marcou ponto (1/2):" ); scanf("%d",&Ponto); if (Ponto==1) { Equipe1[Saque] = Equipe1[Saque-1]+1; Equipe2[Saque] = Equipe2[Saque-1]; } else { Equipe1[Saque] = Equipe1[Saque-1]; Equipe2[Saque] = Equipe2[Saque-1]+1; } Dif = abs(Equipe1[Saque] - Equipe2[Saque]); clrscr(); } while(((Equipe1[Saque]<Set) && (Equipe2[Saque] < Set)) || (Dif < 2) ); FimdeSet(Nome[0],Nome[1],Equipe1[Saque],Equipe2[Saque]); getch(); } 6.9 Exercícios propostos 0. Escreva uma função recursiva que retorne o maior elemento de um vetor. 1. Escreva uma função que exiba as componentes de um vetor na ordem inversa daquela em que foram armazenadas. 2. Um vetor é palíndromo se ele não se altera quando as posições das componentes são invertidas. Por exemplo, o vetor v = {1, 3, 5, 2, 2, 5, 3, 1} é palíndromo. Escreva uma função que verifique se um vetor é palíndromo. 3. Escreva uma função que receba um vetor e o decomponha em dois outros vetores, um contendo as componentes de ordem ímpar e o outro contendo as componentes de ordem par. Por exemplo, se o vetor dado for v = {3, 5, 6, 8, 1, 4, 2, 3, 7}, o vetor deve gerar os vetores u = {3, 6, 1, 2, 7} e w = {5, 8, 4, 3}. 4. Escreva uma função que decomponha um vetor de inteiros em dois outros vetores, um contendo as componentes de valor ímpar e o outro contendo as componentes de valor par. Por exemplo, se o vetor dado for v = {3, 5, 6, 8, 1, 4, 2, 3, 7} a função deve gerar os vetores u = {3, 5, 1, 3, 7} e w = {6, 8, 4, 2}. 5. Um vetor do Rn é uma n-upla de números reais v = {x1, x2, ..., xn}, sendo cada xi chamado de componente. A norma de um vetor v = {x1, x2, ..., xn} é definida por x x xn1 2 2 2 2+ + +... . Escreva uma função que receba um vetor do Rn, n dado, e forneça sua norma. 6. O produto escalar de dois vetores do Rn é a soma dos produtos das componentes correspondentes. Isto e, se u = {x1, x2, ..., xn} e v = {y1, y2, ..., yn}, o produto escalar é x1.y1 + x2.y2 ... + xn.yn. Escreva uma função que receba dois vetores do Rn, n dado, e forneça o produto escalar deles. 7. A amplitude de uma relação de números reais é a diferença entre o maior e o menor valores da relação. Por exemplo, a amplitude da relação 5, 7, 15, 2, 23 21, 3, 6 é 23 - 2 = 21. Escreva uma função que receba uma relação de números e forneça sua amplitude. 8. O desvio padrão de uma relação de números reais é a raiz quadrada da média aritmética dos quadrados dos desvios (ver exemplo 2, seção 6.4). Escreva uma função que receba uma relação de números
Docsity logo



Copyright © 2024 Ladybird Srl - Via Leonardo da Vinci 16, 10126, Torino, Italy - VAT 10816460017 - All rights reserved