C++
Classi / Strutture
Ricerca…
Sintassi
- variabile.member_var = costante;
- variable.member_function ();
- variable_pointer-> member_var = constant;
- variable_pointer-> member_function ();
Osservazioni
Si noti che l' unica differenza tra le parole chiave struct
e class
è che, per impostazione predefinita, le variabili membro, le funzioni membro e le classi base di una struct
sono public
, mentre in una class
sono private
. I programmatori C ++ tendono a chiamarlo classe se ha costruttori e distruttori e la capacità di imporre i propri invarianti; o una struttura se è solo una semplice raccolta di valori, ma il linguaggio C ++ non fa alcuna distinzione.
Nozioni di base sulla classe
Una classe è un tipo definito dall'utente. Una classe viene introdotta con la parola chiave class
, struct
o union
. Nell'uso colloquiale, il termine "classe" di solito si riferisce solo a classi non sindacali.
Una classe è una raccolta di membri della classe , che possono essere:
- variabili membro (chiamate anche "campi"),
- funzioni membro (chiamate anche "metodi"),
- tipi di membri o typedef (ad esempio "classi nidificate"),
- modelli di membri (di qualsiasi tipo: variabile, funzione, modello di classe o alias)
Le parole chiave class
e struct
, chiamate chiavi di classe , sono ampiamente intercambiabili, tranne per il fatto che l'identificatore di accesso predefinito per membri e basi è "privato" per una classe dichiarata con la chiave di class
e "pubblica" per una classe dichiarata con la chiave struct
o union
(vedi modificatori di accesso ).
Ad esempio, i seguenti frammenti di codice sono identici:
struct Vector
{
int x;
int y;
int z;
};
// are equivalent to
class Vector
{
public:
int x;
int y;
int z;
};
Dichiarando una classe è stato aggiunto un nuovo tipo al tuo programma ed è possibile creare un'istanza di oggetti di quella classe
Vector my_vector;
Si accede ai membri di una classe usando la sintassi del punto.
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;
Specifier di accesso
Ci sono tre parole chiave che fungono da specificatori di accesso . Questi limitano l'accesso ai membri della classe seguendo l'identificatore, fino a quando un altro specificatore cambia nuovamente il livello di accesso:
Parola chiave | Descrizione |
---|---|
public | Tutti hanno accesso |
protected | Solo la classe stessa, le classi derivate e gli amici hanno accesso |
private | Solo la classe stessa e gli amici hanno accesso |
Quando il tipo viene definito utilizzando la parola chiave class
, l'identificatore di accesso predefinito è private
, ma se il tipo è definito utilizzando la parola chiave struct
, lo specificatore di accesso predefinito è 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
Gli specificatori di accesso sono principalmente utilizzati per limitare l'accesso ai campi e ai metodi interni e impongono al programmatore di utilizzare un'interfaccia specifica, ad esempio per forzare l'uso di getter e setter invece di fare riferimento direttamente a una variabile:
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;
};
L'utilizzo protected
è utile per consentire a determinate funzionalità del tipo di essere accessibili solo alle classi derivate, ad esempio, nel seguente codice, il metodo calculateValue()
è accessibile solo alle classi derivanti dalla classe base Plus2Base
, come ad esempio 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; }
};
Si noti che la parola chiave friend
può essere utilizzata per aggiungere eccezioni di accesso a funzioni o tipi per l'accesso a membri protetti e privati.
Le parole chiave public
, protected
e private
possono anche essere utilizzate per concedere o limitare l'accesso ai sottooggetti di classe base. Vedi l'esempio dell'Eredità .
Eredità
Le classi / le strutture possono avere relazioni di ereditarietà.
Se una classe / struct B
eredita da una classe / struct A
, ciò significa che B
ha come padre A
Diciamo che B
è una classe / struct derivata da A
, e A
è la classe base / struct.
struct A
{
public:
int p1;
protected:
int p2;
private:
int p3;
};
//Make B inherit publicly (default) from A
struct B : A
{
};
Esistono 3 forme di ereditarietà per una classe / struttura:
-
public
-
private
-
protected
Si noti che l'ereditarietà predefinita è la stessa della visibilità predefinita dei membri: public
se si utilizza la parola chiave struct
e private
per la parola chiave class
.
È anche possibile che una class
derivi da una struct
(o viceversa). In questo caso, l'ereditarietà predefinita è controllata dal figlio, quindi una struct
che deriva da una class
verrà predefinita per ereditarietà pubblica e una class
che deriva da una struct
avrà un'ereditarietà privata per impostazione predefinita.
eredità 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
eredità 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
eredità 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
Si noti che, sebbene l'ereditarietà protected
sia consentita, l'uso effettivo di esso è raro. Un'istanza di come l'ereditarietà protected
viene utilizzata nell'applicazione è nella specializzazione parziale della classe base (di solito indicata come "polimorfismo controllato").
Quando OOP era relativamente nuovo, l'eredità (pubblica) veniva spesso detta per modellare una relazione "IS-A". In altre parole, l'ereditarietà pubblica è corretta solo se un'istanza della classe derivata è anche un'istanza della classe base.
Questo è stato successivamente ridefinito nel Principio di sostituzione di Liskov : l'ereditarietà pubblica dovrebbe essere utilizzata solo quando / se un'istanza della classe derivata può essere sostituita per un'istanza della classe base in qualsiasi circostanza possibile (e comunque ha senso).
In genere si dice che l'ereditarietà privata incarna un rapporto completamente diverso: "è implementato in termini di" (a volte chiamato una relazione "HAS-A"). Ad esempio, una classe Stack
può ereditare privatamente da una classe Vector
. L'ereditarietà privata ha una somiglianza molto maggiore con l'aggregazione che con l'eredità pubblica.
L'ereditarietà protetta non viene quasi mai utilizzata e non esiste un accordo generale sul tipo di relazione che incarna.
Eredità virtuale
Quando si utilizza l'ereditarietà, è possibile specificare la parola chiave virtual
:
struct A{};
struct B: public virtual A{};
Quando la classe B
ha una base virtuale A
, significa che A
risiederà nella maggior parte della classe derivata dell'albero di ereditarietà, e quindi che la maggior parte della classe derivata è anche responsabile dell'inizializzazione di quella base virtuale:
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`
}
Se cancelliamo un commento /*A(88)*/
non riceveremo alcun errore dal momento che C
sta inizializzando la sua base virtuale indiretta A
Inoltre, quando creiamo object
variabili, la maggior parte della classe derivata è C
, quindi C
è responsabile della creazione (chiamante costruttore di) A
e quindi il valore di A::member
è 88
, non 5
(come sarebbe se fossimo noi creando oggetto di tipo B
).
È utile quando si risolve il problema dei diamanti .:
A A A
/ \ | |
B C B C
\ / \ /
D D
virtual inheritance normal inheritance
B
e C
ereditano entrambi da A
, e D
eredita da B
e C
, quindi ci sono 2 istanze di A
in D
! Ciò si traduce in ambiguità quando si accede ai membri di A
a D
, in quanto il compilatore non ha modo di sapere da quale classe si desidera accedere a quel membro (quello che B
eredita, o quello che è ereditato da C
?) .
L'ereditarietà virtuale risolve questo problema: poiché la base virtuale risiede solo nella maggior parte degli oggetti derivati, ci sarà solo un'istanza di A
in 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
}
};
La rimozione dei commenti risolve l'ambiguità.
Eredità multipla
Oltre all'eredità singola:
class A {};
class B : public A {};
Puoi anche avere ereditarietà multipla:
class A {};
class B {};
class C : public A, public B {};
C
ora erediterà da A
e B
allo stesso tempo.
Nota: questo può portare ad ambiguità se gli stessi nomi sono usati in più class
ereditate o struct
. Stai attento!
Ambiguità nell'ereditarietà multipla
L'ereditarietà multipla può essere utile in alcuni casi ma, a volte, tipo strano di incontri problematici durante l'utilizzo dell'ereditarietà multipla.
Ad esempio: due classi base hanno funzioni con lo stesso nome che non sono sovrascritte nella classe derivata e se si scrive codice per accedere a quella funzione utilizzando l'oggetto della classe derivata, il compilatore mostra errore perché, non può determinare quale funzione chiamare. Ecco un codice per questo tipo di ambiguità in ereditarietà multipla.
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( )
}
Ma questo problema può essere risolto usando la funzione di risoluzione dello scope per specificare quale funzione classificare sia base1 o base2:
int main()
{
obj.base1::function( ); // Function of class base1 is called.
obj.base2::function( ); // Function of class base2 is called.
}
Accesso ai membri della classe
Per accedere alle variabili membro e alle funzioni membro di un oggetto di una classe, il .
operatore è usato:
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();
Quando si accede ai membri di una classe tramite un puntatore, viene comunemente utilizzato l'operatore ->
. In alternativa, l'istanza può essere dereferenziata e il .
operatore utilizzato, anche se questo è meno comune:
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();
Quando si accede a membri di classi statiche, viene utilizzato l'operatore ::
, ma sul nome della classe anziché un'istanza di esso. In alternativa, è possibile accedere al membro statico da un'istanza o da un puntatore a un'istanza utilizzando .
o ->
operatore, rispettivamente, con la stessa sintassi dell'accesso a membri non statici.
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();
sfondo
L'operatore ->
è necessario perché l'operatore di accesso membro .
ha la precedenza sull'operatore di dereferenziazione *
.
Ci si aspetterebbe che *pa
avrebbe la dereferenziazione p
(risultante in un riferimento all'oggetto p
sta puntando a) e quindi l'accesso al suo membro a
. Ma in effetti, tenta di accedere al membro a
di p
e quindi dereferenziarlo. Ie *pa
è equivalente a *(pa)
. Nell'esempio sopra, ciò comporterebbe un errore del compilatore a causa di due fatti: in primo luogo, p
è un puntatore e non ha un membro a
. In secondo luogo, a
è un numero intero e, quindi, non può essere dereferenziato.
La soluzione non comune a questo problema sarebbe quella di controllare esplicitamente la precedenza: (*p).a
Invece, l'operatore ->
è quasi sempre usato. È una scorciatoia per il primo dereferenziamento del puntatore e quindi per accedervi. Cioè (*p).a
è esattamente uguale a p->a
.
L'operatore ::
è l'operatore dell'ambito, utilizzato nello stesso modo dell'accesso a un membro di uno spazio dei nomi. Questo perché un membro di classe statico è considerato presente nello scope di quella classe, ma non è considerato un membro delle istanze di quella classe. L'uso del normale .
e ->
è consentito anche per i membri statici, nonostante non siano membri di istanze, per ragioni storiche; questo è utile per scrivere codice generico nei template, poiché il chiamante non deve preoccuparsi se una data funzione membro è statica o non statica.
Ereditarietà privata: limitazione dell'interfaccia di base della classe
L'ereditarietà privata è utile quando è necessario limitare l'interfaccia pubblica della classe:
class A {
public:
int move();
int turn();
};
class B : private A {
public:
using A::turn;
};
B b;
b.move(); // compile error
b.turn(); // OK
Questo approccio impedisce in modo efficiente l'accesso ai metodi pubblici A eseguendo il casting sul puntatore A o sul riferimento:
B b;
A& a = static_cast<A&>(b); // compile error
Nel caso dell'ereditarietà pubblica questo tipo di casting fornirà l'accesso a tutti i metodi pubblici A nonostante metodi alternativi per prevenirlo in derivati B, come nascondere:
class B : public A {
private:
int move();
};
o privato utilizzando:
class B : public A {
private:
using A::move;
};
quindi per entrambi i casi è possibile:
B b;
A& a = static_cast<A&>(b); // OK for public inheritance
a.move(); // OK
Classi e strutture finali
Derivare una classe può essere vietato con specificatore final
. Dichiariamo una classe finale:
class A final {
};
Ora qualsiasi tentativo di sottoclasse provocherà un errore di compilazione:
// Compilation error: cannot derive from final class:
class B : public A {
};
La classe finale può apparire ovunque nella gerarchia di classi:
class A {
};
// OK.
class B final : public A {
};
// Compilation error: cannot derive from final class B.
class C : public B {
};
Amicizia
La parola chiave friend
viene utilizzata per fornire ad altre classi e funzioni l'accesso ai membri privati e protetti della classe, anche se sono definiti al di fuori dell'ambito della classe.
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
Classi / strutture annidate
Una class
o una struct
possono anche contenere un'altra definizione di class
/ struct
all'interno di se stessa, che viene chiamata una "classe nidificata"; in questa situazione, la classe contenente viene chiamata "classe di inclusione". La definizione di classe nidificata è considerata un membro della classe di inclusione, ma è altrimenti separata.
struct Outer {
struct Inner { };
};
Dall'esterno della classe di inclusione, è possibile accedere alle classi nidificate utilizzando l'operatore dell'ambito. Dall'interno della classe di inclusione, tuttavia, le classi nidificate possono essere utilizzate senza qualificatori:
struct Outer {
struct Inner { };
Inner in;
};
// ...
Outer o;
Outer::Inner i = o.in;
Come con una class
/ struct
non nidificata, le funzioni membro e le variabili statiche possono essere definite all'interno di una classe nidificata o nello spazio dei nomi che lo racchiude. Tuttavia, non possono essere definiti all'interno della classe che li include, poiché è considerato una classe diversa dalla classe annidata.
// 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() {}
Come con le classi non nidificate, le classi nidificate possono essere inoltrate dichiarate e definite successivamente, purché siano definite prima di essere utilizzate direttamente.
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; }
}
Prima di C ++ 11, le classi nidificate avevano accesso solo ai nomi dei tipi, ai membri static
e agli enumeratori della classe che li includeva; tutti gli altri membri definiti nella classe allegata erano off-limits.
A partire da C ++ 11, le classi nidificate e i loro membri vengono trattati come se fossero friend
della classe che li include e possono accedere a tutti i suoi membri, secondo le consuete regole di accesso; se i membri della classe nidificata richiedono la possibilità di valutare uno o più membri non statici della classe che li include, devono quindi essere passati a un'istanza:
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;
};
Viceversa, la classe che acclude non viene trattata come un amico della classe nidificata, e quindi non può accedere ai suoi membri privati senza il permesso esplicito.
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.
}
};
Gli amici di una classe annidata non sono automaticamente considerati amici della classe che li accoglie; se hanno bisogno di essere amici della classe che li ospita, questo deve essere dichiarato separatamente. Viceversa, poiché la classe che chiude non è automaticamente considerata un amico della classe annidata, nemmeno gli amici della classe che li accoglie saranno considerati amici della classe annidata.
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.
}
Come con tutti gli altri membri della classe, le classi nidificate possono essere nominate solo al di fuori della classe se hanno accesso pubblico. Tuttavia, è consentito accedervi indipendentemente dal modificatore di accesso, a condizione che non li nominiate esplicitamente.
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.
Puoi anche creare un alias di tipo per una classe nidificata. Se un alias di tipo è contenuto nella classe di inclusione, il tipo nidificato e l'alias di tipo possono avere modificatori di accesso diversi. Se l'alias di tipo è esterno alla classe di inclusione, richiede che la classe nidificata o una sua typedef
sia pubblica.
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.
Come con altre classi, le classi nidificate possono derivare o derivare da altre classi.
struct Base {};
struct Outer {
struct Inner : Base {};
};
struct Derived : Outer::Inner {};
Questo può essere utile in situazioni in cui la classe che racchiude è derivata da un'altra classe, consentendo al programmatore di aggiornare la classe nidificata come necessario. Questo può essere combinato con un typedef per fornire un nome coerente per ogni classe nidificata della classe che racchiude:
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();
Nel caso precedente, sia BaseOuter
che DerivedOuter
forniscono rispettivamente il tipo membro Inner
, come BaseInner_
e DerivedInner_
. Ciò consente di derivare i tipi nidificati senza interrompere l'interfaccia della classe che li include e consente di utilizzare il tipo annidato in modo polimorfico.
Tipi di membri e alias
Una class
o una struct
può anche definire alias del tipo di membro, che sono alias di tipo contenuti all'interno e trattati come membri della classe stessa.
struct IHaveATypedef {
typedef int MyTypedef;
};
struct IHaveATemplateTypedef {
template<typename T>
using MyTemplateTypedef = std::vector<T>;
};
Come i membri statici, è possibile accedere a questi typedef utilizzando l'operatore scope, ::
.
IHaveATypedef::MyTypedef i = 5; // i is an int.
IHaveATemplateTypedef::MyTemplateTypedef<int> v; // v is a std::vector<int>.
Come con gli alias di tipo normale, a ogni alias del tipo di membro è consentito fare riferimento a qualsiasi tipo definito o aliasato prima, ma non dopo, la sua definizione. Allo stesso modo, un typedef esterno alla definizione della classe può fare riferimento a qualsiasi typedef accessibile all'interno della definizione della classe, a condizione che venga dopo la definizione della classe.
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.
Gli alias dei membri possono essere dichiarati con qualsiasi livello di accesso e rispetteranno il modificatore di accesso appropriato.
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.
};
Questo può essere usato per fornire un livello di astrazione, permettendo al progettista di una classe di cambiare il suo funzionamento interno senza rompere il codice che si basa su di esso.
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();
In questa situazione, se la classe helper viene modificata da SomeComplexType
in un altro tipo, è necessario modificare solo la typedef
e la dichiarazione friend
; finché la classe helper fornisce la stessa funzionalità, qualsiasi codice che lo utilizza come Something::MyHelper
invece di specificarlo per nome funzionerà in genere senza modifiche. In questo modo, riduciamo al minimo la quantità di codice che deve essere modificata quando l'implementazione sottostante viene modificata, in modo tale che il nome del tipo debba essere modificato solo in una posizione.
Questo può anche essere combinato con decltype
, se lo si desidera.
class SomethingElse {
AnotherComplexType<bool, int, SomeThirdClass> helper;
public:
typedef decltype(helper) MyHelper;
private:
InternalVariable<MyHelper> ivh;
// ...
public:
MyHelper& get_helper() const { return helper; }
// ...
};
In questa situazione, la modifica dell'implementazione di SomethingElse::helper
modificherà automaticamente il typedef per noi, a causa di decltype
. Ciò minimizza il numero di modifiche necessarie quando si desidera cambiare l' helper
, riducendo al minimo il rischio di errore umano.
Come con tutto, tuttavia, questo può essere preso troppo lontano. Se il typename viene usato solo una o due volte internamente e zero volte esternamente, ad esempio, non è necessario fornire un alias per questo. Se viene usato centinaia o migliaia di volte in un progetto, o se ha un nome abbastanza lungo, può essere utile fornirlo come typedef invece di usarlo sempre in termini assoluti. Bisogna bilanciare la compatibilità e la praticità con la quantità di rumore non necessario creato.
Questo può essere usato anche con classi template, per fornire accesso ai parametri del template dall'esterno della classe.
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);
Questo è comunemente usato con i contenitori, che di solito forniscono il loro tipo di elemento, e altri tipi di helper, come alias del tipo di membro. La maggior parte dei contenitori della libreria standard C ++, ad esempio, fornisce i seguenti 12 tipi di helper, insieme a qualsiasi altro tipo speciale di cui potrebbero aver bisogno.
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;
};
Prima di C ++ 11, era anche comunemente usato per fornire un " typedef
template" di sorta, in quanto la funzione non era ancora disponibile; questi sono diventati un po 'meno comuni con l'introduzione di modelli alias, ma sono ancora utili in alcune situazioni (e sono combinati con modelli di alias in altre situazioni, che possono essere molto utili per ottenere singoli componenti di un tipo complesso come un puntatore a funzione ). Solitamente usano il type
nome per il loro alias di tipo.
template<typename T>
struct TemplateTypedef {
typedef T type;
}
TemplateTypedef<int>::type i; // i is an int.
Questo è stato spesso utilizzato con tipi con più parametri del modello, per fornire un alias che definisce uno o più parametri.
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>.
Membri della classe statici
Una classe può anche avere membri static
, che possono essere variabili o funzioni. Questi sono considerati nella portata della classe, ma non sono trattati come membri normali; hanno una durata di archiviazione statica (esistono dall'inizio del programma fino alla fine), non sono legati a una particolare istanza della classe e esiste solo una copia per l'intera classe.
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)
Le variabili membro statiche non sono considerate definite all'interno della classe, solo dichiarate, e quindi hanno la loro definizione al di fuori della definizione della classe; il programmatore è autorizzato, ma non richiesto, a inizializzare le variabili statiche nella loro definizione. Quando si definiscono le variabili membro, la parola chiave static
viene omessa.
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.
A causa di ciò, le variabili statiche possono essere tipi incompleti (a parte il void
), a condizione che vengano successivamente definiti come 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];
Le funzioni membro statiche possono essere definite all'interno o all'esterno della definizione della classe, come con le normali funzioni membro. Come per le variabili membro statiche, la parola chiave static
viene omessa quando si definiscono funzioni membro statiche al di fuori della definizione della classe.
// 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; }
Se una variabile membro statica è dichiarata const
ma non volatile
ed è di tipo integrale o di enumerazione, può essere inizializzata alla dichiarazione, all'interno della definizione della classe.
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 partire da C ++ 11, le variabili membro statiche dei tipi LiteralType
(tipi che possono essere costruiti in fase di compilazione, secondo le regole di constexpr
) possono anche essere dichiarate come constexpr
; in tal caso, devono essere inizializzati all'interno della definizione della classe.
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.
Se una variabile membro const
o constexpr
è odr-used (informalmente, se ha il suo indirizzo preso o è assegnato a un riferimento), allora deve ancora avere una definizione separata, al di fuori della definizione della classe. Questa definizione non può contenere un inizializzatore.
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.
Poiché i membri statici non sono legati a una determinata istanza, è possibile accedervi utilizzando l'operatore scope, ::
.
std::string str = Example::static_str;
Possono anche essere accessibili come se fossero membri normali e non statici. Ciò è di importanza storica, ma viene utilizzato meno comunemente dell'operatore di ambito per evitare confusione sul fatto che un membro sia statico o non statico.
Example ex;
std::string rts = ex.static_str;
I membri della classe possono accedere ai membri statici senza qualificare il loro ambito, come con i membri della classe non statici.
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;
Non possono essere mutable
, né dovrebbero essere; poiché non sono legati a nessuna istanza data, se un'istanza è o non è const non influisce sui membri statici.
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.
I membri statici rispettano i modificatori di accesso, proprio come i membri non statici.
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.
Poiché non sono legati a una determinata istanza, le funzioni membro statiche non hanno this
puntatore; a causa di ciò, non possono accedere a variabili membro non statiche a meno che non abbiano passato un'istanza.
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.
};
Non avendo this
puntatore, i loro indirizzi non possono essere archiviati nelle funzioni di puntatori a membro e vengono invece memorizzati in normali puntatori a funzioni.
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.
Non avendo this
puntatore, non possono essere const
o volatile
, né possono avere qualifiche di ref. Inoltre, non possono essere virtuali.
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.
};
Poiché non sono legati a una determinata istanza, le variabili membro statiche vengono effettivamente trattate come variabili globali speciali; vengono creati all'avvio del programma e distrutti quando escono, indipendentemente dal fatto che esistano effettivamente delle istanze della classe. Esiste solo una singola copia di ogni variabile membro statica (a meno che la variabile non sia dichiarata thread_local
(C ++ 11 o successivo), nel qual caso c'è una copia per thread).
Le variabili membro statiche hanno lo stesso collegamento della classe, indipendentemente dal fatto che la classe abbia un collegamento interno o esterno. Le classi locali e le classi senza nome non possono avere membri statici.
Funzioni membro non statiche
Una classe può avere funzioni membro non statiche , che operano su singole istanze della classe.
class CL {
public:
void member_function() {}
};
Queste funzioni sono chiamate su un'istanza della classe, in questo modo:
CL instance;
instance.member_function();
Possono essere definiti all'interno o all'esterno della definizione della classe; se definiti all'esterno, vengono specificati come appartenenti all'ambito della classe.
struct ST {
void defined_inside() {}
void defined_outside();
};
void ST::defined_outside() {}
Possono essere qualificati CV e / o ref-qualificati , influenzando il modo in cui vedono l'istanza su cui sono chiamati; la funzione vedrà l'istanza come avente i qualificatori di cv specificati, se ce ne sono. La versione che viene chiamata sarà basata sui qualificatori di cv dell'istanza. Se non esiste una versione con gli stessi qualificatori di cv dell'istanza, verrà chiamata una versione più cv se disponibile.
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.
I qualificatori di ref della funzione membro indicano se la funzione è destinata a essere chiamata su istanze rvalue e utilizzare la stessa sintassi della funzione 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.
I qualificazioni di CV e i qualificazioni di qualificazione possono anche essere combinati, se necessario.
struct BothCVAndRef {
void func() const& {} // Called on normal instances. Sees instance as const.
void func() && {} // Called on temporary instances.
};
Possono anche essere virtuali ; questo è fondamentale per il polimorfismo e consente a una o più classi figlio di fornire la stessa interfaccia della classe genitore, pur fornendo le proprie funzionalità.
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().
Per ulteriori informazioni, vedere qui .
Struttura / classe senza nome
La struct
senza nome è consentita (il tipo non ha nome)
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;
};
e più tardi
Circle circle;
circle.center.x = 42.f;
ma NON struct
anonima (tipo senza nome e oggetto senza nome)
struct InvalidCircle
{
struct /* No name */ {
float centerX;
float centerY;
}; // No member either.
float radius;
};
Nota: alcuni compilatori consentono la struct
anonima come estensione .
lamdba può essere visto come una speciale
struct
senza nome .decltype
consente di recuperare il tipo distruct
senza nome :decltype(circle.point) otherPoint;
l' istanza
struct
nonstruct
può essere parametro del metodo template: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"; }