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;