Articles

Ponteiros C+++: porque precisamos deles, quando os usamos, como eles diferem do acesso ao próprio objeto

Even embora a maioria dos programadores entenda qual é a diferença entre objetos e ponteiros para eles, às vezes não é totalmente claro, qual das formas de acesso ao objeto a escolher. Tentamos responder esta pergunta abaixo.

Question

Vindo de um fundo Java, começamos a trabalhar com objetos em C++. Mas uma coisa que me ocorreu é que as pessoas frequentemente usam apontadores para objetos ao invés dos próprios objetos, por exemplo esta declaração:

Object *myObject = new Object;

rather than:

Object myObject;

Or ao invés de usar uma função, digamos testFunc(), como esta:

myObject.testFunc();

temos que escrever:

myObject->testFunc();

Mas não consigo entender porque devemos fazer desta maneira. Eu assumiria que tem a ver com eficiência e velocidade, já que temos acesso direto ao endereço da memória. Estou certo?

Resposta

Por sinal, devemos notar, que os ponteiros em Java não são usados explicitamente, por exemplo, um programador não pode acessar um objeto em código através de um ponteiro para ele. No entanto, em Java todos os tipos, excepto base, são referenciados: o acesso a eles passa pelo link, embora não se possa passar explicitamente o parâmetro por link. Além disso, novidades em C++ e Java ou C# são coisas diferentes.

Para dar uma pequena idéia sobre os ponteiros em C++ , vamos dar dois fragmentos de código similares:

Java:

Object object1 = new Object(); //A new object is allocated by JavaObject object2 = new Object(); //Another new object is allocated by Javaobject1 = object2; //object1 now points to the object originally allocated for object2//The object originally allocated for object1 is now "dead" – //nothing points to it, so it//will be reclaimed by the Garbage Collector.//If either object1 or object2 is changed, //the change will be reflected to the other

O equivalente mais próximo a isto, é:

C++:

Object * object1 = new Object(); //A new object is allocated on the heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete object1;//Since C++ does not have a garbage collector, //if we don't do that, the next line would //cause a "memory leak", i.e. a piece of claimed memory that //the app cannot use //and that we have no way to reclaim...object1 = object2; //Same as Java, object1 points to object2.

Vejamos o caminho alternativo em C++:

Object object1; //A new object is allocated on the STACKObject object2; //Another new object is allocated on the STACKobject1 = object2;//!!!! This is different! //The CONTENTS of object2 are COPIED onto object1,//using the "copy assignment operator", the definition of operator =.//But, the two objects are still different. //Change one, the other remains unchanged.//Also, the objects get automatically destroyed //once the function returns...

Agora estamos a obter um ganho de velocidade, acedendo directamente à memória?

Atualmente, de forma alguma. Os ponteiros são normalmente usados para o acesso à pilha enquanto os objetos estão localizados na pilha – esta é uma estrutura mais simples e mais rápida. Se você é um iniciante, temos para você algum material no qual dizemos em detalhes o que é uma pilha e uma pilha.

Estritamente falando, esta pergunta combina duas questões diferentes. Primeiro: quando é que usamos alocação dinâmica de memória? Segundo: quando é melhor usar apontadores? Claro, não vamos fazer sem palavras comuns que você deve sempre escolher a ferramenta mais apropriada para o trabalho. Quase sempre há melhor realização do que usar alocação dinâmica manual (alocação dinâmica) e/ou ponteiros brutos.

É muito lamentável que você veja a alocação dinâmica com tanta freqüência. Isso só mostra quantos programadores C++ maus existem.

Em certo sentido, você tem duas perguntas agrupadas em uma só. A primeira é quando devemos usar a alocação dinâmica (usando nova)? A segunda é quando devemos usar apontadores?

A importante mensagem take-home é que você deve sempre usar a ferramenta apropriada para o trabalho. Em quase todas as situações, há algo mais apropriado e seguro do que realizar alocação dinâmica manual e/ou usar apontadores brutos.

Alocação dinâmica

Na sua pergunta, você demonstrou duas maneiras de criar um objeto. A principal diferença é a duração do armazenamento do objeto. Ao fazer Object myObject; dentro de um bloco, o objeto é criado com duração de armazenamento automático, o que significa que ele será destruído automaticamente quando sair do escopo. Quando você faz um novo objeto(), o objeto tem duração de armazenamento dinâmica, o que significa que ele permanece vivo até que você o apague explicitamente. O usuário só deve usar a duração do armazenamento dinâmico quando precisar dela. Ou seja, você deve sempre preferir criar objetos com duração de armazenamento automático quando puder.

As duas principais situações nas quais você pode precisar de alocação dinâmica:

  1. Você precisa do objeto para sobreviver ao escopo atual – aquele objeto específico naquele local de memória específico, não uma cópia dele. Se você está bem com copiar/mover o objeto (na maioria das vezes você deve estar), você deve preferir um objeto automático.
  2. Você precisa alocar muita memória, que pode facilmente encher a pilha. Seria bom se não tivéssemos que nos preocupar com isso (na maioria das vezes você não deveria ter que se preocupar), pois está realmente fora do âmbito do C++, mas infelizmente temos que lidar com a realidade dos sistemas que estamos desenvolvendo para.
  3. Você não sabe exatamente o tamanho do array, que você terá que usar. Como você sabe, em C++ o tamanho das matrizes é fixo. Isso pode causar problemas, por exemplo, enquanto lê a entrada do usuário. O ponteiro define apenas aquela seção da memória, onde o início de um array será escrito, não limitando seu tamanho.

Se for necessário o uso de alocação dinâmica, você deve encapsulá-la usando o ponteiro inteligente ou de outro tipo que suporte o idioma “Resource acquisition is initialization” (containers padrão suportam-no – é um idioma, de acordo com o qual o recurso: um bloco de memória, arquivo, conexão de rede, etc. – são inicializados ao entrar no construtor, e depois cuidadosamente destruídos pelo destrutor). Por exemplo, apontadores inteligentes são std::unique_ptr e std::shared_ptr

Pointers

No entanto, existem outros usos mais gerais para apontadores em bruto além da alocação dinâmica, mas a maioria tem alternativas que você deve preferir. Como antes, sempre prefira as alternativas a menos que você realmente precise de ponteiros.

  1. Você precisa de semântica de referência. Às vezes você quer passar um objeto usando um ponteiro (independentemente de como ele foi alocado) porque você quer que a função para a qual você está passando tenha acesso a esse objeto específico (e não uma cópia dele). No entanto, na maioria das situações, você deve preferir tipos de referência a ponteiros, porque é para isso que eles são projetados especificamente. Note que não se trata necessariamente de estender a vida útil do objeto para além do escopo atual, como na situação 1 acima. Como antes, se você estiver de acordo em passar uma cópia do objeto, você não precisa de semântica de referência.
  2. Você precisa de polimorfismo. Você só pode chamar funções polimorfas (ou seja, de acordo com o tipo dinâmico de um objeto) através de um ponteiro ou referência ao objeto. Se esse é o comportamento que você precisa, então você precisa usar apontadores ou referências. Novamente, referências devem ser preferidas.
  3. Você quer representar que um objeto é opcional, permitindo que um nullptr seja passado quando o objeto está sendo omitido. Se for um argumento, você deve preferir usar argumentos padrão ou sobrecarga de funções. Caso contrário, você deve preferir usar um tipo que encapsule este comportamento, como std::optional (introduzido em C++17 – com padrões C++ anteriores, use boost::optional).
  4. Você quer desacoplar unidades de compilação para melhorar o tempo de compilação. A propriedade útil de um ponteiro é que você só precisa de uma declaração de avanço do ponteiro para o tipo (para realmente usar o objeto, você vai precisar de uma definição). Isto permite desacoplar partes do seu processo de compilação, o que pode melhorar significativamente o tempo de compilação. Veja a linguagem Pimpl.
  5. Você precisa fazer interface com uma biblioteca em C ou uma biblioteca no estilo C. Neste ponto, você é forçado a usar apontadores em bruto. A melhor coisa que você pode fazer é certificar-se de que você só deixa seus ponteiros brutos soltos no último momento possível. Você pode obter um ponteiro bruto a partir de um ponteiro inteligente, por exemplo, usando sua função get member. Se uma biblioteca executa alguma alocação para você que espera que você desaloque através de um handle, você pode frequentemente envolver o handle em um ponteiro inteligente com um deletador personalizado que irá desalocar o objeto apropriadamente.