учебное пособие. Часть1. Информатика
.pdfusing namespace std; int main ()
{
int i = 1; do
{
printf ( "\n %d", i ); i = i+2;
}
while ( i <= 10 ); return 0;
}
Для переменных, описанных вне функций, т. е. глобальных перемен- ных, по умолчанию назначается внешний класс памяти (extern). Область действия внешней переменной от точки во входном файле, где она была опи- сана, до конца этого файла. Если внутри блока или функции описана автома- тическая переменная, имя которой совпадает с именем глобальной перемен- ной, то внутри блока или функции глобальная переменная маскируется ло- кальной. Это означает, что внутри данного локального блока будет видна именно автоматическая локальная переменная. По умолчанию считается, что все функции внешние.
Для внешних переменных память отводится только один раз и остается занятой до окончания выполнения программы. К внешним переменным воз- можен доступ из других файлов. Для того чтобы внешнюю переменную ис- пользовать в другом файле, для неё следует указать спецификатор памяти extern. Пример использования внешних переменных двумя файлами при- веден в следующей программе:
// Пример pr44 #include <iostream> using namespace std; #include "pr59.cpp" int a = 1;
void f ()
{
cout << "В функции f a=" << a<< "\n"; a++;
}
int main ()
{
clrscr ();
cout << "В главной функции до вызова функции f a=" <<a << "\n";
261
f ();
cout << "В главной функции после вызова функции f a=" <<a << "\n";
f1 ();
cout << "В главной функции после вызова функции f1 a=" << a << "\n";
return 0;
}
// Пример pr45 extern int a; void f1()
{
a++;
cout << "В функции f1 a=" << a << "\n";
}
Вприведенном примере память выделяется один раз при описании внешней переменной a в файле pr44.cpp. Результаты работы этой про- граммы следующие:
Вглавной функции до вызова функции f a = 1
Вфункции f a = 1
Вглавной функции после вызова функции f a = 2
Вфункции f1 a = 3
Вглавной функции после вызова функции f1 a = 3
При создании больших программ рекомендуется все внешние описания размещать в отдельном файле с расширением .h и вставлять копию этого файла в каждый файл, где требуется описание внешних переменных.
Статическая переменная может быть внутренней и внешней. Внутрен- ние статические переменные локальны по отношению к отдельной функции, так же как и автоматические, но в отличие от последних они продолжают существовать, а не возникают и уничтожаются при каждой активизации функции. Это означает, что внутренние статические переменные являются собственной постоянной памятью для функции. Внешние статические пере- менные известны внутри оставшейся части исходного файла, в котором они описаны, однако в других файлах они не известны. Если необходимо сделать функцию недоступной из других файлов, то ей следует приписать специфи- катор static. В качестве примера рассмотрим следующую программу:
//Пример pr46 #include <iostream> using namespace std; void f ()
262
{
int a = 1; static int s = 1;
cout << "a=" << a << "\ts=" << s << "\n"; a++;
s++;
}
int main ()
{
int i;
for ( i = 1; i <= 5; i++ )
{
cout << "Итерация " << i << ":\n"; f();
}
return 0;
}
В результате работы этой программы получим следующее:
Итерация 1:
a = 1 |
s = 1 |
Итерация 2: |
s = 2 |
a = 1 |
|
Итерация 3: |
s = 3 |
a = 1 |
|
Итерация 4: |
s = 4 |
a = 1 |
|
Итерация 5: |
s = 5 |
a = 1 |
Спецификатор register может использоваться только для автомати- ческих переменных [5]. Такой спецификатор говорит о том, что пользователь желает разместить переменные не в оперативной памяти, а в регистрах ком- пьютера. Так как количество регистров ограничено, то в случае избыточно-
сти описаний слово register будет игнорировано. Кроме того, нельзя об- ращаться к адресу таких переменных. Спецификатор register рекоменду- ется использовать для переменных, к которым происходит интенсивное об- ращение. Полученный в результате код будет выполняться быстрее и станет более компактным. Область действия и время существования регистровых переменных определяетя так же, как у автоматических.
Следует заметить, что при отсутствии явной инициализации перемен- ных внешние и статические переменные будут инициализироваться нулем, а
автоматические и регистровые переменные будут иметь неопределенные
263
значения (мусор). Не рекомендуется инициализировать автоматические мас- сивы.
10.2.Расширение области видимости имен
Спомощью операции :: расширяется зона видимости имен. Появляется доступ к глобальным переменным, скрытым локальными переменными. Рас- смотрим фрагмент программы:
int i = 0; int f () { ...
int i = 0;
i++; // локальная ::i++; // глобальная
...
}
В следующем примере автоматическая переменная a в функциях f и main маскируют глобальную переменную a.
//Пример pr47 #include <stdio.h> float a = 1.3;
int f ( int c ) {int a = c+13;
printf ( "В функции f a локальная=%d\n", a ); printf ( "В функции f a внешняя=%f\n", ::a ); return a;
}
void main ()
{
int a = f (13);
printf ( "В функции main a локальная=%d\n", a ); printf ( "В функции main a внешняя=%f\n", ::a );
}
10.3.Перегрузка функций
Вязыке С каждая функция должна иметь свое уникальное имя. В языке С++ разрешается определять две или несколько функций с одинаковыми именами. Цель перезрузки функций состоит в том, чтобы функции с одина- ковыми именами по-разному выполнялись и возвращали различные значения при обращении к ним. К какой функции в данный момент происходит обра-
264
щение, определяется по контексту, так как каждая фукция имеет оригиналь- ный набор параметров. Функции могут иметь как разные по типу, так и по количеству фактические параметры. В качестве примера рассмотрим функ- ции обмена местами для переменных различных типов.
//Пример pr48 #include <iostream> using namespace std;
//Функция обмена для переменных типа float void sw ( float *x, float *y )
{
float z; z = *x; *x = *y; *y = z;
}
//Функция обмена для переменных типа int void sw ( int *x, int *y )
{
int z; z = *x;
*x = *y; *y = z;
}
//Функция, вычисляющая z по формуле
float sw ( int a)
{
float z;
z = a*3.14; return z;
}
int main ()
{
float x,y;
int a1 = 9, a2 = 8;
cout << "Введите x y"; cin >> x >> y;
float z;
z = sw (a1);
cout << "z=" << z; sw ( &a1 ,&a2 ); sw ( &x, &y );
265
cout << "\n" << x << "\t" << y << "\n" <<a1<<"\t"<<a2; return 0;
}
Судя по этому примеру легко различить, какая функция, в какой мо- мент будет вызываться. Перегружаемые функции не могут различаться толь- ко по типу возвращаемого результата, обязательно отличие по параметрам. В
ранних версиях языка использовался перед именем функции спецификатор overload, теперь он является не только необязательным, но и нежелательным из-за несовместимости с некоторыми версиями языка. Чаще всего перегрузку функций используют, когда возникает необходимость выполнить одни и те же действия над переменными различных типов.
10.4.Шаблоны функций
Вотличие от перегруженных функций шаблоны позволяют не пи- сать один и тот же код программы для переменных различного типа. Шаблоны служат для автоматизации создания функций, которые могут обрабатывать разнотипные данные. Шаблон функций представляет со- бой обобщенное определение, из которого компилятор может автомати- чески генерировать код функции. В отличие от механизма перегрузки, когда для каждой сигнатуры определяется своя функция, шаблон семей- ства функций определяется один раз, но это определение параметризу-
ется [4]. Параметризировать в шаблоне функций можно тип возвращае- мого функцией значения и типы любых передаваемых параметров, ко- личество и порядок размещения которых должны быть фиксированы.
Для параметризации используется список параметров шаблона.
В определении шаблона семейства функций используется служебное слово template. Для параметризации применяется список формальных па- раметров шаблона, который заключается в угловые скобки <>. Каждый
формальный параметр шаблона обозначается ключевым словом class, за которым следует имя параметра (идентификатор), обозначающего тип [12]. Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин различных типов:
template <class type>
type abs (type x) {return x>0 ? x: -x;}
Шаблон семейства функций состоит из двух частей – заголовка шаблона: template <class type>
и из обыкновенного определения функции, в котором тип возвращае-
мого значения и типы любых параметров обозначаются именами параметров шаблона, введенных в его заголовок. Здесь type используется как имя пара- метра шаблона. Те же имена параметров шаблона могут использоваться и в теле определения функции для обозначения типов локальных объектов.
266
Например, функция обмена переменных местами будет выглядеть сле- дующим образом:
template <class T> void swap(T *x, T *y)
{
T z; z=*x; *x=*y; *y=z;
}
Здесь T – параметр шаблона функций. Он используется не только в за- головке для спецификации формальных параметров, но и в теле функции для обозначения типа переменных.
Шаблон семейства функций служит для автоматического формирова- ния конкретных определений функций по тем вызовам, которые компилятор обнаруживает в тексте программы. Например, для вышеприведенной функ- ции возможны следующие вызовы:
long k = 4, d = 8; swap ( &k, &d ); int a = 1, b =2; swap ( &a, &b );
Компилятор в соответствии с вызовом сформирует определение функ- ции. Следующая программа демонстрирует использование шаблонов на при- мере сортировки элементов массива: сначала в одномерном массиве из целых чисел, а затем – из вещественных.
//Пример pr49 #include <iostream> using namespace std;
//Функция сортировки элементов массива методом выбора template <class type>
void sort ( int n, type d[] )
{
int im; type w;
for ( int i = 0; i<n; i++ )
{
im = i;
for ( int j = i+1; j<n; j++ ) im = d[im] < d[j] ? im:j; w = d[im];
d[im] = d[i];
267
d[i] = w;
}
}
int main ()
{
int n = 4;
static int x[] = {10, 20, 30, 14}; sort ( n, x );
cout << "\nМассив Х:"; for ( int I = 0; i<n; i++ ) cout << "\t" << x[i];
static float y[] = {10.3, 20.5, 10.5, 15.2, 1.7, 8.3}; sort ( 6, y );
cout << "\nМассив Y:"; for ( i = 0; i<6; i++ ) cout << "\t" << y[i]; return 0;
}
10.5.Значения фоpмальных паpаметpов по умолчанию
ВС++ фоpмальным паpаметpам функций могут быть пpисвоены значе- ния по умолчанию. Если параметр имеет значение по умолчанию, то все па- раметры, стоящие справа от него тоже должны иметь значение по умолчанию
[7].Эти значения передаются функции, когда данный параметр не указан при ее вызове.
Предположим, что вы написали функцию drawcircle, которая рисует на экране окружность, и задали ее прототип:
void drawсircle (int x = 100, int y =100, int r=100)
или
void drawсircle ( int = 100, int = 100, int = 100 ).
Тогда вызовы этой функции будут проинтерпретированы в зависимо- сти от передаваемых параметров:
drawcircle(); //Окружностьсцентром(100,100),радиусом100 drawcircle (200);//Окружностьсцентром(200,100),радиусом100 drawcircle(200,300);//Окружностьсцентром(200,300),радиусом100 drawcircle(200,300,200);//Окружностьсцентром(200,300),радиусом200
drawcircle ( , , 50); //Ошибка
268
10.6. Подставляемые функции
Спецификатор inline используется перед определением для того, чтобы компилятор помещал ее код непосредственно в место вызова функции. Чаще всего применение inline-подстановок ускоряет решение задачи. В традицион- ном языке С употреблялись директивы препроцессора – макросы, их приме- нение усложняло программу. Рассмотрим пример использования подстав- ляемой функции в следующем фрагменте программы:
inline int FCUBE (int x)
{
return x*x*x;
}
……
b = FCUBE (a);
c = FCUBE (a++);
Функции со спецификатором inline не может содержать операторов управления if, for, case, switch, while. Подставляемая функция, функция, вы- зов которой заменяется (необязательно) при компиляции ее телом, то есть последовательностью реализующих ее команд. Тем самым при многократных вызовах подставляемой функции размеры программы увеличиваются, однако исключаются затраты на передачи управления к вызываемой функции и воз- враты из нее. Наиболее эффективно использовать подставляемые функции в тех случаях, когда к функции приходится часто обращаться.
10.8. Рекурсивные функции
Рекурсивным называется объект, частично состоящий или определяе- мый с помощью самого себя [5]. Рекурсия встречается не только в математи- ке, но и в повседневной жизни. Например, рекламная картинка, содержащая свое собственное изображение, или проблема курицы и яйца: курица вылу- пилась из яйца, но яйцо снесла курица, которая, в свою очередь, вылупилась из яйца, и т. д.
Если функция вызывает саму себя, то говорят, что возникает рекурсия. Рекурсию можно рассматривать просто, как еще одну управляющую струк- туру − управление из точки рекурсивного вызова передается на начало функ- ции. В действительности же рекурсия − это мощный инструмент для разра- ботки программ, с помощью которого даже большие объёмы действий могут быть записаны всего несколькими строками программного кода.
Рекурсией следует пользоваться осторожно и внимательно. При входе в обычную функцию выход из неё происходит всегда раньше, чем повторный вход, а для рекурсивной функции это необязательно. При ис-
269
полнении рекурсивного вызова система сохранит в стеке значения фор- мальных параметров и локальных переменных функции. При заверше- нии рекурсивного вызова будут восстановлены из стека значения фор- мальных параметров и локальных переменных функции, управление пе- редается оператору, стоящему непосредственно за оператором вызова. Стек − это специальная область памяти, характерной особенностью ко- торой является такая организация, что данные, поступившие первыми, считываются последними. Эта особенность и обеспечивает возможность
восстанавливать соответствующие значения локальных переменных функции и параметров при многократном рекурсивном вызове. Для раз- мещения в стеке локальных переменных и параметров необходимы па- мять и время.
С помощью рекурсии можно объяснить или определить многие понятия. В некоторых случаях рекурсивное построение алгоритма явля- ется наиболее естественным и экономичным путем решения проблемы. В тех же случаях, когда есть очевидное итерационное решение, рекур- сивный подход применять не рекомендуется. Различают прямую и кос- венную рекурсию. Если в теле функции явно используется вызов этой функции, то имеет место прямая рекурсия. Функция называется косвен- но рекурсивной в том случае, если она содержит обращение к другой функции, содержащей прямой или косвенный вызов определяемой (пер- вой) функции.
Следующая программа является классическим примером для объясне- ния рекурсии, но при этом не является примером для подражания.
//Пpогpамма, вычисляющая n!, используя рекурсивную функцию
//Пример pr50 #include <cstdio>
using namespace std;
long fac ( int n ) // Описание рекурсивной функции
{
if ( n == 0 )
{ // Тривиальное решение return 1;
}
return n*fac (n-1); //Рекурсивный вызов
}
int main ()
{
int n; long fact;
puts ( "Введите n : " ); scanf ( "%d", &n );
270
