Classes C ++ - C++ classes

Uma classe em C ++ é um tipo definido pelo usuário ou estrutura de dados declarada com palavra - chaveclass que possui dados e funções (também chamadas de variáveis ​​de membro e funções de membro ) como seus membros, cujo acesso é governado pelos três especificadores de acesso private , protected ou public . Por padrão, o acesso aos membros de uma classe C ++ é privado . Os membros privados não são acessíveis fora da classe; eles podem ser acessados ​​apenas por meio de métodos da classe. Os membros públicos formam uma interface com a classe e são acessíveis fora da classe.

As instâncias de um tipo de dados de classe são conhecidas como objetos e podem conter variáveis ​​de membro, constantes , funções de membro e operadores sobrecarregados definidos pelo programador.

Diferenças entre uma estrutura e uma classe em C ++

Em C ++, uma classe definida com a classpalavra-chave possui membros privados e classes básicas por padrão. Uma estrutura é uma classe definida com a structpalavra - chave. Seus membros e classes base são públicos por padrão. Na prática, structs são normalmente reservados para dados sem funções. Ao derivar uma estrutura de uma classe / estrutura, o especificador de acesso padrão para uma classe / estrutura base é público. E ao derivar uma classe, o especificador de acesso padrão é privado.

Classes agregadas

Uma classe agregada é uma classe sem construtores declarados pelo usuário, sem membros de dados não estáticos privados ou protegidos, sem classes de base e sem funções virtuais. Essa classe pode ser inicializada com uma lista separada por vírgulas entre chaves de cláusulas inicializadoras. O código a seguir tem a mesma semântica em C e C ++.

struct C {
  int a;
  double b;
};

struct D {
  int a; 
  double b;
  C c;
};

// initialize an object of type C with an initializer-list
C c = {1, 2.0};

// D has a sub-aggregate of type C. In such cases initializer-clauses can be nested
D d = {10, 20.0, {1, 2.0}};

POD-structs

Uma POD-struct (Plain Old Data Structure) é uma classe agregada que não tem membros de dados não estáticos do tipo não-estrutura POD, não-união POD (ou matriz de tais tipos) ou referência, e não tem nenhum usuário operador de atribuição definido e nenhum destruidor definido pelo usuário . Um POD-estrutura poderia ser considerado o C ++ equivalente de um C struct. Na maioria dos casos, uma estrutura POD terá o mesmo layout de memória que uma estrutura correspondente declarada em C. Por essa razão, as estruturas POD são algumas vezes coloquialmente referidas como "estruturas estilo C".

Propriedades compartilhadas entre structs em C e POD-structs em C ++

  • Membros de dados são alocados de forma que membros posteriores tenham endereços mais altos dentro de um objeto, exceto quando separados por um especificador de acesso.
  • Dois tipos de estruturas POD são compatíveis com o layout se tiverem o mesmo número de membros de dados não estáticos e os membros de dados não estáticos correspondentes (em ordem) têm tipos compatíveis com o layout.
  • Uma estrutura POD pode conter preenchimento sem nome .
  • Um ponteiro para um objeto de estrutura POD, adequadamente convertido usando uma conversão de reinterpretação , aponta para seu membro inicial e vice-versa, o que implica que não há preenchimento no início de uma estrutura POD.
  • Uma estrutura POD pode ser usada com o deslocamento da macro.

Declaração e uso

As classes C ++ têm seus próprios membros. Esses membros incluem variáveis ​​(incluindo outras estruturas e classes), funções (identificadores específicos ou operadores sobrecarregados) conhecidos como métodos, construtores e destruidores. Os membros são declarados como publicamente ou privadamente acessíveis usando os especificadores public:e private:access respectivamente. Qualquer membro encontrado após um especificador terá o acesso associado até que outro especificador seja encontrado. Também há herança entre classes que podem fazer uso do protected:especificador.

Classe global e local

Uma classe definida fora de todos os métodos é uma classe global porque seus objetos podem ser criados de qualquer lugar no programa. Se for definido dentro de um corpo de função, então é uma classe local porque os objetos de tal classe são locais para o escopo da função.

Declaração básica e variáveis ​​de membro

As classes são declaradas com a palavra-chaveclass ou . A declaração dos membros está incluída nesta declaração. struct

struct Person {

  string name;
  int age;
};
class Person {
 public:
  string name;
  int age;
};

As definições acima são funcionalmente equivalentes. Qualquer um dos códigos definirá objetos do tipo Personcomo tendo dois membros de dados públicos, namee age. Os pontos e vírgulas após as chaves de fechamento são obrigatórios.

Após uma dessas declarações (mas não ambas), Personpode ser usado da seguinte maneira para criar variáveis ​​recém-definidas do Persontipo de dados:

#include <iostream>
#include <string>

struct Person {
  std::string name;
  int age;
};

int main() {
  Person a;
  Person b;
  a.name = "Calvin";
  b.name = "Hobbes";
  a.age = 30;
  b.age = 20;
  std::cout << a.name << ": " << a.age << std::endl;
  std::cout << b.name << ": " << b.age << std::endl;
}

Executar o código acima resultará

Calvin: 30
Hobbes: 20

Funções de membro

Um recurso importante da classe e estrutura C ++ são as funções de membro . Cada tipo de dados pode ter suas próprias funções integradas (chamadas de métodos) que têm acesso a todos os membros (públicos e privados) do tipo de dados. No corpo dessas funções de membro não estáticas, a palavra-chave thispode ser usada para se referir ao objeto para o qual a função é chamada. Isso é comumente implementado passando o endereço do objeto como um primeiro argumento implícito para a função. Pegue o Persontipo acima como exemplo novamente:

#include <iostream>

class Person {
 public:
  void Print() const;

 private:
  std::string name_;
  int age_ = 5;
};

void Person::Print() const {
  std::cout << name_ << ':' << age_ << '\n';
  // "name_" and "age_" are the member variables. The "this" keyword is an
  // expression whose value is the address of the object for which the member
  // was invoked. Its type is "const Person*", because the function is declared
  // const.
}

No exemplo acima, a Printfunção é declarada no corpo da classe e definida qualificando-a com o nome da classe seguido por ::. Ambos name_e age_são privados (padrão para a classe) e Printsão declarados como públicos, o que é necessário se for usado de fora da classe.

Com a função de membro Print, a impressão pode ser simplificada em:

a.Print();
b.Print();

onde ae bacima são chamados de remetentes, e cada um deles fará referência às suas próprias variáveis ​​de membro quando a Print()função for executada.

É prática comum separar a declaração de classe ou estrutura (chamada de interface) e a definição (chamada de implementação) em unidades separadas. A interface, necessária para o usuário, é mantida em um cabeçalho e a implementação é mantida separadamente na forma de origem ou compilada.

Herança

O layout de classes não-POD na memória não é especificado pelo padrão C ++. Por exemplo, muitos compiladores C ++ populares implementam herança única por concatenação dos campos da classe pai com os campos da classe filho, mas isso não é exigido pelo padrão. Essa escolha de layout torna a referência a uma classe derivada por meio de um ponteiro para o tipo de classe pai uma operação trivial.

Por exemplo, considere

struct P {
  int x;
};
struct C : P {
  int y;
};

Uma instância de Pcom um P* papontando para ele pode ter a seguinte aparência na memória:

+----+
|P::x|
+----+
↑
p

Uma instância de Ccom um P* papontando para ele pode ter a seguinte aparência:

+----+----+
|P::x|C::y|
+----+----+
↑
p

