5. Структура программы

В отличие от Паскаля в Си нет разделения на процедуры и функции. Имеются только функции, а если они ничего не возвращают, то есть являются аналогом процедур в Паскале, то считается (и явно указывается), что они возвращают значение "пустого" типа void. Приведем сначала очень простые примеры, поскольку сложными нам еще придется заниматься в дальнейшем.

int nod(int a, int b) 
{ if ( !b) return a;

else return nod(b, a%b) ;

}

Обратите внимание на оператор return. Именно с его помощью функция возвращает значение (и в этом месте заканчивает работу). Подробнее об этом — чуть ниже, в разделе 5.3. Еще один пример:

void day(int n) 
{ switch (n)

{    case 1: printf("\nПн");break;

case 2: printf("\nВт"};break;

case 3: printf("\nCp");break;

  case 4: printf("\nЧт");break;
  case 5: printf("
\nПт");break;
  case 6: printf("\nCб");break;
 
case 7: printf("\nBc");break;
  default: printf
("\nФункция day: ошибка в параметре ");

} .    
   
}

Упрощенно заголовок функции может быть описан так:

<тип возвращаемого значения> <имя функции>(<список формальных параметров>) Повторим, это упрощенная форма записи заголовка функции!

5.2. Формальные параметры функций

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

Пример: в объявлении функции int sum(int a, int b) а и b — формальные параметры, при вызове — sum(x,y) х и у — фактические. Итак, в этом разделе речь идет именно о формальных параметрах.

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

Пример: int f(int n, int m, float r, char с)

При этом надо иметь в виду следующее:

Необходимо явно указывать тип каждого параметра (даже если типы подряд идущих параметров совпадают).

Пример: void f(int n,int m) — верно, void f(int n,m) — неверно.

Замечание. Знатоки Си в этом месте непременно воскликнут: "Да что вы тут нам лапшу на уши вешаете! Все знают, что приведенные примеры на самом деле эквивалентны и второй также является верным!" Ответ им будет таким: да, приведенные примеры действительно эквивалентны, но лишь потому, что Си по умолчанию считает, что если тип параметра не задан, то тип этот — int. Если написать что-нибудь вроде void f (float r,p) , то понято это будет как void f (float f, int p) . А поэтому надо всегда явно указывать тип параметров, а не пользоваться мало кому известными "соглашениями".

Все параметры, кроме массивов, передаются "по значению". Для тех, кто программирует на Паскале, это переводится как "без var". Массивы передаются по ссылке,

Замечание. Конечно, имеется возможность передавать параметры как угодно, в том числе и по ссылке. И мы еще вернемся к этому вопросу, когда займемся указателями и массивами.

Примеры:

void f(int a) 
{
а++;}

Следующий фрагмент: t=l; f (t) ; оставит значение переменной t без изменения (как была она равна 1, так и останется).

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

procedure f(var a) ;

begin inc (a); end;

Приведем теперь пример функции, в которую передается массив:

void init_10(int a[10])
{ int i;
  for (i=0;i<10;i++) a[i]=i;

}

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

Замечание. В Паскале для изменения значений элементов аналогичного массива в процедуре мы написали бы:

type arrayl0=array[0..9] of integer;
procedure init_10(var a:arrayl0);
  var i: integer;
    begin
       for i:=0 to 9 do a[i]=i 
  end;

5.3. Возвращаемые значения функций

Формально верная фраза такая: функция может возвращать значения любого типа, кроме массива или функции (но она может возвращать указатель на массив или функцию). Вам понятно? Детям — точно нет (на этом этапе изучения языка). Поэтому лучше сказать так: давайте пока считать, что функции могут возвращать значения только простых типов.

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

На Паскале:

function simple(n: integer):booleah; {является ли число простым}
 var i: integer; sqrtn:real;

begin

simple:=true;

i:=2;sqrtn:=sqrt (n) ;

while (i<=sqrtn) and (n mod i <> 0) inc(i);

if (n mod i=0) then simple:=false
end;

На Си:

int simple(int n) /*является ли число простым*/
 { int i=2; float sqrtn=sqrt(n);
   while (<i<=sqrtn) && (n%i)) i++;

return (n%i) ;

}

Обратите внимание, что в примере на Паскале после первого присваивания simple :=true ничего не происходит, а вот на Си оператор return "выбросил" бы нас из функции. Переменная sqrtn в обеих программах введена для некоторой оптимизации цикла (иначе получалось бы уж совсем "неприлично"). Ну и, наконец, для корректной работы этой функции в Си требуется подключить math.h (включить в программу строчку #include <math.h>). Почему — подробнее рассказано в разделе 5.5.

5.4. Самая равная среди равных (функция main)

В простейшем случае программа на Си состоит из одного файла, который, в свою очередь, содержит единственную функцию. И имя ей — main. Можно считать (собственно, так оно и есть), что функция main является аналогом тела программы в Паскале. В начале выполнения программы на Си управление передается на первый исполняемый оператор функции main. Функция main, конечно, не совсем обычная, и у нее имеется ряд особенностей. Во-первых, она так же, как и все другие функции, может возвращать значения, но лишь определенных типов. Во-вторых, и этому позднее будет посвящен соответствующий раздел, в нее тоже можно передавать параметры (это те самые параметры, которые указываются в командной строке вызова ЕХЕ-файла).

     5.5. Прототипирование (предварительное объявление) функций.
       Директива препроцессора #include

Рассмотрим следующий пример:

void main(void)
{ printf("Здравствуй, Мир!");}

То, как будет работать эта программа (и пройдет ли она вообще компиляцию!), зависит от компилятора языка Си, с которым вы работаете. К сожалению, в большинстве случаев программа эта компиляцию пройдет и даже работать будет так, как и предполагается, но на самом деле программа эта неправильная! В правильном варианте (мы приводили его в качестве примера первой программы) обязательно должна присутствовать строчка #include <stdio.h>. Если вы посмотрите на содержимое файла stdio.h (а это вполне можно сделать, это обычный текстовый файл, обычно он располагается в каталоге, указанном в Include Directories), то увидите в нем большое количество строк, являющихся объявлениями функций (объявление — это фактически некоторый шаблон заголовка). Мы, например, нашли объявление функции printf в виде

int _Cdecl printf (const char *_format, ...);

Пожалуйста, не пугайтесь и не пытайтесь сейчас понять смысл этой записи, важно не это. Важно то, что сама по себе функция printf не является принадлежностью самого языка Си! В самом языке функций вообще нет! И, для того чтобы компилятор правильно понял, что такое printf, он должен познакомиться с ее описанием (т.е. с тем, какие параметры она может иметь и какое значение должна возвращать). Директива # include включает в текст вашей программы описание множества функций, среди которых есть и приведенное описание printf. И, увидев это описание, он правильно компилирует программу. (Кстати, вполне можно "вынуть" описание функции printf из файла stdio.h и вставить его непосредственно в текст программы. Все будет работать.)

int cdecl printf(const char *__format, ...);

void main(void)
{ printf("Здравствуй, Мир!");}

(Если вы заметили, мы внесли еще одно "косметическое" изменение, но к обсуждаемому нами вопросу оно отношения не имеет.)

Итак, прототипирование — это предварительное объявление функций (описание того, как выглядят их заголовки). Большинство файлов, подключаемых директивой #include, содержат именно прототипы функций.

Приведем еще один пример,

#include <stdio.h>
/* Прототипы функций DayInRussian и DayInEnglish */
int DayInRussian(int);
int DayInEnglish(int);

void main(void)
{ int 1,n;
   printf ( "Введите язык (1 — русский; 0 — английский");scanf("%d",&l);
   printf("Введите номер для недели:");scanf("%d",&n);
   if (!((1)?DayInRussian(n):DayInEnglish(n)))
      printf("Произошла ошибка при вызове функции печати дня недели");

}

int DayInRussian(int n)
  { switch (n)
    
{case 1 printf("\nПн");break;
      
case 2 printf ( "\nВт");break;

       case 3 printf ( "\nCp" );break;
      
case 4 printf("\nЧт");break;
      
case 5 printf("\nПт");break;
      
case 6 printf("\nCб");break;
     
case 7 printf("\nBc");break;

           default: printf("\nФункция DayInRussian: ошибка в параметре ");
        }
    }

int DayInEnglish(int n) 
 
{ switch (n)

{ case 1 printf("\nMo");break;
   case 2 printf("\nTu");break;
  
case 3 printf("\nWe");break;
  
case 4 printf("\nTh"); break;
  
case 5 printf("\nFr"); break;
  
case 6 print'f ("\nSt"); break;
  
case 7 printf ("\nSu"); break;

default: printf("\nФункция DayInEnglish: ошибка в параметре ");
}
}

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

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

#include <stdio.h> 
  void main(void)
   { int a,b;
     
printf("a/b=%5.2f",division(a,b));
    }
   float division(int a,int b)
   { return (float)a/b;}

При компиляции этой программы в зависимости от настроек среды используемого компилятора Си вы получите сообщение об ошибке terror) или на худой конец предупреждение {warning). Так, в Турбо Си будет выдано сообщение об ошибке Type mistmach in redeclaration of division (неверный тип при переопределении функции division). При использовании компилятора Си++ сообщение будет гораздо более информативным, например, таким: Function division should have a prototype (функция division должна иметь прототип). Суть такова (собственно, надо объяснить лишь первый случай, второй в комментариях не нуждается). Если компилятор встречает функцию, прототипа которой он не видел, то делает некоторое предположение о том, каков он, этот прототип, и дальше, даже увидев само описание функции, своего мнения не меняет Одно из предположений компилятора такое: все функции возвращают значения типа int. Именно так он и "подумал" про функцию division, увидев ее в первый раз (то, что внутри строки формата printf указан вещественный тип, никакой роли не играет, эту-то строчку компилятор не разбирает). Увидев же позднее, что на самом деле division возвращает значение не целого, а вещественного типа, компилятор "ужасно удивился" и немедленно выдал сообщение об ошибке. Если бы мы в начале программы написали прототип функции division, все было бы прекрасно! Вот так:

#include <stdio.h>
 float division
(int,int);

 void main(void)
{ int a,b;
 
printf("a/b=%5.2f",division(a,b)) ;

 }
float division(int a,int b)
 
{ return (float)a/b;}

Далее, необходимо сказать о следующем (мы уже обращали внимание на этот факт, но повторимся): мы сейчас пока обсуждаем примеры, в которых программа на Си состоит из одного файла (написанного нами, а то особо "продвинутые" дети обычно в этом месте начинают вспоминать про файлы стандартных библиотек и пр.) В таких "однофайловых" программах - явное прототипирование всех функций прямо в начале самого файла уместно и удобно. А вот выносить такие описания в отдельный заголовочный файл (h-файл; кстати, h — это от header) неудобно. Приведенный выше пример, конечно, можно было бы переписать так: сделать два файла например, dayfunc.h и day.c.

В первый поместить только прототипы:

int DayInRussian(int);
int DayInEnglish(int);

А второй файл сделать таким:

#include <stdio.h>
#include "dayfunc.h"
 int DayInEnglish(int)
;
 void main(void)
    { <...> }
}
int DayInRussian(int n)
{ <...> }
int DayInEnglish(int n)
{ <...> }

(Мы, конечно, не стали повторять длинный текст самих функций и соответствующие фрагменты заключили в угловые скобки. Они не имеют отношения к Си, работать в таком виде программа не будет!  :-) )

Итак, мы вынесли описания в отдельный файл. И, конечно, читать программу стало менее удобно! Ведь теперь, чтобы увидеть сами прототипы, надо будет "залезать" в файл dayfunc.h и смотреть, что в нем написано.

Что, снова кто-то тянет руку?! Безусловно! Или самый "продвинутый" (хочет показать себя), или самый внимательный. Вопрос такой: почему имя stdio.h заключено в угловые скобки, a dayfunc.h — в двойные кавычки? Короткий ответ такой: все, что заключено в угловые скобки, компилятор ищет в каталоге (ах), который (е) в настройках среды заданы как каталоги Inclide Directories. Так как свои собственные файлы помещать в них не следует (представьте, что будет, если каждый пользователь на "разделяемом" компьютере начнет помещать в эти каталоги свои заголовочные файлы!), то свои файлы мы обычно оставляем в одном каталоге с самой программой (или в другом, специальном каталоге, главное, не в том, который задан в настройках среды). В данном случае считается, что файл dayfunc.h находится в текущем каталоге.

5.6. С функциями более или менее понятно.
      Ну а как же со структурой программы?

А со структурой программы напомним, что пока мы говорим об "однофайловых" программах, все просто: программа состоит из функций, среди которых обязательно должна быть функция main. Все используемые в программе функции должны иметь прототипы. Прототипы "стандартных" функций содержатся, как правило, в стандартных h-файлах, собственные функции надо прототипировать самостоятельно. Таким образом, на "внешнем уровне" в программе могут содержаться прототипы функции, описания функций, директивы препроцессора (пока мы знакомы только с include, с остальными разберемся позднее), описания глобальных переменных и описания типов. Мы не случайно выделили последние слова: об этом мы вроде бы еще не говорили. С описаниями типов мы познакомимся позднее, а вот с описаниями глобальных переменных разберемся прямо сейчас.

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

void main(void)

{ <...> }
 int a;

void inca(void)
{ a++;}

void unvisibleal(int a)

{ <...> }
void deca(void)

{ a--;}

void unvisibiea2(void)
{ float a;

<...>

}

Мы специально совместили в одном примере два. Во-первых, здесь объявляется глобальная переменная а, которая "видна" ниже точки ее описания (т.е. в функциях inca, deca,... и все). Приведенная нами формулировка правила видимости глобальных переменных очень проста, но надо иметь в виду и исключение: переменная видна, если она не "перекрыта", т.е. если в данном блоке не описана переменная с тем же именем (тип не важен, мы специально, чтобы продемонстрировать это, описали в функции unvisibiea2 вещественную переменную а).

Все переменные, которые не являются глобальными, локальные. Область видимости локальной переменной — блок, в котором она описана... правда, опять же, если она не "перекрывается" в другом блоке, вложенном в данный. Дело в том, что локальные переменные можно описывать не только в начале функции, но и в начале любого блока. Приведем пример:

void main(void)
{ <...> }
 int a;

void inca(void)
 
(   a++;

 <...>

{float a <...>}

      <...>
}

void unvisibleal(int a)
 
{ <...>

{float a <...>}

<...>
}

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

TopList