Laboratório de Programação Prof. Oscar Luiz Monteiro de Farias [email protected] Capítulo 2 Tipos, Operadores e Expressões Variáveis, operadores e exps Variáveis e constantes são os objetos de dados básicos manipulados por um programa em C. As declarações listam as variáveis a serem usadas, definem os tipos assumidos pelas mesmas e eventualmente estabelecem os seus valores iniciais. Os operadores especificam as operações que devem ser realizadas com as variáveis. As expressões combinam variáveis e constantes, por meio dos operadores, a fim de produzir novos valores (que são eventualmente armazenados em variáveis). Identificadores (1) Os nomes de variáveis e constantes simbólicas são compostos por letras e dígitos; o primeiro caracter deve ser uma letra. Não inicie identificadores com um '_', pois as funções da biblioteca padrão normalmente usam nomes assim. Maiúsculas e minúsculas são distintas (x e X representam nomes distintos) Use minúsculas para as variáveis e maiúsculas para as constantes simbólicas (convenção do C). Identificadores (2) Pelo menos os 31 primeiros caracteres de um nome interno são significativos. Para os nomes externos o padrão ANSI garante que apenas os 6 primeiros caracteres são significativos, bem como um único tipo de letra (m ou M). As keywords (if, else, int, for, float, etc.) não podem ser usadas como nomes de variável. As keywords devem ser escritas em minúsculas. Tipos de dados básicos (1) char - um único byte - capaz de representar um caracter no conjunto de caracteres local int - inteiro, reflete o tamanho dos inteiros no hardware da máquina → usualmente 16bits (-32768 <= valor <= 32767) ou 32 bits float – ponto flutuante em precisão simples usualmente 32 bits - com pelo menos 6 dígitos significativos 10-38 <=valor<=10+38 double - ponto flutuante com precisão dupla Os tamanhos dos objetos em ponto flutuante são definidos pela implementação Tipos de dados básicos (2) Há qualificadores que podem ser aplicados aos tipos básicos long double - ponto flutuante com precisão estendida. short int - inteiro curto, normalmente ocupa16 bits long int - inteiro longo, normalmente ocupa 32 bits Ex.: short int counter; Cada compilador é livre para escolher os tamanhos adequados ao seu hw; Restrições: i) short e int devem ocupar pelo menos 16 bits; ii) short <=int <= long Os qualificadores signed ou unsigned podem ser aplicados a char ou a qualquer inteiro. Tipos de dados básicos (3) Números unsigned são sempre positivos ou zero. Obedecem as leis da aritmética módulo 2n, onde n é o número de bits do tipo correspondente. ex.: unsigned char x; → 0 <= x <= 255 signed char x; → -128 <= x <= 127 (em máquinas que usam o complemento a dois) Os arquivos headers standard <limits.h> e <float.h> contêm constantes simbólicas para todos os tamanhos, juntamente com soutra propriedades da máquina e do compilador. Os arquivos headers do C estão localizados em /usr/include. Ver o apêndice B do livro K&R. Constantes (1) Constante inteira → 1234 Constante long → 123456789L Um inteiro muito grande para caber em um int será considerado como long. Constantes não sinalizadas são escritas com a letra u ou U ao final O sufixo ul ou UL indica unsigned long Constantes de ponto flutuante contêm um ponto decimal → 123.4 ou um expoente → 1e-2 Constantes (2) Seu tipo é double, a menos que tenham um sufixo. F ou f → indicam uma constante float L ou l → constante long double O valor de um inteiro pode ser especificado em decimal, octal ou hexadecimal Um zero inicial em uma constante inteira significa octal → 037 (octal) = 31 em decimal 0X ou 0x inicial significam um número hexadecimal → 0x1f ou 0X1F = 31 em decimal Constantes octais ou decimais podem ser seguidas por l ou L (para torná-las long) ou por u ou U (para torná-las unsigned). Constantes (3) Uma constante de caracter é um inteiro, escrito como um caracter entre aspas → 'x' O valor de uma constante de caracter é o valor numérico do caracter no conjunto de caracteres da máquina Ex.: a constante de caracter '0' tem o valor 48 no conjunto de caracteres ASCII, que não está relacionado ao valor numérico 0. Escrevendo '0' no lugar do valor numérico 48 o programa se torna independente do valor e mais fácil de ser lido. Constantes de caracteres participam em operações aritméticas como quaisquer outros inteiros Seqüência de escape → \n - \t - \b (um único caracter) Constantes (4) Um padrão de bits arbitrário, do tamanho de um byte, pode ser especificado por '\000', onde 00 é de um a dois dígitos octais (0..7). ou por '\xhh`, onde hh é de um a dois dígitos hexadecimais (0...9, A...F). #define VTAB '\013' /* ASCII vertical tab */ #define BELL '\007' /* ASCII bell character */ #define VTAB '\xb' /* ASCII vertical tab */ #define BELL '\x7' /* ASCII bell character */ Seqüências de escape \\ \a alert (bell) backslash \b backspace \? question mark \f formfeed \' single quote \n newline \" double quote \r carriage return \ooo octal number Constantes (5) Uma expressão constante é uma expressão que envolve apenas constantes É avaliada em tempo de compilação Podem ser usadas como constantes também Exemplos: #define MAXLINE 1000 char line[MAXLINE+1]; #define LEAP 1 /* in leap years */ int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31]; Constantes (6) Uma constante do tipo cadeia (string) ou literal do tipo cadeia é uma seqüência de zero ou mais caracteres delimitados por aspas ''Eu sou uma cadeia de caracteres'' '''' /* cadeia vazia ou nula */ As mesmas seqüências de escape ssão usadas nas cadeias Constantes do tipo cadeia podem ser concatenadas em tempo de compilação ''Rio'' ''de'' ''Janeiro'' == ''Rio de Janeiro'' Constantes (7) Uma cadeia é um vetor cujos elementos são caracteres. A representação interna de uma cadeia possui um caracter nulo '\0' ao seu final. A área física de armazenamento é sempre um a mais do que o número de caracteres entre aspas. A função da biblioteca padrão strlen (s) retorna o tamanho da cadeia s, excluindo o '\0' final. Ver prog17-chap02-pg38.c O arquivo <string.h> possui declarações de várias funções de cadeias Constantes (8) 'x' não é o mesmo que ''x'' 'x' é uma constante de caracter, um inteiro usado para produzir o valor numérico da letra x no conjunto de caracteres da máquina. ''x'' é uma cadeia de caracteres (ou vetor) que contém o caracter x e um '\0'. Constante de enumeração (1) É uma lista de valores inteiros constantes enum boolean { NO, YES }; o primeiro nome em uma enum tem o valor 0, o seguinte 1, e assim por diante, a não ser que valores explícitos sejam especificados. Se nem todos os valores foram especificados, aqueles que não o foram continuama progressão a partir do último valor informado enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }; /* FEB = 2, MAR = 3, etc. */ Constante de enumeração (2) Os nomes em diferentes enumerações devem ser distintos. As enumerações fornecem uma forma conveniente de associar valores constantes a nomes. É uma alternativa ao uso de #define, com a vantagem dos valores serem gerados pelo compilador. Declarações (1) Todas as variáveis devem ser declaradas antes de serem usadas Certas declarações podem ser feitas implicitamente pelo contexto (eg. variáveis extern). Uma declaração especifica um tipo e é seguida por uma ou mais variáveis daquele tipo int lower, upper, step; char c, line[1000]; Declarações (2) Pode-se inicializar uma variável em sua declaração char esc = '\\'; int i =0; float eps = 1.0e-5; Se a variável não for automática (local à função) a inicialização é feita uma única vez, antes do início do programa e o inicializador deve ser uma expressão constante. Declarações (3) Uma variável automática explicitamente inicializada tem seu valor inicializado toda vez que a função ou bloco em que se encontra ganhar o controle do programa. Por default as variáveis externas e estáticas são inicializadas com zero. Variáveis automáticas sem inicializações explícitas assumem valores iniciais indefinidos (lixo). Declarações (4) O qualificador const pode ser aplicado à declaração de qualquer variável, indicando que a mesma não poderá ser alterada. const double e = 2.71828182845905; const char msg[] = "warning: "; const pode ser aplicado a argumentos do tipo vetor, indicando que os seus elementos não podem ser alterados int strlen(const char[]); Operadores aritméticos +, -, *, /, % (módulo) A divisão inteira trunca a parte fracionária x % y é igual a zero se x for um múltiplo de y; em caso contrário é o resto da divisão de x por y + e – possuem a mesma precedência que é menor que a dos operadores *, / e %, que por sua vez é menor que a dos operadores unários + e -. Os operadores aritméticos se associam da esquerda para a direita. Operadores relacionais e lógicos (1) Relacionais: >, >= , < , <= possuem a mesma precedência. De igualdade: == , != têm maior precedência que os operadores relacionais. Os operadores relacionais possuem menor precedência que os aritméticos. Expressões como i < lim -1 são avaliadas como i < (lim -1) Lógicos: &&, || Operadores relacionais e lógicos (2) Expressões conectadas por && e || são avaliadas da esquerda para a direita, e a avaliação pára, assim que a veracidade ou falsidade do resultado for conhecida. A maioria dos programas em C fundamenta-se nessas propriedades. for (i=0; i < lim-1 && (c = getchar()) != '\n' && c != EOF; ++i) s[i] = c; A precedência de && é maior que a de || e ambos têm menor precedência que os operadores relacionais e de igualdade. A precedência de != é maior que a da atribuição = Operadores relacionais e lógicos (3) Por definição, o valor numérico de uma expressão relacional ou lógica é 1 se a relação for verdadeira e 0 se a relação for falsa. O operador unário de negação ! converte um operando diferente de 0, ou verdadeiro, no valor 0, e um operador 0 ou falso no valor 1. if (!valid) /* ao invés de */ if (valid == 0) Conversões de tipo (1) Operador com operandos de diferentes tipos → conversão dos operandos para um tipo comum, segundo regras. Em geral, as conversões automáticas são aquelas que convertem um operando mais particular/estreito para um mais geral/largo, sem perder informações (e.g. inteiro → ponto flutuante, nas expressões do tipo f + i). Expressões sem sentido não são permitidas (eg. usar um float como subscrito de um vetor). Expressões que perdem informações, e.g. atribuir um long a um short int, ou um float a um int, podem gerar um aviso do compilador, porém, em geral, são válidas. Conversões de tipo (2) Um char é apenas um pequeno inteiro, de modo que podem ser usados em expressões aritméticas. Esta propriedade fornece grande flexibilidade em certos tipos de transformações de caracteres. Ver prog18-chap02-pg40.c - função atoi s[i] – '0' fornece o valor numérico do caracter armazenado em s[i], pois os valores de '0', '1' … '9' formam uma seqüência continuamente crescente (vide ASCII table). Ver prog19-chap02-pg40.c - função lower (não é válida para o conjunto de caracteres EBCDIC) Conversões de tipo (3) O arquivo header padrão <ctype.h> descrito no apêndice B do livro do K&R (/usr/include/ctype.h) fornece funções para teste e conversão de caracteres, independente do conjunto de caracteres usado. tolower(c) / isdigit(c) C não especifica se as variáveis do tipo char são quantidades signed ou unsigned. Dependendo do hardware a conversão de um char para int pode produzir um inteiro negativo ou positivo. Conversões de tipo (4) Em algumas máquinas um char, cujo bit mais à esquerda é 1 será convertido para um inteiro negativo (''sign extension''). Em outras, um char é promovido a um int, incluindo-se 0 nos bits mais à esquerda, e é sempre positivo. A definição de C garante que qualquer caracter pertencente ao conjunto de caracteres da máquina jamais será negativo. Conversões de tipo (5) Lembrar que o valor de uma expressão relacional ou lógica é 1 (verdadeiro) ou 0 (falso). d = c >= '0' && c <= '9' atribui 1 a d, se c for um dígito, e 0 se não for. Todavia, funções como isdigit (c) podem retornar qualquer valor não-zero, indicando um resultado verdadeiro. Na parte de teste de if, while, for, etc., verdadeiro significa não-zero. Conversões de tipo – regras (1) Ver apêndice A, seção 6, do livro do K&R. Se não houver operandos do tipo unsigned, o seguinte conjunto informal de regras basta: a) If either operand is long double, convert the other to long double. b) Otherwise, if either operand is double, convert the other to double. c) Otherwise, if either operand is float, convert the other to float. d) Otherwise, convert char and short to int. e) Then, if either operand is long, convert the other to long. Conversões de tipo – regras (2) Em geral as funções matemáticas, como as de <math.h> usam precisão dupla. As regras de conversão são mais complicadas quando operandos unsigned estão envolvidos. A comparação entre valores signed e unsigned são dependentes da máquina, pois dependem do tamanho (# de bits) assumidos pelos inteiros. Supor int (16 bits) e long (32 bits) → -1L < 1U, porque 1U, unsigned int, é promovido a signed long. -1L > 1UL porque -1L é promovido a unsigned long e aparenta ser um número positivo grande. Conversões de tipo – regras (3) int i; char c; ... i = c; c = i; O valor de c é inalterado. Isto é verdade independente de se ter ou não extensão de sinal envolvida. Todavia, invertendo-se a ordem dos comandos de atribuição poderá levar à perda de informação. Conversões de tipo – regras (4) Como os argumentos de uma função são expressões, ocorrerá uma conversão de tipos quando eles forem passados para a função (atribuídos aos parâmetros). usar um protótipo de função para forçar a coerção de tipos (será visto + adiante) Typecast – a conversão explícita de tipos poderá ser forçada em qualquer expressão através do operador unário cast (molde). Em (type name) expression expression será convertida para o tipo especificado, segundo as regras já mencionadas. Conversões de tipo – regras (5) Se n é um int deve-se ter sqrt ((double(n)) pois sqrt espera um double como argumento. O typecast converte o int para double, antes de passá-lo para sqrt. Se forem declarados argumentos por um protótipo de função (como normalmente deve ser), esta declaração causará uma coerção automática dos argumentos, quando a função for invocada. double sqrt(double) /* protótipo de sqrt */ raiz2 = sqrt(2); /* chamada de sqrt */ Força a conversão do inteiro 2 para um double sem precisar do cast. Ops de Incremento e Decremento ++ e - - Podem ser usados prefixados ou pós-fixados. ++n e n++ (ambos incrementam n) ++n incrementa n antes que seu valor seja usado, n++ incrementa n depois que seu valor foi usado. Se n = 5 x = n++; /* atribui 5 a x */ x = ++n; /* atribui 6 a x */ em ambos os casos n torna-se 6. Ver exemplos: prog21-chap02-pg44.c e prog22- chap02pg44.c Operadores Lógicos Bit-a-bit Podem ser aplicados a operandos integrais → char, short, int e long, sinalizados ou não. & - AND bit-a-bit | - inclusive OR bit-a-bit ^ - exclusive OR bit-a-bit << - left shift >> - right shift ~ - one's complement (unary) Operador AND Bit-a-bit & O operador AND bit-a-bit & é muito usado para mascarar algum conjunto de bits: n = n &0177; zera todos os bits de de n, exceto os 7 menos significativos. n1n2n3n4n5n6n7n8|n9n10n11n12n13n14n15n16 01020304050607080|099111010111111111212111313111414111515111616 ------------------------------------------01020304050607080|099nn1010nn1111nn1212nn1313nn1414nn1515nn1616 Operador OU Bit-a-bit | O operator OU bit-a-bit | é usado para ativar bits: x = x | ativo; torna 1, em x, os bits que são 1 em ativo. n1n2n3n4n5n6n7n8|n9n10n11n12n13n14n15n16 1112031405060708|09110011112013114115116 → ativo -----------------------------------1112n314n5n6n7n8|n9110n11112n13114115116 Operador OU exclusivo Bit-a-bit ^ O operator OU exclusivo bit-a-bit ^ torna 1 cada posição de bit onde seus operandos possuem bits diferentes e 0 onde forem iguais. Deve-se distingüir os operadores bit-a-bit & e | dos operadores lógicos && e ||, os quais implicam em uma avaliação da esquerda para a direita de umvalor booleano. P. ex., se x==1 e y==2, então x & y é zero, enquanto x && y é 1. Operadores de deslocamento (1) << → executa deslocamento à esquerda do operando à esquerda, pelo número de bits informado pelo operando à direita. >> → executa deslocamento à direita do operando à esquerda, pelo número de bits informado pelo operando à direita. O operando à direita deve ser um inteiro positivo. x << 2 desloca os bits de x para a esquerda de duas posições (equivalente a multiplicar por quatro). Operadores de deslocamento (2) Um deslocamento à direita de uma quantidade sem sinal, sempre preenche os bits vagos com zero. Um deslocamento à direita de uma quantidade com sinal preenche os bits vagos com o bit de sinal (“deslocamento aritmético”) em algumas máquinas e com bits zero (“deslocamento lógico”) em outras. Operador complemento de um ~ O operador unário ~ gera o complemento a 1 de um inteiro, isto é, converte cada bit 1 em 0 e vice-versa. x = x & ~ 077; (077 == 0000000000111111) zera os seis últimos bits de x. x & ~ 077 é independente do tamanho da palavra, e é preferível a x & 0177700, que assume que x possui 16 bits. (0177700 == 1111111111000000) Exemplo de uso de ops bit-a-bit Ver prog23-chap02-pg45.c função getbits(x,p,n) que retorna (ajustado à direita) o campo de n-bits de x que se inicia na posição p. Assume-se que 0 é a posição mais à direita e que n e p são valores positivos e com sentido. getbits(x, 4, 3) → retorna os tres bits referentes às posições 4, 3 e 2, ajustados à direita. A expressão x >> p + 1 – n move o campo desejado para a direita da palavra. ~0 → todos os bits 1; ~0 << n → coloca 0 nos n bits mais à direita. ~ (~0 << n) → transforma os n bits mais à direita em 1. ex.: .|.|.|.|.|.|.|.|.|.|.|4|3|2|1|0 p=4, n =3 Ops e exps de atribuição i =i + 2 é equivalente a i += 2 += é um operador de atribuição A maioria dos operadores binários possui um operador de atribuição op=, onde op pode ser: + - * / % << >> & ^ |. Se exp1 e exp2 são expressões, então exp1 op= exp2 é equivalente a exp1 = (exp1) op (exp2) x *= y + 1 é equivalente a x = x * (y +1) Operador de atribuição - exemplo Ver prog24-chap02-pg46.c A função bitcount conta o número de bits 1 no seu argumento inteiro. Declarar o argumento x como unsigned asssegura que, ao ser deslocado à direita, os bits vagos serão preenchidos com zeros, e não com bits de sinal, não importando a máquina em que o programa está sendo executado. O comando de atribuição tem um valor, e pode ocorrer em expressões. Ex.: while ((c = getchar()) != EOF) O tipo de uma expressão de atribuição é o tipo do seu operando à esquerda, e o valor é o valor após a atribuição. Expressões condicionais (1) if (a > b) /* z = max(a, b) */ z = a; else z = b; /* é equivalente a: */ -----------------------------------z = (a > b) ? a : b; /* z = max(a, b) */ exp1 ? exp2 : exp3 /* ?: → operador ternário */ Primeiramente a expressão exp1 é avaliada. Se for diferente de zero (verdadeira), então exp2 é avaliada e este é o valor da expressão condicional. Caso contrário exp3 é avaliada e este é o valor. Se exp2 e exp3 são de tipos diferentes, aplicam-se as regras de de conversão de tipos. Expressões condicionais (2) Obs.: a precedência do operador ternário ?: é muito baixa, logo acima da atribuição. A expressão condicional leva a códigos compactos. Ex: O loop a seguir imprime n elementos de um vetor, 10 por linha, com cada coluna separada por um espaço, e com cada linha, inclusive a última, terminada por um caracter de new line (\n). for (i = 0; i < n; i++) printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : ' '); Precedência e ordem de avaliação(1) Operadores Associatividade () [] -> . E→ D ! ~ ++ -- + - * (type) sizeof D→E */% E→ D +- E→ D << >> E→ D < <= > >= E→ D == != E→ D & E→ D ^ E→ D | E→ D && E→ D || E→ D ?: D→E = += -= *= /= %= &= ^= |= <<= >>= D→E , E→ D Precedência e ordem de avaliação(2) () refere-se à invocação (chamada) de uma função. -> e . são usados para se ter acesso a membros de estruturas (capítulo 6). sizeof (tamanho de um objeto) - (capítulo 6). * (indireção através de pointer) - (capítulo 5). Operador , (capítulo 3). Precedência e ordem de avaliação(3) A precedência dos operadores bit-a-bit &, ^ e | fica abaixo de == e !=. if ((x & MASK) == 0) ... C, como a maioria das linguagens, não especifica a ordem em que os operandos de um operador são avaliados (exceções: &&, ||, ?: e ',') em x = f() + g(); f pode ser avaliado antes de g e viceversa. Se f ou g alterar uma variável do qual a outra depende, o valor de x dependerá da ordem de avaliação. Precedência e ordem de avaliação(4) A ordem em que os argumentos de uma função são avaliados não é especificada. Em printf("%d %d\n", ++n, power(2, n)); /* errado */ poderá produzir resultados diferentes em compiladores diferentes. Certo seria: ++n; printf("%d %d\n", n, power(2, n)); Chamadas de funções, comandos de atribuição aninhados e operadores de incremento e decremento podem produzir efeitos colaterais (alguma variável é alterada, como resultado da avaliação de uma expressão). Moral da história: o código dos programas deve independer, sempre, da ordem de avaliação de expressões.