Por que devo usar um ponteiro em vez do próprio objeto?


1602

Estou vindo de um plano de fundo Java e comecei a trabalhar com objetos em C ++. Mas uma coisa que me ocorreu é que as pessoas costumam usar ponteiros para objetos em vez dos próprios objetos, por exemplo, esta declaração:

Object *myObject = new Object;

ao invés de:

Object myObject;

Ou, em vez de usar uma função, digamos testFunc()assim:

myObject.testFunc();

nós temos que escrever:

myObject->testFunc();

Mas não consigo descobrir por que devemos fazer dessa maneira. Eu diria que isso tem a ver com eficiência e velocidade, já que temos acesso direto ao endereço de memória. Estou certo?


405
Parabéns a você por questionar essa prática, em vez de apenas segui-la. Na maioria das vezes, os ponteiros são usados ​​em excesso.
Luchian Grigore 03/03

120
Se você não vê um motivo para usar ponteiros, não. Preferir objetos. Prefira objetos antes de unique_ptr antes de shared_ptr antes de ponteiros brutos.
Stefan

113
nota: em java, tudo (exceto os tipos básicos) é um ponteiro. então você deve perguntar o contrário: por que preciso de objetos simples?
309 Karoly Horvath #

119
Observe que, em Java, os ponteiros estão ocultos pela sintaxe. No C ++, a diferença entre um ponteiro e um não ponteiro é explicitada no código. Java usa ponteiros em todos os lugares.
Daniel Martín

216
Fechar como muito amplo ? A sério? Por favor, pessoal, observe que esse modo de programação Java ++ é muito comum e um dos problemas mais importantes na comunidade C ++ . Deve ser tratado com seriedade.
precisa saber é o seguinte

Respostas:


1574

É muito lamentável que você veja alocação dinâmica com tanta frequência. Isso apenas mostra quantos programadores C ++ ruins existem.

De certa forma, você tem duas perguntas agrupadas em uma. A primeira é quando devemos usar a alocação dinâmica (usando new)? A segunda é quando devemos usar ponteiros?

A mensagem importante para levar para casa é que você sempre deve 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 ponteiros 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 será destruído automaticamente quando ficar fora do escopo. Ao fazê-lo new Object(), o objeto tem duração de armazenamento dinâmico, o que significa que permanece ativo até você explicitamente delete. Você só deve usar a duração do armazenamento dinâmico quando precisar. Ou seja, você sempre deve preferir criar objetos com duração de armazenamento automático quando puder .

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

  1. Você precisa que o objeto sobreviva ao escopo atual - esse objeto específico naquele local de memória específico, não uma cópia dele. Se você estiver bem em copiar / mover o objeto (na maioria das vezes você deveria), você deve preferir um objeto automático.
  2. Você precisa alocar muita memória , o 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), pois está realmente fora do alcance do C ++, mas, infelizmente, temos que lidar com a realidade dos sistemas estamos desenvolvendo para.

Quando você precisar absolutamente de alocação dinâmica, encapsule-o em um ponteiro inteligente ou em outro tipo que execute RAII (como os contêineres padrão). Ponteiros inteligentes fornecem semântica de propriedade de objetos alocados dinamicamente. Dê uma olhada std::unique_ptre std::shared_ptr, por exemplo. Se você usá-los adequadamente, poderá quase inteiramente evitar executar seu próprio gerenciamento de memória (consulte a Regra do Zero ).

Ponteiros

