Pular para o conteúdo principal

Capítulo . 9 (Tipos de Dados Avançados)

Modificadores de Acesso
Estes modificadores, como o próprio nome indica, mudam a maneira com a qual a
variável é acessada e modificada.
const
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.141; 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.
O uso mais importante de const não é declarar variáveis constantes no programa. Seu uso mais comum é 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 tanto, basta declarar o parâmetro como const. Veja o exemplo:
#include
int sqr (const int *num);
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
O modificador volatile diz ao compilador que a variável em questão pode ser alterada sem que este seja avisado. Isto evita "bugs" seríssimos. Digamos que, por exemplo, tenhamos uma variável que o BIOS do computador altera de minuto em minuto (um relógio por exemplo). Seria muito bom que declarássemos esta variável como sendo volatile.
Especificadores de Classe de Armazenamento
87
Estes modificadores de tipo atuam sobre a maneira com a qual o compilador vai
armazenar a variável.
auto
O especificador de classe de armazenamento auto define variáveis automáticas, isto é, variáveis locais. Raramente usado pois todas as variáveis locais do C são auto por definição.
extern
O extern define variáveis que serão usadas em um arquivo apesar de terem sido
declaradas em outro. Ao contrário dos programas até aqui vistos, podemos ter programas de vários milhares de linhas. Estes podem ser divididos em vários arquivos (módulos) que serão compilados separadamente. Digamos que para um programa grande tenhamos duas variáveis globais: um inteiro count e um float sum. Estas variáveis são declaradas normalmente em um dos módulos do programa. Por exemplo:
int count;
float sum;
main (void)
{
...
return 0;
}
Num outro módulo do programa temos uma rotina que deve usar as variáveis globais acima. Digamos que a rotina que queremos se chama RetornaCount() e retorna o valor atual de count. O problema é que este módulo será compilado em separado e não tomará conhecimento dos outros módulos. O que fazer? Será que funcionaria se fizermos assim:
int count; /* errado */
float sum;
int RetornaCount (void)
{
return count;
}
Não. O módulo compilaria sem problema, mas, na hora que fizermos a linkagem (união dos módulos já compilados para gerar o executável) vamos nos deparar com uma mensagem de erro dizendo que as variáveis globais count e sum foram declaradas mais de uma vez. A maneira correta de se escrever o módulo com a função RetornaCount() é: extern int count; /* certo */ extern float sum;
int RetornaCount (void)
{
return count;
}
Assim, o compilador irá saber que count e sum estão sendo usados no bloco mas que foram declarados em outro.
static
O funcionamento das variáveis declaradas como static depende 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 modulos. Isto é util se quisermos isolar pedaços de um programa para evitar mudanças acidentais em variáveis globais.
Variáveis locais static 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
O computador tem a memória principal e os registradores da CPU. As variáveis (assim como o programa como um todo) são armazenados na memória. O modificador register diz ao compilador que a variável em questão deve ser, se possível, usada em um registrador da CPU.
Vamos agora ressaltar vários pontos importantes. Em primeiro lugar, porque usar o
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. Em segundo lugar, em que tipo de variável usar o register? O register não pode ser usado em variáveis globais. Isto implicaria que um registrador da CPU ficaria o tempo todo ocupado por conta de uma variável.
Os tipos de dados onde é mais aconselhado o uso do register são os tipos char e int, mas pode-se usá-lo em qualquer tipo de dado. Em terceiro lugar, o register é um pedido que o programador faz ao compilador. Este não precisa ser atendido necessariamente. Um exemplo do uso do register é dado:
main (void)
{
register int count;
for (count=0;count<10;count++)
{
...
}
return 0;
}
O loop for acima 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.
Auto-Avaliação
Veja como você está:
89
Considerando o conceito e finalidade dos modificadores de tipo, relacione as afirmativas com as palavras reservadas correspondentes (todas as afirmativas devem ser preenchidas com o número relacionado ao modificador correspondente, e existe pelo menos uma afirmativa para cada modificador):
(1)const (3)extern (5)register (7)void
(2)volatile (4)static (6)auto
( ) informa ao compilador que o valor da variável não pode ser alterado por nenhum comando do programa, mas que pode ser inicializado.
( ) informa ao compilador que nenhum valor será devolvido pela função
( ) informa ao compilador que a variável pode ser modificada por algum evento que não está sob o controle do programa
( ) avisa ao compilador que as variáveis que o seguem já foram declaradas em outro lugar.
( ) torna a variável permanente, mantendo seu valor entre chamadas
( ) útil ao escrever funções generalizadas e funções de biblioteca que podem ser usadas por outros programadores, pois permite esconder porções do programa de outras partes do código, evitando assim o uso de variável global
( ) quando apontadores forem passados para a função, garante que nenhum código na função poderá modificar os objetos apontados
( ) armazena o valor da variável em um registrador da CPU, acelerando operações
( ) usada para declarar variáveis locais automáticas, mas muito pouco usada por já ser o padrão (default)
( ) avisa ao compilador que a variável em questão sera largamente usada e deve permanecer acessível da forma mais eficiente possível
( ) permite ao compilador conhecer a variável sem criar armazenamento para ela novamente em outro modulo
Conversão de Tipos
Em atribuições no C temos o seguinte formato:
destino=orígem;
Se o destino e a orígem são de tipos diferentes o compilador faz uma conversão entre os tipos. 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. É 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
90
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
Modificadores de Funções
A forma geral de uma função é, como já foi visto,
tipo_de_retorno nome_da_função (declaração_de_parâmetros)
{
corpo_da_função
}
Uma função pode aceitar um modificador de tipo. Este vai modificar o modo como a
função opera na passagem de parâmetros. A forma geral da função ficaria então:
modificador_de_tipo tipo_de_retorno nome_da_função (declaração_de_parâmetros)
{
corpo_da_função
}
O nosso curso não aborda detalhes do funcionamento interno de funções. Para saber mais, consulte o manual do seu compilador ou algum livro especializado.
Pascal
Faz com que a função use a convenção de funções da linguagem de programação Pascal. Isto faz com que as funções sejam compatíveis com programas em Pascal.
Cdecl O modificador de tipo cdecl faz com que a função use a convenção para funções do C.
Raramente é usado pois é o default. Pode-se pensar no cdecl como sendo o "inverso" do pascal.
interrupt
Diz ao compilador que a função em questão será usada como um manipulador de
interrupções. Isto faz com que o compilador preserve os registradores da CPU antes e depois da chamada à função. Mais uma vez este tópico está fora do escopo do curso.
Ponteiros para Funções
O C permite que acessemos variáveis e funções através de ponteiros! Podemos então fazer coisas como, por exemplo, passar uma função como argumento para outra função. Um ponteiro para uma função tem a seguinte declaração:
tipo_de_retorno (*nome_do_ponteiro)();
ou
tipo_de_retorno (*nome_do_ponteiro)(declaração_de_parâmetros);
Repare nos parênteses que devem ser colocados obrigatoriamente. Se declaramos:
tipo_de_retorno * nome(declaração_de_parâmetros);
Estaríamos, na realidade, declarando uma função que retornaria um ponteiro para o tipo especificado. Porém, não é obrigatório se declarar os parâmetros da função. Veja um exemplo do uso de ponteiros para funções:
#include
#include
void PrintString (char *str, int (*func)(const char *));
main (void)
{
char String [20]="Curso de C.";
int (*p)(const char *); /* Declaracao do ponteiro para função
Funcao apontada e’ inteira e recebe como parametro
uma string constante */
p=puts; /* O ponteiro p passa a apontar para a função puts
que tem o seguinte prototipo: int puts(const char *) */
PrintString (String, p); /* O ponteiro é passado como parametro para PrintString
*/
return 0;
}
void PrintString (char *str, int (*func)(const char *))
{
(*func)(str); /* chamada a função através do ponteiro para função */
func(str); /* maneira também válida de se fazer a chamada a função puts
através do ponteiro para função func */
}
Veja que fizemos a atribuição de puts a p simplesmente usando:
p = puts;
Disto, concluímos que o nome de uma função (sem os parênteses) é, na realidade, o endereço daquela função! Note, também, as duas formas alternativas de se chamar uma função através de um ponteiro. No programa acima, fizemos esta chamada por:
(*func)(str);
e
func(str);
Estas formas são equivalentes entre si. Além disto, no programa, a função PrintString() usa uma função qualquer func para imprimir a string na tela. O programador pode então fornecer não só a string mas também a função que será usada para imprimí-la. No main() vemos como podemos atribuir, ao ponteiro para funções p, o endereço da função puts() do C.
Em síntese, ao declarar um ponteiro para função, podemos atribuir a este ponteiro o endereço de uma função e podemos também chamar a função apontada através dele. Não podemos fazer algumas coisas que fazíamos com ponteiros "normais", como, por exemplo, incrementar ou decrementar um ponteiro para função.
Alocação Dinâmica
A alocação dinâmica permite ao programador alocar memória para variáveis quando o programa está sendo executado. Assim, poderemos definir, por exemplo, um vetor ou uma matriz cujo tamanho descobriremos em tempo de execução. O padrão C ANSI define apenas funções para o sistema de alocação dinâmica, disponíveis na biblioteca stdlib.h:
No entanto, existem diversas outras funções que são amplamente utilizadas, mas
dependentes do ambiente e compilador. Neste curso serão abordadas somente estas funções padronizadas.
malloc
A função malloc() serve para alocar memória e tem o seguinte protótipo:
void *malloc (unsigned int num);
A funçao toma o número de bytes que queremos alocar (num), aloca na memória e
retorna um ponteiro void * para o primeiro byte alocado. O ponteiro void * pode ser atribuído a qualquer tipo de ponteiro. Se não houver memória suficiente para alocar a memória requisitada a função malloc() retorna um ponteiro nulo. Veja um exemplo de alocação
dinâmica com malloc():
#include
#include /* Para usar malloc() */
main (void)
{
int *p;
int a;
int i;
... /* Determina o valor de a em algum lugar */
p=(int *)malloc(a*sizeof(int)); /* Aloca a números inteiros
p pode agora ser tratado como um
vetor com
a posicoes */
if (!p)
{
printf ("** Erro: Memoria Insuficiente **");
exit;
}
for (i=0; iposicoes */
p[i] = i*i;
...
return 0;
}
No exemplo acima, é alocada memória suficiente para se armazenar a números inteiros.
O operador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber o tamanho de tipos. O ponteiro void* que malloc() retorna é convertido para um int* pelo cast e é atribuído a p. A declaração seguinte testa se a operação foi bem sucedida. Se não tiver sido, p terá um valor nulo, o que fará com que !p retorne verdadeiro. Se a operação tiver sido bem sucedida, podemos usar o vetor de inteiros alocados normalmente, por exemplo, indexando-o de p[0] a p[(a-1)].
calloc
A função calloc() também serve para alocar memória, mas possui um protótipo um
pouco diferente: void *calloc (unsigned int num, unsigned int size); A funçao aloca uma quantidade de memória igual a num * size, isto é, aloca memória suficiente para um vetor de num objetos de tamanho size. Retorna um ponteiro void * para o primeiro byte alocado. O ponteiro void * pode ser atribuído a qualquer tipo de ponteiro. Se não houver memória suficiente para alocar a memória requisitada a função calloc() retorna um ponteiro nulo. Veja um exemplo de alocação dinâmica com calloc():
#include
#include /* Para usar calloc() */
main (void)
{
int *p;
int a;
int i;
... /* Determina o valor de a em algum lugar */
p=(int *)calloc(a,sizeof(int)); /* Aloca a números inteiros
p pode agora ser tratado como um
vetor com
a posicoes */
if (!p)
{
printf ("** Erro: Memoria Insuficiente **");
exit;
}
for (i=0; i
posicoes */
p[i] = i*i;
...
return 0;
}
No exemplo acima, é alocada memória suficiente para se colocar a números inteiros. O operador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber o tamanho de tipos. O ponteiro void * que calloc() retorna é convertido para um int * pelo cast e é atribuído a p. A declaração seguinte testa se a operação foi bem sucedida. Se não tiver sido, p terá um valor nulo, o que fará com que !p retorne verdadeiro. Se a operação tiver sido bem sucedida, podemos usar o vetor de inteiros alocados normalmente, por exemplo, indexando-o de p[0] a p[(a-1)].
realloc
A função realloc() serve para realocar memória e tem o seguinte protótipo:
void *realloc (void *ptr, unsigned int num);
A funçao modifica o tamanho da memória previamente alocada apontada por *ptr para aquele especificado por num. O valor de num pode ser maior ou menor que o original. Um ponteiro para o bloco é devolvido porque realloc() pode precisar mover o bloco para aumentar seu tamanho. Se isso ocorrer, o conteúdo do bloco antigo é copiado no novo bloco, e nenhuma informação é perdida. Se ptr for nulo, aloca size bytes e devolve um ponteiro; se size é zero, a memória apontada por ptr é liberada. Se não houver memória suficiente para a alocação, um ponteiro nulo é devolvido e o bloco original é deixado inalterado.
#include
#include /* Para usar malloc() e realloc*/
main (void)
{
int *p;
int a;
int i;
... /* Determina o valor de a em algum lugar */
a = 30;
p=(int *)malloc(a*sizeof(int)); /* Aloca a números inteiros
p pode agora ser tratado como um
vetor com
a posicoes */
if (!p)
{
printf ("** Erro: Memoria Insuficiente **");
exit;
}
for (i=0; i
posicoes */
p[i] = i*i;
/* O tamanho de p deve ser modificado, por algum motivo ... */
a = 100;
p = realloc (p, a*sizeof(int));
for (i=0; i
posicoes */
p[i] = a*i*(i-6);
...
95
return 0;
}
free
Quando alocamos memória dinamicamente é necessário que nós a liberemos quando ela não for mais necessária. Para isto existe a função free() cujo protótipo é:
void free (void *p); Basta então passar para free() o ponteiro que aponta para o início da memória alocada. Mas você pode se perguntar: como é que o programa vai saber quantos bytes devem ser liberados? Ele sabe pois quando você alocou a memória, ele guardou o número de bytes alocados numa "tabela de alocação" interna. Vamos reescrever o exemplo usado para a função malloc() usando o free() também agora:
#include
#include /* Para usar malloc e free */
main (void)
{
int *p;
int a;
...
p=(int *)malloc(a*sizeof(int));
if (!p)
{
printf ("** Erro: Memoria Insuficiente **");
exit;
}
...
free(p);
...
return 0;
}
AUTO AVALIAÇÃO
Veja como você está. Refaça os exemplos desta página, mas ao invés de trabalhar com um vetor de inteiros, use um vetor de strings (ou uma matriz de char, como você preferir). Faça leituras e apresente os resultados na tela.
96
Alocação Dinâmica de Vetores e Matrizes
Alocação Dinâmica de Vetores
A alocação dinâmica de vetores utiliza os conceitos aprendidos na aula sobre ponteiros e as funções de alocação dinâmica apresentados. Um exemplo de implementação para vetor real é fornecido a seguir:
#include
#include
float *Alocar_vetor_real (int n)
{
float *v; /* ponteiro para o vetor */
if (n < 1) { /* verifica parametros recebidos */
printf ("** Erro: Parametro invalido **\n");
return (NULL);
}
/* aloca o vetor */
v = (float *) calloc (n, sizeof(float));
if (v == NULL) {
printf ("** Erro: Memoria Insuficiente **");
return (NULL);
}
return (v); /* retorna o ponteiro para o vetor */
}
float *Liberar_vetor_real (float *v)
{
if (v == NULL) return (NULL);
free(v); /* libera o vetor */
return (NULL); /* retorna o ponteiro */
}
void main (void)
{
float *p;
int a;
... /* outros comandos, inclusive a inicializacao de a */
p = Alocar_vetor_real (a);
... /* outros comandos, utilizando p[] normalmente */
p = Liberar_vetor_real (p);
}
Alocação Dinâmica de Matrizes
A alocação dinâmica de memória para matrizes é realizada da mesma forma que para vetores, com a diferença que teremos um ponteiro apontando para outro ponteiro que aponta para o valor final, ou seja é um ponteiro para ponteiro, o que é denominado indireção múltipla. A indireção múltipla pode ser levada a qualquer dimensão desejada, mas raramente é necessário mais de um ponteiro para um ponteiro. Um exemplo de implementação para matriz real bidimensional é fornecido a seguir. A estrutura de dados utilizada neste exemplo é composta por um vetor de ponteiros (correspondendo ao primeiro índice da matriz), sendo que cada ponteiro aponta para o início de uma linha da matriz. Em cada linha existe um vetor alocado dinamicamente, como descrito anteriormente (compondo o segundo índice da matriz).
97
#include
#include
float **Alocar_matriz_real (int m, int n)
{
float **v; /* ponteiro para a matriz */
int i; /* variavel auxiliar */
if (m < 1 || n < 1) { /* verifica parametros recebidos */
printf ("** Erro: Parametro invalido **\n");
return (NULL);
}
/* aloca as linhas da matriz */
v = (float **) calloc (m, sizeof(float *)); / Um vetor de m ponteiros para float */
if (v == NULL) {
printf ("** Erro: Memoria Insuficiente **");
return (NULL);
}
/* aloca as colunas da matriz */
for ( i = 0; i < m; i++ ) {
v[i] = (float*) calloc (n, sizeof(float)); /* m vetores de n floats */
if (v[i] == NULL) {
printf ("** Erro: Memoria Insuficiente **");
return (NULL);
}
}
return (v); /* retorna o ponteiro para a matriz */
}
float **Liberar_matriz_real (int m, int n, float **v)
{
int i; /* variavel auxiliar */
if (v == NULL) return (NULL);
if (m < 1 || n < 1) { /* verifica parametros recebidos */
printf ("** Erro: Parametro invalido **\n");
return (v);
}
for (i=0; ifree (v); /* libera a matriz (vetor de ponteiros) */
return (NULL); /* retorna um ponteiro nulo */
}
void main (void)
{
float **mat; /* matriz a ser alocada */
int l, c; /* numero de linhas e colunas da matriz */
int i, j;
... /* outros comandos, inclusive inicializacao para l e c */
mat = Alocar_matriz_real (l, c);
for (i = 0; i < l; i++)
for ( j = 0; j < c; j++)
mat[i][j] = i+j;
98
... /* outros comandos utilizando mat[][] normalmente */
mat = Liberar_matriz_real (l, c, mat);
...
}
AUTO AVALIAÇÃO
Veja como você está. Faca um programa que multiplique duas matrizes. O programa devera’ estar estruturado de maneira que:
1- o usuario forneca as dimensoes das matrizes (teste se as dimensoes sao compativeis, isto
e’, se as matrizes podem ser multiplicadas);
2- as matrizes sejam alocadas dinamicamente (voce pode usar a funcao vista nesta pagina
para isto);
3- as matrizes sejam lidas pelo teclado (faca uma funcao para leitura das matrizes);
4- as matrizes sejam, entao, multiplicadas (faca uma funcao para a multiplicacao);
5- a matriz resultante seja apresentada em tela (faca uma funcao para apresentar a matriz na tela).
OBS:
a) Faca, tambem, alocacao dinamica da matriz resultante.
b) Caso alguém não conheça o procedimento para a multiplicação de matrizes, segue aqui alguma orientação. Suponha as matrizes A(mXn)
| a11 a12 ... a1n |
A = | a21 a22 ... a2n |
| : |
| am1 am2 ... amn |
e B(nXt)
| b11 b12 ... b1t |
B = | b21 b22 ... b2t |
| : |
| bn1 bn2 ... bnt |
O elemento ij da matriz C é resultante da multiplicação da linha i de A pela coluna j de B. Portanto, a matriz C (mXt) = A*B será da seguinte forma:
C =
| a11*b11 +a12*b21 + ... +a1n*bn1 a11*b12 +a12*b22 + ... + a1n*bn2 ... a11+b1t
+a12*b2t + ... + a1n*bnt |
| a21*b11 +a22*b21 + ... +a2n*bn1 a21*b12 +a22*b22 + ... + a2n*bn2 ... a21+b1t
+a22*b2t + ... + a2n*bnt |
| ... ...
... ... |
| am1*b11 +am2*b21 +...+amn*bn1 am1*b12 +am2*b22 +...+ amn*bn2 ... am1+b1t
+am2*b2t +...+amn*bnt |
99
Capítulo . 10 (Tipos de Dados Definidos Pelo Usuário)
Estruturas - Primeira parte
Uma estrutura agrupa várias variáveis numa só. Funciona como uma ficha pessoal que tenha nome, telefone e endereço. A ficha seria uma estrutura. A estrutura, então, serve para agrupar um conjunto de dados não similares, formando um novo tipo de dados.
Criando
Para se criar uma estrutura usa-se o comando struct. Sua forma geral é:
struct nome_do_tipo_da_estrutura
{
tipo_1 nome_1;
tipo_2 nome_2;
...
tipo_n nome_n;
} variáveis_estrutura;
O nome_do_tipo_da_estrutura é o nome para a estrutura. As variáveis_estrutura são
opcionais e seriam nomes de variáveis que o usuário já estaria declarando e que seriam do tipo
nome_do_tipo_da_estrutura. Um primeiro exemplo:
struct est{
int i;
float f;
} a, b;
Neste caso, est é uma estrutura com dois campos, i e f. Foram também declaradas duas variáveis, a e b que são do tipo da estrutura, isto é, a possui os campos i e f, o mesmo acontecendo com b. Vamos criar uma estrutura de endereço:
struct tipo_endereco
{
char rua [50];
int numero;
char bairro [20];
char cidade [30];
char sigla_estado [3];
long int CEP;
};
100
Vamos agora criar uma estrutura chamada ficha_pessoal com os dados pessoais de uma pessoa: struct ficha_pessoal
{
char nome [50];
long int telefone;
struct tipo_endereco endereco;
};
Vemos, pelos exemplos acima, que uma estrutura pode fazer parte de outra ( a struct tipo_endereco é usada pela struct ficha_pessoal).
Usando
Vamos agora utilizar as estruturas declaradas na seção anterior para escrever um
programa que preencha uma ficha.
#include
#include
struct tipo_endereco
{
char rua [50];
int numero;
char bairro [20];
char cidade [30];
char sigla_estado [3];
long int CEP;
};
struct ficha_pessoal
{
char nome [50];
long int telefone;
struct tipo_endereco endereco;
};
main (void)
{
struct ficha_pessoal ficha;
strcpy (ficha.nome,"Luiz Osvaldo Silva");
ficha.telefone=4921234;
strcpy (ficha.endereco.rua,"Rua das Flores");
ficha.endereco.numero=10;
strcpy (ficha.endereco.bairro,"Cidade Velha");
strcpy (ficha.endereco.cidade,"Belo Horizonte");
strcpy (ficha.endereco.sigla_estado,"MG");
ficha.endereco.CEP=31340230;
return 0;
}
O programa declara uma variável ficha do tipo ficha_pessoal e preenche os seus dados. O exemplo mostra como podemos acessar um elemento de uma estrutura: basta usar o ponto (.). Assim, para acessar o campo telefone de ficha, escrevemos:
ficha.telefone = 4921234;
101
Como a struct ficha pessoal possui um campo, endereco, que também é uma struct, podemos fazer acesso aos campos desta struct interna da seguinte maneira:
ficha.endereco.numero = 10;
ficha.endereco.CEP=31340230;
Desta forma, estamos acessando, primeiramente, o campo endereco da struct ficha e, dentro deste campo, estamos acessando o campo numero e o campo CEP.
Matrizes de estruturas
Um estrutura é como qualquer outro tipo de dado no C. Podemos, portanto, criar
matrizes de estruturas. Vamos ver como ficaria a declaração de um vetor de 100 fichas pessoais: struct ficha_pessoal fichas [100];
Poderíamos então acessar a segunda letra da sigla de estado da décima terceira ficha fazendo: fichas[12].endereco.sigla_estado[1];
Analise atentamente como isto está sendo feito ...
AUTO AVALIAÇÃO
Veja como você está. Escreva um programa fazendo o uso de struct’s. Você deverá criar uma struct chamada Ponto, contendo apenas a posição x e y (inteiros) do ponto. Declare 2 pontos, leia a posição (coordenadas x e y) de cada um e calcule a distância entre eles. Apresente no final a distância entre os dois pontos.
Estruturas - Segunda parte
Atribuindo
Podemos atribuir duas estruturas que sejam do mesmo tipo. O C irá, neste caso, copiar uma estrutura, campo por campo, na outra. Veja o programa abaixo:
struct est1 {
int i;
float f;
};
void main()
{
struct est1 primeira, segunda; /* Declara primeira e segunda como structs do tipo
est1 */
primeira.i = 10;
primeira.f = 3.1415;
segunda = primeira; /* A segunda struct e’ agora igual a primeira */
printf(" Os valores armazenasdos na segunda struct sao : %d e %f ", segunda.i ,
segunda.f);
}
São declaradas duas estruturas do tipo est1, uma chamada primeira e outra chamada
segunda. Atribuem-se valores aos dois campos da struct primeira. Os valores de primeira são copiados em segunda apenas com a expressão de atribuição:
102
segunda = primeira;
Todos os campos de primeira serão copiados na segunda. Note que isto é diferente do que acontecia em vetores, onde, para fazer a cópia dos elementos de um vetor em outro, tínhamos que copiar elemento por elemento do vetor. Nas structs é muito mais fácil!
Porém, devemos tomar cuidado na atribuição de structs que contenham campos
ponteiros. Veja abaixo:
#include
#include
#include
struct tipo_end
{
char *rua; /* A struct possui um campo que é um ponteiro */
int numero;
};
void main()
{
struct tipo_end end1, end2;
char buffer[50];
printf("\nEntre o nome da rua:");
gets(buffer); /* Le o nome da rua em uma string de buffer */
end1.rua = (char *) malloc((strlen(buffer)+1)*sizeof(char)); /* Aloca a quantidade de
memoria
suficiente para armazenar a
string */
strcpy(end1.rua, buffer); /* Copia a string */
printf("\nEntre o numero:");
scanf("%d", &end1.numero);
end2 = end1; /* ERRADO end2.rua e end1.rua estao apontando para a mesma
regiao de memoria */
printf("Depois da atribuicao:\n Endereco em end1 %s %d \n Endereco em end2 %s
%d", end1.rua,end1.numero,end2.rua, end2.numero);
strcpy(end2.rua, "Rua Mesquita"); /* Uma modificacao na memoria
apontada por end2.rua causara’ a modificacao do
que e’ apontado por end1.rua, o que,
esta’ errado !!! */
end2.numero = 1100; /* Nesta atribuicao nao ha
problemas */
printf(" \n\nApos modificar o endereco em end2:\n Endereco em end1 %s %d \n
Endereco em end2 %s %d", end1.rua, end1.numero, end2.rua, end2.numero);
}
Neste programa há um erro grave, pois ao se fazer a atribuição end2 = end1, o campo rua de end2 estará apontando para a mesma posição de memória que o campo rua de end1.
Assim, ao se modificar o conteúdo apontado por end2.rua estaremos também modificando o conteúdo apontado por end1.rua !!!
Passando para funções No exemplo apresentado no ítem usando, vimos o seguinte comando: strcpy (ficha.nome,"Luiz Osvaldo Silva");
Neste comando um elemento de uma estrutura é passado para uma função. Este tipo de operação pode ser feita sem maiores considerações.
Podemos também passar para uma função uma estrutura inteira. Veja a seguinte função:
void PreencheFicha (struct ficha_pessoal ficha)
{
...
}
Como vemos acima é fácil passar a estrutura como um todo para a função. Devemos observar que, como em qualquer outra função no C, a passagem da estrutura é feita por valor. A estrutura que está sendo passada, vai ser copiada, campo por campo, em uma variável local da função PreencheFicha. Isto significa que alterações na estrutura dentro da função não terão efeito na variável fora da função. Mais uma vez podemos contornar este pormenor usando ponteiros e passando para a função um ponteiro para a estrutura.
Ponteiros
Podemos ter um ponteiro para uma estrutura. Vamos ver como poderia ser declarado um
ponteiro para as estruturas de ficha que estamos usando nestas seções:
struct ficha_pessoal *p;
Os ponteiros para uma estrutura funcionam como os ponteiros para qualquer outro tipo de dados no C. Para usá-lo, haveria duas possibilidades. A primeira é apontá-lo para uma variável struct já existente, da seguinte maneira:
struct ficha_pessoal ficha;
struct ficha_pessoal *p;
p = &ficha;
A segunda é alocando memória para ficha_pessoal usando, por exemplo, malloc():
#include
main()
{
struct ficha_pessoal *p;
int a = 10; /* Faremos a alocacao dinamica de 10 fichas pessoais */
p = (struct ficha_pessoal *) malloc (a * sizeof(struct ficha_pessoal));
p[0].telefone = 3443768; /* Exemplo de acesso ao campo telefone da primeira
ficha apontada por p */
free(p);
}
Há mais um detalhe a ser considerado. Se apontarmos o ponteiro p para uma estrutura
qualquer (como fizemos em p = &ficha; ) e quisermos acessar um elemento da estrutura
poderíamos fazer:
104
(*p).nome
Os parênteses são necessários, porque o operador . tem precedência maior que o
operador * . Porém, este formato não é muito usado. O que é comum de se fazer é acessar o elemento nome através do operador seta, que é formado por um sinal de "menos" (-) seguido por um sinal de "maior que" (>), isto é: -> . Assim faremos:
p->nome
A declaração acima é muito mais fácil e concisa. Para acessarmos o elemento CEP dentro de endereco faríamos:
p->endereco.CEP
Fácil, não?
AUTO AVALIAÇÃO
Seja a seguinte struct que é utilizada para descrever os produtos que estão no estoque de uma loja :
struct Produto {
char nome[30]; /* Nome do produto */
int codigo; /* Codigo do produto */
double preco; /* Preco do produto */
};
a) Escreva uma instrução que declare uma matriz de Produto com 10 itens de produtos;
b) Atribua os valores "Pe de Moleque", 13205 e R$0,20 aos membros da posição 0 e os valores
"Cocada Baiana", 15202 e R$0,50 aos membros da posição 1 da matriz anterior;
c) Faça as mudanças que forem necessárias para usar um ponteiro para Produto ao invés de
uma matriz de Produtos. Faça a alocação de memória de forma que se possa armazenar 10
produtos na área de memória apontada por este ponteiro e refaça as atribuições da letra b;
d) Escreva as instruções para imprimir os campos que foram atribuídos na letra c.
Declaração Union
Uma declaração union determina uma única localização de memória onde podem estar armazenadas várias variáveis diferentes. A declaração de uma união é semelhante à declaração de uma estrutura:
union nome_do_tipo_da_union
{
tipo_1 nome_1;
tipo_2 nome_2;
...
tipo_n nome_n;
} variáveis_union;
Como exemplo, vamos considerar a seguinte união:
union angulo
{
float graus;
float radianos;
};
105
Nela, temos duas variáveis (graus e radianos) que, apesar de terem nomes diferentes, ocupam o mesmo local da memória. Isto quer dizer que só gastamos o espaço equivalente a um único float. Uniões podem ser feitas também com variáveis de diferentes tipos. Neste caso, a memória alocada corresponde ao tamanho da maior variável no union. Veja o exemplo:
#include
#define GRAUS ’G’
#define RAD ’R’
union angulo
{
int graus;
float radianos;
};
void main()
{
union angulo ang;
char op;
printf("\nNumeros em graus ou radianos? (G/R):");
scanf("%c",&op);
if (op == GRAUS)
{
ang.graus = 180;
printf("\nAngulo: %d\n",ang.graus);
}
else if (op == RAD)
{
ang.radianos = 3.1415;
printf("\nAngulo: %f\n",ang.radianos);
}
else printf("\nEntrada invalida!!\n");
}
Temos que tomar o maior cuidado pois poderíamos fazer:
#include
union numero
{
char Ch;
int I;
float F;
};
main (void)
{
union numero N;
N.I = 123;
printf ("%f",N.F); /* Vai imprimir algo que nao e’ necessariamente 123 ...*/
return 0;
}
O programa acima é muito perigoso pois você está lendo uma região da memória, que foi "gravada" como um inteiro, como se fosse um ponto flutuante. Tome cuidado! O resultado pode não fazer sentido.
106
Enumerações
Numa enumeração podemos dizer ao compilador quais os valores que uma determinada variável pode assumir. Sua forma geral é:
enum nome_do_tipo_da_enumeração {lista_de_valores} lista_de_variáveis;
Vamos considerar o seguinte exemplo:
enum dias_da_semana {segunda, terca, quarta, quinta, sexta,
sabado, domingo};
O programador diz ao compilador que qualquer variável do tipo dias_da_semana só
pode ter os valores enumerados. Isto quer dizer que poderíamos fazer o seguinte programa:
#include
enum dias_da_semana {segunda, terca, quarta, quinta, sexta,
sabado, domingo};
main (void)
{
enum dias_da_semana d1,d2;
d1=segunda;
d2=sexta;
if (d1==d2)
{
printf ("O dia e o mesmo.");
}
else
{
printf ("São dias diferentes.");
}
return 0;
}
Você deve estar se perguntando como é que a enumeração funciona. Simples. O
compilador pega a lista que você fez de valores e associa, a cada um, um número inteiro.Então, ao primeiro da lista, é associado o número zero, o segundo ao número 1 e assim por diante. As variáveis declaradas são então variáveis int.
O Comando sizeof
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. 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:
#include
struct tipo_endereco
{
char rua [50];
int numero;
char bairro [20];
char cidade [30];
char sigla_estado [3];
long int CEP;
};
struct ficha_pessoal
{
char nome [50];
long int telefone;
struct tipo_endereco endereco;
};
void main(void)
{
struct ficha_pessoal *ex;
ex = (struct ficha_pessoal *) malloc(sizeof(struct ficha_pessoal));
...
free(ex);
}
O Comando typedef
O comando typedef permite ao programador definir um novo nome para um
determinado tipo. Sua forma geral é:
typedef antigo_nome novo_nome;
Como exemplo vamos dar o nome de inteiro para o tipo int:
typedef int inteiro;
Agora podemos declarar o tipo inteiro.
O comando typedef também pode ser utilizado para dar nome a tipos complexos, como as estruturas. As estruturas criadas no exemplo da página anterior poderiam ser definidas como tipos através do comando typedef. O exemplo ficaria:
#include
typedef struct tipo_endereco
{
char rua [50];
int numero;
char bairro [20];
char cidade [30];
char sigla_estado [3];
long int CEP;
} TEndereco;
108
typedef struct ficha_pessoal
{
char nome [50];
long int telefone;
TEndereco endereco;
}TFicha;
void main(void)
{
TFicha *ex;
...
}
Veja que não é mais necessário usar a palavra chave struct para declarar variáveis do tipo ficha pessoal. Basta agora usar o novo tipo definido TFicha.
Uma aplicação de structs: as listas simplesmente encadeadas
Várias estruturas de dados complexas podem ser criadas utilizando simultaneamente
structs e ponteiros. Uma destas estruturas é a lista encadeada. Uma lista encadeada é uma seqüência de structs, que são os nós da lista, ligados entre si através de ponteiros. Esta seqüência pode ser acessada através de um ponteiro para o primeiro nó, que é a cabeça da lista. Cada nó contém um ponteiro que aponta para a struct que é a sua sucessora na lista. O ponteiro da última struct da lista aponta para NULL, indicando que se chegou ao final da lista.
Esta estrutura de dados é criada dinamicamente na memória (utiliza-se malloc() e free()), de modo que se torna simples introduzir nós nela, retirar nós, ordenar os nós, etc. Não vamos entrar em detalhes sobre todos os algoritmos que poderíamos criar em uma lista encadeada, pois isto geralmente é feito em cursos de algoritmos e estruturas de dados, não se incluindo no escopo deste curso. Aqui, veremos somente formas de se criar uma lista encadeada em C e também maneiras simples de percorrer esta lista.
Supondo que queiramos criar uma lista encadeada para armazenar os produtos
disponíveis em uma loja. Poderíamos criar um nó desta lista usando a seguinte struct:
struct Produto {
int codigo; /* Codigo do produto */
double preco; /* Preco do produto */
struct Produto *proximo; /* Proximo elemento da lista
encadeada de Produtos */
};
Note que esta struct possui, além dos campos de dados codigo e preco, um campo
adicional que é um ponteiro para uma struct do tipo Produto. É este campo que será utilizado para apontar para o próximo nó da lista encadeada. O programa a seguir faz uso desta struct, através de um novo tipo criado por um typedef, para criar uma lista de produtos de uma loja:
#include
#include
/* Estrutura que será usada para criar os nós da lista */
typedef struct tipo_produto {
int codigo; /* Codigo do produto */
double preco; /* Preco do produto */
109
struct tipo_produto *proximo; /* Proximo elemento da lista encadeada de Produtos */
} TProduto;
/* Prototipos das funcoes para inserir e listar produtos */
void inserir(TProduto **cabeca);
void listar (TProduto *cabeca);
int main()
{
TProduto *cabeca = NULL; /* Ponteiro para a cabeca da lista */
TProduto *noatual; /* Ponteiro a ser usado para percorrer a lista no momento de
desalocar seus elementos*/
char q; /* Caractere para receber a opcao do usuario */
do {
printf("\n\nOpcoes: \nI -> para inserir novo produto;\nL -> para listar os produtos; \nS -
> para sair \n:");
scanf("%c", &q); /* Le a opcao do usuario */
switch(q) {
case ’i’: case ’I’: inserir(&cabeca); break;
case ’l’: case ’L’: listar(cabeca); break;
case ’s’: case ’S’: break;
default: printf("\n\n Opcao nao valida");
}
fflush(stdin); /* Limpa o buffer de entrada */
} while ((q != ’s’) && (q != ’S’) );
/* Desaloca a memoria alocada para os elementos da lista */
noatual = cabeca;
while (noatual != NULL)
{
cabeca = noatual->proximo;
free(noatual);
noatual = cabeca;
}
}
/* Lista todos os elementos presentes na lista encadeada */
void listar (TProduto *noatual)
{
int i=0;
while( noatual != NULL) /* Enquanto nao chega no fim da lista */
{
i++;
printf("\n\nProduto numero %d\nCodigo: %d \nPreco:R$%.2lf", i, noatual->codigo,
noatual->preco);
noatual = noatual->proximo; /* Faz noatual apontar para o proximo no */
}
}
/* Funcao para inserir um novo no, ao final da lista */
110
void inserir (TProduto **cabeca)
{
TProduto *noatual, *novono;
int cod;
double preco;
printf("\n Codigo do novo produto: ");
scanf("%d", &cod);
printf("\n Preco do produto:R$");
scanf("%lf", &preco);
if (*cabeca == NULL) /* Se ainda nao existe nenhum produto na lista */
{
/* cria o no cabeca */
*cabeca = (TProduto *) malloc(sizeof(TProduto));
(*cabeca)->codigo = cod;
(*cabeca)->preco = preco;
(*cabeca)->proximo = NULL;
}
else
{
/* Se ja existem elementos na lista, deve percorre-la ate’ o seu final e inserir o novo elemento
*/
noatual = *cabeca;
while(noatual->proximo != NULL)
noatual = noatual->proximo; /* Ao final do while, noatual aponta para o ultimo no
*/
novono = (TProduto *) malloc(sizeof(TProduto));/* Aloca memoria para o novo no */
novono->codigo = cod;
novono->preco = preco;
novono->proximo = NULL;
noatual->proximo = novono; /* Faz o ultimo no apontar para o novo no */
}
}
É interessante notar que, no programa anterior não existe limite para o número de
produtos que se vai armazenar na lista. Toda vez que for necessário criar um novo produto, memória para ele será alocada e ele será criado no final da lista. Note que a função inserir recebe o endereço do ponteiro cabeça da lista. Qual a razão disto? A razão é que o endereço para o qual a cabeça da lista aponta poderá ser modificado caso se esteja inserindo o primeiro elemento na lista. Tente entender todos os passos deste programa, pois ele possui várias das características presentes em programas que manipulam listas encadeadas. Também é importante notar que várias outras estruturas de dados complexas podem ser criadas com structs contendo ponteiros que apontam para outras structs.
AUTO AVALIAÇÃO
Crie uma struct para descrever restaurantes. Os campos devem armazenar o nome do restaurante, o endereço, o tipo de comida (brasileira, chinesa, francesa, italiana, japonesa, etc) e uma nota para a cozinha (entre 0 e 5). Crie uma lista encadeada com esta struct e escreva um programa que:
a) Insira um novo restaurante na lista;
b) Leia uma lista de restaurantes a partir de um arquivo;
c) Grave a lista de restaurantes para um arquivo;
d) Liste todos os restaurantes na tela;
e) Liste os restaurantes com cozinha com nota superior a um determinado valor,
determinado pelo usuário;
f) Liste todos os restaurantes com determinado tipo de comida, determinado pelo
usuário.
Bibliografia:
Este documento foi retirado do Curso de C, ministrado via Internet pela UFMG. Os
interessados podem acessar http://ead1.eee.ufmg.br/curso/C

Comentários