Aula 2 – Expressões em C

Os cinco tipos básicos de dados e seus modificadores

Em C temos os tipos de dados básicos char (caractere), int (inteiro), float (ponto flutuante), double (ponto flutuante de dupla precisão) e void (sem valor).
O tamanho que cada tipo ocupa na memória varia muito para cada plataforma, mas normalmente um caractere ocupa 1 byte e um inteiro 2 bytes.

Modificando os Tipos Básicos

Um modificador é usado para adaptar um tipo básico de dados para funcionar de maneira diferente. Lista de modificadores:
signed
unsigned
long
short

Por exemplo, um char ocupa 8 bits na memória e corresponde aos números -127 à 127. Se declararmos uma variável como sendo unsigned char, esta ainda terá 8 bits porém representará valores de 0 à 255.

Se o código declarar uma variável do tipo inteira com sinal, o compilador analisa o bit mais significativo, se este for 0 o número é positivo, se for 1 o número é negativo. Logo, dos 16 bits, poderemos usar somente 15 para representar o número:

número 32.767:
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Entendendo o complemento de 2
Por que usar o complemento de 2?
R: Simples, o computador faz apenas somas. Para subtrair, ele aplica o complemento de dois ao número e depois efetua a soma. A divisão é uma sucessão de subtrações. A multiplicação é uma sucessão de somas, e claro a soma é só somar.

Observação de desempenho: dividir é a operação que exige maior consumo de processamento, para evitar esse problema, usa-se o shift que veremos mais a frente.

Por exemplo, tendo o seguinte binário com sinal (em vermelho indica positivo):
0 0 0 0 0 0 1 1 = 3

Qual será o binário correspondente ao -3?

1° Passo: invetemos o número 3:
1 1 1 1 1 1 0 0
2° Passo: Somamos 1 ao número invertido
1 1 1 1 1 1 0 0 + 1 = 1 1 1 1 1 1 0 1 = – 125

Logo o binário correspondente ao -3 é: 1 1 1 1 1 1 0 1

Prova real: Somamos 3 (0000011) de -3 ( 1111101) = 10000000
Ignoramos o primeiro bit, afinal não faz parte dos 7 que representam o número, e teremos: 0000000 = 0 que é a subtração de 3 – 3.

Nomes de Identificadores

Identificadores são nomes de variáveis, funções, rótulos e outros objetos definidos pelo usuário. Eles podem variar de 1 a vários caracteres, sendo o primeiro uma letra ou um sublinhado (underline), e os demais devem ser letras, números ou sublinhados. Por exemplo:

Certo                          Errado
count                         1count
teste                           hi!teste
high_balance          high…balance

O tamanho máximo para um nome de identificador, se formos trabalhar com link edição, é de 8 caracteres, do contrário podemos usar até 31. Se usarmos mais caracteres, os demais (não significativos) serão ignorados pelo compilador.

C é case-sensitive, ou seja, um caractere maiúsculo é diferente de um minúsculo:
count != COUNT != Count

O identificador não pode ser igual a uma palavra-chave da linguagem e não pode ter o mesmo nome que funções definidas pelo programa ou em alguma biblioteca C.

Variáveis

O que é?
R: É um endereço para uma posição da memória com um nome
Por que usar?
R: Como diria o professor Simão: “Porque decorar o endereço da memória é f…”

Antes de usarmos uma variável em C, precisamos declará-la, usando a sintaxe:
tipo lista_de_variáveis;

Tipo deve ser um tipo de dado válido em C, e a lista de variáveis pode consistir em um ou mais nomes identificadores separados por vírgulas, por exemplo:

int i, j, k
short int number;
unsigned int unsigned_number;
double balance, loss;

Onde são declaradas?
Dentro de funções, na definição dos parâmetros das funções e fora de todas as funções. Sendo variáveis locais, parâmetros formais e variáveis globais, respectivamente.

Variáveis Locais (escopo local)

São declaradas dentro de funções. Só podem ser referenciadas dentro do bloco em que se encontram. Elas são criadas quando o bloco de código em que se encontram é executado e em seguida são destruídas. Por exemplo:


