Aula 7 – Estruturas, Uniões, Enumerações e Tipos Definidos pelo Usuário

Há cinco formas diferentes de criar tipos de dados definíveis pelo usuário em C, veremos a seguir.

Estruturas

Uma estrutura é uma coleção de variáveis, logicamente relacionadas, referenciadas por um nome. Estas variáveis são chamadas de elementos da estrutura.  A palavra chave struct informa ao compilador que um modelo de estrutura está sendo definido:

struct addr{
    char name[30];
    char street[40];
    char city[20];
    char state[3];
    unsigned long int zip;
};

Como a definição da estrutura é um comando, observe que ela termina em ponto-e-vírgula. O rótulo da estrutura addr identifica esta estrutura em particular. O script acima não declara nenhumva variável, apenas faz uma definição de dados. Para declararmos uma variável com esta estrutura usamos:

struct addr addr_info;

O comando acima define uma estrutura do tipo addr chamada addr_info. Não existe nenhuma variável do tipo addr até que ela seja realmente declarada.

O compilador aloca automaticamente memória para todas as variáveis da estrutura. Então assumindo caracteres de 1 byte e inteiros de 2 bytes a variável addr_info ocuparia 97 bytes de memória RAM:

Struct em C

Podemos também declarar variáveis do tipo da estrutura que está sendo definida no mesmo comando:

struct addr{
    char name[30];
    char street[40];
    char city[20];
    char state[3];
    unsigned long int zip;
} addr_info, binfo, cinfo;

Se precisamos de apenas uma variável do tipo da estrutura, então podemos omitir seu nome:

struct{
    char name[30];
    char street[40];
    char city[20];
    char state[3];
    unsigned long int zip;
} addr_info;

A forma geral de definição de estrutura é:

struct nome {
tipo nome_da_variável;
tipo nome_da_variável;
...
} variáveis_estrutura;

sendo que nome ou variáveis_estrutura podem ser omitidos, mas não ambos.

Referenciando Elementos de Estruturas

Para referenciarmos um elemento individual da estrutura usamos o operador ponto. O código abaixo atribui o CEP 12345 ao campo zip da variável estrutura addr_info:

addr_info.zip = 12345;

A forma geral para referenciar uma estrutura é:

nome_da_estrutura.nome_do_elemento

Poderíamos por exemplo, atribuir uma entrada de caracteres para a variável nome da estrutura:

scanf(“%s”, addr_info.name);

Para acesso de elementos individuais da variável name da estrutura,p oderíamos fazer:

register int t;
for ( t = 0; addr_info.name[t]; ++t)
    putchar(addr_info.name[t]);

Atribuição de Estruturas

Atribuições de estrutura não eram permitidas na versão original C, porém seguindo o padrão ANSI, e assumindo que nosso compilador o faz, podemos atribuir estruturas diretamente, sem precisar acessar cada variável individualmente.

#include <stdio.h>
void main(void)
{
    struct {
        int a;
        int b;
    } x, y;

    x.a = 10;
    //  Atribui uma estrutura a outra
    y = x;
    printf("%d\n", y.a);
}

O exemplo acima irá imprimir o valor 10 na tela, pois y.a contém este valor.

Matrizes de Estruturas

Um uso comum de estrutura é com uma matriz de estrutura. Para fazê-lo é simples:

struct addr addr_info[100];

O comando acima cria uma matriz de 100 elementos da estrutura addr. Se quiséssemos acessar algum nome da estrutura, usaríamos um índice :

printf(“%d\n”, addr_info[10].zip);

Um Exemplo de Lista Postal

A estrutura usada conterá cadastros de nosta lista. O campo zip é long pois CEPs maiores que 64000 não podem ser representados com um inteiro de dois bytes.

#include <stdio.h>
#include <stdlib.h>
#define MAX 100

struct addr {
    char name[30];
    char street[40];
    char city[20];
    char state[3];
    unsigned long int zip;
} addr_info[MAX];

void init_list(void), enter (void);
void delete(void), list(void);
int menu_select(void), find_free(void);

void main(void)
{
    char choice;

    //  Inicializa a matriz de estruturas
    init_list();

    for (;;){
        choice = menu_select();
        switch(choice){
            case 1:
                enter();
                break;
            case 2:
                delete();
                break;
            case 3:
                list();
                break;
            case 4:
                exit(0);
        }
    }
}

/**
 * Inicializa o campo nome dos itens da estrutura com caractere nulo.
 * O programa irá verificar o campo nome e caso esteja vazio assume que
 * a estrutura em questão não está sendo utilizada
 */
void init_list(void)
{
    register int t;
    for (t = 0; t < MAX; ++t)
        addr_info[t].name[0] = '\0';
}

/**
 * Exibe opções ao usuário e retorna a opção selecionada por ele
 * @return Opção de 1 à 4 que o usuário selecionou
 */
menu_select(void)
{
    char s[80];
    int c;

    printf("1 - Inserir um nome\n");
    printf("2 - Excluir um nome\n");
    printf("3 - Listar o arquivo\n");
    printf("4 - Sair\n");

    do{
        printf("\nEntre com sua escolha: ");
        scanf("%s", s);
        c = atoi(s);
    } while(c < 0 || c > 4);

    return c;
}

/**
 * Insere dados de cadastro oriundos do usuário
 * Usa a função find_free() para procurar um elemento não usado na matriz,
 * caso não encontre exibe a mensagem "lista cheia"
 */
void enter(void)
{
    int slot;
    char s[80];

    slot = find_free();

    if (slot == -1){
        printf("\nlista cheia");
        return;
    }

    printf("Entre com o nome: ");
    scanf("%s", addr_info[slot].name);
    printf("Insira a rua: ");
    scanf("%s", addr_info[slot].street);
    printf("Insira a cidade: ");
    scanf("%s", addr_info[slot].city);
    printf("Entre com o estado: ");
    scanf("%s", addr_info[slot].state);

    printf("entre com o CEP: ");
    scanf("%s", s);
    addr_info[slot].zip = strtoul(s, '\0', 10);
}

/**
 * Procura por uma estrutura não usada
 * Se não encontrar, retorna -1, que é um valor seguro visto
 * que não pode existir um índice -1 em uma matriz
 * @return
 */
find_free(void)
{
    register int t;
    for(t = 0; addr_info[t].name[0] && t < MAX; ++t);
    //  Se não houver elementos livres, retorna -1
    if (t == MAX) return -1;
    return t;
}

/**
 * Excluir um registro da lista
 */
void delete(void)
{
    register int slot;
    char s[80];
    printf("Apagar qual registro?: ");
    scanf("%s", s);
    slot = atoi(s);
    if (slot >= 0 && slot < MAX) addr_info[slot].name[0] = '\0';
}

/**
 * Exibe a lista na tela
 */
void list(void)
{
    register int t;

    for (t = 0; t < MAX; ++t){
        if (addr_info[t].name[0]){
            printf("%s\n", addr_info[t].name);
            printf("%s\n", addr_info[t].street);
            printf("%s\n", addr_info[t].city);
            printf("%s\n", addr_info[t].state);
            printf("%lu\n", addr_info[t].zip);
        }
    }

    printf("\n\n");
}

Passando Estruturas para Funções

Estudaremos aqui passagem de estruturas e seus elementos para funções.

Passando Elementos de Estrutura para Funções

Ao passar um elemento de uma variável estrutura, estamos de fato passando seu valor, exceto as matrizes.

    struct fred {
        char x;
        int y;
        float z;
        char s[10];
    } mike;

Abaixo exemplos de passagem de parâmetros:

func(mike.x); // Passa o valor do caractere x
func2(mike.y); //  Passa o valor inteiro de y
func3(mike.z); // Passa o valor float de z
func4(mike.s); // Passa o endereço da string s
func5(mike.s[2]); // Passa o valor do caractere de s[2]

Se quisermos passar o endereço de um elemento da estrutura, usamos o operador & antes do nome da estrutura. Por exemplo:

func(&mike.x); // Passa o endereço do caractere x
func2(mike.s); // Passa o endereço da string s (lembrando que o nome da matriz equivale ao endereço do 1º elemento)
func3(&mike.s[2]); // Passa o endereço do caractere s[2]

Passando Estruturas Inteiras para Funções

O exemplo abaixo passa uma estrutura, com escopo local, como parâmetro para uma função. Logo, quaisquer alterações feitas dentro da função não farão alteração no valor original da estrutura. Lembrando que o tipo de argumento deve coincidir com o tipo de parâmetro, no exemplo que segue tanto o argumento arg como o parâmetro parm são declarados como o mesmo tipo de estrutura:

#include <stdio.h>
void main(void)
{
    struct {
        int a, b;
        char ch;
    } arg;

    arg.a = 1000;

    f1(arg);
}

f1(
    struct {
        int x, y;
        char ch;
    } parm
)
{
    printf("%d", parm.x);
}

O programa acima é reconhecido com erros pelo analisador de síntaxe do netbeans, porém compila e roda normalmente. Ele imprime o valor 1000 na tela. Apesar de ser perfeitamente válido, uma abordagem mais comum é usada com estrutura e parâmetros. O que é feito é definir a estrutura como global e então usar seu nome para declarar variáveis. O mais importante do programa abaixo é que ele ajuda a garantir que os arguemntos e os parâmetros coincidam.

#include <stdio.h>
//  Define a estrutura com escopo global
struct struct_type {
    int a, b;
    char ch;
};

void main(void)
{
    //  Define a variável arg como sendo uma estrutura do tipo struct_type
    struct struct_type arg;
    arg.a = 1000;
    f1(arg);
}

f1(struct struct_type parm)
{
    printf("%d\n", parm.a);
}

Ponteiros para Estruturas

Da mesma forma que C permite ponteiros para outros tipos de variáveis, ele permite para estruturas, porém existem alguns aspectos especiais que devemos revisar.

Declarando um Ponteiro para Estrutura

Assim como declaramos um ponteiro para uma variável, o fazemos para uma estrutura. Abaixo addr_pointer é um ponteiro para uma estrutura do tipo addr.

struct addr *addr_pointer;

Usando Ponteiros para Estruturas

O custo para passar uma grande estrutura para uma função é muito grande, devido ao fato de que todos os elementos dela precisam ser copiados para a memória e depois lidos dela para só então ocorrer o processamento da função. Logo, para estes casos, e quando precisamos alterar o valor diretamente na estrutura, usamos um ponteiro para uma estrutura.

Usando um ponteiro, apenas o valor dele é passado para a pilha em uma chamada de função, tornando-se possível a chamada muito rápida de funções. Para encontrar o endereço da estrutura usamos o operador & antes do nome dela. Por exemplo:

struct bal{
    float balance;
    char name[80];
} person;

//  Declara um ponteiro para a estrutura bal
struct bal *p;
//  Põe o endereço da estrutura person no ponteiro p
p = &person;

Para acessar os elementos da estrutura através do ponteiro, devemos usar o operador -> também conhecido como seta:

p->balance

A seta é usada no lugar do operador ponto quando usamos um ponteiro para fazer acesso à estrutura.

Abaixo vemos o uso de ponteiros para estrutura, com um programa que implementa um atraso (delay) via software.

#include <stdio.h>

struct my_time {
    int hours;
    int minutes;
    int seconds;
};

void display(struct my_time *t);
void update(struct my_time *t);
void delay(void);

void main(void)
{
    struct my_time systime;

    systime.hours = 0;
    systime.minutes = 0;
    systime.seconds = 0;

    for (;;){
        update(&systime);
        display(&systime);
    }
}

void update(struct my_time *t)
{
    t->seconds++;
    if (t->seconds == 60){
        t->seconds = 0;
        t->minutes++;
    }
    if (t->minutes == 60){
        t->minutes = 0;
        t->hours++;
    }
    if (t->hours == 24) t->hours = 0;
    delay();
}

void display(struct my_time *t)
{
    printf("%02d:", t->hours);
    printf("%02d:", t->minutes);
    printf("%02d:", t->seconds);
}

void delay(void)
{
    long int t;
    for (t = 1; t < 128000; ++t);
}

Matrizes e Estruturas Dentro de Estruturas

Estruturas podem conter elementos simples (tipos primários de dados) ou complexos (matrizes ou outras estruturas). Uma estrutura pode conter como um elemento, outra estrutura. Chamamos isso de estrutura aninhada. Suponhamos a estrutura abaixo:

struct emp {
struct addr address;
float wage;
} worker;

Vemos que a estrutura emp contém dois elementos. O primeiro deles é do tipo addr e possui o endereço de um empregado, o outro é wage e contém o salário do empregado. O fragmento de código abaixo atribui 89560 ao elemento zip de address.

worker.address.zip = 89560;

Como podemos ver, o acesso à estrutura ocorre de fora para dentro. O padrão ANSI estabelece que estruturas podem ser aninhadas em até 15 níveis.

Campos de Bits

Acessar um bit dentro de um byte pode ser muito útil quando:

  • O armazenamento é limitado, você precisa armazenar diversas variáveis Booleanas dentro de um byte.
  • Certos dispositivos transmitem informações codificadas nos bits dentro de um byte.
  • Certas rotinas de criptografia precisam acessar os bits dentro de um byte.

Embora, possamos fazer tudo com os operadores bita a bit, uma campo de bit pode acrescentar mais estrutura e eficiência ao nosso código.

A forma geral de uma definição de campo de bit é:

struct nome {
tipo nome1: comprimento;

tipo nomeN: comprimento;
} lista de variáveis;

Um campo de bit deve ser declarado como int, unsigned ou signed. Se o campo tiver comprimento de apenas 1bit então obrigatoriamente deve ser do tipo int unsigned, afinal 1 único bit não pode armazenar sinal.

Campos de bit geralmente são usados para análise de entrada de um dispositivo de hardware. Suponhamos um adaptador de comunicações seriais que devolve um byte de estado organizado desta forma:

Bit Significado Quando Ligado
0 alteração na linha clear-to-send
1 alteração em data-set-ready
2 borda de subida da portadora detectada
3 alteração na linha de recepção
4 clear-to-send
5 data-set-ready
6 chamada do telefone
7 sinal recebido

Podemos representar a informação acima em um byte de estado uando o seguinte campo de bits:

struct status_type{
    unsigned delta_cts:     1;
    unsigned delta_dsr:     1;
    unsigned tr_edge:       1;
    unsigned delta_rec:     1;
    unsigned cts:           1;
    unsigned dsr:           1;
    unsigned ring:          1;
    unsigned rec_line:      1;
} status;

Podemos usar uma rotina semelhante a esta para permitir que o programa detecte o envio/recebimento de dados:

status = get_port_status();
if (status.cts) printf(“livre para enviar”);
if (status.dsr) printf(“dados prontos”);

Para fazer atribuição a um campo de bit, simplesmente usamos a forma para acessar qualquer elemento de estrutura. O exemplo abaixo limpa o campo ring:

status.ring = 0;

Não precisamos dar um nome a todo o campo de bit. Isso permite albançar de maneira fácil o bit que desejamos usar, contornando os não-usados. Digamos que apenas cts e dtr importam, poderíamos então declara a estrutura status_type desta maneira:

struct status_type{
    unsigned :              4;
    unsigned cts:           1;
    unsigned dsr:           1;
} status;

Note que os dois bits restantes não foram utilizados, portanto não preicsam ser especificados.

Algumas restrições das variávies de campo de bit são: Não podemos obter seus endereços, elas não podem ser organizadas em matrizes e não podemos ultrapassar os limites de um inteiro. Em resumo, usando variáveis de campo de bit, estaremos inserindo algumas dependências de máquina no código.

É válido também misturarmos elementos normais de estrutura com elementos de campo de bit. No exemplo abaixo temos um registro de empregado que usa apenas um byte para conter informações do estado do empregado, se ele é assalariado e o número de deduções. Se não usásse-mos campo de bits, ocuparíamos três bytes:

struct emp{
    struct addr address;
    float pay;
    //  Ocioso ou ativo
    unsigned lay_off:       1;
    //  Pagamento por horas ou salário
    unsigned hourly:        1;
    //  Deduções de imposto
    unsigned deduction:     3;
};

Uniões

Em C, uma union é uma posição de memória que é compartilhada por duas ou mais variáveis, geralmente de tipos diferentes, em momentos diferentes. Sua forma geral é:

union nome {
tipo nome_da_variável;
tipo nome_da_variável;

} variáveis_união;

Por exemplo:

union u_type {
int  i;
char ch;
};

A definição acima não declara nenhuma variável, para fazê-lo escrevemos:

union u_type cnvt;

Na union cnvt, tanto o inteiro i quanto o caractere ch compartilham a mesma posição de memória, cada qual com seu tamanho de memória como visto abaixo:

union

Uma union sempre aloca o espaço de memória da maior variável. No caso acima, para a union cnvt, o compilador alocará 2 bytes de memória (assumindo um inteiro de 2 bytes).

Unions são geralmente usadas para conversões de tipo. Por exemplo, podemos usar uma union para escrever a representação binário de um inteiro em um arquivo em disco.

Primeiro criamos uma união entre um inteiro e uma matriz de 2 bytes:

union pw {
int i;
char ch[2];
};

Então, uma função que imprime os valores:

putw(union pw word, FILE *fp)
{
    //  Escreve a primeira metade
    putc(word->ch[0], fp);
    //  Escreve a segunda metade
    putc(word->ch[1], fp);
}

Enumerações

Uma enumeração é um conjunto de constantes inteiras que especifica todos os valores possiveis para uma variável deste tipo. Semelhante as estruturas, a palavra chave enum assinala o início de um tipo de enumeração. Sua forma geral:

enum nome { lista de enumeração } lista_de_variáveis;

O código abaixo define uma enumeração chamada coin e declara monney como sendo deste tipo:

enum coin {penny, nichekl, dime, quarter, half_dollar, dollar};
enum coin money;

Tendo esta definição em mente, os comandos abaixo são perfeitamente válidos:

money = dime;
if (money == quarter) printf(“É um uqarto\n”);

O funcionamento de uma enumeração é simples, cada símbolo representa um valor inteiro, começando do 0. Portanto o fragmento de código abaixo mostra 0 2 na tela:

printf(“%d %d”, penny, dime);

Podemos especificar valores de um ou mais símbolos, e quando o faemos, o próximo símbolo continua do valor inteiro após o maior precedente, por exemplo, o código abaixo atribui o valor 100 a quarter:

enum coin { penny, nickel, dime, quarter = 100, half_dollar, dollar

Os valores dos símbolos serão:

penny 0
nickel 1
dime 2
quarter 100
half_dollar 101
dollar 102

Uma enumeração é frequentemente usada para definir tabela de símbolos de um compilador. Ela também pode ser útil para provar a validade de um programa, produzindo uma verificação redundante em tempo de compilação, confirmando que apenas valores válidos sejam atribuídos a uma variável.

Usando sizeof para Assegurar Portabilidade

Vimos que estruturas, uniões e enumerações podem ser usadas para criação de variáveis de diferentes tamanhos e que o tamanho real irá depender da máquina. Para garantirmos portabilidade, o operador unário sizeof pode ser usado, visto que ele calcula o tamnho de qualquer variável ou tipo.

Assumindo os tamanhos de tipos de dados mostrados abaixo:

Tipo Tamanho em bytes
char 1
int 2
long 4
float 4
double 8

Desta forma, o código abaixo escreverá os números 1, 2 e 4 na tela:

char ch;
int i;
float f;
printf(“%d”, sizeof(ch));
printf(“%d”, sizeof(i));
printf(“%d”, sizeof(f));

sizeof é um operador de tempo de compilação, portanto toda informação que se precisa para calcular o tamanho de uma varível é conhecida em tempo de compilação.

Na union abaixo, sizeof(u_var) é 4. Não importa o que ela guarda e sim o tamanho da maior variável que pode ser armazenada por um de seus elementos.

union x {
char ch;
int i;
float f;
} u_var;

typedef

C nos permite criar um novo nome para um tipo de dado já existente. A forma geral de um comando typedef é:

typedef tipo nome;

onde o tipo é qualquer tipo de dados permitido, e nome é o novo nome para este tipo. O novo nome é uma opção para o tipo e não uma substituição. Por exemplo, podemos criar um novo nome para float usando:

typedef float balance;

O comando acima diz ao compilador para reconhecer balance como outro nome para float. Assim podemos criar uma variável float, usando balance:

balance over_due;

Acima, over_due é uma variável de ponto flutuante do tipo balance que é outro nome para float.

Usar typedef pode tornar o código mais legível e portável, visto que só precisamos altear os typedef para a nova arquitetura. Lembre-se: não estamos criando nenhum tipo novo de dados usando typedef.

 

Minha Reflexão

Até aqui vimos a base mínima para qualquer programa em C. Digamos que passamos agora da introdução do livro, onde 205 páginas foram estudadas. A partir de agora veremos mais código e exemplos práticos de uso para programas como na próxima aula onde iniciaremos entradas e saídas pelo console.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *