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 class
palavra-chave possui membros privados e classes básicas por padrão. Uma estrutura é uma classe definida com a struct
palavra - 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".
- 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 Person
como tendo dois membros de dados públicos, name
e 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), Person
pode ser usado da seguinte maneira para criar variáveis recém-definidas do Person
tipo 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 this
pode 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 Person
tipo 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 Print
funçã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 Print
sã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 a
e b
acima 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 P
com um P* p
apontando para ele pode ter a seguinte aparência na memória:
+----+ |P::x| +----+ ↑ p
Uma instância de C
com um P* p
apontando para ele pode ter a seguinte aparência:
+----+----+ |P::x|C::y| +----+----+ ↑ p
Portanto, qualquer código que manipule os campos de um P
objeto pode manipular os P
campos dentro do C
objeto sem ter que considerar nada sobre a definição dos C
campos 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 D
herda P
e 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 D
tipo para P
ou 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 Integer
em que a variável realmente armazena um inteiro, mas chamando Integer * Integer
a 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 struct
declaraçã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);
}
i
acima representa a própria variável de membro do remetente, enquanto k.i
representa a variável de membro da variável de argumento k
.
A const
palavra-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_type
com o tipo de dados do valor de retorno ( int
, bool
, estruturas etc.)
O int
parâ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 argument
parte.
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 new
palavra - chave e a delete
palavra - 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 Person
variá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 Person
construtor 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 TwoNums
estrutura 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 const
palavra-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 this
palavra - chave para todas as funções de membro. A this
palavra-chave atua como um ponteiro para o objeto atual. Seu tipo é o de um ponteiro para o objeto atual.
A this
palavra-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
- Modificadores de acesso
- Herança virtual
- Aula (programação de computador)
- Programação baseada em aulas
- Composição de objetos
- Conversão de tipo
Referências
Referências gerais:
- Cplusplus.com tutorial lição 5.2 , acessado em janeiro de 2006
- Cplusplus.com tutorial lição 2.5 , acessado em fevereiro de 2006