/**
* x e y são parâmetros formais. Devemos ter certeza de quais valores queremos
* receber na hora de definir estas variáveis.
* as três variáveis (x, y e z) são criadas ao iniciar a função e em seguida,
* quando a função termina, são destruídas
*/
int soma(int x, int y)
{
    int z;
    z = x + y;
    return z;
}

/**
* esta variável z não tem nenhuma relação com o z da função soma.
*/
int test(void)
{
    int z;
    z = 10;
    return z;
}

Por padrão, variáveis locais são armazenadas na pilha, que é uma área dinâmica e mutável. Por esse motivo elas não podem armazenar valores entre as chamadas.

Variáveis Globais (escopo global)

Estas são reconhecidas por qualquer lugar no programa, e guardam valores por toda a execução do programa, logo, consomem memória o tempo todo. Para definir uma variável global, basta declará-la fora de qualquer função:


// soma é uma variável global
int soma;
void somar(int x, int y);
    soma = x + y);
}

Elas são úteis quando a informação que contém é usada em várias partes do programa. Porém, devem ser usadas com cautela, afinal em uma manutenção o programador pode esquecer que a variável é global e alterando seu valor pode causar erros inesperados no programa.

Modificadores de Tipo de Acesso

const : Variáveis com este modificador são inicializadas na sua declaração e não podem ser alteradas na execução do programa.
Esse modificador pode ser usado para garantir que o valor apontado por um parâmetro que recebe um ponteiro em uma função, não seja alterado:


#include “stdio.h”
void sp_to_dash(const char *str);
void main(void)
{
    sp_to_dash(“isso é um teste”);
}

void sp_to_dash( const char *str)
{
    while(*str){
        if ( * str == ‘ ‘ ) printf ( “c%”, ‘-’);
        else printf(“%c”, *str);
            str++;
    }
}

Se tentarmos modificar o valor para o qual *str aponta, teríamos um erro.

volatile
Para garantir que o compilador analise sempre o valor de uma variável, usamos este modificador. Isso é útil, pois muitos compiladores C tem rotinas que otimizam certas expressões.
valatile indica ao compilador que não necessariamente o programador irá modificar o valor da variável em questão. Um exemplo disto é o valor de memória que corresponda à uma porta externa, por exemplo: 0x378 que é a LPT1. Neste caso podemos usar também o modificador const para garantir que o programanão irá modificar o valor da variável:


const volatile unsigned char *lpt1 = 0x378;

Expecificadores de Tipo de Classe de Armazenamento

Estes epecificadores informam ao compilador como as variáveis serão armazenadas. Eles precedem o resto da declaração da varíavel.

extern
C permite que módulos de um programa sejam compilados separadamente. Neste caso, se arquivos fonte de diferentes módulos possuim o mesmo nome para funções globais, podemos ter um problema. É importante informarmos ao linkeditor sobre isso, do contrário, ele irá alegar variáveis com mesmo nome, ou simplesmente escolher uma e usar.
Arquivo 1                        Arquivo2
int x, y;                              extern int x, y;
main(void)                      test(void)
{                                           {
x = 1;                                  x = y + 10;
}                                            }

static
São variáveis permantentes, que mantém seus valores entre as chamadas. Os efeitos são diferntes para variáveis locais e globais.

static local
Definida em escopo local, ela se comporta semelhante à global, ou seja, retém seu valor nas chamadas do bloco, porém só é reconhecida naquele escopo. Um exemplo de uso seria um gerador de uma série numérica:


series (void)
{
    static int series_num;
    series_num = series_num + 23;
    return (series_num);
}

series_num armazena o valor da última chamada, e pode ser usada para novas chamadas, garantindo assim um número sempre diferente para aquela execução do programa.
Se dado um valor de inicialização para a variável estática, este será atribuído a ela somente uma vez:


series(void)
{
    static int series_num = 10;
    series_num = series_num + 23;
    return (series_num);
}

static global
Neste caso o compilador irá criar uma variável global, porém que será reconhecida apenas no arquivo qual foi declarada. Isso faz com que ela não fique sujeita a efeitos colaterais, como alteração de seu valor em algum lugar indevido.
Para entender melhor, vamos a um exemplo do livro:


// Usar isto em arquivo separado
static int series_num;
void series_start(int seed);
int seires(void);

int series(void)
{
    series_num = series_num + 23;
    return series_num;
}

