Перейти к содержанию

Си++/Объектно-ориентированное программирование

Материал из Викиучебника — открытых книг для открытого мира

Средства объектно-ориентированного программирования[править]

Основными механизмами в объектно-ориентированном программировании есть:

  • полиморфизм;
  • наследование;
  • инкапсуляция.

Инкапсуляция[править]

Инкапсуляция — это объединение переменных и функций (свойств и методов) в классе.

Полем класса может быть функция:

class C
{
public:
 void f();
};

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

f(); // вызовется глобальная f
C::f(); // ошибка
С obj;
obj.f(); // можно
C* ptr;
ptr->f(); // тоже можно

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

class C
{
public:
 void f()
 {
   printf("We are %p\n", this);
 }
};

Но можно её описать (т.е. написать тело) и ниже, например, в .cpp при помещении объявления класса в .h:

class C
{
public:
 void f();
};

void С::f()
{
  printf("We are %p\n", this);
}

Такие функции называются «функции-члены» или «методы».

Так как метод всегда зовется для какого-то объекта, ему всегда передается адрес этого объекта. Этот указатель доступен в теле метода как ключевое слово this. Типа псевдо-переменной this — C* const, если метод описан без слова const в конце, и const C* const — иначе:

class C
{
 int i;
public:
 void f();
 void fc() const;
};

void С::f()
{
  this->i = 0; // работает
}

void С::fc() const
{
  this->i = 0; // ошибка, this указывает на константу, все её поля тоже константы
}

Т.е. метод со словом const не может изменять свой объект.

Кроме того, в методах не обязательно писать this->f, достаточно просто написать "f".

Имена, примененные в методе, ищутся:

  • по {} блокам в самом методе, формальные параметры есть часть самого внешнего блока;
  • поля класса, если нашлось такое — то понимается как this->name;
  • глобалы.
class C
{
 int i;
public:
 void f();
};

void С::f()
{
  i = 0; // это this->i
}

static поля и методы[править]

Вот тут var — не поле класса, а просто такая хитрая глобальная переменная:

class C
{
public:
 static int var;
};

Её надо обязательно где-то описать, в классе она лишь объявлена:

int C::var = 1;

Отличия от глобала:

  • для нее действуют метки private/protected/public;
  • имя объявлено в пространстве имен класса, а не в глобальном пространстве имен.

Точно так же statfunc — не настоящий метод, а просто такая функция:

class C
{
public:
 static void statfunc();
};

Описывается она так же, как и метод, но в ней не бывает this и она не может прямо использовать имена нестатических (настоящих) полей и методов класса.

Вызвана она может быть:

  • из любого метода, и настоящего, и статического, как statfunc();
  • извне класса как C::statfunc, если позволяют метки доступа;
  • как obj.statfunc() или pobj->statfunc(), в этих случаях obj и pobj не более чем указывают на тип и не используются при вызове.

Права доступа к именам в классах[править]

Внутри объявления класса могут встречаться метки доступа protected, public и private. Они задают уровень доступа ко всем объявлениям, следующим до следующей такой метки или конца класса. Меток доступа может быть сколько угодно.

Единственное отличие struct от class в том, что по умолчанию до первой такой метки объявления считаются private в class и public в struct.

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

Все, что объявлено public, может быть использовано где угодно.

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

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

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

Наследование[править]

Наследование — это помещение в производный класс (который наследует) свойств и методов базового класса (которого наследуют).

Возможно:

class B
{
 int fb;
};
class D : public B
{
 int fd;
};

Это означает, что в объекте класса D есть все поля класса B (fb), плюс ещё и дополнительно объявленные, специфичные поля для D (fd). Концептуально это означает, что любой объект класса D — это, помимо всего прочего, ещё и полноценный объект класса B. Это и есть наследование.

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

Таким образом, любой объект D есть разновидность объекта B, такая, что к ней добавлено что-то ещё.

Класс B называется базовым, D — производным.

Возможно указать несколько базовых классов (т.н. «множественное наследование»):

class B
{
 int fb;
};
class B2
{
 int fb2;
};
class D : public B, public B2
{
 int fd;
};

Тут в объекте D будет подобъекты B и B2, и три поля: fb, fb2 и fd.

Преобразование указателей от D* к B* работает и здесь. При этом, если преобразование к единственной (и вообще к первой) базе есть умозрительная операция внутри компилятора, не генерирующая кода, то преобразование ко второй и далее базе — D* к B2* — означает генерацию кода, который прибавит к указателю значение смещения B2 внутри D.

Аналогично работает и приведение «вниз» — от B2* к D*.

