Baixe o app para aproveitar ainda mais
Prévia do material em texto
PDF gerado usando o pacote de ferramentas em código aberto mwlib. Veja http://code.pediapress.com/ para mais informações. PDF generated at: Fri, 06 Dec 2013 10:28:06 UTC De Objective Caml para C e C++ Conteúdo Páginas Introdução 1 Os tipos básicos 5 Variáveis e funções 14 Instruções 17 Construção de tipos 33 Introdução à programação orientada a objetos 50 Referências Fontes e Editores da Página 53 Fontes, Licenças e Editores da Imagem 54 Licenças das páginas Licença 55 Introdução 1 Introdução A linguagem C é uma linguagem bastante simples, criada na década de 70 e pouco atualizada desde então. A linguagem C provê uma camada de abstração em cima do nível de linguagem de assemblagem. Essa camada é composta por tipos básicos (basicamente tipos numéricos), construtores de tipos simples, construções algorítmicas clássicas, e modularização funcional. Os principais pontos fortes da linguagem C são uma ampla difusão, a rapidez de execução dos programas compilados, e a possibilidade de interação com recursos de hardware. A principal desvantagem é que não possui conceitos de linguagens de programação mais avançados, o que pode penalizar a produtividade dos programadores naquela linguagem. A facilidade de acesso aos recursos de hardware, como a memória principal, é uma faca de dois gumes: permite programas muito rápidos, e falhas extremamente súteis (do tipo que se manifesta só em um determinado ambiente, em condições de uso muito específicas e que podem levar semanas a serem identificadas e corrigidas). A linguagem C++ é uma linguagem derivada de C, e inclui novos paradigmas de programação como a orientação a objetos, tipos referências, polimorfismo paramétrico. C++ oferece muito mais recursos que C e é uma linguagem significativamente mais complexa e difícil de se dominar. Uma propriedade interessante é a sua compatibilidade quase total com a linguagem C, fazendo com que código C possa ser compilado por um compilador C++. Essa compatibilidade se estende até o código objeto: código C++ pode fazer uso de código desenvolvido em C. Embora a linguagem C++ seja bastante complexa, felizmente, não é necessário entender e dominar todos os recursos providos por essa linguagem para poder utilizá-la nos seus projetos de programação. As linguagens C e C++ possuem ambas uma biblioteca padrão que provê algumas extensões práticas como a possibilidade de realizar entradas e saídas, de interagir com o sistema de arquivos. A biblioteca padrão de C oferece alguns poucos recursos de algoritmos e estruturas de dados. Em compensação a linguagem C++ possui uma biblioteca bem mais rica que Diferente de Objective Caml, que pode-se interpretada ou compilada para bytecode e linguagem nativa, as linguagens C e C++ podem apenas ser compiladas. As diferentes atividades que compõem o processo de trabalho com essas linguagens são: •• Edição: A grande maioria dos editores de texto possuem destaques de sintaxe para as linguagens C e C++. As duas principais são vi (e seus descendentes vim e gvim) e emacs (ou seu primo xemacs). Os editores de texto geralmente são configurados para oferecer destaque de sintaxe para a linguagem C quando o arquivo editado possui o prefixo .c e .h, e para a linguagem C++, quando o nome do arquivo é prefixado com ".C", ".cc", ".cpp" e ".hh". •• Compilação: Existem diversos compiladores para essas linguagens. Os compiladores gcc e g++, além de serem gratuitos e de código aberto, podem ser utilizados em qualquer plataforma (Linux, Free BSD, Windows, MacOS). •• Execução: O código é compilado em linguagem nativa da plataforma e só pode ser executado naquela plataforma. Um primeiro programa em C++ Esse primeiro exemplo vai ilustrar passo a passo as diferentes fases de criação de um programa. Iremos desenvolver em C++ um programa que imprime uma linha na saída padrão com o texto "ola." e que retorna o valor 0. Em Objective Caml, poderíamos programar isso com o seguinte código: let _ = Printf.printf "ola.\n"; 0 O código seguinte é equivalente ao anterior, e fica mais próximo da versão C++: Introdução 2 let main () = Printf.printf "ola.\n"; 0 let _ = main() Agora vamos para o C++... Você vai reparar que é um pouco mais complexo, mas fundamentalmente não é muito diferente. Para começar, devemos 'criar um programa'. Abra então um editor de texto e cria um novo documento. Inclui o seguinte texto no documento sendo editado: #include <iostream> int main () { std::cout << "ola.\n" return 0 } Guarda o conteúdo do documento em um arquivo, por exemplo chamado exemplo1.cpp. Temos então nosso primeiro programa em C++! O que faz? Discutiremos isso depois, pois vamos partir logo para a segunda fase, que é a de 'compilação'. Para isso, devemos utilizar o compilador C++. Podemos fazer isso através de um console de comandos. Abra então um console de comandos e digite o seguinte comando para compilar o arquivo: g++ -ansi -Wall exemplo1.cpp Esse comando é composto pelo nome do compilador (g++), duas opções (-ansi e -Wall) e o nome do arquivo a ser compilado. A opção -ansi instrui o compilador a verificar que você está seguindo as recomendações do padrão ANSI da linguagem C++. Isso garante a portabilidade de seu código. A opção -Wall é ainda mais importante, pois ela instrui o compilador a reportar avisos sobre erros possíveis no seu programa. A experiência prova que 99% dos erros possíveis são erros de fatos. Vale então a pena utilizar esse recurso para descobrir (pelo menos parte dos) erros que inevitavelmente são cometidos pelos programadores: 'errare humanum est'. Deve aparecer a seguinte mensagem no console: exemplo1.cpp: In function 'int main()': exemplo1.cpp:6: error: expected `;' before '}' token O compilador indica que na função int main(), há um erro, localizado na linha 6 (pode ser que você tenha introduzido linhas adicionais no seu editor, neste caso o número da linha onde ocorre esse erro poderá ser diferente). O que aconteceu? Bom, a mensagem de erro é bastante clara: o compilador estava esperando o caractere ; antes do }. E o compilador está certíssimo, esquecemos de colocar um ponto e vírgula no final da linha 5 e quando encontrou } o compilador reparou essa falta! Corrija então esse erro no seu editor, salve novamente no arquivo e repete o comando de compilação. Agora não há mais erro detectado pelo compilador. Vamos então executar o nosso programa... Mas onde está ele? Lista o conteúdo do diretório atual (o comando ls permite fazer isto) e observará a presença de um programa chamado a.out. Ao menos que seja instruído de outra forma, o compilador gera um programa em um arquivo com esse nome esquisito. Para gerar o programa com um outro nome, deve se utilizar uma opção específica do compilador, que é -o seguido do nome desejado. Por exemplo, se desejarmos chamar esse programa de exemplo1, o comando seria: Introdução 3 g++ -ansi -Wall exemplo1.cpp -o exemplo1 Para executar o programa, basta digitar no console o caminho até o arquivo que contem esse programa, por exemplo: ./exemplo1 Aparece então a seguinte mensagem: ola. Ufa!... Escrevemos o nosso primeiro programa em C++, e vimos como compilá-lo. Aprendemos três opções muito úteis do compilador g++, e verificamos que o compilador tem um papel importante para apoiar o desenvolvimento de programas corretos através da emissão de mensagens de erro e de avisos. Explicações complementares Para terminar essa introdução, precisamos ainda explicar como esse programa funciona. No processo começaremos a aprender algumas coisas da linguagem C++. Então, o que faz esse programa? Vamos incluí-lo novamente, dessa vez identificando as linhas com números: 1 #include <iostream> 2 int main () 3 { 4 std::cout << "ola.\n"; 5 return 0; 6 } Nós não vamos entrar muito em detalhes aqui. A linha 1 faz referência a um arquivo chamado iostream,o qual faz parte da biblioteca padrão do C++. Este arquivo contém declarações de tipos e funções relacionadas com fluxos de entrada e saída (iostream abrevia input/output stream), ou seja com leitura e escrita de textos. Em C e em C++, todas as linhas iniciando com o caractere # ('jogo da velha') são 'diretivas de pré-processamento'. Nessas linguagens, a compilação começa com uma fase dita de pré-processamento que realiza diversas operações de manipulação do código fonte como a inclusão de arquivos. Por exemplo, a diretiva #include resulte na inclusão do conteúdo de um arquivo. As linhas 2 a 6 contém a definição de uma função, cujo nome é main (informações importantes sobre esse nome são dadas no final desta seção). Antes do nome da função vem o tipo do resultado da mesma: aqui é o tipo int que corresponde a números inteiros. Depois do nome da função, vem uma lista de parâmetros entre parênteses. Aqui a lista é vazia e não há parâmetros. main portanto é uma função que não tem argumentos, e que retorna um valor do tipo int. Em Caml esse tipo é denotado unit->int. Depois da lista de parâmetros, vem o corpo da função, colocado entre chaves. Neste exemplo, o corpo dessa função é uma seqüência composta de dois comandos (ou instruções). O primeiro comando (linha 4) aplica o operador de impressão C++, que se escreve << (menor menor). Esse operador é infixo e tem dois argumentos: o primeiro, a esquerda, é um fluxo de saída, no caso é std::cout, a saída padrão, o segundo argumento é o valor que será impresso, no caso é o texto "ola.\n". O resultado da aplicação deste operador é o próprio fluxo de saída. Assim, ele pode ser encadeado, como no exemplo seguinte: std::cout << "Oi\n" << "Tudo bem?"; O segundo comando (linha 5) é uma instrução pré-definida da linguagem C++ que, tem um argumento, e instrui que a função deve terminar a sua execução e retornar o valor dado em argumento, no caso o valor 0. Introdução 4 Agora que explicamos cada linha do exemplo, devemos voltar ao nome da função: main. As linguagens C e C++ têm como convenção que a execução de um programa sempre inicia com a chamada da função chamada main. O valor que retorna a função main é comunicado ao sistema operacional e pode ser usado para informar da ocorrência ou não de algum problema. Por convenção, o valor 0 indica uma execução bem-sucedida. Portanto, quando irá desenvolver um programa nessas linguagens, sempre deverá definir uma função com esse nome, que será o ponto inicial da execução desse programa. Um primeiro programa em C O programa C seguinte é equivalente ao programa C++ dado em exemplo no parágrafo anterior. #include <stdio.h> int main () { fprintf(stdout, "ola.\n"); return 0; } Para experimentar a programação em C, abre um editor de texto, cópia o código dado e grave o mesmo em um arquivo chamado exemplo2.c. Para compilar esse arquivo e gerar um programa executável nomeado exemplo2, entre com o seguinte comando no console: gcc -ansi -Wall exemplo2.c -o exemplo2 Então quais são as diferenças com o programa C++? Basicamente, são reduzidas ao uso de funções diferentes para realizar a impressão da mensagem ola. na saída padrão. Em C, utiliza-se uma função nomeada printf que é disponibilizada na biblioteca padrão através do arquivo stdio.h. Observe a similaridade com o comando Printf.printf da linguagem Objective Caml: o primeiro argumento é o fluxo de saída (stdout designa a saída padrão em C), e o segundo argumento é um texto a ser impresso. Em Objective Caml, como em C, esse texto pode ter diretivas de formatação de valores que são então passados como argumentos adicionais à função fprintf. Lembre-se... •• C e C++ não possuem interpretadores: Código nessas linguagens deve ser compilado para poder ser executado. • Um compilador para C é o programa gcc, para C++ pode usar o g++. • Um programa C ou C++ deve possuir uma função chamada main. Um programa sempre começa a se executar pela função main. Os tipos básicos 5 Os tipos básicos Introdução O sistema de tipos de uma linguagem de programação é composta por: •• Tipos básicos, que são os elementos de base para representar informações mais simples e construir tipos mais complexos • Construtores de tipos, que tem como papel combinar tipos mais elementares para construir tipos mais complexos; •• Regras de conversão entre tipos, que definem se e como valores de um tipo podem ser convertidos para um outro tipo. O sistema de tipos também possui os construtores de tipos, que são apresentados em um módulo específico sobre . Ainda possui regras que definem quando, e como valores de um tipo podem ser convertidos entre se. Os tipos básicos de C e de C++ são apresentados nesse módulo. Primeiro lembramos os tipos básicos de Objective Caml são unit, bool, char, int, float e string. Os tipos básicos de C++ são: • void: o tipo vazio, • bool: o tipo booleano, • int: o tipo inteiro, • char: o tipo dos caracteres, • float e double são tipos para os números decimais. A seguinte tabela provê uma correspondência entre os tipos básicos de C e C++ e os de Objective Caml. Objective Caml C e C++ - void unit - char char int short int, long int, long long int float float, double, long double string - (*) (*) As linguagens C e C++ possuem construções para representar textos (string), mas essas não fazem parte conjunto dos tipos básicos. A biblioteca padrão da linguagem C fornece funções de tratamento de textos utilizando uma técnica baseada em ponteiros para caracteres. Essas funções são disponíveis na linguagem C++ que ainda fornece, através de sua biblioteca padrão, um tipo nomeado string e funções que manipulam valores desse tipo. O tipo vazio Em Objective Caml, o tipo unit é o tipo das computações seqüênciais e possui apenas um valor, denotado (). Em C e C++, existe um tipo similar, embora mais simples ainda! Trata-se do tipo void, que não possui valor algum. Ele é usado para definir funções que não retornam resultado. Booleanos A linguagem C++ possui um tipo para representar os booleanos. Tem como nome bool e os valores true e false, representando respectivamente verdadeiro e falso. Os operadores booleanos são: • negação: !, • conjunção: &&, Os tipos básicos 6 • disjunção: ||. É importante notar que os operadores booleanos binários possuem uma ordem pré-definida para a avaliação de seus operandos. • A avaliação da expressão c1 && c2 começa pela avaliação da sub-expressão c1. Se a mesma for igual ao valor false, a avaliação da expressão completa é terminada e resulta no valor false. Caso contrário, a segunda sub-expressão c2, é avaliada e o resultado sera o da expressão completa. • Similarmente, na avaliação de uma disjunção c1 || c2, caso a avaliação da sub-expressão resulte no valor true, então é concluída a avaliação da expressão completa, resultando no valor true. Caso contrário, o valor da expressão completa é o da sub-expressão c2. Existe um quarto operador, que pode ser chamado operador condicional. É similar à construção if...then...else de Objective Caml. A sintaxe é ... ? ... : ..., onde cada ... representa um argumento. Possui três argumentos, sendo que o primeiro é uma condição e os dois últimos devem ter tipos compatíveis. O valor da expressão é o valor do segundo argumento se a condição for verdadeira, do terceiro argumento caso contrário. Considere, como exemplo, a seguinte expressão: i >= 0 ? 1 : -1 O valor dessa expressão será caso o valor de i for maior ou igual a zero, e caso contrário (>= é o operador maior ou igual). No caso do operador condicional, o segundo argumento é avaliado apenas se o resultado da avaliação do primeiro for true, enquanto que o terceiro argumento será avaliado apenas quando o resultado da avaliação do primeiro for false. Booleanos na linguagem C Na versão inicial da linguagem C, não existia tipo para os booleanos. Em C, as condições possuem o tipo inteiro,onde a condição falso é representada pelo valor 0, e a condição verdadeiro por qualquer valor não nulo. A biblioteca padrão da linguagem C, versão 1999, existe uma definição para o tipo booleano que é idêntica à da linguagem C++. Para ter acesso a essa definição, o programador deve incluir um arquivo utilizando o comando seguinte no início do arquivo: #include <stdbool.h> Impressão de valores booleanos Para imprimir um valor do tipo booleano, pode-se utilizar a função printf e o operador <<. Em impressão no estilo C, temos então: // exemplo de programa C++ que imprime os dois valores do tipo bool, utilizando uma função de impressão herdada de C #include <cstdio> int main () { fprintf(stdout, "verdadeiro: %i\n", true); fprintf(stdout, "falso: %i\n", false); } Um programa de impressão de valores booleanos no estilo C++ seria: // exemplo de programa C++ que imprime os dois valores do tipo bool, utilizando o operador de impressão do C++ #include <iostream> using namespace std; int main () Os tipos básicos 7 { cout << "verdadeiro: " << true << endl; cout << "falso: " << false << endl; } A execução de ambos programas resultará na seguinte impressão na saída padrão: verdadeiro: 1 falso: 0 Caracteres As linguagens C e C++ possuem ambas um mesmo tipo para representar caracteres: trata-se do tipo denominado char. Os valores desse tipo podem ser denotados colocando o caractere entre aspas simples. Nesse aspecto Objective Caml é muito similar, pois adota exatamente as mesmas convenções. A linha seguinte possui alguns valores do tipo char: 'a' 'A' '0' '\n' '\\' '\042' Esses valores representam, respectivamente, a letra a minúsculo, a letra a maiúsculo, o algarismo zero, o caractere especial de quebra de linha (caracteres especiais são precedidos de uma contra-barra), o caractere de contra-barra, e o caractere cujo código ASCII é 42 (o caractere de aspa dupla). Na verdade, as linguagens C e C++ tratam o tipo char como um tipo inteiro, onde oito bits são representados para representar os valores. São portanto 256 valores diferentes. Existem duas variações: • signed char, correspondendo aos números de -128 até 127; • unsigned char, correspondendo aos números de 0 até 255. Isso significar que 'a' + 1 é uma expressão legal, tanto em C quanto em C++, e ela é igual a 'b'. Impressão de caracteres Para imprimir um valor do tipo char, pode-se utilizar a função printf e o operador <<. Em impressão no estilo C, temos então: // exemplo de programa C++ que imprime um valor do tipo char, utilizando uma função de impressão herdada de C #include <cstdio> int main () { fprintf(stdout, "%i - %c\n", 'a', 'a'); } Note que o texto de impressão possui duas diretivas de formatação. A primeira é %i: manda imprimir o (primeiro) valor 'a' como se fosse um valor do tipo int. A segunda é %c e manda imprimir o (segundo) valor 'a' como se fosse um valor do tipo char. A saída resultante é: 97: a No parágrafo anterior, observe nosso grifo em como se fosse. O que acontece efetivamente é o seguinte. A diretiva %i indica que o argumento correspondente é um valor do tipo int. No caso, o valor é do tipo char. O que acontece aqui é que o compilador insere uma conversão do valor do tipo char para o tipo int. Em C, ou em C++, essa conversão é realizada automaticamente, e o compilador nem emite um aviso que foi feita essa conversão. Isto é um exemplo bastante esclarecedor da diferença de rigor entre o sistema de tipo de C e C++ e o de Objective Caml, onde seria exigido a aplicação de uma função para converter os valores desses tipos. Os tipos básicos 8 Um programa de impressão de valores booleanos no estilo C++ seria: // exemplo de programa C++ que imprime os dois valores do tipo bool, utilizando o operador de impressão do C++ #include <iostream> using namespace std; int main () { cout << 'a' << endl; } A execução desse programa resultará na seguinte impressão na saída padrão: a Números inteiros As linguagens C e C++ possuem uma variedade de tipos inteiros. Esses tipos são pré-definidos e variam em função de dois aspectos: •• presença ou não de sinal; •• tamanho da representação binária, que vai de um até oito bytes. Vamos olhar para o exemplo mais simples, que é o tipo char, já discutido na seção sobre caracteres. Um char é um tipo inteiro codificado com um byte, ou seja oito bits. Possui portanto valores diferentes. São duas variantes para esse tipo: com sinal ou sem sinal. Na variante com sinal, denominada signed char, ou simplesmente char os valores vão de -128 até 127. Na variante sem sinal, denominada unsigned charos valores ficam na faixa de 0 até 255. Você pode verificar quem, em ambos casos, temos 256 valores diferentes. Existem quatro tamanhos possíveis que são um, dois, quatro e oito bytes. Acabamos de (re)ver o caso do tamanho de um byte. Os demais três tamanhos são nomeados short int, long int e long long int. Também são chamados de short, long e long long. Todos possuem variantes com sinal e sem sinal que são acessíveis utilizando os prefixos signed e unsigned, sendo que a ausência de tal prefixo corresponde ao tipo com sinal. Vale salientar que existe um tipo int que corresponde a short int em plataformas computacionais com processador de 16 bits (ou seja 2 bytes) e a long int em plataformas computacionais com um processador de 32 bits (ou seja 4 bytes). É importante salientar que, como em Objective Caml, a aritmética implementada é a aritmética do relógio, ou aritmética módulo. Quando uma computação resulte em um valor ultrapassa os limites do tipo, então que não pode ser representado, o resultado da computação será igual a esse valor módulo o tamanho do tipo do resultado dessa computação. Constantes inteiros Os valores máximos e mínimos de cada um dos tipos possuem nomes. A tabela seguinte faz um resumo dos mesmos. Os tipos básicos 9 SCHAR_MIN -127 valor mínimo para um objeto do tipo signed char SCHAR_MAX +127 valor máximo para um objeto do tipo signed char UCHAR_MAX +255 valor máximo para um objeto do tipo unsigned char SHRT_MIN -32767 valor mínimo para um objeto do tipo signed short SHRT_MAX +32767 valor máximo para um objeto do tipo signed short USHRT_MAX +65535 valor máximo para um objeto do tipo unsigned short INT_MIN -32767 valor mínimo para um objeto do tipo int INT_MAX +32767 valor máximo para um objeto do tipo int UINT_MAX +65535 valor máximo para um objeto do tipo unsigned int LONG_MIN -2147483647 valor mínimo para um objeto do tipo long int LONG_MAX +2147483647 valor máximo para um objeto do tipo long int ULONG_MAX +4294967295 valor máximo para um objeto do tipo unsigned long int LLONG_MIN -9223372036854775807 valor mínimo para um objeto do tipo long long int LLONG_MAX +9223372036854775807 valor máximo para um objeto do tipo long long int ULONG_MAX +18446744073709551615 valor máximo para um objeto do tipo unsigned long long int Em C, acesso a esses nomes é realizado colocando a seguinte diretiva no cabeçalho do arquivo: #include <limits.h> Em C++, o acesso é realizado com a diretiva seguinte de inclusão: #include <climit> Existem outras definições pertinentes agrupadas no arquivo stdint.h, mas que não detalharemos aqui. Operadores inteiros Os valores de todos os tipos inteiros podem ser combinados através de operadores aritméticos como a adição, subtração, etc. São eles: • adição: +, • subtração: -, • multiplicação: *, • divisão inteira: /, • resto da divisão inteira: %, • negação: -. No caso dos operadores de divisão, se o segundo operando for nulo, o resultado é indefinido. Esse conceito de resultado indefinido não existe em Objective Caml. Isso quer dizer que qualquer coisa pode acontecer... Para se ter uma idéia da gravidade disso, imagine então que o disco rígido seja reformatado cada vez que é realizada uma divisão por zero.Soa absurdo para você? Bem, esse comportamento é legal tanto em C quanto em C++! Em conclusão, é a tarefa do programador evitar que os operadores / e % sejam aplicados com o seu segundo argumento igual a zero. Diferente de Objective Caml, o sistema de exceções da linguagem não vai tratar isso. Também existem operadores de comparação que podem ser aplicados aos tipos inteiros: • igualdade: ==; • diferença: !=; • menor que: <; Os tipos básicos 10 • menor ou igual a: <=; • maior que: >; • maior ou igual a: >=. Finalmente, existem operadores que não possuem equivalentes em Objective Caml e que permitem manipular valores em nível de bits. São eles: • deslocamento a esquerda: <<; • deslocamento a direita: >>; • conjunção bit a bit: &; • disjunção bit a bit: |; • disjunção exclusiva bit a bit: ^; • negação bit a bit: ~. Para entender o funcionamento desses operadores, devemos colocar-mo-nós ao nível dos bits. Considere uma variável c do tipo unsigned char. c é representada por um byte, ou seja oito bits. Supondo que o valor de c seja , os oito bits serão então . O sentido de cada um dos operador será apenas explicado através de exemplos, agrupados na seguinte tabela: c >> 2 23 >> 2 00010111 >> 2 00000101 5 c << 1 23 >> 1 00010111 << 1 00101110 46 c & 24 23 & 24 00010111 & 00011000 00010000 16 c ^ 24 24 ^ 24 00010111 >> 0001100 00001011 11 ~c ~23 ~00010111 11101000 232 Exercícios • Utilizando operadores de combinação bit a bit, escreva uma função que dada dois inteiros e retorna . Impressão de valores inteiros A impressão de um valor inteiro utilizando a função fprintf é realizada através de uma diretiva de formatação que pode ter como componentes uma diretiva de tamanho e uma diretiva de sinal. A diretiva de tamanho é hh para char, h para short, omitida para int, l para long e ll para long long. A diretiva de sinal é i para tipos com sinal e u para tipos sem sinal. Assim o programa seguinte permite imprimir valores constantes inteiros disponibilizados no arquivo climits: #include <climits> #include <cstdio> int main () { fprintf(stdout, "SCHAR_MIN = %hhi\n", SCHAR_MIN); fprintf(stdout, "SCHAR_MAX = %hhi\n", SCHAR_MAX); fprintf(stdout, "UCHAR_MAX = %hhu\n", UCHAR_MAX); fprintf(stdout, "SHRT_MIN = %hi\n", SHRT_MIN); fprintf(stdout, "SHRT_MAX = %hi\n", SHRT_MAX); fprintf(stdout, "USHRT_MAX = %hu\n", USHRT_MAX); fprintf(stdout, "INT_MIN = %i\n", INT_MIN); fprintf(stdout, "INT_MAX = %i\n", INT_MAX); fprintf(stdout, "UINT_MAX = %u\n", UINT_MAX); Os tipos básicos 11 fprintf(stdout, "LONG_MIN = %li\n", LONG_MIN); fprintf(stdout, "LONG_MAX = %li\n", LONG_MAX); fprintf(stdout, "ULONG_MAX = %lu\n", ULONG_MAX); fprintf(stdout, "LLONG_MIN = %lli\n", LLONG_MIN); fprintf(stdout, "LLONG_MAX = %lli\n", LLONG_MAX); fprintf(stdout, "ULLONG_MAX = %llu\n", ULLONG_MAX); } A execução desse programa resulte na impressão seguinte na saída padrão: SCHAR_MIN = -128 SCHAR_MAX = 127 UCHAR_MAX = 255 SHRT_MIN = -32768 SHRT_MAX = 32767 USHRT_MAX = 65535 INT_MIN = -2147483648 INT_MAX = 2147483647 UINT_MAX = 4294967295 LONG_MIN = -2147483648 LONG_MAX = 2147483647 ULONG_MAX = 4294967295 LLONG_MIN = -9223372036854775808 LLONG_MAX = 9223372036854775807 ULLONG_MAX = 18446744073709551615 Graças à sobrecarga do operador de impressão, C++ não precisa das diretivas de formatação: ele adapta-se automaticamente em função do tipo do valor a ser impresso. Assim, a mesma saída pode ser obtida com o seguinte programa: #include <climits> #include <iostream> using namespace std; int main () { cout << "SCHAR_MIN = " << SCHAR_MIN << endl << "SCHAR_MAX = " << SCHAR_MAX << endl << "UCHAR_MAX = " << UCHAR_MAX << endl << "SHRT_MIN = " << SHRT_MIN << endl << "SHRT_MAX = " << SHRT_MAX << endl << "USHRT_MAX = " << USHRT_MAX << endl << "INT_MIN = " << INT_MIN << endl << "INT_MAX = " << INT_MAX << endl << "UINT_MAX = " << UINT_MAX << endl << "LONG_MIN = " << LONG_MIN << endl << "LONG_MAX = " << LONG_MAX << endl << "ULONG_MAX = " << ULONG_MAX << endl << "LLONG_MIN = " << LLONG_MIN << endl << "LLONG_MAX = " << LLONG_MAX << endl Os tipos básicos 12 << "ULLONG_MAX = " << ULLONG_MAX << endl; } Números decimais As linguagens C e C++ possuem três tipos reais, chamados tipos flutuantes reais, para representar números decimais. São eles: float, double e long double, em ordem crescente de tamanho e precisão. Constantes decimais As constantes dos tipos flutuantes podem ser escritos em base 10 e 16 (utilizando o prefixo 0x). Uma constante flutuante possui até três partes: • O significante é uma seqüência de dígitos, com possívelmente um ponto para separar a parte inteira da parte decimal. • O expoente é iniciado com o caractere e ou Eseguido de uma seqüência de dígitos para os números escritos em base 10. Para os números flutuantes escritos em base 16, o expoente inicia com o caractere p ou P e o expoente é aplicado ao número 2 ao invés de 10. Se o significante contem um ponto, então o expoente é opcional. • O sufixo flutuante que indica qual é o tipo da constante. Quando é ausente, o tipo é double. Pode ser f ou F para indicar que é do tipo float e pode ser l ou L para indicar que é do tipo long double. Não podemos esquecer de assinalar que tanto o significante como o expoente podem iniciar opcionalmente com um sinal positivo (caractere +) ou negativo (caractere -). Seguem alguns exemplos de como escrever o número : 3.14 /* tipo: double */ .314e1/* tipo: double */ 314e-2 /* tipo: double */ 314e-2f /* tipo: float */ 314e-2L /* tipo: long double */ Operadores decimais Os valores de todos os tipos flutuantes podem ser combinados através de operadores aritméticos como a adição, subtração, etc. São eles: •• adição: +, •• subtração: -, •• multiplicação: *, •• divisão: /, •• negação: -. No caso dos operadores de divisão, se o segundo operando for nulo, o resultado é indefinido. Repare que, diferente de Objective Caml, que provê operadores diferentes para os tipos int e float, tanto em C e C++, os operadores aritméticos são os mesmos tanto para os tipos inteiros quanto para os tipos flutuantes. A biblioteca padrão de C fornece funções que implementam os operadores matemáticos clássicos. Para utilizar essas funções, deve-se fazer a seguinte diretiva: #include <cmath> Ou, se for programa em C: #include <math.h> Os tipos básicos 13 Segue uma tabela com apenas parte das funcionalidades disponíveis. A cada operador matemático, são associadas três funções, cada um correspondendo a um dos três tipos flutuantes: funcionalidade Tipo double Tipo float Tipo long double seno: double sin(double x); float sinf(float x); long double sinl(long double x); cosseno: double cos(double x); float cosf(float x); long double cosl(long double x); tangente: double tan(double x); float tanf(float x); long double tanl(long double x); arccosseno: double acos(double x); float acosf(float x); long double acosl(long double x); arcseno: double asin(double x); float asinf(float x); long double asinl(long double x); arctangente: double atan(double x); float atanf(float x); long double atanl(long double x); expoente em base : double exp(double x); float expf(float x); long double expl(long double x); expoente em base 2: double exp2(double x); float exp2f(float x); long double exp2l(long double x); logaritmo em base (logaritmo natural): double log(double x); float logf(float x); long double logl(long double x); logaritmo em base : double log2(double x); float log2f(float x); long double log2l(long double x); logaritmo em base : double log10(double x); float log10f(float x); long double log10l(long double x);raiz quadrada. double sqrt(double x); float sqrtf(float x); long double sqrtl(long double x); raiz cúbica: double cbrt(double x); float cbrtf(float x); long double cbrtl(long double x); valor absoluto: double fabs(double x); float fabsf(float x); long double fabsl(long double x); potência: double pow(double x, double y); float powf(float x, float y); long double powl(long double x, long double y); arredondamento para cima: double ceil(double x); float ceilf(float x); long double ceill(long double x); arredondamento para baixo: double floor(double x); float floorf(float x); long double floorl(long double x); arredondamento para o inteiro mais próximo: double round(double x); float roundf(float x); long double roundl(long double x); arredondamento para o mais próximo int: int rint(double x); int rintf(float x); int rintl(long double x); arredondamento para o mais próximo long int: long int lrint(double x); long int lrintf(float x); long int lrintl(long double x); arredondamento para o mais próximo long long int: long long int llrint(double x); long long int llrintf(float x); long long int llrintl(long double x); truncamento: double trunc(double x); float truncf(float x); long double truncl(long double x); Os tipos básicos 14 Apresentamos de forma muito superficial os tipos flutuantes. A biblioteca padrão oferece outras funcionalidades, em particular permite um controle muito afinado de problemas de precisão e de estouro. As funções apresentadas devem porém devem satisfazer 99,9% das necessidades de programação. Variáveis e funções Variáveis A definição de uma variável faz-se com a seguinte sintaxe: tipo nome = valor; onde tipo é uma expressão que define o tipo da variável, nome é o nome da variável, e valor é uma expressão que define o valor inicial da variável. Diferente de Objective Caml, não é obrigatório associar um valor inicial a uma variável e a seguinte sintaxe também é legal: tipo nome; Seguem então alguns exemplos: char c = 'a'; int i = 0; long long num; Na primeira linha, é definida uma variável, cujo nome é c, o tipo é char e que tem como valor inicial 'a'. Na segunda linha, é definida uma variável nomeada i, de tipo inteiro, cujo valor inicial é 0. Enfim, na terceira linha, é definida uma variável de nome num, de tipo long long, e cujo valor é qualquer. Pode se definir múltiplas variáveis do mesmo tipo, separando seus nomes por vírgulas. Assim, na linha seguinte, são definidas três variáveis inteiras, chamadas respectivamente i, j, e k, onde apenas j possui um valor inicial, que é 2. int i, j = 2, k; Funções A definição de uma função faz-se com a seguinte sintaxe: tipo nome (parametros) corpo onde tipo é o tipo de retorno da função, nome é o nome da função, parametros é a lista de parâmetros da função e corpo é um bloco de instruções. argumentos é uma lista de declarações de parâmetros separadas por vírgulas. Essa lista pode ser vazia, quando a função não possui nenhum parâmetro. Cada parâmetro é declarado com seu tipo e o seu nome. corpo é um bloco, entre símbolos de chaves de instruções C/C++. Veremos mais adiante quais construções podemos utilizar, por enquanto basta saber que um bloco pode conter declarações de variáveis e instruções algorítmicas similares aquelas da linguagem Objective Caml (atribuição de variável, chamada de função, construções condicionais e repetitivas). Uma diferença significativa é que um bloco C ou C++ não pode conter a definição de uma função, portanto não existe o conceito de uma função local a outra função. Segue agora um exemplo completo de uma função: Variáveis e funções 15 int square (int n) { return n * n; } Parâmetros opcionais A linguagem C++ oferece algumas facilidades relacionadas aos parâmetros que não existem na linguagem C. Assim, é possível dar um valor defaut a alguns parâmetros de uma função. Vejamos isso com base um exemplo: int f (int a, int b = 0) { return a + b; } int g (int k) { return f (k); } A função f possui dois parâmetros, porém o segundo (nomeado b) é opcional. Quando a função f é chamada dentro da função g, o parâmetro opcional recebe o valor defaut especificado na definição da função, no caso desse exemplo. O mecanismo de associação entre parâmetros efetivos e parâmetros formais é realizado da esquerda para a direita. Quando o número de parâmetros efetivos é menor que o número de parâmetros formais, esses últimos recebem o valor defaut especificado na definição da função. Se não houver valor defaut especificado, então ocorrerá um erro na compilação. Considere agora o seguinte programa: #include <iostream> using namespace std; int f2 (int a = 1, int b = 2, int c = 3) { return a * b * c; } int main g() { cout << f2() << endl; cout << f2(5) << endl; cout << f2(5, 10) << endl; cout << f2(5, 10, 15) << endl; return 0; } A saída será: 6 30 150 750 Variáveis e funções 16 Na primeira chamada, não há parâmetros efetivos, logo os parâmetros formais receberão seu valor defaut e o resultado é . Na segunda chamada, há um parâmetro efetivo, que será associado ao primeiro parâmetro formal, e o resultado é . Na terceira chamada, há dois parâmetros efetivos, que serão associados aos dois primeiros parâmetros formais, e o resultado é . Na quarta e última chamada, há trˆ´s parâmetros efetivos, que são associados aos parâmetros formais, e o resultado é . Vale salientar que uma função pode ter um número qualquer de parâmetros opcionais, mas eles devem aparecer nas últimas posições da lista de parâmetros. Assim a seguinte definição de função é ilegal: int uma_funcao_mal_definida (int a = 0, int b) { return a + b; } Sobrecarga Em c++ é possivel fazer com que duas ou mais funções diferentes tenham o mesmo nome desde que tenham parâmetros diferentes e isso é chamado de sobrecarga de função por exemplo: # include <iostream> # include <string> using namespace std; int soma (int a,int b) { return a + b; } float soma (float a,float b) { return a + b; } string soma (char a,char b) { return a + b; } Nenhuma dessas três funções soma seria sobreescrevida ou ocorreria um erro pois todas possuem paramentros diferentes e para chama-las é necessário apenas colocar os parâmetros correspondentes por exemplo: soma (a,b); Isso chamaria a terceira função de soma que tem como parâmetros dois caracteres. Instruções 17 Instruções Instruções de expressão As instruções de expressão compõem a forma mais simples de construir uma instrução, pois permite instruir o compilador que uma dada expressão deve ser avaliada nesse ponto. A sintaxe da instrução de expressão é: expressao; Ou seja, é apenas uma expressão seguida de um ponto e vírgula. A seguir vamos então ver quais são as expressões que são mais frequentemente encontradas em instrução expressão. Além das expressões sobre os tipos básicos já apresentadas, existem uma série de expressões diferentes providas que permitem compor os blocos básicos de algoritmos seqüênciais. São elas: •• Atribuição; •• Chamada de função; •• Pós-incremento; •• Pré-incremento; •• Atribuição composta; •• Composição sequencial. Atribuição A sintaxe da atribuição em C e C++ é a seguinte: alvo = expressao A semântica dessa construção é a seguinte: a expressão fonte expressao é avaliada, e o valor resultante dessa avaliação é memorizado por alvo. Qual a natureza do alvo? Os conceitores da linguagem chamam alvo de lvalue. Esse conceito é similar ao de uma referência em Objective Caml: trata-se de uma entidade que pode guardar um valor. Por enquanto, a única classe de entidades que vimos que podem ser lvalues são as variáveis. Segue um pequenoexemplo: int i, j; i = 0; j = i + 2; Nesse trecho de código, a primeira linha declara i e j como sendo variáveis do tipo inteiro, cujo valor inicial é indefinido. Na segunda linha, é realizada uma atribuição. O alvo é a variável i e a expressão é o valor inteiro 0. Após a execução dessa instrução, i passa a guardar o valor 0. Na terceira linha, há uma segunda atribuição. O alvo é a variável j e a expressão fonte é i + 2, cujo valor é . Após essa instrução, o valor memorizado por j passa a ser 2 e o valor guardado por i continua sendo 0. O operador de atribuição tem um valor de retorno, que é o próprio valor atribuído. Ele também tem a propriedade de ser associativo a direita. Essas duas propriedades fazem com que seja possível realizar diversas atribuições encadeadas em uma única instrução: int alt, larg; alt = larg = 5; alt e larg são ambas variáveis do tipo inteiro com valor inicial indefinido. A segunda linha é um encadeamento de atribuições. Como o operador de atribuição é associativo a direita, a expressão deve ser lida assim: alt = (larg = 5). A atribuição à variável alt é realizada, fazendo a avaliação da expressão larg = 5 que é uma Instruções 18 outra atribuição. Essa segunda atribuição é então avaliada fazendo a avaliação da expressão fonte 5 cujo resultado (o número ) é atribuído à variável larg e é retornado como valor da expressão larg = 5. Esse valor é atribuído à variável alt e é o resultado da expressão alt = larg = 5. Ao final então da avaliação dessa expressão, ambas variáveis alt e larg guardam o valor . Chamada de função A chamada de função é realizada colocando o nome da função seguida de uma lista de parâmetros entre parênteses. Se a lista for vazia, deve-se colocar mesmo assim os parênteses - caso contrário, a expressão representaria a função sem realizar a chamada à mesma. Alguns exemplos de chamada de funções definidas na biblioteca padrão de C/C++: printf("oi\n"); /* executa a função printf com o argumento "oi\n" */ printf("%i", 42); /* executa a função printf com os dois argumentos "%i" e 42 */ getchar(); /* executa a função getchar sem nenhum argumento */ exit (0); /* executa a função exit com o argumento 0 */ Operadores de incremento, decremento e atribuição C/C++ possuem uma série de operadores de atribuição que tem como objetivo tornar o código mais enxuto e, em determinados casos, permitir ou facilitar o trabalho do compilador para tirar melhor proveito das facilidades fornecidas pelo micro-processador subjacente. Pós-incremento Iniciaremos vendo o talvez mais difundido desses operadores: pós-incremento. Veja um exemplo: int i = 0; i++; Temos então uma variável inteira i que tem como valor inicial . Na segunda linha, temos um operador de pós-incremento. Esse operador faz o seguinte: lê o valor inicial de i, adiciona a esse valor, guarda o resultado dessa soma em i e retorna o valor inicial de i. Portanto é basicamente equivalente à função seguinte: int post_increment (int & i) { int result = i; i = i + 1; return result; } Enfim, para concluir sobre o operador de pós-incremento, deve-se salientar que apenas pode ser aplicado a um lvalue: não faria sentido nenhum escrever 2++... Exercício •• Considere o seguinte trecho de código: int i, j, k; i = j = k = 0; i = j++ = k++; Qual o valor de cada variável ao término da execução desse trecho de código? •• Considere o seguinte trecho de código: Instruções 19 int i = 0; f(i++); Qual o valor do parâmetro efetivo na chamada da função f? Pré-incremento O operador de pré-incremento é similar ao de pós-incremento e se escreve da mesma maneira! A única forma de diferencia-lo é na sua posição com relação ao seu argumento. Veja um exemplo: int i = 0; ++i; O operador de pré-incremento portanto é localizado antes do seu argumento, que deve ser um lvalue. Quando aplicado, o argumento é avaliado, o resultado dessa avaliação é somado com o número e o resultado dessa soma é atribuído ao argumento. Esse resultado também é o resultado da expressão. O operador de pré-incremento portanto é equivalente à seguinte função: int post_increment (int & i) { i = i + 1; return i; } Exercício •• Considere o seguinte trecho de código: int i, j, k; i = j = k = 0; i = ++j = ++k; Qual o valor de cada variável ao término da execução desse trecho de código? •• Considere o seguinte trecho de código: int i = 0; f(++i); Qual o valor do parâmetro efetivo na chamada da função f? •• Na sua opinião, qual dos dois operadores de incremento deve ser executado mais rapidamente? Operadores de decremento Existem dois operadores para decrementar um lvalue. Não surpreendemente são chamados pós-decremento e pré-decremento, tem como sintaxe -- e se distinguem pela posição relativa ao seu operando. Temos um exemplo de ambos operadores no trecho de código seguinte: int i = 5, j = 5; --i; j--; A segunda e terceira linhas contêm, respectivamente, o operador de pré-decremento aplicado à variável i e o operador de pós-decremento aplicado à variável j. O valor do primeio decremento é e o do segundo é . Instruções 20 Exercícios •• Considere o seguinte trecho de código: int i, j, k; i = j = k = 0; i = j-- = --k; Qual o valor das variáveis i, j e k ao término da execução desse trecho de código? •• Escreva funções que tem papel idêntico aos dois operadores de decremento. Operadores de atribuição composta Além do operador de atribuição simples, existem uma série de outros operadores de atribuição que correspondem ao efeito conjugado de uma operação aritmética ou binária e de uma atribuição. A seguinte tabela resume esses operadores += soma atribuição i += j equivale a i = i + j *= produto atribuição i *= j equivale a i = i * j -= subtração atribuição i -= j equivale a i = i - j /= divisão atribuição i /= j equivale a i = i / j %= resto atribuição i %= j equivale a i = i % j <<= deslocamento esquerda atribuição i <<= j equivale a i = i << j >>= deslocamento direita atribuição i >>= j equivale a i = i >> j &= conjunção binária atribuição i &= j equivale a i = i & j ^= disjunção exclusiva binária atribuição i ^= j equivale a i = i ^ j Exercício Considere o seguinte trecho de código: int i = 0; i += 5; i -= -5; i *= 3; i /= 2; i %= 2; i <<= 1; i >>= 2; i &= 15; i |= (1 << 5); Qual o valor de i após cada atribuição? Instruções 21 Composição seqüêncial O operador , (vírgula) pode ser utilizado para combinar seqüencialmente expressões. É um operador binário infixo. A sintaxe portanto é: exp1 , exp2 A expressão exp1 é avaliada primeiro, e a expressão exp2 é então avaliada. O resultado da segunda sub-expressão é o resultado da expressão completa. Assim, a seguinte expressão a = 2, a += 1 tem como valor 3. É um operador binário associativo a esquerda e pode ser repetido para encadear sequencialmente mais de duas expressões. O valor da seqüência é o valor da última expressão da seqüência. No exemplo seguinte (puramente ilustrativo...): a = (2, 3, 5, 7) o operador é encadeado para formar uma seqüência de quatro expressões, sendo o valor da última ( ) o valor da combinação. Nesse exemplo, a variável a é atribuída então esse valor . Blocos e seqüênciamento de instruções Como já foi ilustrado em números exemplos, as instruções podem ser combinadas em seqüência: a ordem nas quais as instruções aparecem corresponde à ordem na qual serão executadas. É conveniente ter uma construção para agrupar uma seqüência de instrução em um único bloco. Em Objective Caml, isso é feito com os delimitadores begin e end. Em C e C++, os blocos são delimitados por chaves: • { para iniciar, e • } para concluir. Em C, declarações de variáveis só podem ocorrer antes da primeira instrução do bloco. Já em C++, as declarações de variáveispodem ocorrer em qualquer posição. Um exemplo de bloco C ou C++ é: { int n, sq; scanf("%i", &n); sq = n * n; printf("%i * %i = %i\n", n, n, sq); } O bloco seguinte só é legal em C++ (não o é em C): { int n; scanf("%i", &n); int sq = n * n; printf("%i * %i = %i\n", n, n, sq); } Um bloco pode ocorrer em qualquer posição onde uma instrução pode ocorrer, assim os blocos podem ser aninhados como no exemplo seguinte: { int n; Instruções 22 scanf("%i", &n); { int sq = n * n; printf("%i * %i = %i\n", n, n, sq); } } É importante salientar que qualquer variável declarada em um bloco é local a esse bloco. O escopo da mesma inicia logo após a sua declaração e termina no final do bloco. Desta forma, há um erro no bloco seguinte, pois a variável sq é local ao bloco mais interno e não é conhecida no bloco mais externa. { int n; scanf("%i", &n); { int sq = n * n; } printf("%i * %i = %i\n", n, n, sq); /* atenção: erro nessa linha !!! */ } Retorno de função Como as instruções de C e C++ não retornam valor, precisamos de um mecanismo para poder indicar qual valor uma função deve retornar. É o papel da instrução de retorno. Ela tem a seguinte sintaxe: return expressao; onde expressao é uma expressão. A semântica é que essa expressão é avaliada e o valor resultante é o valor de retorno da função. A execução da função que contem a instrução é interrompida e o valor obtido através da avaliação da expressão é retornado. Um exemplo Segue um exemplo de uso da instrução de retorno em uma função que calcula o quadrado de um dado número decimal: float quadrado (float x) { return x * x; } Observação sobre a função main Lembre-se que a função main é, por convenção, o ponto inicial de execução de um programa escrito em C++ ou em C. O tipo de retorno da função main é o tipo int. O valor de retorno pode ser utilizado para fornecer informações sobre o desenrolar da execução do nosso programa ao sistema operacional ou a demais programas. Por convenção nos sistemas Unix e similares, um programa retorna o valor 0 quando a execução transcorreu normalmente. Em C e C++, o comportamento defaut da função main é de retornar o valor 0. Assim, o seguinte programa: int main () { } Instruções 23 é equivalente ao seguinte programa int main () { return 0; } Exercícios Escreva uma função que, dada um número decimal, retorna o cubo desse número. Condição A execução de uma instrução, ou de um bloco de instrução, pode ser condicionada ao valor de uma condição, utilizando a tradicional construção if ... then ... else .... A sintaxe básica é a seguinte: if (condicao) instrucao1 else intrucao2 Note que a condição deve aparecer entre parênteses, e apenas pode haver uma ocorrência da construção sintática "instrução" em cada ramo de execução. Caso seja preciso realizar mais de uma instrução em um ramo, deve-se agrupá-las em um bloco utilizando a construção de blocos vista anteriormente. Um primeiro exemplo de condição que permite atribuir à variável menor o menor valor de duas variáveis n e m é: if (n <= m) menor = n; else menor = m; Esse segundo exemplo demostra como combinar uma instrução condicional com blocos: if (n <= m) { min = n; max = m; } else { min = m; max = n; } Uma outra forma bastante comum de escrever a construção anterior, utilizando um leiaute um pouco diferente é: if (n <= m) { min = n; max = m; } else { min = m; max = n; } Instruções 24 Em Objective Caml, as instruções formam uma classe de expresssões e algumas restrições de tipos aplicam-se às instruções como a instrução condicional, onde as instruções da ambos ramos devem ter o mesmo tipo. Tanto em C quanto em C++, as instruções não possuem tipos, logo não existe essa restrição. É importante salientar que o ramo else é opcional. Assim a construção sintática seguinte é perfeitamente legal: if (condicao) instrucao1 Um exemplo que ilustra essa forma é if (menor > maior) { int tmp = menor; menor = maior; maior = tmp; } Enfim, pode-se encadear um número qualquer de instruções condicionais, utilizando a seguinte forma: if (condicao1) instrucao1 else if (condicao2) intrucao2 else if (condicao3) intrucao3 else instrucaoe Nesse caso, o último ramo também é opcional, como demostrado no seguinte exemplo: if (mes = 1) printf("janeiro"); else if (mes = 2) printf("fevereiro"); else if (mes = 3) printf("marco"); else if (mes = 4) printf("abril"); else if (mes = 5) printf("maio"); else if (mes = 6) printf("junho"); else if (mes = 7) printf("julho"); else if (mes = 8) printf("agosto"); else if (mes = 9) printf("setembro"); else if (mes = 10) printf("outubro"); else if (mes = 11) printf("novembro"); else if (mes = 12) printf("dezembro"); Exemplos Com o operador condicional, e utilizando a recursividade, já podemos a definir algumas funções interessantes. Esse primeiro exemplo calcula o -ésimo elemento da seqüência de Fibonacci. int fib (int n) { if (n == 1 || n == 2) return 1; else Instruções 25 return fib(n-1) + fib(n-2); } Esse segundo exemplo calcula onde representa um número decimal e representa um número natural. int power (float x, int n) { if (n == 0) return 1; else { float x2 = power (x, n/2); float quad = x2 * x2; if (n % 2 == 1) { quad *= x; } return quad; } } Exercícios •• Defina uma função que calcula o maior divisor comum de dois números naturais maiores que um. •• Defina uma função que testa se um número positivo é primo. Seleção As linguagens C e C++ possuem um segundo tipo de instrução condicional, que identificamos como instrução de seleção. Enquanto uma instrução permite orientar o fluxo de execução em função do valor de uma condição, uma instrução de seleção permite orientar o fluxo de execução em função de um valor inteiro. A sintaxe da instrução de seleção é: switch (expressao) instrucao com a restrição que expressao é um expressão de tipo inteiro. Em geral instrucao é um bloco de instruções (entre chaves). A semântica é um pouco complexa a ser explicada, mas, felizmente, é mais fácil de entender!... A semântica é que expressao é avaliada, e em função do valor obtido através dessa avaliação, o fluxo de execução continua a partir de um certo ponto de controle ou após a instrução de seleção. Para efeitos de explicação, vamos assumir que é o valor de expressão. Existe dois tipos de pontos de controle: case ou default. Os pontos casesão seguidos de uma expressão. Se existir pelo menos um ponto case tal que o valor da expressão associada seja também igual a , então a fluxo de execução continua a partir do primeiro desses pontos. Se não existir nenhum ponto case satisfazendo essa condição, e existir um ponto default, então a execução continua a partir do ponto default. Se também não existir ponto default, a execução continua após a instrução de seleção. Instruções 26 Exemplos Para passar a intuição, então vamos lançar mão de alguns exemplos. •• O primeiro exemplo é: int i = 1; switch (i) { case 0: printf("0\n"); case 1: printf("1\n"); } printf("saiu\n"); A expressão de seleção tem valor que também é o valor do segundo ponto de controle. A execução desse trecho de código resultará na seguinte impressão na saída padrão: 1 saiu •• O segundo exemplo é: int i = 2; switch (i) { case 0: printf("0\n"); case 1: printf("1\n"); } printf("saiu\n"); A expressão de seleção tem valor que é o valor de nenhum ponto de controle case. Como não há ponto de controle default, o fluxo de execução prossegue após a instruçãode seleção. A execução desse trecho de código resultará na seguinte impressão na saída padrão: saiu •• O terceiro exemplo é: int i = 2; switch (i) { case 0: printf("0\n"); case 1: printf("1\n"); default: printf("outro\n"); } printf("saiu\n"); A expressão de seleção tem valor que é o valor de nenhum ponto de controle case. Como há um ponto de controle default, o fluxo de execução prossegue após esse ponto de controle. A execução desse trecho de código resultará na seguinte impressão na saída padrão: outro saiu •• O quarto exemplo é: int i = 0; switch (i) { Instruções 27 case 0: printf("0\n"); case 1: printf("1\n"); default: printf("outro\n"); } printf("saiu\n"); Agora a expressão de seleção tem valor . O primeiro ponto de controle tem valor , e a execução prossegue a partir dele. A saída portanto será: 0 1 outro saiu Será que é isso que você estava esperando? Se for, muito bem, você leu atentamente os parágrafos iniciais. Se não for, talvez é porque você ainda tem em mente o comportamento da construção de casamento de padrões de Objective Caml. Nessa construção, apenas é avaliada a expressão que corresponde ao primeiro padrão que casa. Na verdade esse tipo de comportamento é mais comum em algoritmos e processamento de dados que o comportamento exibido por esse último exemplo. Vamos ver na seqüência como obtê-lo em C ou C++. Give me a break A instrução break permite interromper o fluxo de execução em uma instrução de seleção. Quando o fluxo de execução encontra uma instrução break, o fluxo sai da instrução de seleção mais interna contendo essa instrução. Continuando a linha de exemplos da seção, vejamos como empregar a instrução break: int i = 0; switch (i) { case 0: printf("0\n"); break; case 1: printf("1\n"); break; default: printf("outro\n"); } printf("saiu\n"); O valor da expressão de seleção é que também é o valor do primeiro ponto de controle case. A instrução de impressão é executada e uma instrução break é encontrada, resultando no desvio do fluxo de execução para a primeira instrução após a instrução de seleção. A execução desse trecho de código resulta então na seguinte impressão na saída padrão 0 saiu Instruções 28 Exemplo Para concluir sobre a instrução de seleção, mostramos um exemplo completo. Esse exemplo ilustra os diferentes pontos já visto e ainda a possibilidade de enumerar diversos pontos de controle. void imprime_numero_em_portugues (int n) { switch (n) { case 0: printf("nenhum"); break; case 1: printf("um"); break; case 2: case 3: printf("poucos"); break; case 4: case 5: case 6: printf("alguns"); break; default: printf("muitos"); } } Repetição As linguagens C e C++ possuem três construções de repetição diferentes: instrução while, instrução do e instrução for. Instrução while A sintaxe da instrução while é a seguinte: while (condicao) instrucao A semântica é similar à da linguagem Objective Caml: • A expressão condicao é avaliada • Se o resultado dessa avaliação é (ou ), então termina a execução da instrução while, • Se o resultado dessa avaliação é (ou diferente de ), então a instrução instrucao é avaliada e a instrução while é executada novamente (volta-se ao primeiro ponto). Nessa construção, o corpo da repetição é executado zero ou mais vezes. Segue aqui um exemplo onde a instrução é empregada para calcular o n-ésimo valor da seqüência de Fibonacci. int fibonacci (int n) { if (n <= 2) return 1; int corrente, anterior, i; corrente = anterior = 1; i = 2; Instruções 29 while (i != n) { i += 1; int tmp = anterior; anterior = corrente; corrente += tmp; } return corrente; } Exercício Utilizando a instrução while, escreva funções para: •• calcular o fatorial de um número inteiro positivo; •• elevar um número decimal à uma potência inteira; •• calcular o maior divisor comum de dois números inteiros positivos. Instrução do A instrução do tem a seguinte sintaxe: do instrucao while (condicao); A semântica é similar à construção while, a diferença ficando no momento que a condição de repetição é avaliada: após a execução do corpo da instrução. Assim, em uma instrução do, o corpo é executado pelo menos uma vez. Exemplo A função seguinte emprega uma instrução do e calcula o número mínimo de bits necessários para representar um número inteiro. int logaritmo2 (int n) { int result = 0; do { n /= 2; result += 1; } while (n); return result; } Instruções 30 Instrução for A instrução for é a mais complexa das construções de repetição das linguagens C e C++. A sintaxe dela é a seguinte: for (expressao_init; condicao_c; expressao_inc) instrucao onde: • expressao_init é uma expressão ou declaração de inicialização; • condicao_c é uma condição de repetição • expressao_inc é uma expressão de incremento • instrucao é a instrução que constitui o corpo da repetição. A semântica é a seguinte. Primeiro expressao_init é avaliada. Em seguida, condicao_c é avaliada. Se o resultado da avaliação for ou , então a execução prossegue após a instrução for. Caso contrário, o corpo instrucao é executado. Após isso, a expressão de incremento expressao_inc é avaliada. Em uma instância típica de uma instrução for, a expressão expressao_init é utilizada para declarar e inicializar variáveis, e expressao_inc para incrementar essas variáveis. Por exemplo, o código seguinte resulte na impressão dos 10 primeiros números naturais na saída padrão: for (int n = 0; n < 10; ++n) printf("%i\n", n); Diferente de Objective Caml, onde a construção de repetição limitada só permite incrementar ou decrementar a variável de laço, em C e C++, qualquer operação pode ser utilizada para construir expressao_inc. Assim, a função seguinte imprime todos os múltiplos de que são menores ou iguais a topo: int imprime_multiplos (int m, int topo) { for (multiplo = m; multiplo <= topo; multiplo += m) printf("%d\n", multiplo); } Evidentemente, pode-se utilizar uma instrução bloco para combinar mais de uma instrução no corpo de uma iteração for: int imprime_multiplos_em_uma_linha_só (int m, int topo) { for (int multiplo = m; multiplo <= topo; multiplo += m) { printf("%d", multiplo); if (multiplo + m <= topo) { printf(" "); } else { printf("\n"); } } Instruções 31 Modificação do fluxo de execução em repetições Existem duas formas de modificar o fluxo de execução no corpo de uma repetição: através de uma interrupção, ou passando para a próxima iteração. Interrupção A instrução de interrupção tem a seguinte sintaxe: break; Observe que é a mesma palavra-chave que utilizamos em instruções de seleção (switch). Blocos de instrução em repetições e seleção são as duas únicas possibilidades para ter uma instrução de interrupção break. A semântica é que, quando o fluxo de execução encontra uma instrução de interrupção, ele pula diretamente para depois da instrução de repetição mais aninhada que o contem. Próxima repetição A instrução de próxima repetição ou continuação tem a seguinte sintaxe: continue; Essa instrução só pode ocorrer dentro do bloco de instruções de uma instrução de repetição. A semântica é que o fluxo de execução pula diretamente para o fim do corpo da repetição. Seguem três trechos esquemáticos desse comportamento while (/* condição */) { /* algumas instruções */ continue; /* outras instruções */ /* alvo do pulo do continue é aqui*/ } do { /* algumas instruções */ continue; /* outras instruções */ /* alvo do pulodo continue é aqui*/ } while (/* condição */); for (/* inicialização */; /* teste */; /* incremento */) { /* algumas instruções */ continue; /* outras instruções */ /* alvo do pulo do continue é aqui*/ } Instruções 32 Exercícios Defina funções que desenham as seguintes formas geométricas na saída padrão: • Um quadrado, de lado Exemplo de saída para n = 5: ***** * * * * * * ***** • Um retângulo, de largura e altura Exemplo de saída para n = 5 e m = 4 ***** * * * * ***** • Um quadrado rotacionado de 45 graus, cuja diagonal é . Exemplo de saída para n = 2 * * * * * * * * Construção de tipos 33 Construção de tipos Conceitos iniciais As linguagens C e C++ possuem muitos mecanismos para construir e estruturar dados complexos. Diferentemente dos tipos básicos, onde há uma forte correspondência entre essas linguagens e a linguagem Objective Caml, a construção de tipos complexos em C e C++ é bem diferente daquela provida por Objective Caml. Esse capítulo não aborda a construção de tipos via classes de objetos, que é um paradigma presente nas linguagens Objective Camle e C++, mas que é ausente da linguagem C. O sistema de tipos de C e C++ é bastante complexo e, em prol da lisibilidade desse texto, algumas aproximações e simplificações foram realizadas. Apresentaremos as seguintes construções para a criação e manipulação de tipos complexos: •• Enumerações correspondem a uma forma muito básica de tipos variantes de Objective Caml. •• Arranjos correspondem aos arranjos de Objective Caml, embora sejam objetos muito mais simples. A linguagem C++ provê, através da sua biblioteca, uma outra implementação de arranjos que tem um nível de abstração mais próximo ao dos arranjos de Objective Caml. •• Registros correspondem aos tipos registros de Objective Caml. •• Ponteiros correspondem às referências de Objective Caml. •• As referências, presentes apenas na linguagem C++, não tem correspondência direta em Objective Caml, embora possam ser simuladas por ponteiros, logo pelas referências de Objective Caml. •• As uniões correspondem aproximadamente aos tipos variantes de Objective Caml, embora não sejam tão práticas para serem manipuladas. •• Enfim, terminaremos com a apresentação dos tipos funcionais, que existem em C e em C++. Embora não sejam tão flexíveis quanto os tipos funcionais de Objective Caml, permitem ainda programação de ordem superior. Vale destacar que em C e em C++, pode-se dar um nome a um tipo, utilizando a seguinte construção, ou alguma variação: typedef expressaodetipo nome; Por exemplo, podemos definir um tipo chamado inteiro que é idêntico ao tipo básico int com a seguinte construção: typedef int inteiro; // exemplo de definição de um tipo chamado "inteiro", que nada mais é que o tipo int Então a mais simples expressão de tipo que exista é um nome de um tipo básico, ou de outro tipo que já foi definido dessa forma. Uma vez definido um nome para um tipo, ele pode ser usado para declarar variáveis ou funções da mesma forma que os tipos básicos. Por exemplo: inteiro soma3 (inteiro a, inteiro b, inteiro c) // exemplo (artificial) onde usamos um nome de tipo definido por nós { inteiro resultado = a; resultado += b; resultado += c; return resultado; } Também pode ser utilizar uma expressão de tipo diretamente na declaração de uma variável. Assim esse código: struct Spoint { // nesse exemplo, usamos uma expressão de tipo registro, detalhes sobre essa construção seguem mais adiante double x; Construção de tipos 34 double y; } point; // point é uma variável cujo tipo é um tipo registro struct Spoint é equivalente a typedef struct Spoint { double x; double y; } Tpoint; // definimos Tpoint como o nome de um tipo registro struct Spoint Tpoint point; // point é uma variável cujo tipo é Tpoint, ou seja o tipo registro struct Spoint Enumerações Com relação ao sistema de tipos de Objective Caml, os tipos enumerados lembram de forma superficial os tipos variantes. Os tipos enumerados são porém muito mais limitados, e correspondem mais precisamente a tipos variantes onde as alternativas devem ser todas constantes. Um tipo enumerado contem um número finito de valores. Cada valor tem um nome que o identifica. Por exemplo a seguinte definição: enum Ecor {vermelho, azul, amarelo}; introduz um tipo enumerado, que possui como rótulo Ecor, e três enumeradores, que são valores inteiros constantes com identificadores vermelho, azul, amarelo. Pode-se dar um nome a esse tipo, utilizando uma definição de tipo: typdef enum Ecor {vermelho, azul, amarelo} Tcor; Em seguida, podemos declarar e utilizar variáveis com esse tipo, e os valores que foram introduzidos na operação podem também aparecer no programa, onde serão consideradas como valores inteiros. Segue um trecho de programa que utiliza a definição acima: void imprime_cor_em_ingles (Tcor c) // o parâmetro poderia também ter sido declarado como enum Ecor c { switch (c1) { case vermelho: printf("red"); break; case azul: printf("blue"); break; case vermelho: printf("amarelo"); break; } } Afirmamos que um valor inteiro é associado a cada valor de uma enumeração. Podemos realizar essa associação de forma implícita ou explícita. A forma explícita é realizada associando o número inteiro na definição da enumeração, como demostrado no seguinte exemplo: enum Edirecao { norte = 0; oeste = 90; sul = 180; leste = 270 }; Construção de tipos 35 Caso não aparece associação explícita, a associação é realizada de maneira implícita, utilizando as seguintes regras: o primeiro valor da enumeração recebe o valor zero, e um valor que não está na primeira posição é associado com a soma de um com o inteiro associado ao valor anterior. Finalmente, podemos também misturar as associações explícitas e implícitas, como no seguinte exemplo: enum Ecodigo { NO_ERROR = 0; IO_ERROR = 10; FORMAT_ERROR; TIMEOUT_ERROR }; Nesse exemplo, FORMAT_ERROR e TIMEOUT_ERROR denotam respectivamente os inteiros e . Exercício Considere o seguinte tipo de dados definido em Objective Caml type card_suit_t = Spades | Hearts | Diamonds | Clubs Define um tipo equivalente em C/C++. Arranjos A linguagem C permite declarar arranjos uni-dimensionais, dando as seguintes informações: o tipo dos elementos, e o tamanho do arranjo. Por exemplo, o seguinte código define uma variável notas para guardar quatro valores do tipo float, utilizaremos o seguinte código: float notas [4]; Portanto a sintaxe consiste em identificar o tipo dos elementos, através de uma expressão de tipos, o nome do arranjo, e entre colchetes o tamanho do arranjo. Opcionalmente, podemos associar um valor inicial aos elementos do arranjo assim definido. Há duas possibilidades • Para definir o mesmo valor em todas as posições do arranjo, colocamos apenas o valor do primeiro elemento. Por exemplo, o código seguinte corresponde à definição de um arranjo similar ao exemplo anterior, mas onde todas as posições do arranjo armazenam o valor : float notas [4] = { 0.0f }; Portanto a sintaxe exige que a expressão de inicialização seja colocada entre chaves. •• Também pode-se definir individualmente cada posição do arranjo. Isso é realizado colocando entre chaves, a enumeração dos valores do arranjo. O exemplo seguinte portanto é equivalente ao anterior: float notas [4] = {0.0f, 0.0f, 0.0f, 0.0f}; A sintaxe exige que os elementos da inicialização sejam separados por vírgulas. Operadores sobre arranjos C e C++ oferecem a possibilidade de acessar cada posição do arranjo individualmente. Como em Objective Caml, o endereço da primeira posição é e as demais posições são obtidas somando um à anterior. Por exemplo, o código seguinte declaraum arranjo e inicializa todas as suas posições em seqüência. float notas [4]; notas[0] = 0.0f; notas[1] = 0.0f; notas[2] = 0.0f; notas[3] = 0.0f; Construção de tipos 36 Evidentemente, uma repetição for é uma construção particularmente adequada para percorrer todas as posições de um arranjo. Por exemplo: float notas [4]; for (int i = 0; i < 4; ++i) notas[i] = 0.0f; Objective Caml possui uma função que, aplicada a um arranjo, retorna o número de posições do arranjo. Isso não existe nem C nem em C++ (C++ tem uma outra implementação de arranjo, chamada vector que tem essa funcionalidade; será apresentada mais adiante). Objective Caml possui também mecanismos para verificar que o acesso aos arranjos é realizado dentro dos limites. C e C++ não oferecem esses mecanismos, e podemos perfeitamente escrever o seguinte programa: #include <cstdio> int main () { int arranjo[2] = {1, 2}; arranjo[2] = 3; printf("arranjo[0] = %i, arranjo[1] = %i, arranjo[2] = %i\n", arranjo[0], arranjo[1], arranjo[2]); } Observe que a função main inclui a definição de uma variável arranjo, com duas posições armazenando valores do tipo int. As posições válidas portanto são e . O programa realiza um acesso em escrita na posição , portanto fora dos limites e também um acesso em leitura. Utilizando g++ versão 4.0.1, esse programa é compilado sem erro (nem aviso). Ele pode até ser executado! E funciona! Olhe: daviddeharbe $ g++ -Wall prog.cpp -o prog daviddeharbe $ ./prog arranjo[0] = 1, arranjo[1] = 2, arranjo[2] = 3 Então o programa está correto? Claro que não. Nessa oportunidade ele executou normalmente, mas pode ser que em alguma outra plataforma, tenha um erro em tempo de execução. Nesse exemplo, é bastante fácil detectar esse erro lendo o código, mas em programas mais extensos essa tarefa de verificação por leitura é muito mais difícil. Esse pequeno programa ilustra uma diferença de filosofia entre as linguagens C e C++ de um lado, e a linguagem Objective Caml do outro. As primeiras privilegiam o desempenho em tempo de execução, mas não oferecem suporte para verificações elementares, criando grandes riscos do que erros passam despercebidos durante muito tempo até eles causarem problemas. Objective Caml, do outro lado, é muito mais rigorosa, e aplica um maior número de regras, tanto em tempo de compilação, quanto em tempo de execução. A implementação em Objective Caml de um dado algoritmo, será mais lenta que a implementação em C ou em C++, mas em compensação, oferecerá muito mais garantias de correção e de segurança. Construção de tipos 37 Arranjos como parâmetros de funções Nas linguagens C e C++, como em Objective Caml, os parâmetros são passados por valor. Isso significa que quando uma função é chamada, as variáveis que correspondem aos parâmetros formais da função comportam se como se fossem variáveis locais ao corpo da função que são inicializadas com o valor dos argumentos que foram utilizados para chamar essa função. No caso dos arranjos, o parâmetro formal é o mesmo arranjo que o argumento efetivo da chamada da função. O seguinte exemplo ilustra essa particularidade: #include <cstdio> void swap1 (int i, int j) { int tmp = i; i = j; j = tmp; } void swap2 (int tab[2]) { int tmp = tab[0]; tab[0] = tab[1]; tab[1] = tmp; } int main () { int a = 1, b = 2; printf("antes: a = %i, b = %i\n", a, b); swap1(a, b); printf("depois: a = %i, b = %i\n", a, b); int arranjo[2] = {1, 2}; printf("antes: arranjo[0] = %i, arranjo[1] = %i\n", arranjo[0], arranjo[1]); swap2(arranjo); printf("depois: arranjo[0] = %i, arranjo[1] = %i\n", arranjo[0], arranjo[1]); } A execução desse programa resultará na seguinte impressão na saída padrão antes: a = 1, b = 2 depois: a = 1, b = 2 antes: arranjo[0] = 1, arranjo[1] = 2 depois: arranjo[0] = 2, arranjo[1] = 1 Resumidamente, a explicação disso é que, em C e C++, um arranjo corresponde ao endereço da primeira posição do arranjo. Quando a função swap2 é chamada com o argumento arranjo, o endereço da primeira posição de arranjo é copiada para tab, que passa a ter então a sua primeira posição nesse endereço. Logo tab[0] e tab[1] são respectivamente idênticos a arranjo[0] e arranjo[1]. Construção de tipos 38 Arranjos como resultados de funções As linguagens C e C++ não permitem que o tipo de retorno de uma função seja um tipo arranjo. Uma possibilidade para contornar essa limitação é ter um parâmetro a mais que será atualizado no corpo da função. O exemplo seguinte ilustra essa técnica. Um primeiro exemplo O seguinte programa provê uma implementação em C++ de um algoritmo que calcula e imprime os números primos até um certo valor . Esse algoritmo é conhecido como o crivo de Eratosthenes. // programa que lê um número n da entrada padrão e imprime na saída padrão todos os números primos até n // implementa o crivo de Eratosthenes #include <cstdio> Observe que o exemplo inclui uma função tal que um dos parâmetros é um arranjo de booleanos. void compute_primes (bool sieve[], int n) // preenche o crivo de 0 até n-1 { sieve[0] = sieve[1] = false; for (int i = 2; i * i < n; ++i) { if (sieve[j]) { for (int j = i + i; j < n; j += i) { sieve[j] = false; } } } } int main () { int i, n; scanf("%d", &n); if (n < 2) return; bool sieve [n+1] = {true}; compute_primes(sieve, n+1); for (int i = 2; i < n+1; ++i) { if (sieve[i]) printf("%d ", i); } printf("\n"); } Detalhes sobre o crivo podem ser obtidos em: Weisstein, Eric W. "Sieve of Eratosthenes." From MathWorld--A Wolfram Web Resource. http:/ / mathworld. wolfram. com/ SieveofEratosthenes. html Construção de tipos 39 Arranjos multi-dimensionais Como em Objective Caml, um arranjo de dimensão de valores de um tipo t podem ser implementados em C e em C++ usando um arranjo de arranjos de dimensão de valores do tipo t. Uma matriz bi-dimensional de inteiros, de dimensão por , pode então ser declarada como: int matriz [n][m]; Um segundo exemplo float media (float tab[], int n) // calculo da media de um arranjo tab de floats de tamanho n. { float sum = 0.0f; for (int i = 0; i < n; ++i) sum += tab[i]; return sum/n; } int main() { int n; cin >> n; // o numero de alunos e lido if (n == 0) return 0; float notas[3][n]; for (int i = 0; i < n; ++i) // para cada aluno cin >> notas[0][i] >> notas[1][i] >> notas[2][i]; // ler as tres notas e guardar no arranjo notas for (int i = 0; i < 3; ++i) { // calculo e impressao da media de cada prova cout << "A media da " << i << "a prova e " << media(tab[i], n) << endl; } } Tipos registros Os tipos registros de C/C++ são essencialmente idênticos aos de Objective Caml: é um forma de agregar um número fixo de valores de diferentes tipos. Esses valores são chamados campos do registro. Cada campo possui um nome que o identifica e um tipo que especifica quais valores ele pode ter. Vamos agora ver a sintaxe em C/C++ através de um exemplo. Considere o seguinte tipo registro (em OCaml): type point = { x : float ; y : float } Em C/C++, o equivalente é typedef struct Sponto { float x; float y; } point; Pode-se acessar aos campos utilizando um operador que tem a mesma sintaxe que o de Objective Caml. Assim, caso desejarmos definir uma função que calcula a distância entre dois pontos dados, podemos escrever: #include <math.h> float distance (point p1, point p2) { Construção de tipos 40 float dx = p1.x - p2.x; float dy = p1.y - p2.y; return sqrtf(dx*dx + dy+dy); } C/C++ diferem de Objective Caml na forma de denotar valores de um tipo registro. Enquanto que em Objective Caml tem que explicitamente
Compartilhar