/** Inicializa series_num */
void series_start(int seed)
{
    series_num = seed;
}

Para iniciar o gerador de números, precisamos chamar series_start e passar por argumento algum número inteiro. Feito isso, cada chamada de series retornará um valor diferente.

Lembrando: Nomes de variáveis static locais são conhecidos apenas na função ou bloco em que foram declaradas. Nomes de variáveis static globais são reconhecidos apenas no arquivo em que elas estão. Então podemos colocar estas funções em uma biblioteca e usá-las sem problemas, porém não poderemos acessar diretamente o valor da variável series_num, pois esta está escondida do resto do programa.

register
Originalmente register só funcionava com dados int e char. Com o padrão ANSI ele se tornou possível para todos os tipos de dados. Usando este especificador de armazenamento, o compilador armazena o valor nos registradores da CPU, fazendo com que não haja perca de tempo lendo estes valores na memória RAM. Logo, a velocidade de acesso é muito mais rápida.
Agora que qualquer tipo de dado pode ser register, o padrão ANSI diz que “o acesso ao objeto é o mais rápido possível”, na prática ele só tem efeitos para variáveis do tipo inteiro e caractere. Apenas variáveis locais e parâmetros formais podem receber o especificador register, veja o exemplo:


int_pwr(int m, register int e)
{
    register int temp;
    temp = 1;
    for (; e; e--) temp = temp * m;
        return temp;
    }
}

Inicialização de Variáveis

A maioria das variáveis pode ser inicializada com algum valor no momento em que são declaradas. Exemplos:


char ch = ‘a’;
int first = 0;
float balance = 123.23;

Constantes

São valores que não são alterados pelo programa. Podem ser qualquer um dos cinco tipos de dados básicos.

Constantes Hexadecimais e Octais
Podemos definir constantes hexadecimais e octais. Sendo que um valor em exadecimal sempre é precedido de 0x e um valor octal de 0:

// 128 em decimal ou em binário: 1000 0000 (separamos de 4 em 4 para hexadecimal)
int hex = 0x80;
// 10 em decimal ou em binário: 00 001 010 (separamos de 3 em 3 para octal)
int oct = 012;

Constantes String

Uma string é um conjunto de caracteres. Lembrando que uma constante de um único caractere é definida com aspas simples, já uma string é definida com aspas duplas:


// Isto é uma constante caractere
char caractere = ‘a’;
// Isteo é uma constante string
char string = “a”;

Constantes Caractere de Barra Invertida

Alguns caracteres não podem ser digitados pelo teclado, são exemplos destes o retorno de carro (CR), Retrocesso (BS) entre outros:

Código                Significado
\b                         Retrocesso (BS)
\f                          Alimentação de formulário (FF)
\n                         Nova linha (LF)
\r                          Retorno de carro (CR)
\t                          Tabulação horizontal (HT)
\”                          Aspas duplas “
\’                           Aspas simples ‘
\0                         Nulo
\\                          Barra invertida
\v                         Tabulação vertical
\a                         Alerta (beep)
\N                        Constante octal (onde N é uma constante octal)
\xN                     Constante hexadecimal (onde N é uma constante hexadecimal)

Operadores

C define quatro tipos de operadores: aritméticos, relacionais, lógicos e bit a bit.

Operador de atribuição

Sempre usamos a seguinte sintaxe:
nome_da_variável = expressão;

Onde expressão pode ser qualquer valor aceito pelo tipo da variável à esquerda.

Conversão de Tipos em Atribuições

Por regra, o valor do lado direito (o lado da expressão – rvalue) é convertido no tipo do lado esquerdo (a variável de destino – lvalue), por exemplo:


char ch;
int x;
void func(void)
{
    ch = x		//	linha1
}

Na linha1 os bits mais significativos do valor inteiro x são ignorados, deixando ch com os 8 bits menos significativos. Exemplo:
x = 1025 = 00000100 00000001
ch = x
ch = 1 = 00000001

Exemplos assumindo uma palavra de 16bits:
Tipo destino Tipo da Expressão Possível Informação Perdida
signed char char Se valor > 127, o destino é negativo
char short int Os 8 bits mais significativos
char int Os 8 bits mais significativos
char long int Os 24 bits mais significativos
int long int Os 16 bits mais significativos
int float A parte fracionária e possivelmente mais
float double Precisão, o resultado é arredondado
double long double Precisão, o resultado é arredondado

