Всем привет. Продолжаю серию статей про любимый язык программирования.
На этот раз коснёмся вечного вопроса "Java vs C++" в масштабах промышленного программирования.
Хотя некоторые вещи касаются вполне и выбора языка на ранних стадиях обучения, в том числе некоторые вещи применимы и для выбора языка для олимпиад. Я собственно так и выбрал Java :)
Самым авторитетным для меня источником стала статья моего тренера - Федора Владимировича Меньшикова. Эта статья как раз была и написана, когда встал вопрос "Что изучать после Pascal" :)
PS: Все копирайты соблюдены. Статья публикуется с разрешения автора.
Вы интересовались, какой язык лучше изучать - C++ или Ява. Я написал на
каждом из них десятки тысяч строк кода, поэтому попробую расписать
достоинства и недостатки каждого.
Небольшой обзор разницы идеологий языков далее.
Рассмотрим вопрос применимости языков для промышленного программирования -
создания программ в десятки и сотни тысяч строк кода.
Языки будем сравнивать по двум критериям:
1. Как известно, основное требование к программе - корректная работа.
Поэтому основным критерием сравнения будет "насколько язык располагает к
совершению ошибок."
2. Вторым фактором будет "насколько удобно писать на языке".
Критерий 1. Насколько язык располагает к совершению ошибок.
Ошибка 1.1. Обращение к чему-то несуществующему.
Когда происходит обращение к неизвестно чему, то и программа работает
неизвестно как. Скажем, в 255 случаях из 256 она может работать, а в 1
случае может сбоить. А ещё это "неизвестно что" может оказаться данными
другого модуля программы.
Ошибка 1.1.1. Обращение к несуществующему элементу массива.
Язык Ява, как и C#, гарантирует, что при индексации массива нельзя
обратиться к несуществующему элементу массива. Если обращение к
несуществующему элементу произошло, генерируется исключение.
Язык C++ унаследовал классические массивы от языка Си, а в языке Си
проверки при индексации массива не только отсутствуют, но и вообще
невозможны, поскольку в процедуры, например, не передаётся длина
обрабатываемого массива, только ссылка на его начало.
Однако C++ не заставляет пользоваться классическими массивами. На C++ можно
написать шаблон класса, имитирующего работу с массивом с проверкой
диапазонов. Всё различие будет заключаться в описании переменной:
StaticArray<int, 5> a;
вместо
int a[5];
а работа с таким "массивом" может происходить ровно тем же образом, как
работа с классическим массивом.
Я для себя такой шаблон написал, и подобные шаблоны используются в крупных
проектах, однако для чего-нибудь маленького вроде решения олимпиадной
задачи такой способ сделать язык безопасным не проходит - на написание
StaticArray никто не захочет тратить такое драгоценное на олимпиаде время.
Ошибка 1.1.2. Обращение к уже несуществующему объекту через указатель.
Язык Ява, как и C#, гарантирует, что объект не удаляется, пока к нему можно
обратиться через указатель. Поэтому обратиться к уже удалённому объекту
невозможно.
В языке C++ программист сам отвечает за удаление объектов, так что никто
ему не мешает создать два указателя на объект, через один указатель этот
объект удалить, а через второй потом поработать с тем местом, где объект лежал.
Однако C++ не заставляет пользоваться классическими указателями. В C++
можно написать шаблон класса, имитирующего указатель с проверкой отсутствия
обращений к объекту после его удаления. Ещё большую ценность представляет
класс так называемых "умных" указателей, которые сами удаляют объекты, на
которые указывают, как только исчезает последняя ссылка. Как и в случае с
массивами, всё различие в использовании такого шаблона будет в описании
переменной:
Ptr<MyClass> p;
вместо
MyClass *p;
Я в большом проекте использую такой шаблон, но для чего-нибудь маленького
вроде олимпиадных задач писать такой шаблон ну очень накладно.
Ошибка 1.1.3. Удаление объекта изнутри его самогО.
Как я уже упоминал, в Яве программист не может сам удалить объект, поэтому
проблемы нет. А в C++ программист сам отвечает за удаление объекта. Особый
случай - когда прямо или косвенно удаляется объект, в котором находится
исполнение. Проблема заключается в том, что после удаления объекта все его
поля являются недействительными, и после удаления самого себя нужно строго
выйти из процедуры, ни к чему больше не притрагиваясь. Как правило это не
так просто, если не знаешь, что ты себя удалил. Решением является в том
объекте, который провоцирует удаление, проверять, не в удаляемом ли объекте
сейчас находится поток управления. Решение работает, но такие проверки -
это, конечно, дополнительные усилия со стороны программиста.
Ошибка 1.2. Использование неинициализированных переменных.
Как в случае с обращением к неизвестно чему (1.1.1) можно получить
неизвестно какой результат, так и в случае обращения к переменной, в
которую значение не было занесено заранее, ничего хорошего ждать не приходится.
В Яве, как и в C#, гарантируется, что все поля объекта при создании
зануляются. Если это int - значение 0, если boolean - false, если объект -
null.
В C++ зануления нет, и это очень опасно. Реально сталкивался с проблемой
невоспроизводимых ошибок. При одном состоянии памяти программа переходит в
состояние ошибки, при другом состоянии памяти замечательно работает. Ошибка
оказалась в отсутствии инициализации поля в одном (!) из двух конструкторов.
Однако в C++ есть возможность переопределить операцию new, так что она
будет не только выделять память, но и занулять место под динамической
переменной. В моём проекте на C++ все классы унаследованы от класса с таким
переопределённым оператором new.
Как и в случае с прочими упоминавшимися возможностями, написание такого
переопределения оператора new дело не совсем тривиальное, и в маленьких
проектах вроде олимпиадных задач нет возможности тратить на это время.
Ошибка 1.3. Отсутствие удаления/закрытия ресурса.
Если по предыдущим пунктам могло сложиться впечатление, что Ява намного
лучше C++, то этот пункт будет скорее в пользу C++.
Как выглядят внутренности обычной процедуры обработки файла:
<открыть файл>
<читать и обрабатывать данные>
<закрыть файл>
Пока последовательность действий линейна, всё хорошо, открытый файл будет
закрыт. Но что если в ходе обработки файла случится исключение или будет
желание выйти через return из функции? Будет ли файл корректно закрыт?
В Яве для гарантированного закрытия таких ресурсов приходится операторы
<читать и обрабатывать данные> заключать в блок try-finally. На самом деле
не очень эстетично выглядит, а уж когда несколько переменных таким образом
гарантированно закрыть надо - тут уже синтаксис получается туши свет. И что
самое противное - ничто не обязывает программиста писать блок finally, так
что ничто не поддерживает систему гарантированного освобождения ресурсов.
В C++ в смысле освобождения ресурсов всё, наоборот, хорошо. Язык
гарантирует вызов деструкторов всех локальных объектов при выходе из
процедуры любым способом - и через return, и через исключение. Поэтому всё,
что нужно - это обернуть ресурс в объект, и в конструкторе объекта
прописать открытие ресурса, а в деструкторе - закрытие. И в тексте основной
процедуры не появляется никаких finally, и где бы этот объект не
использовался, везде при любых обстоятельствах освобождение ресурса
гарантируется.
Ошибка 1.4. Неожиданные для программиста свойства языка.
Язык C++ имеет ряд особенностей, из-за которых программы могут работать не
так, как ожидает программист, а чтобы они работали как надо нужно что-то
неочевидное изменить. Такие особенности просто нужно знать и учитывать.
Проблема в том, что люди, начинающие писать на C++, этих особенностей не
знают, и узнаЮт об этих граблях только через некоторое время. Существуют
даже книжки о таких граблях. Мне нравится книга Скотта Мейерса (он же
Майерс, он же Мэйерс) "Эффективное использование C++".
В Яве таких особенностей практически нет. С одним похожим случаем я
столкнулся только при использовании одного класса стандартной библиотеки, а
в C++ таких особенностей десяток в самОм языке.
Критерий 2. Насколько удобно писать на языке.
Возможность 2.1. Циклический импорт.
Иногда два класса зависят друг от друга. В компиляторе может быть, скажем,
такая ситуация: в выражении (скажем, при приведении к какому-то типу) может
встречаться тип, а в типе (скажем, в описании диапазона индексов массива)
может встречаться выражение. Первый должен вызвать методы второго, а второй
- методы первого. Возникает вопрос, кто кого должен импортировать.
В Яве этот вопрос решается просто - циклический импорт разрешён, классы
друг друга увидят. В C++ циклический импорт запрещён, кого-то нужно
объявлять первым. Решение на C++, конечно же, существует и заключается в
наследовании от интерфейсного класса с чисто виртуальными функциями, но как
заумно это звучит, так же плохо это и выглядит, хотя замечательно работает.
Возможность 2.2. Библиотеки шаблонов структур данных.
Как C++, так и Ява, в отличие от Паскаля, оба содержат библиотеки с
шаблонами вроде std::vector, std::map и т.п. Например, std::map - это
аналог массива, только индексы не обязаны быть целыми числами, достаточно
уметь сравнивать две переменные типа ключа на меньше. Эффективная
реализация подобных структур данных для каждого конкретного случая
потребовала бы от программиста титанических усилий - а тут всё уже готово,
бери и пользуйся.
К сожалению, стандартные контейнеры C++ поступают в духе Си и не
обеспечивают никаких проверок диапазонов и т.п. К счастью, существует
бесплатно распространяемая библиотека STLPort, где можно включить режим
проверок. Но её нужно скачивать и устанавливать, с известными компиляторами
она не поставляется.
А ещё очень хочется оторвать руки тому, кто в C++ у std::vector (массив с
возможностью роста) значение функции size() сделал беззнаковым числом.
После этого цикл
for (int i = 0; i < a.size(); i++)
приходится записывать или как
for (int i = 0; i < (int)a.size(); i++)
или как
for (std::vector<int>::size_type i = 0; i < a.size(); i++)
или получать предупреждение компилятора о сравнении числа со знаком и числа
без знака.
Возможность 2.3. Исключения.
Как C++, так и Ява, в отличие от Оберона, оба поддерживают исключения.
Что должен сделать компилятор при обнаружении ошибки? Сообщить о ней - и на
этом в принципе миссия завершена. Традиционный подход в простеньком
компиляторе - после выдачи ошибки просто завершить программу с помощью
средств вроде exit() C++, System.exit() Явы или halt() Паскаля. А что если
компилятор встроен в редактор? Всё-то он не должен выносить, должна
завершиться только стадия компиляции. Вот тут как раз исключения и могут
помочь. Они позволяют из процедуры выдачи ошибки перескочить на самый
верхний уровень без дополнительного кода в процедурах синтаксического
анализа. Помнится, я писал компилятор на Обероне, так там раз 200 в
синтаксическом анализаторе встречалась строка IF error THEN EXIT; END; Вот
какие ужасы могут твориться в языке без поддержки исключений.
Вот краткий обзор возможностей языков. В плане обеспечения надёжности для
больших проектов C++ и Ява практически равны. Для надёжных набольших
проектов Ява, похоже, подходит лучше. А для тех, кто считает, что пишет
только правильные программы, в которых нет ошибок и которые не нуждаются в
отладке, безусловно, подходит "чистый" C++. :-)
PS:
Прошу не оценивать данную статью, ибо за её качество ответственность несёт сам автор.
Я не имею никакого права получать за неё + или - ,т.к. она не моя.
Лично мне она понравилась, считаю её весьма ценной и выложил исключительно для того, чтобы народ смог оценить кое-какие грани рассматриваемых языков и сделать нужный выбор.