Como os membros da classe C ++ são inicializados se eu não faço isso explicitamente?

158

Suponha que eu tenho uma classe com memebers privadas ptr, name, pname, rname, crnamee age. O que acontece se eu não os inicializar? Aqui está um exemplo:

class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

E então eu faço:

int main() {
    Example ex;
}

Como os membros são inicializados no ex? O que acontece com os ponteiros? Faça stringe intobtenha 0 inicializado com construtores padrão string()e int()? E o membro de referência? E o que dizer das referências const?

Sobre o que mais devo saber?

Alguém conhece um tutorial que cubra esses casos? Talvez em alguns livros? Tenho acesso na biblioteca da universidade a muitos livros em C ++.

Gostaria de aprender para poder escrever programas melhores (sem erros). Qualquer feedback ajudaria!

bodacydo
fonte
3
Para recomendações de livros, consulte stackoverflow.com/questions/388242/…
Mike Seymour
Mike, uau, quero dizer capítulo de um livro que explica isso. Livro não inteiro! :)
bodacydo
Provavelmente, seria uma boa idéia ler um livro inteiro sobre um idioma em que você pretende programar. E se você já leu um e não explicou isso, não era um livro muito bom.
Tyler McHenry
2
Scott Meyers (um popular guru ex-profissional de consultoria em C ++) afirma no Effective C ++ : "as regras são complicadas - muito complicadas para valer a pena memorizar, na minha opinião ... certifique-se de que todos os construtores inicializem tudo no objeto". Portanto, na sua opinião, a maneira mais fácil de (tentar) escrever código "livre de bugs" não é tentar memorizar as regras (e de fato ele não as expõe no livro), mas inicializar explicitamente tudo. Observe, no entanto, que mesmo se você adotar essa abordagem em seu próprio código, poderá trabalhar em projetos escritos por pessoas que não o fazem, portanto as regras ainda podem ser valiosas.
Kyle Strand
2
@TylerMcHenry Quais livros sobre C ++ você considera "bons"? Eu li vários livros sobre C ++, mas nenhum deles explicou isso completamente. Conforme observado no meu comentário anterior, Scott Meyers se recusa explicitamente a fornecer as regras completas no Effective C ++ . Também li o C ++ moderno eficaz de Meyers , o Conhecimento comum em C ++ em Dewhurst e A Tour of C ++ em Stroustrup . Na minha memória, nenhum deles explicou as regras completas. Obviamente, eu poderia ter lido o padrão, mas dificilmente consideraria um "bom livro"! : D E espero que o Stroustrup provavelmente o explique na linguagem de programação C ++ .
Kyle Strand

Respostas:

207

Em vez de inicialização explícita, a inicialização de membros em classes funciona de forma idêntica à inicialização de variáveis ​​locais em funções.

Para objetos , seu construtor padrão é chamado. Por exemplo, para std::string, o construtor padrão o define como uma sequência vazia. Se a classe do objeto não tiver um construtor padrão, será um erro de compilação se você não inicializá-lo explicitamente.

Para tipos primitivos (ponteiros, entradas, etc), eles não são inicializados - eles contêm qualquer lixo arbitrário que aconteceu anteriormente naquele local de memória.

Para referências (por exemplo std::string&), é ilegal não inicializá-las, e seu compilador irá reclamar e se recusar a compilar esse código. As referências sempre devem ser inicializadas.

Portanto, no seu caso específico, se eles não forem explicitamente inicializados:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk
Tyler McHenry
fonte
4
+1. Vale a pena notar que, pela estrita definição padrão, instâncias de tipos primitivos, juntamente com uma variedade de outras coisas (qualquer região de armazenamento), são todos considerados objetos .
stinky472
7
"Se a classe do objeto não tiver um construtor padrão, será um erro de compilação se você não inicializar explicitamente" isso está errado ! Se uma classe não tem um construtor padrão, é dado um construtor padrão vazio.
Wizard
19
@ wiz Acho que ele quis dizer literalmente 'se o objeto não possui um construtor padrão', como também não gerou, o que seria o caso se a classe definisse explicitamente quaisquer construtores que não o padrão (nenhum ctor padrão será gerado). Se ficarmos muito pedanáticos, provavelmente confundiremos mais do que ajuda e Tyler faz um bom argumento sobre isso em sua resposta a mim antes.
stinky472
8
@ wiz-loz eu diria que foo tem um construtor, é apenas implícito. Mas isso é realmente um argumento da semântica.
Tyler McHenry
4
Eu interpreto "construtor padrão" como um construtor que pode ser chamado sem argumentos. Esse seria um dos que você define ou gerou implicitamente pelo compilador. Portanto, a falta disso significa nem definido por você nem gerado. Ou é assim que eu vejo.
5ound
28

Primeiro, deixe-me explicar o que é uma lista de inicializadores de mem . Um mem-inicializador-lista é uma lista separada por vírgulas de mem-inicializador s, onde cada mem-inicializador é um nome membro seguido por (, seguido por uma lista de expressão , seguido por um ). A lista de expressões é como o membro é construído. Por exemplo, em

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

a lista de inicializadores de mem do construtor sem argumentos fornecido pelo usuário name(s_str, s_str + 8), rname(name), crname(name), age(-4). Essa lista de inicializadores de memórias significa que oname membro é inicializado pelo std::stringconstrutor que leva dois iterators de entrada , o rnamemembro é inicializado com uma referência para name, o crnamemembro é inicializado com um const-referência para name, e o agemembro é inicializado com o valor -4.

Cada construtor tem seu próprio lista de inicializadores de memórias e os membros podem ser inicializados apenas em uma ordem prescrita (basicamente a ordem na qual os membros são declarados na classe). Assim, os membros Examplesó podem ser inicializados na ordem: ptr, name, pname, rname, crname, e age.

Quando você não especifica um mem-inicializador de um membro, o padrão C ++ diz:

Se a entidade for um membro de dados não estático ... do tipo de classe ..., a entidade será inicializada por padrão (8.5). ... Caso contrário, a entidade não será inicializada.

Aqui porque name ser um membro de dados não estáticos do tipo de classe, ele será inicializado por padrão se nenhum inicializador tiver namesido especificado na lista de inicializadores de mem . Todos os outros membros de Examplenão têm tipo de classe, portanto, não são inicializados.

Quando o padrão diz que eles não foram inicializados, isso significa que eles podem ter qualquer valor. Assim, como o código acima não foi inicializado pname, poderia ser qualquer coisa.

Observe que você ainda precisa seguir outras regras, como a regra de que as referências sempre devem ser inicializadas. É um erro do compilador não inicializar referências.

Daniel Trebbien
fonte
Essa é a melhor maneira de inicializar membros quando você deseja separar estritamente a declaração (in .h) e a definição (in .cpp) sem mostrar muitos detalhes internos.
Matthieu
12

Você também pode inicializar membros de dados no ponto em que os declara:

class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

Eu uso esse formulário quase que exclusivamente, embora tenha lido que algumas pessoas o consideram 'ruim', talvez porque tenha sido introduzido apenas recentemente - acho que no C ++ 11. Para mim é mais lógico.

Outra faceta útil para as novas regras é como inicializar membros de dados que são classes. Por exemplo, suponha que CDynamicStringseja uma classe que encapsule o tratamento de strings. Possui um construtor que permite especificar seu valor inicial CDynamicString(wchat_t* pstrInitialString). Você pode muito bem usar essa classe como um membro de dados dentro de outra classe - digamos, uma classe que encapsule um valor de registro do Windows que, nesse caso, armazene um endereço postal. Para 'codificar' o nome da chave do Registro no qual isso grava, você usa chaves:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

Observe que a segunda classe de string que contém o endereço postal real não possui um inicializador; portanto, seu construtor padrão será chamado na criação - talvez configurando-o automaticamente para uma string em branco.


fonte
9

Se o exemplo da classe for instanciado na pilha, o conteúdo dos membros escalares não inicializados será aleatório e indefinido.

Para uma instância global, os membros escalares não inicializados serão zerados.

Para membros que são instâncias de classes, seus construtores padrão serão chamados, para que seu objeto string seja inicializado.

  • int *ptr; // ponteiro não inicializado (ou zerado se for global)
  • string name; // construtor chamado, inicializado com uma string vazia
  • string *pname; // ponteiro não inicializado (ou zerado se for global)
  • string &rname; // erro de compilação se você não inicializar isso
  • const string &crname; // erro de compilação se você não inicializar isso
  • int age; // valor escalar, não inicializado e aleatório (ou zerado se for global)
Paul Dixon
fonte
Eu experimentei e parece que string nameestá vazio depois de inicializar a classe na pilha. Você tem certeza absoluta de sua resposta?
bodacydo
1
corda vai ter um construtor que forneceu uma string vazia por padrão - Eu vou esclarecer a minha resposta
Paul Dixon
@bodacydo: Paul está correto, mas se você se importa com esse comportamento, nunca é demais ser explícito. Jogue-o na lista de inicializadores.
Stephen
Obrigado por esclarecer e explicar!
bodacydo
2
Não é aleatório! Aleatório é palavra muito grande para isso! Se os membros escalares fossem aleatórios, não precisaríamos de outros geradores de números aleatórios. Imagine um programa que analise "sobras" de dados - como arquivos não apagados na memória - os dados estão longe de serem aleatórios. Nem é indefinido! Geralmente é difícil de definir, porque geralmente não sabemos o que nossa máquina faz. Se isso "dados aleatórios" que você acabou recuperados é a única imagem do seu pai, sua mãe pode até achar que é ofensivo se você disser que a sua aleatório ...
slyy2048
5

Membros não estáticos não inicializados conterão dados aleatórios. Na verdade, eles terão apenas o valor da localização da memória à qual estão atribuídos.

Obviamente, para parâmetros do objeto (como string), o construtor do objeto poderia fazer uma inicialização padrão.

No seu exemplo:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value
bruxo
fonte
2

Membros com um construtor terão seu construtor padrão chamado para inicialização.

Você não pode depender do conteúdo dos outros tipos.

janm
fonte
0

Se estiver na pilha, o conteúdo de membros não inicializados que não têm seu próprio construtor será aleatório e indefinido. Mesmo que seja global, seria uma má idéia confiar neles como zerados. Esteja na pilha ou não, se um membro tiver seu próprio construtor, isso será chamado para inicializá-lo.

Portanto, se você tiver a string * pname, o ponteiro conterá lixo aleatório. mas para o nome da string, o construtor padrão da string será chamado, fornecendo uma string vazia. Para suas variáveis ​​de tipo de referência, não tenho certeza, mas provavelmente será uma referência a algum pedaço aleatório de memória.

George
fonte
0

Depende de como a classe é construída

Responder a essa pergunta vem do entendimento de uma enorme declaração de caso de mudança no padrão da linguagem C ++, e uma que é difícil para os meros mortais obterem intuição.

Como um exemplo simples de como as coisas são difíceis:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

Na inicialização padrão, você começaria em: https://en.cppreference.com/w/cpp/language/default_initialization , vamos para a parte "Os efeitos da inicialização padrão são" e iniciamos a instrução case:

  • "se T é um não-POD ": não (a definição de POD é, por si só, uma enorme declaração de chave)
  • "se T é um tipo de matriz": não
  • "caso contrário, nada é feito": portanto, é deixado um valor indefinido

Então, se alguém decidir inicializar o valor, vamos para https://en.cppreference.com/w/cpp/language/value_initialization "Os efeitos da inicialização do valor são" e iniciamos a instrução case:

  • "se T é um tipo de classe sem construtor padrão ou com um construtor padrão excluído ou fornecido pelo usuário": não é o caso. Agora você passará 20 minutos pesquisando esses termos no Google:
    • temos um construtor padrão definido implicitamente (em particular porque nenhum outro construtor foi definido)
    • não é fornecido pelo usuário (definido implicitamente)
    • não é excluído ( = delete)
  • "se T é um tipo de classe com um construtor padrão que não é fornecido nem excluído pelo usuário": yes
    • "o objeto é inicializado com zero e, em seguida, é inicializado por padrão se tiver um construtor padrão não trivial": nenhum construtor não trivial, apenas inicializado com zero. A definição de "zero-initialize" pelo menos é simples e faz o que você espera: https://en.cppreference.com/w/cpp/language/zero_initialization

É por isso que eu recomendo fortemente que você nunca confie na inicialização zero "implícita". A menos que haja fortes razões de desempenho, inicialize tudo explicitamente, seja no construtor se você definiu um ou usando a inicialização agregada. Caso contrário, você torna as coisas muito arriscadas para futuros desenvolvedores.

Ciro Santilli adicionou uma nova foto
fonte