Возможно использовать и приватное наследование — class D : private B. В этом случае вся B-часть класса D доступна только из контекста класса D, т.е. аналогична тому, что объявлено private в классе B. Даже приведение указателя к B* возможно только в контексте класса D — приватный базовый класс считается деталью реализации, неизвестной внешнему миру.

В публичном же случае вся публичная часть B становится публичной частью D, а protected и private части В — приватными частями D.

Полиморфизм[править]

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

Представим себе:

class B
{
public:
 void f();
};
class D : public B
{
public:
 void f();
};

и далее:

B* pb = new D;
pb->f();

то в данном случае решение о том, какую функцию звать — B::f или D::f — принимается на основе известного компилятору типа указателя, т.е. B::f.

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

Такое требует специальной поддержки во время выполнения, ибо компилятор, увидев B*, не знает, есть ли это объект, созданный как B, или же B-часть в объекте класса D.

Эта возможность есть, она выглядит так:

class B
{
public:
 virtual void f();
};
class D : public B
{
public:
 void f();
};

В этом случае pb->f() будет исполнять совсем иной код. Обычно этот код находит указатель на таблицу виртуальных методов в начале объекта и выбирает функцию из этой таблицы. Указатель же на таблицу зависит от того, как создавался объект B — как подобъект в D или же непосредственно как B. Заполнение этого указателя обычно делается специальным кодом, автоматически сгенерированным внутри конструктора.

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

Допустимо ещё и такое:

class B
{
public:
 virtual void f() = 0;
};

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

Класс, в котором не объявлено ничего, кроме чисто-виртуальных функций, совпадает с понятием «интерфейс» в других языках программирования. На такой класс можно объявлять указатели и потом звать через них эти виртуальные методы, не зная на самом деле, что именно позовется. На таком основана технология COM фирмы Microsoft (а равно все дизайн-паттерны «банды четырех»).

Явная квалификация pb->D::f() пересиливает виртуальный механизм вызова, зовется D::f.

Полиморфизм и множественное наследование[править]

struct B
{
 virtual void b();
};

struct B2
{ 
 virtual void b2();
};

struct D : public B, public B2
{
 void b();
 void b2();
};

void D::b2()
{
 // здесь this указывает на D
}

D d;
B2* pb2 = &d; // автоприведение вверх по решетке наследования к не-первой базе, к значению
              // адреса D добавилось смещение

// в другом файле

pb2->b2();    // а здесь компилятор не знает, что pb2 на самом деле указывает на B2-часть
              // в D, потому он действует в лоб, а именно генерит что-то вроде
              // (pb2->__vtbl[0])(pb2);
              // Все бы хорошо, но вызвать в итоге надо D::b2, а у нее this - это D*, а не B2*,
              // приведение требует вычитания смещения, а где его делать?
              // Поступают так: компилятор генерит "переходничок", который приводит B2 к D
              // и потом зовет D::b2, и именно адрес этого "переходничка" и кладут в __vtbl

См. ассемблерный код, сгенеренный при использовании библиотеки Microsoft ATL — там такое на каждом шагу.

Конструктор[править]

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

Эта возможность реализуется конструктором.

Синтаксически конструктор есть метод, имя которого совпадает с именем класса:

class C
{
 int i;
public:
 C(int p)
 { 
  i = p; 
 }
};

Создание объекта такого класса обязательно требует указания параметра типа int, который помещается в поле i объекта.

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

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

Объект классового типа может возникнуть в коде как:

  • глобал

Конструкторы глобалов зовутся до входа в main платформо-зависимым кодом.

  • локал

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

MyClass c;
MyClass c1(); // одно и то же, используется конструктор без параметров - УЖЕ НЕТ в новом
              // стандарте языка! для явно заданного конструктора таки одно и то же, но
              // при отсутствии конструкторов эти записи различны тем, делается ли заполнение
              // нулями или нет. См. value и default initialization в описании языка.
              // Язык изменен для того, чтобы T(), которое используется в std::string<T> как
              // завершающий символ строки, всегда заполнялось нулями
MyClass c2(1, 2); // зовется конструктор с 2мя параметрами целочисленных типов
MyClass c3 = MyClass(1, 2); // строго говоря, это создание временного объекта с копированием
                            // в c3, но компилятор имеет право исключать временные объекты,
                            // потому это то же, что и c2
MyClass c4(1);
MyClass c5 = MyClass(1);
MyClass c6 = 1;  // три равносильные записи, последняя использует преобразование типа посредством
                 // конструктора из int во временный объект MyClass, который потом копируется
                 // в c6 - временный объект исключается компилятором

В С++ есть ключевое слово explicit, позволяющее запретить неявное преобразование аргумента конструктора. Применяется только к конструкторам.

class MyClass
{
  int i;
public:
  explicit MyClass(int r)
  {
    i = r;
  }
};

int main()
{
  MyClass A(9); // правильно
  MyClass B = 9; // ошибка компиляции, неявное преобразование запрещено explicit-конструктором
  
  return 0;
}

Вообще, если конструктор имеет 1 параметр, рекомендуется использовать explicit, т.к. неявное преобразование типов потенциально опасно.

  • создание по new

Создание массива по new обязательно использует конструктор без параметров. Иначе же:

 MyClass* obj = new MyClass(1, 2);

Скобки круглые (квадратные скобки - массив) и идут за именем класса (скобки за словом new передают параметры в operator new, а не в конструктор).

  • поле класса или подобъект базового класса.

Для таких случаев в синтаксисе конструктора предусмотрен так называемый ctor-инициализатор (список инициализации):

class B
{
public:
 B(int);
};

class D : public B
{
 B fb;
public:
 D();
}

D::D()
 : B(1), fb(2)   // это и есть ctor-инициализатор (список инициализации), сначала
                 // для подобъекта базы, потом для поля fb
{
}

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

  • временный объект

Явный синтаксис для временного объекта — MyClass(1, 2). Это выражение, его значение есть временный объект типа MyClass, проинициализированный вызовом MyClass::MyClass(1, 2).

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

Также временный объект создается для исполнения оператора приведения к типу (MyClass), если тип назначения классовый. То же самое может быть сделано и неявно, если требуется объект данного классового типа, а значение выражения какого-то иного типа - ищется подходящий конструктор, и делается временный объект. Это т.н. «преобразование типа посредством конструктора».

Таким образом мы получаем, что (MyClass)1 и MyClass(1) — строго одно и то же.

Более того, Си++ не делает разницы в синтаксисе для классовых и неклассовых типов, т.е. синтаксис long(1) вполне возможен и опять же есть то же самое, что и (long)1. Точно так же можно:

 long i = 1;
 long i(1); // одно и то же

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

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

Конструкторы не могут быть виртуальными, т.е. вызываемыми тем механизмом, что и virtual функции. Понятие "виртуальный конструктор" означает совсем иное (паттерн factory, реализованный на объекте типа type_info).

В конструкторах невозможны классические способы обработки ошибок (через код ошибки), возможно лишь: а) throw; б) никогда не писать способный к отказу код в конструкторе, заведя отдельный метод Init.

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

Если же у класса есть поля, обязательно требующие ctor-инициализатора (базы без конструктора по умолчанию/такие же поля/ссылки) — то такая генерация невозможна, и требуется обязательное написание конструкторов явно.

Конструктор с 1 параметром типа «ссылка на этот же класс» — C::C(C&) — называется «конструктор копирования». Если у класса таковой не написан явно, то компилятор генерирует его автоматически (независимо от наличия каких-то ещё конструкторов), сгенеренный конструктор зовет такие же конструкторы у всех базовых классов и полей. Неклассовые поля копируются побитово.

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

Следует отличать C::C(C&) и C::operator=(C&). Вторая функция вызывается для уже построенного объекта слева, конструктор же копирования — нет, он и строит этот объект (разница инициализации и присваивания).

Деструктор[править]

В Си++ есть возможность гарантированного освобождения занятых объектом ресурсов при выходе объекта из области его жизни, т.е. гарантированное разрушение.

Для такого разрушения используется деструктор. Синтаксически это функция с именем ~MyClass:

class C
{
public:
 ~C();
};

Деструктор вызывается когда-то для любого объекта.

  • глобал — после main платформо-зависимым кодом
  • локал — по закрывающей } (включая переходы return/break/continue/goto, а также исключения)
  • по new — в delete или delete[]
  • поле или база класса — автосгенеренным кодом в прологе/эпилоге деструктора производного класса после исполнения явного тела этого деструктора
  • временный — когда-то по усмотрению компилятора.

Как и с конструктором, нельзя брать адрес деструктора и он не возвращает значений (даже void).

Деструкторы не наследуются. Если у класса не написан деструктор явно - он генерится компилятором, сгенеренный деструктор аналогичен пустому телу — ~C() {}.

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

Тело деструктора должно разрушить все поля объекта — позвать close(), CloseHandle, ->Release() и тому подобные функции.

Деструктор может быть объявлен private. Объекты такого класса могут создаваться только оператором new без квадратных скобок, любая другая попытка создания даст ошибку "разрушение невыполнимо-деструктор приватен". Такие объекты должны иметь метод разрушения, в котором будет исполняться delete this;

См. также[править]