Atribuições múltiplas

Podemos fazer várias atribuições de valores em um único comando:
x = y = z = 0;

Operadores Aritméticos

Operador Ação
Subtração, também menos unário
+ Adição
* Multiplicação
/ Divisão
% Mótulo da divisão (resto)
Decremento
++ Incremento

Os operadores -, + , * e / trabalham semelhante a outras linguagens. Quando / é aplicado a um inteiro ou caractere, qualquer resto será truncado, por exemplo 5/2 = 2. O operador % retorna o resto de uma divisão inteira, e não pode ser usado em dados com ponto flutuante.

Com o operador unário, qualquer número precedido por um sinal de menos, troca de sinal.

Incremento e Decremento

++ irá somar 1 ao seu operando e — subtrair:
x = 1;
x++; // x = 2
x–; // x = 1;

Se usarmos o operador antes do operando temos um efeito interessante:
x = 10; // x = 10
y = ++x; // y = 11, x = 11

x = 10; // x = 10
y = x++; // y = 10, x = 11

Por gerar código-objeto ao usarmos estes operadores, x = x + 1 é mais lento que x++.
Precedência:
maior ++ —

* / %
menor + –

Operadores Relacionais e Lógicos

A ideia de verdadeiro e falso está por trás dos conceitos dos operadores lógicos e relacionais. Em C, qualquer coisa diferente de 0 é verdadeiro, e 0 é falso. As expressões que usamo operadores relacionais sempre retornarão verdadeiro ou falso.

Tabela verdade de operadores:

p q p&&q p||q !p
0 0 0 0 1
0 1 0 1 1
1 1 1 1 0
1 0 0 1 0

Podemos fazer combinações:


10 > 5 && !(10 < 9) || 3 <= 4.

Isto resultará verdadeiro. O livro informa que C não tem nenhum operador lógico xor. Caso queira usar o ou exclusivo (xor), use a seguinte função:


xor(int a, int b) {
return (a || b) && !(a && b);
}

Precedência:

maior ! > >= < <= &&

menor ||

A expressão !0 && 0 || 0 retorna falso.

Com a adição dos parênteses !(0 && 0) || 0 ela retornará verdadeiro.

Operadores Bit a Bit

Como foi projetada para substituir a linguagem assembly, C suporta um conjunto completo de operadores bit a bit. As operações bit não podem ser usadas em float, double, long double e void. C não possui o operador lógico XOR, mas possui o operador bit a bit XOR, confira na tabela-verdade:

p q p ^ q
0 0 0
1 0 1
1 1 0
0 1 1

No operador XOR, o resultado é verdadeiro se e somente se um dos operandos for verdadeiro. Muito útil para uso de máscaras em circuitos elétricos, por exemplo:

BIT1 = 00001000

BIT2 = 00010000

STATUS_ENVIO = 00000000

Deseja-se ativar apenas o BIT1, então faz-se um OR entre o STATUS_ENVIO e o BIT1, resultando o

STATUS_ENVIO em 00001000.

Deseja-se desativar apenas o BIT1, então aqui vem o segredo, aplica-se o XOR entre o BIT1 e o STATUS_ENVIO, resultando em: 00000000.

Caso o BIT2 fosse ativado em meio a este processo não haveria problema algum, pois o ou exclusivo (XOR) cuidaria disto. Usei estas regras em projetos de automação. Pouparam-me um bom tempo e ficou muito fácil de entender o que estava acontecendo.

Lembrando: Operadores lógicos sempre produzem um resultado que é 0 ou 1. Já operadores bit a bit produzem um valor de acordo com a operação específica. Se x = 7, então x && 8 é verdadeiro, enquanto que x & 8 é falso.

Os operadores de deslocamento, >> e << movem todos os bits para a direita ou para esquerda o número de veses especificado. Exemplo de uso:

#include
int main(void) {
    unsigned char test = 0xff; //	11111111	= 255
    printf("valor de char : % d\n", test);
    test = test << 7; //	10000000	=	128
    printf("valor de char depois da operacao: %d\n", test);
    test = test >> 7;	 // 00000001	=	1
    printf("valor de char depois da segunda operacao : % d\n", test);
}

Um deslocamento não é uma rotação, então o que sair por um lado não entrará pelo outro. Como defini a variável sendo unsigned char (0 à 255) o que sair pela esquerda é perdido, por isso ocorreu esta “perca” de informações ao “desfazer” o deslocamento.

Divisão e multiplicação usando deslocamento de bits
Mais simples do que eu mesmo imaginava, aprendi isso agora hehe.
Para dividir deslocamos um bit à direita e para multiplicar por 2, deslocamos um bit à esquerda. Confira:

char x; x a cada execução da sentença Valor de x
x = x << 1; 00001010 10
x = x << 1 00010100 20
x = x << 1; 00101000 40
x = x << 1; 01010000 80
x = x >> 3; 00001010 10
x = x >> 1; 00000101 5
x = x >> 1; 00000010 2
x = x << 2; 00001000 8

Note que se perdeu informação quando deslocamos muito à direita. Operadores bit a bit são frequentemente usados em rotinas de criptografia. Funções simples para criptografia que invertem os bits usando o operador de complemento a um (~):


char encode(char ch) {
return (~ch);
}

char decode(char ch) {
return (~ch);
}

Operador ?

O operador ternário é muito útil onde precisa-se usar if else. Sua sintaxe é simples:

Exp1 ? Exp2 : Exp3 (se Exp1 for verdadeiro então Exp2, senão Exp3):


x = 10; y = x > 9 ? 100: 200;

// lê-se: se x maior que 9 então y recebe 100, senão recebe 200.

Os Operadores de Ponteiros & e *

Um ponteiro é um endereço de uma variável na memória. Ponteiros tem 3 grandes funções em C:
fornecem uma maneira rápida de referenciar elementos de uma matriz;
permitem que as funções modifiquem seus parâmetros de chamada;
suportam listas encadeadas e outras estruturas dinâmicas de dados;

O operador & retorna o endereço de memória de seu operando. Lembrando que um operador unário requer apenas um operando. Por exemplo:

m = &count; // lê-se: m recebe o endereço de count

O código acima insere o endereço na memória da variável count em m. Esse endereço é a posição interna da variável no computador.

O operador * é o complemento de &. Ele retorna o valro da variável localizada no endereço que o segue. Exemplo:

m = &count;
q = *m; // lê-se: q recebe o valor da variável apontada por m

Apesar de serem iguais, & e * não tem nada a ver com o AND e a multiplicação, respectivamente.

Ao usarmos variáveis de tipo ponteiro, devemos declará-las do mesmo tipo de dados para qual apontam. Por exemplo se ch for um ponteiro para um char, devemos declarar:

char *ch; /** lê-se: ch é um ponteiro que aponta para um endereço de memória de uma variável do tipo caractere **/

Esse programa irá imprimir o valor 10:

#include <stdio.h>

void main(void) {
    int target, source, *m;
    source = 10;
    m = &source;
    target = *m;
    printf(" % d", target);
}

O operador em Tempo de Compilação sizeof

O operador sizeof é um operador unário que retorna o tamanho, em bytes, da variável que recebe. Ele pode ajudar muito a gerar códigos portáveis que dependam do tamanho dos tipos de dados internos de C.

O operador Vírgula
É usado para encadear diversas expressões. Pode ser lido como faça isso e isso:
x = (y = 3, y + 1);
Primeiro atribui o valor 3 para y e, em seguida, atribui o valor 4 a x. É necessário usar parênteses, pois o perador vírgula tem precedência menor que o operador de atribuição.

Os operadores Ponto (.) e Seta (->)

Estes operadores referenciam elementos individuiais de estrutura e uniões. Estrutura e uniões são tipos de dados compostos que podem ser referenciados por um único nome (veremos mais a frente isso). Por exemplo:

#include <stdio.h>

struct employee {
    char name[80];
    int age;
    float wage;
} emp;

struct employee *p = &emp;

int main(void) {
    // Acesso via ponto
    emp.wage = 123.23;
    printf("wage: %f\n", emp.wage);

    // Acesso via seta
    p->wage = 321.22;
    printf("wage new: %f\n", p->wage);
}

