c++ - portugues - resolva os problemas




Por que devo usar um ponteiro em vez do objeto em si? (15)

Eu 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 isso 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?


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

Eu vou comparar como funciona dentro do corpo da função se você usa:

Object myObject;

Dentro da função, seu myObject será destruído assim que esta função retornar. Então isso é útil se você não precisa do seu objeto fora da sua função. Esse objeto será colocado na pilha de encadeamentos atual.

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

 Object *myObject = new Object;

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

Agora, se você for um programador Java, o segundo exemplo estará 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 o lixo, enquanto sob c ++ ele não será liberado, você deve em algum lugar chamar explicitamente `delete myObject; ' caso contrário, você introduzirá vazamentos de memória.

Desde c ++ 11 você pode usar formas seguras de alocações dinâmicas: new Object , armazenando 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 são frequentemente armazenados em containers, como map-s ou vector-s, eles gerenciam automaticamente a vida útil de seus objetos.


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 tem a mesma sintaxe do C ++, as linguagens são semelhantes. Nada pode estar mais longe da verdade. Essa desinformação é parte do motivo pelo qual os programadores Java vão para o C ++ e usam a sintaxe semelhante a Java sem entender as implicações de seu código.

Avante nós vamos

Mas não consigo descobrir por que devemos fazer isso 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. O heap é muito mais lento que a pilha, porque a pilha é muito simples em comparação com o heap. Variáveis ​​de armazenamento automáticas (também conhecidas como variáveis ​​de pilha) têm seus destruidores chamados assim que saem do escopo. Por exemplo:

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

Por outro lado, se você usar um ponteiro alocado dinamicamente, seu destruidor deve ser chamado manualmente. delete chama esse destruidor para você.

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

Isso não tem nada a ver com a new sintaxe predominante em C # e Java. Eles são usados ​​para propósitos completamente diferentes.

Benefícios da alocação dinâmica

1. Você não precisa saber o tamanho da matriz com antecedência

Um dos primeiros problemas que muitos programadores de C ++ enfrentam é que, quando estão aceitando entradas arbitrárias de usuários, você só pode alocar um tamanho fixo para uma variável de pilha. Você não pode alterar o tamanho das matrizes também. Por exemplo:

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

Claro, se você usou um std::string , o std::string internamente se redimensiona 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 : 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 das extensões do GCC. Então, o seguinte int arr[n] não deve ser confiável.

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. Arrays não são ponteiros

Como isso é um benefício que você pergunta? A resposta ficará clara quando você entender a confusão / o mito por trás dos arrays e ponteiros. É comumente assumido que eles são os mesmos, mas eles não são. Esse mito vem do fato de que ponteiros podem ser subscritos como matrizes e por causa de 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 seu sizeof informações. Portanto, 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 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 arrays é feita em Java e C #, os iniciantes não entendem a diferença.

3. Polimorfismo

Java e C # possuem recursos que permitem tratar objetos como outro, por exemplo, usando a palavra-chave as. Então, se alguém quiser tratar um objeto Entity como um objeto Player , você pode fazer Player player = Entity as Player; 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 obtida de forma 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();
}

Então diga se apenas os Triângulos tivessem uma função Rotate, seria um erro do compilador se você tentasse chamá-la em todos os objetos da classe. Usando dynamic_cast , você pode simular a palavra-chave as. Para ser claro, se um elenco falhar, ele retorna um ponteiro inválido. Portanto, o !test é essencialmente uma forma abreviada de verificar se o 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 utilizaria alocação dinâmica o tempo todo? Eu já te disse uma razão, a pilha está lenta. E se você não precisa de toda essa memória, não deveria abusar dela. Então, aqui estão algumas desvantagens em nenhuma ordem particular:

  • É propenso a erros. Alocação de memória manual é perigosa e você está propenso a vazamentos. Se você não é proficiente em usar o depurador ou valgrind (uma ferramenta de vazamento de memória), você pode tirar o cabelo da cabeça. Felizmente expressões idiomáticas RAII e ponteiros inteligentes aliviam isso um pouco, mas você deve estar familiarizado com práticas como The Rule Of Three e The Rule Of Five. É muita informação para absorver, e os iniciantes que não sabem ou não se importam vão cair nessa armadilha.

  • Não é necessário. Ao contrário de Java e C #, onde é idiomático para usar a new palavra-chave em todos os lugares, em C ++, você só deve usá-lo se você precisar. A frase comum diz que tudo parece um prego se você tem 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 Java e C # começam usando ponteiros sem entendê-lo! Isso está literalmente saindo do pé errado. Você deve abandonar tudo o que sabe porque a sintaxe é uma coisa, aprender a língua é outra.

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

Uma otimização que muitos compiladores fazem são coisas chamadas de elisão e otimização do valor de retorno . Essas coisas podem evitar copys desnecessários, o que é útil para objetos que são muito grandes, como um vetor contendo 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 ao início da mudança de semântica e ponteiros inteligentes .

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


C ++ oferece 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, boolean etc). Se você quiser usar o C ++ não apenas como um brinquedo estranho, então é melhor você conhecer a diferença entre essas três formas.

Java finge que não existe tal 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, o java pode vazar memória ). Na verdade, o GC lhe dá uma falsa sensação de segurança. Quanto maior o seu SUV, maior o seu caminho para o evacuador.

O C ++ deixa você cara-a-cara com o gerenciamento do ciclo de vida do objeto. Bem, existem meios para lidar com isso (família de ponteiros inteligentes , QObject no Qt e assim por diante), mas nenhum deles pode ser usado na maneira 'acionar e esquecer' 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.

Não está com medo ainda? Ok: referências cíclicas - manuseie você mesmo, humano. E lembre-se: mate cada objeto com precisão uma vez, nós, os tempos de execução do C ++, não gostamos daqueles que mexem com cadáveres, deixam os mortos sozinhos.

Então, de volta à sua pergunta.

Quando você passa seu objeto por valor, não por ponteiro ou por referência, você copia o objeto (o objeto inteiro, seja um par de bytes ou um enorme despejo de banco de dados - você é esperto o suficiente para se preocupar em evitar isso, 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, você copia apenas alguns bytes (4 em sistemas de 32 bits, 8 em 64 bits), a saber - o endereço deste objeto. E para mostrar isso a todos, você usa esse sofisticado operador '->' quando acessa os membros. Ou você pode usar a combinação de '*' e '.'

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

E, para explodir sua mente mais uma vez: quando você declara várias variáveis ​​separadas por vírgulas, então (observe as mãos):

  • Tipo é dado a todos
  • Valor / ponteiro / modificador de 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

Em C ++, os objetos alocados na pilha (usando Object object; instrução dentro de um bloco) somente viverão dentro do escopo em que são 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 continuam a viver em heap até você chamar delete obj .

Eu criaria um objeto na pilha quando eu gosto de usar o objeto não apenas no bloco de código que o declarou / alocou.


Há muitas respostas excelentes para essa questão, incluindo os casos de uso importantes de declarações de encaminhamento, polimorfismo etc., mas sinto que uma parte da "alma" de sua pergunta não é respondida - ou seja, o que as diferentes sintaxes significam 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 o caminho alternativo 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 nisso é que - mais ou menos - Java (implicitamente) lida com ponteiros para objetos, enquanto o C ++ pode manipular os ponteiros para os objetos ou os próprios objetos. Há exceções para isso - por exemplo, se você declarar tipos Java "primitivos", eles são valores reais que são copiados e não ponteiros. 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 o caminho correto ou errado para lidar com as coisas; no entanto, outras respostas cobriram isso satisfatoriamente. A idéia geral é que em C ++ você tem muito mais controle sobre a vida útil dos objetos e sobre onde eles irão viver.

Take home point - a construção Object * object = new Object() é, na verdade, a mais próxima da semântica típica de Java (ou C #).


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.


Há muitos benefícios de usar ponteiros para objetos

  1. Eficiência (como você já apontou). Passar objetos para funções significa criar novas cópias de objetos.
  2. Trabalhando com objetos de bibliotecas de terceiros. Se o seu objeto pertence a um código de terceiros e os autores pretendem o uso de seus objetos apenas através de ponteiros (sem construtores de cópia, etc.), a única maneira de passar este objeto é usando ponteiros. Passar por valor pode causar problemas. (Cópia profunda / problemas de cópia rasos).
  3. se o objeto possui um recurso e você deseja que a propriedade não seja salva com outros objetos.

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

Eu tenho uma classe de item simples:

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

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

std::vector<Item> inventory;

Eu crio um milhão de objetos Item e os empurro de volta para o vetor. Classifico o vetor pelo nome e, em seguida, faço uma pesquisa binária iterativa simples para um nome de item específico. Eu testo o programa e demora mais de 8 minutos para concluir a execução. Então eu mudo meu vetor de inventário assim:

std::vector<Item *> inventory;

... e crie meus milhões de objetos Item por meio de novos. As únicas mudanças que eu faço para o meu código são para usar os ponteiros para itens, com exceção de um loop que eu 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 otimizações de compilador mostra apenas um aumento de 3.4x na máquina que eu acabei de testar, que ainda é considerável.


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

Outra coisa que você mencionou na sua pergunta:

Object *myObject = new Object;

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

delete myObject;

Isso chama o 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 isso você pode tentar ponteiros compartilhados , isso diminui um pouco o desempenho, mas é muito mais fácil trabalhar com eles.

E agora alguma introdução acabou e volta a questionar.

Você pode usar ponteiros em vez de objetos para obter melhor desempenho durante a transferência de dados entre funções.

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

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

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

Outra coisa a mencionar, quando você cria um objeto de forma regular , você aloca memória na pilha, mas enquanto você cria com newvocê aloca heap. É muito mais rápido alocar pilha, mas é meio pequeno para matrizes realmente grandes de dados, então se você precisar de um objeto grande você deve usar o heap, porque você pode obter um estouro de pilha, mas geralmente esse problema é resolvido usando containers STL e lembre-se std::stringtambém é contêiner, alguns caras esqueceram :)


Isso tem sido discutido em detalhes, mas em Java tudo é um ponteiro. Ele não faz distinção entre alocações de pilha e heap (todos os objetos são alocados no heap), portanto, você não percebe que está usando ponteiros. Em C ++, você pode misturar os dois, dependendo dos seus requisitos de memória. O uso de desempenho e memória é mais determinístico em C ++ (duh).


Tecnicamente, é um problema de alocação de memória, no entanto, 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, você não poderá mais acessá-lo após o bloco de código em que está definido, ao passo que se você definir um ponteiro com "novo", pode acessá-lo de qualquer lugar que você tenha um ponteiro para essa memória até que você chame "delete" no mesmo ponteiro. 2) Se você quiser passar argumentos para uma função, você quer passar um ponteiro ou uma referência para ser mais eficiente. Quando você passa um Objeto, o objeto é copiado, se este for um objeto que usa muita memória, isso pode consumir a CPU (por exemplo, você copia um vetor cheio de dados). Quando você passa um ponteiro, tudo que você passa é um int (dependendo da implementação, mas a maioria deles é um int).

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


Um ponteiro referencia diretamente a localização da memória de um objeto. Java não tem nada disso. Java possui referências que fazem referência à localização do objeto por meio de tabelas hash. Você não pode fazer nada como a aritmética de ponteiros em Java com essas referências.

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


Vamos dizer que você tem class Aque conter class BQuando você quer chamar alguma função de class Bfora, class Avocê simplesmente obterá um ponteiro para esta classe e poderá fazer o que quiser e também mudará o contexto de class Bsua classe .class A

Mas tenha cuidado com o objeto dinâmico


Você não deveria . Pessoas (muitas pessoas, infelizmente) escrevem por ignorância.

Às vezes, a alocação dinâmica tem seu lugar, mas, nos exemplos que você dá, está errado .

Se você quiser pensar em eficiência, então isso é pior , porque introduz a indireção sem um bom motivo. Esse tipo de programação é mais lento e mais propenso a erros .


Object *myObject = new Object;

Isso cria uma referência a um objeto (no heap) 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) sair do escopo.





c++11