No entanto, existem outros usos mais gerais para ponteiros brutos além da alocação dinâmica, mas a maioria possui alternativas que você prefere. Como antes, sempre prefira as alternativas, a menos que você realmente precise de indicadores .

  1. Você precisa de semântica de referência . Às vezes, você deseja passar um objeto usando um ponteiro (independentemente de como foi alocado), porque deseja que a função para a qual você está passando tenha acesso a esse objeto específico (não uma cópia dele). No entanto, na maioria das situações, você deve preferir tipos de referência a ponteiros, porque é especificamente para isso que eles foram projetados. Observe que não se trata necessariamente de estender a vida útil do objeto além do escopo atual, como na situação 1 acima. Como antes, se você concorda em passar uma cópia do objeto, não precisa de semântica de referência.

  2. Você precisa de polimorfismo . Você só pode chamar funções polimorficamente (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, use ponteiros ou referências. Novamente, as referências devem ser preferidas.

  3. Você deseja representar que um objeto é opcional , permitindo que um nullptrseja passado quando o objeto estiver sendo omitido. Se for um argumento, você deve preferir usar argumentos padrão ou sobrecargas de função. Caso contrário, você deve preferencialmente usar um tipo que encapsule esse comportamento, como std::optional(introduzido no C ++ 17 - com padrões anteriores do C ++, use boost::optional).

  4. Você deseja 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 encaminhamento do tipo apontado (para realmente usar o objeto, você precisará de uma definição). Isso permite desacoplar partes do seu processo de compilação, o que pode melhorar significativamente o tempo de compilação. Veja o idioma Pimpl .

  5. Você precisa interagir com uma biblioteca C ou uma biblioteca de estilo C. Neste ponto, você é forçado a usar ponteiros brutos. A melhor coisa que você pode fazer é garantir que você solte os ponteiros brutos no último momento possível. Você pode obter um ponteiro bruto de um ponteiro inteligente, por exemplo, usando sua getfunção de membro. Se uma biblioteca executar uma alocação para você que espera desalocar por meio de um identificador, muitas vezes você pode agrupar o identificador em um ponteiro inteligente com um deleter personalizado que desalocará o objeto adequadamente.


83
"Você precisa do objeto para sobreviver ao escopo atual." - Uma observação adicional sobre isso: há casos em que parece que você precisa do objeto para sobreviver ao escopo atual, mas na verdade não precisa. Se você colocar seu objeto dentro de um vetor, por exemplo, o objeto será copiado (ou movido) para o vetor e o objeto original poderá ser destruído quando seu escopo terminar.

25
Lembre-se que s / copiar / mover / em muitos lugares agora. Retornar um objeto definitivamente não implica uma mudança. Você também deve observar que o acesso a um objeto por meio de um ponteiro é ortogonal à forma como ele foi criado.
Filhote de cachorro

15
Sinto falta de uma referência explícita ao RAII nesta resposta. C ++ é tudo (quase todos) sobre gestão de recursos e RAII é a maneira de fazê-lo em C ++ (E o principal problema que ponteiros crus gerar: Breaking RAII)
Manu343726

11
Ponteiros inteligentes existiam antes do C ++ 11, por exemplo, boost :: shared_ptr e boost :: scoped_ptr. Outros projetos têm seu próprio equivalente. Você não pode obter semântica de movimento, e a atribuição de std :: auto_ptr é falha, portanto, o C ++ 11 melhora as coisas, mas o conselho ainda é bom. (E um triste comentário, não basta ter acesso a um compilador C ++ 11, é necessário que todos os compiladores que você deseja que seu código funcionem com suporte ao C ++ 11. Sim, Oracle Solaris Studio, estou olhando para você).
ARMB

7
@ MDMoore313 Você pode escreverObject myObject(param1, etc...)
user000001

173

Existem muitos casos de uso para ponteiros.

Comportamento polimórfico . Para tipos polimórficos, ponteiros (ou referências) são usados ​​para evitar o fatiamento:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Semântica de referência e evitando copiar . Para tipos não polimórficos, um ponteiro (ou uma referência) evitará copiar um objeto potencialmente caro

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Observe que o C ++ 11 possui semântica de movimentação que pode evitar muitas cópias de objetos caros no argumento de função e como valores de retorno. Mas o uso de um ponteiro definitivamente os evitará e permitirá vários ponteiros no mesmo objeto (enquanto um objeto só pode ser movido uma vez).

Aquisição de recursos . Criar um ponteiro para um recurso usando o newoperador é um antipadrão no C ++ moderno. Use uma classe de recurso especial (um dos contêineres Padrão) ou um ponteiro inteligente ( std::unique_ptr<>ou std::shared_ptr<>). Considerar:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Um ponteiro bruto deve ser usado apenas como uma "visualização" e não de forma alguma envolvida na propriedade, seja por meio da criação direta ou implicitamente por meio de valores de retorno. Consulte também estas perguntas e respostas nas perguntas frequentes sobre C ++ .

Controle de vida útil mais refinado Sempre que um ponteiro compartilhado é copiado (por exemplo, como argumento de função), o recurso para o qual ele aponta é mantido vivo. Objetos regulares (não criados por você new, diretamente por você ou dentro de uma classe de recurso) são destruídos ao sair do escopo.


17
"Criar um ponteiro para um recurso usando o novo operador é um antipadrão". Acho que você pode até melhorar que ter um ponteiro bruto com algo é um antipadrão . Não apenas a criação, mas a transmissão de ponteiros brutos como argumentos ou valores de retorno que implicam transferência de propriedade IMHO está obsoleta desde unique_ptr/ move semântica
dyp

1
@dyp tnx, atualizado e referência às perguntas e respostas sobre C ++ FAQ sobre este tópico.
TemplateRex

4
Usar ponteiros inteligentes em qualquer lugar é um anti-padrão. Existem alguns casos especiais em que é aplicável, mas na maioria das vezes, a mesma razão que argumenta para alocação dinâmica (vida útil arbitrária) também se opõe a qualquer um dos indicadores inteligentes habituais.
James Kanze

2
@JamesKanze Eu não quis dizer que ponteiros inteligentes devem ser usados ​​em todos os lugares, apenas para propriedade, e também que ponteiros em bruto não devem ser usados ​​para propriedade, mas apenas para visualizações.
TemplateRex

2
@TemplateRex Isso parece um pouco tolo, pois hun(b)também requer conhecimento da assinatura, a menos que você esteja bem em não saber que forneceu o tipo errado até a compilação. Embora o problema de referência geralmente não seja detectado no momento da compilação e exija mais esforço para depurar, se você estiver verificando a assinatura para garantir que os argumentos estejam corretos, também poderá ver se algum dos argumentos é referência portanto, o bit de referência se torna um problema (principalmente ao usar IDEs ou editores de texto que mostram a assinatura de uma função selecionada). Além disso const&,.
JAB

130

Existem muitas respostas excelentes para essa pergunta, incluindo os importantes casos de uso de declarações avançadas, polimorfismo etc., mas sinto que uma parte da "alma" da sua pergunta não foi respondida - ou seja, o que significam as diferentes sintaxes em Java e C ++.

Vamos examinar a situação comparando os dois idiomas:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = 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 disso é:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete 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.

Vamos ver a maneira alternativa do C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = 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...

A melhor maneira de pensar é que - mais ou menos - Java (implicitamente) manipula ponteiros para objetos, enquanto o C ++ pode manipular ponteiros para objetos ou os próprios objetos. Há exceções a isso - por exemplo, se você declarar tipos "primitivos" de Java, eles são valores reais que são copiados, e não indicadores. Assim,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Dito isto, o uso de ponteiros NÃO é necessariamente a maneira correta ou errada de lidar com as coisas; no entanto, outras respostas cobriram isso satisfatoriamente. A idéia geral, porém, é que, em C ++, você tem muito mais controle sobre a vida útil dos objetos e sobre onde eles irão morar.

O ponto principal Object * object = new Object()é que a construção é a que mais se aproxima da semântica típica de Java (ou C #).


7
Object2 is now "dead": Eu acho que você quer dizer myObject1ou mais precisamente the object pointed to by myObject1.
Clément

2
De fato! Reescrevi um pouco.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();é um código muito ruim. O segundo novo ou o segundo construtor de objeto pode ser lançado e agora o objeto1 está vazado. Se você estiver usando news não processados , você deve newagrupar objetos editados em wrappers RAII o mais rápido possível.
PSKocik

8
De fato, seria se este fosse um programa, e nada mais estivesse acontecendo em torno dele. Felizmente, este é apenas um trecho de explicação que mostra como um ponteiro em C ++ se comporta - e um dos poucos lugares em que um objeto RAII não pode ser substituído por um ponteiro bruto, está estudando e aprendendo sobre ponteiros brutos ...
Gerasimos R

80

Outro bom motivo para usar ponteiros seria para declarações de encaminhamento . Em um projeto grande o suficiente, eles podem realmente acelerar o tempo de compilação.


7
isso está realmente contribuindo para a mistura de informações úteis, tão feliz por você ter respondido!
TemplateRex

3
std :: shared_ptr <T> também funciona com declarações para a frente de T. (std :: unique_ptr <T> não )
Berkus

13
@berkus: std::unique_ptr<T>trabalha com declarações avançadas de T. Você só precisa ter certeza de que quando o destruidor do std::unique_ptr<T>é chamado, Té um tipo completo. Isso normalmente significa que sua classe que contém o std::unique_ptr<T>declara seu destruidor no arquivo de cabeçalho e a implementa no arquivo cpp (mesmo que a implementação esteja vazia).
David Stone

Os módulos irão corrigir isso?
Trevor Hickey

@TrevorHickey Comentário antigo, eu sei, mas para responder de qualquer maneira. Os módulos não removerão a dependência, mas devem tornar a inclusão da dependência muito barata, quase gratuita em termos de custo de desempenho. Além disso, se a aceleração geral dos módulos for suficiente para obter os tempos de compilação em um intervalo aceitável, também não será mais um problema.
Aidiakapi 13/10/19

79

Prefácio

Java não é nada como C ++, ao contrário do hype. A máquina de hype do Java gostaria que você acreditasse que, como o Java possui sintaxe semelhante ao C ++, as linguagens são semelhantes. Nada pode estar mais longe da verdade. Essa desinformação é parte do motivo pelo qual os programadores Java acessam o C ++ e usam sintaxe do tipo Java sem entender as implicações de seu código.

Em diante vamos

Mas não consigo descobrir por que devemos fazer dessa maneira. Eu diria que isso tem a ver com eficiência e velocidade, já que temos acesso direto ao endereço de memória. Estou certo?

Pelo contrário, na verdade. A pilha é muito mais lenta que a pilha, porque a pilha é muito simples em comparação com a pilha. As variáveis ​​de armazenamento automático (também conhecidas como variáveis ​​de pilha) têm seus destruidores chamados quando saem do escopo. Por exemplo:

{
    std::string s;
}
// s is destroyed here

Por outro lado, se você usar um ponteiro alocado dinamicamente, seu destruidor deverá ser chamado manualmente. deletechama esse destruidor para você.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Isso não tem nada a ver com a newsintaxe predominante em C # e Java. Eles são usados ​​para finalidades completamente diferentes.

Benefícios da alocação dinâmica

1. Você não precisa saber o tamanho da matriz antecipadamente

Um dos primeiros problemas com os quais muitos programadores de C ++ se deparam é que, quando aceitam entrada arbitrária dos usuários, você só pode alocar um tamanho fixo para uma variável de pilha. Você também não pode alterar o tamanho das matrizes. Por exemplo:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Obviamente, se você usou um std::string, std::stringredimensiona-se internamente para que não seja um problema. Mas, essencialmente, a solução para esse problema é a alocação dinâmica. Você pode alocar memória dinâmica com base na entrada do usuário, por exemplo:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Nota lateral : Um erro que muitos iniciantes cometem é o uso de matrizes de comprimento variável. Esta é uma extensão GNU e também uma em Clang porque elas refletem muitas extensões do GCC. Portanto, o seguinte int arr[n]não deve ser invocado.

Como o heap é muito maior que a pilha, é possível alocar / realocar arbitrariamente a quantidade de memória necessária, enquanto a pilha tem uma limitação.

2. Matrizes não são ponteiros

Como isso é um benefício que você pergunta? A resposta ficará clara quando você entender a confusão / mito por trás de matrizes e ponteiros. É comum presumir que eles são iguais, mas não são. Esse mito deriva do fato de que os ponteiros podem ser subscritos da mesma forma que matrizes e, devido a matrizes decaírem para ponteiros no nível superior em uma declaração de função. No entanto, uma vez que uma matriz decai para um ponteiro, o ponteiro perde suasizeof informações. Assim sizeof(pointer), fornecerá o tamanho do ponteiro em bytes, que geralmente é de 8 bytes em um sistema de 64 bits.

Você não pode atribuir a matrizes, apenas inicializá-las. Por exemplo:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

Por outro lado, você pode fazer o que quiser com ponteiros. Infelizmente, como a distinção entre ponteiros e matrizes é feita manualmente em Java e C #, os iniciantes não entendem a diferença.

3. Polimorfismo

Java e C # têm recursos que permitem tratar objetos como outro, por exemplo, usando a aspalavra - chave. Portanto, se alguém quisesse tratar um Entityobjeto como um Playerobjeto, Player player = Entity as Player;seria possível. Isso é muito útil se você pretende chamar funções em um contêiner homogêneo que deve ser aplicado apenas a um tipo específico. A funcionalidade pode ser alcançada de maneira semelhante abaixo:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Por exemplo, digamos que se apenas Triângulos tivesse uma função Girar, seria um erro do compilador se você tentasse chamá-la em todos os objetos da classe. Usando dynamic_cast, você pode simular oas palavra chave. Para ficar claro, se uma conversão falhar, ele retornará um ponteiro inválido. Portanto, !testé essencialmente um atalho para verificar se testé NULL ou um ponteiro inválido, o que significa que a conversão falhou.

Benefícios das variáveis ​​automáticas

Depois de ver todas as grandes coisas que a alocação dinâmica pode fazer, você provavelmente está se perguntando por que alguém NÃO usaria alocação dinâmica o tempo todo? Eu já te disse uma razão, a pilha é lenta. E se você não precisa de toda essa memória, não deve abusar dela. Então, aqui estão algumas desvantagens em nenhuma ordem específica:

  • É propenso a erros. A alocação manual de memória é perigosa e você é propenso a vazamentos. Se você não for proficiente no uso do depurador ouvalgrind (uma ferramenta de vazamento de memória), poderá arrancar o cabelo da cabeça. Felizmente, expressões idiomáticas da RAII e indicadores inteligentes aliviam um pouco isso, mas você deve estar familiarizado com práticas como A Regra dos Três e a Regra dos Cinco. É muita informação, e iniciantes que não sabem ou não se importam cairão nessa armadilha.

  • Não é necessário. Ao contrário de Java e C #, onde é idiomático usar a newpalavra - chave em qualquer lugar, em C ++, você deve usá-la somente se precisar. A frase comum diz: tudo parece um prego se você tiver um martelo. Enquanto os iniciantes que começam com C ++ têm medo de ponteiros e aprendem a usar variáveis ​​de pilha por hábito, os programadores de Java e C # começam usando ponteiros sem entendê-los! Isso está literalmente saindo com o pé errado. Você deve abandonar tudo o que sabe, porque a sintaxe é uma coisa, aprender o idioma é outra.

1. (N) RVO - Aka, (nomeado) Otimização do valor de retorno

Uma otimização que muitos compiladores fazem são coisas chamadas elision e otimização de valor de retorno . Essas coisas podem impedir cópias desnecessárias, úteis para objetos muito grandes, como um vetor que contém muitos elementos. Normalmente, a prática comum é usar ponteiros para transferir a propriedade, em vez de copiar os objetos grandes para movê- los. Isso levou à criação de semânticas de movimento e ponteiros inteligentes .

Se você estiver usando ponteiros, (N) RVO NÃO ocorre. É mais benéfico e menos propenso a erros tirar proveito do (N) RVO em vez de retornar ou passar ponteiros se você estiver preocupado com a otimização. Vazamentos de erro podem ocorrer se o chamador de uma função for responsável por deleteum objeto alocado dinamicamente e tal. Pode ser difícil rastrear a propriedade de um objeto se ponteiros estiverem sendo passados ​​como uma batata quente. Basta usar variáveis ​​de pilha porque é mais simples e melhor.


"Então! Test é essencialmente um atalho para verificar se o teste é NULL ou um ponteiro inválido, o que significa que a conversão falhou." Penso que esta frase deve ser reescrita para maior clareza.
22714 berkus

4
"A máquina de hype do Java gostaria que você acreditasse" - talvez em 1997, mas agora isso é anacrônico, não há mais motivação para comparar o Java com o C ++ em 2014.
Matt R

15
Pergunta antiga, mas no segmento de código { std::string* s = new std::string; } delete s; // destructor called.... certamente isso deletenão funcionará porque o compilador não saberá mais o que sé?
badger5000

2
NÃO estou dando -1, mas não concordo com as declarações de abertura, como estão escritas. Primeiro, eu discordo que existe qualquer "hype" - pode ter sido em torno do ano 2000, mas agora os dois idiomas estão bem compreendidos. Segundo, eu argumentaria que eles são bem parecidos - C ++ é filho de C casado com Simula, o Java adiciona Virtual Machine, Garbage Collector e HEAVILY reduz os recursos, e o C # otimiza e reintroduz os recursos ausentes no Java. Sim, isso faz com que os padrões e o uso válido sejam HUGELY diferentes, mas é benéfico entender a infraestrutura / projeto comum para que se possa ver as diferenças.
precisa

1
@ James Matta: É claro que você está certo de que a memória é memória, e ambos são alocados a partir da mesma memória física, mas uma coisa a considerar é que é muito comum obter melhores características de desempenho trabalhando com objetos alocados pela pilha, porque a pilha - ou pelo menos seus níveis mais altos - tem uma chance muito alta de ficar "quente" no cache, à medida que as funções entram e saem, enquanto o heap não tem esse benefício, portanto, se você estiver perseguindo o heap, poderá receber várias falhas de cache, o que você provavelmente não estaria na pilha. Mas toda essa "aleatoriedade" normalmente favorece a pilha.
Gerasimos R

23

O C ++ fornece três maneiras de passar um objeto: por ponteiro, por referência e por valor. Java limita você com o último (a única exceção são tipos primitivos como int, booleano etc.). Se você deseja usar o C ++ não apenas como um brinquedo estranho, é melhor conhecer a diferença entre essas três maneiras.

Java finge que não há nenhum problema como 'quem e quando deve destruir isso?'. A resposta é: O Coletor de Lixo, Ótimo e Horrível. No entanto, ele não pode fornecer 100% de proteção contra vazamentos de memória (sim, java pode vazar memória ). Na verdade, o GC fornece uma falsa sensação de segurança. Quanto maior o seu SUV, maior o seu caminho para o evacuador.

O C ++ o deixa frente a frente com o gerenciamento do ciclo de vida do objeto. Bem, existem meios de lidar com isso ( família de ponteiros inteligentes , QObject no Qt e assim por diante), mas nenhum deles pode ser usado da maneira "atire e esqueça" como o GC: você deve sempre ter em mente o manuseio da memória. Não apenas você deve se preocupar em destruir um objeto, mas também deve evitar destruir o mesmo objeto mais de uma vez.

Ainda não está assustado? Ok: referências cíclicas - lide com você mesmo, humano. E lembre-se: mate cada objeto precisamente uma vez; nós, tempos de execução C ++, não gostamos daqueles que mexem com cadáveres, deixam os mortos em paz.

Então, voltando à sua pergunta.

Quando você passa seu objeto por valor, não por ponteiro ou por referência, copia o objeto (todo o objeto, seja um par de bytes ou um imenso despejo de banco de dados - você é esperto o suficiente para evitar o último, não é ' você?) toda vez que você faz '='. E para acessar os membros do objeto, você usa '.' (ponto).

Quando você passa seu objeto por ponteiro, copia apenas alguns bytes (4 em sistemas de 32 bits, 8 em sistemas de 64 bits), a saber - o endereço desse objeto. E para mostrar isso a todos, você usa esse operador sofisticado '->' ao acessar os membros. Ou você pode usar a combinação de '*' e '.'.

Quando você usa referências, obtém o ponteiro que finge ser um valor. É um ponteiro, mas você acessa os membros através de '.'.

E, para surpreender mais uma vez: quando você declara várias variáveis ​​separadas por vírgulas, observe (observe os ponteiros):

  • O tipo é dado a todos
  • O modificador de valor / ponteiro / referência é individual

Exemplo:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrestá obsoleto, não o use.
Neil

2
Certamente você não pode ter uma referência como membro sem também fornecer ao construtor uma lista de inicialização que inclua a variável de referência. (A referência tem de ser inicializado imediatamente Mesmo o corpo do construtor é tarde demais para defini-lo, IIRC..)
Chao

20

No C ++, os objetos alocados na pilha (usando a Object object;instrução dentro de um bloco) permanecerão apenas dentro do escopo em que foram declarados. Quando o bloco de código termina a execução, o objeto declarado é destruído. Considerando que, se você alocar memória no heap, usando Object* obj = new Object(), eles continuarão permanecendo no heap até você ligar delete obj.

Eu criaria um objeto na pilha quando gostaria de usá-lo não apenas no bloco de código que o declarou / alocou.


6
Object objnem sempre está na pilha - por exemplo, variáveis ​​globais ou membros.
tenfour

2
@LightnessRacesinOrbit Mencionei apenas sobre os objetos alocados em um bloco, não sobre variáveis ​​globais e membros. A coisa é que não estava claro, agora corrigido - adicionado "dentro de um bloco" na resposta. Espero que não seja falsa informação agora :)
Karthik Kalyanasundaram

20

Mas não consigo descobrir por que devemos usá-lo assim?

Vou comparar como ele funciona dentro do corpo da função se você usar:

Object myObject;

Dentro da função, você myObjectserá destruído quando essa função retornar. Portanto, isso é útil se você não precisar do seu objeto fora da sua função. Este objeto será colocado na pilha de threads atual.

Se você escrever dentro do corpo da função:

 Object *myObject = new Object;

a instância da classe Object apontada por myObjectnão será destruída quando a função terminar e a alocação estiver no heap.

Agora, se você é programador Java, o segundo exemplo está mais próximo de como a alocação de objetos funciona em java. Esta linha: Object *myObject = new Object;é equivalente a java: Object myObject = new Object();. A diferença é que, em java, myObject será coletado como lixo, enquanto em c ++ não será liberado, você deve chamar explicitamente em algum lugar `delete myObject; caso contrário, você introduzirá vazamentos de memória.

Desde o c ++ 11, você pode usar maneiras seguras de alocações dinâmicas: new Objectarmazenando valores em shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Além disso, os objetos costumam ser armazenados em contêineres, como map-s ou vector-s, e gerenciam automaticamente a vida útil dos seus objetos.


1
then myObject will not get destroyed once function endsAbsolutamente será.
Lightness Races em Órbita

6
No caso do ponteiro, myObjectainda será destruído, assim como qualquer outra variável local. A diferença é que seu valor é um ponteiro para um objeto, não o objeto em si, e a destruição de um ponteiro idiota não afeta seu pontapé. Portanto, o objeto sobreviverá à destruição.
Chao

Corrigido que, naturalmente, as variáveis ​​locais (que incluem ponteiro) serão liberadas - elas estão na pilha.
21413 Marc

13

Tecnicamente, é um problema de alocação de memória, mas aqui estão mais dois aspectos práticos disso. Tem a ver com duas coisas: 1) Escopo, quando você define um objeto sem um ponteiro, não será mais possível acessá-lo após o bloco de código em que está definido, enquanto que se você definir um ponteiro com "novo", poderá você pode acessá-lo de qualquer lugar que tenha um ponteiro para esta memória até chamar "excluir" no mesmo ponteiro. 2) Se você deseja passar argumentos para uma função, deseja passar um ponteiro ou uma referência para ser mais eficiente. Quando você passa um Objeto, o objeto é copiado; se este é um objeto que usa muita memória, isso pode consumir CPU (por exemplo, você copia um vetor cheio de dados). Quando você passa um ponteiro, tudo o que passa é um int (dependendo da implementação, mas a maioria deles é um int).

Fora isso, você precisa entender que "novo" aloca memória no heap que precisa ser liberado em algum momento. Quando você não precisa usar "novo", sugiro que você use uma definição de objeto regular "na pilha".


6

Bem, a questão principal é: por que devo usar um ponteiro em vez do próprio objeto? E a minha resposta, você (quase) nunca deve usar ponteiro em vez de objeto, porque o C ++ tem referências , é mais seguro que o ponteiro e garante o mesmo desempenho que o ponteiro.

Outra coisa que você mencionou na sua pergunta:

Object *myObject = new Object;

Como funciona? Ele cria ponteiros do Objecttipo, aloca memória para caber em um objeto e chama o construtor padrão, soa bem, certo? Mas, na verdade, não é tão bom, se você alocou memória dinamicamente (palavra-chave usada new), também precisa liberar memória manualmente, ou seja, no código você deve ter:

delete myObject;

Isso chama destruidor e libera memória, parece fácil, no entanto, em grandes projetos pode ser difícil detectar se um thread liberou memória ou não, mas para esse objetivo, você pode tentar ponteiros compartilhados , isso diminui um pouco o desempenho, mas é muito mais fácil trabalhar com eles.


E agora algumas apresentações terminam e voltam à questão.

Você pode usar ponteiros em vez de objetos para obter melhor desempenho ao transferir dados entre funções.

Dê uma olhada, você tem std::string(também é objeto) e contém realmente muitos dados, por exemplo, XML grande, agora você precisa analisá-los, mas para isso você tem uma função void foo(...)que pode ser declarada de diferentes maneiras:

  1. void foo(std::string xml); Nesse caso, você copiará todos os dados da sua variável para a pilha de funções, leva algum tempo, portanto, seu desempenho será baixo.
  2. void foo(std::string* xml); Nesse caso, você passará o ponteiro para o objeto, com a mesma velocidade da size_tvariável de passagem ; no entanto, esta declaração tem tendência a erros, porque você pode passar o NULLponteiro ou o ponteiro inválido. Ponteiros geralmente usados Cporque não possuem referências.
  3. void foo(std::string& xml); Aqui você passa a referência, basicamente é o mesmo que passar o ponteiro, mas o compilador faz algumas coisas e você não pode passar a referência inválida (na verdade, é possível criar uma situação com referência inválida, mas está enganando o compilador).
  4. void foo(const std::string* xml); Aqui é o mesmo que o segundo, apenas o valor do ponteiro não pode ser alterado.
  5. void foo(const std::string& xml); Aqui é o mesmo que o terceiro, mas o valor do objeto não pode ser alterado.

O que mais quero mencionar, você pode usar essas 5 maneiras de passar dados, independentemente da maneira de alocação que você escolheu (com newou regular ).


Outra coisa a mencionar, quando você cria um objeto de maneira regular , aloca memória na pilha, mas enquanto o cria, newaloca heap. É muito mais rápido para alocar stack, mas é tipo um pequeno para realmente grandes conjuntos de dados, por isso, se você precisar de grande objeto que você deve usar pilha, porque você pode obter estouro de pilha, mas normalmente este problema é resolvido usando contêineres STL e lembre-se std::stringtambém é contêiner, alguns caras esqueceram :)


5

Digamos que você class Acontenha class BQuando você quiser chamar alguma função de class Bfora, class Avocê simplesmente obterá um ponteiro para esta classe e poderá fazer o que quiser e isso também mudará o contexto de class Bsua classe .class A

Mas tenha cuidado com o objeto dinâmico


5

Há muitos benefícios em usar ponteiros para objetar -

  1. Eficiência (como você já apontou). Passar objetos para funções significa criar novas cópias do objeto.
  2. Trabalhando com objetos de bibliotecas de terceiros. Se o seu objeto pertencer a um código de terceiros e os autores pretenderem usá-lo apenas por meio de ponteiros (sem construtores de cópia, etc.), a única maneira de contornar esse objeto é usando ponteiros. Passar por valor pode causar problemas. (Cópia profunda / cópia superficial).
  3. se o objeto possuir um recurso e você desejar que a propriedade não seja compartilhada com outros objetos.

3

Isso foi discutido detalhadamente, mas em Java tudo é um ponteiro. Não faz distinção entre alocações de pilha e heap (todos os objetos são alocados na pilha), portanto você não percebe que está usando ponteiros. No C ++, você pode misturar os dois, dependendo dos requisitos de memória. O desempenho e o uso da memória são mais determinísticos em C ++ (duh).


3
Object *myObject = new Object;

Isso criará uma referência a um Objeto (na pilha) que deve ser excluído explicitamente para evitar vazamento de memória .

Object myObject;

Isso criará um objeto (myObject) do tipo automático (na pilha) que será excluído automaticamente quando o objeto (myObject) ficar fora do escopo.


1

Um ponteiro faz referência direta à localização da memória de um objeto. Java não tem nada assim. Java possui referências que referenciam a localização do objeto através de tabelas de hash. Você não pode fazer nada como aritmética de ponteiro em Java com essas referências.

Para responder sua pergunta, é apenas sua preferência. Eu prefiro usar a sintaxe semelhante a Java.


Tabelas de hash? Talvez em algumas JVMs, mas não conte com isso.
precisa

E a JVM que acompanha o Java? É claro que você pode implementar QUALQUER COISA que você possa imaginar como uma JVM que use ponteiros diretamente ou um método que faça cálculos matemáticos. É como dizer "as pessoas não morrem de resfriado comum" e obter uma resposta "Talvez a maioria das pessoas não morra, mas não conte com isso!" Ha ha.
RioRicoRick 25/03

2
O @RioRicoRick HotSpot implementa as referências Java como ponteiros nativos, consulte docs.oracle.com/javase/7/docs/technotes/guides/vm/… Até onde posso ver, o JRockit faz o mesmo. Ambos suportam compactação OOP, mas nunca usam tabelas de hash. As consequências do desempenho provavelmente seriam desastrosas. Além disso, "é apenas a sua preferência" parece implicar que as duas são apenas sintaxes diferentes para comportamentos equivalentes, o que é claro que não são.
precisa saber é o seguinte


0

Com ponteiros ,

  • pode falar diretamente com a memória.

  • pode impedir muitos vazamentos de memória de um programa manipulando ponteiros.


4
" em C ++, usando ponteiros, você pode criar um coletor de lixo personalizado para seu próprio programa " que soa como uma péssima idéia.
quant

0

Uma razão para o uso de ponteiros é a interface com as funções C. Outro motivo é economizar memória; por exemplo: em vez de passar um objeto que contém muitos dados e possui um construtor de cópias com uso intensivo de processador para uma função, basta passar um ponteiro para o objeto, economizando memória e velocidade, especialmente se você estiver em loop, no entanto, um referência seria melhor nesse caso, a menos que você esteja usando uma matriz de estilo C.


0

Em áreas onde a utilização da memória é excelente, os ponteiros são úteis. Por exemplo, considere um algoritmo minimax, onde milhares de nós serão gerados usando rotina recursiva e, posteriormente, os utilizará para avaliar a próxima melhor jogada no jogo, a capacidade de desalocar ou redefinir (como em ponteiros inteligentes) reduz significativamente o consumo de memória. Enquanto a variável não-ponteiro continua ocupando espaço até que a chamada recursiva retorne um valor.


0

Incluirei um caso de uso importante de ponteiro. Quando você está armazenando algum objeto na classe base, mas pode ser polimórfico.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Portanto, neste caso, você não pode declarar bObj como um objeto direto, você precisa ter um ponteiro.


-5

"Necessidade é a mãe da invenção." A diferença mais importante que gostaria de destacar é o resultado de minha própria experiência em codificação. Às vezes você precisa passar objetos para funções. Nesse caso, se o seu objeto for de uma classe muito grande, passá-lo como um objeto copiará seu estado (o que você pode não querer ... E PODE SER MUITO GRANDE), resultando em uma sobrecarga de cópia do objeto. Enquanto o ponteiro for corrigido Tamanho de 4 bytes (assumindo 32 bits). Outras razões já foram mencionadas acima ...


14
você deve preferir passar por referência
bolov

2
Eu recomendo passando por constante referência como para a variável std::string test;que temos void func(const std::string &) {}, mas a menos que a função precisa mudar a entrada nesse caso, eu recomendo o uso de ponteiros (de modo que qualquer um que lê o código faz aviso &, e entende a função pode mudar a sua entrada)
Top- Mestre

-7

Já existem muitas respostas excelentes, mas deixe-me dar um exemplo:

Eu tenho uma classe simples de itens:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Eu faço um vetor para segurar um monte deles.

std::vector<Item> inventory;

Crio um milhão de objetos Item e os empurro de volta para o vetor. Classifico o vetor por nome e, em seguida, faço uma pesquisa binária iterativa simples para um nome de item específico. Eu testo o programa e leva mais de 8 minutos para concluir a execução. Então eu altero meu vetor de inventário da seguinte forma:

std::vector<Item *> inventory;

... e crie meus milhões de objetos de itens por meio de novos. As únicas alterações que faço no meu código são usar os ponteiros para os itens, exceto um loop que adiciono para limpeza de memória no final. Esse programa é executado em menos de 40 segundos, ou melhor que um aumento de velocidade de 10x. EDIT: O código está em http://pastebin.com/DK24SPeW Com as otimizações do compilador, ele mostra apenas um aumento de 3,4x na máquina em que acabei de testá-lo, o que ainda é considerável.


2
Bem, você está comparando os ponteiros então ou ainda compara os objetos reais? Duvido muito que outro nível de indireção possa melhorar o desempenho. Forneça o código! Você limpa adequadamente depois?
Stefan

1
@stefan Comparo os dados (especificamente, o campo de nome) dos objetos para a classificação e a pesquisa. Eu limpo corretamente, como já mencionei no post. a aceleração provavelmente se deve a dois fatores: 1) std :: vector push_back () copia os objetos; portanto, a versão do ponteiro precisa apenas copiar um único ponteiro por objeto. Isso tem vários impactos no desempenho, além de menos dados serem copiados, mas o alocador de memória da classe vetorial é menos afetado.
Darren

2
Aqui está o código que mostra praticamente nenhuma diferença para o seu exemplo: classificação. O código do ponteiro é 6% mais rápido que o código não-ponteiro apenas para a classificação, mas no geral é 10% mais lento que o código não-ponteiro. ideone.com/G0c7zw
stefan

3
Palavra-chave: push_back. Claro que isso copia. Você deveria estar emplaceno local ao criar seus objetos (a menos que precise que eles sejam armazenados em cache em outro lugar).
underscore_d

1
Vetores de ponteiros quase sempre estão errados. Por favor, não os recomende sem explicar detalhadamente as advertências e os prós e contras. Você parece ter encontrado um pro, que é apenas uma consequência de um contra-exemplo mal codificados, e deturpado ele
Leveza raças em órbita
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.