Наконец-то мы добрались до самой важной темы во вступительном курсе. Сегодня мы будем говорить о классах и объектах. Выпуск небольшой и не сложный. Что есть хорошо.

Класс - не что иное, как структура, к которой добавили функции. А объект - это структурная переменная.

Данный материал будет более понятным, если вы хорошо освоились со структурами.
Структурное программирование

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

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

Ключевыми понятиями в ООП являются объекты и классы. Классы - это структуры, в которые добавили функции. А объекты - это структурные переменные.
Определение класса

Определение класса должно располагаться до main.

Начнём с простых примеров:

код на языке c++
class soldier
{
public:
  int x,y;
  int ammo;
};

В данном примере определение класса почти идентично определению структур. Есть только два отличия: в заголовке вместо ключевого слово struct стоит class. Второе - в первой строке определения класса стоит public с двоеточием. Вот с public мы разберёмся совсем скоро. Сначала же создадим переменную типа soldier.

soldier a;

a.x = 3;
a.y = 4;
a.ammo = 5;

Здесь мы создали объект a класса soldier. Смотрите, совсем никаких отличий от структурных переменных. В данном случае объекты (переменные классов) можно использовать также как и структурные переменные.
Спецификаторы доступа public и private

По умолчанию в структурах используется спецификатор доступа public, а в классах private. Рассмотрим примеры без спецификаторов:

код на языке c++
struct soldier
{
  int x,y;
};

class soldier
{
  int x,y;
};

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

код на языке c++
struct soldier
{
public:
  int x,y;
};

class soldier
{
private:
  int x,y;
};

Компилятор автоматически вставляет public и private. В структурах по умолчанию используется public, в классах по умолчанию используется private.
Спецификатор доступа public

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

Спецификатор public позволяет переменным/объектам созданным на основе данной структуры/класса получить доступ к полям.

код на языке c++
class soldier
{
public:
  int x,y;
};

soldier a;
a.x = 3;
cout << a.x << "\n";

В данном случае использование классов ничем не отличается от структур.
Спецификатор доступа private

Мы всегда будем объявлять спецификаторы доступа явно: и private и public.

код на языке c++
class soldier
{
private:
  int ammo;

public:
  int x,y;
}

soldier a;
a.x = 3;
a.y = 4;

a.ammo = 5; // !!! Данный код некорректен

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

За пределами класса - значит, в любом месте за фигурными скобками определения класса (есть исключение, но об этом ниже). Если объект класса пытается получить доступ к переменной класса (a.ammo), это тоже считается "за пределами класса".

Спецификаторы доступа (и public, и private) действуют с того момента, как они появляются в классе и до того момента, когда встречается другой спецификатор или определение класса не заканчивается.

Для чего же всё-таки определять переменные в блоке со спецификатором public? Дело в том, что классы помимо переменных, могут содержать и функции.

Функции определённые внутри класса называются методами этого класса.
Методы классов

Рассмотрим пример: допустим мы хотим научить нашего солдата ходить. Вот как это будет выглядеть при использовании структур:

код на языке c++
struct soldier
{
  int x, y;  
};

soldier a = {0,0};
a.x += 1; // боец переместился вправо

Конечно же лучше для передвижения написать функцию:

void move (int x, int y) {}

А как должно выглядеть тело функции? Мы можем просто записать:

a.x += x;
a.y += y;

Но тогда, если у нас будет несколько переменных типа soldier, мы не сможем применить функцию move к ним. Поэтому нам необходимо передавать ещё один аргумент - структурную переменную чьи поля будут изменены. Итак:

код на языке c++
soldier move (soldier a, int x, int y)
{
  a.x += x;
  a.y += y;
  return a;
}

Теперь посмотрим, как рабоать с этой функцией:

код на языке c++
soldier b = {0, 0};
soldier c = {1, 1};

b = move(b, 0, 1); // двигаем b вверх
c = move(c, 0, -1); // двигаем c вниз

В функцию передаётся переменная soldier, но её нужно и вернуть из функции. В функцию дополнительно передаётся 8 байт (поля x,y переменной soldier) и из функции возвращается восемь байт. Лучше конечно передавать soldier по ссылке.

Смотрите сколько сложностей! С классами всё намного проще:

код на языке c++
class soldier
{
public:
  int x,y;

  void move (int dx, int dy)
  {
    x += dx;
    y += dy;
  }
};

Внутри функции мы определили функцию move. Заметьте, в неё не нужно передавать объекты класса - метод сам знает, какой объект его вызвал. dx и dy - значения на которые изменятся поля класса.

Как всё это работает:

soldier a = {0,0};
soldier b = {5,6};
a.move(1,1);
b.move(-2,3);

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

Теперь давайте всё-таки разберёмся, для чего нужен спецификатор доступа private.

Рассмотрим пример:

код на языке c++
class soldier
{
private:
  int x,y;

public:
  void move(int dx, int dy)
  {
    x += dx;
    y += dy;
  }
};

У нас нет доступа к переменным x,y. Но они доступны внутри класса. Метод move также имеет к ним доступ, так как он является методом класса soldier.

Теперь, изменить x и y можно только с помощью метода move. x и y скрыты. И это очень правильно. Понять, почему это правильно, мы сможем не скоро - только когда начнём работать с большими классами.
Конструктор класса

Теперь, когда нашы данные надёжно спрятаны с помощью спецификатора privatе, попробуем их проинициализировать:

soldier a = {0, 0};

Но тут компилятор выдаст ошибку: дескать, чтож это ты друг пытаешься получить доступ к скрытым данным?

Что же делать? Получается что у нас есть доступ к этип переменным через move, а как же задать этим переменным начальные значения?

Если вы попытаетесь следующий способ:

код на языке c++
class soldier
{
private
  int x = 0;
  int y = 0;
};

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

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

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

код на языке c++
class soldier
{
private:
  int x,y;

public:
  soldier () // конструктор
  {
    x = 0; // инициализация переменных
    y = 0;
  }

  void move (int dx, int dy)
  {
    x += dx;
    y += dy;
  }  
};

Конструктор вызывается при создании объектов класса:

soldier a, b;

Здесь для обоих объектов a и b вызывается конструктор.

Возможна и такая форма записи:

soldier a(), b();

Здесь как бы намекается, что при создании a и b вызывается функция (конструктор).

Кстати! Конструкторы вызываются не только для пользовательских типов данных, но и для стандартных. Например, при определении переменной типа char вызывается соответствующий конструктор. При этом данный конструктор выделяет память под переменную - один байт.
Список инициализации конструктора

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

soldier () : x(0), y(0), ammo(100)
{}

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

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

Допустим, нам нужны две одинаковые функции, работающие с разными типами данных. В одну функцию передаётся аргумент типа int, а в другую - типа float. В данном случае для двух функций можно использовать одно имя. Такие функции и называются перегруженными:

код на языке c++
int function (int x)
{
  return 0;
}

int function (float x)
{
  return 0;
}

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

код на языке c++
int function (int x)
{
  return 0;
}

float function (int x)
{
  return 0;
}

Перегруженные конструкторы

Теперь рассмотрим применение перегруженных функций на практике:
Так как конструкторы - это не что иное, как функции, то соответственно их можно перегружать. Для этого в определении класса нужно определить два конструктора:

код на языке c++
soldier() : x(0), y(0),ammo(0)
{}

soldier (int x_init, int y_init, int ammo_init) : x(x_init),
             y(y_init), ammo(ammo_init)
{}

Заголовок второго конструктора получился довольно длинным, но это компенсируется появившимися у нас возможностями:

soldier s1;
soldier s1(3,4,10);

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

Методы класса можно определять не только внутри классов. При этом в классе должно быть объявление (прототип) метода:

код на языке c++
class soldier
{
public:
  int attack();
};

int soldier::attack()
{
  return 0;
}

Внутри класса в блоке public мы объявляем метод attack. Определение метода находится за пределами класса, но метод всё равно может пользоваться сокрытыми даннами своего класса. Для того, чтобы было понятно, что данная функция является методом класса, используется операция глобального разрешения ::. Сначала мы записываем имя класса, знак операции глобального разрешения и затем - имя метода. Во всём остальном определение функции совпадает с тем, что мы уже видели.

Вот как будет выглядить класс soldier если мы вынесем метод move за пределы класса:

код на языке c++
class soldier
{
private:
  int x, y;
  int ammo;

public:
  soldier() : x(0), y(0), ammo(0) {}
  soldier(int x_init, int y_init, int ammo_init) : x(x_init),
              y(y_init), ammo(ammo_init)
  {}

  void move(int, int);
};

void soldier::move(int dx, int dy)
{
  x = dx;
  y = dy;
}

Методы выносятся за пределы определения класса при создании крупных классов.
Статические данные класса

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

код на языке c++
class soldier
{
public:
  static int counter;

  soldier()
  {
    counter++;
  }
}

int soldier::counter = 0;

Переменную counter мы объявили с ключевым словом static. Кроме того за пределами класса мы инициализировали эту переменную. При инициализации мы также как и в классах использовали операцию глобального разрешения.

В конструкторе мы увеличиваем переменную на единицу.

Теперь каждый раз, когда в программе будет создаваться объект типа soldier, переменная counter будет инкрементироваться.
Упражнения:

Изучите тему Стеки и очереди и выполните к ней упражнения: доработайте представленные там классы. Если будут вопросы, пишите на e-mail.

Статья взята с shatalov.su