Programação Imperativa

Aula 22

Sumário

Alocação dinâmica de memória


Visão mais detalhada da memória do computador

Quando vos falei da representação das variáveis na memória do computador, disse-vos que cada variável ficava guardada numa posição de memória. Por exemplo, se fizéssemos um programa que contivesse a seguinte declaração,

    int x = 9;

o x poderia ir parar à posição de memória E2 quando o programa fosse executado. Na realidade, as coisas não são bem assim. Cada posição de memória corresponde apenas a um byte e uma variável inteira ocupa geralmente 4 bytes. Ou seja, o número 9 não vai estar representado numa única posição de memória mas sim em 4 posições de memória (ex: E2, E3, E4 e E5). Aliás, aquilo que está nessas 4 posições de memória é a representação em binário do número 9. A figura que se segue dá esta visão mais detalhada da memória.

Apesar da variável x estar localizada nos endereços E2, E3, E4 e E5, costuma dizer-se que o endereço da variável x é E2. Isto é, o endereço de uma variável é o endereço do primeiro byte que a variável ocupa.

E os arrays? Como é que são guardados?

Se declararmos um array a de 10 inteiros, o compilador vai reservar um bloco de memória consecutivo que permita guardar esses 10 inteiros. Se um inteiro ocupar 4 bytes, o compilador terá de reservar um bloco de 40 bytes (por exemplo, do endereço E100 até ao endereço E139).

      a[0]  vai ocupar as posições E100, E101, E102, E103
      a[1]  vai ocupar as posições E104, E105, E106, E107
      a[2]  vai ocupar as posições E108, E109, E110, E111
      ...
      ...
      a[9]  vai ocupar as posições E136, E137, E138, E139

Do mesmo modo que dizemos que o endereço da variável x é E2, podemos dizer que o endereço do array a é E100. Isto é, o endereço do array é o endereço do primeiro byte que o array ocupa. De facto, quando declararmos:

      int a[10];

O nome a é o endereço da primeira posição do array. Por outras palavras,

      a é sinónimo de &a[0]

Como tal, é perfeitamente válido fazer coisas deste estilo,

      int a[10];
      int *p;

      p = a;
      *p = 4;         /* equivalente a dizer a[0] = 4 */

Alocação dinâmica de memória

Quando demos os arrays, disse-vos que tinham de declarar os arrays com uma dimensão máxima. Por exemplo, se quisessem fazer um programa para ordenar uma lista de n números, em que n seria um número a ser introduzido pelo utilizador, teriam de fazer qualquer coisa deste estilo:

    #include <stdio.h>

    #define N 10000

    main()
    {
      int a[N];  
      int n;  

      printf("Quantos números quer ordenar? ");
      scanf("%d", &n);
      if( n > N )
         printf("erro: a dimensão máxima do array é %d\n", N);
      else
         /* código para introduzir um array de n números e ordená-los */ 
         ...
    }

O programa acima tem a limitação de só funcionar quando n<=N. O truque que vos ensinei foi o de definir N como sendo um número muito grande e depois só usar parte do array. Esta solução tem a desvantagem de desperdiçar memória. Estamos a reservar um array de 10000 inteiros e muito provavelmente o número n introduzido pelo utilizador vai ser só 10 ou 20!

A alternativa a esta solução é requisitar a memória ao computador durante a própria execução do programa. A ideia é perguntar ao utilizador a dimensão do array. Se o utilizador introduzir 10, pedimos ao computador para nos dar um bloco de memória que permita guardar 10 inteiros. Deste modo, o array ocupa apenas o espaço que é estritamente necessário.

Na biblioteca stdlib.h existe uma função chamada malloc que permite requisitar ao computador n bytes de memória (malloc é uma abreviatura de memory alocation). Aqui vai o programa:

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

    main()
    {
      int *a;  
      int n;  

      printf("Quantos números quer ordenar? ");
      scanf("%d", &n);
      a = (int *) malloc( n * sizeof(int) );
      if( a == NULL )
       {
         printf("Erro: não há memória\n");
         exit(1);
       }
      /* código para introduzir um array de n números e ordená-los */ 
         ...
      free( a ); 
    }

Em vez de declaramos a variável a como sendo um array de inteiros, declaramos a como sendo um apontador para um inteiro. A ideia é que a vai ser o endereço do primeiro elemento do array.

Concentrem-se na linha do programa que tem o malloc. A linha tem um aspecto feio mas não é nada de complicado.

      a = (int *) malloc( n * sizeof(int) );

A função malloc tem um argumento que é o número de bytes de memória que pretendemos utilizar. Se quisermos guardar n inteiros temos de pedir n vezes o número de bytes que cada inteiro ocupa.

O número de bytes que cada inteiro ocupa é dado por sizeof(int) No caso geral, sizeof(T) dá o número de bytes que uma variável do tipo T ocupa.

Aquilo que o malloc retorna é o endereço do primeiro byte do bloco de memória que foi pedido. Caso não haja memória, o malloc retorna NULL.

O (int *) que aparece antes do malloc é necessário porque a variável a é um apontador para inteiro. No caso geral, se quisermos alocar memória para um array de n elementos de um tipo T temos de fazer assim:

      T *a;
      a = (T *) malloc( n * sizeof(T) );

Importante: Quando reservamos memória com malloc, devemos sempre libertar essa memória quando esta já não for mais necessária. Isso faz-se utilizando a função free. Uma vez feito o free, a memória pode ser reutilizada pelo sistema operativo.

O programa completo

Aqui vai o programa completo para ordenar um array de números.
    #include <stdio.h>
    #include <stdlib.h>
        
    main()
    {
      int *a;
      int i, k, m, min, temp, n;
          
      /* Alocar memória para guardar o array */
      printf("Quantos números quer ordenar? ");
      scanf("%d", &n);
      a = (int *) malloc( n * sizeof(int) );
      if( a == NULL )
        {
          printf("ERRO: nao ha memoria.\n");
          exit(1);
        }

      /* Receber os valores a ordenar */
      for (i=0; i<n; i++)
        {
          printf("%d.º numero -> ",i+1);
          scanf("%d",&a[i]);
        }
      
      /* Ordenar o array */   
      for( k=0; k<=n-1; k++ )
        {
          /* descobre o índice do mínimo em a[k], a[k+1], ..., a[n-1] */
          min = a[k];
          m = k;
          for( i=k; i<=n-1; i++ )
            if( a[i] < min )
              {
                min = a[i];
                m = i;
              }
          
          /* troca a[k] com a[m] */
          temp = a[k];
          a[k] = a[m];
          a[m] = temp;
        }
      
      /* Escrever os elementos do array ordenados */
      for( i=0; i<n; i++ )
        printf("%d ", a[i]);
      printf("\n");
    
      /* libertar a memória */
      free( a );
    }

O programa ainda podia ser melhorado mas vou deixar isso para a próxima aula.

Mais coisas sobre a memória...

Quando um programa é executado, o sistema operativo coloca o código do programa (as instruções em linguagem máquina) na memória do computador. Além das instruções, o sistema operativo também reserva memória para as variáveis que o vosso programa vai utilizar.

O mais interessante de tudo é que o vosso programa não é o única coisa que está na memória do computador. Enquanto o vosso programa está a ser executado, também podem estar a correr um editor de texto (ex: emacs) e um browser (ex: Netscape) para aceder à Internet. O emacs e o Netscape também são programas (ver curiosidade no final da página) e também ocupam memória. E isto não fica por aqui. Pode haver vários utilizadores, cada qual a correr vários programas no mesmo computador.

Resumindo, a memória do computador é um recurso que é partilhado pelos vários programas dos vários utilizadores. Quem se encarrega de fazer a gestão da memória é o sistema operativo (o Linux no nosso caso).

O próprio sistema operativo também é um programa (mais correctamente, é um conjunto de programas) e ele próprio também está a ocupar parte da memória do computador.

Quando a memória do computador começa a ficar cheia, o sistema operativo pode utilizar parte do disco para servir de memória RAM. A desvantagem disso é que o tempo de acesso ao disco é muito menor do que o tempo de acesso à memória RAM. É por essa razão que por vezes os computadores que vocês utilizam nas aulas práticas ficam lentos.

Curiosidades:

O primeiro browser gráfico da Web chamava-se Mosaic e foi feito em 1993 por Marc Andreesen, na altura tinha 22 anos e era estudante na Universidade de Illinois. Passados uns meses, Marc Andreesen terminou o curso e formou uma empresa chamada Netscape.

Aquilo que vocês aprendem nesta disciplina é apenas o aieou da programação. Ainda vos resta muito para aprender. Se estudarem muito e forem dedicados, um dia poderão ser capazes de escrever programas como o Netscape e o Linux.