Характеристики текстовых и двоичных файлов

Компонент объединения может быть любого типа С++, кроме типа объявляемого объединения. К объединениям применяются те же операции, что и к структурам. Основное назначение объединений – экономия памяти. Включение в компоненты объединения данных, которые в программе обрабатываются последовательно, позволяет этим данным использовать общий участок памяти.

Пример объявления объединения:

union

{

int a[1000];

float b[2000];

};

8. Модульное программирование

8.1. Нисходящее проектирование и программирование

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

Метод нисходящего проектирования заключается в следующем:

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

· Далее каждую подзадачу разбивают на свои подзадачи. После этого решение каждой подзадачи описывается в терминах выделенных из нее подзадач.

· Такое разбиение выполняют до тех пор, пока каждая подзадача не станет элементарной.

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

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

При модульном программировании, как правило, используют метод нисходящего программирования. Метод нисходящего программирования заключается в следующем:

· Вначале кодируется модуль верхнего уровня, а модули нижнего уровня заменяются «заглушкам» – модулями, не выполняющими никаких действий. Выполняется тестирование и отладка модуля верхнего уровня.

· Далее последовательно каждая «заглушка» первого уровня заменяется реальным кодом, а вызываемые из нее модули второго уровня заменяются «заглушками». Последовательно отлаживаются модули первого уровня.

· Процесс последовательно применяется ко всем уровням программы.

Модульное программирование используется для достижения следующих целей:

· уменьшения сложности программирования;

· уменьшения размера программы за счет повторного использования кода: алгоритм решения некоторой подзадачи оформляется в виде одного модуля, который многократно вызывается для выполнения подзадачи с разными данными;

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

· уменьшения срока разработки программы путем распараллеливания работы между несколькими программистами: программисты могут программировать разные выделенные подзадачи (модули) относительно независимо;

· возможности использования кода разработанного модуля для других программ;

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

Для использования модульного программирования при кодировании программы на языке программирования, в том числе и на языке С++, надо знать:

· как оформить алгоритм решения задачи в виде модуля;

· где в программе должно находиться определение модуля;

· как вызвать модуль для выполнения;

· как передать в модуль данные на обработку и как получить из модуля результаты.

В языке С++ модули называются функциями.

 

8.2. Определение и вызов функции

 

Синтаксис определения функции:

заголовок функции

{

тело функции

}

Заголовок функции задает интерфейс функции. Тело функции определяет алгоритм функции.

Синтаксис заголовка функции:

[Тип возвращаемого значения] имя функции ([список формальных параметров ])

Формальные параметры в списке заголовка функции разделяются запятыми.

Синтаксис формального параметра:

Тип имя

Тип возвращаемого функцией значения может быть любым типом С++, кроме массива (но может быть указателем на массив) и функции (но может быть указателем на функцию).

Если функция не возвращает значения, то перед ее именем указывается тип void. Если функция возвращает целое значение, то перед ее именем можно не указывать тип int. Таким образом, два заголовка функции int f() и f() эквивалентны.

Тело функции может содержать операторы, объявления и директивы препроцессора. Если функция возвращает значение, то в ее теле должен быть как минимум один оператор return вида:

return выражение;

Пример определения функции, которая имеет два целочисленных параметра и возвращает целое значение, равное сумме значений параметров:

int sum (int x, int y)

{

return x+y;

}

Функция sum возвращает целое значение, поэтому перед ее именем указан тип int, а в ее теле присутствует оператор return выражение, тип выражения также int.

Функция активизируется только в момент ее вызова. Функция вызывается по имени. Если у функции есть параметры, то при вызове после имени функции в кругых скобках указываются фактические параметры функции (аргументы функции). Аргументы указываются без типов и разделяются запятыми. Если у функции нет параметров, то она вызывается с пустыми круглыми скобками после имени функции.

Синтаксис вызова функции:

Имя функции ([список аргументов])

Пример вызова функции sum:

void main()

{

int a, b;

cout<<”sum=”<<sum(2, 5)<<endl; //вызов функции

cout<<”a, b? “; cin>>a>>b;

cout<<”sum=”<<sum(a, a+b); //вызов функции

getch();

}

Пример программы с функцией без параметров и без возвращаемого значения:

#include <iostream.h>

#include <conio.h>

// Функция выводит горизонтальную линию из 60 символов

void line( )

{

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

cout<<’-‘;

cout<<endl;

}

void main()

{

line(); //при вызове функции скобки обязательны

getch();

}

Если функция не возвращает значение, то наличие в теле функции оператора return не обязательно (функция завершает работу по последней фигурной скобке).

 

8.3. Место определения функции в программе

 

Функции в С++ не могут быть вложенными - функции в программе записываются последовательно друг за другом. Местоположение функции в последовательности функций определяется основным правилом типизированных языков: «Любое имя, в том числе и имя функции, должно быть объявлено до его использования». Компилятор использует список формальных параметров и тип возвращаемого функцией значения для проверки правильности ее вызова. Размещение определения функции до вызыва функции приводит к естественному выполнению рассмотренного правила.

Пример программы вычисления значения выражения с=n!m!/(n+m)!, в которой алгоритм вычисления факториала оформлен в виде функции:

#include <iostream.h>

#include <conio.h>

int fact (int n)

{

int f=1;

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

f*=i;

return f;

}

void main()

{

int n, m;

float c;

cout <<”n, m? ”;

cin>>n>>m;

c=(float) fact(n) * fact(m)/ fact (n+m); //функция вызывается 3 раза

cout<<”c=”<<c;

getch();

}

В С++ определение функции может находиться и после вызывающей ее функции (и даже в другом файле), что более соответствует нисходящему программированию, когда вначале кодируется и отлаживается главная функция (main), вызывающая другие функции, а затем кодируются вызываемые функции. При отладке главной функции вызываемые функции заменяются «заглушками». Размещение определения функции после ее вызова требует присутствия в программе до вызова функции прототипа (предварительного объявления) вызываемой функции. Прототип функции используется компилятором для проверки правильности вызова функции. Неправильный вызов обнаруживается на стадии компиляции, что облегчает отладку программы и повышает ее надежность.

Синтаксис прототипа функции:

тип_результата имя_функции ([список типов ФОП]) ;

или

тип_результата имя_функции ([список ФОП]);

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

Примеры прототипов функции:

int sum (int, int);

int sum (int x, int y);

Прототип функции может находиться:

· Вне функций. В этом случае он действует на все вызовы функции, расположенные ниже прототипа в любых функциях.

· Внутри одного из блоков (составного оператора) вызывающей функции. В этом случае он действует на вызовы функции из этого блока.

Желательно прототипы всех функций программы располагать в одном месте, например, в начале файла с программой или в отдельном файле (в заголовочном файле). В этом случае программа становится более наглядной, а определения функций и их вызовы функций могут следовать в любом порядке.

Пример программы вычисления коэффициента надежности теста. Программа состоит из нескольких функций.

Группа из n (n<=300) испытуемых последовательно выполняет задания двух параллельных тестов. Суммарные оценки по двум тестам записываются в двух массивах (x и y). Один из критериев качества теста – коэффициент надежности теста. Для «идеального» теста он равен 1. Коэффициент надежности теста вычисляется по формуле:

где Mx и математическое ожидание и стандартное отклонение тестовых оценок испытуемых по тесту х, My и математическое ожидание и стандартное отклонение тестовых оценок испытуемых по тесту y. Mx, , My и вычисляются по формулам:

, , ,

 

Алгоритм расчета коэффициента надежности на псевдокоде:

Ввод n

Ввод х

Ввод y

Вычисление Mx

Вычисление My

Вычисление

Вычисление

Вычисление r

Вывод r

Текст программы:

#include <iostream.h>

#include <conio.h>

#include <math.h>

void input (int a[], int n); //ввод массива

float m (int a[], int n); //математическое ожидание

float d (int a[], int n, float ma); //стандартное отклонение

float r (int a[], int b[], int n, float ma, float mb, float da,

float db); //коэффициент надежности

void main()

{

int n; //количество испытуемых

int x[300], y[300]; //массивы оценок

float mx, my ;//математические ожидания

float dx, dy; //стандартные отклонения

float rt; //коэффициент надежности

cout<<”n? ”; cin>>n;

cout<<”x? ”;

input(x,n);

cout<<”y? ”;

input(y,n);

mx=m(x,n);

my=m(y,n);

dx=d(x,n,mx);

dy=d(y,n,my);

rt=r(x, y,n,mx,my,dx,dy);

cout<<”rt=”<<rt;

getch();}

void input (int a[],int n)

{

for (int i=0; i<n; i++)

cin>>a[i];

}

float m (int a[],int n)

{

int s=0;

for (int i=0; i<n; i++)

s+=a[i];

return (float)s/n;

}

float d (int a[],int n, float ma)

{

float s=0;

for (int i=0; i<n; i++)

s+=(ma-a[i])*( ma-a[i]);

return sqrt(s/n);

}

float r (int a[], int b[], int n, float ma,float mb, float da, float db)

{

float s=0;

for (int i=0; i<n; i++)

s+=(a[i]-ma)*( b[i]-mb);

return n*s/(da*db);

}

 

8.4. Обмен данными между функциями

 

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

Для обмена данными между функциями в С++ используются следующие способы:

· оператор return;

· глобальные переменные;

· аппарат формальных и фактических параметров.

С помощью оператора return можно передать в вызывающую функцию единственный результат работы функции.

 

8.4.1. Использование глобальных переменных

Переменная, объявленная вне функций, называется глобальной. Переменная, объявленная в функции, в том числе и в заголовке функции, называется локальной.

Пример использования глобальной переменной двумя функциями: одна функция изменяет значение этой переменной, другая – выводит значение глобальной переменной на экран:

#include <conio.h>

#include <iostream.h>

int a=1; //глобальная переменная

void f1(int x) / x – локальная переменная

{

int y=10; //локальная переменная

a=x+y;

cout<<”x=”<<x<<” a=”<<a<<endl; //x=2 a=12

}

int main( )

{

int z=2; //локальная переменная

//Вывод значения глобальной переменной

cout<<”a=”<<a<<endl; //a=1

f1(z); //вызов функции

cout<<”a=”<<a; <<a=12

getch();

}

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

Локальные переменные имеют, как правило, локальную продолжительность жизни: память для них выделяется при входе в блок (составной оператор), а освобождается – при выходе из блока.

Глобальную переменную можно использовать от точки ее объявления до конца файла с текстом программы, за исключениием функций, где переменная переобъявлена. Таким образом, глобальные переменные можно использовать для обмена данными между функциями. Например, одна функция может задавать значение глобальной переменной, другая – изменять это значение, а третья – выводить значение переменной на экран.

Пример программы, которая вводит последовательность из n целых чисел и выводит на экран все простые числа этой последовательности:

#include <conio.h>

#include <iostream.h>

int x; //глобальная переменная

bool simple(); //прототип функции проверки числа: простое?

void main()

{

int n; //количество чисел

bool ok=false; //true – простое, false - нет

cout <<”n? ” cin>>n;

for (int i=0; i<n; i++)

{

cout <<”x? ”;

cin>>x;

if (simple())

{ cout<<x<<’ ‘; ok=true; }

}

if (!ok)

cout<<”No”;

getch();

}

bool simple()

{

int d=2; //пробный делитель

while (d<=sqrt(x) && x%d!=0)

d++;

if (d<=sqrt(x) || x==1)

return false;

return true;

}

Тесты:

n=6, a: 2 13 4 1 121 20 результат: 2 13

n=6, a: 4 15 65 22 24 100 результат: “No”

Недостатки использования глобальных переменных:

· не позволяют создавать гибкие параметрические функции;

· снижают надежность программы: если глобальная переменная должна использоваться в функции только как входная, то можно непреднамеренно изменить значение переменной внутри функции;

· усложняют распределение работы над программой между несколькими программистами.

 

8.4.2. Использование аппарата формальных и фактических параметров

 

Формальные параметры – это переменные, которые объявляются в заголовке функции при ее определении. Список формальных параметров определяет: количество данных, которое должна получить или вернуть функция, типы этих данных и порядок следования данных. Фактические параметры (аргументы) функции указываются в круглых скобках при вызове функции после ее имени. Они задают конкретные данные, которые передаются в вызываемую функцию, или переменные, в которые вызываемая функция передает результаты своей работы.

В С++ существуют два способа передачи параметров в функцию:

· по значению;

· по ссылке.

При передаче параметров по значению в формальные параметры функции копируются значения фактических параметров. Функция работает с копиями фактических параметров, не изменяя сами фактические параметры. Поэтому такой способ передачи параметров используется только для получения функцией входных данных и не может быть использован для получения от функции результатов.

Синтаксис формального параметра-значения:

тип имя_параметра

Пример прототипа функции с формальным параметром-значением:

float f (int x);

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

При передаче параметров функции по ссылке в формальный параметр функции передается не копия значения фактического параметра, а его адрес. Термин «ссылка» означает ссылку на область памяти. Функция, работая с формальным параметром, обращается по указанному в формальном параметре адресу и изменяет значение фактического параметра. Таким образом, формальные параметры-ссылки могут использоваться для получения из функции выходных данных (результатов работы функции).

Для реализации передачи по ссылке используются два способа:

· связь через формальный параметр-указатель;

· связь через ссылочный формальный параметр.

Пример программы вычисления суммы и разности двух целых чисел, в которой используется передача выходных данных функции через указатели:

#include <conio.h>

#include <iostream.h>

void sumraz(int a, int b, int *s, int *r) //s сумма a и b, r - разность

{

*s=a+b;

*r=a-b;

}

void main()

{

int x, y, sum, raz;

cout<<"x,y? "; cin>>x>>y;

sumraz(x,y,&sum, &raz); //вызов функции

cout<<sum<<’ ‘<<raz;

getch();

}

При вызове функции sumraz в формальные параметры-указатели s и r передаются адреса переменных sum и raz: &sum и &raz. Функция sumraz с помощью операций разыменования (косвенной адресации) *s и *r изменяет значения, которые находятся по полученным параметрами s и r адресам.

Пример программы вычисления суммы и разности двух целых чисел, в которой используется передача выходных данных функции через параметры-ссылки:

#include <conio.h>

#include <iostream.h>

void sumraz(int a, int b, int &s, int& r)

{

s=a+b;

r=a-b;

}

void main()

{

int x, y, sum, raz;

cout<<"x,y "; cin>>x>>y;

sumraz(x,y,sum,raz); //вызов функции

cout<<sum<<’ ‘<<raz;

getch(); }

При использовании ссылочных параметров &s и &r компилятор автоматически трактует имена формальных параметров как адреса аргументов. Все действия с параметром-ссылкой в теле функции происходят в действительности с переменной, которая является аргументом функции. Таким образом, параметр-ссылка автоматизирует механизм связи параметров по ссылке. Синтаксис становится проще и, следовательно, в программе будет меньше ошибок.

Передавать по ссылке можно не только выходные данные функции, но и входные. Например, функция sumraz может быть такой:

void sumraz(int & a, in& b, int &s, int& r)

{s=a+b; r=a-b;}

При использовании способа передачи параметров по ссылке аргументы функции не могут быть выражениями – они могут быть только переменными.Это связано с тем, что значением ссылочной переменной может быть только адрес другой переменной. Например, нельзя для последнего варианта функции использовать ее вызов в виде: sumraz(5,4,sum,raz);

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

сonst тип& имя_параметра

Заголовок функции sumraz с входными константными ссылочными параметрами:

void sumraz(const int &a, const int &b, int &s, int& r)

В случае константных ссылочных параметров компилятор при обнаружении в функции операторов, изменяющих константные формальные параметры функции, выдает сообщение об ошибке – ошибка в коде обнаруживается до выполнения программы.

8.4.3. Передача массивов в функцию

Чтобы передать массив в функцию надо указать его имя в списке аргументов функции при ее вызове, например, для вызова функции сортировки целочисленного массива a из 100 элементов используется синтаксис:

sort(a, 100);

Так как в С++ имя массива – это константный указатель, значением которого является адрес первого элемента массива, то в этом вызове в функцию передается адрес массива. Массив всегда передается в функцию по ссылке (в функцию передается адрес массива-аргумента). Формальный параметр функции для массива должен иметь синтаксис:

тип элементов имя [ ]

или

тип элементов имя [максимальный размер массива]

или

тип элементов *имя

Примеры задания формальных параметров-массивов:

void sort (int a[], int n)

void sort (int a[100], int n)

void sort (int *a, int n)

Если в теле функции изменяются значения элементов массива, то изменяется сам передаваемый через параметр массив.

Пример программы вычисления суммы максимальных элементов двух целочисленных массивов из 20 и 15 элементов. В программе алгоритм поиска максимального элемента целочисленного массива из n элементов оформлен в виде функции.

#include <conio.h>

#include <iostream.h>

int max (int a[ ], int n); //поиск максимального элемента массива

void main()

{

int b[20], c[15];

int i; //номер элемента

for (i=0; i<20; i++)

cin>>b[i];

for (i=0; i<15; i++)

cin>>c[i];

cout<<” sum =”<< max(b,20)+ max(c,15);

getch();

}

int max (int a[ ], int n)

{

int amax; //значение максимального элемента

amax=a[0];

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

if( a[i]>amax) amax=a[i];

return amax;

}

Пример программы удаления из текста символов, совпадающих с заданным символом. В программе алгоритм удаления символов, совпадающих с заданным символом, оформлен в виде функции.

#include <conio.h>

#include <iostream.h>

//Функция удаления из строки s символов c

void del(char s[ ], char c);

int main( )

{

char s[80]; //текст

char c; //удаляемый символ

cout<<”s? “;

cin.getline(s,80);

cout<<”c? “;

cin>>c;

del(s,c);

if (s[0]==’\0’)

cout<<”s is empty”; //строка стала пустой

else

cout<<s;

getch();

}

void del(char s[], char c)

{

int i=0; //номер сравниваемого символа строки

int j=0; //позиция символа, не совпадающего с символом с

while(s[i]!='\0') //цикл по всем символам строки

{

if (s[i]!=с)

{s[j]=s[i]; j++;}

i++;

}

s[j]='\0'; //завершение строки 0-символом

}

Тесты:

s: Ааа рaрр еaaеее, c: a результат: А ррр ееее

s: ааaaaaa, c: a результат: строка пуста

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

Синтаксис формального параметра двумерного массива:

тип элементов имя [][максимальное количество столбцов]

или

тип элементов имя [максимальное количество строк] [максимальное количество столбцов]

Пример программы удаления из матрицы целых чисел строки с заданным номером, в которой алгоритм удаления строки оформлен в виде функции:

#include <conio.h>

#include <iostream.h>

#include <iomanip.h>

//Функция удаления из матрицы a из n строк и m столбцов

// строки c номером k

void del(int a[ ][50], int &n, int m, int k);

void main( )

{

int a[100][50]; //матрица

int n, m; //количества строк и столбцов

int k; //номер удаляемой строки

cout<<”n, m? “;

cin>>n>>m;

cout<<”a? “;

for (int i=0; i<n; i++)

for (int j=0; j<m; j++)

cin>>a[i][j];

cout<<”k? “; cin>>k;

del(a, n, m, k); //вызов функции

if (n==0)

cout<<”a is empty”;

else

//Вывод матрицы в виде прямоугольника

for (int i=0; i<n; i++)

{

for (int j=0; j<m; j++)

cout<<setw(7)<<a[i][j];

cout<<endl;

}

getch();

}

void del(int a[][50], int &n, int m, int k)

{

for (int i=k; i<n-1; i++)

for (int j=0; j<m; j++)

a[i][j]=a[i+1][j];

n--;

}

В программе для форматирования вывода матрицы используется манипулятор вывода setw(7). Манипулятор определяет ширину поля вывода для любого значения a[i][j] в 7 позиций, что позволяет выровнять столбцы значений матрицы на экране. Манипулятор вывода setw доступен программе при наличии в ней директивы:

#include <iomanip.h>

8.5. Перегрузка функции

Перегрузка функции – это использование в программе одного имени для обозначения разных функций.

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

Чтобы перегрузить функцию, надо записать в программе все варианты одноименных функций, которые будут использоваться. Перегруженные функции должны отличаться друг от друга типом хотя бы одного формального параметра и (или) количеством параметров. Компилятор, анализируя аргументы функции в ее вызове, будет связывать вызов с конкретной перегруженной функцией. Иными словами: компилятор по контексту использования имени функции связывает вызов функции с нужным вариантом одноименной функции.

Пример программы удаления из двух массивов, массива целых чисел и массива строк, элементов с заданными значениями. Алгоритмы ввода, вывода и удаления элементов массивов оформлены в виде перегруженных функций.

#include <iostream.h>

#include <conio.h>

#include <string.h>

void input (int a[], int n); //ввод массива целых чисел

void input (char a[][81], int n); //ввод массива слов

void output (int a[], int n); //вывод массива целых чисел

void output (char a[][81], int n); //вывод массива слов

void del (int a[], int &n, int x); //удаление из массива чисел х

void del (char a[][81], &n, char x[]); //удаление из массива слов х

void main()

{

int c[100]; //массив чисел

char s[100][15]; //массив строк

int nc, ns; //количества чисел в массивах чисел и строк

int xc; //значение удаляемых чисел

char xs[15]; //значение удаляемых строк

cout<<”nc?”;

cin>>nc;

input(c,nc);

output(c,nc);

cout<<”xc?”; cin>>xc;

del (c,nc,xc);

output(c,nc);

cout<<”ns?”; cin>>ns;

input(s,ns);

output(s,ns);

cout<<”xs?”; cin>>xs;

del (s,ns,xs);

output(s,ns);

getch();

}

//Перегруженные функции

void input (int a[ ], int n)

{

cout<<”array int: “;

for int=0; i<n; i++)

cin>>a[i];

}

void input (char a[][15], int n)

{

cout<<”array string: “;

for int=0; i<n; i++)

cin>>a[i];

}

void output (int a[ ], int n)

{

if (n==0)

cout<<” array is empty”<<endl;

else

{

for (int i=0; i<n; i++)

cout<<a[I]<<’ ‘;

cout<<endl;

}

}

void output (char a[ ][15], int n)

{

if (n==0)

cout<<” array is empty”<<endl;

else

{

for (int i=0; i<n; i++)

cout<<a[I]<<’ ‘;

cout<<endl;

}

}

void del (int a[ ], int &n, int x)

{

int i, j;

j=0;

for (i=0; i<n; i++)

if (a[i]!=x)

{ a[j]=a[i]; j++;}

n=j;

}

void del (char a[ ][81], &n, char x[ ])

{

int i, j;

j=0;

for (i=0;i<n; i++)

if (strcmp(a[i],x)!=0)

{strcpy(a[j],a[i]); j++;}

n=j;

}

Пример программы вычисления площадей круга и прямоугольника. Алгоритмы вычисления площадей геометрических фигур оформлены в виде перегруженных функций. Перегруженные функции различаются количеством параметров.

#include <iostream.h>

#include <conio.h>

#include <math.h>

float area (float r); //вычисление площади круга

float area (float w, float h); //вычисление площади прямоугольника

void main()

{

cout<<”s1=”<< area(3,4)<<” s2=”<< area(5);

getch();

}

 

float area (float r)

{

return M_PI * r* r;

}

float area (float w, float h)

{

return w*h;

}

 

8.6. Шаблон функции

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

Назначение шаблона:

· автоматическое создание перегруженных функций;

· уменьшение размера исходного кода программы.

Шаблон следует использовать в тех случаях, когда в программе используется один и тот же алгоритм для разных типов данных и запись этого алгоритма на языке С++ одинакова.

Синтаксис определения шаблона функции:

template <class имя1, class имя2, …,class имяn>

заголовок функции

{тело функции}

Синтаксис прототипа шаблона функции:

template <class имя1, class имя2, …,class имяn>

заголовок функции;

где имя1, имя2, …,имяn… - имена параметрических типов шаблона, настраиваемых на конкретный тип аргумента при вызове функции.

Параметрический тип может использоваться в шаблоне функции для объявления:

· типа формального параметра функции;

· типа возвращаемого функцией значения;

· типа локальной переменной.

Все параметрические типы, введенные в шаблоне, должны обязательно использоваться в формальных параметрах шаблона. Использование параметрического типа только для локальных переменных и /или для возвращаемого шаблоном значения недостаточно.

Пример программы удаления из числовых массивов чисел с заданным значением. Алгоритмы ввода, вывода и удаления элементов массивов оформлены в виде шаблонов функций. Шаблоны функций тестируются для трех массивов.

#include <iostream.h>

#include <conio.h>

//Прототипы шаблонов функций

template <class t>

void input (t a[], int n);

template <class t>

void output (t a[], int n);

template <class t>

void del (t a[], int &n, t x);

void main()

{

int a[100], b[100];

float c[100];

int n; //количество чисел в массиве

int xi; //значение удаляемых из целочисленного массива чисел

float xf; //значение удаляемых из вещественного массива чисел

cout<<”n_int? ”;

cin>>n;

input(a,n);

output(a,n); //вывод исходного массива

cout<<”x_int? ”;

cin>>xi;

del (a,n, xi);

output(a,n); //вывод массива после удаления из него чисел

cout<<”n_int? ”;

cin>>n;

input(b,n);

output(b,n); //вывод исходного массива

cout<<”x_int? ”;

cin>>xi;

del (b,n, xi);

output(b,n); //вывод массива после удаления из него чисел cout<<”n_float? ”;

cin>>n;

input(c,n);

output(c,n); //вывод исходного массива

cout<<”x_float? ”;

cin>>xf;

del (c,n, xf);

output(c,n); //вывод массива после удаления из него чисел

getch();

}

//Определения шаблонов функций

template <class t>

void input (t a[], int n)

{

cout<<”array: “;

for (int=0; i<n; i++)

cin>>a[i];

}

template <class t>

void output (t a[], int n)

{

if (n==0)

cout<<” array is empty”<<endl;

else

{

for (int i=0; i<n; i++)

cout<<a[i]<<” “;

cout<<endl;

}

}

template <class t>

void del (t a[], int &n, t x)

{

int i, j;

j=0;

for (i=0; i<n; i++)

if (a[i]!=x)

{ a[j]=a[i]; j++;}

n=j;

}

Первый вызов функции, например, input(a,n), приводит к созданию варианта функции ввода целочисленного массива. При повторном вызове функции с теми же типами фактических параметров, которые встречались ранее, например, input(b,n), компилятор не создает повторно код функции ввода массива целых чисел.

В этом примере параметрический тип использовался только для формальных параметров. В следующем примере он используется и для локальной перменной шаблона функции.

Пример программы вычисления средних значений числовых массивов разных типов: целого и вещественного. Алгоритм вычисления среднего оформлен в виде шаблона функции.

#include <iostream.h>

#include <conio.h>

//Определение шаблона функции ввода массива

template <class t>

void input (t a[], int n)

{

cout<<”array int “;

for (int=0; i<n; i++)

cin>>a[i];

}

//Определение шаблона функции вычисления среднего

template <class t>

float avg (t a[], int n)

{

t s=0; // параметрический тип для локальной переменной

for (int i=0; i<n; i++)

s+=a[i];

return (float)s/n;

}

void main()

{

int b[100];

float c[100];

int n;

cout<<”n_b? ”; cin>>n;

input(b,n);

cout<<”avg_b=”<<avg (b,n) <<endl;

cout<<”n_c? ”; cin>>n;

input(c,n);

cout<<”avgb=”<<avg (c,n) <<endl;

cout<<”avgb=”<<avg (b,5);

getch();

}

8.7. Рекурсивные функции

Рекурсивная функция – это функция, которая вызывает сама себя. Такой вызов функции может произойти, если функция, выполнение которой еще не закончилось, вызывается снова. Существуют классы алгоритмов, запись которых с помощью рекурсивных функций является более компактной.

Пример программы с рекурсивной функцией, которая вычисляет n-число Фибоначчи. Последовательность чисел Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …начинается с 0 и 1, а каждое следующее число является суммой двух предыдущих чисел. Числа этой последовательности обозначаются через Fn и формально определяются следующим образом:

F0=0, F1=1, Fn=Fn-1+Fn-2, n>=2

В пределе отношение Fn/Fn-1стремится к значению 1.618033… Это значение называется золотым сеченим или божественной пропорцией. Принято считать, что объекты, содержащие в себе золотое сечение, воспринимаются людьми как наиболее гармоничные.

#include <iostream.h>

#include <conio.h>

//Рекурсивная функция вычисления n-го числа Фибоначчи

int fib(int n)

{

if (n==0 || n==1) //ветвь выхода из рекурсии

return n;

return fib(n-1)+fib(n-2); //рекурсивный вызов

}

void main()

{

int n;

cout<<"n? ";

cin>>n;

cout<<"fib="<<fib(n);

cout<< “f=”<<fib(n);

getch(); }

Любую рекурсивную функцию можно заменить нерекурсивным вариантом. Нерекурсивный вариант функции вычисления числа Фибоначчи может быть таким:

int fib (int n)

{int fnew, fold, foldold; //3 последовательных числа Фибоначчи

if (n==0 || n==1)

return n;

foldold=0; fold=1;

for (int i=2; i<=n; i++)

{

fnew=fold+foldold;

foldold=fold;

fold=fnew;

}

return fnew;

}

Недостатки рекурсивных функций:

1. Увеличение памяти на повторные вызовы функции и многократное размещение в стеке формальных параметров и локальных переменных рекурсивной функции.

2. Расход времени на многократное выполнение команд вызова функции.

3. Переполнение стека программы, при большом количестве рекурсивных вызовов.

 

8.8. Функции с параметрами по умолчанию

В С++ для того, чтобы упростить вызов функции с наиболее часто используемыми входными значениями, в заголовке функции используются параметры по умолчанию. Параметр по умолчанию – это значение, которое используется в качестве аргумента функции, если при вызове функции аргумент не указан. Для задания в функции параметра по умолчанию, надо в заголовке функции объявить формальный параметр с начальным значением. Функция может иметь несколько параметров умолчания, но они должны находиться в конце списка формальных параметров.

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

#include <iostream.h>

#include <conio.h>

//Функция поиска первого вхождения значения х в массив из

//n целых чисел начиная с позиции p

int pos (int a[], int n, int x, int p=0);

void main()

{

int a[100], n, x, p;

int number; //номер первого числа массива со значением х (или -1)

cout<<"n? "; cin>>n;

cout<<”a: ”;

for (int i=0; i<n; i++)

cin>>a[i];

cout<<"x? "; cin>>x;

number= pos(b,n,x); //по умолчанию поиск начинается с 0-позиции

if (number==-1)

cout<<”No”<<endl;

else

cout<<"number_0="<<number<<endl;

number=pos(b,n,x,3); // поиск начинается с 3-позиции

if (number==-1)

cout<<”No”<<endl;

else

cout<<"number_3="<<number;

getch();

}

int pos (int a[], int n, int x, int p)

{for (int i=p; i<n; i++)

if(a[i]==x)

return i; //возврат номера элемента

return -1; //возврат признака отсутствия вхождения элемента

}

Тесты:

а: 8 9 5 2 1 9 10, n=7, x=9, p=0 результат: number=1

а: 8 9 5 2 1 9 10, n=7, x=9, p=3 результат: number=5

а: 8 9 5 2 1 9 10, n=7, x=4, p=0 результат: number =-1

В программе функция pos вызывалась два раза: первый раз с 3 аргументами (использовалось значение умолчания), второй – с 4 аргументами.

Ограничения на использование параметров по умолчанию:

· параметры со значениями по умолчанию должны быть последними в списке формальных параметров;

· если параметров по умолчанию несколько, то при вызове можно не указывать все параметры умолчания или только последние из них;

· инициализация параметров по умолчанию должна выполняться в первомзаголовке функции (в прототипе или в определении функции) и только один раз.

8.9. Передача в функцию другой функции

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

float f1(float x)

{ return x*sin(x); }

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

Синтаксис объявления указателя на функцию:

тип результата функции (*имя указателя)

([список типов формальных параметров функции])

или

тип результата функции (*имя указателя)

([список формальных параметров функции])

Пример объявления указателя f на функцию с одним вещественным параметром, которая возвращает вещественное число:

float(*f) (float x);

Пример прототипа функции вычисления определенного интеграла:

float integ (float a, float b, int n, float (*f)(float x));

В этом объявлении: integ –имя функции вычисления значения интеграла, a и b – пределы интегрирования, n –количество разбиений отрезка интегрирования [a,b], f – указатель на функцию с одним вещественным параметром, которая возвращает значение подынтегральной функции в точке х. f - указатель, через который передается в функцию адрес конкретной подынтегральной функции.

Пример программы вычисления двух интегралов

и методом левых прямоугольников для заданного количества разбиений отрезка интегрирования. Для приближенного вычисления интеграла с пределами интегрирования a и b в программе используется формула левых прямоугольников:

,

где h=(b-a)/n.

#include <iostream.h>

#include <math.h>

#include <conio.h>

float f1(float x); //подынтегральная функция 1 интеграла

float f2(float x); //подынтегральная функция 2 интеграла

//Вычисление интеграла методом левых прямоугольников

float integ(float a,float b,int n, float (*f)(float x));

void main()

{

int n; //количество разбиений

cout<<"n? "; cin>>n;

cout<<"integ1="<<integ(0,M_PI_2,n,f1)<<endl;

cout<<"integ2="<<integ(10.5,1,n,f1);

getch();

}

float integ(float a, float b, int n, float (*f)(float x))

{

float h, x; //длина элементарного отрезка и левая граница отрезка

float s; //сумма

int i; //номер отрезка

h=(b-a)/n;

s=0;

x=a;

for (i=1; i<=n; i++)

{

s=s+f(x);

x=x+h;

}

return s*h;

}

float f1(float x)

{

return x*x*cos(x*x);

}

float f1(float x)

{

return log(sqrt(1/x));

}

9. Работа с файлами

Переменные и массивы предназначены для временного хранения данных. Для постоянного хранения данных используются файлы. Файл имеет имя и место хранения. Например, файл с именем a.txt может храниться на диске с в папке user. В этом случае c:\user\a.txt – это полное имя файла.

С файлами можно выполнять операции:

· чтение;

· создание;

· запись в конец, в начало, в середину;

· исключение части данных;

· поиск;

· вывод;

· изменение данных;

· копирование;

· переименование;

· удаление;

· другие операции.

Некоторые операции с файлами можно выполнять как средствами операционной системы, так и с помощью прикладных программ. Существуют операции, которые можно выполнить только с помощью программирования. С++ имеет языковые средства для программирования любых операций с файлами.

 

9.1. Текстовые и двоичные файлы

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

В текстовых файлах данные всех типов хранятся в текстовом формате. Например, число 21 в текстовом файле хранится как два символа: ‘2’ (с кодом 50) и ‘1’ (с кодом 49). Поэтому разные значения данных одного типа в текстовом файле могут занимать разное количество байтов, например, 2 целых числа 1234 и 34567890 занимают в текстовом файле 4 и 8 байтов соответственно. При чтении данных из текстового файла в оперативную память выполняются команды преобразования данных из текстового формата в двоичный формат. При записи данных из оперативной памяти в текстовый файл происходит обратное преобразование. На выполнения этих команд тратится время.

В двоичных файлах данные имеют такой же формат, как формат данных в оперативной памяти. Поэтому для хранения разных значений данных одного типа в двоичном файле требуется одинаковое количество байтов, например, каждое из чисел 1234 и 34567890 занимает в двоичном файле 4 байта. При чтении данных из двоичного файла в оперативную память и при записи данных в файл не требуется преобразование формата данных.

Сравнительнительные характеристики текстовых и двоичных файлов приведены в табл. 5.

 

Таблица 5

Характеристика файла Текстовый файл Двоичный файл
Возможность чтения и записи без программирования да нет
Меньший размер нет да
Меньшее время чтения и записи нет да
Произвольный доступ к данным нет да

 

9.2. Объявление файловых переменных

 

В С++ файл рассматривается как последовательный поток байтов. При выполнении операций ввода поток байтов файла пересылается от внешнего устройства, например, с дисковода, в оперативную память. При выводе данных из оперативной памяти в файл поток байтов пересылается из оперативной памяти на внешнее устройство. Для работы с файлами в С++ используются классы файловых потоков:

· ifstream - входной файловый поток для чтения данных из файла;

· ofstream - выходной файловый поток для вывода данных в файл;

· fstream двунаправленный файловый поток – для ввода и вывода.

Объявление этих классов находится в заголовочном файле fstream.h, поэтому в программу, работающую с файлами, должен быть включен файл fstream.h. Для работы с файлом надо объявить файловую переменную – переменную типа файловый поток.

Примеры объявления файловых переменных:

ifstream f1;//переменная для чтения файла

ofstream f2;// переменная для вывода в файл

fstream f3; //переменная для чтения и изменения файла

Для связывания файловой переменной с конкретным файлом на компьютере используется функция файловых классов open.

Синтаксис вызова функции open

имя файловой переменной.open(внешнее имя файла

[,режим открытия файла])

Функция имеет два параметра: внешнее имя файла и режим открытия файла. Внешнее имя файла – это полное имя файла. Основные режимы открытия файлов и способы их задания в функции open приведены в табл. 6. Второй параметр необязательный: он устанавливается по умолчанию при объявлении файловой переменной. Значения режима по умолчанию приведены в табл. 7.

 

Таблица 6