Szukaj…


Składnia

  • zmienna.member_var = stała;
  • zmienna.member_function ();
  • zmienna_pointer-> member_var = stała;
  • zmienna_pointer-> member_function ();

Uwagi

Zauważ, że jedyną różnicą między słowami kluczowymi struct i class jest to, że domyślnie zmienne składowe, funkcje struct i class podstawowe structpublic , podczas gdy w classprivate . Programiści C ++ nazywają ją klasą, jeśli ma konstruktory i destruktory oraz zdolność do egzekwowania własnych niezmienników; lub struct, jeśli jest to zwykła kolekcja wartości, ale sam język C ++ nie czyni żadnego rozróżnienia.

Podstawy zajęć

Klasa jest typem zdefiniowanym przez użytkownika. Klasa jest wprowadzana za pomocą słowa kluczowego class , struct lub union . W potocznym użyciu termin „klasa” zwykle odnosi się tylko do klas nieunijnych.

Klasa to zbiór członków klasy , którym mogą być:

  • zmienne składowe (zwane również „polami”),
  • funkcje składowe (zwane również „metodami”),
  • typy członków lub typedefs (np. „klasy zagnieżdżone”),
  • szablony elementów (dowolnego rodzaju: szablon zmiennej, funkcji, klasy lub aliasu)

Słowa kluczowe class i struct , zwane kluczami klas , są w dużej mierze wymienne, z tym wyjątkiem, że domyślny specyfikator dostępu dla członków i baz jest „prywatny” dla klasy zadeklarowanej za pomocą klucza class i „publiczny” dla klasy zadeklarowanej za pomocą klucza struct lub union (por. modyfikatory dostępu ).

Na przykład następujące fragmenty kodu są identyczne:

struct Vector
{
    int x;
    int y;
    int z;
};
// are equivalent to
class Vector
{
public:
    int x;
    int y;
    int z;
};

Deklarując klasę, do programu dodawany jest nowy typ i możliwe jest tworzenie instancji obiektów tej klasy przez

Vector my_vector;

Dostęp do członków klasy można uzyskać za pomocą składni kropkowej.

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;

Dostęp do specyfikatorów

Istnieją trzy słowa kluczowe, które działają jak specyfikatory dostępu . Ograniczają one dostęp do członków klasy podążających za specyfikatorem, dopóki inny specyfikator nie zmieni poziomu dostępu ponownie:

Słowo kluczowe Opis
public Każdy ma dostęp
protected Dostęp ma tylko sama klasa, pochodne klasy i przyjaciele
private Dostęp mają tylko sama klasa i przyjaciele

Gdy typ jest zdefiniowany za pomocą słowa kluczowego class , domyślny specyfikator dostępu jest private , ale jeśli typ jest zdefiniowany za pomocą słowa kluczowego struct , domyślny specyfikator dostępu jest 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

Specyfikatory dostępu są najczęściej używane do ograniczania dostępu do wewnętrznych pól i metod oraz do zmuszania programisty do korzystania z określonego interfejsu, na przykład do wymuszenia użycia metod pobierających i ustawiających zamiast bezpośredniego odwoływania się do zmiennej:

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;

};

Korzystanie z funkcji protected jest przydatne, ponieważ pozwala niektórym funkcjom typu być dostępna tylko dla klas pochodnych, na przykład w poniższym kodzie metoda Plus2Base calculateValue() jest dostępna tylko dla klas wywodzących się z klasy podstawowej Plus2Base , takich jak 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; }
};

Pamiętaj, że słowa kluczowego friend można używać do dodawania wyjątków dostępu do funkcji lub typów dostępu do członków chronionych i prywatnych.

public , protected i private słów kluczowych można również użyć do przyznania lub ograniczenia dostępu do podobiektów klasy podstawowej. Zobacz przykład dziedziczenia .

Dziedzictwo

Klasy / struktury mogą mieć relacje dziedziczenia.

Jeśli klasa / struktura B dziedziczy po klasie / strukturze A , oznacza to, że B ma jako rodzic A Mówimy, że B jest klasą pochodną / strukturą od A , a A jest klasą bazową / strukturą.

struct A
{
public:
    int p1;
protected:
    int p2;
private:
    int p3;
};

//Make B inherit publicly (default) from A
struct B : A
{
};

