Я писал на языке C давно, и сейчас вернулся к нему просто для души. Мне просто нравится.
Однако я вообще не знаю C++ и никогда не писал на нём, хотя это и покажется странным. Многие уверены, что C и C++ это практически одно и то же, ну как Java и JavaScript :)
Да, C это подмножество C++, и поэтому можно написать программу на C, дать файлу расширение .cpp и скомпилировать компилятором C++. Формально это будет программа, написанная на C++, но на самом деле нет.
Можно писать на C, используя некоторые фишки из C++, например задание значений полей структуры по умолчанию:
Так мы делаем свой C немного более продвинутым, оставаясь в уютном ламповом комфорте.
Есть и другие вещи, вроде использования потоков ввода-вывода вместо привычных puts() или printf():
Но как раз это мне активно не нравится, так как ломает логику изначального синтаксиса языка.
В общем, в C++ есть много нововведений, но чего конкретно мне стало не хватать в C, так это ООП. Я бы не отказался от варианта чистого C с добавленным ООП и ничего более. Но ведь как раз это и можно осуществить!
Зачем ООП?
Первая проблема это структуризация кода. Например, в проекте GUI, которым я сейчас занимаюсь, есть большое количество функций, выполняющих разные задачи. Их нельзя организовать иерархически, каждая функция это просто подпрограмма одной большой программы.
Всё, что я могу сделать, это разнести их по разным файлам. Так, функции диспетчера находятся в файле dispatcher.c, функции рендера в файле render.c, и т.д.
Но тут возникает вторая проблема. Так как функция никому не принадлежит, её имя может пересекаться с именем какой-то другой функции. В том числе это может произойти, когда подключается библиотека с чужими функциями. Поэтому как выход используются уникальные префиксы. Например, мой проект называется GUI, поэтому у всех функций в нём будет префикс GUI_, чтобы не совпасть с функциями из какого-то другого проекта. Далее, все функции диспетчера я называю GUI_dispatcher_*, все функции рендера GUI_render_* и т.д.
Использование префиксов загромождает код, но это ещё не все. Я эмулирую ООП-подход, делая как бы "класс" диспетчера в виде структуры GUI_Dispatcher, и укомплектовывая эту структуру как бы "методами класса" в виде функций, которые называются GUI_dispatcher_*. Естественно, они никак не связаны c "классом" GUI_Dispatcher, и поэтому указатель на экземпляр "класса" должен передаваться им всем в качестве параметра:
Надо сказать, что даже в языках с ООП такой подход практикуется, например, в Perl и Python. Первый параметр метода это всегда ссылка на экземпляр класса, который называют self.
Здесь C держит некоторый паритет, но возможности ООП идут гораздо дальше, так что я займусь изучением ООП в C++.
Да, именно изучением, потому что на данный момент я ничего не знаю и буду писать прямо вот на ходу.
От структуры к классу
Создать описание класса не представляет проблем:
Смотрите, я всего-навсего заменил struct на class, и у меня получился класс. Класс!
Очень интересно происходит инстанциация класса, то есть создание объекта этого класса. Во всех языках, которые я помню, если мы заводим переменную под объект какого-либо класса, это ещё не сам объект, а только лишь ссылка на него, которая пока пуста:
MyClass test;
Объект получается, когда мы вызываем конструктор класса new MyClass(). Конструктор класса возвращает ссылку на объект, и эта ссылка присваивается переменной:
test = new MyClass();
В C++ это не так. Когда мы заводим переменную любого типа, будь то хоть простой int, хоть целая структура, нужное количество памяти сразу резервируется на стеке:
struct MyStruct test;
Мы уже можем пользоваться полями структуры struct MyStruct, она уже есть в виде 8-ми байт на стеке. То же самое происходит, когда мы создаём переменную класса MyClass:
MyClass test;
По сути это та же самая структура, так что логично, что память под неё сразу выделилась, объект уже создан и не надо вызывать конструктор.
Тем не менее, конструкторы в C++ существуют и вызываются автоматически при объявлении переменной класса, но чем они в таком случае занимаются? В нашем классе нет конструктора, но его можно добавить:
Конструктор это функция внутри описания класса, у которой нет никакого типа (даже void), и которая называется так же, как класс. Заниматься она может, например, инициализацией свойств класса, да и вообще чем угодно.
Но сейчас ничего работать не будет, потому что в вышеприведённом листинге и свойства класса, и его конструктор по умолчанию считаются приватными, то есть недоступными извне. Чтобы сделать их публичными, добавляем метку public:
И теперь программа работает, и мы можем убедиться, что конструктор вызвался автоматически при объявлении переменной:
Но что такое this в конструкторе? Это указатель на наш объект test, который попадает туда автоматически. Чтобы было нагляднее, расмотрим вариант чистого C. Для эмуляции такого класса нам пришлось бы сделать структуру MyStruct:
И написать для неё функцию-конструктор, которая бы инициализировала свойства структуры:
В функцию передаётся указатель на структуру, которую надо инициализировать. Вот это оно и есть, только в классе нам не надо отдельно вызывать конструктор, потому что он вызовется сам, и не надо передавать в конструктор указатель на объект, потому что он и так неявно передаётся и называется this.
Методы класса
Это обычные наши функции, которые объявляются внутри класса:
В данном примере я добавил методы setX() и getX(), которые устанавливают либо возвращают значение свойства x. Дополнительно я вынес свойства x, y из раздела public, и они стали приватными. Хотя приватное это всё, что не в public, для большей ясности можно использовать метку private. Но когда реально нужна метка private? Когда после публичного раздела надо опять расположить приватный, хотя я не уверен, что это будет хорошей практикой:
Теперь доступ к свойству x возможен только через методы:
Есть также раздел protected, но это уже стандартная ООП-тема и поэтому ничего изучать не надо, всё уже изучено. Оно не понадобится до тех пор, пока мы не перейдём к наследованию.
Обратим внимание на метод setX():
Здесь передаётся параметр с именем x, который присваивается свойству класса также с именем x. Это разные x: один из них просто x, а другой this->x. Смотрим, что будет, если написать x без this:
Параметр x я переименовал в num, а вместо this->x написал x. Результат получился тот же самый. То есть мы можем опускать this->, если в классе есть свойство x. Оно подставится по умолчанию. А теперь настоящая битва двух ёкодзун:
Что мы здесь реально получим? Параметр x = x, или this->x = this->x? Вообще без разницы – это выражение ничего не меняет. Но на самом деле this->x там не участвует, так как перекрывается локальным параметром x. Так что одной бесполезной загадкой меньше.
Вообще практика опускания this встречается, кажется, в C# и, кажется, в Java, и точно была в уже мёртвом ActionScript 3. Не считаю её хорошей и сам не пользуюсь именно потому, что становится сложно следить, какая переменная из какого контекста берётся.
Вот у меня уже есть ООП-инструментарий, который легко покроет нужды того же проекта GUI, но конечно, изучение продолжится в следующих выпусках.
Читайте дальше: