Variáveis, tipos, literais, expressões e Cia

Este material cobre diversos aspectos relacionados com o conceito de variável, p.ex. declaração, atribuição, inicialização, tipo. A linguagem C possui várias categorias de variáveis e a linguagem C++ estendeu ainda mais estas categorias.

O material abaixo focaliza nas variáveis locais simples .

Em diversas linguagens de programação existe o conceito de variável. Uma variável corresponde a uma posição ou espaço de memória. Uma posição ou espaço de memória pode guardar um valor. Uma declaração de variável define seu tipo e o identificador (nome) e também outros atributos. Uma das coisas que podemos fazer com uma variável é usa-la como alvo de uma iniciação/inicialização ou ainda como alvo de uma operação de atribuição (assignment). Na linguagem C a Declaração pode ser distinta da Definição de uma variável. Mas não vamos explorar isso por agora

A sintaxe simplificada para a declaração de uma variável é:
<tipo> <nomeDeVariavel>;
Esta forma apenas declara a variável; outra sintaxe:
<tipo> <nomeDeVariavel>=<expressao>;
Esta segunda forma corresponde a declarar e iniciar/inicializar o valor da posição de memória correspondente à variável.

Mais à frente na disciplina veremos que as varoáveis podem ser definidas basicamente dentro de dois escopos: (i)dentro de uma função ou (ii)fora de uma função. Além disso uma variável pode (i) ter o modificador static ou não ter o modificador static. Neste material estaremos falando “basicamente” de uma variável definida dentro de uma função e sem o modificador static. Na definição deste tipo devariável sem “inicialização” o conteúdo inicial deste tipo de variável é indefinido (“lixo”de memória”).

A sintaxe simplificada de um comando ou operação de atribuição é:
<identificador da variável> = <expressão>;
Neste comando o valor correspondente à expressão é copiado para a posição de memória correspondente à variável. Note que o antigo valor é perdido. Só podemos atribuir um valor para uma variável que tenha sido declarada. A sintaxe e semântica de uma
<expressão> serão definidas mais tarde; os programas abaixo apresentam expressões simples e bem intuitivas. Um exemplo de tipo em C é o tipo int correspondente a um subconjunto finito do conjunto dos inteiros da Matemática e outro exemplo é o tipo double correspondente a um subconjunto finito do conjunto dos números reais da Matemática.

Quando o fluxo de controle atinge um “comando de declaração” de varável local a variável passa a ficar disponível para atribuições e acesso. Quando o fluxo de controle atinge uma atribuição a “expressão” à direita do operador de atribuição (=) é avaliada e o resultado da avaliação da expressão passa a ser o valor da variável que está à esquerda o operador de atribuição. Há uma certa similaridade sintática e semântica entre o comando de atribuição e a iniciação ou inicialização de uma variável.

O trecho de programa a seguir ilustra algumas declarações de variáveis e comandos de atribuição:
int v1;
double v2;
v1=123;
v2=3.1415;
No trecho acima são declaradas duas variáveis, a variável v1 é do tipo int e a variável v2 é do tipo double. Vemos ainda dois comandos de atribuição usando expressões simples.

O tipo restringe os valores que podem ser atribuidos a uma variável. O tipo int quando tem 32 bits permite a representação de valores de -2.15 bilhões até +2.15 bilhões, o tipo long quando tem 64 bits permite a representação de valores de -9.22 quintilhões ate' +9.22 quintilhões. A restrição de tipo para uma variável acarreta maior disciplina por parte do programador e permite programas mais eficientes e com menor probabilidade de erros. As regras com relação a tipos e os literais ou constantes variam de linguagem para linguagem. O trecho a seguir infelizmente é permitido na linguagem C:
int v1;
v1=3.1415; /* poderia/deveria ser erro! mas não é... */


 A associação de um tipo a uma variável na Computação é similar à amarração de uma variável a um domínio como ocorre na Matemática. No entanto deve ser ressaltado que o conceito de variável em Computação e o conceito de variável em Matemática são conceitos que apresentam aspectos distintos. Uma das distinções mais notáveis é que uma variável na Matemática, usualmente, não tem um valor específico. Considere a  expressão da Matemática
y=x+3
A variável x não tem um valor associado a ela, esta expressão pode ser interpretada como representando o lugar geométrico dos pontos de um plano correspondentes a uma reta. A variável (da Matemática) x pode tomar qualquer valor real. As vezes os autores cuidadosos explicitam que a variável x "pode tomar valores em" ou "pertence ao" domínio dos números Reais, outros autores assumem que o leitor irá deduzir isso.  Na Computação, uma variável, no escopo onde ela é válida, sempre tem um determinado valor associado a ela, este valor possivelmente muda ao longo do tempo, mas a cada momento o valor de uma variável é único. Na Matemática, uma expressão envolvendo uma variável, deve sempre considerar que aquela variável, em todo ponto que ela aparece, tem sempre um mesmo valor. Na Computação, em particular na linguagem C, isto não é verdade, considere o trecho de programa abaixo:
int x;
x=(x=((x=1)+1)+1);
Na expressão acima a variável x aparece três vezes. Em cada uma das vezes x tem um valor distinto associado. (A expressão acima não tem muito sentido (melhor escrever
x=3;) mas, mais a frente na disciplina, veremos o conceito de função ou método e teremos oportunidade de construir expressões onde este aspecto faça mais sentido)

A linguagem C tem vários tipos

int, float, char, short (short integer) , long (long integer), double (double-precision float)

novos tipos podem ser construídos com arranjos, estruturas e uniões, na linguagem C, além das várias extensões que a linguagem C++ introduziu e serão vistas mais à frente na disciplina.

É interessante observar que não existe o mnemônico integer ao invés de int, ou real ao invés de float, afinal os computadores representam apenas um subconjunto finito dos domínios infinitos correspondentes encontrados na Matemática.

A existência de tipos, além de disciplinar o programador, é justificada por questões de eficiência. Com a tecnologia atual ainda não temos sistemas gerais que conseguem descobrir dinamicamente e de forma eficiente como representar os valores com um número variável de posições de memória para os diferentes domínios de problemas. O programador fica encarregado de definir e operar com tipos apropriados para a solução que está sendo implementada.

As constantes inteiras ou literais inteiros podem ser do tipo int, por exemplo 1234, podem ser do tipo longo colocando a letra L (maiúscula ou minuscula no final), por exemplo 1234L ou 1234l, podem ser escritas na base 8 (octal) colocando o dígito 0 no início por exemplo 012 equivale a 10, podem ser escritas na base 16 (hexadecimal) prefixando com 0x ou 0X por exemplo 0x12 equivale a 18. As contantes inteiras com um u ou U no final são consideradas do tipo unsigned e 1234UL é uma contante do tipo long sem sinal.

Um outro tipo de variável da linguagem C são as variáveis que armazenam endereços de memória, em particular endereços de memória de outras variáveis. Essas variáveis as vezes são chamadas simplesmente de “ponteiros” (Como será visto à frente, na disciplina, uma das decisões de projeto da linguagem C foi que a passagem de parâmetro das funções seria apenas por valor e 1 (uma) das razões da necessidade de ponteiros reside no desejo de que as funções pudessem alterar o conteúdo de variáveis de seus invocadores, aguarde!)

No trecho de programa abaixo x é uma variável do tipo inteiro e p é uma variável do tipo apontador para inteiro. Os operadores * e & são discutidos mais abaixo.

int x=3;
int *p; // (*p) é um inteiro e p é um endereço ou apontador para inteiro
p=&x;
printf(“
%d\n”, x);
*p=5;// a variável p é usada para modificar o conteúdo de x
printf(“%d\n”, x);

Uma das maneiras que podemos ver estas variáveis: x está no endereço 890ABCDE de memória, considere  que cada byte tenha um endereço e que a variável x ocupa 4 bytes, neste caso a variável x corresponde ao espaço de memória correspondente aos endereços [890ABCDE; 890ABCDF; 890ABCE0; 89ABCE1]. Podemos assumir que a variável p ocupa 4 bytes de memória (um espaço de endereçamento de 32 bits) e portanto ocupa, 4 endereços: [890ABCE2; 890ABCE3; 890ABCE4; 890ABCE5]. o conteúdo de x inicialmente é 3; o conteúdo de p é o endereço da variável x:

890ABCDE:03; 890ABCDF:00; 890ABCE0:00; 890ABCE1:00
890ABCE2:DE; 890ABCE3:BC; 890ABCE4:0A; 890ABCE5:89

Uma outra convenção de visualização para as variáveis x e p é dada pela figura abaixo:

Usaremos este tipo de convenção na disciplina. Nesta figura a memória é representada por uma “fita” (duas linhas paralelas horizontais) e as variáveis são regiões delimitadas com um nome. O conteúdo de x é 3 e o conteúdo de p é uma “seta” que representa o endereço de memória que p “aponta”. No caso acima p aponta para a região de memória correspondente à variável x. Infelizmente a maioria do stextos (e este texto aqui também!) não são precisos com relação ao uso dos termos. Algumas vezes o termo “ponteiro”se refere à variavel p (o endereço de memória onde está p e o conteúdo de p) outras vezes o termo “ponteiro” se refere apenas ao conteúdo da variável p.

O asterisco * é um operador unário e denota “indireção”. Quando o * é aplicado a uma expressão, se esta expressão obedece a certas regras e representa um endereço de memória então o resultado é o valor associado ao endereço correspondente a esta expressão.

O e-comercial & é um operador unário e denota “endereço de”. Quando o & é aplicado a uma expressão, se esta expressão obedece a certas regras e representa um endereço de memória então o resultado é o endereço de memória correspondente a esta expressão ( O operador & só pode ser aplicado a variáveis simples e elementos de arranjos que serão vistos abaixo).

As sentenças envolvendo variável, endereço de variável e valor de uma variável são potencialmente confusas porque uma variável do lado esquerdo de uma atribuição corresponde a um endereço, uma variável do lado direito de uma atribuição corresponde a um valor, e os operadores * e & trazem novos significados.

Podemos colocar mais de um nível de indireção:

    int x=3;
    int *p;
    int **q;
    p=&x;
    q=&p;
    printf("%d\n", x);
    **q=5;
    printf("%d\n", x);

Exercício: Faça o desenho das variáveis x, p e q correspondentes ao trecho acima.

Mas não é razoável ou usual colocar mais de um nível de operador “endereço de” Tente entender porque o programa abaixo está errado:

    int x=3;
    int *p;
    int **q;
    q=&&x;
    printf("%d\n", x);
    **q=5;
    printf("%d\n", x)
;


Se o compilador fosse “bastante inteligente”  talvez ele conseguisse descobrir onde está o endereço do endereço de x! O fato mais básico é que só podemos aplicar o operador & (endereço de) em certas expressões: por agora somente variáveis.

A linguagem C nos permite o seguinte tipo de declaração:

int d[9];

Neste caso d não é uma variável! d passa a denotar uma sequência de variáveis do tipo int. o número de variáveis corresponde ao valor da expressão entre colchetes. Uma declaração como acima tenta simular a seguinte declaração int d[0], d[1], ..., d[8]; onde cada d[i] é uma variável. As variáveis definidas utilizando arranjos têm índice sempre iniciando de 0 (zero). Os projetistas da linguagem C quiseram que o nome “d” funcionasse (sintaticamente) como um ponteiro/“pointer”. Mas lembre-se que d não é uma variável (mas pode haver confusão porque d denota o endereço inicial do arranjo que fica alocado sequencialmente em memória!!)!

Podemos trabalhar com cada uma das variáveis utilizando o operador “indexação” [] junto ao nome do arranjo.

O seguinte trecho de programa:

  int cpf=123456789;
  int d0, d1, d2, d3, d4, d5, d6, d7, d8;

  int i;
  d0=cpf%10;         d1=cpf/10%10;       d2=cpf/100%10;
  d3=cpf/1000%10;    d4=cpf/10000%10;    d5=cpf/100000%10;
  d6=cpf/1000000%10; d7=cpf/10000000%10; d8=cpf/100000000%10;

  printf(“%d\n%d\n%d\n%d\n%d\n%d\n%d\n%d\n%d\n”,d0,d1, d2, d3, d4, d5, d6, d7,d8);

pode ser colocado em correspondência com este trecho:

   int cpf=123456789;
   int d[9];
   int i;
   int aux=cpf;
   for(i=0; i<9; i++){ d[i]=aux%10; aux/=10;}
   for(i=0; i<9; i++) printf("%d\n", d[i]);

observe o uso do operador de indexação!

Na linguagem C o operador indexação [] é definido de forma a satisfazer exp1[exp2] é idêntico a *(exp1+exp2), como exemplo d[0] corresponde a *d. Ou seja a linguagem C define uma espécie de “aritmética de ponteiros” que tornou-se parte inseparável da linguagem. Portanto observe que a declaração:

int a[10];

define um arranjo (chamado genericamente de “arranjo a”) de tamanho 10. Consiste em um bloco de 10 variáveis consecutivas: a[0], a[1], ... a[9]. A notação a[i] refere-se ao i-ésimo elemento do arranjo. A declaração:

int *pa;

define uma  variável (de nome pa) do tipo “ponteiro para int”. A atribuição

pa=&a[0];

atribui para pa o endereço do primeiro elemento do arranjo a. A atribuição

*(pa+1)=3;

atribui a a[1] o valor 3! Ou seja a aritmética de ponteiros leva em conta o tamanho em bytes de um tipo na memória e faz as operações aritméticas de forma a preservar exp1[exp2]  equivale a *(exp1+exp2).

A linguagem C permite a definição de “constantes do tipo enumerado”:

enum boolean {FALSO, VERDADEIRO};

O primeiro nome em uma enumeração (enum) tem o valor 0, o seguinte 1 e assim sucessivamente. Com a definição acima temos o nome FALSO como um sinônimo para 0; podemos atribuir zero para uma variável desta forma: int j=FALSO; No entanto os valores podem ser explicitados:

enum meses {JAN=1, FEV, MAR,ABR, MAI, JUN, JUL, AGO, SET, OUT, NOV, DEZ}; /* FEV=2 etc*/

Nomes em diferentes enumerações devem ser distintos, valores nas enumerações não necessitam ser distintos. A linguagem C oferece ainda o pré-processador com a opção #define para definir contantes.

O espaço em memória de uma variável pode ser alocado dinamicamente através da função malloc(). Veja os exemplos abaixo:

int a; a=10;  equivale a int a; int *pa=&a; *pa=10; equivale a int *pa; pa=(int *)malloc(sizeof(int)); *pa=10;

O termo sizeof(int) corresponde a uma das formas do operador sizeof(<nome-de-tipo>) e devolve o tamanho em bytes de um certo tipo.

double a; a=10.;  equivale a double a; double *pa=&a; *pa=10.; equivale a
double *pa; pa=(double *)malloc(sizeof(double)); *pa=10.;

int a[]={10,20}; equivale a int *pa; pa=(int *)malloc(sizeof(int)*2); pa[0]=10; pa[1]=20;

Há uma forma mais legível da chamada malloc(sizeof(int)*2), trata-se da função calloc: calloc(2, sizeof(int)). Portanto:

int a[]={10,20}; equivale a int *pa; pa=(int *)calloc(2, sizeof(int)); pa[0]=10; pa[1]=20;