Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Программирование на языке Си. Учебно-методическое пособие
.pdf
51
7. Функции и рекурсия
Функция – относительно самостоятельная часть программы, со своим
именем, оформленная соответствующим образом. Написать достаточно
большую программу без использования функций проблематично, так как:
можно реализовывать каждый блок программы независимо от других,
автономно его тестировать и отлаживать;
повышаются надежность программы;
повышается наглядность программы;
увеличивается универсальность;
можно создавать библиотеки функций.
Все объекты, определенные в теле функции, ограниченном открывающей и
закрывающей фигурными скобками, являются локальными для этой функции в
смысле области видимости и времени существования.
Функции являются независимыми друг от друга, вложенных функций в
языке Си нет.
Для организации связи между функциями используется аппарат
формальных и фактических параметров. Глобальные или внешние переменные
использовать нежелательно во избежание побочных эффектов. В языке Си нет
процедур, но функция может иметь тип void.
Определяя функцию, прежде всего, необходимо указать ее имя и тип
возвращаемого значения, задать список формальных параметров и определить
тип каждого из них. Такую совокупность описаний принято называть
заголовком функции. Вслед за ним должно размещаться тело функции,
представляющее собой правильный блок, т. е. набор описаний и операторов,
заключенных в фигурные скобки.
Функция может быть описана перед главной функцией main() или после
неё, в этом случае до вызова функции нужно указать её прототип. Прототип
функции должен указываться до вызова функции для того, чтобы компилятор
мог выполнить проверку соответствия типов аргументов и параметров.
Прототип функции по форме такой же, как и заголовок функции, но в конце
него ставится точка с запятой, а тело фунции не приводится. Тогда сама
функция вместе с телом может быть описана после главной функции main().
Можно сделать процедуру без параметров (скобки все равно обязательны),
но она будет всегда выполнять совершенно одинаковые действия (если не
используются глобальные переменные).
Пример функции, выводящей строку из 80 звездочек.
void write() {
int i;
printf("\n");
for(i=1;i<80;i++)
printf("%c",'*');

52
printf("\n");
}
Формальными параметрами мы будем называть аргументы функции,
стоящие в ее заголовке. Фактическими же параметрами являются произвольные
выражения, значения которых передаются формальным параметрам при
обращении к функции. Таким образом, реализуется возможность передачи
необходимой информации в вызываемую функцию.
Пример функции, выводящей n заданных символов
void write(int n, char c) {
int i;
printf("\n");
for(i=1;i<n;i++)
printf("%c",c);
printf("\n");
}
В свою очередь, имя вызываемой функции может служить носителем
выходного значения, получаемого в результате работы этой функции, делая
его доступным в точке вызова.
Выполнение инструкций в теле функции начинается с самого первого
оператора и продолжается до тех пор, пока не встретится оператор возврата
return, либо пока не будет достигнут конец внешнего блока. Возвращаемое
функцией значение равно значению выражения в операторе return, а при его
отсутствии считается неопределенным.
Пример функции вычисления факториала
long long fakt(int x) {
long long f=1;
for(int i=2; i<=x; i++)
f*=i;
return f;
}
Пример функции вычисления значения синуса через ряд Тейлора sin(x) = x x3/3! +x5/5! – x7/7!... с точностью eps=0.0001. Точность считается достигнутой,
когда очередной элемент ряда по модулю становиться меньше eps.
#include <stdio.h>
const double P2=2*3.141592658979;
double sinus(double x); //прототип функции
main(){
float x=10;
printf("sinus(%f) = %f\n",x,sinus(x));

53
}
double sinus(double x) {
double s=0, e, c, f=1;
int z=1, n=2;
while(x>2*3.141592658979) x-=2*3.141592658979; //Нормируем х
c=x;
do {
e=c/f;
s+=e*z;
z=-z; //смена знака
c*=x*x; //числитель
f*=n*(n+1);//знаменатель
n+=2;
}
while (e>0.0001);
return s;
}
Память под размещение локальных переменных выделяется динамически
из программного стека.
Поскольку всякая функция работает лишь с копиями значений своих
аргументов, а не с их адресами, то никакие изменения значений формальных
параметров в теле функции не могут отразиться на значениях фактических
параметров. Это в свою очередь означает, что аргументы функции являются
носителями лишь входной информации и не могут быть использованы для
передачи результатов ее работы вызвавшей функции.
Побочные эффекты при использовании функций
Использование глобальных объектов в функциях нежелательно –
возможны побочные эффекты. Кроме того, при изменении в функции
глобальных переменных, значения выражения может стать зависимым от
порядка операндов.
Пример
int z;
int change(int x) {
//int z;
z=2*x;
return z;
}
main() {

54
int a;
z=1; a=change(z)*change(1); printf("\n %d",a);// 4
z=1; a=change(1)*change(z); printf("\n %d",a);// 8
}
Правильно – использовать только локальные переменные (в данном примере –
сделать z локальной переменной).
Рекурсия
Рекурсия – процесс повторения чего-либо само подобным способом.
Рекурсивная функция – если она применена в теле своего же собственного
определения.
Факториал целого неотрицательного числа n (обозначается n!) определяется как
n!=(n-1)!*n при n > 0 и n! = 1 при n = 0
Пример рекурсивной функции вычисления факториала
int fact ( int n )
{
if ( n <= 1 ) return 1;
return n*fact(n-1);
}
При использовании рекурсивных процедур и функций велика опасность,
что вложенные вызовы будут продолжаться бесконечно (это похоже на
зацикливание цикла while). Поэтому в таких функциях необходимо предус-
мотреть условие, которое проверяется на каждом шаге и заканчивает
вложенные вызовы, для прекращения рекурсии.
У рекурсии есть прямой и обратный ход (погружение в рекурсию и выход
из нее).
Пример: вывод чисел от n до 1 и от 1 до n
void f(int n){
if(n<=0) return; //условие выхода из рекурсии
printf("\n%d",n); //прямой ход
f(n-1); //рекурсия
printf("\n%d",n); //обратный ход
}
Пример рекурсивной функции для определения суммы цифр, входящих в
число.
int f(int x) {
if (x<10) return x;
return f(x/10)+x%10;

55
}
Пример рекурсивной функции для перевода числа из двоичной записи в
десятичную
int f(int x) {
if (x<2) return x;
return 2*f(x/10)+(x%10);
}
main() {
int x=1010;
printf("%d",f(x));
}
При каждом рекурсивном вызове в стеке (особой области памяти)
создается новый набор локальных переменных. Поэтому при каждом вызове
затрачивается новая память и расходуется время на вызов функции и возврат из
нее. Память выделяется в стеке.
Пример функции для ввода чисел с клавиатуры до 0 и вывода их в обратном
порядке. (Рекурсия вместо массива.)
void s() {
int x;
cin>>x;
if (x) s();
cout<<x<<" ";
}
Глубина рекурсии (количество вложенных вызовов) должна была
достаточно мала. Программа, использующая рекурсию с большой глубиной,
будет выполняться долго и может вызвать переполнение стека (нехватку
стековой памяти). Поэтому если задача может быть легко решена без
использования рекурсии, рекурсию использовать нежелательно.
Например, числа Фибоначчи определяются с помощью рекуррентного
соотношения:
Первое и второе числа Фибоначчи равны 1
Для n > 2, n-e число Фибоначчи равно сумме (n − 1)-го и (n − 2)-го чисел
Фибоначчи
int fib ( int n ){
if ( n == 0 ) return 0;
if ( n == 1 ) return 1;
return fib(n-1) + fib(n-2);
}

56
Но каждый рекурсивный вызов при n > 1 порождает еще 2 вызова
функции, многие выражения (числа Фибоначчи для малых n) вычисляются
много раз. (Для n=10 будет 177 вызовов функции, для n=20 20000 вызовов.) В
данном случае функция без использования рекурсии будет гораздо
эффективнее.
int fib(int n){
int i, f1 = 1, f2 = 1, x=1;
for (i = 3; i <= n; i ++) {
x = f1 + f2;
f1 = f2; f2 = x;
}
return x;
}
Рассмотрим еще несколько примеров рекурсивных функций.
Пример рекурсивной функции для вычисления суммы чисел в массиве (если
задано количество чисел).
#define N 10
int f(int a[], int i) {
i--;
if(i<0) return 0;
return a[i]+f(a,i);
}
main() {
int a[N]={1,2,3,4,5,-6,7,-8,9,-10};
printf("%d",f(a,N));
}
Пример рекурсивной функции для вычисления суммы чисел в массиве (до
первого числа равного 0).
int f(int a[]) {
if(a[0]==0) return 0;
return a[0]+f(a+1);
}
main() {
int a[]={1,2,3,4,5,0,7,-8,9,0};
printf("%d",f(a));
}

57
Контрольные вопросы
Где может быть описана функция? Для чего объявляется прототип?
Что такое формальные и фактические параметры?
Какие переменные называют глобальными, и какие локальными?
Что такое рекурсия?
Где хранятся локальные переменные при выполнении рекурсии?
Достоинства и недостатки рекурсивных программ.
8. Указатели и динамические массивы
Указателем называется объект, содержащий адрес в памяти. Его значение
говорит о том где расположена переменная, а не о ее значении. С помощью
указателей можно одномерные и многомерные массивы, списки, деревья, и т. д.
Описывается указатель с помощью *
int *p;
основные операции
* – обращение по адресу
& – вычисление адреса
int *p, i=1;
p=&i;
i=*p;
Операции & и * обратные по своей сути i то же самое что и *&i
Размер указателя зависит не от типа переменной, а от модели памяти
(обычно 4 байта для 32 битных компиляторов).
Используются следующие виды памяти:
код программы;
статические данные – память выделяется компилятором;
динамические данные – память выделяется программистом;
стек – данные описанные внутри функций.
Пример
int *p, i, a;
*p=10; // ошибка – запись значения в неинициализированную область памяти.
i=15;
p=&i; cout<<*p; //15
a=*p; cout<<a; //15
*p=20; cout<<i; //20

58
Прежде чем использовать указатель, его необходимо
проинициализировать – присвоить значение какой-либо выделенной области
памяти или выделить новую.
Пример
int i, *p1, *p2;
p1=&i;
p2=p1;
Функции работы с памятью в С и С++
p=(тип)malloc(размер);
размер – в байтах, возвращает адрес начала выделенной области, или NULL
если выделить память не удалось.
p=(int *)malloc(100);
p=(int *)malloc(25*sizeof(int));
Еще одна функция выделения памяти:
p=(тип)calloc(размер элемента, количество элементов);
p=(int *)calloc(sizeof(int),25);
дополнительно обнуляет выделенную область памяти
При выделении памяти желательно контролировать результат
if((p=(int *)malloc(1000000)) == NULL) exit(1);
free(адрес)
освобождает ранее выделенную область памяти. после освобождения памяти
значение указателя не изменяется, но использовать его уже нельзя.
Можно дополнительно написать z=NULL;
p=(тип)realloc(адрес,новый_размер);
изменяет размер ранее выделенного блока памяти, данные в памяти
сохраняются.
p=(int *)realloc(p,200);
при увеличении – обычно новый адрес, при уменьшении – тот же.
В С++ можно использовать операторы new и delete, выделяющие и
освобождающие память
p=new int; //выделение памяти под целое значение
p=new int[10]; //выделение памяти под 10 целых значений
delete p;
Пример
int *x, *y, *z;
x= new int;
y= new int;

59
z= new int;
*x=1; *y=2; *z=3;
y=x; // ошибка, потеря области памяти.
*y=*z; все стали равны 3
Динамические массивы
С помощью указателей можно создавать динамические массивы – то есть
массивы, память под которые выделяется по ходу работы программы в
динамической области памяти.
Пример программы, формирующий массив из 10 элементов целого типа, и
копирующей четные элементы в один динамический массив, нечетные – в
другой.
#include <iostream>
#include <stdlib.h>
using namespace std;
main() {
int m1[10];
int *m2, *m3, n=0, m=0;
for(int i=0;i<10;i++) {
m1[i]=rand()%50;
cout<<m1[i]<<" ";
if(m1[i]&1) n++; else m++;
}
cout<<endl;
m2=(int *)malloc(n*sizeof(int));
m3=new int[m];
for(int i=0,j=0,k=0; i<10; i++)
if(m1[i]&1) m2[j++]=m1[i];
else m3[k++]=m1[i];
for(int i=0;i<n;i++) cout<<m2[i]<<" ";
cout<<endl;
for(int i=0;i<m;i++) cout<<m3[i]<<" ";
}
Отличие статических и динамических массивов:
Имя статического массива – указатель константа, имя динамического –
указатель переменная. И тот и другой указатель – на начало массива.
Под статический массив память отводится компилятором, а под
динамический – программой во время ее работы.
В предыдущем примере можно написать – m2=m1; нельзя – m1= m2;

60
Арифметические действия над указателями
int *p1, *p2, n;
p1=new int[10];
сложение указателя с числом p2=p1+4– новый указатель, адрес больше
на 4 элемента;
вычитание из указателя числа p1=p2-4; - новый указатель, адрес меньше
на 4 элемента;
вычитание из указателя другого указателя n=p2-p1 – результат равен
количеству элементов между p1 и p2.
Действия производятся в единицах типа, к которому относится указатель.
int m[10];
&m[0] то что и m
&m[9] то что и m+9
m[0] то что и *m
m[9] то что и *(m+9)
Пример использования арифметики с указателями: ввести 10 чисел в массив и
вывести их на экран
int main(){
int i, *p;
int m[10];
for(i=0; i<10; i++) scanf("%d", m+i);
p=m;
while(p<m+10) printf("%5d",*p++);
}
Можно в памяти совместить массивы различных типов.
int m[10];
char *c;
c=(char *)m; m[0]=0;
c[0]=1; // m[0]=1
c[1]=1; // m[0]=256
c[2]=1; // m[0]=65536
Пример сортировки с помощью индексного массива
#define N 10
int main(){
int i, k=1, t, j, n;
int m[N], ind[N];
for(i=0; i<N; i++) { m[i]=rand() %100; ind[i]=i; }
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