Istnieją 3 formy dziedziczenia dla klasy / struktury:

  • public
  • private
  • protected

Zauważ, że domyślne dziedzictwo jest takie samo jak domyślna widoczność członków: public jeśli używasz słowa kluczowego struct , i private dla słowa kluczowego class .

Możliwe jest nawet, aby class wywodziła się ze struct (lub odwrotnie). W takim przypadku dziedziczenie domyślne jest kontrolowane przez dziecko, więc struct wywodząca się z class będzie domyślnie dziedziczona publicznie, a class wywodząca się ze struct będzie domyślnie miała dziedziczenie prywatne.

dziedzictwo 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

spadek 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

protected dziedzictwo:

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

Należy pamiętać, że chociaż protected dziedzictwo jest dozwolone, faktyczne korzystanie z niego jest rzadkie. Jednym z przykładów zastosowania protected dziedziczenia w aplikacji jest częściowa specjalizacja klasy podstawowej (zwykle określana jako „kontrolowany polimorfizm”).

Kiedy OOP było stosunkowo nowe, często mówiono, że (publiczne) dziedzictwo modeluje relację „IS-A”. Oznacza to, że dziedziczenie publiczne jest poprawne tylko wtedy, gdy wystąpienie klasy pochodnej jest również wystąpieniem klasy podstawowej.

Zostało to później dopracowane do zasady substytucji Liskowa : dziedziczenia publicznego należy używać tylko wtedy, gdy / jeśli wystąpienie klasy pochodnej można zastąpić wystąpieniem klasy podstawowej w dowolnych okolicznościach (i nadal ma to sens).

Mówi się, że dziedziczenie prywatne zawiera całkowicie inną relację: „jest realizowane w kategoriach” (czasami nazywane relacją „HAS-A”). Na przykład klasa Stack może dziedziczyć prywatnie po klasie Vector . Dziedziczenie prywatne wykazuje znacznie większe podobieństwo do agregacji niż do dziedziczenia publicznego.

Dziedziczenie chronione prawie nigdy nie jest używane i nie ma ogólnej zgody na to, jaki rodzaj relacji zawiera.

Dziedziczenie wirtualne

Korzystając z dziedziczenia, możesz określić virtual słowo kluczowe:

struct A{};
struct B: public virtual A{};

Gdy klasa B ma wirtualną bazę A , oznacza to, że A będzie znajdować się w większości pochodnych klas drzewa dziedziczenia, a zatem większość pochodnych klas jest również odpowiedzialna za inicjowanie tej wirtualnej bazy:

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`
}

Jeśli cofniemy komentarz /*A(88)*/ , nie otrzymamy żadnego błędu, ponieważ C inicjuje teraz swoją pośrednią wirtualną bazę A

Zauważ też, że kiedy tworzymy object zmienny, najbardziej pochodną klasą jest C , więc C jest odpowiedzialny za tworzenie (wywoływanie konstruktora) A a zatem wartość A::member wynosi 88 , a nie 5 (tak jakbyśmy to zrobili tworzenie obiektu typu B ).

Przydaje się przy rozwiązywaniu problemu z diamentem :

  A                                        A   A
 / \                                       |   |
B   C                                      B   C
 \ /                                        \ /
  D                                          D
virtual inheritance                   normal inheritance

B i C dziedziczą po A , a D dziedziczy po B i C , więc istnieją 2 przypadki A w D ! Powoduje to dwuznaczność, gdy uzyskujesz dostęp do członka od A do D , ponieważ kompilator nie ma możliwości dowiedzenia się, z której klasy chcesz uzyskać dostęp do tego członka (tego, który dziedziczy B , lub dziedziczonego przez C ?) .

Wirtualne dziedziczenie rozwiązuje ten problem: ponieważ wirtualna baza znajduje się tylko w większości pochodnych obiektów, będzie tylko jedna instancja A w 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
    }
};

Usunięcie komentarzy rozwiązuje niejednoznaczność.

Wielokrotne dziedziczenie

Oprócz pojedynczego dziedziczenia:

class A {};
class B : public A {};

Możesz także mieć wiele elementów dziedziczenia:

class A {};
class B {};
class C : public A, public B {};

C będzie teraz dziedziczyć po A i B w tym samym czasie.

Uwaga: może to prowadzić do dwuznaczności, jeśli te same nazwy są używane w wielu odziedziczonych class lub struct . Bądź ostrożny!

Niejednoznaczność w wielokrotnym dziedziczeniu

Wielokrotne dziedziczenie może być pomocne w niektórych przypadkach, ale czasem dziwne problemy napotykają podczas korzystania z wielokrotnego dziedziczenia.

Na przykład: Dwie klasy podstawowe mają funkcje o tej samej nazwie, które nie są zastępowane w klasie pochodnej, a jeśli napiszesz kod, aby uzyskać dostęp do tej funkcji za pomocą obiektu klasy pochodnej, kompilator wyświetli błąd, ponieważ nie może określić, którą funkcję wywołać. Oto kod tego rodzaju niejednoznaczności w wielokrotnym dziedziczeniu.

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( )  
}

Problem ten można jednak rozwiązać za pomocą funkcji rozdzielczości zakresu, aby określić, która funkcja ma być klasą base1 lub base2:

int main()
{
    obj.base1::function( );  // Function of class base1 is called. 
    obj.base2::function( );  // Function of class base2 is called.
}

Dostęp do członków klasy

Aby uzyskać dostęp do zmiennych składowych i funkcji składowych obiektu klasy, należy . operator jest używany:

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();

Podczas uzyskiwania dostępu do członków klasy za pomocą wskaźnika powszechnie używany jest operator -> . Alternatywnie instancję można wyrejestrować, a właściwość . użyty operator, chociaż jest to mniej powszechne:

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();

Podczas uzyskiwania dostępu do statycznych członków klasy używany jest operator :: , ale w nazwie klasy zamiast w jej instancji. Alternatywnie do elementu statycznego można uzyskać dostęp z instancji lub wskaźnika do instancji za pomocą . lub -> odpowiednio, z taką samą składnią jak dostęp do elementów niestatycznych.

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();

tło

Operator -> jest potrzebny, ponieważ operator dostępu do członka . ma pierwszeństwo przed operatorem dereferencyjnym * .

Można się spodziewać, że *pa odrzuci p (skutkując odwołaniem do obiektu, na który wskazuje p ), a następnie uzyska dostęp do swojego elementu a . Ale w rzeczywistości, to próbuje uzyskać dostęp do użytkownika a o p a następnie dereference go. Tj. *pa jest równoważne *(pa) . W powyższym przykładzie spowodowałoby to błąd kompilatora z powodu dwóch faktów: Po pierwsze, p jest wskaźnikiem i nie ma elementu a . Po drugie, a jest liczbą całkowitą i dlatego nie można się od niej oderwać.

Niezbyt często stosowanym rozwiązaniem tego problemu byłoby jawne kontrolowanie pierwszeństwa: (*p).a

Zamiast tego prawie zawsze używany jest operator -> . Jest to skrót do najpierw dereferencji wskaźnika, a następnie uzyskania do niego dostępu. Tj. (*p).a jest dokładnie takie samo jak p->a .

Operator :: jest operatorem zakresu, używanym w taki sam sposób, jak dostęp do elementu przestrzeni nazw. Wynika to z tego, że statyczny element członkowski klasy należy do zakresu tej klasy, ale nie jest uważany za element członkowski instancji tej klasy. Zastosowanie normalnego . oraz -> jest również dozwolone dla członków statycznych, mimo że nie są członkami instancji, z przyczyn historycznych; jest to przydatne do pisania ogólnego kodu w szablonach, ponieważ wywołujący nie musi się martwić, czy dana funkcja składowa jest statyczna czy niestatyczna.

Dziedziczenie prywatne: ograniczenie interfejsu klasy podstawowej

Dziedziczenie prywatne jest przydatne, gdy jest wymagane ograniczenie publicznego interfejsu klasy:

class A {
public:
    int move();
    int turn();
};

class B : private A {
public:
    using A::turn; 
};

B b;
b.move();  // compile error
b.turn();  // OK

Takie podejście skutecznie uniemożliwia dostęp do metod publicznych A poprzez rzutowanie na wskaźnik A lub odwołanie:

B b; 
A& a = static_cast<A&>(b); // compile error

W przypadku dziedziczenia publicznego takie przesyłanie zapewni dostęp do wszystkich metod publicznych A, pomimo alternatywnych sposobów zapobiegania temu w pochodnej B, takich jak ukrywanie:

class B : public A {
private:
    int move();  
};

lub prywatny przy użyciu:

class B : public A {
private:
    using A::move;  
};

w obu przypadkach możliwe jest:

B b;
A& a = static_cast<A&>(b); // OK for public inheritance
a.move(); // OK

Końcowe zajęcia i struktury

C ++ 11

Wyprowadzenie klasy może być zabronione z final specyfikatorem. Zadeklarujmy klasę końcową:

class A final {
};

Teraz każda próba jego podklasy spowoduje błąd kompilacji:

// Compilation error: cannot derive from final class:
class B : public A {
};

Klasa końcowa może pojawić się w dowolnym miejscu w hierarchii klas:

class A {
};

// OK.
class B final : public A {
};

// Compilation error: cannot derive from final class B.
class C : public B {
};

Przyjaźń

friend kluczowe friend służy do zapewnienia innym klasom i funkcjom dostępu do prywatnych i chronionych członków klasy, nawet jeśli są zdefiniowane poza zakresem klasy.

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

Zagnieżdżone klasy / struktury

class lub struct może również zawierać w sobie inną definicję class / struct , która nazywa się „klasą zagnieżdżoną”; w tej sytuacji klasa zawierająca jest określana jako „klasa obejmująca”. Definicja klasy zagnieżdżonej jest uważana za członka klasy obejmującej, ale poza tym jest osobna.

struct Outer {
    struct Inner { };
};

Dostęp do klas zagnieżdżonych spoza klasy otaczającej można uzyskać za pomocą operatora zasięgu. Jednak wewnątrz klasy otaczającej można używać klas zagnieżdżonych bez kwalifikatorów:

struct Outer {
    struct Inner { };

    Inner in;
};

// ...

Outer o;
Outer::Inner i = o.in;

Podobnie jak w przypadku nie zagnieżdżonej class / struct , funkcje składowe i zmienne statyczne można zdefiniować albo w zagnieżdżonej klasie, albo w otaczającej przestrzeni nazw. Nie można ich jednak zdefiniować w klasie zamykającej, ponieważ uważa się ją za klasę inną niż klasa zagnieżdżona.

// 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() {}

Podobnie jak w przypadku klas nie zagnieżdżonych, klasy zagnieżdżone mogą być zadeklarowane w przód i zdefiniowane później, pod warunkiem że zostaną zdefiniowane przed bezpośrednim użyciem.

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; }
}

C ++ 11

Przed wersją C ++ 11 klasy zagnieżdżone miały dostęp tylko do nazw typów, elementów static i modułów wyliczających z klasy zamykającej; wszyscy inni członkowie zdefiniowani w klasie zamykającej byli poza limitem.

C ++ 11

Począwszy od C ++ 11, klasy zagnieżdżone i ich członkowie są traktowani tak, jakby byli friend klasy zamykającej i mogą uzyskiwać dostęp do wszystkich swoich członków, zgodnie ze zwykłymi zasadami dostępu; jeśli członkowie klasy zagnieżdżonej wymagają zdolności do oceny jednego lub więcej elementów niestatycznych klasy zamykającej, należy im przekazać instancję:

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;
};

I odwrotnie, klasa obejmująca nie jest traktowana jako przyjaciel klasy zagnieżdżonej, a zatem nie może uzyskać dostępu do swoich prywatnych członków bez wyraźnego zezwolenia.

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.
    }
};

Przyjaciele klasy zagnieżdżonej nie są automatycznie uważani za przyjaciół klasy zamykającej; jeśli muszą być również przyjaciółmi klasy zamykającej, należy to zadeklarować osobno. I odwrotnie, ponieważ klasa zamykająca nie jest automatycznie uważana za przyjaciela klasy zagnieżdżonej, przyjaciele klasy zamykającej nie będą też uważani za przyjaciół klasy zagnieżdżonej.

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.
}

Podobnie jak w przypadku wszystkich innych członków klasy, zagnieżdżone klasy można nazwać spoza klasy tylko wtedy, gdy mają dostęp publiczny. Możesz jednak uzyskać do nich dostęp bez względu na modyfikator dostępu, o ile nie podasz ich wprost.

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.

Możesz także utworzyć alias typu dla zagnieżdżonej klasy. Jeśli alias typu jest zawarty w klasie zamykającej, typ zagnieżdżony i alias typu mogą mieć różne modyfikatory dostępu. Jeśli alias typu znajduje się poza klasą otaczającą, wymaga, aby albo klasa zagnieżdżona, albo jej typedef , była publiczna.

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.

Podobnie jak w przypadku innych klas, zagnieżdżone klasy mogą zarówno pochodzić, jak i pochodzić z innych klas.

struct Base {};

struct Outer {
    struct Inner : Base {};
};

struct Derived : Outer::Inner {};

Może to być przydatne w sytuacjach, w których obejmująca klasa pochodzi od innej klasy, pozwalając programiście na aktualizację zagnieżdżonej klasy w razie potrzeby. Można to połączyć z typedef, aby zapewnić spójną nazwę dla każdej zagnieżdżonej klasy:

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();

W powyższym przypadku zarówno BaseOuter , jak i DerivedOuter dostarczają typ elementu Inner , odpowiednio BaseInner_ i DerivedInner_ . Umożliwia to wyprowadzanie typów zagnieżdżonych bez przerywania interfejsu klasy zamykającej i pozwala na stosowanie typu zagnieżdżonego polimorficznie.

Typy członków i aliasy

class lub struct może również definiować aliasy typu członka, które są aliasami typu zawartymi w klasie i traktowanymi jako elementy samej klasy.

struct IHaveATypedef {
    typedef int MyTypedef;
};

struct IHaveATemplateTypedef {
    template<typename T>
    using MyTemplateTypedef = std::vector<T>;
};

Podobnie jak elementy statyczne, do tych typów typów można uzyskać dostęp za pomocą operatora zakresu, :: .

IHaveATypedef::MyTypedef i = 5; // i is an int.

IHaveATemplateTypedef::MyTemplateTypedef<int> v; // v is a std::vector<int>.

Podobnie jak w przypadku zwykłych aliasów, każdy alias typu członka może odnosić się do dowolnego typu zdefiniowanego lub aliowanego przed, ale nie po jego definicji. Podobnie typedef poza definicją klasy może odnosić się do każdego dostępnego typedef w definicji klasy, pod warunkiem, że występuje po definicji klasy.

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.

Aliasy typu członka mogą być deklarowane na dowolnym poziomie dostępu i będą respektować odpowiedni modyfikator dostępu.

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.
};

Można to wykorzystać do zapewnienia poziomu abstrakcji, umożliwiając projektantowi klasy zmianę jego wewnętrznego działania bez rozbijania kodu, który jest na nim oparty.

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();

W tej sytuacji, jeśli klasa pomocnika zostanie zmieniona z SomeComplexType na inny typ, konieczna będzie modyfikacja tylko deklaracji typedef i friend ; tak długo, jak klasa pomocnicza zapewnia tę samą funkcjonalność, każdy kod, który używa go jako Something::MyHelper zamiast określać go po nazwie, zwykle będzie nadal działał bez żadnych modyfikacji. W ten sposób minimalizujemy ilość kodu, który należy zmodyfikować po zmianie podstawowej implementacji, tak że nazwa typu musi zostać zmieniona tylko w jednym miejscu.

Można to również połączyć z decltype , jeśli ktoś sobie tego życzy.

class SomethingElse {
    AnotherComplexType<bool, int, SomeThirdClass> helper;

  public:
    typedef decltype(helper) MyHelper;

  private:
    InternalVariable<MyHelper> ivh;

    // ...

  public:
    MyHelper& get_helper() const { return helper; }

    // ...
};

W tej sytuacji zmiana implementacji SomethingElse::helper automatycznie zmieni dla nas typedef, z powodu decltype . Minimalizuje to liczbę koniecznych modyfikacji, gdy chcemy zmienić helper , co minimalizuje ryzyko błędu ludzkiego.

Tak jak w przypadku wszystkiego, można jednak posunąć się za daleko. Jeśli na przykład nazwa typu jest używana tylko raz lub dwa razy wewnętrznie, a zero razy na zewnątrz, nie ma potrzeby podawania aliasu. Jeśli jest używany setki lub tysiące razy w całym projekcie lub ma wystarczająco długą nazwę, przydatne może być podanie go jako typedef zamiast używania go bezwzględnie. Należy zrównoważyć kompatybilność i wygodę z ilością wytwarzanego niepotrzebnego hałasu.


Można tego również użyć z klasami szablonów, aby zapewnić dostęp do parametrów szablonu spoza klasy.

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);

Jest to powszechnie stosowane w przypadku kontenerów, które zwykle dostarczają typ elementu i inne typy pomocników, jako aliasy typu członka. Na przykład większość kontenerów w standardowej bibliotece C ++ zawiera następujące 12 typów pomocników oraz wszelkie inne specjalne typy, których mogą potrzebować.

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;
};

W wersjach wcześniejszych niż C ++ 11 był także powszechnie używany do zapewnienia „szablonów typedef ”, ponieważ funkcja nie była jeszcze dostępna; stały się one nieco mniej powszechne po wprowadzeniu szablonów aliasów, ale nadal są przydatne w niektórych sytuacjach (i są łączone z szablonami aliasów w innych sytuacjach, które mogą być bardzo przydatne do uzyskiwania poszczególnych składników typu złożonego, takich jak wskaźnik funkcji ). Często używają type nazwy dla swojego aliasu typu.

template<typename T>
struct TemplateTypedef {
    typedef T type;
}

TemplateTypedef<int>::type i; // i is an int.

Często używano tego z typami z wieloma parametrami szablonu, aby zapewnić alias, który definiuje jeden lub więcej parametrów.

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>.

Członkowie klasy statycznej

Klasa może również mieć elementy static , które mogą być zmiennymi lub funkcjami. Są one uważane za objęte klasą, ale nie są traktowane jak zwykłe elementy; mają statyczny czas przechowywania (istnieją od początku programu do końca), nie są powiązane z konkretną instancją klasy i istnieje tylko jedna kopia dla całej klasy.

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)

Statyczne zmienne składowe nie są uważane za zdefiniowane wewnątrz klasy, tylko deklarowane, a zatem mają swoją definicję poza definicją klasy; programista może inicjować zmienne statyczne w swojej definicji, ale nie jest to wymagane. Podczas definiowania zmiennych static słowo kluczowe static jest pomijane.

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.

Z tego powodu zmienne statyczne mogą być niekompletnymi typami (oprócz void ), o ile zostaną później zdefiniowane jako kompletny typ.

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];

Statyczne funkcje składowe mogą być zdefiniowane wewnątrz lub poza definicją klasy, tak jak w przypadku zwykłych funkcji składowych. Podobnie jak w przypadku statycznych zmiennych składowych, słowo kluczowe static jest pomijane podczas definiowania statycznych funkcji składowych poza definicją klasy.

// 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; }

Jeśli statyczna zmienna const jest zadeklarowana jako const ale nie jest volatile i jest typu całkowego lub wyliczeniowego, można ją zainicjować w deklaracji w definicji klasy.

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.
C ++ 11

Począwszy od C ++ 11, statyczne zmienne LiteralType typów LiteralType (typy, które można konstruować w czasie kompilacji, zgodnie z regułami constexpr ) mogą być również deklarowane jako constexpr ; jeśli tak, należy je zainicjować w ramach definicji klasy.

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.

Jeśli constexpr jest zmienna statyczna const lub constexpr odr (nieoficjalnie, jeśli jej adres jest pobrany lub przypisany do odwołania), to musi ona nadal mieć osobną definicję poza definicją klasy. Ta definicja nie może zawierać inicjatora.

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.

Ponieważ elementy statyczne nie są powiązane z daną instancją, można uzyskać do nich dostęp za pomocą operatora zasięgu, :: .

std::string str = Example::static_str;

Można również uzyskać do nich dostęp, jakby byli normalnymi, niestatycznymi członkami. Ma to historyczne znaczenie, ale jest używane rzadziej niż operator zakresu, aby zapobiec nieporozumieniom, czy element jest statyczny czy niestatyczny.

Example ex;
std::string rts = ex.static_str;

Członkowie klasy mają dostęp do elementów statycznych bez określania ich zakresu, tak jak w przypadku elementów klasy niestatycznej.

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;

Nie mogą być mutable , ani nie muszą być; ponieważ nie są one powiązane z żadną instancją, to czy instancja jest stała, czy nie, nie wpływa na elementy statyczne.

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.

Członkowie statyczni szanują modyfikatory dostępu, podobnie jak członkowie niestatyczni.

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.

Ponieważ statyczne funkcje składowe nie są powiązane z daną instancją, nie mają this wskaźnika; z tego powodu nie mogą uzyskać dostępu do niestatycznych zmiennych składowych, dopóki nie przejdą instancji.

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.
};

Z powodu braku this wskaźnika ich adresy nie mogą być przechowywane w funkcjach wskaźników do elementów, a zamiast tego są przechowywane w normalnych wskaźnikach do funkcji.

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.

Ponieważ nie mają this wskaźnika, nie mogą one być const lub volatile , ani nie mogą mieć kwalifikatorów ref. Nie mogą też być wirtualne.

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.
};

Ponieważ nie są powiązane z danym wystąpieniem, statyczne zmienne składowe są skutecznie traktowane jako specjalne zmienne globalne; są tworzone podczas uruchamiania programu i niszczone po jego zakończeniu, niezależnie od tego, czy rzeczywiście istnieją jakieś wystąpienia klasy. Istnieje tylko jedna kopia każdej statycznej zmiennej elementu członkowskiego (chyba że zmienna jest zadeklarowana jako thread_local (C ++ 11 lub nowszy), w którym to przypadku jest jedna kopia na wątek).

Statyczne zmienne składowe mają takie samo powiązanie jak klasa, niezależnie od tego, czy klasa ma powiązanie zewnętrzne czy wewnętrzne. Klasy lokalne i klasy nienazwane nie mogą mieć elementów statycznych.

Niestatyczne funkcje składowe

Klasa może mieć niestatyczne funkcje składowe , które działają na poszczególnych instancjach klasy.

class CL {
  public:
    void member_function() {}
};

Funkcje te są wywoływane w instancji klasy, podobnie jak:

CL instance;
instance.member_function();

Można je zdefiniować w definicji klasy lub poza nią; jeśli są zdefiniowane na zewnątrz, są określone jako objęte zakresem klasy.

struct ST {
    void  defined_inside() {}
    void defined_outside();
};
void ST::defined_outside() {}

Mogą być zakwalifikowani do CV i / lub przekwalifikowani , co wpływa na to, jak widzą instancję, do której zostali wezwani; funkcja zobaczy instancję jako posiadającą określone kwalifikatory cv, jeśli istnieją. Która wersja jest wywoływana, będzie zależała od kwalifikatorów cv instancji. Jeśli nie ma wersji z tymi samymi kwalifikatorami CV co instancja, wywoływana jest wersja bardziej kwalifikująca się do CV, jeśli jest dostępna.

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.
C ++ 11

Kwalifikatory referencyjne funkcji elementu wskazują, czy funkcja ma być wywoływana w instancjach rvalue, i używają tej samej składni, co kwalifikatory cv funkcji.

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.

Kwalifikatory CV i kwalifikatory referencyjne można również łączyć w razie potrzeby.

struct BothCVAndRef {
    void func() const& {} // Called on normal instances.  Sees instance as const.
    void func() &&     {} // Called on temporary instances.
};

Mogą być również wirtualne ; ma to fundamentalne znaczenie dla polimorfizmu i pozwala klasom potomnym zapewnić ten sam interfejs co klasa macierzysta, zapewniając jednocześnie własną funkcjonalność.

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().

Aby uzyskać więcej informacji, zobacz tutaj .

Nienazwany struct / class

Nienazwany struct jest dozwolone (typ nie ma nazwy)

void foo()
{
    struct /* No name */ {
        float x;
        float y;
    } point;
    
    point.x = 42;
}

lub

struct Circle
{
    struct /* No name */ {
        float x;
        float y;
    } center; // but a member name
    float radius;
};

i później

Circle circle;
circle.center.x = 42.f;

ale NIE anonimowy struct (typ bez nazwy i obiekt bez nazwy)

struct InvalidCircle
{
    struct /* No name */ {
        float centerX;
        float centerY;
    }; // No member either.
    float radius;
};

Uwaga: Niektóre kompilatory pozwalają na anonimową struct jako rozszerzenie .

C ++ 11
  • lamdba może być postrzegana jako specjalna nienazwana struct .

  • decltype pozwala pobrać typ nienazwanej struct :

    decltype(circle.point) otherPoint;
    
  • nienazwana instancja struct może być parametrem metody szablonu:

    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";
    }
    


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow