Управление ресурсами
Одна из наиболее серьезных проблем, требующих решения при проектировании интерфейса библиотеки (а также класса или пакета), – это управление ресурсами, которыми библиотека распоряжается самостоятельно или совместно с вызывающим ее окружением. Наиболее важным из таких ресурсов является память: кто должен ее выделять и высвобождать? Кроме того, среди других ресурсов есть открытые файлы, а также переменные, значения которых представляют общий интерес. Грубо говоря, проблемы с ресурсами можно разделить на инициализацию, поддержание заданного состояния, совместное использование и копирование, а также высвобождение.
В прототипе нашего пакета CSV для задания начальных значений указателей, счетчиков и прочих подобных вещей применялась статическая инициализация. Однако подобный подход довольно ограничен: мы не можем вернуть библиотеку в начальное состояние после того, как были вызваны какие-либо функции этой библиотеки. Альтернативный способ инициализации – создание отдельной специальной функции, которая бы устанавливала все внутренние переменные в корректные начальные значения. При таком подходе возврат в стартовое состояние возможен в любой момент, даже после вызова функций библиотеки, однако пользователь должен будет сам вызывать эту функцию явным образом. Для этой цели функция reset из второй версии библиотеки могла бы быть сделана видимой (то есть public).
В C++ и Java для инициализации данных внутри класса используются конструкторы. Должным образом определенные конструкторы дают нам гарантию, что все данные класса инициализированы и способа создать неинициализированный объект не существует. Набор конструкторов может поддерживать различные виды инициализации. Так, мы могли бы снабдить Csv конструктором, получающим имя файла, или конструктором, получающим входной поток.
А как насчет копирования информации, обрабатываемой библиотекой, – такой, как вводимые строки и поля? Наша С-программа csvgetline предоставляет прямой доступ к вводимым данным (строкам и полям), возвращая указатели на них. У такого свободного доступа существует ряд недостатков. Пользователь может перезаписать память, так что информация окажется некорректной. Например, выражение вроде:
strcpy(csvfield(1), csvfield(2));
Может в целом ряде случаев сработать некорректно, – скорее всего, перезаписав начало второго поля, если оно окажется длиннее первого. Пользователь библиотеки должен сделать копию всей информации, которую нужно будет сохранить после очередного вызова csvgetline. Так, после выполнения вот такого фрагмента кода, указатель вполне может оказаться неверным, если второй вызов csvgetline приведет к новому выделению памяти для буфера строк:
Версия на C++ безопаснее, поскольку строки в ней являются всего лишь копиями, которые можно менять как заблагорассудится.
Java использует ссылки для обращения к объектам, то есть ко всему, кроме базовых типов вроде int. Это более эффективно, чем создание копий, однако пользователь может быть введен в заблуждение, считая, что ссылка является копией; ошибка подобного рода имела место в ранней Java-версии программы markov. Надо сказать, что данная проблема является вечным источником ошибок при работе со строками С. Не стоит забывать, что при необходимости создания копии методы клонирования позволяют вам сделать и это.
Обратной стороной инициализации или конструирования чего-либо, является его финализация (finalization), или деструкция, – то есть очистка и высвобождение ресурсов после того, как они больше не нужны. Особенно важно высвобождение памяти. Очевидно, что программе, которая не высвобождает неиспользуемую память, этой самой памяти в какой-то момент не хватит. Как ни странно, большая часть современных программ страдает этим недостатком. Схожая проблема возникает и в ситуации, когда приходит время закрывать открытые файлы: если данные были буферизованы, этот буфер нередко надо уничтожить (а память, занимаемую им, очистить).
Для функций стандартной библиотеки С высвобождение происходит автоматически после нормального окончания работы программы, все остальные случаи должны обрабатываться программой. В С и C++ стандартная функция atexit предоставляет способ получить управление непосредственно перед тем, как программа будет завершена нормально; создателям интерфейсов не стоит пренебрегать такой возможностью для высвобождения ресурсов.