Retorno de Ponteiro para Variável Local

por Fabio A. Mazzarino

Tá aí uma falha que pode causar problemas intermitentes e consequentemente muitas horas de desenvolvimento para corrigi-la. Vamos aprender a identificá-la e como evitar esse problema.

Abertura de Escopo

Tem muito programador C/C++ por aí que não sabe como abrir um escopo novo. Basicamente qualquer par de chaves abre um novo escopo em C/C++, e as variáveis ali declaradas somente tem validade dentro desse escopo. Em tempo de compilação isso serve para determinar a validade de cada variável.

Em tempo de execução faz com que o compilador libere a memória reservada para as variáveis declaradas dentro do escopo. Ou seja, cada vez que um escopo é aberto é configurado uma área de memória para alocar as variáveis declaradas dentro do escopo. Quando o escopo é fechado as variáveis declaradas dentro do escopo são liberadas.

Vamos a um trecho de código:

int main() {
    int x = 10;
    int y = 20;
    if (x == 10) {
        int z = x + y;
    }
    printf("z: %d\n", z);
}

Ao tentar compilar o código acima teremos o seguinte erro:

'z' undeclared (first use in this function)

Indicando que o compilador não detectou a declaração da variável z. Isso ocorre porque z foi declarada em um escopo que começou com o if, e terminou antes do printf. O mesmo se aplica para funções. Cada função tem um escopo próprio, já que obrigatoriamente deve ter um conjunto de chaves.

Porém, o mais importante, o erro que pode gerar problemas difíceis de resolver, após o fechamento do escopo do if, a variável z será desalocada, e seu endereço de memória estará livre para alocar outra variável.

Trapaceando

Já vi muito programador fazendo uma trapaça pra tentar recuperar o valor de z, por exemplo tentando retornar um ponteiro para a variável:

int* addxy(int x, int y) {
    int z = x + y;
    return &z;
}
int subxy(int x, int y) {
    int w = x - y;
    return w;
} 
int main() {
    int x = 10;
    int y = 20;
    int *p = null;
    if (x == 10 ) {
        int z = x + y;
        p = &z;
    }
    printf("z: %d\n", *p);
}

Obtendo a seguinte saída:

z: 30

Por que chamei de trapaça? Porque o espaço de memória que armazena não é mais válido. Por sorte, pura sorte, o espaço de memória ainda não tinha sido alocado para outra variável, e portanto ainda continha os dados esperados.

Tudo o que Pode Dar Errado Dará

Mas vamos supor que por algum motivo um novo escopo seja criado antes do printf:

int* addxy(int x, int y) {
    int z = x + y;
    return &z;
}
int subxy(int x, int y) {
    int w = x - y;
    return w;
} 
int main() {
    int x = 10;
    int y = 20;
    int *p = addxy(x, y);
    int w = subxy(x, y);
    printf("z: %d\n", z);
}

A saída será:

z: -10

Que por sorte é o valor de w, simplesmente porque as alocações no escopo da função subxy são idênticas às alocações no escopo da função addxy.

Note o tamanho do problema. Em uma situação em que só existisse a função addxy, e o código funcionasse conforme esperado, quando da criação da função subxy o código iria parar de funcionar, e uma análise superficial colocaria a culpa da falha na função subxy, que está correta.

E Muitos Caem Assim

A maioria das falhas com ponteiros para variáveis locais acontecem assim:

char* build_filename(char name[9], char ext[4]) {
	char filename[13];
	char *f;
	char *n;
	char *e;
	for (f = filename, n = name; *n; f++, n++) 
		*f = *n;
	*f++ = '.';
	for (e = ext; *e; f++, e++) 
		*f = *e;
	*f = '\x0';
	f = filename;
	return f; 
}

int main() {
	char *filename;
	filename = build_filename("labcpp", "txt");
	printf("filename: [%s]\n");
}

O código acima vai compilar sem falhas, nem warnings, mas o resultado pode funcionar, pode não funcionar. Tudo depende da dinâmica da alocação das variáveis. O problema acontece quando o código passa em todos os testes e entra em produção.

Certa vez fiquei dois dias pra achar um erro desse tipo. Por que demorou tanto? Porque a minha alteração era no começo de uma função muito grande, e o bug estava acontecendo em uma função que era chamada no final da função muito grande. Deu muito trabalho.

Como Evitar Problemas como Esse?

Algumas formas pra evitar esse tipo de problema:

  • Dar atenção aos warnings, muitas vezes eles indicarão falhas do tipo
  • Utilizar um analisador estático de código, como o lint, ou o cppcheck.
  • Praticar o pair review, ou avaliação entre pares.