Zoeken…


Syntaxis

  • variable.member_var = constant;
  • variable.member_function ();
  • variable_pointer-> member_var = constant;
  • variable_pointer-> member_function ();

Opmerkingen

Merk op dat het enige verschil tussen de sleutelwoorden struct en class is dat de lidvariabelen, lidfuncties en basisklassen van een struct public , terwijl ze in een class private . C ++ programmeurs noemen het meestal een klasse als het constructors en destructors heeft, en het vermogen om zijn eigen invarianten af te dwingen; of een struct als het slechts een eenvoudige verzameling waarden is, maar de taal C ++ zelf maakt geen onderscheid.

Basisbeginselen van de klas

Een klasse is een door de gebruiker gedefinieerd type. Een klasse wordt geïntroduceerd met het trefwoord class , struct of union . In informeel gebruik verwijst de term "klasse" meestal alleen naar niet-vakbondsklassen.

Een klas is een verzameling klasleden , die kunnen zijn:

  • lidvariabelen (ook "velden" genoemd),
  • lidfuncties (ook "methoden" genoemd),
  • lidtypen of typedefs (bijv. "geneste klassen"),
  • ledersjablonen (van welke aard dan ook: sjabloon voor variabele, functie, klasse of alias)

De class en struct sleutelwoorden, class-sleutels genoemd , zijn grotendeels uitwisselbaar, behalve dat de standaardtoegangsaanduiding voor leden en bases 'privé' is voor een klasse die is gedeclareerd met de class sleutel en 'public' voor een klasse die is gedeclareerd met de struct of union . (zie Toegangsmodificaties ).

De volgende codefragmenten zijn bijvoorbeeld identiek:

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

Door een klasse aan te geven, wordt een nieuw type aan uw programma toegevoegd en is het mogelijk om objecten van die klasse te instantiëren door

Vector my_vector;

Leden van een klas worden benaderd met behulp van puntsyntaxis.

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;

Toegangsspecificaties

Er zijn drie sleutelwoorden die fungeren als toegangsspecificaties . Deze beperken de toegang tot klasseleden die de specificatie volgen, totdat een andere specificatie het toegangsniveau opnieuw wijzigt:

keyword Beschrijving
public Iedereen heeft toegang
protected Alleen de klas zelf, afgeleide klassen en vrienden hebben toegang
private Alleen de klas zelf en vrienden hebben toegang

Wanneer het type wordt gedefinieerd met behulp van het sleutelwoord class , is de standaardtoegangsaanduiding private , maar als het type wordt gedefinieerd met het sleutelwoord struct , is de standaardtoegangsaanduiding 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

Toegangsspecificaties worden meestal gebruikt om de toegang tot interne velden en methoden te beperken en de programmeur te dwingen een specifieke interface te gebruiken, bijvoorbeeld om het gebruik van getters en setters te dwingen in plaats van rechtstreeks naar een variabele te verwijzen:

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;

};

Het gebruik van protected is handig om bepaalde functionaliteit van het type alleen toegankelijk te maken voor de afgeleide klassen, bijvoorbeeld in de volgende code is de methode Plus2Base calculateValue() alleen toegankelijk voor klassen die zijn afgeleid van de basisklasse Plus2Base , zoals 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; }
};

Merk op dat het friend sleutelwoord kan worden gebruikt om toegangsuitzonderingen toe te voegen aan functies of types voor toegang tot beschermde en privé-leden.

De public , protected en private trefwoorden kunnen ook worden gebruikt om toegang te verlenen tot subobjecten van de basisklasse. Zie het voorbeeld van overerving .

Erfenis

Klassen / structuren kunnen erfelijkheidsrelaties hebben.

Als een klasse / struct B erft van een klasse / struct A , betekent dit dat B als ouder A . We zeggen dat B een afgeleide klasse / struct van A is en A de basisklasse / struct is.

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

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

Er zijn 3 vormen van overerving voor een klasse / struct:

  • public
  • private
  • protected

Houd er rekening mee dat de standaardovererving hetzelfde is als de standaardzichtbaarheid van leden: public als u het sleutelwoord struct gebruikt en private voor het sleutelwoord class .

Het is zelfs mogelijk om een class afleiden uit een struct (of vice versa). In dit geval wordt de standaardovererving beheerd door het kind, dus een struct die uit een class voortkomt, wordt standaard overgezet naar publieke overerving, en een class die voortkomt uit een struct heeft standaard privé-overerving.

public erfenis:

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

private erfenis:

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 erfenis:

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

Merk op dat hoewel protected erfenis is toegestaan, het feitelijke gebruik ervan zeldzaam is. Een voorbeeld van hoe protected erfenis wordt gebruikt in de toepassing is in gedeeltelijke basisklasse specialisatie (meestal aangeduid als "gecontroleerd polymorfisme").

Toen OOP relatief nieuw was, werd vaak gezegd dat (publieke) erfenis een "IS-A" -relatie modelleerde. Dat wil zeggen, openbare erfenis is alleen correct als een instantie van de afgeleide klasse ook een instantie van de basisklasse is.

Dit werd later verfijnd in het Liskov-vervangingsprincipe : openbare erfenis mag alleen worden gebruikt wanneer / als een instantie van de afgeleide klasse een instantie van de basisklasse kan vervangen onder elke mogelijke omstandigheid (en nog steeds zinvol is).

Van privé-erfenis wordt doorgaans gezegd dat het een geheel andere relatie belichaamt: "wordt geïmplementeerd in termen van" (soms een "HAS-A" -relatie genoemd). Een Stack klasse kan bijvoorbeeld privé erven van een Vector klasse. Privé-erfenis vertoont een veel grotere gelijkenis met aggregatie dan met openbare erfenis.

Beschermde erfenis wordt bijna nooit gebruikt, en er is geen algemene overeenstemming over wat voor soort relatie het belichaamt.

Virtueel erfdeel

Wanneer u overerving gebruikt, kunt u het virtual trefwoord opgeven:

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

Wanneer klasse B virtuele basis A heeft, betekent dit dat A zich in de meest afgeleide klasse van de erfenisboom bevindt, en dus dat de meeste afgeleide klasse ook verantwoordelijk is voor het initialiseren van die virtuele basis:

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

Als we de reactie van /*A(88)*/ opheffen, krijgen we geen fout, omdat C nu zijn indirecte virtuele basis A initialiseert.

Merk ook op dat wanneer we een variabel object , de meest afgeleide klasse C , dus C is verantwoordelijk voor het maken van (aanroepende constructor van) A en dus is de waarde van A::member 88 , niet 5 (zoals het zou zijn als we object van type B ) maken.

Het is handig bij het oplossen van het diamantprobleem :

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

B en C erven beide van A , en D erft van B en C , dus er zijn 2 gevallen van A in D ! Dit resulteert in dubbelzinnigheid wanneer u lid bent van A tot en met D , omdat de compiler geen manier heeft om te weten uit welke klasse u toegang wilt krijgen tot dat lid (degene die B erft, of degene die wordt geërfd door C ?) .

Virtuele overerving lost dit probleem op: aangezien virtuele base zich alleen in het meeste afgeleide object bevindt, zal er slechts één exemplaar van 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
    }
};

Het verwijderen van de opmerkingen lost de dubbelzinnigheid op.

Meerdere overerving

Afgezien van enkele erfenis:

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

U kunt ook meerdere overerving hebben:

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

C zal nu tegelijkertijd van A en B erven.

Opmerking: dit kan tot dubbelzinnigheid leiden als dezelfde namen worden gebruikt in meerdere overgeërfde class of struct . Doe voorzichtig!

Ambiguïteit in meervoudige overerving

Meerdere overerving kan in bepaalde gevallen nuttig zijn, maar soms een vreemd soort problemen tegenkomen tijdens het gebruik van meervoudige overerving.

Bijvoorbeeld: twee basisklassen hebben functies met dezelfde naam die niet worden genegeerd in afgeleide klasse en als u code schrijft om toegang te krijgen tot die functie met behulp van een object van afgeleide klasse, geeft de compiler fout omdat deze niet kan bepalen welke functie moet worden aangeroepen. Hier is een code voor dit soort dubbelzinnigheid bij meervoudige overerving.

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

Maar dit probleem kan worden opgelost met behulp van de bereikresolutie om aan te geven welke functie moet worden geclassificeerd in base1 of base2:

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

Toegang tot klasleden

Om toegang te krijgen tot lidvariabelen en lidfuncties van een object van een klasse, de . operator wordt gebruikt:

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

Bij toegang tot de leden van een klasse via een pointer, wordt de -> operator vaak gebruikt. Als alternatief kan het exemplaar worden verwijderd en de . operator gebruikt, hoewel dit minder gebruikelijk is:

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

Bij toegang tot statische klassenleden wordt de operator :: gebruikt, maar op de naam van de klasse in plaats van een instantie ervan. Als alternatief kan het statische lid worden benaderd vanuit een instantie of een pointer naar een instantie met behulp van de . of -> operator, respectievelijk, met dezelfde syntaxis als toegang tot niet-statische leden.

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

Achtergrond

De operator -> is nodig omdat de operator voor toegang tot leden . heeft voorrang op de dereferencing-operator * .

Men zou verwachten dat *pa p zou verlagen (resulterend in een verwijzing naar het object waarnaar p verwijst) en vervolgens toegang zou krijgen tot zijn lid a . Maar in feite probeert het toegang te krijgen tot het lid a van p en het vervolgens te verwijderen. Dat wil zeggen *pa is gelijk aan *(pa) . In het bovenstaande voorbeeld zou dit resulteren in een compilerfout vanwege twee feiten: ten eerste is p een pointer en heeft geen lid a . Ten tweede is a een geheel getal en kan er dus niet naar worden afgeleid.

De vaak gebruikte oplossing voor dit probleem zou zijn om expliciet de prioriteit te controleren: (*p).a

In plaats daarvan wordt de operator -> bijna altijd gebruikt. Het is een korte hand om eerst de aanwijzer van de verwijzing te verwijderen en vervolgens toegang te krijgen. Dat wil zeggen (*p).a is precies hetzelfde als p->a .

De operator :: is de scope-operator, die op dezelfde manier wordt gebruikt als voor toegang tot een lid van een naamruimte. Dit komt omdat een statisch lid van de klasse wordt beschouwd als behorend tot het bereik van die klasse, maar niet wordt beschouwd als een lid van instanties van die klasse. Het gebruik van normaal . en -> is ook toegestaan voor statische leden, ondanks dat ze geen instantieleden zijn, om historische redenen; dit is nuttig voor het schrijven van generieke code in sjablonen, omdat de beller zich geen zorgen hoeft te maken of een bepaalde lidfunctie statisch of niet-statisch is.

Privé-erfenis: beperking van de basisklasse-interface

Privé-erfenis is handig wanneer het nodig is om de openbare interface van de klasse te beperken:

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

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

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

Deze benadering voorkomt efficiënt toegang tot de A openbare methoden door te casten naar de A pointer of verwijzing:

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

In het geval van openbare erfenis zal dergelijke casting toegang bieden tot alle openbare A-methoden ondanks alternatieve manieren om dit te voorkomen in afgeleide B, zoals verbergen:

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

of privé met:

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

dan is het voor beide gevallen mogelijk:

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

Laatste klassen en structuren

C ++ 11

Het afleiden van een klasse kan worden verboden met de final specificeerder. Laten we een laatste klas verklaren:

class A final {
};

Nu zal elke poging om het te subclasseren een compilatiefout veroorzaken:

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

Eindklasse kan overal in de klassenhiërarchie verschijnen:

class A {
};

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

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

Vriendschap

Het friend sleutelwoord wordt gebruikt om andere klassen en functies toegang te geven aan privé- en beschermde leden van de klas, zelfs als deze buiten het bereik van de klas zijn gedefinieerd.

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

Geneste klassen / structuren

Een class of struct kan ook een andere class / struct definitie in zichzelf bevatten, die een "geneste klasse" wordt genoemd; in deze situatie wordt de bevattende klasse de "omsluitende klasse" genoemd. De definitie van de geneste klasse wordt beschouwd als een lid van de omringende klasse, maar is verder gescheiden.

struct Outer {
    struct Inner { };
};

Van buiten de omringende klasse worden geneste klassen geopend met de scope-operator. Vanuit de omringende klasse kunnen geneste klassen echter zonder kwalificaties worden gebruikt:

struct Outer {
    struct Inner { };

    Inner in;
};

// ...

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

Net als bij een niet-geneste class / struct , kunnen struct en statische variabelen worden gedefinieerd binnen een geneste klasse of in de omringende naamruimte. Ze kunnen echter niet worden gedefinieerd binnen de omsluitende klasse, omdat deze als een andere klasse wordt beschouwd dan de geneste klasse.

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

Net als bij niet-geneste klassen, kunnen geneste klassen later worden gedeclareerd en later worden gedefinieerd, op voorwaarde dat ze worden gedefinieerd voordat ze direct worden gebruikt.

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

Vóór C ++ 11 hadden geneste klassen alleen toegang tot typenamen, static leden en opsommers van de omringende klasse; alle andere leden die in de omringende klasse waren gedefinieerd, waren verboden terrein.

C ++ 11

Vanaf C ++ 11 worden geneste klassen en leden daarvan behandeld alsof ze friend van de omringende klasse zijn en hebben ze toegang tot alle leden volgens de gebruikelijke toegangsregels; als leden van de geneste klasse de mogelijkheid vereisen om een of meer niet-statische leden van de omringende klasse te evalueren, moeten ze daarom een instantie worden doorgegeven:

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

Omgekeerd wordt de omringende klasse niet behandeld als een vriend van de geneste klasse en heeft hij dus geen toegang tot zijn privéleden zonder expliciet toestemming te hebben gekregen.

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

Vrienden van een geneste klas worden niet automatisch beschouwd als vrienden van de omringende klas; als ze ook vrienden van de omringende klasse moeten zijn, moet dit apart worden verklaard. Omgekeerd, omdat de omringende klasse niet automatisch wordt beschouwd als een vriend van de geneste klasse, zullen vrienden van de omringende klasse ook niet worden beschouwd als vrienden van de geneste klasse.

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

Zoals bij alle andere groepsleden, kunnen geneste klassen alleen van buiten de klas worden benoemd als ze openbare toegang hebben. U mag ze echter openen, ongeacht de toegangsmodificator, zolang u ze niet expliciet een naam geeft.

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.

U kunt ook een alias voor een geneste klasse maken. Als een type-alias zich in de omsluitende klasse bevindt, kunnen het geneste type en de type-alias verschillende toegangsmodificatoren hebben. Als het type alias zich buiten de omringende klasse bevindt, moet de geneste klasse of een typedef daarvan openbaar zijn.

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.

Net als bij andere klassen, kunnen geneste klassen zowel afkomstig zijn van of afgeleid zijn van andere klassen.

struct Base {};

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

struct Derived : Outer::Inner {};

Dit kan handig zijn in situaties waarin de omsluitende klasse wordt afgeleid door een andere klasse, door de programmeur in staat te stellen de geneste klasse indien nodig bij te werken. Dit kan worden gecombineerd met een typedef om een consistente naam te geven voor de geneste klasse van elke omsluitende klasse:

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

In het bovenstaande geval, zowel BaseOuter en DerivedOuter leveren de lid soort Inner , als BaseInner_ en DerivedInner_ resp. Hiermee kunnen geneste typen worden afgeleid zonder de interface van de omsluitende klasse te verbreken en kan het geneste type polymorf worden gebruikt.

Typen leden en aliassen

Een class of struct kan ook lidtype-aliassen definiëren. Dit zijn type-aliassen die deel uitmaken van en worden behandeld als leden van de klasse zelf.

struct IHaveATypedef {
    typedef int MyTypedef;
};

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

Net als statische leden zijn deze typedefs toegankelijk via de scope-operator, :: .

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

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

Net als bij normale aliassen, mag elk lidtype-alias verwijzen naar elk type dat is gedefinieerd of alias vóór, maar niet na, de definitie ervan. Evenzo kan een typedef buiten de klassedefinitie verwijzen naar toegankelijke typedefs binnen de klassedefinitie, op voorwaarde dat het na de klassedefinitie komt.

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.

Lidtype-aliassen kunnen met elk toegangsniveau worden gedeclareerd en respecteren de juiste toegangsmodificator.

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

Dit kan worden gebruikt om een abstractieniveau te bieden, waardoor de ontwerper van een klasse zijn interne werking kan wijzigen zonder code die erop vertrouwt te verbreken.

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

Als in deze situatie de SomeComplexType wordt gewijzigd van SomeComplexType in een ander type, typedef alleen de typedef en de friend te worden gewijzigd; zolang de helperklasse dezelfde functionaliteit biedt, zal elke code die het gebruikt als Something::MyHelper plaats van het op te geven bij de naam, meestal nog steeds werken zonder enige aanpassingen. Op deze manier minimaliseren we de hoeveelheid code die moet worden gewijzigd wanneer de onderliggende implementatie wordt gewijzigd, zodat de typenaam slechts op één locatie hoeft te worden gewijzigd.

Dit kan ook worden gecombineerd met decltype , indien decltype .

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

  public:
    typedef decltype(helper) MyHelper;

  private:
    InternalVariable<MyHelper> ivh;

    // ...

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

    // ...
};

In deze situatie zal het wijzigen van de implementatie van SomethingElse::helper automatisch het typedef voor ons wijzigen, vanwege decltype . Dit minimaliseert het aantal aanpassingen dat nodig is wanneer we de helper willen veranderen, wat het risico op menselijke fouten minimaliseert.

Zoals met alles kan dit echter te ver worden doorgevoerd. Als de typenaam bijvoorbeeld slechts een of twee keer intern en nul keer extern wordt gebruikt, hoeft u er geen alias voor te geven. Als het honderden of duizenden keren in een project wordt gebruikt, of als het een naam heeft die lang genoeg is, dan kan het handig zijn om het als een typef te verstrekken in plaats van het altijd in absolute termen te gebruiken. Men moet een balans vinden tussen compatibiliteit en gemak met de hoeveelheid onnodige ruis die wordt gecreëerd.


Dit kan ook worden gebruikt met sjabloonklassen om toegang te bieden tot de sjabloonparameters van buiten de klasse.

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

Dit wordt meestal gebruikt met containers, die meestal hun elementtype, en andere helpertypen, als aliassen van het lidtype leveren. De meeste containers in de standaardbibliotheek C ++ bieden bijvoorbeeld de volgende 12 helpertypen, samen met eventuele andere speciale typen die ze mogelijk nodig hebben.

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

Voorafgaand aan C ++ 11, werd het ook vaak gebruikt om een soort "template typedef " te bieden, omdat de functie nog niet beschikbaar was; deze zijn iets minder gebruikelijk geworden bij de introductie van aliasjablonen, maar zijn nog steeds nuttig in sommige situaties (en worden gecombineerd met aliasjablonen in andere situaties, die erg handig kunnen zijn voor het verkrijgen van afzonderlijke componenten van een complex type zoals een functiepointer ). Ze gebruiken meestal het type voor hun type alias.

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

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

Dit werd vaak gebruikt met typen met meerdere sjabloonparameters, om een alias te bieden die een of meer van de parameters definieert.

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

Statische klasleden

Een klasse mag ook static leden hebben, dit kunnen variabelen of functies zijn. Deze worden geacht binnen het bereik van de klasse te vallen, maar worden niet als normale leden behandeld; ze hebben een statische opslagduur (ze bestaan vanaf het begin van het programma tot het einde), zijn niet gebonden aan een bepaald exemplaar van de klasse en er bestaat slechts één exemplaar voor de hele klasse.

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)

Statische lidvariabelen worden niet beschouwd als gedefinieerd binnen de klasse, alleen gedeclareerd, en hebben dus hun definitie buiten de klassendefinitie; het is de programmeur toegestaan, maar niet verplicht, om statische variabelen in hun definitie te initialiseren. Bij het definiëren van de lidvariabelen wordt het trefwoord static weggelaten.

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.

Hierdoor kunnen statische variabelen onvolledige typen zijn (behalve void ), zolang ze later als een volledig type worden gedefinieerd.

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

Statische lidfuncties kunnen binnen of buiten de klassedefinitie worden gedefinieerd, net als bij normale lidfuncties. Net als bij statische lidvariabelen wordt het trefwoord static weggelaten bij het definiëren van statische lidfuncties buiten de klassedefinitie.

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

Als een statische lidvariabele const maar niet volatile wordt verklaard, en van een integraal of opsommingstype is, kan deze bij declaratie worden geïnitialiseerd, binnen de klassedefinitie.

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

Vanaf C ++ 11 kunnen statische lidvariabelen van LiteralType typen (types die tijdens het compileren kunnen worden geconstrueerd volgens constexpr regels) ook als constexpr worden verklaard; als dat zo is, moeten ze worden geïnitialiseerd binnen de klassedefinitie.

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.

Als een const of constexpr statische lidvariabele odr wordt gebruikt (informeel, als het adres is overgenomen of is toegewezen aan een referentie), moet deze nog steeds een afzonderlijke definitie hebben, buiten de klassedefinitie. Deze definitie mag geen initialisatie bevatten.

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.

Aangezien statische leden niet zijn gekoppeld aan een bepaald exemplaar, kunnen ze worden geopend met de scope-operator, :: .

std::string str = Example::static_str;

Ze zijn ook toegankelijk alsof het normale, niet-statische leden zijn. Dit is van historisch belang, maar wordt minder vaak gebruikt dan de scope-operator om verwarring te voorkomen over de vraag of een lid statisch of niet-statisch is.

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

Klasleden hebben toegang tot statische leden zonder hun bereik te kwalificeren, zoals niet-statische klasleden.

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;

Ze kunnen niet mutable , noch zouden ze moeten zijn; omdat ze niet gebonden zijn aan een bepaalde instantie, heeft een instantie geen invloed op statische leden.

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.

Statische leden respecteren toegangsmodificatoren, net als niet-statische leden.

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.

Omdat ze niet gebonden zijn aan een bepaald exemplaar, hebben statische lidfuncties this aanwijzer niet; hierdoor hebben ze geen toegang tot niet-statische lidvariabelen tenzij ze een exemplaar hebben doorgegeven.

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

Vanwege het ontbreken van een this pointer, laat de adressen niet worden opgeslagen pointers naar lid-functies, en in plaats daarvan opgeslagen in normale wijzers naar functies.

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.

Omdat ze this aanwijzer niet hebben, kunnen ze ook niet const of volatile , noch kunnen ze ref-kwalificaties hebben. Ze kunnen ook niet virtueel zijn.

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

Omdat ze niet gebonden zijn aan een bepaald exemplaar, worden statische lidvariabelen effectief behandeld als speciale globale variabelen; ze worden gemaakt wanneer het programma start en vernietigd wanneer het wordt afgesloten, ongeacht of er instanties van de klasse bestaan. Er bestaat slechts één exemplaar van elke statische lidvariabele (tenzij de variabele thread_local wordt verklaard (C ++ 11 of hoger), in welk geval er één exemplaar per thread is.

Statische lidvariabelen hebben dezelfde koppeling als de klasse, ongeacht of de klasse externe of interne koppeling heeft. Lokale klassen en klassen zonder naam mogen geen statische leden hebben.

Niet-statische lidfuncties

Een klasse kan niet-statische lidfuncties hebben , die op afzonderlijke instanties van de klasse werken.

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

Deze functies worden als volgt op een instantie van de klasse aangeroepen:

CL instance;
instance.member_function();

Ze kunnen binnen of buiten de klassedefinitie worden gedefinieerd; indien buiten gedefinieerd, worden ze gespecificeerd als zijnde binnen het bereik van de klasse.

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

Ze kunnen CV-gekwalificeerd en / of ref-gekwalificeerd zijn , wat invloed heeft op hoe ze de instantie zien waarnaar ze worden opgeroepen; de functie ziet de instantie als de opgegeven cv-kwalificatie (s), indien aanwezig. Welke versie wordt genoemd, is gebaseerd op de cv-kwalificaties van de instantie. Als er geen versie is met dezelfde cv-kwalificaties als de instantie, wordt een meer-cv-gekwalificeerde versie aangeroepen indien beschikbaar.

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

Lidfunctie ref-kwalificaties geven aan of de functie al dan niet moet worden aangeroepen in rvalue-instanties en gebruiken dezelfde syntaxis als functie cv-kwalificaties.

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-kwalificaties en ref-kwalificaties kunnen indien nodig ook worden gecombineerd.

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

Ze kunnen ook virtueel zijn ; dit is van fundamenteel belang voor polymorfisme en stelt een kindklasse (n) in staat dezelfde interface te bieden als de ouderklasse, terwijl ze hun eigen functionaliteit bieden.

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

Zie hier voor meer informatie.

Naamloze struct / klasse

Naamloze struct is toegestaan (type heeft geen naam)

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

of

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

en later

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

maar NIET anonieme struct (naamloos type en naamloos object)

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

Opmerking: sommige compilers staan anonieme struct als extensie .

C ++ 11
  • lamdba kan worden gezien als een speciale naamloze struct .

  • decltype maakt het mogelijk om het type naamloze struct :

    decltype(circle.point) otherPoint;
    
  • naamloze struct instantie kan de parameter van de sjabloonmethode zijn:

    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
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow