Резервирование, распределение и освобождение виртуальной памяти
Определение потока
Потоком в Windows называется объект ядра, которому операционная система выделяет процессорное время для выполнения приложения. Каждому потоку принадлежат следующие ресурсы:
□ код исполняемой функции;
□ набор регистров процессора;
□ стек для работы приложения;
□ стек для работы операционной системы;
□ маркер доступа, который содержит информацию для системы безопасности.
Все эти ресурсы образуют контекст потока в Windows. Кроме дескриптора каждый поток в Windows также имеет свой идентификатор, который уникален для потоков выполняющихся в системе. Идентификаторы потоков используются служебными программами, которые позволяют пользователям системы отслеживать работу потоков.
В операционных системах Windows различаются потоки двух типов:
□ системные потоки;
□ пользовательские потоки.
Системные потоки выполняют различные сервисы операционной системы и запускаются ядром операционной системы. Пользовательские потоки служат для решения задач пользователя и запускаются приложением. На рис. 3.1показана диаграмма состояний потока, работающего в среде операционной системе Windows 2000. В работающем приложении различаются потоки двух типов:
□ рабочие потоки (workingthreads);
□ потоки интерфейса пользователя (userinterfacethreads).
Рис. 3.1. Модель состояний потока в Windows 2000
Рабочие потоки выполняют различные фоновые задачи в приложении. Потоки интерфейса пользователя связаны с окнами и выполняют обработку сообщений, поступающих этим окнам. Каждое приложение имеет, по крайней мере, один поток, который называется первичным (primary) или главным
(main) потоком. В консольных приложениях это поток, который исполняет функцию main. В приложениях с графическим интерфейсом это поток, который исполняет функциюWinMain.
3.2Задача.Определить, сколько в массиве положительных элементов и вывести их на экран.
#include <iostream>
#include <stdio.h>
constint N=10;
intmain(intargc, char** argv) {
inti, A[N]={-7,0,-6,1,2,4,9,5,22,33}, count = 0;
for ( i=0; i<N; i++ )
if ( A[i]>0 ) {
count ++;
printf("%d ", A[i]);
}
printf("count =%d ", count);
return 0;
}
Модульное программирование
С увеличением объема программы становится невозможным удерживать в памяти все детали. Естественным способом борьбы со сложностью любой задачи является ее разбиение на части. В C++ задача может быть разделена на более простые и обозримые с помощью функций, после чего программу можно рассматривать в более укрупненном виде – на уровне взаимодействия функций. Это важно, поскольку человек способен помнить ограниченное количество фактов. Использование функций является первым шагом к повышению степени абстракции программы и ведет к упрощению ее структуры.
Разделение программы на функции позволяет также избежать избыточности кода, поскольку функцию записывают один раз, а вызывать ее на выполнение можно многократно из разных точек программы. Процесс отладки программы, содержащей функции, можно лучше структурировать. Часто используемые функции можно помещать в библиотеки. Таким образом, создаются более простые в отладке и сопровождении программы.
Следующим шагом в повышении уровня абстракции программы является группировка функций и связанных с ними данных в отдельные файлы (модули), компилируемые раздельно. Получившиеся в результате компиляции объектные модули объединяются в исполняемую программу с помощью компоновщика. Разбиение на модули уменьшает время перекомпиляции и облегчает процесс отладки, скрывая несущественные детали за интерфейсом модуля и позволяя отлаживать программу по частям (или разными программистами).
Модуль содержит данные и функции их обработки. Другим модулям нежелательно иметь собственные средства обработки этих данных, они должны пользоваться для этого функциями первого модуля. Для того чтобы использовать модуль, нужно знать только его интерфейс, а не все детали его реализации. Чем более независимы модули, тем легче отлаживать программу. Это уменьшает общий объем информации, которую необходимо одновременно помнить при отладке. Разделение программы на максимально обособленные части является сложной задачей, которая должна решаться на этапе проектирования программы.
Модульность в языке C++ поддерживается с помощью директив препроцессора, пространств имен, классов памяти, исключений и раздельной компиляции (строго говоря, раздельная компиляция не является элементом языка, а относится к его реализации).
2.5Задача взаимного исключения
Теперь рассмотрим задачу взаимного исключения. Чтобы упростить рассуждения, эта задача будет сформулирована только для двух параллельных потоков. Сначала предположим, что два параллельных потока работают с одним и тем же ресурсом, который в этом случае называется разделяемым или совместно используемым ресурсом. Далее считаем, что в каждом потоке программный код, который осуществляет доступ к этому ресурсу, заключен в свою критическую секцию. Тогда задача взаимного исключения для двух потоков может быть сформулирована следующим образом: обеспечить двум потокам взаимоисключающий доступ к некоторому совместно используемому ресурсу. Причем решение этой задачи должно удовлетворять следующим требованиям:
□ требование безопасности — в любой момент времени в своей критической секции может находиться только один поток;
□ требование поступательности — потоки не могут блокировать работу друг друга, ожидая разрешения на вход в критическую секцию;
□ требование справедливости — каждый поток получает доступ в критическую секцию за ограниченное время.
Ниже приведено простейшее из известных решений задачи взаимного исключения для двух потоков, которое было опубликовано Гэри Л. Петерсоном в 1981 году.
boolxl = false;
boolx2 = false;
intq; // номер потока, которому предоставляется очередь входа в
// критическую секцию
voidthread_l() // потокthread_l
{
while(true)
{
non_critical_section_l(); // кодвнекритическойсекции
xl = true; // поток thread_l хочет войти в критическую секцию
q = 2; // предоставить очередь потоку thread_2
while(х2 &&q == 2); // ждем, пока в критической секции находится
// поток thread_2
critical_section_l(); // входим в критическую секцию
xl = false; // поток thread_l находится вне критической секции
}
}
void thread_2() // поток thread_2
{
while(true)
{
_critical_section_2(); // код вне критической секции
х2 = true; // поток thread_2 хочет войти в критическую секцию
q = 1; // предоставить очередь потоку thread_l
while(xl&&q == 1); // ждем, пока в критической секции находится
// поток thread_l
critical_section_2(); // входим в критическую секцию
х2 = false; // поток thread_2 находится вне критической секции
3.5 Задача. Найти и вывести на экран минимальный элемент в массиве A и его номер
#include <iostream>
using namespace std;
const int N=5;
int main ()
{
int array []= {4,0,-3,1,5};
int smallest = array[0]0;
for ( int i=1; i < sizeof(array)/sizeof(array[0]); ++i )
if ( array[i] <array[smallest] )
smallest = i ;
cout <<array[smallest]<< '\n' ;
cout << smallest << '\n' ;
return 0;
}
1.8 Наследование
При большом количестве никак не связанных классов управлять ими становится невозможным. Наследование позволяет справиться с этой проблемой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Механизм наследования классов позволяет строить иерархии, в которых производные классы получают элементы родительских, или базовых, классов и могут дополнять их или изменять их свойства.
Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных черт. Множественное наследование позволяет одному классу обладать свойствами двух и более родительских классов.
Виды наследования
При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулируется с помощью модификаторов наследования private, protected и public:
classимя : [private | protected | public] базовый_класс{тело класса};
Если базовых классов несколько, они перечисляются через запятую. Перед каждым может стоять свой модификатор наследования. По умолчанию для классов он private, а для структур - public.
Если задан модификатор наследования public, оно называется открытым. Использование модификатора protected делает наследование защищенным, а модификатора private - закрытым. Это не просто названия: в зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
Кроме спецификаторов доступа private и public для любого элемента класса может также использоваться спецификатор protected, который для одиночных классов, не входящих в иерархию, равносилен private. Разница между ними проявляется при наследовании.
Private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса.
Элементы protected при наследовании с ключом private становятся в производном классе private, в остальных случаях права доступа к ним не изменяются.
Доступ к элементам public при наследовании становится соответствующим ключу доступа.
Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе, объявив их в секции public производного класса с помощью операции доступа к области видимости:
class Base{...
public: void f();};
class Derived : private Base{...
public: Base::void f();};
Простое наследование
Простым называется наследование, при котором производный класс имеет одного родителя. Для различных элементов класса существуют разные правила наследования. Рассмотрим наследование классов на примере.
Создадим производный от класса monster класс daemon, добавив полезную в некоторых случаях способность думать:
enum color {red, green, blue};
// ------------- Класс monster -------------
class monster
{
// ------------- Скрытыеполякласса:
int health, ammo;
color skin;
char *name;
public:
// ------------- Конструкторы:
monster(int he = 100, int am = 10);
monster(color sk);
monster(char * nam);
monster(monster &M);
// ------------- Деструктор:
~monster() {delete [] name;}
// ------------- Операции:
monster& operator ++(){++health; return *this;}
monster operator ++(int)
{monster M(*this); health++; return M;}
operator int(){return health;}
bool operator >(monster &M)
{
if( health>M.get_health()) return true;
return false;
}
monster& operator = (monster &M)
{
if (&M == this) return *this;
if (name) delete [] name;
if (M.name)
{
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
return *this;
}
// ------------- Методы доступа к полям:
intget_health() const {return health;}
intget_ammo() const {return ammo;}
// ------------- Методы, изменяющиезначенияполей:
void set_health(int he){ health = he;}
void draw(int x, int y, int scale, int position);
};
// ------------- Реализациякласса monster -------------
monster::monster(int he, int am):
health (he), ammo (am), skin (red), name (0){}
monster::monster(monster &M)
{
if (M.name)
{
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
}
monster::monster(color sk)
{
switch (sk)
{
case red:health = 100; ammo = 10; skin = red; name = 0; break;
case green:health = 100;ammo = 20;skin = green; name = 0; break;
case blue: health = 100; ammo = 40; skin = blue; name = 0;break;
}
}
monster::monster(char * nam)
{
name = new char [strlen(nam)+1];
strcpy(name, nam);
health = 100; ammo = 10; skin = red;
}
void monster::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка monster */ }
// ------------- Класс daemon -------------
class daemon : public monster
{ int brain;
public:
// ------------- Конструкторы:
daemon(intbr = 10){brain = br;};
daemon(color sk) : monster (sk) {brain = 10;}
daemon(char * nam) : monster (nam) {brain = 10;}
daemon(daemon &M) : monster (M) {brain = M.brain;}
// ------------- Операции:
daemon& operator = (daemon &M)
{
if (&M == this) return *this;
brain = M.brain;
monster::operator = (M);
return *this;
}
// ------------- Методы, изменяющие значения полей:
void draw(int x, int y, int scale, int position);
voidthink();};
// ------------- Реализация класса daemon -------------
void daemon::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка daemon */ }
void daemon:: think(){ /* ... */ }
В классе daemon введено поле brain и метод think, определены собственные конструкторы и операция присваивания, а также переопределен метод отрисовки draw. Все поля класса monster, операции (кроме присваивания) и методы get_health, get_ammo и set_health наследуются в классе daemon, а деструктор формируется по умолчанию.
2.8 Мьютексы
Для решения проблемы взаимного исключения между параллельными потоками, выполняющимися в контекстах разных процессов, в операционных системах Windows используется объект ядра мьютекс. Слово мьютекс происходит от английского слова mutex, которое в свою очередь является сокращением от выражения mutualexclusion, что на русском языке значит "взаимное исключение". Мьютекс находится в сигнальном состоянии, если он не принадлежит ни одному потоку. В противном случае мьютекс находится в несигнальном состоянии. Одновременно мьютекс может принадлежать только одному потоку.
Потоки, ждущие сигнального состояния мьютекса, обслуживаются в порядке FIFO, т. е. потоки становятся в очередь к мьютексу с дисциплиной обслуживания "первый пришел — первый вышел". Однако если поток ждетнаступления асинхронного события, то функции ядра могут исключить поток из очереди к мьютексу для обслуживания наступления этого события.
После этого поток становится в конец очереди мьютекса. Создается мьютекс вызовом функции CreateMutex, которая имеет следующий прототип:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибутызащиты
BOOL blnitialOwner, // начальныйвладелецмьютекса
LPCTSTR lpName // имямьютекса
);
Пока значение параметра lpsecurity_attributes будем устанавливать в null. Это означает, что атрибуты защиты заданы по умолчанию, т. е. дескриптор мьютекса не является наследуемым и доступ к мьютексу открыт для всех пользователей. Теперь перейдем к другим параметрам. Если значение параметра blnitialOwner равно true, to мьютекс сразу переходит во владение потоку, которым он был создан. В противном случае вновь созданный мьютекс свободен. Поток, создавший мьютекс, имеет все права доступа к этому мьютексу.
Значение параметра lpName определяет уникальное имя мьютекса для всех процессов, выполняющихся под управлением операционной системы. Это имя позволяет обращаться к мьютексу из других процессов, запущенных под управлением этой же операционной системы. Длина имени не должна превышать значение мах_ратн. Значением параметра lpName может быть пустой указатель null. В этом случае система создает безымянный мьютекс. Отметим также, что имена мьютексов являются чувствительными к нижнему и верхнему регистрам.
В случае удачного завершения функция CreateMutex возвращает дескриптор созданного мьютекса. В случае неудачи эта функция возвращает значение null. если мьютекс сзаданным именем уже существует, тофункцияCreateMutex возвращает дескриптор этого мьютекса, а функция GetLastError, вызванная послефункцииCreateMutex, вернетзначениеERROR_ALREADY_EXISTS.
Мьютекс захватывается потоком посредством любой функции ожидания, а освобождается функцией ReleaseMutex, которая имеет следующий прототип:
BOOL ReleaseMutex (
HANDLE hMutex // дескриптормьютекса
);
В случае успешного завершения функция ReleaseMutex возвращает ненулевое значение, а в случае неудачи — false. Если поток освобождает мьютекс, которым он не владеет, то функция ReleaseMutex возвращает значение false.
3.8 Задача. Выполнить сортировку массива методом пузырька.
#include <iostream>
#include <stdio.h>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char** argv)
{
int a[10],i,n,j,t,k,f,m=0;
printf("enter n:");
scanf("%d",&n);
printf("enter elements:");
for(i=0;i<n;i++)
scanf("%d",&a[i]);
printf("the array is :\n");
for(i=0;i<n;i++)
printf("%d\t",a[i]);
printf("\n");
for(k=1;k<n;k++)
{
f=0;
for(i=0,j=i+1;i<n,j<n;i++,j++)
{
if(a[i]>a[j])
{
t=a[i];
a[i]=a[j];
a[j]=t;
f=1;
}
}
if(f==0)
break;
m=k;
}
printf("the sorted array is:\n");
for(i=0;i<n;i++)
printf("%d\t",a[i]);
printf("\n");
printf("the number of passes is %d",m);
return 0;
}
1.11 Механизм позднего связывания
Имеются два термина, часто используемых, когда речь заходит об объектно-ориентированных языках программирования: раннее и позднее связывание. По отношению к С++ эти термины соответствуют событиям, которые возникают на этапе компиляции и на этапе исполнения программы соответственно.
В терминах объектно-ориентированного программирования раннее связывание означает, что объект и вызов функции связываются между собой на этапе компиляции. Это означает, что вся необходимая информация для того, чтобы определить, какая именно функция будет вызвана, известна на этапе компиляции программы. В качестве примеров раннего связывания можно указать стандартные вызовы функций, вызовы перегруженных функций и перегруженных операторов. Принципиальным достоинством раннего связывания является его эффективность — оно более быстрое и обычно требует меньше памяти, чем позднее связывание. Его недостатком служит невысокая гибкость.
Позднее связывание означает, что объект связывается с вызовом функции только во время исполнения программы, а не раньше. Позднее связывание достигается в С++ с помощью использования виртуальных функций и производных классов. Его достоинством является высокая гибкость. Оно может использоваться для поддержки общего интерфейса, позволяя при этом различным объектам иметь свою собственную реализацию этого интерфейса. Более того, оно помогает создавать библиотеки классов, допускающие повторное использование и расширение.
Какое именно связывание должна использовать программа, зависит от предназначения программы. Фактически достаточно сложные программы используют оба вида связывания. Позднее связывание является одним из самых мощных добавлений языка С++ к возможностям языка С. Платой за такое увеличение мощи программы служит некоторое уменьшение ее скорости исполнения. Поэтому использование позднего связывания оправдано только тогда, когда оно улучшает структурированность и управляемость программы. Надо иметь в виду, что проигрыш в производительности невелик, поэтому когда ситуация требует позднего связывания, можно использовать его без всякого сомнения.
2.11 Взаимоисключающий доступ к переменным
Иногда параллельным потокам необходимо выполнять некоторые несложные действия над общими переменными, исключая совместный доступ к этим переменным. Если в этом случае для синхронизации доступа к переменным использовать критические секции или мьютексы (см. разд. 6.1, 6.3), то может возникнуть такая ситуация, что затраты на синхронизацию потоков значительно превышают затраты на выполнение самих операций. Для решения этой проблемы используются специальные функции, которые выполняют несложные действия над переменными, блокируя при этом доступ к этим переменным со стороны других потоков. Такие функции называются блокирующими функциями(interlockedfunctions). Блокирующие функции выполняют одну или несколько элементарных операций, которые объединяются в одну неделимую операцию, которая в этом случае также называется атомарной операцией. Блокирующие функции могут использоваться потоками, выполняющимися в разных процессах, для взаимоисключающего доступа к переменным, расположенным в разделяемой этими процессами памяти. Процессы могут разделять общую память при помощи механизма отображения файлов в память, который описан в гл. 30.
Атомарная операция обычно включает операцию, выполняющую некоторое действие, и, может быть, операцию сравнения, которая позволяет выполнять это действие при некотором условии. В качестве операций, которые выполняют действия, могут выступать операции замены значения переменной или арифметические операции. Такие операции характеризуют атомарную операцию и используются для ее названия.
В операционных системах Windowsблокирующие функции можно разбить на четыре группы, принимая во внимание типы характерных элементарных операций, которые они выполняют. Эти блокирующие функции рассмотрены в разделах этой главы.
Заметим, что все блокирующие функции требуют, чтобы адреса переменных были выровнены на границу слова, т. е. были кратны 32. Для такого выравнивания адреса достаточно, чтобы переменная была объявлена в программеСО спецификатором типа long, ИЛИ unsignedlong, ИЛИ ОДНИМ ИЗ ИХ СИНОНИМОВ LONG, ULONGИЛИ DWORD.
3.11Задача. Ввести с клавиатуры два целых числа и вывести на экран их сумму.
#include <iostream>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char** argv)
{
int a, b, sumOfTwoNumbers;
cout << "Enter two integers: ";
cin >> a >> b;
sumOfTwoNumbers = a + b;
cout << a << " + " << b << " = " << sumOfTwoNumbers;
return 0;
}
1.14 Отличия структур и объединений от классов
Точно так же, как структуры и классы связаны между собой, связаны и объединения и классы. Объединения представляют по существу структуру, в которой все элементы хранятся в одном и том же месте. Объединения могут содержать конструкторы и деструкторы, а также функции-члены и дружественные функции. Подобно структурам, члены объединения по умолчанию имеют в качестве спецификатора доступа public. Например, следующая программа использует объединение для вывода символов, соответствующих старшему и младшему байтам короткого целого (имеющего размер в два байта как для 16-битных, так и для 32-битных сред):
#include<iostream.h>
unionu_type {
u_type(shortinta); // по умолчанию публичные
voidshowchars ();
shortinti;
charch[2];
};
// конструктор
u_type::u_type(shortinta)
{
i = a;
}
// показ символов, которые содержит shortint.
voidu_type::showchars()
cout<<ch [ 0 ] << " ";
cout<<ch [ 1 ] << "\n";
intmain()
u_typeu(1000);
u.showchars ();
return 0;
}
Важно понимать, что подобно структуре, объявление объединения определяет тип класса. Это означает, что принципы инкапсуляции сохраняют свою силу.
Имеется несколько ограничений, которые необходимо иметь в виду при использовании объединений в С++. Первое — объединение не может наследовать какие-либо другие классы. Далее объединение не может использоваться в качестве базового класса. Объединение не может иметь виртуальные функции-члены. Никакие статические переменные не могут быть членами объединения. Объединение не может иметь в качестве члена какой-либо объект, перегружающий оператор =. Наконец, никакой объект не может быть членом объединения, если этот объект имеет конструктор или деструктор.
2.14 Состояния виртуальной памяти процесса
Линейный адрес процесса в Windows состоит из 32 бит и изменяется в пределах от 0хоооооооо до 0xffffffff. Это теоретически позволяет процессу обращаться к 4 Гбайт логической памяти. В операционных системах семейства WindowsNT процессу доступны два младших гигабайта этой памяти с диапазоном адресов от 0хоооооооо до 0x7fffffff, а ее старшие два гигабайта с диапазоном адресов от 0xsooooooo до 0xffffffff используются системой. В операционной системе Windows 98 из 2 Гбайт памяти доступной процессу, операционная система использует еще 64 Кбайт памяти с диапазоном адресов от 0хоооооооо до 0x0000ffff для проверки присваивания значений через указатели, значения которых инициализированы в null, и для поддержки совместимости со старыми операционными системами MS-DOS и Windows 3.1.
В операционных Windows виртуальный адрес процесса отличается от линейного адреса этого же процесса только интерпретацией бит линейного адреса. Поэтому, можно сказать, что каждому процессу в Windows также доступно два гигабайта виртуальной памяти. Это не значит, что процесс может использовать всю эту память одновременно. Количество виртуальной памяти, доступной процессу, зависит от емкости физической памяти и дисков. Чтобы ограничить процесс в использовании виртуальной памяти, некоторые страницы в таблице страниц могут быть помечены как недоступные.
После этих замечаний перейдем к описанию состояния виртуальной памяти процесса. С точки зрения процесса, страницы его виртуальной памяти могут находиться в одном из трех состояний:
□ свободны для использования (free);
□ распределены процессу для использования (committed);
□ зарезервированы, но не используются процессом (reserved).
Определение потока
Потоком в Windows называется объект ядра, которому операционная система выделяет процессорное время для выполнения приложения. Каждому потоку принадлежат следующие ресурсы:
□ код исполняемой функции;
□ набор регистров процессора;
□ стек для работы приложения;
□ стек для работы операционной системы;
□ маркер доступа, который содержит информацию для системы безопасности.
Все эти ресурсы образуют контекст потока в Windows. Кроме дескриптора каждый поток в Windows также имеет свой идентификатор, который уникален для потоков выполняющихся в системе. Идентификаторы потоков используются служебными программами, которые позволяют пользователям системы отслеживать работу потоков.
В операционных системах Windows различаются потоки двух типов:
□ системные потоки;
□ пользовательские потоки.
Системные потоки выполняют различные сервисы операционной системы и запускаются ядром операционной системы. Пользовательские потоки служат для решения задач пользователя и запускаются приложением. На рис. 3.1показана диаграмма состояний потока, работающего в среде операционной системе Windows 2000. В работающем приложении различаются потоки двух типов:
□ рабочие потоки (workingthreads);
□ потоки интерфейса пользователя (userinterfacethreads).
Рис. 3.1. Модель состояний потока в Windows 2000
Рабочие потоки выполняют различные фоновые задачи в приложении. Потоки интерфейса пользователя связаны с окнами и выполняют обработку сообщений, поступающих этим окнам. Каждое приложение имеет, по крайней мере, один поток, который называется первичным (primary) или главным
(main) потоком. В консольных приложениях это поток, который исполняет функцию main. В приложениях с графическим интерфейсом это поток, который исполняет функциюWinMain.
3.2Задача.Определить, сколько в массиве положительных элементов и вывести их на экран.
#include <iostream>
#include <stdio.h>
constint N=10;
intmain(intargc, char** argv) {
inti, A[N]={-7,0,-6,1,2,4,9,5,22,33}, count = 0;
for ( i=0; i<N; i++ )
if ( A[i]>0 ) {
count ++;
printf("%d ", A[i]);
}
printf("count =%d ", count);
return 0;
}
Модульное программирование
С увеличением объема программы становится невозможным удерживать в памяти все детали. Естественным способом борьбы со сложностью любой задачи является ее разбиение на части. В C++ задача может быть разделена на более простые и обозримые с помощью функций, после чего программу можно рассматривать в более укрупненном виде – на уровне взаимодействия функций. Это важно, поскольку человек способен помнить ограниченное количество фактов. Использование функций является первым шагом к повышению степени абстракции программы и ведет к упрощению ее структуры.
Разделение программы на функции позволяет также избежать избыточности кода, поскольку функцию записывают один раз, а вызывать ее на выполнение можно многократно из разных точек программы. Процесс отладки программы, содержащей функции, можно лучше структурировать. Часто используемые функции можно помещать в библиотеки. Таким образом, создаются более простые в отладке и сопровождении программы.
Следующим шагом в повышении уровня абстракции программы является группировка функций и связанных с ними данных в отдельные файлы (модули), компилируемые раздельно. Получившиеся в результате компиляции объектные модули объединяются в исполняемую программу с помощью компоновщика. Разбиение на модули уменьшает время перекомпиляции и облегчает процесс отладки, скрывая несущественные детали за интерфейсом модуля и позволяя отлаживать программу по частям (или разными программистами).
Модуль содержит данные и функции их обработки. Другим модулям нежелательно иметь собственные средства обработки этих данных, они должны пользоваться для этого функциями первого модуля. Для того чтобы использовать модуль, нужно знать только его интерфейс, а не все детали его реализации. Чем более независимы модули, тем легче отлаживать программу. Это уменьшает общий объем информации, которую необходимо одновременно помнить при отладке. Разделение программы на максимально обособленные части является сложной задачей, которая должна решаться на этапе проектирования программы.
Модульность в языке C++ поддерживается с помощью директив препроцессора, пространств имен, классов памяти, исключений и раздельной компиляции (строго говоря, раздельная компиляция не является элементом языка, а относится к его реализации).
2.5Задача взаимного исключения
Теперь рассмотрим задачу взаимного исключения. Чтобы упростить рассуждения, эта задача будет сформулирована только для двух параллельных потоков. Сначала предположим, что два параллельных потока работают с одним и тем же ресурсом, который в этом случае называется разделяемым или совместно используемым ресурсом. Далее считаем, что в каждом потоке программный код, который осуществляет доступ к этому ресурсу, заключен в свою критическую секцию. Тогда задача взаимного исключения для двух потоков может быть сформулирована следующим образом: обеспечить двум потокам взаимоисключающий доступ к некоторому совместно используемому ресурсу. Причем решение этой задачи должно удовлетворять следующим требованиям:
□ требование безопасности — в любой момент времени в своей критической секции может находиться только один поток;
□ требование поступательности — потоки не могут блокировать работу друг друга, ожидая разрешения на вход в критическую секцию;
□ требование справедливости — каждый поток получает доступ в критическую секцию за ограниченное время.
Ниже приведено простейшее из известных решений задачи взаимного исключения для двух потоков, которое было опубликовано Гэри Л. Петерсоном в 1981 году.
boolxl = false;
boolx2 = false;
intq; // номер потока, которому предоставляется очередь входа в
// критическую секцию
voidthread_l() // потокthread_l
{
while(true)
{
non_critical_section_l(); // кодвнекритическойсекции
xl = true; // поток thread_l хочет войти в критическую секцию
q = 2; // предоставить очередь потоку thread_2
while(х2 &&q == 2); // ждем, пока в критической секции находится
// поток thread_2
critical_section_l(); // входим в критическую секцию
xl = false; // поток thread_l находится вне критической секции
}
}
void thread_2() // поток thread_2
{
while(true)
{
_critical_section_2(); // код вне критической секции
х2 = true; // поток thread_2 хочет войти в критическую секцию
q = 1; // предоставить очередь потоку thread_l
while(xl&&q == 1); // ждем, пока в критической секции находится
// поток thread_l
critical_section_2(); // входим в критическую секцию
х2 = false; // поток thread_2 находится вне критической секции
3.5 Задача. Найти и вывести на экран минимальный элемент в массиве A и его номер
#include <iostream>
using namespace std;
const int N=5;
int main ()
{
int array []= {4,0,-3,1,5};
int smallest = array[0]0;
for ( int i=1; i < sizeof(array)/sizeof(array[0]); ++i )
if ( array[i] <array[smallest] )
smallest = i ;
cout <<array[smallest]<< '\n' ;
cout << smallest << '\n' ;
return 0;
}
1.8 Наследование
При большом количестве никак не связанных классов управлять ими становится невозможным. Наследование позволяет справиться с этой проблемой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Механизм наследования классов позволяет строить иерархии, в которых производные классы получают элементы родительских, или базовых, классов и могут дополнять их или изменять их свойства.
Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных черт. Множественное наследование позволяет одному классу обладать свойствами двух и более родительских классов.
Виды наследования
При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулируется с помощью модификаторов наследования private, protected и public:
classимя : [private | protected | public] базовый_класс{тело класса};
Если базовых классов несколько, они перечисляются через запятую. Перед каждым может стоять свой модификатор наследования. По умолчанию для классов он private, а для структур - public.
Если задан модификатор наследования public, оно называется открытым. Использование модификатора protected делает наследование защищенным, а модификатора private - закрытым. Это не просто названия: в зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
Кроме спецификаторов доступа private и public для любого элемента класса может также использоваться спецификатор protected, который для одиночных классов, не входящих в иерархию, равносилен private. Разница между ними проявляется при наследовании.
Private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса.
Элементы protected при наследовании с ключом private становятся в производном классе private, в остальных случаях права доступа к ним не изменяются.
Доступ к элементам public при наследовании становится соответствующим ключу доступа.
Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе, объявив их в секции public производного класса с помощью операции доступа к области видимости:
class Base{...
public: void f();};
class Derived : private Base{...
public: Base::void f();};
Простое наследование
Простым называется наследование, при котором производный класс имеет одного родителя. Для различных элементов класса существуют разные правила наследования. Рассмотрим наследование классов на примере.
Создадим производный от класса monster класс daemon, добавив полезную в некоторых случаях способность думать:
enum color {red, green, blue};
// ------------- Класс monster -------------
class monster
{
// ------------- Скрытыеполякласса:
int health, ammo;
color skin;
char *name;
public:
// ------------- Конструкторы:
monster(int he = 100, int am = 10);
monster(color sk);
monster(char * nam);
monster(monster &M);
// ------------- Деструктор:
~monster() {delete [] name;}
// ------------- Операции:
monster& operator ++(){++health; return *this;}
monster operator ++(int)
{monster M(*this); health++; return M;}
operator int(){return health;}
bool operator >(monster &M)
{
if( health>M.get_health()) return true;
return false;
}
monster& operator = (monster &M)
{
if (&M == this) return *this;
if (name) delete [] name;
if (M.name)
{
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
return *this;
}
// ------------- Методы доступа к полям:
intget_health() const {return health;}
intget_ammo() const {return ammo;}
// ------------- Методы, изменяющиезначенияполей:
void set_health(int he){ health = he;}
void draw(int x