Найти тему
ZDG

От C к C++: ООП

Оглавление

Я писал на языке C давно, и сейчас вернулся к нему просто для души. Мне просто нравится.

Однако я вообще не знаю C++ и никогда не писал на нём, хотя это и покажется странным. Многие уверены, что C и C++ это практически одно и то же, ну как Java и JavaScript :)

Да, C это подмножество C++, и поэтому можно написать программу на C, дать файлу расширение .cpp и скомпилировать компилятором C++. Формально это будет программа, написанная на C++, но на самом деле нет.

Можно писать на C, используя некоторые фишки из C++, например задание значений полей структуры по умолчанию:

Так мы делаем свой C немного более продвинутым, оставаясь в уютном ламповом комфорте.

Есть и другие вещи, вроде использования потоков ввода-вывода вместо привычных puts() или printf():

-2

Но как раз это мне активно не нравится, так как ломает логику изначального синтаксиса языка.

В общем, в C++ есть много нововведений, но чего конкретно мне стало не хватать в C, так это ООП. Я бы не отказался от варианта чистого C с добавленным ООП и ничего более. Но ведь как раз это и можно осуществить!

Зачем ООП?

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

Всё, что я могу сделать, это разнести их по разным файлам. Так, функции диспетчера находятся в файле dispatcher.c, функции рендера в файле render.c, и т.д.

Но тут возникает вторая проблема. Так как функция никому не принадлежит, её имя может пересекаться с именем какой-то другой функции. В том числе это может произойти, когда подключается библиотека с чужими функциями. Поэтому как выход используются уникальные префиксы. Например, мой проект называется GUI, поэтому у всех функций в нём будет префикс GUI_, чтобы не совпасть с функциями из какого-то другого проекта. Далее, все функции диспетчера я называю GUI_dispatcher_*, все функции рендера GUI_render_* и т.д.

Использование префиксов загромождает код, но это ещё не все. Я эмулирую ООП-подход, делая как бы "класс" диспетчера в виде структуры GUI_Dispatcher, и укомплектовывая эту структуру как бы "методами класса" в виде функций, которые называются GUI_dispatcher_*. Естественно, они никак не связаны c "классом" GUI_Dispatcher, и поэтому указатель на экземпляр "класса" должен передаваться им всем в качестве параметра:

-3

Надо сказать, что даже в языках с ООП такой подход практикуется, например, в Perl и Python. Первый параметр метода это всегда ссылка на экземпляр класса, который называют self.

Здесь C держит некоторый паритет, но возможности ООП идут гораздо дальше, так что я займусь изучением ООП в C++.

Да, именно изучением, потому что на данный момент я ничего не знаю и буду писать прямо вот на ходу.

От структуры к классу

Создать описание класса не представляет проблем:

-4

Смотрите, я всего-навсего заменил struct на class, и у меня получился класс. Класс!

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

MyClass test;

Объект получается, когда мы вызываем конструктор класса new MyClass(). Конструктор класса возвращает ссылку на объект, и эта ссылка присваивается переменной:

test = new MyClass();

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

struct MyStruct test;

Мы уже можем пользоваться полями структуры struct MyStruct, она уже есть в виде 8-ми байт на стеке. То же самое происходит, когда мы создаём переменную класса MyClass:

MyClass test;

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

Тем не менее, конструкторы в C++ существуют и вызываются автоматически при объявлении переменной класса, но чем они в таком случае занимаются? В нашем классе нет конструктора, но его можно добавить:

-5

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

Но сейчас ничего работать не будет, потому что в вышеприведённом листинге и свойства класса, и его конструктор по умолчанию считаются приватными, то есть недоступными извне. Чтобы сделать их публичными, добавляем метку public:

-6

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

-7

Но что такое this в конструкторе? Это указатель на наш объект test, который попадает туда автоматически. Чтобы было нагляднее, расмотрим вариант чистого C. Для эмуляции такого класса нам пришлось бы сделать структуру MyStruct:

-8

И написать для неё функцию-конструктор, которая бы инициализировала свойства структуры:

-9

В функцию передаётся указатель на структуру, которую надо инициализировать. Вот это оно и есть, только в классе нам не надо отдельно вызывать конструктор, потому что он вызовется сам, и не надо передавать в конструктор указатель на объект, потому что он и так неявно передаётся и называется this.

Методы класса

Это обычные наши функции, которые объявляются внутри класса:

-10

В данном примере я добавил методы setX() и getX(), которые устанавливают либо возвращают значение свойства x. Дополнительно я вынес свойства x, y из раздела public, и они стали приватными. Хотя приватное это всё, что не в public, для большей ясности можно использовать метку private. Но когда реально нужна метка private? Когда после публичного раздела надо опять расположить приватный, хотя я не уверен, что это будет хорошей практикой:

-11

Теперь доступ к свойству x возможен только через методы:

-12

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

Обратим внимание на метод setX():

-13

Здесь передаётся параметр с именем x, который присваивается свойству класса также с именем x. Это разные x: один из них просто x, а другой this->x. Смотрим, что будет, если написать x без this:

-14

Параметр x я переименовал в num, а вместо this->x написал x. Результат получился тот же самый. То есть мы можем опускать this->, если в классе есть свойство x. Оно подставится по умолчанию. А теперь настоящая битва двух ёкодзун:

-15

Что мы здесь реально получим? Параметр x = x, или this->x = this->x? Вообще без разницы – это выражение ничего не меняет. Но на самом деле this->x там не участвует, так как перекрывается локальным параметром x. Так что одной бесполезной загадкой меньше.

Вообще практика опускания this встречается, кажется, в C# и, кажется, в Java, и точно была в уже мёртвом ActionScript 3. Не считаю её хорошей и сам не пользуюсь именно потому, что становится сложно следить, какая переменная из какого контекста берётся.

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

Читайте дальше:

Наука
7 млн интересуются