.: [предыдущая | оглавление | следующая] :.

2.12. Программы и подпрограммы

Мы уже дали определение алгоритма как последовательности действий. Теперь сосредоточим внимание возможности группирования действий, т.е. объединение действий как единого укрупненного действия. Если теперь рассмотреть алгоритм с позиций компьютера, это последовательность действий, представленная в виде последовательности команд. Объединение последовательности команд и поименование этой последовательности привело к понятию подпрограммы. Понятие «подпрограммы» расширяет понятие «команды». Можно сказать, что подпрограмма - это команда, созданная пользователем-программистом. Обычно подпрограмму специальным образом оформляют: обычно вначале записывают имя подпрограммы, а в конце команду возврата из подпрограммы. Чтобы воспользоваться подпрограммой, необходимо вызвать ее. Это делается обычно с помощью команды call. Механизм вызова следующий:

  1. адрес следующей команды за call запоминается в системном стеке;
  2. в PC записывается адрес подпрограммы, который хранится в call, и она начинает выполняться;
  3. после выполнения подпрограммы должна быть выполнена команда return, которая берет из стека адрес возврата и записывает его в PC.

Поскольку call - обычная команда процессора, то вызвать подпрограмму можно из любой другой подпрограммы. Особый интерес - это вызов подпрограммы в теле самой подпрограммы. Так называемая рекурсивная организация подпрограмм. Рассмотрим важный вопрос для использования подпрограмм - это обмен информацией между программой и подпрограммой. Обмен информацией предполагает:

  1. передачу информации в подпрограмму;
  2. передачу информации из подпрограммы в вызывающую программу.

Рассмотрим способы передачи информации в подпрограмму.

  1. Использование регистров - наиболее быстрый способ организации передачи информации в подпрограмму. В этом случае, при написании подпрограммы учитывается, что необходимая информация перед вызовом подпрограммы заносится в известные заранее регистры. Например, если подпрограмма выводит символ на экран с повторением, то заранее оговаривают, что регистр r1 содержит код символа, а регистр r2 - количество повторений. В этом случае регистры r1 и r2, говорят, содержат значения параметров.
  2. Использование общей памяти. В данном случае выделяется определенный размер памяти, доступ к которому имеет как вызываемая программа, так и подпрограмма. Обычно это некоторая статическая память, управление которой не зависит от подпрограммы. В данном случае, вызывающая программа заносит значения в статическую память, а подпрограмма берет эти значения и производит вычисления.
  3. Использование стековой памяти. Этот механизм в настоящее время является общепринятым и заключается в следующем: перед вызовом подпрограммы значения параметров заносятся в системный стек, в подпрограмме значения параметров выбираются относительно вершины стека, поэтому один и тот же код может работать с различными участками памяти, тем самым возможен рекурсивный вызов подпрограмм.

Рассмотрим варианты передачи информации из подпрограммы в вызывающую программу.

  1. Использование регистров. В этом случае заранее известно, что такой-то регистр после выполнения подпрограммы содержит искомое значение.
  2. Использование общей памяти, в данном случае подпрограмма заносит вычисляемые значения в общую память.
  3. Использование стека. Существует механизм передачи информации через системный стек.

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

2.12.1. Функции в Си

int //описание возвращаемого значения
      func( //имя функции
            int i, //описание первого параметра
            int j //описание второго параметра
            ) //
{ //тело функции
      return i%j; // оператор возврата
} //конец тела функции.

Для функции, которая не возвращает значения, необходимо записать описатель void. Например:

void //функция ничего не возвращает
      Show( //имя функции
            int i, //описание первого параметра
            int j //описание второго параметра
            ) //
{ //тело функции
      printf(“%d %d\n”,i,j); // вывод значения i, j
} //конец тела функции, оператор return может отсутствовать .

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

func(10,5);
Show(i+1,k++);

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

x=func(2,3)+20;

Список параметров в функции может отсутствовать.

Рассмотрим вопросы обмена информацией между программной и подпрограммой. Возможны следующие варианты:

  1. использование параметров;
  2. использование общей памяти.

Рассмотрим передачу информации через параметры. Заранее известно, что значения параметров в функцию передаются по значению. Это означает, что значение переменной или выражения переписывается в локальную память функции (причем локальная память выделяется из системного стека). Поэтому значения переменных, подставленные вместо параметров, не изменяются. Например:

void f(int i) { i=4; }
...
int j=5;
f(j);

В этом примере значение j не изменится при вызове функции f. Для того чтобы значение переменной изменилось, необходимо передать адрес переменной, это можно сделать, используя указатели. Например:

void v(int * p) { *p=4; }
...
int k=5;
v(&k);

В этом примере значение переменной k изменится, поскольку было передано не значение переменной, а ее адрес. Часто такой подход используют при работе со структурами.

typedef struct A { type1 a1; type2 a2; ... } ASTRUCT;
ASTRUCT x, y;
FuncA(ASTRUCT * s) { s->a1=0; s->a2=0; ... }
...
FuncA(&x);
FuncA(&y);
...

Рассмотрим вопросы организации передачи информации через внешние переменные. Как это организовать?

... prog1.c //первый модуль
int x; //это статическая переменная, объявленная вне функции
void func1() { x=100; }
void func2(int i) { x=i; }
...
... prog1.c //второй модуль
extern int x;
void Show() { printf(“%d\n”,x); }
void func3() { return x; }

Файлы prog1.c и prog2.c, которые имеют внешнюю память x и функции, которые читают и пишут значения в эту внешнюю память. В первом файле объявляется статическая переменная x, во втором объявляется, что переменная x описывается как переменная, которая объявлена вне данного файла.

Есть еще одна важная деталь: порядок занесения значений параметров в стек при вызове функции. Существует несколько вариантов:

  1. занесение с конца списка параметров (cdecl);
  2. занесение с начала списка параметров (pascal).

Функция может иметь переменное число параметров. Для записи функции с переменным числом параметров необходимо использовать лексему … (три точки). Например:

void printf(char * format,...);

Три точки, стоящие вместо описания параметра, указывают на то, что функция имеет переменное число параметров. Например:

int sum_vect( int n,...){
      int *vec=&n+1;
      int sum=0;
      int i;
      for(i=0; i<n; i++) sum+=vec[i];
      return sum;
}

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

int s=sum_vect(5,20,10,15,1,2); //s=20+10+15+1+2
int count=sum_vect(3,i,j,k); //count=i+j+k

В системном каталоге INCLUDE имеется заголовочный файл stdarg.h, который содержит описания макросов для выделения аргумента заданного типа из списка аргументов:

void va_start(va_list ap, lastfix);
type va_arg(va_list ap, type);
void va_end(va_list ap);
где:
va_list - это тип указателя на список аргументов;
ap - указатель на список аргуметов;
lastfix - это имя параметра, стоящего перед ... (тремя точками);
va_start - инициализация указателя на список аргументов;
type - тип аргумента;
va_arg - выделение значения очередного значения аргумента, при этом указатель ap автоматически перемещается на следующий;
va_end - макрос завершения.
#include <stdio.h>
#include <stdarg.h>
//вычислить сумму последовательности целых чисел, заканчивающуюся на 0
void sum(char *msg, ...)
{
      int total = 0;
      va_list ap;
      int arg;
      va_start(ap, msg);
      while ((arg = va_arg(ap,int)) != 0) {
            total += arg;
      }
      printf(msg, total);
      va_end(ap);
}

int main(void) {
      sum("The total of 1+2+3+4 is %d\n", 1,2,3,4,0);
      return 0;
}

//
найти суммы разнотипных чисел, используя строку формата
#include <stdio.h>
#include <stdarg.h>
double sum2(char *format, ...)
{
      double total = 0;
      va_list ap;
      int arg;
      va_start(ap, format);
      while(*format){
            if(*format==’i’) total+=va_arg(ap,int);
            else
                  if(*format==’d’) total+=va_arg(ap,double);
            format++;
      }
      va_end(ap);
      return total;
}

int main(void) {
      printf("%f",sum2("iddi", 1,2.77,3.5,4));
      return 0;
}
.: [предыдущая | оглавление | следующая] :.