Portanto, qualquer código que manipule os campos de um Pobjeto pode manipular os Pcampos dentro do Cobjeto sem ter que considerar nada sobre a definição dos Ccampos de. Um programa C ++ escrito corretamente não deve fazer suposições sobre o layout dos campos herdados, em qualquer caso. Usar os operadores de conversão de tipo static_cast ou dynamic_cast garantirá que os ponteiros sejam convertidos corretamente de um tipo para outro.

A herança múltipla não é tão simples. Se uma classe Dherda Pe C, os campos de ambos os pais precisam ser armazenados em alguma ordem, mas (no máximo) apenas uma das classes pai pode estar localizada na frente da classe derivada. Sempre que o compilador precisar converter um ponteiro do Dtipo para Pou C, o compilador fornecerá uma conversão automática do endereço da classe derivada para o endereço dos campos da classe base (normalmente, este é um cálculo de deslocamento simples).

Para saber mais sobre herança múltipla, consulte herança virtual .

Operadores sobrecarregados

Em C ++, os operadores , como + - * /, podem ser sobrecarregados para atender às necessidades dos programadores. Esses operadores são chamados de operadores sobrecarregáveis .

Por convenção, operadores sobrecarregados deve comportar-se quase o mesmo que eles fazem em built-in tipos de dados ( int, float, etc.), mas isso não é necessário. Pode-se declarar uma estrutura chamada Integerem que a variável realmente armazena um inteiro, mas chamando Integer * Integera soma, em vez do produto, dos inteiros pode ser retornado:

struct Integer {
  Integer() = default;
  Integer(int j) : i{j} {}

  Integer operator*(const Integer& k) const {
    return Integer(i + k.i);
  }

  int i = 0;
};

O código acima fez uso de um construtor para "construir" o valor de retorno. Para uma apresentação mais clara (embora isso possa diminuir a eficiência do programa se o compilador não puder otimizar a instrução equivalente acima), o código acima pode ser reescrito como:

Integer operator*(const Integer& k) const {
  Integer m;
  m.i = i + k.i;
  return m;
}

Os programadores também podem colocar um protótipo do operador na structdeclaração e definir a função do operador no escopo global:

struct Integer {
  Integer() = default;
  Integer(int j) : i{j} {}

  Integer operator*(const Integer& k) const;

  int i = 0;
};
 
Integer Integer::operator*(const Integer& k) const {
  return Integer(i * k.i);
}

iacima representa a própria variável de membro do remetente, enquanto k.irepresenta a variável de membro da variável de argumento k.

A constpalavra-chave aparece duas vezes no código acima. A primeira ocorrência, o argumento const integer& k, indica que a variável do argumento não será alterada pela função. A segunda incidência no final da declaração promete ao compilador que o remetente não seria alterado pela execução da função.

Em const integer& k, o e comercial (&) significa "passar por referência". Quando a função é chamada, um ponteiro para a variável é passado para a função, em vez do valor da variável.

As mesmas propriedades de sobrecarga acima também se aplicam às classes.

Observe que aridade , associatividade e precedência de operadores não podem ser alteradas.

Operadores binários sobrecarregáveis

Operadores binários (operadores com dois argumentos) são sobrecarregados ao declarar uma função com um operador "identificador" (algo) que chama um único argumento. A variável à esquerda do operador é o remetente, enquanto a variável à direita é o argumento.

Integer i = 1; 
/* we can initialize a structure variable this way as
   if calling a constructor with only the first
   argument specified. */
Integer j = 3;
/* variable names are independent of the names of the
   member variables of the structure. */
Integer k = i * j;
std::cout << k.i << '\n';

'3' seria impresso.

A seguir está uma lista de operadores binários sobrecarregáveis:

Operador Uso geral
+ - * /% Cálculo aritmético
^ &! << >> Cálculo bit a bit
<> ==! = <=> = Comparação lógica
&& Conjunção lógica
!! Disjunção lógica
= << = >> = Atribuição composta
, (sem uso geral)

O operador '=' (atribuição) entre duas variáveis ​​do mesmo tipo de estrutura é sobrecarregado por padrão para copiar todo o conteúdo das variáveis ​​de uma para outra. Ele pode ser substituído por outra coisa, se necessário.

Os operadores devem ser sobrecarregados um a um, ou seja, nenhuma sobrecarga está associada. Por exemplo, <não é necessariamente o oposto de >.

Operadores sobrecarregáveis ​​unários

Enquanto alguns operadores, conforme especificado acima, levam dois termos, remetente à esquerda e o argumento à direita, alguns operadores têm apenas um argumento - o remetente, e eles são chamados de "unários". Os exemplos são o sinal negativo (quando nada é colocado à esquerda dele) e o " NÃO lógico " ( ponto de exclamação , !).

O remetente de operadores unários pode estar à esquerda ou à direita do operador. A seguir está uma lista de operadores sobrecarregáveis ​​unários:

Operador Uso geral Posição do remetente
+ - Sinal positivo / negativo direito
* & Desreferência direito
! ~ NÃO lógico / bit a bit direito
++ - Pré-incremento / decremento direito
++ - Pós-incremento / decremento deixou

A sintaxe de uma sobrecarga de um operador unário, onde o remetente está à direita, é a seguinte:

return_type operator@ ()

Quando o remetente está à esquerda, a declaração é:

return_type operator@ (int)

@acima significa que o operador está sobrecarregado. Substituir return_typecom o tipo de dados do valor de retorno ( int, bool, estruturas etc.)

O intparâmetro significa essencialmente nada além de uma convenção para mostrar que o remetente está à esquerda do operador.

const argumentos podem ser adicionados ao final da declaração, se aplicável.

Suportes de sobrecarga

O colchete []e o colchete redondo ()podem ser sobrecarregados em estruturas C ++. O colchete deve conter exatamente um argumento, enquanto o colchete pode conter qualquer número específico de argumentos ou nenhum argumento.

A declaração a seguir sobrecarrega o colchete.

return_type operator[] (argument)

O conteúdo dentro do colchete é especificado na argumentparte.

O suporte redondo está sobrecarregado de maneira semelhante.

return_type operator() (arg1, arg2, ...)

O conteúdo do colchete na chamada do operador é especificado no segundo colchete.

Além dos operadores especificados acima, o operador de seta ( ->), a seta com estrela ( ->*), a newpalavra - chave e a deletepalavra - chave também podem ser sobrecarregados. Esses operadores relacionados à memória ou ao ponteiro devem processar as funções de alocação de memória após a sobrecarga. Como o =operador de atribuição ( ), eles também são sobrecarregados por padrão se nenhuma declaração específica for feita.

Construtores

Às vezes, os programadores podem querer que suas variáveis ​​tenham um valor padrão ou específico na declaração. Isso pode ser feito declarando construtores .

Person::Person(string name, int age) {
  name_ = name;
  age_ = age;
}

Variáveis ​​de membro podem ser inicializadas em uma lista de inicializadores, com a utilização de dois pontos, como no exemplo abaixo. Isso difere do anterior porque inicializa (usando o construtor), em vez de usar o operador de atribuição. Isso é mais eficiente para tipos de classe, pois só precisa ser construído diretamente; enquanto com atribuição, eles devem ser inicializados primeiro usando o construtor padrão e, em seguida, atribuídos a um valor diferente. Além disso, alguns tipos (como referências e tipos const ) não podem ser atribuídos e, portanto, devem ser inicializados na lista de inicializadores.

Person(std::string name, int age) : name_(name), age_(age) {}

Observe que as chaves não podem ser omitidas, mesmo se estiverem vazias.

Os valores padrão podem ser atribuídos aos últimos argumentos para ajudar a inicializar os valores padrão.

Person(std::string name = "", int age = 0) : name_(name), age_(age) {}

Quando nenhum argumento é fornecido ao construtor no exemplo acima, é equivalente a chamar o seguinte construtor sem argumentos (um construtor padrão):

Person() : name_(""), age_(0) {}

A declaração de um construtor se parece com uma função com o mesmo nome do tipo de dados. Na verdade, uma chamada para um construtor pode assumir a forma de uma chamada de função. Nesse caso, uma Personvariável de tipo inicializada pode ser considerada como o valor de retorno:

int main() {
  Person r = Person("Wales", 40);
  r.Print();
}

Uma sintaxe alternativa que faz a mesma coisa que o exemplo acima é

int main() {
  Person r("Wales", 40);
  r.Print();
}

Ações específicas do programa, que podem ou não estar relacionadas à variável, podem ser adicionadas como parte do construtor.

Person() {
  std::cout << "Hello!" << std::endl;
}

Com o construtor acima, um "Hello!" será impresso quando o Personconstrutor padrão for chamado.

Construtor padrão

Os construtores padrão são chamados quando os construtores não são definidos para as classes.

struct A {
  int b;
};
// Object created using parentheses.
A* a = new A();  // Calls default constructor, and b will be initialized with '0'.
// Object created using no parentheses.
A* a = new A;  // Allocate memory, then call default constructor, and b will have value '0'.
// Object creation without new.
A a;  // Reserve space for a on the stack, and b will have an unknown garbage value.

No entanto, se um construtor definido pelo usuário foi definido para a classe, ambas as declarações acima chamarão esse construtor definido pelo usuário, cujo código definido será executado, mas nenhum valor padrão será atribuído à variável b.

Destruidores

Um destruidor é o inverso de um construtor. É chamado quando uma instância de uma classe é destruída, por exemplo, quando um objeto de uma classe criada em um bloco (conjunto de chaves "{}") é excluído após a chave de fechamento, então o destruidor é chamado automaticamente. Será acionado ao esvaziar o local da memória que armazena as variáveis. Os destruidores podem ser usados ​​para liberar recursos, como memória alocada em heap e arquivos abertos quando uma instância dessa classe é destruída.

A sintaxe para declarar um destruidor é semelhante à de um construtor. Não há valor de retorno e o nome do método é igual ao nome da classe com um til (~) na frente.

~Person() {
  std::cout << "I'm deleting " << name_ << " with age " << age_ << std::endl;
}

Semelhanças entre construtores e destruidores

  • Ambos têm o mesmo nome da classe em que foram declarados.
  • Se não forem declarados pelo usuário, ambos estarão disponíveis em uma classe por padrão, mas agora eles só podem alocar e desalocar memória dos objetos de uma classe quando um objeto é declarado ou excluído.
  • Para uma classe derivada: Durante o tempo de execução do construtor da classe base, o construtor da classe derivada ainda não foi chamado; durante o tempo de execução do destruidor da classe base, o destruidor da classe derivada já foi chamado. Em ambos os casos, as variáveis ​​de membro da classe derivada estão em um estado inválido.

Modelos de aulas

Em C ++, as declarações de classe podem ser geradas a partir de modelos de classe. Esses modelos de classe representam uma família de classes. Uma declaração de classe real é obtida instanciando o modelo com um ou mais argumentos do modelo. Um modelo instanciado com um determinado conjunto de argumentos é chamado de especialização de modelo.

Propriedades

A sintaxe do C ++ tenta fazer com que todos os aspectos de uma estrutura se pareçam com os tipos de dados básicos . Portanto, os operadores sobrecarregados permitem que estruturas sejam manipuladas da mesma forma que números inteiros e de ponto flutuante, matrizes de estruturas podem ser declaradas com a sintaxe de colchetes ( some_structure variable_name[size]) e ponteiros para estruturas podem ser referenciados da mesma forma que ponteiros para dados internos tipos de dados.

Consumo de memória

O consumo de memória de uma estrutura é pelo menos a soma dos tamanhos de memória das variáveis ​​constituintes. Veja a TwoNumsestrutura abaixo como exemplo.

