Referências, em C

De maneira geral existem duas maneiras de passarmos parâmetros para uma função: Por valor e por referência. A segunda maneira, em C, é conceitual, já que podemos apenas passar valores − meso que sejam valores de ponteiros. A idéia dos dois tipos de parâmetros é que o primeiro só tem visibilidade, para alteração, de dentro da própria função. O parâmetro passado deste jeito não aparece alterado para o chamador. No segundo tipo, a função altera o valor do parâmetro e essa mudança é aparente para o chamador. Por exemplo:

/* Passagem por valor */
void f(int x)
{
  x *= 2;
  ... faz alguma coisa aqui ...
}

/* Pasagem por referência */
void g(int *x)
{
  *x *= 2;
  ... faz alguma coisa aqui ...
}

No retorno de f() o valor de x continuará o mesmo que era para o chamador. Somente dentro da função a alteração de x é visível. Como se o parâmetro fosse (e é!) uma variável local da função. Na função g(), o chamador passa o ponteiro para uma variável do tipo int e, portanto, poderá ver as modificações que g() fez ao conteúdo apontado.

Neste ponto é importante notar que C++ tem um modificador que indica que um tipo é, na verdade, uma referência para outra variável. Basta adicionar & depois do tipo:

/* Pasagem por referência */
void g(int& x)
{
  x *= 2;
  ... faz alguma coisa aqui ...
}

Isto é a mesma coisa que a função g() anterior. Só que sem a notação de ponteiros.

Voltando ao C… A passagem por referência dá a grande vantagem de evitar criar cópias do mesmo objeto. A única cópia criada é a do próprio ponteiro, que é descartada assim que a função termina. Mas a passagem por referência nos cria um problema de organização: Quando podemos saber se a referência é somente uma maneira de evitar a cópia ou se ele é usada para que a função modifique, intencionalmente, a variável. Um exemplo do último tipo é este:

/* Aloca um buffer de tamanho 'size' e inicializa um ponteiro 'p',
   passado por referência, com o endereço do início do buffer.

   Modifica o tamanho buffer, se 'p' já aponta para um buffer antes 
   da chamada da rotina.

   Retorna 0 se OK ou -1 em caso de erro (e também 'seta' errno) */

int my_alloc(void **p, size_t size)
{
  void *tmpp;

  if ((tmpp = realloc(*p, size)) == NULL)
    return -1;

  *p = tempp;

  return 0;
}

No caso de ponteiros para void a intenção do programador, mesmo que não saibamos o conteúdo da rotina, pode parecer bastante explícito. Afinal, para quê pasar um ponteiro para um ponteiro do tipo void? Mas, em se tratando de ponteiros para char a coisa fica mais nebulosa. Basta ver a possível declaração da função main:

int main(int argc, char **argv);

Embora essa declaração não esteja errada, ela é enganadora. As especificações da linguagem nos dizem que a declaração mais “correta” seria:

int main(int argc, char *argv[]);

A diferença das declarações é que a última nos diz que o que estamos passando é um array de ponteiros para char. A primeira poderia muito bem ser uma referência para um ponteiro para char. A confusão vem do fato de que o nome da variável declarada como array é sempre, na verdade, um ponteiro para o primeiro item do array. Por isso as declarações são intercambiáveis. Hummm… nem sempre, como vimos aqui.

Para efeitos de organização é sempre bom declarar aquilo que é para ser visto como um array usando “[]” e aquilo que é visto para ser uma referência com a notação de ponteiros.

OBS: Algumas linguagens, notavelmente Java e C#, tentam resolver o problema com a seguinte regra: Parâmetros de tipos primitivos são sempre passados por valor e parâmetros que são objetos (ou arrays de qualquer tipo) são sempre passados por referência (o que significa ponteiros, por debaixo dos panos!). O motivo nada tem haver com organização, mas com o funcionamento do garbage collector. Tipos primitivos ou são parte do escopo de uma função e, por isso, são automaticamente jogados fora quando a função termina (via limpeza da pilha), ou fazem parte de um objeto, que o garbage collector pode jogar no lixo. Objetos, por sua vez, para não serem copiados para todo lado, exaurindo a capacidade do heap e criando sérios problemas de performance construindo cópias em cada chamada, mantém um contador de referência (é mais complicado que isso!). A cada vez que é uma referência é “criada”, adiciona 1 a esse contador. A cada vez que a referência é “liberada” subtrai 1… Quando o contador chega a zero o garbage collector pode marcar o objeto como “lixo”, se quiser.

Isso não resolve o velho problema de memory leakage. Existem cenários, bem comuns, onde o contador de referência jamais chega a zero! De fato, este é um dos principais problemas de Java e C# (N.A: E, lembre-se, C# é só o Java sem a sopa de letrinhas!).

E se você acha que isso é coisa somente do Java e do C#, está enganado… Desde o TURBO PASCAL até o famoso Delphi este recurso é usado para o tipo String, que é definido como:

typedef struct {
  int ref_count;
  int size;
  char buffer[];   /* A referência ao tipo sempre aponta para
                      o início deste buffer! */
} String;

A função StrLength(), da biblioteca do PASCAL é literalmente definida como:

function StrLength(S: String); assembler;
asm
  mov eax,[eax-4] { Pega o 'size' apontado pela referência. }
end.

E, sempre que uma string é passada como parâmetro o compilador gera código mais ou menos assim:

; Supondo que ESI aponta para a estrutura da string...
inc dword ptr [esi-8]     ; incrementa o contador referência.
mov eax,esi
call StrLength
...

; Dentro de StrLength, antes de limpar a pilha e retornar, 
; o compilador coloca:
dec dword ptr [esi-8]      ; decrementa o contador de referência.

O contador de referências está lá para satisfazer as necessidades de algumas APIs do Windows, como a Common Object Modules (COM), por exemplo. (N.A: O motivo pelo qual usei ESI, como exemplo do retorno da função, e não EAX, é que provavelmente o compilador fará uma cópia do ponteiro da referência passado no parâmetro, uma vez que EAX (o retorno) será sobrescrito).

Ainda, para efeito de informação, em C++ a declaração abaixo é perfeitamente válida (mas, para mim, soa ainda mais confusa):

int my_alloc(void*& p, size_t size)

Neste caso, p é uma referência para o tipo “ponteiro para void“.

Anúncios

3 comentários sobre “Referências, em C

  1. Frederico ao utilizar argumento da função int &x, justamente é o mesmo que fazer (&x[0]), porque na verdade o ponteiro x aponta para o endereço de memória de &x[0], então por isso permite q uma variável q não seja ponteiro, possa ser referenciado, poque o que importa é o endereço de memória passado como argumento para função, estou correto?

    1. Eduardo… foi, mais ou menos, isso que eu falei no artigo (acredito)… Ou seja, uma referência É um ponteiro, somente existe a conveniência de esconder a notação de ponteiro.

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s