Artigo original: Structured Data Types in C Explained

‌            

Existem variáveis de diferentes tipos na linguagem C, como os ints, chars e floats. Todos permitem o armazenamento de dados.

Também temos os arrays, que permitem o agrupamento de itens de dados que possuem o mesmo tipo.

Porém, na realidade, nem sempre vamos ter o luxo de possuir dados de apenas um tipo. É nesse cenário que utilizamos as estruturas. Neste artigo, vamos aprender mais sobre tipos de dados estruturados em C.

Índice

A. Fundamentos

  1. Definição e declaração
  2. Inicialização e acesso aos membros de uma estrutura
  3. Operações com uma variável de estrutura
  4. Array de uma estrutura
  5. Estruturas aninhadas

B. Alocação de memória

  1. Alinhamento de dados
  2. Preenchimento de estruturas
  3. Alinhamento de membro de estrutura
  4. Empacotamento de estrutura

C. Ponteiros

  1. Ponteiro como um membro
  2. Ponteiro para estrutura
  3. Ponteiro e array de estrutura

D. Funções

  1. Função como membro
  2. Estrutura como argumento de função
  3. Estrutura como retorno de uma função

E. Estrutura autorreferencial

F. Conclusão

Vamos começar!

Fundamentos

1. Definição e declaração

Uma estrutura é uma coleção de uma ou mais variáveis, possivelmente de tipos distintos, agrupadas em um nome comum. É um tipo de dado definido pelo usuário.

As estruturas auxiliam na organização de dados complicados em programas grandes, já que permitem com que um grupo de variáveis logicamente associadas entre si sejam tratadas desse modo.

Por exemplo, um estudante pode ter as propriedades nome, idade, sexo e notas. Podemos criar um array do tipo char para a variável nome, um int para matrícula, um char para sexo e um array do tipo int para as notas.

Porém, se existem 20 ou 100 estudantes, vai ser difícil lidar com essas variáveis.

Podemos declarar uma estrutura utilizando a palavra reservada struct de acordo com a sintaxe abaixo:

 /* Sintaxe */
 struct nomeEstrutura
        {
            tipoDado variavelMembro1;
            tipoDado variavelMembro2;
            ...
        };

 /* Exemplo */
struct estudante
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    };

O código acima define um novo tipo de dado, struct estudante. Cada variável pertencente a esse tipo de dado vai possuir um nome[20], matricula, sexo e notas[5]. Essas propriedades citadas são os membros da estrutura.

Uma vez que a estrutura é declarada como um novo tipo de dado, podemos criar as variáveis que possuem o tipo dessa estrutura.

 /* Declaração de variável */
struct nomeEstrutura variavelEstrutura;

 /* Exemplo /*
struct estudante est1;
struct estudante est2,est3,est4;

Cada variável de struct estudante tem sua própria cópia das propriedades membro.

Algumas informações importantes:

  1. Os membros da estrutura não ocupam memória alguma até que uma variável de estrutura seja criada.
  2. Você já deve ter notado que estamos usando a palavra struct para declarar a variável também. Não é um pouco tedioso?

Se utilizarmos a palavra reservada typedef na declaração da estrutura, podemos evitar a repetição da palavra struct.

typedef struct estudantes
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    } ESTUDANTE; 

/* ou */
typedef struct
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    } ESTUDANTE; 


ESTUDANTE est1,est2,est3,est4;

Como convenção, são utilizadas letras maiúsculas para definição de tipos (como ESTUDANTE).

3. A definição de estrutura e declaração de variável podem ser combinadas desta maneira:

struct estudante
    {
        char nome[20];
        int matricula;
        chat sexo;
        int notas[5];
    }est1, est2, est3, est4;

4. O uso de nomeEstrutura para declarar a estrutura é opcional. O código abaixo, por exemplo, é totalmente válido.

struct
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    }est1, est2, est3, est4;

5.  Estruturas geralmente são declaradas no começo do arquivo do código-fonte, antes mesmo das definições das funções (você já vai ver o motivo).

6.  A linguagem C não permite a inicialização de uma variável dentro da declaração de uma estrutura.

