Baixe o app para aproveitar ainda mais
Prévia do material em texto
Versão antiga Em breve, versão revista L I N G U A G E M C Otton Teixeira da Silveira Filho Registro N° 70.630 Biblioteca Nacional - Escritório de Direitos Autorais 1997 Números de página SUMÁRIO 1)Introdução 2)C, A linguagem 2.1) Identificadores 1 2.1) Palavras-chave 3 2.2) Tipos de variáveis 4 2.3) Modificadores 4 3)Operadores 4 de atribuição 6 vírgula 6 aritméticos 6 de incremento e decremento 7 de endereço 8 de bit 8 lógicos 8 relacionais 9 Precedência de operadores 9 Abreviação de operações 10 Conversão de tipos 11 4) Constantes 12 5)Controle de fluxo 13 if-else 13 ? : 13 switch-case-default 13 goto 14 6)Laços 15 while 15 do-while 15 for 15 Palavras auxiliares 16 break 16 continue 16 7) Fatos e mitos - Velocidade de processamento17 8)Programas em C 18 7.1) Denteação 24 9)Funções 25 Forma clássica Variáveis Globais, Locais e Automáticas28 Um Comando do Preprocessador, cabeçalhos e arquivos Departamento de Ciência da Computação-UFF Números de página de inclusão 29 Algumas funções úteis 31 10) Ponteiros 36 Ponteiros e funções 36 Ponteiros e vetores 39 Aritmética de Ponteiros 42 Inicializando vetores 43 Ponteiros para funções 44 Mais Sobre Funções 46 11) O Preprocessador O comando #define (do preprocessador)51 Mais comandos do preprocessador 54 12) Vetores Multidimensionais 56 13) Vetores de Ponteiros e Ponteiros de Vetores 58 Ponteiros de Ponteiros & Ponteiros de Ponteiros de Ponteiros e etc...59 10) Funções 60 Em notação moderna Funções em Forma Moderna 60 Mais Algumas Funções 61 11) main() com parâmetros 63 12) Modificadores 65 Variáveis estáticas 65 Variáveis externas 67 Variáveis registrador 67 13) Reservando memória para uso temporário 69 Fragmentação 70 14) Estruturas e uniões 71 Estruturas 71 Estruturas e Manipulação de Bits 73 Estruturas e funções 74 Uniões 76 Inicializando estruturas e unimos 81 15) Determinando o tamanho de Estruturas, Uniões e etc82 15) Enumeração 83 16) Definição de Tipos 85 16) Trabalhando com arquivos em disco Funções de Alto Nível 86 Funções de Baixo Nível 91 Departamento de Ciência da Computação-UFF Números de página 17)Arquivos padrões e como usar a impressora 97 18) Respostas de problemas 99 19) Apêndices Tabelas dos operadores lógicos101 Departamento de Ciência da Computação-UFF Números de página INTRODUÇÃO C é uma linguagem de programação que tem como características principais grande flexibilidade, escrita compacta, padronização bem feita e alta velocidade de processamento. Por estas características, ela é chamada, muitas vezes, de uma Linguagem de Programador pois envolve aspectos, como os já citados, que agradam a estes profissionais. Foi desenvolvida no início da década de 70 por Dennis Ritchie influenciada pela linguagem B, esta desenvolvida por Ken Thompson. A linguagem B teve como fonte inspiradora uma outra linguagem, o BCPL. B é uma linguagem que só tem um tipo de dado que corresponde ao tamanho da palavra do computador no qual trabalha. A linguagem C, por sua vez, tem uma gama relativamente grande de tipos de variáveis mas não é tão fortemente tipada como Pascal. Permite ainda a criação de estruturas de dados não homogêneas. Apesar destas diferenças, BCPL, B e C são filosoficamente semelhantes. A ideia é ter uma linguagem de escrita compacta, com recursos de manipulação de dados a baixo nível mas com características de linguagem de alto nível. Criar um compilador de C é uma tarefa consideravelmente mais fácil que criar um compilador Fortran, por exemplo, além de poder ser bem pequeno existindo versões que completas e funcionais que ocupam menos de 100KB de espaço em disco, exigem memória da ordem de 256KB, restricões que nos dias de hoje (1997) nos parecem paleolíticas. As primeiras versões para micro (Apple II, por exemplo) funcionavam bem melhor que compiladores de outras linguagens. As idéias por trás de C são tão simplificadoras e "naturais" que ela serve de base para outras linguagens. Dando apenas um exemplo, existe uma linguagem que se baseia simultaneamente em C e no conceito de objetos[]. Esta linguagem, o C++ (pronuncia-se cê mais mais), é bem definida existindo o padrão ANSI. Atualmente existem compiladores dos mais diversos fabricantes e ainda versões de domínio público largamente usados em ensino e pesquisa no mundo inteiro. O caso mais notório é o GNU C (embora o Projeto GNU não se limite à este compilador) e existe versões do mesmo para os mais variados ambientes (UNIX/Linux, McIntosh, MS-DOS/Windows). C originalmente foi uma linguagem construída para desenvolver programas de sistemas, ou seja : Sistemas operacionais Assemblers Editores Interpretadores Compiladores Gerenciadores de rede, etc. Por exemplo, o sistema operacional UNIX é escrito em C, estando esta linguagem e este sistema operacional intimamente ligados inclusive sob o ponto de vista de filosofia de trabalho e construção de programas. O que faz esta linguagem aplicável a estas funções é que C permite um nível de manipulação de dados, só conseguido anteriormente pelo uso de programas em Assembler. Estes últimos são de escrita difícil e tediosos de escrever além de ter depuração geralmente problemática. Um fator que pode levar a adoção do Assembler é a velocidade mas, nem sempre este é o fator principal e mesmo neste ponto C não faz feio. C, permite o uso de recursos de "baixo nível" numa linguagem de "alto nível" e boa velocidade de execução. Por isso, muitas vezes C é chamada de linguagem de "médio nível". Como se pode perceber, não há nenhuma conotação ruim nos termos baixo ou médio. Para dar exemplos, Linguagens deBaixo Nível são os Assembly de cada máquina. Linguagens de Alto Nível são FORTRAN, ALGOL, BASIC, PASCAL, etc. As Linguagens de Médio Nível mais conhecidas são C e FORTH, esta última tendo uma estrutura mais flexível e poderosa do que C mas geralmente é de execução mais lenta, por ser (nas suas versões mais comuns) parcialmente interpretada, além de ser de programação confusa. É claro que não demorou muito tempo para que uma linguagem com este poder fosse se difundindo fora da área de programas de sistemas. Hoje, boa parte das aplicações de alto desempenho são cada vez mais escritas em C, sejam elas comerciais ou científicas. Você poderá escrever em C qualquer programa que execute qualquer tarefa escrita originalmente em linguagens como FORTRAN, Pascal, COBOL ou BASIC e coisas que estas não fazem ou que, para serem feitas, exigem conhecimento de truques ou macetes confusos ou específicos do produto com o qual você estiver trabalhando. Mas nem tudo se resume a maravilhas. Em troca, C exige do programador muita responsabilidade e atenção. Outras linguagens criam barreiras que protegem certas estruturas na memória do computador, mas C não. Portanto se acostume com o lema de C : "O programador é inteiramente responsável pelo que faz." A filosofia é de que o programador não precisa ser tutelado. Departamento de Ciência da Computação-UFF Números de página C não menospreza a sua inteligência, mas também não se responsabiliza por descuidos. Não se esqueça que um algoritmo mal feito vai funcionar mal em qualquer linguagem. Mas não se assuste. Há uma brincadeira entre os não usuários de C e mesmo os que iniciaram o seu estudo de maneira descuidada. Estes dizem: "Você gosta de viver perigosamente ? Programe em C!" Se fosse tão "perigosa" assim, C não seria tão usada. Iniciaremos a discusão da linguagem C seguindo como referência o que é chamado "C padrão", ou seja, a definição mínima contida no livro "A Linguagem de Programação C" escrito por Brian Kernigham e Dennis Ritchie em sua primeira edição. Haverá ainda informações sobre uma complementaçãodefinida pelo American National Standards Institute (ANSI). A esta chamaremos de "Padrão ANSI". Sobre o parágrafo anterior é bom chamar a atenção que cada fabricante de software que produz para uma máquina específica, tende a incluir “extenções da linguagem” fora dos padrões acima. Nem sempre é possível ignorar estas modificações do padrão. É o caso dos famigerados “modos de memória” em máquinas microsoft/Intel. No entanto, por uma questão de produtividade, flexibilidade e transportabilidade devemos nos ater da melhor maneira possível aos padrões não dependentes de fabricante. Esta preocupação em seguir padrões facilita a construção de produtos muito mais do que limita a produção. Seguir uma padronização é considerada uma atitude louvável sendo que no caso do Unix todo produto que carrega os dizeres “POSIX COMPLIANT” é considerado de maneira mais “respeitosa”. Como ponto ainda, temos versões “Integradas” e “não-integradas”, ou seja, versões onde temos um sistema que integra os passos de edição, compilação e depuração e sistemas em que cada uma destas fases são executadas separadamente. Os sistemas integrados são mais “confortáveis” mais o sistema de integração ocupa espaço em memória e ainda é um elemento estranho ao processamento do programa (podemos ter reações diferentes do programa se ele roda dentro ou fora do integrador). Muitas vezes (principalmente em programas críticos) é mais seguro botar a preguiça de lado de sofrer o “imenso desconforto” de apertar uns botões a mais. O texto, no essencial, não se prende a um compilador ou a uma máquina específica. Quanto a referências a C++, damos o livro de definição da linguagem de Stroutrop e ainda chamamos a atenção que as versões mais recentes da versão de C do Projeto GNU já são efetivamente compiladores de C++. Produtores tradicionais de compiladores C também estão criando produtos já admitindo C++. Quero lembrar que este texto, pressupõe um conhecimento prévio de técnicas de programação em uma linguagem como FORTRAN, Pascal, Assemblers, etc. Não é recomendável a adoção de C como primeira linguagem de programação. Os programadores que tiverem alguma experiência com assemblers, provavelmente, absolverão com mais facilidade alguns conceitos incomuns contidos em C e não em outras linguagens. Como este curso presupõe tão variadas origens, alguns pontos podem aparentar ser redundantes, óbvios ou (para alguns) um tanto quanto obscuros. Antes de mais nada cuidado com as aparências. O óbvio pode não ser tão óbvio e o obscuro pode ter origem no “sotaque” que você carrega da linguagem de programação que você está mais acostumado. Quanto da forma do texto, fazemos inicialmente uma descrição da linguagem de uma maneira geral e apresentamos exemplos simples que (ao se apresentar novas estruturas) vamos modificando sistematicamente fazendo uma apresentação informal da linguagem. A medida que evoluimos, introduzimos mais conceitos novos e sofisticamos os exemplos até englobar a linguagem como um todo. De certa forma, esta estratégia tem por inspiração a origem de C. UNIX foi criado com a filosofia de construção que devemos ter ferramentas de software simples, dedicadas a determinada operação e confiável, algo como: Menos é mais . Devemos então adotar esta como filosofia de programação. Departamento de Ciência da Computação-UFF Números de página C, A LINGUAGEM IDENTIFICADORES Um ponto importante em C são as especificações dos identificadores e algumas convenções na criação dos mesmos. Os identificadores em C, podem começar por qualquer letra maiúscula ou minúscula ou o sinal de sublinhar (underscore _). No meio de um nome, poderam haver letras, números ou o sinal _ (sublinhado ou underscore) e nada mais. O número de caracteres que difere um identificador de outro, pode variar de compilador para compilador sendo que na definição de C padrão K&R são levadas em consideração 8 caracteres e em C ANSI 32 caracteres. Exemplos corretos e distintos (nem por isto todos são recomendáveis!) : A a exemplo_de_identificador OUTRO_exemplo_DE_identificador MAIS_1_ExEmPlO _e_outro Exemplos incorretos : 1_exemplo (começa por número) 1a (idem) um-exemplo-errado (hífem entre palavras) +um_exemplo_errado (carater inválido "+") esta_errado_tambem! (caráter inválido "!") Chamamos a atenção para convenções adotadas para os identificadores : "Variáveis tem seus identificadores em letras minúsculas" "Constantes tem seus identificadores em maiúsculas" "Tipos criados pelo usuário devem ter os seus identificadores começando por uma maiúscula". Recomenda-se, por questões de padronização, que você adote esta convenção, além do que ela facilita a visualização de "quem é quem" dentro do programa. Votaremos a falar dentro em pouco de constantes e mais tarde de declarações de tipo. Variações podem ser feitas sempre que facilite o entendimento. Mas lembre-se: estas são apenas recomendações. Caso violá-las facilite a compreensão de um programa, é um caso a se pensar. Lembre-se novamente que C distingue entre letras maiúsculas e minúsculas, portanto, os identificadores abaixo são diferentes entre si. SOMA soma Soma Fazemos ainda a recomendação que você ao definir os seus identificadores o faça da maneira mais óbvia possível. Se uma variável acumula a soma de um orçamento, denomine-a soma_do_orcamento e não só, xpto ou algo que o valha. Fica muito mais fácil de entender e é uma maior garantia de que, meses depois você (ou outra pessoa) se localize com mais facilidade (o próprio nome da variável passa a fazer parte da documentação). Como veremos, C é uma linguagem de escrita compacta, o que facilita a sua digitação mas pode levar a escrevermos programas incompreensíveis se não formos óbvios sempre que pudermos. Hoje o que mais vale não é o programador cheio de truques e malandragens de programação mas aquele que prefere construir programas claros e de fácil visualização. Isto facilita a manutenção do programa e permite uma produção mais constante, ou seja, se você programa por hobby ou em desenvolvimento de projetos científicos, a atitude de ser simples e claro lhe trará menos trabalho nos seus projetos. Caso você seja um profissional de produção de software, isto significará menor dispêndio de tempo e, consequentemente, maior eficiência. Departamento de Ciência da Computação-UFF Números de página Palavras-chave C será discutido em duas partes: as palavras-chave e as funções. As primeiras são palavras reservadas, ou seja, identificadores que não podem ser usados pelo usuário. São identificadores que são ou constituem partes de comandos ou declarações da linguagem. Quanto às funções, nada podemos fazer de prático em C sem elas. Apesar disto, inicialmente só falaremos das palavras-chave. As palavras-chave são bem poucas, se compararmos com o número de comandos de linguagens como PASCAL e abaixo temos a relação de todas elas. PALAVRAS-CHAVE auto double int struct break else long switch case enum@ register typedef char extern return union const@ float short unsigned continue for signed@ void@ default goto sizeof volatile@ do if static while @ extensões ANSI As palavras marcadas com @ são conhecidas como extensões ANSI e as demais são as palavras padrão K&R. Variando de compilador para compilador, podemos ter algumas palavras extras. Atenção: O fato de usar palavras-chave fora do padrão, poderá criar problemas caso você queira que um determinado programa construido por você possa ser levado de um compilador para outro ou mesmo para outra máquina. A esta característica de podermos migrar um programa com um mínimo de modificações é chamada de Portabilidade. Se você pretende fazer com que seu programa seja o mais portável possível, evite o uso de palavras-chave diferentes das acima. Alémdisto, alguns compiladores mais antigos não aceitam as extensões ANSI. Um bom conhecimento da documentação de seu compilador é vital. Toda palavra-chave em C se escreve em minúsculas, como se pode notar, mas o importante é chamar novamente a atenção que C distingue duas palavras iguais mas escritas uma em maiúsculas e outra em minúsculas, o que a maioria das linguagens não faz. Isto será fonte de erros no início dos seus trabalhos com C. Outra fonte de erros surgirá principalmente para os de imaginação mais fértil e que gostam de fazer paralelos entre linguagens, confiando em aparentes semelhanças. A maior parte das vezes, as dificuldades de entender C tem origem exatamente devido a suposições do tipo: "Este comando é "igual" a daquela outra linguagem e, portanto, deve funcionar de maneira parecida." Toda vez que você pensar coisas deste tipo, REPRIMA! Muitas semelhanças são apenas aparentes. Portanto, tenha em mente o lema: C se parece apenas com C. Departamento de Ciência da Computação-UFF Números de página Tipos de Variáveis Passemos a mais um ponto fundamental: os tipos de variáveis. Os tipos disponíveis ao programador com a gama de valores correspondentes estão relacionadas abaixo : Variável Tamanho(Bytes) Gama char 1 [0,255] int 2 [-32768,32767] short int 1 [-128,127] unsigned int 2 [0,65535] long int 4 [-2147483648,2147483647] Variável Tamanho(Bytes) Gama Algarismos sig. float 4 10 6 Double 8 12 OBS: Os valores dados para os tipos float e double, poderão tomar valores negativos nos respectivos domínios. É comum que se dispense a palavra int depois de short, unsigned e long. Observe que aqui temos várias palavras-chave. Temos além destas algumas que atuam sobre o tipo da variável e elas são listadas abaixo e que chamaremos aqui de modificadores : Modificadores Aqui chamamos de modificadores à palavras reservadas que modificam certas características de tipos de variável definidas anteriormente auto signed@ extern static register volatile@ @ extensões ANSI Apesar de termos vários tipos de variáveis, C permite muitas liberdades no trabalho destas. Se a operação tem significado ou não, isto é deixado a você, o programador. Chamamos a atenção que o tipo inteiro básico em C é geralmente o de dois bytes, embora isto possa variar (Não se esqueça sempre verifique a documentação de seu compilador). Ainda temos a dizer que na definição padrão de C, todas as operações de ponto flutuante são executadas em double. É bom frizar isto, já que em programas que usem pesadamente deste tipo de operação, o processamento do mesmo será mais lento do que se fosse feito em linguagens que trabalhem com variáveis de ponto flutuante de quatro bytes, por razões óbvias. No entanto, muitos compiladores permitem que você use menor precisão como uma opção o que acelerará a execução embora com detrimento da precisão. Departamento de Ciência da Computação-UFF Números de página OPERADORES Temos, além das palavras-chave, a definição de operadores de C. E eles são abundantes: Operador de atribuição '=' Ex.: a = b À variável a é atribuido o valor de b. No caso de atribuição de caracteres, o caracter deverá estar entre aspas simples. Ex.: a = 'b'. Operador vírgula , Este operador serve para separar várias operações a serem executadas dentro de parênteses. Se a sequência de operações é atribuida a uma variável, o valor atribuido a variável será a da última expressão entre parênteses. Ex : Seja a = 5 e b = 7, então a expressão c = (a-b, b * 2) fará que c tome o valor 14. É comum que encontremos este operador colocado separando operações dentro da declaração for, que veremos abaixo. Operadores aritméticos Suporemos aqui que b e c são inteiros e respectivamente iguais a 7 e 5. * multiplicação Ex. : a = b * c (a = 35) / divisão Ex. : a = b / c (a = 1) % modulo Ex. : a = b % c (a = 2) + adição Ex. : a = b + c (a = 12) - subtração Ex. : a = b - c (a = 2) Como a maioria das linguagens de programação, C tem vários operadores sobrecarregados, ou seja, um mesmo operador atua sobre vários tipos. Os operadores de multiplicação, divisão, soma e subtração atuam indiscriminadamente sobre double, float, int e suas modificações. Note que o operador de módulo também é sobrecarregado pois vale para qualquer inteiro. Observe ainda que não existe em C um operador de potenciação. Isto não é um fato exclusivo desta linguagem. Por exemplo, Pascal tem esta mesma ausência. Também é bom observar (embora seja óbvio) que o operador % só tem sentido para operandos inteiros. Operadores de incremento e de decremento ++ incremento Ex. : a = b++ ou a = ++b (resultados diferentes!) -- decremento Ex. : a = b-- ou a = --b (resultados diferentes!) Para clarear o que ocorre, trabalhemos inicialmente somente com o operador de incremento. Ao colocarmos o operador antes do identificador da variável, primeiro a variável será incrementada e então este valor será usado em outras operações na linha onde se encontra. Caso o operador estiver depois do identificador, então este valor será usado e só depois haverá o incremento. Vamos exemplificar o funcionamento destes operadores com o caso abaixo onde temos uma variável com um determinado valor e esta, com o uso dos operadores acima, é atribuida a outra variável. O valor da variável é dado ao lado de cada caso. Para b = 5 a = b++ (a = 5) a = ++b (a = 6) a = b-- (a = 5) a = --b (a = 4) Obs: Chamemos a atenção sobre uma questão de nomenclatura. Vamos tomar como exemplo o operador de incremento. Se o operador é colocado antes da variável (por exemplo, ++b) dizemos estar fazendo um pré- incremento. Se o operador está depois da variável (b++) dizemos então que fazemos um pós-incremento. Vale o análogo para o operador de decremento. Departamento de Ciência da Computação-UFF Números de página Operadores de endereço Estes aqui serão meramente apresentados. A maneira de utilizar cada um será vista em detalhe no decorrer do texto. & "o endereço de" . Ex. : a = &b. a recebe o endereço da variável b. Vide ponteiros. * "no endereço de". Ex. : c = *d. c recebe o valor da variável de endereço dado por d. Vide ponteiros. [ ] "no endereço de ... mais um índice" . Ex. : c = a[1]. c recebe o elemento de a de índice 1. Vide vetores. ."elemento da estrutura" (operador ponto). Vide estruturas. -> "elemento da estrutura apontada por" (operador seta). Vide estruturas. Operadores de Bit Estes operadores trabalham a nível de bits. Tais operadores são necessários a uma linguagem que pretende substituir o assembler em projetos de software. Para ilustrar, suporemos aqui que a e b são inteiros e respectivamente iguais a 5 e 6, ou seja, 00000101 e 00000110 em base binária). << desloca para a esquerda. Ex. : c = a<<2 ( 20) (00010100) >> desloca para a direita. Ex. : c = a>>1 ( 2) (00000010) & E lógico Ex. : c = a & b ( 4) (00000100) | OU lógico Ex. : c = a | b (7) (00000111) ^ OU Exclusivo Ex. : c = a ^ b (c = 3) (00000011) ~ NAO Ex.: c = ~b (c = -32761) (11111001) Os operadores de deslocamento à direita e a esquerda, deslocam os bits dentro das variáveis nestas direções. Estas operações, no caso de atuar sobre inteiros, é equivalente a multiplicar (deslocamento à esquerda) ou a dividir (deslocamento à direita) por uma potência de dois. OBS.: Observe que o operador & aparentemente faz dois serviços: um como operador de endereços e outro como E lógico. A diferença está que no caso do operador de endereço, ele atua somente sobre um elemento (operador unário), enquanto no caso de ser o operador E lógico, ele precisa de dois elementos para obter um resultado (operador binário). Operadores lógicos Em C não temos variáveis do tipo lógico e a definição do verdadeirológico é dada por qualquer valor diferente de zero. Obviamente o falso lógico é o valor zero. Portanto estes operadores trabalharão sobre quaisquer variáveis dando uma resposta correspondente a condição examinada. && E lógico Ex. : condição1 && condição2. || OU lógico Ex. : condição1 || condição2. ! NAO lógico Ex. : !condição1. Departamento de Ciência da Computação-UFF Números de página Operadores relacionais Os operadores abaixo trabalham fazendo comparações entre dois valores devolvendo como resultado um falso ou um verdadeiro. > "maior que". Ex. : a > b < "menor que" Ex. : a < b == "igual a". Ex. : a == b >= "maior ou igual a". Ex. : a >= b <= "menor ou igual a". Ex. : a <= b != "não igual a". Ex. : a != b OBS.: I) Não confunda o operador == (igual a) com o operador = (de atribuição). II) Lembre-se novamente que o verdadeiro lógico é qualquer valor diferente de zero e o falso é o zero. Não confunda os operadores acima com os operadores de bit. Departamento de Ciência da Computação-UFF Números de página ABREVIAÇÕES Outro ponto interessante de C, é que podemos abreviar algumas expressões. Por exemplo, se temos algo do formato <variável1> = <variável1> <operador> <expressão> podemos sempre escrever <variável1> < operador> = <expressão> Exemplos: x += u é equivalente a x = x + u x += delta * b é equivalente a x = x + delta * b a >>= b é equivalente a a = a >> b a &= b é equivalente a a = a & b É óbvio que estas abreviações podem confundir quem se inicia na linguagem ou tem pouco conhecimento da mesma. Tome os devidos cuidados, então. PRECEDÊNCIA DOS OPERADORES Abaixo temos a precedência de cada operador dentro de cada grupo e no caso geral. OPERADORES ARITMÉTICOS 1) ++, -- 2) - (UNÁRIO) 3) * , /, % 4) +, - OPERADORES RELACIONAIS E LÓGICOS 1) ! 2) > , >=, <, <= 3) ==, != 4) && 5) || OPERADORES DE ENDEREÇO 1) ., -> 2) *, & Departamento de Ciência da Computação-UFF Números de página PRECEDÊNCIA GERAL 1)( ) Chamada de função [ ] Elemento de matriz -> Ponteiro para membro de estrutura . Membro de estrutura 2) ! ~ ++ -- - Unário (tipo) Moldagem * "no endereço de" & "o endereço de" sizeof 3) * Multiplicação / % 4) + - 5) << >> 6) < <= >= > 7) == != 8) & E de bit 9) ^ OU Exclusivo de bit 10) | OU de bit 11) && 12) || 13) ?: 14) = *= /= %= += -= <<= >>= &= ^= |= 15) , Mudança de Precedência A precedência poderá ser mudada pelo uso de expressões entre parênteses, tendo os parênteses mais internos maior prioridade que os demais. OBS. : Lembre-se: é preferível colocar parênteses sobrando para clarear o que ocorre do que permitir interpretações dúbias. Departamento de Ciência da Computação-UFF Números de página CONVERSÕES DE TIPO E CONVERSÕES FORÇADAS (cast, ou moldagem) Agora que já falamos de tipos e operações, devemos nos preocupar com o uso de operações com tipos mistos, por exemplo, calcular o produto de uma variável inteira com uma de ponto flutuante e este resultado ser colocado numa variável de precisão dupla. Há algumas conversões que são feitas automaticamente. Em operações mistas odos os char e short int são convertidos em int. Todos os float são convertidos em double. Sempre a conversão entre pares de operandos é feita para o tipo do maior operando, ou seja, operando-se um double e um int, temos como resultado um double e se operamos um long e um int, temos um long como resultado. O que queremos dizer como maior é o tipo que puder representar o maior número. Portanto, operando um inteiro longo e um número de ponto flutuante, ambos de 4 bytes de comprimento, teremos a conversão do resultado em um número de ponto flutuante. No entanto, nem sempre o resultado que temos é o esperado. Algumas vezes pode ser interessante forçar uma conversão de tipo. Podemos fazer isto escrevendo, entre parênteses, o tipo ao qual queremos converter a variável a esquerda. Este construtor é denominado Cast ou ainda Moldagem. Vamos a um exemplo. Suponha que i e j sejam duas variáveis inteiras de dois bytes (tipo int) e x uma variável de ponto flutuante (tipo float). Poderíamos escrever o que se segue i = (int)x /j Teríamos a divisão de x por j, mas não o ponto flutuante x mas o valor convertido para inteiro. A moldagem pode ser também parte da documentação. Mesmo que saibamos que determinadas conversões serão feitas automaticamente, é sempre bom indicar explicitamente através da moldagem as nossas “intenções”. Isto muitas vezes facilita a compreenção do programa. Ainda devemos tomar cuidado ao forçar certas conversões. O resultado pode não só ser inútil como gerador de dores de cabeça, caso seja feita de forma pouco cuidadosa. Departamento de Ciência da Computação-UFF Números de página CONSTANTES C tem uma notação especial para constantes de forma a ficar claro o seu tipo. Caso seja atribuído a uma variável um número de ponto flutuante, este será convertido primeiramente para double, seja a variável tipo float ou não. Outras vezes é mais prático, por motivo de documentação, escrever uma constante em base hexadecimal do que em base decimal. Para evitar conversões automáticas desnecessárias (algumas vezes prejudiciais) além de facilitar a documentação, os criadores desta linguagem adotaram o padrão de colocar após o último algarismo uma letra indicando o tipo de constante. Usa-se F para indicar que a constante é tipo float, L para indicar que é do tipo long e U para unsigned. No caso dos números hexadecimais, usa-se 0x como prefixo. Pode-se usar tanto letras maiúsculas quanto minúsculas. Podemos ainda fazer combinações com estes modificadores. Portanto, teríamos que 100F é o número 100 de ponto flutuante 120000L é o número 120000 inteiro longo 65000U é um número inteiro não assinalado 13500UL é um número inteiro longo não assinalado 0X130 é o número 130 na base hexadecimal Não custa relembrar que se não houver nenhuma declaração que o negue, as constantes são do tipo int. Além destas há uma série de constantes pré-definidas (usualmente chamadas Sequências Escape ou de Fuga) que são caracteres de controle de dispositivos como impressoras, monitores, etc. Abaixo temos estas constantes. \a Alerta(bell) \b Retrocesso \f Alimentação de formulário \t Tabulação horizontal \v Tabulação vertical \\ Barra invertida \? Interrogação \' Apóstrofo \o Número octal \x Número Hexadecimal Departamento de Ciência da Computação-UFF Números de página COMANDOS Um comando é uma expressão válida seguida de um ponto-e-vírgula. Uma expressão válida por sua vez é um conjunto de variáveis e operadores colocados de forma coerente. Para dar sentido mais claro a estas palavras vamos a alguns exemplos de comandos válidos : a = b; (É atribuido à variável a o valor contido em b) a = a + c * d; (a recebe o produto de c por d e mais o próprio valor corrente) ; (Comando nulo) Para comandos não válidos teremos : a = c (falta o ponto-e-vírgula) a = a ^& b; (^& não é um operador) a = 1z3 * b; (variável inválida) BLOCOS Um bloco é definido como um conjunto de comandos logicamente conectados. Ou seja, eles são trabalhados como se fossem um único comando. A indicação de um bloco é determinada por um abre chave no seu início e um fecha chave ao seu final, ou seja, { COMANDO 1 COMANDO 2 COMANDO 3 . . . COMANDO n } Pela definição, um bloco pode conter blocos. Departamento de Ciência da Computação-UFF Números de página CONTROLE DE FLUXO Temos aqui as formas de controle de fluxo em C. a) if (CONDIÇÃO) COMANDO1 else COMANDO2 Se CONDIÇÃOfor verdadeira (diferente de “zero”), COMANDO1 será executada. Caso seja falsa (ou seja, igual a “zero”), então COMANDO2 será executada. Atenção: O else é opcional. b) ? : É uma simplificação da declaração if. Exemplifiquemos seu uso : var = expr1 ? expr2 : expr3 Se a expressão 1 for verdadeira, então é avaliada a expressão 2, senão é avaliada a expressão 3 e o resultado, num caso ou no outro, será atribuído à variável var. c) switch , default e case Estas declarações trabalham juntas, sendo que default é opcional. switch (EXPRESSÃO) { case constante1 : COMANDO case constante2 : COMANDO . . . case constante n : COMANDO default : COMANDO } Quando uma opção é tomada, a declaração correspondente é executada e TODAS as instruções abaixo também o serão. Para que tal fato não ocorra, é necessário terminar cada bloco com um break, palavra que veremos mais abaixo. Caso o valor que EXPRESSÃO tomar não seja igual a nenhuma das constantes após a palavra case, então a declaração que estiver associada a palavra default será executada. Caso ela não for usada, haverá interrupção do laço. Atenção: Observe o fato que os blocos de instruções na declaração switch, depois do case, dispensarem o abre e fecha colchetes. Pela própria natureza da execução, não há a necessidade desta explicitação. Muitos programadores, por questão de padronização, colocam abaixo de cada case um par de chaves definindo um bloco mesmo este não sendo necessário. Como sempre, faça o que você achar mais claro. Departamento de Ciência da Computação-UFF Números de página d) goto É uma declaração comum em outras linguagens, mas que deve ser evitada sempre que possível, pois geralmente dificulta o entendimento do programa. Dificilmente você encontrará um bom programador usando-a a não ser em último caso. Há situações nas quais esta declaração, ao contrário do habitual, facilita a compreensão do programa, embora sejam muito raras. Para a sua utilização, é necessário a indicação de um rótulo que é um identificador seguido de : (dois pontos). . . goto ok; . . ok : ....; . Aqui não veremos exemplos com goto. Possivelmente você verá seu emprego muito raramente se é que algum dia você verá. OBS.: Embora, a estrutura básica do if e do switch-case se assemelhe a de outras linguagens, observe bem as diferenças. Departamento de Ciência da Computação-UFF Números de página LAÇOS Como toda linguagem, C tem uma série de instruções que executam laços controlados. Novamente, tome cuidado com semelhanças. a) while (EXPRESSÃO) COMANDO Enquanto a expressão entre parênteses for verdadeira (diferente de zero) COMANDO é executado. b) do COMANDO while (EXPRESSÃO); Funciona de maneira semelhante ao ítem "a". A diferença é que aqui a decisão de parada é após a execução da EXPRESSÃO. c) for (EXPRESSÃO; CONDIÇÃO; EXPRESSÃO) COMANDO Esta declaração é provavelmente conhecida por todos, por aparecer em várias linguagens. É comum que a EXPRESSÃO1 seja uma inicialização de uma variável, CONDIÇÃO a condição de parada e EXPRESSÃO2 o modificador do valor da variável mas poderemos ter coisas mais complicadas e interessantes. Este laço é equivalente a um laço while com a seguinte estrutura : expressao1; while(condição) { comando expressão2; } Alguns programadores mais radicais recomendam o "esquecimento" para o for. Não chegamos a tanto. Uma ressalva importante é que CONDIÇÃO é avaliada no início do laço, como podemos observar da equivalência mostrada acima. O esquecimento deste fato pode levar a ações inesperadas. Para os que acham que “todo for é igual”, daremos um exemplo das particularidades deste laço. A expressão abaixo é válida em C for (; ; ) COMANDO e define um laço sem fim. Palavras Auxiliares a) break; É um comando auxiliar no controle de laços. Se você fizer uma construção com vários laços embutidos, ao encontrar esta palavra chave o programa interrompe o laço no qual se encontra e salta para o imediatamente externo. b) continue; Ao encontrar este comando, o programa saltará diretamente para a condição do laço, desconsiderando os comandos posteriores a ela. No caso de usado num for, ao encontrar um continue, o controle passará para a declaração 2 dentro dos parênteses do for. Só pode ser usado dentro de laços. Departamento de Ciência da Computação-UFF Números de página Fatos e mitos - Velocidade de processamento Como já foi dito acima, C é uma linguagem que gera programas de alta velocidade de processamento. Algumas pessoas já usaram C e acham que isto é uma balela já que "Na linguagem xxx um programa meu foi executado mais rapidamente que em C." Um fato que devemos desde já chamar a atenção com ênfase é que C faz todas as suas operações de ponto flutuante com o tipo double se você não obrigar que estas sejam feitas com outra precisão. Obviamente isto gera duas consequências: Maior precisão no cálculo e maior tempo de execução. C é uma linguagem que gera programas rápidos, continuo insistindo, mas a única maneira de se fazer um programa o mais rápido possível é o conhecimento não só da linguagem na qual programamos como também da implementação particular do compilador que usamos e o computador no qual estamos trabalhando. Haverá situações em que um programa em C será mais lento que o mesmo programa em alguma outra linguagem. Isto obviamente não é nenhum demérito. Se C ou outra linguagem qualquer solucionasse todos os problemas de programação, é claro que não haveria a profusão de linguagens que existe, cada uma mais adaptada a determinadas tarefas. Devemos ter em mente as vantagens e limitações de cada linguagem e a adequação dela ao problema que queremos resolver. Para evitar problemas e tomar uma atitude mais realista e madura quanto à programar lembre-se da Lei de Murphy[ ] aplicada à computação: Se você quer fazer uma besteira é fácil, mas se quiser fazer uma grande besteira, você vai precisar de um computador. Departamento de Ciência da Computação-UFF Números de página ESTRUTURAS DE PROGRAMAS EM C: A FUNÇÃO main() Um programa em C é uma chamada de função, no caso uma função especial de nome main(), a qual chamaremos de função principal. Ela é a única função obrigatória de existir num programa em C e inicialmente trabalharemos apenas com ela. O mais simples programa em C, contendo apenas a função main() (mas que não funcionará!), tem a forma que se segue main() { DECLARAÇÃO DAS VARIÁVEIS COMANDO1 COMANDO2 . . COMANDOn } Todas as variáveis contidas no programa deverão ser declaradas. Como toda linguagem de programação, em C você poderá também inserir comentários. Isto é feito colocando em qualquer coluna uma barra seguida de um asterisco depois escrevendo seu comentário e então, ao acaba-lo, teclando outro asterisco seguido de uma barra, ou seja, /* comentario */ Obs: Não é permitido o aninhamento de comentários no ANSI C mas outras versões poderão permitir. Em alguns momentos o aninhamento de comentários é cômoda mas muitas vezes (dependendo da forma de implementação) pode ser fonte de erros bobos. Escrevamos o nosso primeiro programa que: a)Some dois números inteiros e coloque o valor da soma num outro número inteiro. b)Se a soma for maior que 9, faça a variável que contém a soma, ser igual a 9. main() { /* Primeiro programa */ int a, b, c; a = 5; b = 3; c = a + b; if ( c > 9 ) { c = 9; } } Temos acima a declaração das variáveis a, b e c como inteiras seguida da atribuição de valores e então sua soma sendo atribuida a variável c. Logo após temos o teste do valor da soma. Como a = 5 e b = 3, temos que o resultado finalserá c = 8. Portanto, depois do teste, c continuará com o valor 8. Para marcar bem a diferença entre o operador de atribuição = em C e em outras linguagens, vamos reescrever este programa. Departamento de Ciência da Computação-UFF Números de página main() { /* Primeiro programa, segunda versão */ int a = 5, b = 3, c; if ( (c = a + b) > 9 ) c = 9; } O operador de atribuição é usado para inicializar as variáveis no momento de sua definição. Dentro dos parênteses do if, há a atribuição do valor da soma de a com b à variável c. Só após feito isto é que haverá a comparação e avaliação da condição. Esta maneira de usar o operador de atribuição pode parecer estranha inicialmente mas, indiscutivelmente, tem o seu charme. Observe ainda que como só temos um comando para o if , não foi criado um bloco. Uma nova versão pode ser feita, agora com o uso do operador ? : main() { /* primeiro programa, terceira versão */ int a = 5, b = 3, c; c = (c = a + b) > 9 ? 9 : c; } O resultado é idêntico. Aqui usamos o operador ? : no lugar do if. Este operador trabalha, no programa acima, da seguinte maneira: Inicialmente as variáveis a e b são somadas e depois o resultado é atribuido à variável c. Verifica-se se o resultado é maior que 9. Se for, a c é atribuido o valor 9. Se não, a c é atribuido o próprio c, que já contém a soma de a e b. Observe os parênteses obrigando que a atribuição da soma seja feita antes do teste. Façamos mais um programa. Queremos somar um número de ponto flutuante de precisão simples a ele mesmo, 10 vezes enquanto ele for menor que 2000. Ao final devemos ter uma variável inteira que contenha quantas vezes o processo de soma foi feito. main() { /* Segundo programa */ int n; float x; x = 235.; for (n = 1; n <= 10; n++) { x = x + x; if ( x >= 2000.0) break; } } Observe o uso do operador de incremento na sua versão de pós-incremento dentro do for. Lembre-se: queremos dizer com esta expressão que o valor da variável será usada e só então haverá o incremento. Note ainda o uso do break para interromper o laço do for. Como já foi descrito, no momento que a palavra-chave break for encontrada, o laço, dentro do qual está o break, será interrompido. Vamos reescrever o programa. Departamento de Ciência da Computação-UFF Números de página main() { /* Segundo programa, segunda versão */ int n; float x; x = 235.; n = 0; do { x += x; n++; } while ( (x < 2000.) && (n < 10)); } Aqui temos o mesmo programa só que agora a saida é feita através de uma condição composta. São testadas as condições de parada de tal forma que se pelo menos uma das duas for falsa haverá a interrupção da execução. Lembre-se que o while continua sendo executado enquanto a expressão dentro dos parênteses for verdadeira. Temos ainda o uso do operador de pós-incremento usado isoladamente e também a utilização da notação abreviada para o cálculo de x. Observe bem as diferenças entre as duas versões não esquecendo que são totalmente equivalentes, embora o for ter uma equivalência com o while e não com o do-while. Vamos a mais um programa. Temos uma variável do tipo caracter (char) e vamos provocar as seguintes reações : a) Se for um 'a' ou um 'b', uma variável inteira tomará o valor 1; b) Se for um 'c', valor 2; c) Se for um 'd', 3; d) Se for um 'e', o valor 4. main() { /* Terceiro programa */ int numero; char letra; if ((letra == 'a') || (letra == 'b')) { numero = 1; } else { if (letra == 'c') { numero = 2; } else { if (letra == 'd') { numero = 3; } else { if (letra == 'e') { numero = 4; } } } } } Departamento de Ciência da Computação-UFF Números de página Observe mais uma condição composta no primeiro if. Neste caso basta uma das duas expressões serem verdadeiras para que a variável numero tome o valor 1. Embora interessante como exemplo de uso de if, o programa não é um bom exemplo de programação. Reescrevendo este programa (como está se tornando hábito) temos: main() { int numero; char letra; switch (letra) { case 'a' : numero = 1; break; case 'b' : numero = 1; break; case 'c' : numero = 2; break; case 'd' : numero = 3; break; case 'e' : numero = 4; } } Aqui substituimos uma sequência de if's por uma declaração do tipo switch-case. A função do break, chamamos novamente a atenção, é interromper a execução do bloco do switch. Nada nos impede de reescrevermos o programa como o que se segue: main() { /* Terceiro programa, terceira vercao */ int numero; char letra; switch (letra) { case 'a' : case 'b' : numero = 1; break; case 'c' : numero = 2; break; case 'd' : numero = 3; break; case 'e' : numero = 4; } } Como o valor tomado pela variável numero é igual para os casos do caracter ser a ou b, deixamos sem nenhuma declaração após os :. No fluxo do programa, caso o caracter seja um a, automaticamente teremos como execução a atribuição do valor 1 a variável numero seguida da execução do break, que interromperá o switch-case. A maneira prolixa da primeira versão (com if) foi para dramatizar as vantagens do uso do swith-case. Departamento de Ciência da Computação-UFF Números de página Vamos a mais um exemplo. Agora descreveremos um problema a partir de sequência de instruções, ou seja, de forma algorítmica. No caso será o algoritmo de Euclides para calcular o MDC, ou seja, o Máximo Divisor Comum (quem diria, voltamos à escolinha!). O algoritmo é dado a seguir : i) Sejam dois números inteiros e positivos m e n dos quais queremos achar o mdc. Seja ainda uma variável r. ii) Faça r tomar o valor do resto da divisão de m por n, fazendo então m tomar o valor de n e este último o valor de r. iii) Se r não for nulo, volte a ii). Caso r for nulo, m terá o valor do mdc. O programa que faz tais operações afim de calcularmos o MDC é dado abaixo : main() { /* programa que calcula o mdc de dois numeros m e n */ int m = 120, n = 9, r; do { r = m % n; m = n; n = r; } while(r != 0); } Usamos aqui mais um operador aritmético, o %, que dá o resto da divisão de dois números. No while poderiamos usar no lugar da expressão r != 0 algo como !r. Funcionaria do mesmo jeito com uma aparente vantagem de ser mais compacto sendo, no entanto, mais enigmático. Aqui se apostou em ser claro e, sempre que possível, esta deve ser uma tática de programação. Departamento de Ciência da Computação-UFF Números de página Indentação Repare que em todos os programas já exibidos, cada palavra reservada, cada variável, não é escrita a esmo mas obedece uma regra de posicionamento em cada linha. Esta técnica geralmente é chamada de indentação e é usada em todas linguagens ditas estruturadas. A idéia por trás de tal comportamento não é só fazer programas bonitinhos mas sim, fazer programas mais legíveis. C já é uma linguagem muito concisa e se comecamos a teclar cada linha de qualquer jeito, teremos problemas de legibilidade que poderão dificultar a depuração e manutenção de programas e sistemas. Notem ainda que são colocados espaços entre variáveis e operadores e espaço entre as linhas em muitas situações. O estilo usado aqui é simplesmente um estilo, existindo maneiras diferentes de organizar as linhas de um programa. A linguagem C permite esta liberdade no estilo da escrita de tal forma que podemos modificar ligeiramente a forma de escrever um programa da maneira que mais nos agradar.Os programas estão escritos neste texto de forma relativamente comum não sendo, obviamente, A Maneira Correta mas apenas (repito) uma maneira de se escrever programas em C. É comum se usar de 2 à 5 espaços para marcar a indentação mas isto pode variar de acordo com seu próprio estilo. Por exemplo, podemos escrever uma declaração if como if ( a > b) { c = a + b; d = a * c; .} else { c = a - b; d = a + c * b; } if (a > b) { c = a + b; d = a * c; { else { c = a + b; d = a * c; } if (a > b) { c = a + b; d = a * c; } else { c = a + b; d = a * c; } ou outra maneira qualquer. No entanto, seja lá qual estilo você adotar, tenha em mente melhorar a legibilidade do programa. Para perceber o que queremos dizer, vamos a um caso extremo: o programa do MDC poderia ser escrito como abaixo main(){int m=120,n=9,r;do{r=m%n;m=n;n=r;}while(r!=0);} As vantagens de escrever assim são ainda desconhecidas mas alguns insistem..... Departamento de Ciência da Computação-UFF Números de página FUNÇÕES Forma Clássica Até o momento, sobre funções só foi dito que existiam e que main() era uma função. Agora falaremos de funções de forma mais genérica. Em C as funções podem ser escritas da seguinte forma, (chamada forma clássica) TIPO nome_da_função(parâmetro 1,parâmetro 2,...,parâmetro n) DECLARAÇÃO DE PARÂMETROS { DECLARAÇÃO DE VARIÁVEIS COMANDO1 COMANDO2 . . COMANDOn return(parâmetro de saida); } onde TIPO diz respeito ao tipo de variável que será devolvida pela função. Observe mais uma palavra chave, return. Podemos encontrar esta palavra chave sendo usada de duas maneiras: com o parâmetro de saida entre parênteses e com o mesmo parâmetro logo após o return. Esta palavra é optativa, não havendo necessidade de aparecer em nenhum ponto do bloco principal da função. Também podemos ter vários return dentro de uma função embora isto não seja recomendável. A semelhança da definição de uma função com um programa em C é por uma razão óbvia: o programa principal de C é uma função, como já foi dito. Dependendo da forma de definição de função, você pode colocar a declaração de função antes ou depois da função main(). Aqui colocaremos as funções antes, de forma que um programa em C pode ter a seguinte estrutura (mas ainda não funcionará!): TIPO nome_da_função(parâmetro 1,parâmetro 2,...,parâmetro n) DECLARAÇÃO DE PARÂMETROS { DECLARAÇÃO DE VARIÁVEIS COMANDOS } main() { DECLARAÇÃO DAS VARIÁVEIS COMANDOS } É bom chamar a atenção para que as variáveis declaradas dentro das funções são "invisíveis" para o programa principal ou para as outras funções (daqui a pouco falaremos mais sobre esta "invisibilidade"). As funções sempre devolvem algum valor, mesmo que não haja um return. Se este valor tem significado ou não, quem decide é o programador. Além disto, não há obrigatoriedade de usarmos o valor retornado. Quanto aos argumentos, eles são passados por valor e não por referência. Isto significa que os valores dados através de variáveis do programa principal não serão modificados no programa principal mesmo que você as manipule dentro da função. Se tiver como parâmetros, por exemplo, x e y que valem respectivamente 2 e 3, se houver uma multiplicação entre um e outro atribuindo a x o resultado dentro da função, veremos que o valor de x continuará igual a 2 no programa principal. Isto aparentemente limita o uso mas, como veremos, este fato não se trata de uma limitação mas apenas uma maneira de termos, explicitamente, conhecimento do alcance das operações que serão feitas através das funções. Departamento de Ciência da Computação-UFF Números de página Como exemplo faremos do primeiro programa uma função a qual chamaremos tetof(). int tetof(a,b,teto) int a,b,teto; { int c; if ( (c = a + b) > teto) c = teto; return(c); } main () { int a = 5, b = 3, c, tetof(); /* Primeiro programa, quarta vercao */ c = tetof(a, b, 9); } Novamente o funcionamento é igual. Outro detalhe a saber é que se nada for indicado, ficará pressuposto que a função devolverá um inteiro. A razão disto já sabemos: C trabalha preferencialmente com variáveis do tipo inteiro. Portanto se nada for especificado quanto ao tipo de valor que uma função devolverá, se considerará que o resultado será um inteiro. Se a função devolver um valor não compatível com inteiro, você precisa avisar a função que a chama, o tipo de resultado que ela irá devolver. Isto é feito junto ao definir a função e com a declaração de variáveis. Vamos dizer que uma função é do tipo char e seu nome é funcao1(). Então, na função que a chama deve haver junto, a declaração de variáveis, o que se segue: char funcao1(); Observe que não é necessário colocar os parâmetros da função mas é necessário colocar os parênteses para que o compilador saiba que é uma definição do tipo de valor devolvido pela função e não a declaração de uma variável. Como mais um exemplo, daremos uma função que calcula o fatorial de um número. Lembre-se que o fatorial de um número inteiro positivo não nulo é o produto dele por todos os números positivos não nulos menores que ele. int fatorial(n) int n; { int i, fat; for (i = 1; i <= n; i++) { fat = fat * i; } return(fat); } Departamento de Ciência da Computação-UFF Números de página main() { int fatorial(), n = 5, f; f = fatorial(n); } Repare que na lista de declaração de variáveis encontramos fatorial() colocado como outra variável qualquer. Mas lembre-se, o que queremos dizer é que o valor retornado é inteiro. Chamemos a atenção para alguns aspectos sobre funções em C. Ao contrário de outras linguagens (Pascal, por exemplo) C não permite aninhamento de funções, ou seja, não admite que tenhamos funções declaradas dentro de funções. Ainda temos que todas as funções em C são “visíveis” umas pelas outras. Tal propriedade, no jargão habitual, se traduz como: As funções em C são globais. EXERCÍCIO I) O programa acima só calcula corretamente fatoriais até o de 7 (sete). Porque ? Departamento de Ciência da Computação-UFF Números de página VARIÁVEIS GLOBAIS, LOCAIS E AUTOMÁTICAS Já foi dito que as variáveis declaradas dentro de uma função são "invisíveis" para outras funções. Algumas vezes queremos que uma determinada variável "exista" não só para uma função ou para o programa principal, mas para todas as funções constituintes do programa, ou seja, seja de conhecimento GLOBAL. Partindo da nossa definição imperfeita de programa em C (que ainda não funcionará...), apresentamos como definir tais variáveis: DECLARAÇÃO DE VARIÁVEIS GLOBAIS TIPO nome_da_função(PARÂMETRO 1,PARÂMETRO 2,...,PARÂMETRO n) DECLARAÇÃO DE PARÂMETROS { DECLARAÇÃO DE VARIÁVEIS DECLARAÇÕES } main() { DECLARAÇÃO DAS VARIÁVEIS DECLARAÇÕES } Aqui devemos dar uma parada para dar nomes aos bois. As variáveis declaradas fora das funções (incluindo a main()) são chamadas GLOBAIS e as declaradas dentro das chaves são chamadas de LOCAIS, DINÂMICAS ou AUTOMÁTICAS. Usaremos aqui o termo LOCAIS. Não há uma palavra reservada para declarar variáveis como globais mas existe uma para declarar como automática: ela é auto. Como todas as variáveis em C são criadas como automáticas, raramente (para não dizer nunca) você verá esta palavra (auto) em uso. Outra coisa a ser dita é que as variáveis globais ocupam espaço na memória o tempo todo enquanto as variáveis locais só ocupam espaço enquanto a execução do programa estiver dentro dos blocos nos quais estas variáveis estão declaradas. Quando a execução sai destesblocos, as variáveis locais "evaporam", poupando memória. Além disto, é sempre boa política usar o mínimo de variáveis globais, já que estas são ativas para toda função dentro de um programa podendo, assim, gerar reações inesperadas por ter um trecho de programa alterando uma variável global que em outro trecho era esperada com outro valor. Uso de variáveis globais de forma indiscriminada leva à dificuldades no reaproveitamento de código e dificuldades no acompanhamento do processamento de um programa já construido. Os usuários de FORTRAN são particularmente tentados à usar variáveis globais a todo isntante. Se for o seu caso lembre-se que aqui você está programando em C, pensando em C e que FORTRAN é outro departamento. Departamento de Ciência da Computação-UFF Números de página PREPROCESSADOR: CABEÇALHOS E ARQUIVOS DE INCLUSÃO Imagine que você criasse um conjunto de funções que trabalhasse com a tela do computador, outro conjunto de funções que contivesse todas as funções de entrada e saída de dados e outro conjunto e outro e mais outro. Se você é um programador organizado, talvez achasse interessante que pudesse separar estas funções em coleções de funções e invocar apenas as coleções que fossem necessárias. Em C podemos fazer isto de maneira simples: Basta criar arquivos contendo as funções, constantes e definições relativas às mesmas. Assim, um programa em C poderá ser constituído de uma coleção de vários arquivos. No entanto, tais arquivos podem depender de definições em comum. Como então fazer com que todos os arquivos compartilhem estas definições? O que temos é um tipo especial de arquivo denominado Arquivo de Cabeçalho e é justamente nele que guardamos tais definições. Quanto ao nome deste arquivo, existe a convenção de usar a extensão .h para marcar o tipo deste arquivo. Se você tem uma vasta biblioteca de funções para os mais variados fins (teclado, gráficos, gerenciar arquivos em disco, etc.) poderá invocar apenas as que necessitar. Como é que eu invoco tais coleções é uma coisa ainda não dita. Para fazer esta invocação, usaremos uma declaração do chamado preprocessador, que é mais uma parte integrante da linguagem C. Toda declaração do preprocessador é indicada por começar pelo caracter # (“cardinal” ou “velha”). A que veremos aqui é a declaração #include que se responsabilizará por carregar o cabeçalho invocado, também chamado arquivo de inclusão. A forma geral deste comando é o que se segue #include "nome_do_cabecalho.h" mas poderá ser encontrada também nesta forma #include <nome_do_cabecalho.h> A diferença entre um e outro é que o primeiro (entre aspas duplas) o arquivo de inclusão se encontra no diretório de trabalho corrente, enquanto o segundo (entre < e >) se encontra no diretório padrão onde se localizam os arquivos de inclusão. Obs.: I) Um aspecto que devemos chamar a atenção é que dentro de um arquivo de cabeçalho podemos ter outros #include. O número de aninhamentos destas declarações é dependente de cada compilador. II) Alguns compiladores automaticamente incluem alguns cabeçalhos automaticamente. Antes de ser uma comodidade isto é um problema. Corremos o risco de ter um programa compilando e funcionando numa máquina e não funcionando em outra. Para evitar vícios e (com isto) problemas futuros, seja explícito sempre e configure seu compilador para não aceitar “bibliotecas default”. Com a inclusão do preprocessador, um programa em C pode ter a seguinte forma (que, finalmente, funcionará!): Departamento de Ciência da Computação-UFF Números de página COMANDOS DO PREPROCESSADOR DECLARAÇÃO DAS VARIÁVEIS GLOBAIS FUNÇÃO1(PARÂMETROS) DECLARAÇÃO DE PARÂMETROS { DECLARAÇÃO DAS VARIÁVEIS LOCAIS CORPO DA FUNÇÃO } FUNÇÃO2(PARÂMETROS) . . main() { DECLARAÇÃO DE VARIÁVEIS LOCAIS COMANDOS } A estrutura acima é muito comum de se encontrar, embora possamos colocar comandos do preprocessador no meio de um programa. Falaremos detalhadamente sobre o preprocessador mais adiante. OBS.: Uma pergunta que deve estar surgindo na cabeça do leitor mais atento é: Se toda função retorna um valor a função main() devolve o que? E para quem? Um pouco mais a frente teremos estas respostas. Departamento de Ciência da Computação-UFF Números de página ALGUMAS FUNÇÕES ÚTEIS (Da stdio.h) A linguagem C não tem comandos para impressão. C deixa isto a cargo de funções e aqui vamos aprender como funcionam algumas para podermos trabalhar um pouco mais satisfeitos. Abaixo temos algumas que se encontram em stdio.h, ou seja, entrada e saída padrão (stardard in/out). Primeiro uma que escreve caracteres na tela : printf("CONTROLE", PARÂMETRO1,PARÂMETRO2,....,PARÂMETROn); onde CONTROLE, contém declarações de formato e caracteres. O formato tem as opções: %c caracter simples %d decimal inteiro %f ponto flutuante %e ponto flutuante em notação científica) %o octal %s cadeia de caracteres %u decimal inteiro sem sinal %x hexadecimal Há ainda caracteres especiais (alguns já apresentados quando falamos de constantes), alguns dos quais estão listados abaixo: \b retrocesso \f saltar página (ou limpar tela) \n saltar para próxima linha \r retorno de carro \t tabulação horizontal \0 nulo \\ Barra reversa Um exemplo. printf(" i = %d \n x = %f -- y = %f", i,x,y); Se i for igual a 1, x igual a 2.345679 e y igual a 3.141582, a saída será da forma : i = 1 x = 2.345679 -- y = 3.141582 Observe: o que não for especificação de formato ou caracter especial é escrito como foi colocado. Os valores impressos são ajustados para a esquerda. Além disto, se passamos um número de ponto flutuante para esta função, ele sairá com todos os algarismos que o seu tipo permitir. Podemos formatar a saída limitando o número de casas decimais num ponto flutuante ou posicionando as variáveis de maneira mais conveniente. O formato para ponto flutuante é o que se segue [-]m.d onde o sinal - indicaria ajuste a esquerda, m o espaço total e d o número de casa decimais que devem parecer. O simples fato de querermos formatar a saída, faz com que o ajuste se faça pela direita, daí a necessidade do sinal negativo para indicarmos que queremos que o ajuste continue pela esquerda. Isto é feito colocando antes do símbolo de porcentagem um número que corresponderá ao espaço que será reservado para a variável em questão. No caso de um número de ponto flutuante, podemos dar não só o espaço para o número a ser impresso como também o número de casas decimais depois do ponto decimal adicionando após o número de espaços reservados para o número a ser impresso um ponto seguido do número de casas decimais. Abaixo temos várias opções de formato escritas a partir do exemplo acima não formatado e com suas correspondentes saídas. a) printf("\n i = %10d x = %10f -- y = %10f", i,x,y); i = 1 x = 2.345679 -- y = 3.141582 Departamento de Ciência da Computação-UFF Números de página b) printf("\n i = %-10d x = %-10f -- y = %-10f", i,x,y); i = 1 x = 2.345679 -- y = 3.141582 c) printf("\n i = %-10.4d x = %-10.4f -- y = %-10.4f", i,x,y); i = 0001 x = 2.3456 -- y = 3.1415 Outras funções úteis são especificadas abaixo: a) ch = getch() - Devolve em ch um inteiro sem sinal correspondente ao caracter lido no teclado. (Esta função se encontra definida em conio.h (complementar in/out)) b) putchar(ch) - Manda um caracter para a tela. c) exit(i) - Interrompe a execução do programa. Se i = 0, indica uma finalização normal. Façamos mais um programa. Este deverá, inicialmente, escrever na tela um menu com o seguinte conteúdo : <1> : imprime em formato ASCII <2> : imprime em octal <3> : imprime em decimal <4> : imprime em hexadecimal<Outras> : sai do programa e após escrever uma mensagem para entrar com um caracter. Feito isto, leia o caracter, imprima no formato pedido e volte ao menu. Departamento de Ciência da Computação-UFF Números de página #include <stdio.h> #include <conio.h> main() { do { int ch, opcao; printf(" <1> : imprime em formato ASCII \n"); /* Impressao do cabecalho */ printf(" <2> : imprime em octal \n"); printf(" <3> : imprime em decimal \n"); printf(" <4> : imprime em hexadecimal \n"); printf("<Outras> : sai \n"); printf(" Entre com uma opção "); opcao = getch(); printf("\n"); printf(" Entre com um caracter "); ch = getch(); switch (opcao) { case '1' : printf("\n ASCII - %c\n",ch); break; case '2' : printf("\n octal - %o\n",ch); break; case '3' : printf("\n decimal - %d\n",ch); break; case '4' : printf("\n hexadecimal - %x\n",ch); break; default : exit(0); } } while(1); } Observe o uso do while com a expressão de controle permanentemente verdadeira (diferente de zero) e, portanto, criando um laço sem fim. Assim, a única forma de saída do programa é através do apertar de qualquer tecla, menos as válidas, pois isto forcaria a execução da função exit() que, como já vimos, interrompe o programa. Faça alguns testes e você verá que se você entrar com, por exemplo, uma letra teremos um número como resposta, se pedirmos uma opção diferente da de número 1. Isto não deve causar estranheza pois os formatos de saída do printf(), nas outras opções, são do tipo numérico. Lembramos que ao apertarmos a tecla correspondente a um caracter ou outra qualquer, não estamos na realidade "mandando" uma letra para o computador mas simplesmente um código. Como os caracteres tem nos computadores uma representação interna numérica dada por um determinado padrão, fica fácil de entender os números que vão surgindo durante o uso deste programa. Você encontrará tabelas de algumas destas representações num dos apêndices deste texto. Se examinarmos o programa acima, encontraremos um pequeno defeito. Se você quiser sair do programa o mesmo pedirá para você entrar com um caracter, o que é totalmente desnecessário. Vamos criar uma versão deste programa no qual evitaremos este problema. Departamento de Ciência da Computação-UFF Números de página #include <stdio.h> #include <conio.h> int le_char() { printf(" Entre com um caracter "); return (getch()); } main() { do { char ch, opcao, le_char(); /* Impressao do cabecalho */ printf(" <1> : imprime em formato ASCII \n"); printf(" <2> : imprime em octal \n"); printf(" <3> : imprime em decimal \n"); printf(" <4> : imprime em hexadecimal \n"); printf("<Outras> : sai \n"); /* --------------------- */ printf(" Entre com uma opcao "); opcao = getch(); printf("\n"); switch (opção) { case '1' : ch = le_char(); printf(" ASCII - %c\n",ch); break; case '2' : ch = le_char(); printf(" octal - %o\n",ch); break; case '3' : ch = le_char(); printf(" decimal - %d\n",ch); break; case '4' : ch = le_char(); printf("hexadecimal - %x\n",ch); break; default : exit(0); } } while(1); } Criamos uma função com o objetivo de mandar uma mensagem para a tela e ler o caracter. Daqui a pouco faremos uma outra versão um pouco mais interessante. OBS.: A forma que usamos o while (colocando uma constante dentro dos parênteses) NÃO DEVE SER IMITADA. Observe que 1 não tem significado nenhum em si. Devemos sempre usar uma variável ou uma definição (isto veremos abaixo) no lugar de colocar constantes “mágicas”. Comentários ao lado podem até esclarecer o funcionamento mas o ideal é que deixemos a variável "falar por si", ou seja, que ela tenha um "nome" que já seja o suficiente para mostrar a sua função. Departamento de Ciência da Computação-UFF Números de página Mais um exemplo será útil para marcar as diferenças entre o for de C e laços semelhantes de outras linguagens. Aqui teremos duas variáveis sendo “contadas” pelo mesmo for mas em “ritmos” diferentes. main() { int impr, i, nimpr, nt; nt = 100; nimpr = 10; for (impr = 1, i = 1; impr < nt; impr += nimpr, i++) { printf("\n impr = %d ### i = %d \n", impr, i); } } Neste caso, serão impressos os valores de impr que irão de 1 até 91 de 10 em 10, ou seja, serão gerados os números 1, 11, 21,...,91 enquanto i irá de 1 até 10 estando o laço sob controle da condição sobre impr. EXERCÍCIOS I)Experimente não usar a palavra reservada break no programa três, terceira versão. II) Use a função printf() para observar as saídas de todos os programas dados anteriormente como exercício. (Não se esqueça de colocar #include <stdio.h>). III) Desenvolva uma função que imprima um inteiro ou um caracter em forma binária. Feito isto, utilize-a no programa acima, criando outra opção no menu. Departamento de Ciência da Computação-UFF Números de página PONTEIROS E FUNÇÕES Vamos agora falar de um tipo de variável extremamente poderosa: o ponteiro, também chamado apontador. Recordemos que existem dois operadores chamados operadores de endereço e estes são representados por: & "o endereço de" . Ex. : a = &b. a recebe o endereço da variável "b". * "no endereço de". Ex. : c = *d. c recebe o valor da variável "apontada" por d. Observemos que num computador, quando escrevemos algo como : a = b; O computador faz as seguintes operações: pega o conteúdo da posição da memória correspondente a variável b, coloca este valor na posição da memória correspondente a variável a. Ou seja, é uma transferência do conteúdo de endereço para conteúdo de endereço. Os operadores de endereço nos permite obtermos endereços de variáveis ou tendo endereço de uma variável, saber seu conteúdo. As variáveis que contém os endereços de outras variáveis denominamos PONTEIROS ou APONTADORES. É claro que aqui surge uma suspeita: Como as variáveis tem estruturas diferentes, provavelmente teremos tipos diferentes de ponteiros para cada tipo de variável. Para usarmos o ponteiro certo com a variável certa, teremos que definir os ponteiros tal como fazemos com as outras variáveis. A notação é a que se segue TIPO *nome_d_ponteiro_1, *nome_do_ponteiro_2, ...; Podemos fazer, se quisermos, a definição dos ponteiros junto com as das variáveis. Lembre-se sempre que um ponteiro não tem "nenhuma" informação, o que ele tem é o endereço de uma informação. Mas, neste ponto, alguém pode perguntar: Porque o título desta parte é PONTEIROS E FUNÇÕES? É justamente aqui que veremos uma dos usos dos operadores de endereço. Para demonstrar esta utilidade, daremos um exemplo clássico de uma função que NÃO FUNCIONA. Abaixo temos escrita uma função que faria a permuta entre duas variáveis. #include <stdio.h> permutar(a,b) int a,b; { int auxiliar; auxiliar = a; a = b; b = auxiliar; } main() { int a = 5, b = 3, c = 7; permutar(a,b); permutar(a,c); printf(" %d, %d, %d .",a,b,c); } O que aconteceria se executássemos este programa? As variáveis a e b não teriam os seus valores trocados. Lembrem-se que as variáveis, como estão declaradas são locais e que funções em C só trabalham passando valores. O que fizemos dentro da função permutar() não afetará o conteúdo das variáveis a, b e c do programa principal. Departamento de Ciência da Computação-UFF Números de página A saída desta situação não esta em declarar a, b e c como variáveis globais, já que assim você sópoderia intercambiar estas variáveis. A função ficaria restrita ao programa que você esta fazendo e a certas variáveis de entrada. A saída esta no fato de que a diferença entre as variáveis locais de cada função (a permutar() e a main ()) é que elas ocupam posições diferentes na memória. Se passarmos o endereço da variável em vez do valor da variável, temos uma maneira de afetar este valor. Vejamos uma versão modificada do programa anterior: #include <stdio.h> permutar(a, b) int *a,*b; { int auxiliar; auxiliar = *a; *a = *b; *b = auxiliar; } main() { int a = 5, b = 3, c = 7; permutar(&a, &b); permutar(&a, &c); printf(" %d, %d, %d .", a, b, c); } Através do operador & ("o endereço de"), passamos para a função permutar() os endereços de a e b do programa principal. Ao entrar na função, observemos que os parâmetros foram declarados com o uso do operador * ("no endereço de"). Portanto *a e *b são os conteúdos das variáveis a e b do programa principal. No momento que eu estou fazendo as trocas dentro da função, estamos na verdade trocando os conteúdos das variáveis a e b do programa principal! Uma pergunta a ser feita é: Já que toda função em C retorna algo, como fica permutar() que não precisa devolver nada? A intenção em não colocar nada na declaração do tipo de saída é tentar passar a idéia de que qualquer coisa retornada pela função não tem significado. No entanto, esta não é uma boa maneira. O ANSI C apresenta uma solução e a veremos mais a frente. Repare ainda o formato de saída das variáveis. Departamento de Ciência da Computação-UFF Números de página PONTEIROS E VETORES Vetores são encontradas em muitas linguagens e C não escapa disto. Se declara um vetor em C da seguinte maneira: TIPO nome_do_vetor [TAMANHO] com uma ressalva: vetores em C começam do índice 0 (zero) e acabam em TAMANHO menos um. Por exemplo int vetor[10]; float evento[4]; Acima estamos declarando um vetor inteiro de dez posições e um vetor de ponto flutuante de quatro posições. O elemento vetor[0] é o primeiro e vetor[9] é o último. Analogamente para o vetor evento, o primeiro elemento será evento[0] e o último evento[3]. No caso de cadeia de caracteres temos uma situação especial. Se declararmos o vetor char titulo[10] teremos uma cadeia de caracteres de no máximo 9 (NOVE) caracteres e não dez. Uma das características de C é que toda cadeia de caracteres tem seu fim marcado por um caracter nulo. (Como atribuir à um vetor uma cadeia de caracteres? Um momentinho só! Já veremos isto.) Então lembre-se sempre : "Para trabalhar com cadeias de caracteres, devemos deixar espaço para o caracter terminador da cadeia (o nulo). " Outra particularidade : "Não há verificação dos limites de um vetor. " O esquecimento deste fato é uma grande fonte de erros. Mas o que os vetores tem com os ponteiros? Para explicar isto, de novo vamos falar de como o computador trabalha só que agora com vetores. Um vetor é geralmente armazenado na memória do computador em posições contíguas. Tendo a primeira posição de memória (que chamaremos de posição zero), acrescentando o índice do vetor a esta posição multiplicado pelo tamanho do tipo de variável, teremos a posição do elemento do vetor correspondente ao do índice. Em C um vetor, na verdade, é um ponteiro para o início da seqüência de elementos do vetor. Podemos, então, referenciarmos elementos do vetor tanto pelos índices como através de ponteiros. Devemos aqui chamar a atenção que, o trabalho com ponteiros é mais rápido que o trabalho com os índices. Esta opção dupla existe já que certos algoritmos (como os de ordenação) são mais simples de serem implementados se a referência é feita por índice enquanto outros ficam mais simples com o uso de ponteiros. Como exemplo, criaremos uma função que escreve na tela uma cadeia de caracteres usando a função putchar(), já descrita. #include <stdio.h> putstr(s,n) /* Setimo programa */ char *s; int n; { int i; for (i = 0; i < n; ++i) { putchar(s[i]); } } main() Departamento de Ciência da Computação-UFF Números de página { char s[10]; . . putstr(s,10); } Você terá que passar como parâmetros não só o vetor como também o seu tamanho. Na verdade, não há a necessidade de passar o número de elementos. Poderíamos usar uma palavra chave que no informa o tamanho de uma variável, mas não iremos nesta direção. Mas, e se lembramos que uma cadeia de caracteres termina por nulo? Vejamos uma versão do programa acima que se utiliza disto. #include <stdio.h> putstr(s) char *s; { int i; for (i = 0; s[i]; ++i) { putchar(s[i]); } } main() { char s[10]; . . putstr(s); } Observe a condição de parada do for feita usando o fato que em C, uma cadeia de caracteres termina num nulo. Ficou bem elegante, embora um tanto quanto desagradável. Ainda podemos melhora-la. #include <stdio.h> putstr(s) char *s; { while (*s) putchar(*s++); } main() { char s[10]; . . putstr(s); } Observe que, com ponteiros, a função fica mais simples e ainda mais elegante. Esta função, ou melhor, sua equivalente, está na biblioteca padrão de C com o nome de puts(). Departamento de Ciência da Computação-UFF Números de página ARITMÉTICA DE PONTEIROS O número de operações aritméticas que podem ser feitas com os ponteiros é bem restrita, pela própria natureza deles. Lembre-se que o ponteiro em si não tem nenhum significado. Ele apenas nos diz onde se encontra a informação que queremos acessar. É bom sempre ter na cabeça que : " Ponteiros não são inteiros." Como a estrutura de endereçamento pode variar de computador para computador, não necessariamente o endereço pode ser representado por um número inteiro. Além disto, mesmo que pudéssemos representar os ponteiros como inteiros, obviamente os conceitos envolvidos são totalmente diferentes e, lembro mais uma vez, não devem ser misturados. Abaixo tabelamos os operadores que são válidos em operações com ponteiros. Suponhamos dois ponteiros p e q e um inteiro i: ++p pré-incrementa o valor do ponteiro --p pré-decrementa o valor do ponteiro p++ pós-incrementa o valor do ponteiro p-- pós-decrementa o valor do ponteiro *p acessa o conteúdo do endereço apontado por p p+i Soma de um ponteiro com um inteiro p-i Subtração de um inteiro sobre o ponteiro p-q Subtração entre ponteiros desde que apontem para o mesmo vetor Os operadores de incremento e de decremento com ponteiros, trabalham de tal forma que levam em consideração o tipo de ponteiro que está sendo usado. Portanto se estamos usando um destes operadores com um ponteiro de inteiro, automaticamente o incremento (ou decremento) será de dois bytes. Se temos um ponteiro para ponto flutuante do tipo float, o incremento (ou decremento) será de quatro bytes. Algumas vezes é interessante compararmos ponteiros. Imagine que tenhamos dois ponteiros p1 e p2 (p1 > p2), que referenciam um determinado vetor. Logo p1 - p2 nos dará o número de elementos entre estes ponteiros. Observe, no entanto, que não há sentido em compararmos ponteiros que não estejam referenciando a mesma estrutura de dados. Departamento de Ciência da Computação-UFF Números de página INICIALIZANDO VETORES Podemos inicializar os valores dos vetores no momento da declaração desde que eles tenham sido declarados globais. A forma geral é a seguinte TIPO nome[TAMANHO] = {ELEMENTO0, ELEMENTO1,..,ELEMENTO(n-1)); e para o caso especial de vetores do tipo char char nome[TAMANHO + 1] = "cadeia de caracteres"; ou char nome[] = "cadeia de caracteres"; com o fato interessante de não determinarmos o tamanho da cadeia. Mais uma gentileza do compilador que dimensiona por você. Se a entrada da cadeia de caracteres
Compartilhar