В языке C не существует структурированного пространства имен. Спрятать глобальную переменную или функцию можно только при помощи ключевого слова static в пределах одного модуля. Однако в C++ ситуация изменилась, внутри определения класса можно объявлять подтипы, функции (методы) и данные. Классы имеют разграничение на права доступа - private, public и protected. public - доступ открыт для всех. private - доступ открыт только для friend'ов и членов самого класса. protected - доступ открыт для friend'ов, членов самого класса и членов всех классов-потомков. Такой подход позволяет создать пользовательский интерфейс классу и закрыть внутренние данные, другими словами в C++ возможна инкапсуляция данных. Это позволяет структурировать пространство имен, сократить число коллизий, увеличить читабельность и гибкость кода. Приведу пример определения подтипов:
... class AVLTree { ... typedef (*Callback)(void *param1, void *data); enum NodeState { State1, State2, State3 }; ... }; ...В C++ не стоит делать так:
... typedef (*AVLCallback)(void *param1, void *data); enum AVLNodeState { State1, State2, State3 }; class AVLTree { ... }; ...Во втором случае State1, State2, State3 повлияют на глобальное пространство имен. Еще хуже в случае с использованием препроцессора:
... #define State1 1 #define State2 2 #define State3 3 ...В случае с использованием препроцессора при отладке программы будет невозможно увидеть имя типа и значение переменной в текстовом виде . Также во втором и третьем случае легко получить коллизию по именам, причем, если во втором компилятор укажет ошибку корректно, то в третьем это не обязательно. Представьте себе ситуацию, что в одном заголовке было глобальное описание StateX define'ами, а в другом enum'ом. Такая коллизия, например, имеется между библиотеками Qt и Xlib. Имя "GrayScale" в Qt описано по всем правилам внутри класса, а в заголовках Xlib "GrayScale" это глобальный define. enum является частью грамматики компилятора, поэтому идентификаторы неанонимного enum'а попадают в отладочную информацию. define заменяются на цифровые значения на стадии препроцессинга.
Вывод: надо заворачивать все свободные имена из глобального пространства имен в классы. Если есть совсем несвязываемая информация - используйте класс Global. Например:
... int foo(int x); int bar(int y); ...привести к виду:
... class Global { public: static int foo(int x); static int bar(int y); }; ...
Для внешних библиотек так сделать не получится. Если коллизий между используемыми библиотеками не возникает, то не стоит даже тратить время на изменение имен в этих библиотеках. Однако если возникают коллизии по именам, самый простой обходной путь, при наличии исходных текстов библиотек - namespace. Namespace достаточно новое ключевое слово, и еще не поддерживается всеми компиляторами C++. Если не поддерживается namespace или библиотека имеется только в бинарном виде, можно добавить префиксы к именам функций.
Преимущество инкапсулированных объектов
Первый пример класса Point
Второй пример класса Point
Заметьте, что при изменении формата хранения данных, пользовательский
интерфейс совсем не изменился. Размер класса point в первом случае 8 байт,
во втором 4 байта. А 4-х байтный класс это уже немалое ускорение на
32-битных платформах.
C++ поддерживает полиморфные функции. На первый взгляд вместо:
void getRGB(int index, int &r, int &g, int &b); void getRGB(int index, unsigned char *vec);можно написать на C:
void getRGB3(int index, int *r, int *g, int *b); void getRGBv(int index, unsigned char *vec);Но на практике намного удобнее первый вариант, поскольку не надо тратить время на придумывание запутанных венгерских нотаций - компилятор сам знает, где какие типы. Полиморфизмом нельзя злоупотреблять. Все полиморфные функции должны выполнять одно и то же действие - в случае getRGB - получить цвет из системной палитры, иначе можно легко запутать разработчиков.
У многих сложилось мнение, что C++ программа будет намного больше в исполняемом виде, чем аналогичная, но написанная на C и будет медленнее работать. Это неверно. Размер исполняемого файла в основном растет из-за информации исключений (Exceptions Frame Information), RTTI (Run-Time Type Information) и отладочной информации.
Исключения - достаточно большие накладные расходы на каждую функцию программы. Сами-же исключения не предоставляют значительного выигрыша при обработке ошибок. По производительности - исключения гораздо медленнее дерева условий. По совместимости - исключения еще плохо поддерживаются многими компиляторами. Думаю пока стоит отказаться от использования исключений.
С RTTI ситуация полностью аналогичная исключениям. Работает неэффективно, и не оправдывает затраты. Не поддерживается многими компиляторами. Чтобы не использовать RTTI, можно добавить дополнительный член класса - тип объекта. Это обычно целое число, равное сгенерированному уникальному номеру. В некоторых случаях удобнее использовать строковую константу или виртуальную функцию classOf(), которая возвращает тип объекта. Самый быстрый вариант - использовать целый идентификатор типа. Такой подход дополнительно позволяет ограничить количество классов, несущих информацию о своем типе.
Отладочная информация. C++ достаточно сложен и генерирует огромные по сравнению с C, объемы debug info. Об этом волноваться не стоит, в релизе ее все равно не будет.
Виртуальные методы. Для разъяснения привожу программу на C++ и полный аналог
на C. Легко можно понять, какие в действительности идут накладные расходы.
На C++:
class X { virtual void f(); }; .... X *p; .... p->f(); ....На C:
struct X_virtual_table { void (*f)(struct X *this); }; struct X { struct S_virtual_table *_vt; }; .... struct X *p; .... (*p->_vt->f)(p); ....
В C в заголовках находились только объявления переменных, функций и определения типов. В C++ ситуация усложнилась, в заголовках оказались инлайновые функции и шаблоны. Плюс дополнительная документация, и получаем, что объемы C++ заголовков намного больше C-шных. И это приводит к проблеме. Когда в модуле включается слишком много файлов-заголовков, скорость компиляции сильно падает.
При разработке компонент, обычно будет происходить так, что одна компонента зависит от другой (через наследование, композицию, использование каких-либо методов, данных, типов ...). Могут возникать циклические зависимости - они разрешаются специальными приемами, о которых будет сказано позднее. По примеру простого заголовка point.h видно, что будет в реальном проекте, где средний размер заголовка не 1kb, а 10kb, и их не менее 50 штук. В C++ появляется новая задача, которая, естественно, тоже вызвала отвращение у программистов - минимизация зависимостей между заголовками. Хорошо, что C++ позволяет это делать без заметных накладных расходов. Как это сделать будет рассказано далее.
Валерий Щедрин
<valery@forthpick.kiev.ua>