2. Inicialização e acesso aos membros de uma estrutura

Como qualquer outra variável, uma variável de estrutura também pode ser inicializada na mesma linha onde foi declarada.

 /* Inicialização de variável */
nomeEstrutura nomeVariavel = { valor1, valor2,...};

 /* Exemplo */
typedef struct
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    }ESTUDANTE;

void main(){
ESTUDANTE est1 = { "Alex", 43, 'M', {76, 78, 56, 98, 92}};
ESTUDANTE est2 = { "Max", 33, 'M', {87, 84, 82, 96, 78}};
}

Para acessar os membros, precisamos usar o . (operador de ponto)

 /* Acessando os membros de uma estrutura */
variavelEstrutura.membroVariavel;

/* Exemplo */
 printf("Nome: %s\n", est1.nome);
 printf("Matricula: %d\n", est1.matricula);
 printf("Sexo: %c\n", est1.sexo);
 for( int i = 0; i < 5; i++)
   printf("Notas na matéria n°%d: %d\n", i, est1.notas[i]);

/* Saída */
Nome: Alex
Matricula: 43
Sexo: M
Notas na matéria n°0: 73
Notas na matéria n°1: 78
Notas na matéria n°2: 56
Notas na matéria n°3: 89
Notas na matéria n°4: 92

Os membros podem ser inicializados na declaração da variável em qualquer ordem, apenas utilizando o ..

ESTUDANTE est3 = { .sexo = 'M', .matricula = 23, .nome = "Gasly", .notas = { 99, 45, 67, 78, 94}};

Também podemos inicializar os primeiros membros e deixar os membros restantes vazios. Contudo, os membros que não foram inicializados devem estar somente no final da lista.

Inteiros e números de ponto flutuante que não foram inicializados tem um valor padrão 0. Enquanto que caracteres e strings não inicializados possuem o valor padrão \0 (NULL) .

ESTUDANTE est4 = { "Mário", 65};
 /* Equivalente a { "Mario", 65, '\0', { 0, 0, 0, 0, 0} } */

3. Operações com a variável de estrutura

Ao contrário das variáveis de tipos primitivos, não podemos utilizar operadores aritméticos como +, -, *, / para variáveis de estrutura. Também não é possível o uso de operadores relacionais e de igualdade.

Contudo, podemos copiar uma variável de estrutura para outra, desde que pertençam à mesma estrutura.

 /* Operações inválidas */
est1 + est2
est1 - est2
est1 == est2
est1 != est2

 /* Operação válida */
est1 = est2

Para comparar as variáveis de estrutura, vamos comparar os membros de cada uma individualmente.

#include <stdio.h>
#include <string.h>

 struct estudante
    {
        char nome[20];
        double matricula;
        char sexo;
        int notas[5];
    }est1,est2;


void main()
{
    struct estudante est1= { "Alex", 43, 'M', {76, 78, 56, 98, 92}};
    struct estudante est2 = { "Max", 33, 'M', {87, 84, 82, 96, 78}};

    if( strcmp(est1.nome,est2.nome) == 0 && est1.matricula == est2.matricula)
        printf("Os dois dados pertencem ao mesmo estudante.\n");
    else printf("Dados diferentes, estudantes diferentes.\n");

     /* Copiando a variável de estrutura */
    est2 = est1;

    if( strcmp(est1.name,est2.name) == 0 && est1.matricula == est2.matricula)
        printf("Os dois dados pertencem ao mesmo estudante.\n");
    else printf("Dados diferentes, estudantes diferentes.\n");
}

 /* Saida */
Os dois dados pertencem ao mesmo estudante.

Dados diferentes, estudantes diferentes.

4. Array de uma estrutura

Você deve ter percebido que criamos 4 variáveis diferentes do tipo struct student para armazenar os dados de 4 estudantes.

Uma maneira melhor de armazenar esses dados seria criar um array de struct student (da mesma maneira que um array de ints )

struct estudante
    {
        char nome[20];
        double matricula;
        char sexo;
        int notas[5];
    };

struct estudante estu[4];