Parênteses e Colchetes como Operadores

Em C, parênteses são operadores que aumentam a precedência das operações dentro deles.
Colchetes fazem indexação de matrizes e serão discutidos mais adiante. Exemplo:

#include
char s[80];

int main(void) {
s[3] = ‘X’;
printf(” % c”, s[3]);
}

Aqui atribuímos X ao quarto elementro da matriz (elas começam sempre em 0) e depois imprimimos este valor.

Resumo das Precedências
Ordem de procedência de operadores em C:

Maior () [] -> 

! ~++ — – (tipo) * & sizeof

* / %

+ –

<< >>

< <= > >=

== !=

&

^

|

&&

||

?

= += -= *= /=

Menor ,

Expressões

Operadores, constantes e variáveis são os elementos que constituem as expressões. Uma expressão em C é qualquer combinação válida desses elementos.

Ordem de avaliação

Como o padrão ANSI não especifica ordem de avaliação das expressões, cada compilador é livre para implementar da maneira que quiser, por exemplo:
x = f1() + f2();

Não temos garantia que f1 será executada antes que f2, a menos que usemos parênteses para definir uma ordem.

Conversão de Tipos em Expressões

Quando constantes e variáveis são misturadas em uma expressão, elas são convertidas a um mesmo tipo, seguindo as seguintes regras:

SE um operando é long double
ENTÃO o segundo é convertido para long double.
SENÃO, SE um operando é double
ENTÃO o segundo é convertido para double.
SENÃO, SE um operando é float
ENTÃO o segundo é convertido para float.
SENÃO, SE um operando é unsigned long
ENTÃO o segundo é convertido para unsigned long.
SENÃO, SE um operando é long
ENTÃO o segundo é convertido para long.
SENÃO, SE um operando é unsigned
ENTÃO o segundo é convertido para unsigned.

Se um operando é long e o outro é unsigned, e se o valor de unsigned não pode ser representado por um long, os dois operandos são convertidos para unsigned long.
Exemplo:


char ch;
int i;
float f;
double d;
result = (ch / i)		+ 	(f * d) 		-	(f + i);

Primeiro, o caractere ch é convertido para int, e float f é convertido para um double. Em seguida, o resultado de ch / i é convertido para double porque f * d é double. O resultado final é double porque, neste momento, os dois operandos são double.

Casts

Podemos forçar uma expressão a ser de algum tipo específico usando uma construção chamada cast. Seu uso:
(tipo) expressão
(float) x / 2 Garante que o valor da divisão de x por 2 será um float.
Exemplo de uso:

#include

int main(void) {
int i;
for (i = 1; i <= 100; ++i)
printf(“%d / 2 e: %d\n”, i, (float) i / 2);
}

Sem o (float), uma divisão inteira seria efetuada, o cast neste caso está assegurando que a parte fracionária da resposta seja mostrada.

Espacejamento e Parênteses

É muito importante que nossos códigos sejam legíveis tanto quanto possível. Então procure sempre usar espaços e tabulações à vontade. Siga ou crie um padrão próprio para isso, mas use um padrão. As expressões abaixo são iguais:

x=10/y~(127/x);
x = 10 y ~(127 / x);

O uso de parênteses não deixa mais lenta a execução do programa, então devemos sempre deixar claro a ordem de avaliação de uma expressão, por exemplo:

x=y/2-34*temp&127;
x = ( y/3 ) – ( ( 34*temp ) & 127 );

Avreviações C

Podemos apreviar o uso de certos comandos em C, por exemplo:

x = x + 10 // pode se escrito:
x += 10; // lê-se: x recebe x + 10

Programas profissionais usam muito essas abreviações, então devemos nos familiarizar com elas.

Minha reflexão

No começo parecia que seria impossível em 7 horas ver um capítulo inteiro, mas vi que não é tão difícil quanto parece. Ler, entender, e escrever de uma forma que mais pessoas possam compreender isso é algo muito gratificante. Mesmo que ninguém além de mim leia, sinto que a missão está cumprida.

Comments

  1. By Wandson da Rocha Cos

    Responder

  2. By tamires

    Responder

    • Responder

  3. By Darci

    Responder

    • Responder

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *