Programar em C/Tipos de dados definidos pelo usuário
Tipos de dados definidos pelo usuário
Muitas vezes é necessário manipular dados complexos que seriam difíceis de representar usando apenas os tipos primitivos (char, int, double, float). Para isso, há, em C, três tipos de dados que podem ser definidos pelo usuário:
- estruturas (struct);
- uniões (union);
- enumerações (enum).
As estruturas e uniões são compostas por várias variáveis (escolhidas pelo programador), por isso são ditos definidos pelo usuário. Já as enumerações são, resumidamente, tipos cujos valores devem pertencer a um conjunto definido pelo programador.
Estruturas
Uma estrutura (ou struct) é um tipo de dados resultante do agrupamento de várias variáveis nomeadas, não necessariamente similares, numa só; essas variáveis são chamadas membros da estrutura. Para declarar uma estrutura, usamos a palavra-chave struct
, seguida do nome que se deseja dar à estrutura (ao tipo de dados) e de um bloco contendo as declarações dos membros. Veja um exemplo:
struct mystruct { int a, b, c; double d, e, f; char string[25]; };
Este exemplo cria um tipo de dados denominado mystruct, contendo sete membros (a, b, c, d, e, f, string). Note que o nome mystruct é o nome do tipo de dados, não de uma variável desse tipo.
Um exemplo simples de aplicação de estruturas seria uma ficha pessoal que tenha nome, telefone e endereço; a ficha seria uma estrutura.
Ou, mais amplamente, uma estrutura seria uma representação de qualquer tipo de dado definido por mais de uma variável. Por exemplo, o tipo FILE*
é na verdade um ponteiro para uma estrutura que contém alguns dados que o sistema usa para controlar o acesso ao fluxo/arquivo. Não é necessário, para a maioria dos programadores, conhecer a estrutura do tipo FILE.
Definindo o tipo
A definição de um tipo de estrutura é feita com a palavra-chave struct, seguida do nome a ser dado ao tipo e de um bloco contendo as declarações dos elementos da estrutura:
struct nome_do_tipo { tipo_elem a; tipo_elem b, c; ... };
É muito importante incluir o ponto-e-vírgula ao final do bloco!
Declarando
Para declarar uma variável de um tipo já definido, fornecemos o nome do tipo, incluindo a palavra-chave struct:
struct nome_do_tipo variavel;
Também é possível condensar a definição do tipo e a declaração em um passo, substituindo o nome do tipo pela definição, sem o ponto-e-vírgula:
struct mystruct { int a, b, c; double d, e, f; char string[25]; } variavel;
Também é possível inicializar uma estrutura usando as chaves {} para envolver os elementos da estrutura, separados por vírgulas. Os elementos devem estar na ordem em que foram declarados, mas não é obrigatório inicializar todos; no entanto, para inicializar um elemento, todos os anteriores devem ser inicializados também. Por exemplo, poderíamos declarar valores iniciais para a variável acima da seguinte maneira:
struct mystruct variavel = {4, 6, 5, 3.14, 2.718, 0.95, "Teste"}; struct mystruct v2 = {9, 5, 7};
Inicializador designado
Para quem usa o C99 com o compilador GNU.
Durante a inicialização de um estrutura é possível especificar o nome do campo com '.nome_do_campo =' antes do valor.
Exemplo:
struct mystruct v2 = {.a=9,.b=5,.c=7};
Acessando
Para acessar e modificar os membros de uma estrutura, usamos o operador de seleção.
(ponto). À esquerda do ponto deve estar o nome da variável (estrutura) e à direita, o nome do membro. Podemos usar os membros como variáveis normais, inclusive passando-os para funções como argumentos:
variavel.a = 5; variavel.f = 6.17; strcpy (variavel.string, "Bom dia"); printf ("%d %f %s\n", variavel.a, variavel.f, variavel.string);
Vetores de estruturas
Sendo as estruturas como qualquer outro tipo de dados, podemos criar vetores de estruturas. Por exemplo, suponha algum programa que funcione como um servidor e permita até 10 usuários conectados simultaneamente. Poderíamos guardar as informações desses usuários num vetor de 10 estruturas:
struct info_usuario { int id; char nome[20]; long endereco_ip; time_t hora_conexao; }; struct info_usuario usuarios[10];
E, por exemplo, para obter o horário em que o 2º usuário usuário se conectou, poderíamos escrever usuarios[1].hora_conexao
.
Atribuição e cópia
Podemos facilmente copiar todos os campos de uma estrutura para outra, fazendo uma atribuição simples como a de inteiros:
struct ponto { int x; int y; }; ... struct ponto a = {2, 3}; struct ponto b = {5, 8}; b = a; // agora o ponto b também tem coordenadas (2, 3)
No entanto, devemos ter cuidado se a estrutura contiver campos ponteiros, pois, nesses casos, o que será copiado é o endereço de memória (e não o conteúdo daquele endereço). Por exemplo, se tivermos uma estrutura que comporta um inteiro e uma string, uma cópia sua conterá o mesmo inteiro e um ponteiro para a mesma string, o que significa que alterações na string da cópia serão refletidas também no original!
Passando para funções
Já vimos acima que podemos normalmente passar membros de uma estrutura como argumentos de funções. Também é possível passar estruturas inteiras como argumentos:
#include <stdio.h> struct ponto { int x; int y; }; void imprime_ponto (struct ponto p) { printf ("(%d, %d)\n", p.x, p.y); } int main () { struct ponto a = {3, 7}; imprime_ponto (a); return 0; }
No entanto, há dois possíveis problemas nisso:
- Alterações nos membros da estrutura só terão efeito dentro da função chamada, mas não na função que a chamou. Isso ocorre pois a estrutura é passada por valor (e não por referência).
- Quando a estrutura contiver muitos elementos, a passagem por valor tornar-se-á um processo de cópia de muitos dados. Por isso, é de costume passar estruturas por referência (como ponteiros), mesmo que a estrutura em questão seja pequena.
Uniões
Uniões são parecidas com estruturas, mas há uma diferença fundamental: nas uniões, todos os elementos ocupam o mesmo espaço de memória. Por isso, só é possível acessar um elemento por vez, já que uma mudança em um elemento causará mudança em todos os outros. A definição e a declaração de uniões é igual à das estruturas, trocando a palavra struct por union.
Há principalmente dois usos para as uniões:
- economia de espaço, já que guardam-se várias variáveis no mesmo espaço;
- representação de uma informação de mais de uma maneira. Um exemplo disso são os endereços IP, que na biblioteca de sockets podem ser representados como um grupo de 4 octetos (
char
) ou como um único valor inteiro (int
). Isso é feito com uma união parecida com esta:union ip_address { int s_long; char s_byte[4]; };
Dessa maneira, o endereço pode ser facilmente representado de maneira humanamente legível (com 4 octetos), sem dificultar o processamento interno (com o valor inteiro).
Enumerações
Enumeração (enum) ou tipo enumerado é um tipo de dados que tem como conjunto de valores possíveis um conjunto finito de identificadores (nomes) determinados pelo programador. Em C, cada identificador em uma enumeração corresponde a um inteiro.
Enumerações são definidas de maneira similar às estruturas e uniões, com algumas diferenças. A palavra chave usada é enum.
enum nome_enumeração { IDENTIFICADOR_1, IDENTIFICADOR_2, ... IDENTIFICADOR_n };
Note as diferenças: não há ponto-e-vírgula no final ou no meio das declarações (mas ainda há no final do bloco), e não há declaração de tipos.
Com essa declaração, ao IDENTIFICADOR_1 será atribuido o valor 0, ao IDENTIFICADOR_2 será atribuído o valor 1, e assim por diante. Podemos também explicitar os valores que quisermos colocando um sinal de igual e o valor desejado após o identificador.
- Caso não haja valor determinado para o primeiro identificador, ele será zero. Para os demais identificadores, o padrão é seguir a ordem dos números, a partir do valor do identificador anterior.
- Podemos misturar identificadores de valor determinado com identificadores de valor implícito, bastando seguir a regra acima.
Por exemplo:
enum cores { VERMELHO, /* 0 */ AZUL = 5, /* 5 */ VERDE, /* 6 */ AMARELO, /* 7 */ MARROM = 10 /* 10 */ };
Uso
Da mesma maneira que criamos uma variável de um tipo struct ou union, podemos criar variáveis de um tipo enumerado (enum):
enum cores cor_fundo;
Para atribuir valores a uma variável enumerada, podemos usar como valor tanto o identificador quanto o valor correspondente. Seriam equivalentes, portanto:
cor_fundo = VERDE; cor_fundo = 6;
Na verdade, variáveis enumeradas agem de maneira quase igual aos inteiros; é possível, assim, atribuir valores que não correspondem a nenhum dos identificadores.
Campo de bits
Na linguagem c o campo de bits (bitfields) é uma estrutura um pouco estranha , em vez de usar variáveis com tipos diferentes os campos são formados com as partes de um inteiro. O tamanho de um campo de bits não pode ser maior que o tipo usado , aqui um short .
typedef struct { unsigned short campo_1: 6, /* Tamanho 6 bit */ campo_2: 6, campo_3: 1, campo_4: 1, campo_5: 2; }BIT_FIELD_1;
Essa estrutura esta formada por um tipo que tem o tamanho de um short esse mesmo tipo será divido em porções menores. No exemplo acima os campos tem os tamanhos 6,6,1,1,2 igual a 16 bits que é o tamanho de um unsigned short . Para acessar os campos usamos o mesmo método que usamos com estruturas normais .
BIT_FIELD_1 meu_campo;
meu_campo.campo_1 = 16; meu_campo.campo_4 = 0;