Programar em C/Mais sobre variáveis

Este módulo precisa ser revisado por alguém que conheça o assunto (discuta).

typedef

editar

A instrução typedef serve para definir um novo nome para um certo tipo de dados ― intrínseco da linguagem ou definido pelo usuário. Por exemplo, se fizéssemos a seguinte declaração:

typedef unsigned int uint;

poderíamos declarar variáveis inteiras sem sinal (unsigned int) da seguinte maneira:

uint numero;
// equivalente a "unsigned int numero;"

Como exemplo vamos dar o nome de inteiro para o tipo int:

typedef int inteiro;

Como se vê, typedef cria uma espécie de "apelido" para um tipo de dados, permitindo que esse tipo seja referenciado através desse apelido em vez de seu identificador normal.

Um dos usos mais comuns de typedef é abreviar a declaração de tipos complexos, como structs ou estruturas. Veja este exemplo:

struct pessoa {
   char nome[40];
   int idade;
};

struct pessoa joao;

Observe que, para declarar a variável joao, precisamos escrever a palavra struct. Podemos usar typedef para abreviar essa escrita:

typedef struct _pessoa {
   char nome[40];
   int idade;
} Pessoa;

Pessoa joao;

Um "apelido" de tipo é utilizado com bastante frequência, embora não costumemos dar por isso: é o tipo FILE, usado nas funções de entrada/saída de arquivos.

typedef struct _iobuf
{
   char* _ptr;
   int	  _cnt;
   char* _base;
   int   _flag;
   int   _file;
   int   _charbuf;
   int   _bufsiz;
   char* _tmpfname;
} FILE;

Então, quando declaramos algo como

FILE *fp;

na verdade estamos a declarar um ponteiro para uma estrutura, que será preenchida mais tarde pela função fopen.

Atenção! Você não deve tentar manipular uma estrutura do tipo FILE; sua composição foi apresentada apenas como exemplo ou ilustração.

sizeof

editar

O operador sizeof é usado para se saber o tamanho de variáveis ou de tipos. Ele retorna o tamanho do tipo ou variável em bytes como uma constante. Devemos usá-lo para garantir portabilidade. Por exemplo, o tamanho de um inteiro pode depender do sistema para o qual se está compilando. O sizeof é um operador porque ele é substituído pelo tamanho do tipo ou variável no momento da compilação. Ele não é uma função. O sizeof admite duas formas:

sizeof nome_da_variável
sizeof (nome_do_tipo)

Se quisermos então saber o tamanho de um float fazemos sizeof(float). Se declararmos a variável f como float e quisermos saber o seu tamanho faremos sizeof f. O operador sizeof também funciona com estruturas, uniões e enumerações.

Outra aplicação importante do operador sizeof é para se saber o tamanho de tipos definidos pelo usuário. Seria, por exemplo, uma tarefa um tanto complicada a de alocar a memória para um ponteiro para a estrutura ficha_pessoal, criada na primeira página desta aula, se não fosse o uso de sizeof. Veja o exemplo:

typedef struct {
   const char *nome;
   const char *sobrenome;
   int idade;
} Pessoa;


int main(void)
{
   Pessoa *joaquim;
   joaquim = malloc(sizeof(Pessoa));
   joaquim->nome = "Joaquim";
   joaquim->sobrenome = "Silva";
   joaquim->idade = 15;
}

Outro exemplo:

#include <string.h>
#include <stdio.h>
int
main(void)
{
   char *nome;
   nome = malloc(sizeof(char) * 10);
   sprintf(nome, "wikibooks");
   printf("Site: http://pt.%s.org/", nome);
   /*
      Imprime:
        Site: http://pt.wikibooks.org/
   */
}

A sentença abaixo NÃO funciona, pois sizeof é substituído pelo tamanho de um tipo em tempo de compilação.

const char *FRASE;
FRASE = "Wikibooks eh legal";
printf("Eu acho que o tamanho da string FRASE é %d", sizeof(FRASE));

Conversão de tipos

editar

As atribuições no C tem o seguinte formato:

destino=origem;

Se o destino e a origem são de tipos diferentes o compilador faz uma conversão entre os tipos. Mas nem todas as conversões são possíveis. O primeiro ponto a ser ressaltado é que o valor de origem é convertido para o valor de destino antes de ser atribuído e não o contrário.

Em C, cada tipo básico ocupa uma determinada porção de bits na memória, logo, a conversão entre tipos nem sempre é algo nativo da linguagem, por assim dizer. Há funções como atol e atof que convertem string em inteiro longo (long int) e string em double, respectivamente. Mas em muitos casos é possível usar o casting.

É importante lembrar que quando convertemos um tipo numérico para outro, nós nunca ganhamos precisão. Nós podemos perder precisão ou no máximo manter a precisão anterior. Isto pode ser entendido de uma outra forma. Quando convertemos um número não estamos introduzindo no sistema nenhuma informação adicional. Isto implica que nunca vamos ganhar precisão.

Abaixo vemos uma tabela de conversões numéricas com perda de precisão, para um compilador com palavra de 16 bits:

De              Para	    Informação Perdida
unsigned char	char	    Valores maiores que 127 são alterados
short int	char	    Os 8 bits de mais alta ordem
int	        char	    Os 8 bits de mais alta ordem
long int	char	    Os 24 bits de mais alta ordem
long int	short int   Os 16 bits de mais alta ordem
long int	int	    Os 16 bits de mais alta ordem
float	        int	    Precisão - resultado arredondado
double	        float	    Precisão - resultado arredondado
long double	double	    Precisão - resultado arredondado

Casting: conversão manual

editar

Se declararmos a = 10/3, sabemos que o resultado é 3,333, ou seja a divisão de dois números inteiros dá um número real. Porém o resultado em C será o inteiro 3. Isso acontece, porque as constantes são do tipo inteiro e operações com inteiros tem resultado inteiro. O mesmo ocorreria em a = b/c se b e c forem inteiros.

Se declararmos:

int a;

O resultado será 3.

Mesmo que declarássemos:

float a;

o resultado continua a ser 3 mas desta vez, 3,0000.

Para fazer divisão que resulte número real, é necessário fazer cast para um tipo de ponto flutuante:

a = (float)10/3
a = 10/(float)3

Nesse caso, o 10 ou o 3 é convertido para float. O outro número continua como inteiro, mas ao entrar na divisão com um float, ele é convertido automaticamente para float. A divisão é feita e depois atribuída à variável a.

Em poucas palavras, casting é colocar um tipo entre parênteses antes da atribuição de uma variável. A forma geral para cast é:

(tipo)variável
(tipo)(expressão)
variavel_destino = (tipo)variavel_origem;

Mas existem umas conversões automáticas:

int f(void)
{
    float f_var;
    double d_var;
    long double l_d_var;
    f_var = 1; d_var = 1; l_d_var = 1;
    d_var = d_var + f_var;    /*o float é convertido em double*/
    l_d_var = d_var + f_var;    /*o float e o double convertidos em long double*/
    return l_d_var;
 }

Repare que a conversão é feita de menor para o maior.

É possível fazer a conversão ao contrário de um tipo com mais bits para um com menos bits e isso é truncar. Nesse caso, o cast explícito é necessário. Assim, um número float: 43.023 ao ser convertido para int deverá ser "cortado", ficando inteiro: 43. Se converter long para short, os bits mais significativos são perdidos na conversão.

O operador cast também e bastante utilizado para estruturar áreas de estoque temporários (buffer). A seguir um pequeno exemplo:

#include <stdio.h>
typedef struct estruturar{
   char a ;
   char b ; 
};
 
int main()
{
   char buffer[2] = {17, 4};
   estruturar *p;
   p = (struct estruturar*) &buffer;
   char* x = (char*)malloc(10);
 
   printf("a: %i b: %i", p->a,p->b);
   getchar();
   return 0;
}

Atributos das variáveis

editar

Estes modificadores, como o próprio nome indica, mudam a maneira com a qual a variável é acessada e modificada. Alguns dos exemplos usam conceitos que só serão abordados nas seções seguintes, então você pode deixar esta seção para depois se assim o desejar.

O modificador const faz com que a variável não possa ser modificada no programa. Como o nome já sugere é útil para se declarar constantes. Poderíamos ter, por exemplo:

const float PI = 3.1415;

Podemos ver pelo exemplo que as variáveis com o modificador const podem ser inicializadas. Mas PI não poderia ser alterado em qualquer outra parte do programa. Se o programador tentar modificar PI o compilador gerará um erro de compilação.

Outro uso de const, aliás muito comum que o outro, é evitar que um parâmetro de uma função seja alterado pela função. Isto é muito útil no caso de um ponteiro, pois o conteúdo de um ponteiro pode ser alterado por uma função. Para proteger o ponteiro contra alterações, basta declarar o parâmetro como const.

#include <stdio.h>

int sqr (const int *num);

int main(void)
{
   int a = 10;
   int b;
   b = sqr(&a);
}

int sqr (const int *num)
{
   return ((*num)*(*num));
}

No exemplo, num está protegido contra alterações. Isto quer dizer que, se tentássemos fazer

*num = 10;

dentro da função sqr(), o compilador daria uma mensagem de erro.

volatile

editar

O modificador volatile diz ao compilador que a variável em questão pode ser alterada sem que este seja avisado. Isto evita "bugs" que poderiam ocorrer se o compilador tentasse fazer uma otimização no código que não é segura quando a memória é modificada externamente.

Digamos que, por exemplo, tenhamos uma variável que o BIOS do computador altera de minuto em minuto (um relógio, por exemplo). Seria importante que declarássemos esta variável como volatile.

Um uso importante de variáveis volatile é em aplicações que acessam dados relativos ao hardware, por exemplo: enviam ou lêem dados de uma porta serial ou paralela. Não deve ser usado em aplicações com vários threads, para essas o padrão C estipula um tipo próprio (atomic).

extern

editar

O modificador extern diz ao compilador que a variável indicada foi declarada em outro arquivo que não podemos incluir diretamente, por exemplo o código de uma biblioteca padrão. Isso é importante pois, se não colocarmos o modificador extern, o compilador irá declarar uma nova variável com o nome especificado, "ocultando" a variável que realmente desejamos usar. E se simplesmente não declarássemos a variável, já sabemos que o compilador não saberia o tamanho da variável.

Quando o compilador encontra o modificador extern, ele marca a variável como não resolvida, e o montador se encarregará de substituir o endereço correto da variável.

extern float sum;
extern int count;

float returnSum (void)
{
   count++;
   return sum;
}

Neste exemplo, o compilador irá saber que count e sum estão sendo usados no arquivo mas que foram declarados em outro.

Uma variável externa frequentemente usada é a variável errno (declarada no arquivo-cabeçalho errno.h), que indica o último código de erro encontrado na execução de uma função da biblioteca padrão ou do sistema.

static

editar

O funcionamento das variáveis declaradas como static depende de se estas são globais ou locais.

  • Variáveis globais static funcionam como variáveis globais dentro de um módulo, ou seja, são variáveis globais que não são (e nem podem ser) conhecidas em outros módulos (arquivos). Isto é util se quisermos isolar pedaços de um programa para evitar mudanças acidentais em variáveis globais. Isso é um tipo de encapsulamento — que é, simplificadamente, o ato de não permitir que uma variável seja modificada diretamente, mas apenas por meio de uma função.
  • Variáveis locais estáticas são variáveis cujo valor é mantido de uma chamada da função para a outra. Veja o exemplo:
int count (void)
{
   static int num = 0;
   num++;
   return num;
}

A função count() retorna o número de vezes que ela já foi chamada. Veja que a variável local int é inicializada. Esta inicialização só vale para a primeira vez que a função é chamada pois num deve manter o seu valor de uma chamada para a outra. O que a função faz é incrementar num a cada chamada e retornar o seu valor. A melhor maneira de se entender esta variável local static é implementando. Veja por si mesmo, executando seu próprio programa que use este conceito.

register

editar

O computador pode guardar dados na memória (RAM) e nos registradores internos do processador. As variáveis (assim como o programa como um todo) costumam ser armazenadas na memória. O modificador register diz ao compilador que a variável em questão deve ser, se possível, guardada em um registrador da CPU.

Vamos agora ressaltar vários pontos importantes:

  • Porque usar register? Variáveis nos registradores da CPU vão ser acessadas em um tempo muito menor pois os registradores são muito mais rápidos que a memória. No entanto, a maioria dos compiladores otimizantes atuais usa registradores da CPU para variáveis, então o uso de register é freqüentemente desnecessário.
  • Em que tipo de variável podemos usar o register? Antes da criação do padrão ANSI C, register aplicava-se apenas aos tipos int e char, mas o padrão atual permite o uso de register para qualquer um dos quatro tipos fundamentais. É claro que seqüências de caracteres, arrays e estruturas também não podem ser guardadas nos registradores da CPU por serem grandes demais.
  • register é um pedido que o programador faz ao compilador. Este não precisa ser atendido necessariamente, e alguns compiladores até ignoram o modificador register, o que é permitido pelo padrão C.
  • register não pode ser usado em variáveis globais, pois isto implicaria em um registrador da CPU ficar o tempo todo ocupado por essa variável.

Um exemplo do uso do register é dado a seguir:

int main (void)
{
   register int count;
   for (count = 0; count < 10; count++)
   {
      ...
   }
   return 0;
}

O loop acima, em compiladores que não guardam variáveis em registradores por padrão, deve ser executado mais rapidamente do que seria se não usássemos o register. Este é o uso mais recomendável para o register: uma variável que será usada muitas vezes em seguida.


 

Esta página é um esboço de informática. Ampliando-a você ajudará a melhorar o Wikilivros.