Поиск…


Синтаксис

  • variable.member_var = константа;
  • variable.member_function ();
  • variable_pointer-> member_var = constant;
  • variable_pointer-> member_function ();

замечания

Обратите внимание, что единственная разница между ключевыми словами struct и class заключается в том, что по умолчанию переменные-члены, функции-члены и базовые классы struct являются public , а в class они являются private . Программисты на С ++, как правило, называют это классом, если у него есть конструкторы и деструкторы, а также возможность принудительно применять свои собственные инварианты; или struct, если это просто простой набор значений, но сам язык C ++ не делает различий.

Основы классов

Класс является определяемым пользователем типом. Класс вводится с ключевым словом class , struct или union . В разговорной речи термин «класс» обычно относится только к не-профсоюзным классам.

Класс - это набор членов класса , который может быть:

  • переменные-члены (также называемые «полями»)
  • функции-члены (также называемые «методы»)
  • типы членов или typedefs (например, «вложенные классы»)
  • шаблоны членов (любого типа: шаблон переменной, функции, класса или псевдонима)

Ключевые слова class и struct , называемые ключами классов , в значительной степени взаимозаменяемы, за исключением того, что спецификатор доступа по умолчанию для членов и баз является «закрытым» для класса, объявленного с ключом class и «общедоступным» для класса, объявленного с помощью ключа struct или union (см. модификаторы доступа ).

Например, следующие фрагменты кода идентичны:

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

Объявляя класс, в вашу программу добавляется новый тип, и можно создавать объекты этого класса посредством

Vector my_vector;

Доступ к членам класса осуществляется с помощью точечного синтаксиса.

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;

Спецификаторы доступа

В качестве спецификаторов доступа действуют три ключевых слова . Они ограничивают доступ к членам класса после спецификатора, пока другой спецификатор снова не изменит уровень доступа:

Ключевое слово Описание
public У каждого есть доступ
protected Только сам класс, производные классы и друзья имеют доступ
private Только у самого класса и друзей есть доступ

Когда тип определяется с помощью ключевого слова class , спецификатор доступа по умолчанию является private , но если тип определяется с помощью ключевого слова struct , спецификатор доступа по умолчанию является 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

Спецификаторы доступа в основном используются для ограничения доступа к внутренним полям и методам и заставляют программиста использовать определенный интерфейс, например, чтобы принудительно использовать геттеры и сеттеры вместо прямого обращения к переменной:

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;

};

Использование protected полезно для того, чтобы определенные функциональные возможности этого типа были доступны только для производных классов, например, в следующем коде, метод calculateValue() доступен только для классов, полученных из базового класса Plus2Base , например 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; }
};

Обратите внимание, что ключевое слово friend может использоваться для добавления исключений доступа к функциям или типам для доступа к защищенным и закрытым членам.

public , protected и private ключевые слова также могут использоваться для предоставления или ограничения доступа к подобъектам базового класса. См. Пример Inheritance .

наследование

Классы / структуры могут иметь отношения наследования.

Если класс / структура B наследуется от класса / структуры A , это означает, что B имеет родительский A Мы говорим, что B - производный класс / struct из A , а A - базовый класс / struct.

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

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

Существует 3 формы наследования для класса / структуры:

  • public
  • private
  • protected

Обратите внимание, что наследование по умолчанию такое же, как видимость членов по умолчанию: public если вы используете ключевое слово struct , и private для ключевого слова class .

Можно даже получить class из struct (или наоборот). В этом случае наследование по умолчанию контролируется дочерним элементом, поэтому struct которая выводится из class , по умолчанию будет иметь наследование по умолчанию, а class , полученный из struct , по умолчанию будет иметь частное наследование.

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

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 наследование:

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

Обратите внимание, что хотя protected наследование разрешено, фактическое его использование встречается редко. Одним из примеров того, как protected наследование используется в приложении, является частная специализация базового класса (обычно называемая «контролируемый полиморфизм»).

Когда ООП был относительно новым, (публичное) наследование часто говорилось о моделировании отношения «IS-A». То есть, наследование является правильным, только если экземпляр производного класса также является экземпляром базового класса.

Это позже было уточнено в Принципе замещения Лискова : публичное наследование должно использоваться только тогда, когда / если экземпляр производного класса может быть заменен экземпляром базового класса при любых возможных обстоятельствах (и по-прежнему имеет смысл).

Как правило, частное наследство воплощает совершенно разные отношения: «реализуется в терминах» (иногда называемое отношением «HAS-A»). Например, класс Stack может наследовать конфиденциально из класса Vector . Частное наследование имеет гораздо большее сходство с агрегированием, чем государственное наследование.

Защищенное наследование почти никогда не используется, и нет общего согласия относительно того, какие отношения он воплощает.

Виртуальное наследование

При использовании наследования вы можете указать ключевое слово virtual :

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

Когда класс B имеет виртуальную базу A это означает, что A будет находиться в большинстве производных классов дерева наследования и, следовательно, большинство производных классов также отвечает за инициализацию этой виртуальной базы:

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

Если мы не будем комментировать /*A(88)*/ мы не получим никакой ошибки, так как C теперь инициализирует ее косвенную виртуальную базу A

Также обратите внимание, что когда мы создаем переменный object , большинство производных классов C , поэтому C отвечает за создание (вызывающий конструктор) A и, следовательно, значение A::member равно 88 , а не 5 (как и было бы, если бы мы были создавая объект типа B ).

Это полезно при решении проблемы алмаза :

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

B и C , как наследовать от A и D наследует от B и C , так что 2 экземпляра A в D ! Это приводит к двусмысленности при доступе к члену от A до D , поскольку компилятор не знает, из какого класса вы хотите получить доступ к этому члену (тот, который B наследует, или тот, который наследуется C ?) ,

Виртуальное наследование решает эту проблему: поскольку виртуальная база находится только в большинстве производных объектов, будет только один экземпляр A в 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
    }
};

Удаление комментариев устраняет двусмысленность.

Многократное наследование

Помимо одиночного наследования:

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

Вы также можете иметь множественное наследование:

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

Теперь C будет наследовать от A и B одновременно.

Примечание. Это может привести к двусмысленности, если одни и те же имена используются в нескольких унаследованных class s или struct s. Быть осторожен!

Неоднозначность в множественном наследовании

Множественное наследование может быть полезным в некоторых случаях, но иногда нечетным видом проблем при использовании множественного наследования.

Например: у двух базовых классов есть функции с тем же именем, которые не переопределены в производном классе, и если вы пишете код для доступа к этой функции с использованием объекта производного класса, компилятор показывает ошибку, потому что он не может определить, какую функцию вызывать. Вот код для этого типа двусмысленности в множественном наследовании.

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

Но эту проблему можно решить с помощью функции разрешения разрешающей способности, чтобы указать, какая функция относится к классу base1 или base2:

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

Доступ к членам класса

Чтобы получить доступ к переменным - членам и функциям - членам объекта класса, то . используется оператор:

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

При доступе к членам класса с помощью указателя обычно используется оператор -> . Альтернативно, экземпляр может быть разыменован и . оператора, хотя это реже:

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

При доступе к статическим членам класса используется оператор :: , но на имя класса вместо экземпляра его. Альтернативно, статический член может быть доступен из экземпляра или указателя на экземпляр с помощью . или -> , соответственно, с тем же синтаксисом, что и доступ к нестационарным членам.

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

Фон

Оператор -> необходим, потому что оператор доступа к члену . имеет приоритет над оператором разыменования * .

Можно было бы ожидать, что *pa будет разыменовывать p (в результате ссылается на объект p , указывающий на), а затем обращается к его члену a . Но на самом деле он пытается получить доступ к члену a из p а затем разыменовать его. Ie *pa эквивалентно *(pa) . В приведенном выше примере это приведет к ошибке компилятора из-за двух фактов: во-первых, p является указателем и не имеет члена a . Во-вторых, a является целым числом и, следовательно, не может быть разыменован.

Необычно используемым решением этой проблемы было бы прямое управление приоритетом: (*p).a

Вместо этого оператор- -> почти всегда используется. Это короткая рука для первого разыменования указателя, а затем доступа к нему. Ie (*p).a точно такое же, как p->a .

Оператор :: оператор области видимости, используемый таким же образом, как и доступ к члену пространства имен. Это связано с тем, что статический член класса рассматривается как область видимости этого класса, но не считается членом экземпляров этого класса. Использование нормального . и -> также допускается для статических членов, несмотря на то, что они не являются членами экземпляра по историческим причинам; это полезно для написания генерического кода в шаблонах, поскольку вызывающему абоненту не нужно беспокоиться о том, является ли данная функция-член статичной или нестатической.

Частное наследование: ограничение интерфейса базового класса

Частное наследование полезно, когда требуется ограничить публичный интерфейс класса:

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

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

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

Этот подход эффективно предотвращает доступ к общедоступным методам A путем отбрасывания указателя A или ссылки:

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

В случае публичного наследования такое литье предоставит доступ ко всем общедоступным методам A, несмотря на альтернативные способы предотвращения этого в производном B, например, скрытие:

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

или частное использование:

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

то для обоих случаев это возможно:

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

Заключительные классы и структуры

C ++ 11

Вывод класса может быть запрещен с final спецификатором. Объявим окончательный класс:

class A final {
};

Теперь любая попытка подкласса вызовет ошибку компиляции:

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

Конечный класс может отображаться в любом месте иерархии классов:

class A {
};

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

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

дружба

Ключевое слово friend используется для предоставления другим классам и функциям доступа к закрытым и защищенным членам класса, даже если они определены вне рамок класса.

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

Вложенные классы / структуры

class или struct также могут содержать другое определение class / struct внутри себя, которое называется «вложенным классом»; в этой ситуации содержащий класс называется «охватывающим классом». Вложенное определение класса считается членом входящего класса, но в противном случае оно является отдельным.

struct Outer {
    struct Inner { };
};

За пределами закрывающего класса доступ к вложенным классам осуществляется с помощью оператора области видимости. Однако изнутри закрывающего класса вложенные классы могут использоваться без квалификаторов:

struct Outer {
    struct Inner { };

    Inner in;
};

// ...

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

Как и в случае не-вложенного class / struct , функции-члены и статические переменные могут быть определены либо внутри вложенного класса, либо в охватывающем пространстве имен. Тем не менее, они не могут быть определены внутри охватывающего класса, поскольку он считается другим классом, чем вложенный класс.

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

Как и в случае не-вложенных классов, вложенные классы могут быть объявлены и определены позже, если они определены до их непосредственного использования.

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

До C ++ 11 вложенные классы имели доступ только к именам типов, static членам и счетчикам из охватывающего класса; все остальные члены, определенные в охватывающем классе, были недоступны.

C ++ 11

Начиная с C ++ 11, вложенные классы и их члены обрабатываются так, как если бы они были friend s входящего класса и могли обращаться ко всем своим членам в соответствии с обычными правилами доступа; если члены вложенного класса требуют возможности оценить один или несколько нестатических членов охватывающего класса, они должны, таким образом, передать экземпляр:

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

И наоборот, класс-оболочка не рассматривается как друг вложенного класса и, следовательно, не может получить доступ к его частным членам без явного разрешения.

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

Друзья вложенного класса автоматически не считаются друзьями окружающего класса; если они также должны быть друзьями окружающего класса, это должно быть объявлено отдельно. И наоборот, поскольку закрывающий класс автоматически не считается другом вложенного класса, ни друзья окружающего класса не будут считаться друзьями вложенного класса.

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

Как и все другие члены класса, вложенные классы могут быть названы только вне класса, если они имеют открытый доступ. Тем не менее, вам разрешен доступ к ним независимо от модификатора доступа до тех пор, пока вы не укажете их явно.

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.

Вы также можете создать псевдоним типа для вложенного класса. Если псевдоним типа содержится в охватывающем классе, вложенный тип и псевдоним типа могут иметь разные модификаторы доступа. Если тип псевдоним находится вне его класса, он требует , чтобы либо вложенный класс, или typedef их, быть публичным.

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.

Как и в случае с другими классами, вложенные классы могут быть получены из или из других классов.

struct Base {};

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

struct Derived : Outer::Inner {};

Это может быть полезно в ситуациях, когда охватывающий класс получен из другого класса, позволяя программисту обновлять вложенный класс по мере необходимости. Это можно объединить с typedef, чтобы обеспечить согласованное имя для каждого вложенного класса вложенного класса:

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

В приведенном выше случае, как BaseOuter и DerivedOuter поставляют тип члена Inner , как BaseInner_ и DerivedInner_ , соответственно. Это позволяет выводить вложенные типы без нарушения интерфейса охватывающего класса и позволяет использовать вложенный тип в полиморфном виде.

Типы участников и псевдонимы

class или struct также могут определять псевдонимы типа члена, которые являются псевдонимами типа, содержащимися внутри и рассматриваемыми как члены самого класса.

struct IHaveATypedef {
    typedef int MyTypedef;
};

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

Как и статические члены, эти typedefs доступны с помощью оператора области видимости :: .

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

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

Как и в случае с обычными псевдонимами типа, каждому псевдониму типа члена разрешается ссылаться на любой тип, определенный или сглаженный раньше, но не после его определения. Аналогично, typedef вне определения класса может ссылаться на любые доступные typedef в определении класса, если он приходит после определения класса.

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.

Алиасы типа пользователя могут быть объявлены с любым уровнем доступа и будут уважать соответствующий модификатор доступа.

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

Это можно использовать для обеспечения уровня абстракции, позволяя дизайнеру класса изменять свои внутренние работы без нарушения кода, который опирается на него.

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

В этой ситуации, если класс-помощник изменен с SomeComplexType на какой-либо другой тип, нужно будет изменить только объявление typedef и friend ; поскольку класс-помощник предоставляет те же функции, любой код, который использует его как Something::MyHelper вместо указания его по имени, как правило, будет работать без каких-либо изменений. Таким образом, мы минимизируем количество кода, которое необходимо изменить при изменении базовой реализации, так что имя типа необходимо изменить только в одном месте.

Это также можно сочетать с decltype , если этого так decltype .

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

  public:
    typedef decltype(helper) MyHelper;

  private:
    InternalVariable<MyHelper> ivh;

    // ...

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

    // ...
};

В этой ситуации изменение реализации SomethingElse::helper автоматически изменит typedef для нас из-за decltype . Это минимизирует количество необходимых изменений, когда мы хотим изменить helper , который минимизирует риск человеческой ошибки.

Однако, как и во всем, это можно занять слишком далеко. Если имя типа используется только один или два раза внутренне и нулевое время извне, например, нет необходимости предоставлять псевдоним для него. Если он используется сотни или тысячи раз во всем проекте, или если у него есть достаточно длинное имя, тогда может быть полезно предоставить его как typedef, а не всегда использовать его в абсолютных выражениях. Нужно балансировать вперед совместимость и удобство с количеством создаваемого ненужного шума.


Это также можно использовать с классами шаблонов, чтобы обеспечить доступ к параметрам шаблона вне класса.

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

Обычно это используется с контейнерами, которые обычно предоставляют свой тип элемента и другие вспомогательные типы в качестве псевдонимов типа члена. Например, большинство контейнеров в стандартной библиотеке C ++ предоставляют следующие 12 вспомогательных типов, а также любые другие специальные типы, которые могут потребоваться.

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

До C ++ 11 он также обычно использовался для создания «шаблона typedef », поскольку эта функция еще не была доступна; они стали немного менее распространены с введением шаблонов псевдонимов, но по-прежнему полезны в некоторых ситуациях (и сочетаются с шаблонами псевдонимов в других ситуациях, что может быть очень полезно для получения отдельных компонентов сложного типа, таких как указатель на функцию ). Они обычно используют type имени для своего псевдонима типа.

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

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

Это часто использовалось с типами с несколькими параметрами шаблона, чтобы обеспечить псевдоним, который определяет один или несколько параметров.

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

Статические члены класса

Класс также может иметь static элементы, которые могут быть либо переменными, либо функциями. Они считаются находящимися в классе, но не рассматриваются как обычные члены; они имеют статическую продолжительность хранения (они существуют с самого начала программы до конца), не привязаны к конкретному экземпляру класса, и для всего класса существует только одна копия.

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)

Статические переменные-члены не считаются определенными внутри класса, только объявлены и, следовательно, имеют свое определение вне определения класса; программисту разрешено, но не требуется, инициализировать статические переменные в их определении. При определении переменных-членов ключевое слово static опущено.

class Example {
    static int num_instances;               // Declaration.

  public:
    static std::string static_str;          // Declaration.

    // ...
};

int         Example::num_instances;         // Definition.  Zero-initialised.
std::string Example::static_str = "Hello."; // Definition.

В связи с этим статические переменные могут быть неполными (кроме void ), если они позже определены как полный тип.

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

Функции статического члена могут быть определены внутри или вне определения класса, как и для обычных функций-членов. Как и для статических переменных-членов, ключевое слово static опущено при определении статических функций-членов вне определения класса.

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

Если статическая переменная-член объявляется const но не является volatile и имеет тип интеграла или перечисления, ее можно инициализировать при объявлении внутри определения класса.

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

Начиная с C ++ 11, статические переменные- LiteralType типов LiteralType (типы, которые могут быть constexpr во время компиляции, в соответствии с правилами constexpr ) также могут быть объявлены как constexpr ; если это так, они должны быть инициализированы в определении класса.

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.

Если const или constexpr статическая переменная член УСО-используется (неофициально, если он имеет свой адрес Предпринятые или назначается в качестве ссылки), то он должен еще отдельное определение, вне определения класса. Это определение не позволяет содержать инициализатор.

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.

Поскольку статические члены не привязаны к данному экземпляру, к ним можно получить доступ с помощью оператора области видимости :: .

std::string str = Example::static_str;

Их также можно получить, как если бы они были обычными, нестационарными членами. Это имеет историческое значение, но используется реже, чем оператор области, чтобы предотвратить путаницу в отношении того, является ли элемент статическим или нестационарным.

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

Члены класса могут получить доступ к статическим членам без квалификации своей области, как и для нестатических членов класса.

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;

Они не могут быть mutable и не должны быть такими; поскольку они не привязаны к какому-либо конкретному экземпляру, независимо от того, является ли экземпляр или не является константой, не затрагивает статические члены.

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.

Статические члены рассматривают модификаторы доступа, как и нестатические элементы.

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.

Поскольку они не привязаны к данному экземпляру, статические функции-члены не имеют this указателя; из-за этого они не могут получить доступ к нестационарным переменным-членам, если не передал экземпляр.

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

Из-за отсутствия this указателя их адреса не могут храниться в указателях-членах-функциях и вместо этого сохраняются в обычных указателях-функциях.

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.

Из - за не имея this указатель, они также не могут быть const или volatile , они не могут иметь реф-классификаторы. Они также не могут быть виртуальными.

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

Поскольку они не привязаны к данному экземпляру, статические переменные-члены эффективно рассматриваются как специальные глобальные переменные; они создаются при запуске программы и уничтожаются при ее выходе, независимо от того, существуют ли какие-либо экземпляры класса. Существует только одна копия каждой статической переменной-члена (если только переменная не объявлена thread_local (C ++ 11 или новее), в этом случае есть один экземпляр для потока).

Статические переменные-члены имеют ту же связь, что и класс, независимо от того, имеет ли класс внешнюю или внутреннюю связь. Локальным классам и неназванным классам не разрешено иметь статические члены.

Нестатические функции-члены

Класс может иметь нестатические функции-члены , которые работают с отдельными экземплярами класса.

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

Эти функции вызываются в экземпляре класса, например:

CL instance;
instance.member_function();

Они могут быть определены как внутри, так и вне определения класса; если они определены снаружи, они указываются как входящие в область класса.

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

Они могут быть квалифицированными и / или квалифицированными квалифицированными специалистами , влияющими на то, как они видят экземпляр, к которому они обращаются; функция увидит экземпляр как имеющий указанный cv-квалификатор (ы), если он есть. Какая версия называется, будет основана на cv-квалификаторах экземпляра. Если в качестве экземпляра нет версии с теми же cv-квалификаторами, то будет выпущена версия с более высокой квалификацией, если она доступна.

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

Функция ref-qualifiers функции-члена указывает, должна ли функция быть вызвана для экземпляров rvalue, и использовать тот же синтаксис, что и функция cv-qualifiers.

struct RefQualifiers {
    void func() &  {} // 1: Called on normal instances.
    void func() && {} // 2: Called on rvalue (temporary) instances.
};

RefQualifiers rf;
rf.func();              // Calls #1.
RefQualifiers{}.func(); // Calls #2.

CV-квалификаторы и ref-квалификаторы также могут быть объединены, если необходимо.

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

Они также могут быть виртуальными ; это имеет основополагающее значение для полиморфизма и позволяет дочернему классу (классам) предоставлять тот же интерфейс, что и родительский класс, и при этом предоставлять свои собственные функции.

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

Для получения дополнительной информации см. Здесь .

Без названия struct / class

Доступна struct (тип не имеет имени)

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

или же

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

и позже

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

но НЕ анонимная struct (неназванный тип и неназванный объект)

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

Примечание. Некоторые компиляторы разрешают анонимную struct как расширение .

C ++ 11
  • lamdba можно рассматривать как специальную неназванную struct .

  • decltype позволяет получить тип неназванной struct :

    decltype(circle.point) otherPoint;
    
  • unnamed struct instance может быть параметром метода шаблона:

    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
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow