Laboratório de Programação Prof. Oscar Luiz Monteiro de Farias [email protected] Capítulo 5 Estruturas Estruturas (1) Uma estrutura (registros, em outras LP's) é uma coleção de uma ou mais variáveis, possivelmente de diferentes tipos. Estas variáveis são colocadas juntas, sob um único nome, para manipulação conveniente. Permitem que um grupo de variáveis relacionadas sejam tratadas como uma unidade. Ex.: registro relativo a um aluno em um sistema de administração acadêmica (#matrícula, nome, sexo, curso, etc.) Aplicações gráficas: ponto (par de coordenadas), retângulo (par de pontos)... Estruturas (2) Estruturas podem ser copiadas e atribuídas, passadas como argumentos para funções e retornadas por funções. Estruturas e vetores automáticos podem ser inicializados. A palavra-chave (keyword) struct introduz uma declaração de estrutura, que é uma lista de declarações entre chaves. Um nome opcional, chamado etiqueta de estrutura (structure tag), pode se seguir à palavra struct, e pode ser usada posteriormente como uma abreviação para a parte da declaração entre chaves. As variáveis nomeadas em uma estrutura são denominadas membros (members). Estruturas (3) Exemplos de estruturas em aplicações gráficas O objeto básico é um ponto, com coordenadas x e y. struct point { int x; int y; }; Estruturas (4) Um membro ou uma etiqueta (tag) de uma estrutura podem ter o mesmo nome que uma variável comum, pois sempre poderão ser distingüidas pelo contexto. Os mesmos nomes de membros podem ocorrer em diferentes estruturas, sem conflitos. Uma declaração struct define um tipo. O fecha-chave ''}'' que termina a lista de membros pode ser seguido por uma lista de variáveis, de modo similar a qualquer tipo básico. struct { ... } x, y, z; É sintaticamente análogo a: int x, y, z; Estruturas (5) Uma declaração de estrutura que não é seguida por uma lista de variáveis não reserva qualquer área de memória. Simplesmente descreve um template ou modelo de uma estrutura. Se a declaração da estrutura tiver uma tag, esta poderá posteriormente ser usada em definições de instâncias da estrutura. struct point pt; Define uma variável pt que é uma struct do tipo point. Estruturas (6) Pode-se inicializar uma estrutura seguindo-se a sua definição por uma lista de inicializadores para os membros, cada um sendo uma expressão constante. struct maxpt = { 320, 200 }; Uma estrutura automática pode também ser inicializada por atribuição ou invocando-se uma função que retorne uma estrutura do tipo desejado. Em uma expressão referencia-se um membro de uma dada estrutura por meio de uma construção do tipo: structure-name.member Estruturas (7) O operador de membro de estrutura ''.'' conecta o nome de uma variável do tipo estrutura e o nome do membro. Para se imprimir as coordenadas do ponto pt, p.ex.: printf("%d,%d", pt.x, pt.y); Para cálculo da distância do ponto (0,0) a pt: double dist, sqrt(double); dist = sqrt((double)pt.x * pt.x + (double)pt.y * pt.y); Estruturas (8) Estruturas podem ser edentadas. Ex.: um retângulo é um par de pontos, denotando vértices opostos. struct rect { struct point pt1; struct point pt2; }; Se declararmos: struct rect screen; então screen.pt1.x irá referir-se à coordenada x do membro pt1 de screen. Estruturas e funções (1) As operações legais em estruturas são a ''cópia'' e ''atribuição'' (como uma unidade), tomando-se seu endereço com o operador ''&'' e fazendo acesso a seus membros. ''cópia'' e ''atribuição'' incluem a passagem de argumentos para funções e o retorno de valores de funções. Estruturas não podem ser comparadas. Uma estrutura pode ser inicializada por uma lista de valores constantes correspondendo a seus membros. Uma estrutura automática pode ser inicializada por uma atribuição. Estruturas e funções (2) Funções, envolvendo estruturas, para manipular pontos e retângulos: /* makepoint: make a point from x and y components */ struct point makepoint(int x, int y) { struct point temp; temp.x = x; temp.y = y; return temp; } Não há conflitos entre os nomes de argumento e membros com mesmo nome. Estruturas e funções (3) makepoint pode ser usada para inicializar estruturas dinâmicamente, ou para fornecer argumentos de estrutura para uma função: Estruturas e funções (4) Funções para realizar aritmética com pontos. Os argumentos e o valor retornado são estruturas. Os parâmetros da função, do tipo struct, são passados por valor, como quaisquer outros. Estruturas e funções (5) A função ptinrect testa se um ponto se encontra ''dentro'' de um retângulo. Convenção: um retângulo inclui sua área interna e mais seus lados esquerdo e inferior. Um retângulo está representado pelos seus vértices inferior esquerdo e superior direito. Estruturas e funções (6) A função a seguir retorna um retângulo necessariamente em forma canônica: Estruturas e funções (7) Se uma estrutura maior tiver que ser passada para uma função, em geral é mais eficiente se passar um pointer do que copiar a estrutura inteira. Os pointers para estruturas são similares aos pointers para variáveis comuns. struct point *pp; diz que pp é um pointer para um tipo struct point. pp é a estrutura (*pp).x e (*pp).y são seus membros. Estruturas e funções (8) Para se usar pp, poder-se-ia, p. ex., escrever: struct point origin, *pp; pp = &origin; printf("origin is (%d,%d)\n", (*pp).x, (*pp).y); Os parênteses são necessários em (*pp).x porque a precedência do operador de membros de estrutura ''.'' é maior do que a de ''*''. A expressão *pp.x significaria *(pp.x), que seria ilegal, pois x não é um pointer. Estruturas e funções (9) Os apontadores para estruturas são usados com tanta freqüência que existe uma notação alternativa, abreviada. Se p é um pointer para uma estrutura, então p->member-of-structure refere-se ao membro em particular. printf("origin is (%d,%d)\n", pp->x, pp->y); ''.'' e ''->'' associam da esquerda para a direita. Se tivermos struct rect r, *rp = &r; Serão equivalentes as expressões: r.pt1.x / rp->pt1.x/ (r.pt1).x / (rp->pt1).x Estruturas e funções (10) O operador de estruturas . e ->, juntamente com () para invocação de funções e [] para subscritos, estão no topo da hierarquia de precedência e, assim prendem fortemente. Dada a declaração struct { int len; char *str; } *p; então ++p->len incrementa len, e não p, pois a parentetisação implícita é ++(p->len). Estruturas e funções (11) (++p)->len incrementa p antes do acesso a len. (p++)->len incrementa p após o acesso (Neste caso os parentêses são redundantes). *p->str obtém qualquer objeto apontado por str. *p->str++ incrementa str após ter acesso ao que ele (str) aponta (tal como *s++). (*p->str)++ incrementa aquilo para o qual str aponta. *p++->str incrementa p após ter acesso ao que str aponta. Vetores de Estruturas (1) Considere um programa para contar as ocorrências de cada palavra-chave em C. Será necessário um vetor de cadeias de caracteres para armazenar as palavras-chaves e um outro vetor de inteiros para armazenar os contadores. Pode-se usar dois vetores independentes e paralelos: char *keyword[NKEYS]; int keycount[NKEYS]; Ou ainda, um vetor de estruturas. Vetores de Estruturas (2) struct key { char *word; int count; } keytab[NKEYS]; Declara uma estrutura do tipo key, define um array keytab de estruturas desse tipo, e reserva área de armazenamento para eles. Cada elemento do array é uma estrutura. Cada elemento do array é um par (por palavra-chave): char *word; int count; Vetores de Estruturas (3) Alternativamente poder-se-ia ter: struct key { char *word; int count; }; struct key keytab[NKEYS]; Como a estrutura keytab contém um conjunto constante de nomes é conveniente torná-la uma variável externa e inicializá-la, quando de sua definição. A definição é seguida por uma lista de inicializadores entre chaves. Vetores de Estruturas (4) Inicialização de vetores de estruturas: Vetores de Estruturas (5) Programa de contagem de palavras-chaves (keywords) em um programa escrito em C. ver prog61-chap06-pg110.c i) O programa tem início com a definição da estrutura keytab. A lista de keywords deve estar ordenada em ordem crescente em keytab. ii) main lê a entrada repetidamente invocando a função getword, que obtém da entrada, uma palavra de cada vez. iii) cada palavra é pesquisada em keytab por uma versão da pesquisa binária binsearch. Vetores de Estruturas (6) Cada chamada a getword encontra uma palavra, que é copiada para um vetor (cadeia de caracteres) indicada pelo seu primeiro argumento. O tamanho do array keytab é determinado em tempo de compilação. O número de elementos em keytab é dado por: (tamanho de keytab) / (tamanho da struct key) Ou, usando-se o operador size of: define NKEYS (sizeof keytab / sizeof(struct key)) ou #define NKEYS (sizeof keytab / sizeof(keytab[0])) O operador sizeof sizeof objeto ou sizeof (nome do tipo) geram um inteiro igual ao tamanho, em bytes, do objeto ou tipo especificado. sizeof produz um valor inteiro não sinalizado,cujo tipo size_t é definido no header <stdef.h>. Um sizeof não pode ser usado em uma linha #if, pois o pré-processador não analisa nomes de tipo. Mas a expressão no #define não é avaliada pelo pré-processador e o código sugerido é válido. Pointers para estruturas (1) Para ilustrar alguns conceitos eis uma nova versão do programa de contagem de palavras-chaves envolvendo pointers ao invés de índices de arrays. ver prog62-chap06-pg112.c A declaração de binsearch deve indicar o retorno de um pointer para struct key, ao invés de um inteiro. Isto afeta o protótipo da função e a própria função binsearch. Se binsearch encontra a palavra, ela retorna um pointer para o elemento do array de estruturas que a contém; senão, retorna NULL. Agora, tem-se acesso aos elementos de keytab via pointers. O mais importante é ajustar o algoritmo para que não gere pointers ilegais ou tente efetuar um acesso for a dos limites do array. Pointers para estruturas (2) Em main tem-se: for (p = keytab; p < keytab + NKEYS; p++) Se p é um pointer para uma estrutura, aritmética de ponteiros em p leva em consideração o tamanho da estrutura, assim p++ incrementa p para apontar para o próximo elemento do array de estruturas. Não se deve assumir, todavia, que o tamanho de uma estrutura é a soma do tamanho de seus membros. Pode haver necessidade de ''alinhamentos''. Pointers para estruturas (3) struct { char c; int i; }; se um char ocupar 1 byte e um int, 4 bytes, o tamanho da estrutura acima poderá exigir 8 bytes. O operador sizeof sempre retornará o valor apropriado. Convenção para editar os programas que possuem funções que retornam pointers para estruturas, Estruturas auto-referenciadas (1) Problema: contar as ocorrências de todas as palavras que aparecem em um fluxo de entrada. ver prog63-chap06-pg115.c Como a lista de palavras não é conhecida a priori, não se pode ordená-la e usar a pesquisa binária. Não se pode fazer uma pesquisa linear para cada palavra, assim que a mesma estiver disponível, pois o seu tempo de execução poderia crescer exponencialmente. Uma solução é manter o conjunto de palavras vistas até o momento ordenadas, colocando-as na posição correta assim que estiverem disponíveis. Estruturas auto-referenciadas (2) Esta solução não pode ser aplicada a um vetor linear, pois demandaria muito tempo. Alternativamente será usada uma estrutura de dados denominada árvore binária. “now is the time for all good men to come to the aid of their party” Estruturas auto-referenciadas (3) A árvore contém um nodo para cada palavra distinta. Cada nodo contém: - um pointer para o texto da palavra - um contador do número de ocorrências - um pointer para o nodo filho esquerdo - um pointer para o nodo filho direito Nenhum nodo pode ter mais de dois filhos. A organização da árvore é tal que, para qualquer nodo, a subárvore esquerda contenha apenas palavras lexicograficamente menores que a palavra no nodo, e, a subárvore à direita, palavras maiores. Estruturas auto-referenciadas (4) Aplica-se um algoritmo recursivo para se verificar se a palavra procurada já está na árvore. i) Inicia na raiz e compara a palavra com o texto armazenado neste nodo. ii) Se for igual, então achou. iii) Se a palavra for menor lexicograficamente que o texto, procura na subárvore da esquerda. iv) Se não procura na subárvore da direita. Se não houver filho no sentido especificado (D|E), a nova palavra não está na árvore. Estruturas auto-referenciadas (5) Um nodo será representado por: Esta é uma declaração recursiva. struct tnode *left; declara left sendo um pointer para tnode, e não sendo ele mesmo um tnode, o que não seria admissível. Estruturas auto-referenciadas (6) Estruturas que se referem umas às outras: A função main lê as palavras com getword e as instala na árvore com addtree. A função addtree é recursiva. Estruturas auto-referenciadas (7) Eventualmente a palavra casa com algum texto já armazenado em algum nodo na árvore (incrementa-se o contador), ou um null pointer é encontrado, indicando que um novo nodo deve ser criado (para armazenar a palavra) e adicionado à árvore. Nesse último caso, addtree returna um pointer para ele, o qual é instalado no nodo pai. A memória para um novo nodo é obtida por talloc, que retorna um pointer para um espaço livre capaz de armazenar um nodo da árvore, e a nova palavra é copiada para lá por strdup. O contador é inicializado, e os dois filhos (E|D) são feitos null. Esta parte do código é executada apenas nas folhas da árvore, quando um novo nodo está sendo adicionado. Estruturas auto-referenciadas (8) treeprint imprime a árvore ordenada; em cada nodo, ela imprime a subárvore da esquerda (<s ) , então a própria palavra, então a sub-árvore da direita (>s). Se a árvore se tornar desbalanceada, o tempo de execução do programa pode crescer rapidamente. Pior caso: se as palavras já estiverem ordenadas. Há generalizações de árvore binária que solucionam esse problema... A função malloc assegura o alinhamento da struct. É declarada no arquivo header <stdlib.h> Pesquisa em tabela (1) Problema: package para pesquisa em tabelas, ilustrando o uso de estruturas e pointers. ver prog64-chap06-pg118.c Problema típico do gerenciamento de uma tabela de símbolos. #define IN 1 com esta definição, o nome IN e o texto substituto 1 são armazenados em uma tabela. Mais além, quando IN aparecer em um comando do tipo state = IN; deverá ser substutuído por 1. Pesquisa em tabela (2) Há duas funções para manipular os nomes e textos substitutos: install(s,t) registra o nome s e o texto substituto t em uma tabela; s e t são cadeias de caracteres.. lookup(s) procura por s na tabela, e retorna um pointer para o local onde foi encontrado, ou NULL se s não se encontrava na tabela.. Usa-se um algoritmo hash e uma hashtable para inclusão do par nome/texto substituto na tabela, se o mesmo ainda não existir, ou para a sua substituição, em caso contrário. Pesquisa em tabela (3) No algoritmo da pesquisa, o nome de entrada é convertido (via a função hash) em um short int não negativo, que é então usado como um índice para um array de pointers. Um elemento do array aponta para o início de uma lista encadeada de blocos descrevendo os nomes que possuem aquele valor hash. Ele tem o valor NULL se nenhum nome possui aquele valor hash. Pesquisa em tabela (4) Um bloco na lista é uma estrutura contendo pointers para o nome, o texto substituto, e o próximo bloco na lista. Um pointer next igual a NULL sinaliza o fim da lista. Pesquisa em tabela (5) O vetor de pointers resume-se a: #define HASHSIZE 101 static struct nlist *hashtab[HASHSIZE]; /* pointer table */ A função hash, que é usada por lookup e install, gera um número n, tal que 0 <= n < HASHSIZE, para ser usado como índice em hashtab, o array de pointers. Não é a melhor função hash, porém é simples e eficiente. O tipo de retorno não sinalizado assegura que os valores gerados por hash serão não-negativos. Pesquisa em tabela (6) O processo de hash/pesquisa (lookup) produz um índice inicial no array hashtab; Se a cadeia já existe, ela estará na lista de blocos iniciando-se nesse índice. A pesquisa é realizada pela função lookup. Se lookup identifica que a cadeia já está presente ela retorna um pointer para a mesma; senão ela retorna NULL. install usa lookup para determinar se o nome sendo instalado já está presente; se estiver, a nova definição substituirá a antiga. Senão, uma nova entrada será criada. Install retorna NULL se, por qualquer razão, não há mais espaço para uma nova entrada. Typedef (1) C provê uma facilidade – typedef – para criar novos nomes para tipos de dados. typedef int Length; faz que o nome Length seja um sinônimo para int. O tipo Length pode ser usado em declarações, casts, etc., exatamente da mesma forma que o tipo int. Length len, maxlen; Length *lengths[]; Typedef (2) typedef char *String; torna String um sinônimo para char * ou pointer para uma cadeia de caracteres, podendo ser usado em declarações e casts: String p, lineptr[MAXLINES], alloc(int); int strcmp(String, String); p = (String) malloc(100); Sintaticamente, typedef é como as classes de armazenamento extern, static, etc. Inicia-se o nome dos typedef's com letras maiúsculas, a fim de destacá-los. Typedef (3) Uso de typedef's para a definição dos nodos de uma árvore ( vide definição da árvore em prog63-chap06-pg115.c) typedef struct tnode *Treeptr; typedef struct tnode { /* the tree node: */ char *word; /* points to the text */ int count; /* number of occurrences */ Treeptr left; /* left child */ Treeptr right; /* right child */ } Treenode; Esta declaração cria dois novas keywords para tipos: Treenode (uma estrutura) e Treeptr (um pointer para a estrutura). Typedef (4) A função talloc tornar-se-ia: Treeptr talloc(void) { return (Treeptr) malloc(sizeof(Treenode)); } Deve-se enfatizar que uma declaração typedef não cria um novo tipo em qualquer sentido; ela simplesmente fornece um novo nome para um tipo existente. Typedef (5) typedef não adiciona nova semântica ao C. As variáveis declaradas via typedef têm exatamente as mesmas propriedades das variáveis declaradas explicitamente. typedef é como #define, exceto que, pelo fato de ser interpretada pelo compilador, ela pode lidar com substituições textuais que vão além das capacidades do pré-processador. Por exemplo: typedef int (*PFI)(char *, char *); cria o tipo PFI, significando pointer para função de dois argumentos char * e que retorna um int. Typedef (6) O tipo PFI poder ser usado em contextos similares ao do programa de ordenação prog58-chap05-pg98.c PFI strcmp, numcmp; /* ao invés de */ int (*comp)(void *, void *) Há dois motivos principais para se usar typedef's: i) parametrizar um programa para resolver problemas de portabilidade. Se typedef's forem usados para tipos de dados dependentes da máquina, apenas os typedef's precisarão ser alterados. ii) melhorar a documentação do programa. Unions (1) Uma union é uma variável que pode conter (em momentos diferentes) objetos de diferentes tipos e tamanhos, com o compilador mantendo o controle dos requisitos de tamanho e alinhamento. Unions fornecem um meio para manipular diferentes tipos de dados em uma única área da memória, sem a necessidade de acrescentar qualquer informação dependente de máquina ao programa. São semelhantes aos variant records do Pascal. Unions (2) Exemplo: Um problema típico de gerenciamento da tabela de símbolos de um compilador. Supor que uma constante possa ser do tipo int, float ou um pointer para uma cadeia de caracteres. O valor da constante deve ser armazenado em uma variável do tipo apropriado, por outro lado, facilita o gerenciamento da tabela que o valor ocupe a mesma quantidade de memória e que seja guardado no mesmo lugar, independente do seu tipo. Esta é a finalidade da union. Unions (3) A sintaxe da union é baseada em structs. union u_tag { int ival; float fval; char *sval; } u; A variável u terá tamanho suficiente para conter o maior dos três tipos. O tamanho específico é dependente da implementação. Unions (4) Qualquer um desses tipos pode ser atribuído a u e depois usado em expressões, desde que o uso seja consistente: o tipo recuperado deve ser o tipo mais recentemente armazenado. A responsabilidade é do programador. O resultado depende da implementação, caso algo seja armazenado como um tipo e extraído como outro. Sintaticamente tem-se acesso aos membros de uma union, de forma similar aos membros de uma struct. union-name.member union-pointer->member Unions (5) Se a variável utype é usada para controlar o tipo correntemente armazenado em u, então em um trecho de um programa poder-se-ia encontrar: Unions (6) As unions podem ocorrer dentro de structs e arrays e viceversa. A notação para se ter acesso a membros de uma union em uma struct (ou vice-versa) é idêntica àquela das estruturas aninhadas. Unions (7) Referencia-se o membro ival da forma: symtab[i].u.ival Referencia-se o 10 caracter da cadeia sval por: symtab[i].u.sval /* ou alternativamente por */ symtab[i].u.sval[0] Ou seja, uma union é uma struct em que todos os membros possuem deslocamento zero a partir da base. A struct é grande o suficiente para conter o maior membro e o alinhamento é adequado a todos os tipos de union. Unions (8) As mesmas operações são permitidas em unions e structs: atribuição ou cópia como unidade, tomada de endereço, e acesso a membros. Uma union só pode ser inicializada com um valor compatível com o tipo de seu primeiro membro (no exemplo anterior, um int). O alocador de memória do capítulo 8 mostra como uma union pode ser usada para forçar uma variável a ser alinhada com a fronteira de uma área de armazenamento específica. Campos de bits (1) Em situações de escassez de memória pode ser necessário empacotar diversos objetos em uma única palavra de máquina. Um uso comum seria um conjunto de bits flags em aplicações como tabelas de símbolos em compiladores e formatos de dados impostos externamente, como interfaces para dispositivos de hardware. Imagine um fragmento do código de um compilador que manipula uma tabela de símbolos. Cada identificador em um programa possui certas informações associadas com ele, do tipo se é ou não uma keyword, external, static, etc. A forma mais compacta de codificar tais informações é um conjunto de flags de 1bit acondicionadas em um char ou int. Campos de bits (2) O usual é definir-se um conjunto de máscaras correspondentes às posições dos bits relevantes. #define KEYWORD 01 #define EXTERNAL 02 #define STATIC 04 /* alternativamente */ enum {KEYWORD = 01, EXTERNAL = 02, STATIC = 04}; Os números devem ser potência de 2. Para se ter acesso à informação precisa-se apenas manusear os bits com operações de deslocamento, complemento e máscara. Campos de bits (3) flags |= EXTERNAL | STATIC; ativaria os bits EXTERNAL e STATIC em flags, enquanto flags &= ~(EXTERNAL | STATIC); os desativaria. if ((flags & (EXTERNAL | STATIC)) == 0) ... seria verdadeiro se ambos os bits estivessem off. C oferece a possibilidade de se definir e ter acesso a campos diretamente dentro de uma palavra, ao invés de se usar os operadores lógicos bit a bit. Campos de bits (4) Um campo de bits (bit-field) é um conjunto adjacentes de bits dentro de uma única unidade de armazenamento definida pela implementação (palavra ou word). A sintaxe de definição e acesso aos campos é baseada em estruturas. A tabela de símbolos #define (slide 61) poderia ser substituída por: struct { unsigned int is_keyword : 1; unsigned int is_extern : 1; unsigned int is_static : 1; } flags; que define uma variável flag com três campos de 1 bit. O número após '':'' representam a largura do campo em bits. Campos de bits (5) Os campos são declarados como unsigned int para se ter certeza de serem quantidades não sinalizadas. Os campos individuais são referenciados da mesma forma que outros membros de estrutura: flags.is_keyword, flags.is_extern, etc. Os campos comportam-se como short int's e podem participar de expressões aritméticas, assim como outros inteiros. Os exemplos anteriores poderiam ser escritos como: flags.is_extern = flags.is_static = 1; /* para ativar os bits */ flags.is_extern = flags.is_static = 0; /* para desativá-los */ if (flags.is_extern == 0 && flags.is_static == 0) /* para testálos */ Campos de bits (6) Quase tudo a respeito de campos de bits depende da implementação, e.g., se um bit field pode ultrapassar o limite de uma palavra. Os bit fields não precisam ser nomeados. Campos não nomeados (só '':'' e o tamanho) são usados para preenchimento. O tamanho especial 0 pode ser usado para forçar o alinhamento no próximo limite de palavra. Campos são atribuídos da direita para a esquerda ou da esquerda para a direita (depende da máquina). Os campos de bits só podem ser declarados como ints. Especifique signed ou unsigned explicitamente, por questões de portabilidade. O operador & não pode ser aplicado aos campos de bits.