struct TwoNums {
  int a;
  int b;
};

A estrutura consiste em dois inteiros. Em muitos compiladores C ++ atuais, os inteiros são inteiros de 32 bits por padrão , portanto, cada uma das variáveis ​​de membro consome quatro bytes de memória. A estrutura inteira, portanto, consome pelo menos (ou exatamente) oito bytes de memória, como segue.

+----+----+
| a  | b  |
+----+----+

No entanto, o compilador pode adicionar preenchimento entre as variáveis ​​ou no final da estrutura para garantir o alinhamento de dados adequado para uma determinada arquitetura de computador, muitas vezes variáveis ​​de preenchimento para serem alinhadas em 32 bits. Por exemplo, a estrutura

struct BytesAndSuch { 
  char c;
  char C;
  char D;
  short int s;
  int i;
  double d;
};

poderia parecer

+-+-+-+-+--+--+----+--------+
|c|C|D|X|s |XX|  i |   d    |
+-+-+-+-+--+--+----+--------+

na memória, onde X representa bytes preenchidos com base no alinhamento de 4 bytes.

Como as estruturas podem fazer uso de ponteiros e matrizes para declarar e inicializar suas variáveis ​​de membro, o consumo de memória das estruturas não é necessariamente constante . Outro exemplo de tamanho de memória não constante são as estruturas de modelo.

Campos de bits

Os campos de bits são usados ​​para definir os membros da classe que podem ocupar menos armazenamento do que um tipo integral. Este campo é aplicável apenas para tipos integrais (int, char, short, long, etc.) e exclui float ou double.

struct A { 
  unsigned a:2;  // Possible values 0..3, occupies first 2 bits of int
  unsigned b:3;  // Possible values 0..7, occupies next 3 bits of int
  unsigned :0;  // Moves to end of next integral type
  unsigned c:2; 
  unsigned :4;  // Pads 4 bits in between c & d
  unsigned d:1;
  unsigned e:3;
};
  • Estrutura de memória
	 4 byte int  4 byte int
	[1][2][3][4][5][6][7][8]
	[1]                      [2]                      [3]                      [4]
	[a][a][b][b][b][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ]

	[5]                      [6]                      [7]                      [8]
	[c][c][ ][ ][ ][ ][d][e] [e][e][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ]

Os campos de bits não são permitidos em uma união. É aplicável apenas para as classes definidas usando a palavra-chave struct ou class.

Passe por referência

Muitos programadores preferem usar o e comercial (&) para declarar os argumentos de uma função envolvendo estruturas. Isso ocorre porque, ao usar o "e" comercial de desreferenciação, apenas uma palavra (normalmente 4 bytes em uma máquina de 32 bits, 8 bytes em uma máquina de 64 bits) deve ser passada para a função, ou seja, o local da memória para a variável. Caso contrário, se a passagem por valor for usada, o argumento precisará ser copiado toda vez que a função for chamada, o que é caro com estruturas grandes.

Uma vez que a passagem por referência expõe a estrutura original a ser modificada pela função, a constpalavra-chave deve ser usada para garantir que a função não modifique o parâmetro (ver correção const ), quando isso não é pretendido.

A esta palavra-chave

Para facilitar a capacidade das estruturas de referenciar a si mesmas, C ++ implementa a thispalavra - chave para todas as funções de membro. A thispalavra-chave atua como um ponteiro para o objeto atual. Seu tipo é o de um ponteiro para o objeto atual.

A thispalavra-chave é especialmente importante para funções-membro com a própria estrutura como valor de retorno:

Complex& operator+=(const Complex& c) {
  real_part_ += c.real_part_;
  imag_part_ += c.imag_part_;
  return *this;
}

Conforme afirmado acima, thisé um ponteiro, portanto o uso do asterisco (*) é necessário para convertê-lo em uma referência a ser retornada.

Veja também

Referências

Referências gerais: