PVOID - Классы (I)

Классы (I)

Этот раздел является переводом туториала C++ Language


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

Объект - это экземпляр класса. С точки зрения переменных класс будет типом, а объект будет переменной.

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

class class_name {
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names;

 Где class_name это допустимый идентификатор для класса, object_names это необязательный список имен объектов этого класса. Тело объявления может содержать члены, которые могут быть объявлениями как полей, так и методов, и, возможно, спецификаторы доступа.

Классы имеют тот же формат, что и простые структуры данных, за исключением того, что они могут также включать методы и иметь спецификаторы доступа. Спецификатор доступа - одно из следующих трех ключевых слов: private, public или protected. Эти спецификаторы изменяют права доступа для членов, которые следуют за ними.

  • закрытые (private) члены класса доступны только из других членов этого же класса (или из его "друзей");
  • защищенные (protected) члены доступны из других членов этого же класса (или из его "друзей"), а также из членов производных классов;
  • открытые (public) члены доступны везде, где доступен объект.

 По умолчанию все члены класса, объявленного с помощью ключевого слова class, имеют закрытый (private) доступ. Следовательно, любой член, который объявлен перед любым другим спецификатором доступа, автоматически получает закрытый доступ. Например: 

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area (void);
} rect;

 Этот код объявляет класс (т.е. тип), называемый Rectangle, и объект (т.е. переменную) этого класса, называемую rect. Этот класс содержит четыре члена: два поля типа int (width и height) с закрытым доступом (поскольку private - это уровень доступа по умолчанию) и два метода с открытым доступом: методы set_values и area, для которых сейчас имееотся только объявление, но не определение.

Обратите внимание на разницу между именем класса и именем объекта. В приведенном примере Rectangle является именем класса (т.е. типом), тогда как rect является объектом типа Rectangle. В таком же отношении находятся int и a в следующем объявлении:

int a;

 Где int - это имя типа (класс), а a - имя переменной (объект). После объявления Rectangle и rect, любой из открытых членов объекта rect может быть доступен, как если бы они были обычными функциями или обычными переменными, вставкой точки (.) между именем объекта и именем члена. Это повторяет синтаксис доступа к членам простых структур данных. Например:

rect.set_values (3,4);
myarea = rect.area();

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

Ниже приведен законченый пример класса Rectangle:

// пример класса
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area() {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect;
  rect.set_values (3,4);
  cout << "area: " << rect.area();
  return 0;
}
area: 12

 

В этом примере содержится оператор разрешения области видимости (::, два двоеточия), который встречался в предыдущих главах в отношении пространств имен (namespaces). Здесь он используется в определении функции set_values чтобы определить член класса вне самого класса.

Обратите внимание, что определение метода area было включено непосредственно в определение класса Rectangle с целью упрощения. И наоборот, прототип set_values просто объявляется в классе, но реализация метода находится за пределами класса. В этом внешнем определении оператор разрешения области видимости (::) используется для указания, что определяемая функция является членом класса Rectangle, а не обычной функцией.

Оператор разрешения области видимости (::) указывает класс, к которому принадлежит определяемый член, и предоставляет точно такую же область видимости, как если бы это определение функции было непосредственно включено в определение класса. Например, функция set_values в предыдущем примере имеет доступ к переменным width и height,  являющимся закрытыми членами класса Rectangle, и таким образом доступным только из других членов класса, таким как этот.

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

Поля width и height имеют закрытый доступ (если не указаны иные спецификаторы доступа, все члены класса, определенного ключевым словом class, имеют закрытый доступ). Объявление их закрытыми делает невозможным доступ к ним извне класса. Это имеет смысл, так как мы уже определили метод для задания значений этим полям внутри объекта: метод set_values. Таким образом, остальная часть программы не должна иметь прямого доступа к ним. Возможно, в таком простом примере, как этот, сложно увидеть, как ограничение доступа к этим полям может быть полезно, но в больших проектах может быть очень важно, чтобы эти значения не могли быть изменены неожиданным образом (неожиданным с точки зрения объекта).

