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

Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Перегрузка функций и операторов 81
COMP COMP::operator-(COMP r)
{
COMP tmp;
tmp.a = a-r.a; tmp.b = b-r.b;
return tmp;
}
COMP COMP::operator*(COMP r)
{
COMP tmp;
tmp.a = a*r.a-b*r.b; tmp.b = a*r.b+b*r.a;
return tmp;
}
Конструктор с параметрами выполняет присваивания вещественной и мнимой частям создаваемого комплексного числа значений, указанных в ка­честве аргументов конструктора. Конструктор без параметров обнуляет веще­ственную и мнимую часть создаваемого числа.
Перегрузку рассмотрим на примере операции сложения. Как известно, при сложении двух комплексных чисел попарно складываются их вещест­венные и мнимые части. Внутри тела функции-оператора организуется ло­кальная переменная tmp — комплексное число. Следующие два присваива­ния добавляют к левому операнду — первому слагаемому правый операнд — второе слагаемое. Результат остается в промежуточной переменной и возвра­щается инструкцией return. Аналогично перегружается оператор вычитания и, в соответствии с правилом умножения комплексных чисел, — оператор произведения.
Теперь нам осталось взглянуть на вызывающую наш пример часть про­граммы, организованную в форме реакции на нажатие кнопки Button1:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
COMP z1(1.1,2.2),z2(-1.0,-2.0),z3,z5;
COMP *z4 = new COMP();
*z4 = z1+z2;
z3 = z1-z2;
z5 = z1*z2;
ShowMessage(FloatToStr(z4->b));
ShowMessage(FloatToStr(z3.b));
ShowMessage(FloatToStr(z5.a));
}
Здесь стандартным образом с использованием конструкторов с парамет­рами создаются два комплексных числа z1 è z2. С помощью конструкторов без параметров создаются числа z3 è z5. Комплексное число z4 объявляется через указатель с использованием оператора new. Далее демонстрируется вы­полнение сложения, вычитания и умножения с перегруженными на ком плексные числа операторами. Результаты выводятся в панели ShowMessage.
-
82 Глава 2. Объектно-ориентированное программирование
Многопотоковые приложения
Вычислительные мощности современных компьютеров очень велики, что позволяет реализовывать на одной машине одновременное выполнение не­скольких заданий. Традиционно считается, что это связано с существенным различием скоростей выполнения операций непосредственно процессором (логика и арифметика) и операций обмена информацией с относительно мед­ленными внешними устройствами. Кроме того, богатые возможности много­задачности открываются с внедрением в практику многопроцессорных сис­тем. Под термином многозадачности понимается возможность, предоставляе­мая операционной системой (Windows, Linux и другими) одновременного выполнения нескольких приложений. Типичным примером, иллюстрирую­щим многозадачность, является ситуация, когда вы «скачиваете» откуда-то из Интернета большой файл и одновременно работаете с каким-либо докумен­том, например, в редакторе Word.
Современные системы программирования, включая C++ Builder, предос­тавляют возможность одновременного выполнения нескольких заданий в од­ном приложении. Это уже не многозадачность, а многопотоковость, хотя по­следний термин не слишком удачен, поскольку поток в информационных системах трактуется очень широко. В этой связи необходимо сделать некото­рые разъяснения. В одной программе выполняются параллельно во времени несколько заданий, которые могут быть как совершенно независимыми, так и взаимосвязанными. Последнее обеспечено тем, что задания выполняются в едином для приложения адресном и объектном пространстве. Наряду с тер­мином поток для обозначения параллельно выполняемых заданий в одном приложении используется термин нить (thread). Распределением вычисли­тельных ресурсов между нитями самостоятельно занимается система, однако программист имеет возможность регулировать это распределение посредством установки приоритетов нитей. Отметим также, что пара нить — главная про­грамма также выполняются параллельно.
 Ñ++ Builder нити рекомендуется оформлять в форме объектов специ­альных классов, наследуемых от предопределенного в системе родительского класса Thread со всеми его свойствами и методами. Автоматически создавае­мый конструктор нити является методом с одним аргументом Create- Suspended. Этот аргумент определяет способ запуска нити. Если при создании объекта-нити конструктор вызывается со значением CreateSuspended = false, то нить начинает выполняться сразу. Если же аргумент равен true, то нить за­пускается из вызывающей программы с помощью метода Resume. Исполнение нити можно приостановить, вызвав метод Suspend, и снова запустить, вызвав Resume еще раз. Метод Terminate выполняет окончательное завершение (раз­рушение) нити.
Тело нити-операции, выполняемое в потоке, записывается в методе Execute. Здесь, собственно, и кодируется алгоритм желаемых действий. При проектировании метода Execute â C++ Builder следует учесть одно важнейшее обстоятельство. В этой системе программирования, как мы уже видели, чрез-
Многопотоковые приложения 83
вычайно широко используются визуальные компоненты, размещенные на фор­ме. Форма, как правило, является атрибутом главного модуля программы, в то время как объект нити создается из другого модуля. При этом и главный мо­дуль и нити могут одновременно оперировать с визуальными компонентами типа таймеров, полей редактирования и другими. Такая ситуация чревата кон­фликтами, и они действительно часто возникают, если не принимать специаль­ных мер. Способ разрешения конфликтов заключается в том, что в функции
Execute вызывается единственная функция-член класса нити, называемая Synchronize. Ее назначением и является отслеживание и урегулирование раз-
личных «пересечений» при параллельном выполнении заданий. Единственным параметром метода Synchronize является имя самодельной функции-члена (ко­торую рекомендуется делать закрытой), куда переносятся все действия нити.
Теперь, не вдаваясь в детали синтаксиса оформления многопотокового приложения, сразу перейдем к примеру. Разработаем программу, которая вы­полняет следующее: в основном модуле на форме каждую секунду рисуется (перерисовывается) закрашенный эллипс, длины осей которого являются слу­чайными числами в пределах размера картинки, изменяющиеся от перерисов­ки к перерисовке. Нить обеспечивает ежесекундный вывод в поле редактиро­вания на форме текущего времени.
Создайте новый проект и на его форме разместите таймер (Timer), три кнопки (Button), поле редактирования (Edit) и графическое поле (Image). Форма должна выглядеть примерно так, как это показано на рис. 2.7.
Ðèñ. 2.7
Сразу же добавим к проекту новую нить. Для этого из пункта File главно­го меню Builder'а необходимо открыть подпункт New и далее Other. В открыв­шемся окне New Items следует выбрать пиктограмму Thread Object. После на­жатия кнопки ÎÊ в диалоговом окне указывается имя создаваемого класса нити. Пусть это будет TIM. После этого имеет смысл сохранить проект с по
-
84 Глава 2. Объектно-ориентированное программирование
мощью комбинации клавиш Ctrl+Shift+S (сохранить все). Подтвердите пред­лагаемые по умолчанию имена сохранения. Теперь в указанном вами месте сохранения вы можете увидеть, что в проекте имеются два заголовочных фай­ла Unit1.h è Unit2.h и два модуля реализации Unit1.cpp è Unit2.cpp.
Заголовок Unit1 оставляем без изменения, а в Unit2 добавляем одну строку в раздел private. Теперь этот заголовок должен выглядеть следующим образом:
#ifndef Unit2H #define Unit2H //—————————————————————————————————————­#include <Classes.hpp> //—————————————————————————————————————­class TIM : public TThread { private:
void __fastcall WORK();
protected:
void __fastcall Execute();
public:
__fastcall TIM(bool CreateSuspended); }; //—————————————————————————————————————­#endif
В раздел закрытых членов мы внесли объявление функции WORK. Ýòî —
та самая функция, которая будет являться аргументом метода Synchronize.
Теперь перейдем в модуль реализации Unit2.cpp, представленный ниже:
#include <vcl.h> #pragma hdrstop #include "Unit1.h" #include "Unit2.h" #pragma package(smart_init) extern BOOL KEY,TTT; __fastcall TIM::TIM(bool CreateSuspended)
: TThread(CreateSuspended) { } //—————————————————————————————————————­void __fastcall TIM::Execute() {
Synchronize(WORK); } //—————————————————————————————————————­void __fastcall TIM::WORK() {
while(KEY)
{
if(TTT)
{
TTT = false;
Многопотоковые приложения 85
Form1->Edit1->Text = Time();
}
Application->ProcessMessages();
}
}
Мы видим, что метод Execute вызывает функцию Synchronize, и больше ничего в нем не делается. В функции WORK организован цикл, который по­вторяется до тех пор, пока логическая переменная KEY не примет значение false. Сразу укажем, что присвоение этой переменной значения false будет производиться из главного модуля при нажатии кнопки STOP. Внутри цикла проверяется, является ли значение булевой переменной ÒÒÒ истиной. Если это так, то TTT сразу принимает противоположное значение (скоро поймем зачем), а в поле редактирования формы с помощью функции Time выводится текущее время. В главном модуле сделаем так, чтобы ÒÒÒ получало значение истины при срабатывании таймера раз в секунду. Это даст сигнал к выводу времени, а ÒÒÒ станет ложью до следующего срабатывания.
Последним выражением в цикле является вызов системной функции ProcessMessages. Эта функция принудительно извлекает из очереди сообще­ния, которыми обмениваются между собой операционная система и процес­сы, и выполняет действия, соответствующие этим сообщениям. Безусловно, согласимся с тем, что сказанное воспринимается достаточно туманным. Столь же туманно и разъяснение в справке по этой функции. Реально дело обстоит следующим образом. По каким-то причинам при выполнении циклов далеко не все внешние по отношению к циклу события немедленно анализируются. Так, в нашем случае, нажатие кнопки STOP в отсутствии функции ProcessMessages игнорируется, и только благодаря ей удается «вовремя» опре­делить, что переменная KEY изменила значение. Ситуация достаточно рас­пространенная, в силу чего сделаем следующую рекомендацию. Если обнару­живается неправильная работа цикла, который должен реагировать на внеш­ние события, попробуйте включить в цикл указанную функцию.
Теперь рассмотрим главный модуль Unit1.cpp:
#include <vcl.h> #pragma hdrstop #include "Unit1.h" #include "Unit2.h" //—————————————————————————————————————­#pragma package(smart_init) #pragma resource "*.dfm" TForm1 *Form1; TIM *Tim = new TIM(true); BOOL KEY = true,TTT = false; //—————————————————————————————————————­__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner) { } //—————————————————————————————————————-
86 Глава 2. Объектно-ориентированное программирование
void __fastcall TForm1::Button1Click(TObject *Sender) {
Timer1->Enabled=true; } //—————————————————————————————————————-
void __fastcall TForm1::Timer1Timer(TObject *Sender) {
Image1->Canvas->Brush->Color = clWhite;
Image1->Canvas->FillRect(Rect(0,0, Image1->Width,Image1->Height));
Image1->Canvas->Brush->Color = clRed;
Image1->Canvas->Ellipse(0,0, random(100),random(100));
TTT = true; } //—————————————————————————————————————-
void __fastcall TForm1::Button2Click(TObject *Sender) {
Tim->Resume(); } //—————————————————————————————————————­void __fastcall TForm1::Button3Click(TObject *Sender) {
KEY = false;
Close(); }
Прежде всего, обратим внимание на то, что к этому модулю необходимо подключить не только заголовок Unit1.h, что делается автоматически, но и Unit2.h, о чем необходимо позаботиться программисту. Это необходимо, по­скольку класс и его методы описаны во внешних файлах. Кстати, и обратное тоже необходимо — к модулю Unit2.cpp нужно подключить Unit1.h. В качестве глобального сформирован объект класса TIM с именем Tim. Как можно ви­деть, здесь мы вызвали конструктор с помощью оператора new. Аргументом конструктора является значение true, следовательно, нить сразу не запускает­ся, а ждет вызова метода Resume. Также глобально описаны булевы перемен­ные KEY è ÒÒÒ, назначение которых нам уже понятно.
Далее модуль содержит обработчики событий. Нажатие кнопки Button1 (START) переключает свойство таймера Enabled â true, тем самым запуская таймер. Обработчик срабатывания таймера выполняет следующую последова­тельность действий:
1. Цвет кисти для рисования на канве задается белым.
2. Прямоугольник, совпадающий по размерам со всей графической обла-
стью, закрашивается белым цветом, то есть стирается все, что там было.
3. Цвет кисти задается красным.
4. Красным цветом (со сплошной заливкой) рисуется эллипс со случай-
ными размерами осей.
5. Переменная ÒÒÒ устанавливается в true, тем самым разрешая выпол
нить действия в цикле в нити.
-
Многопотоковые приложения 87
Нажатие кнопки Button2 (RESUME) запускает выполнение нити. Нако­нец, кнопка Button3 (STOP) задает переменной KEY значение false, в резуль­тате чего происходит выход из цикла в нити. Сразу после этого срабатывает функция Close, закрывающая приложение.
Теперь, если все сделано правильно, после запуска программы, нажатия кнопки START и запуска нити на экране можно видеть картинку, представ­ленную на рис. 2.8.
Ðèñ. 2.8
Если интервал срабатывания таймера установлен в 1000 (миллисекунд), то каждую секунду изображение меняется — отображается новое текущее время и новый эллипс.
Глава 3
ПЕРЕДАЧА ИНФОРМАЦИИ В СЕТЯХ
Введение в компьютерные коммуникации
Понятно, что тема передачи данных в цифровых сетях совершенно необъ­ятна. Соответствующим частным и общим вопросам посвящено громадное количество литературы. Более того, отрасль сетевой транспортировки инфор­мации стремительно развивается, и при всем желании уследить за всеми по­следними достижениями здесь просто невозможно. По этой причине пред­ставляемый здесь материал совершенно не претендует на хоть малую степень полноты изложения, а всего лишь может рассматриваться как стартовая пло­щадка для тех, кто намерен заниматься этой серьезной темой. Тем не менее, мы представим минимум сведений, которые позволят нам разрабатывать «действующие макеты» приложений, связанных с сетевыми технологиями.
Основой основ сетевых коммуникаций является понятие коммуникаци­онных протоколов. В самом простейшем варианте под термином сетевой про­токол следует понимать набор правил, по которым формируются пакеты (порции) данных для их дальнейшей передачи по компьютерным сетям. Про­токолов создано великое множество, и, что самое поразительное, практически все это множество находит свое применение для решения тех или иных задач. Для того чтобы хоть как-то упорядочить все разработанные протоколы, при­нята специальная классификация, в основе которой лежат так называемые уровни. Заметим здесь, что таких уровней вычленили семь, и появился целый раздел информатики, называемый «семиуровневая иерархия сетевых протоко­лов». Для нас наибольший интерес будет представлять та часть иерархической структуры протоколов, корнем которой будет группа протоколов сетевого уровня. Сетевые протоколы ориентированы на конкретные виды сетей. Из всего этого семейства нам здесь понадобится наиболее популярный протокол, называемый просто IP — Internet Protocol. IP-протокол предназначен для ра­боты в глобальных и локальных вычислительных сетях, в которых все узлы снабжены так называемыми IP-адресами. Назначение протокола сетевого уровня заключается только в обеспечении возможности передачи пакетов, то есть обеспечении маршрутизации данных. Сам по себе сетевой протокол не гарантирует доставку пакетов. Никакие функции контроля правильности пе редачи данных или исправления ошибок непосредственно на сетевой прото
-
-
Введение в компьютерные коммуникации 89
кол не возлагаются. Этим занимаются так называемые протоколы транспорт­ного уровня.
Передаваемые по сети данные можно разделить на два типа: командные данные и информационные данные. К командной информации относятся сигналы вызова, разъединения, а также другие служебные сообщения. Ин­формационные данные могут представлять собой текст, цифровой звук, циф­ровое изображение. Основное требование к передаче командной информа­ции — отсутствие ошибок передачи. В результате необходимо использовать достоверный (надежный) протокол доставки сообщений. Обычно в качестве такого протокола используется протокол транспортного уровня TCP (Transport Control Protocol), обеспечивающий гарантированную доставку со­общений. Необходимо отчетливо представлять себе, что пакеты данных, путе­шествующие по сети, могут искажаться в результате сбоев в работе аппарат­ного обеспечения узлов или вообще теряться, например, из-за того, что время существования пакета может быть ограниченным. В этой связи во многих си­туациях (хотя и не всегда) контроль за правильностью прохождения пакетов оказывается совершенно необходимым. Именно этот контроль и выполняет протокол TCP. Не вдаваясь в детали, укажем только, что протокол имеет воз­можность получать из пункта назначения пакета информацию, подтверждаю­щую правильность передачи данных. Если по истечении заданного времени соответствующее подтверждение не получено, производится повторная пере­сылка пакета. Так может продолжаться, в принципе, очень продолжительное время. Теперь, наверное, понятно, что при работе в Интернете, а иногда и в загруженной локальной сети, мы сталкиваемся с большими задержками при транспортировке данных. При передаче командной информации с этим при­ходится мириться. В то же время может возникать ситуация, когда гарантиро­ванная доставка информации не только не обязательна, но и нежелательна.
Ситуация, о которой мы говорим, имеет место, например, при передаче звука в реальном времени. Действительно, живой звук — человеческая речь, музыка, это объект, который нельзя «заморозить» во времени, по крайней мере, на сколько-нибудь продолжительное время. Оборванная на середине слова фраза в разговоре совершенно недопустима. (Считается, что макси­мально возможные задержки в речи не должны превышать величину 200—300 миллисекунд.) Запись цифрового звука осуществляется порциями. Совершенно очевидно, что и передача звуковой (и любой другой) информа­ции производится порциями. В этой связи понятно, что если ошибки переда­чи пакетов не слишком частые, то лучше пожертвовать отдельным фрагмен­том речи длительностью, например, в десятую долю секунды, чем ждать, воз­можно, очень долго, пока этот фрагмент все же будет правильно передан. С передачей изображения ситуация менее жесткая. Изображение как раз и можно «заморозить» на экране. Хотя и здесь в некоторых случаях имеет смысл пропустить один или несколько кадров.
Теперь понятно, что для передачи мультимедиа реального времени прото­кол TCP не очень подходит. Необходимы инструменты негарантированной пе­редачи информации, и такие инструменты имеются. В наших разработках мы будем применять протокол транспортного уровня UDP (User Datagram
90 Глава 3. Передача информации в сетях
Protocol — протокол передачи пользовательских дейтаграмм). Задачей этого инструмента является только отправка на заданный адрес порции данных лю­бого типа — пользовательских дейтограмм. При этом факт неправильной пере­сылки информации фиксируется, но никакие действия при этом не предпри­нимаются. Очевидно, что для передачи служебных данных UDP не применим, но для передачи звука с возможными потерями фрагментов речи или музыки протокол оказывается очень полезным. При использовании UDP, помимо по­терь отдельных блоков данных, может происходить нарушение очередности следования пакетов. Поскольку блоки информации могут следовать по различ­ным маршрутам и на пути следования испытывать различные задержки, суще­ствует вероятность того, что пакет, отправленный позже предыдущего, достиг­нет адресата раньше. Для передачи звука такая ситуация крайне нежелательна. Хотя практика показала, что подобные нарушения очередности возникают достаточно редко, меры к их устранению все же принимаются. Одним из вари­антов борьбы с этим явлением является использование джиттер-буфера вре­менного хранения группы пришедших пакетов, где они упорядочиваются по времени отправки. Неизбежным при этом будет задержка, обусловленная на­коплением пакетов, но это все же лучше, чем обмен местами, например, нача­ла и конца слова или фразы. Другим способом устранения этой досадной по­мехи в современных профессиональных системах IP-телефонии является ис­пользование протокола RTP (Real Time Protocol) и его сателлита — протокола RTCP (Real Time Control Protocol). Протокол реального времени также являет­ся протоколом негарантированной доставки пакетов и также работает на осно­ве сетевого IP. Говоря точнее, в отличие от UDP этот протокол выполняет га­рантированную доставку только в течение заданного интервала времени. Если в этом интервале передать блок не удалось, то на этом обработка текущей пор­ции данных заканчивается и начинает передаваться следующая. Таким обра­зом, хотя и не обеспечивается доставка каждого из блоков, нарушений очеред­ности не возникает и передача и прием пакетов строго распределены по рав­ным временным интервалам.
Итак, мы имеем в своем распоряжении несколько наборов правил фор­мирования
пакетов данных, которые можем применять для тех или иных за­дач транспортировки информации. Работая с сетевым протоколом IP, мы имеем возможность адресовать передачу с помощью IP-адресов. В дальней­шем узел, которому предназначена транспортировка данных, будем называть удаленным хостом (remote host), а узел, являющийся источником информа­ции, — локальным хостом (local host). Обычно пара из передающего и прини­мающего узлов называется парой КЛИЕНТ — СЕРВЕР, хотя такая классифи­кация узлов довольно условна. Адреса узлов могут быть представлены четы­рехбайтовым числом, в котором десятичные представления байтов отделены друг от друга символом точка — обычное цифровое представление IP-адреса. Кроме того, хост может быть идентифицирован и символьным именем типа mail.ru. Символьное имя однозначно связано с цифровым адресом и назнача­ется специальной службой сети. При программировании наших задач мы бу­дем использовать исключительно цифровые IP-адреса. Здесь важно напом­нить, что если компьютер не подключен к сети, но в операционную систему