Para acessar os elementos do array estu e os membros de cada elemento, podemos utilizar laços de repetição (loops).

 /* Registrando valores para os estudantes */

for(int i = 0; i < 4; i++)
    {
        printf("Digite o nome:\n");
        scanf("%s",&estu[i].nome);
        printf("Digite a matrícula:\n");
        scanf("%d",&estu[i].matricula);
        printf("Digite o sexo:\n");
        scanf(" %c",&estu[i].sexo);

        for( int j = 0; j < 5; j++)
        {
            printf("Digite as notas da matéria n°%d:\n",j);
            scanf("%d",&estu[i].notas[j]);
        }

        printf("\n-------------------\n\n");
    }

 /* Calculando a média de notas e imprimindo o resultado */

for(int i = 0; i < 4; i++)
    {
        float soma = 0;
        for( int j = 0; j < 5; j++)
        {
            soma += estu[i].notas[j];
        }

        printf("Nome: %s\nMédia = %.2f\n\n", estu[i].nome,soma/5);
    }

5. Estruturas aninhadas

Aninhar uma estrutura significa ter uma ou mais variáveis de estrutura dentro de uma outra estrutura. Do mesmo modo com que declaramos um membro do tipo int ou um membro do tipo char, podemos também declarar uma variável de estrutura como um membro.

struct nascimento
    {
       int dia;
       int mes;
       int ano;
    };

struct estudante
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
        struct nascimento dataNascimento;
    };

void main(){
 struct estudante estu1;

A variável de estrutura dataNascimento de tipo struct nascimento está aninhada dentro de struct estudante. É importante frisar que você não consegue aninhar uma variável de estrutura do tipo struct estudante dentro de struct estudante.

Perceba que a estrutura que vai ser aninhada deve ser declarada antes. Usando o ., podemos acessar os membros contidos dentro da estrutura interna, assim como os outros membros.

 /* Exemplo */
estu1.dataNascimento.dia
estu1.dataNascimento.mes
estu1.dataNascimento.ano
estu1.nome

Variáveis de estrutura de diferentes tipos podem ser aninhadas.

struct nascimento
    {
       int dia;
       int mes;
       int ano;
    };
 
struct parentesco
    {
        char nomePai[20];
        char nomeMae[20];
    };

struct estudante
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
        struct nascimento dataNascimento;
        struct parentesco pais;
    };

Alocação de memória

Quando uma variável de estrutura de um certo tipo é declarada, os membros da estrutura são alocados em memória de maneira contígua (adjacente).

struct estudante
    {
        char nome[20];
        int matricula;
        char sexo;
        int notas[5];
    } estu1;

Aqui, haverá memória sendo alocada para nome[20, seguido de matricula, sexo, e notas[5]. Isso implica que o tamanho de estu1 ou de struct estudante será a soma do tamanho dos membros, certo? Vamos dar uma olhada.

void main()
{
    printf("Soma do tamanho dos membros = %I64d bytes\n", sizeof(estu1.nome) + sizeof(estu1.matricula) + sizeof(estu1.sexo) + sizeof(estu1.notas));
    printf("Usando o operador sizeof() = %I64d bytes\n",sizeof(estu1));
}

 /* Saída */
Soma do tamanho dos membros =  45 bytes
Usando o operador sizeof() = 48 bytes
Já que o operador sizeof() retorna um long long unsigned int, utilize %I64D para especificar o formato. Você talvez precise usar %llu ou lld, dependendo do seu compilador.
Usar %d retornará um aviso -  format '%d' expects argument of type 'int', but argument 2 has type 'long long unsigned int'. (formato '%d' pede um argumento do tipo 'int', mas o argumento 2 tem tipo 'long long unsigned int').

Utilizando o operador sizeof() na variável de estrutura, tivemos 3 bytes a mais do que a soma do tamanho dos membros. Por que? Onde estão esses 3 bytes na memória?

Vamos responder à segunda pergunta primeiro. Podemos imprimir os endereços de memória dos membros para achar estes 3 bytes.

void main()
{
    printf("Endereço do membro 'nome' = %d\n", &estu1.nome);
    printf("Endereço do membro 'matricula' = %d\n", &estu1.matricula);
    printf("Endereço do membro 'sexo' = %d\n", &estu1.sexo);
    printf("Endereço do membro 'notas' = %d\n", &estu1.notas);
}

 /* Saída */
Endereço do membro 'nome' = 4225408
Endereço do membro 'matricula' = 4225428
Endereço do membro 'sexo' = 4225432
Endereço do membro 'notas' = 4225436

Podemos ver que o array notas[5], ao invés de estar sendo alocado no endereço 4225433, está no endereço 4224536. Por que isso ocorre?

1. Alinhamento de dados

Antes de darmos uma olhada no alinhamento de dados, é importante entender como o processador lê dados da memória.

Um processador lê uma palavra em um ciclo. Essa palavra possui 4 bytes para um processador de 32 bits e 8 bytes para um de 64 bits. Quanto menor o número de ciclos, melhor o desempenho da CPU.

Um modo de alcançar um menor número de ciclos é por meio do alinhamento de dados. Na prática, alinhar é quando uma variável de tamanho t de um tipo primitivo qualquer é armazenada em um endereço que é múltiplo de t.  Esse processo ocorre por padrão.

Endereços alinhados para certos tipos de dados

Tipos de dados Tamanho (em bytes) Endereço
char 1 múltiplo de 1
short 2 múltiplo de 2
int, float 4 múltiplo de 4
double, long, * (ponteiros) 8 múltiplo de 8
long double 16 múltiplo de 16

2. Preenchimento de estruturas

Talvez você precise inserir alguns bytes extras entre os membros da estrutura para poder alinhar dados. Esses bytes extras são conhecidos como preenchimento (padding).

No código acima, os 3 bytes acrescentados foram o preenchimento. Sem este preenchimento, notas[0], que é de tipo int (endereço múltiplo de 4), teria seu endereço como 4225433 (não múltiplo de 4).

Você provavelmente já deve ter percebido por que estruturas não podem ser comparadas diretamente.

3. Alinhamento de membro de estrutura

Para explicar este tópico, vamos para outro exemplo (você vai entender o por quê).

struct exemplo
    {
        int i1;
        double d1;
        char c1;
        
    } exemplo1;

void main()
{
    printf("tamanho = %I64d bytes\n",sizeof(exemplo1));
}

Qual seria a saída desse programa? Vamos aplicar nossos conhecimentos.

i1 possui 4 bytes. Essa variável vai ser seguida de um preenchimento de 4 bytes porque o endereço de d1 deve ser divisível por 8.

Isso será seguido de 8 e 1 byte respectivamente para d1 e c1. Logo, a saída deve ser 4 + 4 + 8 + 1 = 17 bytes.

 /* Saída */
tamanho = 24 bytes

O quê?! Errado de novo! Por quê? Através de um array de tipo struct exemplo, podemos entender melhor. Também vamos imprimir o endereço dos membros de exemplo2[0].

void main()
{
    struct exemplo exemplo2[2];
    printf("Endereço de exemplo2[0].i1 = %d\n", &exemplo2[0].i1);
    printf("Endereço de exemplo2[0].d1 = %d\n", &exemplo2[0].d1);
    printf("Endereço de exemplo2[0].c1 = %d\n", &exemplo2[0].c1);

}

 /* Saída */
Endereço de exemplo2[0].i1 = 4225408
Endereço de exemplo2[0].d1= 4225416
Endereço de exemplo2[0].c1 = 4225424

Vamos supor que o tamanho de exemplo2[0] é 17 bytes. Isso implica que o endereço de exemplo2[1].i1 será 4225425. Isso não é possível já que o endereço de um int deve ser múltiplo de 4.

Logicamente, um endereço possível para exemplo2[1].i1 pode ser 4225428, um múltiplo de 4.

Isso também está errado. Você sabe o por quê? O endereço de exemplo2[1].d1 agora será (28 + 4 (i1) + 3 (preenchimento)) 4225436, que não é um múltiplo de 8.

Para que possamos evitar o desalinhamento, o compilador aplica alinhamento em toda estrutura. Isso é feito ao adicionar bytes extra após o último membro, processo conhecido como alinhamento de membro de estrutura.

No exemplo que discutimos no começo desta seção, isso não era necessário (por isso, precisamos de outro exemplo.)

Uma maneira simples de lembrar deste conceito é por meio desta regra: o endereço e tamanho da estrutura devem ser múltiplos de t_max, onde t_max é o tamanho máximo que um membro da estrutura ocupa.

No caso de struct exemplo, 8 bytes é o tamanho máximo de d1. Portanto, haverá um preenchimento de 7 bytes no fim da estrutura, fazendo com que seu tamanho seja de 24 bytes.

Seguindo as duas regras abaixo, você pode facilmente encontrar o tamanho de qualquer estrutura:

  1. Todo tipo de dado armazena seu valor em um endereço que é múltiplo de seu tamanho.
  2. Toda estrutura ocupa um tamanho que é múltiplo do tamanho máximo de bytes que um membro ocupa.

Apesar de sermos capazes de diminuir os ciclos de uma CPU, existe uma quantidade significativa de memória que será desperdiçada.

Uma maneira de diminuir a quantidade de preenchimento para o mínimo possível é declarando os membros em ordem decrescente de tamanho.

Se aplicarmos este método em struct exemplo, o tamanho da estrutura é reduzido para 16 bytes. O preenchimento é reduzido de 7 para 3 bytes.

struct exemplo
    {
        double d1; 
        int i1;
        char c1;
        
    } exemplo3;

void main()
{
    printf("tamanho = %I64d bytes\n",sizeof(exemplo3));
}

 /* Saída */
tamanho = 16 bytes

4. Empacotamento de estrutura

Empacotar (packing) é o oposto de preencher. Empacotar previne o compilador de preencher e remove a memória não alocada.

No caso do Windows, podemos usar a diretiva #pragma pack, que especifica o empacotamento de membros de estrutura.

#pragma pack(1)

struct exemplo
    {
        double d1; 
        int i1;
        char c1;
        
    } exemplo4;

void main()
{
    printf("tamanho = %I64d bytes\n",sizeof(exemplo4));
}

 /* Saída */
tamanho = 13 bytes

Isso garante que os membros da estrutura estejam alinhados em um limite de 1 byte. Em outras palavras, o endereço de qualquer tipo de dado deve ser um múltiplo ou de 1 byte ou do seu tamanho (depende de qual for o menor).

Ponteiros

Se quiser repassar o conteúdo sobre ponteiros antes de seguir em frente com o artigo, aqui está um link que cobre ponteiros de maneira aprofundada (texto em inglês).

1. Ponteiro como um membro

Uma estrutura pode ter ponteiros como membros.

struct estudante
    {
        char *nome;
        int *matricula;
        char sexo;
        int notas[5];
    };

void main()
{   int alexMatricula = 44;
   struct estudante estu1 = { "Alex", &alexMatricula, 'M', { 76, 78, 56, 98, 92 }};
}

Utilizando o . (operador ponto), podemos novamente acessar os membros. Já que matricula agora tem o endereço de alexMatricula, teremos que desreferenciar estu1.matricula (e não estu1.(*matricula)) para obter o valor

 printf("Nome: %s\n", estu1.nome);
   printf("Matrícula: %d\n", *(estu1.matricula));
   printf("Sexo: %c\n", estu1.sexo);

   for( int i = 0; i < 5; i++)
    printf("Notas na matéria %d: %d\n", i, estu1.notas[i]);

 /* Saída */
Nome: Alex
Matrícula: 43
Sexo: M
Notas na matéria 0: 76
Notas na matéria 1: 78
Notas na matéria 2: 56
Notas na matéria 3: 98
Notas na matéria 4: 92

2. Ponteiro para estrutura

Assim como ponteiros de inteiros, ponteiros de arrays e ponteiros de funções, temos ponteiros para estruturas também.

struct estudante {
    char nome[20];
    int matricula;
    char sexo;
    int notas[5];
};

struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};

struct estudante*ptrEstu1 = &estu1;

Declaramos o ponteiro ptrEstu1 do tipo struct estudante. Definimos o endereço de estu1 para ptrEstu1.

ptrEstu1 armazena o endereço base de estu1, que é o endereço base do primeiro membro da estrutura. Incrementar em 1 aumentaria o endereço em sizeof(estu1) bytes.

printf("Endereço da estrutura = %d\n", ptrEstu1);
printf("Endereço do membro `nome` = %d\n", &estu1.nome);
printf("Incrementar em 1 resulta em %d\n", ptrEstu1 + 1);

/* Saída */
Endereço da estrutura = 6421968
Endereço do membro `nome` = 6421968
Incrementar em 1 resulta em 6422016

Podemos acessar os membros de estu1 usando ptrEstu1 de duas formas. Usando * (operador de indireção) ou usando -> (operador infix ou arrow).

Com *, continuaremos usando o . (operador ponto) enquanto que com -> não precisaremos do operador de ponto.

printf("Nome sem usar ptrEstu1 : %s\n", estu1.nome);
printf("Nome usando ptrEstu1 e * : %s\n", (*ptrEstu1).nome);
printf("Nome usando ptrEstu1 e -> : %s\n", ptrEstu1->nome);

/* Output */
Nome sem usar ptrEstu1 : Alex
Nome usando ptrEstu1 e * : Alex
Nome usando ptrEstu1 e -> : Alex

De modo similar, podemos acessar e modificar outros membros também. Note que os parênteses são necessários enquanto utilizamos o * já que o operador de ponto (.) tem procedência maior que *.

3. Ponteiro e array de estrutura

Podemos criar um array do tipo struct estudante e usar um ponteiro para acessar os elementos e seus membros.

struct estudante estu[10];

 /* Ponteiro para o primeiro elemento (estrutura) do array */
struct estudante *ptrEstu_tipo1 = estu;

 /* Ponteiro para um array de tamanho 10 do tipo struct estudante */
struct estudante (*ptrEstu_tipo2)[10] = &estu;

Note que ptrEstu_tipo1 é um ponteiro para estu[0], enquanto que ptrEstu_tipo2 é um ponteiro para o array inteiro de tamanho 10 e tipo struct estudante. Adicionar 1 a ptrEstu_tipo1 apontaria para estu[1].

Podemos usar ptrEstu_tipo1 com um laço de repetição (ou loop) para percorrer os elementos e seus membros.

for( int i = 0; i <  10; i++)
printf("%s, %d\n", ( ptrEstu_tipo1 + i)->nome, ( ptrEstu_tipo1 + i)->matricula);

Funções

1. Função como membro

Funções não podem ser membro de uma estrutura. Contudo, utilizando ponteiros de função, podemos chamar funções utilizando .. Apenas tenha em mente que isso não é recomendado.

 struct exemplo
    {
        int i;
        void (*ptrMensagem)(int i);


    };

void mensagem(int);

void mensagem(int i)
{
    printf("Olá, eu sou um membro de uma estrutura. Esta estrutura também tem um inteiro de valor %d", i);
}

void main()
{
    struct exemplo eg1 = {6, mensagem};
    eg1.ptrMensagem(eg1.i);
}

Declaramos dois membros, um inteiro i e um ponteiro de função ptrMensagem dentro de struct exemplo. O ponteiro de função aponta para uma função que recebe um inteiro e retorna void.

mensagem é esta função. Inicializamos eg1 com valor 6 e mensagem. Depois usamos . para chamar a função usando ptrMensagem e passar como valor eg1.i.

2. Estrutura como argumento de função

Assim como variáveis, podemos passar membros individuais de estruturas como argumentos.

#include <stdio.h>

struct estudante {
    char nome[20];
    int matricula;
    char sexo;
    int notas[5];
};

void display(char a[], int b, char c, int notas[])
{
    printf("Nome: %s\n", a);
    printf("Matrícula: %d\n", b);
    printf("Sexo: %c\n", c);

    for(int i = 0; i < 5; i++)
        printf("Notas na matéria %d: %d\n",i,notas[i]);
}
void main()
{
    struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};
    display(estu1.nome, estu1.matricula, estu1.sexo, estu1.notas);
}

 /* Saída */
Nome: Alex
Matrícula: 43
Sexo: M
Notas na matéria 0: 76
Notas na matéria 1: 98
Notas na matéria 2: 68
Notas na matéria 3: 87
Notas na matéria 4: 93

Note que a estrutura struct estudante é declarada dentro de main(), logo no começo. Isto é para assegurar que ela estará disponível globalmente e que display() possa usá-la.

Se a estrutura for definida dentro de main(), seu escopo estará limitado somente à main().

Passar membros de estrutura como argumento não é muito eficiente quando temos um número grande deles. Então, variáveis de estrutura podem ser passadas para uma função.

void display(struct estudante a)
{
    printf("Nome: %s\n", a.nome);
    printf("Matrícula: %d\n", a.matricula);
    printf("Sexo: %c\n", a.sexo);

    for(int i = 0; i < 5; i++)
        printf("Notas na matéria %d: %d\n",i,a.notas[i]);
}
void main()
{
    struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};
    display(estu1);
}

Se o tamanho da estrutura é muito grande, então passar uma cópia dela será ineficiente. Poderíamos passar um ponteiro de estrutura para uma função. Nesse caso, o endereço da estrutura será passado como um argumento de fato.

void display(struct estudante *p)
{
    printf("Nome: %s\n", p->nome);
    printf("Matrícula: %d\n", p->matricula);
    printf("Sexo: %c\n", p->sexo);

    for(int i = 0; i < 5; i++)
        printf("Notas na matéria %d: %d\n",i,p->notas[i]);
}
void main()
{
    struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};
    struct estudante *ptrEstu1 = &estu1;
    display(ptrEstu1);
}

Passar um array de estrutura para uma função é semelhante a passar um array de qualquer tipo a uma função. O nome do array, que é o endereço base do array da estrutura, é passado à função.

void display(struct estudante *p)
{   
    for( int j = 0; j < 10; j++)
   {
       printf("Nome: %s\n", (p+j)->nome);
        printf("Matrícula: %d\n", (p+j)->matricula);
        printf("Sexo: %c\n", (p+j)->sexo);

        for(int i = 0; i < 5; i++)
        printf("Notas na matéria %d: %d\n",i,(p+j)->notas[i]);
   }
}

void main()
{
    struct estudante estu1[10];
    display(estu1);
}

3. Estrutura como retorno de função

Podemos retornar uma variável de estrutura, assim como qualquer outra variável.

#include <stdio.h>

struct estudante {
    char nome[20];
    int matricula;
    char sexo;
    int notas[5];
};


struct estudante incrementaEm5(struct estudante p)
{
    for( int i =0; i < 5; i++)
        if(p.notas[i] + 5 <= 100)
           {
               p.notas[i]+=5;
           }
    return p;
}

void main()
{
    struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};
    estu1 = incrementaEm5(estu1);
    
    printf("Nome: %s\n", estu1.nome);
    printf("Matrícula: %d\n", estu1.matricula);
    printf("Sexo: %c\n", estu1.sexo);
    
     for(int i = 0; i < 5; i++)
        printf("Nota na matéria %d: %d\n",i,estu1.notas[i]);
}

 /* Saída */
Nome: Alex
Matrícula: 43
Sexo: M
Notas na matéria 0: 81
Notas na matéria 1: 98
Notas na matéria 2: 73
Notas na matéria 3: 92
Notas na matéria 4: 98

A função incrementaEm5() soma 5 à todas as notas de matérias que, após a soma, sejam menores ou iguais a 100. Note que o tipo de retorno é uma variável de estrutura do tipo struct estudante.

Enquanto retorna um membro de estrutura, o tipo de retorno deve ser o mesmo que o do membro.

Um ponteiro de estrutura também pode ser retornado por uma função.

#include <stdio.h>
#include <stdlib.h>

struct retangulo {
    int tamanho;
    int amplitude;
};

struct retangulo* function(int tamanho, int amplitude)
{
    struct retangulo *p  = (struct retangulo *)malloc(sizeof(struct retangulo));
     p->tamanho = tamanho;
     p->amplitude = amplitude;
    return p;
}

void main()
{
    struct retangulo *retangulo1 = function(5,4);
    printf("Tamanho do retangulo = %d unidades\n", retangulo1->tamanho);
    printf("Amplitude do retangulo = %d unidades\n", retangulo1->amplitude);
    printf("Área do retangulo = %d unidades quadradas\n", retangulo1->length * retangulo1->amplitude);
}

 /* Saída */
Tamanho do retangulo = 5 unidades
Amplitude do retangulo = 4 unidades
Area do retangulo = 20 unidades quadradas

Note que alocamos a memória de tamanho struct retangulo de forma dinâmica usando malloc(). Já que essa função retorna um ponteiro void, temos que utilizar typecast para transformar este retorno em um ponteiro do tipo struct retangulo.

Estrutura autorreferencial

Vimos que ponteiros podem ser um membro de uma estrutura também. O que ocorre, porém, se o ponteiro for um ponteiro de estrutura? O ponteiro pode ser tanto do mesmo tipo da estrutura ou diferente.

Estruturas autorreferenciais são aquelas que possuem ponteiro(s) de estrutura do mesmo tipo do seu(s) membro(s).

struct estudante {
    char nome[20];
    int matricula;
    char sexo;
    int notas[5];
    struct estudante *next;
 };

Essa é uma estrutura autorreferencial onde next é um ponteiro de estrutura do tipo struct estudante.

Agora, vamos criar duas estruturas de variáveis estu1 e estu2 e inicializá-las. Depois, vamos armazenar o endereço de estu2 no membro next de estu1.

void main()
{
    struct estudante estu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}, NULL};
    struct estudante estu2 = { "Max", 33, 'M', {87, 84, 82, 96, 78}, NULL};
    estu1.next = &estu2;
}

Isso nos permite acessar os membros de estu2 usando estu1 e next.

void main()
{
    printf("Nome: %s\n", estu1.next->nome);
    printf("Matrícula: %d\n", estu1.next->matricula);
    printf("Sexo: %c\n", estu1.next->sexo);

    for(int i = 0; i < 5; i++)
        printf("Notas na matéria %d: %d\n",i,estu1.next->notas[i]);
}

 /* Saída */
Nome: Max
Matrícula: 33
Sexo: M
Notas na matéria 0: 87
Notas na matéria 1: 84
Notas na matéria 2: 82
Notas na matéria 3: 96
Notas na matéria 4: 78

Suponha que queremos uma estrutura de variável diferente após stu1, ou seja, inserir outra estrutura de variável entre estu1 e estu2. Isso pode ser feito facilmente.

void main()
{
    struct estudante estuBetween = { "Gasly", 23, 'M', {83, 64, 88, 79, 91}, NULL};
    est1.next = &estuBetween;
    estuBetween.next = &estu2;
}

Agora estu1.next armazena o endereço de estuBetween. E estuBetween.next tem o endereço de estu2. Podemos, agora, acessar todas as três estruturas usando estu1.

  printf("Matrícula de %s: %d\n", estu1.next->nome, estu1.next->matricula);
    printf("Sexo de %s: %c\n", estu1.next->next->nome, estu1.next->next->sexo);

 /* Saída */
Matrícula de Gasly: 23
Sexo de Max: M

Note como formamos uma ligação entre estu1, estuBetween e estu3. O que discutimos aqui foi o começo de uma Lista Ligada (Linked List)

Estruturas autorreferenciais são muito úteis na criação de estruturas de dados como a própria lista ligada, pilhas, filas, grafos e daí por diante.

Conclusão

Feito! Abordamos tudo, desde a definição do que é uma estrutura, até o uso de estruturas autorreferenciais.

Tente revisar todos os subtópicos que você ler. Se conseguir lembrá-los, parabéns! Leia os que não conseguir lembrar novamente.

O próximo passo lógico seria aprender mais sobre listas ligadas e outras várias estruturas de dados que foram usadas aqui.

Continue aprendendo.