В предыдущей статье я немного поговорил о том, чем различаются понятия "точка входа" и "начало выполнения программы". Так же там были приведены примеры кода, которые способны убить или подвесить программу до вызова функции main. Разумеется, вопросы копания кривыми лапами в коде загрузчика не рассматриваются. Мы говорим только о коде, который пишется в рамках типичных задач. В статье приводились примеры, характерные для процедурного подхода, которые одинаково применимы к программам написанным и на чистом C, и на C++.
В данной статье мы поговорим о специфичных для ООП ловушках, которые могут убить программу на самом начальном этапе. Причем ловушки, специфичные для C++ более коварны, так как менее очевидны.
При использовании процедурного подхода, код, который будет использоваться для инициализации глобальных переменных виден явно. Соответственно, косяк, допущенный в этом коде, будет заметен. В случае C++ мы имеем дело с кодом, который может быть вызван неявно. Как вы уже догадались - речь пойдет о классах. Достаточно написать ключевое слово 'class' и у нас уже автоматически появится накая функция, которую мы возможно и не писали. Разумеется. речь идет о конструкторе. А в современных реалиях - даже о двух конструкторах, которые компилятор вполне может добавить сам, без всяких просьб с Вашей стороны. Это конструктор по умолчанию, и конструктор копирования. Если программист сам их не определял, и естественно - не писал для них код, забыть про них достаточно просто.
Приведем первый, самый простой пример. Мы хотим определить некую строковую константу. Часто это делается примерно так:
const std::string MY_CONST="abc";
И нет в этом ничего плохого, или не правильного. Более того, применение именно класса в таком случае полностью оправдано, так как может позволить провести сравнение строк более корректно. Но, надо понимать, что в отличие от аналогичной по смыслу конструкции
const char MY_CONST[]="abc";
при использовании std::string произойдет вызов нескольких функций этого класса, хотя в явном виде мы их и не вызывали.
Разумеется, библиотека stl написана вполне хорошо, и проверена веками. Особых сюрпризов от нее ждать не стоит. Но ведь похожим образом может быть использован и какой-либо самописный класс.
Теперь перейдем к значительно более сложному примеру. Поговорим о том, как борьба с глобальными переменными в программе на C++ приводит к появлению глобальной переменной. Звучит на первый взгляд странно. Так и хочется закричать - "Какие глобальные переменные?! Это же зло и измышления врага рода человеческого! Да еще и в ООП, где все так ладно, стройно, и красиво!!!" А вот откуда эти глобальные переменные (в оптимальном случае - только одна) берутся...
И вот сидит разработчик, и размышляет, как бы ему эти глобальные переменные убрать. При этом - сохранив всю информацию и возможность доступа к ней из любой точки программы. Но так, чтобы менять данные, как бог на душу положит никто не мог. И вспоминает он про такое волшебное слово ООП, как "инкапсуляция". И появляется гениальное решение: Написать класс Application, обеспечить его синглтонность, а все, что было глобальными переменными, сделаем полями этого класса. И пусть он сам разбирается, кто к этим полям какой доступ получит. Идея - супер! Применяется очень часто. Кстати. такой подход использует даже Microsoft в своей библиотеке MFC. Достаточно посмотреть сгенерированный Wizard код, чтобы обнаружить класс, производный от CWinApp.
Прием действительно хороший. Он вполне работоспособен и даже изящен. Но, у него есть одно 'но', о котором надо помнить. Мы ведь хотим, чтобы этот класс был доступен из любого места программы, чтобы каждый мог к нему обратиться за нужной информацией. А значит, объект этого класса должен иметь глобальную видимость. А как это обеспечить? Можно создать экземпляр такого класса в функции main. Но тогда указатель или ссылку на созданный экземпляр придется передавать всем как параметр. А это, естественно, не есть гуд. И после всех размышлений оказывается, что единственный приемлимый путь - создать соответствующую глобальную переменную. И появляется что-то типа такого:
CMySuperApplication theApp;
Именно так, кстати и поступает Microsoft в библиотеке MFC. А объявление глобальной переменной типа class автоматом приводит именно в этом месте к чему? Правильно - к вызову конструктора. То есть, конструктор будет вызван ДО официальной точки входа в программу (функции main).
Само по себе это не страшно. Но требует аккуратности в проектировании. Код дефолтного конструктора должен учитывать этот факт и не полагаться на то, что к моменту вызова уже все проинициализировано и доступно. Точно также, он категорически не должен пытаться делать слишком много. По сути - единственная его задача - инициализировать поля класса дефолтными значениями, в качестве которых не может использоваться ничего, сложнее констант.
Ведь чем сложнее инициализация, тем выше вероятность ошибки. При этом - не обязательно ошибки разработчика. Например - захотели прочитать файл конфигурации, а злобный юзверь решил почистить место и этот файл удалил...
Проблема в том, что практически любая ошибка в коде, который вызывается до функции main приводит к тому, что программа даже не сможет нормально начаться. А ситуации, когда ПО валится даже до входа в main скверно действует на неокрепшие программерские умы.
В общем - давайте не будем забывать, что как C, так и C++ - мощнейший инструмент. Но как всякое мощное оружие он требует осторожного обращения, чтобы не прострелить себе ногу