As 20 linhas da vergonha

April 5, 2009 · Posted in C/C++, Opinião 

Enquanto o arroz vai cozinhado, lembrei-me de um caso curioso que aconteceu comigo no início da carreira, envolvendo gerenciamento de memória em C++. Naquela época, eu era muito mais. Mais jovem, mais rápido, mais arrogante e mais newbie…

Um colega de trabalho mais experiente estava explicando para um outro colega menos experiente que para cada new deve haver um respectivo delete. Caso contrário o objeto persistirá em memória podendo ocorrer memory leaks.

Nesse momento eu o interrompi e disse que “não necessariamente, pois basta que o objeto saia de escopo para que o destrutor seja chamado automaticamente.”.

A partir daí rolou uns 10 min de debate com argumentos e contra-argumentos, tendo toda a equipe parada assistindo. No final, metade da equipe concordava com ele e a outra metade comigo, e naquele momento ninguém tinha na cabeça uma forma de checar pra ver. Olhar na internet não era uma opção.

Como eu tinha todas as respostas, afinal já programava em PHP, Java e C++ fiquei de demonstrar posteriormente que eu estava certo.

Dias depois, num daqueles momentos filosóficos, lembrei que um endereço na memória é um número, e que é possível tanto converter um endereço para um número comum, quanto fazer o caminho inverso, embora a segunda opção normalmente seja inútil.

Bolei então uma forma de provar a minha teoria: Criaria um objeto num escopo restrito alocando com new. Guardaria o endereço desse objeto como um número comum num long fora do escopo restrito, esperaria o escopo restrito finalizar, o destrutor do obejto ser chamado e então, no escopo externo, usaria o número que representava o endereço do objeto para acessar aquela área de memória mostrando que receberia um SIGSEGV (segmentation fault) por tentar acessar uma área de memória inválida. Gerei então o código abaixo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
 
using namespace std;
 
int main(int argc, char** argv) {
    unsigned long num = 0;
 
    {
        string* str = new string("Hello World!!!");
        num = (unsigned long)str;
 
        cout << "str: " << str <<  endl;
        cout << "num: " << num <<  endl;
    }
 
    cout << *((string*)num) << endl;
 
    return 0;
}

Eis que para minha surpresa o programa NÃO explodiu na minha cara. Ele funcionava corretamente demonstrando que na verdade EU era quem estava errado.

Pesquisando mais constatei que sim, para cada new deve haver um e somente delete, e mais, para cada new[] deve haver um e somente um delete[]. O que eu achava que sabia sobre gerenciamento de memória e escopos, provavelmente era alguma confusão entre diferentes linguagens.

Quando eu iniciei a discussão dias antes, eu cometi todos os erros básicos que um newbie comete:

  1. Se meter onde não foi chamado.
  2. Jurar de pé junto que está certo sem ter provas.
  3. Subestimar o conhecimento de alguém mais experiente.
  4. Achar que tem todas as respostas só porque acabou de ver isso na aula.
  5. Achar que só porque conhece o básico de várias linguagens, então tem todas as respostas.

Como todo bom integrante da espécie dos Homo sapiens, falei merda. A minha reação a esta descoberta foi assumir para e equipe inteira que eu estava errado, pedir desculpas por ter falado besteira e mostrar essas vinte linhas de código que usei para demonstrar isso. Além disso, mostrei todas as fontes que pesquisei, e ainda trouxe à tona outros detalhes sobre gerenciamento de memória que nos estavam escapando, como referências circulares. Isso levou até a um re-desing de algumas partes do projeto.

As minhas 20 linhas de cógigo ficaram conhecidas como “As 20 linhas da vergonha” e natualmente eu fui sacaneado por isso. Cada vez que alguém tinha alguma dúvida falavam “faz aí as 20 linhas da vegonha pra testar”. O significado era mais ou menos como “faz aí um teste simples pra não passar vergonha depois”.

O mais importante é que a minha atitude pós-cagada foi bem aceita e trouxe benefícios para a equipe. Se eu tivesse me esquivado, mais cedo ou mais tarde alguém ia provar que eu estava errado na frente de todo mundo, ou pior, nos bastidores. Admitir o erro e aprender com isso mostrou que eu estava preparado para ser contestado. Adicionalmente isso mostrou para equipe que por mais que eu parecesse arrogante (eu pareço mais do que sou…), eu sabia que era um humano comum, que errava como qualquer um, não me achando superior a ninguém. Isso abriu caminho para que outros membros da equipe se sentissem à vontade para fazer o mesmo e nós nos tornamos uma equipe ainda mais coesa.

Comments

  • Marcus Antonius

    História muito interessante!!
    Mais bagagem pra quem acompanhou o debate!

    Outra coisa interessante é a falta de 18 dias para o lançamento do novo Ubuntu!!
    (mas já?????????? O tempo voa!)

  • http://blabos.org blabos

    Ultimamente pra mim o tempo não tá voando, tá indo de teleporte…

  • Junio

    Blabos, muito obrigado pela passagem de experiência. Muito rico o texto. Paz e saúde para ti.

  • ♣♦ Cheshire Hime ♥♠

    Hmm… Eu não tenho certeza qual compilador foi usado ou qual versão da linguagem, mas, que eu saiba, o C não exibe erros ao acessar memória desalocada a não ser que seja protegida pelo sistema operacional… O C++ exibe?

  • http://blabos.org Blabos de Blebe

    Primeiramente obrigado pela visita!

    É C++ com g++, pequena padawan :)

    Depende do que você considera “exibir erros”.

    Em tempo de compilação ou em tempo de execução?

    No caso acima, a inexperiência esperava (cacofônica essa!) a destruição automática por saída de escopo (o que não existe para um objeto dinamicamente alocado em C++), seguida pela derreferenciação de um ponteiro que apontava para uma área (previamente) desalocada de memória, ocasionando um erro fatal em tempo de execução.

    Adicione um delete str; após a linha 14 que você perceberá a sutil diferença.

    Ambos os testes não dão nem warning em tempo de compilação.

  • ♣♦ Cheshire Hime ♥♠

    Não não, eu falo em tempo de execução mesmo… No início, quando eu estava aprendendo a programar, nosso professor nos ensinou vetores antes de alloc, malloc e calloc e, desta forma, nós usávamos vetores livremente sem alocar memória…

    Raramente dava algum erro de memória protegida pelo sistema operacional…
    De resto, não aparecia erro algum, nem na compilação nem na execução.

    Claro que isto não era com objetos, era apenas com C, nas aulas de programação estruturada… Por isso a minha dúvida… Em C++, usando new ao invés de alloc, o programa gera erros em tempo de execução se eu tentar usar uma memória não alocada?

    Acho que seria bom vermos nas especificações da linguagem, afinal, o destrutor, além de desalocar memória, pode protegê-la até a finalização do programa ou algo assim… ou isto não acontece?

  • http://blabos.org Blabos de Blebe

    A expressão “nós usávamos vetores livremente sem alocar memória” é bastante imprecisa, pois vetores precisam de memória para existir. Provavelmente você quis dizer “nós usávamos vetores livremente sem alocar memória dinamicamente”.

    A especificação diz que derreferenciar um ponteiro nulo resulta em comportamento indefinido, o que na maioria dos sistemas (mas não todos), significa um erro fatal.

    Isso não é o que está exemplificado acima. No exemplo há uma tentativa de acesso a uma área ‘supostamente’ desalocada.

    Ponteiro nulo != ponteiro para área inválida de memória.


    int* ptr_null = 0;

    // ptr_null é um ponteiro nulo

    int* ptr_foo = new int(123456);
    delete ptr_foo;

    // ptr_foo aponta para uma área de memória
    // que não pertence mais a este programa

    printf("%pn", ptr_null);
    printf("%pn", ptr_foo);

    Compile e execute os exemplos que você vai ver a diferença.

    Quanto ao último parágrafo, não, não é isso que acontece. Você está misturando as coisas, o destrutor é só uma rotina que é chamada automaticamente quando um objeto é desalocado, seja por uma chamada a delete sobre objetos dinamicamente alocados ou por saída de escopo de objetos alocados na pilha.

blog comments powered by Disqus