Pers.narod.ru. Обучение. Лекции по Си. Глава 5

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

 

Программа на Си включает следующие элементы:

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

·  объявления - описания переменных, функций, структур, классов и типов данных;

·  определения - тела выполняемых функций проекта.

 

5.1 Объявление переменной - задает имя и атрибуты переменной, приводит к выделению для нее памяти, а также может явно или неявно задавать начальное значение:

int x,y; float r=0;

Все переменные в языке Си должны быть явно объявлены перед использованием.

Объявления имеют следующий общий синтаксис:

<класс_памяти> <знаковость> <длина> тип список_переменных;

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

Класс памяти может принимать следующие значения:

·       отсутствует или auto - переменная определена в том блоке { }, в котором описана, и вложенных в него блоках. Определенная вне всех блоков переменная видима до конца файла. Принят по умолчанию в объявлении переменной на внутреннем уровне. Переменные класса auto автоматически не инициализируются. Память отводится в стеке. Как правило, ключевое слово auto опускается.

·       static - переменная существует глобально независимо от того, на каком уровне блоков определена. Область действия - до конца файла, в котором она определена. По умолчанию имеет значение 0.

·       extern - переменная или функция определена в другом файле, объявление представляет собой ссылку, действующую в рамках одного проекта;

·       register - (только для типов данных char и int) -переменная при возможности хранится в регистре процессора.

Спецификации классов памяти auto и register не допускаются к явному указанию на внешнем уровне.

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

·              signed (по умолчанию) - переменная со знаком;

·              unsigned - переменная без знака.

Длина определена для типов int, float, double:

·              short - короткий вариант типа;

·              отсутствует - вариант типа по умолчанию;

·              long - длинный вариант типа.

Действие этих модификаторов зависит от компилятора и аппаратной платформы. Например, на IBMPC совместимых компьютерах типы short int и int совпадают и занимают по 2 байта оперативной памяти, long int требует выделения 4 байт.

Базовыми типами данных являются:

·              char - символьный;

·              int - целочисленный;

·              float - вещественный (плавающий) одинарной точности;

·              double - вещественный (плавающий) двойной точности;

·              void - "пустой" тип, имеет специальное назначение. Указание void в объявлении функции означает, что она не возвращает значений, а в списке аргументов объявления функции - что функция не принимает аргументов. Нельзя создавать переменные типа void, но можно указатели.

Переменные любых типов могут быть объявлены в любом месте проекта.

 

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

ТипФункции имя (тип1 параметр1, :, типN параметрN);

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

float f1(double t, double v) {

 return t+v;

 //Для этой функции прототип не указан

}

extern char f0 ();

 //f0 определена в другом файле проекта

f2(); //Прототип нужен т.к. тело функции

      //определено ниже ее вызова

void main () {

 f1 (1.,2.); f2 ();

}

int f2(void){//в прототипе не указан тип

             //функции, предполагается int

 return 0;

}

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

·       модификатор extern;

·       включение заголовочного файла внешнего модуля директивой препроцессора #include <ИмяФайла.h>

В примере ниже функция f4() определена во внешнем файле и должна быть доступна к моменту сборки приложения:

int f1 (int n) {

 extern int f4(int);

 return f4(n);

}

Обратите внимание, что если у функции опущен тип, то предполагается int:

main () {

 return 0;

}

но

void main () { : }

В следующем примере в проект включены 2 файла, file1.cpp и file2.cpp. При этом прототип функции f4(), определенной  в файле file2.cpp, включен в заголовочный файл file2.h и подключен к файлу file1.cpp с помощью директивы #include:

----------------------- листинг file1.cpp

#include <stdio.h>

#include <file2.h>

 

int f3(int n) {

 return f4(n);

}

 

void main () {

 printf ("\n%d",f3(3));

}

----------------------- листинг file2.cpp

int f4 (int k) {

 return ++k;

}

----------------------- листинг file2.h

int f4 (int);

 

5.3. Объявление типа позволяет создать собственный тип данных. Оно состоит в присвоении имени некоторому базовому или составному типу языка Си. Для типа понятия объявления и определения совпадают.

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

struct list {

 char name [20];

 long int phone;

}

struct list mylist [20];

Здесь объявлен структурный тип list, а затем описан массив структур типа list с именем mylist, состоящий из 20 элементов.

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

typedef float real;

typedef long int integer;

typedef struct {

 float x,y;

} Point;

Point r;

Обратите внимание, что в отличие от директивы #define, имеющей синтаксис

#define новое_слово старое_слово

определение существующего типа через typedef имеет вид

typedef старый_тип новый_тип;

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

#include <stdio.h>

typedef unsigned int word;

void f() {

 typedef unsigned long longint;

 #define byte unsigned char

 byte b;

}

 

void main () {

 word w=65535;

 byte b=65; //ошибки нет - #define

  //действует и вне блока, в котором указан

 longint l; //ошибка - тип longint

  //определен  только внутри функции f()

 printf ("\n%u,%c",w,b);

}

 

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

int f (int a, int b) {

 return (a+b)/2;

}

В определении функции допускается указание спецификации класса памяти static или extern, а также модификаторов типа функций pascal, cdecl, interrupt, near, far и huge (см. п. 11).

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

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

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

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

Формальные параметры могут иметь базовый тип, либо быть структурой, объединением, указателем или массивом. Указание первой (или единственной) размерности для массива не обязательно. Массив воспринимается как указатель на тип элементов массива.

Параметры могут иметь класс памяти auto (по умолчанию) или register. По умолчанию формальный параметр имеет тип int.

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

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

После преобразования все порядковые формальные параметры имеют тип int, а вещественные - тип double.

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

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

Возможность доступа из функции не к копиям значений, а к самим переменным обеспечивают указатели (см. п. 7) и параметры‑ссылки (см. п. 5.5).

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

Программа на Си должна содержать хотя бы одно определение функции - функции с именем main. Функция main является единственной точкой входа в программу. На весь проект может быть только одна функция с именем main.

Текст программы может быть разделен на несколько исходных файлов. При компиляции программы каждый из исходных файлов должен быть скомпилирован отдельно, а затем связан с другими файлами компоновщиком. Исходные файлы можно объединять в один файл, компилируемый как единое целое, посредством директивы препроцессора #include. В тексте примера ниже файл file3.cpp включает file2.cpp, используя из него функцию f4():

#include <stdio.h>

#include "file2.cpp"

void main () {

 printf ("\n%d",f4(0));

}

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

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

#include "ИмяФайла.h"

предполагающей поиск заголовочного файла в текущей папке проекта.

Директива

#include <ИмяФайла.h>

предназначена для подключения стандартных заголовочных файлов и ищет их в папках, указанных в настройке Include directories компилятора.

Функция main также может иметь формальные параметры. Значения формальных параметров main могут быть получены извне - из командной строки при вызове программы и из таблицы контекста операционной системы. Таблица контекста заполняется системными командами SET и PATH:

int main (int argc, char *argv[], char *envp []) { : }

Доступ к первому аргументу, переданному программе, можно осуществить с помощью выражения argv[1], к последнему аргументу - argv[argc-1]. Аргумент argv[0] содержит строку вызова самого приложения.

Параметр envp представляет собой указатель на массив строк, определяющих системное окружение, т.е. среду выполнения программы. Стандартные библиотечные функции getenv и putenv (библиотека stdlib.h) позволяют организовать удобный доступ к таблице окружения.

Существует еще один способ передачи аргументов функции main - при запуске программы как независимого подпроцесса из другой программы, также написанной на Си (функции семейства ехес и spawn, бибилиотека process.h).

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

#include <stdio.h>

int long power(int,int); //прототип функции

//необходим, т.к. тело функции ниже вызова

void printLong (char *s, long int l) {

//без прототипа

printf ("%s%ld",s,l);

}

void main() {

for (int i = 0; i <= 30; i++) {

  int long l1=power(2,i);

  printf("%d",i);

  printLong (" ",l1);

  printf("\n");

}

}

int long power(int x, int n) {

int i; long int p;

for (i=1,p=1L; i <= n; ++i) p *= x;

return (p);

}

Функции на Си могут быть рекурсивными:

int long power(int x, int n) {

return (n>1 ?

 (long int)x*power(x,n-1) : x);

}

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

 

5.5 Передача параметров по значению и по ссылке. По умолчанию аргументы функций передаются по значению. При передаче по ссылке перед аргументом в прототипе и в заголовке указывается операция "адрес" (&):

void swap (int &,int &);

//...

void swap (int &a, int &b) {

 int c=a; a=b; b=c;

}

//...

swap (x1,x2);

Здесь функция swap поменяла местами фактические значения аргументов x1 и x2.

Так как компилятор на первом этапе формирует внешние имена функций, в одном проекте возможны функции, имеющее одинаковые имена, но отличающиеся списком параметров:

int f(int,int);

float f(float,float);

но не

float f(int,int);

 

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

 

Табл. 5.1. Время жизни и область действия

Объект

Время жизни

Область действия

функция

глобально для всех функций (все время выполнения программы)

ниже по тексту от объявления или определения

переменная, определенная на внешнем уровне (вне всех блоков)

глобально

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

переменная, определенная внутри блока

локально, пока выполняется этот и вложенные блоки (если не указан класс памяти static)

в этом и вложенных блоках, может быть переопределена во вложенных блоках

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

#include <stdio.h>

static int i=12;

  //глобальная статическая переменная

void f() {

 printf ("\n%d",i); //i=12

}

void main () {

 int i=5;

 printf ("\n%d\n",i); //i=5

 {//без этого блока компиляторы выведут

  //ошибку "множественная декларация

  //переменной", т.к. открывающая часть

  //цикла for выполняется до его тела

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

   printf ("%d ",i); //i=0,1,:,9

 }

 printf ("\n%d",i); //i=5

 f();

}

 

 

Рейтинг@Mail.ru
вверх гостевая; E-mail
Hosted by uCoz