C++
Clases / Estructuras
Buscar..
Sintaxis
- variable.member_var = constante;
- variable.member_function ();
- variable_pointer-> member_var = constante;
- variable_pointer-> miembro_función ();
Observaciones
Tenga en cuenta que la única diferencia entre las palabras clave struct
y class
es que, de forma predeterminada, las variables miembro, las funciones miembro y las clases base de una struct
son public
, mientras que en una class
son private
. Los programadores de C ++ tienden a llamarlo una clase si tienen constructores y destructores, y la capacidad de imponer sus propios invariantes; o una estructura si es solo una simple colección de valores, pero el lenguaje C ++ en sí mismo no hace distinción.
Conceptos básicos de clase
Una clase es un tipo definido por el usuario. Se introduce una clase con la palabra clave class
, struct
o union
. En el uso coloquial, el término "clase" generalmente se refiere solo a las clases no sindicalizadas.
Una clase es una colección de miembros de la clase , que puede ser:
- variables miembro (también llamadas "campos"),
- funciones miembro (también llamadas "métodos"),
- tipos de miembros o typedefs (por ejemplo, "clases anidadas"),
- Plantillas de miembros (de cualquier tipo: plantilla de variable, función, clase o alias)
Las palabras clave class
y struct
, llamadas claves de clase , son en gran medida intercambiables, excepto que el especificador de acceso predeterminado para miembros y bases es "privado" para una clase declarada con la clave de class
y "público" para una clase declarada con la clave struct
o union
(cf. modificadores de acceso ).
Por ejemplo, los siguientes fragmentos de código son idénticos:
struct Vector
{
int x;
int y;
int z;
};
// are equivalent to
class Vector
{
public:
int x;
int y;
int z;
};
Al declarar una clase` se agrega un nuevo tipo a su programa, y es posible crear una instancia de los objetos de esa clase mediante
Vector my_vector;
Se accede a los miembros de una clase usando la sintaxis de puntos.
my_vector.x = 10;
my_vector.y = my_vector.x + 1; // my_vector.y = 11;
my_vector.z = my_vector.y - 4; // my:vector.z = 7;
Especificadores de acceso
Hay tres palabras clave que actúan como especificadores de acceso . Estos limitan el acceso a los miembros de la clase siguiendo el especificador, hasta que otro especificador cambie de nuevo el nivel de acceso:
Palabra clave | Descripción |
---|---|
public | Todos tienen acceso |
protected | Sólo la clase en sí, las clases derivadas y los amigos tienen acceso. |
private | Sólo la clase en sí y los amigos tienen acceso. |
Cuando el tipo se define con la palabra clave de class
, el especificador de acceso predeterminado es private
, pero si el tipo se define con la palabra clave struct
, el especificador de acceso predeterminado es public
:
struct MyStruct { int x; };
class MyClass { int x; };
MyStruct s;
s.x = 9; // well formed, because x is public
MyClass c;
c.x = 9; // ill-formed, because x is private
Los especificadores de acceso se usan principalmente para limitar el acceso a los campos y métodos internos, y obligan al programador a usar una interfaz específica, por ejemplo, para forzar el uso de captadores y definidores en lugar de hacer referencia a una variable directamente:
class MyClass {
public: /* Methods: */
int x() const noexcept { return m_x; }
void setX(int const x) noexcept { m_x = x; }
private: /* Fields: */
int m_x;
};
Usar protected
es útil para permitir que cierta funcionalidad del tipo solo sea accesible para las clases derivadas, por ejemplo, en el siguiente código, el método calculateValue()
solo es accesible para las clases derivadas de la clase base Plus2Base
, como FortyTwo
:
struct Plus2Base {
int value() noexcept { return calculateValue() + 2; }
protected: /* Methods: */
virtual int calculateValue() noexcept = 0;
};
struct FortyTwo: Plus2Base {
protected: /* Methods: */
int calculateValue() noexcept final override { return 40; }
};
Tenga en cuenta que la palabra clave friend
se puede usar para agregar excepciones de acceso a funciones o tipos para acceder a miembros protegidos y privados.
Las palabras clave public
, protected
y private
también se pueden usar para otorgar o limitar el acceso a subobjetos de clase base. Ver el ejemplo de herencia .
Herencia
Las clases / estructuras pueden tener relaciones de herencia.
Si una clase / estructura B
hereda de una clase / estructura A
, esto significa que B
tiene como padre A
Decimos que B
es una clase / estructura derivada de A
, y A
es la clase / estructura base.
struct A
{
public:
int p1;
protected:
int p2;
private:
int p3;
};
//Make B inherit publicly (default) from A
struct B : A
{
};
Hay 3 formas de herencia para una clase / estructura:
-
public
-
private
-
protected
Tenga en cuenta que la herencia predeterminada es la misma que la visibilidad predeterminada de los miembros: public
si usa la palabra clave struct
, y private
para la palabra clave class
.
Incluso es posible tener una class
derivada de una struct
(o viceversa). En este caso, la herencia predeterminada está controlada por el hijo, por lo que una struct
que se deriva de una class
se establecerá de forma predeterminada como herencia pública, y una class
que se deriva de una struct
tendrá herencia privada de forma predeterminada.
herencia public
:
struct B : public A // or just `struct B : A`
{
void foo()
{
p1 = 0; //well formed, p1 is public in B
p2 = 0; //well formed, p2 is protected in B
p3 = 0; //ill formed, p3 is private in A
}
};
B b;
b.p1 = 1; //well formed, p1 is public
b.p2 = 1; //ill formed, p2 is protected
b.p3 = 1; //ill formed, p3 is inaccessible
herencia private
struct B : private A
{
void foo()
{
p1 = 0; //well formed, p1 is private in B
p2 = 0; //well formed, p2 is private in B
p3 = 0; //ill formed, p3 is private in A
}
};
B b;
b.p1 = 1; //ill formed, p1 is private
b.p2 = 1; //ill formed, p2 is private
b.p3 = 1; //ill formed, p3 is inaccessible
herencia protected
:
struct B : protected A
{
void foo()
{
p1 = 0; //well formed, p1 is protected in B
p2 = 0; //well formed, p2 is protected in B
p3 = 0; //ill formed, p3 is private in A
}
};
B b;
b.p1 = 1; //ill formed, p1 is protected
b.p2 = 1; //ill formed, p2 is protected
b.p3 = 1; //ill formed, p3 is inaccessible
Tenga en cuenta que aunque se permite la herencia protected
, el uso real de la misma es raro. Una instancia de cómo se usa la herencia protected
en la aplicación es en la especialización de clase base parcial (generalmente denominada "polimorfismo controlado").
Cuando la POO era relativamente nueva, se decía con frecuencia que la herencia (pública) modelaba una relación "IS-A". Es decir, la herencia pública es correcta solo si una instancia de la clase derivada es también una instancia de la clase base.
Más tarde, esto se refinó en el Principio de Sustitución de Liskov : la herencia pública solo debe usarse cuando / si una instancia de la clase derivada puede sustituirse por una instancia de la clase base bajo cualquier circunstancia posible (y aún tiene sentido).
En general, se dice que la herencia privada incorpora una relación completamente diferente: "se implementa en términos de" (a veces se denomina relación "HAS-A"). Por ejemplo, una clase de Stack
podría heredar de forma privada de una clase Vector
. La herencia privada tiene una similitud mucho mayor con la agregación que con la herencia pública.
La herencia protegida casi nunca se usa, y no hay un acuerdo general sobre qué tipo de relación abarca.
Herencia virtual
Al usar la herencia, puede especificar la palabra clave virtual
:
struct A{};
struct B: public virtual A{};
Cuando la clase B
tiene una base virtual A
, significa que A
residirá en la mayoría de la clase derivada del árbol de herencia y, por lo tanto, la mayoría de la clase derivada también es responsable de inicializar esa base virtual:
struct A
{
int member;
A(int param)
{
member = param;
}
};
struct B: virtual A
{
B(): A(5){}
};
struct C: B
{
C(): /*A(88)*/ {}
};
void f()
{
C object; //error since C is not initializing it's indirect virtual base `A`
}
Si anulamos el comentario /*A(88)*/
no obtendremos ningún error ya que C
ahora está inicializando su base virtual indirecta A
También tenga en cuenta que cuando estamos creando un object
variable, la mayoría de la clase derivada es C
, por lo que C
es responsable de crear (llamar al constructor de) A
y, por lo tanto, el valor de A::member
es 88
, no 5
(como lo sería si fuésemos creando objeto de tipo B
).
Es útil para resolver el problema del diamante .
A A A
/ \ | |
B C B C
\ / \ /
D D
virtual inheritance normal inheritance
B
y C
heredan de A
, y D
hereda de B
y C
, por lo que hay 2 instancias de A
en D
! Esto se traduce en ambigüedad cuando se accede al miembro de A
a D
, ya que el compilador no tiene forma de saber de qué clase desea acceder a ese miembro (¿el que B
hereda o el que hereda C
?) .
La herencia virtual resuelve este problema: como la base virtual reside solo en la mayoría de los objetos derivados, solo habrá una instancia de A
en D
struct A
{
void foo() {}
};
struct B : public /*virtual*/ A {};
struct C : public /*virtual*/ A {};
struct D : public B, public C
{
void bar()
{
foo(); //Error, which foo? B::foo() or C::foo()? - Ambiguous
}
};
Eliminar los comentarios resuelve la ambigüedad.
Herencia múltiple
Aparte de la herencia única:
class A {};
class B : public A {};
También puede tener herencia múltiple:
class A {};
class B {};
class C : public A, public B {};
C
ahora tendrá herencia de A
y B
al mismo tiempo.
Nota: esto puede generar ambigüedad si se usan los mismos nombres en varias class
o struct
heredadas. ¡Ten cuidado!
La ambigüedad en la herencia múltiple
La herencia múltiple puede ser útil en ciertos casos pero, a veces, algún tipo de problema se encuentra mientras se usa la herencia múltiple.
Por ejemplo: dos clases base tienen funciones con el mismo nombre que no se anulan en la clase derivada y si escribe código para acceder a esa función utilizando el objeto de la clase derivada, el compilador muestra error porque no puede determinar a qué función llamar. Aquí hay un código para este tipo de ambigüedad en herencia múltiple.
class base1
{
public:
void funtion( )
{ //code for base1 function }
};
class base2
{
void function( )
{ // code for base2 function }
};
class derived : public base1, public base2
{
};
int main()
{
derived obj;
// Error because compiler can't figure out which function to call
//either function( ) of base1 or base2 .
obj.function( )
}
Pero, este problema se puede resolver utilizando la función de resolución de alcance para especificar qué función se debe clasificar como base1 o base2:
int main()
{
obj.base1::function( ); // Function of class base1 is called.
obj.base2::function( ); // Function of class base2 is called.
}
Acceso a los miembros de la clase
Para acceder a las variables y funciones miembro de un objeto de una clase, el .
el operador es usado:
struct SomeStruct {
int a;
int b;
void foo() {}
};
SomeStruct var;
// Accessing member variable a in var.
std::cout << var.a << std::endl;
// Assigning member variable b in var.
var.b = 1;
// Calling a member function.
var.foo();
Cuando se accede a los miembros de una clase a través de un puntero, el operador ->
se usa comúnmente. Alternativamente, la instancia puede ser anulada y la .
Operador utilizado, aunque esto es menos común:
struct SomeStruct {
int a;
int b;
void foo() {}
};
SomeStruct var;
SomeStruct *p = &var;
// Accessing member variable a in var via pointer.
std::cout << p->a << std::endl;
std::cout << (*p).a << std::endl;
// Assigning member variable b in var via pointer.
p->b = 1;
(*p).b = 1;
// Calling a member function via a pointer.
p->foo();
(*p).foo();
Cuando se accede a miembros de la clase estática, se utiliza el operador ::
, pero en el nombre de la clase en lugar de una instancia de la misma. Alternativamente, se puede acceder al miembro estático desde una instancia o un puntero a una instancia usando el .
o ->
operador, respectivamente, con la misma sintaxis que para acceder a miembros no estáticos.
struct SomeStruct {
int a;
int b;
void foo() {}
static int c;
static void bar() {}
};
int SomeStruct::c;
SomeStruct var;
SomeStruct* p = &var;
// Assigning static member variable c in struct SomeStruct.
SomeStruct::c = 5;
// Accessing static member variable c in struct SomeStruct, through var and p.
var.a = var.c;
var.b = p->c;
// Calling a static member function.
SomeStruct::bar();
var.bar();
p->bar();
Fondo
El operador ->
es necesario porque el operador de acceso miembro .
Tiene prioridad sobre el operador de desreferenciación *
.
Uno esperaría que *pa
desreferenciara p
(resultando en una referencia al objeto p
que apunta) y luego accediendo a su miembro a
. Pero, de hecho, intenta acceder al miembro a
de p
y luego desreferenciarlo. Ie *pa
es equivalente a *(pa)
. En el ejemplo anterior, esto daría como resultado un error del compilador debido a dos hechos: Primero, p
es un puntero y no tiene un miembro a
. En segundo lugar, a
es un número entero y, por lo tanto, no se puede eliminar la referencia.
La solución poco común a este problema sería controlar explícitamente la prioridad: (*p).a
En cambio, el operador ->
casi siempre se usa. Es una mano corta para desreferenciar primero el puntero y luego acceder a él. Ie (*p).a
es exactamente lo mismo que p->a
.
El operador ::
es el operador de alcance, que se utiliza de la misma manera que para acceder a un miembro de un espacio de nombres. Esto se debe a que se considera que un miembro de la clase estática está dentro del alcance de esa clase, pero no se considera un miembro de las instancias de esa clase. El uso de lo normal .
y ->
también está permitido para miembros estáticos, a pesar de que no sean miembros de instancia, por razones históricas; esto es útil para escribir código genérico en plantillas, ya que la persona que llama no necesita preocuparse de si una función miembro dada es estática o no estática.
Herencia privada: restringiendo la interfaz de clase base
La herencia privada es útil cuando se requiere restringir la interfaz pública de la clase:
class A {
public:
int move();
int turn();
};
class B : private A {
public:
using A::turn;
};
B b;
b.move(); // compile error
b.turn(); // OK
Este enfoque evita de manera eficiente el acceso a los métodos públicos A mediante el envío al puntero o referencia A:
B b;
A& a = static_cast<A&>(b); // compile error
En el caso de la herencia pública, dicha conversión proporcionará acceso a todos los métodos públicos A, a pesar de que existen formas alternativas de evitar esto en B derivada, como la ocultación:
class B : public A {
private:
int move();
};
o privado utilizando:
class B : public A {
private:
using A::move;
};
Entonces para ambos casos es posible:
B b;
A& a = static_cast<A&>(b); // OK for public inheritance
a.move(); // OK
Clases finales y estructuras.
Derivar una clase puede estar prohibido con especificador final
. Vamos a declarar una clase final:
class A final {
};
Ahora cualquier intento de subclase causará un error de compilación:
// Compilation error: cannot derive from final class:
class B : public A {
};
La clase final puede aparecer en cualquier lugar de la jerarquía de clases:
class A {
};
// OK.
class B final : public A {
};
// Compilation error: cannot derive from final class B.
class C : public B {
};
Amistad
La palabra clave friend
se utiliza para otorgar acceso a otras clases y funciones a miembros privados y protegidos de la clase, incluso a través de su definición fuera del alcance de la clase.
class Animal{
private:
double weight;
double height;
public:
friend void printWeight(Animal animal);
friend class AnimalPrinter;
// A common use for a friend function is to overload the operator<< for streaming.
friend std::ostream& operator<<(std::ostream& os, Animal animal);
};
void printWeight(Animal animal)
{
std::cout << animal.weight << "\n";
}
class AnimalPrinter
{
public:
void print(const Animal& animal)
{
// Because of the `friend class AnimalPrinter;" declaration, we are
// allowed to access private members here.
std::cout << animal.weight << ", " << animal.height << std::endl;
}
}
std::ostream& operator<<(std::ostream& os, Animal animal)
{
os << "Animal height: " << animal.height << "\n";
return os;
}
int main() {
Animal animal = {10, 5};
printWeight(animal);
AnimalPrinter aPrinter;
aPrinter.print(animal);
std::cout << animal;
}
10
10, 5
Animal height: 5
Clases / Estructuras Anidadas
Una class
o struct
también puede contener otra definición de class
/ struct
dentro de sí misma, que se denomina "clase anidada"; en esta situación, la clase contenedora se conoce como la "clase adjunta". La definición de clase anidada se considera un miembro de la clase adjunta, pero por lo demás es separada.
struct Outer {
struct Inner { };
};
Desde fuera de la clase adjunta, se accede a las clases anidadas mediante el operador de alcance. Sin embargo, desde el interior de la clase adjunta, las clases anidadas se pueden usar sin calificadores:
struct Outer {
struct Inner { };
Inner in;
};
// ...
Outer o;
Outer::Inner i = o.in;
Al igual que con una class
/ struct
no anidada, las funciones miembro y las variables estáticas se pueden definir ya sea dentro de una clase anidada o en el espacio de nombres adjunto. Sin embargo, no se pueden definir dentro de la clase adjunta, debido a que se considera que es una clase diferente a la clase anidada.
// Bad.
struct Outer {
struct Inner {
void do_something();
};
void Inner::do_something() {}
};
// Good.
struct Outer {
struct Inner {
void do_something();
};
};
void Outer::Inner::do_something() {}
Al igual que con las clases no anidadas, las clases anidadas se pueden declarar y definir posteriormente, siempre que se definan antes de usarlas directamente.
class Outer {
class Inner1;
class Inner2;
class Inner1 {};
Inner1 in1;
Inner2* in2p;
public:
Outer();
~Outer();
};
class Outer::Inner2 {};
Outer::Outer() : in1(Inner1()), in2p(new Inner2) {}
Outer::~Outer() {
if (in2p) { delete in2p; }
}
Antes de C ++ 11, las clases anidadas solo tenían acceso a nombres de tipo, miembros static
y enumeradores de la clase adjunta; todos los demás miembros definidos en la clase adjunta estaban fuera de los límites.
A partir de C ++ 11, las clases anidadas y sus miembros se tratan como si fueran friend
de la clase adjunta y pueden acceder a todos sus miembros, de acuerdo con las reglas de acceso habituales; si los miembros de la clase anidada requieren la capacidad de evaluar uno o más miembros no estáticos de la clase adjunta, por lo tanto, se les debe pasar una instancia:
class Outer {
struct Inner {
int get_sizeof_x() {
return sizeof(x); // Legal (C++11): x is unevaluated, so no instance is required.
}
int get_x() {
return x; // Illegal: Can't access non-static member without an instance.
}
int get_x(Outer& o) {
return o.x; // Legal (C++11): As a member of Outer, Inner can access private members.
}
};
int x;
};
A la inversa, la clase adjunta no se trata como un amigo de la clase anidada y, por lo tanto, no puede acceder a sus miembros privados sin un permiso explícito.
class Outer {
class Inner {
// friend class Outer;
int x;
};
Inner in;
public:
int get_x() {
return in.x; // Error: int Outer::Inner::x is private.
// Uncomment "friend" line above to fix.
}
};
Los amigos de una clase anidada no se consideran automáticamente amigos de la clase adjunta; Si también necesitan ser amigos de la clase adjunta, esto debe declararse por separado. A la inversa, como la clase adjunta no se considera automáticamente un amigo de la clase anidada, tampoco se considerarán amigos de la clase anexa amigos de la clase anidada.
class Outer {
friend void barge_out(Outer& out, Inner& in);
class Inner {
friend void barge_in(Outer& out, Inner& in);
int i;
};
int o;
};
void barge_in(Outer& out, Outer::Inner& in) {
int i = in.i; // Good.
int o = out.o; // Error: int Outer::o is private.
}
void barge_out(Outer& out, Outer::Inner& in) {
int i = in.i; // Error: int Outer::Inner::i is private.
int o = out.o; // Good.
}
Al igual que con todos los demás miembros de la clase, las clases anidadas solo pueden nombrarse desde fuera de la clase si tienen acceso público. Sin embargo, puede acceder a ellos sin importar el modificador de acceso, siempre y cuando no los nombre explícitamente.
class Outer {
struct Inner {
void func() { std::cout << "I have no private taboo.\n"; }
};
public:
static Inner make_Inner() { return Inner(); }
};
// ...
Outer::Inner oi; // Error: Outer::Inner is private.
auto oi = Outer::make_Inner(); // Good.
oi.func(); // Good.
Outer::make_Inner().func(); // Good.
También puede crear un alias de tipo para una clase anidada. Si un alias de tipo está contenido en la clase adjunta, el tipo anidado y el alias de tipo pueden tener diferentes modificadores de acceso. Si el alias de tipo está fuera de la clase adjunta, requiere que la clase anidada, o un typedef
misma, sea pública.
class Outer {
class Inner_ {};
public:
typedef Inner_ Inner;
};
typedef Outer::Inner ImOut; // Good.
typedef Outer::Inner_ ImBad; // Error.
// ...
Outer::Inner oi; // Good.
Outer::Inner_ oi; // Error.
ImOut oi; // Good.
Al igual que con otras clases, las clases anidadas pueden derivar o derivarse de otras clases.
struct Base {};
struct Outer {
struct Inner : Base {};
};
struct Derived : Outer::Inner {};
Esto puede ser útil en situaciones en las que la clase adjunta se deriva de otra clase, al permitir que el programador actualice la clase anidada según sea necesario. Esto se puede combinar con un typedef para proporcionar un nombre coherente para cada clase anidada de la clase envolvente:
class BaseOuter {
struct BaseInner_ {
virtual void do_something() {}
virtual void do_something_else();
} b_in;
public:
typedef BaseInner_ Inner;
virtual ~BaseOuter() = default;
virtual Inner& getInner() { return b_in; }
};
void BaseOuter::BaseInner_::do_something_else() {}
// ---
class DerivedOuter : public BaseOuter {
// Note the use of the qualified typedef; BaseOuter::BaseInner_ is private.
struct DerivedInner_ : BaseOuter::Inner {
void do_something() override {}
void do_something_else() override;
} d_in;
public:
typedef DerivedInner_ Inner;
BaseOuter::Inner& getInner() override { return d_in; }
};
void DerivedOuter::DerivedInner_::do_something_else() {}
// ...
// Calls BaseOuter::BaseInner_::do_something();
BaseOuter* b = new BaseOuter;
BaseOuter::Inner& bin = b->getInner();
bin.do_something();
b->getInner().do_something();
// Calls DerivedOuter::DerivedInner_::do_something();
BaseOuter* d = new DerivedOuter;
BaseOuter::Inner& din = d->getInner();
din.do_something();
d->getInner().do_something();
En el caso anterior, tanto BaseOuter
como DerivedOuter
suministran el tipo de miembro Inner
, como BaseInner_
y DerivedInner_
, respectivamente. Esto permite que los tipos anidados se deriven sin romper la interfaz de la clase envolvente, y permite que el tipo anidado se utilice de forma polimórfica.
Tipos de miembros y alias
Una class
o struct
también puede definir alias de tipo de miembro, que son alias de tipo contenidos dentro de la clase y tratados como miembros de la misma clase.
struct IHaveATypedef {
typedef int MyTypedef;
};
struct IHaveATemplateTypedef {
template<typename T>
using MyTemplateTypedef = std::vector<T>;
};
Al igual que los miembros estáticos, se accede a estos typedefs usando el operador de alcance, ::
.
IHaveATypedef::MyTypedef i = 5; // i is an int.
IHaveATemplateTypedef::MyTemplateTypedef<int> v; // v is a std::vector<int>.
Al igual que con los alias de tipo normal, cada alias de tipo miembro puede referirse a cualquier tipo definido o con alias antes, pero no después, de su definición. Del mismo modo, un typedef fuera de la definición de clase puede referirse a cualquier typedef accesible dentro de la definición de clase, siempre que venga después de la definición de clase.
template<typename T>
struct Helper {
T get() const { return static_cast<T>(42); }
};
struct IHaveTypedefs {
// typedef MyTypedef NonLinearTypedef; // Error if uncommented.
typedef int MyTypedef;
typedef Helper<MyTypedef> MyTypedefHelper;
};
IHaveTypedefs::MyTypedef i; // x_i is an int.
IHaveTypedefs::MyTypedefHelper hi; // x_hi is a Helper<int>.
typedef IHaveTypedefs::MyTypedef TypedefBeFree;
TypedefBeFree ii; // ii is an int.
Los alias de tipo de miembro se pueden declarar con cualquier nivel de acceso y respetarán el modificador de acceso apropiado.
class TypedefAccessLevels {
typedef int PrvInt;
protected:
typedef int ProInt;
public:
typedef int PubInt;
};
TypedefAccessLevels::PrvInt prv_i; // Error: TypedefAccessLevels::PrvInt is private.
TypedefAccessLevels::ProInt pro_i; // Error: TypedefAccessLevels::ProInt is protected.
TypedefAccessLevels::PubInt pub_i; // Good.
class Derived : public TypedefAccessLevels {
PrvInt prv_i; // Error: TypedefAccessLevels::PrvInt is private.
ProInt pro_i; // Good.
PubInt pub_i; // Good.
};
Esto se puede usar para proporcionar un nivel de abstracción, permitiendo que el diseñador de una clase cambie su funcionamiento interno sin romper el código que se basa en él.
class Something {
friend class SomeComplexType;
short s;
// ...
public:
typedef SomeComplexType MyHelper;
MyHelper get_helper() const { return MyHelper(8, s, 19.5, "shoe", false); }
// ...
};
// ...
Something s;
Something::MyHelper hlp = s.get_helper();
En esta situación, si la clase auxiliar se cambia de SomeComplexType
a algún otro tipo, solo será necesario modificar el typedef
y la declaración de friend
; Siempre que la clase auxiliar proporcione la misma funcionalidad, cualquier código que lo use como Something::MyHelper
lugar de especificarlo por su nombre, por lo general funcionará sin ninguna modificación. De esta manera, minimizamos la cantidad de código que debe modificarse cuando se cambia la implementación subyacente, de modo que el nombre de tipo solo necesita cambiarse en una ubicación.
Esto también se puede combinar con decltype
, si uno lo desea.
class SomethingElse {
AnotherComplexType<bool, int, SomeThirdClass> helper;
public:
typedef decltype(helper) MyHelper;
private:
InternalVariable<MyHelper> ivh;
// ...
public:
MyHelper& get_helper() const { return helper; }
// ...
};
En esta situación, cambiar la implementación de SomethingElse::helper
cambiará automáticamente el typedef para nosotros, debido a decltype
. Esto minimiza el número de modificaciones necesarias cuando queremos cambiar de helper
, lo que minimiza el riesgo de error humano.
Como con todo, sin embargo, esto puede ser llevado demasiado lejos. Si el nombre de tipo solo se usa una o dos veces internamente y cero veces externamente, por ejemplo, no es necesario proporcionar un alias para él. Si se usa cientos o miles de veces a lo largo de un proyecto, o si tiene un nombre lo suficientemente largo, entonces puede ser útil proporcionarlo como un typedef en lugar de usarlo siempre en términos absolutos. Hay que equilibrar la compatibilidad y la conveniencia con la cantidad de ruido innecesario creado.
Esto también se puede utilizar con clases de plantilla, para proporcionar acceso a los parámetros de la plantilla desde fuera de la clase.
template<typename T>
class SomeClass {
// ...
public:
typedef T MyParam;
MyParam getParam() { return static_cast<T>(42); }
};
template<typename T>
typename T::MyParam some_func(T& t) {
return t.getParam();
}
SomeClass<int> si;
int i = some_func(si);
Esto se usa comúnmente con los contenedores, que normalmente proporcionarán su tipo de elemento, y otros tipos de ayuda, como alias de tipo de miembro. La mayoría de los contenedores en la biblioteca estándar de C ++, por ejemplo, proporcionan los siguientes 12 tipos de ayuda, junto con cualquier otro tipo especial que puedan necesitar.
template<typename T>
class SomeContainer {
// ...
public:
// Let's provide the same helper types as most standard containers.
typedef T value_type;
typedef std::allocator<value_type> allocator_type;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef MyIterator<value_type> iterator;
typedef MyConstIterator<value_type> const_iterator;
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
};
Antes de C ++ 11, también se usaba comúnmente para proporcionar un tipo de "plantilla typedef
", ya que la característica aún no estaba disponible; se han vuelto un poco menos comunes con la introducción de plantillas de alias, pero siguen siendo útiles en algunas situaciones (y se combinan con plantillas de alias en otras situaciones, lo que puede ser muy útil para obtener componentes individuales de un tipo complejo, como un puntero de función). ). Comúnmente usan el type
nombre para su alias de tipo.
template<typename T>
struct TemplateTypedef {
typedef T type;
}
TemplateTypedef<int>::type i; // i is an int.
Esto se usaba a menudo con tipos con múltiples parámetros de plantilla, para proporcionar un alias que define uno o más de los parámetros.
template<typename T, size_t SZ, size_t D>
class Array { /* ... */ };
template<typename T, size_t SZ>
struct OneDArray {
typedef Array<T, SZ, 1> type;
};
template<typename T, size_t SZ>
struct TwoDArray {
typedef Array<T, SZ, 2> type;
};
template<typename T>
struct MonoDisplayLine {
typedef Array<T, 80, 1> type;
};
OneDArray<int, 3>::type arr1i; // arr1i is an Array<int, 3, 1>.
TwoDArray<short, 5>::type arr2s; // arr2s is an Array<short, 5, 2>.
MonoDisplayLine<char>::type arr3c; // arr3c is an Array<char, 80, 1>.
Miembros de la clase estatica
Una clase también puede tener miembros static
, que pueden ser variables o funciones. Se considera que estos están dentro del alcance de la clase, pero no se tratan como miembros normales; tienen una duración de almacenamiento estático (existen desde el inicio del programa hasta el final), no están vinculados a una instancia particular de la clase y solo existe una copia para toda la clase.
class Example {
static int num_instances; // Static data member (static member variable).
int i; // Non-static member variable.
public:
static std::string static_str; // Static data member (static member variable).
static int static_func(); // Static member function.
// Non-static member functions can modify static member variables.
Example() { ++num_instances; }
void set_str(const std::string& str);
};
int Example::num_instances;
std::string Example::static_str = "Hello.";
// ...
Example one, two, three;
// Each Example has its own "i", such that:
// (&one.i != &two.i)
// (&one.i != &three.i)
// (&two.i != &three.i).
// All three Examples share "num_instances", such that:
// (&one.num_instances == &two.num_instances)
// (&one.num_instances == &three.num_instances)
// (&two.num_instances == &three.num_instances)
Las variables miembro estáticas no se consideran definidas dentro de la clase, solo se declaran, y por lo tanto tienen su definición fuera de la definición de la clase; el programador puede, pero no es obligatorio, inicializar variables estáticas en su definición. Al definir las variables miembro, se omite la palabra clave static
.
class Example {
static int num_instances; // Declaration.
public:
static std::string static_str; // Declaration.
// ...
};
int Example::num_instances; // Definition. Zero-initialised.
std::string Example::static_str = "Hello."; // Definition.
Debido a esto, las variables estáticas pueden ser tipos incompletos (aparte del void
), siempre que se definan más adelante como un tipo completo.
struct ForwardDeclared;
class ExIncomplete {
static ForwardDeclared fd;
static ExIncomplete i_contain_myself;
static int an_array[];
};
struct ForwardDeclared {};
ForwardDeclared ExIncomplete::fd;
ExIncomplete ExIncomplete::i_contain_myself;
int ExIncomplete::an_array[5];
Las funciones miembro estáticas se pueden definir dentro o fuera de la definición de clase, como ocurre con las funciones miembro normales. Al igual que con las variables miembro estáticas, la palabra clave static
se omite al definir funciones miembro estáticas fuera de la definición de clase.
// For Example above, either...
class Example {
// ...
public:
static int static_func() { return num_instances; }
// ...
void set_str(const std::string& str) { static_str = str; }
};
// Or...
class Example { /* ... */ };
int Example::static_func() { return num_instances; }
void Example::set_str(const std::string& str) { static_str = str; }
Si una variable miembro estática se declara const
pero no es volatile
, y es de tipo integral o de enumeración, se puede inicializar en declaración, dentro de la definición de clase.
enum E { VAL = 5 };
struct ExConst {
const static int ci = 5; // Good.
static const E ce = VAL; // Good.
const static double cd = 5; // Error.
static const volatile int cvi = 5; // Error.
const static double good_cd;
static const volatile int good_cvi;
};
const double ExConst::good_cd = 5; // Good.
const volatile int ExConst::good_cvi = 5; // Good.
A partir de C ++ 11, las variables miembro estáticas de tipos LiteralType
(tipos que pueden construirse en tiempo de compilación, de acuerdo con constexpr
reglas constexpr
) también pueden declararse como constexpr
; si es así, deben inicializarse dentro de la definición de clase.
struct ExConstexpr {
constexpr static int ci = 5; // Good.
static constexpr double cd = 5; // Good.
constexpr static int carr[] = { 1, 1, 2 }; // Good.
static constexpr ConstexprConstructibleClass c{}; // Good.
constexpr static int bad_ci; // Error.
};
constexpr int ExConstexpr::bad_ci = 5; // Still an error.
Si una variable de miembro estática const
o constexpr
se usa de forma estándar (de manera informal, si tiene su dirección tomada o está asignada a una referencia), todavía debe tener una definición separada, fuera de la definición de clase. Esta definición no puede contener un inicializador.
struct ExODR {
static const int odr_used = 5;
};
// const int ExODR::odr_used;
const int* odr_user = & ExODR::odr_used; // Error; uncomment above line to resolve.
Como los miembros estáticos no están vinculados a una instancia determinada, se puede acceder a ellos utilizando el operador de alcance, ::
.
std::string str = Example::static_str;
También se puede acceder a ellos como si fueran miembros normales, no estáticos. Esto es de importancia histórica, pero se usa con menos frecuencia que el operador de alcance para evitar confusiones sobre si un miembro es estático o no estático.
Example ex;
std::string rts = ex.static_str;
Los miembros de la clase pueden acceder a miembros estáticos sin calificar su alcance, al igual que con los miembros de clase no estáticos.
class ExTwo {
static int num_instances;
int my_num;
public:
ExTwo() : my_num(num_instances++) {}
static int get_total_instances() { return num_instances; }
int get_instance_number() const { return my_num; }
};
int ExTwo::num_instances;
No pueden ser mutable
, ni deberían serlo; como no están vinculados a ninguna instancia dada, si una instancia es o no const no afecta a los miembros estáticos.
struct ExDontNeedMutable {
int immuta;
mutable int muta;
static int i;
ExDontNeedMutable() : immuta(-5), muta(-5) {}
};
int ExDontNeedMutable::i;
// ...
const ExDontNeedMutable dnm;
dnm.immuta = 5; // Error: Can't modify read-only object.
dnm.muta = 5; // Good. Mutable fields of const objects can be written.
dnm.i = 5; // Good. Static members can be written regardless of an instance's const-ness.
Los miembros estáticos respetan los modificadores de acceso, al igual que los miembros no estáticos.
class ExAccess {
static int prv_int;
protected:
static int pro_int;
public:
static int pub_int;
};
int ExAccess::prv_int;
int ExAccess::pro_int;
int ExAccess::pub_int;
// ...
int x1 = ExAccess::prv_int; // Error: int ExAccess::prv_int is private.
int x2 = ExAccess::pro_int; // Error: int ExAccess::pro_int is protected.
int x3 = ExAccess::pub_int; // Good.
Como no están vinculados a una instancia determinada, las funciones miembro estáticas no tienen this
puntero; Debido a esto, no pueden acceder a las variables miembro no estáticas a menos que se pase una instancia.
class ExInstanceRequired {
int i;
public:
ExInstanceRequired() : i(0) {}
static void bad_mutate() { ++i *= 5; } // Error.
static void good_mutate(ExInstanceRequired& e) { ++e.i *= 5; } // Good.
};
Debido a que no tienen un puntero de this
, sus direcciones no se pueden almacenar en funciones de punteros a miembros y, en cambio, se almacenan en punteros a funciones normales.
struct ExPointer {
void nsfunc() {}
static void sfunc() {}
};
typedef void (ExPointer::* mem_f_ptr)();
typedef void (*f_ptr)();
mem_f_ptr p_sf = &ExPointer::sfunc; // Error.
f_ptr p_sf = &ExPointer::sfunc; // Good.
Debido a no tener un this
puntero, sino que también no pueden ser const
o volatile
, ni pueden tener Ref-calificadores. Tampoco pueden ser virtuales.
struct ExCVQualifiersAndVirtual {
static void func() {} // Good.
static void cfunc() const {} // Error.
static void vfunc() volatile {} // Error.
static void cvfunc() const volatile {} // Error.
static void rfunc() & {} // Error.
static void rvfunc() && {} // Error.
virtual static void vsfunc() {} // Error.
static virtual void svfunc() {} // Error.
};
Como no están vinculados a una instancia determinada, las variables miembro estáticas se tratan efectivamente como variables globales especiales; se crean cuando se inicia el programa y se destruyen cuando se sale, independientemente de si existe alguna instancia de la clase. Solo existe una copia de cada variable miembro estática (a menos que la variable se declare thread_local
(C ++ 11 o posterior), en cuyo caso hay una copia por subproceso).
Las variables miembro estáticas tienen el mismo enlace que la clase, ya sea que la clase tenga enlace externo o interno. Las clases locales y las clases sin nombre no tienen permitido tener miembros estáticos.
Funciones miembro no estáticas
Una clase puede tener funciones miembro no estáticas , que operan en instancias individuales de la clase.
class CL {
public:
void member_function() {}
};
Estas funciones se llaman en una instancia de la clase, como así:
CL instance;
instance.member_function();
Se pueden definir dentro o fuera de la definición de clase; si se definen fuera, se especifican como dentro del alcance de la clase.
struct ST {
void defined_inside() {}
void defined_outside();
};
void ST::defined_outside() {}
Pueden ser calificados para CV y / o ref , lo que afecta la forma en que ven la instancia a la que se les solicita; la función considerará que la instancia tiene el calificador (es) cv especificado, si corresponde. La versión que se llame se basará en los calificadores cv de la instancia. Si no hay una versión con los mismos calificadores de CV que la instancia, se llamará una versión más calificada de CV, si está disponible.
struct CVQualifiers {
void func() {} // 1: Instance is non-cv-qualified.
void func() const {} // 2: Instance is const.
void cv_only() const volatile {}
};
CVQualifiers non_cv_instance;
const CVQualifiers c_instance;
non_cv_instance.func(); // Calls #1.
c_instance.func(); // Calls #2.
non_cv_instance.cv_only(); // Calls const volatile version.
c_instance.cv_only(); // Calls const volatile version.
Los ref-qualifiers de la función miembro indican si la función se debe llamar o no en instancias de rvalue, y usan la misma sintaxis que la función cv-qualifiers.
struct RefQualifiers {
void func() & {} // 1: Called on normal instances.
void func() && {} // 2: Called on rvalue (temporary) instances.
};
RefQualifiers rf;
rf.func(); // Calls #1.
RefQualifiers{}.func(); // Calls #2.
CV-calificadores y ref-calificadores también se pueden combinar si es necesario.
struct BothCVAndRef {
void func() const& {} // Called on normal instances. Sees instance as const.
void func() && {} // Called on temporary instances.
};
También pueden ser virtuales ; esto es fundamental para el polimorfismo, y permite que una (s) clase (s) infantil (es) proporcione la misma interfaz que la clase primaria, al tiempo que proporciona su propia funcionalidad.
struct Base {
virtual void func() {}
};
struct Derived {
virtual void func() {}
};
Base* bp = new Base;
Base* dp = new Derived;
bp.func(); // Calls Base::func().
dp.func(); // Calls Derived::func().
Para más información, ver aquí .
Estructura / clase sin nombre
Se permite la struct
sin nombre (el tipo no tiene nombre)
void foo()
{
struct /* No name */ {
float x;
float y;
} point;
point.x = 42;
}
o
struct Circle
{
struct /* No name */ {
float x;
float y;
} center; // but a member name
float radius;
};
y después
Circle circle;
circle.center.x = 42.f;
pero NO struct
anónima (tipo sin nombre y objeto sin nombre)
struct InvalidCircle
{
struct /* No name */ {
float centerX;
float centerY;
}; // No member either.
float radius;
};
Nota: algunos compiladores permiten struct
anónimas como extensión .
lamdba puede ser visto como una
struct
especial sin nombre .decltype
permite recuperar el tipo destruct
sin nombre :decltype(circle.point) otherPoint;
La instancia de
struct
sin nombre puede ser un parámetro del método de plantilla:void print_square_coordinates() { const struct {float x; float y;} points[] = { {-1, -1}, {-1, 1}, {1, -1}, {1, 1} }; // for range relies on `template <class T, std::size_t N> std::begin(T (&)[N])` for (const auto& point : points) { std::cout << "{" << point.x << ", " << point.y << "}\n"; } decltype(points[0]) topRightCorner{1, 1}; auto it = std::find(points, points + 4, topRightCorner); std::cout << "top right corner is the " << 1 + std::distance(points, it) << "th\n"; }