Наиболее важным свойством класса является то, что он является типом, и мы можем объявить множество объектов этого типа. Например, как и в предыдущем примере класса Rectangle, мы можем объявить объект rectb в дополнение к объекту rect:

// пример: один класс, два объекта
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area () {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect, rectb;
  rect.set_values (3,4);
  rectb.set_values (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30 

 

В данном конкретном случае, класс (тип объектов) это Rectangle, два экземпляра (т.е. объекта) которого создаются: rect и rectb. Каждый из них имеет свои собственные поля и методы.

Обратите внимание, что вызов rect.area() не дает того же результата, что вызов rectb.area(). Это потому что каждый объект класса Rectangle имеет свои собственные переменные width и height, а также в некотором роде свои собственные методы set_value и area, которые оперируют с собственными полями объекта.

Классы позволяют программировать с использованием объектно-ориентированной парадигмы: Данные и функции являются членами объекта, уменьшая необходимость передачи и обработки других переменных в качестве аргументов функции, потому что являются частью объекта, член которого вызывается. Обратите внимание, что методам rect.area и rectb.area не передаются аргументы. Эти методы используют непосредственно поля соответствующих объектов rect и rectb.

 

Конструкторы


Что произойдет в предыдущем примере, если мы вызовем метод area до вызова set_values? Результат не определен, так как полям width и height никогда не присваивается значение.

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

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

Класс Rectangle можно легко улучшить путем реализации конструктора:

// пример: конструктор класса
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int,int);
    int area () {return (width*height);}
};

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 30

 

Результат этого примера будет идентичен результату из предыдущего примера. Но сейчас класс Rectangle не имеет метода  set_values, а вместо этого имеет конструктор, который выполняет аналогичные действия: он инициализирует значения width и height значениями аргументов, переданных в него.

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

Rectangle rect (3,4);
Rectangle rectb (5,6);

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

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

 

Перегрузка конструкторов


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

// перегрузка конструкторов класса
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle ();
    Rectangle (int,int);
    int area (void) {return (width*height);}
};

Rectangle::Rectangle () {
  width = 5;
  height = 5;
}

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb;
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}
rect area: 12
rectb area: 25

 

В этом примере создаются два объекта класса Rectangle: rect и rectb. rect инициализируется конструктором с двумя аргументами, как в примере выше.

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

Rectangle rectb;   // вызывается конструктор по умолчанию
Rectangle rectc(); // конструктор по умолчанию НЕ вызывается

 Это происходит потому, что пустые круглые скобки делают rectc объявлением функции, ане объявлением объекта: это будет функция, не принимающая аргументов и  возвращающая значение типа Rectangle.

 

Унифицированная инициализация


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

Во-первых, конструкторы с одним параметром могут быть вызваны с использованием синтаксиса инициализации переменной (использование знака равенства):

class_name object_name = initialization_value;

Совсем недавно в C++ появилась возможность вызова конструкторов с использованием унифицированной инициализации, которая по сути является такой же, как функциональная форма, но использует фигурные скобки ({}) вместо круглых (()):

class_name object_name { value, value, value, ... }

При необходимости этот последний синтаксис может включать знак равенства перед фигурными скобками.

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

// классы и унифицированная инициализация
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) { radius = r; }
    double circum() {return 2*radius*3.14159265;}
};

int main () {
  Circle foo (10.0);   // functional form
  Circle bar = 20.0;   // assignment init.
  Circle baz {30.0};   // uniform init.
  Circle qux = {40.0}; // POD-like

  cout << "foo's circumference: " << foo.circum() << '\n';
  return 0;
}
foo's circumference: 62.8319

 

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

Rectangle rectb;   // вызывается конструктор по умолчанию
Rectangle rectc(); // объявление функции (конструктор по умолчанию НЕ вызывается)
Rectangle rectd{}; // вызывается конструктор по умолчанию

 Выбор синтаксиса для вызова конструкторов это во многом вопрос стиля. Большая часть кода в настоящее время использует функциональную форму, но некоторые наиболее новые руководства по стилю предлагают выбирать унифицированную инициализацию, несмотря на то, что она также имеет свои потенциальные проблемы, как наиболее предпочтительную для initializer_list своего типа.

 

Инициализация членов в конструкторе


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

class Rectangle {
    int width,height;
  public:
    Rectangle(int,int);
    int area() {return width*height;}
};

 Конструктор этого класса может быть определен как обычно:

Rectangle::Rectangle (int x, int y) { width=x; height=y; }

Однако, он также может быть определен с использованием инициализации членов:

Rectangle::Rectangle (int x, int y) : width(x) { height=y; }

Или даже:

Rectangle::Rectangle (int x, int y) : width(x), height(y) { }

 Обратите внимание, как в последнем случае конструктор не делает ничего, кроме инициализации членов, поэтому тело конструктора является пустым.

Для членов фундаментальных типов нет разницы между определенными выше конструкторами, поскольку они не инициализированы по умолчанию, но для объектов (чьим типом является класс), если они не инициализированы после двоеточия, они инициализируются по умолчанию.

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

// member initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) : radius(r) { }
    double area() {return radius*radius*3.14159265;}
};

class Cylinder {
    Circle base;
    double height;
  public:
    Cylinder(double r, double h) : base (r), height(h) {}
    double volume() {return base.area() * height;}
};

int main () {
  Cylinder foo (10,20);

  cout << "foo's volume: " << foo.volume() << '\n';
  return 0;
}
foo's volume: 6283.19

 

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

Эту инициализацию можно реализовать также с помощью синтаксиса унифицированной инициализации, используя фигурные скобки {} вместо круглых ():

Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }

 

Указатели на классы


Имеется также возможность создавать указатели на объекты: После объявления класс становится допустимым типом, поэтому он может использоваться как тип, на который указывает указатель. Например:

Rectangle * prect;

 это указатель на объект класса Rectangle.

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

// pointer to classes example
#include <iostream>
using namespace std;

class Rectangle {
  int width, height;
public:
  Rectangle(int x, int y) : width(x), height(y) {}
  int area(void) { return width * height; }
};


int main() {
  Rectangle obj (3, 4);
  Rectangle * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangle (5, 6);
  baz = new Rectangle[2] { {2,5}, {3,6} };
  cout << "obj's area: " << obj.area() << '\n';
  cout << "*foo's area: " << foo->area() << '\n';
  cout << "*bar's area: " << bar->area() << '\n';
  cout << "baz[0]'s area:" << baz[0].area() << '\n';
  cout << "baz[1]'s area:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}	

 

Этот пример использует несколько операторов для работы с объектами и указателями (операторы *, &, ., ->, [ ]). Они могут быть интерпретированы как:

 выражение   интерпретация 
 *x  указатель x
 &x  адрес x
 x.y  член y объекта x
 x->y  член y объекта, на который указывает x 
 (*x).y  член y объекта, на который указывает x (эквивалентно предыдущему) 
 x[0]  первый объект, на который указывает x
 x[1]  второй объект, на который указывает x
 x[n]  (n+1)-й объект, на который указывает x

 

 Большинство этих выражений были представлены в предыдущих разделах. Прежде всего, это раздел о массивах, в котором описан оператор смещения ([ ]), и раздел о простых структурах данных, в котором описан оператор стрелка (->).

 

Классы, определенные как struct и union


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

Ключевое слово struct, как правило используемое для объявления простых структур данных, может также быть использовано для объявления классов, имеющих методы, с помощью точно такого же синтаксиса, что и для ключевого слова class. Единственным различием между ними является то, что члены классов, объявленных как struct, имеют открытый доступ по умолчанию, в то время как члены классов, объявленных как class, по умолчанию имеют закрытый доступ. Во всех других отношениях оба ключевых слова эквивалентны в данном контексте.

Концепция объединений отличается от классов, объявленных и как struct, и как class, поскольку объединения хранят только один член данных (поле) поле одновременно, но они также являются классами и поэтому также могут иметь методы. Объединения имеют открытый доступ по умолчанию.