Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Теллес М. - Borland C++ Builder. Библиотека программиста - 1998

.pdf
Скачиваний:
767
Добавлен:
13.08.2013
Размер:
4.35 Mб
Скачать

Borland C++ Builder (+CD). Библиотека программиста 131

//Определяем используемый объект множество std::set<std::string, less<std::string> > setString;

//В цикле пытаемся добавить элементы,

//введенные пользователем int bDone = FALSE;

while (!bDone )

{

//Получаем информацию от пользователя printf("Введите значение для поиска

(DONE для выхода):");

//Храним результат в строке

char szBuffer[256]; gets(szBuffer);

// Проверяем, не пора ли выходить if (!stricmp(szBuffer, "done") )

{

bDone = TRUE; continue;

}

// Пытаемся добавить элемент в множество int bOkToAdd = setString.insert (szBuffer ); if (bOkToAdd == FALSE)

{

printf("Не добавляется:

такой элемент уже есть!\n");

}

}

//Когда ввод завершен, выводим все элементы std::set<std::string, std::less<std::string> >::

iterator setIterator; setIterator = setString.begin();

//Проходим по всем значениям

while (setIterator != setString.end() )

{

printf("Элемент: %s\n", (*setIterator).c_str() );

}

return 0;

}

В данном примере есть несколько интересных моментов. Во-первых, здесь не применяется конструкция using namespace std, которую использовали вплоть до этой точки при работе с STL. Для этого есть причины: во-первых, выражение using применять не обязательно, это просто более удобный способ распознавания областей видимости. Вам действительно нужно научиться работать с областями namespace, которые мы рассмотрим подробнее ниже.

Borland C++ Builder (+CD). Библиотека программиста 132

Вторым интересным моментом является то, что множество не более чем обычная структура данных. Вы можете добавлять в него данные и определять итератор (iterator) для него. Это важно, так как слишком много программистов, судя по всему, считают, что множество можно использовать только для проверки, есть ли в нем некоторый элемент. Класс iterator работает с множеством точно также, как и с любой другой структурой в классах STL.

И, наконец, последний момент заключается в том, что мы использовали саму структуру данных для проверки корректности ввода. В основном классы, определяющие структуры данных ( list, vector и т.д.) не имеют никаких встроенных проверок. Пользователь может набрать одну и ту же строку два раза, и классы list и vector радостно поместят оба экземпляра в конец списка (массива). В классах же set и map (который мы рассматривали в предыдущем примере) хранятся только уникальные значения. Также как и у класса map, у класса set есть кровный брат класс multiset (мультимножество), в котором можно хранить несколько одинаковых элементов. Но такое чудо используется крайне редко, так как для подобных вещей лучше подходят другие структуры данных, например список ( list).

Небольшое отступление: С++ и именованные области видимости

Мы уже рассматривали два способа работы с именованными областями (namespaces) в STL, тем не менее крайне важно для вас понять последующее рассуждение. В C++ namespace — что-то вроде сверхкласса, который содержит вложенные классы. Например, так мы создаем сверхкласс с именем fred:

namespace fred

{

class A

{

}

class B

{

}

}

Код содержит два подкласса, определенные как class A и class B. Область namespace — это просто класс, в котором все поля являются общими (public). Таким образом к классу A внутри fred можно обращаться как к fred::A, а к классу B как fred::B. Почему это важно? Представьте, что у вас в библиотеке находится класс, описанный таким образом (в данном случае мы проигнорируем факт, что ваш менеджер не дал бы вам называть классы просто A и B). Предположим теперь, что вам нужно подключить к проекту внешнюю (third-party) библиотеку. Эта внешняя библиотека поставляется без исходников и содержит в себе следующее описание:

namespace thirdpartylib

{

class A

{

}

class B

{

}

Borland C++ Builder (+CD). Библиотека программиста 133

}

Очевидно, у вас появляются проблемы. Есть два конфликтующих класса A и два класса B, так что вам нужно пройтись по всему вашему исходному коду и поменять эти имена, а также изменить эти имена на новые в классах, которые на них ссылаются, не говоря уже о функциях, которые с ними работают. Ведь так? Слава богу, нет. В этих двух примерах используются различные именованные области (namespaces), так что, к примеру, для обращения к вашему классу A вам придется написать fred::A. Для обращения к классу A из внешней библиотеки нужно использовать выражение thirdpartylib::A. Эти два имени являются различными с точки зрения компилятора, так что все будет работать так, как и задумывалось.

Однако я слышу глухой стон, вырывающийся из вашей груди. Вы решили, что в коде классов A и B вам постоянно придется писать примерно следующие страшные конструкции:

// Конструктор класса A в области fred

fred::A::A(void)

{

}

Слава богу, такие вещи писать необязательно. Вы можете заключить заголовочный и исходный файл в одну и ту же область ( namespace). На заголовочный файл мы уже смотрели, а вот как будет выглядеть исходный файл (в общем-то, он ничем не отличается):

namespace fred

{

// Конструктор класса A в области fred A::A(void)

{

}

}

Заметьте, что вы можете неоднократно открывать и закрывать блок namespace в одном и том же файле. Все эти разрозненные куски будут помещены вместе в одну область. Например, если у вас два класса определены в одном и том же исходном файле, но в разных областях namespace (вряд ли вы станете так делать, но теоретически у вас такая возможность есть), то вы можете написать:

namespace fred // Открываем область fred

{

//Код в области fred

}// Закрываем область fred

namespace george // Открываем область george

{

//Код в области george

}// Закрываем область george

// Еще какой-нибудь код

Есть еще одна деталь насчет именованных областей (namespaces). Если вам нужно использовать класс, определенный в другой области namespace, вы можете подумать что достаточно написать что-нибудь такое:

namespace fred

Borland C++ Builder (+CD). Библиотека программиста 134

{

class george::A; // Импорт класса A из namespace george

class Foo

{

george::A aGeorgeA;

}

}

К сожалению, приведенный выше код не сработает. Хотя в основном к областям namespace можно относиться как к некой надклассовой структуре сверхкласс»), на самом деле эти области таковыми не являются. В один и тот же момент может быть открыта только одна область, и нельзя использовать модификаторы namespace в ссылках на еще не определенные классы ссылки вперед»). Как же вам справиться с такой задачей? Ответ очевиден, если только вдуматься в проблему. Вы закрываете одну область namespace и открываете другую.

namespace fred

{

// Внутренние описания области fred

}

namespace george

{

class A; // Описание класса A,

// который будет определен дальше

}

// Теперь вы можете написать namespace fred

{

class A

{

george::A aGeorgeA;

}

}

Приведенный код будет компилироваться любым стандартным компилятором ANSI C++ (к которым относится и CBuilder) и замечательно работать. Все это приводит нас к следующей дискуссии о том, что такое оператор using и почему его использовать не нужно.

Оператор using

Оператор using в C++ убирает необходимость употреблять имя области namespace в данном блоке кода. Например, если у вас есть несколько классов, определенных в области namespace std, то

выражение

using namespace std;

позволяет вам обращаться к любому классу в этой области без указания модификатора std::. Однако такое изменение отразится также на всех других классах и областях namespace в системе. Предположим, что в нашем прошлом примере областей fred и george вы бы написали:

Borland C++ Builder (+CD). Библиотека программиста 135

using namespace fred; using namespace george;

int func(void)

{

A anAObject;

}

Что в данном случае означает A в функции func? Это из области fred или из области george? Мы и сами этого не знаем, а уж компилятор и подавно. Ситуация еще более осложнится, если класс A описан вне всяких областей namespace. Представьте, что вы написали свой собственный класс list, который делает что-нибудь совершенно другое, чем класс list из STL. Тогда, если использовать выражение using для области namespace std, то напрямую к классу list обратиться будет невозможно. Вам придется по-прежнему использовать оператор разрешения видимости (scope operator, ::), так что весь смысл областей namespace в данном случае потеряется. Так что я советую вам закусить удила и использовать оператор разрешения видимости std:: для использования классов STL в вашем приложении. Оператор using был придуман не для новых разработок, а скорее для поддержки уже существующего кода, работающего с заголовочными файлами, которые позже были помещены в область namespace. Если у вас есть программа, написанная пару лет назад и использующая STL, то вы можете решить проблему с областью namespace std при помощи оператора using. В новых разработках, однако, вам стоит всегда использовать оператор std :: для доступа к вашим классам в области namespace std.

А теперь мы вернемся к нашей дискуссии о классах STL.

Стеки и очереди

Наверное, ни одна структура данных не применяется в компьютерной индустрии так широко, как стек. Операционные системы построены на этой простой структуре данных, и наверное каждый программист неоднократно получал радостное сообщение о переполнении стека в .своей программе.

STL предлагает вам готовую реализацию и стека, и очереди. Стек является как бы «односторонней» очередью, то есть вы можете помещать (push) элементы на верхушку стека и брать (pop) элементы с верхушки стека. С другой стороны, очереди позволяют добавлять элементы в начало списка голова» очереди)и забирать элементы с другого конца хвост» очереди). Стек часто называют структурой вида LIFO (Last In First Out), так как элемент, добавленный последним в стек командой push, будет являться первым, извлеченным из стека командой pop. Соответственно, очередь это структура FIFO (First In First Out): элемент, добавленный в очередь первым, будет первым же из нее и извлечен.

В C++ стек и очередь обычно реализуются в виде связного списка. Многие программисты на заре своего обучения программированию писали такие структуры, так что подробно рассматривать их в данной книге мы не будем. Используя реализацию, предложенную в STL, вы получаете два преимущества перед своим собственным стеком (или очередью), основанным на связном списке. Во-первых, код в STL тщательно проверялся и поэтому скорее всего будет работать без ошибок. Даже если в нем и найдется ошибка, то она будет исправлена, и новая версия библиотеки будет бесплатно распространена и интегрирована в разнообразные среды разработки (включая Borland CBuilder). Во-вторых (что более важно), внутренняя структура данных может измениться в STL, но при этом прототипы функций останутся такими же, так что вы можете спокойно писать программу, использующую стек или очередь, не беспокоясь о том, что в будущем их внутренняя реализация может измениться.

Borland C++ Builder (+CD). Библиотека программиста 136

Давайте рассмотрим, что за методы есть у классов stack (стек) и queue (очередь), а затем немного поговорим об их использовании. В табл. 5.6 приведены основные методы класса stack в STL.

Таблица 5.6 Важные методы класса stack

empty

Указывает, есть ли элементы(FALSE) или нет (TRUE) в стеке size

Возвращает количество элементов в стеке top

Возвращает элемент с верхушки стека, но не удаляет его push

Добавляет элемент на верхушку стека (или очереди) pop

Забирает верхний элемент из стека (очереди) front

Возвращает элемент, находящийся в голове очереди, не удаляя его back

Возвращает элемент, находящийся в хвосте очереди (добавленный последним), не удаляя его operator=

Копирует стек или очередь в объект такого же типа constructor

Позволяет программисту указать тип данных, содержащихся в стеке (очереди) и структуру,

используемую для хранения элементов

Как вы видите из предыдущей таблицы, стек и очередь не являются самостоятельными структурами данных в STL, а строятся на других структурах, таких как список и дек <$FДек (deque) — это внебрачный потомок стека и очереди, то есть структура, в которой можно добавлять элементы как в начало, так и в конец списка, и соответственно забирать их с любой стороны. Дек является классическим примером, когда удобно применять множественное наследование в языке C++. — Примеч. перев.>(deque), которым вполне хватает функциональности для реализации таких вещей.

Одно важное замечание относительно стека и очереди: для этих структур нет понятия итератора. Вы не можете использовать итератор, чтобы ходить по стеку или очереди, вам вместо это нужно использовать методы push и pop для добавления и удаления элементов.

Давайте посмотрим на простенький пример программы, использующей стек.

#include <stdio.h> #include <string.h> #include <stdlib.h>

#include <stack> #include <string> #include <list>

int main(int argc, char **argv)

{

std::stack< std::string, std::list<std::string> > stringStack;

// Запихать все аргументы командной строки в стек

Borland C++ Builder (+CD). Библиотека программиста 137

for (int i=1; i<argc; ++i)

{

stringStack.push(argv[i]);

}

// Теперь вынуть в обратном порядке while (!stringStack.empty() )

{

std::string s = stringStack.top(); printf("%s\n", s.c_str() ); stringStack.pop();

}

return 0;

}

Как видите, программа просто берет набор аргументов командной строки и печатает их в обратном порядке. Так что, если вы запустите программу следующей командой (в командной строке окна MS-DOS в Windows 95 или NT):

stktest Hello world how are you?

разумеется, если вы сохранили проект как stktest, то вы увидите следующий вывод программы:

you? are how world Hello

Заметьте, что процесс удаления элемента из стека состоит из двух шагов. Во-первых, получить элемент, используя метод top класса stack (который возвращает элемент, не удаляя его при этом), а уже затем удалить элемент из стека методом pop. В отличие от удаления, добавление (метод push) происходит за один шаг.

Замечание про двойные угловые скобки: вы, наверное, заметили, что при описании объектов, использующих STL, частенько приходится писать конструкции, в которых попадается подряд несколько угловых скобок (>). Пример объект класса stack, для которого требуется второй параметр, тоже шаблонный класс. В результате вы напишете что-нибудь такое:

std::stack< std::string,

std::list<std::string>> stringStack;

Никогда так не делайте. Скушав такую строку, компилятор пожалуется на целую кучу ошибок и выдаст вам гору имен. Проблема в том, что в языке C (и, соответственно, C++) символ >> является оператором сдвига вправо. Когда у вас получаются странные ошибки в коде с шаблонами, проверьте, что у вас между двумя угловыми скобками поставлены пробелы. Это все говорит об ошибке (по крайней мере, недоработке) в дизайне системы шаблонов в C++, а не об ошибке в компиляторе.

Borland C++ Builder (+CD). Библиотека программиста 138

Объединяя все вместе: Scribble версии 2

Как вы помните, в главе 2 мы написали программу Scribble (каракули), которая являются учебной программой в Visual C++ и занимает добрую сотню страниц кода. Если не помните, ничего страшного в этом нет. Характерные моменты мы разберем в данном примере.

Программа Scribble позволяет рисовать на форме с помощью мыши. Нажав левую кнопку, вы начинаете процесс рисования, так что можно, удерживая левую кнопку нажатой, перемещать мышь по форме, оставляя за собой след. Когда мы оставили программу Scribble, в ней была куча проблем. Первой серьезной проблемой являлось ограничение (связанное с дизайном программы) в 100 точек, участвующих в процессе рисования. Хотя это число можно поменять, но все равно программа останется привязанной к конкретному ограниченному числу точек. Мы собираемся решить эту проблему, используя класс STL vector вместо обычного статического массива точек.

Вторая проблема в программе Scribble не так заметна, но тем не менее она есть. Когда вы рисуете линию в Scribble и отпускаете кнопку мыши, программа перестает следить за перемещением курсора. Если вы еще раз нажмете кнопку, то у вас получится отдельный кусок изображения. Все это здорово, однако проблема проявляется, когда мы перерисовываем форму (в процедуре paint). Если, например, вы нарисовали несколько линий, а затем свернули и вновь раскрыли форму, то отобразится только последняя линия. Это происходит из-за того, что мы останавливаем запись координат, когда отпускается кнопка мыши, и запускаем запись по новой (с начала), когда кнопка нажимается. В данном примере мы исправим Scribble раз и навсегда, так что все будет работать правильно.

ЗАМЕЧАНИЕ

Полный исходный код данного примера находится на прилагаемом компакт-диске в каталоге Chapter5\ScribbleSTL. Если хотите, можете не набирать код, а просто скопировать его с компакт- диска.

Вместо того, чтобы все перестроить в дизайне проекта, мы будем исправлять старый проект, добавляя вещи, которые нам нужны. Если вам интересно, как это все было построено изначально, просмотрите главу 2.

Первый шаг заменить существующие массивы точек на массив типа vector, содержащий точки. Нам также хотелось бы изменить процесс, чтобы запоминать перемещения мыши, а также момент, когда отпускается кнопка мыши, так что мы собираемся хранить информацию в объекте. Этот новый класс C++ будет содержать координаты X и Y всех точек, а также флаг, указывающий, было ли это простым перемещением или рисованием линии. Режим move будет означать, что кнопка мыши отпущена в данный момент, а режим draw — что кнопка нажата.

Для этого нам нужно сделать несколько вещей. Во-первых, нужно определить новый класс, содержащий описанную выше информацию. Во-вторых, нам нужно изменить класс родительского окна MDI, чтобы можно было использовать наш новый класс в массиве (vector) из библиотеки STL и получать из этого массива необходимую информацию.

Исправляем родительское окно

Оказывается, основные исправления, относящиеся к родительскому окну, приходятся на заголовочный файл (MainForm.h). Сначала давайте добавим код, описывающий класс для хранения точек. Просмотрите код, а затем мы обсудим, что в нем происходит:

const int MoveMode = 1;

Borland C++ Builder (+CD). Библиотека программиста 139

const int DrawMode = 2;

class TScribblePoint

{

int FnX; int FnY;

int FnMode; public:

TScribblePoint(void)

{

FnX = 0;

FnY = 0;

FnMode = MoveMode;

}

TScribblePoint (int nMode, int nX, int nY )

{

FnX = nX;

FnY = nY; FnMode = nMode;

}

int GetMode(void)

{

return FnMode;

}

int GetX(void)

{

return FnX;

}

int GetY(void)

{

return FnY;

}

};

Как видите, класс TScribblePoint довольно прост. В нем содержатся три переменных члена класса для координат X и Y точки и режима, в котором происходит событие, связанное с этой точкой. Кроме класса, мы описали две константы, обозначающие два типа режима.

Кроме переменных, класс TScribblePoint содержит функции, дающие доступ к данным, что является хорошим тоном при программировании на C++. Вместо того, чтобы предоставлять конечному пользователю доступ к внутренней структуре класса, вы контролируете этот доступ и решаете, можно ли записывать или считывать данные. Этот момент является очень важным и будет рассмотрен ниже, во время дискуссии об определении свойств компонентов. Если вы уже

привыкли работать с внутренними данными как с частной собственностью и предоставлять пользователю доступ только к необходимым данным, то вам будет гораздо легче научиться писать компоненты и их свойства.

Итак, небольшая лекция закончена, пора переходить ко второму шагу: модифицировать класс главной формы с использованием нашего нового класса TScribblePoint. Вот изменения, которые вам необходимо внести в класс, чтобы он поддерживал наш новый объект:

Borland C++ Builder (+CD). Библиотека программиста 140

class TForm2 : public TForm

{

__published: // IDE-managed TMenuItem *File1; TMenuItem *New1; TMenuItem *Exit1; TMenuItem *Update1; TMenuItem *AllWindows1;

void __fastcall New1Click(TObject *Sender);

void __fastcall AllWindows1Click(TObject *Sender); void __fastcall Exit1Click(TObject *sender);

private: // User declarations std::vector<TScribblePoint> FvPoints;

public: // User declarations

__fastcall TForm2(TComponent *Owner); void ClearPoints(void)

{

FvPoints.erase (FvPoints.begin(), FvPoints.end() );

}

void AddPoint(int nMode, int X, int Y)

{

TScribblePoint point(nMode, X, Y);

FvPoint.insert (FvPoints.end(), point );

}

int NumberOfPoints(void)

{

return FvPoints.size();

}

void GetPoint (int Index, int& X, int& Y, int& Mode )

{

X = FvPoints[Index].GetX();

Y = FvPoints[Index].GetY();

Mode = FvPoints[Index].GetMode();

}

};

Эти изменения позволят главной форме работать с вектором (массивом) точек. Заметьте, что за исключением добавления параметра Mode в методе GetPoint (что нужно для реализации новых возможностей) никакие интерфейсы методов не изменились. Это еще один важный момент в объектно-ориентированном программировании. Если вы корректно напишите методы доступа ( accessor methods) к данным, то вы можете менять внутреннюю структуру данных, о чем клиентские программы, использующие ваш объект, даже не будут знать. Если вы прослушали несколько курсов по программированию, то вы вероятно постоянно слышали об этом, но ни разу не видели реальной ситуации, где это имеет значение. Ну что ж, теперь вы знаете, где и зачем это применяется.

В данном случае проявляется преимущество использования языка C++, а не C или Visual Basic. Если бы, например, вы использовали Visual Basic, и использовали переменны для хранения данных, как мы храним точки, то вам пришлось бы найти все места, где используются эти переменные, и поменять их. На самом деле, конечно, такой проблемы у вас бы не появилось, если бы вы действительно программировали на Visual Basic, потому что STL поддерживается только в языке C++. В данном конкретном случае это также является довольно большим преимуществом

Соседние файлы в предмете Программирование на C++